用React-Native开发一款App

作者:bibodeng 发布于:2016-6-5 12:21 Sunday 分类:技术交流

React-Native开发一款App

想做一款App

我有一个用emlog的博客,我常常在上面发点碎语,吐槽一下生活,偶尔发发段子。虽然只有几个比较要好的哥们看,但是我还是觉得不够便捷,每次都需要打开电脑上的浏览器,访问我的博客,才能看到里面的文字。于是,我打算搞一个App出来,说搞就搞,于是在emlog的基础上,将一个php程序改写成API的方式,输出Json格式的数据。只用了一个转换函数:

  1. // 返回json编码的数据
  2. function response_json($status, $status_message, $data)
  3. {
  4. header("HTTP/1.1 $status $status_message");
  5. $response['status'] = $status;
  6. $response['status_message'] = $status_message;
  7. $response['data'] = $data;
  8. $json_response = json_encode($response);
  9. echo $json_response;
  10. }

使用起来就像这个样子:

  1. function getTwitter()
  2. {
  3. $tid = isset($_GET['tid']) ? intval($_GET['tid']) : 1;
  4. $Twitter_Model = new Twitter_Model();
  5. $tw = $Twitter_Model->getTweet($tid);
  6. }

当然,服务端为了完成所有的功能,还需要自己动手编写逻辑代码,由于主要是数据库的增删改查,这里就按下不提了。

Why React-Native

为啥用React-Native呢?因为Object-C和Swift我都不会。对于曾经的前端工程师而言,没有什么能比JavaScript更熟悉了,JavaScript算的上是一款相对应用广泛的语言,前端后端终端都能通吃。在iOS及Android上开发App,除了以前的Native实现方式之外,有了一种全新的模式可供选择,这就是React-Native,一种基于JavaScript的Facebook推出的框架。它通过独特的虚拟DOM,组件化与Native组件进行一层转接,这样就能兼顾开发效率和运行效率了,最终效果和编写Native代码写出来的程序没有很大区别。而这种组件化的方式,能够尽可能地让你复用组件,而不是自己开发。

React-Native开发

使用React-Native进行开发,最好的文档就是Facebook的官方文档了,看了它的工作方式和一些基本技术之后,才具备实现一个App的基础。我是通过一本图灵社区的书《React Native入门与实践》来入门的,这本书对于入门者更加友好一点,也有详尽的例子供练习。

了解基础布局

要实现App中的布局,就必须要了解JSX语法和React-Native中的flex布局,JSX是一种类似于HTML语法标记的语言,可以在JavaScript中嵌入这类标记,从而实现元素布局和嵌套。一个很简单的JSX例子就是直接返回一个标签,一个React-Native组件有好几个组成部分,整个组件被一个Class定义,内部含有初始化状态的getInitialState, 组件加载完成后的componentDidMount及组件渲染函数Render

  1. import React, {
  2. AppRegistry,
  3. Component,
  4. StyleSheet,
  5. Text,
  6. View
  7. } from 'react-native';
  8. class MonkeySay extends Component {
  9. // 组件渲染
  10. render() {
  11. return (
  12. <View style={styles.container}>
  13. <Text style={styles.welcome}>
  14. Welcome to React Native!
  15. </Text>
  16. <Text style={styles.instructions}>
  17. To get started, edit index.android.js
  18. </Text>
  19. <Text style={styles.instructions}>
  20. Shake or press menu button for dev menu
  21. </Text>
  22. </View>
  23. );
  24. }
  25. }

对于布局,它和CSS布局有相似之处,也有不同之处。例如color和width,height这些常见的布局是类似的,但是flex布局和CSS的绝对相对布局有很大不一样,那就是flex布局尽可能地让box根据一些规则自动排列,例如下面的一些属性,用于控制box的呈现:

  • alignItems 在主轴上的对齐方式
  • alignSelf 在次轴上的对齐方式
  • flex 用于控制box的比例
  • flexDirection 用于选择主轴,默认是Column
  • flexWrap 元素包围方式,空白or紧凑
  • justifyContent 次轴上的对齐模式

一个很经典的练习是,如何在屏幕中布局出一个回字型的box,并且能够让里面的字体都居中展示。 
完成了以上的练习,还需要了解一些在组件中传递数据的基础知识,在组件中,可以通过属性传递值,即在标签Render的时候,传递一个属性值 如<MyList tweet={tweets} > </MyList>,这样就可以将tweets变量传递到MyList组件中,引用的方式是this.props.tweet,另外一种传递方式是,通过在具有passProps属性的组件(如Navigator)中传递到组件中。 
还有一个比较重要的概念是组件的this.state,在该组件创建的时候,便会执行一个初始化函数getInitialState,设置组件初始的state值,一般情况下this.state的函数变量的值发生变化的时候,Render函数将进行一次重绘,是否重绘由另外一个自定义函数决定。

实现核心逻辑

「猿说」要做的很简单,就是把我博客上的碎语呈现到App列表中来,如果可以还能查看详情或者评论。那么我们需要使用Ajax技术来获取在服务器上的数据,然后用ListView来实现一个列表,容纳碎语的数据,并设计其布局,让其完美呈现出来。

首先,按照默认程序的结构,新建一个组件单独存储到一个js文件中,我们暂且就叫它TweetList.js吧,下面一步步来实现其功能。

  1. 'use strict';
  2. var React = require('react-native');
  3. var TweetDetail = require('./TweetDetail'); // 详情页面
  4. var API = require('./ServerAPI'); // API地址
  5. var Authorize = require('./Authorize'); // API授权相关
  6. var {
  7. Text,
  8. View,
  9. StyleSheet,
  10. Image,
  11. Navigator,
  12. TouchableOpacity,
  13. ActivityIndicatorIOS,
  14. TabBarIOS,
  15. ListView,
  16. AppState,
  17. RefreshControl,
  18. Platform,
  19. Component,
  20. AlertIOS,
  21. } = React;
  22. var ds = new ListView.DataSource({
  23. rowHasChanged: (row1, row2) => row1 !== row2,
  24. sectionHeaderHasChanged: (s1, s2) => s1 !== s2
  25. });
  26. var TweetsList = React.createClass({
  27. getInitialState: function() {
  28. return {
  29. dataSource: [],
  30. loaded: false,
  31. message: '', // 报错信息
  32. maxPage: 1,
  33. refreshing: true,
  34. screen_name: '游客',
  35. };
  36. },
  37. // 加载
  38. componentDidMount: function() {
  39. // 尚未加载过,则请求初始数据
  40. if(!this.state.loaded) {
  41. this._loadinitData(1);
  42. }
  43. },
  44. // 获取数据
  45. getData: async function(pos, pageIndex) {
  46. if(!this.state.dataSource) {
  47. var pageIndex = 1;
  48. }
  49. var url = API.getTwitters+ pageIndex;
  50. var nonce = Authorize.makeNonce();
  51. fetch(url, {
  52. method: 'GET',
  53. headers: {
  54. 'Accept': 'application/json',
  55. 'appId': Authorize.appId,
  56. 'nonce': nonce,
  57. 'sign': Authorize.makeSign(Authorize.appId, Authorize.appKey, nonce), // API做了签名处理
  58. }
  59. })
  60. .then((response) => response.json())
  61. .then((responseData) => {
  62. if(responseData.status == 200) {
  63. if(this.state.dataSource == null
  64. || this.state.dataSource.length < 1) { // 首次加载
  65. this.setState({
  66. dataSource: [responseData.data], // 将数据绑定到state中
  67. loaded: true,
  68. maxPage: 1,
  69. });
  70. } else {
  71. var arr = this.state.dataSource;
  72. if(pos=="top")
  73. {
  74. arr.unshift(responseData.data);
  75. }
  76. else
  77. {
  78. arr.push(responseData.data);
  79. }
  80. this.setState({
  81. dataSource: arr,
  82. loaded: true,
  83. maxPage: this.state.maxPage < pageIndex ? pageIndex : this.state.maxPage,
  84. });
  85. }
  86. } else {
  87. AlertIOS.alert('提示','暂无最新,请稍等片刻~');
  88. }
  89. }
  90. ).catch(error=>{
  91. })
  92. .done();
  93. },
  94. // 异步加载数据
  95. _loadinitData :async function() {
  96. await this.getData("bottom",1);
  97. },
  98. // 点击单条查看详情
  99. rowPressed: function(tid) { // 点击进入详情
  100. this.props.navigator.push({
  101. title: "详情",
  102. component: TweetDetail,
  103. passProps: {tid: tid, screen_name: this.state.screen_name} // 使用了passProps
  104. });
  105. },
  106. // 渲染整个列表,列表带refreshControl
  107. render: function(){
  108. try
  109. {
  110. //this.refreshState();
  111. if (!this.state.loaded)
  112. {
  113. return this.renderLoading();
  114. }
  115. else
  116. {
  117. return (
  118. <ListView
  119. dataSource={ds.cloneWithRowsAndSections(this.state.dataSource)}
  120. renderRow={this._renderRow}
  121. style={styles.listView, {marginTop: (Platform.OS === 'ios')? 64:48}}
  122. initialListSize={20}
  123. pageSize={10}
  124. scrollRenderAheadDistance={50}
  125. removeClippedSubviews={true}
  126. minPulldownDistance={30} // 最新下拉长度
  127. onEndReached={this.onEndReached}
  128. onEndReachedThreshold={100}
  129. renderFooter={this.renderFooter}
  130. refreshControl={
  131. <RefreshControl
  132. ref="listRefreshControl"
  133. refreshing={!this.state.loaded}
  134. onRefresh={this._reloadLists}
  135. tintColor= "#ccc"
  136. title="正在拉取数据..."
  137. />
  138. }
  139. />);
  140. }
  141. }
  142. catch(err)
  143. {
  144. AlertIOS.alert("提示",err);
  145. }
  146. }
  147. // 渲染单条
  148. _renderRow: function(rowData, sectionID, rowID) {
  149. try
  150. {
  151. if(rowData!=null)
  152. {
  153. return (
  154. <TouchableOpacity
  155. activeOpacity={0.4}
  156. onPress={() => this.rowPressed(rowData.id)}
  157. >
  158. <View>
  159. <View style={styles.rowContainer}>
  160. <Image style={styles.thumbnail} source={require('image!writer')} />
  161. <View style={styles.textContainer}>
  162. <Text style={styles.name}>bibo-果冻</Text>
  163. <Text style={styles.tContent}>{rowData.resource.content}</Text>
  164. {(() => {
  165. if(rowData.resource.img!=null&&rowData.resource.img.length>0)
  166. {
  167. return (<View style={styles.imgContainer}>
  168. <Image style={styles.tpic} source={{uri: API.imgBaseUrl + rowData.resource.img}} />
  169. </View>);
  170. }
  171. else
  172. {
  173. return (<Text></Text>);
  174. }
  175. })()}
  176. <View style={styles.row}>
  177. <Text style={{flex:1}}>{rowData.resource.date}</Text>
  178. <Text style={styles.commNum}> 评论({rowData.resource.replynum})</Text>
  179. </View>
  180. </View>
  181. </View>
  182. <View style={styles.separator}/>
  183. </View>
  184. </TouchableOpacity>
  185. );
  186. }
  187. else
  188. {
  189. return (<Text>None</Text>);
  190. }
  191. }
  192. catch(err)
  193. {
  194. AlertIOS.alert("提示",err);
  195. }
  196. },
  197. // 渲染加载标志
  198. renderLoading: function()
  199. {
  200. return (<ActivityIndicatorIOS
  201. hidden='false'
  202. size='large'/> );
  203. },
  204. // 下拉重新渲染,获取最新数据
  205. _reloadLists: function() {
  206. this.setState({loaded: false});
  207. setTimeout(() => {
  208. this.setState({dataSource: []});
  209. this.getData("top", 1); // 从第一页获取
  210. this.setState({loaded: true});
  211. }, 500);
  212. },
  213. });

App的总体逻辑就是,加载之后,先通过API获取初始的数据并渲染,总体程序如上。除了上面所说的React-Native组件中的几个基础函数,要实现一个真正可以用的List,还需要添加很多业务逻辑。渲染方面,使用state中的数据进行渲染,渲染一个碎语列表,可以看成是渲染多个单条的碎语,故而可以把渲染单条的代码抽出来,即使用renderRow方法,甚至是将单条的碎语独立封装为一个组件,即在标签内部嵌入子标签。此外最核心的,莫过于从服务器端获取数据,我们使用一个fetch函数进行http的请求,从url指定的API中获取数据。fetch结构如下:

  1. fetch(url, {
  2. method: 'GET',
  3. headers: {
  4. 'Accept': 'application/json',
  5. 'appId': Authorize.appId,
  6. 'nonce': nonce,
  7. 'sign': Authorize.makeSign(Authorize.appId, Authorize.appKey, nonce), // API做了签名处理
  8. }
  9. })
  10. .then((response) => response.json())
  11. .then((responseData) => {
  12. // TODO:自定义逻辑
  13. }
  14. ).catch(error=>{
  15. // 建议做异常处理
  16. })
  17. .done();

fetch请求发出后,将会返回一个promise对象,你可以对它进行.then().then.()的链处理。很经典的处理办法就是讲response中的数据json化,然后在下面的自定义逻辑中,将json数据赋予到state中。 
数据加载到本地之后,对数据的渲染还需要添加布局属性,给标签内的style属性赋值,即可让该标签拥有布局属性了。

  1. var styles = StyleSheet.create({
  2. container: {
  3. flex: 1,
  4. flexDirection: 'row',
  5. justifyContent: 'center',
  6. alignItems: 'center',
  7. backgroundColor: '#F5FCFF',
  8. },
  9. listView: {
  10. paddingTop: 60,
  11. backgroundColor: '#EDEDED',
  12. },
  13. });

使用StyleSheet.create接口即可创建一组布局描述类,在使用的时候,即可直接引用styles.listView来给标签的style属性赋值:

  1. return (
  2. <ListView
  3. dataSource={ds.cloneWithRowsAndSections(this.state.dataSource)}
  4. renderRow={this._renderRow}
  5. style={styles.listView, {marginTop: (Platform.OS === 'ios')? 64:48}}
  6. initialListSize={20}
  7. pageSize={10}
  8. />);

sytle可以通过两种方式添加描述,一种是赋值,一种是内嵌,直接在标签里定义style={{marginTop: 64}},即可指定组件距离顶部位置了,两种方法是等效的,相当于css中独立样式文件和内嵌样式。 
组件实现后的最终效果应该是这样的。 
猿说界面

App打包和上架

经过一番开发之后,如果App已经完成了,那么就可以提交上架了。上架的过程包括 申请证书、申请AppId、打包、上传包和提交审核。申请证书和申请AppId之类的有许多文章可供参考,例如这篇 App上架流程 ,里面从申请开发者账号到下载各种证书都已经有详细的步骤。

我们这里主要说一下打包,通过XCode进行编译打包,需要将App编译输出调整至Generic iOS Device,然后将React-Native调试模式(调试模式时代码在电脑的服务上)的代码切换成从编译的代码包中获取,即从OPTION1切换到OPTION2。

  1. /**
  2. * Loading JavaScript code - uncomment the one you want.
  3. *
  4. * OPTION 1
  5. * Load from development server. Start the server from the repository root:
  6. *
  7. * $ npm start
  8. *
  9. * To run on device, change `localhost` to the IP address of your computer
  10. * (you can get this by typing `ifconfig` into the terminal and selecting the
  11. * `inet` value under `en0:`) and make sure your computer and iOS device are
  12. * on the same Wi-Fi network.
  13. */
  14. //jsCodeLocation = [NSURL URLWithString:@"http://127.0.0.1:8081/index.ios.bundle?platform=ios&dev=false"];
  15. /**
  16. * OPTION 2
  17. * Load from pre-bundled file on disk. The static bundle is automatically
  18. * generated by the "Bundle React Native code and images" build step when
  19. * running the project on an actual device or running the project on the
  20. * simulator in the "Release" build configuration.
  21. */
  22. jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

然后从Xcode-》product-》Archive中进行打包,打包完成可以上传到AppStore上,但是注意,项目配置中的Version+build不能已经存在,否则上传会失败。上传完成后,在苹果developer后台即可看到上传的包了,填写相关信息如截图、评级、广告标识等,即可提交审核了,苹果一般在一周时间内审核,然后告知结果。注意,广告标识等一些东西,有即勾选有,没有即勾选没有,否则也能被核查出来,判为不过。

如果幸运,你的App通过审核了,那么上架后即可在AppStore上看到了,因为存在缓存,一般在上架后半小时才能查到。下图是猿说上线后在AppStore中搜索的结果。 
AppStore搜索猿说

标签: app iOS 猿说

发表评论:

Powered by emlog 京ICP备16017775