前端架构
前言: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.jsimport { combineReducer } from "redux";import entities from "./entities";  import home from "./home";import detail from "./detail";import app from "./app";//合并成根reducerconst rootReducer = combineReducer({    entities,    home,    detail,    app}) | 
export default rootReducer
| 
 1 
2 
3 
4 
5 
6 
 | 
//各子reducer.jsconst 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 typesexport 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.jsconst 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.jsimport { 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.jsimport { 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.jsimport 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 creatorsexport 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;//selectorsexport 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.jsimport 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小时,你以为别人都和你一样发呆或者刷视频

                
            
        
浙公网安备 33010602011771号