手把手教你搭建自己的Angular组件库 - DevUI
<p>摘要:DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导<code>沉浸</code>、<code>灵活</code>、<code>至简</code>的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠、取悦眼球的设计。如果你正在开发 <code>ToB</code> 的<code>工具类产品</code>,DevUI 将是一个很不错的选择!</p><p> </p>
<blockquote>DevUI是一支兼具设计视角和工程视角的团队,服务于华为云<span style="text-decoration: underline">DevCloud</span>平台和华为内部数个中后台系统,服务于设计师和前端工程师。<br>官方网站:<span style="text-decoration: underline">devui.design<br></span>Ng组件库:<span style="text-decoration: underline">ng-devui</span>(欢迎Star)</blockquote>
<h1>引言</h1>
<p>作为前端开发者,随着公司业务的不断发展和增长,业务对组件功能、交互的诉求会越来越多,不同产品或者团队之间公用的组件也会越来越多,这时候就需要有一套用于支撑内部使用的组件库,也可以是基于已有组件扩展或者封装一些原生三方库。本文会手把手教你搭建自己的Angular组件库。</p>
<h1>创建组件库</h1>
<p>我们首先创建一个Angular项目,用来管理组件的展示和发布,用以下命令生成一个新的项目</p>
<div class="cnblogs_code">
<pre>ng <span style="color: rgba(0, 0, 255, 1)">new</span> <my-project></pre>
</div>
<p> </p>
<p><code class="juejin-editor-code"><br></code> </p>
<p>项目初始化完成后,进入到项目下运行以下cli命令初始化lib目录和配置, 生成一个组件库骨架</p>
<div class="cnblogs_code">
<pre>ng generate library <my-lib> --prefix <my-prefix></pre>
</div>
<p> </p>
<p><code class="juejin-editor-code"><br></code> </p>
<p><code class="juejin-editor-code">my-lib</code>为自己指定的library名称,比如devui,<code class="juejin-editor-code">my-prefix</code>为组件和指令前缀,比如d-xxx,默认生成的目录结构如下</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210ae52597f091?w=346&h=292&f=png&s=11629"></p>
<p>angular.json配置文件中也可以看到projects下面多出了一段项目类型为library的配置</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">my-lib</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">: {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projectType</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">library</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">root</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">sourceRoot</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib/src</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">prefix</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">dev</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">architect</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">: {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">build</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">: {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">builder</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">@angular-devkit/build-ng-packagr:build</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">options</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">: {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">tsConfig</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib/tsconfig.lib.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">project</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib/ng-package.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
},
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">configurations</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">: {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">production</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">: {
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">tsConfig</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib/tsconfig.lib.prod.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
}
}
},
...</span></pre>
</div>
<p> </p>
<h1>关键配置修改</h1>
<h2>目录布局调整</h2>
<p>从目录结构可以看出默认生成的目录结构比较深,参考<code class="juejin-editor-code">material design</code>,我们对目录结构进行自定义修改如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210ae6b447fdee?w=270&h=288&f=png&s=11786"></p>
<p>修改说明:</p>
<ul>
<li>删除了my-lib目录下的src目录,把src目录下的test.ts拷贝出来,组件库测试文件入口</li>
<li>把组件平铺到my-lib目录下,并在my-lib目录下新增my-lib.module.ts(用于管理组件的导入、导出)和index.ts(导出my-lib.module.ts,简化导入)</li>
<li>修改angular.json中my-lib下面的<code class="juejin-editor-code">sourceRoot</code>路径,指向my-lib即可</li>
</ul>
<p>修改如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> my-lib.module.ts</span>
<span style="color: rgba(0, 0, 0, 1)">
import { NgModule } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">@angular/core</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import { CommonModule } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">@angular/common</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import { AlertModule } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">my-lib/alert</span><span style="color: rgba(128, 0, 0, 1)">'</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 此处按照按需引入方式导入,my-lib对应我们的发布库名</span>
<span style="color: rgba(0, 0, 0, 1)">
@NgModule({
imports: [ CommonModule ],
exports: ,
providers: [],
})
export </span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> MyLibModule {}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> index.ts</span>
export * <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./my-lib.module</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">angular.json</span>
<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projectType</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">library</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">root</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">sourceRoot</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">projects/my-lib</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这里路径指向我们新的目录</span>
<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">prefix</span><span style="color: rgba(128, 0, 0, 1)">"</span>: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">devui</span><span style="color: rgba(128, 0, 0, 1)">"</span></pre>
</div>
<p> </p>
<h2>库构建关键配置</h2>
<p><code class="juejin-editor-code">ng-package.json</code>配置文件,angular library构建时依赖的配置文件</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
</span>"$schema": "../../node_modules/ng-packagr/ng-package.schema.json"<span style="color: rgba(0, 0, 0, 1)">,
</span>"dest": "../../publish"<span style="color: rgba(0, 0, 0, 1)">,
</span>"lib"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"entryFile": "./index.ts"<span style="color: rgba(0, 0, 0, 1)">
},
</span>"whitelistedNonPeerDependencies": ["lodash-es"<span style="color: rgba(0, 0, 0, 1)">]
}</span></pre>
</div>
<p> </p>
<p>关键配置说明:</p>
<ul>
<li>dest,lib构建输出路径,这里我们修改为publish目录,和项目构建dist目录区分开</li>
<li>lib/entryFile,指定库构建入口文件,此处指向我们上文的index.ts</li>
</ul>
<p>whitelistedNonPeerDependencies(可选),如果组件库依赖了第三方库,比如lodash,需要在此处配置白名单,因为<code class="juejin-editor-code">ng-packagr</code>构建时为了避免第三方依赖库可能存在多版本冲突的风险,会检查package.json的<code class="juejin-editor-code">dependencies</code>依赖配置,如果不配置白名单,存在<code class="juejin-editor-code">dependencies</code>配置时就会构建失败。</p>
<p><code class="juejin-editor-code">package.json</code>配置,建议尽量使用peerDependcies,如果业务也配置了相关依赖项的话</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
</span>"name": "my-lib"<span style="color: rgba(0, 0, 0, 1)">,
</span>"version": "0.0.1"<span style="color: rgba(0, 0, 0, 1)">,
</span>"peerDependencies"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"@angular/common": "^9.1.6"<span style="color: rgba(0, 0, 0, 1)">,
</span>"@angular/core": "^9.1.6"<span style="color: rgba(0, 0, 0, 1)">,
</span>"tslib": "^1.10.0"<span style="color: rgba(0, 0, 0, 1)">
}
}</span></pre>
</div>
<p> </p>
<p>详细完整的配置,可以参考angular官方文档 <span style="text-decoration: underline">https://github.com/ng-packagr/ng-packagr/blob/master/docs/DESIGN.md</span></p>
<h1>开发一个Alert组件</h1>
<h2>组件功能介绍</h2>
<p>我们参考DevUI组件库的<span style="text-decoration: underline">alert组件</span>开发一个组件,用来测试我们的组件库,alert组件主要是根据用户传入的类型呈现不同的颜色和图标,用于向用户显示不同的警告信息。视觉显示如下</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210ae87975bee3?w=338&h=115&f=png&s=2634"></p>
<h2>组件结构分解</h2>
<p>首先,我们看一下alert组件目录包含哪些文件</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210ae9ebe7f16f?w=286&h=197&f=png&s=9169"></p>
<p>目录结构说明:</p>
<ul>
<li>组件是一个完整的module(和普通业务模块一样),并包含了一个单元测试文件</li>
<li>组件目录下有一个package.json,用于支持二级入口(单个组件支持按需引入)</li>
<li>public-api.ts用于导出module、组件、service等,是对外暴露的入口,index.ts会导出public-api,方便其它模块</li>
</ul>
<p>关键内容如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> package.json</span>
<span style="color: rgba(0, 0, 0, 1)">{
</span>"ngPackage"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"lib"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"entryFile": "public-api.ts"<span style="color: rgba(0, 0, 0, 1)">
}
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">public-api.ts</span><span style="color: rgba(0, 128, 0, 1)">
/*</span><span style="color: rgba(0, 128, 0, 1)">
* Public API Surface of Alert
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
export </span>* from './alert.component'<span style="color: rgba(0, 0, 0, 1)">;
export </span>* from './alert.module';</pre>
</div>
<p> </p>
<h2>定义输入输出</h2>
<p>接下来我们就开始实现组件,首先我们定义一下组件的输入输出,alert内容我们采用投影的方式传入,Input参数支持指定alert类型、是否显示图标、alert是否可关闭,Output返回关闭回调,用于使用者处理关闭后的逻辑</p>
<div class="cnblogs_code">
<pre>import { Component, Input } from '@angular/core'<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义alert有哪些可选类型</span>
export type AlertType = 'success' | 'danger' | 'warning' | 'info'<span style="color: rgba(0, 0, 0, 1)">;
@Component({
selector: </span>'dev-alert'<span style="color: rgba(0, 0, 0, 1)">,
templateUrl: </span>'./alert.component.html'<span style="color: rgba(0, 0, 0, 1)">,
styleUrls: [</span>'./alert.component.scss'<span style="color: rgba(0, 0, 0, 1)">],
})
export class AlertComponent {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Alert 类型</span>
@Input() type: AlertType = 'info'<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 是否显示图标,用于支持用户自定义图标</span>
@Input() showIcon = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 是否可关闭</span>
@Input() closeable = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 关闭回调</span>
@Output() closeEvent: EventEmitter<<span style="color: rgba(0, 0, 255, 1)">boolean</span>> = <span style="color: rgba(0, 0, 255, 1)">new</span> EventEmitter<<span style="color: rgba(0, 0, 255, 1)">boolean</span>><span style="color: rgba(0, 0, 0, 1)">();
hide </span>= <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
constructor() {}
close(){
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.closeEvent.emit(<span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.hide = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
}
}</span></pre>
</div>
<p> </p>
<h2>定义布局</h2>
<p>根据api定义和视觉显示我们来实现页面布局结构,布局包含一个关闭按钮、图标占位和内容投影 ,组件关闭时,我们采用清空dom的方式处理。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="dev-alert {{ type }} "</span><span style="color: rgba(255, 0, 0, 1)"> *ngIf</span><span style="color: rgba(0, 0, 255, 1)">="!hide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="button"</span><span style="color: rgba(255, 0, 0, 1)"> class</span><span style="color: rgba(0, 0, 255, 1)">="dev-close"</span><span style="color: rgba(255, 0, 0, 1)"> (click)</span><span style="color: rgba(0, 0, 255, 1)">="close()"</span><span style="color: rgba(255, 0, 0, 1)"> *ngIf</span><span style="color: rgba(0, 0, 255, 1)">="closeable"</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">span </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="dev-alert-icon icon-{{ type }}"</span><span style="color: rgba(255, 0, 0, 1)"> *ngIf</span><span style="color: rgba(0, 0, 255, 1)">="showIcon"</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">ng-content</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">ng-content</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p> </p>
<p>到这里,我们组件的页面布局和组件逻辑已经封装完成,根据视觉显示再加上对应的样式处理就开发完成了。</p>
<h1>测试Alert组件</h1>
<h2>开发态引用组件</h2>
<p>组件开发过程中,我们需要能够实时调试逻辑和调整UI展示,打开根目录下的tsconfig.json,修改一下paths路径映射,方便我们在开发态就可以本地调试我们的组件,这里直接把my-lib指向了组件源码,当然也可以通过<code class="juejin-editor-code">ng build my-lib --watch</code>来使用默认的配置, 指向构建好的预发布文件,此时这里就要配置成我们修改过的目录<code class="juejin-editor-code">public/my-lib/*</code></p>
<div class="cnblogs_code">
<pre>"paths"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"my-lib"<span style="color: rgba(0, 0, 0, 1)">: [
</span>"projects/my-lib/index.ts"<span style="color: rgba(0, 0, 0, 1)">
],
</span>"my-lib/*"<span style="color: rgba(0, 0, 0, 1)">: [
</span>"projects/my-lib/*"<span style="color: rgba(0, 0, 0, 1)">
],
}</span></pre>
</div>
<p> </p>
<p>配置完成后,就可以在应用中按照npm的方式使用我们正在开发的库了,我们在app.module.ts中先导入我们的正在开发的组件,这里可以从my-lib.module导入全部组件,或者直接导入我们的AlertModule(前面已经配置支持二级入口)</p>
<div class="cnblogs_code">
<pre>import { AlertModule } from 'my-lib/alert'<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> import { MyLibModule } from 'my-lib';</span>
<span style="color: rgba(0, 0, 0, 1)">
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> MyLibModule</span>
<span style="color: rgba(0, 0, 0, 1)"> AlertModule
],
providers: [],
bootstrap:
})
export class AppModule { }</span></pre>
</div>
<p> </p>
<p>此时在app.component.html页面中就可以直接使用我们正在开发的alert组件了</p>
<div class="cnblogs_code">
<pre><section>
<dev-alert>我是一个默认类型的alert</dev-alert>
</section></pre>
</div>
<p> </p>
<p>打开页面,就可以看到当前开发的效果,这时候我们就可以根据页面表现来调整样式和交互逻辑,此处就不继续展示了</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210aebda99c016?w=510&h=57&f=png&s=2183"></p>
<h2>编写单元测试</h2>
<p>前面提到我们有一个单元测试文件,组件开发为了保证代码的质量和后续重构组件的稳定性,在开发组件的时候,有条件的建议加上单元测试。</p>
<p>由于我们调整了目录结构,我们先修改一下相关配置</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> angular.json</span>
"my-lib"<span style="color: rgba(0, 0, 0, 1)">: {
...
</span>"test"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"builder": "@angular-devkit/build-angular:karma"<span style="color: rgba(0, 0, 0, 1)">,
</span>"options"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"main": "projects/my-lib/test.ts", <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这里指向调整后的文件路径</span>
"tsConfig": "projects/my-lib/tsconfig.spec.json"<span style="color: rgba(0, 0, 0, 1)">,
</span>"karmaConfig": "projects/my-lib/karma.conf.js"<span style="color: rgba(0, 0, 0, 1)">
}
},
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">my-lib 目录下的tsconfig.spec.json</span>
"files"<span style="color: rgba(0, 0, 0, 1)">: [
</span>"test.ts" <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 指向当前目录下的测试入口文件</span>
]</pre>
</div>
<p> </p>
<p>下面是一个简单的测试参考,只简单测试了<code class="juejin-editor-code">type</code>类型是否正确,直接测试文件中定义了要测试的组件,场景较多的时候建议提供demo,直接使用demo进行不同场景的测试。</p>
<div class="cnblogs_code">
<pre>import { async, ComponentFixture, TestBed } from '@angular/core/testing'<span style="color: rgba(0, 0, 0, 1)">;
import { Component } from </span>'@angular/core'<span style="color: rgba(0, 0, 0, 1)">;
import { AlertModule } from </span>'./alert.module'<span style="color: rgba(0, 0, 0, 1)">;
import { AlertComponent } from </span>'./alert.component'<span style="color: rgba(0, 0, 0, 1)">;
import { By } from </span>'@angular/platform-browser'<span style="color: rgba(0, 0, 0, 1)">;
@Component({
template: `
</span><dev-alert ="type" = "showIcon"="closeable" (closeEvent)="handleClose($event)">
<span>我是一个Alert组件</span>
</dev-alert>
<span style="color: rgba(0, 0, 0, 1)">`
})
class TestAlertComponent {
type </span>= 'info'<span style="color: rgba(0, 0, 0, 1)">;
showIcon </span>= <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
closeable </span>= <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
clickCount </span>= 0<span style="color: rgba(0, 0, 0, 1)">;
handleClose(value) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.clickCount++<span style="color: rgba(0, 0, 0, 1)">;
}
}
describe(</span>'AlertComponent', () =><span style="color: rgba(0, 0, 0, 1)"> {
let component: TestAlertComponent;
let fixture: ComponentFixture</span><TestAlertComponent><span style="color: rgba(0, 0, 0, 1)">;
let alertElement: HTMLElement;
beforeEach(async(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
TestBed.configureTestingModule({
imports: ,
declarations: [ TestAlertComponent ]
})
.compileComponents();
}));
beforeEach(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
fixture </span>=<span style="color: rgba(0, 0, 0, 1)"> TestBed.createComponent(TestAlertComponent);
component </span>=<span style="color: rgba(0, 0, 0, 1)"> fixture.componentInstance;
alertElement </span>=<span style="color: rgba(0, 0, 0, 1)"> fixture.debugElement.query(By.directive(AlertComponent)).nativeElement;
fixture.detectChanges();
});
describe(</span>'alert instance test', () =><span style="color: rgba(0, 0, 0, 1)"> {
it(</span>'should create', () =><span style="color: rgba(0, 0, 0, 1)"> {
expect(component).toBeTruthy();
});
});
describe(</span>'alert type test', () =><span style="color: rgba(0, 0, 0, 1)"> {
it(</span>'Alert should has info type', () =><span style="color: rgba(0, 0, 0, 1)"> {
expect(alertElement.querySelector(</span>'.info')).not.toBe(<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">);
});
it(</span>'Alert should has success type', () =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 修改type,判断类型改变是否正确</span>
component.type = 'success'<span style="color: rgba(0, 0, 0, 1)">;
fixture.detectChanges();
expect(alertElement.querySelector(</span>'.success')).not.toBe(<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">);
});
}</span></pre>
</div>
<p> </p>
<p>通过执行<code class="juejin-editor-code"> ng test my-lib</code>就可以执行单元测试了,默认会打开一个窗口展示我们的测试结果</p>
<p>到这一步,组件开发态引用、测试就完成了,功能和交互没有问题的话,就可以准备发布到npm了。</p>
<p>更多测试内容参考官方介绍:<span style="text-decoration: underline">https://angular.cn/guide/testing</span></p>
<h1>发布组件</h1>
<p>组件开发完成后,单元测试也满足我们定义的门禁指标,就可以准备发布到npm提供给其他同学使用了。</p>
<p>首先我们构建组件库,由于ng9之后默认使用ivy引擎。官方并不建议把 Ivy 格式的库发布到 NPM 仓库。因此在发布到 NPM 之前,我们使用 <code class="juejin-editor-code">--prod</code> 标志构建它,此标志会使用老的编译器和运行时,也就是视图引擎(View Engine),以代替 Ivy。</p>
<div class="cnblogs_code">
<pre>ng build my-lib --prod</pre>
</div>
<p> </p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210aed9dd68e22?w=556&h=230&f=png&s=12460"></p>
<p>构建成功后,就可以着手发布组件库了,这里以发布到npm官方仓库为例</p>
<ol>
<li>如果还没有npm账号,请到<span style="text-decoration: underline">官网网站</span>注册一个账号,选用public类型的免费账号就可以</li>
<li>已有账号,先确认配置的registry是否指向npm官方registry <span style="text-decoration: underline">https://registry.npmjs.org/</span></li>
<li>在终端中执行<code class="juejin-editor-code">npm login</code>登录已注册的用户</li>
</ol>
<p>准备工作都完成后,进入构建目录,这里是publish目录,然后执行<code class="juejin-editor-code"> npm publish --access public</code>就可以发布了,注意我们的库名需要是在npm上没有被占用的,名字的修改在my-lib目录下的package.json中修改。</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/14/17210af15af59ff6?w=480&h=151&f=png&s=13670"></p>
<p>
npm发布参考: <span style="text-decoration: underline">https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry</span></p>
<div>
<p><span style="text-decoration: underline"> </span></p>
如果是内部私有库,按照私有库的要求配置registry就可以了,发布命令都是一样的。
<div>
<p> </p>
<h1>加入我们</h1>
</div>
<div> </div>
<p>我们是<span style="text-decoration: underline">DevUI团队</span>,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。</p>
<div>文/DevUI June</div>
<div>
<p> </p>
<p>往期文章推荐</p>
《2021 年最值得推荐的 7 个 Angular 前端组件库 - DevUI》</div>
<div>《手把手教你使用Rollup打包并发布自己的工具库》</div>
<div>《html2canvas实现浏览器截图的原理(包含源码分析的通用方法)》<br>
<p> </p>
</div>
<p> </p>
</div>
<p><br><br></p><br><br>
来源:https://www.cnblogs.com/kagol/p/14702441.html
頁:
[1]