使用Node.js實(shí)現(xiàn)簡(jiǎn)易MVC框架的方法
在使用Node.js搭建靜態(tài)資源服務(wù)器一文中我們完成了服務(wù)器對(duì)靜態(tài)資源請(qǐng)求的處理,但并未涉及動(dòng)態(tài)請(qǐng)求,目前還無(wú)法根據(jù)客戶端發(fā)出的不同請(qǐng)求而返回個(gè)性化的內(nèi)容。單靠靜態(tài)資源豈能撐得起這些復(fù)雜的網(wǎng)站應(yīng)用,本文將介紹如何使用Node
處理動(dòng)態(tài)請(qǐng)求,以及如何搭建一個(gè)簡(jiǎn)易的 MVC 框架。因?yàn)榍拔囊呀?jīng)詳細(xì)介紹過(guò)靜態(tài)資源請(qǐng)求如何響應(yīng),本文將略過(guò)所有靜態(tài)部分。
一個(gè)簡(jiǎn)單的示例
先從一個(gè)簡(jiǎn)單示例入手,明白在 Node 中如何向客戶端返回動(dòng)態(tài)內(nèi)容。
假設(shè)我們有這樣的需求:
當(dāng)用戶訪問(wèn)/actors
時(shí)返回男演員列表頁(yè)
當(dāng)用戶訪問(wèn)/actresses
時(shí)返回女演員列表
可以用以下的代碼完成功能:
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);
上面代碼的核心是路由匹配,當(dāng)請(qǐng)求抵達(dá)時(shí),檢查是否有對(duì)應(yīng)其路徑的邏輯處理,當(dāng)請(qǐng)求匹配不上任何路由時(shí),返回 404。匹配成功時(shí)處理相應(yīng)的邏輯。
上面的代碼顯然并不通用,而且在僅有兩種路由匹配候選項(xiàng)(且還未區(qū)分請(qǐng)求方法),以及尚未使用數(shù)據(jù)庫(kù)以及模板文件的前提下,代碼都已經(jīng)有些糾結(jié)了。因此接下來(lái)我們將搭建一個(gè)簡(jiǎn)易的MVC框架,使數(shù)據(jù)、模型、表現(xiàn)分離開(kāi)來(lái),各司其職。
搭建簡(jiǎn)易MVC框架
MVC 分別指的是:
M: Model (數(shù)據(jù))
V: View (表現(xiàn))
C: Controller (邏輯)
在 Node 中,MVC 架構(gòu)下處理請(qǐng)求的過(guò)程如下:
請(qǐng)求抵達(dá)服務(wù)端
服務(wù)端將請(qǐng)求交由路由處理
路由通過(guò)路徑匹配,將請(qǐng)求導(dǎo)向?qū)?yīng)的 controller
controller 收到請(qǐng)求,向 model 索要數(shù)據(jù)
model 給 controller 返回其所需數(shù)據(jù)
controller 可能需要對(duì)收到的數(shù)據(jù)做一些再加工
controller 將處理好的數(shù)據(jù)交給 view
view 根據(jù)數(shù)據(jù)和模板生成響應(yīng)內(nèi)容
服務(wù)端將此內(nèi)容返回客戶端
以此為依據(jù),我們需要準(zhǔn)備以下模塊:
server: 監(jiān)聽(tīng)和響應(yīng)請(qǐng)求
router: 將請(qǐng)求交由正確的controller處理
controllers: 執(zhí)行業(yè)務(wù)邏輯,從 model 中取出數(shù)據(jù),傳遞給 view
model: 提供數(shù)據(jù)
view: 提供 html
創(chuàng)建如下目錄:
-- server.js -- lib -- router.js -- views -- controllers -- models
server
創(chuàng)建 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`); } });
先不管這個(gè)文件里的細(xì)節(jié),router是下面將要完成的模塊,這里先引入,請(qǐng)求抵達(dá)后即交由它處理。
router 模塊
router模塊其實(shí)只需完成一件事,將請(qǐng)求導(dǎo)向正確的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(); });
總的來(lái)說(shuō),我們希望它同時(shí)支持路由中間件和非中間件,請(qǐng)求抵達(dá)后會(huì)由 router 交給匹配上的中間件們處理。中間件是一個(gè)可訪問(wèn)請(qǐng)求對(duì)象和響應(yīng)對(duì)象的函數(shù),在中間件內(nèi)可以做的事情包括:
執(zhí)行任何代碼,比如添加日志和處理錯(cuò)誤等
修改請(qǐng)求 (req) 和響應(yīng)對(duì)象 (res),比如從 req.url 獲取查詢參數(shù)并賦值到 req.query
結(jié)束響應(yīng)
調(diào)用下一個(gè)中間件 (next)
Note:
需要注意的是,如果在某個(gè)中間件內(nèi)既沒(méi)有終結(jié)響應(yīng),也沒(méi)有調(diào)用 next 方法將控制權(quán)交給下一個(gè)中間件, 則請(qǐng)求就會(huì)掛起
__非路由中間件__通過(guò)以下方式添加,匹配所有請(qǐng)求:
router.use(fn);
比如上面的例子:
router.use((req, res, next) => { console.info('New request arrived'); next() });
__路由中間件__通過(guò)以下方式添加,以 請(qǐng)求方法和路徑精確匹配:
router.HTTP_METHOD(path, fn)
梳理好了之后先寫(xiě)出框架:
/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 等方法,每當(dāng)調(diào)用這些方法時(shí),給 routes 添加一條 route 規(guī)則。
Note:
Javascript 中函數(shù)是一種特殊的對(duì)象,能被調(diào)用的同時(shí),還可以擁有屬性、方法。
接下來(lái)的重點(diǎn)在 router 函數(shù),它需要做的是:
從req對(duì)象中取得 method、pathname
依據(jù) method、pathname 將請(qǐng)求與routes數(shù)組內(nèi)各個(gè) route 按它們被添加的順序依次匹配
如果與某個(gè)route匹配成功,執(zhí)行 route.handler,執(zhí)行完后與下一個(gè) route 匹配或結(jié)束流程 (后面詳述)
如果匹配不成功,繼續(xù)與下一個(gè) route 匹配,重復(fù)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(); };
對(duì)于非路由中間件,直接調(diào)用其 handler。對(duì)于路由中間件,只有請(qǐng)求方法和路徑都匹配成功時(shí),才調(diào)用其 handler。當(dāng)沒(méi)有匹配上的 route 時(shí),直接與下一個(gè)route繼續(xù)匹配。
需要注意的是,在某條 route 匹配成功的情況下,執(zhí)行完其 handler 之后,還會(huì)不會(huì)再接著與下個(gè) route 匹配,就要看開(kāi)發(fā)者在其 handler 內(nèi)有沒(méi)有主動(dòng)調(diào)用 next() 交出控制權(quán)了。
在__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(); });
每個(gè)請(qǐng)求抵達(dá)時(shí),首先打印出一條 log,接著匹配其他route。當(dāng)匹配上 actors 或 actresses 的 get 請(qǐng)求時(shí),直接發(fā)回演員名字,并不需要繼續(xù)匹配其他 route。如果都沒(méi)匹配上,返回 404。
在瀏覽器中依次訪問(wèn) http://localhost:9527/erwe、http://localhost:9527/actors、http://localhost:9527/actresses 測(cè)試一下:
network
中觀察到的結(jié)果符合預(yù)期,同時(shí)后臺(tái)命令行中也打印出了三條 New request arrived
語(yǔ)句。
接下來(lái)繼續(xù)改進(jìn) router 模塊。
首先添加一個(gè) router.all 方法,調(diào)用它即意味著為所有請(qǐng)求方法都添加了一條 route:
router.all = (path, fn) => { METHODS.forEach(item => { const method = item.toLowerCase(); router[method](path, fn); }) };
接著,添加錯(cuò)誤處理。
/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); }); ...
默認(rèn)情況下,遇到錯(cuò)誤時(shí)會(huì)返回 500,但開(kāi)發(fā)者使用 router 模塊時(shí)可以傳入自己的錯(cuò)誤處理函數(shù)將其替代。
修改一下代碼,測(cè)試是否能正確執(zhí)行錯(cuò)誤處理:
router.use((req, res, next) => { console.info('New request arrived'); next(new Error('an error')); });
這樣任何請(qǐng)求都應(yīng)該返回 500:
繼續(xù),修改 route.path 與 pathname 的匹配規(guī)則。現(xiàn)在我們認(rèn)為只有當(dāng)兩字符串相等時(shí)才讓匹配通過(guò),這沒(méi)有考慮到 url 中包含路徑參數(shù)的情況,比如:
localhost:9527/actors/Leonardo
與
router.get('/actors/:name', someRouteHandler);
這條route應(yīng)該匹配成功才是。
新增一個(gè)函數(shù)用來(lái)將字符串類型的 route.path 轉(zhuǎn)換成正則對(duì)象,并存入 route.pattern:
const getRoutePattern = pathname => { pathname = '^' + pathname.replace(/(\:\w+)/g, '\(\[a-zA-Z0-9-\]\+\\s\)') + '$'; return new RegExp(pathname); };
這樣就可以匹配上帶有路徑參數(shù)的url了,并將這些路徑參數(shù)存入 req.params 對(duì)象:
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]; } } }
添加個(gè) route 測(cè)試一下:
router.get('/actors/:year/:country', (req, res) => { res.end(`year: ${req.params.year} country: ${req.params.country}`); });
訪問(wèn)http://localhost:9527/actors/1990/China
試試:
router 模塊就寫(xiě)到此,至于查詢參數(shù)的格式化以及獲取請(qǐng)求主體,比較瑣碎就不試驗(yàn)了,需要可以直接使用 bordy-parser 等模塊。
現(xiàn)在我們已經(jīng)創(chuàng)建好了router模塊,接下來(lái)將 route handler 內(nèi)的業(yè)務(wù)邏輯都轉(zhuǎn)移到 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 中同時(shí)引入了 view 和 model, 其充當(dāng)了這二者間的粘合劑。回顧下 controller 的任務(wù):
controller 收到請(qǐng)求,向 model 索要數(shù)據(jù)
model 給 controller 返回其所需數(shù)據(jù)
controller 可能需要對(duì)收到的數(shù)據(jù)做一些再加工
controller 將處理好的數(shù)據(jù)交給 view
在此 controller 中,我們將調(diào)用 model 模塊的方法獲取演員列表,接著將數(shù)據(jù)交給 view,交由 view 生成呈現(xiàn)出演員列表頁(yè)的 html 字符串。最后將此字符串返回給客戶端,在瀏覽器中呈現(xiàn)列表。
從 model 中獲取數(shù)據(jù)
通常 model 是需要跟數(shù)據(jù)庫(kù)交互來(lái)獲取數(shù)據(jù)的,這里我們就簡(jiǎn)化一下,將數(shù)據(jù)存放在一個(gè) 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 中定義一些方法來(lái)訪問(wèn)這些數(shù)據(jù)。
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; });
當(dāng) controller 從 model 中取得想要的數(shù)據(jù)后,下一步就輪到 view 發(fā)光發(fā)熱了。view 層通常都會(huì)用到模板引擎,如 dust 等。同樣為了簡(jiǎn)化,這里采用簡(jiǎn)單替換模板中占位符的方式獲取 html,渲染得非常有限,粗略理解過(guò)程即可。
創(chuàng)建 /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; };
在瀏覽器中測(cè)試一下:
至此,就大功告成啦!
以上這篇使用Node.js實(shí)現(xiàn)簡(jiǎn)易MVC框架的方法就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
如何用Node寫(xiě)頁(yè)面爬蟲(chóng)的工具集
這篇文章主要介紹了如何用Node寫(xiě)頁(yè)面爬蟲(chóng)的工具集,主要介紹了三種方法,分別是Puppeteer、cheerio和Auto.js,感興趣的小伙伴們可以參考一下2018-10-10Node.JS枚舉統(tǒng)計(jì)當(dāng)前文件夾和子目錄下所有代碼文件行數(shù)
這篇文章主要介紹了Node.JS枚舉統(tǒng)計(jì)當(dāng)前文件夾和子目錄下所有代碼文件行數(shù),本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08Nodejs處理Json文件并將處理后的數(shù)據(jù)寫(xiě)入新文件中
這篇文章主要介紹了Nodejs處理Json文件并將處理后的數(shù)據(jù)寫(xiě)入新文件中,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10node.js中的path.basename方法使用說(shuō)明
這篇文章主要介紹了node.js中的path.basename方法使用說(shuō)明,本文介紹了path.basename的方法說(shuō)明、語(yǔ)法、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12node.JS的crypto加密模塊使用方法詳解(MD5,AES,Hmac,Diffie-Hellman加密)
本文將詳細(xì)介紹node.JS的加密模塊crypto實(shí)現(xiàn)MD5,AES,Hmac,Diffie-Hellman加密的詳解方法,需要的朋友可以參考下2020-02-02