吐槽:
谁没事用 angular 啊!各个版本跨度大,上网找文档都费劲 😡,
可是没办法,工作中需要,向生活低头 😔

不过 ionic 这个框架还是不错的,现在可以用 vue 和 react 进行开发,组件样式也很好看,
但是不支持打包国内的小程序,跨端不如用 uni-app,更适合国人一些 😇

完整技术栈以及顺序:

  • typescript(angular 要求使用,vue、react 不硬性要求)
  • angular/vue/react
  • ionic
  • android、ios

typescript

基于 ES 的增强语言,完全兼容 ES 的写法。
增加了类型定义、类型检测、接口等强语言特性。
学习曲线很友好,只有接口部分较为复杂,需要理解用法。

interface Dog {
  name: string
}

let obj1: { [ propName: string ]: FormControl } | null = null;
let obj2: Dog | null = null;
let obj3: { name: string } | null = null;

let arr1: Array<string> | Array<Array<string>> = [];
let arr2: Array<Dog> | Array<Array<Dog>> = [];
let arr3: Array<{ name: string }> = [];
/**
 *  定义一个接口
 *  key 后面加 ? 代表这个属性/方法可有可无
 **/
interface PersonBase {
  name: string
  age: number
  country?: string
  
  getName(): string,

  getCountry?(): string

  sayName(): void
}

// 定义一个对象,使其实现接口
let user: PersonBase = {
  name: 'jason',
  age: 18,
  country: '中国',

  getName(): string {
    return this.name
  },
  getCountry(): string {
    return this.country
  },
  sayName(): void {
    console.log(this.name)
  }
}

// 定义一个类,使其实现接口
class User implements PersonBase {
  name: string
  age: number

  getName(): string {
    return this.name
  }

  sayName() {
    console.log(this.name)
  }
}

angular

一个很蛋疼的框架,国内的开发基本都不用这玩意
使用起来也不如 vue 方便

基本的模板语法

<!-- 通过标签向组件传递数据 -->
<!-- [dataA] 为单向绑定 -->
<!-- [(dataB)] 为双向绑定(语法糖) -->
<!-- (dataAChange) 为事件,其中 $event 为 emit 时的参数 -->
<app-list
  [dataA]="dataA"
  [(dataB)]="dataB"
  (dataAChange)="this.dataA = $event"></app-list>

<!-- ngModel 解析: -->
<!-- [ngModel]:组件 => input -->
<!-- (ngModel):input => 组件 -->
<!-- ([ngModel]): 为双向绑定 -->
<input type='text' [ngModel]='username' />
<input type='text' (ngModel)='username' />
<input type='text' ([ngModel])='username' />

<!-- 通过 [hidden] 来确定组件是否显示 -->
<p [hidden]="isShow"></p>

<!-- 通过 ngIf 来确定组件是否渲染 -->
<ul *ngIf="isShow">
  <!-- For 指令可以指定局部变量,常用的有以下 -->
  <!-- let i = index -->
  <!-- let count = count -->
  <!-- let first = first -->
  <!-- let last = last -->
  <!-- let odd = odd -->
  <!-- let even = even -->
  <li *ngFor="let item of data; let i = index;">
    {{ i }}
  </li>
</ul>

<!-- 原生事件 angular 做了封装 -->
<button (click)="submit()">
  点击我触发
</button>

生成一个组件

建议通过命令行的方式生成

# 生成一个组件
ng generate component <component-name>
# 将会在 src/app 目录下生成 <component-name> 目录
# 并在 app.module.ts 引入并注册

组件的通讯

父子组件的通讯就像 vue 一样是单向数据流的。
属性向下、事件向上。

/**
 *  子组件 component.js
 **/
export class ListComponent implements OnInit {
  /**
   *  通过 @Input 装饰器修饰属性来接收父组件传来的值
   *  Input 在 @angular/core 模块中
   **/
  @Input() title?: string
  @Input() data: Array<string> = []
  
  /**
   *  通过 @Output 装饰器修饰属性来向父组件发送消息
   *  被 Output 修饰的属性,值应该定义为 EventEmitter 的实例
   *  Output 和 EventEmitter 在 @angular/core 模块中
   *  
   *  若想实现 "双向绑定" 的语法糖,事件的 key 必须为 inputChange 模式
   *  这样父组件模板中可以 [(input)]="data" 的形式即可实现 "双向绑定"
   **/
  @Output() dataChange = new EventEmitter<Array<string>>()
  
  // 在组件内部使用 this.dataChange.emit(payload) 向父组件发出事件
  onClick () {
    this.dataChange.emit(['a', 'b'])
  }
}
<!-- 父组件 component.html -->
<app-list
  [title]="title"
  [data]="data"
  (dataChange)="this.data = $event"></app-list>
<!-- OR -->
<app-list
  [title]="title"
  [(data)]="data"></app-list>

组件的计算属性

可以通过 getter 实现计算属性

export class AppComponent implements OnChanges {
  name: string = 'jason'
  get fullName (): string {
    return 'my is ' + this.name
  }
}

组件属性的监听

ngOnChanges 方法可以监听 @Input 进来的属性
使用 set 可以监听组件内部的属性

export class AppComponent implements OnChanges {
  @Input() data: Array<string> = []
  
  _name: String = ''
  set name (val: String) {
    // 在 set 中实现监听
    this._name = val
  }

  // 当 data 改变时会触发 
  ngOnChanges (changes: SimpleChanges) {
    /**
     *  {
     *    data: {
     *      // 新值
     *      currentValue,
     *      // 是否为传入时触发
     *      firstChange,
     *      // 老值,首次传入时触发时,此值为 undefined
     *      previousValue
     *    }
     *  }
     **/
    console.log(changes)
  }
}

组件的插槽用法

主要使用内置的 ng-content 标签实现。
ng-content 默认显示所有父组件定义的内容。
可以通过 select 属性来指定显示位置,匹配规则为 css 选择器。

<!-- 子组件(app-list) component.html  -->
<div>
  <ng-content select="[slot='header']"></ng-content>
  <ng-content></ng-content>
  <ng-content select="[slot='footer']"></ng-content>
</div>
<!-- 父组件 component.html -->
<app-list>
  <div>我是内容,匹配子组件的 ng-content</div>
  <div slot="header">我是头部,匹配子组件的 ng-content[slot="header"]</div>
  <div slot="footer">我是内容,匹配子组件的 ng-content[slot="footer"]</div>
</app-list>

管道(vue 中的过滤器)

将值进行处理后显示。
angular 内置了非常多的实用 pipe,文档如下:
https://angular.cn/api/common#%E7%AE%A1%E9%81%93
自定义方法如下:

/**
 *  创建 pipe 的 ts 文件
 *  用 Pipe 修饰器修饰
 *  继承 PipeTransform 接口
 *  在 @NgModule declarations 中注册
 **/
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'hash' })

export class hashPipe implements PipeTransform {
  transform (value: any, p1?: any, p2?: any): string {
    return value + '...' + p1 + p2
  }
}
<!-- 注意传参方式,很特别 -->
{{ 1621324650486 | date:"YYYY/MM/dd HH:mm:SS" }}
{{ fullName | hash: 50: 20 }}

表单(暂时放一放,用法不确定)

angular 提供了两种方法实现表单数据的管理
简单来说,复杂的用 “响应式表单”,简单的用 “模板驱动表单”
在实际情况中,可以只用 “响应式表单”,能满足所有情况下使用。

主要使用 @angular/forms 模块的 FormGroup, FormControl。
具体看代码吧

import { FormGroup, FormControl } from "@angular/forms";

export class FormComponent implements OnInit {
  form: FormGroup = new FormGroup({
    username: new FormControl('默认值'),
    password: new FormControl('默认值')
  })
 
  submit () {
    // 输出 FormGroup 实例
    console.log(this.form)
    // 输出 FormControl 实例组成的对象
    console.log(this.form.controls)
    // 输出 value 组成的对象
    console.log(this.form.value)
  }
  
  ngOnInit () {
    // 可以通过 addControl 方法动态向 FormGroup 实例中添加控件
    this.form.addControl('gender', new FormControl('男'))
  }
}
<form 
  [formGroup]="form"
  (submit)="submit()">
  <div>
    用户名:
    <input formControlName="username" type="text">
  </div>
  <div>
    密码:
    <input formControlName="password" type="password">
  </div>
  <div>
    性别:
    <input formControlName="gender" type="radio" value="男">
    <input formControlName="gender" type="radio" value="女">
  </div>
  <div>
    <button>提交</button>
  </div>
</form>

服务基础

服务是 angular 比较重要,并且强大的功能。
简单来说就是预先定义好 “服务类”,类中有属性和方法,
在注入到组件中,使组件可以使用 “服务类” 中的属性和方法
并且不同的组件可以通过 “作用域相同” 的 “服务类” 共享数据,类似于 vuex。

# 命令行生成一个服务
ng generate service heroes/hero
// src/app/heroes/hero.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  // 表示注入到最顶层,在整个应用中都是可见的
  // 是最顶层的注入方式
  providedIn: 'root',
})

export class HeroService {
  constructor() { }
}
// 在组件中,或服务中(服务也可以注入服务)
export class AppComponent implements OnInit {
  // 在构造器中注入,注意加 private 修饰符。
  constructor (private heroService: HeroService) {
  }
  
  async ngOnInit () {
    // 在内部即可调用
    console.log(this.heroService)
  }
}

服务的实例以及作用域

服务可以注册到 “root”、”模块”、”组件” 这三个不同的作用域中。
并且按照 “先内后外” 的顺序确定作用域。
服务也可以注入服务中,但是需要在相同的作用域中。

// 在服务内部 providedIn: 'root' 代表注册到最顶级作用域
// 在整个项目中都可以使用,并且共用一个服务实例
@Injectable({
  providedIn: 'root'
})
export class HeroService {
}
// 在模块中,指定注入服务
// 该模块中的所有组件、服务,共享一个服务实例
@NgModule({
  // ...
  providers: [
    HeroService
  ]
  // ...
})
export class AppModule {
}
// 在组件中注册服务
// 此时只有在当前的组件中才能使用该服务
// 并且与其他的组件不会共享同一个实例。
@NgModule({
  // ...
  providers: [
    HeroService
  ]
  // ...
})
export class AppModule {
}

SPA 路由 - 基础

依赖 @angular/router 模块

// 创建 app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { ListComponent } from "./pages/list/list.component";
import { ListChildComponent } from "./pages/list/list-child/list-child.component";
import { DetailComponent } from "./pages/detail/detail.component";

const routes: Routes = [
  {
    path: 'list',
    component: ListComponent,
    children: [
      {
        path: 'child',
        component: ListChildComponent
      }
    ]
  },
  { path: 'detail', component: DetailComponent },

  // 通配符路由,写在最后
  { path: '**', redirectTo: 'list' }
];

@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule {
}
// 在 app.module.ts 中导入路由模块
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  // ....
  imports: [
    AppRoutingModule
  ]
  // ....
})
<!-- 在组件中使用 router-outlet 标签渲染页面组件 -->
<router-outlet></router-outlet>

SPA 路由 - 跳转和传参

有两种方式,html 和 js 传参。

<!-- /detail?id=10 -->
<a routerLink="/detail" [queryParams]="{ id: 10 }">detail</a>

<!-- /detail/:id/:other => /detail/10 -->
<a routerLink="/detail/10/other">detail</a>
<a [routerLink]="['/detail', 10, 'other']">detail</a>

<!-- queryParamsHandling 参数 -->
<!-- 值为 preserve,则放弃当前页面的查询参数,用自定义的查询参数,跳转页面 -->
<!-- 值为 merge,则保留当前页面查询参数,与新的查询参数进行合并后,跳转给下一个页面使用 -->

<!-- 跳转前:/list?pagenum=20&page=1 -->
<a routerLink="/list" [queryParams]="{ page: 2 }" queryParamsHandling="merge">list</a>
<!-- 跳转后:/list?pagenum=20&page=2 -->
// 在 JS 中引入 Router 服务
import { Router } from "@angular/router";

export class ListComponent implements OnInit {

  // 注入 Router
  constructor (public router: Router) {
  }
  
  // 使用 router.navigate 方法进行跳转、传参
  go (path: string) {
    this.router.navigate(['/detail'], {
      queryParams: {
        id: 10086
      }
    })
    // => /detail?10086
    
    this.router.navigate(['/detail', 10, 'other'])
    // => /detail/10/other

    // 当前是 /list?pagenum=10&page=1&other=other
    this.router.navigate(['/list'], {
      queryParamsHandling: 'merge',
      queryParams: {
        page: 2
      }
    })
    // => /list?pagenum=10&page=2&other=other
  }

}

SPA 路由 - 接收参数

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

export class DetailComponent implements OnInit {

  // 需要注入 ActivatedRoute 服务
  constructor (private route: ActivatedRoute) {
  }

  ngOnInit (): void {
    // http://localhost:4200/detail/10?id=10086

    // 从查询参数中获取
    this.route.queryParamMap.subscribe(params => {
      console.log(
        params.get('id')
      )
      // => 10086
    });

    // 从路径中获取
    this.route.paramMap.subscribe(params => {
      console.log(
        params.get('id')
      )
      // => 10
    });
  }

}

Http 请求

angular 内置了 http 服务,注入即可在组件中使用。

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

// 被弃用的
// import { HttpModule } from '@angular/http';

@NgModule({
  // ...
  imports: [
    HttpClientModule
    // HttpModule
  ]
  // ...
})
import { HttpClient } from '@angular/common/http'

export class MyComp {
  constructor (private http: HttpClient) {
    this.http.get()
    this.http.post()
    this.http.put()
    this.http.delete()
    
    // 默认用 "订阅(subscribe)" 的方式处理异步(真心用不惯...)
    // 可以使用 toPromise 方法转为 Promise 方式处理
    this.http.get().toPromise()
      .then(res => {
        // 想要获得真正的 json 数据对象,需要使用 json 方法,将数据 "转一下"
        // PS: res 中 _body 是请求响应的字符串,使用 JSON.parse 也可以正常获取。
        console.log(res.json())
        console.log(JSON.parse(res._body))
      })
      .catch()
  }
}
也可以设置拦截器,这里暂不做深究了。

ionic3

ionic3 和 ionic4+ 提供的方法跨度比较大。
基本需要重新学习,至少路由部分是这样的。

文档

命令行工具

# 生成页面,pipe,服务等
# 建议使用该命令生成所需要的部分
ionic generate

# 生成页面可以指定路径
ionic generate page [name] --pagesDir [path]

页面生命周期

ionic3-lifecycle.webp

全局配置

https://ionicframework.com/docs/v3/api/config/Config/

// 是否隐藏子页面上的选项卡,如果 true 不会在子页面上显示选项卡。
// 很奇葩的是这个值默认居然是 false。
tabsHideOnSubPages: false

路由定义

是通过 @IonicPage 修饰符来定义页面,非常简单
如果用 ionic generate page 生成页面组件,则完全不用自己操心。

文档

例子

@IonicPage({
  // name 默认为组件的类名,常用于 this.navCtrl.push 时跳转。
  name: 'my-page',
  // segment 默认为组件的标签名,用于 url。
  segment: 'some-path'
})

渲染路由(ion-nav 组件)

https://ionicframework.com/docs/v3/api/components/nav/Nav/

跳转页面、传参

https://ionicframework.com/docs/v3/api/navigation/NavController/
https://ionicframework.com/docs/v3/api/components/nav/NavPush/
https://ionicframework.com/docs/v3/api/components/nav/NavPop/

tabs 页面跳转:
https://ionicframework.com/docs/v3/api/components/tabs/Tabs/

/**
 *  Tab 模式的跳转规则一般是
 *  A -> A1 -> A2 或 B -> B1 -> B2(前进)
 *  或
 *  A2 -> A1 -> A 或 B2 -> B -> B1(后退)
 *  
 *  如果像下面这种逻辑,可以参考下面的代码,文档中也有
 *  A -> A1(前进)
 *  A1 -> B(返回)
 */
// 先返回到根部
this.navCtrl.popToRoot()
// 切换到指定的 Tab,参数 2 是 tab 索引
this.navCtrl.parent.select(2)

接收页面参数

https://ionicframework.com/docs/v3/api/navigation/NavParams/

import { NavParams } from 'ionic-angular';

export class MyClass{
 constructor(public navParams: NavParams){
   this.navParams.get('userParams');
 }
}