有一段时间没有写东西了,因为最近项目开始尝试使用React Native(以下简称RN)来开发,所以这段时间一直在研究,目前为止开发的内容不多,所以使用过的东西也不算多,这里也只是做个简单的记录
这里我打算从以下几个方面来讲:
1.背景介绍
2.环境的配置
3.RN所需要知道的知识
4.RN与原生的交互
5.本地调试与本地打包调试
6.远程热更新
7.踩坑记录
8.相关资料
0x00 背景介绍
RN是Facebook在React.js 2015大会上公布开源的,它是基于开源框架React.js来实现的,它支持了iOS和Android两大平台,解决开发者们编写重复代码的痛点,实现了所谓的跨平台开发,Write Once , Run Anywhere,这是目前很多开发者所追求的,特别是一些独立开发者或者项目快速迭代的团队,可以尝试使用RN来开发,另外包括方便的npm管理,快速的调试等等
那么既然优点这么明显,为什么大部分的团队还是采用传统的iOS、Android开发呢,踩过坑的同学都知道,首先在支持上还做得不够完善,在使用组件时,RN原有提供的组件往往不能很好的支持,与原生组件多少存在着差异,而且在使用第三方组件时,又会因为长期不更新的原因,存在很多坑,对于新手来说,根本不知道坑在哪,完全无从下手。另外RN的性能也不能和原生的相提并论,特别是列表组件在渲染大量数据时,流畅性方面还是原生更加优越,而且并非所以代码iOS和Android都能公用,如果某个组件只支持某一个平台,那你必须分开编写代码,实际上还是存在重复代码,除此之外学习的成本以及团队RN推广等等原因都需要考量,但是我相信,跨平台开发始终是一个趋势,RN整个社区也在不断的发展,相信未来我们会实现真正意义上的跨平台开发~
0x01 环境配置
相对于Android的环境配置过程来说,iOS可以说是简单轻松…出现的问题要少很多
首先我们需要安装Homebrew
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
然后安装node和watchman(用于监测文件系统的变更)
brew install node
brew install watchman
RN的命令行工具react-native-cli
npm install -g react-native-cli
如果遇到权限问题,只要前面加个sudo即可
sudo npm install -g react-native-cli
yeah~that's all~我在配置的过程中,基本没有报错,如果有出现配置问题的话,请自行Google一下,看看大家的解决方法
0x02 RN所需要知道的知识
RN的运行机制
在开始写代码之前,我们需要了解RN的运行机制是怎么样的,这样写起来思路会更加清晰
首先,程序需要有个入口,我们可以创建很多的组件,但是有且只有一个组件用来做为程序的入口,RN的入口则类似于iOS的main.m,在iOS里我们会在main函数里设置应用程序类的代理类
return UIApplicationMain(argc, argv, nil, NSStringFromClass([KDAppDelegate class]));
同样,RN里我们需要注册入口的名称,并且这个名称要和原生的初始化RN界面时的入口名称保持一致
// 引用navigation使用的组件
import React, { Component } from 'react';
import {
AppRegistry,
...
} from 'react-native';
// 创建navigation类
class navigation extends Component {
// set compnent
}
// 注册navigation为程序的入口
AppRegistry.registerComponent('navigation', () => navigation);
在iOS原生这边需要用到RN的地方,我们需要初始化它
NSURL *jsCodeLocation =
[[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];
// [NSURL URLWithString:@"http://172.17.9.188:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"navigation"
initialProperties:nil
launchOptions:nil];
self.view = rootView;
Tip:
jsCodeLocation 是RN资源加载的路径,我们有两种方式去加载,一种是加载本地的js文件及其他资源文件,一种是我们将其打包成bundle文件,前者的优势在于方便调试,后者是用来打包发布上线用
moduleName
是对应于RN的入口名字,且这个是唯一的,那我们如果原生有多个入口需要初始化不同的RN界面,那该怎么办呢?这就用到了initialProperties
,它是字典类型,我们可以将入口作为路由,在initialProperties
里传入我们需要初始化的界面名称,入口获取到名称之后,渲染对应的界面即可
RN组件的生命周期
在RN里面,所谓的界面应该称作类或者组件更为合适并且组件也有它的生命周期,和iOS里的viewWillAppear、viewDidDisappear等等很像,下面生命周期内容取自于http://www.race604.com/react-native-component-lifecycle/
我们可以把组件生命周期大致分为三个阶段:
第一阶段:是组件第一次绘制阶段,如图中的上面虚线框内,在这里完成了组件的加载和初始化;
第二阶段:是组件在运行和交互阶段,如图中左下角虚线框,这个阶段组件可以处理用户交互,或者接收事件更新界面;
第三阶段:是组件卸载消亡的阶段,如图中右下角的虚线框中,这里做一些组件的清理工作。
下面来详细介绍生命周期中的各回调函数。
getDefaultProps
在组件创建之前,会先调用 getDefaultProps()
,这是全局调用一次,严格地来说,这不是组件的生命周期的一部分。在组件被创建并加载候,首先调用 getInitialState()
,来初始化组件的状态。
componentWillMount
然后,准备加载组件,会调用componentWillMount()
,其原型如下:
void componentWillMount()
这个函数调用时机是在组件创建,并初始化了状态之后,在第一次绘制 render()
之前。可以在这里做一些业务初始化操作,也可以设置组件状态。这个函数在整个生命周期中只被调用一次。
componentDidMount
在组件第一次绘制之后,会调用 componentDidMount()
,通知组件已经加载完成。函数原型如下:
void componentDidMount()
这个函数调用的时候,其虚拟 DOM 已经构建完成,你可以在这个函数开始获取其中的元素或者子组件了。需要注意的是,RN 框架是先调用子组件的 componentDidMount()
,然后调用父组件的函数。从这个函数开始,就可以和 JS 其他框架交互了,例如设置计时 setTimeout
或者 setInterval
,或者发起网络请求。这个函数也是只被调用一次。这个函数之后,就进入了稳定运行状态,等待事件触发。
componentWillReceiveProps
如果组件收到新的属性(props),就会调用componentWillReceiveProps()
,其原型如下:
void componentWillReceiveProps(
object nextProps
)
输入参数 nextProps
是即将被设置的属性,旧的属性还是可以通过 this.props
来获取。在这个回调函数里面,你可以根据属性的变化,通过调用 this.setState()
来更新你的组件状态,这里调用更新状态是安全的,并不会触发额外的 render()
调用。如下:
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}
shouldComponentUpdate
当组件接收到新的属性和状态改变的话,都会触发调用shouldComponentUpdate(...)
,函数原型如下:
boolean shouldComponentUpdate(
object nextProps, object nextState
)
输入参数nextProps
和上面的 componentWillReceiveProps
函数一样,nextState
表示组件即将更新的状态值。这个函数的返回值决定是否需要更新组件,如果 true
表示需要更新,继续走后面的更新流程。否者,则不更新,直接进入等待状态。
默认情况下,这个函数永远返回 true
用来保证数据变化的时候 UI 能够同步更新。在大型项目中,你可以自己重载这个函数,通过检查变化前后属性和状态,来决定 UI 是否需要更新,能有效提高应用性能。
componentWillUpdate
如果组件状态或者属性改变,并且上面的 shouldComponentUpdate(...)
返回为 true
,就会开始准更新组件,并调用 componentWillUpdate()
,其函数原型如下:
void componentWillUpdate(
object nextProps, object nextState
)
输入参数与 shouldComponentUpdate
一样,在这个回调中,可以做一些在更新界面之前要做的事情。需要特别注意的是,在这个函数里面,你就不能使用 this.setState
来修改状态。这个函数调用之后,就会把 nextProps
和nextState
分别设置到 this.props
和this.state
中。紧接着这个函数,就会调用 render()
来更新界面了。
componentDidUpdate
调用了 render()
更新完成界面之后,会调用 componentDidUpdate()
来得到通知,其函数原型如下:
void componentDidUpdate(
object prevProps, object prevState
)
因为到这里已经完成了属性和状态的更新了,此函数的输入参数变成了 prevProps
和 prevState
。
componentWillUnmount
当组件要被从界面上移除的时候,就会调用 componentWillUnmount()
,其函数原型如下:
void componentWillUnmount()
在这个函数中,可以做一些组件相关的清理工作,例如取消计时器、网络请求等。
下表是生命周期函数的调用次数,以及能否使用setSate():
RN的设计模式
目前设计模式也非常多,如Flux,Reflux,Redux,Relay,Marty,不过以上都不是很了解,可以参考ReactNative的组件架构设计学习了解一下,由于做客户端的同学接触的最多的是MVC,MVVM、MVCS等等,所以我觉得选用类似MVCS的模式可能更加适合新手的学习,比如写组件时,通常我们会创建一个组件,里面会包含数据的处理,页面的渲染,样式的设置,网络请求,当这些内容过多时,组件就会显得特别臃肿,所以我们需要将其拆分开为数据模型(Model),页面渲染,样式设置,网路请求(Service),这里的页面渲染和样式设置,不能算是称作为iOS里的Controller和View,应该跟前端一样,在html文件里面写布局,css文件里面写样式,感觉像是MVCS和前端的融合
0x03 RN与原生的交互
在写RN时不免会遇到与原生交互,下面我分JS调用原生、原生调用JS来讲
JS调用原生
在调用原生时,我们需要实现RCTBridgeModule
和RCT_EXPORT_MODULE()
;
RCT_EXPORT_MODULE();
则是一个宏定义,返回moduleName,并且调用+ load
方法注册
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
例如我们增加一个bridge方法,获取版本号,getVersion
为方法名,callback
是原生回调给JS的内容
RCT_EXPORT_METHOD(getVersion : (RCTResponseSenderBlock)callback) {
NSString *version =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
callback(@[[NSNull null], @[version]]);
}
然后返回方法的队列为主队列
- (dispatch_queue_t)methodQueue {
return dispatch_get_main_queue();
}
在JS文件里,我们可以定义一个全局变量
var ZanIntentModule = NativeModules.ZanIntentModule;
然后在使用的时候调用我们在原生时定义方法
ZanIntentModule.getVersion(
(callback) => {
// do some thing
})
原生调用JS
老版本的调用方式为,但是接口被标记为deprecated:__deprecated_msg("Subclass RCTEventEmitter instead");
[self.bridge.eventDispatcher sendAppEventWithName:kGiftReloadData body:nil];
新版本的调用方式为
ZanEventEmitter *emitter = [[ZanEventEmitter alloc] init];
emitter.bridge = self.bridge;
[emitter sendEventWithName:kGiftReloadData body:nil];
但是新版本坑的是,直接这样调用时bridge居然是nil,网上说用单例,但是也不行...所以我还是用老版本的调用方法,有哪个大神知道怎么用新版本接口调用的正确姿势,请留言交流哈
然后在实现RCTBridgeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
return [[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];
// return [NSURL URLWithString:@"http://172.17.9.94:8081/index.ios.bundle?platform=ios"];
}
在对应的组件里,需要在componentWillMount
增加监听
componentWillMount() {
this.eventEmitter = NativeAppEventEmitter.addListener(
'GiftReloadData',
() => this._reloadData()
);
}
对应的也需要移除掉监听
componentWillUnmount() {
subscription.remove();
}
然后原生发送action之后,会触发我们设定好的reloadData()
方法
0x04 本地调试与打包调试
在编写的过程中,也需要进行调试,调试有两种方法:一种是本地调试,一种是打包调试
本地调试
我们在加载bundle时,需要替换成你的ip地址,端口号不要变
[NSURL URLWithString:@"http://172.17.9.94:8081/index.ios.bundle?platform=ios"]
如果你是在真机上调试,你需要开启HTTP代理,填写你的ip地址和端口号
在终端上,先进入到你的项目目录(与node_modules目录同级),然后开启服务
yzydeMacBook-Pro:shangjiaban-ios yzy$ npm start
你修改了某处之后,在模拟器上点击Shake Gesture
或者快捷键,在真机上只要摇一摇就可以
在模拟器弹出框里选择
Roload
,这样就会重新加载你本地的JS文件如果你想查看JS里面的log日志,你可以选择
Start Remote JS Debugging
,在chrome浏览器里就能看到输出的日志了打包调试
另外一种就是打包调试,但是比较麻烦,首先我们要讲bundle加载方式改为
[[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];
然后在终端里面,输入
yzydeMacBook-Pro:shangjiaban-ios yzy$ react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output ./xxx/bundle/index.ios.jsbundle --assets-dest ./xxx/bundle
--bundle-output ./xxx/bundle/index.ios.jsbundle
指的是输出的bundle文件路径
[20:54:43] <START> Building Dependency Graph
[20:54:43] <START> Crawling File System
[20:54:43] <START> find dependencies
[20:54:48] <END> Crawling File System (4712ms)
[20:54:48] <START> Building in-memory fs for JavaScript
[20:54:48] <END> Building in-memory fs for JavaScript (230ms)
[20:54:48] <START> Building in-memory fs for Assets
[20:54:48] <END> Building in-memory fs for Assets (154ms)
[20:54:48] <START> Building Haste Map
[20:54:48] <START> Building (deprecated) Asset Map
[20:54:48] <END> Building (deprecated) Asset Map (66ms)
[20:54:48] <END> Building Haste Map (154ms)
[20:54:48] <END> Building Dependency Graph (5261ms)
transformed 372/372 (100%)
[20:54:49] <END> find dependencies (6402ms)
bundle: start
bundle: finish
bundle: Writing bundle output to: ./Koudaitong/bundle/index.ios.jsbundle
bundle: Copying 5 asset files
bundle: Done writing bundle output
bundle: Done copying assets
当看到这样的信息的时候,说明已经打包成功了,再将生成的bundle文件夹以Create folder references
形式加到工程里,然后就可以run了
Tip:
在真机调试时,需要在Edit Scheme里在Run模式里,将Build Configuration改为Release模式
0x05 远程热更新
这块网上的方案大同小异,因为目前我们还是采取本地打包加载的方式,还未上热更新,所以在这不好多做说明,等上了热更新之后,我再来补充~
0x06 踩坑记录
踩坑最多是应该是使用上的
1.RN系统的组件并不是所有都是共用的,比如segment支持iOS,不支持Android,Alert分为iOS和Android等等,所以还是要写重复的代码
2.ListView不支持iOS原生的滑动操作,需要使用第三方库,但是第三方库不能控制只编辑一个Cell
3.由于原先iOS和Android的代码仓库是分开的,所以接入RN时,JS文件也是跟着仓库走的,这样iOS和Android会存在重复代码,并且目前两个人分别接iOS和Android,写JS时,有时并不共享,容易代码写着写着就有差异了,偏离了Write Once , Run Anywhere的初衷
0x07 相关资料
React Native
React Native 中文网
汇集了各类react-native学习资源、开源App和组件
写给 iOS 开发者的 React Native 学习路线
江清清的技术专栏
网友评论