上半年带领公司移动端团队开始使用ReactNative,这是当时我在公司内部分享ReactNative开发的文档。欢迎大家多提意见。
基础:
js语法
基础语法不在此处介绍。
react-native框架
环境搭建
参考地址 https://reactnative.cn/docs/getting-started/
踩坑点:
React-native要与react版本对应。
0.45 及以上版本需要下载 boost 等几个第三方库编译。
安装第三库时候,注意第三库是否有原生代码。第三方库有原生代码需要react-native link [库名],到原生项目中。
jsx语法
JSX就是Javascript和XML结合的一种格式。React发明了JSX,利用HTML语法来创建虚拟DOM。当遇到<,JSX就当HTML解析,遇到{就当JavaScript解析。
如下(JS写法):
var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);
等价于(JSX写法)
var root =(
<ul className="my-list">
<li>First Text Content</li>
<li>Second Text Content</li>
</ul>
);
后者将XML语法直接加入JS中,通过代码而非模板来高效的定义界面。之后JSX通过翻译器转换为纯JS再由浏览器执行。在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript,JSX的语法不会带来任何性能影响。另外,由于JSX只是一种语法,因此JavaScript的关键字class, for等也不能出现在XML中,而要如例子中所示,使用className, htmlFor代替,这和原生DOM在JavaScript中的创建也是一致的。JSX只是创建虚拟DOM的一种语法格式而已,除了用JSX,我们也可以用JS代码来创建虚拟DOM.
JSX语法简单学习react中会逐渐掌握。
props
大多数组件在创建时就可以使用各种参数来进行定制。用于定制的这些参数就称为props
(属性)。
以常见的基础组件Image
为例,在创建一个图片时,可以传入一个名为source
的 prop 来指定要显示的图片的地址,以及使用名为style
的 prop 来控制其尺寸。
import React, { Component } from 'react';
import { Image } from 'react-native';
export default class Bananas extends Component {
render() {
let pic = {
uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
};
return (
<Image source={pic} style={{width: 193, height: 110}} />
);
}
}
请注意{pic}
外围有一层括号,我们需要用括号来把pic
这个变量嵌入到 JSX 语句中。括号的意思是括号内部为一个 js 变量或表达式,需要执行后取值。因此我们可以把任意合法的 JavaScript 表达式通过括号嵌入到 JSX 语句中。
自定义的组件也可以使用props
。通过在不同的场景使用不同的属性定制,可以尽量提高自定义组件的复用范畴。只需在render
函数中引用this.props
,然后按需处理即可。下面是一个例子:
import React, { Component } from 'react';
import { Text, View } from 'react-native';
class Greeting extends Component {
render() {
return (
<View style={{alignItems: 'center', marginTop: 50}}>
<Text>Hello {this.props.name}!</Text>
</View>
);
}
}
export default class LotsOfGreetings extends Component {
render() {
return (
<View style={{alignItems: 'center'}}>
<Greeting name='Rexxar' />
<Greeting name='Jaina' />
<Greeting name='Valeera' />
</View>
);
}
}
注意props只能在组件外部更改,不要在组件内部改变自己的props。
state
我们使用两种数据来控制一个组件:props
和state
。props
是在父组件中指定,而且一经指定,在被指定的组件的生命周期中则不再改变。 对于需要改变的数据,我们需要使用state
。
一般来说,你需要在 constructor 中初始化state
(译注:这是 ES6 的写法,早期的很多 ES5 的例子使用的是 getInitialState 方法来初始化 state,这一做法会逐渐被淘汰),然后在需要修改时调用setState
方法。
你也可以使用一些“状态容器”比如Redux来统一管理数据流。
每次调用setState
时,BlinkApp 都会重新执行 render 方法重新渲染。这里我们使用定时器来不停调用setState
,于是组件就会随着时间变化不停地重新渲染。
State 的工作原理和 React.js 完全一致,所以对于处理 state 的一些更深入的细节,你可以参阅React.Component API。
初学者应该牢记的要点:
一切界面变化都是
状态state变化
-
state
的修改必须通过
setState()
方法
- this.state.likes = 100; // 这样的
直接赋值修改无效!
- setState 是一个 merge 合并操作,只修改指定属性,不影响其他属性
- setState 是
异步
操作,修改不会马上生效
- this.state.likes = 100; // 这样的
样式
React-native使用 JavaScript 来写样式。所有的核心组件都接受名为style
的属性。这些样式名基本上是遵循了 web 上的 CSS 的命名,只是按照 JS 的语法要求使用了驼峰命名法,例如将background-color
改为backgroundColor
。
style
属性可以是一个普通的 JavaScript 对象。这是最简单的用法,因而在示例代码中很常见。你还可以传入一个数组——在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承。
实际开发中组件的样式会越来越复杂,我们建议使用StyleSheet.create
来集中定义组件的样式。比如像下面这样:
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View } from 'react-native';
export default class LotsOfStyles extends Component {
render() {
return (
<View>
<Text style={styles.red}>just red</Text>
<Text style={styles.bigBlue}>just bigBlue</Text>
<Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
<Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
</View>
);
}
}
const styles = StyleSheet.create({
bigBlue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
});
flexbox布局
React Native 中的 Flexbox 的工作原理和 web 上的 CSS 基本一致,当然也存在少许差异。首先是默认值不同:flexDirection的默认值是column而不是row,而flex也只能指定一个数字值。
具体可参考flex使用:https://www.runoob.com/w3cnote/flex-grammar.html
长列表
React Native 提供了几个适用于展示长列表数据的组件,一般而言我们会选用FlatList或是SectionList。
FlatList
组件用于显示一个垂直的滚动列表,其中的元素之间结构近似而仅数据不同。
FlatList
更适于长列表数据,且元素个数可以增删。和ScrollView
不同的是,FlatList
并不立即渲染所有元素,而是优先渲染屏幕上可见的元素。
FlatList
组件必须的两个属性是data
和renderItem
。data
是列表的数据源,而renderItem
则从数据源中逐个解析数据,然后返回一个设定好格式的组件来渲染。
下面的例子创建了一个简单的FlatList
,并预设了一些模拟数据。首先是初始化FlatList
所需的data
,其中的每一项(行)数据之后都在renderItem
中被渲染成了Text
组件,最后构成整个FlatList
。
import React, { Component } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
export default class FlatListBasics extends Component {
render() {
return (
<View style={styles.container}>
<FlatList
data={[
{key: 'Devin'},
{key: 'Jackson'},
{key: 'James'},
{key: 'Joel'},
{key: 'John'},
{key: 'Jillian'},
{key: 'Jimmy'},
{key: 'Julie'},
]}
renderItem={({item}) => <Text style={styles.item}>{item.key}</Text>}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 22
},
item: {
padding: 10,
fontSize: 18,
height: 44,
},
})
redux使用
redux理解
(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。
三大核心:
Action :
把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
Reducer:
Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
Reducer 就是一个普通的函数 ,当被Redux调用的时候会给Reducer传递两个参数:State 和 Action
它会根据 Action 的type属性来对旧的 State 进行操作,返回新的State。
combineReducers专门来管理这些子Reducer
reducer 里不要使用 Object.assign(state, newData),应该使用 Object.assign({}, state, newData)。这样才不会覆盖旧的 state。也可以使用 ES7 对象的 spread 操作 特性中的 return { ...state, ...newData }。
Store:
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Redux 提供createStore这个函数,用来生成 Store。
再次强调一下: Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
根据已有的 reducer 来创建 store 是非常容易的。在前一个章节中,我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传递给createStore函数。
// 创建store
const store = createStore(reducer)
store.getState(): 获取应用当前State。获取当前所有信息
store.subscribe():添加一个变化监听器。
store.dispatch():分发 action。修改State。
store.replaceReducer():替换 store 当前用来处理 state 的 reducer。
三大原则:
单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于一个 唯一的store 中。
State 只读:惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
使用纯函数来执行修改:为了描述 action 如何改变 state tree ,你需要编写 reducers。
React-redux使用:
connect()
React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。
import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);
上面代码中,TodoList是 UI 组件,VisibleTodoList就是由 React-Redux 通过connect方法自动生成的容器组件。
但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。
- 输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数
- 输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。
因此,connect方法的完整 API 如下。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
上面代码中,connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
mapStateToProps()
mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。
作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数,后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。
下面就是getVisibleTodos的一个例子,用来算出todos。
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。
// 容器组件的代码
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。
mapDispatchToProps()
mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
<Provider>组件
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。
一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。
React-Redux 提供Provider组件,可以让容器组件拿到state。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。
redux示例-计数器:
我们来看一个实例。下面是一个计数器组件,它是一个纯的 UI 组件。
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
上面代码中,这个 UI 组件有两个参数:value和onIncreaseClick。前者需要从state计算得到,后者需要向外发出 Action。
接着,定义value到state的映射,以及onIncreaseClick到dispatch的映射。
function mapStateToProps(state) {
return {
value: state.count
}
}
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
// Action Creator
const increaseAction = { type: 'increase' }
然后,使用connect方法生成容器组件。
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
然后,定义这个组件的 Reducer。
// Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}
最后,生成store对象,并使用Provider在根组件外面包一层。
import { loadState, saveState } from './localStorage';
const persistedState = loadState();
const store = createStore(
todoApp,
persistedState
);
store.subscribe(throttle(() => {
saveState({
todos: store.getState().todos,
})
}, 1000))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
项目实战扩展知识:
react-navigation:
控制页面跳转与导航
文档地址:https://reactnavigation.org/docs/zh-Hans/navigation-prop.html
createStackNavigator
为你的应用程序提供一种在每个新屏幕放置在堆栈顶部的屏幕之间转换的方法。
默认情况下,stack navigator 被配置为具有熟悉的iOS和Android外观 & 感觉:新屏幕从iOS右侧滑入,从Android底部淡入。 在iOS上,stack navigator 也可以配置为屏幕从底部滑入的模式样式。
api定义:
createStackNavigator(RouteConfigs, StackNavigatorConfig);
路由配置对象是从路由名称到路由配置的映射,它告诉导航器该路由呈现的内容。
createStackNavigator({
//配置页面
Profile: {
// `screen` 对应着页面地址
screen: ProfileScreen,
// `navigationOptions` 可以对导航栏进行详细配置,具体可参考文档。
navigationOptions: ({ navigation }) => ({
title: `${navigation.state.params.name}'s Profile'`,
}),
},
...MyOtherRoutes,
});
createBottomTabNavigator
页面底部的标签栏,可让您在不同路由之间进行切换。 路由被懒加载 - 它们的屏幕组件只有在第一次获取焦点时才会被加载。
实现并列几个页面。
createBottomTabNavigator(RouteConfigs, BottomTabNavigatorConfig);
createSwitchNavigator
SwitchNavigator 的用途是一次只显示一个页面。 默认情况下,它不处理返回操作,并在你切换时将路由重置为默认状态。
应用场景:从登录切换到主页面,或者广告页切换到主页面。
createSwitchNavigator(RouteConfigs, SwitchNavigatorConfig);
Navigation prop
应用中的每个页面组件都会自动提供 navigation
prop, 该 prop 包含便捷的方法用于触发导航操作, 如下所示:
-
this.props.navigation
-
navigate
-转到另一个屏幕, 计算出需要执行的操作 -
goBack
-关闭活动屏幕并在堆栈中向后移动 -
addListener
-订阅导航生命周期的更新 -
isFocused
-函数返回true
如果屏幕焦点和false
否则。 -
state
-当前状态/路由 -
setParams
-对路由的参数进行更改 -
getParam
-获取具有回退的特定参数 -
dispatch
- 向路由发送 action -
dangerouslyGetParent
- 返回父级 navigator 的函数
-
重要的是要强调navigation
prop 不传递给所有组件; 只有screen
组件会自动收到此 prop! React Navigation在这里没有做神奇的操作。 例如,如果要定义MyBackButton
组件并将其呈现为屏幕组件的子组件,则无法访问其上的navigation prop
。
this.props.navigation
上有一些取决于当前 navigator 的附加函数
如果是 StackNavigator,除了navigate
和 goBack
,还提供了如下方法:
-
this.props.navigation
-
push
- 推一个新的路由到堆栈 -
pop
- 返回堆栈中的上一个页面 -
popToTop
- 跳转到堆栈中最顶层的页面 -
replace
- 用新路由替换当前路由 -
reset
操作会擦除整个导航状态,并将其替换为多个操作的结果。 -
dismiss
- 关闭当前堆栈
-
网络层
React Native 提供了和 web 标准一致的Fetch API,用于满足开发者访问网络的需求。如果你之前使用过XMLHttpRequest
(即俗称的 ajax)或是其他的网络 API,那么 Fetch 用起来将会相当容易上手。这篇文档只会列出 Fetch 的基本用法,并不会讲述太多细节,你可以使用你喜欢的搜索引擎去搜索fetch api
关键字以了解更多信息。
要从任意地址获取内容的话,只需简单地将网址作为参数传递给 fetch 方法即可(fetch 这个词本身也就是获取
的意思):
fetch('https://mywebsite.com/mydata.json');
Fetch 还有可选的第二个参数,可以用来定制 HTTP 请求一些参数。你可以指定 header 参数,或是指定使用 POST 方法,又或是提交数据等等:
fetch('https://mywebsite.com/endpoint/', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstParam: 'yourValue',
secondParam: 'yourOtherValue',
}),
});
提交数据的格式关键取决于 headers 中的Content-Type
。Content-Type
有很多种,对应 body 的格式也有区别。到底应该采用什么样的Content-Type
取决于服务器端,所以请和服务器端的开发人员沟通确定清楚。常用的'Content-Type'除了上面的'application/json',还有传统的网页表单形式,示例如下:
fetch('https://mywebsite.com/endpoint/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'key1=value1&key2=value2',
});
可以参考Fetch 请求文档来查看所有可用的参数。
示例:
/**
* @param {string} url 接口地址
* @param {string} method 请求方法:GET、POST,只能大写
* @param {JSON} [params=''] body的请求参数,默认为空
* @return 返回Promise
*/
function fetchRequest(url, method, params = '') {
let header = {
"Content-Type": "application/json;charset=UTF-8",
"accesstoken": token //用户登陆后返回的token,某些涉及用户数据的接口需要在header中加上token
};
console.log('request url:', url, params); //打印请求参数
if (params == '') { //如果网络请求中带有参数
return new Promise(function (resolve, reject) {
fetch(common_url + url, {
method: method,
headers: header
}).then((response) => response.json())
.then((responseData) => {
console.log('res:', url, responseData); //网络请求成功返回的数据
resolve(responseData);
})
.catch((err) => {
console.log('err:', url, err); //网络请求失败返回的数据
reject(err);
});
});
} else { //如果网络请求中没有参数
return new Promise(function (resolve, reject) {
fetch(common_url + url, {
method: method,
headers: header,
body: JSON.stringify(params) //body参数,通常需要转换成字符串后服务器才能解析
}).then((response) => response.json())
.then((responseData) => {
console.log('res:', url, responseData); //网络请求成功返回的数据
resolve(responseData);
})
.catch((err) => {
console.log('err:', url, err); //网络请求失败返回的数据
reject(err);
});
});
}
}
组件库
beeshell —— 开源的 React Native 组件
原生交互
有时候 App 需要访问平台 API,但 React Native 可能还没有相应的模块包装;或者你需要复用一些 Java 代码或者OC代码,而不是用 Javascript 重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
我们把 React Native 设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不认为它应当在日常开发的过程中经常出现,但具备这样的能力是很重要的。如果 React Native 还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。
iOS与原生交互:参考资料
Android与原生交互:参考资料