V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
houfeng
V2EX  ›  前端开发

在 React 工程中利用 Mota 编写面向对象的业务模型

  •  
  •   houfeng · 2018-02-06 18:09:07 +08:00 · 1701 次点击
    这是一个创建于 2512 天前的主题,其中的信息可能已经有所发展或是发生改变。

    简述

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

    好了,关于绑定就这些了。

    文档

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1082 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 19:00 · PVG 03:00 · LAX 11:00 · JFK 14:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.