-
Notifications
You must be signed in to change notification settings - Fork 12
Home
在介绍背景之前,先聊聊什么是一款企业级的复杂应用?
企业级的应用往往应对的是一个专业性非常高的场景,用户具备很强的专业素养,与之对应的,对于这些高阶玩家,需求是非常灵活与复杂。
具备专业性强,复杂度高,有特有业务模型等特点。
这类产品在 SPA 的形式下,通常受限于以下 3 个方面:
在单一路由下:
- 视图组件过多(view)
- 数据状态多且零散 (data)
- 业务逻辑分散 (logical)
针对以上的特点,有以下的诉求:
- 简单高效
拒绝模板代码 - 复杂度收敛
不随业务增长而升高,最好可以提供对复杂场景进行拆解的指导性心智。 - 可维护
怎么去保证建筑的延续性,在迭代中如何易于维护,如何易于扩展,如何很方便的调整,如何具有伸缩性。这些问题既不是某个语言层面去解决的,也不是某个模块采用什么设计模式可以达到的问题域。 - 多人协作
正视开发者的能力差异,每个开发的习惯和能力都不一样,但是需要抹平开发者差异对整体结果的影响,怎么让后人可以精准地做填空题,在合适的地方干合适的事。
领域驱动设计 领域驱动是为了提升架构的稳定性,在展开之前,先介绍一下概念:
- 领域即应用软件的问题域,问题域一旦确定,边界与最优解随之确定。
- 领域知识即业务流程与规则。
- 而领域模型则是对领域知识的严格组织和选择性的抽象。
在传统的前端开发流程中,前端和业务之间是通过视觉稿来联系的,一个需求对应一个视觉稿,视觉稿的变更即代表了需求的变更,随之带来大量的修改成本,像组件UI,业务逻辑和状态管理等,在这种模式下存在 2 个问题:
- 视觉稿与真实的业务诉求存在偏差,开发者没法理解真实的业务需求,很难触及用户的真实痛点
- 见山便是山的开发方式,很难沉淀出稳定的功能结构,容易产生历史包袱,从而复杂度不易收敛
而领域驱动设计的开发流程,会先由业务专家和开发团队根据领域知识与业务期望从问题域中抽象出稳定的领域模型, 因为领域模型是现实世界中业务的映射,所以天然与UI无关。 有了稳定的模型,再去搭建上层的业务逻辑,不易受到需求的冲击。 而这时,组件层只需实现视觉稿与和组装业务逻辑,可以有很高的灵活性。 并且因为数据层与逻辑层的稳定,多端适配和数据同步的问题也很容易得到解决。
状态分层 状态分层为了解决 3 类问题:
- view :视图组件不再通用,出现大量雷同的组件,复用性降低
- state :redux体系下随着迭代的进行,中间状态不断膨胀,导致复杂度无法收敛
- control: 业务逻辑分散,随着人员更替,模块定位不再清晰,模块边界逐渐模糊
通过区分视图状态和数据状态,由 page Module 负责页面级别的状态,承担组装与调度功能模块的职责,contianer Module 聚焦于功能模块内部的状态管理,而 domain Module 则单独处理业务数据的状态,通过分层的拆解,页面整体的复杂度不会再随迭代的进行而膨胀。
为什么不是 redux?
首先,Redux 是一个优秀的 State Manager 实现库,提供了可预测(predictable)的状态管理。
然而当我们尝试使用 redux 去构建一个应用时,往往需要以下的步骤:
第一步:每一个新的actionType,先要定义type常量,以便统一使用
constants.js
const ADD_TODO = 'ADD_TODO'
const TOGGLE_TODO = 'TOGGLE_TODO'
const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
第二步:每一个新的actionType,都需要手动声明一个对象
actionTypes.js
{ type: ADD_TODO, text: 'Go to swimming pool' }
{ type: TOGGLE_TODO, index: 1 }
{ type: SET_VISIBILITY_FILTER, filter: 'SHOW_ALL' }
第三步:每个action需要定义对应的creator方法
actionCreators.js
function addTodo(text) {
return { type: ADD_TODO, text }
}
function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
第四步:每一个新的reducer,都必须添加到switch case中,与之前定义的actionType对应。另外多个reducer需要用手动combineRecuers合并成rootReducer,最终生成store。
reducers.js
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
第五步:每一个新的saga,都需要定义actionType监听,以及effect函数。另外多个saga需要手动合成一个rootSaga,最终交给middleware。
sagas.js
import { delay } from 'redux-saga'
import { put, takeEvery, all } from 'redux-saga/effects'
import helloSaga from './hello'
// effect
function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// 监听
function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// 合并
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
第六步:每个redux应用,都有差不多类似的入口代码:载入middleware,载入reducer,载入router,创建store,渲染视图等。
entry.js
import React from 'react'
import { render } from 'react-dom'
import { combineReducers, createStore } from 'redux'
import { Provider } from 'react-redux'
import {
ConnectedRouter,
routerReducer,
routerMiddleware,
} from 'react-router-redux';
import createSagaMiddleware from 'redux-saga'
import App from './containers/App'
import { visibilityFilter, todos } from './reducers'
import rootSaga from './sagas'
const history = createHistory();
// 初始化saga middleware
const sagaMiddleware = createSagaMiddleware()
// 初始化react-router-redux middleware
const reactRouterMiddleware = routerMiddleware(history);
const middlewares = [sagaMiddleware, reactRouterMiddleware];
// 合并reducer
const reducer = combineReducers({ visibilityFilter, todos,
router: routerReducer })
// 创建stroe
const store = createStore(reducer,
// 载入saga中间件,或者其他
applyMiddleware(...middlewares))
// 执行rootSaga
sagaMiddleware.run(rootSaga)
// 渲染
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)
这些繁琐的模板代码,非常影响开发效率和可维护性。
同类业务的逻辑需要在 reducer, saga, action 几个文件之间来回切换。经常要手动写很多重复性的代码,比如actionType声明,saga对action的监听、saga的组合,reducer的组合等。
并且,还有其他的很多问题:
- redux 并没有提供应用架构的规范,往往一个项目有一个最佳实践,
- 状态逻辑都集中到了 store 中,杂糅一团,随着项目的需求增加与迭代,复杂度逐渐变高
- 在数据一致性的问题上往往需要人为的进行保证
- ...
dva 是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
dva 在提效方面做得非常棒,开发使用时的体感也很棒。
数据流也足够清爽:
用户交互行为或者浏览器行为(如路由跳转等)通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
dva 解决了诉求中的【简单高效】,并且做的相当不错,但是,仅仅着手于提效是无法覆盖复杂应用的痛点的。
需要框架层面来正视【复杂度收敛】,【可维护】和 【多人协作】这些大型应用中的致命伤。
在 dva 中你可以 dispatch action 到任意的 model 中,这种灵活性在大型应用中是一把双刃剑,往往导致模块之间的边界越来越模糊,耦合性逐渐上升,长期看来,是弊大于利的。
并且仍然将所有状态聚合到 store 中,随着业务的不断迭代,这棵树可能过于丰满,承载了不该承受之重~,其实侧面反映了复杂度在随着业务的迭代而逐渐升高。碰伤,脆弱这些特点往往也会在这个时候冒出来。
redux-with-domain 的数据流虽然也是基于 redux/saga,但却有些不一样:
当用户或者外部行为触发 action 后,会被 dispatch 到 page 层与 container 层的 effects 中,container 层同时会去调度底层的 domain 层对领域模型中数据进行更新。
其中 page 控制着路由页面下的 UI 与数据状态,container 控制着模块内部的 UI 与数据状态,而 domain 管理着领域模型中的数据。
通过垂直方向的 3 层划分,一个 action 产生的各种中间状态会合理地收敛到各个层中,复杂度也就拆解到了各个模块中。经过 Effect 后,Reducer 会去更新所属模块的 store 信息。
注意,这里的 store 根据不同的模块进行了拆解。每个模块独立维护着自己的 store 信息。
与 dva 不同的是,redux-with-domain 对 dispatch 的方向进行了限制。
约束如下:
- 同层之间不可直接调用
- 下层不可直接调用上层
通过这样的约束,我们可以保证模块之间的边界清晰,耦合性不会随着迭代而上升,也降低了多人协作中,个人能力对模块的影响。 区别: - 拆解了 store
- 增加了垂直方向的分层,拆解复杂度,将复杂度分散到各个层级中
- 提供了 module 之间的约束
数据流的区别只是一个剪影,redux-with-domain 在提效的基础上,正视了其他框架忽略的一些问题,他的定位旨在避免架构的腐败以及提供拆解复杂应用的心智与思路。如果你正为项目中的复杂度而焦头烂额,不妨试试 redux-with-domain,一套经过大数据平台企业级项目沉淀出的方法论。