Ethereum / DApp architecture / Lesson 22.01

一个 DApp,究竟由什么组成?

你在网页上点一次“确认”,并不是把命令直接塞进区块链。它会依次穿过 界面、接口描述、JavaScript 库、Provider、钱包、节点、EVM, 最后才可能成为所有节点共同承认的新状态。

  • 正式目录:DApp 完整结构
  • 约 75 分钟
  • 4 个本地实验
  • 6 题自检
01 / NOT ONE PROGRAM

DApp 不是一个程序,而是一组边界。界面、钱包、节点和合约由不同主体运行,失败方式也不同。

02 / LIBRARY IS A TRANSLATOR

Ethers.js 或 Web3.js 不是“区块链后端”。它们主要把开发者友好的调用翻译为 ABI 与 JSON-RPC。

03 / READ ≠ WRITE

读操作是本地模拟,写操作是全网状态转换。是否需要账户、签名、Gas 和确认,由这条分界线决定。

01 / ORIENTATION

先把“网页调用合约”这句话拆开

一次点击跨越了多个运行环境,也跨越了多个信任主体

Ethereum.org 把 DApp 概括为前端用户界面与去中心化网络上的智能合约的组合[1] 这个定义抓住了两端,却省略了中间最容易让初学者困惑的桥梁:前端如何知道合约有哪些函数, 如何把参数变成字节,如何找到节点,谁负责签名,以及结果如何回到屏幕。

运行在哪里?

浏览器 / 服务器 / 节点

前端通常在用户设备运行;可选后端在开发者服务器运行;合约由收到同一交易的以太坊节点重复执行。

谁控制?

开发者 / 用户 / 协议

开发者发布界面,用户的钱包保管授权能力,协议决定什么交易和状态转换有效。

哪部分共享?

状态,不是屏幕

全网达成共识的是链上状态和交易历史,不是某个网站的按钮、文案、缓存或推荐顺序。

02 / THE LAYERS

四层不是四个产品,而是四种责任

理解“谁负责什么”,比背一套技术栈更耐久

目录中的“前端应用、Ethers.js/Web3.js、智能合约、以太坊网络”可以看成四层。 真实系统还会在接口库和网络之间加入 Provider、钱包与 RPC 节点。它们不是额外的花活, 而是跨越浏览器安全边界所必需的连接机制

LAB 01 / RESPONSIBILITY INSPECTOR

点一层,看清它能做什么、不能做什么

01 / FRONTEND
主要输入 人的点击、表单、当前页面状态
核心责任 解释业务、收集参数、展示进度与结果
明确不能 不能凭自己改变链上状态,也不应接触用户私钥
常见故障 旧地址、错误网络、欺骗性文案、状态缓存过期

静态结论:前端负责表达;接口库负责编码;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]

链上适合

必须共同验证的事实

资产余额、所有权、抵押率、治理票数、协议参数、不可绕过的结算规则。

链下适合

不值得全网重复的工作

搜索、推荐、图片、复杂报表、通知、缓存、私有数据,以及可以从链上数据重建的索引。

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

同一个“合约对象”,读与写会长成什么请求?

READ / VIEW
人类意图 查询 Alice 的代币余额
ABI 标识 0x70a08231
RPC 方法 eth_call
账户 / 签名 不需要
链上副作用 无;只模拟执行并返回字节
前端最终得到 uint256,库再格式化为可读余额
{
  "method": "eth_call",
  "params": [{ "to": "0xToken…", "data": "0x70a08231…" }, "latest"]
}

静态结论:ABI 不执行交易,它只定义字节语言;地址不证明合约安全,它只定位某段代码; Provider 不替你判断语义,它只传递请求。

05 / LIBRARY, PROVIDER, WALLET

三个经常被叫作“Web3”的对象,其实各管一件事

库负责翻译,Provider 负责通路,钱包负责账户授权

EIP-1193:浏览器应用与钱包之间的窄桥

EIP-1193 的核心不是某个品牌钱包,而是一个最小 Provider 约定: 应用可以调用 request 发出 RPC 请求,并监听 connectdisconnectchainChangedaccountsChanged 等事件。 规范还明确提醒:网页是潜在不可信环境,Provider 对象也应按可能被对手影响来设计。 [2]

connect可以发 RPC 请求
chainChanged目标链已变化,旧地址可能失效
accountsChanged当前授权账户已变化
disconnect无法再向目标链发请求

当浏览器装有多个钱包时,单一 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 还允许用 latestsafefinalized 等标签明确读取哪个状态。 [7]

不需要

账户许可与签名

公开状态可由任何节点查询。某些模拟可以带 from,但那不是签名,也不会证明账户同意。

不支付

链上 Gas Fee

节点仍然消耗计算资源,所以 RPC 服务商可以限流或收费;只是不会形成由用户支付的链上交易费。

不保证

未来写入一定成功

模拟结束后,状态、nonce、Base Fee、预言机价格或竞争交易都可能变化;读到“可执行”不等于之后必然成功。

07 / WRITE PATH

写链是一场状态转换提案

用户签的是交易,节点验证的是规则,区块提供的是排序与确认

当调用会改变状态时,前端先构造一个交易请求;钱包把链、目标地址、ETH 数量、calldata 与费用信息 展示给用户;用户确认后,钱包才用账户授权能力签名并广播。节点逐项验证,验证者把交易纳入区块, EVM 执行成功后才产生新的共享状态与收据。

LAB 03 / REQUEST JOURNEY

在“读、写、事件回流”之间切换

READ · 5 STEPS
  1. 01
    界面收集参数用户输入一个地址
    UI
  2. 02
    ABI 编码函数选择器 + 参数
    LIB
  3. 03
    发送 eth_call指定 latest / safe / finalized
    RPC
  4. 04
    节点模拟 EVM不持久化任何变化
    EVM
  5. 05
    解码并渲染字节 → uint256 → 可读单位
    UI
需要钱包?不需要
需要签名?不需要
会付 Gas?不会产生链上费用
结果是什么?某个区块状态上的查询结果

静态结论:读路径止于单个节点的模拟结果;写路径必须经过用户授权、广播、区块执行和收据; 事件路径从已执行交易的日志回到应用视图。

不要把四种“成功”混在一起

REQUESTED钱包收到请求还没有用户同意
SIGNED交易已签名还可能没有广播
SUBMITTED拿到交易哈希还没有被区块执行
INCLUDED收据 status = 1已经执行成功,但仍有确认深度
FINALIZED所在区块已最终化协议层给出更强不可逆保证

08 / EVENTS & LOGS

合约不会“推送 UI”,它只把日志留在收据里

事件是链上执行留下的可检索信号,前端或索引器负责把它变成视图

Solidity 合约可以在执行中 emit 事件。日志会随交易收据进入区块, 外部应用可以通过 RPC 查询或订阅,再按 ABI 解码。Ethereum.org 将它描述为合约与前端或其他订阅应用 沟通的重要方式。[9]

误解

“事件就是合约给浏览器发 WebSocket 消息。”

事实

事件首先是区块中的日志;节点或索引器再把日志通过查询、轮询或订阅交给应用。

Storage、返回值与 Event 各解决什么

STORAGE

当前事实

合约以后还要读取、验证或更新的数据。写入昂贵,但参与后续状态转换。

RETURN DATA

本次调用结果

eth_call 能直接取回;普通已上链交易的调用者不会像同步函数那样从前端拿到返回值。

EVENT LOG

历史信号

供外部应用检索和重建视图。合约本身不能把过去日志当作普通 Storage 随意读取。

09 / PRODUCTION ARCHITECTURE

最小 DApp 只有四层,生产 DApp 往往还有六个“辅助系统”

辅助系统提升体验,却也把中心化、缓存和权限重新带回架构

直接从浏览器逐条扫描日志、计算排行榜或保存大图片,既慢又昂贵。成熟应用会加入索引器、缓存、 后端、对象存储、自动化执行者与监控。它们不必写进协议,却必须在架构图中被看见。

随链上数据增长,直接聚合会越来越耗时,因此应用常用索引层构建查询友好的派生视图。 [10] 大文件通常放在链下或去中心化存储中,因为让每个以太坊节点复制它们既昂贵又不合适。 [11] 这些系统可以被替换,但如果应用没有备用读取路径,用户体验仍可能被单点故障阻断。

LAB 04 / FAILURE INJECTION

关掉一层:什么坏了,什么还在?

INTERFACE FAILURE
用户看到 网页无法加载,或页面被托管方下架
链上事实 合约代码、余额和历史仍在;已确认状态不受影响
替代路径 使用已验证 ABI 和地址,从另一前端、区块浏览器或脚本直接交互
设计动作 公开地址与 ABI;准备镜像或内容寻址前端;让用户能验证版本

静态结论:界面、RPC 和索引器坏掉通常不会抹去已确认链上状态;钱包拒签意味着没有授权; 合约 revert 意味着规则拒绝这次状态转换。

生产前必须画出的六条信任边界

01
前端发布权

谁能替换网页代码?用户如何核对版本或使用备用入口?

02
RPC 观察权

端点能看到哪些地址查询?故障或错误响应如何检测与回退?

03
钱包授权权

页面如何解释链、目标地址、方法、金额和模拟结果?

04
合约管理权

是否可升级、可暂停?管理员、时间锁和多签拥有什么能力?

05
数据派生权

索引器显示的排行榜、历史和头寸,能否从区块与日志重建?

06
外部事实输入

价格、身份与现实事件来自哪里?预言机错误会如何影响合约?

10 / MINIMUM BLUEPRINT

用一个计数器,把所有层放回代码

本课不是让你复制代码,而是让每一行都有明确归属

假设测试网已部署一个 Counter 合约。它公开一个只读函数 number(), 一个写函数 increment(),并在成功写入后发出 NumberChanged 事件。 下面用 Ethers v6 风格展示最小结构;官方文档将 Provider 定义为只读链连接,将 Signer 定义为账户操作抽象。 [12]

01 / CONTRACT · Solidity 在 EVM 中运行
contract Counter {
    uint256 public number;
    event NumberChanged(uint256 next, address indexed caller);

    function increment() external {
        number += 1;
        emit NumberChanged(number, msg.sender);
    }
}
02 / INTERFACE · JavaScript 在浏览器中运行
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)"
];
03A / READ 不请求账户
const readProvider =
  new JsonRpcProvider(RPC_URL);

const reader =
  new Contract(address, abi, readProvider);

const current = await reader.number();
screen.textContent = current.toString();
03B / WRITE 需要用户确认
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 钱包接入 Ethers
getSigner()请求一个可代表所选账户授权的对象
tx.wait()等待交易被纳入并取得收据,不等于自动满足所有最终性要求

从本课走向下一课的实现顺序

  1. 01
    本地链

    先让合约、测试和前端在可重置环境中闭环。

  2. 02
    测试网部署

    记录 chain ID、部署地址、构造参数、编译器与源码验证。

  3. 03
    只读界面

    先完成无需钱包的读取、空状态、加载与 RPC 错误。

  4. 04
    钱包与写入

    再接账户、网络切换、模拟、签名、pending 与收据。

  5. 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、账户授权、节点执行和收据等协议概念更稳定。 写代码时仍需锁定版本并阅读对应版本文档。本页不加载外部脚本,示例也不连接真实网络。

  1. 01
    Ethereum.org · Technical introduction to dapps

    DApp 的前端与智能合约定义、开放接口与前端托管边界。

  2. 02
    EIP-1193 · Ethereum Provider JavaScript API

    Provider 的 request、事件、连接语义与安全边界。

  3. 03
    Ethereum.org · Introduction to smart contracts

    智能合约的公开规则、执行方式和访问链外数据限制。

  4. 04
    Solidity · Contract ABI Specification

    函数选择器、参数编码、JSON 接口、事件和错误描述。

  5. 05
    EIP-6963 · Multi Injected Provider Discovery

    多浏览器钱包的 Provider 发现、事件模型与安全考虑。

  6. 06
    Web3.js · Official documentation

    库的官方定位、Provider 与 Contract 文档,以及 2025 sunset 提示。

  7. 07
    Ethereum.org · JSON-RPC API

    应用连接节点、eth_call、eth_getLogs、区块标签与合约 calldata。

  8. 08
    Ethereum.org · Nodes and clients

    节点验证、RPC 端点、自运行节点的隐私与信任优势。

  9. 09
    Ethereum.org · Anatomy of smart contracts

    Storage、函数、事件与前端处理日志的关系。

  10. 10
    Ethereum.org · Data and analytics

    数据增长、聚合成本与索引层在 DApp 中的角色。

  11. 11
    Ethereum.org · Decentralized storage

    为什么大数据不适合直接存入 Ethereum,以及链下存储的取舍。

  12. 12
    Ethers v6 · Getting started

    Provider、Signer、交易与常用连接方式的官方定义。

  13. 13
    Ethers v6 · Providers API

    BrowserProvider、EIP-1193、ContractRunner、等待交易与读取区块。

  14. 14
    Ethereum.org · Interacting with smart contracts

    读取、写入、ABI、交易与日志的完整交互入口。

22.01 / COMPLETE

现在你看到的,不再是“一个网页连接一条链”。

你看到的是:人的意图如何被界面表达,被 ABI 与库翻译,被钱包授权, 被节点传播和验证,被 EVM 执行,再以状态、收据和日志回到应用。

下一步才是动手:在本地环境部署一个最小合约,把读取、写入、事件和错误处理真正接成闭环。