我们通过2篇文章来介绍NFT的发布,第一篇重点讲解如何上链,可以归属于后端逻辑。第二篇讲解官方搭建,可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例,希望能够帮助到希望进入web3行业的同学。
上篇文章我们介绍了,如何部署NFT的智能合约到链上,这相当于NFT的后端部分完成,但作为NFT的宣发需要1个网站,属于NFT的前端部分。
这个网站需要达成下面这几个目标:
为了达成这些目标,我们通常会先完成草图的设计,在这里我直接使用项目最后的截图来展示UI架构,大家可以使用figma等工具完成第一步的草图设计。
可以看到,页面可以分为3部分
同时点击连接钱包按钮,我们需要1个钱包选择页面(目前只支持MetaMask)进行连接。
其中我们需要重点关注的是连接钱包按钮,钱包选择页面,mint页面, NFT展示页面。其余页面比较简单,为了完整性还是将其余部分代码展现,如果有前端基础的同学只查看上述重点页面实现即可。
项目框架: 项目采用Reac框架,页面整体状态由mbox框架来进行管理,我们只使用mbox最简单的用法。
钱包交互: 我们使用MetaMask API
合约交互: 使用ethers.js API
除了上述依赖库外,我们的项目将尽可能减少依赖库的使用
//使用react 官方脚手架初始化我们的项目
npx create-react-app nft-web3-explorer-page
cd nft-web3-explorer-page
//尝试运行项目
npm start
使用vscode打开我们的工程
我们按照页面框架功能区,在src下新建3个目录topbar、content、bottombar,并在3个目录下分别新建组件TopBarComponent.js、ContentComponent.js、BottomBarComponent.js。(css样式大家自行调整,不是本文重点)
TopBarComponent.js
import './topbar.css';
const TopBarComponent = () => {
return (
<div className='topbar'>
</div>
);
}
export default TopBarComponent;
ContentComponent.js、BottomBarComponent.js代码一致,不再重复。
调整index.js和App.js代码
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
App.js
import './App.css';
import BottomBarComponent from './bottombar/BottomBarComponent';
import ContentComponent from './content/ContentComponent';
import TopBarComponent from './topbar/TopBarComponent';
function App() {
return (
<div className="App">
<TopBarComponent />
<ContentComponent />
<BottomBarComponent />
</div>
);
}
export default App;
此时我们的代码运行结果,已经将页面划分为3个区域(topbar因为和文章背景一致所以看不出来)
接下来,我们就去逐步完善每个区域。
从最开始的图中可以看到TopBar可以分为3个区域:项目/社区名称,主功能tabs,连接钱包入口。
import './topbar.css';
const TitleComponent = () => {
return (
<div className='title'>
<h1 className='title-text' > Web3探索者 </h1>
</div>
)
}
export default TitleComponent;
import './topbar.css';
const MainFeatureTabsComponent = ()=> {
return (
<div className="tablist">
<div className="tablist-item">
<a href='/' className='tablist-item-a'>简介</a>
</div>
<div className="tablist-item" >
<a href='/mint' className='tablist-item-a'> mint</a>
</div>
<div className="tablist-item" >
<a href='/list' className='tablist-item-a'>
NFT列表
</a>
</div>
</div>
);
}
export default MainFeatureTabsComponent;
每次点击的时候切换子页面,后面Content页面的内容就会监听链接变化,发生变化则Content内部进行页面切换
钱包连接入口这里的逻辑有一些复杂。我们需要实现下面的目标
钱包相关状态的读取,我们使用MetaMask提供的API,我们在src目录下新建utils目录,在utils下新建walletUtils.js封装和MetaMask钱包的交互。
//判断钱包是否安装
export const isMetaMaskInstalled = () => {
const { ethereum } = window;
return Boolean(ethereum && ethereum.isMetaMask);
};
//请求连接钱包
export const metamaskConnect = async () => {
try {
const { ethereum } = window;
return await ethereum.request({ method: 'eth_requestAccounts' });
} catch (error) {
console.error(error);
}
};
//获取当前已经连接的钱包地址
export const getConnectAccount = async () => {
const { ethereum } = window;
const account = await ethereum.request({ method: 'eth_accounts' });
console.log("getConnectAccount" + account);
return account;
}
由于钱包状态是1个全局状态,并非组件内状态,所以我们使用mobx来进行存储以及其状态变化的通知监听。
添加mobx依赖
npm i mobx
在src目录下新增Store.js来管理全局状态
import { observable } from "mobx";
import { getConnectAccount } from "./utils/wallet_utils";
const help = () => {
// 钱包选择页面板是否展示状态
const wallet_is_show = observable.box(false);
// 当前连接的钱包地址,未连接则存储""
const currentAccount = observable.box("");
//连接钱包地址发生变化时的处理,更新存储状态
const handleAccountChange = (accounts) => {
if (accounts.length === 0) {
console.log("accounts is null");
currentAccount.set("");
} else if (accounts[0] !== currentAccount) {
console.log("accounts is " + accounts[0]);
currentAccount.set(accounts[0]);
}
}
//注册钱包状态的监听
const registerAccountChange = () => {
console.log("registerAccountChange");
const { ethereum } = window;
//注册时先获取一次当前状态
getConnectAccount().
then((accounts) => {
handleAccountChange(accounts);
}).catch((err) => {
console.error(err);
});
//使用metamask提供的API注册钱包状态监听
ethereum.on('accountsChanged', (accounts) => {
handleAccountChange(accounts);
});
}
return {wallet_is_show, registerAccountChange,currentAccount};
};
export const Store = help();
在App.js中增加初始化监听钱包连接变化的调用
useEffect(() => {
Store.registerAccountChange()
}, []);
完成钱包入口组件ToolbarComponent.js
import {Store} from '../Store';
import './topbar.css';
import {useState } from 'react';
const ToolbarComponent = () => {
//组件内部定义入口按钮展示文案
const [text, setText] = useState(Store.currentAccount.get() === "" ? "连接钱包" : Store.currentAccount.get());
useEffect(() => {
//使用mobx监听钱包状态的变化
Store.currentAccount.observe_((newAccount) => {
if (newAccount.newValue === "") {
setText("连接钱包");
} else {
setText(newAccount.newValue);
}
});
}, []);
return (
<div className='toolbar'>
<div className='connect-wallet' >
<h2 className='connect-wallet-text'
onClick={() => { onclick_wallet() }}> {text} </h2>
</div>
</div>
);
}
const onclick_wallet = () => {
if (Store.currentAccount.get() === "") {
Store.wallet_is_show.set(true);
}
}
export default ToolbarComponent;
我们点击钱包入口的时候,如果未连接需要展示钱包选择页面。
在src目录下新建1个钱包页面目录wallet_page
我们钱包组件有以下能力
在wallet_page下新建组件WalletComponent.js
import {Store} from '../Store';
import './wallet-page.css';
import { useEffect, useState } from 'react';
import { isMetaMaskInstalled, metamaskConnect } from '../utils/walletUtils';
const WalletComponent = () => {
//组件内部定义钱包选择页面是否展示的状态。
const [wallet_is_show, setWalletShow] = useState(false);
useEffect(() => {
//如果其他地方打开或关闭钱包选择页面,组件内部状态需要同步
Store.wallet_is_show.observe_((change) => {
setWalletShow(change.newValue);
});
// 如果账户状态发生改变,需要关闭该页面,比如连接钱包成功应该自动关闭该页面
Store.currentAccount.observe_((change) => {
Store.wallet_is_show.set(false);
});
}, []);
if (!wallet_is_show) {
return <div className='display-none'></div>
}
const text = isMetaMaskInstalled() ? "MetaMask" : "请先安装 MetaMask";
return (
<div className='wallet-page-bg' onClick={cancel_click}>
<div className='wallet-page-panel' onClick={(e) => {metaMask_click(e)}}> {text} </div>
</div>
);
};
const cancel_click = () => {
console.log("cancel_click");
Store.wallet_is_show.set(false);
}
const metaMask_click = async (e) => {
e.stopPropagation();
if (isMetaMaskInstalled()) {
metamaskConnect();
}
console.log("metaMask_click");
}
export default WalletComponent;
在App.js中添加WalletComponent组件
注意,在测试钱包连接入口组件和钱包选择页面时,我们可以在metamask中断开已连接的网站,这样就可以进行反复测。
import './bottombar.css';
const TWITTER_ICON = {图片链接}
const DISCORD_ICON = {图片链接}
function JoinComponent() {
return (
<div className='join'>
<a href="https://twitter.com/twitter链接" target="_blank" className="join_item">
<img src={TWITTER_ICON} loading="lazy" alt=""/>
</a>
<a href="https://discord.gg/discord链接" target="_blank" className="join_item">
<img src={DISCORD_ICON} loading="lazy"/>
</a>
</div>
);
}
export default JoinComponent;
在BottomBarComponent.js中添加该组件
Content根据不同的tab将会展示3个页面:项目/社区简介页面, NFT展示页面, mint页面
在content目录下新建MainPageComponent.js组件
import './content.css';
function MainPageComponent() {
return (
<div className='mainpage'>
<h1 className='overview-title'>
项目介绍
</h1>
<p className='overview-text'>
白皮书内容
</p>
</div>
);
}
export default MainPageComponent;
这个页面将是1个展示所有已经mint NFT的列表。我们这里只展示每1个NFT的图片以及名字。
NFT列表展示,需要从合约中读取一些数据,包括
这里使用ethres.js与合约进行交互,添加ethres.js依赖
npm i ethers
另外需要注意,需要将MetaMask网络切换到rinkeby, 因为上篇文章我们是将合约部署在rinkeby测试网络中,只有在该网络从才可以获取到相应的合约信息。
根目录下新建abi目录将我们上篇文章中编译合约生成的NFT_WEB3_EXPOLRER.json 复制进去。
另外还有2点
添加pinata/ipfs-gateway-tools依赖
npm i @pinata/ipfs-gateway-tools
在utils目录下新增contractUtils.js, 来实现一系列的封装方法达成上述功能。
// import Web3 from "web3"
import { ethers } from 'ethers';
import IPFSGatewayTools from '@pinata/ipfs-gateway-tools/dist/browser';
const gatewayTools = new IPFSGatewayTools();
//获取tokenID对应的URI
export const getTokenURI = async (tokenID) => {
const contract = getContract();
return await contract.tokenURI(tokenID);
}
//获取合约
const getContract = () => {
//在这里我们使用MetaMask向浏览器注入的provider
const provider = new ethers.providers.Web3Provider(window.web3.currentProvider);
const address = "合约地址";
const abi = require("../abi/NFT_WEB3_EXPOLRER.json").abi;
return new ethers.Contract(address, abi, provider);
}
//获取已经mint的token总数
export const totalSupplyToken = async () => {
const contract = getContract();
return await contract.totalSupply();
}
//获取已mint tokenURI对应的metadata
export const getMetaDataList = async () => {
const totalSupply = await totalSupplyToken();
console.log("totalSupplyToken is" + totalSupply);
const list = []
for (let tokenID = 0; tokenID < totalSupply; tokenID++) {
const tokenURI = await getTokenURI(tokenID);
const res = await parseMetaData(tokenURI);
list.push(res);
}
return list;
}
//通过tokenURI获取metadata
export const parseMetaData = async (TokenURI) => {
const res = await fetch(TokenURI);
const json = await res.json();
return json;
}
//将ipfs地址转为http地址
export const ipfsToHttp = (ipfsURL) => {
return gatewayTools.convertToDesiredGateway(ipfsURL, "https://gateway.pinata.cloud");
}
接下来在content目录下新建NFTListComponent组件调用上述方法进行展示
import './content.css';
import { getMetaDataList, ipfsToHttp } from "../utils/contractUtils";
import { useEffect, useState } from 'react';
function NFTListComponent() {
const [metadatalist, setMetadataList] = useState([]);
useEffect(() => {
//获取NFT的MetaMata列表
getMetaDataList()
.then((arr) => {
setMetadataList(arr);
})
.catch((err) => {
console.error("err is" + err);
});
}, []);
const items = metadatalist.map((metadata, index) =>
<div className='nft-list-item' key={index}>
<img className='nft-list-item-img' src={ipfsToHttp(metadata.image)} />
<h4 className='nft-list-item-name'> {metadata.name} </h4>
</div>
);
console.log("items is" + items);
return (
<div className='nft-list'>
{items}
</div>
);
}
export default NFTListComponent;
接下来我们来实现最后1个页面NFT的mint页面
这个页面有2个功能
为了实现这2个功能,我们需要增加一些与合约交互的能力。在contractUtils.js中新增3个函数
//获取最大可mint数量
export const maxSupplyToken = async () => {
const contract = getContract();
return await contract.MAX_SUPPLY();
}
//同时返回当前mint总数和最大mint总数
export const getMintInfo = async () => {
const totalSupply = await totalSupplyToken();
const maxSupply = await maxSupplyToken();
return [totalSupply, maxSupply];
}
// mint功能,这里同样是使用metamask进行签名,调用该方法MetaMask会弹出交易确认按钮
export const mint = async () => {
const contract = getContract();
const provider = new ethers.providers.Web3Provider(window.web3.currentProvider);
const signer = provider.getSigner();
const contractWithSigner = contract.connect(signer);
const price = await contract.PRICE_PER_TOKEN();
console.log("price is" + price);
const tx = await contractWithSigner.mint(1, { value: price });
console.log(tx.hash);
await tx.wait();
}
实现MintPageComponent组件
import { useEffect, useState } from 'react';
import { Store } from '../Store';
import { getMintInfo, mint } from '../utils/contractUtils';
import './content.css';
const MintPageComponent = () => {
const [totalSupply, setTotalSupply] = useState(0);
const [maxSupply, setMaxTotalSupply] = useState(0);
useEffect(() => {
updataMintInfo();
}, []);
const updataMintInfo = () => {
getMintInfo()
.then((info) => {
setTotalSupply(info[0]);
setMaxTotalSupply(info[1]);
})
.catch((err) => {
console.error(err);
});
}
const mintClick = () => {
if (Store.currentAccount.get() === "") {
window.alert("请先连接钱包");
return;
}
mint()
.then(() => {
//mint成功后更新mint信息
updataMintInfo();
window.alert("mint成功");
}).catch();
}
return (
<div className='mintpage'>
<div className='mint-info'>当前mint进度 {"" + totalSupply} / {"" + maxSupply}</div>
<div className='mint-btn' onClick={mintClick}>一键mint</div>
</div>
);
}
export default MintPageComponent;
最后我们在ContentComponent增加MainPageComponent、MintPageComponent、NFTListComponent,并设置路由状态变化的监听
import { useEffect, useState } from 'react';
import './content.css';
import MainPageComponent from './MainPageComponent';
import MintPageComponent from './MintPageComponent';
import NFTListComponent from './NFTListComponent';
const ContentComponent = () => {
const [route, setRoute] = useState("");
useEffect(() => {
setRoute(window.location.pathname);
});
return (
<div className='content'>
{(route != "/mint" && route != "/list") && <MainPageComponent />}
{(route == "/mint") && <MintPageComponent />}
{(route == "/list") && <NFTListComponent />}
</div>
);
}
export default ContentComponent;
好了,目前为止我们的NFT官网就搭建完成了。基本的功能都已经实现,页面框架也有了。当然如果要进行上线官网的UI还需要进行仔细设计。后面还可以增加查看归属自己的NFT等功能,同时比如mint进度这里我们没有监听其他人在我们浏览页面过程中的mint事件,可能导致更新不及时,篇幅原因这里不再介绍。
我们通过2篇文章来介绍NFT的发布,第一篇重点讲解如何上链,可以归属于后端逻辑。第二篇讲解官方搭建,可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例,希望能够帮助到希望进入web3行业的同学。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!