程沛权
程沛权

用Vite更简单的解决Vue3项目的预渲染问题

作者:程沛权2022-06-02
Star on GitHub

之前 Webpack 项目经常会用到预渲染,现在团队都开始用 Vite 了,所以弄一个基于 Vite 的 Vue 3 预渲染 demo 可以参考。

预渲染和静态生成器比较接近,也可以参考我的 SSG 博客vite-ssgvite-plugin-pages 来处理。

不过 Vite 本身对预渲染也提供了原生的支持,简单的预渲染可以自己写写代码来改造实现。

HTML 部分

项目根目录下 index.html 里需要追加至少两条资源注入位置的注释:

注释语句作用是否必须
<!--preload-links-->预加载资源
<!--app-html-->页面内容
<!--title-->SEO 优化:写入标题
<!--description-->SEO 优化:写入描述
<!--keywords-->SEO 优化:写入关键词

并把入口文件改成 entry-client.ts ,原来的 main.ts 会作为客户端和服务端启动时的引用。

完整代码如下(源码:index.html ):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!--title-->
    <!--description-->
    <!--keywords-->
    <!--preload-links-->
  </head>
  <body>
    <div id="app"><!--app-html--></div>
    <script type="module" src="/src/entry-client.ts"></script>
  </body>
</html>

其中除了两条必须的注释语句外,可选的部分见 SEO 优化

入口文件

普通项目是使用 src/main.ts 作为入口文件,需要改造成两个入口:

注释语句作用源码
entry-client.ts客户端入口查看源码
entry-server.ts服务端入口查看源码

而原来的 main.ts 只作为入口函数导出,详见源码: main.ts

路由

不再需要手动配置路由结构了,改造后直接读取 src/views 的路由组件来生成页面路由。

详见源码: router

SEO 优化

做预渲染为的就是做 SEO ,所以需要自己提前配置好 SEO 的 TKD 三大要素,这里我也是放在 src/router 目录下一起管理了。

实现逻辑见 预渲染 部分的说明,这里是以最终每个页面的相对路径来判断要写入的 TKD 信息的。

export default [
  {
    url: '/',
    title: '首页',
    description: '这是首页',
    keywords: ['关键词1', '关键词2'],
  },
  // ...
]

详见源码: seo

预渲染

scripts/prerender.ts 这个文件是执行预渲染行为,可以按照路由目录的结构渲染为静态 HTML 文件。

运行 npm run generate ,可以把 dist/static 作为静态站点部署。

当然我也封装了 npm run build 一次性编译所有平台( Client / Server / Static )。

详见: package.json 里的 scripts 部分。

常见问题

改造过程中遇到的几个问题:

水合节点不匹配

控制台报错:

Hydration node mismatch:
- Client vnode: div 
- Server rendered DOM: <!--app-html-->  

警告来自于 hydration.ts ,一般可以无视……

当然也可以了解更多的知识点: understand-and-solve-hydration-errors-in-vue-js

路由跳转

控制台报错:

Unhandled error during execution of scheduler flush

需要使用 <Suspense /> 标签来包裹路由视图,详见 Suspense

<template>
-  <!-- <router-view :key="key" /> -->
+  <router-view :key="key" v-slot="{ Component }">
+    <Suspense>
+      <div>
+        <component :is="Component" />
+      </div>
+    </Suspense>
+  </router-view>
</template>