簡易實現一個express

慕晨同學發表於2019-03-03

寫在前面

Express是一個簡潔、靈活的node.js Web應用開發框架,它提供一系列強大的特性,幫助你建立各種web和移動應用。豐富的HTTP快捷方法和任意排列組合的Connect中介軟體,讓你建立健壯、友好的API變得既快捷又簡單。

本文將從以下幾部分進行總結:

  1. 路由的實現
  2. 中介軟體的實現
  3. 內建中介軟體實現

路由的實現

簡單地說,就是根據方法和路徑執行匹配成功後執行對應的回撥函式。以下為路由的使用例子:

// express是一個函式
let express = require(`./express-route`)
// 監聽函式
let app = express()

// RESTFUL API根據方法名的不同 做對應的資源的處理
app.get(`/name`, (req, res) => {
  res.end(`name`)
})

app.get(`/age`, (req, res) => {
  res.end(`9`)
})

app.post(`/name`, (req, res) => {
  res.end(`post name`)
})

// all匹配所有的方法, *表示匹配所有的路徑
app.all(`*`, function(req, res) {
  res.end(req.method)
})

app.listen(3000, () => {
  console.log(`server start 3000`)
})
複製程式碼

通過觀察上面的例子可以發現,可以定義一個express-route.js檔案,並暴露出express函式,get,post,all,listen等方法,將其引入即可執行上面的例子。以下為簡易實現express路由的程式碼:

let http = require(`http`);
let url = require(`url`);
function createApplication() {
  // 監聽函式
  let app = (req, res) => {
    // 取出每一個layer
    // 1. 獲取請求的方法
    let getMethods = req.method.toLowerCase();
    let { pathname } = url.parse(req.url, true);

    for (let i = 0; i < app.routes.length; i++) {
      let {method, path, handler} = app.routes[i];
      if ((method === getMethods || method === `all`) && (path === pathname || path === `*`)) {
        // 匹配成功後執行對應的callback
        handler(req, res);
      }
    }
    res.end(`Cannot ${getMethods} ${pathname}`)
  }
  // 存放路由
  app.routes = [];
  // 實現all方法
  app.all = function(path, handler) {
    let layer = {
      method: `all`, // 表示全部匹配
      path,
      handler
    }
    app.routes.push(layer);
  }
  // http.METHODS可以獲取所有的http方法
  http.METHODS.forEach(method => {
    // 將方法名轉換成小寫的
    method = method.toLocaleLowerCase();
    // 實現get,post等方法
    app[method] = function(path, handler) {
      let layer = {
        method,
        path,
        handler
      }
      app.routes.push(layer);
    }
  })
  // 實現get方法
  // app.get = function(path, handler) {
  //   let layer = {
  //     method: `get`,
  //     path,
  //     handler
  //   }
  //   app.routes.push(layer);
  // }
  // 實現listen方法
  app.listen = function() {
    let server = http.createServer(app)
    server.listen(...arguments)
  }
  return app
}

module.exports = createApplication;
複製程式碼

通過向外暴露一個createApplication函式,並返回app物件。通過http.METHODS可以獲取所有的http方法,在app物件裡定義listen,all還有get,post等請求方法。當客戶端請求時即遍歷routes,當匹配到理由時,即執行相應的handler函式。

中介軟體的實現

簡易實現一個express

如上圖所示,中介軟體就是處理HTTP請求的函式,用來完成各種特定的任務,比如檢查使用者是否登入,檢查使用者是否有許可權訪問,執行一個請求需要多長時間等。它的特點是:

  • 一箇中介軟體處理完請求和響應後可以把相應的資料再傳遞給下一個中介軟體
  • 回撥函式的next參數列示接受其他中介軟體的的呼叫
  • 可以根據路徑區分返回執行不同的的中介軟體
    以下為express中介軟體的簡單使用例子:
// middleware use
// 中介軟體 在執行路由之前 要幹一些處理工作 就可以採用中介軟體
let express = require(`./express-middleware.js`);

let app = express();

// use方法第一個引數預設預設就是/
// 中介軟體可以擴充套件一些方法
app.use(`/name`, function(req, res, next) {
  res.setHeader(`Content-Type`,`text/html;charset=utf-8`);
  console.log(`middleware1`);
  next(`名字不合法`)
})

app.use(`/`, function(req, res, next) {
  console.log(`middleware2`);
  next();
})

app.get(`/name`, (req, res, next) => {
  // res.setHeader(`Content-Type`,`text/html;charset=utf-8`);
  res.end(`姓名`);
  next(`名字不合法`)
})

app.get(`/age`, (req, res) => {
  console.log(req.path);
  console.log(req.hostname);
  console.log(req.query);
  res.end(`年齡`);
})

// 錯誤中介軟體(4個引數)放在路由的下面
app.use(function(err, req, res, next) {
  console.log(err)
  next()
})

app.listen(3001, () => {
  console.log(`server start 3001`)
})
複製程式碼

通過觀察上面的例子可以發現,app.use方法呼叫中介軟體,其中next為非常重要的一個引數。要執行上面的例子,可以定義一個express-middware.js,來實現app.use方法,如果執行next,會呼叫下一個下一個中介軟體。其大致原理為:定義一個next函式並定義一個index索引值,每呼叫一次next,索引值加1。如果當前的layer與routes中的項相等,則匹配成功。實現express中介軟體的簡易程式碼如下:

let http = require(`http`);
let url = require(`url`);
function createApplication() {
  // 監聽函式
  let app = (req, res) => {
    // 取出每一個layer
    // 獲取請求的方法
    let getMethods = req.method.toLowerCase();
    let { pathname } = url.parse(req.url, true);

    // 通過next方法進行迭代
    let index = 0;
    function next(err) {
      // 如果陣列全部迭代完成還沒有找到,說明路徑不存在
      if (index === app.routes.length) {
        return res.end(`Cannot ${getMethods} ${pathname}`)
      }
      // 每次呼叫next方法呼叫下一個layer
      let { method, path, handler } = app.routes[index++];
      if (err) {
        // 如果有錯誤,應該去找錯誤中介軟體,錯誤中介軟體有一個特點,handler有4個引數
        if (handler.length === 4) {
          handler(err, req, res, next)
        } else {
          // 如果沒有匹配到,要將err繼續傳遞下去
          next(err); // 繼續找下一個layer進行判斷
        }
      } else {
        // 處理中介軟體
        if (method === `middleware`) {
          if (path === `/` || path === pathname || pathname.startsWith(path + `/`)) {
            handler(req, res, next);
          } else {
            // 如果這個中介軟體沒有匹配到,那麼繼續走下一個中介軟體匹配
            next(); 
          }
        } else { // 處理路由
          if ((method === getMethods || method === `all`) && (path === pathname || path === `*`)) {
            // 匹配成功後執行對應的callback
            handler(req, res);
          } else {
            next();
          }
        }
      }
    }
    // 中介軟體中的next方法
    next();

    res.end(`Cannot ${getMethods} ${pathname}`)
  }
  // 存放路由
  app.routes = [];
  // 實現use方法
  app.use = function(path, handler) {
    // use方法第一個引數可以預設,預設是/
    if (typeof handler !== `function`) {
      handler = path;
      path = `/`;
    }
    let layer = {
      // 約定method是`middleware`就表示是一箇中介軟體
      method: `middleware`, 
      path,
      handler
    }
    // 將中介軟體放在容器內
    app.routes.push(layer);
  }
  // express內建中介軟體
  app.use(function(req, res, next) {
    let { pathname, query } = url.parse(req.url, true);
    let hostname = req.headers[`host`].split(`:`)[0];
    req.path = pathname;
    req.query = query;
    req.hostname = hostname;
    next();
  })
  // 實現all方法
  app.all = function(path, handler) {
    let layer = {
      method: `all`, // 表示全部匹配
      path,
      handler
    }
    app.routes.push(layer);
  }
  // http.METHODS可以獲取所有的http方法
  http.METHODS.forEach(method => {
    // 將方法名轉換成小寫的
    method = method.toLocaleLowerCase();
    // 實現get,post等方法
    app[method] = function(path, handler) {
      let layer = {
        method,
        path,
        handler
      }
      app.routes.push(layer);
    }
  })
  // 實現listen方法
  app.listen = function() {
    let server = http.createServer(app)
    server.listen(...arguments)
  }
  return app
}

module.exports = createApplication;
複製程式碼

內建中介軟體的實現

express通過中介軟體的形式擴充套件了很多的內建API,舉一個簡單的例子:

  app.use(function(req, res, next) {
    let { pathname, query } = url.parse(req.url, true);
    let hostname = req.headers[`host`].split(`:`)[0];
    req.path = pathname;
    req.query = query;
    req.hostname = hostname;
    next();
  })
複製程式碼

即可在req封裝hostname, path, query, hostname。

相關文章