我正在参加「掘金·启航计划」
Vue 中处理页面数据有两种交互方式:
- 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面
- 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管理系统等不太看重用户体验的页面,或者提交数据的场景
本文适用于消息弹窗类页面的加载场景和数据提交的场景。
痛点描述
我们考虑一个简单但经典的提交数据的交互:
- 点击提交按钮时,展示全屏的 loading 遮罩层
- 提交完成时,展示操作成功的提示信息,并跳转到特定页面
- 提交失败时,展示操作失败的错误提示
代码如下:
export default {
methods: {
async submit() {
const loading = this.$loading()
try {
await this.$http.post('/post/add', this.formData)
this.$message.success('操作成功')
this.$router.back()
} catch {
this.$message.error('操作失败')
} finally {
loading.close()
}
},
},
}
其实提交函数本来是很简单的,但为了展示 loading 遮罩和弹出结果提示,让这个函数变得复杂。
export default {
methods: {
async submit() {
await this.$http.post('/post/add', this.formData)
this.$router.back()
},
},
}
其实我们可以根据提交函数的状态来自动展示各种交互界面,这次我们要实现的目标就是自动展示这些界面。看看这样改进后的代码:
export default {
mixins: [
// 加载数据成功时不需要提示
asyncDialog('getData', { success: false }),
// 提交数据是展示完整的提示
asyncDialog('submit'),
],
methods: {
async getData() {
this.tableData = await this.$http.get('/post/list')
},
async submit() {
try {
await this.$http.post('/post/add', this.formData)
this.$router.back()
return '操作成功'
} catch {
throw '操作失败'
}
},
},
}
可以看到,这种方式不但适用于提交,也适用于数据加载。加载函数和提交函数都变得非常纯净,不需要写一大堆界面交互代码,非常舒适。
Mixin 设计
基本用法
我们需要指定一下 methods 中的哪些方法需要展示交互状态,我们只需要为这些指定的方法添加交互状态。根据我之前在文章《我可能发现了Vue Mixin的正确用法——动态Mixin》中的看法,可以使用函数形式的 mixin 来指定。
export default {
mixins: [asyncDialog('submit')],
methods: {
async submit() {
try {
await this.$http.post('/post/add', this.formData)
this.$router.back()
return '操作成功'
} catch {
throw '操作失败'
}
},
}
这会为名为 submit 的方法添加交互,具体包括:
- 在方法执行过程中展示 loading 遮罩
- 在方法执行成功时,展示方法返回的字符串
- 在方法执行失败时,展示方法抛出的字符串
这里解释一下,为什么需要抛出字符串而不是 Error 对象呢?这是为了防止不小心展示了方法中意外抛出的代码错误。只有返回和抛出字符串时,才会展示处理结果。
指定多个方法
也可以用数组指定多个方法名。
export default {
mixins: [asyncDialog(['save', 'submit'])],
}
不展示特定的交互
指定的方法执行时,默认会展示3个交互:加载中、成功提示和失败提示。我们可以传入第二个参数,来指定不展示其中一些交互。比如加载数据的场景,一般会跳过成功提示。
export default {
mixins: [asyncDialog('getData', { success: false })],
}
总结
总结一下,我们需要使用函数形式来实现 mixin,函数接收两个参数,分别用于指定需要添加交互的方法名和指定展示的交互类型。
/**
* @/mixins/async-dialog.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下交互状态:
* - 执行过程中展示 loading 遮罩
* - 执行成功时展示方法返回的字符串
* - 执行失败时展示方法抛出的字符串
* @param {string|string[]} methods 方法名,可指定多个
* @param {DialogSettings} [dialogs] 指定要展示的交互,默认展示所有交互
*
* @typedef DialogSettings
* @type {object}
* @prop {boolean} [loading=true] 是否在执行过程中展示 loading 遮罩
* @prop {boolean} [success=true] 是否在执行成功时展示方法返回的字符串
* @prop {boolean} [error=true] 是否在执行失败时展示方法抛出的字符串
*/
export function asyncDialog(methods, dialogs) {}
函数返回真正的 mixin 对象,为组件中的异步方法展示交互状态。
Mixin 实现
重写方法的时机
我们要重写组件实例方法来添加交互,首先要找准重写方法的时机。我们希望尽可能早进行重写,至少在执行 render 函数之前,以便模板中绑定的是重写后的方法,但又需要在组件方法初始化之后。
所以,你需要熟悉 Vue 的组件渲染流程。在 Vue2 的源码中有这样一段组件初始化代码:
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
// ...
};
而其中的 initState 方法的源码如下:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
所以总结一下 Vue 组件的初始化流程:
- 执行
beforeCreate - 挂载
props - 挂载
methods - 执行并挂载
data - 挂载
computed - 监听
watch - 执行
created
我们必须在 methods 初始化之后才能进行重写,否则方法还没挂载到组件实例上。可以选择的是 data 或者 created。为了尽早重写,我们应该选择在 data 中重写。
// @/mixins/async-dialog.mixin.js
export function asyncDialog(methods, dialogs) {
return {
data() {
// 在这里对方法进行重写
return {}
}
}
}
处理参数
我们把 methods 处理成数组形式,并取出 dialogs 中指定的交互。
// @/mixins/async-dialog.mixin.js
export function asyncDialog(methods, dialogs) {
methods = [].concat(methods)
const { loading = true, success = true, error = true } = dialogs || {}
}
重写方法
// @/mixins/async-dialog.mixin.js
export function asyncDialog(methods, dialogs) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
// 在这里重写方法
this[method] = async (...args) => {
// 打开 loading 遮罩
const loadingMask = loading && this.$loading()
try {
// 调用原始方法
const result = await fn.call(this, ...args)
// 成功时提示
if (success && typeof result === 'string') {
this.$message.success(result)
}
return result
} catch (err) {
// 失败时提示
if (error && typeof err === 'string') {
this.$message.error(err)
}
throw err
} finally {
// 关闭 loading 遮罩
loadingMask?.close()
}
}
}
}
}
}
}
完整代码
最后整合一下完整的代码。
/**
* @/mixins/async-dialog.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下交互状态:
* - 执行过程中展示 loading 遮罩
* - 执行成功时展示方法返回的字符串
* - 执行失败时展示方法抛出的字符串
* @param {string|string[]} methods 方法名,可指定多个
* @param {DialogSettings} [dialogs] 指定要展示的交互,默认展示所有交互
*
* @typedef DialogSettings
* @type {object}
* @prop {boolean} [loading=true] 是否在执行过程中展示 loading 遮罩
* @prop {boolean} [success=true] 是否在执行成功时展示方法返回的字符串
* @prop {boolean} [error=true] 是否在执行失败时展示方法抛出的字符串
*
* @example
* export default {
* mixins: [
* asyncMethodStatus('submit')
* ],
* methods: {
* async submit() {
* try {
* await this.$http.post('/post/add', this.formData)
* this.$router.back()
* return '操作成功'
* } catch {
* throw '操作失败'
* }
* }
* }
* }
*/
export function asyncDialog(methods, dialogs) {
methods = [].concat(methods)
const { loading = true, success = true, error = true } = dialogs || {}
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
this[method] = async (...args) => {
const loadingMask = loading && this.$loading()
try {
const result = await fn.call(this, ...args)
if (success && typeof result === 'string') {
this.$message.success(result)
}
return result
} catch (err) {
if (error && typeof err === 'string') {
this.$message.error(err)
}
throw err
} finally {
loadingMask?.close()
}
}
}
}
return {}
}
}
}