解决 prerender-spa-plugin 插件无法对 Vue.js SPA 进行预渲染问题

程序员

什么是预渲染

根据 Vue 文档描述来看,预渲染:即无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。

为什么需要预渲染

使用 Vue 技术开发的 SPA (单页应用程序 (Single-Page Application)) 对 SEO 不友好,如果项目有 SEO 的需求,需要使用服务器端渲染(SSR)或预渲染的方式来满足。对当前开发的项目而言,我们仅仅只有少数营销页面(例如 /, /about, /contact 等)需要 SEO,所以选择采用预渲染的方式实现。点击这里查看 SSR预渲染 的比较。

预渲染工具库

prerender-spa-plugin

使用prerender-spa-plugin库把部分静态页面预渲染,prerender-spa-plugin库会依赖 google 的 puppeteer工具,在安装的时候,puppeteer会下载一个 chromium,大小约为 140M,安装时,终端最好开启代理(梯子),如果没有梯子可能会安装失败或卡死。安装成功后,你会在本地项目路径中看到下载的 chromium:\node_modules\puppeteer\.local-chromium\win64-662092\chrome-win

注:puppeteer 是 google chrome 团队官方开发的无界面(headless)chrome 工具,puppetter 可以实现生成网页页面的截图和 PDF、抓取 SSR、抓取网站内容、模拟登陆等

实现

环境

* windows 10 64位
* node v10.15.3
* npm 6.4.1
* yarn 1.13.0
* vue-cli 3.5.0
* vue 2.6.6
* vue-router 3.0.3
* prerender-spa-plugin 3.4.0

3.0 安装 vue-cli 脚手架

npm install -g @vue/cli# ORyarn global add @vue/cli

更多实现请查看官方文档

3.1 安装 prerender-spa-plugin

打开初始化的项目,终端输入下面命令

npm install prerender-spa-plugin --save

3.2 配置 vue.config.js

项目根目录下如果没有 vue.config.js,则手动创建一个,存在则继续在 vue.config.js 文件中增加:

const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
module.exports = {
  configureWebpack: () =>
  {
    if (process.env.NODE_ENV === "production") {
      return {
        plugins: [
          new PrerenderSPAPlugin({
            // 【必须】生成文件的路径,也可以与webpakc打包的一致
            staticDir: path.join(__dirname, "dist"),            // 【必须】对应自己的路由文件,如路由有参数,则需写成 /a/param1
            routes: ["/", "/about", "/news", "/contact"],       //【可选】服务器端口配置
            server: {
              port: 8188
              },
              // 这个很重要,如果没有配置这段,也不会进行预编译
              renderer: new Renderer({
                inject: {
                  foo: "bar"
                  },
                  headless: false, // 对应 src/main.js 中 document.dispatchEvent(new Event('custom-render-trigger')),两者的事件名称要相同
                  renderAfterDocumentEvent: "custom-render-trigger"
                })
              })
            ]
          };
        }
      }
    }

prerender-spa-plugin更多高级用法请查看官方文档

3.3 编辑 src/main.js

new Vue({    router,    render: h => h(App),+ mounted () {+    document.dispatchEvent(new Event('custom-render-trigger'))+ }}).$mount('#app')

3.4 配置router.js

将路由模式改为 mode: "history",因 prerender-spa-plugin 仅适用于使用 HTML5 的 history 路由,使用 hash 路由将不起作用。

3.5 打包预渲染

满心欢喜在终端敲下 npm run build,等待奇迹时,莫名其妙的坑出现了

“莫名其妙”的坑

运行 build 命令后,“莫名其妙”的坑如下:

> vue-cli-service build-  Building for production...{ TimeoutError: Timed out after 30000 ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r662092    at Timeout.onTimeout (E:\github\DeGao-FrontEnd\node_modules\puppeteer\lib\Launcher.js:353:14)    at ontimeout (timers.js:436:11)    at tryOnTimeout (timers.js:300:5)    at listOnTimeout (timers.js:263:5)    at Timer.processTimers (timers.js:223:10) name: 'TimeoutError' }[Prerenderer - PuppeteerRenderer] Unable to start Puppeteer(node:940) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'close' of null    at PuppeteerRenderer.destroy (E:\github\DeGao-FrontEnd\node_modules\@prerenderer\renderer-puppeteer\es6\renderer.js:140:21)    at Prerenderer.destroy (E:\github\DeGao-FrontEnd\node_modules\@prerenderer\prerenderer\es6\index.js:87:20)    at PrerendererInstance.initialize.then.then.then.then.then.then.then.then.catch.err (E:\github\DeGao-FrontEnd\node_modules\prerender-spa-plugin\es6\index.js:144:29)(node:940) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)(node:940) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

为什么说是“莫名其妙”的坑,因为在这项目的前几天,使用预渲染的方式都非常正常,可以正常打包,输出静态文件。但是这个时候就抽风了,开发环境完全相同,当下有点摸不着头脑的感觉。

当然,遇坑则填坑,看终端打印的信息,分析可能有以下原因:

  • puppeteet 无法启动 chromium,权限不足?
  • puppeteet 下载的 chromium 版本错误?
  • puppeteet 下载的 chromium 文件损坏?

针对权限疑惑,检查了项目目录的相关权限,未发现有问题;接着到 chromium 站点下载 Win 64 位的 r662092 版本包,重新覆盖 \node_modules\puppeteer\.local-chromium\win64-662092\chrome-win下的所有文件,重新 build,依旧输出同样错误。

解决不了,只有 Google 大法了,寻寻觅觅一圈依旧没有找到解决方法。当看到 Puppeteer Api 文档时,它提供一个属性 executablePath 人为指定运行绑定的 Chromium 版本默认情况下,Puppeteer 下载并使用特定版本的 Chromium 以及其 API 保证开箱即用。 如果要将 Puppeteer 与不同版本的 chrome 或 chromium 一起使用,在创建Browser实例时传入 chromium 可执行文件的路径即可。 如同看到了希望一样,回到 vue.config.js 文件中,添加 executablePath属性:

module.exports = {
  configureWebpack: () => {
    if (process.env.NODE_ENV === "production") {
      return {
        plugins: [
          new PrerenderSPAPlugin({                 // 【必须】生成文件的路径,也可以与webpakc打包的一致
          staticDir: path.join(__dirname, "dist"), // 【必须】对应自己的路由文件,如路由有参数,则需写成 /a/param1
          routes: ["/", "/about", "/news", "/contact"],            //【可选】服务器端口配置
          server: {
            port: 8188
          },
          // 这个很重要,如果没有配置这段,也不会进行预编译
          renderer: new Renderer({
            inject: {
              foo: "bar"
            },
            headless: false,
            // 对应 src/main.js 中 document.dispatchEvent(new Event('custom-render-trigger')),两者的事件名称要相同
            renderAfterDocumentEvent: "custom-render-trigger",              // 更改运行绑定的 Chromium 版本,即:使用本机 chrome 来执行预渲染操作,注意 win、mac、linux 不同环境的路径指定
            executablePath: 'Your/Path/Google/Chrome/Application/chrome.exe'
            })
          })
        ]
      };
    }
  }
}

重新运行 npm run build,成功打包输出 html 静态文件,算是暂时解决了这个“莫名其妙”的坑,不算完全解决上面出现的坑,后续再观望观望。