野火🔥

RN学习1——前奏,app插件化和热更新的探索

2016/06/20 Share

React Native(以下简称RN)有大量前端开发者的追捧。前端开发是一个活跃的社区,一直尝试着一统前后端,做一个全栈开发,RN就是他们在客户端领域的尝试。

说是从零开始,但其实我还是懂一点点JS代码的,而且算是一个有经验的iOS、Android开发,对很多js和native交互的细节和特性还算了解,在QDaily里面也做过好多hybird的尝试,还经常用JSPatch做hotfix,总的来说,就是对hot update、插件化以及hybird编程非常非常感兴趣。RN也许是已知的开源方案中最好的一个吧。

一、写在最前

先开始提供个思路,作为一个移动客户端开发(区别于前端开发),我使用RN的目的根本上是为了插件化以及插件的线上热更新,对于前端开发那种全应用RN化的雄心是敬谢不敏的,同事对这种方案开发全app的能力也是存疑的(后文会解释原因)。

至于为什么要学习RN,主要是个人以后目标是做一个更加成熟团队的客户端负责人(or客户端架构师),需要对整个客户端横向技术栈都要有自己的理解和认识,现在已经能够在Android和iOS方面有一些自己的认识,进入RN领域其实也是顺理成章的了。

在未来RN的学习和使用过程中,我也会更加倾向于关于这种方案的内在原理、使用场景、使用边界以及一些其它优缺点方面,实际使用中也会先在一些比较轻量级的场景使用。

二、动态配置

客户端的新版本都依赖用户进行升级才行,如何能够第一时间让用户使用最新的版本是app开发者的永恒话题。

如果采用后台热更新,无论采用何种方式,我们的流程总是可以归结为以下三部曲:“从 Server 获取配置 –> 解析 –> 执行native代码”。

针对客户端程序的混合开发非常有必要,可以有效的进行app一些突发模块的开发和处理。已知的一般思路包括:

  • 1、简单的js bridge方式,内容呈现采用H5,增加一些和native的交互。现在好奇心日报就是这个方案,虽然简陋,但基本就是这个思路。上个东家微信在这方面也基本就采用了相似的方案,只不过在加密和安全方面做了更多的处理。

  • 2、后台以zip包得形式下发html、css、js和相关png组件,下载之后将所有资源按照原有目录结构放在app本地的一个http服务器中,app中得webview请求localhost的固定地址进行请求,这种请求会有AJAX跨域问题,一般只有native端完全接管网络请求可以使用,例如微信春晚红包就是这个方案。方案2基本是方案1的优化变种,由于兼容性好,入门简单,比较成熟。

  • 3、以zip包得形式下发html、css、js和相关png组件,客户端深订制一套方案,可以让js使用一些原生的UI组件能力,比较典型的就是增加下拉刷新组件。这套方案在淘宝、支付宝广泛使用,需要进行学习和研究,一方面在部署架构,另一方面是具体实现细节。阿里开源了其中的weex组件,基本思路也是这样的。

  • 4、纯js或者lua以patch的形式进行原生开发,通过反射调起原生代码。这个方案在iOS下可行,Android下面还存疑。而且是比较重的客户端耦合,好奇心日报现在用它进行一些紧急bug的处理。

  • 5、采用react-native进行混编,这种方案比较完整,就是原生native客户端集成react-native组件,通过后台订制下发一些使用js和css编写的资源模块,通过react-native框架进行渲染解析,成为原生应用。方案已经在天猫iPad客户端的某些模块上、QZone的某些使用,而且facebook的f8大会的app全部采用其编写,其生产能力是不容质疑的。方案原理和方案3、方案4类似,只不过中间封装了一个更加完善的中间件。

三、好奇心日报的现有尝试

1、基于css的ui配置方案。

基本思路是在css文件中定义ui组件的边距、颜色、字体、大小、背景色等等与UI相关的内容,在代码中通过宏(Android中用import static)进行UI渲染。

在app启动时,将css文件load进入内存,保存成k-v的形式,具体UI代码直接面对这些k-v数据结构即可。

这种方法的好处是:

  • 1、在适配夜间模式或者一些固定屏幕版本时,只需增加一套css文件即可处理;
  • 2、而且,由于修改资源文件,app不需要重新打包编译;
  • 3、同时,这种方案可以通过后台配置新的资源文件,在运行时替换掉内存中的约定key下的value,从而实现线上条件下的UI调整。

局限性也是非常明显的:对native代码的依赖太硬,能做的非常少,也就能改改样式,与热更新和热修复都扯不上关系。

2、基于js bridge的bybird方案

基于H5的webview hybird方案算是在性能上做一些妥协后比较成熟的方案了。

在android和iOS部分各封装一个js-bridge用于js和native的交互,相当于一个中间件。该中间件包含一个native部分和一个js部分,两部分沟通采用各自平台的特性方案,iOS采用订制request scheme并拦截request的方案,android采用@javascript的方案。通过中间件,前端开发者仅仅使用1套代码就可以兼容两个平台,两个平台各自暴露native方法给前端。

该方案实现的部分有:

  • 1、两个js-bridge,用于交互。并以此约定标准化调用接口。
  • 2、为webview发起的请求绑定cookie,以能够进行用户识别
  • 3、为webview发起的请求定制化UA,以区分浏览器还是app,以及android还是iOS。
  • 4、针对webview中所有资源(html、js、css、image)都进行本地的持久化,以提高访问速度。

方案好处都能看见,缺陷也很明显:效率太依赖机器性能以及浏览器内核(不过就算内核再好效率也是存疑的),同时针对原生部分的调用依赖于原生提供能接口,几乎是每增加一个功能,native部分也需要对应开发一边接口。

以上两个方案各有特征,但终究没离开采用约定好的配置信息就行混合编程的路子。从本质上来说,就是移动端和服务端约定了一套协议,但是协议内容严重依赖于应用内提供的能力,不利于拓展。尤其是方案1,只是在解析字符串,它完全不具备运行和调试的能力。方案2的效率问题也非常明显。

3、jspatch的热修复方案

iOS7以后,系统中包含了jscontext进行js语言的解析,相当于从读取配置文件到读取逻辑一个质的飞跃。

jspatch将js代码进行解析,并通过反射(invoker)调用objective-c的代码,几乎可以做所有oc可以做的事情(因为OC的runtime实在太强大)。

方案在QDaily中主要用于线上热修复。这个方案也有其不好之处:一个是只支持iOS,针对android还是无能为力;二是编写页面实在难用,难以调试,从整个生态来讲,也都使用比较轻量。

react native在iOS端实现远离和jspatch的远离基本一致,同事结合了方案2中语法的一些特性以及方案1中的配置特性。相信是现在已知的最优解决方案。

四、混编

决定学习之初直接上混编,因为这才是使用的目的,只有支持这个才具备插件使用的条件。

开一个官方demo——AwesomeProject,然后开始修改(如何安装和配置请自行google,官方教程很详细)。

1、OC调起RN

直接上代码,我们假设native页面本来好好的,点击了一个按钮跳到了一个RN的活动页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)viewDidLoad {
[super viewDidLoad];
UIButton* startRNVC = [[UIButton alloc] initWithFrame:CGRectMake(20, 50, 60, 40)];
[startRNVC setTitle:@"Start RN" forState:UIControlStateNormal];
[startRNVC addTarget:self action:@selector(gotoRN) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:startRNVC];
}
- (void) gotoRN {
NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"AwesomeProject"
initialProperties:nil
launchOptions:nil];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
[self.navigationController pushViewController:rootViewController animated:YES];
}

####2、OC中等待RN调起的部分
RN有比较完整的调用代码,只要按步骤做就好了。关键是RCT_EXPORT_MODULE这个宏,会在class的load方法中进行register,这点和js-bridge的方案很像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation SpringBoard
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(gotoIM:(RCTResponseSenderBlock)callback)
{
AppDelegate* appdelegate = (AppDelegate*) [UIApplication sharedApplication].delegate;
UINavigationController *controller = (UINavigationController*)[appdelegate.window rootViewController];
CDLoginVC *loginVC = [[CDLoginVC alloc] init];
[controller pushViewController:loginVC animated:YES];
callback(@[[NSNull null]]);
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
@end

3、RN部分调起Native

RN还不是很理解,就把代码都贴上来了。这里会在页面启动时候直接alert出来,点击会再跳回native部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
'use strict';
var React = require('React');
var RN = require('react-native');
var {
Image,
AppRegistry,
ListView,
StyleSheet,
Text,
View,
AlertIOS,
} = RN;
var styles = RN.StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
function setup(): React.Component {
AlertIOS.alert(
'Foo Title',
'My Alert Msg',
[
{text: 'Foo', onPress: function FooClick() {
var SpringBoard = RN.NativeModules.SpringBoard;
SpringBoard.gotoIM((events) => { });
}},
{text: 'Bar', onPress: () => console.log('Bar Pressed!')},
]
)
class AwesomeProject extends React.Component {
render() {
return <View style={styles.container}>
<Text>This is a simple application.</Text>
</View>;
}
}
return AwesomeProject;
}
AppRegistry.registerComponent('AwesomeProject', setup);

总结:事实上,效果很好,轻松实现了混编和调用,考虑到RN在调起Native部分需要OC进行代码定制化编写,所以未来考虑增加其与jspatch的协作,增强其能力;android部分还需要继续研究,相信不是问题(QZone已经在进行相关的研究和应用了)。

五、学习计划和曲线

我个人是一个双平台开发者,同时对hybird编程比较感兴趣,也做过一些研究和尝试,所以RN中关于平台接口部分、原理以及js-native交互部分学习是比较平缓的。但我javascript只是一点三脚猫功夫,更别提ES6、React一个有一个生僻而又让然懵逼的名字,还有node.js等等神一样的存在…这部分估计学习要非常陡峭。

本着先难后易的原则,学习部分会优先进行ES6标准的基本语法和习惯开始,然后通过改造QDaily一个模块进行React和RN的熟悉,在其中不断学习f8的代码和使用方式,并在过程中将RN彻底融入原有app项目中。

这过程可能需要一本基于ES6的javascript的书籍,一套比较权威的RN教程和文档,f8的代码以及针对其的解读,还有若干大牛的博客和社区。

六、本文结束

mark一下本文的参考文献,以及可能要学习的一些东西:

CATALOG
  1. 1. 一、写在最前
  2. 2. 二、动态配置
  3. 3. 三、好奇心日报的现有尝试
    1. 3.1. 1、基于css的ui配置方案。
    2. 3.2. 2、基于js bridge的bybird方案
    3. 3.3. 3、jspatch的热修复方案
  4. 4. 四、混编
    1. 4.1. 1、OC调起RN
    2. 4.2. 3、RN部分调起Native
  5. 5. 五、学习计划和曲线
  6. 6. 六、本文结束