原生node寫一個靜態資源伺服器
myanywhere
用原生node做一個簡易閹割版的anywhere靜態資源伺服器,以提升對node與http的理解。
相關知識
- es6及es7語法
- http的相關網路知識
- 響應頭
- 快取相關
- 壓縮相關
path模組
- path.join拼接路徑
- path.relative
- path.basename
- path.extname
http模組
fs模組
fs.stat函式
使用 fs.stat函式取得stats來獲取檔案或資料夾的引數
- stats.isFile 判斷是否為資料夾
fs.createReadStream(filePath).pipe(res)
檔案可讀流的形式,使讀取效率更高
fs.readdir
...
promisify
- async await
1.實現讀取檔案或資料夾
const http= require('http') const conf = require('./config/defaultConfig') const path = require('path') const fs = require('fs') const server = http.createServer((req, res) => { const filePath = path.join(conf.root, req.url) // http://nodejs.cn/api/fs.html#fs_class_fs_stats fs.stat(filePath, (err, stats) => { if (err) { res.statusCode = 404 res.setHeader('Content-text', 'text/plain') res.end(`${filePath} is not a directoru or file`) } // 如果是一個檔案 if (stats.isFile()) { res.statusCode = 200 res.setHeader('Content-text', 'text/plain') fs.createReadStream(filePath).pipe(res) } else if (stats.isDirectory()) { fs.readdir(filePath, (err, files) => { res.statusCode = 200 res.setHeader('Content-text', 'text/plain') res.end(files.join(',')) }) } }) }) server.listen(conf.port, conf.hostname, () => { const addr = `http:${conf.hostname}:${conf.port}` console.info(`run at ${addr}`) })
2. async await非同步修改
為了避免多層回調出現,我們使用jsasync 和 await來 改造我們的程式碼
router.js
把邏輯相關的程式碼從app.js中抽離出來放入router.js中,分模組開發
const fs = require('fs') const promisify = require('util').promisify const stat = promisify(fs.stat) const readdir = promisify(fs.readdir) module.exports = async function (req, res, filePath) { try { const stats = await stat(filePath) if (stats.isFile()) { res.statusCode = 200 res.setHeader('Content-text', 'text/plain') fs.createReadStream(filePath).pipe(res) } else if (stats.isDirectory()) { const files = await readdir(filePath) res.statusCode = 200 res.setHeader('Content-text', 'text/plain') res.end(files.join(',')) } } catch (error) { res.statusCode = 404 res.setHeader('Content-text', 'text/plain') res.end(`${filePath} is not a directoru or file`) } }
app.js
const http= require('http')
const conf = require('./config/defaultConfig')
const path = require('path')
const route = require('./help/router')
const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url)
route(req, res, filePath)
})
server.listen(conf.port, conf.hostname, () => {
const addr = `http:${conf.hostname}:${conf.port}`
console.info(`run at ${addr}`)
})
3. 完善可點選
上面的工作 已經可以讓我們在頁面中看到資料夾的目錄,但是是文字,不可點選
使用handlebars渲染
引用handlebars
const Handlebars = require('handlebars')
建立模板html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{title}}</title> <style> body { margin: 10px } a { display: block; margin-bottom: 10px; font-weight: 600; } </style> </head> <body> {{#each files}} <a href="{{../dir}}/{{file}}">{{file}}</a> {{/each}} </body> </html>
router.js配置
引用時使用絕對路徑
const tplPath = path.join(__dirname, '../template/dir.html') const source = fs.readFileSync(tplPath, 'utf8') const template = Handlebars.compile(source)
建立資料
data
.... module.exports = async function (req, res, filePath) { try { ... } else if (stats.isDirectory()) { const files = await readdir(filePath) res.statusCode = 200 res.setHeader('Content-text', 'text/html') const dir = path.relative(config.root, filePath) const data = { // path.basename() 方法返回一個 path 的最後一部分 title: path.basename(filePath), // path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'); // 返回: '../../impl/bbb' dir: dir ? `/${dir}` : '', files } console.info(files) res.end(template(data)) } } catch (error) { ... } }
4. mime
新建mime.js檔案
const path = require('path')
const mimeTypes = {
....
}
module.exports = (filePath) => {
let ext = path.extname(filePath).toLowerCase()
if (!ext) {
ext = filePath
}
return mimeTypes[ext] || mimeTypes['.txt']
}
mine.js 根據檔案字尾名來返回對應的mime
5. 壓縮頁面優化效能
對讀取的stream壓縮
在 defaultConfig.js中 新增 compress項
module.exports = {
// process.cwd() 路徑能隨著執行路徑的改變而改變
// process cwd() 方法返回 Node.js 程序當前工作的目錄。
root: process.cwd(),
hostname: '127.0.0.1',
port: 9527,
compress: /\.(html|js|css|md)/
}
編寫壓縮處理 compress
const {createGzip, createDeflate} = require('zlib')
module.exports = (rs, req, res) => {
const acceptEncoding = req.headers['accept-encoding']
if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
return
} else if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip')
return rs.pipe(createGzip())
} else if (acceptEncoding.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate')
return rs.pipe(createGzip())
}
}
/*
match() 方法可在字串內檢索指定的值,或找到一個或多個正則表示式的匹配。
該方法類似 indexOf() 和 lastIndexOf() ,但是它返回指定的值,而不是字串的位置。
*/
router.js中讀取檔案的更改
...
let rs = fs.createReadStream(filePath)
if (filePath.match(config.compress)) {
rs = compress(rs, req, res)
}
rs.pipe(res)
檔案結果compress壓縮後,壓縮率可達 70%
6.處理快取
快取大致原理
使用者請求 本地快取 --no--> 請求資源 --> 協商快取 返回響應
使用者請求 本地快取 --yes--> 判斷換存是否有效 --有效--> 本地快取 --無效--> 協商快取 返回響應
快取header
- expires 老舊 現在不用
- Cache-Control 相對與上次請求的時間
- If-Modified-Since / Last-Modified
- If-None-Match / ETag
cache.js
const {cache} = require('../config/defaultConfig')
function refreshRes(stats, res) {
const { maxAge, expires, cacheControl, lastModified, etag } = cache
if (expires) {
res.setHeader('Expores', (new Date(Date.now() + maxAge * 1000)).toUTCString())
}
if (cacheControl) {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`)
}
if (lastModified) {
res.setHeader('Last-Modified', stats.mtime.toUTCString())
}
if (etag) {
res.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`)
}
}
module.exports = function isFresh(stats, req, res) {
refreshRes(stats, res)
const lastModified = req.headers['if-modified-since']
const etag = req.headers['if-none-match']
if (!lastModified && !etag) {
return false
}
if (lastModified && lastModified !== res.getHeader('Last-Modified')) {
return false
}
if (etag && res.getHeader('ETag').indexOf(etag) ) {
return false
}
return true
}
router.js
// 如果檔案是是新鮮的 不用更改,就設定響應頭 直接返回
if (isFresh(stats, req, res)) {
res.statusCode = 304
res.end()
return
}
7.自動開啟瀏覽器
編寫openUrl.js
const {exec} = require('child_process')
module.exports = url => {
switch (process.platform) {
case 'darwin':
exec(`open ${url}`)
break
case 'win32':
exec(`start ${url}`)
}
}
只支援Windows和 mac系統
在app.js中使用
server.listen(conf.port, conf.hostname, () => {
const addr = `http:${conf.hostname}:${conf.port}`
console.info(`run at ${addr}`)
openUrl(addr)
})
總結
domo不難,但是涉及到的零碎知識點比較多,對底層的node有個更進一步瞭解,也感受到了node在處理網路請求這一塊的強大之處,另外es6和es7的新語法很是強大,以後要多做功課。