DApp 不是一个程序,而是一组边界。界面、钱包、节点和合约由不同主体运行,失败方式也不同。
Ethereum / DApp architecture / Lesson 22.01
一个 DApp,究竟由什么组成?
你在网页上点一次“确认”,并不是把命令直接塞进区块链。它会依次穿过 界面、接口描述、JavaScript 库、Provider、钱包、节点、EVM, 最后才可能成为所有节点共同承认的新状态。
Ethers.js 或 Web3.js 不是“区块链后端”。它们主要把开发者友好的调用翻译为 ABI 与 JSON-RPC。
读操作是本地模拟,写操作是全网状态转换。是否需要账户、签名、Gas 和确认,由这条分界线决定。
01 / ORIENTATION
先把“网页调用合约”这句话拆开
一次点击跨越了多个运行环境,也跨越了多个信任主体
Ethereum.org 把 DApp 概括为前端用户界面与去中心化网络上的智能合约的组合。 [1] 这个定义抓住了两端,却省略了中间最容易让初学者困惑的桥梁:前端如何知道合约有哪些函数, 如何把参数变成字节,如何找到节点,谁负责签名,以及结果如何回到屏幕。
浏览器 / 服务器 / 节点
前端通常在用户设备运行;可选后端在开发者服务器运行;合约由收到同一交易的以太坊节点重复执行。
开发者 / 用户 / 协议
开发者发布界面,用户的钱包保管授权能力,协议决定什么交易和状态转换有效。
状态,不是屏幕
全网达成共识的是链上状态和交易历史,不是某个网站的按钮、文案、缓存或推荐顺序。
02 / THE LAYERS
四层不是四个产品,而是四种责任
理解“谁负责什么”,比背一套技术栈更耐久
目录中的“前端应用、Ethers.js/Web3.js、智能合约、以太坊网络”可以看成四层。 真实系统还会在接口库和网络之间加入 Provider、钱包与 RPC 节点。它们不是额外的花活, 而是跨越浏览器安全边界所必需的连接机制。
LAB 01 / RESPONSIBILITY INSPECTOR
点一层,看清它能做什么、不能做什么
静态结论:前端负责表达;接口库负责编码;Provider 与钱包负责通信和授权; 合约与网络负责按共识规则执行。任一层都不能替代其他层的安全职责。
- 前端应用 / UI
- 普通的 HTML、CSS、JavaScript 或 React 等界面代码。它决定用户看见什么、输入什么、什么时候请求钱包,但不能绕过协议直接改状态。
- 接口库
- Ethers.js、Web3.js 等客户端库提供高层对象,把“调用 balanceOf”转成 ABI 字节和 RPC 请求,再把十六进制返回值解码为程序能使用的数据。
- Provider 与钱包
- Provider 是应用访问 Ethereum RPC 的接口;钱包通常在此接口背后管理账户许可与签名。EIP-1193 把浏览器中的 Provider 约束为最小、事件驱动的通用接口。[2]
- 合约与以太坊网络
- 合约是部署在地址上的 Bytecode 与持久状态;节点验证交易并在 EVM 中确定性执行,最终把有效结果纳入共享状态。
03 / CONTRACT BOUNDARY
智能合约像公开 API,但不是普通服务器
它能保持共享规则,却不能替你完成所有应用工作
把合约称为 DApp 的“后端”有助于入门,但必须马上加上限定: 它是一台由许多节点重复执行、输入与输出公开、计算昂贵、能力受限的共享状态机, 不是一台可以任意访问数据库和互联网的云服务器。[3]
- 验证
msg.sender、签名、权限、余额 - 计算确定性逻辑与资产规则
- 持久化写入合约 Storage
- 组合同步调用其他合约
- 发信号写入 Event Logs
同一前状态 → 同一输出
同一后状态 所有诚实执行客户端必须得到一致结果
- HTTP主动访问网页或第三方 API
- 秘密把普通明文永久藏在公开链上
- 定时唤醒没有交易就不会自行运行
- 大文件低成本存储图片与视频
- 主观判断知道用户“真正想做什么”
必须共同验证的事实
资产余额、所有权、抵押率、治理票数、协议参数、不可绕过的结算规则。
不值得全网重复的工作
搜索、推荐、图片、复杂报表、通知、缓存、私有数据,以及可以从链上数据重建的索引。
04 / ABI + ADDRESS
地址告诉你“找谁”,ABI 告诉你“怎么说”
两者缺一,前端都无法可靠地把人类意图变成 EVM 字节
合约部署后,网络保存的是地址、Bytecode 和状态,不保存一个供 JavaScript 直接调用的类。 编译器生成的 ABI 是一份 JSON 接口描述:它列出函数、参数、返回值、事件与自定义错误。 Solidity 的 ABI 规范定义了这些数据如何编码和解码。[4]
从函数名到 calldata
例如,用户看到的是“查询余额”,开发者写的是 balanceOf(address)。
ABI 编码器把函数签名的 Keccak-256 哈希前 4 字节作为函数选择器,
再把参数按 32 字节槽规则编码。balanceOf(address) 的选择器是
0x70a08231;节点收到的不是函数名,而是一串以它开头的 calldata。
LAB 02 / INTENT → BYTES
同一个“合约对象”,读与写会长成什么请求?
0x70a08231
eth_call
uint256,库再格式化为可读余额
{
"method": "eth_call",
"params": [{ "to": "0xToken…", "data": "0x70a08231…" }, "latest"]
}
静态结论:ABI 不执行交易,它只定义字节语言;地址不证明合约安全,它只定位某段代码; Provider 不替你判断语义,它只传递请求。
05 / LIBRARY, PROVIDER, WALLET
三个经常被叫作“Web3”的对象,其实各管一件事
库负责翻译,Provider 负责通路,钱包负责账户授权
Ethers.js / Web3.js
- 构造合约对象
- ABI 编码 / 解码
- 格式化数量与地址
- 封装 RPC 与错误
request({ method, params })
- 接收 RPC 请求
- 报告连接状态
- 报告链与账户变化
- 把消息交给钱包或节点
Signer / account authority
- 保存或隔离密钥
- 选择账户与网络
- 向用户展示请求
- 经确认后签名
EIP-1193:浏览器应用与钱包之间的窄桥
EIP-1193 的核心不是某个品牌钱包,而是一个最小 Provider 约定:
应用可以调用 request 发出 RPC 请求,并监听 connect、
disconnect、chainChanged、accountsChanged 等事件。
规范还明确提醒:网页是潜在不可信环境,Provider 对象也应按可能被对手影响来设计。
[2]
当浏览器装有多个钱包时,单一 window.ethereum 会产生覆盖与竞态。
EIP-6963 用浏览器事件让多个 EIP-1193 Provider 分别宣布自己,由用户明确选择。
[5]
Ethers.js 与 Web3.js:同一协议上的不同开发者体验
| 维度 | Ethers.js | Web3.js | 协议事实 |
|---|---|---|---|
| 读 / 写抽象 | 明确区分 Provider 与 Signer | 常从 Web3 实例与 Contract 方法进入 | 底层仍是查询、签名与 RPC |
| 合约接口 | new Contract(address, abi, runner) |
new web3.eth.Contract(abi, address) |
都依赖地址 + ABI |
| 钱包接入 | BrowserProvider 包装 EIP-1193 |
可直接使用 EIP-1193 Provider | 钱包仍掌握最终授权 |
| 2026 维护语境 | 官方 v6 文档持续维护 | 官方文档已提示库于 2025-03-04 进入 sunset | 库状态会变,RPC / ABI 概念更稳定 |
06 / READ PATH
读链不是“免费交易”,而是让一个节点替你模拟
没有全网状态变化,就没有签名、Gas 支付和上链确认
查询余额、读取公开变量、预演一次调用,通常使用 eth_call。
应用指定目标地址、calldata 与一个区块标签;节点在该区块状态上执行 EVM,
返回结果字节,但不把变化写回链上。JSON-RPC 还允许用
latest、safe、finalized 等标签明确读取哪个状态。
[7]
eth_call目标地址 + calldata + block tag
账户许可与签名
公开状态可由任何节点查询。某些模拟可以带 from,但那不是签名,也不会证明账户同意。
链上 Gas Fee
节点仍然消耗计算资源,所以 RPC 服务商可以限流或收费;只是不会形成由用户支付的链上交易费。
未来写入一定成功
模拟结束后,状态、nonce、Base Fee、预言机价格或竞争交易都可能变化;读到“可执行”不等于之后必然成功。
07 / WRITE PATH
写链是一场状态转换提案
用户签的是交易,节点验证的是规则,区块提供的是排序与确认
当调用会改变状态时,前端先构造一个交易请求;钱包把链、目标地址、ETH 数量、calldata 与费用信息 展示给用户;用户确认后,钱包才用账户授权能力签名并广播。节点逐项验证,验证者把交易纳入区块, EVM 执行成功后才产生新的共享状态与收据。
LAB 03 / REQUEST JOURNEY
在“读、写、事件回流”之间切换
- 01界面收集参数用户输入一个地址UI
- 02ABI 编码函数选择器 + 参数LIB
- 03发送 eth_call指定 latest / safe / finalizedRPC
- 04节点模拟 EVM不持久化任何变化EVM
- 05解码并渲染字节 → uint256 → 可读单位UI
静态结论:读路径止于单个节点的模拟结果;写路径必须经过用户授权、广播、区块执行和收据; 事件路径从已执行交易的日志回到应用视图。
不要把四种“成功”混在一起
08 / EVENTS & LOGS
合约不会“推送 UI”,它只把日志留在收据里
事件是链上执行留下的可检索信号,前端或索引器负责把它变成视图
Solidity 合约可以在执行中 emit 事件。日志会随交易收据进入区块,
外部应用可以通过 RPC 查询或订阅,再按 ABI 解码。Ethereum.org 将它描述为合约与前端或其他订阅应用
沟通的重要方式。[9]
eth_getLogs按地址、区块、topics 过滤“事件就是合约给浏览器发 WebSocket 消息。”
事件首先是区块中的日志;节点或索引器再把日志通过查询、轮询或订阅交给应用。
Storage、返回值与 Event 各解决什么
当前事实
合约以后还要读取、验证或更新的数据。写入昂贵,但参与后续状态转换。
本次调用结果
eth_call 能直接取回;普通已上链交易的调用者不会像同步函数那样从前端拿到返回值。
历史信号
供外部应用检索和重建视图。合约本身不能把过去日志当作普通 Storage 随意读取。
09 / PRODUCTION ARCHITECTURE
最小 DApp 只有四层,生产 DApp 往往还有六个“辅助系统”
辅助系统提升体验,却也把中心化、缓存和权限重新带回架构
直接从浏览器逐条扫描日志、计算排行榜或保存大图片,既慢又昂贵。成熟应用会加入索引器、缓存、 后端、对象存储、自动化执行者与监控。它们不必写进协议,却必须在架构图中被看见。
随链上数据增长,直接聚合会越来越耗时,因此应用常用索引层构建查询友好的派生视图。 [10] 大文件通常放在链下或去中心化存储中,因为让每个以太坊节点复制它们既昂贵又不合适。 [11] 这些系统可以被替换,但如果应用没有备用读取路径,用户体验仍可能被单点故障阻断。
LAB 04 / FAILURE INJECTION
关掉一层:什么坏了,什么还在?
静态结论:界面、RPC 和索引器坏掉通常不会抹去已确认链上状态;钱包拒签意味着没有授权; 合约 revert 意味着规则拒绝这次状态转换。
生产前必须画出的六条信任边界
谁能替换网页代码?用户如何核对版本或使用备用入口?
端点能看到哪些地址查询?故障或错误响应如何检测与回退?
页面如何解释链、目标地址、方法、金额和模拟结果?
是否可升级、可暂停?管理员、时间锁和多签拥有什么能力?
索引器显示的排行榜、历史和头寸,能否从区块与日志重建?
价格、身份与现实事件来自哪里?预言机错误会如何影响合约?
10 / MINIMUM BLUEPRINT
用一个计数器,把所有层放回代码
本课不是让你复制代码,而是让每一行都有明确归属
假设测试网已部署一个 Counter 合约。它公开一个只读函数 number(),
一个写函数 increment(),并在成功写入后发出 NumberChanged 事件。
下面用 Ethers v6 风格展示最小结构;官方文档将 Provider 定义为只读链连接,将 Signer 定义为账户操作抽象。
[12]
contract Counter {
uint256 public number;
event NumberChanged(uint256 next, address indexed caller);
function increment() external {
number += 1;
emit NumberChanged(number, msg.sender);
}
}
import {
BrowserProvider,
Contract,
JsonRpcProvider
} from "ethers";
const address = "0x已验证的测试网合约地址";
const abi = [
"function number() view returns (uint256)",
"function increment()",
"event NumberChanged(uint256 next, address indexed caller)"
];
const readProvider =
new JsonRpcProvider(RPC_URL);
const reader =
new Contract(address, abi, readProvider);
const current = await reader.number();
screen.textContent = current.toString();
const provider =
new BrowserProvider(selectedWalletProvider);
const signer = await provider.getSigner();
const writer =
new Contract(address, abi, signer);
const tx = await writer.increment();
const receipt = await tx.wait();
address + abi决定“找谁、怎么说”JsonRpcProvider提供无需签名的读取通路BrowserProvider把 EIP-1193 钱包接入 EthersgetSigner()请求一个可代表所选账户授权的对象tx.wait()等待交易被纳入并取得收据,不等于自动满足所有最终性要求从本课走向下一课的实现顺序
- 01本地链
先让合约、测试和前端在可重置环境中闭环。
- 02测试网部署
记录 chain ID、部署地址、构造参数、编译器与源码验证。
- 03只读界面
先完成无需钱包的读取、空状态、加载与 RPC 错误。
- 04钱包与写入
再接账户、网络切换、模拟、签名、pending 与收据。
- 05事件与恢复
让页面在刷新、重组、索引滞后和端点故障后仍能恢复正确视图。
11 / RECAP
把整课压缩成一张可迁移的心智模型
以后遇到任何 DApp,都沿着同一组问题向下追踪
01先定位运行环境:这一段代码在浏览器、服务器、钱包还是 EVM 中执行?
02再定位权威来源:页面显示、RPC 响应、交易收据和最终化状态分别能证明什么?
03读写必须分开:读是节点模拟;写是用户授权后提交全网验证的状态转换。
04接口不是实现:ABI 描述如何交互,地址定位代码,但它们本身都不证明逻辑安全。
05辅助层必须可见:托管、RPC、索引器、后端和存储都可能形成新的单点与权限。
06库可以更换:Ethers.js、Web3.js 或其他工具只是对稳定协议概念的封装。
SELF CHECK / 6 QUESTIONS
六题自检:你是在背名词,还是已经能追踪一次调用?
01网页 JavaScript 单独能够直接修改合约 Storage 吗?
查看解释
前端、ABI 和库都不拥有修改共识状态的特权。它们只能构造请求。
02“合约地址 + ABI”最准确地提供了什么?
查看解释
还要核对 chain ID、实际 Bytecode、代理实现和升级权限。
03为什么读取 view 函数通常不需要钱包签名?
查看解释
view 调用仍可执行 EVM 代码,只是结果不写回共享状态。
04前端拿到交易哈希后,最安全的界面状态是什么?
查看解释
还需检查收据 status、所在区块以及业务要求的确认或最终性级别。
05Solidity 的 event 如何到达浏览器?
查看解释
事件不是网页推送协议;WebSocket 只是某些 Provider 交付日志更新的一种链下传输方式。
06一个 DApp 的官方网站下线,最能说明什么?
查看解释
是否真的存在可用替代入口,取决于地址、ABI、源码、前端副本和管理权限是否公开透明。
12 / GLOSSARY & PRIMARY SOURCES
术语钉牢,再继续动手
本课优先引用协议、语言与官方项目文档;资料核对于 2026-07-26
- DApp
- 把前端界面与去中心化网络上的智能合约组合起来的应用;生产系统通常还包含链下辅助服务。
- ABI
- Application Binary Interface。描述合约外部函数、参数、返回值、事件和错误的接口及编码规则。
- Provider
- 应用访问 Ethereum RPC 的抽象接口。浏览器钱包常暴露 EIP-1193 Provider。
- Signer
- 能代表某个账户授权消息或交易的抽象;密钥可以在钱包、硬件设备或其他安全边界内。
- JSON-RPC
- 应用与节点交换方法名、参数、结果和错误的轻量请求协议。
- Calldata
- 交易或调用交给目标账户的只读输入字节;合约函数选择器和参数通常编码在其中。
- Receipt
- 交易被区块执行后形成的收据,包含成功状态、Gas 使用量与日志等执行结果。
- Event Log
- 合约执行时写入收据的结构化日志,供外部系统检索与构建派生视图。
接口库和工具的 API 会变化;ABI、JSON-RPC、账户授权、节点执行和收据等协议概念更稳定。 写代码时仍需锁定版本并阅读对应版本文档。本页不加载外部脚本,示例也不连接真实网络。
-
01
Ethereum.org · Technical introduction to dapps
DApp 的前端与智能合约定义、开放接口与前端托管边界。
-
02
EIP-1193 · Ethereum Provider JavaScript API
Provider 的 request、事件、连接语义与安全边界。
-
03
Ethereum.org · Introduction to smart contracts
智能合约的公开规则、执行方式和访问链外数据限制。
-
04
Solidity · Contract ABI Specification
函数选择器、参数编码、JSON 接口、事件和错误描述。
-
05
EIP-6963 · Multi Injected Provider Discovery
多浏览器钱包的 Provider 发现、事件模型与安全考虑。
-
06
Web3.js · Official documentation
库的官方定位、Provider 与 Contract 文档,以及 2025 sunset 提示。
-
07
Ethereum.org · JSON-RPC API
应用连接节点、eth_call、eth_getLogs、区块标签与合约 calldata。
-
08
Ethereum.org · Nodes and clients
节点验证、RPC 端点、自运行节点的隐私与信任优势。
-
09
Ethereum.org · Anatomy of smart contracts
Storage、函数、事件与前端处理日志的关系。
-
10
Ethereum.org · Data and analytics
数据增长、聚合成本与索引层在 DApp 中的角色。
-
11
Ethereum.org · Decentralized storage
为什么大数据不适合直接存入 Ethereum,以及链下存储的取舍。
-
12
Ethers v6 · Getting started
Provider、Signer、交易与常用连接方式的官方定义。
-
13
Ethers v6 · Providers API
BrowserProvider、EIP-1193、ContractRunner、等待交易与读取区块。
-
14
Ethereum.org · Interacting with smart contracts
读取、写入、ABI、交易与日志的完整交互入口。
22.01 / COMPLETE
现在你看到的,不再是“一个网页连接一条链”。
你看到的是:人的意图如何被界面表达,被 ABI 与库翻译,被钱包授权, 被节点传播和验证,被 EVM 执行,再以状态、收据和日志回到应用。
下一步才是动手:在本地环境部署一个最小合约,把读取、写入、事件和错误处理真正接成闭环。