如何发行一款NFT(下)

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

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

官网规划

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

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

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

542.jpeg

543.jpeg

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 (
    <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因为和文章背景一致所以看不出来)

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;

主功能tab

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内部进行页面切换 546.jpeg

钱包连接入口

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

  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 (
        <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

我们钱包组件有以下能力

  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 <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列表展示,需要从合约中读取一些数据,包括

  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 < 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个功能

  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 (
        <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行业的同学。

  • 学分: 64
  • 分类: NFT
  • 标签: NFT 
点赞 5
收藏 9
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
web3探索者
web3探索者
0x3167...f450
元宇宙新著民致力研究web3 会定期分享web3技术 公众号:web3探索者