本文主要介绍使用prerender-spa-plugin插件对前端代码进行预渲染。预渲染(SSG)和服务器端(SSR)渲染之间存在一定的区别。想了解更多可以看:https://segmentfault.com/a/1190000023469150。背景由于之前的网站是使用Vue开发的,这种前端JavaScript渲染的开发模式对搜索引擎非常不友好,没有办法抓取到有效信息。因此出于SEO目的,我们需要对页面进行一些预渲染。预渲染更适合静态或变化不大的页面。它可以在部署前通过静态渲染呈现页面上的大部分内容。这样搜索引擎在抓取的时候,就可以抓取到相关的内容信息。商奇通官网目前的状态罗列如下:技术栈使用Vue,脚手架使用vue-cli,使用JavaScript前端渲染方案(该方案对技术栈无要求,兼容)与所有解决方案)。发布工具使用公司的Tool,打包过程中HTML资源投递到域名A,CSS、JS、Image等资源投递到域名B,目标是开启预渲染,让页面在没有第一次执行JavaScript时携带足够的信息,即将JavaScript渲染的内容提前渲染成HTML。该版本预计不会进行太多更改。解决方案我们这次的解决方案主要是通过webpack插件prerender-spa-plugin来实现的。它的主要原理是启动浏览器,抓取渲染后的HTML,然后替换原来的HTML。我们要实现预渲染,所以需要完成以下几件事:插件的引入和配置。本地认证。改进了打包构建过程。在线验证。接下来我们就一一说说我们是怎么做到的。插件介绍及配置首先,我们需要引入一个预渲染插件,执行命令:mnpmiprerender-spa-plugin-D这个命令除了安装插件本身外,还依赖于puppeteer,以及然后puppeteer依赖登陆chromium,所以最后我们其实是需要在dependencies中安装一个chromium。如果你安装puppeteer很慢或者经常失败,可以参考这个文档中的方法:https://brickyang.github.io/2019/01/14/国内下载安装-Puppeteer-Method/,指定puppeteer下载图片。安装完成后,我们就可以在webpack的配置文件中添加相应的配置了。如果你也在使用vue-cli,那么我们需要添加的配置就在vue.config.js中。如果直接修改webpack的配置,那么方法类似。我们以vue.config.js的修改为例:constPrerenderSPAPlugin=require('prerender-spa-plugin');module.exports={...,configureWebpack:{...,chainWebpack:config=>{config.plugin('prerender').use(PrerenderSPAPlugin,[{staticDir:path.join(__dirname,'build'),routes:['/','/product','/case','/about','/register',],renderer:newRenderer({headless:true,executablePath:'/Applications/GoogleChrome.app/Contents/MacOS/GoogleChrome',//在main.jsdocument.dispatchEvent(newEvent('render-event'))中,两者的事件名称要对应。renderAfterDocumentEvent:'渲染事件',}),},]);}}}因为我们在项目中使用了webpack-chain,所以我们的语法类似于上面的链式调用方式。如果直接修改,会使用vue原有的配置修改方式。下面给大家简单介绍一下上面几个配置的含义:staticDir:这里指的是输出预渲染文件的目录。routes:这里指的是需要预渲染的路由。这里需要注意的是,vue的hash路由策略是没有办法预渲染的,所以如果要预渲染,需要改成history路由,预渲染后会变成多个html文件,每个文件都有全量路由功能,只是默认路由不同。renderer:这个是puppeteer的配置,可以传入。说一下我用过的配置:-headless:是否使用headless模式渲染,建议选择true。-executablePath:指定chromium的路径(也可以是chrome)。这个配置需要在talos中指定,talos中的chrome地址默认为/usr/bin/google-chrome。-renderAfterDocumentEvent:这意味着在触发哪个事件后,将获取预渲染。这个事件需要在代码中使用dispatchEvent来触发,这样可以控制预渲染的时机。一般我们在最外层组件的挂载钩子中触发。如果有其他需求,也可以自己指定。更多可以在插件的官方文档中找到。开发完成后,我们可以在本地进行构建,看看能否生成符合我们预期的代码。Vue.config.js指定publicPath导致预渲染失败。如果你和我的项目一样,在vue.config.js中传入publicPath指定第三方CDN域名,CSS、JavaScript、Image等资源会投递到不同的域名,类似的配置如下:module.exports={...,publicPath:`//awp-assets.cdn.net/${projectPath}`,...,};如果没有预渲染,这个方案会在打包完成后上传到不同的CDN域名,在线访问是没有问题的。但是在本地,此时CSS和JS资源还没有上传到CDN,浏览器无法加载相应的资源来渲染页面,会导致本地预渲染失败。为了解决这个问题,有两种解决方案。【建议】调整打包策略,将非HTML资源上传至同CDN域名。这样,我们就可以使用相对路径来访问这些资源,而无需将新的域名传递给publicPath,这样我们在本地构建时就可以访问这些值了。这是一种比较可靠合理的方法,推荐使用。(如果以上方法实在不行,可以考虑这个方案)在预渲染之前,可以通过相对路径在本地访问资源。这个时候用替换的方式替换HTML中的资源文件地址,然后pre-render渲染完成后替换掉。这种方法比较hack,但经过实际验证确实有效。具体方法是自己写一个简单的webpack插件。首先,我们需要安装一个新的npm包来替换文件中的内容(你也可以自己写regex,不过用这个会更方便),具体命令如下:mnpmireplace-in-file安装后,我们需要添加两个webpack插件,分别作用于afterEmit和done的两个hook节点。如果你想了解为什么会出现这两个hook节点,那么你可以阅读webpack插件的开发章节。constreplace=require('replace-in-file');letpublicPath=`//awp-assets.cdn.net/${projectPath}`;//第一个替换插件主要是带CDN替换路径具有相对路径的域名functionReplacePathInHTMLPlugin1(cb){this.apply=compiler=>{if(compiler.hooks&&compiler.hooks.afterEmit){compiler.hooks.afterEmit.tap('replace-url',cb);}};}functionreplacePluginCallback1(){replace({files:path.join(__dirname,'/build/**/*.html'),from:newRegExp(publicPath.replace(/([./])/g,(match,p1)=>{return`\\${p1}`;}),'g'),to:'',}).then(results=>{console.log('替换HTMLstaticresourcessuccess',results);}).catch(e=>{console.log('替换HTML静态资源失败',e);});}//第二个替换插件主要是到相对路径中渲染后的HTML文件被替换成CDN域名的路径点击('放置url',cb);}};}functionreplacePluginCallback2(){replace({files:path.join(__dirname,'/build/**/*.html'),from:[/href="\/css/g,/href="\/js/g,/src="\/js/g,/href="\/favicon.ico"/g],到:[`href="${publicPath}/css`,`href="${publicPath}/js`,`src="${publicPath}/js`,`href="${publicPath}/favicon.ico"`,],}).then(results=>{console.log('替换HTML静态资源成功',results);}).catch(e=>{console.log('替换HTML静态资源失败',e);});}上面的代码是我们的两个webpack替换插件和对应的回调需要添加功能。接下来我们看看如何在webpack中配置它们module.exports={publicPath,outputDir,crossorigin:'anonymous',chainWebpack:config=>{config.plugin('replaceInHTML').use(newReplacePathInHTMLPlugin1(replacePluginCallback));config.plugin('prerender').use(PrerenderSPAPlugin,[{staticDir:path.join(__dirname,'build'),//我们应该只使用根路径,因为它是一个哈希路由,所以其他页面预渲染没有意义,所以没有预渲染routes:['/'],renderer:newRenderer({headless:true,executablePath:'/Applications/GoogleChrome.app/Contents/MacOS/GoogleChrome',//inmain.jsdocument.dispatchEvent(newEvent('render-event')),两者的事件名称要对应。renderAfterDocumentEvent:'render-event',}),},]);config.plugin('replaceInHTML2').use(newReplacePathInHTMLPlugin2(replacePluginCallback2));}我们的第一个替换插件需要在预渲染插件之前执行。预渲染插件执行前,将HTML中的资源地址替换为本地相对路径;第二个需要在替换后执行,让预渲染后端资源中的相对路径替换为CDN地址。通过这两个插件,我们可以替换预渲染之前的路径来完成预渲染,然后在预渲染之后完成替换,保证在线可用。本地验证通过上面的方法,我们应该已经得到了一个预渲染的HTML,接下来我们需要验证这个HTML是否符合预期。比较简单的验证方式,可以直接访问HTML文件,或者启动HTTP静态资源服务进行验证。为了验证,你可以使用curl来请求,这样JavaScript就不会被执行,你可以看到HTML的源文件是什么。FAQ在chrome版本比较低的情况下(比如v73)会提示渲染失败?这是因为chrome版本太低,导致预渲染失败。解决方法是将chrome/chromium版本升级到最新(目前v93没问题)版本。总结如果我们需要实现SSG(静态站点生成),那么我们可以使用prerender-spa-plugin插件来实现。这个插件可以在本地启动chromium抓取HTML内容,然后写回HTML文件,因为我们需要对静态资源文件进行处理,我们可以使用replacement插件来替换处理前后的内容,以满足我们的要求。虽然直接替换压缩代码看起来很有效,但它强烈依赖于压缩算法和内容顺序。强烈不建议直接用脚本修改替换压缩文件。最好在webpack的done钩子回调中处理。
