您好,登錄后才能下訂單哦!
小編給大家分享一下Node如何搭建一個靜態資源服務器,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
使用 Node 的內置模塊,創建一個可以訪問目錄的靜態資源服務器,支持fs文件讀取,資源壓縮與緩存等。
一、創建 HTTP Server 服務器
Node 的 http 模塊提供 HTTP 服務器和客戶端接口,通過 require('http')
使用。
先創建一個簡單的 http server。配置參數如下:
// server/config.js module.exports = { root: process.cwd(), host: '127.0.0.1', port: '8877' }
process.cwd()方法返回 Node.js 進程的當前工作目錄,和 Linus 命令 pwd
功能一樣,
Node 服務器每次收到 HTTP 請求后都會調用 http.createServer() 這個回調函數,每次收一條請求,都會先解析請求頭作為新的 request 的一部分,然后用新的 request 和 respond 對象觸發回調函數。以下創建一個簡單的 http 服務,先默認響應的 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(`<html><body><h2>Hello World! </h2><p>${filePath}</p></body></html>`) response.end() }) server.listen(config.port, config.host, () => { const addr = `http://${config.host}:${config.port}` console.info(`server started at ${addr}`) })
客戶端請求靜態資源的地址可以通過 request.url
獲得,然后使用 path 模塊拼接資源的路徑。
執行 $ node server/http.js
后訪問 http://127.0.0.1 :8877/ 后的任意地址都會顯示該路徑:
每次修改服務器響應內容,都需要重新啟動服務器更新,推薦自動監視更新自動重啟的插件supervisor,使用supervisor啟動服務器。
$ npm install supervisor -D $ supervisor server/http.js
二、使用 fs 讀取資源文件
我們的目的是搭建一個靜態資源服務器,當訪問一個到資源文件或目錄時,我們希望可以得到它。這時就需要使用 Node 內置的 fs 模塊讀取靜態資源文件,
使用 fs.stat()
讀取文件狀態信息,通過回調中的狀態 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()
是分段讀取文件到內存,優化高并發的情況。
修改之前的 http server ,引入上面新建的 route.js 作為響應函數:
// 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}`) })
再次執行 $ node server/http.js
如果是文件夾則顯示目錄:
如果是文件則直接輸出:
成熟的靜態資源服務器 anywhere,深入理解 nodejs 作者寫的。
三、util.promisify 優化 fs 異步
我們注意到 fs.stat()
和 fs.readdir()
都有 callback 回調。我們結合 Node 的 util.promisify() 來鏈式操作,代替地獄回調。
util.promisify 只是返回一個 Promise 實例來方便異步操作,并且可以和 async/await 配合使用,修改 route.js 中 fs 操作相關的代碼:
// 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`) } }
因為 fs.stat()
和 fs.readdir()
都可能返回 error,所以使用 try-catch 捕獲。
使用異步時需注意,異步回調需要使用 await 返回異步操作,不加 await 返回的是一個 promise,而且 await 必須在async里面使用。
四、添加模版引擎
從上面的例子是手工輸入文件路徑,然后返回資源文件。現在優化這個例子,將文件目錄變成 html 的 a 鏈接,點擊后返回文件資源。
在第一個例子中使用 response.write()
插入 HTML 標簽,這種方式顯然是不友好的。這時候就使用模版引擎做到拼接 HTML。
常用的模版引擎有很多,ejs、jade、handlebars,這里的使用ejs:
npm i ejs
新建一個模版 src/template/index.ejs ,和 html 文件很像:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Node Server</title> </head> <body> <% files.forEach(function(name){ %> <a href="../<%= dir %>/<%= name %>" rel="external nofollow" > <%= name %></a><br> <% }) %> </body> </html>
再次修改 route.js,添加 ejs 模版并 ejs.render()
,在文件目錄的代碼中傳遞 files、dir 等參數:
// server/route.js const fs = require('fs') const util = require('util') const path = require('path') const ejs = require('ejs') const config = require('./config') // 異步優化 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) // 讀出來的是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) // 相對于根目錄 const data = { files, dir: dir ? `${dir}` : '' // path.relative可能返回空字符串() } const template = ejs.render(sourse.toString(),data) response.end(template) } } catch (err) { response.statusCode = 404 ··· } }
重啟動 $ node server/http.js
就可以看到文件目錄的鏈接:
五、匹配文件 MIME 類型
靜態資源有圖片、css、js、json、html等,
在上面判斷 stats.isFile()
后響應頭設置的 Content-Type 都為 text/plain,但各種文件有不同的 Mime 類型列表。
我們先根據文件的后綴匹配它的 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() // 取擴展名 if (!ext) { // 如果沒有擴展名,例如是文件 ext = filePath } return mimeTypes[ext] || mimeTypes['txt'] }
匹配到文件的 MIME 類型,再使用 response.setHeader('Content-Type', 'XXX')
設置響應頭:
// 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) }
運行 server 服務器訪問一個文件,可以看到 Content-Type 修改了:
六、文件傳輸壓縮
注意到 request header 中有 Accept—Encoding:gzip,deflate,告訴服務器客戶端所支持的壓縮方式,響應時 response header 中使用 content-Encoding 標志文件的壓縮方式。
node 內置 zlib 模塊支持文件壓縮。在前面文件讀取使用的是 fs.createReadStream()
,所以壓縮是對 ReadStream 文件流。示例 gzip,deflate 方式的壓縮:
最常用文件壓縮,gzip等,使用,對于文件是用ReadStream文件流進行讀取的,所以對ReadStream進行壓縮:
// server/compress.js const zlib = require('zlib') module.exports = (readStream, request, response) => { const acceptEncoding = request.headers['accept-encoding'] if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) { return readStream } else if (acceptEncoding.match(/\bgzip\b/)) { response.setHeader("Content-Encoding", 'gzip') return readStream.pipe(zlib.createGzip()) } else if (acceptEncoding.match(/\bdeflate\b/)) { 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) }
運行 server 可以看到不僅 response header 增加壓縮標志,而且 3K 大小的資源壓縮到了 1K,效果明顯:
七、資源緩存
以上的 Node 服務都是瀏覽器首次請求或無緩存狀態下的,那如果瀏覽器/客戶端請求過資源,一個重要的前端優化點就是緩存資源在客戶端。 緩存有強緩存和協商緩存 :
強緩存在 Request Header 中的字段是 Expires 和 Cache-Control;如果在有效期內則直接加載緩存資源,狀態碼直接是顯示 200。
協商緩存在 Request Header 中的字段是:
If-Modified-Since(對應值為上次 Respond Header 中的 Last-Modified)
If-None—Match(對應值為上次 Respond Header 中的 Etag)
如果協商成功則返回 304 狀態碼,更新過期時間并加載瀏覽器本地資源,否則返回服務器端資源文件。
首先配置默認的 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,設置響應頭:
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 需要轉成字符串,否則在 windows 環境下會報錯 } } 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 訪問某個文件,在第一次請求成功時 Respond Header 返回緩存時間:
一段時間后再次請求該資源文件,Request Header 發送協商請求字段:
以上是“Node如何搭建一個靜態資源服務器”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。