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

 

 

 

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

 

八、关于

放了一个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。

 

转载于:https://www.cnblogs.com/rock-roll/p/6099833.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值