首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法变现,坚持下去也是靠的是本身的热情和你们的鼓励。各位读者的Star是激励我前进的动力,请不要吝惜。javascript
Vue同构系列的文章已经出到第三篇了,前两篇文章Vue同构(一): 快速上手与Vue同构(二):路由与代码分割都取得了不错的反响(多是错觉),前两篇文章本质上讲了如何在服务端渲染中使用Vue与Vue Router,基本的Vue全家桶中除了Vuex尚未讲,这篇文章也是围绕这个主题来说的。vue
一直很认同Redux做者Dan Abramov的一句话:java
Flux 架构就像眼镜:你自会知道何时须要它。node
其中很有几分“只可意会不可言传”的感受,咱们先来看看什么状况下咱们须要在服务端渲染中引入Vuex?ios
前面的两篇文章的例子都足够的简单,然而实际的业务场景并不会如此的简单。好比咱们想要渲染的是文章的列表,那咱们确定须要向数据源请求数据。在客户端渲染中,这一切太稀疏日常了。你可能立刻会想到在组件的生命周期mounted
方法中去请求异步的数据接口,而后将请求的数据赋值给Vue的响应式数据,Vue会自动刷新界面,一切都是如此的完美,好比像下面的例子:git
<template>
// ......省略
</template>
<script>
export default {
data: function(){
return {
items: []
}
},
mounted: function(){
// 咱们并不关心请求接口的具体实现逻辑
fetchAPI().then(data => {
// 赋值
this.items = data.items;
})
}
}
</script>
复制代码
可是到了服务器渲染中,你想这么干是铁定行不通了,由于在服务端压根就不会执行到mounted
的生命周期中,咱们以前说过在服务器端Vue的实例仅仅只会执行生命周期函数beforeCreate
和created
,那么咱们把数据请求的逻辑放置在这个两个生命周期中是否可行呢?答案是不能够的,由于数据请求的操做是异步的,咱们并不能预期何时数据能返回。而且咱们还须要考虑到,不只服务端在渲染界面的时候须要数据,客户端也须要首屏页面的数据,由于客户端须要对其进行激活,难道咱们须要分别在服务端和服务端两次请求同一份数据吗?那么不管是服务器仍是数据源都会压力陡增,确定不是咱们所但愿看到的。github
其实解决方案仍是比较明确的:数据和组件分离,咱们在服务器渲染组件以前就将数据准备好并放置在容器中,所以服务器渲染的过程当中就能够直接从容器中拿现成的数据渲染。不只如此,咱们能够将该容器中的数据直接序列化,注入到请求的HTML中,这样客户端激活组件的时候,也能直接拿到相同的数据进行渲染,不只仅能减小相同的数据的请求而且还能够防止由于请求数据的不相同致使的激活失败从而客户端从新渲染(开发模式下,生产模式下不会检测,则激活就会出错)。那谁来担任数据容器的职责呢,显然就是咱们今天讲的Vuex了。vue-router
咱们接着在上一篇文章中代码的构建配置基础上开始咱们的尝试(文末会有代码连接),首先咱们来讲说咱们目标,咱们借用CNode提供的文章接口,而后在界面中渲染出不一样标签下的文章列表,不一样路由标签之间切换能够加载不一样的文章列表。咱们使用axios
做为Node服务端和浏览器客户端通用的HTTP请求库。先写接口, CNode给咱们提供了以下的接口:vuex
GET URL: cnodejs.org/api/v1/topi…axios
参数: page Number 页数 参数: tab 主题分类。目前有 ask share job good 参数: limit Number 每一页的主题数量
咱们此次就选三个tab主题分别使用,分别是精华(good
)、分享(share
)、问答(ask
)
首先对组件提供接口:
// api/index.js
import axios from "axios";
export function fetchList(tab = "good") {
const url = `https://cnodejs.org/api/v1/topics?limit=20&tab=${tab}`;
return axios.get(url).then((data)=>{
return data.data;
})
}
复制代码
做为演示咱们仅渲染前20条数据。
接下来咱们引入Vuex,以前两篇文章都提到了咱们须要为每次请求都生成新的Vue与Vue Router实例,其根本缘由是防止不一样请求之间数据共享致使的状态污染。Vuex也是相同的缘由,咱们须要为每次请求都生成新的Vuex实例。
import Vue from 'vue'
import Vuex from 'vuex'
import { fetchList } from '../api'
Vue.use(Vuex)
export function createStore() {
return new Vuex.Store({
state: {
good: [],
ask: [],
share: []
},
actions: {
fetchItems: function ({commit}, key = "good") {
return fetchList(key).then( res => {
if(res.success){
commit('addItems', {
key,
items: res.data
})
}
})
}
},
mutations: {
addItems: function (state, payload) {
const {key, items} = payload;
state[key].push(...items);
}
}
})
}
复制代码
这里咱们假设你已经对Vuex有所了解,首先咱们调用Vue.use(Vuex)
将Vuex注入到Vue中,而后每次调用createStore
都会返回新的Vuex实例,其中state
中包含good
、ask
、share
数组用来存储对应主题的文章信息。 名为addItems
的 mutation
负责向state
中对应的数组中增长数据,而名为fetchItems
的action
则负责调用异步接口请求数据并更新对应的mutation
。
那咱们何时调用fetchItems
是须要考虑一下。特定路由对应于特定的组件,而特定的组件则须要特定数据作渲染。咱们说过的实现逻辑是在组件渲染前就获取到所用的数据,在纯客户端渲染的程序中咱们将请求的逻辑放置在对应组件的生命周期中,在服务端渲染中,咱们仍然将该逻辑放置在组件内,这样,不只在服务端渲染的时候经过匹配的组件就能执行其请求数据的逻辑,而且在客户端激活后,组件内部也能够在必要的时刻中执行逻辑去请求或者更新数据。咱们看例子:
// TopicList.vue
<template>
<div>
<div v-for="item in items">
<span>{{ item.title }}</span>
<button @click="openTopic(item.id)">打开</button>
</div>
</div>
</template>
<script>
export default {
name: "topic-list",
asyncData: function ({ store, route}) {
// 演示逻辑,不想屡次加载数据
if(store.state[route.params.id].length <=0){
return store.dispatch("fetchItems", route.params.id)
}else {
return Promise.resolve()
}
},
computed: {
items: function () {
return this.$store.state[this.$route.params.id];
}
},
methods: {
openTopic: function (id) {
window.open(`https://cnodejs.org/topic/${id}`)
}
}
}
</script>
<style scoped>
</style>
复制代码
Vue组件的模板不须要解释,之因此增长button
按钮来打开对应文章的连接主要是想验证客户端是否正确激活。该组件从store
中获取数据,其中route
的id
表示文章的主题。最不同凡响的是,该组件咱们对外暴露了一个自定义的静态函数asyncData
,由于是组件的静态函数,所以咱们能够在组件都没建立实例以前就调用方法,可是由于还未建立实例,所以函数内部不能访问this
。asyncData
内部逻辑是触发store
中的fetchItems
的action
。
接下来咱们看路由的配置:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter() {
return new Router({
mode: "history",
routes: [{
path: '/good',
component: () => import('../components/TopicListCopy.vue')
},{
path: '/:id',
component: () => import('../components/TopicList.vue')
}]
})
}
复制代码
咱们给good
路由配置了特殊的TopicListCopy
组件,他与TopicList
除了名字以外,其余的所有同样,其余的路由咱们使用前面介绍的TopicList
组件,之因此要这么作主要是出于方便后面介绍其中的操做。
而后咱们看一下应用的入口app.js
:
import Vue from 'vue'
import { createStore } from './store'
import { createRouter } from './router'
import App from './components/App.vue'
export function createApp() {
const store = createStore()
const router = createRouter()
const app = new Vue({
store,
router,
render: h => h(App)
})
return {
app,
store,
router
}
}
复制代码
和以前的代码大体相同,只不过在每次调用createApp
函数的时候,建立Vuex
的实例store
,并给Vue
实例注入store
实例。
接下来看服务端渲染的入口entry-server.js
:
// entry-server.js
import { createApp } from './app'
export default function (context) {
return new Promise((resolve, reject) => {
const {app, store, router} = createApp()
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if(matchedComponents.length <= 0){
return reject({ code: 404 })
}else {
Promise.all(matchedComponents.map((component) => {
if(component.asyncData){
return component.asyncData({
store,
route: router.currentRoute
})
}
})).then(()=> {
context.state = store.state
resolve(app)
})
}
}, reject)
})
}
复制代码
服务端的渲染入口文件和以前的结构基本保持一致,onReady
会在全部的异步钩子函数和异步组件加载完毕以后执行传递的回调函数。上篇文章是在onReady
回调函数中直接执行了resolve(app)
将对应的组件实例传递。可是在这里咱们作了一些其余的工做。首先咱们调用了router.getMatchedComponents()
获取了当前路由匹配的路由组件,注意咱们这里匹配的路由组件并非实例而仅仅只是配置对象,而后咱们调用全部匹配的路由组件中的asyncData
静态方法,加载各个路由组件所需的数据,等到全部的路由组件的数据都加载完毕以后,将当前store
中的state
赋值给context.state
并resolve
了组件实例。须要注意的是,这时store
中存有首屏渲染组件所需的全部数据,咱们将其值赋值给context.state
,renderer
若是使用的是template
的话,会将状态序列化并经过注入HTML的方式存储到window.__INITIAL_STATE__
上。
接下来咱们看浏览器渲染入口entry-client.js
:
//entry-client.js
import { createApp } from './app'
const {app, store, router} = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
app.$mount('#app')
})
复制代码
浏览器激活的逻辑也和上篇文章相相似,惟一不一样的是,咱们在一开始就调用replaceState
将store
中的状态state
替换成window.__INITIAL_STATE__
,这样客户端直接能够用此数据激活避免二次请求。
与上一篇文章中的代码相比,服务器的server.js
代码保持一致,没有其余的修改。如今咱们打包看一下咱们程序的效果:
咱们发现服务端获取了数据渲染了文章列表而且点击右侧的按钮能够打开文章的连接,说明客户端已经被正确的激活。可是当咱们在不一样路由之间进行切换的时候,发现其余的主题并无加载,这是由于咱们只写了服务端渲染中的数据获取,而在客户端中不一样的路由切换对应的数据加载应该是客户端独立请求的。所以咱们须要添加这部分的逻辑。
以前咱们已经说过,咱们把数据请求的逻辑预置在组件的静态函数asyncData
中,客户端的请求的走这个逻辑,那么客户端应该在何时去调用这个函数呢?
官方文档中给出两个思路,一个是在路由导航以前就解析好数据。一个是在视图渲染后再请求数据
先请求数据,等到数据请求完毕以后,再渲染组件,要实现这个逻辑咱们要借助Vue Router中的beforeResolve
解析守卫,在全部组件内守卫和异步路由组件被解析以后,beforeResolve
解析守卫就被调用。让咱们改造一下客户端渲染入口逻辑:
import { createApp } from './app'
const {app, store, router} = 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))
})
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')
})
复制代码
上面的beforeResolve
中的代码逻辑,首先比较to
与from
路由的匹配路由组件,而后找出两个匹配列表的差别组件,再调用全部差别组件中的asyncData
去获取数据,待全部数据获取到后,调用next
继续执行。
这时候咱们打包并运行程序,咱们发现good
切换到ask
或者share
是能够加载数据的,可是ask
和share
切换是无法加载数据的,以下图:
这是为何呢?还记得咱们以前专门为good
路由设置了TopicListCpoy
路由组件,为share
与ask
路由设置了TopicList
路由组件,所以share
与ask
切换过程当中并且并不存在差别组件,只是路由参数发生了变化。为了解决这个问题,咱们增长组件内守卫解决这个问题:
beforeRouteUpdate: function (to, from, next) {
this.$options.asyncData({
store: this.$store,
route: to
});
next()
}
复制代码
组件守卫beforeRouteUpdate
会在当前路由改变,可是仍然属于该组件被复用时调用,好比动态参数发生改变的时候,beforeRouteUpdate
就会被调用。这时咱们执行加载数据的逻辑,问题就会获得解决。在使用先预取数据,再加载组件的方式存在一个易见的问题就是会感觉到明显的卡顿感,由于你不能保证数据何时能请求结束,若是请求数据时间过长而致使组件迟迟不能渲染,用户体验就会大打折扣,所以建议在加载的过程当中提供一个统一的加载指示器,来尽可能下降带来的交互体验降低。
先渲染组件再请求数据的逻辑比较接近与纯客户端渲染的逻辑,咱们将数据预取的逻辑放置在组件的beforeMount
或者mounted
生命周期函数中,路由切换以后,组件会被当即渲染,可是会存在渲染组件时不存在完整数据,所以这个组件内部自身须要提供相应加载状态。数据预取的逻辑能够在每一个路由组件单独调用,固然也能够经过Vue.mixin
的方式全局实现:
Vue.mixin({
beforeMount () {
const { asyncData } = this.$options
if (asyncData) {
asyncData({
store: this.$store,
route: this.$route
})
}
}
})
复制代码
固然这种也会存在咱们前面说过的,路由切换可是组件复用的状况,所以仅仅只在beforeMount
作操做作数据获取是不够的,咱们在路由参数发生改变可是组件复用的状况下,也应该去请求数据,这个问题仍然能够经过组件守卫beforeRouteUpdate
来处理。
到此为止咱们已经介绍了如何在服务器渲染中处理数据和预览的问题,须要看源码的同窗请移步到这里。若是有表达不正确的地方,欢迎指出,但愿你们关注个人Github博客以及接下来的系列文章。