关于carverry

整体设计

这是一套比较激进的可视化搭建方案(激进体现在适用场景和技术栈的选择上),核心思想就是极致的内聚;因此可以看到除了必要的组件配置,其他所有相关的配置都砍掉了,理由就是这些配置完全可以通过内聚使其成为内部参数,而减少不必要的参数暴露(这一点可以参照现有的一些比较流行的可视化搭建方案,它们都尽可能实现对组件框架的所有语法的完全映射,以便达到最大的自由度);从可视化搭建业务的角度来说,一般就是封装的程度越高,其自由度越低,而搭建效率就越高[1];而这套设计就是为了提高业务搭建效率,所以自然就倾向于内聚;

预期适用场景

  • 面向人群:前端编程人员;
  • 预期解决的问题:减少花在拼接组件的过程中写胶水代码的时间,更加直接直观地去搭积木;
  • 内聚:业务和组件/代码区块的内聚程度越高,复用和可视化搭建的效率也就越高;
  • 面向项目Vue3≥3.2,用于支持setup模式) + TS

数据模型

image-20220217184413785

从数据模型就不难发现所谓的激进之处在哪,差不多砍掉了大部分的语法映射,只保留组件最最核心的部分(即没法通过内聚合并参数来解决);为何可以做到只保留这些属性就可以实现完全的映射?因为其他诸如生命周期,指令等在形成业务逻辑时,完全不必要暴露出来,直接在内部进行处理,而仅仅需要注入完全需要外部数据的部分;

关于变量的注入

可以看到,变量的配置分为两个部分:

  • 位置:这里的位置就是指文件的位置,可以是本地文件路径,也可以是其他位置(诸如仓库地址,包等等);这样就可以不局限于从本地获取;
  • 标识符:不管是何种类型的变量,始终都是通过标识符进行暴露和定位的;

因此,通过这两部分信息理论上可以从任何地方获取变量(数据);

技术栈的选择

技术栈的选择则又是激进的另一部分,很多都是全新的,不太考虑向下兼容性(因为不存在历史包袱);

@carverry/core

  • ts-node:这是一个node端的TS执行器,可以达到用TSnode代码而不预先编译就执行的目的;
  • farrow:这是一个基于TS编写、类型友好、开箱即用,轻量简单的node服务端框架;配合ts-node/TS使用良好;
  • 命令行相关工具:inquirercommanderchalk
  • lowdb:基于JSON文件做数据管理的轻量级数据库工具;
  • ws:简单轻便的nodewebsocket服务搭建工具;

@carverry/app

可视化搭建应用的技术栈选择可以说是无关紧要了,毕竟只要最后的输出产物可以运行即可……所以采用了目前最顺手的一套:

  • Vue3
  • TS
  • element-plus
  • vite

规范设计

物料项目

所谓的物料项目,实际上就是给可视化搭建平台提供组件原料的组件库;而为了快速生产满足可视化搭建使用要求的物料,则需要对物料设计一些规范:

  • 项目无需编译打包:为了避免重复依赖引入,项目所有的dependencies都放到peerDependencies里面,并且指定包的版本兼容范围;这样做的好处就是把依赖安装全部集中到使用项目本身,避免一些相同包的不同版本出现重复打包;虽说无需编译,但是一些额外的元数据还是需要进行预编译比较好,以便在使用这些物料时无需再额外生成元数据,而这个编译命令都集中在@carverry/corecli里了,所以没啥负担;

image-20220428151453354

image-20220428151736633

  • 项目结构:为了方便统一的编译和有明确模式的引入(即在对应项目按照某种模式自动化的引入物料),所以确定一个统一的项目结构是比较明智的;

image-20220428152932460

​ 实际上就是限定了所有源码放在src,其中物料源码都放在materials里面,而materials里面的每个子目录都代表一个具体的组件,至于src里面其他的文件夹则完全不需要硬性的约束了。具体到单个物料的目录,也有一定的规范:

image-20220428153950811

​ 至于carverry.material.json文件的作用,其实就是补充物料的一些无法自动获取的元数据及一些配置,目前满足如下的JSON schema

image-20220428154228024

  • 单元测试:对于公共组件,自然少不了单测,否则无法维持一个可信任、可持续迭代、健壮的组件库;这里选用了vitest作为单测框架,无他,vite生态的最佳选择;
  • 组件文档:采用storybook,可以考虑直接部署文档;

命令行设计

为了提升物料相关的开发效率及启动可视化搭建等功能,@carverry/core提供了一系列cli命令:

image-20220428160201164

image-20220428160314884

细节

基于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模块;而目前nodeLTS版本(16)还不全面支持ESM模块,只是把这个功能放到实验特性里面(作为加载器使用,好在ts-node完全提供了ESM模块的加载器);所以基于TS开发的node代码索性全部采用更为激进的ESM模块,不过不需要太担心,ESMnode的未来主流,且已知在下一个LTS版本(18)可以原生全面支持ESM[4]

目前ESM的限制

关于更多的写法不同,可以参考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-nodets-node完全支持作为Shebang的解释器来执行TS文件作为脚本[6];且ts-node也完全提供了ESM模块的解释器,因此也可以直接执行ESM模块的TS文件作为脚本:

1
2
3
#!/usr/bin/env ts-node-esm

// 剩下的就是正常的TS代码

不过有一点需要注意的是,如果要让这个bin文件可以在任意命令行都可以执行,那么就需要全局安装ts-node

关于拖拽的交互

如果将一个组件拖拽放入可视化界面中,如何推测用户要插入的精准位置?这里有一个有意思的思路[7],那就是根据用户插入组件时其鼠标位置命中的最近的一个物料组件(即对应一个配置节点),然后根据距离该组件DOMRect的边距来得到一个距离最近的边距,得出插入位置的推断:

  • left/top:插入该组件之前;
  • right/bottom:插入该组件之后;

image-20220505103801689

当然,得出这种推断的前提是该容器内布局为正常文档流(从左往右,从上至下);

不同应用上下文之间的事件通信

由于直接使用了本地项目的上下文环境作为可视化预览页面(这样做的好处就是可以直接复用当前项目的vite配置,无需额外再配置),而可视化搭建应用属于另一个独立的应用,可视化搭建应用通过iframe来引入这个预览页面,因此势必造成了iframe跨域的问题;

由于现代浏览器的同源策略,iframe的跨域会导致很多的限制,除了熟知的网络请求限制,更多的是控制的限制,父级文档无法控制子iframe文档[8],导致很多交互(DOM层面)不能在两个上下文之间流通;比如从搭建应用拖拽一个组件原料放入到iframe中的页面时,正常情况下完全不可能[9],因为跨域限制了父级事件流通到iframeDOM里面,所以只能另找门路;

事件机制

DOM的事件机制提供了一套可以主动触发事件及构造自定义/原生事件的API

因此,有了这套事件机制,只要事件触发端把相应的事件信息发送到要传递的浏览上下文,然后在接收端进行相应事件的构造并触发,就完全可以绕过iframe跨域的限制,完成事件通信/流转;

那么剩下的就是如何完成两个跨域浏览上下文之间的信息传递了;

信息传递

其实完成不同浏览上下文之间的信息传递的方法不止一种:

  • websocket:在不同上下文之间建立socket连接,就可以很自然地进行双向通信了;所以一开始我很天真的想到了这种办法,而忽视了DOM本身的机制;
  • window.postMessage():为不同上下文进行安全通信而生的DOM API,其实就是不二选择;

Hit Testing

Hit Testing,即命中测试,是图形学中的一个专业术语,一般用于描述用户鼠标位置击中当前图形交互界面(GUI)中的哪个图元(图形物体);而具体到前端,可以理解为用户鼠标位置命中的是哪个DOM元素(DOM元素的图形本质就是一个矩形区域);

为啥要做命中测试?因为从父级上下文传递事件的时候,并不知道触发事件对应的是子级上下文的哪个元素,因此需要在子级上下文构造事件的同时对当前浏览上下文做一次命中测试,以便精准地从相应的DOM元素触发事件;巧的是DOM刚好提供了命中测试相关的API

在使用鼠标位置进行命中测试一定要确保这个鼠标位置是相对于当前document对象而言的,即确保坐标原点是当前document对象的左上角;

过程图解

image-20220429153436462

iframe的事件穿透

// TODO

dataset的使用

为了更精准地进行事件交互,比如弄清组件插入位置所属的配置节点,那么最好给相应的DOM节点标注上所需的信息,以便更好的处理交互;而要给DOM标记自定义属性,那么dataset自然是最佳也是最符合规范的选择。

在 DOM tree 的基础上构建配置树

:在设计中,一个配置节点就等同于一个物料组件;

image-20220501120033666

胶水代码的输出

在通过可视化搭建得到了相应配置树结构,以及通过填充数据补充变量等细节配置后,如何将配置树转化为对应的Vue代码?其实很简单,每个配置节点就是一个Vue组件,只需要将配置转为Vue组件源码,然后递归处理子级即可;

那么,关键就在于如何将一个配置节点转化为等价的Vue组件;其实有了setup模式(需要vue3.2+)的加持,对组件数据的组装变得异常简单了起来,因为setup模式的核心就是导入即可使用,无需像以前option模式或者早期Vue3composition API那样需要专门分配到对应类型的选项里面,如需要分辨导入变量是方法、单纯的变量还是组件等;而setup模式只需要将变量导入,Vue框架会根据变量类型自动分配到对应的选项里去,所以说setup模式就是高级的语法糖,进一步减少了胶水代码,自动帮你进行composition的处理;

可以说正是有了setup模式,我才会水到渠成地想到这整套数据模型;那么基于setup模式,将配置节点输出为Vue组件源码可以分为两个部分:

  • script部分:只需要将配置节点使用到的变量全部引入即可,然后判断是否有子配置节点,如有则递归处理得到子配置节点的源码并引入;
  • template部分:负责将数据进行填充即可,包括使用子组件;

script部分和template部分进行合并就得到了配置节点对应的Vue组件源码了,那么最外层的组件自然就是整个配置树的入口了;

image-20220501122906554

第三方 UI 库元数据的转换

:我个人是很不赞成在需要高内聚的可视化搭建系统直接使用通用组件的,诚然,通用组件可以使得搭建更灵活,但往往只能支撑很简单的业务场景,在复杂业务场景下通用组件的使用效率就很低了;当然,抱着可行性的态度我还是去尝试了一下,毕竟至少可以作为某种回退操作(更广的兼容性);

虽说是第三方UI库,但跟上述提到的物料库项目一样,不过事实上我们并不需要构造源码文件,因为可以直接引入第三方库,所以针对第三方库,我们只需要提取其组件元数据即可,而在引入的时候做一个路径转换即可;

一般来说,只要一个组件库的项目结构是规范的,那么其组件源码一定是按照某个固定结构进行排列的,因此只需要找到这个结构然后就可以批量进行元数据提取了;这里以element-plus为例,他们的组件源码放在packages/components里面,其中里面的每个文件夹都是一个相对独立的组件,而组件的入口文件通常为[component-name]/src/[component-name].vue

image-20220501131703736

因此根据这一特征就可以批量的从其源码文件提取对应组件的元数据,不过需要注意的是第三方库可能由于内部高度模块化,而导致一般的从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,根据不同的自定义后缀以及挂载参数来相应返回不同的文件源码,然后借由异步组件动态加载,其子配置节点也是递归处理的;

image-20220502121044079

这样做的好处就是可以不必自行搭建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的语法啥的);其实要做的工作也不多,无非就是合并所有配置,把配置变成只有一类,就跟填写表单一样,然后把对应的参数设计得不那么编程人员思维,加上足够详细的参数文档;

相关文档

扩展:关于前端的各种可视化


  1. https://github.com/CntChen/cntchen.github.io/issues/15 ↩︎

  2. Monorepo——大型前端项目的代码管理方式 - SegmentFault 思否 ↩︎

  3. lerna+yarn workspace+monorepo项目的最佳实践_小平果118的博客-CSDN博客_lerna yarn ↩︎

  4. Node.js 18 新特性解读 - 知乎 ↩︎

  5. Shebang - 维基百科,自由的百科全书 ↩︎

  6. https://github.com/TypeStrong/ts-node#shebang ↩︎

  7. https://lowcode-engine.cn/demo/index.html ↩︎

  8. https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy#跨源脚本api访问 ↩︎

  9. 251718 - HTML5 Drag and drop not working between iframe and parent when on a different domain - chromium ↩︎

  10. Suspense | Vue.js ↩︎

  11. 【第2226期】逻辑编排在优酷可视化搭建中的实践之上 ↩︎

  12. 狼叔:F2C 能否让前端像运营配置一样开发? ↩︎

  13. https://tengine.taobao.org/document_cn/http_concat_cn.html ↩︎