资讯专栏INFORMATION COLUMN

Node构建一个静态文件服务器

banana_pi / 301人阅读

摘要:第二次客户端需要此数据的时候,要取得缓存的标识,然后去问一下服务器我的资源是否是最新的。服务器收到请求,将服务器的中此文件的跟请求头中的相比较如果值是一样的说明缓存还是最新的服务器将发送响应码给客户端表示缓存未修改过,可以使用。

静态文件服务器实现的功能
读取静态文件 
MIME类型支持
缓存支持/控制
支持gzip压缩
Range支持,断点续传
发布为可执行命令并可以后台运行,可以通过npm install -g安装

首先先构建好项目目录,项目目录如下:
project
|---bin 命令行实现放置脚本
|
|---public 静态文件服务器默认静态文件夹
|
|---src 实现功能的相关代码
| |
| |__template 模板文件夹
| |
| |__app.js 主要功能文件(main文件)
| |__config.js 配置文件
|
|---package.josn (初始化)

要启动一个服务器,我们需要知道这个服务器的启动时的端口号,在config.js配置一下:

let config = {
 host:"localhost" //提示用 ,
port:8080 //服务器启动时候的默认端口号,
path:path.resolve(__dirname,"..","test-dir") //静态服务器启动时默认的工作目录
 }

读取静态文件之前首先要先启动服务器,之后所有的方法都在class Server方法里

//handlebar 编译模板,得到一个渲染的方法,然后传入实际数据数据就可以得到渲染后的HTML了
function list() {
let tmpl = fs.readFileSync(path.resolve(__dirname, "template", "list.html"),"utf8");
return handlebars.compile(tmpl);//进行编译,最后渲染
}class Server {
    constructor(argv) {
        this.list = list();
        this.config = Object.assign({}, this.config, argv);
    }
    start() {
        let server = http.createServer();//创建服务器
        //当客户端向服务端发出数据的时候,会出发request事件
        server.on("request", this.request.bind(this));
        server.listen(this.config.port, () => {//监听端口号
            let url = `http://${this.config.host}:${this.config.port}`;
            debug(`server started at ${chalk.green(url)}`);
        });
    }
//发送错误信息,
sendError(err, req, res) {
    res.statusCode = 500;
    res.end(`${err.toString()}`);
}}
module.exports = Server;
读取静态文件
设计思路
首先输入一个url时,可能对应服务器上的一个文件,或者对应一个目录,
检查是否文件还是目录如果文件不存在,返回404状态码,发送not found页面到客户端
如果文件存在:打开文件读取
设置response header 发送文件到客户端
如果是目录就打开目录列表
async request(req, res) {
    //先取到客户端想要的是文件或文件夹路径 
    let { pathname } = url.parse(req.url);//获取路径的文件信息
    let filepath = path.join(this.config.root, pathname);//服务器上的对应服务器物理路径
    try {
        let statObj = await stat(filepath);//获取路径的文件信息
        if (statObj.isDirectory()) {//如果是目录的话,应该显示目录 下面的文件列表
            let files = await readdir(filepath);//读取文件的文件列表
            files = files.map(file => ({//把每个字符串变成对象
                name: file,
                url: path.join(pathname, file)
            }));
            //handlebar 编译模板
            let html = this.list({
                title: pathname,
                files
            });
            res.setHeader("Content-Type", "text/html");设置请求头
            res.end(html);
        } else {
            this.sendFile(req, res, filepath, statObj);//读取文件
        }
    } catch (e) {//不存在访问内就发送错误信息
        debug(inspect(e));//inspect把一个对象转成字符
        this.sendError(e, req, res);
    }
}

缓存支持/控制

设计思路
缓存分为强制缓存和对比缓存: 

两类缓存规则可以同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则.

 强制缓存如果生效,不需要再和服务器发生交互,而对比缓存不管是否生效,都需要与服务端发生交互

第一次访问服务器的时候,服务器返回资源和缓存的标识,客户端则会把此资源缓存在本地的缓存数据库中。

第二次客户端需要此数据的时候,要取得缓存的标识,然后去问一下服务器我的资源是否是最新的。如果是最新的则直接使用缓存数据,如果不是最新的则服务器返回新的资源和缓存规则,客户端根据缓存规则缓存新的数据。

通过最后修改时间来判断缓存是否可用

Last-Modified:响应时告诉客户端此资源的最后修改时间 

If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向服务器请求时带上头If-Modified-Since。

服务器收到请求后发现有头If-Modified-Since则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应最新的资源内容并返回200状态码; 

若最后修改时间和If-Modified-Since一样,说明资源没有修改,则响应304表示未更新,告知浏览器继续使用所保存的缓存文件。

ETag是资源标签。如果资源没有变化它就不会变。

1.客户端想判断缓存是否可用可以先获取缓存中文档的ETag,然后通过If-None-Match发送请求给Web服务器询问此缓存是否可用。
2 服务器收到请求,将服务器的中此文件的ETag,跟请求头中的If-None-Match相比较,如果值是一样的,说明缓存还是最新的,Web服务器将发送304 Not Modified响应码给客户端表示缓存未修改过,可以使用。 
3.如果不一样则Web服务器将发送该文档的最新版本给浏览器客户端

handleCache(req, res, filepath, statObj) {
    let ifModifiedSince = req.headers["if-modified-since"];
    let isNoneMatch = req.headers["is-none-match"];
    res.setHeader("Cache-Control", "private,max-age=30");//max-age=30缓存内容将在30秒后失效
    res.setHeader("Expires", new Date(Date.now() + 30 * 1000).toGMTString());
    let etag = statObj.size;
    let lastModified = statObj.ctime.toGMTString();
    res.setHeader("ETag", etag);//获取ETag
    res.setHeader("Last-Modified", lastModified);//服务器文件的最后修改时间
    //任何一个对比缓存头不匹配,则不走缓存
    if (isNoneMatch && isNoneMatch != etag) {//缓存过期
        return fasle;
    }
    if (ifModifiedSince && ifModifiedSince != lastModified) {//缓存过期
        return fasle;
    }
    //当请求中存在任何一个对比缓存头,则返回304,否则不走缓存
    if (isNoneMatch || ifModifiedSince) {//缓存有效
        res.writeHead(304);
        res.end();
        return true;
    } else {
        return false;
    }
}
支持gzip压缩
设计思路

浏览器都会携带自己支持的压缩类型,最常用的两种是gzip和deflate。根据请求头Accept-Encoding,返回不同的压缩格式.

getEncoding(req, res) {

    let acceptEncoding = req.headers["accept-encoding"];//获取客户端发送的压缩请求头的信息
    if (/gzip/.test(acceptEncoding)) {//如果是gzip的格式
        res.setHeader("Content-Encoding", "gzip");
        return zlib.createGzip();
    } else if (/deflate/.test(acceptEncoding)) {//如果是deflate的格式
        res.setHeader("Content-Encoding", "deflate");
        return zlib.createDeflate();
    } else {
        return null;//不压缩
    }
}
Range支持,断点续传

设计思路

该选项指定下载字节的范围,常应用于分块下载文件

服务器告诉客户端可以使用range response.setHeader("Accept-Ranges", "bytes")  

Server通过请求头中的Range:bytes=0-xxx来判断是否是做Range请求,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,如果无效,则返回416状态码,表明Request

Range Not Satisfiable

 getStream(req, res, filepath, statObj) {
    let start = 0;//可读流起始位置
    let end = statObj.size - 1;//可读流结束位置
    let range = req.headers["range"];//获取客户端的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
    });
}

发布为可执行命令

首先在package.json配置一下"bin": { "http-static": "bin/www" }

#! /usr/bin/env node     //这段代码一定要写在开头,为了兼容各个电脑平台的差异性
// -d --root 静态文件目录 -o --host 主机 -p --port 端口号let yargs = require("yargs");
let  Server = require("../src/app.js");
let argv = yargs.option("d",{   
 alias:"root", 
 demand:"false",  
 type:"string",   
 default:process.cwd(),  
 description:"静态文件跟目录"    })
.option("o",{  
  alias:"host",  
  demand:"localhost",  
  type:"string",    
description:"请配置监听的主机"})
.option("p",{  
  alias:"root",  
  demand:"false",   
 type:"number",   
 default:8080,  
  description:"请配置端口号"})
.usage("http-static [options]").example(  
  "http-static -d / 8080 -o localhost","在本机的9090端口上监听客户端的请求"
).help("h").argv;
// argv = {d,root,o,host,p,port}let server = new Server(argv);//启动服务server.start();

这样命令行当中通过输入http-static来直接启动静态文件服务器了,那么命令行调用的功能也就实现了,最后用npm publish发布一下,发布到npm上面去了,我们就可以通过npm install -g来进行全局安装了

参考资料

Node.js静态文件服务器实战

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93555.html

相关文章

  • 我的博客发布上线方案 — Hexo

    摘要:首发于樊浩柏科学院之前一直在使用推荐的发布方案,缺点是本地依赖环境,无法随时随地地更新博客。为了摆脱环境约束进而高效写作,有了下述的发布方案。我的写作环境为,博客发布在阿里云的上,文章托管在。 首发于 樊浩柏科学院 之前一直在使用 Hexo 推荐的发布方案,缺点是本地依赖 Hexo 环境,无法随时随地地更新博客。为了摆脱 Hexo 环境约束进而高效写作,有了下述的发布方案。 show...

    yangrd 评论0 收藏0
  • 我的博客发布上线方案 — Hexo

    摘要:首发于樊浩柏科学院之前一直在使用推荐的发布方案,缺点是本地依赖环境,无法随时随地地更新博客。为了摆脱环境约束进而高效写作,有了下述的发布方案。我的写作环境为,博客发布在阿里云的上,文章托管在。 首发于 樊浩柏科学院 之前一直在使用 Hexo 推荐的发布方案,缺点是本地依赖 Hexo 环境,无法随时随地地更新博客。为了摆脱 Hexo 环境约束进而高效写作,有了下述的发布方案。 show...

    Michael_Lin 评论0 收藏0
  • ThinkJS 项目构建 Docker 镜像

    摘要:而项目代码会随着需求变更而经常变化。项目启动后一般会有两个目录会写入文件。 其实这个话题很简单,不是很想写这篇文章。不过的确还是有很多朋友在打包构建部署上存在一些问题,恰巧最近使用 Docker 部署了几个 ThinkJS 相关的项目,所以还是拿出来说说吧。需要提前说明的是本文并不是 Docker 的基础教程,默认大家都是了解 Docker 的。然后我会分享一下我觉得 ThinkJS ...

    fuyi501 评论0 收藏0
  • [手把手系列之]Docker 部署 vue 项目

    摘要:部署项目写在前面作为轻量级虚拟化技术,拥有持续集成版本控制可移植性隔离性和安全性等优势。容器可以被创建启动停止删除暂停等。重新运行应用容器直接基于镜像来启动容器,运行命令将宿主机的挂载到容器的目录上。Docker 部署 vue 项目 1.写在前面: Docker 作为轻量级虚拟化技术,拥有持续集成、版本控制、可移植性、隔离性和安全性等优势。本文使用Docker来部署一个vue的前端应用,并尽...

    VPointer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<