React Native 贯穿全栈开发 APP

课程网址

0 课程介绍

0.1 谁需要这门课程?

课程价值

即将工作,需要一个足够打动面试官的求职作品。

职业发展遇到瓶颈,需要一个打通前后端任督二脉的超车技术栈。

想独立创业,但公司技术人员有限,需要一种能力通吃前后端开发。

882D0CDB-DB4D-4AC5-B1A1-92C80B1CFB19

适用人群

AED8B097-DB89-4C53-8CB2-E3139B18D44A

0.2 实战项目介绍

完整的 APP 开发流程

570D9EFE-A0CC-469F-BCA4-91C211CA3979

0.3 APP 功能介绍

桌面图标和启动画面

2F3BB186-3327-4570-9A2E-E5A045F7B577

过度页面和轮播效果

18A3986C-D085-4AB6-9ADF-840A6F430F78

注册登录和账户管理

130F9764-04B8-4F04-9ED5-AF3AB490607C52A268EC-E64E-4474-9165-F49A79918AADF9E03C12-AB29-4BC0-85DD-39C1CD318FCD

视频的配音制作页面

FB55C162-8EEB-4D53-BFBF-84D8088EB6D5673134D8-1109-491F-8847-F350D1C15D21

DDB6085D-A10F-4F77-B3DD-88CA39E4B307 1AC37DB1-5952-4F98-948A-652E76AA26F1

展现所有制作完毕的创意视频列表

9D5AE200-A2A3-493F-98F7-E556D9A0E7F6F5A2E11D-8BCE-41F9-910B-ED6ED175665ACAE44957-D7E2-49C9-9AFF-C62A01C947BD

0.4 APP 后台

A91AFC02-5CD0-4503-9F50-0D7FD254E490

0.5 要点

环境搭建

557CE284-84C8-48C0-912E-B785D565264F

服务器端

17B2329A-63D9-4F63-B913-719BD59CFE20

前后端连调

5B6CDA95-0E79-4BA9-923F-71673D97C9F9

1 课程预热

1.1 React 与 React Native

React 是 Facebook 推出的一种解决页面组建的抽象和形态的技术方案。它有两个工程上的实现

Reactjs, 也就是我们通常说的 React,用在 Web 网页端,也就是用在浏览器中。

React Native, 我们俗称 RN, React Native 用在移动客户端,也就是 iOS 或者 android 手机 APP。看看市面上使用了 React Native 的 showcase

React Native 的性能

B37DFE6F-B67F-4087-B827-0BD00621E4EA

Hybrid 混合应用

原生的应用中利用 webview 装入一些 HTML 界面。

codava “包皮” 应用

原生的部分只是一个壳子,开发用的全是 WEB 技术,比如 PhoneGap ,一次编写,到处使用。相比原生应用,一些复杂的页面有明显的性能问题。

5A927112-FBB2-4237-87DA-552ED6984037

React Native

RN 可以看作是基于 React 之上的一种针对特定平台的技术开发方案,其内在依然是 React。用开发WEB的方式开发 APP,利用原生 API 渲染界面,从而获得接近原生 APP 的性能。

1.2 为何选择 React Native

☑ 职业发展

535E8284-335C-4DB4-9652-BDA122A9F6EB

☑ 优秀的核心思想

FFAA5529-FE25-4BD1-AC20-0A77ECD232E4

☑ 刚毕业或刚入行的新人,会感觉跟不上市场对前端技术的需求。因为你们可能没有兼容过 IE6、7、8、火狐那个年代,每一行代码都断点调试,对 JS 的内部机制,DOM 的性能等细小的技术点没有那么多时间实践和研究,也没有经历过打包、组件抽象和构建,各种异步加载起的折腾,如果现在花时间重新走一遍,反而跟不上市场的脚步。预期为这个历史买单,到不如投资未来,直接切入到 React 和 React Native 的中,反而没有历史包袱。利用这个机会可以弯道超车,直接进入前端开发或者 APP 开发这个职业。职业发展是阶段性的,技术学习是终身的,现在市场上活下来,再沉淀,直到切入到更细分的技术选型。

9598C18F-AC6B-4711-BD81-CE085B720691

☑ 在语法这个技术层面 RN 的优势

  • RN 是纯的 JavaScript 组件化,不掺和不同的语言形态。
  • RN 的技术框架,允许你很方便的介入和调用到各个平台下的 API。
  • RN 的布局用到的是 flex 布局,上手成本很小。
  • 渲染原生视图的能力,性能有保障。
  • 组件化开发,快速开发原型,在交互上混,唯快不破!

总而言之,学会 React 的基本语法以后,可以去开发网页,可以去开发 iOS APP ,可以去开发 Android APP,一招走遍天下。

1.3 RN 适合你吗?

DBAAAD84-B012-49AD-A19A-D3CE9237EEE3

2 初识 React Native

2.1 本地环境搭建

参考

Facebook React Native

中文文档(持续跟进新版本)

环境准本

(1) MAC OS X 系统

(2) 系统升级到最新

(3) 下载最新版的 Xcode

1
$ xcode-select --install

(4) 安装 homebrew

1
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

(5) 安装 watchman 和 flow

  • watchman 是 faceboook 的一个开源项目,它用来监视文件并且记录文件的改动情况。
  • Flow 是一个 JavaScript 的静态类型检查器,用于找出 JavaScript 代码中的类型错误。
1
$ brew install watchman flow git gcc pkg-config cairo libpng jpeg gitlib mongodb

(6) 安装 Nodejs,建议用 nvm 来装

1
2
3
4
5
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash
$ nvm install v7.9.0
$ nvm use v7.9.0
$ nvm alias default v7.9.0
$ npm install cnpm -g

(7) 确保 8081 端口没有被占用

  • 确保没有本地服务占用 8081 端口。
  • chrome 的一些本地插件有可能使用了 websocket 占用了 8081,有的话关闭。

2.2 忍不住尝尝鲜

1
2
3
4
5
$ cnpm install -g react-native-cli -g
$ react-native -v
react-native-cli: 2.0.1

$ react-native init imoocApp

ADD36C2F-99A1-4216-95F8-48D2BB0F210A

1
2
# 使用在 iOS 模拟器中启动的第一种运行方式
$ cd imoocApp && react-native run-ios

BA5FE45C-6DD0-49FE-9737-3E22901464AF

2.3 了解 RN 项目代码结构

准备

sublime text 代码高亮

(1) 安装package control

(2) command + shift + p

  • Babel Snippets
  • SublimeLinter(对 js 语法进行检查)
  • SublimeLinter-contrib-eslint
  • Sublimelinter-jsxhint

(3) JS 代码风格检查

1
$ npm install -g eslint babel-eslint --registry=http://registry.npm.taobao.org

目录

1
2
3
4
5
6
7
8
9
.
├── __tests__
├── android
├── ios
├── app.json # 应用相关配置
├── index.android.js # android 入口
├── index.ios.js # ios 入口
├── node_modules
└── package.json

3 RN 入门知识学习

3.1 如何选择 RN 版本

RN 版本升级很频繁,好的一面是,社区狂热的氛围保障了 RN 未来的影响力,坏的一面是,一旦跟定某个版本进行开发,可能某些问题只有这些版本才会有,同时有些组件没有来得及更新或者升级,你就不敢升级,导致自己动手修改或者规避,会有一个维护的成本,因此选择本本确实是一个非常头疼的问题。

另一个话题是,真正在项目中如何选择版本?对一个 APP 开发的负责人来说,要跟进官方的迭代速度,即要保持跟官方一致的版本升级,很可能掉进一个泥潭,版本之间的差异性可以摧毁你或者你的团队。因为每个版本都会增加新的特性,或者删除老特性,而这些在文档中可能不会充分体现,那对一个稍微复杂的 APP ,升级可能会引发大面积报错。

我的建议是,只要当前的版本是稳定的,性能上没有明显的问题,功能上也能够满足当下的需求,那就不要着急去升级,尤其是线上环境。本地可能等版本至少发布两个月后再尝试,因为这个时候就算遇到一些坑,前人也已经踩得差不多了。所以,在 Github 或 Stackoverflow 或 Google 会比较容易找到解决方案。

3.2 React 的组件生命周期

3.2.1 传统的 web 开发中的组件

基本生命周期

  1. 下载 JS 组件模块(如果是同步组件组件,则没有这一步);
  2. 初始化请求参数;
  3. 请求组件数据;
  4. 拼装组件结构;
  5. 绑定事件监听;

分析

CSS 的部分需要全局先加载好,JS 也要提前加载好,因为如果都异步会导致网络延迟。但这个不是主要问题,问题是,如果全局提前加载,就需要将 CSS 打包到 CSS 文件,JS 打包到 JS 文件,从开发到上线是两套分离的逻辑,不存在组件生命周期这回事儿。到了准备数据、拼装结构、安装组件,一共三步,那么绑定事件监听就比较恶心。组件安装后,通过触发事件,在 JS 中找到 DOM 结构,再到 DOM 结构中改变样式或复原样式,要有一个操作样式和复原样式的过程。这带来两个问题:

  1. 组件的开发和上线形态要分为两套逻辑,组件安装后的事件触发需要显式地借助 JS 来操作 DOM 节点,这个操作会让数据和行为、行为和展现耦合在一起,界限是模糊的。
  2. 组件的状态是无序的、不直接的,包含在了 CSS 的交互里面,包含在了用户的鼠标事件中,包含在组件内外部的通信过程中,如果在组件的里面或外面管理组件固有的数据和状态,如何传递就成了传统 WEB 开发所面临的问题。对组件生命周期的抽象和数据状态的管理非常考察一个工程师的设计能力。

3.2.2 React 的组件

传统的 WEB 组件, WEB 中交互以及交互带来的变化要通过 JS 明确操作 DOM 结构来完成,而 RN 中都需要通过状态来控制组件。

React 的思想主要是通过构建可复用组件来构建用户界面。可以粗暴来比喻,在 React 中,一切皆组件。对组件来说,什么最重要?有人说是事件封装,有人说是界面的实现,有人说是交互状态和数据重新渲染的管理,也有人说是组件间的耦合和接口,还有人说是生命周期。对 RN 来说,可以把组件理解成有限状态机,渲染什么样的界面,由状态来决定,而每个组件是有自己的生命周期的。生命周期规定了组件的状态和方法,分别是在哪个阶段改变和执行的。也就是说,组件是有生命的,在生命周期的不同阶段要做不同的事情,这些不同的事情就是有限状态集。少年的时候就是要身体发育和早恋逃课;30岁就要成家生娃;50岁就会白了头发,退休在家。

需要注意的是,有限状态机可以理解成几个有限的状态,这个状态可以进行跳转和切换,甚至是可逆的,但人过了30岁就不能回到少年了。

什么是组件的生命周期呢?就是一个对象从开始到初始化,到证实创建,中间历经各种更新,直到最终卸载所经历的全部的状态。

React 生命周期图示

先不管父亲儿子孙子这种多层嵌套的组件,先来看一下一个单一组件的生命周期。如下

39579D57-7B5C-49D2-B860-0C852E5DC9E6

1 初始化: 渲染 > 装载

钩子函数 说明 备注
getDefaultProps 获取初始的配置参数,全局只调用一次。也就是说,组件被重新装载也不会再调用该方法。 严格说来不算组件生命周期的一部分,属性往往是在预先在组件中设置好的或者直接通过副组件传递过来。
getInitialState 获取组件初始状态值,会在组件挂载前调用一次。 一般会把只在这个组件中使用的而且会发生变化的数据放置在状态中,
componentWillMount 通知组件的调用者,组件即将被渲染和装载。
render 进行 diff 算法后,对组件进行渲染。
componentDidMount 通知组件已经挂载。 这时通常可以发送一些异步请求来进一步获取组件其它的数据。

2 运行时: state 变化 > 重新渲染

钩子函数 说明 备注
shouldComponentUpdate 通知 state 发生变化,询问要不要更新组件。默认 都返回 true。 常常为了性能,需要在这个方法中,为状态的变化的幅度和数量来做比对,从而判断是否马上更新,是否等积累到一定量才进行更新。注意,不要在该方法中调用 setState ,否则会导致死循环。
componentWillUpdate 组件即将开始渲染。 一旦 shouldComponentUpdate 返回 true ,该方法将会被调用。
render 重新进行 diff 算法后,对组件进行渲染。
componentDidUpdate 组件重新渲染后。

2 运行时: props 变化 > 重新渲染

还有一种会使组件更新的方式是外部属性的变化,这种通常来自于父组件的属性变化,是常见的组件之间相互传递数据,和同步状态的手段。在 React 中非常实用。比如父组件中登录了,子组件也需要同步这个变化。同步就是通过外层组件传入 props 来通知变化。

钩子函数 说明 备注
componentWillReceiveProps 通知外层传入的 props 属性发生了改变。 可以通过对老属性和新属性的比对,判断是否需要更新状态,需要的话,就在该函数中通过 setState 修改 state。这个修改是安全的,不会导致重复 render。注意,该方法在初始化渲染的时候是不会被调用的,只有在组件接收到 props 变化的时候才会被调用。使用该方法的时机是,在外部传入的 props 发生了变化之后,在组件更新之前,在该方法通过更新 state 引发组件重新渲染。这是在组件渲染之前最后一次可以更新 state 的机会。

3 卸载

钩子函数 说明 备注
componentWillUmount 通知组件即将被卸载。

3.2.3 RN 示例

统计屏幕点击次数。

imoocApp/index.ios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

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

export default class imoocApp extends Component {
constructor(props) {
super(props)
this.state = { times: 0 }
}
timesPlus() {
let times = this.state.times
times++
this.setState({ times })
}
componentWillMount() {
console.log('componentWillMount')
}
componentDidMount() {
console.log('componentDidMount')
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate')
return true
}
componentWillUpdate() {
console.log('componentWillUpdate')
}
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
console.log('render')
return (
<View style={styles.container}>
<Text style={styles.welcome} onPress={this.timesPlus.bind(this)}>
有本事点我一下
</Text>
<Text style={styles.instructions}>
你点了我 {this.state.times} 次
</Text>
<Text style={styles.instructions}>
Press Cmd+R to reload,{'\n'}
Cmd+D or shake for dev menu
</Text>
</View>

);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});

AppRegistry.registerComponent('imoocApp', () => imoocApp);

0B4E47F3-1143-419B-8205-1D7AC00E4C8D

3.3 父子组件撕逼大战

对于嵌套的组件的生命周期, 下面借助一个例子分析。

3.3.1 代码

index.ios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/

var React = require('react')
var {
AppRegistry,
StyleSheet,
Text,
View
} = require('react-native')

var Son = React.createClass({
getDefaultProps() {
console.log('son', 'getDefaultProps')
},
getInitialState() {
console.log('son', 'getInitialState')
return {
times: this.props.times
}
},
timesPlus() {
let times = this.state.times
times++
this.setState({ times })
},
timesReset() {
this.props.timesReset()
},

componentWillMount() {
console.log('son', 'componentWillMount')
},
componentDidMount() {
console.log('son', 'componentDidMount')
},
componentWillReceiveProps(props) {
console.log('son', 'componentWillReceiveProps')
this.setState({
times: props.times
})
},
shouldComponentUpdate() {
console.log('son', 'shouldComponentUpdate')
return true
},
componentWillUpdate() {
console.log('son', 'componentWillUpdate')
},
componentDidUpdate() {
console.log('son', 'componentDidUpdate')
},
componentWillUmount() {
console.log('son', 'componentWillUmount')
},
render() {
console.log('son', 'render')
return (
<View style={styles.container}>
<Text style={styles.welcome} onPress={this.timesPlus}>
有本事揍我啊
</Text>
<Text style={styles.instructions}>
你居然揍了我 {this.state.times} 次
</Text>
<Text style={styles.instructions} onPress={this.timesReset}>
信不信我亲亲你
</Text>
</View>
)
}
})

var imoocApp = React.createClass({
getDefaultProps() {
console.log('father', 'getDefaultProps')
},
getInitialState() {
console.log('father', 'getInitialState')
return {
hit: false,
times: 2
}
},
timesPlus() {
let times = this.state.times
times += 3
this.setState({ times })
},
timesReset() {
this.setState({ times: 0 })
},
willHit() {
this.setState({
hit: !this.state.hit
})
},
componentWillMount() {
console.log('father', 'componentWillMount')
},
componentDidMount() {
console.log('father', 'componentDidMount')
},
shouldComponentUpdate() {
console.log('father', 'shouldComponentUpdate')
return true
},
componentWillUpdate() {
console.log('father', 'componentWillUpdate')
},
componentDidUpdate() {
console.log('father', 'componentDidUpdate')
},
render() {
console.log('father', 'render')
return (
<View style={styles.container}>
{
this.state.hit
? <Son times={this.state.times} timesReset={this.timesReset}/>
: null
}
<Text style={styles.welcome} onPress={this.timesReset}>
老子说: 心情好就放你一马
</Text>
<Text style={styles.instructions} onPress={this.willHit}>
到底揍不揍
</Text>
<Text style={styles.instructions}>
就揍了你 {this.state.times} 次而已
</Text>
<Text style={styles.instructions} onPress={this.timesPlus}>
不听话再揍你 3 次
</Text>
</View>
)
}
})

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
}
})

AppRegistry.registerComponent('imoocApp', () => imoocApp)

3.3.2 分析

(1) 父组件初始化渲染和加载

4DCEEEE1-4B7D-44BB-8E04-8A5FFEFE3F64

25C37AA9-39B8-4473-98DA-53673A993F2C

(2) 子组件的初始渲染和加载

7581DC8D-FC75-4CE0-9A7F-03A0576598E5

FB3C5ECD-85BE-4DE6-9D1C-1CEEEFE66648

5EE7ACF3-2FDB-491C-AA39-A981B0FADF9E

(3) 子组件 state 变化

88901F07-9679-4B04-ABB9-8367DCF60E4D

62E6F933-DEDF-4B80-8D65-823FA7569057

(4) 父组件 state 变化 > 组件件 props 变化

9FC5DD40-F8B2-4DCA-A225-09BA3F63AAFC

EC2B84EF-FDA1-4C15-8066-760CA1D96BE3

3.4 ES5 和 ES6 两种组件形态

3.4.1 ECMAScript 历史

ECMAScript 规范的历史就是浏览器厂商、规范制订者和开发者的撕逼史。

  • 1997 ECMAScript 1.0
  • 1998 ECMAScript 2.0
  • 1999 ECMAScript 3.0
  • 2007 ECMAScript 4.0 草案,受到抵制
  • 2008 ECMAScript 3.1
  • 2009 ECMASCript 5.0
  • 2015 ECMASCript 6.0 ECMASCript 2015(ES6)
  • 2016 ECMASCript 2016(ES7)

3.4.2 ES5 和 ES6 两种组件实现方式的对比

要点

要点 ES5 方式 ES6 方式
事件绑定需要通过 bind 绑定 this
组件的声明 React.create() class(继承 Component)
state 获取初始值 getInitialState() 在构造器中给 this.state 赋值
props 获取默认值 getDefaultProps() props 作为构造器的参数传入,因此可以通过 ES6 的形式参数默认值来实现。
是否支持mixin 支持 不支持

案例

前面父子组件撕逼大战的例子index.ios.js

ES5版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
var React = require('react')
var ReactNative = require('react-native')
var AppRegistry = ReactNative.AppRegistry
var StyleSheet = ReactNative.StyleSheet
var Text = ReactNative.Text
var View = ReactNative.View

var Son = React.createClass({
getDefaultProps() {
console.log('son', 'getDefaultProps')
},
getInitialState() {
console.log('son', 'getInitialState')
return {
times: this.props.times
}
},
timesPlus() {
var times = this.state.times
times++
this.setState({ times })
},
timesReset() {
this.props.timesReset()
},

componentWillMount() {
console.log('son', 'componentWillMount')
},
componentDidMount() {
console.log('son', 'componentDidMount')
},
componentWillReceiveProps(props) {
console.log('son', 'componentWillReceiveProps')
this.setState({
times: props.times
})
},
shouldComponentUpdate() {
console.log('son', 'shouldComponentUpdate')
return true
},
componentWillUpdate() {
console.log('son', 'componentWillUpdate')
},
componentDidUpdate() {
console.log('son', 'componentDidUpdate')
},
componentWillUmount() {
console.log('son', 'componentWillUmount')
},
render() {
console.log('son', 'render')
return (
<View style={styles.container}>
<Text style={styles.welcome} onPress={this.timesPlus}>
有本事揍我啊
</Text>
<Text style={styles.instructions}>
你居然揍了我 {this.state.times} 次
</Text>
<Text style={styles.instructions} onPress={this.timesReset}>
信不信我亲亲你
</Text>
</View>

)
}
})

var imoocApp = React.createClass({
getDefaultProps() {
console.log('father', 'getDefaultProps')
},
getInitialState() {
console.log('father', 'getInitialState')
return {
hit: false,
times: 2
}
},
timesPlus() {
let times = this.state.times
times += 3
this.setState({ times })
},
timesReset() {
this.setState({ times: 0 })
},
willHit() {
this.setState({
hit: !this.state.hit
})
},
componentWillMount() {
console.log('father', 'componentWillMount')
},
componentDidMount() {
console.log('father', 'componentDidMount')
},
shouldComponentUpdate() {
console.log('father', 'shouldComponentUpdate')
return true
},
componentWillUpdate() {
console.log('father', 'componentWillUpdate')
},
componentDidUpdate() {
console.log('father', 'componentDidUpdate')
},
render() {
console.log('father', 'render')
return (
<View style={styles.container}>
{
this.state.hit
? <Son times={this.state.times} timesReset={this.timesReset}/>
: null
}
<Text style={styles.welcome} onPress={this.timesReset}>
老子说: 心情好就放你一马
</Text>
<Text style={styles.instructions} onPress={this.willHit}>
到底揍不揍
</Text>
<Text style={styles.instructions}>
就揍了你 {this.state.times} 次而已
</Text>
<Text style={styles.instructions} onPress={this.timesPlus}>
不听话再揍你 3 次
</Text>
</View>
)
}
})

var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
}
})

AppRegistry.registerComponent('imoocApp', function(){ return imoocApp })

ES6 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/


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

class Son extends Component{
constructor(props) {
super(props)
this.state = {
times: this.props.times
}
}
timesPlus() {
let times = this.state.times
times++
this.setState({ times })
}
timesReset() {
this.props.timesReset()
}

componentWillMount() {
console.log('son', 'componentWillMount')
}
componentDidMount() {
console.log('son', 'componentDidMount')
}
componentWillReceiveProps(props) {
console.log('son', 'componentWillReceiveProps')
this.setState({
times: props.times
})
}
shouldComponentUpdate() {
console.log('son', 'shouldComponentUpdate')
return true
}
componentWillUpdate() {
console.log('son', 'componentWillUpdate')
}
componentDidUpdate() {
console.log('son', 'componentDidUpdate')
}
componentWillUmount() {
console.log('son', 'componentWillUmount')
}
render() {
console.log('son', 'render')
return (
<View style={styles.container}>
<Text style={styles.welcome} onPress={this.timesPlus.bind(this)}>
有本事揍我啊
</Text>
<Text style={styles.instructions}>
你居然揍了我 {this.state.times} 次
</Text>
<Text style={styles.instructions} onPress={this.timesReset.bind(this)}>
信不信我亲亲你
</Text>
</View>

)
}
}

class imoocApp extends Component {
constructor(props) {
super(props)
this.state = {
hit: false,
times: 2
}
}

timesPlus() {
let times = this.state.times
times += 3
this.setState({ times })
}
timesReset() {
this.setState({ times: 0 })
}
willHit() {
this.setState({
hit: !this.state.hit
})
}
componentWillMount() {
console.log('father', 'componentWillMount')
}
componentDidMount() {
console.log('father', 'componentDidMount')
}
shouldComponentUpdate() {
console.log('father', 'shouldComponentUpdate')
return true
}
componentWillUpdate() {
console.log('father', 'componentWillUpdate')
}
componentDidUpdate() {
console.log('father', 'componentDidUpdate')
}
render() {
console.log('father', 'render')
return (
<View style={styles.container}>
{
this.state.hit
? <Son times={this.state.times} timesReset={this.timesReset.bind(this)}/>
: null
}
<Text style={styles.welcome} onPress={this.timesReset.bind(this)}>
老子说: 心情好就放你一马
</Text>
<Text style={styles.instructions} onPress={this.willHit.bind(this)}>
到底揍不揍
</Text>
<Text style={styles.instructions}>
就揍了你 {this.state.times} 次而已
</Text>
<Text style={styles.instructions} onPress={this.timesPlus.bind(this)}>
不听话再揍你 3 次
</Text>
</View>
)
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
}
})

AppRegistry.registerComponent('imoocApp', () => imoocApp)

4 升级 React Native 的重要补录

4.1 一期答疑总结

好习惯

  • 培养自己选找答案的功能

  • 经常去问答区看看

常见问题

xcode 升级导致的问题

代码粗心

  • 使用排除法锁定出错的代码。

4.2 Run 起来课程源代码

4.2.1 检查环境

1
2
$ node -v
$ react-native -v
环境 讲师
Nodejs v6.9.1 v7.9.0
rnpm 1.9.0 1.9.0
react 未知 16.0.0-alpha.6
react-native-cli 0.1.10 2.0.1
react-native 0.22.2 0.44.0

4.2.2 升级项目

1
2
3
4
$ cd imoocApp

# 依赖的包可能过时了,替换为最新的
$ rm -rf node_modules && npm install

4.2.3 保持环境干净

  • 关闭全局的翻墙
  • 关闭已有的 RN 服务进程(如果有的话)
  • 关闭模拟器

4.2.4 跑起来

1
$ react-native run-ios

4.2.5 错误修复

技巧: 如果报错且没有头绪,可以用 xcode 编译运行项目, 一般报错信息更丰富些。

(1) Semantic Issue

F507C0C1-45D1-4027-BE25-C364B8125CE5

删除-Werror-Wall

(2) ‘RTCBundleURLProvider.h’ file not found

1
2
3
$ cd imoocApp
# 在项目中建立和第三方模块的链接
$ rnpm link

4.3 暴力升级到 0.36 (上)

1
2
3
4
5
6
7
# 升级 nodejs
$ nvm install v7.9.0
$ nvm alias default v7.9.0

# 升级 react-native、react-native-cli、rnpm
$ npm i react-native-cli@2.0.1 -g
$ npm i rnpm@1.9.0 -g

项目小升级

1
2
3
4
5
6
7
8
# 升级项目
$ cd imoocApp
$ rnpm unlink
$ rm -rf node_modules
$ npm install
$ npm install react-native@0.44.0 -S
$ npm install react@16.0.0-alpha.6 -S
$ rnpm link

4.4 暴力升级到 0.36(下)

项目大升级(脱胎换骨)

  • 优点: 能抹平环境差异带来的问题。
  • 缺点: 给代码的带来非常大的变更量,不变易历史版本比对和代码版本跟踪。

(1) 在本地新建一个 RN 项目

1
2
3
$ react-native init newRNProject
$ cd newRNProject
$ react-native run-ios # 测试下通过官方脚手架搭建的项目是否正常

(2) 用新的配置文件覆盖旧的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ mv newRNProject/.babelrc imoocApp/
$ mv newRNProject/.buckconfig imoocApp/
$ mv newRNProject/.flowconfig imoocApp/
$ mv newRNProject/.gitignore imoocApp/
$ mv newRNProject/.watchmanconfig imoocApp/
$ mv newRNProject/package.json imoocApp/
$ mv newRNProject/__test__ imoocApp/
$ mv newRNProject/android imoocApp/
$ mv newRNProject/index.android.js imoocApp/
$ mv newRNProject/ios/imoocApp.xcodeprog imoocApp/ios/
$ mv newRNProject/ios/imoocAppTests imoocApp/ios/
$ mv newRNProject/ios/AppDelegate.h imoocApp/ios/
$ mv newRNProject/ios/AppDelegate.m imoocApp/ios/
$ mv newRNProject/ios/main.m imoocApp/ios/
$ mv newRNProject/ios/ imoocApp/ios/

(3) 删除多余的文件

1
$ rm -rf imoocApp/ios/build

(4) 安装依赖包

1
2
$ npm i react react-native lodash query-string react-native-audio@2.2.0 react-native-button@1.7.1 react-native-image-picker@0.22.12 react-native-progress@3.1.0 react-native-sk-countdown@1.0.1 react-native-swiper@1.5.2 react-native-vector-icons@2.1.0 react-native-video@0.9.0 react-addons-update@15.3.0 sha1 --save
$ rnpm link

(5) imoocApp/ios/imoocApp/Info.plist

1
2
<key>NSAllowsArbitraryLoads</key>
<true/>

4.4 课程进阶拥抱 ES6(上)

  • 模块引入使用 import
  • 组件声明换成 class 方式(记得 export 出去)
  • 事件绑定加上 bind

4.5 课程进阶拥抱 ES6(下)

一些第三方组件通过 import 引入方式需要使用包名

  • react-native-image-picker

node_modules/react-native-sk-countdown/CountDownText.js

1
2
3
4
5
6
7
import React, { Component } from 'react'
import {
StyleSheet,
Text
} from 'react-native'
import update from 'react-addons-update'
import countDown from './countDown'

ActivityIndicatorIOS is deprecated. Use ActivityIndicator instead.
将文件夹 imoocApp/app 下的 ActicityIndicatorIOS 全局替换为 ActicityIndicator。

animated supplied to Modal has been deprecated.Use the animationType prop instead.

1
2
3
<Modal
animationType={'slide'}
...>

imoocApp/app/edit/index.js

1
2
3
4
5
6
7
...
uploadContainer: {
...
height: 210,
...
}
...

轮播去掉容器样式
轮播按钮有些丑
imoocApp/app/creation/index.js

1
2
3
4
5
6
...
<RefreshControl
refreshing={this.state.isRefreshing}
onRefresh={this._onRefresh.bind(this)}
/>
...

5 项目初始准备

5.1 狗狗说 App 功能介绍

狗狗说功能分析

  • 启动画面
  • 轮播图
  • 短信登录
  • 制作页
  • 列表页
  • 账户主页

D49EBA1A-7626-49FF-9346-83D5A5B8D8D1

2E271031-FFC4-40B9-9D0B-DB01613B85E8

8F9195B0-DF0D-43DB-8D1E-7C0B7D336ACC

3501449C-9A19-49EA-B726-D6FAEB9CF951

0F99F927-3486-4087-9003-EDB6D5E67F8E

5.2 TabBarIOS 开启 App 首页

D49EBA1A-7626-49FF-9346-83D5A5B8D8D1

从大往小作,一开始不要纠结样式、功能等琐碎的小细节,先把大框架描出来。放眼整个 APP,先把几个主要的视图界面狑出来 ,然后组合一下,再对着这个大的结构进行更加详细的功能模块的拆解和具体方案的架构。

RN 插件: 图标工具

react-native-vector-icons@4.1.1: 一个流行的图标库管理工具

RN 插件包管理器: rnpm

作用是把模块链接到 iOS 工程中,把必要的资源文件比如图标字体也拷贝到工程中,方便 RN 调用。

Github 地址

614E0AEB-15F3-484F-8BBE-14D57E2A54A8

显然 rnpm 已经被合并到了 react-native core ,用法和 rnpm 类似,以 react-native-vector-icons 为例

1
2
# rnpm link react-native-vector-icons
$ react-native link react-native-vector-icons

RN 升级

1
2
# 本地环境升级,除了升级 RN 核心库外,配置文件也会相应进行升级到最新。
$ react-native upgrade

5.3 TabBarIOS 开启 APP 首页(2)

5.3.1 使用 react-native-vector-icons

9B23DCB1-CE53-45DF-BF53-95F3640FAF02

项目中具体使用的是 Ionicons图标库。

1
import Icon from 'react-native-vector-icons/Ionicons'

注意: 链接后需要重新编译才能生效。

5.3.2 使用 Icon.TabBarItem 取代 TabBarIOS.Item

Icon.TabBarItem是对TabBarIOS.Item的封装,用来生成图标列表项。

5.4 APP 流程结构及开发计划

下面先开发哪个页面?理论上选择哪个都可以,只不过如果先对项目有一个功能的拆解和梳理之后,在选择项目的开发顺序,心里更有底。在前期的开发中,能有一些适当的扩展和接口,给到后面的页面,真个开发顺序会更加自然。
为了让大家印象更加深刻,先来对 APP 的功能进行一下细化,过一下用户的交互流程。

8ED42960-E255-4E4F-B77D-AF81B3DAC6F6

5.5 页面重构

将 Tab 对应的视图分别放在单独文件中。

B1F170F2-BD34-4B34-ACB2-D18C948BCBCF

5.6 飞速创建后台和 Mock 假数据

5.6.1 传统 Mock 假数据

一般开发网页的时候伪造数据,包括

  • 在代码中写死一个 JS 对象或数组存放假数据。
  • 本地用一个 JSON 文件存放假数据,然后请求这个文件就好了。
  • 本地开一个 python 或者 Nodejs 静态服务,简单写一下响应规则。

缺点

  • 请求不够真实,比如都是基于内网环境,没有真实环境可能的网络延迟,因此看不出应用在地网络环境下的表现。
  • 如果本地开发后台,比较辛苦,需求如果频繁变化,本地后台的规则也需要频繁变化,造成开发成本过高。
  • 不能满足定制化的需求,比如随机返回数据的个数,返回不同的图片地址或者返回变化的数据,包括无法验证传参的正确性。

5.6.2 Rap 和 Mockjs

Rap: 处理网络请求和输入数据规则。
Mockjs: 本地解析数据规则来生成更佳个性化的具体数据,避免大量数据传输的体积。

(1) 在 Rap官网 配置好某个请求的Mock规则。

708E7778-BACE-4086-99C5-D2175D23BFEF

(2) 产看请求的 Mock 数据

C4A2C36C-4E7A-43C4-BF47-6902EB43028F

其实也可以在 Mockjs 官网可以试下规则的解析结果,如下

FDD9C85D-6D12-4D46-AA2D-4140F3E22ADB

6 开发视频配音界面

6.1 视频列表挖坑开发(1)

9E15FAF0-4397-418C-BE0E-D177ABB003BA

6.2 视频列表挖坑开发(2)

完成视频列表的基本雏形,这个雏形远远没有完成,留下许多问题需要解决:

  1. resizeMode 是啥?(图片的显示模式)
  2. fontSize 的单位是什么?
  3. 如果通过请求获取线上数据?

C88BC6E4-8DDC-4492-9101-0FA459384A9F

6.3 RN 的异步请求与封装

6.3.1 RN 的 3 种异步请求方式

B6D9792B-84CC-4A93-8B3E-AA17DA78ABDB

在 RN 中获取网络数据常见的有上面3种方式,分别说明下。

(1) Fetch
说明: 标准委员会制定的网络 API 的一个标准,在 RN 中直接可用,非常简洁。

参数 说明
1 请求地址
2(可选) 配置项,比如请求方法、请求头设置

(2) WebSocket
通过 TCP 连接建立的一个双工模式的数据传输通道,非常强大。

(3) XMLHttpRequest
就是我们常说的异步请求接口,是对 iOS 网络层 API 的一个上层封装,类似浏览器提供的 XMLHttpRequest。两者用法差不多,最大的区别是,在浏览器呦同源策略,请求会受到安全限制,不能发起跨域的请求,在 RN 中就没有这个限制。因此可以通过 XHR 实现无后台的 APP,请求第三方开发的接口。如果是在浏览器中的话,往往要借助 JSONP,或者服务端设置,或者通过服务端发送异步请求,在来转发这个数据,最后给到浏览器。

6.3.2 列表页使用 fetch 请求数据

异步请求的时机: 一般会在组件挂载之后(componentDidMount)发起异步请求来获取数据。

(1) 使用 mockjs 将来自 Rap 的规则转换为数据。

1
$ npm i mockjs -S

注意: 由于 /node_modules/mockjs/dist/mock.jsdataImage 方法使用了 canvas API ,在 RN 中会出问题,需要安装 node-canvas,而后者可能带来更多问题,因此干脆就把这个方法删掉。

(2) 在程序中使用 mockjs 将通过 fetch 的列表数据规则生成数据。

  • 通过封装 fetch 抽象出 get 和 post 工具方法;
  • 将请求相关的请求 URL 和请求头等放在配置文件中;
  • 使用上面封装的 get 方法完成对列表数据的请求。

6.4-6.5 列表页上滑预加载和下拉刷新效果

dig_talk_list

6.6 iOS 屏幕尺寸及分辨率知识点解析

6.6.1 基础知识

显示单元

8F5A96B4-7D69-407D-836F-448F87C2040F

英寸(inch)和厘米(cm)

E6892C1C-FDCF-4C0B-A10B-F0F83F4186C6

6.6.2 iphone 6

屏幕尺寸

CC8BA961-F966-4B0C-BBCC-59E454E9794F

逻辑分辨率 & 设备分辨率 & 设备像素比 & PPI

3FB67D4A-A0ED-49AD-9873-209D0C05FA0B

6.6.3 iphone 6 plus

3297081A-DCD1-4CB1-85B2-AE09391D60D0

down sampled

实际上,对于 Retina 屏,屏幕上实际的采样会低于真实的设备分辨率,比如 iphone 6 plus 的采样率为 87%,所以 1242 x 2208 的分辨率实际能渲染的像素点为 1080 x 1920。密度有所下降,但肉眼无法分辨。这种把高分辨率以倍数的方式缩小到一个特定分辨率的做法,也就是我们通常所说的视网膜技术。

6.6.4 iPhone 尺寸分布

市面上有很多中高清屏,像素密度不同,像素比也不同,有1倍的、1.5倍的、4倍的。相关的概念有很多,这里不一一涉及。关于苹果设备的分辨率,下面提供一个像素表。基本包括所有我们需要关心的参数。

CA41CECC-BD8C-44B4-A089-0C1FDD807CF3

对开发者带来了一些苦恼,好在用户对苹果设备的更新换代速度比较快,所以放在 2016 的当下,我们可以以 iPhone 6 作为基本设备,可以不考虑 iPhone 4 ,向上兼容到 iPhone6 P,向下兼容到 iPhone 5。

宽度自适应
在 RN 中,没有百分比这种单位,可以获取屏幕的宽度值,乘一定的比例,从而得到能能够适应不同宽度屏幕的值,动态获取长度值。

图片自适应
按照 iPhone 6 P 的分辨率设计图片,然后分别压缩到 750 和 640这两种规格,然后在 RN 中分别动态判断一下,分别引入不同规格的图片。

设计和工程方面的更多话题

  • iPhone 4 到 iPhone 5 为什么宽度不变,高度增加了 88 个点?
  • iPhone 5 及之后,屏幕的宽高比为什么维持在 0.56 附近,依据是什么?

6.7 列表页点赞功能

(1) 在 Rap 增加点赞的接口
(2) 在 Item 组件中实现点赞功能

6.8 RN 导航器 Navigator 的用法

利用 Navigator 实现从列表页跳转到详情页的功能。

navigate_from_list_to_detail

6.9 详情页视频播放控制

1
2
$ npm i -S react-native-video
$ react-native link react-native-video

1B3A7CAD-DBEC-4669-95AD-DDC92206FB03

6.10 详情页视频播放控制 loading-进度条 -重播功能

(1) 视频载入前显示 loading 图标
(2) 视频播放中显示进度条
(3) 视频播放到最后时,显示重播按钮

6.11 详情页视频播放控制–暂停、播放控制

(1) 视频播放时,点击播放器会暂停播放,并显示继续播放按钮;
(2) 点击继续播放,会隐藏暂停播放的按钮并让视频继续播放。

6.12 详情页视频播放控制–容错处理、返回导航

1EBF2D97-BC73-44B1-8603-6AFD12601ED7

6.13 详情页视频信息补全

C302B147-4C39-4B10-B7B1-470758992807

6.14 获取视频评论列表(1)

详情页
(1) 在 componentDidMount 时异步加载数据评论;
(2) 使用 ListView 外实现评论列表。

9EE0C411-9DAE-422B-A349-9309C8CBE507

6.15 获取视频评论列表(2)

详情页
(1) 用 ListView 的 renderHeader 方案取代 ScrollView 嵌套 ListView 的方案;
(2) 实现评论列表的分页加载。

6.16 RN 里面提交评论表单(1)

(1) 评论列表顶部添加评论框;

EA055CC9-F6FF-4047-A891-A41A76302A0A

(2) 获得焦点时,用 RN 的 Modal 组件实现在浮层中显示评论编辑;

6CF1C956-66FF-4347-AE7F-1F99D564D36D

6.17 RN 里面提交评论表单

(1) 使用 react-native-button 组件创建评论提交按钮

1
2
# 注意:  不需要 react-native link ,可以直接使用!
$ npm i -S react-native-button

D6A29161-9413-438F-81D7-5E6FDA5AEC50

(2) 添加提交评论的接口,并在提交完成后关闭评论浮层。
遇到一个诡异的问题,如果先设置 state 中的 content 为空字符串,再关闭浮层,就会崩溃;反过来顺序就没事儿。

D58D76CE-3E13-4090-95F1-ABC1799EDD95

7 RN 知识进阶串讲

7.1 React 与 MVC

BB444226-6690-4063-AA7C-D31047EAB84F

React 革新了传统的 MVC 架构模式。

MVC 架构模式

84E70B6A-82A4-4E05-A8E2-E9E6E16B2C3C

834EA6C8-DEF9-41D0-BAC7-8EB7DF02A54B

React 架构模式

C901DF12-E053-400D-B314-7B500473706B

React 并不是清清白白的 View,因为每一个组件的内部都可以自行控制数据的变化,也就是 C 的作用。

B3B9A8EB-0651-4C97-A7A5-4647B4EAAD70

也就是说,React 组件充当了 View 和 Controller 两种角色。而且组件之间是独立的,组件内部是耦合的,因此组件可以看成 VC 而不是一个简单的 V 。

2954407D-CC97-40AF-AB7F-58EF436BA678

React 组件数据传递

520EFCE0-6500-4B8A-8487-672A09457A84

7.2 RN 的 30+ 组件

7.2.1 组件

序号 v0.45.0-rc.0 版本控件 说明
1 ActivityIndicator
2 Button
3 DatePickerIOS
4 FlatList
5 Image
6 KeyboardAvoidingView
7 Layout
8 ListView
9 Modal
10 FlatList
11 NavigatorIOS
12 Picker
13 PickerIOS
14 ProgressViewIOS
15 RegreshControl
16 ScrollView
17 SectionList
18 SegmentedControlIOS
19 Slider
20 StatusBar
21 SwipeableListView
22 Switch
23 TabBarIOS
24 Text
25 TextInput
26 Touchable*
27 TrabsparentHitTestExample
28 View
29 WebView

7.2.2 运行官方 RN 示例

react-native GitHub,当下的最新版本为 v0.45.0-rc.0。官方提供了 RN 使用的 demos ,路径为react-native/RNTester

1
2
3
$ git clone https://github.com/facebook/react-native.git
$ cd react-native && npm install
$ cd RNTester && open .

2B9F739E-2C27-4FEB-A5DD-6EEEB6D818BA

93C08351-31F6-40C4-B208-A87255889259

B6539CF5-B6AA-4F64-B64C-D562B1401E9C

F948F4E2-D122-4224-A4BC-01D55FB84763

CEBB97A5-3C64-4EAF-A1AB-E9A96DC31F79

D758664D-50B0-4D05-9FAC-F0ED2AF61EBA

示例程序中有每种组件的各种用法演示,比图 View

2A2EE967-F031-4983-AA3A-4BA8129F529E

752FD120-9182-456E-9803-A7651ADF4DB7

7.3 Flexbox 弹性布局的魔法属性

RN 使用的是 flexbox (弹性盒子伸缩布局,或者叫伸缩容器布局)。当页面适配不同屏幕不同宽度的时候,让所有元素能够以一种合适的方式来呈现。有两点需要注意:

  1. RN 的 flexbox 跟 css3 的 flexbox 属性名称写法不同, RN 里面全部是驼峰标示;
  2. RN 的 flexbox 支持的属性很有限,它是 Flexbox 的一个子集。

要把子项目放进去到父容器,一共分几步?
3 步
(1) 父容器定义对齐方式
(2) 子项目扔到父容器
(3) 子项目定义个性对齐样式

flexDirection

说明
row 子项目一律从左往右,一个挨一个地排列到右侧
column(默认) 子项目竖着排列,从上往下一个一个挨着排

flexWrap

说明
wrap 主轴上排不下,就折行继续排
nowrap(默认) 主轴上排不下也不折行,溢出父容器

justifyContent

沿主轴方向的排列方式。

说明
flex-start(默认) 沿主轴居前
flex-end 沿主轴居后
space-between 沿主轴向两边对齐
space-around 沿主轴平分空间
center 沿主轴居中排列

alignItems

沿侧轴方向的空间利用(排列)方式。在父元素上定义,作用在子元素。

说明
center 沿侧轴垂直
flex-start(默认) 沿侧轴居前
flex-end 沿侧轴居后
stretch 沿侧轴方向拉伸子元素,直到填充完父容器边界

7.4 Flex 弹性布局的魔法属性

alignSelf

类似 alignItems,不同的地方在于它声明在子元素上也作用在子元素。覆盖在父元素上设置的 alignItems 对子元素的作用。

7DEE610A-EEC2-4962-A55A-DC69C9A4E8F3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/


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

export default class Account extends Component {
render() {
return (
<View style={styles.container}>
<Text style={[styles.item, styles.item1]}>1111111111111</Text>
<View style={[styles.item, styles.item2]}>
<Text>22222222222222</Text>
</View>
<View style={[styles.item, styles.item3]}>
<Text>33333333333333</Text>
</View>
</View>

)
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
flexWrap: 'nowrap',
paddingTop: 30,
paddingBottom: 70,
justifyContent: 'space-between',
alignItems: 'flex-end',
backgroundColor: '#ff6600'
},
item1: {
backgroundColor: '#ccc',
alignSelf: 'flex-start'
},
item2: {
backgroundColor: '#999',
alignSelf: 'center'
},
item3: {
backgroundColor: '#666',
alignSelf: 'flex-end'
}
})

flex

控制元素沿主轴方向所占空间的多少。

7.5 如何甄选第三方组件

搜索

比较

  • 在 GitHub 比较

比较依据:

(1) star 数量: 值越大,反应组件越受欢迎
(2) Fork 数量:值越大, 反映了参与这个组件开发的人数越多,同时也说明该组件不能满足用户的情况也多。
(3) Pull requests: 值越大,尤其是 closed 的数量越多,反应组件越活跃,完善的可能性越大。
(4) Issues: 值越大,反应问题越多,其中 closed 的越多代表作者越积极地在完善组件。
(5) 最近的更新时间
(6) 兼容性

  • stackoverflow 相关问题数量
  • 文档是否规范,最好有效果示意图
  • 示例代码是否丰富

7.6 RN 的 AsyncStorage 异步存储

说明: RN 官方为我们提供了 AsyncStorage 这个异步持久化方案,它采用键值对存储系统,跟浏览器的 localStorage 本地存储有相似之处。
注意: 官方是建议我们对 AsyncStorage 进行抽象封装之后再使用,而不是直接使用它,因为 AsyncStorage 是全局操作的。

这节先演示下原生的 AsyncStorage 是如何存储和取值的,等到实际项目的时候再来使用 AsyncStorage 的封装模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
AsyncStorage
.getItem('user')
// 如果能拿到 user 这个数据,则将本地的这个数据同步到 state,然后再修改后存储到本地
.then(data => {
if (data) {
console.log(data)
data = JSON.parse(data)
this.setState({
user: data
}, () => {
data.times++
const userData = JSON.stringify(data)

AsyncStorage
.setItem('user', userData)
.then(() => {
console.log('save ok')
})
.catch(err => {
console.log(err)
console.log('save failes')
})
})
}
// 如果 user 这个这个数据不存在,就从 state 拿出来,编辑后存到本地
else {
const user = this.state.user
user.times++
const userData = JSON.stringify(user)
AsyncStorage
.setItem('user', userData)
.catch(err => {
console.log(err)
console.log('save fails')
})
.then(() => {
console.log('save ok')
})
}
})

7.7 如何在手机上安装演示 RN 原型项目

(1) 数据线连接 Mac
(2) 清理 Mac 上的 RN 环境
(3) localhost 改为 Wi-Fi 环境下的 IP 地址

以 imoocApp 这个项目为例:
imoocApp/ios/imoocApp/AppDelegate.m

06A34D33-B556-43A7-B417-EBA2B557B443

(4) Xcode 选择 iphone 真机来启动

8 App 內注册登录

8.1 伪造 Rap 注册登录接口

92F80514-7EE1-439A-BA4C-77F915B1BF8D

8.2 实现注册登录页面1(输入验证码)

(1) 伪造发送验证码接口;
(2) 完成登录界面基本 UI 部分;

8.3 实现注册登录页面2(倒计时)

(1) 伪造登录接口;

(2) 使用 react-native-sk-countdown实现倒计时重新获取验证码功能;
由于第三方的react-native-sk-countdown似乎已经停止维护,对 RN 0.44 不兼容,因此我Fork 了一个新项目,修复了部分兼容问题。

8.4 本地管理应用登录状态

登录流程

C9BDDE9C-DAB5-4939-B7F5-31E8B337C5BB

9 用户账户页面

9.1-9.3 头像展示与相册选取

  • 使用 react-native-image-native 从本地选择图片作为头像

选择图片崩溃的问题
要在 iOS 10 中正常使用 react-native-image-native,需要设置 info.plist

9.4-9.5 XHR 异步上传图片到 cloudinary 图床

cloudinary.com

(1) 在 cloudinary.com 配置好空间信息

7EE44700-7980-4222-8195-FF44685DE6B2

(2) 在 RAP 创建假的签名验证接口,然后在项目中中模拟签名验证过程
(3) 项目中实现上传逻辑

  • 读取本地相册
  • 构建上传的表单
  • 发起异步请求
  • 针对图床个性化的签名

9.6-9.7 饼状图显示图片上传进度

1 安装 react-native-progress

安装 react-native-progress 组件,并通过 xcode 手动添加到工程中(ART 不支持通过 react-native link 引入)

F6CF9482-1C97-4EB8-B747-587D15CBEF39

(1) 在 xcode 中手动添加 ART 到 iOS 工程中

B11C2C42-8E91-4B50-8190-79806B7CC84C

A07E9217-B50D-4030-953B-A5AF0D1585A4
(2) 配置引入的 ART 库
CFAEDF54-24C6-493A-9EF3-505795880056

5DEF9FE5-504B-4222-86F4-A21A02DF8E16

(3) 重启调试环境,包括 run-ios、模拟器

2 完成上传进度

3 完成图像上传成功后的后续处理

  • 更新服务器上用户的头像数据
  • 将更新的数据留一份在本地(AsyncStorage),从而每次打开都是最新上传的那个头像

9.8-9.9 编辑和保存用户资料

(1) 丰富“用户资料更新”接口的字段
(2) 实现在弹出 Modal 编辑用户资料界面功能
(3) 实现保存资料和退出登录

10 用 Koa 开发本地 API 后台

10.1 本地安装 Mongodb 数据库

注意: 因为之前在虚拟机(Arch Linux)上装过一个 MongoDB 数据库(参见《NodeJS线上服务器部署与发布-2.2.2》),所以后面实际的练习是在用的虚拟机上的数据库。如果没有特别的原因,并不推荐在 OS X 上装一个数据库,因为污染开发环境。

安装和使用
(1) Homebrew

(2) Install MongoDB Community Edition on OS X

如果下载有问题,可以这样做

1
2
3
$ cd /Users/<你的用户名>/Library/Caches/Homebrew
$ brew prune # 清理下
$ brew install mongodb

(3) 启动

1
$ sudo mongod

(4) 登录数据库

1
$ sudo mongo

扩展: 如果需要卸载 OS X 上的 MongoDB

1
$ brew unlink mongodb && brew uninstall mongodb

10.2 搭建 Koa 初始项目架构

注意: 课程中使用的是 koa1 ,我实践中使用的是 koa2,两者 api 还有有不少差异的,主要是 koa2 全面使用了 async/await 这种 ES2017 语法。

目录和 package.json

1
2
3
4
$ mkdir dogtalk-server
$ cd dogtalk-server
$ npm init
$ npm i -S koa koa-logger koa-session koa-bodyparser koa-router mongoose sha1 lodash uuid xss bluebird speakeasy

依赖的第三方库

  • koa
  • koa-logger: 开发环境下的日志中间件,作用于请求的最先和最后环节,建议放到应用的最开始部分。
  • koa-session: 基于 cookie 的会话中间件,保持用户会话状态。可以在 seesion 中存储一些会话中的数据。
  • koa-bodyparser: 解析表单数据等等来生成对象结构数据的中间件
  • koa-router: 路由中间件,为不同的 url 地址分配不同的规则。
  • mongoose: Mongodb 数据库的对象建模工具。
  • sha1: 哈希算法库,加密一些数据用。
  • lodash: js 编程的一把瑞士军刀,提供许多好用的工具函数。
  • uuid: 用来生成唯一的不会重复的 ID。
  • xss: 一个对用户数据的内容进行过滤,避免 XSS 恶意攻击的模块。
  • bluebird: 对于 promise 规范实现封装后的 promise 库,有更优雅的 API 和更好的性能。
  • speakeasy: 基于一系列安全算法,能生成随机数字的工具库,比如短信验证码。

10.3 Koa 中配置使用 Mondodb

10.3.1 增加需要的数据库用户

1
2
3
4
5
6
7
# 创建超级用户
> use admin
> db.createUser({user: 'imooc_cases_owner', pwd: 'Safe1*24$', roles: [{role: 'userAdminAnyDatabase', db: 'admin'}]})
> db.auth('imooc_cases_owner', 'Safe1*24$')
# 创建 dogtalk 数据库用户
> use dogtalk
> db.createUser({user: 'dogtalk_runner', pwd: 'F**k9001$', roles: [{role: 'readWrite', db: 'dogtalk'}]})

10.3.2 mongoose model

技巧: 由于 Model 可能会很多,因此推荐动态递归遍历加载需要的数据库对应的 Model。

10.5-10.6 用螺丝帽在 Koa 中发送短信

螺丝帽可以为开发者提供一系列服务,包括

  • 短信服务
  • 语音验证
  • 邮件服务
  • 人机验证

螺丝帽短信服务

使用螺丝帽配置短信服务:
(1) 注册账号

(2) 在管理台中添加签名

(3) 在管理台中添加短信模版

6C92A268-6A76-487C-A70F-D221F743E5B8

(4) 查看接口文档的示例

实现短信发送

(1) 在官方示例代码的基础上进行修改

(2) 实现发送短信验证码-校验短信验证码逻辑

10.7 用 DHC 插件(Restlet Client)快速测试本地 API 服务

说明: DHC 看样子改名为 Restlet Client 了。
(1) 根据从 RAP 到处的接口文档实现后台接口,然后通过 Restlet Client 测试接口能否正常工作。以 用户资料更新接口为例:

60254477-FCA7-4E96-945C-FAB63847C73C

(2) 通过定义中间件实现对请求的统一校验
(3) 注意看看是否都进行了xss 处理

知识点: 用户提交的需要持久化到数据库或用于查询数据库的字段都需要经过 trim 和 xss 处理。

10.8 服务器端实现图床签名接口

(1) 原先的签名信息是由客户端生成的,这种方式不够安全,现在改为在服务器端生成。
(2) 发现原来 koa-router 中间件写法有错误,修正了下。
(3) 至此,打通了以下接口

  • 注册并获取验证码(/api/u/signup)
  • 验证用户提交的验证码是否正确(/api/u/verify)
  • 更具上传的头像相关信息,从服务器获取签名信息(/api/signature)
  • 上传用户头像到 cloudinary.com,然后更新用户头像信息(/api/u/update)

10.9-10.10 用七牛上传图片资源

(1) 在七牛的对象存储中新建一个空间,拿到测试域名

FB49560C-6477-40C9-81FF-91EF7FE3C44E

(2) 在七牛个人中心->秘钥管理拿到其 AccessKey 和 SecretKey, 配置到后端代码中。

(3) 安装七牛官方的 nodejs SDK

1
$ npm i qiniu -S

(4) 重构服务端签名接口代码,增加对七牛对象存储服务的签名支持。
(5) 重构客户端上传图片的代码,将图片上传到七牛。参考七牛提供的 upload 接口文档

11 开发视频配音页面

FB63992D-F55C-496E-A9EE-D158F2654E19

3D9347CA-8904-4E17-8972-F47DD2278486

11.1-11.2 视频选择器与视频预览

华龙先画骨,盖房子先画图😊。

  • [ ] 遇到了通过 Image 加载的应用中的图片没显示出来的问题(也没有报错)
1
首先这个问题,不是必现的,用 xcode 启动项目,图片显示出来了。猜测是通过 react-native run-ios 启动项目有一定问题。

11.3-11.4 用七牛上传视频资源

(1) 重新规划七牛对象存储,创建三个新的存储空间 dogtalkavatardogtalkvideodogtalkaudio

(2) 视频上传到七牛的 dogtalkvideo 存储空间中,返回的接口类如下

1
2
3
4
5
{
hash: "FqzqKrAA25Sj4apuKmmryPdnU4RA"
key: "13953a64-2583-480a-9350-ca5490afa346.mp4"
persistentId: "z1.593edb8f8a3c0c3794e3b6ea"
}
字段 用途 测试
key 可以通过该字段访问到存储在七牛上的文件 http://org5nla9w.bkt.clouddn.com/13953a64-2583-480a-9350-ca5490afa346.mp4
persistentId 查阅数据处理-持久化处理状态查询可以知道,可以通过这个字段查询到文件的元信息 http://api.qiniu.com/status/get/prefop?id=z1.593edb8f8a3c0c3794e3b6ea

(3) 查询元信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{ 
"code": 0, // 请求是否成功
"desc": "The fop was completed successfully",
"id": "z1.593edb8f8a3c0c3794e3b6ea", // 持久化 id
"inputBucket": "dogtalkvideo",
"inputKey": "13953a64-2583-480a-9350-ca5490afa346.mp4", // 上传到存储空间的原始文件名
// 对文件进行操作后的结果(可以连续进行多个操作,返回每个操作的结果组成的数组)
"items": [
{
"cmd": "avthumb/mp4/an/1", // 操作对应的规则
"code": 0,
"desc": "The fop was completed successfully",
"hash": "FgbIXe0cIKVEoeIaRRGvn6y-sKjT",
"key": "tif2A2xwqtgaelwOgLOIfPiOteo=/FqzqKrAA25Sj4apuKmmryPdnU4RA", // 操作后生成的新资源的名字
"returnOld": 0
}
],
"pipeline": "0.default",
"reqid": "exkAAL-Nob3VcscU"
}

(4) 利用上一步得到的元信息中的操作后新资源的 key,得到处理后资源的链接
http://org5nla9w.bkt.clouddn.com/tif2A2xwqtgaelwOgLOIfPiOteo=/FqzqKrAA25Sj4apuKmmryPdnU4RA

11.5-11.6 服务端存储视频信息

11.7 控制录音倒计时

11.8-11.9 实现视频录音功能

11.10-11.11 上传音频到云空间

11.12 服务器端存储音频数据

11.13-11.14 合并音频视频并同步封面

11.15-11.16 发布整个视频音频创意

11.17 服务器端保存视频创意

11.18 视频列表页对接后台数据

11.19 完善评论和点赞后台