13-NodeJS核心API-http模块

1.什么是HTTP模块

我们知道传统的HTPP服务器会由Aphche、Nginx、IIS之类的软件来担任,但是nodejs并不需要,nodejs提供了http模块,自身就可以用来构建服务器,而且http模块是由C++实现的,性能可靠。大部分的node使用者,都是用node来做Web API的,而HTTP模块是提供Web API的基础。为了支持所有的HTTP应用,node中的HTTTP模块提供的API是偏向底层化的。利用HTTP模块,我们可以简单快速搭建一个Web Server。

2.搭建web服务器

node提供了http这个核心模块(不用安装哦,直接require就可以了),用于创建http server服务,使用下面代码,轻松在本机的3000端口创建一个http服务器
下面我们来搭建一个简易的http服务器

let http = require("http");

http.createServer(function(req,res){
    res.writeHead(200,{
        "content-type":"text/plain"
    });
    res.write("NodeJS学习之旅");
    res.end();
}).listen(3000);

打开浏览器,输入localhost:3000我们就可以看到屏幕上的"NodeJS学习之旅"了,这表明这个最简单的nodejs服务器已经搭建成功了。
而上面的createServer方法中的参数函数中的两个参数req和res则是分别代表了请求对象和响应对象。
writeHead方法的第一个参数表示HTTP的响应状态(200)表示一切正常;第二个参数是“Content-Type”,表示我响应给客户端的内容类型。
然再后我们调用了write方法,写入我们需要传递给客户端的内容。最后一步我们调用了end方法,表示此次请求已处理完成, end方法中也可以返回数据。
.listen(port)
此函数有两个参数,第一个参数表示我们需要监听的端口,第二个参数是回调函数(其实是listening事件),当监听开启后立刻触发。

上面的实例代码使用的createServer方法返回了一个http.Server对象,这其实是一个创建http服务的捷径,如果我们用以下代码来实现的话,也将一样可行

let http = require("http");

// 1.创建一个服务器实例对象
let sever = new http.Server();
// 2.注册请求监听
sever.on("request", function (req, res) {
    // writeHead方法的作用: 告诉浏览器返回的数据是什么类型的, 返回的数据需要用什么字符集来解析
    res.writeHead(200, {
        "Content-Type": "text/plain; charset=utf-8"
    });
    // end方法的作用: 结束本次请求, 并且返回数据
    res.end("NodeJS学习之旅");
});
// 指定监听的端口
sever.listen(3000);

以上代码是通过直接创建一个http.Server对象,然后为其添加request事件监听,其实也就说createServer方法其实本质上也是为http.Server对象添加了一个request事件监听。
createServer方法中的参数函数中的两个参数req和res则是分别代表了请求对象和响应对象。其中req是http.IncomingMessage的实例,res是http.ServerResponse的实例。
http.IncomingMessage
http.IncomingMessage是HTTP请求的信息,是后端开发者最关注的内容,一般由http.Server的request事件发送,并作为第一个参数传递,包含三个事件
data:当请求体数据到来时,该事件被触发,该事件提供一个参数chunk,表示接受的数据,如果该事件没有被监听,则请求体会被抛弃,该事件可能会被调用多次(这与nodejs是异步的有关系)
end:当请求体数据传输完毕时,该事件会被触发,此后不会再有数据
close:用户当前请求结束时,该事件被触发,不同于end,如果用户强制终止了传输,也是用close
http.ServerResponse
http.ServerResponse是返回给客户端的信息,决定了用户最终看到的内容,一般也由http.Server的request事件发送,并作为第二个参数传递,它有三个重要的成员函数,用于返回响应头、响应内容以及结束请求
res.writeHead(statusCode,[heasers]):向请求的客户端发送响应头,该函数在一个请求中最多调用一次,如果不调用,则会自动生成一个响应头
res.write(data,[encoding]):想请求的客户端发送相应内容,data是一个buffer或者字符串,如果data是字符串,则需要制定编码方式,默认为utf-8,在res.end调用之前可以多次调用
res.end([data],[encoding]):结束响应,告知客户端所有发送已经结束,当所有要返回的内容发送完毕时,该函数必需被调用一次,两个可选参数与res.write()相同。如果不调用这个函数,客户端将用于处于等待状态。

3.http路径分发

路径分发也称之为路由, 就是根据不同的请求路径返回不同的数据

如何根据不同的请求路径返回不同的数据?

通过请求监听方法中的request对象, 我们可以获取到当前请求的路径
通过判断请求路径的地址就可以实现不同的请求路径返回不同的数据

let http = require("http");

// 1.创建一个服务器实例对象
let sever = http.createServer();
// 2.注册请求监听
sever.on("request", function (req, res) {
    res.writeHead(200, {
        "Content-Type": "text/plain; charset=utf-8"
    });
    // startsWith方法在这里的作用是判断url是否是以/index开头
    if (req.url.startsWith("/index")){
        res.end("首页");
    } else if (req.url.startsWith("/login")){
        res.end("登录");
    } else {
        res.end("没有数据");
    }
});
// 3.指定监听的端口
sever.listen(3000);

打开浏览器,输入localhost:3000的后面加上"/index"或者"/login"我们就能获取到不同的数据
上面的代码中, 因为req对象是http.IncomingMessage 类的实例, 所以它可以使用这个类中方法.上面代码中url方法就是这个类的方法, url方法的作用是可以获取到用户请求的路径
res对象其实是http.ServerResponse类的实例, 上面的end方法其实是这个类的方法, end方法的作用是结束本次请求, 并且返回数据
end方法和write方法都可以返回数据, 那么二者有什么不同呢?
如果通过end方法来返回数据, 那么只会返回一次
如果通过end方法来返回数据, 那么可以返回多次, 但是write方法不具有结束本次请求的功能, 所以还需要手动调用end方法来结束本次请求

// 这里只会返回"首页1"
res.end("首页1");
res.end("首页2");

// 这里会返回"首页1"和"首页2", 但是浏览器会一直停留在请求数据的状态
res.write("首页1");
res.write("首页2");
// 还需要通过end方法结束请求
res.end();

4.响应完整页面

如何通过地址栏的路径改变响应不同的页面, 可以在拿到用户请求的路径后利用fs模块将对应的网页返回
示例:
在这个代码文件同级文件夹下的www文件夹下面有index.html和login.html两个文件, 通过浏览器地址栏localhost:3000后面的路径跳转到对应的页面

const http = require("http");
const path = require("path");
const fs = require("fs");

let sever = http.createServer();
sever.on("request", function (req, res) {
    let filePath = path.join(__dirname, "www", req.url);
    fs.readFile(filePath, "utf8", function (err, data) {
        if (err) res.end("Sever Error");
        res.end(data);
    });
});
sever.listen(3000);

5.响应静态资源

在给浏览器返回数据的时候, 如果没有指定响应头的信息, 如果没有设置返回数据的类型, 那么浏览器不一定能正确的解析, 所以无论返回什么类型的静态资源都需要添加对应的响应头信息, 需要使用 MIME 来确定类型。

什么是MIME

MIME 是一种多用途 Internet 邮件扩展(MIME)类型是用一种标准化的方式来表示文档的 "性质" 和 "格式"。 简单说, 浏览器通过 MIME 类型来确定如何处理文档. 因此在响应对象的头部设置正确 MIME 类型是非常重要的.如果配置不正确,浏览器可能会曲解文件内容,网站将无法正常工作,并且下载的文件也会被错误处理。

MIME 的组成结构非常简单: 由类型与子类型两个字符串中间用 / 分隔而组成, 其中没有空格. MIME 类型对大小写不敏感,但是传统写法都是小写.

例如:

  • text/plain : 是文本文件默认值。意思是 未知的文本文件 ,浏览器认为是可以直接展示的.
  • text/html : 是所有的HTML内容都应该使用这种类型.
  • image/png : 是 PNG 格式图片的 MIME 类型.

在服务器中, 我们通过设置 Content-Type 这个响应头部的值, 来指示响应回去的资源的 MIME 类型. 在 Node.js 中, 可以很方便的用响应对象的 writeHead 方法来设置响应状态码和响应头部.

MIME 有两种默认类型:

  • text/plain 表示文本文件的默认值。一个文本文件应当是人类可读的,并且不包含二进制数据。
  • application/octet-stream 表示所有其他情况的默认值。一种未知的文件类型应当使用此类型。

常见 MIME 类型列表

如何使用MIME

首先我们需要获取到准备响应给客户端的文件的 后缀名.
要做到这一步我们可以通过req.url拿到用户输入的路径, 然后通过路径模块的.exname方法获取后缀名

let filePath = path.join(__dirname, "www", req.url);
let extName = path.extname(filePath);

获取了文件后缀之后, 我们需要查找其对应的 MIME 类型了. 这一步可以很轻松的使用第三方模块 MIME 来实现. 你可以自行去 NPM 上去查阅它的使用文档.
但是我这里直接有mime.json的文件也就没有去下载模块了
需要这个文件的朋友可以点击下载, 链接: https://pan.baidu.com/s/17yDEy2pb_hrdXJZSWYCwfw 提取码: fkyq

最重要的东西 MIME 类型我们得到后. 接下来只要在响应对象的 writeHead 方法里设置好 Content-Type 就行了.

const http = require("http");
const path = require("path");
const fs = require("fs");
const mime = require("./mime");

let sever = http.createServer();
sever.on("request", function (req, res) {
    readFile(req, res);
});
sever.listen(3000);

function readFile(req, res) {
    let filePath = path.join(__dirname, "www", req.url);
    let extName = path.extname(filePath);
    let type = mime[extName];
    if (type.startsWith("text")){
        type += "; charset=utf-8";
    } 
    res.writeHead(200, {
        "Content-Type" : type
    });
    /*
    注意: 
    1.加载文本外的资源, 不能写"utf8"
    2.如果服务器在响应数据的时候没有指定响应头, 那么在有的浏览器可能无法响应
    * */
    // fs.readFile(filePath, " utf8", function (err, data) {
    fs.readFile(filePath, function (err, data) {
        if (err) res.end("Sever Error");
        res.end(data);
    });
}

重构代码

现在来看看这个代码, 是不是开始感觉有点乱糟糟的. 可以发现, 整个静态文件服务器的代码就是在做一件事: 响应回客户端想要的静态文件. 这段代码职责单一, 且复用频率很高. 那么我们有理由将其封装成一个模块.
具体的过程我就不赘述了. 以下是我的模块代码:

const path = require("path");
const fs = require("fs");
const mime = require("./mime");

function readFile(req, res, rootPath) {
    let filePath = path.join(rootPath, req.url);
    let extName = path.extname(filePath);
    let type = mime[extName];
    if (type.startsWith("text")){
        type += "; charset=utf-8";
    }
    res.writeHead(200, {
        "Content-Type" : type
    });
    fs.readFile(filePath, function (err, data) {
        if (err) res.end("Sever Error");
        res.end(data);
    });
}
exports.StaticSever = readFile;

封装好了模块之后, 我们就可以删去服务器代码里那段读取文件的代码了, 直接引用模块就行了. 以下是我修改后的代码:

const http = require("http");
const path = require("path");
let ss = require("./15-StaticSever");

let sever = http.createServer();
sever.on("request", function (req, res) {
    let rootPath = path.join(__dirname, "www");
    ss.StaticSever(req, res, rootPath);
});
sever.listen(3000);

6.Get参数处理

由于GET请求直接被嵌入在路径中,URL完整的请求路径,包括了?后面的部分,因此你可以手动解析后面的内容作为GET的参数,Nodejs的url模块中的parse函数提供了这个功能。

url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
将一个URL字符串转换成对象并返回。
urlString 要解析的url地址
parseQueryString 解析出来的查询字符串还是查询对象,true是对象 false是字符串
例如:http://foo/bar?a=123, true的话 query: {a: '123'}, false的话 query: 'a=123' 默认是false
slashesDenoteHost 是否要解析出来host
例如://foo/bar 会被解析为{host: 'foo', pathname: '/bar},否则{pathname: '//foo/bar'}.默认是false

const http = require('http');
const url = require('url');

let str = "http://root:123456@www.baidu.com:80/index.html?name=abc&age=34#search";
let obj = url.parse(str, true);

http.createServer((req, res) => {
    let obj = url.parse(req.url, true);
    res.end(obj.query.name + "---" + obj.query.age);
}).listen(3000);

url模块中还有一个format方法, 作用是将对象解析为url地址
url.format(urlObject)

url.format({
  protocol: 'https',
  hostname: 'example.com',
  pathname: '/some/path',
  query: {
    page: 1,
    format: 'json'
  }
});

// => 'https://example.com/some/path?page=1&format=json'

7.POST参数处理

用POST方式提交的数据会附带在请求正文里面,所以我们需要获取到附带在request正文里的信息
用form表单提交数据

<form action="http://127.0.0.1:3000/index.html" method="post">
    <input type="text" name="userName">
    <input type="text" name="password">
    <input type="submit" value="提交">
</form>

如何拿到POST请求传递过来的参数--使用querystring模块
querystring.parse(str[, sep[, eq[, options]]])
将参数转换为对象
str 欲转换的字符串
sep 设置分隔符,默认为 ‘&'
eq 设置赋值符,默认为 ‘='
[options] maxKeys 可接受字符串的最大长度,默认为1000

let http = require("http");
let queryString = require("querystring");

let sever = http.createServer();
sever.on("request", function (req, res) {
    // 定义变量保存传递过来的参数
    let params = "";
    // 注意 在NodeJS中 ,POST请求的参数我们不能一次性拿到, 必须分批获取
    req.on("data", function (chunk) {
        params += chunk;
    });
    req.on("end", function () {
        let obj = queryString.parse(params);
        res.end(obj.userName + "---" + obj.password);
    });
});
sever.listen(3000);

与get请求不同的是,服务端接收post请求参数不是一次就可以获取的,通常需要多次
post请求参数不能使用url模块解析,因为他不是一个url,而是一个请求体对象

querystring模块中还有一个stringify方法, 作用是将对象转换为参数
querystring.stringify(obj[, sep[, eq[, options]]])
将对象转换为参数
obj 欲转换的对象
sep 设置分隔符,默认为 ‘&'
eq 设置赋值符,默认为 ‘='

querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
// 返回 'foo=bar&baz=qux&baz=quux&corge='

8.在服务端如何区分用户发送的是GET请求和POST请求?

通过HTTP模块http.IncomingMessage 类的.method属性

const http = require("http");

let server = http.createServer();
server.on("request", function (req, res) {
    if (req.url !== "/favicon.ico"){
        if (req.method.toLowerCase() === "get"){
            console.log("GET请求");
        } else if (req.method.toLowerCase() === "post"){
            console.log("POST请求");
        }
    } 
    res.end();
});
server.listen(3000);

上面代码中req.url !== "/favicon.ico"是为了过滤掉favicon请求
在第一次request请求的时候,客户端会发送一个隐式的请求给服务器,这个请求就是为了获取到网页的图标(就是每个网页打开后Title旁边的那个小图标),所以,当我们提交表单数据的时候,实际是触发了两次请求。

9.动态网站

编写一个简单的动态网站, 实现用户在地址栏输入127.0.0.1:3000/index.html后进入主页, 然后再输入框输入姓名后跳转到对应姓名的详情页面
index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="./info.html" method="post">
    <input type="text" name="userName">
    <input type="submit" value="查询">
</form>
</body>
</html>

info.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li>姓名: !!!name!!!</li>
    <li>性别: !!!gender!!!</li>
    <li>年龄: !!!age!!!</li>
</ul>
</body>
</html>

dynamic.js

// 1.导入需要的模块
const http = require("http");
const path = require("path");
const url = require("url");
const fs = require("fs");
const querystring = require("querystring");

// 4.22创建对象存储信息
let persons = {
    "lisi": {
        name: "lisi",
        gender: "male",
        age: "33"
    },
    "zhangsan": {
        name: "zhangsan",
        gender: "female",
        age: "20"
    }
};

let server = http.createServer();
// 2.创建服务器
server.on("request", function (req, res) {
    // 3.处理get请求
    if (req.url.startsWith("/index") && req.method.toLowerCase() === "get"){
        let obj = url.parse(req.url);
        let filePath = path.join(__dirname, obj.pathname);
        fs.readFile(filePath, "utf8", (err, data) => {
            if (err){
                res.writeHead(404, {
                    "Content-Type": "text/plain; charset=utf-8"
                });
                res.end("Page Not Found");
            }
            res.writeHead(200, {
                "Content-Type": "text/html; charset=utf-8"
            });
            res.end(data);
        });
    } 
    // 4.处理post请求
    else if (req.url.startsWith("/info") && req.method.toLowerCase() === "post"){
        // 4.1获取用户请求的数据
        let params = "";
        req.on("data", function (chunk) {
            params += chunk;
        });
        // 4.2获取完成
        req.on("end", function () {
            // 4.21将数据转为对象
            let obj = querystring.parse(params);
            // 4.23从信息对象中拿到数据
            let per = persons[obj.userName];
            // 4.24拼接路径并读取文件  
            let filePath = path.join(__dirname, req.url);
            fs.readFile(filePath, "utf8", (err, data) => {
                if (err){
                    res.writeHead(404, {
                        "Content-Type": "text/plain; charset=utf-8"
                    });
                    res.end("Page Not Found");
                }
                res.writeHead(200, {
                    "Content-Type": "text/html; charset=utf-8"
                });
                data = data.replace("!!!name!!!", per.name);
                data = data.replace("!!!gender!!!", per.gender);
                data = data.replace("!!!age!!!", per.age);
                res.end(data);
            });
        });
    }
});
server.listen(3000);

效果


我们可以发现上面的代码看起来还是比较杂乱的, 我们还可以使用 art-template模板引擎优化代码, 下面来看看具体步骤
1.在当前文件的目录下输入指令npm init -y初始化包, 然后我们就可以看到一个package.jaon的文件。
2.根据官方提供的指令npm install art-template --save安装包
3.改造info.html文件, 将以前占位的符号都改为模板的形式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <!--<li>姓名: !!!name!!!</li>
    <li>性别: !!!gender!!!</li>
    <li>年龄: !!!age!!!</li>-->

    <li>姓名: <%=name%></li>
    <li>性别: <%=gender%></li>
    <li>年龄: <%=age%></li>
</ul>
</body>
</html>

4.修改dynamic.js文件
4.1引入模板, 因为这里是第三方模块, 所以引入的时候不需要加绝对路径
4.2直接舍弃掉fs.readFile方法, 改用art-template官方样式代码

// 1.导入需要的模块
const http = require("http");
const path = require("path");
const url = require("url");
const fs = require("fs");
const querystring = require("querystring");
let template = require("art-template");

// 4.22创建对象存储信息
let persons = {
    "lisi": {
        name: "lisi",
        gender: "male",
        age: "33"
    },
    "zhangsan": {
        name: "zhangsan",
        gender: "female",
        age: "20"
    }
};

let server = http.createServer();
// 2.创建服务器
server.on("request", function (req, res) {
    // 3.处理get请求
    if (req.url.startsWith("/index") && req.method.toLowerCase() === "get"){
        let obj = url.parse(req.url);
        let filePath = path.join(__dirname, obj.pathname);
        fs.readFile(filePath, "utf8", (err, data) => {
            if (err){
                res.writeHead(404, {
                    "Content-Type": "text/plain; charset=utf-8"
                });
                res.end("Page Not Found");
            }
            res.writeHead(200, {
                "Content-Type": "text/html; charset=utf-8"
            });
            res.end(data);
        });
    } 
    // 4.处理post请求
    else if (req.url.startsWith("/info") && req.method.toLowerCase() === "post"){
        // 4.1获取用户请求的数据
        let params = "";
        req.on("data", function (chunk) {
            params += chunk;
        });
        // 4.2获取完成
        req.on("end", function () {
            // 4.21将数据转为对象
            let obj = querystring.parse(params);
            // 4.23从信息对象中拿到数据
            let per = persons[obj.userName];
            // 4.24拼接路径并读取文件  
            let filePath = path.join(__dirname, req.url);
            
            // 使用art-template
            let html = template(filePath, per);
            res.writeHead(200, {
                "Content-Type": "text/html; charset=utf-8"
            });
            res.end(html);
        });
    }
});
server.listen(3000);

这样我们才算真正完成简单的动态网站

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容