TL;DR
主导着当今世界前端技术发展的两家公司依然是 Google, Facebook;
目前前端的”三大框架“:Google 「Angular」 ,Facebook 「React」,而「Vue」也不完全算是是“自立门户”,也是吸收并借鉴了前者的优秀思想并进一步融合。
而 “三大框架” ,哪怕自身只是一个 View Library, 无一列外也都离不开 MV 的设计模式,在此设计模式下开发,就绕不开至关重要的的概念:状态管理,笔者也一如既往固执己见地认为,整个状态管理的过程其实就是贯彻 MV 对 Model、View 、 Controller/View Model 三者分层管理具体实施的过程,从而尽可能地保证 View 层的单薄,保证 Controller 的逻辑“C位”, 保证 Model 的数据中心,自始至终将应用的可维护性控制在一个较好的水平。
今天借 「State Management」 题发挥,基于团队目前项目所采用的技术框架 Vue, 来聊一聊状态管理。
Intro
Vue 框架也是深受 MVVM
(model, view, view-model) 优秀的编程模型规范的启发,
同时结合了组件化的先进理念,自然而然地驱使开发者将整个WEB应用抽象分离到一个一个组件的颗粒度, 当一个应用不断被组件化抽象,小到一个按钮,大到一个页面,每个组件本身就是一个个独立的 vm
单元。
在 SFC (single-file components) 单文件组件的开发范式下,每个 *.vue
文件就是一个 vm
, <template>...</template>
可以理解为 vm
中的 v
;而 <style><style>
可以理解为 vm
中的 m
的样式控制部分,<script></script>
可以理解为 vm
中的 m
的逻辑和数据部分, 另外框架底层实现了一个 MVVM
模型中的“绑定器” ,通过开发者对每个视图模型的声明式编程来驱动程序的视图更新。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// *.vue
vm
├── v // view
│ └── template
└── m // model
├── style
└── script
├── data
│ ├── data(){}
│ └── computed:{}
└── logic
├── lifecycle-hooks // created,...
├── watch:{}
└── methods{}
// 这种“奇怪的结构“思维,源于笔者入门编程时,深受 `MVC`(model, view, controller)编程模型启蒙式的“洗脑”,应用程序分层的思维早已根深蒂固
如此,开发者管理维护单个组件(vm
实例)本身,确实可以做到非常高效、清晰,
但是随着应用迭代,不知不觉,整个应用也变成了庞大的 “vm” 集合, 相隔层级较深的组件与组件之间可能形成了较长的通信路径。
而 Vue
本身内置的数据流管理方案有限, props
(其实本质就像一个函数定义的参数), event emit
等其实在复杂场景发挥有限,不适合长路径的通信,无法支撑组织管理整个”前端应用“的状态,虽然又不是不能用,但是要追求合理可维护性则显的捉襟见肘。
另外,在整个前端应用生命周期下,数据既可能来源服务端接口通信,有可能来源于用户的交互操作,还有可能来源与组件之间的通信变更等,依托”绑定器“,不同流向的数据不断双向交织,并且如“化学反应”般不断产生“次生”数据,随着应用逐渐壮大的同时,形成的一张极其庞大的数据“网”笼罩着前端开发者,应用也变得越来越难以维护,开发者顿时失去了 MMVM 分层模式带来的舒适的开发体验,前端应用似乎也迎来了生命周期的”软件危机“,变的危如累卵,应用的故障将在任何时候任何地方变的一触即发。
如何再次贯彻 MV* 的模式思想?是否有办法再次将整个前端应用如”上帝视角“般的分层?前端开发者如何再次破局,突破瓶颈,拯救”软件危机“?
Flux
答案是肯定的,就在彼时,“状态管理”的思想横空出世也顺利成章(其实万变不离其宗,依然是编程模式,依然是分层的思想),将前端应用着实变成了一部“状态机”,管理好状态,即是管理好了应用。
依然是前端界风向标 Facebook
首当其冲,引入经典的编程思想,在前端生态提出了 Flux
设计思想, 「状态管理」也由此慢慢在前端生态大行其道,可谓一发不可收拾, redux、mobx、dvajs、rematchjs 、 vuex 等优秀的状态管理方案,也如雨后春笋般纷纷涌现,成为前端开发者的福音。
- View: 视图层
- Action(动作):视图层发出的消息(比如mouseClick)
- Dispatcher(派发器):用来接收Actions、执行回调函数
- Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面
用户访问 View
, View
发出用户的 Action
, Dispatcher
收到 Action
, 要求 Store
进行相应的更新,可能是从服务端获取新数据,可能是从其他组件获取其他组件数据,Store
根据一系列业务逻辑更新后,发出一个 “change”
事件, View
收到 “change”
事件后,最终呈现更新后的 View
.
整个应用改变的过程,数据永远来源于 Store
, 形成单一数据源,有效的将交织的数据流梳理清理,不用去管数据流在 Store
前是什么,导致应用突变的逻辑永远只存在 Store
中,开发只需要关注 Store
就能有效的洞察应用的改变逻辑,应用的可维护性得到了大大跃升。
对于开发者过度分离,导致 Store
过于臃肿,完全可以模块化 Store
后,通过与组件的生命周期保持同步,实现 Store
模块的动态异步的加载与卸载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// Vue CLI "SFC" Example
src
├── components // 通用组件
├── container // 容器组件
│ ├── index.vue // App.vue
│ ├── Hello // Hello 组件
│ │ ├── components // Hello 组件独有子组件
│ │ │ └── ...
│ │ ├── api // Hello 组件独有 api
│ │ ├── store // Hello 组件独有的状态,为了将状态从 vm 中二次分层,并且保持与 vm 生命周期同步,动态注册卸载
│ │ └── index.vue // Hello 组件 vm
│ └── ... // other compoents
├── store // 状态模块(状态实例的初始化以及全局等需要共享的状态)
├── service // 网络服务 (通用的 api、白名单配置以及 请求封装)
├── settings // 程序设置
├── utils // 工具
├── mixins // 混入
├── main.js //程序初始化入口
└── ...
/**
* @description
* store 的异步加载:
* 1. https://vuex.vuejs.org/guide/modules.html#dynamic-module-registration
* 2. https://medium.com/@arliber/vuex-next-steps-namespaces-and-dynamic-modules-92ea23a0ee9a
*/
Example
一个原汁原味的 Flux 例子
Dispatch Actions
初始化派发器
1
2
3// Facebook Dispatcher Library.
// see: https://facebook.github.io/flux/docs/dispatcher
var AppDispatcher = new Dispatcher();页面中有个对列表新增一个 item 的按钮
1
<button onClick={ this.createNewItem }>New Item</button>
按钮上绑定了一个点击事件
createNewItem
,发送一个new-item
的"action"
1
2
3
4
5
6createNewItem: function( evt ) {
AppDispatcher.dispatch({
actionName: 'new-item',
newItem: { name: 'Stan' }
});
}
Store Responds
列表 Store
1
2
3var ListStore = {
items: []
};派发器上注册一个响应的处理器
1
2
3
4
5
6
7
8AppDispatcher.register( function( payload ) {
switch( payload.actionName ) {
case 'new-item':
ListStore.items.push( payload.newItem );
break;
……
}
});
至此,视图可以通过派发器触发 Store
更新.
Store Emits Event
Can use MicroEvent.js this is easy
接下来就是在 Store
中处理具体的业务逻辑,那么最终需要再触发 View
更新。
现在很多优秀的状态管理方案,如 Redux
, Vuex
等已经底层实现自动触发视图更新,只需要在初始化应用的时候去将应用于你的 Store
绑定即可, 原理其实类似 jQuery
时代的 trigger
.
赋予
Store
发送事件的能力1
MicroEvent.mixin( ListStore );
Store
数据更新后同步发送change
事件1
2
3
4
5
6
7// ...
case 'new-item':
ListStore.items.push( payload.newItem );
// Tell the view store changed!
ListStore.trigger( 'change' );
break;
// ...
View Responds
组件初始化订阅
change
事件1
2
3
4
5// ...
componentDidMount: function() {
ListStore.bind( 'change', this.listChanged );
},
// ...组件初始化注销监听
change
事件1
2
3
4
5// ...
componentWillUnmount: function() {
ListStore.unbind( 'change', this.listChanged );
},
// ...change
事件只做一件事情,触发组件视图更新1
2
3
4
5// ...
listChanged: function() {
this.forceUpdate();
},
// ...React jsx render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
render: function() {
// Remember, ListStore is global!
var items = ListStore.getAll();
// Build list items markup by looping
// over the entire list
var itemHtml = items.map( function( listItem ) {
// "key" is important, should be a unique
// identifier for each list item
return <li key={ listItem.id }>
{ listItem.name }
</li>;
});
return <div>
<ul>
{ itemHtml }
</ul>
<button onClick={ this.createNewItem }>New Item</button>
</div>;
}
}
One More Things
actions
可以统一的组织,一个 store
可以有许多 actions
1
2
3
4
5
6
7
8
9ListActions = {
add: function( item ) {
AppDispatcher.dispatch({
eventName: 'new-item',
newItem: item
});
},
// other actions...
};
End, Thanks!
现如今众多的状态管理方案以其「丰富的API
」以及「语法糖」设计, 开箱即用,帮助开发者保证了高效快速开发的同时依然可以随心所欲的对应用编程合理地分层,当然,凡事过犹不及,对于程序的分层设计、状态管理也是一样的,需要开发者在实际项目开发中三思后实施。
至此,碎碎念地表达了拥抱 State Management
,如有不当,恭请指正~。