和 koa 不同的 express 是怎麼實現

夢想攻城獅發表於2018-09-30

koa和express

kao和express都是同一個團隊開發的,koa框架會用也會寫—(koa的實現)已經介紹koa的原理,而koa在express的基礎上進行了優化:

  • koa使用了類的概念,express沒有使用類,而是直接使用函式物件,在上面掛載很多方法
  • koa封裝了ctx屬性,並且在上面掛載了path、url等屬性,而express沒有ctx,所以其屬性直接掛載在res和req上
  • koa將router邏輯從express中抽離出來形成koa-router外掛,所以express中router和中介軟體共用一個佇列,中介軟體預設的路由為'/'
  • koa將static從express中抽離出形成koa-static,express自帶static
  • koa將views從express中抽離出形成koa-views,express自帶static
  • koa將bodyparse從express中抽離出形成koa-bodyparse,express通過body-parser

express的整體框架

從上面的可以知道express的大致框架:

  • express是一個物件,上面掛載了static、view、bodyparse等邏輯方法;express也是一個函式,執行會返回一個app物件
  • app是一個監聽請求時的處理函式,也是一個物件上面掛載了很多方法。
const  http = require('http');
const url = require('url');
cosnt methods = require('methods'); //[get,post.......]

function createApplication() {
  //監聽函式
  function app(req, res) {
    let method = req.method.toLowerCase();
    let { pathname, query } = url.parse(req.url,true);
    let index = 0;
    
    // 建立了一個next函式,用來派發中介軟體、路由,事項洋蔥模型
    function next() { 
      if(app.routes.length == index) return res.end(`Cannot found`)
      let layer = app.routes[index++];
      //這裡中介軟體的處理邏輯和路由的處理邏輯不一樣,下面在介紹
      layer.handler(req,res,next);
    }
    next();
  }
  
  //內部封裝了http模組
  app.listen = function () {
    let server = http.createServer(app);
    server.listen(...arguments)
  }
  
  app.routes = [];  //中介軟體和路由佇列
  
  // 批量建立方法:app.get、app.set.....註冊路由
  methods.forEach(method => {
    app[method] = function (pathname, handler) {
      let layer = {
        pathname,  //路由
        handler, //處理函式
        method  //路由為get、post....,中介軟體為middleware
      }
      
      //處理帶引數的路由/user/:id/:name
      let keys = [];
      if(pathname.includes(":")){
        let regStr = pathname.replace(/:([^/]*)/g,function () {
          keys.push(arguments[1]);
          return '([^\/]*)'
        });
        layer.params = keys;    // 路徑引數key陣列[id,name]
        // 轉化成正則類似/\/user\/([^\/]*)\/([^\/]*)/來匹配請求路徑
        // 後面會介紹使用的地方
        layer.reg = new RegExp(regStr); 
      }
      app.routes.push(layer);
    }
  });
  
  app.all = function (pathname, handler) {
    let layer = {
      pathname,
      handler,
      method:'all'
    }
    app.routes.push(layer);
  }
  
  // 註冊中介軟體
  app.use = function (pathname,handler) {
    if(typeof handler === 'undefined'){
      handler = pathname;
      pathname = '/';  //中介軟體也是一種路由,所有的路由都能匹配
    }
    let layer = {
      pathname,
      handler,
      method:'middleware'   //用來和router的方法區別
    }
    app.routes.push(layer);
  }
  
  return app;
}
module.exports = createApplication;
複製程式碼

擴充套件res和req

function createApplication() {
  function app(req, res) {
    let method = req.method.toLowerCase();
    let { pathname, query } = url.parse(req.url,true);
    let index = 0;
    function next() { 
      if(app.routes.length == index) return res.end(`Cannot found`)
      let layer = app.routes[index++];
      layer.handler(req,res,next);
    }
    next();
  }
  app.listen = function () {
    let server = http.createServer(app);
    server.listen(...arguments)
  }
  app.routes = []; 
  
  app.use = function (pathname,handler) {
    if(typeof handler === 'undefined'){
      handler = pathname;
      pathname = '/';  
    }
    let layer = {
      pathname,
      handler,
      method:'middleware'
    }
    app.routes.push(layer);
  }
  
  // 用app.use註冊了內建中介軟體來擴充套件req和res的
  app.use(function (req,res,next) { 
    let method = req.method.toLowerCase();
    let { pathname, query } = url.parse(req.url, true);
    req.path = pathname;
    req.query = query;
    req.hostname = req.headers.host.split(':')[0];
    
    //res.send對返回各種型別相容處理
    res.send = function (params) {
      res.setHeader('Content-Type', 'text/html;charset=utf8');
      if (typeof params === 'object') {//返回json物件
        res.setHeader('Content-Type', 'application/json;charset=utf8');
        res.end(util.inspect(params));
      } else if (typeof (params) === 'number') {//數字對應狀態碼
        res.statusCode = params;
        res.end(require('_http_server').STATUS_CODES[params]);
      } else {
        res.end(params);
      }
    }
    
    //res.sendFile返回檔案
    res.sendFile = function (pathname) {
      res.setHeader('Content-Type', require('mime').getType(pathname) + ';charset=utf8');
      fs.createReadStream(pathname).pipe(res);
    }
    
    //res.redirect重定向
    res.redirect = function (pathname) {
      res.statusCode = 302;
      res.setHeader('Location',pathname);
      res.end();
    }
    next();
  })
  return app;
}
module.exports = createApplication;
複製程式碼

區分處理中介軟體和路由

function app(req, res) {
    let method = req.method.toLowerCase();
    let { pathname, query } = url.parse(req.url,true);
    let index = 0;
    function next(err) {
        let layer = app.routes[index++];
        if(layer){
            //中介軟體的處理,包含存在請求路徑處理exp:/user/info匹配/user/
            if (layer.method === 'middle') {
            if (layer.pathname === '/' || req.path.startsWith(layer.pathname + '/') || req.path === layer.pathname) {
              return layer.handler(req, res, next); //把控制權next給了使用者
            } else {
              next(); // 匹配不到路徑就執行next()匹配下一個中介軟體
            }
          } else {
            //router處理含請求引數的路由
            if (layer.params) {
              if (layer.method === method && (layer.reg.test(req.path))) {
                
                // layer.reg => /\/user\/([^\/]*)\/([^\/]*)/
                // req.path => '/user/1/kbz'
                // matchers => ['/user/1/kbz','1','2']
                // layer.params => [id,name]
                // req.params => {id:'1',name:'kbz'}
                
                let matchers = req.path.match(layer.reg);
                req.params = layer.params.reduce((memo, current, index) => (memo[current] = matchers[index + 1], memo), {});
                return layer.handler(req, res);
              }
            }
            //router處理不含請求引數的路由
            if ((layer.pathname === req.path || layer.pathname === '*') && (layer.method === method || layer.method === 'all')) {
              return layer.handler(req, res);
            }
            next()  //router的處理會自動呼叫next()
          }
      }else{
        res.end(`Cannot ${pathname} ${method}`);
      }
    }
    next();
  }
複製程式碼

next(err)錯誤處理

function app(req, res) {
    let method = req.method.toLowerCase();
    let { pathname, query } = url.parse(req.url,true);
    let index = 0;
    function next(err) {
      let layer = app.routes[index++];
      if(layer){
        if(err){
          //處理錯誤,應該找到錯誤處理中介軟體,特點是擁有4個引數
          //由使用者定義,放在對列最後
          if (layer.method === 'middle' && layer.handler.length===4 ){
            return layer.handler(err,req,res,next)
          }else{
            next(err);  //不是錯誤處理中介軟體,就向後繼續查詢
          }
        }else{
          if (layer.method === 'middle') { 
            if (layer.pathname === '/' || req.path.startsWith(layer.pathname + '/') || req.path === layer.pathname) {
              return layer.handler(req, res, next); 
            } else {
              next(); 
            }
          } else {
            if (layer.params) {
              if (layer.method === method && (layer.reg.test(req.path))) {
                let matchers = req.path.match(layer.reg);
                req.params = layer.params.reduce((memo, current, index) => (memo[current] = matchers[index + 1], memo), {});
                return layer.handler(req, res);
              }
            }
            if ((layer.pathname === req.path || layer.pathname === '*') && (layer.method === method || layer.method === 'all')) {
              return layer.handler(req, res);
            }
            next()
          }
        }
      }else{
        res.end(`Cannot ${pathname} ${method}`);
      }
    }
    next();
  }
複製程式碼

內建view渲染邏輯

使用express渲染邏輯主要是呼叫res.render方法,其中使用最多的就是ejs模板引擎,ejs渲染邏輯可以參考koa框架會用也會寫—(koa-view、koa-static)

app.set('views','view');    //渲染檔案目錄
app.set('view engine','html');  //更改省略的字尾為html,而不是.ejs
app.engine('html',require(ejs').__express); //用ejs模板渲染 
複製程式碼
function createApplication() {
    app.use = function (pathname,handler) {
        if(typeof handler !== 'function'){
        handler = pathname;
        pathname = '/';
    }
    let layer = {
      method:'middle',
      pathname,
      handler
    }
    app.routes.push(layer);
  }
  
  // 配置
  app.settings = {}
  app.set = function (key,value) {
    app.settings[key] = value;
  }
  app.engines = {}
  app.engine = function (ext,renderFn) {
    app.engines[ext] = renderFn
  }

  app.use(function (req,res,next
    let method = req.method.toLowerCase();
    let { pathname, query } = url.parse(req.url, true);
    req.path = pathname;
    req.query = query;
    req.hostname = req.headers.host.split(':')[0];
    res.send = function (params) {
      res.setHeader('Content-Type', 'text/html;charset=utf8');
      if (typeof params === 'object') {
        res.setHeader('Content-Type', 'application/json;charset=utf8');
        res.end(util.inspect(params));
      } else if (typeof (params) === 'number') {
        res.statusCode = params;
        res.end(require('_http_server').STATUS_CODES[params]);
      } else {
        res.end(params);
      }
    }
    res.sendFile = function (pathname) {
      res.setHeader('Content-Type', require('mime').getType(pathname) + ';charset=utf8');
      fs.createReadStream(pathname).pipe(res);
    }
    res.redirect = function (pathname) {
      res.statusCode = 302;
      res.setHeader('Location',pathname);
      res.end();
    }
    
    //通過render對頁面進行渲染
    res.render = function (filename,obj) {
      let dirname = app.settings['views'] ;
      let extname = app.settings['view engine'] ;
      let p = path.resolve(dirname,filename+'.'+extname);
      app.engines[extname](p,obj,function (data) {
        res.end(data);
      });
    }
    next();
  })
}
複製程式碼

內建static邏輯

這裡簡化了邏輯:

createApplication.static = function (dir) {
  return function (req,res,next) {
    let p = req.path === '/' ? '/index.html' : req.path
    let realPath = path.join(dir, p);
    let flag = fs.existsSync(realPath);
    if(flag){
      fs.createReadStream(realPath).pipe(res);
    }else{
      next();
    }
  }
}
複製程式碼

express的bodyparser

express的bodyparser也是通過外掛引入

// body-parser.js
function urlencoded() {
  return (req,res,next)=>{
    if (req.headers['content-type']==='application/x-www-form-urlencoded'){
      let arr = [];
      req.on('data',function (data) {
        arr.push(data);
      })
      req.on('end', function (data) {
        req.body = require('querystring').parse(Buffer.concat(arr).toString());
        next();
      })
    }else{
      next();
    }
  }
}
function json() {
  return (req, res, next) => {
    if (req.headers['content-type'] === 'application/json') {
      let arr = [];
      req.on('data', function (data) {
        arr.push(data);
      })
      req.on('end', function (data) {
        req.body = JSON.parse(Buffer.concat(arr).toString());
        next();
      })
    } else {
      next();
    }
  }
}
module.exports.urlencoded = urlencoded
module.exports.json = json
複製程式碼

結語

前面koa框架會用也會寫—(koa的實現)已經詳細介紹了koa的原理和中介軟體,這裡主要是表示express和koa不同的地方,主要的外掛邏輯可能都簡化了,再次說明express的不同:

  • koa使用了類的概念,express沒有使用類,而是直接使用函式物件,在上面掛載很多方法
  • koa封裝了ctx屬性,並且在上面掛載了path、url等屬性,而express沒有ctx,所以一些屬性和方法直接掛載在res和req上
  • koa將router邏輯從express中抽離出來形成koa-router外掛,所以express中router和中介軟體共用一個佇列,中介軟體預設的路由為'/'
  • koa將static從express中抽離出形成koa-static,express自帶static
  • koa將views從express中抽離出形成koa-views,express自帶res.render
  • koa將bodyparse從express中抽離出形成koa-bodyparse,express自帶bodyparse

相關文章