上篇文章(《服務器端渲染與Nuxt.js》)介紹了服務器端渲染和一些Nuxt.js的概念,如今咱們就Vue SSR方面,從基礎開始,分低、中、高三個層面,來手寫實現下傳說中的服務端渲染。css
在正式搭建項目以前,咱們仍是要回顧下vue服務器端渲染的一些特性。
服務器端渲染的 Vue.js 應用程序,是使vue應用既能夠在客戶端(瀏覽器)執行,也能夠在服務器端執行,咱們稱之爲「同構」或「通用」。html
之因此可以實現同構,是由於在客戶端和服務端都建立了vue應用程序,並都用webpack進行打包,生成了server bundle和client bundle。server bundle用於服務器渲染,client bundle是一個客戶端的靜態標記,服務器渲染好html頁面片斷後,會發送給客戶端,而後混合客戶端靜態標記,這樣應用就具備vue應用的特性。Vue.js is a framework for building client-side applications. By default, Vue components produce and manipulate DOM in the browser as output. However, it is also possible to render the same components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client.vue
beforeCreate
和created
兩個鉤子函數,其它的只會在客戶端執行。那麼之前spa應用中,在created
中建立一個setInterval
,而後在destroyed
中將其銷燬的相似操做就不能出現了,服務器渲染期間不會調用銷燬鉤子函數,因此這個定時器會永遠保留下來,服務器很容易就崩了。window
和document
,在node.js(好比created
鉤子函數)中執行就會報錯。而且,咱們使用的第三方API中,須要確保能在node和瀏覽器都能正常運行,好比axios,它向服務器和客戶端都暴露相同的 API(瀏覽器的源生XHR就不行)。咱們先不考慮同構、不考慮各類配置,先實現一個基礎的服務器端渲染demo。node
npm install vue vue-server-renderer express --save
複製代碼
vue-server-renderer
是vue
服務器端渲染的核心模塊,它須要匹配你的vue版本。安裝express
是由於咱們等會會使用它來起個服務看到咱們的頁面效果。webpack
// 第 1 步:建立一個 Vue 實例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello Vue SSR</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 Vue SSR</div>
})
複製代碼
上面只是生產了一個html代碼片斷,通常來講,須要將html片斷插入一個模板文件裏。OK,那咱們就來寫一個模板文件index.html
。ios
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
複製代碼
在渲染的時候,html片斷會被插入到<!--vue-ssr-outlet-->
這個註釋標記這裏。git
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello Vue SSR</div>`
})
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html) // html 將是注入應用程序內容的完整頁面
})
複製代碼
咱們用fs
模塊將文件讀取進來丟入render的template
中,再重複上述步驟將html片斷插入到咱們的標記位。github
如今,咱們將使用express來啓動一個node服務,驗證一下頁面效果。web
const Vue = require('vue')
// 第一步: 建立一個 express 應用
const server = require('express')()
// 第二步: 建立一個 Vue 實例
const app = new Vue({
data: {
msg: 'Hello Vue SSR'
},
template: `<div>{{msg}}</div>`
})
// 第三步: 建立一個 renderer
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.html', 'utf-8')
})
// 第四步: 設置路由,"*" 表示任意路由均可以訪問它
server.get('*', (req, res) => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html)
})
})
// 第五步: 啓動服務並監遵從8080端口進入的全部鏈接請求
server.listen(8080)
複製代碼
這樣,咱們的一個簡單的頁面渲染就完成了,看下頁面效果和Response數據。vue-router
知道了怎麼在服務器端渲染出一個頁面,下一步就是實現同構啦。爲了跳過各類項目配置,咱們就從熟悉的vue-cli模板下手。
官方提供了vue-cli的項目快速構建工具,能夠用它也進行SPA項目的快速搭建,咱們如今就把這個模板,改形成一個可以集成SSR的模板。
第二部份內容參考讓vue-cli初始化後的項目集成支持SSR,侵刪。
安裝vue-cli (至少v2.x版本)後,使用基礎模板搭建個項目
vue init webpack spa_ssr
cd spa_ssr
複製代碼
跑一下確保項目可以正常運行,而後記得安裝vue-server-renderer
模塊
npm install vue-server-renderer --save-dev
複製代碼
安裝完成,咱們就開始進入下一步。
src
下的文件咱們須要在src
目錄下建立兩個js。
src
├── router
│ └── index.js
├── components
│ └── HelloSsr.vue
├── App.vue
├── main.js
├── entry-client.js # 僅運行於瀏覽器
└── entry-server.js # 僅運行於服務器
複製代碼
這兩個entry
以後會進行配置,先來改造main.js
。
在改造main.js
以前,須要說明一下,因單線程的機制,在服務器端渲染時,過程當中有相似於單例的操做,那麼全部的請求都會共享這個單例的操做,因此應該使用工廠函數來確保每一個請求之間的獨立性。好比在main.js
中,咱們原先直接建立一個Vue實例,並直接掛載到DOM。如今的main.js
做爲通用entry文件,它應該改形成一個能夠重複執行的工廠函數,爲每一個請求建立新的應用程序實例。掛載的工做,是由以後的客戶端entry來完成。
import Vue from 'vue'
import App from './App'
import { CreateRouter } from './router'
export function createApp () {
const router = new CreateRouter()
const app = new Vue({
router,
render: h => h(App)
})
return { app, router }
}
複製代碼
在/router/index.js
中,咱們一樣須要使用工廠函數來建立路由實例。而後將路由配置改成history
模式(由於哈希不支持)
import Vue from 'vue'
import Router from 'vue-router'
import HelloSsr from '@/components/HelloSsr'
Vue.use(Router)
export function CreateRouter () {
return new Router({
mode: 'history',
routes: [{
path: '/ssr',
name: 'HelloSsr',
component: HelloSsr
}]
})
}
複製代碼
接下來咱們來寫客戶端的entry和服務器端的entry。客戶端的entry要作的很簡單,就是將vue實例掛載到DOM上,只不過,考慮到可能存在異步組件,須要等到路由將異步組件加載完畢,才進行此操做。
// entry-client.js
import { createApp } from './main'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
複製代碼
服務器entry要作的有兩步:1.解析服務器端路由;2.返回一個vue實例用於渲染。
// entry-server.js
import { createApp } from './main'
export default context => {
// 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise,
// 以便服務器可以等待全部的內容在渲染前,
// 就已經準備就緒。
return new Promise((resolve, reject) => {
const { app, router } = createApp()
// 設置服務器端 router 的位置
router.push(context.url)
// 等到 router 將可能的異步組件和鉤子函數解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,執行 reject 函數,並返回 404
if (!matchedComponents.length) {
// eslint-disable-next-line
return reject({ code: 404 })
}
// Promise 應該 resolve 應用程序實例,以便它能夠渲染
resolve(app)
}, reject)
})
}
複製代碼
webpack
配置vue相關代碼已處理完畢,接下來就須要對webpack
打包配置進行修改了。 官方推薦了下面配置:
build
├── webpack.base.conf.js # 基礎通用配置
├── webpack.client.conf.js # 客戶端打包配置
└── webpack.server.conf.js # 服務器端打包配置
複製代碼
咱們的項目中的配置文件是base
,dev
,prod
,如今咱們仍然保留這三個配置文件,只須要增長webpack.server.conf.js
便可。
webpack.base.conf.js
修改咱們首先修改webpack.base.conf.js
的entry
入口配置爲:./src/entry-client.js
,來生成客戶端的構建清單client manifest
。服務器端的配置因爲引用base配置,entry
會經過merge
覆蓋,來指向server-entry.js
。
// webpack.base.conf.js
module.exports = {
entry: {
// app: './src/main.js'
app: './src/entry-client.js' // <-修改入口文件改成
},
// ...
}
複製代碼
webpack.prod.conf.js
修改在客戶端的配置prod
中,咱們須要引入一個服務器端渲染的插件client-plugin
,用來生成vue-ssr-client-manifest.json
(用做靜態資源注入),同時,咱們須要把HtmlWebpackPlugin
給去掉,在SPA應用中,咱們用它來生成index.html
文件,可是這裏咱們有vue-ssr-client-manifest.json
以後,服務器端會幫咱們作好這個工做。
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// ...
plugins: [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"' // 增長process.env.VUE_ENV
}),
// ...
// 如下內容註釋(或去除)
// new HtmlWebpackPlugin({
// filename: config.build.index,
// template: 'index.html',
// inject: true,
// minify: {
// removeComments: true,
// collapseWhitespace: true,
// removeAttributeQuotes: true
// // more options:
// // https://github.com/kangax/html-minifier#options-quick-reference
// },
// // necessary to consistently work with multiple chunks via CommonsChunkPlugin
// chunksSortMode: 'dependency'
// }),
// ...
// 此插件在輸出目錄中生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
// ...
複製代碼
webpack.server.conf.js
配置server
配置基本參考官方的配置,這裏仍是說明下:
webpack-node-externals
來加快構建速度和減少打包體積,因此咱們要先安裝一下它:npm install webpack-node-externals --save-dev
。prod
配置同樣,這裏須要引入並使用server-plugin
插件來生成vue-ssr-server-bundle.json
。這東西是用來等會作服務器端渲染的。const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
// 去除打包css的配置
baseConfig.module.rules[1].options = ''
module.exports = merge(baseConfig, {
// 將 entry 指向應用程序的 server entry 文件
entry: './src/entry-server.js',
// 這容許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動態導入(dynamic import),
// 而且還會在編譯 Vue 組件時,
// 告知 `vue-loader` 輸送面向服務器代碼(server-oriented code)。
target: 'node',
// 對 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 此處告知 server bundle 使用 Node 風格導出模塊(Node-style exports)
output: {
libraryTarget: 'commonjs2'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應用程序依賴模塊。可使服務器構建速度更快,
// 並生成較小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 須要處理的依賴模塊。
// 你能夠在這裏添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
whitelist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
// 這是將服務器的整個輸出
// 構建爲單個 JSON 文件的插件。
// 默認文件名爲 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
複製代碼
package.json
打包命令修改"scripts": {
//...
"build:client": "node build/build.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
"build": "rimraf dist && npm run build:client && npm run build:server"
}
複製代碼
這裏須要先安裝cross-env
。(cross-env
用來防止使用NODE_ENV =production 來設置環境變量時,Windows命令提示會報錯)
npm install --save-dev cross-env
複製代碼
index.html
如第一層說的,咱們須要在這個index.html
外層模板文件中,插入一個<!--vue-ssr-outlet-->
註釋標記,用來標識服務器渲染的html代碼片斷插入的地方,同時刪掉原先的<div id="app">
。
服務器端會在這個標記的位置自動生成一個<div id="app" data-server-rendered="true">
,客戶端會經過app.$mount('#app')
掛載到服務端生成的元素上,並變爲響應式的。
npm run build
複製代碼
在dist目錄下會生成兩個json文件:vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
,用於服務端端渲染和靜態資源注入。
這裏仍是採用express
來做爲服務器端,先進行安裝:
npm install express --save
複製代碼
以後在根目錄下建立server.js
,代碼主要分爲3步:
createBundleRenderer
來建立renderer
,咱們引入以前生成好的json文件,並讀取index.html
做爲外層模板;const express = require('express')
const app = express()
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')
const resolve = file => path.resolve(__dirname, file)
// 生成服務端渲染函數
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
// 模板html文件
template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
// client manifest
clientManifest: require('./dist/vue-ssr-client-manifest.json')
})
function renderToString (context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html)
})
})
}
app.use(express.static('./dist'))
app.use(async(req, res, next) => {
try {
const context = {
title: '服務端渲染測試', // {{title}}
url: req.url
}
// 設置請求頭
res.set('Content-Type', 'text/html')
const render = await renderToString(context)
// 將服務器端渲染好的html返回給客戶端
res.end(render)
} catch (e) {
console.log(e)
// 若是沒找到,放過請求,繼續運行後面的中間件
next()
}
})
app.listen(3000)
複製代碼
完過後啓動服務命令:
node server.js
複製代碼
訪問localhost:3000/ssr,就能獲取咱們以前定義好的頁面。
Nuxt.js
源碼初探Nuxt.js
是什麼Nuxt.js
是Vue官方推薦的一個項目,它是一個基於 Vue.js 的通用應用框架。預設了服務器端渲染所需的各類配置,如異步數據,中間件,路由,只要遵循其中的規則就能輕鬆實現SSR。開箱即用,體驗友好。經過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。
Nuxt.js
的一小小小部分源碼解讀Nuxt.js
源碼涉及的內容比較多,咱們不一一細說(好吧,是我功力不夠,吃不透 = =||)。咱們就來看看,Nuxt.js
做爲中間件的時候,整個流程都幹了些什麼。
Nuxt.js
官方提供的examples裏有一個custom-server
,Nuxt
會做爲中間件傳入express中,咱們來看下代碼:
import express from 'express'
import { Nuxt, Builder } from 'nuxt'
const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000
// Import and set Nuxt.js options
let config = require('./nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
const nuxt = new Nuxt(config)
// Start build process in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
builder.build()
}
// Give nuxt middleware to express
app.use(nuxt.render)
// Start express server
app.listen(port, host)
複製代碼
這段代碼有部分地方和咱們以前寫的類似,都用express起了一個服務,這裏涉及Nuxt.js
的代碼有2處:
Nuxt是根據pages文件夾下的目錄結構來生成對應的路由的,這是Nuxt的特色之一,這部份內容咱們這裏不展開細說,之後再詳細討論。咱們主要來講說new Nuxt()
裏的事。
上面咱們看的,咱們是將Nuxt
的實例化對象的render屬性值傳做爲中間件傳給了express,咱們在源碼中全局搜索找到Nuxt
構造函數:
Renderer
構造函數。代碼太長我就不全貼了。這麼多內容感受仍是無從下手,不慌,既然以前說了,
vue-server-renderer
是SSR服務器端渲染的核心模塊,那咱們嘗試在這裏搜索
vue-server-renderer
,果真,搜到以下內容:
在createRenderer函數中,咱們經過註釋能夠看到,爲服務器端渲染建立了一個
bundle renderer
,這不就是咱們以前本身實現的服務器渲染函數麼,它的第一個參數
this.resources.serverBundle
,咱們在文件中能夠搜到:
咱們的nuxt項目會在運行的時候自動構建,生成一個.nuxt的文件夾,裏面就包含了:
這一串代碼聯合起來,咱們能夠看到,
Nuxt.js
一樣是使用預編譯的應用程序包
createBundleRenderer
來建立了渲染器,其中所需的
server-bundle.json
和
client-mainfest.json
會由nuxt在運行的時候自動構建生成。
renderRoute
方法中找到了它:
這段代碼向咱們展現了一段比較完整的渲染流程,調用渲染函數生成HTML片斷,拼接HTML片斷,拼接HEAD片斷,丟進預置的模板中渲染出完整的html。由此能夠看出,
Nuxt.js
的核心服務器端渲染原理和咱們以前寫的demo基本相同。
服務器端渲染的優劣都很是明顯,若是僅僅是爲了優化網頁的SEO,咱們還能夠嘗試Vue官方給咱們推薦的預渲染(Prerendering),這裏就很少贅述。 以上Demo僅僅是做爲服務器端渲染的一種實現demo,若是須要正式用到項目中,還須要更加複雜的配置。若是隻是使用服務器端渲染來開發簡單的項目,咱們能夠直接用Nuxt.js
便可。 以上有些知識點在概念上有誤差,歡迎指正。