使用Node.js實(shí)現(xiàn)一個(gè)express框架
作者:Peter譚金杰
本文介紹了如何使用Node.js來(lái)實(shí)現(xiàn)一個(gè)express框架的,一起來(lái)看看吧。
手寫一個(gè)express系列
express的基本用法
- const express = require("express");
- const app = express();
- app.get("/test", (req, res, next) => {
- console.log("*1");
- // res.end("2");
- next();
- });
- app.get("/test", (req, res, next) => {
- console.log("*2");
- res.end("2");
- });
- app.listen(8888, (err) => {
- !err && console.log("監(jiān)聽(tīng)成功");
- });
- 當(dāng)我訪問(wèn)localhost:8888/test時(shí)候,返回了:2,服務(wù)端打印了
- *1
- *2
- 從上面可以看到什么?
- express默認(rèn)引入調(diào)用后返回一個(gè)app對(duì)象
- app.listen 會(huì)啟動(dòng)進(jìn)程監(jiān)聽(tīng)端口
- 每次收到請(qǐng)求,對(duì)應(yīng)的url和method會(huì)觸發(fā)相應(yīng)掛載在app上對(duì)應(yīng)的回調(diào)函數(shù)
- 調(diào)用 next 方法,會(huì)觸發(fā)下一個(gè)
一起來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的express框架
- 定義屬于我們的express文件入口,這里使用class來(lái)實(shí)現(xiàn)
- class express {
- }
- module.exports = express;
- 需要的原生模塊http,創(chuàng)建進(jìn)程監(jiān)聽(tīng)端口
- const { createServer } = require("http");
- 給 class 定義 listen 方法,監(jiān)聽(tīng)端口
- class express {
- listen(...args) {
- createServer(cb).listen(...args);
- }
- }
- 這樣就可以通過(guò)調(diào)用 class 的 listen 去調(diào)用 http 模塊的 listen 了,這里的cb我們可以先不管,你要知道每次接受到請(qǐng)求,必然會(huì)調(diào)用 cb 函數(shù),這個(gè)是 createServer 原生模塊幫我們封裝好的
實(shí)現(xiàn)接收到請(qǐng)求觸發(fā)
- 實(shí)現(xiàn)app.get app.post等方法
- 目前我們接受到響應(yīng),就會(huì)觸發(fā) cb 這個(gè)回調(diào)函數(shù),那我們打印下,看看是什么參數(shù)?
- class express {
- cb() {
- return (req, res) => {
- console.log(res, res, "開(kāi)始行動(dòng)");
- };
- }
- listen(...args) {
- createServer(this.cb()).listen(...args);
- }
- }
- 發(fā)現(xiàn)此時(shí)的 req 和 res 正是我們想要的可讀流和可寫流.
- 開(kāi)始編寫 get 和 post 方法
- 這里注意,有路由是'/'的,這種是不管任何路由都會(huì)觸發(fā)一次
- constructor() {
- this.routers = {
- get: [],
- post: [],
- };
- }
- get(path, handle) {
- this.routers.get.push({
- path,
- handle,
- });
- }
- post(path, handle) {
- this.routers.post.push({
- path,
- handle,
- });
- }
- 初始化時(shí)候定義 get、post 的數(shù)組儲(chǔ)存對(duì)應(yīng)的 path 和 handle.
- 需要觸發(fā)路由回調(diào)的時(shí)候,首先要找到對(duì)應(yīng)的請(qǐng)求方式下對(duì)應(yīng)的 url 的 handle 方法,然后觸發(fā)回調(diào).
- 如何找到對(duì)應(yīng)請(qǐng)求方式下的 url 對(duì)應(yīng)的 handle 方法? 在接到請(qǐng)求時(shí)候就要遍歷一次
- 這里要考慮匹配多個(gè)路由,意味著,我們可能遇到像最開(kāi)始一樣,有兩個(gè) get 方式的 test 路由
- cb() {
- return (req, res) => {
- const method = req.method.toLowerCase();
- console.log(this.routers[method], ",method");
- const url = req.url;
- this.routers[method].forEach((item) => {
- item.path === url && item.handle(req, res);
- });
- };
- }
- listen(...args) {
- createServer(this.cb()).listen(...args);
- }
- 上面根據(jù) method 找到對(duì)應(yīng)的數(shù)組,遍歷找到請(qǐng)求的路由,觸發(fā)回調(diào),此時(shí)已經(jīng)能正常返回?cái)?shù)據(jù)了
- [ { method: 'get', path: '/test', handle: [Function] } ] ,method
- 此時(shí)最簡(jiǎn)單的express已經(jīng)完成了,但是我們好像忘了最重要的中間件
完成最重要的中間件功能
- 首先要知道,express中間件分兩種,一種帶路由的,那就是根據(jù)路由決定是否觸發(fā)
- 另外一種就是不帶路由的,像靜態(tài)資源這種. 是用戶訪問(wèn)任何路由都要觸發(fā)一次的
- 那我們需要一個(gè) all 數(shù)組儲(chǔ)存這種任意路由都需要匹配觸發(fā)的
- constructor() {
- this.routers = {
- get: [],
- post: [],
- all: [],
- };
- }
- 之前的直接通過(guò) push 方式是太粗暴.如果用戶需要中間件功能,不傳路由,那就要做特殊處理,這里通過(guò)一個(gè)中間函數(shù)處理下
- 改造get、post方法,定義handleAddRouter方法
- handleAddRouter(path, handle) {
- let router = {};
- if (typeof path === "string") {
- router = {
- path,
- handle,
- };
- } else {
- router = {
- path: "/",
- handle: path,
- };
- }
- return router;
- }
- get(path, handle) {
- const router = this.handleAddRouter(path, handle);
- this.routers.get.push(router);
- }
- post(path, handle) {
- const router = this.handleAddRouter(path, handle);
- this.routers.post.push(router);
- }
- use(path, handle) {
- const router = this.handleAddRouter(path, handle);
- this.routers.all.push(router);
- }
- 每次添加之前,先觸發(fā)一次handleAddRouter,如果是 path 為空的中間件,直接傳入函數(shù)的,那么 path 幫它設(shè)置成'/'
- 我們還遺留了一個(gè)點(diǎn),next的實(shí)現(xiàn),因?yàn)槲覀儸F(xiàn)在加了all這個(gè)數(shù)組后,意味著可能有多個(gè)中間件,那么可能一次請(qǐng)求打過(guò)來(lái),就要觸發(fā)多個(gè)路由
這里要注意,promise.then 源碼實(shí)現(xiàn)和 express 的 next、以及 koa 的洋蔥圈、redux 的中間件實(shí)現(xiàn),有著一丁點(diǎn)相似,當(dāng)你能真的領(lǐng)悟前后端框架源碼時(shí)候,發(fā)現(xiàn)大都相似
- 閱讀我的文章,足以擊破所有前后端源碼.而且可以手寫出來(lái), 我們只學(xué)最核心的,抓重點(diǎn)學(xué)習(xí),野蠻生長(zhǎng)!
實(shí)現(xiàn)next
- 思路:
- 首先要找到所有匹配的路由
- 然后逐個(gè)執(zhí)行(看 next 的調(diào)用)
- 定義search方法,找到所有匹配的路由
- search(method, url) {
- const matchedList = [];
- [...this.routers[method], ...this.routers.all].forEach((item) => {
- item.path === url && matchedList.push(item.handle);
- });
- return matchedList;
- }
- cb() {
- return (req, res) => {
- const method = req.method.toLowerCase();
- const url = req.url;
- const matchedList = this.search(method, url);
- };
- }
- matchedList就是我們想要找到的所有路由
- 為了完成next,我們要將req ,res , matchedList存入閉包中,定義handle方法
- handle(req, res, matchedList) {
- const next = () => {
- const midlleware = matchedList.shift();
- if (midlleware) {
- midlleware(req, res, next);
- }
- };
- next();
- }
- cb() {
- return (req, res) => {
- const method = req.method.toLowerCase();
- const url = req.url;
- const matchedList = this.search(method, url);
- this.handle(req, res, matchedList);
- };
- }
- 這樣我們就完成了next方法,只要手動(dòng)調(diào)用 next 就會(huì)調(diào)用下一個(gè)匹配到的路由回調(diào)函數(shù)
- 不到一百行代碼,就完成了這個(gè)簡(jiǎn)單的express框架
寫在最后
- 只要你根據(jù)我這些文章去認(rèn)真自己實(shí)現(xiàn)一次,一年內(nèi)拿個(gè) P6 應(yīng)該沒(méi)什么問(wèn)題
- 大道至簡(jiǎn),希望你能通過(guò)這些文章真的學(xué)到框架的原理,進(jìn)而自己能寫出一些框架,走向更高的層級(jí)
- 我是Peter,曾經(jīng) 20 萬(wàn)人超級(jí)群桌面軟件的架構(gòu)師,現(xiàn)在就職于明源云,擔(dān)任分公司前端負(fù)責(zé)人,目前深圳這邊需要招聘兩位中高級(jí)前端,3D數(shù)據(jù)可視化方向,期待你的到來(lái)
- 如果感覺(jué)本文對(duì)你有幫助,別忘了點(diǎn)個(gè)在看和關(guān)注. 我們的技術(shù)團(tuán)隊(duì)也會(huì)不斷產(chǎn)出原創(chuàng)文章, 一起見(jiàn)證各位的成長(zhǎng)
責(zé)任編輯:龐桂玉
來(lái)源:
segmentfault