Skip to content

Latest commit

 

History

History
290 lines (250 loc) · 8.15 KB

1. 前端架构.md

File metadata and controls

290 lines (250 loc) · 8.15 KB

项目架构设计

项目目录结构采用的是ducks类型

简言之就是将reducer、action types和action写到一个文件里,以应用的状态作为模块的划分依据

components/  (应用级别的通用组件)
containers/  
  feature1/
    components/  (功能拆分出的专用组件)
    feature1.js  (容器组件)
    index.js     (feature1对外暴露的接口)
redux/
  index.js (combineReducers)
  module1.js (reducer, action types, actions creators)
  module2.js (reducer, action types, actions creators)
index.js

设计Redux状态模块

  • 领域实体状态:例如商品、点评、订单、评论等
  • 页面状态:各页面UI的状态
  • 前端基础状态:登录态、全局异常错误状态

redux
    /middleware:    中间件的编写
    /modules:    模块
        /entities:    领域实体状态
        // 页面状态
        app.js
        detail.js
        home.js
        index.js
    store.js

这种模式其实是redux中的一种模式,叫做normalizr,范式化数据。

概括起来就一句话:减少层级,唯一id索引,用后端建表的方法构建我们的数据结构

具体设计思想可以参考normalizr

也可以直接看老师的文章。书里也有写Redux进阶系列2: 如何合理地设计State

网络请求层封装

编写工具类,对fetch进行第一次封装

const headers = new Headers({
    "Accept": "application/json",
    "Content-Type": "application/json"
})

function get(url) {
    return fetch(url, {
        method: 'GET',
        headers: headers,
    }).then(responce => {
        handleResponce(url, responce)
    }).catch(error => {
        console.error(`GET Request fail. url:${url}. message:${error}`)
        Promise.reject({
            error: {
                message: 'GET Request failed.'
            }
        })
    })
}

function post(url, data) {
    return fetch(url, {
        method: 'POST',
        headers: headers,
        data: data
    }).then(responce => {
        handleResponce(url, responce)
    }).catch(error => {
        console.error(`POST Request fail. url:${url}. message:${error}`)
        Promise.reject({
            error: {
                message: 'POST Request failed.'
            }
        })
    })
}

function handleResponce(url, responce) {
    let res = responce
    if (res.status == 200) {
        return res.json()
    } else {
        console.error(`Request fail. url:${url}`)
        Promise.reject({
            error: {
                message: 'Request failed due to server error'
            }
        })
    }
}

使用Redux进行开发时,遇到请求,我们往往需要很复杂的过程,并且这个过程是重复的。我们往往会把一个请求拆分成三个阶段,对应到三个Action Type中去,并且配合redux-thunk中间件,将一个异步action进行拆分,分别对应请求的三个阶段。如下所示:

import {get} from "../../utils/request"
import url from "../../utils/url"

// ...省略actionTypes
export const actions = {
  loadLikes: () => {
    return (dispatch, getState) => {
      // 发送请求 action
      dispatch(fetchLikesRequest());
      return get(url.getProductList(0, 10)).then(
        data => {
          // 请求成功发送成功action
          dispatch(fetchLikesSuccess(data));
        },
        error => {
          // 请求失败发送失败action
          dispatch(fetchLikesFailure(error))
        }
      )
    }
  }
}

// ...省略三个action和reducer
export default reducer;

因此要写很多像上面这样的模板代码,那能不能抽象出一种模式,简化我们的代码呢?

这个项目中使用了中间件,来进一步封装网络请求了

进一步封装

我的想法是通过一个这样的action去描述我们的网络请求

{
    // FETCH_DATA表明我们是需要经过中间件处理的action
    FETCH_DATA: {
        // 对应三种状态
        types: ['request', 'success', 'fail'],
        endpoint: url,
        // 领域实体
        schema 
    }
}
import { get } from '../../utils/request'

// 经过中间件处理的action所具有的标识
export const FETCH_DATA = 'FETCH DATA'

export default store => next => action => {
    // 判断给action是否需要该中间件处理
    const callAPI = action[FETCH_DATA]
    
    if (typeof callAPI === 'undefined') {
        return next(action)
    }
    console.log("第一步:callAPI中间件开始处理action", callAPI)
    // 派发的action必须有着三个属性
    const { endpoint, schema, types } = callAPI
    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('必须指定一个包含了三个action type的数组')
    }
    if (!types.every(type => typeof type === 'string')) {
        throw new Error('action type必须为字符串')
    }

    // 这个函数是为了处理带额外参数的action
    const actionWith = data => {
        const finalAction = {...action, ...data}
        // 删除action标识
        delete finalAction[FETCH_DATA]
        return finalAction
    }
    // 三种类型对应着三种请求
    const [requestType, succesType, failureType] = types
    // 异步请求中
    next(actionWith({ type: requestType }))

    return fetchData(endpoint, schema).then(
        response => {
            console.log('第五步,最终dispatch的数据', actionWith({
                type: succesType,
                response
            }))
            next(actionWith({
                type: succesType,
                response
            }))
        },
        // 请求失败
        error =>
            next(actionWith({
                type: failureType,
                error: error.message || '获取数据失败'
            }))
    )
}

// 执行网络请求
const fetchData = (endpoint, schema) => {
    console.log('第二步,开始发送网络请求,地址:', endpoint)
    return get(endpoint).then(data => {
        console.log('第三步,请求data成功', data)
        return normalizeData(data, schema)
    })
}

/*
范式化数据,将获取到的json按照normalizr的方式,来规范
schema类似于数据表
举个例子,也就是首页的例子。



范式化前
product.json = [
    {id: 'p-1', price: 19.9, ...},
    {id: 'p-2', price: 29.9, ...},
    {id: 'p-3', price: 39.9, ...},
    ...
]

范式化后
{
    product: {
        'p-1': {id: 'p-1', price: 19.9, ...},
        'p-2': {id: 'p-2', price: 29.9, ...},
        'p-3': {id: 'p-3', price: 39.9, ...},
        ...
    },
    ids: ['p-1', 'p-2'] // 保证有序性
}

在 reducer 中捕获entities.product的变化, 更新entities
在 Likes组件(发起action的组件)的 reducer中维护ids的变化
*/
const normalizeData = (data, schema) => {
    const { id, name } = schema
    let kvObj = {}
    let ids = []
    if (Array.isArray(data)) {
        data.forEach(item => {
            kvObj[item[id]] = item
            ids.push(item[id])
        })
    } else {
        kvObj[data[id]] = data
        ids.push(data[id])
    }
    var newOjb = {
        [name]: kvObj,
        ids
    }
    console.log('第四步:扁平化后data数据:', newOjb)
    return {
        [name]: kvObj,
        ids
    }
}

中间件中一些log代码是开发的时候方便开发,上线的时候要删除。

这个中间件还是代码比较长,实际上做了两件事情

  1. 封装了三种请求的样板代码,简化代码书写
  2. 范式化请求得到的数据,方便后序处理

第一步中间件的设计实际上是一个redux官方real-wordl的例子api.js

参考: 优雅地减少redux请求样板代码 Redux进阶(一)