RNAPPGithub
学习课程:
《双平台真实开发GitHub App React Native技术全面掌握》399 大洋,含着泪吃了一个月泡面..
Github 代码库:https://github.com/ReverseScale/RNAPPGithub.git
知识点:
- 1.整理 ReactNative 网络封装及常用的三方模块
- 2.ReactNative 的 AsyncStorage数据库技术、离线缓存
- 3.ReactNative 的数据 DAO 层设计技巧,数据状态实时更新
- 4.ReactNative 的代码提取技巧,组合模式应用技巧,数据异步刷新与动态添加
- 5.ReactNative 的版本升级、数据统计、社会化分享、第三方登录,热更新等
- 6.ReactNative 项目发布前的优化、打包与上线
目录结构:
- 生命周期及使用场景
- 项目介绍
- 组件化
- 集成与管理
- 布局约束
- 组件封装
- 本地持久化
- 网络请求封装
- 功能调试
- 双平台适配
- 开源组件库的使用
- 热更新
生命周期及使用场景
常用方法:
- constructor:
- componentWillMount:
- render:
- componentDidMount:
- componentWillReceiveProps:
- shouldComponentUpdate:
- componentWillUpdate:
- componentDidUpdate
- componentWillUnmount:
1.constructor:
在组件创建的时候调用一次,这个方法进行this.state初始化状态机。
constructor(props) {
super(props); // 初始状态
this.state = {
isShow:true
};
}
复制代码
2.componentWillMount:
在组件生命周期中只会被执行一次,在初始渲染(render函数)前被执行。 注意:
-
如果这个函数通过setState函数修改状态机变量,RN框架不会额外执行渲染(界面刷新)
-
如果子组件也有componentWillMount函数使用,会在父组件之后调用
-
需要从本地存储中读取数据用于显示,常用这个函数
3.render:
该函数组件必有的,通过返回JSX或其他组件来构成DOM,换言之,就是组件的核心渲染过程。
4.componentDidMount:
在React Native组件的生命周期中,这个函数只会被执行一次,它在初始渲染完成后会马上被调用。在这之后开发者可以通过子组件的引用来访问,操作任何子组件。 如果RN组件的子组件也有componentDidMount函数,并会在父组件的componentDidMount函数之前被调用。
- 常用来网络请求数据
5.componentWillReceiveProps:
在React Native组件的初始渲染完成后,props改变时,这个函数被调用,参数是个新的props。
6.shouldComponentUpdate:
React Native组件的初始渲染执行完成后, RN组件接收到新的state或者props时这个函数会调用。
- 通过这个函数阻止无必要的重新渲染,是提高React Native应用程序性能的一大技巧。
7.componentWillUpdate:
初始渲染完成后,重新渲染前会调用这个函数。
-
这个函数不能通过this.setState再次改变状态机变量的值。
-
组件更新时调用。
8.componentDidUpdate
RN组件初始渲染完成后,RN框架在重新渲染RN组件完成后调用。 参数是渲染前的props和state。
- 组件更新完毕时调用。
9.componentWillUnmount:
ReactNative 组件被卸载前会调用,通常做一些清理内容。
项目介绍
双平台效果预览:
React Native是React在移动端的跨平台方案。如果想更快地理解和掌握React Native开发,就必须先了解React。
React是FaceBook开源的一个前端框架,它起源于 Facebook 的内部项目,并于 2013 年 5 月开源。因为React 拥有较高的性能,代码逻辑非常简单,所以越来越多的人已开始关注和使用它,目前该框架在Github上已经有7万+star。
ReactNative 技术导图:
组件化
React采用组件化的方式开发,通过将view构建成组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。有一句话说的很形象:在React中,构建应用就像搭积木一样。
组件化特征
React认为一个组件应该具有如下特征:
-
可组合(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件;
-
可重用(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景;
-
可维护(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护;
封装好的导航栏就可以被称之为一个组件,它符合上述三个特点:
-
可组合:可以将导航栏组件放在页面组件中作为页面组件的子组件。而且在导航栏组件的内部,也有按钮组件等子组件。
-
可重用:如果封装好了该组件,就可以放在任意需要导航栏的页面(组件)使用,也可以放在其他项目中使用。
-
可维护:因为具有独立的功能和展示逻辑,所以便于定位和修改。
组件的属性与状态
在React Native(React.js)里,组件所持有的数据分为两种:
-
属性(props):组件的props是不可变的,它只能从其他的组件(例如父组件)传递过来。
-
状态(state):组件的state是可变的,它负责处理与用户的交互。在通过用户点击事件等操作以后,如果使得当前组件的某个state发生了改变,那么当前组件就会触发render()方法刷新自己。
我们可以看到这个页面有两个子页面,一个是‘最热’页面(组件),另一个是‘趋势‘页面(组件)。那么这两个组件都有什么props和state呢?
1.props: 由于props是从其父组件传递过来的,那么可想而知,props的声明应该是在当前组件的父组件里来做。在React Native中,通常props的声明是和当前组件的声明放在一起的:
//最热子页面
'最热' flag={FlAG_STORAGE.flag_popular}/>
//趋势子页面
'趋势' flag={FlAG_STORAGE.flag_trending}/>
复制代码
在这里,收藏页面是父组件,而最热页面和趋势页面是其子组件。在收藏页面组件里声明了最热页面和趋势页面的组件。
而且我们也可以看到,最热页面和趋势页面组件都用的是同一个组件:FavoriteTabPage,而这两个页面的不同点只在于传入的两个props的不同:tabLabel和flag。
而在FavoriteTabPage组件内部,如果想调用flag这个props,可以使用this.props.flag来调用。
2.state:
下面是最热和趋势页面的组件:
class FavoriteTabPage extends Component{
//组件的构造方法
constructor(props){
super(props);
this.state={
dataSource:new ListView.DataSource({rowHasChanged:(r1,r2)=>r1!==r2}),isLoading:false,
}
}
复制代码
这里面定义了两个state:
- dataSource:列表的数据源
- isLoading:是否正在刷新
这两个state都是将来可能经常变化的。比如在网络请求以后,列表的数据源会被替换掉,这个时候就要调用:
this.setState({
//把新的值newDataArr对象传给dataSource
dataSource:newDataArr
})
复制代码
3.DOM DOM 是前端的一个概念,暂时可以粗略理解为一个页面的树形结构。 React 生命周期的三大阶段
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
在每个阶段都有相应的状态和与之对应的回调函数,具体可以看下图:
上图来自:贾鹏辉的技术博客:React Native之React速学教程(中)
集成与管理
1.指定版本初始化 在终端输入react-native demo --version 0.40.0命令以后,就会初始化一个React Native版本为0.40.0的项目。这个最初项目里面直接就包含了iOS和Android的工程文件夹,可以用对应的IDE打开后编译运行。
在新建一个React Native项目之后的根目录结构是这样的:
2.使用 Cocoapods 管理 ReactNative Podfile 文件格式:
pod 'React', :path => './node_modules/react-native', :subspecs => [
'Core',
'RCTText',
'RCTImage',
'RCTActionSheet',
'RCTGeolocation',
'RCTNetwork',
'RCTSettings',
'RCTVibration',
'RCTWebSocket',
]
复制代码
ReactNative 0.42.0 以上版本需在 Podfile 配置 yoga:
# 如果你的RN版本 >= 0.42.0,请加入下面这行
pod "yoga", :path => "./node_modules/react-native/ReactCommon/yoga"
复制代码
输入react-native run-ios或者react-native run-android指令, 就会自动打开模拟器运行项目(前提是安装了相应的开发环境)。
但是一个比较完整的项目仅仅有这些类别的文件是不够的,还需要一些工具类,模型类,资源等文件。为了很好地区分它们,使项目结构一目了然,需要组织好项目文件夹以及类的命名,下面是我将教程里的文件夹命名和结构稍加修改后的一个方案,可供大家参考:
布局约束
采用Flex布局的元素,被称为Flex container,其所有子元素被称为Flex item;容器默认存在两个轴,分别是主轴(main axis)和垂直的交叉轴(cross axis),主轴开始的位置叫做main start,结束的位置叫main end;交叉轴的开始位置叫做cross start,结束的位置叫做cross end;单个item占据的主轴空间叫做main size,占据的交叉轴控件叫做cross size。
如下图所示:
组件化驱动下,搜索结果页中展示的 Cell 与之前的列表页 Cell 可以重用:
我们把该组件定名为:RespositoryCell,结合代码来看一下具体的实现:
export default class RepositoryCell extends Component {
constructor(props) {
super(props);
this.state = {
isFavorite: this.props.projectModel.isFavorite,
favoriteIcon: this.props.projectModel.isFavorite ? require('../../res/images/ic_star.png') : require('../../res/images/ic_unstar_transparent.png'),
};
}
componentWillReceiveProps(nextProps) {
this.setFavoriteState(nextProps.projectModel.isFavorite)
}
setFavoriteState(isFavorite) {
this.props.projectModel.isFavorite = isFavorite;
this.setState({
isFavorite: isFavorite,
favoriteIcon: isFavorite ? require('../../res/images/ic_star.png') : require('../../res/images/ic_unstar_transparent.png')
})
}
onPressFavorite() {
this.setFavoriteState(!this.state.isFavorite)
this.props.onFavorite(this.props.projectModel.item, !this.state.isFavorite)
}
render() {
let item = this.props.projectModel.item? this.props.projectModel.item:this.props.projectModel;
let favoriteButton=this.props.projectModel.item?
this.onPressFavorite()} underlayColor='transparent'>
:null;
return (
{item.full_name}
{item.description}
Author:
Star:
{item.stargazers_count}
{favoriteButton}
)
}
}
复制代码
-
这里声明了RespositoryCell组件,它继承于Component,也就是组件类,即是说,声明组件的时候必须都要继承与这个类。
-
集中看一下该组件的render方法,它返回的是该组件的实际布局:在语法上使用JSX,类似于HTML的标签式语法,很清楚地将cell的层级展现了出来:
-
最外层被一个View组件包裹着,里面第一层有三个子组件:两个Text组件和一个作为底部背景的View组件。
-
底部背景的View组件又有三个子组件:View组件(显示作者信息),View组件(显示star信息),收藏按钮。
-
结构分解图:
组件封装
我的页面 | 个人中心 |
---|---|
对于“我的页面”和“个人中心”这类结构相似的页面,建议进行组件封装,封装后的 AboutPage 实现代码简洁如下:
export default class AboutPage extends Component{
constructor(props) {
super(props);
this.aboutCommon=new AboutCommon(props,(dic)=>this.updateState(dic),FLAG_ABOUT.flag_about,config);
this.state = {
projectModels: [],
author:config.author
}
}
componentDidMount() {
this.aboutCommon.componentDidMount();
}
componentWillUnmount() {
this.aboutCommon.componentWillUnmount();
}
updateState(dic){
this.setState(dic);
}
onClick(tab) {
let TargetComponent, params = {...this.props,menuType:tab};
switch (tab) {
case MORE_MENU.About_Author:
TargetComponent = AboutMePage;
break;
case MORE_MENU.Website:
TargetComponent = WebViewPage;
params.title='GitHubPopular';
var url='https://reversescale.github.io';
params.url=url;
break;
case MORE_MENU.Feedback:
var url='mailto://[email protected]';
Linking.canOpenURL(url).then(supported => {
if (!supported) {
console.log('Can\'t handle url: ' + url);
} else {
return Linking.openURL(url);
}
}).catch(err => console.error('An error occurred', err));
break;
case MORE_MENU.Share:
break;
}
if (TargetComponent) {
this.props.navigator.push({
component: TargetComponent,
params: params,
});
}
}
render() {
let content=
{this.aboutCommon.renderRepository(this.state.projectModels)}
{ViewUtils.getSettingItem(()=>this.onClick(MORE_MENU.Website), require('../../../res/images/ic_computer.png'), MORE_MENU.Website, this.props.theme.styles.tabBarSelectedIcon)}
{ViewUtils.getSettingItem(()=>this.onClick(MORE_MENU.About_Author), require('../my/img/ic_insert_emoticon.png'), MORE_MENU.About_Author, this.props.theme.styles.tabBarSelectedIcon)}
{ViewUtils.getSettingItem(()=>this.onClick(MORE_MENU.Feedback), require('../../../res/images/ic_feedback.png'), MORE_MENU.Feedback, this.props.theme.styles.tabBarSelectedIcon)}
return this.aboutCommon.render(content, {
'name': 'GitHub Popular',
'description': '这是一个用来查看GitHub最受欢迎与最热项目的App,它基于React Native支持Android和iOS双平台。',
"avatar": "http://og1yl0w9z.bkt.clouddn.com/18-3-28/61685877.jpg",
"backgroundImg": "http://og1yl0w9z.bkt.clouddn.com/18-3-28/37407402.jpg",
});
}
}
复制代码
本地持久化
主题选择界面 | 切换主题后界面 |
---|---|
在涉及如主题变更等操作时,需要将状态信息保存,这时就需要用到类似于iOS 中的NSUserDefault, AsyncStorage 是React Native中的 Key-Value 存储系统,可以做本地持久化。
首先看它主要的几个接口:
根据键来获取值,获取的结果会放在回调函数中:
static getItem(key: string, callback:(error, result))
复制代码
根据键来设置值:
static setItem(key: string, value: string, callback:(error))
复制代码
根据键来移除项:
static removeItem(key: string, callback:(error))
复制代码
获取所有的键:
static getAllKeys(callback:(error, keys))
复制代码
设置多项,其中 keyValuePairs 是字符串的二维数组,比如:[['k1', 'val1'], ['k2', 'val2']]:
static multiSet(keyValuePairs, callback:(errors))
复制代码
获取多项,其中 keys 是字符串数组,比如:['k1', 'k2']:
static multiGet(keys, callback:(errors, result))
复制代码
删除多项,其中 keys 是字符串数组,比如:['k1', 'k2']:
static multiRemove(keys, callback:(errors))
复制代码
清除所有的项目:
static clear(callback:(error))
复制代码
网络请求封装
在React Native中,经常使用Fetch函数来实现网络请求,它支持GET和POST请求并返回一个Promise对象,这个对象包含一个正确的结果和一个错误的结果。
来看一下用Fetch发起的POST请求封装:
static post(url,data){
return new Promise((resolve,reject)=>{
fetch(url,{
method:'POST',
header:{
'Accept':'application/json',
'Content-Type':'application/json'
},
body:JSON.stringify(data)
})
.then(response=>response.json())
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error);
})
})
}
复制代码
从上面的代码中,我们可以大致看到:Fetch函数中,第一个参数是请求url,第二个参数是一个字典,包括方法,请求头,请求体等信息。
随后的then和catch分别捕捉了fetch函数的返回值:一个Promise对象的正确结果和错误结果。注意,这里面有两个then,其中第二个then把第一个then的结果拿了过来。而第一个then做的事情是把网络请求的结果转化为JSON对象。
那么什么是Promise对象呢?
Promise 是异步编程的一种解决方案,Promise对象可以获取某个异步操作的消息。它里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
它分为三种状态:
Pending(进行中)、Resolved(已成功)和Rejected(已失败)
它的构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject:
- resolve函数的作用:将Promise对象的状态从“未完成”变成“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
- reject函数的作用:将Promise对象的状态从“未完成”变成“成功”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
GET 请求封装:
static get(url){
return new Promise((resolve,reject)=>{
fetch(url)
.then(response=>response.json())
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error);
})
})
}
复制代码
因为只是GET请求,所以不需要配置请求体,而且因为这个fetch函数返回值是一个Promise对象, 所以我们可以用.then和.catch来捕捉正确和错误的结果。
功能调试
我们可以使用浏览器的开发者工具来调试React Native项目,可以通过打断点的方式来看数据信息以及方法的调用:
- 首先在iOS模拟器中点击command + D,然后再弹出菜单里点击Debug JS Remotely。随后就打开了浏览器进入了调试。
- 浏览器一般会展示下面的页面,然后点击command + option + J进入真生的调试界面。
双平台适配
因为React Native讲求的是一份代码跑在两个平台上,而客观上这两个平台又有一些不一样的地方,所以就需要在别要的时候做一下两个平台的适配。
例如导航栏:在iOS设备中是存在导航栏的,而安卓设备上是没有的。所以在定制导航栏的时候,在不同平台下给导航栏设置不同的高度:
const NAV_BAR_HEIGHT_IOS = 54;
const NAV_BAR_HEIGHT_ANDROID = 50;
// css
navBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
height: Platform.OS === 'ios' ? NAV_BAR_HEIGHT_IOS : NAV_BAR_HEIGHT_ANDROID,
},
复制代码
上面的Platform是React Native内置的用于区分平台的库,可以在引入后直接使用。
建议在调试程序的时候,同时打开iOS和Android的模拟器进行调试,因为有些地方可能在某个平台上是没问题的,但是另一个平台上有问题,这就需要使用Platform来区分平台。
开源组件库的使用
ReactNative 的组件与原生的组件有许多共同之处,如下拉刷新,同样的 Github 中开源组件已经相当完善。
开源组件库方法如下,安装最新版本:
npm install react-native-tab-navigator --save
复制代码
安装指定版本:
npm install --save [email protected]
复制代码
react-native 集成组件绑定(ReactNative 0.27以后,自集成RNPM):
react-native link react-native-splash-screen
复制代码
常用开源库:
npm install --save [email protected]
npm install --save [email protected]
npm install --save [email protected]
npm install [email protected]
npm install --save [email protected]
npm install --save [email protected]
npm install --save [email protected]
npm install [email protected]
复制代码
下图为使用 react-native-splash-screen 后的效果演示:
热更新
CodePush 是微软提供的一套用于热更新 React Native 和 Cordova 应用的服务。
CodePush 是提供给 React Native 和 Cordova 开发者直接部署移动应用更新给用户设备的云服务。CodePush 作为一个中央仓库,开发者可以推送更新 (JS, HTML, CSS and images),应用可以从客户端 SDK 里面查询更新。
Code-Push 推包命令:
code-push release-react [options]
复制代码
示例:
code-push release-react RNAPPGithub ios --t 1.0.0 --dev false --d Staging --des "1.热更新我的页面背景色" --m true
复制代码
code-push release-react RNAPPGithub ios --t 1.0.1 --dev false --d Staging --des "1.热更新相关设置" --m true
复制代码
Code-Push 线上查看更新:
code-push deployment ls RNAPPGithub
复制代码
Code-Push 查看项目Key:
code-push deployment ls RNAPPGithub -k
复制代码
Code-Push iOS更新打包方法:
react-native bundle --platform ios --entry-file index.ios.js --bundle-output release_ios/main.jsbundle --assets-dest release_ios/ --dev fasle
复制代码
总结
之前也有零零散散的调研这门技术,但是经过系统的 15 个下午的坚持学习,深感跨平台技术的独到之处,特别是在与原生交互的编写上简直让人欲生欲死,还好在 Github 上已经有各路大神开源的各种方便开发的组件库可以供我们使用。
分享链接
《React Native 开发常用命令行(持续更新)》(https://juejin.im/post/5abc54c86fb9a028da7c998c)
《ReactNative 开发常用的三方模块》(https://www.jianshu.com/p/53ff78168acc)
《使用VS Code调试React-Native程序》(https://jingyan.baidu.com/article/ad310e80fb13fc1849f49ed1.html)
参考资料
-
React Native中文网
-
贾鹏辉的技术博客
-
从一个实战项目来看一下React Native开发的几个关键技术点
-
Marno:给所有开发者的React Native详细入门指南
-
大漠:一个完整的Flexbox指南
-
阮一峰:Flex 布局教程:语法篇
-
八段代码彻底掌握 Promise
-
阮一峰:Promise对象
-
asce1885:React Native 高质量学习资料汇总
-
世锋日上:ReactNative 学习资源大汇集