使用 vue 渲染静态模板
最近再一次需要做纯静态页面(无任何脚本语言,只保留 css 和 html),以往我直接使用 ejs 生成,但是工作中一直使用 jsx 和 vue 来组装页面,就突发奇想,难道 react、vue 不能只渲染纯静态页面吗?
有了这个想法,我就想验证下可行性,万能百度开始,找了一圈,发现基本都是需要脚本依赖的,这就意味着必须引入 react 或者 vue,而我只是想纯粹地想渲染纯页面,压根不需要脚本。或许是我搜索的姿势不对,最后没有找到合适的插件,那就自己组装一个。
说干就干,首先我需要说明下编译环境的要求,可以读写文件,可以运行 js 脚本,选择 node 还是 deno,看你自己,我选择的是 node(用的太多了),版本需要 >= 16,依赖库选择的是 vue3,准备好了,那就可以开始了。
直接找 vue3 源码阅读,发现在 server-renderer 包中的 README.md 中,作者已经贴出示例代码:
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')
const app = createSSRApp({
data: () => ({ msg: 'hello' }),
template: `<div>{{ msg }}</div>`
})
;(async () => {
const html = await renderToString(app)
console.log(html)
})()
那就要围绕这个开始,我本身有自己的需求,单独的 vue 文件和 css 注入,首先需要编译单独的 vue 文件,而不是 template 字符串,源码中看到 compiler-sfc ,在阅读之后,发现它就是 .vue 文件转换器,我们可以找到插件 rollup-plugin-vue 来编译 .vue 文件,对照着 rollup 官网,直接祭出以下代码:
import { rollup } from 'rollup'
import vue from 'rollup-plugin-vue'
const bundle = await rollup({
input: 'src/index.js',
plugins: [
vue({ target: 'node' })
]
})
await bundle.write({
format: 'es', // 这里是为了配合 node,后续会说到
file: 'cache.tmp.js'
})
if (bundle) {
await bundle.close()
}
这样基本就算打包成功了,但是会有个警告,就是 external 需要我们处理下,查看了下编译之后的代码,发现其依赖 2 个包 vue
和 vue/server-rendered
,其实就是 vue3 的包,我们稍微修改下:
import { rollup } from 'rollup'
import vue from 'rollup-plugin-vue'
const bundle = await rollup({
input: 'src/index.js',
+ external: ['vue', 'vue/server-renderer'],
plugins: [
vue({ target: 'node' })
]
})
await bundle.write({
format: 'es', // 这里是为了配合 node,后续会说到
file: 'cache.tmp.js'
})
if (bundle) {
await bundle.close()
}
这时候警告没有,将 vue 文件编译成 js 文件成功,接下来就是样式,直接找到对应的插件,rollup-plugin-postcss
、rollup-plugin-scss
、rollup-plugin-sass
、rollup-plugin-less
等(太多了,自己选择),我使用的是 rollup-plugin-less
, 重新调整代码:
import { rollup } from 'rollup'
import vue from 'rollup-plugin-vue'
+ import less from 'rollup-plugin-less'
const bundle = await rollup({
input: 'src/index.js',
external: ['vue', 'vue/server-renderer'],
plugins: [
vue({ target: 'node' }),
+ less({ output: 'cache.tmp.css' })
]
})
await bundle.write({
format: 'es', // 这里是为了配合 node,后续会说到
file: 'cache.tmp.js'
})
if (bundle) {
await bundle.close()
}
到这里,基本上 css 和 js 都准备好了,原材料准备好,我们就可以开始组装纯静态网页了。在此之前,我们需要准备一个 html 模板,可以是单独的文件,这样直接使用 fs.readFile 读取内容,也可以使用字符串,我直接选择字符串,代码如下:
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>$css</style>
</head>
<body>
<div data-ssr="true">$ssr</div>
</body>
</html>
`.trimStart()
这里面有 2 个占位符,一个是 $css
,为了填充 css 代码,一个是 $ssr
,为了填充 html 代码,css 代码我们直接读取刚刚生成的 css 文件即可,代码如下:
const rawCss = await fs.readFile('cache.tmp.css', 'utf-8')
html 代码,需要借助 vue 来完成转变成,代码如下:
const App = await import('./' + 'cache.tmp.js') // import 如果绝对路径那么引入的是 node_modules 中的包
const app = createSSRApp(App.default)
const rawHtml = await renderToString(app)
这样总算是完成了最后一环,那么我们可以得到一份完整的页面字符串了,现在只需要借助 fs 模块写入即可,最后贴出完成代码(润色之后的):
import fs from 'node:fs/promises'
import { rollup } from 'rollup'
import vue from 'rollup-plugin-vue'
import less from 'rollup-plugin-less'
import { format } from 'prettier'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const OUTPUT_JS = 'cache.tmp.js'
const OUTPUT_CSS = 'cache.tmp.css'
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>$css</style>
</head>
<body>
<div data-ssr="true">$ssr</div>
</body>
</html>
`.trimStart()
const compileCode = async function() {
const bundle = await rollup({
input: 'src/index.js',
external: ['vue', 'vue/server-renderer'],
plugins: [
vue({ target: 'node' }),
less({ output: OUTPUT_CSS, option: { compress: true } })
]
})
await bundle.write({
format: 'es',
file: OUTPUT_JS
})
if (bundle) {
await bundle.close()
}
}
;(async () => {
await compileCode()
const App = await import('./' + OUTPUT_JS)
const app = createSSRApp(App.default)
const rawHtml = await renderToString(app)
const rawCss = await fs.readFile(OUTPUT_CSS, 'utf-8')
const html = await format(htmlTemplate.replace('$css', rawCss.trim()).replace('$ssr', rawHtml), { parser: 'html' })
await fs.writeFile('index.html', html)
await fs.rm(OUTPUT_CSS)
await fs.rm(OUTPUT_JS)
})()
到此结束,有点大材小用的感觉😊。
一些你需要注意的点
-
默认你是懂 js 代码的,至少不是一窍不通的,所以基础知识不做介绍
-
代码中的 package.json 采用是 type: "module" 来处理的,这就是为什么 rollup 输出的 ** format: 'es' ** 代码,如果你采用默认的 cjs,format 需要修改
-
代码中所有的生成文件都是在根级路径,当然可以使用
node:path
来处理,我懒的做更细致的优化了 -
其它的关于代码压缩,css 前缀,脚本注入,看你自己配置了
-
代码中的
prettier
是我用来格式化的,可移除或者替换,看你自己选择