ReactNativeAppDemo
ReactNativeAppDemo 是一個以react-native+mobx為基礎搭建的app案例,旨在讓初學者瞭解基本的RNApp的搭建與應用。
教程包含基礎框架搭建、路由封裝、導航欄封裝、service封裝、全域性報錯處理封裝、高階元件封裝、全域性事件訊息匯流排封裝...
支援平臺
- IOS
- Android
效果圖
基礎環境搭建
按照react-native中文官網搭建基礎環境
$ react-native init ReactNativeAppDemo
複製程式碼
相關配置:(如Eslint、git、Babel等)
1.Eslint配置: 在根目錄新增.eslintrc 和 .eslintignore,具體配置看本文原始碼,也可查閱官方資料
$ yarn add eslint babel-eslint eslint-config-prettier eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-promise eslint-plugin-react -D
複製程式碼
2.git配置:根據官方自行配置,也可參考本文原始碼
路由功能
一般RN的路由通過官方推薦的react-navigation來實現,目前官方出到3.x
在你的 React Native 專案中安裝react-navigation
這個包
$ yarn add react-navigation
複製程式碼
然後,安裝 react-native-gesture-handler。
$ yarn add react-native-gesture-handler
複製程式碼
Link 所有的原生依賴
$ react-native link react-native-gesture-handler
複製程式碼
若想獲取更多配置和用法,請移步至react-navigation中文官網
路由元件封裝
1.根目錄新建src/pages/
目錄,在pages
下新建home/home.js
a)home.js
示例:(所用的history及colors在後文也有示例)
import React, { Component } from 'react'
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
import {colors} from '../../assets/styles/colors-theme';
import history from '../../common/history';
export default class Home extends Component {
render() {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>Home</Text>
<TouchableOpacity style={styles.button} onPress={() => history.push(this, '/list', {name: 'niunai'})}>
<Text style={styles.buttonText}>跳轉到List</Text>
</TouchableOpacity>
</View>
)
}
}
const styles = StyleSheet.flatten({
button: {
marginTop: 20,
width: 100,
height: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.statusBarColor
},
buttonText: {
color: '#fff'
}
})
複製程式碼
b)在pages
下新建page1/page1.js
、page2/page2.js
、page3/page3.js
、list/list.js
、detail/detail.js
,示例如下:(僅寫一個示例,其他自行模仿)
page1
示例:
import React, { Component } from 'react'
import { View, Text } from 'react-native'
export default class Page1 extends Component {
render() {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>Page1</Text>
</View>
)
}
}
複製程式碼
c)在pages
下新建router.js
,示例如下(tab-nav
下文會講到):
import {createStackNavigator, createAppContainer} from 'react-navigation'
import List from './pages/list/list';
import Detail from './pages/detail/detail';
import {TabNav} from './common/tab-nav';
function generateRoute(path, screen) {
return {
path,
screen
}
}
const stackRouterMap = {
list: generateRoute('/list', List),
detail: generateRoute('/detail', Detail),
main: TabNav
}
const stackNavigate = createStackNavigator(stackRouterMap, {
initialRouteName: 'main',
headerMode: 'none'
})
const Router = createAppContainer(stackNavigate)
export default Router
複製程式碼
d)tab-nav
的封裝: src
目錄下新建common/tab-nav.js
,示例如下(Icon 下文有介紹):
import React from 'react'
import Icon from 'react-native-vector-icons/Ionicons'
import {createBottomTabNavigator} from 'react-navigation';
import Home from '../pages/home/home';
import Page3 from '../pages/page3/page3';
import Page1 from '../pages/page1/page1';
import Page2 from '../pages/page2/page2';
import {colors} from '../assets/styles/colors-theme';
const TabRouterMap = {
home: {
screen: Home,
navigationOptions: {
tabBarLabel: 'Home',
tabBarIcon:({focused}) => (
<Icon
focused={focused}
name="md-close-circle"
color={focused ? colors.statusBarColor : '#000'}
/>
)
}
},
page1: {
screen: Page1,
navigationOptions: {
tabBarLabel: 'Page1',
tabBarIcon:({focused}) => (
<Icon
focused={focused}
name="md-close-circle"
color={focused ? colors.statusBarColor : '#000'}
/>
)
}
},
page2: {
screen: Page2,
navigationOptions: {
tabBarLabel: 'Page2',
tabBarIcon:({focused}) => (
<Icon
focused={focused}
name="md-close-circle"
color={focused ? colors.statusBarColor : '#000'}
/>
)
}
},
page3: {
screen: Page3,
navigationOptions: {
tabBarLabel: 'Page3',
tabBarIcon:({focused}) => (
<Icon
focused={focused}
name="md-close-circle"
color={focused ? colors.statusBarColor : '#000'}
/>
)
}
}
}
export const TabNav = createBottomTabNavigator(TabRouterMap,{
initialRouteName: 'home',
tabBarOptions: {
//當前選中的tab bar的文字顏色和圖示顏色
activeTintColor: colors.statusBarColor,
//當前未選中的tab bar的文字顏色和圖示顏色
inactiveTintColor: '#000',
//是否顯示tab bar的圖示,預設是false
showIcon: true,
//showLabel - 是否顯示tab bar的文字,預設是true
showLabel: true,
//是否將文字轉換為大小,預設是true
upperCaseLabel: false,
//material design中的波紋顏色(僅支援Android >= 5.0)
pressColor: 'red',
//按下tab bar時的不透明度(僅支援iOS和Android < 5.0).
pressOpacity: 0.8,
//tab bar的樣式
// style: {
// backgroundColor: '#fff',
// paddingBottom: 1,
// borderTopWidth: 0.2,
// paddingTop:1,
// borderTopColor: '#ccc',
// },
//tab bar的文字樣式
labelStyle: {
fontSize: 11,
margin: 1
},
//tab 頁指示符的樣式 (tab頁下面的一條線).
indicatorStyle: {height: 0},
},
//tab bar的位置, 可選值: 'top' or 'bottom'
tabBarPosition: 'bottom',
//是否允許滑動切換tab頁
swipeEnabled: true,
//是否在切換tab頁時使用動畫
animationEnabled: false,
//是否懶載入
lazy: true,
//返回按鈕是否會導致tab切換到初始tab頁? 如果是,則設定為initialRoute,否則為none。 預設為initialRoute。
backBehavior: 'none'
})
複製程式碼
e)在App.js
中使用路由:
import React, {Component} from 'react';
import {StatusBar} from 'react-native';
import {SafeAreaView} from 'react-navigation'
import Router from './src/router';
import {colors} from './src/assets/styles/colors-theme';
export default class App extends Component {
render() {
return (
<SafeAreaView
style={{flex: 1, backgroundColor: colors.statusBarColor}}
forceInset={{
top: 'always',
bottom: 'always'
}}
>
<StatusBar
animated={true}
barStyle={'light-content'}
backgroundColor={colors.statusBarColor}
translucent={true}
/>
<Router/>
</SafeAreaView>
);
}
}
複製程式碼
2.history的基本封裝
history是控制路由跳轉的模組,一般封裝出push、replace、goback、pop等,在src
目錄下新建common/history.js
,示例如下:
const NAVIGATION_THROTTLE = 1000; // 1s內不準重複跳轉
const lastNavigationTimeStamps = {};
/**
* 校驗頁面跳轉引數 防止同一個path在很短的時間內被反覆呼叫
* @param path
*/
function validate(path) {
const timestamp = new Date().valueOf();
if(lastNavigationTimeStamps[path] && (timestamp - lastNavigationTimeStamps[path]) < NAVIGATION_THROTTLE) {
lastNavigationTimeStamps[path] = timestamp
return false
} else {
lastNavigationTimeStamps[path] = timestamp
}
return true
}
/**
* 處理路由跳轉的狀態
* @param prevState
* @param newState
* @param action
*/
export function handleNavigationChange(prevState, newState, action) {
console.log('@@@@@ prevState', prevState)
console.log('@@@@@ newState', newState)
console.log('@@@@@ action', action)
}
const history = {
push: (instance, path, state) => {
if(validate(path)) {
const navigationController = instance.props.navigation;
const nativePath =
path.charAt(0) === '/' ? path.substring(1, path.length) : path;
navigationController.push(nativePath, state)
}
},
replace: (instance, path, state) => {
if(validate(path)) {
const navigationController = instance.props.navigation;
const nativePath =
path.charAt(0) === '/' ? path.substring(1, path.length) : path;
navigationController.replace(nativePath, state)
}
},
goBack: (instance) => {
if(instance) {
const navigationController = instance.props.navigation;
navigationController.goBack()
}
},
pop: (instance, n) => {
if(instance) {
const navigationController = instance.props.navigation;
navigationController.pop(-1 * n || -1)
}
}
}
export default history;
複製程式碼
修改App.js
中的Router
<Router/>
改為
import {handleNavigationChange} from './src/common/history'
<Router
onNavigationStateChange={handleNavigationChange}
/>
複製程式碼
字型圖示庫
app中需要用到大量的小圖示,本文選擇react-native-vector-icons
$ yarn add react-native-vector-icons
$ react-native link react-native-vector-icons
使用方法:
import Icon from 'react-native-vector-icons/Ionicons'
<Icon
name="md-close-circle"
color={'#000'}
/>
複製程式碼
其他配置
1.樣式配置
src
目錄下新建assets/styles/colors-theme.js
示例:全域性控制整個APP所需的顏色
export const colors = {
statusBarColor: '#23A2FF'
}
複製程式碼
2.服務基礎配置
src/common
目錄下新建constants.js
, 用於配置全域性所需的服務地址、裝置號、裝置型別、版本號、分頁數量等等
(如果不需要裝置號則無需下載)
$ yarn add react-native-device-info
$ react-native link react-native-device-info
/**
* 提供基礎配置資訊
* constants.js 提供如伺服器地址、分頁數量、裝置型別、裝置號、版本號等配置
*/
import { Platform } from 'react-native'
import DeviceInfo from 'react-native-device-info'
export default {
serverUrl: 'http://127.0.0.1:3600/portal',
pageSize: 10,
deviceType: Platform.OS.toUpperCase(),
deviceNo: DeviceInfo.getUniqueID().replace('-').substr(0, 12),
versionName: DeviceInfo.getVersion(), //也可寫死如'1.0.0'
}
複製程式碼
3.服務報錯配置
src/common
目錄下新建service-error.js
, 用於配置全域性服務報錯
/**
* 服務報錯處理
*/
export default class ServiceError extends Error{
constructor(code, message){
super(message);
this.code = code;
this.hash = Math.random() * 100000000000000000;
this.signature = 'ServiceError';
}
}
複製程式碼
4.code配置
src/common
目錄下新建code.js
, 用於配置全域性請求code
/**
* code.js提供全域性的請求服務欄位處理
*/
export default {
SUCCESS: 'SUCCESS', //請求成功
REQUEST_FAILED: 'REQUEST_FAILED', //請求失敗
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', //請求超時
UN_KNOWN_ERROR: 'UN_KNOWN_ERROR', //未知錯誤
TOKEN_INVALID: 'TOKEN_INVALID', //token失效
SESSION_TIMEOUT: 'SESSION_TIMEOUT', //會話超時
}
複製程式碼
封裝頂部導航欄NaviBar
頂部導航欄用於顯示當前頁面的標題,操作路由的跳轉,放置部分功能模組,如分享、彈框、設定等
新增prop-types
,用於封裝型別校驗
$ yarn add prop-types
複製程式碼
在src
下新建components/navi-bar.js
,示例如下:
import React, { Component } from 'react'
import { View, Text, StyleSheet, Dimensions, TouchableOpacity, Platform } from 'react-native'
import PropTypes from 'prop-types'
import {colors} from '../assets/styles/colors-theme';
import Icon from 'react-native-vector-icons/Ionicons'
const { width } = Dimensions.get('window')
export default class NaviBar extends Component {
static propTypes = {
style: PropTypes.object,
leftItem: PropTypes.node, //原則上控制在寬度40的icon
rightItem: PropTypes.node, //原則上控制在寬度40的icon
title: PropTypes.string,
titleColor: PropTypes.string,
onBack: PropTypes.func,
iconColor: PropTypes.string
}
render() {
const props = this.props;
return (
<View style={[styles.naviBar, props.style]}>
<View style={{width: 40}}>
{
props.leftItem ? props.leftItem : (
props.onBack ? (
<TouchableOpacity style={{paddingLeft: 15}} onPress={props.onBack}>
<Icon
name="md-arrow-back"
size={20}
color={props.iconColor || '#ffffff'}
/>
</TouchableOpacity>
) : <View/>
)
}
</View>
<Text style={{color: props.titleColor || '#fff'}}>{props.title}</Text>
<View style={{width: 40}}>
{
props.rightItem ? props.rightItem : <View/>
}
</View>
</View>
)
}
}
const styles = StyleSheet.flatten({
naviBar: {
width,
height: Platform.OS === 'ios' ? 44 : 56, //ios原生導航高度是44,android是56
backgroundColor: colors.statusBarColor,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
}
})
複製程式碼
修改list.js
,示例如下:
import React, { Component } from 'react'
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native'
import NaviBar from '../../components/navi-bar';
import history from '../../common/history';
import {colors} from '../../assets/styles/colors-theme';
export default class List extends Component {
render() {
return (
<View style={{flex: 1}}>
<NaviBar
title={'List列表頁'}
onBack={history.goBack.bind(this, this)}
/>
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>List</Text>
<TouchableOpacity style={styles.button} onPress={() => history.push(this, '/detail', {name: 'suannai'})}>
<Text style={styles.buttonText}>跳轉到Detail</Text>
</TouchableOpacity>
</View>
</View>
)
}
}
const styles = StyleSheet.flatten({
button: {
marginTop: 20,
width: 100,
height: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.statusBarColor
},
buttonText: {
color: '#fff'
}
})
複製程式碼
基礎服務請求返回封裝
一般來說,大專案都需要統一封裝一個基礎服務元件,通過這個元件去全域性處理request和返回response,處理全域性的服務報錯。
1.fetch
攔截器的實現:
$ yarn add fetch-intercept
示例如下:
import fetchIntercept from 'fetch-intercept'
fetchIntercept.register({
request: function (url, config) {
return [url, config];
},
requestError: function (error) {
return Promise.reject(error);
},
response: function (res) {
return res;
},
responseError: function (error) {
return Promise.reject(error);
}
});
複製程式碼
2.在src
下新建services/base-service.js
,封裝BaseService
(constants、ServiceError、code的封裝在上面)
/**
* 基礎服務類封裝
* BaseService
*/
import fetchIntercept from 'fetch-intercept'
import constants from '../common/constants';
import ServiceError from '../common/service-error';
import code from '../common/code';
const fetchApi = fetch; // eslint-disable-line
fetchIntercept.register({
request: function (url, config) {
return [url, config];
},
requestError: function (error) {
return Promise.reject(error);
},
response: function (res) {
return res;
},
responseError: function (error) {
return Promise.reject(error);
}
});
export default class BaseService {
constructor(props){
if(props && props.showLoading){
this.showLoading = props.showLoading;
}
if(props && props.hideLoading){
this.hideLoading = props.hideLoading;
}
}
async request(method, url, params, errorMsgIndex, showLoading = true, acceptType = 'application/json') {
// 如果url不全,則自動補全
if(url.indexOf('http://') < 0 && url.indexOf('https://') < 0){
url = constants.serverUrl + '/' + url;
}
if(showLoading && this.showLoading){
this.showLoading();
}
let res = null
let timer = null
try {
const options = {
method: method,
credentials: 'include',
headers: {
'content-type': 'application/json',
'accept': acceptType,
'Cache-Control': 'no-cache'
}
}
if(method === 'POST' || method === 'PUT') {
params.DeviceType = constants.deviceType
params.DeviceNo = constants.deviceNo
options.body = JSON.stringify(params || {})
}
res = await fetchApi(url, options)
} catch (e) {
if(this.hideLoading){
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
this.hideLoading()
}, 2000)
}
throw new ServiceError(code.REQUEST_FAILED, '網路請求失敗')
}
if(res.status && res.status >= 200 && res.status < 300) {
const contentType = res.headers.get('Content-Type')
if(this.hideLoading){
this.hideLoading()
}
if(contentType.indexOf('text/plain') >= 0 || contentType.indexOf('text/html') >= 0){
return res.text()
}else{
const responseJson = await res.json();
if (responseJson && !responseJson.jsonError) {
return responseJson
} else {
throw new ServiceError(responseJson.jsonError[0]._exceptionMessageCode || code.REQUEST_FAILED, responseJson.jsonError[0]._exceptionMessage);
}
}
} else {
if(this.hideLoading){
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
this.hideLoading()
}, 2000)
}
if (res.status === 401) {
throw new ServiceError(code.REQUEST_TIMEOUT, res.data.message);
} else if (res.ok) {
try {
const responseJson = await res.json();
const { message } = responseJson;
throw new ServiceError(code.REQUEST_FAILED, message);
} catch (e) {
throw new ServiceError(code.REQUEST_FAILED, '服務未知錯誤');
}
}
}
}
/**
* GET 後臺資料
* @param url
* @param errorMsg 報錯訊息
* @returns {Promise<*>}
*/
async fetchJson(url, errorMsg, showLoading = true){
return await this.request('GET', url, null, errorMsg, showLoading)
}
/**
* POST請求
* @param url
* @param params
* @param errorMsg 報錯訊息
* @returns {Promise.<void>}
*/
async postJson(url, params, errorMsg, showLoading = true){
return await this.request('POST', url, params, errorMsg, showLoading)
}
}
複製程式碼
使用BaseService
在src/services
下新建list-service.js
/**
* 列表頁服務
*/
import BaseService from './base-service';
export default class ListService extends BaseService {
/**
* 獲取列表
* @return {Promise<void>}
*/
async getList() {
const res = await this.postJson('qryList.do', {})
return res;
}
}
複製程式碼
修改list.js
...
import ListService from '../../services/list-service';
export default class List extends Component {
constructor(props) {
super(props)
...
this.listService = new ListService(props)
}
async componentDidMount() {
const res = await this.listService.getList();
...
}
...
}
複製程式碼
基礎服務的使用
1.封裝LoadingView
封裝LoadingView
是給全域性提供一個載入動畫,伺服器的載入需要時間,一般以載入動畫來過渡。目前我選擇國際上最火的lottie,動畫所需json
檔案自行去lottiefiles下載
$ yarn add lottie-react-native
$ react-native link lottie-react-native
$ react-native link lottie-ios
針對IOS的XCode配置
General > Embedded Binaries > add Lottie.framework
複製程式碼
在src/common
下新建loading.js
, 同時在src/assets
下新建animations/loading.json
import React, { Component } from 'react'
import {View, Dimensions, StyleSheet} from 'react-native';
import LottieView from 'lottie-react-native';
import LoadingAnimation from '../assets/animations/loading';
const { width, height } = Dimensions.get('window')
export default class LoadingView extends Component {
render(){
if(this.props.visible){
return (
<View style={styles.wrapper}>
<View style={styles.loading}>
<LottieView source={LoadingAnimation} autoPlay={this.props.visible} loop={this.props.visible} />
</View>
</View>
)
}else{
return <View/>
}
}
}
const styles = StyleSheet.flatten({
wrapper: {
position: 'absolute',
top: 0,
left: 0,
width: width,
height: height,
backgroundColor: 'rgba(0, 0, 0, 0.2)'
},
loading:{
position: 'absolute',
top: height / 2 - 100,
left: width / 2 - 70,
width: 140,
height: 140
}
});
複製程式碼
在App.js
中使用LoadingView
,引入LoadingView放在Router下方就行
import LoadingView from './src/common/loading'
...
<Router
onNavigationStateChange={handleNavigationChange}
/>
<LoadingView visible={this.state.loadingCount > 0} />
...
複製程式碼
2.loading-hoc高階元件封裝
在src
下新建hocs/loading-hoc.js
, loading-hoc是一個高階元件,用於在頁面外部以@
修飾符引用LoadingHoc
(檢視Event事件訊息匯流排封裝)
import React, {Component} from 'react';
import {Dimensions, View} from 'react-native';
const { width, height } = Dimensions.get('window')
import Event from '../common/event'
export default function LoadingHoc(WrappedComponent) {
return class ComposedComponent extends Component {
showLoading(){
Event.emit('SHOW_LOADING')
}
hideLoading(){
Event.emit('HIDE_LOADING')
}
render() {
const props = {...this.props, ...{
showLoading: this.showLoading.bind(this),
hideLoading: this.hideLoading.bind(this)
}};
return (
<View style={{width, height}}>
<WrappedComponent {...props} />
</View>
);
}
};
}
複製程式碼
在List
中引入LoadingHoc
...
@LoadingHoc
export default class List extends Component {
constructor(props) {
super(props)
...
this.listService = new ListService(props)
}
async componentDidMount() {
const res = await this.listService.getList();
...
}
}
複製程式碼
3.全域性事件訊息匯流排封裝
在src/common
下新建notification-center.js
和event.js
notification-center.js示例:
const __notices = [];
/**
* addNotification
* 註冊通知物件方法
*
* 引數:
* name: 註冊名,一般let在公共類中
* selector: 對應的通知方法,接受到通知後進行的動作
* observer: 註冊物件,指Page物件
*/
function addNotification(name, selector, observer) {
if (name && selector) {
if (!observer) {
console.log(
"addNotification Warning: no observer will can't remove notice"
);
}
const newNotice = {
name: name,
selector: selector,
observer: observer
};
addNotices(newNotice);
} else {
console.log('addNotification error: no selector or name');
}
}
/**
* 僅新增一次監聽
*
* 引數:
* name: 註冊名,一般let在公共類中
* selector: 對應的通知方法,接受到通知後進行的動作
* observer: 註冊物件,指Page物件
*/
function addOnceNotification(name, selector, observer) {
if (__notices.length > 0) {
for (let i = 0; i < __notices.length; i++) {
const notice = __notices[i];
if (notice.name === name) {
if (notice.observer === observer) {
return;
}
}
}
}
this.addNotification(name, selector, observer);
}
function addNotices(newNotice) {
// if (__notices.length > 0) {
// for (var i = 0; i < __notices.length; i++) {
// var hisNotice = __notices[i];
// //當名稱一樣時進行對比,如果不是同一個 則放入陣列,否則跳出
// if (newNotice.name === hisNotice.name) {
// if (!cmp(hisNotice, newNotice)) {
// __notices.push(newNotice);
// }
// return;
// }else{
// __notices.push(newNotice);
// }
// }
// } else {
// }
__notices.push(newNotice);
}
/**
* removeNotification
* 移除通知方法
*
* 引數:
* name: 已經註冊了的通知
* observer: 移除的通知所在的Page物件
*/
function removeNotification(name, observer) {
console.log('removeNotification:' + name);
for (let i = 0; i < __notices.length; i++) {
const notice = __notices[i];
if (notice.name === name) {
if (notice.observer === observer) {
__notices.splice(i, 1);
return;
}
}
}
}
/**
* postNotificationName
* 傳送通知方法
*
* 引數:
* name: 已經註冊了的通知
* info: 攜帶的引數
*/
function postNotificationName(name, info) {
console.log('postNotificationName:' + name);
if (__notices.length === 0) {
console.log("postNotificationName error: u hadn't add any notice.");
return;
}
for (let i = 0; i < __notices.length; i++) {
const notice = __notices[i];
if (notice.name === name) {
notice.selector(info);
}
}
}
// 用於對比兩個物件是否相等
function cmp(x, y) { // eslint-disable-line
// If both x and y are null or undefined and exactly the same
if (x === y) {
return true;
}
// If they are not strictly equal, they both need to be Objects
if (!(x instanceof Object) || !(y instanceof Object)) {
return false;
}
// They must have the exact same prototype chain, the closest we can do is
// test the constructor.
if (x.constructor !== y.constructor) {
return false;
}
for (const p in x) {
// Inherited properties were tested using x.constructor === y.constructor
if (x.hasOwnProperty(p)) {
// Allows comparing x[ p ] and y[ p ] when set to undefined
if (!y.hasOwnProperty(p)) {
return false;
}
// If they have the same strict value or identity then they are equal
if (x[p] === y[p]) {
continue;
}
// Numbers, Strings, Functions, Booleans must be strictly equal
if (typeof x[p] !== 'object') {
return false;
}
// Objects and Arrays must be tested recursively
if (!Object.equals(x[p], y[p])) {
return false;
}
}
}
for (const p in y) {
// allows x[ p ] to be set to undefined
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
return false;
}
}
return true;
}
module.exports = {
addNotification: addNotification,
removeNotification: removeNotification,
postNotificationName: postNotificationName,
addOnceNotification: addOnceNotification
};
複製程式碼
event.js示例:
/**
* 一個JavaScript 事件訊息匯流排
*/
import NotificationCenter from './notification-center';
export default class Event {
static listen(eventName, callback, observer) {
NotificationCenter.addNotification(eventName, callback, observer);
}
static emit(eventName, params) {
NotificationCenter.postNotificationName(eventName, params);
}
static remove(eventName, observer) {
NotificationCenter.removeNotification(eventName, observer);
}
}
複製程式碼
4.全域性報錯處理
在src/common
下新建global-error-handler.js
global-error-handler.js示例:
import code from './code';
import Event from './event'
export function handleErrors(error){
if(error && error.signature && error.signature === 'ServiceError') {
defaultServiceErrorHandler(error);
}else{
defaultErrorHandler(error);
}
}
function defaultServiceErrorHandler(error){
if(error && error.code === code.SESSION_TIMEOUT){
Event.emit('GLOBAL_ERROR', {
type: 'SESSION_TIMEOUT'
})
}else if(error && error.message) {
Event.emit('GLOBAL_ERROR', {
type: 'SERVICE_ERROR',
message: error.message
})
}else {
Event.emit('GLOBAL_ERROR', {
type: 'SERVICE_ERROR',
message: '服務出錯,請稍後再試.'
})
}
}
function defaultErrorHandler(error){
if(error && error.message) {
Event.emit('GLOBAL_ERROR', {
type: 'SERVICE_ERROR',
message: error.message
})
}else {
Event.emit('GLOBAL_ERROR', {
type: 'SERVICE_ERROR',
message: '服務出錯,請稍後再試.'
})
}
}
複製程式碼
5.全域性監聽
$ yarn add promise-polyfill
$ yarn add @ant-design/react-native
$ react-native link @ant-design/icons-react-native
複製程式碼
App.js
新增promise
的報錯處理和使用antd-mobileRN版本
來進行彈框報錯。
注:如果需要使用Modal
以及Toast
還需要在 App
的入口處加上Provider
, 因mobx
也需要使用Provider
, 本文另定義為ProviderAntd
, mobx
的使用教程在下面可找到
import {Provider as ProviderAntd, Modal} from '@ant-design/react-native'
import LoadingView from './src/common/loading';
import {handleErrors} from './src/common/global-error-handler';
import Event from './src/common/event';
@observer
export default class App extends Component {
constructor(props) {
super(props)
this.timer = null
this.state = {
loadingCount: 0
}
require('promise/setimmediate/rejection-tracking').enable({
allRejections: true,
onUnhandled: (id, error) => {
handleErrors(error);
}
})
this._handleGlobalError = this.handleGlobalError.bind(this)
this._handleShowLoading = this.handleShowLoading.bind(this)
this._handleHideLoading = this.handleHideLoading.bind(this)
}
componentDidMount() {
// 監聽全域性報錯
Event.listen('GLOBAL_ERROR', this._handleGlobalError, this)
// 顯示載入動畫
Event.listen('SHOW_LOADING', this._handleShowLoading, this)
// 隱藏載入動畫
Event.listen('HIDE_LOADING', this._handleHideLoading, this)
}
//元件解除安裝之前移除監聽
componentWillUnmount() {
Event.remove('GLOBAL_ERROR', this)
Event.remove('SHOW_LOADING', this)
Event.remove('HIDE_LOADING', this)
}
render() {
return (
<Provider rootStore={stores}>
<ProviderAntd>
<SafeAreaView ...>
...
<LoadingView visible={this.state.loadingCount > 0} />
</SafeAreaView>
</ProviderAntd>
</Provider>
);
}
/**
* showLoading
*/
handleShowLoading() {
if(this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.setState({
loadingCount: this.state.loadingCount + 1
})
}, 50)
}
/**
* hideLoading
* @param bForece
*/
handleHideLoading(bForece){
if(this.timer){
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
if(this.state.loadingCount > 0){
this.setState({
loadingCount: (bForece ? 0 : this.state.loadingCount - 1)
});
}
}, 50)
}
/**
* 全域性報錯處理
* @param event
*/
handleGlobalError(event) {
// 報錯時,取消載入動畫
if(this.state.loadingCount > 0){
this.handleHideLoading(true)
}
if(event && event.type){
switch(event.type){
case 'SESSION_TIMEOUT':
Modal.alert('會話超時', '您的會話已超時,請重新登入')
break;
case 'SERVICE_ERROR':
if(event.message) {
Modal.alert('出錯了', event.message)
}
break;
default:
if(event.message) {
Modal.alert('溫馨提示', '系統未知異常')
}
break;
}
}
}
}
複製程式碼
mobx的使用
$ yarn add mobx mobx-react
$ yarn add @babel/cli @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-object-rest-spread @babel/plugin-transform-classes @babel/plugin-transform-flow-strip-types @babel/plugin-transform-runtime @babel/polyfill @babel/preset-env @babel/preset-flow @babel/preset-react babel-loader babel-plugin-import babel-plugin-module-resolver babel-plugin-transform-runtime babel-polyfill babel-preset-es2015 babel-preset-react babel-preset-react-native babel-preset-react-native-stage-0 babel-preset-react-native-syntax -D
複製程式碼
.babelrc
或babel.config.js
新增如下程式碼:
{
presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'],
plugins: [
'@babel/transform-flow-strip-types',
[
'@babel/plugin-proposal-decorators', { 'legacy' : true }
],
[
'@babel/plugin-proposal-class-properties', {'loose': true}
],
[
'@babel/plugin-transform-runtime', {}
],
['import', { 'libraryName': '@ant-design/react-native' }]
]
}
複製程式碼
在src
下新建stores/app-store.js
和stores/index.js
stores/app-store.js
示例:
import {observable, action} from 'mobx'
class AppStore {
@observable
list = []
@observable
timer = 0
@action
setList(data){
this.list = data
}
@action
resetTimer() {
this.timer = 0
}
@action
tick() {
this.timer += 1
}
}
const appStore = new AppStore()
export {appStore}
複製程式碼
stores/index.js
示例:
import {appStore} from './app-store'
export {appStore}
複製程式碼
在App.js
中新增mobx
,具體使用參考mobx中文文件
App.js
示例:
...
import { Provider, observer } from 'mobx-react'
import * as stores from './src/stores/index';
@observer
export default class App extends Component {
...
render() {
return (
<Provider rootStore={stores}>
...
</Provider>
);
}
}
複製程式碼
在home.js
中引入mobx
, 統計點選list的次數和給List頁傳值
...
import NaviBar from '../../components/navi-bar';
import { inject, observer } from 'mobx-react';
@inject('rootStore')
@observer
export default class Home extends Component {
constructor(props) {
...
this.store = props.rootStore.appStore
}
render() {
return (
<View style={{flex: 1}}>
<NaviBar title={'Home'}/>
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>Home</Text>
<TouchableOpacity style={styles.button} onPress={() => {
//新增timer的次數
this.store.tick();
const list = this.store.list;
list.push({
number: this.store.timer,
label: '第'+this.store.timer + '次點選'
})
this.store.setList(list)
history.push(this, '/list', {name: 'niunai'})
}}>
<Text style={styles.buttonText}>跳轉到List</Text>
</TouchableOpacity>
<View style={{marginTop: 30}}>
<Text>統計跳轉到List的次數: {this.store.timer}</Text>
</View>
<TouchableOpacity style={[styles.button, {width: 140}]} onPress={() => {
this.store.setList([]);
this.store.resetTimer();
}}>
<Text style={styles.buttonText}>重置List和timer</Text>
</TouchableOpacity>
</View>
</View>
)
}
}
複製程式碼
在list.js
中引入mobx
, 統計點選list的次數和渲染list
import React, { Component } from 'react'
import {View, Text, TouchableOpacity, StyleSheet, Dimensions} from 'react-native'
import NaviBar from '../../components/navi-bar';
import history from '../../common/history';
import {colors} from '../../assets/styles/colors-theme';
import ListService from '../../services/list-service';
import LoadingHoc from '../../hocs/loading-hoc';
import {inject, observer} from 'mobx-react';
const { width } = Dimensions.get('window')
@LoadingHoc
@inject('rootStore')
@observer
export default class List extends Component {
constructor(props) {
super(props)
this.state = {
list: [],
name: props.navigation.state.params.name
}
this.listService = new ListService(props)
this.store = props.rootStore.appStore
}
async componentDidMount() {
const res = await this.listService.getList();
if(res) {
this.setState({
list: res
})
}
}
render() {
return (
<View style={{flex: 1}}>
<NaviBar
title={'List列表頁'}
onBack={history.goBack.bind(this, this)}
/>
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<View style={{marginBottom: 50}}>
<Text>Home頁傳過來的name:{this.state.name}</Text>
</View>
<View style={{marginBottom: 50}}>
<Text>統計到{this.store.timer}次跳轉到List</Text>
</View>
<View style={{flexDirection: 'row', backgroundColor: '#ccc'}}>
<View style={styles.number}>
<Text>次數</Text>
</View>
<View style={styles.label}>
<Text>描述</Text>
</View>
</View>
{
this.store.list.map((item, index) => {
return (
<View style={{flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#ccc'}}>
<View style={styles.number}>
<Text>{item.number}</Text>
</View>
<View style={styles.label}>
<Text>{item.label}</Text>
</View>
</View>
)
})
}
<TouchableOpacity style={styles.button} onPress={() => history.push(this, '/detail', {name: 'suannai'})}>
<Text style={styles.buttonText}>跳轉到Detail</Text>
</TouchableOpacity>
</View>
</View>
)
}
}
const styles = StyleSheet.flatten({
button: {
marginTop: 20,
width: 100,
height: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.statusBarColor
},
buttonText: {
color: '#fff'
},
number: {
width: 0.3 * width,
height: 40,
alignItems: 'center',
justifyContent: 'center'
},
label: {
width: 0.7 * width,
height: 40,
alignItems: 'center',
justifyContent: 'center'
}
})
複製程式碼
注意:本例中主要是通過在App.js
中建立一個全域性的store
,命名為rootStore
來控制所有頁面的狀態,
在home.js
頁去觸發action
增加timer
和setList
以及重置timer
和list
,
在list.js
去渲染timer
和list
Demo總結
至此,本文主要描述瞭如何構架一個react-native的APP,從基礎搭建、全域性的路由、路由history
控制、全域性event
事件訊息匯流排封裝、全域性error
報錯處理和監聽、服務配置、基礎service
搭建到使用mobx
進行全域性狀態監控就基本上講完了,其中還包含了如何封裝頂部導航欄NaviBar
、引用字型圖示庫react-native-vector-icons
、引入螞蟻金服的@ant-design/react-native
, 同時涉及到高階元件loading-hoc
的搭建使用和lottie
動畫的使用。