五分钟轻仿一套express源码

目录

  • 概述
  • hello-world 实例
  • 运行原理
  • 多路由多回调以及中间件

概述

Express是一个基于 Node.js 平台,快速、开放、极简的 web 开发框架。主要有 <font color=Chocolate>路由</font>、<font color=Chocolate>中间件</font>、<font color=Chocolate>模板引擎</font>、 <font color=Chocolate>错误处理</font>等功能

Hello world 实例

在test文件夹中新加1.helloworld.js

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.end('Hello World!');
});

var server = app.listen(3000, function () {
    console.log('Example app listening at 3000');
});

运行 1.helloworls.js

node 1.helloworls.js

上面代码会在本机的3000端口启动一个网站,网页显示Hello World。

运行原理

现在新建lib文件夹我们手写一个自己的express库 了解其运行原理

YUAN-EXPRESS
|
|
| - lib
|   | - application.js #包裹app层
|   | - express.js  #框架入口
|
| - test
|   | - 1.helloworld.js
|

express.js

const Application = require('./application');
function createApplicaton() {
    return new Application();
}
module.exports = createApplicaton;

目的:在application.js中实现实例中<font color=Chocolate>app.get</font>、<font color=Chocolate>app.listen</font>两个方法

操作:构造Appliaction函数,在原型上添加 <font color=Chocolate>get</font>、<font color=Chocolate>listen</font>方法

application.js

const http = require('http')
const url = require('url')

let router = [{
    path:"*",
    method:"*",
    handler(req,res){
        res.end(`Cannot ${req.method}_${req.url}`)
    }
}]
function Application() {

}

Application.prototype.get = function (path,handler) {//在Application原型上添加get方法
    router.push({
        path,
        method: 'get',
        handler
    })
}

Application.prototype.listen = function () {//在Application原型上添加listen方法匹配路径,执行对应的handler方法
    let self = this
    const server = http.createServer(function (req,res) {
        let { pathname } = url.parse(req.url,true)
        for(var i = 1;i<router.length;i++){
            let { path,method,handler } = router[i]
            if (pathname == path && req.method.toLocaleLowerCase() == method){
                return handler(req,res)
            }
        }
        router[0].handler(req,res)
    })
    server.listen(...arguments)
}


module.exports = Application

Express框架建立在node.js内置的http模块上。

上面代码的关键是http模块的createServer方法,表示生成一个HTTP服务器实例。该方法接受一个回调函数,该回调函数的参数,分别为代表HTTP请求和HTTP回应的request对象和response对象。

循环请求过来时放入router数组的对象,当请求方法和路径与对象中的一致时,执行回调handler方法。

多路由多回调以及中间件

  1. 测试用例
const express = require('../lib/express');
const app = express();
/**
 * 1.get是指定多个处理函数
 * 2.中间件错误处理
 * 3. 子路径系统 单独创建一个子路径系统,并且把它挂载到主路径 系统上
 *
 */
/**
 * app.use
 * express.Router();
 */
app.use(function (req, res, next) {
    console.log('Ware1:', Date.now());
    next();
});
//路由是完整匹配的。/ != /user 所以进不来
app.get('/', function (req, res, next) {
    res.end('1');
});
//创建一个新的路由容器,或者说路由系统
const user = express.Router();// router
user.use(function (req, res, next) {
    console.log('Ware2', Date.now());
    next();
});
//在子路径里的路径是相对于父路径
user.get('/2', function (req, res, next) {
    res.end('2');
});
//use表示使用中间件,只需要匹配前缀就可以了
app.use('/user', user);//user第二个参数是处理函数 (req,res,next)
// req.url = /user/3
//app.use('/user', artcile);
app.use(function (err, req, res, next) {
    res.end('catch ' + err);
});
app.listen(3000, function () {
    console.log('server started at port 3000');
});
  1. 先对项目结构改造
iExpress/
|
|   
| - application.js  #包裹app层
|
| - route/
|   | - index.js    #Router类
|   | - route.js    #Route类
|   | - layer.js    #Layer类
|
| - middle/
|   | - init.js     #内置中间件
|
| - test/
|    | - 测试用例文件1
|    | - ...
|
·- express.js       #框架入口
  • app从字面量变为Application类
  • 丰富HTTP请求方法
  • 封装Router
  • 路径一样的路由整合为一组,引入Layer的概念
  • 增加路由控制,支持next方法,并增加错误捕获功能
  • 执行Router.handle的时候传入out参数
  1. 理清逻辑

测试代码中 注册添加了多个路由且能添加多个回调方法,将逻辑分为三步。

(1)Application容器将请求方法和handler分发给router,在执行listen监听函数时,执行self._router.handle(req, res, done),让塞入Router中的逻辑运行。

image

Application类

const Router = require('./router');

Application.prototype.lazyrouter = function () {
    if (!this._router) {
        this._router = new Router();
    }
}
methods.forEach(function (method) {
    Application.prototype[method] = function () {
        this.lazyrouter();
        //这样写可以支持多个处理函数
        this._router[method].apply(this._router, slice.call(arguments));
        return this;
    }
});
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);
}

(2) 在Router中每一个方法的请求都会往当前的路由系统中添加一个层,在层(layer)中创建一个<font color=Chocolate>route</font>实例


image

Router类

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);//在Router中新增一层layer
    return route;
}

methods.forEach(function (method) {
    proto[method] = function (path) {//请求过来
        let route = this.route(path);//往Router里添一层
        route[method].apply(route, slice.call(arguments, 1));//
        return this;
    }
});

如果是中间件,默认没有path 所以layer的route设为undefined

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);
    return this
}

Application开始监听端口时,执行Router的handle方法。
添加 <font color=Chocolate>next</font>
函数主要负责将控制权交给下一个中间件,如果当前中间件没有终结请求,并且next没有被调用,那么请求将被挂起,后边定义的中间件将得不到被执行的机会。

当Router中的路径和方法匹配时,走到当前layer中,运行layer.handle_request 执行route中添加的方法。

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 (slashAdded) {
            req.url = '';
            slashAdded = false;
        }
        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 {
                    if (req.url == '') {
                        req.url = '/';
                        slashAdded = true;
                    }
                    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();
}
image

(3)进入到当前layer,按照顺序执行添加的每一个route

Layer类

Layer.prototype.handle_request = function (req, res, next) {
    this.handler(req, res, next);
}

注意 这里的this.handler方法,是添加layer时加入的route.dispatch.bind(route),dispatch是在router.route方法中,初始化layer的时候绑定到Layer.handler上的,解析下dispatch代码:

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();
}

文字结构图如下

Application
|
|
Router
|
| - stack
    |
    | - Layer
        |
        | - path  router
                    |
                    | - method  handler
            

Router
Layer

  • Router Layer 路径 处理函数(route.dispatch) 有一个特殊的route属性
  • Route layer 路径 处理函数(真正的业务代码) 有一特殊的属性method

Application只做包装幻术及路由分发,
Router实现
<font color=Chocolate>app.use</font>、
<font color=Chocolate>app.param</font>、
<font color=Chocolate>app.get</font>、
<font color=Chocolate>app.post</font>等路由方法方法的封装

逻辑说明图

image

源码

仓库地址:源码链接点这里~

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

推荐阅读更多精彩内容