Vue3 出来一年多了 @vue/composition-api 也比较稳定了 最近想用 @vue/composition-api 对项目组件进行改造。
本文主要讲述对一个小组件进行重构的过程,对于 Composition API 相关知识可以去官网等渠道了解。
当然这个组件比较简单,也不是一定要重构。
首先是 Options API 的写法
<template>
<div>
<span v-if="tags && tags.length" class="tags">
<el-tag
v-for="(item, index) in tags"
type="success"
:key="item"
:closable="true"
:close-transition="false"
@close="handleCloseTag(index)"
>
{{ item }}
</el-tag>
</span>
<el-input
v-if="tagVisible"
v-model="tag"
class="tag-input"
:placeholder="placeholder"
@blur="handleAddTag()"
@keyup.enter.native="handleAddTag()"
></el-input>
<el-button
v-else
size="small"
icon="el-icon-plus"
@click="tagVisible = true"
>
添 加
</el-button>
</div>
</template>
<script>
export default {
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
watch: {
value(newValue, oldValue) {
if (newValue !== oldValue) {
this.tags = newValue
}
}
},
data() {
return { tags: this.value, tag: '', tagVisible: false }
},
methods: {
handleAddTag() {
const { tag, tags } = this
if (tag && !tags.includes(tag)) {
tags.push(tag)
}
this.tagVisible = false
this.tag = ''
},
handleCloseTag(index) {
this.tags.splice(index, 1)
}
}
}
</script>
<style lang="scss" scoped>
.tags {
.el-tag {
margin-right: 10px;
}
}
.tag-input {
width: 120px;
}
</style>
经过初步重构变成
<script>
import { defineComponent, ref, watch } from '@vue/composition-api'
export default defineComponent({
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
setup(props) {
const tags = ref(props.value)
const tag = ref('')
const tagVisible = ref(false)
watch(
() => props.value,
(newValue, oldValue) => {
if (newValue !== oldValue) {
tags.value = newValue
}
}
)
function handleAddTag() {
if (tag.value && !tags.value.includes(tag.value)) {
tags.value.push(tag.value)
}
tagVisible.value = false
tag.value = ''
}
function handleCloseTag(index) {
tags.value.splice(index, 1)
}
return {
tags,
tagVisible,
tag,
handleCloseTag,
handleAddTag
}
}
})
</script>
太多 .value 不友好稍微优化一下
function handleAddTag() {
if (tag.value && !tags.value.includes(tag.value)) {
tags.value.push(tag.value)
}
tagVisible.value = false
tag.value = ''
}
改为
function handleAddTag() {
const tagValue = tag.value
const tagsValue = tags.value
if (tagValue && !tagsValue.includes(tagValue)) {
tagsValue.push(tagValue)
}
tagVisible.value = false
tag.value = ''
}
然后根据逻辑功能来组织代码变成
<script>
import { defineComponent, ref, watch } from '@vue/composition-api'
export default defineComponent({
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
setup(props) {
const tags = ref(props.value)
watch(
() => props.value,
(newValue, oldValue) => {
if (newValue !== oldValue) {
tags.value = newValue
}
}
)
function handleCloseTag(index) {
tags.value.splice(index, 1)
}
const tag = ref('')
const tagVisible = ref(false)
function handleAddTag() {
const tagValue = tag.value
const tagsValue = tags.value
if (tagValue && !tagsValue.includes(tagValue)) {
tagsValue.push(tagValue)
}
tagVisible.value = false
tag.value = ''
}
return {
tags,
handleCloseTag,
//
tagVisible,
tag,
handleAddTag
}
}
})
</script>
提炼函数
function handleAddTag() {
const tagValue = tag.value
const tagsValue = tags.value
if (tagValue && !tagsValue.includes(tagValue)) {
tagsValue.push(tagValue)
}
tagVisible.value = false
tag.value = ''
}
改为
function addTag(tag) {
if (!tag) {
return
}
const tagsValue = tags.value
!tagsValue.includes(tag) && tagsValue.push(tag)
}
function handleAddTag() {
addTag(tag.value)
tag.value = ''
tagVisible.value = false
}
封装变量
const tags = ref(props.value)
watch(
() => props.value,
(newValue, oldValue) => {
if (newValue !== oldValue) {
tags.value = newValue
}
}
)
改为
const tags = ref(getModelValue())
watch(getModelValue, (newValue, oldValue) => {
if (newValue !== oldValue) {
tags.value = newValue
}
})
function getModelValue() {
return props.value
}
现在看起来会好一点
<script>
import { defineComponent, ref, watch } from '@vue/composition-api'
export default defineComponent({
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
setup(props) {
const tags = ref(getModelValue())
watch(getModelValue, (newValue, oldValue) => {
if (newValue !== oldValue) {
tags.value = newValue
}
})
function getModelValue() {
return props.value
}
function addTag(tag) {
if (!tag) {
return
}
const tagsValue = tags.value
!tagsValue.includes(tag) && tagsValue.push(tag)
}
function handleCloseTag(index) {
tags.value.splice(index, 1)
}
const tag = ref('')
const tagVisible = ref(false)
function handleAddTag() {
addTag(tag.value)
tag.value = ''
tagVisible.value = false
}
return {
tags,
handleCloseTag,
//
tagVisible,
tag,
handleAddTag
}
}
})
</script>
一般来说到这里就差不多了 不知道还能不能更进一步 (.value 实在难受 不知道下面这样会不会好一点)
封装了一个 dataRef
function dataRef(value) {
return {
ref: ref(value),
get() {
return this.ref.value
},
set(value) {
this.ref.value = value
}
}
}
const tags = ref(getModelValue())
改成
const Tags = dataRef(getModelValue())
用大写开头是为了使得 Tags.get() 看起来像静态方法,而且取值变量不需要重命名
const tags = Tags.get()
现在长这样
<script>
import { defineComponent, ref, watch } from '@vue/composition-api'
function dataRef(value) {
return {
ref: ref(value),
get() {
return this.ref.value
},
set(value) {
this.ref.value = value
}
}
}
export default defineComponent({
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
setup(props) {
const Tags = dataRef(getModelValue())
watch(getModelValue, (newValue, oldValue) => {
if (newValue !== oldValue) {
Tags.set(newValue)
}
})
function getModelValue() {
return props.value
}
function addTag(tag) {
if (!tag) {
return
}
const tags = Tags.get()
!tags.includes(tag) && tags.push(tag)
}
function handleCloseTag(index) {
Tags.get().splice(index, 1)
}
const Tag = dataRef('')
const TagVisible = dataRef(false)
function handleAddTag() {
addTag(Tag.get())
Tag.set('')
TagVisible.set(false)
}
return {
tags: Tags.ref,
handleCloseTag,
//
tagVisible: TagVisible.ref,
tag: Tag.ref,
handleAddTag
}
}
})
</script>
还能不能更进一步?
watch 在赋值前有个 if 判断
handleAddTag 这个函数在执行完 addTag 函数后有一个重置的操作
function dataRef(value) {
return {
ref: ref(value),
get() {
return this.ref.value
},
set(value) {
this.ref.value = value
}
}
}
改成
import clone from 'rfdc/default'
function dataRef(value, { isSkipSameValue } = { isSkipSameValue: false }) {
const _defaultValue = clone(value)
return {
ref: ref(value),
get() {
return this.ref.value
},
set(value) {
if (isSkipSameValue && this.get() === value) {
return
}
this.ref.value = value
},
reset() {
this.set(clone(_defaultValue))
}
}
}
重构完成,最终结果如下
<template>
<div>
<span v-if="tags && tags.length" class="tags">
<el-tag
v-for="(item, index) in tags"
type="success"
:key="item"
:closable="true"
:close-transition="false"
@close="handleCloseTag(index)"
>
{{ item }}
</el-tag>
</span>
<el-input
v-if="tagVisible"
v-model="tag"
class="tag-input"
:placeholder="placeholder"
@blur="handleAddTag()"
@keyup.enter.native="handleAddTag()"
></el-input>
<el-button
v-else
size="small"
icon="el-icon-plus"
@click="tagVisible = true"
>
添 加
</el-button>
</div>
</template>
<script>
import { defineComponent, ref, watch } from '@vue/composition-api'
import clone from 'rfdc/default'
function dataRef(value, { isSkipSameValue } = { isSkipSameValue: false }) {
const _defaultValue = clone(value)
return {
ref: ref(value),
get() {
return this.ref.value
},
set(value) {
if (isSkipSameValue && this.get() === value) {
return
}
this.ref.value = value
},
reset() {
this.set(clone(_defaultValue))
}
}
}
export default defineComponent({
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
setup(props) {
const Tags = dataRef(getModelValue() || [], { isSkipSameValue: true })
watch(getModelValue, Tags.set.bind(Tags))
function getModelValue() {
return props.value
}
function addTag(tag) {
if (!tag) {
return
}
const tags = Tags.get()
!tags.includes(tag) && tags.push(tag)
}
function handleCloseTag(index) {
Tags.get().splice(index, 1)
}
const Tag = dataRef('')
const TagVisible = dataRef(false)
function handleAddTag() {
addTag(Tag.get())
Tag.reset()
TagVisible.reset()
}
return {
tags: Tags.ref,
handleCloseTag,
//
tagVisible: TagVisible.ref,
tag: Tag.ref,
handleAddTag
}
}
})
</script>
<style lang="scss" scoped>
.tags {
.el-tag {
margin-right: 10px;
}
}
.tag-input {
width: 120px;
}
</style>
假如觉得 setup 内容太长,还可以继续提炼函数,setup 只留下主干
import { defineComponent, ref, watch } from '@vue/composition-api'
import clone from 'rfdc/default'
function dataRef(value, { isSkipSameValue } = { isSkipSameValue: false }) {
const _defaultValue = clone(value)
return {
ref: ref(value),
get() {
return this.ref.value
},
set(value) {
if (isSkipSameValue && this.get() === value) {
return
}
this.ref.value = value
},
reset() {
this.set(clone(_defaultValue))
}
}
}
function useTags(props) {
const Tags = dataRef(getModelValue() || [], { isSkipSameValue: true })
watch(getModelValue, Tags.set.bind(Tags))
function getModelValue() {
return props.value
}
function addTag(tag) {
if (!tag) {
return
}
const tags = Tags.get()
!tags.includes(tag) && tags.push(tag)
}
function handleCloseTag(index) {
Tags.get().splice(index, 1)
}
return { addTag, tags: Tags.ref, handleCloseTag }
}
function useTag() {
const Tag = dataRef('')
const TagVisible = dataRef(false)
return {
tag: Tag.ref,
tagVisible: TagVisible.ref,
createHandleAddTag(callback) {
return function () {
callback(Tag.get())
Tag.reset()
TagVisible.reset()
}
}
}
}
export default defineComponent({
name: 'TagsContainer',
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '' }
},
setup(props) {
const { addTag, tags, handleCloseTag } = useTags(props)
const { createHandleAddTag, tag, tagVisible } = useTag()
return {
tags,
handleCloseTag,
//
tag,
tagVisible,
handleAddTag: createHandleAddTag(addTag)
}
}
})
</script>
成为码农差不多 5 年了,前端做的比较少加起来不够一年,JQuery Backbone Angular Vue React 等等主流框架都有用来开发过项目,在掘金看了这么多文章,第一次发文,请各位多多包涵。