筆者最近在和小夥伴對vue項目進行ssr的升級,本文筆者將根據一個簡單拿vue cli構建的客戶端渲染的demo一步一步的教你們打造本身的ssr,拙見勿噴哈。javascript
在學習一項新技術的時候咱們首先要了解一下他是什麼。這裏引用官網的一句話:css
Vue.js 是構建客戶端應用程序的框架。默認狀況下,能夠在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做DOM。然而,也能夠將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序。html
知道是什麼後咱們要知道這項技術對咱們現有的項目有什麼好處,簡單總結一下:vue
這裏咱們用vue-cli去簡單的作一個vue客戶端渲染的demo,具體過程就不作贅述了。java
demo地址: https://github.com/LNoe-lzy/vue-ssr-demo/tree/masternode
這裏咱們根據以前寫好的客戶端渲染的demo來一步一步的改形成服務端渲染。先甩下demo連接:webpack
demo地址: https://github.com/LNoe-lzy/vue-ssr-demo/tree/vue-ssr-servergit
先附一張鎮文之圖,官網的構建流程: github
爲了不單例的影響,咱們須要在每一個請求都建立一個新的vue的實例,從而避免請求狀態的污染,咱們來封裝一個createApp的工廠函數:web
import Vue from 'vue'
import App from './App'
export function createApp () {
const app = new Vue({
render: h => h(App)
})
return { app }
}
複製代碼
跑在服務端的Vue中全部的生命週期鉤子函數中,只有 beforeCreate 和 created 會在服務器端渲染過程當中被調用,而其餘的鉤子在客戶端纔會被調用,畢竟咱們的服務端是沒法執行dom操做的,因此咱們要在路由匹配的組件上定義一個靜態函數,這個函數要作的也很簡單,就是去dispatch咱們的action從而異步獲取數據:
import { mapActions } from 'vuex'
export default {
asyncData ({ store }) {
return store.dispatch('getNav')
},
methods: {
...mapActions([
'getList'
])
}
// ...
}
複製代碼
一樣爲了不單例的影響,咱們也須要用工廠函數封裝咱們的router和store
// router
export function createRouter () {
return new Router({
mode: 'history',
routes: []
})
}
// store
export function createStore () {
return new Vuex.Store({
state: {},
actions,
mutations
})
}
複製代碼
根據構建流程圖咱們還須要webpack去構建兩個bundle,服務端根據Server Bundle去作ssr,瀏覽器根據Client Bundle去混合靜態標記。
爲此咱們在src目錄下新建兩個文件,entry-server.js 和 entry-client.js。前者在每次渲染中須要重複調用,執行服務端的路有匹配和數據預取邏輯。後者負責掛載DOM節點,以及先後端vuex數據狀態的同步。
// entry-server.js
import { createApp } from './main'
export default context => {
// 可能爲異步組件,返回一個promise
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject(new Error(`error: ${fullPath}`))
}
router.push(url)
// 須要等到的異步組件和鉤子函數解析完
router.onReady(() => {
// 獲取匹配到的組件
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
store,
route: router.currentRoute
}))).then(() => {
// 將預取的數據從store中取出放到context中
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
複製代碼
這裏咱們須要注意兩點,一個是咱們的數據預取是調用組件的asyncData方法,因此須要Promise.all來保證拿到所有的預渲染數據;另外一點是context.state = store.state,這時候服務端拿到的預渲染數據會封在**window.INITIAL_STATE**中經過node服務器send到客戶端。
import Vue from 'vue'
import { createApp } from './main'
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
// 也是處理異步組件
router.onReady(() => {
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
let diffed = false
// 篩選發生更新的組件
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
if (!asyncDataHooks.length) {
return next()
}
Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
.then(() => {
next()
})
.catch(next)
})
console.log('router ready')
app.$mount('#app')
})
複製代碼
看到window.INITIAL_STATE咱們就能夠知道了客戶端拿到了預取的數據,而後去存到客戶端的vuex中,這也就是你們常常談論的經過vuex實現先後端的狀態共享。
至於vuex是否是必須的,固然不是(尤大issuse有說),題外話,筆者也實現了沒有vuex的版本哦。
webpack的配置上面其實和純客戶端應用相似,爲了區分客戶端和服務端兩個環境咱們將配置分爲base、client和server三部分,base就是咱們的通用基礎配置,而client和server分別用來打包咱們的客戶端和服務端代碼。
首先是webpack.server.conf.js,用於生成server bundle來傳遞給createBundleRenderer函數在node服務器上調用,入口是咱們的entry-server:
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: './src/entry-server.js',
// 以 Node 適用方式導入
target: 'node',
// 對 bundle renderer 提供 source map 支持
devtool: '#source-map',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
externals: nodeExternals({
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()
]
})
複製代碼
其次是webpack.client.conf.js,這裏咱們能夠根據官方的配置生成clientManifest,自動推斷和注入資源預加載,以及 css 連接 / script 標籤到所渲染的 HTML。入口是咱們的client-server:
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.conf')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const config = merge(base, {
entry: {
app: './src/entry-client.js'
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"client"'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
return (
/node_modules/.test(module.context) &&
!/\.css$/.test(module.request)
)
}
}),
// 這將 webpack 運行時分離到一個引導 chunk 中,
// 以即可以在以後正確注入異步 chunk。
// 這也爲你的 應用程序/vendor 代碼提供了更好的緩存。
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
}),
new VueSSRClientPlugin()
]
})
複製代碼
服務端框架咱們採用Express(固然Koa2也是能夠的):
const express = require('express')
const fs = require('fs')
const path = require('path')
const {
createBundleRenderer
} = require('vue-server-renderer')
const app = express()
const resolve = file => path.resolve(__dirname, file)
// 生成服務端渲染函數
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
runInNewContext: false,
template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
clientManifest: require('./dist/vue-ssr-client-manifest.json'),
basedir: resolve('./dist')
})
// 引入靜態資源
app.use(express.static(path.join(__dirname, 'dist')))
// 分發路由
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html')
const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if (err.code === 404) {
res.status(404).send('404 | Page Not Found')
} else {
// Render Error Page or Redirect
res.status(500).send('500 | Internal Server Error')
console.error(`error during render : ${req.url}`)
console.error(err.stack)
}
}
const context = {
title: 'Vue SSR demo', // default title
url: req.url
}
renderer.renderToString(context, (err, html) => {
console.log('render')
if (err) {
return handleError(err)
}
res.send(html)
})
})
app.on('error', err => console.log(err))
app.listen(3000, () => {
console.log(`vue ssr started at localhost:3000`)
})
複製代碼
經過觀察localhost咱們能夠很清楚的發現,經過服務端send過來的html字符串僅包括咱們根據數據預取渲染出來的dom結構以及服務端混入的window.INITIAL_STATE
經過Performance咱們也能夠看出在採用了ssr的應用中,咱們的首屏渲染並不依賴於客服端的js文件了,這就大大加快了首屏的渲染速度,畢竟傳統的SPA應用時須要拿到客戶端js文件後才能夠進行虛擬dom的構建以及數據的獲取工做才渲染頁面的。
不使用vuex其實很頭疼,但又有了點靈感,平時咱們在開發項目的時候是如何處理組件間通訊的,一個是vuex,另外一個是EventBus,EventBus就是個Vue的實例啊,數據存這裏不也行麼?
在此筆者的思路是:建立一個Vue的實例充當倉庫,那麼咱們能夠用這個實例的data來存儲咱們的預取數據,而用methods中的方法去作數據的異步獲取,這樣咱們只須要在須要預取數據的組件中去調用這個方法就能夠了。demo很簡單,戳這裏
還有一個思路是在筆者學習的時候看別人博客學到的:只用了vuex的store和一些支持服務端渲染的api,沒有走action、mutation那套,而是將數據手動寫入state,爲了表示對別人博客的尊重,細節就請轉到做者的博客吧,戳這裏
本文經過一個簡單的客戶端渲染demo來一步一步的交你們如何搭建屬於本身的ssr程序,文筆拙略還請你們諒解了。
不過學習雖好,可是細節到使用上,你們仍是斟酌是否適合在本身的項目中。
多謝支持!