鸽了一年,不重要,下一篇解释
现在几乎现代框架 SSR 默认都是用 node
,难道不能用别的?原因很简单,同构,有天然适合的执行/运行时环境。拿 React
举例子,SSR 分两个步骤:
- 静态部分先用服务器渲染一遍,最基本的都是拿入口过一遍
renderToString
- 运行时水合,也就是不用
render
而是hydrate
,因为已经不需要在运行时创建节点,只需要绑定
所以问题就很清晰:默认或者常规手段的 hydrate
和 renderToString
都是 JS 函数
当 QuickJS 刚出现在我的眼前的时候,特别是有人给 QuickJS 提供 rust 绑定的时候,我就在想一个问题:这 B 是不是可以直接拿来做 SSR ?
Why QuickJS
毕竟 rust 早就有 v8 绑定,deno
用的就是
你说得对,但是 QuickJS 是由著名神仙 Fabrice Bellard 所打造的一个小型并且可嵌入的 Javascript 引擎,它支持ES2020规范,包括模块,异步生成器和代理器。在这里,你可以用任何现代的 JavaScript 语言片段在几乎任何语言环境中运行,在自由的环境中邂逅不同的语言和技术栈,同时它还支持 Decimal 提案和运算符重载提案……
其实就是不挑
前端
流水账,主要是踩坑流程
前端部分自不必多说,玩了两年半的 solidjs
,现在只要不是上班都是用这款(其实上班能夹带私货的时候也用)
solid-ssr 提供 SSR 各种形态的 demo,但他们都有一个特点:使用非常原生的 rollup
插件作为演示[1],那我正经用也不用这个啊,那不行,就得按 vite 工作流来。
so
pnpm dlx degit solidjs/templates/ts
首先 vite.config.ts
有两处调整:
- solid 插件激活 ssr,这在处理 ssr 输出时有用
noExternal
,后面讲为什么
改完之后大概是这样
export default defineConfig({
plugins: [solidPlugin({ ssr: true })],
build: {
target: "esnext",
},
ssr: {
noExternal: true,
},
});
package.json
新增一个 script
,不加也行
{
"type": "module",
"scripts": {
"build": "vite build --outDir ./dist/client",
"build:server": "vite build --ssr ./src/entry-server.tsx --outDir ./dist/server",
}
}
这里把输出路径都有点调整,目的是把客户端产物和服务端产物分开,不这么做也行。但入口是肯定分开的,即使这很基操不需要写下来
按照 vite 建议的做法,客户端入口从 index.tsx
改为 entry-client.tsx
,则服务端入口为 entry-server.tsx
,内容每个框架都差不多,按照前端框架的建议做即可
记得客户端真实入口 index.html
里的 script 路径也要改!
过一下 build,能看到 dist 下两组目录,下一步
服务端
Web Framework 不用挑,用的最多的(可能也是两年半),可能也是目前来说依然最虎的 actix-web
,需要花心思的是找 binding
首先用的是 theduke/quickjs-rs,在品鉴这个项目的时候还顺便搜到另一个项目 galvez/fast-vue-ssr,这是一个类似的操作:通过 QuickJS SSR Vue,可以读读看
Node outperforms QuickJS by a wide margin. Especially with enough cores and memory. However, QuickJS is very small and has very low memory consumption, so running it threaded in a Rust shell makes it possible to have very high throughput using very few resources in comparison.
顺便品鉴完这个项目之后,开始缝合。不出意外的话马上要出意外了
坑1:
unsupported keyword: export
这里可以讲讲为什么上面要 noExternal
,原因很简答,vite 默认你是会用 node 的,加上 type 是 module
,结果就是编译后的服务端入口依然有 import
和 export
所以目的很简单:把所有用到的 js 都打成一个文件。并且这里还需要一个操作:去掉最后一行,也就是 export
部分
默认情况下是在 QuickJS global 环境下 eval 程序,直接当成 REPL
来用就完事了
坑2:
找不到 setTimeout
一番查找,setTimeout
由 QuickJS 内置 module: os 提供,按道理来说 setTimeout
应该能在 globalThis 直接调用
没关系,直接 eval 进去吧
坑3:
不存在 os
这你妈你说你妈呢? 这怎么可能啊
只见 Issue 区赫然有这么一行
Ok,寄
Finish
本来看着是 star 最多的库
算了,还得是直接绑定来得稳定,虽然比较麻烦,相当于换个语言直接使用框架。但高级封装至少比低级封装容易使用
于是改用这个高级封装:DelSkayn/rquickjs,这个就非常纯,需要直接生吃 Document
简单验证
let test = ctx.eval("let a = 1; let b = 2; a + b").unwrap();
assert!(3, test);
发现没问题,直接搬
let result = context.with(|ctx| {
let _a: String = ctx.eval_file("ssr/dist/server/entry-server.js").unwrap();
let result: Object = ctx.eval(r#"render()"#).unwrap();
let head: String = result.get("head").unwrap();
let body: String = result.get("body").unwrap();
let mut map = HashMap::<String, String>::new();
map.insert("body".to_string(), body);
map.insert("head".to_string(), head);
map
});
这里转一次 HashMap
的原因是:Object
的类型没有声明,直接 return 出去有静态检查问题,rust 不允许
但因为是概念验证,先转成已知类型出去顶着用
编译没问题,继续搬
let template = read_to_string("ssr/dist/client/index.html").unwrap();
let html = template
.replace("<!--app-head-->", result.get("head").unwrap())
.replace("<!--app-body-->", result.get("body").unwrap());
打印 html,确认内容就是一个完整的 html 文件。成了!
那直接上 server-side,简简单单写个 get
#[get("/")]
async fn hello() -> impl Responder {
let html = make_html();
HttpResponse::Ok()
.status(StatusCode::OK)
.content_type("text/html")
.body(html)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
// 静态文件映射
.service(Files::new("/assets", "ssr/dist/client/assets"))
.service(hello)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
浏览器输入一个令人熟悉的 localhost:8080
,正常运行,JS 绑定也一切正常
代码会发布在 colmugx/quickjs-ssr
最后
这里只是一个简单验证,SSR 的 real world 肯定不是这么两步就能完成,否则就不需要像 next.js
和 SolidStart
这样的上层框架
并且,除了服务端需要考虑线程池等等这些问题之外,拥有 JIT 的 v8 可能性能上会更优
但我的目的也很简单,我要的不是 SSR,而是两个需求
- 给
ayaka
提供一个静态页面生成的方式,但默认还是像saika
那样的运行时 fetch - 给
kazusa
提供脚本执行环境,不过这是个 optional,主要维护还是 lua 执行环境,这与既定用户群有关
别问为什么又来一个 ayaka
,问就是灵感来源 2nthony/saika,而不叫sakuya
的原因仅是因为那会在玩原神银趴
最后,这可能是第一篇来自于这个主题 3.5 版本的文章,想给用 hexo 的这段时间一个完美的休止符,然后去开发有新设计风格的 nlvi 4
但是重构一半,交互上还是有缺陷,只不过说不影响阅读
Links
solidjs 的 vite 插件来源于 rollup 插件的上层封装 ↩︎