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中会指定的ipport,和配置的网络类型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.jsrequire()语句,用于声明依赖。
  • 你可以通过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的语法。同时,当前的合约抽象层可以与以太坊的包管理标准,EIP190eip190,进行无缝集成。但这意味着大家必须改变原有代码的语法。

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,并运行起来。虽然在有些情况下,这带来了极大的便利,但在其它的场景下却显得极为鸡肋。

去年中,基于以太坊的应用持续的在增加。从开始的仅仅支持Webdapp应用,发展为可以支持原生语言,在手机或电脑上独立运行。支持各种各样的用户场景,一直是Truffle的初衷,所以我们决定移除默认的构建流。如果你想使用Truffle来集成你的应用,你仍然可以编写自定义的构建流3,因为Truffle打算专注做智能合约相关的最好用工具。所以关于构建,通过集成更好工具,如webpackbrowserifyGruntMetalsmith,来实现。

尽管构建流默认被移除了,但并不意味着你没得选择。在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。所以上面提到的所有版本升级需要进行的调整,都适用使用默认构建器。

使用自定义构建流程/构建工具

自定义构建流程并没有想像中难以使用,真正的难点在于写一个构建流程能兼容各种各样的构建需求。我推荐你去看看适合你自己项目的构建工具,比如之前提及的webpackbrowserifyGruntMetalsmith。最终哪个的特性能满足你的需要,需要视你的项目及构建需求来定。

无论你想构建一个浏览器应用,命令行工具,JS库,还是原生 的手机应用。合约的初始化,部署合约的使用均遵循通用的流程。

当你配置你的自定义工具或应用时,应遵循下述的流程:

  • 编译合约文件,将生成的.json结果放到./build/contracts目录下。
  • 通过truffle-contract[truffle-contract],将你编译的合约结果,转为合约抽象层,来方便你的使用。
  • 为你的合约抽象层设置web3 provider。需要注意的是在MetamaskMist中,环境内会自动提供。但在其它情况下,你需要手动通过配置完成指定。

下面是集成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]。

处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。