如何使用nodejs编码基本的HTTP服务器
#javascript #node #backend #http

编程学习和进步的一种好方法是尝试重新编码现有项目。这是我在本文中提出的,以了解有关Node.js的更多信息。

本文针对初学者,而没有在本环境的基本概念上居住。随时访问Node.jsJavaScript上的文章列表以了解更多信息。

â€to介绍我有关技术和业务on my personal blog的更多博客文章! -

当我开始学习Node.js时,HTTP服务器是所有文章,博客和其他课程中引用的典型示例。目的是显示使用Node.js创建Web服务器的容易性。

const http = require('http')

const server = http.createServer(function (req, res) {
  res.writeHead(200)
  res.end('Hello, World!')
})

server.listen(8080)

“你好,世界!” nodejs

好吧,我们很快意识到,要归功于Node.js,拥有真实网站需要更多的代码。这就是为什么Node.js的后端框架迅速乘以。其中最著名的称为Express。

Express易于使用,快速设置并拥有一个非常大的社区。

const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(3000)

“你好,世界!”由Expressjs

无需进一步使代码复杂化,上面的代码允许您返回HTTP方法和特定路由的字符串。以与/相同的方式添加路线非常简单。

Express非常轻巧。它使用中间件系统,该系统增加了框架的功能。

例如,要允许Express了解HTTP请求正文中的JSON,您必须使用bodyParser Middleware。

Express如何工作?

对于那些在本文之前已经知道Express的人,甚至现在发现它的人 - 您是否曾经想过Express是如何工作的?

如果我们编码一个小型网络框架以了解我们的工具的工作原理怎么办?不要误解我的意图。本文的目的不是在生态系统中编写已经饱和的生态系统中的另一个框架,甚至更少以生产生产准备代码。

此外,我建议您不要在项目中重新发明轮子。 Node.js拥有丰富的开源模块,可满足您的所有需求,请随时进行挖掘。

另一方面,即使在日常生活中最好使用现有的后端框架,这并不能阻止您为纯粹的教育目的编码一个框架!

让我们在node.js中代码一个后端框架

当我把node.js的“你好,world!这将作为我们的目标。

import { createServer } from './lib/http/create-server.js'
import { url, router } from './lib/http/middlewares/index.js'
import { get } from './lib/http/methods/index.js'

const server = createServer(
  url(),
  router(
    get('/', () => `Hello, World!`),
  )
)

server.listen(3000)

“你好世界!”我们的“从头开始”框架

此代码与Express中的代码完全相同。语法有很大的不同之处 - 这是greatly inspired by RxJS.pipe()ââ€,但它仍在展示“你好,世界!”当用户访问/并返回404时,如果去了一条未知的路线。

在这个想法中,我们找到了请求将通过该响应以返回客户端的响应(用户的浏览器)的Express Middlewares。

How to code a basic HTTP server using NodeJS

这是一个非常简化的图,但是您明白了。

您可能已经理解了这一点,这要归功于语法(以及对RXJS的引用),我希望我们能够在此项目中具有相当功能的方法。做得好后,我发现它产生了更具表现力的代码。

HTTP服务器

实现的第一个功能是createServer。它只是node.js的http模块中同名功能的包装器。

import http from 'http'

export function createServer() {
  return http.createServer(() => {
    serverResponse.end()
  })
}

createServer创建服务器并将其返回。然后,我们可以使用.listen()启动服务器。

http.createServer()回调函数可以将IncommingMessage(客户端的请求)和ServerResponse(我们想返回到它的响应)作为参数。

我们的目标之一是拥有一个中间件系统,该系统将依次修改请求,以逐步构建响应以返回给客户端。为此,我们需要每次都会通过请求和响应的中间列表。

import http from 'http'

export function createServer(middlewares = []) {
  return http.createServer((incommingMessage, serverResponse) => {
    for (const middleware of middlewares) {
      middleware(incommingMessage, serverResponse)
    }

    serverResponse.end()
  })
}

要检索中间Wares并具有与RXJS的.pipe()相同的语法,我使用了我们在上一篇文章中看到的rest parameter

import http from 'http'

export function createServer(...middlewares) {
  return http.createServer((incommingMessage, serverResponse) => {
    for (const middleware of middlewares) {
      middleware(incommingMessage, serverResponse)
    }

    serverResponse.end()
  })
}

因此,诸如urlrouter之类的中间件可以通过参考来修改incommingMessage和/或serverResponse。在修改管道结束时,我们触发了.end()事件,该事件将向客户端发送响应。

一些事情仍然困扰着我这个代码:

  • incommingMessageserverResponse是直接修改的。我希望他们不要更接近功能编程的哲学。
  • 必须在我们的实施中调用所有中间件,但是Express允许您在必要时停止修改的管道。例如,这对于身份验证中间件很有用。
  • 某些中间件可能需要阻止修改管道的执行,以执行稍长的任务。目前,我们的代码不会等待,并且某些中间件的执行可能会随时间重叠。

所以让我们修复所有这些。首先,最后一点。这是最简单的设置。

import http from 'http'

export function createServer(...middlewares) {
  return http.createServer(async (incommingMessage, serverResponse) => {
    for (const middleware of middlewares) {
      await middleware(incommingMessage, serverResponse)
    }

    serverResponse.end()
  })
}

因此,如果中间件是异步执行的,我们将等待其执行的结束。

为了避免修改incommingMessageserverResponse,我认为最简单的方法是使用中间Wares的返回值。这些是简单的函数,让我们这样使用:输入值不得修改,而是用于构建返回值的输入值。然后将此返回值用作以下(中间件)函数的输入值。等等。

import http from 'http'

export function createServer(...middlewares) {
  return http.createServer(async (incommingMessage, serverResponse) => {
    let requestContext = {
      statusCode: 200,
    }

    for (const middleware of middlewares) {
      requestContext = await middleware(incommingMessage, requestContext)
    }

    serverResponse.writeHead(requestContext.statusCode)

    if (requestContext.responseBody != null) {
      serverResponse.end(requestContext.responseBody)
      return
    }

    serverResponse.end()
  })
}

我创建了一个requestContext,这是中间件的工作对象。它作为输入值与请求一起传递给所有中间件。

中间Wares处理这些值并创建一个新的上下文,然后它们返回。我们用新的上下文覆盖旧的上下文。

当我们通过中间Wares修改上下文时,我们会使用它来发出服务器的响应。

最后,为了能够在中间停止管道,我发现没有什么比小的boolean更好了。我们可以将其添加到上下文中。如果中间件将其更改为true,则管道被损坏,并且响应直接以当前上下文发送:

import http from 'http'

export function createServer(...middlewares) {
  return http.createServer(async (incommingMessage, serverResponse) => {
    let requestContext = {
      statusCode: 200,
      closeConnection: false
    }

    for (const middleware of middlewares) {
      if (requestContext.closeConnection === true) {
        break
      }

      requestContext = await middleware(incommingMessage, requestContext)
    }

    serverResponse.writeHead(requestContext.statusCode)

    if (requestContext.responseBody != null) {
      serverResponse.end(requestContext.responseBody)
      return
    }

    serverResponse.end()
  })
}

解析请求URL

让我们继续写我们的第一个中间件:url。其目的是解析请求的URL,以向我们提供其他中间人可能需要的信息。

export function url() {
  return (incomingMessage, requestContext) => ({ ...requestContext })
}

中间件API在我们的框架中很简单。

这是一个函数,可以作为参数的操作所需的所有内容或允许用户修改其操作。这里url不使用任何参数。

此功能将返回另一个功能,该功能将在createServer回调的for loop (const middleware of middlewares)稍后执行。正是此功能将incommingMessagerequestContext作为参数,然后返回requestContext的新版本。

export function url() {
  return (incomingMessage, requestContext) => ({
    ...requestContext,
    url: new URL(incomingMessage.url, `http://${incomingMessage.headers.host}`),
  })
}

url中间件添加到请求的上下文中,url属性包含一个解析的koude33 object

因此,我们可以制作requestContext.url.pathname并可以访问请求的pathname

我们框架的路由器

我们框架的路由器也将是可与createServer一起使用的中间件。

使其简单,我们将定义一条路线,如下:

const route = {
  method: 'GET',
  pathname: '/',
  controller(incommingMessage, requestContext) {
    return `Hello, World!`
  }
}

此对象由我们的路由器的三个基本信息组成:

  • 关心的pathname
  • 使用的HTTP动词
  • 能够生成一些返回客户端的功能

作为我们router中间件的输入,我们将采用一系列路线。

export function router(routes = []) {
  return (incommingMessage, requestContext) => {
    for (const route of routes) {
      if (route.pathname !== requestContext.url.pathname) continue
      if (route.method !== requestContext.method) continue

      return {
        ...requestContext,
        responseBody: route.controller(incommingMessage, requestContext),
      }
    }
  }
}

每次客户端向服务器提出请求时,路由器都会向下滚动路由列表,并检查哪个与客户端请求匹配。一旦找到一个,它就会停止循环并执行路由的控制器功能。

然后可以将controller返回的内容添加到上下文中作为响应机构。这是服务器返回客户端的内容。

export function router(routes = []) {
  return (incommingMessage, requestContext) => {
    for (const route of routes) {
      if (route.pathname !== requestContext.url.pathname) continue
      if (route.method !== requestContext.method) continue

      return {
        requestContext,
        responseBody: route.controller(incommingMessage, requestContext),
        closeConnection: true,
      }
    }

    return {
      ...requestContext,
      statusCode: 404,
    }
  }
}

使客户了解路由器找不到请求的路由时会发生什么,我添加了循环后的404错误代码处理。循环中的返回完全停止函数,因此,如果函数在循环的中间没有停止,则一定是我们在声明的路由中没有找到匹配项。因此,我们在404上使用statusCode返回上下文。

使用此功能,我们可以按以下方式声明我们的路线:

router([
  { 
    method: 'GET', 
    pathname: '/', 
    controller() { 
      return `Hello, World!` 
    } 
  },
])

一切正常,我们可以在那里停留,但是我们没有与文章开头显示的代码完全相同的代码。

缺少的只是句法糖:代码只会使我们的功能更容易和更有趣。

export function get(pathname, controller) {
  return {
    method: 'GET',
    pathname,
    controller,
  }
}

此功能仅用于返回router函数所需的路由对象。这里没什么复杂的。

export function get(pathname, controller) {
  if (typeof controller !== 'function') {
    throw new Error('The get() HTTP method needs a controller to work as expected')
  }

  if (typeof pathname === 'string') {
    throw new Error('The get() HTTP method needs a pathname to work as expected')
  }

  return { method: 'GET', controller, pathname }
}

此功能的另一个优点是,它允许我们完全简单地测试我们的数据而不使路由器的代码复杂化。

现在使用的路由器如下:

router([
  get('/', () => `Hello, World!`)
])

我们非常接近要实现的结果。最后一个更改是在实施router本身时进行的。

export function router(...routes) {
  return (incommingMessage, requestContext) => {
    for (const route of routes) {
      if (route.pathname !== requestContext.url.pathname) continue
      if (route.method !== requestContext.method) continue

      return {
        requestContext,
        responseBody: route.controller(incommingMessage, requestContext),
        closeConnection: true,
      }
    }

    return {
      ...requestContext,
      statusCode: 404,
    }
  }
}

i仅修改了routes参数,因此它不再是一个数组,而是一个rest参数,如createServer

因此,我们实现了我们的目标并结束了本文。感谢您的辛勤工作:

import { createServer } from './lib/http/create-server.js'
import { url, router } from './lib/http/middlewares/index.js'
import { get } from './lib/http/methods/index.js'

const server = createServer(
  url(),
  router(
    get('/', () => `Hello, World!`),
  )
)

server.listen(3000)

如果您想更深入地了解本文中的概念或进一步了解: