Nodejs学习笔记,3 创建服务器:Web 模块(http与 express 框架

目录


《了不起的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实现服务器功能,涉及到两个模块:httpexpresshttp模块主要用于搭建HTTP 服务端客户端express是一个简洁而灵活的 Nodejs Web应用框架,提供了一系列强大的特性帮助我们创建各种 Web 应用,同时包含丰富的 HTTP 工具。

1. 使用 http 模块创建服务器

1.1 实现思路及代码

 HTTP即超文本传输协议,使用Nodejs http 模块的 createServer 方法创建服务器,获取前端的文件请求,然后根据请求将本地的文件写入到前端页面中,因此,需要依赖 fs 模块来读取文件,依赖 url 模块来解析链接,详细实现代码如下:

index.html

<!DOCTYPE html>
<html >
<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
HTMLtext/html
XMLtext/xml
JSONapplication/json
PNGimage/png
JPEGimage/jpeg

 除了内容类型,头信息还包括了HTTP状态码(statusCode),状态码就是告诉客户端服务器的响应状态,下面是常见的HTTP状态码(HTTP状态码 | 菜鸟教程):

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误

 除了状态码statusCode和内容类型Content-Type,头信息还包括了DateConnectionTransfer-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 >
    <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 >
<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

npm官网 body-parser 的API文档

 通过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-datatext/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两种基本请求方法的区别

  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来获取字段的值。

 四个地方需要注意:

  1. html中form的method属性(GET or POST);
  2. js中app.get和app.post使用;
  3. js中req.query和req.body使用;
  4. 在使用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)对象

requestresponse 对象的具体介绍:

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>');