最近公司有一个后台业务虽然也是写在了现有的后台系统中, 但是之后要为这个业务单独拉出来新建一个后台系统, 所以现有的后台系统中的 vue 组件库, 就不能用了(因为不知道将来的系统要基于什么组件库, 以防给未来移植项目带来麻烦), 这次业务中又遇到了弹窗的功能, 所以只能手动写一个了(虽然说弹窗组件很简单, 也是想自己总结一下, 有不对的地方也请指出), 一开始用传统的 props,$emit 但是觉得要接两个取消与确认的回调这块的逻辑分散了所以就用了 promise 两个回调的方式把两个回调写在了一起, 并不一定好, 算是提供一种思路吧.
一. 概览
先看最后的调用方式
props $emit 方式
- <chat-modal ref="chat-modal" v-model="showModal" cancelText="取消" sureText="确认" title="弹窗标题" small @on-ok="onOK" @on-cancel="onCancel">
- <div>slot 的东西, 想向弹窗中添加自定义的内容</div>
- </chat-modal>
- methods: {
- display() {
- this.showModal = true;// 交互点击手动触发显示弹窗
- },
- onOK() {},// 点击确认的回调
- onCancel() {}// 点击取消的回调
- }
promise 的回调方式
- <chat-modal ref="chat-modal"></chat-modal>
- methods: {
- display() {
- this.$refs["chat-modal"].openModal({
- title: "弹窗标题",
- sureText: "确认",
- cancelText: "取消"
- }).then(res => {
- // 点击确认的回调
- }, res => {
- // 点击取消的回调
- })
- }
- }
第二种方式的好处就是把所有的逻辑都集中到了一个方法里.
二. 看下组件的源码
tip: 样式有些烂...
- <template>
- <div>
- <div class="shadow" v-show="showModal"></div>
- <div class="modal" :class="{'smSize': otherText.small || small}" v-show="showModal">
- <div class="header">{{ otherText.title || title}}</div>
- <div class="body">
- <slot></slot>
- </div>
- <div class="footer">
- <div class="item success" id="sure" ref="sure" @click="makeSure" v-show="otherText.sureText || sureText">{{ otherText.sureText || sureText }}</div>
- <div class="item red" id="cancel" ref="cancel" @click="makeCancel" v-show="otherText.cancelText || cancelText">{{ otherText.cancelText || cancelText }}</div>
- </div>
- </div>
- </div>
- </template>
- <script>
- // 此组件提供两种调用方法, 可以在组件上 v-model 一个表示是否显示弹窗的对话框, 然后需要的一些值通过 props 传入, 然后 $emit 在组件上 @监听做回调
- // 第二中方法所有的传值回调都只需要在组件内部的一个方法调用然后在组件外部 this.$refs[xxx].open 调用然后. then 触发回调, 比上一种方便些
- var initOtherText = {
- sureText: "",
- cancelText: "",
- title: "",
- small: false
- };
- export default {
- props: {
- title: {
- type: String
- },
- sureText: {
- type: String
- },
- cancelText: {
- type: String
- },
- value: {
- type: Boolean
- },
- small: {
- type: Boolean
- }
- },
- watch: {
- value(newVal) {
- this.showModal = newVal;
- }
- },
- data() {
- return {
- otherText: JSON.parse(JSON.stringify(initOtherText)),
- showModal: this.value
- };
- },
- methods: {
- makeSure() {
- this.$emit("on-ok");
- this.$emit("input", false);
- },
- makeCancel() {
- this.$emit("on-cancel");
- this.$emit("input", false);
- },
- openModal(otherText) {
- this.otherText = { ...otherText };
- this.showModal = true;
- var pms = new Promise((resolve, reject) => {
- this.$refs["sure"].addEventListener("click", () => {
- this.showModal = false;
- resolve("点击了确定");
- });
- this.$refs["cancel"].addEventListener("click", () => {
- this.showModal = false;
- reject("点击了取消");
- });
- });
- return pms;
- }
- }
- };
- </script>
- <style lang="sCSS" scoped>
- .shadow {
- background-color: rgba(0, 0, 0, 0.5);
- display: table;
- height: 100%;
- left: 0;
- position: fixed;
- top: 0;
- transition: opacity 0.3s ease;
- width: 100%;
- z-index: 50;
- }
- .modal {
- display: table-cell;
- vertical-align: middle;
- overflow-x: hidden;
- position: fixed;
- background-color: white;
- box-shadow: rgba(0, 0, 0, 0.33) 0px 2px 8px;
- border-radius: 5px;
- outline: 0px;
- overflow: hidden;
- transition: all 0.3s ease;
- width: 600px;
- height: 400px;
- top: 50%;
- left: 50%;
- margin-top: -200px;
- margin-left: -300px;
- }
- .header {
- align-items: center;
- background-color: #62a39e;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.16);
- color: #fff;
- font-weight: bold;
- display: -ms-flexbox;
- display: flex;
- height: 3.5rem;
- padding: 0 1.5rem;
- position: relative;
- z-index: 1;
- }
- .body {
- align-items: center;
- padding: 1.5rem;
- }
- .footer {
- justify-content: flex-end;
- padding: 1.5rem;
- position: absolute;
- bottom: 0;
- width: 100%;
- float: right;
- }
- .item {
- color: white;
- text-align: center;
- border-radius: 5px;
- padding: 10px;
- cursor: pointer;
- display: inline-block;
- }
- .info {
- background-color: #2196f3;
- }
- .success {
- background-color: #62a39e;
- }
- .red {
- background-color: #e95358;
- }
- .smSize {
- height: 200px;
- }
- </style>
首先分析一下第一种方式: 调用者需要在组件外部 v-model 上绑定一个变量 (例中为 showModal) 来指示弹窗是否显示, 显示的时候需要在组件外部手动设置
this.showModal = true
, 组件内部 props 定义一个属性来接这个值为
value: {type: Boolean}
, 同时在组件内部在用声明一个变量用来同步外部传进来的 props 值
默认值为 showModal: this.value
(内部声明的值也叫了 showModal), 在 watch 中监听进行同步
watch: { value(newVal) { this.showModal = newVal } }
; 然后把组件内部的这个 showModal 值绑定在需要显示或者隐藏的 DOM 元素上. 向外抛出事件的时候是在点击组件内部的确定与关闭按钮时候
- makeSure() {
- this.$emit("on-ok");
- this.$emit("input", false);
- },
- makeCancel() {
- this.$emit("on-cancel");
- this.$emit("input", false);
- }
- this.$emit("on-ok");this.$emit("on-cancel");
这两句的是向外抛出事件在组件外部 @接一下然后写自己需要的回调函数. 这时就可以实现弹窗的显示与隐藏了, 你可能发现并没有一句代码去设置 this.showModal = false; 弹窗就隐藏了. 主要是因为这几句代码
v-model = 'showModal'
和 组件内部的
props: {value: {type: Boolean}} this.$emit("input", false)
.v-model 其实是 vue 的语法糖,
<chat-modal v-model="showModal">
其实可以写为
<chat-modal :value="showModal" @input="showModal = arguments[0]">
所以要求我们在组件内部必须规定 props 的名字必须为 value, 然后在组件内部触发确定或者取消的时候在组件内部触发
this.$emit("input", false)
这样实现了直接隐藏弹窗而不必打扰用户让用户在组件外部在手动将 showModal 置为 false.
然后来看 promise 的方式: 第一种方式传进来的值都通过 props 来接的, 这种方式通过在组件内部定义了另一个对象来接传进来的值,
- var initOtherText = {
- sureText: "",
- cancelText: "",
- title: "",
- small: false
- };
- otherText: JSON.parse(JSON.stringify(initOtherText)),
然后在 menthods 里定义了一个名为 openModal 的方法, 然后把传进来的一系列参数赋值给组件内部的对象
this.otherText = { ...otherText }; this.showModal = true;
并且将 showModal 置为 true, 然后每次触发的时候新建一个 promise 对象, 里面的异步事件为点击确定和取消的两个点击事件, 这里要操作 DOM 了
- this.$refs["sure"].addEventListener("click", () => {
- this.showModal = false;
- resolve("点击了确定");
- });
获取确定按钮的 DOM 元素绑定点击事件, 回调里将 showModal 置为 false 并且 resolve,
- this.$refs["cancel"].addEventListener("click", () => {
- this.showModal = false;
- reject("点击了取消");
- });
获取取消按钮的 DOM 绑定点击事件, 回调里 reject.
遇到的坑
这之前遇到了一个坑, 因为第一次已经绑定了点击事件, 第二次 resolve 和 reject 就会失败, 本想取消一下绑定事件, 但是因为将整个弹窗 v-show="showModal" 的原因整个 DOM 被 display:none; 了就不需要手动解绑了. 第二个是关于用 v-if 还是 v-show 来隐藏弹窗, 一开始用的是 v-if 但是发现在这步时
- this.showModal = true;
- var pms = new Promise((resolve, reject) => {
- this.$refs["sure"].addEventListener.xxx// 省略
- });
- return pms;
将 showModal 置为 true 时然后就去绑定事件这时候还没有 DOM 还没有解析玩 DOM 树上还没有, 要不就得用 this.$nextTick 增加了复杂度, 最后采用了 v-show;
关于优先级问题
如果既在组件上用 prop 传了值 (title,sureText 之类的) 如
<chat-modal"title="xx"sureText="xxx"></chat-modal>
也在方法里传了
- this.$refs["chat-modal"].openModal({
- title: "服务小结",
- sureText: "提交并结束",
- cancelText: "取消"
- }).then();
是以方法的优先级为高, 在组件内部 DOM 元素上通过 || 设置了优先级, 比如
<div class="header popModal">{{ otherText.title || title}}</div>
有方法的值取方法的值, 没有取 props 得值.
有不好或者不对的地方欢迎指正
来源: https://juejin.im/post/5afe42a4f265da0b873abc23