当前位置: 首页 > news >正文

native react 更新机制_[React-Native] 实现增量热更新的思路

所谓热更新就是在不重新安装的前提下进行代码和资源的更新,相信在整个宇宙中还不存在觉得热更新不重要的程序猿。

增量热更新就更牛逼了,只需要把修改过和新增的代码和资源推送给用户下载即可,增量部分的代码和资源都比较小,所以整个热更新流程可以在用户无感的情况下完成,我已经想不到更好的更新方式可以让我装更大的逼了。

一.实现脚本的热更新

1.为什么可以热更新

简单地说,因为RN是使用脚本语言来编写的,所谓脚本语言就是不需要编译就可以运行的语言,也就是“即读即运行”。我们在“读”之前将之替换成新版本的脚本,运行时执行的便是新的逻辑了,稍微抽象一下,图片资源是不是也是“即读即运行”?所以脚本本质上和图片资源一样,都是可以进行热更新的。

2.RN加载脚本的机制

要实现RN的脚本热更新,我们要搞明白RN是如何去加载脚本的。 在编写业务逻辑的时候,我们会有许多个js文件,打包的时候RN会将这些个js文件打包成一个叫index.android.bundle(ios的是index.ios.bundle)的文件,所有的js代码(包括rn源代码、第三方库、业务逻辑的代码)都在这一个文件里,启动App时会第一时间加载bundle文件,所以脚本热更新要做的事情就是替换掉这个bundle文件。

3.生成bundle文件

我们在RN项目根目执行以下命令来得到bundle文件和图片资源:

react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false```

其中*--entry*是入口js文件,android系统就是index.android.js,ios系统就是index.ios.js,*--bundle-output*就是生成的bundle文件路径,*--platform*是平台,*--assets-dest*是图片资源的输出目录,这个在后面的图片增量更新中会用到,*--dev*表示是否是开发版本,打正式版的安装包时我们将其赋值为false。 生成的bundle文件体积还是不小的,空项目的话恐怕至少也有900K,所以我们将其打成zip包并放到web服务器上以供客户端去下载。

####4.下载bundle文件

下载文件可以使用原生语言来写,也可以使用js实现,我个人推荐使用[React Native FileTransfer](https://github.com/remobile/react-native-file-transfer)来实现下载功能。 实现方法很简单:

import FileTransfer from 'react-native-file-transfer';

let fileTransfer = new FileTransfer();

fileTransfer.onprogress = (progress) => {

console.log(parseInt(progress.loaded * 100 / progress.total))};

// url:新版本bundle的zip的url地址

// bundlePath:存在新版本bundle的路径

// unzipJSZipFile:下载完成后执行的回调方法,这里是解压缩

zipfileTransfer.download(url, bundlePath, unzipJSZipFile, (err) => { console.log(err); }, true);```

解压缩的工作我们可以使用react-native-zip来完成。

import Zip from 'react-native-zip';

function unzipJSZipFile() {

// zipPath:zip的路径

// documentPath:解压到的目录

Zip.unzip(zipPath, documentPath, (err)=>{

if (err) {

// 解压失败

} else {

// 解压成功,将zip删除

fs.unlink(zipPath).then(() => {

// 通过解压得到的补丁文件生成最新版的jsBundle

});

} });

}```

解压成功后,我们使用[react-native-fs](https://github.com/johanneslumpe/react-native-fs)来将zip删除。

####5.替换bundle文件

安装包中的bundle文件是在asset目录下的,而asset目录我们是没有写权限的,所以我们不能修改安装包中的bundle文件。好在RN中提供了修改读取bundle路径的方法。以android为例(ios的类似),在ReactActivity类中有这么一个方法:

/** * Returns a custom path of the bundle file.

This is used in cases the bundle should be loaded * from a custom path.

By default it is loaded from Android assets, from a path specified * by {@link getBundleAssetName}.

e.g. "file://sdcard/myapp_cache/index.android.bundle" */

protected @Nullable String getJSBundleFile() { return null;}```

该方法返回了一个自定义的bundle文件路径,如果返回默认值null,RN会读取asset里的bundle。我们在MainActivity类中重写这个方法,返回可写目录一下的bundle文件路径:

@Overrideprotected @Nullable String getJSBundleFile() {

String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";

File file = new File(jsBundleFile);

return file != null && file.exists() ? jsBundleFile : null;

}```

如果可写目录下没有bundle文件,还是返回null,RN依然读取的是asset中的bundle,如果可写目录下存在bundle,RN就会读取可写目录下的bundle文件。

我们将下载好的zip解压到**getFilesDir().getAbsolutePath()**目录下,再次启动App时便会读取该目录下的bundle文件了,以后再有新版本的bundle文件,依然是下载、解压并覆盖掉这个bundler文件,至此,我们便完成了代码的热更新工作。

####6.图片不见了

当我们使用可写目录下的bundle文件时会出现一个很严重的问题:所有的本地图片资源都无法显示了。

我们的图片资源都是通过require来获取的:

```

为了找到图片消失的原因,我们打开image.android.js或者image.ios.js,找到渲染图片的方法:

render: function() {

var source = resolveAssetSource(this.props.source);

var loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource); // ...}```

原来是通过resolveAssetSource方法来获取资源,那么找到resolveAssetSource方法:

function resolveAssetSource(source: any): ?ResolvedAssetSource {

if (typeof source === 'object') {

return source;

}

var asset = AssetRegistry.getAssetByID(source);

if (asset) {

return assetToImageSource(asset);

}

return null;

}

function assetToImageSource(asset): ResolvedAssetSource {

var devServerURL = getDevServerURL();

return { __packager_asset: true,

width: asset.width,

height: asset.height,

uri: devServerURL ? getPathOnDevserver(devServerURL, asset) : getPathInArchive(asset),

scale: pickScale(asset.scales, PixelRatio.get()), };}```

又发现是通过getPathInArchive方法来获取资源的,那么继续找到getPathInArchive方法:

/** * Returns the path at which the asset can be found in the archive */

function getPathInArchive(asset) {

var offlinePath = getOfflinePath();

if (Platform.OS === 'android') {

if (offlinePath) {

// E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'

return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);

}

// E.g. 'assets_awesomemodule_icon'

// The Android resource system picks the correct scale.

return assetPathUtils.getAndroidResourceIdentifier(asset);

} else {

// E.g. '/assets/AwesomeModule/icon@2x.png'

return offlinePath + getScaledAssetPath(asset);

}

}```

该方法的逻辑是如果有离线脚本,那么就从该脚本所在目录里寻找图片资源,否则就从asset中读取图片资源,所谓离线脚本就是我们刚刚下载并解压的bundle文件,而我们并没有将图片资源放在这个目录下,所以所有的图片都不见了。 找到原因就好办了,我们在使用bundle命令生成bundle文件的时候也将图片资源输出出来了,那打包bundle文件的时候我们将所有图片也一并打包进zip,客户端下载zip并解压缩后,客户端可写目录下也就有了**所有**的图片资源,这样就即实现了脚本的热更新又实现了图片的热更新。

#二.减小更新包体积

将一个完整bundle文件和所有图片都打成zip,zip的体积让人不敢直视。

####1.增量更新图片

每一次的版本更新我们都将所有图片装进zip包未免有点太任性了,其实我们只需要将修改过和新增的图片资源放进zip就行了。 我们修改一下获取图片资源的方法里的逻辑:

/** * Returns the path at which the asset can be found in the archive */

function getPathInArchive(asset) {

var offlinePath = getOfflinePath();

if (Platform.OS === 'android') {

if (offlinePath) {

// 热更新修改 开始

if(global.patchList){

let picName = ${asset.name}.${asset.type};

for (let i = 0; i < global.patchList.length; i++) {

if(global.patchList[i].endsWith(picName)){

return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);

}

} } // 热更新修改 结束

// E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png' //

return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);

} // E.g. 'assets_awesomemodule_icon'

// The Android resource system picks the correct scale.

return assetPathUtils.getAndroidResourceIdentifier(asset);

} else {

// E.g. '/assets/AwesomeModule/icon@2x.png'

return offlinePath + getScaledAssetPath(asset);

}

}```

其中global.patchList是一个数组,里面放的是自安装包版本以来所有修改过和新增的图片名,如果访问的图片名在这个数组中就从离线脚本所在目录里寻找图片资源,否则还是从asset中寻找图片资源。 我们在打包zip的时候,就只装修改过和新增的图片,并将这些图片名记录在更新配置文件里,客户端去读取更新配置文件时将配置中的图片名读取到并生成global.patchList,这样我们的更新包就小了许多了。 这么做的缺点就是每次更新RN版本的时候,都需要修改下RN的源码,不过我觉得这点小麻烦还是可以接受的,毕竟已上线的产品,我们还是以稳定为主,能不升级RN就不升级RN。

2.增量更新脚本

bundle文件的体积,我们也得想想办法去减少它。 有两种思路:

分离bundle。bundle里存放了RN源码、第三方库代码和业务逻辑代码,其中频繁更新的就只有业务逻辑代码,所以我们将RN源码和第三方库代码打包成一个bundle,业务逻辑打包成一个bundle,热更新的时候就只更新业务逻辑的bundle即可。

打包补丁文件。我们可以使用bsdiff对比两个版本的bundle文件得到差异文件,也就是“补丁”,客户端下载好补丁文件,将其与本地的bundle进行融合从而得到最新版本的bundle文件。

这里重点讲解第二个思路的做法。

1) 生成补丁。

我们从bsdiff官网上下载到最新的源码,然后进行编译就得到可执行的二进制文件了。

如果是win系统,可以直接到这里百度网盘下载,下载密码:zq1x。解压下载好的zip,使用命令行进入到bsdiff的目录,输入命令:

bsdiff a.txt b.txt c.pat```

上面的命令就是生成a.txt、b.txt两个文件的补丁c.pat。

如果是linux系统,可以依次执行以下命令:

yum install bzip2-develwget http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gztar zxvf bsdiff-4.3.tar.gzcd bsdiff-4.3```

编译完成后,会在目录下生成2个二进制文件:bsdiff、bspatch,这2个二进制文件可以直接使用,不过推荐拷贝到/usr/local/sbin/下:

cp bsdiff /usr/local/sbin/cp bspatch /usr/local/sbin/```

这样就可以在命令行中直接使用了:

bsdiff a.txt b.txt c.pat```

2) 使用补丁。 得到了补丁文件,下一步就会使用补丁了,拿上面的a.txt、b.txt、c.pat做测试:

bspatch a.txt d.txt c.pat```

得到文件d.txt,将其开打看看是否和b.txt一样,如果一样,说明测试成功。

#####3) 在RN中使用bsdiff。 待续。。。

相关文章:

  • combox数据过滤 wpf_ComboBox实现输入自动过滤
  • mysql 数据库表被锁住了_MYSQL数据库MYSQL 解锁与锁表介绍
  • 华为设备如何将接口配置为中继模式_华为5700系列交换机常用配置示例
  • 前端图片合成技术_前端图片合并方案调研
  • 可靠性测试设备技术含量_设备软件可靠性测试
  • plsql怎么用字段查表明_在oracle中怎么通过字段名查询其所在的表
  • 微信小程序云开发用户身份登录_微信小程序+云开发实现欢迎登录注册
  • pandas提取时间里面的年月日_pandas进行时间数据的转换和计算时间差并提取年月日...
  • iserdese2接口详解_Xilinx Notes
  • postman启动没反应_高压软起动通电没反应维修控制器烧毁
  • python策略模式包含以下哪些角色_python---策略模式
  • mybatis 插件_建议收藏,mybatis插件原理详解
  • h5引入json_Html5页面内使用JSON动画的实现
  • bootstrap列表加序号_用vue.js做一个列表,类似于百度的搜索排名,用v-for来循环...
  • 中将2个map的值合并_如果是我,不纠结卫生间留1个还是2个,主卫次卫大合并,宽敞舒适...
  • canvas 高仿 Apple Watch 表盘
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  • es6(二):字符串的扩展
  • happypack两次报错的问题
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • magento 货币换算
  • quasar-framework cnodejs社区
  • React组件设计模式(一)
  • Sass 快速入门教程
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • 第2章 网络文档
  • 来,膜拜下android roadmap,强大的执行力
  • 深度学习入门:10门免费线上课程推荐
  • 一个完整Java Web项目背后的密码
  • 【干货分享】dos命令大全
  • 7行Python代码的人脸识别
  • 关于Kubernetes Dashboard漏洞CVE-2018-18264的修复公告
  • #考研#计算机文化知识1(局域网及网络互联)
  • (4)事件处理——(7)简单事件(Simple events)
  • (LeetCode 49)Anagrams
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (Redis使用系列) SpringBoot 中对应2.0.x版本的Redis配置 一
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (转)创业的注意事项
  • (转)为C# Windows服务添加安装程序
  • (转)原始图像数据和PDF中的图像数据
  • .NET Micro Framework初体验(二)
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .net 反编译_.net反编译的相关问题
  • .net2005怎么读string形的xml,不是xml文件。
  • @html.ActionLink的几种参数格式
  • @ModelAttribute 注解
  • [ 攻防演练演示篇 ] 利用通达OA 文件上传漏洞上传webshell获取主机权限
  • [ 渗透测试面试篇 ] 渗透测试面试题大集合(详解)(十)RCE (远程代码/命令执行漏洞)相关面试题
  • [2016.7 test.5] T1
  • [22]. 括号生成
  • [BZOJ1008][HNOI2008]越狱
  • [BZOJ2281][SDOI2011]黑白棋(K-Nim博弈)
  • [C/C++] -- 二叉树