跳绳鸭
67.76M · 2026-02-04
前段时间突然回顾了一下之前做过的一件事:上一份工作的核心任务之一,其实就是一个可视化 / 低代码平台。
当时受限于时间和复杂度,整体方案基本是基于 vue-sfc-playground 这一套思路,通过 iframe + 浏览器端编译的方式来实现远程组件的扩展能力。虽然最终把功能跑通了,但在真实使用过程中,这套方案逐渐暴露出不少问题。
比较典型的有:
站在现在这个时间点回看,我觉得这个问题本身并不复杂,只是当时的实现方式并不优雅——它其实是有更好的解法的。
为了方便后续讨论,我们先假定一个明确的需求:
在这个前提下,我给自己定了几个明确的目标:
原因也很现实:
在线写组件这条路,优化成本太高了,一旦引入复杂依赖、真实业务代码,维护难度会指数级上升。
开发阶段整体的数据流和职责关系如下:
flowchart LR
IDE[本地 IDE]
IDE -->|文件变更| Rsbuild
Rsbuild -->|HMR / WS| Renderer
Renderer -->|defineAsyncComponent| VueRuntime
开发阶段的核心诉求其实很简单:
如果你看过 Vue 3 的官方文档,会发现它提供了一个非常关键的能力:defineAsyncComponent,用于异步加载组件。
import { defineAsyncComponent } from "vue";
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// 从服务端获取组件
resolve(/* 组件对象 */);
});
});
需要注意的一点是:
resolve 返回的并不是字符串形式的源码,而是经过编译后的组件对象,本质上类似于 vue-loader 处理后的产物。
到这里,背景铺垫就结束了。
在开发阶段,我选择 Rsbuild 作为构建核心,整体思路大致如下:
API Key 和 Project IDdefineAsyncComponent 动态注入组件除此之外,还需要提前定义一些结构和规范,例如:
global.css:用于声明全局样式composes/:用于存放多个自定义组件在 dev 模式启动后,插件会:
这里有两个非常重要的点:
style,一旦检测到直接报错,防止出现难以审查和回滚的样式污染。vue 本身 external 掉。
其他依赖(如 dayjs、UI 框架等)直接打进包里。虽然这样会导致 JS 体积偏大,但这是开发态,为了效率和稳定性,这个代价是完全可以接受的。
vue3-sfc-loader?一个常见的问题是:
为什么不直接使用 vue3-sfc-loader 在浏览器端加载 SFC?
核心原因在于依赖管理。
vue3-sfc-loader 需要手动维护 moduleCache,而一旦涉及真实项目,就必然要引入大量第三方包。这就意味着你需要结合 importmap 来管理依赖关系和版本冲突。
例如:
<script type="importmap">
{
"imports": {
"vue": "https://play.vuejs.org/vue.runtime.esm-browser.js",
"vue/server-renderer": "https://play.vuejs.org/server-renderer.esm-browser.js"
}
}
</script>
这种方式在 Demo 场景下还可以接受,但在真实低代码平台中,维护成本会非常高,几乎不可控。
flowchart TB
subgraph Dev[开发阶段]
IDE --> Rsbuild
Rsbuild --> Renderer
Renderer --> Browser
end
subgraph Prod[生产阶段]
Config[JSON / DSL]
Config --> Monorepo
Monorepo --> Build
Build --> OSS
OSS --> Browser
end
生产环境的目标只有一个:性能。
整体思路是结合 Monorepo,将低代码平台中的配置还原为一个真实可构建的工程。
flowchart TB
Root[monorepo]
Root --> Composes[composes/* 组件包]
Root --> Apps[apps/platform]
Apps --> Pages[页面还原]
components/ 目录下,作为 Monorepo 的子包apps/platform 中,根据平台生成的 JSON 配置:
package.json 依赖中随后直接执行 build。
由于使用的是基于 Rust 的构建工具(如 Rsbuild / Rspack),即使是全量构建,耗时也基本控制在 30 秒以内。
对于支持多页面的低代码平台来说,结合这种拆分方式,每一个页面本质上只包含自己的业务代码。
const routes = [
{
path: "/remote-js",
component: () => import("https://my-server.com/assets/RemoteComponent.js"),
},
];
浏览器原生已经支持通过 import() 加载远程 ESM 模块,只要返回的是一个 Promise,Vue Router 就可以正常工作。
这套方案更多是一次架构思路的延展,以及一些关键落地点的总结。
真正落地时,依然会有很多细节需要打磨,例如:
不过整体主线是清晰的:
如果你对其中某些设计有不同的想法,或者有类似的实践经验,也欢迎交流一波。