Angular 包格式
Angular 包格式
本文档描述了 Angular 包格式 (APF)。APF 是针对 npm 包结构和格式的 Angular 专用规范,所有第一方 Angular 包(@angular/core
、 @angular/material
等)和大多数第三方 Angular 库都使用了该规范。
APF 能让包在使用 Angular 的大多数常见场景下无缝工作。使用 APF 的包与 Angular 团队提供的工具以及更广泛的 JavaScript 生态系统兼容。建议第三方库开发者也都遵循这种格式。
APF 与 Angular 的其余部分一起进行版本控制,每个主要版本都改进了包格式。你可以在此 google doc 中找到 v13 之前版本的规范。
为什么要指定包格式?
在当今的 JavaScript 环境中,开发人员将使用多种不同的工具链(Webpack、rollup、esbuild 等)以多种不同的方式使用包。这些工具可能理解并需要不同的输入 —— 一些工具能处理最新的 ES 语言版本,而其他工具也许要直接使用较旧的 ES 版本。
这种 Angular 分发格式支持所有常用的开发工具和工作流,并着重于优化,从而缩小应用程序有效负载大小或缩短开发迭代周期(构建时间)。
开发人员可以依靠 Angular CLI 和 ng-packagr(Angular CLI 使用的构建工具)来生成 APF 格式的包。
文件布局
以下示例显示了 @angular/core
包文件布局的简化版本,并附有对包中每个文件的解释。
此表描述了 node_modules/@angular/core
下的文件布局,注释为描述文件和目录的用途:
文件 |
用途 |
---|---|
README.md
|
包 README,由 npmjs web UI 使用。 |
package.json
|
主要的 |
index.d.ts
|
主入口点 |
|
未展平的 ES2020 格式的 |
esm2020/testing/
|
未扁平化的 ES2020 格式的 |
fesm2015/
─ core.mjs
─ core.mjs.map
─ testing.mjs
─ testing.mjs.map
|
扁平化 (FESM) ES2015 格式的所有入口点的代码,以及源码映射。 |
fesm2020/
─ core.mjs
─ core.mjs.map
─ testing.mjs
─ testing.mjs.map
|
扁平化 (FESM) ES2020 格式的所有入口点的代码,以及源码映射。 |
testing/
|
代表 |
testing/index.d.ts
|
为 |
package.json
主 package.json
包含重要的包元数据,包括以下内容:
- 它把此包声明为 EcmaScript 模块 (ESM) 格式
- 它包含一个
"exports"
字段,用于定义所有入口点的可用源码格式 - 它包含定义主入口点
@angular/core
的可用源码格式的一些键,供不理解 "exports"
的工具使用。这些键已弃用,随着对 "exports"
的支持在整个生态系统中逐步退出,这些键将被删除。 - 它声明此包是否包含副作用
ESM 声明
顶级 package.json
包含此键:
{
"type": "module"
}
这会通知解析器,此包中的代码正在使用 EcmaScript 模块而不是 CommonJS 模块。
"exports"
"exports"
字段具有以下结构:
"exports": {
"./schematics/*": {
"default": "./schematics/*.js"
},
"./package.json": {
"default": "./package.json"
},
".": {
"types": "./core.d.ts",
"esm2020": "./esm2020/core.mjs",
"es2020": "./fesm2020/core.mjs",
"es2015": "./fesm2015/core.mjs",
"node": "./fesm2015/core.mjs",
"default": "./fesm2020/core.mjs"
},
"./testing": {
"types": "./testing/testing.d.ts",
"esm2020": "./esm2020/testing/testing.mjs",
"es2020": "./fesm2020/testing.mjs",
"es2015": "./fesm2015/testing.mjs",
"node": "./fesm2015/testing.mjs",
"default": "./fesm2020/testing.mjs"
}
}
主要看 "."
和 "./testing"
这两个键,它们分别定义了 @angular/core
主要入口点和 @angular/core/testing
次要入口点的可用代码格式。对于每个入口点,可用的格式为:
格式 |
详情 |
---|---|
类型定义( |
TypeScript 在依赖于给定包时使用 |
es2020
|
已展平为单个源文件的 ES2020 代码。 |
es2015
|
已展平为单个源文件的 ES2015 代码。 |
esm2020
|
未展平的源文件中的 ES2020 代码 |
认识这些键的工具可以优先从 "exports"
中选择所需的代码格式。其余 2 个键控制工具的默认行为:
-
"node"
在 Node.js 中加载包时选择扁平化的 ES2015 代码。
使用这种格式是由于 zone.js
的要求,因为它不支持原生的 async
/ await
ES2017 语法。因此,指示 Node 使用 ES2015 代码,其中 async
/ await
结构已降级为 Promises。
-
"default"
为所有其他消费者选择扁平化的 ES2020 代码。
库可能希望公开其他静态文件,这些文件没有被基于 JavaScript 的入口点(比如 Sass mixins 或预编译的 CSS)的导出所捕获。
旧版解析键
除了 "exports"
之外,顶级 package.json
还为不支持 "exports"
的解析器定义了旧模块解析键。对于 @angular/core
,这些是:
{
"fesm2020": "./fesm2020/core.mjs",
"fesm2015": "./fesm2015/core.mjs",
"esm2020": "./esm2020/core.mjs",
"typings": "./core.d.ts",
"module": "./fesm2015/core.mjs",
"es2020": "./fesm2020/core.mjs",
}
如上,模块解析器可以用这些键来加载特定的代码格式。
注意:
与 "default"
不同,"module"
是为 Node 以及任何未配置为使用特定键的工具选择的格式。它基本和 "node"
一样,但由于 ZoneJS 的限制,选择了 ES2015 代码。
副作用
package.json
的最后一个功能是声明此包是否有副作用。
{
"sideEffects": false
}
大多数 Angular 包不应该依赖于顶级副作用,因此应该包含这个声明。
入口点和代码拆分
APF 中的包,包含一个主要入口点和零到多个次要入口点(比如 @angular/common/http
)。入口点有多种功能。
- 它们定义了用户要从中导入代码的模块说明符(比如,
@angular/core
和 @angular/core/testing
)。 - 它们定义了可以惰性加载代码的粒度。
用户通常将这些入口点视为具有不同用途或功能的不同符号组。
特定入口点可能仅用于特殊目的,比如测试。此类 API 可以与主入口点分离,以减少它们被意外或错误使用的机会。
许多现代构建工具只能在 ES 模块级别进行“代码拆分”(又名惰性加载)。由于 APF 主要为每个入口点使用一个“扁平” ES 模块,这意味着大多数构建工具无法将单个入口点中的代码拆分为多个输出块。
APF 包的一般规则是为尽可能小的逻辑相关代码集使用入口点。比如,Angular Material 包将每个逻辑组件或一组组件作为单独的入口点发布 - 一个用于按钮,一个用于选项卡等。如果需要,这允许单独惰性加载每个 Material 组件。
并非所有库都需要这样的粒度。大多数具有单一逻辑目的的库应该作为单一入口点发布。比如 @angular/core
为运行时使用单个入口点,因为 Angular 运行时通常用作单个实体。
次要入口点的解析
可以通过包的 package.json
的 "exports"
字段解析辅助入口点。
自述文件
markdown 格式的自述文件,用于在 npm 和 github 上显示包的描述。
@angular/core
包的示例自述内容:
Angular
=======
The sources for this package are in the main [Angular](https://github.com/angular/angular) repo.Please file issues and pull requests against that repo.
License: MIT
部分编译
APF 格式的库必须以“部分编译”模式发布。这是 ngc
的一种编译模式,它生成不依赖于特定 Angular 运行时版本的已编译 Angular 代码,与用于应用程序的完整编译形成对比,其中 Angular 编译器和运行时版本必须完全匹配。
要部分编译 Angular 代码,请在 tsconfig.json
中的 "angularCompilerOptions"
中使用 "compilationMode"
标志:
{
…
"angularCompilerOptions": {
"compilationMode": "partial",
}
}
然后,在应用程序构建过程中,Angular CLI 将部分编译的库代码转换为完全编译的代码。
优化
ES 模块的扁平化
APF 指定代码要以“扁平化”的 ES 模块格式发布。这显著减少了 Angular 应用程序的构建时间以及最终应用程序包的下载和解析时间。请查看 Nolan Lawson 发表的优秀文章“小模块的成本”。
Angular 编译器支持生成索引 ES 模块文件,然后可以让这些文件借助 Rollup 等工具生成扁平化模块,从而生成我们称为扁平化 ES 模块或 FESM 的文件格式。
FESM 是一种文件格式,它会将所有可从入口点访问的 ES 模块扁平化为单个 ES 模块。它是通过跟踪包中的所有导入并将该代码复制到单个文件中而生成的,同时保留所有公共 ES 导出并删除所有私有导入。
缩写名称“FESM”(发音为“phesom”)后面可以有一个数字,比如“FESM5”或“FESM2015”。数字是指模块内 JavaScript 的语言级别。所以 FESM5 文件将是 ESM+ES5(导入/导出语句和 ES5 源代码)。
要生成扁平化的 ES 模块索引文件,请在 tsconfig.json 文件中使用以下配置选项:
{
"compilerOptions": {
…
"module": "esnext",
"target": "es2020",
…
},
"angularCompilerOptions": {
…
"flatModuleOutFile": "my-ui-lib.js",
"flatModuleId": "my-ui-lib"
}
}
一旦 ngc 生成了索引文件(比如 my-ui-lib.js
),打包器和优化器(如 Rollup)就可用于生成扁平化的 ESM 文件。
注意 package.json 中的默认值
从 webpack v4 开始,对于 webpack 用户来说,ES 模块优化的扁平化应该不是必需的,其实理论上我们应该能够在不扁平化 webpack 中的模块的情况下获得更好的代码拆分。在实践中,当使用非扁平化模块作为 webpack v4 的输入时,我们仍然会看到大小增加了。这就是为什么 package.json 中的 "module"
和 "es2020"
条目仍然指向 fesm 文件的原因。我们正在调查此问题,并希望在解决大小回归问题后将 package.json 中的 "module"
和 "es2020"
入口点切换到未扁平化的文件。APF 目前包含未扁平化的 ESM2020 代码,目的是验证此类未来的更改。
“副作用”标志
默认情况下,EcmaScript 模块是有副作用的:从模块导入可确保该模块顶层的任何代码都将执行。这通常是不可取的,因为典型模块中的大多数副作用代码并不是真正的副作用,而是仅影响特定符号。如果没有导入和使用这些符号,通常需要在称为 tree-shaking 的优化过程中将它们删除,而副作用代码可以防止这种情况发生。
诸如 Webpack 之类的构建工具支持一个标志,该标志允许包声明它们并不依赖于其模块顶层的副作用代码,从而使工具可以更自由地对包中的代码进行摇树优化。这些优化的最终结果应该是较小的包大小和代码拆分后包块中更好的代码分布。如果此优化包含非本地副作用,则此优化可能会破坏你的代码 - 然而,这在 Angular 应用程序中并不常见,并且通常是糟糕设计的标志。我们的建议是让所有包通过将 sideEffects 属性设置为 false 来声明无副作用状态,并且让开发人员遵循 Angular 风格指南,这自然会导致代码没有非本地副作用。
更多信息:关于副作用的 webpack 文档
ES2020 语言级别
ES2020 语言级别现在是 Angular CLI 和其他工具使用的默认语言级别。Angular CLI 会将捆绑包降级到所有目标浏览器在应用程序构建时都支持的语言级别。
d.ts 捆绑/类型定义的扁平化
从 APF v8 开始,我们现在更喜欢运行 API Extractor 来打包 TypeScript 定义,以便整个 API 都出现在单个文件中。
在之前的 APF 版本中,每个入口点都会在 .d.ts 入口点旁边有一个 src
目录,该目录包含与原始源代码结构匹配的单个 d.ts 文件。虽然这种分发格式仍然被允许和支持,但非常不鼓励它,因为它会弄晕 IDE 之类的工具,然后提供错误的自动完成,并允许用户依赖深度导入的路径,这些路径通常不被认为是库或包的公共 API。
库
从 APF v10 开始,我们建议添加 tslib 作为主要入口点的直接依赖项。这是因为 tslib 版本与用来编译库的 TypeScript 版本相关联。
术语定义
本文档中特意使用了以下术语。在本节中,我们定义所有这些以便更清晰。
包
发布到 NPM 并一起安装的最小文件集,比如 @angular/core
。该包中包含一个名为 package.json 的清单、编译后的源代码、TypeScript 定义文件、源码映射、元数据等。该包是通过 npm install @angular/core
安装的。
符号
包含在模块中的类、函数、常量或变量,可选择通过模块导出,以便对外界可见。
模块
ECMAScript 模块的缩写。包含导入和导出符号的语句的文件。这与 ECMAScript 规范中模块的定义相同。
ESM
ECMAScript 模块的缩写(见上文)。
FESM
Flattened ES Modules 的缩写,由一种文件格式组成,该文件格式是通过将所有可从入口点访问的 ES 模块扁平化为单个 ES 模块而创建的。
模块标识
导入语句中使用的模块的标识符(比如 @angular/core
)。此 ID 通常直接映射到文件系统上的路径,但由于有各种模块解析策略,情况也并非总是如此。
模块说明符
模块标识符(见上文)。
模块解析策略
用于将模块 ID 转换为文件系统路径的算法。Node.js 就有一个良好定义且广泛使用的,TypeScript 支持多种模块解析策略,Closure Compiler 还有另一种策略。
模块格式
模块语法规范,至少涵盖用于从文件导入和导出的语法。常见的模块格式是 CommonJS(CJS,通常用于 Node.js 应用程序)或 ECMAScript 模块(ESM)。模块格式仅表示单个模块的封装,而不表示用于构成模块内容的 JavaScript 语言特性。正因为如此,Angular 团队经常使用语言级别说明符作为模块格式的后缀,比如 ESM+ES2015 指定模块为 ESM 格式并包含降级到 ES2015 的代码。
捆绑包
单个 JS 文件形式的工件,由构建工具(比如 Webpack或Rollup)生成,其中包含源自一个或多个模块的符号。捆绑包是一种浏览器专用的解决方案,可减少浏览器开始下载数百甚至数万个文件时可能造成的网络压力。Node.js 通常不使用捆绑包。常见的捆绑包格式是 UMD 和 System.register。
语言级别
代码的语言(ES2015 或 ES2020)。独立于模块格式。
入口点
旨在由用户导入的模块。它由唯一的模块 ID 引用,并导出该模块 ID 引用的公共 API。一个例子是 @angular/core
或 @angular/core/testing
。@angular/core
包中存在两个入口点,但它们导出不同的符号。一个包可以有许多入口点。
深度导入
从不是入口点的模块中检索符号的过程。这些模块 ID 通常被认为是私有 API,它们可以在项目的生命周期内或在创建给定包的捆绑包时更改。
顶级导入
来自入口点的导入。可用的顶级导入定义了公共 API,并在“@angular/name”模块中公开,比如 @angular/core
或 @angular/common
。
摇树优化
识别和删除应用程序中未使用的代码的过程 - 也称为死代码消除。这是使用 Rollup 、 Closure Compiler 或 Terser 等工具在应用程序级别执行的全局优化。
AOT 编译器
Angular 的预先编译器。
扁平类型定义
从 API Extractor 生成的捆绑 TypeScript 定义。