懒加载对话框的方式做到了在你调用弹框展示API的那一刻,才去加载对话框文件并最终显示弹框效果。它最大的意义在于减少了你初次进入页面时加载的文件资源请求量和大小(加载资源小了,网页加载速度自然就提升了),将它封装成API顺带也做到了统一管理对话框的目的并且使得对话框文件和页面文件分离,起到了一个模块化的效果。
目前很多前端组件库提供的对话框组件在使用的时候,都是把对话框的代码和页面代码放在一起。例如下面Element官网的例子:
这样一来你的页面如果有不少对话框的话,整个代码看上去就会很杂乱,同时也增大了你当前页面的大小。有人会把对话框抽离出去,用import引进使用的页面。这样仅仅做到了代码的分离,当你页面加载的时候,这部分的弹框代码也会被一同加载,而不管是否用到了这个弹框。下面我们看几组代码示例和效果动态图来一起对比一下,下方对话框的主体代码摘自Element官网示例。
第一组(对话框代码和页面冗在一起):
测试页面代码如下:
点击打开 Dialog
这是一段信息
展示动态效果如下:
我们从响应报文里面看到文件的长度 Content-Length为23772B
第二组(对话框代码和页面代码分离,主体页面通过import引入对话框组件):
(下方仅做了最简单的处理,不建议照笔者所示应用于实际项目)
对话框组件代码如下:
这是一段信息
测试页面代码如下:
点击打开 Dialog
展示动态效果如下:
我们从响应报文里面看到文件的长度 Content-Length为42495B, 代码分离之后最终的文件大小比第一组多了快一倍大小。
第三组(使用封装好的API唤起弹框):
弹框文件代码如下:
这是一段信息
测试页面代码如下:
点击打开 Dialog
展示动态效果如下:
通过控制台的network面板,我们可以直观的看到进入测试页面只加载了页面的文件(Content-Length为18710B),当我们唤起弹框的时候才会去请求弹框的文件(Content-Length为21983B),这说明我们封装的API起到了该有的效果。下面让我们一起来看一下该如何完成这个API的封装。
下面是该API的实现代码(文件名为dialog.js):
import {snake2Camel, camel2Snake} from '@/utils/String'
import Vue from 'vue'
// 获取需要动态创建的弹窗组件
const dialogsContext = require.context('./', true, /@([a-zA-Z\-0-9]+)\.vue$/, 'lazy')
const dialogs = dialogsContext.keys().reduce((views, key) => {
const fileName = key.match(/@([a-zA-Z\-0-9]+)\.vue$/i)[1]
if (!fileName) return views
let componentName = camel2Snake(fileName)
let clsName = snake2Camel(componentName)
return Object.assign(views, {[clsName]: key})
}, {})
async function createDialogAsync (path, data) {
let componentContext = await dialogsContext(path)
let temp = componentContext.default
return new Promise(function (resolve, reject) {
// 初始化配置参数
let opt = {
data
}
let component = Object.assign({}, temp)
// const parent = this
let initData = {
visible: true
}
Object.assign(initData, component.data())
opt.data && Object.assign(initData, JSON.parse(JSON.stringify(opt.data)))
component.data = function () {
return initData
}
// 创建构造器创建实例挂载
let DialogC = Vue.extend(component)
let dialog = new DialogC()
// 关闭事件
let _onClose = dialog.$options.methods.onClose
dialog.onClose = function () {
resolve()
dialog.$destroy()
_onClose && _onClose.call(dialog)
document.body.removeChild(dialog.$el)
}
// 回调事件
let _onCallback = dialog.$options.methods.onCallback
dialog.onCallback = function (...arg) {
try {
_onCallback && _onCallback()
resolve(...arg)
dialog.$destroy()
_onClose && _onClose.call(dialog)
document.body.removeChild(dialog.$el)
} catch (e) {
console.log(e)
}
}
dialog.$mount()
dialog.$watch('visible', function (n, o) {
dialog === false && dialog.onClose()
})
document.body.appendChild(dialog.$el)
})
}
function init (values) {
let dialogComponents = {}
if (!values) return
Object.keys(values).forEach((name) => {
dialogComponents[name] = function (data) {
return createDialogAsync.call(this, values[name], data)
}
})
return dialogComponents
}
Vue.prototype.$dialog = init(dialogs)
snake2Camel, camel2Snake是两个工具函数,用作字符串的格式转化,分别是蛇形转驼峰,驼峰转蛇形。代码如下:
function snake2Camel (str, capLower) {
let s = str.replace(/[-_](\w)/g, function (x) {
return x.slice(1).toUpperCase()
})
s = s.replace(/^\w/, function (x) {
return capLower ? x.toLowerCase() : x.toUpperCase()
})
return s
}
function camel2Snake (str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
重头戏来了,我们开始解析dialog.js的代码。 要完成懒加载文件需要借助require.context,不熟悉这个的朋友可以去找相关资料看看require的一些常用功能。require.context 的第三个入参决定了是否加载懒加载文件。拿到了文件之后,我们还需要初始化弹框,并把他显示出来。这里涉及到了Vue的两个API,extend和$mount的知识(不熟悉的朋友可以去Vue官网的API部分查看了解一下)。我们用Vue.extend从require.context获取的文件内容里面创建一个组件构造器,通过new构造器得到一个对话框的组件实例,再调用实例的$mount方法把它变成未挂载状态。最后我们通过document.body.appendChild方法把该未挂载的实例挂载到了body节点上,这样一来我们就完成了对话框文件的加载以及最后的对话框展示效果了。
理清楚了基本的操作之后,我们还需要取消对话框的回调方法,有时候我们还会在对话框结束之后向页面回调一些信息,为了完成这个共功能,这里在我们的对话框实例上绑定了onClose和onCallback方法。并且用Promise封装了弹框实例化的相关步骤,这都是为了顺利的向调用方传递回调结果,并可以做到流程的控制。需要注意的一点就是,当取消对话框的时候,我们把对话框用removeChild方法从body节点移除还需要手动调用对话框实例的$destroy()销毁实例,这样就完成最后的垃圾清理。
讲完了最核心的内容之后。我们来看最终实现的API处理流程:
第一步:我们的API会从require.context传入的文件地址里面去遍历获取以@开头,.vue结尾的文件上下文。
第二步:遍历这些上下文,获取一个dialogs映射对象。文件名除去@和.vue部分余下的字符串经过转化,最后以驼峰形式作为映射对象的key,而上下文的具体内容则成为了对应的value值。
第三步:编写createDialogAsync方法,从上下文内容中生成弹窗并进行最终的挂载展示。核心原理部分上文有讲到,这里不再重复。
第四步:编写init方法,把dialogs映射对象的所有上下文都绑上createDialogAsync,并返回一个新的dialogComponents
映射对象。dialogComponents的key和dialogs的key一样,只不过value换成了createDialogAsync函数。
第五步: 把init(dialogs)的结果绑定到Vue原型对象上,例如Vue.prototype.$dialog。这样我们就可以在任意组件内通过this.$dialog.XXXX()来调用弹框了。其中XXX是弹框文件的驼峰形式。
现在我们已经完成了懒加载对话框API的封装了。这里对弹框文件的文件名有命名的要求,必须是要以@开头,.vue结尾。如果你想自定义命名规范那么请同步修改require.context('./', true, /@([a-zA-Z\-0-9]+)\.vue$/, 'lazy')中的第三个参数,否则将会找不到弹框文件。下面我举例几个弹框文件的文件名和调用的方式:
1: 文件名:@confirm.vue
调用方式1: this.$dialog.Confrim()
调用方式2: await this.$dialog.Confirm() 或者 let ret = await this.$dialog.Confirm(); if (ret) xxxxxx
调用方式3: this.$dialog.Confrim().then((info) => {console.log(info)})
2: 文件名:@add-user.vue
调用方式1: this.$dialog.AddUser()
调用方式2: await this.$dialog.AddUser() 或者 let ret = await this.$dialog.AddUser(); if (ret) xxxxxx
调用方式3: this.$dialog.AddUser().then((info) => {console.log(info)})
到这里为止,懒加载对话框API的封装技巧的讲解就结束了。如有任何疑问,可与下方留言交流。