当前位置:首页 » 《随便一记》 » 正文

也许是全网最全的 Angular 新手入门指南

5 人参与  2023年04月02日 16:00  分类 : 《随便一记》  评论

点击全文阅读


文章目录

Angular概述Angular程序架构Angular优势@angular/cli脚手架文件加载顺序项目目录结构 Angular模块@NgModule 装饰器内置模块自定义模块模块的tips Angular组件@Component 元数据数据绑定脏值检测父子组件通讯投影组件 Angular指令内置属性型指令内置结构型指令指令事件样式绑定 Angular生命周期Angular路由路由基本配置激活路由路由参数路由懒加载 Angular服务@Injectable()装饰器依赖注入 Angular 管道内置管道自定义管道 Angular操作DOM原生JS操作ElementRefViewChildRenderer2 Angular网络请求HttpClient拦截器 Angular表单模版驱动表单响应式表单表单验证动态表单 Angular CDK

Angular概述

Angular 是谷歌开发的一款开源的 web 前端框架,基于 TypeScript 。

和 react 与 vue 相比, Angular 更适合中大型企业级项目。

Angular程序架构

在这里插入图片描述

Angular优势

可伸缩性:基于RxJS 、immutable.js和其他推送模型,能适应海量数据需求跨平台:渐进式应用(高性能、离线使用、免安装),原生(Ionic),桌面端生产率:模版(通过简单而强大的模版语法,快速创建UI视图),CLI(快速进入构建环节、添加组件和测试,然后立即部署)测试:单元测试(支持Karma、Jasmine等工具进行单元测试),端到端测试(支持Protractor等工具进行端到端测试)

@angular/cli脚手架

ng new 新建项目 ——routing 配置路由——style=css|scss|less 配置css样式 ng serve 启动项目 ——port 4200 端口号,默认4200——open 自动打开浏览器 ng build 打包项目 ——aot 预编译——prod 压缩打包——base-href=/static/ ng generate 创建模块/组件/服务 module ——routing 创建模块component 创建组件service / 创建服务

文件加载顺序

main.ts => app.module.ts => app.component.ts => index.html => app.component.html

项目目录结构

|-- project|-- .editorconfig // 用于在不同编辑器中统一代码风格|-- .gitignore // git中的忽略文件列表|-- README.md // markdown格式的说明文件|-- angular.json // angular的配置文件|-- browserslist // 用于配置浏览器兼容性的文件|-- karma.conf.js // 自动化测试框架Karma的配置文件|-- package-lock.json // 依赖包版本锁定文件|-- package.json // npm的包定义文件|-- tsconfig.app.json // 用于app项目的ts配置文件|-- tsconfig.json // 整个工作区的ts配置文件|-- tsconfig.spec.json // 用于测试的ts配置文件|-- tslint.json // ts的代码静态扫描配置|-- e2e // 自动化集成测试目录|-- src // 源代码目录 |-- src // 源代码目录|-- favicon.ico // 收藏图标|-- index.html // 单页应用到宿主HTML|-- main.ts // 入口 ts 文件|-- polyfills.ts // 用于不同浏览器的兼容脚本加载|-- styles.css // 整个项目的全局css|-- test.ts // 测试入口|-- app // 工程源码目录|-- assets // 资源目录|-- environments // 环境配置|-- environments.prod.ts // 生产环境|-- environments.ts // 开发环境

Angular模块

在 app.module.ts 中定义 AppModule,这个根模块会告诉 Angular 如何组装应用。

在这里插入图片描述

@NgModule 装饰器

@NgModule 接受一个元数据对象,告诉 Angular 如何编译和启动应用

设计意图

静态的元数据(declarations)运行时的元数据(providers)组合与分组(imports 和 exports)

元数据

declarations 数组:模块拥有的组件、指令或管道,注意每个组件/指令/管道只能在一个模块中声明providers 数组: 模块中需要使用的服务imports 数组:导入本模块需要的依赖模块,注意是模块exports 数组: 暴露给其他模块使用的组件、指令或管道等bootstrap 数组:指定应用的主视图(称为根组件)通过引导根 AppModule 来启动应用,即项目刚加载时选择读哪个组件entryComponents 数组:一般用于动态组件

内置模块

常用的有:核心模块、通用模块、表单模块、网络模块等

在这里插入图片描述

自定义模块

当项目比较小的时候可以不用自定义模块

但是当项目非常庞大的时候,把所有的组件都挂载到根模块里面就不太合适了

所以可以使用自定义模块来组织项目,并且通过自定义模块可以实现路由的懒加载

模块的tips

导入其他模块时,需要知道使用该模块的目的

如果是组件,那么需要在每一个需要的模块中都进行导入如果是服务,那么一般来说在根模块导入一次即可

需要在每个需要的模块中进行导入的

CommonModule : 提供绑定、*ngIf 和 *ngFor 等基础指令,基本上每个模块都需要导入它FormsModule / ReactiveFormsModule : 表单模块需要在每个需要的模块导入提供组件、指令或管道的模块

只在根模块导入一次的

HttpClientModule / BrowerAnimationsModule NoopAnimationsModule只提供服务的模块

Angular组件

在这里插入图片描述

组件是 Angular 的核心,是 Angular 应用中最基本的 UI 构造块,控制屏幕上被称为视图的一小片区域组件必须从属于某个 NgModule 才能被其他组件或应用使用组件在 @NgModule 元数据的 declarations 字段中引用

@Component 元数据

selector :选择器,选择相匹配的HTML里的指令模版templateUrl :将选择器中匹配的指令同级替换成值的模版template :内嵌模版,直接可以在里面写HTML模版styleUrls :对应模版的样式,为一个数组,可以引入多个css样式控制组件encapsulation:组件样式封装策略
@Component({  selector: 'app-xxx',  templateUrl: 'XXX',  styleUrls: ['XXX'],  encapsulation:ViewEncapsulation.Emulated  // 不写则默认该值,表示该组件样式只作用于组件本身,不影响全局样式,在 head 中生成单独的 style 标签})

数据绑定

数据绑定 {{data}}

属性绑定 [id]="id",其中[class.样式类名]=“判断表达式”是在应用单个class样式时的常用技巧

事件绑定 (keyup)="keyUpFn($event)"

样式绑定可以用 :host 这样一个伪类选择器,绑定的样式作用于组件本身

双向数据绑定 [(ngModel)]

// 注意引入:FormsModuleimport { FormsModule } from '@angular/forms';<input type="text" [(ngModel)]="inputValue"/> {{inputValue}}// 其实是一个语法糖[ngModel]="username" (ngModelChange)="username = $event"

脏值检测

脏值检测:当数据改变时更新视图(DOM)

如何进行检测:检测两个状态值(当前状态和新状态)

何时触发脏值检测:浏览器事件(clickmouseoverkeyup等)、setTimeout()setInterval()、HTTP请求

Angular 有两种变更检测策略:DefaultOnPush

可以通过在@Component元数据中设置changeDetection: ChangeDetectionStrategy.OnPush进行切换

Default

优点:每一次有异步事件发生,Angular 都会触发变更检测,从根组件开始遍历其子组件,对每一个组件都进行变更检测,对dom进行更新。

缺点:有很多组件状态没有发生变化,无需进行变更检测。如果应用程序中组件越多,性能问题会越来越明显。

OnPush

优点:组件的变更检测完全依赖于组件的输入(@Input),只要输入值不变就不会触发变更检测,也不会对其子组件进行变更检测,在组件很多的时候会有明显的性能提升。

缺点:必须保证输入(@Input)是不可变的(可以用Immutable.js解决),每一次输入变化都必须是新的引用。

父子组件通讯

在这里插入图片描述

父组件给子组件传值 @input

父组件不仅可以给子组件传递简单的数据,还可把自己的方法以及整个父组件传给子组件。

// 父组件调用子组件的时候传入数据<app-header [msg]="msg"></app-header>// 子组件引入 Input 模块import { Component, OnInit ,Input } from '@angular/core';// 子组件中 @Input 装饰器接收父组件传过来的数据export class HeaderComponent implements OnInit {  @Input() msg:stringconstructor() { }ngOnInit() { }}// 子组件中使用父组件的数据<h2>这是头部组件--{{msg}}</h2>

**子组件触发父组件的方法 @Output **

// 子组件引入 Output 和 EventEmitterimport { Component,OnInit,Input,Output,EventEmitter} from '@angular/core';// 子组件中实例化 EventEmitter// 用 EventEmitter 和 @Output 装饰器配合使用 <string> 指定类型变量@Output() private outer=new EventEmitter<string>();// 子组件通过 EventEmitter 对象 outer 实例广播数据sendParent(){  this.outer.emit('msg from child')}// 父组件调用子组件的时候,定义接收事件,outer 就是子组件的 EventEmitter 对象 outer<app-header (outer)="runParent($event)"></app-header>// 父组件接收到数据会调用自己的 runParent, 这个时候就能拿到子组件的数据// 接收子组件传递过来的数据  runParent(msg:string){   alert(msg);}

父组件通过 ViewChild 主动调用子组件DOM和方法

// 给子组件定义一个名称<app-footer #footerChild></app-footer>// 引入 ViewChildimport { Component, OnInit ,ViewChild} from '@angular/core';// ViewChild 和子组件关联起来@ViewChild('footerChild') footer;// 调用子组件run(){   this.footer.footerRun();}

投影组件

在这里插入图片描述

由于组件过度嵌套会导致数据冗余和事件传递,因此引入投影组件的概念

投影组件 ng-content 作为一个容器组件使用

主要用于组件动态内容的渲染,而这些内容没有复杂的业务逻辑,也不需要重用,只是一小部分 HTML 片段

使用 ng-content 指令将父组件模板中的任意片段投影到它的子组件上

组件里面的 ng-content 部分可以被组件外部包裹的元素替代

// 表现形式: <ng-content select="样式类/HTML标签/指令"></ng-content><ng-content select="[appGridItem]"></ng-content>

select 表明包含 appGridItem 的指令的元素才能投影穿透过来


Angular指令

在这里插入图片描述

指令可以理解为没有模版的组件,它需要一个宿主元素(Host)

推荐使用方括号 [] 指定 Selector,使它变成一个属性

@Directive({selector: '[appGridItem]'})

内置属性型指令

NgClass

ngClass 是自由度和拓展性最强的样式绑定方式

<div [ngClass]="{'red': true, 'blue': false}">  这是一个 div</div>

NgStyle

ngStyle由于是嵌入式样式,因此可能会覆盖掉其他样式,需谨慎

<div [ngStyle]="{'background-color':'green'}">你好 ngStyle</div>

NgModel

// 注意引入:FormsModuleimport { FormsModule } from '@angular/forms';<input type="text" [(ngModel)]="inputValue"/> {{inputValue}}

内置结构型指令

ngIf

ngIf 根据表达式是否成立,决定是否展示 DOM 标签

<p *ngIf="list.length > 3">这是 ngIF 判断是否显示</p>

ngIf else

<div *ngIf="show else ElseContent">这是 ngIF 内容</div><ng-template #ElseContent>  <h2>这是 else 内容</h2></ng-template>// 结构性指令都依赖于 ng-template,*ngIf 实际上就是 ng-template 指令的 [ngIf] 属性。

ngFor

<ul>  <li *ngFor="let item of list;let i = index;">     {{item}} --{{i}}  </li></ul>

ngSwitch

<ul [ngSwitch]="score">   <li *ngSwitchCase="1">已支付</li>   <li *ngSwitchCase="2">已确认</li>   <li *ngSwitchCase="3">已发货</li>   <li *ngSwitchDefault>已失效</li></ul>

指令事件样式绑定

@HostBinding 绑定宿主的属性或者样式

@HostBinding('style.display') display = "grid";// 用样式绑定代替rd2的 this.setStyle('display','grid');

@HostListener 绑定宿主的事件

@HostListener('click',['$event.target'])// 第一个参数是事件名,第二个是事件携带参数

Angular生命周期

生命周期函数通俗的讲就是组件创建、组件更新、组件销毁的时候会触发的一系列的方法

当 Angular 使用构造函数新建一个组件或指令后,就会按下面规定的顺序在特定时刻调用生命周期钩子

constructor :构造函数永远首先被调用,一般用于变量初始化以及类实例化

ngOnChanges :被绑定的输入属性变化时被调用,首次调用一定在 ngOnInit 之前。输入属性发生变化是触发,但组件内部改变输入属性是不会触发的。注意:如果组件没有输入,或者使用它时没有提供任何输入,那么框架就不会调用 ngOnChanges

ngOnInit :组件初始化时被调用,在第一轮 ngOnChanges 完成之后调用,只调用一次。使用 ngOnInit 可以在构造函数之后马上执行复杂的初始化逻辑,同时在 Angular 设置完输入属性之后,可以很安全的对该组件进行构建

ngDoCheck :脏值检测时调用,在变更检测周期中 ngOnChanges 和 ngOnInit 之后

ngAfterContentInit :内容投影ng-content完成时调用,只在第一次 ngDoCheck 之后调用

ngAfterContentChecked: 每次完成被投影组件内容的变更检测之后调用(多次)

ngAfterViewInit :组件视图及子视图初始化完成时调用,只在第一次 ngAfterContentChecked 调用一次

ngAfterViewChecked: 检测组件视图及子视图变化之后调用(多次)

ngOnDestroy 当组件销毁时调用,可以反订阅可观察对象和分离事件处理器,以防内存泄漏


Angular路由

路由(导航)本质上是切换视图的一种机制,路由的导航URL并不真实存在

Angular 的路由借鉴了浏览器URL变化导致页面切换的机制

Angular 是单页程序,路由显示的路径不过是一种保存路由状态的机制,这个路径在 web 服务器上不存在

路由基本配置

/** * 在功能模块中定义子路由后,只要导入该模块,等同于在根路由中直接定义 * 也就是说在 AppModule 中导入 HomeModule 的时候, * 由于 HomeModule 中导入了 HomeRouting Module * 在 HomeRoutingModule 中定义的路由会合并到根路由表 * 相当于直接在根模块中定义下面的数组。 * const routes = [{ *   path: 'home', *   component: HomeContainerComponent * }] */const routes: Routes = [  {path: 'home', component: HomeComponent},  {path: 'news', component: NewsComponent},  {path: 'newscontent/:id', component: NewscontentComponent},  // 配置动态路由  {    path: '',    redirectTo: '/home',  // 重定向    pathMatch: 'full'},  //匹配不到路由的时候加载的组件 或者跳转的路由  {     path: '**', /*任意的路由*/     // component:HomeComponent     redirectTo:'home'  }]@NgModule({  /**   * 根路由使用 `RouterModule.forRoot(routes)` 形式。   * 而功能模块中的路由模块使用 `outerModule.forChild(routes)` 形式。   * 启用路由的 debug 跟踪模式,需要在根模块中设置 `enableTracing: true`   */  imports: [RouterModule.forRoot(routes, { enableTracing: true })],  exports: [RouterModule]})export class AppRoutingModule { }

激活路由

找到 app.component.html 根组件模板,配置 router-outlet

通过模版属性访问路由,即路由链接 routerLink

<h1>  <a [routerLink]="['/home']">首页</a>  <a [routerLink]="['/home',tab.link]">首页</a><!-- 路径参数 -->  <a [routerLink]="['/home',tab.link,{name:'val1'}]">首页</a> <!-- 路径对象参数 -->  <a [routerLink]="['/home']" [queryParams]="{name:'val1'}">首页</a> <!-- 查询参数 --></h1><router-outlet></router-outlet>  <!-- 路由插座,占位标签 --><!--  路由显示的内容是插入到 router-outlet 的同级的下方节点  而不是在 router-outlet 中包含--><!--  当事件处理或者达到某个条件时,可以使用手动跳转this.router.navigate(['home']); this.router.navigate(['home',tab.link]); this.router.navigate(['home',tab.link,{name:'val1'}]); this.router.navigate(['home'],{queryParams:{name:'val1'}}); -->

控制路由激活状态的样式 routerLinkActive

<h1>    <a routerLink="/home" routerLinkActive="active">首页</a>    <a routerLink="/news" routerLinkActive="active">新闻</a></h1><h1>   <a [routerLink]="[ '/home' ]" routerLinkActive="active">首页</a>   <a [routerLink]="[ '/news' ]" routerLinkActive="active">新闻</a></h1>.active{   color:red;}

路由参数

路径参数读取

this.route.paramsMap.subscribe(params => {...})

查询参数读取

this.route.queryParamsMap.subscribe(params => {...})

路由传递一个参数及其接收方法:

传递参数:path:’info/:id’

接收参数:

constructor(private routerInfo: ActivatedRoute){}ngOnInit(){this.routerInfo.snapshot.params['id']}

路由传递多个参数及其接收方法:

传递:[queryParams]=‘{id:1,name:‘crm’}’

接收参数:

constructor(private routerInfo: ActivatedRoute){}ngOnInit(){this.routerInfo.snapshot.params['id']this.routerInfo.snapshot.params['name']}

路由懒加载

懒加载子模块,子模块需要配置路由设置启动子模块 loadChildren

const routes: Routes = [    {path:'user',loadChildren:'./module/user/user.module#UserModule' },    {path:'product',loadChildren:'./module/product/product.module#ProductModule'},    {path:'article',loadChildren:'./module/article/article.module#ArticleModule'},    {path:'**',redirectTo:'user'}];// 上面好像会报错 Error find module // 配置懒加载const routes: Routes = [    {path:'user',loadChildren:()=>import('./module/user/user.module').then(mod=>mod.UserModule)},    {path:'article',loadChildren:()=>import('./module/article/article.module').then(mod=>mod.ArticleModule)},    {path:'product',loadChildren:()=>import('./module/product/product.module').then(mod=>mod.ProductModule)},    {path:'**',redirectTo:'user'}];

Angular服务

组件不应该直接获取或保存数据,应该聚焦于展示数据,而把数据访问的职责委托给某个服务

获取数据和视图展示应该相分离,获取数据的方法应该放在服务中

类似 VueX,全局的共享数据(通用数据)及非父子组件传值、共享数据放在服务中

组件之间相互调用各组件里定义的方法

多个组件都用的方法(例如数据缓存的方法)放在服务(service)里

import { Injectable } from '@angular/core';@Injectable({  providedIn: 'root',})export class HeroService {  aa = 'abc';  constructor(){ }  ngOnInit(){ }}import { HeroService } from '../../../services/hero/hero.service';export class AComponent implements OnInit{  constructor(private heroService : HeroService) {} //实例化  ngOnInit(){    console.log(this.heroService.aa)  }}

@Injectable()装饰器

在 Angular 中,要把一个类定义为服务,就要用 @Injectable() 装饰器来提供元数据,以便让 Angular 把它作为依赖注入到组件中。

同样,也要使用 @Injectable () 装饰器来表明一个组件或其它类(比如另一个服务、管道或 NgModule)拥有一个依赖。

@Injectable () 装饰器把这个服务类标记为依赖注入系统的参与者之一,它是每个 Angular 服务定义中的基本要素。

在未配置好 Angular 的依赖注入器时,Angular 实际上无法将它注入到任何位置。

@Injectable () 装饰器具有一个名叫 providedIn 的元数据选项,providedIn 设置为 'root',即根组件中,那么该服务就可以在整个应用程序中使用了。

providedIn 提供这些值:‘root''platform''any'null

对于要用到的任何服务,必须至少注册一个提供者。

服务可以在自己的元数据中把自己注册为提供者,可以让自己随处可用,也可以为特定的模块或组件注册提供者。

要注册提供者,就要在服务的 @Injectable () 装饰器中提供它的元数据,或者在 @NgModule ()@Component () 的元数据中。

在组件中提供服务时,还可以使用 viewProdiversviewProviders 对子组件树不可见

可以使用不同层级的提供者来配置注入器,也表示该服务的作用范围

Angular 创建服务默认采用的方式:在服务本身的 @Injectable () 装饰器中

该服务只在某服务中使用:在 NgModule 的 @NgModule () 装饰器中

该服务在某组件中使用:在组件的 @Component () 装饰器中

依赖注入

在项目中,有人提供服务,有人消耗服务,而依赖注入的机制提供了中间的接口,并替消费者创建并初始化处理

消费者只需要知道拿到的是完整可用的服务就好,至于这个服务内部的实现,甚至是它又依赖了怎样的其他服务,都不需要关注。

Angular 通过 service共享状态,而这些管理状态和数据的服务便是通过依赖注入的方式进行处理的

Angular 的 service 的本质就是依赖注入,将service作为一个Injector注入到component

归根到底,很多时候我们创建服务,是为了维护公用的状态和数据,通过依赖注入的方式来规定哪些组件可共享

在这里插入图片描述

正是因为 Angular 提供的这种依赖注入机制,才能在构造函数中直接声明实例化

  constructor(private heroService : HeroService) {} // 依赖注入

在这里插入图片描述

先看一下 Angular 中 TS 单文件的注入

// 首先写 @injectable 我们需要注入的东西,比如说 product@Injectable()class Product {  constructor(    private name: string,    private color: string,    private price: number,  ) { }}class PurchaseOrder {  constructor(private product: Product){ }} export class HomeGrandComponent implements OnInit {  constructor() { }  ngOnInit() {    // 构造一个 injector 用 create 方法 里面 providers 数组中写我们需要构造的东西    const injector = Injector.create({      providers: [        {          provide: Product,          // 构造 Product 在 useFactory 中就会把上面定义的 product 注入到这里          useFactory: () => {            return new Product('大米手机', '黑色', 2999);          },          deps: []        },        {          provide: PurchaseOrder,          deps: [Product]        },        {          provide: token,          useValue: { baseUrl: 'http://local.dev' }        }      ]    });     console.log('injector获取product', injector.get(PurchaseOrder).getProduct);    console.log(injector.get(token));  }

再看一下Angular 中 module 模块的注入

// .service.ts 中 @Injectable () 依赖注入@Injectable()export class HomeService {  imageSliders: ImageSlider[] = [    {      imgUrl:'',      link: '',      caption: ''    }  ]  getBanners() {    return this.imageSliders;  }}// 使用模块对应的.module.ts 中@NgModule({  declarations: [    HomeDetailComponent,  ],  providers:[HomeService], // 在 providers 直接写对应服务,直接将服务注入模块  imports: [SharedModule, HomeRoutingModule]})

不管是在组件内还是在模块内,我们使用 providers 的时候,就是进行了一次依赖注入的注册和初始化

其实模块类(NgModule)也和组件一样,在依赖注入中是一个注入器,作为容器提供依赖注入的接口

NgModule 使我们不需要在一个组件中注入另一个组件,通过模块类(NgModule)可以进行获取和共享


Angular 管道

Angular 管道是编写可以在 HTML 组件中声明的显示值转换的方法

管道将数据作为输入并将其转换为所需的输出

管道其实就是过滤器,用来转换数据然后显示给用户

管道将整数、字符串、数组和日期作为输入,用 | 分隔,然后根据需要转换格式,并在浏览器中显示出来

在插值表达式中,可以定义管道并根据情况使用

Angular 应用程序中可以使用许多类型的管道

内置管道

String -> String UpperCasePipe 转换成大写字符LowerCasePipe 转换成小写字符TitleCasePipe 转换成标题形式,第一个字母大写,其余小写 Number -> String DecimalPipe 根据数字选项和区域设置规则格式化值PercentPipe 将数字转换为百分比字符串CurrencyPipe 改变人名币格式 Object -> String JsonPipe 对象序列化DatePipe 日期格式转换 Tools SlicePipe 字符串截取AsyncPipe 从异步回执中解出一个值I18nPluralPipe 复数化I18nSelectPipe 显示与当前值匹配的字符串

使用方法

<div>{{ 'Angular' | uppercase }}</div>  <!-- Output: ANGULAR --><div>{{ data | date:'yyyy-MM-dd' }}</div>  <!-- Output: 2022-05-17 --><div>{{ { name: 'ccc' } | json }}</div>  <!-- Output: { "name": "ccc" } --><!-- 管道可以接收任意数量的参数,使用方式是在管道名称后面添加: 和参数值若需要传递多个参数则参数之间用冒号隔开 --><!-- 可以将多个管道连接在一起,组成管道链对数据进行处理 --><div>{{ 'ccc' | slice:0:1 | uppercase }}</div>

自定义管道

管道本质上就是个类,在这个类里面去实现 PipeTransfrom 接口的 transform 这个方法

使用 @Pipe 装饰器定义 Pipemetadata 信息,如 Pipe 的名称 - 即 name 属性实现 PipeTransform 接口中定义的 transform 方法
// 引入PipeTransform是为了继承transform方法import { Pipe, PipeTransform } form '@angular/core'; // name属性值惯用小驼峰写法, name的值为html中 | 后面的名称@Pipe({ name: 'sexReform' }) export class SexReformPipe implements PipeTransform {    transform(value: string, args?: any): string {    // value的值为html中 | 前面传入的值, args为名称后传入的参数        switch(value){            case 'male': return '男';            case 'female': return '女';            default: return '雌雄同体';        }     }}// demo.component.tsexport Class DemoComponent {    sexValue = 'female';}// demo.component.html<span>{{ sexValue | sexReform }}</span>// 浏览器输出女// 管道可以链式使用,还可以传参<span> {{date | date: 'fullDate' | uppercase}} </span>// 每一个自定义管道都需要实现 PipeTransform 接口,这个接口非常简单,只需要实现 transform 方法即可。// transform()方法参数格式 - transform(value: string, args1: any, args2?: any): // value为传入的值(即为需要用此管道处理的值, | 前面的值); // args 为传入的参数(?:代表可选);// html 中使用管道格式 - {{ 数据 | 管道名 : 参数1 : 参数2 }}// 与 component 一样,pipe 需要先在 declarations 数组中声明后使用

Angular操作DOM

原生JS操作

ngAfterViewInit(){   var boxDom:any=document.getElementById('box');   boxDom.style.color='red';}

ElementRef

ElementRef 是对视图中某个原生元素的包装类

因为 DOM 元素不是 Angular 中的类,所以需要一个包装类以便在 Angular 中使用和标识其类型

ElementRef 的背后是一个可渲染的具体元素。在浏览器中,它通常是一个 DOM 元素

class ElementRef<T> {  constructor(nativeElement: T)  nativeElement: T  //背后的原生元素,如果不支持直接访问原生元素,则为 null(比如:在 Web Worker 环境下运行此应用的时候)。}

当需要直接访问 DOM 时,请把本 API 作为最后选择 。优先使用 Angular 提供的模板和数据绑定机制

如果依赖直接访问 DOM 的方式,就可能在应用和渲染层之间产生紧耦合。这将导致无法分开两者,也就无法将应用发布到 Web Worker 中

ViewChild

使用模板和数据绑定机制,使用 @viewChild

// 模版中给 DOM 起一个引用名字,以便可以在组件类或模版中进行引用 <div #myattr></div>// 引入 ViewChildimport { ViewChild,ElementRef } from '@angular/core';// 用 ViewChild 绑定 DOM@ViewChild('myattr') myattr: ElementRef;// 在 ngAfterViewInit 生命周期函数里可以很安全的获取 ViewChild 引用的 DOMngAfterViewInit(){   let attrEl = this.myattr.nativeElement;}

父组件中可以通过 ViewChild 调用子组件的方法

// 给子组件定义一个名称<app-footer #footerChild></app-footer>// 引入 ViewChildimport { Component, OnInit ,ViewChild} from '@angular/core';// ViewChild 和子组件关联起来 // 如果想引用模版中的 Angular 组件,ViewChild 中可以使用引用名,也可以使用组件类型@ViewChild('footerChild') footer;// @ViewChild('imageSlider', { static: true }) // static指定是动态还是静态,在*ngFor或者*ngIf中是动态,否则即为静态,动态为 true// 调用子组件run(){   this.footer.footerRun();}

引用多个模版元素,可以用@ViewChildren,在ViewChildren中可以使用引用名

或者使用 Angular 组件/指令的类型,声明类型为 QueryList<?>

<img  #img  *ngFor="let slider of sliders"  [src]="slider.imgUrl"  [alt]="slider.capiton">// 使用 ViewChildren 引用获取@ViewChildren(’img‘);// 使用类型引用获取imgs: QueryList<ElementRef>;

Renderer2

Renderer2 是 Angular 提供的操作 element 的抽象类,使用该类提供的方法,能够实现在不直接接触 DOM 的情况下操作页面上的元素。

Renderer2 的常用方法:

addClass /removeClassdirective 的宿主元素添加或删除 class
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core';@Directive({    selector: '[testRenderer2]'})export class TestRenderer2Directive implements OnInit {    constructor(private renderer: Renderer2, private el: ElementRef) {} // 实例化    ngOnInit() {    this.renderer.addClass(this.el.nativeElement, 'test-renderer2');    // this.renderer.removeClass(this.el.nativeElement, 'old-class');    }}
createElement /appendChild/createText 创建 DIV 元素,插入文本内容,并将其挂载到宿主元素上
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core';constructor(private renderer: Renderer2, private el: ElementRef) {}ngOnInit() {    const div = this.renderer.createElement('div');    const text = this.renderer.createText('Hello world!');        this.renderer.appendChild(div, text);    this.renderer.appendChild(this.el.nativeElement, div);}
setAttribute /removeAttribute 在宿主元素上添加或删除 attribute
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core';constructor(private renderer: Renderer2, private el: ElementRef) {}ngOnInit() {    this.renderer.setAttribute(this.el.nativeElement, 'aria-hidden', 'true');}
setStyle /removeStyle 在宿主元素上添加 inline-style
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core';constructor(private renderer: Renderer2, private el: ElementRef) {}ngOnInit() {    this.renderer.setStyle(        this.el.nativeElement,        'border-left',        '2px dashed olive'    );}

移除 inline-style :

constructor(private renderer: Renderer2, private el: ElementRef) {}ngOnInit() {    this.renderer.removeStyle(this.el.nativeElement, 'border-left');}
setProperty 设置宿主元素的 property 的值
constructor(private renderer: Renderer2, private el: ElementRef) {}ngOnInit() {    this.renderer.setProperty(this.el.nativeElement, 'alt', 'Cute alligator');}

直接操作DOM,Angular不推荐。尽量采用 @viewChildrenderer2 组合,Angular推荐使用 constructor(private rd2: Renderer2) {} 依赖注入,

import {  Component,  OnInit,  Renderer2,  ViewChild,} from '@angular/core';import { AboxItemComponent } from './abox-item/abox-item.component'; @Component({  selector: 'app-abox',  templateUrl: './abox.component.html',  styleUrls: ['./abox.component.less'],})export class AboxComponent implements OnInit {  private container;  activeIndex: number;  @ViewChild('containers') containers: any;  constructor(private rd2: Renderer2) {}   ngOnInit(): void {}   ngAfterViewInit(): void {    this.container = this.containers.nativeElement;    this.initCarouselWidth();  }      initCarouselWidth() {    this.rd2.setStyle(this.container, 'width', '100px');  }}

Angular网络请求

HttpClient

需导入 HttpClientModule ,只在根模块中导入,并且整个应用只需导入一次,不用在其他模块导入

在构造函数中注入HttpClientget/post方法对应HTTP方法,这些方法是泛型的,可以直接把返回的JSON转换成对应类型。若是不规范的请求,使用request方法

返回的值是 Observable,必须订阅才会发送请求,否则不会发送

get 请求数据

// 在 app.module.ts 中引入 HttpClientModule 并注入import {HttpClientModule} from '@angular/common/http';imports: [  BrowserModule,  HttpClientModule]// 在用到的地方引入 HttpClient 并在构造函数声明import {HttpClient} from "@angular/common/http";constructor(private http: HttpClient,private cd: ChangeDetectorRef) { } // 依赖注入// get 请求数据var api = "http://baidu.com/api/productlist";this.http.get(api).subscribe(response => {  console.log(response);  this.cd.markForCheck();  // 如果改变了脏值检测的变更原则 changeDetection: ChangeDetectionStrategy.OnPush  // 则需要使用 this.cd.markForCheck() 手动提醒 Angular 这里需要进行脏值检测});

post 提交数据

// 在 app.module.ts 中引入 HttpClientModule 并注入import {HttpClientModule} from '@angular/common/http';imports: [   BrowserModule,   HttpClientModule]// 在用到的地方引入 HttpClient 、HttpHeaders 并在构造函数声明 HttpClientimport {HttpClient,HttpHeaders} from "@angular/common/http";constructor(private http:HttpClient) { } // 实例化// post 提交数据const httpOptions = {    headers: new HttpHeaders({ 'Content-Type': 'application/json' })};var api = "http://127.0.0.1:4200/doLogin";this.http.post(api,{username:'瑞萌萌',age:'22'},httpOptions).subscribe(response => {console.log(response);});

Jsonp请求数据

// 在 app.module.ts 中引入 HttpClientModule、HttpClientJsonpModule 并注入import {HttpClientModule,HttpClientJsonpModule} from'@angular/common/http';imports: [   BrowserModule,   HttpClientModule,   HttpClientJsonpModule]// 在用到的地方引入 HttpClient 并在构造函数声明import {HttpClient} from "@angular/common/http";constructor(private http:HttpClient) { } // 实例化// jsonp 请求数据var api = "http://baidu.com/api/productlist";this.http.jsonp(api,'callback').subscribe(response => {   console.log(response);});

拦截器

Angular 拦截器是 Angular 应用中全局捕获和修改 HTTP 请求和响应的方式,例如携带 Token 和捕获 Error

前提是只能拦截使用 HttpClientModule 发出的请求,如果使用 axios 则拦截不到

创建拦截器

// 使用命令 ng g interceptor name,在这里创建拦截器 ng g interceptor LanJieQi// cli 生成拦截器是没有简写方式的import { Injectable } from '@angular/core';import {  HttpRequest,  HttpHandler,  HttpEvent,  HttpInterceptor} from '@angular/common/http';import { Observable } from 'rxjs';@Injectable()export class LanJieQiInterceptor implements HttpInterceptor {  constructor() {}  // 默认的 intercept() 方法只是单纯的将请求转发给下一个拦截器(如果有),并最终返回 HTTP 响应体的 Observable  // request: HttpRequest<unknown> 表示请求对象,包含了请求相关的所有信息,unknown指定请求体body的类型  // next: HttpHandler 请求对象修改完成,将修改后的请求对象通过next中的handle方法传回真正发送请求的方法中  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {    // next 对象表示拦截器链表中的下一个拦截器(在应用中可以设置多个拦截器)    return next.handle(request);  }}

注入拦截器

// 在 @NgModule 模块中注入拦截器// 拦截器也是一个由 Angular 依赖注入 (DI) 系统管理的服务,也必须先提供这个拦截器类,才能使用它// 由于拦截器是 HttpClient 服务的依赖,所以必须在提供 HttpClient 的同一个(或其各级父注入器)注入器中提供这些拦截器@NgModule({  imports: [    HttpClientModule    // others...  ],  providers: [    {      provide: HTTP_INTERCEPTORS,      useClass: LanJieQiInterceptor,      // multi: true 表明 HTTP_INTERCEPTORS 是一个多重提供者的令牌,表示这个令牌可以注入多个拦截器      multi: true    },  ],  bootstrap: [AppComponent]})export class AppModule { }

请求头拦截

@Injectable()export class LanJieQiInterceptor implements HttpInterceptor {  constructor() {}  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {    // 为了统一设置请求头,需要修改请求    // 但 HttpRequest 和 HttpResponse 实例的属性却是只读(readonly)的    // 所以修改前需要先 clone 一份,修改这个克隆体后再把它传给 next.handle()    let req = request.clone({    setHeaders:{      token:"123456" // 在请求头中增加 token:123456    }// setHeaders 和 headers: request.headers.set('token', '123456') 一致  })  return next.handle(req)// 将修改后的请求返回给应用  }}

响应捕获

@Injectable()export class LanJieQiInterceptor implements HttpInterceptor {  constructor() {}  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {    let req = request.clone({      setHeaders:{        token:"123456" // 在请求头中增加 token:123456      }    })    return next.handle(req).pipe(      retry(2), // RxJS的重试操作符 retry 发现异常后自动再请求2次      catchError((error:HttpErrorResponse) => throwError(error))    )  }}

如果有多个拦截器,请求顺序是按照配置顺序执行,响应拦截则是相反的顺序

如果提供拦截器的顺序是先 A再 B再 C,那么请求阶段的执行顺序就是 A->B->C,而响应阶段的执行顺序则是 C->B->A


Angular表单

模版驱动表单

模板驱动表单在往应用中添加简单的表单时非常有用,但是不像响应式表单那么容易扩展

如果有非常基本的表单需求和简单到能用模板管理的逻辑,就使用模板驱动表单

响应式表单和模板驱动表单共享了一些底层构造块:

FormControl 实例用于追踪单个表单控件的值和验证状态

FormGroup 用于追踪一个表单控件组的值和状态

FormArray 用于追踪表单控件数组的值和状态,有长度属性,通常用来代表一个可以增长的字段集合

ControlValueAccessor 用于在 Angular 的 FormControl 实例和原生 DOM 元素之间创建一个桥梁

FormControlFormGroup 是 angular 中两个最基本的表单对象

FormControl 代表单一的输入字段,它是 Angular 表单中最小单员,它封装了这些字段的值和状态,比如是否有效、是否脏(被修改过)或是否有错误等

FormGroup 可以为一组 FormControl 提供总包接口(wrapper interface),来管理多个 FormControl

当我们试图从 FormGroup 中获取 value 时,会收到一个 “键值对” 结构的对象

它能让我们从表单中一次性获取全部的值而无需逐一遍历 FormControl,使用起来相当顺手

FormGroupFormControl 都继承自同一个祖先 AbstractControltractControl(这是 FormControlFormGroupFormArray 的基类)

首先加载 FormsModule

// 先在 NgModule 中导入了 FormsModule 表单库// FormsModule 为我们提供了一些模板驱动的指令,例如:ngModel、NgFormimport {   FormsModule} from '@angular/forms'; @NgModule({   declarations: [     FormsDemoApp,     DemoFormSku,     // ... our declarations here   ],   imports: [     BrowserModule,     FormsModule,  ],   bootstrap: [ FormsDemoApp ] }) class FormsDemoAppModule {}

接下来创建一个模版表单

 <div>      <h2>基础表单:商品名称</h2>      <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">        <div class="sku">          <label for="skuInput">商品名称:</label>          <input            type="text"            id="skuInput"            placeholder="商品名称"            name="sku" //使用form时必须定义,可以理解为当前控件的名字            ngModel          />        </div>        <button>提交</button>      </form>    </div>

我们导入了 FormsModule,因此可以在视图中使用 NgForm

当这些指令在视图中可用时,它就会被附加到任何能匹配其 selector 的节点上

NgForm 做了一件便利但隐晦的工作:它的选择器包含 form 标签(而不用显式添加 ngForm 属性)

这意味着当导入 FormsModule 时候,NgForm 就会被自动附加到视图中所有的标签上

NgForm 提供了两个重要的功能:

一个 ngFormFormGroup 对象一个输出事件 (ngSubmit)
 <form #f="ngForm" (ngSubmit)="onSubmit(f.value)" > <!-- 这里使用了 #f=“ngForm”,#v=thing 的意思是我们希望在当前视图中创建一个局部变量这里为视图中的 ngForm 创建了一个别名,并绑定到变量 #f这个 ngForm 是由 NgForm 指令导出的ngForm 的类型的对象是 FormGroup 类型的这意味着可以在视图中把变量 f 当作 FormGroup 使用,而这也正是我们在输出事件 (ngSubmit) 中的使用方法在表单中绑定 ngSubmit 事件 (ngSubmit)=“onSubmit (f.value)“(ngSubmit) 来自 NgForm 指令onSubmit() 将会在组件类中进行定义f 就是 FormGroup ,而 .value 会以键值对的形式返回 FormGroup 中所有控件的值总结:当提交表单时,将会以该表单的值作为参数,调用组件实例上的 `onSubmit` 方法-->

NgModel 会创建一个新的 FormControl 对象,把它自动添加到父 FormGroup 上(这里也就是 form 表单对象)

并把这个 FormControl 对象绑定到一个 DOM 上

也就是说,它会在视图中的 input 标签和 FormControl 对象之间建立关联

这种关联是通过 name 属性建立的,在本例中是 "name"

响应式表单

使用 ngForm 构建 FormControlFormGroup 很方便,但是无法提供定制化选项,因此引入响应式表单

响应式表单提供了一种模型驱动的方式来处理表单输入,其中的值会随时间而变化

使用响应式表单时,通过编写 TypeScript 代码而不是 HTML 代码来创建一个底层的数据模型

在这个模型定义好以后,使用一些特定的指令将模板上的 HTML 元素与底层的数据模型连接在一起

FormBuilder 是一个名副其实的表单构建助手(可以把他看作一个 “工厂” 对象)

在先前的例子中添加一个 FormBuilder,然后在组件定义类中使用 FormGroup

// 先在 NgModule 中导入了 ReactiveFormsModule 表单库import {   ReactiveFormsModule } from '@angular/forms'; @NgModule({  imports: [    FormsModule,    ReactiveFormsModule  ]}) // 使用 formGroup 和 formControl 指令来构建这个组件,需要导入相应的类import {   FormBuilder,   FormGroup,  ReactiveFormsModule} from '@angular/forms'; // 在组件类上注入一个从 FormBuilder 类创建的对象实例,并把它赋值给 fb 变量(来自构造函数)export class DemoFormSkuBuilder {   myForm: FormGroup;  // myForm 是 FormGroup 类型  constructor(fb: FormBuilder) {     // FormBuilder 中的 group 方法用于创建一个新的 FormGroup    // group 方法的参数是代表组内各个 FormControl 的键值对    this.myForm = fb.group({  // 调用 fb.group () 来创建 FormGroup      // 设置一个名为 sku 的控件,控件的默认值为 "123456"      'sku': ['123456']     });   }  onSubmit(value: string): void {     console.log('submit value:', value);   } }

在视图表单中使用自定义的 FormGroup

<h2 class="ui header">Demo Form: Sku with Builder</h2><!--  当导入 FormsModule 时,ngForm 就会自动创建它自己的 FormGroup但这里不希望使用外部的 FormGroup,而是使用 FormBuilder 创建这个 myForm 实例变量Angular提供了 formGroup 指令,能让我们使用现有的 FormGroupNgForm 不会应用到带 formGroup 属性的节点上这里我们告诉Angular,想用 myForm 作为这个表单的 FormGroup--><form [formGroup]="myForm"   <label for="skuInput"> SKU </label>   <input type="text"      id="skuInput"      placeholder="SKU"      [formControl]="myForm.controls['sku']"><!--  将 FormControl 绑定到 input 标签上 : ngModel 会创建一个新的 FormControl 对象并附加到父 FormGroup 中但在例子中,我们已经用 FormBuilder 创建了自己的 FormControl要将现有的 FormControl 绑定到 input 上,可以用 formControl 指令将 input 标签上的 formControl 指令指向 myForm.controls 上现有的 FormControl 控件 sku  -->

记住以下两点:

如果想隐式创建新的 FormGroup 和 FormControl,使用:ngForm、ngModel如果要绑定一个现有的 FormGroup 和 FormControl,使用:formGroup、formControl

表单验证

用户输入的数据格式并不总是正确的,如果有人输入错误的数据格式,我们希望给他反馈并阻止他提交表单

因此,我们要用到验证器,由 validators 模块提供

Validators.required 是最简单的验证,表明指定的字段是必填项,否则就认为 FormControl 是无效的

如果 FormGroup 中有一个 FormControl 是无效的, 那整个 FormGroup 都是无效的

要为 FormControl 对象分配一个验证器 ,可以直接把它作为第二个参数传给 FormControl 的构造函数

const control = new FormControl('name', Validators.required);// 在组件定义类中使用 FormBuilder  constructor(fb: FormBuilder) {     this.myForm = fb.group({       'name': ['',Validators.required]     });     this.name = this.myForm.controls['name'];   } 

在视图中检查验证器的状态,并据此采取行动

template:`<div>      <h2>商品表单:商品名称</h2>      <form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">        <div>          <label for="nameInput">商品名称:</label>          <input            type="text"            id="nameInput"            placeholder="请输入名称"            [formControl]="myForm.controls['name']"          />          <div style="color:red" *ngIf="!name.valid">            名称无效          </div>          <div style="color:red" *ngIf="name.hasError('textinvalid')">            名称不是以“123”开头          </div>          <div *ngIf="name.dirty">            数据已变动          </div>        </div>        <div>          <label for="codeInput">商品料号:</label>          <input            type="text"            id="codeInput"            placeholder="请输入料号"            [formControl]="myForm.controls['code']"          />          <div            style="color:red"            *ngIf="myForm.controls.code.hasError('required')"          >            该项必填          </div>          <div            style="color:red"            *ngIf="myForm.controls.code.hasError('pattern')"          >            只可输入数字和英文          </div>        </div>        <div style="color:green" *ngIf="myForm.isvalid">          表单无效        </div>        <div style="color:green" *ngIf="myForm.valid">          表单有效        </div>        <button type="submit">提交</button>      </form>    </div>`export class NonInWarehouseComponent implements OnInit {  myForm: FormGroup;  name: AbstractControl;  constructor(fb: FormBuilder) {    this.myForm = fb.group({      name: ['牛奶', Validators.compose([Validators.required, textValidator])],      code: ['', [Validators.required, Validators.pattern('^[A-Za-z0-9]*$')]],    });    this.name = this.myForm.controls.name;  }  ngOnInit() {    const nameControl = new FormControl('nate');    console.log('nameControl', nameControl);  }  onSubmit(a: any) {    console.log('a', a);  }}

内置校验器

Angular 提供了几个内置校验器,下面是比较常用的校验器:

Validators.required - 表单控件值非空Validators.email - 表单控件值的格式是 emailValidators.minLength() - 表单控件值的最小长度Validators.maxLength() - 表单控件值的最大长度Validators.pattern() - 表单控件的值需匹配 pattern 对应的模式(正则表达式)

自定义验证器

假设我们的 name 有特殊的验证需求,比如 name 必须以 123 作为开始

当输入值(控件的值 control.value)不是以 123 作为开始时,验证器会返回错误代码 invalidSku

// angular 源代码中实现 Validators.required export class Validators {  // 接收一个 AbstractControl 对象作为输入static required(control: AbstractControl): ValidationErrors | null;}// 当验证器失败时,会返回一个 String Map<string,any> 对象,他的键是” 错误代码 “,它的值是 trueexport declare type ValidationErrors = {    [key: string]: any;};// 自定义验证器function textValidator(  controls: FormControl // 因为FormControl继承于 AbstractControl 所以也可以写成FormControl对象): {  [s: string]: boolean;} {  if (!controls.value.match(/^123/)) {    return { textinvalid: true };  }}

FormControl 分配验证器,但是 name 已经有一个验证器了,如何在同一个字段上添加多个验证器

Validators.compose 来实现

Validators.compose 把两个验证器包装在一起,我们可以将其赋值给 FormControl

只有当两个验证器都合法时,FormControl 才是合法的

Validators.compose([Validators.required, textValidator])// 不用compose   [Validators.required, textValidator]// 保留 compose 是为了向以前历史版本进行兼容,不用 compose 也可实现

动态表单

要实现 Angular 动态表单,主要使用 formArray 方法,formArray 生成的实例是一个数组,在这个数组中可以动态的放入 formGroupformControl,这样便形成了动态表单。

export class ReativeFormsComponent implements OnInit {  ngOnInit() {    this.addContact()  }  //动态表单  personMess: FormGroup = new FormGroup({    //生成动态表单数组    contacts: new FormArray([])   })  //获取数组对象  get contacts(){    return this.personMess.get('contacts') as FormArray  }  //增加一个表单组  addContact(){    let myContact = new FormGroup({      name: new FormControl(),      phone: new FormControl()    })    this.contacts.push(myContact)  }   //删除一个表单组  deleteContact(i:number){    this.contacts.removeAt(i)  }  //提交表单  OnSubmit() {    console.log(this.personMess.value)  }}
<form [formGroup]="personMess" (submit)="OnSubmit()">  <div formArrayName="contacts">    <!-- 注意:这里遍历的时contacts.controls -->    <div *ngFor="let contact of contacts.controls;let i =index" [formGroupName]="i">      <input type="text" formControlName="name">      <input type="text" formControlName="phone">      <button (click)="deleteContact(i)">删除信息</button>    </div>  </div>  <button (click)="addContact()">添加信息</button><br>  <input type="submit"></form>

Angular CDK

CDK 是 Component Dev kit 的简称,是 Angular Material 团队在开发 Library 时发现组件有很多相似的地方,最后进行了抽取,提炼出了公共的逻辑,这部分即是 CDK

官方用了一个很形象的比喻:如果组件库是火箭飞船,那么 CDK 就是发动机零件盒
在这里插入图片描述


点击全文阅读


本文链接:http://zhangshiyu.com/post/58037.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1