构建文件管理器(0x00: 静态资源服务器、中间件)

简单的静态资源服务器实现

初始版本0x00

import path, { extname } from 'path';
import http from 'http';
import url from 'url';

const documentRoot = 'C:\\Code\\exp';

const server = http.createServer((req, res) => {

  const visitPath = path.join(documentRoot, decodeURI(req.url || ''));

  fs.stat(visitPath, (err, stats) => {
    if (err) {
      res.statusCode = 500;
      res.end(err.message);
      return;
    }
    if (stats.isDirectory()) {
      fs.readdir(visitPath, (err, fileList) => {
        if (err) {
          res.statusCode = 500;
          res.end(err.message);
          return;
        }
        res.setHeader('Content-Type', 'text/html; charset=utf8');
        res.write(`<!DOCTYPE html>
          <html>
            <body>
              <p>${req.url} 有 ${fileList.length} 个文件</p>
              <ul>
                <li><a href="..">..</a></li>
                ${fileList.map(x => `<li><a href="${path.join(req.url || '/', x)}">${x}</a></li>`).join('\r\n')}
              </ul>
            </body>
          </html>
        `);
        res.end();
      });
    }
    else {
      const extName = path.extname(visitPath);
      if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
        res.setHeader('Content-Type', 'image/' + extName.slice(1));
      }
      else {
        res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
      }
      res.statusCode = 200;
      fs.createReadStream(visitPath).pipe(res);
    }
  });
});

server.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
  • 目的在于搭建静态服务器类似功能的服务器,如拥有文件进入回退、文件下载、图片预览等功能。
  • 代码实现
    • http.createServer创建一个http服务器,用于处理浏览器的请求。
    • server.listen(9527)指定http服务器监听9527端口,并处理该端口接收到的request
    • documentRoot 指定用户访问到的服务器的静态资源入口
    • path.join()接口文档,是智能的将你传入的参数url拼接起来成为合法的url地址。decodeURI(req.url)目的是拿到用户请求的url,并且考虑到有中文的情况将其decodeURI
    • fs.stat()文档包含文件的信息,是否是文件夹,等。
    • 检测访问地址是否在根目录(documentRoot)存在。存在则检测是否是文件夹,如果是文件夹就返回一个html文档,列表循环出所有文件。
    • fs.readdir 读取文件夹内容 文档
    • 如果不是文件夹,那么获取req.url的扩展名path.extname(visitPath),检测是否是图片,如果是,那么就将response头添加内容标记Content-Type: image/png,如果不是,则response头添加标记Content-Disponsition文档,表示文件以附件的方式下载到本地。
      fs.createReadStream(visitPath).pipe(res), visitPath是当前文件的路径,基于这个文件创建读取流,通过pipe管道的方式去输出到一个写入流res。这里涉及到一个流的概念。一般我们通过res去写入流是调用res.write等方法。其实本质上也是指定了一个流。这里的做法是,我通过某个文件创建一个读取流,那么这个流就是文件本身的所有数据。管道pipe也就是指定输入和输出流。这里指定了写入流就是res。所以会表现为客户端的就是文件内容本身。

到此,一个实现了基本功能的静态服务器就搭建完成了。但是也注意到有不少可以优化的地方

  • 社区是否存在基于http server的优秀实现?
  • 什么是中间件,中间件的好处,你的功能,社区有优秀的实践吗?
  • callback hell 回调地狱的问题
  • node开发中,sync和async的优劣?为什么要存在异步和同步的代码?

版本0x01: 社区优秀的server实现,expresskoa...

import fs from 'fs';
import path, { extname } from 'path';
import http from 'http';
import express from 'express';

const documentRoot = 'C:\\Code\\exp';

const app = express();

app.use((req, res) => {
  const visitPath = path.join(documentRoot, decodeURI(req.url || ''));

  fs.stat(visitPath, (err, stats) => {
    if (err) {
      res.statusCode = 500;
      res.end(err.message);
      return;
    }
    if (stats.isDirectory()) {
      fs.readdir(visitPath, (err, fileList) => {
        if (err) {
          res.statusCode = 500;
          res.end(err.message);
          return;
        }
        res.setHeader('Content-Type', 'text/html; charset=utf8');
        res.write(`<!DOCTYPE html>
          <html>
            <body>
              <p>${req.url} 有 ${fileList.length} 个文件</p>
              <ul>
                <li><a href="..">..</a></li>
                ${fileList.map(x => `<li><a href="${path.join(req.url || '/', x)}">${x}</a></li>`).join('\r\n')}
              </ul>
            </body>
          </html>
        `);
        res.end();
      });
    }
    else {
      const extName = path.extname(visitPath);
      if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
        res.setHeader('Content-Type', 'image/' + extName.slice(1));
      }
      else {
        res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
      }
      res.statusCode = 200;
      fs.createReadStream(visitPath).pipe(res);
    }
  });
})

app.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
  • 眼尖的伙伴注意到了,这里的代码,只是换成了app.use app.listen这些express提供的API,其实我们点进去express的源码就可以发现,其实req、res是继承 http.IncomingMessagehttp.ServerResponse。这就表示app.use是基于http来实现的。所以,我们之前写的代码直接拿来用,也是可以运行滴。

版本0x02:什么是中间件,中间件的好处,你的功能,社区有优秀的实践吗?

import fs from 'fs';
import path, { extname } from 'path';
import http from 'http';
import express from 'express';
import serveIndex from 'serve-index';

const documentRoot = 'C:\\Code\\exp';

const app = express();

app.use('/static', express.static(documentRoot));
app.use('/static', serveIndex(documentRoot));

app.use('/file', (req, res) => {
  // 访问 /file/document 相当于要访问根目录下的 /document
  const requestPath = req.path;
  const visitPath = path.join(documentRoot, requestPath.replace(/^\/file/, ''));

  fs.stat(visitPath, (err, stats) => {
    if (err) {
      res.statusCode = 500;
      res.end(err.message);
      return;
    }
    if (stats.isDirectory()) {
      fs.readdir(visitPath, (err, fileList) => {
        if (err) {
          res.statusCode = 500;
          res.end(err.message);
          return;
        }
        res.json({ code: 0, message: 'ok', data: { fileList } });
      });
    }
    else {
      const extName = path.extname(visitPath);
      if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
        res.setHeader('Content-Type', 'image/' + extName.slice(1));
      }
      else {
        res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
      }
      res.statusCode = 200;
      fs.createReadStream(visitPath).pipe(res);
    }
  });
});

app.use((req, res) => {
  res.send('It works');
});

app.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
  • 什么是中间件?

中间件(英语:Middleware),又译中间件、中介层,是一类提供系统软件应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。
中间件在现代信息技术应用框架如Web服务面向服务的体系结构等中应用比较广泛,如数据库、Apache的Tomcat,IBM公司的WebSphere,BEA公司的WebLogic应用服务器,东方通的Tong系列中间件等都属于中间件。
严格来讲,中间件技术已经不局限于应用服务器、数据库服务器。围绕中间件,Apache组织、IBM、Oracle(BEA)、微软各自发展出了较为完整的软件产品体系。(Microsoft Servers微软公司的服务器产品)。

个人浅显的理解:在node应用中:http通信过程存在相互的reqestresponse。那么中间件的作用就是接受requset,实现相应的处理,然后再输出你的期望输出。假如: 我们有个用作身份认证的中间件。这个中间件检测请求头是否存在某个特征值,如果该特征值合法,那么我正常返回数据,如果特征值不合法或者不存在,那么我可以拒绝这个请求。这就是简单的一个中间件的作用。实际上中间件可以做的东西很多,社区也有很多优秀的实现。开发的过程不妨去查看社区是否有star多的实现。不必再造轮子。当然为了学习或贡献,你可以自己自己编写中间件。

  • 社区的优秀实现
app.use('/static', express.static(documentRoot));
app.use('/static', serveIndex(documentRoot));

上文的代码中这两行就是express已经为你实现好的中间件,当浏览器命中/static这个url的时候,表示客户端想要访问静态资源。这时候,我们定义了这个中间件提供静态资源服务去正确响应请求。express.static() serveIndex()分别是静态文件访问中间件和文件index中间件。其实也就是版本0x00实现的功能。代码中的

const extName = path.extname(visitPath);
      if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
        res.setHeader('Content-Type', 'image/' + extName.slice(1));
      }
      else {
        res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
      }
      res.statusCode = 200;
      fs.createReadStream(visitPath).pipe(res);

这一块功能,其实express.static已经实现了,这里是重复的,后面版本会将其删除。

版本0x03: callBackHell

const fs = require('fs');
const path = require('path');
const documentRoot = 'C:\\Code\\exp';
const express = require('express');
const pify = require('pify');
const app = express();

// 处理静态资源请求
app.use('/static', express.static(documentRoot));

// 处理目录读取
app.use('/file', async(req, res) => {
  // 访问 /file/document 相当于要访问根目录下的 /document
  const requestPath = req.path;
  const visitPath = path.join(documentRoot, requestPath.replace(/^\/file/, ''));
  try {
    const stats = await pify(fs.stat)(visitPath)
    if (stats.isDirectory()) {
      const { err, fileList } = await pify(fs.readdir)(visitPath)
      res.json({ code: 0, message: 'ok', data: { fileList } });
    } else {
      res.json({ code: 9001, message: 'Not a directory' });
    }
  } catch (err) {
    res.status(500).end(err.massage)
    return
  }
});

app.use('/sync', (req, res) => {
  const start = +new Date();
  while(+new Date() - start < 10000) {}
  res.send('finish');
});

app.use('/async', (req, res) => {
  setTimeout(() => res.send('finish'), 10000);
});

// 直出前端视图
app.use((req, res) => {
  res.send('It works!!!');
});


app.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
  • 关于commonJSES Module的问题。这里的变更是因为我用了hotnode热重载的一个包,但是这个包不支持ES module语法。所以我将其改成了commonJS的写法。但是社区其实有更好用的热重载包,对ES Module语法也是支持的。ts-node-dev.推荐使用,还可以支持调试npx ts-node-dev --inspect -- server.ts
  • 同步和异步的问题
app.use('/sync', (req, res) => {
  const start = +new Date();
  while(+new Date() - start < 10000) {}
  res.send('finish');
});

app.use('/async', (req, res) => {
  setTimeout(() => res.send('finish'), 10000);
});

这段代码其实是非相关的,但是这里我留着是觉得这里很重要。node是单线程异步IO的。所以,我们写node的代码的时候,为了利用node的高性能,我们实际上IO操作等是一定要用异步操作的。接下来说说为什么,代码里面的两个例子。由于node是单线程的,所以当出现阻塞的时候,那么就不能马上处理新的请求。/sync是同步执行的。会阻塞10s钟,/async是异步执行的,setTimeout也是10s的等待,但是不会阻塞,原因是node会先将它丢到一个等待队列,等node的执行栈空了才进行回调。这个等待过程node是可以继续处理请求的。此处代码可以做个简单的小实验:启动浏览器A(客户端A)先去调用'localhost:9527/sync', 再去启动浏览器B(客户端B) 调用静态资源服务(localhost:9527/static/本地存在的一个文件如‘我的头像.png’)。这个是否你会发现无论是客户端A和客户端B都是在等待。因为客户端A阻塞了请求。所以,编写IO操作、文件操作等时间成本较长的操作,一定要是异步执行。而如果是先调用/async再去静态资源请求‘我的头像.png’,你会发现,服务端马上就响应并返回该图片。

  • callback hell问题:Promise
    Promise就是避免回调地狱而产生的。
try {
    const stats = await pify(fs.stat)(visitPath)
    if (stats.isDirectory()) {
      const { err, fileList } = await pify(fs.readdir)(visitPath)
      res.json({ code: 0, message: 'ok', data: { fileList } });
    } else {
      res.json({ code: 9001, message: 'Not a directory' });
    }
  } catch (err) {
    res.status(500).end(err.massage)
    return
  }

pify是个让node的异步操作Promise化的一个库。这样我们就可以实现编写同步代码,享受异步执行带来的高性能。
看着更简洁,更易读。

  • 整片代码可以看出来已经拆分了几个中间件。分别是/static静态文件访问、/file用作api数据返回、其他地址直出前端视图。
下一篇文章将讲述:如何从空文件夹一步步搭建前端项目(我这里是react + ts),如何将自己搭建的服务器用于自己的前端项目。

PS:技术博客的编写经验不足,加上个人的水平有限,如果有错误可以指出一起共同探讨!请大家多多指教~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354