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"
]
)
当引用另一个包时,我们将使用双斜杠 ( //) 进入工作区的根目录,然后从那里进入所需的包。
another_package_rule(
name = "some_name",
deps = [
"//foo/bar:a_rule"
]
)
如果规则名称与包名称匹配,则可以省略冒号后面的规则名称。
引用等于//foo/bar:bar号//foo/bar。
Bazel 和 Angular
Bazel 与语言和框架无关,但我首先想看看它如何与 Angular 一起使用,并了解其工作原理。
让我们安装所需的依赖项并创建一个新项目。
npm install -g @angular/bazel
ng new --collection=@angular/bazel
此命令将创建一个新项目,并已将 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
构建完成后,我们将能够浏览和/或修改生成的文件。
我们先从……开始/src/BUILD.bazel。我会把文件分成几个部分,以便于解释。
package(default_visibility = ["//visibility:public"])
最顶层是可见性属性。可见性属性决定了其他包是否能够访问此包中的规则。可见性的作用域可以设置。在本例中,我们将其设置为公开。
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")
然后是所有我们将要用到的导入语句,它们都会通过 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"],
),
)
每条规则的开头都有它的名称。这样我们就可以在其他地方引用它,例如,将其作为依赖项添加到另一条规则中。
第一条规则名为 `<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",
],
)
首先,我们将规则名称与目录名称相同,这样便于后续引用。将主规则名称与包目录名称相同是一种良好的实践。接下来,我们设置源文件(所有 .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",
],
)
最后一部分将设置和配置我们的打包器、开发服务器、生产构建和测试。
使用 Bazel 时,所有依赖项都已声明,输入和输出也已明确。这使我们能够分析系统运行状况并构建有向图。我们可以使用 `query` 命令查询任何包、规则或仓库及其依赖项。`query` 命令功能强大,可以提出各种请求,并且还可以获取不同的输出类型。我建议您查阅相关文档。我们还会使用一个工具将查询图的结果转换为图像。
// get al the packages in the workspace
bazel query --output=graph ... | dot -Tpng > graph.png
信息量似乎有点大。幸运的是,我们可以选择图表中的部分内容。
bazel query "kind(rule, allpaths(//src:devserver, //...))" --output=graph | dot -Tpng > devserver.png
让我们开始利用现有资源优化和定制我们的构建。首先,在这个项目中,我决定不使用任何 .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",
],
)
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",
],
)
我认为,随着工作区规模的扩大,使用 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)
}
]
仔细观察,你会发现main.dev.ts我们main.prod.ts也在引导工厂模块。
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
既然我们已经了解了如何使用 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



