利用webpack4搭建vue伺服器端渲染SSR(一)
- 為什麼使用伺服器渲染? :point_right:官方解釋
- 應該對VueSSR指南簡單瞭解:point_right:官方文件
- 應該對webpack簡單瞭解:point_right:官方文件
- Node.js框架Koa簡單瞭解:point_right:官方文件
正文
構建伺服器端渲染(SSR)我們可以利用 vue-server-renderer
外掛更簡單的構建SSR。官方的一段程式碼:
// 第 1 步:建立一個 Vue 例項 const Vue = require('vue') const app = new Vue({ template: `<div>Hello World</div>` }) // 第 2 步:建立一個 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:將 Vue 例項渲染為 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // => <div data-server-rendered="true">Hello World</div> }) 複製程式碼
按照官網的步驟,執行 node server.js
可以看到控制檯列印 <div data-server-rendered="true">Hello World</div>
:

從這段程式碼我們應該可以明白 vue-server-renderer
的作用是拿到vue例項並渲染成html結構,但它不僅僅只做著一件事,後面會介紹其他配置引數和配合webpack進行構建。
拿到html結構渲染到頁面上是我們接下來要做的事情,這裡官方事例用的是express搭建伺服器,我這裡採用Koa,為什麼用Koa?我不會express 。Koa起一個服務非常簡單,我們還需要藉助Koa-router來做路由的處理。
npm i koa koa-router -S 複製程式碼
修改 server.js
:
const Vue = require('vue') const Koa = require('koa') const Router = require('koa-router') const renderer = require('vue-server-renderer').createRenderer() //第 1 步:建立koa、koa-router 例項 const app = new Koa() const router = new Router() // 第 2 步:路由中介軟體 router.get('*', async (ctx, next) => { // 建立Vue例項 const app = new Vue({ data: { url: ctx.url }, template: `<div>訪問的 URL 是: {{ url }}</div>` }) // 有錯誤返回500,無錯誤返回html結構 try { const html = await renderer.renderToString(app) ctx.status = 200 ctx.body = ` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> ` } catch (error) { console.log(error) ctx.status = 500 ctx.body = 'Internal Server Error' } }) app .use(router.routes()) .use(router.allowedMethods()) // 第 3 步:啟動服務,通過http://localhost:3000/訪問 app.listen(3000, () => { console.log(`server started at localhost:3000`) }) 複製程式碼
從上段程式碼我們就可以看出伺服器端渲染的基本原理了,其實說白了,無伺服器端渲染時,前端打包後的html只是包含head部分,body部分都是通過動態插入到id為 #app
的dom中。如圖:

而伺服器端渲染(SSR)就是伺服器來提前編譯Vue生成HTML返回給web瀏覽器,這樣網路爬蟲爬取的內容就是網站上所有可呈現的內容。
為了可以個性化頁面,我們可以把html結構抽成一個模板template,通過雙花括號 {{}}
進行傳值,新建 index.template.html
按照官網編寫如下程式碼:
<!DOCTYPE html> <html lang="en"> <head> <!-- 三花括號不進行html轉義 --> {{{ meta }}} <title>{{ title }}</title> </head> <body> <!--vue-ssr-outlet--> </body> </html> 複製程式碼
server.js
需要通過Node模組 fs
讀取模板,作為 vue-server-renderer
的template引數傳入
const renderer = require('vue-server-renderer').createRenderer({ // 讀取傳入template引數 template: require('fs').readFileSync('./index.template.html', 'utf-8') }) // ... router.get('*', async (ctx, next) => { // title、meta會插入模板中 const context = { title: ctx.url, meta: ` <meta charset="UTF-8"> <meta name="descript" content="基於webpack、koa搭建的SSR"> ` } try { // 傳入context渲染上下文物件 const html = await renderer.renderToString(app, context) ctx.status = 200 // 傳入了template, html結構會插入到<!--vue-ssr-outlet--> ctx.body = html } catch (error) { ctx.status = 500 ctx.body = 'Internal Server Error' } }) // ... 複製程式碼

可以看到我們的標題和meta都被插入啦!:clap::clap::clap:。到這裡,我們才實現了最基本的用法,接下來我們終於要使用webpack來構建我們專案。
Node.js伺服器是一個長期執行的程序、當我們的程式碼進入該程序時,它將進行一次取值並留存在記憶體中。這意味著如果建立一個單例物件,它將在每個傳入的請求之間共享,所以我們需要為 為每個請求建立一個新的根 Vue 例項
不僅vue例項,接下來要用到的vuex、vue-router也是如此。我們利用webpack需要分別對客戶端程式碼和伺服器端程式碼分別打包, 伺服器需要「伺服器 bundle」然後用於伺服器端渲染(SSR),而「客戶端 bundle」會發送給瀏覽器,用於混合靜態標記。貼一下官方構建圖:

我們可以大致的理解為伺服器端、客戶端通過倆個入口 Server entry
、 Clinet entry
獲取原始碼,再通過webpack打包變成倆個bundle vue-ssr-server-bundle.json
、 vue-ssr-client-manifest.json
,配合生成完成HTML,而 app.js
是倆個入口通用的程式碼部分,其作用是暴露出vue例項。所以我們可以按照官方建議整理檔案目錄,並按照 ofollow,noindex"> 官方事例程式碼 編寫,其中起服務的 server.js
我們用的是Koa,所以可以先不用改。

生成的倆個bundle其實是作為引數傳入到 createBundleRenderer()
函式中,然後在renderToString變成html結構,與 createRenderer
不同的是前者是通過bundle引數獲取vue元件編譯,後者是需要在 renderToString
時傳入vue例項:point_right:文件。我們先編寫webpack成功生成bundle後,再去編寫server.js,這樣有利於我們更好的理解和測試。
首先我們建立build資料夾,用於存放webpack相關配置,在vue-cli3之前,vue init 初始化後的專案都是有build資料夾的,可以清楚看到webpack配置。而vue-cli3後,使用webpack4,並將配置隱藏了起來,如果想了解webpack4構建vue單頁面應用可以去我的github上檢視:point_right: 地址 。我們可以模仿vue-cli,建立通用配置webpack.base.conf.js、客戶端配置webpack.client.conf.js、服務端配置webpack.server.conf.js。檔案目錄為
├── build │├── webpack.base.conf.js# 基本webpack配置 │├── webpack.client.conf.js # 客戶端webpack配置 │└── webpack.server.conf.js # 伺服器端webpack配置 ├── src ├── index.template.html └── server.js 複製程式碼
webpack.base.conf.js
配置主要定義通用的rules,例如vue-loader對.vue檔案編譯,對js檔案babel編譯,處理圖片、字型等。其基本配置如下:
const path = require('path') // vue-loader v15版本需要引入此外掛 const VueLoaderPlugin = require('vue-loader/lib/plugin') // 用於返回檔案相對於根目錄的絕對路徑 const resolve = dir => path.posix.join(__dirname, '..', dir) module.exports = { // 入口暫定客戶端入口,服務端配置需要更改它 entry: resolve('src/entry-client.js'), // 生成檔案路徑、名字、引入公共路徑 output: { path: resolve('dist'), filename: '[name].js', publicPath: '/' }, resolve: { // 對於.js、.vue引入不需要寫字尾 extensions: ['.js', '.vue'], // 引入components、assets可以簡寫,可根據需要自行更改 alias: { 'components': resolve('src/components'), 'assets': resolve('src/assets') } }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { // 配置哪些引入路徑按照模組方式查詢 transformAssetUrls: { video: ['src', 'poster'], source: 'src', img: 'src', image: 'xlink:href' } } }, { test: /\.js$/, // 利用babel-loader編譯js,使用更高的特性,排除npm下載的.vue元件 loader: 'babel-loader', exclude: file => ( /node_modules/.test(file) && !/\.vue\.js/.test(file) ) }, { test: /\.(png|jpe?g|gif|svg)$/, // 處理圖片 use: [ { loader: 'url-loader', options: { limit: 10000, name: 'static/img/[name].[hash:7].[ext]' } } ] }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, // 處理字型 loader: 'url-loader', options: { limit: 10000, name: 'static/fonts/[name].[hash:7].[ext]' } } ] }, plugins: [ new VueLoaderPlugin() ] } 複製程式碼
webpack.client.conf.js
主要是對客戶端程式碼進行打包,它是通過 webpack-merge
實現對基礎配置的合併,其中要實現對css樣式的處理,此處我用了stylus,同時要下載對應的stylus-loader來處理。在這裡我們先不考慮開發環境,後面會詳細介紹開發環境的webpack配置。
const path = require('path') const webpack = require('webpack') const merge = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') // css樣式提取單獨檔案 const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 服務端渲染用到的外掛、預設生成JSON檔案(vue-ssr-client-manifest.json) const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') module.exports = merge(baseWebpackConfig, { mode: 'production', output: { // chunkhash是根據內容生成的hash, 易於快取, // 開發環境不需要生成hash,目前先不考慮開發環境,後面詳細介紹 filename: 'static/js/[name].[chunkhash].js', chunkFilename: 'static/js/[id].[chunkhash].js' }, module: { rules: [ { test: /\.styl(us)?$/, // 利用mini-css-extract-plugin提取css, 開發環境也不是必須 use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader'] }, ] }, devtool: false, plugins: [ // webpack4.0版本以上採用MiniCssExtractPlugin 而不使用extract-text-webpack-plugin new MiniCssExtractPlugin({ filename: 'static/css/[name].[contenthash].css', chunkFilename: 'static/css/[name].[contenthash].css' }), //當vendor模組不再改變時, 根據模組的相對路徑生成一個四位數的hash作為模組id new webpack.HashedModuleIdsPlugin(), new VueSSRClientPlugin() ] }) 複製程式碼
編寫完,我們需要在package.json定義命令來執行webpack打包命令。如果沒有該檔案,需要通過 npm init
初始化生成
"scripts": { "build:client": "webpack --config build/webpack.client.conf.js", # 打包客戶端程式碼 "build:server": "webpack --config build/webpack.server.conf.js", # 打包服務端程式碼 "start": "node server.js" # 啟動服務 } 複製程式碼
我們現在可以通過 npm run build:client
執行打包命令,需要注意的是,執行命令之前要把依賴的npm包下載好,目前所需要到的依賴見下圖:

當打包命令執行完畢後,我們會發現多了一個dist資料夾,其中除了靜態檔案以外,生成了用於服務端渲染的JSON檔案:vue-ssr-client-manifest.json。

同理,我們需要編寫服務端webpack配置,同樣打包生成vue-ssr-server-bundle.json。配置程式碼如下:
const path = require('path') const webpack = require('webpack') const merge = require('webpack-merge') const nodeExternals = require('webpack-node-externals') const baseWebpackConfig = require('./webpack.base.conf') const VueServerPlugin = require('vue-server-renderer/server-plugin') module.exports = merge(baseWebpackConfig, { mode: 'production', target: 'node', devtool: 'source-map', entry: path.join(__dirname, '../src/entry-server.js'), output: { libraryTarget: 'commonjs2', filename: 'server-bundle.js', }, // 這裡有個坑... 服務端也需要編譯樣式,但不能使用mini-css-extract-plugin, // 因為它會使用document,但服務端並沒document,導致打包報錯。詳情見 // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/48#issuecomment-375288454 module: { rules: [ { test: /\.styl(us)?$/, use: ['css-loader/locals', 'stylus-loader'] } ] }, // 不要外接化 webpack 需要處理的依賴模組 externals: nodeExternals({ whitelist: /\.css$/ }), plugins: [ new webpack.DefinePlugin({ 'process.env.VUE_ENV': '"server"' }), // 預設檔名為 `vue-ssr-server-bundle.json` new VueServerPlugin() ] }) 複製程式碼
我們新建一個 build:server
命令來執行打包伺服器端程式碼。最後會在dist檔案下生成vue-ssr-server-bundle.json,同時我們可以新建 build
命令來一起執行倆端的配置。

好了,現在我們可以修改我們的server.js來實現整個伺服器端渲染流程。我們需要獲取倆個JSON檔案、html模板作為引數傳入 createBundleRenderer
,vue例項不再需要,context需要url,因為服務端端入口(entry-server.js) 需要獲取訪問的路徑來匹配對應的vue元件。部分改動程式碼如下:
/* 將createRenderer替換成createBundleRenderer,不同之處在上面提到過... */ const { createBundleRenderer } = require('vue-server-renderer') // ... // 獲取客戶端、伺服器端生成的json檔案、html模板檔案 const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') const template = require('fs').readFileSync('./index.template.html', 'utf-8') // 傳入 json檔案和template, 渲染上下文url需要傳入,服務端需要匹配路由 router.get('*', async (ctx, next) => { const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, // 推薦 template, // 頁面模板 clientManifest // 客戶端構建 manifest }) const context = { url: ctx.url, // ... } // ... 複製程式碼
改動後,我們執行 npm run start
,發現頁面已經成功渲染出來,但這時有個問題,載入的資源都失敗了,檔案存在於dist中,很顯然,一定是路徑不對導致的。這時我們可以通過koa-send來實現靜態資源的傳送。我們需要在server.js中加入這行程式碼:
const send = require('koa-send') // 引入/static/下的檔案都通過koa-send轉發到dist檔案目錄下 router.get('/static/*', async (ctx, next) => { await send(ctx, ctx.path, { root: __dirname + '/dist' }); }) 複製程式碼
再重新執行,開啟控制檯可以看到資源載入成功,並且載入的doc裡面包含頁面上所有內容。:clap:

結束語
但我們目前還沒有涉及到需要非同步載入的資料渲染,以及如何在開發中進行編碼,快取路由等問題。在下一篇文章我會帶大家先實現開發環境的webpack配置,以及修改server.js,這樣,就不需要我們每次都要打包來測試我們程式碼,為接下來學習資料預取,快取路由等做好準備。一起加油吧。感謝觀看~