美文网首页
Node之手写静态资源服务器

Node之手写静态资源服务器

作者: 小武song | 来源:发表于2018-03-02 16:59 被阅读0次

    背景

    学习服务端知识,入门就是要把文件挂载到服务器上,我们才能去访问相应的文件。本地开发的时候,我们也会经常把文件放在服务器上去访问,以便达到在同一个局域网内,通过同一个服务器地址访问相同的文件,比如我们会用xampp,会用sulime的插件sublime-server等等。本篇文章就通过node,手写一个静态资源服务器,以达到你可以随意定义任何一个文件夹为根目录,去访问相应的文件,达到anywhere is your static-server。

    主要实现功能

    1. 读取静态文件
    2. 静态资源缓存
    3. 资源压缩
    4. MIME类型支持
    5. 断点续传
    6. 发布为可执行命令并可以后台运行,可以通过npm install -g安装

    Useage

    //install
    $ npm i st-server -g
    //forhelp
    $ st-server -h
    //start
    $ st-server
    // or with port
    $ st-server -p 8800
    // or with hostname
    $ st-server -o localhost -p 8888
    // or with folder
    $ st-server -d / 
    // full parameters
    $ st-server -d / -p 9900 -o localhost
    

    其中可以配置三个参数,-d代表你要访问的根目录,-p代表端口号(目前暂不支持多次开启用同一个端口号,需要手动杀死之前的进程),-o代表hostname。
    所有源代码已经上传至github

    源码分析

    • 全部代码基于一个StaticServer类进行实现,在构造函数中首先引入所有的配置,argv是通过命令行敲入传进来的参数,然后在获取需要编译的模板,该模板是简单的显示一个文件夹下所有文件的列表。基于handlebars实现。然后开启服务,监听请求,由this.request()处理
    class StaticServer{
        constructor(argv){
            this.config = Object.assign({},config,argv);
            this.compileTpl = compileTpl();
        }
        startServer(){
            let server = http.createServer();
            server.on('request',this.request.bind(this));
            server.listen(this.config.port,()=>{
                let serverUrl = `http://${this.config.host}:${this.config.port}`;
                debug(`服务已开启,地址为${chalk.green(serverUrl)}`);
            })
        }
    }
    
    • 主线就是读取想要搭建静态服务的地址,如果是文件夹,则查找该文件夹下是否有index.html文件,有则显示,没有则列出所有的文件;如果是文件的话,则直接显示该文件内容。大前提在显示具体的文件之前,要判断有没有缓存,有直接获取缓存,没有的话再请求服务器。
     async request(req,res){
            let {pathname} = url.parse(req.url);
            if(pathname == '/favicon.ico'){
                return this.sendError('NOT FOUND',req,res);
            }
            //获取需要读的文件目录
            let filePath = path.join(this.config.root,pathname);
            let statObj = await fsStat(filePath);
            if(statObj.isDirectory()){//如果是一个目录的话 列出目录下面的内容
                let files = await readDir(filePath);
                let isHasIndexHtml = false;
                files = files.map(file=>{
                    if(file.indexOf('index.html')>-1){
                        isHasIndexHtml = true;
                    }
                    return {
                        name:file,
                        url:path.join(pathname,file)
                    }
                })
                if(isHasIndexHtml){
                    let statObjN = await fsStat(filePath+'/index.html');
                    return this.sendFile(req,res,filePath+'/index.html',statObjN);
                }
                let resHtml = this.compileTpl({
                    title:filePath,
                    files
                })
                res.setHeader('Content-Type','text/html');
                res.end(resHtml);
            }else{
                this.sendFile(req,res,filePath,statObj);
            }
            
        }
        sendFile(req,res,filePath,statObj){
            //判断是否走缓存
            if (this.getFileFromCache(req, res, statObj)) return; //如果走缓存,则直接返回
            res.setHeader('Content-Type',mime.getType(filePath)+';charset=utf-8');
            let encoding = this.getEncoding(req,res);
            //常见一个可读流
            let rs = this.getPartStream(req,res,filePath,statObj);
            if(encoding){
                rs.pipe(encoding).pipe(res);
            }else{
                rs.pipe(res);
            }
        }
    

    sendFile方法就是向浏览器输出内容的方法,主要包括以下几个重要的点:

    1. 缓存处理
     getFileFromCache(req,res,statObj){
            let ifModifiedSince = req.headers['if-modified-since'];
            let isNoneMatch = req.headers['if-none-match'];
            res.setHeader('Cache-Control','private,max-age=60');
            res.setHeader('Expires',new Date(Date.now() + 60*1000).toUTCString());
            let etag = crypto.createHash('sha1').update(statObj.ctime.toUTCString() + statObj.size).digest('hex');
            let lastModified = statObj.ctime.toGMTString();
            res.setHeader('ETag', etag);
            res.setHeader('Last-Modified', lastModified);
            if (isNoneMatch && isNoneMatch != etag) {
                return false;
            }
            if (ifModifiedSince && ifModifiedSince != lastModified) {
                return false;
            }
            if (isNoneMatch || ifModifiedSince) {
                res.statusCode = 304;
                res.end('');
                return true;
            } else {
                return false;
            }
        }
    

    这里我们通过Last-Modified,ETag实现协商缓存,Cache-Control,Expires实现强制缓存,当所有缓存条件成立时才会生效。Last-Modified原理是通过文件的修改时间,判断文件是否修改过,ETag通过文件内容的加密判断是否修改过。Cache-Control,Expire通过时间进行强缓。

    1. 对文件进行压缩,压缩文件以后可以减少体积,加快传输速度和节约带宽 ,这里支持gzip和deflate两种方式,用node本身的模块zlib进行处理。
      getEncoding(req,res){
            let acceptEncoding = req.headers['accept-encoding'];
            if(acceptEncoding.match(/\bgzip\b/)){
                res.setHeader('Content-Encoding','gzip');
                return zlib.createGzip();
            }else if(acceptEncoding.match(/\bdeflate\b/)){
                res.setHeader('Conetnt-Encoding','deflate');
                return zlib.createDeflate();
            }else{
                return null;
            }
        }
    
    1. 通过range,进行断点续传的处理
     getPartStream(req,res,filePath,statObj){
            let start = 0;
            let end = statObj.size -1;
            let range = req.headers['range'];
            if(range){
                res.setHeader('Accept-Range','bytes');
                res.statusCode = 206;
                let result = range.match(/bytes=(\d*)-(\d*)/);
                if(result){
                    start = isNaN(result[1]) ? start : parseInt(result[1]);
                    end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
                }
            }
            return fs.createReadStream(filePath,{
                start,end
            })
        }
    
    1. 生成命令行工具,用npm安装yargs包进行操作,并在package.json中添加 "bin": {
      "st-Server": "bin/www"
      },指向需要执行命令的文件,然后在www中配置对应的命令,并且开启子进程进行主代码的操作,为了解决你开启命令后,命令行一直处于卡顿的状态。开启子进程也是node原生模块child_process支持的。
    #! /usr/bin/env node
    
    let yargs = require('yargs');
    let argv = yargs.option('d', {
        alias: 'root',
        demand: 'false',
        type: 'string',
        default: process.cwd(),
        description: '静态文件根目录'
    }).option('o', {
        alias: 'host',
        demand: 'false',
        default: 'localhost',
        type: 'string',
        description: '请配置监听的主机'
    }).option('p', {
        alias: 'port',
        demand: 'false',
        type: 'number',
        default: 8800,
        description: '请配置端口号'
    })
        .usage('st-server [options]')
        .example(
        'st-server -d / -p 9900 -o localhost', '在本机的9900端口上监听客户端的请求'
        ).help('h').argv;
    
    let path = require('path');
    let {
     spawn
    } = require('child_process');
    
    let p1 = spawn('node', ['www.js', JSON.stringify(argv)], {
     cwd: __dirname
    });
    p1.unref();
    process.exit(0);
    

    参考

    anywhere

    相关文章

      网友评论

          本文标题:Node之手写静态资源服务器

          本文链接:https://www.haomeiwen.com/subject/jrnaxftx.html