# 如何发行一款NFT(下) **Published by:** [explorer_of_web3](https://paragraph.com/@explorer-of-web3/) **Published on:** 2022-05-04 **URL:** https://paragraph.com/@explorer-of-web3/nft-2 ## Content 我们通过2篇文章来介绍NFT的发布,第一篇重点讲解如何上链,可以归属于后端逻辑。第二篇讲解官方搭建,可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例,希望能够帮助到希望进入web3行业的同学。 上篇文章我们介绍了,如何部署NFT的智能合约到链上,这相当于NFT的后端部分完成,但作为NFT的宣发需要1个网站,属于NFT的前端部分。官网规划这个网站需要达成下面这几个目标:项目介绍NFT mint进度展示 以及mint功能NFT 列表展示联系方式为了达成这些目标,我们通常会先完成草图的设计,在这里我直接使用项目最后的截图来展示UI架构,大家可以使用figma等工具完成第一步的草图设计。542.jpeg543.jpeg544.jpeg可以看到,页面可以分为3部分顶部栏: 包含项目名称、主功能tab、连接钱包按钮。内容区: 根据顶部栏所选tab不同分为项目简介页面、mint进度以及mint功能页面、NFT列表展示页面。底部栏: 包含联系方式。同时点击连接钱包按钮,我们需要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.jsimport './topbar.css'; const TopBarComponent = () => { return ( <div className='topbar'> </div> ); } export default TopBarComponent; ContentComponent.js、BottomBarComponent.js代码一致,不再重复。 调整index.js和App.js代码 index.jsimport 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.jsimport './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因为和文章背景一致所以看不出来)545.jpeg接下来,我们就去逐步完善每个区域。TopBar区域完善从最开始的图中可以看到TopBar可以分为3个区域:项目/社区名称,主功能tabs,连接钱包入口。项目/社区名称import './topbar.css'; const TitleComponent = () => { return ( <div className='title'> <h1 className='title-text' > Web3探索者 </h1> </div> ) } export default TitleComponent; 主功能tabimport './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.jsimport {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 我们钱包组件有以下能力打开钱包面板时判断是否已经安装MetaMask,如果未安装,则直接提示请先安装MetaMask点击钱包面板时发起连接钱包请求钱包连接成功,自动关闭点击面板外其他位置时关闭面板在wallet_page下新建组件WalletComponent.jsimport {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中断开已连接的网站,这样就可以进行反复测。BottomBar区域完善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区域完善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; NFT展示页面这个页面将是1个展示所有已经mint NFT的列表。我们这里只展示每1个NFT的图片以及名字。与合约进行交互NFT列表展示,需要从合约中读取一些数据,包括TokenID对应的TokenURI, 因为token的image name等信息都存储在TokenURI指向的metadata中目前已经Mint过的所有tokenID,因为上篇文章我们的mint规则是顺序mint,所以我们只需要获得当前已经Mint的token总数,然后从0遍历即可。这里使用ethres.js与合约进行交互,添加ethres.js依赖npm i ethers 另外需要注意,需要将MetaMask网络切换到rinkeby, 因为上篇文章我们是将合约部署在rinkeby测试网络中,只有在该网络从才可以获取到相应的合约信息。 根目录下新建abi目录将我们上篇文章中编译合约生成的NFT_WEB3_EXPOLRER.json 复制进去。 另外还有2点我们需要解析TokenURI对应的metadata,从中获取到名字、图片等具体数据因为我们上篇文章中,metadata的image是使用Ipfs存储,浏览器不直接支持ipfs协议,所以我们需要将ipfs协议转换成为https协议,使用的工具库是我们上篇文章上传ipfs的平台https://app.pinata.cloud/提供。添加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页面NFT mint页面这个页面有2个功能展示mint进度提供mint入口为了实现这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行业的同学。 ## Publication Information - [explorer_of_web3](https://paragraph.com/@explorer-of-web3/): Publication homepage - [All Posts](https://paragraph.com/@explorer-of-web3/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@explorer-of-web3): Subscribe to updates