吐槽:
谁没事用 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]
页面生命周期

全局配置
// 是否隐藏子页面上的选项卡,如果 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/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');
 }
}
