发布于 2026-01-06 0 阅读
0

Angular + Bazel:准备就绪!DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

Angular + Bazel:准备就绪!

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

在这篇文章中,我将介绍什么是 Bazel,如何将其与 Angular CLI 一起使用,以及为什么采用它是个好主意。

Bazel是什么?

Bazel 是 Google 开发的一款支持多语言的构建工具/系统。
它具有可扩展性,这意味着您可以添加对其他语言和框架的支持。
它还能识别何时需要重新构建代码的特定部分,并缓存构建产物,从而实现极快的构建速度。

概念

工作区:工作区是包含待构建源文件的根目录。它至少应该包含一个名为 `<workspace_name>` 的文本文件WORKSPACE。如果WORKSPACE在任何子目录中找到该文件,父工作区将忽略它,并创建一个新的工作区。

:与工作区类似,包是工作区下的目录,由 `.bush`BUILD文件(或 `.bush.bazel` 文件)定义。如果一个子目录Package包含一个 ` .bush`BUILD文件,则它被视为不同的工作区Package

目标:目标的要素Package主要分为两类:

  • 由我们提供的构建文件或源文件生成的文件。
  • 规则会根据输入(源文件或生成文件)返回输出(生成文件)。规则的输出始终属于该规则所在的同一包。

参考文献

引用文件和输出有多种方法。
如果要引用同一包内的规则,可以使用冒号 ( :) 和规则名称。

a_rule(
    name = "rule_name"
)

another_rule(
    name = another_rule_name,
    deps = [
        ":rule_name"
    ]
)
Enter fullscreen mode Exit fullscreen mode

当引用另一个包时,我们将使用双斜杠 ( //) 进入工作区的根目录,然后从那里进入所需的包。

another_package_rule(
    name = "some_name",
    deps = [
        "//foo/bar:a_rule"
    ]
)
Enter fullscreen mode Exit fullscreen mode

如果规则名称与包名称匹配,则可以省略冒号后面的规则名称。

引用等于//foo/bar:bar//foo/bar

Bazel 和 Angular

Bazel 与语言和框架无关,但我首先想看看它如何与 Angular 一起使用,并了解其工作原理。
让我们安装所需的依赖项并创建一个新项目。

npm install -g @angular/bazel
ng new --collection=@angular/bazel
Enter fullscreen mode Exit fullscreen mode

此命令将创建一个新项目,并已将 Bazel 设置为构建工具。

如果我们ng serve在新项目中运行我们的应用程序(),将会创建一些新文件。

  • /工作区
  • /BUILD.bazel
  • .bazelignore
  • .bazelrc
  • /src/BUILD.bazel
  • /e2e/BUILD.bazel

我们可以确定,CLI 定义了一个 `<package_name>` workspace、`<package_name>` 和三个 `<package_name>`packages文件夹。根据包定义,位于根目录的包将包含工作区中的所有内容,但 `<package_name>` 和src` e2e<package_name>` 文件夹除外。
如果我们停止服务器,这些文件将被再次删除。
这就是zero-configBazel!

但是,如果我们因为安装了新库或想要优化构建而需要调整构建,该怎么办呢?我们可以手动创建 WORKSPACE 和 BUILD 文件,也可以使用 `--leaveBazelFilesOnDisk` 标志,这样生成的文件就不会被删除。

ng build --leaveBazelFilesOnDisk
Enter fullscreen mode Exit fullscreen mode

构建完成后,我们将能够浏览和/或修改生成的文件。

我们先从……开始/src/BUILD.bazel。我会把文件分成几个部分,以便于解释。

package(default_visibility = ["//visibility:public"])
Enter fullscreen mode Exit fullscreen mode

最顶层是可见性属性。可见性属性决定了其他包是否能够访问此包中的规则。可见性的作用域可以设置。在本例中,我们将其设置为公开。

load("@npm_angular_bazel//:index.bzl", "ng_module")
load("@npm_bazel_karma//:index.bzl", "ts_web_test_suite")
load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle", "history_server")
load("@build_bazel_rules_nodejs//internal/web_package:web_package.bzl", "web_package")
load("@npm_bazel_typescript//:index.bzl", "ts_devserver", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "multi_sass_binary", "sass_binary")
Enter fullscreen mode Exit fullscreen mode

然后是所有我们将要用到的导入语句,它们都会通过 load 命令加载。这将从扩展中导入一个符号。

之后,剩下的就是规则及其配置了。

sass_binary(
  name = "global_stylesheet",
  src = glob(["styles.css", "styles.scss"])[0],
  output_name = "global_stylesheet.css",
)


multi_sass_binary(
    name = "styles",
    srcs = glob(
      include = ["**/*.scss"],
      exclude = ["styles.scss"],
    ),
)

Enter fullscreen mode Exit fullscreen mode

每条规则的开头都有它的名称。这样我们就可以在其他地方引用它,例如,将其作为依赖项添加到另一条规则中。

第一条规则名为 `<rule_name>` global_stylesheet,这意味着我们可以通过调用 `<rule_name>`(来自同一包)来获取其输出的引用:global_stylesheet。源文件可以是 `.css` 或 `.scss`,具体取决于我们的配置。我们还需要配置输出文件。

我们的第二条规则styles将转换所有 .scss 文件,但不包括全局样式文件。

如果你(像我一样)更喜欢使用 CSS,我们可以去掉很多这类代码。我们稍后会这样做。

我们继续学习下一条规则ng_module

ng_module(
    name = "src",
    srcs = glob(
        include = ["**/*.ts"],
        exclude = [
            "**/*.spec.ts",
            "main.ts",
            "test.ts",
            "initialize_testbed.ts",
        ],
    ),
    assets = glob([
      "**/*.css",
      "**/*.html",
    ]) + ([":styles"] if len(glob(["**/*.scss"])) else []),
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser",
        "@npm//@angular/router",
        "@npm//@types",
        "@npm//rxjs",
    ],
)
Enter fullscreen mode Exit fullscreen mode

首先,我们将规则名称与目录名称相同,这样便于后续引用。将主规则名称与包目录名称相同是一种良好的实践。接下来,我们设置源文件(所有 .ts 文件,忽略模块之外的文件)和资源文件(.css 和 .html 文件,以及styles转换 .scss 文件的规则的输出结果)。最后,我们声明依赖项,在本例中,这些依赖项位于npm工作区中。

rollup_bundle(
    name = "bundle",
    entry_point = ":main.prod.ts",
    deps = [
        "//src",
        "@npm//@angular/router",
        "@npm//rxjs",
    ],
)

web_package(
    name = "prodapp",
    assets = [
        # do not sort
        "@npm//:node_modules/zone.js/dist/zone.min.js",
        ":bundle.min.js",
        ":global_stylesheet",
    ],
    data = [
        "favicon.ico",
    ],
    index_html = "index.html",
)

history_server(
    name = "prodserver",
    data = [":prodapp"],
    templated_args = ["src/prodapp"],
)

filegroup(
    name = "rxjs_umd_modules",
    srcs = [
        # do not sort
        "@npm//:node_modules/rxjs/bundles/rxjs.umd.js",
        ":rxjs_shims.js",
    ],
)

ts_devserver(
    name = "devserver",
    port = 4200,
    entry_module = "project/src/main.dev",
    serving_path = "/bundle.min.js",
    scripts = [
        "@npm//:node_modules/tslib/tslib.js",
        ":rxjs_umd_modules",
    ],
    static_files = [
        "@npm//:node_modules/zone.js/dist/zone.min.js",
        ":global_stylesheet",
    ],
    data = [
        "favicon.ico",
    ],
    index_html = "index.html",
    deps = [":src"],
)

ts_library(
    name = "test_lib",
    testonly = 1,
    srcs = glob(["**/*.spec.ts"]),
    deps = [
        ":src",
        "@npm//@angular/core",
        "@npm//@angular/router",
        "@npm//@types",
    ],
)

ts_library(
    name = "initialize_testbed",
    testonly = 1,
    srcs = [
        "initialize_testbed.ts",
    ],
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser-dynamic",
        "@npm//@types",
    ],
)

ts_web_test_suite(
    name = "test",
    srcs = [
        "@npm//:node_modules/tslib/tslib.js",
    ],
    runtime_deps = [
        ":initialize_testbed",
    ],
    # do not sort
    bootstrap = [
        "@npm//:node_modules/zone.js/dist/zone-testing-bundle.js",
        "@npm//:node_modules/reflect-metadata/Reflect.js",
    ],
    browsers = [
        "@io_bazel_rules_webtesting//browsers:chromium-local",
    ],
    deps = [
        ":rxjs_umd_modules",
        ":test_lib",
        "@npm//karma-jasmine",
    ],
)
Enter fullscreen mode Exit fullscreen mode

最后一部分将设置和配置我们的打包器、开发服务器、生产构建和测试。

使用 Bazel 时,所有依赖项都已声明,输入和输出也已明确。这使我们能够分析系统运行状况并构建有向图。我们可以使用 `query` 命令查询任何包、规则或仓库及其依赖项。`query` 命令功能强大,可以提出各种请求,并且还可以获取不同的输出类型。我建议您查阅相关文档我们还会使用一个工具将查询图的结果转换为图像。

// get al the packages in the workspace
bazel query --output=graph ... | dot -Tpng > graph.png
Enter fullscreen mode Exit fullscreen mode

工作区输出图

信息量似乎有点大。幸运的是,我们可以选择图表中的部分内容。

bazel query "kind(rule, allpaths(//src:devserver, //...))" --output=graph | dot -Tpng > devserver.png
Enter fullscreen mode Exit fullscreen mode

开发服务器输出图

让我们开始利用现有资源优化和定制我们的构建。首先,在这个项目中,我决定不使用任何 .scss 文件,因此可以移除该styles规则。请记住同时移除所有对它的引用。

我还希望应用程序目录成为一个独立的包,所以我将在其中创建一个 BUILD.bazel 文件。

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

load("@npm_angular_bazel//:index.bzl", "ng_module")

ng_module(
    name = "app",
    srcs = glob(
        include = ["**/*.ts"],
        exclude = ["**/*.spec.ts"],
    ),
    assets = glob([
      "**/*.css",
      "**/*.html",
    ]),
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser",
        "@npm//@angular/router",
        "@npm//@types",
        "@npm//rxjs",
    ],
)

ts_library(
    name = "test_lib",
    testonly = 1,
    srcs = ["app.component.spec.ts"],
    deps = [
        ":app",
        "@npm//@angular/core",
        "@npm//@angular/router",
        "@npm//@types",
    ],
)
Enter fullscreen mode Exit fullscreen mode

src/app我们在名为 `<package_name> ` 的新包中声明了一条新规则app。我们还创建了一条规则来处理此包中的测试。
接下来,我们移除了对该规则的引用styles,并将其移除,改为将 `<package_name>` 设置styles.css为源。此外,我们还添加了对 `<app>` 包中测试的引用。

ng_module(
    name = "src",
    srcs = glob(
        include = ["**/*.ts"],
        exclude = [
            "**/*.spec.ts",
            "main.ts",
            "test.ts",
            "initialize_testbed.ts",
        ],
    ),
    assets = glob([
      "**/*.css",
      "**/*.html",
    ]),
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser",
        "@npm//@angular/router",
        "@npm//@types",
        "@npm//rxjs",
        "//src/app"
    ],
)

rollup_bundle(
    name = "bundle",
    entry_point = ":main.prod.ts",
    deps = [
        "//src",
        "@npm//@angular/router",
        "@npm//rxjs",
    ],
)

web_package(
    name = "prodapp",
    assets = [
        # do not sort
        "@npm//:node_modules/zone.js/dist/zone.min.js",
        ":bundle.min.js",
        "styles.css",
    ],
    data = [
        "favicon.ico",
    ],
    index_html = "index.html",
)

history_server(
    name = "prodserver",
    data = [":prodapp"],
    templated_args = ["src/prodapp"],
)

filegroup(
    name = "rxjs_umd_modules",
    srcs = [
        # do not sort
        "@npm//:node_modules/rxjs/bundles/rxjs.umd.js",
        ":rxjs_shims.js",
    ],
)

ts_devserver(
    name = "devserver",
    port = 4200,
    entry_module = "project/src/main.dev",
    serving_path = "/bundle.min.js",
    scripts = [
        "@npm//:node_modules/tslib/tslib.js",
        ":rxjs_umd_modules",
    ],
    static_files = [
        "@npm//:node_modules/zone.js/dist/zone.min.js",
        "styles.css",
    ],
    data = [
        "favicon.ico",
    ],
    index_html = "index.html",
    deps = [":src"],
)

ts_library(
    name = "test_lib",
    testonly = 1,
    srcs = glob(["**/*.spec.ts"]),
    deps = [
        ":src",
        "@npm//@angular/core",
        "@npm//@angular/router",
        "@npm//@types",
    ],
)

ts_library(
    name = "initialize_testbed",
    testonly = 1,
    srcs = [
        "initialize_testbed.ts",
    ],
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser-dynamic",
        "@npm//@types",
    ],
)

ts_web_test_suite(
    name = "test",
    srcs = [
        "@npm//:node_modules/tslib/tslib.js",
    ],
    runtime_deps = [
        ":initialize_testbed",
    ],
    # do not sort
    bootstrap = [
        "@npm//:node_modules/zone.js/dist/zone-testing-bundle.js",
        "@npm//:node_modules/reflect-metadata/Reflect.js",
    ],
    browsers = [
        "@io_bazel_rules_webtesting//browsers:chromium-local",
    ]`
    deps = [
        ":rxjs_umd_modules",
        ":test_lib",
        "//src/app:test_lib",
        "@npm//karma-jasmine",
    ],
)
Enter fullscreen mode Exit fullscreen mode

如果我们再次分析一下我们的开发服务器。
依赖关系图已更新

我认为,随着工作区规模的扩大,使用 Bazel 的大部分优势会逐渐显现。对于每个新增模块,其模式大致相同。
例如,我添加了一个核心模块和一个供其他两个模块(用户和仓库)使用的共享模块。您可以轻松地开始探索依赖关系树。

扩展图示例

由于 Bazel 始终采用 AOT(应用时序)机制,因此请注意,在延迟加载模块时,必须使用工厂模块。
例如:

const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./users/users.ngfactory').then(m => m.UsersModuleNgFactory)
  },
  {
    path: 'repositories',
    loadChildren: () =>
        import('./repositories/repositories.module.ngfactory').then(m => m.RepositoriesModuleNgFactory)
  }
]
Enter fullscreen mode Exit fullscreen mode

仔细观察,你会发现main.dev.ts我们main.prod.ts也在引导工厂模块。

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
Enter fullscreen mode Exit fullscreen mode

既然我们已经了解了如何使用 Bazel,接下来就来谈谈它的优势。
我认为它的大部分优势在拥有大量测试的大型代码库中会更加明显。幸运的是,它的采用可以循序渐进(一个包就能完成所有操作),您可以先创建更具体的包,然后开始声明它们的依赖关系。

您还可以利用远程构建和缓存。由于对于给定的输入集,输出始终相同,因此您可以缓存结果并共享。如果您的持续集成 (CI) 系统已经运行了测试,那么当您在本地进行更改时,可以使用未受影响测试的缓存结果,仅运行受影响的测试。构建过程也是如此,从而缩短构建时间。

我们最初只是从 Angular 应用程序开始,但 Bazel 可以做更多的事情,而且因为它与语言无关,所以你也可以开始引用后端(可能是用另一种语言编写的),并将 Bazel 用作构建工具。

尽管 Bazel 具有可扩展性,但所需的规则已经准备就绪,因此您几乎不需要编写自定义规则。如果您想知道如何编写规则,规则是用Starlark( Python 的一种方言)编写的。

这里介绍的一些功能目前仍属于 Angular Labs 的一部分,将来可能会发生变化。

参考

喜欢这篇文章吗?欢迎访问This Dot Labs了解我们!我们是一家专注于 JavaScript 和前端的技术咨询公司,尤其擅长 Angular、React 和 Vue 等开源软件。

文章来源:https://dev.to/thisdotmedia/angular-bazel-getting-ready-4b0i