前端架构
前言: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小时,你以为别人都和你一样发呆或者刷视频