以太坊DApp开发利器Truffle全解析
1. Truffle简介
Truffle是一个用于构建基于以太坊的去中心化应用(DApps)的强大工具,它集开发环境、框架和资产管道于一体。作为开发环境,它提供了命令行工具,可用于编译、部署、测试和构建项目;作为框架,它提供了各种包,让编写测试、部署代码以及构建客户端等工作变得轻松;作为资产管道,它支持发布包并使用他人发布的包。
安装Truffle
在开始使用Truffle之前,你需要先进行安装。可以使用npm全局安装Truffle,命令如下:
npm install -g truffle
在继续之前,请确保你正在运行网络ID为10的testrpc,原因与之前讨论的一致。
2. 初始化Truffle项目
2.1 创建项目目录
首先,你需要为你的应用创建一个目录,例如命名为 altcoin 。
mkdir altcoin
cd altcoin
2.2 初始化项目
在 altcoin 目录下,运行以下命令来初始化你的项目:
truffle init
初始化完成后,你将得到一个具有以下结构的项目:
- contracts :Truffle期望在此目录中找到Solidity合约文件。
- migrations :用于存放包含合约部署代码的文件。
- test :存放用于测试智能合约的测试文件。
- truffle.js :Truffle的主要配置文件。
默认情况下, truffle init 会为你提供一组示例合约( MetaCoin 和 ConvertLib ),它们类似于基于以太坊构建的简单替代币。以下是 MetaCoin 智能合约的源代码:
pragma Solidity ^0.4.4;
import "./ConvertLib.sol";
contract MetaCoin {
mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
function MetaCoin() {
balances[tx.origin] = 10000;
}
function sendCoin(address receiver, uint amount) returns(bool sufficient) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Transfer(msg.sender, receiver, amount);
return true;
}
function getBalanceInEth(address addr) returns(uint){
return ConvertLib.convert(getBalance(addr),2);
}
function getBalance(address addr) returns(uint) {
return balances[addr];
}
}
MetaCoin 会将10000个MetaCoin分配给部署合约的账户地址。用户可以使用 sendCoin() 函数将这些MetaCoin发送给其他人,使用 getBalance() 函数随时查询账户余额。假设一个MetaCoin等于两个以太币,你可以使用 getBalanceInEth() 函数查询以太币余额。 ConvertLib 库用于计算MetaCoin的以太币价值,它提供了 convert() 方法。
3. 编译合约
3.1 编译命令
在Truffle中编译合约会生成包含ABI(应用二进制接口)和未链接二进制代码的工件对象。编译命令如下:
truffle compile
Truffle只会编译自上次编译以来发生更改的合约,以避免不必要的编译。如果你想覆盖此行为,可以使用 --all 选项:
truffle compile --all
编译后的工件可以在 build/contracts 目录中找到,你可以根据需要编辑这些文件。这些文件在运行编译和迁移命令时会被修改。
3.2 编译前注意事项
在编译之前,需要注意以下几点:
- 合约文件名必须与合约定义的名称完全匹配。例如,如果文件名为 MyContract.sol ,则合约文件中应存在 contract MyContract{} 或 library myContract{} 。
- 文件名匹配是区分大小写的,即如果文件名没有大写,合约名也不应大写。
- 可以使用Solidity的 import 命令声明合约依赖关系。Truffle会按正确的顺序编译合约,并在必要时自动链接库。依赖关系必须相对于当前的Solidity文件指定,以 ./ 或 ../ 开头。
- Truffle版本3.1.2使用编译器版本0.4.8,目前不支持更改编译器版本。
4. 配置文件
4.1 默认配置文件
truffle.js 是一个JavaScript文件,用于配置项目。它可以执行任何必要的代码来创建项目配置,并且必须导出一个表示项目配置的对象。以下是默认的配置文件内容:
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
}
}
};
配置对象可以包含各种属性,最基本的是 networks 属性,它指定了可用于部署的网络以及与每个网络交互时的特定交易参数(如 gasPrice 、 from 、 gas 等)。默认的 gasPrice 是100,000,000,000, gas 是4712388, from 是以太坊客户端中第一个可用的合约。
4.2 自定义配置
你可以根据需要指定任意数量的网络。例如,将配置文件修改为以下内容:
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "10"
},
live: {
host: "localhost",
port: 8545,
network_id: "1"
}
}
};
在上述代码中,我们定义了两个网络: development 和 live 。
4.3 Windows系统注意事项
在Windows上使用命令提示符时,默认的配置文件名可能会与Truffle可执行文件发生冲突。如果出现这种情况,建议使用Windows PowerShell或Git BASH,因为这些shell不会有此冲突。或者,你可以将配置文件重命名为 truffle-config.js 以避免冲突。
5. 部署合约
5.1 合约部署的网络环境
即使是最小的项目也至少会与两个区块链进行交互:一个在开发者的机器上,如EthereumJS TestRPC;另一个是开发者最终将应用部署的网络(例如以太坊主网或私有联盟网络)。
由于合约抽象在运行时会自动检测网络,这意味着你只需要部署一次应用或前端。当应用运行时,运行中的以太坊客户端将决定使用哪些工件,这使得你的应用非常灵活。
5.2 迁移文件
包含将合约部署到以太坊网络的代码的JavaScript文件称为迁移文件。这些文件负责安排部署任务,并且是基于你的部署需求会随时间变化的假设编写的。随着项目的发展,你将创建新的迁移脚本来推动区块链上的项目演进。
迁移文件的文件名前缀为数字,例如 1_initial_migration.js 和 2_deploy_contracts.js 。编号前缀用于记录迁移是否成功运行。 Migrations 合约会在 last_completed_migration 中存储一个数字,该数字对应于 migrations 文件夹中最后应用的迁移脚本。 Migrations 合约总是首先部署,编号约定为 x_script_name.js ,其中 x 从1开始。你的应用合约通常从编号2的脚本开始。
5.3 编写迁移脚本
在迁移文件的开头,需要使用 artifacts.require() 方法告诉Truffle你想要与之交互的合约。这个方法类似于Node的 require ,但在我们的场景中,它专门返回一个合约抽象,可在部署脚本的其余部分使用。
所有迁移都必须通过 module.exports 语法导出一个函数。每个迁移导出的函数应接受一个 deployer 对象作为其第一个参数。这个对象通过提供清晰的API来部署智能合约,并执行一些部署的常规任务,如将部署的工件保存到工件文件中以供后续使用、链接库等,协助进行部署。
deployer 对象的方法如下:
- deployer.deploy(contractAbstraction, args..., options) :部署由合约抽象对象指定的特定合约,可选择提供构造函数参数。这对于单例合约很有用,确保你的DApp中只有一个该合约的实例。部署后会设置合约的地址(即工件文件中的 address 属性将等于新部署的地址),并覆盖之前存储的任何地址。你可以选择传递一个合约数组或数组的数组,以加快多个合约的部署。此外,最后一个参数是一个可选对象,可以包含一个键 overwrite 。如果 overwrite 设置为 false ,如果该合约已经部署过, deployer 将不会再次部署。该方法返回一个Promise。
- deployer.link(library, destinations) :将已部署的库链接到一个或多个合约。 destinations 参数可以是单个合约抽象或多个合约抽象的数组。如果目标中的任何合约不依赖于要链接的库, deployer 将忽略该合约。该方法返回一个Promise。
- deployer.then(function(){}) :用于运行任意部署步骤。在迁移过程中使用它来调用特定的合约函数,以添加、编辑和重新组织合约数据。在回调函数中,你可以使用合约抽象API来部署和链接合约。
以下是一个根据网络条件进行部署的示例:
module.exports = function(deployer, network) {
if (network != "live") {
// Perform a different step otherwise.
} else {
// Do something specific to the network named "live".
}
}
在项目中,你会找到两个迁移文件: 1_initial_migration.js 和 2_deploy_contracts.js 。除非你知道自己在做什么,否则不要编辑第一个文件。以下是 2_deploy_contracts.js 文件的代码:
var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");
module.exports = function(deployer) {
deployer.deploy(ConvertLib);
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);
};
在上述代码中,我们首先创建了 ConvertLib 库和 MetaCoin 合约的抽象。无论使用哪个网络,我们都会部署 ConvertLib 库,然后将其链接到 MetaCoin 合约,最后部署 MetaCoin 合约。
5.4 运行迁移
要运行迁移(即部署合约),可以使用以下命令:
truffle migrate --network development
这里我们告诉Truffle在 development 网络上运行迁移。如果不提供 --network 选项,Truffle将默认使用名为 development 的网络。
运行上述命令后,Truffle会自动更新 ConvertLib 库和 MetaCoin 合约在工件文件中的地址,并更新链接。
migrate 子命令的其他重要选项如下:
- --reset :从开始运行所有迁移,而不是从最后完成的迁移继续。
- -f number :从特定的迁移开始运行合约。
你可以随时使用 truffle networks 命令查找项目中合约和库在各个网络中的地址。
5.5 迁移流程
graph LR
A[开始] --> B[创建迁移文件]
B --> C[编写迁移脚本]
C --> D[运行迁移命令]
D --> E[部署合约]
E --> F[更新工件文件]
F --> G[结束]
6. 单元测试合约
6.1 单元测试概述
单元测试是一种测试应用的方法,它对应用中最小的可测试部分(称为单元)进行单独和独立的检查,以确保其正常运行。单元测试可以手动进行,但通常是自动化的。
Truffle默认提供了一个单元测试框架,用于自动化测试你的合约。在运行测试文件时,它会提供一个干净的环境,即Truffle会在每个测试文件开始时重新运行所有迁移,以确保你有一组全新的合约进行测试。
6.2 测试方式
Truffle允许你以两种不同的方式编写简单且易于管理的测试:
- JavaScript :从应用客户端测试你的合约。
- Solidity :从其他合约测试你的合约。
两种测试风格各有优缺点,我们将学习这两种编写测试的方法。
6.3 测试文件要求
所有测试文件应位于 ./test 目录中。Truffle只会运行具有以下文件扩展名的测试文件: .js 、 .es 、 .es6 、 .jsx 和 .sol ,其他文件将被忽略。
6.4 测试客户端选择
在运行自动化测试时, ethereumjs-testrpc 比其他客户端快得多。此外, testrpc 包含一些特殊功能,Truffle可以利用这些功能将测试运行时间缩短近90%。作为一般的工作流程,建议在正常开发和测试期间使用 testrpc ,然后在准备部署到实时或生产网络时,针对 go-ethereum 或其他官方以太坊客户端运行一次测试。
6.5 编写JavaScript测试
Truffle的JavaScript测试框架基于 mocha 构建, mocha 是一个用于编写测试的JavaScript框架,而 chai 是一个断言库。
测试框架用于组织和执行测试,断言库提供了验证事物是否正确的工具。断言库使测试代码变得更加容易,你无需编写数千个 if 语句。大多数测试框架不包含断言库,允许用户选择自己想要使用的断言库。
在继续之前,你需要学习如何使用 mocha 和 chai 编写测试。可以访问 https://mochajs.org/ 学习 mocha ,访问 http://chaijs.com/ 学习 chai 。
你的测试文件应位于 ./test 目录中,并且以 .js 扩展名结尾。
合约抽象是从JavaScript进行合约交互的基础。由于Truffle无法检测你在测试中需要与哪些合约进行交互,因此你需要使用 artifacts.require() 方法显式请求这些合约。所以在测试文件中首先要做的是为你想要测试的合约创建抽象。
以下是Truffle生成的用于测试 MetaCoin 合约的默认测试代码:
// Specifically request an abstraction for MetaCoin.sol
var MetaCoin = artifacts.require("./MetaCoin.sol");
contract('MetaCoin', function(accounts) {
it("should put 10000 MetaCoin in the first account", function() {
return MetaCoin.deployed().then(function(instance) {
return instance.getBalance.call(accounts[0]);
}).then(function(balance) {
assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
});
});
it("should send coin correctly", function() {
var meta;
// Get initial balances of first and second account.
var account_one = accounts[0];
var account_two = accounts[1];
var account_one_starting_balance;
var account_two_starting_balance;
var account_one_ending_balance;
var account_two_ending_balance;
var amount = 10;
return MetaCoin.deployed().then(function(instance) {
meta = instance;
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_starting_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_starting_balance = balance.toNumber();
return meta.sendCoin(account_two, amount, {from: account_one});
}).then(function() {
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_ending_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_ending_balance = balance.toNumber();
assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken");
assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to");
});
});
});
在上述代码中,所有合约的交互代码都是使用 truffle-contract 库编写的,代码具有自解释性。
最后,Truffle允许你访问 mocha 的配置,以便更改 mocha 的行为。 mocha 的配置位于 truffle.js 文件导出对象的 mocha 属性下。例如:
mocha: {
useColors: true
}
6.6 编写Solidity测试
以下是一个使用Solidity编写的测试合约示例:
pragma Solidity ^0.4.2;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";
contract TestMetacoin {
function testInitialBalanceUsingDeployedContract() {
MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
function testInitialBalanceWithNewMetaCoin() {
MetaCoin meta = new MetaCoin();
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
}
另一个示例:
import "truffle/Assert.sol";
contract TestHooks {
uint someValue;
function beforeEach() {
someValue = 5;
}
function beforeEachAgain() {
someValue += 1;
}
function testSomeValueIsSix() {
uint expected = 6;
Assert.equal(someValue, expected, "someValue should have been 6");
}
}
这个测试合约表明,你的测试函数和钩子函数共享相同的合约状态。你可以在测试前设置合约数据,在测试期间使用该数据,并在测试后重置数据以准备下一次测试。需要注意的是,就像JavaScript测试一样,下一个测试函数将从上一个运行的测试函数的状态继续。
Truffle没有提供直接的方法来测试合约是否应该抛出异常(即对于使用 throw 表示预期错误的合约),但可以在 http://truffleframework.com/tutorials/testing-for-throws-in-Solidity-tests 找到一个变通的解决方案。
6.7 测试流程
graph LR
A[开始] --> B[创建测试文件]
B --> C[编写测试代码]
C --> D[运行测试命令]
D --> E[执行测试]
E --> F[生成测试报告]
F --> G[结束]
通过以上步骤,你可以全面地使用Truffle进行以太坊DApp的开发、部署和测试,确保你的项目的质量和灵活性。
7. 总结与最佳实践
7.1 关键步骤总结
为了更清晰地回顾使用Truffle开发以太坊DApp的整个流程,下面以表格形式总结关键步骤:
| 步骤 | 操作内容 | 命令示例 |
| ---- | ---- | ---- |
| 安装Truffle | 使用npm全局安装Truffle | npm install -g truffle |
| 初始化项目 | 创建项目目录并初始化Truffle项目 | mkdir altcoin && cd altcoin && truffle init |
| 编译合约 | 编译合约生成工件对象 | truffle compile 或 truffle compile --all |
| 配置项目 | 编辑 truffle.js 文件配置网络等信息 | |
| 部署合约 | 编写迁移文件并运行迁移命令 | truffle migrate --network development |
| 单元测试 | 编写测试文件并运行测试 | |
7.2 最佳实践建议
- 合约开发
- 遵循合约文件名与合约定义名称完全匹配的规则,且注意大小写。
- 合理使用
import命令声明合约依赖关系,确保合约编译顺序正确。
- 迁移文件编写
- 不要随意修改
1_initial_migration.js文件,除非你清楚其影响。 - 利用
deployer对象的方法,如deploy和link,高效部署和链接合约。 - 根据不同网络环境,使用条件语句编写迁移脚本,提高代码的灵活性。
- 不要随意修改
- 单元测试
- 在正常开发和测试期间使用
ethereumjs - testrpc提高测试效率,在部署前针对官方以太坊客户端进行测试。 - 结合 JavaScript 和 Solidity 两种测试方式,全面测试合约功能。
- 合理使用
mocha和chai进行断言,确保测试代码的准确性。
- 在正常开发和测试期间使用
7.3 未来展望
随着以太坊技术的不断发展,Truffle也会不断更新和完善。未来可能会支持更多的编译器版本,提供更丰富的测试工具和功能,进一步简化DApp的开发流程。同时,随着区块链应用场景的不断拓展,Truffle在更多领域的应用也值得期待。
8. 常见问题与解决方案
8.1 安装与配置问题
- 问题 :在Windows上使用命令提示符时,配置文件名与Truffle可执行文件冲突。
-
解决方案 :使用Windows PowerShell或Git BASH,或者将配置文件重命名为
truffle - config.js。 -
问题 :安装Truffle时出现权限问题。
- 解决方案 :在Linux或macOS系统上,使用
sudo npm install -g truffle命令;在Windows系统上,以管理员身份运行命令提示符。
8.2 编译问题
- 问题 :编译时提示合约文件名与合约定义名称不匹配。
-
解决方案 :检查合约文件名和合约定义名称,确保它们完全一致,且注意大小写。
-
问题 :编译时出现依赖关系错误。
- 解决方案 :检查
import命令的路径是否正确,依赖关系是否相对于当前的Solidity文件指定。
8.3 部署问题
- 问题 :迁移脚本运行失败。
-
解决方案 :检查迁移文件的语法错误,确保
deployer对象的方法使用正确。同时,检查网络配置是否正确,确保以太坊客户端正常运行。 -
问题 :合约部署后地址未更新。
- 解决方案 :检查迁移脚本中
deploy方法的使用,确保没有设置overwrite为false。
8.4 测试问题
- 问题 :测试运行时出现合约未部署错误。
-
解决方案 :确保在测试文件开始时使用
artifacts.require()方法正确请求合约抽象,并且迁移脚本能够正确部署合约。 -
问题 :测试代码中断言失败。
- 解决方案 :检查测试代码逻辑,确保合约方法调用和断言条件正确。同时,检查合约的初始状态和方法实现是否符合预期。
9. 拓展学习资源
9.1 官方文档
Truffle的官方文档是学习和使用Truffle的重要资源,它提供了详细的功能介绍、命令参考和示例代码。可以访问 Truffle官方文档 进行学习。
9.2 在线教程
网上有许多关于Truffle的在线教程,例如以太坊官方教程、一些技术博客等。这些教程通常会结合实际案例,帮助你更好地理解和掌握Truffle的使用。
9.3 开源项目
参考一些使用Truffle开发的开源以太坊DApp项目,可以学习到他人的开发经验和最佳实践。可以在GitHub等代码托管平台上搜索相关项目。
9.4 社区论坛
参与以太坊和Truffle的社区论坛,如以太坊官方论坛、Stack Overflow等。在这些论坛上,你可以与其他开发者交流经验,解决遇到的问题。
10. 实战案例分析
10.1 案例背景
假设我们要开发一个简单的众筹DApp,允许用户向项目进行捐款。我们将使用Truffle来完成合约开发、部署和测试。
10.2 合约开发
首先,我们编写众筹合约 Crowdfunding.sol :
pragma solidity ^0.4.4;
contract Crowdfunding {
address public owner;
mapping(address => uint) public contributions;
uint public goal;
uint public totalContributions;
event ContributionReceived(address contributor, uint amount);
function Crowdfunding(uint _goal) public {
owner = msg.sender;
goal = _goal;
}
function contribute() public payable {
contributions[msg.sender] += msg.value;
totalContributions += msg.value;
ContributionReceived(msg.sender, msg.value);
}
function checkGoalReached() public view returns (bool) {
return totalContributions >= goal;
}
}
10.3 迁移文件编写
创建迁移文件 2_deploy_crowdfunding.js :
var Crowdfunding = artifacts.require("./Crowdfunding.sol");
module.exports = function (deployer) {
deployer.deploy(Crowdfunding, 1000000000000000000); // 目标金额为1以太币
};
10.4 单元测试
编写测试文件 testCrowdfunding.js :
var Crowdfunding = artifacts.require("./Crowdfunding.sol");
contract('Crowdfunding', function (accounts) {
it("should set the correct goal", function () {
return Crowdfunding.deployed().then(function (instance) {
return instance.goal.call();
}).then(function (goal) {
assert.equal(goal.toNumber(), 1000000000000000000, "Goal was not set correctly");
});
});
it("should accept contributions", function () {
var crowdfunding;
var account_one = accounts[0];
var amount = 100000000000000000;
return Crowdfunding.deployed().then(function (instance) {
crowdfunding = instance;
return crowdfunding.contribute({ from: account_one, value: amount });
}).then(function () {
return crowdfunding.contributions.call(account_one);
}).then(function (contribution) {
assert.equal(contribution.toNumber(), amount, "Contribution was not recorded correctly");
});
});
});
10.5 部署与测试流程
graph LR
A[开始] --> B[编写合约代码]
B --> C[编写迁移文件]
C --> D[编写测试文件]
D --> E[编译合约]
E --> F[运行迁移部署合约]
F --> G[运行测试]
G --> H[结束]
通过这个实战案例,我们可以更深入地理解如何使用Truffle进行以太坊DApp的开发、部署和测试。
综上所述,Truffle为以太坊DApp开发提供了强大的支持,通过遵循上述步骤和最佳实践,结合常见问题的解决方案,以及拓展学习资源和实战案例的学习,你可以更高效地开发出高质量的以太坊DApp。
超级会员免费看
1192

被折叠的 条评论
为什么被折叠?



