美文网首页让前端飞程序员前端开发那些事
Vue-SSR系列(二)vue2.0+vuex+node+exp

Vue-SSR系列(二)vue2.0+vuex+node+exp

作者: 范小饭_ | 来源:发表于2018-09-03 22:41 被阅读55次

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

    // 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


    5001.gif

    npm run dev

    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还可以继续优化。就留这有时间的吧。

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

    相关文章

      网友评论

      • My_Life001:都是9年义务教育,凭什么你这么优秀?

      本文标题:Vue-SSR系列(二)vue2.0+vuex+node+exp

      本文链接:https://www.haomeiwen.com/subject/byvmwftx.html