Server Side Rendering(服務端渲染)css
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)
複製代碼
vue ssr的核心就是:html
當編寫純客戶端 (client-only) 代碼時,咱們習慣於每次在新的上下文中對代碼進行取值,但Node.js 服務器是一個長期運行的進程。vue
當咱們的代碼進入該進程時,它將進行一次取值並留存在內存中。這意味着若是建立一個單例對象,它將在每一個傳入的請求之間共享。node
咱們須要爲每一個請求建立一個新的根 Vue 實例,==若是咱們在多個請求之間使用一個共享的實例,很容易致使交叉請求狀態污染==webpack
// vue實例工廠
const createApp = createApp (context) {
return new Vue({
data: {
url: context.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`
})
}
server.get('*', (req, res) => {
const context = { url: req.url }
// 每次請求都生成一個新的實例
const app = createApp(context)
renderer.renderToString(app, (err, html) => {
res.end(html)
})
})
複製代碼
(1)經過webpack 來打包咱們的 Vue 應用程序git
因此,對於客戶端應用程序和服務器應用程序,咱們都要使用 webpack 打包:github
(2)webpack源碼結構web
基本上和普通vue項目沒什麼區別,主要強調一下下面幾個文件vue-router
router.js 路由vuex
服務器代碼使用了一個 * 處理程序,它接受任意 URL。這容許咱們將訪問的 URL 傳遞到咱們的 Vue 應用程序中,而後對客戶端和服務器複用相同的路由配置!
因此官方建議使用vue-router
vue ssr路由採用history方式
// router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: () => import('@/components/Home') }
]
})
}
複製代碼
app.js
app.js 建立根實例的工廠函數
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
import './assets/common.css'
import '@node_modules/font-awesome/css/font-awesome.min.css'
export function createApp () {
// 建立router 和 store 實例
const router = createRouter()
const store = createStore()
sync(store, router)
const app = new Vue({
// 注入router 到跟 vue實例
router,
store,
render: h => h(App)
})
return { app, router, store }
}
複製代碼
entry-server.js 服務器 entry 使用 default export 導出函數,並在每次渲染中重複調用此函數
import { createApp } from './app'
export default context => {
// 有多是異步路由鉤子函數或組件,因此將返回一個Promise
// 以便服務器可以等待全部的內容在渲染前 就已經準備就緒
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
router.push(context.url)
// 等到 router 將可能的異步組件和鉤子函數解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length){
return reject({ code: 404 })
}
// 對全部匹配的路由組件調用 asyncData
Promise.all(matchedComponents.map(Component => {
if(Component.asyncData){
return Component.asyncData({
store,
router: router.currentRoute
})
}
})).then(() => {
// 在全部預取鉤子 resolve後, store已經填充渲染應用程序所需的狀態
// 將狀態附加到上下文
// 狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
複製代碼
router.onReady 是幹什麼用的
在全部的vue組件建立以前(包括App.vue)調用,這意味着它能夠解析全部的異步進入鉤子和路由初始化相關聯的異步組件。
這能夠有效確保服務端渲染時服務端和客戶端輸出的一致。
entry-client.js 客戶端 entry 只需建立應用程序,而且將其掛載到 DOM 中:
import Vue from 'vue'
import { createApp } from './app'
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
// 添加路由鉤子函數,用於處理 asyncData.
// 在初始路由 resolve 後執行,
// 以便咱們不會二次預取(double-fetch)已有的數據。
// 使用 `router.beforeResolve()`,以便確保全部異步組件都 resolve。
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))
})
if (!activated.length) {
return next()
}
// 這裏若是有加載指示器 (loading indicator),就觸發
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 中止加載指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
複製代碼
server.js
// server.js
const express = require('express')
const { createBundleRenderer } = require('vue-server-renderer')
const app = express()
function createRenderer (bundle, options) {
return createBundleRenderer(bundle, Object.assign(options, {
cache: new LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
basedir: resolve('./dist'),
runInNewContext: false
}))
}
let renderer
let readyPromise
const templatePath = resolve('./src/index.template.html')
if (isProd) {
// In production: create server renderer using template and built server bundle.
// The server bundle is generated by vue-ssr-webpack-plugin.
const template = fs.readFileSync(templatePath, 'utf-8')
const bundle = require('./dist/vue-ssr-server-bundle.json')
// The client manifests are optional, but it allows the renderer
// to automatically infer preload/prefetch links and directly add <script>
// tags for any async chunks used during render, avoiding waterfall requests.
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
renderer = createRenderer(bundle, {
template,
clientManifest
})
} else {
// In development: setup the dev server with watch and hot-reload,
// and create a new renderer on bundle / index template update.
readyPromise = require('./build/setup-dev-server')(
app,
templatePath,
(bundle, options) => {
renderer = createRenderer(bundle, options)
}
)
}
function render (req, res) {
const s = Date.now()
res.setHeader("Content-Type", "text/html")
res.setHeader("Server", serverInfo)
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 HN 2.0', // default title
meta: `<mata charset="utf-8">`,
url: req.url
}
renderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err)
}
res.send(html)
if (!isProd) {
console.log(`whole request: ${Date.now() - s}ms`)
}
})
}
app.get('*', isProd ? render : (req, res) => {
readyPromise.then(() => render(req, res))
})
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
複製代碼
這麼長,那server入口文件都作了什麼呢,總結一下:
(3)webpack配置
webpack配置是一個很複雜的過程,不建議本身從頭搭建
咱們能夠參照一個網上的例子 github.com/mtgr1020/vu…
首屏渲染依賴於一些異步數據,那麼在開始渲染過程以前,須要先預取和解析好這些數據
==另外一個須要關注的問題是在客戶端,在掛載 (mount) 到客戶端應用程序以前,須要獲取到與服務器端應用程序徹底相同的數據 - 不然,客戶端應用程序會由於使用與服務器端應用程序不一樣的狀態,而後致使混合失敗。==
爲了解決這個問題,獲取的數據須要位於視圖組件以外,即放置在專門的數據預取存儲容器中
因而須要引入vuex
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import { fetchItem } from '../api'
export function createStore () {
return new Vuex.Store({
state: {
items: {}
},
actions: {
fetchItem ({ commit }, id) {
return fetchItem(id).then(item => {
commit('setItem', { id, item })
})
}
},
mutations: {
setItem (state, { id, item }) {
Vue.set(state.items, id, item)
}
}
})
}
複製代碼
而後再看 app.js:
// app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
export function createApp () {
// 建立 router 和 store 實例
const router = createRouter()
const store = createStore()
// 同步路由狀態(route state)到 store
sync(store, router)
// 建立應用程序實例,將 router 和 store 注入
const app = new Vue({
router,
store,
render: h => h(App)
})
// 暴露 app, router 和 store。
return { app, router, store }
}
複製代碼
每一次訪問都要建立一個新的vue對象,同時應用新的router和store對象
帶有邏輯配置的組件
Vue SSR路由組件上暴露出一個自定義靜態函數 asyncData,
注意:因爲此函數會在組件實例化以前調用,因此它沒法訪問 this。須要將 store 和路由信息做爲參數傳遞進去
<!-- Item.vue -->
<template>
<div>{{ item.title }}</div>
</template>
<script>
export default {
asyncData ({ store, route }) {
// 觸發 action 後,會返回 Promise
return store.dispatch('fetchItem', route.params.id)
},
computed: {
// 從 store 的 state 對象中的獲取 item。
item () {
return this.$store.state.items[this.$route.params.id]
}
}
}
</script>
複製代碼
服務器端數據預取
在 entry-server.js 中,咱們能夠經過路由得到與 router.getMatchedComponents() 相匹配的組件,若是組件暴露出 asyncData,咱們就調用這個方法。而後咱們須要將解析完成的狀態,附加到渲染上下文(render context)中。
// entry-server.js
import { createApp } from './app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 對全部匹配的路由組件調用 `asyncData()`
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 在全部預取鉤子(preFetch hook) resolve 後,
// 咱們的 store 如今已經填充入渲染應用程序所需的狀態。
// 當咱們將狀態附加到上下文,
// 而且 `template` 選項用於 renderer 時,
// 狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
複製代碼
客戶端數據預取
1)在路由導航以前解析數據
使用此策略,應用程序會等待視圖所需數據所有解析以後,再傳入數據並處理當前視圖。好處在於,能夠直接在數據準備就緒時,傳入視圖渲染完整內容,可是若是數據預取須要很長時間,用戶在當前視圖會感覺到"明顯卡頓"。所以,若是使用此策略,建議提供一個數據加載指示器
2)匹配要渲染的視圖後,再獲取數據
此策略將客戶端數據預取邏輯,放在視圖組件的 beforeMount 函數中。當路由導航被觸發時,能夠當即切換視圖,所以應用程序具備更快的響應速度。然而,傳入視圖在渲染時不會有完整的可用數據。所以,對於使用此策略的每一個視圖組件,都須要具備條件加載狀態。
這兩種策略是根本上不一樣的用戶體驗決策,應該根據你建立的應用程序的實際使用場景進行挑選。可是不管你選擇哪一種策略,當路由組件重用(同一路由,可是 params 或 query 已更改,例如,從 user/1 到 user/2)時,也應該調用 asyncData 函數
Vue 在瀏覽器端接管由服務端發送的靜態 HTML,使其變爲由 Vue 管理的動態 DOM 的過程。
在 entry-client.js 中,咱們用下面這行掛載(mount)應用程序:
因爲服務器已經渲染好了 HTML,咱們顯然無需將其丟棄再從新建立全部的 DOM 元素。相反,咱們須要"激活"這些靜態的 HTML,而後使他們成爲動態的(可以響應後續的數據變化)。
<div id="app" data-server-rendered="true">
複製代碼
data-server-rendered 特殊屬性,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務端渲染的,而且應該以激活模式進行掛載
// 強制使用應用程序的激活模式
app.$mount('#app', true)
複製代碼
在開發模式下,Vue 將推斷客戶端生成的虛擬 DOM 樹 (virtual DOM tree),是否與從服務器渲染的 DOM 結構 (DOM structure) 匹配。若是沒法匹配,它將退出混合模式,丟棄現有的 DOM 並從頭開始渲染。在生產模式下,此檢測會被跳過,以免性能損耗
大多數自定義指令直接操做 DOM,所以會在服務器端渲染 (SSR) 過程當中致使錯誤
使用「SSR + 客戶端混合」時,須要瞭解的一件事是,瀏覽器可能會更改的一些特殊的 HTML 結構。例如,當你在 Vue 模板中寫入:
<table>
<tr><td>hi</td></tr>
</table>
複製代碼
瀏覽器會在
內部自動注入 ,然而,因爲 Vue 生成的虛擬 DOM (virtual DOM) 不包含 ,因此會致使沒法匹配。爲可以正確匹配,請確保在模板中寫入有效的 HTML。1)自行維護node服務
2)本身維護vue
3)基礎優化部分要本身處理
ok,以上就最近看的一些vue ssr基礎
由於最近一直在用nuxt開發項目,具體vue ssr都作了什麼不清楚。 主要目的仍是基礎掃盲。