以前剛開始學node的時候寫過幾個小項目練手,但都是一開始就用到了express架構,這段時間重學node基礎,想到如何用node實作一個類似于express的架構,于是就想從靜态檔案伺服器開始實作一部分功能,實作了基本的檔案伺服器後我還繼續擴充了路由邏輯處理的功能,用于簡易伺服器背景的實作與搭建
一、建立伺服器
利用http子產品的createServer建立一個伺服器,就是這麼簡單, 如下為app.js啟動檔案的配置
const config = require('./config');
const router = require('./router/router');
const handle = require('./handle');
const http = require('http');
var httpServer = http.createServer(onRequest);
httpServer.listen(config.port || 8888);
httpServer.on('error', (err) => {
console.log(err);
});
function onRequest(request, response) {
console.log('request received');
router(request, response, handle);
}
.config為配置檔案,用于導出配置資訊,便于配置資訊的統一修改,定義了伺服器的端口位置,和支援的MINE類型(可自行擴充)
module.exports = {
port: 8888,
ip: '127.0.0.1',
mine: {
html: 'text/html',
js: 'text/javascript',
css: 'text/css',
gif: 'image/gif',
jpg: 'image/jpeg',
png: 'image/png',
ico: 'image/icon',
txt: 'text/plain',
json: 'application/json',
xml: 'text/xml',
pdf: 'application/pdf',
default: 'application/octet-stream'
}
};
二、路由的實作
路由子產品主要用于解析請求URL中的路徑,并對不同的路徑實施不用的處理邏輯。
用到的子產品:url 解析請求URL傳回Object對象
path 處理路徑
首先,我們先要實作一個根據path擷取MINE類型的方法
var getFileMine = (pathname) => {
let extname = path.extname(pathname).substr(1);
let mineType = config.mine;
if (mineType.hasOwnProperty(extname)) {
return mineType[extname];
} else {
return false;
}
};
核心處理邏輯就是,首先檢查請求URL的檔案MINE類型,如果是我們配置檔案config.js 中定義的類型之一,則調用routerHandle子產品(稍後會講)的檔案讀取功能去讀取并傳回對應的請求檔案;若URL沒有MINE類型的字尾名,則預設為調用路由處理方法,根據handle.js中配置的handle對象的方法處理;若在handle對象中沒有對應的路由處理方法,則傳回404 Not Found.
完整的實作代碼為:
const url = require('url');
const path = require('path');
const routerHandle = require('./routerHandle');
const config = require('../config');
module.exports = (function() {
var getFileMine = (pathname) => {
let extname = path.extname(pathname).substr(1);
let mineType = config.mine;
if (mineType.hasOwnProperty(extname)) {
return mineType[extname];
} else {
return false;
}
};
return function(request, response, handle) {
let pathname = url.parse(request.url).pathname;
let mine = getFileMine(pathname);
pathname = decodeURI(pathname);
if(mine) {
routerHandle.static(request, response, pathname, mine);
} else if (typeof handle[pathname] === 'function') {
handle[pathname](request, response);
} else {
response.writeHead(404, {'Content-Type': 'text/plain'});
response.write('404 Not Found');
response.end();
}
};
})();
三、路由handle的實作
最後就是對于不同的路由調用不用等handle來處理了,這裡我在routerHandle.js中定義了一個static方法專門用來處理靜态檔案的讀取傳回操作
exports.static = (request, response, pathname, mine) => {
pathname = path.join('./static', path.normalize(pathname.replace(/\.\./g, '')));
let promise = new Promise((resolve, reject) => {
fs.exists(pathname, (exists) => {
if(!exists) {
reject();
} else {
resolve();
}
});
});
promise.then(() => {
response.writeHead(200, {'Content-Type': mine});
let readStream = fs.createReadStream(pathname);
readStream.on('error', (err) => {
serverException(response); //自定義方法,處理500錯誤
});
readStream.pipe(response);
}).catch(() => {
notFoundException(response); //自定義方法,處理404錯誤
});
};
然後,為了提高相容性,我對于 '/' 路徑的請求做了一個重定向處理,預設通路 ./static 檔案夾下的index.html檔案
exports.start = (request, response) => {
let redirect = 'http://' + request.headers.host + '/index.html';
response.writeHead(301, { 'location': redirect});
response.end();
};
至此,一個基本的靜态檔案伺服器就已經實作了。
對于,自定義路由,在handle.js檔案中require自己建立的路由檔案(建議放在router檔案夾下),并通過handle對象注冊自定義路由檔案中自定義的功能(如:handle['/test'] = myRouter.myTest;)
const routerHandle = require('./router/routerHandle');
const myRouter = require('./router/myRouter');
// 1. 添加自定義子產品
let handle = {};
handle['/'] = routerHandle.start;
// 2. 為自定義子產品定義路由
handle['/test'] = myRouter.myTest;
module.exports = handle;
以上代碼,我建立了一個myRouter.js檔案用于放置自定義的路由方法,并通過handle對象擴充這一路由檔案下的myTest方法,如果要添加其他方法,可以效仿。