实战vue-ssr服务端渲染的单页应用

439 阅读3分钟

这次带领各位看官来实现真正的请求数据并且服务端渲染,框架结构vuex+node+express+webpack。

为了节省时间,此处不再做项目配置过程,有需要可以自己百度一下,只在本篇文章的代码中进行修改。

在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。

另一个需要关注的问题是在客户端,在挂载(mount)到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。 为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。

所以我们先引入vuex

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
import Vue from 'vue'import Vuex from 'vuex'import actions from './actions'import mutations from './mutations'
Vue.use(Vuex)
export default function createStore () { return new Vuex.Store({ state: { list: {} }, actions, mutations })}

其中 actions和mutations我单独封装了一个js

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
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)        })     }}
export default { setItem (state, data) { state.list = data }}

并且在app.js中引入

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
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({ store, router, render: h => h(App) }) return { app, store, router }}

对于需要请求数据的组件,我们暴露出一个自定义静态函数asyncData,注意,由于此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
<template>  <div class="hello">    <div v-if="loading"></div>    <div v-else>      <h1>这是首页</h1>      <div v-html="msg.noticeContent"></div>          </div>
</div></template>
<script>export default { data () { return { title: '' } }, asyncData ({ store, route }) { // 触发 action 后,会返回 Promise return store.dispatch('fetchItem') }, computed: { msg(){ return this.$store.state.list } }, beforeMount(){ this.dataPromise.then(()=>{ //对数据再处理 //computed是在被调用时才会加载数据,data在初始化时不能直接调用computed的数据否则会抛出异常,可以把赋值操作放到这里 }) }, mounted () {
}}</script>
<!-- Add "scoped" attribute to limit CSS to this component only --><style scoped>h1, h2 { font-weight: normal;}ul { list-style-type: none; padding: 0;}li { display: inline-block; margin: 0 10px;}a { color: #42b983;}</style>

服务端数据预存

在 entry-server.js 中,我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
import { createApp } from './app'
export default (context) => { 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.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject({ code: 404 }) } Promise.all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute }) } })).then(() => { context.state = store.state resolve(app) }).catch(reject)
}, reject) })}

当使用 template 时,context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中.而在客户端,在挂载到应用程序之前,store 就应该获取到状态:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
import Vue from 'vue'import 'es6-promise/auto'import { createApp } from './app'
Vue.mixin({ data(){ return { loading:false } }, beforeMount(){ const {asyncData}=this.$options let data=null; try{ data=this.data; }catch(e){}
if(asyncData&&!data){ this.loading=true; this.dataPromise=asyncData({store,route:router.currentRoute}) this.dataPromise.then(()=>{ this.loading=false; }).catch(e=>{ this.loading=false; }) }else if(asyncData){ this.dataPromise=Promise.resolve(); } }})
const { app, store, router } = createApp()
if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__)}
router.onReady(() => { app.$mount('#app')})
function isLocalhost() { return /^http(s)?:\/\/localhost/.test(location.href);}
if (('https:' === location.protocol || isLocalhost()) && navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js')}

这样我们就把整个vue-ssr项目配置完了。

  • ounter(line
npm run server
  • ounter(line
npm run dev

至于为什么要引入mixins?

客户端数据预取数据时,可以在视图组件的 beforeMount 函数中。当路由导航被触发时,可以立即切换视图,因此应用程序具有更快的响应速度。然而,传入视图在渲染时不会有完整的可用数据。因此,对于使用此策略的每个视图组件,都需要具有条件加载状态。

什么是mixins?

这里就引入官网的介绍吧,说的很明白、

混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
var myMixin = {  created: function () {    this.hello()  },  methods: {    hello: function () {      console.log('hello from mixin!')    }  }}

var Component = Vue.extend({ mixins: [myMixin]})
var component = new Component()

选项合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合 同名钩子函数将混合为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
var mixin = {  created: function () {    console.log('混入对象的钩子被调用')  }}
new Vue({ mixins: [mixin], created: function () { console.log('组件钩子被调用') }})

值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
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() vm.bar() vm.conflicting()

注意,我们文章中的例子是全局混入,一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。使用恰当时,可以为自定义对象注入处理逻辑。

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
Vue.mixin({  created: function () {    var myOption = this.$options.myOption    if (myOption) {      console.log(myOption)    }  }})
new Vue({ myOption: 'hello!'})

现在一个项目的骨架基本出来,要想它变得有血有肉,就需要根据项目具体进行配置了,当然如果对webpack十分熟练的话,这个demo还可以继续优化。就留这有时间的吧。