实现一个简易的静态服务器

静态服务器就是网站把一些在通常操作下不会发生改变的资源给浏览器。显示网站外观的图片和CSS文件,在浏览器中运行的JavaScript代码,没有动态组件的HTML文件就是这种资源中的代表,统称为静态文件。

这里我用node来开发一个服务器,提供的其他功能有:

  1. 缓存
  2. 压缩
  3. 解析用户命令行输入

我们会用到的node包有:http,fs,path,url,zlib

http是提供web服务的核心包

fs是文件或文件夹操作包

path是将请求的路径转换成本机路径

url是将请求的网址转换成对象,方便我们调用

zlib是压缩包
复制代码

我们还会用到一些第三方包:ejs,mime,commander

ejs是一个javascript的模板引擎

mime是文件类型判断包

commander用来解析用户在命令行输入的参数
复制代码

我们的静态服务器的实现思路就是,启动一个静态服务器,监听用户发送的请求,当请求到来时,解析拿到请求的地址。如果请求的是文件,就读取相应的文件并返回给用户。如果的文件夹,就读取文件夹下的所有文件名,然后把文件名放在模板html里并返回给用户。

class Server{
    constructor(opts = {}){
        this.host = opts.host || '127.0.0.1'
        this.port = opts.port || 3000
        this.staticPath = opts.staticPath || 'public'
    }
    start(){
        let server = http.createServer(this.handleRequest.bind(this));

        server.listen(this.port, this.host, ()=>{
            console.log(`服务已启动:${this.host}:${this.port}`)
        })
    }
    handleRequest(req, res){
        let self = this;

        //获取请求的文件名
        let {pathname} = url.parse(req.url);
        if(pathname == '/favicon.ico') return res.end();

        //把请求的文件名转换成public下的绝对路径
        let p = path.join(__dirname, '../', this.staticPath, pathname);

        fs.stat(p, function(err, stats){
            if(err){
                res.end(err);
            }

            if(stats.isDirectory()){
                self.sendDir(req, res, stats, p);
            }else{
                self.sendFile(req, res, stats, p);
            }
        })
    }
    sendFile(req, res, stats, p){
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8')//发送的数据类型
        
        fs.createReadStream(p).pipe(res)
    }

    sendDir(req, res, stats, p){
        let {pathname} = url.parse(req.url);

        res.setHeader('Content-Type', 'text/html;charset=utf-8')//发送的数据类型
        let template = fs.readFileSync(path.join(__dirname, 'template.html'), 'utf-8')
        let files = fs.readdirSync(p)
        files = files.map(file=>{
            return {
                filename: file,
                filepath: path.join(pathname, file)
            }
        })
        let str = ejs.render(template, {
            name:`index of ${pathname}`, 
            arr:files,
        })

        res.end(str)
    }
}
复制代码

template.html就是用来显示文件夹下的文件名,主要用到的就是ejs模板引擎,内容为

<h2><%=name%></h2>
<%arr.forEach(item=>{%>
    <li><a href="<%=item.filepath%>"><%=item.filename%></a></li>
<%})%>
复制代码

缓存功能也很简单,就是设置一些响应头,给Server类增加一个原型方法

setCache(req, res, stats, p){
    res.setHeader('Cache-Control', 'max-age=10')//缓存存活时间
    res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())//缓存存活时间
    
    let etag = stats.ctime.getTime() + '-' + stats.size;
    let LastModified = stats.ctime.toGMTString();
    
    let ifNoneMatch = req.headers['if-none-match']
    let ifModifiedSince = req.headers['if-modified-since']//文件最后修改时间

    res.setHeader('Last-Modified', LastModified)
    res.setHeader('Etag', etag)
    
    if(etag == ifNoneMatch && LastModified == ifModifiedSince){
        return true;
    }

    return false;
}
复制代码

在发送文件方法里开启缓存

sendFile(req, res, stats, p){
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8')//发送的数据类型
    
    if(this.setCache(req, res, stats, p)){
        res.statusCode = 304;
        return res.end();
    }

    fs.createReadStream(p).pipe(res)
}
复制代码

压缩功能跟缓存类似,增加方法

gzip(req, res, stats, p){
    let encoding = req.headers['accept-encoding']

    if(encoding){
        if(encoding.match(/\bgzip\b/)){
            res.setHeader('Content-Encoding', 'gzip')//压缩类型
            return zlib.createGzip();
        }
        if(encoding.match(/\bdeflate\b/)){
            res.setHeader('Content-Encoding', 'deflate')//压缩类型
            return zlib.createDeflate();
        }

        return false;
    }else{
        return false;
    }
}
复制代码

开启压缩功能

sendFile(req, res, stats, p){
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8')//发送的数据类型
    
    if(this.setCache(req, res, stats, p)){
        res.statusCode = 304;
        return res.end();
    }

    let transform = this.gzip(req, res, stats, p);
    if(transform){
        return fs.createReadStream(p).pipe(transform).pipe(res)
    }

    fs.createReadStream(p).pipe(res)
}
复制代码

至此我们的主要功能就实现了,但是我们的服务不够智能,比如端口固定是3000,这样会出现端口冲突的问题。我们可以用commander包来接收用户的配置,来动态修改端口。

let program = require('commander')

program
    .option('-p,--port <n>', 'config port')
    .option('-o,--host [value]', 'config host')

program.parse(process.argv);
复制代码

启动服务的时候把program传进去就可以了

let server = new Server(program);
server.start();
复制代码

猜你喜欢

转载自juejin.im/post/5b726d8051882560f53c6dc6