美文网首页
ReactNative热更新

ReactNative热更新

作者: 一亩三分甜 | 来源:发表于2021-04-22 10:56 被阅读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

    相关文章

      网友评论

          本文标题:ReactNative热更新

          本文链接:https://www.haomeiwen.com/subject/ajltrltx.html