所謂的Vue服務端渲染就是,將vue實例在服務端渲染成HTML字符串,將它們直接發送給瀏覽器,最後將靜態標記「混合」爲客戶端上徹底交互的應用程序。css
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = {
plugins: [
...
new PrerenderSPAPlugin({
// Required - The path to the webpack-outputted app to prerender.
staticDir: path.join(__dirname, 'dist'),
// Required - Routes to render.
routes: [ '/', '/about', '/some/deep/nested/route' ],
})
]
}
複製代碼
服務端:Nodejs 前端框架 Vue2.0+ 前端構建工具:webpack 代碼檢查:eslint 源碼:es6 前端路由:vue-router 狀態管理:vuex 服務端通訊:axios 日誌管理:log4js 項目自動化部署工具:jenkinshtml
// 第 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>
})
複製代碼
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
複製代碼
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'
import { sync } from 'vuex-router-sync'
import Element from 'element-ui'
Vue.use(Element)
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)
/**
* 建立vue實例
* 在這裏注入 router store 到全部的子組件
* 這樣就能夠在任何地方使用 `this.$router` and `this.$store`
* @type {Vue$2}
*/
const app = new Vue({
router,
store,
render: h => h(App)
})
/**
* 導出 router and store.
* 在這裏不須要掛載到app上。這裏和瀏覽器渲染不同
*/
export { app, router, store }
複製代碼
import 'es6-promise/auto'
import { app, store, router } from './app'
// prime the store with server-initialized state.
// the state is determined during SSR and inlined in the page markup.
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
/**
* 異步組件
*/
router.onReady(() => {
// 開始掛載到dom上
app.$mount('#app')
})
// service worker
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
}
複製代碼
import { app, router, store } from './app'
const isDev = process.env.NODE_ENV !== 'production'
// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => {
const s = isDev && Date.now()
return new Promise((resolve, reject) => {
// set router's location router.push(context.url) // wait until router has resolved possible async hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents() // no matched routes if (!matchedComponents.length) { reject({ code: 404 }) } // Call preFetch hooks on components matched by the route. // A preFetch hook dispatches a store action and returns a Promise, // which is resolved when the action is complete and store state has been // updated. Promise.all(matchedComponents.map(component => { return component.preFetch && component.preFetch(store) })).then(() => { isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) // After all preFetch hooks are resolved, our store is now // filled with the state needed to render the app. // Expose the state on the render context, and let the request handler // inline the state in the HTML response. This allows the client-side // store to pick-up the server-side state without having to duplicate // the initial data fetching on the client. context.state = store.state resolve(app) }).catch(reject) }) }) } 複製代碼
咱們的通用代碼是一套代碼能夠分別在瀏覽器環境和node.js環境跑起來的,因此書寫代碼有些事情須要注意: 一、服務端渲染過程當中只有beforeCreate和created生命週期函數會被調用。其餘生命週期只能在瀏覽器環境惰性調用,node.js 會忽略掉這部分生命週期函數。 二、通用代碼不可接受特定平臺的 API(好比document、window),使用跨平臺的axios(對瀏覽器和node.js暴露相同的API)作瀏覽器和node.js環境的請求發送。 三、大多數自定義指令直接操做 DOM,所以會在服務器端渲染(SSR)過程當中致使錯誤前端
一、引入vue-router來作頁面的單頁應用 二、代碼分割:應用程序的代碼分割或惰性加載,有助於減小瀏覽器在初始渲染中下載的資源體積,能夠極大地改善大致積 bundle 的可交互時間 (TTI - time-to-interactive)。這裏的關鍵在於,對初始首屏而言,」只加載所需"。vue
// 這裏進行修改……
import Foo from './Foo.vue'
// 改成這樣:
const Foo = () => import('./Foo.vue')
複製代碼
在服務器端渲染(SSR)期間,咱們本質上是在渲染咱們應用程序的"快照",因此若是應用程序依賴於一些異步數據,那麼在開始渲染過程以前,須要先預取和解析好這些數據。node
另外一個須要關注的問題是在客戶端,在掛載(mount)到客戶端應用程序以前,須要獲取到與服務器端應用程序徹底相同的數據 - 不然,客戶端應用程序會由於使用與服務器端應用程序不一樣的狀態,而後致使混合失敗。webpack
爲了解決這個問題,獲取的數據須要位於視圖組件以外,即放置在專門的數據預取存儲容器(data store)或"狀態容器(state container))"中。首先,在服務器端,咱們能夠在渲染以前預取數據,並將數據填充到 store 中。此外,咱們將在 HTML 中序列化(serialize)和內聯預置(inline)狀態。這樣,在掛載(mount)到客戶端應用程序以前,能夠直接從 store 獲取到內聯預置(inline)狀態。ios
爲此,咱們將使用官方狀態管理庫 Vuex。git
所謂客戶端激活,指的是 Vue 在瀏覽器端接管由服務端發送的靜態 HTML,使其變爲由 Vue 管理的動態 DOM 的過程。 若是你檢查服務器渲染的輸出結果,你會注意到應用程序的根元素有一個特殊的屬性:es6
<div id="app" data-server-rendered="true">
複製代碼
data-server-rendered 特殊屬性,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務端渲染的,而且應該以激活模式進行掛載。github
在開發模式下,Vue 將推斷客戶端生成的虛擬 DOM 樹 (virtual DOM tree),是否與從服務器渲染的 DOM 結構 (DOM structure)匹配。若是沒法匹配,它將退出混合模式,丟棄現有的 DOM 並從頭開始渲染。在生產模式下,此檢測會被跳過,以免性能損耗。
客戶端配置(client config)和基本配置(base config)大致上相同。顯然你須要把 entry 指向你的客戶端入口文件。除此以外,若是你使用 CommonsChunkPlugin,請確保僅在客戶端配置(client config)中使用,由於服務器包須要單獨的入口 chunk
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
entry: '/path/to/entry-client.js',
plugins: [
// 重要信息:這將 webpack 運行時分離到一個引導 chunk 中,
// 以即可以在以後正確注入異步 chunk。
// 這也爲你的 應用程序/vendor 代碼提供了更好的緩存。
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
// 此插件在輸出目錄中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
複製代碼
服務器配置,是用於生成傳遞給 createBundleRenderer 的 server bundle。它應該是這樣的:
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 將 entry 指向應用程序的 server entry 文件
entry: '/path/to/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$/
}),
// 這是將服務器的整個輸出
// 構建爲單個 JSON 文件的插件。
// 默認文件名爲 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})
複製代碼
在大多數狀況下,服務器渲染的應用程序依賴於外部數據,所以本質上頁面內容是動態的,不能持續長時間緩存。然而,若是內容不是用戶特定(user-specific)(即對於相同的 URL,老是爲全部用戶渲染相同的內容),咱們能夠利用名爲 micro-caching 的緩存策略,來大幅度提升應用程序處理高流量的能力。
const microCache = LRU({
max: 100,
maxAge: 1000 // 重要提示:條目在 1 秒後過時。
})
const isCacheable = req => {
// 實現邏輯爲,檢查請求是不是用戶特定(user-specific)。
// 只有非用戶特定(non-user-specific)頁面纔會緩存
}
server.get('*', (req, res) => {
const cacheable = isCacheable(req)
if (cacheable) {
const hit = microCache.get(req.url)
if (hit) {
return res.end(hit)
}
}
renderer.renderToString((err, html) => {
res.end(html)
if (cacheable) {
microCache.set(req.url, html)
}
})
})
複製代碼
對於 vue-server-renderer 的基本 renderer 和 bundle renderer 都提供開箱即用的流式渲染功能。全部你須要作的就是,用 renderToStream 替代 renderToString:
const stream = renderer.renderToStream(context)
複製代碼
返回的值是 Node.js stream:
let html = ''
stream.on('data', data => {
html += data.toString()
})
stream.on('end', () => {
console.log(html) // 渲染完成
})
stream.on('error', err => {
// handle error...
})
複製代碼
從頭搭建一個服務端渲染的應用是至關複雜的。Nuxt 是一個基於 Vue 生態的更高層的框架,爲開發服務端渲染的 Vue 應用提供了極其便利的開發體驗