express

1.express模块自身就是一个函数,而我们要做的就是利用这个函数去实例化出一个server对象。

2.express被设计成了插件机制,如果你想实现什么功能的话,那么使用相对应的中间件即可。使用中间件的方法如下所示:

1
2
3
4
5
6
const express = require('express');
const morgan = require('morgan');

const app = express();

app.use(morgan('dev'));

3.实例化Promise对象时所传递的callback是立执行的。resolve以及reject中的参数将会作为then()以及catch()的参数。一个Promise只能够被决议一次。

4.express中间件的形式形如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function middleWare(request, response, next) {
// do stuff with request and/or response
next(); // when our middleware done,call the next middleWare to do work
}

// 你甚至可以创建一些下面这种没意思但是有趣的middleware
let i = 1;
app.use((req, res, next) => {
console.log(i, req.method, req.url); // 打印请求

// a joke
let second = new Date().getSeconds();
if (second % 2 === 0) {
next();
} else {
res.status(403).json({"from_me": "403 forbidden"});
}
});

5.一些不可错过的中间件之静态文件中间件:

1
app.use(express.static(PUBLIC_PATH));

关于express.static中间件有一个需要注意的问题就是,当这个中间件进行了有效的服务的话(指分发了静态资源),那么它将会阻止中间件 chain,相当于不会调用next;但是如果没有匹配到需要分发的静态资源的话,那么会调用next,将执行权利继续交给下一个midlleware。所以最佳实践是将静态资源中间件放在后面的适当位置(自然是要放在404 middleware前面的)。

6.路由:routing is a way to map requests to specific handlers depending on their URL and HTTP verb

1
2
3
app.get('/hello/:who', (req, res) => {
res.end(`<h1>hello ${req.params.who}</h1>`);
});

7.重定向:

1
2
3
4
app.get('/redirect', (req, res) => {
res.redirect('/index'); // 站内重定向,或者像下面这样,重定向到其他站点
// res.redirect('http://www.expressjs.com');
});

8.发文件:

1
2
3
4
const filePath = path.resolve(__dirname, 'somePath');
app.get('/file', (req, res) => {
res.sendFile(filePath); // 需要注意的是并不会触发下载操作
});

9.获取访问ip(利用这个功能很容易实现拦截指定IP用户访问站点):

1
2
3
4
app.use((req, res, next) => {
console.log(req.ip, req.method, req.url);
next();
});

10.获取请求头中的某个字段的值:

1
2
3
4
app.use((req, res, next) => {
console.log(req.ip, req.method, req.url, req.get('Accept-Language'));
next();
});

11.视图(采用ejs视图为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var app = express();
const path = require('path');

app.set('views', path.resolve(__dirname, 'VIEWS_PATH')); // 第一步设置视图文件所在的文件位置
app.set('view engine','ejs'); // 设置视图引擎为EJS

app.get('/', (req, res) => {
res.render('index', {message : 'ejs template'});
});

// VIEWS_PATH/index.ejs

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>ejs</title>
</head>
<body>
<%= message %>
</body>
</html>

12.让所有视图都可以使用某个数据(将这个数据挂载到app.locals下面即可):

1
2
3
const app = express();

app.locals.allCanUse = 'all the views template can use this me';

13.res.end表明node已经处理好了响应对象,是时候将响应对象发送给客户端了。

14.Express中的middleware stack

首先有很重要的一点需要明白,那就是express是基于node进行处理的,前面也提到了,node处理输入并作出响应可以被抽象为对req object以及res object的处理,node把每个网络中传输过来的二进制请求转换为JavaScript object,当我们做好了响应后,使用res.end便又把响应数据发送给了客户端。在原生的node里面,这两个对象在一个函数过程里面进行处理。而在express中,req,res这两个对象是经由一系列的函数数组进行处理的,这个函数数组便叫做middleware stack了。具体过程便是:1.请求到达node server->2.交接给express APP进行处理->3.express中middleware stack中的每一个函数对请求进行处理->4.处理好了之后将结果返回给node server->5.发回给客户。

middleware stack中的每一个函数都接受三个参数,其中前两个分别是req和res,他们都是node server产生的,当然express在两个对象的基础上进行了一些增强。第三个参数就是next,是一个函数类型的变量。

当处理好了响应(响应数据已经可以发送给客户端了)的时候,调用res.end方法即可;当然在express中,我们也可以通过调用其他方法比如说res.send和res.sendFile或者json等,无论如何,上面这些提到的方法都将调用res.end。

对于一个中间件来说,下面两个动作必须执行其中的某一个,否则会发生错误(比如请求一直被挂起)。两个动作分别如下所示:

  • 1.作出响应,具体来说就是调用res.end或者res.send,以及res.sendFile等;
  • 2.调用next()将执行权利交出去,让middleware stack中的下一个函数执行;

15.错误中间件,错误中间件具有四个参数,按照顺序来分别是(err, req, res, next)。如果在某个正常的中间件中,调用next方法的时候传入了一个Error参数的话,那么当这个中间件执行完后,接下来执行的中间件有可能就不是下一个中间件了,而是下一个错误捕获中间件。并且错误中间件也能够调用next方法,同理,如果往next里面传入了一个错误参数的话那么也是跳到最近的下一个错误中间件。举个例子:假如有A,B,C,D,E,F这六个中间件,其中中间件C和中间件E都是错误中间件的话,那么当在执行B中间件的时候如果next的参数是一个error的话,那么执行权就到了最近的下一个错误捕获中间件C,如果继续在中间件C中的next方法传入一个error参数的话,那么执行权就又到了E错误中间件。

16.路由:express能够根据HTTP动作和请求资源将请求链接到相应的controller上面。

动态路由匹配使用下面这种模式:

1
2
3
app.get('/users/:userId', (req, res) => {
// do some stuff
});

类似于上面这样的路由能够动态匹配到/users/123和/users/ebooks等类似的;但是对于像下面这样的路由是匹配不到的:/users/或者/users/123/photo等类似的。

17.动态路由匹配之使用正则表达式

1
2
3
app.get(/^\/users\/(\d+)$/, (req, res) => {
let userId = parseInt(req.params[0], 10);
});

对于上面这个例子来说,我们定义的正则表达式所表达的意思是指:必须是以/users开始,以一个数字或者多个数字结尾的路由;使用正则表达式来定义动态路由匹配和使用普通字符串来匹配路由有一个区别就是,正则表达式没有给动态内容命名,对于这种情况,express是这样处理的:利用索引进行访问,每个被正则表达式所匹配到的值都会顺序占据数组的一个位置。所以看看下面这个例子:

1
2
3
4
app.get(/^\/users\/(\d+)-(\d+)$/, (req, res) => { // 貌似一个圆括号是一个匹配单位
let startId = parseInt(req.params[0], 10);
let endId = parseInt(req.params[1], 10);
});

再看一个更加复杂的匹配要求,要求匹配 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,其中x是任意一个十六进制中的一个数,y是8,9,A,B中的某一个:

1
2
3
4
let reg = /^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89AB][0-9a-f]{3}-[0-9a-f]{12})$/i;
app.get(reg, (req, res) => {
let uuid = req.params[0];
});

18.路由值查询字符串匹配

先看一个正常的例子:

1
2
3
4
// url像这样:/search?key=chaos
app.get('/search', (req, res) => {
let key = req.query.key; // 值是一个字符串
});

再看一个不太正常的例子:

1
2
3
4
// url像这样:/search?key=oh&key=no ; 糟糕的url设计
app.get('/search', (req, res) => {
let keyArr = req.query.key; // 由于两个query的键名一样,所以键值是一个数组!!!
});

19.express路由:

Routers就是一个中间件的角色,可以使用app.use方法在express的实例化对象中使用这个这个中间件。换句话说,routes把我们整个应用分割成了几份小app来对待,这可以避免在app.js里面出现大量的路由逻辑代码,避免app.js变得越来越臃肿。

下面可以看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// index.js
const apiRouter = require('./routes/apiRouter');

app.use('/api', apiRouter);

// routes/apiRouter.js
const express = require('express');

const api = express.Router();

api.get('/users', (req, res) => {
res.status(200).send('获取用户列表数据成功');
});

api.get('/topic', (req, res) => {
res.status(200).send('获取热点数据成功');
});

module.exports = api;

20.https一些基础知识

https之所以比http安全,是因为他在http的基础上增加了一层协议——TLS或SSL,这里简单的提一下TLS。TLS使用的加密技术简单来说就是这样:每个节点中的设备都具有一个public key和一个private key,其中public key只有自己知道,而private key则所有网络中的其他节点都能知道。当节点A想传数据给节点B的时候,节点A发送的信息会使用自己的private key和节点B的public key进行加密,当节点B接收到数据后,在利用自己的private key和节点A的public key进行解密。由于理论上,自己的private key只有自己知道,因此即使传输数据被其它设备给非法获取的话,那么也不能够破解出传输内容。

21.在express中既使用http服务也使用https服务:

利用HTTPS的话首先需要用户私钥以及凭证,我们可以利用OpenSSL来生成,如下所示:

1
2
3
## 项目根目录
openssl genrsa -out privatekey.pem 1024 ## 生成私钥
openssl req -new -key privatekey.pem -out request.pem ## 生成凭证

在express中既启动http服务又启动https服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 项目根目录index.js
const express = require('express');
const http = require('http');
const https = require('https');
const fs = require('fs');

const app = express();

const httpsOptions = {
key: fs.readFileSync('./privatekey.pem'),
cert: fs.readFileSync('./request.pem')
};

http.createServer(app).listen(80);
https.createServer(httpsOptions, app).listen(443);

22.http动词进一步认识

  • 1.GET: GET方法不应该改变应用的状态,GET是幂等性的,这意味着你从服务器连续取500次某种资源的结果和只取一次的结果应该是一样的,资源不应该发生变化。

23.api version:为了兼容老用户,但是新的业务逻辑又的确需要更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// api1.js
const express = require('express');

const api = express.Router();

api.get('time', (req, res) => {
let str = new Date(new Date() - 24*60*60*1000);
res.status(200).json({ time: str });
});

// app.js
const express = require('express');
const routerV1 = require('./api1');

const app = express();

app.use('v1', routerV1); // url => /v1/time
app.listen(8080, () => {
console.log('port 8080');
});

24.一些HTTP状态码:

    1. 100 => Continue
    1. 101 => Switch Protocol
    1. 200 => Ok
    1. 201 => 成功,常见于PUT请求和POST请求
    1. 202 => 成功收到请求,但是请求的东西还在处理过程中
    1. 301 => don’t visit this url any more
    1. 401 => 未授权 Unauthorized
    1. 403 => 无权 Forbidden
    1. 404 => 资源找不到
    1. 500 => 服务器出错了

25.从req.headers中获取到你想要访问的请求头

  • req.headers.origin: 资源发起位置
  • req.headers.referer: 原始资源位置

26.HTTP的OPTIONS方法用于获取目的资源所支持的通信选项,客户端可以对特定的URL使用OPTIONS方法,也可以对整站使用OPTIONS方法(通过将URL设置为通配符*)即可。同时,在跨域中,在发起一个正式请求之前也一定会发起一个预检请求,这个预检请求会被服务器检查是否支持接下来的实际请求,这个预检请求报文中的Access-Control-Request-Method首部字段的值就是实际请求中的Access-Control-Request-Method的值,预检请求报文中的Access-Control-Request-Headers首部字段的值就是接下来实际请求中的Access-Control-Request-Method的值,服务器在收到OPTOINS预检请求后,会返回一个Access-Control-Allow-Methods首部字段,这个字段会表明服务器所允许的请求方法,和Allow字段类似,但是Access-Control-Allow-Methods只会用在CORS上面(注意GET,HEAD,POST可能不受此限制)。

27.前面提到了预检请求,那么在客户端发送了一个预检请求后,如果预检请求后客户端发送请求头中method和服务端返回的响应头中的method不匹配的话,那么该怎么阻挡下次正式请求发送呢?答案就是浏览器会自动判断,如果不匹配的话,那么浏览器会自动阻止下次正式请求的发起。