vue项目一次 code review,学会将视图与容器分离

402 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

前言

曾经为电商类公司做一个需求,开发一个分类列表,分别有鞋子、书籍等等;当时用 vue2 开发,当时还在校,开发的代码质量很一般,几个分类列表写在了同个组件,有533行,最近有空就对之前的项目 code review,重构了一下,做个记录。

image.png

image.png

我们应该把呈现的组件作为展示的接口,而编写不同的容器(逻辑)组件来接入该接口(列表/标题),让该接口展示数据,容器去操作数据。这种开发组件的方式的核心是将 容器 与 视图 分离。

分别是两个标题和列表,可以将标题和列表分成两个视图组件,但是我觉得作为示例有点麻烦,所以在以下的代码,我把标题和列表写在了同个组件里,而且样式也跟上面的图片不一样。

以下仅仅是作为一个示例来理解为什么用这种方式重构。

为什么

新的组件采用 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 = () => {/* ... */}

总结

希望通过这篇文章,使自己对于 容器与视图分离 这种开发模式有一个更深的理解,也请大家多多指教。