使用Node.js实现简易MVC框架的方法


Posted in Javascript onAugust 07, 2017

在使用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。匹配成功时处理相应的逻辑。

使用Node.js实现简易MVC框架的方法

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

使用Node.js实现简易MVC框架的方法

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:

使用Node.js实现简易MVC框架的方法

继续,修改 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试试:

使用Node.js实现简易MVC框架的方法

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;
};

在浏览器中测试一下:

使用Node.js实现简易MVC框架的方法

至此,就大功告成啦!

以上这篇使用Node.js实现简易MVC框架的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS 统计时间
Mar 09 Javascript
Ext对基本类型的扩展 ext,extjs,format
Dec 25 Javascript
读jQuery之十四 (触发事件核心方法)
Aug 23 Javascript
js 获取浏览器版本以此来调整CSS的样式
Jun 03 Javascript
使用jquery.qrcode.js生成二维码插件
Oct 17 Javascript
angular实现表单验证及提交功能
Feb 01 Javascript
jQuery实现拼图小游戏(实例讲解)
Jul 24 jQuery
vue获取input输入值的问题解决办法
Oct 17 Javascript
AngularJS实现的select二级联动下拉菜单功能示例
Oct 25 Javascript
使用classList来实现两个按钮样式的切换方法
Jan 24 Javascript
用Axios Element实现全局的请求loading的方法
Mar 15 Javascript
垃圾回收器的相关知识点总结
May 13 Javascript
ES6新增的math,Number方法
Aug 06 #Javascript
ComboBox(下拉列表框)通过url加载调用远程数据的方法
Aug 06 #Javascript
JS解析url查询参数的简单代码
Aug 06 #Javascript
使用bootstraptable插件实现表格记录的查询、分页、排序操作
Aug 06 #Javascript
JS中定位 position 的使用实例代码
Aug 06 #Javascript
Node.js 基础教程之全局对象
Aug 06 #Javascript
Node.js  REPL (交互式解释器)实例详解
Aug 06 #Javascript
You might like
基于PHP与XML的PDF文档生成技术
2006/10/09 PHP
PHP中的类型约束介绍
2015/05/11 PHP
Javascript优化技巧之短路表达式详细介绍
2015/03/27 Javascript
JS拖动鼠标画出方框实现鼠标选区的方法
2015/08/05 Javascript
使用JQuery实现的分页插件分享
2015/11/05 Javascript
jquery ui dialog替代confirm实例分析
2016/01/25 Javascript
Angular2-primeNG文件上传模块FileUpload使用详解
2017/01/14 Javascript
easyui-datagrid特殊字符不能显示的处理方法
2017/04/12 Javascript
基于node.js express mvc轻量级框架实践
2017/09/14 Javascript
jQuery扩展方法实现Form表单与Json互相转换的实例代码
2018/09/05 jQuery
es6函数之箭头函数用法实例详解
2020/04/25 Javascript
微信小程序12行js代码自己写个滑块功能(推荐)
2020/07/15 Javascript
React Ant Design树形表格的复杂增删改操作
2020/11/02 Javascript
[02:38]DOTA2 夜魇暗潮2020活动介绍官方视频
2020/11/04 DOTA
详解python的几种标准输出重定向方式
2016/08/15 Python
使用Python对SQLite数据库操作
2017/04/06 Python
Pandas实现dataframe和np.array的相互转换
2019/11/30 Python
浅谈tensorflow中张量的提取值和赋值
2020/01/19 Python
python自动脚本的pyautogui入门学习
2020/04/01 Python
伦敦最著名的老字号百货公司:Selfridges(塞尔福里奇百货)
2016/07/25 全球购物
美国知名的家庭连锁百货商店:Boscov’s
2017/07/27 全球购物
澳洲本土太阳镜品牌:Quay Australia
2019/07/29 全球购物
澳大利亚在线高跟鞋商店:Shoe Me
2019/11/19 全球购物
花卉与景观设计系大学生求职信
2013/10/01 职场文书
质检部岗位职责
2013/11/11 职场文书
人力资源管理专业自荐书范文
2014/02/10 职场文书
文案策划求职信
2014/03/18 职场文书
2014年秋季新学期寄语
2014/08/02 职场文书
2014统计局民主生活会对照检查材料思想汇报
2014/10/02 职场文书
幼儿园六一儿童节演讲稿
2015/03/19 职场文书
交通处罚决定书
2015/06/24 职场文书
考试后的感想
2015/08/07 职场文书
导游词之广西漓江
2019/11/02 职场文书
python 实现两个变量值进行交换的n种操作
2021/06/02 Python
JS轻量级函数式编程实现XDM三
2022/06/16 Javascript
Python可视化神器pyecharts绘制地理图表
2022/07/07 Python