对比原生Node封装的Express路由 和 express框架路由

Posted 白瑕

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对比原生Node封装的Express路由 和 express框架路由相关的知识,希望对你有一定的参考价值。


前言

我将拟express方法和正规的express方法做了很多对比(类似"这个方法是为了实现express的哪个功能")

全篇主要在两个文件里完成, 一个是"父目录/app.js", 另一个是"父目录/module/route.js";
app.js部分因为有个很长的方法会有些繁琐, 那是为了完成路由中间件的注册和阻复注册, 不过我加了很多注释, 我觉得你应该能看懂…


一、routes.js

以下是源码, 我会拆开说的:

const fs = require('fs');
const path = require('path');

let changesRes = function (res) {
    res.send = (data) => {
        res.writeHead(200, { 'Content-Type': 'text/html;charset="utf-8"' });
        res.end(data);
    }
}

let getFileMime = function (extname) {
    let data = fs.readFileSync('./data/mime.json');
    let mimeObj = JSON.parse(data.toString());
    return mimeObj[extname];
}

let initStatic = function (req, res, staticPath) {
    const { url } = req;
    const { host } = req.headers;
    const myURL = new URL(url, `http://${host}`);
    let pathname = myURL.pathname;
    let extname = path.extname(pathname);

    if (extname) {
        try {
            let data = fs.readFileSync('./' + staticPath + pathname);
            if (data) {
                let mime = getFileMime(extname);
                res.writeHead(200, { 'Content-Type': '' + mime + ';charset="utf-8"' });
                res.end(data);
            }
        } catch (error) {
            console.log(error)
        }
    }

}
let server = () => {
    let G = {
        _get: {},
        _post: {}, 
        staticPath: "static",
    };
    let app = function (req, res) {
        changesRes(res);
        initStatic(req, res, G.staticPath);

        const { url } = req;
        const { host } = req.headers;
        const myURL = new URL(url, `http://${host}`);
        let pathname = myURL.pathname;
        let method = req.method.toLowerCase();
        let extname = path.extname(pathname);
        if (!extname) {
            if (G['_' + method][pathname]) {
                if (method == "get") {
                    G['_' + method][pathname](req, res);

                } else {
                    let postData = '';
                    req.on('data', (chunk) => {
                        postData += chunk;
                    })
                    req.on('end', () => {
                        req.body = postData;
                        G['_' + method][pathname](req, res);
                    })
                }
            } else {
                res.writeHead(404, { 'Content-Type': 'text/html;charset="utf-8"' });
                res.end("页面早已离你而去了...");
            }
        }

    }
    app.get = function (str, cb) {
        G._get[str] = cb;
    }
    app.post = function (str, cb) {
        G._post[str] = cb;
    }

    app.static = function (staticPath) {
        G.staticPath = staticPath;
    }
    return app;
}

module.exports = server();

1.引入模块

因为原本的static()需要fs文件模块来读取文件.

const fs = require('fs');
const path = require('path');

2.changesRes() - send()

为了实现express的res.send()方法.
把send()放到changesRes()里, 通过在app()中调用changesRes()的手段将send()释放进app.js;

这层changesRes()外壳存在的意义是将内部的send()完好的释放到app()中,并从app()中摄取res.

因为app.js直接调用了send(), 所以send()可以接收到app.js传入的data(也就是renderFile()的执行结果, 即渲染后的页面数据), 而app.js也调用了app()和其内部的changesRes()为send()提供了res;
最后由send()中的end()将data放到页面上.

let changesRes = function (res) {
//在app()内调用changesRes()相当于直接将send()写到app()中;
    res.send = (data) => {
        res.writeHead(200, { 'Content-Type': 'text/html;charset="utf-8"' });
        res.end(data);
    }
}

changesRes()方法也简化了app.js中的路由队列, 现在路由表里只要写一句"res.send(data)"就好(然而偷懒直接传了一个字符串);


3.getFileMime() - type()

算是半个res.type()吧, 这个方法调用后返回这个文件对应的文件类型即Content-Type属性到app()中, 由initStatic()方法进行接收并设置到writeHead里.
或许该说是getFileMime()和initStatic()共同组成了拟res.type();

它接收extname作为参数(别担心, extname会由app()调用传入), 这个参数是从URL里拿到的文件路径所指向文件的扩展名, 因为我们在封装静态web, 针对那些带有文件扩展名的URL进行处理, 至于没扩展名的…
扔给路由吧! (啪)

//根据后缀名获取文件类型;
let getFileMime = function (extname) {
    let data = fs.readFileSync('./data/mime.json');
    let mimeObj = JSON.parse(data.toString());  //拿到data转换为字符串再转换为对象;
    return mimeObj[extname];
    //extname是".xxx"的形式, 如果用mimeObj.extname会翻车;
}

拿到后缀名之后readFileSync()利用同步阻塞从外部文件mime.json中读取相应的Content-Type, 读取结果赋值给data.

但是读取到的是16进制的数字码, 先用toString()将其转化为字符串, 再JSON.parse()把它们转换为原本在文件里时候的对象形态, 赋值给mimeObj,这样用起来就舒服多了.

额, 记得return出去…


4.initStatic

一个为了读取文件而设立的方法.
该方法读取URL中的文件路径, 提取文件内容并传给end();
end()内传入执行完毕后要呈现的内容,如果指定了 data 的值,就意味着在执行完 res.end() 之后,会接着执行如下语句:

response.write(data , [对应的字符编码encoding]);

来看看完整方法:

//静态web服务的方法;
let initStatic = function (req, res, staticPath) {

    const { url } = req;
    const { host } = req.headers;
    const myURL = new URL(url, `http://${host}`);//拼凑绝对路径, 并解析;
    let pathname = myURL.pathname;               //拿到解析结果中的pathname属性即文件路径;
    let extname = path.extname(pathname);        //利用path模块提供的方法拿到文件路径指向的文件的后缀名;
    if (extname) {                               //如果文件扩展名存在, 判定交由route.js的静态web服务处理;                        //否则交付至app.js中的路由进行处理;
        try {
            let data = fs.readFileSync('./' + staticPath + pathname);  //将同步读取的文件内容存入变量data;
            if (data) {
                let mime = getFileMime(extname); //传入文件扩展名,mime被赋值文件类型;
                res.writeHead(200, { 'Content-Type': '' + mime + ';charset="utf-8"' });
                res.end(data);
            }
        } catch (error) {
            console.log(error);                  //尝试执行出错, 抓取错误信息输出;
        }
    }

}

5.server()

外壳负责提供G对象内部的数据和方法;
app()负责依据请求方法调用G中的方法(既路由激活后的处理办法);
app.get()负责将各路由受get请求的处理方法注册入G;
app.post()负责将各路由受post请求的处理方法注册入G;
app.static()负责将静态路径存入G供initStatic(ststicPath)读取路由需要的文件;

let server = () => {
    let G = {
        _get: {},  //在G里声明空对象_get;
        _post: {},  //在G里声明空对象_post;\\
        staticPath: "static",
    };
    //app();
    //app.get();
    //app.post();
    //app.stsatic();
    return app;
}

app() - 注册中间件

依据本次请求的方法来决定是调用app.get()注册到G里的回调函数还是调用app.post()注册到G里的回调函数.

这步其实可以看作是express中对路由中间件进行的抽离注册, 然后在合适的时候挂载到Router对象上;

    //把app放在server里防止全局污染;
let app = function (req, res) {
    changesRes(res);
    //相当于书写了如下:
    /* res.send = (data) => {
    res.writeHead(200, {'Content-Type':'text/html;charset="utf-8"'});
    res.end(data);
    app.js中可以直接调用send()方法了;
    */
} 

    initStatic(req, res, G.staticPath);    //根据请求的URL读取响应文件.
    const { url } = req;
    const { host } = req.headers;
    const myURL = new URL(url, `http://${host}`);
    let pathname = myURL.pathname;         //拿取pathname文件路径(相对路径);
    let method = req.method.toLowerCase(); //获取本次请求的方法(GET/POST),并转换为小写(get/post)赋值给method;
    let extname = path.extname(pathname);  //用path模块的自带方法extname()来获取到相对路径所指向文件的扩展名;
    if (!extname) {                        //判定扩展名是否不存在, 存在就留给路由执行;
      if (G['_' + method][pathname]) {     //G中有无归属于"_post"或"_get"对象还[符合路径]的方法;
        if (method == "get") {             //判定本次请求方式是否为get(req.method获取请求方式)
          G['_' + method][pathname](req, res); //如果是,调用G._get里与路径(比如"/login")同名的方法(路径是从app传入的参数)
            } else {
              //因为post的请求以流的形式进行, 服务器也是分段接收;
              //所以要监听两个阶段点判断是不是传输完成;
              let postData = '';         
              //声明并赋值postData为空字符串预备存储data;
              //POST提交数据以流的形式提交,服务器在接收POST数据的时候是分块接收的
              //所以用监听阶段点的方式判断是否传输完成;
              req.on('data', (chunk) => {
                    postData += chunk;     //用变量chunk接收拿到的数据, 然后填入postData中;
              })
              req.on('end', () => {       //监听end事件, 将postData赋值给res.body中
                req.body = postData;
                  G['_' + method][pathname](req, res);//调用app传入并注册的该页的post型回调函数;
              })
            }
        } else {
          //如果没有找到为这个页面的路由注册的任何回调函数, 直接向客户端返回404;
          res.writeHead(404, { 'Content-Type': 'text/html;charset="utf-8"' });
          res.end("页面早已离你而去了..."); //或者直接G['/404'](req, res);
        }
  }
}

app.get() - get()

express里可以直接在注册路由的时候规定这条路由只有甚麽方法才能触发:

router.get('/api/list', list);

这里就是为了拟这个功能:

将app.js传入的回调函数注册到G.get, 作为该条路由触发时的处理办法.
GET与POST的提交途径必须分别交由app.get()和app.post(), 防止G中出现覆盖注册;

app.get = function (str, cb) {    //接收app传来的参数, 在G._get中为传来的回调函数cb(既"callback的简写")进行注册;   
    G._get[str] = cb;
}

app.post() - post()

express里可以直接在注册路由的时候规定这条路由只有甚麽方法才能触发:

router.post('/api/list', list);

这里就是为了拟这个功能:

将app.js传入的回调函数注册到G.post, 作为该条路由触发时的处理办法.
GET与POST的提交途径必须分别交由app.get()和app.post(), 防止G中出现覆盖注册;

app.post = function (str, cb) {
  //接收app传来的参数, 在G._post中为传来的回调函数cb(既"callback的简写")进行注册;
    G._post[str] = cb;
}

app.static()

将app中传入的静态路径存放到G中, 供initStatic()使用.

app.static = function (staticPath) {
//app.static()会在app.js中首先调用, 获取staticPath存入G中,生成G.staticPath, 供initStatic()使用;
    G.staticPath = staticPath;//将参数staticPath赋值到G.staticPath中.
}

二、app.js

利用route.js中封装好的拟express方法完成拟express路由的主体结构.
以下是源码, 我会拆开说的:

const http = require('http');
const app = require('./module/route');
const ejs = require('ejs');

let app = function(req,res){

}

http.createServer(app).listen(3000);

app.static("static"); 
app.get("/login", function (req, res) {
    ejs.renderFile('./views/form.ejs', {}, (err, data) => {
        res.send(data);
    });
})

app.get("/news", function (req, res) {
    res.send("展示");
})

app.get("/register", function (req, res) {
    res.send("注册页");
})

app.get("/", function (req, res) {
    res.send("首页");
})

app.post("/doLogin", function (req, res) {
    res.send(req.body);
})

1.引入模块

就是引入依赖, 没什么好说的…

const http = require('http');
//引入route模块,里面有个server方法要用到;
const app = require('./module/route');
//引入ejs模块, 待会要使用renderFile();
const ejs = require('ejs');

2.简化 createServer()

用app()作为createServer的回调函数, 触发请求直接执行app().

//let app = (req,res) => {}
//http.createServer((req, res) => {});
/* 观察可知createServer方法中的function结构与app方法的外框完全相同;
因此app()的回调函数可以放入createServer()内,这样用户一旦在客户端发动请求, 马上就会触发app(); */

http.createServer(app).listen(3000);

3.拟express路由的挂载

在express框架中, 当有多个子路由需要操作的时候, 将子路由挂载到父级路由router然后直接挂载router会是一个更好的选择, 只要几句就好了:

const router = express.Router();
router.get('路径', 路由中间件);
app.use('/', router);

但如果用app.get()一个个来, 在暴露和引入使用时都不会太方便.很遗憾我只能使用这种"一个个来"的方式在app.js里做路由, 因为router对象是express提供的, 而这是拟express:

app.static("static");    
//向route模块末的static方法传参静态目录以修改G中的默认静态web目录;
//从而完成在静态web模式下对网页文件的读取;

app.get("/login", function (req, res) {
    //app.get()接收到参数后将传入的回调函数cb注册到route.js对象G._get中;
    ejs.renderFile('./views/form.ejs', {}, (err, data) => {
        //login页面的路由被触发, 渲染路径form.ejs;
        res.send(data);
        //res.send()将从目标文件读取到的内容data传到客户端;
    });
})

app.get("/", function (req, res) {
    //在此处向引入的route模块中的app.get()方法传

以上是关于对比原生Node封装的Express路由 和 express框架路由的主要内容,如果未能解决你的问题,请参考以下文章

6.23日学习总结url地址,express框架搭建服务器,路由,原生Node创建服务器

原生http模块与使用express框架对比

node路由封装,减轻后端的主路由的入口app.js的负重

node路由封装,减轻后端的主路由的入口app.js的负重

Node.js教程

Node.js Express框架 详细总结