持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情
formily
中后台复杂场景的数据+协议驱动的表单框架,阿里巴巴统一表单解决方案,可以完成复杂表单需求,提供表单设计器。
优势
高性能 字段数据多也能快速相应
跨端 与框架无关,兼容react和vue
生态好 支持antd和element等组件库
协议驱动 json
架构
三部分,
@formily/core 内核:管理表单的校验和联动
@formily/react ui桥接库,接入内核数据实现表单交互效果
@formily/antd 封装了场景化的组件
比较其他产品
自定义组件成本低、性能好、支持动态渲染、开箱即用、支持跨端、开发效率高、视图代码可维护性高、场景化封装能力强,支持预览表单
但是学习成本高,api太多了,比我用过的 x-render复杂多了,写个例子要用好多库
安装
npm i @formily/reactive @formily/core @formily/reactive-react @formily/react @formily/antd ajv less --save
#vite.config.ts
resolve: {
// less文件 引入antd 中 有一个 ~ ,这种写法对esm构建工具不兼容,转化一下
alias: [
{ find: /^~/, replacement: '' }
]
},
// react17 jsx转换器修改 React.createElemtnt() =>jsx()
// vite默认自动用jsx去编译,可能会报错,所以要用古典的转换器
plugins: [react(
{
jsxRuntime: 'classic'
}
)],
css: { // less 用到js了,所以要开
preprocessorOptions: {
less: {//less3.0后 less的javascriptEnabled默认是false
javascriptEnabled: true
}
}
}
-
tsconfig.json
"noImplicitAny": false, //先不用类型
优势策略
字段多
依赖响应式解决方案,构建响应式表单的领域模型实现精确渲染
简单说 就是 表单数据更新的时候,他只会重渲染他对应的那个组件,按需渲染
mvvm
model-view-viewmodel,逻辑和视图分离
view负责维护ui层,viewmodel负责数据视图交互,model就是数据模型
viewmodel数据变化会触发ui更新 #id.value =
ui更新触发viewmodel更新数据模型 onchange
formily提供了view和viewmodel,view是formily/react,viewmodel是@formily/core
响应式
文档:reactive.formilyjs.org/zh-CN/guide…
例子:reactive.formilyjs.org/zh-CN/api/r…
据说是类似mobx,看起来像vue3,参考我写的 vue3-响应式基础篇
流程
代理对象
tracker执行读取对象属性,触发拦截,将当前属性和
tracker绑定修改对象属性时,触发拦截,执行当前属性依赖的
tracker又执行
tracker, 读取对象属性,触发拦截,将当前属性和tracker绑定。。。
observable
主要用于创建不同响应式行为的 observable 对象。
- 例子:修改
obs的值时,会触发执行tracker
import { observable, autorun } from './@formily/reactive';
//observable是用来创建不同的响应式行为的可观察对象的
const obs = observable({ name: '1' });
//reaction 可订阅对象的订阅者
//当tracker函数的执行的时候,如果函数内部有对observable对象的某个属性进行读操作,则会进行依赖收集
//那么当前的reaction就会与属性进行一个绑定,当属性发生了写操作,就会触发tracker重新执行
const tracker = () => {
console.log(obs.name);
}
//autorun可以创建一个自动执行的响应器
//可以接收一个tracker函数,如果函数内闻有消费observable数据,数据发生变化的时候tracker会重新执行
autorun(tracker);
obs.name = '2';
- 源码:
import { observable, autorun } from './@formily/reactive';
// 真正的源码里 currentReaction 是数组
let currentReaction; // 当前正执行的tracker行为
const RawReactionsMap = new WeakMap(); // 收集依赖,结构和vue3的类似 {map:[]
export function observable(value) {
// 劫持代理对象
return new Proxy(value, baseHandler);
}
const baseHandler: any = {
get(target, key) {
const result = target[key];//获取属性的原始值
//当前存在一个响应器,则把此响应器和对象以及属性进行绑定,
//以后当此对象的这个属性发生改变后会重新执行此响应器
// 对属性进行取值时,将此属性和当前正执行的tracker行为 联立起来
if (currentReaction) {
addRawReactionsMap(target, key, currentReaction);
}
return result;
},
set(target, key, value) {
// 写值时,触发这个属性对应的 tracker行为,重渲染相关组件
target[key] = value;
RawReactionsMap.get(target)?.get(key)?.forEach(reaction => {
reaction();
});
return true;
}
}
/**
* 则把此响应器和对象以及属性进行绑定
* @param target 目标对象
* @param key 目标属性
* @param reaction 响应器
*/
function addRawReactionsMap(target, key, reaction) {
const reactionsMap = RawReactionsMap.get(target);
if (reactionsMap) {
const reactions = reactionsMap.get(key);
if (reactions) {
reactions.push(reaction);
} else {
reactionsMap.set(key, [reaction])
}
return reactionsMap;
} else {
const reactionsMap = new Map();
reactionsMap.set(key, [reaction]);
RawReactionsMap.set(target, reactionsMap);
return reactionsMap;
}
}
export function autorun(tracker) {
//reaction被 称为响应器,它也是一个函数,vue3是 currentReaction = tracker ,有点不一样,但差不多
const reaction = () => {
currentReaction = reaction;
tracker();
currentReaction = null;
}
reaction();
}
Observer
- 例子:组件改时,下面的值跟着改,加
Observer只是为了精确渲染,减少多余的渲染
import React from 'react'
import { observable } from '@formily/reactive'
import { Observer } from '@formily/reactive-react'
const obs = observable({
value: 'Hello world',
})
export default () => {
return (
<div>
<div>
<Observer>
{() => (
<input
}}
value={obs.value}
onChange={(e) => {
obs.value = e.target.value
}}
/>
)}
</Observer>
</div>
<Observer>{() => <div>{obs.value}</div>}</Observer>
</div>
)
}
- 源码:
import { useReducer, useRef, useState } from "react";
import { Tracker } from "../reactive";
export function Observer(props) {
//先定义一个可以让组件强行刷新的方法
//const [, forceUpdate] = useReducer(x => x + 1, 0);
const [, forceUpdate] = useState({});
//创建一跟踪的ref
const trackerRef = useRef(null);
if (!trackerRef.current) {
//创建一个手动的跟踪器 下面两种都行
// trackerRef.current = new Tracker(useReducer);
trackerRef.current = new Tracker(() => forceUpdate({}));
}
return trackerRef.current.track(props.children);
}
const baseHandler: any = {
。。。// 这里改了些 优先执行 reaction._scheduler()
set(target, key, value) {
target[key] = value;
RawReactionsMap.get(target)?.get(key)?.forEach(reaction => {
if (typeof reaction._scheduler === 'function') {
reaction._scheduler();
} else {
reaction();
}
});
return true;
}
}
export class Tracker {
constructor(scheduler) {//forceUpdate
this.track._scheduler = scheduler;//调度更新器
}
// 和上面的autorun差不多
track: any = (tracker) => {
currentReaction = this.track;
const result = tracker();
currentReaction = null;
return result;
}
}
字段关联逻辑复杂
怎么处理的?做了模型,里面自带很多属性和方法,做了formpath方便赋值
领域模型
针对表单做的领域模型
https://core.formilyjs.org/zh-CN/api/entry/create-form
import { createForm } from '@formily/core'
const form = createForm({
initialValues: {
say: 'hello',
},
})
https://core.formilyjs.org/zh-CN/api/models/field#%E5%B1%9E%E6%80%A7
调用createField所返回的 Field 模型
const field = form.createField({name:'xx'})
DDD(领域驱动)
domain-driven design 领域驱动设计。思考问题的方法论,用于对实际问题建模
以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,把概念设计成一个领域模型。用代码实现
form:{值、是否可见、提交} 表单
field:{值、是否可见、设置值} 字段
路径 FormPath
core.formilyjs.org/zh-CN/api/e…
- 例子
import { FormPath } from '@formily/core';
const target = { array: [] };
//点路径
FormPath.setIn(target, 'a.b.c', 'dotValue');
console.log(FormPath.getIn(target, 'a.b.c'));
//下标
FormPath.setIn(target, 'array.0.d', 'dValue');
console.log(FormPath.getIn(target, 'array.0.d'));
//解构在前后端 数据结构不一致的情况下特别方便
FormPath.setIn(target, 'parent.[f,g]', [1, 2]);
console.log(target); //{a:{b:{c:'dotValue'}},array[{d:'dValue'}],parent:{f:1,g:2}}
生命周期
响应式和路径系统是一个黑盒,怎么实现在某个过程阶段完成一些自定义逻辑
这里面有一堆:该有的都有 有表单的,有字段的
core.formilyjs.org/zh-CN/api/e…
协议驱动
json-schema,可以跨端适配。简单搞笑描述表单逻辑。
但只适合描述数据字段,所以要扩展 去描述ui
扩展
DSL、领域特定语言、domain specific language、具有受限表达性的一种计算机程序语言,受限于领域
用 x-*表示ui样式
{type:'string',
'x-component':'Input', //ui 组件
三种表单渲染
jsx
不推荐
import { createForm } from '@formily/core';
import { Field } from '@formily/react';
import 'antd/dist/antd.css';
import { Form, FormItem, Input, NumberPicker } from '@formily/antd';
//纯JSX
const form = createForm();
function App() {
return (
<Form form={form} labelCol={6} wrapperCol={5}>
<Field
name="name"
title="姓名"
required
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<Field
name="age"
title="年龄"
required
component={[NumberPicker, {}]}
decorator={[FormItem, {}]}
/>
</Form>
)
}
export default App
json-schema
代码少的时候,推荐,很长的时候不是很推荐
import { createForm } from '@formily/core';
import { Field, createSchemaField } from '@formily/react';
import 'antd/dist/antd.css';
import { Form, FormItem, Input } from '@formily/antd';
//纯JSX
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem, Input
}
});
const schema = {
type: 'object',
properties: {
name: {
title: '姓名',
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input'
},
email: {
title: '邮箱',
type: 'string',
required: true,
'x-validator': 'email',
'x-decorator': 'FormItem',
'x-component': 'Input'
}
}
}
function App() {
return (
<Form form={form} labelCol={6} wrapperCol={5}>
<SchemaField schema={schema} />
</Form>
)
}
export default App
markup schema
标注schema,其实也差不多,2.0新添加的,比较推荐
<Form form={form} labelCol={6} wrapperCol={5}>
<SchemaField.String
name = 'name'
title='xx'
required
x-component='Input'
x-component-props={{}}
x-decorator='FormItem'
/>
</Form>
联动校验
react.formilyjs.org/zh-CN/api/s…
effects或者x-reactions里实现
主动联动
reactions包含target时
type SchemaReaction<Field = any> =
| {
dependencies?: string[] | Record<string, string> //依赖的字段路径列表,只能以点路径描述依赖,支持相对路径,如果是数组格式,那么读的时候也是数组格式,如果是对象格式,读的时候也是对象格式,只是对象格式相当于是一个alias
when?: string | boolean //联动条件
target?: string //要操作的字段路径,支持FormPathPattern路径语法,注意:不支持相对路径!!
effects?: SchemaReactionEffect[] //主动模式下的独立生命周期钩子
fulfill?: {
//满足条件
state?: IGeneralFieldState //更新状态
schema?: ISchema //更新Schema
run?: string //执行语句
}
otherwise?: {
//不满足条件
state?: IGeneralFieldState //更新状态
schema?: ISchema //更新Schema
run?: string //执行语句
}
}
- 例子
// 来源 输入123时下面的输入框出现,否则消失
import { createForm } from '@formily/core';
import { Field, createSchemaField } from '@formily/react';
import 'antd/dist/antd.css';
import { Form, FormItem, Input } from '@formily/antd';
//纯JSX
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem, Input
}
});
const schema = {
type: 'object',
properties: {
source: {
title: '来源',
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入'
},
'x-reactions': [
{
'target': 'target', // 主动指向目标
'when': "{{$self.value == '123'}}",
"fulfill": {
'state': {
visible: true
}
},
"otherwise": {
'state': {
visible: false
}
}
}
]
},
target: {
title: '目标',
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入'
}
}
}
}
function App() {
return (
<Form form={form} labelCol={6} wrapperCol={5}>
<SchemaField schema={schema} />
</Form>
)
}
export default App
被动
和上面的效果一样
const schema = {
type: 'object',
properties: {
source: {
title: '来源',
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入'
}
},
target: {
title: '目标',
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入'
},
'x-reactions': [
{
'dependencies': ['source'],
'when': "{{$deps[0] == '123'}}",
"fulfill": {
'state': {
visible: true
}
},
"otherwise": {
'state': {
visible: false
}
}
}
]
}
}
}
effect
知道个大概行了,真正用再去看
挂载的时候还有更改的时候去判断。
const form = createForm({
effects() {
onFieldMount('target', (field: any) => {
form.setFieldState(field.query('target'), (targetState) => {
if (field.value === '123') {
targetState.visible = true;
} else {
targetState.visible = false;
}
});
});
onFieldValueChange('source', (field: any) => {
form.setFieldState(field.query('target'), (targetState) => {
if (field.value === '123') {
targetState.visible = true;
} else {
targetState.visible = false;
}
});
});
}
});