最近兩週剛剛接手工程化的工作,領導希望能夠整合一個可以預渲染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.fanqiangma.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預渲染)