手把手带你用 82 行代码实现一个简易版 Express 框架

本文将带大家实现轻量级 web 框架 connect 的主要功能,只要 82 行代码就能搞定。

我并没有标题党,因为 Express 在 v3 版本之前就是基于 connect 进行封装的,不过在 v4 版本就将 connect 依赖移除了,代码被搬到 Express 仓库里,并做了一些细微调整。因此某种程度上,学习 connect 就是在学习 Express。

connect 的 repo 描述是:"Connect is a middleware layer for Node.js",也就是一个 Node.js 的中间件层。中间件层是一个非常有用的机制,它类似一个插件系统,让我们可以通过插拔的方式组合不同功能来处理请求。

基本使用

先来看看 connect 的使用。

javascript 复制代码
const connect = require('connect')

const app = connect()

// respond to all requests
app.use(function(req, res){
  res.end('Hello from Connect!\n')
})

// create node.js http server and listen on port
http.createServer(app).listen(3000)

跟 Express 一样。

另外,app 上还提供了 .listen() 方法,用于替代 http.createServer(app).listen(3000) 的冗长写法。

javascript 复制代码
app.listen(3000) // 等价于 http.createServer(app).listen(3000)

再看看中间件的使用。

javascript 复制代码
app.use(function middleware1(req, res, next) {
  // middleware 1
  next()
});
app.use(function middleware2(req, res, next) {
  // middleware 2
  next()
});

我们通过 app.use() 方法收集并使用中间件。

中间件就是一个函数,包含 3 个参数:reqres 还有 next()。在一个中间件内调用 next(),就进入到下一个中间件的执行。

同时,我们还可以为中间件指定路由,这样中间件只在特定路径下起作用。

javascript 复制代码
app.use('/foo', function fooMiddleware(req, res, next) {
  // req.url starts with "/foo"
  next()
})
app.use('/bar', function barMiddleware(req, res, next) {
  // req.url starts with "/bar"
  next()
})

本质上,纯中间件的写法就是在设置根路由'/'),所以会对所有请求有效。

javascript 复制代码
app.use(function middleware1(req, res, next) {
  // middleware 1
  next()
})
// 等同于
app.use('/', function middleware1(req, res, next) {
  // middleware 1
  next()
})

不过还有一类特殊中间件------异常中间件,专门用于处理前面流程里的异常错误。

javascript 复制代码
// regular middleware
app.use(function (req, res, next) {
  // i had an error
  next(new Error('boom!'));
});

// error middleware for errors that occurred in middleware
// declared before this
app.use(function onerror(err, req, res, next) {
  // an error occurred!
});

异常中间件必须是 4 个参数,第一个参数就是 error,对应前面流程中传递给 next()Error 对象。

以上,我们就讲完了 connect 库的基本使用。接下来,就着手实现。

代码实现

基于 connect v3.7.0 版本

刚学 Node.js 的时候,我们学到第一个例子,可能就是启动一个会说"Hello World"的服务器了。

javascript 复制代码
const http = require('node:http')

const hostname = '127.0.0.1'
const port = 3000

const server = http.createServer((req, res) => {
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/plain')
  res.end('Hello World\n')
})

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`)
})

回顾 connect 的使用。

javascript 复制代码
const connect = require('connect')

const app = connect()

// respond to all requests
app.use(function(req, res){
  res.end('Hello from Connect!\n')
})

// create node.js http server and listen on port
app.listen(3000)

实现 app.listen()

我们已经知道 app.listen(3000) 内部实现就是 http.createServer(app).listen(3000)

因此,我们先实现 .listen() 方法。

javascript 复制代码
module.exports = function createApplication() {
  const app = {}

  app.listen = function listen(...args) {
    const server = require('node:http').createServer(/* ? */)
    return server.listen(args);
  }

  return app
}

假设 app 是一个对象。不过,http.createServer(/* ? */) 中的 ? 内容该如何实现呢?

实现 app.use()

前一步,我们做了 app.use() 的调用。

javascript 复制代码
// respond to all requests
app.use(function(req, res){
  res.end('Hello from Connect!\n')
})

所以,当服务启动后,访问 localhost:3000 时,应该返回 "Hello from Connect!" 的文本。

同时,app.use() 又支持重复调用。

javascript 复制代码
// respond to all requests
app.use(function(req, res, next) {
  console.log('log req.url', req.url)
  next()
})

// respond to all requests
app.use(function(req, res) {
  res.end('Hello from Connect!\n')
})

那我们就考虑先用个数组,把通过 app.use() 调用传入进来的回调函数存起来。

javascript 复制代码
module.exports = function createApplication() {
  const app = {}
	app.stack = []
  
  app.use = function use(route, fn) {
  	let path = route
  	let handle = fn

    // default route to '/'
  	if (typeof route !== 'string') {
      path = '/'
      handle = route
    }
    
    this.stack.push({ route: path, handle })
    return this
  }
  
  app.listen = function listen() {
    const server = http.createServer(/* ? */)
    return server.listen.apply(server, arguments)
  }

  return app
}

我们把调用 app.use() 传入的中间件都存到了 app.stack 里。

根据定义可知,http.createServer(/* ? */) 中的 ? 内容应该是一个函数。针对当前场景,它是用来处理 stack 中的这些中间件的。

实现 app.handle()

我们把这些逻辑写在 app.handle() 内。

javascript 复制代码
module.exports = function createApplication() {
  const app = {}
  app.stack = []

  // ...

  app.handle = function handle(res, res) {
    // TODO
  }

  return app
}

每当请求来临,都由 app.handle 负责处理。

app.handle 的主要逻辑主要是处理 3 件事情。

  1. 获取当前要处理的路由,没有的话就交由最终处理函数 done
  2. 路由不匹配就跳过
  3. 路由匹配就执行当前中间件
javascript 复制代码
app.handle = function handle(req, res) {
  let index = 0

  const done = function (err) { /* ... */ }

  function next(err) {
    // next callback
    const layer = app.stack[index++]

    // 1) all done
    if (!layer) {
      setImmdiate(done, err)
      return
    }

    // route data
    const path = require('node:url').parse(req.url).pathname
    const route = layer.route

    // 2) skip this layer if the route doesn't match
    if (!path.toLowerCase().startsWith(route.toLowerCase())) {
      return next(err)
    }

    // 3) call the layer handle
    const arity = handle.length
    const hasError = !!err
    let error = err

    try {
      if (hasError && arity === 4) {
        // error-handling middleware
        layer.handle(err, req, res, next)
        return
      } else if (!hasError && arity < 4) {
        // request-handling middleware
        layer.handle(req, res, next)
        return
      }
    } catch (e) {
      error = e
    }

    next(error)
  }

  next()
}

以上的关键处理就封装在 next() 函数中。而 next() 函数就是传递给 connect 中间件的 next 参数。

这样,每次请求进来,我们都会从 app.stack 的第一个中间件(stack[0])开始处理,就实现了以 next 参数为连接桥梁的中间件机制。

值得注意的是调用当前中间件的逻辑,当我们调用 layer.handle(err, req, res, next)/layer.handle(req, res, next) 时,处理流程会流入中间件内部,当内部调用 next() 函数后,控制权会重新回到 app.handle,继续处理队列中的下一个中间件。

当请求最终没有任何中间件可以处理时,就会流入到 done,这是最终处理器。处理器内部,会根据是否存在错误,分别返回 4045xx 响应。

javascript 复制代码
const done = function (err) {
  if (err) {
    res.statusCode = err.status ?? err.statusCode ?? 500
    res.statusMessage = require('node:http').STATUS_CODES[404]
  } else {
    res.statusCode = 404
    res.statusMessage = `Cannot ${req.method} ${require('node:url').parse(req.url).pathname}`
  }
  res.end(`${res.statusCode} ${res.statusMessage}`)
}

至此,我们基本写完了所有的逻辑。

当然,有一个地方,可以做一个小小的优化。将 http.createServer(app.handle.bind(app)) 简化成 http.createServer(this),不过此时 app 就不能是对象,而是函数了。

javascript 复制代码
module.exports = function createApplication() {
	function app(req, res) { app.handle(req, res) }

  // ...
  
  app.listen = function listen(...args) {
    const server = require('node:http').createServer(app)
    return server.listen(...args)
  }

  // ...
  
	return app
}

最后,我们整体来回顾一下。

javascript 复制代码
module.exports = function createApplication() {
  function app(req, res) { app.handle(req, res) }
  app.stack = []

  app.use = function use(route, fn) {
    let path = route
    let handle = fn
    
    // default route to '/'
    if (typeof route !== 'string') {
      path = '/'
      handle = route
    }

    this.stack.push({ route: path, handle })
    return this
  }

  app.listen = function listen(...args) {
    const server = require('node:http').createServer(app)
    return server.listen(...args)
  }

  app.handle = function handle(req, res) {
    let index = 0

    const done = function (err) {
      if (err) {
        res.statusCode = err.status ?? err.statusCode ?? 500
        res.statusMessage = require('node:http').STATUS_CODES[404]
      } else {
        res.statusCode = 404
        res.statusMessage = `Cannot ${req.method} ${require('node:url').parse(req.url).pathname}`
      }
      res.end(`${res.statusCode} ${res.statusMessage}`)
    }

    function next(err) {
      // next callback
      const layer = app.stack[index++]

      // 1) all done
      if (!layer) {
        setImmediate(done, err)
        return
      }

      const path = require('node:url').parse(req.url).pathname
      const route = layer.route
      
      // 2) skip this layer if the route doesn't match
      if (!path.toLowerCase().startsWith(route.toLowerCase())) {
        return next(err)
      }

      // 3) call the layer handle
      const arity = handle.length
      const hasError = !!err
      let error = err

      try {
        // error-handling middleware
        if (hasError && arity === 4) {
          layer.handle(err, req, res, next)
          return
        // request-handling middleware
        } else if (!hasError && arity < 4) { 
          layer.handle(req, res, next)
          return
        }
      } catch (e) {
        error = e
      }

      next(error)
    }

    next()
  }
  
  return app
}

连上注释,我们只用了 82 行代码,就实现了 connect 的主要功能。

总结

本文带大家实现了轻量级 Web 框架 connect 的主要功能,同样这也是一个简易版本 Express!

实现核心是 2 个函数。

  • app.use(route, fn):用于收集中间件
  • app.handle(res, req):用于消费中间件。主要逻辑位于 next() 函数,这是传递给中间件的 next 参数。每一次接收请求来临时,都由 app.handle 负责处理

而这两个函数之间的桥梁就是 app.stack

行文最后,给大家留一个思考题。

connect() 实例的真实实现,是支持作为子应用,挂载到父应用之上的,也就是下面的用法。

javascript 复制代码
const connect = require('connect')
const app = connect()
const blogApp = connect()

app.use('/blog', blogApp)
app.listen(3000)

甚至 http.Server 实例也支持挂载。

javascript 复制代码
const connect = require('connect')
const app = connect()

const blog = http.createServer(function(req, res){
  res.end('blog')
})

app.use('/blog', blog)

那是如何实现呢?

大家可以参照 app.use() 函数的源码进行学习。

感谢的你的阅读,再见~

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax