網站首頁 編程語言 正文
文章目錄
前言
我將擬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()方法傳參,routes模塊的app.get()方法接收到參數后將傳入的回調函數作為參數cb注冊到對象G._get中;
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');
});
//use會出現正則匹配錯誤的情況出現, router并不會.
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…
最后, 如果這篇文章幫到了你, 我很高興.
原文鏈接:https://blog.csdn.net/qq_52697994/article/details/121074719
相關推薦
- 2022-05-19 gorm整合進go-zero的實現方法_Golang
- 2024-01-10 Springboot應用中@EntityScan和@EnableJpaRepositories的用法
- 2022-04-08 從頭學習C語言之for語句和循環嵌套_C 語言
- 2022-05-12 在pycharm中設置快速創建
- 2022-08-23 C++?primer超詳細講解順序容器_C 語言
- 2024-03-28 存儲過程整合springboot
- 2022-06-06 Docker鏡像構建之docker?commit的使用_docker
- 2022-04-11 C#基于SerialPort類實現串口通訊詳解_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支