当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2024-02-06 10:09:34 415观看
导读本文将带大家实现轻量级 web 框架 connect[1] 的主要功能,只要 82 行代码就能搞定。我并没有标题党,因为 Express 在 v3 版本之前就是基于 connect 进行封装的,不过在 v4 版本就将 connect 依赖移除了[2],代码被搬到 Expr

本文将带大家实现轻量级 web 框架 connect[1] 的主要功能,只要 82 行代码就能搞定。TdZ28资讯网——每日最新资讯28at.com

我并没有标题党,因为 Express 在 v3 版本之前就是基于 connect 进行封装的,不过在 v4 版本就将 connect 依赖移除了[2],代码被搬到 Express 仓库里,并做了一些细微调整。因此某种程度上,学习 connect 就是在学习 Express。TdZ28资讯网——每日最新资讯28at.com

connect 的 repo 描述是:“Connect is a middleware layer for Node.js”,也就是一个 Node.js 的中间件层。中间件层是一个非常有用的机制,它类似一个插件系统,让我们可以通过插拔的方式组合不同功能来处理请求。TdZ28资讯网——每日最新资讯28at.com

基本使用

先来看看 connect 的使用。TdZ28资讯网——每日最新资讯28at.com

const connect = require('connect')const app = connect()// respond to all requestsapp.use(function(req, res){  res.end('Hello from Connect!/n')})// create node.js http server and listen on porthttp.createServer(app).listen(3000)

跟 Express 一样。TdZ28资讯网——每日最新资讯28at.com

另外,app 上还提供了 .listen() 方法,用于替代 http.createServer(app).listen(3000) 的冗长写法。TdZ28资讯网——每日最新资讯28at.com

app.listen(3000) // 等价于 http.createServer(app).listen(3000)

再看看中间件的使用。TdZ28资讯网——每日最新资讯28at.com

app.use(function middleware1(req, res, next) {  // middleware 1  next()});app.use(function middleware2(req, res, next) {  // middleware 2  next()});

我们通过 app.use() 方法收集并使用中间件。TdZ28资讯网——每日最新资讯28at.com

中间件就是一个函数,包含 3 个参数:req、res 还有 next()。在一个中间件内调用 next(),就进入到下一个中间件的执行。TdZ28资讯网——每日最新资讯28at.com

同时,我们还可以为中间件指定路由,这样中间件只在特定路径下起作用。TdZ28资讯网——每日最新资讯28at.com

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()})

本质上,纯中间件的写法就是在设置根路由('/'),所以会对所有请求有效。TdZ28资讯网——每日最新资讯28at.com

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

不过还有一类特殊中间件——异常中间件,专门用于处理前面流程里的异常错误。TdZ28资讯网——每日最新资讯28at.com

// regular middlewareapp.use(function (req, res, next) {  // i had an error  next(new Error('boom!'));});// error middleware for errors that occurred in middleware// declared before thisapp.use(function onerror(err, req, res, next) {  // an error occurred!});

异常中间件必须是 4 个参数,第一个参数就是 error,对应前面流程中传递给 next() 的 Error 对象。TdZ28资讯网——每日最新资讯28at.com

以上,我们就讲完了 connect 库的基本使用。接下来,就着手实现。TdZ28资讯网——每日最新资讯28at.com

代码实现

基于 connect v3.7.0 版本[3]。TdZ28资讯网——每日最新资讯28at.com

刚学 Node.js 的时候,我们学到第一个例子,可能就是启动一个会说“Hello World”的服务器了。TdZ28资讯网——每日最新资讯28at.com

const http = require('node:http')const hostname = '127.0.0.1'const port = 3000const 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 的使用。TdZ28资讯网——每日最新资讯28at.com

const connect = require('connect')const app = connect()// respond to all requestsapp.use(function(req, res){  res.end('Hello from Connect!/n')})// create node.js http server and listen on portapp.listen(3000)

实现 app.listen()

我们已经知道 app.listen(3000) 内部实现就是 http.createServer(app).listen(3000)。TdZ28资讯网——每日最新资讯28at.com

因此,我们先实现 .listen() 方法。TdZ28资讯网——每日最新资讯28at.com

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(/* ? */) 中的 ? 内容该如何实现呢?TdZ28资讯网——每日最新资讯28at.com

实现 app.use()

前一步,我们做了 app.use() 的调用。TdZ28资讯网——每日最新资讯28at.com

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

所以,当服务启动后,访问 localhost:3000 时,应该返回 "Hello from Connect!" 的文本。TdZ28资讯网——每日最新资讯28at.com

同时,app.use() 又支持重复调用。TdZ28资讯网——每日最新资讯28at.com

// respond to all requestsapp.use(function(req, res, next) {  console.log('log req.url', req.url)  next()})// respond to all requestsapp.use(function(req, res) {  res.end('Hello from Connect!/n')})

那我们就考虑先用个数组,把通过 app.use() 调用传入进来的回调函数存起来。TdZ28资讯网——每日最新资讯28at.com

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(...args) {    const server = require('node:http').createServer(/* ? */)    return server.listen(...args)  }  return app}

我们把调用 app.use() 传入的中间件都存到了 app.stack 里。TdZ28资讯网——每日最新资讯28at.com

根据定义可知,http.createServer(/* ? */) 中的 ? 内容应该是一个函数。针对当前场景,它是用来处理 stack 中的这些中间件的。TdZ28资讯网——每日最新资讯28at.com

实现 app.handle()

我们把这些逻辑写在 app.handle() 内。TdZ28资讯网——每日最新资讯28at.com

module.exports = function createApplication() {  const app = {}  app.stack = []  // ...  app.listen = function listen(...args) {    const server = require('node:http').createServer(app.handle.bind(app))    return server.listen(...args)  }  app.handle = function handle(res, res) {    // TODO  }  return app}

每当请求来临,都由 app.handle 负责处理。TdZ28资讯网——每日最新资讯28at.com

app.handle 的主要逻辑主要是处理 3 件事情。TdZ28资讯网——每日最新资讯28at.com

  1. 获取当前要处理的路由,没有的话就交由最终处理函数 done
  2. 路由不匹配就跳过
  3. 路由匹配就执行当前中间件
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 参数。TdZ28资讯网——每日最新资讯28at.com

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

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

当请求最终没有任何中间件可以处理时,就会流入到 done,这是最终处理器。处理器内部,会根据是否存在错误,分别返回 404 或 5xx 响应。TdZ28资讯网——每日最新资讯28at.com

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}`)}

至此,我们基本写完了所有的逻辑。TdZ28资讯网——每日最新资讯28at.com

当然,有一个地方,可以做一个小小的优化。将 http.createServer(app.handle.bind(app)) 简化成 http.createServer(this),不过此时 app 就不能是对象,而是函数了。TdZ28资讯网——每日最新资讯28at.com

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}

最后,我们整体来回顾一下。TdZ28资讯网——每日最新资讯28at.com

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 的主要功能。TdZ28资讯网——每日最新资讯28at.com

TdZ28资讯网——每日最新资讯28at.com

总结

本文带大家实现了轻量级 Web 框架 connect 的主要功能,同样这也是一个简易版本  Express!TdZ28资讯网——每日最新资讯28at.com

实现核心是 2 个函数。TdZ28资讯网——每日最新资讯28at.com

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

而这两个函数之间的桥梁就是 app.stack。TdZ28资讯网——每日最新资讯28at.com

行文最后,给大家留一个思考题。TdZ28资讯网——每日最新资讯28at.com

connect() 实例的真实实现,是支持作为子应用,挂载到父应用之上的,也就是下面的用法。TdZ28资讯网——每日最新资讯28at.com

const connect = require('connect')const app = connect()const blogApp = connect()app.use('/blog', blogApp)app.listen(3000)

甚至 http.Server 实例也支持挂载。TdZ28资讯网——每日最新资讯28at.com

const connect = require('connect')const app = connect()const blog = http.createServer(function(req, res){  res.end('blog')})app.use('/blog', blog)

那是如何实现呢?TdZ28资讯网——每日最新资讯28at.com

大家可以参照 app.use()[4] 函数的源码进行学习。TdZ28资讯网——每日最新资讯28at.com

感谢的你的阅读,再见~TdZ28资讯网——每日最新资讯28at.com

参考资料

[1]connect: https://github.com/senchalabs/connectTdZ28资讯网——每日最新资讯28at.com

[2]在 v4 版本就将 connect 依赖移除了: https://github.com/expressjs/express/compare/3.21.2...4.0.0#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519TdZ28资讯网——每日最新资讯28at.com

[3]connect v3.7.0 版本: https://github.com/senchalabs/connect/blob/3.7.0/index.jsTdZ28资讯网——每日最新资讯28at.com

[4]app.use(): https://github.com/senchalabs/connect/blob/3.7.0/index.js#L76TdZ28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-74660-0.html手把手带你用 82 行代码实现一个简易版 Express 框架

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 「鹅来运转 新春添囍」!小天鹅为尔滨投送龙年新囍福利!

下一篇: 从 0 开始用 PyTorch 构建完整的 NeRF

标签:
  • 热门焦点
  • 6月安卓手机好评榜:魅族20 Pro蝉联冠军

    性能榜和性价比榜之后,我们来看最后的安卓手机好评榜,数据来源安兔兔评测,收集时间2023年6月1日至6月30日,仅限国内市场。第一名:魅族20 Pro好评率:95%5月份的时候魅族20 Pro就是
  • 十个可以手动编写的 JavaScript 数组 API

    JavaScript 中有很多API,使用得当,会很方便,省力不少。 你知道它的原理吗? 今天这篇文章,我们将对它们进行一次小总结。现在开始吧。1.forEach()forEach()用于遍历数组接收一参
  • 摸鱼心法第一章——和配置文件说拜拜

    为了能摸鱼我们团队做了容器化,但是带来的问题是服务配置文件很麻烦,然后大家在群里进行了“亲切友好”的沟通图片图片图片图片对比就对比,简单对比下独立配置中心和k8s作为配
  • 如何通过Python线程池实现异步编程?

    线程池的概念和基本原理线程池是一种并发处理机制,它可以在程序启动时创建一组线程,并将它们置于等待任务的状态。当任务到达时,线程池中的某个线程会被唤醒并执行任务,执行完任
  • 梁柱接棒两年,腾讯音乐闯出新路子

    文丨田静 出品丨牛刀财经(niudaocaijing)7月5日,企鹅FM发布官方公告称由于业务调整,将于9月6日正式停止运营,这意味着腾讯音乐长音频业务走向消亡。腾讯在长音频领域还在摸索。为
  • 机构称Q2国内智能手机销量同比下滑4% vivo份额重回第1

    7月29日消息,根据市场调查机构Counterpoint Research公布的最新报告,2023年第2季度中国智能手机销量同比下降4%,创新自2014年以来第2季度销量新低。报
  • iQOO Neo8 Pro评测:旗舰双芯加持 最强性能游戏旗舰

    【Techweb评测】去年10月,iQOO推出了一款Neo7手机,该机搭载了联发科天玑9000+,配备独显芯片Pro+,带来了同价位段最佳的游戏体验,一经上市便受到了诸多用
  • 联想YOGA 16s 2022笔记本将要推出,屏幕支持触控功能

    联想此前宣布,将于11月2日19:30召开联想秋季轻薄新品发布会,推出联想 YOGA 16s 2022 笔记本等新品。官方称,YOGA 16s 2022 笔记本将搭载 16 英寸屏幕,并且是一
  • 三翼鸟智能家居亮相电博会,让用户体验更真实

    2021电博会在青岛国际会展中心开幕中,三翼鸟直接把“家”搬到了现场,成为了展会的一大看点。这也是三翼鸟继9月9日发布了行业首个一站式定制智慧家平台后的
Top