react-native+mobx的基礎搭建

牛奶發表於2019-04-23

ReactNativeAppDemo

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

教程包含基礎框架搭建、路由封裝、導航欄封裝、service封裝、全域性報錯處理封裝、高階元件封裝、全域性事件訊息匯流排封裝...

檢視github地址

支援平臺

  • IOS
  • Android

效果圖

demo

基礎環境搭建

按照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示例:(所用的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動畫的使用。

相關文章