讲解在同名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
因为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>