React Native (一) 入门实践

时间:2022-08-29 14:43:08

上周末开始接触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的流程:

React Native (一) 入门实践

流程描述:

第一个场景是登录界面,输入账号和密码以后登录。登录后进入第二个场景,这是一个tabview容器,包含了三个tab:搜图书列表、电影排行列表和一个关于界面。列表视图支持上啦加载更多和下拉刷新,返回顶部以及跳转列表项的详情。关于界面里放了个静态的 swiper、说明以及一个登出的按钮,会返回到登录页。

React Native (一) 入门实践

说明:1.登录页做的是假的,后期可以加上session验证。2.搜图书和电影Top250排行都直接调用的豆瓣开放接口

项目结构如下:

React Native (一) 入门实践

目录描述:

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这方面正好相反,但思想基本相同。

四、登录

登陆页面很简单,主要是一些布局:

React Native (一) 入门实践

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实现的那种水波效果。

React Native (一) 入门实践

// 导入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上的介绍和示例代码。

六、搜图书

React Native (一) 入门实践

从最直观的列表开始介绍。列表采用ListView实现,ListView继承自ScrollView,扩展了一些功能,列表需要一个DataSource管理数据,我们取回的数据都必须用它改造一遍:

// 各种导入
// ... const ds = new ListView.DataSource({
// 更新策略(列表某项是否需要重新渲染):新旧值不相等
rowHasChanged: (v1, v2) => v1 !== v2
});

rowsHadChanged定义了怎么判断列表项是否有改变。虚拟DOMdiff算法是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有多少层都可以直接退回到上个场景:

React Native (一) 入门实践

这个虚拟机里面请求外网数据很缓慢,加载页面更慢...

八、关于

放了一个swiper组件,是一个第三方的组件:Github。下面放了一个登出按钮,点击以后弹出确任的对话框。点击确定,通过导航器的resetTo方法直接跳转到登录界面,并重置掉路由栈。

功能比较简单就不做过多介绍。

React Native (一) 入门实践

九、调试

ctrl+m或者打开菜单,点击'Debug JS Remotely',可以开启远程调试:

React Native (一) 入门实践

React Native (一) 入门实践

在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。