使用TheGraph实现Dapp的万倍速度优化

本文我们从NFT列表展示速度慢的现象开始分析,发现前端展示流程不合理的地方,通过引入TheGraph将NFT列表展示耗时降低了1万多倍,实际中还有很多类似的应用。

极慢的前端体验

6407.jpeg
6407.jpeg

如图所示,我们需要不断循环请求获取tokenId对应的tokenURI,然后通过tokenURI请求获取metadata来解析数据,每次循环都是2次http通信 按照较低时延http通信耗时300ms,每次循环将消耗600ms, 而且这个数字将随着NFT数量增加线性增加,我们的页面耗时5、6s是因为我们的NFT只mint了10个左右,正常上线的NFT一般会有接近1万个toknId,那么整体耗时将达到 100分钟,这样的速度没有任何用户可以容忍。所以我们一定需要其他方案来优化这一流程。

6408.jpeg
6408.jpeg

理想状态下前端页面只需要1次请求获取所需要的数据即可,而不是进行多次请求。这就需要1个链下的后端服务帮我们去做类似的逻辑并存储数据,待前端页面需要展示时请求这个后端服务中存储的数据,而TheGraph就是这样的一种服务,与我们自行搭建的后端服务不同,TheGraph是去中心化的,这极大的避免了我们单节点的中心化风险。

TheGraph工作原理简介

6409.jpeg
6409.jpeg

以太坊事件event

之前在以太坊技术系列-以太坊数据结构 中介绍过以太坊中有3棵树,状态树,交易树,收据树。以太坊的事件存储在了收据树中。在智能合约中写入1个事件的方式为使用event关键字定义,使用emit关键字发送。

//定义事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

//发送事件
emit Transfer(owner, address(0), tokenId);

GraphQL简介

GraphQL主要为我们提供了较为细粒度的与服务端交互方式,相比之前的REST API,有如下几个优点:

  1. 减少字段冗余,只返回查询中需要的字段。
  2. 避免多次请求数据,如第一次请求获取id 第二次请求通过id获取其他属性的场景可以一次返回。
  3. 更有利于前后端分离,前端只需要在请求中增加字段即可获得新增字段,不需要后端配合。

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步:

  1. 确定合约中需要使用的事件, 因为TheGraph是基于以太坊中的事件触发的,所以我们需要在合约中合适的位置发送事件。
  2. 完成后端代码-TheGraph对于事件的处理并进行存储,我们需要定义数据的存储结构,并编写处理函数在接受到事件的时候进行处理,将数据按照定义结构进行存储。
  3. 完成前端代码,请求获取TheGraph的数据,我们通过GraphQL的方式从存储中获取需要的数据。

合约中加入事件

既然触发源是以太坊中的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

部署到TheGraph服务器

//首次部署需要设置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: "thegrapha项目地址",
        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;
}

获取属于自己NFT

如果我们增加1个功能,展示属于自己的NFT,那么只需要在查询中指定owenr为自己的地址即可,如下所示

{
  metaDatas(where:{owner:"地址"}) {
    id
    name
    image
  }
}

在使用TheGrpha后,我们的NFT列表展示速度稳定控制在500ms左右,极大地提升了用户体验,相对于直接与合约交互耗时降低了1万多倍。

结尾

本文我们从NFT列表展示速度慢的现象开始分析,发现前端展示流程不合理的地方,通过引入TheGraph将NFT列表展示耗时降低了1万多倍,实际中还有很多类似的应用。由于需要尽可能地减少以太坊中存储,我们将部分数据存入链下而且即使在以太坊中的存储也可能没有合适的索引,只能前端拿到所有数据再建立索引(比如属于某个地址的tokenId集合)。这样的情况下我们就需要后端服务来帮助我们建立链上数据的索引并整合链下数据,可以极大地优化用户体验。

Subscribe to explorer_of_web3
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.