前言:在之前学习和使用HTTP协议的时候我已经写过几篇相关博客,但内容都写的非常繁杂,这篇博客主要基于HTTP协议在nodejs中的应用,将一些基础但又重要的内容以示例的方式展现出来,这里不会深入探讨HTTP的原理,重点介绍一些HTTP基础的实际应用。
上面是一个简单的B/S系统架构示意图,这里不是介绍系统架构,而是从这个架构中了解到HTTP协议作用的地方,如果你对HTTP协议有所了解就可以很容易想到,在上面的网络结构图中HTTP协议作用的地方就是客户端和服务器主机。
有时候因为浏览器的原理通常会说它是基于超文本传输协议HTTP、TCP/IP实现网络数据交互,但不是浏览器就只支持HTTP协议。在浏览器中资源管理对象CachedResource管理了多种不同类型的资源,在针对不同的资源会采取不同的网络协议,同时不论是什么类型的资源都基本遵守因特网资源定位标识符的通用URL语法。在统一的资源定位符语法下,为了实现多种资源的传输交互操作,就提供了不同的解决方案,这些解决方案就是具体的资源传输协议:HTTP、HTTPS、Mailto、FTP、RTSP/RTSPU、FILE、NEWS、TELNET、websocket。
来自大佬的HTPP协议解析系列文章:HTTP协议详解
我之前关于HTTP协议的博客: 计算机网络Web应用层与运输层(HTTP/TCP)
1.1什么是HTTP协议?
在计算机网络中,负责两台计算机的通信,其底层基于TCP/IP的可靠会话连接,负责处理超文本的传输,比如HTML、JPEG等文件的传输,它的全称是HyperText Transfer Protocol,即超文本传输协议。
1.2资源定位符URL:浏览器寻找信息时所需要的资源位置,通过URL人类和应用程序才能找到、使用并共享英特网上大量的数据资源。
HTTP规范将更通用的URI作为其资源标识符,所以,实际上HTTP应用程序处理的只是URI的URL子集。URL分为三部分:协议、主机地址、资源路径。协议告知客户端怎样访问资源,主机地址告知客户端资源位于何处,资源路径说明了请求是服务器上那个特定的本地资源。
URL使用的字符集是US-ASCII。
1.2HTTP报文:
报文流:HTTP报文是基于流的传输方式,从一端传输到另一端,也就是说客户端和服务器分别是流的源和目的地,它们各自维护着一个双工流,以实现从网络资源上读写数据,其实本质上这部分是由TCP/IP实现的。
关于报文流详细参考:TCP/IP协议相关博客、nodejs流相关博客
报文的组成部分:描述报文的起始行、报文首部、报文数据主体。
起始行与报文首部都是由行分隔的ASCII文本,所谓行分隔的ASCII文本就是指HTTP报文除了数据主体部分都是以ASCII字符集编码,每行都由两个字符组成的行终止序列作为结束。这两个组成行终止序列就是回车符+换行符,ASCII码分别是13、10,这个行终止序列可以写作CRLF,HTTP规范中说明应该使用CRLF来标识终止,但服务的具体实现应该接受单个换行符作为行的终止,因为有一些老版本的HTTP服务不会同时发送回车符又发送换行符。
起始行的内容包括:请求方法、请求资源定位符URL、协议版本、状态码、原因短语,但需要注意HTTP报文分来请求报文和响应报文,只有请求报文会全部包含这五个要素,响应报文中没有请求方法和请求资源定位符。
起始行五个要素简单解析:
请求方法:用于告知服务器要做些什么,常用的HTTP请求方法有:get、head、post、put、trace、options、delete。其中只有post、put两个请求方法的报文包含主体。HTTP规范是允许对请求方法进行扩展的。 请求资源定位符URL:用于告知服务器请求的资源的具体位置。 版本号:告诉应用程序应该使用那个版本的来解析报文。 状态码:用于告诉应用程序当前的发生了什么,比如服务器响应给客户端的状态码500则表示服务器出错了。 原因短语:原因短语和状态码是相对应的,比如响应200 OK则表示请求的数据响应成功了,数据都在报文主体中;响应401 Unauthorized则表示要输入用户名和密码等。
状态码分类:
报文首部:报文首部和方法共同作用,决定客户端和服务器能做什么事情。报文首部分为五大类:通用首部、请求首部、响应首部、实体首部、扩展首部。
通用首部:客户端和服务器都能使用的首部。
//通用信息首部 Connection:允许客户端和服务器指定与请求/响应连接有关的选项。 Date:提供日期和事件标志,说明报文什么时间创建的。 MIME-Version:给出发送端使用的MIME版本。 Trailer:如果报文采用了分块传输编码方式,就可以用这个首部列出位于报文拖挂部分的首部集合。 Transfer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式。 Update:给出了发送端可能想要“升级”使用的新版本或协议。 Via:显示了报文经过的中间节点(代理、网关) //通用报文缓存首部 Cache-Control:用于随报文传送缓存指示。 Pragma:另一种随报文传送指示的方式,但并不专用于缓存。
请求首部:请求报文特有的首部,它为服务器提供一些额外的信息,比如客户端希望接受什么类型的数据。
//请求的信息性首部 Client-IP:提供了运行客户端的机器的IP地址 From:提供了客户端用户的E-mail地址 Host:给出了接受请求的服务器的主机名和端口号 Referer:提供了包含当前请求URI的文档的URL UA-Color:提供了客户端显示器的显示颜色有关信息 UA-CPU:给出了客户端CPU的类型或制造商 UA-Disp:提供了客户端显示器能力有关的信息 UA-OS:给出了运行在客户端机器上的操作信息名称及版本 UA-Pixels:提供了客户端显示器的像素信息 User-Agent:将发起请求的应用程序名称告知服务器 //Accept首部 Accept:告诉服务器能够发送哪些媒体类型 Accept-Charset:告诉服务器能够发送哪些字符集 Accept-Encoding:告诉服务能够发送哪些编码方式 Accept-Language:告诉服务器能够发送哪些语言 TE:告诉服务器可以使用哪些扩展传输编码 //条件请求首部 Expect:允许客户端累出某请求所要求的服务器行为 If-Match:如果实体标记与当前文档实体标记相匹配,就获取这份文档 If-Modified-Since:除非在某个指定的日期之后被修改过,否则就限制这个请求 If-None-Match:如果提供的实体标记与当前文档标记不相符,就获取文档 If-Range:允许对文档的某个范围进行条件请求 If-Unmodified-Since:除非在某个指定日期之后资源没有被修改过,否则就限制这个请求 Range:如果服务器支持范围请求,就请求资源的指定范围 //安全请求首部 Authorization:包含了客户端提供给服务器,以便对其自身进行认证的数据 Cookie:客户端用它向服务器传送一个令牌(它并不是真正的安全首部,但确时隐含了安全功能) Cookie2:用来说明请求端支持的cookie版本 //代理请求首部 Max-Forward:在通往源端服务器的路径上,将请求转发给其他代理或网关的最大次数(与TRACE方法一同使用) Proxy-Authorization:与Authorization首部相同,但这个首部是在代理进行认证时使用的 Proxy-Connection:与Connection首部相同,但这个首部是在与代理建立连接时使用的
响应首部:响应报文特有的首部,为客户端提供信息。
//响应的信息首部 Age:响应持续时间(从最初创建开始) Public:服务器为其资源支持的请求方法列表 Retry-After:如果资源不可用的话,在此日期或时间重试 Server:服务器应用程序软件的名称和版本 Title:对HTML文档来说,就是HTML文档的源端给出的标题 Warning:比原因短语中更详细一些的警告报文 //协商首部 Accept-Ranges:对此资源来说,服务器可接受的范围类型 Vary:服务器查看的其他首部列表,可能会使响应发生变化;也就是说,这是一个首部列表,服务器会根据这些首部的内容挑选出最合适的资源版本发送给客户端 //安全响应首部 Proxy-Authenticate:来自代理的对客户端的质询列表 Set-Cookie:不是真正的安全首部,但隐含有安全功能;可以在客户端设置一个令牌,以便服务器对客户端进行标识 Set-Cookie2:与Set-Cookie类似,RFC 2965 Cookie定义 WWW-Authenticate:来自服务器对客户端的质询列表
实体首部:作用当前报文的实体部分,用于解析报文的实体部分。
//实体信息首部 Allow:列出了可以对此实体执行的请求方法 Location:告知客户端实体实际上位于何处;用于将接收端定向到资源(可能是新的)位置(URL)上去 //内容首部 Content-Base:解析主体中的相对URL时使用的基础URL Content-Encoding:对主体执行的任意编码方式 Content-Language:理解主体时最适宜使用的自然语言 Content-Length:主体的长度或尺寸 Content-Location:资源实际所处的位置 Content-MD5:主体的MD5校验和 Content-Range:在整个资源中此实体表示的字节范围 Content-Type:这个主体的对象类型 //实体缓存首部 ETag:与此实体相关的实体标记 Expires:实体不再有效,要从原始的源端再次获取此实体的日期和时间 Last-Modified:这个实体最后以此被修改的日期和时间
扩展首部:即非HTTP标准的首部,由开发者自行添加的首部,即使不知道这些首部含义,HTTP程序也要接收它们并对其转发。
1.3HTTP协议更多内容参考:
HTTP缓存相关内容:HTTP缓存机制、浏览器加载解析渲染网页原理、cookie机制、session
网络连接与数据传输相关内容:UDP、TCP/IP网络连接与数据传输
Ajax相关:AJAX原理解析与兼容方法封装
跨域相关:JSONP跨域、 计算机网络之iframe内联框架跨域、服务器代理跨域实现图片瀑布流
HTTPS安全协议:基于node搭建HTTPS安全服务
nodejs的url模块解析:URL+querystring模块
nodejs的http模块相关API解析:nodejs入门API之http模块(这篇博客的内容实际上就是将官方文档一些不容易理解的接口描述做了一些解析,但注意这种解析不保证完全符合接口原意,不过这篇博客对get、post请求方法做了详细的解析,以及重定向也做了一些解析,静态资源服务器搭建案例还是值得参考的)
2.1创建HTTP服务:
1 const http = require('http'); 2 const url = require('url'); 3 //创建http服务对象 4 const server = http.createServer((req, res)=>{ 5 let {pathname, query} = url.parse(req.url, true); //解析请求资源的路径,请求数据 6 let version = req.httpVersion; //获取当前请求报文的http版本信息 7 let method = req.method; //获取请求方法 8 let headers = req.headers; //获取请求首部 9 let bodyArr = null; //获取请求主体 10 console.log(method); 11 if(method === 'GET'){ 12 res.writeHead(200); 13 res.write(JSON.stringify(query)); //这里将get请求在url后面携带的请求参数相应回去,如果是要响应静态资源就是文件数据传入write 14 res.end(); 15 }else if(method === "POST"){ 16 req.on('data',(data)=>{ 17 bodyArr = bodyArr ? bodyArr.push(data) : [data]; 18 res.writeHead(200); 19 res.write(Buffer.concat(bodyArr)); 20 res.end(); 21 }); 22 } 23 24 }); 25 //启动服务监听对应的端口 26 server.listen(12306,()=>{ 27 console.log('server is start...'); 28 });
关于HTTP模块如果只是从API层面来解析,并不能完全理解它的内部执行逻辑,除非你将所有的API源码精读一遍,如果只是为了应用HTTP模块来搭建服务精读HTTP模块的源码的必要性不大,我能可以从逻辑架构层面来理解它的底层逻辑。
首先,基于http.createServer([options][, requestListener])创建HTTP服务实例其底层是基于net.server(),并将 requestListener绑定到这个实例的request事件上。通过request事件监听的回调任务如果被触发会获取两个参数:request,response,这两个参数分别是http.IncomingMessage、http.ServerResponse的实例。
在前面的TCP相关博客中我详细的介绍了,net.server()底层是基于一个双工流实现。这个双工流的两端分别就是requestListener接收的参数request、response。
request负责表达网络资源接收到的请求相关任务,response负责表达服务对网络请求的响应任务。
客户端发送请求到服务上时对双工流做的写入操作有系统的网络资源完成,request不具备对双工流的做写的能力,所以在官方文档中http.IncomingMessage的解析中有明确的描述其为一个可读流,request只对网络资源写入数据时的具体情况输出行为表达,比如触发一些事件,将数据写到request实例上的相关API上。
服务端的行为则由response来表达,它主要负责向流写入数据,并对写入数据及相关网络状态做事件监听,所以你会发现在response上能使用net.createConnection()的全部事件和方法。
由上面的解析解可以了解如果要理解HTTP的任务逻辑,最好是先了解tream模块,然后了解创建TCP服务的net.createConnection()相关应用,然后了解URL模块如何解析资源定位符和HTTP报文相关内容,就能非常熟练的掌握如何应用nodejs的HTTP服务。
2.2代理客户端跨域:
这个代理客户端跨域很多时候又被称为反向代理,所谓反向代理就是服务器代理客户去向真正的数据源服务器发送请求,然后将请求到的数据响应给客户。关于这个代理客户端跨域和反向代理这个模式的解析有很多,如果又不理解的自行查阅,这里就不多做解释了。
在实现代理客户端跨域实例之前,先来分析以下具体要解决哪些问题,首先我们需要在服务上有一个可以向数据源服务发送请求的代理服务,关于这个代理服务可以使用http上的http.request()来实例化一个http的客户端实例。然后实现这个代理服务与数据源服务之间的请求和响应交互,通过下面的示例来了解代理服务与数据源服务之间的get、post相关请求与相应实现。
基于node的http代理客户端与服务交互(get),http.get()本质上就是用来向服务发送一个没有正文的http请求,也就是说请求报文没有主体,详细参考http.get(options[, callback])官方文档:http://nodejs.cn/api/http.html#httpgetoptions-callback:
1 //服务端 2 const http = require('http'); 3 const url = require('url'); 4 const querystring = require('querystring'); 5 let server = http.createServer((req,res)=>{ 6 let {pathname, query} = url.parse(req.url); 7 let data = querystring.parse(query); 8 console.log(data); 9 res.writeHead(200); 10 res.end(JSON.stringify(data)); 11 }); 12 server.listen(12306,()=>{ 13 console.log('HTTP服务已启动:12306'); 14 }); 15 //代理客户端 16 const http = require('http'); 17 18 let req = http.get({ 19 host:'localhost', 20 port:12306, 21 path:'/?a=1&b=2' 22 },(res)=>{ 23 if (res.statusCode !== 200) { 24 return; 25 } 26 let data = null; 27 res.on('data',(chunk)=>{ 28 data = chunk.toString(); 29 }); 30 res.on('end',()=>{ 31 console.log(data); 32 }); 33 });
然后分别使用nodemon启动服务和代理客户测试,先启动服务端再启动客户端可以看到在客户端输出:
//服务端 [Object: null prototype] { a: '1', b: '2' } //代理客户端 {"a":"1","b":"2"}
基于nodejs的http代理客户端与服务交互(POST),详细参考http.request(options[, callback])官方文档:http://nodejs.cn/api/http.html#httprequestoptions-callback
1 //服务端 2 const http = require('http'); 3 const url = require('url'); 4 const querystring = require('querystring'); 5 const server = http.createServer((req, res)=>{ 6 let {pathname, query} = url.parse(req.url); 7 let arr = []; 8 req.on('data',(data)=>{ 9 arr.push(data); 10 }); 11 req.on('end',()=>{ 12 let obj = Buffer.concat(arr).toString(); 13 if(req.headers['content-type'] === "application/json"){ 14 let a = JSON.parse(obj); 15 console.log(a); 16 a.add = '博客园'; 17 res.end(JSON.stringify(a)); 18 }else if(req.headers['content-type'] === "application/x-www-form-urlencoded"){ 19 let ret = querystring.parse(obj); 20 console.log(ret); 21 res.end(JSON.stringify(ret)); 22 } 23 }); 24 }); 25 server.listen(12306,()=>{ 26 console.log('server is runing...'); 27 }); 28 //代理客户端 29 const http = require('http'); 30 let options = { 31 host:'localhost', 32 port:12306, 33 path:'/?a=1', 34 method:'POST', 35 headers:{ 36 'content-type':'application/json' 37 // 'content-type':'application/x-www-form-urlencoded' 38 } 39 }; 40 let req = http.request(options,(res)=>{ 41 let arr = []; 42 res.on('data',(data)=>{ 43 arr.push(data); 44 }); 45 res.on('end',()=>{ 46 console.log(Buffer.concat(arr).toString()); 47 }) 48 }); 49 req.end('{"name":"他乡踏雪"}'); 50 // req.end('a=15&b=2');
使用nodemon分别启动服务端和客户端打印以下结果:
//服务端 { name: '他乡踏雪' } //代理客户端 {"name":"他乡踏雪","add":"博客园"}
在POST测试代码中有两行注释代码,这两行代码分别用来替代它们的上一行代码测试表单的方式发送请求,其本省差异就是数据格式与之相匹配的HTTP首部设置,你可以分别测试他们来了解在http代理客户端上发送表单请求,以下是测试结果:
//服务端 [Object: null prototype] { a: '15', b: '2' } //代理客户端 {"a":"15","b":"2"}
事件一个简易的http代理客户服务:
代理客户端服务简单的来说就是在服务上接收到用户的请求时,创建一个HTTP请求向数据服务请求数据,等待数据服务响应回数据然后再响应给客户端,所以就是在前面的代理客户端上套上一个HTTP服务,代码实现参考下面的示例:
1 //有数据的http服务 2 const http = require('http'); 3 const server = http.createServer((req,res)=>{ 4 let arr = []; 5 req.on('data',(data)=>{ 6 arr.push(data); 7 }); 8 req.on('end',()=>{ 9 console.log(Buffer.concat(arr).toString()); 10 let ret = Buffer.concat(arr).toString(); 11 res.end('拿到了客户端的数据:' + ret); 12 }); 13 }); 14 server.listen(12306, ()=>{ 15 console.log('外部服务端启动了'); 16 }); 17 //负责处理客户端请求的服务(http代理服务) 18 const http = require('http'); 19 let options = { 20 host:'localhost', 21 port:12306, 22 path:'/', 23 method:'POST' 24 }; 25 let server = http.createServer((request, response)=>{ 26 let req = http.request(options, (res)=>{ 27 let arr = []; 28 res.on('data',(data)=>{ 29 arr.push(data); 30 }); 31 res.on('end',()=>{ 32 // console.log(Buffer.concat(arr).toString()); 33 let ret = Buffer.concat(arr).toString(); 34 response.setHeader('Content-type','text/html;charset=utf-8') 35 response.end(ret); 36 }); 37 }); 38 39 req.end('他乡踏雪'); 40 }); 41 server.listen(12305,()=>{ 42 console.log('本地的服务端启动了'); 43 });
然后就可以在浏览器上向代理服务发送请求测试: http://127.0.0.1:12305
3.1创建HTTP静态服务:
这部分不是为了创建一个完整功能的HTTP静态服务,只是简单的搭建一个HTTP静态服务来呈现它的基本逻辑,为后面搭建静态服务工具准备。
在这个静态服务中使用了一个mime包,这个包用来解析静态资源的mime类,为HTTP响应头Content-Type动态配置资源的mime类型。注意这个mime安装时好像需要管理员权限,但我不太确定是不是这个原因,只是我是用普通用户安装时报了4048错误,使用管理员权限安装解决了这个问题。如果对mime类型不太了解的可以参考这篇博客:MIME类型是什么?
静态服务的文件路径结构:
工作区间 --www ----index.html --node_modules --index.css --index.html --server.js --package.json
静态资源的一些测试代码,如果使用我的代码测试记得自己要安装mime工具包 npm install mime :
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <link rel="stylesheet" href="../../index.css"> 9 </head> 10 <body> 11 <h2>测试内容222</h2> 12 </body> 13 </html>www/index.html
1 body{ 2 background-color: lightblue; 3 }index.css
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <link rel="stylesheet" href="./index.css"> 9 </head> 10 <body> 11 <h2>测试内容111</h2> 12 </body> 13 </html>index.html
1 { 2 "name": "static-server", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1", 8 "start": "node server.js" 9 }, 10 "keywords": [], 11 "author": "", 12 "license": "ISC", 13 "dependencies": { 14 "mime": "^3.0.0" 15 } 16 }package.json
server.js具体代码:
1 const http = require('http'); 2 const url = require('url'); 3 const path = require('path'); 4 const fs = require('fs'); 5 const mime = require('mime'); //用来自动获取文件的mime类型 6 7 const server = http.createServer((req,res)=>{ 8 let {pathname, query} = url.parse(req.url); 9 pathname = decodeURIComponent(pathname); 10 let absPath = path.join(__dirname,pathname); 11 console.log(absPath); 12 fs.stat(absPath,(err, statObj)=>{ 13 if(err){ 14 res.statusCode = 404; 15 res.end('Not Found'); 16 return; 17 } 18 if(statObj.isFile()){ 19 fs.readFile(absPath,(err, data)=>{ 20 res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8'); 21 res.end(data); 22 }); 23 }else{ 24 fs.readFile(path.join(absPath, 'index.html'),(err,data)=>{ 25 res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8'); 26 res.end(data); 27 }); 28 } 29 }); 30 }); 31 server.listen(12306,()=>{ 32 console.log('server is start...'); 33 });
3.2搭建HTTP静态服务工具:
该工具可以通过命令行指令启动服务向web提供的浏览文件的静态页面,简单的说就是启动一个HTTP服务,通过这个服务可以在浏览器上浏览服务指定的相关文件。
项目使用的外部依赖模块:
commander:命令行工具基础模块(中文文档)
mime:HTTP首部文件mime类型工具
ejs:nodejs生成静态页面的模板工具
静态服务工具的工作区间文件路径结构:
工作区间 --bin ----www.js --node_modules --template.html --main.js --package.json
实现代码:
1 #! /usr/bin/env node 2 3 const {program} = require('commander'); //这是一个构建node命令行工具的基础模块 4 5 // console.log("执行了"); 6 // program.option('-p --port', 'set server port'); 7 8 //配置信息 9 let options = { 10 '-p --port <dir>':{ //端口 11 'description': 'init server port', 12 'example':'static-serve -p 3306' 13 }, 14 '-d --directory <dir>':{ //用于指定启动项目的路径。该指令暂时只能接收绝对路径参数,或者不传参默认为启动static-serve的当前地址 15 'description': 'init server directory', 16 'example':'static-serve -d cli' 17 }, 18 }; 19 //遍历配置信息--可用于格式化配置参数等其他遍历配置信息的操作 20 function traversalConfig(configs, cb){ 21 Object.entries(configs).forEach(([key, val])=>{ 22 cb(key, val); 23 }); 24 } 25 //格式化配置信息 26 traversalConfig(options,(cmd, val) =>{ 27 program.option(cmd, val.description); 28 29 }); 30 //--help命令的回调事件,遍历循环打印配置信息中的示例 31 program.on('--help',()=>{ 32 console.log('Example: '); 33 traversalConfig(options,(cmd, val)=>{ 34 console.log(val.example); 35 }); 36 }); 37 38 program.name('static-serve'); //设置工具名称 39 let version = require('../package.json').version; //获取工具版本号 40 program.version(version); //设置当前工具的版本号,设置后可以通过-V查看当前工具的版本号 41 42 let cmdConfig = program.parse(process.argv).opts(); 43 44 let Server = require('../main.js'); //导入工具的服务模块 45 new Server(cmdConfig).start(); //启动静态服务,启动服务时传入当前启动服务相关配置信息www.js
1 const http = require('http'); 2 const url = require('url'); 3 const path = require('path'); 4 const fs = require('fs').promises; //为了避免fs的异步回调出现“回调地狱”,这里使用了fs新版本中的promises类型来实现文件操作 5 const {createReadStream} = require('fs'); //文件可读流API 6 const mime = require('mime'); //用于自动获取HTTP的首部Content-type的mime类型 7 const ejs = require('ejs'); //用于实现HTTP页面的模板工具模块 8 const {promisify} = require('util'); //将回调模式转化成promise模式的內置工具 9 10 //配置默认信息 11 function mergeConfig(config){ 12 console.log(config); 13 return { 14 port:12355, 15 directory:process.cwd(), 16 ...config //如果在www.js中接收到了以命令的方式传入的port和directory,则会覆盖它们的默认参数,详细了解ES6的展开语法 17 } 18 } 19 //实现一个HTTP服务类 20 class Server{ 21 constructor(config){ 22 this.config = mergeConfig(config); //将命令中的配置信息缓存到服务实例的config上 23 } 24 start(){ //静态服务接口 25 let server = http.createServer(this.serveHandle.bind(this)); //实例化HTTP服务,将它的回调方法导入并将this指向Server的实例 26 server.listen(this.config.port, ()=>{ //启动服务 27 console.log('服务以启动...'); 28 }); 29 } 30 async serveHandle(req, res){ //HTTP服务实例的回调任务,this指向Server的实例 31 let {pathname} = url.parse(req.url); //获取请求的资源路径(url) 32 pathname = decodeURIComponent(pathname); //将请求路径中的GB2312编码转换成当前程序的编码,如默认的utf-8 33 let abspath = path.join(this.config.directory, pathname); //将请求路径拼接到指定的静态资源根目录后面,生成服务上真正的文件路径 34 try{ 35 let statObj = await fs.stat(abspath); //获取文件的属性对象 36 if(statObj.isFile()){ //判断文件路径是否是文件(也可能是文件夹) 37 this.fileHeandle(req, res, abspath); //如果是文件就调用Server上的文件响应工具,将文件响应给客户都安 38 }else{ //如果式文件夹,则调用相关的模板工具,将该路径下的文件信息生成为HTML响应回去 39 let dirs = await fs.readdir(abspath); //获取文件夹中所有文件/文件夹的名称列表数组 40 dirs = dirs.map((item)=>{ //通过文件名称和文件所属的路径拼接文件的path,然后使用一个包含文件路径和文件名称的对象替换数组中的文件名称元素 41 return { 42 path: path.join(pathname, item), 43 dirs: item 44 } 45 }); 46 let renderFile = promisify(ejs.renderFile); //将模板工具读取模板的异步方法转换成Promise模式 47 let parentpath = path.dirname(pathname); //获取当前文件所在的目录,用于作为模板中上一层的href的值 48 let ret = await renderFile(path.resolve(__dirname, 'template.html'), { //向模版中注入数据,生成真正的html文件 49 arr:dirs, 50 parent:pathname === '/' ? false : true, 51 parentpath:parentpath, 52 title:path.basename(abspath) 53 }); 54 res.end(ret); 55 } 56 }catch(err){ 57 this.errorHandle(req,res,err); 58 } 59 } 60 errorHandle(req, res, err){ 61 console.log(err); 62 res.statusCode = 404; 63 res.setHeader('Content-type', 'text/html;charset=utf-8'); 64 res.end('Not Found'); 65 } 66 fileHeandle(req, res, abspath){ 67 res.statusCode = 200; 68 res.setHeader('Content-type', mime.getType(abspath) + ';charset=utf-8'); 69 createReadStream(abspath).pipe(res); 70 } 71 } 72 module.exports = Server;main.js
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <style> 9 *{ 10 list-style: none; 11 } 12 </style> 13 </head> 14 <body> 15 <h3>indexOf <%=title%></h3> 16 <ul> 17 <%if(parent){%> 18 <li><a href="<%=parentpath%>">上一层</a></li> 19 <%}%> 20 21 <%for(let i = 0; i < arr.length; i++) {%> 22 <li><a href="<%=arr[i].path%>"><%=arr[i].dirs%></a></li> 23 <%}%> 24 </ul> 25 </body> 26 </html>template.html
1 { 2 "name": "lgerve", 3 "version": "1.0.0", 4 "description": "", 5 "main": "main.js", 6 "bin": { 7 "static-serve": "bin/www.js" 8 }, 9 "scripts": { 10 "test": "echo \"Error: no test specified\" && exit 1" 11 }, 12 "keywords": [], 13 "author": "", 14 "license": "ISC", 15 "dependencies": { 16 "commander": "^9.2.0", 17 "ejs": "^3.1.7", 18 "mime": "^3.0.0" 19 } 20 }package.json
实现代码后在工作区间根目录下将项目link到全局:
npm link
然后使用bin的注册的命令启动服务:
static-serve
如果不指定服务的端口号默认为12355,服务提供的浏览文件目录是当前项目的根目录下的所有文件,可以按照以下命令格式指定服务的端口号及服务提供的浏览文件目录:
static-serve -p <port> -d <path>
注意path需要提供绝对路径,测试截图: