如何使用Fleek建立一个DApp并将其托管在IPFS上的教程

886 阅读12分钟

一个应用程序的部署阶段是开发过程中的一个关键步骤。在这一阶段,应用程序从本地托管到可供世界上任何地方的目标受众使用。

随着区块链在构建应用程序方面的使用越来越多,你可能已经想知道与智能合约互动的DApps是如何托管的。

在本教程中,你将通过使用React、Hardhat和Alchemy构建一个去中心化的宠物收养应用样本,学习如何用Fleek托管DApps。

在开始本教程之前你需要什么

本教程包含几个实践步骤。为了跟上进度,我建议你做以下工作:

  • 安装React,我们将用它来构建用户界面。我在本教程中使用的是React v14。
  • 安装Hardhat,我们将使用它作为我们的开发环境。
  • Alchemy区块链开发者平台创建一个免费账户
  • Fleek创建一个免费账户,你将在下一节了解更多信息
  • 下载MetaMask浏览器扩展

MetaMask是一个加密货币钱包,允许用户通过浏览器或移动应用程序访问DApps。你还会想要一个以太坊测试网的MetaMask测试账户,用于测试智能合约。我在本教程中使用的是Ropsten测试网络。

什么是Fleek?

Fleek是一个Web3解决方案,旨在使你的网站、DApps和服务的部署过程无缝化。目前,Fleek提供了一个网关,用于在InterPlanetary文件系统(IPFS)Dfinity的互联网计算机(IC) 上托管您的服务

Fleek将自己描述为相当于Web3应用程序的Netlify。因此,你会发现一些与Netlify相似的功能,如使用docker镜像执行构建和生成部署预览。

根据IPFS的博客,"Fleek在2022年的主要目标是重组其IPFS基础设施,以进一步分散和激励。它还将包括新的Web3基础设施供应商,用于网络建设堆栈的不同部分"。

Fleek为IPFS的挑战提供了一个解决方案,即每次更新时,你的网站的哈希值都会改变,因此很难有一个固定的地址哈希值。在最初的部署之后,Fleek将建立、钉住和更新你的网站。

让我们在下一节开始构建我们的样本DApp,并使用Fleek部署它。我们将把DApp托管在IPFS上。

构建一个DApp样本以部署到Fleek上

在这一节中,我们将为一家宠物店建立一个去中心化的收养跟踪系统。

如果你熟悉Truffle套件,你可能会认出这个练习的某些部分。这个DApp的灵感来自于Truffle指南。我们将通过使用Alchemy、Hardhat和React使事情更进一步。

为了使你能够专注于编写智能合约和部署DApp,我已经建立了UI组件和状态。智能合约和React代码将被包含在一个项目中。

只需从我的GitHub仓库克隆React应用程序,即可开始编写智能合约:

git clone  https://github.com/vickywane/react-web3 

接下来,将目录改为克隆的文件夹,并安装package.json 文件中列出的依赖项:

# change directory
cd react-web3

# install application dependencies
npm install

随着React应用程序的建立,让我们继续创建宠物收养智能合约。

创建宠物领养智能合约

react-web3 目录中,创建一个合约文件夹,以存储我们的宠物收养智能合约的 Solidity 代码。

使用您的代码编辑器,创建一个名为Adoption.sol 的文件,并粘贴以下代码,以在智能合约中创建必要的变量和函数,包括:

  • 一个16长度的数组来存储每个宠物收养者的地址
  • 一个用于收养宠物的函数
  • 一个检索所有被收养宠物的地址的函数
//SPDX-License-Identifier: Unlicense
// ./react-web3/contracts/Adoption.sol
pragma solidity ^0.8.0;

contract Adoption {
  address[16] public adopters;
  event PetAssigned(address indexed petOwner, uint32 petId);

  // adopting a pet
  function adopt(uint32 petId) public {
    require(petId >= 0 && petId <= 15, "Pet does not exist");

    adopters[petId] = msg.sender;
    emit PetAssigned(msg.sender, petId);
  }

  // Retrieving the adopters
  function getAdopters() public view returns (address[16] memory) {
    return adopters;
  }
}

接下来,在合约文件夹中创建另一个名为deploy-contract-script.js 的文件。将下面的JavaScript代码粘贴到该文件中。该代码将作为一个脚本,使用Hardhat中的异步getContractFactory 方法来创建一个收养智能合约的工厂实例,然后将其部署:

// react-web3/contract/deploy-contract-script.js
require('dotenv').config()
const { ethers } = require("hardhat");

async function main() {
  // We get the contract to deploy
  const Adoption = await ethers.getContractFactory("Adoption");
  const adoption = await Adoption.deploy();
  await adoption.deployed();

  console.log("Adoption Contract deployed to:", adoption.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
});

最后,创建一个名为hardhat.config.js 的文件。这个文件将指定Hardhat的配置。

hardhat.config.js 文件中添加以下 JavaScript 代码,以指定 Solidity 版本和你的 Ropsten 网络账户的 URL 端点。

require("@nomiclabs/hardhat-waffle");
require('dotenv').config();

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    ropsten: {
      url: process.env.ALCHEMY_API_URL,
      accounts: [`0x${process.env.METAMASK_PRIVATE_KEY}`]
    }
  }
};

我使用环境变量ALCHEMY_API_URLMETAMASK_PRIVATE_KEY 来存储用于Ropsten网络配置的URL和私人账户密钥值:

  • METAMASK_PRIVATE_KEY 与您的MetaMask钱包相关联。
  • ALCHEMY_API_URL 链接到一个Alchemy应用程序。

你可以使用.env 文件和dotenv 包在这个react-web3 项目中存储和访问这些环境变量。我们将在下一节回顾如何做到这一点。

如果你一直成功地跟随,你在项目的这个阶段还没有创建一个Alchemy应用程序。你将需要使用其API URL端点,所以让我们继续在Alchemy上创建一个应用程序。

创建一个Alchemy应用程序

Alchemy提供的功能使您能够连接到一个网络的外部远程过程调用(RPC)节点。RPC节点使您的DApp和区块链的通信成为可能。

使用您的网络浏览器,导航到Alchemy网络仪表板并创建一个新的应用程序。

为该应用程序提供一个名称和描述,然后选择Ropsten网络。点击 "创建应用程序 "按钮,继续。

Alchemy Web Dashboard Showing New Alchemy App Being Created

应用程序创建后,你会发现它列在页面底部。

点击 "查看密钥",显示Alchemy应用程序的API密钥。请注意HTTP URL。我在下面的图片中编辑了这个信息。

Alchemy Dashboard Showing Popup Box Containing App API Key And Other Info

在你的Hardhat项目中创建一个.env 文件,如下图所示。你将使用这个.env 文件来存储你的Alchemy应用程序的URL和MetaMask私钥。

// react-web3/.env

ALCHEMY_API_URL=<ALCHEMY_HTTP_URL>
METAMASK_PRIVATE_KEY=<METAMASK_PRIVATE_KEY>

用Alchemy的HTTP URL和你的MetaMask私钥替换上面的ALCHEMY_HTTP_URLMETAMASK_PRIVATE_KEY 占位符。按照MetaMask导出私钥指南,了解如何为你的钱包导出这些信息。

最后,执行下一个命令,将你的宠物收养智能合约部署到指定的Ropsten网络。

npx hardhat run contracts/deploy-contract-script.js --network ropsten

如下图所示,注意合同部署后返回到你的控制台的地址。你将在下一节中需要这个地址:

Console Showing Returned Smart Contract Address

在这一点上,宠物领养智能合约已经部署完毕。现在让我们把注意力转移到DApp本身,并创建与宠物收养智能合约互动的功能。

构建DApp前台

与Truffle指南中的宠物店教程类似,我们的DApp将显示十六个不同品种的狗,可以被收养。每只狗的详细信息都存储在 [src/pets.json](https://github.com/vickywane/react-web3/tree/master/src) 文件中。我们正在使用TailwindCSS来设计这个DApp。

要开始,请打开 [state/context.js](https://github.com/vickywane/react-web3/tree/master/src/state) 文件,用下面的代码替换现有内容:

// react-web3/state/context.js

import React, {useEffect, useReducer} from "react";
import Web3 from "web3";
import {ethers, providers} from "ethers";

const {abi} = require('../../artifacts/contracts/Adoption.sol/Adoption.json')

if (!abi) {
    throw new Error("Adoptiom.json ABI file missing. Run npx hardhat run contracts/deploy-contract-script.js")
}

export const initialState = {
    isModalOpen: false,
    dispatch: () => {
    },
    showToast: false,
    adoptPet: (id) => {
    },
    retrieveAdopters: (id) => {
    },
};

const {ethereum, web3} = window

const AppContext = React.createContext(initialState);
export default AppContext;

const reducer = (state, action) => {
    switch (action.type) {
        case 'INITIATE_WEB3':
            return {
                ...state,
                isModalOpen: action.payload,
            }
        case 'SENT_TOAST':
            return {
                ...state,
                showToast: action.payload.toastVisibility
            }
        default:
            return state;
    }
};

const createEthContractInstance = () => {
    try {
        const provider = new providers.Web3Provider(ethereum)
        const signer = provider.getSigner()
        const contractAddress = process.env.REACT_APP_ADOPTION_CONTRACT_ADDRESS

        return new ethers.Contract(contractAddress, abi, signer)
    } catch (e) {
        console.log('Unable to create ethereum contract. Error:', e)
    }
}

export const AppProvider = ({children}) => {
    const [state, dispatch] = useReducer(reducer, initialState);

    const instantiateWeb3 = async _ => {
        if (ethereum) {
            try {
                // Request account access
                return await ethereum.request({method: "eth_requestAccounts"})
            } catch (error) {
                // User denied account access...
                console.error("User denied account access")
            }
        } else if (web3) {
            return
        }
        return new Web3(Web3.givenProvider || "ws://localhost:8545")
    }

    const adoptPet = async id => {
        try {
            const instance = createEthContractInstance()
            const accountData = await instantiateWeb3()

            await instance.adopt(id, {from: accountData[0]})

            dispatch({
                type: 'SENT_TOAST', payload: {
                    toastVisibility: true
                }
            })

            // close success toast after 3s
            setTimeout(() => {
                dispatch({
                    type: 'SENT_TOAST', payload: {
                        toastVisibility: false
                    }
                })
            }, 3000)
        } catch (e) {
            console.log("ERROR:", e)
        }
    }

    const retrieveAdopters = async _ => {
        try {
            const instance = createEthContractInstance()
            return await instance.getAdopters()
        } catch (e) {
            console.log("RETRIEVING:", e)
        }
    }

    useEffect(() => {
        (async () => { await instantiateWeb3() })()
    })

    return (
        <AppContext.Provider
            value={{
                ...state,
                dispatch,
                adoptPet,
                retrieveAdopters
            }}
        >
            {children}
        </AppContext.Provider>
    );
};

通过阅读上面的代码块,你会观察到以下情况:

以太坊和Web3对象从浏览器窗口被解构。MetaMask扩展将Ethereum对象注入到浏览器中。

createEthContractInstance 辅助函数使用合同的ABI和Alchemy的地址创建并返回一个宠物收养合同的实例。

instantiateWeb3 辅助函数将在一个数组中检索并返回用户的账户地址,使用MetaMask来验证以太坊窗口对象是否被定义。

instantiateWeb3 辅助函数也在useEffect 钩子中执行,以确保用户在网络浏览器中打开应用程序后立即与MetaMask连接。

adoptPet 函数期望一个数字petId 参数,创建收养合同实例,并使用createEthContractInstanceinstantiateWeb3 辅助函数检索用户的地址。

petId 参数和用户账户地址从宠物收养合同实例传入adopt 方法,以收养宠物。

retrieveAdopters 函数在收养实例上执行getAdopters 方法,以检索所有收养的宠物的地址。

保存这些修改,启动React开发服务器,在http://localhost4040/,查看宠物店DApp。

在这一点上,收养合同内的功能已经在state/context.js 文件中实现,但还没有执行。如果不通过MetaMask认证,用户的账户地址将是未定义的,所有收养宠物的按钮将被禁用,如下图所示。

DApp Frontend Without MetaMask Authentication Showing Adopt Buttons Disabled

让我们继续添加宠物领养合同地址作为环境变量,在Fleek上托管DApp。

将React DApp部署到Fleek上

在Fleek上托管DApp可以通过Fleek仪表盘、Fleek CLI,甚至是使用Fleek GitHub Actions的编程方式完成。在本节中,你将学习如何使用Fleek CLI,因为我们通过Fleek在IPFS上托管宠物店DApp。

配置Fleek CLI

执行下面的命令,在你的电脑上全局安装Fleek CLI:

npm install -g @fleek/cli

要使用已安装的Fleek CLI,你需要有一个Fleek账户的API密钥作为环境变量存储在你的终端上。让我们继续使用Fleek网页仪表板为你的账户生成一个API密钥。

使用你的网络浏览器,导航到你的Fleek账户仪表板,点击你的账户头像,显示一个弹出菜单。在这个菜单中,点击 "设置 "来导航到你的Fleek账户设置。

Fleek Account Dashboard Showing Popup Menu From Right Control Panel

在你的Fleek账户设置中,点击 "生成API "按钮,启动 "API详情 "模式,这将为你的Fleek账户生成一个API密钥。

Fleek Account Settings Page With Launched API Details Modal

拿着生成的API密钥,替换下面命令中的FLEEK_API_KEY 占位符。

export FLEEK_API_KEY='FLEEK_API_KEY'

执行这个命令可以将Fleek API密钥导出为你电脑终端的一个临时环境变量。当你对你的Fleek账户执行命令时,Fleek CLI将读取FLEEK_API_KEY 这个变量的值。

通过Fleek CLI初始化一个网站

在你使用Fleek在IPFS上托管DApp及其文件之前,你需要在本地生成React DApp的静态文件。

在构建过程中,可以通过指定Docker镜像和用于构建静态文件的命令来自动生成静态文件。然而,在本教程中,你将手动生成静态文件。

执行下面的npm命令,在build 目录中为DApp生成静态文件。

npm run build

接下来,使用以下命令在react-web3 文件夹中初始化一个Fleek站点工作区。

fleek site:init

初始化过程对每个Fleek站点来说都是一次性的步骤。Fleek CLI将启动一个交互式会话,指导你完成初始化网站的过程。

在初始化过程中,你会被提示输入一个teamId ,如下图所示。

Fleek App Initialization Process Showing TeamID Input Prompt

你可以在Fleek仪表板的URL中找到你的teamId ,作为数字。一个例子teamId ,如下图所示。

Fleek Dashboard URL Showing Example TeamID Boxed Inside Orange Dotted Lines

在这一点上,Fleek CLI已经在react-web3 目录下生成了一个.fleek.json 文件,其中有Fleek的主机配置。然而,还缺少一件事:包含宠物收养智能合约地址的环境变量。

让我们继续看看如何为Fleek上的本地部署站点添加环境变量。

添加环境变量

Fleek使开发者能够通过Fleek仪表板或配置文件安全地管理其网站的敏感凭证。由于你在本教程中是通过命令行本地托管网站,你将在.fleek.json 文件中指定你的环境变量。

在下面的代码中,将ADOPTION_CONTRACT_ADDRESS 占位符替换为宠物收养智能合约地址。记住,这个地址是我们在本教程前面使用npx命令创建Alchemy应用程序并部署智能合约后返回的。

 {
  "site": {
    "id": "SITE_ID",
    "team": "TEAM_ID",
    "platform": "ipfs",
    "source": "ipfs",
    "name": "SITE_NAME"
  },
  "build": {
    "baseDir": "",
    "publicDir": "build",
    "rootDir": "",
    "environment": {
       "REACT_APP_ADOPTION_CONTRACT": "ADOPTION_CONTRACT_ADDRESS"
    }
  }
}

注意:当你初始化Fleek网站时,Fleek CLI会在.fleek.json 文件中自动生成SITE_IDTEAM_IDSITE_NAME 占位符。

上面的代码还包含以下对象,你应该把它添加到你的.fleek.json 文件中的构建对象中:

    "environment": {
       "REACT_APP_ADOPTION_CONTRACT": "ADOPTION_CONTRACT_ADDRESS"
    }

上面的JSON对象指定了Fleek在构建DApp时使用的一个node docker镜像。在构建过程中,command 字段中的npm命令将被执行。

执行下面的命令,使用.fleek.json 文件中的新构建配置重新部署DApp。

fleek site:deploy

Console Showing DApp Being Redeployed

恭喜你!DApp已经完全部署完毕。DApp已经完全部署完毕,你现在可以通过你的网络浏览器访问实时网站。你也可以通过Fleek仪表盘按照以下步骤获得关于托管DApp的更多详细信息。

  • 导航到你的Fleek仪表盘
  • 点击你部署的DApp的名称
  • 在左边看到部署的网站URL
  • 在右边看到一个部署预览图

Fleek Dashboard Showing Hosted DApp Details With Arrows Pointing To Deployed Site URL, Deployment Location, And Deploy Preview Image

点击网站URL,在新的浏览器标签中打开DApp。DApp启动后,会立即提示您连接一个MetaMask钱包。钱包连接后,你将能够通过点击 "收养 "按钮,收养16只狗中的任何一只。

Finished DApp Frontend With Connected MetaMask Wallet And Active Adopt Buttons

这就是了!你的宠物领养DApp样本已经被部署到Fleek。

总结

在本教程中,我们重点介绍了通过Fleek在IPFS上构建和托管一个示例DApp。这个过程的开始与Truffle指南中的宠物收养智能合约类似。然后,你通过构建一个DApp来与宠物收养智能合约进行互动,从而更进一步。

如果你想利用本教程中的步骤来托管一个可生产的DApp,我强烈建议你考虑以下几点。

首先,确保将Fleek连接到代码主机提供商,如GitHub,并从其存储库中的生产分支部署DApp。这将允许Fleek在你向部署的分支推送新的代码提交时自动重新部署DApp。

第二,如果你使用.fleek.json 文件来存储环境变量,在你的.gitignore 文件中包括.fleek.json 的文件名。这样做将确保.fleek.json 文件不会被推送,你的环境变量也不会被暴露。

我希望你觉得这个教程很有用。如果你有任何问题,欢迎分享评论。