前言
不知不觉写文章没有以前频繁了,主要是转到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
的显示,那么当我们有多个需要管理的异步数据的时候,就可能会转化成loading
和disable
的行为控制,随着单业务体量的增加,慢慢的,data
和methods
会渐渐的分散,形成礁石代码
。当我们再一次添加迭代功能时,就像是在礁石群中行驶的轮船,可能会发生触礁风险。
那么,问题就回到了标题,对于在option
中的异步代码该怎么去做处理呢?
官方给我们提供了mixins
的混入规则,那么我们可以美滋滋的将其进行混入的时候,这样是不是就解决问题了呢?看上去是的,mixins
很好的解决了我们代码复用的形式,但同时也又新暴露出来了一些问题,由于mixins
的机制,被混入的代码是不可知的。所以说当你使用mixins
的时候就需要注意两边是否都定义了相同的东西,会不会产生覆盖的冲突。
- 代码意外冲突
- 混入不明确
- 心智负担大
那么我们是不是可以思考,在mixins
上面做更多的事情,解决掉mixins
的一些负担的同时能够对其进行改造,这样的话对于代码的管理
和维护
也会有一个不错的效果。
如何解决问题?
在解决问题的时候,我参考了umi
的dva model
,但如果说将数据放在Vuex
中管理其实并不好,所以我还是把mixins
拖出来鞭尸了。 最后的最后,我将mixins
改的面目全非了。最后的结果就是将这些异步数据代码单独拆分出来,形成一个数据访问层
作为处理,在这里我们对数据进行处理,当我们这些数据或者逻辑需要被更改的时候,我们只需要单独的对数据访问层
这个模型进行修改,就可以快速的进行替换逻辑了。
什么是数据访问层?
很多人会发出疑问,文章开头中的数据访问层
是个什么东西,从字面意思上大部分人获取就知道了该层的作用,其实它就是用来和Model
数据模型层沟通的桥梁,也可以看成是controller
中剥离出来的一个行文模式,我们暂且称呼它为“数据访问层
”,我们仅仅需要知道这是一个沟通工具就好了。
- JSON数据获取
- Api数据获取
- 第三方数据接入
- 其他
以上种种都属于数据访问层。这些代码往往和业务逻辑还有后台数据挂钩,做一个承上启下
的桥梁作用。
Mixin的Model设计
既然要对mixins
做出处理,那么就尽量让它的语法更加贴合团队,因此,我们在项目的页面路径下创建一个model.js
来进行数据访问层
的声明,为了考虑对其Api
的学习程度,将其修改为一个类vuex moudule
的Api
来进行的。
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
中的数据最终会被转换为mixin
下data
中创建一个与命名空间名称相同的一个对象,而不是扁平化的平铺在我们页面(view)
层下的data
数据中,这样,我们就只需要关注引入的model
命名空间就可以友好的避免相关的mixins
冲突了,不过相对应的是要更为严谨的对需要reactive
数据有一个理解,使用$set
进行没有在观察范围内的数据。
actions
actions也会进行一层命名空间的包装,它们会被带入到页面(view)
层下的methods
混入,同样的我们时刻会混入一个dispatch
的methods
用来做为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
的参数非常简单,大部分情况下只需要两个参数,后续参数根据业务的逻辑可以自定义一些loading
和toast
,常见的就是类似于在uni-app
下的showLoading
。
type
传入的type
是一个讲究的东西,你需要通过namespace/actionName
的形式传入,来指定你需要调用哪个命名空间下的异步方法。payload
给action
的一个参数,类似于普通的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.js
成mixins
- 为
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
}
源码
最近刚看了工业聚
大佬的一些文章,有一些新的想法。我也把文章贴一下咯
实例代码
<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
真正落地实战还是需要静候一些时间啊。
如果本文对你有用,不妨点个赞支持一下吧。有好的建议也可以在评论区留言哦。