在 React 工程中利用 Mota 編寫面向對象的業務模型

圖片描述

簡述

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

mota 是一個主張「面向對象」的、支持「雙向綁定」的 React 應用輔助庫,基於 mota 你能夠用純 JavaScript 爲應用編寫徹底面向對象的「業務模型」,並輕易的將「業務模型」關聯到 React 應用中。vue

連接

示例

在線 TodoList 示例
(示例源碼)react

安裝

經過 npm 安裝,以下git

$ npm i mota --save

或經過 dawn 腳手腳加建立工程,以下github

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

須要先安裝 dawn(Dawn 安裝及使用文檔npm

工程結構

一個 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

編寫業務模型

在你編寫模型以前,先放下 React 也放下 mota,就用單純的 JavaScript 去編寫你的業務模型,或有一個或多個類、或就是幾個 Object,依它們應有的、天然的關係去抽像就好了,業務模型不依賴於 UI、也不依賴於某個框架,它易於測試,你能夠針對它作單元測試。它易於重用,你能夠將它用在合適的地方。最後, mota 只是出場把它關聯到 react。json

在 mota 中「模型」能夠是由一個 class 或普通的的 Object,整個「業務模型層」會由多個 class 和多個 Object 組成,而編寫模型所須要的知識就是 JavaScript 固有的面向對象編程的知識。數組

以下示例經過編寫一個名爲 Userclass 建立了一個「用戶模型」app

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);

好了,關於綁定就這些了。

文檔

相關文章
相關標籤/搜索