ReactNative之生成android debug安装包

我们使用react-native init创建一个空的项目,想要让它在android设备上跑起来,官网教程给的方案就是使用react-native run-android命令开启联网调试,或者使用./gradlew assembleRelease来生成安装包。 前者必须依赖开发机开启联网服务,然后手机设置好服务器和端口并连接,否则要么屏幕一片空白,要么提示红屏报错。找到android/app/build/output/appDebug.apk可以看到安装包内没有Bundle等资源。后者需要配置签名。实际上如果我们想查看apk实际运行状况,可以很快生成一个debug安装包。

打包bundle

这是最重要的一步,在项目根目录下执行

react-native bundle --entry-file index.android.js --platform android --dev false --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

这里最重要的是指定–bundle-output和–assets-dest两个参数。首先是–bundle-output,它必须放在android/app/src/main/assets目录下,如果该目录不存在就创建。必须指定文件名为index.android.bundle。 所有的图片资源必须放在android/app/src/main/res下,否则会无法找到图片资源而不能显示。

安装运行

到android目录下执行./gradlew installDebug即可

tips

在打包bundle那一步中,很多参数只是遵循默认的设置,比如bundle文件名叫index.android.bundle,位置在assets目录下,这些都可以按自己的需求来改,但前提是在java代码内也要做相应的调整,如果对这块不是很熟悉就按默认的来就行了。

不过–assets-dest目录必须在res目录下这个一定要遵守,因为Android系统要使用安装包内的资源,必须先转换成res id,如果放到别的文件夹下,没法转换,最后肯定找不到资源,也就没法显示图片了。

ReactNative中的Component的生命周期

本文介绍一下Component生命周期相关的一些函数,主要参考自官方文档

render

render函数是必须实现的,它用来渲染界面。一般会由this.props和this.state来控制如何显示。在这个函数里不能调用setState,否则会导致死循环,因为setState会导致render被调用。
render函数返回true,false,null,undefined都是合法的jsx语法,但它们都不会产生渲染。render函数应该避免太复杂耗时的操作。另外注意一下组件里如果想显示bool值,null,undefined时也需要显示转换成字符串才行。

mount

constructor

构造函数是最早被调用的,构造函数的第一句应该是super(props)。 state的初始化应该放在构造函数里。当然还有声明类的成员变量。

componentWillMount

当Component将要被加载时被调用,它在render之前被调用。在这个函数里执行setState的话,不会触发componentWillUpdate和render。因为render还没被调用,所以在这个函数里想通过ref获取子控件是不行的。componentWillMount为第一次render提供了准备数据的机会,我们可以放心的操作props和state。

componentWillMount也适合用来注册事件监听,假如有的事件在渲染时触发,那么在render前就注册显然更合适。componentWillMount的父节点会早于子节点被调用。

componentDidMount

当component已经被加载后调用,它在render之后被调用。所以在这个函数里执行setState会触发componentWillUpdate和render。对于同级节点,先渲染的componentDidMount会先被调用,对于父子节点,子节点的componentDidMount会比父节点先调用。前面说到render不应该调用setState来触发re-render,而componentWillMount调用setState又不会触发re-render,显然componentDidMount则完全没问题了,比如我们有一个component的尺寸未知,取决于另一个component的已知尺寸,就可以在componentDidMount里获取数据,计算好后setState来重新render。

constructor,componentWillMount和componentDidMount分别适合做什么事情

constructor显然适合做初始化,比如初始化state,成员变量,绑定成员函数等。而componentWillMount和componentDidMount的主要区别就是一个在render前,一个在render后。如果需要处理原始props和state,就应该放到componentWillMount中,比如使用props给state赋值,此外注册监听适合放在componentWillMount内。除此之外,大多数情况下的操作应该放到componentDidMount里,比如发起异步请求,开启计时器等等。

update

componentWillReceiveProps(nextProps)

当component的props被更新时被调用,但需要注意的是,update流程的所有回调触发的前提都是有update,也就是我们在父控件代码中改变子控件props但不触发update的话,这个回调也不会被调用的。例子很简单,比如

render(){
    return (<View>
        <TestComponent num=this._num>
        <Button onPress={()=>{this._num=999}}>
    </View>)
}

点击按钮改变了this._num,而this._num又作为TestComponent的props传递进去了,但因为并没有触发update,所以TestComponent的任何update回调都不会被触发。

另外需要注意,该回调被调用时,不一定props发生了变化,这里的没有发生变化有两种情形,一种是父控件没有改变子控件的props,例如父组件代码内调用setState刷新界面,此时子控件的componentWillReceiveProps也会被调用,但props没有发生变化。还有就是父控件传递过去的props是一个复杂数据类型,所以实际上是个引用,即使props发生了变化,this.props和nextProps也是相同的。

什么时候会用到这个回调呢?就是如果我们的state受props影响时,当传入的props发生了变化,我们在这里去修改state。在这里可以放心去执行setState

shouldComponentUpdate(nextProps, nextState)

通过这个回调函数返回true还是false来决定是否re-render。如果不重写的话会使用源代码的默认实现。默认实现可以参考源代码,它使用了fbjs/lib/shallowEqual.js内的shallowEqual方法来比较新旧props和state,只要props和state有一个改变了,就会触发re-render。 从shallowEqual代码看,首先是===强等判断,所以如果是同一个引用,那么不管内容怎么变,都认为是不变。例如

let old = this.state;
old.num=999;
this.setState(old);

这样不会触发re-render。然后是比较key-value,value的比较也是===强等。举个例子

this.state = {num:1};
...
this.setState({num:1});

这里虽然setState时将state变成了一个新的对象,但因为key-value完全一致,所以也不会导致re-render。如果value是复杂数据类型同理,例如

this.state = {obj:{num:1}}
...
let old = this.state.obj;
old.num = 999;
this.setState({obj:old})

这里虽然num变了,但并不会触发re-render。

以上是源代码中的默认实现,如果需要的话就重写通过限制一些re-render的触发条件,可以起到优化性能的作用。

componentWillUpdate(nextProps, nextState)

这个方法在每次re-render之前都会被调用,因为是在render之前,所以有点类似于componentWillMount,我们在这个回调里为下次render做好准备,通过this.props和this.state可以获取到当前的props和state,通过传进来的nextProps和nextState参数可以获得新的props和state。与componentWillMount不一样的是,在这里我们是可以操作UI的,但并不建议这么做,因为此时操作的是上次渲染的UI,它们有可能在下次render时就失效了。我们也不应该在这里调用setState,因为setState又会触发componentWillUpdate,这就造成了死循环,当然如果通过对nextProps或者nextState做判断是可以杜绝进入死循环的,但最好还是避免这样操作。

componentDidUpdate(prevProps, prevState)

正如前面说的componentWillUpdate对应componentWillMount,这里componentDidUpdate就对应着componentDidMount。它在render之后被调用,在这里就可以放心的获取和操作UI了。它的参数prevProps和prevState对应着componentWillUpdate里的this.props和this.state,而这个函数里的this.props和this.state就是当前的props和state,也就是componentWillUpdate里的nextProps和nextState。子节点的回调先于父节点被调用。在这里调用setState也需要非常小心,很可能会造成无限循环,如果确实需要的话,应该配合shouldComponentUpdate加以限制。如果我们需要对UI进行交互,比如获取某个UI的尺寸位置,这里是最合适的位置。

componentWillUnmount

当一个component不再被渲染时会被调用。例如

constructor(props){
    super(props);
    this.state={bVisible:true};
}

render(){
    return (<View>
        {this.state.bVisible ? <MyComponent /> : null} 
        <Button onPress={()=>{
            this.setState({bVisible:false})
        }}>
    </View>)
}

这个demo起始时MyComponent显示,点击按钮后不显示。不要误以为MyComponent只是被隐藏了,实际上它被销毁了,它所占用的内存都会被回收,此时它的componentWillUnmount会被调用。我们在componentWillMount或者componentDidMount里如果注册了一些监听,就需要在这里注销掉。与componentDidMount相反,父控件的componentWillUnmount会比子控件的先被调用。

ReactNative中的reducer函数中的浅拷贝和深拷贝

我们都知道reducer函数必须是纯函数,不能修改传入的state参数。先看一段示例代码

export function updateData(state=[], action) {
    switch(action.type){
        case Actions.ADD:
            return state.concat(action.data);
        case Actions.UPDATE:
            state[action.index] = action.data
            return state
        default:
            return state;
    }
}

这里state是一个简单的数组,ADD操作返回的是一个新的state,因为concat函数会创建并返回一个新的array。而UPDATE操作是修改了原state后返回原state。在demo中的表现就是,当reducer收到ADD时,会触发render刷新界面。而当收到update时,不会触发render,但如果再次收到ADD触发render刷新界面时,能看到UPDATE操作的数据已经被更新了。

结论就是

  • 只要返回的是原state,就不会触发render。这也正是我们default需要返回state本身的原因。即使state数据发生了变化,也不会触发render,但数据的变化被存储起来了。
  • 只要返回的是新state,就会触发render,即使数据完全不变

如果希望上面的UPDATE生效的话也很简单,把return state改成return state.slice(0)就可以了。但这种操作是不对的,虽然通过返回一个新的state来让render触发了,但它修改了state,它会导致的问题就是在component的componentWillReceiveProps函数里,nextProps和原来的props完全一致。reducer的原则是每个action对应着一个state,如果在action操作前后state相同,那就失去了这个特性了。

我们一般使用的Object.assign来构造一个新的state,Object.assign执行的就是浅拷贝而不是深拷贝,所以如果我们操作的state是一个比较复杂的结构,那么应该想办法手动执行深拷贝,否则使用浅拷贝的话,对应的内容就是同一份。

export function updateDeep(state={sth:[]}, action) {
    let sth = state.sth;
    switch(action.type){
        case Actions.ADD:
            sth.push({data:action.data});
            return Object.assign({}, {sth:sth})
        case Actions.DELETE:
            sth.splice(action.index, 1);
            return Object.assign({}, {sth:sth})
        case Actions.UPDATE:
            sth[action.index].data = action.data;
            return Object.assign({}, {sth:sth})
        default:
            return state;
    }
}

这个reducer函数直接修改了state.sth,虽然使用Object.assign返回了一个新的state,触发了render进行了刷新,但如果在componentWillReceiveProps函数里观察,就会发现this.props里的sth和nextprops里的sth是一模一样的,也就是执行action操作前后的state没有区分开来。

要解决上面的问题,可以有两种方案,一个是把reducer函数细分,确保在操作state时不会执行对对象进行浅拷贝。比如第二个例子的state改为[],用combineReducer来合并细分后的reducer函数,但如果数组成员是Object,而不是简单数据类型,就仍然有浅拷贝的问题,所以可以用第二种方案,先把state深拷贝一份,然后修改这个拷贝后的state并返回,如果state是个很复杂的数据结构,深拷贝一次代价会比较大。所以实际中应该这两种方案结合起来。

一个比较好的git分支管理方案

假如当前处于开发分支develop上,接到一个任务时,使用git的话,应该优先考虑开一个分支来做这件事情,假设我们开了一个分支b1,这个任务未必是可以一次开发完,可能中间去做了别的事情,导致写了一半的代码暂时提交上去,这样到这个任务完成时,这个分支有了commit1, commit2…..一系列的提交。但假设这是一件完整的单独的功能,我们需要让人review代码并作为一次完整的提交的时候,我们可以这么做:

在b1分支上先git reset –soft回到开分支的节点去,这样我们在这个分支上所有的改动,都变成了待commit的状态,然后我们找人review并提交。之后回到develop分支上,用git cherry-pick把刚才那个提交搬到develop分支上来,在develop分支上就只会看到一个干干净净的提交节点了。然后我们删除本地分支b1就可以了。

相比其它做法,比如在develop分支上使用merge来合并,它的缺点是查看develop分支的log时,会看到b1分支跟它交错,如果我们有很多个分支b1,b2….那develop分支的log看起来就会像蜘蛛网一样乱了。

当然我们可以用git merge –no-ff或者git rebase来保持develop分支的干净,但这样也有缺陷,我们在b1分支上有很多临时提交节点,也全部在develop分支上显示了,明明只是一个单独的任务,却有很多个commit节点,在查看log的时候,也会比较麻烦。

有的人不想临时提交,所以要做别的事情时,使用stash来暂存工作区,再次回到分支来时,使用stash pop恢复工作区,这是非常不可取的,一旦分支多了,在某个分支上stash,另一个分支上pop,就容易乱套了。所以还是尽量commit,但如果又不想让commit节点太多太乱的话,就可以用reset的办法来合并成一次提交,就干净了。

ReactNative中的listView使用介绍

今天下午稍微把listView的js代码看了一遍,大致总结一下它的接口和使用,官方文档上的介绍太过简单。listView的js源代码位于node_modules\react-native\Libraries文件夹内。

一个最简单的listView,代码如下

<ListView
      dataSource={this.state.dataSource}
      renderRow={(rowData) => <Text>{rowData}</Text>}
 />

dataSource

dataSource顾名思义是为了给listView提供数据源,这个类的源代码为ListViewDataSource.js。这个类除了存储数据外,还提供了4个接口给listView调用,这4个接口我们都可以自定义实现,其中2个有默认实现,2个没有。这段源代码就是

this._rowHasChanged = params.rowHasChanged;
this._getRowData = params.getRowData || defaultGetRowData;
this._sectionHeaderHasChanged = params.sectionHeaderHasChanged;
this._getSectionHeaderData =
  params.getSectionHeaderData || defaultGetSectionHeaderData;

rowHasChanged是一定要实现的,用来区分两个row是否相同,如果没有特殊需求的话,使用下面的就可以了

rowHasChanged: (r1, r2) => r1 !== r2

sectionHeaderHasChanged是如果listView有分节,则必须要的,没有特殊需求的话可以直接

sectionHeaderHasChanged: (s1, s2) => s1 !== s2

getRowData这个源代码有提供默认的实现,它用来获取每行的需要显示的数据,没有特殊需求的话不用实现
getSectionHeaderData源代码也有默认实现,顾名思义它用来获取每节的数据。

dataSource要拿来使用,除了实现必须的接口之外,还需要给它提供数据,这就用到了cloneWithRows和cloneWithRowsAndSections这两个方法,前者是后者的简化版,所以我们就拿最复杂的来举例说明。cloneWithRowsAndSections的函数声明为

 cloneWithRowsAndSections(
  dataBlob: any,
  sectionIdentities: ?Array<string>,
  rowIdentities: ?Array<Array<string>>): ListViewDataSource

它接受三个参数,并返回一个ListViewDataSource的实例对象。第一个参数dataBlob是传入的数据,它应该是一个Object,第二个参数是一个数组,它指定了一些key,也就是说dataBlob这个Object里,sectionIdentities里的keys对应的value才是需要显示的。rowIdentities则是一个二维数组,它指定了每节的数据中,哪些key对应的数据需要显示。前面我们说过,需要显示的数据是由getRowData和getSectionHeaderData来获取的,所以上面所说的是系统默认的实现,我们当然也可以自己去实现这两个接口,来自定义数据的获取行为。 以下是一段示例代码

let data=[["1","2", "3"],["4","5", "6"],["7","8", "9"]];
const ds = new ListView.DataSource({
    rowHasChanged: (r1, r2) => r1 !== r2, 
    sectionHeaderHasChanged: (s1, s2) => s1 !== s2
});
<ListView
    dataSource={ds.cloneWithRowsAndSections(data, ["0", "2"], [[0,1], [1,2]])}
/>

这段代码的行为是显示两个section,第一个section显示1和2,第二个section会显示8和9。这个例子里dataBlob是一个数组,它只是比较特殊的Object,key是0123…,如果是普通的key-value也是一样处理。cloneWithRows同理但更为简单,它只有两个参数,第一个参数dataBlob是传入的数据,第二个参数是一个数组,指定需要显示的rowIdentities。

对于这样一段代码

let data = ["111", "222", "333"]
<ListView
    dataSource={ds.cloneWithRowsAndSections(data)}
/>

显示效果会是有三个section,每个section有3个row,这是由于系统默认的getRowData和getSectionHeaderData实现方式决定的,思考一下就能明白了。

ListView

把dataSource弄明白之后,接下来看listView。从源代码的propTypes可以查看它能接收的props。

  • dataSource就是我们上面说到的,给它传递一个dataSource实例即可

  • renderSeparator函数声明为(sectionID, rowID, adjacentRowHighlighted) => renderable
    这个函数可以不用实现,系统会有默认实现,它的作用是绘制listView里每节中各个行的分割线。adjacentRowHighlighted为bool值,它的值由renderRow函数指定。

  • renderRow函数声明为(rowData, sectionID, rowID, highlightRow) => renderable,这是必须实现的,作用是绘制每个单元行,rowData就是从dataSource里获取来的数据,sectionID为所在节id,rowID为所在行的id。这三个数据都是dataSource传递过来的,所以像上面的那个dataSource例子,sectionID分别是”0”, “1”, “2”, rowID是0, 1, 2。最后的highlightRow参数是一个function,它可以在renderRow函数里适当的时候调用(比如按钮被点击),调用highlightRow(sectionID, rowID)可以让这个单元在上面的renderSeparator函数里接受到的adjacentRowHighlighted变为true。不要直接在renderRow函数里调用highlightRow函数,它会导致死循环然后调用栈溢出

  • renderSectionHeader函数声明为(sectionData, sectionID) => renderable,如果listView有分节,则实现此函数来绘制每节的头部,返回null或者undefined则不会渲染。

  • initialListSize用来指定起始时渲染多少个单元行,如果不指定的话,系统默认是10个。但首次渲染的单元行数量不完全取决于此,还取决于一个属性值DEFAULT_SCROLL_RENDER_AHEAD = 1000,这代表整个listView最多渲染多少个逻辑像素高,首次渲染的单元行数量取这两者中较大的那个。这是用来做性能优化的,如果确实碰到性能瓶颈时,需要将源代码完全看明白才能着手,所以建议在没完全看明白源代码之前没必要去碰这些参数。

  • scrollRenderAheadDistance这是一个数字,前面说过了,它参与限制首次渲染的单元行数量,默认值为1000,我们写一个demo,设置数据量比较大,然后让这个值为不同的值,可以看到首次渲染的单元行数量会不一样

  • onEndReached在整个listView滚动到最底部时会被调用的回调

  • onEndReachedThreshold是一个数字,指定当滑动了多少距离时会触发onEndReached事件

  • pageSize指定了每次事件循环时渲染的单元格数量

  • renderFooter和renderHeader函数声明为() => renderable,它们渲染整个listView的头部和底部,头部和底部会随着listView滑动。

  • renderScrollComponent函数声明为(props) => renderable,用来渲染装listView内容的容器,默认实现是直接返回了一个ScrollView

  • onChangeVisibleRows函数声明为(visibleRows, changedRows) => void,它在当前正在显示哪些单元行发生变化时被调用,visibleRows是一个字典,形式为{ sectionID: { rowID: true }},表示所有当前可见的单元行,changedRows也是一个字典,形式为{ sectionID: { rowID: true | false }},表示visible发生了变化的单元行

  • removeClippedSubviews 这是一个bool值,默认是true,用来优化数据量很大时的显示性能

  • stickyHeaderIndices,这是一个数字数组,用来指定哪些单元行在listview滑动时固定在屏幕顶端,只在ios平台而且是竖直方向的listView上才生效

  • enableEmptySections,这是一个Bool值,用来指定当一个没有数据的节是否需要展示,例如dataSource数据为[[1,2],[],[3,4]]时,中间的那个节(包括sectionHead和rows)是否会展示,如果不指定的话这个值为undefined,就不会显示。

以上就是listView的所有props了,也就是说明白了上面的内容,使用listView来完成功能肯定没问题了,但如果希望了解更多的细节,还是需要查看源代码,包括js端以及native端的源码。react-native的最新版本提供了flatList组件,它是listView的升级版,应该优先考虑使用flatList。

ps:当我们修改listView的数据源时,即使只是修改数组内的一项,也会导致整个listView都重新刷新,如果短时间内频繁更新数据源的话,可能导致性能瓶颈,应该优化为缓存数据后集中更新一次。

ReactNative之Image控件从js到java的追踪流程

我们以image控件为例,简单的介绍下一个系统控件的实现,方便进行自定义,以及了解它的内部实现。

我们要使用Image的话,第一步就是

import {Image} from "react-native"

我们需要找到源文件export出Image的地方,它位于node_modules/react-native/Libraries/Image目录下。基本上RN的js源代码都在这个Libraries目录下。

在Image.android.js里,文件结尾是

module.exports = Image;

证明我们使用的Image就是这里导出的。在这个文件的render函数里,可以看出来它使用了RKImage控件。然后查找RKImage的来源

var RKImage = requireNativeComponent('RCTImageView', Image, cfg);

可以看出来,RKImage是native的实现,所以我们到node_modules/react-native/ReactAndroid/src/main/java目录下搜索关键字RCTImageView,找到在哪里注册的。

观察一下搜索结果,可以看到有两个类RCTImageViewManager和ReactImageManager,他们都是导出到js层的类,且导出名字都是RCTImageView。

我们找一下这两个类是在哪里注册的,如果对流程比较熟悉的话,看到它们继承自ViewManager也已经知道了。在node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\shell\MainReactPackage.java的createViewManagers方法里可以看到

if(useFlatUi) {
    viewManagers.add(new RCTImageViewManager());
}else{
    viewManagers.add(new ReactImageManager());
}

所以根据useFlatUi的值,Image控件的native实现,可能是node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\views\image\ReactImageView.java或者node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\flat\RCTImageView.java

其余控件如果想追踪native实现,也可以按这个流程走就行了。

创建一个redux saga项目的简要流程

我们先新建一个RN项目

react-native init testrn

然后安装redux

cd testrn
npm install --save redux

在动手之前确保自己已经理解了redux的概念 redux官方中文文档

现在开始写代码了,第一步我们需要设计好state的结构,我们写一个很简单的demo,页面有两个text和两个Button,点第一个button修改第一个text的文字内容,点第二个button修改第二个text的文字内容。我们将state设计为

{
    "subState1":{"text":""},
    "subState2":{"text":""}
}

实际项目中的state肯定不会这么简单,可能会非常庞大,这就要求结构不能太草率,因为是树状结构,所以要掌握好分级的粒度,层级太少则可能单个子state过于复杂,因为一个reducer处理一个子state,则会导致reducer函数过于繁冗。层级太多则不易梳理结构,显得混乱。

然后我们给这两个修改text的动作设定两个action

const CHANGE1 = "change1";
const CHANGE2 = "change2";

然后根据这个结构来写reducer

function reducer1(state={"text":""}, action) {
    if(action.type === CHANGE1) {
        return Object.assign({}, state, {"text":"hello" + action.data});
    }
    return state;
}
function reducer2(state={"text":""}, action) {
    if(action.type === CHANGE2) {
        return Object.assign({}, state, {"text":"world" + action.data});
    }
    return state;
}
let reducer = combineReducers({"subState1":reducer1, "subState2":reducer2});

有的人在使用combineReducers时,习惯使用ES5的对象简写,例如

let reducer = combineReducers({reducer1, reducer2})

这样不是很好,这样写的话,state的实际内容就是

{
    "reducer1":{"text":""},
    "reducer2":{"text":""}
}

我们在使用state数据时,以reducer1为key,而reducer1本身又是reducer函数的名称,一不小心就容易搞糊涂。所以我们提倡在设计state时就想好子state的key,然后在combine时用key:reducer的形式。

为了方便使用redux,我们还需要react-redux库

npm install --save react-redux

我们使用connect这个API,来简化触发和监听action的操作。我们开始把这个页面写出来

import {Provider, connect} from "react-redux"

class TestRN extends Component{
    render(){
        return (<View>
            <Text>{this.props.subState1.text}</Text>
            <Text>{this.props.subState2.text}</Text>
            <Button title="btn1" onPress={()=>{
                this.props.createSagaAction1();
            }}/>
            <Button title="btn2" onPress={()=>{
                this.props.createSagaAction2();
            }}/>
        </View>)
    }
}

const mapStateToProps = (state, ownProps) =>{
    return state;
}
const mapDispatchToProps = (dispatch, ownProps)=>{
    return bindActionCreators({createSagaAction1, createSagaAction2}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(TestRN)

这段代码里,我们点击按钮后,并不是发出了CHANGE1和CHANGE2事件,而是createSagaAction1和createSagaAction2,这是因为我打算在接下来使用redux saga,让它收到而是createSagaAction1和createSagaAction2后,模拟一次异步操作,再由saga来发出CHANGE1和CHANGE2事件

redux saga是用来处理异步操作的,所以我们先写个模拟异步的功能函数

function delay(cb, time) {
    return new Promise((resolve)=>{
        setTimeout(()=>{
            cb(parseInt(Math.random() * 100));
            resolve();
        }, time);
    })
}
async function getNum(){
    let a;
    await delay((num)=>{
        a = num;
    }, 5000);
    return a;
}

然后开始redux saga相关,首先是安装saga

npm install --save redux-saga

然后开始代码,我们把点击按钮要发出的两个action实现出来然后监听并处理

import regeneratorRuntime from "regenerator-runtime";

const SAGA_ACTION1 = "sagaAction1";
const SAGA_ACTION2 = "sagaAction2";
function createSagaAction1(){
    return {"type": SAGA_ACTION1};
}
function createSagaAction2(){
    return {"type": SAGA_ACTION2};
}
function* sagaFunc1(){
    while(true) {
        yield take(SAGA_ACTION1);
        let a = yield call(getNum);
        yield put({type:CHANGE1, data:a});
    }
}
function* sagaFunc2(){
    while(true) {
        yield take(SAGA_ACTION2);
        let a = yield call(getNum);
        yield put({type:CHANGE2, data:a});
    }
}
function* mySaga(){
    yield fork(sagaFunc1);
    yield fork(sagaFunc2);
}

这里需要注意的就是saga函数必须是generators函数,不能用async, await。 sagaFunc1通过使用三个effect来进行操作,首先使用take来监听SAGA_ACTION1,然后使用call来阻塞调用异步函数,最后使用put来发出CHANGE1这个action,这时我们前面写的reducer收到CHANGE1这个action,就会修改state,从而让页面发生变化。

然后就是不要忘了import regeneratorRuntime,否则项目是跑不起来的,会出现红屏错误,提示cannot read property ‘mark’ of undefined。这是babel处理generators函数所需要的。

最后是创建store和使用provider了

let middle = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(middle));
middle.run(mySaga);

export default class TestRNContainer extends Component{
    render(){
        return (
            <Provider store = {store}>
            <TestRN />
            </Provider>
        )
    }
}

到此为止,一个新建项目使用redux saga就全部完成了

ReactNative之ref赋值应使用成员函数

在线上项目收集的js error里,发现有一个问题,有的ref成员变量可能变为null,然后就出了问题。先看下面的代码。

class Test extends Component{
    constructor(props) {
        super(props);
        this._ref = null;
    }

    render(){
        return (<View>
        <MyComponent ref={component=>this._ref=component}/>
        <Button onPress={this._onPress.bind(this)}/>
        </View>	
        )
    }

    _onPress(){
        this.setState({});
        this._ref.test();
    }
}

当_onPress被调用时,this._ref就可能变为null。

在facebook的官方文档里,最后一段是一个警告信息

If the ref callback is defined as an inline function, it will get called twice during updates, first with null and then again with the DOM element. This is because a new instance of the function is created with each render, so React needs to clear the old ref and set up the new one. You can avoid this by defining the ref callback as a bound method on the class, but note that it shouldn’t matter in most cases.

可以很清楚的看到,如果ref函数每次都是一个新函数,就可能导致当render函数被执行时,ref被赋值两次,第一次被赋值为null,第二次才被赋值为需要指定的component。上面例子里我们在_onPress函数里调用setState导致页面被重新渲染,然后_ref被重新赋值,有比较小的几率_ref会变为null,如果此时我们使用了_ref变量,就出现问题了。

解决的办法很简单,我们不应该让ref赋值函数每次都是新的函数,而不管是箭头函数,还是bind方法,每次都是生成一个新函数。所以我们在constructor函数里先绑定好一个成员函数来,然后使用它就可以了。这里就不写代码了,相信对js熟练的同学肯定很快就能写出来。

ReactNative中的pixelRatio

关于pixelRatio,官方文档有相关内容。通过

PixelRatio.get()

接口可以获取到设备的像素密度,但有的同学不清楚这个像素密度做什么用的,比如我发现在项目里,给图片设置尺寸时,都乘以了这个像素密度值,实际上这是不对的。

手机屏幕像素是大家都知道的,比如1080*1920,他们指的是物理像素。除此之外还有一个参数叫做设备独立像素,在这里可以看到部分移动设备的相关参数。这个设备独立像素,我们用Dimensions API可以获取到

import {Dimensions} from "react-native"
let size = Dimensions.get("window");
console.log("width: " size.width + " height: " + size.height);

而像素密度比pixelRatio,就是物理像素除以独立像素的值。比如我手里的坚果pro,它的物理像素是1080*1920,设备独立像素是360*640,所以它的像素密度是3。 在上面界面里也可以看到,例如ipad, GalaxyTab等平板一般像素密度是1,中断设备像素密度是2,高端些的设备像素密度就是3,当然也有一些设备像素密度是1.5或者2.5等。

首先我们要明确一点,我们在给图片设置尺寸时,是不需要写单位的,实际上单位就是独立像素。

假设有两个设备,它们的独立像素相同,都是360*640,那么我有一张图片,设置宽度为180,在这两个设备上都是占了一半屏幕宽。而如果有一个pad它的独立像素是768*1024,我们仍然希望它占一半屏幕的话,就需要把宽度设置为768*0.5=384了。例如对于背景图片,显然需要在任何设备上都占满屏幕,所以给它设置尺寸直接就是设备独立像素的值,也就是Dimension.get(“window”)得到的值。有的时候我们不希望随着屏幕变,例如那张图片仍然设置宽度为180,那么它在pad上宽度只占了23.4%。所以我们可以得知,给图片设置尺寸时,只决定于它在不同的独立像素屏幕上,需要显示成什么效果。最简单的方案是两种,要么设为固定值,要么跟屏幕独立像素成比例。否则就根据不同的屏幕来细分了。

那么这个像素密度拿来做什么用呢?

仍然说那两个设备,它们独立像素都是360*640,A设备像素密度为1,B设备像素密度为3,很容易得知A设备物理像素为360*640,而B设备物理像素为1080*1920。我有一张图片,美术给的尺寸是360*360,现在我给它设的尺寸宽度为180*180,根据上面所说,在这两个设备上图片都占屏幕一半宽,但A设备屏幕一半的物理像素是180,对于图片物理像素宽度360来说,缩小了一半。B设备屏幕一半的物理像素是540,对于图片物理像素宽度360来说,放大了1.5倍。不论放大还是缩小,都会导致图片显示模糊。如果要达到最好的显示效果,就应该给A设备提供180*180尺寸的图片,给B设备提供540*540尺寸的图片,这就是像素密度的作用。

是否要根据不同屏幕提供多套素材资源,取决于项目要求。我们在实际项目中可以使用pixelRation接口来实现物理像素和独立像素之间数值的转换,只要记住物理像素=独立像素*像素密度这个公式即可。

ReactNative拆分bundle实践

实践了一下最简单的ReactNative bundle拆分方案。将项目bundle拆分成RN源代码和业务代码,这样在热更新时不用每次都更新整个bundle。

首先新建一个项目testrn,将index.android.js内代码注释掉,只保留两行import

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View
} from 'react-native';

然后使用bundle命令打包成common.bundle,这就是RN源代码部分

react-native bundle --entry-file ./index.android.js --platform android --dev false --bundle-output ./output/common.bundle

这里dev可以选择true或者false,如果为了试验建议先设成true,正式使用时使用false

然后我们将index.android.js内注释代码打开,再使用bundle命令打包成total.bundle

我们在total.bundle内搜索项目名testrn,很容易找到这么一段代码:(或者使用软件例如compareBeyond对比)

__d(/* testrn/index.android.js */function(global, require, module, exports) {Object.defineProperty(exports, "__esModule", {
    value: true});
    var _jsxFileName = 'F:\\test\\testrn\\index.android.js';

    //中间代码省略...

    _reactNative.AppRegistry.registerComponent('testrn', function () {
    return testrn;
    });
}, 0, null, "testrn/index.android.js");

其实这是整个的一句,从当前的__d到下个__d

我们把这一段放到一个新建文件叫bussiness.bundle内,同时把common.bundle的最后两行剪切到bussiness.bundle的最后

;require(120);
;require(0);

然后我们修改一下C++代码,加载bundle时改成读取common.bundle和bussiness.bundle并连接起来,就可以了。我的修改代码如下,已经测试运行正常:

//node_modules\react-native\ReactAndroid\src\main\jni\xreact\jni\CatalystInstanceImpl.cpp
void CatalystInstanceImpl::jniLoadScriptFromAssets(
    jni::alias_ref<JAssetManager::javaobject> assetManager, const std::string& assetURL) {
    const int kAssetsLength = 9;  // strlen("assets://");
    auto sourceURL = assetURL.substr(kAssetsLength);

    auto manager = extractAssetManager(assetManager);
    // auto script = loadScriptFromAssets(manager, "index.android.bundle");
    auto script1 = loadScriptFromAssets(manager, "common.bundle");
    auto script2 = loadScriptFromAssets(manager, "diff.js");

    auto script = folly::make_unique<JSBigBufferString>(script1->size()+script2->size());
    memcpy(script->data(), script1->c_str(), script1->size());
    memcpy(script->data() + script1->size(), script2->c_str(), script2->size());

    if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) {instance_->loadUnbundle(
        folly::make_unique<JniJSModulesUnbundle>(manager, sourceURL),
        std::move(script),
        sourceURL);
        return;
    } else {
        instance_->loadScriptFromString(std::move(script), sourceURL);
    }
}