首页 / 资讯中心 / 文章详情

T3架构Web3项目实战:从合约开发到链上数据可视化

T3架构Web3项目实战:从合约开发到链上数据可视化 ★ FEATURED ARTICLE
我刚把一个基于 T3 架构的 Web3 项目从零跑到链上数据可视化整个过程踩了不少坑也沉淀出一些心得。这篇文章不打算写成教科书主要围绕 t3code 这个项目聊聊我怎么把思路落成代码、怎么处理链上数据、又怎么把前后端串起来最后填了哪些坑。先说清楚 t3code 是什么它本质上是一个面向 Layer3也就是 T3生态的开发者工具型项目核心功能是围绕去中心化应用的开发、部署和链上数据展示展开的。简单理解就是你不是要跟 Layer1/Layer2 的主链逻辑死磕而是利用 T3 这一层提供的可扩展能力把应用逻辑更轻量地跑起来。t3code 解决的痛点恰恰是很多 DApp 开发者都遇到过的——底层链上逻辑写好了但怎么高效对接、怎么快速构建出可用的前端、怎么把链上数据变成业务可用信息这块的工程化支持一直很弱。这篇文章适合三类人看一是刚接触 Layer3 生态、想快速上手开发的开发者二是已经在写 Solidity 或链上合约、但被前端和链下数据问题困扰的从业者三是想了解 T3 层到底能做什么的技术决策者。废话不多说我直接把这套项目的完整拆解、核心代码逻辑、部署流程和排坑记录全部分享出来。1. 整体设计思路与方案选型1.1 为什么选择 T3 而不是直接在主链上开发先回答最核心的问题T3 到底是干嘛的。Layer1 承担的是资产结算和基础共识Layer2 解决的是吞吐量和手续费问题而 Layer3也就是 T3这一层说白了就是把业务逻辑和基础链解耦。你可以把 T3 想象成一个中间业务层它有自己独立的运行环境但安全和结算最终还是锚定在底层链上。这种架构的好处非常直接你的合约逻辑可以更灵活、更复杂但不会因为计算量太大把 gas 费推高也不会因为业务状态太复杂拖慢主链。我在设计 t3code 的时候第一版其实考虑过直接把所有逻辑写在 Layer2 上。但后来实际跑了一轮测试就发现问题业务里有一段数据聚合逻辑需要频繁读取链上事件并做统计如果全放在 L2 上状态读取和数据处理的成本会高得离谱而且每次迭代业务逻辑都要重新部署完整的合约开发和审计成本都上来了。换到 T3 之后底层链只用关注资产安全和最终结算复杂的业务编排全部放到 T3 层迭代速度明显快了很多。从工程角度讲T3 还有几个天然优势业务状态可以独立维护不用污染主链状态。合约升级的粒度更细不需要动不动就动底层基础合约。可以针对具体业务场景定制执行环境比如给数据密集型的逻辑配更高性能的节点。对开发者友好通常提供更贴近传统 Web 开发的 SDK 和接口。1.2 t3code 的核心架构拆解t3code 的整体架构我分成四个模块来设计模块一合约层。负责定义核心业务逻辑和状态转换规则可以在 T3 执行环境里跑也负责向底层链提交最终需要结算的关键结果。我在这层实现了代币相关的核心操作、跨链消息的解析和业务数据的聚合计算。模块二索引层。这层负责把链上原始事件转换成结构化的业务数据。比如说底层链上发生了一笔转账原始事件里只有 from、to、amount 这些字段但业务上我们需要知道这个地址在过去 24 小时内的交易频次、涉及哪些合约、累计金额是多少这些聚合逻辑不可能每次都在前端算所以必须有一个链下索引服务来做预处理。模块三接口层。这层是给前端和外部系统用的提供了一组 RESTful API 和 WebSocket 接口封装了后端服务的复杂逻辑让前端只需要按需拉取数据、订阅状态变更不需要理解链上数据的原始结构。模块四前端层。用户最终看到的部分包括资产看板、交易历史、业务数据图表、合约交互面板等。这四个模块之间的关系是链式的合约层产生原始数据索引层负责清洗和聚合接口层负责对上层屏蔽复杂度前端层负责呈现和交互。任何一个模块的失败都会影响整体体验但边界清晰之后每一层可以独立迭代。1.3 技术栈选型背后的考量技术栈选型这块我围绕几个原则来定的合约语言选的是 Solidity不太需要解释T3 生态对 Solidity 的支持最成熟而且参考资料多遇到问题好排查。但这里有个补充如果业务逻辑非常复杂需要用到大量计算而不只是状态转换那可以考虑在 T3 层引入 Rust 或者 Go 写的 off-chain 服务把计算密集的部分拿到链下来做。我在 t3code 里就用了一个思路——合约只做关键状态变更和事件广播复杂的统计和聚合全部放到链下服务。后端这块我选了 Node.js Express。原因也很实际T3 生态的 SDK 绝大多数都有 TypeScript 版本的实现选 Node.js 可以复用生态里现成的库降低对接成本。如果你更熟 Python 或者 Go也不是不行但要做好自己封装底层接口的准备。前端选了 React Vite再加 Tailwind CSS 来做样式。React 生态的链上交互库最丰富wagmi 和 viem 都用得很顺手。Vite 的启动速度和热更新的体验比 Webpack 好太多对于需要频繁调试合约交互的场景这一点极其重要。数据库选型上我用了 PostgreSQL主要存两类数据一是索引层处理完的业务数据二是用户的配置和偏好。PostgreSQL 的 JSONB 类型在处理复杂业务数据的灵活性上表现很好不用为每种数据结构都建一张专门的表。Redis 则用来做热数据的缓存比如资产价格、实时交易对信息这些数据查询频率高没必要每次都打到数据库。2. T3 链上合约的开发与关键逻辑实现2.1 开发环境准备如果你跟着做环境这块我建议直接按下面的版本来都是实测过的组合乱配版本容易出兼容性问题。node -v # v18.17.0 或以上 npm -v # v9.6.7 或以上 solc --version # solc 0.8.21 forge --version # forge 0.2.0我用的是 Foundry 来开发和测试合约。对比 HardhatFoundry 最大的优势是编译和测试速度快而且它对 Solidity 的支持更原生写测试的时候直接用 Solidity 写断言不用切到 JavaScript 去。项目初始化命令forge init t3code_contracts cd t3code_contracts初始化完成之后目录结构大概是这样的t3code_contracts/ ├── src/ # 合约源代码 ├── test/ # 测试代码 ├── script/ # 部署脚本 ├── lib/ # 依赖库 └── foundry.toml # Foundry 配置文件2.2 核心合约代码解析t3code 里有一个核心合约负责治理提案的创建、投票和链上执行。最开始写合约的时候我在数据结构上就犯了错用 mapping 存整个提案对象导致提案数量上来之后检索特别慢。后来改成提案 ID 到提案详情加用户地址到已投票提案集合两个映射分开存储查询效率才上来。核心合约里的一段关键逻辑是投票权重计算。T3 生态的特点之一是用户的链上行为会影响投票权重也就是行为证明机制。具体来说用户持有核心资产的时间越长、参与生态交互的频率越高投票权重越大。这个设计不是简单地给每个地址一样的投票权而是鼓励真实参与。// SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract T3Governance { struct Proposal { uint256 id; string title; string description; uint256 voteStart; uint256 voteEnd; uint256 yesVotes; uint256 noVotes; bool executed; address proposer; } mapping(uint256 Proposal) public proposals; mapping(address mapping(uint256 bool)) public hasVoted; uint256 public proposalCount; address public t3AssetAddress; // T3 绑定的主链资产合约地址用于计算投票权重 constructor(address _t3AssetAddress) { t3AssetAddress _t3AssetAddress; } function createProposal( string memory _title, string memory _description, uint256 _votingPeriod ) external returns (uint256) { require(_votingPeriod 0 _votingPeriod 14 days, invalid period); proposalCount; Proposal storage p proposals[proposalCount]; p.id proposalCount; p.title _title; p.description _description; p.voteStart block.timestamp; p.voteEnd block.timestamp _votingPeriod; p.proposer msg.sender; return proposalCount; } function getVotingPower(address user) public view returns (uint256) { // 读取用户在 T3 生态的行为积分 // 这里通过接口与 T3 生态的积分合约交互 IT3Asset t3 IT3Asset(t3AssetAddress); uint256 balance t3.balanceOf(user); uint256 engagementScore t3.getEngagementScore(user); return balance * 10 engagementScore * 2; } function vote(uint256 proposalId, bool support) external { Proposal storage p proposals[proposalId]; require(block.timestamp p.voteStart, voting not started); require(block.timestamp p.voteEnd, voting ended); require(!hasVoted[msg.sender][proposalId], already voted); uint256 weight getVotingPower(msg.sender); require(weight 0, zero voting power); if (support) { p.yesVotes weight; } else { p.noVotes weight; } hasVoted[msg.sender][proposalId] true; } }关于getVotingPower这段我补一句真实项目中balanceOf和getEngagementScore这两个函数里嵌了很多业务逻辑比如用户是否完成过 KYC、是否持有超过某个时间线的资产、最近有没有参与过生态活动。我这里做的是简写实战中你可以把不同维度的数据加权求和。这个设计的核心在于权重不能被少数人垄断也不能被刷子轻易刷出来。2.3 合约部署脚本与网络配置合约写完之后部署这一步有很多小细节。Foundry 的部署脚本可以用 Solidity 直接写// script/DeployGovernance.s.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; import forge-std/Script.sol; import ../src/T3Governance.sol; contract DeployGovernance is Script { function run() external { uint256 deployerKey vm.envUint(PRIVATE_KEY); address t3Asset vm.envAddress(T3_ASSET_ADDRESS); vm.startBroadcast(deployerKey); T3Governance governance new T3Governance(t3Asset); console.log(Governance deployed at:, address(governance)); vm.stopBroadcast(); } }部署前先设置环境变量export PRIVATE_KEY你的钱包私钥 export T3_ASSET_ADDRESS生态积分合约地址 forge script script/DeployGovernance.s.sol:DeployGovernance --rpc-url https://rpc.t3network.io --broadcast部署的时候我强烈建议先在一个测试网环境完整的跑一遍流程包括提案创建、投票、执行。不要直接在主网上试因为合约一旦部署就不能轻易改逻辑出问题只能通过升级合约的方式补救多一道流程多一份风险。2.4 合约测试与安全注意事项合约测试这块我写了几个核心测试场景// SPDX-License-Identifier: MIT pragma solidity ^0.8.21; import forge-std/Test.sol; import ../src/T3Governance.sol; contract T3GovernanceTest is Test { T3Governance governance; address mockAsset address(0x123); function setUp() public { governance new T3Governance(mockAsset); } function testCreateProposal() public { vm.prank(address(0xABC)); uint256 proposalId governance.createProposal(Test, Desc, 7 days); assertEq(proposalId, 1); } function testDoubleVoteBlocked() public { vm.prank(address(0xABC)); governance.createProposal(Test, Desc, 7 days); vm.prank(address(0xABC)); vm.mockCall(mockAsset, abi.encodeWithSignature(balanceOf(address)), abi.encode(100)); vm.prank(address(0xABC)); governance.vote(1, true); // 第二次投票应该 revert vm.prank(address(0xABC)); vm.expectRevert(already voted); governance.vote(1, false); } }做合约测试我摸索出几个必须覆盖的点权限校验只有谁可以调用 createProposal只有谁可以执行提案都要有明确的限制。状态边界投票时间窗口的起始、结束边界如何判断等于 voteEnd 的时候算不算有效建议明确block.timestamp voteEnd 才 revert等于时允许投票。权重计算边界balance 为零、积分很高的情况如何处理必须有逻辑兜底。重入攻击防护虽然这里没有转账逻辑但如果合约里有对外转账一定用 OpenZeppelin 的ReentrancyGuard。提示合约里涉及的金额、投票权重数值开发阶段一定用 mock 数据测试极限情况。我见过很多项目上线后因为权重计算溢出或者边界判断不严谨而出问题的审计不是万能的开发者自己多想一步很重要。3. 链下索引与数据服务的构建3.1 事件监听与数据同步合约部署完之后前端不可能直接拿合约事件来渲染页面因为链上事件是离散的、非结构化的。我做了一个事件监听服务订阅合约的各类事件然后同步到本地数据库。先定义事件监听的入口// src/listener/eventListener.ts import { createPublicClient, http, parseAbiItem } from viem; import { t3 } from t3-network/chain; const client createPublicClient({ chain: t3, transport: http(process.env.T3_RPC_URL), }); const GOVERNANCE_ABI [ event ProposalCreated(uint256 proposalId, address proposer, string title), event VoteCast(uint256 proposalId, address voter, uint256 weight, bool support), event ProposalExecuted(uint256 proposalId), ] as const; export async function watchGovernanceEvents() { return client.watchContractEvent({ address: process.env.GOVERNANCE_CONTRACT_ADDRESS as 0x${string}, abi: GOVERNANCE_ABI, onLogs: async (logs) { for (const log of logs) { await handleGovernanceLog(log); } }, }); }事件监听服务的主要难点在于确认数与回滚。如果新区块倒回导致之前的事件失效你的本地数据其实已经写入数据库了不及时处理就会出现前端数据与链上不一致的情况。我在这个服务里加了一个 pending 队列和 confirmed 队列pending 的事件先暂存等区块确认数达到要求之后再写入主库。这样虽然会带来一定的延迟但确保了数据的一致性。3.2 数据聚合与 API 设计数据同步进 PostgreSQL 之后接下来是业务聚合。我专门建了一张 proposal_stats 表记录每个提案的统计数据CREATE TABLE proposal_stats ( id SERIAL PRIMARY KEY, proposal_id INTEGER NOT NULL, total_yes NUMERIC NOT NULL DEFAULT 0, total_no NUMERIC NOT NULL DEFAULT 0, total_voters INTEGER NOT NULL DEFAULT 0, last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() );每次收到 VoteCast 事件就往这张表里更新数据用 proposal_id 做分组聚合。API 层我建了三个主要接口// src/api/routes/proposals.ts import { Router } from express; import { getProposalList, getProposalDetail, getProposalStats } from ../services/proposalService; const router Router(); // 获取提案列表支持分页 router.get(/proposals, async (req, res) { const page parseInt(req.query.page as string) || 1; const pageSize parseInt(req.query.pageSize as string) || 20; const data await getProposalList(page, pageSize); res.json(data); }); // 获取单个提案详情 router.get(/proposals/:id, async (req, res) { const id parseInt(req.params.id); const data await getProposalDetail(id); res.json(data); }); // 获取提案投票统计 router.get(/proposals/:id/stats, async (req, res) { const id parseInt(req.params.id); const data await getProposalStats(id); res.json(data); }); export default router;接口设计上我坚持一个原则后端不向前端暴露任何未经处理的原始数据。哪怕是查询单个提案也会把合约地址、创建时间、投票截止时间这些字段统一转成前端友好的格式而不是让前端自己去拼接。这样前端开发完全不需要理解合约 ABI数据消费链路短了一半。3.3 WebSocket 实时推送提案列表不止要能查还要能感知变化。我用 WebSocket 推实时更新前端订阅之后一旦有新的投票页面自动更新数据。// src/services/wsService.ts import { WebSocketServer } from ws; const wss new WebSocketServer({ port: 8080 }); export function broadcastProposalUpdate(proposalId: number) { const payload JSON.stringify({ type: proposal_update, proposalId, timestamp: Date.now(), }); wss.clients.forEach((client) { if (client.readyState WebSocketServer.OPEN) { client.send(payload); } }); }在事件监听回调里调用广播async function handleGovernanceLog(log: any) { // ... 写入数据库 await updateProposalStats(proposalId); broadcastProposalUpdate(proposalId); }用 WebSocket 而不是前端轮询理由很简单投票不是一个高频动作但如果投票正好集中在一段时间内比如提案截止前半小时轮询会有明显延迟而且大量无价值的 HTTP 请求会白白消耗带宽和服务器资源。WebSocket 实现精准推送连接建立后只在真正有数据变更时才下发消息负载小效率高。4. 前端集成与完整交互流程4.1 前端项目初始化与依赖配置前端这部分我用 Vite 搭了一个 React TypeScript 的项目。初始化命令npm create vitelatest t3code_frontend -- --template react-ts cd t3code_frontend npm install wagmi viem tanstack/react-query tailwindcss这里重点说一下 wagmi。它是一个 React 专用的链上交互库把钱包连接、合约读取、合约写入这些操作封装成 hooks开发者不需要手写一堆 JSON-RPC 调用。我在项目里用了它的useAccount、useContractRead和useContractWrite连接钱包和调用合约的部分变得非常顺手。4.2 钱包连接与链上身份认证钱包连接这一块我用 wagmi 内置的RainbowKit插件来快速构建钱包选择界面。核心代码如下// src/main.tsx import { WagmiConfig, createConfig, configureChains } from wagmi; import { RainbowKitProvider, getDefaultWallets } from rainbow-me/rainbowkit; import { t3 } from t3-network/chain; import { publicProvider } from wagmi/providers/public; const { chains, publicClient } configureChains( [t3], [publicProvider()] ); const { connectors } getDefaultWallets({ appName: t3code, projectId: 你的WalletConnect项目ID, chains, }); const config createConfig({ autoConnect: true, connectors, publicClient, });这里有个关键点RainbowKit 需要你在 WalletConnect Cloud 上注册一个项目 ID不注册的话有些钱包连接方式不可用。我第一次没注册直接用默认配置结果 MetaMask 能连WalletConnect 死活连不上排查了很久才意识到是项目 ID 的问题。连接钱包之后拿到用户地址然后从后端拉取该地址的投票历史和资产信息来做展示。这里就不再贴完整代码了核心是用useAccount拿 address再配合useQuery去请求接口。4.3 投票交互前端实现用户投票的交互流程是这样的前端读取提案详情检查当前用户是否已经投过票如果没投过渲染两个按钮一个支持一个反对。点击之后调合约的 vote 方法等待交易确认后刷新 UI。// src/components/VotePanel.tsx import { useContractWrite, usePrepareContractWrite, useWaitForTransaction } from wagmi; import { governanceABI } from ../abis/governanceABI; export function VotePanel({ proposalId, support, disabled }: any) { const { config } usePrepareContractWrite({ address: import.meta.env.VITE_GOVERNANCE_ADDRESS as 0x${string}, abi: governanceABI, functionName: vote, args: [proposalId, support], enabled: !disabled, }); const { data, write } useContractWrite(config); const { isLoading, isSuccess } useWaitForTransaction({ hash: data?.hash }); return ( button disabled{!write || isLoading} onClick{() write?.()} classNamepx-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50 {isLoading ? 确认中... : isSuccess ? 投票成功 : 投票} /button ); }这里我踩了两个坑说出来给大家提个醒。第一个坑是usePrepareContractWrite必须在投票开启状态才启用否则用户在投票时间窗口外点击按钮会直接触发合约 revert前端如果没有预先拦截用户看到的就是一个莫名其妙的交易失败。我在enabled参数里判断了当前时间是否在投票窗口内如果不在按钮直接灰掉不给用户发起交易的机会。第二个坑是交易确认后的 UI 刷新。合约写入完成不等于数据已经同步到后端因为索引服务还需要一段时间才能把链上事件写入数据库。如果前端在交易确认后立刻调后端接口获取最新统计大概率拿到的还是旧数据。我在这里做了二次校验交易确认后先更新按钮状态为投票成功但投票统计区的刷新延迟 5 秒给索引服务留出处理时间。4.4 前端与后端联调中的接口约定联调过程中前后端最容易出问题的地方在于数据格式的约定。我整理了一个接口协议表前端和后端严格按照这个对齐接口路径请求方式参数返回关键字段说明/api/proposalsGETpage, pageSizelist, total, page提案分页列表/api/proposals/:idGET无id, title, status, voteStart, voteEnd提案详情/api/proposals/:id/statsGET无yesVotes, noVotes, totalVoters投票统计信息/api/user/:address/votesGETaddressvotedProposalIds, votingPower用户投票历史前后端字段命名统一用驼峰式时间格式统一用 ISO 8601 字符串金额统一用字符串类型传输避免 JavaScript 的精度丢失问题。这几点看着简单但实际项目里一旦前期不约定好联调阶段就是改来改去、摩擦不断。提示链上金额和权重数据后端返回给前端的时候一定不要用 Number() 转换直接用字符串。JavaScript 的 Number 精度只有 2^53一旦数值过大就会出现精度丢失而链上数据动辄就是几十位的大数转换之后就错了。5. 部署、监控与常见问题排查5.1 完整部署流程t3code 的部署流程分四块合约部署、后端服务部署、前端静态资源部署、数据库初始化。如果你用的是 Linux 服务器加 Docker Compose部署脚本大致是这么写的# docker-compose.yml version: 3.8 services: postgres: image: postgres:15-alpine environment: POSTGRES_USER: t3code POSTGRES_PASSWORD: change_me POSTGRES_DB: t3code volumes: - pgdata:/var/lib/postgresql/data ports: - 5432:5432 redis: image: redis:7-alpine ports: - 6379:6379 indexer: build: ./indexer env_file: .env depends_on: - postgres - redis api: build: ./api env_file: .env ports: - 3000:3000 depends_on: - postgres - redis frontend: build: ./frontend ports: - 8080:80 depends_on: - api volumes: pgdata:跑起来三步走docker compose build docker compose up -d postgres redis docker compose up -d部署完先不要急着接真实链上数据。先用测试网跑一天确认索引服务是稳定的、事件监听没有漏数据、前后端联调逻辑没有死路然后再切换到主网。5.2 监控与告警配置项目上线之后监控是必不可少的一环。我这里分享两个监控方案。第一个方案是链下事件漏监听告警。我在索引服务里加了一个定时任务每小时检查一次数据库里最新事件的时间戳如果发现最新事件时间比当前时间晚了超过 10 分钟就触发告警推送企业微信、邮件、Telegram 都可以。第二个方案是 RPC 节点健康检查。如果 T3 RPC 节点暂不可用事件监听会断掉但不会报错——连接失败会被当成普通网络错误稍后又自动重连。问题在于如果节点持续不稳定事件堆积会越来越严重。我的做法是在服务里记录连接失败次数当连续失败超过 5 次时主动触发一次健康检查并发出告警。5.3 高频问题排查实录开发过程中我收集了几个高频问题的排查方法整理成一个速查表。问题现象可能原因排查方式解决方案前端提交的投票交易持续 pending用户 gas 设置过低或网络拥堵检查交易状态查看 pending 时间建议用户调高 gas或在前端显示 gas 预估值供用户确认投票数据迟迟不显示在页面上索引服务事件监听断开检查索引服务日志确认 latest block number 是否持续更新重启索引服务或切换备用 RPC 节点页面显示的总票数与链上实际不一样重复投票事件被重复写入检查事件监听是否重复处理在数据库中为 proposalId voter 组合加唯一约束合约调用报voting ended前端时间与链上时间不同步检查本地系统时间和 RPC 返回的 block timestamp统一使用合约返回的时间判断投票状态连接钱包后页面白屏RainbowKit 配置缺少 projectId检查控制台报错注册 WalletConnect 项目 ID重新配置需要单独强调一下第三行的问题也就是重复写入。事件监听在设计上要保证 at-least-once 或 exactly-once 的语义。区块链事件在极端情况下的确可能被重复推送比如重连之后重新同步历史区块如果数据库层没有做幂等约束统计数字就会被重复计算。我处理方式是在数据库表上加了唯一索引ALTER TABLE proposal_stats ADD CONSTRAINT unique_proposal_voter UNIQUE (proposal_id, voter_address);这样即使事件被重复处理第二次插入也会被数据库拒绝不会污染统计数据。5.4 性能优化实践上线跑了一周之后我发现数据量涨起来之后接口响应变慢了。排查下来原因很简单PostgreSQL 中 proposal_stats 表的数据量涨到一定规模之后查询统计信息就慢了。优化方式并不复杂做了两件事。第一件事给高频查询的字段建索引。按 proposal_id 查询的字段建了 B-tree 索引按时间区间查询的建了复合索引CREATE INDEX idx_proposal_id ON proposal_stats (proposal_id); CREATE INDEX idx_last_updated ON proposal_stats (last_updated DESC);第二件事把热点数据的查询缓存到 Redis。提案列表、投票统计这类高频读接口设置了 30 秒的缓存有效期。数据变更时主动失效缓存。虽然区块链本身的数据是透明的但链下服务的架构设计和性能优化一点都不能含糊用户感知的流畅度决定产品口碑。我在实际测试中的体会是L3 架构带来的灵活性和效率提升是肉眼可见的但能不能把这个优势真正发挥出来关键在工程细节的打磨。智能合约也好、链下服务也好、前端交互也好任何一个环节掉链子体验就会崩。上面这套 t3code 的完整实践希望对正在做 T3 生态开发的朋友有实际帮助尤其是事件监听和数据一致性这块值得多花时间磨。
阅读完成 · 觉得有帮助?
咨询建站