出码
所谓出码,即将低代码编排出的 schema 进行解析并转换成最终可执行的代码的过程。
为什么要出码
出码是为了更高效的运行和更灵活地定制渲染。相对而言,基于 Schema 运行进行渲染的优点在于其能实时响应内容的变化和接入成本低,而缺点是实时解析运行的性能开销和包大小都比较大,时无法自由地进行扩展二次开发,功能自由度受到一定程度限制。
什么场景下考虑进行出码
当然,出码也会存在一些限制:一方面需要额外的接入成本,另一方面通常需要额外的生成代码和打包构建的时间,难以做到基于 Schema 的运行时渲染那样保存即预览的效果。
所以不是所有场景都建议做出码,一般来说以下 3 个场景可以考虑使用出码进行优化。
场景一:想要极致的页面打开速度,降低 LCP/FID
这种场景比较常见的是 C 端应用,比如手淘上的页面和手机钉钉上的页面,要求能够尽快得响应用户操作,不要出现卡死的情况。当一个流入协议大小比较大的时候,前端进行解析时的开销也比较大。如果能把这部分负担转移到编译时去完成的话,前端依赖包大小就会减少许多。从而也提升了加载速度,降低了带宽消耗。页面越简单,这其中的 gap 就会越明显。
场景二:老项目 + 新需求,用搭建方式产出页面
这是一个很常见的场景,毕竟迁移或者重构都是有一个过程的,阿里的业务都是一边跑一边换发动机。在这种场景中,我们不可能要求使用运行时方案来做实现,因为运行时是一个项目级别的能力,最好是项目中统一使用他这一种方式,保证体验的一致性与连贯性。所以我们可以只在低代码平台上搭建新的业务页面,然后通过出码模块导出这些页面的源码,连同一些全局依赖模块,一起Merge 到老项目中。完成开发体验的优化。
场景三:协议不能描述部分代码逻辑(协议功能不足或特别定制化的逻辑)
当我们发现一些逻辑诉求不能在目前协议中很好地表达的时候,这其实是项目复杂度较高的一个信号。比较好的方式就是将低代码研发和源码研发结合起来。这种模式下最大的诉求点之一就是,需要将搭建的内容输出为可读性和确定性都比较良好的代码模块。这也就是出码模块需要支持好的使用场景了。
出码模块原理
出码模块的输入和输出很简单:
这里有几个概念:
- schema: 搭建协议内容,指符合《低代码引擎搭建协议规范》的 schema
- solution:出码方案,指具体的项目框架(如 Rax,Ice.js)
- Source Codes:生成的源代码,以目录树的形式进行描述
可以看出,这是一个与用户基本没有交互,通过既定的流程完成整个功能链路的模块。其核心暴露的是一个将搭建协议 schema 按既定的 solution 转换为代码的函数。对于使用者来说就是一个输入输出都确定的黑盒系统。
出码流程概述
出码模块和编译器很类似,都是将代码的一种表现形式转换成另一种表现形式,如:
编译器流程
出码模块流程
出码流程详解
协议解析
协议解析主要是将输入的 schema 解析成更适合出码模块内部使用的数据结构的过程。这样在后面的代码生成过程中就可以直接用这些数据,不必重复解析了。
主要步骤如下:
- 解析三方组件依赖
- 分析 ref API 的使用情况
- 建立容器之间的依赖关系索引
- 分析容器内的组件依赖关系
- 分析路由配置
- 分析 utils 和 NPM 包依赖关系
- 其他兼容处
前置优化
前置优化是计划基于策略对 schema 做一些优化。
主要逻辑分为分析、规则和优化三个部分,组合为一个支持通过配置进行一定程度定制化的策略包。每个策略包会先执行分析器,对输入进行特征提取,然后通过规则对特征进行判断,决定是否执行优化动作:
代码生成
代码生成的流程如下:
如果简单粗暴地拼字符串生成源代码将难以扩展和维护,因此出码模块在代码生成过程中将代码进行了一些抽象化。
日常开发中,我们常常是基于某一个特定的项目框架,将一些配置、UI 代码、逻辑代码放到他们应该在的地方,最终形成一套可以 run 起来的业务系统。那么其实对于出码这件事,我们也可以层层拆解,项目 -> 插槽 -> 模块 -> 文件 -> 代码块(代码片段)。这样就能将复杂的项目产出问题,拆分为一个个相对专注且单一的代码块产出问题,同时也支持组合复用。
插槽
首先来看下插槽,插槽描述了对应模块在项目中相对路径,并且可以对模块做固定的命名。每个插槽都有一系列插件来完成代码产出工作。生成的一个或多个文件,最终会依照插槽的描述放入项目中。
// 项目模版
export interface IProjectTemplate {
slots: Record<string, IProjectSlot>;
}
// 插槽
interface IProjectSlot {
path: string[];
fileName?: string;
}
// 插槽出码插件配置
interface IProjectPlugins {
[slotName: string]: BuilderComponentPlugin[];
}
代码块
代码块是出码产物的最小单元,由出码模块插件产出,多个代码块最后会被组装为代码文件。每个代码块通过 name 描述自己,再通过 linkAfter 描述应该跟在哪些 name 的代码块后面。
interface ICodeChunk {
type: ChunkType; // 处理类型 ast | string | json
fileType: string; // 文件类型 js | css | ts ...
name: string; // 代码块名称,与 linkAfter 相关
subModule?: string; // 模块内文件名,默认是 index
content: ChunkContent; // 代码块内容,数据格式与 type 相关
linkAfter: string[]; //
}
后置优化
后置优化分为文件级别和项目级别两种:
- 文件级别:在生成完一个文件后进行处理
- 项目级别:在所有文件都生成完了只有进行处理
文件级别的后置优化目前主要是有 prettier 这个代码格式化工具。