前言: 关于 http server
应该有小伙伴了解或用过http-server
,http-server
是一个node环境下的命令行http服务器,这里是npm官网的链接 www.npmjs.com/package/htt… ,
可以从npm的官网查到其用法:
即npm安装后,在命令行输入指令http-server
直接开启服务器,在服务器启动的目录下,默认会找public静态资源目录,去访问默认的127.0.0.1:8080 可以访问到静态目录站点。
非常易用和方便,如果我们想改变端口,默认目录,或者主机名等等,可以在启动时在命令行直接配置
cmd: http-server -p 3001
那么启动时就会访问3001端口,其他配置可以参考npm官网了解,在这里就不赘述了。本篇主要想通过http-server的底层原理,实现一个简易的http-server包,实现后还可以发到npm上成为自己的作品哦,一起看看吧。
npm 注册
想要了解发布npm的同学可以在npm官网注册属于自己的npm账号,注意邮箱一定要验证哦,不然发布不了自己的包
开始项目前最好提前了解的一些内容
- nodejs环境
- fs模块,包括读取文件,读写流等
- async,promise
- http知识
- ejs模板引擎
一 项目搭建
package.json
首先要安装node环境,因为我们的项目是基于nodejs的http工具。 创建项目文件夹后用npm或yarn初始化都可以,创建项目的package.json文件,配置过程跟随包管理工具的提示即可,输入name的时候,可以使用你想要发布的包的名字;author输入你在npm官网注册的用户名,版本就默认是1.0.0就好,我的pachage.json配置如下
这里bin我们配置为 启动命令行自定义名:"bin/www.js",作为我们的命令行启动配置文件
main: index.js是我们主要逻辑脚本
author是作者名,这里使用npm官网的用户名保持一致即可
文件夹结构
建议大家这样配置
首先有bin文件夹,下放www.js,这里我们用于配置命令行工具,也是启动包的关键所在
node_modules为安装依赖后生成的,请忽略
public是我们想要让用户访问的静态资源目录,可以随意放一些文件夹和文件
src是我们的核心js和配置
src/template.html是展示的界面,因为我们要搭建的是一个静态资源站点,用户需要访问页面,可以点击目录或文件等操作。
二 核心代码
1. src/config.js
首先我们构建config.js,即默认配置项
module.exports = {
port: 8080, // 默认端口
host: 'localhost', // 默认主机名
dir: process.cwd() // 默认读取目录
}
我们导出默认的端口号,默认的主机名,和默认的文件目录,
其中 process.cwd()是读取进程当前的工作目录,你在哪启动,就读哪个目录
2. src/template.html
上面讲到了template.html用于展示目录,我们这里采用ejs模板引擎,服务器端渲染的方式展示。 这里有ejs介绍,可以简单了解写法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h2><%=name%></h2>
<%arr.forEach(item=>{%>
<li><a href="<%=item.href%>"><%=item.name%></a></li>
<%})%>
</body>
</html>
h2
标签中我们展示当前路径
li
为文件目录结构,可供点击进入文件或文件夹;可以看出,我们要给这个页面输出name和arr两个数据,在核心js中会详细讲如何渲染template.
3. src/index.js
这里是我们的主要逻辑脚本了,本js里会封装Server类,用于在www.js中启动服务器
简要架构:
我们引入的模块主要有:
- http http模块
- util util工具模块,主要使用promisify方法
- url url模块 可方便获取路径
- zlib 文件压缩模块,用于创建gzip或deflate的转化流
- fs 文件系统模块
- path 路径拼接
- querystring 路径处理
- ejs 模板引擎
- chalk 粉笔模块 可以给输出的命令行文本加颜色喔
- mime 获取文件类型
- debug debug模块
- config 我们自己写的配置文件
下面开始构建 Server 类,用于处理请求中的各种情况,返回不同的内容
① 构造Server
class Server {
constructor(command) {
this.config = {...config,...command} // config和命令行的内容展示
this.template = template;
}
}
② 主要的请求处理方法 ※
class Server {
...
async handleRequest(req, res) {
let { dir } = this.config; // 需要将请求的路径和dir拼接在一起
//如 http://localhost:8080/index.html
let { pathname } = url.parse(req.url);
// 如果独到的是网站小图标,就直接输出
if (pathname === '/favicon.ico') return res.end();
pathname = decodeURIComponent(pathname); // 对文件夹名称进行转码处理
// p是决定文件路径
let p = path.join(dir, pathname);
try {
// 判断当前路径是文件 还是文件夹
let statObj = await stat(p);
if (statObj.isDirectory()) {
// 读取当前访问的目录下的所有内容 readdir 数组 把数组渲染回页面
res.setHeader('Content-Type', 'text/html;charset=utf8')
let dirs = await readdir(p);
dirs = dirs.map(item=>({
name:item,
// 因为点击第二层时 需要带上第一层的路径,所有拼接上就ok了
href:path.join(pathname,item)
}))
// 渲染template.html中需要填充的内容,name是当前文件目录,arr为当前文件夹下的目录数组
let str = ejs.render(this.template, {
name: `Index of ${pathname}`,
arr: dirs
});
// 响应中返回填充内容
res.end(str);
} else {
// 如果不是文件夹,则直接输出文件内容
this.sendFile(req, res, statObj, p);
}
} catch (e) {
debug(e); // 发送错误
this.sendError(req, res);
}
}
...
}
③ 处理用户缓存
用于告知服务器是否本次缓存,如果是浏览器客户端已经缓存的文件,直接读取缓存即可,优化性能
class Server {
...
cache(req, res, statObj, p ) {
// 设置缓存头
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).getTime());
// 设置etag和上次最新修改时间
let eTag = statObj.ctime.getTime() + '-' + statObj.size;
let lastModified = statObj.ctime.getTime();
// 传给客户端
res.setHeader('Etag', eTag);
res.setHeader('Last-Modified', lastModified);
// 客户端把上次设置的带过来
let ifNoneMatch = req.headers['if-none-match'];
let ifModifiedSince = req.headers['if-modified-since'];
// 其中任意一个不生效缓存就不生效
if (eTag !== ifNoneMatch && lastModified !== ifModifiedSince) {
return false;
}
return true;
}
...
}
④ 是否压缩
返回压缩文件,优化访问速度
class Server {
...
// 是否压缩
gzip(req, res, statObj, p) {
// 判断请求头是否设置了接收编码
let encoding = req.headers['accept-encoding'];
// 如果有则判断是否有gzip或者deflate
if (encoding) {
// gzip
if (encoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
}
// deflate
if (encoding.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
}
return false;
}
else {
return false;
}
}
...
}
⑤ 判断是否有范围请求
判断是否请求头
class Server {
...
range(req, res, statObj, p) {
let range = req.headers['range'];
// 有范围请求时返回读流,断点续传
if (range) {
let [, start, end] = range.match(/bytes=(\d*)-(\d*)/);
start = start ? Number(start) : 0;
end = end ? Number(end) : statObj.size - 1;
res.statusCode = 206;
res.setHeader('Content-Range', `bytes ${start}-${end}/${statObj.size - 1}`);
fs.createReadStream(p, {start, end}).pipe(res);
}
else {
return false;
}
}
...
}
⑥ 发送文件
发送文件方法,即我们在 handleRequest时如果判断走到认为读取的是一个文件,则发送这个文件展示给用户
class Server {
...
sendFile(req, res, statObj, p) {
if (this.cache(req, res, statObj, p)) {
res.statusCode = 304;
return res.end();
}
// 是范围请求就忽略
if (this.range(req, res, statObj, p)) return;
// 设置文件类型头,如果不设置,我们访问一个html文件可能会导致下载
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
// 如果是需要压缩则定义gzip转化流,讲文件压缩后输出
let transform = this.gzip(req, res, statObj, p);
if (transform) {
return fs.createReadStream(p).pipe(transform).pipe(res);
}
// 如果不是不需要压缩则直接返回文件
fs.createReadStream(p).pipe(res);
}
...
}
⑦ 处理报错
在handleRequest方法中的处理错误方法,
class Server {
...
sendError(req, res){
// 返回的状态码设置为404
res.statusCode = 404;
// 页面返回文字
res.end(`404 Not Found`);
this.start();
}
...
}
⑧ 启动方法
原生http模块的创建服务器方法,创建成功后再cmd界面输出,告知主机和端口
class Server {
...
start() {
let server = http.createServer(this.handleRequest.bind(this));
server.listen(this.config.port, this.config.host, ()=> {
console.log(`server start http://${this.config.host}:${chalk.green(this.config.port)}`);
});
}
...
}
至此,我们的核心代码已经写完了,简易处理了服务器端需要处理的一些状况,有兴趣的同学可以补充和完善
三 bin/www.js 执行脚本
我们在命令行中输入的指令,调器执行脚本,并开启服务器
注意在www.js开头一定要写 #! /usr/bin/env node
告知操作系统node环境执行,以下为www.js的内容
#! /usr/bin/env node
let Server = require('../src/index.js'); // 导入Server
let commander = require('commander'); // 导入命令行模块
let {version} = require('../package.json'); // 读取package.json的版本
// 配置命令行
commander
.option('-p,--port <n>', 'config port') // 配置端口
.option('-o,--host [value]', 'config hostname') // 配置主机名
.option('-d,--dir [value]', 'config directory') // 配置访问目录
.version(version, '-v,--version').parse(process.argv); // 展示版本
let server = new Server(commander);
server.start(); // 启动
let config =require('../src/config');
commander = {...config, ...commander}
let os = require('os');
// 执行模块
let {exec} = require('child_process')
// 判断操作系统平台,win32是windows,执行访问程序,会自动弹出默认浏览器喔
if (os.platform() === 'win32') {
exec(`start http://${commander.host}:${commander.port}`);
}
else {
exec(`open http://${commander.host}:${commander.port}`);
}
再看一下我们在package.json中的配置
"bin": {
"zyx-http-server": "bin/www.js"
},
即我们的启动命令是 zyx-http-server,也可以根据自己的配置,补充其他命令,如 zyx-http-server -d public
,则读取public作为静态资源根目录
npm link
处理npm install 我们包中的依赖(ejs, chalk, debug等)之外还需要执行
npm link:将一个任意位置的npm包链接到全局执行环境,从而在任意位置使用命令行都可以直接运行该npm包
四 运行
在文件夹启动命令行工具 执行zyx-http-server -d public
,没有什么问题的话,我们会弹出
点击a文件夹
打开文件
至此,我们实现了传说中的http-server静态服务器,由于我叫zyx啦,所以取名叫zyx-http-server
五 发布到npm官网
没有注册的同学看到这一步的话请先去npm官网注册一个属于自己的账号,然后我们才能发布到自己包。
进行之前有这么几点需要注意
- 切换源到npm节点,如果平时使用cnpm或者其他节点的同学,请在命令行输入
nrm use npm
切换 - 在npm的注册邮箱一定要验证才可以,官网会发一份验证邮件给你,点击进行验证;我遇到了一种情况是验证过了,但是没生效,这时候你去官网再改一次邮箱试试
- 要发的包的命名,是package.json中的name配置项,一开始没有配置的,可以去写一个自己的包名,可以通过访问 www.npmjs.com/package/ + 你的包名,看看在npm有没有被占用,被占用的话就换一个哦,一般被占用的话,你也无法提交
npm login
需要在命令行执行 npm login
登录npm,如果你一开始没有切换到npm官网节点,cnpm用npm账号也是可以登录的,所以请提前先切换到npm
npm publish
登录成功后,就可以执行
npm publish
指令,发布成功有如下提示
更新代码的流程和发布是一样的,但是你要更新package.json中的version号。 这是我的包的地址,大家有兴趣可以看看 zyx-http-server
使用自己的包
依然是使用 npm i zyx-http-server -g
全局安装或局部安装这个包,我们试一下:
可以启动
中文文件夹bug已修复
在handleRequest方法中对pathname进行转码处理
希望我的文章可以帮到你~