责编:陈秋歌,关注前端开发领域,寻求报道或者投稿请发邮件chenqg#csdn.net。
欢迎加入“CSDN前端开发者”微信群,参与热点、难点技术交流。请加群主微信「Rachel_qg」,申请入群,务必注明「公司+职位」。另可申请加入CSDN前端开发QQ群:465281214。
SDCC 2016 中国软件开发者大会将于11月18日在北京京都信苑饭店召开,本届大会汇集100+讲师,设置了12大专题论坛。本文作者QQ音乐&全民K歌高级工程师袁聪,已受邀担任SDCC 2016前端开发专题演讲嘉宾,分享全民K歌React Native最佳实践。
以下内容为会前袁聪给大家带来的营养小餐,技术大餐即将在SDCC 2016软件开发者大会上呈现。
React Native(以下用RN简称代替)的开源是App开发的一个里程碑式的事件,它为App开发提供了新的开发模式和思路。RN既有传统Hybrid框架的优势,又提供了统一的Native体验,吸引着越来越多的项目去实践。
全民K歌去年十月份完成了RN的接入实践,至此已经有一年多了。现在的我们已经不再满足简单的接入和优化,我们开始大胆尝试,做一点不一样的RN,今天我们来就聊一聊全民K歌不一样的RN。
这里先给大家上两份小菜,大餐在11.18号下午SDCC前端开发专题会场,不见不散。
P.S. RN的代码已经被分析来分析去太多遍了,相关资料很多,为避免冗长枯燥,本文只提示基于0.35版本的关键代码。
为什么要分包?
关于性能瓶颈,我们先来看一张图:
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打到个基础包里:
再带上这份manifest来打包原版的Hello World,最终生成的文件如下:
surprise,业务Bundle就是这么小。是不是等不及来看看怎么实现的了?
下面分析一下分包涉及的源码修改和实现(源码根据0.35版本):
首先我们在local-cli添加了两个参数:
–manifest-output: 打包时把bundle包含的模块导出生成为一个manifestFile(打基础包)
–manifest-file : 打包时传入指定的manifestFile进行过滤(打业务包)
在打包生成时(拿到所有依赖模块列表后),如果有传入manifestOutput,把模块列表按固定格式输出一份JSON文件(模块名: 模块id)
在依赖解析中require模块时,如果有传入manifestFile,require 已有的模块时使用manifestFile中对应的id。比较方便就是在getModuleId()中做一个处理。
最后在打包前,如果有传入manifestFile,从resolutionResponse按照其过滤掉已存在的模块。
主要的源码涉及就是Bundler和Bundle部分,其次还有cli的添加,过滤掉polyfill的重复插入等。
需要注意的问题:
id重复:
目前的RN打包模块使用递增数字id,分开打包时id就会重复。我的做法是把基础包的最大id输出来接着递增,或者多个业务包时给id加前缀(–id-prefix)。增强方案也可学习webpack使用filename hash做id。
好了,前端同学已经帮我们拆出来的多个Bundle,那么客户端怎么去加载呢?
先来看下一个简单的JavaScript VM模型:
JSContext是JavaScript执行的上下文,由GlobalObject管理。我们在同一个GlobalObject对应的同一个JSContext中执行JavaScript代码,执行一个和执行多个JavaScript是没有区别的,所以上面多个包的加载完全没有问题。
这里不赘述RN的Bundle加载流程,最终用到了JSC的一个API——JSEvaluateScript去执行Bundle,我们只需要针对API封装,提供一个方法给上层调用即可,当然注意执行顺序,在基础Bundle加载完再去执行业务Bundle,iOS和Android实现基本类似。
听说RN支持随时发版,这么好?先来一斤需求!
一开始听到这种提问,第一反应是去解释,RN提供了基础的Native组件(这里我们把Native Module和封装的View统称为Native组件),RN的动态发布是基于已有的Native组件的动态发布,我们可以根据业务的需要去扩展Native组件,但是不可能在一个版本里面把所有可能用到的Native组件都封装好。后来源码读得多了,也开始思考,是不是真的可以随时无限制的去做需求?如何在不更新版本的基础上如何让RN真正的实现需求快速迭代开发,放飞产品同学们的想象力呢?
动态添加Native组件并将其动态加载。
动态添加Native组件,这对于iOS来说可能有点难度,但是对于Android,插件和热补丁大家都玩了这么多年了,这只是个小问题,很容易解决。
那么如何去动态加载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.cpp
ModuleRegistryHolder::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.cpp
ModuleRegistry::ModuleRegistry(std::vector<std::unique_ptr > modules)
: modules_(std::move(modules)) {}
这里需要留意的是ModuleRegistry中的moduleNames()函数,这是一个关键的方法,返回值是所有NativeModule方法的名字,这在后面向JS中注册NativeModule时举足轻重。
//ModuleRegistry.cpp
std::vector<std::string> ModuleRegistry::moduleNames() {
std::vector<std::string> 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.cpp
JSCExecutor::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<std::string> 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.js
const BatchedBridge = new MessageQueue(() => global.__fbBatchedBridgeConfig);
//MessageQueue.js
type 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.js
const 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,只需要:
主要代码:
在NativeModules.js中为global添加了新的function,以便在JNI中调用该方法去动态向NativeModules对象中添加新的NativeModule描述:
//NativeModules.js
global.__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.cpp
void JSCExecutor::addNativeModuleDynamically(){
std::vector<std::string> 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前端开发专题的所有演讲嘉宾已全部确定,以下为嘉宾名单及演讲议题(排名不分先后),详情请见:SDCC 2016前端开发专题讲师、议题大揭底。
想与这些专家现场面对面进行技术探讨吗?目前SDCC 2016大会门票8折销售中,团购更有优惠,是给辛勤工作一年的你,年终最好的礼物,或许这样,SDCC才能更真切地服务好开发者。【注册参会】