关于carverry
整体设计
这是一套比较激进的可视化搭建方案(激进体现在适用场景和技术栈的选择上),核心思想就是极致的内聚;因此可以看到除了必要的组件配置,其他所有相关的配置都砍掉了,理由就是这些配置完全可以通过内聚使其成为内部参数,而减少不必要的参数暴露(这一点可以参照现有的一些比较流行的可视化搭建方案,它们都尽可能实现对组件框架的所有语法的完全映射,以便达到最大的自由度);从可视化搭建业务的角度来说,一般就是封装的程度越高,其自由度越低,而搭建效率就越高[1];而这套设计就是为了提高业务搭建效率,所以自然就倾向于内聚;
预期适用场景
- 面向人群:前端编程人员;
- 预期解决的问题:减少花在拼接组件的过程中写胶水代码的时间,更加直接直观地去搭积木;
- 内聚:业务和组件/代码区块的内聚程度越高,复用和可视化搭建的效率也就越高;
- 面向项目:
Vue3
(≥3.2
,用于支持setup
模式) +TS
;
数据模型
从数据模型就不难发现所谓的激进之处在哪,差不多砍掉了大部分的语法映射,只保留组件最最核心的部分(即没法通过内聚合并参数来解决);为何可以做到只保留这些属性就可以实现完全的映射?因为其他诸如生命周期,指令等在形成业务逻辑时,完全不必要暴露出来,直接在内部进行处理,而仅仅需要注入完全需要外部数据的部分;
关于变量的注入
可以看到,变量的配置分为两个部分:
- 位置:这里的位置就是指文件的位置,可以是本地文件路径,也可以是其他位置(诸如仓库地址,包等等);这样就可以不局限于从本地获取;
- 标识符:不管是何种类型的变量,始终都是通过标识符进行暴露和定位的;
因此,通过这两部分信息理论上可以从任何地方获取变量(数据);
技术栈的选择
技术栈的选择则又是激进的另一部分,很多都是全新的,不太考虑向下兼容性(因为不存在历史包袱);
@carverry/core
- ts-node:这是一个
node
端的TS
执行器,可以达到用TS
写node
代码而不预先编译就执行的目的; - farrow:这是一个基于
TS
编写、类型友好、开箱即用,轻量简单的node
服务端框架;配合ts-node/TS
使用良好; - 命令行相关工具:
inquirer
、commander
、chalk
; - lowdb:基于
JSON
文件做数据管理的轻量级数据库工具; - ws:简单轻便的
node
端websocket
服务搭建工具;
@carverry/app
可视化搭建应用的技术栈选择可以说是无关紧要了,毕竟只要最后的输出产物可以运行即可……所以采用了目前最顺手的一套:
Vue3
TS
element-plus
vite
规范设计
物料项目
所谓的物料项目,实际上就是给可视化搭建平台提供组件原料的组件库;而为了快速生产满足可视化搭建使用要求的物料,则需要对物料设计一些规范:
- 项目无需编译打包:为了避免重复依赖引入,项目所有的
dependencies
都放到peerDependencies
里面,并且指定包的版本兼容范围;这样做的好处就是把依赖安装全部集中到使用项目本身,避免一些相同包的不同版本出现重复打包;虽说无需编译,但是一些额外的元数据还是需要进行预编译比较好,以便在使用这些物料时无需再额外生成元数据,而这个编译命令都集中在@carverry/core
的cli
里了,所以没啥负担;
- 项目结构:为了方便统一的编译和有明确模式的引入(即在对应项目按照某种模式自动化的引入物料),所以确定一个统一的项目结构是比较明智的;
实际上就是限定了所有源码放在src
,其中物料源码都放在materials
里面,而materials
里面的每个子目录都代表一个具体的组件,至于src
里面其他的文件夹则完全不需要硬性的约束了。具体到单个物料的目录,也有一定的规范:
至于carverry.material.json
文件的作用,其实就是补充物料的一些无法自动获取的元数据及一些配置,目前满足如下的JSON schema
:
- 单元测试:对于公共组件,自然少不了单测,否则无法维持一个可信任、可持续迭代、健壮的组件库;这里选用了vitest作为单测框架,无他,
vite
生态的最佳选择; - 组件文档:采用
storybook
,可以考虑直接部署文档;
命令行设计
为了提升物料相关的开发效率及启动可视化搭建等功能,@carverry/core
提供了一系列cli
命令:
细节
基于workspaces的开发
由于carverry
相关的包有一定的关联性,所以一开始就打算放在一个项目里面进行开发,采用目前流行的分包开发模式,而分包开发目前主要有一种模式:
- monorepo:“Monorepo 是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个 repo。”[2]
而yarn workspaces
本身提供了一种比较便捷的monorepo
开发方式,仅需要增加一些yarn
的配置即可;而其他的包管理器(pnpm/npm
)本身也有类似的配置。
不过由于一开始并不了解monorepo
的管理机制,所以在git
提交上面犯了错误,导致不同包之间的git
提交混合在了一起,所以生成的changelog
版本号十分奇怪……这里就推荐一个最佳实践[3];
基于ts-node的开发
其实一开始基于ts-node/TS
开发倒也没发现太多的限制,但是由于项目中使用了lowdb
这个较为特殊的库,这个库的最新版本(3.0+
)全面采用ESM
模块,不兼容CommonJS
模块,导致使用该库的项目也必须采用ESM
模块;而目前node
的LTS
版本(16)还不全面支持ESM
模块,只是把这个功能放到实验特性里面(作为加载器使用,好在ts-node
完全提供了ESM
模块的加载器);所以基于TS
开发的node
代码索性全部采用更为激进的ESM
模块,不过不需要太担心,ESM
是node
的未来主流,且已知在下一个LTS
版本(18)可以原生全面支持ESM
[4];
目前ESM的限制
- 最明显的就是用
import
替代require
; - 一些内置的系统变量不能直接使用,如:
__dirname
; - 无法再像之前的
CommonJS
模块一样,直接导入json
文件;解决方法可以参考这个——How to import JSON files in ES modules (Node.js) | Stefan Judis Web Development,而后续LTS
版本应该会支持在ESM
模块直接导入json
文件;
关于更多的写法不同,可以参考ts-node
官方给出的指南——ESM support: soliciting feedback · Issue #1007 · TypeStrong/ts-node;
ESM Shebang 执行
在计算领域中,Shebang(也称为Hashbang)是一个由井号和叹号构成的字符序列#!,其出现在文本文件的第一行的前两个字符。 在文件中存在Shebang的情况下,类Unix操作系统的程序加载器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令,并将载有Shebang的文件路径作为该解释器的参数。[5]
Shebang
的作用实际上可以让node
执行bin
文件时,可以按照Shebang
指令指定的解释器和参数运行命令,丰富了脚本的使用场景;而具体到ts-node
,ts-node
完全支持作为Shebang
的解释器来执行TS
文件作为脚本[6];且ts-node
也完全提供了ESM
模块的解释器,因此也可以直接执行ESM
模块的TS
文件作为脚本:
1 |
|
不过有一点需要注意的是,如果要让这个bin
文件可以在任意命令行都可以执行,那么就需要全局安装ts-node
;
关于拖拽的交互
如果将一个组件拖拽放入可视化界面中,如何推测用户要插入的精准位置?这里有一个有意思的思路[7],那就是根据用户插入组件时其鼠标位置命中的最近的一个物料组件(即对应一个配置节点),然后根据距离该组件DOMRect
的边距来得到一个距离最近的边距,得出插入位置的推断:
left/top
:插入该组件之前;right/bottom
:插入该组件之后;
当然,得出这种推断的前提是该容器内布局为正常文档流(从左往右,从上至下);
不同应用上下文之间的事件通信
由于直接使用了本地项目的上下文环境作为可视化预览页面(这样做的好处就是可以直接复用当前项目的vite
配置,无需额外再配置),而可视化搭建应用属于另一个独立的应用,可视化搭建应用通过iframe
来引入这个预览页面,因此势必造成了iframe
跨域的问题;
由于现代浏览器的同源策略,iframe
的跨域会导致很多的限制,除了熟知的网络请求限制,更多的是控制的限制,父级文档无法控制子iframe
文档[8],导致很多交互(DOM
层面)不能在两个上下文之间流通;比如从搭建应用拖拽一个组件原料放入到iframe
中的页面时,正常情况下完全不可能[9],因为跨域限制了父级事件流通到iframe
的DOM
里面,所以只能另找门路;
事件机制
DOM
的事件机制提供了一套可以主动触发事件及构造自定义/原生事件的API
:
- EventTarget.dispatchEvent - Web API 接口参考 | MDN:在指定
DOM
开始触发指定事件 - CustomEvent() - Web APIs | MDN:构造自定义事件
- DragEvent() - Web APIs | MDN:可以构造拖拽相关事件,其他的原生事件只要找到相应的构造函数一样都可以进行构造
因此,有了这套事件机制,只要事件触发端把相应的事件信息发送到要传递的浏览上下文,然后在接收端进行相应事件的构造并触发,就完全可以绕过iframe
跨域的限制,完成事件通信/流转;
那么剩下的就是如何完成两个跨域浏览上下文之间的信息传递了;
信息传递
其实完成不同浏览上下文之间的信息传递的方法不止一种:
websocket
:在不同上下文之间建立socket
连接,就可以很自然地进行双向通信了;所以一开始我很天真的想到了这种办法,而忽视了DOM
本身的机制;- window.postMessage():为不同上下文进行安全通信而生的
DOM API
,其实就是不二选择;
Hit Testing
Hit Testing
,即命中测试,是图形学中的一个专业术语,一般用于描述用户鼠标位置击中当前图形交互界面(GUI
)中的哪个图元(图形物体);而具体到前端,可以理解为用户鼠标位置命中的是哪个DOM
元素(DOM
元素的图形本质就是一个矩形区域);
为啥要做命中测试?因为从父级上下文传递事件的时候,并不知道触发事件对应的是子级上下文的哪个元素,因此需要在子级上下文构造事件的同时对当前浏览上下文做一次命中测试,以便精准地从相应的DOM
元素触发事件;巧的是DOM
刚好提供了命中测试相关的API
:
在使用鼠标位置进行命中测试一定要确保这个鼠标位置是相对于当前document
对象而言的,即确保坐标原点是当前document
对象的左上角;
过程图解
iframe的事件穿透
// TODO
dataset
的使用
为了更精准地进行事件交互,比如弄清组件插入位置所属的配置节点,那么最好给相应的DOM
节点标注上所需的信息,以便更好的处理交互;而要给DOM
标记自定义属性,那么dataset
自然是最佳也是最符合规范的选择。
在 DOM tree 的基础上构建配置树
注:在设计中,一个配置节点就等同于一个物料组件;
胶水代码的输出
在通过可视化搭建得到了相应配置树结构,以及通过填充数据补充变量等细节配置后,如何将配置树转化为对应的Vue
代码?其实很简单,每个配置节点就是一个Vue
组件,只需要将配置转为Vue
组件源码,然后递归处理子级即可;
那么,关键就在于如何将一个配置节点转化为等价的Vue
组件;其实有了setup
模式(需要vue3.2+
)的加持,对组件数据的组装变得异常简单了起来,因为setup
模式的核心就是导入即可使用,无需像以前option
模式或者早期Vue3
的composition API
那样需要专门分配到对应类型的选项里面,如需要分辨导入变量是方法、单纯的变量还是组件等;而setup
模式只需要将变量导入,Vue
框架会根据变量类型自动分配到对应的选项里去,所以说setup
模式就是高级的语法糖,进一步减少了胶水代码,自动帮你进行composition
的处理;
可以说正是有了setup
模式,我才会水到渠成地想到这整套数据模型;那么基于setup
模式,将配置节点输出为Vue
组件源码可以分为两个部分:
script
部分:只需要将配置节点使用到的变量全部引入即可,然后判断是否有子配置节点,如有则递归处理得到子配置节点的源码并引入;template
部分:负责将数据进行填充即可,包括使用子组件;
将script
部分和template
部分进行合并就得到了配置节点对应的Vue
组件源码了,那么最外层的组件自然就是整个配置树的入口了;
第三方 UI 库元数据的转换
注:我个人是很不赞成在需要高内聚的可视化搭建系统直接使用通用组件的,诚然,通用组件可以使得搭建更灵活,但往往只能支撑很简单的业务场景,在复杂业务场景下通用组件的使用效率就很低了;当然,抱着可行性的态度我还是去尝试了一下,毕竟至少可以作为某种回退操作(更广的兼容性);
虽说是第三方UI
库,但跟上述提到的物料库项目一样,不过事实上我们并不需要构造源码文件,因为可以直接引入第三方库,所以针对第三方库,我们只需要提取其组件元数据即可,而在引入的时候做一个路径转换即可;
一般来说,只要一个组件库的项目结构是规范的,那么其组件源码一定是按照某个固定结构进行排列的,因此只需要找到这个结构然后就可以批量进行元数据提取了;这里以element-plus
为例,他们的组件源码放在packages/components
里面,其中里面的每个文件夹都是一个相对独立的组件,而组件的入口文件通常为[component-name]/src/[component-name].vue
:
因此根据这一特征就可以批量的从其源码文件提取对应组件的元数据,不过需要注意的是第三方库可能由于内部高度模块化,而导致一般的从AST
(抽象语法树)解析得到的组件元数据并不完整(如vue-docgen-api
这个插件),这时可以配合编译后的组件数据进行补充,一般来说组件库都会提供编译后的版本,即ES
版本(通常是把组件源码同构地编译为ES5/ES6
这种js
代码),一般这种js
代码就是导出了组件的构造方法,因此引入该构造方法即可完全得到相应的组件运行时数据,这里可以得到完整的组件元数据,只不过类型粒度很粗;
node 代码调试
如果想要调试node
的代码,实际上VSCode
直接提供了工具,可以参考——Debug Node.js Apps using Visual Studio Code;
本地包调试存在的坑
想不起来了……
夭折的方案
异步渲染的坑
在最初的时候,我曾想过直接使用Vue
里面的异步组件[10]来进行预览,一个配置节点对应一个异步组件,而异步组件的内容则借由vite
插件机制来完成;实际上就是用vite plugin
作为dev server
,根据不同的自定义后缀以及挂载参数来相应返回不同的文件源码,然后借由异步组件动态加载,其子配置节点也是递归处理的;
这样做的好处就是可以不必自行搭建dev server
,而是直接借助vite plugin
来实现dev server
的功能,且异步组件用于挂载节点组件的事件恰好不过,方便管理;但实际上这种方案有太多的问题:
- 由于
vite
加载本地模块采用的是get
请求,因此携带的参数长度受限,当页面模块较多时,配置JSON
形成的参数很快就会超过最大长度,即便采用字符压缩也改善不了多少; - 异步组件层级过多,数量较多时性能极其差;
- 异步组件跟
Promise
的机制类似,一旦判断为加载完成,本身加载状态无法再改变,即无法刷新自身状态,这样就无法充分利用热更新了,只能从父级容器进行控制; - 综上,一旦要刷新预览状态,这里的请求量和异步状态就会爆表,主线程阻塞过于显著;
直接运行APP
一开始还想着不打包可视化搭建应用,直接以开发模式运行应用来着,事实证明这是脑残操作😅;
方案可能存在的问题
假设这套方案可以完全达到预期设计目标的功能使用,那么会有什么问题?
- 如果组件内聚不够,或粒度太细,导致暴露参数较多(或者说需要处理的参数过多),那么开发模式显然就变成了无尽地填充变量,以及提供这些变量和变量之间副作用处理的逻辑;显然,这还是内聚的问题,如果没有一个足够内聚(高度模块化)的业务场景,使用这套方案带来的收益显然是不够的;
- 如果业务自由度较大,经常变动,且变动的粒度很细,那么很显然没必要使用这个方案;
- 一开始使用这种可视化搭建可能有些不适,因为这里把逻辑和视图完全分开了,视图的部分使用可视化搭建来拼接,逻辑则是采用配置进行注入的;但是如果真的熟悉
Vue
这种数据驱动渲染框架的话,那么就很快会适应的,尤其是Vue3
推出的composition
思想,composition
本身就提倡逻辑与视图上下文解耦的,因此数据也好逻辑也罢,可以完全只考虑数据与数据之间的副作用关系即可,视图无非只是使用这些数据,至于怎么去使用这些注入的数据,那是组件内部的事情;
Roadmap
现阶段这些功能对于整体使用来说可能够用,但是需要一些补充功能来优化使用体验;
精准类型推导与类型匹配
这里主要指的是在提取元数据的时候,可以得到组件和变量的精准类型,就像language server
得到的类型一样;这样做的好处主要有:
- 在使用变量的时候可以进行类型的精准匹配,即完全匹配变量和组件入参的类型;一个是可以减少非正确类型变量的干扰,另一个是提前进行严格的类型校验;
- 在获取到精准类型后(主要是联合字面量类型这种,可以提供选项支持),可以提供相应字面量类型的直接数据支持;目前来说,如果是只想给定一个固定字面量作为参数,也需要手动暴露一个参数来注入,这样确实多有不便;
逻辑复用
如果逻辑本身也能通过可视化来进行复用,也是一样是有意思的事情,只不过类似的,我不看好实行完全语法映射的逻辑可视化(或者粒度很细的);目前利用逻辑可视化进行逻辑的复用,实际上也早就有人进行尝试了[11][12]。
脑洞大开
这里不妨脑洞一下,如果扩展使用场景会怎样;
纯云端搭建
如果说把整个搭建应用全部放到云上,即没有本地项目的概念,把整个搭建应用作为一个web
服务,那么这套系统应该怎么适配?
- 脑洞一:直接在云端主机(容器)运行本地项目即可,然后暴露端口访问;不过这种方案显然不适用于面向多个用户,不然一个用户就需要一个主机(容器);
- 脑洞二:稍微整合一下
dev server
变成一个专门处理物料信息的服务端,那么基于此就可以提供异步渲染组件所需要的源码信息,然后也可直接编译产物成为单独的应用并开放访问(这也是目前云端编译的常见方案);不过这么做也有些问题:- 异步渲染组件进行预览的性能问题(上面提到过),不过目前没有证实直接在生产环境应用异步组件也是这么拉跨;
- 异步渲染组件进行预览热更新的问题;
- 如果页面复杂,可能同时进行请求的异步组件过多,请求阻塞;不过可以考虑在服务层合并异步组件粒度,控制请求数量,貌似
Tengine
可以合并请求[13]来着;
面向非编程人员
进一步,如果要把搭建应用面向非编程人员使用(这也是可视化搭建最强烈的需求),那么目前这套配置对于他们还是有点难以理解(比如变量、Vue
的语法啥的);其实要做的工作也不多,无非就是合并所有配置,把配置变成只有一类,就跟填写表单一样,然后把对应的参数设计得不那么编程人员思维,加上足够详细的参数文档;
相关文档
- Yarn Workspace 使用指南 - 简书
- All in one:项目级 monorepo 策略最佳实践 - SegmentFault 思否
- Monorepo最佳实践之Yarn Workspaces - 掘金
- lerna+yarn workspace+monorepo项目的最佳实践_小平果118的博客-CSDN博客_lerna yarn
扩展:关于前端的各种可视化
- 「可视化搭建系统」——从设计到架构,探索前端领域技术和业务价值 - 知乎
- 悟空活动中台系列文章
- 淘宝前端在搭建服务上的探索 - 知乎
- 凹凸技术揭秘 · Deco 智能代码 · 开启产研效率革命 | Aotu.io「凹凸实验室」
- imgcook/imove: Move your mouse, generate code from flow chart:(
ts/js
)逻辑可视化 - imove三分钟上手 · 语雀
- 狼叔:F2C 能否让前端像运营配置一样开发?: flow 2 code,流程化逻辑
- 【第2192期】所见即所得! iMove 在线执行代码探索
- 【第2226期】逻辑编排在优酷可视化搭建中的实践之上:很不错的逻辑可视化/组件化的实践
- 【第2227期】逻辑编排在优酷可视化搭建中的实践之下
- 「可视化搭建系统」——从设计到架构,探索前端领域技术和业务价值 - Lucas HC的文章 - 知乎
- 基于React+Koa实现一个h5页面可视化编辑器-Dooring - 徐小夕的文章 - 知乎
- 厌倦了写活动页?快来撸一个页面生成器吧! - 知乎:移动端
- 可视化拖拽组件库一些技术要点原理分析 - 知乎
- buqiyuan/vite-vue3-lowcode: vue3.x vite2.x vant element-plus H5移动端低代码平台 lowcode 可视化拖拽 可视化编辑器 visual editor:一种实践
- Designable Playground:基于formily的低代码可视化表单搭建工具
- 开放与收敛 - 搭建系统的资产体系设计 - 知乎:云凤蝶搭建架构设计
- 描绘现实世界的桥梁–Formily - 知乎
- 「全码」 通用搭建:现代 Web 研发体系中的新一代低/零码搭建:很好地说明了目前大厂和常见lowcode、nocode等方案的不足地方,但是提出的新的搭建理念需要验证
- 从零开发一款可视化搭建框架dooringx-lib - lowcode低代码可视化社区
- 低代码/无代码十日谈(一)——趋势背后的逻辑 - 知乎:从当前行业发展角度解析低代码发展的前景及突破点,值得一读
- 低代码引擎 | LowCodeEngine:阿里的一个可视化搭建引擎,目前看起来还是走向通用,映射选项多;
- 页面可视化搭建工具前生今世 · Issue #15 · CntChen/cntchen.github.io:比较详尽地对可视化搭建的需求和作用进行了整理,从多个维度进行解析不同类型的可视化搭建工具,精品
lerna+yarn workspace+monorepo项目的最佳实践_小平果118的博客-CSDN博客_lerna yarn ↩︎
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy#跨源脚本api访问 ↩︎
251718 - HTML5 Drag and drop not working between iframe and parent when on a different domain - chromium ↩︎
https://tengine.taobao.org/document_cn/http_concat_cn.html ↩︎