前言
我将拟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) {
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());
return 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]);
来看看完整方法:
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);
}
}
}
5.server()
外壳负责提供G对象内部的数据和方法;
app()负责依据请求方法调用G中的方法(既路由激活后的处理办法);
app.get()负责将各路由受get请求的处理方法注册入G;
app.post()负责将各路由受post请求的处理方法注册入G;
app.static()负责将静态路径存入G供initStatic(ststicPath)读取路由需要的文件;
let server = () => {
let G = {
_get: {},
_post: {},
staticPath: "static",
};
return app;
}
app() - 注册中间件
依据本次请求的方法来决定是调用app.get()注册到G里的回调函数还是调用app.post()注册到G里的回调函数.
这步其实可以看作是express中对路由中间件进行的抽离注册, 然后在合适的时候挂载到Router对象上;
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() - get()
express里可以直接在注册路由的时候规定这条路由只有甚麽方法才能触发:
router.get('/api/list', list);
这里就是为了拟这个功能:
将app.js传入的回调函数注册到G.get, 作为该条路由触发时的处理办法.
GET与POST的提交途径必须分别交由app.get()和app.post(), 防止G中出现覆盖注册;
app.get = function (str, cb) {
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) {
G._post[str] = cb;
}
app.static()
将app中传入的静态路径存放到G中, 供initStatic()使用.
app.static = function (staticPath) {
G.staticPath = 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');
const app = require('./module/route');
const ejs = require('ejs');
2.简化 createServer()
用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");
app.get("/login", function (req, res) {
ejs.renderFile('./views/form.ejs', {}, (err, data) => {
res.send(data);
});
})
app.get("/", function (req, res) {
res.send("首页信息");
})
依据用户跳转到的URL, 调用不同的方法向app.get()或app.post()传回调函数, 并且在G._post 或 G._get 里注册这些回调函数, 它们就相当于是express的路由中间件了.
JS的解析是从上至下, 而一次请求只会触发一次createServer(), 而res.send()或res.join()这种方法如果执行完毕就自动返回, 这一轮响应到这就结束了, 后面的路由根本没有机会接受匹配, so, 不要把"/"的路由写在最上面.
与Vue的路由不太像, Vue-router(我是说路由表那边)不会出现这种结果, Vue路由的结构更加像是标准版的express路由:
用express模块的Router()注册路由对象(router是个函数, 也可以作为中间件), 在Vue中这个"中间件"应该可以看作Vue的路由表routes.
const router = express.Router();
然后在这个中间件上安插各条路由, 也就像在routes数组中配置各条路由.
router.get('/', (req, res, next) => {
res.send('hello');
});
router.get('/index', (req, res, next) => {
res.send('index pages');
});
rouer对象就像是脊椎, 而这些中间件就像是肋骨.
小路由卡上去以后, 将router暴露.
module.exports = router;
就像vue中暴露路由表至app.js以注册到全局:
app.use('/', router);
三、总结
这篇拖了好久, 它几乎占用了这几天拿来写博客的所有时间, 我的最初方案是直接记录下拟express路由的制作方案就结束的, 但因为觉得没有意义马上否定了这个方案, 如果不进行任何对比, 不明白它是如何模拟express路由, 与express有何相似, 又有何区别的话, 这种记录毫无意义, 我想自己以后也不会回来看的.
另外就是因为方法封的比较多, 整篇文章看起来有点乱七八糟的, 那些很长的方法拆开又不行(点名批评app), 但太长大家又不愿意看, 里面再加一堆注释这看起来就更眼花缭乱了, 我在想如何去解决这个问题(虽然最后也没解决好).
然后就打算拖一拖等到express学的差不多再回来补上封装的各个方法与express的对比(事实是每天都要回来改一改这篇文章), 这期间我也完成了几篇小博文, 但是觉得都有点瑕疵就没敢发出去, 我觉得哪怕写一些浅显的知识, 但至少不能有错误.
然后…然后就咕咕咕了…en…
最后, 如果这篇文章帮到了你, 我很高兴.