最近两周刚刚接手工程化的工作,领导希望能够集成一个可以预渲染html的插件来解决webpack生成的空白模板问题。
通过调研发现webpack有一个prerender-spa-plugin的插件完全可以满足需求,故将这个插件集成到我们内部的构建流程中。整个过程还是有很多坑的,主要我对node、webpack这些还是小白的阶段,所以犯了很多错误,特意记录一下。
为什么要使用prerender-spa-plugin
主要原因有两点:
1、SEO:SPA应用的SEO比较差,爬虫爬到的页面结构大多只有入口页,然而入口页又几乎没有什么内容,因此搜索排名很低。
2、首屏体验:由webpack生成的入口页基本上只有一个主js,本身页面没有内容,因此在这个js加载完成之前,用户看到的都是一片空白。为了解决这个问题,一般是把规定好的静态页给后台,用户请求直接返回的是静态页,这样在主js加载之前用户还是能看到一些东西的。但是这样也会有问题,比如我们想做一个Loading的组件放到首屏,就非常麻烦,同时也是又一个和后台沟通的成本(其实就是大佬不想总是依赖后台-_-)。所以如果我们自己能直接生成体验良好的首屏页面,那肯定是极好的。
这就是prerender-spa-plugin的作用,它能够直接生成拥有静态结构的html,可以尽情地往首屏里面放入你想展现的东西。
安装
执行npm i prerender-spa-plugin即可,但是如果不能够*的话就会遇到一个坑。
prerender-spa-plugin插件是需要依赖puppeteer的,也就是谷歌出品的无头浏览器插件,这个插件会下载最新版的chromium(大约200M ),所以如果不能*,下载的时候就报错了。
如图中所示,可以通过设置参数跳过该步骤,但是如果省略了,调用的时候还会报错。
网上大多搜到的解决办法都是针对使用puppeteer本身的,通过指定本机路径再启动,但是prerender-spa-plugin在puppeteer外层,因此并不适用。这个问题处理的过程比较长,我放到下面再描述,这里先讲能够*的情况下之后的使用。
webpack中配置
使用的主要的目的就是取代build后生成的html,因此需要修改的就是webpack.prod.conf.js这个文件,一般在这文件中如果使用脚手架工具会有html-webpack-plugin的插件,如果有就不要动了,因为prereder的插件是需要这个插件生成的html作为模板的,只需要在html-webpack-plugin的配置后加上新的配置即可。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseWebpackConfig = require('./webpack.base.conf')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
const webpackConfig = merge(baseWebpackConfig, {
plugins: [
// vue-cli生成的配置中就已有这个了,不要动
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency'
}),
// 在vue-cli生成的文件的基础上,只有下面这个才是我们要配置的
new PrerenderSPAPlugin({
// 生成文件的路径,也可以与webpakc打包的一致。
// 下面这句话非常重要!!!
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
staticDir: path.join(__dirname, '../dist'),
// 对应自己的路由文件,比如index有参数,就需要写成 /index/param1。
routes: ['/', '/index', '/skin', '/slimming', '/exercise', '/alPay', '/wxPay'],
// 这个很重要,如果没有配置这段,也不会进行预编译
renderer: new Renderer({
inject: {
foo: 'bar'
},
headless: false,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
renderAfterDocumentEvent: 'render-event'
})
})
]
})
到此为止,如果只是个人使用的话,就全部OK了,之后在需要预渲染的页面的js中触发docurender-event事件(比如vue中就在main.js的mounted函数中加上dispatchEvent),执行npm run build 就会生成预渲染的html,至于具体的配置项,可以去github上的prerender-spa-plugin查看 https://github.com/chrisvfritz/prerender-spa-plugin
踩坑
首先我们的项目是要集成到所有开发人员都要是使用的工程化工具中,因此就需要考虑刚才安装时候的两个问题:
1、假设所有的开发人员都不能*,要怎么下载这个插件?
2、200M大小的chromium是不是太大?如果每个项目都要装这么大的插件很显然会太臃肿。
针对这两个问题,我们最终讨论的方案如下:
1、将prerender-spa-plugin变成我们自己的组件(简称myPlugin)
2、将myPlugin中的puppeteer依赖指向全局的myPUppeteer(本来是指向它自己的puppeteer)
3、将prerender-spa-plugin中依赖的puppeteer组件也变成我们自己的(用于解除强依赖,简称myPuppeteer)
4、我个人将不同系统的最新版chromium下载到公司的ftp中
5、修改myPuppeteer组件里下载chromium的地址,指向ftp
6、在myPlugin中添加错误提示,当require不到puppeteer时提示开发者全局安装myPuppeteer
这样一通操作的最终效果:我们的开发人员只需要在第一次使用该插件时,全局安装一次myPuppeteer,并且不会有网络问题。
当然每个项目还是要安装myPlugin的
下面按照这个顺序说一下要怎么搞。
实现步骤
步骤一/二
首先要找到哪里依赖了puppeteer
1、prerender-spa-plugin里:
2、@prerender/renderer-puppeteer里:
所以我们需要在@prerender/renderer-puppeteer里替换掉依赖puppeteer的这句话
try{
puppeteer = require('prerender-browser')
}catch(err){
console.log("You are using prerender-html-plugin to generate html but no puppeteer installed");
console.log("If you don't want this function, you can set prerender to false in marauder.config");
console.log("If you want this function, you can run:1、npm install -g prerender-browser 2、npm link prerender-browser");
}
prerender-browser就是上文中的myPuppeteer组件。
try catch是为了能够在使用过程中可以在控制台输出报错语句。
这里可以看到我提示大家执行两句node命令
1、npm install -g prerender-browser
2、npm link prerender-browser
这里涉及到一个全局包的引用问题:
当我全局安装并且代码引用之后,我发现还是会报没有定义的错误。经过查询后发现require会从node_modules中查询所需的包,直到最顶层的全局变量。如果全局安装但并没有生效,就是因为环境变量中没有配置全局的node_modules路径。
所以在网上很多方法都是要去设置环境变量,但还有另一种简单的方法就是直接执行node link <package_name> 这个命令。npm link命令可以将一个任意位置的npm包链接到全局执行环境,这样就不会有引用无法找到的问题了。
步骤三/四/五
在puppeteer的BrowserFetcher.js文件中可以找到DEFAULT_DOWNLOAD_HOST和downloadURLs这两个变量,将这两个变量替换为公司内部可以访问的资源地址即可。
const DEFAULT_DOWNLOAD_HOST = 'http://www.*ma.com';
const downloadURLs = {
linux: '%s/chromium-browser-snapshots/Linux/chrome-linux.zip',
mac: '%s/chromium-browser-snapshots/Mac/chrome-mac.zip',
win32: '%s/chromium-browser-snapshots/Win32/chrome-win32.zip',
win64: '%s/chromium-browser-snapshots/Win64/chrome-win32.zip',
};
这里还有两个坑:
1、在BrowserFetcher.js代码的下载地址上原本会拼上版本号,比如:
const url = util.format(downloadURLs[this._platform], this._downloadHost,revision);
revision就代表版本号,这个版本号是从puppeteer的package.json中读取的,这个参数即用来拼接下载的url,同时还用来指定chromium的执行地址。这里我们只需要修改下载地址,所以我将代码中需要拼接版本号的url都去掉了revision这个参数,否则会提示无法下载。
2、即便安装时能够下载了,但在运行时,会报无法关闭puppeteer实例的错误。看代码发现,puppeteer在启动的时候(launch函数)还需要指定到chromium的安装地址,这里还需要上面的revision参数,修改Puppeteer.js中的启动函数
static launch(options) {
options.executablePath = this.executablePath('revision');
return Launcher.launch(options);
}
这里的代码比较迷,我也没有太看懂,为什么只是修改了下载地址却没有自动指定启动目录,有兴趣的大佬可以探究一下。
TIPS:
提取和发布组件的时候遇到两个小问题:
1、将组件拆分成自己的组件之后,有一些依赖就需要咱们手动添加到package.json中了,这个大家根据提示一一加上就好。
2、在npm发布组件的时候遇到了一个非常白痴的错误,首先淘宝源是不能够发布组件的,必选换成npm源,但是我当时在网上搜到的大多数npm源都是http://www.npmjs.org。这个源安装依赖是不会有问题的,但是如果想要发布必须是 https://registry.npmjs.org,否则就会报错。这个真的是坑死我了,我一开始都没有怀疑过npm源的正确性,而且windos使用npm发包还真的就有issue。。。。。
结尾
到此,所有的工作就全部完成了,经过试验,我们组的小伙伴都可以愉快的生成静态页了。
这是我第一次写文章记录工作中的技术,也是第一次用markdown,可能写的不太好,啰嗦的地方或者没有提到的点还希望见谅,如果有问题欢迎指出。
参考文章:
1、 puppeteer 安装失败的解决办法
2、 vue项目做seo(prerender-spa-plugin预渲染)