Vue自定义render统一项目组弹框功能


Posted in Javascript onJune 07, 2020

一、本文收获

pick

二、为什么要统一封装弹框;

要封装成怎样

通过举例常规弹框的写法。我们可以体会到,通常要弹出一个页面,需要创建一个页面 normalDialog.vue 包裹 dialogBody.vue (弹框主体);需要 parent.vue 设置flag控制弹框显示隐藏, normalDialog.vue 关闭的时候设置 parent.vue 对应 flag 。缺点: 流程繁杂、配置繁琐、不灵活、样式不统一和参数传递麻烦等 。如果一个项目弹框较多的时候,弊端将会更明显,大量的 isXxxDialogShow ,大量的 vue 文件。因此项目组急需一个能简单配置就能弹出弹框的 API

1. 常规弹框写法 dialoBody.vue (弹框主体) ,此处采用 Composition API 的写法。只做了简单的页面,包含校验,抽取保存数据的常规逻辑。

<template>
 <div class="dialog-body">
 <div class="item">
 <div>名称</div>
 <el-input v-model="name"></el-input>
 </div>
 <div class="item">
 <el-radio-group v-model="attention">
 <el-radio label="已关注"></el-radio>
 <el-radio label="等下关注"></el-radio>
 </el-radio-group>
 </div>
 <div class="item">
 <el-radio-group v-model="like">
 <el-radio label="已点赞"></el-radio>
 <el-radio label="等下点赞"></el-radio>
 </el-radio-group>
 </div>
 </div>
</template>

<script>
import { reactive, toRefs } from '@vue/composition-api'
import pick from 'lodash/pick'
import { Message } from 'element-ui'

export default {
 props: {
 defaultName: String,
 },
 setup(props, ctx) {
 const ATTENTIONED = '已关注'
 const LIKED = '已点赞'
 const state = reactive({
 name: props.defaultName, // 名称
 attention: '已关注', // 关注
 like: '已点赞', // 点赞
 })
 /*************************************************************
 * 页面绑定的事件
 * 建议写法:
 * 1. 定义methods常量
 * 2. 处理相关业务逻辑的时候,需要绑定事件到页面的时
 * 建议通过methods.onXxx = ()=>{ // 相关逻辑 }的形式定义
 * 好处1: onXxx定义的位置和相关业务逻辑代码关联一起
 * 好处2: 可以统一通过...methods的形式在setup统一解构
 * 好处3: 当页面逻辑复杂,需要操作的数据关联性强,不可拆解组件;
 *  可将相关业务的代码在独立模块定义;
 *  独立模块暴露API handleXxx(methods,state),流水线加工methods;
 *  和Vue2源码一样,流水线加工的思想.
 */
 const methods = {}
 // 校验名称
 methods.onNameBlur = () => {}

 // ************************ 向外暴露的API ************************
 const apiMethods = {
 // 保存前校验
 isCanSave() {
 if (state.attention !== ATTENTIONED || state.like !== LIKED) {
  Message.error('未关注或者点赞,不能关闭,嘻嘻')
  return false
 }
 return true
 },
 // 获取保存数据
 getSaveData() {
 // ******* lodash pick 从对象中抽取数据
 return pick(state, ['name', 'attention', 'like'])
 },
 }
 return {
 ...toRefs(state),
 ...methods,
 apiMethods,
 }
 },
}
</script>

<style lang="less">
.dialog-body {
 width: 100%;
 height: 100px;
}
</style>

2.normalDialog.vue 包裹弹框主体 dialoBody.vue

<template>
 <el-dialog 
 title="帅哥,美女,我是标题" 
 :visible.sync="isShow" 
 width="30%" 
 :before-close="onClose"
 >
 <dialog-body default-name="参数传递的名称" ref="inner"></dialog-body>
 <span slot="footer" class="dialog-footer">
 <el-button @click="onClose">取 消</el-button>
 <el-button type="primary" @click="onOK">确 定</el-button>
 </span>
 </el-dialog>
</template>

<script>
import dialogBody from './dialogBody.vue'
export default {
 components: {
 dialogBody,
 },
 data() {
 return {
 isShow: true,
 }
 },
 methods: {
 onClose() {
 // *********** 修改parent.vue ********
 this.$parent.isNormalDialogShow = false
 },
 // ******* 控制保存流程 ********
 onOK() {
 const inner = this.$refs.inner
 // 校验是否可以保存
 if (inner.apiMethods.isCanSave()) {
 // 获取保存数据
 const postData = inner.apiMethods.getSaveData()
 console.log('>>>>> postData >>>>>', postData)
 // 保存成功后关闭弹框
 this.onClose()
 }
 },
 },
}
</script>

parent.vue

// html 部分
<normal-dialog v-if="isNormalDialogShow" />

// Js部分
data(){
	isNormalDialogShow:false
}
methods:{
 onDialogShow(){ // ******控制弹框显示*****
 this.isNormalDialogShow = true
 }
}

2. 要封装成怎样

2.1 API诉求:

isXxxDialogShow
el-dialog

2.2 理想API:

import dialogBody from './dialogBody.vue'
const dialog = new JSDialog({
 comonent: dialogBody, 
 dialogOpts: { // 可扩展配置
 title: 'JSDialog设置的弹框标题',
 width: '400px'
 },
 props: {
 defaultName: 'JSDialog传递的参数',
 },
 onOK() {
 const inner = dialog.getInner() // 能取到dialogBody的引用
 // 控制流程
 if (inner.apiMethods.isCanSave()) {
 // 获取保存数据
 const postData = inner.apiMethods.getSaveData()
 console.log('>>>>> postData >>>>>', postData)
 // 关闭弹框
 dialog.close()
 }
 },
 onCancel() {
 dialog.close() // 弹框关闭
 },
})
dialog.show() // 弹框显示

三、如何封装

动态控制显示内容,脑海浮现的三个方案: 卡槽、动态组件和重写 render 。下面在动态弹框场景下简单对比三个方案。

  • slot(卡槽) ,和 el-dialog 原理类似,只是再封装了一层,少定义了 normalDialog.vue 文件。 缺点:调用复杂,不灵活;不容易控制关闭的流程;只能在 template 中定义 。
  • component(动态组件) ,创建 commonDialog.vue ,统一挂在 App.vue 下,利用 <component :is="componentId"></component> 动态切换弹框主体, commonDialog.vue 监听 componentId 变化来切换弹框主体。 缺点:要提前将所有弹框主体组件注册到commonDialog.vue页面的components上;依赖于vuex,侵入性较强;纯js文件通过vuex弹出弹框相对复杂,不灵活 。
  • 重写 render , render 是 Vue 对造轮子开发者开放的后门。动态弹框可作为独立的功能模块,内部通过new Vue ,重写 render 控制渲染内容。 独立 Vue 实例,可预先创建,可在任何位置控制弹框,灵活,清晰 。 缺点:暂无

1. 整体代码

先整体预览一下代码,下面再细分讲解。

import Vue from 'vue'
import merge from 'lodash/merge'
import orderBy from 'lodash/orderBy'

// 按钮配置项构造器
function btnBuilder(options) {
 const defaultBtn = {
 text: '按钮', // 显示文本
 clickFn: null, // 点击回调
 type: 'default', // 样式
 isHide: false, // 是否隐藏
 order: 2 // 顺序
 }
 return { ...defaultBtn, ...options }
}

export default class JSDialog {
 constructor(originOptions) {
 this.options = {}
 this.vm = null
 this._mergeOptions(originOptions)
 this._initVm()
 }
 // 参数合并
 _mergeOptions(originOptions) {
 const defaultOptions = {
 component: '', // 弹框主体vue页面
 // 可扩展el-dialog官方api所有配置项,小驼峰aaaBbbCcc
 dialogOpts: {
 width: '40%',
 title: '默认标题'
 },
 // 传入弹框主体vue组件的参数
 props: {},
 // 点击确定回调
 onOK: () => {
 console.log('JSDialog default OK'), this.close()
 },
 // 点击取消回调
 onCancel: () => {
 console.log('JSDialog default cancel'), this.close()
 },
 footer: {
 ok: btnBuilder({
  text: '确定',
  type: 'primary',
  order: 0
 }),
 cancel: btnBuilder({
  text: '取消',
  order: 1
 })
 }
 }
 // 参数合并到this.options
 merge(this.options, defaultOptions, originOptions)
 const footer = this.options.footer
 Object.entries(footer).forEach(([key, btnOptions]) => {
 // 确定和取消默认按钮
 if (['ok', 'cancel'].includes(key)) {
 const clickFn = key === 'ok' ? this.options.onOK : this.options.onCancel
 // 默认按钮回调优先级: footer配置的clickFn > options配置的onOK和onCancel
 btnOptions.clickFn = btnOptions.clickFn || clickFn
 } else {
 // 新增按钮
 // 完善配置
 footer[key] = btnBuilder(btnOptions)
 }
 })
 }
 _initVm() {
 const options = this.options
 const beforeClose = this.options.footer.cancel.clickFn // 弹框右上角关闭按钮回调
 this.vm = new Vue({
 data() {
 return {
  // 需要响应式的数据
  footer: options.footer, // 底部按钮
  visible: false // 弹框显示及关闭
 }
 },
 methods: {
 show() {
  // 弹框显示
  this.visible = true
 },
 close() {
  // 弹框关闭
  this.visible = false
 },
 clearVm() {
  // 清除vm实例
  this.$destroy()
 }
 },
 mounted() {
 // 挂载到body上
 document.body.appendChild(this.$el)
 },
 destroyed() {
 // 从body上移除
 document.body.removeChild(this.$el)
 },
 render(createElement) {
 // 弹框主体
 const inner = createElement(options.component, {
  props: options.props, // 传递参数
  ref: 'inner' // 引用
 })
 // 控制按钮显示隐藏
 const showBtns = Object.values(this.footer).filter(btn => !btn.isHide)
 // 控制按钮顺序
 const sortBtns = orderBy(showBtns, ['order'], ['desc'])
 // 底部按钮 jsx 写法
 const footer = (
  <div slot="footer">
  {sortBtns.map(btn => (
  <el-button type={btn.type} onClick={btn.clickFn}>
  {btn.text}
  </el-button>
  ))}
  </div>
 )
 // 弹框主体
 const elDialog = createElement(
  'el-dialog',
  {
  // el-dialog 配置项
  props: {
  ...options.dialogOpts,
  visible: this.visible,
  beforeClose
  },
  // **** 看这里,visible置为false后,el-dialog销毁后回调 *****
  on: {
  closed: this.clearVm
  },
  ref: 'elDialog'
  },
  // 弹框内容:弹框主体和按钮
  [inner, footer]
 )
 return elDialog
 }
 }).$mount()
 }
 // 封装API
 // 关闭弹框
 close() {
 this.vm.close()
 }
 // 显示弹框
 show() {
 this.vm.show()
 }
 // 获取弹框主体实例,可访问实例上的方法
 getInner() {
 return this.vm.$refs.inner
 }
}

2. 参数合并

​ 要做到 API 诉求中的:调用简单、传参简便和可扩展控制弹框样式。参数合并便是 成本最小 的实现方案,配合 TS 效果更佳。定义默认参数,通过 lodash 的 merge ,合并深层属性。通过参数合并还能做到自定义 footer 按钮,控制文本,样式,顺序和执行回调。

// 参数合并
_mergeOptions(originOptions) {
 const defaultOptions = {
 component: '', // 弹框主体vue页面
 // 可扩展el-dialog官方api所有配置项,小驼峰aaaBbbCcc
 dialogOpts: {
 width: '40%',
 title: '默认标题'
 },
 // 传入弹框主体vue组件的参数
 props: {},
 // 点击确定回调
 onOK: () => {
 console.log('JSDialog default OK'), this.close()
 },
 // 点击取消回调
 onCancel: () => {
 console.log('JSDialog default cancel'), this.close()
 },
 footer: {
 ok: btnBuilder({
 text: '确定',
 type: 'primary',
 order: 0
 }),
 cancel: btnBuilder({
 text: '取消',
 order: 1
 })
 }
 }
 // 参数合并到this.options
 merge(this.options, defaultOptions, originOptions)
 const footer = this.options.footer
 Object.entries(footer).forEach(([key, btnOptions]) => {
 // 确定和取消默认按钮
 if (['ok', 'cancel'].includes(key)) {
 const clickFn = key === 'ok' ? this.options.onOK : this.options.onCancel
 // 默认按钮回调优先级: footer配置的clickFn > options配置的onOK和onCancel
 btnOptions.clickFn = btnOptions.clickFn || clickFn
 } else { // 新增按钮
 // 完善配置
 footer[key] = btnBuilder(btnOptions)
 }
 })
}

3. render函数

​ 摘取一段 渲染函数 & JSX 官方文档关于 render 的描述: Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用 渲染函数 ,它比模板更接近编译器。 ​ ​ 官方文档对渲染函数的写法,参数,对应JSX写法介绍已经很详细,这里就不再赘述。下面代码是在最新vue-cli创建项目上运行的,尝试了JS参数创建元素和JSX创建元素两种写法。

render(createElement) {
 // 弹框主体
 const inner = createElement(options.component, {
 props: options.props, // 传递参数
 ref: 'inner' // 引用
 })
 // 控制按钮显示隐藏
 const showBtns = Object.values(this.footer).filter(btn => !btn.isHide)
 // 控制按钮顺序
 const sortBtns = orderBy(showBtns, ['order'], ['desc'])
 // 底部按钮 jsx 写法
 const footer = (
 <div slot="footer">
 {sortBtns.map(btn => (
 <el-button type={btn.type} onClick={btn.clickFn}>
  {btn.text}
 </el-button>
 ))}
 </div>
 )
 // 弹框主体
 const elDialog = createElement(
 'el-dialog',
 {
 // el-dialog 配置项
 props: {
 ...options.dialogOpts,
 visible: this.visible
 },
 on: {
 closed: this.clearVm
 },
 ref: 'elDialog'
 },
 // 弹框内容:弹框主体和按钮
 [inner, footer]
 )
 return elDialog
}

4. 封装API

​ 暂时只封装了三个 API ,可根据不同的场景扩展 API ,比如弹框不销毁隐藏,弹框刷新等。

show() ,弹框显示

显示主要是修改 el-dialog 的 visible 为 true ,控制挂载到 body 上的弹框显示。

show() {
 this.vm.show()
}

close() ,弹框关闭

关闭处理流程:修改 el-dialog 的 visible 为 false ;触发 el-dialog 的 closed 事件;执行 clearVm ;执行 vm 的 $destroy() ; destroyed() 回调中将 $el 从 body 中移除。

close() {
 this.vm.close()
}

getInner() ,获取弹框主体实例,可用于访问实例上的方法,控制按钮流程

getInner() {
 return this.vm.$refs.inner
}

四、如何使用

1. 最简单场景,只配置页面

按钮事件回调采用默认的回调,确定和取消按钮都可关闭弹框

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
 component: dialogBody,
})
dialog.show() // 弹框显示

效果如下:

Vue自定义render统一项目组弹框功能 

2. 控制弹框样式及确定流程

可自定义el-dialog支持的配置项,见 Dialog 对话框 ;比如:title、 customClass 。通过customClass可统一控制项目内弹框的风格;可控制确定取消按钮代码回调。

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
 component: dialogBody,
 dialogOpts: {
 title: '靓仔,美女欧嗨呦',
 customClass:'js-dialog'
 },
 props: {
 defaultName: 'JSDialog传递的参数'
 },
 onOK() {
 const inner = dialog.getInner() // 能取到dialogBody的引用
 // 控制流程
 if (inner.apiMethods.isCanSave()) {
  // 获取保存数据
  const postData = inner.apiMethods.getSaveData()
  console.log('>>>>> postData >>>>>', postData)
  // 关闭弹框
  dialog.close()
 }
 },
 onCancel() {
 dialog.close() // 弹框关闭
 }
})

效果如下:

Vue自定义render统一项目组弹框功能 

3. 自定义footer

自定义按钮可控制执行回调,样式,顺序,显示与隐藏

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
 component: dialogBody,
 footer: {
 ok: { // 修改默认按钮
  text: '新增'
 },
 cancel: { // 隐藏默认按钮
  isHide: true
 },
 add: { // 新增按钮
  text: '另存为',
  clickFn() {
  dialog.close()
  },
  order: -1 // 控制按钮顺序,order小的显示在右边
 },
 add2: {
  text: '新增按钮2',
  clickFn() {
  dialog.close()
  },
  order: 3
 }
 }
})
dialog.show() // 弹框显示

效果如下:

Vue自定义render统一项目组弹框功能

总结

到此这篇关于Vue自定义render统一项目组弹框功能的文章就介绍到这了,更多相关Vue自定义render项目组弹框内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JavaScript 输入框内容格式验证代码
Feb 11 Javascript
js有关元素内容操作小结
Dec 20 Javascript
jQuery之排序组件的深入解析
Jun 19 Javascript
Jquery中children与find之间的区别详细解析
Nov 29 Javascript
js形成页面的一种遮罩效果实例代码
Jan 04 Javascript
关于JS中的apply,call,bind的深入解析
Apr 05 Javascript
使用JS实现图片展示瀑布流效果的实例代码
Sep 12 Javascript
MUI 实现侧滑菜单及其主体部分上下滑动的方法
Jan 25 Javascript
Vue引用第三方datepicker插件无法监听datepicker输入框的值的解决
Jan 27 Javascript
vue-router的HTML5 History 模式设置
Sep 08 Javascript
JavaScript面向对象编程小游戏---贪吃蛇代码实例
May 15 Javascript
vue 实现基础组件的自动化全局注册
Dec 25 Vue.js
用云开发Cloudbase实现小程序多图片内容安全监测的代码详解
Jun 07 #Javascript
Electron整合React使用搭建开发环境的步骤详解
Jun 07 #Javascript
vue路由权限校验功能的实现代码
Jun 07 #Javascript
Vue使用自定义指令实现拖拽行为实例分析
Jun 06 #Javascript
JS原型对象操作实例分析
Jun 06 #Javascript
JS中的继承操作实例总结
Jun 06 #Javascript
ES6 async、await的基本使用方法示例
Jun 06 #Javascript
You might like
PHP获取指定函数定义在哪个文件中以及其所在的行号实例
2014/05/08 PHP
解决PHP 7编译安装错误:cannot stat ‘phar.phar’: No such file or directory
2017/02/25 PHP
JavaScript的面向对象(二)
2006/11/09 Javascript
jQuery中与toggleClass等价的程序段 以及未来学习的方向
2010/03/18 Javascript
使用Firebug对js进行断点调试的图文方法
2011/04/02 Javascript
ASP.NET jQuery 实例4(复制TextBox的文本到本地剪贴板上)
2012/01/13 Javascript
JavaScript中“基本类型”之争小结
2013/01/03 Javascript
js加密解密字符串可自定义密码因子
2014/05/13 Javascript
IE6-IE9中tbody的innerHTML不能赋值的解决方法
2014/06/05 Javascript
JavaScript插件化开发教程 (三)
2015/01/27 Javascript
Ajax中解析Json的两种方法对比分析
2015/06/25 Javascript
详解JavaScript的流程控制语句
2015/11/30 Javascript
学习使用bootstrap3栅格系统
2016/04/12 Javascript
Input文本框随着输入内容多少自动延伸的实现
2017/02/15 Javascript
JavaScript中this的学习笔记及用法整理
2020/02/17 Javascript
jquery.validate自定义验证用法实例分析【成功提示与择要提示】
2020/06/06 jQuery
JavaScript 声明私有变量的两种方式
2021/02/05 Javascript
ssh批量登录并执行命令的python实现代码
2012/05/25 Python
Python实现微信公众平台自定义菜单实例
2015/03/20 Python
用Python写飞机大战游戏之pygame入门(4):获取鼠标的位置及运动
2015/11/05 Python
Google开源的Python格式化工具YAPF的安装和使用教程
2016/05/31 Python
python 反向输出字符串的方法
2018/07/16 Python
pycharm设置当前工作目录的操作(working directory)
2020/02/14 Python
Python序列化pickle模块使用详解
2020/03/05 Python
Django项目在pycharm新建的步骤方法
2021/03/02 Python
化工专业推荐信范文
2013/11/28 职场文书
项目专员岗位职责
2013/12/04 职场文书
薪酬专员岗位职责
2014/02/18 职场文书
2014年公务员思想汇报范文:全心全意为人民服务
2014/03/06 职场文书
党的群众路线对照检查材料(个人)
2014/09/24 职场文书
2014年社区宣传工作总结
2014/12/02 职场文书
追悼会答谢词
2015/01/05 职场文书
升学宴学生答谢词
2015/01/05 职场文书
结婚幸福感言
2015/08/01 职场文书
感恩的心主题班会
2015/08/12 职场文书
解决Vue+SpringBoot+Shiro跨域问题
2021/06/09 Vue.js