前端架構


前言: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);

注:項目來自慕課網  

人與人的差距是:每天24小時除了工作和睡覺的8小時,剩下的8小時,你以為別人都和你一樣發呆或者刷視頻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM