手写一个静态服务能够对node中http模块有更深的理解,这是咱们的初衷。http-server相信你们都用过,这里咱们要实现相似个功能。功能以下css
let path = require('path');
let config = {
hostname:'127.0.0.1', //默认主机
port:3000, //默认端口
dir:path.join(__dirname,'','public') //默认打开的目录(绝对路径)
};
module.exports = config;
复制代码
以上代码都能看得懂,下面开始写咱们主文件html
let http = require('http');
let url = require('url');
let path = require('path');
let util = require('util');
let fs = require('fs');
let zlib = require('zlib');
let mime = require('mime'); // 获得内容类型
let debug = require('debug')('*'); // 打印输出 会根据环境变量控制输出
let chalk = require('chalk'); // 粉笔
let ejs = require('ejs'); // 模板引擎
//先声明好,下面解释
let config = require('./config');
let stat = util.promisify(fs.stat);//promise化 fs.stat方法
let readdir = util.promisify(fs.readdir);
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //读取ejs的模板文件
复制代码
/*运行的条件 指定主机名
* 指定启动的端口号
* 指定运行的目录
*/
let config = require('./config'); //引入配置文件
class Server { //声明类
constructor() {
this.config = config; //讲配置挂载再咱们的实例上
}
handleRequest(req,res){ //确保这里的this都是实例
}
start(){//服务开始的方法
let server =http.createServer(this.handleRequest.bind(this));
let {hostname,port} = this.config; //解构主机名和端口
server.listen(port,hostname);
debug(`http://${hostname}:${port} start`) //命令行中打印
}
}
//开启一个服务
let server = new Server();
server.start(); //调用start方法
复制代码
截至到目前位置,简单的服务已经开启了,先来测试下效果吧node
完美,控制台打印出了内容,git
列出咱们要作什么github
let stat = util.promisify(fs.stat);//promise化 fs.stat方法
async handleRequest(req,res){ //确保这里的this都是实例
let {pathname} = url.parse(req.url,true); //获取url的路径
let p = path.join(this.config.dir,pathname); // 多是G:/cgp-server/public 多是G://cgp-server/public/index.html
//一、根据路径 返回不一样结果 若是是文件夹 显示文件夹里的内容
//二、若是是文件 显示文件的内容
try{
let statObj=await stat(p);
}catch (e) {
//文件不存在状况
this.sendError(req,res,e)
}
}
复制代码
try catch用于捕获错误,当文件不存在,调用sendError方法,先来实现这个错误的处理方法npm
sendError(req,res,e){
debug(util.inspect(e)); //输出错误,util模块提供方法
res.statusCode = 404;
res.end('Not Found');
}
复制代码
写了这么多了,测试下错误文件可否打印错误 json
![]()
测试完美,此时咱们应该判断打开的是文件仍是目录,并给对应的方法,下面咱们开始目录的渲染方法api
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //读取ejs的模板文件
class Server{
constructor(){
this.template = template //挂载到实例上
}
}
复制代码
if(statObj.isDirectory()){
//若是是目录 列出目录内容能够点击
let dirs = await readdir(p); //public下面的目录结构=>[index.html,style.css]
dirs =dirs.map(dir=>{
return {
filename:dir,
path:path.join(pathname,dir)
}
});
//dirs就是要渲染的数据
//格式以下[{filename:index.html,path:'/index.html'},{{filename:style.css,path:''/style.css}}]
let str =ejs.render(this.template,{dirs}); //ejs渲染方法
// console.log(str);
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(str);
}
复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
//循环dirs中的内容到页面中
<% dirs.map(item=>{%>
<li><a href="<%=item.path%>"><%=item.filename%></a></li>
<%})%>
</body>
</html>
复制代码
渲染目录结构,咱们已经写完了,测试下看能不能运行promise
目前来看,无bug,接下来实现若是是文件的话,直接把文件内容渲染出来浏览器
sendFile(req,res,p,statObj){
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8');
fs.createReadStream(p).pipe(res);//可读流pipe到可写流
}
复制代码
功能已经实现拉,不信咱们测下
sendFile(req,res,p,statObj){
// 一、检测是否有缓存
if(this.cache(req,res,p,statObj)){ //若是有缓存
res.statusCode = 304;
res.end();
return
}
//二、检测是否支持压缩
....
//三、检测是否有范围请求
....
}
复制代码
缓存有两种方式,强制缓存和协商缓存
看完这个图,相信你们应该懂啦。下面开始写缓存方法
cache(req,res,p,statObj){ //实现缓存
/* 强制缓存 服务端 Cache-Control Expires
协商缓存 服务端 Last-Modified Etag
协商缓存 客户端 if-modified-since if-none-match
etag ctime + 文件的大小
Last-modified ctime
强制缓存
*/
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString());//10秒后从新发请求
let etag = statObj.ctime.toGMTString() + statObj.size; //文件修改时间和文件大小
let lastModified = statObj.ctime.toGMTString(); //文件的修改时间
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) { //不相等,不走缓存
return false;
}
if (lastModified != ifModifiedSince) { //同理
return false;
}
return true; //不然走缓存
}
复制代码
缓存功能写完了,咱们测试下设置的头有没有添加上
缓存咱们就已经实现啦
gzip(req,res,p,statObj){
// 客户端 Accept-Encoding: gzip, deflate, br
// 服务端 Content-Encoding: gzip
let encoding = req.headers['accept-encoding']; //获取请求头的接收的压缩格式
if (encoding) {
if (encoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip')
return zlib.createGzip();//返回一个gzip的压缩流
} else if (encoding.match(/\bdeflate\b/)) {
res.setHeader('content-encoding', 'deflate');
return zlib.createDeflate(); //返回createDeflate的压缩流
} else {
return false; //不然不支持压缩
}
} else {
return false;//不然不支持压缩
}
}
复制代码
sendFile(req,res,p,statObj){
// 一、检测是否有缓存
if(this.cache(req,res,p,statObj)){ //若是有缓存
res.statusCode = 304;
res.end();
return
}
//二、检测是否支持压缩
res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
let compress =this.gzip(req,res,p,statObj);
if(compress){ //检测是否压缩。返回的是压缩流
return fs.createReadStream(p).pipe(compress).pipe(res);
}else{ //不支持压缩直接把文件读出来便可
return fs.createReadStream(p).pipe(res)
}
//三、检测是否有范围请求
....
}
复制代码
用1.txt文件测试下
目前来看都还ok,还剩最后一个功能,实现范围请求
因为可能同时会有压缩和范围请求,咱们稍微改下前面的代码
sendFile(req,res,p,statObj){
// 一、检测是否有缓存
....
//二、检测是否支持压缩同时加上范围请求
res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
let compress =this.gzip(req,res,p,statObj);
let {start,end} = this.range(req,res,p,statObj); //解构开始和结束的位置
if(compress){ //检测是否压缩。返回的是压缩流
return fs.createReadStream(p,{start,end}).pipe(compress).pipe(res);
}else{
// res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
return fs.createReadStream(p,{start,end}).pipe(res)
}
}
复制代码
range(req, res, statObj, p) {
//客户端 Range:bytes=0-3
//服务端 Accept-Range:bytes Content-Range:bytes 0-3/8777
let range = req.headers['range']; //若是有范围请求
if (range) {
let [, start, end] = range.match(/(\d*)-(\d*)/); //解构出开始和结束的位置
start = start ? Number(start) : 0; //start设置默认值
end = end ? Number(end) : statObj.size - 1; //end设置默认值
res.statusCode = 206; //状态码 206范围请求
res.setHeader('Accept-Ranges',"bytes");
res.setHeader('Content-Length',end-start+1);
res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`);
return {start,end};
}else {
return {start:0, end:statObj.size};
}
}
复制代码
基本功能已经实现,测试下代码,咱们用curl工具发送请求,用法
测试完美,接下来咱们还想实现输入cgp-server ,自动开启浏览器,打开目录。咱们须要引用一个模块 yargs
#! /usr/bin/env node //执行命令后会执行 bin/www.js
const yargs = require('yargs');
let argv = yargs.option('port',{ //yargs的基础用法
alias: 'p', //别名
default: 3000, //默认值
description:'this is port', //描述
demand:false // 是否必须
}).option('hostname',{
alias: 'h',
default: 'localhost',
description:'this is hostname',
demand:false
}).option('dir',{
alias: 'd',
default: process.cwd(),
description:'this is cwd',
demand:false
}).usage('cgp-server [options]' ).argv;
//开启服务
let Server = require('../src/app.js');
new Server(argv).start();
// 判断是win仍是mac平台
let platform = require('os').platform();
//开启子进程
let {exec} = require('child_process');
//win系统 win32
if(platform==="win32"){
exec(`start http://${argv.hostname}:${argv.port}`)
}else {
exec(`open http://${argv.hostname}:${argv.port}`)
}
复制代码
yargs.option('port',{ //yargs的基础用法
alias: 'p', //别名
default: 3000, //默认值
description:'this is port', //描述
demand:false // 是否必须
})
复制代码
测试下看能不能启动
若是你能看到这里,真的不容易,点个赞再走吧,源码分享给你