web 开发从早期的静态页面发展到如今复杂的单页应用(SPA),功能越来越丰富,UI 交互越来越复杂。如果没有良好的架构,代码很容易变成“意大利面条式” --- 逻辑混乱、难以维护、无法测试。
使用架构模式可以
- 分离关注点:将数据逻辑(Model) 、 界面展示(view) 和 控制逻辑(Controller/Presenter/ViewModel) 分开
- 提高可维护性:当需求变化时,只需修改一层,不影响其他部分
- 提高可测试性:Model 和 View/Presenter 可以脱离 UI 进行单元测试
- 促进团队协作:
- 前端工程师专注于 View
- 后端或逻辑工程师专注于 Model 和 Controller
- 设计师甚至可以基于 View 原型开发
- 支持复杂状态管理: 现代应用拥有大量的状态(如用户登陆、购物车、实时消息)等,架构模式帮助统一管理状态流动,避免“状态失控”
一、 架构模式
1. MVC(Model-View-Controller)
MVC 是最早出现的模式(源于 1970 年的 Smalltalk)
- Model: 负责管理应用程序的数据和业务逻辑(如数据验证、计算、储存),不依赖于 View 和 Controller
- View:负责数据的展示(如按钮、表单、页面)、仅关注“如何显示数据”,不处理业务逻辑
- Controller:作为 Model 和 View 之间的协调者,处理用户输入(如点击、输入框变更),更新 Model,并选择合适的 View 进行展示
常用于 Web 应用程序(JSP + Serlet)和桌面应用程序的开发中,如: SpringMVC、Struts、Java Swing、Qt 。
工作流程
- 用户于 View 交互(如点击按钮)
- View 将请求发送给 Controller
- Controller 处理请求,可能调用 Model 更新数据
- Model 更新后,Controller 选择一个 View 来展示结果
- View 从 Model 中获取最新数据并渲染
特点
- 传统 Web 开发中常见(如 ASP.NET MVC、 Ruby on Rails)
- View 和 Model 之间可以有联系(View 可以观察 Model 的变化)
- Controller 是核心协调者,职责较重
优点
- 结构清晰,适合初学者
- 广泛支持,框架成熟
缺点
- Controller 容易变得臃肿(“大控制器问题”)
- View 和 Model 可能存在直接依赖(如 Model 直接通知 View 更新),导致耦合度较高
- 在现代前端开发中,实时数据更新支持较弱
2. MVP(Model-View-Presenter)
MVP 是在 MVC 的基础上(1990 年代),限制了通信方式, Model 和 View 交互要通过 Presenter。 对 Model 和 View 进行解耦,提升项目的维护性和模块复用。常用于 Android 应用程序(早期)、桌面应用的开发。
- Model: 同 MVC,管理数据和业务逻辑
- View:负责展示,但更加被动,通常是一个接口,由 Presenter 驱动更新
- Presenter:完全取代 Controller,负责处理 UI 逻辑,从 Model 获取数据并格式化后传递给 View
工作流程
- 用户操作 View
- View 将事件通知 Presenter
- Presenter 调用 Model 获取或更新数据
- Model 返回数据给 Presenter
- Presenter 将处理的数据推给 View 进行显示
关键点
- View 和 Model 之间不能直接通信,所有交互通过 Presenter
- View 通常是被动的,Presenter 决定显示什么
优点
- 更利于单元测试(Presenter 不依赖 UI)
- 职责更清晰,适合复杂的 UI 逻辑
缺点
- Presenter 与 View 交互频繁时,代码量可能激增
- Presenter 可能变得很大
- 需要大量接口定义
- Presenter 依赖 View 接口,若 View 变更可能需要修改 Presenter
3. MVVM(Model-View-ViewModel)
MVVM 是现代前端/移动开发的主流模式(2005 年由微软提出,随 WPF 推广),抽离了视图、数据、逻辑,并限定 Model 和 View 只能通过 VM 进行通信。VM 通过订阅 Model 并在数据更新时同步到视图。MVVM 只是将 MVP 的 Presenter 进行改造,用 MV 代替 P ,将许多手动的数据 => 视图的操作自动化,降低了代码的复杂度,提高了可维护性。
angular、react、vue、WPF(微软推出基于 Windows 的用户界面框架)、Android Jetpack (ViewModel + LiveData + DataBinding)都应用了 mvvm 数据模型模式。
- Model:数据和业务逻辑
- View:UI 层,通过数据绑定(Data Binding)自动更新
- ViewModel:暴露数据和命令给 View,是 View 的抽象。它观察 Model 的变化,并通过绑定机制自动更新 View。
核心机制:数据绑定(Data Binding)
- View 和 ViewModel 之间通过绑定自动同步,无需手动更新 UI
- 支持双向绑定(如表单输入自动更新 ViewModel)
工作流程
- 用户与 View 交互
- View 通过绑定将变化通知 ViewModel
- ViewModel 更新 Model
- Model 变化通知 ViewModel
- ViewModel 通过绑定自动更新 View
优点
- 高度解耦,View 不需要主动获取数据
- 及大小少了模版代码(如
findViewById、setText等) - 非常适合现代前端框架(如 Angular、vue、WPF、Android 的 Jetpack Compose 前身)
缺点
- 学习曲线较陡(尤其是数据绑定和响应式编程)
- 调试可能复杂(绑定链难以追踪)
- 性能开销(绑定系统需要额外资源)
4. 三者对比
| 特性 | MVC | MVP | MVVM |
|---|---|---|---|
| View 与 Model 是否直接通信 | ✅ | ❌ | ❌ |
| 主要协调者 | Controller | Presenter | ViewModel |
| 数据更新方式 | 手动调用 | 手动调用 | 数据绑定(自动) |
| 测试性 | 一般 | 好 | 很好 |
| 使用场景 | 传统 Web 应用 | 复杂 UI 应用 | 现代响应式 UI 框架 |
| 代码量 | 中等 | 较多(接口) | 较少(绑定减少模版代码) |
二、 react 模式(嗯,我的理解有问题,只能借助 AI)
在 React + TypeScript 开发中,代码 MVVM 架构 详细职责。
- View : 是 用户界面的声明式描述,它定义“长什么样”和“绑定到哪些数据”
- ViewModel:是一个 JavaScript 对象 ,它暴露数据和命令(如函数)给 View 使用
- Model:是业务逻辑和数据(如外部服务(API 请求)、状态管理库(Redux、MobX)、领域模型)
所以,写的代码(tsx)是 View 部分
1. 简单的 tsx 代码解析
function UserView() {
/** useState 是 ViewModel 的一部分 */
const [name, setName] = useState('Tom');
const [age, setAge] = useState(86);
function changeName() {
setName('Jerry');
}
return (
<div>
<h1>古德猫宁,{name || 'Tom'} !</h1>
<p>年龄:{age}</p>
<button onclick={changeName}>更名</button>
</div>
);
}
| 代码部分 | MVVM 角色 | 说明 |
|---|---|---|
<h1>古德猫宁,{name || 'Tom'}!</h1> | View | UI 结构,使用 name 数据 |
const [name, setName] = useState('Tom') | ViewModel | 管理 UI 状态,暴露 name 和 setName |
setName('Jerry') | ViewModel 的命令 | View 触发的行为 |
return (...) | View | JSX/TSX 是对 UI 的声明,即 View |
ViewModel 是:组件中管理 UI 状态和逻辑的部分,在 React 中,ViewModel 体现:
useState提供的状态 (如name、age)useEffect处理副作用(如加载数据)- 自定义 Hook (如
useUser()、useCart()) - 事件处理函数(如
handleClick、changeName)
2. 也可以 ViewModel 与 View 分离
// ✅ ViewModel:专门管理 UI 所需的数据和逻辑
function useUserViewModel() {
const [name, setName] = useState('Tom');
const [age, setAge] = useState(86);
const updateName = (newName: string) => {
setName(newName);
};
const loadFromAPI = async () => {
const data = await fetchUser();
setName(data.name);
setAge(data.age);
};
useEffect(() => {
loadFromAPI();
}, []);
return { name, age, updateName }; // 暴露给 View 使用
}
// ✅ View:只负责展示和绑定
function UserView() {
const { name, age, updateName } = useUserViewModel();
function changeName() {
updateName('Jerry');
}
return (
<div>
<h1>古德猫宁, {name}!</h1>
<p>年龄: {age}</p>
<button onClick={changeName}>更名</button>
</div>
);
}
useUserViewModel是 ViewModelUserView是 ViewfetchUser()或 API 调用可以认为是 Model
3. 何为 Model
Model 是业务逻辑和数据源,比如:
- API 请求(
fetchUser()) - 数据库操作
- 业务规则(如“用户必须年满 18 才能注册”)
- 领域模型类(如
class User {...})
class User {
constructor(public name: string, public age: number) {}
isAdult() {
return this.age >= 18;
}
}
async function fetchUser(): Promise<User> {
const res = await api.get('/user/1');
return new User(res.name, res.age);
}
4. React 源码的角色
React 源码是实现 MVVM 机制的框架 ,它提供了:
useState,useEffect➙ 帮助创建 ViewModel- 虚拟 DOM 和渲染机制 ➙ 实现 数据绑定 (当 state 变化,自动更新 view )
- JSX 编译 ➙ 把 View 转换成可执行的 UI
所以:React 是 MVVM 的“引擎”或“运行时”,不是 ViewModel 本身
就像汽车引擎不是司机,但司机(ViewModel)通过引擎(React)控制汽车(View)
| MVVM 角色 | React 中对应 | 例子 |
|---|---|---|
| View | TSX/JSX 组件 | <h1>古德猫宁,{name || 'Tom'}</h1> |
| ViewModel | useState、useEffect,自定义组件 | const [name, setName] = useState('Tom') |
| Model | API、业务逻辑、领域模型 | fetchUser()、class User |
| 框架(绑定机制) | React 核心(虚拟 DOM、渲染) | React 源码,实现相应式更新 |
使用 TSX 描述“要显示什么”,用 Hook 管理“数据从哪里、怎么变”,React 负责“数据变了自动更新界面”。
三、 模块化
1. commonjs
- 每一个文件都是一个模块,
module.exports代表模块,require引用模块 - 运行的时候被加载,值拷贝,不会影响原来的值
- 引入是动态的,可以是变量
- 引入值是可任意改变
- 不支持异步加载
2. ES6 model
- 编译的时候加载,加载后改变是影响原值的
- 引入是静态的引入,引入必须在顶端
- 引入值是只读的
- 可异步加载
在之前外链 js 文件的时候,如果遇到 <script src="xxx.js"> 会阻止 DOM 解析,直到 js 文件加载,执行结束之后继续再进行 DOM 解析。
ES Module 文件的加载会有所不同。当使用 type=module 会默认加入 defer 属性。也就是说文件是进行异步加载的,等待 DOM 解析结束之后才会执行。
CommonJs 和 ESM 的区别
CommonJS 和 ES6 模块(ESM)是 javascript 中两种主要的模块化规范。
语法差异
- CommonJS 使用
require导入,module.exports或exports导出 - ES6 模块使用
import和export关键字
加载模式
-
CommonJS 是运行时动态加载,导入的值是值拷贝
// 运行时才能确定导入路径
const path = './' + 'module.js';
const module = require(path); -
ES6 模块是编译时静态分析,导入的值是引用
// 必须在编译时确定导入路径
import module from './module.js'; // 正确
import module from './' + 'module.js'; // 错误
执行时机
- CommonJS 模块在
require时同步执行,且只执行一次 - ES6 模块是在解析时编译,遇到
import会先加载依赖模块
作用域和 this
- CommonJS 模块的每一个模块有单独的作用域,
this指向当前模块的exports对象 - ES6 每一个模块有独立的作用域,
this是undefined
循环依赖处理
- CommonJS 通过缓存结果执行结果处理循环依赖,可能得到不完整的模块
- ES6 通过引用绑定处理循环依赖,能获取到最新值
使用环境
- CommonJs 主要用于 Node.js 环境(默认系统模块)
- ES6 模块是现在的浏览器原生的支持, 在 Node.js 中需通过 .mjs 扩展名或配置
package.json的"type": "module"启用
默认导出差异
- CommonJS 本质只有一个导出对象(module.exports)
- ES6 模块可以有一个默认导出和多个命名导出
嗯,小结
- CommonJS 更适合动态加载和 Node.js 环境,强调运行时特性
- ES6 模块更适合静态分析和浏览器环境,支持摇树(Tree-shaking)优化
- 现代浏览器开发通常使用 ES6 模块配合打包工具(Webpack、Vite 等),后端 Node.js 仍以 CommonJS 为主,但也逐渐支持 ES6 模块
CommonJS 在 Node.js 中使用广泛的原因
1. 契合 Node.js 的服务器端需求
Node.js 作为服务器端运行环境,需要处理文件系统操作、数据库交互等 I/O 密集型任务。CommonJS 采用同步加载模块的方式,这在服务器启动阶段加载模块时是合理的 —— 服务器启动时模块加载是一次性的,同步加载能让代码逻辑更清晰,避免异步加载带来的回调嵌套复杂性。
例如,Node.js 读取本地模块时,同步加载可以确保模块依赖就绪后再执行后续代码:
// 同步加载文件系统模块
const fs = require('fs');
// 确保 fs 模块已加载完成再使用
fs.readFileSync('file.txt');
2. 早期模块化方案的优势
在 ES6 模块规范推出前(2015 年),JavaScript 缺乏官方模块化标准。Node.js 2009 年诞生时,选择 CommonJS 作为模块化方案,填补了服务器端 JavaScript 代码组织的空白。
这种早期选择形成了庞大的生态系统:
- 大量 npm 包(如 Express、Lodash)基于 CommonJS 开发
- 开发者已习惯 CommonJS 的 require/exports 语法
- 工具链(如早期的构建工具)对 CommonJS 有完善支持
3. 适应动态特性和灵活性
CommonJS 支持动态导入路径,允许在运行时根据条件加载模块,这对服务端场景很实用:
// 运行时根据环境加载不同的配置
const config = require(`./config/${process.env.NODE_ENV}`);
此时,CommonJS 的模块加载是运行时执行的,模块内部可能包含条件判断、循环等逻辑,可灵活应对复杂的场景。
4. 缓存机制提升性能
CommonJS 模块加载后会被缓存,后续 require 统一模块时直接复用缓存的结果,避免重复加载和执行,这对服务器长期运行的进程很重要:
// 第一次加载会执行模块代码
const module = require('./module.js');
// 第二次加载直接使用缓存,不重复执行
const module2 = require('./module.js');
console.log(module === module2); // true
5. 与 Node.js 模块查找机制深度融合
Node.js 实现了一套完善的模块查找策略(优先核心模块、再查找 node_modules 等),而 CommonJS 规范与这一机制深度融合,让开发者可以更简洁的引用模块:
// 引用核心模块
const path = require('path');
// 引用第三方模块(自动查找 node_modules)
const lodash = require('lodash');
// 引用本地模块
const utils = require('./utils');
虽然 ES6 模块已成为 JavaScript 官方标准,Node.js 也逐步支持(通过 .mjs 或 package.json 的 "type": "module" ),但 CommonJS 仍占据主导地位:
- 大量现有项目和 npm 包依赖 CommonJs
- Node.js 对 CommonJs 的兼容性保障(可混合使用两种模式)
- 服务器端场景对动态加载的需求仍存在
其他形式的模块
CommonJS (Node.js 环境)
CommonJS 是 Node.js 采用的模块化标准规范,使用 require() 导入和 module.exports 导出:
// 导出模块 (math.js)
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
add,
multiply,
};
// 导入模块 (app.js)
const math = require('./math.js');
console.log(math.add(2, 3)); // 5
console.log(math.multiply(2, 3)); // 6
AMD(Asynchronous Module Definition)
AMD 是为浏览器设计的异步模块化规范,主要用户 RequireJS:
// 定义模块 (math.js)
define(function () {
return {
add: function (a, b) {
return a + b;
},
multiply: function (a, b) {
return a * b;
},
};
});
// 加载模块
require(['./math.js'], function (math) {
console.log(math.add(2, 3)); // 5
});
Umd(Universal module definition)
UMD 是一种通用格式,可同时支持 CommonJS、AMD 和全局变量方式:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['exports'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports);
} else {
// 全局变量
factory((root.mathModule = {}));
}
})(this, function (exports) {
exports.add = function (a, b) {
return a + b;
};
});
四、 webpack 和 vite
webpack 和 vite 在开发环境、配置复杂度、插件生态有显著的区别:
-
开发:webpack 采用的是开发模式下的热模块(HMR)来更新代码,但遇大型启动或 🧬 延迟问题;Vite 采用 ES Model 的开发服务器,只编译和缓存实际改动的模块,大幅提高了开发环境的响应速度;
-
配置复杂度:Webpack 的配置相对复杂,适合大型项目和复杂构建需求;Vite 设计上更注重开箱即用,配置较为简单,适用于简单到中型项目或需要快速开发的场景;
-
插件生态:Webpack 拥有庞大且成熟的插件生态系统,能满足各种前端开发需求;Vite 的插件生态仍在发展中,虽然兼容部分 Rollup 插件,但在数量和种类上不及 Webpack,不过,对于许多常见需求而言,Vite 已经提供了足够的支持。
-
打包效率不同:Webpack 在打包时,会把所有的模块打包成一个 bundle,这会导致初次加载速度较慢;而 Vite 则利用了浏览器对 ES Module 的原生支持,只打包和缓存实际改动的模块,从而极大提高了打包效率;
vite 的预构建是采用的 ESbuild。
以下是 VUE 的内容,应当移动或移除
组合式 api 和选项式的区别
同一套底层提供的不同的借口。选项式 API 是 在组合式 API 的基础上实现的
组合式 api 的特点
- 响应式 API 可以直接创建响应式的状态、计算属性、监听器
- 生命周期钩子 可以在生命周期的各个阶段添加逻辑
- 注入依赖 使用 vue 依赖注入系统
是以 VUe 中数据可变的、细粒度的响应系统为基础,而函数式编程通常强调数据的不可变
- 更好的逻辑复用性 通过组合函数实现更加简洁高效的逻辑复用。组合式 API 解决了 mixins 的所有缺陷
- 更灵活的代码组织 逻辑复杂的时候选项式可读性低
- 更好的类型推导 基于 Class 的 API 和选项式 API 在逻辑复用和代码组织方面有限制
- 更小的生产包体积代码压缩更好。本地变量的名字可以被压缩,但是对象的属性名则不能
组件封装时需要考虑的问题
- 需要高性能、低耦合、简洁、可扩展、可复用性、可配置性、
- 数据从父组件传入
- 在父组件中进行处理事件
- 留一个 slot
- 不依赖 Vuex
- 合理使用 scoped
- 组件职责的单一
$nextTick
vue 采用的是数据驱动的思想,但是在一定情况下,还需要操控 DOM 。dom1 数据发生变化,dom2 从 dom1 中获取的数据就会发现 dom1 的视图并没有更新获取不到 dom1 更新后的数据。
这时候就需要用到 $nextTick。在下一次 dom 更新结束之后执行指定的回调。(在数据更新后,就要更新后的 dom 进行某些操作要在 nextTick 中进行执行)如果不采用异步更新,在每次数据更新后,都会对当前组件重新渲染。为了性能考虑,vue 会在本轮数据更新后,再去异步更新视图。
响应式 双向绑定
- 响应式 数据驱动视图,单向
- 双向绑定 数据改变视图,视图也可以改变数据
vue2 对对象使用的是 Object.defineProperty 对属性的读取和修改拦截(数据劫持)。
vue2 对数组使用的是重写更新数组进行拦截(数组的变更进行了包裹)。
vue3 使用的 Proxy 代理的模式进行拦截数据,包含:属性值的读写、属性的添加、属性的删除。
vue3 通过 Reflect 对原对象进行更新数据
双向绑定
数据劫持 订阅者模式 :
- object.definedProperty() 方法来劫持各个属性的 setter、getter
- 数据变化 setter 拦截发布通知, dep 收到 observer 后通知以来给订阅者
- watcher 是 observer 和 compile 的桥梁,数据变化调用 compile
- compile 用于解析模版指令将模版指令中变量替换成数据
- observer 数据对象遍历
- compile 解析模版指令
- watch 自身实例化往属性中添加自己、自身包含 update() 、
- MVVM 作为数据绑定的入口,整合了 observer、compile、watcher 三折