原文:Creating Secondary Entry Points for your Angular Library
自从 Angular 库功能(从 Angular 7 开始)发布以来,现在开发 Angular 库比以往任何时候都容易。 Angular 库本身配备了一个名为 ng-packagr 的社区驱动包,它几乎是核心。 在本文中,我们将看看如何利用 ng-packagr 辅助入口点进一步拆分我们的 Angular 库!
Why do we need secondary entry points?
我们希望拥有辅助入口点的原因之一是使我们能够拆分我们的依赖项。 让我们看一个例子,其中一个模块有 peerDependencies,而另一个没有。
假设我们有如下的 library 文件夹结构:
library 名称:my-awesome-lib
两个 module,awesome-plain 和 awesome-time
查看 awesome-plain Component 的实现:
import { Component } from '@angular/core';
@Component({
selector: 'awesome-plain',
template: `
<div>Hey I'm just a plain text with no dependencies!</div>
`
})
export class AwesomePlainComponent {}
以及 awesome-time Component 的实现:
import { Component } from '@angular/core';
import * as moment_ from 'moment';
const moment = moment_;
@Component({
selector: 'awesome-time',
template: `
<div>Hey, Awesome Time:</div>
<div>{{ time }}</div>
`
})
export class AwesomeTimeComponent {
time: string;
constructor() {
this.time = moment().format();
}
}
其中 plain Component 没有任何依赖,而 time Component 依赖于 moment.
moment 依赖的定义在 library 的 package.json 里:
{
"name": "my-awesome-lib",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14",
"moment": "^2.26.0"
}
}
请注意,这些 peerDependencies 放在库 my-awesome-lib 的范围内,而不是放在单个模块(库内的文件)上。
最后,以下是在 my-awesome-lib/src/public-api.ts 下如何导出 awesome-plain 和 awesome-time:
export * from './awesome-plain/awesome-plain.component';
export * from './awesome-plain/awesome-plain.module';
export * from './awesome-time/awesome-time.component';
export * from './awesome-time/awesome-time.module';
The problem: Client needs to install ALL peer dependencies from the library
上面这样设计的问题是什么?
假设我们有一个 Angular 应用程序想要使用 my-awesome-lib。 客户端应用程序(Angular 应用程序)需要做的第一件事是安装库:
npm i my-awesome-lib
安装后,客户端应用程序然后继续导入和使用,例如只有 awesome-plain 组件。 这是客户端应用程序中的代码可能看起来很像:
// app.module.ts
import { AwesomePlainModule } from 'my-awesome-lib';
@NgModule({
...,
imports: [
...,
AwesomePlainModule,
],
bootstrap: [AppComponent]
})
export class AppModule {}
// app.component.html
<awesome-plain></awesome-plain>
然而,ng serve 命令行会导致如下错误:
ERROR in ./node_modules/my-awesome-lib/fesm2015/my-awesome-lib.js Module not found: Error: Can't resolve 'moment' in '/app-showcase-v8/node_modules/my-awesome-lib/fesm2015'
它说它找不到安装在客户端应用程序中的时刻。 嗯,这就是发生的事情。 尽管客户端应用程序仅导入并使用 awesome-plain,但 Angular 编译器仍会要求安装 my-awesome-lib 中定义的所有 peerDependencies,这在本例中很重要。
如果客户端应用程序同时使用 awesome-plain 和 awesome-time,当前情况可能会很好。 然而,想象一下,如果库变大并且有不止 2 个模块,假设有 10 个模块。 让我们再夸大一点; 如果 10 个模块中有 5 个具有不同的 peerDependencies 会怎样? 如果有一个客户端应用程序使用这个库并且只使用 1 个没有任何 peerDependencies 的模块,那么客户端应用程序仍然需要安装所有 5 个 peerDependencies! 当然,应该有比这更好的方法,对吧?
Enter the secondary entry points!
幸运的是,很有可能优化当前的方法。 到目前为止,库中使用的方法仅使用称为主要入口点的东西。 这由 package.json 文件表示,该文件仅存在于 my-awesome-lib/package.json 下,其中定义了整个库的所有 peerDependencies。
通过二级入口点,我们可以进一步将 peerDependencies 拆分到库级别之外; 它使得在库内的文件夹或模块中定义 peerDependencies 变得可行。
例如,通过将 awesome-time 设置为二级入口点,我们可以在子目录中创建另一个 package.json 文件,该文件包含仅适用于 awesome-time 模块的 peerDependencies。
因此,我们不再在库级别定义 peerDependencies; 我们改为在
子目录中定义它们。
此外,辅助入口点使我们能够像下面这样导入库:
// Primary entry points
import { AwesomePlainModule } from 'my-awesome-lib';
// Secondary entry points
import { AwesomeTimeModule } from 'my-awesome-lib/awesome-time';
这样,如果 Client App 只使用 AwesomePlainModule,编译器就不会再要求安装 moment 了!
Implement secondary entry points
希望上面的解释能让大家对我们为什么要使用辅助入口点有一个大致的了解。
好消息是,实现二级入口点相当简单明了,因为 ng-packagr 将在幕后完成大部分工作!
我们将使用 my-awesome-lib 作为以下实施指南的上下文。 在这种情况下,我们将设置 awesome-time 作为次要入口点,而 awesome-plain 将保持原样(仍然是主要入口点)。
(1) Place the folders for secondary entry points directly under the library folder.
根据 ng-packagr 文档,辅助入口点的文件夹布局示例之一是如下所示:
有趣的是,这是@angular/common 包中使用的类似文件夹布局,它以@angular/common 作为主要入口点,而@angular/common/testing 作为次要入口点。
文件夹结构如下:
(2) Create additional package.json and public-api.ts files in secondary entry points folder.
要创建辅助入口点,我们需要告诉 ng-packagr 要查找哪个文件夹。 这可以通过在 /my-awesome-lib/awesome-time 文件夹下创建另一个 package.json 和 public-api.ts 文件来实现,除了主入口点的文件。 仅通过这样做,ng-packagr 将动态发现辅助入口点。
/my-awesome-lib/awesome-time/package.json 的内容可以是:
{
"ngPackage": {
"lib": {
"entryFile": "public-api.ts",
"umdModuleIds": {
"moment": "moment"
}
}
},
"peerDependencies": {
"moment": "^2.26.0"
}
}
请注意,到目前为止,我们将 moment 作为 peerDependencies 放置在这里。 此外,“umdModuleIds”用于在构建库时从 ng-packagr 中删除警告。
以及 /my-awesome-lib/awesome-time/public-api.ts 的内容如下:
/*
* Public API Surface of my-awesome-lib/awesome-time
*/
export * from './awesome-time.component';
export * from './awesome-time.module';
(3) Remove secondary entry points peer dependencies from the main package.json and secondary entry point exported files from the main public-api.ts.
{
"name": "my-awesome-lib",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14"
}
}
moment 包已被删除,因为它已在 /my-awesome-lib/awesome-time/package.json 中定义。
此外,我们将删除在主 my-awesome-lib/src/public-api.ts 中导出的 awesome-time 文件。 该文件现在应该只导出 awesome-plain 文件,如下所示:
/*
* Public API Surface of my-awesome-lib
*/
export * from './awesome-plain/awesome-plain.component';
export * from './awesome-plain/awesome-plain.module';
(4) Build the library.
现在一切都已设置完毕,我们现在可以尝试通过执行命令 ng build my-awesome-lib 来构建库。 如果正确完成,您应该在终端中看到以下内容:
另外,如果打开库构建文件夹 dist/my-awesome-library,文件夹内应该还有一些名为 my-awesome-lib-awesome-time..js 的文件,例如 dist/fesm2015 和 dist/bundles . 如果将它与没有辅助入口点的那个进行比较,构建文件夹通常只包含 my-awesome-lib..js,这是仅针对库本身的构建。
(5) Install and import the library in the Client App.
最后一步是最终在 Angular 应用程序中使用它。 由于我们从主要入口点移动了 awesome-time,因此导入路径会略有变化。 要在客户端应用程序中使用新的库文件夹结构,它应该如下所示:
// Primary entry points
import { AwesomePlainModule } from 'my-awesome-lib';
// Secondary entry points
import { AwesomeTimeModule } from 'my-awesome-lib/awesome-time';
现在,如果客户端应用程序只使用 AwesomePlainModule,我们应该可以在不安装 moment 的情况下运行应用程序(仅在 AwesomeTimeModule 中使用)。
请记住,实施辅助入口点可能会导致您的 Angular 库发生重大变化。 原因是因为使用您的库的客户端应用程序必须更新导入路径。 否则,他们的应用程序将中断,因为现在不再从“your-lib”导入辅助入口点文件。 因此,此更改不向后兼容。
Should there be any primary entry points at all? Is it okay to only have secondary entry points for the library?
您可能想知道,我们甚至应该使用主要入口点吗? 在我看来,只有次要入口点是可以的,主要是因为@angular/material 只使用次要入口点。 另一方面,对于逻辑上相似的功能或特性,一般也建议使用主要入口点。 以下是在 Angular Package Format 文档中编写的:
Angular Package Format 的一般规则是为最小的逻辑连接代码集生成 FESM 文件。 例如,Angular 包有一个用于@angular/core 的 FESM。 当开发人员使用来自@angular/core 的 Component 符号时,他们很可能也会直接或间接使用诸如 Injectable、Directive、NgModule 等符号。 因此,所有这些部分都应该捆绑到一个单一的 FESM 中。 对于大多数库情况,应该将单个逻辑组组合到一个 NgModule 中,并且所有这些文件应该捆绑在一起作为包中的单个 FESM 文件,代表 npm 包中的单个入口点。
此外,就我而言,经验法则是将某些模块作为辅助入口点,如果它们具有不同的 peerDependencies。 这是为了防止客户端应用程序被迫手动安装所有依赖项,尽管它们并未使用所有依赖项。
Conclusions
总而言之,次要入口点是一个巧妙的功能,它允许我们进一步拆分 Angular 库,尤其是在处理 peerDependencies 时。 它也很容易实现,因为 ng-packagr 将通过子目录的 package.json 动态发现辅助入口点。
好处之一是它会减少客户端应用程序被迫安装所有依赖项的机会,即使该应用程序没有导入/使用依赖于已安装依赖项的任何库函数。
更多Jerry的原创文章,尽在:"汪子熙":