摘要: ## 简述 React 是一个「视图层」的 UI 框架,以常见的 MVC 来讲 React 仅是 View,而我们在编写应用时,通常还需要关注更加重要的 model,对于 React 来讲,我们常常需要一个「状态管理」库。然而,目前大多数针对 React 的状态管理库都是「强依赖」过多的侵入本应该独立的业务模型中,导致「业务逻辑」对应的代码并不能轻易在其它地方重用,往往这些框架还具有「强排它

原文:https://zhuanlan.zhihu.com/p/33778168

简述

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 固有的面向对象编程的知识。

如下示例通过编写一个名为 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 {@autoruntest() {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);

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

文档

  • 快速开始
  • 编写业务模型
  • 将组件属性映射到模型
  • 自执行函数
  • 监听模型变化
  • 将模型数据与表单绑定

链接

  • 开源地址
  • 版本发布日志
  • MIT 开源协议


识别以下二维码,干货

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

  1. java jmf 视屏监控的核心代码_Java中利用JMF编写摄像头拍照程序_java

    我把程序分为两种,有趣的和无趣的,最近做了几个有趣的项目,其中一个,应当就算是摄像头拍照程序了.用于现场拍照,生成照片,主要用到java Media Framework(JMF). 首先到SUN下载最 ...

  2. react本地储存_如何在React项目中利用本地存储

    react本地储存 以及为什么要这么做. 本地存储是现代Web浏览器固有的Web API. 它允许网站/应用程序在浏览器中存储数据(简单和有限),从而使数据在以后的浏览器会话中可用. 在开始学习本教程 ...

  3. 如何在 React 18中 利用Suspense 实现 服务端渲染(SSR)

    概述 React 18 将包括对 其服务器端渲染 (SSR) 性能的架构做了改进.这些改进带来了实质性的效果,是几年来其团队工作的结晶.大多数的改进点都是在幕后进行的,但您需要了解一些选择加入机制,尤 ...

  4. 利用模板化应对ERP业务模型的快速变化

    源宝导读:ERP这类复杂系统中,业务模型是系统功能的核心抽象,但业务模型对于不同的客户会有差异,也会随着业务发展而变化.虽然可以对业务组件进行复用,但客户定制的成本依然较高,本文将讨论如何利用模板化应 ...

  5. 在react工程中使用antd组件时,组件显示语言英文切换的方式

    antd组件所显示的语言是本工程所采用的默认语言,修改方法为在入口文件src/index.js中引入zhCN,然后在render中使用,如下图 import zhCN from 'antd/es/lo ...

  6. Java中利用JMF编写摄像头拍照程序

    摄像头拍照程序了.用于现场拍照,生成照片,主要用到Java Media Framework(JMF). 首先到SUN下载最新的JMF,然后安装. 然后,说一下需求 1. 用摄像头拍照 2. 在文本框输 ...

  7. 如何在React工程中使用JavaScript Barcode SDK创建Web条形码应用

    基于WebAssembly构建的Dynamsoft JavaScript Barcode SDK让Web开发者能够创建适用于浏览器的高性能条码应用.这篇文章分享下如何使用React快速创建一个简单的W ...

  8. linux中利用脚本编写数组,shell脚本编程之数组

    bash仅支持一维数组,类型声明: declare -i    : 表示数值 declare -a  :表示普通数组(默认,可不用声明类型) declare -A :表示关联数组(必须声明类型,bas ...

  9. linux中利用脚本编写数组,Linux基础之bash脚本进阶篇-数组(示例代码)

    数组 什么是数组? 计算机中的数组指的是具有相同类型的若干变量按照一定的顺序组织起来的一种数据类型. 通俗点来说数组就是变量的一种,按现在的术语来说就是变量圈子中的一个,只不过这个变量圈子都是相同类型 ...

最新文章

  1. MySQL中的常见子查询语句
  2. 计算类class的sizeof大小
  3. 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板
  4. android开发模式,Android开发中无处不在的设计模式
  5. 完美解决 keil5.25 某宝Jlink无法使用问题
  6. 一道笔试题(vue,react)
  7. 02 | 服务治理:Nacos 如何实现微服务服务治理
  8. 快速沃尔什变换(FWT)
  9. laravel数组转换为字符串_LeetCode刷题实战108:将有序数组转换为二叉搜索树
  10. android stub,Android:ViewStub
  11. 机电大作业(第一阶段)
  12. 中美高通商标案一审宣判:驳回上海高通索赔1亿请求
  13. Rs—Ri—部分差Rsf部分
  14. 应用 Serverless 化,让业务开发心无旁骛
  15. linux代码布局,Linux内存布局(示例代码)
  16. Vue css最高优先级
  17. Python解压.gz,.zip文件
  18. 多项式乘积求极值点与拐点“比较快速”的方法:沉鱼落雁闭月羞花
  19. Android在线源码网站推荐
  20. linux查看脚本运行进度,实用小脚本: 查看 MLDonkey 下载进度

热门文章

  1. 计算机的定点运算器原理,计算机组成原理第二章第10讲定点运算器的组成.ppt
  2. python绘制3d坐标轴_matplotlib在python上绘制3D散点图实例详解
  3. python静态递归函数_Python递归函数相关
  4. rman打开oracle归档日志,Oracle RAC中使用RMAN管理归档日志
  5. 突发!美国财政部、商务部双双出手制裁!大疆、旷视等8家被列入投资黑名单!34家被拉入实体清单,GPU龙头景嘉微在列!...
  6. 人工智能时代来临,还需要那么多人吗?
  7. 最年轻图灵奖女性得主:谁说女的数学都比男的差
  8. 战胜柯洁战胜不了高中生?DeepMind挑战高中数学题,完败
  9. 基因编辑婴儿追踪:该被编辑的是我们的心
  10. 【福利】囚犯抓绿豆,谁生谁死?