构建文件管理器(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(`
          
            
              

${req.url} 有 ${fileList.length} 个文件

  • ..
  • ${fileList.map(x => `
  • ${x}
  • `).join('\r\n')}
`); 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实现,express、koa...

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(`
          
            
              

${req.url} 有 ${fileList.length} 个文件

  • ..
  • ${fileList.map(x => `
  • ${x}
  • `).join('\r\n')}
`); 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.IncomingMessage 和 http.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:技术博客的编写经验不足,加上个人的水平有限,如果有错误可以指出一起共同探讨!请大家多多指教~

你可能感兴趣的:(构建文件管理器(0x00: 静态资源服务器、中间件))