簡述
React 是一個「檢視層」的 UI 框架,以常見的 MVC 來講 React 僅是 View,而我們在編寫應用時,通常還需要關注更加重要的 model,對於 React 來講,我們常常需要一個「狀態管理」庫。然而,目前大多數針對 React 的狀態管理庫都是「強依賴」過多的侵入本應該獨立的業務模型中,導致「業務邏輯」對應的程式碼並不能輕易在其它地方重用,往往這些框架還具有「強排它性」,但是「業務模型」應該是沒有過多依賴,應該是無關框架的,它應該隨時可以被用在任何合適的 JavaScript 環境中,使用 mota 你可以用原生的普通的 JavaScript 程式碼編寫你的「業務模型」,並讓你的「業務模型」在不同框架、不同執行環境下重用更為容易。
mota 是一個主張「物件導向」的、支援「雙向繫結」的 React 應用輔助庫,基於 mota 你可以用純 JavaScript 為應用編寫完全物件導向的「業務模型」,並輕易的將「業務模型」關聯到 React 應用中。
連結
示例
安裝
通過 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 固有的物件導向程式設計的知識。
如下示例通過編寫一個名為 User
的 class
建立了一個「使用者模型」
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>;
}
}
複製程式碼
上邊的程式碼通過 mapping
將 Demo
這個元件的 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 能夠實現類似 ng
或 vue
的繫結效果。還是前邊小節中的模型,我們來稍微改動一下元件的程式碼
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
的值是一個「繫結表示式字串」,繫結表示式執行的 scope
是 model
而不是 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-scope
的 attribute
,通過它能改變要繫結的 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-scope
將 input
的繫結上下文物件宣告為當前迴圈變數 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 拿到值」的元件,通過如上的程式碼就能完成包裝了。
對於有 onChange
和 value
的這類文字輸入元件,因為 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
最終的狀態是什麼,並在函式中返回。再次,在「事件處理函式」中通過繫結的「模型資料的型別」決定將什麼值回寫到模型中。
通過「屬性處理函式」和「事件處理函式」幾乎就能將任意的自定義元件轉換為「可繫結元件」了。
另外,對於常見的 CheckBox
和 Radio
型別的元件 mota 也提供了內建的 opts
配置支援,如果一個自定義元件擁有和「原生 checkbox 一致的屬性和事件模型」,那邊可以直接用簡單的方式去包裝,如下
const MyCheckBox = bindable('checkbox',CheckBox);
const MyRadio = bindable('radio',Radio);
複製程式碼
好了,關於繫結就這些了。