express实现原理

通过手写源码可以加深对express的理解,例如路由调用、中间件的运行机制、错误捕获,功能扩展等。

express官方源码

express使用

下面是初始化express最简单的示例:

const Express = require("./express")
const app = new Express()

app.use(function(req,res,next){
    req.str = "use";
    next();
});

app.get("/",(req,res,next)=>{
    req.str = "-get1";
    next()
},(req,res,next)=>{
    req.str+="-get2"
    next();
});

// get 请求
app.get("/",(req,res)=>res.end(req.str))
// post 请求
app.post("/post",(req,res)=>res.end("post"))

app.listen(3000);

运行上面的示例,打开地址localhost:3000可以看到use-get1-get2的结果,上面的示例主要包含这几个步骤:

  • 实例化一个app应用
  • app实例包含中间件方法use、路由getpost方法
  • 路由方法包含路径回调函数两个参数
  • requestresponse对象封装到回调函数里面
  • next方法可以决定是否向下执行
  • app实例包含启动和监听服务的方法listen

通过上面步骤的分析,有几点是比较重要的:

1、可以利用数组stack保存用户设置的路由函数和中间件函数。
2、保存在stack里面的函数应该有对应的名称或者路径方便匹配,可以封装成layer对象(名称,路径,方法)。
3、逐一取出stack里面的函数和请求request匹配 (路径和方法) ,匹配成功则执行,并把下一次的执行函数next作为参数传递。

项目结构

根据express先创建出对应的项目结构:

/- lib
    /- index.js
    /- application.js
    /- router
        /- index.js
        /- route.js
        /- layer.js

初始化

/lib/index.js初始化方法:

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

function createApplication(){
    return new Application()
}

module.exports = createApplication;

/lib/application.js内容如下:

const http = require("http")

function Application() {}

Application.prototype.listen = function () {
    // 执行监听函数后,开始创建Http服务
    const requestHandler = (req,res)=>{};
    http.createServer(requestHandler).listen(...arguments);
}

module.exports = Application

路由

主要的方法集中在router文件夹里面,里面包含index.jsroute.jslayer.js

分析:

  • 封装请求方法例如getpostdelete等,通过methods包获取
  • router/index 处理app下的路由和中间件
  • router/route 处理路由method下的函数
  • router/layer 保存路径方法函数
  • 每个layer先保存在数组stack里面
  • handleRequest取出layer进行匹配,匹配成功则执行,否则下一个layer匹配(封装成next方法)

router/layer.js

// 保存路径和方法
function Layer(path, handler) {
  this.path = path
  this.method = null;
  this.handler = handler
}
// 匹配路径和请求方法
Layer.prototype.match = function (pathname, method) {
  if (!this.method) {
    let path = this.path === "/" ? "/" : this.path + "/"
    return pathname.startsWith(path) || this.path === pathname;
  } else if (pathname === this.path && method === this.method) {
    return true
  }
}

module.exports = Layer

router/route.js

const methods = require("methods");
const Layer = require("./layer");

function Route(){
    this.stack = [];
}
// 封装请求方法、get、post、delete、put等
methods.forEach(method=>{
    Route.prototype[method] = function(handlers){
        handlers.forEach(handler=>{
            const layer = new Layer("/",handler);
            this.stack.push(layer);
        })
    }
})
// 取出stack保存的方法执行
Route.prototype.handler = function (req, res, next) {
    const dispatch = (index)=>{
        const layer = this.stack[index++];
        if(!layer) return next();
        layer.handler(req,res,next);
    }
    dispatch(0);
}

module.exports = Route;

router/index.js

这里有一点不容易理解,Router里面stack数组保存的layer,包含中间件函数use和路由route实例,
route实例里面也有自己的stack数组,都需要取出来匹配看是否执行:

// route 里面保存的layer
let route1 = [layer,layer,layer]
let route2 = [layer,layer,layer]
let route3 = [layer,layer,layer]

// Router里面中间件就是一个layer
let use = layer;

// Router里面的layer包含中间件和route实例
let Router = [use,route1,route2,route3]

决定是否往下执行的方法dispatch

const dispatch = (index)=>{
    const layer = this.stack[index++];
    // 不存在layer说明已经完成
    if(!layer) return done();
    // next 函数执行下一次的dispatch
    const next = ()=>dispatch(index);
    layer.handler(req,res,next);
}
dispatch(0);

router/index内容如下:

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

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

Router.prototype.use = function(path,handler){
    // 中间件函数,如果只有一个参数,重置path参数
    if (typeof path === "function") {
      handler = path
      path = "/"
    }
    const layer = new Layer(path,handler);
    this.stack.push(layer);
}

// 路由方法
Router.prototype.route = function (path, method) {
  const route = new Route()
  // layer里面的handler方法其实是route实例的handler
  const layer = new Layer(path, route.handler.bind(route))
  // 保存请求方法
  layer.method = method
  this.stack.push(layer)
  return route
}

methods.forEach(method=>{
    Router.prototype[method]=function(path,handlers){
        const route = this.route(path,method);
        // 执行route的请求方法,handlers会被保存到route实例的stack数组里面
        route[method](handlers);
    }
})

Router.prototype.handler=function(req,res){
    // 所有layer都没有被匹配到执行done
    const done = ()=>res.end(`Not Found ${req.method} ${req.url}`);
    // 逐一取出layer匹配,看是否执行
    const dispatch=(index)=>{
        const layer = this.stack[index++];
        if(!layer) return done();
        const method = req.method.toLowerCase();
        const {pathname} = url.parse(req.url,true);
        const next = ()=>dispatch(index);
        layer.match(pathname,method) ? layer.handler(req,res,next) : next();
    }
    dispatch(0);
}

module.exports = Router;

application.js

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

function Application() {}

// 路由懒加载
Application.prototype.lazy_router = function () {
  if (!this.router) {
    this.router = new Router()
  }
}
// 绑定中间件
Application.prototype.use = function (path, handler) {
  this.lazy_router()
  this.router.use(path, handler)
}
// 绑定路由
methods.forEach((method) => {
  Application.prototype[method] = function (path, ...handlers) {
    this.lazy_router()
    this.router[method](path, handlers)
  }
})
Application.prototype.listen = function () {
  this.lazy_router()
  // 重新绑定this
  const handleRequest = this.router.handler.bind(this.router)
  http.createServer(handleRequest).listen(...arguments)
}

module.exports = Application

错误处理

express可以给next方法传参,参数代表出现错误信息,在以四个参数的中间件中,第一个参数为错误参数:

app.use(function(req,res,next){
    try {
        // 这里没有test方法,会被catch捕获
        req.test();
    } catch (error) {
        next(error)
    }
})

app.use(function(error,req,res,next)=>{
  // 第一个参数为next传递的错误信息
  // 显示结果:TypeError: req.test is not a function
  res.end(error);
});

上面的示例中,req并没有test方法,直接执行会被tryCatch捕获,将错误信息传递给next,将在中间件中出现四个参数的方法里面,以第一个参数的方式被取到。

接下来就需要对中间件的处理函数dispatch进行修改了,让它能够支持传参,并能够传递到出现四个参数的中间件方法中。

修改router/route.js如下:

Route.prototype.handler = function (req, res, next) {
  // 将累加的指针提取出来
  let index = 0
  // 支持传参
  const dispatch = (error) => {
    const layer = this.stack[index++];
    // 如果中间件取完或者出现错误参数,直接跳出
    if (!layer || error) return next(error)
    layer.handler(req, res, (error) => dispatch(error));
  }
  dispatch()
}

修改router/index.js如下:

Router.prototype.handler = function (req, res) {
  let index = 0
  // 保存错误信息
  let errorMsg = ""

  const done = () => {
    if (errorMsg) {
      // 如果有错误信息,并且没有中间件处理,在页面显示出来
      res.statusCode = 500
      res.end(`handle Error ${errorMsg}`)
    } else {
      res.statusCode = 404
      res.end(`Not Found ${req.method} ${req.url}`)
    }
  }
  // 支持传参
  const dispatch = (error) => {
    errorMsg = error
    const layer = this.stack[index++]
    if (!layer) return done()
    const { pathname } = url.parse(req.url, true)
    const method = req.method.toLowerCase()
    const next = (error) => dispatch(error)
    if (error) {
    // 如果有错误参数,交给handleError处理
      layer.handleError(error, req, res, next)
    } else if (layer.match(pathname, method)) {
    // 正常匹配
      layer.handleRequest(req, res, next)
    }else{
        next(error);
    }
  }

  dispatch()
}

同时给layer添加上两种响应处理,修改router/layer.js如下:

Layer.prototype.handleError = function (error, req, res, next) {
  // 没有method方法,代表是中间件,有四个参数代表是错误处理中间件
  if (!this.method && this.handler.length === 4) {
    return this.handler(error, req, res, next)
  } else {
    return next(error)
  }
}

Layer.prototype.handleRequest = function (req, res, next) {
  // 没有四个参数的方法才处理
  return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}

带参数路由

express带参数的路由可以通过request.params获取,如下:</