使用Node.js搭建静态资源服务器一文中我们完成了服务器对静态资源请求的处理,但并未涉及动态请求,目前还无法根据客户端发出的不同请求而返回个性化的内容。单靠静态资源岂能撑得起这些复杂的网站应用,本文将介绍如何使用Node处理动态请求,以及如何搭建一个简易的 MVC 框架。因为前文已经详细介绍过静态资源请求如何响应,本文将略过所有静态部分。

一个简单的示例

先从一个简单示例入手,明白在 Node 中如何向客户端返回动态内容。

假设我们有这样的需求:

  1. 当用户访问 /actors 时返回男演员列表页
  2. 当用户访问 /actresses 时返回女演员列表

可以用以下的代码完成功能:

  1. const http = require('http');
  2. const url = require('url');
  3. http.createServer((req, res) => {
  4. const pathName = url.parse(req.url).pathname;
  5. if (['/actors', '/actresses'].includes(pathName)) {
  6. res.writeHead(200, {
  7. 'Content-Type': 'text/html'
  8. });
  9. const actors = ['Leonardo DiCaprio', 'Brad Pitt', 'Johnny Depp'];
  10. const actresses = ['Jennifer Aniston', 'Scarlett Johansson', 'Kate Winslet'];
  11. let lists = [];
  12. if (pathName === '/actors') {
  13. lists = actors;
  14. } else {
  15. lists = actresses;
  16. }
  17. const content = lists.reduce((template, item, index) => {
  18. return template + `<p>No.${index+1} ${item}</p>`;
  19. }, `<h1>${pathName.slice(1)}</h1>`);
  20. res.end(content);
  21. } else {
  22. res.writeHead(404);
  23. res.end('<h1>Requested page not found.</h1>')
  24. }
  25. }).listen(9527);

上面代码的核心是路由匹配,当请求抵达时,检查是否有对应其路径的逻辑处理,当请求匹配不上任何路由时,返回 404。匹配成功时处理相应的逻辑。

上面的代码显然并不通用,而且在仅有两种路由匹配候选项(且还未区分请求方法),以及尚未使用数据库以及模板文件的前提下,代码都已经有些纠结了。因此接下来我们将搭建一个简易的MVC框架,使数据、模型、表现分离开来,各司其职。

搭建简易MVC框架

MVC 分别指的是:

  1. M: Model (数据)
  2. V: View (表现)
  3. C: Controller (逻辑)

在 Node 中,MVC 架构下处理请求的过程如下:

  1. 请求抵达服务端
  2. 服务端将请求交由路由处理
  3. 路由通过路径匹配,将请求导向对应的 controller
  4. controller 收到请求,向 model 索要数据
  5. model 给 controller 返回其所需数据
  6. controller 可能需要对收到的数据做一些再加工
  7. controller 将处理好的数据交给 view
  8. view 根据数据和模板生成响应内容
  9. 服务端将此内容返回客户端

以此为依据,我们需要准备以下模块:

  1. server: 监听和响应请求
  2. router: 将请求交由正确的controller处理
  3. controllers: 执行业务逻辑,从 model 中取出数据,传递给 view
  4. model: 提供数据
  5. view: 提供 html

代码结构

创建如下目录:

  1. -- server.js
  2. -- lib
  3. -- router.js
  4. -- views
  5. -- controllers
  6. -- models

server

创建 server.js 文件:

  1. const http = require('http');
  2. const router = require('./lib/router')();
  3. router.get('/actors', (req, res) => {
  4. res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp');
  5. });
  6. http.createServer(router).listen(9527, err => {
  7. if (err) {
  8. console.error(err);
  9. console.info('Failed to start server');
  10. } else {
  11. console.info(`Server started`);
  12. }
  13. });

先不管这个文件里的细节,router是下面将要完成的模块,这里先引入,请求抵达后即交由它处理。

router 模块

router模块其实只需完成一件事,将请求导向正确的controller处理,理想中它可以这样使用:

  1. const router = require('./lib/router')();
  2. const actorsController = require('./controllers/actors');
  3. router.use((req, res, next) => {
  4. console.info('New request arrived');
  5. next()
  6. });
  7. router.get('/actors', (req, res) => {
  8. actorsController.fetchList();
  9. });
  10. router.post('/actors/:name', (req, res) => {
  11. actorsController.createNewActor();
  12. });

总的来说,我们希望它同时支持路由中间件和非中间件,请求抵达后会由 router 交给匹配上的中间件们处理。中间件是一个可访问请求对象和响应对象的函数,在中间件内可以做的事情包括:

  1. 执行任何代码,比如添加日志和处理错误等
  2. 修改请求 (req) 和响应对象 (res),比如从 req.url 获取查询参数并赋值到 req.query
  3. 结束响应
  4. 调用下一个中间件 (next)

Note:

需要注意的是,如果在某个中间件内既没有终结响应,也没有调用 next 方法将控制权交给下一个中间件, 则请求就会挂起

__非路由中间件__通过以下方式添加,匹配所有请求:

  1. router.use(fn);

比如上面的例子:

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

__路由中间件__通过以下方式添加,以 请求方法和路径精确匹配:

  1. router.HTTP_METHOD(path, fn)

梳理好了之后先写出框架:

/lib/router.js

  1. const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'];
  2. module.exports = () => {
  3. const routes = [];
  4. const router = (req, res) => {
  5. };
  6. router.use = (fn) => {
  7. routes.push({
  8. method: null,
  9. path: null,
  10. handler: fn
  11. });
  12. };
  13. METHODS.forEach(item => {
  14. const method = item.toLowerCase();
  15. router[method] = (path, fn) => {
  16. routes.push({
  17. method,
  18. path,
  19. handler: fn
  20. });
  21. };
  22. });
  23. };

以上主要是给 router 添加了 usegetpost 等方法,每当调用这些方法时,给 routes 添加一条 route 规则。

Note:

Javascript 中函数是一种特殊的对象,能被调用的同时,还可以拥有属性、方法。

接下来的重点在 router 函数,它需要做的是:

  1. req对象中取得 method、pathname
  2. 依据 method、pathname 将请求与routes数组内各个 route 按它们被添加的顺序依次匹配
  3. 如果与某个route匹配成功,执行 route.handler,执行完后与下一个 route 匹配或结束流程 (后面详述)
  4. 如果匹配不成功,继续与下一个 route 匹配,重复3、4步骤
  1. const router = (req, res) => {
  2. const pathname = decodeURI(url.parse(req.url).pathname);
  3. const method = req.method.toLowerCase();
  4. let i = 0;
  5. const next = () => {
  6. route = routes[i++];
  7. if (!route) return;
  8. const routeForAllRequest = !route.method && !route.path;
  9. if (routeForAllRequest || (route.method === method && pathname === route.path)) {
  10. route.handler(req, res, next);
  11. } else {
  12. next();
  13. }
  14. }
  15. next();
  16. };

对于非路由中间件,直接调用其 handler。对于路由中间件,只有请求方法和路径都匹配成功时,才调用其 handler。当没有匹配上的 route 时,直接与下一个route继续匹配。

需要注意的是,在某条 route 匹配成功的情况下,执行完其 handler 之后,还会不会再接着与下个 route 匹配,就要看开发者在其 handler 内有没有主动调用 next() 交出控制权了。

在__server.js__中添加一些route:

  1. router.use((req, res, next) => {
  2. console.info('New request arrived');
  3. next()
  4. });
  5. router.get('/actors', (req, res) => {
  6. res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp');
  7. });
  8. router.get('/actresses', (req, res) => {
  9. res.end('Jennifer Aniston, Scarlett Johansson, Kate Winslet');
  10. });
  11. router.use((req, res, next) => {
  12. res.statusCode = 404;
  13. res.end();
  14. });

每个请求抵达时,首先打印出一条 log,接着匹配其他route。当匹配上 actors 或 actresses 的 get 请求时,直接发回演员名字,并不需要继续匹配其他 route。如果都没匹配上,返回 404。

在浏览器中依次访问 http://localhost:9527/erwehttp://localhost:9527/actorshttp://localhost:9527/actresses 测试一下:

network 中观察到的结果符合预期,同时后台命令行中也打印出了三条 New request arrived语句。

接下来继续改进 router 模块。

首先添加一个 router.all 方法,调用它即意味着为所有请求方法都添加了一条 route:

  1. router.all = (path, fn) => {
  2. METHODS.forEach(item => {
  3. const method = item.toLowerCase();
  4. router[method](path, fn);
  5. })
  6. };

接着,添加错误处理。

/lib/router.js

  1. const defaultErrorHander = (err, req, res) => {
  2. res.statusCode = 500;
  3. res.end();
  4. };
  5. module.exports = (errorHander) => {
  6. const routes = [];
  7. const router = (req, res) => {
  8. ...
  9. errorHander = errorHander || defaultErrorHander;
  10. const next = (err) => {
  11. if (err) return errorHander(err, req, res);
  12. ...
  13. }
  14. next();
  15. };

server.js

  1. ...
  2. const router = require('./lib/router')((err, req, res) => {
  3. console.error(err);
  4. res.statusCode = 500;
  5. res.end(err.stack);
  6. });
  7. ...

默认情况下,遇到错误时会返回 500,但开发者使用 router 模块时可以传入自己的错误处理函数将其替代。

修改一下代码,测试是否能正确执行错误处理:

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

这样任何请求都应该返回 500:

继续,修改 route.path 与 pathname 的匹配规则。现在我们认为只有当两字符串相等时才让匹配通过,这没有考虑到 url 中包含路径参数的情况,比如:

  1. localhost:9527/actors/Leonardo

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

这条route应该匹配成功才是。

新增一个函数用来将字符串类型的 route.path 转换成正则对象,并存入 route.pattern:

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

这样就可以匹配上带有路径参数的url了,并将这些路径参数存入 req.params 对象:

  1. const matchedResults = pathname.match(route.pattern);
  2. if (route.method === method && matchedResults) {
  3. addParamsToRequest(req, route.path, matchedResults);
  4. route.handler(req, res, next);
  5. } else {
  6. next();
  7. }
  1. const addParamsToRequest = (req, routePath, matchedResults) => {
  2. req.params = {};
  3. let urlParameterNames = routePath.match(/:(\w+)/g);
  4. if (urlParameterNames) {
  5. for (let i=0; i < urlParameterNames.length; i++) {
  6. req.params[urlParameterNames[i].slice(1)] = matchedResults[i + 1];
  7. }
  8. }
  9. }

添加个 route 测试一下:

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

访问http://localhost:9527/actors/1990/China试试:

router 模块就写到此,至于查询参数的格式化以及获取请求主体,比较琐碎就不试验了,需要可以直接使用 bordy-parser 等模块。

controller

现在我们已经创建好了router模块,接下来将 route handler 内的业务逻辑都转移到 controller 中去。

修改__server.js__,引入 controller:

  1. ...
  2. const actorsController = require('./controllers/actors');
  3. ...
  4. router.get('/actors', (req, res) => {
  5. actorsController.getList(req, res);
  6. });
  7. router.get('/actors/:name', (req, res) => {
  8. actorsController.getActorByName(req, res);
  9. });
  10. router.get('/actors/:year/:country', (req, res) => {
  11. actorsController.getActorsByYearAndCountry(req, res);
  12. });
  13. ...

新建__controllers/actors.js__:

  1. const actorsTemplate = require('../views/actors-list');
  2. const actorsModel = require('../models/actors');
  3. exports.getList = (req, res) => {
  4. const data = actorsModel.getList();
  5. const htmlStr = actorsTemplate.build(data);
  6. res.writeHead(200, {
  7. 'Content-Type': 'text/html'
  8. });
  9. res.end(htmlStr);
  10. };
  11. exports.getActorByName = (req, res) => {
  12. const data = actorsModel.getActorByName(req.params.name);
  13. const htmlStr = actorsTemplate.build(data);
  14. res.writeHead(200, {
  15. 'Content-Type': 'text/html'
  16. });
  17. res.end(htmlStr);
  18. };
  19. exports.getActorsByYearAndCountry = (req, res) => {
  20. const data = actorsModel.getActorsByYearAndCountry(req.params.year, req.params.country);
  21. const htmlStr = actorsTemplate.build(data);
  22. res.writeHead(200, {
  23. 'Content-Type': 'text/html'
  24. });
  25. res.end(htmlStr);
  26. };

在 controller 中同时引入了 view 和 model, 其充当了这二者间的粘合剂。回顾下 controller 的任务:

  1. controller 收到请求,向 model 索要数据
  2. model 给 controller 返回其所需数据
  3. controller 可能需要对收到的数据做一些再加工
  4. controller 将处理好的数据交给 view

在此 controller 中,我们将调用 model 模块的方法获取演员列表,接着将数据交给 view,交由 view 生成呈现出演员列表页的 html 字符串。最后将此字符串返回给客户端,在浏览器中呈现列表。

从 model 中获取数据

通常 model 是需要跟数据库交互来获取数据的,这里我们就简化一下,将数据存放在一个 json 文件中。

/models/test-data.json

  1. [
  2. {
  3. "name": "Leonardo DiCaprio",
  4. "birth year": 1974,
  5. "country": "US",
  6. "movies": ["Titanic", "The Revenant", "Inception"]
  7. },
  8. {
  9. "name": "Brad Pitt",
  10. "birth year": 1963,
  11. "country": "US",
  12. "movies": ["Fight Club", "Inglourious Basterd", "Mr. & Mrs. Smith"]
  13. },
  14. {
  15. "name": "Johnny Depp",
  16. "birth year": 1963,
  17. "country": "US",
  18. "movies": ["Edward Scissorhands", "Black Mass", "The Lone Ranger"]
  19. }
  20. ]

接着就可以在 model 中定义一些方法来访问这些数据。

models/actors.js

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

view

当 controller 从 model 中取得想要的数据后,下一步就轮到 view 发光发热了。view 层通常都会用到模板引擎,如 dust 等。同样为了简化,这里采用简单替换模板中占位符的方式获取 html,渲染得非常有限,粗略理解过程即可。

创建 /views/actors-list.js:

  1. const actorTemplate = `
  2. <h1>{name}</h1>
  3. <p><em>Born: </em>{contry}, {year}</p>
  4. <ul>{movies}</ul>
  5. `;
  6. exports.build = list => {
  7. let content = '';
  8. list.forEach(actor => {
  9. content += actorTemplate.replace('{name}', actor.name)
  10. .replace('{contry}', actor.country)
  11. .replace('{year}', actor["birth year"])
  12. .replace('{movies}', actor.movies.reduce((moviesHTML, movieName) => {
  13. return moviesHTML + `<li>${movieName}</li>`
  14. }, ''));
  15. });
  16. return content;
  17. };

在浏览器中测试一下:

至此,就大功告成啦!

参考

  1. Nodejs实现一个简单的服务器
  2. Creating an MVC framework for our Node.js page - getting ready for scalability

源码

戳我的 GitHub repo: node-mvc-framework

博文也同步在 GitHub,欢迎讨论和指正:使用Node.js实现简易MVC框架

使用Node.js实现简易MVC框架的更多相关文章

  1. [转] 使用Node.js实现简易MVC框架

    在使用Node.js搭建静态资源服务器一文中我们完成了服务器对静态资源请求的处理,但并未涉及动态请求,目前还无法根据客户端发出的不同请求而返回个性化的内容.单靠静态资源岂能撑得起这些复杂的网站应用,本 ...

  2. node.js 在 Express4.0 框架使用 Connect-Busboy 实现文件上传

    node.js下四种post提交数据的方式 今天说分享的是其中一种,就是上传文件. Express 4.0 以后,将功能原子化,高内聚,低耦合,独立出了很多中间件 今天主要分享文件上传 对于conne ...

  3. node.js 开发简易的小爬虫

    node.js  开发简易的小爬虫 最近公司开发一款医药类的软件,所以需要一些药品的基础数据,所以本人就用node.js写一个简易的小爬虫,并写记录这个Demo以供大家参考. 一.开发前的准备: 1, ...

  4. Node.js中的express框架获取http参数

    最近本人在学习开发NodeJs,使用到express框架,对于网上的学习资料甚少,因此本人会经常在开发中做一些总结. express获取参数有三种方法:官网介绍如下 Checks route para ...

  5. hexo —— 简单、快速、强大的Node.js静态博客框架

    hexo是一款基于Node.js的静态博客框架.目前在GitHub上已有1375 star 和 219 fork. 特性 风一般的速度 Hexo基于Node.js,支持多进程,几百篇文章也可以秒生成. ...

  6. [js高手之路]Node.js实现简易的爬虫-抓取博客文章列表信息

    抓取目标:就是我自己的博客:http://www.cnblogs.com/ghostwu/ 需要实现的功能: 抓取文章标题,超链接,文章摘要,发布时间 需要用到的库: node.js自带的http库 ...

  7. [js高手之路]Node.js实现简易的爬虫-抓取博客所有文章列表信息

    抓取目标:就是我自己的博客:http://www.cnblogs.com/ghostwu/ 需要实现的功能: 抓取博客所有的文章标题,超链接,文章摘要,发布时间 需要用到的库: node.js自带的h ...

  8. 配置node.js中的express框架

    玩node.js,不玩后台那就是杀鸡牛刀,今天没事整理一下以前开发node.js后台的心得 1.首先安装node.js以及cnpm,在这儿我就不说了,看我node.js中的另一篇文章node.js的安 ...

  9. Node.js 打造实时多人游戏框架

    在 Node.js 如火如荼发展的今天,我们已经可以用它来做各种各样的事情.前段时间UP主参加了极客松活动,在这次活动中我们意在做出一款让“低头族”能够更多交流的游戏,核心功能便是 Lan Party ...

随机推荐

  1. phpcms v9栏目列表调用每一篇文章内容方法

    {pc:content action="lists" catid="$catid" num="25" order="id DESC ...

  2. 几种Android数据序列化方案

    一.引言 数据的序列化在Android开发中占据着重要的地位,无论是在进程间通信.本地数据存储又或者是网络数据传输都离不开序列化的支持.而针对不同场景选择合适的序列化方案对于应用的性能有着极大的影响. ...

  3. 解决运行pytorch程序多线程问题

    当我使用pycharm运行  (https://github.com/Joyce94/cnn-text-classification-pytorch )  pytorch程序的时候,在Linux服务器 ...

  4. Linux系统网卡设置

    由于做了虚拟机的克隆,发现克隆机和被克隆机的MAC地址相同了,下面我将要介绍一下linux中网卡的配置步骤,我使用的linux是CentOS release 6.9 (Final) 1.root用户编 ...

  5. 【转】Header Only Library的介绍

    什么是Header Only Library Header Only Library把一个库的内容完全写在头文件中,不带任何cpp文件. 这是一个巧合,决不是C++的原始设计. 第一次这么做估计是ST ...

  6. Docker部署DVWA

    上次在Docker手动配置了一个Ubuntu的Lamp镜像,这次来试验一下使用这个镜像部署一个简单的web应用吧. 首先从Lamp镜像运行一个容器 root@VM-149-127-debian:~/a ...

  7. Spring事务管理的两种方式

    参考文档: http://www.iteye.com/topic/1123347 http://blog.csdn.net/lcj8/article/details/2835432 PS:好像还是tx ...

  8. 数据结构3——浅谈zkw线段树

    线段树是所有数据结构中,最常用的之一.线段树的功能多样,既可以代替树状数组完成"区间和"查询,也可以完成一些所谓"动态RMQ"(可修改的区间最值问题)的操作.其 ...

  9. 拥抱.NET Core系列:Logging (1)

    在之前我们简单介绍了 .NET Core 中的 DI组件,没来及了解的童鞋可以翻翻我之前的文章. 接下来会对 .NET Core 中的 Logging 进行介绍. 本文中使用了"Micros ...

  10. 有关各个版本的Visual Studio(VS)和SQL Server安装的顺序总结

    前几天从网上买了块三星的SSD,把原来的HDD放在了光驱位,然后重新安装了系统.想起来收集储存的好多源代码还是VS2008开发的,然后打算把之前用过的VS2008也装上,安装过程出了点问题.发现安装完 ...