VUE SEO方案一 - 預渲染及其cdn配置
項目接入VUE這樣的框架后,看起來真是太漂亮了,奈何與MCV框架比起來,單頁應用程序卻滿足不了SEO的業務需求,首屏渲染時間也是個問題。總不能白學VUE,預渲染和SSR還是要搞起來。
1.原理
為什么做服務端渲染之前先去了解了預渲染呢?因為預渲染方案相比服務端渲染簡單太多了,而且並不是所有項目都需要服務端渲染的。
預渲染是怎么實現的呢?原理很簡單,在項目開發完成之后,將有限的需要SEO的頁面挑選出來,借助prerender-spa-plugin
插件實行一次瀏覽器渲染,再將渲染好的源代碼(.html)打包到項目包中,為這些頁面在服務端額外配置路由(不再返回首頁模板index.html)。這樣這些頁面就有單獨的靜態頁面,從而解決了SEO和首屏問題。
2.適用場景及其優劣
預渲染適合那些SEO需求頁面有限的應用,比如一個功能型的app。當只有一些推廣頁面需要SEO的時候,針對這些頁面預渲染就非常方便了。相反,如果是一個資訊型的app,有成千上萬的文章頁面,這時再去做預渲染就比較坑了。
另外,如果預渲染的頁面中含有一些動態的數據,那么預渲染的頁面首屏數據不會自動更新(因為那是我們已經事先渲染好的,當然不會和用戶需要的一致)。這種情況就需要事先布置好骨架屏,等等頁面vue接管后重新渲染正確的數據,以免出現不對的數據。當然如果這些數據是SEO相關的,那預渲染就解決不了了。
所以個人認為,預渲染已經可以解決多數SEO問題已經首屏速度問題了,實在不行再上SSR。
3.開始配置
首先我們需要安裝預渲染插件prerender-spa-plugin
1npm i -D prerender-spa-plugin
對於整個項目都部署在靜態服務根目錄下的只要簡單的配置就好了
1// vue.config.js
2const path = require('path')
3const resolve = dir => path.join(__dirname, dir)
4const PrerenderSPAPlugin = require('prerender-spa-plugin')
5const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
6
7module.exports = {
8 outputDir: resolve('dist'),
9 publicPath: '/',
10 indexPath: resolve('dist/index.html'),
11 ...
12 chainWebpack: config => {
13 if(process.env.NODE_ENV === 'production'){
14 config.plugin('prerender-spa-plugin')
15 .use(PrerenderSPAPlugin, [{
16 // 必須 - app出口,和outputDir一致就好.
17 staticDir: resolve('dist'),
18 // 必須 - 需要預渲染的路由.
19 routes: ['/', '/about/'],
20 // 必須 - 模擬瀏覽器渲染
21 renderer: new Renderer({
22 // 渲染時打開瀏覽器debug
23 headless: false,
24 // 渲染時機,正確的時機保證渲染頁面的完整性
25 renderAfterDocumentEvent: 'render-event'
26 })
27 }])
28 }
29 }
30}
31
32//main.js
33new Vue({
34 router,
35 store,
36 render: h => h(App)
37 mounted () {
38 // 這里的觸發事件就是renderAfterDocumentEvent中的事件
39 document.dispatchEvent(new Event('render-event'))
40 }
41})
4.cdn的坑
大部分情況下,上面的配置是無法滿足項目需求的,因為一般我們會把模板和靜態資源分開部署,使用cdn加速。這時候配置就有一點繁瑣了。
1// vue.config.js
2const path = require('path')
3const resolve = dir => path.join(__dirname, dir)
4const PrerenderSPAPlugin = require('prerender-spa-plugin')
5const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
6
7module.exports = {
8 outputDir: process.env.NODE_ENV === 'production' ? resolve('dist') : resolve('/www/statics/'),
9 publicPath: process.env.NODE_ENV === 'production' ? 'http://localhost/' : '/',
10 indexPath: resolve('dist/index.html'),
11 ...
12 if(process.env.NODE_ENV === 'production'){
13 config.plugin('prerender-spa-plugin')
14 .use(PrerenderSPAPlugin, [{
15 staticDir: resolve('dist'),
16 indexPath: resolve('/www/statics/index.html'),
17 routes: ['/', '/about/'],
18 postProcess (renderedRoute) {
19 renderedRoute.html = renderedRoute.html.replace(/http:\/\/localhost/g, 'http://cdn.path/')
20 return renderedRoute
21 },
22 renderer: new Renderer({
23 injectProperty: '__PRERENDER_INJECTED__',
24 inject: 'prerender',
25 headless: false,
26 renderAfterDocumentEvent: 'render-event'
27 })
28 }])
29 }
30}
首先我們將輸出模板配置到
indexPath: resolve('dist/index.html')
中但是我們分離了靜態資源,
outputDir: resolve('/www/statics/')
分離了靜態資源后,預渲染入口
index.html
也會打包到resolve('/www/statics/')
中,因此要為PrerenderSPAPlugin
配置indexPath: resolve('/www/statics/index.html')
這時,按理我們應該將靜態資源配置為
publicPath:http://cdn.path/
。但是這樣預渲染的時候因為線上cdn中並沒有對應的資源,因此會渲染失敗。我們需要預先配置一個能夠訪問到本地資源的地址,比如publicPath:http://localhost/
(這邊的http://localhost/是我項目中自己開的本地服務),這樣預渲染服務器就能渲染成功了,但是靜態html中的靜態資源地址都變成了http://localhost/
,因此我們在PrerenderSPAPlugin
中配置postProcess
,在其中將http://localhost/
替換為http://cdn.path/
,再返回出的html就是擁有正確cdn地址的文件了,如果其中有多個cdn或者其他需求也可以自行匹配替換。看似成功了,但是我們發現,頁面中vue接管的時候,webpack中的
baseUrl
也成了http://localhost/
,因此從chunk-vendors.js中異步加載的組件.js
的下載域名也是http://localhost/
,因此加載錯誤,所以這邊我們需要額外配置webpack。首先配置
PrerenderSPAPlugin
的renderer屬性injectProperty: 'PRERENDER_INJECTED__'`與`inject: 'prerender'`,這樣,在預渲染的時候會配置參數`window.__PRERENDER_INJECTED = prerender
在入口頁面的最上方我們引入配置文件
@/common/prerender.js
1import '@/common/prerender'
2import Vue from 'vue'
3import App from './App.vue'
4import { createRouter } from './router'
5import { createStore } from './store'
6...
7
8// prerender.js
9/* eslint camelcase: "off" */
10if (process.env.NODE_ENV === 'production') {
11 let isPrerender = window.__PRERENDER_INJECTED__ === 'prerender'
12 __webpack_public_path__ = isPrerender ? 'http://localhost/' : 'http://cdn.path/'
13}
14// 此配置在預渲染模式下繼續使用'http://localhost/'這樣的本地域名,而正常打包時,這替換為'http://cdn.path/',這樣項目就可以正常運行了
15// 因為'__webpack_public_path__'命名不符合eslint,但是我們又需要此規則,所以'prerender.js'第一行加注釋'/* eslint camelcase: "off" */'
16//同時因為'__webpack_public_path__'變量是webpack的全局屬性,所以配置:
17globals: {
18 __webpack_public_path__: 'writable'
19},
5.最后
預渲染的頁面被服務器返回之后,瀏覽器就會自動抓取頁面信息,首屏渲染之后,vue引擎啟動並接管頁面,這時vue會根據自己加載的組件.js
重新渲染頁面,這顯然是不必要的,因此我們需要在layout-App.vue
的根元素(<div>
)中添加data-server-rendered="true"
屬性,這樣vue就知道dom是服務端渲染的,它會自動比較並更新需要更新的dom,不會全部重新渲染
