ethers.js vs web3.js vs viem vs wagmi

1. 核心定位与层级关系

四者分属不同层级,服务于以太坊 Web3 前端开发的不同需求:

定位 层级 核心价值
ethers.js 以太坊 JavaScript 库 底层 提供与以太坊交互的核心功能
web3.js 以太坊 JavaScript 库 底层 以太坊基金会官方维护的全功能库
viem 以太坊 JavaScript 库 底层 现代、轻量的以太坊交互库
wagmi React Hooks 工具库 上层 基于底层库封装的前端开发工具

2. 详细功能对比

(1)ethers.js

  • 核心功能
    • 钱包管理(生成、导入、签名交易)
    • 智能合约交互(部署、调用、监听事件)
    • 网络连接(与以太坊节点通信)
    • 以太坊数据解析(地址、交易、区块等)
  • 特点
    • 轻量级(体积小,按需引入)
    • API 现代(使用 Promise,支持 async/await)
    • TypeScript 友好
    • 文档清晰,社区活跃
  • 使用场景
    • 需要直接与以太坊区块链交互的项目
    • 对包体积敏感的应用
    • 追求现代 API 设计的开发者

(2)web3.js

  • 核心功能
    • 与 ethers.js 类似,提供全面的以太坊交互功能
    • 支持更多底层 RPC 方法
    • 内置更多工具函数
  • 特点
    • 功能全面(官方维护,覆盖所有以太坊功能)
    • 相对较重(包体积大)
    • API 设计较传统(早期版本使用回调,新版支持 Promise)
    • 历史悠久,生态成熟
  • 使用场景
    • 需要使用官方全功能实现的项目
    • 传统 Web3 项目迁移
    • 对官方支持有强依赖的场景

(3)viem

  • 核心功能
    • 与 ethers.js 类似,提供以太坊交互的核心功能
    • 支持 EIP-1193 钱包标准
    • 内置链数据和地址工具
  • 特点
    • 极致轻量(体积比 ethers.js 更小)
    • 性能优化(更快的交易签名和网络请求)
    • 现代 API 设计(链式调用,类型安全)
    • 专注于前端使用场景
  • 使用场景
    • 对包体积和性能要求极高的项目
    • 现代前端框架(如 React 18+)
    • 追求最新技术栈的开发者

(4)wagmi

  • 核心功能
    • 基于 React Hooks 的以太坊交互封装
    • 钱包连接与管理(支持多种钱包)
    • 智能合约调用的 Hook 化(如 useContractRead
    • 交易发送与状态管理
    • @tanstack/react-query 集成,提供数据缓存
  • 特点
    • 前端开发友好(Hooks 化 API)
    • 支持多底层库(默认支持 viem,可选 ethers.js)
    • 与现代前端生态集成(React、TypeScript)
    • 配置灵活,扩展性强
  • 使用场景
    • React 前端 Web3 项目
    • 需要快速实现钱包连接、合约交互的场景
    • 追求开发效率和代码简洁的项目

3. 联系与依赖关系

  • 底层库之间的关系
    • ethers.jsweb3.jsviem 均为底层以太坊交互库,功能重叠但设计理念和性能不同
    • viem 是较新的库,针对前端场景优化,体积和性能优于传统库
  • wagmi 与底层库的关系
    • wagmi v1+ 默认使用 viem 作为底层库(替代之前的 ethers.js)
    • 仍支持通过配置使用 ethers.js(兼容性考虑)
    • wagmi 将底层库的复杂逻辑封装为简洁的 React Hooks
  • 功能互补
    • 底层库(ethers.js/web3.js/viem)负责与区块链交互的核心逻辑
    • 上层库(wagmi)简化前端开发,提供 Hooks 化 API 和状态管理
  • 生态组合
    • 现代 Web3 前端开发的主流组合:viem + wagmi + RainbowKit(或其他钱包连接库)
    • 传统组合:ethers.js/web3.js + 自定义钱包连接逻辑

4. 技术演进与选择建议

  • 技术趋势
    • web3.jsethers.js:追求更现代、轻量的 API
    • ethers.jsviem:进一步优化性能和体积
    • 从直接使用底层库到 wagmi:提升前端开发效率
  • 选择建议
    • 现代前端项目:推荐使用 viem + wagmi 组合
      • 优势:极致轻量、性能优异、开发效率高
    • 需要兼容性:选择 ethers.js + wagmi
      • 优势:生态成熟、文档丰富、社区支持广
    • 传统项目:继续使用 web3.js
      • 优势:官方维护、功能全面、历史兼容性好
    • 底层定制需求:直接使用 viemethers.js
      • 优势:灵活性高,可按需实现特定功能

5. 总结

  • 底层库(ethers.js/web3.js/viem):负责与以太坊区块链交互的核心逻辑,选择依据为性能、体积和 API 偏好
  • 上层库(wagmi):简化前端开发,提供 Hooks 化 API 和状态管理,大幅提升开发效率
  • 最佳实践:现代项目优先选择 viem + wagmi 组合,兼顾性能和开发体验

这种分层设计反映了 Web3 前端开发的演进趋势:从复杂的底层操作到简洁的上层抽象,从单一功能库到系统化的生态工具链。

设计模式全解:原理、UML 与项目实战

系统梳理 GoF 23 种设计模式,结合前端与 Node.js 实战代码,配 UML 类图与时序示意图辅助理解。


目录

  1. 什么是设计模式
  2. 创建型模式(5 种)
  3. 结构型模式(7 种)
  4. 行为型模式(11 种)
  5. 速查总表

一、什么是设计模式

设计模式(Design Pattern) 是软件工程中反复出现问题的通用可复用解决方案。1994 年,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(即「四人组 GoF」)在《设计模式:可复用面向对象软件的基础》中总结了 23 种经典模式,按目的分为三类:

1
2
3
创建型(Creational)  ——  解决对象的创建问题
结构型(Structural) —— 解决类/对象的组合问题
行为型(Behavioral) —— 解决对象间职责与通信问题

设计原则(SOLID)

原则 说明
S ingle Responsibility 一个类只做一件事
O pen/Closed 对扩展开放,对修改关闭
L iskov Substitution 子类可替换父类而不破坏程序
I nterface Segregation 接口最小化,不强迫实现不需要的方法
D ependency Inversion 依赖抽象,而非具体实现

二、创建型模式(5 种)

2.1 单例模式(Singleton)

意图:保证一个类只有一个实例,并提供全局访问点。

场景:全局状态管理(Store)、日志记录器、配置管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Store {
static #instance = null;

#state = {};

static getInstance() {
if (!Store.#instance) {
Store.#instance = new Store();
}
return Store.#instance;
}

getState() { return this.#state; }
setState(patch) { Object.assign(this.#state, patch); }
}

const a = Store.getInstance();
const b = Store.getInstance();
console.log(a === b); // true

2.2 工厂方法模式(Factory Method)

意图:定义创建对象的接口,让子类决定实例化哪个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Button {
render() { throw new Error('abstract'); }
}
class PrimaryButton extends Button {
render() { return `<button class="btn-primary">OK</button>`; }
}
class DangerButton extends Button {
render() { return `<button class="btn-danger">Delete</button>`; }
}

function createButton(type) {
const map = { primary: PrimaryButton, danger: DangerButton };
const Ctor = map[type];
if (!Ctor) throw new Error(`Unknown type: ${type}`);
return new Ctor();
}

createButton('primary').render(); // <button class="btn-primary">OK</button>

2.3 抽象工厂模式(Abstract Factory)

意图:提供一个接口,用于创建一系列相关或依赖对象,而不指定具体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const lightFactory = {
createButton: () => ({ type: 'button', theme: 'light' }),
createInput: () => ({ type: 'input', theme: 'light' }),
};
const darkFactory = {
createButton: () => ({ type: 'button', theme: 'dark' }),
createInput: () => ({ type: 'input', theme: 'dark' }),
};

function buildUI(factory) {
return {
button: factory.createButton(),
input: factory.createInput(),
};
}
buildUI(darkFactory); // { button: {theme:'dark'}, input: {theme:'dark'} }

2.4 建造者模式(Builder)

意图:将复杂对象的构造表示分离,同一构建过程可创建不同表示。

场景:SQL 查询构建、配置对象构造、链式调用 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class QueryBuilder {
#table = '';
#conditions = [];
#fields = ['*'];
#limit = null;

from(table) { this.#table = table; return this; }
select(...f) { this.#fields = f; return this; }
where(cond) { this.#conditions.push(cond); return this; }
limit(n) { this.#limit = n; return this; }

build() {
let sql = `SELECT ${this.#fields.join(', ')} FROM ${this.#table}`;
if (this.#conditions.length) sql += ` WHERE ${this.#conditions.join(' AND ')}`;
if (this.#limit !== null) sql += ` LIMIT ${this.#limit}`;
return sql;
}
}

new QueryBuilder()
.from('users')
.select('id', 'name')
.where('age > 18')
.where('active = 1')
.limit(10)
.build();
// SELECT id, name FROM users WHERE age > 18 AND active = 1 LIMIT 10

2.5 原型模式(Prototype)

意图:通过复制(克隆)已有实例来创建新对象,而不是通过 new

1
2
3
4
5
6
7
8
9
10
11
12
13
const userProto = {
greet() { return `Hi, I'm ${this.name}`; },
clone() { return Object.create(this); },
};

const alice = Object.create(userProto);
alice.name = 'Alice';

const bob = alice.clone();
bob.name = 'Bob';

alice.greet(); // Hi, I'm Alice
bob.greet(); // Hi, I'm Bob

三、结构型模式(7 种)

3.1 适配器模式(Adapter)

意图:将一个接口转换成调用方期望的另一个接口——解决不兼容问题。

场景:旧接口兼容、第三方 SDK 包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 旧日志库,接口为 log(msg)
class OldLogger {
log(msg) { console.log('[OLD]', msg); }
}

// 新系统要求 info / warn / error 三个方法
class LoggerAdapter {
constructor(logger) { this.logger = logger; }
info(msg) { this.logger.log(`INFO ${msg}`); }
warn(msg) { this.logger.log(`WARN ${msg}`); }
error(msg) { this.logger.log(`ERROR ${msg}`); }
}

const logger = new LoggerAdapter(new OldLogger());
logger.info('server started'); // [OLD] INFO server started

3.2 桥接模式(Bridge)

意图:将抽象部分与实现部分分离,使二者可以独立变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 实现部分:渲染引擎
const canvasRenderer = { draw: (shape) => `Canvas: draw ${shape}` };
const svgRenderer = { draw: (shape) => `SVG: draw ${shape}` };

// 抽象部分:形状
class Shape {
constructor(renderer) { this.renderer = renderer; }
}
class Circle extends Shape {
draw() { return this.renderer.draw('circle'); }
}
class Square extends Shape {
draw() { return this.renderer.draw('square'); }
}

new Circle(svgRenderer).draw(); // SVG: draw circle
new Square(canvasRenderer).draw(); // Canvas: draw square

3.3 组合模式(Composite)

意图:将对象组织成树形结构,使单个对象和组合对象的使用方式一致。

场景:文件系统、组件树、权限树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class File {
constructor(name, size) { this.name = name; this.size = size; }
getSize() { return this.size; }
}
class Folder {
constructor(name) { this.name = name; this.children = []; }
add(child) { this.children.push(child); return this; }
getSize() { return this.children.reduce((s, c) => s + c.getSize(), 0); }
}

const root = new Folder('root')
.add(new File('a.js', 10))
.add(
new Folder('src')
.add(new File('index.js', 20))
.add(new File('utils.js', 15))
);

root.getSize(); // 45

3.4 装饰器模式(Decorator)

意图:在不改变原对象的前提下,动态地为其添加功能。

场景:中间件(Koa/Express)、日志增强、缓存包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function withLogging(fn) {
return function (...args) {
console.log(`[CALL] ${fn.name}(${args})`);
const result = fn.apply(this, args);
console.log(`[RET] ${result}`);
return result;
};
}

function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// [CALL] add(2,3)
// [RET] 5

3.5 外观模式(Facade)

意图:为子系统提供统一的高层接口,隐藏内部复杂性。

场景:SDK 封装、初始化流程统一入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AuthService   { login(u, p)    { return { token: 'tok_' + u }; } }
class ProfileService { fetch(token) { return { name: 'Alice' }; } }
class LogService { record(action) { console.log('[LOG]', action); } }

class AppFacade {
#auth = new AuthService();
#profile = new ProfileService();
#log = new LogService();

async signIn(username, password) {
const { token } = this.#auth.login(username, password);
const profile = this.#profile.fetch(token);
this.#log.record(`login:${username}`);
return { token, profile };
}
}

new AppFacade().signIn('alice', 'pw');

3.6 享元模式(Flyweight)

意图:共享细粒度对象以节省内存,将对象内在状态与外在状态分离。

场景:字符渲染、粒子系统、大量相似节点池。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CharStyle {
constructor(font, size, color) {
this.font = font; this.size = size; this.color = color;
}
}

class CharStylePool {
#pool = new Map();
get(font, size, color) {
const key = `${font}-${size}-${color}`;
if (!this.#pool.has(key)) this.#pool.set(key, new CharStyle(font, size, color));
return this.#pool.get(key);
}
size() { return this.#pool.size; }
}

const pool = new CharStylePool();
// 渲染 1000 个字符,只需要极少的 CharStyle 对象
const chars = Array.from('Hello World x1000').map(ch => ({
char: ch,
style: pool.get('Arial', 14, '#333'),
}));
console.log(pool.size()); // 1(所有字符共享同一样式对象)

3.7 代理模式(Proxy)

意图:为另一个对象提供代理以控制对它的访问。

场景:懒加载、权限控制、缓存、数据校验(ES Proxy)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createValidator(target, rules) {
return new Proxy(target, {
set(obj, prop, value) {
if (rules[prop] && !rules[prop](value)) {
throw new TypeError(`Invalid value for "${prop}": ${value}`);
}
obj[prop] = value;
return true;
},
});
}

const user = createValidator({}, {
age: v => typeof v === 'number' && v >= 0 && v <= 150,
email: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
});

user.age = 25; // OK
user.email = 'a@b.com'; // OK
// user.age = -1; // TypeError: Invalid value for "age": -1

四、行为型模式(11 种)

4.1 责任链模式(Chain of Responsibility)

意图:将请求沿着处理者链传递,直到某个处理者处理它为止。

场景:中间件管道(Koa)、审批流程、事件冒泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Handler {
setNext(handler) { this.next = handler; return handler; }
handle(req) { return this.next?.handle(req); }
}

class AuthHandler extends Handler {
handle(req) {
if (!req.token) return '401 Unauthorized';
return super.handle(req);
}
}
class RoleHandler extends Handler {
handle(req) {
if (req.role !== 'admin') return '403 Forbidden';
return super.handle(req);
}
}
class BusinessHandler extends Handler {
handle(req) { return `200 OK: Hello ${req.user}`; }
}

const chain = new AuthHandler();
chain.setNext(new RoleHandler()).setNext(new BusinessHandler());

chain.handle({ token: 'tok', role: 'admin', user: 'Alice' }); // 200 OK
chain.handle({ token: 'tok', role: 'guest' }); // 403 Forbidden
chain.handle({}); // 401 Unauthorized

4.2 命令模式(Command)

意图:将操作封装为对象,支持撤销、重做、队列调度。

场景:编辑器操作历史、任务队列、宏录制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TextEditor {
#text = '';
#history = [];

execute(command) {
this.#text = command.execute(this.#text);
this.#history.push(command);
return this;
}
undo() {
const cmd = this.#history.pop();
if (cmd) this.#text = cmd.undo(this.#text);
return this;
}
value() { return this.#text; }
}

const appendCmd = (str) => ({
execute: (t) => t + str,
undo: (t) => t.slice(0, -str.length),
});

const ed = new TextEditor();
ed.execute(appendCmd('Hello')).execute(appendCmd(', World'));
ed.value(); // Hello, World
ed.undo();
ed.value(); // Hello

4.3 解释器模式(Interpreter)

意图:为语言定义文法,并实现解释器来解释该语言的句子。

场景:模板引擎、DSL 解析、规则引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 极简四则运算解释器
function interpret(expr) {
expr = expr.trim();
const addIdx = expr.lastIndexOf('+');
const subIdx = expr.lastIndexOf('-');
const pivot = Math.max(addIdx, subIdx);
if (pivot > 0) {
const left = interpret(expr.slice(0, pivot));
const right = interpret(expr.slice(pivot + 1));
return expr[pivot] === '+' ? left + right : left - right;
}
return parseFloat(expr);
}

interpret('1 + 2 + 3'); // 6
interpret('10 - 3 + 2'); // 9

4.4 迭代器模式(Iterator)

意图:提供顺序访问集合元素的方法,而不暴露其内部表示。

JavaScript 内置迭代器协议(Symbol.iterator)即此模式的语言级实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Range {
constructor(start, end, step = 1) {
this.start = start; this.end = end; this.step = step;
}
[Symbol.iterator]() {
let cur = this.start;
return {
next: () => cur <= this.end
? { value: cur += this.step, done: false }
: { done: true },
};
}
}

[...new Range(1, 10, 2)]; // [3, 5, 7, 9, 11] — 每步 +2
for (const n of new Range(0, 5)) console.log(n); // 1 2 3 4 5

4.5 中介者模式(Mediator)

意图:用中介对象封装一组对象的交互,减少对象间直接引用。

场景:聊天室、事件总线、机场调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
#listeners = new Map();
on(event, fn) { (this.#listeners.get(event) ?? this.#listeners.set(event, new Set()).get(event)).add(fn); }
off(event, fn) { this.#listeners.get(event)?.delete(fn); }
emit(event, data) { this.#listeners.get(event)?.forEach(fn => fn(data)); }
}

const bus = new EventBus();
bus.on('login', ({ user }) => console.log(`Welcome, ${user}`));
bus.on('login', ({ user }) => console.log(`Log: ${user} logged in`));
bus.emit('login', { user: 'Alice' });
// Welcome, Alice
// Log: Alice logged in

4.6 备忘录模式(Memento)

意图:在不破坏封装的前提下,捕获并外部化对象的内部状态,以便恢复。

场景:撤销/重做、草稿保存、游戏存档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FormState {
#snapshots = [];
#current = {};

set(patch) { this.#current = { ...this.#current, ...patch }; }
snapshot() { this.#snapshots.push({ ...this.#current }); }
restore() { if (this.#snapshots.length) this.#current = this.#snapshots.pop(); }
get() { return { ...this.#current }; }
}

const form = new FormState();
form.set({ name: 'Alice' });
form.snapshot();
form.set({ name: 'Bob', age: 30 });
form.get(); // { name: 'Bob', age: 30 }
form.restore();
form.get(); // { name: 'Alice' }

4.7 观察者模式(Observer)

意图:对象间一对多依赖,当一个对象状态变化时,自动通知所有依赖对象。

场景:Vue/React 响应式、DOM 事件、数据绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Subject {
#observers = new Set();
#state;

subscribe(fn) { this.#observers.add(fn); }
unsubscribe(fn) { this.#observers.delete(fn); }

setState(val) {
this.#state = val;
this.#observers.forEach(fn => fn(val));
}
getState() { return this.#state; }
}

const counter = new Subject();
counter.subscribe(v => console.log('A sees:', v));
counter.subscribe(v => console.log('B sees:', v));
counter.setState(42);
// A sees: 42
// B sees: 42

4.8 状态模式(State)

意图:允许对象在内部状态改变时改变其行为,使对象看起来修改了它的类。

场景:订单状态机、红绿灯、播放器状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const states = {
idle: { fetch: (ctx) => ctx.transition('loading') },
loading: {
resolve: (ctx, data) => { ctx.data = data; ctx.transition('success'); },
reject: (ctx, err) => { ctx.error = err; ctx.transition('error'); },
},
success: { reset: (ctx) => ctx.transition('idle') },
error: { reset: (ctx) => ctx.transition('idle') },
};

class AsyncMachine {
#state = 'idle';
data = null;
error = null;

transition(s) { this.#state = s; console.log('→', s); }
dispatch(action, payload) {
const handler = states[this.#state]?.[action];
if (!handler) return console.warn(`No "${action}" in state "${this.#state}"`);
handler(this, payload);
}
getState() { return this.#state; }
}

const m = new AsyncMachine();
m.dispatch('fetch'); // → loading
m.dispatch('resolve', { id: 1 }); // → success
m.dispatch('reset'); // → idle

4.9 策略模式(Strategy)

意图:定义一系列算法,封装每一个并使它们可互换,独立于使用者而变化。

场景:排序算法、支付方式、表单验证、折扣计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const sortStrategies = {
bubble: (arr) => {
const a = [...arr];
for (let i = 0; i < a.length; i++)
for (let j = 0; j < a.length - i - 1; j++)
if (a[j] > a[j+1]) [a[j], a[j+1]] = [a[j+1], a[j]];
return a;
},
quick: (arr) => {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = arr.slice(1).filter(x => x <= pivot);
const right = arr.slice(1).filter(x => x > pivot);
return [...sortStrategies.quick(left), pivot, ...sortStrategies.quick(right)];
},
};

class Sorter {
constructor(strategy = 'quick') { this.strategy = strategy; }
sort(arr) { return sortStrategies[this.strategy](arr); }
}

new Sorter('bubble').sort([3, 1, 4, 1, 5]); // [1, 1, 3, 4, 5]
new Sorter('quick').sort([3, 1, 4, 1, 5]); // [1, 1, 3, 4, 5]

4.10 模板方法模式(Template Method)

意图:在基类定义算法骨架,将某些步骤的实现延迟到子类,不改变结构只改变细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DataExporter {
// 模板方法:算法骨架
export(data) {
const processed = this.process(data);
const formatted = this.format(processed);
this.save(formatted);
}

process(data) { return data.filter(Boolean); } // 默认实现
format(data) { throw new Error('abstract'); } // 子类实现
save(content) { console.log('saving:', content.slice(0, 40)); }
}

class CSVExporter extends DataExporter {
format(data) { return data.map(r => Object.values(r).join(',')).join('\n'); }
}
class JSONExporter extends DataExporter {
format(data) { return JSON.stringify(data, null, 2); }
}

const rows = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
new CSVExporter().export(rows); // saving: 1,Alice\n2,Bob
new JSONExporter().export(rows); // saving: [\n {\n "id": 1,

4.11 访问者模式(Visitor)

意图:在不改变元素类的前提下,为其定义作用于这些元素的新操作。

场景:AST 遍历、报表统计、编译器优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class NumberNode  { constructor(v)    { this.value = v; } accept(v) { return v.visitNumber(this); } }
class AddNode { constructor(l, r) { this.left = l; this.right = r; } accept(v) { return v.visitAdd(this); } }
class MulNode { constructor(l, r) { this.left = l; this.right = r; } accept(v) { return v.visitMul(this); } }

// 求值访问者
const evalVisitor = {
visitNumber: (n) => n.value,
visitAdd: (n) => n.left.accept(evalVisitor) + n.right.accept(evalVisitor),
visitMul: (n) => n.left.accept(evalVisitor) * n.right.accept(evalVisitor),
};

// 打印访问者
const printVisitor = {
visitNumber: (n) => `${n.value}`,
visitAdd: (n) => `(${n.left.accept(printVisitor)} + ${n.right.accept(printVisitor)})`,
visitMul: (n) => `(${n.left.accept(printVisitor)} * ${n.right.accept(printVisitor)})`,
};

// (2 + 3) * 4
const ast = new MulNode(new AddNode(new NumberNode(2), new NumberNode(3)), new NumberNode(4));
ast.accept(evalVisitor); // 20
ast.accept(printVisitor); // ((2 + 3) * 4)

五、速查总表

分类 模式 核心意图 常见场景
创建型 单例 唯一实例 Store、Logger、Config
创建型 工厂方法 子类决定实例化 组件工厂、解析器
创建型 抽象工厂 相关对象族 主题 UI、跨平台组件
创建型 建造者 分步构建复杂对象 QueryBuilder、配置
创建型 原型 克隆已有实例 对象池、深拷贝
结构型 适配器 接口转换 旧库兼容、SDK 包装
结构型 桥接 抽象与实现分离 渲染引擎、驱动层
结构型 组合 树形统一接口 文件系统、组件树
结构型 装饰器 动态扩展功能 中间件、HOC、缓存
结构型 外观 统一高层接口 SDK 初始化入口
结构型 享元 共享细粒度对象 字符渲染、粒子池
结构型 代理 控制对象访问 懒加载、权限、校验
行为型 责任链 链式处理请求 中间件管道、审批流
行为型 命令 操作对象化 撤销/重做、任务队列
行为型 解释器 语言文法解析 模板引擎、DSL
行为型 迭代器 统一遍历接口 for…of、Generator
行为型 中介者 集中对象交互 EventBus、聊天室
行为型 备忘录 状态快照/恢复 撤销历史、草稿
行为型 观察者 自动通知订阅者 Vue 响应式、事件
行为型 状态 状态驱动行为 订单流、状态机
行为型 策略 算法可互换 支付、排序、折扣
行为型 模板方法 固定骨架/可变步骤 导出流程、生命周期
行为型 访问者 不改类增加操作 AST 遍历、报表

iOS 开发中的 JSPatch

由浅入深,从原理到源码,全面解析 JSPatch 的设计思想、实现机制与关键技术细节


一、JSPatch 是什么?

1.1 定位与目标

JSPatch 是一个 iOS 动态更新框架,由 bang590 开源。其核心能力是:在 App 内引入极小的引擎后,用 JavaScript 调用任意 Objective-C 接口,并可替换原生方法实现,从而实现:

  • 热修复:下发 JS 脚本修复线上 Bug,无需发版、无需审核
  • 动态能力:为项目动态添加模块或替换原生逻辑

1.2 与 Apple 审核政策的关系

⚠️ 重要说明:Apple 开发者协议 3.3.2 明确禁止「下载、安装或执行未包含在应用中的可执行代码」。JSPatch 通过 JS 间接调用 Runtime、替换方法 IMP,被认为绕过审核、改变应用行为,已被 Apple 明确禁止上架使用。
本文仅从原理分析、技术学习与架构设计角度展开,不鼓励在正式上架 App 中接入。

1.3 技术栈位置

1
2
3
4
5
6
7
┌─────────────────────────────────────────────────────────────┐
│ JSPatch 技术栈层次 │
├─────────────────────────────────────────────────────────────┤
│ JS 脚本层 │ require / defineClass / 业务补丁逻辑 │
│ 桥接层 │ __c() 元函数、JPEngine、JPBoxing、类型转换 │
│ 系统层 │ JavaScriptCore、Objective-C Runtime │
└─────────────────────────────────────────────────────────────┘

二、基础原理:为什么 JS 能调用 OC?

2.1 根本原因:Objective-C 的动态性

JSPatch 能通过 JS 调用和改写 OC 方法的根本原因是:Objective-C 是动态语言。在 OC 中,类与方法的查找、调用、替换都在运行时通过 Objective-C Runtime 完成,而不是在编译期写死。

因此可以:

  • 通过类名/方法名字符串反射得到类和方法
  • 替换某个类的方法实现(IMP)
  • 动态注册新类、添加方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 反射调用
Class class = NSClassFromString("UIViewController");
id vc = [[class alloc] init];
SEL sel = NSSelectorFromString("viewDidLoad");
[vc performSelector:sel];

// 替换方法实现
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, @selector(viewDidLoad), (IMP)newViewDidLoad, "v@:");

// 动态注册类
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);

结论:JSPatch 的基本原理就是——JS 把类名、方法名、参数等以字符串/结构化数据传给 OC,OC 通过 Runtime 接口完成「查找类 → 查找方法 → 调用/替换」

2.2 整体数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
JS: require('UIView').alloc().init()


__c() 元函数:解析调用链,得到 类名/方法名/参数/调用者


OC 桥接层:JPEngine 接收参数,类型转换,构造 NSInvocation


Runtime:objc_msgSend / NSInvocation 调用,返回结果


结果经 JPBoxing/包装后回传 JS,继续链式调用或使用

三、方法调用:从 JS 到 OC 的完整链路

下面以一段典型代码为例,拆解「JS 调用 OC」的五个环节:

1
2
3
4
require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)

涉及:require 机制 → JS 接口设计 → 消息传递 → 对象持有/转换 → 类型转换

3.1 require:在 JS 中「引入」OC 类

require('UIView') 的作用是:在 JS 全局作用域 上创建一个同名变量,指向一个表示 OC 类的 JS 对象。该对象用 __clsName 保存类名,并标记「这是 OC 类」。

1
2
3
4
5
6
7
8
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}

于是 require('UIView') 之后,全局有:

1
UIView === { __clsName: "UIView" }

后续 UIView.alloc() 等调用,都基于这个对象进行。

3.2 JS 接口设计:如何让 UIView.alloc() 不报错?

3.2.1 问题:JS 没有「未定义方法」的转发机制

在 JS 中,若对象没有 alloc 属性,调用 UIView.alloc() 会直接抛错。不像 OC/Lua/Ruby 有「方法缺失 → 转发」的机制。

早期思路是:在 require 时向 OC 要该类(及父类)的全部方法名,在 JS 对象上为每个方法名挂一个函数,函数内部再调 OC。这样 JS 上就有 allocinit 等「真实存在」的属性。

问题在于:一个类就有几百个方法,还要沿继承链汇总,内存暴涨,且要维护 OC→JS 的方法列表同步,难以接受。

3.2.2 方案:正则替换 + __c() 元函数(关键优化)

不改变「JS 语法」,但在 OC 执行 JS 脚本之前,用正则把所有方法调用统一替换成对 __c() 的调用,从而在 JS 侧实现「任意方法名 → 统一入口」的转发:

1
2
3
4
5
// 替换前
UIView.alloc().init()

// 替换后(示意)
UIView.__c('alloc')().__c('init')()

再给 JS 的 Object.prototype 增加 __c 方法,使任意对象(类对象、实例对象)都能走到同一套逻辑:

1
2
3
4
5
6
7
8
9
10
Object.defineProperty(Object.prototype, '__c', {
value: function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this;
return function() {
var args = Array.prototype.slice.call(arguments);
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper);
};
}
});
  • 若调用者是 OC 类(有 __clsName):把类名、方法名、参数传给 OC,由 OC 通过 Runtime 调类方法。
  • 若调用者是 OC 实例(有 __obj):把对象指针、方法名、参数传给 OC,调实例方法。

这样不需要在 JS 上枚举任何 OC 方法,内存占用大幅下降,是 JSPatch 中最重要的一步优化。

3.3 消息传递:JS 与 OC 如何互传数据?

OC 端在启动 JSPatch 时会创建 JavaScriptCoreJSContext,并在 context 上挂载 OC 实现的 Block/方法。JS 调这些方法时,参数和返回值会由 JavaScriptCore 自动在 JS 与 OC 类型之间转换(如 NSArray ↔ Array、NSString ↔ string、NSNumber ↔ number 等)。

因此,_methodFunc 只需把「类名 / 对象 / 方法名 / 参数列表」通过 context 上暴露给 JS 的函数传给 OC;OC 用 Runtime 完成调用后,再把返回值通过同一机制回传给 JS。

3.4 对象持有与转换:OC 对象在 JS 侧的表示

  • 类对象:在 JS 里就是 { __clsName: "UIView" },不涉及 OC 对象生命周期。
  • 实例对象:OC 的 id 若直接以指针形式交给 JS,JS 无法「理解」这个指针,但可以再把它传回 OC。
    为了在 JS 里识别「这是一个 OC 实例」,OC 在把对象返回给 JS 前会做一层包装,例如:
1
2
3
static NSDictionary *_wrapObj(id obj) {
return @{@"__obj": obj};
}

在 JS 侧就变成:

1
{ __obj: [OC 对象指针] }

这样在 __c() 里可以通过「是否有 __obj」判断调用者是 OC 实例,并取出 __obj 与方法名、参数一起传回 OC,完成实例方法调用。

对象生命周期:当 JS 侧有变量引用该包装对象时,OC 对象引用计数 +1;JS 侧释放后 -1,由 OC/JS 共同管理。

3.5 类型转换:参数与返回值的 OC 类型

OC 侧实际调用是通过 NSInvocation 完成的。要正确调用并拿到返回值,需要:

  1. 根据 OC 方法的 NSMethodSignature 得到每个参数的类型,把 JS 传过来的对象(如 NSNumber、NSDictionary)转成对应类型(如 intfloatCGRect 等)再传入。
  2. 根据返回值类型NSInvocation 取出返回值,再包装成 JS 可用的对象(或 JPBoxing 等)传回 JS。

例如 view.setAlpha(0.5):JS 传的是 NSNumber,OC 根据 setAlpha: 的签名得知参数是 float,于是把 NSNumber 转为 float 再调用。


四、方法替换(热修复的核心)

4.1 基础思路:替换 IMP

OC 的类方法列表里,每个方法对应一个 Method(SEL + 类型编码 + IMP)。通过 Runtime 可以:

  • 保留原 IMP:给类新增一个方法(如 ORIGviewDidLoad),其 IMP 指向原来的实现。
  • 替换原方法的 IMP:把 viewDidLoad 的 IMP 改成自定义函数,在自定义函数里调 JS 传入的实现,并在需要时再调 ORIGviewDidLoad

以替换 UIViewControllerviewDidLoad 为例(无参数情况):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void viewDidLoadIMP(id slf, SEL sel) {
// 从 JS 侧取到的函数并调用
JSValue *jsFunction = ...;
[jsFunction callWithArguments:nil];
}

Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
IMP imp = method_getImplementation(method);
char *typeDescription = (char *)method_getTypeEncoding(method);

// 原实现保留到 ORIGviewDidLoad
class_addMethod(cls, @selector(ORIGviewDidLoad), imp, typeDescription);
// viewDidLoad 指向新实现
class_replaceMethod(cls, selector, (IMP)viewDidLoadIMP, typeDescription);

这样,所有对 viewDidLoad 的调用都会走到 viewDidLoadIMP,进而执行 JS 逻辑;JS 里可通过 self.ORIGviewDidLoad() 调回原实现。

4.2 有参数时的问题:通用 IMP 如何拿到所有参数?

需要一个通用 IMP,能对「任意方法、任意参数个数与类型」都拿到参数并传给 JS。这里就出现了 32 位与 64 位 的差异。

4.2.1 32 位:va_list 取参(已不可用于 64 位)

最初用可变参数实现:

1
2
3
4
5
6
7
8
static void commonIMP(id slf, ...) {
va_list args;
va_start(args, slf);
NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:selector];
// 根据 methodSignature 的 typeEncoding 用 va_arg 逐个取出参数,组成 NSArray
// 再 [jsFunction callWithArguments:list];
va_end(args);
}

arm64 上,va_list 的 ABI 与 32 位不同,无法用上述方式正确取参,会 crash,因此 64 位必须换方案。

4.2.2 64 位:利用消息转发与 NSInvocation

OC 在「找不到方法实现」时会走消息转发链路,最终会到 -forwardInvocation:,此时会拿到一个 NSInvocation,其中已经包含了本次调用的 selector、参数类型、参数值、返回值类型。因此可以:

  1. 把要替换的方法的 IMP 改为 _objc_msgForward,这样一旦调用该方法,就会直接进入转发流程,最终进入 -forwardInvocation:
  2. 重写该类的 -forwardInvocation::在实现里判断「若是我们替换的方法」,则从 NSInvocation 里解出所有参数,调用我们新增的 _JPxxx 方法(该方法内部再调 JS);否则调原来的 ORIGforwardInvocation:,保证其他转发逻辑不受影响。
  3. 新增 ORIGviewWillAppear:_JPviewWillAppear::前者指向原 IMP,后者是「从 NSInvocation 取参并调 JS」的桥接实现。

这样在 64 位上就能通用地拿到任意方法的参数并交给 JS,无需依赖 va_list

4.3 返回值是 struct 时的注意点:_objc_msgForward_stret

部分架构下,当方法返回值是「较大的 struct」时,用的是 objc_msgSend_stret 的调用约定(返回值通过内存指针传回),若仍用 _objc_msgForward 会出错。此时需要改用 _objc_msgForward_stret
是否「special struct return」没有公开 API,JSPatch 通过 NSMethodSignaturedebugDescription 是否包含 "is special struct return? YES" 来判断,在非 arm64 上对这类方法使用 _objc_msgForward_stret

4.4 新增方法、Protocol、Property

  • 新增方法:OC 侧通过 class_addMethod 动态添加,参数与返回值类型先统一为 id(因为新增方法主要给 JS 用)。若类声明实现了某 Protocol,则从 Protocol 的方法描述里取类型信息,保证与 Protocol 一致(如 tableView:sectionForSectionIndexTitle:atIndex: 等)。
  • Property:已有属性直接通过 getter/setter 方法在 JS 里按普通方法调用即可。动态新增成员则用 objc_setAssociatedObject / objc_getAssociatedObject 模拟(因为 class_addIvar 只能在类注册前使用,无法给已有类加 ivar)。

4.5 self 与 super

  • self:在 defineClass 的实例方法执行前,把「当前实例」写入一个 JS 全局变量(如 self),方法执行完后清空,这样在 JS 里写的 self 就指向当前 OC 实例。
  • superself.super()__c() 里做特殊处理,返回一个带 __isSuper: 1 标记的对象。OC 侧若发现是 super 调用,则取父类该方法的 IMP,为当前类临时加一个方法(如 SUPER_viewDidLoad)指向该 IMP,再转调该方法,从而模拟 OC 的 super 语义。

五、扩展能力:Struct 与 C 函数

5.1 Struct 支持

JS 与 OC 之间不能直接传 C struct,需要序列化/反序列化。JSPatch 的做法是:

  • 内置:对常用类型如 NSRange、CGRect、CGSize、CGPoint 等做专门转换。
  • 可扩展:在 JS 里通过 defineStruct 声明 struct 的「名字、类型串、字段名」,OC 根据类型串按内存布局逐字段读写,再封装成 NSDictionary 与 JS 互传。这样新增 struct 不需要改 OC 代码,只需在 JS 声明布局即可(依赖当前 ABI 下 struct 内存布局稳定)。

5.2 C 函数支持

C 函数无法通过 Runtime 反射调用,因此采用「在 JSContext 上挂 OC Block 包装」的方式:在 context 上暴露与 C 函数同名的 JS 可调接口,内部转调 C 函数,并做好指针等类型转换。为避免引擎体积和启动时一次性注册过多 C 函数,设计了 JPExtension 机制:通过 +main:(JSContext *)formatJSToOC / formatOCToJS 等接口,让扩展按需注册 C 函数,JS 端通过 require('JPEngine').addExtensions(['JPMemory']) 等方式按需加载。


六、关键实现细节

6.1 JPBoxing:避免可变集合被 JavaScriptCore 自动转换

NSMutableArray / NSMutableDictionary / NSMutableString 从 OC 返回给 JS 时,JavaScriptCore 会强制转成 JS 的 Array / Object / String,导致「回到 OC 时无法再调原生的可变方法」。
解决办法:不直接返回这些对象,而是用 JPBoxing 包装一层(把 OC 对象放在 Boxing 的 property 里),返回 Boxing 实例给 JS。JS 再把这个 Boxing 传回 OC 时,OC 从 Boxing 里取出原对象,即可继续调可变方法。同时,为规则统一,NSArray/NSDictionary/NSString 也采用「默认以指针形式在 JS 侧存在,需要再调 .toJS() 转成纯 JS 类型」的策略。

6.2 nil / NSNull 的区分与链式调用

  • nil 与 NSNull:JS 的 null/undefined 传到 OC 时统一变成 nil;若需要明确表示 NSNull,在 JS 里使用全局变量 nsnull,OC 侧据此区分。
  • 链式调用:OC 里 [[obj returnNil] doSomething] 是安全的(对 nil 发消息不崩溃),但 JS 里 null 没有方法,无法写 require("JPObject").returnNil().hash()。JSPatch 用 false 表示 OC 返回的 nil:在 JS 里 false 也是对象可调方法,同时 if (!obj) 仍可用来判断「是否为 nil」。这样链式调用在 JS 侧也能安全进行。唯一的小坑是:若 OC 参数类型是 NSNumber* 而 JS 传 false,OC 会得到 nil 而非 NSNumber,需要业务侧注意。

6.3 下划线 _ 的歧义

OC 方法名用 : 分隔参数,JSPatch 在 JS 里用 单个下划线 _ 连接多参数方法名,例如:

  • setObject:forKey:setObject_forKey_

若 OC 方法名里本身带下划线(如 set_object:forKey:),就会与「参数分隔符」混淆。约定:OC 方法名中的字面下划线在 JS 里用双下划线 __ 表示,例如 set__object_forKey_。这样 OC 的 _ 与 JSPatch 的「参数分隔」可以区分开。

6.4 内存与 ARC

  • 从 NSInvocation 取参数/返回值:若用 id arg; [invocation getArgument:&arg atIndex:i];,ARC 会在退出作用域时对 arg 做 release,但 getArgument:atIndex: 并不会自动做 retain,容易造成 double release。解决方式是用 __unsafe_unretained__weak,或通过 void * + __bridge 明确所有权。
  • alloc / new / copy / mutableCopy 返回值:按 OC 约定,这些方法返回的对象调用方持有,retainCount 已 +1。从 NSInvocation 取返回值时,若 selector 是这类方法,需用 __bridge_transfer 把所有权交给 ARC,否则会泄漏。

七、核心模块与源码结构

模块 / 文件 职责
JPEngine 初始化 JSContext、注入 require/defineClass 等全局方法,执行脚本入口;提供 OC 侧与 JS 的桥接入口(如接收类名、方法名、参数并调用 Runtime)。
JPBoxing 包装 OC 对象(含 NSMutableArray/Dictionary/String、C 指针、Class 等),避免被 JavaScriptCore 自动转换或无法在 JS 侧标识类型。
JPLoader 负责从网络/本地加载、解密、执行 JS 补丁;版本管理、条件执行等。
JPExtension (JPExtension) 扩展接口:暴露 JSContext 与类型转换方法,供 C 函数、自定义 Struct 等扩展按需注册。
JS 脚本预处理 正则替换方法调用为 __c('methodName') 形式,以及 defineClass 中 self/super 的注入等。

源码阅读顺序建议:JPEngine 初始化与注入 → JS 中的 __c_methodFunc → OC 侧根据类名/对象/方法名调用 Runtime(含 NSInvocation)→ 方法替换(forwardInvocation + ORIG/JP 前缀)→ JPBoxing 与类型转换

关键调用链(OC 侧)

1
2
3
4
5
6
7
JS 调用 UIView.alloc().init()
→ _methodFunc 被调用,参数 [className="UIView", methodName="alloc", args=[]]
→ 通过 JSContext 注册的桥接函数进入 OC(如 callSelector:selectorName:arguments:...)
→ JPEngine 内根据 className 取 Class,根据 selectorName 取 SEL,组装 NSInvocation
→ 设置 target、arguments,invoke
→ 返回值经 formatOCToJS / JPBoxing 包装后回传 JS
→ JS 侧得到包装对象 { __obj: 实例 },再调用 .__c('init')() 继续链式调用

方法替换调用链(64 位)

1
2
3
4
5
6
OC 代码调用 [vc viewWillAppear:YES]
→ viewWillAppear: 的 IMP 已被改为 _objc_msgForward
→ 进入消息转发,最终到 forwardInvocation:
→ 自定义 forwardInvocation 实现中:从 NSInvocation 解出参数,调 _JPviewWillAppear:(BOOL)
→ _JPviewWillAppear: 内部把参数打包,通过 JSContext 调 JS 里 defineClass 定义的 viewWillAppear
→ 若 JS 里调 self.ORIGviewWillAppear(),则 OC 再调 ORIGviewWillAppear:,即原实现

八、设计思想总结

  1. 用字符串与 Runtime 打通 JS 与 OC
    不依赖预编译或代码生成,完全依赖「类名/方法名 + Runtime 反射 + NSInvocation」,使任意 OC 接口都能被 JS 调用和替换。

  2. 用「正则替换 + 元函数」规避 JS 语言限制
    JS 没有「未定义方法转发」,通过脚本预处理把方法调用统一成 __c('methodName'),用一层元函数模拟「消息转发」,避免在 JS 侧枚举海量方法,兼顾内存与实现复杂度。

  3. 区分 32/64 位与返回值类型
    32 位用 va_list 取参,64 位用 forwardInvocation + NSInvocation;对 special struct return 用 _objc_msgForward_stret,体现对 ABI 与底层调用约定的细致处理。

  4. 用包装类型统一「跨引擎对象」
    JPBoxing、__obj/__clsName 等,把「OC 对象/类在 JS 侧的句柄」标准化,便于在 __c() 中统一分支(类方法 / 实例方法 / super)。

  5. 扩展点清晰
    Struct 用类型串 + 键名在 JS 侧声明;C 函数通过 JPExtension 按需注册,既控制体积又保持能力可扩展。


九、合规性与替代方案

维度 说明
Apple 态度 3.3.2 禁止未包含在应用内的可执行代码的下载与执行;JSPatch 通过 JS 调 Runtime 替换方法,被视为违规。
现状 作者已不再维护,新上架 App 不建议使用。
替代思路 热修:RN/Weex/Flutter 等脚本层 OTA;紧急修复:服务端降级、开关、兜底逻辑;架构上减少对「运行时替换原生实现」的依赖。

十、小结

JSPatch 通过 Objective-C Runtime + JavaScriptCore,用「类名/方法名 + 参数」在 JS 与 OC 之间架起桥梁,并用 正则替换 + __c() 元函数 在 JS 侧实现无需枚举方法的调用转发;方法替换在 64 位上依赖 消息转发与 NSInvocation 通用地获取参数。再配合 JPBoxing、nil 用 false 表示、Struct/C 函数扩展 等细节,在技术上演进出一套完整的热修方案。理解其原理有助于掌握 Runtime、消息转发、JS–Native 桥接与 ABI 等知识;在实际项目中则应优先采用符合当前审核政策的热更新与架构方案。


本文基于 JSPatch 官方 Wiki、作者博客及公开技术资料整理,仅用于学习与原理分析。