去中心化的宠物领养 DApp 应用
1. 知识点
- 合约框架 Truffle 的使用,如何编译、发布合约到测试区块链 Ganache 中
- 前端是一个基本的 HTML+JS 的工程,在没有使用前端框架的情况下,如何使用 web3.js
- 前端 JS 中如何使用 web3.js 与合约交互
- 初始化 Provider,连接 MetaMask
- 调用合约,处理合约返回的数据
2. 阅读前提
3. 实现步骤
产品需求:实现一个宠物领养的页面,可以点击领养,查询所有已经领养的宠物,这里宠物领养后的归属人为以太坊账户。
最终效果:运行结果
实现方案:
3.1 使用 Truffle 完成合约编写&部署
3.1.1. 初始化工程
mkdir pet-shop && cd pet-shop
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个,另外还定义了两个函数:
- function adopt():领养宠物,宠物使用数组索引代替,合约的调用者 msg.sender 代表领养人
- function getadopters():获取所有领养人信息
3.1.3. 编译合约代码
truffle compile
执行完上述命令后,truffle 会编译 contracts
目录中的合约文件,在项目根目录build/contracts
生成编译后的 Adoption.json 文件,这个 Json 文件包含了合约的重要信息,如 ABI 和 bytecode。
3.1.4. 编写部署脚本
在项目migrations
目录中新建1_deploy_contract.js
var Adoption = artifacts.require('Adoption');
module.exports = function(deployer) {
deployer.deploy(Adoption);
};
这里的脚本名称有规则,必须”数字_“开头,否则truffle不能识别到部署脚本
3.1.5. 部署合约
部署合约分两步:
- 启动本地 Ganache 区块链
- 在 Truffle 的配置文件 truffle-config.js 中指定区块链连接信息
启动 Ganache 区块链,获取连接信息
配置 truffle-config.js 文件,配置连接,执行truffle migrate
部署合约到链中
上面可以看到当我们执行完truffle migrate
后,合约已经部署成功,对应区块链上的地址也已经生成,至此合约部分已经完成,这里可以理解为后端的处理服务已经完成,下面开始编写前端与合约交互部分。
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
,打开浏览器检查我们搭建的静态服务是否已经可用
至此我们的静态服务器搭建完成,下面开始创建与合约交互的脚本逻辑
3.2.2. 引入合约处理库
编辑index.html
引入下面web3.js、truffle-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处理合约交互
合约交互分为以下几个流程:
- 初始化 web3 对象,获取 provider
- 为 contract 设置 provider
- 调用 contract.getadopters() 函数获取历史领养数据
- 注册 adopt 按钮事件,调用 contract.adopt() 领养函数
- 初始化 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();
},
- 为 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();
},
- 调用 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);
});
},
- 注册 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
即可
4.2. 完整代码下载路径
https://github.com/wangjunneil/my-pet-shop
版权所有,本作品采用知识共享署名-非商业性使用 3.0 未本地化版本许可协议进行许可。转载请注明出处:https://www.wangjun.dev//2022/12/pet-shop-dapp/