用node模拟一个简单的静态服务器

我们都知道在本地起个服务直接一个http-server -p 3000 一个端口为3000的服务就起来了,我们可以直接在浏览器访问3000端口,就能拿到我们需要的页面,那么如果想自己实现一个这样的工具怎么做呢?不要急,看我慢慢分析写出来

写之前我们先要搞清楚要做什么用自己写的包,起一个服务,访问3000端口回车,应该显示出public下的目录列表,后面加/index.html,就应该显示index.html的内容来

  • 首先先init一个项目,并下载一些包,mime(解析返回头类型),chalk(五颜六色的输出),debug
    这里写图片描述
  • 建立自己的目录结构:

这里写图片描述
index.css

body{
    background: red
}

index.html

<!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>
    我很美
    <link rel="stylesheet" href="/index.css">
</body>
</html>

config.js

let path = require('path')
//启动服务的配置项
let config = {
    hostname:'localhost',
    port:3000,
    dir:path.join(__dirname,'..','public')
}
module.exports = config

app.js
这里用到了debug(用法请看这里)

// set DEBUG=static:app (win32
// export DEBUG=static:app (ios

let config = require('./config')
let path = require('path')
let fs = require('fs')
let mime = require('mime')
let chalk = require('chalk')
let util = require('util')
let url = require('url')
let http = require('http')
let stat = util.promisify(fs.stat)
//debug 可以后面放参数,可以根据后面的参数决定是否打印

let debug = require('debug')('static:app') 
//console.log(chalk.green('hello'));
//debug('app')

class Server {  //首先写一个Server
    constructor(){
        this.config = config
    }
    handleRequest(){
        return (req,res)=>{
        }
    }
    start(){   //实例上的start方法
        let {port,hostname} = this.config
       let server =  http.createServer(this.handleRequest())
       //用http启动一个服务,回调里执行handleRequest方法
        let url = `http://${hostname}:${chalk.green(port)}`
         debug(url);
        server.listen(port, hostname);
    }
}
let server = new Server()
server.start()

node执行app.js,(在执行之前要先执行set DEBUG=static:app)得到下图
这里写图片描述
如果你想实时监控项目的变化可以安装一个supervisor(npm install supervisor -g),直接执行supervisor app.js就能监控了,不过不是很稳定······
这个时候可以假设访问的是http://localhost:3000/index.html,是个文件,我们就可以写handleRequest方法了

  handleRequest(){
        return async(req,res)=>{
            //处理路径
            let {pathname} = url.parse(req.url,true)
            //因为拿到的pathname会是/index,这样会直接指向c盘,加./的话就变成当前
            let  p = path.join(this.config.dir,'.'+pathname)

            try{
               let statObj = await stat(p)//判断p路径对不对
                if(statObj.isDirectory()){

                }else{
                    //是文件就直接读了
                    res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
                    fs.createReadStream(p).pipe(res)
                }
            }catch(e){
                res.statusCode = 404;
                res.end()
            }
        }
    }

这时候访问 http://localhost:3000/index.html,就能出页面信息了

  • 架子搭出来了,那么就开始写吧,因为报错和展示页面信息要重复利用,所以把他们单独提出来封成两个方法
  sendFile(req,res,p){
        //是文件就直接读了
        res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
        fs.createReadStream(p).pipe(res)
    }
    sendError(req,res,e){
        debug(util.inspect(e).toString())
        res.statusCode = 404;
        res.end()
    }
  • 假如访问的是个目录的话,我们应该把目录展示出来,这样的话最好使用模板引擎,常见的模板引擎有:handlebar ejs
    这我们用的是ejs,用法 render(‘文件内容’,‘变量参数’),装一下 :npm install ejs
    src/tmpl.ejs
<!DOCTYPE html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>staticServer</title>
  </head>
  <body>
      <!-- 碰到js就 <%  %>包起来,求值用= -->
     <%dirs.forEach(dir=>{%>
         <li><a href="<%=dir.path%>"><%=dir.name%></a></li>
    <% })%>
  </body>
</html>

那在app.js中就要放进去

let ejs = require('ejs')
let tmpl = fs.readFileSync(path.join(__dirname,'tmpl.ejs'),'utf8')
let readDir = util.promisify(fs.readdir)//读取目录用的方法

再把tmpl挂在this上
app.js那么如果是目录的话这个代码就这么写

 if(statObj.isDirectory()){
                    //如果是目录的话就应该把目录放出去
                    //用模板引擎写 handlebal ejs underscore jade 
                    let dirs = await readDir(p)
                    debug(dirs)//返回的是个数组[index.css,index.html]
                   dirs = dirs.map(dir => ({
                        path: path.join(pathname, dir),
                        name: dir
                    }))
                let content = ejs.render(this.tmpl,{dirs})
                    res.setHeader('Content-Type','text/html;charset=utf8')
                    res.end(content)
                }else{
                    this,this.sendFile(req,res,p)
                }
  • 下面就是细化的问题了,总共3个方向:

    1. 如果文件访问过,就应该有缓存的功能,
    2. 文件很大应该有压缩,
    3. 范围请求
      缓存
 cache(req,res,statObj){
        //etag if-none-match
        //Last-Modified  if-modified-since
        //Cache-Control 
        //ifNoneMatch一般是内容的md5戳 => ctime+size
        let ifNoneMatch = req.headers['if-none-match']
        //ifModifiedSince文件的最新修改时间
        let ifModifiedSince = req.headers['if-modified-since']
        let since = statObj.ctime.toUTCString();//最新修改时间
        //代表的是服务器文件的一个描述
        let etag = new Date(since).getTime()  +'-'+statObj.size
        res.setHeader('Cache-Control','max-age=10') 
        //10秒之内强制缓存
        res.setHeader('Etag',etag)
        res.setHeader('Last-Modified',since) //请求头带着
        //再访问的时候对比,如果相等,就走缓存
        if(ifNoneMatch !== etag){
            return false
        }
        if(ifModifiedSince != since){
            return false
        }
        res.statusCode = 304
        res.end()
        return true

    }

sendFile 中加这句话

  //缓存
        if(this.cache(req,res,statObj)) return

那么访问index.html
访问的画面是
这里写图片描述
查看他们的头
这里写图片描述
当然再刷新的话200就会变成304,走的是缓存了
压缩
因为用到了zlib所以要在头上加上

let zlib = require('zlib');

压缩方法

   compress(req,res,statObj){
          // 压缩 Accept-Encoding: gzip,deflate,br
        // Content-Encoding:gzip
       let header = req.headers['accept-encoding']
       if(header){

        if(header.match(/\bgzip\b/)){
            res.setHeader('Content-Encoding','gzip') 
            return zlib.createGzip()
        }else if(header.match(/\bdeflate\b/)){
            res.setHeader('Content-Encoding','deflate') 
            return zlib.createDeflate()
        }else{
            return false //不支持压缩
        }
       }else{
           return false
       }

    }

sendFile

    sendFile(req,res,p,statObj){
        //缓存
        if(this.cache(req,res,statObj)) return
        //压缩
        let s = this.compress(req, res, p, statObj);
        console.log(s)
        res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
         let rs = fs.createReadStream(p)
        if(s){
            //如果支持就是返回的流
            rs.pipe(s).pipe(res)
        }else{
            rs.pipe(res)
        }
        //是文件就直接读了

      //  fs.createReadStream(p).pipe(res)
    }

查看一下是否压缩成功:访问http://localhost:3000/index.css这里写图片描述
可以看到支持的是gzip,成功!
范围请求 方法

range(req,res,statObj){
        //范围请求的头 :Rang:bytes=1-100
        //服务器 Accept-Ranges:bytes
        //Content-Ranges:1-100/total
        let header = req.headers['range']
        //header =>bytes=1-100
        let start = 0;
        let end = statObj.size;//整个文件的大小
        if(header){
            res.setHeader('Content-Range','bytes')
            res.setHeader('Accept-Ranges',`bytes ${start}-${end}/${statObj.size}`)
            let [,s,e] = header.match(/bytes=(\d*)-(\d*)/);
            start = s?parseInt(s):start
            end = e? parseInt(e):end

        }
        return {start,end:end-1}//因为start是从0开始

    }

sendFile 文件就是这样了

  sendFile(req, res, p, statObj) {
        // 缓存的功能 对比 强制
        if (this.cache(req, res, statObj)) return;
        // 压缩 Accept-Encoding: gzip,deflate,br
        // Content-Encoding:gzip
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
        let s = this.compress(req, res, p, statObj);
        // 范围请求 
        let {start,end} = this.range(req,res,statObj);
        let rs = fs.createReadStream(p,{start,end})
        if (s) {
            rs.pipe(s).pipe(res);
        } else {
            rs.pipe(res);
        }
    }

在命令行工具下执行
curl -v –header “Range:bytes=1-3” http://localhost:3000/index.html
就可以看到效果了
这里写图片描述
如果你的window不能执行curl 可以看这里
git地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值