从一个 get 请求开始分析 Express 原理

119 阅读4分钟

Express 的简单使用

index.js

// 不是 es6 的写法,  构造函数, 异步处理, 迭代 都是通过回调的方式
const express = require("express");
const app = express();
app.get("/", function (req, res) {
  res.end("/");
});

app.get("/hello", function (req, res) {
  res.end("/hello");
});

app.listen(3000);

命令行运行,在浏览器中打开,一个简单的 express 项目就启动了,浏览器中访问localhost:3000/ 返回 ‘/’,访问 ‘/hello’ 返回 ‘/hello’

node index.js

从这个demo 中可以得出以下几点:

  • express 导出的是一个函数
  • 函数执行后返回一个对象
  • 这个对象上有 get 和 listen 两个方法
  • get 方法是一个订阅事件
  • listen 方法用来启动一个 httpserver 并对不同的请求做出响应(发布事件)。

版本一

大概结构

function express() {
    const stack = []
    return {
        get(){},
        listen() {}
    }
}
module.exports = express

先实现 get 的订阅逻辑

const stack = []
function get(path, handler) {
    stack.push({
        path,
        handler
    })
}

listen 的发布逻辑

const url = require('url')

function listen() {
  const server = http.createServer((req, res) => {
   const { pathname } = url.parse(req.url); // 获取请求路径
   console.log(pathname);
   const methodName = req.method.toLocaleLowerCase(); // 请求方法
   function out() {
     res.end(`Cannot ${req.method} ${req.url}`);
   }
   let idx = 0;
   const dispatch = () => {
     if (idx === this.stack.length) {
       return out(); // 如果路由处理不了,做一个兜底处理
     }
     let layer = this.stack[idx++];
     if (layer.path === pathname) {
       layer.handler(req, res, dispatch);
     } else {
       dispatch();
     }
   };
   dispatch();
 });
 server.listen(...arguments);
}

目前最最简单的 Express 已经可以使用了。

版本二

进一步优化和完善项目

本着单一职责的原则,我们把express 拆分为应用和路由管理

lib/express.js 入口只创建应用

const Application = require("./application");

function createApplication() {
 return new Application();
}

module.exports = createApplication;

lib/application.js

应用有 get、listen 方法,路由的配置归属应用来管理。


const http = require("http");
const Router = require("./router");

function Application() {
 // 路由的配置应该归属应用来管理
 this._router = new Router();
}

Application.prototype.get = function (path, handler) {
 this._router.get(path, handler);
 // 订阅
};

Application.prototype.listen = function () {
 const server = http.createServer((req, res) => {
   // 应用提供一个找不到的方法, 路由匹配不到的时候交给应用
   function done() {
     res.end(`Cannot ${req.method} ${req.url}`);
   }
   this._router.handler(req, res, done);
 });
 server.listen(...arguments);
};
module.exports = Application;

lib/router/index.js

具体的匹配路由和路由执行的逻辑交由路由自己处理

const url = require("url");

function Router() {
 this.stack = [];
}

Router.prototype.get = function (path, handler) {
  this.stack.push({
      path, handler
  });
};

Router.prototype.handler = function (req, res, out) {
 const { pathname } = url.parse(req.url); // 获取请求路径
 console.log(pathname);
 const methodName = req.method.toLocaleLowerCase(); // 请求方法

 let idx = 0;
 const dispatch = () => {
   if (idx === this.stack.length) {
     return out(); // 如果路由处理不了, 交给 application 处理
   }
   let layer = this.stack[idx++];
   // 路由必须满足路径匹配了才能执行
   if (layer.path === pathname) {
     layer.handler(req, res, dispatch);
   } else {
     dispatch();
   }
 };
 dispatch();
};

module.exports = Router;

此时 express 中各个函数的职能更加明确了。

express 还有一种用法就是不但能处理异步逻辑,还可以有多个回调函数

app.get(
  "/",
  function (req, res, next) {
    console.log(1)
    next();
  },
  function (req, res, next) {
     console.log(2)
    next();
  },
  function (req, res, next) {
     console.log(3)
    next();
  }
);

app.get(
  "/",
  function (req, res, next) {
     console.log(4)
    res.end("a");
  }
);

最终访问 ‘/’ 会依次打印 1234, app.get 的所有 callback 函数都执行了。 每一次 get 请求都会往 router 的 stack 中放入一个对象{method,path, handler}, 我们可以把 router 中 stack 的对象单独抽出一个类 Layer。

layer 的功能很简单。主要存两个东西 path, handler 即 路径处理方法。这里没有存储 method。

而每个请求又可能对应多个 method, 这里再引入一个类 Route。 Route 中也有一个 stack, stack 中放入 method 和真正的处理函数

大概的流程图如下:

router.png

lib/router/layer.js

function Layer(path, handler) {
    this.path = path;
    this.handler = handler
}

Layer.prototype.match = function(pathname) {
    return this.path === pathname
}

这里改写一下以下几个文件

lib/application.js


Application.prototype.get = function (path, ...handlers) { // 用 handlers 数组接收可能的回调函数
  this._router.get(path, handlers);
};

lib/router/index.js

const url = require("url"); 
const Layer = require("./layer");
const Route = require("./route");

function Router() {
  this.stack = [];
}

Router.prototype.route = function (path) {
  const route = new Route()
  // 每次 app.get 都会在 Router 的 stack 中放入一层
  const layer = new Layer(path, route.dispatch.bind(route))
  // 而每一层又会有对应的 method 和 多个处理函数
  layer.route = route
  this.stack.push(layer);
  return route
};

Router.prototype.get = function (path, handler) {
// 每次 app.get 都会在 Router 的 stack 中放入一层
  const route = this.route(path);
  // 而每一层又会有对应的 method 和 多个处理函数
  route.get(handlers);
};

Router.prototype.handler = function (req, res, out) {
  const { pathname } = url.parse(req.url); // 获取请求路径
  console.log(pathname);
  const methodName = req.method.toLocaleLowerCase(); // 请求方法

  let idx = 0;
  const dispatch = () => {
    if (idx === this.stack.length) {
      return out(); // 如果路由处理不了, 交给 application 处理
    }
    let layer = this.stack[idx++];
    // 必须满足路径匹配了才能执行
    if (layer.match(pathname)) { // 匹配逻辑也交由具体的 layer 处理
      layer.handler(req, res, dispatch);
    } else {
      dispatch();
    }
  };
  dispatch();
};

module.exports = Router;

./lib/router/route.js

function Route() {
  this.stack = [];
}

Route.prototype.get = function(handlers) {
// 遍历处理函数,放入 stack 中
  handlers.forEach(handler => {
    const layer = new Layer('/', handler)
    layer.method = 'get'
    this.stack.push(layer)
  })
}

Route.prototype.dispatch = function(req, res, next) {
  const methodName = req.method.toLowerCase();
  let idx = 0
  function dispatch() {
    if(idx === this.stack.length) {return next()}
    const layer = this.stack[idx++]
    if(layer.method === methodName) {
      layer.handler(req, res, dispatch)
    }else {
      dispatch()
    }
  }

  dispatch()
}

这样就满足了基本的 express 的写法。

版本三

还有一些功能需要完善,比如 express 的 use 方法

app.use(function(req, res, next) {
    console.log(1)
    req.a = 1
    next()
})
app.use('/'function(req, res, next) {
console.log(2)
    req.a++
    next()
})
app.use('/a'function(req, res, next) {
console.log(3)
    req.a++
    next()
})
app.get('/', function(req, res, next) {
console.log(4)
   res.end(req.a + '')
})
app.get('/a', function(req, res, next) {
console.log(5)
   res.end(req.a + '')
})

当请求 / 时,最终页面上显示的是 2。 执行的是 1,2,4

当请求 /a 时,最终页面上显示的也是 3。执行的是 1,2,3,5

use 方法的回调函数可以是一个参数,也可以是两个参数(path, handler),而且的参数的匹配不是绝对的匹配,而是以 path 开头的都可以的匹配上

lib/application.js

Application.prototype.use = function(path, handler) {
    this._router.use(path, hander)
}

lib/router/index.js

Router.prototype.use = function() {
    if(typeof path === 'function') {
        handler = path
        path = '/'
    }
    const layer = new Layer(path, handler)
    layer.route = undefined; // 用 route 属性来区分是路由还是中间件
    this.stack.push(layer)
}

lib/router/layer.js

改写一下 match 方法已适用 use 中间件的 path 匹配规则


Layer.prototype.match = function(pathname) {
    if(this.path === pathname) {
        return true
    }
    if(!this.route) {
        if(this.path === '/') {
            return true
        }
        return path.startsWith(this.path + '/') // /a/b. /a/
    }
}