如何在Vue中使用Mixin来管理异步数据的实践思考

4,338 阅读4分钟

前言

不知不觉写文章没有以前频繁了,主要是转到React后,花费了一些精力从使用React到搞懂,再加上花了大价钱入手的红宝书不能不看所以说这个月除了工作时间都是在看红宝书计划。先沉淀下自己。

本文内容仅供参考,本方案也是一个不成熟的考究,欢迎朋友们给我一些优化的建议。

异步数据的问题

为什么会有一个异步数据的管理解决方案?我们为什么要去进行管理它?大家都知道在很久之前,我们Api调用方法还写在页面上面的时候,你自己都可以想象到这个方式是有多糟糕了。随之而在的就是nwtwork拆分,大家都有条不紊的将api的接口拆分出去,有单独写成函数文件的,有直接做约定式的,大体上不变的就是剔除了页面上的this.$http.get.....等等方法。

但是,我们的问题解决了吗?

事实上并没有,在最近浏览一些开源项目的时候,我们都可以频繁的看到如下类似的业务代码

<template>
	<view>demo</view>
</template>

<script>
import { couponListApi } from '@/api'
export default {
	data: () => ({
		viewLoading: false, // 页面加载loading事件
		buttonLoading: false, // 页面点击loading & disable事件
	}),
	methods: {

		// 获取优惠券列表
		getCouponSyncList() {
			try {
				this.viewLoading = true
				const data = await couponListApi()
				if (data) {
					// ...todo
				}
			} finally {
				this.viewLoading = false
			}
		}
	}
}
</script>

基本逻辑其实就是请求一个列表数据,这个过程中会触发viewLoading的更新,更新完了后隐藏loading的显示,那么当我们有多个需要管理的异步数据的时候,就可能会转化成loadingdisable的行为控制,随着单业务体量的增加,慢慢的,datamethods会渐渐的分散,形成礁石代码。当我们再一次添加迭代功能时,就像是在礁石群中行驶的轮船,可能会发生触礁风险。

那么,问题就回到了标题,对于在option中的异步代码该怎么去做处理呢?

官方给我们提供了mixins的混入规则,那么我们可以美滋滋的将其进行混入的时候,这样是不是就解决问题了呢?看上去是的,mixins很好的解决了我们代码复用的形式,但同时也又新暴露出来了一些问题,由于mixins的机制,被混入的代码是不可知的。所以说当你使用mixins的时候就需要注意两边是否都定义了相同的东西,会不会产生覆盖的冲突。

  • 代码意外冲突
  • 混入不明确
  • 心智负担大

那么我们是不是可以思考,在mixins上面做更多的事情,解决掉mixins的一些负担的同时能够对其进行改造,这样的话对于代码的管理维护也会有一个不错的效果。

如何解决问题?

在解决问题的时候,我参考了umidva model,但如果说将数据放在Vuex中管理其实并不好,所以我还是把mixins拖出来鞭尸了。 最后的最后,我将mixins改的面目全非了。最后的结果就是将这些异步数据代码单独拆分出来,形成一个数据访问层作为处理,在这里我们对数据进行处理,当我们这些数据或者逻辑需要被更改的时候,我们只需要单独的对数据访问层这个模型进行修改,就可以快速的进行替换逻辑了。

什么是数据访问层?

很多人会发出疑问,文章开头中的数据访问层是个什么东西,从字面意思上大部分人获取就知道了该层的作用,其实它就是用来和Model数据模型层沟通的桥梁,也可以看成是controller中剥离出来的一个行文模式,我们暂且称呼它为“数据访问层”,我们仅仅需要知道这是一个沟通工具就好了。

  • JSON数据获取
  • Api数据获取
  • 第三方数据接入
  • 其他

以上种种都属于数据访问层。这些代码往往和业务逻辑还有后台数据挂钩,做一个承上启下的桥梁作用。

Mixin的Model设计

既然要对mixins做出处理,那么就尽量让它的语法更加贴合团队,因此,我们在项目的页面路径下创建一个model.js来进行数据访问层的声明,为了考虑对其Api的学习程度,将其修改为一个类vuex mouduleApi来进行的。

example

export default {
  namespace: 'test',
  state: {
    move: {},
    a: 1,
    b: 2,
  },
  actions: {
    /**
     * 获取当前用户身份信息
     * @param { Object extends VueData } state 
     * @param { Object } payload  
     * @param { function } set  
     */
    async getUser(state, { payload, set }) {
      const data = await test()
      await set(state, 'move', data)
      return state.move
    }
  }
}

state

当前模块必须指定命名空间,state中的数据最终会被转换为mixindata中创建一个与命名空间名称相同的一个对象,而不是扁平化的平铺在我们页面(view)层下的data数据中,这样,我们就只需要关注引入的model命名空间就可以友好的避免相关的mixins冲突了,不过相对应的是要更为严谨的对需要reactive数据有一个理解,使用$set进行没有在观察范围内的数据。

actions

actions也会进行一层命名空间的包装,它们会被带入到页面(view)层下的methods混入,同样的我们时刻会混入一个dispatchmethods用来做为actions的入口,被混入的methods已经被套上了命名空间的标识,避免产生相同的方法导致被替换。

这个时候action有几个参数:

  • state 当前命名空间的$data数据,可以通过它直接对state进行赋值
  • option.payload dispatch传入的参数对象,可以拿到从dispatch传入的对象。
  • option.set 将当前实例下的$set传入过来,和this.$set同等,两者都可以使用。

dispatch

dispatch作为所有的mixin methods执行的入口,承担的意义不仅仅是解析混入的mixins model命名空间,它话需要承载全大部分的统一处理,依旧是上述的实例,我们每一个action执行后其实都会改变对应的model action状态,这个状态可以在$data中的model变量下查看。

this.dispatch("test/setUser", { 
	user: "1111" 
}, true).then((res) => {
	console.log(res); 
});

dispatch的参数非常简单,大部分情况下只需要两个参数,后续参数根据业务的逻辑可以自定义一些loadingtoast,常见的就是类似于在uni-app下的showLoading

  • type 传入的type是一个讲究的东西,你需要通过namespace/actionName的形式传入,来指定你需要调用哪个命名空间下的异步方法。
  • payloadaction的一个参数,类似于普通的argument

执行效果

如何注入

引入Model混入

import { createModel } from "@/plugin/controller";

引入对应的model

export default createModel({
    name: "componentName",
    methods: {
      getUserData() {
        this.dispatch("test/setUser", { 
			user: "1111" 
		}, true).then((res) => {
          console.log(res);
        });
      },
    },
  },["test", "index"]
);

createModel做了什么?

  • 获取当前目录下的model.js
  • 解析和包装model.jsmixins
  • component注入或者合并解析后的mixins
  • 被vue解析产生混合的组件

拆解代码

代码的话统一写在了一个GenerateModel类,通过这个类可以对model进行统一的处理。

查找所有的model.js

getCurrentModel获取当前编写的一个实例model存放起来,这个方法的一个作用就是通过webpack来抽调所有pages下的model.js,从而过滤出我们需要的model.js将其存放在resultStock中间。

getCurrentModel() {
    const context = require.context('../../pages', true, /model.js$/)
    context.keys().forEach(key => {
      const currentModel = context(key).default
      if (this.modelNames.indexOf(currentModel.namespace) !== -1) {
        this.resultStock.push(currentModel)
      }
    })
  }

伪装mixins

通过generateModelMixinData方法,可以将存放在resultStock的数据依次转换成约定的格式,进行命名混淆。

generateModelMixinData() {
    const mixinQueue = []
    if (this.resultStock.length > 0 ) {
      this.resultStock.forEach(model => {
        const transformMethods = {}
        const transformLoadingEffect = {}
        const { namespace, state = {}, actions = {} } = model
        Object.keys(actions).forEach(fun => {
          transformMethods[`${namespace}__${fun}`] = actions[fun]
          transformLoadingEffect[`${namespace}/${fun}`] =  false
        })
        mixinQueue.push({
          data: () => ({
            model: {...transformLoadingEffect},
            [namespace]: { ...state }
          }),
          methods: {
            ...transformMethods,
            dispatch: this.dispatch
          }
        })
      })
    } else {
      this.warning('没有找到mdoel数据')
    }
    return mixinQueue
  }

dispatch调用

dispatch中,对于其本身来说,主动暴露出一个Promise能够在方法的前后注入一些业务逻辑,如比较常见的async methods的状态更新,loading的加载和隐藏。将我们托管在dispatch的参数交托给我们的action

dispatch  (modelCursor, payload, loading = false) {
    const [ namespace, actionName ] = modelCursor.split('/')
    return new Promise((success, fail) => {
      loading && uni.showLoading({
        title: 'loading'
      })
      this.$set(this.model, `${namespace}/${actionName}`, true)
      this[`${namespace}__${actionName}`](this[namespace], {
        payload,
        set: this.$set
      }).then(res => success(res)).catch(err => fail(err)).finally(() => {
        loading && uni.hideLoading()
        this.$set(this.model, `${namespace}/${actionName}`, false)
      })
    })
  }

注入组件

createModel最终会和component中的mixin合并,合并后成为新的一个component进行服务,所以是可以和mixins共存的。

export function createModel (components, names = []) {
  const createSetup = new GenerateModel(names)
  createSetup.getCurrentModel()
  const transFormModel = createSetup.generateModelMixinData()
  if (components?.mixins) {
    const preMixin = components.mixins
    components.mixins = transFormModel.concat(preMixin)
  }
  components.mixins = [...transFormModel]
  console.log(components)
  return components
}

源码

最近刚看了工业聚大佬的一些文章,有一些新的想法。我也把文章贴一下咯

面向 Model 编程的前端架构设计 @工业聚

实例代码

<template>
  <view class="content">
    <view>
      <!-- <u-button type="error">危险按钮</u-button> -->
      <text class="title">{{ JSON.stringify(test.move) }}</text>
      <u-button
        type="error"
        :disabled="model['test/setUser']"
        @click="getUserData"
        >获取个人信息</u-button
      >
    </view>
  </view>
</template>

<script>
import { createModel } from "../../plugin/controller";
export default createModel(
  {
    name: "test",
    methods: {
      getUserData() {
        this.dispatch("test/setUser", { 
					user: "1111" 
				}, true).then((res) => {
          console.log(res);
        });
      },
    },
  },
  ["test", "index"]
);
</script>

源码

class GenerateModel {
  constructor (modelNames) {
    this.modelNames = modelNames
    this.resultStock = []
  }

  getCurrentModel() {
    const context = require.context('../../pages', true, /model.js$/)
    context.keys().forEach(key => {
      const currentModel = context(key).default
      if (this.modelNames.indexOf(currentModel.namespace) !== -1) {
        this.resultStock.push(currentModel)
      }
    })
  }

  generateModelMixinData() {
    const mixinQueue = []
    if (this.resultStock.length > 0 ) {
      this.resultStock.forEach(model => {
        const transformMethods = {}
        const transformLoadingEffect = {}
        const { namespace, state = {}, actions = {} } = model
        Object.keys(actions).forEach(fun => {
          transformMethods[`${namespace}__${fun}`] = actions[fun]
          transformLoadingEffect[`${namespace}/${fun}`] =  false
        })
        mixinQueue.push({
          data: () => ({
            model: {...transformLoadingEffect},
            [namespace]: { ...state }
          }),
          methods: {
            ...transformMethods,
            dispatch: this.dispatch
          }
        })
      })
    } else {
      this.warning('没有找到mdoel数据')
    }
    return mixinQueue
  }

    dispatch  (modelCursor, payload, loading = false) {
      const [ namespace, actionName ] = modelCursor.split('/')
      return new Promise((success, fail) => {
        loading && uni.showLoading({
          title: 'loading'
        })
        this.$set(this.model, `${namespace}/${actionName}`, true)
        this[`${namespace}__${actionName}`](this[namespace], {
          payload,
          set: this.$set
        }).then(res => success(res)).catch(err => fail(err)).finally(() => {
          loading && uni.hideLoading()
          this.$set(this.model, `${namespace}/${actionName}`, false)
        })
      })
    }

  warning (message) {
    console.warn('model.js: ', message)
  }
}

export function createModel (components, names = []) {
  const createSetup = new GenerateModel(names)
  createSetup.getCurrentModel()
  const transFormModel = createSetup.generateModelMixinData()
  if (components?.mixins) {
    const preMixin = components.mixins
    components.mixins = transFormModel.concat(preMixin)
  }
  components.mixins = [...transFormModel]
  console.log(components)
  return components
}

思考和总结

思考仅仅是一种思考,这只是一种不成熟的方案。其中的问题非常的显而易见,不论是开销还是汇报比例来说都处于一个有待考究的层面上,在之前我是比较不喜欢mixin混入的形式的,但是任何一个api的提出都有其用意,而我们能做的就是思考api的灵活性来达到我们自身的业务需求,技术最终是要服务于业务的。在这里其实就是做一个唠叨话,当我们的业务足够庞大的时候,如果项目的清晰度不够,那么后续维护起来将会是一个非常糟糕的问题。

也看了大多数文章都不推荐mixins进行代码混入,本文实践属于在一个折腾中的东西。所希望的是将异步代码从我们的vue页面和组件中拆分出来,形成一个单独的逻辑层,而不是和主视图堆放在一起。那么在不相互之间产生影响的同时快速定位代码的位置进行统一更改。

现在大部分人都在追Vue3,但是距离Vue3真正落地实战还是需要静候一些时间啊。

如果本文对你有用,不妨点个赞支持一下吧。有好的建议也可以在评论区留言哦。