React Native (一) 入门实践
上周末开始接触react native,版本为0.37,边学边看写了个demo,语法使用es6/7和jsx。准备分享一下这个过程。之前没有native开发和react的使用经验,不对之处烦请指出。笔者认为从Vue过度到React应该是非常容易的,毕竟都是面向组件的,一通百通,只是每个框架的特性可能有所不同。本文的目的在于希望对在web开发有一定基础,又想涉及app开发的同学有帮助,共同进步。
一、环境安装
首先是开发环境安装,我是在win7/8 64位环境下安装的安卓环境。模拟器用的是android studio自带模拟器(android emulator),安卓API 24(7.0),因为我没有mac -.-。文中组件的使用也会以安卓的为主。具体的安装流程官网或中文网都有讲解,我也是按照那个流程走的。
这里说下安装和运行过程中经常出现但教程又没有提到的问题:
1.gradle-2.4-all.zip无法安装引发的错误,可以参考这里。
2.环境安装完成,app成功构建,但是更改代码react packager没有监听到文件变动并重新打包bundle,导致reload后无法更新app的问题。只有重新构建app并重启react packager才能解决。但这样太麻烦了,解决办法可以参考这里。我按照这个方式修改以后,环境表现就正常了。
3.安卓模拟器经常崩溃。
第一个问题与是否使用科学上网工具以及安卓环境配置和SDK有关系。第二个问题在win7环境下遇到过,修改数值后正常了,win8正常。第三个问题无解。
rn在windows下的安卓开发环境的坑还是比较多的。
4.这是一个比较完整的参考
二、demo app的功能和项目结构
首先看看这个demo的流程:
流程描述:
第一个场景是登录界面,输入账号和密码以后登录。登录后进入第二个场景,这是一个tabview容器,包含了三个tab:搜图书列表、电影排行列表和一个关于界面。列表视图支持上啦加载更多和下拉刷新,返回顶部以及跳转列表项的详情。关于界面里放了个静态的 swiper、说明以及一个登出的按钮,会返回到登录页。
说明:1.登录页做的是假的,后期可以加上session验证。2.搜图书和电影Top250排行都直接调用的豆瓣开放接口。
项目结构如下:
目录描述:
common - 公用工具(公用方法以及豆瓣接口Model的封装)
components - 全局公用组件(和业务无关)
images - 公用组件和业务视图组件的图片
views - 业务视图组件和业务公用组件(按照场景分文件夹)
views/MainView - 根组件(渲染了一个Navigator来控制整个App的场景跳转)
index.android.js - 入口文件(注册根组件,runApplication的前奏)
package.json - rn和第三方相关依赖
下面开始对每个场景进行拆分介绍。
三、入口文件和根组件
index.android.js这个文件按照官方文档的写法就可以,需要注意的是registerComponent方法传入的项目名一定要和命令行工具中执行react-native init xxx初始化命令时候输入的项目名称保持一致。
import React, {Component} from 'react';
import {AppRegistry} from 'react-native';
import MainView from './views/MainView';
AppRegistry.registerComponent('rndemo', () => MainView);
MainView.js作为根组件主要渲染了一个导航器来控制App场景跳转,所有业务视图组件都在它的控制下。
import React, {Component} from 'react';
import {View, Navigator} from 'react-native';
import LoginView from './login/LoginView';
export default class MainView extends Component {
render() {
return (
<Navigator
initialRoute={{name: 'LoginView', component: LoginView}}
configureScene={(route) => Navigator.SceneConfigs.PushFromRight}
renderScene={(route, navigator) => <route.component {...route.params} navigator={navigator} />}
/>
);
}
}
这个导航器类似于路由栈,是一种栈式结构,出栈和入栈的配合就能实现最基本的界面跳转,也提供有更高级的方法。
initialRoute要求指定默认显示的组件,这里import了登录视图组件,并指定为导航器的默认组件。confgureScene是导航器手势控制和动画配置,renderScene会渲染当前导航栈中被压入或者指定跳转的组件。
需要注意的是 <route.component {...route.params} navigator={navigator} /> 这里, {...route.params} 这是一个es6延展操作语法,能够进行不定参数接收、数组和对象的拆分等。能够进行批量赋值,可以将params对象的所有key->value结构转换成不同的prop。
比如:route.params值为 {a: 123,b: (a) => a + 1,c: true} ,最后的结果相当于 <route.component a={123} b={(a) => a + 1} c={true} navigator={navigator} />
虽然是语法范畴,但经常会用到,还是介绍一下。route.params可以用来给要跳转到的视图组件传递参数。如果数据过为复杂还是需要专门的数据流(状态)管理工具。这个demo因为数据简单,props传参已足够使用,也就没有用上redux,后面的各类组件也不会用到。有兴趣的话可以到别处了解一下redux。我在前段时间的vue.js组件化开发实践中对flux思路下的vuex和redux有一定介绍。vuex是专门针对vue的一个定制版,泛用性没有redux高,但和vue组件契合度高。redux这方面正好相反,但思想基本相同。
四、登录
登陆页面很简单,主要是一些布局:
jsx结构:
<ScrollView> <Image style={sty.back} source={require('../../images/login/banner_2.png')}/> <View style={[sty.back, sty.mask]}></View> <View style={sty.loginView}> <View style={sty.bannerWrap}> <Image style={sty.bg} source={require('../../images/login/banner_1.png')}/> <View style={sty.logoTextWrap}> <Animated.Text style={[sty.logoText, {opacity: this.state.fadeAnim}]}>Demo</Animated.Text> </View> <View style={sty.copyRightWrap}> <Text style={sty.copyRightText}>©2016</Text> </View> </View> <View style={sty.inputWrap}> <Text style={sty.inputTitle}>SIGN IN</Text> <TextInput {/* ... 账号 */}/> <TextInput {/* ... 密码 */}/> <Animated.View style={{opacity: this.state.fadeAnim}}> <TouchableOpacity style={sty.loginBtn} onPress={this.login.bind(this)} > <Text style={sty.loginBtnText}>登录</Text> </TouchableOpacity> </Animated.View> </View> <View style={sty.footer}> <Image style={sty.footerLogo} source={require('../../images/login/react_native_logo.png')} /> <Text style={sty.footerText}>Powered by React-Native</Text> </View> </View> </ScrollView>
const sty = StyleSheet.create({
back: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
height: height - 24
},
mask: {
backgroundColor: '#2B75DE',
opacity: 0.2
},
loginView: {
height: height - 24
},
bannerWrap: {
},
bg: {
width: 375 * refRat,
height: 235 * refRat
},
logoTextWrap: {
position: 'absolute',
bottom: 90
},
logoText: {
width: 375 * refRat,
textAlign: 'center',
fontSize: 40,
color: '#fff'
},
copyRightWrap: {
position: 'absolute',
bottom: 0,
paddingTop: 6,
paddingBottom: 6,
width: 375 * refRat,
borderTopWidth: onePx,
borderTopColor: '#fff',
opacity: 0.5
},
copyRightText: {
textAlign: 'center',
fontSize: 12,
color: '#fff'
},
inputWrap: {
alignItems: 'center'
},
inputTitle: {
paddingTop: 5,
paddingBottom: 20,
textAlign: 'center',
fontSize: 12,
color: '#fff',
opacity: 0.5
},
input: {
width: 230 * refRat,
textAlign: 'center',
color: '#fff',
borderBottomWidth: onePx,
borderBottomColor: '#fff',
},
loginBtn: {
marginTop: 30,
padding: 10,
width: 90 * refRat,
alignItems: 'center',
borderRadius: 20,
backgroundColor: '#FF8161'
},
loginBtnText: {
color: '#fff'
},
footer: {
position: 'absolute',
bottom: 0,
width: 375 * refRat,
alignItems: 'center'
},
footerLogo: {
width: 20 * refRat,
height: 20 * refRat
},
footerText: {
marginTop: 5,
textAlign: 'center',
fontSize: 12,
color: '#fff'
}
});
banner的文字和登录按钮有一个简单的淡入效果,这是通过Animated实现的,官方文档有更多的介绍。
登录以后跳转到TabView,使用的事导航器的restTo方法:
navigator.resetTo({
name: 'TabView',
component: TabView
});
它会跳转到新的场景,并重置整个路由栈。也就是说不能再直接通过pop出栈返回到登录页,必须指定跳转才行。这样可以避免在TabView进行了右滑等手势操作,无意间又回到了登录界面。
五、TabView
这是在github上找的一个第三方组件:react-native-tab-view,需要0.36以上版本支持。提供了顶部和底部tabbar,以及左右滑动来切换tab的容器,也支持点击tabbar按钮切换tab。tabbar点击自带有水波扩散你动画,类似于css3 scale实现的那种水波效果。
// 导入rn相关
// ...
// 导入需要的组件
// ...
export default class TabView extends Component {
constructor (props){
super(props);
const {navigator} = props;
this.state = {
index: 0,
routes: [
{key: '1',title: '搜图书'},
{key: '2',title: '电影排行'},
{key: '3',title: '关于'}
],
routeMap: {
1: <BookListView navigator={navigator} />,
2: <MovieListView navigator={navigator} />,
3: <AboutView navigator={navigator} />
}
};
}
handleChangeTab (index){
this.setState({index});
}
renderFooter (props){
return <TabBar tabStyle={{backgroundColor: '#6C6A6A'}} {...props} />;
}
renderScene ({route, navigator}){
return this.state.routeMap[route.key] || null;
}
render (){
return (
<TabViewAnimated
style={sty.container}
navigationState={this.state}
renderScene={this.renderScene.bind(this)}
renderFooter={this.renderFooter}
onRequestChangeTab={this.handleChangeTab.bind(this)}
/>
);
}
}
const sty = StyleSheet.create({
// 样式
// ...
});
导入搜图书、电影排行和关于这三个视图组件,简单配置便可运行。具体可查看github上的介绍和示例代码。
六、搜图书
从最直观的列表开始介绍。列表采用ListView实现,ListView继承自ScrollView,扩展了一些功能,列表需要一个DataSource管理数据,我们取回的数据都必须用它改造一遍:
// 各种导入
// ...
const ds = new ListView.DataSource({ // 更新策略(列表某项是否需要重新渲染):新旧值不相等 rowHasChanged: (v1, v2) => v1 !== v2 });
rowsHadChanged定义了怎么判断列表项是否有改变。虚拟DOM和diff算法是react的很巧妙的地方。新render出的虚拟DOM视图树会和当前的树进行比较,和得到不同地方,以类似于打补丁的方式打到当前的树上,也称之为'和解'过程。这就意味着只有真正改变了的地方才需要重新绘制,在有很多元素的时候能大幅提升渲染性能。
下面是BookListView类的结构:
// ...
export default class BookListView extends Component { constructor (props){ super(props); this.state = { showLoading: true, scrollY: 0, q: '红楼梦', start: 0, noMore : false, isLoading: false, data: [], dataSource: ds.cloneWithRows([]) }; } async setListData (data){ await this.setState({ data: data, dataSource: ds.cloneWithRows(data) }); } async componentWillMount (){ let data = await this.getListData(); await this.setListData(data); this.setState({showLoading: false}); } async getListData (){ this.setState({isLoading: true}); let {q, start} = this.state; let data = await searchBook({q, start, count}); await this.setState({ start: start + count, isLoading: false }); return data.books; } async listRefresh (){ await this.setState({start: 0,noMore : false}); let data = await this.getListData(); this.setListData(data); if (!data.length) ToastAndroid.show("没有数据", ToastAndroid.SHORT); } renderFooter (){ if (this.state.isLoading || this.state.data.length < 1) return null; if (this.state.data.length < count) return <ListGetMore />; return <ListGetMore isLoadAll={true}/>; } async onEnd (){ if (this.state.isLoading || this.state.noMore) return; let data = await this.getListData(); if (data.length < count) this.setState({noMore: true}); let newList = this.state.data.concat(data); this.setListData(newList); } search (){ let {q} = this.state; if (!q) return ToastAndroid.show("请输入书名", ToastAndroid.SHORT); this.listRefresh(); } toDetail (data){ const {navigator} = this.props; navigator.push({ name: 'BookDetailView', component: BookDetailView, params: {data} }); } onScroll (e){ let scrollY = e.nativeEvent.contentOffset.y; this.setState({scrollY}); } render (){ return ( <View> <View style={sty.searchWrap}> <TextInput style={sty.searchInput} ref={(SearchInput) => {_SearchInput = SearchInput;}} value={'' + this.state.q} placeholder={'输入书名'} autoCorrect={false} clearButtonMode={'while-editing'} underlineColorAndroid={'transparent'} autoCapitalize={'none'} onChangeText={(q) => this.setState({q})} /> <TouchableOpacity style={sty.searchBtn} onPress={() => { _SearchInput.blur(); _ListView.scrollTo({y: 0, animated: false}); this.search(); }} > <Text style={sty.searchBtnText}>搜索</Text> </TouchableOpacity> </View> <ListView style={sty.listWrap} ref={(ListView) => {_ListView = ListView;}} enableEmptySections={true} automaticallyAdjustContentInsets={false} dataSource={this.state.dataSource} renderRow={(rowData) => <BookListItem {...rowData} toDetail={this.toDetail.bind(this)}></BookListItem>} renderFooter={this.renderFooter.bind(this)} onEndReached={this.onEnd.bind(this)} onEndReachedThreshold={50} onScroll={this.onScroll.bind(this)} scrollEventThrottle={5} refreshControl={ <RefreshControl onRefresh={this.listRefresh.bind(this)} refreshing={this.state.isLoading} colors={['#ff0000', '#00ff00', '#0000ff']} enabled={true} /> } /> {this.state.scrollY > (height - 30 - 40 * refRat) ? <ListToTop listView={_ListView}/> : null} {this.state.showLoading ? <ActivityIndicator style={sty.loading} size={"large"} /> : null} </View> ); } }
在布局上ListView位于整个顶部搜索栏的下方,样式为flex布局,其内部子组件将按列排布。具体的属性和配置,以及dataSource数据集等在文档均有说明。
这里需要介绍下组件调用别的组件的方法:
BookListView作为导出的组件,它是由类中的state和方法,以及render方法返回的一个各种组件组成的复合组件,在实例中,我们点击了搜索按钮,输入框失去了焦点,让列表返回顶部,并触发了列表的搜索更新。都是通过refs这个属性来实现的。
<TextInput // ... ref={(SearchInput) => {_SearchInput = SearchInput;}}
<TouchableOpacity
// ...
style={sty.searchBtn}
onPress={() => {
_SearchInput.blur();
_ListView.scrollTo({y: 0, animated: false});
this.search();
}}
>
// ...
<ListView style={sty.listWrap} ref={(ListView) => {_ListView = ListView;}}
// ...
可以看到ListView这个组件类配置了一个ref属性,值为一个函数,入参即为这个ListView类在运行的时候实例化出的ListView对象,然后赋值给了_ListView这个变量,在点击搜索按钮的时候我们调用了_ListView的scrollTo方法来返回顶部,然后调用了BookListView类最后实例出的对象的search方法,也就是 this.search(); 。基于箭头函数的特性,this是在写的时候决定,因此它一定是指向BookListView对象的。如果是这样调用: <TouchableOpacity onPress={this.search.bind(this)} > ,就需要这个this通过es5的bind方法传入进去,强制要求search方法被调用的时候其内部this一定指向BookListView实例化出来的那个对象,因为这个search方法内部可能需要用到这个对象的属性和方法。如果不用bind方法来强行指定上下文环境,this指向的会是TouchableOpacity类实例化出的那个对象。这也是属于语法范畴。
ref属性可以不一定赋予一个函数作为值,一个字符串也是可行的。比如: ref={'ListView'} ,然后可以通过 this.refs['ListView'] 取到ListView这个对象,即可调用它的方法。当然,在使用时this一定要保证是指向BookListView对象的。
this的指向如果弄错,如果遇到这类报错,可以从这点开始排查,会经常出现’undefined is not a function‘这类报错。
基于React流程的setState方法是异步的(不受React控制的流程除外),这个一定要记住,如果在setState后直接获取state,值有可能还没有改变,要想保证改变,请使用es7的async/await特性,让异步操作用同步的方式来书写,其他异步方式也能解决。
列表的下拉刷新是通过配置refreshControl来实现的。
回到顶部按钮的显示与否是通过监听列表滚动的Y轴偏移来判断的,列表每次滚动会调用onScroll这个回调,从事件中获取到偏移,通过偏移量来决定按钮是否显示。由数据来驱动视图:
// ...
onScroll (e){ let scrollY = e.nativeEvent.contentOffset.y; this.setState({scrollY}); }
// ...
<ListView
// ...
onScroll={this.onScroll.bind(this)}
scrollEventThrottle={5}
// ...
{this.state.scrollY > (height - 30 - 40 * refRat) ? <ListToTop listView={_ListView}/> : null}
注意:
1.jsx里不能食用if else 等,只支持一个语句,所以有判断的地方必须使用三元表达式。
2.scrollEventThrottle是节流控制,类似于jquery的debounce-throttle插件,可以避免每一次的scroll都执行onScroll回调带来的性能问题,毕竟我们一秒钟的滚动时间会触发很多很多次onScroll事件。
上拉加载更多是通过滚动到底部的检测来触发事件。官方文档中都有配置介绍。
列表项BookListItem是封装的一个业务组件,通过传入props来提供渲染需要的数据。很简单布局的一个组件,这里不再详细说。
跳转图书详情视图BookDetailView是通过push压栈的方式进行的,之所以没有使用resetTo方法,是因为希望进入详情以后能通过pop出栈便能返回上一个视图:
// ...
toDetail (data){
const {navigator} = this.props;
navigator.push({
name: 'BookDetailView',
component: BookDetailView,
params: {data}
});
}
// ...
豆瓣开放接口的简单封装,很简单,就2个接口,哈哈。使用了rn环境自带的fetch:
// common/model.js
import {ToastAndroid} from 'react-native'; const _fetch = (url, param) => { let qstring = ''; for (let key in param) qstring += key + '=' + param[key] + '&'; url += '?' + qstring; return fetch(url); } const handle = async (url, param = {}) => { try { let response = await _fetch(url, param); let res = await response.json(); return res; } catch (error){ ToastAndroid.show('网络请求错误:' + error, ToastAndroid.LONG); return {books: [],subjects: []}; } } // 豆瓣开放API url const domain = 'https://api.douban.com'; const douban = { searchBook : domain + '/v2/book/search' , movieTop250 : domain + '/v2/movie/top250' }; /** * 搜索图书 * @param {q 查询关键字 tag 查询标签 start 本次偏移 count 本次条数} * @return {start 本次偏移 count 本次条数 total 总条数 books[] 图书集合} */ export const searchBook = param => handle(douban.searchBook, param);/** * 电影Top250排行 * @param {start 本次偏移 count 本次条数} * @return {start 本次偏移 count 本次条数 total 总条数 total 总条数 subjects[] 电影集合} */ export const movieTop250 = param => handle(douban.movieTop250, param);
七、电影排行
这个视图的列表相关组件以及详情组件与搜图书视图基本是一致的,只是少了搜索而已。
优化点:其实这两个视图的列表组件可以提取出公用的地方来抽象一次,封装为一个具有基本功能的公用List业务组件。搜图书列表和电影排行列表都可以继承自它,按需重写和扩展其他方法即可。
因为列表和详情基本与搜图书界面的功能基本一致,这里就只介绍一下webview内嵌豆瓣h5这里。从豆瓣取回的电影数据,有一个叫'alt'的字段存放了这个电影url,通过webview加载这个url,即可访问豆瓣的web页面:
<View>
<View style={sty.header}>
<TouchableOpacity
style={sty.backBtn}
onPress={this.back.bind(this)}
>
<Text style={sty.backBtnText}>{'<'}</Text>
</TouchableOpacity>
<Text numberOfLines={1} style={sty.headerText}>{this.props.title}</Text>
{this.state.canGoBack ?
<TouchableOpacity
style={sty.rightBtn}
onPress={this.directBack.bind(this)}
>
<Text style={sty.rightBtnText}>{'关闭'}</Text>
</TouchableOpacity> :
<Text style={sty.rightBtn}></Text>}
</View>
<WebView
style={sty.webView}
ref={'webview'}
automaticallyAdjustContentInsets={false}
source={{uri: this.props.url}}
javaScriptEnabled={true}
domStorageEnabled={true}
decelerationRate="normal"
startInLoadingState={true}
renderLoading={() => <ActivityIndicator style={sty.loading} size={"large"} />}
onNavigationStateChange={this.onNavigationStateChange.bind(this)}
onError={this.loadError.bind(this)}
/>
<Dialog
ref={'dialog'}
content={'刷新吗?'}
cancelAction={this.directBack.bind(this)}
okAction={this.reloadWebView.bind(this)}
/>
</View>
头部有三个元素:左边的后退按钮,中间的标题,右边的直接关闭按钮。
后端按钮可以控制webview的后退,只要webview的history还没有back到底,否则将直接通过整个app的导航组件回到电影详情界面:
async back (){
if (this.state.canGoBack){
this.refs['webview'].goBack();
} else {
this.directBack();
}
}
directBack (){
const {navigator} = this.props;
navigator.pop();
}
webview的每次history变化都会触发onNavigationStateChange事件,然后回调这个方法:
async onNavigationStateChange (navState){
var {canGoBack, canGoForward, title, loading} = navState;
await this.setState({
canGoBack,
title: loading ? '' : title
});
}
传入navState状态对象,我们可以取到canGoBack和canGoForward这两个布尔值,它们表示了当前webview的history状况,能否前进和后退。如果canGoBack为true,我们通过调用webview的back方法,可以实现history.back的功能。navState.loading为false表示加载完成,这时我们可以取到web页面的title作为header的title。
并且我们在state里维护了canGoBack这个状态值,当他为true的时候,会显示右侧的关闭按钮,点击这个按钮,可以直接回退到电影详情界面。好处在于:当我们在webview中点击web页面的连接前进了很多次之后,不想再不停的点击后退按钮,不论history有多少层都可以直接退回到上个场景:
这个虚拟机里面请求外网数据很缓慢,加载页面更慢...
八、关于
放了一个swiper组件,是一个第三方的组件:Github。下面放了一个登出按钮,点击以后弹出确任的对话框。点击确定,通过导航器的resetTo方法直接跳转到登录界面,并重置掉路由栈。
功能比较简单就不做过多介绍。
九、调试
ctrl+m或者打开菜单,点击'Debug JS Remotely',可以开启远程调试:
在js代码里console打出的信息都会在Console tab展示出来,报错和警报也会有。还Sources还可以打断点等。但是我开启远程调试后,有些时候非常卡,但帧数并不低。
除了菜单里的'Toggle Inspector'可以简易的查看一下元素以外,还可以安装react-devtools,下载地址:Github。也可以在chrome应用商店搜索安装(需要科学上网工具)。
这个调试插件我安装好以后,并没有使用起。即便在扩展管理里勾选了'允许访文件地址',在开启远程调试以后并没有探测到rn工程,但是访问豆瓣h5等使用react-js构建的站点时,可以嗅探到,并在chrome开发者工具里成功唤起了react-devtools的tab。Git上查看了issue,发现很多人也有这个问题,重新安装插件也没法解决...可能和chrome版本有关系,太高版本可能会出这个问题。
十、公共组件
这个demo抽象和封装了一些公共组件,但是没有提取完,还有优化点。这里介绍一下components目录下的Dialog对话框:
export default class Dialog extends Component {
constructor (props){
super(props);
this.state = {
show: false
};
}
async showDialog (){
await this.setState({show: true});
}
async hideDialog (){
await this.setState({show: false});
}
render (){
return (
<Modal
visible={this.state.show}
animationType='slide'
transparent={true}
onShow={() => {}}
onRequestClose={() => {}}
>
<View style={sty.modalStyle}>
<View style={sty.subView}>
<Text style={sty.titleText}>{this.props.title || '提示'}</Text>
<Text style={sty.contentText}>{this.props.content || '确定吗?'}</Text>
<View style={sty.horizontalLine}></View>
<View style={sty.buttonView}>
<TouchableHighlight
underlayColor='transparent'
style={sty.buttonStyle}
onPress={() => {
this.props.cancelAction && this.props.cancelAction();
this.hideDialog.bind(this)();
}}
>
<Text style={sty.buttonText}>取消</Text>
</TouchableHighlight>
<View style={sty.verticalLine}></View>
<TouchableHighlight
underlayColor='transparent'
style={sty.buttonStyle}
onPress={() => {
this.props.okAction && this.props.okAction();
this.hideDialog.bind(this)();
}}
>
<Text style={sty.buttonText}>确定</Text>
</TouchableHighlight>
</View>
</View>
</View>
</Modal>
);
}
}
十一、总结
1. windows下的安装环境的坑确实很多,而且这还只跑是在模拟器上,如果真机测试的话,不同机型、厂商应该会有适配的问题出现。mac下的表现应该要好得多,毕竟大家都说ios才是亲儿子。相信安卓方面以后还会不断的优化。如果需要一套代码同时跑安卓和ios两个平台,底层一定需要做组件封装,来屏蔽平台的差异。业务开发的时候,就不太需要考虑平台差异了。
2. 调试的提示信息有时候不太明确,需要挨着排查代码。
3. 布局和样式需要适应。
4. 组件使用上的限制文档没有明确提出,很多时候都是用到那里,那样写了,才发现不对。
5. html现在都讲究结构和样式分离,结构和逻辑分离。jsx又把我们拉回了以前的时代。
6. rn的生态圈还是很好的。
7. 其他 ...
以上希望对学习react native的同学有所帮助。不对的地方也请指出。
最后分享一个github上找到的一个不错的react native系列文章,包含作者原创和翻译的各种资料,原理、构架设计、性能优化、离线打包、增量更新都有介绍,入门rn以后可以看看,一定会有帮助的,可以基于此重构你的demo。