一個包括文件緩存、傳輸壓縮、ejs 模版引擎、MIME 類型匹配等功能的 Node 靜態資源服務器,使用 Node 的內置模塊實現,能夠經過連接訪問資源。javascript
Node 的 http 模塊提供 HTTP 服務器和客戶端接口,經過require('http')
使用。css
先建立一個簡單的 http server。配置參數以下:html
// 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:java
// 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><h1>Hello World! </h1><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
執行$ node server/http.js
後訪問 http://127.0.0.1:8877/ 後的任意地址都會顯示該路徑:git
每次修改服務器響應內容,都須要從新啓動服務器更新,推薦自動監視更新自動重啓的插件supervisor,使用supervisor啓動服務器。github
$ npm install supervisor -D
$ supervisor server/http.js
複製代碼
咱們的目的是搭建一個靜態資源服務器,當訪問一個到資源文件或目錄時,咱們但願能夠獲得它。這時就須要使用 Node 內置的 fs 模塊讀取靜態資源文件,npm
使用 fs.stat()
讀取文件狀態信息,經過回調中的狀態stats.isFile()
判斷文件仍是目錄,並使用fs.readdir()
讀取目錄中的文件名json
// 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 做者寫的。
咱們注意到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 %>"> <%= 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
就能夠看到文件目錄的連接:
靜態資源有圖片、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 方式的壓縮:
// 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 中的字段是:
若是協商成功則返回 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 靜態資源服務器。能夠在個人 github NodeStaticServer 上clone這個項目