Getting Started: Starting A Real Project

项目实战

实际项目往往不会只有一个独立的文件,而是会依赖于各种库或包,例如Hack 标准库或者是其他一些可选的工具。

一个良好的项目应该是这样启动的:

  • 安装 Composer
  • 创建一个空白的 .hhconfig 文件
  • 创建 src/tests/ 子目录
  • 配置自动加载
  • 使用 Composer 来安装公共依赖和工具

自动加载

在 HHVM 中是没有“编译”这个步骤的,每个文件都是按需执行。当前,我们需要为 HHVM 提供哪个文件定义了哪些类/函数等的映射关系,例如,当执行代码 new Foo() 的时候,HHVM 需要知道 Foo 类是定义在 src/Foo.hack 的。

hhvm-autoload 可以生成这种映射关系。你可以通过以下命令将 hhvm-autoload 添加到你的项目中去:

$ php /path/to/composer.phar require hhvm/hhvm-autoload

hhvm-autoload 的配置文件是 hh_autoload.json。对于大多数项目而言,最简单的配置结构如下:

{
  "roots": [
    "src/"
  ],
  "devRoots": [
    "tests/"
  ],
  "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
}

roots 节点指定生产环境中需要加载的目录

devRoots 节点中的是开发和测试环境中需要自动加载的目录,在生产环境中时这些目录不会被加载

devFailureHandler 是后备策略的完全限定名称。当你添加了一个新的类或者函数并且没有执行 hh-autoload 时,自动加载映射关系不会被自动更新。HHVM 在自动加载映射中找不到你的类型、常量或者是函数等时,就会调用后备。

后备会尝试在运行时加载类型、常量或者是函数等等(此过程会大大降低代码执行速度,因此不应该在生产环境中使用它)。同时,不是所有的常量或函数都能/会被 HHClientFallbackHandler 找到,你可以在 GitHub 仓库 中查阅更多信息。

一旦配置文件创建好,vendor/bin/hh-autoload 就可以用来生成或更新映射表并创建 vendor/autoload.hack

例子

按顺序执行以下命令可以完整地初始化一个带有常用依赖的 Hack 项目:

$ touch .hhconfig
$ mkdir bin src tests
$ cat > hh_autoload.json
{
  "roots": [
    "src/"
  ],
  "devRoots": [
    "tests/"
  ],
  "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
}
$ composer require hhvm/hsl hhvm/hhvm-autoload
$ composer require --dev hhvm/hhast hhvm/hacktest facebook/fbexpect

由于 Composer 安装方式可能不一样,你在使用 Composer 的时候可能需要使用绝对路径。

以下是同样的命令,以及其输出(译者注:在你的机器上可能会略有不同):

$ touch .hhconfig
$ mkdir bin src tests
$ cat > hh_autoload.json
{
  "roots": [
    "src/"
  ],
  "devRoots": [
    "tests/"
  ],
  "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
}
$ composer require hhvm/hsl hhvm/hhvm-autoload
Using version ^4.0 for hhvm/hsl
Using version ^2.0 for hhvm/hhvm-autoload
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing hhvm/hsl (v4.0.0): Loading from cache
  - Installing hhvm/hhvm-autoload (v2.0.3): Loading from cache
Writing lock file
Generating autoload files
/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/vendor/autoload.hack
$ composer require --dev hhvm/hhast hhvm/hacktest facebook/fbexpect
Using version ^4.0 for hhvm/hhast
Using version ^1.4 for hhvm/hacktest
Using version ^2.5 for facebook/fbexpect
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 7 installs, 0 updates, 0 removals
  - Installing facebook/difflib (v1.1): Loading from cache
  - Installing hhvm/hsl-experimental (v4.0.1): Loading from cache
  - Installing hhvm/type-assert (v3.3.1): Loading from cache
  - Installing facebook/hh-clilib (v2.1.0): Loading from cache
  - Installing hhvm/hhast (v4.0.4): Loading from cache
  - Installing hhvm/hacktest (v1.4): Loading from cache
  - Installing facebook/fbexpect (v2.5.1): Loading from cache
Writing lock file
Generating autoload files
/private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/vendor/autoload.hack
$

添加函数或者类

作为一个实验性例子,我们将会创建一个函数,它可以遍历计算数字向量的平方值。保存下面的代码到 src/square_vec.hack

use namespace HH\Lib\Vec;

function square_vec(vec<num> $numbers): vec<int> {
  return Vec\map($numbers, $number ==> $number * $number);
}

如果你运行 hh_client,它会向你报一个错:

src/square_vec.hack:4:10,57: Invalid return type (Typing[4110])
  src/square_vec.hack:3:53,55: This is an int
  src/square_vec.hack:4:40,56: It is incompatible with a num (int/float) because this is the result of an arithmetic operation with a num as the first argument, and no floats.
  src/square_vec.hack:3:35,35: Here is why I think the argument is a num: this is a num

这时只需要将返回类型由 vec<int> 改成 vec<num> 即可修复

到此我们就有了一个合法的 Hack 函数,但它没有被测试过,也没有被调用。

添加可执行程序

将下面的代码保存为 bin/square_some_things.hack:

#!/usr/bin/env hhvm

require_once(__DIR__.'/../vendor/autoload.hack');

<<__EntryPoint>>
async function main(): Awaitable<void> {
  \Facebook\AutoloadMap\initialize();

  $squared = square_vec(vec[1, 2, 3, 4, 5]);
  foreach ($squared as $square) {
    printf("%d\n", $square);
  }
}

这段程序:

  • 载入并初始化了自动加载器,使得上面我们定义的函数可以被加载进来
  • 调用了之前定义的函数
  • 打印了函数结果

<<__EntryPoint>> 注解将这个函数标记为这段可执行程序在被执行时的入口(函数名 main 没有特别之处)。

现在你可以用 HHVM 显式地执行你的新程序,或者将其标记为可执行文件来执行(译者注:在代码文件中的第一行加入 #!/usr/bin/env hhvm,这种写法叫做 Shebang 或者 Hashbang):

$ hhvm bin/square_some_things.hack
1
4
9
16
25
$ chmod +x bin/square_some_things.hack
$ bin/square_some_things.hack
1
4
9
16
25

Linting

大多数项目都会使用 linter 来执行一些代码风格化,尽管不是语言本身要求的,但这么做可以使得你的代码风格看起来更加一致。HHAST 是推荐用于 Hack 代码的 linter。HHAST 的 linter 由项目根目录中的 hhast-lint.json 启用。一个好的新项目应该为所有包含代码的目录开启所有 linters。将下面的内容保存为 hhast-lint.json

{
  "roots": [ "bin/", "src/", "tests/" ],
  "builtinLinters": "all"
}

在之前执行 composer require 的时候,HHAST 已经被安装到 vendor/ 子目录了,你可以直接执行:

$ vendor/bin/hhast-lint
Function "main()" does not match conventions; consider renaming to "main_async"
  Linter: Facebook\HHAST\Linters\AsyncFunctionAndMethodLinter
  Location: /private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/bin/square_some_things.hack:5:0
  Code:
  >
  ><<__EntryPoint>>
  >async function main(): Awaitable<void>

单元测试

HackTest 被用于创建单元测试类,而 fbexpect 被用于表达断言。我们创建一个基本的测试 tests/MyTest.hack

use function Facebook\FBExpect\expect;
use type Facebook\HackTest\{DataProvider, HackTest};

final class MyTest extends HackTest {
  public function provideSquaresExamples(): vec<(vec<num>, vec<num>)> {
    return vec[
      tuple(vec[1, 2, 3], vec[1, 4, 9]),
      tuple(vec[1.1, 2.2, 3.3], vec[1.1 * 1.1, 2.2 * 2.2, 3.3 * 3.3]),
    ];
  }

  <<DataProvider('provideSquaresExamples')>>
  public function testSquares(vec<num> $in, vec<num> $expected_output): void {
    expect(square_vec($in))->toBeSame($expected_output);
  }
}

然后我们可以用 HackTest 来运行测试:

$ vendor/bin/hh-autoload
$ vendor/bin/hacktest tests/
..

Summary: 2 test(s), 2 passed, 0 failed, 0 skipped, 0 error(s).

不是所有时候都需要重新生成自动加载映射表(用 hh-autoload),但是如果类在自动加载映射表中找不到时,你可能会得到有关反射类不存在的异常。因此建议在执行测试套件之前,确保自动加载映射表时完整的。

如果我们人为加入一个错误,例如 tuple(vec[1, 2, 3], vec[1, 2, 3]),HackTest 会报:

$ vendor/bin/hacktest tests/
..F

1) MyTest::testSquares with data set #3 (vec [
  1,
  2,
  3,
], vec [
  1,
  2,
  3,
])

Failed asserting that vec [
  1,
  4,
  9,
] is the same as vec [
  1,
  2,
  3,
]

/private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/tests/MyTest.hack(15): Facebook\FBExpect\ExpectObj->toBeSame()


Summary: 3 test(s), 2 passed, 1 failed, 0 skipped, 0 error(s).

配置 Git

vendor/ 目录不应该提交到 Git;在其他系统或分支,用 composer install 来安装依赖。这个操作会使用已经生成好的 composer.lock (这个文件应该提交到 Git)来安装相同版本的依赖。

$ echo vendor/ > .gitignore

如果你编写的是库,那这个库的用户可能不想要你的单元测试,因为如果包含了你的单元测试,他们需要安装兼容版本的 fbexpecthacktest才不会得到 Hack 报错。

由于 Composer 使用的 GitHub releases 是由 git export 自动打包的,因此最简单的方式就是通过配置 git export 来忽略 tests/ 目录:

$ echo 'tests/ export-ignore' > .gitattributes

配置 TravisCI

我们推荐在 TravisCI 上使用 Docker 来对 Hack 项目进行持续集成。我们通常通过创建在容器中单独执行的 .travis.sh 来实现。举个例子,一份 .travis.tml 大概会长下面这样:

sudo: required
language: generic
services: docker
env:
- HHVM_VERSION=latest
- HHVM_VERSION=nightly
install:
- docker pull hhvm/hhvm:$HHVM_VERSION
script:
- docker run --rm -w /var/source -v $(pwd):/var/source hhvm/hhvm:$HHVM_VERSION ./.travis.sh

... 及其对应的 .travis.sh:

#!/bin/sh
set -ex
apt update -y
DEBIAN_FRONTEND=noninteractive apt install -y php-cli zip unzip
hhvm --version
php --version

(
  cd $(mktemp -d)
  curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
)
composer install

hh_client
vendor/bin/hacktest tests/
if !(hhvm --version | grep -q -- -dev); then
  vendor/bin/hhast-lint
fi

使用以上配置,TravisCI 在运行的时候将会检查 Hack 错误、单元测试失败;以及在发布构建时执行 hhast-lint。我们不在 -dev 构建时执行 hhast-lint,因为 hhast-lint 依赖与 HHVM/Hack 的实现细节,而 HHVM/Hack 的实现细节迭代得很频繁。


本节由 Y!an 翻译