在開始開發任何專案前,最好先決定如何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檔案
- 最好一開始便Setup好ESLint,確保Project code是consistent。(可參考建置eslint開發環境)
src/
.
├── AppStartUpWrapper.js
├── ReduxWrapper.js
├── common/
├── components/
├── constants/
├── containers/
├── navigation/
├── package.json
├── redux/
├── services/
└── utils/
App Hierarchy
App.js
:rootReduxWrapper.js
:加入Redux storeAppStartUpWrapper.js
:加入notification listener / deeplink listener等等navigation/
:加入Navigation- 各個containers/screens
- 各個components
- 各個containers/screens
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 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
詳細可看官方文檔(雖然也沒有解釋甚麼)。