在使用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。匹配成功时处理相应的逻辑。
上面的代码显然并不通用,而且在仅有两种路由匹配候选项(且还未区分请求方法),以及尚未使用数据库以及模板文件的前提下,代码都已经有些纠结了。因此接下来我们将搭建一个简易的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 rou