仿Express原始碼實現(-)

_shine發表於2018-03-17

Express是什麼

Express 是一個基於 Node.js 封裝的上層服務框架,它提供了更簡潔的 API 更實用的新功能。它通過中介軟體和路由讓程式的組織管理變的更加容易;它提供了豐富的 HTTP 工具;它讓動態檢視的渲染變的更加容易;本文主要根據原始碼,實現核心的中介軟體,路由功能

中介軟體

中介軟體就是處理HTTP請求的函式,用來完成各種特定的任務 比如檢查使用者是否登入、檢測使用者是否有許可權訪問等,它的特點是:

  • 一箇中介軟體處理完請求和響應可以把相應資料再傳遞給下一個中介軟體
  • 回撥函式的next引數,表示接受其他中介軟體的呼叫,函式體中的next(),表示將請求資料傳遞給下一個中介軟體
  • 可以根據路徑來區分進行返回執行不同的中介軟體
    中介軟體

中介軟體用法

const express = require('./express');
const app = express();
//使用use來定義一箇中介軟體 next也是一個函式,呼叫它則意味著當前的中介軟體執行完畢,可以繼續向下執行別的中介軟體了
app.use(function (req, res, next) {
    res.setHeader('Content-Type', 'text/html;charset=utf8');
    console.log('沒有路徑的中介軟體');
    //呼叫next的時候如果傳一個任意引數就表示此函式發生了錯誤,
    //然後express就會跳過後面所有的中介軟體和路由
    //交給錯誤處理中介軟體來處理
    next('我錯了');
});
app.use('/water', function (req, res, next) {
    console.log('過濾雜質');
    next();
});
//錯誤處理中介軟體有四個引數
app.use('/hello', function (err, req, res, next) {
    res.end('hello ' + err);
});
app.use('/water', function (err, req, res, next) {
    //res.end('water1 ' + err);
    next(err);
});
app.use('/water', function (err, req, res, next) {
    res.end('water2 ' + err);
});
app.listen(8080, function () {
    console.log('server started at 8080');
});
複製程式碼

路由

相比中介軟體,Routing 顯然更好。與中間價類似,路由對請求處理函式進行了拆分。不同的是,路由根據請求的 URL 和 HTTP 方法來決定處理方式的。

  • 路由的用法
const express = require('express');
const app = express();
app.route('/user').get(function (req, res) {
    res.end('get');
}).post(function (req, res) {
    res.end('postget');
}).put(function (req, res) {
    res.end('put');
}).delete(function (req, res) {
    res.end('delete');
})
app.listen(3000);
複製程式碼

路由和中介軟體

  • 通過用法可以看出,路由和中介軟體在使用上,都是通過註冊地址,進行相關的操作
  • 原始碼中,路由和中介軟體在具體實現上,都是有相同的操作,都有如下關係
  • express例項 定義express例項,並匯出,在這裡主要是建立了一個application例項
const http = require('http');
const url = require('url');
const Router = require('./router');
const Application = require('./application');
function createApplicaton() {
    return new Application();
}
createApplicaton.Router = Router;
module.exports = createApplicaton;
複製程式碼
  • 路由和中介軟體的方法都定義在~application~方法中
//實現Router 和應用的分離
const Router = require('./router');
const http = require('http');
const methods = require('methods');//['get','post']
const slice = Array.prototype.slice;
// methods = http.METHODS
function Application() {
    this.settings = {};//用來儲存引數
    this.engines = {};//用來儲存副檔名和渲染函式的函式
}
Application.prototype.lazyrouter = function () {
    if (!this._router) {
        this._router = new Router();
    }
}
Application.prototype.param = function (name, handler) {
    this.lazyrouter();
    this._router.param.apply(this._router, arguments);
}
// 傳二個參數列示設定,傳一個參數列示獲取
Application.prototype.set = function (key, val) {
    if (arguments.length == 1) {
        return this.settings[key];
    }
    this.settings[key] = val;
}
//規定何種檔案用什麼方法來渲染
Application.prototype.engine = function (ext, render) {
    let extension = ext[0] == '.' ? ext : '.' + ext;
    this.engines[extension] = render;
}


methods.forEach(function (method) {
    Application.prototype[method] = function () {
        if (method == 'get' && arguments.length == 1) {
            return this.set(arguments[0]);
        }
        this.lazyrouter();
        //這樣寫可以支援多個處理函式
        this._router[method].apply(this._router, slice.call(arguments));
        return this;
    }
});
Application.prototype.route = function (path) {
    this.lazyrouter();
    //建立一個路由,然後建立一個layer ,layer.route = route.this.stack.push(layer)
    this._router.route(path);
}
//新增中介軟體,而中介軟體和普通的路由都是放在一個陣列中的,放在this._router.stack
Application.prototype.use = function () {
    this.lazyrouter();
    this._router.use.apply(this._router, arguments);
}
Application.prototype.listen = function () {
    let self = this;
    let server = http.createServer(function (req, res) {
        function done() {//如果沒有任何路由規則匹配的話會走此函式
            res.end(`Cannot ${req.method} ${req.url}`);
        }
        //如果路由系統無法處理,也就是沒有一條路由規則跟請求匹配,是會把請求交給done
        self._router.handle(req, res, done);
    });
    server.listen(...arguments);
}
module.exports = Application;
複製程式碼
  • 路由和中介軟體的地址每一層通過註冊layer 層,在layer層中放入route物件具體實現如下
  • layer實現
const pathToRegexp = require('path-to-regexp');
function Layer(path, handler) {
    this.path = path;
    this.handler = handler;
    this.keys = [];
    // this.path =/user/:uid   this.keys = [{name:'uid'}];
    this.regexp = pathToRegexp(this.path, this.keys);
}
//判斷這一層和傳入的路徑是否匹配
Layer.prototype.match = function (path) {
    if (this.path == path) {
        return true;
    }
    if (!this.route) {//這一層是一箇中介軟體層  /user/2
        // this.path = /user  
        return path.startsWith(this.path + '/');
    }
    //如果這個Layer是一個路由的 Layer
    if (this.route) {
        let matches = this.regexp.exec(path); //   /user/1
   
        if (matches) {
            this.params = {};
            for (let i = 1; i < matches.length; i++) {
                let name = this.keys[i - 1].name;
                let val = matches[i];
                this.params[name] = val;
            }
            return true;
        }
    }
    return false;
}
Layer.prototype.handle_request = function (req, res, next) {
    this.handler(req, res, next);
}
Layer.prototype.handle_error = function (err, req, res, next) {
    if (this.handler.length != 4) {
        return next(err);
    }
    this.handler(err, req, res, next);
}
module.exports = Layer;
複製程式碼
  • route實現
const Layer = require('./layer');
const methods = require('methods');
const slice = Array.prototype.slice;
function Route(path) {
    this.path = path;
    this.stack = [];
    //表示此路由有有此方法的處理函式
    this.methods = {};
}
Route.prototype.handle_method = function (method) {
    method = method.toLowerCase();
    return this.methods[method];
}
methods.forEach(function (method) {
    Route.prototype[method] = function () {
        let handlers = slice.call(arguments);
        this.methods[method] = true;
        for (let i = 0; i < handlers.length; i++) {
            let layer = new Layer('/', handlers[i]);
            layer.method = method;
            this.stack.push(layer);
        }
        return this;
    }
});

Route.prototype.dispatch = function (req, res, out) {
    let idx = 0, self = this;
    function next(err) {
        if (err) {//如果一旦在路由函式中出錯了,則會跳過當前路由
            return out(err);
        }
        if (idx >= self.stack.length) {
            return out();//route.dispath裡的out剛好是Router的next
        }
        let layer = self.stack[idx++];
        if (layer.method == req.method.toLowerCase()) {
            layer.handle_request(req, res, next);
        } else {
            next();
        }
    }
    next();
}
module.exports = Route;
複製程式碼
  • 通過router 物件導致實現如下
const Route = require('./route');
const Layer = require('./layer');
const url = require('url');
const methods = require('methods');
const init = require('../middle/init');
const slice = Array.prototype.slice;
//let r = Router()
//let r = new Router();
function Router() {
    function router(req, res, next) {
        router.handle(req, res, next);
    }
    Object.setPrototypeOf(router, proto);
    router.stack = [];
    //宣告一個物件,用來快取路徑引數名它對應的回撥函式陣列
    router.paramCallbacks = {};
    //在router一載入就會載入內建 中介軟體
    // query
    router.use(init);
    return router;
}
let proto = Object.create(null);
//建立一個Route例項,向當前路由系統中新增一個層
proto.route = function (path) {
    let route = new Route(path);
    let layer = new Layer(path, route.dispatch.bind(route));
    layer.route = route;
    this.stack.push(layer);

    return route;
}
proto.use = function (path, handler) {
    if (typeof handler != 'function') {
        handler = path;
        path = '/';
    }
    let layer = new Layer(path, handler);
    layer.route = undefined;//我們正是通過layer有沒有route來判斷是一箇中介軟體函式還是一個路由
    this.stack.push(layer);
}
methods.forEach(function (method) {
    proto[method] = function (path) {
        let route = this.route(path);//是在往Router裡添一層
        route[method].apply(route, slice.call(arguments, 1));
        return this;
    }
});
proto.param = function (name, handler) {
    if (!this.paramCallbacks[name]) {
        this.paramCallbacks[name] = [];
    }
    // {uid:[handle1,hander2]}
    this.paramCallbacks[name].push(handler);
}
/**
 * 1.處理中介軟體
 * 2. 處理子路由容器 
 */
proto.handle = function (req, res, out) {
    //slashAdded是否新增過/ removed指的是被移除的字串
    let idx = 0, self = this, slashAdded = false, removed = '';
    // /user/2
    let { pathname } = url.parse(req.url, true);
    function next(err) {
        if (removed.length > 0) {
            req.url = removed + req.url;
            removed = '';
        }
        if (idx >= self.stack.length) {
            return out(err);
        }
        let layer = self.stack[idx++];
        //在此匹配路徑 params   正則+url= req.params
        if (layer.match(pathname)) {// layer.params
            if (!layer.route) { //這一層是中介軟體層//  /user/2
                removed = layer.path;//  /user
                req.url = req.url.slice(removed.length);// /2
                if (err) {
                    layer.handle_error(err, req, res, next);
                } else {
                    layer.handle_request(req, res, next);
                }
            } else {
                if (layer.route && layer.route.handle_method(req.method)) {
                    //把layer的parmas屬性拷貝給req.params
                    req.params = layer.params;
                    self.process_params(layer, req, res, () => {
                        layer.handle_request(req, res, next);
                    });
                } else {
                    next(err);
                }
            }
        } else {
            next(err);
        }
    }
    next();
}
//用來處理param引數,處理完成後會走out函式
proto.process_params = function (layer, req, res, out) {
    let keys = layer.keys;
    let self = this;
    //用來處理路徑引數
    let paramIndex = 0 /**key索引**/, key/**key物件**/, name/**key的值**/, val, callbacks, callback;
    //呼叫一次param意味著處理一個路徑引數
    function param() {
        if (paramIndex >= keys.length) {
            return out();
        }
        key = keys[paramIndex++];//先取出當前的key
        name = key.name;// uid
        val = layer.params[name];
        callbacks = self.paramCallbacks[name];// 取出等待執行的回撥函式陣列
        if (!val || !callbacks) {//如果當前的key沒有值,或者沒有對應的回撥就直接處理下一個key
            return param();
        }
        execCallback();
    }
    let callbackIndex = 0;
    function execCallback() {
        callback = callbacks[callbackIndex++];
        if (!callback) {
            return param();//如果此key已經沒有回撥等待執行,則意味本key處理完畢,該執行一下key
        }
        callback(req, res, execCallback, val, name);
    }
    param();
}
module.exports = Router;
複製程式碼
  • 實現結構如下
/**
 * Router
 *   stack
 *      layer
 *         path route
 *                 method handler
 * Layer
 * Router Layer 路徑 處理函式(route.dispatch) 有一個特殊的route屬性
 * Route  layer  路徑 處理函式(真正的業務程式碼)  有一特殊的屬性method
 */
複製程式碼

由於本人文筆有限,能力有限,對於具體的細節未實現之處,待後續修改補充

相關文章