本文我们从NFT列表展示速度慢的现象开始分析,发现前端展示流程不合理的地方,通过引入TheGraph将NFT列表展示耗时降低了1万多倍,实际中还有很多类似的应用。
在如何发行一款NFT(下)的官网搭建中,我们发现NFT列表页面,展示速度非常缓慢,经常需要等待5、6s,这一体验是不可接受的,我们先来分析为什么这个页面如此耗时,下面是获取数据的流程:
如图所示,我们需要不断循环请求获取tokenId对应的tokenURI,然后通过tokenURI请求获取metadata来解析数据,每次循环都是2次http通信 按照较低时延http通信耗时300ms,每次循环将消耗600ms, 而且这个数字将随着NFT数量增加线性增加,我们的页面耗时5、6s是因为我们的NFT只mint了10个左右,正常上线的NFT一般会有接近1万个toknId,那么整体耗时将达到 100分钟,这样的速度没有任何用户可以容忍。所以我们一定需要其他方案来优化这一流程。
我们的理想流程是:
理想状态下前端页面只需要1次请求获取所需要的数据即可,而不是进行多次请求。这就需要1个链下的后端服务帮我们去做类似的逻辑并存储数据,待前端页面需要展示时请求这个后端服务中存储的数据,而TheGraph就是这样的一种服务,与我们自行搭建的后端服务不同,TheGraph是去中心化的,这极大的避免了我们单节点的中心化风险。
TheGraph初始化的时候会从所有历史区块扫描我们所需要的事件,每扫描到1个就会调用我们编写的事件处理函数,直至到达最新区块,到达最新区块后TheGraph会监听每一次新出块并寻找是否有需要的事件。 我们通常会在事件处理函数中,进行逻辑操作并最终进行存储。此时我们处理过的数据将保存在TheGraph中。等到前端需要这些数据时可以通过GraphQL方式请求获取。完整流程如下图所示:
之前在以太坊技术系列-以太坊数据结构 中介绍过以太坊中有3棵树,状态树,交易树,收据树。以太坊的事件存储在了收据树中。在智能合约中写入1个事件的方式为使用event关键字定义,使用emit关键字发送。
//定义事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
//发送事件
emit Transfer(owner, address(0), tokenId);
GraphQL主要为我们提供了较为细粒度的与服务端交互方式,相比之前的REST API,有如下几个优点:
1个GraphQL请求-响应如下所示 请求:
{
metaDatas(first:1) {
id
name
image
owner
}
}
响应:
{
"data": {
"metaDatas": [
{
"id": "0",
"name": "nft-web3-explorer",
"image": "ipfs://xxx/0.png",
"owner": "0xxxx"
}
]
}
}
接下来介绍我们如何通过TheGraph来优化NFT列表展示的。具体我们分为如下3步:
既然触发源是以太坊中的event,那么我们肯定需要在合适的时机发送event,由于我们采用ERC721的实现,查看代码会在mint的时候会发送Transfer event,event中带有mint地址以及tokenId,符合我们的需求,所以我们选择使用该event做为我们的触发源。
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
到TheGrapha官网申请subGraph,需要连接github。 https://thegraph.com/hosted-service
本地工程初始化
//安装graph脚手架
npm install -g @graphprotocol/graph-cli
//初始化工程,选择合约地址,abi文件(会自动从合约地址读取,读取失败可以本地上传),网络(我们依旧使用rinkeby测试网络)
graph init <GITHUB_USERNAME>/<SUBGRAPH_NAME> <DIRECTORY>
在schema.graphql中定义我们的存储结构
//记录每次的Transfer事件(非必要)
type QLTransfer @entity {
id: ID!
from: Bytes! # address
to: Bytes! # address
tokenId: BigInt!
tokenURI: String!
}
//记录Metadata数据
type MetaData @entity {
//每条数据需要唯一id,这里我们使用tokenId
id: ID!
//持有tokenId的地址
owner:Bytes!
//metadata文件中的name
name: String
//metadata文件中的image
image: String
}
通过定义的存储结构生成对应的ts代码以方便逻辑代码中直接调用。
graph codegen
在mapping.ts中完成我们的逻辑处理,使用AssemblyScript编写(AssemblyScript是TypeScript的子集,AssemblyScript规范参考)
export function handleTransfer(event: Transfer): void {
const qlTransfer = new QLTransfer(event.transaction.hash.toHexString());
qlTransfer.from = event.params.from;
qlTransfer.to = event.params.to;
qlTransfer.tokenId = event.params.tokenId;
const contract = NFT_WEB3_EXPOLRER.bind(event.address);
//与合约交互获取tokenId对应的tokenURI
qlTransfer.tokenURI = contract.tokenURI(event.params.tokenId);
qlTransfer.save();
log.info('qlTransfer id is {}', [qlTransfer.id]);
//将http转换为ipfs数据
const splitstr = qlTransfer.tokenURI.split("/ipfs/");
if(splitstr.length < 2) {
return;
}
const ipfsPath = splitstr[1];
log.info('ipfsPath is {}', [ipfsPath]);
//获取metadata数据
const data = ipfs.cat(ipfsPath)
if (!data) {
return;
}
log.error('data is {}', [data.toString()]);
//转换为json格式
const value = json.fromBytes(data);
//新建MetaData结构
const meta = new MetaData(qlTransfer.tokenId.toString());
const obj = value.toObject();
if (obj != null) {
//解析name
const name = obj.get("name")
if (name != null) {
meta.name = name.toString();
}
//解析image
const image = obj.get("image")
if (image != null) {
meta.image = image.toString();
}
}
meta.owner = qlTransfer.to;
//数据存储
meta.save();
log.info('meta id is {}', [meta.id]);
}
完成代码编写后进行编译
graph build
//首次部署需要设置key授权
graph auth --product hosted-service {key}
//部署
graph deploy --product hosted-service EXPLORER-OF-WEB3/nft_web3_explorer_subgraph
TheGraph官网会有部署进度,因为要扫描所有区块所以会比较慢,部署完成后我们就有了后端数据,接下来前端展示只要去请求该数据即可。
我们使用apollo这个库来帮助我们完成GraphQL交互。
添加依赖
npm i @apollo/client graphql
在uitls目录下新建 graphql_utils.js来管理graphql交互
import {
ApolloClient,
InMemoryCache,
gql
} from "@apollo/client";
export const getListData = async () => {
//建立连接
const client = new ApolloClient({
uri: "thgraph项目地址",
cache: new InMemoryCache()
});
//按照graphql形式请求
const data = await client.query({
query: gql`
query res {
metaDatas{
id
name
image
}
}`
});
const list = data?.data?.metaDatas;
console.log("getListData" + list);
return list;
}
如果我们增加1个功能,展示属于自己的NFT,那么只需要在查询中指定owenr为自己的地址即可,如下所示
{
metaDatas(where:{owner:"地址"}) {
id
name
image
}
}
在使用TheGrpha后,我们的NFT列表展示速度稳定控制在500ms左右,极大地提升了用户体验,相对于直接与合约交互耗时降低了1万多倍。
本文我们从NFT列表展示速度慢的现象开始分析,发现前端展示流程不合理的地方,通过引入TheGraph将NFT列表展示耗时降低了1万多倍,实际中还有很多类似的应用。由于需要尽可能地减少以太坊中存储,我们将部分数据存入链下而且即使在以太坊中的存储也可能没有合适的索引,只能前端拿到所有数据再建立索引(比如属于某个地址的tokenId集合)。这样的情况下我们就需要后端服务来帮助我们建立链上数据的索引并整合链下数据,可以极大地优化用户体验。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!