前端架构


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