WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

Angular #60

Open WangShuXian6 opened 5 years ago

WangShuXian6 commented 5 years ago

Angular

https://angular.cn

安装 Angular CLI

sudo npm install -g @angular/cli

创建工作空间和初始应用 Angular 工作空间就是你开发应用的上下文环境。 每个工作空间包含一些供一个或多个项目使用的文件。 每个项目都是一组由应用、库或端到端(e2e)测试构成的文件 ng new 命令来自动创建项目骨架

ng new my-app

将创建下列工作空间和初始项目文件:

  • 一个新的工作空间,根目录名叫 my-app
  • 一个初始的骨架应用项目,也叫 my-app(但位于 src 子目录下)
  • 一个端到端测试项目(位于 e2e 子目录下)
  • 相关的配置文件

启动开发服务器

cd my-app
ng serve --open
// --open(或只用 -o)选项会自动打开浏览器,并访问 http://localhost:4200/

ng generate component message ng generate service message

ng generate component content/pages/my-page app/content/pages/my-page


Component(组件)是整个框架的核心,也是终极目标。“组件化”的意义有 2 个:一是分治,因为有了组件之后,我们可以把各种逻辑封装在组件内部,避免混在一起;二是复用,封装成组件之后不仅可以在项目内部复用,而且还可以沉淀下来跨项目复用。

NgModule(模块)是组织业务代码的利器,按照自己的业务场景,把组件、服务、路由打包到模块里面,形成一个个的积木块,然后再用这些积木块来搭建出高楼大厦。

Router(路由)的角色也非常重要,它有 3 个重要的作用:一是封装浏览器的 History 操作;二是负责异步模块的加载;三是管理组件的生命周期。


老版本使用 AngularJS 指代,所有新版本都叫做 Angular。原因很好理解,因为老版本是用 JS 开发的,所以带一个 JS 后缀,而新版本是基于 TypeScript 开发的,带 JS 后缀不合适。


官方版本发布计划:

每 6 个月发布一个主版本(第一位版本号,主版本) 每个主版本发布 1 ~ 3 个小版本(第二位版本号,Feature 版本号) 每周发布一个补丁版本(第三位版本号,Hotfix 版本号)


ng serve 是在内存里面生成项目,如果你想看到项目编译之后的产物,请运行 ng build。构建最终产品版本可以加参数,ng build --prod。


ng

自动创建组件(ng generate component MyComponent,ng g c MyComponent),创建组件的时候也可以带路径,如 ng generate component mydir / MyComponent。 自动创建指令:ng g d MyDirective。 自动创建服务:ng g s MyService。 构建项目:ng build,如果想构建最终的产品版本,可以用 ng build --prod 命令。


@angular/cli 在 Windows 平台上需要依赖 Python 环境、Visual Studio 环境。


VS Code 安装 Debugger for Chrome 插件


angular cli 使用 cdn 资源

設定方式1

.angular-cli.json Angular CLI 提供 deployUrl 的參數,該參數所設定的網址會影響 index.html 內的 main.bundle.js、vendor.bundle.js、 css 裡面的圖片等網址,這些網址會被加上 deployUrl 所設定的網址

所以透過這個參數,就可以很簡單的將 CDN 的位置,加到現有的 script 的 src 裡。

設定方式2

建置指令掛參數

ng build --deploy-url=『CDN 網址』

或是

ng build -d 『CDN 網址』

WangShuXian6 commented 5 years ago

双向绑定

[(ngModel)] 是 Angular 的双向数据绑定语法

ngModel 是一个有效的 Angular 指令 它属于一个可选模块 FormsModule

这里把 hero.name 属性绑定到了 HTML 的 textbox 元素上,以便数据流可以双向流动:从 hero.name 属性流动到 textbox,并且从 textbox 流回到 hero.name

<div>
<label>name:
<input [(ngModel)]="hero.name" placeholder="name">
</label>
</div>

每个组件都必须声明在(且只能声明在)一个 NgModule 中


*ngFor

*ngFor 是一个 Angular 的复写器(repeater)指令。 它会为列表中的每项数据复写它的宿主元素

<ul>
<li class="heros" *ngFor="let hero of heroes">
<span>{{hero.id}}--{{hero.name}}</span>
</li>
</ul>

click 事件绑定

click 外面的圆括号会让 Angular 监听这个

  • 元素的 click 事件。 当用户点击
  • 时,Angular 就会执行表达式 onSelect(hero)。

    Angular 会把所点击的

  • 上的 hero 对象传给它,这个 hero 也就是前面在 *ngFor 表达式中定义的那个。

    <ul>
    <li class="heros"
    *ngFor="let hero of heroes"
    (click)="onSelect(hero)">
    <span>{{hero.id}}--{{hero.name}}</span>
    </li>
    </ul>

    click 事件处理器

    
    import {Component, OnInit} from '@angular/core';
    import {Hero} from '../hero';
    import {HEROES} from '../mock-heroes';
  • @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.less'] }) export class HeroesComponent implements OnInit { hero: Hero = { id: 1, name: 'harry potter' };

    heroes = HEROES;
    
    selectedHero: Hero;
    
    onSelect(hero: Hero): void {
        this.selectedHero = hero;
    }
    
    constructor() {
    }
    
    ngOnInit() {
    }

    }

    
    #### *ngIf 隐藏空白的详情
    ```html
    <h2>{{hero.name | uppercase}} details</h2>
    <div><span>id:</span>{{hero.id}}</div>
    <div><span>name:</span>{{hero.name}}</div>
    
    <div>
        <label for="">
            <input [(ngModel)]="hero.name" placeholder="name">
        </label>
    </div>
    
    <ul>
        <li class="heros"
            *ngFor="let hero of heroes"
            (click)="onSelect(hero)">
            <span>{{hero.id}}--{{hero.name}}</span>
        </li>
    </ul>
    
    <div *ngIf="selectedHero">
        <h2>{{selectedHero.name | uppercase}} Details</h2>
        <div><span>id: </span>{{selectedHero.id}}</div>
    </div>

    CSS 类绑定

    [class.some-css-class]="some-condition" [class.selected]="hero === selectedHero"

    <li *ngFor="let hero of heroes"
    [class.selected]="hero === selectedHero"
    (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
    .selected{
    color: yellow;
    }

    主从组件

    单向数据绑定 属性绑定语法 [hero]="selectedHero"

    父组件 把 HeroesComponent.selectedHero 绑定到该元素的 hero 属性

    <app-hero-detail [hero]="selectedHero"></app-hero-detail>

    子组件 hero 属性必须是一个带有 @Input() 装饰器的输入属性,因为外部的 HeroesComponent 组件将会绑定到它

    导入 Input 符号

    
    import {Component, OnInit, Input} from '@angular/core';
    import {Hero} from '../hero';

    @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.less'] }) export class HeroDetailComponent implements OnInit {

    @Input() hero: Hero;
    // 带有 @Input() 装饰器的 hero 属性
    // HeroDetailComponent 模板中绑定了组件中的 hero 属性,它的类型是 Hero
    
    constructor() {
    }
    
    ngOnInit() {
    }

    }

    ```html
    <div *ngIf="hero">
      <h2>{{hero.name | uppercase}}</h2>
      <div>id:{{hero.id}}</div>
      <input [(ngModel)]="hero.name" placeholder="name">
    </div>
    WangShuXian6 commented 5 years ago

    服务

    why 组件不应该直接获取或保存数据,它们不应该了解是否在展示假数据。 它们应该聚焦于展示数据,而把数据访问的职责委托给某个服务。 依靠 Angular 的依赖注入机制把它注入到 组件或服务 的构造函数中

    服务是在多个“互相不知道”的类之间共享信息的好办法

    ng generate service hero


    @Injectable() 服务

    注意,这个新的服务导入了 Angular 的 Injectable 符号,并且给这个服务类添加了 @Injectable() 装饰器。 它把这个类标记为依赖注入系统的参与者之一。HeroService 类将会提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖

    Service 可以从任何地方获取数据:Web 服务、本地存储(LocalStorage)或一个模拟的数据源。

    从组件中移除数据访问逻辑,意味着将来任何时候你都可以改变目前的实现方式,而不用改动任何组件。 这些组件不需要了解该服务的内部实现。


    提供(provide)

    通过注册提供商 在要求 Angular 把 HeroService 注入到 HeroesComponent 之前,你必须先把这个服务提供给依赖注入系统

    默认情况下,Angular CLI 命令 ng generate service 会通过给 @Injectable 装饰器添加元数据的形式,为该服务把提供商注册到根注入器上。

    hero.service.ts

    
    import {Injectable} from '@angular/core';
    import {Observable, of} from 'rxjs';
    import {Hero} from './hero';
    import {HEROES} from './mock-heroes';
    import {MessageService} from './message.service';

    @Injectable({ providedIn: 'root' }) export class HeroService { // 服务中的服务 // 把 MessageService 注入到了 HeroService 中,而 HeroService 又被注入到了 HeroesComponent 中。

    // 1. 声明了一个私有 heroService 属性,2. 把它标记为一个 HeroService 的注入点
    // 当 Angular 创建 HeroesComponent 时,依赖注入系统就会把这个 heroService 参数设置为 HeroService 的单例对象。
    constructor(private messageService: MessageService) {
    
    }
    
    getHeroes(): Observable<Hero[]> {
        this.messageService.add('heroService :fetch heroes');
        return of(HEROES); // 用 RxJS 的 of() 函数来模拟从服务器返回数据。
        // of(HEROES) 会返回一个 Observable<Hero[]>,它会发出单个值,这个值就是这些模拟英雄的数组。
    }

    }

    
    >message.service.ts
    ```ts
    import {Injectable} from '@angular/core';
    
    @Injectable({
        providedIn: 'root' // 为该服务把提供商注册到根注入器上。
        // 当你在顶层提供该服务时,Angular 就会为 HeroService 创建一个单一的、共享的实例,并把它注入到任何想要它的类上
    })
    export class MessageService {
    
        constructor() {
        }
    
        messages: string[] = [];
    
        add(message: string) {
            this.messages.push(message);
        }
    
        clear() {
            this.messages = [];
        }
    }

    heroes.component.ts

    
    import {Component, OnInit} from '@angular/core';
    import {Hero} from '../hero';
    // import {HEROES} from '../mock-heroes';
    import {HeroService} from '../hero.service';

    @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.less'] }) export class HeroesComponent implements OnInit { heroes: Hero[];

    selectedHero: Hero;
    
    constructor(private heroService: HeroService) {
    }
    
    ngOnInit() {
        this.getHeroes();
    }
    
    onSelect(hero: Hero): void {
        this.selectedHero = hero;
    }
    
    getHeroes(): void {
        this.heroService.getHeroes()
            .subscribe((heroes) => {
                this.heroes = heroes;
            });
    }

    }

    
    >message.component.ts
    ```ts
    import {Component, OnInit} from '@angular/core';
    import {MessageService} from '../message.service';
    
    @Component({
        selector: 'app-message',
        templateUrl: './message.component.html',
        styleUrls: ['./message.component.less']
    })
    export class MessageComponent implements OnInit {
    
        // 必须是公共属性,因为你将会在模板中绑定到它
        // Angular 只会绑定到组件的公共属性
        constructor(public messageService: MessageService) {
        }
    
        ngOnInit() {
        }
    
    }

    message.component.html

    <div *ngIf="messageService.messages.length">
    <h2>message</h2>
    <!--把按钮的 click 事件绑定到了 MessageService.clear()-->
    <button (click)="messageService.clear()">clear</button>
    <div *ngFor="let message of messageService.messages">{{message}}</div>
    </div>

    app.module.ts

    
    import {BrowserModule} from '@angular/platform-browser';
    import {NgModule} from '@angular/core';
    import {FormsModule} from '@angular/forms';

    import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; import {HeroesComponent} from './heroes/heroes.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { MessageComponent } from './message/message.component';

    @NgModule({ declarations: [ AppComponent, HeroesComponent, HeroDetailComponent, MessageComponent ], imports: [ BrowserModule, AppRoutingModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

    >@NgModule.bootstrap属性把这个AppComponent标记为引导 (bootstrap) 组件。 当 Angular 引导应用时,它会在 DOM 中渲染AppComponent,并把结果放进index.html的<my-app>元素标记内部
    ***
    >你把数据访问逻辑重构到了 HeroService 类中。
    >你在根注入器中把 HeroService 注册为该服务的提供商,以便在别处可以注入它。
    >你使用 Angular 依赖注入机制把它注入到了组件中。
    >你给 HeroService 中获取数据的方法提供了一个异步的函数签名。
    >你发现了 Observable 以及 RxJS 库。
    >你使用 RxJS 的 of() 方法返回了一个模拟英雄数据的可观察对象 (Observable<Hero[]>)。
    >在组件的 ngOnInit 生命周期钩子中调用 HeroService 方法,而不是构造函数中。
    >你创建了一个 MessageService,以便在类之间实现松耦合通讯。
    >HeroService 连同注入到它的服务 MessageService 一起,注入到了组件中。
    
    ***
    #### 有4中情况下, 服务是全局单例的
    ```ts
    // 有4中情况下, 服务是全局单例的
    
    // 1. 仅在服务中使用根注入器
    @Injectable({
        providedIn:'root'
    })
    
    // 2. 在服务中声明所属立即加载的模块
    @Injectable({
        providedIn:M1RoutingModule
    })
    
    // 3. 在根模块中提供服务
    @NgModule({
        providers:[M2Service]
    })
    
    // 4. 在被直接或间接导入根模块的模块中的服务(也就是非懒加载路由模块)
    @NgModule({
        providers:[M2Service]
    })
    
    WangShuXian6 commented 5 years ago

    路由

    添加 AppRoutingModule Angular 的最佳实践之一就是在一个独立的顶级模块中加载和配置路由器,它专注于路由功能,然后由根模块 AppModule 导入它

    ng generate module app-routing --flat --module=app
    // --flat 把这个文件放进了 src/app 中,而不是单独的目录中。
    // --module=app 告诉 CLI 把它注册到 AppModule 的 imports 数组中。

    典型的 Angular 路由(Route)有两个属性: path:一个用于匹配浏览器地址栏中 URL 的字符串。 component:当导航到此路由时,路由器应该创建哪个组件。

    
    import { HeroesComponent }      from './heroes/heroes.component';

    const routes: Routes = [ { path: 'heroes', component: HeroesComponent } ];

    
    ***
    
    #### RouterModule.forRoot()
    
    >必须首先初始化路由器,并让它开始监听浏览器中的地址变化。
    
    >把 RouterModule 添加到 @NgModule.imports 数组中,并用 routes 来配置它。你只要调用 imports 数组中的 RouterModule.forRoot() 函数就行了。
    
    ```ts
    imports: [ RouterModule.forRoot(routes) ],
    // 这个方法之所以叫 forRoot(),是因为你要在应用的顶级配置这个路由器。 forRoot() 方法会提供路由所需的服务提供商和指令,还会基于浏览器的当前 URL 执行首次导航。

    路由出口 (RouterOutlet)

    \<router-outlet> 会告诉路由器要在哪里显示路由到的视图 能在 AppComponent 中使用 RouterOutlet,是因为 AppModule 导入了 AppRoutingModule,而 AppRoutingModule 中导出了 RouterModule。

    <h1>{{title}}</h1>
    <router-outlet></router-outlet>
    <app-messages></app-messages>

    路由链接 (routerLink)

    routerLink 属性的值为 "/heroes",路由器会用它来匹配出指向 HeroesComponent 的路由。 >routerLink 是 RouterLink 指令的选择器,它会把用户的点击转换为路由器的导航操作。 它是 RouterModule 中公开的另一个指令。

    <nav>
    <a routerLink="/heroes">Heroes</a>
    </nav>

    默认路由 把下列路由添加到 AppRoutingModule.Routes 数组中 这个路由会把一个与空路径“完全匹配”的 URL 重定向到路径为 '/dashboard' 的路由

    { path: '', redirectTo: '/dashboard', pathMatch: 'full' }

    参数化路由 path 中的冒号(:)表示 :id 是一个占位符,它表示某个特定英雄的 id

    { path: 'detail/:id', component: HeroDetailComponent },
    
    content_copy 

    <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">

    {{hero.name}}

    ***
    
    >app-routing.module.ts
    ```ts
    import {NgModule} from '@angular/core';
    import {Routes, RouterModule} from '@angular/router';
    import {DashboardComponent} from './dashboard/dashboard.component';
    import {HeroesComponent} from './heroes/heroes.component';
    import {HeroDetailComponent} from './hero-detail/hero-detail.component';
    
    // 配置路由器
    const routes: Routes = [
        {path: '', redirectTo: '/dashboard', pathMatch: 'full'},
        {path: 'dashboard', component: DashboardComponent},
        {path: 'detail/:id', component: HeroDetailComponent},
        {path: 'heroes', component: HeroesComponent}
    ];
    
    @NgModule({
        // 初始化路由器,并让它开始监听浏览器中的地址变化
        // 在应用的顶级配置这个路由器
        // forRoot() 方法会提供路由所需的服务提供商和指令,还会基于浏览器的当前 URL 执行首次导航。
        imports: [RouterModule.forRoot(routes)],
        //  导出 RouterModule 让路由器的相关指令可以在 AppModule 中的组件中使用。
        exports: [RouterModule]
    })
    export class AppRoutingModule {
    }

    app.module.ts

    
    import {BrowserModule} from '@angular/platform-browser';
    import {NgModule} from '@angular/core';
    import {FormsModule} from '@angular/forms';

    import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; import {HeroesComponent} from './heroes/heroes.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { MessageComponent } from './message/message.component'; import { DashboardComponent } from './dashboard/dashboard.component';

    @NgModule({ declarations: [ AppComponent, HeroesComponent, HeroDetailComponent, MessageComponent, DashboardComponent ], imports: [ BrowserModule, AppRoutingModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

    
    >hero.service.ts
    ```ts
    import {Injectable} from '@angular/core';
    import {Observable, of} from 'rxjs';
    import {Hero} from './hero';
    import {HEROES} from './mock-heroes';
    import {MessageService} from './message.service';
    
    @Injectable({
        providedIn: 'root'
    })
    export class HeroService {
        // 服务中的服务
        // 把 MessageService 注入到了 HeroService 中,而 HeroService 又被注入到了 HeroesComponent 中。
    
        // 1. 声明了一个私有 heroService 属性,2. 把它标记为一个 HeroService 的注入点
        // 当 Angular 创建 HeroesComponent 时,依赖注入系统就会把这个 heroService 参数设置为 HeroService 的单例对象。
        constructor(private messageService: MessageService) {
    
        }
    
        getHeroes(): Observable<Hero[]> {
            this.messageService.add('heroService :fetch heroes');
            return of(HEROES); // 用 RxJS 的 of() 函数来模拟从服务器返回数据。
            // of(HEROES) 会返回一个 Observable<Hero[]>,它会发出单个值,这个值就是这些模拟英雄的数组。
        }
    
        getHero(id: number): Observable<Hero> {
            this.messageService.add(`hero`);
            return of(HEROES.find((hero) => {
                return hero.id === id;
            }));
        }
    }

    app.component.html

    <!--The content below is only a placeholder and can be replaced.-->
    <h1>{{title}}</h1>
    <!--<app-heroes></app-heroes>-->
    <nav>
    <a routerLink="/dashboard">dashboard</a>
    <a routerLink="/heroes">heroes</a>
    </nav>
    <router-outlet></router-outlet>
    <app-message></app-message>

    dashboard.component.html

    <h3>top</h3>
    <div>
    <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}">
    <h4>{{hero.name}}</h4>
    </a>
    </div>

    dashboard.component.ts

    
    import {Component, OnInit} from '@angular/core';
    import {Hero} from '../hero';
    import {HeroService} from '../hero.service';

    @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.less'] }) export class DashboardComponent implements OnInit { heroes: Hero[] = [];

    constructor(private heroService: HeroService) {
    }
    
    ngOnInit() {
        this.getHeroes();
    }
    
    getHeroes(): void {
        this.heroService.getHeroes()
            .subscribe((heroes) => {
                // 只返回四个顶级英雄(第二,第三,第四和第五)
                this.heroes = heroes.slice(1, 5);
            });
    }

    }

    
    >heroes.component.html
    ```html
    <h2>heroes</h2>
    <ul>
        <li *ngFor="let hero of heroes">
            <a routerLink="/detail/{{hero.id}}"></a>
            <span>{{hero.id}}-{{hero.name}}</span>
        </li>
    </ul>

    heroes.component.ts

    
    import {Component, OnInit} from '@angular/core';
    import {Hero} from '../hero';
    // import {HEROES} from '../mock-heroes';
    import {HeroService} from '../hero.service';

    @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.less'] }) export class HeroesComponent implements OnInit { heroes: Hero[];

    selectedHero: Hero;
    
    constructor(private heroService: HeroService) {
    }
    
    ngOnInit() {
        this.getHeroes();
    }
    
    getHeroes(): void {
        this.heroService.getHeroes()
            .subscribe((heroes) => {
                this.heroes = heroes;
            });
    }

    }

    
    >hero-detail.component.html
    ```html
    <div *ngIf="hero">
      <h2>{{hero.name | uppercase}}</h2>
      <div>id:{{hero.id}}</div>
      <input [(ngModel)]="hero.name" placeholder="name">
      <button (click)="goBack()">go back</button>
    </div>

    hero-detail.component.ts

    
    import {Component, OnInit, Input} from '@angular/core';
    import {ActivatedRoute} from '@angular/router';
    import {Location} from '@angular/common';
    import {Hero} from '../hero';
    import {HeroService} from '../hero.service';

    @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.less'] }) export class HeroDetailComponent implements OnInit {

    @Input() hero: Hero;
    
    // ActivatedRoute 保存着到这个 HeroDetailComponent 实例的路由信息。 这个组件对从 URL 中提取的路由参数感兴趣。 其中的 id 参数就是要显示的英雄的 id。
    // HeroService 从远端服务器获取英雄数据,本组件将使用它来获取要显示的英雄。
    // location 是一个 Angular 的服务,用来与浏览器打交道。 稍后,你就会使用它来导航回上一个视图。
    constructor(
        private route: ActivatedRoute,
        private heroService: HeroService,
        private location: Location
    ) {
    }
    
    ngOnInit() {
        this.getHero();
    }
    
    getHero(): void {
        // route.snapshot 是一个路由信息的静态快照,抓取自组件刚刚创建完毕之后。
        // paramMap 是一个从 URL 中提取的路由参数值的字典。 "id" 对应的值就是要获取的英雄的 id。
        // 路由参数总会是字符串。 JavaScript 的 (+) 操作符会把字符串转换成数字,英雄的 id 就是数字类型。
    
        // 在浏览器的地址栏中粘贴了 localhost:4200/detail/11,路由器也会导航到 id: 11 的英雄("Mr. Nice")的详情视图。
        const id = +this.route.snapshot.paramMap.get('id');
        this.heroService.getHero(id)
            .subscribe((hero) => {
                this.hero = hero;
            });
    }
    
    goBack(): void {
        this.location.back();
    }

    }

    
    
    ***
    >添加了 Angular 路由器在各个不同组件之间导航。
    >你使用一些 <a> 链接和一个 <router-outlet> 把 AppComponent 转换成了一个导航用的壳组件。
    >你在 AppRoutingModule 中配置了路由器。
    >你定义了一些简单路由、一个重定向路由和一个参数化路由。
    >你在 <a> 元素中使用了 routerLink 指令。
    >你把一个紧耦合的主从视图重构成了带路由的详情视图。
    >你使用路由链接参数来导航到所选英雄的详情视图。
    >在多个组件之间共享了 HeroService 服务。
    WangShuXian6 commented 5 years ago

    Angular 跨域

    https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/proxy.md

    假如待请求的接口服务器为 https://xxx.xxxxxxx.com/api/card/login

    项目根目录 proxy.config.json target 为接口服务器

    {
    "/api": {
    "target": "https://xxx.xxx.com/api",
    "changeOrigin": true,
    "secure": false,
    "logLevel": "debug",
    "pathRewrite": {
    "^/api": ""
    }
    }
    }
    export class AuthenticationService implements AuthService {
        API_URL = '/api/';
        API_ENDPOINT_LOGIN = 'card/Login';
    
        public login(credential: Credential): Observable<any> {
            // Expecting response from API
            // tslint:disable-next-line:max-line-length
            // {"id":1,"username":"admin","password":"demo","email":"admin@demo.com","accessToken":"access-token-0.022563452858263444","refreshToken":"access-token-0.9348573301432961","roles":["ADMIN"],"pic":"./assets/app/media/img/users/user4.jpg","fullname":"Mark Andre"}
            return this.http.get<AccessData>(this.API_URL + this.API_ENDPOINT_LOGIN + '?' + this.util.urlParam(credential)).pipe(
                map((result: any) => {
                    if (result instanceof Array) {
                        return result.pop();
                    }
                    return result;
                }),
                tap(this.saveAccessData.bind(this)),
                catchError(this.handleError('login', []))
            );
        }
    }

    Proxy To Backend

    Using the proxying support in webpack's dev server we can highjack certain URLs and send them to a backend server. We do this by passing a file to --proxy-config

    Say we have a server running on http://localhost:3000/api and we want all calls to >http://localhost:4200/api to go to that server.

    We create a file next to our project's package.json called proxy.conf.json with the content

    {
      "/api": {
        "target": "http://localhost:3000",
        "secure": false
      }
    }

    You can read more about what options are available here.

    We can then add the proxyConfig option to the serve target:

    "architect": {
    "serve": {
    "builder": "@angular-devkit/build-angular:dev-server",
    "options": {
    "browserTarget": "your-application-name:build",
    "proxyConfig": "proxy.conf.json"
    },

    Now in order to run our dev server with our proxy config we can call ng serve.

    After each edit to the proxy.conf.json file remember to relaunch the ng serve process to make your changes effective.

    Rewriting the URL path One option that comes up a lot is rewriting the URL path for the proxy. This is supported by the pathRewrite option.

    Say we have a server running on http://localhost:3000 and we want all calls to >http://localhost:4200/api to go to that server.

    In our proxy.conf.json file, we add the following content

    {
    "/api": {
    "target": "http://localhost:3000",
    "secure": false,
    "pathRewrite": {
    "^/api": ""
    }
    }
    }

    If you need to access a backend that is not on localhost, you will need to add the changeOrigin option as follows:

    {
    "/api": {
    "target": "http://npmjs.org",
    "secure": false,
    "pathRewrite": {
    "^/api": ""
    },
    "changeOrigin": true
    }
    }

    To help debug whether or not your proxy is working properly, you can also add the logLevel option as follows:

    {
    "/api": {
    "target": "http://localhost:3000",
    "secure": false,
    "pathRewrite": {
    "^/api": ""
    },
    "logLevel": "debug"
    }
    }

    Possible options for logLevel include debug, info, warn, error, and silent (default is info)

    Multiple entries If you need to proxy multiple entries to the same target define the configuration in proxy.conf.js instead of proxy.conf.json e.g.

    
    const PROXY_CONFIG = [
    {
    context: [
    "/my",
    "/many",
    "/endpoints",
    "/i",
    "/need",
    "/to",
    "/proxy"
    ],
    target: "http://localhost:3000",
    secure: false
    }
    ]

    module.exports = PROXY_CONFIG;

    >Make sure to point to the right file (.js instead of .json):
    ```ts
    "architect": {
      "serve": {
        "builder": "@angular-devkit/build-angular:dev-server",
        "options": {
          "browserTarget": "your-application-name:build",
          "proxyConfig": "proxy.conf.js"
        },

    Bypass the Proxy If you need to optionally bypass the proxy, or dynamically change the request before it's sent, define the configuration in proxy.conf.js e.g.

    
    const PROXY_CONFIG = {
    "/api/proxy": {
    "target": "http://localhost:3000",
    "secure": false,
    "bypass": function (req, res, proxyOptions) {
    if (req.headers.accept.indexOf("html") !== -1) {
    console.log("Skipping proxy for browser request.");
    return "/index.html";
    }
    req.headers["X-Custom-Header"] = "yes";
    }
    }
    }

    module.exports = PROXY_CONFIG;

    >Using corporate proxy
    >If you work behind a corporate proxy, the regular configuration will not work if you try to proxy calls to any URL outside your local network.
    
    >In this case, you can configure the backend proxy to redirect calls through your corporate proxy using an agent:
    
    >npm install --save-dev https-proxy-agent
    >Then instead of using a proxy.conf.json file, we create a file called proxy.conf.js with the following content:
    ```ts
    var HttpsProxyAgent = require('https-proxy-agent');
    var proxyConfig = [{
      context: '/api',
      target: 'http://your-remote-server.com:3000',
      secure: false
    }];
    
    function setupForCorporateProxy(proxyConfig) {
      var proxyServer = process.env.http_proxy || process.env.HTTP_PROXY;
      if (proxyServer) {
        var agent = new HttpsProxyAgent(proxyServer);
        console.log('Using corporate proxy server: ' + proxyServer);
        proxyConfig.forEach(function(entry) {
          entry.agent = agent;
        });
      }
      return proxyConfig;
    }
    
    module.exports = setupForCorporateProxy(proxyConfig);

    This way if you have a http_proxy or HTTP_PROXY environment variable defined, an agent will automatically be added to pass calls through your corporate proxy when running npm start.

    WangShuXian6 commented 5 years ago

    Angular 7 上传图片

    upload.component.html

    <div class="example-full-width">
    <input placeholder="请上传公司logo" type="file"
    (change)="onFileUpload($event)" accept="image/*"
    name="logo" #selectFile>
    <img [src]="imagePreview" >
    </div>

    upload.component.ts

    
    import {Component, OnInit, ChangeDetectionStrategy} from '@angular/core';
    import {FormControl} from '@angular/forms';
    import {ENTER, COMMA} from '@angular/cdk/keycodes';
    import {Observable} from 'rxjs';
    import {startWith, map} from 'rxjs/operators';
    import {MatChipInputEvent} from '@angular/material';
    import {MatSnackBar} from '@angular/material';
    import {ImageService} from '../../services/image.service';

    export class State { constructor( private imageService: ImageService) { } }

    @Component({ selector: 'upload', templateUrl: './upload.component.html', styleUrls: ['./upload.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, styles: [` .example-container { display: flex; flex-direction: column; }

        .example-full-width {
            width: 100%;
        }
    
        .m-checkbox-inline > mat-checkbox {
            padding-right: 20px;
        }
    `]

    }) export class UploadComponent implements OnInit { public model: any = { name: '', phone: '', email: '', company: '', logo: '', number: '', id: '', password: '', rePassword: '' };

    selectedFile: File;
    imagePreview: string;
    
    constructor(
        private imageService: ImageService
    ) {
    }
    
    ngOnInit() {
    }
    
    onFileUpload(event) {
        this.selectedFile = event.target.files[0];
        this.imageService.uploadImage(this.selectedFile)
            .subscribe((imageData) => {
                console.log('imageData--', imageData);
            });
        // const reader = new FileReader();
        // reader.onload = () => {
        //     this.imagePreview = reader.result;
        // };
        // reader.readAsDataURL(this.selectedFile);
    }

    }

    
    >image.service.ts
    ```ts
    import {Injectable} from '@angular/core';
    import {HttpClient} from '@angular/common/http';
    import {ImageApi} from '../../config/api';
    
    @Injectable({
        providedIn: 'root'
    })
    export class ImageService {
    
        constructor(private http: HttpClient) {
        }
    
        uploadImage(selectedFile) {
            // return this.http.post(ImageApi, selectedFile);
            const uploadFormData = new FormData();
            uploadFormData.append('file', selectedFile, selectedFile.name);
            return this.http.post(ImageApi, uploadFormData);
        }
    }

    主app pages.module.ts

    
    import {LayoutModule} from '../layout/layout.module';
    import {NgModule} from '@angular/core';
    import {CommonModule} from '@angular/common';
    import {PagesRoutingModule} from './pages-routing.module';
    import {PagesComponent} from './pages.component';
    import {CoreModule} from '../../core/core.module';
    import {FormsModule} from '@angular/forms';
    import {HttpClientModule} from '@angular/common/http';

    @NgModule({ declarations: [ PagesComponent, ], imports: [ CommonModule, HttpClientModule, FormsModule, PagesRoutingModule, CoreModule, LayoutModule, ], providers: [] }) export class PagesModule { }

    WangShuXian6 commented 5 years ago

    模板引用变量

    
    <div class="example-full-width" *ngFor="let button of buttonList;index as i">
    <div class="clearfix">
    <nz-upload
    [nzAction]="imageApi"
    nzListType="picture-card"
    [(nzFileList)]="buttonList[i]"
    [nzShowButton]="buttonList[i].length < 6"
    [nzPreview]="handlePreviewButton"
    nzFileType="image/png,image/jpeg"
    (nzChange)="handleUploadButton($event,i)"
    [nzRemove]="handleRemoveButton">
    <i nz-icon type="plus"></i>
    <div class="ant-upload-text">Upload</div>
    </nz-upload>
    <div class="name-wrapper" *ngFor="let btn of buttonList[i];index as ii">
    <mat-form-field class="example-full-width">
    <input matInput  maxlength="10" placeholder="请输入按钮名称"
    required [(ngModel)]="buttonList[i][ii].buttonName"
    #series{{i}}{{ii}}="ngModel" name="btn-{{i}}-{{ii}}">
    <!--<mat-hint align="start"><strong>按钮名称</strong> </mat-hint>-->
    <!--<mat-hint align="end">{{message.value.length}} / 6</mat-hint>-->
    </mat-form-field>
    </div>
    </div>
                <div class="m-separator m-separator--dashed"></div>
    
                <mat-hint align="start"></mat-hint>
              </div>
    WangShuXian6 commented 5 years ago

    组件

    app.component.ts

    
    import { Component } from '@angular/core';

    @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent {

    }

    
    
    > - @Component:这是一个 Decorator(装饰器),其作用类似于 Java 里面的 Annotation(注解)。Decorator 这个特性目前处于 Stage 2(草稿)状态,还不是 ECMA 的正式规范,请点击这里查看具体详情。
    > - selector:组件的标签名,外部使用者可以这样来使用以上组件,<app-root>。默认情况下,ng 命令生成出来的组件都会带上一个 app 前缀,如果你不喜欢,可以在 angular-cli.json 里面修改 prefix 配置项,设置为空字符串将会不带任何前缀。
    > - templateUrl:引用外部 HTML 模板。如果你想直接编写内联模板,可以使用 template,支持 ES 6 引入的“模板字符串”写法,请点击这里查看具体详情。
    > - styleUrls:引用外部 CSS 样式文件,这是一个数组,也就意味着可以引用多份 CSS 文件。
    > - export class AppComponent:这是 ES 6 里面引入的模块和 class 定义方式。
    WangShuXian6 commented 5 years ago

    把 CSS 预编译器改成 Sass

    https://gitee.com/mumu-osc/learn-component

    手动修改

    angular-cli.json 里面的 styles.css 后缀改成 .scss 当你后面再使用 ng g c *** 自动创建组件的时候,默认就会生成 .scss 后缀的样式文件了 1

    angular-cli.json 里面的 styleExt 改成 .scss 2

    src 下面 style.css 改成 style.scss 3

    app.component.scss 4

    app.component.ts 里面对应修改 5

    WangShuXian6 commented 5 years ago

    组件模板

    6

    对比各种 JS 模板引擎的设计思路 Mustache(八字胡)语法 模板内的局部变量 属性绑定、事件绑定、双向绑定 在模板里面使用结构型指令 ngIf、ngFor、ngSwitch 在模板里面使用属性型指令 NgClass、NgStyle、NgModel 在模板里面使用管道格式化数据 一些小 Feature,如安全导航、非空断言


    “轻逻辑”,不能在模板里面编写非常复杂的 JavaScript 表达式。比如,Angular 的模板语法就有规定:

    不能在模板里面 new 对象 不能使用 =、+=、-= 这类的表达式 不能用 ++、-- 运算符 不能使用位运算符

    Angular 模板

    <ul>
    <li *ngFor="let race of races">
    {{race.name}}
    </li>
    </ul>

    浏览器不认识 *ngFor 和 {{...}} 这种语法,因此必须在浏览器里面进行“编译”,获得对应的模板函数,然后再把数据传递给模板函数,最终结合起来获得一堆 HTML 标签后才能把这一堆标签插入到 DOM 树里面去。

    如果启用了 AOT,处理的步骤有一些变化,@angular/cli 会对模板进行“静态编译”,避免在浏览器里面动态编译的过程。

    Handlebars 这种模板引擎完全是运行时编译模板字符串的,可以编写以下代码: 在运行时把模板字符串“编译”成了一个 JS 函数。

    
    //定义模板字符串
    var source=`
    <ul>
    {{#each races}}
    <li>{{name}}</li>
    {{/each}}
    </ul>
    `;

    //在运行时把模板字符串编译成 JS 函数 var templateFn=Handlebars.compile(source);

    //把数据传给模板函数,获得最终的 HTML var html=templateFn([ {name:'人族'}, {name:'神族'}, {name:'虫族'} ]);

    >Handlebars 是一款非常优秀的模板引擎,它在内部做了各种优化和缓存处理。模板字符串一般只会在第一次被调用的时候编译一次,Handlebars 会把编译好的函数缓存起来,后面再次调用的时候会从缓存里面获取,而不会多次进行“编译”。
    
    >必须提供一个 JS 版的“编译器”,让这个“编译器”运行在浏览器里面,这样才能在运行时把用户编写的模板字符串“编译”成模板函数。
    
    >有一些模板引擎会真的去用 JS 编写一款“编译器”出来,比如 Angular 和 Handlebars,它们确实都编写了一款 JS(TS)版的编译器。而有一些简单的模板引擎,例如 Underscore 里面的模板函数,只是用正则表达式做了字符串替换而已
    
    >Angular 每次要刷新组件外观的时候,都需要去调用一下模板函数,如果你在模板里面编写了非常复杂的代码,一定会增加渲染时间,用户一定会感到界面有“卡顿”
    
    >人眼的视觉延迟大约是 100 ~ 400 ms,如果整个页面的渲染时间超过了 400 ms,界面基本上就卡得没法用了。有一些做游戏的开发者会追求 60 fps 刷新率的细腻感觉,60 分之 1 秒约等于 16.7 ms,如果 UI 整体的渲染时间超过了 16.7 ms,就没法达到这个要求了。
    
    ***
    
    #### Mustache 语法
    
    >Mustache 语法也就是双花括号语法 {{...}},老外觉得它像八字胡子
    
    >它可以获取到组件里面定义的属性值
    >它可以自动计算简单的数学表达式,如加、减、乘、除、取模
    >它可以获得方法的返回值
    
    >插值语法关键代码实例:
    ```html
    <h3>
        欢迎来到{{title}}!
    </h3>
    public title = '假的星际争霸2'; 

    简单的数学表达式求值:

    <h3>1+1={{1+1}}</h3>

    调用组件里面定义的方法:

    <h3>可以调用方法{{getVal()}}</h3>
    public getVal():any{
    return 65535;
    }

    模板内的局部变量

    <input #heroInput>
    <p>{{heroInput.value}}</p>

    如果在模板里面定义的局部变量和组件内部的属性重名 Angular 会按照以下优先级来进行处理:

    模板局部变量 > 指令中的同名变量 > 组件中的同名属性

    这种优先级规则和 JSP 里面的变量取值规则非常类似


    属性绑定

    属性绑定是用方括号来做的,写法如下:

    <img [src]="imgSrc" />
    public imgSrc:string="./assets/imgs/1.jpg";

    很明显,这种绑定是单向的。


    事件绑定

    事件绑定是用圆括号来做的,写法如下:

    <button class="btn btn-success" (click)="btnClick($event)">测试事件</button>

    对应 Component 内部的方法定义:

    public btnClick(event):void{
    alert("测试事件绑定!");
    }

    双向绑定

    双向绑定是通过方括号里面套一个圆括号来做的,模板写法如下:

    <font-resizer [(size)]="fontSizePx"></font-resizer>

    对应组件内部的属性定义:

    public fontSizePx:number=14;

    Angular.js 是第一个把“双向数据绑定”这个特性带到前端来的框架

    “双向数据绑定”这个特性可以大幅度压缩前端代码的规模。 回想一下 jQuery 时代的做法,如果要实现类似的效果,是不是要自己去编写大量的代码?尤其是那种大规模的表单,一大堆的赋值和取值操作,都是非常丑陋的“面条”代码,而有了“双向数据绑定”特性之后,一个绑定表达式就能搞定。


    在模板里面使用结构型指令

    Angular 有 3 个内置的结构型指令:ngIf、ngFor、ngSwitch。ngSwitch 的语法比较啰嗦,使用频率小一些。

    *ngIf 代码实例:

    <p *ngIf="isShow" style="background-color:#ff3300">显示还是不显示?</p>
    <button class="btn btn-success" (click)="toggleShow()">控制显示隐藏</button>
    
    public isShow:boolean=true;

    public toggleShow():void{ this.isShow=!this.isShow; }

    
    >*ngFor 代码实例:
    ```html
    <li *ngFor="let race of races;let i=index;">
        {{i+1}}-{{race.name}}
    </li>
    public races:Array<any>=[
        {name:"人族"},
        {name:"虫族"},
        {name:"神族"}
    ];

    *ngSwitch 代码实例:

    <div [ngSwitch]="mapStatus">
    <p *ngSwitchCase="0">下载中...</p>
    <p *ngSwitchCase="1">正在读取...</p>
    <p *ngSwitchDefault>系统繁忙...</p>
    </div>
    public mapStatus:number=1;

    特别注意:一个 HTML 标签上只能同时使用一个结构型的指令。

    因为“结构型”指令会修改 DOM 结构,如果在一个标签上使用多个结构型指令,大家都一起去修改 DOM 结构,到时候到底谁说了算?

    那么需要在同一个 HTML 上使用多个结构型指令应该怎么办呢?有两个办法:

    1加一层空的 div 标签 2加一层 \<ng-container>


    在模板里面使用属性型指令

    使用频率比较高的 3 个内置指令分别是:NgClass、NgStyle 和 NgModel。

    关于 NgClass 的使用示例代码如下:

    <div [ngClass]="currentClasses">同时批量设置多个样式</div>
    <button class="btn btn-success" (click)="setCurrentClasses()">设置</button>
    
    public currentClasses: {};

    public canSave: boolean = true; public isUnchanged: boolean = true; public isSpecial: boolean = true;

    setCurrentClasses() { this.currentClasses = { 'saveable': this.canSave, 'modified': this.isUnchanged, 'special': this.isSpecial }; } .saveable{ font-size: 18px; } .modified { font-weight: bold; } .special{ background-color: #ff3300; }

    
    >关于 NgStyle 的使用示例代码如下:
    
    ```html
    <div class="color-wrapper" [ngStyle]="{'background-color':element.color}"></div>
    <div [ngStyle]="currentStyles">
        用 NgStyle 批量修改内联样式!
    </div>
    <button class="btn btn-success" (click)="setCurrentStyles()">设置</button>
    public currentStyles: {}
    public canSave:boolean=false;
    public isUnchanged:boolean=false;
    public isSpecial:boolean=false;
    
    setCurrentStyles() {
        this.currentStyles = {
            'font-style':  this.canSave      ? 'italic' : 'normal',
            'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
            'font-size':   this.isSpecial    ? '36px'   : '12px'
        };
    }

    ngStyle 这种方式相当于在代码里面写 CSS 样式,比较丑陋,违反了注意点分离的原则,而且将来不太好修改,非常不建议这样写。

    关于 NgModel 的使用示例代码如下:

    <p class="text-danger">ngModel 只能用在表单类的元素上面 </p>
    <input [(ngModel)]="currentRace.name">
    <p>{{currentRace.name}}</p>
    public currentRace:any={name:"随机种族"};

    请注意,如果需要使用 NgModel 来进行双向数据绑定,必须要在对应的模块里面 import FormsModule。


    管道

    管道的一个典型作用是用来格式化数据,举一个最简单的例子:

    {{currentTime | date:'yyyy-MM-dd HH:mm:ss'}}
    public currentTime: Date = new Date();

    Angular 里面一共内置了 12 个指令:

    angular 12

    自定义指令 https://angular.cn/guide/pipes https://angular.io/guide/pipes

    管道还有另一个典型的作用就是用来做国际化

    WangShuXian6 commented 5 years ago

    组件间通讯

    直接调用 @Input 和 @Output 利用 Service 单例进行通讯 利用 cookie 或者 localstorage 进行通讯 利用 Session 进行通讯

    在真实的应用中,组件最终会构成树形结构,就像人类社会中的家族树一样 在树形结构里面,组件之间有几种典型的关系:父子关系、兄弟关系、没有直接关系。

    相应地,组件之间也有以下几种典型的通讯方案:

    • 直接的父子关系,父组件直接访问子组件的 public 属性和方法
    • 直接的父子关系,借助于 @Input 和 @Output 进行通讯
    • 没有直接关系,借助于 Service 单例进行通讯
    • 利用 cookie 和 localstorage 进行通讯
    • 利用 Session 进行通讯

    直接调用

    对于有直接父子关系的组件,父组件可以直接访问子组件里面 public 型的属性和方法,示例代码片段如下:

    <child #child></child>
    <button (click)="child.childFn()" class="btn btn-success">调用子组件方法</button>

    显然,子组件里面必须暴露一个 public 型的 childFn 方法,就像这样:

    public childFn():void{
    console.log("子组件的名字是>"+this.panelTitle);
    }

    以上是通过在模板里面定义局部变量的方式来直接调用子组件里面的 public 型方法。 在父组件的内部也可以访问到子组件的实例,需要利用到 @ViewChild 装饰器,示例如下:

    @ViewChild(ChildComponent)
    private childComponent: ChildComponent;

    很明显,如果父组件直接访问子组件,那么两个组件之间的关系就会被固定死了。父子两个组件紧密依赖,谁也离不开谁,也就说都不能单独的使用。因此,除非你知道自己在做什么,最好不要在父组件里面直接访问子组件上的属性和方法,以免未来一改一大片的 bug。


    @Input 和 @Output

    我们可以利用 @Input 装饰器,让父组件直接给子组件传递参数,

    子组件上这样写:

    
    @Input()

    public panelTitle:string;

    
    >父组件上可以这样设置 panelTitle 这个参数:
    
    ```html
    <child panelTitle="一个新的标题"></child>

    @Output 的本质是事件机制,我们可以利用它来监听子组件上派发的事件,

    子组件上这样写:

    @Output()
    public follow=new EventEmitter<string>();

    触发 follow 事件的方式如下:

    this.follow.emit("follow");

    父组件上可以这样监听 follow 事件:

    <child (follow)="doSomething()"></child>

    我们可以利用 @Output 来自定义事件,监听自定义事件的方式也是通过小圆括号,与监听 HTML 原生事件的方式一模一样。


    利用 Service 单例进行通讯

    service

    如果你在根模块(一般是 app.module.ts)的 providers 里面注册一个 Service,那么这个 Service 就是全局单例的,这样一来我们就可以利用这个单例的 Service 在不同的组件之间进行通讯了。

    1比较粗暴的方式:我们可以在 Service 里面定义 public 型的共享变量,然后让不同的组件都来访问这块变量,从而达到共享数据的目的。

    2优雅一点的方式:利用 RxJS,在 Service 里面定义一个 public 型的 Subject(主题),然后让所有组件都来 Subscribe(订阅)这个主题,类似于一种“事件总线”的效果。

    实例代码

    
    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs/Observable';
    import { Subject } from 'rxjs/Subject';

    /**

    }

    
    ```ts
    import { Component, OnInit } from '@angular/core';
    import { EventBusService } from '../service/event-bus.service';
    
    @Component({
      selector: 'child-1',
      templateUrl: './child-1.component.html',
      styleUrls: ['./child-1.component.css']
    })
    export class Child1Component implements OnInit {
    
      constructor(public eventBusService:EventBusService) { }
    
      ngOnInit() {
      }
    
      public triggerEventBus():void{
        this.eventBusService.eventBus.next("第一个组件触发的事件");
      }
    }
    import { Component, OnInit } from '@angular/core';
    import { EventBusService } from '../service/event-bus.service';
    
    @Component({
      selector: 'child-2',
      templateUrl: './child-2.component.html',
      styleUrls: ['./child-2.component.css']
    })
    export class Child2Component implements OnInit {
      public events:Array<any>=[];
    
      constructor(public eventBusService:EventBusService) {
    
      }
    
      ngOnInit() {
        this.eventBusService.eventBus.subscribe((value)=>{
          this.events.push(value+"-"+new Date());
        });
      }
    }

    利用 cookie 或者 localstorage 进行通讯

    cookie localstorage

    public writeData():void{
        window.localStorage.setItem("json",JSON.stringify({name:'大漠穷秋',age:18}));
    }
    var json=window.localStorage.getItem("json");
    // window.localStorage.removeItem("json");
    var obj=JSON.parse(json);
    console.log(obj.name);
    console.log(obj.age);

    cookie、localstorage 这些东西都可以直接用原生的 API 进行操作。千万别忘记原生的那些 API 啊,都能用的!

    利用 Session 进行通讯

    session

    WangShuXian6 commented 5 years ago

    生命周期钩子

    https://angular.cn/guide/lifecycle-hooks https://angular.io/guide/lifecycle-hooks

    UI 组件的生命周期 Angular 组件的生命周期有什么特别的地方 OnPush 策略的使用方式 简要介绍脏检查的实现原理


    UI 组件的生命周期

    ui

    无论使用什么样的前端框架,只要编写 UI 组件,生命周期都是必须要考虑的重要内容。 设计 UI 系统,组件有几个重要的阶段一定是绕不开的

    • 初始化(init)阶段:在这个阶段需要把组件 new 出来,把一些属性设置上去等这些操作。
    • 渲染(render)阶段:在这个阶段需要把组件的模板和数据结合起来,生成 HTML 标签结构,并且要整合到现有的 DOM 树里面去。
    • 存活阶段:既然带有 UI,那么在组件的存活期内就一定会和用户进行交互。一般来说,带有 UI 的系统都是通过事件机制进行用户交互的。也就是说,这个阶段将会处理大量的用户事件,如鼠标点击、键盘按键、手指触摸。
    • 销毁(destory)阶段:最后,组件使用完了,需要把一些资源释放掉。最典型的操作是需要把组件上的所有事件全部清理干净,避免造成内存泄漏。

    在组件生命的不同阶段,框架一般会暴露出一些“接口”,开发者可以利用这些接口来实现一些自己的业务逻辑。这种接口在有些框架里面叫做“事件”,在 Angular 里面叫做“钩子”,但其底层的本质都是一样的。


    Angular 组件的生命周期钩子

    angular

    • Angular 一共暴露了 8 个“钩子”,构造函数不算。
    • 并没有组件或者指令会实现全部钩子。
    • 绿色的 4 个钩子可能会被执行很多次,紫色的只会执行一次。
    • Content 和 View 相关的 4 个钩子只对组件有效,指令上不能使用。因为在新版本的 Angular 里面,指令不能带有 HTML 模板,指令没有自己的 UI,当然就没有 View 和 Content 相关的“钩子”了。
    • 请不要在生命周期钩子里面实现复杂的业务逻辑,尤其是那 4 个会被反复执行的钩子,否则一定会造成界面卡顿。

    OnPush 策略

    在真实的业务系统中,组件会构成 Tree 型结构,就像这样: tree

    当某个叶子组件上的数据模型发生了变化之后,就像这样: default

    这时候,Angular 将会从根组件开始,遍历整颗组件树,把所有组件上的 ngDoCheck() 方法都调用一遍: angular ngdocheck

    请注意,默认情况下,无论哪个叶子组件上发生了变化,都会把整个组件树遍历一遍。如果组件树非常庞大,嵌套非常深,很明显会有效率问题。在绝大部分时间里面,并不会出现每个组件都需要刷新的情况,根本没有必要每次都去全部遍历。因此 Angular 提供了一种叫做 OnPush 的策略,只要把某个组件上的检测策略设置为 OnPush,就可以忽略整个子树了,就像这样: onpush

    很明显,使用了 OnPush 策略之后,检查效率将会获得大幅度的提升,尤其在组件的数量非常多的情况下: onpush

    Angular 内置的两种变更检测策略

    • Default:无论哪个组件发生了变化,从根组件开始全局遍历,调用每个组件上的 ngDoCheck() 钩子。
    • OnPush:只有当组件的 @Input 属性发生变化的时候才调用本组件的 ngDoCheck() 钩子。

    原理

    default

    Angular.js 是第一个把“双向数据绑定”这种设计带到前端领域来的框架,“双向数据绑定”最典型的场景就是对表单的处理。

    双向数据绑定的目标很明确:数据模型发生变化之后,界面可以自动刷新;用户修改了界面上的内容之后,数据模型也会发生自动修改。

    很明显,这里需要一种同步机制,在 Angular 里面这种同步机制叫做“变更检测”。

    在老版本 Agnular.js 里面,变更检测机制实现得不太完善,经常会出现检测不到变更的情况,所以才有了让大家很厌烦的 $apply() 调用。

    在新版本的 Angular 里面不再存在这个问题了,因为新版本的 Angular 使用 Zone.js 这个库,它会把所有可能导致数据模型发生变更的情况全部都拦截掉,从而在数据发生变化的时候去通知 Angular 进行刷新。

    在浏览器环境下,有可能导致数据模型发生变化的情况只有以下 3 种典型的回调。

    • 事件回调:鼠标、键盘、触摸
    • 定时器回调:setTimeout 和 setInterval
    • Ajax 回调

    Zone.js 覆盖了所有原生的实现,当开发者在调用这些函数的时候,并不是调用的原生方法,而是调用的 Zone.js 自己的实现,所以 Zone.js 就可以做一些自己的处理了。 详细解释 https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html


    import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
    
    @Component({
      selector: 'life-cycle',
      templateUrl: './life-cycle.component.html',
      styleUrls: ['./life-cycle.component.css'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class LifeCycleComponent implements OnInit {
      public logs: Array<string> = [];
    
      @Input()
      public title: string = "父层的传递进来的标题";
    
      constructor() {
        /**
         * 构造函数里面最好只做一些简单的赋值操作,不要做复杂的逻辑
         * 获取服务端数据、业务逻辑等不要实现在构造函数里面
         * 想知道详细的原因请看这篇解释http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/
         */
        this.logs.push("constructor...");
      }
    
      ngOnChanges() {
        console.log("ngOnChanges...");
        this.logs.push("ngOnChanges...");
      }
    
      ngOnInit() {
        this.logs.push("ngOnInit...");
      }
    
      ngDoCheck() {
        this.logs.push("ngDoCheck...");
      }
    
      ngAfterContentInit() {
        this.logs.push("ngAfterContentInit...");
      }
    
      ngAfterContentChecked() {
        this.logs.push("ngAfterContentChecked...");
      }
    
      ngAfterViewInit() {
        this.logs.push("ngAfterViewInit...");
      }
    
      ngAfterViewChecked() {
        this.logs.push("ngAfterViewChecked...");
      }
    
      ngOnDestory() {
        this.logs.push("ngOnDestory...");
      }
    }
    
    WangShuXian6 commented 5 years ago

    组件动效

    Angular 默认的动画模块使用的是 Web Animations 规范,这个规范目前处于 Editor’s Draft 状态(2017-09-22) https://drafts.csswg.org/web-animations/

    目前,各大浏览器厂商对 Web Animations 规范的支持并不好 ![Uploading 目前,各大浏览器厂商对 Web Animations 规范的支持并不好.png…]() http://caniuse.com/#feat=web-animation

    Web Animations 这套新的规范在 FireFox、Chrome、Opera 里面得到了完整的支持,而其他所有浏览器内核几乎都完全不支持,所以请慎重选择。我的建议是,请优先使用 CSS 3 规范里面的 anmimation 方案


    用法示范

    第一步:导入动画模块 default

    第二步:编写动效 default

    flyIn 是这个动效的名称,后面就可以在组件里面引用 flynIn 这个名字了。

    动效整体上是由“状态”和“转场”两个部分构成。

    • 以上代码里面的星号()表示“不可见状态”,void 表示任意状态,这是两种内置的状态,(=>void)表示进场动画,而(void=>*)表示离场动画。当然也可以定义自己的状态名称,注意不要和内置的状态名称发生冲突。
    • keyframes 里面的内容是关键帧的定义,语法和 CSS 3 里面定义动画的方式非常类似。

    第三步:在组件里面使用 flyIn 这个动效

    flyin

    flyin 2


    Angular 官方的动效文档 https://angular.io/guide/animations https://angular.cn/guide/animations

    如果不愿意自己编写动效,推荐这个开源项目,它和 Angular 之间结合得比较紧 https://github.com/jiayihu/ng-animate


    ani

    /src/app/app.module.ts

    
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

    import { AppComponent } from './app.component'; import { TestFlyInComponent } from './test-fly-in/test-fly-in.component';

    @NgModule({ declarations: [ AppComponent, TestFlyInComponent ], imports: [ BrowserModule, BrowserAnimationsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

    
    >/src/app/app.component.ts
    ```ts
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
    
    }
    

    /src/app/app.component.html

    <div class="container">
    <app-test-fly-in></app-test-fly-in>
    </div>

    /src/app/animations/fly-in.ts

    
    import { trigger, state, style, transition, animate, keyframes } from '@angular/animations';

    export const flyIn = trigger('flyIn', [ transition('void => ', [ animate(3000, keyframes([ style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }), style({ opacity: 1, transform: 'translateX(25px)', offset: 0.3 }), style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }) ])) ]), transition(' => void', [ animate(300, keyframes([ style({ opacity: 1, transform: 'translateX(0)', offset: 0 }), style({ opacity: 1, transform: 'translateX(-25px)', offset: 0.7 }), style({ opacity: 0, transform: 'translateX(100%)', offset: 1.0 }) ])) ]) ]);

    
    >/src/app/test-fly-in/test-fly-in.component.ts
    ```ts
    import { Component, OnInit } from '@angular/core';
    import { flyIn } from '../animations/fly-in';
    
    @Component({
      selector: 'app-test-fly-in',
      templateUrl: './test-fly-in.component.html',
      styleUrls: ['./test-fly-in.component.css'],
      animations:[flyIn]
    })
    export class TestFlyInComponent implements OnInit {
      state:string;
    
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    

    /src/app/test-fly-in/test-fly-in.component.html

    
    <div class="panel panel-primary" [@flyIn]="void">
    <div class="panel-heading">飞入效果</div>
    <div class="panel-body">
    飞入效果
    </div>
    </div>
    WangShuXian6 commented 5 years ago

    动态组件

    dynamic-component

    我们可以通过标签的方式使用组件,也可以通过代码的方式来动态创建组件。动态创建组件的过程是通过 ViewContainerRef 和 ComponentFactoryResolver 这两个工具类来配合完成的。

    我们可以定义一个这样的模板:

    <div #dyncomp></div>

    在组件定义里面需要首先 import 需要用到的工具类:

    import { Component, OnInit,ViewChild,ViewContainerRef,ComponentFactoryResolver, ComponentRef } from '@angular/core';

    组件内部这样写

    
    //这里引用模板里面定义的 dyncomp 容器标签
    @ViewChild("dyncomp",{read:ViewContainerRef})
    dyncomp:ViewContainerRef;

    comp1:ComponentRef; comp2:ComponentRef;

    constructor(private resolver:ComponentFactoryResolver) { }

    
    >然后就可以在 ngAfterContentInit 这个钩子里面用代码来动态创建组件了:
    ```ts
    ngAfterContentInit(){
        const childComp=this.resolver.resolveComponentFactory(Child11Component);
        this.comp1=this.dyncomp.createComponent(childComp); 
    }

    对于创建出来的 comp1 这个组件,可以通过代码直接访问它的 public 型属性,也可以通过代码来 subscribe(订阅)comp 1 上面发出来的事件,就像这样:

    this.comp1.instance.title="父层设置的新标题";
    this.comp1.instance.btnClick.subscribe((param)=>{
    console.log("--->"+param);
    });

    对于用代码动态创建出来的组件,我们可以通过调用 destory() 方法来手动销毁:

    public destoryChild():void{
    this.comp1.destroy();
    this.comp2.destroy();
    }

    注意:用代码动态创建组件这种方式在一般的业务开发里面不常用,而且可能存在一些隐藏的坑,如果你一定要用,请小心避雷。


    dm

    /src/app/app.module.ts

    
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';

    import { AppComponent } from './app.component'; import { DynamicCompComponent } from './dynamic-comp/dynamic-comp.component'; import { Child11Component } from './dynamic-comp/child11/child11.component';

    @NgModule({ declarations: [ AppComponent, DynamicCompComponent, Child11Component ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent], entryComponents:[Child11Component] }) export class AppModule { }

    >/src/app/app.component.html
    ```html
    <div class="container">
      <dynamic-comp></dynamic-comp>
    </div>

    /src/app/dynamic-comp/dynamic-comp.component.ts

    
    import {Component, OnInit, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentRef, AfterContentInit} from '@angular/core';
    import {Child11Component} from './child11/child11.component';
    import {state} from '@angular/animations';

    @Component({ selector: 'dynamic-comp', templateUrl: './dynamic-comp.component.html', styleUrls: ['./dynamic-comp.component.scss'] }) export class DynamicCompComponent implements OnInit, AfterContentInit { // 这里引用模板里面定义的dyncomp容器标签 @ViewChild('dyncomp', {read: ViewContainerRef}) dyncomp: ViewContainerRef;

    comp1: ComponentRef; comp2: ComponentRef;

    constructor( private resolver: ComponentFactoryResolver) {

    }

    ngOnInit() { }

    ngAfterContentInit() { console.log('动态创建组件的实例...'); const childComp = this.resolver.resolveComponentFactory(Child11Component); this.comp1 = this.dyncomp.createComponent(childComp); this.comp1.instance.title = '111'; this.comp1.instance.btnClick.subscribe((param) => { console.log('--->' + param); });

    // 可以创建多个组件实例出来
    // let temp1 = this.dyncomp.createComponent(childComp);
    // temp1.instance.title = "第2个动态子组件";
    // let temp2 = this.dyncomp.createComponent(childComp);
    // temp2.instance.title = "第3个动态子组件";
    // let temp3 = this.dyncomp.createComponent(childComp);
    // temp3.instance.title = "第4个动态子组件";
    // let temp4 = this.dyncomp.createComponent(childComp);
    // temp4.instance.title = "第5个动态子组件";
    // let temp5 = this.dyncomp.createComponent(childComp, 0);
    // temp5.instance.title = "第6个动态子组件";
    
    /**
     * createComponent方法可以调用很多次,会动态创建出多个组件实例
     * 方法有第二个参数,表示组件渲染的顺序
     */
    this.comp2 = this.dyncomp.createComponent(childComp);
    this.comp2.instance.title = '第二个子组件';

    }

    public destoryChild(): void { this.comp1.destroy(); this.comp2.destroy(); } }

    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">这是父组件</div>
      <div class="panel-body">
        <div #dyncomp></div>
        <button class="btn btn-danger" (click)="destoryChild()">销毁子组件</button>
      </div>
    </div>

    /src/app/dynamic-comp/child11/child11.component.ts

    
    import { Component, OnInit,Input, Output, EventEmitter } from '@angular/core';

    @Component({ selector: 'child11', templateUrl: './child11.component.html', styleUrls: ['./child11.component.scss'] }) export class Child11Component implements OnInit { @Input() public title:string="默认的标题";

    @Output() btnClick:EventEmitter=new EventEmitter();

    constructor() { }

    ngOnInit() { }

    public triggerEvent():void{ this.btnClick.emit("第一个子组件的点击事件..."); } }

    >/src/app/dynamic-comp/child11/child11.component.html
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">{{title}}</div>
      <div class="panel-body">
        <button class="btn btn-success" (click)="triggerEvent()">触发事件</button>
      </div>
    </div>
    WangShuXian6 commented 5 years ago

    ShadowDOM 组件

    shadowdom

    根据 Angular 官方的说法,Angular 组件的设计灵感来源于 Web Component,在 Web Component 里面,ShadowDOM 是重要的组成部分。在底层,Angular 渲染组件的方式有以下三种。

    • Native:采用 ShadowDOM 的模式来进行渲染。
    • Emulated:模拟模式。对于不能支持 ShadowDOM 模式的浏览器,Angular 在底层会采用模拟的方式来渲染组件,这是 Angular 默认的渲染模式。
    • None:不采用任何渲染模式。直接把组件的 HTML 结构和 CSS 样式插入到 DOM 流里面,这种方式很容易导致组件互相之间出现 CSS 命名污染的问题。

    在定义组件的时候,可以通过 encapsulation 配置项手动指定组件的渲染模式,关键代码如下

    @Component({
    selector: 'emulate-mode',
    encapsulation:ViewEncapsulation.Emulated,//默认模式
    templateUrl: './emulate-mode.component.html',
    styleUrls: ['./emulate-mode.component.scss']
    })

    效果 default

    ShadowDOM 模式的封装性更好,运行效率也更高。 ShadowDOM 在 W3C 的状态是 Working Draft(2017-09-22), ShadowDOM 目前只有 Chrome 和 Opera 支持得非常好,其他浏览器都非常糟糕

    一般来说,不需要自己手动指定组件的渲染模式,除非你自己知道在做什么

    WangShuXian6 commented 5 years ago

    内容投影

    ng-content

    最简单的组件模板 投影一块内容 投影多块内容 投影一个复杂的组件 内容投影这个特性存在的意义是什么


    最简单的组件模板

    假如我编写了一个这样的面板组件: default 组件对应的模板代码

    <div class="panel panel-primary">
    <div class="panel-heading">标题</div>
    <div class="panel-body">
    内容
    </div>
    <div class="panel-footer">
    底部
    </div>
    </div>

    投影一块内容

    但是,我希望把面板里面的标题设计成可变的,让调用者能把这个标题传进来,而不是直接写死。这时候 “内容投影” 机制就派上用场了,可以这样来编写组件的模板:

    <div class="panel panel-primary">
    <div class="panel-heading">
    <ng-content></ng-content>
    </div>
    <div class="panel-body">
    内容
    </div>
    <div class="panel-footer">
    底部
    </div>
    </div>

    请注意以上模板里面的 ,可以把它想象成一个占位符,我们用它先来占住一块空间,等使用方把参数传递进来之后,再用真实的内容来替换它。使用方可以这样来传递参数: 父组件

    <test-child-two>
    <h3>这是父层投影进来的内容</h3>
    </test-child-two>

    效果 标题的部分是由使用方从外部传递进来的 2


    投影多块内容

    我不仅希望面板的标题部分是动态的,还希望面板的主体区域和底部区域全部都是动态的

    <div class="panel panel-primary">
    <div class="panel-heading">
    <ng-content select="h3"></ng-content>
    </div>
    <div class="panel-body">
    <ng-content select=".my-class"></ng-content>
    </div>
    <div class="panel-footer">
    <ng-content select="p"></ng-content>
    </div>
    </div>

    然后使用方可以这样来使用我所编写的组件

    <test-child-two>
    <h3>这是父层投影进来的内容</h3>
    <p class="my-class">利用CSS选择器</p>
    <p>这是底部内容</p>
    </test-child-two>

    3


    里面的那个 select 参数,其作用和 CSS 选择器非常类似。 这种投影多块内容的方式叫 “多插槽模式”(multi-slot),可以把 想象成一个一个的插槽,内容会被插入到这些插槽里面。

    投影一个复杂的组件

    我不仅仅想投影简单的 HTML 标签到子层组件里面,还希望把自己编写的一个组件投影进去

    <div class="panel panel-primary">
    <div class="panel-heading">
    <ng-content select="h3"></ng-content>
    </div>
    <div class="panel-body">
    <ng-content select="test-child-three"></ng-content>
    </div>
    <div class="panel-footer">
    <ng-content select="p"></ng-content>
    </div>
    </div>

    使用方可以这样来使用这个组件

    <test-child-two>
    <h3>这是父层投影进来的内容</h3>
    <test-child-three (sayhello)="doSomething()"></test-child-three>
    <p>这是底部内容</p>
    </test-child-two>

    4

    请注意 里面的内容,把 select 属性设置成了子组件的名称。

    同时,对于被投影的组件 来说,我们同样可以利用小圆括号的方式来进行事件绑定,就像上面例子里的 (sayhello)="doSomething()" 这样


    内容投影这个特性存在的意义

    如果没有“内容投影”特性我们也能活得很好,那么它就没有存在的必要了,而事实并非如此,如果没有“内容投影”,有些事情就没法做了,典型的有两类:

    • 组件标签不能嵌套使用 -不能优雅地包装原生的 HTML 标签

    比如编写了两个组件 my-comp-1 和 my-comp-2,如果没有内容投影,这两个组件就没办法嵌套使用,比如这样用就不行

    <my-comp-1>
    <my-comp-2></my-comp-2>
    </my-comp-1>

    因为没有“内容投影”机制,my-comp-1 无法感知到 my-comp-2 的存在,也无法和它进行交互。这明显有违 HTML 设计的初衷,因为 HTML 的本质是一种 XML 格式,标签能嵌套是最基本的特性,原生的 HTML 本身就有很多嵌套的情况:

    <ul>
      <li>神族</li>
      <li>人族</li>
      <li>虫族</li>
    </ul>

    在真实的业务开发里面,另一个典型的嵌套组件就是 Tab 页,以下代码是很常见的: 如果没有内容投影机制,想要这样嵌套地使用自定义标签也是不可能的。

    <tab>
    <pane title="第一个标签页"/>
    <pane title="第二个标签页"/>
    <pane title="第三个标签页"/>
    </tab>

    内容投影存在的第二个意义与组件的封装有关。

    虽然 Angular 提供了 @Component 装饰器让开发者可以自定义标签,但是请不要忘记,自定义标签毕竟与 HTML 原生标签不一样,原生 HTML 标签上面默认带有很多属性、事件,而我自己定义的标签是没有的。

    从宏观的角度看,所有的自定义标签都只不过是一层“虚拟的壳子”,浏览器并不认识自定义标签,真正渲染出来的还是 div、form、input 之类的原生标签。因此,自定义标签只不过是一层逻辑上的抽象和包装,让人类更容易理解和组织自己的代码而已。

    既然如此,自定义标签和 HTML 原生标签之间的关系是什么呢?本质上说,这是“装饰模式”的一种应用,而内容投影存在的意义就是可以让这个“装饰”的过程做得更加省力、更加优雅一些。


    default

    /src/app/app.module.ts

    
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';

    import { AppComponent } from './app.component'; import { NgContentComponent } from './ng-content/ng-content.component'; import { ChildTwoComponent } from './ng-content/child-two/child-two.component'; import { ChildThreeComponent } from './ng-content/child-three/child-three.component';

    @NgModule({ declarations: [ AppComponent, NgContentComponent, ChildTwoComponent, ChildThreeComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

    
    >/src/app/ng-content/ng-content.component.ts
    ```ts
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'test-ng-content',
      templateUrl: './ng-content.component.html',
      styleUrls: ['./ng-content.component.scss']
    })
    export class NgContentComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
      public doSomething():void{
        alert("最外层sayhello!");
      }
    }
    

    /src/app/ng-content/ng-content.component.html

    <div class="panel panel-primary">
    <div class="panel-heading">父组件</div>
    <div class="panel-body">
    <test-child-two>
    <h3>这是父层投影进来的内容</h3>
    <test-child-three (sayhello)="doSomething()"></test-child-three>
    <p>这是底部内容</p>
    </test-child-two>
    </div>
    </div>

    /src/app/ng-content/child-two/child-two.component.ts

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

    @Component({ selector: 'test-child-two', templateUrl: './child-two.component.html', styleUrls: ['./child-two.component.scss'] }) export class ChildTwoComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/src/app/ng-content/child-two/child-two.component.html
    ```html
    <div class="panel panel-primary">
        <div class="panel-heading">
            <ng-content select="h3"></ng-content>
        </div>
        <div class="panel-body">
            <ng-content select="test-child-three"></ng-content>
        </div>
        <div class="panel-footer">
            <ng-content select="p"></ng-content>
        </div>
    </div>

    /src/app/ng-content/child-three/child-three.component.ts

    
    import { Component, OnInit,Output,EventEmitter } from '@angular/core';

    @Component({ selector: 'test-child-three', templateUrl: './child-three.component.html', styleUrls: ['./child-three.component.scss'] }) export class ChildThreeComponent implements OnInit { @Output() public sayhello:EventEmitter=new EventEmitter();

    constructor() { }

    ngOnInit() { }

    public sayHello():void{ this.sayhello.emit("sayhello"); } }

    >/src/app/ng-content/child-three/child-three.component.html
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">
        这是被投影的自定义组件
      </div>
      <div class="panel-body">
        <button class="btn btn-success" (click)="sayHello()">sayhello</button>
      </div>
    </div>
    WangShuXian6 commented 5 years ago

    @ContentChild & @ContentChildren

    contentchild

    @ContentChild

    我们可以利用 @ContentChild 这个装饰器来操控被投影进来的组件。

    <child-one>
    <child-two></child-two>
    </child-one>
    
    import { Component, ContentChild, ContentChildren, ElementRef, OnInit, QueryList } from '@angular/core';

    //注解的写法 @ContentChild(ChildTwoComponent) childTwo:ChildTwoComponent;

    //在 ngAfterContentInit 钩子里面访问被投影进来的组件 ngAfterContentInit():void{ console.log(this.childTwo); //这里还可以访问 this.childTwo 的 public 型方法,监听 this.childTwo 所派发出来的事件 }

    
    ***
    
    #### @ContentChildren
    >从名字可以看出来,@ContentChildren 是一个复数形式。当被投影进来的是一个组件列表的时候,我们可以用 @ContentChildren 来进行操控。
    
    ```html
    <child-one>
        <child-two></child-two>
        <child-two></child-two>
        <child-two></child-two>
        <child-two></child-two>
        <child-two></child-two>
        <child-two></child-two>
        <child-two></child-two>
        <child-two></child-two>
    </child-one>
    import { Component, ContentChild, ContentChildren, ElementRef, OnInit, QueryList } from '@angular/core';
    
    //这时候不是单个组件,是一个列表 QueryList
    @ContentChildren(ChildTwoComponent) 
    childrenTwo:QueryList<ChildTwoComponent>;
    
    //遍历列表
    ngAfterContentInit():void{
        this.childrenTwo.forEach((item)=>{
            console.log(item);
        });
    }

    contentchildren

    /src/app/test-content-child/test-content-child.component.ts

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

    @Component({ selector: 'test-content-child', templateUrl: './test-content-child.component.html', styleUrls: ['./test-content-child.component.scss'] }) export class TestContentChildComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/src/app/test-content-child/test-content-child.component.html
    ```html
    <div class="panel panel-primary">
        <div class="panel-heading">父组件</div>
        <div class="panel-body">
            <child-one>
              <h3>投影的标题</h3>
              <p>投影的底部</p>
              <child-two></child-two>
              <child-two></child-two>
              <child-two></child-two>
              <child-two></child-two>
              <child-two></child-two>
              <child-two></child-two>
              <child-two></child-two>
              <child-two></child-two>
            </child-one>
        </div>
      </div>

    /src/app/test-content-child/child-one/child-one.component.ts

    
    import { Component, ContentChild, ContentChildren, ElementRef, OnInit, QueryList } from '@angular/core';
    import { ChildTwoComponent } from '../child-two/child-two.component';

    @Component({ selector: 'child-one', templateUrl: './child-one.component.html', styleUrls: ['./child-one.component.scss'] }) export class ChildOneComponent implements OnInit { // @ContentChild(ChildTwoComponent) // childTwo:ChildTwoComponent; @ContentChildren(ChildTwoComponent) childrenTwo: QueryList;

    constructor() { }

    ngOnInit() {

    }

    ngAfterContentInit(): void { // console.log(this.childTwo); this.childrenTwo.forEach((item) => { console.log(item); }); } }

    >/src/app/test-content-child/child-one/child-one.component.html
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">
        <ng-content select="h3"></ng-content>
      </div>
      <div class="panel-body">
        <ng-content select="child-two"></ng-content>
      </div>
      <div class="panel-footer">
        <ng-content select="p"></ng-content>
      </div>
    </div>

    /src/app/test-content-child/child-two/child-two.component.ts

    
    import { Component, OnInit, EventEmitter } from '@angular/core';

    @Component({ selector: 'child-two', templateUrl: './child-two.component.html', styleUrls: ['./child-two.component.scss'] }) export class ChildTwoComponent implements OnInit { constructor() { }

    ngOnInit() { }

    public sayHello(): void { console.log("Hello 大漠穷秋!"); } }

    >/src/app/test-content-child/child-two/child-two.component.html
    ```html
    <div class="panel panel-primary">
        <div class="panel-heading">被投影的子组件</div>
        <div class="panel-body">
        </div>
      </div>
    WangShuXian6 commented 5 years ago

    @ViewChild & @ViewChildren

    viewchild

    @ViewChild

    我们可以利用 @ViewChild 这个装饰器来操控直属的子组件。

    <div class="panel panel-primary">
    <div class="panel-heading">父组件</div>
    <div class="panel-body">
    <child-one></child-one>
    </div>
    </div>
    
    import { Component, OnInit, ViewChild, ViewChildren, QueryList } from '@angular/core';

    @ViewChild(ChildOneComponent) childOne:ChildOneComponent;

    //在 ngAfterViewInit 这个钩子里面可以直接访问子组件 ngAfterViewInit():void{ console.log(this.childOne); //用代码的方式订阅子组件上的事件 this.childOne.helloEvent.subscribe((param)=>{ console.log(this.childOne.title); }); }

    
    #### @ViewChildren
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">父组件</div>
      <div class="panel-body">
        <child-one></child-one>
        <child-one></child-one>
        <child-one></child-one>
        <child-one></child-one>
        <child-one></child-one>
      </div>
    </div>
    import { Component, OnInit, ViewChild, ViewChildren, QueryList } from '@angular/core';
    
    @ViewChildren(ChildOneComponent)
    children:QueryList<ChildOneComponent>;
    
    ngAfterViewInit():void{
        this.children.forEach((item)=>{
            // console.log(item);
            //动态监听子组件的事件
            item.helloEvent.subscribe((data)=>{
            console.log(data);
            });
        });
    }

    viewchild

    /src/app/test-view-child/test-view-child.component.ts

    
    import { Component, OnInit, ViewChild, ViewChildren, QueryList } from '@angular/core';
    import { ChildOneComponent } from './child-one/child-one.component';

    @Component({ selector: 'test-view-child', templateUrl: './test-view-child.component.html', styleUrls: ['./test-view-child.component.scss'] }) export class TestViewChildComponent implements OnInit { // @ViewChild(ChildOneComponent) // childOne:ChildOneComponent; @ViewChildren(ChildOneComponent) children: QueryList;

    constructor() { }

    ngOnInit() { }

    ngAfterViewInit(): void { // console.log(this.childOne); // this.childOne.helloEvent.subscribe((param)=>{ // console.log(this.childOne.title); // }); this.children.forEach((item) => { // console.log(item); //动态监听子组件的事件 item.helloEvent.subscribe((data) => { console.log(data); }); }); } }

    >/src/app/test-view-child/test-view-child.component.html
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">父组件</div>
      <div class="panel-body">
        <child-one></child-one>
        <child-one></child-one>
        <child-one></child-one>
        <child-one></child-one>
        <child-one></child-one>
      </div>
    </div>
    
    <div class="panel" contenteditable="true">
    </div>

    /src/app/test-view-child/child-one/child-one.component.ts

    
    import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

    @Component({ selector: 'child-one', templateUrl: './child-one.component.html', styleUrls: ['./child-one.component.scss'] }) export class ChildOneComponent implements OnInit { public id: string;

    @Input() public title: string = "我是ChildOne";

    @Output() helloEvent: EventEmitter = new EventEmitter();

    constructor() { this.id = "ID-" + Math.floor(Math.random() * 100000000); }

    ngOnInit() { }

    public sayHello(): void { this.helloEvent.emit(this.id); } }

    >/src/app/test-view-child/child-one/child-one.component.html
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">{{title}}</div>
      <div class="panel-body">
          <button class="btn btn-success" (click)="sayHello()">触发事件</button>
      </div>
    </div>
    WangShuXian6 commented 5 years ago

    与 Polymer 封装组件的方式简单对比

    一些开发者认为 Angular 的组件设计不如 Polymer 那种直接继承原生 HTMLElement 的方式优雅

    Polymer 组件的定义方式:

    polymer

    Polymer 的根类 Polymer.Element 的源代码: polymer polymer element

    在 Polymer 中,开发者自定义标签的地位与浏览器原生标签完全是平等的,属性、事件、行为,都是平等的,Polymer 组件的渲染由浏览器内核直接完成。

    Polymer 的这种封装方式和目前市面上的大部分前端框架都不一样,Polymer 直接继承原生 HTML 元素,而其他大部分框架都只是在“包装”、“装饰”原生 HTML 元素,这是两种完全不同的设计哲学。

    https://www.polymer-project.org

    WangShuXian6 commented 5 years ago

    封装并发布组件库

    市面上可用的 Angular 组件库介绍

    开源免费的组件库:

    • PrimeNG,这款组件库做得比较早,代码质量比较高。
    • NG-Zorro 来自阿里云团队,外观是 AntDesign 风格。
    • Clarity 来自 Vmware 团队。
    • Angular-Material,Angular 官方提供的组件库。目前(2017 年 10 月)一共规划了 38 个组件,Material Design 风格,代码质量比较高。
    • Element-Angular,作者来自饿了么团队。
    • Jigsaw(七巧板),来自 ZTE 中兴通讯,组件数量比较多,外观不够漂亮。
    • Ionic,专门为移动端打造的组件库,自带周边工具,生态很完善。

    收费版组件库:

    • 来自 Telerik 的 KendoUI for Angular,Telerik 的这套组件库的特色是组件的功能比较强大,尤其是 Grid,做的非常强。

    如何在项目里面引入开源组件库

    以 PrimeNG 为例,首先在 package.json 里面定义好依赖: primeng package json

    然后打开终端用 cnpm install 安装 PrimeNG 到本地,在你自己的业务模块里面 import 需要用到的组件模块就好了: default

    如何把组件库发布到 npm 上去

    npm 官方有一个固定的 registry url,可以把它的作用想象成一个 App Store,全球所有开发者编写的 Node 模块都需要发布上去,然后其他人才能安装使用

    如果你开发了一个很强大的 Angular 组件库,希望发布到 Node 上面让其他人也能使用,应该怎么做呢?简略地处理步骤如下:

    • 第 1 步,用 npm init 初始化项目(只要项目里面按照 npm 的规范编写一份 package.json 文件就可以了,不一定用 npm init 初始化);
    • 第 2 步,编写你自己的代码;
    • 第 3 步,到 https://www.npmjs.com/ 网站上注册一个账号;
    • 第 4 步,用 npm publish 把项目 push 上去。

    publish 之后,全球开发者都可以通过名字查找并安装你的这个模块了

    经验

    两个常见的误区

    第一个误区是:开源组件可以满足你的所有需求,我可以负责任地告诉你,这是不可能的!开源组件库都是通用型的组件,并不会针对任何特定的行业或者领域进行设计。无论选择哪一款开源组件库,组件的外观 CSS 总要重新写一套的吧?组件里面缺的那些功能总得自己去写吧?组件里面的那些 bug 总得自己去改掉吧?因此,千万不要幻想开源组件能帮你解决所有问题,二次开发是必然的。

    第二个误区是:开发组件库很简单,分分钟可以搞定。在 jQuery 时代,有一款功能极其强大的树组件叫 zTree,点击这里查看详情,你能想到的那些功能 zTree 都实现了,而且运行效率特别高。但是你要知道,zTree 的作者已经花了超过 5 年的时间来维护这个组件,维护一个组件尚且如此,何况要长期维护一个庞大的库呢?因此,做好一个组件库并不像有些人想象的那么轻松,这件事是需要花钱、花时间的。做开源,最让使用者头疼的不是功能够不够强大,而是开发者突然弃坑,这也是很多企业宁愿花钱自己开发组件库的原因。如果你只是单兵作战,最好选一款现有的开源库,在此基础上继续开发,强烈建议你只做一个组件,就像 zTree 的作者那样,把一个组件做好、做透,并且长期维护下去,这比搞一个庞大的组件库,每个组件做得都像个玩具,然后突然弃坑要好很多。


    WangShuXian6 commented 5 years ago

    指令

    master

    https://angular.io/guide/attribute-directives https://angular.cn/guide/attribute-directives

    组件与指令之间的关系

    default

    核心源代码

    default

    根据 Angular 官方文档的描述,Angular 里面有 3 种类型的指令。

    • Component 是 Directive 的子接口,是一种特殊的指令,Component 可以带有 HTML 模板,Directive 不能有模板。
    • 属性型指令:用来修改 DOM 元素的外观和行为,但是不会改变 DOM 结构,Angular 内置指令里面典型的属性型指令有 ngClass、ngStyle,如果打算封装自己的组件库,属性型指令是必备的内容。
    • 结构型指令:可以修改 DOM 结构,内置的常用结构型指令有 ngFor、ngIf 和 NgSwitch。由于结构型指令会修改 DOM 结构,因而同一个 HTML 标签上面不能同时使用多个结构型指令,否则大家都来改 DOM 结构,到底听谁的呢?如果要在同一个 HTML 元素上面使用多个结构性指令,可以考虑加一层空的元素来嵌套,比如在外面套一层空的 \<ng-container>\<\/ng-container>,或者套一层空的 \<div>。

    上面这些框架走的是组件化的路子,Swing 和 ExtJS 完全是“代码流”,所有 UI 都通过代码来创建;而 Flex 和 React 是“标签流”,也就通过标签的方式来创建 UI。

    但是,所有这些框架都没有“指令”这个概念,为什么 Angular 里面一定要引入“指令”这个概念呢?

    根本原因是:我们需要用指令来增强标签的功能,包括 HTML 原生标签和你自己自定义的标签。

    若想给原生的 HTML 标签再扩展一些属性,比方说:你想给

    标签增加一个自定义的属性叫做 my-high-light,当鼠标进入 div 内部时,div 的背景就会高亮显示,可以这样使用

    这时候,没有指令机制就无法实现了。


    自定义指令

    官方文档里面的一个例子,运行效果 default

    核心代码

    
    import { Directive, ElementRef,HostListener,HostBinding,Input } from '@angular/core';

    @Directive({ selector: '[my-high-light]' }) export class MyHighLightDirective { @Input() highlightColor: string;

    constructor(private el: ElementRef) { }

    @HostListener('mouseenter') onMouseEnter() { this.highlight(this.highlightColor); }

    @HostListener('mouseleave') onMouseLeave() { this.highlight(null); }

    private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; } }

    
    >以上指令的用法如下:
    ```html
    <p my-high-light highlightColor="#ff3300">内容高亮显示!</p>

    自定义结构型指令

    这个例子会动态创建 3 个组件,每个延迟 500 毫秒,运行效果如下: default

    指令代码

    
    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

    @Directive({ selector: '[appDelay]' }) export class DelayDirective { constructor( private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef ) { }

    @Input() set appDelay(time: number) {
        setTimeout(() => {
            this.viewContainerRef.createEmbeddedView(this.templateRef);
        }, time);
    }

    }

    
    >指令的用法核心代码:
    >结构性指令在使用的时候前面都会带上星号,即使是你自定义的结构性指令,也是一样的。
    ```html
    <div *ngFor="let item of [1,2,3]">
        <card *appDelay="500 * item">
            第 {{item}} 张卡片
        </card>
    </div>

    default

    /src/app/directives/delay.directive.ts

    
    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

    @Directive({ selector: '[appDelay]' }) export class DelayDirective { constructor( private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef ) { }

    @Input() set appDelay(time: number) {
        setTimeout(() => {
            this.viewContainerRef.createEmbeddedView(this.templateRef);
        }, time);
    }

    }

    
    >/src/app/directives/my-high-light.directive.ts
    ```ts
    import { Directive, ElementRef,HostListener,HostBinding,Input } from '@angular/core';
    
    @Directive({
      selector: '[my-high-light]'
    })
    export class MyHighLightDirective {
      @Input() 
      highlightColor: string;
    
      // @HostBinding("style.border")
      // border:string;
    
      @HostBinding("class")
      myClass:string;
    
      constructor(private el: ElementRef) {
        console.log(el);
        el.nativeElement.style.backgroundColor = '#ff3300';
      }
    
      @HostListener('mouseenter') onMouseEnter() {
        this.highlight(this.highlightColor);
      }
    
      @HostListener('mouseleave') onMouseLeave() {
        this.highlight(null);
      }
    
      private highlight(color: string) {
        this.el.nativeElement.style.backgroundColor = color;
      }
    
      // @HostListener('click') onClick() {
      //   if(this.myClass){
      //     this.myClass="";
      //   }else{
      //     this.myClass="my-border";
      //   }
      // }
    }
    

    /src/app/app.module.ts

    
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';

    import { AppComponent } from './app.component'; import { MyHighLightDirective } from './directives/my-high-light.directive'; import { DelayDirective } from './directives/delay.directive'; import { CardComponent } from './card/card.component';

    @NgModule({ declarations: [ AppComponent, MyHighLightDirective, DelayDirective, CardComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

    
    >/src/app/app.component.ts
    ```ts
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
    }
    

    /src/app/app.component.html

    <div class="container">
    <h1>My First Attribute Directive</h1>
    <p my-high-light highlightColor="#ff3300">内容高亮显示!</p>
    <div my-high-light highlightColor="#00ff00">这是DIV里面的内容</div>
    <span my-high-light highlightColor="#0000ff">这是span里面的内容</span>
    <div *ngFor="let item of [1,2,3]">
    <card *appDelay="500 * item">
    第 {{item}} 张卡片
    </card>
    </div>
    </div>
    WangShuXian6 commented 5 years ago

    直接在组件里面操作 DOM

    test-component

    既然组件是指令的子类,那么指令里面能干的事儿组件应该都能干,可以在指令里面直接操作 DOM

    直接在组件里面来实现背景高亮效果,关键代码如下:

    
    @Component({
    selector: 'test',
    templateUrl: './test.component.html',
    styleUrls: ['./test.component.scss']
    })
    export class TestComponent implements OnInit {
    @Input() 
    highlightColor: string;

    private containerEl:any;

    constructor(private el: ElementRef) {

    }

    ngOnInit() { }

    ngAfterContentInit() { console.log(this.el.nativeElement); console.log(this.el.nativeElement.childNodes); console.log(this.el.nativeElement.childNodes[0]); console.log(this.el.nativeElement.innerHTML);

    this.containerEl=this.el.nativeElement.childNodes[0];

    }

    @HostListener('mouseenter') onMouseEnter() { this.highlight(this.highlightColor); }

    @HostListener('mouseleave') onMouseLeave() { this.highlight(null); }

    private highlight(color: string) { this.containerEl.style.backgroundColor = color; } }

    
    >组件的标签结构如下:
    ```html
    <div class="my-container">
      鼠标移进来就会改变背景
    </div>

    这个组件的使用方式如下:

    <div class="container">
    <test highlightColor="#F2DEDE"></test>
    </div>

    直接在组件里面操作 DOM 是可以的,但是一旦把操作 DOM 的这部分逻辑放在组件里面,就没法再在其他标签上面使用了。


    WangShuXian6 commented 5 years ago

    模块 @NgModule

    default

    https://angular.cn/guide/ngmodules

    @NgModule 的定义方式

    一个最简单的 @NgModule 的定义:

    
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';

    import { AppComponent } from './app.component'; import { TestViewChildComponent } from './test-view-child/test-view-child.component'; import { ChildOneComponent } from './test-view-child/child-one/child-one.component';

    @NgModule({ declarations: [ AppComponent, TestViewChildComponent, ChildOneComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

    
    
    >其中:
    
    > - declarations,用来放组件、指令、管道的声明;
    > - imports,用来导入外部模块;
    > - providers,需要使用的 Service 都放在这里;
    > - bootstrap,定义启动组件,你可能注意到了这个配置项是一个数组,也就是说可以指定做个组件作为启动点,但是这种用法是很罕见的。
    
    ***
    
    #### @NgModule 的重要作用
    
    > - NgModule 最根本的意义是帮助开发者组织业务代码,开发者可以利用 NgModule 把关系比较紧密的组件组织到一起,这是首要的。
    > - NgModule 用来控制组件、指令、管道等的可见性,处于同一个 NgModule 里面的组件默认互相可见,而对于外部的组件来说,只能看到 NgModule 导出(exports)的内容,这一特性非常类似 Java 里面 package 的概念。也就是说,如果你定义的 NgModule 不 exports 任何内容,那么外部使用者即使 import 了你这个模块,也没法使用里面定义的任何内容。
    > - NgModule 是 @angular/cli 打包的最小单位。打包的时候,@angular/cli 会检查所有 @NgModule 和路由配置,如果你配置了异步模块,cli 会自动把模块切分成独立的 chunk(块)。这一点是和其他框架不同的,其他框架基本上都需要你自己去配置 Webpack,自己定义切分 chunck 的规则;而在 Angular 里面,打包和切分的动作是 @angular/cli 自动处理的,不需要你干预。当然,如果你感到不爽,也可以自己从头用 Webpack 配一个环境出来,因为 @angular/cli 底层也是用的 webpack。
    > - NgModule 是 Router 进行异步加载的最小单位,Router 能加载的最小单位是模块,而不是组件。当然,模块里面只放一个组件是允许的,很多组件库都是这样做的。
    
    ***
    
    #### @NgModule 的注意点
    
    > - 每个应用至少有一个根模块,按照惯例,根模块的名字一般都叫 AppModule,如果没有非常特别的理由,就不要随意改这个名字了,这相当于一个国际惯例。
    > - 组件、指令、管道都必须属于一个模块,而且只能属于一个模块。
    > - NgModule 和 ES 6 里面的 Module 是完全不同的两个概念。ES 6 里面的模块是通过 export 和 import 来进行声明的,它们是语法层面的内容;而 NgModule 完全不是这个概念,从上面的作用列表也能看出来。最重要的一点,目前,ES 6 里面的 import 只能静态引入模块,并不能异步动态加载模块,而 NgModule 可以配合 Router 来进行异步模块加载,在后面的 Router 这一课里面会有实例代码。
    > - 模块的定义方式会影响依赖注入机制:对于直接 import 的同步模块,无论把 @Injectable 类型的组件定义在哪个模块里面,它都是全局可见的。比如,在子模块 post.module.ts 的 providers 数组里面定义了一个 PostListService,可能会觉得这个 PostListService 只有在 post.module.ts 里面可见。而事实并非如此,PostListService 是全局可见的,就相当于一个全局单例。与此对应,如果把 PostListService 定义到一个异步加载的模块里面,它就不是全局可见的了,因为对于异步加载进来的模块,Angular 会为它创建独立的 DI(依赖注入)上下文。所以,如果你想让 PostListService 全局可见,应该把它定义在根模块 app.module 里面。同时要特别注意,如果希望 PostListService 是全局单例的,只能在 app.module 里面的 providers 数组里面定义一次,而不能在其他模块里面再次定义,否则就会出现多个不同的实例。
    >关于 DI 机制更详细的描述请参见这里。https://angular.cn/guide/dependency-injection
    
    ***
    
    #### Angular 内核中的目录结构
    
    >Angular 本身也是用模块化的方式开发的,当你用 cnpm install 装好了开发环境之后,可以打开 node_modules 目录查看整体结构。左侧第一列 6 个目录比较常用,这个顺序是按照我自己的理解排列的,按照常用程度从上到下。
    ![angular](https://user-images.githubusercontent.com/30850497/49346848-1d9a8b80-f6d3-11e8-98c8-878c5aab76b0.png)
    >注意,一个目录里面可能会放多个模块,比如 forms 目录里面就有 FormsModule 和 ReactiveFormsModule 两个模块。
    
    >Form 模块的关键类结构图如下:
    ![form](https://user-images.githubusercontent.com/30850497/49346853-36a33c80-f6d3-11e8-809a-89ad8c14a415.png)
    WangShuXian6 commented 5 years ago

    ng路由

    Angular 中的 Router 模块会负责模块的加载、组件的初始化、销毁等操作,它是整个乐队的总指挥

    前端为什么要路由

    • 如果没有 Router,浏览器的前进后退按钮没法用。做过后台管理系统的开发者应该遇到过这种场景,整个系统只有一个 login.jsp 和 index.jsp,用户从 login.jsp 登录完成之后,跳转到 index.jsp 上面,然后浏览器地址栏里面的 URL 就一直停留在 index.jsp 上面,页面内部的所有内容全部通过 Ajax 进行刷新。这种处理方式实际上把浏览器的 URL 机制给废掉了,整个系统只有一个 URL,用户完全无法通过浏览器的前进、后退按钮进行导航。
    • 如果没有 Router,你将无法把 URL 复制并分享给你的朋友。比如,在某段子网站上看到了一个很搞笑的内容,你把 URL 复制下来分享给了你的朋友,如果这个段子网站没有做好路由机制,你的朋友将无法顺利打开这个链接。

    Router 的本质是记录当前页面的状态,它和当前页面上展示的内容一一对应。

    在 Angular 里面,Router 是一个独立的模块,定义在 @angular/router 模块里面,它有以下重要的作用:

    • Router 可以配合 NgModule 进行模块的懒加载、预加载操作;
    • Router 会管理组件的生命周期,它会负责创建、销毁组件。

    服务端的配置

    很多开发者会遇到这个问题:代码在开发状态运行得好好的,但是部署到真实的环境上之后所有路由都 404。

    这是一个非常典型的问题,你需要配置一下 Server 才能很好地支持前端路由。

    既然启用了前端路由,也就意味着浏览器地址栏里面的那些 URL 在 Server 端并没有真正的资源和它对应,直接访问过去当然 404 了。

    以 Tomcat 为例,需要在 web.xml 里面加一段配置:

    <error-page>
    <error-code>404</error-code>
    <location>/</location>
    </error-page>

    这意思就是告诉 Tomcat,对于 404 这种事你别管了,直接扔回前端去。由于 Angular 已经在浏览器里面接管了路由机制,因而接下来就由 Angular 来负责了。

    如果你正在使用其他的 Web 容器,请从这个链接里面查找对应的配置方式,在 How to: Configure your server to work with html5Mode 这个小节里面把常见的 Web 容器的配置方式都列举出来了,包括 IIS、Apache、Nginx、NodeJS、Tomcat 全部都有,过去抄过来就行。

    Angular 新版本的路由机制极其强大,除了能支持无限嵌套之外,还能支持模块懒加载、预加载、权限控制、辅助路由等高级功能

    Angular Router 模块的作者是 Victor Savkin,他的个人 Blog 请单击这里,他专门编写了一本小薄书来完整描述 Angular 路由模块的设计思路和运行原理,这本书只有 151 页,如果有兴趣请点这里查看

    WangShuXian6 commented 5 years ago

    路由基本用法

    default

    效果 default 代码结构: 2

    app.routes.ts 里面就是路由规则配置

    
    import { RouterModule } from '@angular/router';
    import {HomeComponent} from './home/home.component';
    import {JokesComponent} from './jokes/jokes.component';

    export const appRoutes=[ { path:'', redirectTo:'home', pathMatch:'full' }, { path:'home', component:HomeComponent }, { path:'jokes', component:JokesComponent }, { path:'**',//通配符匹配必须写在最后一个 component:HomeComponent } ];

    
    >app.module.ts 里面首先需要 import 这份路由配置文件:
    ```ts
    import { appRoutes } from './app.routes';

    然后 @NgModule 里面的 imports 配置项内容如下:

    import { appRoutes } from './app.routes';
    imports: [
    BrowserModule,
    RouterModule.forRoot(appRoutes,{enableTracing:true})
    ]

    HTML 模板 html

    • 整个导航过程是通过 RouterModule、app.routes.ts、routerLink、router-outlet 这几个东西一起配合完成的。

    • 请单击顶部导航条,观察浏览器地址栏里面 URL 的变化,这里体现的是 Router 模块最重要的作用,就是对 URL 和对应界面状态的管理。

    • 请注意路由配置文件 app.routes.ts 里面的写法,里面全部用的 component 配置项,这种方式叫“同步路由”。也就是说,@angular/cli 在编译的时候不会把组件切分到独立的 chunk(块)文件里面去,当然也不会异步加载,所有的组件都会被打包到一份 js 文件里面去

    • 通配符配置必须写在最后一项,否则会导致路由无效。


    路由与懒加载模块

    default

    提升 JS 文件的加载速度、提升 JS 文件的执行效率。

    对于一些大型的后台管理系统来说,里面可能会有上千份 JS 文件,如果把所有 JS 全部都压缩到一份文件里面,那么这份文件的体积可能会超过 5M,这是不能接受的,尤其对于移动端应用。

    因此,一个很自然的想法就是:我们能不能按照业务功能把这些 JS 打包成多份 JS 文件,当用户导航到某个路径的时候,再去异步加载对应的 JS 文件。对于大型的系统来说,用户在使用的过程中不太可能会用到所有功能,所以这种方式可以非常有效地提升系统的加载和运行效率。

    我们来把上面这个简单的例子改成异步模式,把“主页”和“段子”切分成两个独立的模块,并且做成异步加载的模式。

    整体代码结构改成这样: default 给 home 和 jokes 分别加了一个 module 文件和一个 routes 文件。

    home.module.ts

    
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    import { homeRoutes } from './home.routes';

    @NgModule({ declarations: [ HomeComponent ], imports: [ RouterModule.forChild(homeRoutes) ], providers: [], bootstrap: [] }) export class HomeModule { }

    
    >home.routes.ts
    ```ts
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    
    export const homeRoutes=[
        {
            path:'',
            component:HomeComponent
        }
    ];

    jokes 模块相关的代码类似

    最重要的修改在 app.routes.ts 里面,路由的配置变成了这样:

    
    import { RouterModule } from '@angular/router';

    export const appRoutes=[ { path:'', redirectTo:'home', pathMatch:'full' }, { path:'home', loadChildren:'./home/home.module#HomeModule' }, { path:'jokes', loadChildren:'./jokes/jokes.module#JokesModule' }, { path:'**', loadChildren:'./home/home.module#HomeModule' } ];

    
    >我们把原来的 component 配置项改成了 loadChildren。
    
    ***
    
    #### N 层嵌套路由
    
    >修改上面的例子,给它加一个二级菜单
    ![default](https://user-images.githubusercontent.com/30850497/49347332-5a1cb600-f6d8-11e8-9f49-96dd4f659793.png)
    
    >于是 home 模块的代码结构变成了这样:
    ![home](https://user-images.githubusercontent.com/30850497/49347338-69036880-f6d8-11e8-96ed-a8997b990eae.png)
    
    >重点的变化在 home.routes.ts 里面:
    ```ts
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    import { PictureComponent } from './picture/picture.component';
    import { TextComponent } from './text/text.component';
    
    export const homeRoutes=[
        {
            path:'',
            component:HomeComponent,
            children:[
                {
                    path:'',
                    redirectTo:'pictures',
                    pathMatch:'full'
                },
                {
                    path:'pictures',
                    component:PictureComponent
                },
                {
                    path:'text',
                    component:TextComponent
                }
            ]
        }
    ];

    理论上,路由可以无限嵌套


    共享模块

    在页面的侧边栏上面加一个展示用户资料的 Panel(面板) 这个展示用户资料的 Panel 在“段子”这个模块里面也要用,而且未来还可能在其他地方也要使用。 default

    根据 Angular 的规定:组件必须定义在某个模块里面,但是不能同时属于多个模块。

    如果你把这个 UserInfo 面板定义在 home.module 里面,jokes.module 就不能使用了,反之亦然。

    把 UserInfo 定义在根模块 app.module 里面 确实可以这样做。但是这样会造成一个问题:如果系统的功能不断增多,你总不能把所有共用的组件都放到 app.module 里面吧?如果真的这样搞,app.module 最终打包出来会变得非常胖。

    更优雅的做法是切分一个“共享模块”出来,就像这样: default 对于所有想使用 UserInfo 的模块来说,只要 import 这个 SharedModule 就可以了


    处理路由事件

    Angular 的路由上面暴露了 7 个事件:NavigationStart 、RoutesRecognized、RouteConfigLoadStart 、RouteConfigLoadEnd、NavigationEnd、NavigationCancel、NavigationError

    https://angular.io/guide/router#router-events

    我们可以监听这些事件,来实现一些自己的业务逻辑,示例如下:

    
    import { Component, OnInit } from '@angular/core';
    import { Router,NavigationStart } from '@angular/router';

    @Component({ selector: 'home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit {

    constructor(private router: Router) {

    }

    ngOnInit() { this.router.events.subscribe((event) => { console.log(event); //可以用instanceof来判断事件的类型,然后去做你想要做的事情 console.log(event instanceof NavigationStart); }); } }

    >https://angular.io/guide/router#router-events
    
    ***
    
    #### 如何传递和获取路由参数
    >Angular 的 Router 可以传递两种类型的参数:简单类型的参数、“矩阵式”参数。
    
    >routerLink 的写法
    ```html
    <ul class="nav navbar-nav">
        <li routerLinkActive="active" class="dropdown">
            <a [routerLink]="['home','1']">主页</a>
        </li>
        <li routerLinkActive="active" class="dropdown">
            <a [routerLink]="['jokes',{id:111,name:'damo'}]">段子</a>
        </li>
    </ul>

    在 HomeComponent 里面,我们是这样来获取简单参数的:

    
    constructor(
    public router:Router,
    public activeRoute: ActivatedRoute) { 

    }

    ngOnInit() { this.activeRoute.params.subscribe( (params)=>{console.log(params)} ); }

    
    >在 JokesComponent 里面,我们是这样来接受“矩阵式”参数的:
    ```ts
    constructor(
        public router: Router,
        public activeRoute: ActivatedRoute) {
    
    }
    
    ngOnInit() {
        this.activeRoute.params.subscribe(
            (params) => { console.log(params) }
        );
    }

    矩阵式”传参 [routerLink]="['jokes',{id:111,name:'damo'}]" 对应的 URL 是这样一种形态:

    http://localhost:4200/jokes;id=111;name=damo

    是合法的。它不是 W3C 的规范,但是互联网之父 Tim Berners-Lee 在 1996 年的文档里面有详细的解释,主流浏览器都是支持的 这种方式的好处是,我们可以传递大块的参数,因为第二个参数可以是一个 JSON 格式的对象。


    用代码触发路由导航

    除了通过 主页 这种方式进行导航之外,还可以通过代码的方式来手动进行导航:

    this.router.navigate(["/jokes"],{ queryParams: { page: 1,name:222 } });

    接受参数的方式如下:

    this.activeRoute.queryParams.subscribe(
    (queryParam) => { console.log(queryParam) }
    );

    basic

    /src/app/app.routes.ts

    
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home/home.component';
    import { JokesComponent } from './jokes/jokes.component';

    export const appRoutes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: 'jokes', component: JokesComponent }, { path: '**',//通配符匹配必须写在最后一个 component: HomeComponent } ];

    >/src/app/app.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent } from './app.component';
    import { HomeComponent } from './home/home.component';
    import { JokesComponent } from './jokes/jokes.component';
    
    import { appRoutes } from './app.routes';
    
    @NgModule({
      declarations: [
        AppComponent,
        HomeComponent,
        JokesComponent
      ],
      imports: [
        BrowserModule,
        RouterModule.forRoot(appRoutes,{enableTracing:true})
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    /src/app/app.component.ts

    
    import { Component } from '@angular/core';

    @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { }

    >/src/app/app.component.html
    ```html
    <!-- 顶部导航 -->
    <div class="navbar navbar-fixed-top main-nav" role="navigation">
      <div class="container">
          <div class="navbar-header">
              <button #button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse">
                  <span class="sr-only">Toggle Navigation</span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
              </button>
              <a routerLink="home" class="navbar-brand navbar-brand-my">
                  NiceFish
              </a>
          </div>
          <div class="collapse navbar-collapse navbar-responsive-collapse" aria-expanded="false">
              <ul class="nav navbar-nav">
                  <li routerLinkActive="active" class="dropdown">
                      <a routerLink="home">主页</a>
                  </li>
                  <li routerLinkActive="active" class="dropdown">
                    <a routerLink="jokes">段子</a>
                  </li>
              </ul>
          </div>
      </div>
    </div>
    <!-- 主体内容区域 -->
    <div class="container main-container">
      <router-outlet></router-outlet>
    </div>
    <!-- 底部区域 -->
    <div class="footer bs-footer">
      <div class="container">
          <div class="row">
              <p>
                  Powered by <a href="http://git.oschina.net/mumu-osc/NiceFish" target="_blank">NiceFish</a>
              </p>
          </div>
      </div>
    </div>
    

    /src/app/home/home.component.ts

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

    @Component({ selector: 'home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/src/app/home/home.component.html
    ```html
    <h3>
      这里是首页。
    </h3>
    <div class="list-group">
      <a href="#" class="list-group-item active">
        Cras justo odio
      </a>
      <a href="#" class="list-group-item">Dapibus ac facilisis in</a>
      <a href="#" class="list-group-item">Morbi leo risus</a>
      <a href="#" class="list-group-item">Porta ac consectetur ac</a>
      <a href="#" class="list-group-item">Vestibulum at eros</a>
    </div>

    /src/app/jokes/jokes.component.ts

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

    @Component({ selector: 'jokes', templateUrl: './jokes.component.html', styleUrls: ['./jokes.component.scss'] }) export class JokesComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/src/app/jokes/jokes.component.html
    ```html
    <h3>
      这里是段子。
    </h3>
    

    async-module

    async-module

    /src/app/app.routes.ts

    
    import { RouterModule } from '@angular/router';

    export const appRoutes=[ { path:'', redirectTo:'home', pathMatch:'full' }, { path:'home', loadChildren:'./home/home.module#HomeModule' }, { path:'jokes', loadChildren:'./jokes/jokes.module#JokesModule' }, { path:'**', loadChildren:'./home/home.module#HomeModule' } ];

    >/src/app/app.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent } from './app.component';
    import { appRoutes } from './app.routes';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        RouterModule.forRoot(appRoutes)
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    /src/app/app.component.ts

    
    import { Component } from '@angular/core';

    @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { }

    >/src/app/app.component.html
    ```html
    <!-- 顶部导航 -->
    <div class="navbar navbar-fixed-top main-nav" role="navigation">
      <div class="container">
          <div class="navbar-header">
              <button #button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse">
                  <span class="sr-only">Toggle Navigation</span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
              </button>
              <a routerLink="home" class="navbar-brand navbar-brand-my">
                  NiceFish
              </a>
          </div>
          <div class="collapse navbar-collapse navbar-responsive-collapse" aria-expanded="false">
              <ul class="nav navbar-nav">
                  <li routerLinkActive="active" class="dropdown">
                      <a routerLink="home">主页</a>
                  </li>
                  <li routerLinkActive="active" class="dropdown">
                    <a routerLink="jokes">段子</a>
                  </li>
              </ul>
          </div>
      </div>
    </div>
    <!-- 主体内容区域 -->
    <div class="container main-container">
      <router-outlet></router-outlet>
    </div>
    <!-- 底部区域 -->
    <div class="footer bs-footer">
      <div class="container">
          <div class="row">
              <p>
                  Powered by <a href="http://git.oschina.net/mumu-osc/NiceFish" target="_blank">NiceFish</a>
              </p>
          </div>
      </div>
    </div>
    

    /src/app/jokes/jokes.routes.ts

    
    import { RouterModule } from '@angular/router';
    import { JokesComponent } from './jokes.component';

    export const jokesRoutes=[ { path:'', component:JokesComponent } ];

    >/src/app/jokes/jokes.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { JokesComponent } from './jokes.component';
    import { jokesRoutes } from './jokes.routes';
    
    @NgModule({
      declarations: [
        JokesComponent
      ],
      imports: [
        RouterModule.forChild(jokesRoutes)
      ],
      providers: [],
      bootstrap: []
    })
    export class JokesModule { }
    

    /src/app/jokes/jokes.component.ts

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

    @Component({ selector: 'jokes', templateUrl: './jokes.component.html', styleUrls: ['./jokes.component.scss'] }) export class JokesComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/src/app/jokes/jokes.component.html
    ```html
    <p>
      这里是段子!
    </p>
    

    /src/app/home/home.routes.ts

    
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';

    export const homeRoutes=[ { path:'', component:HomeComponent } ];

    >/src/app/home/home.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    import { homeRoutes } from './home.routes';
    
    @NgModule({
      declarations: [
        HomeComponent
      ],
      imports: [
        RouterModule.forChild(homeRoutes)
      ],
      providers: [],
      bootstrap: []
    })
    export class HomeModule { }
    

    /src/app/home/home.component.ts

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

    @Component({ selector: 'home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/src/app/home/home.component.html
    ```html
    
    <p>
      默认跳转到首页!
    </p>
    

    nested-router

    nested-router

    /src/app/home/home.routes.ts

    
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    import { PictureComponent } from './picture/picture.component';
    import { TextComponent } from './text/text.component';

    export const homeRoutes = [ { path: '', component: HomeComponent, children: [ { path: '', redirectTo: 'pictures', pathMatch: 'full' }, { path: 'pictures', component: PictureComponent }, { path: 'text', component: TextComponent }, { path: '**', component: PictureComponent } ] } ];

    >/src/app/home/home.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';
    import { homeRoutes } from './home.routes';
    import { PictureComponent } from './picture/picture.component';
    import { TextComponent } from './text/text.component';
    
    @NgModule({
      declarations: [
        HomeComponent,
        PictureComponent,
        TextComponent
      ],
      imports: [
        RouterModule.forChild(homeRoutes)
      ],
      providers: [],
      bootstrap: []
    })
    export class HomeModule { }
    

    shared-module

    shared-module

    /Users/wangshuxian/test/learn-router/src/app/app.routes.ts

    
    import { RouterModule } from '@angular/router';

    export const appRoutes=[ { path:'', redirectTo:'home', pathMatch:'full' }, { path:'home', loadChildren:'./home/home.module#HomeModule' }, { path:'jokes', loadChildren:'./jokes/jokes.module#JokesModule' }, { path:'**', loadChildren:'./home/home.module#HomeModule' } ];

    >/Users/wangshuxian/test/learn-router/src/app/app.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent } from './app.component';
    import { appRoutes } from './app.routes';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        RouterModule.forRoot(appRoutes)
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    /Users/wangshuxian/test/learn-router/src/app/app.component.ts

    
    import { Component } from '@angular/core';

    @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { title = 'app'; }

    >/Users/wangshuxian/test/learn-router/src/app/app.component.html
    ```html
    <!-- 顶部导航 -->
    <div class="navbar navbar-fixed-top main-nav" role="navigation">
      <div class="container">
          <div class="navbar-header">
              <button #button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse" (click)="toggle(button)">
                  <span class="sr-only">Toggle Navigation</span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
              </button>
              <a routerLink="home" class="navbar-brand navbar-brand-my">
                  NiceFish
              </a>
          </div>
          <div class="collapse navbar-collapse navbar-responsive-collapse" aria-expanded="false">
              <ul class="nav navbar-nav">
                  <li routerLinkActive="active" class="dropdown">
                      <a routerLink="home">主页</a>
                  </li>
                  <li routerLinkActive="active" class="dropdown">
                    <a routerLink="jokes">段子</a>
                  </li>
              </ul>
          </div>
      </div>
    </div>
    <!-- 主体内容区域 -->
    <div class="container main-container">
      <router-outlet></router-outlet>
    </div>
    <!-- 底部区域 -->
    <div class="footer bs-footer">
      <div class="container">
          <div class="row">
              <p>
                  Powered by <a href="http://git.oschina.net/mumu-osc/NiceFish" target="_blank">NiceFish</a>
              </p>
          </div>
      </div>
    </div>
    

    /Users/wangshuxian/test/learn-router/src/app/shared/shared.module.ts

    
    import { NgModule } from '@angular/core';
    import { UserInfoComponent } from '../user-info/user-info.component';
    import { OrderInfoComponent } from '../order-info/order-info.component';

    @NgModule({ declarations: [ UserInfoComponent, OrderInfoComponent ], imports: [], exports:[ UserInfoComponent, OrderInfoComponent ], providers: [], bootstrap: [] }) export class SharedModule { }

    
    >/Users/wangshuxian/test/learn-router/src/app/user-info/user-info.component.ts
    ```ts
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'user-info',
      templateUrl: './user-info.component.html',
      styleUrls: ['./user-info.component.scss']
    })
    export class UserInfoComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    

    /Users/wangshuxian/test/learn-router/src/app/user-info/user-info.component.html

    <div class="panel panel-primary">
    <div class="panel-heading">用户资料</div>
    <div class="panel-body">
    这是用来展示用户资料的组件
    </div>
    </div>

    /Users/wangshuxian/test/learn-router/src/app/order-info/order-info.component.ts

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

    @Component({ selector: 'order-info', templateUrl: './order-info.component.html', styleUrls: ['./order-info.component.scss'] }) export class OrderInfoComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/Users/wangshuxian/test/learn-router/src/app/order-info/order-info.component.html
    ```html
    <div class="panel panel-primary">
      <div class="panel-heading">订单数据</div>
      <div class="panel-body">
        这里用来展示订单数据!
      </div>
    </div>

    /Users/wangshuxian/test/learn-router/src/app/jokes/jokes.routes.ts

    
    import { RouterModule } from '@angular/router';
    import { JokesComponent } from './jokes.component';

    export const jokesRoutes=[ { path:'', component:JokesComponent } ];

    >/Users/wangshuxian/test/learn-router/src/app/jokes/jokes.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { SharedModule } from '../shared/shared.module';
    import { JokesComponent } from './jokes.component';
    import { jokesRoutes } from './jokes.routes';
    
    @NgModule({
      declarations: [
        JokesComponent
      ],
      imports: [
        SharedModule,
        RouterModule.forChild(jokesRoutes)
      ],
      providers: [],
      bootstrap: []
    })
    export class JokesModule { }
    

    /Users/wangshuxian/test/learn-router/src/app/jokes/jokes.component.ts

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

    @Component({ selector: 'jokes', templateUrl: './jokes.component.html', styleUrls: ['./jokes.component.scss'] }) export class JokesComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/Users/wangshuxian/test/learn-router/src/app/jokes/jokes.component.html
    ```html
    <div class="row">
      <div class="col-xs-3">
        <user-info></user-info>
      </div>
      <div class="col-xs-9">
        <h3>
          这里是段子!
        </h3>
      </div>
    </div>
    <!-- <order-info></order-info> -->

    /Users/wangshuxian/test/learn-router/src/app/home/home.routes.ts

    
    import { RouterModule } from '@angular/router';
    import { HomeComponent } from './home.component';

    export const homeRoutes=[ { path:'', component:HomeComponent } ];

    >/Users/wangshuxian/test/learn-router/src/app/home/home.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { SharedModule } from '../shared/shared.module';
    import { HomeComponent } from './home.component';
    import { homeRoutes } from './home.routes';
    
    @NgModule({
      declarations: [
        HomeComponent    
      ],
      imports: [
        SharedModule,
        RouterModule.forChild(homeRoutes)
      ],
      providers: [],
      bootstrap: []
    })
    export class HomeModule { }
    

    /Users/wangshuxian/test/learn-router/src/app/home/home.component.ts

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

    @Component({ selector: 'home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit {

    constructor() { }

    ngOnInit() { }

    }

    >/Users/wangshuxian/test/learn-router/src/app/home/home.component.html
    ```html
    <div class="row">
      <div class="col-xs-3">
        <user-info></user-info>
      </div>
      <div class="col-xs-9">
        <h3>
          默认跳转到首页!
        </h3>
      </div>
    </div>
    <!-- <order-info></order-info> -->

    router-events


    router-params

    router-params

    /Users/wangshuxian/test/learn-router/src/app/app.routes.ts

    
    import { RouterModule } from '@angular/router';
    import {HomeComponent} from './home/home.component';
    import {JokesComponent} from './jokes/jokes.component';

    export const appRoutes=[ { path:'', redirectTo:'home/1', pathMatch:'full' }, { path:'home/:page', component:HomeComponent }, { path:'jokes', component:JokesComponent }, { path:'**', component:HomeComponent } ];

    >/Users/wangshuxian/test/learn-router/src/app/app.module.ts
    ```ts
    import { NgModule } from '@angular/core';
    import { RouterModule } from '@angular/router';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent } from './app.component';
    import { appRoutes } from './app.routes';
    import { HomeComponent } from './home/home.component';
    import { JokesComponent } from './jokes/jokes.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        HomeComponent,
        JokesComponent
      ],
      imports: [
        BrowserModule,
        RouterModule.forRoot(appRoutes)
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    /Users/wangshuxian/test/learn-router/src/app/app.component.ts

    
    import { Component } from '@angular/core';

    @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent {

    }

    >/Users/wangshuxian/test/learn-router/src/app/app.component.html
    ```html
    <!-- 顶部导航 -->
    <div class="navbar navbar-fixed-top main-nav" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button #button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse">
                  <span class="sr-only">Toggle Navigation</span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
              </button>
                <a [routerLink]="['home','1']" class="navbar-brand navbar-brand-my">
                  NiceFish
              </a>
            </div>
            <div class="collapse navbar-collapse navbar-responsive-collapse" aria-expanded="false">
                <ul class="nav navbar-nav">
                    <li routerLinkActive="active" class="dropdown">
                        <a [routerLink]="['home','1']">主页</a>
                    </li>
                    <li routerLinkActive="active" class="dropdown">
                        <a [routerLink]="['jokes',{id:111,name:'damo'}]">段子</a>
                    </li>
                </ul>
            </div>
        </div>
    </div>
    <!-- 主体内容区域 -->
    <div class="container main-container">
        <router-outlet></router-outlet>
    </div>
    <!-- 底部区域 -->
    <div class="footer bs-footer">
        <div class="container">
            <div class="row">
                <p>
                    Powered by <a href="http://git.oschina.net/mumu-osc/NiceFish" target="_blank">NiceFish</a>
                </p>
            </div>
        </div>
    </div>

    /Users/wangshuxian/test/learn-router/src/app/jokes/jokes.component.ts

    
    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute, Router, Params } from '@angular/router';

    @Component({ selector: 'jokes', templateUrl: './jokes.component.html', styleUrls: ['./jokes.component.scss'] }) export class JokesComponent implements OnInit {

    constructor( public router: Router, public activeRoute: ActivatedRoute ) { }

    ngOnInit() { this.activeRoute.queryParams.subscribe( (queryParam) => { console.log(queryParam) } ); this.activeRoute.params.subscribe( (params) => { console.log(params) } ); } }

    >/Users/wangshuxian/test/learn-router/src/app/jokes/jokes.component.html
    ```ts
    <h3>
      这里是段子!
    </h3>
    

    /Users/wangshuxian/test/learn-router/src/app/home/home.component.ts

    
    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute, Router, Params } from '@angular/router';

    @Component({ selector: 'home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit {

    constructor( public router: Router, public activeRoute: ActivatedRoute) {

    }

    ngOnInit() { this.activeRoute.params.subscribe( (params) => { console.log(params) } ); }

    public manualNav(): void { // this.router.navigateByUrl("/jokes"); //navigate方法不支持矩阵式参数 this.router.navigate(["/jokes"], { queryParams: { page: 1, name: 222 } }); } }

    >/Users/wangshuxian/test/learn-router/src/app/home/home.component.html
    ```html
    <h3>
      默认跳转到首页!
    </h3>
    <button class="btn btn-primary" (click)="manualNav()">手动导航</button>

    WangShuXian6 commented 5 years ago

    模块预加载

    Angular 内置了两种预加载策略:PreloadAllModules 和 NoPreloading, PreloadAllModules 的意思是:预加载所有模块,不管有没有被访问到。 也就是说,要么就一次预加载所有异步模块,要么就彻底不做预加载。

    关键

    import { RouterModule, PreloadAllModules } from '@angular/router';
    RouterModule.forRoot(appRoutes,{preloadingStrategy:PreloadAllModules})

    app.module.ts

    
    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { RouterModule, PreloadAllModules } from '@angular/router';

    import { AppComponent } from './app.component'; import { MyPreloadingStrategy } from './common/my-preloading-strategy';

    import { appRoutes } from './app.routes';

    @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes,{preloadingStrategy:PreloadAllModules}) // ,RouterModule.forRoot(appRoutes,{preloadingStrategy:MyPreloadingStrategy}) ], providers: [MyPreloadingStrategy], bootstrap: [AppComponent] }) export class AppModule { }

    
    ***
    ##### 实现自己的预加载策略,最好能在路由配置里面加入一些自定义的配置项,让某些模块预加载、某些模块不要进行预加载
    
    >当 preload 这个配置项为 true 的时候,就去预加载对应的模块,否则什么也不做,于是你实现了一个自己的预加载策略
    ```ts
    {
        path:'jokes',
        data:{preload:true},
        loadChildren:'./jokes/jokes.module#JokesModule'
    },
    {
        path:'picture',
        data:{preload:false},
        loadChildren:'./picture/picture.module#PictureModule'
    }

    my-preloading-strategy.ts

    
    import { Route,PreloadingStrategy } from '@angular/router';
    import { Observable } from "rxjs";
    import "rxjs/add/observable/of";

    export class MyPreloadingStrategy implements PreloadingStrategy { preload(route: Route, fn: () => Observable): Observable{ return route.data&&route.data.preload?fn():Observable.of(null); } }

    
    >修改一下 app.module.ts 里面的配置,换成你自己的预加载策略
    >可以继续修改这个预加载策略,比如用加个延时,或者根据其他某个业务条件来决定是不是要执行预加载
    ```ts
    RouterModule.forRoot(appRoutes,{preloadingStrategy:MyPreloadingStrategy})

    完整代码 src/app/app.module.ts

    
    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { RouterModule, PreloadAllModules } from '@angular/router';

    import { AppComponent } from './app.component'; import { MyPreloadingStrategy } from './common/my-preloading-strategy';

    import { appRoutes } from './app.routes';

    @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes,{preloadingStrategy:PreloadAllModules}) // ,RouterModule.forRoot(appRoutes,{preloadingStrategy:MyPreloadingStrategy}) ], providers: [MyPreloadingStrategy], bootstrap: [AppComponent] }) export class AppModule { }

    
    >src/app/app.routes.ts
    ```ts
    import { RouterModule } from '@angular/router';
    
    export const appRoutes=[
        {
            path:'',
            redirectTo:'home',
            pathMatch:'full'
        },
        {
            path:'home',
            loadChildren:'./home/home.module#HomeModule'
        },
        {
            path:'jokes',
            data:{preload:true},
            loadChildren:'./jokes/jokes.module#JokesModule'
        },
        {
            path:'picture',
            data:{preload:false},
            loadChildren:'./picture/picture.module#PictureModule'
        },
        {
            path:'**',
            loadChildren:'./home/home.module#HomeModule'
        }
    ];
    

    src/app/common/my-preloading-strategy.ts

    
    import { Route,PreloadingStrategy } from '@angular/router';
    import { Observable } from "rxjs";
    import "rxjs/add/observable/of";

    export class MyPreloadingStrategy implements PreloadingStrategy { preload(route: Route, fn: () => Observable): Observable{ return route.data&&route.data.preload?fn():Observable.of(null); } }

    WangShuXian6 commented 5 years ago

    路由守卫

    在实际的业务开发过程中,我们经常需要限制某些 URL 的可访问性,比如,对于系统管理界面,只有那些拥有管理员权限的用户才能打开。

    我看到过一些简化的处理方案,比如把菜单隐藏起来,但是这样做是不够的,因为用户还可以自己手动在地址栏里面尝试输入,或者更暴力一点,可以通过工具来强行遍历 URL。

    在 Angular 里面,权限控制的任务由“路由守卫”来负责,路由守卫的典型用法:

    • 控制路由能否激活
    • 控制路由的能否退出
    • 控制异步模块能否被加载