参考资料
《了不起的Node.js》[劳奇(Guillermo Rauch)-2014.1]
HTTP | Node.js Documentation
Web 模块 | 菜鸟教程
Express 模块 | 菜鸟教程
express官方API
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
1 |
|
server.js
1 | var http = require('http'); |
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部分信息)
1 | △ headers: # 头信息 |
1.2.2 Response 头信息:文件类型、状态码、连接和转换码
当收到请求,服务器借助response对象完成响应。response对象中,最重要的是它的头信息——response._header
,由于 HTTP 的目的是进行文档交换,它在请求和响应消息前使用头信息(header)来描述不同的消息内容。
我们借助response.writeHead()
函数来写入头信息(如下)。其中,200是状态码,{'Content-Type': 'text/html'}
声明了发送的内容为html文档。
1 | /** |
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
时,得到如下信息:
1 | HTTP/1.1 200 OK |
- 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()
和 response.end()
是为 http 响应中填写内容的主要方法,例如,我们可以直接在write()或end()中写入HTML语句:
1 | // 前提是定义内容类型为html |
也可以使用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对象:
1 | url.parse("http://user:pass@host.com:8080/p/a/t/h?query=string#hash"); |
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码转换成字符串的形式:
1 | console.log(data); |
2. 使用 http 模块创建客户端
这个用的比较少,且方法也比较简单,简单介绍一下,根据代码来就行了。
1 | /** |
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
1 |
|
server.js
1 | // 依赖项 |
3.2.1 编码解析 body-parser
通过body-parser创建中间件,当接收到客户端请求时,所有的中间件都会给req.body
添加属性(即开始解析请求数据),若请求体为空或者Content-Type
不匹配,则解析为空{}
(或出现某个错误)。
1 | // 借助body-parser创建中间件并使用 |
由于用于试验的表单内容不包括文件等复杂类型,又可能出现中文内容,因此在处理POST请求时用到了 bodyParser.urlencoded()
来解析请求体,对 默认类型 application/x-www-form-urlencoded
进行解析。
body-parser
提供了多种方法(如下,详细解释见上方参考资料的官方文档)用以解析不同类型的请求数据(类型在表单元素form
中定义:<form enctype="value">
,其中,value
的可选值有:application/x-www-form-urlencoded
【默认】、multipart/form-data
、text/plain
)。
1 | bodyParser.json([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为“::”的方法
监听函数:1
app.listen(port, [hostname], [backlog], [callback])
监听时需要调用app的listen方法,若直接采用如下方式调用,console.log()输出的结果是:
1 | 应用实例,访问地址为http://:::3333。 |
1 | var server = app.listen(3333, function () { |
因此我们需要在函数参数中制指定主机名称(localhost或者127.0.0.1):
1 | var server = app.listen(3333,'localhost',function () {//(3.2.3) |
4. express 的更多应用
4.1 什么是 express 中间件
中间件(MiddleWare)可以理解为一个对用户请求(request)进行过滤和预处理的东西,就像一张滤网,一般不会直接对客户端进行响应,而是将处理之后的结果传递下去。它是一个过滤器,可以拦截任何请求,可以对请求的request和response做相关处理。
引用中间件最简单的方法就是使用app.use()啦,下面是一个最简单的例子。当然了,中间件除了引用已有的,还可以自定义(需要再写一篇笔记来专门讲讲中间件了),引用及自定义的详细使用方法见官方文档。
1 | 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)
使用GET方法
.html(GET)
1 | <form action="http://127.0.0.1:3333/insert_get" method="GET"> |
.js(响应insert_get,使用req.query
访问字段)
1 | app.get('/insert_get', function(req, res) { |
使用POST方法
.html(POST)
1 | <form action="http://127.0.0.1:3333/insert_post" method="POST"> |
.js(响应insert_post,使用req.body
访问字段)
1 | app.post('/insert_post', function(req, res) { |
4.3 静态文件(express.static)
express 提供了内置的中间件express.static
来设置静态文件如:图片, CSS,JavaScript 等。
可以使用express.static中间件来设置静态文件路径。例如,想将写好的静态网页、CSS文件、js文件、图片、文档(放在G:/MyWebs/中)提供给大家访问,那么可以这么写:
1 | app.use(express.static('public')); |
若要将脚本文件所在的文件夹(当前目录)作为静态文件,可以这么写:
1 | 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
:获得「请求主体」/ Cookiesreq.fresh / req.stale
:判断请求是否还「新鲜」req.hostname / req.ip
:获取主机名和IP地址req.originalUrl
:获取原始请求URLreq.params
:获取路由的parametersreq.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])
:设置Cookieopition: domain / expires / httpOnly / maxAge / path / secure / signed
res.clearCookie()
:清除Cookieres.download()
:传送指定路径的文件res.get()
:返回指定的HTTP头res.json()
:传送JSON响应res.jsonp()
:传送JSONP响应res.location()
:只设置响应的Location HTTP头,不设置状态码或者close responseres.redirect()
:设置响应的Location HTTP头,并且设置状态码302res.render(view,[locals],callback)
:渲染一个view,同时向callback传递渲染后的字符串,如果在渲染过程中有错误发生next(err)将会被自动调用。callback将会被传入一个可能发生的错误以及渲染后的页面,这样就不会自动输出了。res.send()
:传送HTTP响应res.sendFile(path [,options] [,fn])
:传送指定路径的文件 - 会自动根据文件extension设定Content-Typeres.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()方法的比较
1 | res.type('html'); // 直接使用write时,仍需要指定类型,不然会是乱码 |
1 | res.send('<h1>啊哈!</h1>'); // send()方法会自动解析数据类型并予以发送 |