8. express

109 阅读7分钟

express与koa对比

  • express处理请求的时候全部采用的回调函数的方式, koa采用的是promise + async + await
  • express内部采用的是es5语法, koa采用的是es6来编写
  • express比koa功能多,多了一些内置的中间件(路由,静态服务,模版渲染)代码体积比koa大
  • koa中为了扩展采用的是ctx扩展了request/response对象,express直接在原生的req/res的基础上进行了扩展
  • express中的特点内部采用的是回调 (组合 内部不支持promise串联) koa支持promise串联

express基本实现

  • 使用
const express = require('express');

// express创建应用是通过express()来创建的
const app = express();

app.get("/", function(req,res){
    res.end('home');
})

app.get("/hello", function(req, res) {
    res.end('hello');
})

app.all('*', function(req, res) { // 找不到的情况
    res.end('all');
})

app.listen(3000, function() {
    console.log('server start on 3000')
});

  • 实现
// express.js
const http = require("http");
const url = require('url');
let routers = [
    {
        method: "all",
        path: "*",
        handler(req, res){
            res.end(`Cannot ${req.method}/${req.url}`)
        }
    }
];
function createApplication() {
    return {
        get(path, handler) {
            routers.push({
                method: "get",
                path,
                handler
            })
        },
        listen(...args) {
            const server = http.createServer(function(req, res) {
                let {pathname, query} = url.parse(req.url, true);
                let requestMethod = req.method.toLowerCase();
                for(let i = 1; i < routes.length; i++) {
                    let {method, path, handler} = routes[i];
                    if(pathname == path && method == requestMethod){
                        return handler(req, res);
                    }
                }
                return routers[0].handler(req, res);
            });
            server.listen(...args);
        }
    }
}
  • 代码重构
    • 每次express()都创建一个应用
    • 将应用和路由系统进行拆分
// router.js
function Router() {
    this.stack = []
}
Router.prototype.get = function(path, handler) {
    this.stack.push({
        method: "get",
        path,
        handler
    })
}
Router.prototype.handle = function(req, res, done) {
    let {pathname, query} = url.parse(req.url, true);
    let requestMethod = req.method.toLowerCase();
    for(let i = 0; i < this.stack.length; i++) {
        let {method, path, handler} = this.stack[i];
        if(pathname == path && method == requestMethod){
            return handler(req, res);
        }
    }
    return done();
} 
module.exports = Router;




// application.js
const http = require("http");
const url = require('url');
const Router = require("./router");

function Application() {
    this.router = new Router();
}
Application.prototype.get = function(path, handler) {
    this.router.get(path, handler);
}
Application.prototype.listen = function(...args) {
    const server = http.createServer(function(req, res) {
        function done() {
            res.end(`Connot ${req.url}/${req.method}`)
        }
        this.router.handle(req, res, done);
    });
    server.listen(...args);
}

module.exports = Application;



// express.js
const Application = require("./application");
function createApplication() {
    new Application();
}
module.exports = createApplication;

路由功能

const express = require('express');
const app = express();
// app.get里面可以注册多个回调
// 例如当访问/的时候 需要判断用户权限 如果是某个权限做某件事 最终响应结果
app.get("/", function(req,res,next){
    console.log(1);
    next();
    console.log(5);
}, function(req,res,next){
    console.log(2);
    next();
    console.log(6);
}, function(req,res,next){
    console.log(3);
    next();
    console.log(7)
})

// 还有可能这样写
app.route('/').post(function(req, res){
    res.end('post')
}).get(function(req, res){
    res.end('get')
})
  • 重点:关系理解
/*
1.当每次用户调用get 会走到 Router.prototype.get方法 => 
创建一个route,有一个栈stack, 
里面存放着get后面传入的回调函数包装成的layer(不包含路径path)
并且给每个layer一个method的属性 表示方法类型 (如get) layer的handler属性对应就是一个回调函数

2.Router也有一个栈stack, 每次调用get会往这个stack里放上一层layer,
layer上存放着路径path; layer.route属性和上面的route建立关联
layer存放的handler是route.dispatch

3.当路径匹配上时,会触发route.dispatch

小结
Router是路由系统 每调用一个get 往stack里加入一层Layer
route是每一层都配置的一个用于存放用户回调的地方, 栈中也是一个个layer
二者通过layer.route = route进行关联
当路径匹配上之后 执行route.dispatch

外层layer 匹配路径 路径配成功 执行route.dispatch
内层中的layer匹配方法 匹配方法成功 执行用户回调
执行完成后出来 到达下一个外层的layer
*/

image.png

  • 两个优化
    • 应用的路由懒加载
    • 路由的匹配, 外层要匹配路径和粗略看一下route中是否有此方法,有再调用dispatch,没有就跳过,执行后续逻辑
  • 路由的完整实现
// router/layer.js
function Layer(path, handler) {
    this.path = path;
    thia.handler = handler;
}
Layer.prototype.match = function(pathname) {
    return this.path = pathname;
}
Layer.prototype.handle_request = function(req, res, next) {
    return this.handler(req, res, next);
}

module.exports = Layer;









//  router/Route.js
const Layer = require("./layer");
function Route() {
    this.stack  = [];
    this.methods = {}; // 用来标识route熵包含哪些方法
}
Route.prototype.dispatch = function(req,res,out) {
    let i = 0;
    let next = () => {
        if(i === this.stack.length) {
            return out(); // 从路由栈的layer到下一个layer
        }
        let layer = this.stack[i++];
        if(layer.method === req.method.toLowerCase()) {
            layer.handle_request(req,res,next) // 用户注册的回调
        }else{
            next();
        }
    }
    next();
}
methods.forEach(method => {
    Route.prototype[method] = function(handlers) {
        handlers.forEach(handler => {
            let layer =  new Layer("路径不在意", handler);
            layer.method = method; // 每一层都添加一个方法
            this.methods[method] = true;
            this.stack.push(layer);
        })
    }
})
// Route.prototype.get = function(handlers) {
//     handlers.forEach(handler => {
//         let layer =  new Layer("路径不在意", handler);
//         layer.method = 'get'; // 每一层都添加一个方法
//         this.stack.push(layer);
//     })
// }
module.exports = Route;








// router.js
const Layer = require('./layer')
function Router() {
    this.stack = []
}
Router.prototype.route = function(path) {
    let route = new Route(); // 创建route
    let layer = new Layer(path, route.dispatch.bind(route)); // 创建layer
    layer.route = route; // 建立关联
    this.stack.push(layer);
    return route;
}
methods.forEach(method => {
    Router.prototype[method] = function(path, handlers) {
        // 调用一次get就创建一个layer
        // 要创建一个layer 还要创建一个route 要将layer.route = route创建关联
        let route = this.route(path); // 创建route 并返回route
        route[method](handlers);
    }
})
// Router.prototype.get = function(path, handlers) {
//     // 调用一次get就创建一个layer
//     // 要创建一个layer 还要创建一个route 要将layer.route = route创建关联
//     let route = this.route(path); // 创建route 并返回route
//     route.get(handlers);

// }
Router.prototype.handle = function(req, res, done) {
    // 先匹配路径 在路由的栈中查找 找不到就找下一个 找到了将下一个的执行权限传递进去
    const {pathname} = url.parse(req.url, true);
    let i = 0;
    const next = () => {
        if(i === this.stack.length) {
            return done(); // 整个栈都筛了一遍没有找到 交给应用来处理 调用done
        }
        let layer = this.stack[i++];
        if(layer.match(pathname)) { // 如果路径匹配
            // 外层还是判断一下有没有这个方法
            if(layer.route.methods[req.method.toLowerCase()]) { // 并且方法中能找到这个方法
                layer.handle_request(req, res, next); // route.dispatch
            }else{
                next();
            }
        }else{
            next();
        }
    }
    next();
} 
module.exports = Router;










// application.js
const http = require("http");
const url = require('url');
const Router = require("./router");
const methods = require("methods"); // 第三方模块

function Application() { // 每个应用默认创建一个路由系统 有可能用户只用应用,不用路由系统
    // 所以等调用get等方法时再去创建路由系统 即路由懒加载
    // this.router = new Router();
}
Application.prototype.lazy_route = function() {
    if(!this.router) {
        this.router = new Router();
    }
}

methods.forEach(method => {
    Application.prototype[method] = function(path, ...handlers) {
        this.lazy_route();
        this.router[method](path, handlers);
    }
})
// Application.prototype.get = function(path, ...handlers) {
//     this.router.get(path, handlers);
// }
Application.prototype.listen = function(...args) {
    const server = http.createServer(function(req, res) {
        function done() {
            res.end(`Connot ${req.url}/${req.method}`)
        }
        this.lazy_route();
        this.router.handle(req, res, done); // 请求响应交给路由来处理
    });
    server.listen(...args);
}

module.exports = Application;








// express.js
const Application = require("./application");
function createApplication() {
    new Application();
}
module.exports = createApplication;



中间件的实现

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

// 中间件
// 和koa中间件的区别在于可以传入路径 只对某一些进行处理
// 前缀能匹配上 中间件就会执行, 不写时相当于"/" 
// 可以决定是否向下执行 可以扩展方法和属性 一般还用于做权限处理
app.use("/user", function(req, res, next){ // 拦截器
    console.log("user middleware");
    next();
})
app.get("/user", function(req, res) {
    res.end('user');
})
app.get("/user/admin", function(req, res) {
    res.end('user admin');
})
app.get("/admin", function(req, res) {
    res.end('admin');
})
app.listen(3000, function(req, res) {
    console.log('start listen on 3000')
})
// 中间件的实现原理
// 只有外层layer layer.route = undefined
ApplicationCache.prototype.use = function() {
    this.lazy_route();
    this.router.use(...arguments);
}


Router.prototype.use = function(path) {
    let args =  Array.from(arguments);
    let handlers = [];
    if(typeof path === 'function'){
        path = '/';
        handlers = [...args];
    }else{
        handlers = args.slice(1);
    }
    handlers.forEach(handler => {
        let layer = new Layer(path, handler);
        // 后面判断 如果layer上有route属性 说明是路由 没有说明是中间件
        layer.route = undefined; 
        this.stack.push(layer);
    });
}

Layer.prototype.match = function(pathname) {
    // 无论是中间件layer 还是路由layer 只要一样肯定匹配到
    if(this.path === pathname) {
        return true;
    }
    if(!this.route) { // 是中间件
        if(this.path === '/') { // 中间件路径是/表示可以匹配到
            return true;
        }
        // 匹配到前缀即可
        return pathname.startsWith(this.path);
    }
    return false;
}


Router.prototype.handle = function(req, res, done) {
    // 先匹配路径 在路由的栈中查找 找不到就找下一个 找到了将下一个的执行权限传递进去
    const {pathname} = url.parse(req.url, true);
    let i = 0;
    const next = () => {
        if(i === this.stack.length) {
            return done(); // 整个栈都筛了一遍没有找到 交给应用来处理 调用done
        }
        let layer = this.stack[i++];
        // 无论路由还是中间件 都得匹配路径 但是中间件不匹配方法
        if(layer.match(pathname)) {
            
            if(!layer.route) { // 中间件
                layer.handle_request(req, res, next);
            }else{ // 路由
                if(layer.route.methods[req.method.toLowerCase()]) { // 并且方法中能找到这个方法
                    layer.handle_request(req, res, next); // route.dispatch
                }else{
                    next();
                }
            }
        }else{
            next();
        }
    }
    next();
} 

错误处理中间件

  • next()中传递参数表示是一个错误
  • 错误中间件一般会放到最底部,比普通中间件会多出一个参数 (err, req, res, next)=>{}
  • 当next传递了参数时会跳过其他正常中间件的执行 直接执行错误中间件
  • 当中间件的参数为4个时即为错误中间件
const express = require("express");
const app = express();

app.use("/user", function(req, res, next){
    console.log('user middleware');
    next('error')
})
app.get("/user", function(req,res){
    res.end('user');
})

app.use((error, req, res, next) => { // 错误中间件
    console.log(error);
    res.end(error)
})

app.listen(3000, function() {
    console.log('server start on 3000')
})
Route.prototype.dispatch = function(req,res,out) {
    let i = 0;
    let next = (err) => {
+        if(err) return out(err); // 内部路由抛出错误 就将错误派发到外层处理
        if(i === this.stack.length) {
            return out(); // 从路由栈的layer到下一个layer
        }
        let layer = this.stack[i++];
        if(layer.method === req.method.toLowerCase()) {
            layer.handle_request(req,res,next) // 用户注册的回调
        }else{
            next();
        }
    }
    next();
}


Router.prototype.handle = function(req, res, done) {
    // 先匹配路径 在路由的栈中查找 找不到就找下一个 找到了将下一个的执行权限传递进去
    const {pathname} = url.parse(req.url, true);
    let i = 0;
    const next = (err) => {
        if(i === this.stack.length) {
            return done(); // 整个栈都筛了一遍没有找到 交给应用来处理 调用done
        }
        let layer = this.stack[i++];
+       if(err){
+            // 如果有错误就在栈中查找错误处理中间件 不是错误处理中间件的就跳过 不执行
+            if(!layer.route){ // 中间件
+                if(layer.handler.length === 4){ // 错误中间件
+                   layer.handler(err, req, res, next);
+               }else{
+                   next(err); // 正常中间件
+               }
+            }else{ // 路由
+                next(err); //将错误向下传递
+            }
        }else{ // 走原来的逻辑
            // 无论路由还是中间件 都得匹配路径 但是中间件不匹配方法
            if(layer.match(pathname)) {

                if(!layer.route) { // 中间件
+                    if(layer.handler.length === 4){ // 正常情况下不走错误中间件
+                        next();
                    }else{
                        layer.handle_request(req, res, next);
                    }
                    
                }else{ // 路由
                    if(layer.route.methods[req.method.toLowerCase()]) { // 并且方法中能找到这个方法
                        layer.handle_request(req, res, next); // route.dispatch
                    }else{
                        next();
                    }
                }
            }else{
                next();
            }
        }
        
    }
    next();
} 

正则路由

  • path-to-regexp 把路径转化成正则,和请求来的路径做匹配 获得对象
  • req.params
app.get('/user/:id/:name', function(req,res) {
    res.end(JSON.stringify(req.params));
})
function pathToRegExp(str, keys){
    str = str.replace(/:([^\/]+)/g, function(){
        keys.push(arguments[1]);
        return '([^\/]+)'
    })
    return new RegExp(str);
}
let p = '/user/:id/:name/xxx';
let keys = [];
let reg = pathToRegExp(p, keys);
let url = '/user/111/haha/xxx';
console.log(url.match(reg).slice(1), keys);

二级路由

  • 每次执行 express.Router() 都会单独创建一个路由系统
  • 在进去路由系统匹配时删除前缀 从那个路由系统出来再加上前缀
  • 进去和出来调用的都是next 在next的前后进行截取和拼接
// user.js
const express = require('express');
const router = express.Router();

router.get('/add', function(req, res){
    res.end('user add')
})
router.get('/remove', function(req, res){
    res.end('user remove')
})
module.exports = router;



// article.js
const express = require('express');
const router = express.Router();

router.get('/add', function(req, res){
    res.end('article add')
})
router.get('/remove', function(req, res){
    res.end('article remove')
})
module.exports = router;




// server.js
const express = require('express');
const user = require('./routes/user.js');
const article = require('./routes/article.js');
const app = express();

app.use('/user', user);
app.use('/article', article);

app.listen(3000, function(){
    console.log(`server start on 3000`)
})

路由参数

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

app.param('id', function(req, res, next, value, key){
    console.log(value);
    next()
})
app.param('id', function(req, res, next, value, key){
    console.log(value);
    next()
})
app.param('name', function(req, res, next, value, key){
    console.log(value);
    next()
})
app.param('name', function(req, res, next, value, key){
    console.log(value);
    next()
})

app.get('/user/:id/:name/xxx', function(req, res){
    res.end('user')
})

app.listen(3000, function(){
    console.log('server start on 3000')
})

中间件注册

app.use((req, res, next) => {
    res.send = function(data) {
        if(typeof data === 'object') {
            res.end(JSON.stringify(data));
        }else if(typeof data === 'string' || Buffer.isBuffer(data)){
            res.end(data);
        }
    }
    res.sendFile = function(filePath){
        fs.createReadStream(filePath).pipe(res);
    }
    next();
})