本文主要是介紹使用prerender-spa-plugin外掛在針對前端程式碼進行預渲染。
預渲染(SSG)和服務端(SSR)渲染有一定的區別,大家想要了解的話可以看:https://segmentfault.com/a/1190000023469150。
背景
因為之前的網站是使用Vue開發的,這種前端JavaScript渲染的開發模式,對於搜尋引擎來說非常的不友好,沒有辦法抓取到有效的資訊。因此為了進行SEO,我們需要對頁面進行一些預渲染。
預渲染比較適合靜態或者變化不大的頁面,能夠通過部署前的一次靜態渲染,將頁面上大部分內容都渲染出來。這樣搜尋引擎在爬取的時候,就能夠爬到相關的內容資訊。
現狀
目前商企通官網情況列舉如下:
- 技術棧使用的是Vue,腳手架使用的是vue-cli,使用JavaScript前端渲染方案(這個方案對技術棧沒有要求,相容所有方案)
- 釋出工具使用的是公司的工具,打包過程中,HTML資源傳遞到A域名下,CSS、JS、Image等資源傳遞到B域名下。
目標
希望能夠通過預渲染,讓頁面在初次訪問沒有執行JavaScript時,就能夠攜帶足夠的資訊,即將JavaScript渲染的內容提前渲染到HTML中。
釋出期望不做過多的修改。
方案
我們本次方案主要採用的是prerender-spa-plugin這個webpack的外掛來實現的。
它的主要原理是啟動瀏覽器,渲染完成後抓取HTML,然後再替換掉原有HTML。
我們需要實現預渲染,那麼我們需要完成以下幾件事情:
- 外掛引入和配置。
- 本地驗證。
- 改造打包構建流程。
- 線上驗證。
下面,我們一個一個來說下,我們如何做這個事情的。
外掛引入和配置
首先,我們需要引入一個預渲染外掛,執行命令:
mnpm i prerender-spa-plugin -D
這個命令除了安裝外掛本身以外,依賴了puppeteer,然後puppeteer又依賴落地chromium,所以最後我們其實是需要在依賴中安裝一個chromium。
如果大家安裝puppeteer非常慢或者經常失敗,可以參考下這個文件中的方法:https://brickyang.github.io/2019/01/14/國內下載安裝-Puppeteer-的方法/,指定puppeteer下載映象。
安裝完成後,我們就可以在webpack的配置檔案中增加對應的配置了。
如果大家使用的也是vue-cli,那麼我們需要增加的配置是在vue.config.js中,如果是直接修改webpack的配置,那麼方法也是類似。
下面我們以vue.config.js的修改為例:
const PrerenderSPAPlugin = 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: new Renderer({
headless: true,
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),兩者的事件名稱要對應上。
renderAfterDocumentEvent: 'render-event',
}),
},
]);
}
}
}
因為我們在專案中使用了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來觸發的,這樣自己可以控制預渲染的時機。一般我們都是在最外層的元件的mounted鉤子中觸發,如果大家有其他需求也可以自己指定。
更多的可以看外掛的官方文件。
開發完成後,我們可以在本地構建一次,看看是否能夠生成符合我們預期的程式碼。
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中的資原始檔地址替換掉,然後預渲染完成後再替換回來。這個方法比較hack,但是經過實際驗證確實是可以生效。具體的做法是自己寫一個簡單的webpack外掛。
首先,我們需要安裝一個新的NPM包,用來對檔案中的內容進行替換(自己寫正則也可以,不過用這個會方便一些),具體命令如下:
mnpm i replace-in-file
安裝後,我們需要增加兩個webpack的外掛,分別作用在afterEmit和done這兩個鉤子節點上。如果想要了解為什麼是這兩個鉤子節點,那麼你可以閱讀下webpack外掛的開發章節。
const replace = require('replace-in-file');
let publicPath = `//awp-assets.cdn.net/${projectPath}`;
// 第1個替換外掛,主要是將原先打包過程中帶有CDN域名的路徑替換成相對路徑
function ReplacePathInHTMLPlugin1(cb) {
this.apply = compiler => {
if (compiler.hooks && compiler.hooks.afterEmit) {
compiler.hooks.afterEmit.tap('replace-url', cb);
}
};
}
function replacePluginCallback1() {
replace({
files: path.join(__dirname, '/build/**/*.html'),
from: new RegExp(
publicPath.replace(/([./])/g, (match, p1) => {
return `\\${p1}`;
}),
'g'
),
to: '',
})
.then(results => {
console.log('replace HTML static resources success', results);
})
.catch(e => {
console.log('replace HTML static resources fail', e);
});
}
// 第2個替換外掛,主要是將預渲染後的HTML檔案中的相對路徑替換成帶有CDN域名的路徑
function ReplacePathInHTMLPlugin2(cb) {
this.apply = compiler => {
if (compiler.hooks && compiler.hooks.done) {
compiler.hooks.done.tap('replace-url', cb);
}
};
}
function replacePluginCallback2() {
replace({
files: path.join(__dirname, '/build/**/*.html'),
from: [/href="\/css/g, /href="\/js/g, /src="\/js/g, /href="\/favicon.ico"/g],
to: [
`href="${publicPath}/css`,
`href="${publicPath}/js`,
`src="${publicPath}/js`,
`href="${publicPath}/favicon.ico"`,
],
})
.then(results => {
console.log('replace HTML static resources success', results);
})
.catch(e => {
console.log('replace HTML static resources fail', e);
});
}
上述程式碼就是我們需要增加的兩個webpack的替換外掛和對應的回撥函式,接下來我們看下在webpack中怎麼配置。
module.exports = {
publicPath,
outputDir,
crossorigin: 'anonymous',
chainWebpack: config => {
config.plugin('replaceInHTML').use(new ReplacePathInHTMLPlugin1(replacePluginCallback));
config.plugin('prerender').use(PrerenderSPAPlugin, [
{
staticDir: path.join(__dirname, 'build'),
// 我們應該只會使用根路徑,因為是hash路由,所以其他頁面預渲染沒有意義,因此不進行預渲染
routes: ['/'],
renderer: new Renderer({
headless: true,
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),兩者的事件名稱要對應上。
renderAfterDocumentEvent: 'render-event',
}),
},
]);
config.plugin('replaceInHTML2').use(new ReplacePathInHTMLPlugin2(replacePluginCallback2));
}
我們第一個替換外掛,需要在預渲染外掛前執行,在預渲染外掛執行前,將HTML中的資源的地址替換成本地的相對路徑;第二個則需要在替換後執行,這樣將預渲染後端資源中的相對路徑,再替換成CDN地址。
通過這兩個外掛,我們就可以完成在預渲染前替換掉路徑完成預渲染,然後在預渲染後再完成替換保證線上可用。
本地驗證
通過上面的方式,我們應該已經得到了一個預渲染完成的HTML,接下來我們就是要驗證下這個HTML是否符合預期了。
比較簡單的驗證方式,可以直接訪問那個HTML檔案,或者啟動一個HTTP靜態資源服務來驗證。
驗證的話,你可以使用curl來進行請求,這種情況下JavaScript不會執行,你可以看到HTML的原始檔是什麼。
FAQ
- 在chrome版本比較低的情況下(比如v73),會提示渲染失敗?
這個是因為chrome的版本過低,導致預渲染失敗。解決方案是升級chrome/chromium版本到最新(目前v93無問題)版本即可。
總結
如果我們需要實現SSG(靜態站點生成),那麼我們可以使用prerender-spa-plugin這個外掛來做,這個外掛可以在本地啟動chromium來抓取HTML內容,再寫回HTML檔案中,如我們我們需要對其中的靜態資原始檔進行處理,我們可以使用替換的外掛,針對處理前後的內容進行替換,來達到我們的訴求。
直接替換壓縮後程式碼雖然看起來有效,但是這個強依賴壓縮的演算法和內容順序,強烈不推薦直接用指令碼修改替換壓縮後檔案,最好是在webpack的done鉤子回撥中處理。