使用Node.js實現簡易MVC框架的方法

使用Node.js實現簡易MVC框架的方法
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

在使用Node.js搭建靜態資源伺服器一文中我們完成了伺服器對靜態資源請求的處理,但並未涉及動態請求,目前還無法根據客戶端發出的不同請求而返回個性化的內容。單靠靜態資源豈能撐得起這些複雜的網站應用,本文將介紹如何使用Node處理動態請求,以及如何搭建一個簡易的 MVC 框架。因為前文已經詳細介紹過靜態資源請求如何響應,本文將略過所有靜態部分。

一個簡單的示例

先從一個簡單示例入手,明白在 Node 中如何向客戶端返回動態內容。

假設我們有這樣的需求:

當使用者訪問/actors時返回男演員列表頁

當使用者訪問/actresses時返回女演員列表

可以用以下的程式碼完成功能:


const http = require('http');
const url = require('url');
http.createServer((req, res) => {
const pathName = url.parse(req.url).pathname;
if (['/actors', '/actresses'].includes(pathName)) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
const actors = ['Leonardo DiCaprio', 'Brad Pitt', 'Johnny Depp'];
const actresses = ['Jennifer Aniston', 'Scarlett Johansson', 'Kate Winslet'];
let lists = [];
if (pathName === '/actors') {
lists = actors;
} else {
lists = actresses;
}
const content = lists.reduce((template, item, index) => {
return template   `<p>No.${index 1} ${item}</p>`;
}, `<h1>${pathName.slice(1)}</h1>`);
res.end(content);
} else {
res.writeHead(404);
res.end('<h1>Requested page not found.</h1>')
}
}).listen(9527);

上面程式碼的核心是路由匹配,當請求抵達時,檢查是否有對應其路徑的邏輯處理,當請求匹配不上任何路由時,返回 404。匹配成功時處理相應的邏輯。

simple request

上面的程式碼顯然並不通用,而且在僅有兩種路由匹配候選項(且還未區分請求方法),以及尚未使用資料庫以及模板檔案的前提下,程式碼都已經有些糾結了。因此接下來我們將搭建一個簡易的MVC框架,使資料、模型、表現分離開來,各司其職。

搭建簡易MVC框架

MVC 分別指的是:

M: Model (資料)

V: View (表現)

C: Controller (邏輯)

在 Node 中,MVC 架構下處理請求的過程如下:

請求抵達服務端

服務端將請求交由路由處理

路由通過路徑匹配,將請求導向對應的 controller

controller 收到請求,向 model 索要資料

model 給 controller 返回其所需資料

controller 可能需要對收到的資料做一些再加工

controller 將處理好的資料交給 view

view 根據資料和模板生成響應內容

服務端將此內容返回客戶端

以此為依據,我們需要準備以下模組:

server: 監聽和響應請求

router: 將請求交由正確的controller處理

controllers: 執行業務邏輯,從 model 中取出資料,傳遞給 view

model: 提供資料

view: 提供 html

建立如下目錄:


-- server.js
-- lib
-- router.js
-- views
-- controllers
-- models

server

建立 server.js 檔案:


const http = require('http');
const router = require('./lib/router')();
router.get('/actors', (req, res) => {
res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp');
});
http.createServer(router).listen(9527, err => {
if (err) {
console.error(err);
console.info('Failed to start server');
} else {
console.info(`Server started`);
}
});

先不管這個檔案裡的細節,router是下面將要完成的模組,這裡先引入,請求抵達後即交由它處理。

router 模組

router模組其實只需完成一件事,將請求導向正確的controller處理,理想中它可以這樣使用:


const router = require('./lib/router')();
const actorsController = require('./controllers/actors');
router.use((req, res, next) => {
console.info('New request arrived');
next()
});
router.get('/actors', (req, res) => {
actorsController.fetchList();
});
router.post('/actors/:name', (req, res) => {
actorsController.createNewActor();
});

總的來說,我們希望它同時支援路由中介軟體和非中介軟體,請求抵達後會由 router 交給匹配上的中介軟體們處理。中介軟體是一個可訪問請求物件和響應物件的函式,在中介軟體內可以做的事情包括:

執行任何程式碼,比如新增日誌和處理錯誤等

修改請求 (req) 和響應物件 (res),比如從 req.url 獲取查詢引數並賦值到 req.query

結束響應

呼叫下一個中介軟體 (next)

Note:

需要注意的是,如果在某個中介軟體內既沒有終結響應,也沒有呼叫 next 方法將控制權交給下一個中介軟體, 則請求就會掛起

__非路由中介軟體__通過以下方式新增,匹配所有請求:


router.use(fn);

比如上面的例子:


router.use((req, res, next) => {
console.info('New request arrived');
next()
});

__路由中介軟體__通過以下方式新增,以 請求方法和路徑精確匹配:


router.HTTP_METHOD(path, fn)

梳理好了之後先寫出框架:

/lib/router.js


const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'];
module.exports = () => {
const routes = [];
const router = (req, res) => {
};
router.use = (fn) => {
routes.push({
method: null,
path: null,
handler: fn
});
};
METHODS.forEach(item => {
const method = item.toLowerCase();
router[method] = (path, fn) => {
routes.push({
method,
path,
handler: fn
});
};
});
};

以上主要是給 router 新增了 use、get、post 等方法,每當呼叫這些方法時,給 routes 新增一條 route 規則。

Note:

Javascript 中函式是一種特殊的物件,能被呼叫的同時,還可以擁有屬性、方法。

接下來的重點在 router 函式,它需要做的是:

從req物件中取得 method、pathname

依據 method、pathname 將請求與routes陣列內各個 route 按它們被新增的順序依次匹配

如果與某個route匹配成功,執行 route.handler,執行完後與下一個 route 匹配或結束流程 (後面詳述)

如果匹配不成功,繼續與下一個 route 匹配,重複3、4步驟


const router = (req, res) => {
const pathname = decodeURI(url.parse(req.url).pathname);
const method = req.method.toLowerCase();
let i = 0;
const next = () => {
route = routes[i  ];
if (!route) return;
const routeForAllRequest = !route.method && !route.path;
if (routeForAllRequest || (route.method === method && pathname === route.path)) {
route.handler(req, res, next);
} else {
next();
}
}
next();
};

對於非路由中介軟體,直接呼叫其 handler。對於路由中介軟體,只有請求方法和路徑都匹配成功時,才呼叫其 handler。當沒有匹配上的 route 時,直接與下一個route繼續匹配。

需要注意的是,在某條 route 匹配成功的情況下,執行完其 handler 之後,還會不會再接著與下個 route 匹配,就要看開發者在其 handler 內有沒有主動呼叫 next() 交出控制權了。

在__server.js__中新增一些route:


router.use((req, res, next) => {
console.info('New request arrived');
next()
});
router.get('/actors', (req, res) => {
res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp');
});
router.get('/actresses', (req, res) => {
res.end('Jennifer Aniston, Scarlett Johansson, Kate Winslet');
});
router.use((req, res, next) => {
res.statusCode = 404;
res.end();
});

每個請求抵達時,首先列印出一條 log,接著匹配其他route。當匹配上 actors 或 actresses 的 get 請求時,直接發回演員名字,並不需要繼續匹配其他 route。如果都沒匹配上,返回 404。

在瀏覽器中依次訪問 http://localhost:9527/erwe、http://localhost:9527/actors、http://localhost:9527/actresses 測試一下:

404

network 中觀察到的結果符合預期,同時後臺命令列中也列印出了三條 New request arrived語句。

接下來繼續改進 router 模組。

首先新增一個 router.all 方法,呼叫它即意味著為所有請求方法都新增了一條 route:


router.all = (path, fn) => {
METHODS.forEach(item => {
const method = item.toLowerCase();
router[method](path, fn);
})
};

接著,新增錯誤處理。

/lib/router.js


const defaultErrorHander = (err, req, res) => {
res.statusCode = 500;
res.end();
};
module.exports = (errorHander) => {
const routes = [];
const router = (req, res) => {
...
errorHander = errorHander || defaultErrorHander;
const next = (err) => {
if (err) return errorHander(err, req, res);
...
}
next();
};

server.js


...
const router = require('./lib/router')((err, req, res) => {
console.error(err);
res.statusCode = 500;
res.end(err.stack);
});
...

預設情況下,遇到錯誤時會返回 500,但開發者使用 router 模組時可以傳入自己的錯誤處理函式將其替代。

修改一下程式碼,測試是否能正確執行錯誤處理:


router.use((req, res, next) => {
console.info('New request arrived');
next(new Error('an error'));
});

這樣任何請求都應該返回 500:

error stack

繼續,修改 route.path 與 pathname 的匹配規則。現在我們認為只有當兩字串相等時才讓匹配通過,這沒有考慮到 url 中包含路徑引數的情況,比如:

localhost:9527/actors/Leonardo

router.get('/actors/:name', someRouteHandler);

這條route應該匹配成功才是。

新增一個函式用來將字串型別的 route.path 轉換成正則物件,並存入 route.pattern:


const getRoutePattern = pathname => {
pathname = '^'   pathname.replace(/(\:\w )/g, '\(\[a-zA-Z0-9-\]\ \\s\)')   '$';
return new RegExp(pathname);
};

這樣就可以匹配上帶有路徑引數的url了,並將這些路徑引數存入 req.params 物件:


const matchedResults = pathname.match(route.pattern);
if (route.method === method && matchedResults) {
addParamsToRequest(req, route.path, matchedResults);
route.handler(req, res, next);
} else {
next();
}

const addParamsToRequest = (req, routePath, matchedResults) => {
req.params = {};
let urlParameterNames = routePath.match(/:(\w )/g);
if (urlParameterNames) {
for (let i=0; i < urlParameterNames.length; i  ) {
req.params[urlParameterNames[i].slice(1)] = matchedResults[i   1];
}
}
}

新增個 route 測試一下:


router.get('/actors/:year/:country', (req, res) => {
res.end(`year: ${req.params.year} country: ${req.params.country}`);
});

訪問http://localhost:9527/actors/1990/China試試:

url parameters

router 模組就寫到此,至於查詢引數的格式化以及獲取請求主體,比較瑣碎就不試驗了,需要可以直接使用 bordy-parser 等模組。

現在我們已經建立好了router模組,接下來將 route handler 內的業務邏輯都轉移到 controller 中去。

修改__server.js__,引入 controller:


...
const actorsController = require('./controllers/actors');
...
router.get('/actors', (req, res) => {
actorsController.getList(req, res);
});
router.get('/actors/:name', (req, res) => {
actorsController.getActorByName(req, res);
});
router.get('/actors/:year/:country', (req, res) => {
actorsController.getActorsByYearAndCountry(req, res);
});
...

新建__controllers/actors.js__:


const actorsTemplate = require('../views/actors-list');
const actorsModel = require('../models/actors');
exports.getList = (req, res) => {
const data = actorsModel.getList();
const htmlStr = actorsTemplate.build(data);
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(htmlStr);
};
exports.getActorByName = (req, res) => {
const data = actorsModel.getActorByName(req.params.name);
const htmlStr = actorsTemplate.build(data);
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(htmlStr);
};
exports.getActorsByYearAndCountry = (req, res) => {
const data = actorsModel.getActorsByYearAndCountry(req.params.year, req.params.country);
const htmlStr = actorsTemplate.build(data);
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(htmlStr);
};

在 controller 中同時引入了 view 和 model, 其充當了這二者間的粘合劑。回顧下 controller 的任務:

controller 收到請求,向 model 索要資料
model 給 controller 返回其所需資料
controller 可能需要對收到的資料做一些再加工
controller 將處理好的資料交給 view

在此 controller 中,我們將呼叫 model 模組的方法獲取演員列表,接著將資料交給 view,交由 view 生成呈現出演員列表頁的 html 字串。最後將此字串返回給客戶端,在瀏覽器中呈現列表。

從 model 中獲取資料

通常 model 是需要跟資料庫互動來獲取資料的,這裡我們就簡化一下,將資料存放在一個 json 檔案中。

/models/test-data.json


[
{
"name": "Leonardo DiCaprio",
"birth year": 1974,
"country": "US",
"movies": ["Titanic", "The Revenant", "Inception"]
},
{
"name": "Brad Pitt",
"birth year": 1963,
"country": "US",
"movies": ["Fight Club", "Inglourious Basterd", "Mr. & Mrs. Smith"]
},
{
"name": "Johnny Depp",
"birth year": 1963,
"country": "US",
"movies": ["Edward Scissorhands", "Black Mass", "The Lone Ranger"]
}
]

接著就可以在 model 中定義一些方法來訪問這些資料。

models/actors.js


const actors = require('./test-data');
exports.getList = () => actors;
exports.getActorByName = (name) => actors.filter(actor => {
return actor.name == name;
});
exports.getActorsByYearAndCountry = (year, country) => actors.filter(actor => {
return actor["birth year"] == year && actor.country == country;
});

當 controller 從 model 中取得想要的資料後,下一步就輪到 view 發光發熱了。view 層通常都會用到模板引擎,如 dust 等。同樣為了簡化,這裡採用簡單替換模板中佔位符的方式獲取 html,渲染得非常有限,粗略理解過程即可。

建立 /views/actors-list.js:


const actorTemplate = `
<h1>{name}</h1>
<p><em>Born: </em>{contry}, {year}</p>
<ul>{movies}</ul>
`;
exports.build = list => {
let content = '';
list.forEach(actor => {
content  = actorTemplate.replace('{name}', actor.name)
.replace('{contry}', actor.country)
.replace('{year}', actor["birth year"])
.replace('{movies}', actor.movies.reduce((moviesHTML, movieName) => {
return moviesHTML   `<li>${movieName}</li>`
}, ''));
});
return content;
};

在瀏覽器中測試一下:

test mvc

至此,就大功告成啦!

以上這篇使用Node.js實現簡易MVC框架的方法就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援指令碼之家。

您可能感興趣的文章:

14款NodeJS Web框架推薦10個最優秀的Node.js MVC框架淺談Node.js輕量級Web框架Express4.x使用指南基於node.js express mvc輕量級框架實踐

相關文章

前端開發 最新文章