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.png

bundle存放目录结构

/*
 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文件夹中,否则会加载不到图片

实现热更新步骤.png

III.根据不同的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.png

4.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.png
require '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
上一篇下一篇

猜你喜欢

热点阅读