Angular7入門總結篇

一、Angular 介紹

Angualr 是一款來自谷歌的開源的 web 前端框架,誕生於 2009 年,由 Misko Hevery 等 人創建,後爲 Google 所收購。是一款優秀的前端 JS 框架,已經被用於 Google 的多款產品當中。

  • 根據項目數統計 angular(1.x 、2.x 、4.x、5.x、6.x、7.x)是現在網上使用量最大的框架
  • Angualr 基於 TypeScriptreactvue 相比, Angular 更適合中大型企業級項目。

目前 2018 年 11 月 25 日 angular 最新版本 angular7.x。根據官方介紹,Angular 每過幾個月 就會更新一個版本。此教程同樣適用於後期更新的 Angular8.xAngular9.x

image.png

學習 Angular 必備基礎

必備基礎:htmlcssjses6Typescript

二、Angular 環境搭建及創建項目

2.1 環境搭建

1. 安裝 nodejs

安裝 angular 的計算機上面必須安裝最新的 nodejs--注意安裝 nodejs 穩定版本

2. 安裝 cnpm

npm 可能安裝失敗建議先用 npm 安裝一下 cnpm 用淘寶鏡像安裝https://npm.taobao.org

npm install -g cnpm --registry=https://registry.npm.taobao.org

3. 使用 npm/cnpm 命令安裝 angular/cli

npm install -g @angular/cli 

# 或者 
cnpm install -g @angular/cli

ng v 查看版本信息

angular cli

4. 安裝插件

image.png

5. 安裝chrome擴展

https://augury.angular.io/

augury查看component結構,更方便調試

image.png

2.2 創建項目

# 創建項目
ng new my-app

cd my-app

# 運行項目
ng serve --open

2.3 目錄結構分析

image.png

詳情參考 https://www.angular.cn/guide/file-structure

app目錄(重點)

app目錄是我們要編寫的代碼目錄。我們寫的代碼都是放在這個目錄。
一個Angular程序至少需要一個模塊和一個組件。在我們新建項目的時候命令行已經默認生成出來了

image.png

  • app.component.ts:這個文件表示組件,
  • 組件是Angular應用的基本構建模塊,可以理解爲一段帶有業務邏輯和數據的Html
    我們來看看app.component.ts中的代碼,並解釋下代碼的意義

app.component.ts

/*這裏是從Angular核心模塊裏面引入了component裝飾器*/
import {Component} from '@angular/core';

/*用裝飾器定義了一個組件以及組件的元數據  所有的組件都必須使用這個裝飾器來註解*/
@Component({
  /*組件元數據  Angular會通過這裏面的屬性來渲染組件並執行邏輯
  * selector就是css選擇器,表示這個組件可以通過app-root來調用,index.html中有個<app-root>Loading...</app-root>標籤,這個標籤用來展示該組件的內容
  *templateUrl  組件的模板,定義了組件的佈局和內容
  *styleUrls   該模板引用那個css樣式
  * */
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
/*AppComponent本來就是一個普通的typescript類,但是上面的組件元數據裝飾器告訴Angular,AppComponent是一個組件,需要把一些元數據附加到這個類上,Angular就會把AppComponent當組件來處理*/
export class AppComponent {
  /*這個類實際上就是該組件的控制器,我們的業務邏輯就是在這個類中編寫*/
  title = '學習Angular';
}

組件相關的概念

  1. 組件元數據裝飾器(@Component
    簡稱組件裝飾器,用來告知Angular框架如何處理一個TypeScript類.
    Component裝飾器包含多個屬性,這些屬性的值叫做元數據,Angular會根據這些元數據的值來渲染組件並執行組件的邏輯
  2. 模板(Template
    我們可以通過組件自帶的模板來定義組件的外觀,模板以html的形式存在,告訴Angular如何來渲染組件,一般來說,模板看起來很像html,但是我們可以在模板中使用Angular的數據綁定語法,來呈現控制器中的數據。
  3. 控制器(controller
    控制器就是一個普通的typescript類,他會被@Component來裝飾,控制器會包含組件所有的屬性和方法,絕大多數的業務邏輯都是寫在控制器裏的。控制器通過數據綁定與模板來通訊,模板展現控制器的數據,控制器處理模板上發生的事件。

裝飾器,模板和控制器是組件的必備要素。還有一些可選的元素,比如:

  • 輸入屬性(@inputs):是用來接收外部傳入的數據的,Angular的程序結構就是一個組件樹,輸入屬性允許在組件樹種傳遞數據
    提供器(providers):這個是用來做依賴注入的
  • 生命週期鉤子(LifeCycle Hooks):一個組件從創建到銷燬的過程中會有多個鉤子會被觸發,類似於Android中的Activity的生命週期
  • 樣式表:組件可以關聯一些樣式表
  • 動畫(Animations): Angular提供了一個動畫包來幫助我們方便的創建一些跟組件相關的動畫效果,比如淡入淡出等
  • 輸出屬性(@Outputs):用來定義一些其他組件可能需要的事件或者用來在組件之間共享數據

組件的中關係就如下圖所示

image.png

下面我們來看看模塊文件

  • app.module.ts:這個文件表示模塊
  • AppComponent類似,模塊也需要裝飾器來裝飾
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';

@NgModule({
  declarations: [
   // 聲明模塊裏有什麼東西 只能聲明:組件/指令/管道
    AppComponent,
    HeroesComponent
  ],
  // 聲明該模塊所依賴的模塊
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  // 默認情況下是空的
  providers: [],
  // 聲明模塊的主組件是什麼
  bootstrap: [AppComponent]
})
export class AppModule { }

2.4 Angular cli

https://cli.angular.io

通過ng g列出當前命令

ng g

1. 創建新組件 ng generate component component-name

ng g component components/header 指定生成到哪個目錄

該命令會把生成的組件,添加到 src/app/app.module.ts 文件中 @NgModuledeclarations 列表中聲明

image.png

2. 使用 Angular CLI 創建一個名叫 hero 的服務

ng generate service hero

該命令會在 src/app/hero.service.ts 中生成 HeroService 類的骨架。 HeroService 類的代碼如下:

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

@Injectable({
  providedIn: 'root',
})
export class HeroService {

  constructor() { }

}

3. 添加 AppRoutingModule

ng generate module app-routing --flat --module=app
  • --flat 把這個文件放進了 src/app 中,而不是單獨的目錄中。
  • --module=app 告訴 CLI 把它註冊到 AppModuleimports 數組中。

生成的文件是這樣的:

src/app/app-routing.module.ts (generated)
content_copy
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class AppRoutingModule { }

修改後

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

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

三、angular組件及組件裏的模板

3.1 創建angualr組件

1. 創建組件

ng g component components/header

2. 使用組件

<app-header></app-header>

3.2 Angular 綁定數據

1. 數據文本綁定

定義數據幾種方式

定義數據幾種方式

<h1>{{title}}</h1>

2. 綁定HTML

 this.h="<h2>這是一個 h2 用[innerHTML]來解析</h2>"
 <div [innerHTML]="h"></div>

3.3 聲明屬性的幾種方式

  • public 共有(默認) 可以在類裏外使用
  • protected 保護類型 只能在當前類和子類中使用
  • private 私有類型 只能在當期類使用

3.4 綁定屬性

[]包裹

 <div [id]="id" [title]="msg">調試工具看看我的屬性</div>

image.png

3.5 數據循環 *ngFor

*1. ngFor 普通循環

export class HomeComponent implements OnInit {

  arr = [{ name: 'poetries', age: 22 }, { name: 'jing' , age: 31}];
  constructor() { }

  ngOnInit() {
  }

}
<ul *ngIf="arr.length>0">
      <li *ngFor="let item of arr">{{item.name}}- {{item.age}}</li>
</ul>

2. 循環的時候設置 key

<ul>
<li *ngFor="let item of list;let i = index;"> <!-- 把索引index賦給i -->
     {{item}} --{{i}}
</li> </ul>

3. template 循環數據

<ul>
  <li template="ngFor let item of list">
{{item}}
</li> </ul>

3.6 條件判斷 *ngIf

<p *ngIf="list.length > 3">這是 ngIF 判斷是否顯示</p>

<p template="ngIf list.length > 3">這是 ngIF 判斷是否顯示</p>

3.7 *ngSwitch

<ul [ngSwitch]="score">
<li *ngSwitchCase="1">已支付</li>
<li *ngSwitchCase="2">訂單已經確認</li> <li *ngSwitchCase="3">已發貨</li>
<li *ngSwitchDefault>無效</li>
</ul>

3.8 執行事件 (click)=”getData()”

<button class="button" (click)="getData()"> 點擊按鈕觸發事件
</button>
<button class="button" (click)="setData()"> 點擊按鈕設置數據
</button>
getData(){ /*自定義方法獲取數據*/ //獲取
  alert(this.msg);
} 
setData(){
    //設置值
    this.msg='這是設置的值';
}

3.9 表單事件

<input
type="text"
(keyup)="keyUpFn($event)"/>

<input type="text" (keyup)="keyUpFn($event)"/>
keyUpFn(e){
    console.log(e)
}

3.10 雙向數據綁定

<input [(ngModel)]="inputVal">

注意引入:FormsModule

import {FormsModule} from '@angular/forms'

NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    FooterComponent,
    NewsComponent
  ], 
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
<!--使用-->
<input type="text" [(ngModel)]="inputValue"/> {{inputValue}}

3. 11 [ngClass]、[ngStyle]

1. [ngClass]:

<div [ngClass]="{'red': true, 'blue': false}"> 
    這是一個 div
</div>
public flag=false;
<div [ngClass]="{'red': flag, 'blue': !flag}">
這是一個 div </div>
public arr = [1, 3, 4, 5, 6];
<ul>
<li *ngFor="let item of arr, let i = index"> <span [ngClass]="{'red': i==0}">{{item}}</span>
</li> </ul>

2. [ngStyle]:

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

3.12 管道

 public today=new Date();
 <p>{{today | date:'yyyy-MM-dd HH:mm:ss' }}</p>

其他管道

angular中的管道(pipe)是用來對輸入的數據進行處理,如大小寫轉換、數值和日期格式化等

angular中的管道(pipe) 以及自定義管道適用於angular4 angualr5 angualr6 angular7

常用的管道(pipe)有

1. 大小寫轉換

<!--轉換成大寫-->
<p>{{str | uppercase}}</p>

<!--轉換成小寫-->
<p>{{str | lowercase}}</p>

2. 日期格式轉換

<p>
{{today | date:'yyyy-MM-dd HH:mm:ss' }}
</p> 

3. 小數位數

接收的參數格式爲{最少整數位數}.{最少小數位數}-{最多小數位數}

<!--保留2~4位小數-->

<p>{{p | number:'1.2-4'}}</p> 

4. JavaScript 對象序列化

<p>
    {{ { name: 'semlinker' } | json }}
</p> 
<!-- Output: { "name": "semlinker" } -->

5. slice

<p>{{ 'semlinker' | slice:0:3 }}</p> 
<!-- Output: sem -->

6. 管道鏈

<p>
{{ 'semlinker' | slice:0:3 | uppercase }}
</p> 

<!-- Output: SEM -->

7. 自定義管道

自定義管道的步驟:

  • 使用 @Pipe 裝飾器定義 Pipemetadata 信息,如 Pipe 的名稱 - 即 name 屬性
  • 實現 PipeTransform 接口中定義的 transform 方法

7.1 WelcomePipe 定義

import { Pipe, PipeTransform } from '@angular/core';

[@Pipe](/user/Pipe)({ name: 'welcome' })

export class WelcomePipe implements PipeTransform {
  transform(value: string): string {
    if(!value) return value;
    if(typeof value !== 'string') {
      throw new Error('Invalid pipe argument for WelcomePipe');
    }
    return "Welcome to " + value;
  }
} 

7.2 WelcomePipe 使用

<div>
   <p ngNonBindable>{{ 'semlinker' | welcome }}</p>
   <p>{{ 'semlinker' | welcome }}</p> <!-- Output: Welcome to semlinker -->
</div>

7.3 RepeatPipe 定義

import {Pipe, PipeTransform} from '@angular/core';

[@Pipe](/user/Pipe)({name: 'repeat'})
export class RepeatPipe implements PipeTransform {
    transform(value: any, times: number) {
        return value.repeat(times);
    }
}

7.4 RepeatPipe 使用

<div>
   <p ngNonBindable>
   {{ 'lo' | repeat:3 }}
   </p>
   <p>
    {{ 'lo' | repeat:3 }}
   </p> 
   <!-- Output: lololo -->
</div>

3.13 實現一個人員登記表單-案例

image.png

# 創建組件
ng g component components/form
import { Component, OnInit } from '@angular/core';

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

  public peopleInfo:any = {
    username: '',
    sex: '2',
    cityList: ['北京', '上海', '深圳'],
    city: '上海',

    hobby:[{
          title: '喫飯',
          checked:false
      },{
            title:'睡覺',
            checked:false
        },{

          title:'敲代碼',
          checked:true
      }],

      mark:''
  }

  constructor() { }

  ngOnInit() {

  }
  doSubmit(){
    /*    
    jquery  dom操作
      <input type="text" id="username" />
      let usernameDom:any=document.getElementById('username');
      console.log(usernameDom.value);    
    */

    console.log(this.peopleInfo);
  }
}
<h2>人員登記系統</h2>

<div class="people_list">
  <ul>
    <li>姓 名:<input type="text" id="username" [(ngModel)]="peopleInfo.username" value="fonm_input" /></li>
    <li>性 別:
      <input type="radio" value="1" name="sex" id="sex1" [(ngModel)]="peopleInfo.sex"> <label for="sex1">男 </label>   
      <input type="radio" value="2" name="sex"  id="sex2" [(ngModel)]="peopleInfo.sex"> <label for="sex2">女 </label>
    </li>
   <li>
    城 市:
      <select name="city" id="city" [(ngModel)]="peopleInfo.city">
          <option [value]="item" *ngFor="let item of peopleInfo.cityList">{{item}}</option>
      </select>
    </li>
    <li>
        愛 好:
        <span *ngFor="let item of peopleInfo.hobby;let key=index;">
            <input type="checkbox"  [id]="'check'+key" [(ngModel)]="item.checked"/> <label [for]="'check'+key"> {{item.title}}</label>
            &nbsp;&nbsp;
        </span>
     </li>
     <li>
       備 注:
       <textarea name="mark" id="mark" cols="30" rows="10" [(ngModel)]="peopleInfo.mark"></textarea>
     </li>
  </ul>

  <button (click)="doSubmit()" class="submit">獲取表單的內容</button>
  <br>
  <br>
  <br>
  <br>

  <pre>
    {{peopleInfo | json}}
  </pre>
</div>
h2{
    text-align: center;
}
.people_list{
    width: 400px;
    margin: 40px auto;
    padding:20px;
    border:1px solid #eee;
    li{
        height: 50px;
        line-height: 50px;
        .fonm_input{
            width: 300px;
            height: 28px;
        }
    }

    .submit{
        width: 100px;
        height: 30px;
        float: right;
        margin-right: 50px;
        margin-top:120px;
    }
}

3.14 實現一個完整的ToDo-案例

image.png

基礎版

# 創建組件
ng g component components/todo
<h2>todoList</h2>
<div class="todolist">
    <input class="form_input" type="text" [(ngModel)]="keyword" (keyup)="doAdd($event)" />
    <hr>
    <h3>待辦事項</h3>
    <ul>
      <li *ngFor="let item of todolist;let key=index;" [hidden]="item.status==1">
       <input type="checkbox" [(ngModel)]="item.status" />  {{item.title}}   ------ <button (click)="deleteData(key)">X</button>
      </li>
    </ul>
    <h3>已完成事項</h3>
    <ul>
        <li *ngFor="let item of todolist;let key=index;" [hidden]="item.status==0">
         <input type="checkbox" [(ngModel)]="item.status" />  {{item.title}}   ------ <button (click)="deleteData(key)">X</button>
        </li>
      </ul>
</div>
import { Component, OnInit } from '@angular/core';

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

  public keyword: string;

  public todolist: any[] = [];

  constructor() { }

  ngOnInit() {
  }
  doAdd(e){
    if(e.keyCode == 13){
        if(!this.todolistHasKeyword(this.todolist, this.keyword)){
          this.todolist.push({
            title: this.keyword,
            status: 0                   //0表示代辦事項  1表示已完成事項
          });
          this.keyword='';
        }else{
          alert('數據已經存在');
          this.keyword='';
        }
     }
  }

  deleteData(key){
    this.todolist.splice(key,1);
  }
  
  //如果數組裏面有keyword返回true  否則返回false
  todolistHasKeyword(todolist:any, keyword:any){
    //異步  會存在問題
    // todolist.forEach(value => {

    //   if(value.title==keyword){

    //       return true;
    //   } 
    // });
    if(!keyword)  return false;

    for(var i=0; i<todolist.length; i++){
      if(todolist[i].title==keyword){
          return true;
      } 
    }
    return false;
  }

}

h2{
    text-align: center;
}
.todolist{

    width: 400px;
    margin: 20px auto;
    .form_input{

        margin-bottom: 20px;

        width: 300px;
        height: 32px;
    }


    li{

        line-height: 60px;
    }

}

3.15 搜索緩存數據-案例

基礎版

# 創建組件
ng g component components/search
<div class="search">
    <input type="text" [(ngModel)]="keyword" />  <button (click)="doSearch()">搜索</button>
    <hr>
    <ul>
      <li *ngFor="let item of historyList;let key=index;">{{item}}   ------ <button (click)="deleteHistroy(key)">X</button></li>
    </ul>
</div>
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
  public keyword: string;
  public historyList: any[] = [];

  constructor() { }
  ngOnInit() {
  }
  doSearch(){
    if(this.historyList.indexOf(this.keyword)==-1){
      this.historyList.push(this.keyword);
    }
    this.keyword = '';    
  }
  deleteHistroy(key){
      alert(key);
      this.historyList.splice(key,1);
  }
}
.search{

    width: 400px;
    margin: 20px auto;
    input{

        margin-bottom: 20px;

        width: 300px;
        height: 32px;
    }

    button{
        height: 32px;
        width: 80px;
    }
}

四、Angular 中的服務

4.1 服務

定義公共的方法,使得方法在組件之間共享調用

image.png

1. 創建服務命令

ng g service my-new-service

# 創建到指定目錄下面
ng g service services/storage

2. app.module.ts 裏面引入創建的服務

// app.module.ts 裏面引入創建的服務

import { StorageService } from './services/storage.service';
// NgModule 裏面的 providers 裏面依賴注入服務

NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    FooterComponent,
    NewsComponent,
    TodolistComponent
], imports: [
    BrowserModule,
FormsModule
  ],
  providers: [StorageService],
  bootstrap: [AppComponent]
})
export class AppModule { }

3. 使用的頁面引入服務,註冊服務

 import { StorageService } from '../../services/storage.service';
 constructor(private storage: StorageService) {
 
 }
// 使用

addData(){
     // alert(this.username);
    this.list.push(this.username); 
    this.storage.set('todolist',this.list);
}
removerData(key){
    console.log(key); 
    this.list.splice(key,1); 
    this.storage.set('todolist',this.list);
}

4.2 改造上面的Todo、searchList

searchList

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

// 引入服務
import { StorageService } from '../../services/storage.service';

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

  public keyword: string;
  public historyList: any[] = [];

  constructor(public storage: StorageService) {
    console.log(this.storage.get());
   }

  ngOnInit() {
   // 修改的地方
    var searchlist:any=this.storage.get('searchlist');
    if(searchlist){
      this.historyList=searchlist;        
    }
  }

  doSearch(){
    if(this.historyList.indexOf(this.keyword)==-1){
      this.historyList.push(this.keyword);
      
      // 修改的地方
      this.storage.set('searchlist',this.historyList);
    }
    this.keyword = '';    
  }

  deleteHistroy(key){
      alert(key);
      this.historyList.splice(key,1);
  }

}

TODOLIST

  ngOnInit() {
  // 修改的地方
    var todolist:any=this.storage.get('todolist');

    if(todolist){
      this.todolist=todolist;        
    }
 }
doAdd(e){
    if(e.keyCode==13){
        if(!this.todolistHasKeyword(this.todolist,this.keyword)){
          this.todolist.push({
            title:this.keyword,
            status:0                   //0表示代辦事項  1表示已完成事項
          });
          this.keyword='';

          // 修改的地方
          this.storage.set('todolist',this.todolist);          //用到this一定要注意this指向
        }else{
          alert('數據已經存在');
          this.keyword='';
        }
     }
  }
 // 修改的地方
checkboxChange(){
    console.log('事件觸發了');

    this.storage.set('todolist',this.todolist); 
  }
<h2>todoList</h2>
<div class="todolist">
    <input class="form_input" type="text" [(ngModel)]="keyword" (keyup)="doAdd($event)" />
    <hr>
    <h3>待辦事項</h3>
    <ul>
      <li *ngFor="let item of todolist;let key=index;" [hidden]="item.status==1">
      <!-- add checkboxChange-->
       <input type="checkbox" [(ngModel)]="item.status"  (change)="checkboxChange()"/>  {{item.title}}   ------ <button (click)="deleteData(key)">X</button>
      </li>
    </ul>
    <h3>已完成事項</h3>
    <ul>
        <li *ngFor="let item of todolist;let key=index;" [hidden]="item.status==0">
   <!-- add checkboxChange-->
         <input type="checkbox" [(ngModel)]="item.status" (change)="checkboxChange()" />  {{item.title}}   ------ <button (click)="deleteData(key)">X</button>
        </li>
      </ul>
</div>

五、Dom 操作以及@ViewChild、 執行 css3 動畫

1. Angular 中的 dom 操作(原生 js)

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

2. Angular 中的 dom 操作(ViewChild)

 import { Component ,ViewChild,ElementRef} from '@angular/core';
 @ViewChild('myattr') myattr: ElementRef;
<div #myattr></div>
ngAfterViewInit(){
let attrEl = this.myattr.nativeElement;
}

@viewChild

3. 父子組件中通過 ViewChild 調用子組件 的方法

調用子組件給子組件定義一個名稱

<app-footer #footerChild></app-footer>

引入 ViewChild

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

ViewChild 和剛纔的子組件關聯起來

 @ViewChild('footerChild') footer

在父組件中調用子組件方法

 run(){ 
    this.footer.footerRun();
}

六、Angular 父子組件以及組件之間通訊

父子組件通訊

6.1 父組件給子組件傳值-@input

父組件不僅可以給子組件傳遞簡單的數據,還可把自己的方法以及整個父組件傳給子組件

1. 父組件調用子組件的時候傳入數據

<app-header [msg]="msg"></app-header>

2. 子組件引入 Input 模塊

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

3. 子組件中 @Input 接收父組件傳過來的數據

export class HeaderComponent implements OnInit {
  @Input() msg:string
  
  constructor() { }
  
  ngOnInit() {
  }
}

4. 子組件中使用父組件的數據

<p>
  child works!
  {{msg}}
</p>

5. 把整個父組件傳給子組件

通過this傳遞整個組件實例

<app-header [home]="this"></app-header>
export class HeaderComponent implements OnInit {
  @Input() home:any
  
  constructor() { }
  
  ngOnInit() {
  }
}

執行父組件方法 this.home.xxx()

6.2 子組件通過@Output 觸發父組件的方法(瞭解)

1. 子組件引入 Output 和 EventEmitter

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

2. 子組件中實例化 EventEmitter

@Output() private outer=new EventEmitter<string>(); /*用EventEmitter和output裝飾器配合使用 <string>指定類型變量*/

3. 子組件通過 EventEmitter 對象 outer 實例廣播數據

sendParent(){
  // alert('zhixing');
  this.outer.emit('msg from child')
}

4. 父組件調用子組件的時候,定義接收事件 , outer 就是子組件的 EventEmitter 對象 outer

<!--$event就是子組件emit傳遞的數據-->
 <app-header (outer)="runParent($event)"></app-header>

5. 父組件接收到數據會調用自己的 runParent 方法,這個時候就能拿到子組件的數據

//接收子組件傳遞過來的數據 
runParent(msg:string){
   alert(msg);
 }

6.3 父組件通過@ViewChild 主動獲取子組 件的數據和方法

1. 調用子組件給子組件定義一個名稱

<app-footer #footerChild></app-footer>

2. 引入 ViewChild

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

3. ViewChild 和剛纔的子組件關聯起來

 @ViewChild('footerChild') footer;

4. 調用子組件

run(){ this.footer.footerRun();
}

6.4 非父子組件通訊

  • 公共的服務
  • Localstorage (推薦)
  • Cookie

七、Angular 中的生命週期函數

7.1 Angular中的生命週期函數

官方文檔:https://www.angular.cn/guide/lifecycle-hooks

  • 生命週期函數通俗的講就是組件創建、組件更新、組件銷燬的時候會觸發的一系列的方法。
  • Angular 使用構造函數新建一個組件或指令後,就會按下面的順序在特定時刻調用這些 生命週期鉤子方法。
  • 每個接口都有唯一的一個鉤子方法,它們的名字是由接口名再加上ng前綴構成的,比如OnInit接口的鉤子方法叫做ngOnInit.

1. 生命週期鉤子分類

基於指令與組件的區別來分類

指令與組件共有的鉤子

  • ngOnChanges
  • ngOnInit
  • ngDoCheck
  • ngOnDestroy

組件特有的鉤子

  • ngAfterContentInit
  • ngAfterContentChecked
  • ngAfterViewInit
  • ngAfterViewChecked

生命週期鉤子

2. 生命週期鉤子的作用及調用順序

1、ngOnChanges - 當數據綁定輸入屬性的值發生變化時調用
2、ngOnInit - 在第一次 ngOnChanges 後調用
3、ngDoCheck - 自定義的方法,用於檢測和處理值的改變
4、ngAfterContentInit - 在組件內容初始化之後調用
5、ngAfterContentChecked - 組件每次檢查內容時調用
6、ngAfterViewInit - 組件相應的視圖初始化之後調用
7、ngAfterViewChecked - 組件每次檢查視圖時調用
8、ngOnDestroy - 指令銷燬前調用

3. 首次加載生命週期順序

export class LifecircleComponent {

    constructor() {

        console.log('00構造函數執行了---除了使用簡單的值對局部變量進行初始化之外,什麼都不應該做')
    }

    ngOnChanges() {

        console.log('01ngOnChages執行了---當被綁定的輸入屬性的值發生變化時調用(父子組件傳值的時候會觸發)'); 
    }

    ngOnInit() {
        console.log('02ngOnInit執行了--- 請求數據一般放在這個裏面');
    }
    ngDoCheck() {
        console.log('03ngDoCheck執行了---檢測,並在發生 Angular 無法或不願意自己檢測的變化時作出反應');
    }
    ngAfterContentInit() {
        console.log('04ngAfterContentInit執行了---當把內容投影進組件之後調用');
    }
    ngAfterContentChecked() {
        console.log('05ngAfterContentChecked執行了---每次完成被投影組件內容的變更檢測之後調用');
    }
    ngAfterViewInit() : void {
        console.log('06 ngAfterViewInit執行了----初始化完組件視圖及其子視圖之後調用(dom操作放在這個裏面)');
    }
    ngAfterViewChecked() {
        console.log('07ngAfterViewChecked執行了----每次做完組件視圖和子視圖的變更檢測之後調用');
    }

    ngOnDestroy() {
        console.log('08ngOnDestroy執行了····');
    }

    //自定義方法
    changeMsg() {

        this.msg = "數據改變了";
    }
}

生命週期調用順序

check的可以對數據做響應操作

<button (click)="changeMsg()">數據改變了</button>
<input type='text' [(ngModel)]="userInfo" />

點擊按鈕/雙向數據綁定此時觸發了以下生命週期。只要數據改變

cheked

可以在check做一些操作

ngDoCheck() {
        //寫一些自定義的操作

        console.log('03ngDoCheck執行了---檢測,並在發生 Angular 無法或不願意自己檢測的變化時作出反應');
        if(this.userinfo!==this.oldUserinfo){
            console.log(`你從${this.oldUserinfo}改成${this.userinfo}`);
            this.oldUserinfo = this.userinfo;
        }else{
            
            console.log("數據沒有變化");          
        }

    }

7.2 生命週期鉤子詳解

7.2.1 constructor-掌握

constructor,來初始化類。Angular中的組件就是基於class類實現的,在Angular中,constructor用於注入依賴。組件的構造函數會在所有的生命週期鉤子之前被調用,它主要用於依賴注入或執行簡單的數據初始化操作。

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

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <p>Hello {{name}}</p>
  `,
})
export class AppComponent {
  name: string = '';

  constructor(public elementRef: ElementRef) {//使用構造注入的方式注入依賴對象
    // 執行初始化操作
    this.name = 'Semlinker'; 
  }
}

7.2.2 ngOnChanges()

Angular(重新)設置數據綁定輸入屬性時響應。該 方法接受當前和上一屬性值的 SimpleChanges 對象 當被綁定的輸入屬性的值發生變化時調用,首次調用一 定會發生在 ngOnInit()之前。

<!-- 父組件中 傳遞title屬性給header子組件 -->
<app-header [title]="title"></app-header>

此時改變title會觸發ngOnChanges生命週期,並且也會觸發

cheked

7.2.3 ngOnInit()--掌握

Angular 第一次顯示數據綁定和設置指令/組件的輸入屬性之後,初始化指令/組件。在第一輪 ngOnChanges() 完成之後調用,只調用一次。可以請求數據

  • 使用 ngOnInit() 有兩個原因:
    • 在構造函數之後馬上執行復雜的初始化邏輯
    • Angular 設置完輸入屬性之後,對該組件進行準備。有經驗的開發者會認同組件的構建應該很便宜和安全
import { Component, Input, OnInit } from '@angular/core';

@Component({
    selector: 'exe-child',
    template: `
     <p>父組件的名稱:{{pname}} </p>
    `
})
export class ChildComponent implements OnInit {
    @Input()
    pname: string; // 父組件的名稱

    constructor() {
        console.log('ChildComponent constructor', this.pname); 
        // Output:undefined
    }

    ngOnInit() {
        console.log('ChildComponent ngOnInit', this.pname); 
        // output: 輸入的pname值
    }
}

7.2.4 ngDoCheck()

檢測,並在發生 Angular 無法或不願意自己檢測的變 化時作出反應。在每個 Angular 變更檢測週期中調用, ngOnChanges()ngOnInit()之後。

7.2.5 ngAfterContentInit()

當把內容投影進組件之後調用。第一次 ngDoCheck() 之後調用,只調用一次

7.2.6 ngAfterContentChecked()

每次完成被投影組件內容的變更檢測之後調用。 ngAfterContentInit() 和每次 ngDoCheck() 之後調

7.2.7 ngAfterViewInit()--掌握

初始化完組件視圖及其子視圖之後調用。第一 次 ngAfterContentChecked() 之後調用,只調用一次。在這裏可以操作DOM

7.2.8 ngAfterViewChecked()

每次做完組件視圖和子視圖的變更檢測之後調用。 ngAfterViewInit()和每次 ngAfterContentChecked() 之後 調用。

7.2.9 ngOnDestroy()--掌握

Angular 每次銷燬指令/組件之前調用並清掃。在這兒反訂閱可觀察對象和分離事件處理器,以防內存泄 漏。在 Angular 銷燬指令/組件之前調用。比如:移除事件監聽、清除定時器、退訂 Observable 等。

@Directive({
    selector: '[destroyDirective]'
})
export class OnDestroyDirective implements OnDestroy {
  sayHello: number;
  
  constructor() {
    this.sayHiya = window.setInterval(() => console.log('hello'), 1000);
  }
  
  ngOnDestroy() {
     window.clearInterval(this.sayHiya);
  }
}

八、Rxjs 異步數據流編程

8.1 Rxjs介紹

RxJSReactiveX 編程理念的 JavaScript 版本。ReactiveX 來自微軟,它是一種針對異步數據 流的編程。簡單來說,它將一切數據,包括 HTTP 請求,DOM 事件或者普通數據等包裝成流的形式,然後用強大豐富的操作符對流進行處理,使你能以同步編程的方式處理異步數據,並組合不同的操作符來輕鬆優雅的實現你所需要的功能。

  • RxJS 是一種針對異步數據流編程工具,或者叫響應式擴展編程;可不管如何解釋 RxJS 其目 標就是異步編程,Angular 引入 RxJS 爲了就是讓異步可控、更簡單。
  • RxJS 裏面提供了很多模塊。這裏我們主要給大家講 RxJS 裏面最常用的Observable 和 fromEvent

目前常見的異步編程的幾種方法:

  • 回調函數
  • 事件監聽/發佈訂閱
  • Promise
  • Rxjs

8.2 Promise和RxJS處理異步對比

新建一個services

ng g service services/rxjs

services/rxjs.service.ts中寫以下方法

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


@Injectable({
  providedIn: 'root'
})
export class RxjsService {
  constructor() { }
  
 // Promise 處理異步
  getPromiseData() {
    return new Promise(resolve = >{
    setTimeout(() = >{
        resolve('---promise timeout---');
    },
    2000);
  });

// RxJS 處理異步:
getRxjsData() {
   return new Observable(observer = >{
      setTimeout(() = >{
        observer.next('observable timeout');
      },
      2000);
  }); 
}

}
// 在其他組件使用服務
import { Component, OnInit } from '@angular/core';
import { RxjsService } from '../../services/rxjs.service';

@Component({
  selector: 'app-rxjs',
  templateUrl: './rxjs.component.html',
  styleUrls: ['./rxjs.component.scss']
})
export class RxjsComponent implements OnInit {
  
  // 注入服務
  constructor(public request: RxjsService) {

   }

  ngOnInit() {
    // 調用方法
     this.request.getRxjsData().subscribe(data=>{
      console.log(data)
    })
  }

}
  • 從上面列子可以看到 RxJSPromise的基本用法非常類似,除了一些關鍵詞不同。Promise 裏面用的是 then()resolve(),而 RxJS裏面用的是 next()subscribe()
  • Rxjs相比Promise要強大很多。 比如 Rxjs 中可以中途撤回、Rxjs 可以發射多個值、Rxjs 提供了多種工具函數等等

8.3 Rxjs unsubscribe 取消訂閱

Promise 的創建之後,動作是無法撤回的。Observable 不一樣,動作可以通過 unsbscribe() 方法中途撤回,而且 Observable 在內部做了智能的處理.

Promise 創建之後動作無法撤回

let promise = new Promise(resolve = >{
    setTimeout(() = >{
        resolve('---promise timeout---');
    },
    2000);
});
promise.then(value = >console.log(value));

Rxjs 可以通過 unsubscribe() 可以撤回 subscribe 的動作

let stream = new Observable(observer = >{
    let timeout = setTimeout(() = >{
        clearTimeout(timeout);
        observer.next('observable timeout');
    },
    2000);
});
let disposable = stream.subscribe(value = >console.log(value));
setTimeout(() = >{
    //取消執行 disposable.unsubscribe();
},
1000);

8.4 Rxjs 訂閱後多次執行

  • 如果我們想讓異步裏面的方法多次執行,比如下面代碼。

這一點 Promise是做不到的,對於 Promise來說,最終結果要麼 resole(兌現)、要麼 reject (拒絕),而且都只能觸發一次。如果在同一個 Promise 對象上多次調用 resolve 方法, 則會拋異常。而 Observable不一樣,它可以不斷地觸發下一個值,就像 next()這個方法的 名字所暗示的那樣。

let promise = new Promise(resolve = >{
    setInterval(() = >{
        resolve('---promise setInterval---');
    },
    2000);
});
promise.then(value = >console.log(value));

Rxjs

let stream = new Observable < number > (observer = >{
    let count = 0;
    setInterval(() = >{
        observer.next(count++);
    },
    1000);
});
stream.subscribe(value = >console.log("Observable>" + value));

8.5 Angualr6.x之前使用Rxjs的工具函數 map filter

注意:Angular6 以後使用以前的rxjs 方法,必須安裝 rxjs-compat 模塊纔可以使用 mapfilter 方法。

angular6 後官方使用的是 RXJS6的新特性,所以官方給出了一個可以暫時延緩我們不需要修 改 rsjx 代碼的辦法

npm install rxjs-compat
import {Observable} from 'rxjs'; import 'rxjs/Rx';
let stream = new Observable < any > (observer = >{
    let count = 0;
    setInterval(() = >{
        observer.next(count++);
    },
    1000);
});
stream.filter(val = >val % 2 == 0).subscribe(value = >console.log("filter>" + value));
stream.map(value = >{
    return value * value
}).subscribe(value = >console.log("map>" + value));

8.6 Angualr6.x 以後 Rxjs6.x 的變化以及 使用

8.6.1 Rxjs 的變化參考

Angular5升級到Angular6angular6相比較於angular5總體變化不大,但是在RXJS上面卻有一些變動,下面給大家講講關於Angular6版本升級和RXJS6新特性的講解

1. angular6 Angular7中使用以前的rxjs

對於寫了半年多的項目,模塊已經很多了,所以不可能在升級到angular6後馬上更新所有代碼關於RXJS6的新特性,所以官方給出了一個可以暫時延緩我們不需要修改rsjx代碼的辦法。

npm install --save rxjs-compat
  • 優點: 暫時不用改代碼,可以一點點地改,直到改完後吧這個包卸掉
  • 缺點: 對於rxjs6renameoperator無效,所以,如果有用到renameAPI,必須手動修改

2. Angular6 以後 RXJS6的變化

RXJS6改變了包的結構,主要變化在 import方式和operator上面以及使用pipe()

2.1 Imports 方式改變

image.png

rxjs中類似像導入observable subject 等的不再進一步導入,而是止於rxjs, rxjs6在包的結構上進行了改變

2.2 operator的改變

image.png

總而言之: 類似於創建之類的用的API都是從rxjs引入的,類似於map 之類的操作都是從rxjs/operators引入的

image.png

2.3 pipeable observable

image.png

2.4 被重新命名的API

image.png

RXJS6 改變了包的結構,主要變化在 import 方式和 operator 上面以及使用 pipe()

import {Observable} from 'rxjs';
import {map,filter} from 'rxjs/operators';
let stream= new Observable<any>(observer => {
    let count = 0;
    setInterval(() = >{
        observer.next(count++);
    },
    1000);
});

stream.pipe(filter(val = >val % 2 == 0))
.subscribe(value = >console.log("filter>" + value));

stream
.pipe(
    filter(val = >val % 2 == 0), 
    map(value = >{
        return value * value
}))
.subscribe(value = >console.log("map>" + value));

8.7 Rxjs 延遲執行

import {
    Observable,
    fromEvent
}
from 'rxjs';
import {
    map,
    filter,
    throttleTime
}
from 'rxjs/operators';

var button = document.querySelector('button');

fromEvent(button, 'click')
.pipe(throttleTime(1000))
.subscribe(() = >console.log(`Clicked`));

九、Angular 中的數據交互(get jsonp post)

9.1 Angular get 請求數據

Angular5.x 以後 getpost 和和服務器交互使用的是 HttpClientModule 模塊。

1. 在 app.module.ts 中引入 HttpClientModule 並注入

import {HttpClientModule} from '@angular/common/http';
imports: [
    BrowserModule,
    HttpClientModule
]

2. 在用到的地方引入 HttpClient 並在構造函數聲明

import {HttpClient} from "@angular/common/http";

constructor(public http:HttpClient) { }

3. get 請求數據

var api = "http://a.itying.com/api/productlist";

this.http.get(api).subscribe(response => {
console.log(response); });

9.2 Angular post 提交數據

Angular5.x 以後 getpost 和和服務器交互使用的是HttpClientModule 模塊。

1. 在 app.module.ts 中引入 HttpClientModule 並注入

import {HttpClientModule} from '@angular/common/http';

imports: [
    BrowserModule,
    HttpClientModule
]

2. 在用到的地方引入 HttpClient、HttpHeaders 並在構造函數聲明 HttpClient

import {HttpClient,HttpHeaders} from "@angular/common/http";

constructor(public http:HttpClient) { }

3. post 提交數據

express搭建一個server

// package.json
{
  "dependencies": {
    "ejs": "^2.5.6",
    "express": "^4.15.3",
    "socket.io": "^2.0.3",
    "body-parser": "~1.17.1"
  }
}
// app.js 代碼
var express = require('express');
var app=express();
var bodyParser = require('body-parser');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

/*express允許跨域*/
app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    res.header("X-Powered-By",' 3.2.1')
    if(req.method=="OPTIONS") res.send(200);
    else  next();
});

//app.use(express.static(path.join(__dirname, 'public')));

app.get('/',function(req,res){
    res.send('首頁');
})
app.post('/dologin',function(req,res){
    console.log(req.body);
    res.json({"msg":'post成功'});
})

app.get('/news',function(req,res){

    //console.log(req.body);
    res.jsonp({"msg":'這是新聞數據'});

})

app.listen(3000,'127.0.0.1',function(){
   console.log('項目啓動在3000端口')
});
// angular代碼

doLogin() {
  
  // 手動設置請求類型
  const httpOptions = {
    headers: new HttpHeaders({
        'Content-Type': 'application/json'
    })
};
var api = "http://127.0.0.1:3000/doLogin";

this.http.post(api, {
    username: '張三',
    age: '20'
},
httpOptions).subscribe(response = >{
    console.log(response);
});
}

9.3 Angular Jsonp 請求數據

1. 在 app.module.ts 中引入 HttpClientModule、HttpClientJsonpModule 並注入

 import {HttpClientModule,HttpClientJsonpModule} from '@angular/common/http';
imports: [
 BrowserModule,
 HttpClientModule,
 HttpClientJsonpModule
]

3. 在用到的地方引入 HttpClient 並在構造函數聲明

import {HttpClient} from "@angular/common/http";

constructor(public http:HttpClient) { }

3. jsonp 請求數據

// 接口支持jsonp請求
var api = "http://a.itying.com/api/productlist";

this.http.jsonp(api,'callback').subscribe(response => {
console.log(response); });

9.4 Angular 中使用第三方模塊 axios 請求數據

1. 安裝 axios

cnpm install axios --save

2. 用到的地方引入 axios

import axios from 'axios';

3. 看文檔使用

axios.get('/user?ID=12345').then(function(response) {
    // handle success
    console.log(response);
}).
catch(function(error) {
    // handle error console.log(error);
}).then(function() {
    // always executed 
});
    

十、Angular 中的路由

10.1 Angular 創建一個默認帶路由的項目

1. 命令創建項目

ng new angualrdemo08 --skip-install

2. 創建需要的組件

ng g component home
ng g component news
ng g component newscontent

3. 找到 app-routing.module.ts 配置路由

// 引入組件

import { HomeComponent } from './home/home.component';
import { NewsComponent } from './news/news.component';
import { NewscontentComponent } from './newscontent/newscontent.component';

// 配置路由
const routes: Routes = [
  {path: 'home', component: HomeComponent},
  {path: 'news', component: NewsComponent},
  {path: 'newscontent/:id', component: NewscontentComponent},
  {
    path: '',
    redirectTo: '/home',
    pathMatch: 'full'
} ];

4. 找到 app.component.html 根組件模板,配置 router-outlet 顯示動態加載的路由

<h1>
<a routerLink="/home">首頁</a> <a routerLink="/news">新聞</a>
</h1>
<router-outlet></router-outlet>

10.2 routerLink 跳轉頁面 默認路由

 <a routerLink="/home">首頁</a> 
 <a routerLink="/news">新聞</a>
//匹配不到路由的時候加載的組件 或者跳轉的路由
{
   path: '**', /*任意的路由*/ 
   // component:HomeComponent 
   redirectTo:'home'
}

10.3 routerLinkActive 設置routerLink 默認選中路由

<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;
}

10.4 routerLink Get傳遞參數

1. 跳轉

  <li *ngFor="let item of list;let key=index;">
      <!-- <a href="/news-detail?aid=123">{{key}}--{{item}}</a> -->
       
      <a [routerLink]="['/news-detail']" [queryParams]="{aid:key}">{{key}}--{{item}}</a>

    </li>

2. 接收參數

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

    constructor(public route:ActivatedRoute) { }

   this.route.queryParams.subscribe((data)=>{
        console.log(data);
   })

10.5 動態路由

1.配置動態路由

const routes: Routes = [
  {path: 'home', component: HomeComponent},
  {path: 'news', component: NewsComponent},
  {path: 'newscontent/:id', component: NewscontentComponent},
  {
    path: '',
    redirectTo: '/home',
    pathMatch: 'full'
} ];

2. 跳轉傳值

<a [routerLink]="[ '/newscontent/',aid]">跳轉到詳情</a> 
<a routerLink="/newscontent/{{aid}}">跳轉到詳情</a>

3. 獲取動態路由的值

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

constructor( private route: ActivatedRoute) { }
 
ngOnInit() {
  console.log(this.route.params);
  this.route.params.subscribe(data=>this.id=data.id);
}

10.6 動態路由的 js 跳轉

// 引入
import { Router } from '@angular/router';

// 初始化

export class HomeComponent implements OnInit { 
    constructor(private router: Router) {}
    ngOnInit() {}
    goNews(){
    // this.router.navigate(['/news', hero.id]);
         this.router.navigate(['/news']);
      }
    }
// 路由跳轉
this.router.navigate(['/news', hero.id]);

10.7 路由 get 傳值 js 跳轉

1. 引入 NavigationExtras

import { Router ,NavigationExtras} from '@angular/router';

2. 定義一個 goNewsContent 方法執行跳轉,用 NavigationExtras 配置傳參。

goNewsContent() {
    let navigationExtras: NavigationExtras = {
        queryParams: {
            'session_id': '123'
        },
        fragment: 'anchor'
    };
    this.router.navigate(['/news'], navigationExtras);
}

3. 獲取 get 傳值

 constructor(private route: ActivatedRoute) {
     console.log(this.route.queryParams);
}

10.8 父子路由

1. 創建組件引入組件

import { NewsaddComponent } from './components/newsadd/newsadd.component';
import { NewslistComponent } from './components/newslist/newslist.component';

2. 配置路由

{
    path: 'news',
    component: NewsComponent,
    children: [{
        path: 'newslist',
        component: NewslistComponent
    },
    {
        path: 'newsadd',
        component: NewsaddComponent
    }]
}

3. 父組件中定義 router-outlet

 <router-outlet></router-outlet>

十一、更多參考

鏈接:https://www.jianshu.com/p/f0f81a63cbcb

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章