Angular接入库项目

694 阅读11分钟

1. 前言

Angular CLI 7.0.0 增加了脚手架配置项:--create-application,其中默认值是true,如果不设置,则在新工作空间的src文件夹中创建一个新的初始应用程序项目。如果为false,则创建一个没有初始应用程序的空工作区。想了解更多配置项,点击这里

2. 创建库

第1步:创建库工作空间

Angular CLI 7.0.0的键入以下命令:

ng new foo-lib --create-application=false

这个时候我们会看到项目文件中的一些变化:

  • package.json
    • angular需要的所有常用依赖项
  • angular.json
    • Angular配置文件,但没有项目
  • README.md、tsconfig.json、tslint.json、node_modules
    • 基本和我们的构建初始化项目的内容结构一致

因为我们使用--create-application=false创建的应用,所以目录中是没有src目录的。

第2步:初始化库项目

键入以下命令创建Lib项目:

cd foo-lib
ng generate library foo-lib --prefix=foo

其中--prefix指令是用于初始项目的时候生成选择器(ng genreate)的前缀。详细配置项请看前言部分的超链接。如果你不指定,默认是lib。

执行完命令之后,我们发现项目中多了一个project文件夹,里边有个Library工程:foo-lib

第3步:创建库测试项目(一定要创建,否则无法运行)

我们需要一个可以用来调用我们的Angular库的项目,键入以下命令:

ng generate application foo-tester

执行完命令之后,我们可以看到,project文件下又多出了一个文件夹:foo-tester,即我们的测试项目。另外,Angular CLI还添加了一个foo-tester-e2e项目,用于端到端测试。对于不写测试用例.spec的强迫症患者拯救大心丸:--minimal=true。

第4步:开发和测试

Codeing...

第5步:构建打包

Angular CLI从6.1开始,始终在生产模式下构建库,因此我们不使用--prod,只需键入以下命令:

ng build foo-lib
提醒:

如果想构建自己的测试项目则键入以下命令:

ng build foo-tester --prod

和构建Library库不一样的是,构建测试应用必须指定:--prod。

如果想启动自己的测试项目,则键入以下命令:

ng serve foo-tester

如果想测试自己的Library,则键入以下命令:

ng test foo-lib

如果想测试自己的测试项目,则键入以下命令:

ng test foo-tester

第6步:发布我们自己的库

如果想发布到npm,则需注册一个自己的npm账号,如果已经有了且已经登录,则键入以下命令:

cd dist/foo-lib
npm publish

第7步:使用我们的库

和其他第三方包一样,只需要npm install你的自己发布的Library包即可,项目根目录终端键入以下命令:

npm i -S foo-lib

这个时候你会看到你的项目package.json中的dependencies依赖项中增加了一项:foo-lib。然后在Angular模块中引入即可。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FooLibModule } from 'foo-lib'; // 导入你的Library

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FooLibModule // 导入你的Library
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

第8步:最后的惊喜,如何维护自己的库

npm发布版本有些注意事项,每次构建发布需要注意以下规则:

// 1.npm插件发布
npm addUser  // 分别输入用户名、密码、邮箱
npm publish  // 直接发布
npm login    // 非第一次发版本则用此命令
npm unpublish --force // 取消插件发布【谨慎使用】
npm deprecate <pkg>[@<version>] <message> // 并不会在社区里撤销你已有的包,但会在任何人尝试安装这个包的时候得到警告
npx force-unpublish package-name '原因描述' // 撤销不了??试试这个

// 2.npm插件更新
npm version patch  // 补丁【1.0.1】
npm version minor  // 小改【1.1.0】
npm version major  // 大改【2.0.0】
                   // 注意需要再一次执行:npm publish

// 3.查看远程包版本信息
npm view xxx versions

// 4.npm查看本地全局安装过的包
npm list -g --depth=0

// 5.npm查看全局的包的安装路径
npm root -g

// 6.npm查看当前包的安装路径
npm root

// 7.npm将包安装到全局环境中
npm install xxx -g

// 8.npm将信息写入package.json,并自动把模块和版本号添加到dependencies部分
npm install xxx –save
npm i -S xxx // 简写版本

// 9.npm将信息写入package.json,并自动把模块和版本号添加到devdependencies部分
npm install xxx –save-dve
npm i -D xxx // 简写版本

// 10.npm单独更新某个包
npm update xxx

// 11.npm更新至最新版
npm install -g npm

// 12.npm淘宝镜像
npm config set registry http://registry.npm.taobao.org

将库提交代码至 gitLab/github仓库

// 第一种: 从命令行创建新的存储库
echo "# init project" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin xxx.git
git push -u origin master


第二种:从命令行推送现有存储库
git remote add origin xxx.git
git push -u origin master

安装我们私有的库

使用的方式有两种:sshhttp

  • ssh
npm i -S git+ssh://git@xxx.git
  • http
npm i -S git+http://xxx.git

温馨提示:我们开发中可能需要安装特定的某个分支或者tag或者某次提交的版本,命令是: npm i -S xxx.git#<tag|branch|commit>

更多npm install信息请手动点击这里查阅官方文档:官方文档

3. 使用我们私有的库

引入的方式很多,常用的是通过引入module的方式,而我们工作中平时都是封装一个附带路由的大模块,因此懒加载的方式更适合我们。但是这还是不够,由于安全以及其他业务上的关联模块之间可能会有一些数据依赖:例如token。所以使用路由插座router-outlet的方式,插座提供了两个回调:即实例化新组件时,路由器插座会发出激活activate事件,以及销毁组件时的停用deactivate事件。灵感来源于此文章,以下以我们真实的项目作为例子:

  • 第一步:创建Library载体
// 1. 创建一个component组件作为Lib的载体,使用router-outlet的方式引入Lib库,且配置配置相应的信息和注册必要的Lib库Event回调
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
export const ENV_TITLE: string = environment.title;

@Component({
    selector: 'app-monitor-platform-outlet',
    template: `
        <!-- 路由插座作为Library库载体 -->
        <router-outlet (activate)="onActivate($event)" (deactivate)="onDeactivate()"></router-outlet>
    `,
    styles: [``]
})
export class LibOutletComponent implements OnInit {
    cloudCall: Subscription;
    classDetail: Subscription;

    constructor() { }

    ngOnInit(): void { }

    // 组件激活
    onActivate(componentReference) {
        // 1. @Input() baseInfo【object】为Lib库需要的基础信息,请严格按照格式传入!
        componentReference.baseInfo = {
            env: ENV_TITLE,//用来判断运行环境,或者直接赋值给window在库中获取
            token: 'xxxx'
        };
        // 2. @Output cloudCallClicked【function】事件回调
        if (componentReference.cloudCallClicked) {
            this.cloudCall = componentReference.cloudCallClicked.subscribe((data) => {
                console.log('点击云呼click回调信息: ', data);
            });
        }
    }

    // 组件销毁
    onDeactivate() {
        if (this.cloudCall) {
            this.cloudCall.unsubscribe();
        }
    }

}

  • 第二步:创建容器

    创建容器的原因呢是为了解决CLI编译的问题,详细的可以查看angular-cli github issess

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooModule } from 'foo-lib/dist'; // 我们的库模块

@NgModule({
    declarations: [],
    imports: [
        CommonModule,
        FooModule // 我们库模块
    ]
})
export class LibContainerModule { }
  • 第三步:配置路由懒加载Library模块

    我们以创建的载体作为主路由,Lib库作为子路由挂载。angular8里的懒加载子路由有变动,需要格外注意,angular8的具体变动信息可以查阅官方博客

const routes: Routes = [
    {
        path: 'xxx', component: LibOutletComponent, children: [
            /* 这里的loadChildren路径为LibContainerModule容器的相对路径 */
            { path: '', loadChildren: './lib-outlet/lib-container.module#LibContainerModule' }
        ]
    }
];
  • 第四步:导入样式

    我们的Library难免会有自己的全局样式,这个时候我们可以在Lib库里创建一个assets文件夹,在assets文件夹下创建一个全局的样式文件golbal.scss。使用者使用我们的Lib库的时候只需要导入我们Lib库的这个全局样式即可,如下:

/* Lib库样式,必须引入 */
@import '~xxx-lib/dist/golbal.scss';
  • 第五步:资源文件配置

    angular.json在build的时候提供了assets选项在构建项目时复制文件和文件夹,我们可以使用资产对象的形式从项目外部复制assets资源。想了解更详细的配置信息可以访问官方文档。这里举个例子:angular.json文件中配置以下代码

    • glob:一个 node-glob 它使用 input 作为基准目录。
    • input:相对于工作空间根目录的路径。
    • output:相对于 outDir 的路径(默认为 dist/project-name )。
    • ignore:要排除的 glob 列表。
{
    "assets": [
        {
            "glob": "**/*",
            "input": "./node_modules/xxx-lib/dist/images/",
            "output": "assets/images"
        }
    ]
}
  • 第六步(可选):编译配置

    当项目在build的时候找不到Library模块时,我们需要手动配置ts编译选项,以便让cli在将ts编译为js的时候能找到我们的Lib模块。typescript创建的项目在根目录下都会有一个tsconfig.json文件,文件中指定了用来编译这个项目的根文件和编译选项。更详细的配置项,可以查阅官方文档,编译选项可以查阅这个官方文档。我们的解决办法是手动配置compilerOptions下的path选项:添加非相对路径模块映射。即:在tsconfig.json中配置Lib的路径

{
    "compilerOptions": {
        // 加入以下代码【模块名到基于baseUrl的路径映射】。这里要注意:如果已经在tsconfig.app.json里定义里baseUrl会覆盖tsconfig.json中的baseUrl!
        "baseUrl": "./",
        "paths": {
            "xxx-lib": [
                "node_modules/xxx-lib/dist"
            ]
        }
    }
}

4. 以下是目前遇到的问题

  • Lib库打包assets文件没有被打包到dist里?

    目前并没有好的方式可以打包静态资源,详情问题可以查阅angular-cli github issues。所以我们只能自己手动写脚本或者使用其他插件打包我们的静态assets资源文件,例如使用gulp。

  • Lib写的路由在项目中使用提示:Error: Cannot match any routes. URL Segment: 'xxx/xxx'

    这个就是路由没有匹配上,在Lib库里边我们的路由要写成相对路由,这样前置路径就可以正确的匹配上。更详细的介绍可以查阅官方文档。举个例子如下:

// 注入服务
constructor(
    private route: ActivatedRoute,
    private router: Router
) { }

// 相对路由写法
this.router.navigate([`../monitor-progress`], {
    relativeTo: this.route, // 相对路径
    queryParams: {
        classLessonId: item.classLessonId
    }
});

  • 本地跑的好好的,一到线上就不好,Lib包在Jenkins发版的时候不更新

    由于我们私有的Lib库没有发布到npm上,没有每一次打包的dist版本镜像,所以npm install的时候无法匹配版本导致无法更新Lib包。我们可以通过重新安装Lib的方式来进行更新(PS:当然如果Jenkins构建的时候删除了node_module文件夹的话,就不需要进行这样的配置了)。package.json配置如下:

{
  "scripts": {
    "update:lib": "npm install monitor-platform-lib",
    "build:prod": "npm run update:lib && npm run service_worker && node --max_old_space_size=4096 ./node_modules/.bin/ng build --prod && npm run static_share prod"
  },
  "private": true,
  "dependencies": {
    "monitor-platform-lib": "git+http://gs.blingabc.com/web/monitor-platform-lib.git"
  }
}

  • npm下载的Library库,除了dist目录之外还有其他一堆文件和文件夹,我并不希望下载这些,应该怎么办?

    package.json的配置项里有一个files选项可以满足你的需求。详细的介绍可以查阅官方文档,以下是我们的package.json配置:

{
  "name": 'xxx',
  "scripts": {
      ...
  },
  "dependencies": {
      ...
  },
  "files": [
    "dist"
  ]
}
  • Lib库如何和我们的项目进行通信呢?

    我们使用router-outlet路由插座作为中间载体,组件被激活(挂载)的时候router-outlet会提供activate回调,回调里会给我们这个激活(挂载)组件的实例,我们在组件实例使用@Input和@output装饰器即可完成双向通信。具体的例子可以查阅[这篇文章](medium.com/@sujeeshdl/…

  • Lib库里的某些组件样式我不满意,Lib库又没有提供相应的方法和属性供我使用,我改怎么办呢?

    首先视图封装的模式,一般四种:ShadowDom、Native 、Emulated、None,angular默认的视图封装模式是Emulated,顾名思义也就是:只进不出,全局样式能进来,组件样式出不去。我们可以使用伪类deep做到修改Lib库样式。具体的使用方式可以查阅官方文档。当然不建议使用,样式的封装还是交由开发Library库的人员来进行维护。

  • Ionic封装的Library模块,项目在安装完build的时候报错:Interface 'HTMLIonActionSheetControllerElement' Cannot simultaneously extend types 'HTMLStencilElement' and 'HTMLStencilElement'. Named property 'componentOnReady' of types 'HTMLStencilElement.'

    我们在封装Lib模块的时候@ionic/angular版本不一致导致,为了做兼容,我们可以把这个组件库依赖项写到dependencies里。

{
    "dependencies": {
        ...
    },
    "devDependencies": {
        "@ionic/angular": "^4.0.1"
    }
}
  • Lib模块在开发的时候好好的,但是在别的项目了引入之后build编译报一些依赖项错误

    检查以下看是否是我们对应Lib模块库package.json里的dependencies配置选项里锁死里某个package包的版本导致其他项目使用的时候版本不兼容编译报错。

  • 项目本地运行好好的,但是一发到线上就报编译错误...

    仔细查阅报错信息,看看是不是对应的依赖package包最近有更新,更新的版本是否有向下兼容,如果没有就锁住当前版本(PS:使用里package-lock.json的除外)。