使用node搭建静态资源服务器(1)

node实现静态文件服务器的优势

提到这就不得不说node的优势啦,node适合的场景是高并发I/O密集型,高并发就是同一时刻访问的人特别多,大家都跟服务器说我要什么什么,在这一时刻服务器要处理的请求也特别多。那I/O密集指什么呢,先来说下什么是I/O操作,一般对文件,数据库,网络操作都算为I/0操作,与之对应的概念是CPU操作,包括解密,加密,压缩,解压等。正常一个请求过来服务器就会开启一个线程,如果同时有多个请求就会开启多个线程,但操作系统能支持的并发数量是有限的,而node作为服务器端的JavaScript,也保持了单线程的特点。单线程的好处是什么?不会阻塞后续请求的响应。(额,似乎不够具体)举个例子,假设你开了家饭店,雇了3个服务员和3个大厨,客人来了服务员接待点餐,点餐后等待大厨做好饭菜,传给服务员,服务员在拿给客人,然后服务员才能接待下一位客人,假如同时来了4位客人,那必须有一位客人等待,以上是多线程并发的情景。单线程是什么样呢,我就需要一位服务员(作为老板的你好开心,少发了很多工资),客人来了服务员接待点餐,点餐后把客人点的菜通知给大厨,大厨就开始做菜,与此同时服务员可以接待下一位客人,而不必等待大厨把菜做好,等大厨把菜做好后就会通知服务员,服务员再把菜端给客人,对应的就是非阻塞请求,就是大厨在做菜过程中(类比操作系统执行I/O操作)服务员不会失去对其它客人请求的响应(类比为服务器可以处理后续的请求,不会占用过多的CPU资源)。从2个不同模式的点菜过程就可以看出node单线程带来的好处啦。

一个静态资源服务器要实现的功能

和需求的对话

假设你作为一个node新手,来想象下你和需求方的的一段对话:
需求方:当我访问一个url时,如果是文件,返回对应文件内容,如果是目录,返回文件列表,并且文件列表是可点击的。
你:这个没问题。(内心活动:基本功能必须满足)
需求方:还有最好有缓存功能,不希望每次都请求服务器。
你:(停顿半秒)好的。(内心活动:缓存一般网站都支持,是要设置头标签啥的,具体方法要搜索啦,可以搞得定)

简单的流程

根据需求,你开始写代码之前,简单梳理了下程序的流程:
(1)在本地根据指定端口启动一个http server,等待着来自客户端的请求
(2)当请求抵达时,根据请求的url,以设置的静态文件目录为base,映射得到文件位置
(3)检查文件是否存在
(4)如果文件不存在,返回404状态码,发送not found页面到客户端
(5)如果文件存在:
* 打开文件待读取(要考虑缓存的实现)
* 设置response header
* 发送文件到客户端
(6)等待来自客户端的下一个请求

代码实现

基本功能的代码

//config.js
module.exports = {
    root:process.cwd(),
    hostname:"127.0.0.1",
    port:"9876"
}
//dir.tpl
 <!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>{{title}}</title>
   </head>
   <body>
      {{#each files}} 
        <a href="{{../dir}}/{{this}}">{{this}}</a>
      {{/each}}
   </body>
   </html>
//app.js
const http = require('http');
const config = require('./config/config');
const path = require('path')
const fs =require('fs');
const Handlebars = require('handlebars') 
const tplPath = path.join(__dirname,'./template/dir.tpl');
const source =fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString())

const server = http.createServer((req,res)=>{
    //将客户端当前文件夹路径与请求的url拼接起来
    const filePath = path.join( config.root,req.url,) 
    //判断目录是否存在
    fs.stat(filePath,(err,stats) =>{
        if(err){ 
            res.statusCode = 404;
            res.setHeader('Content-Type','text/plain');
            res.end(`${filePath} is not a directory or file`);  
            return;
        }
        //如果请求路径对应是文件还是文件夹
        if(stats.isFile()){
            res.statusCode = 200;
            res.setHeader('Content-Type','text/html');
            fs.createReadStream(filePath).pipe(res);
        }else if(stats.isDirectory()){
            fs.readdir(filePath,(err,files)=>{
                res.statusCode =200;
                res.setHeader('Content-Type','text/html');
                const dir =path.relative(config.root,filePath);
                const data ={
                    title:path.basename(filePath),
                    dir:dir ? `/${dir}`:'',
                    files
                }
                res.end(template(data))
            })
        }
    })
});

server.listen(config.port,config.hostname,()=>{
    console.log(`Servser started at ${config.hostname}:${config.port}`)
});

到这里,就实现了静态资源服务器的基本功能,这里文件列表的渲染采用了handlebars模板引擎。我们用到handlebars模板引擎的流程是:
(1)拿到模板文件
(2使用handbars的compile()方法将模板文件编译成template
(3)将数据传给template,返回html。
这里只用到这三个方法,其它api可以去handlebars模板引擎官网看看。
我们可以看到app.js里还是有很多回调,我们可以用util模块promisify把回调的方式改成异步调用。
(1)引用对应的包

const promisify = require('util').promisify;

(2)然后将回调的方法变成异步的,举例:

const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);

(3)用await关键字调用,有一个要注意的是await必须写在async修饰的function里。

const stats = await  stat(filePath)
 const files = await readdir(filePath);

缓存功能的代码

先梳理下客户端发起请求,浏览器响应的流程(是否使用缓存)。
对应流程图
常见几个缓存头:

  • Experies:返回绝对时间校验,涉及到时区等,很少用。
  • Cache-Control:返回相对时间,相对上次请求的秒数,用Max-age表示
  • If-Modified-Since/Last-Modified:服务器时间校验
  • If-None-Match/Etag:服务器哈希校验
//cache.js
const { cache } = require('../config/config')

function refreshRes(stats, res) {
    const { maxAge, expires, cacheControl, lastModified, etag } = cache;
    if (expires) {
        res.setHeader('Expires', (new Date(Date.now() + maxAge * 1000)).toUTCString());
    }
    if (cacheControl) {
        res.setHeader('Cache-Control', `public,max-age=${maxAge}`);
    }
    if (lastModified) {
        res.setHeader('Last-Modified', stats.mtime.toUTCString());
    }
    if (etag) {
        res.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`);
    }
}

module.exports = function isFresh(stats, req, res) {
    refreshRes(stats, res);
    const lastModified = req.headers['if-modified-since'];
    const etag = req.headers['if-none-match'];
    if (!lastModified && !etag) {
        return false;
    }

    if (lastModified && lastModified !== res.getHeader('Last-Modified')) {
        return false;
    }

    if (etag && etag !== res.getHeader('ETag')) {
        return false;
    }
    console.log("refreshRes执行完毕");
    return true;
}
//判断是否过期

 if(isFresh(stats,req,res)){
    res.statusCode = 304;
    res.end();
    return;
   }

总结

这个项目主要涉及到常用模块http,fs,其它的就是一些辅助的工具模块。这个静态服务器只有最基本的功能,还有一些功能,例如文件类型的判断,压缩等都没有实现,下篇文章继续更新。
涉及api概览

猜你喜欢

转载自www.cnblogs.com/JessicaIsEvolving/p/9455056.html