sensai2.0

309
15

1. Node.js服务端框架的一般套路——中间件模式

node.js服务端开启httpserver的方式非常easy:

const http = require('http');
const server = http.createServer((req, res) => {
  // ...
  res.end();
});
server.listen(9999);

所有基于Node的Web框架都是基于这个搭建起来的。

一个优秀的框架往往具有可插拔的各种功能,框架需要拥有路由匹配、模板渲染、Ajax Response格式化、UrlParser、BodyParser等等功能,好的设计方式需要将各个功能进行完美的解耦、各个功能相互之间应该独立并且可以根据用户的需求任意加载。那么设计模式中的“责任链模式”就可以完美的满足这个需求。

在责任链模式中,所有的相互独立模块都被封装成为一个“中间件”,每个中间件处理请求上下文中自己负责的那一部分,并且将结果带回到上下文中,这种设计方式在Web应用中还是非常常见的,像Laravel、RoR、Flask等框架都是采用的这种模式;

对于Node来说,实现“中间件”的方法和上述的框架还是有些许不同的。主要的问题在于Node“异步”的特性,有的时候当前中间件方法的return并不代表当前中间件的结束(比如当前中间件中做了io操作时,io的返回才表示中间件的结束)。


2. Connect和Express的基本实现原理:

Connect和Express是Node.js下最基础的​Web框架,它们中间件实现的方式非常巧妙。

先看一个基本的中间件的定义:

const middleware = (req, res, next) => {
  // 相关逻辑...
  next();
}
我们希望把中间件放入到httpServer中,那么首先想到的简单粗暴的方式是这样的:


那么如果有两个中间件需要处理,就会变成下面这样:


不难看出,这又回到了nodejs最初的问题“回调地狱”了。

那么框架所做的事情就是优雅的解决这个问题。

简单考虑,我们需要一个类来存储所有相关的信息,这里简单叫App吧。

App里面至少应该存有所有的middlewares,那就用一个数组来存,并给出一个use方法来注册中间件:即往数组里面push一个中间件方法:


接下来写对http的封装处理:


http处理中封装了一个handle的方法,这个是框架的核心。

实现的思路也不复杂,实际上就是一个next方法的不断递归。


这样,一个初步的中间件框架就基本实现了。

那么之前恶心的中间件代码就可以直接简化成下面几句话:


实际上像Express或者Connect这类基础Web框架实现的原理基本如此。

一个好的框架除了处理正确逻辑以外还需要处理一些错误的分支,比如中间件中如果调用了文件io然后失败了,那么应该将失败的对象合理的抛出。

考虑下这个需求:从url中读取查询path=???然后将调用系统命令获取path路径下的所有文件名并返回。那么当输入path不存在的或者无读取权限的时候需要抛出异常。

所以可以尝试给next()方法添加一个error的参数,如下面的方法所示,readDir中间件尝试读取输入path的目录下所有文件名,如果path不合法,将会通过next(error)抛出相关异常:


那么处理函数需要多一层判断:当next()传入为空的时候继续后续的中间件处理,否则通过事件抛出异常,那么作以下的修改:


我这里的处理比较偷懒,直接抛出异常事件就完事了,实际上一个完善的框架应该给业务方提供错误处理的中间件,一旦内部出现异常,那么会自动next到异常处理中间件去。

最终在业务层就可以处理异常case了:


测试一下最终结果,看上去还不错~

最后附上最终写出的框架和业务代码​如下:

const http = require('http')
const url = require('url');
const fs = require('fs');
const { EventEmitter } = require('events');

function App() {
  this.middlewares = [];
}

App.prototype = Object.create(EventEmitter.prototype);

App.prototype.use = function(fn) {
  if (fn.__proto__.constructor.name == 'Function') {
    this.middlewares.push(fn);
  } else {
    throw new Error('exception is invalid!');
  }
}

App.prototype.handle = function(req, res) {
  const next = (index) => {
    if (index >= this.middlewares.length) {
      res.end(res.body);
      return;
    }
    this.middlewares[index](req, res, (error) => {
      if (error) {
        this.emit('error', { req, res, error });
        return false;
      }
      return next(index + 1);
    });
  }
  next(0);
}

App.prototype.start = function() {
  const server = http.createServer((req, res) => {
    this.handle(req, res);
  });
  server.listen(9999);
}


const querystring = (req, res, next) => {
  const urlObj = url.parse(req.url, true);
  req.querystring = urlObj.query;
  next();
}

const getPath = (req, res, next) => {
  const path = (req.querystring && req.querystring.path) || '/';
  res.output.path = path;
  next();
}

const readDir = (req, res, next) => {
  fs.readdir(res.output.path, (error, files) => {
    if (error) {
      next(error);
      return;
    }
    res.body = files.join(',');
    next();
  });
}

const app = new App();
app.use(querystring);
app.use(getPath);
app.use(readDir);
app.on('error', ({req, res, error}) => {
  res.end(error.toString());
});
app.start();


回复

对话列表

×