React Native教學 Part 2 - Project Structure

2018/04/01 posted in  React Native comments

在開始開發任何專案前,最好先決定如何structure整個專案。沒有好的structure,日後規模漸大,開發起來只會越來越困難。

I. Overview

以Expo app為例,個人推薦的structure如下:

Project-level

.
├── App.js  # App entry point
├── app.json  # CRNA / Expo 用到的config檔
├── assets/  # 放Icon、圖片等
├── .eslintrc.js  # ESLint config檔
├── node_modules/  # npm packages
├── package.json  # npm dependencies
└── src/  # 所有js檔案

src/

.
├── AppStartUpWrapper.js
├── ReduxWrapper.js
├── common/
├── components/
├── constants/
├── containers/
├── navigation/
├── package.json
├── redux/
├── services/
└── utils/

App Hierarchy

  • App.js:root
    • ReduxWrapper.js:加入Redux store
      • AppStartUpWrapper.js:加入notification listener / deeplink listener等等
        • navigation/:加入Navigation
          • 各個containers/screens
            • 各個components

redux/

建議使用Redux,方便「跨Component」共用data。如果不用Redux或類似工具,所有data和callback都要一層一層地pass下去,十分麻煩。

不過Redux的概念需要一些時間掌握,建議先看Dan Abramov(Redux作者)一段還在開發Redux時的演講,理解一下Redux的初衷和優點。

日後再寫關於Redux的教學。

navigation/

每個App都需要一個Navigation Library,例如如果你選擇的是React Navigation,你的App是Tab-based,結構可能長這樣:

  • TabNavigator
    • StackNavigator
    • StackNavigator
    • StackNavigator

把這些Navigators放在navigation/裡。

相關教學:React Native教學 - Part 2.5 選擇Navigation Library

components/ vs containers/

  • components/:
    • 放置Reusable、不依賴Redux、收到甚麼props就顯示甚麼的Components
  • containers/ (或者叫screens/或views/):
    • Containers負責使用各種Components,排版,處理data的互動
    • 可以是頁面,也可以是頁面的一部分

其他folders

  • common/:放置各Components會共用到的東西,例如主要顏色,按鈕大小等等
  • constants/:放置自行定義的constants,例如model的enum、API keys、Server endpoint等等。
  • redux/:放置所有Action Creators、Reducers
  • services/:提供其他地方會重複取用的服務,例如Server request
  • utils/:就是普通util functions

II. 解決大型專案的Import問題

package.json解決無限'../../../'地獄的良藥

Import是nodejs的一大痛處,因為它使用relative path,一般從一個檔案import另一個檔案,需要像這樣:

// src/containers/Home/SliderSection.js
import MyButton from '../../../components/MyButton';

解決辦法

React Native使用了Metro bundler來打包js檔,它提供了一個預設功能,只要在任何一個地方中建立一個package.json,就可以命名該path,就像一般node_modules一樣。

例如建立src/package.json

{
  "name": "@"
}

然後所有檔案都可以這樣使用:

import MyButton from '@/src/components/MyButton';

你也可以把所有folder都命名一遍,例如:

import MyButton from '@components/MyButton';

建議加'@'在前面,例如'@component', '@utils'等去命名,避免與npm packages撞名

III. Network Request (網絡請求)

個人推薦使用axios來進行ajax requests,以Promise的形式處理所有response,非常方便,而且Github stars已經突破40000,顯示它現在有多受歡迎。

基本用法

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

axios doc - Example

個人化axios service

通常app都會有個對應的backend server,這時我們可以建立一個instance,專門負責對應這個server的requests:

/*
  A custom axios instance
  Used for making request to server
 */
const service = axios.create({
  baseURL: 'https://www.mybackend.com/api/v1',
  timeout: 20000 // 20s timeout
});

export default service;

所有對backend的request都不用axios,而是用這個service(假設放於src/services/request):

import service from '@services/request';

service.get('/login/')
    .then(response => {})
    .catch(error => {});

axios doc - Creating an instance

建立了以上的instance後,我們可以做更多只屬於這個instance的customization。

使用Interceptor

Interceptor可以確保所有request都經過同樣的處理,例如在處理API Token時有很大幫助。

處理API Token

一般Login後得到的API Token都會放在redux store裡儲起,以後每次request到server都會放在header來認證使用者的身分。

在沒有interceptor的情況下,所有endpoint都要自行取得token然後一併傳送:


import service from '@services/request';
import { store } from '@/ReduxWrapper';  // 傳入已初始化的store

export const getUserProfile = () => {
  const { token } = store.getState().user;  // 獲取store token
  const auth_token = 'Token ' + token;
  
  return service.get('/me/', {
    headers: {'Authorization': auth_token},  // Send with auth token
  });
};

每個endpoint都要做一次,實在太麻煩了,我們可以加入Request Interceptor來幫忙。

使用Request Interceptor

const service = axios.create(...);

// 只需在create instance後執行一次就可以了
service.interceptors.request.use(function (config) {
    const token = store.getState().user.token;
    if (token) {
      // 在headers裡加入token
      config.headers = {...config.headers, Authorization: `Token ${token}`}
    }
    return config;
  }, function (error) {
    return Promise.reject(error);
});

使用各種endpoint時不用再擔心token的事了。(當然最好處理一下如果沒有token的case)

import service from '@services/request';

service.get('/me/')
    .then(response => {})
    .catch(error => {});

使用Response Interceptor

同樣Response也可以使用interceptor。最常處理的case就是authentication過期,需要移除token和重新登入。

service.interceptors.response.use(function (response) {
  return response.data; // removing status code, etc
}, function(error) {

  if (error.response) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx

    // =========================================================
    // 這裡假設是401,你也可以和backend商量一個專門而設的code
    if (error.response.status === 401 && store.getState().user.token) {
      /*
        Token is provided in request AND Token invalid
        Need to clear token in store
       */
      store.dispatch(logout());
    }
    // =========================================================
        
    return Promise.reject(error.response);

  } else if (error.request) {
    // The request was made but no response was received
    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
    // http.ClientRequest in node.js
    return Promise.reject(error.request);
  } else {
    // Something happened in setting up the request that triggered an Error
    return Promise.reject(error.message);
  }

});

需要注意的是Error有三種:

  • error.response = Server返回的status code非2xx
  • 沒有error.response && 有error.request = Server沒有回應
  • 沒有error.response && 沒有error.request = 建立request時發生錯誤,可能request interceptor有error

詳細可看官方文檔(雖然也沒有解釋甚麼)

axios doc - Interceptors


下一篇:React Native教學 Part 2.5 - 選擇Navigation Library