阅读(1284) (1)

Angular 创建库

2022-07-12 17:36:17 更新

创建库

对于如何创建和发布新库,以扩展 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 文件)来指导别人安装和维护。

把应用中的部分内容重构成一个库

为了让你的解决方案可供复用,你需要对它进行调整,以免它依赖应用特有的代码。在将应用的功能迁移到库中时,需要注意以下几点。

  • 组件和管道之类的可声明对象应该设计成无状态的,这意味着它们不依赖或修改外部变量。如果确实依赖于状态,就需要对每种情况进行评估,以决定它是应用的状态还是库要管理的状态。
  • 组件内部订阅的所有可观察对象都应该在这些组件的生命周期内进行清理和释放
  • 组件对外暴露交互方式时,应该通过输入参数来提供上下文,通过输出参数来将事件传给其它组件
  • 检查所有内部依赖。
    • 对于在组件或服务中使用的自定义类或接口,检查它们是否依赖于其它类或接口,它们也需要一起迁移
    • 同样,如果你的库代码依赖于某个服务,则需要迁移该服务
    • 如果你的库代码或其模板依赖于其它库(比如 Angular Material),你就必须把它们配置为该库的依赖
  • 考虑如何为客户端应用提供服务。
    • 服务应该自己声明提供者(而不是在 NgModule 或组件中声明提供者),以便它们是可摇树优化的。这样,如果服务器从未被注入到导入该库的应用中,编译器就会把该服务从该 bundle 中删除。
    • 如果你在多个 NgModules 注册全局服务提供者或提供者共享,使用​forRoot()​ 和 ​forChild()​ 设计模式提供​RouterModule
    • 如果你的库中提供的可选服务可能并没有被所有的客户端应用所使用,那么就可以通过轻量级令牌设计模式为这种情况支持正确的树状结构

使用代码生成原理图与 CLI 集成

一个库通常都包含可复用的代码,用于定义组件,服务,以及你刚才导入到项目中的其他 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

管理库中的资产(assets)

对于 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 程序包中了解更多信息。

在 Angular CLI 之外使用部分 Ivy 代码

应用将 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 原生库,而无需任何其他配置。