愉快使用Angular-cli构建项目

1,103 阅读13分钟
原文链接: github.com

前言

使用Angular CLI开发Angular应用程序是一个非常愉快的体验!Angular团队为我们提供了惊人的CLI工具,支持大部分开箱即用的构建项目所需要的功能。

标准化的项目结构,具有全面的测试功能(单元测试和e2e测试),代码脚手架,支持使用环境特定配置的生产级构建。这是一个强大功能的脚手架,为我们每个新项目节省了大量的时间。这里要感谢Angular团队

虽然Angular CLI从一开始就非常好用,也有很多吐槽点(吐槽在最后),但是我们可以利用一些潜在的配置改进和最佳实践来使我们的项目更加完善!

我们需要了解什么?

  1. 如何规划我们的模块
  2. 如何使用应用程序和环境文件夹的别名来支持快捷导入
  3. 如何使用Sass和Angular Material(或者NG-ZORRO,或者自己打造UI组件库)
  4. 如何建立良好的生产构建
  5. 如何不使用Phantom JS,而使用Headless Chrome代替测试
  6. 如何通过自动生成的更新日志和正确的版本号来管理我们的项目

安装和使用(需要科学上网,不然以下安装会有各种问题)

安装nodejs

官网下载即可,注意:下载v8.x版本

安装 Angular CLI

npm install -g @angular/cli

注意:Windows下面安装angular-cli有两个典型的坑,一个是node-sass被墙了,第二个就是node-gyp依赖于某些API,必备需要安装:python2.7(一定要2.7)、Visual Studio(包含VB,C++等,不过有点大10g左右)

如果安装不成功可以使用以下方式:

npm i -g cnpm
cnpm i -g @angular/cli

创建新项目

ng new 项目名称
cd 项目名称 (自动去npm install 安装angular/cli提供的依赖包)
ng serve

注意:ng new my-app 失败?npm-gyp没安装,环境不行- Environment setup and configuration

1. 规划模块--基于核心模块,共享模块和特性模块与懒加载的模块结构的最佳实践

我们使用Angular CLI生成了新的项目,那现在呢?我们应该继续生成我们的服务和组件到一些随机的文件夹。接下来该如何构建我们的项目?

一个好的设计方式是把我们的应用程序分成至少三个不同的模块 - 核心模块,共享模块和特性模块(尽管我们可能需要多个特性模块)【特性模块就是我们常说每个功能页面】,如果我们不想使用第三方UI组件库,那我们还需要一个UI模块来修饰。

CoreModule【核心模块】

在Angular官网模块部分也专门指出用核心模块,只在应用启动时导入它一次,而不会在其它地方导入它。

在所有每个应用程序(单例服务)上必须有且仅有一个实例的服务应该在这里实现。典型的例子可以是认证服务或用户服务。我们来看一个CoreModule实现的例子。

core.module.ts

import { NgModule, Optional, SkipSelf } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

/* 我们自己的定制全局服务  */
import { UserService } from './user/user.service';

@NgModule({
  imports: [
    HttpClientModule
  ],
  providers: [
    /* 我们自己的定制全局服务  */
    SomeSingletonService
  ]
})
export class CoreModule {
  /* 确保CoreModule只能在AppModule导入 */
  constructor (
    @Optional() @SkipSelf() parentModule: CoreModule
  ) {
    if (parentModule) {
      throw new Error('CoreModule is already loaded. Import only in AppModule');
    }
  }
}

SharedModule 【共享模块】

在Angular官网模块部分也专门指出用共享模块,只在特性模块里导入它一次,而不需要再去导入其他Angular核心模块和第三方模块,我们自定义组件,指令,管道。

所有的应用组件,指令和管道应该在这里管理。这些组件不会在其构造函数中从核心或其他功能导入和注入服务。他们应该通过使用它们的组件模板中的属性来接收所有的数据。这一切都归结于SharedModule对我们的应用程序的其余部分没有任何依赖性的事实。这也是导入和导出UI组件,业务通用组件的理想场所。

shared.module.ts

/* Angular核心模块 */
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule  } from '@angular/forms';
/* 第三方组件 */
import { MdButtonModule } from '@angular/material';
/* our own custom components */
import { SomeCustomComponent } from './some-custom/some-custom.component';

@NgModule({
  imports: [
    /* Angular核心模块 */
    CommonModule,
    FormsModule,

    /* 第三方组件 */
    MdButtonModule,
  ],
  declarations: [
    ListComponent
  ],
  exports: [
    /* Angular核心模块 */
    CommonModule,
    FormsModule,

    /*  第三方组件 */
    MdButtonModule,

    /* 自定义组件 */
    ListComponent
  ]
})
export class SharedModule { }

CoreModule和SharedModule区别

Module属性 CoreModule SharedModule
imports 必须 必须
providers 必须 禁止
declarations 禁止 必须
exports 禁止 必须

总结:CoreModule 只有导入没有导出,SharedModule有导入导出,却没有服务依赖注入管理,这个需要在 CoreModule 里面操作。

使用Angular CLI构建项目结构

我们可以在创建新项目后立即生成Core和Shared模块。这样,我们将准备从一开始就生成额外的组件和服务。

运行ng generate module core可以生成模块核心。具体规则可以看这里ng generate
然后在core文件夹中创建index.ts文件,并重新导出CoreModule本身。
代码:export * from './core.module';
在进一步开发的过程中,我们会再出口更多的公共服务,这些服务应该在index.ts提供。
core/index.ts

export * from './user/user.service';
export * from './core.module';

如何访问: app.module.ts

import { CoreModule } from './core';

好处我不需要关心里面的CoreModule 所在文件位置,只需要关心我对于的导出依赖名称即可。

同样,我们可以为共享模块做同样的事情。

FeatureModule 【特性模块】

在Angular官网模块部分也专门指出用特性模块,特性模块是带有@NgModule装饰器及其元数据的类,就像根模块一样。 特性模块的元数据和根模块的元数据的属性是一样的。根模块和特性模块还共享着相同的执行环境。 它们共享着同一个依赖注入器,这意味着某个模块中定义的服务在所有模块中也都能用。
它们在技术上有两个显著的不同点:

  • 我们引导根模块来启动应用,但导入特性模块来扩展应用。
  • 特性模块可以对其它模块暴露或隐藏自己的实现。
    特性模块用来提供了内聚的功能集合。 聚焦于应用的某个业务领域、用户工作流、某个基础设施(表单、HTTP、路由),或一组相关的工具集合。

将为我们的应用程序的每个独立功能创建多个功能模块。功能模块应该只从CoreModule导入服务。 如果功能模块A需要从功能模块B导入服务,则考虑将该服务移入CoreModule。

在某些情况下,需要仅由某些功能共享的服务,将它们转移到核心并不合理。在这种情况下,我们可以创建特殊的共享功能模块,不依赖于CoreModule提供的服务和SharedModule提供的组件的任何其他功能的功能。

这将保持我们的代码清洁,易于维护和扩展的新功能。这也减少了重构所需的工作量。如果正确执行,我们将确信更改一个功能不会影响或破坏我们的应用程序的其余部分。

LazyLoading 【懒加载模块】

懒加载需要配合路由完成. 可以看伪代码 app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'user',
    loadChildren: 'app/user/user.module#UserModule'
  },
  {
    path: '**',
    redirectTo: 'user'
  }
];

@NgModule({
  // useHash支持带#号的url地址,
  imports: [RouterModule.forRoot(routes, { useHash: true })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

我们应该尽可能延迟加载我们的功能模块。理论上,应用程序启动期间只能同步加载一个功能模块以显示初始内容。在用户触发导航之后,每个其他功能模块应该被延迟加载。

2. 为应用程序和环境文件夹设置别名

别名我们的应用程序和环境文件夹将使我们能够实施干净的入口,这将在整个应用程序中保持一致。

考虑假设的,但通常的情况。我们正在研究一个功能A中位于三个文件夹深处的组件,并且我们要从位于两个文件夹深处的核心导入服务。这将导致导入语句看起来像import {SomeService} from '../../../core/user/user-settings/user-settings.service'这样。

是不是很不爽。。。

更不爽的是,任何时候我们想改变这两个文件中的任何一个的位置,我们的导入语句就会中断,需要重新去改引入url地址,有些编辑器可以帮我们重构这个问题,例如:Webstorm。

曾经看过vue-cli,在它里面构建里面路径是‘@/xxx’ 如果没有记错,(ps:因为快一年没有使用了)。应该是这样的,@代表src,这个是webpack里面设置的别名功能。

在Angular-cli去改Webpack配置是一个很麻烦的事情,我们可以修改tsconfig.json配置。
为了能够使用别名,我们必须添加baseUrlpaths属性到我们的tsconfig.json文件中,像这样...

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@app/*": ["app/*"],
      "@env/*": ["environments/*"]
    }
  }
}

我们还添加了@env别名,以便能够使用import { environment } from "@env/environment"从我们的应用程序的任何位置轻松访问环境变量。它将适用于所有指定的环境,因为它会根据传递给ng build命令的--env标志自动解析正确的环境文件。

随着我们的路径,我们现在可以导入像这样的环境和服务... user.module.ts

/* Angular核心模块 */
import { NgModule } from '@angular/core';

/* 共享模块 */
import { SharedModule } from '@app/shared';

import { environment } from '@env/environment';

@NgModule({
  imports: [
    /* 共享模块 */
    SharedModule 
  ],
  providers: [
    {
       provide: 'USER_API', useValue: environment.production ? '/api/test':'/api'
    }
  ]
})
export class UserModule { }

您可能已经注意到我们直接从@app/core而不是@app/core/user/user .service导入实体(如上例中的UserService)。这是可能的,这要归功于重新导出主index.ts文件中的每个公共实体。我们创建一个index.ts文件每个包(文件夹),他们看起来像这样...

export * from './core.module';
export * from './user/user.service';

在大多数应用程序中,特定功能模块的组件和服务通常只需要访问来自CoreModule的服务和来自SharedModule的组件即可。有时这可能不足以解决特定的业务案例,我们还需要某种“共享功能模块”,为其他功能模块的有限子集提供功能。

在这种情况下,我们将最终得到来自import { FeatureService } from '@app/shared-feature';因此与核心类似,也使用@app别名访问共享特征。

使用Sass 和第三方UI组件

Sass是一个样式预处理器,它支持像变量这样的强大的东西(即使css也会很快得到变量),函数,mixins 等,你把它命名为...

Sass还需要有效地使用官方Angular Material Components库以及其广泛的主题功能。假设使用Sass是大多数项目的默认选择是安全的。

要使用Sass,我们必须用--style scss标志来使用Angular CLI生成我们的项目,或者在defaults和styleExt来设置。默认情况下,没有添加的是stylePreprocessorOptions和includePaths,我们可以使用强制性的根“./”和可选的“./themes”值来设置。

angular-cli.json

{
  "apps": [
    {      
      ...
      "stylePreprocessorOptions": {
        "includePaths": ["./", "./themes"]
      }
     "defaults": {
         "styleExt": "scss"
      }
    }
  ]
}

常用的UI组件:

想要快速熟悉Angular,推荐自己撸一套UI组件库。

4. 如何建立良好的生产构建

Angular CLI生成的项目只带有一个非常简单的ng build脚本。要生成生产级的工件,我们必须自己做一些定制。

我们在package.json脚本中添加“build:prod”:“ng build --target production --build-optimizer --vendor-chunk”

发布生产

这是一个设置开关,它使代码缩小和默认情况下很多有用的构建标志。这相当于使用以下...

  • --environment prod 使用environment.prod.ts 文件的环境变量
  • --aot 启用前期编译。这是目前版本的Angular CLI的默认设置。如果你使用低版本,必须手动启用它
  • --extract-css true 将所有的CSS提取到独立的样式表文件中
  • --sourcemaps false 禁用压缩文件对应map的生成
  • --named-chunks false 禁用使用人类可读名称的块和使用数字

其他有用的设置

  • --build-optimizer 新功能,导致更小的捆绑,但更长的构建时间,所以谨慎使用!(也应该在未来默认启用)
  • --vendor-chunk 将所有第三方依赖(库)代码提取到单独的块中

另外检查其他可用的配置标志官方文档,这可能在您的个人项目中有用。

5. 使用Headless Chrome代替Phantom JS

PhantomJS是一个非常知名的headless browser(ps: 无头浏览器 很恐怖),它实际上是用于在CI服务器和许多开发机器上运行前端测试的解决方案。

虽然还算不错,但对现代ECMAScript功能的支持还是比较滞后的。更为严重的是,这些非标准的行为在本地没有问题地通过测试的时候就曾多次引起头痛,但是仍然打破了CI​​的环境。

幸运的是,我们不必再处理它了!

正如官方文件所说...

Headless Chrome正在使用Chrome 59.这是在无头环境下运行Chrome浏览器的一种方式。本质上,在没有Chrome的情况下运行Chrome!它将Chromium和Blink渲染引擎提供的所有现代Web平台功能带到命令行。

那么我们如何在Angular CLI项目中使用它?

我们在项目的package.json添加如下代码...

package.json

"scripts": {
    "test": "npm run lint && ng test --single-run",
    "watch": "ng test --browsers ChromeHeadless --reporters spec",
  },

6. 使用标准的提交消息

可以看这里我的这篇文章GET新技能之Git commit message

快速总结一下我们感兴趣的项目的新功能和缺陷修复是非常好的。

让我们为用户提供相同的便利!

手动编写更改CHANGELOG.md将是极其繁琐的容易出错的任务,因此最好是自动执行该过程。
有很多可用的工具Conventional Commits specification可以完成这项工作,但我们只关注标准版本。

常规提交定义了强制类型,可选(范围):后跟提交消息。也可以添加可选的正文和页脚,两者都用空行分隔。通过查看angular-cli的完整提交消息的示例,我们来看看在实践中该如何实现。

feat(@angular/cli): move angular-cli to @angular/cli (#4328)

由于BREAKING CHANGE关键字在提交主体中的存在,标准版本将正确地冲击项目的MAJOR版本。

生成的CHANGELOG.md将会看起来像这样...

v1.6.0-beta.2

Bug Fixes

@ngtools/webpack: fix elide removing whole imports on single match (62f3454), closes #8518

v1.5.2

Bug Fixes

@ngtools/webpack: fix elide removing whole imports on single match (62f3454), closes #8518

看起来是不是很酷呀!那么我们怎样才能在我们的项目中使用这个?

我们首先安装npm install -D standard-version将其保存在我们的devDependencies中,并将“release”:“standard-version”添加到我们的package.json scripts中。
package.json

"scripts": {
    "release": "standard-version"
  },

我们还可以添加git pushnpm publish来自动完成整个过程。
在这种情况下,我们需要改进脚本为下面例子
package.json

"scripts": {
    "release": "standard-version && git push — follow-tags origin master && npm publish"
  },

注意:我们使用 && 并将依赖于平台的命令链在基于Unix的系统上(因此也在Windows上使用Cygwin、Gitbash或Linux的新Win10子系统)。

其他相关技巧(待续,不断完善中...)