开发 dApp 的三个步骤
去中心化应用,或者叫 dApp,是一种不依赖于中心化服务器的应用。相反,dApp 使用像是区块链和预言机这些 Web3 技术,来实现自己的逻辑和后台功能,具备不可篡改和安全的特性。
在这个技术教程中,你会学习到怎样开发一个 end-to-end 的 dApp。在 dApp 中,用户可以通过一个智能合约,获取和存储 ETH 的当前价格。这个教程 demo 代码存储在 Github 中。
要求
你需要先安装以下软件:
去中心化应用是什么?
与传统的 App 在中心化服务器运行后端代码不同的是,dApp 的后端代码是运行在区块链上的。当然,dApp 的前端代码和 UI 可以使用任何语言开发,可以部署在任何服务器上与后端逻辑相交互。
Due to housing their backend logic in highly secure, tamper-proof smart contracts, dApps enjoy many benefits not possible with traditional Web2 systems:
因为 dApp 可以通过安全性很高且不可篡改的智能合约来承载后端逻辑,所以 dApp 有很多 Web2 系统中没有的优势:
- 不会宕机
- 隐私性更强
- 抗操纵
- 在最小信任环境下执行逻辑
然而,这些优势也带来了对应的缺点。因为代码是部署在区块链上,这些逻辑默认是无法修改的,所以 dApp 的维护难度比较高。除此以外,因为代码是运行在分布式网络中,而不是中心化服务器,所以性能会比较低。另外,由于用户需要有 Web3 钱包并且通过有足够的加密资产来支付手续费,所以用户体验也会下降。
dApp 组件
dApp 的组件会有三个不同的类型:智能合约,前端逻辑(UI)和数据存储。
智能合约
智能合约存储了 dApp 的业务逻辑和当前的状态,这个是 dApp 和传统网络应用的最大区别,也正是因为这一点让 dApp 具备了以上提到过的优势。
前端 / UI
尽管后端逻辑需要开发者完成智能合约代码,并把它部署在区块链上,但是在前端,开发者还是使用标准的网络技术,比如 HTML 和 javascript,因此开发者可以使用自己熟悉的工具,库和框架。客户端的 UI 通常通过 Web3.js 和 Ether.js 与智能合约交互。像是对信息进行签名并且发送给智能合约这些操作,通常是通过浏览器的 Web3 钱包 MetaMask 完成。
数据存储
大多数应用需要存储数据,但是因为区块链分布式的特点,在链上存储大量的数据效率很低,而且非常贵。这也是为什么许多 dApp 需要使用 IPFS 或者 Filecoin 这样的链下存储服务来存储数据,只让区块链存储重要的业务逻辑和状态。
当然你也可以选择传统的云存储服务,然而还是有很多开发者选择分布式存储,因为区块链应用可以提供最小信任的特性。
现在我们知道了 dApp 的组件,让我们开发一个简单的 dApp。
第一步:创建智能合约
我们 dApp 中的智能合约是一个简单的例子,它可以查看数据并且反应出区块链上的变化。在这个例子中,我们会通过 Chainlink ETH/USD 喂价对查看 ETH/USD 的价格,然后将结果永久存储在智能合约上。
第一步是打开 Chainlink 的文档,然后导航到 Using Data Feeds 页面。从这里将源代码复制进你的 IDE 中的一个新的文件里(比如 Visual Code),或者你可以点击“Open In Remix”按钮,然后使用在线 IDE Remix。
在这个例子中,我们会使用 Visual Studio Code 和 Hardhat(一个 EVM 开发框架)。
首先,为我们的 dApp 创建一个新的文件夹,并在这个文件夹中创建一个后端文件夹,用来存储智能合约代码:
mkdir chainlink-dapp-example cd chainlink-dapp-example mkdir backend cd backend
接下来,通过 VS Code 打开创建好的文件夹,然后安装 Hardhat
npm init -y npm install --save-dev hardhat npx hardhat (choose create javascript project, choose default parameters)
当安装完成之后,在“contracts”文件夹中删掉 Touch.sol ,然后在这个文件夹中创建一个叫做 PriceConsumerV3.sol 的文件。在这个文件将存储我们的合约,所以将 Chainlink 文档中的代码复制到这个文件中,然后保存。
在样例代码中,你会看到 demo 合约已经有一个叫做 getLatestPrice 的功能来通过 Rinkeby 上的 ETH/USD 喂价对查看 Ethereum 的当前价格。
function getLatestPrice() public view returns (int) { ( /*uint80 roundID*/, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = priceFeed.latestRoundData(); return price;
创建一个新的变量和函数,在智能合约上储存这个值。
int public storedPrice;
然后,创建一个新的函数,它会被 dApp 的前端调用。这个函数会通过调用 getLatestPrice 函数查看 Ethereum 的最新价格,然后将这个值存储在 storedPrice 这个参数中:
function storeLatestPrice() external { storedPrice = getLatestPrice(); }
你的新的合约应该和下面的一样:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.7; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract PriceConsumerV3 { AggregatorV3Interface internal priceFeed; int public storedPrice; /** * Network: Rinkeby * Aggregator: ETH/USD * Address: 0x8A753747A1Fa494EC906cE90E9f37563A8AF630e */ constructor() { priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e); } /** * Returns the latest price */ function getLatestPrice() public view returns (int) { ( /*uint80 roundID*/, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = priceFeed.latestRoundData(); return price; } function storeLatestPrice() external { storedPrice = getLatestPrice(); } }
第二步:部署智能合约
现在你已经可以在 Rinkeby 测试网中编译和部署你的合约了,如果没有测试网的通证的话,可以在 Chainlink faucet 获得一些。
如果你使用的是 Remix 的话,你可以通过 Remix 编译和部署你的合约。如果你使用的是像是 Visual Studio Code 这样的 IDE 的话,我们推荐使用 Hardhat 来管理你的合约。
在部署合约之前,第一步是安装 Hardhat 工具包,Chainlink 合约库和 dotenv 库。dotenv 可以将存储密码和敏感信息存储在一个单独的 .env 文件中:
npm install --save-dev @nomicfoundation/hardhat-toolbox npm install @chainlink/contracts --save npm install dotenv
然后,将 hardhat-config.js 文件中的内容换成下面的内容:
require("@nomicfoundation/hardhat-toolbox"); //require("@nomiclabs/hardhat-ethers") require('dotenv').config() const RINKEBY_RPC_URL = process.env.RINKEBY_RPC_URL || "https://eth-rinkeby.alchemyapi.io/v2/your-api-key" const PRIVATE_KEY = process.env.PRIVATE_KEY || "abcdef" module.exports = { defaultNetwork: "rinkeby", networks: { hardhat: { // // If you want to do some forking, uncomment this // forking: { // url: MAINNET_RPC_URL // } }, localhost: { }, rinkeby: { url: RINKEBY_RPC_URL, accounts: [PRIVATE_KEY], saveDeployments: true, }, }, solidity: "0.8.9", };
下一步是在 backend 文件夹中创建一个 .env 文件。然后你需要从 Web3 钱包中获取你的私钥,然后粘贴到 PRIVATE_KEY 这一行。请再确定一下,为了安全你在这个例子中最好使用一个在主网上没有任何的资产的新 Web3 钱包。
当这些做完以后,你需要一个 RPC endpoint 来接入 Rinkeby 网络。你可以将它粘贴到 .env 文件的 RINKEBY_RPC_URL 中的 RPC URL 中。我们推荐注册一个免费的 Infura 或者 Alchemy 账户以获取一个 RPC URL。
创建 .env 文件
下一步是修改“script” 文件夹中 deploy.js 文件中的内容,使得它可以部署你的新合约。打开文件,然后将代码替换为下列代码。
// We require the Hardhat Runtime Environment explicitly here. This is optional // but useful for running the script in a standalone fashion through `node <script>`. // // You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat // will compile your contracts, add the Hardhat Runtime Environment's members to the // global scope, and execute the script. const hre = require("hardhat"); async function main() { const PriceConsumer = await hre.ethers.getContractFactory("PriceConsumerV3"); const priceConsumer = await PriceConsumer.deploy(); await priceConsumer.deployed(); console.log("Contract deployed to:", priceConsumer.address); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch((error) => { console.error(error); process.exitCode = 1; });
现在你已经可以通过 Hardhat 来编译你的智能合约并且把它部署在 Rinkeby 网络中:
npx hardhat compile
npx hardhat run --network rinkeby scripts/deploy.js
你现在应该看到类似下面这行的信息,会展示你部署在 Rinkeby 网络上的智能合约地址。注意这个地址,我们在后面的步骤中需要用到它。
部署的智能合约
恭喜,你已经完成了 dApp 的合约部分!
第三步:创建前端应用
dApp 的前端逻辑和 UI 可以通过各种框架完成。
React 是最受欢迎的 Javascript 代码库之一,它可以用来开发功能丰富的网页,因此也被许多 Web3 dApp 所使用。除此之外,Ether.js 是一个 Javascript 库,它是用来和 EVM 区块链连接和交互的。当你把这两者结合起来,就可以开始开发你的 dApp 的前端的了。
在这部分,我们将使用 create-react-app 创建一个新的 React 应用。然后我会介绍如何通过 Ether.js 来将 UI 和已经部署的智能合约连接起来,完成一个 end-to-end 的 dApp。
创建 React 应用
开发前端代码之前,需要先安装和初始化一个 cerate-react-app 项目,然后修改它以满足我们的 dApp。第一步将这个库安装到“frontend”文件夹:
cd ..
npx create-react-app frontend
这一步完成后,你应该可以在 “frontend” 文件夹中看到所有相关的 React 代码。打开“frontend” 文件夹然后做以下操作:
- 删除 /src/setupTests.js
- 删除 /src/ReportWebVitals.js
- 删除 /src/logo.svg
- 删除 /src/App.test.js
- 删除 /src/App.css
文件夹结构应该如下所示:
React front-end 文件夹结构
在修改 React 应用代码之前,我们需要先安装 Bootstrap 和 Ether.js。Bootstrap 是一个很流行的前端 CSS 框架,有很多 React 可以使用的 UI widgets 和 CSS 样式。Ether.js 可以将前端代码与区块链上已经部署的智能合约相连接。在 “frontend” 文件夹中输入以下命令:
cd frontend npm install bootstrap npm install ethers
现在我们可以开始修改 React 应用的代码,在 /src/ 文件夹中打开 App.js 文件,然后删掉这些内容。我们从 0 开始编写。
第一步是告诉 app 我们想要使用 React(包括 useEffect 和 useState 库)和 Ether.js:
import React, { useEffect, useState } from 'react';
import { ethers } from "ethers";
下一步,创建一个叫 “App” 的函数然后 export 它:
function App() { } export default App;
现在我们将开始完成 “App” 函数的代码。加入下面的代码,这些代码会做以下的操作:
- 建立 storePrice 和 setStoresPrice 的 react hook。
- 连接你的 Metamask Web3 钱包。
- 设置已经部署的智能合约地址和 ABI。Ether.js 在与已经部署的合约交互的时候需要这两个信息。
- 把智能合约地址这个值(可以在部署的时候获得)插入到 REPLACE_WITH_DEPLOYED_CONTRACT_ADDRESS 这里。
- 智能合约的 ABI 可以从文件 /backend/artifacts/contracts/PriceConsumerV3.json 中获得,你还可以使用 code minifier 对它进行更好的格式化,存储在你的应用中。
const [storedPrice, setStoredPrice] = useState(''); const provider = new ethers.providers.Web3Provider(window.ethereum) const signer = provider.getSigner() const contractAddress = <REPLACE_WITH_DEPLOYED_CONTRACT_ADDRESS>’'; const ABI = '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"getLatestPrice","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"storeLatestPrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"storedPrice","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"}]' const contract = new ethers.Contract(contractAddress, ABI, signer);
现在我们在应用中创建两个函数:
- getStoredPrice 会连接部署的合约,并且通过 storedPrice() 获取当前价格。
- setNewPrice 会调用已部署合约的 storeLatestPrice 函数,等到交易完成,然后调用 getStoredPrice 函数来获取存储在智能合约中的价格。
我们会在应用中加入 getStoredPrice 函数,它会在加载页面的时候调用 getter 函数:
const getStoredPrice = async () => { try { const contractPrice = await contract.storedPrice(); setStoredPrice(parseInt(contractPrice) / 100000000); } catch (error) { console.log("getStoredPrice Error: ", error); } } async function updateNewPrice() { try { const transaction = await contract.storeLatestPrice(); await transaction.wait(); await getStoredPrice(); } catch (error) { console.log("updateNewPrice Error: ", error); } } getStoredPrice() .catch(console.error)
前端代码的最后一步是返回 JSX 代码以让浏览器 render。将下面的代码复制进 App 的函数中,在 getStorePrice() 的下面。这些代码会做下面的操作:
- 返回一个简单的 2 列 grid layout。
- 第一列包含了智能合约中存储的 ETH/USD 价格。
- 第二列包含了一个按钮,用户可以使用这个按钮来与智能合约交互,更新存储的价格。点击按钮,然后调用下面的 setNewPrice 函数。
return ( <div className="container"> <div className="row mt-5"> <div className="col"> <h3>Stored Price</h3> <p>Stored ETH/USD Price: {storedPrice}</p> </div> <div className="col"> <h3>Update Price</h3> <button type="submit" className="btn btn-dark" onClick={updateNewPrice}>Update</button> </div> </div> </div> );
你的应用现在已经完成了。如果需要,你可以和这里完整代码比较,保证你的代码中没有错误。你可以运行你的 dApp 了。
运行你的 dApp
在确认你所有的文件都已经存储以后,在 frontend 文件夹中运行以下命令来启动你的 dApp:
npm run start
在应用被载入以后,浏览器中会有一个新的窗口,展示 dApp 的 UI,你应该从 Metamask 看到一个弹出的通知,请求将钱包连接到这个应用上。
React 前端
在检查你在 Metamask 的账户中有一些 Rinkeby ETH 以后,点击 “Update” 按钮,就可以和你已经部署好的智能合约进行交互。你应该会收到 Metamask 的通知,请求你确认交易。在你完成这些以后,过几秒你的 dApp 会自动刷新,然后当前的 Ethereum 会出现在 “Stored Price” 区域:
React 前端展示 Data Feed 结果
恭喜,你已经成功创建,部署并且交互了一个简单的 dApp!在这个教程中,你只是在你的电脑上运行了一个本地前端,同时你也可以把它部署在云服务器中,或者使用去中心化版本的前端,可以将其部署在 IPFS 中!你也可以修改应用的 CSS 来改变 UI 让它更符合你的使用场景。
总结
去中心化应用可以用像是区块链和智能合约这些 Web3 科技替代传统的后端服务器,带来传统应用没有安全性和抗操纵的特点。
在这个 demo 中,我们创建了一个简单的 dApp,dApp 中包含了一个智能合约,这个智能合约可以从 Chainlink ETH/USD 喂价对中获得最新的价格,然后储存在智能合约之中。然后我们创建了一个简单的 UI,使用了 React 和 Ether.js 连接并且与部署好的合约相交互。
您可以关注 Chainlink 预言机并且私信加入开发者社区,有大量关于智能合约的学习资料以及关于区块链的话题!