利用Dawn工程化工具實踐MobX數據流管理方案

做者:阿里雲前端-也樹
本文首發於阿里雲前端dawn團隊專欄css

項目在最初應用 MobX 時,對較爲複雜的多人協做項目的數據流管理方案沒有一個優雅的解決方案,經過對MobX官方文檔中針對大型可維護項目最佳實踐的學習和應用,把本身的理解抽象出一個簡單的todoMVC應用,供你們交流和討論。html

搭建開發環境

安裝Dawn

要求 Node.js v7.6.0 及以上版本。前端

$ [sudo] npm install dawn -g複製代碼

初始化工程

$ dawn init -t front複製代碼

這裏我選擇使用無依賴的 front 模板,便於自定義個人前端工程。node

目錄結構分析

由 dawn 工具生成的項目目錄以下:react

.
├── .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 就是咱們項目的入口文件。webpack

安裝依賴

"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 也徹底支持自定義這些工具的配置。git

todoMVC with MobX

新的項目目錄設計以下:github

...
├── 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)。web

定義數據模型

數據模型即爲 MVVM(Model/View/ViewModel) 中的 Model。早期的前端開發,需求比較簡單,大可能是基於後端傳輸的數據去直接填充頁面中的「坑位」,沒有定義數據模型的意識。但隨着前端業務複雜度和數據傳輸量的不斷上升,若是沒有數據模型的定義,在多人協做時會讓前端系統維護的複雜性和不可控性急劇上升,直觀體現就是其它人對數據作改動時,很難覆蓋到改動的某個字段會產生的所有影響,直接致使維護的週期和難度不斷增長。npm

定義數據模型有如下好處:

  • 讓數據源變的可控,能夠清晰的瞭解到定義字段的含義、類型等信息,是數據的自然文檔,對多人協做大有裨益。經過應用面向對象的思想,也能夠在模型中定義一些屬性和方法供建立出的實例使用。
  • 實現前端數據持久化,單頁應用常常會遇到多頁面數據共享和實時更新的問題,經過定義數據模型並建立實例,能夠避免異步拉取來的數據進行 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…