安装npm依赖包 推荐使用http
npm i --registry=http://registry.npmjs.org/
关于跨域可以使用nginx代理,关于package.json可以执行npm start,npm build…
"scripts": {
"ng": "ng",
"start": "ng serve",
"start.prod": "ng serve --aot",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"ivy": "ivy-ngcc"
},
注意nodejs版本、以及angular-cli版本
ng --version 查看是否安装成功
ng new taskAngular 生成项目cli
ng serve --host 0.0.0.0 保证项目和手机使用相同的网络 可以真机调试
- ng new xxx 创建一个项目
- 重新安装依赖 要先把原来的删除
- .gitignore git提交忽略的文件
- .editorconfig 编辑器
- browserslist 兼容浏览器
- karma.conf.js 测试配置
- package-lock.json package 里面的包 依赖的其他包 细致的罗列出来 依据package.json文件,控制依赖里面其他依赖的版本
- tsconfig.app.json 继承与tsconfig.json typescript 对于全局的设置
- tslint.json 做静态文件扫描的
- tsconfig.spec.json 是测试相关的配置
- ng lint 对项目的代码进行检查
- typescript 是js的高级
- 真机调试 ng serve --host 0.0.0.0 手机电脑 在同一个Wi-Fi环境 手机打开http://<电脑ip>:4200
使用vscode开启debug模式
使用svccode debugg 工具 进入菜单后 如果没有初始化 先点击设置 设置为浏览器 端口改为 项目运行的端口 前提是项目在运行中 会新开一个窗口
我当前的版本 需要先设置断点 再启动debug 就进入debug模式了
目录结构介绍
**重点理解interface interface 类型的命名,是用来规范class的 **
export interface TopMenu { // interface 做类型命名
title: string;
link: string;
}
interface Add {
(x: number, y:number): number
}
interface Dict {
[key: string]: string
}
export class ScrollableTabComponent {
@Input() selectedTabLink: string; // 路由切换后 页面刷新 让tab 自动选中
@Input() menus: TopMenu[] = [];
@Input() selectedIndex = 0;
@Input() backgroundColor = '#fff';
@Input() titleActiveColor = 'yellow';
@Input() titleColor = 'blue';
@Input() indicatorColor = 'brown';
@Output() tabSelected = new EventEmitter();
/**
* 第一个执行,构造函数是建立这个类的实例
* 之所以我们没有显性的使用 new ScrollableTabComponent()
* 是因为系统框架帮我们做了这个,这是依赖注入的概念
*/
constructor() {
console.log('构造函数');
console.log(this.dict.a)
}
AddFunc: Add = (x, y) => {
return x+y
}
dict: Dict = {
a: '1',
b: '2'
}
handleSelection(index: number) {
this.tabSelected.emit(this.menus[index]);
}
}
使用下面的命令可以快速生成模块文件
常用的指令
<ul [ngStyle]="{ 'background-color': backgroundColor }">
<!-- 指令 ngFor ngStyle ngClass (click) -->
<!-- [class.active]=" i=== 1" 单个样式条件绑定比较合适-->
<!-- [ngClass] [ngStyle] -->
<!-- trackBy 相当于vue的key -->
<!-- ngIf 和 else的 写法 -->
<!-- *ngIf="menu.link === selectedTabLink"
else eleTem -->
<!-- <ng-template #eleTem></ng-template> -->
<li
*ngFor="let menu of menus; let i = index; let even = even; let odd = odd"
[ngClass]="{ even: even, odd: odd }"
>
<a
[ngStyle]="{ color: i === selectedIndex ? titleActiveColor : titleColor }"
(click)="handleSelection(i)"
>
{{ menu.title }}
</a>
<span
class="indicator"
[ngStyle]="{ 'background-color': indicatorColor }"
*ngIf="menu.link === selectedTabLink"
></span>
</li>
</ul>
<ng-content></ng-content>
// 结构指令 ngIf ngFor ngSwitch
// 属性指令 ngClass ngStyle ngModel
- ngStyle
- ngClass
- ngFor 注意里面获取值的方法 trackBy 根据某个值渲染排序
- ngIf
- click事件绑定
- ngModel
<!-- 双向数据绑定 的模版 双向绑定的概念 -->
<input [value]="username" (input)="username = $event.target.value">
<input [(ngModel)]='username'>
<p>
<input type="text" [name]='name' (input)= "name = $event.target.value">
<span>您好 {{name}}</span>
<input type="text" [(ngModel)]= "name">
<span>您好 {{name}}</span>
</p>
angular生命周期
生命周期
// 生命周期
// constructor 构造函数 永远首先被调用
// ngOnChanges 输入属性变化时被调用 @input 发生变化时触发
// ngOnInit 组件初始化时被调用 只执行一次
// ngDoCheck 脏值检测时调用
// ngAfterContentInit 当投影内容ng-content 当组件中子组件初始化 完成时调用
// ngAfterContentChecked 当检测组件中子组件 多次调用
// ngAfterViewInit 当组件包含自己 以及子组件(整体视图) 初始化完成
// ngAfterViewChecked 组件整体视图的 脏值检测
// ngOnDestroy 组件销毁 做一些清除工作
@Component({ // @Component 装饰器 也叫注解 其实就是一个函数 一个返回函数的函数
1、selector: ‘app-root’, // 选择器 找到html的标签
2、 templateUrl: ‘./app.component.html’, // 模版的路径
3、styleUrls: [’./app.component.css’] // 是个数组 可以有多个文件
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
export interface TopMenu { // interface 做类型命名
title: string;
link: string;
}
@Component({
selector: 'app-scroll-top-menu',
templateUrl: './scroll-top-menu.component.html',
styleUrls: ['./scroll-top-menu.component.scss']
})
export class ScrollTopMenuComponent implements OnInit {
title = '';
selected = -1;
// 数据注入 事件监听 例如vue的 prop emit
@Input() menus: TopMenu[] = [];
@Input() backgroundC = ''
@Input() active = ''
@Output() tabClickEvent = new EventEmitter();
handlselect(index: number) {
this.selected = index;
this.tabClickEvent.emit(this.menus[this.selected])
}
constructor() {
// 构造函数 永远首先被调用
console.log('组件构造调用')
}
ngOnChanges(changes) {
// 输入属性变化时 调用
console.log(changes);
}
ngOnInit() {
// 组件初始化
console.log('组件初始化')
}
ngDoCheck() {
// 脏值检测调用
}
ngOnDestroy() {
// 组件销毁时 调用
}
ngAfterContentInit() {
// 组件内容初始化
}
ngAfterViewInit() {
// 组件子组件初始化
}
}
父子组件通信 类似vue(v-model双向绑定)
父组件通过@Input向子组件传递数据 子组件通过@Output()向外传递
可以全局搜索@Input、@Output查看应用实例
装饰器(注解)
其实就是一个 返回函数的 函数 return了一个函数 他是typescript 的特性 而非angular的特性
/Users/niko4/Desktop/pinduoduo/src/app/shared/decorators/index.ts 在share目录下 新建 decorators文件夹
export function Emoji() {
// 返回一个匿名函数的 es6的写法
return (target: object, key: string) => {
let val = target[key] // 原来的值 =》 一个属性
const getter = () => {
return val;
}
const setter = (value: string) => {
val = `😄 ${value}}`
}
Object.defineProperty(target, key, {
set: setter,
get: getter,
enumerable: true,
configurable: true
})
}
}
export function confimable(message: string) {
return (target: object, key: string, descriptor: PropertyDescriptor) =>{
const original = descriptor.value; // 原来的值 =》 一个函数
descriptor.value = function (...args: any) {
const allow = window.confirm(message)
if (allow) {
const result = original.apply(this, args)
return result
} else {
return null;
}
}
return descriptor;
}
}
如何使用:@Emoji() result = ‘hello’ 在模版中绑定{{result}}
app.module.ts 详细介绍
注意下面的注释
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';
import { ShareModule } from './share/share.module';
@NgModule({ // @NgModule 注解
declarations: [ // 当前模块拥有的组件 有哪些组件属于我这个模块
AppComponent,
],
imports: [ // 引入当前模块需要的依赖 // 依赖于那些组件
BrowserModule,
FormsModule,
ShareModule
],
providers: [], // 模块中需要使用的服务
bootstrap: [AppComponent] // 根模块 只有根模块有这个 第一个展示什么组件 也叫引导组件
})
export class AppModule { }
自定义指令
模版也是一种指令
/Users/niko4/Desktop/pinduoduo/src/app/shared/directives 用指令的方法创建模版
- 指令 没有模版,它需要一个宿主(host)元素 寄宿到他的身上。
- 推荐使用方括号 [] 指定 Selector,使它变成一个属性。
import { Directive, ElementRef, HostBinding, HostListener, Input, Renderer2 } from "@angular/core";
@Directive({
selector: '[keepImg]' //选择器 用来规定绑定到那里
})
export class HoriGridImageDirective {
@Input() keepImg = '2rem';
constructor(private elr: ElementRef, private rd2 : Renderer2) {
}
// 指令绑定样式 的 第二种写法
@HostBinding('style.width') width = this.keepImg
@HostBinding('style.grid-area') area = 'images'
@HostBinding('style.height') height = this.keepImg
ngOnInit() {
// this.rd2.setStyle(this.elr.nativeElement, 'grid-area', 'images');
// this.rd2.setStyle(this.elr.nativeElement, 'width', this.keepImg);
// this.rd2.setStyle(this.elr.nativeElement, 'height', this.keepImg);
}
指令的 事件绑定
@HostListener('click', ['$event.target'])
handleClick(ev) {
console.log(ev);
}
}
<div keep *ngFor="let item of channls">
<img [keepImg]= "'4rem'" [src]="item.icon" [alt]="item.caption">
<span keepTitle>{{item.title}}</span>
</div>
ng-content 组件内容投射 可以理解为vue插槽但又有所不同
结合自定指令很好的解决了多级父子孙组件数据和事件传递的且套问题
动态组件 ng-content 增加select 属性 限制要显示的内容
src/app/share/components/hori-grid/hori-grid.component.html
<!-- select 可以选择css 或者是dom元素 去控制显示 -->
<div class="contanier">
<ng-content></ng-content>
</div>
src/app/app.component.html
<app-hori-grid>
<div keep *ngFor="let item of channls">
<img [keepImg]= "'4rem'" [src]="item.icon" [alt]="item.caption">
<span keepTitle>{{item.title}}</span>
</div>
</app-hori-grid>
如何操作dom
src/app/share/components/image-slider/image-slider.component.html
<div class="container">
<div class="image-slider" #imageSliderID>
<img #img *ngFor="let item of sliders" [src]="item.imgUrl" [alt]="item.caption">
</div>
<div class="navsection">
<span *ngFor="let items of sliders" class="slider-button"></span>
</div>
</div>
注意下#开头的ID
src/app/share/components/image-slider/image-slider.component.ts
import { Component, ElementRef, Input, OnInit, QueryList, Renderer2, ViewChild, ViewChildren } from '@angular/core';
export interface ImageSlider {
imgUrl: string,
link: string,
caption: string
}
@Component({
selector: 'app-image-slider',
templateUrl: './image-slider.component.html',
styleUrls: ['./image-slider.component.scss']
})
export class ImageSliderComponent implements OnInit {
@Input() sliders: ImageSlider[] = []
// 如果组件在 if判断中 static 就是false 否则就是 true
@Input() IntervalBySeconds = 2
@ViewChild('imageSliderID', { static: true }) imageSliderID: ElementRef
@ViewChildren('img') imgs: QueryList <ElementRef>
constructor(private rd2: Renderer2) { }
ngOnInit() {
console.log(this.imageSliderID);
}
ngAfterViewInit() {
console.log(this.imgs);
setInterval(() => {
})
// 避免直接操作dom 防止xss攻击
// console.log(this.rd2)
this.imgs.forEach((ele) => {
// this.rd2.setStyle(ele.nativeElement, 'height', '200px')
})
}
}
src/app/app.component.ts
import { Component } from '@angular/core';
import {TopMenu, ImageSlider, channl} from './share/components'
@Component({
selector: 'app-root', // 选择器
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
// 父组件通过input 给子组件传值 子组件通过 output 传递会父组件 进行组件通行
export class AppComponent {
topMenus: TopMenu[] = [
{
title: '热门',
link: ''
},
{
title: '推荐',
link: ''
},
{
title: '手机',
link: ''
},
{
title: '电脑',
link: ''
},
{
title: '热门2',
link: ''
},
{
title: '热门3',
link: ''
},
{
title: '热门1',
link: ''
},
{
title: '热门2',
link: ''
},
{
title: '热门3',
link: ''
},
{
title: '热门1',
link: ''
},
{
title: '热门2',
link: ''
},
{
title: '热门3',
link: ''
}
];
channls : channl[] = [
{
icon: 'http://www.qianbuxian.com/assets/pc/img/pic_mrzx.png',
title: 'one',
caption: ''
},
{
icon: 'http://www.qianbuxian.com/assets/pc/img/pic_qxcj.png',
title: 'two',
caption: ''
},
]
listenTabEvent(tab) {
console.log(tab);
};
sliders: ImageSlider[] = [
{
imgUrl: 'http://www.qianbuxian.com/assets/pc/img/pic_mrzx.png',
link: '',
caption: ''
},
{
imgUrl: 'http://www.qianbuxian.com/assets/pc/img/pic_qxcj.png',
link: '',
caption: ''
},
{
imgUrl: 'http://www.qianbuxian.com/assets/pc/img/pic_mjgd.png',
link: '',
caption: ''
}
]
}
路由传参
应用实例:路由跳转 由第一部分(父)和第二部分组成(子)
handleTabSelected(topMenu: TopMenu) {
this.router.navigate(['home', topMenu.link]);
// 行内写法 [routerLink]="['/home',tad.link]"
}
应用实例:子组件 接受参数
ngOnInit() {
// 路径参数
this.selectedTabLink$ = this.route.paramMap.pipe(
filter(params => params.has('link')),
map(params => params.get('link'))
)
// console.log(this.selectedTabLink$)
// 查询参数
this.sub = this.route.queryParamMap.subscribe(params => { // 如果不用流 要记得在ngOnDestroy卸载 否则会内存泄漏
console.log('查询参数', params);
});
this.imageSliders$ = this.service.getBanners();
this.channels$ = this.service.getChannels();
this.ad$ = this.selectedTabLink$.pipe(
switchMap(tab => this.service.getAdByTab(tab)), // 当前的接口传值 要依赖与selectedTabLink这个流的写法
filter(ads => ads.length > 0),
map(ads => ads[0])
);
this.products$ = this.selectedTabLink$.pipe(
switchMap(tab => this.service.getProductsByTab(tab))
);
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
angular 管道 过滤器
自定义管道
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'appAgo' }) // 管道的名称
export class AgoPipe implements PipeTransform {
transform(value: any): any {
if (value) {
const seconds = Math.floor((+new Date() - +new Date(value)) / 1000);
if (seconds < 29) {
// 小于 30 秒
return '刚刚';
}
const intervals = {
年: 3600 * 24 * 365,
月: 3600 * 24 * 30,
周: 3600 * 24 * 7,
天: 3600 * 24,
小时: 3600,
分钟: 60,
秒: 1
};
let counter = 0;
for (const unitName in intervals) {
if (intervals.hasOwnProperty(unitName)) {
const unitValue = intervals[unitName];
counter = Math.floor(seconds / unitValue);
if (counter > 0) {
return counter + ' ' + unitName + '前';
}
}
}
}
return value;
}
}
应用实例:
<p>{{date | appAgo}}</p>
依赖注入
/Users/niko4/Desktop/pinduoduo/src/app/home/services/home.service.ts
把数据处理拆分出来 放到service文件中,通过依赖注入的方式 实现 service 达到数据出来拆分的 目的
home.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ImageSlider, Channel, TopMenu, Ad, Product } from 'src/app/shared';
import { environment } from 'src/environments/environment';
/**
* 如果采用 `providedIn` ,
* 这个形式是 Angular v6 之后引入的
* 这种写法和传统的在 Module 中设置 providers 数组的写法的区别在于
* 可以让服务在真正被其它组件或服务注入时才编译到最后的 js 中
* 对于引入第三方类库较多的应用可以有效减小 js 大小
*/
@Injectable({
providedIn: 'root'
})
export class HomeService {
constructor(private http: HttpClient) {}
getBanners() {
return this.http.get<ImageSlider[]>(`${environment.baseUrl}/banners`);
}
getChannels() {
return this.http.get<Channel[]>(`${environment.baseUrl}/channels`);
}
getTabs() {
// 固定一个类型 规定返回的类型(数组)<>简称范性 angular会自动转换
return this.http.get<TopMenu[]>(`${environment.baseUrl}/tabs`);
}
getAdByTab(tab: string) {
return this.http.get<Ad[]>(`${environment.baseUrl}/ads`, {
params: { categories_like: tab }
});
}
getProductsByTab(tab: string) {
return this.http.get<Product[]>(`${environment.baseUrl}/products`, {
params: { categories_like: tab }
});
}
}
pinduoduo/src/app/home/services/index.ts
import { InjectionToken } from '@angular/core';
export * from './home.service';
// baseUrl 抽出来 可能多个组件会用到
export const token = new InjectionToken<string>('baseUrl');
应用实例:类的申明和字符串的申明 写法不一样
/Users/niko4/Desktop/pinduoduo/src/app/home/home.module.ts
import { NgModule, InjectionToken } from '@angular/core';
import { HomeRoutingModule } from './home-routing.module';
import { SharedModule } from '../shared';
import { HomeContainerComponent, HomeDetailComponent } from './components';
import { HomeService, token } from './services';
@NgModule({
declarations: [HomeContainerComponent, HomeDetailComponent],
// 传统写法,如果采用这种写法,就不能在 service 中写 `providedIn`
// 只传递一个 特殊的字符串 给组件
providers: [{ provide: token, useValue: 'http://localhost' }],
imports: [SharedModule, HomeRoutingModule]
})
export class HomeModule {}
/Users/niko4/Desktop/pinduoduo/src/app/home/components/home-container/home-container.component.ts
export class HomeContainerComponent implements OnInit {
constructor(
private router: Router,
private service: HomeService, // 类的申明
private route: ActivatedRoute,
@Inject(token) private baseUrl: string // 字符串的申明 特殊写法
) {}
}
什么是脏值检测
当数据改变时更新视图(dom)浏览器的事件、定时器、http请求都会触发脏值检测
先去更新子组件的输入属性以及子组件的方法,父组件更新完DOM以后才会触发脏值检测,脏值检测完以后 再执行下面的方法
changeDetection 脏值检测的OnPush策略将组件变成笨组件,当input属性发生改变的时候触发
@Component({
selector: 'app-home-detail',
templateUrl: './home-detail.component.html',
styleUrls: ['./home-detail.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
// 通过这个变为笨组件,input发生变化了 才会触发脏值检测 否则不触发
})
技巧: 通过引入private cd: ChangeDetectorRef 提示系统手动更新
应用实例:
constructor(
private router: Router,
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.cd.markForCheck()
}
angular HttpClientModule
只在根目录注册一次 src/app/app.module.ts
imports: [ // 依赖于那些组件
HttpClientModule,
],
应用实例:
export class HomeService {
constructor(private http: HttpClient) { } // 使用http
}
src/app/home/interceptors/notification.interceptor.ts 响应拦截
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpResponse
} from '@angular/common/http';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class NotificationInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
// 对响应消息进行处理
return next.handle(req).pipe(
tap((event: HttpEvent<any>) => {
if (
event instanceof HttpResponse &&
event.status >= 200 &&
event.status < 300
) {
console.log('[此处假装弹出消息] 请求成功!');
}
})
);
}
}
src/app/home/interceptors/param.interceptor.ts 请求拦截
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest
} from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable()
export class ParamInterceptor implements HttpInterceptor {
// 截断器方法
intercept(req: HttpRequest<any>, next: HttpHandler) {
// 对请求消息进行处理 先克隆下来
const modifiedReq = req.clone({
setParams: { icode: environment.icode } // 相当于公共 token
});
return next.handle(modifiedReq);
}
}
rxjs angular深度集成 响应式编程类库
异步的 Observable 也是一种流 有三种状态 next erro complete
selectedTabLink$: Observable<string>; // 推荐加$ 表示是一个流
takeWhile(gap => gap >= 0), // 当表达式为真的时候 结束这个流 可以使用takeWhile
如果不用流 要记得在ngOnDestroy卸载 否则会内存泄漏
/rxjs 数据流 写法 //
selectedTabLink$: Observable<string>; // rxjs推荐加$ 特殊写法 表示是一个流
imageSliders$: Observable<ImageSlider[]>;
channels$: Observable<Channel[]>;
ad$: Observable<Ad>;
products$: Observable<Product[]>;
sub: Subscription;
ngOnInit() {
this.selectedTabLink$ = this.route.paramMap.pipe(
filter(params => params.has('tabLink')),
map(params => params.get('tabLink'))
);
this.sub = this.route.queryParamMap.subscribe(params => {
console.log('查询参数', params);
});
this.imageSliders$ = this.service.getBanners();
this.channels$ = this.service.getChannels();
this.ad$ = this.selectedTabLink$.pipe(
switchMap(tab => this.service.getAdByTab(tab)),
filter(ads => ads.length > 0),
map(ads => ads[0])
);
this.products$ = this.selectedTabLink$.pipe(
switchMap(tab => this.service.getProductsByTab(tab))
);
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
selectedTabLink$流在模版中的应用,结合asyn管道
<ng-container *ngIf="selectedTabLink$ | async as tab">
<ng-container *ngIf="tab === 'hot'">
<app-image-slider [sliders]="imageSliders$ | async"> </app-image-slider>
<app-horizontal-grid>
<span appGridItem *ngFor="let item of channels$ | async">
<img appGridItemImage="2rem" [src]="item.icon" alt="" />
<span appGridItemTitle="0.6rem" class="title">{{ item.title }}</span>
</span>
</app-horizontal-grid>
</ng-container>
</ng-container>
<div class="ad-container" *ngIf="ad$ | async as ad">
<img [src]="ad.imageUrl" alt="" class="ad-image" />
</div>
<app-vertical-grid [itemWidth]="'20rem'" [itemHeight]="'2rem'">
<app-product-card
*ngFor="let product of products$ | async"
[product]="product"
[routerLink]="['/products', product.id]"
>
</app-product-card>
</app-vertical-grid>
用rxjs流的方式 写倒计时
import {
Component,
OnInit,
Input,
ChangeDetectionStrategy
} from '@angular/core';
import { Observable, interval } from 'rxjs';
import { map, takeWhile } from 'rxjs/operators';
@Component({
selector: 'app-count-down',
templateUrl: './count-down.component.html',
styleUrls: ['./count-down.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CountDownComponent implements OnInit {
@Input() startDate
@Input() futureDate
private _MS_PER_SECOND = 1000;
countDown$: Observable<string>;
constructor() {}
ngOnInit() {
this.countDown$ = this.getCountDownObservable(
this.startDate,
this.futureDate
);
}
ngOnChanges(changes) {
// 输入属性变化时 调用
console.log(changes);
}
private getCountDownObservable(startDate: Date, futureDate: Date) {
return interval(1000).pipe(
map(elapse => this.diffInSec(startDate, futureDate) - elapse),
takeWhile(gap => gap >= 0), // 当表达式为真的时候 结束这个流
map(sec => ({
day: Math.floor(sec / 3600 / 24),
hour: Math.floor((sec / 3600) % 24),
minute: Math.floor((sec / 60) % 60),
second: Math.floor(sec % 60)
})),
map(({ hour, minute, second }) => `${hour}:${minute}:${second}`)
);
}
private diffInSec = (start: Date, future: Date): number => {
const diff = future.getTime() - start.getTime();
return Math.floor(diff / this._MS_PER_SECOND);
}
}
<div>{{ countDown$ | async }}</div>
项目实战 规范文件模块
share模块下是项目中用到的公共模块,每个模块下都有一个index.ts用导出模块,包括components下也有一个index.ts
最后在app.module.ts 引入 (全局搜索 app.module.ts)
根据上图的目录 依此展示index.ts
export * from './footer.component;
使用 index.ts 有两个好处 * 1. 缩短引用的路径* 2. 更好的封装,目录内部结构的变化不会影响外部;
src/app/shared/components/index.ts
export * from './scrollable-tab';
export * from './image-slider';
export * from './horizontal-grid';
export * from './count-down';
export * from './footer';
export * from './vertical-grid';
export * from './product-card';
export * from './product-tile';
export * from './back-button' // 后退按钮组件
src/app/shared/index.ts
export * from './components';
export * from './directives';
export * from './decorators';
export * from './pipes';
export * from './domain';
export * from './shared.module';
监听 router 对象
监听 router 对象 设置footer的selectedIndex
export class AppComponent implements OnInit { // class类
selectedIndex$: Observable<number>;
constructor(private router: Router, private dialogService: DialogService) {}
ngOnInit(): void {
// 监听 router 对象
this.selectedIndex$ = this.router.events.pipe(
filter(ev => ev instanceof NavigationEnd), //NavigationEnd 过滤出来
map((ev: NavigationEnd) => {
const arr = ev.url.split('/');
return arr.length > 1 ? arr[1] : 'home'; // 如果是根目录 默认跳到home
}),
map(tab => this.getSelectedIndex(tab))
);
}
handleTabSelect(tab: TabItem) {
this.router.navigate([tab.link]);
}
getSelectedIndex(tab: string) {
return tab === 'recommend'
? 1
: tab === 'category'
? 2
: tab === 'chat'
? 3
: tab === 'my'
? 4
: 1;
}
removeDialog() {
this.dialogService.close();
}
}
监听 router 对象 路由切换后 页面刷新 让tab 自动选中
ngOnInit(): void {
this.selectedTabLink$ = this.route.firstChild.paramMap.pipe(
// 路由切换后 页面刷新 让tab 自动选中
filter(params => params.has('tabLink')),
map(params => params.get('tabLink'))
);
}
监听 router 对象 路由对象 获取ID 有ID 再调取接口 获得数据
ngOnInit() {
// 从路由对象 获取ID
const productId$ = this.route.paramMap.pipe(
filter(params => params.has('productId')),
map(params => params.get('productId'))
);
// 如何有ID 再调取接口 获得数据
this.variants$ = productId$.pipe( // 有依赖关系时的写法
switchMap(productId =>
this.orderService.getProductVariantsByProductId(productId)
)
);
}
app根组件 全局对话框的 遮罩 和 容器 第一步
<!--
路由显示的内容是插入到 router-outlet 的同级的下方节点
而不是在 router-outlet 中包含
-->
<router-outlet></router-outlet>
<app-footer
(tabSelected)="handleTabSelect($event)"
[selectedIndex]="selectedIndex$ | async"
></app-footer>
><!-- 对话框的 遮罩 和 容器 -->
<div id="overlay" class="hidden" (click)="removeDialog()"></div>
<div id="dialog-container" class="hidden"></div>
全局dialog模块
创建dialog模块
src/app/dialog/components/dialog.component.ts 模版的行内写法 适应比较少的内容,不建议大面积使用
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-dialog',
template: `
<div class="container">
<ng-content></ng-content>
</div>
`,
styles: [
`
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
height: 100%;
}
`
]
})
export class DialogComponent implements OnInit {
@Input() title = '';
constructor() {}
ngOnInit(): void {}
}
创建两个services文件 dom负责插入元素,dialog负责控制弹框的显示/隐藏 注意里面的注释详解
src/app/dialog/services/dom.service.ts
import {
Injectable,
ComponentFactoryResolver,
ApplicationRef,
Injector,
EmbeddedViewRef,
ComponentRef,
Type,
Inject
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
export interface DialogPos {
top: string;
left: string;
width: string;
height: string;
}
export interface ChildConfig { // 定义一个类
inputs: object;
outputs: object;
position?: DialogPos; // 用来控制位置 见上面的类DialogPos
}
@Injectable({ providedIn: 'root' })
export class DomService {
private childComponentRef: ComponentRef<any>;
// 为了在删除的地方 使用 定义一个成员变量
constructor(
private resolver: ComponentFactoryResolver,
// 在angular中所有的组件 都是通过组件工厂创建出来的 所以需要得到一个组件工厂的类
private appRef: ApplicationRef,
// 得到angular 应用本身的 一个引用
private injector: Injector,
@Inject(DOCUMENT) private document: Document
) { }
// 以上的步骤是 先通过组件工厂创建出来 再通过引用 插到组件树当中
/**
* appendComponentTo 方法 用于把节点插到指定的位置
*/
public appendComponentTo(
parentId: string, // 父节点 dialog-container
child: Type<any>, // 子元素
childConfig: ChildConfig
) {
const childComponentRef = this.resolver
.resolveComponentFactory(child) // 先得到组件工厂
.create(this.injector); // 在工厂里创建 传入injector 依赖一些其他东西 在这个注入器里
this.attachConfig(childConfig, childComponentRef); // 调用config 设置属性
this.childComponentRef = childComponentRef;
this.appRef.attachView(childComponentRef.hostView); // 把创建好的视图 放到angular组件 树中
const childDOMElement = (childComponentRef.hostView as EmbeddedViewRef<any>)
.rootNodes[0] as HTMLElement; // 得到他的html
this.document.getElementById(parentId).appendChild(childDOMElement); // 把子节点插进去
}
/**
* attachConfig 设置属性
*/
public attachConfig(config: ChildConfig, componentRef: ComponentRef<any>) {
// 主要是 把传入的config 设置上
const inputs = config.inputs;
const outputs = config.outputs;
for (const key in inputs) {
if (inputs.hasOwnProperty(key)) {
const element = inputs[key];
// 设置过程
componentRef.instance[key] = element;
}
}
for (const key in outputs) {
if (outputs.hasOwnProperty(key)) {
const element = outputs[key];
// 设置过程
componentRef.instance[key] = element;
}
}
}
/**
* removeComponent 删除组件
*/
public removeComponent() {
this.appRef.detachView(this.childComponentRef.hostView);
}
}
src/app/dialog/services/dialog.service.ts
注意下面localStorage.getItem的应用,防止页面刷新丢数据
import { Injectable, Type, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
import { DomService, ChildConfig } from './dom.service';
@Injectable({ providedIn: 'root' })
export class DialogService {
private dialogElementId = 'dialog-container';
private overlayElementId = 'overlay';
// private data$ = new BehaviorSubject<object | null>(null); 原始的写法 下面是设置缓存的写法
private data$: BehaviorSubject<object | null>; // BehaviorSubject 是一个流 获取的时候永远得到最新的值
constructor(
// 关闭和打开弹框 需要依赖 dom
private domService: DomService,
@Inject(DOCUMENT) private document: Document
) {
const data = localStorage.getItem('initData');
this.data$ = new BehaviorSubject<object | null>(JSON.parse(data))
}
// open 方法
open(component: Type<any>, config: ChildConfig) {
this.domService.appendComponentTo(this.dialogElementId, component, config); // 把上面的id 设置进去
if (config.position) {
// 设置config
const element = this.document.getElementById(this.dialogElementId);
element.style.width = config.position.width;
element.style.height = config.position.height;
element.style.top = config.position.top;
element.style.left = config.position.left;
}
this.toggleAll();
// 新打开的时候 先清空
this.data$.next(null);
}
// close 方法
close() {
// 调用 domService 把 组件删掉
this.domService.removeComponent();
this.toggleAll();
}
// 保存
saveData(data: object | null) {
this.data$.next(data);
localStorage.setItem('initData', JSON.stringify(data));
}
// 获取 通过services 传递大量数据
getData() {
return this.data$.asObservable();
}
private toggleAll() {
this.toggleVisibility(this.document.getElementById(this.dialogElementId));
this.toggleVisibility(this.document.getElementById(this.overlayElementId));
}
private toggleVisibility(element: HTMLElement) {
// 控制显示 隐藏 的方法
if (element.classList.contains('show')) {
element.classList.remove('show');
element.classList.add('hidden');
return;
}
if (element.classList.contains('hidden')) {
element.classList.remove('hidden');
element.classList.add('show');
}
}
}
src/app/dialog/index.ts 将module写入index.ts文件 创新写法
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DialogComponent } from './components';
import { CloseDialogDirective } from './directives';
export * from './components';
export * from './directives';
export * from './services';
@NgModule({
imports: [CommonModule],
declarations: [DialogComponent, CloseDialogDirective],
exports: [DialogComponent, CloseDialogDirective] // 如果外部使用 先要导出 到sharemodule 中导出
})
export class DialogModule {}
最后在sharemodule中导出,因为这个产品的弹框 位于product文件下 product.module.ts只引入了SharedModule
angular的组件是通过组件工厂创建出来,给界面上动态创建出来的元素(ProductVariantDialogComponent) 要放到entryComponents
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
ScrollableTabComponent,
ImageSliderComponent,
HorizontalGridComponent,
CountDownComponent,
FooterComponent,
VerticalGridComponent,
ProductCardComponent,
ProductTileComponent,
BackButtonComponent
} from './components';
import {
GridItemDirective,
GridItemImageDirective,
GridItemTitleDirective,
TagDirective,
AvatarDirective
} from './directives';
import { AgoPipe } from './pipes';
import { DialogModule } from '../dialog';
import { ProductVariantDialogComponent } from './components/product-variant-dialog';
@NgModule({
declarations: [
ScrollableTabComponent,
ImageSliderComponent,
HorizontalGridComponent,
CountDownComponent,
FooterComponent,
VerticalGridComponent,
ProductCardComponent,
ProductTileComponent,
ProductVariantDialogComponent,
BackButtonComponent, // 声明
GridItemDirective,
GridItemImageDirective,
GridItemTitleDirective,
TagDirective,
AvatarDirective,
AgoPipe
],
imports: [CommonModule, FormsModule],
exports: [
CommonModule,
FormsModule,
DialogModule, // 导出 弹层
ScrollableTabComponent,
ImageSliderComponent,
HorizontalGridComponent,
CountDownComponent,
FooterComponent,
VerticalGridComponent,
ProductVariantDialogComponent,
ProductCardComponent,
ProductTileComponent,
BackButtonComponent, // 导出
GridItemDirective,
GridItemImageDirective,
GridItemTitleDirective,
TagDirective,
AvatarDirective,
AgoPipe
],
entryComponents: [ProductVariantDialogComponent] // 用来调用组件 给界面上动态创建出来的元素 要放到entryComponents
})
export class SharedModule {}
src/app/shared/components/product-variant-dialog/product-variant-dialog.component.html 应用了app-dialog组件
<app-dialog>
<div class="image-container">
<img
class="product-image"
[src]="variants[selectedVariantIndex].productVariantImages[0].imgUrl"
/>
<div class="price">
{{ price | currency: 'CNY'
}}<img class="close-button" appCloseDialog src="assets/icons/close.png" />
</div>
<div class="selected-desc">已选择:{{ selectedVariantName }}</div>
</div>
<div class="content">
<div>套餐</div>
<div class="variant">
<!-- 注意下 class 绑定的写法 -->
<div
*ngFor="let variant of variants; let idx = index"
[ngClass]="{ 'variant-selected': idx === selectedVariantIndex }"
(click)="handleSelection(idx)"
>
{{ variant.name }}
</div>
</div>
<app-product-amount
(amountChange)="handleAmountChange($event)"
></app-product-amount>
</div>
<div class="buttons">
<!-- 注意下 事件绑定 与样式 绑定的写法 -->
<button
class="confirm-button"
(click)="handleConfirm()"
[ngStyle]="{
'background-color':
count > 0 && selectedVariantIndex >= 0 ? 'red' : 'darkgray'
}"
>
确定
</button>
</div>
</app-dialog>
src/app/shared/components/product-variant-dialog/product-variant-dialog.component.ts 调用dialog向外传递formSubmitted信息
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy
} from '@angular/core';
import { ProductVariant } from '../../../product/domain';
import { DialogService } from 'src/app/dialog/services';
@Component({
selector: 'app-product-variant-dialog',
templateUrl: './product-variant-dialog.component.html',
styleUrls: ['./product-variant-dialog.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductVariantDialogComponent implements OnInit {
@Input() variants: ProductVariant[] = [];
@Output() formSubmitted = new EventEmitter();
@Output() selected = new EventEmitter<number>(); // number 是后加的
@Input() selectedVariantIndex = -1;
count = 1;
constructor(private dialogService: DialogService) {} // 注入弹框serve 主要是关闭事件的应用
ngOnInit(): void { }
// get 方法的应用
get price() {
// 获取价格 的 方法
if (this.variants.length === 0 || this.selectedVariantIndex < 0) {
return 0;
}
return this.variants[this.selectedVariantIndex].price;
}
get productImage() {
return this.selectedVariantIndex < 0
? this.variants[0].product.imageUrl
: this.variants[this.selectedVariantIndex].product.imageUrl;
}
get selectedVariantName() {
return this.selectedVariantIndex < 0
? ''
: this.variants[this.selectedVariantIndex].name;
}
handleSelection(idx: number) {
// 更新当前的 索引
this.selectedVariantIndex = idx;
this.selected.emit(this.selectedVariantIndex);
}
handleConfirm() {
if (this.selectedVariantIndex < 0 || this.count === 0) {
return;
}
// 提交后即将 生成订单
this.formSubmitted.emit({
variant: this.variants[this.selectedVariantIndex],
count: this.count
});
this.dialogService.close();
}
handleAmountChange(count: number) {
// 来源与 选择数量的 组件
this.count = count;
}
}
本文的核心
注意:
下面的组件是正式调用dialog组件的代码示例,用到了save方法存值以及打开dialog
src/app/product/components/product-container/product-container.component.ts 之所以能接收到formSubmitted信息 因为product.module.ts中引用了SharedModule
注意Subject和behavior的区别
注意传入 Output,EventEmitter 其实就是一个 Subject 可以 next(xxx) next一个元素 也可以subscribe 不会记忆值 错过就错过了,behavior 是流的 一种特殊形式 可以记住最新的值
import {
Component,
OnInit,
ChangeDetectionStrategy,
EventEmitter,
OnDestroy
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import { ProductVariant } from '../../domain';
import { OrderService } from '../../services';
import { DialogService } from 'src/app/dialog';
import { ProductVariantDialogComponent } from 'src/app/shared';
@Component({
selector: 'app-product-container',
templateUrl: './product-container.component.html',
styleUrls: ['./product-container.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductContainerComponent implements OnInit, OnDestroy {
variants$: Observable<ProductVariant[]>;
selectedIndex = 0;
subs: Subscription[] = [];
constructor(
private router: Router,
private route: ActivatedRoute,
private orderService: OrderService,
private dialogService: DialogService // 注入 dialogService
) {}
ngOnInit() {
// 从路由对象 获取ID
const productId$ = this.route.paramMap.pipe(
filter(params => params.has('productId')),
map(params => params.get('productId'))
);
// 如何有ID 再调取接口 获得数据
this.variants$ = productId$.pipe( // 有依赖关系时的写法
switchMap(productId =>
this.orderService.getProductVariantsByProductId(productId)
)
);
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
this.subs = [];
}
handleDirectBuy(variants: ProductVariant[]) {}
handleGroupBuy(variants: ProductVariant[]) {
const top = 40;
const formSubmitted = new EventEmitter();
this.subs.push(
formSubmitted.subscribe(ev => {
this.dialogService.saveData(ev);
this.router.navigate(['/orders', 'confirm']);
})
);
const selected = new EventEmitter<number>();
this.subs.push(
selected.subscribe(idx => {
console.log(idx);
this.selectedIndex = idx;
})
);
// 调用服务下的 方法
this.dialogService.open(ProductVariantDialogComponent, {
// 如果 key 和 value 是一个名字,直接写就可以
inputs: {
variants,
selectedVariantIndex: this.selectedIndex
},
outputs: { formSubmitted, selected },
position: {
top: `${top}%`,
left: '50%',
width: '100%',
height: `${100 - top}%`
}
});
}
}
注意rxjs的应用:
1、share 可以 使 item$ 只调用一次 变为共享的流 自动通知其他方法执行。2、通过tap 可以打印(console.log)
再次强调了Observable和Subject的区别,可以全文搜索查看
src/app/product/components/confirm-order/confirm-order.component.ts 订单结算页面注意可以调整商品的数量
注意merge 和combineLatest 的区别
merge:合并同类型 成一个流 两个流叠成一个流。
combineLatest 多个值进行计算会用到合并 ,当两个流 都有值的情况下去合并 是一个复杂合并,主要是为了计算!
注意ProductVariant的应用来源于domain文src/app/product/domain/index.ts
import { Product, ImageSlider } from 'src/app/shared';
export interface ProductVariant {
id: number;
product: Product;
name: string;
price: number;
listPrice: number;
productVariantImages: ImageSlider[];
}
export interface GroupOrder {
id: number;
productId: number;
startBy: string;
avatar: string;
startAt: Date;
remainingNumber: number;
}
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { DialogService } from 'src/app/dialog';
import { Observable, Subject, combineLatest, merge } from 'rxjs';
import { map, tap, share } from 'rxjs/operators';
import { ProductVariant } from '../../domain';
import { Payment } from '../payment';
@Component({
selector: 'app-confirm-order',
templateUrl: './confirm-order.component.html',
styleUrls: ['./confirm-order.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush // 笨组件
})
export class ConfirmOrderComponent implements OnInit {
item$: Observable<object | null>;
// dialog传递来的数据 一种流 可以获取最新的数据
count$ = new Subject<number>();
// 也是一个流 它可以next 有next方法 也可以subscribe 不会记忆值 错过就错过了
totalPrice$: Observable<number>;
payments: Payment[];
constructor(private dialogService: DialogService) {} // 从service 获取数据
ngOnInit() {
this.payments = this.payments = [
{
id: 1,
name: '微信支付',
icon: 'assets/icons/wechat_pay.png',
desc: '50元以内可免密支付'
},
{
id: 2,
name: '支付宝',
icon: 'assets/icons/alipay.png'
},
{
id: 3,
name: '找微信好友支付',
icon: 'assets/icons/friends.png'
}
];
this.item$ = this.dialogService.getData().pipe( // 第一次
tap(val => console.log(val)), // 通过tap 可以打印
// 通过 share 可以 使 item$ 只调用一次 他自己变为共享的流 通知方法执行
share()
);
// 单价
const unitPrice$ = this.item$.pipe( // 第二次
map(
(item: { variant: ProductVariant; count: number }) => item.variant.price
)
);
// 数量
const amount$ = this.item$.pipe( // 第三次
// 定义item 的类型
map((item: { variant: ProductVariant; count: number }) => item.count)
);
//merge 和combineLatest 的区别
const mergedCount$ = merge(amount$, this.count$); // 传进来的流 和 当前修改的流 合并同类型 成一个流 两个流叠成一个流
this.totalPrice$ = combineLatest([unitPrice$, mergedCount$]).pipe( // 当两个流 都有值的情况下去合并 是一个复杂合并 combineLatest 多个值进行计算会用到合并
map(([price, amount]) => price * amount)
);
}
handleAmountChange(count: number) {
// 可以把数据发射出去
this.count$.next(count);
}
handlePay() {}
}
注意:
多加一层ng-container应用技巧,以及click事件的写法
src/app/product/components/product-container/product-container.component.html
<!-- 后退按钮组件 -->
<app-back-button></app-back-button>
<!-- 多加一层ng-container 将异步返回的值 转换为productVariants 自定义的)方便其他依赖组件使用 -->
<ng-container *ngIf="variants$ | async as productVariants">
<app-image-slider
[sliders]="productVariants[selectedIndex].productVariantImages"
[sliderHeight]="'22rem'"
[intervalBySeconds]="0"
>
</app-image-slider>
<div class="price-section">
<div class="now-price">
{{ productVariants[selectedIndex].price | currency: 'CNY' }}
</div>
<div class="list-price">
{{ productVariants[selectedIndex].listPrice | currency: 'CNY' }}
</div>
<div class="sales-count">
{{ productVariants[selectedIndex].product.priceDesc }}
</div>
<div class="title">
{{ productVariants[selectedIndex].name }}
</div>
<div class="coupon">
<span
appTag
[tagRadius]="'5px'"
[tagPadding]="'2px 10px'"
[tagSize]="'1rem'"
[tagBgColor]="'#e02f29'"
[tagColor]="'#fff'"
>返现</span
>
<span class="desc">店铺内消费满39元返2元</span>
</div>
<div class="guarantee">
<span>全场包邮</span>
<span>•</span>
<span>7天退换</span>
<span>•</span>
<span>48小时发货</span>
<span>•</span>
<span>假一赔十</span>
</div>
<app-group-short-list class="group-short-list"></app-group-short-list>
</div>
<div class="toolbar">
<div class="icon-button">
<img src="/assets/icons/more.png" alt="" />
<div>更多</div>
</div>
<div class="icon-button">
<img src="/assets/icons/favorite.png" alt="" />
<div>收藏</div>
</div>
<div class="icon-button">
<img src="/assets/icons/customer-service.png" alt="" />
<div>客服</div>
</div>
<div class="direct-buy" (click)="handleDirectBuy(productVariants)">
<div>
{{ productVariants[selectedIndex].listPrice | currency: 'CNY' }}
</div>
<div>直接购买</div>
</div>
<div class="group-buy" (click)="handleGroupBuy(productVariants)">
<div>{{ productVariants[selectedIndex].price | currency: 'CNY' }}</div>
<div>发起拼单</div>
</div>
</div>
</ng-container>
路由懒加载
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
/**
* 在功能模块中定义子路由后,只要导入该模块,等同于在根路由中直接定义
* 也就是说在 AppModule 中导入 HomeModule 的时候,
* 由于 HomeModule 中导入了 HomeRouting Module
* 在 HomeRoutingModule 中定义的路由会合并到根路由表
* 相当于直接在根模块中定义下面的数组。
* 需要注意的一个地方是 Angular 路由数组的**顺序**非常重要。
* 所以此处的 `redirect` 这个条目在根路由中起到了定义各功能模块路由顺序的意义。
*
* ```typescript
* const routes = [{
* path: 'home',
* component: HomeContainerComponent
* }]
* ```
*/
// 从上往下走 匹配到就执行 匹配不到继续往下走
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' }, // 匹配一个空路径
// 下面是懒加载的路由 可能会引起组件位置的变化
{ path: 'my', loadChildren: () => import('./my').then(m => m.MyModule) },
{ path: 'recommend', loadChildren:() => import('./recommend').then(m => m.RecommendModule)},
{ path: 'chat', loadChildren:() => import('./chat').then(m => m.ChatModule)},
{ path: 'category', loadChildren:() => import('./category').then(m => m.CategoryModule)},
{ path: 'products', loadChildren: () => import('./product').then(m => m.ProductModule) },
{ path: 'orders', loadChildren:() => import('./product').then(m => m.ProductModule)},
];
@NgModule({
/**
* 根路由使用 `RouterModule.forRoot(routes)` 形式。
* 而功能模块中的路由模块使用 `outerModule.forChild(routes)` 形式。
* 启用路由的 debug 跟踪模式,需要在根模块中设置 `enableTracing: true`
*/
imports: [RouterModule.forRoot(routes, { enableTracing: true })], //forRoot 跟路由 enableTracing 是否允许debugger跟踪
exports: [RouterModule]
})
export class AppRoutingModule {}
扩展 两种编译模式AOT&JIT
1、ahead-of-time: 在时间之前编译->预编译
2、just-in-time: 正要用时编译->即时编译
区别:其实Angular只有一种编译器,这两种编译模式的不同之处只是编译的时机不一样,一个是在程序打包时编译,一个是将编译器和代码load到浏览器之后编译