博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
QQ音乐高级工程师袁聪:大胆尝试,展现不一样的React Native
阅读量:2394 次
发布时间:2019-05-10

本文共 11496 字,大约阅读时间需要 38 分钟。

责编:陈秋歌,关注前端开发领域,寻求报道或者投稿请发邮件chenqg#csdn.net。

欢迎加入“CSDN前端开发者”微信群,参与热点、难点技术交流。请加群主微信「Rachel_qg」,申请入群,务必注明「公司+职位」。另可申请加入CSDN前端开发QQ群:465281214。

将于11月18日在北京京都信苑饭店召开,本届大会汇集100+讲师,设置了12大专题论坛。本文作者QQ音乐&全民K歌高级工程师袁聪,已受邀担任SDCC 2016前端开发专题演讲嘉宾,分享全民K歌React Native最佳实践。

以下内容为会前袁聪给大家带来的营养小餐,技术大餐即将在上呈现。


React Native(以下用RN简称代替)的开源是App开发的一个里程碑式的事件,它为App开发提供了新的开发模式和思路。RN既有传统Hybrid框架的优势,又提供了统一的Native体验,吸引着越来越多的项目去实践。

全民K歌去年十月份完成了RN的接入实践,至此已经有一年多了。现在的我们已经不再满足简单的接入和优化,我们开始大胆尝试,做一点不一样的RN,今天我们来就聊一聊全民K歌不一样的RN。

这里先给大家上两份小菜,大餐在11.18号下午,不见不散。

P.S. RN的代码已经被分析来分析去太多遍了,相关资料很多,为避免冗长枯燥,本文只提示基于0.35版本的关键代码。

Bundle拆分——业务分包

为什么要分包?

  • 避免执行大量JS代码带来的性能瓶颈
  • 减少更新时的流量消耗
  • 业务分离,按需加载

关于性能瓶颈,我们先来看一张图:

图片描述

JS Init + Require的时间在整个RN的启动过程中占了约一半,随着业务的增多以及复杂度的增加,这个比例还会上升。

流量消耗一直是用户关注的重点,用户关注的重点就是我们关注的重中之重,RN基础的Bundle有1.5M,即使压缩后也有300多K,业务Bundle的更新,往往只有几K或者几十K,带上基础Bundle这个大尾巴不太合适,分包可以为用户节省不必要的流量消耗,而且在复杂的外网环境下,包越小越有利于更新成功率的提高。

业务分离,按需加载,非纯RN的应用往往入口在不同的地方,不必一股脑的把所有业务模块一起加载,分包加载可以提升加载速度以及减少不必要的资源浪费。

RN自己提供了一个unbundle的方案,Android端把除了polyfill和startup部分之外的所有模块拆成大量的单独js文件,在引用到js文件的时候通过NativeRequire方法去加载对应的js文件,大量频繁的I/O,Android表示已哭晕。。。而iOS端则是在头部添加一堆index table用来做索引,反而会增大文件。可能是还不成熟的原因,RN还没有正式公开这个方案。而且这个方案不能区分业务,不能满足我们的需求。

那么全民K歌如何去实践根据业务分包的?来看下前端同学马铖(Calvinma)的方案:


首先我们看到RN自带的react-packager打包源码比较轻量简洁,保持轻量和对RN特性关注也是RN不使用webpack和broswerify而是自己实现打包的原因。因此我们的方案主要是通过增强 react-packager的打包源码来实现分包(不造轮子)。

RN的打包结果是一套类似CommonJS的轻量require/define模块系统。要做到上面的文件分离和不重复打包,就是要做到依赖引用(业务包去require基础包中的模块), 因此我们要把基础包中包含的模块列表导出来给业务包打包时使用,类似webapc中DllPlugin的实现。

所以分包的主要逻辑如下:

打包(Compile time):

用一个base.js做Entry(包含认为是基础库的内容,比如RN核心、组件、React)打包出基础包,并导出包含的模块列表

根据基础包模块列表打包业务包,过滤业务包的依赖:去掉重复的模块,require 时使用列表里的模块id

客户端运行(Run time):

适当的时机在JSContext运行基础包(比如App launch ready或者用户进入某些场景、下一步可能开启RN时)

用户进入RN的部分,运行业务包,然后runApplication,适当的机制从 CDN 拉取更新业务包。我们学习一下webpack,把这份模块列表称为manifest 。

一图概括之:

图片描述

这个方案的主要优点在于易施行,在本地打包时做文章,分离的包可以分开前后运行(不需要再在终端做合并)

分包灵活,自己通过Entry去控制基础包里要有什么。不用单独再写构建
降低客户端工作量,不用引入复杂的Diff和合并,只要实现可分开运行(runJSInContext和runApplication分离)就可以,RN的源码里都有接口。

举个栗子:

拿官方Hello World来演示下,先把所有公开的组件和API打到个基础包里:

base.android.js

图片描述

输出来的manifest.json大概长这个样子:

图片描述

再带上这份manifest来打包原版的Hello World,最终生成的文件如下:

图片描述

surprise,业务Bundle就是这么小。是不是等不及来看看怎么实现的了?

下面分析一下分包涉及的源码修改和实现(源码根据0.35版本):

首先我们在local-cli添加了两个参数:

–manifest-output: 打包时把bundle包含的模块导出生成为一个manifestFile(打基础包)

–manifest-file : 打包时传入指定的manifestFile进行过滤(打业务包)

local-cli/bundle/bundleCommandLineArgs.js

bundleCommandLineArgs

在打包生成时(拿到所有依赖模块列表后),如果有传入manifestOutput,把模块列表按固定格式输出一份JSON文件(模块名: 模块id)

local-cli/bundle/output/bundle.js

bundle

packager/react-packager/src/Bundler/Bundle.js

bundle1

在依赖解析中require模块时,如果有传入manifestFile,require 已有的模块时使用manifestFile中对应的id。比较方便就是在getModuleId()中做一个处理。

packager/react-packager/src/Bundler/index.js

图片描述

最后在打包前,如果有传入manifestFile,从resolutionResponse按照其过滤掉已存在的模块。

packager/react-packager/src/Bundler/index.js

图片描述

主要的源码涉及就是Bundler和Bundle部分,其次还有cli的添加,过滤掉polyfill的重复插入等。

需要注意的问题:

id重复:

目前的RN打包模块使用递增数字id,分开打包时id就会重复。我的做法是把基础包的最大id输出来接着递增,或者多个业务包时给id加前缀(–id-prefix)。增强方案也可学习webpack使用filename hash做id。


好了,前端同学已经帮我们拆出来的多个Bundle,那么客户端怎么去加载呢?

先来看下一个简单的JavaScript VM模型:

VM

JSContext是JavaScript执行的上下文,由GlobalObject管理。我们在同一个GlobalObject对应的同一个JSContext中执行JavaScript代码,执行一个和执行多个JavaScript是没有区别的,所以上面多个包的加载完全没有问题。

这里不赘述RN的Bundle加载流程,最终用到了JSC的一个API——JSEvaluateScript去执行Bundle,我们只需要针对API封装,提供一个方法给上层调用即可,当然注意执行顺序,在基础Bundle加载完再去执行业务Bundle,iOS和Android实现基本类似。

Native组件动态加载

听说RN支持随时发版,这么好?先来一斤需求!

一开始听到这种提问,第一反应是去解释,RN提供了基础的Native组件(这里我们把Native Module和封装的View统称为Native组件),RN的动态发布是基于已有的Native组件的动态发布,我们可以根据业务的需要去扩展Native组件,但是不可能在一个版本里面把所有可能用到的Native组件都封装好。后来源码读得多了,也开始思考,是不是真的可以随时无限制的去做需求?如何在不更新版本的基础上如何让RN真正的实现需求快速迭代开发,放飞产品同学们的想象力呢?

动态添加Native组件并将其动态加载。

动态添加Native组件,这对于iOS来说可能有点难度,但是对于Android,插件和热补丁大家都玩了这么多年了,这只是个小问题,很容易解决。

那么如何去动态加载Native组件呢?有几种方法可以实现,

  • 方法一:构建一个通用的代理NativeModule,传递需要执行的类名、方法名以及参数列表,通过反射去调用;
  • 方法二:去了解NativeModule的注册和调用流程,动态向其中添加新的Native组件。

方法一简单易于实现,但是需要自己维护对象和构建参数,同时对JS非透明,应用Native版本升级时若想合入则需要多套JS代码对应于不同的调用方式,版本控制比较麻烦。

方法二实现难度稍大,沿用RN的设计,对JS透明,应用Native版本升级可以随之合并进最新APK中。

鉴于以上原因,我们更倾向于方法二。下面就让我们来看看RN是如何完成Native Module的注册以及JS如何去调用Native Module的(这里不带大家走一遍完整的启动流程和Native、JS互调的源码逻辑了,大家可以在网上搜到很多这类的文章,已经被解释得都很详尽)。

1.JNI层中ModuleRegistryHolder的构造函数中传入了Java层用JavaModuleWrapper封装过的NativeModule列表,将其构造JavaNativeModule容器对象传入ModuleRegistry的构造函数中,保存在其成员变量modules_中。

//ModuleRegistryHolder.cppModuleRegistryHolder::ModuleRegistryHolder(    CatalystInstanceImpl* catalystInstanceImpl,    jni::alias_ref
::javaobject> javaModules, jni::alias_ref
::javaobject> cxxModules) { std::vector
> modules; ... for (const auto& jm : *javaModules) { modules.emplace_back(folly::make_unique
(jm)); } ... registry_ = std::make_shared
(std::move(modules));}
//ModuleRegistry.cppModuleRegistry::ModuleRegistry(std::vector
> modules) : modules_(std::move(modules)) {}

这里需要留意的是ModuleRegistry中的moduleNames()函数,这是一个关键的方法,返回值是所有NativeModule方法的名字,这在后面向JS中注册NativeModule时举足轻重。

//ModuleRegistry.cppstd::vector
ModuleRegistry::moduleNames() { std::vector
names; for (size_t i = 0; i < modules_.size(); i++) { std::string name = normalizeName(modules_[i]->getName()); modulesByName_[name] = i; names.push_back(std::move(name)); } return names;}

2.initializeBridge是一个复杂的过程,它构建了Native和JS相互调用的通道。Native通过NativeToJsBridge中调用JSC的API去执行JS方法,而JS则调用在JavaScript VM中注册的JNI方法,通过JsToNativeBridge去执行Native方法。这里我们要去JSCExecutor的构造函数里面去看看NativeModule注册的相关逻辑。

//JSCExecutor.cppJSCExecutor::JSCExecutor(std::shared_ptr
delegate, std::shared_ptr
messageQueueThread, const std::string& cacheDir, const folly::dynamic& jscConfig) throw(JSException) : m_delegate(delegate), m_deviceCacheDir(cacheDir), m_messageQueueThread(messageQueueThread), m_jscConfig(jscConfig) { initOnJSVMThread(); SystraceSection s("setBatchedBridgeConfig"); folly::dynamic nativeModuleConfig = folly::dynamic::array(); { SystraceSection s("collectNativeModuleNames"); std::vector
names = delegate->moduleNames(); for (auto& name : delegate->moduleNames()) { nativeModuleConfig.push_back(folly::dynamic::array(std::move(name))); } } folly::dynamic config = folly::dynamic::object ("remoteModuleConfig", std::move(nativeModuleConfig)); SystraceSection t("setGlobalVariable"); setGlobalVariable( "__fbBatchedBridgeConfig", folly::make_unique
(detail::toStdString(folly::toJson(config))));}

initOnJSVMThread()函数去创建了JavaScript VM、JSContext、Global对象以及向其中注册了JNI方法。

在initOnJSVMThread()之后,出现了一个我们熟悉的方法moduleNames(),这里拿到所有的NativeModule的名字之后,构建JSON,赋值给Global中的__fbBatchedBridgeConfig对象,完成了NativeModule向JS的初步注册。

3.JS中新建MessageQueue对象,通过在JSCExecutor写入的__fbBatchedBridgeConfig对象去构建remoteModules,这样就把NativeModule映射到JS中了,但这还远远没有结束。

//BatchedBridge.jsconst BatchedBridge = new MessageQueue(() => global.__fbBatchedBridgeConfig);
//MessageQueue.jstype Config = {  remoteModuleConfig: Object,};class MessageQueue {
constructor(configProvider: () => Config) { ... lazyProperty(this, 'RemoteModules', () => { const {remoteModuleConfig} = configProvider(); const modulesConfig = remoteModuleConfig; return this._genModules(modulesConfig); }); } ... }function lazyProperty(target: Object, name: string, f: () => any) { Object.defineProperty(target, name, { configurable: true, enumerable: true, get() { const value = f(); Object.defineProperty(target, name, { configurable: true, enumerable: true, writeable: true, value: value, }); return value; } });}_genModules(remoteModules) { const modules = {}; remoteModules.forEach((config, moduleID) => { // Initially this config will only contain the module name when running in JSC. The actual // configuration of the module will be lazily loaded (see NativeModules.js) and updated // through processModuleConfig. const info = this._genModule(config, moduleID); if (info) { modules[info.name] = info.module; } ... }); return modules; }

4.NativeModules是不是很熟悉?JS就是通过NativeModules.类名.方法名去调用NativeModule的。这里通过去remoteModules中查找NativeModule并且调用JNI方法去获取到NativeModule的方法等信息,完成构建NativeModules对象。

//NativeModules.jsconst NativeModules = {};Object.keys(RemoteModules).forEach((moduleName) => {  Object.defineProperty(NativeModules, moduleName, {    configurable: true,    enumerable: true,    get: () => {      let module = RemoteModules[moduleName];      if (module && typeof module.moduleID === 'number' && global.nativeRequireModuleConfig) {        const config = global.nativeRequireModuleConfig(moduleName);        module = config && BatchedBridge.processModuleConfig(config, module.moduleID);        RemoteModules[moduleName] = module;      }      Object.defineProperty(NativeModules, moduleName, {        configurable: true,        enumerable: true,        value: module,      });      return module;    },  });});

看到这里想必大家都已经明白了,想要去动态加载NativeModule,只需要:

  • 1)在ModuleRegistry的modules_变量中增加新的NativeModule
  • 2)调用moduleNames更新modulesByName_同时得到moduleID
  • 3)执行代码向JS中MessageQueue对象的RemoteModules中添加新的NativeModule描述
  • 4)执行代码向JS中的NativeModules对象添加新的NativeModule描述

主要代码:

在NativeModules.js中为global添加了新的function,以便在JNI中调用该方法去动态向NativeModules对象中添加新的NativeModule描述:

//NativeModules.jsglobal.__UPDATE_NATIVE_MODULE__=function(moduleName){Object.defineProperty(NativeModules,moduleName,{configurable:true,enumerable:true,get:function get(){var module=RemoteModules[moduleName];if(module&&typeof module.moduleID==='number'&&global.nativeRequireModuleConfig){var config=global.nativeRequireModuleConfig(moduleName);module=config&&BatchedBridge.processModuleConfig(config,module.moduleID);RemoteModules[moduleName]=module;}Object.defineProperty(NativeModules,moduleName,{configurable:true,enumerable:true,value:module});return module;}});};

在JSCExecutor中增加函数,向JS中的RemoteModules添加元素以及执行JS方法去更新NativeModules,此处要注意在JS线程中去操作:

//JSCExecutor.cppvoid JSCExecutor::addNativeModuleDynamically(){          std::vector
names = m_delegate->moduleNames(); int index = (int)names.size() - 1; const char* moduleName = names[index].c_str(); m_messageQueueThread->runOnQueue([this, moduleName, index] () { auto global = Object::getGlobalObject(m_context); auto batchedBridgeValue = global.getProperty("__fbBatchedBridge"); auto batchedBridge = batchedBridgeValue.asObject(); auto remote = batchedBridge.getProperty("RemoteModules").asObject(); auto empty = JSObjectMake(m_context, NULL, NULL); JSObjectSetProperty(m_context, empty, String("moduleID"), JSValueMakeNumber(m_context, index), 0, NULL); remote.setProperty(moduleName, Value(m_context, empty)); auto nativemodules = global.getProperty("__UPDATE_NATIVE_MODULE__").asObject(); nativemodules.callAsFunction( {Value(m_context, JSStringCreateWithUTF8CString(moduleName))}); }); }

以上就是在RN源码读完后的一点思考,一点拙见,在构建更快更易扩展的RN应用的实践。如有不足之处欢迎大家斧正。

相关文章:


目前SDCC 2016前端开发专题的所有演讲嘉宾已全部确定,以下为嘉宾名单及演讲议题(排名不分先后),详情请见:。

  • Stackla前端团队Leader蒋定宇
    • 演讲主题:不断归零的前端人生
  • QQ音乐&全民K歌高级工程师袁聪
    • 演讲主题:全民K歌React Native最佳实践
  • 饿了么Node Team负责人黄鼎恒
    • 演讲主题:纯手工搭建一个高性能实时监控系统
  • 360奇舞团前端工程师钟恒
    • 演讲主题:使用Vue.js 2.0开发高交互Web应用
  • Ruff架构师、JavaScript专家周爱民
    • 演讲主题:有前端思想的物联网系统架构
  • 58到家高级前端工程师周俊鹏
    • 演讲主题:基于webpack的前端工程解决方案

想与这些专家现场面对面进行技术探讨吗?目前SDCC 2016大会门票8折销售中,团购更有优惠,是给辛勤工作一年的你,年终最好的礼物,或许这样,SDCC才能更真切地服务好开发者。【】

图片描述

你可能感兴趣的文章
hdu4565 So Easy!(矩阵快速幂)
查看>>
poj2528 Mayor's posters(线段树,离散化)
查看>>
线段树多lazy-tag(两个)
查看>>
hdu4578(三个更新操作,三个求值操作)
查看>>
并查集(初级)小结
查看>>
Treap
查看>>
相似图片搜索——感知哈希算法
查看>>
编译原理 词法分析
查看>>
计算机系统结构 计算机系统结构的基本概念
查看>>
计算机系统结构 计算机指令集结构
查看>>
计算机系统结构 输入/输出系统
查看>>
信息安全技术及应用 常规加密技术
查看>>
02-线性结构1 两个有序链表序列的合并
查看>>
HDU 1080 DP LCS
查看>>
HDU 3308 线段树+区间合并
查看>>
ASP.NET 入手页面控件及事件触发
查看>>
HDU 4123 树状DP+RMQ
查看>>
HDU 4121 模拟
查看>>
vim配置文件(持续更新)
查看>>
Fedora 16下添加终端快捷键
查看>>