作为从事前端开发的你肯定见过不少的弹框组件,你可曾有想过要自己实现一个弹框组件库,又或者想完全定制化的使用各种标准UI框架中的弹框组件呢?
今天这篇文章将会带着你解析这一系列疑问,以vant-weapp组件库为例,从开发标准的弹窗组件使用到高度定制复合自我审美的弹窗,再到完全研究清楚vant-weapp框架弹窗组件部分源码。
vant-weapp组件库是有赞团队开发的 一款灵活简洁且美观的小程序UI组件库 ,此文将以这个组件库的用法为标准,下文提及的弹框组件均指的是此组件库中的弹框。
vant-weapp中弹框主要分为**两大类:弹出层Popup和对话框Dialog,**弹出层一般是带有背景遮罩层和内容展示区域用于在不跳转页面情况下进行详情的展示作用,对话框多数用于带有详情展示的同时还带有希望用户确认等操作。如下图所示,图左为典型的Dialog,图右为典型的Popup。
在使用弹框组件之前记得在小程序的app.json文件中先注册组件,详细介绍见 快速上手 ,例如注册van-popup组件代码如下:
// app.json "usingComponents": { "van-popup": "path/to/@vant/weapp/dist/popup/index" } 复制代码
在项目中实际使用如下:
在本文后续分析van-dialog源码中会发现在dialog的index.json中也定义过van-popup组件,但是我们要直接实行van-popup组件必须在小程序的配置文件app.json中按照上图方式进行定义,微信小程序官网说明过 自定义组件内部的引入组件只在该组件内生效
注册完组件之后,就可以直接在小程序页面中使用这里注册的自定义组件,组件名称为这里 key ,例如:。
最常见的用法就是直接使用van-popup组件,通过组件的show属性来控制其是否展示,组件内部嵌套的其他组件或标签是popup组件的内容,如下所示:
// wxml <button bindtap="showPopup">展示弹出层</button> <van-popup show="{{ show }}" position="top" bind:close="onClose" closeable >内容</van-popup> // js Page({ data: { show: false }, showPopup() { this.setData({ show: true }); }, onClose() { this.setData({ show: false }); } }); 复制代码
van-popup组件可以通过position属性的五个值: center、top、right、bottom、left 来快捷的控制是从哪个位置弹出,例如:上例中的弹框从上往下弹出
可以通过round属性来控制弹窗内容是否显示圆角,closeable可以决定是否显示关闭弹框的图标按钮,例如:上例中的弹窗将不显示圆角,同时显示关闭按钮
各种基本的弹窗形式如下:
对话框则是在popup弹出层的基础上添加了额外的内置的标题,快速确定按钮等组件,用于消息提示、消息确认等场景,下面看看其常见用法。
最常规的用法就是直接使用van-dialog组件,通过组件的show属性来控制其是否展示,组件内部嵌套的其他组件或标签是dialog组件的内容,如下所示:
// wxml <van-dialog title="标题" message="代码是写出来给人看的,附带能在机器上运行" show="{{ show }}" confirm-button-open-type="getUserInfo" bind:close="onClose" bind:getuserinfo="getUserInfo" > <image src="https://img.yzcdn.cn/1.jpg" /> </van-dialog> // js Page({ data: { show: true }, getUserInfo(event) { console.log(event.detail); }, onClose() { this.setData({ close: false }); } }); 复制代码
直接使用van-dialog组件,通过组件的show属性来控制其是否展示,组件内部嵌套的其他组件或标签是dialog组件的内容,不使用use-title-slot且不传递title属性,如下所示:
// wxml <van-dialog show="{{ show }}" confirm-button-open-type="getUserInfo" bind:close="onClose" bind:getuserinfo="getUserInfo" > <view class="message">代码是写出来给人看的,附带能在机器上运行</view> </van-dialog> // js Page({ data: { show: true }, getUserInfo(event) { console.log(event.detail); }, onClose() { this.setData({ close: false }); } }); 复制代码
上述两种用法中的use-slot属性表示使用默认的slot(即van-dialog嵌套的wxml内容,比如此处的
最常规的另一种用法就是直接使用 Dialog、Dialog.alert、Dialog.confirm 的方法快速打开弹窗组件,关闭弹框组件则通过 Dialog.close ,取消弹框的加载状态则使用 Dialog.stopLoading,组件内部嵌套的其他组件或标签是dialog组件的内容,如下所示:
// wxml <van-dialog id="van-dialog"> import Dialog from 'path/to/@vant/weapp/dist/dialog/dialog'; // js Dialog.alert({ title: "标题" message: '代码是写出来给人看的,附带能在机器上运行' }).then(() => { // on close }); 复制代码
这里使用函数调用一定要注意在使用van-dialog的页面的wxml中一定需要写这个来使用组件,下文在分析dialog的源码中会讲到(卖个关子),或者你可以先猜一猜:blush::blush:
上面三种van-dialog的常规使用方法的效果如下:
下面将会提供几个作者在实战中写出的Dialog对话框组件的实战用法
<van-dialog id="van-dialog" show="{{ dialogShow }}" message="资质原件拍照或扫描可以不加盖公章,复印件需盖章\n\n如是三证合一,则无需提供税务登记证、组织机构代码证" message-align="left" confirm-button-text="知道了" confirm-button-color="#EE712F" use-title-slot > <view slot="title" class=" merchant-dialog__title"> <view class="merchant-dialog__title-text">**前,请准备以下资料</view> <van-icon name="cross" size="40rpx" class="merchant-dialog__title-icon" bindtap="closeDialog" /> </view> </van-dialog> // 样式部分的代码此处省略 复制代码
触发弹框显示
handleButtonClick1: function () { this.setData({ dialogShow: true }) }, 复制代码
此例子如要使用了如下特性:
use-title-slot confirm-button-text、confirm-button-color van-icon
对应的效果如下:
<van-dialog id="van-dialog-2" use-slot use-title-slot > <view slot="title" style="padding-bottom: 10px;"> <van-icon name="close" color="#fff" size="30" bindtap="closeDialog2" /> </view> <image class="image" src="https://tva1.sinaimg.cn/large/0082zybply1gbylbcwm44j30rs13bdsg.jpg" mode="aspectFit"></image> </van-dialog> 复制代码
通过触发弹框显示
handleButtonClick2: function () { Dialog({ selector: '#van-dialog-2', showConfirmButton: false, closeOnClickOverlay: false, className: 'dialog2', width: '260px' }) }, 复制代码
此例子如要使用了如下特性:
对应效果如下:
<van-dialog id="van-dialog-3" use-title-slot > <view slot="title" style="color: #000;">提示</view> <view> <view>为了给你推荐更合适的漫展~</view> <view>请开启定位权限~</view> </view> </van-dialog> 复制代码
通过触发弹框显示
handleButtonClick3: function () { Dialog({ selector: '#van-dialog-3', showCancelButton: true, cancelButtonTrext: '取消', confirmButtonText: '去设置', cancelButtonColor: '#C46B85', confirmButtonColor: '#C46B85', message: '为了给你推荐更合适的漫展~\n请开启定位权限~', confirmButtonOpenType: 'openSetting', width: '260px', className: 'dialog3' }) }, 复制代码
外部样式类
.dialog-index--dialog3 { --dialog-background-color: rgba(255,255,255,0.8); --popup-background-color: rgba(255,255,255,0.8); --button-default-background-color: transparent; color: #666; } 复制代码
此例子如要使用了如下特性:
cancelButtonColor、confirmButtonColor --dialog-background-color
对应效果如下:
如果你仔细看过上面中的三种自定义方式的实现代码应该也可以根据UI需求实现自己的弹窗交互效果;这里我已经基于前面提到的三种用法来开发了几个实际场景中的弹框组件:
这部分的可以直接去看源码 github.com/JohnieXu/va…
也可以扫码这个小程序二维码查看效果
在看完上面几种炫酷的弹框效果后,我们还是按照惯例研究下如此强大的弹框组件的源码。在研究弹框部分源码之前有必有分析一下一套完整UI框架所需要注意的框架级别的整体架构
处理样式是所有UI框架比不可忽略的核心逻辑之一,在vant-weapp中对样式的处理主要分为以下三部分;源码对应结构如下图所示,使用less的mixins复用实现主题变量控制、公共样式抽离等。
在var.less文件定义了框架所用到的全部的样式控制相关的变量,其中与弹框相关的部分源码如下:
// Dialog @dialog-width: 320px; @dialog-small-screen-width: 90%; @dialog-font-size: @font-size-lg; @dialog-border-radius: 16px; @dialog-background-color: @white; @dialog-header-font-weight: @font-weight-bold; @dialog-header-line-height: 24px; @dialog-header-padding-top: @padding-lg; @dialog-header-isolated-padding: @padding-lg 0; @dialog-message-padding: @padding-lg; @dialog-message-font-size: @font-size-md; @dialog-message-line-height: 20px; @dialog-message-max-height: 60vh; @dialog-has-title-message-text-color: @gray-7; @dialog-has-title-message-padding-top: @padding-sm; 复制代码
源码: var.less
此文件中的最终会转换成 css变量 ,并非像antd、iview等网页端框架中的样式处理那样编译成变量指向的值。根据css变量作用域的特性,可以在自定义组件的外部样式类中局部覆盖样式变量来改变组件内部的样式。
像清除浮动、文字省略、1像素边框等通用的样式类的处理在mixin文件夹下
.clearfix() { &::after { display: table; clear: both; content: ''; } } 复制代码
使用常见的after伪类来实现清除浮动
.multi-ellipsis(@lines) { display: -webkit-box; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: @lines; /* autoprefixer: ignore next */ -webkit-box-orient: vertical; } .ellipsis() { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } 复制代码
使用less的函数封装了两个处理文字省略方法:单行省略、多行省略
微信小程序官方提供了 Component构造方法 注册自定义组件,为了结合typescript给自定义组件提供更灵活强大的组件注册器对Component进行了下面的功能封装处理
function VantComponent<Data, Props, Methods>( vantOptions: VantComponentOptions< Data, Props, Methods, CombinedComponentInstance<Data, Props, Methods> > = {} ): void { const options: any = {}; mapKeys(vantOptions, options, { data: 'data', props: 'properties', mixins: 'behaviors', methods: 'methods', beforeCreate: 'created', created: 'attached', mounted: 'ready', relations: 'relations', destroyed: 'detached', classes: 'externalClasses' }); const { relation } = vantOptions; if (relation) { makeRelation(options, vantOptions, relation); } // 给所有组件添加默认外部样式类custom-class options.externalClasses = options.externalClasses || []; options.externalClasses.push('custom-class'); // 给所有组件添加默认behaviors options.behaviors = options.behaviors || []; options.behaviors.push(basic); // map field to form-field behavior if (vantOptions.field) { options.behaviors.push('wx://form-field'); } // 默认启用多slot支持、组件中允许全局样式修改 options.options = { multipleSlots: true, addGlobalClass: true }; // 最终使用官网构造方法构造组件 Component(options); } 复制代码
源码: component.ts
behaviors 是微信小程序官方用于组件复用 data、methods 等属性方法的一种方式,和vue中的 mixins 小作用一致,vant-weapp中定义的 mixins 如下图所示:
其中basic是所有自定义组件都复用的一个mxin,给所有自定义的组件提供了三个方法: $emit、 set 和 getRect 。
源码如下:
// basic.ts export const basic = Behavior({ methods: { $emit(...args) { this.triggerEvent(...args); }, set(data: object, callback: Function) { this.setData(data, callback); return new Promise(resolve => wx.nextTick(resolve)); }, getRect(selector: string, all: boolean) { return new Promise(resolve => { wx.createSelectorQuery() .in(this)[all ? 'selectAll' : 'select'](selector) .boundingClientRect(rect => { if (all && Array.isArray(rect) && rect.length) { resolve(rect); } if (!all && rect) { resolve(rect); } }) .exec(); }); } } }); 复制代码
源码: basic.ts
其实生命周期如何命名到不是很重要,vant-weapp对命名进行了转换主要基于以下两个原因:
function mapKeys(source: object, target: object, map: object) { Object.keys(map).forEach(key => { if (source[key]) { target[map[key]] = source[key]; } }); } mapKeys(vantOptions, options, { data: 'data', props: 'properties', mixins: 'behaviors', methods: 'methods', beforeCreate: 'created', created: 'attached', mounted: 'ready', relations: 'relations', destroyed: 'detached', classes: 'externalClasses' }); 复制代码
源码: component.ts#L24
通过 mapKeys 方法对 VantComponent 中传入的生命周期函数进行了转换,转换名生命周期名称与微信小程序一致
微信小程序自定义组件默认样式作用域的范围是为当前组件,也就是说组件文件夹下的wxss中的样式只对该文件夹下的wxml生效(除去标签名、ID选择器)
这种以组件为单位进行样式隔离的模式类似于React框架中处理的组件样式的库 styled-components
要在组件之前共享样式或者让自定义组件接受外部样式,可行方案有如下几种:
| styleIsolation属性配置 |
使用vant-weapp组件库的使用者最佳的自定义组件样式的方式是: 采用外部样式类+CSS变量,在无相关CSS变量时才用自己的样式+ !important 确保样式优先级 ,在自定义组件中使用vant-weapp的组件时候的注意事项参照 样式覆盖 。
自定义组件通信主要包括 组件参数传递 和 事件监听 ,这两个功能都是微信小程序官网提供的;参数传递是由父传到子的单向传递,而事件监听则是相应原生事件或者自定义事件。自定义事件用于对组件的事件进行封装,自定义事件机制如下:
这里在van-dialog组件使用位置监听bindclick事件,最终这个事件会在van-dialog组件内部的button的tap时被触发,后面源码分析中的自定义组件的自定义事件全部采用的此种模式。
popup组件部分源码结构如下:
组件的命名规范与微信小程序自定义组件的规范相符合(README.md为组件的使用说明文档,用于生成官网的组件文档说明)。
popup组件的配置文件标识当前的index为组件,通过 using-components 引入了 van-icon 和 van-overlay 组件,在对应的wxml中可以直接使用。
弹出层组件主要分类 遮盖层 和 内容层 ,内容层嵌套在遮盖层内部来确保视觉上覆盖在遮盖层之上。
遮盖层通过overlay、overlayStyle等组件属性来控制其是否显示以及遮盖层的样式等,遮盖的事件有 onClickOverlay ,通过$emit触发组件的自定义事件close。
onClickOverlay() { this.$emit('click-overlay'); if (this.data.closeOnClickOverlay) { this.$emit('close'); } } 复制代码
通过closable属性决定是否显示默认的关闭按钮,也可以通过关闭图标相关属性配置更改按钮样式,关闭按钮的事件有onClickCloseIcon,通过$emit触发组件的自定义事件close。
onClickCloseIcon() { this.$emit('close'); }, 复制代码
接受一个默认的slot,其位置根据传入的 position 参数不同有 top、right、bottom、left、center 五种,根据这五种位置参数有对应的五种不同的弹出位置和动画
使用transform来实现动画效果,根据 position 参数的五种情况有五种默认动画
// popup/index.less .van-bottom-enter, .van-bottom-leave-to { transform: translate3d(0, 100%, 0); } .van-top-enter, .van-top-leave-to { transform: translate3d(0, -100%, 0); } .van-left-enter, .van-left-leave-to { transform: translate3d(-100%, -50%, 0); } .van-right-enter, .van-right-leave-to { transform: translate3d(100%, -50%, 0); } 复制代码
同时暴露了外部样式类可以用来自定义动画,这里动画阶段划分和vue相同,分类: enter、enter-active、enter-to、leave、leave-active、leave-to
// popup/index.ts VantComponent({ classes: [ 'enter-class', 'enter-active-class', 'enter-to-class', 'leave-class', 'leave-active-class', 'leave-to-class' ], ... } 复制代码
dialog组件部分源码结构如下:
结构同popup组件,不同点在于index.json使用了 van-popup、van-button 组件,以及多了dialog.ts这个暴露API函数调用方法的文件。
dialog组件整体基于popup组件,在其默认slot中添加了顶部标题的slot和按钮组元素,大致结构如下
源码结构:
// dialog/index.wxml <van-popup show="{{ show }}" ... > <view wx:if="{{ title || useTitleSlot }}" class="van-dialog__header {{ message || useSlot ? '' : 'van-dialog--isolated' }}" > <slot wx:if="{{ useTitleSlot }}" name="title" /> <block wx:elif="{{ title }}"> {{ title }}</block> </view> <slot wx:if="{{ useSlot }}" /> <view wx:elif="{{ message }}" class="van-dialog__message {{ title ? 'van-dialog__message--has-title' : '' }} {{ messageAlign ? 'van-dialog__message--' + messageAlign : '' }}" > <text class="van-dialog__message-text">{{ message }}</text> </view> <view class="van-hairline--top van-dialog__footer"> <van-button wx:if="{{ showCancelButton }}" ... > {{ cancelButtonText }} </van-button> <van-button wx:if="{{ showConfirmButton }}" ... > {{ confirmButtonText }} </van-button> </view> </van-popup> 复制代码
在前面中通过Dialog函数调用来打开弹出框组件,实现函数式调用的核心思路主要是: 通过selectComponent(selector)方法查找(类似于查找DOM、Vue中查找组件实例)对页面中定义渲染好的dialog组件,手动更新其组件实例的数据。 ** Dialog方法定义如下:
const Dialog: Dialog = options => { options = { ...Dialog.currentOptions, ...options }; return new Promise((resolve, reject) => { const context = options.context || getContext(); const dialog = context.selectComponent(options.selector); delete options.context; delete options.selector; if (dialog) { dialog.setData({ onCancel: reject, onConfirm: resolve, ...options }); queue.push(dialog); } else { console.warn('未找到 van-dialog 节点,请确认 selector 及 context 是否正确'); } }); }; 复制代码
**
函数式调用时候根据传入的options配置去更新找到的组件实例上的属性
由微信小程序自定义组件限制不能更新slot,slot需要用组件嵌套来传入
函数式调用中的options会有默认值强制覆盖掉van-dialog组件属性处传入的非id等其他属性,即函数调用的时通过组件传入的属性无效
**
确认弹窗
Dialog.confirm({ selector: '#van-dialog', title: '提示', message: '这里放置提示内容' }) 复制代码
Dialog.confirm = options => Dialog({ showCancelButton: true, ...options }); 复制代码
调用Dialog时候默认执行定了显示取消按钮,其他无区别
关闭弹窗
Dialog.close() 复制代码
Dialog.close = () => { queue.forEach(dialog => { dialog.close(); }); queue = []; }; 复制代码
遍历内部缓存的所有调用Dialog方法找到的van-dialog组件实例,执行其close方法
更改对话框默认配置
Dialog.setDefaultOptions(options) 复制代码
Object.assign(Dialog.currentOptions, options); 复制代码
通过Object.assign将传入的默认配置合并到内部Dialog.currentOptions配置上
恢复对话框默认配置
Dialog.resetDefaultOptions() 复制代码
Dialog.resetDefaultOptions = () => { Dialog.currentOptions = { ...Dialog.defaultOptions }; }; 复制代码
恢复Dialog.currentOptions配置为Dialog.defaultOptions
本文讲解了vant-weapp组件库中的弹框组件的基本用法、进阶用法、定制主题、自定义内容等用法,同时还更进一步的研究了vant-weapp组件中的popup组件、dialog组件的实现。也只有彻底弄懂了UI框架的封装思路我们才能更进一步的修改框架来定制化更复杂更贴合项目要求的各种组件,本文按照 由实用到进阶再到研究源码 的思路为各位研究框架源码提供另一种方法。