手擼手, 用 Node.js 摸一個仿 express 的 http 服務器

用 Node.js 能夠快速搭建一個 http 服務器, 本文將會手把手從頭搭建, 最後還會實現一個簡易的 express 服務器, 開始吧~
javascript

1. 搭建基本雛形

首先, 搭建 http 服務器, 須要先引用 Node.js 的核心模塊: http, 實際上這是一個對象, 利用 http 上的 createServer 方法來建立一個 http 服務器, createServer 方法須要接受一個事件函數, 用於處理請求和響應html

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, 大功告成
java

2. 響應數據

如今這個 http 服務器的框架已經搭好了. 啓動一下, 在瀏覽器輸入localhost:8000試一下吧
什麼? 你說你試了一下, 沒有響應?
固然, 咱們這裏尚未返回任何數據呢, 若是沒有訪問數據, 瀏覽器端確定是顯示無響應的.
git

這裏咱們先隨便返回點數據給瀏覽器, 而後重啓服務器github

const http = require('http')
const server = http.createServer((req, res) => {
  res.end('這是我返回的數據噢!')
})
server.listen(8000)
複製代碼

相信你已經看到頁面上顯示的....一堆亂碼了吧^_^, 是的, 由於 JavaScript 默認字符集對中文的支持很差. 咱們須要指定一下返回數據的 Content-Typeexpress

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 那麼完善.

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

大三小前端一枚, 最近在寫一些博客, 歡迎關注(^▽^)

我的博客地址 : github

相關文章
相關標籤/搜索