学习 Angulr 容易忽略的知识点

参考内容: 《Angulr5 高级编程(第二版)》

函数声明式和表达式

// 第一种:函数声明式
myFunc();
function myFunc(){
    ...
}

// 第二种:函数表达式
myFunc();
let myFunc = function(){
    ...
}

虽然上面两种函数声明方式在大部分情况下是一样的,第一种可执行,第二种却不可以执行,这是因为浏览器在解析 js 时找到函数声明,并在执行剩余语句之前设置好函数,此过程称为函数提升,但是函数表达式却不会受到提升,因此无法正常工作。

js 不具备多态性

js 重不能创建名称相同但参数不同的两个函数,它不具备这个多态性,比如你定义的函数中有两个形参,调用函数时只传一个参数,第二形参的值就是 undefined ,如果传的参数大于 3 个,那么会自动忽略多余的参数。可以使用下列方法来处理函数定义参数数量和用于调用函数实际参数数量之间不匹配的问题。

// 使用默认参数
let func = function(age, sex='男'){
    ...
}
func(23);

// 使用可变长参数
let func = function(age, sex, ...extraArgs){
    ...
}
func(23, '女', '张三', '深圳');
// 最后一个参数是一个数组,任何额外的实参都会被赋给这个数组

let 和 war 的区别

使用 let 和 var 声明变量的区别,使用 let 声明变量会把变量的作用范围限定在它所在的代码区域内。而使用 var 所创建的变量的作用域是它所在的函数。

function func(){
    if(false){
        var age = 23;
    }
}

// 上面的代码会被解析成下面的形式,使用 let 则不会出现这样的结果

function func(){
    var age;
    if(false){
        age = 23;
    }
}

相等 == 和恒等 === 以及 连接操作符 +

相等操作符尝试将操作数强制转换为相同的类型,再评估是否相等,实质上相等操作符==是测试二者的值是否相等,而与二者的类型无关;如果要测试值和类型是否都相等则应该用恒等操作符===

5 == '5' // 结果为 true
5 === '5' // 结果为 false

在 js 中,连接操作符的优先级高于加法操作,也就是说5 + '5'的结果是55

不同的模块指定方式

import { Name } from "./modules/NameUtil";// 第一种
import { Compont } from "@angular/core";// 第二种

上面两种导入模块的方式有所不同,第一种是相对模块,第二种是非相对导入。第一种告诉的 TypeScript 编译器,该模块所在的位置是相对于包含 import 语句的文件而言;第二种非相对导入,编译器会用 node_modules 文件夹中的 npm 包来解析它。

如果在导入模块时,出现需要导入两个不同模块但是名字却相同的情况,可以使用as关键字给导入的模块取一个别名。

import { Name as otherName }from "./modules/Name";//取别名

还有一种方法是将模块作为对象导入,如下 import 所示,导入 Name 模块的内容,并创建一个名为 otherName 的对象,然后就可以使用该对象的属性了。

import * as otherName from "./modules/NameUtil";
let name = new otherName.Name("Admin", "China");// Name 是 NameUtil 中的类

多类型和类型断言

在 ts 中允许指定多个类型,使用字符|进行分隔。看下面的的方法,其功能是把华氏温度转换为摄氏温度。

// 使用多类型,该函数可以传入 number 和 string 类型的参数
static convertFtoC(temp: number | string): string {
    /*
    尝试使用 <> 声明一个类型断言,将一个对象转换为指定类型,也可以使用 as 关键字实现下列相同的效果
    let value: number = (temp as number).toPrecision ? temp as number : parseFloat(temp as string);
    */
    let value: number = (<number>temp).toPrecision ? <number>temp : parseFloat(<string>temp);
    return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1);
}

元组是固定长度的数组,数组的每一项都是指定的类型;可索引类型可以将键与值关联起来,创建类似于 map 的集合。

// 元组
let tuple: [string, string, string];
tuple = ["a", "b", "c"];

// 可索引类型
let cities: {[index: string] : [string, string]} = {};
cities["Beijing"] = ["raining", "2摄氏度"];

数据绑定

[target]="expr"// 方括号表示单向绑定,数据从表达式流向目标;

(target)="expr"// 圆括号表示单向绑定,数据从目标流向表达式,用于处理事件的绑定;
[(target)]="expr"// 圆方括号组合表示双向绑定,数据在表达式与目标之间双向流动;
{{ expression }}// 字符串插入绑定。

[] 绑定有很多不同的形式,下面介绍不同表现形式的效果。

<!-- 
    标准属性绑定(dom对象有的属性),将 input 的 value 属性绑定到一个表达式的结果
    因为 model.getProduct(1) 可能返回 null ,所以使用模板空条件操作符 ? 浏览返回结果
    如果返回不为空,那么将读取 name 属性,否则由 null 合并操作符 || 将结果设置为 None
    字符串插入绑定也可以使用这种表达式
 -->
<input [value]="model.getProduct(1)?.name || 'None'">

<!-- 
    元素属性绑定,有时候我们需要绑定的属性在 DOMAPI 上面没有
    可以使用通过在属性名称前加上 attr 前缀的方式来定义目标
 -->
<td [attr.colspan]="model.getProducts().length">
    {{ model.getProduct(1)?.name || 'None' }}
</td>

<!-- 还有其他的 ngClass,ngStyle 等绑定,理解大体上和上面差不多 -->

内置指令

<!-- 
    ngIf指令,如果表达式求值结果为 true ,那么 ngIf 将宿主元素机器内容包含在 html 文件中
    指令前面的星号表示这是一条微模板指令
    组要注意的是,ngIf 会向 html 中添加元素,也会从中删除元素,并非只是显示和隐藏
    如果只是控制可见性,可以使用属性绑定挥着样式绑定
 -->
<div *ngIf="expr"></div>

<!-- 
    ngSwitch指令,
 -->
<div [ngSwitch]="expr">
    <span *ngSwitchCase="expr"></span>
    <span *ngSwitchDefault></span>
</div>

<!-- 
    ngFor指令,见名知意,为数组中的每个对象生成同一组元素
    ngFor 指令还支持其他的一系列可赋给变量的值,有如下局部模板变量

    index:当前对象的位置
    odd:如果当前对象的位置为奇数,那么这个布尔值为 true
    even:同上相反
    first:如果为第一条记录,那么为 true
    last:同上相反
 -->
<div *ngFor="let item of expr; let i = index">
    {{ i }}
</div>

<!-- 
    ngTemplateOutlet指令,用于重复模板中的内容块
    其用法如下所示,需要给源元素指定一个 id 值

    <ng-template #titleTemplate>
        <h1>我是重复的元素哦</h1>
    </ng-template>
    <ng-template [ngTemplateOutlet]="titleTemplate"></ng-template>
    ...省略若万行 html 代码
    <ng-template [ngTemplateOutlet]="titleTemplate"></ng-template>
 -->
<ng-template [ngTemplateOutlet]="myTempl"></ng-template>

<!-- 
    下面两个指令就是见名知意了,不解释
 -->
<div ngClass="expr"></div>
<div ngStyle="expr"></div>

事件绑定

事件绑定使用 (target)="expr",是单向绑定,数据从目标流向表达式,用于响应宿主元素发送的事件。

当浏览器触发一个时间时,它将提供一个对象来描述该事件,对于不同类型的事件有不同类型的事件对象,事件对象被赋给一个名为$event的模板变量,但是所有事件对象都有下面三个属性:

type:返回一个 string 值,用于标识已触发事件类型;
target:返回触发事件的对象,一般是 html元素对象。
timeStamp:返回事件触发事件的 number 值,用 1970.1.1 毫秒数表示。

下面举几个例子,作为理解帮助使用。

<!-- 当数鼠标在上面移动时,就会触发 mouseover 事件 -->
<td *ngFor="let item of getProducts()" (mouseover)="selectedProduct = item.name"></td>

<!-- 当用户编辑 input 元素的内容时就会触发 input 事件 -->
<input (input)="selectedProduct=$event.target.value" />

<input (keyup)="selectedProduct=product.value" />
<!-- 使用事件过滤,上面的写法按下任何一个键都会触发事件,而下面的写法只有回车事件才会触发事件 -->
<input (keyup.enter="selectedProduct=product.value") />

表单验证

Angular 提供了一套可扩展的系统来验证表单元素的内容,总共可以向 input表元素中添加 4 个属性,每个属性定义一条验证规则,如下所示:

required:用于指定必须填写值;
minlength:用于指定最小字符数;
maxlength:用于指定最大字符数,(不能在表单元素直接使用,因为它与同名的 H5 属性冲突);
pattern:该属性用于指定用户填写的值必须匹配正则表达式
<!-- 
    Angular 要求验证的元素必须定义 name 属性
    由于 Angular 使用的验证属性和 H5 规范使用的验证属性相同,
    所以向表单元素中添加 novalidate 属性,告诉浏览器不要使用原生验证功能
    ngSubmit 绑定表单元素的 submit 事件
 -->
<form novalidate (ngSubmit)="addProduct(newProduct)">
    <input class="form-control"
        name="name"
        [(ngModel)]="newProduct.name"
        required
        minlength="5"
        pattern="^[A-Za-z]+$" />
    <button type="submit">提交</button>
</form>

Angular 提供了 3 对验证 CSS 类,这些类可以用于样式化表单元素,向用户提供验证反馈,具体说明如下所示。

ng-untouched ng-touched:如果一个元素未被用户访问,就将其加入到 nguntouched 类中;一旦访问就加入到 ngtouched 类中。
ng-prisstine ng-dirty:元素内容没有被改变被加入到 ng-prisstine 类中,否则将其加入到 ng-dirty 类中。
ng-valid ng-invalid:如果满足验证规则定义的条件,就加入到 ng-valid 类中,否则加入到 ng-invalid 类中。

在实际使用过程中,直接定义对应的样式即可,如下所示:

<style>
input.ng-dirty.ng-invalid{
    border: 2px solid red;
}
input.ng-dirty.ng-valid{
    border: 2px solid green;
}
</style>
<form novalidate (ngSubmit)="addProduct(newProduct)">
    <input class="form-control"
        name="name"
        [(ngModel)]="newProduct.name"
        required
        minlength="5"
        pattern="^[A-Za-z]+$" />
    <button type="submit">提交</button>
</form>

上面的验证方式无法给用户提供更加具体的信息,用户不知道应该做什么,可以使用 ngModel 指令来访问宿主元素的验证状态,当存在验证错误的时候,使用该指令向用户提供指导性信息。

<form novalidate (ngSubmit)="addProduct(newProduct)">
    <input class="form-control"
        #nameRef="ngModel"
        name="name"
        [(ngModel)]="newProduct.name"
        required
        minlength="5"
        pattern="^[A-Za-z]+$" />
    <ul class="text-danger list-unstyled"
        *ngIf="name.dirty && name.invalid">
        <li *ngIf="name.errors?required">
            you must enter a product name
        </li>
        <li *ngIf="name.errors?.pattern">
            product name can only contain letters and spases
        </li>
        <li *ngIf="name.errors?minlength">
            <!-- 
                Angular 表单验证错误描述属性
                required:如果属性已被应用于 input 元素,此属性返回 true
                minlength.requiredLength:返回满足 minlength 属性所需的字符数
                minlength.actualLength:返回用户输入的字符数
                pattern.requiredPattern:返回使用 pattern 属性指定的正则表达式
                pattern.actualValue:返回元素的内容
             -->
            product name must be at least {{ name.errors.minlength.requiredLenth }} characters
        </li>
    </ul>
    <button type="submit">提交</button>
</form>

如果在用户尝试提交表单时就显示大量的错误信息,给人的体验感就会很差,所以可以让用户提交表单时再验证整个表单,示例代码如下所示。

export class ProductionCompont {
    // ...省略若万行代码
    formSubmited: boolean = false;

    submitForm(form: ngForm) {
        this.formSubmited = true;
        if(form.valid) {
            this.addProduct(this.newProduct);
            this.newProduct = new Product();
            form.reset();
            this.formSubmited = true;
        }
    }
}
<form novalidate #formRef="ngForm" (ngSubmit)="submitForm(formRef)">
    <div *ngIf="formsubmited && formRef.invalid">
        there are problems with the form
    </div>
    <!-- 禁用提交按钮,验证成功提交按钮才可用 -->
    <button [disabled]="formSubmited && formRef.valid">提交</button>
</form>

fromSubmited 属性用于指示表单是否已经提交,并将用于在用户提交整个表单之前阻止表单验证。当用户提交表单时,调用 submitForm 方法,并将 ngForm 对象作为实参传入,ngForm 提供了 reset 方法,该方法可以重置表单的验证状态,使其返回到最初的未访问状态。

更高级的还有使用基于模型的表单验证,可以自行查阅相关资料。

使用 json-server 模拟 web 服务

因为json-server会经常用到,建议使用全局安装命令npm install -g json-server。因为开发后端的同学太慢了,而我们如果要等他们把接口都提供给我们的时候再开发程序的话,那效率就太低了,所以使用 json-server 来模拟后端服务。只需要建好一个 json 文件,比如下面的格式:

{
    "user" : [
        {
            "name" : "张三",
            "number" : "1234",
        },
        {
            "name" : "王二",
            "number" : "5678",
        }
    ],
    "praise": [
        {"info":"我是一只小老虎呀!"},
        {"info":"我才是大老虎"}
    ]
}

启动服务使用命令json-server [你的 json 文件路径],然后就可以根据提示访问了,你甚至可以使用http://localhost:3000/user?number=5678去过滤数据。这样就能模拟 web 服务,而不必等后端同学的进度了。

解决跨域请求问题

Angular 跨域请求问题可以通过 Angular 自身的代理转发功能解决,在项目文件夹下新建一个 proxy.conf.json 并在其中添加如下内容。

// 可以通过下列配置解决
"/api": {
    "target": "http://10.9.176.120:8888",
}

在启动时使用npm start,或者使用ng serve --proxy-config proxy.conf.json,Anular 中的/api请求就会被转发到 http://10.9.176.120:8888/api,从而解决跨域请求问题。

使用第三方 js 插件

 共有三种方式引入第三方插件,第一种很简单,直接在 html 中引入插件就可以了;第二种在angular.json中进行配置;第三种在 ts 文件中使用 import 导入库即可。

// 第一种(需要重启服务)
"scripts": ["src/assets/jquery-3.2.1.js","src/assets/jquery.nicescroll.js","src/assets/ion.rangeSlider.js"]

// 第二种
<script type="text/javascript" src="assets/jquery-3.2.1.js"></script>
<script type="text/javascript" src="assets/jquery.nicescroll.js"></script>

// 第三种
import "assets/jquery-3.2.1.js";
import "assets/jquery.nicescroll.js";
import "assets/ion.rangeSlider.js";

深拷贝与浅拷贝

深拷贝与浅拷贝是围绕引用类型变量说的,其本质区别是不可变性,基本类型是不可变得,而引用类型是可变的。

直接使用赋值操作符,就是浅拷贝,如果对拷贝源进行操作,会直接影响在拷贝目标上,因为这个赋值行为本质是内存地址的赋值,为了获得与拷贝源完全相同但又不会影响彼此的对象就要使用深拷贝。

let objA = {
    x: 1,
    y: -1
}
let objB = objA;
objA.x++;
console.log("objA.x:"+objA.x, "objB.x:"+objB.x);
//打印结果如下:
objA.x : 2 
objB.x : 2

Typescript 提供了一种方法来实现引用类型的深拷贝,即Object.assign(target, ...source),此方法接受多个参数,第一个参数为拷贝目标,剩余参数为拷贝源,同名属性会进行覆盖。

let objA = {
    x: 1,
    y: -1,
    c: {
        d: 1,
    }
}
let objB = {};
Object.assign(objB, objA);
objA.x++;
console.log("objA.x:"+objA["x"], "objB.x:"+objB["x"]);
//打印结果如下:
objA.x : 2 
objB.x : 1

需要注意的是,Typescript 提供的深拷贝方法不能实现嵌套对象的深拷贝,会出现下面的情况。

let objA = {
    x: 1,
    y: -1,
    c: {
        d: 1,
    }
}
let objB = {};
Object.assign(objB, objA);
objA.c.d++;
console.log("objA.c.d:"+objA["c"].d, "objB.c.d:"+objB["c"].d);
//打印结果如下:
objA.c.d : 2 
objB.c.d : 2

要实现嵌套对象的深拷贝,可以使用 JSON 对象提供的方法,JSON 对象提供了两个方法,分别为:stringify()parse(),前者将对象 JSON 化,后者将 JSON 对象化,使用这种方式可以实现嵌套深拷贝,但是也有缺点:破坏原型链,不能拷贝属性值为 function 的属性。

let objA = {
    a: 1,
    b: {
        c: 1
    }
}
let objB = JSON.parse(JSON.stringify(objA));
objA.b.c++;
console.log("objA.b.c:"+objA.b.c, "objB.b.c:"+objB.b.c);

//打印结果如下:
objA.b.c:2
objB.b.c:1
前端TypeScriptJavaScriptAngular