前言:react、redux、react-router構建項目。
一、前端架構是什么
前端架構的特殊性
前端不是一個獨立的子系統,又橫跨整個系統
分散性:前端工程化
頁面的抽象、解耦、組合
可控:腳手架、開發規范等
高效:框架、組件庫、Mock平台,構建部署工具等
抽象
頁面UI抽象:組件
通用邏輯抽象:領域實體、網絡請求、異常處理等
二、案例分析
功能路徑
展示:首頁->詳情頁
搜索:搜索頁->結果頁
購買:登錄->下單->我的訂單->注銷
三、前端架構之工程化准備:技術選型和項目腳手架
技術選型考慮的三要素
業務滿足程度
技術棧的成熟度(使用人數、周邊生態、倉庫維護等)
團隊的熟悉度
技術選型
UI層:React
路由:React Router
狀態管理:Redux
腳手架
Create React App
1
|
npx create-react-app dianping-react
|
四、前端架構之工程化准備:基本規范
基本規范
目錄結構
構建體系
Mock數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
//likes.json
[
{
"id"
:
"p-1"
,
"shopIds"
: [
"s-1"
,
"s-1"
,
"s-1"
],
"shop"
:
"院落創意菜"
,
"tag"
:
"免預約"
,
"picture"
:
"https://p0.meituan.net/deal/e6864ed9ce87966af11d922d5ef7350532676.jpg.webp@180w_180h_1e_1c_1l_80q|watermark=0"
,
"product"
:
"「3店通用」百香果(冷飲)1扎"
,
"currentPrice"
: 19.9,
"oldPrice"
: 48,
"saleDesc"
:
"已售6034"
},
{
"id"
:
"p-2"
,
"shopIds"
: [
"s-2"
],
"shop"
:
"正一味"
,
"tag"
:
"免預約"
,
"picture"
:
"https://p0.meituan.net/deal/4d32b2d9704fda15aeb5b4dc1d4852e2328759.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0"
,
"product"
:
"[5店通用] 肥牛石鍋拌飯+雞蛋羹1份"
,
"currentPrice"
: 29,
"oldPrice"
: 41,
"saleDesc"
:
"已售15500"
},
{
"id"
:
"p-3"
,
"shopIds"
: [
"s-3"
,
"s-3"
],
"shop"
:
"Salud凍酸奶"
,
"tag"
:
"免預約"
,
"picture"
:
"https://p0.meituan.net/deal/b7935e03809c771e42dfa20784ca6e5228827.jpg.webp@180w_180h_1e_1c_1l_80q|watermark=0"
,
"product"
:
"[2店通用] 凍酸奶(小杯)1杯"
,
"currentPrice"
: 20,
"oldPrice"
: 25,
"saleDesc"
:
"已售88719"
},
{
"id"
:
"p-4"
,
"shopIds"
: [
"s-4"
],
"shop"
:
"吉野家"
,
"tag"
:
"免預約"
,
"picture"
:
"https://p0.meituan.net/deal/63a28065fa6f3a7e88271d474e1a721d32912.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0"
,
"product"
:
"吉汁燒魚+中杯汽水/紫菜蛋花湯1份"
,
"currentPrice"
: 14,
"oldPrice"
: 23.5,
"saleDesc"
:
"已售53548"
},
{
"id"
:
"p-5"
,
"shopIds"
: [
"s-5"
],
"shop"
:
"醉面 一碗醉香的肉醬面"
,
"tag"
:
"免預約"
,
"picture"
:
"https://p1.meituan.net/deal/a5d9800b5879d596100bfa40ca631396114262.jpg.webp@180w_180h_1e_1c_1l_80q|watermark=0"
,
"product"
:
"單人套餐"
,
"currentPrice"
: 17.5,
"oldPrice"
: 20,
"saleDesc"
:
"已售23976"
}
]
|
五、前端架構之抽象1:狀態模塊定義
抽象1:狀態模塊定義
商品、店鋪、訂單、評論 —— 領域實體模塊(entities)
各頁面UI狀態 —— UI模塊
前端基礎狀態:登錄態、全局異常信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//redux->modules->index.js
import
{ combineReducer } from
"redux"
;
import
entities from
"./entities"
;
import
home from
"./home"
;
import
detail from
"./detail"
;
import
app from
"./app"
;
//合並成根reducer
const rootReducer = combineReducer({
entities,
home,
detail,
app
})
|
export default rootReducer
1
2
3
4
5
6
|
//各子reducer.js
const reducer = (state = {}, action) => {
return
state;
}
export
default
reducer;
|
六、前端架構之抽象2:網絡請求層封裝(redux-thunk) (redux中間件)
抽象2:網絡請求層
原生的fetch API封裝get、post方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
//utils->request.js
//設置響應的header,抽象成一個常量
const headers =
new
Headers({
"Accept"
:
"application/json"
,
"Content-Type"
:
"application/json"
})
//get方法處理get請求
function
get(url) {
return
fetch(url, {
method:
"GET"
,
headers: headers
}).then(response => {
//fetch返回的是一個promise對象,.then方法中可以解析出fetch API返回的數據
handleResponse(url, response);
//response的通用處理:區分符合業務正常預期的response和異常的response
}).
catch
(err => {
//catch中捕獲異常,對異常的處理和handleResponse基本保持一致
console.log(`Request failed. url = ${url}. Message = ${err}`)
return
Promise.reject({error: {
message:
"Request failed."
//不能說“服務端信息異常”了,因為還沒到服務端
}})
})
}
//post方法處理post請求, 多一個data參數
function
post(url, data) {
return
fetch(url, {
method:
"POST"
,
headers: headers,
body: data
}).then(response => {
handleResponse(url, response);
}).
catch
(err => {
console.log(`Request failed. url = ${url}. Message = ${err}`)
return
Promise.reject({error: {
message:
"Request failed."
}})
})
}
//基本的對response處理的函數(重在思路,項目都大致相同)
function
handleResponse(url, response){
if
(response.status === 200){
//符合業務預期的正常的response
return
response.json()
}
else
{
console.log(`Request failed. url = ${url}`)
//輸入錯誤信息
return
Promise.reject({error: {
//為了response可以繼續被調用下去,即使在異常的情況下也要返回一個promise結構,生成一個reject狀態的promise
message:
"Request failed due to server error"
}})
}
}
export
{get, post}
|
項目中使用到的url基礎封裝
1
2
3
4
5
6
7
8
9
10
|
//utils->url.js
//創建一個對象,對象中每一個屬性是一個方法
export
default
{
//獲取產品列表
getProductList: (path, rowIndex, pageSize) => `/mock/products/${path}.json?rowIndex=${rowIndex}&pageSize=${pageSize}`,
//獲取產品詳情
getProductDetail: (id) => `/mock/product_detail/${id}.json`,
//獲取商品信息
getShopById: (id) => `/mock/shops/${id}.json`
}
|
常規使用方式 (redux層比較臃腫、繁瑣)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
//redux->modules->home.js(首頁)
import
{get} from
"../../utils/request"
import
url from
"../../utils/url"
//action types
export
const types = {
//獲取猜你喜歡請求: 值的第一部分以模塊名(HOME)作為命名空間,防止action type在不同的模塊中發生沖突, 第二部分為type名
FETCH_LIKES_REQUEST:
"HOME/FETCH_LIKES_REQUEST"
,
//獲取猜你喜歡請求成功
FETCH_LIKES_SUCCESS:
"HOME/FETCH_LIKES_SUCCESS"
,
//獲取猜你喜歡請求失敗
FETCH_LIKES_FAILURE:
"HOME/FETCH_LIKES_FAILURE"
}
//action: 所有的action放在一個actions對象下
export
const actions = {
//獲取猜你喜歡數據的action
loadLikes: () => {
return
(dispatch, getState) => {
//返回一個函數,接收dispatch 和 getState兩個參數
dispatch(fetchLikesRequest());
//第一步:dispatch一個請求開始的action type
return
get(url.getProductList(0, 10)).then(
//通過get方法進行網絡請求
data => {
//請求成功時,dispatch出去data
dispatch(fetchLikesSuccess(data))
//其實在開發中還需要dispatcn一個module->product中提供的action,由product的reducer中處理,才能將數據保存如product中
//dispatch(action)
},
error => {
//請求失敗時,dispatch出去error
dispatch(fetchLikesFailure(error))
}
)
}
}
}
//action creator
//不被外部組件調用的,為action type所創建的action creator(所以不把它定義在actions內部,而定義在外部,且不把它導出export)
const fetchLikesRequest = () => ({
type: types.FETCH_LIKES_REQUEST
})
const fetchLikesSuccess = (data) => ({
type: types.FETCH_LIKES_SUCCESS,
data
})
const fetchLikesFailure = (error) => ({
type: types.FETCH_LIKES_FAILURE,
error
})
//reducer:根據action type處理不同邏輯
const reducer = (state = {}, action) => {
switch
(action.type) {
case
types.FETCH_LIKES_REQUEST:
//獲取請求
//todo
case
types.FETCH_LIKES_SUCCESS:
//請求成功
//todo
case
types.FETCH_LIKES_FAILURE:
//請求失敗
//todo
default
:
return
state;
}
return
state;
}
export
default
reducer;
|
1
2
3
4
5
6
|
//redux->modules->entities->products.js
const reducer = (state = {}, action) => {
return
state;
}
export
default
reducer;
|
使用redux中間件封裝(簡化模板式內容的編寫)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
//redux->modules->home.js(首頁)
import
{get} from
"../../utils/request"
import
url from
"../../utils/url"
import
{ FETCH_DATA } from
"../middleware/api"
import
{ schema } from
"./entities/products"
export
const types = {
//獲取猜你喜歡請求
FETCH_LIKES_REQUEST:
"HOME/FETCH_LIKES_REQUEST"
,
//獲取猜你喜歡請求成功
FETCH_LIKES_SUCCESS:
"HOME/FETCH_LIKES_SUCCESS"
,
//獲取猜你喜歡請求失敗
FETCH_LIKES_FAILURE:
"HOME/FETCH_LIKES_FAILURE"
}
//簡化模板式內容需要的特殊結構 —— 代表使用redux-thunk進行網絡請求的過程
//(
// FETCH_DATA:{ //表明action是用來獲取數據的
// types:['request', 'success", 'fail'],
// endpoint: url, //描述請求對應的url
// //schema在數據庫中代表表的結構,這里代表領域實體的結構
// schema: { //需要的原因:假設獲取的是商品數據,當中間件獲取到商品數據后還需要對數組格式的數據作進一步“扁平化”處理,轉譯成Key:Value形式
// id: "product_id", //領域數據中的哪一個屬性可以代表這個領域實體的id值
// name: 'products' //正在處理的是哪一個領域實體(相當於中間件在處理數據庫表時哪一張表的名字)
// }
// }
//}
export
const actions = {
//簡化版的action
loadLikes: () => {
return
(dispatch, getState) => {
const endpoint = url.getProductList(0, 10)
return
dispatch(fetchLikes(endpoint))
//dispatch特殊的action,發送獲取請求的(中間件)處理
}
}
}
//特殊的action, 用中間件可以處理的結構
const fetchLikes = (endpoint) => ({
[FETCH_DATA]: {
types: [
types.FETCH_LIKES_REQUEST,
types.FETCH_LIKES_SUCCESS,
types.FETCH_LIKES_FAILURE
],
endpoint,
schema
},
//params 如果有額外的參數params, 當獲取請求成功(已經發送FETCH_LIKES_SUCCESS)后,
// 希望params可以被后面的action接收到, action需要做【增強處理】
})
const reducer = (state = {}, action) => {
switch
(action.type) {
case
types.FETCH_LIKES_REQUEST:
//todo
case
types.FETCH_LIKES_SUCCESS:
//todo
case
types.FETCH_LIKES_FAILURE:
//todo
default
:
return
state;
}
return
state;
}
export
default
reducer;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//redux->modules->entities->products.js
//schema在數據庫中代表的是表的結構,這里代表領域實體的結構
export
const schema = {
name:
'products'
,
//領域實體的名字,products掛載到redux的store的屬性的名稱,保持和文件名相同
id:
'id'
//標識了領域實體的哪一個字段是用來作為id解鎖數據的
}
const reducer = (state = {}, action) => {
if
(action.response && action.response.products){
//(如果)獲取到的數據是一個對象{[name]: KvObj, ids},name是領域實體名字,這里是products
//將獲取到的數據保存【合並】到當前的products【領域數據狀態】中,並且數據是通過中間件扁平化的key value形式的數據
return
{...state, ...action.response.products}
}
return
state;
}
export
default
reducer;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
//redux->middleware->api.js
import
{ get } from
"../../utils/request"
//對get請求進行中間件的封裝
// update(修改)、delete(刪除)同理,只是在調用api請求成功的數據處理中有一些區別
// 大眾點評項目只是純前端項目,不能直接進行修改和刪除的api處理,這里不作展示
//經過中間件處理的action所具有的標識
export
const FETCH_DATA =
'FETCH_DATA'
//中間件的函數式聲明
export
default
store => next => action => {
const callAPI = action[FETCH_DATA]
//解析有FETCH_DATA字段的action就是是需要中間件處理的action
//類型判斷:如果是undefined,表明action不是一個用來獲取數據的action,而是一個其它類型的action, 中間件放過對這個action的處理
if
(
typeof
callAPI ===
'undefined'
){
return
next(action)
//直接交由后面的中間件進行處理
}
const { endpoint, schema, types } = callAPI
//交由這個中間件進行處理的action的三個屬性,必須符合一定的規范
if
(
typeof
endpoint !==
'string'
){
throw
new
Error(
'endpoint必須為字符串類型的URL'
)
}
if
(!schema){
throw
new
Error(
'必須指定領域實體的schema'
)
}
if
(!Array.isArray(types) && types.length !== 3){
throw
new
Error(
'需要指定一個包含了3個action type的數組'
)
}
if
(!types.every(type =>
typeof
type ===
'string'
)){
throw
new
Error(
'action type必須為字符串類型'
)
}
//【增強版的action】——保證額外的參數data會被繼續傳遞下去
const actionWith = data => {
const finalAction = {...action, ...data}
//在原有的action基礎上,擴展了data
delete
finalAction[FETCH_DATA]
//將原action的FETCH_DATA層級的屬性刪除掉
//因為經過中間件處理后,再往后面的action傳遞的時候就已經不需要FETCH_DATA這一層級的屬性了
return
finalAction
}
const [requestType, successType, failureType] = types
next(actionWith({type: requestType}))
//調用next,代表有一個請求要發送
return
fetchData(endpoint, schema).then(
//【真正的數據請求】—— 調用定義好的fetchData方法,返回的是promise結構
response => next(actionWith({
//拿到經過處理的response, 調用next發送響應成功的action
type: successType,
response
//獲取到的數據response —— 是一個對象 {[name]: KvObj, ids},name是領域實體名字如products
}))
error => next(actionWith({
type: failureType,
error: error.message ||
'獲取數據失敗'
}))
)
}
//【執行網絡請求】
const fetchData = (endpoint, schema) => {
return
get(endpoint).then(data => {
//對get請求進行中間件的封裝, endpoint對應請求的url, 解析獲取到的數據data
return
normalizeData(data, schema)
//調用normalizeData方法,對獲取到的data數據,根據schema進行扁平化處理
})
}
//根據schema, 將獲取的數據【扁平化處理】
const normalizeData = (data, schema) => {
const {id, name} = schema
let
kvObj = {}
//領域數據的扁平化結構 —— 定義kvObj作為最后存儲扁平化數據【對象】的變量
let
ids = []
//領域數據的有序性 —— 定義ids【數組結構】存儲數組當中獲取的每一項的id
if
(Array.isArray(data)){
//如果返回到的data是一個數組
data.forEach(item => {
kvObj[item[id]] = item
ids.push(item[id])
})
}
else
{
//如果返回到的data是一個對象
kvObj[data[id]] = data
ids.push(data[id])
}
return
{
[name]: kvObj,
//不同領域實體的名字,如products
ids
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//redux->store.js
import
{ createStore, applyMiddleware } from
"redux"
<br>
//處理異步請求(action)的中間件
import
thunk from
"redux-thunk"
import
api from
"./middleware/api"
import
rootReducer from
"./modules"
let
store;
if
(
process.env.NODE_ENV !==
"production"
&&
window.__REDUX_DEVTOOLS_EXTENSION__
) {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk, api)));
}
else
{
store = createStore(rootReducer, applyMiddleware(thunk, api));
//將中間件api添加到redux的store中
}
export
default
store
|
七、前端架構之抽象3:通用錯誤處理
抽象3:通用錯誤處理
錯誤信息組件 —— ErrorToast會在一定時間內消失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
//component->ErrorToast->index.js
import
React, { Component } from
'react'
import
"./style.css"
;
class
ErrorToast
extends
Component {
render() {
const { msg } =
this
.props
return
(
<div className=
"errorToast"
>
<div className=
"errorToast_text"
>
{msg}
</div>
</div>
);
}
componentDidMount() {
this
.timer = setTimeout(() => {
this
.props.clearError();
}, 3000)
}
componentWillUnmount() {
if
(
this
.timer) {
clearTimeout(
this
.timer)
}
}
}
export
default
ErrorToast;
|
錯誤狀態
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
//redux->modlues->app.js
/**
* 前端的通用基礎狀態
*/
const initialState = {
error:
null
}
export
const types = {
CLEAR_ERROR:
"APP/CLEAR_ERROR"
}
//action creators
export
const actions = {
clearError: () => ({
type: types.CLEAR_ERROR
})
}
const reducer = (state = initialState, action) => {
const { type, error } = action
if
(type === types.CLEAR_ERROR) {
return
{...state, error:
null
}
}
else
if
(error){
return
{...state, error: error}
}
return
state;
}
export
default
reducer;
//selectors
export
const getError = (state) => {
return
state.app.error
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
//containers->App->index.js
import
React, { Component } from
'react'
;
import
{ bindActionCreators } from
'redux'
;
import
{ connect } from
'react-redux'
;
import
ErrorToast from
"../../components/ErrorToast"
;
import
{ actions as appActions, getError } from
'../../redux/modules/app'
import
'./style.css'
;
class
App
extends
Component {
render() {
const {error, appActions: {clearError}} =
this
.props;
return
(
<div className=
"App"
>
{error ? <ErrorToast msg={error} clearError={clearError}/> :
null
}
</div>
)
}
}
const mapStateToProps = (state, props) => {
return
{
error: getError(state)
}
}
const mapDispatchToProps = (dispatch) => {
return
{
appActions: bindActionCreators(appActions, dispatch)
}
}
export
default
connect(mapStateToProps, mapDispatchToProps)(App);
|
注:項目來自慕課網