ReactNative热更新
2021-04-22 本文已影响0人
一亩三分甜
18年金融监管严格,iOS无法上包,做了一款支持热更新的App,线上运行稳定,现对抗下记忆曲线,回忆之前是怎么完成的。
查询资料,参照ReactNativeSplit
1.原生部分,每次启动会调用后台接口,查询是否有更新,有更新则下载更新,进度条
典型的是12306买票
image.png
自己实现的效果
image.png
使用zip文件MD5也就是哈希值有两个好处:
1.第一个是防止android手机篡改SD卡下面的bundle和iOS越狱手机修改文件系统中的bundle,第二个是可以实时删除下载下来的.zip文件,增加内存空间。
2.可以实时删除下载下来的.zip文件,增加内存空间。
{"appUpdateConfig":{"appHasNewVersion":false,"appForceUpdate":false,"appUpdateUrl":"","appUpdateDesc":""},"rnUpdateConfig":{"rnForceUpdate":true,"rnBaseUpdateUrl":"http://internal-artifactory-970728191.cn-north-1.elb.amazonaws.com.cn/artifactory/rn-js-bundle/RNKingReturnApp/test/1.0.0/RNKingReturnApp-ios-1.0.0.10.zip","rnBaseUpdateMd5":"7e0a7cc477fc6c25dc14c4750d938438","rnIncrementUpdateUrl":"http://internal-artifactory-970728191.cn-north-1.elb.amazonaws.com.cn/artifactory/rn-js-bundle/RNKingReturnApp/test/1.0.1/RNKingReturnApp-ios-patch-1.0.1.12.zip","rnIncrementUpdateMd5":"3ab1ff0a4ad91126dd92fceb790af962"}}
image.png
I.第一版App中的bundle是直接拖入工程中,一个基包common.bundle和根据业务需要分的业务包,项目里面是一个apply.bundle包,每个bundle中对应的assets存放图片的文件夹要对应
image.pngbundle存放目录结构
/*
bundle文件结构
documents
├── IOSBundle(存放解压后的bundle)
| ├──common
| | ├──base(存放基础包)
| | ├──merge(存放合并后的包)
| | ├──patch(存放差分包)
| | └──backup(备份,暂时没用)
| |
| ├──other
| |
| ├──apply
| |
| └──login
|
|
└── bundleZip(存放zip包)
├──common
| ├──base(存放基础包zip)
| └──patch(存放差分包zip)
├──apply(存放other.zip)
├──apply(存放apply.zip)
└──login(存放login.zip)
*/
图片.png
II.当有更新业务包或基包时,要将更新后的图片文件夹Assets合并到基包common.bundle同级目录的Assets文件夹中,否则会加载不到图片
实现热更新步骤.pngIII.根据不同的module加载不同的bundle,可将rn参数传入,默认不传vc从keywindow.rootViewController模态视图进入
/**
模态rn视图
@param vc 从哪个控制器present,默认从keyWindow.rootViewController
@param moduleName bundle名称,如login,apply
@param routeName 跳转到的rn界面
@param params 传递给rn的参数
*/
+ (void)presentReactViewControllerInVC:(UIViewController *)vc WithModuleName:(NSString *)moduleName routeName:(NSString *)routeName params:(NSDictionary *)params loginCompletion:(void (^)(void))loginCompletion;
+ (void)presentReactViewControllerInVC:(UIViewController *)theVC WithModuleName:(NSString *)moduleName routeName:(NSString *)routeName params:(NSDictionary *)params loginCompletion:(void (^)(void))loginCompletion {
RCTBridge *bridge = ((AppDelegate *)[UIApplication sharedApplication].delegate).bridge;
//下载bundle
NSString *sourcePath;
if ([moduleName isEqualToString:XMReactApplyModule]) {
sourcePath = [[XMBundleDownloadTool sharedTool] loadApplyBundleFilePath];
}
NSURL *sourceURL = [NSURL fileURLWithPath:sourcePath];
// //加载本地bundle
// NSURL *sourceURL = [[NSBundle mainBundle] URLForResource:[NSString stringWithFormat:@"%@/%@",moduleName,moduleName] withExtension:@"bundle"];
[bridge loadModule:moduleName url:sourceURL onComplete:^(NSError *error) {
if (error) {
XMLog(@"DPNmanager::subb::加载失败error::%@",error);
} else {
XMLog(@"DPNmanager:subb::加载成功");
XMReactViewController *vc = [[XMReactViewController alloc] initWithModuleName:moduleName routeName:routeName params:params];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.navigationBar.hidden = YES;
vc.loginCompletion = loginCompletion;
RCTRootView* view = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:nil];
view.frame = [UIScreen mainScreen].bounds;
[vc setView:view];
if (theVC) {
[theVC presentViewController:nav animated:YES completion:nil];
} else {
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:nav animated:YES completion:nil];
}
}
}];
}
2.React Native部分
1.分包splitconfig文件,需要分多少个包在此指定
{
"package": "",
"base": {
"index": "./base.js",
"includes": [
"./common/*"
]
},
"custom": [
{
"name": "login",
"index": "./xmkd-login-rn/index.js"
},
{
"name": "apply",
"index": "./xmkd-apply-rn/index.js"
},
{
"name": "other",
"index": "./xmkd-other-rn/index.js"
}
]
}
II.index.js中进行分包,分的三个业务包放在不同的git仓库中
'use strict';
require('./split/setupBabel');
const fs = require('fs');
const path = require('path');
const commander = require('commander');
const Util = require('./split/utils');
const Parser = require('./split/parser');
const bundle = require('./split/bundler');
commander
.description('React Native Bundle Spliter')
.option('--output <path>', 'Path to store bundle.', 'build')
.option('--config <path>', 'Config file for react-native-split.')
.option('--platform <string>', 'Specify bundle platform. ')
.option('--dev [boolean]', 'Generate dev module.')
.parse(process.argv);
if (!commander.config) {
throw new Error('You must enter an config file (by --config).');
}
function isFileExists(fname) {
try {
fs.accessSync(fname, fs.F_OK);
return true;
} catch (e) {
return false;
}
}
console.log(commander.platform)
const configFile = path.resolve(process.cwd(), commander.config);
const outputDir = path.resolve(process.cwd(), commander.output);
if (!isFileExists(configFile)) {
console.log('Config file ' + configFile + ' is not exists!');
process.exit(-1);
}
const rawConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
const workRoot = path.dirname(configFile);
const outputRoot = path.join(outputDir, `${commander.platform}`);
Util.ensureFolder(outputRoot);
const config = {
root: workRoot,
dev: commander.dev === 'true',
packageName : rawConfig['package'],
platform : commander.platform,
outputDir : path.join(outputRoot, 'split'),
bundleDir : path.join(outputRoot, 'bundle'),
baseEntry : {
index: rawConfig.base.index,
includes: rawConfig.base.includes
},
customEntries : rawConfig.custom
};
if (!isFileExists(config.baseEntry.index)) {
console.log('Index of base does not exists!');
}
console.log('Work on root: ' + config.root);
console.log('Dev mode: ' + config.dev);
bundle(config, (err, data) => {
if (err) throw err;
console.log('===[Bundle] Finish!===');
const parser = new Parser(data, config);
parser.splitBundle();
});
III.打包脚本build.sh
rm -rf ./build
mkdir build
function adbpush(){
adb shell rm -rf /sdcard/xmkd/.hide/mergefile
adb shell rm -rf /sdcard/xmkd/.hide/businessfile
adb shell mkdir /sdcard/xmkd/.hide/mergefile
adb shell mkdir /sdcard/xmkd/.hide/businessfile
adb push build/$platform/split/common/* /sdcard/xmkd/.hide/mergefile/
adb shell mkdir /sdcard/xmkd/.hide/businessfile/login
adb push build/$platform/split/login/login.bundle /sdcard/xmkd/.hide/businessfile/login
adb push build/$platform/split/login/drawable-xxhdpi/* /sdcard/xmkd/.hide/mergefile/drawable-xxhdpi/
adb push build/$platform/split/login/drawable-xhdpi/* /sdcard/xmkd/.hide/mergefile/drawable-xhdpi/
adb shell mkdir /sdcard/xhkd/.hide/businessfile/apply
adb push build/$platform/split/apply/apply.bundle /sdcard/xmkd/.hide/businessfile/apply
adb push build/$platform/split/apply/drawable-xxhdpi/* /sdcard/xmkd/.hide/mergefile/drawable-xxhdpi/
adb push build/$platform/split/apply/drawable-xhdpi/* /sdcard/xmkd/.hide/mergefile/drawable-xhdpi/
adb shell mkdir /sdcard/xhkd/.hide/businessfile/other
adb push build/$platform/split/other/other.bundle /sdcard/xmkd/.hide/businessfile/other
adb push build/$platform/split/other/drawable-xxhdpi/* /sdcard/xmkd/.hide/mergefile/drawable-xxhdpi/
adb push build/$platform/split/other/drawable-xhdpi/* /sdcard/xmkd/.hide/mergefile/drawable-xhdpi/
adb shell am force-stop com.xmqb.xmkd
adb shell am start -n "com.xmqb.xmkd/com.xmqb.xmkd.ui.start.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
}
function moveAndroidRes(){
for file in ./build/android/split/*
do
if test -d $file
then
echo $file
resDir=$file/res
mkdir $resDir
for drawable in $file/drawable*
do
echo $drawable
mv $drawable $resDir
done
fi
done
}
function echoBundleMd5() {
for file in ./build/$1/split/*
do
if test -d $file
then
for bundle in $file/*.bundle
do
md5 $bundle
done
fi
done
}
if [ $1 ]; then
if [[ $1 -eq 'android' || $1 -eq 'ios' ]]; then
platform=$1
echo $platform
mkdir build/$platform
node ./index.js --platform $platform --output build --config splitconfig --dev false
echoBundleMd5 $platform
adbpush
fi
else
rm -rf ./xmkd-apply-rn
rm -rf ./xmkd-login-rn
rm -rf ./xmkd-other-rn
git clone -b 1.0.0 https://code.xmdev.xyz/mobile/xmkd-apply-rn.git
git clone https://code.xmdev.xyz/mobile/xmkd-login-rn.git
git clone https://code.xmdev.xyz/mobile/xmkd-other-rn.git
mkdir build/android
mkdir build/ios
node ./index.js --platform 'android' --output build --config splitconfig --dev false && node ./index.js --platform 'ios' --output build --config splitconfig --dev false
moveAndroidRes
echoBundleMd5 'android'
echoBundleMd5 'ios'
fi
4.打包后的结果,分包后的内容可上传服务器或拖入工程中
image.png4.jenkinsfile文件打包
frontendWithSubmodule{
deployEnv = [
"1.0.0": "test"
]
buildConfig = [
"1.0.0": "./build.sh"
]
baseVersion = [
"base_version": "1.0.0"
]
modulesVersion = [
"apply": "1.0.0",
"login": "1.0.0",
"other": "1.0.0"
]
}
5.打包bundle和asset文件夹与Xcode工程关联,需通过rb脚本,打包时,下载的bundle和asset放入Xcode工程目录下的ReactBundle文件夹中,但此时仅仅只是放了一个普通的文件夹到Xcode的物理目录下了,相当于没有执行拖入时的关联操作,通过rb脚本将ReactBundle目录下的common和apply脚本关联起来
image.pngrequire 'xcodeproj' #导入
project_path = File.join(File.dirname(__FILE__), "./XMKD.xcodeproj")
project = Xcodeproj::Project.open(project_path)
target = project.targets.first
mapiGroup = project.main_group.find_subpath(File.join('ReactBundle'+'/'+ARGV.first), true)
mapiGroup.set_source_tree('<group>')
mapiGroup.set_path(ARGV.first) #相对于你放代码的文件夹
#移除文件链接
def removeBuildPhaseFilesRecursively(aTarget, aGroup)
aGroup.files.each do |file|
# if file.real_path.to_s.end_with?(".m", ".mm") then
# aTarget.source_build_phase.remove_file_reference(file)
# elsif file.real_path.to_s.end_with?(".plist") then
aTarget.resources_build_phase.remove_file_reference(file)
# end
end
aGroup.groups.each do |group|
removeBuildPhaseFilesRecursively(aTarget, group)
end
end
#添加文件链接
def addFilesToGroup(aTarget, aGroup)
Dir.foreach(aGroup.real_path) do |entry|
filePath = File.join(aGroup.real_path, entry)
# 过滤目录和.DS_Store文件
if entry != ".DS_Store" && !filePath.to_s.end_with?(".meta") &&entry != "." &&entry != ".."then
# 向group中增加文件引用
fileReference = aGroup.new_reference(filePath)
# 如果不是头文件则继续增加到Build Phase中,PB文件需要加编译标志
# if filePath.to_s.end_with?("pbobjc.m", "pbobjc.mm") then
# aTarget.add_file_references([fileReference], '-fno-objc-arc')
# elsif filePath.to_s.end_with?(".m", ".mm") then
# aTarget.source_build_phase.add_file_reference(fileReference, true)
# elsif filePath.to_s.end_with?(".plist") then
aTarget.resources_build_phase.add_file_reference(fileReference, true)
# end
end
end
end
if !mapiGroup.empty? then
removeBuildPhaseFilesRecursively(target,mapiGroup)
mapiGroup.clear()
end
addFilesToGroup(target, mapiGroup)
project.save
print "执行替换文件成功!"
关联后相当于如下效果
image.png