状态管理 VueX & SSR - 04

1,267 阅读7分钟

VueX & SSR

状态管理

本质就是一个对象

状态? 怎么管理? 为什么管理?

前端发展到今天,更主流的模式是 UI = f(state),视图通过底层框架(vuereact 等提供的能力)根据状态进行驱动。

状态:描述视图某个时间点的数据; 状态大致可以分为两类,本地状态共享状态

本地状态就是 vue 中的 datareact中的 state,这里我们一般会用来控制弹窗的现实隐藏、loading效果等

状态自管理应用包含以下几个部分:

状态,驱动应用的数据源; 视图,以声明方式将状态映射到视图; 操作,响应在视图上的用户输入导致的状态变化。

image.png

共享状态其实是最头疼的问题,但又是最常见的场景,业务中肯定会出现大量需要兄弟节点通信、祖孙节点通信等情况的场景,通信的目的是为了状态分享,虽然可以通过一些方式,比如回调函数等手段实现,但都不是最佳实践

状态管理的方式:中心化和去中心化 中心化:redux / vueX 组件的数据最终归到一个数中 去中心化:this.state 没有根节点,可以在任意场合调用。但需要有一个方式处理共享

SPA应用最佳实践:容器组件 + UI 组件 容器组件负责逻辑

所以 Flux 架构及其追随者 Redux Vuex被提出,主要思想是应用的状态被集中存放到一个仓库中,但是仓库中的状态不能被直接修改必须通过特定的方式才能更新状态

理想的状态管理工具需要解决的问题

  1. 状态更新的设计,API 足够少,且简单
  2. 如何共享状态
  3. 状态提升
  4. 状态下降
  5. 同步、异步的处理
  6. 持久状态和临时状态如何区分维护
  7. 状态更新的事务如何管理
  8. 去中心化
  9. ...

总的来说,这仍然是一个有很大空间的方向

VueX 核心原理

集中化管理

VueX 以及 redux 都受到 FLUX 思想影响:

  1. 状态都需要中心化、集中式的存储
  2. 状态的拿取符合单一原则,方式单一,保证准确性

例子:重复

// a.vue
<h1>{{ username }}</h1>

// b.vue
<h2>
  {{ username }}
</h2>

/**
* 如果 username 需要在每个组件都获取一次,是不是很麻烦,虽然可以通过共同的父级传入,但是不都是这种理想情况
*/

集中式状态的好处:

  1. 可以消除重复的状态声明
  2. 消除了不对等的风险(如前后端状态不一致,前端usrname小红,后端小黑)
  3. 代码方便维护和测试

如何更新状态

FLUX 推崇以一种可预测的方式发生变化,而且有且唯一一种,这样的好处是所有的行为可预测,可测试,对于之后做个 dev-tool 去调试、时间旅行都很方便,现在的问题就是要去思考同步和异步的问题了,为了区分的更清楚

定义两种行为,Actions 用来处理异步状态变更(内部还是调用 Mutations),Mutations 处理同步的状态变更,整个链路应该是一个闭环,单向的,完美契合 FLUX 的思想

「页面 用户或默认行为 dispatch/commit」-> 「actions/mutations」-> 「状态变更」-> 「页面更新」-> 「页面 dispatch/commit」...

同步 commit mutations 异步 dispatch action -> commit mutations

image.png

如何和 vue 集成

  1. 每个组件如何拿到 状态实例?

通过 mixin$store 这样的快速访问 store 的快捷属性注入到每一个 vue 实例中

// 例子,组件内
this.$store.state.a 
this.$store.commit

react 不可以,直接import 引入

  1. 数据得和视图响应式的连接起来

利用 vue data 里的状态响应

整个设计思路完成!!

代码

Step 1 - store 注册 (如何使用)

/**
* store.js - store 注册
*/
let Vue

// vue 插件必须要这个 install 函数
export function install(_Vue) {
  // 拿到 Vue 的构造器,存起来
  Vue = _Vue
  // 通过 mixin 注入到每一个vue实例 👉 https://cn.vuejs.org/v2/guide/mixins.html
  Vue.mixin({ beforeCreate: vuexInit })
  
  function vuexInit () {
    const options = this.$options
    // 这样就可以通过 this.$store 访问到 Vuex 实例,拿到 store 了
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

Step2 - 响应式

/**
* store.js - 实现响应式
*/
export class Store {
  constructor(options = {}) {
    resetStoreVM(this, options.state)
  }
  
  get state () {
    return this._vm._data.$$state
  }
}

function resetStoreVM(store, state) {
  // 因为 vue 实例的 data 是响应式的,正好利用这一点,就可以实现 state 的响应式
  store._vm = new Vue({
    data: {
      $$state: state
    }
  })
}

Step3 - 衍生数据 getter

/**
* store.js - 衍生数据(getters)
*/
export class Store {
  constructor(options = {}) {
    
    const state = options.state
    
    resetStoreVM(this, state)
    
    // 重点关注如下代码
    // 我们用 getters 来收集衍生数据 computed
    this.getters = {}
    
    // 简单处理一下,衍生不就是计算一下嘛,传人 state
    _.forEach(this.getters, (name, getterFn) => {
      Object.defineProperty(this.getters, name, {
        get: () => getterFn(this.state)
      })
    })
  }
  
  get state () {
    return this._vm._data.$$state
  }
}

function resetStoreVM(store, state) {
  store._vm = new Vue({
    data: {
      $$state: state
    }
  })
}

Step4 - Actions/Mutations (重点)

/**
* store.js - Actions/Mutations 行为改变数据
*/
export class Store {
  constructor(options = {}) {
    
    const state = options.state
    
    resetStoreVM(this, state)
    
    this.getters = {}
    
    _.forEach(options.getters, (name, getterFn) => {
      Object.defineProperty(this.getters, name, {
        get: () => getterFn(this.state)
      })
    })
    
    // 重点关注注释部分
    // 定义的行为,分别对应异步和同步行为处理
    this.actions = {}
    this.mutations = {}
    
    _.forEach(options.mutations, (name, mutation) => {
      this.mutations[name] = payload => {
        // 最终执行的就是 this._vm_data.$$state.xxx = xxx 这种操作
        mutation(this.state, payload)
      }
    })
    
    _.forEach(options.actions, (name, action) => {
      this.actions[name] = payload => {
        // action 专注于处理异步,这里传入 this,这样就可以在异步里面通过 commit 触发 mutation 同步数据变化了
        action(this, payload)
      }
    })
  }
  
  // action this 可以调用 commit
  // 触发 mutation 的方式固定是 commit
  commit(type, payload) {
    this.mutations[type](payload)
  }
  
  // 触发 action 的方式固定是 dispatch
  dispatch(type, payload) {
    this.actions[type](payload)
  }
  
  get state () {
    return this._vm._data.$$state
  }
}

function resetStoreVM(store, state) {
  store._vm = new Vue({
    data: {
      $$state: state
    }
  })
}

Step5 - 分形,拆分出多个 Module

分形:父组件拆成子组件,完成相同的行为

// module 可以对状态模型进行分层,每个 module 又含有自己的 state、getters、actions 等

// 定义一个 module 基类
class Module {
	constructor(rawModule) {
    this.state = rawModule || {}
    this._rawModule = rawModule
    this._children = {}
  }
  
  getChild (key) {
    return this._children[key]
  }
  
   addChild (key, module) {
    this._children[key] = module
  }
}

// module-collection.js 把 module 收集起来
class ModuleCollection {
  constructor(options = {}) {
    this.register([], options)
  }
  
  register(path, rawModule) {
    const newModule = new Module(rawModule)
    if (path.length === 0 ) {
      // 如果是根模块 将这个模块挂在到根实例上
      this.root = newModule
    }
    else {
      const parent = path.slice(0, -1).reduce((module, key) => {
        return module.getChild(key)
      }, this.root)
      
      parent.addChild(path[path.length - 1], newModule)
    }
    
    // 如果有 modules,开始递归注册一波
    if (rawModule.modules) {
      _.forEach(rawModule.modules, (key, rawChildModule) => {
        this.register(path.concat(key), rawChildModule)
      })
    }
  }
}

// store.js 中
export class Store {
  constructor(options = {}) {
    // 其余代码...
    
    // 所有的 modules 注册进来
    this._modules = new ModuleCollection(options)
    
    // 但是这些 modules 中的 actions, mutations, getters 都没有注册,所以我们原来的方法要重新写一下
    // 递归的去注册一下就行了,这里抽离一个方法出来实现
    installModule(this, this.state, [], this._modules.root);
  }
}

function installModule(store, state, path, root) {
  // getters
  const getters = root._rawModule.getters
  if (getters) {
    _.forEach(getters, (name, getterFn) => {
      Object.defineProperty(store.getters, name, {
        get: () => getterFn(root.state)
      })
    })
  }
  
  // mutations
  const mutations = root._rawModule.mutations
  if (mutations) {
    _.forEach(mutations, (name, mutation) => {
      let _mutations = store.mutations[name] || (store.mutations[name] = [])
      _mutations.push(payload => {
        mutation(root.state, payload)
      })
      
      store.mutations[name] = _mutations
    })
  }
  
  // actions
  const actions = root._rawModule.actions
  if (actions) {
    _.forEach(actions, (name, action) => {
      let _actions = store.actions[name] || (store.actions[name] = [])
      _actions.push(payload => {
        action(store, payload)
      })
      
      store.actions[name] = _actions
    })
  }
  
  // 递归
  _.forEach(root._children, (name, childModule) => {
    installModule(this, this.state, path.concat(name), childModule)
  })
}

Step6 - 插件机制 使用方式

(options.plugins || []).forEach(plugin => plugin(this))

SSR -> 首屏和SEO 友好

CSR VS SSR

浏览器基本渲染原理: image.png

过程如下:

  1. 浏览器通过请求得到一个HTML文本
  2. 渲染进程解析HTML文本,构建DOM
  3. 解析HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(stytle rules),若遇到JavaScript脚本,则会下载执行脚本。 构建样式的时候,会阻塞JS,因为JS可能会改变样式
  4. DOM树和样式规则构建完成之后,渲染进程将两者合并成渲染树(render tree) 到此,浏览器准备好渲染
  5. 渲染进程开始对渲染树进行布局,生成布局树(layout tree
  6. 渲染进程对布局树进行绘制,生成绘制记录
  7. 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成帧
  8. 渲染进程将合成帧信息发送给GPU进程显示到页面中 最后调用显卡,进行绘制

CSR 客户端渲染

一开始只有一个DIV -> 下载并运行 JS -> 框架本身 diff -> 合并数据 -> 调图层 -> 虚拟DOM 上调用 DOM 操作/vue -> 映射到真实的 DOM上

image.png

因此,首屏时间长并且SEO不友好,爬虫爬的都是空DIV

SSR 服务端渲染

框架diff 组装数据 等操作在服务端完成 -> 最终输出 html 的字符串 -> 前端直接渲染

缺点:

  1. 服务端开销较大,尤其是大型项目
  2. 服务端往前端传输的数据量较大
  3. 复杂,同构项目的代码复杂度直线上升,因为要兼容两种环境

同构应用 - 一份代码在服务端和客户端

挂载之后的生命周期都不可以在服务端渲染:服务端不会处理DOM操作,事件等,只会有简单的html轮廓,吐回给前端

水合:将事件等操作合并给拿到的 html,形成交互

水合的好处:更快的渲染了页面

客户端渲染,基本可以这样概括:页面 = 模版 + 数据,应用 = 路由 + 多个页面

模版(<template> + 样式) 数据(data + methods)

  1. 服务端的 webpack 不用关注 CSS,客户端会打包出来的,到时候推 CDN,然后改一下 public path 就好了

  2. 服务端的代码不需要分 chunkNode 基于内存一次性读取反而更高效

  3. 如果有一些方法需要在特定的环境执行,比如客户端环境中上报日志,可以利用 beforeMouted 之后的生命周期都不会在服务端执行这一特点,当然也可以使用 isBrowser 这种判断

  4. CSRSSR 的切换和降级

    // 总有一些奇奇怪怪的场景,比如就只需要 CSR,不需要 SSR
    // 或者在 SSR 渲染的时候出错了,页面最好不要崩溃啊,可以降级成 CSR 渲染,保证页面能够出来
    
    // 互相切换的话,总得有个标识是吧,告诉我用 CSR 还是 SSR
    // search 就不错,/demo?ssr=true
    module.exports = function(req, res) {
      if(req.query.ssr === 'true'){
        const context = { url: req.url }
        renderer.renderToString(context, (err, html) => {
          if(err){
            res.render('demo') // views 文件下的 demo.html
          }
          res.end(html)
        })
      } else {
        res.render('demo')
      }
    }
    
  5. Axios 封装,至少区分环境,在客户端环境是需要做代理的

VUE-SSR 优化方案:

  1. 页面级别的缓存,比如 nginx micro-caching
  2. 设置 serverCacheKey,如果相同,将使用缓存,组件级别的缓存
  3. CGI 缓存,通过 memcache 等,将相同的数据返回缓存一下,注意设置缓存更新机制
  4. 流式传输,但是必须在 asyncData 之后,否则没有数据,说明也可能会被 CGI 耗时阻塞
  5. 分块传输,这样前置的 CGI 完成就会渲染输出,但是这个方案难啊
  6. JSC,就是不用 vue-loader