Vue + Nuxt 服务端渲染从入门到放弃(2)

放几个阿里云的优惠链接 代金券 / 高性能服务器2折起 / 高性能服务器5折

上一篇将了如何项目的基本结构和如何创建。本文讲下服务端渲染的数据同步与Auth的Token问题。
本文额外使用cookie-universal-nuxt模块来快捷处理cookie。

Vuex使用

Nuxt集成了Vuex作为数据共享的组件,所以启用Vuex也非常简单。直接在store目录下新建index.js文件,创建Vuex有些区别,不推荐使用**export default new Vuex.Store({})**这种方式。官网给了例子,只需要直接export state, mutations, actions这些即可。Nuxt还提供了nuxtServerInit方法用户服务端初始化时数据的处理。其他的调用方式基本和客户端渲染时一样。

export const state = () => ({
  token: null
})
export const mutations = {
  setToken(state, token) {
    state.token = token
  }
}

实现简单的身份验证

由于Nuxt服务端不存在localStorage这些在window上的对象,所以不能使用这些在服务端上存数数据。因此需要通过Vuex将数据进行同步。

其实服务端可以实现自己的数据存储方式,毕竟大部分情况下Nuxt都是部署在Node服务器下的,完全可以通过Node的数据存储方案来实现。

虽然Nuxt给出了一个auth-module用于做身份验证,但是对于简单的应用来说并完全可以通过Vuex来自己实现相应的功能。在上面已经设置了token属性,下面来演示如何使用。
首先前端的token采用存储在cookie上面,这样每次请求时会自动发送token。
添加nuxtServerInit来获取请求中的token。当不存在对应的cookie时则认为当前没有通过身份验证。相应的页面显示和逻辑自行实现。

export const actions = {
  nuxtServerInit({commit}, {app}) {
    let token = app.$cookies.get('token')
    commit('setToken', token)
  }
}

创建auth.js中间件检测token

export default function ({ store, redirect }) {
  // 如果用户未经过身份验证
  if (!store.state.token) {
    return redirect('/login')
  }
}

在nuxt.config.js中配置中间件,也可以针对单独页面配置。

  router: {
    middleware: 'auth'
  },

这样当没有登陆的用户访问时会被重定向到login页面进行登陆。当登陆成功后将token写入cookie即可实现一个简单的身份认证功能(需要判断process.browser来确认是否是浏览器,只有浏览器才有cookie保存的功能)。

实现一个DataStore来完成数据的存取

上面虽然实现了数据的共享,但是对于一些数据每次都需要判断是否是浏览器环境显的有些繁琐,下面来实现一个插件来免去这些判断。

创建一个接口定义

定义一些基本的方法。

export class DataStore {
  setItem(key, value) {
  }

  getItem(key) {
  }

  getKeys() {
  }

  removeItem(key) {
  }

  clear() {
  }
}

实现一个Cookie的存储

import Cookies from 'js-cookie'

export class CookieStore extends DataStore {
  name = 'CookieStore'
  prefix = ''
  _keys = '__cookieds_keys__'
  expires = 365

  constructor(prefix = 'ds', expires = 365) {
    super()
    this.prefix = prefix
    this._keys = `${prefix}__cookieds_keys__`
    this.expires = expires
  }

  setItem(key, value) {
    Cookies.set(key, JSON.stringify(value), {expires: this.expires})
    let keys = this.getKeys()
    if (keys.findIndex(k => k === key) >= 0) {
      return
    }
    localStorage.setItem(this._keys, JSON.stringify([...keys, key]))
  }

  getItem(key) {
    const v = Cookies.get(key)
    if (v === undefined) {
      return undefined
    }
    return JSON.parse(v)
  }

  getKeys() {
    const v = localStorage.getItem(this._keys)
    if (v === undefined || v === null) {
      return []
    }
    return JSON.parse(v)
  }

  removeItem(key) {
    Cookies.remove(key)
    let keys = this.getKeys()
    keys = keys.filter(k => k !== key)
    localStorage.setItem(this._keys, JSON.stringify(keys))
  }

  clear() {
    let keys = this.getKeys()
    keys.forEach(k => {
        Cookies.remove(k)
      }
    )
    localStorage.removeItem(this._keys)
  }
}

实现一个Vuex的存储

export class VuexStore extends DataStore {
  vuex = null
  name = 'VuexStore'
  moduleName = null

  constructor(vuex, moduleName = '__vuex_store__') {
    super()
    this.vuex = vuex
    this.moduleName = moduleName
    // 数据同步时只会同步state导致模块未同步,这里用 _mutations 来判断重复注册
    if (vuex._mutations[moduleName + '/__updateStore__']) {
      return
    }
    vuex.registerModule(moduleName, {
      state: {
        map: {}
      },
      namespaced: true,
      mutations: {
        __updateStore__(state, value) {
          state.map = value
        }
      }
    })
  }

  setItem(key, value) {
    const map = Object.assign({}, this.vuex.state[this.moduleName].map)
    map[key] = value
    this.vuex.commit(this.moduleName + '/__updateStore__', map)
  }

  getItem(key) {
    const v = this.vuex.state[this.moduleName].map[key]
    if (typeof v === 'object') {
      return Object.assign({}, v)
    }
    return v
  }

  getKeys() {
    return Object.keys(this.vuex.state[this.moduleName].map)
  }

  removeItem(key) {
    const map = Object.assign({}, this.vuex.state[this.moduleName].map)
    delete map[key]
    this.vuex.commit(this.moduleName + '/__updateStore__', map)
  }

  clear() {
    this.vuex.commit(this.moduleName + '/__updateStore__', {})
  }
}

创建一个代理存储

export class MixStore extends DataStore {
  stores = []
  name = 'MixStore'

  constructor(stores = []) {
    super()
    this.stores = stores
  }

  setItem(key, value) {
    this.stores.forEach(s => {
      if (s.test) {
        if (s.test(key, value)) {
          s.setItem(key, value)
        }
      } else {
        s.setItem(key, value)
      }
    })
  }

  getItem(key) {
    for (let i = 0; i < this.stores.length; i++) {
      const s = this.stores[i]
      if (s.test) {
        if (s.test(key)) {
          const v = s.getItem(key)
          if (v !== undefined) {
            return v
          }
        }
      } else {
        const v = s.getItem(key)
        if (v !== undefined) {
          return v
        }
      }
    }
  }

  getKeys() {
    let keys = []
    this.stores.forEach(s => {
      keys = [...keys, s.getKeys()]
    })
    const tmp = {}
    keys.forEach(key => {
      tmp[key] = 0
    })
    return Object.keys(tmp)
  }

  removeItem(key) {
    this.stores.forEach(s => {
      s.removeItem(key)
    })
  }

  clear() {
    this.stores.forEach(s => {
      s.clear()
    })
  }
}

编写插件逻辑

在plugin下面创建ds.js,根据不同的环境创建不通的存储实例即可。

import Vue from 'vue'

/**
 * 初始化全局公用实例,前后端同步
 * 同步采样Vuex,因为这个是自动同步的
 * 客户端在Vuex之后在添加其他数据存储策略
 */
export default (ctx) => {
  let store = null
  if (process.browser) {
    const stores = []
    stores.push(new VuexStore(ctx.store))
    stores.push(new CookieStore())
    store = new MixStore(stores)
  } else {
    store = new VuexStore(ctx.store)
  }

  Vue.prototype.$ds = store
  ctx.$ds = store
  ctx.store.$ds = store
}

配置nuxt.config.js

 plugins: [
    ...
    '@/plugins/api',
  ],

在使用时只需要调用this.$ds即可获取相应对象。

总结

Nuxt服务端渲染中存在的最大问题便是数据如何同步,虽然官方给出了Vuex作为解决方案,但是要同时处理客户端与服务端中的差异还是存在一些问题,当项目复杂之后还需要考虑数据缓存等问题。尤其时那些直接从单页面项目直接迁移过来的(比如说我),迁移中会有很多的坑需要填(这些以后在讲了)。

你可能感兴趣的:(开发笔记)