# 如何发行一款NFT(下)

By [explorer_of_web3](https://paragraph.com/@explorer-of-web3) · 2022-05-04

---

我们通过2篇文章来介绍NFT的发布，第一篇重点讲解如何上链，可以归属于后端逻辑。第二篇讲解官方搭建，可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例，希望能够帮助到希望进入web3行业的同学。

上篇文章我们介绍了，如何部署NFT的智能合约到链上，这相当于NFT的后端部分完成，但作为NFT的宣发需要1个网站，属于NFT的前端部分。

官网规划
----

这个网站需要达成下面这几个目标:

1.  项目介绍
    
2.  NFT mint进度展示 以及mint功能
    
3.  NFT 列表展示
    
4.  联系方式
    

为了达成这些目标，我们通常会先完成草图的设计，在这里我直接使用项目最后的截图来展示UI架构，大家可以使用figma等工具完成第一步的草图设计。

![542.jpeg](https://storage.googleapis.com/papyrus_images/d4843a8daf447f5b7f767783d29e38fc1a287b9c05f28176a4466b30f087448e.jpg)

542.jpeg

![543.jpeg](https://storage.googleapis.com/papyrus_images/fa45d7205bf22bfccb5641c8863a0600dd11a07ca0528bee6eade7cf64130698.jpg)

543.jpeg

![544.jpeg](https://storage.googleapis.com/papyrus_images/b7dd580bdf983f55dd7833128466fba2f3018f4b6fd47360e050d76bfe53914d.jpg)

544.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.js

    import './topbar.css';
    
    const TopBarComponent = () => {
      return (
        &lt;div className='topbar'>
    
        &lt;/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(
      &lt;App />
    );
    

App.js

    import './App.css';
    import BottomBarComponent from './bottombar/BottomBarComponent';
    import ContentComponent from './content/ContentComponent';
    import TopBarComponent from './topbar/TopBarComponent';
    
    function App() {
      return (
        &lt;div className="App">
          &lt;TopBarComponent />
          &lt;ContentComponent />
          &lt;BottomBarComponent />
        &lt;/div>
      );
    }
    
    export default App;
    

此时我们的代码运行结果,已经将页面划分为3个区域(topbar因为和文章背景一致所以看不出来)

![545.jpeg](https://storage.googleapis.com/papyrus_images/88e2754bbdab6bc581292ab60846548f9a3ab45f79ba16640132aedd2d6f4bc8.jpg)

545.jpeg

接下来，我们就去逐步完善每个区域。

TopBar区域完善
----------

从最开始的图中可以看到TopBar可以分为3个区域:项目/社区名称，主功能tabs，连接钱包入口。

### 项目/社区名称

    import './topbar.css';
    const TitleComponent = () => {
        return (
            &lt;div className='title'>
                &lt;h1 className='title-text' > Web3探索者 &lt;/h1>
            &lt;/div>
        )
    }
    export default TitleComponent;
    

### 主功能tab

    import './topbar.css';
    const MainFeatureTabsComponent = ()=> {
        return (
            &lt;div className="tablist">
                &lt;div className="tablist-item">
                    &lt;a href='/' className='tablist-item-a'>简介&lt;/a>
                &lt;/div>
                &lt;div className="tablist-item" >
                    &lt;a href='/mint' className='tablist-item-a'> mint&lt;/a>
                &lt;/div>
                &lt;div className="tablist-item" >
                    &lt;a href='/list' className='tablist-item-a'>
                        NFT列表
                    &lt;/a>
                &lt;/div>
            &lt;/div>
        );
    }
    
    export default MainFeatureTabsComponent;
    

每次点击的时候切换子页面，后面Content页面的内容就会监听链接变化，发生变化则Content内部进行页面切换

### 钱包连接入口

钱包连接入口这里的逻辑有一些复杂。我们需要实现下面的目标

1.  页面初始化的时候判断钱包是否已经连接，如果已连接展示其钱包地址。
    
2.  中途用户连接钱包或断开钱包连接，需要更新对应状态，如果状态变为连接钱包需要展示钱包地址，如果状态变为未连接则展示连接钱包。
    
3.  钱包未连接状态下按钮可点击，点击拉起钱包选择面板，连接状态下按钮不可点击。
    

#### 钱包状态读取

钱包相关状态的读取，我们使用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 (
            &lt;div className='toolbar'>
                &lt;div className='connect-wallet' > 
                    &lt;h2 className='connect-wallet-text' 
                        onClick={() => { onclick_wallet() }}> {text} &lt;/h2>
                 &lt;/div>
            &lt;/div>
        );
    }
    
    const onclick_wallet = () => {
        if (Store.currentAccount.get() === "") {
            Store.wallet_is_show.set(true);
        }
    }
    export default ToolbarComponent;
    

我们点击钱包入口的时候，如果未连接需要展示钱包选择页面。

#### 钱包选择页面

在src目录下新建1个钱包页面目录wallet\_page

我们钱包组件有以下能力

1.  打开钱包面板时判断是否已经安装MetaMask，如果未安装，则直接提示请先安装MetaMask
    
2.  点击钱包面板时发起连接钱包请求
    
3.  钱包连接成功，自动关闭
    
4.  点击面板外其他位置时关闭面板
    

在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 &lt;div className='display-none'>&lt;/div>
        }
    
        const text = isMetaMaskInstalled() ? "MetaMask" : "请先安装 MetaMask";
        return (
            &lt;div className='wallet-page-bg' onClick={cancel_click}>
                &lt;div className='wallet-page-panel' onClick={(e) => {metaMask_click(e)}}> {text} &lt;/div>
            &lt;/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 (
            &lt;div className='join'>
                &lt;a href="https://twitter.com/twitter链接" target="_blank" className="join_item">
                &lt;img src={TWITTER_ICON} loading="lazy" alt=""/>
                &lt;/a>
                &lt;a href="https://discord.gg/discord链接" target="_blank" className="join_item">
                    &lt;img src={DISCORD_ICON} loading="lazy"/>
                &lt;/a>
            &lt;/div>
        );
    }
    export default JoinComponent;
    

在BottomBarComponent.js中添加该组件

Content区域完善
-----------

Content根据不同的tab将会展示3个页面:项目/社区简介页面, NFT展示页面, mint页面

### 项目/社区简介页面

在content目录下新建MainPageComponent.js组件

    import './content.css';
    function MainPageComponent() {
        return (
            &lt;div className='mainpage'>
                &lt;h1 className='overview-title'>
                    项目介绍
                &lt;/h1>
                &lt;p className='overview-text'>
                    白皮书内容
                &lt;/p>
            &lt;/div>
    
        );
    }
    export default MainPageComponent;
    

### NFT展示页面

这个页面将是1个展示所有已经mint NFT的列表。我们这里只展示每1个NFT的图片以及名字。

#### 与合约进行交互

NFT列表展示，需要从合约中读取一些数据，包括

1.  TokenID对应的TokenURI, 因为token的image name等信息都存储在TokenURI指向的metadata中
    
2.  目前已经Mint过的所有tokenID，因为上篇文章我们的mint规则是顺序mint，所以我们只需要获得当前已经Mint的token总数，然后从0遍历即可。
    

这里使用ethres.js与合约进行交互,添加ethres.js依赖

    npm i ethers
    

另外需要注意，需要将MetaMask网络切换到rinkeby, 因为上篇文章我们是将合约部署在rinkeby测试网络中,只有在该网络从才可以获取到相应的合约信息。

根目录下新建abi目录将我们上篇文章中编译合约生成的NFT\_WEB3\_EXPOLRER.json 复制进去。

另外还有2点

1.  我们需要解析TokenURI对应的metadata,从中获取到名字、图片等具体数据
    
2.  因为我们上篇文章中，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 &lt; 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) => 
            &lt;div className='nft-list-item' key={index}>
                &lt;img className='nft-list-item-img'  src={ipfsToHttp(metadata.image)} />
                &lt;h4 className='nft-list-item-name'> {metadata.name} &lt;/h4>
            &lt;/div>
        );
        console.log("items is" + items);
    
        return (
            &lt;div className='nft-list'>
                {items}
            &lt;/div>
    
        );
    }
    export default NFTListComponent;
    

接下来我们来实现最后1个页面NFT的mint页面

### NFT mint页面

这个页面有2个功能

1.  展示mint进度
    
2.  提供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 (
            &lt;div className='mintpage'>
                &lt;div className='mint-info'>当前mint进度 {"" + totalSupply} / {"" + maxSupply}&lt;/div>
                &lt;div className='mint-btn' onClick={mintClick}>一键mint&lt;/div>
            &lt;/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 (
        &lt;div className='content'>
          {(route != "/mint" && route != "/list") && &lt;MainPageComponent />}
          {(route == "/mint") && &lt;MintPageComponent />}
          {(route == "/list") && &lt;NFTListComponent />}
        &lt;/div>
      );
    }
    
    export default ContentComponent;
    

好了，目前为止我们的NFT官网就搭建完成了。基本的功能都已经实现，页面框架也有了。当然如果要进行上线官网的UI还需要进行仔细设计。后面还可以增加查看归属自己的NFT等功能，同时比如mint进度这里我们没有监听其他人在我们浏览页面过程中的mint事件，可能导致更新不及时，篇幅原因这里不再介绍。

结尾
--

我们通过2篇文章来介绍NFT的发布，第一篇重点讲解如何上链，可以归属于后端逻辑。第二篇讲解官方搭建，可以归属于前端逻辑。希望这些内容可以帮助有发行NFT的小伙伴发行属于自己的NFT。同时这2篇文章也是大家学习DApp开发的一个很好的示例，希望能够帮助到希望进入web3行业的同学。

---

*Originally published on [explorer_of_web3](https://paragraph.com/@explorer-of-web3/nft-2)*
