作者:阿里雲前端-也樹
本文首發於阿里雲前端dawn團隊專欄。
專案在最初應用 MobX 時,對較為複雜的多人協作專案的資料流管理方案沒有一個優雅的解決方案,通過對MobX官方文件中針對大型可維護專案最佳實踐的學習和應用,把自己的理解抽象出一個簡單的todoMVC應用,供大家交流和討論。
搭建開發環境
安裝Dawn
要求 Node.js v7.6.0 及以上版本。
$ [sudo] npm install dawn -g複製程式碼
初始化工程
$ dawn init -t front複製程式碼
這裡我選擇使用無依賴的 front 模板,便於自定義我的前端工程。
目錄結構分析
由 dawn 工具生成的專案目錄如下:
.
├── .dawn # dawn 配置檔案
├── node_modules
├── src
│ ├── assets
│ └── index.js
├── test # 單元測試
├── .eslintrc.json
├── .eslintrc.yml
├── .gitignore
├── .npmignore
├── README.md
├── package.json
├── server.yml
└── tsconfig.json複製程式碼
其中我們重點需要關注的是 src 目錄,其中的 index.js 就是我們專案的入口檔案。
安裝依賴
"devDependencies": {
"react": "^15.6.1",
"react-dom": "^15.6.1"
},
"dependencies": {
"mobx": "^3.2.2",
"mobx-react": "^4.2.2",
// 以下是todoMVC樣式模組
"todomvc-app-css": "^2.1.0",
"todomvc-common": "^1.0.4"
}複製程式碼
安裝好依賴,環境就配置完成了,整個環境搭建過程只需要3步,開箱即用,不需要關注 Webpack 和 ESLint 等開發環境的繁瑣配置。當然,Dawn 也完全支援自定義這些工具的配置。
todoMVC with MobX
新的專案目錄設計如下:
...
├── src
│ ├── assets # 放置靜態檔案
│ │ ├── common.less
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── components # 業務元件
│ │ ├── todoApp.js
│ │ ├── todoEntry.js
│ │ ├── todoItem.js
│ │ └── todoList.js
│ ├── index.js # 入口檔案
│ ├── models # 資料模型定義
│ │ └── TodoModel.js
│ ├── stores # 資料store定義
│ │ ├── TodoStore.js
│ │ ├── ViewStore.js
│ │ └── index.js
│ └── utils # 工具函式
│ └── index.js
...複製程式碼
其中 MobX 資料流實踐的核心概念就是資料模型(Model)和資料儲存(Store)。
定義資料模型
資料模型即為 MVVM(Model/View/ViewModel) 中的 Model。早期的前端開發,需求比較簡單,大多是基於後端傳輸的資料去直接填充頁面中的“坑位”,沒有定義資料模型的意識。但隨著前端業務複雜度和資料傳輸量的不斷上升,如果沒有資料模型的定義,在多人協作時會讓前端系統維護的複雜性和不可控性急劇上升,直觀體現就是其它人對資料做改動時,很難覆蓋到改動的某個欄位會產生的全部影響,直接導致維護的週期和難度不斷增加。
定義資料模型有以下好處:
- 讓資料來源變的可控,可以清晰的瞭解到定義欄位的含義、型別等資訊,是資料的天然文件,對多人協作大有裨益。通過應用物件導向的思想,也可以在模型中定義一些屬性和方法供建立出的例項使用。
- 實現前端資料持久化,單頁應用經常會遇到多頁面資料共享和實時更新的問題,通過定義資料模型並建立例項,可以避免非同步拉取來的資料進行 View 層渲染後就被銷燬。
下面是待辦事項的資料模型定義:
import { observable } from 'mobx';
class TodoModel {
store;
id;
@observable title;
@observable completed;
/**
* 建立一個TodoModel例項
* 用於單個todo列表項的操作
* @param {object} store 傳入TodoStore,獲取領域模型狀態和方法
* @param {string} id 用於前端操作的例項id
* @param {string} title todo項的內容
* @param {boolean} completed 是否完成的狀態
* @memberof TodoModel
*/
constructor(store, id, title, completed) {
this.store = store;
this.id = id;
this.title = title;
this.completed = completed;
}
// 切換列表項的完成狀態
toggle = () => {
this.completed = !this.completed;
}
// 根據id刪除列表項
delete = () => {
this.store.todos = this.store.todos
.filter(todo => todo.id !== this.id);
}
// 設定例項title
setTitle = (title) => {
this.title = title;
}
}
export default TodoModel;複製程式碼
從 TodoModel 的定義中可以清楚的看到一個待辦事項擁有的屬性和方法,通過這些,就可以對建立出的例項進行相應的操作。但是在例項中只能修改例項自身的屬性,怎樣才能把待辦事項的狀態變化通過 viewModel 來渲染到 view 層呢?
定義資料儲存
官方文件對資料儲存的定義是這樣的:
Stores can be found in any Flux architecture and can be compared a bit with controllers in the MVC pattern. The main responsibility of stores is to move logic and state out of your components into a standalone testable unit.
翻譯過來是:資料儲存(Store)可以在任何 Flux 系架構中找到,可以與 MVC 模式中的控制器(Controller)進行類比。它的主要職責是將邏輯和狀態從元件中移至一個獨立的,可測試的單元。
也就是說,Store 就是連線我們的 View 層和 Model 層之間的橋樑,即 ViewModel,所有的狀態和邏輯變化都應該在 Store 中完成。同一個 Store 不應該在記憶體中有多個例項,要確保每個 Store 只有一個例項,並允許我們安全地對其進行引用。
下面通過專案示例來更清晰的理解這個過程。
首先是 todoMVC 的資料 Store 定義:
import { observable } from 'mobx';
import { uuid } from '../utils';
import TodoModel from '../models/TodoModel';
class TodoStore {
// 儲存todo列表項
@observable todos = [];
// 新增todo,引數為todo內容
// 注意:此處傳入的 this 即為 todoStore 例項的引用
// 通過引用使得 TodoModel 有了呼叫 todoStore 的能力
addTodo(title) {
this.todos.push(
new TodoModel(this, uuid(), title, false)
);
}
}
export default TodoStore;複製程式碼
需要注意的是,在建立 TodoModel 傳入的 this 即為 todoStore 例項的引用,通過這裡的引用使得 TodoModel 的例項擁有了呼叫 todoStore 的能力,這也就是我們要保證資料儲存的 Store 只有一個例項的原因。
然後是檢視層對資料進行渲染的方式:
import React, { Component } from 'react';
import { computed } from 'mobx';
import { inject, observer } from 'mobx-react';
import TodoItem from './todoItem';
@inject('todoStore')
@observer
class TodoList extends Component {
@computed get todoStore() {
return this.props.todoStore;
}
render() {
const { todos } = this.todoStore;
return (
<section className="main">
<ul className="todo-list">
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</ul>
</section>
);
}
}
export default TodoList;複製程式碼
我們把這個過程分步來理解:
- 首先,拿到待辦事項的內容(title)和完成狀態,通過 TodoModel 建立一個新的待辦事項的例項。
- 其次,在 todoStore 中把每個建立出的 TodoModel 例項填入 todos 陣列,用於待辦事項列表的渲染。
- 最後,在檢視層中通過 inject 裝飾器注入todoStore,從而引用其中的 todos 陣列,MobX 會響應陣列的變化完成渲染。
如果待辦事項的內容和完成狀態需要改動,就要修改 Model 中對應的型別屬性,然後在 todoStore 中進行相應的加工,最後產出新的檢視展示。而在這個過程中,我們只需要把可能會變化的屬性定義為可觀察的變數,在需要變更的時候進行修改,剩餘的工作 MobX 會幫我們完成。
定義使用者介面狀態
剛才定義的 todoStore 是針對資料儲存的,但是對於前端來講,還有很大一部分工作是 UI 的狀態管理。
UI 的狀態通常沒有太多的邏輯,但會包含大量鬆散耦合的狀態資訊,同樣可以通過定義 UI Store 來管理這部分狀態。
以下是一個 UI Store 的簡單定義:
import { observable } from 'mobx';
export default class ViewStore {
@observable todoBeingEdited = null;
}複製程式碼
這個 Store 只包含一個可觀察的屬性,用於儲存正在編輯的 TodoModal 例項,通過這個屬性來控制檢視層待辦事項的修改:
...
class TodoItem extends Component {
...
edit = () => {
// 設定 todoBeingEdited 為當前待辦事項todo的例項
this.viewStore.todoBeingEdited = this.todo;
this.editText = this.todo.title;
};
...
handleSubmit = () => {
const val = this.editText.trim();
if (val) {
this.todo.setTitle(val);
this.editText = val;
} else {
this.todo.delete();
}
// 提交修改後初始化 todoBeingEdited 變數
this.viewStore.todoBeingEdited = null;
}
render() {
// 根據 todoBeingEdited 和當前 todo 比較的結果判斷是否處於編輯狀態
const isEdit = expr(() =>
this.viewStore.todoBeingEdited === this.todo);
const cls = [
this.todo.completed ? 'completed' : '',
isEdit ? 'editing' : ''
].join(' ');
return (
<li className={cls}>
...
</li>
);
}
}
export default TodoItem;複製程式碼
在檢視中對 UI Store 的可觀察的屬性進行修改,MobX 會收集相應的變化經過處理後響應在檢視上。
原始碼
完整的 todoMVC 程式碼可以通過以下方式獲取:
$ dawn init -t react-mobx複製程式碼
或者在 Github 上檢視原始碼:github.com/xdlrt/dn-te…
總結
基於 MobX 的資料流管理方案,分為以下幾步:
- 定義資料 Model,使資料來源可控並可持久化
- 定義資料 Store 和 UI Store,建立並管理資料 Model 例項及例項屬性的變更
- 將 Store 注入到檢視層,使用其中的資料進行檢視渲染,MobX 自動響應資料的變化更新檢視
以上是我對 MVVM 框架中使用 MobX 管理資料流的一些理解,同時這種方案也在團隊內一個較為複雜的專案中進行實踐,目前專案的健壯性和可維護性比較健康,歡迎提出不同的見解,共同交流。
最後再吃我一發安利
Dawn 是「阿里雲-業務運營事業部」前端團隊開源的前端構建和工程化工具。
它通過封裝中介軟體(middleware) ,如 webpack 和本地 server ,並在專案 pipeline 中按需使用,可以將開發過程抽象為相對固定的階段和有限的操作,簡化並統一開發環境,能夠極大地提高團隊的開發效率。
專案的模板即工程 boilerplate 也可以根據團隊的需要進行定製複用,實現「configure once run everywhere」。
歡迎體驗並提出意見和建議,幫助我們改進。Github地址:github.com/alibaba/daw…