前言
先说下当前普遍的在页面中加载表格数据的方式,常见的数据加载方式是在表格数据未加载前显示一个loading插件,当数据加载完后隐藏掉loading插件,显示已加载完的表格数据。
大概coding如下:
getData(params).then(res => {
this.loading = false;
// 接下来对数据处理
}).catch(err => {
this.loading = false;
})
从上可以看出在coding的时候:
1.我们需要一个可复用的loading组件,可以在各个使用到的页面中用状态控制它的显示与隐藏。-
2.需要在每次获取数据的地方都加上catch判断,隐藏掉loading组件。
这样每次在拉取数据的时候都要加上catch语句,是比较麻烦的,其实我们可以通过自己加上一个webpack loader来自动对这些语句做处理,加上catch函数,执行then函数中的第一句语句。-
所以下文便从这两个角度出发来解决数据加载状态显示的问题~~
编写可复用的loading插件
Vue插件与use方法
要实现全局代码可复用,在vue中一般会使用添加全局方法、全局资源、mixin等,可以封装成插件的形式,插件的功能和范围没有严格的限制,一般有以下几种:
- 添加全局方法或者属性。如: vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如 vue-touch
- 通过全局混入来添加一些组件选项。如 vue-router
- 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
Vue中是通过use方法来使用插件的,Vue.use的源码如下:
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 可以看出Vue.use方法会自动阻止多次注册插件
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// 注入参数,调用插件自定义的install方法
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
由此可见,自定义插件必须提供一个install方法来使自己被添加到Vue中。
自定义指令
由于我最近在跟进的项目是使用了element-ui组件库,里边已经包含了loading插件了,其中便使用了指令的方法,在vue中一般代码复用和抽象的主要形式是组件,但是像loading这种需要对普通dom元素进行底层操作的话(loading的覆盖范围与所使用的dom元素相关), 就需要用到自定义的指令。
// 注册一个自定义指令 v-loading
Vue.directive('loading', {
bind: function(){},
update: function(){},
unbind: function(){}
})
自定义组件的生命周期如下:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind: 只调用一次,指令与元素解绑时调用。
Element的loading指令方式实现:
所以如果想添加一个全局的指令到Vue实例当中,需要loading插件提供一个install方法,在install中注册自定义指令v-loading,在指令的bind生命周期插入loading结点,在update生命周期中更新loading状态,unbind生命周期中销毁loading。
import Vue from 'vue';
import Loading from './loading.vue';
import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom';
import { PopupManager } from 'element-ui/src/utils/popup';
import afterLeave from 'element-ui/src/utils/after-leave';
const Mask = Vue.extend(Loading);
const loadingDirective = {};
// 提供install方法
loadingDirective.install = Vue => {
if (Vue.prototype.$isServer) return;
// 更新loading状态的方法
const toggleLoading = (el, binding) => {
// v-loading="true"时:
if (binding.value) {
// 放入异步更新队列中处理Loading状态更新事件
Vue.nextTick(() => {
if (binding.modifiers.fullscreen) {
// 处理v-loading.fullscreen情况
insertDom(el, el, binding);
} else {
// 处理指令修饰符不含fullscreen的情况
removeClass(el.mask, 'is-fullscreen');
if (binding.modifiers.body) {
// 处理指令修饰符v-loading.body的情况
insertDom(el, el, binding);
} else {
// 处理指令修饰符v-loading的情况
el.originalPosition = getStyle(el, 'position');
insertDom(el, el, binding);
}
}
});
} else {
// 绑定loading隐藏时的回调函数
afterLeave(el.instance, _ => {
//.....
}, 300, true);
el.instance.visible = false;
el.instance.hiding = true;
}
};
const insertDom = (parent, el, binding) => {
// 插入节点的方法....
};
// 钩子函数参数:
// el:指令所绑定的dom元素
// binding: 有关指令的属性,是一个对象,包含name,value,oldValue,expression,arg, modifier
// vnode: vue编译生成的虚拟节点
// oldVnode: 上一个虚拟节点
Vue.directive('loading', {
bind: function(el, binding, vnode) {
const textExr = el.getAttribute('element-loading-text');
const spinnerExr = el.getAttribute('element-loading-spinner');
const backgroundExr = el.getAttribute('element-loading-background');
const customClassExr = el.getAttribute('element-loading-custom-class');
const vm = vnode.context;
const mask = new Mask({
el: document.createElement('div'),
data: {
text: vm && vm[textExr] || textExr,
spinner: vm && vm[spinnerExr] || spinnerExr,
background: vm && vm[backgroundExr] || backgroundExr,
customClass: vm && vm[customClassExr] || customClassExr,
fullscreen: !!binding.modifiers.fullscreen
}
});
el.instance = mask;
el.mask = mask.$el;
el.maskStyle = {};
binding.value && toggleLoading(el, binding);
},
update: function(el, binding) {
el.instance.setText(el.getAttribute('element-loading-text'));
if (binding.oldValue !== binding.value) {
toggleLoading(el, binding);
}
},
unbind: function(el, binding) {
if (el.domInserted) {
el.mask &&
el.mask.parentNode &&
el.mask.parentNode.removeChild(el.mask);
toggleLoading(el, { value: false, modifiers: binding.modifiers });
}
el.instance && el.instance.$destroy();
}
});
};
export default loadingDirective;
ok~到目前为止,我们已经大概了解了loading指令是如何添加到全局Vue中了,接下来就可以直接在需要的组件上添加指令,如下:
<el-table
v-loading="loading"
:data="tableData"
>
<!-- ... -->
</el-table>
编写一个自定义的webpack loader
在组件上添加loading指令后,我们开始来考虑如何自动增加上catch语句
Webpack中有loader和plugin, loader更加专注于文件层次上的内容转换,比如对jsx文件的处理、css预处理等,plugin则不限于文件,可作用的范围也更加广,是用来拓展webpack功能的,比如对文件内容的压缩等。在这个场景,我们主要是对文件内容的增加,所以需要来编写一个自定义的loader.
配置使用
如何配置使用loader?
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: 'babel-loader'
}
]
},
webpack的loader是支持链式调用的.
自定义loader
loader是一个node module,如下:
module.exports = function(source){
//...
return source;
}
传入的source是包含了资源文件的字符串。
由于要实现自动代码注入,所以我们要做的事情大概分为以下这几步:
- 需要先使用parse方法把代码字符串转化为AST抽象语法树,
- 改造AST树:-
2.1 遍历AST树,遍历then函数中的箭头函数。-
2.2 去除箭头函数中的第一句-
2.3 新建一个箭头函数,并赋值上一步取出的语句, 赋给catch函数-
2.4 组合then函数和catch函数,替换原来的then函数。 - 再次使用print方法把抽象语法树转成代码字符串返回。
以下代码主要来自AST与工程化
const recast = require('recast')
const {
identifier: id, // 标志符
memberExpression, // 成员表达式
callExpression, // 函数调用表达式
blockStatement, // 带大括号的表达式序列
arrowFunctionExpression // 箭头函数表达式
} = recast.types.builders
const t = recast.types.namedTypes
module.exports = function (source) {
// parse函数将代码字符串转换为ast树,一般项目会用到babel-loader, 所以解析器也使用recast/parsers/babel
const ast = recast.parse(source, {
parser: require('recast/parsers/babel')
})
// visit遍历
recast.visit(ast, {
visitCallExpression (path) {
const { node } = path
const args = node.arguments
let firstExp
args.forEach(item => {
// 箭头函数并含语句
if (t.ArrowFunctionExpression.check(item) && item.body.body) {
firstExp = item.body.body[0]
// 检查是否为then函数所调用等条件
if (
recast.print(firstExp).code.includes('loading') &&
t.ExpressionStatement.check(firstExp) &&
t.Identifier.check(node.callee.property) &&
node.callee.property.name === 'then'
) {
const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
const originFunc = callExpression(node.callee, node.arguments)
const catchFunc = callExpression(id('catch'), [arrowFunc])
// 组合新的成员表达式
const newFunc = memberExpression(originFunc, catchFunc)
// 替换
path.replace(newFunc)
}
}
})
return false
}
})
// 重新生成代码字符串并返回
return recast.print(ast).code
}
使用自定义的loader
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, '../src/loaders/catch.js')
}
]
}
总结
到这里本文也就结束啦,本文主要介绍了如何在增强交互的同时实现代码可复用性,希望能抛砖引玉,引发小伙伴更多的思考。如果你觉得我的文章对你有所帮助的话,可以点个赞鼓励下嘛(・ω・)ノ
头图来源
本文由 ellila 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Mar 26, 2023 at 12:29 am