使用 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 个包 vuevue/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-postcssrollup-plugin-scssrollup-plugin-sassrollup-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 是我用来格式化的,可移除或者替换,看你自己选择

posted @ 2023-09-05 21:04  神仙梨子丶  阅读(243)  评论(0编辑  收藏  举报