Vue-SSR系列(二)vue2.0+vuex+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

书接上回,根据上集预告,这集要引入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


Vue-SSR系列(二)vue2.0+vuex+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo_第1张图片
5001.gif

npm run dev

Vue-SSR系列(二)vue2.0+vuex+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo_第2张图片
8081.gif

至于为什么要引入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还可以继续优化。就留这有时间的吧。

码字不易,欢迎打赏,且行且珍惜

你可能感兴趣的:(Vue-SSR系列(二)vue2.0+vuex+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo)