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 固有的面向对象编程的知识。数组
以下示例经过编写一个名为 User
的 class
建立了一个「用户模型」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>; } }
上边的代码经过 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);
好了,关于绑定就这些了。