基于 Designable 开发 Taro小程序前端页面可视化搭建工具
预览地址
可视化设计器(体积很大,注意流量,最好用PC打开) lowcode-designable-taro-react.vercel.app demo H5(按 F12 切换设备仿真) lowcode-designable-taro-react-mobile.vercel.app 需要VPN
使用演示
todoList
制作表单
联动逻辑 配置响应器
配置图标
目录介绍
├─ packages
├─ editor: 基于 `Designable` + `Formily.js` 实现的页面可视化搭建设计器,使用 `rspack` 构建,并做了兼容Taro组件H5渲染处理
├─ mobile: Taro项目demo例子
├─ ui: 使用 `@nutui/nutui-react-taro` 组件库做的适配formily的组件
项目启动
依赖安装 本项目用了pnpm去做monorepo 根目录下
npm i -g pnpm
pnpm
Taro Demo运行 packages/mobile 目录下 编译微信小程序或淘宝(支付宝)小程序、h5
pnpm dev:weapp
pnpm dev:alipay
pnpm dev:h5
可视化设计器启动
packages/editor 目录下
packages/editor/start.js 中可修改 Taro Demo 地址
npm start
介绍文章
目前组件比较少,如有需要上生产建议按自身业务搭一套
- Formily.js 表单解决方案
- Formily.js官网
- Formily学习笔记
- alibaba/designable
- 跟Formily.js是同一个作者,目前不维护了,这个fork @pind/designable 功能更新一点
- 没有文档,可以看一些非官方文章或视频教程
- 表单设计器开发指南
- Designable 应用和源码浅析
- Designable其他解读文章
设计器目录详细介绍
├─ editor
├─ src
├─ common 一些组件
├─ components 物料组件
├─ designable designable源代码copy
├─ designable-core 核心逻辑
├─ designable-formily-setters 右侧属性配置栏中复杂属性配置组件
├─ designable-formily-transformer designable的TreeNode与formily的Schema格式互转方法
├─ designable-react 设计器界面组件
├─ designable-shared 通用方法
├─ hooks/useDropTemplate 拖拽物料组件后的处理
├─ locales 国际化配置
├─ schemas 物料组件右侧属性配置栏配置
├─ service 保存页面配置方法
├─ widgets 一些组件设计器界面组件
├─ app.tsx 设计器主界面
├─ index.tsx 入口
前端可视化搭建与designable
已下内容可以结合 github.com/pindjs/desi… formily/antd 目录下的 start 命令启动
designable介绍
组件化搭建领域抽象的最好的搭建引擎,与formily同样的配方,不同的只是解决的不同问题,作为底层搭建引擎,它该有的能力都有,最基础的拖拽,就支持了很多形态的,比如,多选拖拽,跨区域拖拽,跨工作区拖拽,跨iframe拖拽,还有多选,快捷键多选,shift/ctrl加点击交集化多选,还有基于鼠标形态切换的选区式多选,再说说扩展性,它本身内核是一个框架无关的内核,只负责管理模型状态,然后我们想要扩展ui的话,只需要替换ui组件即可,designable本身提供了一系列开箱即用的ui组件,且是绝对遵循组合模式的方案,不搞黑盒插件模式,你想用就用它,不想用就替换它,因为组件本身是无状态的,状态都在内核中管理,所以这就使得了designable的扩展性,极其的强
如上 designable 开源库作者所述,designable 是一个设计器引擎,提供拖拽搭建能力。
我们可以用它来往上层封装出具体产品,比如表单设计器、低代码平台。
前端页面可视化搭建与低代码
首先厘清一下本文的开发范围。一般市面上推出的低代码产品会包含 界面可视化搭建、后端数据存储、应用管理发布等功能,偏向零代码方案面向非开发者;但如 lowcode-engine 阿里低代码引擎文档中所说,低代码本身也不仅仅是为技术小白准备的。在实践中,低代码因为通过组件化、模块化的思路让业务的抽象更加容易,而且在扩展及配置化上带来了更加新鲜的模式探索,技术人员的架构设计成本和实施成本也就降了很多。作为前端开发者,我们接下来先重点关注 web前端页面可视化搭建 如何开发。
低代码渲染有一个简单的公式
按这个公式的理解,可视化搭建中有三个角色
组件库。前端展示组件,不利用 JSONSchema 也可以像antd一样普通的用代码展示出来。协议和渲染器。渲染器根据协议解析 JSONSchema 最终展示出组件。可视化设计器。基于渲染器展示界面,加入拖拽和配置能力,高效产出 JSONSchema。
可以说 组件库 和 渲染器 是基础,有了这两消费端已经可以进行渲染了,可视化设计器 是锦上添花。
渲染器 大概要做以下内容:
- 获取源码组件
- 解析组件的 props
- 获取组件的 children
- 保留并传入上下文,包括循环上下文,插槽上下文等;
- 节点更新,当参数变化时需要更新对应的节点
协议、渲染器 与 组件库
designable 本身只提供拖拉拽等能力,协议、渲染器 主要依赖 formily表单解决方案 的协议驱动能力(标准JSON-Schema)
组件适配协议,要基于@formily的api进行改造,适配协议后好处就是我们可以依赖 Formily MVVM的能力,不止可以渲染页面,还可以低成本地 支配 页面,如 表单校验、异步数据源、页面联动逻辑,最简单的例子是根据用户行为决定一个组件的显示和隐藏,就这么一个简单的需求很多低代码产品都不支持。
怎么接入可以参考@formily/antd
如果是使用第三方组件库,那么根据主框架是 react 或者 vue 可以分别用 @formily/react / @formily/vue UI桥接层。
formily协议驱动
本项目的H5和小程序动态渲染最核心的依赖是 Formily.js 的 JSON Schema 渲染能力
先可看官方文档
协议驱动简单介绍
Schema协议详细介绍
以todolist中每一项任务右边的删除按钮为示例
{
"type": "void", // 类型 void类型表单项就是没有关联表单数据
"title": "Icon", // 标题 一般标题用于显示在FormItem(x-decorator 字段 UI 包装器组件)
"x-component": "Icon", // 字段 UI 组件,对应在SchemaField创建时传入的组件,SchemaField 组件是专门用于解析JSON-Schema动态渲染表单的组件
// https://react.formilyjs.org/zh-CN/api/components/schema-field
"x-component-props": { // 字段 UI 组件属性,就是业务组件Props能拿到的参数
"iconName": "check-disabled",
"style": { // 组件样式
},
"eventsConfig": { // 本项目实现的事件配置
// 点击事件表达式
// $开头的变量是内置表达式作用域,主要用于在表达式中实现各种联动关系
// https://react.formilyjs.org/zh-CN/api/shared/schema#%E5%86%85%E7%BD%AE%E8%A1%A8%E8%BE%BE%E5%BC%8F%E4%BD%9C%E7%94%A8%E5%9F%9F
"scriptClick": "{{ () => { console.log($index, $array); $array.remove($index)} }}"
}
},
"x-designable-id": "956d01mudrx",
"x-index": 1
}
开发自己的formily组件
本项目使用 @nutui/nutui-react-taro
nutui.jd.com/taro/react/…
可参考packages/ui/src/components目录
formily的字段模型核心包含了两类字段模型:数据型字段和虚数据型字段 数据型字段(Field),核心是负责维护表单数据(表单提交时候的值)。 虚数据型字段(VoidField),你可以理解为它就是一个阉割了数据维护能力的 Field,所以它更多的是作为容器维护一批字段的 UI 形式。 字段模型
在ui/src/components目录中,Widget开头的组件和Button是VoidField,只用来展示UI的,不跟表单数据做关联,
其余的像 CheckBox、DatePicker、Input组件是跟表单数据做关联的,用 @formily/react 中的 connect, mapProps 方法包装组件来连接表单,最基础的数据型组件要求Props有 value 和 onChange
Form组件
Form组件是地基,用 @formily/react 中的 FormProvider 组件接收一个Form实例,为children提供formily表单context。
本项目里ui/src/components里面 实现了Form组件和FormPage组件,
FormPage组件除了formily提供的能力外就是一个Taro的View组件,
Form组件则多了nutui Form组件的样式
FormPage组件代码如下
import React, { createContext, useContext } from 'react'
import { Form as FormType, ObjectField } from '@formily/core'
import {
ExpressionScope,
FormProvider,
JSXComponent,
useParentForm,
} from '@formily/react'
import { View } from '@tarojs/components'
import { PreviewText } from '../PreviewText'
export const FormPage = ({
form,
component,
previewTextPlaceholder,
className,
style,
children,
}) => {
const top = useParentForm()
// 重要的是这里 我们的Form组件就简单的用Taro的View组件包住子组件渲染
// ExpressionScope是用context来给 json-schema 表达式传递局部作用域,我们可以用它当做数据源
// PreviewText.Placeholder也是一个context 给预览态显示文本一个缺省值,目前也不重要
const renderContent = (_form: FormType | ObjectField) => (
<ExpressionScope value={{ $$form: _form }}>
<PreviewText.Placeholder value={previewTextPlaceholder}>
<View className={className} style={style}>
{children}
</View>
</PreviewText.Placeholder>
</ExpressionScope>
)
if (form)
// 最重要的是这里,有FormProvider才能提供fomily在react组件中的一系列能力
return <FormProvider form={form}>{renderContent(form)}</FormProvider>
if (!top) throw new Error('must pass form instance by createForm')
return renderContent(top)
}
export default FormPage
FormItem
nutui 提供了一些表单组件
如图所见FormItem的作用就是显示label、必填、校验文案等,并且让表单布局更加美观,我们需要混入formily能力。
我们要改造一下FormItem的最外层,要让designable属性能够挂到dom上,并且阉割掉原来UI库有关Form的功能,化为己用。
用 @formily/react 的 connect,mapProps 来让FormItem组件可以链接到表单
UI组件适配formily
组件适配formily最简单的处理的话只需要用connect包裹 需要修改表单field属性映射到组件props的话就要使用mapProps react.formilyjs.org/zh-CN/api/s…
import React from 'react'
import { connect, mapProps, mapReadPretty } from '@formily/react'
import { Input as Component } from '@nutui/nutui-react-taro'
import {
InputProps,
} from '@nutui/nutui-react-taro/dist/types/index'
import { PreviewText } from '../PreviewText'
import { typePropsFields } from '../type'
type typeProps = typePropsFields & InputProps & {
clearIcon: typeIconImageProps
}
import { getIconImageConfig, typeIconImageProps } from '../Icon/IconImage'
export const Input = connect(
({ clearIcon, ...props }: typeProps) => {
// 图标配置封装
const propNames = ['clearIcon']
const IconImageConfig = getIconImageConfig(propNames, {
clearIcon,
})
if (IconImageConfig.noActiveIcon) {
IconImageConfig.icon = IconImageConfig.noActiveIcon
delete IconImageConfig.noActiveIcon
}
if (props.type === 'number') {
const onChange = props.onChange
props.onChange = (val) => {
onChange(Number(val))
}
}
return <Component {...props}></Component>
},
mapProps((props, field) => {
return {
...props,
}
}),
mapReadPretty(PreviewText.Input)
)
formily的Field模型里像value、onChange、disabled等表单属性跟大多数UI组件库是适配的,nutui的input组件可以配置clearIcon,我们可以做额外的适配。
SchemaField
最后我们用 createSchemaField 注册一下适配好的UI组件,创建一个用于解析JSON-Schema动态渲染表单的组件,并
在本项目 packages/ui/src/components/SchemaField.ts中,创建SchemaField并导出在设计器和实际项目中使用
import { createSchemaField } from '@formily/react'
import { ArrayViews } from './ArrayViews'
import { Button } from './Button'
import { Checkbox } from './Checkbox'
import { DatePicker } from './DatePicker'
import { Form } from './Form'
import { FormItem } from './FormItem'
import { Icon } from './Icon'
import { Image } from './Image'
import { Input } from './Input'
import { InputNumber } from './InputNumber'
import { Radio } from './Radio'
import { Range } from './Range'
import { Rate } from './Rate'
import { Switch } from './Switch'
import { Text } from './Text'
import { TextArea } from './TextArea'
import { WidgetBase } from './WidgetBase'
import { WidgetCell } from './WidgetCell'
import { WidgetCellGroup } from './WidgetCellGroup'
import { WidgetList } from './WidgetList'
import { WidgetPopup } from './WidgetPopup'
export const SchemaField = createSchemaField({
components: {
ArrayViews,
Button,
Checkbox,
DatePicker,
Form,
FormItem,
Icon,
Image,
Input,
InputNumber,
Radio,
Range,
Rate,
Switch,
Text,
TextArea,
WidgetBase,
WidgetCell,
WidgetCellGroup,
WidgetList,
WidgetPopup,
},
})
组件库准备好了之后,我们可以选择用 rollup 打包,也可以选择在项目中直接使用 tsx 文件。
designable可视化设计器使用Taro组件
以下内容可参考本项目 packages/editor 目录
由于 Taro 跨端的特性,让组件库在 h5 环境下展示是一定可以的,不过有两种方案:
- 用完整的Taro项目,接入designable API和组件,适配好在PC上的展示 (这种方式打包较慢,要Taro4.0提供vite打包后才快 可参考仓库github.com/SHRaymondJ/…
- 设计器适配部分Taro在h5中的逻辑,不使用Taro打包
本项目使用方案二
首先 @tarojs/components 使⽤了 Stencil 去实现了⼀个基于 WebComponents 且遵循微信⼩程序规范的组件库,用 reactify-wc 让React项目中能够使用 WebComponent,stenciljs 打包的组件产物中有 defineCustomElements,调用一下才可以把 WebComponents 注册到浏览器中
我们在设计器项目中需要把该方法导出来用一下,还需要引入Taro组件样式
其余H5页面处理可以看
node_modules/@tarojs/taro-loader/lib/h5.js
在设计器 main.tsx 中
import React from 'react'
import { findDOMNode, render, unstable_batchedUpdates } from 'react-dom'
import ReactDOM, { createRoot } from 'react-dom/client'
import { defineCustomElements } from '@tarojs/components/dist/esm/loader.js'
import { createReactApp } from '@tarojs/plugin-framework-react/dist/runtime'
import { createH5NativeComponentConfig } from '@tarojs/plugin-framework-react/dist/runtime'
import App from './app'
// Taro H5 初始化
Object.assign(ReactDOM, { findDOMNode, render, unstable_batchedUpdates }) // Taro H5对于React18的处理
defineCustomElements(window) // 注册WebComponents组件
const appObj = createReactApp(App, React, ReactDOM, {
appId: 'root'
})
createH5NativeComponentConfig(null, React, ReactDOM) // Taro页面管理逻辑和Hooks初始化
appObj.onLaunch()
打包配置参考 plugin-framework-react 这个taro包中的处理
在 'node_modules/@tarojs/plugin-framework-react/dist/index.js' 文件中,有个 modifyH5WebpackChain 方法来处理编译到H5时的webpack配置
function modifyH5WebpackChain(ctx, framework, chain) {
var _a;
setLoader$1(framework, chain);
setPlugin(ctx, framework, chain);
const { isBuildNativeComp = false } = ((_a = ctx.runOpts) === null || _a === void 0 ? void 0 : _a.options) || {};
const externals = {};
if (isBuildNativeComp) {
// Note: 该模式不支持 prebundle 优化,不必再处理
externals.react = {
commonjs: 'react',
commonjs2: 'react',
amd: 'react',
root: 'React'
};
externals['react-dom'] = {
commonjs: 'react-dom',
commonjs2: 'react-dom',
amd: 'react-dom',
root: 'ReactDOM'
};
if (framework === 'preact') {
externals.preact = 'preact';
}
chain.merge({
externalsType: 'umd'
});
}
chain.merge({
externals,
module: {
rule: {
'process-import-taro-h5': {
test: /taro-h5[\\/]dist[\\/]api[\\/]taro/,
loader: require.resolve('./api-loader')
}
}
},
});
chain.merge({
externals,
module: {
rule: {
'process-import-taro-harmony-hybrid': {
test: /plugin-platform-harmony-hybrid[\\/]dist[\\/]api[\\/]apis[\\/]taro/,
loader: require.resolve('./api-loader')
}
}
},
});
}
function setLoader$1(framework, chain) {
function customizer(object = '', sources = '') {
if ([object, sources].every(e => typeof e === 'string'))
return object + sources;
}
chain.plugin('mainPlugin')
.tap(args => {
args[0].loaderMeta = lodash.mergeWith(getLoaderMeta(framework), args[0].loaderMeta, customizer);
return args;
});
}
function setPlugin(ctx, framework, chain) {
var _a, _b;
const config = ctx.initialConfig;
const webpackConfig = chain.toConfig();
const isProd = webpackConfig.mode === 'production';
if (!isProd && ((_b = (_a = config.h5) === null || _a === void 0 ? void 0 : _a.devServer) === null || _b === void 0 ? void 0 : _b.hot) !== false) {
// 默认开启 fast-refresh
if (framework === 'react') {
chain
.plugin('fastRefreshPlugin')
.use(require('@pmmmwh/react-refresh-webpack-plugin'));
}
else if (framework === 'preact') {
chain
.plugin('hotModuleReplacementPlugin')
.use(require('webpack').HotModuleReplacementPlugin);
chain
.plugin('fastRefreshPlugin')
.use(require('@prefresh/webpack'));
}
}
}
所以设计器打包需要额外添加以下Taro配置
export default {
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@tarojs/components$': '@tarojs/components/dist-h5/react', // taro3.6及以上为 @tarojs/components/lib/react
'@tarojs/taro': '@tarojs/taro-h5',
},
},
module: {
rules: [
{
test: /taro-h5[\\/]dist[\\/]index/,
loader: require.resolve(
'@tarojs/plugin-framework-react/dist/api-loader.js'
),
},
...
],
},
...
}
这样就可以获得一个残缺的 Taro h5 React 环境,会有一些api不支持,比如路由跳转。
组件封装物料
@designable/core 提供了两个api
createResource 创建资源基础信息,用于左侧拖拽组件
createBehavior 创建组件的行为,locals、propsSchema 可以描述右侧属性配置栏中可以配置的属性
组件的Behavior,大致就是描述一下组件有哪些属性需要在设计器上配置的,可以配置哪些内容,还有设计器与组件的交互,例如点击、拖拉这个组件会有什么反应。
给组件添加资源,简单的理解就是添加一些在设计器展示的内容,比如需要展示在左边组件区,那就需要一个icon
有了这些配置,组件就变成了
低代码物料
designable里面可以先实现一个Field组件来定义默认的Behavior和Resource
Field.Behavior = createBehavior({
name: 'Field',
selector: 'Field',
designerLocales: AllLocales.Field,
designerProps: {
...behaviorOfResizeAndtranslate,
},
})
用formily的Input组件做designable物料组件,通过 extends: ['Field'] 来继承默认的Behavior和Resource
import React from 'react'
import { Input as component } from 'ui-nutui-react-taro'
import {
createBehavior,
createResource,
} from '@/designable/designable-core/src'
import { DnFC } from '@/designable/designable-react/src'
import { AllLocales } from '../../locales'
import { AllSchemas } from '../../schemas'
import { createFieldSchema } from '../Field'
import { iconimageDesignableConfig } from '../shared'
export const Input: DnFC<React.ComponentProps<typeof component>> = component
const { imgsProperties, imgsLocales } = iconimageDesignableConfig([
{
name: 'clearIcon',
locale: '清除图标',
},
])
const propsSchema = createFieldSchema({
component: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['text', 'number', 'digit', 'idcard'],
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
defaultValue: 'text',
},
},
placeholder: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
align: {
type: 'string',
enum: ['left', 'center', 'right'],
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
defaultValue: 'left',
},
},
maxLength: {
type: 'number',
'x-decorator': 'FormItem',
'x-component': 'NumberPicker',
},
clearable: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
confirmType: {
type: 'string',
enum: [
{
label: '发送',
value: 'send',
},
{
label: '搜索',
value: 'search',
},
{
label: '下一个',
value: 'next',
},
{
label: '前往',
value: 'go',
},
{
label: '完成',
value: 'done',
},
],
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
defaultValue: 'done',
},
},
password: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
...imgsProperties,
},
},
props: {
'component-events-group': [],
},
}) as any
Input.Behavior = createBehavior({
name: 'Input',
extends: ['Field'],
selector: (node) => node.props['x-component'] === 'Input',
designerProps: {
propsSchema,
defaultProps: {},
},
designerLocales: {
'zh-CN': {
title: '输入框',
settings: {
'x-component-props': {
type: '输入框类型',
placeholder: 'placeholder',
align: '输入框内容对齐方式',
maxLength: '限制最长输入字符',
clearable: '展示清除Icon',
confirmType: '键盘右下角按钮的文字(小程序)',
password: '是否是密码',
...imgsLocales,
},
},
},
},
})
Input.Resource = createResource({
icon: 'InputSource',
elements: [
{
componentName: 'Field',
props: {
type: 'string',
title: 'Input',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
],
})
如图所见右侧属性配置中
字段属性 是默认的表单属性配置,组件属性部分通常就是针对每个组件额外字段处理部分,在 propsSchema 中定义
designable使用物料
准备预览运行面板,使用 Form组件 和 SchemaField组件 提供运行时渲染能力,与实际消费端的区别是需要用 designable 提供的 transformToSchema 把拖拉拽面板中的组件树转成JSON协议。
import React, { useMemo } from 'react'
import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import {
Input,
...
} from '@formily/antd'
import { Card, Slider, Rate } from 'antd' // 一些简单的布局组件或输入组件,即使不适配也能用起来
import { TreeNode } from '@pind/designable-core'
import { transformToSchema } from '@pind/designable-formily-transformer'
const SchemaField = createSchemaField({
components: {
Input,
Card,
...
},
})
export interface IPreviewWidgetProps {
tree: TreeNode
}
export const PreviewWidget: React.FC<IPreviewWidgetProps> = (props) => {
const form = useMemo(() => createForm(), [])
const { form: formProps, schema } = transformToSchema(props.tree)
return (
<Form {...formProps} form={form}>
<SchemaField schema={schema} />
</Form>
)
}
app.tsx中,把物料组件导入, 放入 ResourceWidget 和 ComponentTreeWidget 两个设计器组件中。
放入 ResourceWidget 是为了在设计器左侧展示可用物料组件
ComponentTreeWidget 则是组件树渲染器,需要注册一下物料组件
<ResourceWidget
title="sources.Inputs"
sources={[Input, InputNumber, TextArea, Checkbox, Radio, Rate, Switch]}
/>
<ResourceWidget
title="sources.Displays"
sources={[Button, Icon, Image, Text]}
/>
<ResourceWidget title="sources.Arrays" sources={[ArrayViews]} />
<ResourceWidget
title="sources.Layouts"
sources={[
Form,
WidgetBase,
WidgetCell,
WidgetCellGroup,
WidgetList,
WidgetPopup,
]}
/>
<ViewPanel type="DESIGNABLE">
{() => (
<ComponentTreeWidget
className="ComponentTreeWidget"
components={{
ArrayViews,
Button,
Checkbox,
Form,
FormPage,
Field,
Icon,
Image,
Input,
InputNumber,
Radio,
Rate,
Switch,
Text,
TextArea,
WidgetBase,
WidgetCell,
WidgetCellGroup,
WidgetList,
WidgetPopup,
}}
/>
)}
</ViewPanel>
<ViewPanel type="JSONTREE" scrollable={false}>
{(tree, onChange) => {
return <SchemaEditorWidget tree={tree} onChange={onChange} />
}}
</ViewPanel>
<ViewPanel type="PREVIEW">
{(tree) => <PreviewWidget tree={tree} />}
</ViewPanel>
Taro小程序H5渲染JSONSchema
适配小程序端
我们开发好的组件库就是基于Taro组件标签写的,所以在Taro项目编译到H5或小程序就能在想要的平台中展示设计好的页面中。
要渲染 可视化搭建设计器 产出 JSONSchema,需要使用 @formily/core + @formily/react 以及组件库中的 SchemaField
但是要用在小程序端最主要有两个问题 1.在PC设计器上,设置组件样式的单位是px,需要转换为rem
这个问题主要是使用 Taro.pxTransform 将以px为单位的数字转为 以rem为单位的字符串,配合正则就可以实现对某段style进行转换
const pxToRem = (str) => {
const reg = /(\d+(\.\d*)?)+(px)/gi
return String(str).replace(reg, function (x) {
const val = x.replace(/px/gi, '')
return Taro.pxTransform(Number(val))
})
}
对JSONSchema进行递归转换单位就解决了这个问题
2.formily Schema 联动协议需要动态执行JS代码,但小程序不能使用 Function/eval,需要一个JS写的JS解释器
举一个上篇文章中的例子,表单中有字段为a的输入框和字段为b的输入框,单a的值为 'hidden' 时把b隐藏掉
那么这里需要动态执行的代码(表达式)是
$form.values.a !== 'hidden'
在 @formily/json-schema 中源码是使用 new Function 去执行的,把动态代码置于 formily作用域 运行以获得访问表单数据的能力
var Registry = {
silent: false,
compile: function (expression, scope) {
if (scope === void 0) { scope = {}; }
if (Registry.silent) {
try {
return new Function('$root', "with($root) { return (".concat(expression, "); }"))(scope);
}
catch (_a) { }
}
else {
return new Function('$root', "with($root) { return (".concat(expression, "); }"))(scope);
}
},
};
@formily/json-schema 中导出的 Schema 里面 registerCompiler 的方法允许使用者注册自己的逻辑
本项目用JS写的JS解释器去运行动态代码
export function miniCompiler(expression, scope, isStatement?) {
if (scope === void 0) {
scope = {}
}
const scopeKey = Object.keys(scope).filter((str) => str.includes('$'))
scopeKey.forEach((key) => {
const reg = new RegExp(`\\${key}`, 'g')
expression = expression.replace(reg, 'scope.' + key)
})
const bridge = { current: null }
const context = vm.createContext({ bridge, expression, scope, console })
try {
if (isStatement) {
vm.runInContext(`${expression} `, context)
return
}
vm.runInContext(`bridge.current = ${expression} `, context)
} catch (err) {
console.error(err)
}
return bridge.current
}
页面渲染
- 用
@formily/core导出的createForm创建form实例 - 用fomrily组件库中的
Form组件(实际上用了@formily/react导出的FormProvider)桥接form实例与UI - 用fomrily组件库中的
SchemaField组件(实际上用了@formily/react导出的createSchemaField)去渲染JOSNSchema,JOSNSchema使用前先处理一遍style单位
具体参考 packages/mobile/src/pages/index/index.tsx