手寫Express核心原理,再也不怕面試官問我Express原理
一、首先安裝express
二、創(chuàng)建example.js文件
創(chuàng)建myExpress.js文件
實現(xiàn)app.get()方法
實現(xiàn)post等其他方法
實現(xiàn)app.all方法
中間件app.use的實現(xiàn)
什么是錯誤中間件?
學(xué)習(xí)總結(jié)
一、首先安裝express
- npm install express
安裝express是為了示范。
已經(jīng)把代碼放到github:https://github.com/Sunny-lucking/HowToBuildMyExpress ??梢皂樖纸o個star嗎?謝謝大佬們。
二、創(chuàng)建example.js文件
- // example.js
- const express = require('express')
- const app = express()
- const port = 3000
- app.get('/', (req, res) => {
- res.send('Hello World!')
- })
- app.listen(port, () => {
- console.log(`Example app listening at http://localhost:${port}`)
- })
如代碼所示,執(zhí)行node example.js就運行起了一個服務(wù)器。
如下圖所示,現(xiàn)在我們決定創(chuàng)建一個屬于我們的express文件,引入的express改成引入我們手寫的express。
好了,現(xiàn)在開始實現(xiàn)我們的express吧!
創(chuàng)建myExpress.js文件
- const express = require('express')
- const app = express()
由 這兩句代碼,我們可以知道,express得到的是一個方法,然后方法執(zhí)行后得到了app。而app實際上也是一個函數(shù),至于為什么會是函數(shù),我們下面會揭秘。
我們可以初步實現(xiàn)express如下:
- // myExpress.js
- function createApplication() {
- let app = function (req,res) {
- }
- return app;
- }
- module.exports = createApplication;
在上面代碼中,發(fā)現(xiàn)app有l(wèi)isten方法。
因此我們可以進(jìn)一步給app添加listen方法:
- // myExpress.js
- function createApplication() {
- let app = function (req,res) {
- }
- app.listen = function () {
- }
- return app;
- }
- module.exports = createApplication;
app.listen實現(xiàn)的是創(chuàng)建一個服務(wù)器,并且將服務(wù)器綁定到某個端口運行起來。
因此可以這樣完善listen方法。
- // myExpress.js
- let http = require('http');
- function createApplication() {
- let app = function (req,res) {
- res.end('hahha');
- }
- app.listen = function () {
- let server = http.createServer(app)
- server.listen(...arguments);
- }
- return app;
- }
- module.exports = createApplication;
這里可能會有同學(xué)有所疑問,為什么 http.createServer(app)這里要傳入app。
其實我們不傳入app,也就是說,讓app不是一個方法,也是可以的。
我們可以改成這樣。
- // myExpress.js
- let http = require('http');
- function createApplication() {
- let app = {};
- app.listen = function () {
- let server = http.createServer(function (req, res) {
- res.end('hahha')
- })
- server.listen(...arguments);
- }
- return app;
- }
- module.exports = createApplication;
如代碼所示,我們將app改成一個對象,也是沒有問題的。
實現(xiàn)app.get()方法
app.get方法接受兩個參數(shù),路徑和回調(diào)函數(shù)。
- // myExpress.js
- let http = require('http');
- function createApplication() {
- let app = {};
- app.routes = []
- app.get = function (path, handler) {
- let layer = {
- method: 'get',
- path,
- handler
- }
- app.routes.push(layer)
- }
- app.listen = function () {
- let server = http.createServer(function (req, res) {
- res.end('hahha')
- })
- server.listen(...arguments);
- }
- return app;
- }
- module.exports = createApplication;
如上面代碼所示,給app添加了route對象,然后get方法執(zhí)行的時候,將接收到的兩個參數(shù):路徑和方法,包裝成一個對象push到routes里了。
可想而知,當(dāng)我們在瀏覽器輸入路徑的時候,肯定會執(zhí)行http.createServer里的回調(diào)函數(shù)。
所以,我們需要在這里 獲得瀏覽器的請求路徑。解析得到路徑。
然后遍歷循環(huán)routes,尋找對應(yīng)的路由,執(zhí)行回調(diào)方法。如下面代碼所示。
- // myExpress.js
- let http = require('http');
- const url = require('url');
- function createApplication() {
- let app = {};
- app.routes = []
- app.get = function (path, handler) {
- let layer = {
- method: 'get',
- path,
- handler
- }
- app.routes.push(layer)
- }
- app.listen = function () {
- let server = http.createServer(function (req, res) {
- // 取出layer
- // 1. 獲取請求的方法
- let m = req.method.toLocaleLowerCase();
- let { pathname } = url.parse(req.url, true);
- // 2.找到對應(yīng)的路由,執(zhí)行回調(diào)方法
- for (let i = 0 ; i< app.routes.length; i++){
- let {method,path,handler} = app.routes[i]
- if (method === m && path === pathname ) {
- handler(req,res);
- }
- }
- res.end('hahha')
- })
- server.listen(...arguments);
- }
- return app;
- }
- module.exports = createApplication;
運行一下代碼。
可見運行成功:
實現(xiàn)post等其他方法。
很簡單,我們可以直接復(fù)制app.get方法,然后將method的值改成post就好了。
- // myExpress.js
- let http = require('http');
- const url = require('url');
- function createApplication() {
- 。。。
- app.get = function (path, handler) {
- let layer = {
- method: 'get',
- path,
- handler
- }
- app.routes.push(layer)
- }
- app.post = function (path, handler) {
- let layer = {
- method: 'post',
- path,
- handler
- }
- app.routes.push(layer)
- }
- 。。。
- return app;
- }
- module.exports = createApplication;
這樣是可以實現(xiàn),但是除了post和get,還有其他方法啊,難道每一個我們都要這樣寫嘛?,當(dāng)然不是,有個很簡單的方法。
// myExpress.js
- function createApplication() {
- ...
- http.METHODS.forEach(method => {
- method = method.toLocaleLowerCase()
- app[method] = function (path, handler) {
- let layer = {
- method,
- path,
- handler
- }
- app.routes.push(layer)
- }
- });
- ...
- }
- module.exports = createApplication;
如代碼所示,http.METHODS是一個方法數(shù)組。如下面所示的數(shù)組:
- ["GET","POST","DELETE","PUT"]。
遍歷方法數(shù)組,就可以實現(xiàn)所有方法了。
測試跑了一下,確實成功。
實現(xiàn)app.all方法
all表示的是匹配所有的方法,
app.all('/user')表示匹配所有路徑是/user的路由
app.all('*')表示匹配任何路徑 任何方法 的 路由
實現(xiàn)all方法也非常簡單,如下代碼所示:
- app.all = function (path, handler){
- let layer = {
- method: "all",
- path,
- handler
- }
- app.routes.push(layer)
- }
然后只需要續(xù)改下路由器匹配的邏輯,如下代碼所示,只需要修改下判斷。
- app.listen = function () {
- let server = http.createServer(function (req, res) {
- // 取出layer
- // 1. 獲取請求的方法
- let m = req.method.toLocaleLowerCase();
- let { pathname } = url.parse(req.url, true);
- // 2.找到對應(yīng)的路由,執(zhí)行回調(diào)方法
- for (let i = 0 ; i< app.routes.length; i++){
- let {method,path,handler} = app.routes[i]
- if ((method === m || method === 'all') && (path === pathname || path === "*")) {
- handler(req,res);
- }
- }
- console.log(app.routes);
- res.end('hahha')
- })
- server.listen(...arguments);
- }
可見成功。
中間件app.use的實現(xiàn)
這個方法的實現(xiàn),跟其他方法差不多,如代碼所示。
- app.use = function (path, handler) {
- let layer = {
- method: "middle",
- path,
- handler
- }
- app.routes.push(layer)
- }
但問題來了,使用中間件的時候,我們會使用next方法,來讓程序繼續(xù)往下執(zhí)行,那它是怎么執(zhí)行的。
- app.use(function (req, res, next) {
- console.log('Time:', Date.now());
- next();
- });
所以我們必須實現(xiàn)next這個方法。
其實可以猜想,next應(yīng)該就是一個瘋狂調(diào)用自己的方法。也就是遞歸。
而且每遞歸一次,就把被push到routes里的handler拿出來執(zhí)行。
實際上,不管是app.use還說app.all還是app.get。其實都是把layer放進(jìn)routes里,然后再統(tǒng)一遍歷routes來判斷該不該執(zhí)行l(wèi)ayer里的handler方法。可以看下next方法的實現(xiàn)。
- function next() {
- // 已經(jīng)迭代完整個數(shù)組,還是沒有找到匹配的路徑
- if (index === app.routes.length) return res.end('Cannot find ')
- let { method, path, handler } = app.routes[index++] // 每次調(diào)用next就去下一個layer
- if (method === 'middle') { // 處理中間件
- if (path === '/' || path === pathname || pathname.starWidth(path + '/')) {
- handler(req, res, next)
- } else { // 繼續(xù)遍歷
- next();
- }
- } else { // 處理路由
- if ((method === m || method === 'all') && (path === pathname || path === "*")) {
- handler(req, res);
- } else {
- next();
- }
- }
- }
可以看到是遞歸方法的遍歷routes數(shù)組。
而且我們可以發(fā)現(xiàn),如果是使用中間件的話,那么只要path是“/”或者前綴匹配,這個中間件就會執(zhí)行。由于handler會用到參數(shù)req和res。所以這個next方法要在 listen里面定義。
如下代碼所示:
- // myExpress.js
- let http = require('http');
- const url = require('url');
- function createApplication() {
- let app = {};
- app.routes = [];
- let index = 0;
- app.use = function (path, handler) {
- let layer = {
- method: "middle",
- path,
- handler
- }
- app.routes.push(layer)
- }
- app.all = function (path, handler) {
- let layer = {
- method: "all",
- path,
- handler
- }
- app.routes.push(layer)
- }
- http.METHODS.forEach(method => {
- method = method.toLocaleLowerCase()
- app[method] = function (path, handler) {
- let layer = {
- method,
- path,
- handler
- }
- app.routes.push(layer)
- }
- });
- app.listen = function () {
- let server = http.createServer(function (req, res) {
- // 取出layer
- // 1. 獲取請求的方法
- let m = req.method.toLocaleLowerCase();
- let { pathname } = url.parse(req.url, true);
- // 2.找到對應(yīng)的路由,執(zhí)行回調(diào)方法
- function next() {
- // 已經(jīng)迭代完整個數(shù)組,還是沒有找到匹配的路徑
- if (index === app.routes.length) return res.end('Cannot find ')
- let { method, path, handler } = app.routes[index++] // 每次調(diào)用next就去下一個layer
- if (method === 'middle') { // 處理中間件
- if (path === '/' || path === pathname || pathname.starWidth(path + '/')) {
- handler(req, res, next)
- } else { // 繼續(xù)遍歷
- next();
- }
- } else { // 處理路由
- if ((method === m || method === 'all') && (path === pathname || path === "*")) {
- handler(req, res);
- } else {
- next();
- }
- }
- }
- next()
- res.end('hahha')
- })
- server.listen(...arguments);
- }
- return app;
- }
- module.exports = createApplication;
當(dāng)我們請求路徑就會發(fā)現(xiàn)中間件確實執(zhí)行成功。
不過,這里的中間價實現(xiàn)還不夠完美。
因為,我們使用中間件的時候,是可以不用傳遞路由的。例如:
- app.use((req,res) => {
- console.log("我是沒有路由的中間價");
- })
這也是可以使用的,那該怎么實現(xiàn)呢,其實非常簡單,判斷一下有沒有傳遞路徑就好了,沒有的話,就給個默認(rèn)路徑“/”,實現(xiàn)代碼如下:
- app.use = function (path, handler) {
- if(typeof path !== "string") { // 第一個參數(shù)不是字符串,說明不是路徑,而是方法
- handler = path;
- path = "/"
- }
- let layer = {
- method: "middle",
- path,
- handler
- }
- app.routes.push(layer)
- }
看,是不是很巧妙,很容易。
我們試著訪問路徑“/middle”
咦?第一個中間件沒有執(zhí)行,為什么呢?
對了,使用中間件的時候,最后要執(zhí)行next(),才能交給下一個中間件或者路由執(zhí)行。
當(dāng)我們請求“/middle”路徑的時候,可以看到確實請求成功,中間件也成功執(zhí)行。說明我們的邏輯沒有問題。
實際上,中間件已經(jīng)完成了,但是別忘了,還有個錯誤中間件?
什么是錯誤中間件?
錯誤處理中間件函數(shù)的定義方式與其他中間件函數(shù)基本相同,差別在于錯誤處理函數(shù)有四個自變量而不是三個,專門具有特征符 (err, req, res, next):
- app.use(function(err, req, res, next) {
- console.error(err.stack);
- res.status(500).send('Something broke!');
- });
當(dāng)我們的在執(zhí)行next()方法的時候,如果拋出了錯誤,是會直接尋找錯誤中間件執(zhí)行的,而不會去執(zhí)行其他的中間件或者路由。
舉個例子:
如圖所示,當(dāng)?shù)谝粋€中間件往next傳遞參數(shù)的時候,表示執(zhí)行出現(xiàn)了錯誤。然后就會跳過其他陸游和中間件和路由,直接執(zhí)行錯誤中間件。當(dāng)然,執(zhí)行完錯誤中間件,就會繼續(xù)執(zhí)行后面的中間件。
例如:
如圖所示,錯誤中間件的后面那個是會執(zhí)行的。
那原理該怎么實現(xiàn)呢?
很簡單,直接看代碼解釋,只需在next里多加一層判斷即可:
- function next(err) {
- // 已經(jīng)迭代完整個數(shù)組,還是沒有找到匹配的路徑
- if (index === app.routes.length) return res.end('Cannot find ')
- let { method, path, handler } = app.routes[index++] // 每次調(diào)用next就去下一個layer
- if( err ){ // 如果有錯誤,應(yīng)該尋找中間件執(zhí)行。
- if(handler.length === 4) { //找到錯誤中間件
- handler(err,req,res,next)
- }else { // 繼續(xù)徐州
- next(err)
- }
- }else {
- if (method === 'middle') { // 處理中間件
- if (path === '/' || path === pathname || pathname.starWidth(path + '/')) {
- handler(req, res, next)
- } else { // 繼續(xù)遍歷
- next();
- }
- } else { // 處理路由
- if ((method === m || method === 'all') && (path === pathname || path === "*")) {
- handler(req, res);
- } else {
- next();
- }
- }
- }
- }
看代碼可見在next里判斷err有沒有值,就可以判斷需不需要查找錯誤中間件來執(zhí)行了。
如圖所示,請求/middle路徑,成功執(zhí)行。
到此,express框架的實現(xiàn)就大功告成了。
學(xué)習(xí)總結(jié)
通過這次express手寫原理的實現(xiàn),更加深入地了解了express的使用,發(fā)現(xiàn):
- 中間件和路由都是push進(jìn)一個routes數(shù)組里的。
- 當(dāng)執(zhí)行中間件的時候,會傳遞next,使得下一個中間件或者路由得以執(zhí)行。
- 當(dāng)執(zhí)行到路由的時候就不會傳遞next,也使得routes的遍歷提前結(jié)束。
- 當(dāng)執(zhí)行完錯誤中間件后,后面的中間件或者路由還是會執(zhí)行的。