• 【大众点评】—— 前端架构设计


    前言:正在学习react大众点评项目课程,学习react、redux、react-router构建项目。


    一、前端架构是什么

    前端架构的特殊性

    前端不是一个独立的子系统,又横跨整个系统

    分散性:前端工程化

    页面的抽象、解耦、组合

    可控:脚手架、开发规范等

    高效:框架、组件库、Mock平台,构建部署工具等

    抽象

    页面UI抽象:组件

    通用逻辑抽象:领域实体、网络请求、异常处理等

    二、案例分析

    功能路径

    展示:首页->详情页

    搜索:搜索页->结果页

    购买:登录->下单->我的订单->注销

    三、前端架构之工程化准备:技术选型和项目脚手架

    技术选型考虑的三要素

    业务满足程度

    技术栈的成熟度(使用人数、周边生态、仓库维护等)

    团队的熟悉度

    技术选型

    UI层:React

    路由:React Router

    状态管理:Redux

    脚手架

    Create React App

    npx create-react-app dianping-react
    

    、前端架构之工程化准备:基本规范

    基本规范

    目录结构

    构建体系

    Mock数据

    //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模块

    前端基础状态:登录态、全局异常信息

     

    //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
    //各子reducer.js
    const reducer = (state = {}, action) => {
        return state;
    }
    
    export default reducer;  

      

    六、前端架构之抽象2:网络请求层封装(redux-thunk) (redux中间件)

    抽象2:网络请求层

    原生的fetch API封装get、post方法

    //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基础封装

    //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层比较臃肿、繁琐)

    //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;
    //redux->modules->entities->products.js
    const reducer = (state = {}, action) => {
        return state;
    }
    
    export default reducer;  

    使用redux中间件封装(简化模板式内容的编写)

    //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;
    //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;
    
    //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
        }
    } 
    //redux->store.js
    import { createStore, applyMiddleware } from "redux"
    //处理异步请求(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会在一定时间内消失

    //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;
    
    //component->ErrorToast->style.css
    .errorToast {
        top: 0px;
        left: 0px;
         100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.6);
        z-index: 10000001;
        position: fixed;
        display: flex;
        justify-content:center;
        align-items: center;
    }
    
    .errorToast__text {
        max- 300px;
        max-height: 300px;
        padding: 15px;
        border-radius: 10px;
        color: #fff;
        font-size: 14px;
        background-color: #000;
    } 

    错误状态  

    //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
    }
    
    //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);
    
    //containers->App->style.css
    .App {
      text-align: center;
    }
    
    .App-logo {
      height: 40vmin;
      pointer-events: none;
    }
    
    @media (prefers-reduced-motion: no-preference) {
      .App-logo {
        animation: App-logo-spin infinite 20s linear;
      }
    }
    
    .App-header {
      background-color: #282c34;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: calc(10px + 2vmin);
      color: white;
    }
    
    .App-link {
      color: #61dafb;
    }
    
    @keyframes App-logo-spin {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }
    

    注:项目来自慕课网  

    人与人的差距是:每天24小时除了工作和睡觉的8小时,剩下的8小时,你以为别人都和你一样发呆或者刷视频
  • 相关阅读:
    js---选择排序
    js----冒泡排序
    js---快速排序
    js---去重方法(二)
    js---去重方法(一)
    js--进度条
    随机生成6位数验证码
    倒计时
    别踩白块
    贪吃蛇小游戏
  • 原文地址:https://www.cnblogs.com/ljq66/p/14384645.html
Copyright © 2020-2023  润新知