一、痛点:内网项目里那张「Node 门票」
真正的问题不是「有没有 Vite」,而是为什么一个后端项目为了渲染几个组件,必须引入一整套 Node 工具链。
二、更重要的收益:前后端一体化
把 Node 从生产环境里拿掉,表面看只是少装了个运行时;真正的收益是开发模型回到了「一个工程」。
用 easy-vue 之后,页面就是后端工程里的几个 .vue 文件:
-
同一个仓库:Controller、Service 和 views/*.vue 放在一起,接口定义与调用它的页面在同一个 PR 里评审;
-
同一次构建、同一次发布:一个 fat jar(或 thin jar + lib)就是全部交付物,前端源码随包走,不再有「前端包忘了更新」这类事故;
-
同一个进程:后端既是 API 提供者,也是页面宿主,.vue 请求进来现场编译返回,没有独立的 Node 构建服务,也没有构建产物与源码不同步的问题;
-
改完即生效:调整一个按钮文案,改 .vue 刷新浏览器就能看到,不用等一次构建流水线;
-
Java 侧同样成立:配合 easy-vue4j,Spring Boot 里可以直接写、直接跑 .vue,一个工程、一个端口,权限与路由都复用后端的会话体系。
-
对交付尤其明显:客户现场只要一个后端包,内网不需要 Node、不需要 CI 前端流水线,也不需要为静态资源单独配一套 Nginx 规则。
三、关键认知:编译 Vue 不需要 Node 运行时
-
Node.js 在这里承担的只是「执行 JavaScript 的工具」这个角色。而 SFC 编译做的事情,说白了是三步纯字符串/结构处理:
-
把 .vue 文件切成 template / script / style 三块;
-
把模板 AST 编译成 render 函数源码,把 script setup 转成 setup();
-
把 .ts 转译成 JS,并处理 scoped 样式。
-
这些逻辑只要有 JS 引擎就能跑:@vue/compiler-sfc 是纯 JS 包,esbuild 本身就是个 Go 写的原生二进制。于是我把「执行 JS」这一步也用原生代码替掉了。
四、整体架构
项目叫 easy-vue,编译核心用 scriptc(TypeScript → C)编译成原生可执行文件,运行时内嵌 @vue/compiler-sfc;需要转译 .ts 时调用同目录的 esbuild 子进程。对外只暴露两种调用方式:
./easy-vue serve 127.0.0.1:9000
curl -s -XPOST 127.0.0.1:9000/compile \
-d '{"type":"vue","source":"<template><div>{{n}}</div></template><script setup>const n=1</script>","filename":"views/a.vue"}'
# → {"id":null,"ok":true,"js":"// compiled by @vue/compiler-sfc via easy-vue ..."}
返回的 js 就是浏览器能直接 import 的 ESM 模块。后端只需要做一件事:读到 .vue 文件,把源码 POST 上去,再以 text/javascript 返回结果。Python 里大约十行:
import json, subprocess, urllib.request
proc = subprocess.Popen(["./easy-vue", "serve", "127.0.0.1:9901"])
def compile_vue(source: str, filename: str) -> str:
data = json.dumps({"type": "vue", "source": source, "filename": filename}).encode()
with urllib.request.urlopen("http://127.0.0.1:9901/compile", data=data, timeout=10) as r:
return json.loads(r.read().decode())["js"]
五、协议:一次请求就是一次编译
-
请求体字段很少,够用就行:
-
type:vue / ts / js,缺省按 filename 扩展名推断;
-
source:源码内容;
-
filename:编译时的名字,影响 __name、sourcemap 和类型推断;
-
sourcemap:为 true 时产出内联 sourcemap;
-
style:inject(默认)或 separate,决定样式进 JS 还是走响应的 css 字段。
响应固定是 {id, ok, js, css, error},失败时 ok=false 且 error 里有可读信息——后端可以直接把错误透给浏览器控制台,排查体验比黑盒构建链好很多。
六、三个刻意的取舍
1)样式默认注入,而不是单独出 .css。 <style> 的编译结果会以幂等脚本编进 JS 尾部,浏览器运行时自动插 <style> 标签(同组件同内容不重复插)。好处是少一次请求、服务端渲染场景同样安全、后端不用管静态资源映射;确实需要独立 CSS 的调用方传 style: "separate" 即可。
2)无状态,不缓存。 编译结果不落地、不设 TTL,缓存策略完全交给调用方——后端有 mtime、ETag、业务版本号,它比编译器更清楚什么时候该失效。
3)serve 模式禁止按 filename 读服务器文件。 HTTP 接口只接受 source,绝不根据路径去读服务器磁盘;只有本地可信的 convert 模式允许按文件名读。这是把「编译」和「文件系统访问」两件事的信任边界分开。
七、实测数据(macOS x86_64,200 次调用)
-
常驻服务编译一个小 SFC:中位数 4.1 ms,p95 33 ms;冷启动后首次编译约 70 ms;
-
可执行文件体积:macOS 约 2.2 MB,Linux(musl 纯静态)约 4.9 MB;
-
产物长度参考:带 scoped 样式、含 script setup 与 el-button 的小组件编译后 1454 字节 ESM JS。
-
值得一提的是构建期依赖摇树:把 Element Plus 按实际用到的组件名深路径打包后,全量 vendor 从 1.9 MB 降到 417 KB(CSS 同步瘦身),这是「无构建步骤」方案能撑住真实中后台页面的关键。不过这块细节偏内部,对外只需记住结论。
八、它不适合什么场景
九、项目地址
easy-vue 使用 MIT 协议,提供 macOS(arm64/intel)、Linux(x64/arm64 静态)、Windows(x64/arm64)六个平台的免编译包,仓库里还有标准库零依赖的 Python 最小示例和 Go 示例;Java 项目可以配合 easy-vue4j,在 Spring Boot 里直接跑 .vue。
GitHub:https://github.com/easy30/easy-vue
如果你的项目也被「内网装不了 Node」困扰过,欢迎在仓库 Discussions 里聊聊具体场景,我更好奇的是它在哪些地方撑不住。