摘要:一個(gè)包括文件緩存?zhèn)鬏攭嚎s模版引擎類型匹配等功能的靜態(tài)資源服務(wù)器,使用的內(nèi)置模塊實(shí)現(xiàn),可以通過(guò)鏈接訪問(wèn)資源。二使用讀取資源文件我們的目的是搭建一個(gè)靜態(tài)資源服務(wù)器,當(dāng)訪問(wèn)一個(gè)到資源文件或目錄時(shí),我們希望可以得到它。
一個(gè)包括文件緩存、傳輸壓縮、ejs 模版引擎、MIME 類型匹配等功能的 Node 靜態(tài)資源服務(wù)器,使用 Node 的內(nèi)置模塊實(shí)現(xiàn),可以通過(guò)鏈接訪問(wèn)資源。
一、創(chuàng)建 HTTP Server 服務(wù)器
Node 的 http 模塊提供 HTTP 服務(wù)器和客戶端接口,通過(guò)require("http")使用。
先創(chuàng)建一個(gè)簡(jiǎn)單的 http server。配置參數(shù)如下:
// server/config.js module.exports = { root: process.cwd(), host: "127.0.0.1", port: "8877" }
process.cwd()方法返回 Node.js 進(jìn)程的當(dāng)前工作目錄,和 Linus 命令pwd功能一樣,
Node 服務(wù)器每次收到 HTTP 請(qǐng)求后都會(huì)調(diào)用 http.createServer() 這個(gè)回調(diào)函數(shù),每次收一條請(qǐng)求,都會(huì)先解析請(qǐng)求頭作為新的 request 的一部分,然后用新的 request 和 respond 對(duì)象觸發(fā)回調(diào)函數(shù)。以下創(chuàng)建一個(gè)簡(jiǎn)單的 http 服務(wù),先默認(rèn)響應(yīng)的 status 為 200:
// server/http.js const http = require("http") const path = require("path") const config = require("./config") const server = http.createServer((request, response) => { let filePath = path.join(config.root, request.url) response.statusCode = 200 response.setHeader("content-type", "text/html") response.write(`
Hello World!
${filePath}
`) response.end() }) server.listen(config.port, config.host, () => { const addr = `http://${config.host}:${config.port}` console.info(`server started at ${addr}`) })
客戶端請(qǐng)求靜態(tài)資源的地址可以通過(guò)request.url獲得,然后使用 path 模塊拼接資源的路徑。
執(zhí)行$ node server/http.js 后訪問(wèn) http://127.0.0.1:8877/ 后的任意地址都會(huì)顯示該路徑:
每次修改服務(wù)器響應(yīng)內(nèi)容,都需要重新啟動(dòng)服務(wù)器更新,推薦自動(dòng)監(jiān)視更新自動(dòng)重啟的插件supervisor,使用supervisor啟動(dòng)服務(wù)器。
$ npm install supervisor -D $ supervisor server/http.js
二、使用 fs 讀取資源文件
我們的目的是搭建一個(gè)靜態(tài)資源服務(wù)器,當(dāng)訪問(wèn)一個(gè)到資源文件或目錄時(shí),我們希望可以得到它。這時(shí)就需要使用 Node 內(nèi)置的 fs 模塊讀取靜態(tài)資源文件,
使用 fs.stat()讀取文件狀態(tài)信息,通過(guò)回調(diào)中的狀態(tài)stats.isFile()判斷文件還是目錄,并使用fs.readdir()讀取目錄中的文件名
// server/route.js const fs = require("fs") module.exports = function (request, response, filePath){ fs.stat(filePath, (err, stats) => { if (err) { response.statusCode = 404 response.setHeader("content-type", "text/plain") response.end(`${filePath} is not a file`) return; } if (stats.isFile()) { response.statusCode = 200 response.setHeader("content-type", "text/plain") fs.createReadStream(filePath).pipe(response) } else if (stats.isDirectory()) { fs.readdir(filePath, (err, files) => { response.statusCode = 200 response.setHeader("content-type", "text/plain") response.end(files.join(",")) }) } }) }
其中fs.createReadStream()讀取文件流,pipe()是分段讀取文件到內(nèi)存,優(yōu)化高并發(fā)的情況。
修改之前的 http server ,引入上面新建的 route.js 作為響應(yīng)函數(shù):
// server/http.js const http = require("http") const path = require("path") const config = require("./config") const route = require("./route") const server = http.createServer((request, response) => { let filePath = path.join(config.root, request.url) route(request, response, filePath) }) server.listen(config.port, config.host, () => { const addr = `http://${config.host}:${config.port}` console.info(`server started at ${addr}`) })
再次執(zhí)行 $ node server/http.js 如果是文件夾則顯示目錄:
如果是文件則直接輸出:
成熟的靜態(tài)資源服務(wù)器 anywhere,深入理解 nodejs 作者寫的。
三、util.promisify 優(yōu)化 fs 異步
我們注意到fs.stat()和fs.readdir() 都有 callback 回調(diào)。我們結(jié)合 Node 的 util.promisify() 來(lái)鏈?zhǔn)讲僮?,代替地獄回調(diào)。
util.promisify() 只是返回一個(gè) Promise 實(shí)例來(lái)方便異步操作,并且可以和 async/await 配合使用,修改 route.js 中 fs 操作相關(guān)的代碼:
// server/route.js const fs = require("fs") const util = require("util") const stat = util.promisify(fs.stat) const readdir = util.promisify(fs.readdir) module.exports = async function (request, response, filePath) { try { const stats = await stat(filePath) if (stats.isFile()) { response.statusCode = 200 response.setHeader("content-type", "text/plain") fs.createReadStream(filePath).pipe(response) } else if (stats.isDirectory()) { const files = await readdir(filePath) response.statusCode = 200 response.setHeader("content-type", "text/plain") response.end(files.join(",")) } } catch (err) { console.error(err) response.statusCode = 404 response.setHeader("content-type", "text/plain") response.end(`${filePath} is not a file`) } }
因?yàn)?fs.stat()和fs.readdir() 都可能返回 error,所以使用try-catch捕獲。
使用異步時(shí)需注意,異步回調(diào)需要使用 await 返回異步操作,不加 await 返回的是一個(gè) promise,而且 await 必須在async里面使用。
四、添加 ejs 模版引擎
從上面的例子是手工輸入文件路徑,然后返回資源文件?,F(xiàn)在優(yōu)化這個(gè)例子,將文件目錄變成 html 的 a 鏈接,點(diǎn)擊后返回文件資源。
在第一個(gè)例子中使用response.write()插入 HTML 標(biāo)簽,這種方式顯然是不友好的。這時(shí)候就使用模版引擎做到拼接 HTML。
常用的模版引擎有很多,ejs、jade、handlebars,這里的使用ejs:
npm i ejs
新建一個(gè)模版 src/template/index.ejs ,和 html 文件很像:
Node Server <% files.forEach(function(name){ %> <%= name %>
<% }) %>
再次修改 route.js,添加 ejs 模版并ejs.render(),在文件目錄的代碼中傳遞 files、dir 等參數(shù):
// server/route.js const fs = require("fs") const util = require("util") const path = require("path") const ejs = require("ejs") const config = require("./config") // 異步優(yōu)化 const stat = util.promisify(fs.stat) const readdir = util.promisify(fs.readdir) // 引入模版 const tplPath = path.join(__dirname,"../src/template/index.ejs") const sourse = fs.readFileSync(tplPath) // 讀出來(lái)的是buffer module.exports = async function (request, response, filePath) { try { const stats = await stat(filePath) if (stats.isFile()) { response.statusCode = 200 ··· } else if (stats.isDirectory()) { const files = await readdir(filePath) response.statusCode = 200 response.setHeader("content-type", "text/html") // response.end(files.join(",")) const dir = path.relative(config.root, filePath) // 相對(duì)于根目錄 const data = { files, dir: dir ");
重啟動(dòng)$ node server/http.js 就可以看到文件目錄的鏈接:
五、匹配文件 MIME 類型
靜態(tài)資源有圖片、css、js、json、html等, 在上面判斷stats.isFile()后響應(yīng)頭設(shè)置的 Content-Type 都為 text/plain,但各種文件有不同的 Mime 類型列表。
我們先根據(jù)文件的后綴匹配它的 MIME 類型:
// server/mime.js const path = require("path") const mimeTypes = { "js": "application/x-javascript", "html": "text/html", "css": "text/css", "txt": "text/plain" } module.exports = (filePath) => { let ext = path.extname(filePath) .split(".").pop().toLowerCase() // 取擴(kuò)展名 if (!ext) { // 如果沒(méi)有擴(kuò)展名,例如是文件 ext = filePath } return mimeTypes[ext] || mimeTypes["txt"] }
匹配到文件的 MIME 類型,再使用response.setHeader("Content-Type", "XXX")設(shè)置響應(yīng)頭:
// server/route.js const mime = require("./mime") ··· if (stats.isFile()) { const mimeType = mime(filePath) response.statusCode = 200 response.setHeader("Content-Type", mimeType) fs.createReadStream(filePath).pipe(response) }
運(yùn)行 server 服務(wù)器訪問(wèn)一個(gè)文件,可以看到 Content-Type 修改了:
六、文件傳輸壓縮
注意到 request header 中有 Accept—Encoding:gzip,deflate,告訴服務(wù)器客戶端所支持的壓縮方式,響應(yīng)時(shí) response header 中使用 content-Encoding 標(biāo)志文件的壓縮方式。
node 內(nèi)置 zlib 模塊支持文件壓縮。在前面文件讀取使用的是fs.createReadStream(),所以壓縮是對(duì) ReadStream 文件流。示例 gzip,deflate 方式的壓縮:
// server/compress.js const zlib = require("zlib") module.exports = (readStream, request, response) => { const acceptEncoding = request.headers["accept-encoding"] if (!acceptEncoding || !acceptEncoding.match(/(gzip|deflate)/)) { return readStream } else if (acceptEncoding.match(/gzip/)) { response.setHeader("Content-Encoding", "gzip") return readStream.pipe(zlib.createGzip()) } else if (acceptEncoding.match(/deflate/)) { response.setHeader("Content-Encoding", "deflate") return readStream.pipe(zlib.createDeflate()) } }
修改 route.js 文件讀取的代碼:
// server/route.js const compress = require("./compress") ··· if (stats.isFile()) { const mimeType = mime(filePath) response.statusCode = 200 response.setHeader("Content-Type", mimeType) // fs.createReadStream(filePath).pipe(response) + let readStream = fs.createReadStream(filePath) + if(filePath.match(config.compress)) { // 正則匹配:/.(html|js|css|md)/ readStream = compress(readStream,request, response) } readStream.pipe(response) }
運(yùn)行 server 可以看到不僅 response header 增加壓縮標(biāo)志,而且 3K 大小的資源壓縮到了 1K,效果明顯:
七、資源緩存
以上的 Node 服務(wù)都是瀏覽器首次請(qǐng)求或無(wú)緩存狀態(tài)下的,那如果瀏覽器/客戶端請(qǐng)求過(guò)資源,一個(gè)重要的前端優(yōu)化點(diǎn)就是緩存資源在客戶端。緩存有強(qiáng)緩存和協(xié)商緩存:
強(qiáng)緩存在 Request Header 中的字段是 Expires 和 Cache-Control;如果在有效期內(nèi)則直接加載緩存資源,狀態(tài)碼直接是顯示 200。
協(xié)商緩存在 Request Header 中的字段是:
If-Modified-Since(對(duì)應(yīng)值為上次 Respond Header 中的 Last-Modified)
If-None—Match(對(duì)應(yīng)值為上次 Respond Header 中的 Etag)
如果協(xié)商成功則返回 304 狀態(tài)碼,更新過(guò)期時(shí)間并加載瀏覽器本地資源,否則返回服務(wù)器端資源文件。
首先配置默認(rèn)的 cache 字段:
// server/config.js module.exports = { root: process.cwd(), host: "127.0.0.1", port: "8877", compress: /.(html|js|css|md)/, cache: { maxAge: 2, expires: true, cacheControl: true, lastModified: true, etag: true } }
新建 server/cache.js,設(shè)置響應(yīng)頭:
const config = require("./config") function refreshRes (stats, response) { const {maxAge, expires, cacheControl, lastModified, etag} = config.cache; if (expires) { response.setHeader("Expires", (new Date(Date.now() + maxAge * 1000)).toUTCString()); } if (cacheControl) { response.setHeader("Cache-Control", `public, max-age=${maxAge}`); } if (lastModified) { response.setHeader("Last-Modified", stats.mtime.toUTCString()); } if (etag) { response.setHeader("ETag", `${stats.size}-${stats.mtime.toUTCString()}`); // mtime 需要轉(zhuǎn)成字符串,否則在 windows 環(huán)境下會(huì)報(bào)錯(cuò) } } module.exports = function isFresh (stats, request, response) { refreshRes(stats, response); const lastModified = request.headers["if-modified-since"]; const etag = request.headers["if-none-match"]; if (!lastModified && !etag) { return false; } if (lastModified && lastModified !== response.getHeader("Last-Modified")) { return false; } if (etag && etag !== response.getHeader("ETag")) { return false; } return true; };
最后修改 route.js 中的
// server/route.js + const isCache = require("./cache") if (stats.isFile()) { const mimeType = mime(filePath) response.setHeader("Content-Type", mimeType) + if (isCache(stats, request, response)) { response.statusCode = 304; response.end(); return; } response.statusCode = 200 // fs.createReadStream(filePath).pipe(response) let readStream = fs.createReadStream(filePath) if(filePath.match(config.compress)) { readStream = compress(readStream,request, response) } readStream.pipe(response) }
重啟 node server 訪問(wèn)某個(gè)文件,在第一次請(qǐng)求成功時(shí) Respond Header 返回緩存時(shí)間:
一段時(shí)間后再次請(qǐng)求該資源文件,Request Header 發(fā)送協(xié)商請(qǐng)求字段:
以上就是一個(gè)簡(jiǎn)單的 Node 靜態(tài)資源服務(wù)器??梢栽谖业?github NodeStaticServer 上clone這個(gè)項(xiàng)目
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/6697.html
摘要:指定需要處理的路由回調(diào)函數(shù),即請(qǐng)求此路由的處理函數(shù),它可以接收兩個(gè)參數(shù)三個(gè)參數(shù),四個(gè)參數(shù)。如果匹配到自定義的路由,立即執(zhí)行回調(diào)函數(shù),如果處理函數(shù)中沒(méi)有則不再往下執(zhí)行,如果執(zhí)行了會(huì)繼續(xù)向下匹配。 簡(jiǎn)介 Node.js? is a JavaScript runtime built on Chromes V8 JavaScript engine. Node.js uses an event-...
摘要:從零開(kāi)始搭建同構(gòu)應(yīng)用四搭建完善上一篇我們使用了的方式測(cè)試了,這篇文章來(lái)講如何在前文的基礎(chǔ)上搭建一個(gè),實(shí)現(xiàn)真正意義上的。至此,一個(gè)簡(jiǎn)單的框架已經(jīng)搭建完成,剩下的工作就是結(jié)合工作需要,在里面添磚加瓦啦。 從零開(kāi)始搭建React同構(gòu)應(yīng)用(四):搭建Koa Server & 完善SSR 上一篇我們使用了CLI的方式測(cè)試了SSR,這篇文章來(lái)講如何在前文的基礎(chǔ)上搭建一個(gè)Koa Server,實(shí)現(xiàn)真...
摘要:安裝環(huán)境度娘創(chuàng)建前端項(xiàng)目創(chuàng)建基于搭建項(xiàng)目收發(fā)請(qǐng)求請(qǐng)求第三方準(zhǔn)備模塊引入使用設(shè)置請(qǐng)求頭地址獲取的參數(shù)是字符串轉(zhuǎn)整數(shù)接口數(shù)據(jù)請(qǐng)求成功接口數(shù)據(jù)返回?cái)?shù)據(jù)請(qǐng)求獲取請(qǐng)求需要模塊準(zhǔn)備使用 1、安裝node環(huán)境(度娘)2、vue-cli創(chuàng)建前端項(xiàng)目3、創(chuàng)建node 基于[http://www.expressjs.com.cn/][1]搭建node項(xiàng)目 4、node收發(fā)請(qǐng)求 get請(qǐng)求第三方api:...
摘要:目錄一創(chuàng)建項(xiàng)目二配置路由三靜態(tài)資源四模板引擎五結(jié)語(yǔ)是由原班人馬打造的超輕量服務(wù)端框架與相比,除了自由度更高,可以自行引入中間件之外,更重要的是使用了,從而避免了回調(diào)地獄不過(guò)也是因?yàn)榇a升級(jí),所以需要以上的環(huán)境一創(chuàng)建項(xiàng)目手動(dòng)創(chuàng)建一個(gè)項(xiàng)目目錄, 目錄 一、創(chuàng)建項(xiàng)目二、配置路由三、靜態(tài)資源四、模板引擎五、結(jié)語(yǔ) Koa 是由 Express 原班人馬打造的超輕量服務(wù)端框架與 Express 相...
摘要:目錄一創(chuàng)建項(xiàng)目二配置路由三靜態(tài)資源四模板引擎五結(jié)語(yǔ)是由原班人馬打造的超輕量服務(wù)端框架與相比,除了自由度更高,可以自行引入中間件之外,更重要的是使用了,從而避免了回調(diào)地獄不過(guò)也是因?yàn)榇a升級(jí),所以需要以上的環(huán)境一創(chuàng)建項(xiàng)目手動(dòng)創(chuàng)建一個(gè)項(xiàng)目目錄, 目錄 一、創(chuàng)建項(xiàng)目二、配置路由三、靜態(tài)資源四、模板引擎五、結(jié)語(yǔ) Koa 是由 Express 原班人馬打造的超輕量服務(wù)端框架與 Express 相...
閱讀 841·2021-11-15 17:58
閱讀 3641·2021-11-12 10:36
閱讀 3779·2021-09-22 16:06
閱讀 956·2021-09-10 10:50
閱讀 1325·2019-08-30 11:19
閱讀 3309·2019-08-29 16:26
閱讀 928·2019-08-29 10:55
閱讀 3341·2019-08-26 13:48