实战 - Truffle 2.0 升级 3.0 升级指南
备注:这个指南同样适用于从beta 3.0.0-9
升级到3.0.1
的用户。
介绍
3.0
版本引入了大量的新特性,这些特性为我们带来了大量的重要革新性变化。让我们的network
的管理更简单,新的抽象的合约层,允许你从第三方引入各种依赖文件。伴随以太坊的开发工具逐步成熟,我们认为这样的革新非常有价值。下面我们将一步步指引你来享受这些新特性带来的好处。
为了展示2.0到3.0版本的变化,后续会使用下面示例这种对比的方式。
v2.0
// 2.0版本代码
v3.0
// 3.0版本代码
先说最重要的:配置(Configuration)
Truffle 2.0中我们使用了一个不舒服的配置方式。你不仅可以使用一个default
,匿名的网络设置(通过rpc
配置项);同时你也可以使用命名的网络设置,比如ropsten
,或者live
。由此带来的一个后果是,你可能会无意中覆盖了网络配置,或者部署到错误的网络。在Truffle 3.0中,我们解决了这个问题,代价是对配置方式的调整。
在2.0版本中,一个命名网络的声明方式示例如下:
module.exports = {
rpc: {
host: "localhost",
port: 8545
},
networks: {
staging: {
host: "localhost",
port: 8546,
network_id: 1337
},
ropsten: {
host: "158.253.8.12",
port: 8545,
network_id: 3
}
}
};
在3.0中,我们改为了下述的方式。
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*"
},
staging: {
host: "localhost",
port: 8546,
network_id: 1337
},
ropsten: {
host: "158.253.8.12",
port: 8545,
network_id: 3
}
}
};
变化点如下:
- 默认网络配置项
rpc
被移除了。取而代之的是在networks
配置项下的一个专门的名为development
的网络配置项。development
中会指定的ip
和port
,和配置的网络类型1,默认是任意*
。 - 如果没有指定网络,在执行
migrate
等命令时默认使用名为development
的网络。 - 为了避免出现部署到错误网络的情况,你也可以完全移除配置项
development
。如果已经移除development
的网络配置,但在truffle migrate
等命令时不指定网络会有下述错误发生:
$ truffle migrate
Compiling ./contracts/ConvertLib.sol...
Compiling ./contracts/MetaCoin.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts
Error: No network specified. Cannot determine current network.
at Object.detect (/usr/local/lib/node_modules/truffle/lib/environment.js:27:23)
at /usr/local/lib/node_modules/truffle/lib/commands/migrate.js:33:19
at /usr/local/lib/node_modules/truffle/lib/contracts.js:51:11
at /usr/local/lib/node_modules/truffle/lib/contracts.js:83:9
如果要指定网络,如ropsten
,使用命令:
truffle migrate --network ropsten
另外每个网络配置项,都需要指定一个网络ID[networkID],这可以算是一种安全机制来保证部署的安全性,来保证一定是部署到你想要部署的网络。development
这样的网络环境,可以使用*
来表示任意,以方便调测。
移植和测试依赖(Migrations and Test Dependencies)
在没有引入包管理前,Truffle可以假设所有你写的智能合约都是需要移植和测试的。在有了包管理之后,由于依赖可以来自多个不同的地方,我们不再进行默认关联,如果需要关联,你必须明确指定。以减少自动关联带来的潜在问题。下面来看一个migration(移植)
的手动引入关联的例子。
2.0版本中的./migrations/2_deploy_contracts.js
module.exports = function(deployer) {
deployer.deploy(ConvertLib);
deployer.autolink();
deployer.deploy(MetaCoin);
};
3.0版本中的./migrations/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);
};
改进之处如下:
- 我们提供了
artifacts.require()
方法,类似Node.js
的require()
语句,用于声明依赖。 - 你可以通过
artifacts.require()
来引入一个本地的Solidity文件。或者是包中依赖路径,这个方法将负责引入对应的文件。 Javascript
中的测试用例也需要通过artifacts.require()
语句来引入依赖。
Migrations
(移植)不再提供autolink
(自动关联)
正如上一部分所述,自动关联为依赖问题的解决带来了一点点方便。但随着包管理的引入,我们不再可能自动判断在你的移植中所真正需要的所有的依赖。我们提供了替代方案是,通过明确指定的方式来关联你的库。可以参考上一节的artifacts.require()
例子。
合约抽象:JSON格式(不再使用.sol.js
!)
Truffle之前将合约定义为Javascript的格式,以.sol.js
为后缀的文件存储。存为这样的格式的考虑,主要是打算你可以在任何地方容易的使用。但最终发现,我们考虑的并不完善。不仅Javascript中存在一些情况中难以直接使用,更在非Javascript环境完全不可用。为解决这样的局限性,Truffle3.0将所有合约编译后的结果存为JSON
格式,以能随时随地的,跨环境使用。
如果你已经使用Truffle2.0生成了许多的文件,我们提供了一个工具来进行转换。
如果你要升级Truffle2.0生成的.sol.js
,请先安装truffle-soljs-updater
$ npm install -g truffle-soljs-updater
安装好后,你可以通过sjsu
命令来使用这个工具。使用方法如下:
$ cd ./build/contracts
$ sjsu
Converting ConvertLib.sol.js...
Converting MetaCoin.sol.js...
Converting Migrations.sol.js...
Files converted successfully.
具体使用方法是,进入到你的.sol.js
文件所在目录,这里是./build/contracts
。然后运行sjsu
即可。
默认情况下sjsu
命令只会创建新格式的.json
后缀结尾的文件,并不会删除你的原始文件,你需要手动删除旧的.sol.js
来保证Truffle3.0能正常工作。请注意,建议删除前将旧的.sol.js
的文件备份到其它地方。最终确认生成的.json
文件正确的情况下再删除。
另外一种选择是使用强制模式,sjsu -f
,通过加-f
参数,这样sjsu
会删除旧的.sol.js
文件,并生成生的.json
文件。但请务必保证旧的文件已提前进行了妥善的保存,以避免造成不必要的损失。
$ cd ./build/contracts
//务必保证你已经备份了旧的`.sol.js`文件
//`-f`参数会强制删除旧的`.sol.js`文件
$ sjsu -f
Converting ConvertLib.sol.js...
Converting MetaCoin.sol.js...
Converting Migrations.sol.js...
Files converted successfully.
Successfully deleted old .sol.js files.
合约交互抽象层: .deployed()
现在提供了then()
这个改变,会影响你的Migrations
(移植),测试,应用代码
在Truffle2.0中,合约抽象层使用原生的方式来管理网络。提供上面章节中提到的default
的方式,引发潜在的网络不正确的可能,导致可能的部署到错误的网络的问题。
原抽象层提供了一个精心考虑,方便使用的语法,MyContract.deployed().myFunction(...)
,但将错误直接暴露给了开发者。在Truffle3.0中,我们改变了这个语法,为.deployed()
提供了类似promise
的语法。同时,当前的合约抽象层可以与以太坊的包管理标准,EIP190
eip190,进行无缝集成。但这意味着大家必须改变原有代码的语法。
v2.0版本的语法:
MyContract.setProvider(someWeb3Provider);
MyContract.deployed().someFunction().then(function(tx) {
});
v3.0版本的语法:
MyContract.setProvider(someWeb3Provider);
MyContract.deployed().then(function(instance) {
return instance.someFunction();
}).then(function(result) {
});
语法上有些啰嗦,但目的是保证合约抽象层连接到了正确的网络。
我们来看看新语法的例子:
MyContract.setProvider(someWeb3Provider);
var deployed;
MyContract.deployed().then(function(instance) {
deployed = instance;
return deployed.someFunction();
}).then(function(result) {
return deployed.anotherFunction();
}).then(function(result) {
});
这里需要注意的是
- 如果合约方法
f()
不会改变区块链上的数据的,那么调用时要使用instance.f.call()
。 - 如果合约方法
f()
会改变区块链上的数据的,则需使用instance.f()
来进行调用。 - 如果你想在一个
promise
链中调用多个方法,需要注意示例中的instance
的作用域。可以在外部定义一个对象,查看上例中var deployed;
的定义。
一个能跑通的完整例子参考附录2。
合约抽象层:交易结果对象
大家一直以来有个麻烦的事,关于在Web3
中监听事件。在大多数情况下,事件一般不是通过主动跟踪事件,而是我们进行了对应的操作,从而引发对应的事件产生。
尽管主动监听事件的方式仍然支持,但为了让后一种情况实现起来更加简单,我们调整了transaction
的返回结果。
在Truffle2.0版本中,transaction
简单的返回了一个交易的哈希串,而在Truffle3.0中,我们将返回一个包含交易详情的结果对象[demo]。
v2.0
MyContract.deployed().someFunction().then(function(tx) {
});
v3.0
MyContract.deployed().then(function(instance) {
deployed = instance;
return deployed.someFunction();
}).then(function(result) {
//
});
在Truffle3.0中通过返回transaction
的详情信息,我们可以更加方便的决定我们是否要触发某些相应的行为。下面是一个假想的关注合约发布事件的例子。
var assert = require("assert");
var PackageIndex = artifacts.require("PackageIndex.sol");
contract("PackageIndex", function(accounts) {
it("publishes a release correctly", function() {
return PackageIndex.deployed().then(function(deployed) {
return deployed.publish("v2.0.0");
}).then(function(result) {
var found_published_event = false;
for (var i = 0; i < result.logs.length; i++) {
var log = result.logs[i];
if (log.event == "ReleasePublished") {
found_published_event = true;
break;
}
}
assert(found_published_event, "Uh oh! We didn't find the published event!")
});
});
});
虽然这样的话,我们需要在所有返回结果中,去找我们感兴趣的事件,但也许是比PackageIndex.ReleasePublished.watch(...)
更好的一种方式,因为我们可以在当前的代码中管理事件逻辑,而不是分散在代码各处。
一个能跑通的完整例子参考附录[demo]。
构建流:默认不再需要构建器
Truffle1.0和Truffle2.0中,我们将Web应用与整个框架紧紧的捆绑在一起。所以Truffle打包提供了一个默认的构建流,以便你可以快速的建立你的dapp
,并运行起来。虽然在有些情况下,这带来了极大的便利,但在其它的场景下却显得极为鸡肋。
去年中,基于以太坊的应用持续的在增加。从开始的仅仅支持Web
的dapp
应用,发展为可以支持原生语言,在手机或电脑上独立运行。支持各种各样的用户场景,一直是Truffle的初衷,所以我们决定移除默认的构建流。如果你想使用Truffle来集成你的应用,你仍然可以编写自定义的构建流3,因为Truffle打算专注做智能合约相关的最好用工具。所以关于构建,通过集成更好工具,如webpack
,browserify
,Grunt
,Metalsmith
,来实现。
尽管构建流默认被移除了,但并不意味着你没得选择。在Truffle中我们非常注重开发者的使用体验,也永远不会对大家弃之不顾,所以下面我们提供了两种可选方式,后一种选择会让你深度绑定到你最终选择的构建工具上,如webpack
。我们来一起看看,可用的选择吧。
在Truffle3.0中使用旧的构建器
如果你在Truffle2.0中使用了默认的构建器,而你又想升级到Truffle3.0。我们升级了默认构建器4,所以它完全可以与Truffle3.0兼容。但这将是最后一次升级默认的构建器,因为要使之兼容所有的场景,将是一件工程非常复杂的事。所以我们推荐你最终选用后面提到的其它构建系统。
默认构建器,并未集成在Truffle3.0中。所以要使用它,你需要安装truffle-default-builder
,并做为一个依赖进行引入。在你的工程目录运行下面的命令:
$ npm install truffle-default-builder --save
一旦安装,你可以在你的truffle.js
配置文件中,使用默认构建器。我们来看看配置文件的前后变化。
v2.0:Truffle.js
module.exports = {
build: {
"index.html": "index.html",
"app.js": [
"javascripts/app.js"
],
"app.css": [
"stylesheets/app.css"
],
"images/": "images/"
},
rpc: {
host: "localhost",
port: 8545
}
};
v3.0: truffle.js
var DefaultBuilder = require("truffle-default-builder");
module.exports = {
build: new DefaultBuilder({
"index.html": "index.html",
"app.js": [
"javascripts/app.js"
],
"app.css": [
"stylesheets/app.css"
],
"images/": "images/"
}),
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
}
}
};
你会发现,除了需要引入作为依赖的默认构建器包,放入构建器需要的配置文件以外。其它是完全一致的。
由于默认构建器已经使用了最新的合约抽象层5。所以上面提到的所有版本升级需要进行的调整,都适用使用默认构建器。
使用自定义构建流程/构建工具
自定义构建流程并没有想像中难以使用,真正的难点在于写一个构建流程能兼容各种各样的构建需求。我推荐你去看看适合你自己项目的构建工具,比如之前提及的webpack
,browserify
,Grunt
,Metalsmith
。最终哪个的特性能满足你的需要,需要视你的项目及构建需求来定。
无论你想构建一个浏览器应用,命令行工具,JS库,还是原生 的手机应用。合约的初始化,部署合约的使用均遵循通用的流程。
当你配置你的自定义工具或应用时,应遵循下述的流程:
- 编译合约文件,将生成的
.json
结果放到./build/contracts
目录下。 - 通过
truffle-contract
[truffle-contract],将你编译的合约结果,转为合约抽象层,来方便你的使用。 - 为你的合约抽象层设置
web3 provider
。需要注意的是在Metamask
和Mist
中,环境内会自动提供。但在其它情况下,你需要手动通过配置完成指定。
下面是集成NodeJS
的一个例子:
//1. 引入编译好的合约文件结果
var json = require("./build/contracts/MyContract.json");
// 2. 将合约转为合约抽象层实例
var contract = require("truffle-contract");
var MyContract = contract(json);
// 3. 设置合约抽象层实例的web3 provider
MyContract.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));
// 4. 现在你可以使用啦
MyContract.deployed().then(function(deployed) {
return deployed.someFunction();
});
所有的构建流程,均遵循上述的流程。核心关注点是保证自定义流程要加载所有的合约资源,并正确的设置合约抽象层。
一个能跑通的完整例子参考附录[demo]。
关于network_id的配置项说明,参考这里 https://github.com/ethereum/go-ethereum/wiki/Command-Line-Options。 ↩
Truffle3.0集成NodeJS并完全跑通(附详细实例,可能的错误) ↩
编写你自己的构建流: http://truffleframework.com/tutorials/upgrading-from-truffle-2-to-3 ↩
Truffle的默认构建器 https://github.com/trufflesuite/truffle-default-builder ↩
Truffle的合约抽象层,即运行框架工具包 https://github.com/trufflesuite/truffle-contract ↩