携程CRN源码详解之拆包(二)——rn_common打包

1、rn_common

     rn_common是crn所定义的框架包,rn_common包含了RN原来的框架和crn自己的基础框架。作为基础框架,rn_common会被上百个携程业务包共同使用,只有把框架代码抽取出来才能实现框架代码后台预加载功能,这样加载业务页面的时候只需要加载业务代码,大大减少加载时间。

2、打包参数传递

   上一节我们已经知道:打common包的时候会传递--build-common true。所以需要修改RN打包源码使打包代码认得这个参数,我们全局搜索CRN BEGIN可以快速找到CRN改动的地方,这方面CRN还是做得非常良心的。不过这里的build.js是crn自己的代码:

//build.js
function doBuildBundle(args) {
    var options = require('minimist')(args);
    var buildCommand = getBuildCommands(options);
    var buildCommon = options['build-common'];
    if (!util.isBoolean(buildCommon)) {
        buildCommon = buildCommon === 'true';
    }
    var cmd = '';
    if (buildCommon) {
        cmd = 'node node_modules/react-native/local-cli/cli.js bundle --config rn-cli.config.js ' + buildCommand;
        logOutPut.log(cmd);
        execSync(cmd, { stdio: 'inherit' });
    } else {
        cmd = 'node node_modules/react-native/cli.js ram-bundle --config rn-cli.config.js ' + buildCommand + ' --assets-dest bundle_output/publish';
        logOutPut.log(cmd);
        execSync(cmd, { stdio: 'inherit' });
    }

}

这里直接把参数透传给cli.js,再透传到bundle.js文件,这个时候crn改了代码:

///bundle.js
function bundleWithOutput(_, config, args, output) {
  // BEGIN
  //增加 设置全局的buildCommon
  global.CRN_BUILD_COMMON = args.buildCommon;
  //CRN END
  return (0, _buildBundle.default)(args, config, output);
}

把buildCommon参数保存到全局变量,这样就不需要再传参数了

3、只打rn_common

     打common包一直都是比较简单直接的,你只需要把打包的入口设置成只包含框架库的js文件就可以了,crn-cli就指定打包入口为crn_common_entry。

//localPack.js 40行
build([
				'--platform',
				'ios',
				'--entry-file',//这里指定了打包入口
				'crn_common_entry.js',
				'--build-common',
				'true',
				'--dev',
				dev
			]);

再看看crn_common_entry.js文件:

/** 
 * Common包入口文件
 * 注册一个空壳App,预先加载。
 * 进入到业务包页面时,监听native事件上报,加载业务模块,重新渲染页面。
*/
import React, { Component } from 'react';
import {
  AppRegistry,
  View,
  DeviceEventEmitter
} from 'react-native';
var mainComponent = null;
var _component = null;
DeviceEventEmitter.removeAllListeners();
//native访问业务包,上报事件通知需要加载的模块ID
DeviceEventEmitter.addListener("requirePackageEntry", function (event) {
  if (event && event.packagePath) {
    global.CRN_PACKAGE_PATH = event.packagePath; //设置资源加载的路径
  }
  if (event && event.moduleId) {
    mainComponent = require(event.moduleId);
    if (_component) {
      _component.setState({ trigger: true });
      _component = null;
    }
  }
});
class CommonEntryComponent extends Component {
... ... 
  render() {
    _component = this;
    var _content = null;
    if (mainComponent) {
      _content = React.createElement(mainComponent, this.props);
    }
    return _content || ;
  }}
AppRegistry.registerComponent('CRNApp', () => CommonEntryComponent); //CRNApp名字请勿修改

这里做了两件事:1、注册了一个叫CRNApp的空壳,这个空壳依赖了react native控件,打包的时候就会把react native打进去。原生端会预先加载这个空壳,这个名字是写死的不可更改 2、接收原生端加载业务模块的消息,加载业务模块并在空壳上展示

这里有个细节:mainComponent = require(event.moduleId); 要实现这句代码需要做许多工作

4、require(event.moduleId)

   这句看起来平白无奇,但正常的js代码这么写是会有问题的,js代码不允许require一个变量。而为此crn魔改了代码。

在此之前要先知道打包的三大操作:遍历解析js依赖、转换js源码序列化。

而需要支持require(event.moduleId)就要在转换js源码步骤中修改,因为require是需要转换的。

//collectDependencies.js

/**
 * 特殊处理require(event.moduleId),添加lazyRequire识别
 */
......
function getModuleNameFromCallArgs(path) {
  if (path.get("arguments").length !== 1) {
    throw new InvalidRequireCallError(path);
  } 
  //CRN BEGIN

  let reqFirstName = null,
    reqSecondName = null;
  let node = path.node;

  if (node.arguments[0].object) {
    if (
      node.arguments[0].object.name === "event" &&
      node.arguments[0].property &&
      node.arguments[0].property.name === "moduleId"
    ) {//判断require函数里的第一个参数是不是event.moduleId
      reqFirstName = "event";
      reqSecondName = "moduleId";
    } 
.... .... ....
  }

  const nameExpression = node.arguments[0];

  if (reqSecondName && reqFirstName) {//支持这种格式
    return [nameExpression, true, reqFirstName + "." + reqSecondName];
  } 
  //CRN END

  const result = path.get("arguments.0").evaluate();

  if (result.confident && typeof result.value === "string") {
    return result.value;
  }

  return null;
}

.....

mainComponent = require(event.moduleId);还有一个注意点,如果我们写的业务模块是这样的:

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

虽然这个是我们日常的写法,但require这个模块之后是不会返回App模块的,导致mainComponent不可用

这就是为什么官方文档强调“添加一行模块导出代码”

5、后续

    补充以下,rn_common不像react-native-multibundler可以定制化,这个逻辑是死的,如果要定制需要修改其入口文件crn_common_entry和业务打包逻辑源码。

    到这里rn_common的打包已经差不多讲完,还有一些小细节留到后面慢慢分析,到现在官方所描述的功能已经明了了一些:

  • 打包支持框架和业务代码拆分
  • 支持框架代码后台预加载
  • 打包支持增量编译(同一模块,两次打包模块ID不变)
  • iOS&Android统一一套打包产物
  • 首屏加载性能统计
  • LazyRequire

 

 

你可能感兴趣的:(react,naitve,CRN,react,native,拆包,性能)