手撸手, 用 Node.js 摸一个仿 express 的 http 服务器

更多个人博客:(https://github.com/zenglinan/blog)

如果对你有帮助,欢迎star。

用 Node.js 可以快速搭建一个 http 服务器, 本文将会手把手从头搭建, 最后还会实现一个简易的 express 服务器, 开始吧~

1. 搭建基本雏形

首先, 搭建 http 服务器, 需要先引用 Node.js 的核心模块: http, 实际上这是一个对象, 利用 http 上的 createServer 方法来创建一个 http 服务器, createServer 方法需要接受一个事件函数, 用于处理请求和响应

const http = require('http')
const server = http.createServer((req, res) => {
  // req 是请求对象, 可以获取请求的一些方法路径数据等属性
  // res 是响应对象, 可以进行设置响应数据等操作
})

到现在, 这个 http 服务器的雏形已经基本搭建好了!

但是想想还差点东西, 一个 url 里包含 协议 域名 端口, 我们还没指定端口呢.

const http = require('http')
const server = http.createServer((req, res) => {
})
server.listen(8000) // 监听 8000 端口

OK, 大功告成

2. 响应数据

现在这个 http 服务器的框架已经搭好了. 启动一下, 在浏览器输入localhost:8000试一下吧

什么? 你说你试了一下, 没有响应?

当然, 我们这里还没有返回任何数据呢, 如果没有访问数据, 浏览器端肯定是显示无响应的.

这里我们先随便返回点数据给浏览器, 然后重启服务器

const http = require('http')
const server = http.createServer((req, res) => {
  res.end('这是我返回的数据噢!')
})
server.listen(8000)

相信你已经看到页面上显示的....一堆乱码了吧_, 是的, 因为 JavaScript 默认字符集对中文的支持不好. 我们需要指定一下返回数据的 Content-Type

const http = require('http')
const server = http.createServer((req, res) => {
  res.setHeader("Content-Type","text/html;charset=utf-8")
  res.end('这是我返回的数据噢!')
})
server.listen(8000)

3. 处理路由

接下来, 我们需要对路由进行处理, 现在我们不管访问什么路径, 都统一返回一样的数据.

接下来我们实现一下, 访问 /a , /b, /c 三个路由, 返回不同的数据

之前说过, req 对象上存放着请求的一些属性. req.url 上记录着请求的路径, 获取后就可以判断访问路径来返回不同的数据了

const {url} = req

完整代码:

const http = require('http')
const server = http.createServer((req, res) => {
  const {url} = req
  res.setHeader("Content-Type","text/html;charset=utf-8")
  if(url === '/a') res.end(`访问a路由`)
  else if(url === '/b') res.end(`访问b路由`)
  else if(url === '/c') res.end(`访问c路由`)
})
server.listen(8000)

4. 处理查询参数

接下来, 我们对查询参数做一下处理, 这时候, 是不是想到了什么, 上面我们的 url 没有考虑到查询参数的情况, 路由里应该滤除掉查询参数, 我们来一并处理

我们知道, 查询参数的形式是 a=x&b=x, 这里为了方便使用, 我们引用另一个模块 querystring, 他可以把查询参数字符串切割成键值对形式

const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
  const {url} = req
  const path = url.split('?')[0]
  const query = querystring.parse(url.split('?')[1])  // 保存着查询参数的对象

  res.setHeader("Content-Type","text/html;charset=utf-8")

  if(path === '/a') res.end(`访问a路由, 查询参数对象为${JSON.stringify(query)}`)  // 返回序列化的查询参数
  else if(path === '/b') res.end(`访问b路由`)
  else if(path === '/c') res.end(`访问c路由`)
})
server.listen(8000)

5.处理 POST 请求

OK, 接下来要做啥呢? 想了想, 我们目前好像只处理了 GET 请求, 那我们来处理一下 POST 请求吧.

一样的, 请求的 method 可以通过req.method获取

这里要注意: req 对象实现了 ReadableStream 接口, 我们可以用信息流的方式读取传来的数据 (关于的概念可以看后面我的文章)

let postData = ''
req.on('data', chunk => {  // 接收数据流
  postData += chunk.toString()  // 拼接信息流, 注意chunk是二进制格式, 需要转为二进制
})
req.on('end', () => {
  // 接收数据流完毕的回调函数
})

完整代码 :

const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
  const {url, method} = req
  const path = url.split('?')[0]
  const query = querystring.parse(url.split('?')[1])  

  res.setHeader("Content-Type","text/html;charset=utf-8")

  if(path === '/a' && method === 'GET') res.end(`访问a路由, 查询参数对象为${JSON.stringify(query)}`)
  else if(path === '/b' && method === 'GET') res.end(`访问b路由`)
  else if(path === '/c' && method === 'GET') res.end(`访问c路由`)
  else if(path === 'p' && method === 'POST'){
    let postData = ''
    req.on('data', chunk => {  // 接收数据流
      postData += chunk.toString()  // 拼接信息流, 注意chunk是二进制格式, 需要转为二进制
    })
    req.on('end', () => {
      res.end(`我接受到了数据了:${postData}`)
    })
  }
})
server.listen(8000)

OK, 来回顾一下我们做了什么:

  • 我们创建了一个基本的 http 服务器
  • 对路由做了处理
  • 获取了查询参数
  • 处理了 POST 请求

6. 优化路由处理

我们现在来回顾一下自己的代码, 可以看到, 在路由处理的部分有一堆的 if else, 假如每多一个路由就多一个 if , 那就太冗余了.

这里我们用一个数组来存放一个个路由对象, 路由对象里包含了路径, 方法, 回调等必要信息

const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
  const {url, method: realMethod} = req
  const realPath= url.split('?')[0]
  const query = querystring.parse(url.split('?')[1])
  let router = []  // 存放路由信息 

  res.setHeader("Content-Type","text/html;charset=utf-8")
  
  router.push({
    path: '/a',
    method: 'GET',
    handler(req, res){
      res.end(`访问a路由, 查询参数对象为${JSON.stringify(query)}`)
    }
  })
  router.push({
    path: '/b',
    method: 'GET',
    handler(req, res){
      res.end(`访问b路由`)
    }
  })
  router.push({
    path: '/c',
    method: 'GET',
    handler(req, res){
      res.end(`访问c路由`)
    }
  })
  router.push({
    path: '/p',
    method: 'POST',
    handler(req, res){
      let postData = ''
      req.on('data', chunk => {
        postData += chunk.toString()
      })
      req.on('end', () => {
        res.end(`我接受到了数据了:${postData}`)
      })
    }
  })

  // 统一处理路由
  router.forEach(route => {
    let {path, method, handler} = route
    console.log(realPath, realMethod)
    if(realPath === path && realMethod === method){
      return handler()
    }
  })
})
server.listen(8000)

7. 改写为 express 形式

是不是感觉稍微好看一点了, 加一个路由就 push 一个路由对象.

我们离最终目标很接近了, 接下来, 让我们模仿 express , 写一个 express 形式的 http 服务器()

先来看看 express 是怎么写的

const express = require("express");
const app = express(); 
app.get("/a", 
  (req, res) => { 
    res.end("a路由");
  }
);

app.get("/b", 
  (req, res) => {
    res.end('b路由');
  });

app.listen(3000, () => {
  console.log("Example app listen at 3000");
});

可以看到, 导出的 express 是一个函数, 函数内部会 new 一个实例对象出来, 大概的架子便是这样.

const http = require('http')
class Express{
  
}
module.exports = function(){
    return new Express()
}

接下来让我们实现完整代码:

const http = require('http')
class Express{
  constructor(){
    this.router = []  // 存放路由对象
  }
  get(path, handler){
    this.router.push({
      path,
      method: 'GET',
      handler
    })
  }
  post(path, handler){
    this.router.push({
      path,
      method: 'POST',
      handler
    })
  }
  listen(port, listenCallback){
    const server = http.createServer((req,res) => {
      const {url, method:realMethod} = req
      const realPath = url.split('?')[0]
      this.router.forEach((route) => {  // 遍历路由对象
        const {path, method, handler} = route
        if(realPath === path && method === realMethod){
          handler(req, res)
        }
      })
    })
    server.listen(port)
    listenCallback()
  }
}

module.exports = function(){
  return new Express()
}

到这里, 我们已经简单将我们的 http 服务器改写成 express 形式了, 不过考虑的细节还远远不够 express 那么完善.

撒花ヾ(◍°∇°◍)ノ゙

你可能感兴趣的:(手撸手, 用 Node.js 摸一个仿 express 的 http 服务器)