模块化与工程化
约 2119 字大约 7 分钟
2025-11-24
MVC、MVVM、MVP
思考
什么是MVC模式?
什么是MVVM模式?
什么是MVP模式?
在前端开发中,MVC、MVVM 和 MVP 是三种常见的软件设计模式,它们帮助开发者更高效地组织代码,提升代码的可维护性、可扩展性和可测试性。
MVC
概念
MVC 模式将应用程序分为三个主要部分:模型(Model)、视图(View)和控制器(Controller),各部分之间的职责明确且相互协作。
- 模型(
Model):负责处理应用程序的数据逻辑和业务规则,例如数据的存储、获取、更新和验证等操作。模型是独立于用户界面的,它可以被多个视图复用。 - 视图(
View):负责将模型中的数据以可视化的方式呈现给用户,通常是 HTML、CSS 和 JavaScript 构建的用户界面。视图只负责展示数据,不处理业务逻辑。 - 控制器(
Controller):作为模型和视图之间的桥梁,接收用户的输入(如点击事件、表单提交等),根据用户输入调用模型的相应方法进行数据处理,然后根据处理结果更新视图。
工作流程
- 用户与视图进行交互,触发相应的事件。
- 视图将事件传递给控制器。
- 控制器根据事件调用模型的方法进行数据处理。
- 模型处理完数据后,将结果返回给控制器。
- 控制器根据模型的结果更新视图。
优缺点
- 优点:职责分离,便于代码的维护和扩展;模型和视图可以独立开发和测试。
- 缺点:控制器可能会变得过于庞大,包含过多的业务逻辑;视图和模型之间的耦合度较高,修改视图可能会影响到模型,反之亦然。
- 应用:传统后端框架(如 Spring MVC)。
MVVM
概念
MVVM 模式是在 MVC 模式的基础上发展而来的,它引入了视图模型(ViewModel)的概念,通过数据绑定和视图模型来实现视图和模型的分离。
- 模型(
Model):与 MVC 中的模型类似,负责处理数据和业务逻辑。 - 视图(
View):负责用户界面的展示,通常由 HTML 和 CSS 构建。 - 视图模型(
ViewModel):是视图和模型之间的桥梁,它包含了视图的状态和行为,并通过数据绑定机制将视图和模型连接起来。视图模型负责处理视图的交互逻辑,同时将视图的变化反映到模型中,反之亦然。
工作流程
- 视图模型通过数据绑定机制将模型的数据绑定到视图上,实现数据的自动更新。
- 用户与视图进行交互,触发视图的事件。
- 视图模型监听视图的事件,并根据事件更新模型的数据。
- 模型的数据更新后,通过数据绑定机制自动更新视图。
优缺点
- 优点:通过数据绑定和视图模型实现了视图和模型的解耦,降低了代码的耦合度,提高了代码的可维护性和可测试性;开发人员可以专注于业务逻辑的实现,而不需要手动操作
DOM。 - 缺点:数据绑定的实现可能会增加一定的性能开销;对于简单的应用程序,使用 MVVM 模式可能会显得过于复杂。
- 应用:现代前端框架(如 Vue、Angular、React + 状态管理)。
MVP
概念
MVP 模式也是从 MVC 模式演变而来的,它将控制器替换为了 Presenter,进一步强调了视图和模型的分离。
- 模型(
Model):同样负责数据和业务逻辑的处理。 - 视图(
View):负责用户界面的展示,只负责显示数据和接收用户的输入,不包含任何业务逻辑。 - Presenter:作为视图和模型之间的中介,负责处理视图的交互逻辑和业务逻辑。Presenter 接收视图的事件,调用模型的方法进行数据处理,然后根据处理结果更新视图。
工作流程
- 用户与视图进行交互,触发视图的事件。
- 视图将事件传递给
Presenter。 Presenter根据事件调用模型的方法进行数据处理。- 模型处理完数据后,将结果返回给Presenter。
Presenter根据模型的结果更新视图。
优缺点
- 优点:视图和模型之间的耦合度较低,便于代码的维护和测试;
Presenter可以独立于视图和模型进行开发和测试。
CommonJS 与 ES6 导入模块的区别?
CommonJS 导入导出
CommonJS 的导入导出是 Nodejs 早期原生支持的方式,ES6 的导入导出可以通过工具如 Babel 转换为 CommonJS 的方式。
- 导入导出语法
// 导出
const name = "moduleA";
module.exports = {
name,
};
// 引入
const moduleA = require("./moduleA");- 特点
CommonJS 还可以动态导入导出,通过条件判断甚至在函数体内导出模块。
if (condition) { exports.name = "moduleA"; } else { exports.name = "moduleB"; } function exportModule() { exports.name = "moduleA"; }require 是运行时加载(同步加载),且在 require 时会加载和立即执行。
导出的是值的拷贝,但是导入的引用类型共享同一引用。
ES6 导入导出
- 导入导出语法
// 导出
export const name = "moduleA";
// 默认导出
export default "moduleA";
// 引入
import { name } from "./moduleA";- 特点
ES6 导入导出是静态的,即在编译时确定要导入的模块,而不是在运行时,这使得它可以在编译时确定依赖关系。所以它不允许像 CommonJS 那样动态导入导出模块。
ES6 导入是异步加载,不会阻塞其他加载过程。
ES6 导出的是值的引用,修改会影响所有导入该值的地方。
ESM 的设计更利于现代工具链优化和浏览器原生支持,是如今项目首推的导入导出方式。除非你有动态导入模块的场景,否则更推荐使用 ES6 导入导出的方式。
pnpm
思考
pnpm 的优势?
核心原理:全局存储 + 硬链接 + 软链接
pnpm 之所以快且省空间,核心在于它独特的 node_modules 构建方式。
- 全局存储:
- 传统的
npm/yarn会将每个项目的依赖都下载一份到各自的node_modules中。 pnpm会在全局维护一个存储库(通常在~/.pnpm-store)。所有包根据内容的哈希值存储,同一版本同一个包在磁盘上只存在一份。
- 传统的
- 硬链接:
- 当项目安装依赖时,
pnpm不会复制文件,而是从全局存储创建硬链接到项目目录下的.pnpm文件夹中。硬链接指向磁盘上的同一inode,因此不占用额外空间。
- 当项目安装依赖时,
- 软链接:
- 项目的
node_modules文件夹中,包是通过软链接指向.pnpm文件夹中的具体版本。 - 结构示例:
node_modules/ lodash -> .pnpm/[email protected]/node_modules/lodash # 符号链接(软链接) .pnpm/ [email protected]/ node_modules/ lodash/ # 真实目录,但里面的文件是硬链接到全局存储 ├── index.js # 硬链接 → ~/.pnpm-store/xxx ├── package.json # 硬链接 → ~/.pnpm-store/yyy └── ...
- 项目的
解决了什么核心痛点?
A. 幽灵依赖
- 问题: 在
npm/yarnv1 的扁平化结构中,依赖会被提升(hoist)到顶层。这意味着你可以在package.json中没有声明的情况下,通过import 'lodash'使用某个包(因为它是另一个依赖的依赖)。这会导致代码在不同环境(如 CI 或生产环境)下因依赖树不同而报错。 - pnpm 方案:
pnpm的node_modules结构是非扁平化的(虽然为了兼容性做了一些提升,但默认严格)。你只能 import 你在package.json中明确声明的依赖。这强制了依赖的显式声明,提高了项目的健壮性。
B. 磁盘空间浪费
- 问题: 如果你有 10 个项目都用
React 18,npm会存 10 份。 - pnpm 方案: 只存 1 份。对于大型 Monorepo 或多项目开发者,节省的空间可达数 GB 甚至数十 GB。
C. 安装速度慢
- 问题: 大量文件复制 IO 操作耗时。
- pnpm 方案: 硬链接操作几乎是瞬间完成的,且全局缓存命中率高,显著减少网络请求和磁盘 IO。