CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-203 · Angular 高级教程

Angular 22+ 教程 29:构建自己的 Library

当第二个项目要用到同一个组件时,就该考虑把它抽成库了。Angular CLI 对”库”有一等公民支持:ng generate library 生成工程、ng-packagr 负责打包成符合 Angular Package Format(APF)的产物。本文覆盖从创建到发布 npm/私有 registry 的全流程,以及最容易被忽视的版本策略。

创建库

在现有工作区(ng new 创建的应用仓库)中生成:

ng generate library @acme/ui-kit

生成的目录结构:

projects/acme/ui-kit/
├── ng-package.json        # ng-packagr 配置
├── package.json           # 发布元数据(name/version/peerDependencies)
├── tsconfig.lib.json
├── tsconfig.lib.prod.json
└── src
    ├── public-api.ts      # 库的公开出口
    └── lib/
        ├── ui-kit.component.ts
        ├── ui-kit.service.ts
        └── ...

关键约定:

  • public-api.ts 是唯一的公开面。只有在这里 export 的符号才对使用者可见,内部实现放 src/lib 下随意组织
  • tsconfig.json 里自动添加了 paths 映射,工作区内的应用可以直接 import { Button } from '@acme/ui-kit',无需先构建
// src/public-api.ts
export * from './lib/button/button.component';
export * from './lib/toast/toast.service';
export * from './lib/toast/toast.component';

写一个库组件

库组件就是 Standalone 组件,没有特别魔法。v22 中注意两点:服务用 @Service() 装饰器,组件默认 OnPush:

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'acme-button',
  template: `
    <button type="button" [class.primary]="variant() === 'primary'" (click)="pressed.emit()">
      <ng-content />
    </button>
  `,
})
export class ButtonComponent {
  readonly variant = input<'primary' | 'ghost'>('primary');
  readonly pressed = output<void>();
}
@Service()
export class ToastService {
  // providedIn: 'root' 语义,使用方应用自动获得单例
  show(message: string): void { /* ... */ }
}

设计库时多想一步消费方:组件依赖的 Token 通过 input/inject(PROVIDER, { optional: true }) 注入而不是硬编码,给使用方留配置口子。

secondary entry:拆分入口

所有导出都塞进主入口,使用方 import { Button } from '@acme/ui-kit' 时会把整个库拉进构建图——尽管 tree-shaking 最终会裁剪,但类型检查与编译成本仍在。规范做法是按功能拆 secondary entry point:

projects/acme/ui-kit/
├── ng-package.json
├── src/...                      # 主入口 @acme/ui-kit
├── button/
│   ├── ng-package.json          # 声明这是一个 entry
│   └── index.ts                 # 该入口的 public api
└── toast/
    ├── ng-package.json
    └── index.ts

button/ng-package.json 只需要:

{
  "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json"
}

使用方按需导入:

import { ButtonComponent } from '@acme/ui-kit/button';
import { ToastService } from '@acme/ui-kit/toast';

主入口可以 re-export 高频 API 保持便捷,重组件(如富文本编辑器、图表)单独成入口。Angular 官方包(@angular/cdk/overlay@angular/cdk/drag-drop)就是这个结构。

peerDependencies:声明宿主环境

库与应用最大的区别:库不打包 Angular 自身。Angular 版本由使用方应用决定,库只声明兼容范围:

{
  "name": "@acme/ui-kit",
  "version": "0.1.0",
  "peerDependencies": {
    "@angular/core": "^22.0.0",
    "@angular/common": "^22.0.0",
    "rxjs": "^7.8.0"
  }
}

写法要点:

  • peerDependencies 而不是 dependencies——后者会把 Angular 装两份,DI 与静态替换直接崩
  • 版本范围写实际测试过的组合。只测过 v22 就写 ^22.0.0,别盲目放宽
  • 想同时支持多个 Angular 大版本,用 || 组合(如 ^21.0.0 || ^22.0.0),并保证只用这些版本的公共 API 子集

partial compilation:为什么库能跨版本工作

ng-packagr 默认以 partial compilation 模式产出库代码(ng-package.jsoncompilationMode: "partial" 是默认值)。它把组件装饰器编译成”半成品”中间表示,而不是编译到底:

  • 不完整 AOT:库不做最终的代码生成,留给消费应用的编译器按应用自己的 Angular 版本与编译选项完成编译
  • 跨版本兼容:应用编译器比库编译器新时(官方约定 N 与 N+1 兼容),旧库产物仍能被正确编译
  • 装饰器元数据完整保留:AOT、模板类型检查在应用侧照常工作

这是 APF 的核心机制之一,也是”为什么不要用 tsc 直接编译 Angular 库”的答案——手工编译很难产出正确的 partial 产物。

构建与本地验证

ng build @acme/ui-kit

产物输出在 dist/acme/ui-kit,是标准的 npm 包结构(含 fesm2022、类型声明、package.json 的 exports 字段等)。开发期验证有两种:

ng build @acme/ui-kit --watch

方式一:watch 模式,配合应用内的 paths 映射直接调试源码。

cd dist/acme/ui-kit && npm pack

方式二:npm pack 生成 tgz 安装包,在真实消费环境里安装验证。

发布前务必在”干净的应用”里消费一次构建产物(不是 paths 映射到源码),确认 exports、类型、tree-shaking 都符合预期——很多问题只在真实安装后暴露。

发布到 npm 与私有 registry

公开包直接发布:

cd dist/acme/ui-kit
npm publish --access public   # scoped 包首次发布需 public

企业内私有包通过 .npmrc 指向内部 registry:

; 项目根 .npmrc(.npmrc 的 ini 注释用分号)
@acme:registry=https://npm.internal.acme.com/
//npm.internal.acme.com/:_authToken=${NPM_TOKEN}

也可以在库的 package.json 里用 publishConfig 固定发布目标,避免每次敲 registry 参数:

{
  "publishConfig": {
    "registry": "https://npm.internal.acme.com/"
  }
}

版本策略

库一旦被多个应用依赖,版本就是 API 承诺:

  • 遵循 semver:破坏性改动升 major、新能力升 minor、修复升 patch
  • 破坏性改动的判定以使用方视角为准:改组件 input 名、改导出函数签名、改 CSS 结构(使用方可能覆写)都算 major
  • Angular 官方约定库与应用编译器保持 N/N+1 兼容,所以跟随 Angular 大版本升级你的库并升 major,是最清晰的节奏——使用方能从版本号直接推断”需要 Angular 几”
  • 变更日志写清楚迁移步骤;如果库带 schematics(ng update 时的自动迁移),重大改动配套迁移脚本会大幅降低使用方升级成本
  • 发布前跑一遍库的单元测试(ng test @acme/ui-kit),CI 里锁定”构建 + 测试 + 干净消费验证”三道闸

系列导航

← 算法 029:算法思想:二分法 目录 JHipster 开发 29:踩坑全集——我们交过的学费 →
← 返回文章列表