项目目录结构采用的是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
- 领域实体状态:例如商品、点评、订单、评论等
- 页面状态:各页面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代码是开发的时候方便开发,上线的时候要删除。
这个中间件还是代码比较长,实际上做了两件事情
- 封装了三种请求的样板代码,简化代码书写
- 范式化请求得到的数据,方便后序处理
第一步中间件的设计实际上是一个redux官方real-wordl的例子api.js