一,项目简介。
装饰器包装原生 react
动画插件,弹框队列原理等。
二,技术选型。
git
:创库管理代码和版本控制,高效的多人合作。webpack
:静态模块打包器。react
是强大的构建用户界面的 javascript 库。redux
:应用程序的状态容器,提供可预测的状态管理。saga
:异步处理中间件。
这一套工程化的软件,可实现模块化,组件化,自动化,规范化。
- 规范化参考凹凸实验室打造的前端代码规范.
- 自动化包括图标合并,自动化构建,自动化部署等,可参考这篇文章在京东,我们这样配置 webpack。
三,Hook 简介,函数式编程优势。
-
class
组件开始很简单,但随着业务的复杂,class
组件也会变得越来越复杂,且难以理解。很多状态逻辑糅合在一起,后序维护变得艰难且容易出错。 -
Hook 使得函数组件也能使用
react state
及生命周期等特性。 -
逻辑状态可抽离出函数,并且随处复用。
-
更多函数式编程优势参考阮一峰的博客函数式编程初探
四,自动化的 store
。
1,reducers
和 saga
可以写在同一个文件,实现一个模块一个文件,方便管理。那么如何区分 reducers
和 saga
呢?
/**
* 格式化
* @param {Object} actions
* @returns {Object} 由def,reducers和saga 组成的对象
*/
export function format(actions) {
const reducers = {}
const sagas = {}
let def = {}
for (let type in actions) {
const action = actions[type]
if (type === 'default') {
// 添加默认参数
def = action
} else {
// 添加action的type
action.type = type
// 判断是否generator函数
if (isGeneratorFunction(action)) {
sagas[type] = action
} else {
reducers[type] = action
}
}
}
}
/**
* 是否是generator函数
* @param {*} obj
* @returns {Boolean} 是否是generator函数
*/
export function isGeneratorFunction({ constructor }) {
if (!constructor) {
return fasle
}
const { name, displayName } = constructor
if (name === 'GeneratorFunction' || displayName === 'GeneratorFunction') {
return true
}
const { prototype } = constructor
return (
typeof prototype.threw === 'function' &&
typeof prototype.next === 'function'
)
}
2. 多个 reducer 通过 combineRducers 组合起来,方便关联到 store 上。
/**
* 使用combineReducers把所有的reducer函数联合起来
*/
import { combineReducers } from 'redux'
// 导出来的所有reduer和saga函数
import actions from '../actions'
let reducersToCombine = []
for (let stateName in actions) {
const { reducers, def } = actions[stateName]
if (reducers) {
reducersToCombine[stateName] = (state = def, action) => {
const { type } = action
const { [type]: reducer } = reducers
if (reducer) {
return reducer(state, action)
} else {
return state
}
}
}
}
export default combineReducers(reducersToCombine)
3,不用手动监听 saga 函数。
/**
* 自动化监听多个saga函数
*/
import { call, takeEvery, all } from 'redux-saga/effects'
// 导出来的所有reducer和saga函数
import actions from '../actions'
export default function* () {
const effects = []
for (let stateName in actions) {
const { sagas } = actions[stateName]
for (let type in sagas) {
effects.push(
yield takeEvery(type, function* (action) {
const { key } = action
const { [key]: saga } = sagas
try {
saga ? yield call(saga, action) : undefined
} catch (e) {}
})
)
}
}
yield all(effects)
}
五,数据逻辑与 UI 的分离
把公用的逻辑提取到 store
中,供全局使用
export function* WHILE() {
let lock = 1
while (lock) {
yield new Promise((resolve) => {
setTimeout(resolve, 1000)
})
lock = 0
}
}
倒计时事件集中管理
import { put, call, select, all, spawn, delay } from 'redux-saga/effects'
import moment from 'moment'
export default {
counts: [], //事件对象
isStart: false, //事件是否循环
}
/**
* reducer
* @param {Object} state
* @param {Object} param1
* @returns
*/
export function SET_COUNT_DOWN(state = {}, { payload }) {
return { ...state, ...payload }
}
/**
* 添加事件函数
* @param {Object} param0 添加的事件,包括执行函数handler,和剩余时间count
*/
export function* ADD_COUNT_DOWN({ payload = {} }) {
if (!payload.count || !payload.handler) return
const { counts, isStart } = yield select(({ cd }) => cd)
const newCounts = [...counts]
let index = -1
counts.some((item, i) => {
if (item.handler === payload.handler) {
index = i
return true
}
return false
})
if (!~index) {
newCounts.push(payload)
} else {
newCounts.splice(index, 1, payload)
}
yield put({
type: SET_COUNT_DOWN.type,
payload: { counts: newCounts, startTime: new Date() },
})
if (!isStart) {
yield spawn(function* () {
yield put({
type: SET_COUNT_DOWN.type,
payload: { isStart: true },
})
yield call(WHILE_COUNT_DOWN)
})
}
}
/**
* 循环函数
*/
export function* WHILE_COUNT_DOWN() {
while (true) {
const { counts, startTime } = yield select(({ cd }) => cd)
let nowTime = new Date()
const changeTime = Math.floor((nowTime - startTime) / 1000) * 1000
//把变化的值加入数组
const countsArr = []
counts.forEach((item) => {
const n = moment(item.count).format('x') - changeTime
if (n >= 0) {
countsArr.push({ handler: item.handler, count: item.count, change: n })
}
})
const len = countsArr.length
if (len === 0) {
yield put({
type: SET_COUNT_DOWN.type,
payload: { isWhile: false },
})
break
}
// 重新设置事件数组
yield put({
type: SET_COUNT_DOWN.type,
payload: {
counts: countsArr,
},
})
//同时执行所有的函数
yield all(
countsArr.map((item) => {
return call(item.handler, item.change)
})
)
//等待1秒
const goTime = new Date() - nowTime
yield delay(1000 - (goTime % 1000))
}
}
六,使用装饰器包装原生 react
动画插件
react
官方推出的动画插件 react-transition-group
,详细使用方法参考
我们把动画调用函数抽象到装饰器函数里。js
代码如下
import React from 'react'
import { CSSTransition } from 'react-transition-group'
const Transition = ({ type, children, diraction, popupResolves }) => {
const item = popupResolves[type] || {}
const isShow = item.isShow
const resolve = item.resolve
return (
<CSSTransition // 动画插件
in={isShow} // 是否显示
classNames={`popup--${diraction}`} // 样式名称
mountOnEnter // 延迟挂载,才会第一次出动画
unmountOnExit //退出后卸载
appear // 第一次挂载立即出动画
onExited={() => {
// 卸载完成后触发
resolve && resolve() //下一节会讲到作用
}}>
<div className="popup__box">{children(item)}</div>
</CSSTransition>
)
}
// opts为第一次调用时传入的type参数
export const add = function (opts) {
// WrappedComponent为装饰的函数
return function (WrappedComponent) {
// props为react调用时传入的参数
return function ({ ...props }) {
// Transition为上面写的动画插件
// 为了传递动画插件的参数给装饰的函数,多加了一层函数,data为动画插件的item参数对象
return (
<Transition {...opts}>
{(data) => <WrappedComponent {...props} {...data} />}
</Transition>
)
}
}
}
调用装饰器函数
//用@语法糖调用装饰器函数
// type为弹框的类型,diraction为弹窗出现的方向
@add({ type: 'popup', diraction: 'bottom' })
//被装饰的class类函数
export default class extends Component {
render() {
return null
}
}
//或者直接调用装饰器函数
export default add({ type: 'popup', diraction: 'bottom' })(function () {})
七,弹框队列原理
1. 先用 async
函数发送一个 action
,示例代码
import React from 'react'
import { INFO_SHOW } from '../actions'
import store from '../store'
export const showPopup = async ({ ...props }) => {
await store.dispatch({
// 调用显示saga
type: INFO_SHOW.type,
// 传递数据,包括type和data
payload: { ...props },
})
}
// 调用示例
// type为弹框的类型,data为传递给组件的数据
showPopup({ type: 'popup', data })
2. saga
文件接收到这个 action
后,运行 INFO_SHOW
函数
export function* INFO_SHOW({ payload }) {
// 取出传递过来的type
const type = payload.type
let popupResolves = {}
yield call(
() =>
new Promise((resolve) => {
// 传递resolve函数,利用promise的状态未结束来保持程序。直到第七节动画卸载后onExited回调里执行resolve
const popupResolves = {
[type]: {
resolve,
...payload,
isShow: true,
},
}
// 发送一个action储存数据,并更新store,引起view的刷新,弹出弹框。
store.dispatch({
type: SET_DATA.type,
payload: {
popupResolves,
},
})
})
)
}