为什么需要nuxt
- seo– 由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
- 截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引
- 同步是关键, 如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容
- 所以: 如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题
- 更快的到达时间
- 无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记
分析
环境
- nodejs 6+
-
vue-server-renderer
和vue
必须匹配版本 -
vue-server-renderer
依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用 - 构建指令解析
- npm run build 以entry-server.js 与entry-client.js为路口文件 webpack打包
- npm run start:test 以server.js 开启nodejs 服务
未使用模板文件
// 第 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>
})
// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})
express中的使用方式
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为当前renderer渲染后返回的html文档
</html>
`)
})
})
server.listen(8080)
使用模板文件替换
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet--> //此注释不可删除,rendererToString的html插入点
</body>
</html>
- 使用模板切使用转义
//1.使用转义
<html>
<head>
<!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
<title>{{ title }}</title>
<!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
{{{ meta }}}
</head>
<body>
<!--vue-ssr-outlet--> //html
</body>
</html>
//2.模板可以是*.vue文件
- 最终使用方式
const Vue = require('vue');
const server = require('express')();
const template = require('fs').readFileSync('./index.template.html', 'utf-8');
const renderer = require('vue-server-renderer').createRenderer({
template, //使用模板文件服务器在renderToString直接返回完整的html文件--模板文件可包括
//vue的template插入到<!--vue-ssr-outlet-->注释处
//vue的data插入到对应的双花括号处(参考上图)
});
const context = {
title: 'vue ssr',
metas: `
<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue srr demo">
`,
};
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>访问的 URL 是: {{ url }}</div>`,
});
renderer
.renderToString(app, context, (err, html) => {
//最终与createRenderer的模板与Vue的app实例渲染后
//生成html返回给服务器
console.log(html);
if (err) {
res.status(500).end('Internal Server Error')
return;
}
res.end(html);
});
})
server.listen(8080);
生命周期
- 由于没有动态更新
- 只有
beforeCreate
和created
会在服务器端渲染 (SSR) 过程中被调用 - 其他生命周期都在客户端执行(mount)
- 避免在
beforeCreate
和created
生命周期时产生全局副作用的代码, 如setInterval
自定义指令— https://ssr.vuejs.org/zh/api/#directives
- 自定义指令直接操作 DOM,因此会在服务器端渲染 (SSR) 过程中导致错误
- 推荐使用组件作为抽象机制,并运行在「虚拟 DOM 层级(Virtual-DOM level)」(例如,使用渲染函数(render function))。 –组件化
- 如果你有一个自定义指令,但是不是很容易替换为组件,则可以在创建服务器 renderer 时,使用
directives
选项所提供”服务器端版本(server-side version)” –服务端版本
服务端避免VUE实例的状态单例
- Node.js 服务器是一个长期运行的进程
- 代码进入该进程时,进行一次取值并留存在内存中, 这意味着如果创建一个单例对象,它将在每个传入的请求之间共享
- 为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似
构建步骤
图解
webpack的使用
- 通常 Vue 应用程序是由 webpack 和
vue-loader
构建,但 例如通过file-loader
导入文件,通过css-loader
导入 CSS loader在NODE不全支持 - Node.js 最新版本能够完全支持 ES2015 特性,但客户端不支持所以需要编译
项目目录结构–Vue的实例
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry) // createAPP() vue实例工厂
// 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 //在store中创建router的模块做数据同步操作
sync(store, router)
// 创建应用程序实例,将 router 和 store 注入
const app = new Vue({
router,
store,
render: h => h(App)
})
// 暴露 app, router 和 store。
return { app, router, store }
}
├── entry-client.js # 仅运行于浏览器
import { createApp } from './app'
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
//因为路由器必须要提前解析路由配置中的异步组件,
才能正确地调用组件中可能存在的路由钩子-所以调用onReady
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')
})
└── entry-server.js # 仅运行于服务器
// entry-server.js
import { createApp } from './app'
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) {
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
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
}, reject)
})
}
└── server.js # node服务器启动文件
// server.js
const createApp = require('/path/to/built-server-bundle.js') //打包后的entry-server.js
server.get('*', (req, res) => {
const context = { url: req.url }
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) {
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else {
res.end(html)
}
})
})
})
└── store.js # vuex使用
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 假定我们有一个可以返回 Promise 的
// 通用 API(请忽略此 API 具体实现细节)
import { fetchItem } from './api'
export function createStore () {
return new Vuex.Store({
state: {
items: {}
},
actions: {
fetchItem ({ commit }, id) {
// `store.dispatch()` 会返回 Promise,
// 以便我们能够知道数据在何时更新
return fetchItem(id).then(item => {
commit('setItem', { id, item })
})
}
},
mutations: {
setItem (state, { id, item }) {
Vue.set(state.items, id, item)
}
}
})
}
└── store.js # vuex使用
router的实例-参考
- createRouter同vue相同生成工厂模式-router采用history模式
createApp
的Vue工厂实例化绑定router- 在entry-server.js中以promise的方式调用createApp,动态router.push请求url,生成router确保服务器在渲染前数据内容都准备好- 并返回resolve(app);
数据预取–参考
- vuex的使用
- 我们需要通过访问路由,来决定获取哪部分数据 – 这也决定了哪些组件需要渲染。所以在路由组件中放置数据预取逻辑 (asyncData)
- 服务器端的数据预取
- 在entry-server中执行router.onReady,然后获取当前路由匹配的组件(
getMatchedComponents
),匹配到组件则调用asyncData,而后设置context.state = store.state,没有匹配到组件则404
- 在entry-server中执行router.onReady,然后获取当前路由匹配的组件(
- 客户端的数据预取
- 在路由导航之前解析数据()
- router.
beforeResolve
中获取路由匹配的component,后调用asyncData获取所有数据
- router.
- 匹配要渲染的视图后,再获取数据 (用户更快看到页面)
- 全局注册Vue.mixin
beforeRouteUpdate
beforeMount
- 全局注册Vue.mixin
- 在路由导航之前解析数据()
Store 代码拆分--参考
- 我们的 Vuex store 可能会分为多个模块
- 我们可以在路由组件的
asyncData
钩子函数中,使用store.registerModule
惰性注册(lazy-register)这个模块:
// 在路由组件内
<template>
<div>{{ fooCount }}</div>
</template>
<script>
// 在这里导入模块,而不是在 `store/index.js` 中
import fooStoreModule from '../store/modules/foo'
export default {
asyncData ({ store }) {
store.registerModule('foo', fooStoreModule)
return store.dispatch('foo/inc')
},
// 重要信息:当多次访问路由时,
// 避免在客户端重复注册模块。
destroyed () {
this.$store.unregisterModule('foo')
},
computed: {
fooCount () {
return this.$store.state.foo.count
}
}
}
</script>
客户端激活
- 由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素
- 我们需要”激活”这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)
- 注意如table这些标签,如果没有tbody浏览器会自动添加tbody标签,这会导致 如果无法匹配,它将退出混合模式,丢弃现有的 DOM 并从头开始渲染 — 在开发模式下,Vue 将推断客户端生成的虚拟 DOM 树 (virtual DOM tree),是否与从服务器渲染的 DOM 结构 (DOM structure) 匹配
// 强制使用应用程序的激活模式
app.$mount('#app', true)
非框架实现: