在 Vue 3 的项目规模膨胀到数十个业务模块之后,很多团队会发现组件越来越难维护。面向对象设计里的 SOLID 原则提供了一套经过验证的职责划分与依赖管理思路,但在组合式 API 占据主流的工程里,它并不是简单地把逻辑写成 class。我们需要理解每条原则背后要解决的是什么耦合问题,再用 Vue 3 提供的 Composition API、TypeScript 与依赖注入机制去映射实现。

单一职责与接口隔离在 Composable 中的体现
单一职责原则要求一个模块只因一种原因发生变化。在 Vue 3 中,最自然的承载单元不是组件,而是 composable 函数。过去用 Options API 时,我们常把数据请求、表单校验、本地缓存读写全塞进同一个组件的 methods 与 data,导致组件文件超过八百行。现在可以把这些关注点拆成 useUserApi、useFormValidator、useLocalCache 三个独立函数,组件本身只负责调用与模板绑定。
接口隔离原则提醒我们不要强迫调用方依赖它不需要的字段。定义组件 props 或 composable 返回值时,应给出精确类型而不是一个大而全的对象。比如一个用户卡片只需要 id 与 name,那就用单独的 UserPreview 接口,而不是把包含密码哈希的 UserFull 直接传进去。这样当后端在 UserFull 中新增字段时,预览组件不会因为类型变更而需要跟着改。
下面示例展示如何把臃肿组件拆成两个 composable,并定义窄接口:
interface UserPreview {
id: number;
name: string;
}
export function useUserPreview(id: number) {
const user = ref<UserPreview | null>(null);
async function load() {
const res = await fetch('/api/user/' + id);
const data = await res.json();
user.value = { id: data.id, name: data.name };
}
onMounted(load);
return { user };
}
export function useUserNameFormatter(user: Ref<UserPreview | null>) {
const displayName = computed(() => {
return user.value ? user.value.name.toUpperCase() : '';
});
return { displayName };
}
依赖倒置与 inject/provide 解耦实践
依赖倒置原则强调高层模块不应依赖低层模块,二者都应依赖抽象。在 Vue 3 工程里,组件通常属于高层调度者,而具体的存储实现、网络客户端属于低层细节。如果直接在组件里写 new LocalStorageHelper(),就形成了对具体类的硬依赖。通过 app.provide 注入一个抽象接口,组件只管调用 inject('Storage') 拿到的对象,后续换成 IndexedDB 实现也不需要改组件。
这种写法也符合开闭原则的精神:对扩展开放、对修改封闭。我们定义一个 StoragePort 接口,包含 get 与 set 方法,在应用入口处提供基于 localStorage 的实例。测试时可以用内存版 fake 注入,完全不碰浏览器 API。相比把存储逻辑写成组件内的方法,这种结构让单元测试的 mock 成本大幅下降。
示例代码如下,展示如何在入口提供抽象并在子组件消费:
// storage.ts
export interface StoragePort {
get(key: string): string | null;
set(key: string, val: string): void;
}
const localStorageImpl: StoragePort = {
get: (k) => localStorage.getItem(k),
set: (k, v) => localStorage.setItem(k, v)
};
// main.ts
const app = createApp(RootComponent);
app.provide('Storage', localStorageImpl);
// Child.vue
import { inject } from 'vue';
export default {
setup() {
const storage = inject<StoragePort>('Storage')!;
storage.set('token', 'abc');
return { token: storage.get('token') };
}
};
里氏替换与组件继承重构为组合
里氏替换原则要求子类型必须能替换掉父类型且不破坏程序正确性。在 Vue 2 时代,我们常用 extends 来复用组件逻辑,但子类经常覆盖父类的 data 或 mounted 导致隐式契约被破坏。Vue 3 推荐用组合替代继承,把可替换行为做成参数或插槽传入,从而保证任何实现了相同接口的 composable 都能嵌入原流程。
例如一个列表组件依赖 fetchList 函数获取数据,我们可以把该函数作为 prop 或参数传入,而不是让子类重写 methods.fetchList。这样无论是普通 REST 获取,还是带缓存的获取,只要返回结构一致,组件渲染结果就稳定。这也避免了父类内部对子类实现的反向依赖,符合依赖倒置与里氏替换的双重约束。
以下代码演示把获取逻辑作为参数注入,而不是用继承覆盖:
import { ref, onMounted } from 'vue';
export function useListLoader(fetchList: () => Promise<any[]>) {
const items = ref<any[]>([]);
const loading = ref(false);
onMounted(async () => {
loading.value = true;
items.value = await fetchList();
loading.value = false;
});
return { items, loading };
}
// 使用方传入不同实现,均遵守同一返回类型约定
const restImpl = () => fetch('/api/items').then(r => r.json());
const cacheImpl = () => Promise.resolve([{ id: 1 }]);
在工程化落地 SOLID 时,重点不是生搬硬套类与接口,而是识别耦合点并用 Vue 3 的组合式能力将其打散。当单向数据流、依赖注入与精确类型三者配合,原本纠缠的组件自然满足五大原则,后续迭代也不会陷入改一处崩一片的困境。
Vue3SOLIDengineering_design修改时间:2026-08-17 02:06:13