一个例子 - 看尽express.js原理

本文只是实现了express的基本用法的原理,具体代码结构和真正的express.js代码结构不一样,可以说是简化版的express,重要的是理解express的一些基本用法的实现原理,比如路由,中间件,路径参数,错误处理等,这样我们在使用的时候就能了然于心!

测试用例

const express = require("./express.js");
// 初始化express
const app = express();
// 中间件匹配所有路径
app.use(function (req,res,next) {
    console.log('全部匹配');
    next();
});
app.use('/world', function (req,res,next) {
    console.log('只匹配/world');
    //next("特色错误");
    next();
});
app.get("/hello",function(req,res){
    res.end("hello");
});
app.post("/world",function(req,res) {
    res.end("world");
});
// 路径参数
app.post("/man/:age/:name",function(req,res) {
    console.log(req.params);
    res.end("man");
});
// 匹配剩余所有其他路径请求
app.all("*",function(req,res) {
    res.end("all")
});
// 错误中间件
app.use(function (err, req, res, next) {
    console.log(err);
    res.end(err);
});
// 监听端口
app.listen(8080);

express.js源码

let http = require("http");
let url = require("url");

// 返回expres的实例方法,需要用户调用
const createApplication = function () {
    // 真正的请求方法
    let app = function (req, res) {
        var urlObj = url.parse(req.url, true);
        const pathname  = urlObj.pathname;
        const method = req.method.toLowerCase();

        // 增加一些通用参数
        req.path = pathname;
        req.query = urlObj.query;
        req.hostname = req.headers['host'].split(':')[0];

        // 当只有简单的路由匹配的时候可以用循环来处理,因为不会被中断
        // for (let index = 0; index < app.routes.length; index++) {
        //     const route = app.routes[index];
        //     if ((method === route.method || route.method === "all" )
        //      && (pathname === route.path || route.path === "*")) {
        //         return route.handler(req, res);
        //     }
        // }
        
        // 但是要实现遍历所以要声明一个索引递增来控制
        let index = 0;
        // 当要加入中间件的时候,需要用next函数来递归调用,把控制权传递给外部来实现中间件原理
        function next(err) {
            // 所有路由不能匹配时报错
            if (index >= app.routes.length) {
                return res.end(`Cannot find ${method} ${pathname}`);
            }
            let route = app.routes[index++];

            // 外部传入参数时,则定义为err, 直接调用拥有四个参数的处理函数
            if (err) {
                // route.handler.length : 表示函数的参数是四个
                if (route.method == "middle" && route.handler.length == 4) {
                    // console.log("错误处理啦", req, res);
                    route.handler(err, req, res, next);
                } else { // 没有找到继续向下找错误处理函数 
                    next(err);
                }
            } else {
                if (route.method == "middle") {  // 处理中间件部分
                    // route.path == "/": 表示全部匹配 
                    // pathname.startsWith(route.path + "/") : 表示路由匹配以这个开头的所有 ,如 /user/to/...
                    // pathname == route.path : 表示相等 如 /user
                    if (route.path == "/" || pathname.startsWith(route.path + "/") || pathname == route.path ) {
                         route.handler(req,res,next); // 注意这里不能return 因为中间件只是处理,不返回
                    } else {
                        next();
                    }
                } else { 
                    // 处理路径参数部分
                    if (route.params) {
                        /**
                         *  "a/27/jason".match(new RegExp("a/([^\/]+)/([^\/]+)"))
                         *  匹配如下
                         *  0: "a/v/d"
                         *  1: "27"
                         *  2: "jason"
                         */
                        let macther = pathname.match(new RegExp(route.path));
                        // 用于存放参数
                        let params = {};
                        for (let j = 0; j < route.params.length; j++) {
                            params[route.params[j]] = macther[j+1];
                        }
                        // 放入req中
                        // req.params = {age:27, name:jason};
                        req.params = params;
                        // 执行回调
                        route.handler(req, res);
                    }
                    // 处理路由部分
                    // 只有当路由路径完全匹配或者为all全部匹配时才执行处理函数
                    if ((method === route.method || route.method === "all" ) && (pathname === route.path || route.path === "*")) {
                        route.handler(req, res); // 这里要return,因为请求处理完了
                    } else {
                        next();
                    }
                }
            }
        }
        // 默认调用
        next();
        //res.end(`cannot find ${req.method} ${pathname}`);
    }

    // 中间件函数,类型和路由一样,只是方法名都叫middle, 这种思想值得学习
    app.use = function(path, handler) {
        // 匹配只传回调函数的情况
        if (typeof path == "function" && typeof handler != 'function') {
            handler = path;
            path = "/";
        }
        app.routes.push({
            method:"middle",
            path,
            handler
        });
    }

    // 路由表
    app.routes = [];

    // 循环遍历请求方法名, 生成对应的请求函数
    http.METHODS.forEach(function (method) {
        method = method.toLowerCase();
        app[method] = function (path, handler) {
            let layer = {
                method,
                path,
                handler
            };
            // 如果路径中有冒号,则是存在参数的路径
            if (path.includes(":")) {
                let paramsArray = [];
                // 替换处理参数,并存入带正则的路径到路由表
                /**
                 * path = "/a/:age/:name"
                 * 匹配后
                 * layer.path = /a/([^\/]+)/([^\/]+);
                 * layer.params = ["age","name"];
                 */
                layer.path = path.replace(/:([^\/]+)/g,function() {
                    // 把匹配的参数名存入数组
                    paramsArray.push(arguments[1]);
                    // 换成一个正则,留作以后路由匹配时用,这里十分巧妙,一举两得
                    return "([^\/]+)";
                })
                // 存入路由中,留做以后路由匹配时用
                //console.log(layer);
                layer.params = paramsArray;

            }
            app.routes.push(layer);
        }
    });

    // 监听所有请求路径
    app.all = function (path, handler) {
        app.routes.push({
            method: "all",
            path,
            handler
        });
    }

    // 监听函数,此时才启动服务,传递参数到真正的函数
    app.listen = function () {
        // 这里把app函数传入才是真正的监听函数
        let server = http.createServer(app);
        server.listen.apply(server,arguments);
        // 下面会把arguments当作一个对象传入, 不符合实际
        //http.createServer(app).listen(arguments);
    }
    return app;
}
module.exports = createApplication;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容