利用Dawn工程化工具實踐MobX數據流管理方案
本文首發於阿里雲前端dawn團隊專欄,為獲得最佳閱讀效果,請點擊原文
項目在最初應用 MobX 時,對較為複雜的多人協作項目的數據流管理方案沒有一個優雅的解決方案,通過對MobX官方文檔中針對大型可維護項目最佳實踐的學習和應用,把自己的理解抽象出一個簡單的todoMVC應用,供大家交流和討論。
搭建開發環境
安裝Dawn
要求 Node.js v7.6.0 及以上版本。
$ [sudo] npm install dawn -g n
初始化工程
$ dawn init -t front n
這裡我選擇使用無依賴的 front 模板,便於自定義我的前端工程。
目錄結構分析
由 dawn 工具生成的項目目錄如下:
.n├── .dawn # dawn 配置文件n├── node_modulen├── srcn│ ├── assetsn│ └── index.jsn├── test # 單元測試n├── .eslintrc.jsonn├── .eslintrc.ymln├── .gitignoren├── .npmignoren├── README.mdn├── package.jsonn├── server.ymln└── tsconfig.json n
其中我們重點需要關注的是 src 目錄,其中的 index.js 就是我們項目的入口文件。
安裝依賴
"devDependencies": {n "react": "^15.6.1",n "react-dom": "^15.6.1"n},n"dependencies": {n "mobx": "^3.2.2",n "mobx-react": "^4.2.2",n // 以下是todoMVC樣式模塊n "todomvc-app-css": "^2.1.0",n "todomvc-common": "^1.0.4"n} n
安裝好依賴,環境就配置完成了,整個環境搭建過程只需要3步,開箱即用,不需要關注 Webpack 和 ESLint 等開發環境的繁瑣配置。當然,Dawn 也完全支持自定義這些工具的配置。
todoMVC with MobX
新的項目目錄設計如下:
...n├── srcn│ ├── assets # 放置靜態文件n│ │ ├── common.lessn│ │ ├── favicon.icon│ │ └── index.htmln│ ├── components # 業務組件n│ │ ├── todoApp.jsn│ │ ├── todoEntry.jsn│ │ ├── todoItem.jsn│ │ └── todoList.jsn│ ├── index.js # 入口文件n│ ├── models # 數據模型定義n│ │ └── TodoModel.jsn│ ├── stores # 數據store定義n│ │ ├── TodoStore.jsn│ │ ├── ViewStore.jsn│ │ └── index.jsn│ └── utils # 工具函數n│ └── index.jsn... n
其中 MobX 數據流實踐的核心概念就是數據模型(Model)和數據儲存(Store)。
定義數據模型
數據模型即為 MVVM(Model/View/ViewModel) 中的 Model。早期的前端開發,需求比較簡單,大多是基於後端傳輸的數據去直接填充頁面中的「坑位」,沒有定義數據模型的意識。但隨著前端業務複雜度和數據傳輸量的不斷上升,如果沒有數據模型的定義,在多人協作時會讓前端系統維護的複雜性和不可控性急劇上升,直觀體現就是其它人對數據做改動時,很難覆蓋到改動的某個欄位會產生的全部影響,直接導致維護的周期和難度不斷增加。
定義數據模型有以下好處:
- 讓數據源變的可控,可以清晰的了解到定義欄位的含義、類型等信息,是數據的天然文檔,對多人協作大有裨益。通過應用面向對象的思想,也可以在模型中定義一些屬性和方法供創建出的實例使用。
- 實現前端數據持久化,單頁應用經常會遇到多頁面數據共享和實時更新的問題,通過定義數據模型並創建實例,可以避免非同步拉取來的數據進行 View 層渲染後就被銷毀。
下面是待辦事項的數據模型定義:
import { observable } from mobx;nclass TodoModel {n store;n id;n @observable title;n @observable completed;n /**n * 創建一個TodoModel實例n * 用於單個todo列表項的操作n * @param {object} store 傳入TodoStore,獲取領域模型狀態和方法n * @param {string} id 用於前端操作的實例idn * @param {string} title todo項的內容n * @param {boolean} completed 是否完成的狀態n * @memberof TodoModeln */n constructor(store, id, title, completed) {n this.store = store;n this.id = id;n this.title = title;n this.completed = completed;n }n // 切換列表項的完成狀態n toggle = () => {n this.completed = !this.completed;n }n // 根據id刪除列表項n delete = () => {n this.store.todos = this.store.todosn .filter(todo => todo.id !== this.id);n }n // 設置實例titlen setTitle = (title) => {n this.title = title;n }n}nexport default TodoModel;n
從 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;nimport { uuid } from ../utils;nimport TodoModel from ../models/TodoModel;nclass TodoStore {n // 保存todo列表項n @observable todos = [];n // 添加todo,參數為todo內容n // 注意:此處傳入的 this 即為 todoStore 實例的引用n // 通過引用使得 TodoModel 有了調用 todoStore 的能力n addTodo(title) {n this.todos.push(n new TodoModel(this, uuid(), title, false)n );n }n}nexport default TodoStore; n
需要注意的是,在創建 TodoModel 傳入的 this 即為 todoStore 實例的引用,通過這裡的引用使得 TodoModel 的實例擁有了調用 todoStore 的能力,這也就是我們要保證數據儲存的 Store 只有一個實例的原因。
然後是視圖層對數據進行渲染的方式:
import React, { Component } from react;nimport { computed } from mobx;nimport { inject, observer } from mobx-react;nimport TodoItem from ./todoItem;n@inject(todoStore)n@observernclass TodoList extends Component {n @computed get todoStore() {n return this.props.todoStore;n }n render() {n const { todos } = this.todoStore;n return (n <section className="main">n <ul className="todo-list">n {todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}n </ul>n </section>n );n }n}nexport default TodoList; n
我們把這個過程分步來理解:
- 首先,拿到待辦事項的內容(title)和完成狀態,通過 TodoModel 創建一個新的待辦事項的實例。
- 其次,在 todoStore 中把每個創建出的 TodoModel 實例填入 todos 數組,用於待辦事項列表的渲染。
- 最後,在視圖層中通過 inject 裝飾器注入todoStore,從而引用其中的 todos 數組,MobX 會響應數組的變化完成渲染。
如果待辦事項的內容和完成狀態需要改動,就要修改 Model 中對應的類型屬性,然後在 todoStore 中進行相應的加工,最後產出新的視圖展示。而在這個過程中,我們只需要把可能會變化的屬性定義為可觀察的變數,在需要變更的時候進行修改,剩餘的工作 MobX 會幫我們完成。
定義用戶界面狀態
剛才定義的 todoStore 是針對數據儲存的,但是對於前端來講,還有很大一部分工作是 UI 的狀態管理。
UI 的狀態通常沒有太多的邏輯,但會包含大量鬆散耦合的狀態信息,同樣可以通過定義 UI Store 來管理這部分狀態。以下是一個 UI Store 的簡單定義:
import { observable } from mobx;nexport default class ViewStore {n @observable todoBeingEdited = null;n} n
這個 Store 只包含一個可觀察的屬性,用於保存正在編輯的 TodoModal 實例,通過這個屬性來控制視圖層待辦事項的修改:
...nclass TodoItem extends Component {nn ...nn edit = () => {n // 設置 todoBeingEdited 為當前待辦事項todo的實例n this.viewStore.todoBeingEdited = this.todo;n this.editText = this.todo.title;n };nn ...nn handleSubmit = () => {n const val = this.editText.trim();n if (val) {n this.todo.setTitle(val);n this.editText = val;n } else {n this.todo.delete();n }n // 提交修改後初始化 todoBeingEdited 變數n this.viewStore.todoBeingEdited = null;n }nn render() {n // 根據 todoBeingEdited 和當前 todo 比較的結果判斷是否處於編輯狀態n const isEdit = expr(() => n this.viewStore.todoBeingEdited === this.todo);n const cls = [n this.todo.completed ? completed : ,n isEdit ? editing : n ].join( );n return (n <li className={cls}>n ...n </li>n );n }nn}nnexport default TodoItem; n
在視圖中對 UI Store 的可觀察的屬性進行修改,MobX 會收集相應的變化經過處理後響應在視圖上。
源碼
完整的 todoMVC 代碼可以通過以下方式獲取:
$ dawn init -t react-mobx n
或者在 Github 上查看源碼:https://github.com/xdlrt/dn-template-react-mobx
總結
基於 MobX 的數據流管理方案,分為以下幾步:
- 定義數據 Model,使數據源可控並可持久化
- 定義數據 Store 和 UI Store,創建並管理數據 Model 實例及實例屬性的變更
- 將 Store 注入到視圖層,使用其中的數據進行視圖渲染,MobX 自動響應數據的變化更新視圖
以上是我對 MVVM 框架中使用 MobX 管理數據流的一些理解,同時這種方案也在團隊內一個較為複雜的項目中進行實踐,目前項目的健壯性和可維護性比較健康,歡迎提出不同的見解,共同交流。
最後再吃我一發安利
Dawn 是「阿里雲-業務運營事業部」前端團隊開源的前端構建和工程化工具。
它通過封裝中間件(middleware) ,如 webpack 和本地 server ,並在項目 pipeline 中按需使用,可以將開發過程抽象為相對固定的階段和有限的操作,簡化並統一開發環境,能夠極大地提高團隊的開發效率。
項目的模板即工程 boilerplate 也可以根據團隊的需要進行定製復用,實現「configure once run everywhere」。
歡迎體驗並提出意見和建議,幫助我們改進。Github地址:https://github.com/alibaba/dawn推薦閱讀:
※Yarn vs npm:你需要知道的一切
※如何用 Vue.js 實現一個建站應用
※已放棄了遊戲開發,不知道選擇什麼開發?