吐槽:
谁没事用 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');
}
}