对于如何创建和发布新库,以扩展 Angular 的功能,本页面提供了一个概念性的总览
如果你发现自己要在多个应用中解决同样的问题(或者要把你的解决方案分享给其它开发者),你就有了一个潜在的库。简单的例子就是一个用来把用户带到你公司网站上的按钮,该按钮会包含在你公司构建的所有应用中。
使用 Angular CLI,用以下命令在新的工作区中生成一个新库的骨架:
ng new my-workspace --no-create-application
cd my-workspace
ng generate library my-lib
命名你的库
如果你想稍后在公共包注册表(比如 npm)中发布它,则在选择库名称时应该非常小心。
避免使用以 ng-
为前缀的名称,比如 ng-library
。ng-
前缀是 Angular 框架及其库中使用的保留关键字。首选 ngx-
前缀作为用于表示该库适合与 Angular 一起使用的约定。这也是注册表的使用者区分不同 JavaScript 框架库的优秀指示器。
ng generate
命令会在你的工作区中创建 projects/my-lib
文件夹,其中包含带有一个组件和一个服务的 NgModule。
可以使用单一仓库(monorepo)模式将同一个工作区用于多个项目。
当你生成一个新库时,该工作区的配置文件 angular.json
中也增加了一个 'library' 类型的项目。
"projects": {
…
"my-lib": {
"root": "projects/my-lib",
"sourceRoot": "projects/my-lib/src",
"projectType": "library",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
…
可以使用 CLI 命令来构建、测试和 lint 这个项目:
ng build my-lib --configuration development
ng test my-lib
ng lint my-lib
注意,该项目配置的构建器与应用类项目的默认构建器不同。此构建器可以确保库永远使用 AoT 编译器构建。
要让库代码可以复用,你必须为它定义一个公共的 API。这个“用户层”定义了库中消费者的可用内容。该库的用户应该可以通过单个的导入路径来访问公共功能(如 NgModules、服务提供者和工具函数)。
库的公共 API 是在库文件夹下的 public-api.ts
文件中维护的。当你的库被导入应用时,从该文件导出的所有内容都会公开。请使用 NgModule 来暴露这些服务和组件。
你的库里应该提供一些文档(通常是 README 文件)来指导别人安装和维护。
为了让你的解决方案可供复用,你需要对它进行调整,以免它依赖应用特有的代码。在将应用的功能迁移到库中时,需要注意以下几点。
forRoot()
和 forChild()
设计模式提供RouterModule
一个库通常都包含可复用的代码,用于定义组件,服务,以及你刚才导入到项目中的其他 Angular 工件(管道,指令等等)。库被打包成一个 npm 包,用于发布和共享。这个包还可以包含一些原理图,它提供直接在项目中生成或转换代码的指令,就像 CLI 用 ng generate component
创建一个通用的新 component
。比如,用库打包的原理图可以为 Angular CLI 提供生成组件所需的信息,该组件用于配置和使用该库中定义的特定特性或一组特性。这方面的一个例子是 Angular Material 的导航原理图,它用来配置 CDK 的 BreakpointObserver并把它与 Material 的 MatSideNav和 MatToolbar组件一起使用。
创建并包含以下几种原理图。
ng add
可以把你的库添加到项目中。ng generate
可以为项目中的已定义工件(组件,服务,测试等)提供支持。ng update
可以更新你的库的依赖,并提供一些迁移来破坏新版本中的更改。你的库中所包含的内容取决于你的任务。比如,你可以定义一个原理图来创建一个预先填充了固定数据的下拉列表,以展示如何把它添加到一个应用中。如果你想要一个每次包含不同传入值的下拉列表,那么你的库可以定义一个原理图来用指定的配置创建它。然后,开发人员可以使用 ng generate
为自己的应用配置一个实例。
假设你要读取配置文件,然后根据该配置生成表单。如果该表单需要库的用户进行额外的自定义,它可能最适合用作 schematic。但是,如果这些表单总是一样的,开发人员不需要做太多自定义工作,那么你就可以创建一个动态的组件来获取配置并生成表单。通常,自定义越复杂,schematic 方式就越有用。
使用 Angular CLI 和 npm 包管理器来构建你的库并发布为 npm 包。
Angular CLI 使用一个名为 ng-packagr
的工具从已编译的代码中创建可以发布到 npm 的软件包。
你应该总是使用 production
配置来构建用于分发的库。这样可以确保所生成的输出对 npm 使用了适当的优化和正确的软件包格式。
ng build my-lib
cd dist/my-lib
npm publish
对于 Angular 库,可分发文件中可包含一些额外的资产,如主题文件、Sass mixins 或文档(如变更日志)。欲知详情,请参见在构建时将资产复制到库中和将资产嵌入到组件样式中。
当包含额外的资产(如 Sass mixins 或预编译的 CSS)时,你需要将这些手动添加到主入口点的 package.json
中的条件化 "exports"
部分。
ng-packagr
会将手写的 "exports"
与自动生成的 "exports"
合并,以便让库作者配置额外的导出子路径或自定义条件。
"exports": {
".": {
"sass": "./_index.scss",
},
"./theming": {
"sass": "./_theming.scss"
},
"./prebuilt-themes/indigo-pink.css": {
"style": "./prebuilt-themes/indigo-pink.css"
}
}
以上是 @angular/material可分发文件的摘录。
各种 Angular 库应该把自己依赖的所有 @angular/*
都列为同级依赖。这确保了当各个模块请求 Angular 时,都会得到完全相同的模块。如果某个库在 dependencies
列出 @angular/core
而不是用 peerDependencies
,它可能会得到一个不同的 Angular 模块,这会破坏你的应用。
如果要在同一个工作空间中使用某个库,你不必把它发布到 npm 包管理器,但你还是得先构建它。
要想在应用中使用你自己的库:
ng build my-lib在你的应用中,按名字从库中导入:
import { myExport } from 'my-lib';
如果你没有把库发布为 npm 包,然后把它从 npm 安装到你的应用中,那么构建步骤就是必要的。比如,如果你克隆了 git 仓库并运行了 npm install
,编辑器就会把 my-lib
的导入显示为缺失状态(如果你还没有构建过该库)。
当你在 Angular 应用中从某个库导入一些东西时,Angular 就会寻找库名和磁盘上某个位置之间的映射关系。当你用 npm 包安装该库时,它就映射到 node_modules
目录下。当你自己构建库时,它就会在 tsconfig
路径中查找这个映射。
用 Angular CLI 生成库时,会自动把它的路径添加到 tsconfig
文件中。Angular CLI 使用 tsconfig
路径告诉构建系统在哪里寻找这个库。
欲知详情,参见路径映射概览。
如果你发现库中的更改没有反映到应用中,那么你的应用很可能正在使用这个库的旧版本。
每当你对它进行修改时,都可以重建你的库,但这个额外的步骤需要时间。增量构建功能可以改善库的开发体验。每当文件发生变化时,都会执行局部构建,并修补一些文件。
增量构建可以作为开发环境中的后台进程运行。要启用这个特性,可以在构建命令中加入 --watch
标志:
ng build my-lib --watch
CLI 的 build
命令为库使用与应用不同的构建器,并调用不同的构建工具。
@angular-devkit/build-angular
)基于 webpack
,并被包含在所有新的 Angular CLI 项目中。ng-packagr
。只有在使用 ng generate library my-lib
添加库时,它才会添加到依赖项中。这两种构建体系支持不同的东西,即使它们支持相同的东西,它们的执行方式也不同。这意味着同一套 TypeScript 源码在生成库时生成的 JavaScript 代码可能与生成应用时生成的 JavaScript 代码也不同。
因此,依赖于库的应用应该只使用指向内置库的 TypeScript 路径映射。TypeScript 的路径映射不应该指向库的 .ts
源文件。
发布库时可以使用两种分发格式:
分发格式 | 详情 |
---|---|
部分 Ivy(推荐) | 包含可移植代码,从 v12 开始,使用任何版本的 Angular 构建的 Ivy 应用都可以使用这些可移植代码。 |
完全 Ivy | 包含专用的 Angular Ivy 指令,不能保证它们可在 Angular 的不同版本中使用。这种格式要求库和应用使用完全相同的 Angular 版本构建。这种格式对于直接从源代码构建所有库和应用代码的环境很有用。 |
对于发布到 npm 的库,请使用 partial-Ivy 格式,因为它在 Angular 的各个补丁版本之间是稳定的。
如果要发布到 npm,请避免使用完全 Ivy 的方式编译库,因为生成的 Ivy 指令不属于 Angular 公共 API 的一部分,因此在补丁版本之间可能会有所不同。
用于构建应用的 Angular 版本应始终与用于构建其任何依赖库的 Angular 版本相同或更大。比如,如果你有一个使用 Angular 13 版的库,则依赖于该库的应用应该使用 Angular 13 版或更高版本。Angular 不支持为该应用使用早期版本。
如果打算将库发布到 npm,请通过在 tsconfig.prod.json
的 "compilationMode": "partial"
来使用部分 Ivy 代码进行编译。这种部分格式在不同版本的 Angular 之间是稳定的,因此可以安全地发布到 npm。这种格式的代码在应用程序构建期间会使用相同版本的 Angular 编译器进行处理,以确保应用程序及其所有库使用的是同一个版本的 Angular。
如果要发布到 npm,请避免使用完全 Ivy 代码来编译库,因为生成的 Ivy 指令不属于 Angular 公共 API 的一部分,因此在补丁版本之间可能会有所不同。
如果你以前从未在 npm 中发布过软件包,则必须创建一个用户帐户。在发布 npm 程序包中了解更多信息。
应用将 npm 中的许多 Angular 库安装到其 node_modules
目录中。但是,这些库中的代码不能与已编译的应用直接捆绑在一起,因为它尚未完全编译。要完成编译,可以使用 Angular 链接器。
对于不使用 Angular CLI 的应用程序,此链接器可用作 Babel插件。该插件要从 @angular/compiler-cli/linker/babel
导入。
Angular 链接器的 Babel 插件支持构建缓存,这意味着链接器只需一次处理库,而与其他 npm 操作无关。
下面的例子借助 babel-loader把此链接器注册为 Babel 插件,从而将此插件集成到自定义 Webpack 构建中。
import linkerPlugin from '@angular/compiler-cli/linker/babel';
export default {
// ...
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [linkerPlugin],
compact: false,
cacheDirectory: true,
}
}
}
]
}
// ...
}
Angular CLI 自动集成了链接器插件,因此,如果你这个库的使用方也在使用 CLI,则他们可以从 npm 安装 Ivy 原生库,而无需任何其他配置。