Update avaliable. Click RELOAD to update.
目录

去中心化的宠物领养 DApp 应用

1. 知识点

  1. 合约框架 Truffle 的使用,如何编译、发布合约到测试区块链 Ganache
  2. 前端是一个基本的 HTML+JS 的工程,在没有使用前端框架的情况下,如何使用 web3.js
  3. 前端 JS 中如何使用 web3.js 与合约交互
    • 初始化 Provider,连接 MetaMask
    • 调用合约,处理合约返回的数据

2. 阅读前提

3. 实现步骤

产品需求:实现一个宠物领养的页面,可以点击领养,查询所有已经领养的宠物,这里宠物领养后的归属人为以太坊账户

最终效果运行结果

实现方案

web3-pet-shop.drawio

3.1 使用 Truffle 完成合约编写&部署

3.1.1. 初始化工程

mkdir pet-shop && cd pet-shop
truffle init

执行后的目录结构如下:

truffle init

3.1.2. 编写合约代码

在项目的 contracts 目录中创建一个 Adoption.sol 文件,使用 Solidity语言编写如下代码:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

contract Adoption {

    // 定义一个address类型的固定数组,表示收养者
    address[16] public adopters;

    function adopt(uint petId) public returns (uint) {
        require(petId >=0 && petId <=15);

        adopters[petId] = msg.sender;
        return petId;
    }

    function getadopters() public view returns (address[16] memory) {
        return adopters;
    }
    
}

上面代码定义一个 address 类型的数组,用于存储领养人(以太坊账户)的信息,总共16个,对应页面宠物的数据也是 16个,另外还定义了两个函数:

3.1.3. 编译合约代码

truffle compile

执行完上述命令后,truffle 会编译 contracts目录中的合约文件,在项目根目录build/contracts生成编译后的 Adoption.json 文件,这个 Json 文件包含了合约的重要信息,如 ABIbytecode

执行 truffle compile,下载 solc 报错,如何解决?

3.1.4. 编写部署脚本

在项目migrations目录中新建1_deploy_contract.js

var Adoption = artifacts.require('Adoption');

module.exports = function(deployer) {
    deployer.deploy(Adoption);
};

这里的脚本名称有规则,必须”数字_“开头,否则truffle不能识别到部署脚本

3.1.5. 部署合约

部署合约分两步:

  1. 启动本地 Ganache 区块链
  2. 在 Truffle 的配置文件 truffle-config.js 中指定区块链连接信息

启动 Ganache 区块链,获取连接信息

20221220232629

配置 truffle-config.js 文件,配置连接,执行truffle migrate部署合约到链中

20221220232945

上面可以看到当我们执行完truffle migrate后,合约已经部署成功,对应区块链上的地址也已经生成,至此合约部分已经完成,这里可以理解为后端的处理服务已经完成,下面开始编写前端与合约交互部分。

20221220233432

3.2. 前端工程开发

3.2.1. Hello的页面

本节目的,创建 index.html 页面,启动一个 web 服务,运行浏览器显示页面的 Hello 信息,就是一个基本的静态站点

# 初始化一个node工程,安装 lite-server 服务器
npm init --yes
npm install lite-server --save-dev

# 创建一个测试的html文件
mkdir src && cd src
echo "Hello" >> index.html

跟路径创建 lite-server配置文件 bs-config.json,指定服务器启动后加载的资源路径

{ 
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
}

这里值得注意的是bs-config.json文件除了指定了html资源的所在位置,还指定了编译后合约 json 文件的所在位置,后续在 js 中可以不指定路径,直接引用合约的 abi 文件

编辑 package.json文件,增加启动lite-server的运行命令

# Inside package.json...
"scripts": {    
  "dev": "lite-server"
},

运行命令npm run dev,打开浏览器检查我们搭建的静态服务是否已经可用

20221220235351

至此我们的静态服务器搭建完成,下面开始创建与合约交互的脚本逻辑

3.2.2. 引入合约处理库

编辑index.html引入下面web3.jstruffle-contract.js已经后面的app.js文件

<html>
  	......
	<script src="js/web3.min.js"></script>
    <script src="js/truffle-contract.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

这里省略了其余内容以及对应的资源文件,完整的项目文件可在FAQ的完整代码下载路径中获取

3.2.3. 创建app.js处理合约交互

合约交互分为以下几个流程:

  1. 初始化 web3 对象,获取 provider
// 1. 初始化 Web3
initWeb3: async function () {
  if (window.ethereum) {	// Metamask
    App.web3Provider = window.ethereum;
    try {
      await window.ethereum.request({ method: "eth_requestAccounts" });
    } catch (error) {
      console.error("User denied account access");
    }
  } else if (window.web3) {
    App.web3Provider = window.web3.currentProvider;
  } else {
    // 直连
    App.web3Provider = new Web3.providers.HttpProvider("http://127.0.0.1:7545");
  }

  web3 = new Web3(App.web3Provider);

  // 初始化合约
  return App.initContract();
},
  1. 为 contract 设置 provider
// 2. 为合约设置 Provider
initContract: function () {
  // Adoption.json因为lite-server配置了"baseDir": ["./src", "./build/contracts"],所以可以
  // 直接读取编译后的合约文件
  $.getJSON("Adoption.json", function (data) {
    // 获取必要的合约工件文件并使用 @truffle/contract 实例化它
    var AdoptionArtifact = data;
    App.contracts.Adoption = TruffleContract(AdoptionArtifact);

    // 为我们的合约设置provider
    App.contracts.Adoption.setProvider(App.web3Provider);

    // 调用合约检索并标记领养之前领养的宠物
    return App.markAdopted();
  });

  return App.bindEvents();
},
  1. 调用 contract.getadopters() 函数获取历史领养数据
// 查询已经领养的用户(账号),并标记
markAdopted: function () {
  var adoptionInstance;

  App.contracts.Adoption.deployed()
    .then(function (instance) {
    adoptionInstance = instance;

    return adoptionInstance.getadopters.call();
  })
    .then(function (adopters) {
    for (i = 0; i < adopters.length; i++) {
      if (adopters[i] !== "0x0000000000000000000000000000000000000000") {
        $(".panel-pet")
          .eq(i)
          .find("button")
          .text("Success")
          .attr("disabled", true);
      }
    }
  })
    .catch(function (err) {
    console.log(err.message);
  });
},
  1. 注册 adopt 按钮事件,调用 contract.adopt() 领养函数
bindEvents: function () {
  // 绑定页面领养按钮
  $(document).on("click", ".btn-adopt", App.handleAdopt);
},

// 查询已经领养的用户(账号),并标记
markAdopted: function () {
	var adoptionInstance;

	App.contracts.Adoption.deployed()
		.then(function (instance) {
		adoptionInstance = instance;

		return adoptionInstance.getadopters.call();
	})
	.then(function (adopters) {
		for (i = 0; i < adopters.length; i++) {
			if (adopters[i] !== "0x0000000000000000000000000000000000000000") {
        // 标记未领养的宠物
      }
    })
      .catch(function (err) {
      console.log(err.message);
    });
  },

app.js 完整的代码如下:

App = {
  web3Provider: null,
  contracts: {},

  init: async function () {
    
    // 初始化加载页面数据
    $.getJSON("../pets.json", function (data) {
      var petsRow = $("#petsRow");
      var petTemplate = $("#petTemplate");

      for (i = 0; i < data.length; i++) {
        petTemplate.find(".panel-title").text(data[i].name);
        petTemplate.find("img").attr("src", data[i].picture);
        petTemplate.find(".pet-breed").text(data[i].breed);
        petTemplate.find(".pet-age").text(data[i].age);
        petTemplate.find(".pet-location").text(data[i].location);
        petTemplate.find(".btn-adopt").attr("data-id", data[i].id);

        petsRow.append(petTemplate.html());
      }
    });

    // 初始化web3,设置 provider
    return await App.initWeb3();
  },

  // 1. 初始化 Web3
  initWeb3: async function () {
    if (window.ethereum) {
      App.web3Provider = window.ethereum;
      try {
        await window.ethereum.request({ method: "eth_requestAccounts" });
      } catch (error) {
        console.error("User denied account access");
      }
    } else if (window.web3) {
      App.web3Provider = window.web3.currentProvider;
    } else {
      App.web3Provider = new Web3.providers.HttpProvider(
        "http://127.0.0.1:7545"
      );
    }

    web3 = new Web3(App.web3Provider);

    // 初始化合约
    return App.initContract();
  },

  // 2. 为合约设置 Provider
  initContract: function () {
    // Adoption.json因为lite-server配置了"baseDir": ["./src", "./build/contracts"],所以可以
    // 直接读取编译后的合约文件
    $.getJSON("Adoption.json", function (data) {
      // 获取必要的合约工件文件并使用 @truffle/contract 实例化它
      var AdoptionArtifact = data;
      App.contracts.Adoption = TruffleContract(AdoptionArtifact);

      // 为我们的合约设置provider
      App.contracts.Adoption.setProvider(App.web3Provider);

      // 调用合约检索并标记领养之前领养的宠物
      return App.markAdopted();
    });

    return App.bindEvents();
  },

  bindEvents: function () {
    // 绑定页面领养按钮
    $(document).on("click", ".btn-adopt", App.handleAdopt);
  },

  // 查询已经领养的用户(账号),并标记
  markAdopted: function () {
    var adoptionInstance;

    App.contracts.Adoption.deployed()
      .then(function (instance) {
        adoptionInstance = instance;

        return adoptionInstance.getadopters.call();
      })
      .then(function (adopters) {
        for (i = 0; i < adopters.length; i++) {
          if (adopters[i] !== "0x0000000000000000000000000000000000000000") {
            $(".panel-pet")
              .eq(i)
              .find("button")
              .text("Success")
              .attr("disabled", true);
          }
        }
      })
      .catch(function (err) {
        console.log(err.message);
      });
  },

  // 页面点击领养按钮
  handleAdopt: function (event) {
    event.preventDefault();

    var petId = parseInt($(event.target).data("id"));
    var adoptionInstance;

    web3.eth.getAccounts(function (error, accounts) {
      if (error) {
        console.log(error);
      }

      // 这里准确应该使用 window.ethereum.selectAddress
      var account = accounts[0];

      App.contracts.Adoption.deployed()
        .then(function (instance) {
          adoptionInstance = instance;

          // 调用合约领养方法
          return adoptionInstance.adopt(petId, { from: account });
        })
        .then(function (result) {
          return App.markAdopted();
        })
        .catch(function (err) {
          console.log(err.message);
        });
    });
  },
};

// 页面加载执行
$(function () {
  $(window).load(function () {
    App.init();
  });
});

至此我们已经完成了合约和前端交互的开发,这里主要核心还是 app.js 中,合约如何连接、合约如何实例化、合约如何调用的处理。

3.3. 运行测试应用

npm run dev

4. FAQ

4.1. 如何解决truffle compile,下载solc报错?

执行 truffle compile,truffle 会根据源码指定的 Solidity 版本下载对应的编译器 solc,可能会下载失败,建议使用二进制下载 solc 编译器,关于如何安装参看 Installing the Solidity Compiler,安装好打开终端查看编译器版本:

> solc --version
solc, the solidity compiler commandline interface
Version: 0.8.17+commit.8df45f5f.Darwin.appleclang

将合约源码指定的版本号与下载的solc版本号对齐,新增或者修改 truffle-config.js文件,指定编译器为 native,再次执行 truffle compile即可

solc download failure

4.2. 完整代码下载路径

https://github.com/wangjunneil/my-pet-shop

版权所有,本作品采用知识共享署名-非商业性使用 3.0 未本地化版本许可协议进行许可。转载请注明出处:https://www.wangjun.dev//2022/12/pet-shop-dapp/