携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情
前言
曾经为电商类公司做一个需求,开发一个分类列表,分别有鞋子、书籍等等;当时用 vue2 开发,当时还在校,开发的代码质量很一般,几个分类列表写在了同个组件,有533行,最近有空就对之前的项目 code review,重构了一下,做个记录。
我们应该把呈现的组件作为展示的接口,而编写不同的容器(逻辑)组件来接入该接口(列表/标题),让该接口展示数据,容器去操作数据。这种开发组件的方式的核心是将 容器 与 视图 分离。
分别是两个标题和列表,可以将标题和列表分成两个视图组件,但是我觉得作为示例有点麻烦,所以在以下的代码,我把标题和列表写在了同个组件里,而且样式也跟上面的图片不一样。
以下仅仅是作为一个示例来理解为什么用这种方式重构。
为什么
新的组件采用 vue3 + pinia 重构,我们把组件从业务逻辑中分离出来。
容器组件可以获取、处理和修改将显示给用户的数据,把数据传递给视图组件。
而视图组件,仅仅将数据与样式整合(也就是渲染)到页面展示列表,将交互事件派发到容器组件进行逻辑处理。
每个容器内的逻辑可能都不一样,但是视图组件相同,其他容器仍然可以使用这个接口,它只需要作为展示使用。
在以后的维护与测试中,我们也只需要关心功能和数据,而不需要去考虑它是如何展示的。
视图组件
视图组件通过 props 和 emit 实现上面所说的数据展示与事件派发。
cate-list.vue
<template>
<div>
<h4>{{ cateText }}</h4>
<!-- ...排序功能 -->
<ul>
<li v-for="(item, index) in cate">
{{ item }}
<button @click="handlerClick(index)">
删除
</button>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
cate: {
type: Array,
default(){ return []}
},
cateText: {
type: String,
default: ''
}
})
const emit = defineEmits(['click'])
const handlerClick = (index: number) => {
emit('click', index)
}
</script>
容器组件
数据存储到 pinia,通过触发 action 获取数据,传递到视图组件,然后在事件中通过 action 接口操作数据
store
// @/store/cate.ts
import { defineStore } from 'pinia'
export const useCate = defineStore('cate', {
state: () => ({
books: {},
}),
actions: {
async fetchBooks() {
try {
const books = await fetch('http://127.0.0.1:8080/books.json').then(res => res.json())
console.log(books)
}catch (e) {
console.log(e)
}
},
async deleteBookById() {
/**
*
* ...
*
*/
}
}
})
书籍列表
book-list.vue
<template>
<cate-list
@click="handleOnClick"
:cate="books.list"
:cateText="books.title"
/>
</template>
<script setup lang="ts">
import CateList from "./component/cate-list.vue";
import { storeToRefs } from 'pinia'
import {onMounted, ref} from "vue";
import { useCate } from '@/store/cate'
const store = useCate();
const { books } = storeToRefs(store);
onMounted(() => {
store.getBooks()
})
function handleOnClick(index) {
// ...逻辑代码
// store.deleleBookById(index)
}
defineExpose({
handleOnClick,
books
})
</script>
其他列表...
list.vue
<template>
<cate-list
@click="handleOnClick"
:cate="others.list"
:cateText="others.title"
/>
</template>
<script setup lang="ts">
import CateList from "./component/cate-list.vue";
import {onMounted, ref} from "vue";
const store = useCate();
const { others } = storeToRefs(store);
onMounted(() => {
store.getOthers()
})
function handleOnClick(index) {
console.log(index);
}
defineExpose({
handleOnClick,
others
})
</script>
template 部分几乎不需要修改。其他的逻辑代码也简单,复用性强,定义组件的数据和时间就可以;如果你传递书籍,则显示书籍列表。如果后续有其他的类别,我们也可使用该接口。
甚至为了复用性更高,我们可以把从 store 获取数据封装成一个hooks useCateStore
去获取数据,那么页面就只剩下几个 增删改查 的逻辑函数了,例如:
const [list, title] = useCateStore('other')
const handleClick = () => {/* ... */}
const handleDelete = () => {/* ... */}
const handleChange = () => {/* ... */}
const handleSort = () => {/* ... */}
总结
希望通过这篇文章,使自己对于 容器与视图分离 这种开发模式有一个更深的理解,也请大家多多指教。