用 Bazel 构建 TypeScript 项目

NoelJason 发布于1年前

在本篇,我会简单介绍一下Bazel是什么,我们怎么用它构建一个Typescript项目。如果你已经熟知Bazel解决什么问题,那么请跳到“用Bazel构建Typescript”一节。你可以在 我的Github 上找到例子!

用 Bazel 构建 TypeScript 项目

Google's 集成内幕

谷歌管理着无数的源码。在每个独立项目间都有依赖,比如Google Cloud 依赖于 Angular & Angular Material,Angular Material 又依赖于 Angular, 最后它们都依赖 TypeScript.

在谷歌中,项目都有使用独立库(以及第三方库)的版本号。在公司内,这极大的简化了依赖管理。同时升级一个库,能影响所有依赖的项目,这样很方便。这样每个人都能从中得到好处,比如最新的安全更新,性能优化,Bug修复。这样做就意味着微软发布新的Typescript, 它会尽快的同步到谷歌内部,可以想你,Typescript的破坏性变更会影响许多代码!

为了验证库升级后没有原来代码,在内部的持续集成中需要重新构建所有的依赖项目,并执行相应测试。因为有无数的TS文件,无数行TS代码,如果把控不好,整个过程可能需要点时间。

多数情况,项目依赖其它项目,比如Google Cloud依赖它的UI和后端服务。当只更新Typescript时,既然后台服务不依赖Typescript,我们不希望它重新构建后台服务!

Bazel是什么

为了构建项目,谷歌开源了这个Bazel工具。它是一个强大的工具,它可以跟踪不同的包间的依赖,并构建目标。简单说,一个构建就是一个构建规则,比如: “构建一下Typescript库”;  从项目看,一个包对应着一个文件夹中的许多文件,它有清晰的依赖包。在Google Cloud的示例中,Bazel会对应下面这么一个依赖关系图:

用 Bazel 构建 TypeScript 项目

简单说,我们把上图每一个块都对应一个构建目标。当其中某个块变化,Bazel会计算哪些包直接或间接依赖于它,并构建它们。在上面例子,如果TypeScript变化了,它会构建除了 back-end services之外的块。

下面是Bazel的酷的特性:

1、它有一个聪明的算法来计算依赖

2、它有独立的技术栈。你可以用同样的接口构建任意的事情。就像,已经有许多它的插件来服务于:Java,Go,Typescript,Javascript等等。

我们先看看第一条,基于一个项目的依赖图,Bazel会判断哪些构建目标是可以并行处理。但这个特性只在单元测试已经很好的定义了输入项和输出项,且它们不产生副作用时才有效。我们可以理解为它们是“纯函数”才行。这个“可被计算”的模型有一个好处,它很容易通过并行和缓存的方法,就减少计算量。Bazel就是这样的,它会对独立的构建任务缓存产生的输出结果,即使在云端的时候!

为什么要强调云端的缓存问题呢?如果Bazel能够构建后且缓存在云端,任何人都能利用这个构建结果。如果你是一个大公司肯定需要这样,即使小团队也能受益于它。Bazel并不是绑定于某个云平台的,这就是说你可以在Google Cloud, Azure, AWS, 或你自己的设备上获取缓存远程构建这个好处。

好吧,虚的讲了很多了,让我们看个例子吧!

用Bazel构建TypeScript

这个例子时在,我们构建一个小的ts项目,最终生成一个es5的js文件。 这个项目只有以下几个模块:

  • Lexer -  输入字符串后,返回一个token数组

  • Parser - 输入一个token数组。返回抽像语法树AST.

  • Interpreter - accepts an AST and evaluates it

  • Application - wires everything together - passes the program to the lexer, feeds the parser with the lexer’s output, and the interpreter with the produced AST

配置Workspace环境

我们的项目依赖npm包TypeScript, Bazel, 还有一个Bazel的TypeScript规则。 这和其它项目没什么不同的。所以我们现在深入WORKSPACE 这个配置文件一探究竟:

workspace(name = 'lang')

http_archive(
    name = "build_bazel_rules_typescript",
    url = "https://github.com/bazelbuild/rules_typescript/archive/0.21.0.zip",
    strip_prefix = "rules_typescript-0.21.0",
)

# Fetch our Bazel dependencies that aren't distributed on npm
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
rules_typescript_dependencies()

# Setup TypeScript toolchain
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
ts_setup_workspace()

# Setup the Node.js toolchain
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories", "yarn_install")
node_repositories()

# Setup Bazel managed npm dependencies with the `yarn_install` rule.
yarn_install(
  name = "npm",
  package_json = "//:package.json",
  yarn_lock = "//:yarn.lock",
)

要知道这篇文章只是一个浅显的入门介绍。事实上,你可能从未自己管理过Bazel 配置.

上面这个文件使用的语言叫 Starlark ,你可以认为它是Python语言的子集,这是我们声明的:

  1. workspace 名称是 lang

  2. 项目使用 Bazel’s TypeScript rules.要记住它和我们声明在 package.json 的版本号是一致的。

  3. 下一步,我们获取Bazel’s的依赖项 build_bazel_rules_typescript

  4. 设置 TypeScript workspace

  5. 设置 Bazel’s Node.js 的构建工具链. 它是由 Bazel team 维护的一组工具,所以我们可以在此使用 Node.js

  6. 最后我们声明一个规则让Bazel 管理 npm 依赖!

如果上面代码看不明白也没关系,只要记住 load 差不多是Node.js中的  require, 不同的是load 还可以从网络地址获取依赖!

配置编译目标

我们深入到更细的粒度上。我们把项目中独立的模块当成一个个 Bazel 包,在这每一个包中,我们定义一个编译目标。我们上面提到过,我们把每个包看作一个文件夹,它包含里面的文件以及一个构建规则。

每个文件夹都有一个 BUILD 或  BUILD.bzl 文件。先看一下要项目中的BUILD文件:

package(default_visibility = ["//visibility:public"])

load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")

ts_library(
  name = "app",
  srcs = ["test.ts"],
  deps = ["//lexer", "//parser", "//interpreter"],
)

load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle")

rollup_bundle(
  name = "bundle",
  entry_point = "test.js",
  deps = [":app"],
)

我们首先定义这个包是否对外可见,之后加载2个Bazel的规则:

  • ts_library - 用它来编译Typescript文件

  • rollup_bundle - 调用 Rollup.js 把各模块打包到一个文件里去。

ts_library 规则用来构建名为  app 的目标. 这个app的构建目标就是把各个模块串联到一起后的结果. 它依赖于  lexer ,  parser , and the  interpreter . 在这三个文件夹中,还都有着各自的  BUILD 文件,内容差不多也是上面样子.  比如  parser的内容 :

package(default_visibility = ["//visibility:public"])

load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")

ts_library(
    name = "parser",
    srcs = glob(["*.ts"]),
    deps = ["//lexer"],
)

这里我们用 glob 来限定源文件,另外这个模块还依赖着  lexer 模块

诸多依赖就有诸多构建目标,这可能带来混乱。Bazel 有个简洁的功能是依赖关系图必须是静态可分析的。我们能用Bazel查询语法,直接查询依赖关系图。来让我看看看它什么样子吧!

第一步,用 yarn  来安装   package.json  中的所有依赖包,这样就自动安装好 Bazel 。

yarn

启动bazel (可能要先安装graphviz才行)

./node_modules/.bin/bazel query --output=graph ... | dot -Tpng > graph.png

上面命令的输出结果如下:

用 Bazel 构建 TypeScript 项目

我们看到各个目标间的依赖关系。

现在我们可以构建整个项目:

./node_modules/.bin/bazel build :bundle

可以预料上面命令需要一点点时间,因为Bazel加载workspace中的依赖。 之后每一步就应该是瞬间完成了。

现在来验证一下,执行构建后的bundle是否符合预期:

node bazel-bin/bundle.js
43

当你构建过一次Bazel目标后,工作空间会产生许多symlinks,它们包含诸多构建产物。如果你不想要,你可以通过 bazel.rc 来配置

监听模式

怎么监听项目文件的变化,及时进行rebuilding呢?为了这个目的,我们要安装运行 @bazel/ibazel  包

# Don’t forget yarn add -D @bazel/ibazel
./node_modules/.bin/ibazel build :app

通过 ibazel and  bazel 包,在依赖直接或间接变化时,它会重新构建目标

注意,在上面命令中,我们构建的是 :app 的目标。这是因为在代码开发中,基于Bazel的Rollup.js规则,我们不可能直接编码影响到 :bundle 这个构建目标。(言外之意是,  :bundle 完全是通过    :app 生成的)

现在让我们创建npm的命令缩写来代替 ./node_modules/.bin/bazel or  ./node_modules/.bin/ibazel

在 package.json 添加下面两句话:

{
  "name": "bazel-demo",
  "license": "MIT",
  "scripts": {
    "build": "bazel build :bundle",
    "watch": "ibazel build :app"
  },
  "devDependencies": {
    "@bazel/bazel": "^0.19.1",
    "@bazel/typescript": "0.21.0",
    "typescript": "^3.1.6"
  }
}

现在运行 yarn build 来重新构建项目或  yarn watch 来监听并自动构建项目。

结束语

本篇中,我们关注谷歌的构建系统 Bazel。文章解释了 Bazel 解决了哪些问题,来提供给我们一个快速灵活的构建解决方案。

在本篇的第二部分,我们通过一个小型的 Typescript 项目演示了构建的例子。我们编译一组文件然后用 Rollup.js 打包输出为一个文件。在例子里,我们解释了什么是工作空间、构建目标、包,以及对它们进行配置。

至此,我们能看到 Bazel 的核心能力:静态的依赖关系图,它允许了像 缓存、并行等的 的简洁优化。在下篇文章中,我们将更近的关注一下性能的实现!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。

我们的翻译工作遵照 CC 协议 ,如果我们的工作有侵犯到您的权益,请及时联系我们。

查看原文: 用 Bazel 构建 TypeScript 项目

  • crazyfish
  • crazybird
  • organicbear
  • beautifulfish
  • tinydog