参考资料
《了不起的Node.js》[劳奇(Guillermo Rauch)-2014.1]
HTTP | Node.js Documentation
Web 模块 | 菜鸟教程
Express 模块 | 菜鸟教程
express官方API
node.js中的http.response.end方法使用说明_node.js - 阿里云
whiteMu的博客(博客园):nodejs之url模块
W3school:JavaScript的substr()方法
HTTP状态码 | 菜鸟教程
HTTP content-type | 菜鸟教程
Jerry Qu的博客:HTTP 协议中的 Transfer-Encoding
npm官网 body-parser 的API文档
express 第三方中间件
大多数服务器不仅可以运行服务端的脚本语言,而且可以通过脚本语言从数据库获取数据,将结果返回给客户端浏览器。该笔记介绍使用Nodejs实现服务器功能,涉及到两个模块:http
和express
。http
模块主要用于搭建HTTP 服务端 和 客户端,express
是一个简洁而灵活的 Nodejs Web应用框架,提供了一系列强大的特性帮助我们创建各种 Web 应用,同时包含丰富的 HTTP 工具。
1. 使用 http 模块创建服务器
1.1 实现思路及代码
HTTP即超文本传输协议,使用Nodejs http 模块的 createServer 方法创建服务器,获取前端的文件请求,然后根据请求将本地的文件写入到前端页面中,因此,需要依赖 fs 模块来读取文件,依赖 url 模块来解析链接,详细实现代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My test page</title>
</head>
<body>
<h1>My Head</h1>
<p>My paragraph</p>
</body>
</html>
server.js
var http = require('http');
var fs = require('fs');
var url = require('url');
//创建服务器(要点已标记)
http.createServer(function (request, response) {
//解析请求,包括文件名
var pathname = url.parse(request.url).pathname; /***1.3***/
//输出请求的服务名
console.log("Request for " + pathname + " received.");
//若不包含文件名,则默认到达首页
if (pathname == '/'){
pathname = '/index.html';
}
//从文件系统中读取请求的文件内容
fs.readFile(pathname.substr(1), function (err, data) { /***1.4***/
if (err) {
console.log(err);
// HTTP 状态码: 404 : NOT FOUND
// Content Type: text/plain
response.writeHead(404, {'Content-Type': 'text/html'});/***1.2***/
response.write("<h1>Page missing</h1>"); /***1.2***/
}else{
// HTTP 状态码: 200 : OK
// Content Type: text/plain
response.writeHead(200, {'Content-Type': 'text/html'});
// 响应文件内容
response.write(data.toString()); /***1.5***/
}
// 发送响应数据
response.end(); /***1.2***/
});
}).listen(3333);
1.2 HTTP 结构
HTTP 协议构建在请求和响应的概念上,对应在Node.js中就是由http.ServerResquest和http.ServerResponse这两个构造器构造出来的对象,即http.createServer(function(request, response){})
中的request和response。
当用户浏览一个网站时,用户代理(浏览器)会创建一个请求,该请求通过TCP发送给Web服务器,随后服务器会给出响应。
1.2.1 Request中的重要字段
通过上面的描述,我们知道request是客户端代理(浏览器)发出的请求,这个请求往往来自 HTTP 浏览器,不是由服务端定义的。那么请求包含了哪些内容?有哪些是常用的?这引起了我极大的兴趣。借助VS Code的调试功能,我观察到了request这一参数的内容,在此记录几个(自认为)比较重要的字段:
(Win7的系统,在谷歌浏览器中输入http://127.0.0.1:3333,得到的request部分信息)
△ headers: // 头信息
-accept-language:"zh-CN,zh;q=0.9"
-connection:"keep-alive"
-host:"127.0.0.1:3333"
-user-agent:"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
△ httpVersion:"1.1"
△ method:"GET"
△ socket:Socket {connecting: false, _hadError: false, _handle: TCP, …} // 套接字
△ url:"/"
1.2.2 Response 头信息:文件类型、状态码、连接和转换码
当收到请求,服务器借助response对象完成响应。response对象中,最重要的是它的头信息——response._header
,由于 HTTP 的目的是进行文档交换,它在请求和响应消息前使用头信息(header)来描述不同的消息内容。
我们借助response.writeHead()
函数来写入头信息(如下)。其中,200是状态码,{'Content-Type': 'text/html'}声明了发送的内容为html文档。
-
response.writeHead(statusCode[, statusMessage][, headers])
-
statusCode
<number> -
statusMessage
<string> -
headers
<Object>
-
response.writeHead(200, {'Content-Type': 'text/html'});
Web 页面会发送不同类型的内容:文本(text),HTML,XML,JSON,PNG及JPEG图片,等等。发送内容的类型(type)在Content-Type
头信息中标注,下面是常见的文件类型(HTTP content-type | 菜鸟教程):
类型 | Content-Type |
---|---|
文本(text) | text/plain |
HTML | text/html |
XML | text/xml |
JSON | application/json |
PNG | image/png |
JPEG | image/jpeg |
除了内容类型,头信息还包括了HTTP状态码(statusCode
),状态码就是告诉客户端服务器的响应状态,下面是常见的HTTP状态码(HTTP状态码 | 菜鸟教程):
- 200 - 请求成功
- 301 - 资源(网页等)被永久转移到其它URL
- 404 - 请求的资源(网页等)不存在
- 500 - 内部服务器错误
除了状态码statusCode
和内容类型Content-Type
,头信息还包括了Date
,Connection
和Transfer-Encoding
,这三个内容是 Nodejs 自动生成的。
当我们借助调试功能输出response._header
时,得到如下信息:
HTTP/1.1 200 OK
Content-Type: text/html
Date: Sun, 22 Jul 2018 06:50:19 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Date
是响应送出的时间,GMT是格林尼治太阳时(北京时间 - 8h);
Connection
:Node设置的默认值是keep-alive,是Node为了通知浏览器:你和我使用保持连接(这是为了提高性能,因为浏览器不想浪费时间去重新建立和关闭TCP连接。当然我们也可以调用writeHead方法传递一个不同的值,如Close,来将其重写掉);
Transfer-Encoding
:Node设置的默认值是chunked(分块编码),主要的原因是Node天生的异步机制,这样响应就可以逐步产生。在头部加入该字段后,就代表这个报文采用了分块编码。详细说明参见:Jerry Qu的博客:HTTP 协议中的 Transfer-Encoding
1.2.3 写入数据内容及结尾:response.write()和response.end()
response.write(chunk[, encoding][, callback])
和response.end([data][, encoding][, callback])
是为 http 响应中填写内容的主要方法:
-
response.write(chunk[, encoding][, callback])
-
chunk
<string> | <Buffer> -
encoding
<string> Default:'utf8'
-
callback
<Function> - Returns: <boolean>
-
-
response.end([data][, encoding][, callback])
-
data
<string> | <Buffer> -
encoding
<string> -
callback
<Function> - Returns: <this>
-
例如,我们可以直接在write()或end()中写入HTML语句:
response.writeHead(200, {Content-Type: 'text/html'});//前提是定义内容类型为html
response.write('<h1>My Head.</h1>');
response.end('<p>My paragraph.</p>');
也可以使用JavaScript的toString()将fs读取到的文件数据(data)转换成字符串放到write()中去(见1.5).
response.end()
除了可以发送内容,它本身还是一个信号(signal),告诉服务器头信息(headers)和内容主体(body)已经送达,且该方法必须在每个response出现时被调用;
在调用end前,我们可以多次调用response.write()
方法来发送数据(This method may be called multiple times to provide successive parts of the body),由于Node http设置了Transfer-Encoding
的默认值是chunked(分块编码),因此每个write及end都将作为一个数据块进行发送。
1.3 url.parse()
url.parse()
的作用是将一个url的字符串解析并返回一个url对象:
url.parse("http://user:pass@host.com:8080/p/a/t/h?query=string#hash");
/*
返回值:
{
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'
}
没有设置第二个参数为true时,query属性为一个字符串类型
*/
1.4 fs.readFile() 和 substr()
fs.readFile(filename, function (error, data) {})
:读取本地名为filename的文件,将读取到的结果存储在data
中,通过观察得知data
的数据类型为Buffer,以数组的形式存储文件中字符串的ASCII码;pathname.substr(1)
:pathname的内容是'/index.html',substr(1)是JavaScript方法,表示从下标为1开始读取字符串,因此pathname.substr(1) == 'index.html';
1.5 data.toString()
data.toString()
的目的是将data中的ASCII码转换成字符串的形式:
data:
△ Buffer(184) [60, 33, 68, 79, 67, 84, 89, 80, …]
data.toString():
"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My test page</title>
</head>
<body>
<h1>My Head</h1>
<p>My paragraph</p>
</body>
</html>"
2. 使用 http 模块创建客户端
这个用的比较少,且方法也比较简单,简单介绍一下,根据代码来就行了。
/**
* 使用 Node 创建 Web 客户端
*/
var http = require('http');
// 用于请求的选项
var options = {
host: '127.0.0.1',
port: '3333',
path: '/index.html'
};
// 处理响应的回调函数
var callback = function (response) {
// 不断更新数据
var body = '';
response.on('data', function (data) {
body += data;
});
response.on('end', function () {
// 数据接收完成
console.log(body);
});
}
// 向服务端发送请求
var req = http.request(options, callback);
req.end();
3. express 核心特性与第一个实例
3.1 express 的核心特性
express 是一个简洁而灵活的 node.js Web应用框架,提供了一系列强大的特性帮助我们创建各种 Web 应用,并提供了丰富的 HTTP 工具,使用 express 可以快速地搭建一个功能完整的网站。
express 框架核心特性:
- 可以设置中间件(
app.use()
)来响应 HTTP 请求; - 定义了路由表用于执行不同的 HTTP 请求动作;
- 可以通过向模板传递参数来动态渲染 HTML 页面。
3.2 第一个实例
在这个实例中,我们首先在index.html文件中创建了一个表单元素,action指向/insert
页面,
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Submit Your Papers</title>
</head>
<body>
<h2>INSERT DATA</h2>
<form action="http://127.0.0.1:3333/insert" method="POST">
Paper_ID(9-bit): <input type="number" name="Paper_ID"><br>
Paper_Name: <input type="text" name="Paper_Name"><br>
Paper_Type: <select name="Paper_Type">
<option disabled="disabled">--请选择--</option>
<option selected="selected">EI期刊</option>
<option>SCI期刊</option>
<option>中文核心</option>
</select><br>
Author: <input type="text" name="Author"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
server.js
// 依赖项
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var urlencodedParser = bodyParser.urlencoded({ extended: false });// 编码解析(3.2.1)
app.use(urlencodedParser);// 使用中间件
// 获取首页
app.get('/index.htm*', function (req, res) {
res.sendFile(__dirname + '/' + 'index.html');
});
app.get('/', function (req, res) {
res.sendFile(__dirname + '/' + 'index.html');
});
// 响应 INSERT POST
app.post('/insert', function (req, res) { //GET 和 POST的区别和联系(4.2)
res.type('application/json'); // (3.2.2)设置Content-Type的MIME类型
res.json(req.body); // (3.2.2)传送JSON响应
});
// 监听
var server = app.listen(3333,'localhost',function () {//(3.2.3)
var host = server.address().address
var port = server.address().port
console.log('应用实例,访问地址为 http://%s:%s', host, port)
});
3.2.1 编码解析 body-parser
通过body-parser
创建中间件,当接收到客户端请求时,所有的中间件都会给req.body
添加属性(即开始解析请求数据),若请求体为空或者Content-Type
不匹配,则解析为空{}
(或出现某个错误)。
// 借助body-parser创建中间件并使用
var bodyParser = require('body-parser');
var urlencodedParser = bodyParser.urlencoded({ extended: false });// 编码解析
app.use(urlencodedParser);
body-parser
提供了多种方法(如下,详细解释见上方参考资料的官方文档)用以解析不同类型的请求数据(<form enctype="value">
,value= application/x-www-form-urlencoded
[默认]、multipart/form-data
、text/plain
)。由于用于试验的表单内容不包括文件等复杂类型,又可能出现中文内容,因此我们只需要对默认的类型进行解析,因此在处理POST请求时用到了bodyParser.urlencoded()
来解析请求体。
bodyParser.json([options])
bodyParser.raw([options])
bodyParser.text([options])
bodyParser.urlencoded([options])
options
是urlencoded()方法中的唯一参数,其是一个包含“键-值对”的数据结构,其中最关键的“键”是extended
,其决定了允许解析的请求体(req.body)内容。当extended
的值为false时,req.body的内容可以为字符串或者数组,当extended
的值为true时,req.body的内容可以为任何类型的数据。options
所有键值如下(详细参考官方文档):
-
extended
- 用于规定解析内容的范围,这取决于调用的是querystring库(false
)还是qs库(true
)。默认值为true
; -
inflate
- 当设置为true
,压缩的请求体会被解压;当设置为false
,将拒绝接收压缩的请求体。默认值为true
; -
limit
- 规定了请求体的最大尺寸。如果请求体是数字,则该值表示最大字节数;如果请求体是字符串,则先该值传递到字节库(另一个nodejs模块-bytes
)再进行解析。默认值为'100kb'
; -
parameterLimit
- 规定 URL 编码数据中参数的最大数量,如果超过这个值,就会返回413的状态码给客户端。例如在解析表单元素的POST请求时,设置该值为2
,然后在表单元素中设置三个input框,提交数据时就会报错:too many parameters
。默认值为1000
; -
type
- 用于确定中间件将解析何种媒体类型。默认值为application/x-www-form-urlencoded
; -
verify
- 用于核查的键(不知道有什么用)。
3.2.2 res.type() 和 res.json()的功能
-
res.type()
- 设置 Content-Type 的 MIME 类型,类似于http中的writeHead功能; -
res.json()
- 传送JSON响应。可以将json数据放在里面传送至客户端,经试验发现,也可以传递一个对象,如本例中的req.body,json()方法能进行相应的格式转换; - express 中 res 和 req 对象的其他属性及方法详见—>4.4 express 的请求(request)和响应(response)对象
3.2.3 监听时避免address为“::”的方法
监听函数:
app.listen(port, [hostname], [backlog], [callback])
监听时需要调用app的listen方法,若直接采用如下方式调用,console.log()输出的结果是:
应用实例,访问地址为http://:::3333。
var server = app.listen(3333, function () {
var host = server.address().address
var port = server.address().port
console.log('应用实例,访问地址为 http://%s:%s', host, port)
});
因此我们需要在函数参数中制指定主机名称(localhost
或者127.0.0.1
):
var server = app.listen(3333,'localhost',function () {//(3.2.3)
var host = server.address().address
var port = server.address().port
console.log('应用实例,访问地址为 http://%s:%s', host, port)
});
4. express 的更多应用
4.1 什么是 express 中间件
中间件(MiddleWare)可以理解为一个对用户请求(request)进行过滤和预处理的东西,就像一张滤网,一般不会直接对客户端进行响应,而是将处理之后的结果传递下去。它是一个过滤器,可以拦截任何请求,可以对请求的request和response做相关处理。
引用中间件最简单的方法就是使用`app.use()`啦,下面是一个最简单的例子。当然了,中间件除了引用已有的,还可以自定义(需要再写一篇笔记来专门讲讲中间件了),引用及自定义的详细使用方法见[官方文档](http://www.expressjs.com.cn/4x/api.html#app.use)。
```JavaScript
app.use(express.static('G:/MyWebs')); // 设置静态文件
```
express还有哪些中间件?参考:express 第三方中间件
4.2 GET and POST
4.2.1 它们分别是什么?有什么区别?各有什么优缺点?
GET 和 POST 是 HTTP 请求的两种基本方法,最直观的区别就是 GET 把参数包含在 URL 中,POST 通过 request body (请求体)传递参数,大致的区别和优缺点如下:
- GET 请求只能进行url编码,而 POST 支持多种编码方式;
- GET 请求在 URL 中传送的参数是有长度限制的,而 POST 没有;
- 对于参数的数据类型,GET 只接受 ASCII 字符,而POST没有限制;
- GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息;
- GET 参数通过 URL 传递,POST 放在 request body 中。
在我大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。为了避免这种情况发生,交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。
在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起http请求)和服务器(接受http请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然GET可以带request body,也不能保证一定能被接收到哦。
好了,现在你知道,GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
总结来说,HTTP 对 GET 和 POST 参数的传送渠道(url还是request body)提出了要求。这是一个关于安全or性能的问题,想要安全性和更大的数据量,那么请使用 POST(例如我们经常用到的HTML表单,多数情况下使用POST传输),若想要快速且直观地传输数据,那么请使用 GET(例如大多数搜索引擎对于关键字的传递,用的就是GET)。
4.2.2 获取 app.get 和 app.post 中表单字段的方法
鉴于 GET 和 POST 传递参数时参数位置的不同(url 中还是 request body 中),因此在获取表单元素的字段时,采用不同的方法。
仅针对表单元素的请求req。以一个简单的表单为例(如下)。当method为GET时,使用app.get()
+req.query
来获取字段的值;当method为POST时,使用app.post()
+req.body
来获取字段的值。
四个地方需要注意:
- html中form的method属性(GET or POST);
- js中app.get和app.post使用;
- js中req.query和req.body使用;
- 在使用app.post()前,应使用body-parser中间件3.2.1 编码解析 body-parser。
.html(GET)
<form action="http://127.0.0.1:3333/insert_get" method="GET">
INPUT: <input type="text" name="input_text">
<input type="submit" value="Submit">
</form>
.js(响应insert_get,使用req.query访问字段)
app.get('/insert_get', function(req, res) {
res.send(req.query.input_text); // 发送数据
}
.html(POST)
<form action="http://127.0.0.1:3333/insert_post" method="POST">
INPUT: <input type="text" name="input_text">
<input type="submit" value="Submit">
</form>
.js(响应insert_post,使用req.body访问字段)
app.post('/insert_post', function(req, res) {
res.send(req.body.input_text); // 发送数据
}
4.3 静态文件(express.static)
express 提供了内置的中间件express.static
来设置静态文件如:图片, CSS,JavaScript 等。
可以使用express.static
中间件来设置静态文件路径。例如,想将写好的静态网页、CSS文件、js文件、图片、文档(放在G:/MyWebs/
中)提供给大家访问,那么可以这么写:
app.use(express.static('public'));
若要将脚本文件所在的文件夹(当前目录)作为静态文件,可以这么写:
app.use(express.static('./'));
4.4 express 的请求(request)和响应(response)对象
request 和 response 对象的具体介绍:
Request 对象 - request 对象表示 HTTP 请求,包含了请求查询字符串,参数,内容,HTTP 头部等属性。常见属性有:
- req.app:当callback为外部文件时,用req.app访问express的实例
- req.baseUrl:获取路由当前安装的URL路径
- req.body / req.cookies:获得「请求主体」/ Cookies
- req.fresh / req.stale:判断请求是否还「新鲜」
- req.hostname / req.ip:获取主机名和IP地址
- req.originalUrl:获取原始请求URL
- req.params:获取路由的parameters
- req.path:获取请求路径
- req.protocol:获取协议类型
- req.query:获取URL的查询参数串
- req.route:获取当前匹配的路由
- req.subdomains:获取子域名
- req.accepts():检查可接受的请求的文档类型
- req.acceptsCharsets / req.acceptsEncodings /req.acceptsLanguages:返回指定字符集的第一个可接受字符编码
- req.get():获取指定的HTTP请求头
- req.is():判断请求头Content-Type的MIME类型
Response 对象 - response 对象表示 HTTP 响应,即在接收到请求时向客户端发送的 HTTP 响应数据。常见属性有:
- res.app:同req.app一样
- res.append():追加指定HTTP头
- res.set()在res.append()后将重置之前设置的头
- res.cookie(name,value [,option]):设置Cookie
- opition: domain / expires / httpOnly / maxAge / path / secure / signed
- res.clearCookie():清除Cookie
- res.download():传送指定路径的文件
- res.get():返回指定的HTTP头
- res.json():传送JSON响应
- res.jsonp():传送JSONP响应
- res.location():只设置响应的Location HTTP头,不设置状态码或者close response
- res.redirect():设置响应的Location HTTP头,并且设置状态码302
- res.render(view,[locals],callback):渲染一个view,同时向callback传递渲染后的字符串,如果在渲染过程中有错误发生next(err)将会被自动调用。callback将会被传入一个可能发生的错误以及渲染后的页面,这样就不会自动输出了。
- res.send():传送HTTP响应
- res.sendFile(path [,options] [,fn]):传送指定路径的文件 - 会自动根据文件extension设定Content-Type
- res.set():设置HTTP头,传入object可以一次设置多个头
- res.status():设置HTTP状态码
- res.type():设置Content-Type的MIME类型
除了所列的这些 response 方法,express 还继承了 http response 中常用的writeHead()
、write()
、end()
方法,其中,writeHead已经进化为res.type(),end方法也不再是每次response出现时都必须调用,但当我们想要按顺序发送响应数据时,依旧可以使用write()方法实现分块编码
。
res.send()方法和response.write()方法的比较
res.type('html'); // 直接使用write时,仍需要指定类型,不然会是乱码
res.write('<h1>啊哈!</h1>'); // 允许书写多个write
res.write('<h2>哈你大爷呢!</h2>');
res.send('<h1>啊哈!</h1>'); // send()方法会自动解析数据类型并予以发送
res.write('<h2>哈你大爷呢!</h2>'); // 在send() 之后的write()或send()将不起作用
res.send('<h3>哈你二爷呢!</h3>');