Angular Component库:权威指南

网上有很多关于创建Angular组件和的文章,那么创建另一个组件和库又有什么意义呢?
好吧,在跟踪了网上几乎所有与此主题相关的公开信息源之后,我得出的结论是,它们都是不完整的并且是肤浅的,因此我永远无法在他们的文章之后创建可用于生产的库。 然后,我决定创建这篇文章,希望可以帮助将来的开发人员在遇到同样的问题时遇到困难。

毫无疑问,Angular CLI是可用于构建Angular应用程序的最佳工具之一。 在创建应用程序时,它是一个功能强大的工具,但是当您需要创建可重用的模块甚至更糟的组件库时,它并没有真正的帮助。

好消息! CLI团队已经意识到了这一点,并且已经在研究使用Angular CLI开发组件库的官方解决方案。 但是,与此同时,我们需要一个计划B。

我的解决方案需要2个独立的Angular CLI项目和一个包含我的可发行组件库的lib文件夹的组合。

正如前面已经提到的,我们在项目中需要的是1个用于开发库的Angular-CLI项目,1个包含库本身的文件夹和1个额外的Angular CLI项目,这有两个原因:创建文档应用程序并在外部内部测试我们的库用于模拟我们库的实际实现的应用程序。
实际上,我们的目标是释放一个准备好在任何Angular应用程序中安装的库,因此,确保nrwl.io服务于我们的库的唯一方法是将其安装在外部Angular应用程序中,并确保同时使用两者JiT和AoT编译。

以下屏幕截图代表了我理想的库项目结构。
根文件夹是我引导开发Angular-CLI应用程序的地方,我的库位于该文件的源文件夹中。
在doc文件夹中安装了第二个Angular-CLI应用程序,但是我们现在可以忽略它。

从https://angular.io/guide/aot-compiler

更快的渲染
使用AOT,浏览器将下载该应用程序的预编译版本。 浏览器加载可执行代码,因此它可以立即呈现应用程序,而无需等待首先编译应用程序。

异步请求更少
编译器在应用程序JavaScript中内外部HTML模板和CSS样式表,从而消除了对那些源文件的单独ajax请求。

较小的Angular框架下载大小
如果应用程序已经编译,则无需下载Angular编译器。 编译器大约是Angular本身的一半,因此省略编译器会大大减少应用程序的有效负载。

尽早检测模板错误
在用户看到它们之前,AOT编译器会在构建步骤中检测并报告模板绑定错误。

更好的安全性
AOT很早就将HTML模板和组件编译为JavaScript文件,然后才提供给客户端。 没有要读取的模板,也没有风险的客户端HTML或JavaScript评估,因此注入攻击的机会更少。

因为我们需要确保我们的库同时支持AoT和JiT,所以在编写任何代码之前需要澄清一些想法。 艾萨克·曼(Isaac Mann)的一篇好文章在这里提供。 强烈建议阅读。

我认为这是要记住的最重要的要点:

从模板访问的任何成员必须是公共的

  @零件({ 
模板:`
{{context}}
`
})
导出类SomeComponent {
私人环境:任何;
}

这将适用于即时编译,但会破坏AoT。
私有上下文变量需要更改为public而不是private。

明确导出组件

错误:

意外错误:模块“ AppModule”导入了意外值“ undefined”

之前:

export * from 'some-component';

后:

export { SomeComponent } from 'some-component';

peerDependency

这是另一个好点,因为我注意到某些开源库的package.json不包含任何peerDependencies。 当将库安装在具有不同软件包版本的项目中时,这可能导致多个问题。

请查看此NodeJS文章以获取更多信息。

假设您已经在机器上全局安装了Angular CLI,请使用ng new angular-component-library创建一个新的Angular-CLI项目(如果没有,请遵循官方文档)。

现在,我们有了开发应用程序,该应用程序将用于开发和测试我们的组件库。

首先,我们不会创建Angular模块,而是创建一个包含模块集合的库。 这意味着我们将有几个独立的模块一起放在同一个盒子中,并准备在任何Angular应用程序中使用。

这意味着我们的lib文件夹将只导出库的所有不同的公用文件夹,并且每个文件夹都包含一个或多个模块。

我们的lib文件结构将类似于以下内容:

 . 
├── lib
├── module-a

├── components


├── component-a



├── component-a.component.html



├── component-a.component.css



├── component-a.component.ts



├── component-a.component.spec.ts



└── index.ts


└── index.ts

├── module-a.module.ts

└── index.ts
├── module-b

└──
...
└── index.ts

位于lib根目录下的index.ts文件将负责导出项目中的所有公共模块,并且将类似于以下内容:

 从'./module-a/index'导出*; 
从'./module-b/index'导出*;
从'./module-c/index'导出*;
从'./shared/index'导出*;

然后,位于每个模块文件夹内的主要index.ts文件将导出模块本身的所有不同元素:

  // lib / module-a / index.ts 
从“ ./services/index”导出{ComponentAService};
从“ ./components/index”导出{ComponentAComponent};
从'./component-a.module'导出{ComponentAModule};

注意到所有那些/index吗? 好吧,事实证明,当我们想保留使用export关键字的桶的优势时,需要指定它们。 如果丢失,类型定义将在使用Typescript编译器(AKA tsc )的库打包过程中受到损害。 网上有很多有关此主题的讨论和问题,我花了很长时间才弄清楚。

我可以分享在Typescript存储库上报告的此问题作为示例:https://github.com/Microsoft/TypeScript/issues/12974

现在我们可以开始编写我们的组件库,并使用Angular-CLI开发项目来查看它们的运行情况,以便我们可以受益于脚手架工具来生成我们的模块,组件等,并实时重载以监听开发应用程序和我们的库坐在里面。

如果我们可以从开发应用程序内部引用我们的库,就像它是位于node_module文件夹中的真实包一样,那将是node_module 。 因此,让我们开始吧!

从打字稿文档中:

TypeScript编译器使用tsconfig.json文件中的"paths"属性来支持此类映射的声明。 这是有关如何为jquery指定"paths"属性的示例。

  { 
“ compilerOptions”:{
“ baseUrl”:“。”,//如果是“ paths”,则必须指定。
“路径”:{
“ jquery”:[“ node_modules / jquery / dist / jquery”] //此映射相对于“ baseUrl”
}
}
}

有关更多信息,请参阅以下官方文档:https://www.typescriptlang.org/docs/handbook/module-resolution.html

使用打字稿配置文件,我们可以创建一个到我们库的映射,以在我们的开发应用程序中使用,而无需指定其相对路径。

因此,让我们编辑位于应用程序src文件夹内的tsconfig.app.json文件,并添加以下“路径”节点:

  { 
“ compilerOptions”:{
...
“路径”:{
“ @ my / lib”:[“ lib / public_api.ts”]
}
}
}

请注意,我们不需要在tsconfig.spec.ts包含相同的路径,因为我们不会在开发应用程序中进行单元测试,而是所有规范文件仅位于lib文件夹中。

现在,我们可以像下面的示例一样简单地在开发应用程序中引用@my/lib

 从'@ angular / core'导入{NgModule}; 
从'@ angular / common'导入{CommonModule};
从'./accordion.routing'导入{RoutingModule};
从'./accordion.component'导入{AccordionPageComponent};
从'@ my / lib'导入{AccordionModule}; //'@ my / lib'可以替换为'../../../lib'
@NgModule({
声明:[AccordionPageComponent],
进口:[
通用模块
手风琴模块
路由模块
]
})
导出类PageModule {}

为了打包我的库,我将使用ng-packgr。 这是生成具有零(或非常接近)配置的生产就绪Angular库的好工具。 Ng-packgr遵循Angular Package Format指南以及Jason Aden(Google的库开发人员)建议的最佳实践。 观看NG-Cong 2017中有关包装角度的精彩视频。

ng-packgr需要1个配置文件和1个npm依赖项。

  npm i -D ng-packagr 

然后使用软件包脚本更新您的package.json文件:

  { 
“脚本”:{
“ package”:“ ng-packagr -p src / lib / ng-package.json”
}
}

从cli -p参数中选择配置,然后从ng-package.json

src/lib/ng-package.json我们需要编写以下配置:

  { 
“ $ schema”:“ ../../ node_modules / ng-packagr / ng-package.schema.json”,
“ dest”:“ ../../package”,
“ lib”:{
“ entryFile”:“ public_api.ts”
}
}

默认条目文件设置为public_api.ts 。 我们可以在lib文件夹中创建index.ts文件,也可以将index.ts设置为新的入口文件。

我们的库也需要package.json 。 从理论上讲,我们可以回收位于根文件夹中的文件,但由于包含了沙箱应用程序的所有依赖关系,因此充满了不必要的软件包。 当我们提供一个npm软件包时,我们只是希望它依赖于它自己的依赖关系,而不要保留不需要的软件包。 另外,由于我们将发布Angular库,因此我们需要指定一些对等依赖项以避免冲突。

然后,为我们的库提供一个干净的package.json的最佳方法是在我们的lib文件夹中创建一个全新的lib ,以用于库本身的发行版。

我在根文件夹中创建了一个非常基本的bash脚本,以动态替换src/lib/目录中package.tpl.json文件中的某些占位符。 这样,我可以使软件包名称和版本与我的主package.json文件自动同步,而无需在每次发布较新版本的库时都两次替换该值,然后我创建了一个预打包脚本,由npm启动执行我的脚本。

package.json

  { 
“脚本”:{
“ prepackage”:“ ./predeploy.sh”
}
}

predeploy.sh

  #!/ bin / bashPACKAGE_VERSION = $(grep -m1版本package.json 
awk -F:'{打印$ 2}'
sed's / [“,] // g')
PACKAGE_NAME = $(grep -m1名称package.json
awk -F:'{打印$ 2}'
sed's / [“,] // g')
sed -e“ s / {版本} / $ {PACKAGE_VERSION} /” -e“ s#{pkg-name}#$ {PACKAGE_NAME}#” src / lib / package.tpl.json> src / lib / package.json

src / lib / package.tmp.json

  { 
“ name”:“ {pkg-name}”,
“ version”:“ {version}”,
“ peerDependencies”:{
“ @ angular / core”:“ ^ 4.0.0
^ 5.0.0“,
“ @ angular / common”:“ ^ 4.0.0
^ 5.0.0“
},
“依赖关系”:{
“ @ angular / cdk”:“ ^ 2.0.0-beta.12”,
}
}

现在,运行npm run package将生成一个package文件夹,其中包含具有正确依赖性的库的package.json。

现在我们的库已准备就绪,可以“上线”了,我们可以谈谈我之前提到的doc文件夹。

我说过,我们将使用另一个Angular-CLI项目来测试我们的实际库并提供库文档。

在我的doc文件夹中,我引导了Angular-CLI项目,并删除了所有测试工具和逻辑,这意味着我删除了e2e文件夹以及与该文档应用程序相关的所有业力和量角器代码,因为我们的库已经过单元和e2e测试从src文件夹中。 该项目的目的是向我们的库提供文档,因此不需要对其进行测试,但是它将作为对我们的库的验收测试,以确保最终用户将能够在他们的项目中使用我们的创作。

为此,有2种可能的方法(实际上是3种方法,因为您也可以创建该程序包的符号链接,但是这可能会导致其他有害的副作用,因此在本文中我不再赘述。)

  • 将生成的库部署到npm存储库,并将其安装在我们的doc应用程序中。
  • 第二个也是我更喜欢的一个,是打包生成的库分发版本,并使用npm将其安装在doc应用程序内部。

我们可以在生成的软件包分发版本中使用npm pack ,以生成准备与npm一起安装的tarball文件,就像使用npm install的普通npm软件包一样。

我在package.json中定义了一个doc脚本,以自动执行该过程,并在其中注入了最新版本的库后为doc应用程序提供服务。

  “脚本”:{ 
“ doc:clean”:“ rimraf doc / package-lock.json doc / node_modules”,
“ prepack-lib”:“ npm运行包”,
“ pack-lib”:“ cd软件包&& npm软件包”,
“ predoc:serve”:“ npm run pack-lib && npm run doc:clean”,
“ doc:serve”:“ cd doc && npm i && npm i ../package/*.tgz --no-save && npm start”
}

我在这里所做的是生成库的新发行版本,打包所有内容并将生成的tgz文件安装在我的doc应用程序中。 请注意,我指定了--no-save ,这是因为我不想用我的库更新doc的package.json依赖项,因为我想在每次运行文档项目以确保始终保持手动安装时进行手动安装使用生成的库的最新版本。
我也在用
rimraf用于清理所有已安装的节点模块以及Node> 8自动生成的package-lock.json文件。 我将rimraf用作rm -rf而不是rm -rf安装在我的项目中,因为它会自动递归删除所有文件和文件夹,并且不会抱怨目标文件是否不存在。

另外,我在文档应用程序顶部使用了Compodoc,以利用类型定义的力量自动为我的库生成API文档。 此处提供了一个很好的结合Angular和Compodoc的文档示例。

我正在做的是将Compodoc安装为项目的开发依赖项,并在doc文件夹中生成文档项目。

  “ predoc:build”:“ cd doc && npm i && npm i ../package/*.tgz --no-save && npm build --prod”, 
“ doc:build”:“ compodoc -p src / lib / tsconfig-compodoc.json -d doc / docs”

必须使用tsconfig-compodoc.json来指定要用于生成文档的文件。
我也在用另一种
tsconfig文件,因为我不想记录开发应用程序,而只希望记录其中的库。

我真的希望本指南能够指导您的库开发过程,并回答有关该过程的所有问题。