服務器端渲染的優勢在於更好的seo以及更快的渲染速度,所以vue也開始支持服務器端渲染,即ssr。
基本知識
要使用服務器端渲染,需要使用server-entry.js和client-entry.js兩個入口文件,兩者都會使用到app.js進行打包,其中通過server-entry.js打包的代碼是運行在node端,二通過client-entry.js打包代碼運行在客戶端。 具體的流程圖如下所示。
從圖上可以看出,SSR 有兩個入口文件,client.js 和 server.js, 都包含了應用代碼,webpack 通過兩個入口文件分別打包成給服務端用的 server bundle 和給客戶端用的 client bundle. 當服務器接收到了來自客戶端的請求之后,會創建一個渲染器 bundleRenderer,這個 bundleRenderer 會讀取上面生成的 server bundle 文件,並且執行它的代碼, 然后發送一個生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 之后,會和服務端生成的DOM 進行 Hydration (判斷這個 DOM 和自己即將生成的 DOM 是否相同,如果相同就將客戶端的Vue實例掛載到這個 DOM 上, 否則會提示警告)。
client bundle就是vue-ssr-client-manifest.json,而server bundle就是vue-ssr-server-bundle.json,這兩個文件都是非常容易獲取的
在純前端渲染時,一般使用的是web-dev-server這個插件,它可以自動幫助我們開啟一個node端,主要作用是監控並打包代碼,但是實際上還是純前端渲染,另外配合web-hot-middleware來進行HMR熱更新,這樣可以在我們改變了代碼之后自動打包並更新view,以此來提高開發效率。
而在vue服務器端渲染時,就不能只是使用web-dev-server和web-hot-middle了,因為我們需要添加服務器渲染的node代碼邏輯,這樣,我們可以自己開一個node服務器,使用webpack-dev-middle中間件進行打包、使用webpack-hot-middle中間件進行熱更新,並添加服務器端渲染邏輯,即node端通過引入vue-serverer-renderer插件來渲染服務器端打包的bundle文件到客戶端。
而在服務器端的配置文件webpack.server.js如下所示:
const webpack = require('webpack') const merge = require('webpack-merge') const base = require('./webpack.base.config') const nodeExternals = require('webpack-node-externals') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') module.exports = merge(base, { target: 'node', devtool: '#source-map', entry: './client/entry-server.js', output: { filename: 'server-bundle.js', libraryTarget: 'commonjs2' }, externals: nodeExternals({ // do not externalize CSS files in case we need to import it from a dep whitelist: /\.css$/ }), plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 'process.env.VUE_ENV': '"server"', 'process.env.VUE_HOST': JSON.stringify(process.env.VUE_HOST || 'http://localhost:8888') }), new VueSSRServerPlugin() ] })
即通過webpack-merge來merge到webpack.base.config的文件,接着具體配置中,target: 'node'表示在node端運行,devtools: '#source-map'可以保證報錯時定位到出錯文件的行數,output.filename表示輸出文件的名稱,而output.libraryTarget表示模塊入口形式,external表示外部不需要打包,最后就是使用到的plugins。
而之前所提到的webpack-dev-middleware和webpack-hot-middleware這兩個插件可以配置在setup-dev-server.js中的,在index.js中配置服務器時,如果是開發環境,再引入setup-dev-server.js,否則不需要引入。
所以在bi項目中的兩個服務器實際上都是node服務器,其中index.js創建的服務器是node服務器,提供ssr和其他的一些服務,而server下中src的index.js是一個node中間件服務器,起到的是代理服務的作用。
注意事項
1. webpack打包是需要在服務器端進行的。即webpack在之前純前端渲染時都是跑在瀏覽器端的,而如果要使用服務器端渲染,就需要讓webpack打包這個過程代碼跑在服務器端了。因為vue文件是用.vue形式組織的,所以必須在服務器端打包才能進行服務器端渲染,並且因為在客戶端請求是單頁的,所以服務器端打包也應該打包為單個文件。
2. 服務器端index.js流程是如何的? 即npm run dev之后,就會進入index.js,然后引入express作為node服務器,並引入vue-server-renderer來集成,進一步將vue的app來服務器渲染,但是如何在服務器端獲取這個打包之后的app呢? 即通過entry-server.js即可,這個入口文件就會打包node端運行的vue,打包之后,node端會生成了html標記,然后需要一層html外殼,即套用index.html模板template,這個模板中有一個<!--vue-ssr-outlet>注釋,表示
3. 既然有了服務器端渲染,為什么項目中還需要client-server進行客戶端的webpack打包呢?
(1)服務器需要在服務器端打包然后進行渲染,而客戶端打包bundle是需要將客戶端bundle給瀏覽器進行混合靜態標記。那么為什么需要混合靜態標記呢? 默認情況下,可以在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操作 DOM。然而,也可以將同一個組件渲染為服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最后將靜態標記"混合"為客戶端上完全交互的應用程序.如上,因為服務器端發送來的是html字符串,還不是應用程序,比如沒有css等,這樣,就需要在客戶端進行打包,然后混合為應用程序。
(2)另一個需要關注的問題是在客戶端,在掛載(mount)到客戶端應用程序之前,需要獲取到與服務器端應用程序完全相同的數據 - 否則,客戶端應用程序會因為使用與服務器端應用程序不同的狀態,然后導致混合失敗。
(3) 客戶端只需要拿到簡單的vue打包文件,這個文件是一個模板,而不需要獲取到具體的數據。而服務器端是在獲取到數據之后拼接html發送,所以需要在客戶端進行整合。即服務器端代碼打包是為了提供最開始的html頁面,而客戶端代碼打包是為了后期的數據和交互,所以,並不是只需要服務器端打包即可。
4. 配置webpack.server.config時為什么externals中包含css文件?因為此功能webpack無法在node端執行。
5. 服務器端獲取打包之后的代碼是和客戶端一樣都是js文件嗎? 不是的,一般來說,服務器端在獲取webpack打包的代碼應該是 built-server-bundle.js,但是這樣每次在編輯過應用程序代碼之后都需要再重新重啟,會影響開發效率,另外nodejs不支持source map。所以,我們可以使用 bundlerender,這種方式和render是類似的,但它支持sourcemap,熱重載等。在webpack.server.config文件中配置了插件new VueSSRServerPlugin(),這個插件的作用是作為整個服務器的輸出為json文件,而不再是js文件,默認文件名為 vue-ssr-server-bundle.json。
6. 在app.js中,createApp函數的作用是什么?這是為了每個用戶得到一份新的實例,防止狀態污染。
7. 整體過程到底是怎樣的?即首先寫好各種組件、路由、store等,接着app.js中開始進行匯聚,然后entry-client.js和entry-server.js分別進行對兩者的整合。接下來就可以build了,在build客戶端代碼的時候即通過webpack.client.js進入,入口文件為entry-client.js,最后會打包完整的代碼;在build服務器端代碼的時候通過webpack.server.js進入,入口文件為entry-server.js,會打包出vue-ssr-server-bundle.json文件;當然這些打包后的文件都會打包到dist文件夾下。build之后,就可以把代碼放在服務器上運行了,即通過node創建一個服務器進行服務器端渲染。
8. 查看服務器端渲染代碼? 使用查看源代碼即可。 比如使用vue和react做出的網站是SPA,那么通過view source獲得的代碼一定是一個html框架,即<html><head></head><body> ' 這里是空的' </body></html>,即在body標簽中是不存在html代碼的,這樣的結果就是非常不利於seo,且這表示它是沒有做服務器端渲染的。 而如果一個做了服務器端渲染的vue網站,view source得到的代碼中html是填滿的,且包括了所有數據,這就表示這個數據是通過服務器端渲染得到的。或者直接在network中查看接受到的html頁面即可 。如果做了ssr,那么得到的html是包含插入的css的,因為需要到客戶端就顯示,所以html和css都是需要的,而JavaScript對於顯示而言並不是必要的,所以服務器端渲染到的html中不需要引入額外的JavaScript(除了自身寫入的)。
9. vue ssr中,為什么組件已經用preFetch在服務器端獲取數據,客戶端還需要再去fetch? 服務端已經 preFetch 的客戶端當然不用 fetch。preFetch 的數據是存進了服務端 vuex store 里面,然后這些數據會直接內聯在直出的 HTML 里面。客戶端的 vuex store 啟動的時候就直接以這些數據為初始數據了。客戶端組件調用 actions 的時候,vuex store 起到一層緩存的作用,已經有的數據不會再 fetch。
!! 所以可以認為是服務器端在接收到url之后,就開始router.push(url),然后因為服務器端也有對應的打包,所以會請求數據並拼接得到完整的html頁面,並把服務器端vuex的state賦值給context.state並傳遞給前端,前端拿到這個state之后,就會用這個數據,而不會繼續請求新的數據。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="./test1.css"> <link rel="preload" href="./test2.css" as="style"> <link rel="preload" href="./test1.js" as="script"> </head> <body> <h1>title</h1> <p>some contents</p> <script src="./test2.js"></script> </body> </html>
如上,preload使用需要rel中聲明,並且需要使用as標記格式。加載順序為 test2.css -> test1.js -> test1.css -> test2.js,即對於preload的文件會最先加載。但是在瀏覽器執行時可以發現,preload的文件只是加載但是沒有使用,但是如果在下面又希望引入,這時引入會很快,不會重新下載,即后面希望用到的時候立馬有效,可以解決很多問題。 注意: preload是在當前頁面中使用的。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="./test1.css"> <link rel="prefetch" href="./test2.css"> <link rel="prefetch" href="./test1.js"> </head> <body> <h1>title</h1> <p>some contents</p> <script src="./test2.js"></script> </body> </html>
如上而prefetch的使用,它不需要添加as來標記類型,加載順序為 test1.css -> test2.js -> test2.css -> test1.js,一般prefetch會最后加載,所以,使用prefetch往往是用在下一頁可能會用到,這種情況也比較常見,這樣用戶在點入下一個可能的頁面時,由於數據已經加載到,那么速度就非常快了。
18. 在服務器端渲染得到頁面view source時,可以看到 data-server-rendered = "true",是什么意思?data-server-rendered
特殊屬性,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務端渲染的,並且應該以激活模式進行掛載。注意,這里並沒有添加 id="app"
,而是添加 data-server-rendered
屬性:你需要自行添加 ID 或其他能夠選取到應用程序根元素的選擇器,否則應用程序將無法正常激活。在開發模式下,Vue 將推斷客戶端生成的虛擬 DOM 樹(virtual DOM tree),是否與從服務器渲染的 DOM 結構(DOM structure)匹配。如果無法匹配,它將退出混合模式,丟棄現有的 DOM 並從頭開始渲染。在生產模式下,此檢測會被跳過,以避免性能損耗。
即我們可以區分是否是服務器端渲染,只要存在data-server-renderer="true"即可判斷。
可參考這篇文章: https://zhuanlan.zhihu.com/p/25936718
而代碼大部分是參考: https://github.com/vuejs/vue-hackernews-2.0