簡單的靜態(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),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(`<!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.useapp.listen這些express提供的API,其實我們點進(jìn)去express的源碼就可以發(fā)現(xiàn),其實req、res是繼承 http.IncomingMessage 和 http.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通信過程存在相互的reqest和response。那么中間件的作用就是接受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)于
commonJS和ES 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)驗不足,加上個人的水平有限,如果有錯誤可以指出一起共同探討!請大家多多指教~