本文主要记录 Ionic App 使用 cordova hot code push 实现热更新时遇到问题的解决方法,另外也简单记录下使用方法,方便日后查阅。
Cordova hot code push 插件的原作者已经不维护了,我们可以选择一个可能最好的 fork 来使用。 gitpop2 可以帮助我们选择,我从中选择了当前 star 最多的一个 fork。
Ionic App 使用 cordova hot code push 实现热更新的基本步骤如下:
在 ionic 工程中添加 cordova hot code push plugin
$ ionic cordova plugin add https://github.com/snipking/cordova-hot-code-push.git
安装 Cordova Hot Code Push CLI client
$ npm install -g cordova-hot-code-push-cli
为指定平台编译工程
$ ionic cordova prepare android
执行插件初始化
$ cd /path/to/project/root
$ cordova-hcp init
生成插件配置文件
$ cordova-hcp build
运行到设备上
开发和发布应用新版本的 web
// 1. 开发
// 2. 为指定平台编译工程生成 web
$ ionic build --engine=cordova --platform=android
// 3. 生成新插件配置文件
$ cordova-hcp build
// 4. 部署到服务器
在使用的过程中遇到的第一个问题是更新之后白屏。使用 Chrome 的 remote devices 调试 android webview 找到了问题的原因,ionic 应用中 <base href="/" />
, cordova hot code push 会将 web 代码拷贝到外部存储上,webview 使用形如 file:///data/user/0/com.tenneshop.liveupdatedemo/files/cordova-hot-code-push-plugin/2020.01.07-16.16.39/www/index.html
的路径来加载应用,此时 document.baseURI = /
,加载其他相对路径的 js 文件时,是相对这个路径,例如 <script src="cordova.js"></script>
,就是以 /cordova.js
去加载,于是就会提示找不到文件。从上面的分析我们也知道,解决问题的一个办法是修正 base href 的值,我们可以在 index.html 的 head 元素加入下面的代码:
1
2
3
<script>
document.write('<base href="' + document.location.href + '" />');
</script>
这样我们就修正文件路径的问题,很不巧,虽然文件的路径是对了,但是 ionic 默认不响应 file schema 的请求,我们需要做些工作,先让 WebViewLocalServer.java 支持响应 file schema,将 createHostingDetails 改成如下实现:
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
private void createHostingDetails() {
final String assetPath = this.basePath;
if (assetPath.indexOf('*') != -1) {
throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
}
PathHandler handler = new PathHandler() {
@Override
public InputStream handle(Uri url) {
InputStream stream = null;
String path = url.getPath();
try {
if (url.getScheme().equals("file")) {
stream = protocolHandler.openFile(path);
} else if (path.startsWith(contentStart)) {
stream = protocolHandler.openContentUrl(url);
} else if (path.startsWith(fileStart) || !isAsset) {
if (!path.startsWith(fileStart)) {
path = basePath + url.getPath();
}
stream = protocolHandler.openFile(path);
} else {
stream = protocolHandler.openAsset(assetPath + path);
}
} catch (IOException e) {
Log.e(TAG, "Unable to open asset URL: " + url);
Log.e(TAG, e.getLocalizedMessage());
return null;
}
return stream;
}
};
registerUriForScheme(httpScheme, handler, authority);
registerUriForScheme(httpsScheme, handler, authority);
if (!customScheme.equals(httpScheme) && !customScheme.equals(httpsScheme)) {
registerUriForScheme(customScheme, handler, authority);
}
registerUriForScheme("file", handler, "");
}
然后是 isLocalFile 方法:
1
2
3
4
5
6
7
private boolean isLocalFile(Uri uri) {
String path = uri.getPath();
if (path.startsWith(contentStart) || path.startsWith(fileStart) || uri.getScheme().equals("file")) {
return true;
}
return false;
}
做完这些工作后 ionic 就可以响应 file schema 请求了。
继续测试,我发现更新后第二次打开还是显示 App bundle asset 中的 web,这有点奇怪。仔细查看日志,确实有加载外部存储的 web , 但却被 http://localhost/
的请求覆盖了,这是什么原因呢?经过对代码逻辑的一番梳理,我发现是 IonicWebViewEngine 中 onPageStarted 方法的原因:
1
2
3
4
5
6
7
8
9
10
11
12
13
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
String launchUrl = parser.getLaunchUrl();
if (!launchUrl.contains(WebViewLocalServer.httpsScheme) && !launchUrl.contains(WebViewLocalServer.httpScheme) && url.equals(launchUrl)) {
view.stopLoading();
// When using a custom scheme the app won't load if server start url doesn't end in /
String startUrl = CDV_LOCAL_SERVER;
if (!scheme.equalsIgnoreCase(WebViewLocalServer.httpsScheme) && !scheme.equalsIgnoreCase(WebViewLocalServer.httpScheme)) {
startUrl += "/";
}
view.loadUrl(startUrl);
}
}
MainActivity 触发 webview 加载 file:///android_asset/www/index.html
,然后 cordova hot code push plugin 启动工作,它会让 webview 加载外部存储的 web,之后 IonicWebViewEngine 的 onPageStarted 收到 file:///android_asset/www/index.html
的请求的回调,它先停止了 webview 的加载工作,即 cordova hot code push plugin 启动加载外部存储的 web 的请求,再开始 http://localhost/
的请求,也就是打印出来日志的记录。正是这个方法时序的问题导致成功更新之后再重启应用仍然加载 app bundle asset 的 web。一种解决办法是我们直接让 MainActivity 直接加载 http://localhost/
,就像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// enable Cordova apps to be started in the background
Bundle extras = getIntent().getExtras();
if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
moveTaskToBack(true);
}
launchUrl = "http://localhost/";
// Set by <content src="index.html" /> in config.xml
loadUrl(launchUrl);
}
这样热更新就可以正常工作了。
我继续做了点测试,又发现一个和 ionic icon 相关的问题,ionic 4 使用了 Fetch API 来请求 ionic icon 的 svg 资源,由于现在是使用 file schema 来指定资源路径,由于 Fetch API 不支持 file schema 所以就报错 Fetch API cannot load file:///xxx/www/svg/md-star.svg. URL scheme "file" is not supported.
我们得想办法来解决这个问题,一个办法替换 fetch 方法的实现,如:
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
<script>
document.write('<base href="' + document.location.href + '" />');
var originalFetch = window.fetch;
window.fetch = function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var url = args[0];
if (typeof url === 'string' && url.match(/\.svg/)) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
resolve({
ok: true,
status: 200,
text: function() {
return Promise.resolve(req.responseText);
}
});
});
req.addEventListener('error', reject);
req.send();
});
} else {
return originalFetch.apply(void 0, args);
}
};
</script>
在这些测试过程中,我还发现 cordova hot code push 更新时只做了版本字符是否相等的判断,这在服务器端的版本低于本地版本时,插件仍然会做更新,这是有问题的,我们需要严格这里的判断,让它只有在服务端的版本高于本地版本时才做更新。相关代码位于 UpdateLoaderWorker 的 run 方法中。
最后一个要考虑的问题是如何将我们修改的代码和 ionic 的代码很好的整合起来?我现在的想法是创建一个私有的扩展 IonicWebViewEngine 和 WebViewLocalServer,然后借鉴 ionic 通过 config.xml 的 web 偏好设置的方法,像下面的代码:
1
<preference name="webView" value="com.ionicframework.cordova.webview.IonicWebViewEngine" />
回头测试下这个想法,好了有时间也许可以整理好代码提个 Pull Request。
Reference: