<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          動手實現(xiàn)一個 Express 框架(萬字實戰(zhàn)好文)

          共 15903字,需瀏覽 32分鐘

           ·

          2020-12-13 10:30

          上一篇文章我們講了怎么用Node.js原生API來寫一個web服務(wù)器[1],雖然代碼比較丑,但是基本功能還是有的。但是一般我們不會直接用原生API來寫,而是借助框架來做,比如本文要講的Express。通過上一篇文章的鋪墊,我們可以猜測,Express其實也沒有什么黑魔法,也僅僅是原生API的封裝,主要是用來提供更好的擴展性,使用起來更方便,代碼更優(yōu)雅。本文照例會從Express的基本使用入手,然后自己手寫一個Express來替代他,也就是源碼解析。

          本文可運行代碼已經(jīng)上傳GitHub,拿下來一邊玩代碼,一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

          簡單示例

          使用Express搭建一個最簡單的Hello World也是幾行代碼就可以搞定,下面這個例子來源官方文檔:

          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}`);});

          可以看到Express的路由可以直接用app.get這種方法來處理,比我們之前在http.createServer里面寫一堆if優(yōu)雅多了。我們用這種方式來改寫下上一篇文章的代碼:

          const path = require("path");const express = require("express");const fs = require("fs");const url = require("url");
          const app = express();const port = 3000;
          app.get("/", (req, res) => { res.end("Hello World");});
          app.get("/api/users", (req, res) => { const resData = [ { id: 1, name: "小明", age: 18, }, { id: 2, name: "小紅", age: 19, }, ]; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(resData));});
          app.post("/api/users", (req, res) => { let postData = ""; req.on("data", (chunk) => { postData = postData + chunk; });
          req.on("end", () => { // 數(shù)據(jù)傳完后往db.txt插入內(nèi)容 fs.appendFile(path.join(__dirname, "db.txt"), postData, () => { res.end(postData); // 數(shù)據(jù)寫完后將數(shù)據(jù)再次返回 }); });});
          app.listen(port, () => { console.log(`Server is running on http://localhost:${port}/`);});

          Express還支持中間件,我們寫個中間件來打印出每次請求的路徑:

          app.use((req, res, next) => {  const urlObject = url.parse(req.url);  const { pathname } = urlObject;
          console.log(`request path: ${pathname}`);
          next();});

          Express也支持靜態(tài)資源托管,不過他的API是需要指定一個文件夾來單獨存放靜態(tài)資源的,比如我們新建一個public文件夾來存放靜態(tài)資源,使用express.static中間件配置一下就行:

          app.use(express.static(path.join(__dirname, 'public')));

          然后就可以拿到靜態(tài)資源了:

          手寫源碼

          手寫源碼才是本文的重點,前面的不過是鋪墊,本文手寫的目標(biāo)就是自己寫一個express來替換前面用到的express api,其實就是源碼解析。在開始之前,我們先來看看用到了哪些API

          1.express(),第一個肯定是express函數(shù),這個運行后會返回一個app的實例,后面用的很多方法都是這個app上的。2.app.listen,這個方法類似于原生的server.listen,用來啟動服務(wù)器。3.app.get,這是處理路由的API,類似的還有app.post等。4.app.use,這是中間件的調(diào)用入口,所有中間件都要通過這個方法來調(diào)用。5.express.static,這個中間件幫助我們做靜態(tài)資源托管,其實是另外一個庫了,叫serve-static[2],因為跟Express架構(gòu)關(guān)系不大,本文就先不講他的源碼了。

          本文所有手寫代碼全部參照官方源碼寫成[3],方法名和變量名盡量與官方保持一致,大家可以對照著看,寫到具體的方法時我也會貼出官方源碼的地址。

          express()

          首先需要寫的肯定是express(),這個方法是一切的開始,他會創(chuàng)建并返回一個app,這個app就是我們的web服務(wù)器。

          // express.jsvar mixin = require('merge-descriptors');var proto = require('./application');
          // 創(chuàng)建web服務(wù)器的方法function createApplication() { // 這個app方法其實就是傳給http.createServer的回調(diào)函數(shù) var app = function (req, res) {
          };
          mixin(app, proto, false);
          return app;}
          exports = module.exports = createApplication;

          上述代碼就是我們在運行express()的時候執(zhí)行的代碼,其實就是個空殼,返回的app暫時是個空函數(shù),真正的app并沒在這里,而是在proto上,從上述代碼可以看出proto其實就是application.js,然后通過下面這行代碼將proto上的東西都賦值給了app

          mixin(app, proto, false);

          這行代碼用到了一個第三方庫merge-descriptors,這個庫總共沒有幾行代碼,做的事情也很簡單,就是將proto上面的屬性挨個賦值給app,對merge-descriptors源碼感興趣的可以看這里:https://github.com/component/merge-descriptors/blob/master/index.js。

          Express這里之所以使用mixin,而不是普通的面向?qū)ο髞砝^承,是因為它除了要mixin proto外,還需要mixin其他庫,也就是需要多繼承,我這里省略了,但是官方源碼是有的。

          express.js對應(yīng)的源碼看這里:https://github.com/expressjs/express/blob/master/lib/express.js

          app.listen

          上面說了,express.js只是一個空殼,真正的appapplication.js里面,所以app.listen也是在這里。

          // application.js
          var app = exports = module.exports = {};
          app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments);};

          上面代碼就是調(diào)用原生http模塊創(chuàng)建了一個服務(wù)器,但是傳的參數(shù)是this,這里的this是什么呢?回想一下我們使用express的時候是這樣用的:

          const app = express();
          app.listen(3000);

          所以listen方法的實際調(diào)用者是express()的返回值,也就是上面express.js里面createApplication的返回值,也就是這個函數(shù):

          var app = function (req, res) {};

          所以這里的this也是這個函數(shù),所以我在express.js里面就加了注釋,這個函數(shù)是http.createServer的回調(diào)函數(shù)?,F(xiàn)在這個函數(shù)是空的,實際上他應(yīng)該是整個web服務(wù)器的處理入口,所以我們給他加上處理的邏輯,在里面再加一行代碼:

          var app = function(req, res) {  app.handle(req, res);    // 這是真正的服務(wù)器處理入口};

          app.handle

          app.handle也是掛載在app下面的,所以他實際也在application.js這個文件里面,下面我們來看看他干了什么:

          app.handle = function handle(req, res) {  var router = this._router;
          // 最終的處理方法 var done = finalhandler(req, res);
          // 如果沒有定義router // 直接結(jié)束返回 if (!router) { done(); return; }
          // 有router,就用router來處理 router.handle(req, res, done);}

          上面代碼可以看出,實際處理路由的是router,這是Router的一個實例,并且掛載在this上的,我們這里還沒有給他賦值,如果沒有賦值的話,會直接運行finalhandler并且結(jié)束處理。finalhandler也是一個第三方庫,GitHub鏈接在這里:https://github.com/pillarjs/finalhandler。這個庫的功能也不復(fù)雜,就是幫你處理一些收尾的工作,比如所有路由都沒匹配上,你可能需要返回404并記錄下error log,這個庫就可以幫你做。

          app.get

          上面說了,在具體處理網(wǎng)絡(luò)請求時,實際上是用app._router來處理的,那么app._router是在哪里賦值的呢?事實上app._router的賦值有多個地方,一個地方就是HTTP動詞處理方法上,比如我們用到的app.get或者app.post。無論是app.get還是app.post都是調(diào)用的router方法來處理,所以可以統(tǒng)一用一個循環(huán)來寫這一類的方法。

          // HTTP動詞的方法var methods = ['get', 'post'];methods.forEach(function (method) {  app[method] = function (path) {    this.lazyrouter();
          var route = this._router.route(path); route[method].apply(route, Array.prototype.slice.call(arguments, 1)); return this; }});

          上面代碼HTTP動詞都放到了一個數(shù)組里面,官方源碼中這個數(shù)組也是一個第三方庫維護的,名字就叫methods,GitHub地址在這里:https://github.com/jshttp/methods。我這個例子因為只需要兩個動詞,就簡化了,直接用數(shù)組了。這段代碼其實給app創(chuàng)建了跟每個動詞同名的函數(shù),所有動詞的處理函數(shù)都是一樣的,都是去調(diào)router里面的對應(yīng)方法來處理。這種將不同部分抽取出來,從而復(fù)用共同部分的代碼,有點像我之前另一篇文章寫過的設(shè)計模式----享元模式[4]

          我們注意到上面代碼除了調(diào)用router來處理路由外,還有一行代碼:

          this.lazyrouter();

          lazyrouter方法其實就是我們給this._router賦值的地方,代碼也比較簡單,就是檢測下有沒有_router,如果沒有就給他賦個值,賦的值就是Router的一個實例:

          app.lazyrouter = function lazyrouter() {  if (!this._router) {    this._router = new Router();  }}

          app.listen,app.handlemethods處理方法都在application.js里面,application.js源碼在這里:https://github.com/expressjs/express/blob/master/lib/application.js

          Router

          寫到這里我們發(fā)現(xiàn)我們已經(jīng)使用了Router的多個API,比如:

          1.router.handle2.router.route3.route[method]

          所以我們來看下Router這個類,下面的代碼是從源碼中簡化出來的:

          // router/index.jsvar setPrototypeOf = require('setprototypeof');
          var proto = module.exports = function () { function router(req, res, next) { router.handle(req, res, next); }
          setPrototypeOf(router, proto);
          return router;}

          這段代碼對我來說是比較奇怪的,我們在執(zhí)行new Router()的時候其實執(zhí)行的是new proto(),new proto()并不是我奇怪的地方,奇怪的是他設(shè)置原型的方式。我之前在講JS的面向?qū)ο蟮奈恼?sup style="box-sizing: border-box;">[5]提到過如果你要給一個類加上類方法可以這樣寫:

          function Class() {}
          Class.prototype.method1 = function() {}
          var instance = new Class();

          這樣instance.__proto__就會指向Class.prototype,你就可使用instance.method1了。

          Express.js的上述代碼其實也是實現(xiàn)了類似的效果,setprototypeof又是一個第三方庫,作用類似Object.setPrototypeOf(obj, prototype),就是給一個對象設(shè)置原型,setprototypeof存在的意義就是兼容老標(biāo)準(zhǔn)的JS,也就是加了一些polyfill,他的代碼在這里[6]。所以:

          setPrototypeOf(router, proto);

          這行代碼的意思就是讓router.__proto__指向proto,router是你在new proto()時的返回對象,執(zhí)行了上面這行代碼,這個router就可以拿到proto上的全部方法了。像router.handle這種方法就可以掛載到proto上了,成為proto.handle。

          繞了一大圈,其實就是JS面向?qū)ο蟮氖褂?,給router添加類方法,但是為什么使用這么繞的方式,而不是像我上面那個Class那樣用呢?這我就不是很清楚了,可能有什么歷史原因吧。

          路由架構(gòu)

          Router的基本結(jié)構(gòu)知道了,要理解Router的具體代碼,我們還需要對Express的路由架構(gòu)有一個整體的認(rèn)識。就以我們這兩個示例API來說:

          get /api/users

          post /api/users

          我們發(fā)現(xiàn)他們的path是一樣的,都是/api/users,但是他們的請求方法,也就是method不一樣。Express里面將path這一層提取出來作為了一個類,叫做Layer。但是對于一個Layer,我們只知道他的path,不知道method的話,是不能確定一個路由的,所以Layer上還添加了一個屬性route,這個route上也存了一個數(shù)組,數(shù)組的每個項存了對應(yīng)的method和回調(diào)函數(shù)handle。整個結(jié)構(gòu)你可以理解成這個樣子:

          const router = {  stack: [    // 里面很多l(xiāng)ayer    {      path: '/api/users'      route: {          stack: [          // 里面存了多個method和回調(diào)函數(shù)          {            method: 'get',            handle: function1          },          {            method: 'post',            handle: function2          }        ]        }    }  ]}

          知道了這個結(jié)構(gòu)我們可以猜到,整個流程可以分成兩部分:注冊路由匹配路由。當(dāng)我們寫app.getapp.post這些方法時,其實就是在router上添加layerroute。當(dāng)一個網(wǎng)絡(luò)請求過來時,其實就是遍歷layerroute,找到對應(yīng)的handle拿出來執(zhí)行。

          注意route數(shù)組里面的結(jié)構(gòu),每個項按理來說應(yīng)該使用一種新的數(shù)據(jù)結(jié)構(gòu)來存儲,比如routeItem之類的。但是Express并沒有這樣做,而是將它和layer合在一起了,給layer添加了methodhandle屬性。這在初次看源碼的時候可能造成困惑,因為layer同時存在于routerstack上和routestack上,肩負(fù)了兩種職責(zé)。

          router.route

          這個方法是我們前面注冊路由的時候調(diào)用的一個方法,回顧下前面的注冊路由的方法,比如app.get

          app.get = function (path) {  this.lazyrouter();
          var route = this._router.route(path); route.get.apply(route, Array.prototype.slice.call(arguments, 1)); return this;}

          結(jié)合上面講的路由架構(gòu),我們在注冊路由的時候,應(yīng)該給router添加對應(yīng)的layerrouterouter.route的代碼就不難寫出了:

          proto.route = function route(path) {  var route = new Route();  var layer = new Layer(path, route.dispatch.bind(route));     // 參數(shù)是path和回調(diào)函數(shù)
          layer.route = route;
          this.stack.push(layer);
          return route;}

          Layer和Route構(gòu)造函數(shù)

          上面代碼新建了RouteLayer實例,這兩個類的構(gòu)造函數(shù)其實也挺簡單的。只是參數(shù)的申明和初始化:

          // layer.jsmodule.exports = Layer;
          function Layer(path, fn) { this.path = path;
          this.handle = fn; this.method = '';}
          // route.jsmodule.exports = Route;
          function Route() { this.stack = []; this.methods = {}; // 一個加快查找的hash表}

          route.get

          前面我們看到了app.get其實通過下面這行代碼,最終調(diào)用的是route.get

          route.get.apply(route, Array.prototype.slice.call(arguments, 1));

          也知道了route.get這種動詞處理函數(shù),其實就是往route.stack上添加layer,那我們的route.get也可以寫出來了:

          var methods = ["get", "post"];methods.forEach(function (method) {  Route.prototype[method] = function () {    // 支持傳入多個回調(diào)函數(shù)    var handles = flatten(slice.call(arguments));
          // 為每個回調(diào)新建一個layer,并加到stack上 for (var i = 0; i < handles.length; i++) { var handle = handles[i];
          // 每個handle都應(yīng)該是個函數(shù) if (typeof handle !== "function") { var type = toString.call(handle); var msg = "Route." + method + "() requires a callback function but got a " + type; throw new Error(msg); }
          // 注意這里的層級是layer.route.layer // 前面第一個layer已經(jīng)做個path的比較了,所以這里是第二個layer,path可以直接設(shè)置為/ var layer = new Layer("/", handle); layer.method = method; this.methods[method] = true; // 將methods對應(yīng)的method設(shè)置為true,用于后面的快速查找 this.stack.push(layer); } };});

          這樣,其實整個router的結(jié)構(gòu)就構(gòu)建出來了,后面就看看怎么用這個結(jié)構(gòu)來處理請求了,也就是router.handle方法。

          router.handle

          前面說了app.handle實際上是調(diào)用的router.handle,也知道了router的結(jié)構(gòu)是在stack上添加了layerrouter,所以router.handle需要做的就是從router.stack上找出對應(yīng)的layerrouter并執(zhí)行回調(diào)函數(shù):

          // 真正處理路由的函數(shù)proto.handle = function handle(req, res, done) {  var self = this;  var idx = 0;  var stack = self.stack;
          // next方法來查找對應(yīng)的layer和回調(diào)函數(shù) next(); function next() { // 使用第三方庫parseUrl獲取path,如果沒有path,直接返回 var path = parseUrl(req).pathname; if (path == null) { return done(); }
          var layer; var match; var route;
          while (match !== true && idx < stack.length) { layer = stack[idx++]; // 注意這里先執(zhí)行 layer = stack[idx]; 再執(zhí)行idx++; match = layer.match(path); // 調(diào)用layer.match來檢測當(dāng)前路徑是否匹配 route = layer.route;
          // 沒匹配上,跳出當(dāng)次循環(huán) if (match !== true) { continue; }
          // layer匹配上了,但是沒有route,也跳出當(dāng)次循環(huán) if (!route) { continue; }
          // 匹配上了,看看route上有沒有對應(yīng)的method var method = req.method; var has_method = route._handles_method(method); // 如果沒有對應(yīng)的method,其實也是沒匹配上,跳出當(dāng)次循環(huán) if (!has_method) { match = false; continue; } }
          // 循環(huán)完了還沒有匹配的,就done了,其實就是404 if (match !== true) { return done(); }
          // 如果匹配上了,就執(zhí)行對應(yīng)的回調(diào)函數(shù) return layer.handle_request(req, res, next); }};

          上面代碼還用到了幾個LayerRoute的實例方法:

          layer.match(path): 檢測當(dāng)前layerpath是否匹配。

          route._handles_method(method):檢測當(dāng)前routemethod是否匹配。

          layer.handle_request(req, res, next):使用layer的回調(diào)函數(shù)來處理請求。

          這幾個方法看起來并不復(fù)雜,我們后面一個一個來實現(xiàn)。

          到這里其實還有個疑問。從他整個的匹配流程來看,他尋找的其實是router.stack.layer這一層,但是最終應(yīng)該執(zhí)行的回調(diào)卻是在router.stack.layer.route.stack.layer.handle。這是怎么通過router.stack.layer找到最終的router.stack.layer.route.stack.layer.handle來執(zhí)行的呢?

          這要回到我們前面的router.route方法:

          proto.route = function route(path) {  var route = new Route();  var layer = new Layer(path, route.dispatch.bind(route));
          layer.route = route;
          this.stack.push(layer);
          return route;}

          這里我們new Layer的時候給的回調(diào)其實是route.dispatch.bind(route),這個方法會再去route.stack上找到正確的layer來執(zhí)行。所以router.handle真正的流程其實是:

          1.找到path匹配的layer2.拿出layer上的route,看看有沒有匹配的method3.layermethod都有匹配的,再調(diào)用route.dispatch去找出真正的回調(diào)函數(shù)來執(zhí)行。

          所以又多了一個需要實現(xiàn)的函數(shù),route.dispatch。

          layer.match

          layer.match是用來檢測當(dāng)前path是否匹配的函數(shù),用到了一個第三方庫path-to-regexp,這個庫可以將path轉(zhuǎn)為正則表達式,方便后面的匹配,這個庫在之前寫過的react-router源碼[7]中也出現(xiàn)過。

          var pathRegexp = require("path-to-regexp");
          module.exports = Layer;
          function Layer(path, fn) { this.path = path;
          this.handle = fn; this.method = "";
          // 添加一個匹配正則 this.regexp = pathRegexp(path); // 快速匹配/ this.regexp.fast_slash = path === "/";}

          然后就可以添加match實例方法了:

          Layer.prototype.match = function match(path) {  var match;
          if (path != null) { if (this.regexp.fast_slash) { return true; }
          match = this.regexp.exec(path); }
          // 沒匹配上,返回false if (!match) { return false; }
          // 不然返回true return true;};

          layer.handle_request

          layer.handle_request是用來調(diào)用具體的回調(diào)函數(shù)的方法,其實就是拿出layer.handle來執(zhí)行:

          Layer.prototype.handle_request = function handle(req, res, next) {  var fn = this.handle;
          fn(req, res, next);};

          route._handles_method

          route._handles_method就是檢測當(dāng)前route是否包含需要的method,因為之前添加了一個methods對象,可以用它來進行快速查找:

          Route.prototype._handles_method = function _handles_method(method) {  var name = method.toLowerCase();
          return Boolean(this.methods[name]);};

          route.dispatch

          route.dispatch其實是router.stack.layer的回調(diào)函數(shù),作用是找到對應(yīng)的router.stack.layer.route.stack.layer.handle并執(zhí)行。

          Route.prototype.dispatch = function dispatch(req, res, done) {  var idx = 0;  var stack = this.stack; // 注意這個stack是route.stack
          // 如果stack為空,直接done // 這里的done其實是router.stack.layer的next // 也就是執(zhí)行下一個router.stack.layer if (stack.length === 0) { return done(); }
          var method = req.method.toLowerCase();
          // 這個next方法其實是在router.stack.layer.route.stack上尋找method匹配的layer // 找到了就執(zhí)行l(wèi)ayer的回調(diào)函數(shù) next(); function next() { var layer = stack[idx++]; if (!layer) { return done(); }
          if (layer.method && layer.method !== method) { return next(); }
          layer.handle_request(req, res, next); }};

          到這里其實Express整體的路由結(jié)構(gòu),注冊和執(zhí)行流程都完成了,貼下對應(yīng)的官方源碼:

          Router類:https://github.com/expressjs/express/blob/master/lib/router/index.js

          Layer類:https://github.com/expressjs/express/blob/master/lib/router/layer.js

          Route類:https://github.com/expressjs/express/blob/master/lib/router/route.js

          中間件

          其實我們前面已經(jīng)隱含了中間件,從前面的結(jié)構(gòu)可以看出,一個網(wǎng)絡(luò)請求過來,會到router的第一個layer,然后調(diào)用next到到第二個layer,匹配上layerpath就執(zhí)行回調(diào),然后一直這樣把所有的layer都走完。所以中間件是啥?中間件就是一個layer,他的path默認(rèn)是/,也就是對所有請求都生效。按照這個思路,代碼就簡單了:

          // application.js
          // app.use就是調(diào)用router.useapp.use = function use(fn) { var path = "/";
          this.lazyrouter(); var router = this._router; router.use(path, fn);};

          然后在router.use里面再加一層layer就行了:

          proto.use = function use(path, fn) {  var layer = new Layer(path, fn);
          this.stack.push(layer);};

          總結(jié)

          1.Express也是用原生APIhttp.createServer來實現(xiàn)的。2.Express的主要工作是將http.createServer的回調(diào)函數(shù)拆出來了,構(gòu)建了一個路由結(jié)構(gòu)Router3.這個路由結(jié)構(gòu)由很多層layer組成。4.一個中間件就是一個layer。5.路由也是一個layer,layer上有一個path屬性來表示他可以處理的API路徑。6.path可能有不同的method,每個method對應(yīng)layer.route上的一個layer。7.layer.route上的layer雖然名字和router上的layer一樣,但是功能側(cè)重點并不一樣,這也是源碼中讓人困惑的一個點。8.layer.route上的layer的主要參數(shù)是methodhandle,如果method匹配了,就執(zhí)行對應(yīng)的handle。9.整個路由匹配過程其實就是遍歷router.layer的一個過程。10.每個請求來了都會遍歷一遍所有的layer,匹配上就執(zhí)行回調(diào),一個請求可能會匹配上多個layer。11.總體來看,Express代碼給人的感覺并不是很完美,特別是Layer類肩負(fù)兩種職責(zé),跟軟件工程強調(diào)的單一職責(zé)原則不符,這也導(dǎo)致Router,Layer,Route三個類的調(diào)用關(guān)系有點混亂。而且對于繼承和原型的使用都是很老的方式??赡芤彩沁@種不完美催生了Koa的誕生,下一篇文章我們就來看看Koa的源碼吧。12.Express其實還對原生的reqres進行了擴展,讓他們變得更好用,但是這個其實只相當(dāng)于一個語法糖,對整體架構(gòu)沒有太大影響,所以本文就沒涉及了。

          本文可運行代碼已經(jīng)上傳GitHub,拿下來一邊玩代碼,一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

          參考資料

          Express官方文檔:http://expressjs.com/

          Express官方源碼:https://github.com/expressjs/express/tree/master/lib

          文章的最后,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發(fā),請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續(xù)創(chuàng)作的動力。

          作者博文GitHub項目地址:https://github.com/dennis-jiang/Front-End-Knowledges

          作者掘金文章匯總:https://juejin.im/post/5e3ffc85518825494e2772fd

          我也搞了個公眾號[進擊的大前端],不打廣告,不寫水文,只發(fā)高質(zhì)量原創(chuàng),歡迎關(guān)注~

          References

          [1]?上一篇文章我們講了怎么用Node.js原生API來寫一個web服務(wù)器:?https://juejin.im/post/6887797543212843016
          [2]?serve-static:?https://github.com/expressjs/serve-static
          [3]?本文所有手寫代碼全部參照官方源碼寫成:?https://github.com/expressjs/express/tree/master/lib
          [4]?這種將不同部分抽取出來,從而復(fù)用共同部分的代碼,有點像我之前另一篇文章寫過的設(shè)計模式----享元模式:?https://juejin.im/post/6844904168017100813#heading-3
          [5]?我之前在講JS的面向?qū)ο蟮奈恼??https://juejin.im/post/6844904069887164423
          [6]?他的代碼在這里:?https://github.com/wesleytodd/setprototypeof/blob/master/index.js
          [7]?之前寫過的react-router源碼:?https://juejin.im/post/6855129007949398029#heading-8


          ●?【尤大出品】面向未來的前端構(gòu)建工具 - Vite

          ●?一杯茶的時間,上手 Koa2 + MySQL 開發(fā)

          ●?動手實現(xiàn)一個 Koa 框架(萬字實戰(zhàn)好文)



          ·END·

          圖雀社區(qū)

          匯聚精彩的免費實戰(zhàn)教程



          關(guān)注公眾號回復(fù) z 拉學(xué)習(xí)交流群


          喜歡本文,點個“在看”告訴我

          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大屌操骚逼 | 久久国产乱子伦精品免费女,网站 | 久久夜色婷婷 | 国产精品V亚洲精品V日韩精品 | 大香蕉伊在线观看视频 |