源哥带你CodeReview02:提取dialog弹框

360 阅读4分钟

讲解在同名B/D上都有,主要介绍一些跟业务无关的代码技巧

注: 在CodeReview中,部分内容主观性较大,一家之言姑妄听之

本文主要介绍对dialog的基础封装,以下是业务代码抽象,整个文件破千行

<template>
    <div id="app">
        <el-table :data="tableData" style="width: 100%">
            <el-table-column prop="name" label="Name" min-width="180" />
            <el-table-column label="Operations" min-width="120">
                <template #default="scope">
                    <el-button link type="primary" size="small" @click.prevent="editRowHandler(scope.row)">
                        编辑
                    </el-button>
                    <el-button link type="primary" size="small" @click.prevent="controlRowHandler(scope.row)">
                        操作
                    </el-button>
                </template>
            </el-table-column>
        </el-table>
        <el-dialog :visible.sync="editVisible" title="编辑">
            <el-form>
                <el-form-item>
                    <el-input v-model="editForm.name" placeholder="请输入名称"></el-input>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button @click="editVisible = false">取消</el-button>
                    <el-button :loading="editLoading" type="primary" @click="handlerClick">
                        确定
                    </el-button>
                </div>
            </template>
        </el-dialog>
        <el-dialog :visible.sync="controlVisible" title="控制">
            <el-form>
                <el-form-item>
                    <el-input v-model="controlForm.name" placeholder="请输入名称"></el-input>
                </el-form-item>
            </el-form>
        </el-dialog>
    </div>
</template>
  
<script>
import { MessageBox, Message } from 'element-ui'

// 模拟接d口请求
const api = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}
// 1期优化代码
function handleConfirmFlow(fn) {
    return async function (...args) {
        try {
            // 提交包含二次确定
            await MessageBox.confirm('确认提交吗?')
            await fn.call(this, ...args)
        } catch (error) {
            // 统一对错误进行拦截
            if (error === 'cancel') {
                Message.info('提交已取消')
            } else {
                Message.error(error.message || '提交失败')
            }
        }
    }
}

export default {
    data() {
        return {
            editVisible: false,
            controlVisible: false,
            tableData: [{ name: 1 }, { name: 2 }],
            editLoading: false,
            editRow: {},
            editForm: {},
            controlForm: {
                name: undefined
            }
        }
    },
    methods: {
        getList() {
            // 模拟远程获取数据
            this.tableData = [{ name: 111 }, { name: 2 }]
        },
        handlerClick: handleConfirmFlow(async function () {
            // 第一期优化后的函数
            this.editLoading = true
            await api({
                xxxname: this.editForm
            })
            this.editLoading = false
            this.editVisible = false
            // 反写数据 -- 刷新接口
            // this.getList()
            // 反写数据 -- 直接修改数据
            this.editRow.name = this.editForm.name
            this.$message.success('修改成功')
        }),
        editRowHandler(row) {
            this.editRow = row;
            this.editVisible = true;
            // ...很多代码
            this.$set(this.editForm, 'name', row.name);
        },
        controlRowHandler(row) {
            this.controlVisible = true;
            // ...很多代码
            this.controlForm.name = row.name;
        }
    }
}
</script>

重新修改业务流程

dialog提取

这个页面全文超过1000行,肯定是需要切割成更细的组件

基础的八股题有一道是如何做组件化开发,包括封装组件的标准有哪些,每个人的答案是不一样的,至少在这个业务当中,将具体的弹框抽出去就是比较好的一种实践

直接提取

要啥数据,给啥数据,先能用就行

  • 主应用
<template>
  <div id="app">
    <el-table :data="tableData" style="width: 100%">
      <el-table-column prop="name" label="Name" min-width="180" />
      <el-table-column label="Operations" min-width="120">
        <template #default="scope">
          <el-button link type="primary" size="small" @click.prevent="editRowHandler(scope.row)">
            编辑
          </el-button>
          <el-button link type="primary" size="small" @click.prevent="controlRowHandler(scope.row)">
            操作
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <controlDialog :visible.sync="controlVisible" :form="controlForm"></controlDialog>
    <editDialog :visible.sync="editVisible" @update:visible="controlVisible = $event" :form="editForm" :editRow="editRow">
    </editDialog>
  </div>
</template>

<script>
import controlDialog from './components/controlDialog'
import editDialog from './components/editDialog'

export default {
  components: {
    controlDialog,
    editDialog
  },
  data() {
    return {
      editVisible: false,
      controlVisible: false,
      tableData: [{ name: 1 }, { name: 2 }],

      editRow: {},
      editForm: {},
      controlForm: {
        name: undefined
      }
    }
  },
  methods: {
    getList() {
      // 模拟远程获取数据
      this.tableData = [{ name: 111 }, { name: 2 }]
    },
    editRowHandler(row) {
      this.editRow = row;
      this.editVisible = true;
      // ...很多代码
      this.$set(this.editForm, 'name', row.name);
    },
    controlRowHandler(row) {
      this.controlVisible = true;
      // ...很多代码
      this.controlForm.name = row.name;
    }
  }
}
</script>
  • 弹框
<template>
    <el-dialog :visible.sync="visible" title="编辑">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <div class="dialog-footer">
                <el-button @click="$emit('update:visible', false)">取消</el-button>
                <el-button :loading="editLoading" type="primary" @click="handlerClick">
                    确定
                </el-button>
            </div>
        </template>
    </el-dialog>
</template>
<script>
import { MessageBox, Message } from 'element-ui'

// 1期优化代码
function handleConfirmFlow(fn) {
    return async function (...args) {
        try {
            // 提交包含二次确定
            await MessageBox.confirm('确认提交吗?')
            await fn.call(this, ...args)
        } catch (error) {
            // 统一对错误进行拦截
            if (error === 'cancel') {
                Message.info('提交已取消')
            } else {
                Message.error(error.message || '提交失败')
            }
        }
    }
}

// 模拟接d口请求
const api = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}


export default {
    props: ["visible", "form", "editRow"],
    data() {
        return {
            editLoading: false,
        }
    },
    methods: {
        handlerClick: handleConfirmFlow(async function () {
            // 第一期优化后的函数
            this.editLoading = true
            await api({
                xxxname: this.editForm
            })
            this.editLoading = false
            this.$emit('update:visible', false)
            // 反写数据 -- 刷新接口
            // this.getList()
            // 反写数据 -- 直接修改数据
            this.editRow.name = this.form.name
            this.$message.success('修改成功')
        }),
    }
}
</script>

loading

这里用到了上期使用的技巧,但之前的操作是没有loading的,我们用这种操作复用的方式是不对的,考虑下接口报错后,loading的状态。

这里我们需要将loading也交给我们的流程复用系列

Vue.observable

虽然他看起来像vue3,但的确是Vue2.6以后得技巧,useHook系列的前身

如果使用过vuex,可以参考类似的Helpper系列,这里不再二次封装

<template>
    <el-dialog :visible.sync="visible" title="编辑">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <div class="dialog-footer">
                <el-button @click="$emit('update:visible', false)">取消</el-button>
                <el-button :loading="loading" type="primary" @click="handlerClick">
                    确定
                </el-button>
            </div>
        </template>
    </el-dialog>
</template>
<script>
import { MessageBox, Message } from 'element-ui'
import Vue from 'vue'

// ---------这里1
function handleConfirmFlowHelp() {
    const ret = {
        loading: false,
        // visible: false
    }
    Vue.observable(ret)
    ret.handleConfirmFlow = (fn) => {
        return async function (...args) {
            try {
                // 提交包含二次确定
                await MessageBox.confirm('确认提交吗?')
                ret.loading = true
                await fn.call(this, ...args)
                ret.loading = false
            } catch (error) {
                ret.loading = false
                // 统一对错误进行拦截
                if (error === 'cancel') {
                    Message.info('提交已取消')
                } else {
                    Message.error(error.message || '提交失败')
                }
            }
        }
    }
    return ret
}

// 模拟接d口请求
const api = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}
// ---------这里2
const confirmFlowHelp = handleConfirmFlowHelp();


export default {
    props: ["visible", "form", "editRow"],
    computed: {
        // ---------这里3
        loading() {
            return confirmFlowHelp.loading
        }
    },
    methods: {
        // ---------这里4
        handlerClick: confirmFlowHelp.handleConfirmFlow(async function () {
            await api({
                xxxname: this.editForm
            })
            this.$emit('update:visible', false)
            // 反写数据 -- 刷新接口
            // this.getList()
            // 反写数据 -- 直接修改数据
            this.editRow.name = this.form.name
            this.$message.success('修改成功')
        })
    }
}
</script>

直接使用

当然,如果受不了上面怪异的写法,直接用也可以,但一定要注意参数

<template>
    <el-dialog :visible.sync="visible" title="编辑">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <div class="dialog-footer">
                <el-button @click="$emit('update:visible', false)">取消</el-button>
                <el-button :loading="loading" type="primary" @click="handlerClick">
                    确定
                </el-button>
            </div>
        </template>
    </el-dialog>
</template>
<script>
import { MessageBox, Message } from 'element-ui'

// 1期优化代码
function handleConfirmFlow(fn) {
    return async function (...args) {
        try {
            // 提交包含二次确定
            await MessageBox.confirm('确认提交吗?')
            // ----------------------------------- 改变1
            this.loading = true
            await fn.call(this, ...args)
            // ----------------------------------- 改变2
            this.loading = false
        } catch (error) {
            // ----------------------------------- 改变3
            this.loading = false
            // 统一对错误进行拦截
            if (error === 'cancel') {
                Message.info('提交已取消')
            } else {
                Message.error(error.message || '提交失败')
            }
        }
    }
}

// 模拟接d口请求
const api = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}


export default {
    props: ["visible", "form", "editRow"],
    data() {
        return {
            loading: false,
        }
    },
    methods: {
        handlerClick: handleConfirmFlow(async function () {
            await api({
                xxxname: this.editForm
            })
            this.$emit('update:visible', false)
            // 反写数据 -- 刷新接口
            // this.getList()
            // 反写数据 -- 直接修改数据
            this.editRow.name = this.form.name
            this.$message.success('修改成功')
        }),
    }
}
</script>

emit 与 visible

在弹框中,虽然使用了$emit但依然可能会遇见以下bug image.png 因为dialog内部会对visible发送update:visible事件,或者说直接修改visible属性

我们必须对visible的修改进行拦截,有三种调整方式 [这里使用简单的弹框代替]

多加一个参数

第一反应是data + watch 一步一步处理

这种方式的确是最多的

<template>
    <el-dialog :visible.sync="value" title="控制">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
    </el-dialog>
</template>
<script>
export default {
    props: ["visible", "form"],
    data() {
        return {
            value: false
        }
    },
    watch: {
        visible(val) {
            this.value = val
        },
        value(val) {
            this.$emit('update:visible', val)
        }
    }
}
</script>

使用访问器/computed属性

如果没有特殊需求,保持一致的场景,都可以使用访问器/computed的形式处理

<template>
    <el-dialog :visible.sync="value" title="控制">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
    </el-dialog>
</template>
<script>
export default {
    props: ["visible", "form"],
    computed: {
        value: {
            get() {
                return this.visible
            },
            set(val) {
                this.$emit('update:visible', val)
            }
        }
    }
}
</script>

使用函数

使用@input:visible代替 .sync,略

dialog + form 的流程复用

考虑下以下代码的逻辑

为什么不直接传递 row

传递给dialog中的form如果直接修改,会影响到table中的数据,需要再外层处理下

export default {
  methods: {
    editRowHandler(row) {
      this.editRow = row;
      this.editVisible = true;
      // ...很多代码
      this.$set(this.editForm, 'name', row.name);
    },
    controlRowHandler(row) {
      this.controlVisible = true;
      // ...很多代码
      this.controlForm.name = row.name;
    }
  }
}

为什么使用this.$set?

因为data上面对editForm结构做初始化

为什么传递给api的数据需要格式化?

因为后端需要的结构不一样

export default {
  methods: {
        handlerClick: handleConfirmFlow(async function () {
            // ....
            await api({
                xxxname: this.editForm
            })
        }),
  }
}

解决

这是数据转换中,非常经典的流程

  • 原始数据 - raw

当前组件接收到的待处理的数据,他不会被改变,用于备份,后续的操作包括对比/diff,比如修改高亮,如果是非同步的操作,也会使用快照的形式[深拷贝]保留原始值

  • 扭转数据 - form

内部定义的数据,想怎么扭就怎么扭

  • 序列化数据 - serform

交给后端的数据,跟后端有关,好一点是vo跟form保持一致,差一点的需要前端转换为易保存的格式

了解以上内容后,可如下处理

<template>
    <el-dialog :visible.sync="value" title="编辑">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <div class="dialog-footer">
                <el-button @click="value = false">取消</el-button>
                <el-button :loading="loading" type="primary" @click="handlerClick">
                    确定
                </el-button>
            </div>
        </template>
    </el-dialog>
</template>
<script>
import { MessageBox, Message } from 'element-ui'
import Vue from 'vue'

// 1期优化代码
function handleConfirmFlowHelp() {
    const ret = {
        loading: false,
        // visible: false
    }
    Vue.observable(ret)
    ret.handleConfirmFlow = (fn) => {
        return async function (...args) {
            try {
                // 提交包含二次确定
                await MessageBox.confirm('确认提交吗?')
                ret.loading = true
                await fn.call(this, ...args)
                ret.loading = false
            } catch (error) {
                ret.loading = false
                // 统一对错误进行拦截
                if (error === 'cancel') {
                    Message.info('提交已取消')
                } else {
                    Message.error(error.message || '提交失败')
                }
            }
        }
    }
    return ret
}

// 模拟接d口请求
const api = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}

const confirmFlowHelp = handleConfirmFlowHelp();


export default {
    props: ["visible", "raw", "editRow"],
    data() {
        return {
            // ---------------------------- 1 结构初始化
            form: {
                name: ''
            }
        }
    },
    watch: {
        visible(val) {
            // ---------------------------- 2 初始化阶段 -- dialog复用
            if (val) {
                Object.assign(this.form, { name: '' }, this.raw)
            }
        }
    },
    computed: {
        loading() {
            return confirmFlowHelp.loading
        },
        value: {
            get() {
                return this.visible
            },
            set(val) {
                this.$emit('update:visible', val)
            }
        },
        // ----------------------------------- 3. 返回给后端的序列化结构
        serform() {
            return {
                xxxname: this.form.name
            }
        }
    },
    methods: {
        handlerClick: confirmFlowHelp.handleConfirmFlow(async function () {
            await api(this.serform)
            this.value = false
            // 反写数据 -- 刷新接口
            // this.getList()
            // 反写数据 -- 直接修改数据
            this.editRow.name = this.form.name
            this.$message.success('修改成功')
        })
    }
}
</script>

结构初始化

必须固定,没这个没有响应式,后面就得用$set

visible

强调一下对visible的监控,因为这里是dialog的复用,在dialog打开时,我们必须重置所有的业务属性,否则会因为残留数据,引起各种奇怪的bug

另一种方式,则是不要对弹框进行复用,比起这点性能,它产生的问题更多

  • 同一时间更多的dom节点
  • 组件必须提到最外层,需要手动注意,不能放到for循环中 [对流程封装有一定影响]
  • 缓存初始化引起的属性初始化操作 [萌新常犯]
  • ....

这是一种比较固定的描述流程,至少我们可以直接将row传递给整个dialog组件,而不需要关心是否可能产生副作用

反写数据

观察下为什么要注入一个editRow? 因为我们需要将数据内容反写到整个table中

我们封装的是一个业务模型,在弹框这个子模型中,他不应该上一级流程的额外数据,这些并不安全,他是可能丢失的,比如table使用了轮询修改的方式,即使不考虑这些,我们也应该清楚,在我们这个封装的模块中,他只是负责修改form表单属性,并进行提交,其他的内容,交给上一个流程即可

<template>
  <div id="app">
    <el-table :data="tableData" style="width: 100%">
      <el-table-column prop="name" label="Name" min-width="180" />
      <el-table-column label="Operations" min-width="120">
        <template #default="scope">
          <el-button link type="primary" size="small" @click.prevent="editRowHandler(scope.row)">
            编辑
          </el-button>
          <el-button link type="primary" size="small" @click.prevent="controlRowHandler(scope.row)">
            操作
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <controlDialog :visible.sync="controlVisible" :form="controlForm"></controlDialog>
    <editDialog :visible.sync="editVisible" :raw="editRow" @ok="okHandler">
    </editDialog>
  </div>
</template>

<script>
import controlDialog from './components/controlDialog'
import editDialog from './components/editDialog'

export default {
  components: {
    controlDialog,
    editDialog
  },
  data() {
    return {
      editVisible: false,
      controlVisible: false,
      tableData: [{ name: 1 }, { name: 2 }],
      // ------------------------- 1. 注册
      editRow: {},
      controlForm: {
        name: undefined
      }
    }
  },
  methods: {
    getList() {
      this.tableData = [{ name: 111 }, { name: 2 }]
    },
    editRowHandler(row) {
      // ------------------------- 2. 传递
      this.editRow = row;
      this.editVisible = true;
    },
    // 弹框关闭后的流程
    okHandler(form) {
      // ------------------------- 3. 回写  这里的语义可能不准,只提示
      this.editRow.name = form.name;
    },
    controlRowHandler(row) {
      this.controlVisible = true;
    },

  }
}
</script>

other

当我们对dialog里需要改编的内容提出出来后,一个dialog流程就已经出来了,通常,还包括一些通用的内容

  • 核心数据加载

我们传给dialog的不是详细数据,而是id,在获取id时,整个页面会进入到loading状态

  • 核心数据重试

loading加载,但失败了,页面中会有重试的按钮,点击以后再次请求

  • 取消

不常见,但的确会有的操作

这些也可以通过流程复用等方式进行处理,这里业务并不复杂,只提醒,不处理

loader优化

将弹框转换为函数,处理以下问题

  • editVisible,editRowHandler,okHandler的割裂感

其代码如下

<template>
    <div id="app">
      <el-table :data="tableData" style="width: 100%">
        <el-table-column prop="name" label="Name" min-width="180" />
        <el-table-column label="Operations" min-width="120">
          <template #default="scope">
            <el-button link type="primary" size="small" @click.prevent="editRowHandler(scope.row)">
              编辑
            </el-button>
            <el-button link type="primary" size="small" @click.prevent="controlRowHandler(scope.row)">
              操作
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </template>
  
  <script>
  // ------------------------------ 1. 获取弹框函数
  import controlDialog from './components/control.dialog'
  import editDialog from './components/edit.dialog'
  
  
  export default {
    data() {
      return {
        tableData: [{ name: 1 }, { name: 2 }],
      }
    },
    methods: {
      getList() {
        this.tableData = [{ name: 111 }, { name: 2 }]
      },
      async editRowHandler(row) {
        // ------------------------- 2.调用
        await editDialog(row)
        // ------------------------- 3. 会写
        row.name = form.name;
      },
      async controlRowHandler(row) {
        await controlDialog(row)
      }
    }
  }
  </script>

这里的弹框函数处理起来比较麻烦 详细可见 弹框函数 一文

优化后

优化后,代码如下

  • App
<template>
    <div id="app">
      <el-table :data="tableData" style="width: 100%">
        <el-table-column prop="name" label="Name" min-width="180" />
        <el-table-column label="Operations" min-width="120">
          <template #default="scope">
            <el-button link type="primary" size="small" @click.prevent="editRowHandler(scope.row)">
              编辑
            </el-button>
            <el-button link type="primary" size="small" @click.prevent="controlRowHandler(scope.row)">
              操作
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </template>
  
  <script>
  // ------------------------------ 1. 获取弹框函数
  import controlDialog from './components/control.dialog'
  import editDialog from './components/edit.dialog'
  
  
  export default {
    data() {
      return {
        tableData: [{ name: 1 }, { name: 2 }],
      }
    },
    methods: {
      getList() {
        this.tableData = [{ name: 111 }, { name: 2 }]
      },
      async editRowHandler(row) {
        // ------------------------- 2.调用
        await editDialog(row)
        // ------------------------- 3. 会写
        row.name = form.name;
      },
      async controlRowHandler(row) {
        await controlDialog(row)
      }
    }
  }
  </script>
  • editDialog
<template>
    <el-dialog :visible.sync="value" title="编辑">
        <el-form>
            <el-form-item>
                <el-input v-model="form.name" placeholder="请输入名称"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <div class="dialog-footer">
                <el-button @click="value = false">取消</el-button>
                <el-button :loading="loading" type="primary" @click="handlerClick">
                    确定
                </el-button>
            </div>
        </template>
    </el-dialog>
</template>
<script>
import { MessageBox, Message } from 'element-ui'
import Vue from 'vue'

// --------------- utils
function handleConfirmFlowHelp() {
    const ret = {
        loading: false,
        // visible: false
    }
    Vue.observable(ret)
    ret.handleConfirmFlow = (fn) => {
        return async function (...args) {
            try {
                // 提交包含二次确定
                await MessageBox.confirm('确认提交吗?')
                ret.loading = true
                await fn.call(this, ...args)
                ret.loading = false
            } catch (error) {
                ret.loading = false
                // 统一对错误进行拦截
                if (error === 'cancel') {
                    Message.info('提交已取消')
                } else {
                    Message.error(error.message || '提交失败')
                }
            }
        }
    }
    return ret
}

// --------------- utils
const api = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}

const confirmFlowHelp = handleConfirmFlowHelp();


export default {
    props: ["visible", "raw"],
    data() {
        return {
            // ---------------------------- 1 结构初始化,不想声明需要特殊处理
            form: {
                name: ''
            }
        }
    },
    // -------------------------  2. 动态组件,不需要复用,生命周期中处理即可
    created() {
        Object.assign(this.form, { name: '' }, this.raw)
    },
    computed: {
        loading() {
            return confirmFlowHelp.loading
        },
        value: {
            get() {
                return this.visible
            },
            set(val) {
                this.$emit('update:visible', val)
            }
        },
        // ----------------------------------- 3. 返回给后端的序列化结构
        serform() {
            return {
                xxxname: this.form.name
            }
        }
    },
    methods: {
        handlerClick: confirmFlowHelp.handleConfirmFlow(async function () {
            await api(this.serform)
            this.value = false
            this.$emit("ok", this.form)
            this.$message.success('修改成功')
        })
    }
}
</script>