书接上回,根据上集预告,这集要引入vuex,来实现真正的请求数据并且服务端渲染。
所以我们只需在上篇文章的代码中进行修改即可。
在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。
另一个需要关注的问题是在客户端,在挂载(mount)到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。
为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。
所以我们先引入vuex
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
// 假定我们有一个可以返回 Promise 的
// 通用 API(请忽略此 API 具体实现细节)
export default function createStore () {
return new Vuex.Store({
state: {
list: {}
},
actions,
mutations
})
}
其中 actions和mutations我单独封装了一个js
// actions.js
import axios from 'axios'
export default {
fetchItem ({ commit }) {
return axios.get('http://mapi.itougu.jrj.com.cn/xlive_poll/getLastNotice')
.then(function (response) {
commit('setItem', response.data)
})
}
}
// mutations.js
export default {
setItem (state, data) {
state.list = data
}
}
并且在app.js中引入
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
import createStore from './store'
import { sync } from 'vuex-router-sync'
Vue.config.productionTip = false
export function createApp () {
const router = createRouter()
const store = createStore()
sync(store, router)
const app = new Vue({
// el: '#app',
store,
router,
render: h => h(App)
})
return { app, store, router }
}
对于需要请求数据的组件,我们暴露出一个自定义静态函数asyncData,注意,由于此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去
// home.vue
这是首页
服务端数据预存
在 entry-server.js 中,我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。
// entry-server.js
import { createApp } from './app'
export default (context) => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, store, router } = createApp(context)
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
// 对所有匹配的路由组件调用 `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)
})
}
当使用 template 时,context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中.而在客户端,在挂载到应用程序之前,store 就应该获取到状态:
// entry-client.js
import Vue from 'vue'
import 'es6-promise/auto'
import { createApp } from './app'
// 客户端特定引导逻辑……
Vue.mixin({
data(){ //全局mixin一个loading
return {
loading:false
}
},
beforeMount(){ //在挂载之前
const {asyncData}=this.$options
let data=null; //把数据在computed的名称固定为data,防止重复渲染
try{
data=this.data; //通过try/catch包裹取值,防止data为空报错
}catch(e){}
if(asyncData&&!data){ //如果拥有asyncData和data为空的时候,进行数据加载
//触发loading加载为true,显示加载器不显示实际内容
this.loading=true;
//为当前组件的dataPromise赋值为这个返回的promise,通过判断这个的运行情况来改变loading状态或者进行数据的处理 (在组件内通过this.dataPromise.then保证数据存在)
this.dataPromise=asyncData({store,route:router.currentRoute})
this.dataPromise.then(()=>{
this.loading=false;
}).catch(e=>{
this.loading=false;
})
}else if(asyncData){
//如果存在asyncData但是已经有数据了,也就是首屏情况的话返回一个成功函数,防止组件内因为判断then来做的操作因为没有promise报错
this.dataPromise=Promise.resolve();
}
}
})
const { app, store, router } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
// 这里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
app.$mount('#app')
})
// service worker
function isLocalhost() {
return /^http(s)?:\/\/localhost/.test(location.href);
}
if (('https:' === location.protocol || isLocalhost()) && navigator.serviceWorker) {
navigator.serviceWorker.register('/service-worker.js')
}
这样,在第一篇文章的基础下,我们就把整个vue-ssr项目配置完了。
npm run server
npm run dev
至于为什么要引入mixins?
客户端数据预取数据时,可以在视图组件的 beforeMount 函数中。当路由导航被触发时,可以立即切换视图,因此应用程序具有更快的响应速度。然而,传入视图在渲染时不会有完整的可用数据。因此,对于使用此策略的每个视图组件,都需要具有条件加载状态。
什么是mixins?
这里就引入官网的介绍吧,说的很明白、
混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
一个小李子
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合
同名钩子函数将混合为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
注意,我们文章中的例子是全局混入,一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。使用恰当时,可以为自定义对象注入处理逻辑。
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
现在一个项目的骨架基本出来,要想它变得有血有肉,就需要根据项目具体进行配置了,当然如果对webpack十分熟练的话,这个demo还可以继续优化。就留这有时间的吧。
码字不易,欢迎打赏,且行且珍惜