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
相關文章
相關標籤/搜索