Cordova 是一个轻量级的移动端混合开发框架,在公司项目中担任桥接器的作用,为前端提供一些原生底层能力,在“插件”中以异步的形式将执行结果返回给前端。考虑到前端的页面可能在本地、远程、跨各种域等等问题,一般的做法是把
www/cordova.js
www/cordova_plugins.js…
www/plugins/*.js
…
这一连串的 js 放在原生的工程中,由原生动态注入到页面中。功能方面没什么问题,但是插件的调用性能却一直被前端的同事诟病:目前只能等到 cordova 发出 “deviceready” 通知以后,才可以正常调用插件功能。这导致页面想使用插件的信息预加载某些内容时(例如提前做认证等等)没办法正常访问到插件,会提示 Undefine…肉眼可见的效果就是页面前期会白页一两秒。
这两天抽空做了一下 Android 端的尝试,想要的效果就是尽量提前完成 cordova 相关 js 的注入。Demo 中达到了我预期的效果,高兴的一晚上没睡着觉,但是,Android 和前端我只是略懂皮毛,不确定测试的方案有没有问题、应对复杂的场景是否可行,所以写在这里做一个讨论,希望有共同需求的朋友一起探讨一下。
Android 端所作的尝试
Demo 在此。
尝试前
搜了一下 Android 端注入 js 的方式,大部分都是采用下面的方案(目测此方案是对标自 GitHub 上的一个 InjectCordova 插件):
1、监听 WebView 的 onPageFinished
事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class MainActivity extends CordovaActivity { ... @Override public Object onMessage(String id, Object data) { if (id.equals("onPageFinished")) { injectCordova(mEngine); }
return null; } ... }
|
2、拼接一个 <script>
标签 - 通过 document.createElement()
方法创建一个 <script>
标签,标签内容是所有要注入的 js 源码拼接而成的字符串,再使用 appendChild()
方法把 <script>
拼接在 <head>
标签的后面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class MainActivity extends CordovaActivity { ... private final ArrayList<String> preInjectionFileNames = new ArrayList<String>();
private void injectCordova(CordovaWebViewEngine engine) { List<String> jsPaths = new ArrayList<String>(); for (String path: preInjectionFileNames) { jsPaths.add(path); }
jsPaths.add("www/cordova.js"); jsPaths.addAll(jsPathsToInject(getResources().getAssets(), "www/plugins")); jsPaths.add("www/cordova_plugins.js");
StringBuilder jsToInject = new StringBuilder(); for (String path: jsPaths) { jsToInject.append(readFile(getResources().getAssets(), path)); } String jsUrl = "javascript:var script = document.createElement('script');"; jsUrl += "script.src=\"data:text/javascript;charset=utf-8;base64,";
jsUrl += Base64.encodeToString(jsToInject.toString().getBytes(), Base64.NO_WRAP); jsUrl += "\";";
jsUrl += "document.getElementsByTagName('head')[0].appendChild(script);";
engine.loadUrl(jsUrl, false); }
private String readFile(AssetManager assets, String filePath) { ... ... }
private List<String> jsPathsToInject(AssetManager assets, String path) { ... ... } ... }
|
3、使用 WebView.loadUrl() 方法注入拼接后的 js。
上面的方案涉及到了获取、拼接标签元素,如果想在 WebView 加载完成之前提前注入似乎是不太现实的,因为时机太早的话,是拿不到标签的。
尝试后
读了一下 Android WebView 的 API 文档,貌似能够实现 js 注入的方法只有两个:loadUrl(String url)
和 evaluateJavascript(String, ValueCallback)
。考虑到 cordova 相关 js 中一般不会针对前端标签做什么操作,是在页面中声明一个名为 cordova 的“模块”,这个模块又定义了若干个插件模块和方法,即使是自己写的插件,目的也是为了能让前端使用原生能力(相机、相册等等),那是否不需要等到 WebView 把 DOM 的内容都渲染完成才可以开始注入?于是做了如下的尝试:
与之前的方案相似,依旧是循环读取各个 js 中的源码,但不再拼接 <script>
标签,而是使用 evaluateJavascript(String, ValueCallback)
将每段 js 注入到页面中,并且注入时机提前到 onPageStarted
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class MainActivity extends CordovaActivity { ... @Override public Object onMessage(String id, Object data) { if (id.equals("onPageStarted")) { evaluateCordovaJavascript(mWebView); }
return null; } public void evaluateCordovaJavascript(WebView webView) { List<String> jsPaths = new ArrayList<String>(); for (String path: preInjectionFileNames) { jsPaths.add(path); }
jsPaths.add("www/cordova.js"); jsPaths.addAll(jsPathsToInject(getResources().getAssets(), "www/plugins")); jsPaths.add("www/cordova_plugins.js");
for (String path: jsPaths) { String js = readFile(getResources().getAssets(), path); webView.evaluateJavascript(js, null); } } ... }
|
接下来我们做验证,在 index.js 中添加如下测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| document.addEventListener('deviceready', onDeviceReady, false);
function onDeviceReady() { console.log('YY: Running cordova - ' + cordova.platformId + '@' + cordova.version); document.getElementById('deviceready').classList.add('ready'); testMyPlugin('Device Ready: '); }
( function () { console.log('YY: Just do IT!'); testMyPlugin('Device Start: '); } )();
function testMyPlugin(tag) { MPKeyChain.getValueForKey( 'username', function success(result) { console.log('YY: '+ tag + result); }, function error(error) { console.log('YY: '+ tag + "获取用户信息失败"); } ); MPKeyChain.getServerUrl( function success(result) { console.log('YY: '+ tag + result); }, function error(error) { console.log('YY: '+ tag + "获取服务器信息失败"); } ); }
|
根据 log 输出的结果,确实像我所预期的效果,是可以提前注入的:
1 2 3 4 5 6
| "YY: Just do IT!", source: https://localhost/js/index.js (39) "YY: Device Start: zhengyt", source: https://localhost/js/index.js (49) "YY: Device Start: https://tommygirl.cn", source: https://localhost/js/index.js (59) "YY: Running cordova - android@10.1.1", source: https://localhost/js/index.js (70) "YY: Device Ready: zhengyt", source: https://localhost/js/index.js (49) "YY: Device Ready: https://tommygirl.cn", source: https://localhost/js/index.js (59)
|
进行到这一步的时候,有两点我不太确定:一个是 Android WebView 提供的 onPageStarted、onPageFinished 事件与 DOM 的各个生命周期 DOMContentLoaded、Load 事件之间的关系,二是在 DOM 的生命周期完成之前注入 js 是否存在什么我不懂的问题。
在 index.js 中增加了对 DOM 生命周期的监听,打印了一下这些事件的顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 2022-04-15 11:25:26.259 I/System.out: YY: onProgressChanged - 10 2022-04-15 11:25:26.321 I/System.out: YY: onMessage - onPageStarted 2022-04-15 11:25:26.355 I/System.out: YY: onMessage - spinner 2022-04-15 11:25:26.361 I/chromium: [INFO:CONSOLE(39)] "YY: Just do IT!", source: https://localhost/js/index.js (39) 2022-04-15 11:25:26.363 I/chromium: [INFO:CONSOLE(49)] "YY: Device Start: zhengyt", source: https://localhost/js/index.js (49) 2022-04-15 11:25:26.363 I/chromium: [INFO:CONSOLE(59)] "YY: Device Start: https://tommygirl.cn", source: https://localhost/js/index.js (59) 2022-04-15 11:25:26.364 I/chromium: [INFO:CONSOLE(33)] "YY: Ready state change!", source: https://localhost/js/index.js (33) 2022-04-15 11:25:26.364 I/chromium: [INFO:CONSOLE(25)] "YY: DOMContentLoaded", source: https://localhost/js/index.js (25) 2022-04-15 11:25:26.364 I/System.out: YY: onProgressChanged - 80 2022-04-15 11:25:26.365 I/chromium: [INFO:CONSOLE(33)] "YY: Ready state change!", source: https://localhost/js/index.js (33) 2022-04-15 11:25:26.365 I/chromium: [INFO:CONSOLE(29)] "YY: Load", source: https://localhost/js/index.js (29) 2022-04-15 11:25:26.365 I/System.out: YY: onProgressChanged - 100 2022-04-15 11:25:26.365 I/System.out: YY: onProgressChanged - 100 2022-04-15 11:25:26.365 I/chromium: [INFO:CONSOLE(70)] "YY: Running cordova - android@10.1.1", source: https://localhost/js/index.js (70) 2022-04-15 11:25:26.366 I/System.out: YY: onMessage - onPageFinished 2022-04-15 11:25:26.367 I/chromium: [INFO:CONSOLE(49)] "YY: Device Ready: zhengyt", source: https://localhost/js/index.js (49) 2022-04-15 11:25:26.367 I/chromium: [INFO:CONSOLE(59)] "YY: Device Ready: https://tommygirl.cn", source: https://localhost/js/index.js (59)
|
顺序可见:onPageStarted -> IIFE 中可以调用插件 -> DOMContentLoaded -> Load -> onPageFinished -> cordova 通知 deviceready。
那这样是否可以证明提前完成注入是可行的呢?又或者我进入了“越无知越拥有莫名其妙勇气”的误区?🤓哈哈哈,求路过的各位大佬赐教、讨论。
iOS - UIWebView
cordova 4.0 之后,在 UIWebView 中注入 JS 也需要自己手动完成,但 4.0 之前是怎么样的我记不清了…Demo 在此。 借助 InjectCordova 的插件监听 CDVPageDidLoadNotification
通知,在收到通知后使用 UIWebView 的 stringByEvaluatingJavaScriptFromString:
方法把 JS 注入进去。其实完全不用插件,在继承自 CDVViewController
的子类中,做这个监听也是可以的。
UIWebView 中目前不知道怎么可以提前完成注入,貌似 iOS 中 webViewDidStartLoad
和 webViewDidFinishLoad
的概念,同 Android 中的概念不是一回事儿。但…管它呢,升级 WKWebView 吧,美滋滋~~
iOS - WKWebView
如果升级至 WKWebView 的话完全不用纠结上面的内容,之前的文章中我们也提到过了,通过 WKUserScript、WKUserScriptInjectionTimeAtDocumentStart 可以轻松实现提前注入,关于注入的 js 作用域的问题,也欢迎在博客中讨论。在此不再赘述。