在React工程中利用Mota編寫物件導向的業務模型

Houfeng發表於2018-02-11

簡述

React 是一個「檢視層」的 UI 框架,以常見的 MVC 來講 React 僅是 View,而我們在編寫應用時,通常還需要關注更加重要的 model,對於 React 來講,我們常常需要一個「狀態管理」庫。然而,目前大多數針對 React 的狀態管理庫都是「強依賴」過多的侵入本應該獨立的業務模型中,導致「業務邏輯」對應的程式碼並不能輕易在其它地方重用,往往這些框架還具有「強排它性」,但是「業務模型」應該是沒有過多依賴,應該是無關框架的,它應該隨時可以被用在任何合適的 JavaScript 環境中,使用 mota 你可以用原生的普通的 JavaScript 程式碼編寫你的「業務模型」,並讓你的「業務模型」在不同框架、不同執行環境下重用更為容易。

mota 是一個主張「物件導向」的、支援「雙向繫結」的 React 應用輔助庫,基於 mota 你可以用純 JavaScript 為應用編寫完全物件導向的「業務模型」,並輕易的將「業務模型」關聯到 React 應用中。

示例

線上 TodoList 示例
(示例原始碼)

安裝

通過 npm 安裝,如下

$ npm i mota --save

或通過 dawn 腳手腳加建立工程,如下

$ mkdir your_path
$ cd your_path
$ dn init -t mota
$ dn dev

需要先安裝 dawn(Dawn 安裝及使用文件

工程結構

一個 mota 工程的通常結構如下

.
├── README.md
├── package.json
└── src
    ├── assets
    │   ├── common.less
    │   ├── favicon.ico
    │   └── index.html
    ├── components
    │   ├── todoApp.js
    │   └── todoItem.js
    ├── index.js
    └── models
        ├── TodoItem.js
        ├── TodoList.js
        └── index.js

編寫業務模型

在 mota 中「模型」可以是由一個 class 或普通的的 Object,整個「業務模型層」會由多個 class 和多個 Object 組成,而編寫模型所需要的知識就是 JavaScript 固有的物件導向程式設計的知識。

如下示例通過編寫一個名為 Userclass 建立了一個「使用者模型」

export default class User {
  firstName = `Jack`;
  lastName = `Hou`;
  get fullName(){
    reutrn `${this.firstName} ${this.lastName}`;
  }
}

也可以是一個 Object,通常這個模型需要是「單例」時,可採用這種方式,如下

export default {
  firstName: `Jack`,
  lastName: `Hou`,
  get fullName(){
    reutrn `${this.firstName} ${this.lastName}`;
  }
};

在「業務模型」編寫完成後,可以通過 @model 將某個「類」或「類的例項」關聯到指定元件,關聯後便可以在元件中使用 this.model 訪問「模型的成員變數或方法」了,mota 還會自動「收集元件依賴」,在元件「依賴的模型資料」發生變化時,自動響應變化並「驅動元件重新渲染」,如下

import { model,binding } from `mota`;
import React from `react`;
import ReactDOM from `react-dom`;
import User from `./models/user`;

@model(User)
class App extends React.Component {

  onChange(field,event){
    this.model[field] = event.target.value;
  }

  render(){
    return <div>
      <p>{this.model.fullName}</p>
      <p>
        <input onChange={this.onChange.bind(this,`firstName`)}/>
        <br/>
        <input onChange={this.onChange.bind(this,`lastName`)}/>
      </p>
    </div>;
  }
}

ReactDOM.render(<App/>, mountNode);

值得注意的是,在使用 @model 時如果傳入的是一個 class 最終每個元件例項都會自動建立一個 獨立的例項,這樣帶來的好處是「當一個頁面中有同一個元件的多個例項時,不會相互影響」。

屬性對映

在 React 中通常會將應用折分為多個元件重用它們,並在用時傳遞給它「屬性」,mota 提供了將「元件屬性」對映到「模型資料」的能力,基於 model 程式設計會讓「檢視層」更單一,專注於 UI 的呈現,,如下

@model({ value: `demo` })
@mapping([`value`])
class Demo extends React.Component {
  render () {
    return <div>{this.model.value}</div>;
  }
}

上邊的程式碼通過 mappingDemo 這個元件的 value 屬性對映到了 model.value 上,在元件的屬性 value 發生變化時,會自動同步到 model.value 中。

通過一個 map 進行對映,還可以讓「元件屬性」和「模型的成員」使用不同名稱,如下:

@model({ value: `demo` })
@mapping({ content: `value` })
class Demo extends React.Component {
  render () {
    return <div>{this.model.value}</div>;
  }
}

上邊的程式碼,將元件 demo 的 content 屬性對映到了 model.value 上。

自執行函式

mota 中提供了一個 autorun 函式,可用於裝飾 React 元件的成員方法,被裝飾的「成員方法」將會在元件掛載後自動執行一次,mota 將「收集方法中依賴的模型資料」,在依賴的模型資料發生變化時會「自動重新執行」對應的元件方法。

示例

import { Component } from `react`;
import { model, autorun } from `mota`;
import DemoModel from `./models/demo`;

@model(DemoModel)
export default Demo extends Component {

  @autorun
  test() {
    console.log(this.model.name);
  }

}

上邊的示例程式碼中,元件在被掛載後將會自動執行 test 方法,同時 mota 會發現方法中依賴了 model.name,那麼,在 model.name 發生變化時,就會重新執行 test 方法。

監聽模型變化

mota 中提供了一個 watch 函式,可用於裝飾 React 元件的成員方法,watch 可以指定要觀察的「模型資料」,在模型資料發變化時,就會自動執行「被裝飾的元件方法」,watch 還可以像 autorun 一樣自動執行一次,但它和 autorun 還是不盡相同,主要有如下區別

  • autorun 會自動收集依賴,而 watch 不會關心元件方法中有何依賴,需要手動指定依賴的模型資料
  • watch 預設不會「自動執行」,需顯式的指定「立即執行引數為 true」,才會自動執行首次。
  • autorun 依賴的是「模型資料」本身,而 watch 依賴的是「計算函式」每次的「計算結果」

示例

import { Component } from `react`;
import { model, autorun } from `mota`;
import DemoModel from `./models/demo`;

@model(DemoModel)
export default Demo extends Component {

  @watch(model=>model.name)
  test() {
    console.log(`name 發生了變化`);
  }

}

上邊的程式碼,通過 watch 裝飾了 test 方法,並指定了觀察的模型資料 model.name,那麼每當 model.name 發生變化時,都會列印 name 發生了變化.

watch 是否重新執行,取決於 watch 的作為第一個引數傳給它的「計算函式」的計算結果,每當依賴的模型資料發生變化時 watch 都會重執行計算函式,當計算結果有變化時,才會執行被裝飾的「元件方法」,示例

export default Demo extends Component {

  @watch(model=>model.name+model.age)
  test() {
    console.log(`name 發生變化`);
  }

}

有時,我們希望 watch 能首先自動執行一次,那麼可通過向第二個引數傳一個 true 宣告這個 watch 要自動執行一次。

export default Demo extends Component {

  @watch(model=>model.name,true)
  test() {
    console.log(`name 發生變化`);
  }

}

上邊的 test 方法,將會在「元件掛載之後自動執行」,之後在 model.name 發生變化時也將自動重新執行。

資料繫結

基本用法

不要驚詫,就是「雙向繫結」。mota 主張「物件導向」,同樣也不排斥「雙向繫結」,使用 mota 能夠實現類似 ngvue 的繫結效果。還是前邊小節中的模型,我們來稍微改動一下元件的程式碼

import { model,binding } from `mota`;
import React from `react`;
import ReactDOM from `react-dom`;
import User from `./models/user`;

@model(User)
@binding
class App extends React.Component {
  render(){
    const { fullName, firstName, popup } = this.model;
    return <div>
      <p>{fullName}</p>
      <p>
        <input data-bind="firstName"/>
        <button onClick={popup}> click me </button>
      </p>
    </div>;
  }
}
ReactDOM.render(<App/>, mountNode);

其中的「關鍵」就是 @binding,使用 @binding 後,元件便具備了「雙向繫結」的能力,在 jsx 中便可以通過名為 data-bind 的自定義 attribute 進行繫結了,data-bind 的值是一個「繫結表示式字串」,繫結表示式執行的 scopemodel 而不是 this,也就是隻能與 模型的成員 進行繫結。

會有一種情況是當要繫結的資料是一個迴圈變數時,「繫結表示式」寫起會較麻煩也稍顯長,比如

@model(userModel)
@binding
class App extends React.Component {
  render(){
    const { userList } = this.model;
    return <ul>
     {userList.map((user,index)=>(
       <li key={user.id}>
         <input type="checkobx" data-bind={`userList[${index}].selected`}>
         {user.name}
       </li>
     ))}
    </ul>;
  }
}

因為「繫結表示式」的執行 scope 預設是 this.model,以及「表示式是個字串」,看一下 userList[${index}].selected 這並不友好,為此 mota 還提供了一個名為 data-scopeattribute,通過它能改變要繫結的 scope,參考如下示例

@model(userModel)
@binding
class App extends React.Component {
  render(){
    const { userList } = this.model;
    return <ul>
     {userList.map(user=>(
       <li key={user.id}>
         <input type="checkobx" data-scope={user} data-bind="selected">
         {user.name}
       </li>
     ))}
    </ul>;
  }
}

通過 data-scopeinput 的繫結上下文物件宣告為當前迴圈變數 user,這樣就可以用 data-bind 直接繫結到對應 user 的屬性上了。

原生表單控制元件

所有的原生表單控制元件,比如「普通 input、checkbox、radio、textarea、select」都可以直接進行繫結。其中,「普通 input 和 textrea」比較簡單,將一個字元型別的模型資料與控制元件繫結就行了,而對於「checkbox 和 radio」 有多種不同的繫結形式。

將「checkbox 或 radio」繫結到一個 boolean 值,此時會將 checkbox 或 radio 的 checked 屬性和模型資料建立繫結,checked 反應了 boolean 變數的值,參考如下示例

@model({ selected:false })
@binding
class App extends React.Component {
  render(){
    return <div>
      <input type="checkbox" data-bind="selected"/>
      <input type="radio" data-bind="selected"/>
    </div>;
  }
}

如上示例通過 this.model.selected 就能拿到當前 checkbox 或 radio 的選中狀態。

將 checkbox 繫結到一個「陣列」,通常是多個 checkbox 繫結同一個陣列變數上,此時和資料建立繫結的是 checkbox 的 value,資料中會包含當前選中的 checkbox 的 value,如下

@model({ selected:[] })
@binding
class App extends React.Component {
  render(){
    return <div>
      <input type="checkbox" data-bind="selected" value="1"/>
      <input type="checkbox" data-bind="selected" value="2"/>
    </div>;
  }
}

如上示例,通過 this.selected 就能知道當前有哪些 checkbox 被選中了,並拿到所有選中的 value

將多個 radio 繫結我到一個「字元型別的變數」,此時和資料建立繫結的是 raido 的 value,因為 radio 是單選的,所以對應的資料是當前選中的 radio 的 value,如下

@model({ selected:`` })
@binding
class App extends React.Component {
  render(){
    return <div>
      <input type="radio" data-bind="selected" value="1"/>
      <input type="radio" data-bind="selected" value="2"/>
    </div>;
  }
}

通過 this.model.selected 就能拿到當前選中的 radio 的 value

自定義元件

但是對於一些「元件庫」中的「部分表單元件」不能直接繫結,因為 mota 並沒有什麼依據可以判斷這是一個什麼元件。所以 mota 提供了一個名為 bindable 的函式,用將任意元件包裝成「可繫結元件」。

bindable 有兩種個引數,用於分別指定「原始元件」和「包裝選項」

//可以這樣
const MyComponent = bindable(opts, Component);
//也可這樣
const MyCompoent = bindable(Component, opts);

關建是 bindable 需要的 opts,通過 opts 我們可以造訴 mota 如何繫結這個元件,opts 中有兩個重要的成員,它的結構如下

{
  value: [`value 對應的屬性名`],
  event: [`value 改變的事件名`]
}

所以,我們可以這樣包裝一個自定義文字輸入框

const MyInput = bindable(Input,{
  value: [`value`],
  event: [`onChange`]
});

對這種「value 不需要轉換,change 能通過 event 或 event.target.value 拿到值」的元件,通過如上的程式碼就能完成包裝了。

對於有 onChangevalue 的這類文字輸入元件,因為 opts 的預設值就是

{
  value: [`value`],
  event: [`onChange`]
}

所以,可以更簡單,這樣就行,

const MyInput = bindable(Input);

而對於 checkbox 和 radio 來講,如上邊講到的它「根據不同的資料型有不同的繫結形式」,這就需要指定處理函式了,如下

const radioOpts = {
  prop: [`checked`, (ctx, props) => {
    const mValue = ctx.getValue();
    if (typeof mValue == `boolean`) {
      return !!mValue;
    } else {
      return mValue == props.value;
    }
  }],
  event: [`onChange`, (ctx, event) => {
    const { value, checked } = event.target;
    const mValue = ctx.getValue();
    if (typeof mValue == `boolean`) {
      ctx.setValue(checked);
    } else if (checked) ctx.setValue(value);
  }]
};

通過 prop 的第二個值,能指定「屬性處理函式」,event 的第二個值能指取「事件處理函式」,處理函式的 ctx 是個特殊的物件

  • ctx.getValue 能獲取「當前繫結的模型資料」
  • ctx.setValue 能設定「當前繫結的模型資料」

上邊是 radio 的配置,首先,在「屬性處理函式」中通過繫結的「模型資料的型別」決定 checked 最終的狀態是什麼,並在函式中返回。再次,在「事件處理函式」中通過繫結的「模型資料的型別」決定將什麼值回寫到模型中。

通過「屬性處理函式」和「事件處理函式」幾乎就能將任意的自定義元件轉換為「可繫結元件」了。

另外,對於常見的 CheckBoxRadio 型別的元件 mota 也提供了內建的 opts 配置支援,如果一個自定義元件擁有和「原生 checkbox 一致的屬性和事件模型」,那邊可以直接用簡單的方式去包裝,如下

const MyCheckBox = bindable(`checkbox`,CheckBox);
const MyRadio = bindable(`radio`,Radio);

好了,關於繫結就這些了。

文件

連結


相關文章