react-native+mobx的基礎搭建

ReactNativeAppDemo

ReactNativeAppDemo 是一個以react-native+mobx爲基礎搭建的app案例,旨在讓初學者瞭解基本的RNApp的搭建與應用。html

教程包含基礎框架搭建、路由封裝、導航欄封裝、service封裝、全局報錯處理封裝、高階組件封裝、全局事件消息總線封裝...node

查看github地址react

支持平臺

  • IOS
  • Android

效果圖

demo

基礎環境搭建

按照react-native中文官網搭建基礎環境android

$ react-native init ReactNativeAppDemo
複製代碼

相關配置:(如Eslint、git、Babel等)ios

1.Eslint配置: 在根目錄新增.eslintrc 和 .eslintignore,具體配置看本文源碼,也可查閱官方資料git

$ 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配置:根據官方自行配置,也可參考本文源碼github

路由功能

通常RN的路由經過官方推薦的react-navigation來實現,目前官方出到3.xjson

在你的 React Native 項目中安裝react-navigation這個包react-native

$ 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示例:(所用的historycolors在後文也有示例)

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.jspage2/page2.jspage3/page3.jslist/list.jsdetail/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,封裝BaseServiceconstantsServiceErrorcode的封裝在上面)

/** * 基礎服務類封裝 * 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)
  }

}
複製代碼

使用BaseServicesrc/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.jsevent.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	
複製代碼

.babelrcbabel.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.jsstores/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增長timersetList以及重置timerlist, 在list.js去渲染timerlist

Demo總結

至此,本文主要描述瞭如何構架一個react-native的APP,從基礎搭建、全局的路由、路由history控制、全局event事件消息總線封裝、全局error報錯處理和監聽、服務配置、基礎service搭建到使用mobx進行全局狀態監控就基本上講完了,其中還包含了如何封裝頂部導航欄NaviBar、引用字體圖標庫react-native-vector-icons、引入螞蟻金服的@ant-design/react-native, 同時涉及到高階組件loading-hoc的搭建使用和lottie動畫的使用。

相關文章
相關標籤/搜索