什么是预渲染
根据 Vue 文档描述来看,预渲染:即无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
为什么需要预渲染
使用 Vue 技术开发的 SPA (单页应用程序 (Single-Page Application)) 对 SEO 不友好,如果项目有 SEO 的需求,需要使用服务器端渲染(SSR)或预渲染的方式来满足。对当前开发的项目而言,我们仅仅只有少数营销页面(例如 /
, /about
, /contact
等)需要 SEO,所以选择采用预渲染的方式实现。点击这里查看 SSR
和 预渲染
的比较。
预渲染工具库
使用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 静态文件,算是暂时解决了这个“莫名其妙”的坑,不算完全解决上面出现的坑,后续再观望观望。