使用 React、Ethers.js、Solidity 和 Hardhat 构建全栈 dApp

在本教程中,您将学习一个 web3 技术堆栈,该堆栈将允许您利用以太坊虚拟机 (EVM) 在包括 Ethereum、Polygon、Avalanche、Celo 和许多其他区块链网络在内的数十个区块链网络上构建全栈应用程序。

该项目的代码位于此处。本教程的视频课程位于此处。另请查看定义 web3 堆栈

我最近以开发者关系工程师的身份加入Edge & Node,并一直在深入研究以太坊的智能合约开发。我已经确定了我认为使用 Solidity 构建全栈 dApp 的最佳堆栈:

▶︎ 客户端框架 - React ▶︎ 以太坊开发环境 -安全帽 ▶︎ 以太坊 Web 客户端库 - Ethers.js ▶︎ API 层 -图形协议

我在学习这一点时遇到的问题是,虽然对于这些东西中的每一个都有相当好的文档,但对于如何将所有这些东西放在一起并了解它们如何相互工作并没有什么真正的东西. 那里有一些非常好的样板,例如scaffold-eth(其中还包括Ethers、Hardhat 和The Graph),但对于刚入门的人来说可能太多了。

我想要一个端到端的指南,向我展示如何使用最新的资源、库和工具构建全栈以太坊应用程序。

我感兴趣的事情是:

  1. 如何创建、部署和测试以太坊智能合约到本地、测试和主网
  2. 如何在本地、测试和生产环境/网络之间切换
  3. 如何使用 React、Vue、Svelte 或 Angular 等前端的各种环境连接到合约并与之交互

在花了一些时间弄清楚所有这些并开始使用我感到非常满意的堆栈之后,我认为写下如何使用这个堆栈构建和测试一个完整堆栈的以太坊应用程序会很好,不仅适用于其他人有谁可能对这个栈感兴趣,也供自己日后参考。这就是那个参考。

碎片

让我们回顾一下我们将使用的主要部分以及它们如何适合堆栈。

一、以太坊开发环境

在构建智能合约时,您将需要一种无需处理实时环境即可部署合约、运行测试和调试 Solidity 代码的方法。

您还需要一种将 Solidity 代码编译成可以在客户端应用程序(在我们的例子中是 React 应用程序)中运行的代码的方法。稍后我们将详细了解它的工作原理。

Hardhat 是专为全栈开发而设计的以太坊开发环境和框架,也是我将用于本教程的框架。

生态系统中的其他类似工具是GanacheTruffleFoundry

2. 以太坊网络客户端库

在我们的 React 应用程序中,我们需要一种与已部署的智能合约进行交互的方法。我们将需要一种读取数据以及发送新事务的方法。

ethers.js旨在成为一个完整而紧凑的库,用于从 React、Vue、Angular 或 Svelte 等客户端 JavaScript 应用程序与以太坊区块链及其生态系统进行交互。这是我们将使用的库。

生态系统中另一个流行的选项是web3.js

3. 元掩码

Metamask有助于处理帐户管理并将当前用户连接到区块链。MetaMask 使用户能够以几种不同的方式管理他们的帐户和密钥,同时将它们与站点上下文隔离。

window.ethereum一旦用户连接了他们的MetaMask 钱包,您作为开发人员就可以与全球可用的以太坊 API (尽可能让用户理解。

4. React

React 是一个前端 JavaScript 库,用于构建 Web 应用程序、用户界面和 UI 组件。它由 Facebook 和许多个人开发者和公司维护。

React 及其庞大的元框架生态系统(如Next.jsGatsbyRedwoodBlitz.js等)支持所有类型的部署目标,包括传统 SPA、静态站点生成器、服务器端渲染以及这三者的组合。React 似乎继续在前端领域占据主导地位,我认为至少在不久的将来会继续这样做。

5. 图表

对于大多数建立在以太坊等区块链上的应用程序,直接从链中读取数据既困难又耗时,因此您过去常常看到人们和公司构建自己的集中式索引服务器并为来自这些服务器的 API 请求提供服务。这需要大量的工程和硬件资源,并破坏了去中心化所需的安全属性。

Graph 是一种用于查询区块链数据的索引协议,它可以创建完全去中心化的应用程序并解决这个问题,并提供应用程序可以使用的丰富的 GraphQL 查询层。在本指南中,我们不会为我们的应用程序构建子图,但会在以后的教程中这样做。

要了解如何使用 The Graph 构建区块链 API,请查看在Ethereum 上构建 GraphQL API

我们将建造什么

在本教程中,我们将构建、部署和连接几个基本的智能合约:

  1. 在以太坊区块链上创建和更新消息的合约
  2. 用于铸造代币的合约,然后允许合约的所有者将代币发送给其他人并读取代币余额,并允许新代币的所有者也将它们发送给其他人。

我们还将构建一个 React 前端,允许用户:

  1. 从部署到区块链的合约中读取问候语
  2. 更新问候语
  3. 将新铸造的代币从他们的地址发送到另一个地址
  4. 一旦有人收到令牌,允许他们也将他们的令牌发送给其他人
  5. 从部署到区块链的合约中读取代币余额

先决条件

  1. 安装在本地机器上的 Node.js
  2. 您的浏览器中安装了MetaMask Chrome 扩展程序

对于本指南,您不需要拥有任何以太坊,因为我们将在整个教程的测试网络上使用假 / 测试 Ether。

入门

首先,我们将创建一个新的 React 应用程序:

npx create-react-app react-dapp

接下来,切换到新目录并安装ethers.jshardhat使用NPMYarn

npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

安装和配置以太坊开发环境

接下来,使用 Hardhat 初始化一个新的以太坊开发环境:

npx hardhat

? What do you want to do? Create a sample project
? Hardhat project root: <Choose default path>

现在,您应该会在根目录中看到为您创建的以下工件:

hardhat.config.js - 您的整个 Hardhat 设置(即您的配置、插件和自定义任务)都包含在此文件中。 scripts - 包含名为sample-script.js的脚本的文件夹,将在执行时部署您的智能合约 test - 包含示例测试脚本的文件夹 contract - 包含示例 Solidity 智能合约的文件夹

由于MetaMask 配置问题,我们需要将 HardHat 配置上的链 ID 更新为1337。我们还需要更新已编译合约的工件位置,使其位于React 应用程序的src目录中。

要进行这些更新,请打开hardhat.config.js并将其更新module.exports为如下所示:

module.exports = {
  solidity: "0.8.4",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {
      chainId: 1337
    }
  }
};

我们的智能合约

接下来,让我们看一下在contracts/Greeter.sol中提供给我们的示例合约:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";


contract Greeter {
  string greeting;

  constructor(string memory _greeting) {
    console.log("Deploying a Greeter with greeting:", _greeting);
    greeting = _greeting;
  }

  function greet() public view returns (string memory) {
    return greeting;
  }

  function setGreeting(string memory _greeting) public {
    console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
    greeting = _greeting;
  }
}

这是一个非常基本的智能合约。部署时,它会设置一个 Greeting 变量并公开一个函数 ( greet),可以调用该函数来返回问候语。

它还公开了一个允许用户更新问候语 ( setGreeting) 的函数。当部署到以太坊区块链时,这些方法将可供用户交互。

读写以太坊区块链

与智能合约交互的方式有两种,读取或写入/交易。在我们的合约中,greet可以认为是阅读,setGreeting也可以认为是写作/交易。

在写入或初始化交易时,您必须为要写入区块链的交易付费。要完成这项工作,您需要支付[天然气](https://www.investopedia.com/terms/g/gas-ethereum.asp#:~:text=What Is Gas (Ethereum)%3F,on the Ethereum blockchain platform),这是在以太坊区块链上成功进行交易和执行合同所需的费用或价格。

只要您只是从区块链中读取数据而不更改或更新任何内容,您就不需要执行交易,并且这样做不会产生任何气体或成本。然后,您调用的功能仅由您连接的节点执行,因此您无需支付任何费用,并且读取是免费的。

在我们的 React 应用程序中,我们与智能合约交互的方式是使用ethers.js库、合约地址和将由 hardhat 从合约创建的ABI的组合。

什么是 ABI?ABI 代表应用程序二进制接口。您可以将其视为客户端应用程序与您将与之交互的智能合约部署的以太坊区块链之间的接口。

ABI 通常由 HardHat 等开发框架从 Solidity 智能合约编译而来。您还可以经常在Etherscan上找到智能合约的 ABI

编译 ABI

现在我们已经了解了基本的智能合约并知道了 ABI 是什么,让我们为我们的项目编译一个 ABI。

为此,请转到命令行并运行以下命令:

npx hardhat compile

现在,您应该在src目录中看到一个名为artifacts的新文件夹。****artifacts/contracts/Greeter.json文件包含 ABI 作为属性之一。当我们需要使用 ABI 时,我们可以从我们的 JavaScript 文件中导入它:

import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

然后我们可以像这样引用 ABI:

console.log("Greeter ABI: ", Greeter.abi)

请注意,Ethers.js 还启用了人类可读的 ABI,但在本教程中不会涉及到这一点。

部署和使用本地网络/区块链

接下来,让我们将我们的智能合约部署到本地区块链,以便我们对其进行测试。

要部署到本地网络,首先需要启动本地测试节点。为此,请打开 CLI 并运行以下命令:

npx hardhat node

当我们运行此命令时,您应该会看到地址和私钥列表

[ 安全帽节点地址

这些是为我们创建的 20 个测试账户和地址,我们可以使用它们来部署和测试我们的智能合约。每个帐户还加载了 10,000 个假以太币。稍后,我们将学习如何将测试帐户导入 MetaMask 以便我们可以使用它。

接下来,我们需要将合约部署到测试网络。首先将scripts/sample-script.js的名称更新为scripts/deploy.js

现在我们可以运行部署脚本并为我们想要部署到本地网络的 CLI 提供一个标志:

npx hardhat run scripts/deploy.js --network localhost

执行此脚本后,智能合约应部署到本地测试网络,然后我们应该能够开始与之交互。

部署合约时,它使用了我们启动本地网络时创建的第一个帐户。

如果您查看 CLI 的输出,您应该能够看到如下内容:

Greeter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

这个地址是我们将在我们的客户端应用程序中用来与智能合约对话的地址。保持此地址可用,因为我们在从客户端应用程序连接到它时需要使用它。

要将交易发送到智能合约,我们需要使用运行时创建的帐户之一连接我们的 MetaMask 钱包npx hardhat node。在 CLI 注销的合约列表中,您应该会同时看到Account numberPrivate Key

➜  react-dapp git:(main) npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

...

我们可以将此帐户导入 MetaMask,以便开始使用那里可用的一些假 Eth。

为此,首先打开 MetaMask 并启用测试网络

[ 测试网络

接下来,将网络更新为 Localhost 8545:

[ 选择网络

接下来,在 MetaMask 中,从帐户菜单中单击Import Account :

[ 导入账户

复制然后粘贴由 CLI 注销的私钥之一,然后单击****导入。导入帐户后,您应该会看到帐户中的 Eth:

[ 导入账户

现在我们已经部署了一个智能合约和一个可以使用的帐户,我们可以开始从 React 应用程序与它进行交互。

连接 React 客户端

在本教程中,我们不会担心使用 CSS 构建漂亮的 UI 以及所有这些,我们将 100% 专注于核心功能,以帮助您启动和运行。如果你愿意,你可以从那里拿走它,让它看起来不错。

话虽如此,让我们回顾一下我们希望从 React 应用程序中实现的两个目标:

  1. greeting从智能合约中获取 的当前值
  2. 允许用户更新greeting

了解了这些东西,我们如何做到这一点?为了实现这一点,我们需要做以下事情:

  1. 创建一个输入字段和一些本地状态来管理输入的值(更新greeting
  2. 允许应用程序连接到用户的 MetaMask 帐户以签署交易
  3. 创建用于读取和写入智能合约的函数

为此,请src/App.js使用以下代码打开并更新它,将 的值设置greeterAddress为您的智能合约的地址:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

// Update with the contract address logged out to the CLI when it was deployed 
const greeterAddress = "your-contract-address"

function App() {
  // store greeting in local state
  const [greeting, setGreetingValue] = useState()

  // request access to the user's MetaMask account
  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  // call the smart contract, read the current greeting value
  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum)
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
      try {
        const data = await contract.greet()
        console.log('data: ', data)
      } catch (err) {
        console.log("Error: ", err)
      }
    }    
  }

  // call the smart contract, send an update
  async function setGreeting() {
    if (!greeting) return
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner()
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
      const transaction = await contract.setGreeting(greeting)
      await transaction.wait()
      fetchGreeting()
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={fetchGreeting}>Fetch Greeting</button>
        <button onClick={setGreeting}>Set Greeting</button>
        <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />
      </header>
    </div>
  );
}

export default App;

要对其进行测试,请启动 React 服务器:

npm start

当应用程序加载时,您应该能够获取当前问候语并将其注销到控制台。您还应该能够通过与您的 MetaMask 钱包签署合同并使用假 Ether 来更新问候语。

[ 设置和获取问候值

部署和使用实时测试网络

有几个以太坊测试网络,如 Ropsten、Rinkeby 或 Kovan,我们也可以部署到这些网络,以便在无需将其部署到主网的情况下获得可公开访问的合约版本。在本教程中,我们将部署到Ropsten测试网络。

首先,首先更新您的 MetaMask 钱包以连接到 Ropsten 网络。

[ 罗普斯滕网络

接下来,通过访问这个测试水龙头给自己发送一些测试以太币,以便在本教程的其余部分使用。

我们可以通过注册InfuraAlchemy之类的服务来访问 Ropsten(或任何其他测试网络)(我在本教程中使用 Infura)。

在 Infura 或 Alchemy 中创建应用程序后,您将获得一个如下所示的端点:

https://ropsten.infura.io/v3/your-project-id

请务必在 Infura 或 Alchemy 应用程序配置中设置ALLOWLIST ETHEREUM ADDRESSES,以包含您将要部署的帐户的钱包地址。

要部署到测试网络,我们需要使用一些额外的网络信息更新我们的安全帽配置。我们需要设置的一件事是我们将从中部署的钱包的私钥。

要获取私钥,您可以从 MetaMask 中导出它。

[ 导出私钥

我建议不要在您的应用程序中硬编码此值,而是将其设置为环境变量之类的东西。

接下来,添加networks具有以下配置的属性:

module.exports = {
  defaultNetwork: "hardhat",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {},
    ropsten: {
      url: "https://ropsten.infura.io/v3/your-project-id",
      accounts: [`0x${your-private-key}`]
    }
  },
  solidity: "0.8.4",
};

要部署,请运行以下脚本:

npx hardhat run scripts/deploy.js --network ropsten

部署合约后,您应该能够开始与之交互。您现在应该可以在Etherscan Ropsten Testnet Explorer上查看实时合约

铸造代币

智能合约最常见的用例之一是创建代币,让我们看看我们如何做到这一点。由于我们对所有这些是如何工作的有了更多的了解,所以我们会走得更快一点。

在主合约目录中创建一个名为Token.sol的新文件。

接下来,使用以下智能合约更新Token.sol :

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Token {
  string public name = "Nader Dabit Token";
  string public symbol = "NDT";
  uint public totalSupply = 1000000;
  mapping(address => uint) balances;

  constructor() {
    balances[msg.sender] = totalSupply;
  }

  function transfer(address to, uint amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");
    balances[msg.sender] -= amount;
    balances[to] += amount;
  }

  function balanceOf(address account) external view returns (uint) {
    return balances[account];
  }
}

请注意,此代币合约仅用于演示目的,不符合ERC20。我们将在这里介绍 ERC20 代币

该合约将创建一个名为“Nader Dabit Token”的新代币,并将供应量设置为 1000000。

接下来,编译这个合约:

npx hardhat compile

现在,更新scripts/deploy.js中的部署脚本以包含这个新的 Token 合约:

const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  console.log(
    "Deploying contracts with the account:",
    deployer.address
  );

  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, World!");

  const Token = await hre.ethers.getContractFactory("Token");
  const token = await Token.deploy();

  await greeter.deployed();
  await token.deployed();

  console.log("Greeter deployed to:", greeter.address);
  console.log("Token deployed to:", token.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

现在,我们可以将这个新合约部署到本地或 Ropsten 网络:

npx hardhat run scripts/deploy.js --network localhost

部署合约后,您可以开始将这些代币发送到其他地址。

为此,让我们更新我们需要的客户端代码以使其工作:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'
import Token from './artifacts/contracts/Token.sol/Token.json'

const greeterAddress = "your-contract-address"
const tokenAddress = "your-contract-address"

function App() {
  const [greeting, setGreetingValue] = useState()
  const [userAccount, setUserAccount] = useState()
  const [amount, setAmount] = useState()

  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum)
      console.log({ provider })
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
      try {
        const data = await contract.greet()
        console.log('data: ', data)
      } catch (err) {
        console.log("Error: ", err)
      }
    }    
  }

  async function getBalance() {
    if (typeof window.ethereum !== 'undefined') {
      const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' })
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const contract = new ethers.Contract(tokenAddress, Token.abi, provider)
      const balance = await contract.balanceOf(account);
      console.log("Balance: ", balance.toString());
    }
  }

  async function setGreeting() {
    if (!greeting) return
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      console.log({ provider })
      const signer = provider.getSigner()
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
      const transaction = await contract.setGreeting(greeting)
      await transaction.wait()
      fetchGreeting()
    }
  }

  async function sendCoins() {
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      const contract = new ethers.Contract(tokenAddress, Token.abi, signer);
      const transation = await contract.transfer(userAccount, amount);
      await transation.wait();
      console.log(`${amount} Coins successfully sent to ${userAccount}`);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={fetchGreeting}>Fetch Greeting</button>
        <button onClick={setGreeting}>Set Greeting</button>
        <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />

        <br />
        <button onClick={getBalance}>Get Balance</button>
        <button onClick={sendCoins}>Send Coins</button>
        <input onChange={e => setUserAccount(e.target.value)} placeholder="Account ID" />
        <input onChange={e => setAmount(e.target.value)} placeholder="Amount" />
      </header>
    </div>
  );
}

export default App;

接下来,运行应用程序:

npm start

我们应该能够点击获取余额并看到我们的帐户中有 1,000,000 个硬币已注销到控制台。

您还应该能够通过单击导入令牌在 MetaMask 中查看它们:

[ 导入令牌

接下来点击自定义代币并输入代币合约地址,然后添加自定义代币。(如果询问代币小数,请选择 0)现在代币应该在您的钱包中可用:

[ 无损检测

接下来,让我们尝试将这些硬币发送到另一个地址。

为此,请复制另一个帐户的地址,然后使用更新的 React UI 将它们发送到该地址。当您检查代币金额时,它应该等于原始金额减去您发送到地址的金额。

ERC20 代币

ERC20代币标准定义了一套适用于所有 ERC20 代币的规则,使它们能够轻松地相互交互。ERC20 让人们可以很容易地铸造自己的代币,这些代币将与以太坊区块链上的其他人具有互操作性。

让我们看看如何使用 ERC20 标准构建我们自己的代币。

首先,安装OpenZepplin智能合约库,我们将在其中导入基础ERC20令牌:

npm install @openzeppelin/contracts

接下来,我们将通过扩展(或继承自)ERC20合约来创建我们的代币:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NDToken is ERC20 {
    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        _mint(msg.sender, 100000 * (10 ** 18));
    }
}

构造函数允许您设置代币名称和符号,该_mint函数允许您铸造代币并设置数量。

默认情况下,ERC20 将小数位数设置为 18,因此在我们的_mint函数中,我们将 100,000 乘以 10 的 18 次方来铸造总共 100,000 个代币,每个代币有 18 位小数(类似于 1 Eth 由 10 到十八

要进行部署,我们需要传入构造函数值(namesymbol),因此我们可以在部署脚本中执行以下操作:

const NDToken = await hre.ethers.getContractFactory("NDToken");
const ndToken = await NDToken.deploy("Nader Dabit Token", "NDT");

通过扩展原始 ERC20 代币,您的代币将继承以下所有功能和功能:

function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)

部署后,您可以使用这些功能中的任何一个与新的智能合约进行交互。有关 ERC20 代币的另一个示例,请查看 [Solidity by example)( https://solidity-by-example.org/app/erc20/ )

结论

好的,我们在这里涵盖了很多内容,但对我来说,这是开始使用这个堆栈的基本知识/核心,并且是我想要拥有的东西,不仅作为一个正在学习所有这些东西的人,而且在未来,如果我需要参考我将来可能需要的任何东西。我希望你学到了很多。

如果你想在 MetaMask 之外支持多个钱包,请查看Web3Modal,它可以通过相当简单和可定制的配置轻松地在你的应用程序中实现对多个提供程序的支持。

在我未来的教程和指南中,我将深入研究更复杂的智能合约开发,以及如何将它们部署为子图以在它们之上公开 GraphQL API 并实现分页和全文搜索等功能。