prerender-spa-plugin预渲染踩坑

为何要使用预渲染?

为了应付SEO(国内特别是百度)考虑在网站(vue技术栈系列)作一些优化。大概有几种方案能够考虑:css

服务端作优化:html

第一,ssr,vue官方文档给出的服务器渲染方案,这是一套完整的构建vue服务端渲染应用的指南,具体参考https://cn.vuejs.org/v2/guide/ssr.html前端

第二,nuxt 简单易用,参考网站 https://zh.nuxtjs.org/guide/installationvue

 
前端作优化:
第三,vue-meta-info + prerender-spa-plugin作预渲染,这个是针对单页面的meta SEO的另外一种思路,参考网站 https://zhuanlan.zhihu.com/p/29148760
第四,phantomjs 页面预渲染,具体参考 phantomjs.org (已经暂停维护了)
甚至我一度考虑过第五种方案来应付百度:作假html节点(节点最终不展现出来)。
 
权衡了一下,作服务端渲染是没有人力物力了,因此选用了预渲染的方式来处理(第三种),其中遇到几个大坑,记录一下。

 

1. 下载/安装失败

这个问题有网友遇到的比我多,直接引用解决方案:https://blog.csdn.net/wangshu696/article/details/81253124node

基本上是使用cnpm/高版本node都能解决掉。webpack

 

2. 最大的一个坑:CDN支持。

网络上有解决方案,这篇文章写的比较清楚:http://www.javashuo.com/article/p-ypaixyaq-ct.htmlnginx

在github上也有对应的问题,解决方案主要是上面连接中的第三种。https://github.com/chrisvfritz/prerender-spa-plugin/issues/114,里面提供的demo也差很少:https://github.com/Dhgan/prerender-cdn-demo【注:这个例子实际有一个问题,预渲染处理html替换是匹配时会多出一个“/”,好比“https://www.cdn.com//test.img”,正则须要改一下,能够看我下面的例子】git

重点在于理解预渲染的原理:在webpack打包结束并生成文件后(after-emit hook),启动一个server模拟网站的运行,用puppeteer(google官方的headless chrome浏览器)访问指定的页面route,获得相应的html结构,并将结果输出到指定目录,过程相似于爬虫。github

因此CDN配置预渲染失败缘由很简单:在启用puppeteer爬虫时,你的资源在CDN上根本就没有(其余诸如图片资源还好说,可是js资源都没有,咋渲染啊)。
 

个人方案(掘金文章描述的第三种方案:利用webpack的全局变量和正则替换):

网络上方案是提供了,可是貌似细节都不是很全面,这里本人全面的讲述一下。
原理:在webpack打包时使用和本地环境同样的配置,保证puppeteer爬虫时成功,而后分红两步一块儿来来加上CDN:
  • 第一步,对于生成的html文件,使用正则方式将资源的引用路径替换为CDN引用;
  • 第二步,对于解析js时才发起的资源请求,给webpack运行时暴露的全局变量__webpack_public_path__设置publicPath,相关文档,能够用于项目运行时动态加载的js/css修改为cdn域名。
 
第一步处理:
有两个注意事项:
1. 预渲染中output的publicPath须要和预渲染中处理html的正则配对使用。好比网上的例子基本都使用默认值:空字符''(或者不设置)。一旦设定了非空字符的值,预渲染的html匹配要对应修改。网上的例子为:红色字体部分要配对使用,
//webpack.common.js
{
output: {
filename: '[name].js',
path: config.outPath,
// 须要注意,预渲染的publicPath要和PrerenderSPAPlugin中的匹配规则对应
publicPath: '' // 设置成默认值或者不设置也能够
}
}


// webpack.prod.js









{
plugins: [
new PrerenderSPAPlugin({
staticDir: config.build.assetsRoot,
routes: [ '/', '/about', '/contact' ],
postProcess (renderedRoute) {
// add CDN
renderedRoute.html = renderedRoute.html.replace(
/(<script[^<>]*src=\")((?!http|https)[^<>\"]*)(\"[^<>]*>[^<>]*<\/script>)/ig,
`$1${config.build.cdnPath}$2$3`
).replace(
/(<link[^<>]*href=\")((?!http|https)[^<>\"]*)(\"[^<>]*>)/ig,
`$1${config.build.cdnPath}$2$3`
)

return renderedRoute
},

renderer: new Renderer({
injectProperty: '__PRERENDER_INJECTED__',
inject: 'prerender'
})
})

]
}
























 本人的实际运用比上面要复杂一些,publicPath保留了以前项目的值"/",对应的匹配也就要更改。并且添加了对img标签/内联图片以及部分项目特有的处理。web

publicPath要以"/"结尾的,相关文档,因此cdnPath要以“/”结尾

// webpack.common.js
{ output: { filename: '[name].js', path: config.outPath, // 须要注意,预渲染的publicPath要和PrerenderSPAPlugin中的匹配规则对应
        publicPath: '/' } } // webpack.prod.js
webpackConfig.plugins.push(new PrerenderSPAPlugin({ // Required - The path to the webpack-outputted app to prerender.
 staticDir: config.outPath, // indexPath: path.join(config.outPath, 'index.html'),
    // Required - Routes to render.
    routes: [ '/', '/course', '/to-class', '/declare', '/agreement', '/user'], postProcess (renderedRoute) { // add CDN
        // 因为CDN是以"/"结尾的,因此资源开头的“/”去掉
        renderedRoute.html = renderedRoute.html.replace( /(<script[^<>]*src=\")(?!http|https|\/{2})\/([^<>\"]*)(\"[^<>]*>[^<>]*<\/script>)/ig, `$1${config[env].assetsPublicPath}$2$3` ).replace( /(<link[^<>]*href=\")(?!http|https|\/{2})\/([^<>\"]*)(\"[^<>]*>)/ig, `$1${config[env].assetsPublicPath}$2$3` ).replace(/(<img[^<>]*src=\")(?!http|https|data:image|\/{2})\/([^<>\"]*)(\"[^<>]*>)/ig, `$1${config[env].assetsPublicPath}$2$3` ).replace(/(:url\()(?!http|https|data:image|\/{2})\/([^\)]*)(\))/ig,// 样式内联,格式必须是":url(/xxx)",其余格式都不行【用来剔除js代码中相似的字段】
                `$1${config[env].assetsPublicPath}$2$3` ).replace(/(<div class="dialog_mask_\w+">)[\s\S]*<\/div>(<\/body>)/ig, `$2`)// 去掉警告弹窗(由于部分调用比较早的ajax会报错致使多出了弹出框)

        return renderedRoute }, renderer: new Renderer({ injectProperty: '__PRERENDER_INJECTED__', inject: 'prerender', renderAfterDocumentEvent: 'render-event' }) }));
View Code

 publicPath和postProcess配对使用的,postProcess中的匹配有小改动,目的是为了剔除重复的"/"。其中config[env].assetsPublicPath是本人的CDN路径变量。

 

第二步处理

为何要第二步处理?若是vue中的加载 全是同步的加载就没有必要,若是存在 异步的加载(好比异步路由/异步js),此时彻底可能在js中发起另外一个js资源的请求,这个请求再也不html中,上一步没法处理,就须要动态加上CDN前缀。
 这里步有三个处理,首先在预渲染配置中注入变量,
webpackConfig.plugins.push(new PrerenderSPAPlugin({ // 。。。
        renderer: new Renderer({ injectProperty: '__PRERENDER_INJECTED__', inject: 'prerender', renderAfterDocumentEvent: 'render-event' // vue可能须要使用预渲染什么时候开始的事件 }) }));

如上,注入了__PRERENDER_INJECTED__属性,值为"prerender"。

而后使用new webpack.DefinePlugin()向运行时注入变量:process.env.CDN_PATH,如:

new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV), CDN_PATH: JSON.stringify(config[env].assetsPublicPath) } }),

而后再工程的根目录下创建一个public-path.js文件,内容以下

/** * CDN */
/* eslint-disable */ const isPrerender = window.__PRERENDER_INJECTED__ === 'prerender'
// 预渲染过程当中使用相对路径来处理模拟浏览器爬取节点(不然会由于CDN找不到资源而卡住) // 因此预渲染时使用'/'和publicPath一致,真正运行时值为process.env.CDN_PATH
__webpack_public_path__ = isPrerender ? '/' : process.env.CDN_PATH

注意上面红色字体部分,预渲染时使用的路径要和配置的publicPath一致。

 

并在app入口js引用他

import '../public-path'; import Vue from 'vue';

 特别注意:使用相似mini-css-extract-plugin这样的组件将.vue的style样式提取到外部css,这会致使js中添加的__webpack_public_path__在css中不起做用,好比外链css中出现

background:url(/static/img/icon-question.f05e67f.svg) top no-repeat;

 js中的CDN变量就失去做用了。须要想额外办法,解决方案有两种:

1.要么不在css中直接引用图片(在模板中插入背景url),这个用着会比较难受。

2.【推荐使用】在webpack打包时直接给全部图片资源的publicPath配置上CDN路径,图片资源在预渲染加载失败并不会致使整个预渲染失败,放心大胆使用,好比本人的

{
test: /\.(png|jpe?g|gif|svg|ico)(\?.*)?$/,
loader: 'url-loader',
exclude: [path.resolve(__dirname,'../src/assets/fonts')],
options: {
limit: 100,
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
publicPath: config[env].assetsPublicPath
}
}








 其余非js/css的资源(如字体文件/音频/视频文件等)相似。

额外提示: 多页面(多html入口)的项目能够调用屡次预渲染插件。好比本人的项目除了index.html外,还有一个/h5/index.html为入口的大页面。这个页面本人的调用以下

// h5主页预渲染
    webpackConfig.plugins.push(new PrerenderSPAPlugin({ // Required - The path to the webpack-outputted app to prerender.
 staticDir: config.outPath, // The path your rendered app should be output to.
        // outputDir: path.join(config.outPath, 'h5'),
        indexPath: path.join(config.outPath, 'h5/index.html'), // Required - Routes to render.
        routes: ['/h5', '/h5/about', '/h5/invite', '/h5/purchase/starter'], postProcess (renderedRoute) { // add CDN
            // 因为CDN是以"/"结尾的,因此资源开头的“/”去掉
            renderedRoute.html = renderedRoute.html.replace( /(<script[^<>]*src=\")(?!http|https|\/{2})\/([^<>\"]*)(\"[^<>]*>[^<>]*<\/script>)/ig, `$1${config[env].assetsPublicPath}$2$3` ).replace( /(<link[^<>]*href=\")(?!http|https|\/{2})\/([^<>\"]*)(\"[^<>]*>)/ig, `$1${config[env].assetsPublicPath}$2$3` ).replace(/(<img[^<>]*src=\")(?!http|https|data:image|\/{2})\/([^<>\"]*)(\"[^<>]*>)/ig, `$1${config[env].assetsPublicPath}$2$3` ).replace(/(:url\()(?!http|https|data:image|\/{2})\/([^\)]*)(\))/ig,// 样式内联,格式必须是":url(/xxx)",其余格式都不行【用来剔除js代码中相似的字段】
                `$1${config[env].assetsPublicPath}$2$3` ).replace(/(<div class="dialog_mask_\w+">)[\s\S]*<\/div>(<\/body>)/ig, `$2`)// 去掉警告弹窗(由于部分比较早的ajax会报错)

            return renderedRoute }, renderer: new Renderer({ injectProperty: '__PRERENDER_INJECTED__', inject: 'prerender', renderAfterDocumentEvent: 'render-h5-event' }) }));
View Code

 

 3.Vue预渲染以后报:behavior.js:149 [Vue warn]: Cannot find element: #app

缘由是:预渲染模拟浏览器加载页面后,爬取页面节点,这个时候页面index.html的 节点"<div id="app"></div>"已经被替换成对应的组件了。预渲染的vue2的demo中能够看到app.vue模板的div设置了
<template>
    <div id="app">
        ...
    </div>
</template>
因此,须要咱们手动在index.vue中加上这个id="app"
 

4. 微信须要受权的页面的预渲染问题

这类页面很差生成预渲染页面(受权报错),建议不生成。

 

5. 页面加载闪现首页

部分路由是没有作预渲染的,这部分路由在nginx配置的时候每每默认指向index.html好比相似下面的配置

location / { try_files $uri $uri/index.html /index.html; #root /static/front; #站点目录 已经配置了全局root }

因为对首页作了预渲染,因此index.html默认有不少内容的。

解决方案有两种:

  1. 默认根节点隐藏,合适时机再显式出来:https://blog.csdn.net/Christiano_Lee/article/details/94569119。(感受思路可行,可是本人没有实践,后面实践后再加上评论)
  2. 新增一个空页面,路由为'/empty',并为这个路由作预渲染,nginx配置中没有匹配的路由默认指向加载此页面。nginx配置改成
location / { try_files $uri $uri/index.html /empty/index.html; # /index.html; #root /static/front; #站点目录 已经配置了全局root }

 

 

小提示:
  prerender-spa-plugin插件和vue-meta-info插件配合使用效果更佳!
  在预渲染配置过程当中颇有可能那一步出错了而后预渲染失败,让你很抓狂!!!那么请将预渲染的配置改成:headless false
相关文章
相关标签/搜索