本文我们将了解到web3/区块链/智能合约应用如何在前端(HTML和Javascript)一起工作。如何将Metamask、Phantom或其他区块链钱包地址连接到前端页面。以及了解有哪些流行的前端框架(Nextjs / React 等),使我们开发更轻松
也许你刚刚用solidity、rust 编写了一个链上程序,但是如果没有一个很好的前端交互,几乎没有人可以使用它。
在这篇文章中,我们将了解如何在前端应用中,使用HTML和JavaScript与链上应用(智能合约或其他应用)交互。
并通过六种不同的方式,将你的Metamask、Phantom或其他区块链钱包地址连接到前端。最后,我们将看看有哪些流行的Nextjs / React前端软件包,可以辅助我们进行 web3 应用开发。
那么,让我们开始吧。
为了让web3体验友好,我们需要有用户友好的前端网站。全栈软件工程师在刚进入区块链领域可能会遇到一些挑战:
我在问自己这个问题时,看了几乎所有最流行的解决方案,并试图弄清楚应该向开发者推荐什么。因此,在这篇文章中,我们将了解到:
如果你想看看现在一些专业的前端是什么样子,可以看一下Aave或Uniswap网站。
兴奋吗?我也是。我们开始吧。
当然也可以是其他的钱包,如浏览器中的另一个钱包,如Phantom、Walletconnect等。
大多数区块链应用程序使用Hardhat、Brownie、DappTools、Anchor或Foundry等框架构建(或者Remix 工具)。而前端则使用在传统web2开发里学到的哪些东西:HTML、JavaScript、CSS,以及NextJS、React和Angular等框架。
因此,如果你熟悉传统的网络开发,你就会走在别人的前面!
现在,跟上步伐,先安装Metamask,观看这个视频以获得更深入的了解,安装完成之后,在页面右键单击,然后点击”检查(inspect)“:
右击屏幕,点击 检查(inspect)
或 检查元素(inspect element)
之后,可以看到像如下的内容:
他们是显示渲染网站页面的代码。然后,如果你点击顶部栏中的 sources
,会看到如下图内容。(如果你找到sources
,你可以点击>>
按钮来显示更多选项)。
如果你在浏览器中安装了Metamask,你会在左边看到一个 Metamask
文件。如果你安装了Phantom,你会看到一个 Phantom
。
他们是浏览器插件做的一些有趣的事情,它们自动 注入
你的浏览器,并作为你所在网站的一部分显示出来,让网站有机会与它们交互。
每个浏览器中都有一个 window
对象。我们可以通过点击console(控制台)
,进入 JavaScript控制台, 输入window,查看这个对象:
让我们继续输入window
,看看我们得到什么。
我们在浏览器中看到了JavaScript的window
对象。因为我们安装了Metamask
,此时会有一个ethereum
属性附加到window
对象上。输入window.ethereum
,看看返回了什么(如果你有Phantom,你可以试试 window.solana
)。
你会看到返回了一个对象! 如果你没有Metamask,你会得到一个undefined
。每个浏览器的钱包都会给window对象添加自己的属性,你通常可以在各自钱包的文档中找到它。这里是Metamask文档,明确的介绍了window.ethereum
。
注意:在以前的版本中,为
window.web3
,后来改为window.ethereum
。
这就是所谓的区块链提供者(provider),那么我们为什么需要这个呢?
每当我们想从区块链上读取数据,调用函数,或进行交易时,都需要连接到区块链网络。如果我们发送交易,还需要将签名的交易发送到一个区块链节点,这样它就可以将其发送到网络中的所有其他区块链节点。
你可能曾经在区块链应用程序中使用过Alchemy、Infura或Moralis Speedy Nodes的RPC URL。这些都是 ”节点即服务(node-as-a-service)“提供者,他们会提供我们一个HTTP端点来向区块链节点发送请求。加密货币钱包也是如此,Metamasks内置有一个与区块链节点的连接。事实上,如果你去Metamask network
标签,你可以看到Metamask正在使用的RPC URL!
因此,每当我们用Metamask做一些事情,都会通过这个RPC URL进行API调用。
我们将首先展示这一切是如何在HTML和JavaScript中完成的,然后我们将转向使用Nextjs/React例子。在我的Github这里有一个使用HTML/JavaScript连接到加密货币钱包的完整例子,所有例子的列表也在我的GitHub里。
首先,让我们创建一个标准的HTML文档,我们会给它一个连接(connect)
按钮:
<!DOCTYPE html>
<html>
<head>
<title>Javascript Test</title>
</head><body>
<button id="connectButton">Connect</button>
</body>
</html>
可以给我们的按钮添加一些功能,添加一个script
标签,并创建一个JavaScript函数,寻找window.ethereum
,如果找到它,就发出连接请求:
<!DOCTYPE html>
<head>
<title>Javascript Test</title>
</head>
<body>
<button id="connectButton" onclick="connect()">Connect</button>
</body>
<script>
async function connect() {
if (typeof window.ethereum !== "undefined") {
try {
await window.ethereum.request({ method: "eth_requestAccounts" });
} catch (error) {
console.log(error);
}
}}
</script>
</html>
这就是连接需要的全部代码。 eth_requestAccounts
直接来自Metamask文档。如果你把文件命名为index.html
并在浏览器中运行,你的metamask就会弹出要求连接:
现在已经连接了Metamask,是时候发送一个交易了。这时我们可以使用ethersjs和web3js等包来连接我们的提供者,然后发送一个交易。通常情况下,在JavaScript中执行一个函数/发送一个交易的JavaScript 类似于这样:
const etheres = require("ethers")
contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const abi = // some big javascript ABI here...
const provider = new ethers.providers.JsonRpcProvider(/* alchemy or infura */)
const wallet = new ethers.Wallet(/* Private key */, provider)
const contract = new ethers.Contract(contractAddress, abi, wallet)
const contractWithSigner = contract.connect(wallet)
const transactionResponse = contract.someFunction()
在浏览器中发送交易的唯一区别是,我们将提供者改为window.ethereum
,现在wallet
将直接来provider
。由于Metamask即是我们的提供者也是钱包(或签名者),代码将看起来像这样:
const etheres = require("ethers")
contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const abi = // some big javascript ABI here...
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer)
const contractWithSigner = contract.connect(wallet)
const transactionResponse = contract.someFunction()
你会注意到,只有中间的两行改变了,现在从window.ethereum
获得钱包,我们的签名者(signer)来自提供者(即metamask)。
现在,这里有一个问题。我们的浏览器无法识别require
(有时import
也有问题),所以需要添加一些包来帮助我们。
因为我不希望这里变成一个介绍前端的文章,你可以参看我的html-js-ethers-connect的例子,它向我们展示了如何自己运行示例。你只需要安装以下东西就可以了:
然后,你可以按照README.md中的说明进行初始化,用纯HTML和JavaScript做一个完整的例子,在浏览器中发送交易!
你将拥有一个与智能合约一起工作的简约的前端!
没有特别的顺序
现在,让我们开始为全栈应用提供所需的工具。这些配置将包括:
你可以选择最适合你的那一个! 我们用NextJS来做这些工作,因为ReactJS是目前地球上最流行的前端框架,而NextJS是建立在它之上的,在我看来,它比原始的ReactJS更方便使用。然而,你100%可以用Angular、Svelte或其他方式工作。
你可以找到我所有的简约代码示例full-stack-web3-metamask-connectors仓库,其中链接出所有的演示。
为了方便入门,所有这些项目都将从一个基本的NextJS项目开始。需要安装Node、Git和Yarn才能继续。你还可以跟随nextjs入门文档。
运行以下命令:
yarn create next-app full-stack-web3
cd full-stack-web3
现在有了一个基本的项目框架,现在可以运行yarn dev
,看看现在的网站会是什么样子。最后,删除所有开始时的 示例代码
,进入index.js
文件,删除所有内容,仅保留:
export default function Home() {
return <div>Hi</div>;
}
现在前端就显示一个 Hi
。
现在,由于我们要测试函数交互,因此需要一个区块链来发送交易,以及相应的智能合约。代码已经为准备好了,在代码库hardhat-simple-storage GitHub。你可以按照README.md
来进行设置,或者新开一个命令终端(与前端不同的终端)运行以下程序。
git clone https://github.com/PatrickAlphaC/hardhat-simple-storage
cd hardhat-simple-storage
yarn
yarn hardhat node
此时会启动一个本地区块链,给你一些临时私钥(账号),可用于部署 SimpleStorage
合约,合约有一个 store
函数。它接收一个uint256 _favoriteNumber
作为输入参数,并将该数字存储到一个公共变量中。在SimpleStorage.sol
文件中可以查看该合约代码。
现在,要将Metamask连接到我们的本地区块链。这样就可以快速发送交易和测试。本地区块链和真实的区块链类似,但这个区块链是我们可以控制的。如果你愿意,你也可以使用测试网,跳过这一步,但你必须等待很长的时间来处理交易,这是没有人愿意的。
在区块链节点运行的终端,你会看到一个类似的输出:Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
。这就是RPC URL,类似于Alchemy。
现在,在Metamask中(请永远不要使用有真实资金的Metamask进行开发。最好创建一个新的浏览器账号配置(Profile)或下载另一个有Metamask插件的浏览器)点击顶部的网络按钮,然后 添加网络(Add Network)
。
按如下内容设置它,然后点击保存,然后确保你切换到该网络(在网络下拉列表中选择刚设置的网络)。
现在,点击右上方的大圆圈(账号),然后点击 导入账户(import account)
。
然后从 yarn hardhat node
命令的输出中添加一个私钥。之后,你应该看到一个账户,在本地网络上,并且有一些测试ETH。Metamask应该看起来像这样:
然后我们就可以开始了 :)
重要提示:如果你遇到了
nonce
被关闭的问题,或者交易不能正常发送。在metamask中,去右上方的圆圈->设置->高级->重置账户。就可以消除nonce的问题。
最简单的方法是使用一些你已经熟悉的工具,比如Ethers,我们可以从复制粘贴在HTML设置中的内容到index.js
文件中:
import styles from "../styles/Home.module.css";
import { ethers } from "ethers";
import { useEffect, useState } from "react";
export default function Home() {
const [isConnected, setIsConnected] = useState(false);
const [hasMetamask, setHasMetamask] = useState(false);
const [signer, setSigner] = useState(undefined);
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
setHasMetamask(true);
}
});
async function connect() {
if (typeof window.ethereum !== "undefined") {
try {
await ethereum.request({ method: "eth_requestAccounts" });
setIsConnected(true);
const provider = new ethers.providers.Web3Provider(window.ethereum);
setSigner(provider.getSigner());
} catch (e) {
console.log(e);
}
} else {
setIsConnected(false);
}
}
async function execute() {
if (typeof window.ethereum !== "undefined") {
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const abi = [
{
inputs: [
{
internalType: "string",
name: "_name",
type: "string",
},
{
internalType: "uint256",
name: "_favoriteNumber",
type: "uint256",
},
],
name: "addPerson",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
name: "nameToFavoriteNumber",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
name: "people",
outputs: [
{
internalType: "uint256",
name: "favoriteNumber",
type: "uint256",
},
{
internalType: "string",
name: "name",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "retrieve",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "_favoriteNumber",
type: "uint256",
},
],
name: "store",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
];
const contract = new ethers.Contract(contractAddress, abi, signer);
try {
await contract.store(42);
} catch (error) {
console.log(error);
}
} else {
console.log("Please install MetaMask");
}
}
return (
<div>
{hasMetamask ? (
isConnected ? (
"Connected! "
) : (
<button onClick={() => connect()}>Connect</button>
)
) : (
"Please install metamask"
)}
{isConnected ? <button onClick={() => execute()}>Execute</button> : ""}
</div>
);
}
为此,我们添加了一些额外的功能,以便在连接或用户没有Metamask时显示 请安装Metamask
或 已连接
。你还会看到像useState
和useEffect
这样的命令,这些被称为React Hooks,你可以从这个Fireship视频或react docs.中了解它们的全部内容。虽然没有它们,这个应用也可以正常工作,只是我们无法在渲染之间保存应用的状态。
另外,在下面的例子中,我打算从另一个文件中导入abi
,这样就不会让文章的内容臃肿了。
将基于EVM的区块链应用程序连接到钱包的另一种最流行的方式是使用Walletconnect。我将要展示的所有例子(包括原始Ethers的例子)都可以连接到Walletconnect(而且应该连接),使用 Web3Modal 并不是唯一可选的工具。Walletconnect 团队成员创建的创建了这个奇妙的Web3Modal工具,它允许使用一个框架来连接到任何Provider,包括Ledger、WalletConnect、Torus、Coinbase Wallet,等等。
我们只需要导入这个包,之后index.js
可能看起来像这样:
import styles from "../styles/Home.module.css";
import Web3Modal from "web3modal";
import { useState, useEffect } from "react";
import { ethers } from "ethers";
import WalletConnectProvider from "@walletconnect/web3-provider";
import { abi } from "../constants/abi";
let web3Modal;
const providerOptions = {
walletconnect: {
package: WalletConnectProvider, // required
options: {
rpc: { 42: process.env.NEXT_PUBLIC_RPC_URL }, // required
},
},
};
if (typeof window !== "undefined") {
web3Modal = new Web3Modal({
cacheProvider: false,
providerOptions, // required
});
}
export default function Home() {
const [isConnected, setIsConnected] = useState(false);
const [hasMetamask, setHasMetamask] = useState(false);
const [signer, setSigner] = useState(undefined);
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
setHasMetamask(true);
}
});
async function connect() {
if (typeof window.ethereum !== "undefined") {
try {
const web3ModalProvider = await web3Modal.connect();
setIsConnected(true);
const provider = new ethers.providers.Web3Provider(web3ModalProvider);
setSigner(provider.getSigner());
} catch (e) {
console.log(e);
}
} else {
setIsConnected(false);
}
}
async function execute() {
if (typeof window.ethereum !== "undefined") {
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const contract = new ethers.Contract(contractAddress, abi, signer);
try {
await contract.store(42);
} catch (error) {
console.log(error);
}
} else {
console.log("Please install MetaMask");
}
}
return (
<div>
{hasMetamask ? (
isConnected ? (
"Connected! "
) : (
<button onClick={() => connect()}>Connect</button>
)
) : (
"Please install metamask"
)}
{isConnected ? <button onClick={() => execute()}>Execute</button> : ""}
</div>
);
}
你会看到,我们设置了一些providerOptions
来告诉前端要支持哪些钱包,以及我们要支持哪些链以及需要设置一个NEXT_PUBLIC_RPC_URL
,它指向一个RPC_URL来连接到区块链。如果我们使用walletconnect,我们实际上不使用用户的metamasks的内置区块链节点。
如果你想看看Web3Modal、区块链等的一些前沿的前端使用,可以查看Scaffold-ETH。这是一个了不起的学习工具,由Austin Griffith编写,你可以用来解构一些最佳实践。
Moralis(或者更具体地说,react-moralis)是第一个包含上下文管理组件的软件包,它是非常有用的。它允许整个应用在组件之间轻松地共享状态,这是必要的,因为我们需要传递Metamask的授权。
Moralis是由Ivan on Tech及其团队创建,不仅可以帮助开发者连接到Metamask,还可以帮助开发其他后端系统(全栈应用可能需要)。Etherscan和Opensea都是web3应用程序的例子,它们仍然需要后台和数据库。为什么呢?因为很多时候,你想添加大量的功能,在链上做起来会花费太多Gas!所以你仍然想有一个后台和数据库。
因此,你仍然让智能合约做主要工作,而Moralis可以做所有围绕它的一些工作。下面是使用Moralis的代码:
import styles from "../styles/Home.module.css";
import { useMoralis, useWeb3Contract } from "react-moralis";
import { abi } from "../constants/abi";
import { useState, useEffect } from "react";
export default function Home() {
const [hasMetamask, setHasMetamask] = useState(false);
const { enableWeb3, isWeb3Enabled } = useMoralis();
const { data, error, runContractFunction, isFetching, isLoading } =
useWeb3Contract({
abi: abi,
contractAddress: "0x5FbDB2315678afecb367f032d93F642f64180aa3", // your contract address here
functionName: "store",
params: {
_favoriteNumber: 42,
},
});
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
setHasMetamask(true);
}
});
return (
<div>
{hasMetamask ? (
isWeb3Enabled ? (
"Connected! "
) : (
<button onClick={() => enableWeb3()}>Connect</button>
)
) : (
"Please install metamask"
)}
{isWeb3Enabled ? (
<button onClick={() => runContractFunction()}>Execute</button>
) : (
""
)}
</div>
);
}
你会看到Moralis带有强大的Hook函数,如useWeb3Contract
,使获得状态和与合约交互更加容易,而且不需要ethers。
Moralis 还提供的enableWeb3
函数代替了自己编写的connect
函数。
此外,在_app.js
中,需要用一个Context提供者来包装整个应用程序:
import "../styles/globals.css";
import { MoralisProvider } from "react-moralis";
function MyApp({ Component, pageProps }) {
return (
<MoralisProvider initializeOnMount={false}>
<Component {...pageProps} />
</MoralisProvider>
);
}
export default MyApp;
Morlais有内置的属性选项,例如:可以用数据库设置前端,然而,如果你只想使用钩子和函数,你可以把initializeOnMount
设置为false,等将来需要时才设置服务器
Uniswap工程负责人Noah Zinsmeister和朋友们建立了一个优秀的软件包,叫做web3-react。这是被Uniswap、Aave和Compound等顶级项目最广泛使用的包之一。它还包含了一个上下文组件管理器和一些令人难以置信的强大的Hook 函数,让你可以直接上手并开始工作,还内置了一些web3钱包连接。
以下是index.js
修改后的代码:
import styles from "../styles/Home.module.css";
import { useWeb3React } from "@web3-react/core";
import { InjectedConnector } from "@web3-react/injected-connector";
import { abi } from "../constants/abi";
import { useState, useEffect } from "react";
import { ethers } from "ethers";
export const injected = new InjectedConnector();
export default function Home() {
const [hasMetamask, setHasMetamask] = useState(false);
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
setHasMetamask(true);
}
});
const {
active,
activate,
chainId,
account,
library: provider,
} = useWeb3React();
async function connect() {
if (typeof window.ethereum !== "undefined") {
try {
await activate(injected);
setHasMetamask(true);
} catch (e) {
console.log(e);
}
}
}
async function execute() {
if (active) {
const signer = provider.getSigner();
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const contract = new ethers.Contract(contractAddress, abi, signer);
try {
await contract.store(42);
} catch (error) {
console.log(error);
}
} else {
console.log("Please install MetaMask");
}
}
return (
<div>
{hasMetamask ? (
active ? (
"Connected! "
) : (
<button onClick={() => connect()}>Connect</button>
)
) : (
"Please install metamask"
)}
{active ? <button onClick={() => execute()}>Execute</button> : ""}
</div>
);
}
_app.js
代码:
import "../styles/globals.css";
import { Web3ReactProvider } from "@web3-react/core";
import { Web3Provider } from "@ethersproject/providers";
const getLibrary = (provider) => {
return new Web3Provider(provider);
};
function MyApp({ Component, pageProps }) {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<Component {...pageProps} />
</Web3ReactProvider>
);
}
export default MyApp;
正如你所看到的,我们仍然使用ethers与智能合约交互,但我们使用Hook 函数来启用Metamask和任何其他想要的钱包Provider
Ethworks 和最流行的测试框架 waffle , 他们背后是同一个团队,waffle被hardhat使用。现在他们又做了一个类似moralis的框架,你可以利用所有的Hooks和工具来构建一个前端,还包括一个上下文提供者。
下面是使用Ethworks后index.js
的代码:
import styles from "../styles/Home.module.css";
import { useEthers, useContractFunction } from "@usedapp/core";
import { useState, useEffect } from "react";
import { ethers } from "ethers";
import { abi } from "../constants/abi";
export default function Home() {
const { activateBrowserWallet, account } = useEthers();
const [hasMetamask, setHasMetamask] = useState(false);
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
setHasMetamask(true);
}
});
async function connect() {
await activateBrowserWallet();
}
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const contract = new ethers.Contract(contractAddress, abi);
const { send, state } = useContractFunction(contract, "store", {
transactionName: "store",
});
useEffect(() => {
console.log(`State: ${state.status}`);
}, [state]);
return (
<div>
{hasMetamask ? (
account ? (
"Connected! "
) : (
<button onClick={() => connect()}>Connect</button>
)
) : (
"Please install metamask"
)}
{account ? <button onClick={() => send(42)}>Execute</button> : ""}
</div>
);
}
_app.js
如下:
import "../styles/globals.css";
import { DAppProvider } from "@usedapp/core";
const config = {
multicallAddresses: ["0x5FbDB2315678afecb367f032d93F642f64180aa3"],
};
function MyApp({ Component, pageProps }) {
return (
<DAppProvider config={config}>
<Component {...pageProps} />
</DAppProvider>
);
}
export default MyApp;
向应用程序传递参数,用于配置如:支持的区块链和其他连接属性。与Moralis类似,useDapp
带有activateBrowserWallet
功能,用来激活metamask/浏览器钱包,以及像useContractFunction
这样的 hook 函数,与智能合约交互(你不必使用ethers)。
每个工具都有其各自的优缺点,你可以根据自己的喜好、醒目的需求进行选择。
编码愉快!
本翻译由 Duet Protocol 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!