11、HTTP 协议
11-1、协议的定义
HTTP
是一种能够获取如 HTML 这样的网络资源的 protocol(通讯协议)。它是在 Web 上进行数据交换的基础,是一种 client-server 协议,也就是说,请求通常是由像浏览器这样的接受方发起的。一个完整的Web文档通常是由不同的子文档拼接而成的,像是文本、布局描述、图片、视频、脚本等等。(来源:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Overview)。
无论是客户端请求还是服务端响应,本质就是在交换数据。因为交换的数据的多样性,为了能够让双方正确的理解和处理交换的数据,就需要给这些数据定义一些交换的方式和格式,这就是 HTTP
协议的基本作用。
一些参考资料:
11-2、报文
报文 可以理解为交换的数据,在 HTTP
协议中规定了交换的格式,报文分为:
- 请求报文
- 响应报文
请求报文
响应报文
HTTP Headers
HTTP Headers
允许客户端和服务端在请求和响应的时候携带一些附加信息,客户端会根据不同的头信息字段和对应内容做出不同的操作。根据不同的上下文环境,HTTP Headers
可分为:
- General Headers : 普通头,同时适用于请求和响应消息,但与最终消息主体中传输的数据无关的消息头。
- Request Headers : 请求头,包含更多有关要获取的资源或客户端本身信息的消息头。
- Response Headers : 包含有关响应的补充信息,如其位置或服务器本身(名称和版本等)的消息头。
- Entity Headers : 包含有关实体主体的更多信息,比如主体长(Content-Length)度或其MIME类型。
12、不同类型资源的处理
无论是请求还是响应,都会涉及到各种不同类型的资源要进行处理。为了能够让接收方(注:请求的接收方是服务端、响应的接收方是客户端)知道当前接收到内容类型,以便针对该类型进行对应的处理,所以需要在发送正文数据的同时需要同时告知该正文内容的类型格式。
12-1、使用 content-type 响应头返回资源类型
Content-Type
用于说明请求或相应中正文内容的 MIME
类型。
12-2、MIME 的作用与使用
MIME(Multipurpose Internet Mail Extensions)
: 是一种用来表示 文档、文字或字节流的性质和格式的标准。
结构
MIME
的通用结构为 : type/subtype
,type
为大类,subtype
为小类,如:text/html
、text/css
、image/png
等。
参考:https://www.iana.org/assignments/media-types/media-types.xhtml
12-3、设置响应头
使用 http
模块提供的 setHeader
方法就可以向响应中设置对应的头信息。
javascript
// #C12-3-1
...
server.on('request', (req, res) => {
res.setHeader('Content-Type', 'text/html;charset="utf-8"');
});
...
13、动态资源
许多时候,服务端还可能会提供一些动态处理的资源,比如会根据当前时间,当前客户端访问用户的身份权限等条件进行一些逻辑处理后返回对应的资源,这个时候,针对同一个 URL 的请求可能会产生不一样的结果,这种资源我们可以成为:动态资源
。
13-1、使用 URL 路由表优化动态资源处理
因为动态资源的特性,资源并不能像静态资源一样简单的存储在一个外部文件中,然后使用一个 URL 与之关联。所以,针对这种资源,通常情况下,我们需要对不同的 URL 进行不同的处理。
- 把不同的 URL 对应的一些逻辑分别保存(通常是函数)。
- 准备一个路由表(可以使用对象格式进行存储,key是 URL,value是对应的函数)。
- 根据当前访问的 URL ,找到路由表中的对应的函数并执行。
javascript
// #C13-1-1
...
// 路由表
const routesMap = new Map();
routesMap.set('/', async (req, res) => {
res.setHeader('Content-Type', 'text/html;charset="utf-8"');
res.end('首页');
});
routesMap.set('/list', async (req, res) => {
res.setHeader('Content-Type', 'text/html;charset="utf-8"');
res.end('列表');
});
server.on('request', async (req, res) => {
...
const urlObj = new URL(req.url);
...
const pathname = urlObj.pathname;
// 根据当前的 pathname 指定 routeMap 中对应的函数
let routeHandler = routesMap.get(pathname);
if (routeHandler) {
await routeHandler(req, res);
}
});
...
13-2、URL 重定向
我们通过一个 重定向
的需求应用场景来加深一下关于协议(如 http
头)等相关知识的理解
需求 : 访问不存在的 URL ,重新定向到 / 路由。
13-2-1、HTTP 状态码
我们通常在访问出现一些问题的时候返回一个友好的页面(或数据)给客户端,但是客户端仅仅从页面(数据)本身是无法知道这个页面(或数据)是这次请求本身应该对应的数据,还是我们给他的一个错误提示,这个时候就可以在响应的同时返回一个称为:状态码
的内容给客户端,它由一组具有特定含义的数字组成:
- 信息响应(
100
--199
) - 成功响应(
200
--299
) - 重定向(
300
--399
) - 客户端错误(
400
--499
) - 服务器错误 (
500
--599
)
注:虽然服务端可以随意返回指定的状态码,但是我们应当按照实际的情况返回标准中定义好的状态码。
javascript
// #C13-2-1-1
...
// 路由表
...
server.on('request', async (req, res) => {
...
const urlObj = new URL(req.url);
...
const pathname = urlObj.pathname;
// 根据当前的 pathname 指定 routeMap 中对应的函数
let routeHandler = routesMap.get(pathname);
if (routeHandler) {
await routeHandler(req, res);
} else {
// 告知客户端你应该重新发送请求,新的请求地址在 Location 头中。
res.statusCode = 302;
res.setHeader('Location', '/');
res.end();
}
});
...
13-3、404 Not Found
404
也是我们经常听到的一个词,它其实就是状态码中的一种
404 : 请求失败,请求所希望得到的资源未被在服务器上发现。
javascript
// #C13-3-1
...
// 路由表
...
server.on('request', async (req, res) => {
...
const urlObj = new URL(req.url);
...
const pathname = urlObj.pathname;
// 根据当前的 pathname 指定 routeMap 中对应的函数
let routeHandler = routesMap.get(pathname);
if (routeHandler) {
await routeHandler(req, res);
} else {
// 返回一个404的状态码与提示页面
res.statusCode = 404;
res.end('<h1>页面丢失了</h1>');
}
});
...
14、后端渲染
14-1、数据与视图
除了一些静态文件数据以外,服务端还会通过诸如:数据库
、程序运算
等各种方式产生数据,同时我们又需要把这些数据通过页面(HTML,CSS)的方式进行组织(也就是试图)返回给客户端。
javascript
// #C14-1-1
...
let users = [
{id: 1, username: '大海'},
{id: 2, username: '子鼠'},
{id: 3, username: '小蕊'},
];
// 路由表
const routesMap = new Map();
routesMap.set('/', async (req, res) => {
res.setHeader('Content-Type', 'text/html;charset="utf-8"');
res.end(`
<ul>
${users.map(u => {
return `<li>${u.username}</li>`
}).join('')}
</ul>
`);
});
...
server.on('request', async (req, res) => {
...
});
...
14-2、数据与视图分离
如果视图内容直接写在后端程序中,后端程序不好维护,且前端不好配合。
我们可以把前端相关的内容提取到外部文件中,在利用 Node.js
的 fs
模块进行文件读取,并进行一些简单的替换。
javascript
// #C14-2-1
...
const fs = require('fs');
// 数据
...
// 路由表
const routesMap = new Map();
routesMap.set('/', async (req, res) => {
res.setHeader('Content-Type', 'text/html;charset="utf-8"');
let userListHtml = users.map(u => {
return `<li>${u.username}</li>`
}).join('');
res.end( fs.readFileSync('./template/index.html').toString().replace(/${users}/gi, userListHtml) );
});
...
server.on('request', async (req, res) => {
...
});
...
./template/index.html
html
// #C14-2-2
...
<ul>${users}</ul>
...
模板 : 未被处理过的原始文件(如这里的:./template/index.html
)。
视图 : 通过模板与数据和逻辑的处理生成的最终内容。
14-3、使用模板引擎优化渲染
以上的模板数据解析替换方式太简单,复杂一些的视图逻辑就不太方便了。这个时候我们就可以使用一些 模板引擎(功能更加强大的模板解析替换库)
来完成解析工作。
14-3-1、基于 JavaScript
的常用模板引擎
- ejs
- artTemplate
- Nunjucks