構(gòu)建文件管理器(0x00: 靜態(tài)資源服務(wù)器、中間件)

簡單的靜態(tài)資源服務(wù)器實現(xiàn)

初始版本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');
  • 目的在于搭建靜態(tài)服務(wù)器類似功能的服務(wù)器,如擁有文件進(jìn)入回退、文件下載、圖片預(yù)覽等功能。
  • 代碼實現(xiàn)
    • http.createServer創(chuàng)建一個http服務(wù)器,用于處理瀏覽器的請求。
    • server.listen(9527)指定http服務(wù)器監(jiān)聽9527端口,并處理該端口接收到的request
    • documentRoot 指定用戶訪問到的服務(wù)器的靜態(tài)資源入口
    • path.join()接口文檔,是智能的將你傳入的參數(shù)url拼接起來成為合法的url地址。decodeURI(req.url)目的是拿到用戶請求的url,并且考慮到有中文的情況將其decodeURI
    • fs.stat()文檔包含文件的信息,是否是文件夾,等。
    • 檢測訪問地址是否在根目錄(documentRoot)存在。存在則檢測是否是文件夾,如果是文件夾就返回一個html文檔,列表循環(huán)出所有文件。
    • fs.readdir 讀取文件夾內(nèi)容 文檔
    • 如果不是文件夾,那么獲取req.url的擴展名path.extname(visitPath),檢測是否是圖片,如果是,那么就將response頭添加內(nèi)容標(biāo)記Content-Type: image/png,如果不是,則response頭添加標(biāo)記Content-Disponsition文檔,表示文件以附件的方式下載到本地。
      fs.createReadStream(visitPath).pipe(res), visitPath是當(dāng)前文件的路徑,基于這個文件創(chuàng)建讀取流,通過pipe管道的方式去輸出到一個寫入流res。這里涉及到一個流的概念。一般我們通過res去寫入流是調(diào)用res.write等方法。其實本質(zhì)上也是指定了一個流。這里的做法是,我通過某個文件創(chuàng)建一個讀取流,那么這個流就是文件本身的所有數(shù)據(jù)。管道pipe也就是指定輸入和輸出流。這里指定了寫入流就是res。所以會表現(xiàn)為客戶端的就是文件內(nèi)容本身。

到此,一個實現(xiàn)了基本功能的靜態(tài)服務(wù)器就搭建完成了。但是也注意到有不少可以優(yōu)化的地方

  • 社區(qū)是否存在基于http server的優(yōu)秀實現(xiàn)?
  • 什么是中間件,中間件的好處,你的功能,社區(qū)有優(yōu)秀的實踐嗎?
  • callback hell 回調(diào)地獄的問題
  • node開發(fā)中,sync和async的優(yōu)劣?為什么要存在異步和同步的代碼?

版本0x01: 社區(qū)優(yōu)秀的server實現(xiàn),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,其實我們點進(jìn)去express的源碼就可以發(fā)現(xiàn),其實req、res是繼承 http.IncomingMessagehttp.ServerResponse。這就表示app.use是基于http來實現(xiàn)的。所以,我們之前寫的代碼直接拿來用,也是可以運行滴。

版本0x02:什么是中間件,中間件的好處,你的功能,社區(qū)有優(yōu)秀的實踐嗎?

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 相當(dāng)于要訪問根目錄下的 /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),又譯中間件、中介層,是一類提供系統(tǒng)軟件應(yīng)用軟件之間連接、便于軟件各部件之間的溝通的軟件,應(yīng)用軟件可以借助中間件在不同的技術(shù)架構(gòu)之間共享信息與資源。中間件位于客戶機服務(wù)器的操作系統(tǒng)之上,管理著計算資源和網(wǎng)絡(luò)通信。
中間件在現(xiàn)代信息技術(shù)應(yīng)用框架如Web服務(wù)、面向服務(wù)的體系結(jié)構(gòu)等中應(yīng)用比較廣泛,如數(shù)據(jù)庫、Apache的Tomcat,IBM公司的WebSphere,BEA公司的WebLogic應(yīng)用服務(wù)器,東方通的Tong系列中間件等都屬于中間件。
嚴(yán)格來講,中間件技術(shù)已經(jīng)不局限于應(yīng)用服務(wù)器、數(shù)據(jù)庫服務(wù)器。圍繞中間件,Apache組織、IBM、Oracle(BEA)、微軟各自發(fā)展出了較為完整的軟件產(chǎn)品體系。(Microsoft Servers微軟公司的服務(wù)器產(chǎn)品)。

個人淺顯的理解:在node應(yīng)用中:http通信過程存在相互的reqestresponse。那么中間件的作用就是接受requset,實現(xiàn)相應(yīng)的處理,然后再輸出你的期望輸出。假如: 我們有個用作身份認(rèn)證的中間件。這個中間件檢測請求頭是否存在某個特征值,如果該特征值合法,那么我正常返回數(shù)據(jù),如果特征值不合法或者不存在,那么我可以拒絕這個請求。這就是簡單的一個中間件的作用。實際上中間件可以做的東西很多,社區(qū)也有很多優(yōu)秀的實現(xiàn)。開發(fā)的過程不妨去查看社區(qū)是否有star多的實現(xiàn)。不必再造輪子。當(dāng)然為了學(xué)習(xí)或貢獻(xiàn),你可以自己自己編寫中間件。

  • 社區(qū)的優(yōu)秀實現(xiàn)
app.use('/static', express.static(documentRoot));
app.use('/static', serveIndex(documentRoot));

上文的代碼中這兩行就是express已經(jīng)為你實現(xiàn)好的中間件,當(dāng)瀏覽器命中/static這個url的時候,表示客戶端想要訪問靜態(tài)資源。這時候,我們定義了這個中間件提供靜態(tài)資源服務(wù)去正確響應(yīng)請求。express.static() serveIndex()分別是靜態(tài)文件訪問中間件和文件index中間件。其實也就是版本0x00實現(xiàn)的功能。代碼中的

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已經(jīng)實現(xiàn)了,這里是重復(fù)的,后面版本會將其刪除。

版本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();

// 處理靜態(tài)資源請求
app.use('/static', express.static(documentRoot));

// 處理目錄讀取
app.use('/file', async(req, res) => {
  // 訪問 /file/document 相當(dāng)于要訪問根目錄下的 /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');
  • 關(guān)于commonJSES Module的問題。這里的變更是因為我用了hotnode熱重載的一個包,但是這個包不支持ES module語法。所以我將其改成了commonJS的寫法。但是社區(qū)其實有更好用的熱重載包,對ES Module語法也是支持的。ts-node-dev.推薦使用,還可以支持調(diào)試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);
});

這段代碼其實是非相關(guān)的,但是這里我留著是覺得這里很重要。node是單線程異步IO的。所以,我們寫node的代碼的時候,為了利用node的高性能,我們實際上IO操作等是一定要用異步操作的。接下來說說為什么,代碼里面的兩個例子。由于node是單線程的,所以當(dāng)出現(xiàn)阻塞的時候,那么就不能馬上處理新的請求。/sync是同步執(zhí)行的。會阻塞10s鐘,/async是異步執(zhí)行的,setTimeout也是10s的等待,但是不會阻塞,原因是node會先將它丟到一個等待隊列,等node的執(zhí)行??樟瞬胚M(jìn)行回調(diào)。這個等待過程node是可以繼續(xù)處理請求的。此處代碼可以做個簡單的小實驗:啟動瀏覽器A(客戶端A)先去調(diào)用'localhost:9527/sync', 再去啟動瀏覽器B(客戶端B) 調(diào)用靜態(tài)資源服務(wù)(localhost:9527/static/本地存在的一個文件如‘我的頭像.png’)。這個是否你會發(fā)現(xiàn)無論是客戶端A和客戶端B都是在等待。因為客戶端A阻塞了請求。所以,編寫IO操作、文件操作等時間成本較長的操作,一定要是異步執(zhí)行。而如果是先調(diào)用/async再去靜態(tài)資源請求‘我的頭像.png’,你會發(fā)現(xiàn),服務(wù)端馬上就響應(yīng)并返回該圖片。

  • callback hell問題:Promise
    Promise就是避免回調(diào)地獄而產(chǎn)生的。
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化的一個庫。這樣我們就可以實現(xiàn)編寫同步代碼,享受異步執(zhí)行帶來的高性能。
看著更簡潔,更易讀。

  • 整片代碼可以看出來已經(jīng)拆分了幾個中間件。分別是/static靜態(tài)文件訪問、/file用作api數(shù)據(jù)返回、其他地址直出前端視圖。
下一篇文章將講述:如何從空文件夾一步步搭建前端項目(我這里是react + ts),如何將自己搭建的服務(wù)器用于自己的前端項目。

PS:技術(shù)博客的編寫經(jīng)驗不足,加上個人的水平有限,如果有錯誤可以指出一起共同探討!請大家多多指教~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容