├── README.md ├── .DS_Store ├── rnvm └── README.md └── BestActualCombat ├── .DS_Store ├── images ├── you.png ├── .DS_Store ├── rnvm.png ├── moles1.png ├── moles10.png ├── moles2.png ├── moles3.png ├── moles4.png ├── moles5.png ├── moles6.png ├── moles7.jpg ├── moles8.png ├── myctrip.png ├── qrcode.jpg └── appprepare.png └── articles ├── .DS_Store ├── chapter4.md ├── chapter5.md ├── chapter6.md ├── chapter2.md ├── chapter3.md └── chapter1.md /README.md: -------------------------------------------------------------------------------- 1 | # ReactNative 2 | React Native最佳实战 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/.DS_Store -------------------------------------------------------------------------------- /rnvm/README.md: -------------------------------------------------------------------------------- 1 | rnmv的github地址:[rnvm](https://github.com/GammaGos/rnvm/) 2 | -------------------------------------------------------------------------------- /BestActualCombat/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/.DS_Store -------------------------------------------------------------------------------- /BestActualCombat/images/you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/you.png -------------------------------------------------------------------------------- /BestActualCombat/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/.DS_Store -------------------------------------------------------------------------------- /BestActualCombat/images/rnvm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/rnvm.png -------------------------------------------------------------------------------- /BestActualCombat/articles/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/articles/.DS_Store -------------------------------------------------------------------------------- /BestActualCombat/images/moles1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles1.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles10.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles2.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles3.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles4.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles5.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles6.png -------------------------------------------------------------------------------- /BestActualCombat/images/moles7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles7.jpg -------------------------------------------------------------------------------- /BestActualCombat/images/moles8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/moles8.png -------------------------------------------------------------------------------- /BestActualCombat/images/myctrip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/myctrip.png -------------------------------------------------------------------------------- /BestActualCombat/images/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/qrcode.jpg -------------------------------------------------------------------------------- /BestActualCombat/images/appprepare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GammaGos/ReactNative/HEAD/BestActualCombat/images/appprepare.png -------------------------------------------------------------------------------- /BestActualCombat/articles/chapter4.md: -------------------------------------------------------------------------------- 1 | ###moles-packer工具发布了 *React Native 拆包工具* 继上一篇[React Native Bundle拆分](https://github.com/GammaGos/ReactNative/blob/master/BestActualCombat/articles/chapter3.md)之后,在上期的“后续”中说到,要做一个无侵入式的拆包工具,陆陆续续收到不少同学的咨询,大家都想了解下无侵入式的拆包工具的进展如何了。那么今天我们就将无侵入式的拆包工具分享给大家。 github地址:[moles-packer](https://github.com/ctrip-moles/moles-packer) 首先,为什么叫moles-packer呢,是因为这个工具是与我们moles框架配套的打包和拆包工具。今天我们先贡献出moles-packer部分功能,后续我们会将功能不断的放出,之后我们也会对其源码甚至整个moles框架做开源处理。那么moles框架又是什么呢,moles框架是基于React Native开发的一个打通Android、iOS、H5、SEO的一套框架,并且内置了Hybrid Api、性能采集、错误捕获、用户行为等功能,可以说是基于React Native开发的一套解决方案,目前是携程开发React Native项目的必选框架。 在讲moles-packer的使用之前,我想首先要对moles-packer的开发者们表达感谢:储诚栋(代号CCD)、蒋竞(代号竞哥哥)等同学,是他们没日没夜的艰苦奋战,才使的我们的moles-packer赶上了Ctrip App 6.17的末班车,正式的投入了生产环境,发挥其重要的价值。我们在Ctrip App 6.17的我携频道的站内信和机票频道的低价页面中都有使用了React Native技术和我们的moles-packer拆包功能,感兴趣的同学可以装下Ctrip App 6.17版本体验一下哦。其次由于业务的扩展,我们框架研发部前端框架组又有几个名额啦,有想要和我们一起玩耍React Native的同学要抓紧喽,名额有限哦。 接下来,我们就正式开始讲解moles-packer的使用。我们知道React Native的打包,是将所有文件都打在了一个bundle文件中,其中主要包括raect、react-native、业务代码这三部分代码。 在实际开发中,通常的需求就是希望将react、react-native打在一起,业务代码打在一起,然后在通过native代码将其加载在一起进行运行。 通过上面的描述,我们应该可以分析出需要做哪些事情: 1、react、react-native打成common.bundle 2、业务代码打成bu.bundle 3、native部分实现多bundle的加载和执行 所以,moles-packer的打包就是围绕着这几件事情展开的。 首先需要通过npm安装moles-packer包 $ npm install moles-packer -g 接着就可以愉快的拆包啦,在React Native的跟目录下执行如下命令,别忘了把对应的参数换掉哦。 $ moles-packer --input /path/to/project --entry index.ios.js --output /path/toild --common true 参数说明: `input`:项目目录(默认为当前目录) `entry`:入口文件名称(默认为 index.js) `output`:输出目录(默认为 .ild 目录) `common`:是否打common包(默认为false) -------------------------------------------------------------------------------- /BestActualCombat/articles/chapter5.md: -------------------------------------------------------------------------------- 1 | ## moles-packer 开源啦。。。。。。 2 | 3 | 4 | 经过几天的连续奋战,moles-packer终于开源啦。感谢各位在moles-packer发布之初给予的鼓励和支持。有你们的鼓励我们才会有现在动力,我们是有信心将moles-packer做到功能最强大,使用最方便的React Natie打包、拆包工具。 5 | 6 | 我们将moles-packer开源到携程的公众账号ctripcorp上,上面还有很多有意义的开源项目,有种moles-packer找到组织的感觉,并将moles-packer升级到了0.1.3版本。 7 | 8 | 现在向诸位回报下目前的进展和计划 9 | 10 | ### 已完成 11 | 12 | + 支持react、react-native打成common.jsbundle 13 | 14 | + 支持除react、react-native以外的业务代码打成bu.jsbundle 15 | 16 | ### 待完成 17 | 18 | + 单业务线拆包的支持 19 | 20 | + common bundle的生成可配置化 21 | 22 | + 业务模块多bundle的支持 23 | 24 | + load拆包和merge拆包iOS的支持 25 | 26 | + load拆包和merge拆包Android的支持 27 | 28 | + 多业务线拆包的支持 29 | 30 | ### 资讯 31 | 32 | 33 | 很高兴的告诉大家,我们的moles-packer即将推出0.2.0版本,该版本的API将会调整为更符合大家习惯的格式,如下: 34 | 35 | ``` 36 | input:"", 37 | output:"", 38 | paths:{ 39 | "common":"./common.jsbundle", 40 | "foo/bar/bip":"foo/bar/bip/index.js", 41 | "foo/bar/bop":"foo/bar/bop/main.js", 42 | "foo/bar/bee":"foo/bar/bee/index.json", 43 | "react":"react", 44 | "react-natve":"react-native": 45 | }, 46 | common:{ 47 | name: "common", 48 | include: ["react","react-native"] 49 | }, 50 | business: [ 51 | { 52 | name: "foo/bar/bip", 53 | exclude: [ 54 | "foo/bar/bop" 55 | ] 56 | }, 57 | { 58 | name: "foo/bar/bop", 59 | include: [ 60 | "foo/bar/bee" 61 | ] 62 | } 63 | 64 | ], 65 | dev: 66 | 67 | ``` 68 | 69 | 70 | 71 | ### 相关的地址 72 | 73 | [github地址](https://github.com/ctripcorp/moles-packer/):https://github.com/ctripcorp/moles-packer/ 74 | 75 | [npm地址](https://www.npmjs.com/package/moles-packer):https://www.npmjs.com/package/moles-packer 76 | 77 | ### 最后,大家如有什么想法或者建议欢迎发我们的TS组邮箱 78 | 79 | -------------------------------------------------------------------------------- /BestActualCombat/articles/chapter6.md: -------------------------------------------------------------------------------- 1 | ## React Native框架之Moles 2 | 3 | 4 | Hello, 大家好! 很高兴今天能在这里跟大家一起交流。首先,我先自我介绍一下,我是携程技术中心框架研发部前端框架组的魏晓军,主要负责PC端、移动端的框架开发以及一些性能优化等等工作。现阶段主要从事React Native的研究和引入工作。今天我带来的分享主题是React Native框架之Moles。希望通过分享一些Moles框架的知识和经验,让大家了解到我们携程现在在React Native方面的实战经验,从而给大家提供一些在今后的开发中有用的经验。 5 | 6 | 本次分享的内容包括三个方面: 7 | 1、Moles框架在React Native和我们主App的集成中起到了什么作用? 8 | 2、Moles框架是如何打通Android、iOS、H5、SEO,让我们一套代码跑在多个平台上? 9 | 3、Moles框架的组成以及原理是怎样的? 10 | 这些内容将通过以下几个部分的讲解来一一给大家进行解答: 11 | 12 | + React Native的现状 13 | + Moles 框架的出现 14 | + Moles 框架的组成 15 | + Moles 框架的功能 16 | + Moles 框架的原理简析 17 | + Moles 框架的使用 18 | + Moles 框架的案例 19 | + 开源计划。 20 | 下面我们就步入正题吧。 21 | ### React Native的现状 22 | 23 | React Native是2015年3月份Facebook开源的一个Native上的一个框架。那么何以他现在会这么火呢。 24 | 我们先来看看他有什么优点。 25 | 首先,对于做前端的我来说,最吸引我的就是可以用javascript来开发Native应用了。之前javascript只可以开发浏览器上的一些功能,随着Node.js的出现,又让javascript走向了服务端,现在React Native的出现又让javascript走向了Native端。所以要用现在一个时髦的词来形容javascript的话,我觉得“全栈”真的不为过。 26 | 其次,React Native是Facebook将ReactJS的思想移植到Native端。所以React Native就拥有了RectJS的很多特性,如:组件化思想、Virtual Dom技术以及JSX与Flexbox组合完成的布局等等,同时React Native又引入了热更新机制、CssLayout机制,让开发人员尤其是Native开发人员眼前一亮。 27 | 有优点也有缺点,我们再来看看他的不足。 28 | 我们知道React Native先出了iOS版本,然后出了Android版本。两个版本之间存在很多的差异性,甚至有好多组件都会带有平台的后缀,这使得开发人员必须要为这两个平台写不同的代码。此外,对于公司来说,在移动上的投入,不仅有Native还会有H5,而在H5上React Native并没有考虑。从MVC框架的角度来看,React Native只做了View这一层,那么Controller、Model、Router还需要做。从App的完整性来看,只学会React Native并不能开发一个健全的App。它的更新策略、Hybrid API的提供,配套的UI组件、监控机制等等这些都没有。 29 | ### Moles 框架的出现 30 | 31 | 伴随着React Native项目的开发,逐渐的Moles框架就形成了。mole [məʊl] 小鼹鼠,是种凿洞能力非常强的啮齿类动物。把框架称为之为Moles,也是寄希望我们的框架能像mole一样,能够打洞,能够打通Android、iOS、H5、SEO这几个平台。当然一个mole的能力是有限的,所以我们取其复数形式Moles。 32 | 33 | 如果说当前移动端的三大痛点是:性能、动态性、多端适配的话。那么我认为React Native解决了性能、动态性,而我们Moles则解决了多端适配的问题。 34 | 35 | Moles的目标是要尽可能的做到在H5端开发的内容可以直接运行在Native上,在Native端开发的内容也可以直接运行在H5上。 36 | 37 | ###Moles 框架的组成 38 | 39 | 该框架主要由三部分组成 40 | 41 | ####-moles-web 42 | 该部分主要是为H5服务,是将React Native在Android、iOS中`没有差异化`的Components、APIs提取出来,单独封装成一个Library供H5端来使用。这样做的好处是,这个Library只在H5上会是使用到,在Native是不需要的,以减少框架在Native的体积。 43 | ####-moles-cui 44 | 该部分主要是是将React Native在Android、iOS中`有差异化`的Components、APIs提取出来,并且添加一些公司定制化的组件进去,包括:UI组件、监控组件、采集组件、路由组件等等。moles-cui可以说是Moles框架的核心部分,它不但Native开发需要使用,在H5上的开发也需要使用。 45 | ####-moles-cli 46 | 47 | 该部分主要包括Moles项目的初始化、编译、打包等功能 48 | 49 | 下面是moles-web与moles-cui的一个关系图 50 | 51 | ![](../images/moles10.png) 52 | 53 | 54 | 55 | ###Moles 框架的功能 56 | 57 | Moles框架的功能可以用下图来说明 58 | 59 | ![](../images/moles3.png) 60 | 61 | 主要涵盖了对不同平台的适配、对底层API的调用以及对App中性能和错误的监控等等。 62 | 63 | 那么Moles在携程主App中所处的位置如何呢? 64 | ![](../images/moles8.png) 65 | 66 | 如上图所示它就是BU开发人员和React Native、Ctrip React Native 的一个桥梁。让开发人员更专注于自己的业务逻辑,而不必为React Native的更新问题、不同平台的兼容性问题等等而烦恼。 67 | 68 | 69 | 70 | ###Moles 框架的原理简析 71 | 由于Moles涉及的内容众多,如:路由的设计、页面生命周期的设计、打包的设计等等。这里我们仅以组件的设计为例,来简析其实现原理。 72 | 73 | 要做到Native和H5代码共享,通常想到的做法有两种: 74 | 75 | 1、Native组件运行在H5上,如: 76 | 77 | ``` 78 | class HelloWorld extends Component{ 79 | render(){ 80 | return( 81 | 82 | HelloWorld 83 | 84 | ) 85 | } 86 | } 87 | ``` 88 | 89 | 2、H5组件运行在Native上,如: 90 | 91 | ``` 92 | class HelloWorld extends Component{ 93 | render(){ 94 | return( 95 |
96 | HelloWorld 97 |
98 | ) 99 | } 100 | } 101 | ``` 102 | 103 | Moles中组件的设计采用了做法1的思路,就是将Native上支持的View、Text、Navigator等组件运行在H5上。要实现Native组件运行在H5上,需要解决两个难点:1、组件化 2、组件的生命周期。我们刚开始的时候就讲到,React Native上的组件化思想是Facebook将ReactJS的思想用在Native上。这就为我们能在H5上实现Native的组件奠定了基础。所以我们完全可以借助ReactJS来开发这些组件,但是在实际的开发中,发现ReactJS的体量实在是太大了,所以我们最后采用了携程开源的react-lite框架。 104 | ###Moles 框架的使用 105 | 为了减少大家的学习成本,Moles框架在设计方面尽量采用大家比较熟悉的语法和命令。要使用Moles,需要先安装moles-cli。 106 | 107 | 下面是第一次使用moles-cli的简单流程: 108 | 109 | 安装moles-cli 110 | 111 | ``` 112 | $ sudo cnpm install @ctrip/moles-cli -g 113 | 114 | ``` 115 | 初始化Moles项目 116 | 117 | ``` 118 | $ moles init ProjectName 119 | ``` 120 | 安装项目依赖 121 | 122 | ``` 123 | $ cd ProjectName 124 | $ cnpm install 125 | ``` 126 | moles-cli初始化后的目录结构和react-native-cli初始化出来的结构几乎一样,唯一不同的是多了一个web目录,该目录主要是为H5服务。 127 | 128 | 下面来看下简单的代码使用情况 129 | 130 | ``` 131 | import React,{ 132 | View 133 | } from 'react-native' 134 | 135 | ``` 136 | 上面的代码若在Native端会调用React Natie提供的react-native模块,在H5端会调用moles-web模块 137 | 138 | ``` 139 | import{ 140 | Application, 141 | Page 142 | }from 'moles-cui' 143 | 144 | ``` 145 | 上面的代码为moles-cui的使用,不论在Native端还是H5端,都需要通过引入'moles-cui'模块来使用。 146 | 147 | 以运行iOS项目为例 148 | 149 | ``` 150 | $ moles run-ios 151 | ``` 152 | 153 | 打包、拆包项目 154 | 155 | ``` 156 | $ moles packer 157 | 158 | --input /path/to/project 159 | 160 | --entry index.ios.js 161 | 162 | --output /path/to/build 163 | 164 | --bundle bu.bundle 165 | 166 | --common true 167 | 168 | 参数说明: 169 | 170 | input:项目目录(默认为当前目录) 171 | entry:入口文件名称(默认为 index.js) 172 | output:输出目录(默认为 ./build 目录) 173 | bundle:默认输出文件名称与入口文件同名,也可指定文件名 174 | common:是否打common包(默认为false) 175 | 176 | ``` 177 | 178 | 179 | ###Moles 框架的案例 180 | 181 | 目前我们的Moles框架已在携程的主App上投入生产,有兴趣的同学可以安装我们的携程App 6.17,进入我的携程频道,其中的站内信页面就是基于Moles框架开发的。现在还有攻略、游轮等频道也在陆陆续续的接入中。相信在不久的将来,大家会在携程的各大频道上看到Moles的身影。我们也希望Moles能成为携程乃至业内基于React Native开发项目的首选框架。 182 | 183 | 下面是基于Moles开发的效果图: 184 | 185 | ![](../images/myctrip.png) 186 | 187 | 我携频道 188 | 189 | ![](../images/you.png) 190 | 191 | 攻略频道 192 | 193 | 194 | 195 | 196 | ###开源计划 197 | 198 | 记得上次分享的时候,就有不少同学咨询开源的事情。我这里简述下,我们Moles未来将是一个开源的框架。是一个为开发React Native项目提供解决方案的开源框架。我们会将Moles框架的相关产品逐步的开源给大家。 199 | 200 | 目前我们已将Moles框架的打包、拆包工具moles-packer开源在了github上。 201 | 202 | 203 | **关于moles-packer的一些介绍:** 204 | 205 | moles-packer 是由携程框架团队研发的,与携程Moles框架配套使用的React Native 打包、拆包工具,同时支持原生的 React Native 项目。 206 | 207 | 当前版本:`0.1.3` 208 | 209 | [github地址](https://github.com/ctripcorp/moles-packer): 210 | 211 | `https://github.com/ctripcorp/moles-packer` 212 | 213 | [npm地址](https://www.npmjs.com/package/moles-packer): 214 | 215 | `https://www.npmjs.com/package/moles-packer` 216 | 217 | **想了解更多内容,请关注我们的Moles公众号:** 218 | 219 | ![Moles](../images/qrcode.jpg) 220 | -------------------------------------------------------------------------------- /BestActualCombat/articles/chapter2.md: -------------------------------------------------------------------------------- 1 | # ReactNative性能调优 2 | 3 | 自从ReactNative出世,虽然官方一直尽可能的优化其性能,为了能让其媲美原生App的速度,但是现实感觉有点不尽人意。接下来介绍下实践中遇到的一些性能问题以及优化方案。以下对性能参数的依据是来自于React-Native自带的FPS Monitor. 4 | 5 | 6 | 7 | ### 1.Navigator页面切换动画优化 8 | **场景:** 在Navigator还没出来时,导航器是由NavigatorIOS来实现的,当时觉得页面切换动画很流畅,但是一旦用Navigator后,发现定义的切换动画会使JS线程出现严重的掉帧(卡顿现象)。 9 | 10 | **原因:** NavigatorIOS的切换动画是跑在UI主线程上,而不是JS线程上的,所以不受JS线程上的掉帧影响。当然官方还是推荐使用Navigator,其原因如下: 11 | 12 | <1.Navigator扩展性的API设计使得它完全可以通过js定制,而NavigatorIOS则无js层面的定制; 13 | 14 | <2.Navigator使用JavaScript编写,iOS和Android都可以使用,而NavigatorIOS只能在IOS上 15 | 16 | <3.Navigator优化后的动画效果还算不错,而且官方还在不断改进中,当然这个动画比不上NavigatorIOS那么顺滑。但NavigatorIOS并不在FaceBook的应用中使用,也不是其主导开发,而是开源社区主导开发。所以可能坑多又没人给出填坑的解决方法。 17 | 18 | 所以我们选择导航的时候尽量选择Navigator吧。 19 | 20 | **优化切换动画卡顿的问题:** 21 | 22 | 1. 使用API InteractionManager,它的作用就是可以使本来JS的一些操作在动画完成之后执行,这样就可确保动画的流程性。当然这是在延迟执行为代价上来获得帧数的提高。 23 | 24 | ``` 25 | InteractionManager.runAfterInteractions(() => { 26 | // ...耗时较长的同步的任务... 27 | //更新state也需要时间 28 | that.setState({ 29 | ... 30 | }); 31 | //获得某些数据,需要较长时间等待 32 | that.getData(arguments); 33 | }); 34 | 35 | }, 36 | }); 37 | ``` 38 | 39 | 2. 使用LayoutAnimation API(一次性动画),在对动画中途无取消要求或者其他中途回调要求的(比如局部组件特定显示隐藏动画等),则可以使用这个方案。我们可以在调用setState之前,调用LayoutAnimation方法。代码如下: 40 | 41 | ``` 42 | ... 43 | animations: { 44 | layout: { 45 | //easeInEaseOut的config 46 | easeInEaseOut: { 47 | //duration 动画持续时间,单位是毫秒 48 | duration: 300, 49 | //create, 配置创建新视图时的动画。 50 | create: { 51 | type: LayoutAnimation.Types.easeInEaseOut, 52 | property: LayoutAnimation.Properties.scaleXY, 53 | }, 54 | //update, 配置被更新的视图的动画。 55 | update: { 56 | delay: 100, 57 | type: LayoutAnimation.Types.easeInEaseOut, 58 | } 59 | } 60 | } 61 | }; 62 | ... 63 | //计划下一次布局要发生的动画 64 | LayoutAnimation.configureNext(this.animations.layout.easeInEaseOut); 65 | 66 | ``` 67 | 68 | 69 | ###2.数据类型的优化 70 | **场景**:基本上每个页面都需要加载和渲染数据,如果页面列表数据结构复杂,有时刷新数据时state中的未必有修改,但是遇到这样的语句this.setState({data:samedata}) ,界面却被重新render. 71 | 72 | **原因:** 这是react-native的生命周期,当你调用setState时,总是会触发render的方法 73 | 74 | **优化数据问题:**可以使用shouldComponentUpdate生命周期方法,此方法作用是在props或者state改变且接收到新的值时,则在要render方法之前调用。此方法在初始化渲染的时候不会调用,在使用 forceUpdate 方法的时候也不会。所以在这个方法中我们可以增加些判断规则来避免当state或者props没有改变时所造成的重新render. 75 | 76 | ``` 77 | shouldComponentUpdate: function(nextProps, nextState) { 78 | return nextProps.value !== this.props.value; 79 | } 80 | 81 | ``` 82 | 83 | 但仅仅做这层判断是不够的,如果是一个列表的对象,例如下面的例子: 84 | 85 | ``` 86 | var Component = React.createClass({ 87 | getInitialState() { 88 | return { 89 | ... 90 | data:{ 91 | valueobj: { 92 | v1: 'v1', 93 | v2: 'v2' 94 | } 95 | } 96 | } 97 | }, 98 | shouldComponentUpdate(nextProps, nextState){ 99 | return ( 100 | return nextProps.data !==this.props.data; ); 101 | } 102 | ``` 103 | 这里即使使用了shouldComponentUpdate中的判断,但却一直返回true,导致还会执行render。所以必须对对象所有的键值进行进行比较才能确认是否相等。这里推荐使用facebook自家的immutablejs。一个不可变数据类型的库。使用后可以直接使用一下的写法达到我们之前的目的(即使是对象都可以完美的做比较)。修改后代码如下: 104 | 105 | ``` 106 | var { List, Map } = Immutable; 107 | var Component = React.createClass({ 108 | getInitialState() { 109 | return { 110 | ... 111 | data: Immutable.fromJS({ 112 | valueobj: { 113 | v1: 'v1', 114 | v2: 'v2' 115 | } 116 | }) 117 | } 118 | }, 119 | shouldComponentUpdate(nextProps, nextState){ 120 | return ( 121 | return nextProps.data !==this.props.data; ); 122 | } 123 | ... 124 | ``` 125 | 126 | immutablejs其他的具体用法请见: 127 | 128 | ###3.数据加载的优化 129 | **场景:** 在首屏页面加载时,加载前6分钟的数据分6页显示,并需保持当前选择页的时间的前6分钟,如果按照此场景开发所遇到问题是:首页加载时间太长,加载新数据时页面显示加载用户体验不顺畅 130 | 131 | **原因:** 首页请求数据量过大,导致首屏页面加载很慢;后台数据更新时导致用户体验不顺畅 132 | 133 | **优化问题:** 减少首屏加载的数据,实现数据懒加载,其先加载3页的数据量,然后在滑动的时候后台去取后面的数据(例如绑定到Slider组件的onMomentumScrollEnd事件中,每次取3条数据),最后每次保持6分钟的数据在组件中,其他数据则可放到localstorage中作为缓存。这样就可以减少首屏加载事件和提高用户体验。加载数据的滚动列表示例代码如下: 134 | 135 | 初始化(定义数据data): 136 | 137 | ``` 138 | ... 139 | getInitialState: function(){ 140 | return{ 141 | title: title, 142 | data: data, 143 | ... 144 | }; 145 | }, 146 | ``` 147 | 148 | 滚动列表的事件:分为左滑每次到3的倍数页面取当前取过的数据的前3分钟的历史数据;右滑则取之后的时间。 149 | 150 | ``` 151 | ... 152 | //滚动列表的事件 153 | momentumScrollEnd: function(e, state){ 154 | var data = this.state.data; 155 | var that = this; 156 | //当无数据可更新时禁止滚动 157 | if(!data){ 158 | return 0; 159 | } 160 | ... 161 | //(1)左移,手势<- 162 | //时间倒退,index为滑动列表的序列号。每滑动到3的倍数的页面则去加载历史数据 163 | if(index % 3 === 0 && currentIndex < index){ 164 | hour = lastHour; 165 | lastMin = lastMin - 1; 166 | var newDate = new Date(year, month, dayri, hour, lastMin); 167 | //加载历史数据 168 | this.getNewData(newDate, newDate.getMinutes(),3, function(cbData){ 169 | for(var i in cbData){ 170 | data.push(cbData[i]); 171 | } 172 | that.setState({ 173 | data: data 174 | }); 175 | }); 176 | } 177 | //(2)右移,手势-> 178 | //同时监测移动的方向,采用touch事件 179 | //判断4个事件才能确保滑动的方向(touchStart、touchEnd、scrollBeginDrag、momentumScrollEnd 180 | //50为阈值控制 181 | var disLoc = parseFloat(this.state.touchEndLocX) - parseFloat(this.state.touchStartLocX); 182 | var disPage = parseFloat(this.state.touchEndPagex) - parseFloat(this.state.touchStartPagex); 183 | if(this.state.isLoadFront && state.offset.x === 0 && disLoc > 50 && disPage > 50){ 184 | hour = firstHour; 185 | firstMin = firstMin + 1; 186 | var newDate = new Date(year, month, dayri, hour, firstMin); 187 | ... 188 | //右移,加载当前时间的数据 189 | this.getNewData(newDate, newDate.getMinutes(), 1, function(cbData){ 190 | for(var j = cbData.length - 1; j >= 0; j--){ 191 | data.unshift(cbData[j]); 192 | } 193 | ... 194 | }); 195 | }); 196 | }else{ 197 | alert('当前数据为最新数据,暂不用更新'); 198 | } 199 | } 200 | ... 201 | } 202 | ... 203 | ``` 204 | 205 | 206 | 207 | 208 | 209 | 210 | ###4.组件响应速度的优化 211 | **场景:** 一个页面包含多个类别的列表,由于列表都比较长,所以需要增加折叠功能并增加折叠动画,折叠按钮使用的是TouchableHighlight组件。问题是当我点击折叠或者展开按钮时出现延迟响应和动画掉帧的问题。 212 | 213 | **原因:** 在TouchableHighlight组件的onPress方法中执行了setState的操作,由于列表的对象相对来说比较复杂需要大量计算的工作,所以导致了延迟响应和JS线程的掉帧。 214 | 215 | **优化问题:** 使用requestAnimationFrame(fn)在下一帧就立即执行回调,这样就可以异步来提高组件的响应速度。而折叠动画则可以使用LayoutAnimation一次性动画来完成,保证其流畅性。而对于某些状态更新,setNativeProps方法可以让我们直接修改原生视图组件的属性,而不用通过setState来重新渲染结构,这样能使整个组件响应速度变快。 216 | 217 | 218 | ``` 219 | OnPress() { 220 | this.requestAnimationFrame(() => { 221 | \\封装之前OnPress中的操作,比如setState等。 222 | }); 223 | } 224 | ``` 225 | 还有要提醒的是尽量优化组件的View结构,当View的层级很深时渲染的速度也会变慢 226 | 227 | 228 | ###5.资源优化 229 | **场景:** 这里说的资源包括reactnative打出来的Bundle,图片等静态资源。react-native的一股脑儿的打包方式,无疑一下子增大了Bundle包大小。还有一个页面多多少少会包含一些图片,特别是在一些商业APP中,图片是对内容一种补充,能让提高用户体验。为了能更快的加载图片,可以把图片打入包中,当然这个代价是巨大的。对APP来说,控制包的Size不管从商业方面还是开发性能方面都是一个很重要的参数。 230 | 231 | **优化问题:** 232 | 233 | 1. 对于Bundle过大,我们可以通过一些思路来优化它,首先是对其尝试进行拆包,然后对拆包进行约束,使公共基础那部分拆成一类包,使其可以按需加载本地文件,而像业务逻辑等则拆成另一类包,使其可以按需加载线上文件。 234 | 2. 图片我们可以对其转成webp格式。webp大家应该都很熟悉了,它既支持有损压缩又支持无损压缩的图片文件格式。根据官方介绍其无损压缩后的WebP比PNG文件少了45%的文件大小,即使PNG文件经过压缩工具压缩之后,WebP 还可以减少28%的文件大小,这可以大大提高移动端的图片加载速度。据官网介绍在IOS平台中,每次调整Image组件的大小,都需要重新裁剪和缩放原始图片,重新渲染界面。这个操作开销会非常大,尤其是大的图片。比起直接修改尺寸,更好的方案是使用transform: [{scale}]的样式属性来改变尺寸。比如当你点击一个图片,要将它放大到全屏的时候,就可以使用这个属性。 235 | 236 | 237 | ###6.页面加载与显示优化 238 | **场景:** 某些页面需要访问一个或多个业务数据服务,虽然取数据是异步,但是页面总是会有一段较长的loading的时间。 239 | 240 | **优化问题:** 对于首屏所需的数据服务的访问,使其在页面加载阶段尽早的发起数据请求,这样有助于减少等待数据的时间。而对长时间的Loading可能会降低用户体验的问题,我们可以使用Fake页来提高用户体验。先显示一个Fake页,等数据请求后并执行了相应的业务逻辑后,再替换成真正的页面。 241 | 242 | 243 | 以上是我们在实践中的一些优化心得,优化之路漫漫,吾将上下而求索。特别是在React-Native还在成长期这个阶段,优化变得尤为重要。期望React-Native未来在性能上有更好的突破。 244 | -------------------------------------------------------------------------------- /BestActualCombat/articles/chapter3.md: -------------------------------------------------------------------------------- 1 | ### React Native Bundle 拆分的尝试 2 | 3 | ### 引言 4 | 5 | React Native以其独到的特性,吸引着互联网公司纷纷为之投入或多或少的人力。在实际的开发过程中,开发者们也确实尝到了甜头,它的组件化思想、热更新机制以及jsx和es6等的引入,都给开发者们带来了很大的便利。也难怪在npm和github上,每天都会有很多react-native的新模块出现。这也充分表明了各大公司对其的看好。然而,从目前qq群、微信公众号、社区、论坛等各大信息交流平台中了解到,大家都是保持在研究和观望状态,顶多把某个不重要的页面交给React Native来练手。其中缘由纷繁复杂。今天我们这里主要是探讨——————bundle文件太大。 6 | 7 | ### 现状 8 | 9 | React Native应用的开发者们,在项目开发完后,都会遇到一个问题,生成的bundle文件太大。一个AwesomeProject项目,在没有什么逻辑代码的情况下,打完之后约530k。随着业务的增多,业务复杂性的上升,文件的大小势必会急剧增大。react-native打包成一个bundle的做法,必定是要得到解决的。 10 | 11 | ### 分析 12 | 13 | 14 | react-native默认提供的打包方式有两种: 15 | 16 | + 离线打包 17 | 18 | ``` 19 | react-native bundle 20 | --entry-file index.ios.js 21 | --platform ios 22 | --dev true 23 | --bundle-output dest/main.jsbundle 24 | --assets-dest dest 25 | 26 | ``` 27 | 28 | 29 | + 在线打包 30 | 31 | ``` 32 | 33 | http://localhost:8081/index.ios.bundle?platform=ios&dev=true 34 | 35 | ``` 36 | 具体有哪些参数可以打开如下文件进行查看: 37 | 38 | ``` 39 | $youProjectRoot/AwesomeProject/node_modules/react-native/local-cli/bundle/bundleCommandLineArgs.js 40 | 41 | 如: 42 | module.exports = [ 43 | { 44 | command: 'entry-file', 45 | description: 'Path to the root JS file, either absolute or relative to JS root', 46 | type: 'string', 47 | required: true, 48 | }, 49 | ...... 50 | 51 | ``` 52 | 官网中还给出了一些其它的使用方式,地址: 53 | 54 | ``` 55 | https://github.com/facebook/react-native/tree/master/packager 56 | 57 | ``` 58 | 59 | 不过,不论哪种方式都是只有一个“entry-file”,然后根据“entry-file”去进行依赖分析、文件压缩等操作,最后输出在“bundle-output”中。然后通过NSBundle的URLForResource方法来指定加载打好的的bundle文件。 60 | 61 | 如: 62 | 63 | ``` 64 | jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 65 | ``` 66 | 67 | 68 | 这样的打包模式,对用户体验来说是非常不错的。但是考虑到国内的网络状况以及对App size的控制,打成一个Bundle的模式在国内还是行不通的。 69 | 70 | 71 | ### 思考 72 | 73 | 在传统的Hybrid开发中,要解决文件太大的问题,我们常常会想到如下几个办法: 74 | 75 | + 进行拆包 76 | + 按需加载本地文件 77 | + 按需加载线上文件 78 | 79 | 那么,能否把Hybrid开发中的经验应用在React Native项目中呢。在React Native项目中,针对文件大的问题,我们做了如下尝试: 80 | 81 | + 多业务进行拆包 82 | 83 | 借助gulp、grunt等工具,通过配置不同的任务,在调用React Native提供的打包命令,可以将App打包成多个文件。 84 | 85 | + 按需加载本地文件 86 | 87 | 在开发环境的情况下,React Native是支持加载本地文件的。这里想要做的是,在打包完的bundle中也可以加载本地文件,这就需要对require进行扩张了。 88 | 89 | + 按需加载线上文件 90 | 91 | 在开发Hybrid时,为了减少包体积。开发者们经常会将一些不重要的页面或文件,走线上动态获取的方式。这个功能只有在web端的requirejs中有,React Native和webpack中都是不支持的。要实现此项功能,需要对React Native中的require进行扩张。 92 | 93 | + 按需加载react-native模块 94 | 95 | 不论是reactjs还是react-native,在代码的组织方式上都是按模块进行划分的。可能Facebook也意识到react框架太大了。这个模块划分的方式,给开发者们的按需载入创造了机会。 96 | 97 | ### 实现 98 | 99 | 这里简单阐述下部分功能的实现思路 100 | 101 | + React Native自身模块拆分 102 | 103 | 在打完的main.jsbundle中,常常会看到好多polyfills的文件,那这些文件从哪里来的呢。打开 104 | 105 | ``` 106 | node_modules/react-native/packager/react-packager/src/Resolver/index.js 107 | ``` 108 | 文件,会看到这些polyfills文件都是在这里设置的, 109 | 110 | ``` 111 | path.join(__dirname, 'polyfills/String.prototype.es6.js'), 112 | path.join(__dirname, 'polyfills/Array.prototype.es6.js'), 113 | ...... 114 | 115 | ``` 116 | 由名字可以看出,这些是用来对es6、es7进行适配的。所以代码中如果只有es5的语法是不是就可以不需要这些文件了呢,这也是个优化点,不过看起来量不大。 117 | 118 | 有人可能经常会有这样的想法:我们实际项目中用到的React Native模块其实并没几个,我们在打包的时候,是否可以只打包我们需要的模块呢。我们找到文件 119 | 120 | 121 | ``` 122 | /node_modules/react-native/Libraries/react-native/react-native.js 123 | 124 | ``` 125 | 126 | 可以看到所有React Native的模块定义都是在这里了,包括Components、APIs等等。 127 | 128 | ``` 129 | var ReactNative = { 130 | // Components 131 | get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, 132 | get ART() { return require('ReactNativeART'); }, 133 | ...... 134 | 135 | ``` 136 | 所以,可以在打包的时候,根据实际情况,通过脚本等手段,注释掉一些用不到或不常用的模块以减少输出的体积。当然也可以把部分不常用的模块,抽出来单独作为一个文件,在需要的时候,通过按需加载的方式引入进来。 137 | 138 | + 业务模块拆分 139 | 140 | App的设计一般都是按照业务线划分的。每个业务都对应一套自己的逻辑。当然也有部分业务线会出现依赖情况。按React Native提供的打包方法,将所有业务线的逻辑都打在一起,势必会造成好多业务线代码的浪费。有可能那个业务线就根本不会被用户访问到。所以我们就想着能不能将一些基础的、公共的业务线打在一起,其它独立的业务线都各自独立成包。 141 | 142 | React Native提供的打包方法允许输入一个入口文件,那么这个入口文件可以是整个App的入口,也可以是各业务线自己的入口。由此我们可以将各业务线单独成包,但这样的结果并不能直接投入使用。可以想到,这里并没有过滤机制,各业务线之间一些模块会被重复的打进去也包括react-native模块。而React Native打包提供的参数中也只有blacklist会涉及一些过滤,但却无法满足我们的需求。 143 | 144 | 还好packager为我们提供了很多可以的API。通过参考 145 | 146 | ``` 147 | /node_modules/react-native/local-cli/bundle/buildBundle.js 148 | ``` 149 | 中的打包逻辑,我们以一个入口文件的打包为例,可以将打包逻辑设计成如下: 150 | 151 | ``` 152 | 1、加载打包需要用到的模块 153 | var config = require('config.js') 154 | var ReactPackager = require('react-native/packager/react-packager') 155 | var Bundle = require('react-native/packager/react-packager/src/Bundler/Bundle') 156 | var saveAssets = require('react-native/local-cli/bundle/saveAssets') 157 | var outputBundle = require('react-native/local-cli/bundle/output/bundle') 158 | 159 | 2、创建client 160 | var client = ReactPackager.createClientFor({ 161 | projectRoots: config.projectRoots, 162 | blacklistRE: config.blacklistRE, 163 | ... 164 | }) 165 | 3、调用outputBundle进行打包,将打包后的bundle返回 166 | outputBundle.build(client, { 167 | entryFile: config.file, 168 | ...... 169 | }) 170 | 4、分析bundle中的module,将符合条件的module加入到新的bundle中 171 | 定义一个新的bundle 172 | var newBundle = new Bundle(); 173 | bundle.getModules().forEach(function (module) { 174 | if(filter(module.sourcePath)){ 175 | newBundle._modules.push(module); 176 | } 177 | ...... 178 | }) 179 | 180 | 5、定义过滤机制 181 | function filter(path){ 182 | var ret = true; 183 | if( 184 | (path.indexOf('/react-native/')!=-1)|| 185 | (sourcePath.indexOf('/fbjs/')!=-1)|| 186 | ...... 187 | ){ 188 | ret = false; 189 | } 190 | return ret; 191 | 192 | } 193 | 上只是个简单的过滤,在复杂的过滤中,还需要调用ReactPackager.getDependencies找到每个模块的依赖,然后在过滤的时候调用过滤模块的依赖,依次递归才能达到真正的滤掉。 194 | 195 | 6、对module进行合并、替换等处理 196 | newBundle.finalize() 197 | 198 | 7、调用outputBundle输出新的bundle 199 | 200 | outputBundle.save(newBundle, {}, false) 201 | 202 | ``` 203 | 到此,一个带有过滤功能的打包脚本就基本成型了,之后的多文件入口同时打包的功能,也就是要在上面做些扩扩展就可以了。 204 | 205 | 在打包方面,其实也也可走网络打包,packager的网络打包逻辑中,凡是请求以.bundle结尾的文件,都会对这个文件进行打包。而其它格式的文件,则请求什么返回什么。所以可以根据该特性来实现打包。可以将过滤条件作为querystring的方式传递过去,然后在 206 | 207 | ``` 208 | react-native/packager/react-packager/src/Bundler/index.js 209 | 210 | ``` 211 | 文件中对querystring进行拦截,并实现其过滤功能。 212 | 213 | 然而在实际的拆包中会发现,packager中打出的包都会将模块名称替换为数字id。如: 214 | 215 | ``` 216 | __d(14,function(s,t,i,o){"use strict";var r={OS:"ios"};i.exports=r}); 217 | 218 | __d(30,function(n,t,o,r){"use strict";var u,e=t(31);u=e.now?function(){return e.now()}:function(){return Date.now()},o.exports=u}); 219 | 220 | 221 | ``` 222 | 这导致拆出的包中,引入不到某些模块,因为不是在一起打包,模块的id都对不上,或者会出现重复的情况。 223 | 224 | 我们的思路是打包的时候不进行id的替换,依然使用原有的模块名称,做到类似在web中requireJS使用的那样。 225 | 找到文件 226 | 227 | ``` 228 | node_modules/react-native/packager/reat-packager/src/Resolver/index.js 229 | 230 | 将如下代码中的moduleName,替换为model的绝对路径 231 | function defineModuleCode(moduleName, code, verboseName = '') { 232 | return [ 233 | `__d(`, 234 | `${JSON.stringify(moduleName)}/*<-替换的地方*/ /* ${verboseName} */, `, 235 | `function(global, require, module, exports) {`, 236 | `${code}`, 237 | '\n});', 238 | ].join(''); 239 | } 240 | 241 | ``` 242 | 这样只完成了define(如:define(0,...))中的名称替换,我们还需要找到require(如:require(0))中的名称替换,于是找到如下文件 243 | 244 | ``` 245 | node_modules/react-native/packager/reat-packager/src/Bundle/Bundle.js 246 | 247 | 在super(BundleBase)中,定义一个获取模块的方法getModuleName,将下面的super.getMainModuleId替换为super.getModuleName,这样在_addRequireCall就可以拿到模块的绝对路径了 248 | 249 | _addRequireCall(moduleId) { 250 | const code = `;require(${JSON.stringify(moduleId)});`; 251 | const name = 'require-' + moduleId; 252 | ...... 253 | } 254 | 255 | finalize(options) { 256 | options = options || {}; 257 | if (options.runMainModule) { 258 | options.runBeforeMainModule.forEach(this._addRequireCall, this); 259 | this._addRequireCall(super.getMainModuleId());/*<-替换的地方*/ 260 | } 261 | 262 | super.finalize(); 263 | } 264 | 265 | ``` 266 | 267 | 这样就完成了模块名称的保留,我们就可以愉快的使用我们的拆包模块了。 268 | 269 | 270 | + 按需加载实现 271 | 272 | 经过上面的介绍,我们已经完成了模块的拆分。那么光有独立的模块还是不能让App运行起来,需要有一种能力将这些模块联系起来,这就是模块加载机制。常规的加载会有如下两种场景: 273 | 274 | 1、本地模块 275 | 276 | 有时候为了加快页面打开速度,我们常常会选择将首页和非首页的页面进行分开打包,在App启动时,只加载首页的模块,待首页模块加载完毕后,再去异步的加载后续页面的模块。这里的本地模块加载就是用在这种场景中。那么在React Native中该如何实现这种加载方式呢。要读写本地文件,光有javascript是办不到的,所以一定要借助native的能力。简单的代码实现如下: 277 | 278 | ``` 279 | #import "RCTBridgeModule.h" 280 | @implementation RequireLocal 281 | RCT_EXPORT_MODULE() 282 | RCT_EXPORT_METHOD(loadPath:(NSString *)path callback:(RCTResponseSenderBlock)callback) 283 | { 284 | NSString *filePath = [[NSBundle mainBundle] pathForResource:path ofType:nil]; 285 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 286 | NSString *content = [[NSString alloc] initWithContentsOfFile:filePath 287 | ...... 288 | } 289 | @end 290 | ``` 291 | 代码的流程为:按照React Native中对native模块封装的规范,实现RCTBridgeModule协议,并通过定义宏RCT_EXPORT_MODULE、RCT_EXPORT_METHOD将native模块的功能暴露给javascript来调用。在native的模块中,采用NSBundle的pathForResource方法,将文件路拿到。再借助NSString的initWithContentsOfFile方法获取到文件的内容。然后在javascript中,将拿到的内容,进行一次包装,如: 292 | 293 | ``` 294 | var str='__d("'+filePath +'", function(global, require, module, exports) {'+ 295 | content+ 296 | '})' 297 | 298 | ``` 299 | 最后调用eval,便可将拿到的内容执行到当前的jscontext中。 300 | 301 | 2、线上模块 302 | 303 | 在App的开发中经常会为了控制size大小而发愁,尤其是苹果的100m限制,所以各业务线都在绞尽脑汁的想办法减size。自然而然的大家就想到了将一些资源放在服务端,在需要的时候将其异步加载下来,也就是常常听说的直连。对于服务器异步加载的实现,代码如下: 304 | 305 | ``` 306 | fetch(filePath) 307 | .then((response) => response.text()) 308 | .then((responseText) => { 309 | ...... 310 | ``` 311 | 代码的流程为:采用React Native提供的fetch方法,将需要的模块异步的从服务器上拉回来,接下来的动作,和上面的“本地模块”的逻辑一样。在实际的模块加载中,我们还需要对模块进行缓存,以提高模块的访问速度。 312 | 313 | 314 | ### 后续 315 | 316 | 在经过上面的介绍中,我们应该大概知道拆包和按需加载的实现原理。但是大家也都看到了,这要侵入react-native的代码中,进行很多地方的修改。这样不利于之后对react-native的版本升级。所以我们需要想一种更合理的解决办法。也就是我们现在正在做的一个尝试。将React Native中的cmd模块,在线下或运行时编译为AMD模块,然后调用r.js的来对其进行打包,以达到干净的完成拆包和按需加载的功能。而且r.js的打包配置的灵活度我觉得比packager、webpack、browserify等工具都灵活好使。 317 | -------------------------------------------------------------------------------- /BestActualCombat/articles/chapter1.md: -------------------------------------------------------------------------------- 1 | ##React-Native 最佳实战之开发环境搭建及扩展 2 | 3 | ###引言 4 | 5 | React Native的出现,为APP开发者们带来了冲动和激情,令Native开发者和Web开发者都为之痴迷。瞬间各类技术论坛、技术社区甚至出版社都争先报道其相关内容。然而对于一般的初学者来说,最简单要求莫过于按照官方提供的向导来完成基于React Native的处女之作。就是这么简单的一个要求,却把无数开发者拒之门外。其中原因在于初学者在按照教程搭建的过程中,总会出现这样那样的问题,给开发者们的锐气重重一击。下文将以其中一些比较突出的问题为起点,从开发前和开发中两方面来加以分析并给出相应的解决方案,希望能够给广大开发者们提供些许实战经验,少走一些不必要的弯路。 6 | 7 | ##一、开发前 8 | 9 | ###面临的问题 10 | 11 | 按照官方的向导,在Homebrew 、Watchman、Flow、nvm、node等运行环境安装完后,要做的第一件事情便是安装React Native命令行工具并由其初始化React Native项目。代码如下: 12 | 13 | ``` 14 | $ npm install -g react-native-cli 15 | $ react-native init AwesomeProject 16 | ``` 17 | 然而就是这么两句简短的代码,却运行的也不是那么的顺利。经常会听到开发者们抱怨安装个命令行工具也要翻墙。 18 | 19 | 对于初学者们,最捷径的办法,莫过于参考别人的项目。经常会遇到这么一个场景, 项目中要用到一个slider组件,聪明的开者们很快想到了github,在其上一搜发现还真不少,于是乎兴高采烈的将其整个项目clone下来,找到其中的slider组件,纳入自己的项目中,简单的添加了几句引入代码,便急冲冲的执行CMD+R,瞬间模拟器内一片红屏,分析原因,最后发现是fontsize不支持数字了一定要以字符串的方式设置,这是多么沉痛的打击。 20 | 21 | 玩了一段时间的React Native开发者们应该也会发现,随着开发出来的React Native项目的增多,电脑的存储空间会越来越小了,翻开React Native的历代版本可以看到,gzip之后70M左右,解压后在350M左右。这样的体积大小,几个项目下来,几个G的空间瞬间没了。 22 | 23 | 那么归纳起来主要是以下几类问题制约着开发者们。 24 | 25 | + React Native 命令行环境搭建困难 26 | + React Native 初始化项目困难 27 | + React Native 版本升级经常带来API不支持 28 | + 随着React Native 项目的增多,占用空间越大 29 | 30 | 31 | ###原因分析 32 | 33 | React Native 命令行环境搭建困难,主要是慢,为什么呢,从代码 34 | 35 | ``` 36 | npm install -g react-native-cli 37 | ``` 38 | 中可以了解到,这是在从npm服务器上拉取react-native-cli。所以慢的原因便是因为npm服务器不在国内。聪明的国人已给出了解决办法,通过翻墙来解决此问题。更高兴的是npm提供了一个register的属性,可以让开发者自由的设置镜像地址。开发者们最常用的便是淘宝的镜像地址。据统计国内比较常用的镜像地址有: 39 | 40 | ``` 41 | http://r.cnpmjs.org/ 42 | http://registry.npm.taobao.org/ 43 | http://registry.npmjs.eu/ 44 | http://registry.npmjs.org.au/ 45 | http://npm.strongloop.com/ 46 | https://registry.nodejitsu.com/ 47 | http://registry.npmjs.pt/ 48 | 49 | ``` 50 | 这么多眼花缭乱的地址,的要感谢国人开源意识的强大,是他们给开发者们带来了福音,让开发者们再也不用担心下载不到nodejs包了。 51 | 52 | 这是官网初始化React Native 项目的代码: 53 | 54 | ``` 55 | react-native init AwesomeProject 56 | ``` 57 | 可以看到这是通过"react-native init"这个命令来进行初始化的。那“react-native”这个命令又是从哪里来的呢,起初给笔者带来很大的困惑,上面只安装了“react-native-cli”这个node包,怎么会冒出个“react-native”命令而不是“react-native-cli”命令呢。我们翻开“react-native-cli”的安装目录,mac上是在: 58 | 59 | ``` 60 | /usr/local/lib/node_modules/react-native-cli 61 | ``` 62 | 目录,打开其中的“package.json”文件,可以看到有这么一段代码: 63 | 64 | ``` 65 | "bin": { 66 | "react-native": "index.js" 67 | }, 68 | ``` 69 | 这里简单介绍下bin属性,"bin"是由多个“{ 命令名:文件名 }”组成的一个map。在安装的时候会将每个“命令名”链接到prefix/bin(全局初始化)或者./node_modules/.bin/(本地初始化)。上面代码在安装的时候,会将index.js链接到/usr/local/bin/react-native。这样使用"react-native init"进行初始化的困惑也就可以理解啦。那么这个“react-native init”究竟做了什么呢。继续跟踪,打开index.js,其中的部分代码片段: 70 | 71 | ``` 72 | if (args[0] === 'init') { 73 | if (args[1]) { 74 | init(args[1]); 75 | } else { 76 | } 77 | } else { 78 | ...... 79 | } 80 | ``` 81 | 可以看出,index.js其实只对“init”方法做了处理,具体到“init”方法中又做了些什么呢,截取了部分主要代码如下: 82 | 83 | ``` 84 | 1、fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson)); 85 | 86 | 2、run('npm install --save react-native', function(e) { 87 | ``` 88 | 其中代码1是动态生成package.json,代码2是在本地安装react-native模块。到此,之前提到的“React Native 初始化项目困难”也就不难理解啦,原因都是npm惹的祸。 89 | 90 | 既然已经看到这里啦,那就顺带完整的介绍下react-native这个命令。除了“react-native init”命令外,官网还提供了 “react-native bundle”、“react-native run-android”等命令,而index.js文中却只有“init”命令。那其他命令又是怎么来的呢。带着疑问,笔者又找到了如下的代码: 91 | 92 | ``` 93 | var CLI_MODULE_PATH = function() { 94 | return path.resolve( 95 | process.cwd(), 96 | 'node_modules', 97 | 'react-native', 98 | 'cli' 99 | ); 100 | }; 101 | var cli; 102 | try { 103 | cli = require(CLI_MODULE_PATH()); 104 | } catch(e) {} 105 | 106 | if (cli) { 107 | cli.run(); 108 | } else { 109 | ...... 110 | } 111 | 112 | ``` 113 | 打开“CLI_MODULE_PATH”所指的cli文件,其指向了“module.exports = require('./local-cli/cli.js');”, 114 | 继续打开“/local-cli/cli.js”部分代码如下: 115 | 116 | ``` 117 | var documentedCommands = { 118 | 'start': [server, 'starts the webserver'], 119 | 'bundle': [bundle, 'builds the javascript bundle for offline use'], 120 | 'unbundle': [unbundle, 'builds javascript as "unbundle" for offline use'], 121 | 'new-library': [library, 'generates a native library bridge'], 122 | 'link': [link, 'Adds a third-party library to your project. Example: react-native link awesome-camera'], 123 | 'android': [generateWrapper, 'generates an Android project for your app'], 124 | 'run-android': [runAndroid, 'builds your app and starts it on a connected Android emulator or device'], 125 | 'run-ios': [runIOS, 'builds your app and starts it on iOS simulator'], 126 | 'upgrade': [upgrade, 'upgrade your app\'s template files to the latest version; run this after ' + 127 | 'updating the react-native version in your package.json and running npm install'] 128 | }; 129 | ``` 130 | 看到这里,也就眼前豁然开朗起来,原来除“init”命令外,其它命令都在这里啦,这让笔者明白了一件事情:也就是说“init”可以在任何地方使用,而其他命令只能在React Native项目的根目录下使用 ,不得不佩服Facebook设计之巧妙。 131 | 与此同时,另一件重要的事情也就显得清晰可见了。那就是在“React Native”的开发中,其实有两个“react-native”,一个是的“react-native-cli”生成的全局的react-native命令,一个是在初始化项目时安装的“react-native”模块,即在应用开发中通过“require('react-native')”所引用的模块。这两个简单区别便是,一个是全局的模块,一个是局部的模块。全局模块中只提供了一个“init”方法,而局部模块提供了除“init”方法外的所有命令以及React Native开发中用到的所有功能。 132 | 133 | 经过上面对"react-native-cli"与“react-native”的分析,可以看出Facebook应该是推荐“react-native”模块局部化,所以不论在React Native项目初始化的过程中,还是clone已有的React Native项目,都需要在当前项目下下载和安装“react-native”模块,使得React Native 项目占用的空间越来越大。 134 | 135 | 136 | 137 | ###解决办法 138 | 139 | ####1、私有NPM搭建 140 | 141 | 虽然上面讲的国内镜像,已使NPM的下载速度很快啦,但是仍然不如在自己内网架设个NPM私有服务器,给团队成员提供更快捷的下载速度。架设私服,除了速度快以外,还有一个重要的原因就是一些内部的隐私模块也可以发在私服上供内部成员使用。 142 | 市面上的NPM私服也有很多,这里推荐的是一个叫“sinopia”的NPM私服。“sinopia”的做法是优先从自己的仓库中拉取模块,如果发现没有,便从远端的NPM服务器拉取。也许有的开发者早已注意到,这个私服其实在“react-native-cli”的NPM库中[react-native-cli](https://www.npmjs.com/package/react-native-cli)就有介绍,笔者猜想应该是Facebook也推荐诸位使用“sinopia”来搭建NPM私服吧。“sinopia”的github地址为:[sinopia](https://github.com/rlidwka/sinopia)。“sinopia”的搭建比较简单,步骤如下: 143 | 144 | #####-安装命令: 145 | 146 | ``` 147 | $ npm install -g sinopia 148 | 149 | ``` 150 | 151 | #####-启动命令: 152 | 153 | ``` 154 | $ sinopia 155 | ``` 156 | 启动后的日志: 157 | 158 | ``` 159 | warn --- config file - /Users/youname/.config/sinopia/config.yaml 160 | warn --- http address - http://localhost:4873/ 161 | ``` 162 | 163 | 日志中的“config file”为“sinopia”的配置地址,“http address”为“sinopia”的主页地址。打开“/Users/youname/.config/sinopia/config.yaml”可以看到默认的配置信息如下: 164 | 165 | ``` 166 | //仓库保存的路径 167 | storage: /Users/wagon/.local/share/sinopia/storage 168 | //用户验证相关 169 | auth: 170 | htpasswd: 171 | //存放用户信息,与max_users一起配合来管理用户 172 | file: ./htpasswd 173 | //配置npm服务器 174 | uplinks: 175 | npmjs: 176 | url: https://registry.npmjs.org/ 177 | //配置模块信息 178 | packages: 179 | //私有模块配置 180 | '@*/*': 181 | //访问权限的设置 182 | access: $all 183 | //发布权限的设置 184 | publish: $authenticated 185 | //公有模块配置 186 | '*': 187 | access: $all 188 | publish: $authenticated 189 | //公有模块拉取地址的配置,与上面的uplinks属性配合使用 190 | proxy: npmjs 191 | logs: 192 | - {type: stdout, format: pretty, level: http} 193 | ``` 194 | 如果想看更完整的配置可以参考这里[full.yaml](https://github.com/rlidwka/sinopia/blob/master/conf/full.yaml)。 195 | 196 | #####-创建新用户 197 | 198 | ``` 199 | $ npm adduser --registry http://localhost:4873 200 | ``` 201 | 按照命令行中的提示,依次输入Username、Passworld、Email即可完成用户的创建。 202 | 203 | #####-设置npm镜像地址 204 | 205 | ``` 206 | $ npm set registry http://localhost:4873/ 207 | 208 | ``` 209 | 210 | #####-发布npm包 211 | 212 | 在发布模块前,需要先登录 213 | 214 | ``` 215 | $ npm adduser 216 | Username:xxx 217 | Passworld:xxx 218 | Email:xxx@xx.com 219 | 220 | ``` 221 | 登录完成后,便可进入待发布模块的根目录进行发布了。 222 | 223 | ``` 224 | $ npm publish 225 | ``` 226 | 227 | 若没有“package.json”文件的话,需先执行“npm init”进行创建,然后执行上面的命令即可将模块推送到自己的NPM服务器上了。这样我们在安装该模块的时候,便是从自己的NPM服务器上下载了。 228 | 229 | #####-远端访问 230 | 231 | 上面只是在本地搭起了NPM服务器,只能通过本地来访问,如果要做到远端访问的话,需要这样来启动“sinopia”: 232 | 233 | ``` 234 | $ sinopia -l IP地址:端口 235 | ``` 236 | 237 | #####-配置React Native的sinopia服务器 238 | 239 | 对于react-native的配置,官网建议修改packages和max_body_size的配置如下: 240 | 241 | ``` 242 | ...... 243 | packages: 244 | 'react-native': 245 | allow_access: $all 246 | allow_publish: $all 247 | 248 | 'react-native-cli': 249 | allow_access: $all 250 | allow_publish: $all 251 | 252 | '*': 253 | allow_access: $all 254 | proxy: npmjs 255 | max_body_size: '50mb' 256 | ``` 257 | 258 | 从修改中可以看到,主要是对模块发布做了限制,只允许发布‘react-native'和'react-native-cli'模块,其他模块一概不允许发布,猜想应该是怕将其他模块覆盖掉吧。对max_body_size的设置,主要是出于对模块大小的考虑,避免产生"request entity too large"的错误,因为默认的大小为1mb。 259 | 260 | 服务器配好后,接下来就需要将react-native模块和react-native-cli模块发布上去了。为了方便起见,我们建立如下的目录结构: 261 | 262 | ``` 263 | react_native_modules 264 | react-native 265 | v0.21.0 266 | node_modules 267 | react-native 268 | v0.20.0 269 | node_modules 270 | react-native 271 | react-native-cli 272 | v1.0.0 273 | node_modules 274 | react-native-cli 275 | 276 | 277 | ``` 278 | 其中react_native_modules为我们在用户目录下创建文件夹。之所以设计成这样的结构是为了我们方便现在NPM服务器上的模块。如我们要下载0.19.0版本的react-native模块,我们只需要创建react_native_modules\react-native\v0.19.0文件夹,然后在该文件夹中执行 279 | 280 | ``` 281 | $ npm install react-native@0.19.0 282 | ``` 283 | 即可完成从NPM服务器上对该模块的下载。接下来进入到node_modules\react-native目录,执行 284 | 285 | ``` 286 | $ npm set registry http://host:port/ //要记得切换到sinopia服务器哦,否则会将模块发在NPM服务器上而不是sinopia服务器上 287 | 288 | $ npm publish 289 | ``` 290 | 291 | ####2、实现多版本管理 292 | 293 | 如果说sinopia是用来解决速度的问题,那么多版本的管理可以说是用来解决体积的问题。做过node.js开发的同学,都清楚nvm,它是nodejs的版本管理工具,甚至包括React Native的官网也有谈到使用nvm来安装node.js。在react-native版本迭代如此频繁的阶段,居然没有react-native的版本管理工具,这让开发人员们很是受伤。所以,这里将尝试着设计一个react-native的版本管理工具,我们可以亲切的叫它rnvm(react-native version manager)。在了解rnvm的思路前,先了解下rnvm的使用场景. 294 | 295 | 296 | #####-rnvm的使用场景 297 | 298 | rnvm如其名字中的那样,主要是对react-native的版本进行管理的。那么它的使用场景都有哪些呢。这的从一个React Native项目的的获得方式说起。通常情况下有如下几种方式: 299 | 300 | a、通过react-native命令初始化项目获得 301 | 302 | b、通过从github上clone获得 303 | 304 | c、通过拷贝获得 305 | 306 | 对于a中的使用场景,在react-native初始化项目的时候,正常情况下rnvm是插不上手的。如果真要用rnvm,需要侵入/usr/local/lib/node_modules/react-native-cli/index.js文件,将run('npm install --save react-native'改为run('rnvm use ',或者也可以给rnvm添加一个init命令来取代react-native init命令,使用方式为rnvm init AwesomeProject。 307 | 308 | 对于b、c场景,可以直接使用rnvm命令进行处理。 309 | 310 | 然而,这并不是rvnm的优势。rnvm的核心思想是将react-native模块安装在全局目录下,这样每个React Native项目在使用的时候,不需要在本地目录中安装一份,只需要调用全局目录中的react-native即可,给开发者节省了不少的空间。再者rnvm给React Native项目中的对react-native版本的使用带来了灵活性,所以rnvm更适合多React Native项目的开发。 311 | 312 | #####-rnvm的目录结构 313 | 314 | ``` 315 | prefix_node_modules 316 | node_modules 317 | react-native 318 | react-native-cli 319 | react_native_modules 320 | react-native 321 | v0.21.0 322 | node_modules 323 | react-native 324 | v0.20.0 325 | node_modules 326 | react-native 327 | react-native-cli 328 | v1.0.0 329 | node_modules 330 | react-native-cli 331 | 332 | ``` 333 | 还是在用户目录下创建react_native_modules、prefix_node_modules两个目录结构。react_native_modules的目录和上面sinopia发布模块用的是一样的结构,都是用来存放模块的。默认的全局安装目录在“/usr/local/lib/node_modules”,这里的prefix_node_modules目录就是用替换原有的全局安装目录,这样做的好处是不需要每次装全局模块时都要sudo。 334 | 335 | #####-rnvm的执行流程 336 | 337 | 这里需要结合一个场景来分析rnvm的执行流程,某天开发人员从github clone了一份别人写的React Native的代码,重命名为mycloneproject,想在本地运行起来,正常情况下应该是进入mycloneproject项目的根目录,然后执行npm install。这样就会将package.json中指定的所有依赖模块都安装在当前目录的node_modules目录中。那么使用rnvm是怎么安装的呢。 338 | 339 | 现在假设rnvm只有一个use命令,格式为 rnvm use version。 340 | 341 | 具体的执行代码如下: 342 | 343 | ``` 344 | $ cd mycloneproject 345 | $ npm config set prefix ~/prefix_node_modules/node_modules 346 | $ npm set registry http://host:port/ 347 | $ rnvm use 0.20.0 348 | ``` 349 | 在这个过程中发生了些什么呢? 350 | 351 | a、先进入mycloneproject项目的根目录。 352 | 353 | b、设置全局模块安装目录为~/prefix_node_modules/node_modules。 354 | 355 | c、设置npm的镜像指向自己的sinopia服务器,这样之后的所有npm命令就会从sinopia服务器获取模块了。 356 | 357 | d、执行rnvm use命令。代码中看到可以使用全局“rnvm”命令,这就要求rnvm是一个node.js的模块,且该模块实现了package.jon中的bin配置,使其支持全局安装。 358 | 359 | e、rnvm接收到两个参数之后的行为: 360 | 361 | + 在拿到参数后,rvnm会去react_native_modules/react-native中查找是否有v0.20.0目录,如果有则进入该目录,并执行npm link 362 | 363 | + 如果没有,则创建v0.20.0目录,并进入该目录执行npm install react-native@0.20.0。 364 | 365 | + 执行完后,进入node_modules/react-native中,执行npm link 366 | 367 | + 接着在回到mycloneproject项目根目录,执行npm link react-native,然后在执行npm install。这样mycloneproject项目的依赖模块就都安装完毕,且使用了0.20.0版本的react-native, 368 | 369 | 这便是一个rnvm的基本执行流程。当然,这里也可能会有些特殊情况,如只使用rnvm use不传版本信息,这样rnvm就需要先分析package.json中的react-native的版本信息,并结合npm info react-native获取来的版本信息进行处理,得出最终需要的版本信息,然后在执行rnvm use 最终的版本信息。 370 | 371 | 这只是个use命令的执行分析。也可以像nvm一样,实现rnvm install、rnvm ls、rnvm current等命令。 372 | 373 | rnmv的github地址:[rnvm](https://github.com/GammaGos/rnvm/) 374 | 375 | 376 | 377 | 378 | ####3、完整架构 379 | ![](../images/rnvm.png) 380 | 381 | 图1 基于rnvm的开发架构图 382 | 383 | 基于上面的图形,这里做简短的描述。总体分为两个大的部分,一个是server端,一个是client端。server端是指sinopia服务所在的端,主要负责提供NPM私有服务。在搭建该端的时候,需将常用的react-native版本和react-native-cli版本都推送到该服务器上,便于之后客户端的使用。client端是指用户端也就是开发者端。该端负责React Native项目的构建。该端属于消费端是主战场。 384 | 在上图中,该端主要发生的逻辑为: 385 | 386 | 1、开发者先构建了一个React Native ProjectA项目 387 | 388 | 2、然后使用rnvm来安装依赖模块, 389 | 390 | 3、rnmv接着在指定的目录下判断是否有对应的模块, 391 | 392 | 4、有的话会先找到对应的模块,然后再去模块根目录下做npm link的操作,然后回到React Native ProjectA项目的根目录,执行npm link react-native操作,接着执行npm instal的操作。 393 | 394 | 5、如果没有找到对应模块的话,会向sinopia服务器发送请求, 395 | 396 | 6、请求下载需要的模块,并放入指定的目录中, 397 | 398 | 7、待模块下载完毕后, 执行4中的操作。 399 | 400 | 8、如果sinopia服务器也没有的话,会像npm服务器发起请求 401 | 402 | 9、待模块下载完后,执行4中的操作。 403 | 404 | 10、然后,在将改模块publish到sinopia服务器上。 405 | 406 | 407 | ##二、开发中 408 | 409 | ###面临的问题 410 | 411 | 通常项目中,App需要开发Android和iOS两个版本,经常会用到一些图片,并需要将这些图片打入App中。当开发iOS版本时,需要手动加载这些图片资源到xcode中。当开发Android版本时又需要手动的加载一次。这样,当某天某个图片需要更新时,就需要对Android和iOS都进行修改。如果要是能够让两个版本引用同一个图片,那么就会使开发变得简便。 412 | 413 | ###解决办法 414 | 415 | ####起初的想法 416 | 417 | 我们可以借助shell脚本创建、搬运、解析文件的能力,加上一些自定义的规则,来实现Andorid、iOS两个版本引用同一个图片的功能。 418 | 419 | 下面来简短的介绍下实现思想 420 | 421 | iOS版本在Images.xcassets文件夹中创建符合规则的图片文件以及文件夹。Android版本在drawable-hdpi,mdpi等文件夹中创建符合规则的图片。那么这个规则是什么呢,我们可以通过使用json形式的congfig文件来定义这个规则,格式如下: 422 | 423 | ``` 424 | { 425 | "resources":[ 426 | { 427 | //资源的别名 428 | "name":"rose", 429 | //资源的类型 430 | "type":"image", 431 | //资源路径,可以相对也可以绝对 432 | "url":"resources/image/rose.png", 433 | ...... 434 | }, 435 | { 436 | "name":"flower", 437 | "type":"image", 438 | "url":"http://host/path/imagename.jpg" 439 | ...... 440 | } 441 | ] 442 | } 443 | ``` 444 | 445 | 446 | 然后再借助Shell的jq插件,通过解析刚才定义的congfig文件来获得约定的规则,获得规则的主要shell代码如下: 447 | 448 | ``` 449 | ...... 450 | 451 | index=0; 452 | flag=0; 453 | while ((flag<=0));do 454 | read imgname <<< $(cat ./../../resources/image/resource.json |./jq '.[]' |./jq '.['$index']'|./jq '.name') 455 | read url <<< $(cat ./../../resources/image/resource.json |./jq '.[]' |./jq '.['$index']'|./jq '.url') 456 | done 457 | /**省略创建图片的代码**/ 458 | ``` 459 | 待获得规则后,就可以根据规则生成各个版本对应的图片。到此,shell脚本的主体逻辑已经介绍完成,是时候把它融合到两个版本的实际项目中运行了。 460 | 461 | 在Android版本中,我们可以通过开发一个Unix executable文件,来封装自己的run-android运行命令,代码如下: 462 | 463 | ``` 464 | /**省略前面解析json与创建图片的代码**/ 465 | cd ..// 466 | react-native run-android 467 | ``` 468 | 待启动Android项目后,图片顺利的读取到了。 469 | 470 | 在iOS版本中,我们可以通过开发一个shell脚本,并把它添加到Xcode项目的run script phase中,待启动iOS项目后,却发现资源文件根本读不到。这是为什么呢? 471 | 472 | ####原因分析 473 | 474 | #####-iOS中React Native项目启动顺序: 475 | 476 | + 在启动React Native Xcode项目时,会先加载项目所依赖的React项目,接着运行React项目中事先定义好的run script phase,最后运行packger.sh。 477 | 478 | + 其中packger.sh中我们看到如下的代码: 479 | 480 | ``` 481 | node "$THIS_DIR/../local-cli/cli.js" start "$@" 482 | ``` 483 | + 接着我们找到了cli.js,看到里面调用了好多模块。其中default.config.js模块指定了JS和资源的加载路径,server.js模块除了指定server监听的默认端口外还有检测node版本等功能,runServer.js模块用来启动server。 484 | 485 | + 待server启动成功后,才运行到iOS native code。也就是这个时候,才会运行Xcode项目中,事先定义好的run script phase中指定的shell脚本,而在这个时候,在shell脚本中创建资源路径是没有用的。所以就会出现了上面资源文件读不到的情况。 486 | 487 | #####-Android中React Native项目启动顺序: 488 | 489 | + 首先执行上面封装好的Unix executable文件,该文件中会调用资源文件生成的代码,将资源文件生成。 490 | 491 | + 然后在该文件中会继续再执行react-native run-android命令,此时根据react-native-cli模块的package.json中bin的定义,调用node.js执行$prefix/react-native-cli/index.js。 492 | 493 | + 在index.js中会先加载cli.js模块然后运行其run方法。在cli.js模块中做的工作和上面分析的iOS中的cli.js做的工作是一样的。 494 | 495 | + 待server启动成功后,才会运行到Android native code,所以运行封装好的Unix executable是不会导致资源失效的,因为资源生成代码已经在react-native run-android命令运行之前被执行过了。 496 | 497 | 498 | 499 | ####最终的解决办法 500 | 501 | 为了让资源生成的代码执行顺序提前,可以先增加一个名为AppPrepare的Command类型项目,来运行此Shell。然后在Xcode项目Target Dependencies中添加AppPrepare项目,这样就会先运行AppPrepare的项目后才会运行Xcode项目,从而达到了我们的目的。 502 | ![](../images/appprepare.png) 503 | 504 | 图2 Xcode项目依赖图 505 | --------------------------------------------------------------------------------