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都作了什么不清楚。 主要目的仍是基础扫盲。