关于 vue3 中使用 composition 组织代码及其逻辑复用的思考

294 阅读5分钟

关于 vue3 中使用 composition 组织代码及其逻辑复用的思考

简介

很多小伙伴从 vue2 升级到 vue3 时,看到 vue3 提供的一大堆响应式 API(ref、reactive...)啥的 可能会很头痛,官方文档只是列出了这一些 API,组合式函数写法也只是草草的写了个 案例组合式函数. 但是在实际写业务代码时你可能都不知道怎么才能组织写好代码,代码如何复用。

我想把自己在 vue3 中实践的写法分享给大家,希望大家能多一份参考,相信你就算写过一年 vue3 依旧能有一份收获

compositionAPI 组织代码

首先我要谈的时 vue3 的代码组织

针对 compositionAPI 的组织代码,我总结出了以下几点

代码组织
  1. 数据抽离:与当前组件的数据源相关全部抽出组件写在另外的 hook 文件里,并且导出数据全部用 reactive 包裹,导出时用 readonly 包裹(防止外部随意更改), 如果想更改数据源则抛出一个 updateState 函数(对于 v-model 来说,我的建议也是不要使用 v-model,而是使用 modelValue+监听 update:modelValue 手动更改)。
  2. 监听数据源的 watch 定义在 hook 函数里,而不是写在组件内去监听,因为可能会有几个组件都需要监听当前 state(这个在逻辑复用时再探讨)
  3. 当前组件只做一些事件监听处理相关、发起请求的一些与数据源无关的与组件强相关的逻辑。

在 index.vue 中:

<!-- index.vue -->
<template>
	<div>
		<p>原始数据counter:{{ state.counter }}</p>
		<p>经过处理后的数据counter:{{ doubleCounter }}</p>

		<button @click="increment">+1</button>
	</div>
</template>

<script setup>
	import { computed, onMounted } from 'vue';

	import useCounter from './use-counter.js';

	const { state, updateCounter, watchCounter } = useCounter();

	// 一些依赖与state做一些加工展示什么的就直接写在组件内,而不用写在useCounter里,因为useCounter只用来存放一些state相关的东西
	const doubleCounter = computed(() => {
		return state.counter * 2;
	});

	function increment() {
		// 手动调用更新counter的函数
		updateCounter(state.counter + 1);
	}

	onMounted(() => {
		// 监听counter的改变,
		watchCounter((val) => {
			console.log('监听counter,此时的时为:', val);
		});
	});
</script>

在 use-counter.js hook 函数中

// use-counter.js
import { reactive, readonly, watch } from 'vue';

const useCounter = function () {
	const state = reactive({
		counter: 0,
	});

	// 更新counter
	function updateCounter(val) {
		state.counter = val;
	}

	// 监听counter
	function watchCounter(callback, options = {}) {
		watch(() => state.counter, callback, options);
	}

	return {
		state: readonly(state), // 用readonly包裹,不让外界随意更改当前数据源,只能通过update函数更改

		updateCounter,

		watchCounter,
	};
};

export default useCounter;

看看!!这样组织的代码是不是非常清晰,不用把一些 ref、reactive、watch 写在组件内,整体清爽了不少。 不知道同学们发现没有,针对数据源其实用不上 ref!!,用 reactive 就已经能够覆盖 99%的场景了。

上述代码其实还是偏简单了,但是其所有的代码设计思路都是基于这种代码组织方式来是很好的体验。 比如:针对一个表格功能,其存在的数据源有以下

  • loading 加载数据时的 loading
  • list: 表格数据
  • paging 分页信息
  • sortInfo 排序信息

那么就可以写成以下 reactive

<template>
	<!-- 一些其他的组件 -->
	<table v-loading="state.loading" :data="state.list" :sort="state.sort"></table>

	<page :current-page="state.paging.currentPage" :page-size="state.paging.pageSize"> </page>

	<!-- 一些其他的组件 -->
</template>

<script setup>
	import { tableState: state } from './use-table';
</script>
// 在use-table.js中
const tableState = reactive({
  loading: false,
  list: [],
  paging: {
    currentPage: 1, // 当前页
    pageSize: 100 // 每页显示多少条
  },
  sort: {
    key: 'name', // 排序字段
    direction: 'desc', // 排序方式
  },
  ... // 一些其他数据
})

// 如果像counter那个每个属性都写一个更新函数会很麻烦,可以直接统一写一个
function updateTableState(state) {
  Object.assign(tableState, state || {});
}

那如果我们要所有数据呢,直接把默认数据源写成一个函数,再改造一下 updateTableState 函数

const tableState = reactive(getDefaultTableState());

// 默认数据
function getDefaultTableState() {
  return {
    loading: false,
    list: [],
    paging: {
      currentPage: 1, // 当前页
      pageSize: 100 // 每页显示多少条
    },
    sort: {
      key: 'name', // 排序字段
      direction: 'desc', // 排序方式
    },
    ... // 一些其他数据
  }
}

// 如果像counter那样每个属性都写一个更新函数会很麻烦,可以直接统一写一个
function updateTableState(state) {
  // 如果state为空则代表重置数据,否则为更新数据
  Object.assign(tableState, state ? getDefaultTableState() : state);
}

:ok: 看看是不是非常清晰,可能还会有很多同学说你这个例子太简单了!!! 好吧。我承认还是有些简单,那不妨我基于上面的表格逻辑再加一些逻辑 增加以下:

  • 增加可拖动排序,拖动排序主要实现两个功能
    • limitSort(限制那些行能拖拽)
    • sortEnd(拖拽结束、调用接口)
  • 增加右键行出现菜单,菜单有如下选项
    • 复制(会调用接口)
    • 编辑(会调用接口)
    • 删除(会调用接口)

分析: !!!这里是一个非常重要的思考方法 排序和右键菜单功能与 index.vue 不是强关联(仔细想一下), 如果把排序和右键菜单功能都在写 index.vue 里是不是不太合适, 如果像这样的功能过多写在 index.vue 里怕是要上 1000 行代码?那我们和不如写在另外的文件呢? 这种属于表格功能但又不是与表格强耦合的功能代码,封装这样的功能时我习惯称之为 helper-hook

那我们何不如增加两个文件把逻辑写在 helper-hook 里边 如果这两个 helper-hook 需要依赖什么数据直接传入就好了

  • sort-helper.js
  • menu-helper.js
// sort-helper.js
function useSortHelper({ state, updateList }) {
	function limitSort() {
		// ...
	}

	function sortEnd() {
		// ...
		const sortSuccessful = fetchSort(state); // fetchSort是一个排序接口

		// 排序成功则更新表格数据
		if (sortSuccessful) {
			updateList();
		}
	}

	return {
		limitSort,
		sortEnd,
	};
}
// menu-helper.js
function useMenuHelper({ state, updateList }) {
	function copyRow() {
		// ...
		const copySuccessful = fetchCopyRow(state); // fetchCopyRow是一个排序接口

		// 复制成功则更新表格数据
		if (copySuccessful) {
			updateList();
		}
	}

	function editRow() {
		// ...
	}

	function delRow() {
		// ...
	}

	const action = [
		{ key: 'copy', action: copyRow },
		{ key: 'edit', action: editRow },
		{ key: 'del', action: delRow },
	];

	return {
		action,
	};
}

在 index.vue 文件里

<template>
	<!-- 一些其他的组件 -->

	<!-- 假如table内部实现了右键展示action的菜单选项 -->
	<table
		v-loading="state.loading"
		:data="state.list"
		:sort="state.sort"
		:limit-sort="limitSort"
		:action="menuActions"
		@sort-end="sortEnd"
	></table>

	<page :current-page="state.paging.currentPage" :page-size="state.paging.pageSize"> </page>

	<!-- 一些其他的组件 -->
</template>

<script setup>
	import { tableState: state, updateTableState } from './use-table';

	import useSortHelper from './sort-helper';
	import useMenuHelper from './menu-helper';

	const { limitSort, sortEnd } = useSortHelper({ state, updateList });
	const { menuActions } = useMenuHelper({ state, updateList });

	function updateList() {
	  updateTableState({ list: fetchList }); // fetchList是一个请求接口的函数
	}
</script>

看,是不是清晰明了多了!

其实还有一些组织技巧和非常重要的逻辑复用需要讲的!!! 但是这个写文章太麻烦了,就写到这就花了我接近两小时的时间!!!人麻了

打算下次再说说如果逻辑复用的问题

有不懂的小伙伴可以留言,看到我会回答的