1. 一个面试现场,把"模块化"和"组件化"的混乱暴露得很彻底
先讲一个我最近真实遇到的场景。面试一个自称有五年经验的前端候选人,我问了一道自认为很基础的问题:"你怎么理解模块化和组件化的区别?"对方沉默了几秒,然后说:"模块化就是按功能把代码拆开,组件化就是把页面拆成一块一块的,用的时候拼起来。"我追问:"那一个方法应该抽成模块还是一个组件?"他愣住了,最后憋出一句:"看情况吧......一般是能用组件就用组件,抽成模块的话好像没啥概念。"
这个回答不算错,但基本上等于没回答。更让我意外的是,这个问题在团队内部讨论时也炸了锅——有人觉得组件化就是模块化的一种实现,有人说组件是模块的扩展,还有人干脆认为"爱怎么叫就怎么叫,能跑就行"。说实话,工作了这么多年,我见过太多项目死在"模块化"和"组件化"这两个词的边界不清上:有人把组件库当成万能药,什么逻辑都往组件里塞;有人把工具函数拆得比页面还碎,结果依赖关系一团乱麻。今天就借这篇文章,把这两个概念从头到尾彻底掰开揉碎讲清楚。
先说结论,方便你带着框架往下看:
- 模块化(Modularity):面向代码组织,核心解决的是"代码怎么拆、依赖怎么管、复杂度怎么降"的问题。一个模块就是一个封装了特定功能的代码单元,它可以是纯函数、类、一组相关的常量和工具方法,也可以是一个数据请求层、一个状态存储。
- 组件化(Componentization):面向界面构建,核心解决的是"UI怎么复用、交互怎么封装、页面怎么拼装"的问题。一个组件就是包含"结构+样式+行为"的界面单元,它负责一块可独立呈现和交互的UI。
下面是全文的展开逻辑:先追根溯源,看这两条路线在前端演进史上是怎么分岔的;再逐层拆解本质区别;然后用真实代码和工程案例说明落地时的不同选择;最后聊一聊面试考察点和我在项目里踩过的坑。这篇文章适合基础薄弱的新手,也适合那些"用了很多年但说不出所以然"的开发者——尤其是准备跳槽的同学,把这个概念吃透,比多背十个API有价值得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 追根溯源:两条演进路线是怎么分道扬镳的
要理解两个概念的本质区别,最有效的方式不是背定义,而是看历史。前端这两个词的来源本来就不同,它们是为了解决不同的问题而诞生的。
2.1 模块化的演进:一场"全局变量灾难"大逃杀
早期的JavaScript是没有什么模块概念的。你写一个网页,通常就是在HTML里塞好几个script标签,让它们按顺序加载。问题在哪儿?所有文件共享同一个全局作用域,一旦项目大到一定程度,变量冲突、依赖顺序混乱、命名污染这些问题就像定时炸弹一样埋在你的代码里。我记得早年在jQuery时代干活,最怕的就是两个插件都定义了$对象下的同名方法,一加载就报错,排查起来能把人折磨疯。
后来社区开始自救。第一站是IIFE(立即执行函数表达式)+闭包,把不想暴露的内部变量藏进函数作用域里,只通过window挂载一个出口,这就是经典的"模块模式":
javascript复制var myModule = (function () {
var privateCount = 0;
function increment() {
privateCount++;
}
return {
increment: increment,
getCount: function () {
return privateCount;
}
};
})();
但这只是单文件内部的"自封装",跨文件还是要靠全局变量传递。接着,CommonJS在Node.js端落地,用require和module.exports实现了服务端JavaScript真正的模块系统。可它在浏览器端水土不服——require是同步的,没法直接用。于是前端社区又搞出了AMD(代表作RequireJS),用异步加载的方式解决这个痛点。这个阶段说白了,就是大家手忙脚乱地解决同一个问题:怎么把一大坨代码拆成互相依赖又互不污染的独立单元,并且让它们按需加载。
直到ES2015发布,ES Module成为语言规范,import和export终于成为一等公民,这个"模块大战"才逐渐落幕。再往后,webpack、Vite这些构建工具把ES Module的静态分析能力发挥到极致,实现了Tree Shaking、代码分割。现在更是出现了Module Federation这样的方案,可以让多个独立应用在运行时共享模块,把模块化的边界从"单应用内部"提升到了"多应用之间"。
回头看,模块化解决的核心矛盾始终没变:**代码太复杂、依赖太混乱、耦合太严重。**它的衡量标准是:依赖是否清晰、职责是否单一、能否独立测试和独立替换。
2.2 组件化的演进:从"整页模板"到"可拼装的积木"
组件化的根源不在JavaScript,而在UI的复用需求。早年的网站是后端模板一统天下——JSP、PHP、ASP,一个页面就是一个完整的模板文件,HTML、CSS、内联脚本混在一起,想复用页面的一部分?很简单,用include去include同一段模板代码。但这只是模板片段的复用,谈不上独立的交互状态,一个页面里任何一次操作刷新,整页都得重新渲染。
转折点在2010年代。AngularJS的directive概念第一次让开发者可以把"一块HTML+对应的作用域和逻辑"封装成一个自定义标签,这已经非常接近现代组件的雏形了。紧接着,React横空出世,带来了革命性的"以组件为单位的UI构建模型":你用函数或类描述一个组件的状态和渲染输出,React负责在状态变化时高效更新DOM。Vue则用**单文件组件(SFC)**把template、script、style整合进一个.vue文件,从文件组织层面就把组件做成了天然的复用单元。
这个阶段的变化是本质性的:**渲染过程从"整页面刷新"变成了"组件树增量更新",复用粒度从"模板片段"变成了"带状态、带生命周期、可独立通信的界面单元"。**随后组件库爆发(Ant Design、Element Plus)、状态管理方案出现(Redux、Pinia)、组件设计模式成熟(受控组件、组合式组件、Render Props),组件化体系一步步完善,最终成为现代前端框架的核心抽象。
2.3 两条路线的交汇点:组件内部也需要模块化
这里必须强调一个很多人忽略的交汇点:**组件化和模块化并不互斥,它们往往嵌套在一起。**一个Vue组件内部,完全可以通过import引入一堆工具模块、API请求模块、状态管理模块来支撑自己的逻辑;一个React组件也可以拆分成多个hook模块和子组件。组件是"界面的积木",模块是"代码的积木",组件解决的是"屏幕上这一块怎么呈现、怎么交互",模块解决的是"这些功能逻辑怎么组织、怎么复用"。
理解了这两条路线的分岔与交汇,"组件化是模块化的一种实现"这种说法就不攻自破了——它们的目标维度根本不同。接下来我用四个维度把本质区别彻底拆透。
3. 本质区别拆解:关注点、抽象层次、组织方式和复用载体完全不在一个维度
很多人搞不懂模块化和组件化的区别,是因为总在一个维度里找答案。其实这是两套完全不同的"切分哲学"。我从四个维度给你展开。
3.1 关注点不同:一个是"逻辑复杂度",一个是"界面复杂度"
模块化关心的永远是代码层面的问题:这段逻辑要不要拆出来?拆出来之后它和谁的依赖关系是什么?它是否可以在别的地方复用?比如你写了一个formatDate函数,它不涉及任何UI,只是纯粹地把时间对象转成字符串,这就是一个典型的模块。它的存在是为了降低代码的认知负担和维护成本——把特定职责的代码从大文件里拎出来,让每个文件只做一件事。
组件化关心的是界面和交互层面的问题:这组HTML+样式+交互行为是不是一个完整的单元?它接收什么属性、对外触发什么事件、内部有没有自己的状态?比如一个用户头像组件,它展示头像图片、鼠标悬停显示昵称、点击跳转个人主页。它的存在是为了实现界面的复用和交互的隔离——让页面可以通过拼装积木一样的方式搭出来。
两者关注的对象有本质差异:模块的输入输出是数据,组件的输入输出是属性和事件(本质上也是数据,但最终呈现的是UI)。模块从逻辑层解决问题,组件从表现层和交互层解决问题。
3.2 抽象层次不同:一个是"代码单元",一个是"业务单元"
模块化是纯代码层面的抽象。一个模块可以小到是一个单函数,也可以大到一个服务层(比如api/user.js管理一组用户相关的请求)。它不关心这段代码最终呈现在屏幕上的效果,甚至不关心它有没有UI。如果你愿意,你完全可以在Node.js的脚本里用模块化组织你的数据清洗逻辑,而这里没有任何页面。
组件化是业务和技术结合的抽象。一个组件天然带有"可见"的属性——它要渲染成DOM,要响应交互,要出现在某个页面的某个位置上。它的拆分维度往往是业务语义:在电商项目里,"商品卡片""购物车按钮""价格展示"是组件;在后台管理项目里,"表格""表单弹窗""侧边菜单"是组件。组件不是纯技术封装,它是对"业务界面场景"的映射。
提示:这一点在面试里很关键。你说"模块化是代码层面的事,组件化是界面层面的事",比说"模块化是js的,组件化是vue的"要高一个档次。
3.3 组织方式不同:一个是"依赖树",一个是"组件树"
模块化组织起来是一个依赖图(依赖树)。你从一个入口文件出发,跟着import语句往下走,可以画出整棵依赖树。这棵树的节点是模块,边是引用关系。它的约束是:依赖关系要清晰、不能有循环依赖、同一层的模块尽量不互相引用以免形成网状混乱。
组件化组织起来是一个组件树。从根组件开始,一层层嵌套子组件,最终铺满整个页面。这棵树的节点是组件,边是父子关系(通过属性和事件通信)。它的约束是:组件层级要合理、状态提升到合适的位置、同级组件之间最好不要直接互相修改状态。
看这两种"树"的区别,你就能理解为什么面试官爱问"组件通信怎么做"而很少问"模块之间怎么通信"——模块之间传递的是数据引用(通过import),组件之间传递的是属性、事件和状态。模块的"接口"是函数签名和导出变量,组件的"接口"是Props和自定义事件。
3.4 复用载体不同:一个是"代码复制"的敌人,一个是"页面搭建"的积木
模块化的复用,本质上是对逻辑的复用。for...in循环、日期格式化、请求封装、状态管理reducer,这些没有界面形态的逻辑,抽成模块之后可以在任意地方被调用。这种复用是无感的——代码只是一个函数引用,调用方获取的是一个能力。
组件化的复用,是带着结构和样式的完整复用。你复用一个组件,不只是复用了它内部的逻辑,还复用了它的标签结构和CSS。这种复用是有"形"的——调用方在模板里写一个<UserCard :user="user" />,页面里就真的多出来一张卡片UI。组件是"可搭建页面"的最小积木,模块是"可构建系统"的最小砖块。
4. 代码层面的实锤差异:从功能封装到界面复用
理论说再多,不如直接看代码。我拿一个非常常见的场景来做对比:登录表单的校验逻辑。
4.1 如果你走"模块化"路线
你会怎么做?你会把校验规则抽成一个独立的纯函数模块,不携带任何UI代码:
javascript复制// modules/validation.js
export function isValidUsername(username) {
return /^[a-zA-Z0-9_]{4,16}$/.test(username);
}
export function isValidPassword(password) {
return password.length >= 8 && password.length <= 20;
}
export function validateLoginForm({ username, password }) {
const errors = {};
if (!isValidUsername(username)) {
errors.username = '用户名需为4-16位字母、数字或下划线';
}
if (!isValidPassword(password)) {
errors.password = '密码长度需在8到20位之间';
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
这个模块可以放在任何地方被调用:页面上用、命令行脚本用、单元测试用、node脚本用。它不关心调用它的地方长什么样,这就是模块化的价值——逻辑可以在任何执行环境下复用,且可以独立测试。你发现了吗?模块化的输出是"数据"和"结果",它的消费者是"代码"。
4.2 如果你走"组件化"路线
你会把"登录表单"这块UI连同它的校验逻辑一起封装成一个组件,里面可以再引入上面那个校验模块:
vue复制<!-- components/LoginForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="username" placeholder="用户名" />
<p v-if="errors.username" class="error">{{ errors.username }}</p>
<input v-model="password" type="password" placeholder="密码" />
<p v-if="errors.password" class="error">{{ errors.password }}</p>
<button type="submit" :disabled="loading">登录</button>
</form>
</template>
<script setup>
import { ref } from 'vue';
import { loginApi } from '@/api/auth'; // 这里引入了模块
import { validateLoginForm } from '@/utils/validation'; // 这里也引入了模块
import { useUserStore } from '@/stores/user';
const username = ref('');
const password = ref('');
const errors = ref({});
const loading = ref(false);
const userStore = useUserStore();
async function handleSubmit() {
const result = validateLoginForm({ username: username.value, password: password.value });
if (!result.isValid) {
errors.value = result.errors;
return;
}
loading.value = true;
try {
const user = await loginApi({ username: username.value, password: password.value });
userStore.setUser(user);
} finally {
loading.value = false;
}
}
</script>
<style scoped>
.error { color: red; font-size: 12px; }
</style>
这个组件的价值在哪里?它把"登录表单"这个界面场景封装成了一个整体——调用方只需要<LoginForm />一行代码,整个表单的样式、状态、校验、提交、错误提示全都有了。这就是组件化的价值——界面的复用和交互行为的隔离。组件复用者不是"代码",而是"页面搭建者",组件最终呈现在屏幕上。
4.3 一眼看懂区别
把上面两段对比放在一起,差异就非常清楚了:
| 对比维度 | 模块化 | 组件化 |
|---|---|---|
| 最小单元 | 函数、类、常量、若干相关逻辑 | 模板 + 脚本 + 样式 |
| 主要解决的问题 | 逻辑复杂度、依赖混乱 | UI复用、交互隔离 |
| 复用维度 | 功能逻辑复用 | 界面形态复用 |
| 消费者是谁 | 其他代码模块 | 页面和其他组件 |
| 是否依赖渲染层 | 不依赖(可在Node端运行) | 强依赖(必须能在浏览器渲染) |
| 组合方式 | import依赖图 | 组件树嵌套 |
| 最典型的例子 | utils模块、api模块、store模块 | 按钮组件、弹窗组件、页面组件 |
| 可测试性 | 完全独立单元测试 | 需要DOM环境(如Vitest + jsdom) |
还有一点值得注意:组件内部天然需要使用模块化的思维来组织代码。一个大型组件如果内部逻辑过多,你会把请求拆到api模块、把状态拆到store模块、把校验拆到utils模块、把太长的模板部分拆成子组件。这正好说明两者不是非此即彼,而是不同层级和维度上的工程手段。组件是"界面组织维度"上的单元,模块是"代码组织维度"上的单元。
4. 工程落地怎么选:模块化管依赖,组件化管界面,别混着用
进了真实项目,最让人头疼的不是概念本身,而是落地时怎么选。我在多个中大型项目里观察到一个规律:做得好的项目,代码层和界面层是清晰分层、各司其职的;做得烂的项目,往往是因为把组件和模块混用,导致本该是纯逻辑的东西被包在组件里,本该是组件的东西被塞进了一个巨大的模块函数。
4.1 先分层,再切分:一个高效的分层结构
以Vue项目为例,我在实际项目里比较推崇的是下面这种分层:
code复制src/
├── views/ # 页面级组件(路由级别)
│ ├── LoginView.vue
│ └── DashboardView.vue
├── components/ # 业务组件 + 通用组件
│ ├── UserCard.vue
│ ├── ChartPanel.vue
│ └── common/
│ ├── BaseButton.vue
│ └── BaseModal.vue
├── modules/ # 纯逻辑模块(不依赖UI框架)
│ ├── validation.js # 校验规则
│ ├── date.js # 日期处理
│ └── constants.js # 常量
├── api/ # 请求层模块
│ ├── user.js
│ └── order.js
├── stores/ # 状态管理模块(Pinia stores)
│ ├── userStore.js
│ └── cartStore.js
└── composables/ # 组合式函数(复用状态逻辑的模块)
├── usePagination.js
└── useDebounce.js
注意这个结构里的"components"和"modules"是分开独立的目录。modules里的文件不允许引入任何.vue组件,不允许依赖任何组件上下文,它是纯的、可独立运行的逻辑代码。composables是介于两者之间的一个特殊层——它复用"逻辑+响应式状态",但不产出模板,我倾向于把它归类为模块化的延伸。这样可以保证:业务逻辑可以被独立测试、独立重构,甚至未来如果换了UI框架,核心逻辑模块仍然可以直接复用。
4.2 选型的判断标准:问自己三个问题
在实际开发中,决定一段代码该拆成模块还是组件,我一般会问三个问题:
-
它有没有"形"? 就是说,它是否必须渲染出DOM才能体现价值。如果有,初步判断是组件。比如"用户卡片""分页器""下拉加载"这些,它们的核心价值在于界面呈现。如果它没有"形",只是一个计算、一次请求、一组规则,那就是模块。
-
它能不能脱离UI框架独立存在? 比如"日期格式化"函数,丢到Node环境里也能跑,那它必须是一个模块,而且最好不要依赖任何前端框架的API。如果它必须依赖组件生命周期才能工作(比如
onMounted里初始化图表),那它就是组件或者composable。 -
它的复用场景是"多处调用"还是"多页展示"? 如果一个函数被10个页面调用,那它是模块,因为跨页面复用的是逻辑。如果10个页面都要展示一张"用户信息卡片",那它是组件,因为跨页面复用的是界面。
用这三个问题过一遍,绝大多数场景都不会选错。剩下那些模棱两可的,我建议一律"默认模块优先"——把逻辑拆成模块更利于测试,等确实需要在多个页面展示相同UI时,再在模块之上构建组件,组件的实现可以很薄,只是把模块的数据包一层界面壳。
4.3 组件化泛滥:比模块化不足更隐蔽的灾难
在实际工程里,我见过一个比较常见的反面案例:团队为了让"代码复用率"好看,把什么鸡毛蒜皮都抽成组件。结果组件目录膨胀到几百个文件,很多组件只有几行模板,却要通过一两层props才能拿到数据。一个按钮,不同页面需求略微不一致,就加一个variant属性,这个属性一路从页面传到底层子组件,改一次全链路都要跟着动。这就是典型的"把组件当成唯一复用手段"导致的灾难。
更好的做法是:基础展示用通用组件,业务逻辑尽量下沉到模块和composable里。 比如说,多个页面都需要"根据用户权限显示不同的操作按钮",你不应该把所有情况都塞进一个PermissionButton组件里让它背上全部逻辑,而是应该先抽一个userPermission.js模块来判断权限,再做一个很薄的PermissionButton组件只负责根据模块返回的结果去渲染对应的按钮。权限判断逻辑可以被单测覆盖,按钮组件的职责也保持单一。
4.4 模块化不足:组件内部疯狂堆代码
另一个我看到的高频问题是:组件内部逻辑太厚,一个.vue文件两三千行,把所有事情都干完。这种组件名义上是组件,实际上是一个"披着组件外衣的巨石应用"。组件内部的状态、计算属性、watch、请求、事件处理全部混在一起,别说复用,连维护都困难。
解决办法也不复杂,拿到任何组件,先看看它内部有没有可以剥离的非UI逻辑,有就立刻抽成模块。比如一个"订单列表组件",计算订单总价这种逻辑应该抽到modules/order.js里;格式化金额应该抽到modules/format.js里;发送订单请求应该抽到api/order.js里。组件只负责把这些模块拼装出界面。这就是我在前面强调的:一个合格的组件,内部一定大量使用了模块化,二者是嵌套协作的关系。
5. 面试官在面什么:高频追问背后的四个考察点
因为这篇文章的选题里带着"90%的前端开发者都没搞懂",我想专门用一节来拆解面试场景。毕竟现在前端面试竞争激烈,这类概念题几乎天天被问。很多人以为自己背了"模块化是xxx,组件化是xxx"就完事了,结果被面试官一追问就露馅。
5.1 "模块化和组件化你觉得是什么关系?"——考察分层思维
这个追问的目的,不是让你背定义,而是看你有没有自己的架构分层意识。正确的应答路径应该是:
- 先分开定义各自解决的维度。
- 再说明二者的嵌套关系:组件内部也要用模块化来组织。
- 最后给出一个具体的实践例子,证明你不仅在背概念,而是真的在日常开发中用过。
举个例子:"我认为组件化和模块化是两个维度的拆解。模块化解决的是代码组织问题,是把一个复杂系统按职责拆成独立、可复用、可测试的单元;组件化解决的是界面构建问题,是把一个页面按UI和交互拆成可组合的独立单元。它们不矛盾,组件是界面维度的封装,组件内部的逻辑需要靠模块化来支撑。比如我写一个SearchSelect组件,内部会把请求数据、筛选逻辑、防抖逻辑分别抽到api/xxx.js和utils/debounce.js里,组件本身只负责数据和交互的装配。"
这比说"组件分得更细、模块分得更大"靠谱一万倍。
5.2 "有组件了,还需要模块化吗?"——考察对复用本质的理解
这个问题是杀手锏,很多人会掉进"组件是万能的"这个坑里。你需要意识到:一个带状态、带样式的组件的复用成本,要远高于一个纯函数的复用成本。 如果你只是想复用一段干巴巴的逻辑,却要因此带上一套HTML结构和样式,那就是用大炮打蚊子。
比如你有两个页面需要校验"用户名合法性",如果抽成组件,你需要给组件传值、接收事件、处理错误提示的展示位置,用起来非常别扭;如果抽成validators/isValidUsername模块,任何页面都能直接调用,再配合一个可选的FormField组件来处理UI展示,整个体验就非常干净。面试时你要把这个"好钢用在刀刃上"的逻辑讲清楚,面试官会觉得自己在跟一个真正有架构判断力的人对话。
5.3 "模块化经历了哪些阶段?"——考察历史理解和底层积累
这个题在要求你从script标签、IIFE、CommonJS、AMD、ES Module、webpack/Vite这条链路串下来。答的时候不要背干巴巴的年代线,一定要带上一两个关键洞察:
- CommonJS是同步的,浏览器原生不支持,所以前端才需要AMD和后来的构建工具。
- ES Module的
import是静态的,这让构建工具能在编译期静态分析依赖、做Tree Shaking。 - Module Federation的出现把模块化从"构建时"扩展到了"运行时",让不同应用之间也能模块共享。
这些洞察比单纯列时间轴更能展示你真正用过并理解它们。
5.4 "一个业务功能被多个页面复用,你抽象成组件还是模块?"——考察实战判断
这个题目没有标准答案,面试官真正想听的是你做出选择时考虑到了哪些因素。一个合理的回答应该是先说判断维度,再给结论:
"我会先看这个功能的核心价值是什么。如果核心价值是界面呈现和交互,比如一个商品卡片在首页、搜索页、推荐页都要展示,那肯定抽成组件;如果核心价值是逻辑处理,比如订单状态的计算、金额的换算,那抽成模块更合适。而且很多时候我会组合使用——先抽模块把逻辑沉淀下来,再基于模块做组件,这样两边的复用都能拿到。"
你看,这种回答既展示了你有判断框架,又展示了你在实际开发中具备组合使用这两个工具的能力。面试官想听到的是这种层次。
6. 我在项目里踩过的坑:组件化泛滥、模块化不足与边界混乱
最后一个部分,来点实在的踩坑分享。这些教训都是我在真实项目中付出过代价换来的,希望对你有帮助。
6.1 最深的坑:把组件当模块用,逻辑全部堆在组件里
我之前在一个中后台项目里接手过一个"客户列表页面",里面有一个巨大的CustomerTable.vue组件,光是.vue文件就有1800多行。里面的表格列定义、筛选逻辑、数据请求、权限判断、操作按钮的显示隐藏逻辑全部堆在<script setup>里。我当时就意识到,这个组件已经退化成了一个"页面壳子",完全丧失了可复用性。后来我花了两个下午,把里面的数据请求抽到了api/customer.js,把筛选、排序、状态字段映射的纯函数抽到了modules/customer.js,把操作权限判断抽到了modules/permission.js,再让CustomerTable.vue只负责把模块拿到的数据渲染出来。重构完,组件从1800行缩到400行左右,而且每一层都能单独写单元测试。从那以后,"组件内部不允许出现超过100行的业务逻辑"就写进了我们团队的代码评审规范里。
6.2 另一个坑:组件层级过深,props钻透
这个坑在多人协作的大项目里特别常见。最初一个弹窗组件自己管理状态,后来需求变了,弹窗的开关状态被提升到了父页面,再后来父页面又把它传给了另一个业务组件,最后变成了"页面拿状态 → 传给业务组件 → 业务组件传给弹窗 → 弹窗内部还要通过Props回调通知上级",一条数据链跨越四五个层级,中间任何一个组件改一下接口,全链路都要跟着排查。这种痛的本质是什么?组件化的"层级组织"被用到了极致,却忘记了模块化的"状态逻辑"本来是可以用store或composable来共享的。 后来我把弹窗的开关状态、提交状态、表单数据全部放进了stores/dialogStore.js里,组件树里任何一个组件都可以直接读写,层级瞬间清爽。
所以你看,过度组件化和模块化不足,很多时候是同一个问题的两面:没有把状态和逻辑从组件层级关系中解放出来。 组件之间的关系应该是"组合"而非"传声筒",状态和逻辑尽量往模块层下沉,组件之间才能保持松耦合。
6.3 边界混乱:模块里引入组件,组件里写纯逻辑
还有一个我经常在code review里抓的问题:有人会写一个modules/formatUserInfo.js,结果里面import了一个Avatar.vue组件来拼接用户展示信息。这就把一个纯逻辑模块变成了一个依赖渲染环境的模块,测试它的时候还必须搭浏览器环境,得不偿失。反过来,有人会在通用组件里直接写死一个业务API请求,导致这个组件完全没办法在别的项目里复用。这两种都是典型的"边界混乱"。
我的经验是:纯逻辑模块永远不允许引入任何.vue文件或组件实例;通用组件内部永远不允许直接发业务请求。 业务数据必须由外层注入(通过props)或通过模块层获取(通过api层),组件只负责展示和交互。这条规则看起来简单,但严格执行之后,项目的可维护性会有质的飞跃。
6.4 命名即边界:一个能大幅减少返工的技巧
最后分享一个我个人非常受用的实操技巧:给文件和函数命名时,就明确它的归属维度。
- 凡是模块,命名用名词短语或动词短语,体现它做的事情:
formatDate、parseQueryString、buildOrderParams、getUserById。 - 凡是组件,命名用业务名词,体现它是什么界面元素:
UserCard、OrderTable、PermissionButton、ChartPanel。
当你发现自己给一个模块命名的过程中需要加"View""Panel""Container"这种界面词汇,可能它就是组件而不是模块;当你给一个组件命名的过程中需要加"Util""Helper""Manager"这种逻辑词汇,可能你正在把模块逻辑往组件里塞。这个信号比任何规范都管用,我自己靠这个信号规避过好几次设计跑偏。
还有一个小建议:在code review时把"这是模块还是组件?它的消费者是谁?它依赖哪些环境?"作为必问项。 不需要所有人都成为架构师,但只要团队里能用这套思维审视代码,模块化和组件化的边界就不会烂到不可收拾。工程化的本质不是炫技,而是让代码在长时间、多人协作的环境里依然清晰可控。想清楚"这坨代码到底在解决哪个维度的问题",比记住任何框架API都更重要。
