├─packages├─editor:基于`Designable`+`Formily.js`实现的页面可视化搭建设计器,使用`rspack`构建,并做了兼容Taro组件H5渲染处理├─mobile:Taro项目demo例子├─ui:使用`@nutui/nutui-react-taro`组件库做的适配formily的组件项目启动依赖安装本项目用了pnpm去做monorepo根目录下
pnpmdev:weapppnpmdev:alipaypnpmdev:h5可视化设计器启动packages/editor目录下packages/editor/start.js中可修改TaroDemo地址
组件化搭建领域抽象的最好的搭建引擎,与formily同样的配方,不同的只是解决的不同问题,作为底层搭建引擎,它该有的能力都有,最基础的拖拽,就支持了很多形态的,比如,多选拖拽,跨区域拖拽,跨工作区拖拽,跨iframe拖拽,还有多选,快捷键多选,shift/ctrl加点击交集化多选,还有基于鼠标形态切换的选区式多选,再说说扩展性,它本身内核是一个框架无关的内核,只负责管理模型状态,然后我们想要扩展ui的话,只需要替换ui组件即可,designable本身提供了一系列开箱即用的ui组件,且是绝对遵循组合模式的方案,不搞黑盒插件模式,你想用就用它,不想用就替换它,因为组件本身是无状态的,状态都在内核中管理,所以这就使得了designable的扩展性,极其的强
低代码渲染有一个简单的公式
按这个公式的理解,可视化搭建中有三个角色
可以说组件库和渲染器是基础,有了这两消费端已经可以进行渲染了,可视化设计器是锦上添花。
渲染器大概要做以下内容:
以todolist中每一项任务右边的删除按钮为示例
在ui/src/components目录中,Widget开头的组件和Button是VoidField,只用来展示UI的,不跟表单数据做关联,其余的像CheckBox、DatePicker、Input组件是跟表单数据做关联的,用@formily/react中的connect,mapProps方法包装组件来连接表单,最基础的数据型组件要求Props有value和onChange
Form组件是地基,用@formily/react中的FormProvider组件接收一个Form实例,为children提供formily表单context。本项目里ui/src/components里面实现了Form组件和FormPage组件,FormPage组件除了formily提供的能力外就是一个Taro的View组件,Form组件则多了nutuiForm组件的样式
FormPage组件代码如下
如图所见FormItem的作用就是显示label、必填、校验文案等,并且让表单布局更加美观,我们需要混入formily能力。我们要改造一下FormItem的最外层,要让designable属性能够挂到dom上,并且阉割掉原来UI库有关Form的功能,化为己用。用@formily/react的connect,mapProps来让FormItem组件可以链接到表单
最后我们用createSchemaField注册一下适配好的UI组件,创建一个用于解析JSON-Schema动态渲染表单的组件,并在本项目packages/ui/src/components/SchemaField.ts中,创建SchemaField并导出在设计器和实际项目中使用
以下内容可参考本项目packages/editor目录
由于Taro跨端的特性,让组件库在h5环境下展示是一定可以的,不过有两种方案:
本项目使用方案二
在设计器main.tsx中
importReactfrom'react'import{findDOMNode,render,unstable_batchedUpdates}from'react-dom'importReactDOM,{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'importAppfrom'./app'//TaroH5初始化Object.assign(ReactDOM,{findDOMNode,render,unstable_batchedUpdates})//TaroH5对于React18的处理defineCustomElements(window)//注册WebComponents组件constappObj=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配置
exportdefault{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'),},...],},...}这样就可以获得一个残缺的Taroh5React环境,会有一些api不支持,比如路由跳转。
@designable/core提供了两个apicreateResource创建资源基础信息,用于左侧拖拽组件createBehavior创建组件的行为,locals、propsSchema可以描述右侧属性配置栏中可以配置的属性
designable里面可以先实现一个Field组件来定义默认的Behavior和Resource
Field.Behavior=createBehavior({name:'Field',selector:'Field',designerLocales:AllLocales.Field,designerProps:{...behaviorOfResizeAndtranslate,},})用formily的Input组件做designable物料组件,通过extends:['Field']来继承默认的Behavior和Resource
准备预览运行面板,使用Form组件和SchemaField组件提供运行时渲染能力,与实际消费端的区别是需要用designable提供的transformToSchema把拖拉拽面板中的组件树转成JSON协议。
但是要用在小程序端最主要有两个问题1.在PC设计器上,设置组件样式的单位是px,需要转换为rem
这个问题主要是使用Taro.pxTransform将以px为单位的数字转为以rem为单位的字符串,配合正则就可以实现对某段style进行转换
constpxToRem=(str)=>{constreg=/(\d+(\.\d*))+(px)/gireturnString(str).replace(reg,function(x){constval=x.replace(/px/gi,'')returnTaro.pxTransform(Number(val))})}对JSONSchema进行递归转换单位就解决了这个问题
那么这里需要动态执行的代码(表达式)是
$form.values.a!=='hidden'在@formily/json-schema中源码是使用newFunction去执行的,把动态代码置于formily作用域运行以获得访问表单数据的能力
varRegistry={silent:false,compile:function(expression,scope){if(scope===void0){scope={};}if(Registry.silent){try{returnnewFunction('$root',"with($root){return(".concat(expression,");}"))(scope);}catch(_a){}}else{returnnewFunction('$root',"with($root){return(".concat(expression,");}"))(scope);}},};@formily/json-schema中导出的Schema里面registerCompiler的方法允许使用者注册自己的逻辑本项目用JS写的JS解释器去运行动态代码
exportfunctionminiCompiler(expression,scope,isStatement){if(scope===void0){scope={}}constscopeKey=Object.keys(scope).filter((str)=>str.includes('$'))scopeKey.forEach((key)=>{constreg=newRegExp(`\\${key}`,'g')expression=expression.replace(reg,'scope.'+key)})constbridge={current:null}constcontext=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)}returnbridge.current}