Vue prerender工作流程&代码实现&踩坑笔记

prerender的介绍网上有很多,相比于SSR轻量许多。
keywords: Vue prerender 预渲染 页面跳转404 点击事件失效 js不运行

工作原理

webpack插件prerender-spa-plugin在第一次打包之后通过 puppeteer 打开浏览器访问之前配置好的路由地址。通过puppeteer我们可以向页面中注入js代码执行,从而得到相应页面的前端代码写入到本地文件中生成预渲染内容。

工作流程

  • npm run build进行打包生成第一次编译文件
  • 启动prerender-spa-plugin插件
  • 插件启动本地node服务器和puppeteer浏览器
  • 浏览器根据vue.config.js中创建PrerenderSPAPlugin对象时提供的routes配置访问相应的路由地址
  • puppeteer监听创建PrerenderSPAPlugin对象时提供的renderer对象中配置的renderAfterDocumentEvent事件,该事件会在入口文件main.js(或其它类似文件)中的相应时机里触发
  • 当puppeteer监听的事件被触发时开始获取页面代码
  • 当代码爬取完毕进入到创建PrerenderSPAPlugin对象时提供的postProcess函数中,该函数是在预渲染内容输出到本地文件前最后一个钩子对输出内容进行修改。函数接受一个对象入参:
{
  route: String, // Where the output file will end up (relative to outputDir)
  originalRoute: String, // The route that was passed into the renderer, before redirects.
  html: String, // The rendered HTML for this route.
  outputPath: String // The path the rendered HTML will be written to.
}

直接修改该对象的属性值并返回该对象完成处理工作

  • 根据PrerenderSPAPlugin创建时的配置输出文件到本地完成整个预渲染流程

关键代码实现

// 入口文件main.js
const app = new Vue({
  router,
  render: h => h(App),
  /**
   * 在mounted中再加一个定时器触发puppeteer监听事件,为了确保页面的请求和内容能全部加载渲染完成
   */
  mounted() {
    setTimeout(() => {
      console.log("dispatch event");
      window.document.dispatchEvent(new Event("render-event"));
    }, 5000);
  }
});
document.addEventListener("DOMContentLoaded", () => {
  app.$mount("#app");
});
// vue.config.js
configureWebpack: config => {
    let plugins = [];
    if (process.env.NODE_ENV === "production") {
      console.log("production config");
      // 生产环境增加预渲染
      plugins.push(
        new PrerenderSPAPlugin({
          // 要执行预渲染的文件路径
          staticDir: path.join(__dirname, "dist"),
          // 需要进行预渲染的页面路由
          routes: ["/", "/index", "/about"],
          /**
            postProcess是预渲染内容输出之前最后一次对输出内容进行修改的机会
            ctx: {
              route: String, // 相对于outputDir输出的路径
              originalRoute: String, // 页面最初的路由地址,即重定向之前的路径
              html: String, // 爬取到的页面代码
              outputPath: String // 预渲染之后代码的输出路径
            }
          */
          postProcess(ctx) {
            // 当访问根目录时路由会重定向到index上,此时route:'/index'  originalRoute:'/',
            // 做一次赋值让根目录下的index.html中有index路径下的内容
            ctx.route = ctx.originalRoute;
            return ctx;
          },
          // 一些优化配置,参考github说明
          minify: {
            collapseBooleanAttributes: true,
            collapseWhitespace: true,
            decodeEntities: true,
            keepClosingSlash: true,
            sortAttributes: true
          },
          renderer: new Renderer({
            // 需要注入一个值,这样就可以检测页面当前是否是预渲染的
            inject: {},
            // 是否使用无头浏览器
            headless: false,
            // 视图组件是在API请求获取所有必要数据后呈现的,因此监听该事件当视图渲染完毕之后创建快照
            renderAfterDocumentEvent: "render-event"
          })
        })
      );
      config.plugins = [...config.plugins, ...plugins];
    }
}

可能会踩到的坑

  • 一般会在router中对根地址'/'做重定向至'/index',此时在postProcess传入的参数中route:'/index'originalRoute:'/',需要做一次赋值让根目录下的index.html中有index路径下的内容
  • 挂在VUE实例和触发renderAfterDocumentEvent的时机:

DOMContentLoaded的回调中挂载实例确保挂载目标容器存在

使用$mount显式调用开启编译挂载而不是提供el选项立即进入编译

new Vue()mounted中添加一个定时器,在定时器中使用window.document.dispatchEvent(new Event("render-event"))触发renderAfterDocumentEvent事件来确保页面数据加载渲染完成

  • 路由模式history模式,因为预渲染的静态文件都会同步到服务器上,hash不会带到服务器,路由信息会丢失

  • 预渲染之后的页面点击事件失效/JS不运行:因为预渲染之后的文件运行在客户端(浏览器)时会再次执行Vue实例化挂载的过程,而在打包编译预渲染的过程中id="app"的元素已经被替换了,因此在客户端执行的Vue无法正确挂载实例

解决办法:在路由容器外层元素中(一般是App.vueclass="main"的元素)再添加id="app"属性,提供给客户端的Vue进行实例挂载

  • 路由跳转的时候使用$router.push('/brand')而不是$router.push('brand'),后面的跳转匹配不到服务端的文件会发生404