手把手写一个迷你版的express

本文手把手教你如何实现一个迷你版的Express,详细讲解了从初始化类到实现请求处理的整个过程,包括中间件机制、路由处理和HTTP方法的支持。通过实例代码,帮助理解Express的工作原理。
摘要由CSDN通过智能技术生成

express中间件的简单实现

说明

本次的简单实现中实现了express的usegetpostlistenres.json和中间件的next机制。

功能分析

回顾expess的使用:

  1. 声明一个express实例
   const app = require('express')
  1. 在实例上调用方法useuse方法的参数是一个路由+多个函数,当然也可以不传路由,比如:
    app.use(cookieParser());    // 只传函数

    app.use('/api/user', userRouter());     // 传路由+函数

    app.use('/api/demo', loginCheck(), handleDemo());   // 传路由+多个函数
  1. 在实例上调用方法getget方法参数形式与use相同,所不同的是只有当请求方法为get时,app.get中传入的函数才会被执行。

  2. 在实例上调用方法postpostget形式一样,只是请求方法变成了post

  3. 在实例上调用listen方法,用于启动http服务,通常,我们会给listen方法传入一个端口和一个方法:

app.listen(5000, () => {
    console.log(`http is listening at 5000`);
})

也可以只传入一个端口:

app.listen(5000)
  1. 关于路由的处理:
    传路由时就相当于多了一层限制,只有当你指定的路由与请求的路由相同或者是请求的路由的前缀时,app.use/get/post中的函数才会被执行。

    就以app.use为例说明一下,

app.use('/api', (req, res, next) => {
    console.log('处理 /api 路由')
    next()
});

app.use('/api/user', (req, res, next) => {
    console.log('处理 /api/user 路由')
    next()
})

那么当请求的路由为api/user时,这两个方法都会被执行,最后打印出:

处理 /api 路由
处理 /api/user 路由
  1. 另外还有很重要的一点是中间件的next机制。在上面的例子中,如果第一个use的方法里没写next(),那么路由处理到此为止,就不会再去找下一个函数了。就是说,当上面的例子变成:
app.use('/api', (req, res, next) => {
    console.log('处理 /api 路由')
});

app.use('/api/user', (req, res, next) => {
    console.log('处理 /api/user 路由')
    next()
})

那么,只会打印出:

处理 /api 路由
  1. 最后就是在函数的res参数上,绑定了一个json方法,使用res.json,我们可以很方便的给前端返回一个json数据。

代码实现

下面来简单实现一下:

1. 初始化一个类并导出

首先声明一个类MiniExpress,这里直接用es6。最后导出一个工厂函数,这样我们在外部只需要调用这个函数(而不需要new)就可以得到一个实例。

class MiniExpress {
}

module.exports = () => new MiniExpress();

2. 写构造方法

这里我们需要一个数组按顺序存放要处理的中间件:

constructor() {
    this.routes = [];
}

3. 注册公有函数 usegetpost

在函数中,我们需要把函数中传入的所有中间件都放入到对应的数组中。

由于传入的参数要兼容有路由的模式,因此写一个私有函数来处理参数。这里用Symbol来声明私有函数:

const _register = Symbol();

然后实现usegetpost

use() {
    // 将当前函数中的所有参数展开传入_register函数中,
    // 经过_register函数处理后,函数中的所有参数都会被放进一个对象中,
    // 该对象的结构为:{ path: '', queue = [] } path中放路由,queue中放中间件(函数)
    const info = this[_register].apply(this, arguments);
    // 将处理后的对象放入this.routes中
     this.routes.push(info);
}

get() {
    const info = this[_register].apply(this, arguments);
    // 给info添加属性method,便于以后根据请求方法筛选中间件
    info.method = 'get';
    this.routes.push(info);
}

post() {
    const info = this[_register].apply(this, arguments);
    info.method = 'post';
    this.routes.push(info);
}

_register方法:

 [_register](path) {
    const info = {}; // info有path和queue两个属性,其中path中放路由,queue中放中间件函数
    if (typeof path === 'string') {
        // 第一个参数为路由
        info.path = path;
        // 从第二个参数开始裁取参数数组(如果不是数组就转为数组),将结果存入 queue
        info.queue = Array.prototype.slice.call(arguments, 1);
    } else {
        // 没有传路由,则默认赋值为跟路由
        info.path = '/';
        // 将整个参数(转换为)数组存入 queue
        info.queue = Array.prototype.slice.call(arguments, 0);
    }
    // 返回保存了路由和中间件的对象
    return info;
}

经过上面的处理后,所有中间件就被收集了起来。接下来,开始创建http服务。并将请求与http服务连接起来。

4.写listen方法

在这个方法中,我们需要创建一个http服务,并把处理路由的回调函数传进去。这里创建一个私有函数_callback作为这个回调函数:

const http = require('http');
const _callback = Symbol();

注册listen函数:

listen(...args) {
    // 使用http的createServer方法注册一个http服务
    const server = http.createServer(this[_callback]());
    // 将所有参数展开传入listen函数中,参数中的第一项为端口
    server.listen(...args);
}

这里使用展开运算符的作用:

如果传入的参数是数组,那么直接展开,传入函数中

如果传入的参数不是数组,那么先将所有参数都放入数组中,然后再将这个数组展开,传入函数中

5. 实现_callback方法

我们需要在这个函数中返回一个函数。在返回的函数中处理路由。给函数有reqres参数。

首先在res中注册一个json函数,用于给前端返回一个json字符串:

 [_callback]() {
    return (req, res) => {
        res.json = data => {
            res.setHeader('Content-type', 'application/json');
            res.end(JSON.stringify(data));
        };
    }
}

然后根据请求方法和请求路由,把当前匹配到的所有路由放进一个数组中。这里举个例子进一步解释一下:

比如现在的请求方法是get,请求路由是/api/user/info。假设你的express是这么写的:

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

// 方法1
app.use((req, res, next) => {
    next();
});

// 方法2
app.get((req, res, next) => {
    next();
});

// 方法3
app.get('/api', (req, res, next) => {
    next();
});

// 方法4
app.get('/api/user/info', (req, res, next) => {
    next()
    res.json({
        errNo: 0,
        msg: 'get info success'
    });
});

express中,这四个方法会依次执行,最后给前端返回一个json字符串。由此可以想到,在express的实现中,需要将这四个方法的参数统一收集起来。然后为参数中所有的中间件(函数),都实现next机制。而这些参数之前已经放进了一个对象中,并存入了各自的数组(this.routes.allthis.routes.getthis.routes.post)。

这里将收集中间件的方法命名为__match,将实现next机制的方法命名为_handle。这两个函数都是私有函数,因此首先:

const _match = new Symbol();
const _handle = new Symbol();

然后将它们加入_callback函数中:

 [_callback]() {
    return (req, res) => {
        res.json = data => {
            res.setHeader('Content-type', 'application/json');
            res.end(JSON.stringify(data));
        };

        // 将req中的url和method拿出来
        const url = req.url;
        const method = req.method.toLowerCase();

        const resultList = this[_match](method, url);
        this[_handle](req, res, resultList);
    }
}

6. 实现_match方法

之前分析过了_match方法的作用就是根据请求方法和请求路由收集所有匹配到的中间件。这里路由的匹配是匹配前缀,在最开始分析路由处理的时候也讲到过。直接上代码:

 [_match](method, url) {
    // 声明一个数组收集处理结果
    let queue = [];

    // 忽略获得网站图标的请求
    if (url === '/favicon.ico') {
        return queue;
    }

    // 声明一个中间变量,存储根据请求方法匹配后的结果
    let curRoutes = [];
    // 根据请求方法匹配,匹配app.use中的中间件时忽略请求方法
    this.routes.forEach(route => {
        if (!route.method || (route.method && route.method === method)) {
            curRoutes.push(route);
        }
    })

    // 根据请求的路由匹配,匹配到当前路由的前缀就加入queue
    curRoutes.forEach(routeInfo => {
        if (url.indexOf(routeInfo.path) === 0) {
            queue = queue.concat(routeInfo.queue);
        }
    })

    // 返回处理结果
    return queue;
}

7. 实现_handle方法

将所有匹配到的中间件收集起来之后,我们需要用next机制将他们联系起来。next机制刚开始的时候也有介绍,这里就不再重复说了。上代码:

[_handle](req, res, queue) {
    const next = () => {
        // middleware赋值为队首,并将队首弹出
        // 这里的队列中存储的是所有匹配到的中间件
        const middleware = queue.shift();
        if (middleware) {
            // 进入下一个中间件
            middleware(req, res, next);
        }
    };
    next();
}

至此,所有分析结束,最后附上完整的代码:

完整代码

const http = require('http');
const slice = Array.prototype.slice;

// 定义私有方法
const _register = Symbol();
const _match = Symbol();
const _callback = Symbol();
const _handle = Symbol();

class MiniExpress {
    constructor() {
        // 按顺序存放要处理的中间件
        this.routes = [];
    }

    use() {
        // 将当前函数中的所有参数展开传入_register函数中,
        // 经过_register函数处理后,函数中的所有参数都会被放进一个对象中,
        // 该对象的结构为:{ path: '', queue = [] } path中放路由,queue中放中间件(函数)
        const info = this[_register].apply(this, arguments);
        this.routes.push(info);
    }

    get() {
        const info = this[_register].apply(this, arguments);
        info.method = 'get';
        this.routes.push(info);
    }

    post() {
        const info = this[_register].apply(this, arguments);
        info.method = 'post';
        this.routes.push(info);
    }

    listen(...args) {
        const server = http.createServer(this[_callback]());
        server.listen(...args);
    }

    /**
     * 如果传入了路由则将路由和其他参数放进queue,
     * 否则,将路由赋值为跟路由,然后存入queue
     *
     * @param {*} path
     * @returns {object} path:路由,queue:其他参数(中间件)
     * @memberof MiniExpress
     */
    [_register](path) {
        const info = {}; // info有path和queue两个属性,其中path中放路由,queue中放中间件
        if (typeof path === 'string') {
            // 第一个参数为路由
            info.path = path;
            // 从第二个参数开始裁取参数数组(如果不是数组就转为数组),将结果存入 queue
            info.queue = slice.call(arguments, 1);
        } else {
            info.path = '/';
            // 将整个参数(转换为)数组存入 queue
            info.queue = slice.call(arguments, 0);
        }
        return info;
    }

    /**
    * http.createServer方法中的回调函数
    *
    * @returns {function}
    * @memberof MiniExpress
    */
    [_callback]() {
        return (req, res) => {
            res.json = data => {
                res.setHeader('Content-type', 'application/json');
                res.end(JSON.stringify(data));
            };
            const url = req.url;
            const method = req.method.toLowerCase();

            const resultList = this[_match](method, url);
            this[_handle](req, res, resultList);
        }
    }

    [_match](method, url) {
        // 存放最终匹配成功的中间件
        let queue = [];

        // 忽略网站图标的请求
        if (url === '/favicon.ico') {
            return queue;
        }

        // 定义中间变量,存放方法匹配后的结果
        let curRoutes = [];

        // 匹配方法
        this.routes.forEach(route => {
            if (!route.method || (route.method && route.method === method)) {
                curRoutes.push(route);
            }
        })

        // 匹配路由(前缀)
        curRoutes.forEach(routeInfo => {
            if (url.indexOf(routeInfo.path) === 0) {
                queue = queue.concat(routeInfo.queue);
            }
        })
        return queue;
    }

    // next机制
    [_handle](req, res, queue) {
        const next = () => {
            // 第一个匹配的中间件
            const middleware = queue.shift();
            if (middleware) {
                // 执行中间件函数
                middleware(req, res, next);
            }
        };
        next();
    }
}

module.exports = () => {
    // 工厂函数
    return new MiniExpress()
}

测试

测试代码:

const app = require('./MiniExpress')();

app.use((req, res, next) => {
    console.log('请求开始...', req.method, req.url)
    next();
})

app.use((req, res, next) => {
    // 假设在处理 cookie
    console.log('处理 cookie ...')
    req.cookie = {
        userId: 'abc123'
    }
    next();
})

app.use('/api', (req, res, next) => {
    console.log('处理 /api 路由')
    next();
})

app.get('/api', (req, res, next) => {
    console.log('get /api 路由')
    next();
})

// 模拟登录验证
function loginCheck(req, res, next) {
    setTimeout(() => {
        console.log('模拟登录成功')
        next()
    }, 1000);
}

// 添加中间件
app.get('/api/user/info', loginCheck, (req, res, next) => {
    console.log('get /api/user/info')
    res.json({
        errno: 0,
        data: req.cookie
    })
})

app.use((req, res, next) => {
    res.json({
        errno: 404,
        msg: 'not found'
    })
})

app.listen(5000, () => {
    console.log('server is running on port 5000')
})

测试结果

将这个代码运行起来,在浏览器中输入http://localhost:5000/api/user/info。可以看到控制台中会打印出:

server is running on port 5000
请求开始... GET /api/user/info
处理 cookie ...
处理 /api 路由
get /api 路由
模拟登录成功
get /api/user/info

浏览器中显示出一个json字符串:

{
    "errno": 0,
    "data": {
    "userId": "abc123"
    }
}

测试结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值