0%

eventproxy 控制并发

如果并发异步获取两三个地址的数据,并且要在获取到数据之后,对这些数据一起进行利用的话,常规的写法是自己维护一个计数器。
先定义一个 var count = 0,然后每次抓取成功以后,就 count++。如果你是要抓取三个源的数据,由于你根本不知道这些异步操作到底谁先完成,那么每次当抓取成功的时候,就判断一下 count === 3。当值为真时,使用另一个函数继续完成操作。
而 eventproxy 就起到了这个计数器的作用,它来帮你管理到底这些异步操作是否完成,完成之后,它会自动调用你提供的处理函数,并将抓取到的数据当参数传过来\

无限嵌套

1
2
3
4
5
6
7
8
9
10
11
$.get("http://data1_source", function (data1) {
// something
$.get("http://data2_source", function (data2) {
// something
$.get("http://data3_source", function (data3) {
// something
var html = fuck(data1, data2, data3);
render(html);
});
});
});

计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(){
var count = 0;
var result = {};
$.get('http://data1_source', data => {
count++;
handle();
});
$.get('http://data2_source', data => {
count++;
handle();
});
$.get('http://data3_source', data => {
count++;
handle();
});
function handle(){
if(count === 3){
......
}
})

eventproxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var ep = new eventproxy();

// ep.all 监听三个事件,每当一个源的数据抓取完成时,就通过ep.emit()来告诉ep,某某事件完成了。当三个事件未同时完成时,ep.emit()调用之后不会做任何事,当三个事件都完成时,就会调用末尾的那个回调函数。
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {
var html = fuck(data1, data2, data3);
render(html);
});

$.get('http://data1_source', function (data) {
ep.emit('data1_event', data);
});

$.get('http://data2_source', function (data) {
ep.emit('data2_event', data);
});

$.get('http://data3_source', function (data) {
ep.emit('data3_event', data);
});

如果已经确定请求的次数,可以使用eventproxy的afterAPI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let eventproxy = require('eventproxy');
var ep = new eventproxy();


// 命令 ep 重复监听 datas.length 次(在这里也就是 40 次) `data_event` 事件再行动
ep.after('data_event', datas.length, function (data) {
// data 是个数组,包含了 40 次 ep.emit('data_event', pair) 中的那 40 个 pair
}
datas.forEach(item => {
superagent.get(item.url)
.end(function (err, res) {
ep.emit('data_event', res);
});
});

async 控制并发

爬虫时如果太多的并发链接,就会被看做是恶意请求,因此要控制一下并发的数量,如果有1000个链接,并发10个。\

mapLimit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let async = require('async');
let count = 0; // 并发的计数器
let fetchUrl = (url, callback) => {
let delay = parseInt((Math.random() * 10000000) % 2000, 10);
count++;
console.log('现在并发数是', count, ',正在抓取的是', url, ',耗时' + delay + '毫秒');
setTimeout(() => {
count--;
callback(null, url + ' html content');
// 注意callback会将返回结果放在一个数组里
}, delay);
};

var urls = [];
for (var i = 0; i < 30; i++) {
urls.push('http://datasource_' + i);
}

async.mapLimit(urls, 5, (url, callback) => {
fetchUrl(url, callback);
}, (err, result) => {
console.log('final:');
console.log(result);
});

queue

nodejs接收get请求参数

在http协议中,一个完整的url路径如下:

完整的url路径

get请求的参数是直接在url路径中显示的,在path资源路径的后面添加,以?表示参数的开始,以key = value表示参数的键值对,多个参数以&符号分割,hash表示的是资源定位符,由浏览器自己解析处理。

浏览器向服务端发送get请求主要有两种方式,一种是href跳转,url拼接参数;一种是ajax请求发送参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
let url = require('url');
let app = http.createServer();
app.on('request', function (req, res) {

//1.默认情况下,如果url路径中有中文,则会对中文进行URI编码,所以服务端要想获取中文需要对url进行URI解码
console.log(encodeURI(req.url));
// 2.url.parse 方法可以将一个 URL 路径解析为一个方便操作的对象
// 将第二个可选参数指定为 true, 表示将结果中的 query 解析为一个对象
var parseObj = url.parse(req.url, true);
console.log(parseObj);
var pathname = parseObj.pathname; //相当于无参数的url路径
console.log(pathname);
// 这里将解析拿到的查询字符串对象作为一个属性挂载给 req 对象,这样的话在后续的代码中就可以直接通过 req.query 来获取查询字符串了
req.query = parseObj.query;
console.log(req.query);
if (pathname === '/heroAdd') {
fs.readFile('./heroAdd.html', function (err, data) {
if (err) {
throw err;
}
res.end(data);
});
} else if (pathname.indexOf('/node_modules') === 0) {
fs.readFile(__dirname + pathname, function (err, data) {
if (err) {
throw err;
} else {
console.log(data);
res.end(data);
}
});
} else {
res.end('请求路径: ' + req.url);
}
});

app.listen(3000, function () {
});

nodejs接收post请求参数

post请求参数不直接在url路径中拼接,而是放在请求体中发送给服务器,请求三要素:请求行、请求头、请求体。

与get请求不同的是,服务端接收post请求参数不是一次就可以获取的,通常需要多次。

服务端接收表单数据

  • (1)如果表单数据量越多,则发送的次数越多,如果比较少,可能一次就发过来了
  • (2)接收表单数据的时候,需要通过监听 req 对象的 data 事件来取数据
  • (3)每当收到一段表单提交过来的数据,req 的 data 事件就会被触发一次,同时通过回调函数可以拿到该 段 的数据
    服务端需要自己添加数据流
  • (4)当接收表单提交的数据完毕之后,会执行req的 on 事件

服务端处理表单数据

  • (1) 对数据进行解码(中文数据提交会进行url编码)decodeURI(data)
  • (2) 使用querystring对url进行反序列化(解析url将&和=拆分成键值对),得到一个对象。
  • (3) 将数据插入到数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//1.导入http模块
var http = require('http');
//导入文件模块
var fs = require('fs');
//导入路径模块
var path = require('path');
//导入querystring模块(解析post请求数据)
var querystring = require('querystring');

//2.创建服务器
var app = http.createServer();

//3.添加响应事件
app.on('request', function (req, res) {

console.log(req.method);

//1.通过判断url路径和请求方式来判断是否是表单提交
if (req.url === '/heroAdd' && req.method === 'POST') {
/**服务端接收post请求参数的流程
* (1)给req请求注册接收数据data事件(该方法会执行多次,需要我们手动累加二进制数据)
* * 如果表单数据量越多,则发送的次数越多,如果比较少,可能一次就发过来了
* * 所以接收表单数据的时候,需要通过监听 req 对象的 data 事件来取数据
* * 也就是说,每当收到一段表单提交过来的数据,req 的 data 事件就会被触发一次,同时通过回调函数可以拿到该 段 的数据
* (2)给req请求注册完成接收数据end事件(所有数据接收完成会执行一次该方法)
*/
//创建空字符叠加数据片段
var data = '';

//2.注册data事件接收数据(每当收到一段表单提交的数据,该方法会执行一次)
req.on('data', function (chunk) {
// chunk 默认是一个二进制数据,和 data 拼接会自动 toString
data += chunk;
});

// 3.当接收表单提交的数据完毕之后,就可以进一步处理了
//注册end事件,所有数据接收完成会执行一次该方法
req.on('end', function () {

//(1).对url进行解码(url会对中文进行编码)
data = decodeURI(data);
console.log(data);

/**post请求参数不能使用url模块解析,因为他不是一个url,而是一个请求体对象 */

//(2).使用querystring对url进行反序列化(解析url将&和=拆分成键值对),得到一个对象
//querystring是nodejs内置的一个专用于处理url的模块,API只有四个,详情见nodejs官方文档
var dataObject = querystring.parse(data);
console.log(dataObject);
});
}

if (req.url === '/heroAdd' && req.method === 'POST') {
fs.readFile('./heroAdd.html', function (err, data) {
if (err) {
throw err;
}
res.end(data);
});
} else if (req.url.indexOf('/node_modules') === 0) {
fs.readFile(__dirname + req.url, function (err, data) {
if (err) {
throw err;
} else {
res.end(data);
}
});
} else {
res.end('请求路径: ' + req.url);
}
});

//4.监听端口号
app.listen(3000, function () {
});

nodejs使用express框架获取参数的方式

req.params

命名过的参数会以键值对的形式存放,路由/user/:name,浏览器访问/user/a,a值即name的属性会存放在req.params.name;如果有多个参数/find/:group/:name,浏览器访问find/a/ba=req.params.groupb=req.params.name分别获取group和name的两个参数。

req.query

/user/?id=1,req.query.id会得到1,如果有两个或者两个以上的参数用&连接,/user/?id=1&name=test,req.query.id –> 1,req.query.name –> test。

req.body

通过post方式提交的参数$.post('/add', {sid: 'sid'})

1
2
3
4
5
6
7
8
9
10
11
12
13
let bodyParser = require('body-parser')
let multer = require('multer');
let upload = require('multer'); // for parsing multipart/form-data
app.use(bodyParser.urlencode({extended: true})) // for parsing application/x-www-form-urlencoded
app.use(bodyParser.json()) // for parsing application/json
app.post('/add', function(req, res){
let sid = req.body.sid;
})

app.post('/profile', upload.array(), function (req, res, next) {
console.log(req.body);
res.json(req.body);
});

req.param

req.param()是req.query、req.body、以及req.params获取参数的三种方式的封装,req.params(name)返回name参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// POST name=tobi
app.post('/user?name=tobi',function(req,res){
req.param('name');
// => "tobi"
})

// ?name=tobi
req.param('name')
// => "tobi"

// /user/tobi for /user/:name
req.param('name')
// => "tobi"

输入属性(父组件->子组件)

@Input,自定义属性

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
import { Component } from '@angular/core';

@Component({
selector: 'exe-app',
template: `
<exe-counter [count]="initialCount"></exe-counter>
`
})
export class AppComponent {
initialCount: number = 5;
}

counter.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component, Input } from '@angular/core';

@Component({
selector: 'exe-counter',
template: `
<p>当前值: {{ count }}</p>
<button (click)="increment()"> + </button>
<button (click)="decrement()"> - </button>
`
})
export class CounterComponent {
@Input() count: number = 0;

increment() {
this.count++;
}

decrement() {
this.count--;
}
}

输出属性(子组件->父组件)

@Output(),自定义事件

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from '@angular/core';

@Component({
selector: 'exe-app',
template: `
<p>{{changeMsg}}</p>
<exe-counter [count]="initialCount"
(change)="countChange($event)"></exe-counter>
`
})
export class AppComponent {
initialCount: number = 5;

changeMsg: string;

countChange(event: number) {
this.changeMsg = `子组件change事件已触发,当前值是: ${event}`;
}
}
// 自定义事件change,接收发送过来的数据。

counter.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'exe-counter',
template: `
<p>当前值: {{ count }}</p>
<button (click)="increment()"> + </button>
<button (click)="decrement()"> - </button>
`
})
export class CounterComponent {
@Input() count: number = 0;

@Output() change: EventEmitter<number> = new EventEmitter<number>();

increment() {
this.count++;
this.change.emit(this.count);
}

decrement() {
this.count--;
this.change.emit(this.count);
}
}
// 当值改变时,通过事件发射数据接收。

双向绑定

[()],Angular的双向绑定

通过修改绑定属性的方式,使用双向绑定即可,此时在子组件中只需要接收数据。

模板变量

通过子组件标签的#child,则child就相当于子组件component。

parent.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {Component, OnInit} from '@angular/core';
import {ChildComponent} from './child-component.ts';

@Component({
selector: 'parent-component',
template: `
<child-component #child></child-component>
<button (click)="child.name = childName">设置子组件名称</button>
`
})

export class ParentComponent implements OnInit {

private childName: string;

constructor() { }

ngOnInit() {
this.childName = 'child-component';
}
}

child.component.ts

1
2
3
4
5
6
7
8
9
10
import {Component} from '@angular/core';

@Component({
selector: 'child-component',
template: `I'm {{ name }}`
})

export class ChildComponent {
public name: string;
}

路由传参

在查询参数中传递参数

传递参数页面

1
<a [routerLink]="['/cinema-chain/cinema']" [queryParams]="{chain: 1}">查看影院</a>

点击跳转时,/cinema-chain/cinema?chain=1(?chain=1就是从路由里面传递过来的参数)。

接收参数的页面

1
2
3
constructor(private activatedRoute: ActivatedRoute) {
const chain = this.activatedRoute.snapshot.queryParams['chain'];
}

在url路由路径中传递参数

在path中传递参数就需要先修改原有的路径使其可以携带参数。

1
2
3
4
5
6
7
8
const routes: Routes = [
{path: 'main/:type', loadChildren: './index/index.module#IndexModule'},
{path: 'upload', loadChildren: './components/upload/upload.module#UploadModule'},
{path: 'operation', loadChildren: './components/operation/operation.module#OperationModule'},
{path: 'compare/:type', loadChildren: './components/compare/compare.module#CompareModule'},
{path: '**', component: PageNotFoundComponent},
];
整个路径被划分成两段变量

传递参数页面

1
2
3
4
5
6
7
8
9
10
11
<a [routerLink]="['/home',2]">主页</a>
这里的routerLink是一个数组,第一个值为路由的跳转路径,第二值为路由携带参数的值,这里传递的值为2

或者这样传递
constructor(private router: Router) {
this.router.navigate(['/product',1]);
this.router.navigateByUrl('/product/1');
}

或者这样传递
<a routerLink="/home/{{变量名}}"></a>

页面跳转的结果:/home/2

接收参数页面

1
2
3
4
constructor(private activatedRoute: ActivatedRoute) {
const chain = this.activatedRoute.snapshot.params['id'];
或者 chain = this.activatedRoute.snapshot.paramMap.get('id');
}

不能同时使用参数查询方式和路由路径Url 方式传递同一个页面的参数,否则报错。

参数快照和参数订阅

参数快照:获取路由中传递的参数的值得一个方法就用到了参数快照snapshot。

1
2
3
4
5
6
<a [routerLink]="['/home',2]">主页</a>

change_id(){
this.router.navigate(['/home',1]);
}
路由路径中想home同时传递了两个参数,1和2

当在页面第一次加载的时候会创建一次home,将2这个值传入页面,当点击按钮出发change_id事件的时候也会导航到home,但是在此之前主页已经被创建,并已经被赋值,此时导航到主页,主页并不会再次被创建,所以自然不会再次获取第二次导航过来的路由所携带的参数和值,但是路径变为了/home/1。

然而页面上的值仍然是2,获取当前路由所传递的参数值失败。这就是参数快照的弱点,为了解决这个问题引入了参数订阅:subscribe()。

1
2
3
4
5
constructor(private activatedRoute: ActivatedRoute) {
this.activatedRoute.params.subscribe(params => {
const id = params['id'];
});
}

采用参数订阅的方式subscribe()获取到一个类型为Params的属性params,并返回params里面的Id复制给本地变量homeID,这样就不会出现路径在变,但是页面里面的参数值不变的情况;

@ViewChild 装饰器

父组件获取子组件数据需要借助@ViewChild(),子组件直接引用。

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
selector: 'my-app',
template: `
<h4>Welcome to Angular World</h4>
<exe-child></exe-child>
`,
})
export class AppComponent {
@ViewChild(ChildComponent)
childCmp: ChildComponent;

ngAfterViewInit() {
console.log(this.childCmp.name); // "child-component"
}
}

child.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'exe-child',
template: `
<p>Child Component</p>
`
})
export class ChildComponent {
name: string = '';
constructor(private appcomponent:AppComponent) {
this.name='child-component'
}
}

基于RxJS Subject

rxjs版本基于6需要结合rxjs-compat使用
message.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {Injectable} from '@angular/core';
import {of} from 'rxjs/observable/of';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class MessageService {
private subject = new Subject<any>();
message: any;

sendMessage(message: any) {
this.message = message;
this.subject.next(message);
this.subject.complete();
}

clearMessage() {
this.message = null;
this.subject.next();
}

getMessage(): Observable<any> {
// return this.subject.asObservable(); // 数据一直在维持,会产生变化
return of(this.message); // 数据值传递一次
}
}

home.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component } from '@angular/core';
import { MessageService } from './message.service';

@Component({
selector: 'exe-home',
template: `
<div>
<h1>Home</h1>
<button (click)="sendMessage()">Send Message</button>
<button (click)="clearMessage()">Clear Message</button>
</div>`
})

export class HomeComponent {
constructor(private messageService: MessageService) {}

sendMessage(): void {
this.messageService.sendMessage('Message from Home Component to App Component!');
}

clearMessage(): void {
this.messageService.clearMessage();
}
}

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { MessageService } from './message.service';

@Component({
selector: 'my-app',
template: `
<div>
<div *ngIf="message">{{message.text}}</div>
<exe-home></exe-home>
</div>
`
})

export class AppComponent implements OnDestroy {
message: any;
subscription: Subscription;

constructor(private messageService: MessageService) {
this.subscription = this.messageService.getMessage().subscribe( message => {
this.message = message;
});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}

更多RxJS知识以及用法

rxjs版本基于6需要结合rxjs-compat使用
message.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {Injectable} from '@angular/core';
import {of} from 'rxjs/observable/of';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class MessageService {
private subject = new Subject<any>();
message: any;

sendMessage(message: any) {
this.message = message;
this.subject.next(message);
this.subject.complete();
}

clearMessage() {
this.message = null;
this.subject.next();
}

getMessage(): Observable<any> {
// return this.subject.asObservable(); // 数据一直在维持,会产生变化
return of(this.message); // 数据值传递一次
}
}

home.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component } from '@angular/core';
import { MessageService } from './message.service';

@Component({
selector: 'exe-home',
template: `
<div>
<h1>Home</h1>
<button (click)="sendMessage()">Send Message</button>
<button (click)="clearMessage()">Clear Message</button>
</div>`
})

export class HomeComponent {
constructor(private messageService: MessageService) {}

sendMessage(): void {
this.messageService.sendMessage('Message from Home Component to App Component!');
}

clearMessage(): void {
this.messageService.clearMessage();
}
}

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { MessageService } from './message.service';

@Component({
selector: 'my-app',
template: `
<div>
<div *ngIf="message">{{message.text}}</div>
<exe-home></exe-home>
</div>
`
})

export class AppComponent implements OnDestroy {
message: any;
subscription: Subscription;

constructor(private messageService: MessageService) {
this.subscription = this.messageService.getMessage().subscribe( message => {
this.message = message;
});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}

更多RxJS知识以及用法

angular路由缓存

路由缓存,input输入状态, 下拉框选中状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import {RouteReuseStrategy, DefaultUrlSerializer, ActivatedRouteSnapshot, DetachedRouteHandle} from '@angular/router';

export class CustomReuseStrategy implements RouteReuseStrategy {

public handlers: { [key: string]: DetachedRouteHandle } = {};

表示对路由允许复用
shouldDetach(route: ActivatedRouteSnapshot): boolean {
默认对所有路由复用 可通过给路由配置项增加data: { keep: true }来进行选择性使用,代码如下
如果是懒加载路由需要在生命组件的位置进行配置
if (!route.data.keep) {
return false;
}
return true;
}

当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.handlers[route.routeConfig.path] = handle;
}

若path在缓存中有的都认为允许还原路由
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!route.routeConfig && !!this.handlers[route.routeConfig.path];
}

从缓存中获取快照,若无则返回null
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
if (!route.routeConfig) return null;
if (route.routeConfig.loadChildren) return null; 在loadChildren路径上通过修改自定义RouteReuseStrategy中的检索函数时从不检索分离的路由。
return this.handlers[route.routeConfig.path];
}

进入路由触发,判断是否同一路由
shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
return future.routeConfig === current.routeConfig;
}
}

将以上代码引入到app.module.ts文件的providers: [{ provide: RouteReuseStrategy, useClass: AppRoutingCache }]

angular开启hash模式

Hash 模式是基于锚点定位的内部链接机制,在 URL 加上 # ,然后在 # 后面加上 hash 标签,根据不同的标签做定位

  • 针对初始化时带有路由的项目
1
2
3
4
5
配置路由时,routing.module.ts文件中,
@NgModule({
imports: [RouterModule.forRoot(routes , { useHash: true })],
exports: [RouterModule]
})
  • app.module.ts中进行配置
1
2
3
4
// 引入相关服务
import {HashLocationStrategy, LocationStrategy} from '@angular/common';
// 在@NgModule中的配置如下 | 服务依赖注入
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]

** URL 中包含的 hash 信息是不会提交到服务端,所以若要使用 SSR (Server-Side Rendered) ,就不能使用 Hash 模式即不能使用 HashLocationStrategy 策略。**

TypeScript模块系统

模块是指在其自身作用域里执行,而不是在全局作用域里;模块间是依靠 export 和 import 建立关系。编译器在编译过程中,也是依赖这种关系来定位需要编译的文件。

TypeScript 依然还是以 JavaScript 文件的形式发布类库,这会导致类型无法表述,需要配合声明文件对其进行类型描述;因此声明文件成了类库一个必不可少的组成部分。

分类

有声明文件

要分清类库是否有声明文件 *.d.ts

类库自带

从 Npm 安装一个依赖包后,可以直接检查该依赖包库的 package.json 是否包含 typings 节点,该节点对应的文件就是声明文件。

TypeSearch检索

TypeScript 提供了一个叫 TypeSearch 网站,可以直接输入关键词检查是否包含该类库的声明文件。

无声明文件

Angular Cli 创建的项目会包含一个 src/typings.d.ts 声明文件,它会自动包含在全局声明定义中,而把这些类库的声明信息写在这里面再好不过。

一般而言自己很难对一个类库写一个完整的声明文件,这对于成本来说太不合算,因此往往都是只对部分全局对象做一个 any (表示忽略该静态类型检查)亦可,例如:

1
declare const XLSX: any

如何使用?

有声明文件

对于有声明文件,无需额外做什么,只需在需要模块的地方使用 import 来导入即可。

无声明文件

使用 any 来表示忽略静态类型检查,意味者无法享受声明文件带来的智能提示快感,可以在项目的任意位置直接使用它,但也仅仅只能识别声明的变量,而实例的方法或属性是不可知的。

除此之外 TypeScript 编译过程中也不会对 G2 做任何类型检查,G2 是否真的存在只能由自己把握。对于 Angular 而言,是需要额外在 angular.json 的 scripts 节点上明确加载这些模块。

angular路由

Base href

index.html中存在<base>标签,路由需要根据这个来确定应用程序的根目录。例如,当我们转到http://example.com/page1时,如果我们没有定义应用程序的基础路径,路由将无法知道我们的应用的托管地址是http://example.com还是http://example.com/page1

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
<base href="/">
<title>Application</title>
</head>
<body>
<app-root></app-root>
</body>
</html>

Using the router

要使用路由,我们需要在 AppModule 模块中,导入 RouterModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';

@NgModule({
imports: [
BrowserModule,
RouterModule
],
bootstrap: [
AppComponent
],
declarations: [
AppComponent
]
})
export class AppModule {}

RouterModule.forRoot()

RouterModule.forRoot() 方法用于在主模块中定义主要的路由信息,通过调用该方法使得我们的主模块可以访问路由模块中定义的所有指令。

1
2
3
4
5
6
7
8
9
10
11
import { Routes, RouterModule } from '@angular/router';

export const ROUTES: Routes = []; // 便于我们在需要的时候导出ROUTES到其他模块中

@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(ROUTES)
],
})
export class AppModule {}

RouterModule.forChild()

RouterModule.forChild() 与 Router.forRoot() 方法类似,但它只能应用在特性模块中。

根模块中使用 forRoot(),子模块中使用 forChild()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';

export const ROUTES: Routes = [];

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(ROUTES)
],
// ...
})
export class ChildModule {}

Dynamic routes

如果路由始终是静态的,那没有多大的用处。使用动态路由我们可以根据不同的路由参数,渲染不同的页面。

1
2
3
4
5
6
7
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';

export const ROUTES: Routes = [
{ path: '', component: HomeComponent },
{ path: '/profile/:username', component: ProfileComponent }
];

/routeUrl/:params

:params是路由参数,而不是URL的实际部分。

在访问路由的时候routerLink或者navigate的时候就可以直接传递参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'profile-page',
template: `
<div class="profile">
<h3>{{ username }}</h3>
</div>
`
})
export class SettingsComponent implements OnInit {
username: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.params.subscribe((params) => this.username = params.username);
}
}

Child routes

每个路由都支持子路由,在setttings路由中定义了两个子路由,它们将继承父路由的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { SettingsComponent } from './settings/settings.component';
import { ProfileSettingsComponent } from './settings/profile/profile.component';
import { PasswordSettingsComponent } from './settings/password/password.component';

export const ROUTES: Routes = [
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: 'profile', component: ProfileSettingsComponent },
{ path: 'password', component: PasswordSettingsComponent }
]
}
];

@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(ROUTES)
],
})
export class AppModule {}

SettingsComponent组件中需要添加router-outlet指令,因为我们要在设置页面中呈现子路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from '@angular/core';

@Component({
selector: 'settings-page',
template: `
<div class="settings">
<settings-header></settings-header>
<settings-sidebar></settings-sidebar>
<router-outlet></router-outlet>
</div>
`
})
export class SettingsComponent {}

loadChildren

SettingsModule 模块,用来保存所有 setttings 相关的路由信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';

export const ROUTES: Routes = [
{
path: '',
component: SettingsComponent,
children: [
{ path: 'profile', component: ProfileSettingsComponent },
{ path: 'password', component: PasswordSettingsComponent }
]
}
];

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(ROUTES)
],
})
export class SettingsModule {}

在 SettingsModule 模块中我们使用 forChild() 方法,因为 SettingsModule 不是我们应用的主模块。

另一个主要的区别是我们将 SettingsModule 模块的主路径设置为空路径 (‘’)。因为如果我们路径设置为 /settings ,它将匹配 /settings/settings ,很明显这不是我们想要的结果。通过指定一个空的路径,它就会匹配 /settings 路径,这就是我们想要的结果。

AppModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const ROUTES: Routes = [
{
path: 'settings',
loadChildren: './settings/settings.module#SettingsModule'
}
];

@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(ROUTES)
],
// ...
})
export class AppModule {}

通过 loadChildren 属性,告诉 Angular 路由依据 loadChildren 属性配置的路径去加载 SettingsModule 模块。这就是模块懒加载功能的具体应用,当用户访问 /settings/** 路径的时候,才会加载对应的 SettingsModule 模块,这减少了应用启动时加载资源的大小。

  • loadChildren 的属性值,该字符串由三部分组成:
    • 需要导入模块的相对路径
    • #分隔符
    • 导出模块类的名称

History 对象

属性

  • length
    只读的,其值为一个整数,标志包括当前页面在内的会话历史中的记录数量,比如我们通常打开一个空白窗口,length 为 0,再访问一个页面,其 length 变为 1。
  • scrollRestoration
    允许 Web 应用在会话历史导航时显式地设置默认滚动复原,其值为 auto 或 manual。
  • state
    只读,返回代表会话历史堆栈顶部记录的任意可序列化类型数据值,我们可以以此来区别不同会话历史纪录

方法

  • back()
    返回会话历史记录中的上一个页面,等价于 window.history.go(-1) 和点击浏览器的后退按钮。

  • forward()
    进入会话历史记录中的下一个页面,等价于 window.history.go(1) 和点击浏览器的前进按钮。

  • go()
    加载会话历史记录中的某一个页面,通过该页面与当前页面在会话历史中的相对位置定位,如,-1 代表当前页面的上一个记录,1 代表当前页面的下一个页面。若不传参数或传入0,则会重新加载当前页面;若参数超出当前会话历史纪录数,则不进行操作。

  • pushState()
    在会话历史堆栈顶部插入一条记录,该方法接收三个参数,一个state 对象,一个页面标题,一个 URL:

    • 状态对象

      1、存储新添会话历史记录的状态信息对象,每次访问该条会话时,都会触发 popstate 事件,并且事件回调函数会接收一个参数,值为该事件对象的复制副本。

      2、状态对象可以是任何可序列化的数据,浏览器将状态对象存储在用户的磁盘以便用户再次重启浏览器时能恢复数据

      3、一个状态对象序列化后的最大长度是 640K,如果传递数据过大,则会抛出异常

    • 页面标题

      目前该参数值会被忽略,暂不被使用,可以传入空字符串

    • 页面 URL

      1、此参数声明新添会话记录的入口 URL

      2、在调用 pushState() 方法后,浏览器不会加载 URL 指向的页面,我们可以在 popstate 事件回调中处理页面是否加载

      3、此 URL 必须与当前页面 URL 同源,,否则会抛异常;其值可以是绝对地址,也可以是相对地址,相对地址会被基于当前页面 URL 解析得到绝对地址;若其值为空,则默认是当前页面 URL

  • replaceState()

    • 更新会话历史堆栈顶部记录信息,支持的参数信息与 pushState() 一致。
    • pushState() 与 replaceState() 的区别:pushState()是在 history 栈中添加一个新的条目,replaceState() 是替换当前的记录值。此外这两个方法改变的只是浏览器关于当前页面的标题和 URL 的记录情况,并不会刷新或改变页面展示。
  • onpopstate 事件

    • window.onpopstate 是 popstate 事件在 window 对象上的事件句柄。每当处于激活状态的历史记录条目发生变化时,popstate 事件就会在对应 window 对象上触发。如果当前处于激活状态的历史记录条目是由 history.pushState() 方法创建,或者由 history.replaceState() 方法修改过的,则 popstate 事件对象的 state 属性包含了这个历史记录条目的 state 对象的一个拷贝。
    • 调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退、前进按钮 (或者在 JavaScript 中调用 history.back()、history.forward()、history.go() 方法)。
    • 当网页加载时,各浏览器对 popstate 事件是否触发有不同的表现,Chrome 和 Safari 会触发 popstate 事件,而 Firefox 不会。

Hash模式和Html5模式

Hash模式

Hash 模式是基于锚点定位的内部链接机制,在 URL 加上 # ,然后在 # 后面加上 hash 标签,根据不同的标签做定位

针对初始化时带有路由的项目

1
2
3
4
5
配置路由时,routing.module.ts文件中,
@NgModule({
imports: [RouterModule.forRoot(routes , { useHash: true })],
exports: [RouterModule]
})

app.module.ts中进行配置

1
2
3
4
// 引入相关服务
import {HashLocationStrategy, LocationStrategy} from '@angular/common';
// 在@NgModule中的配置如下 | 服务依赖注入
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]

URL 中包含的 hash 信息是不会提交到服务端,所以若要使用 SSR (Server-Side Rendered) ,就不能使用 Hash 模式即不能使用 HashLocationStrategy 策略。

HTML5模式

HTML 5 模式则直接使用跟”真实”的 URL 一样,如上面的路径,在 HTML 5 模式地址如下:

1
https://segmentfault.com/u/angular4/user
  • HTML5模式下URL有两种访问方式:
    • 在浏览器地址栏直接输入 URL,这会向服务器请求加载页面。
    • 在 Angular 应用程序中,访问 HTML 5 模式下的 URL 地址,这不需要重新加载页面,可以直接切换到对应的视图。

在 HTML 5 模式下,Angular 使用了 HTML 5 的 pushState() API 来动态改变浏览器的 URL 而不用重新刷新页面。

开启HTML5模式

导入 APP_BASE_HREF、LocationStrategy、PathLocationStrategy

1
import { APP_BASE_HREF, LocationStrategy, PathLocationStrategy } from '@angular/common'

配置NgModule-providers

1
2
3
4
5
6
7
8
9
10
11
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(routes)
],
..,
providers: [
{ provide: LocationStrategy, useClass: PathLocationStrategy },
{ provide: APP_BASE_HREF, useValue: '/' }
]
})

示例代码中的 APP_BASE_HREF,用于设置资源 (图片、脚本、样式) 加载的基础路径。除了在 NgModule 中配置 provider 外,我们也可以在入口文件,如 index.html 文件 <base> 标签中设置基础路径。

<base> 标签为页面上的所有链接规定默认地址或默认目标。通常情况下,浏览器会从当前文档的 URL 中提取相应的路径来补全相对 URL 中缺失的部分。使用 <base> 标签可以改变这一点。浏览器随后将不再使用当前文档的 URL,而使用指定的基本 URL 来解析所有的相对 URL。这其中包括 <a>、<img>、<link>、<form> 标签中的 URL。具体使用示例如下:

1
<base href="/">

LocationStrategy

LocationStrategy 用于从浏览器 URL 中读取路由状态。Angular 中提供两种 LocationStrategy 策略:

  • HashLocationStrategy

  • PathLocationStrategy

以上两种策略都是继承于 LocationStrategy 抽象类,该类的具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export abstract class LocationStrategy {
// 获取path路径
abstract path(includeHash?: boolean): string;
// 生成完整的外部链接
abstract prepareExternalUrl(internal: string): string;
// 添加会话历史状态
abstract pushState(state: any, title: string, url: string,
queryParams: string): void;
// 修改会话历史状态
abstract replaceState(state: any, title: string, url: string,
queryParams: string): void;
// 进入会话历史记录中的下一个页面
abstract forward(): void;
// 返回会话历史记录中的上一个页面
abstract back(): void;
// 设置popstate监听
abstract onPopState(fn: LocationChangeListener): void;
// 获取base地址信息
abstract getBaseHref(): string;
}

HashLocationStrategy

1
2
3
4
5
6
7
8
9
10
11
12
HashLocationStrategy 类继承于 LocationStrategy 抽象类,它的构造函数如下:

export class HashLocationStrategy extends LocationStrategy {
constructor(
private _platformLocation: PlatformLocation,
@Optional() @Inject(APP_BASE_HREF) _baseHref?: string) {
super();
if (_baseHref != null) {
this._baseHref = _baseHref;
}
}
}

该构造函数依赖 PlatformLocation 及 APP_BASE_HREF 关联的对象。APP_BASE_HREF 的作用,我们上面已经介绍过了,接下来我们来分析一下 PlatformLocation 对象。

PlatformLocation

1
2
3
4
5
// angular2/packages/platform-browser/src/browser.ts
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
...,
{provide: PlatformLocation, useClass: BrowserPlatformLocation},
];

通过以上代码,我们可以知道在浏览器环境中,HashLocationStrategy 构造函数中注入的 PlatformLocation 对象是 BrowserPlatformLocation 类的实例。我们也先来看一下 BrowserPlatformLocation 类的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// angular2/packages/platform-browser/src/browser/location/browser_platform_location.ts
export class BrowserPlatformLocation extends PlatformLocation {
private _location: Location;
private _history: History;

constructor(@Inject(DOCUMENT) private _doc: any) {
super();
this._init();
}

_init() {
this._location = getDOM().getLocation(); // 获取浏览器平台下Location对象
this._history = getDOM().getHistory(); // 获取浏览器平台下的History对象
}
}

在 BrowserPlatformLocation 构造函数中,我们调用 _init() 方法,在方法体中,我们调用 getDOM() 方法返回对象中的 getLocation() 和 getHistory() 方法,分别获取 Location 对象和 History 对象。那 getDOM() 方法返回的是什么对象呢?其实该方法返回的是 DomAdapter 对象。

DomAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let _DOM: DomAdapter = null !;

export function getDOM() {
return _DOM;
}

export function setDOM(adapter: DomAdapter) {
_DOM = adapter;
}

export function setRootDomAdapter(adapter: DomAdapter) {
if (!_DOM) {
_DOM = adapter;
}
}

那什么时候会调用 setDOM() 或 setRootDomAdapter() 方法呢?通过查看 Angular 源码,我们发现在浏览器平台初始化时,会调用 setRootDomAdapter() 方法。具体如下:

1
2
3
4
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
{provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true},
...
];

WebSocket

  • 特点:
    • 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。WebSocket 允许服务器端与客户端进行全双工(full-duplex)的通信。
    • 建立在 TCP 协议之上,服务器端的实现比较容易。
    • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
    • 数据格式比较轻量,性能开销小,通信高效。
    • 可以发送文本,也可以发送二进制数据。
    • 没有同源限制,客户端可以与任意服务器通信,完全可以取代 Ajax。
    • 协议标识符是ws(如果加密,则为wss,对应 HTTPS 协议),服务器网址就是 URL。

WebSocket握手

  • 浏览器发出:
1
2
3
4
5
6
7
GET / HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

HTTP1.1 协议规定,Upgrade表示将通信协议从HTTP/1.1转向该字段指定的协议。Connection字段表示浏览器通知服务器,如果可以的话,就升级到 WebSocket 协议。Origin字段用于提供请求发出的域名,供服务器验证是否许可的范围内(服务器也可以不验证)。Sec-WebSocket-Key则是用于握手协议的密钥,是 Base64 编码的16字节随机字符串。

  • 服务器响应:
1
2
3
4
5
6
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/

服务器同样用Connection字段通知浏览器,需要改变协议。Sec-WebSocket-Accept字段是服务器在浏览器提供的Sec-WebSocket-Key字符串后面,添加“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”字符串,然后再取 SHA-1 的哈希值。浏览器将对这个值进行验证,以证明确实是目标服务器回应了 WebSocket 请求。Sec-WebSocket-Location字段表示进行通信的 WebSocket 网址。

完成握手以后,WebSocket 协议就在 TCP 协议之上,开始传送数据。

客服端API

  • 浏览器对 WebSocket 协议的处理,无非就是三件事。
    • 建立连接和断开连接
    • 发送数据和接收数据
    • 处理错误

1、 构造WebSocket函数

1
var ws = new WebSocket('ws://localhost: 8080');

执行上面语句之后,客户端就会与服务器进行连接。

2、webSocket.readyState

  • readyState属性返回实例对象的当前状态,共有四种。
    • CONNECTING:值为0,表示正在连接。
    • OPEN:值为1,表示连接成功,可以通信了。
    • CLOSING:值为2,表示连接正在关闭。
    • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch (ws.readyState) {
case WebSocket.CONNECTING:
// case 0:
// do something
break;
case WebSocket.OPEN:
// case 1:
// do something
break;
case WebSocket.CLOSING:
// case 2:
// do something
break;
case WebSocket.CLOSED:
// case 3:
// do something
break;
default:
// this never happens
break;
}

3、webSocket的api

  • webSocket.onopen 用于指定连接成功后的回调函数
  • webSocket.onclose 用于指定连接关闭后的回调函数
  • webSocket.onmessage 用于指定收到服务器数据后的回调函数,服务器数据可能是文本,也可能是二进制数据(blob对象或ArrayBuffer)
  • webSocket.send() 用于向服务器发送数据
  • webSocket.bufferedAmount 实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
  • webSocket.onerror

RxJS封装的WebSocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class WebsocketService {
private ws: WebSocket;

constructor() {
}

// 发送数据
send(message: any) {
this.ws.send(message);
}

// 建立连接
connect(url: string): Observable<any> {
this.ws = new WebSocket(url);
return new Observable(observer => {
this.ws.onmessage = (event) => observer.next(event.data);
this.ws.onerror = (event) => observer.error(event);
this.ws.onclose = (event) => {
console.log(event, '服务器端断开链接!');
observer.complete();
};
});
}
// 断开连接
disconnect() {
this.ws.close();
console.log('浏览器端断开链接!');
}
}