当前位置:网站首页>Handwriting Koa.js Source code

Handwriting Koa.js Source code

2020-11-09 11:30:00 A kind of Jiang Pengfei

use Node.js Write a web The server , I've written two articles before :

Express Source code or more complex , With routing processing and static resource support and other functions , The functions are quite comprehensive . Compared with , This article will talk about Koa It's much simpler ,Koa Although it is Express It was written by the original people of , But the design idea is different .Express It's more biased towards All in one Thought , All kinds of functions are integrated together , and Koa The library itself has only one middleware kernel , Other functions like routing processing and static resources are not available , All need to introduce a third-party middleware library to achieve . The following picture can be seen intuitively Express and koa The difference in function , This is from the official document

image-20201029144409936

be based on Koa This kind of architecture , I plan to write in several articles , All source code parsing :

  • Koa Will write an article on the core architecture of , This is the article .
  • For one web The server Come on , Routing is essential , therefore @koa/router Can write an article .
  • In addition, some common middleware may be written , Static files support or bodyparser wait , I haven't decided yet , There may be one or more articles .

This article can run mini version Koa The code has been uploaded GitHub, lift down , Play code while reading the article better :https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore

A simple example

I write source code analysis , Generally follow a simple routine : First introduce it into the warehouse , Write a simple example , Then I write the source code to replace the library , And let our example run smoothly . This article also follows this routine , because Koa Only middleware is the core library of , So the examples we write are also simpler , It's just middleware .

Hello World

The first example is Hello World, Any request for a path will return Hello World.

const Koa = require("koa");
const app = new Koa();

app.use((ctx) => {
  ctx.body = "Hello World";
});

const port = 3001;
app.listen(port, () => {
  console.log(`Server is running on http://127.0.0.1:${port}/`);
});

logger

Then one more logger Well , It's recording how long it took to process the current request :

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

Note that this middleware should be placed in Hello World In front of .

From the code of the two examples above ,Koa Follow Express There are several obvious differences :

  • ctx Replaced the req and res
  • have access to JS The new API 了 , such as async and await

Handwritten source code

Let's see what we used before writing the source code API, These are the goals of our handwriting :

  • new Koa(): First of all, it must be Koa This class , Because he used new instantiate , So we think of him as a class .
  • app.useapp yes Koa An example of ,app.use It seems to be an instance method of adding middleware .
  • app.listen: Instance method to start the server
  • ctx: This is Koa The context of , It seems to replace the previous req and res
  • async and await: Support new syntax , And it can use await next(), explain next() It's likely to be a promise.

The handwritten source code of this article is written with reference to the official source code , Try to keep the file name and function name as consistent as possible , When writing specific methods, I will also paste the official source code address .Koa This library code is not much , It's mainly in this folder :https://github.com/koajs/koa/tree/master/lib, Let's start .

Koa class

from Koa Project package.json Inside main This line of code shows that , The entry point of the whole application is lib/application.js This file :

"main": "lib/application.js",

lib/application.js This document is what we often use Koa class , Although we often call him Koa class , But in the source code, this class is called Application. Let's write down the shell of this class first :

// application.js

const Emitter = require("events");

// module.exports  Direct export Application class 
module.exports = class Application extends Emitter {
  //  Constructor first runs the constructor of the parent class 
  //  And do some initialization work 
  constructor() {
    super();

    // middleware Instance property is initialized to an empty array , It is used to store the following possible middleware 
    this.middleware = [];
  }
};

We can see from this code that ,Koa Use it directly class Keyword to declare the class , Before you saw me Express Friends of source code resolution may also have an impression ,Express The source code is still used in the old prototype To implement object-oriented . therefore Koa In the project introduction Expressive middleware for node.js using ES2017 async functions It's not an empty word , It not only supports ES2017 new API, And in their own source code is also used in the new API. I think so Koa The operating environment must be node v7.6.0 or higher Why? . So here we can actually see that Koa and Express One of the big differences between , That's it :Express Use the old API, More compatible , Can be in the old Node.js Run... On version ;Koa Because of the new API, Only in v7.6.0 Or a later version of .

There's another thing to note about this code , That's it Application Inherited from Node.js Native EventEmitter class , This class is actually a publish subscribe model , You can subscribe and publish messages , I detailed his source code in another article . So he has some methods if he's in application.js I can't find , That might be inherited from EventEmitter, For example, the following line of code :

image-20201029151525287

Here you are this.on This method , It looks like he should be Application An example method of , But this document doesn't contain , In fact, he inherited from EventEmitter, It's for error This event adds the callback function of . This line of code if Inside this.listenerCount It's also EventEmitter An example method of .

Application Class is exactly JS The use of object-oriented , If you are right about JS Object oriented is not very familiar with , You can read this article first :https://juejin.im/post/6844904069887164423.

app.use

From the example we used earlier, you can see app.use Is to add a middleware , We also initialize a variable in the constructor middleware, Used to store middleware , therefore app.use The code is very simple , Just plug the received middleware into this array :

use(fn) {
  //  Middleware must be a function , Otherwise, report an error 
  if (typeof fn !== "function")
    throw new TypeError("middleware must be a function!");

  //  The processing logic is simple , The middleware will be plugged into middleware An array of line 
  this.middleware.push(fn);
  return this;
}

Be careful app.use Method finally returns this, This is kind of interesting , Why return to this Well ? This is actually what I mentioned in other articles : Class returns this Chain call can be implemented . Like here app.use Then you can continue to point , like this :

app.use(middlewaer1).use(middlewaer2).use(middlewaer3)

Why do you have this effect ? Because of the this In fact, this is the current example , That is to say app, therefore app.use() The return value of app,app There's an example method on use, So you can go on app.use().use().

app.use The official source code here : https://github.com/koajs/koa/blob/master/lib/application.js#L122

app.listen

In the previous example ,app.listen Is used to start the server , I've seen it in the front, using the original API Realization web The server Our friends all know , To start the server, you need to call native http.createServer, So this method is used to call http.createServer Of .

listen(...args) {
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

This method itself has not much to say , Just call http The module starts the service , The main logic is this.callback() Inside the .

app.listen The official source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L79

app.callback

this.callback() It's for http.createServer Callback function for , It's also an instance function , This function must conform to http.createServer The parametric form of , That is to say

http.createServer(function(req, res){})

therefore this.callback() The return value of must be a function , And it's in this form function(req, res){}.

Except that the form must conform to ,this.callback() What are you going to do ? He is http Module callback function , So he has to deal with all the web requests , All processing logic must be in this method . however Koa The processing logic of is in the form of middleware , For a request , He had to go through all the middleware one by one , The logic of the specific passage , Of course you can traverse middleware This array , Take out the methods one by one , Of course, it can also be used in a more common way in the industry :compose.

compose Generally speaking, it is to merge a series of methods into one method to facilitate the call , The form of concrete implementation is not fixed , Yes The common use in an interview is reduce Realized compose, Also like Koa In this way, according to their own needs to achieve a separate compose.Koa Of compose It also encapsulates a library separately koa-compose, The source code of this library is also what we must see , Let's go step by step , The first this.callback Write it out .

callback() {
  // compose come from koa-compose library , It is to combine middleware into a function 
  //  We need to achieve 
  const fn = compose(this.middleware);

  // callback The return value must match http.createServer Parameter form 
  //  namely  (req, res) => {}
  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

This method starts with koa-compose The middleware is composed into a function fn, And then in http.createServer The callback uses req and res Created a Koa Common context ctx, Then call this.handleRequest To actually handle network requests . Notice the this.handleRequest It's an example method , And the local variables in the current method handleRequest It's not a thing . Let's look at these methods one by one .

this.callback The corresponding official source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L143

koa-compose

koa-compose Although it was used as a separate library , But his role is crucial , So let's also look at his source code .koa-compose Is to merge an array of middleware into a method for external call . Let's go back to the next one Koa Architecture of middleware :

function middleware(ctx, next) {}

This array has many such middleware :

[
  function middleware1(ctx, next) {},
  function middleware2(ctx, next) {}
]

Koa The idea of merging is not complicated , Is to make compose Return a function , The returned function will start traversing the array :

function compose(middleware) {
  //  Parameter check ,middleware It must be an array 
  if (!Array.isArray(middleware))
    throw new TypeError("Middleware stack must be an array!");
  //  Each item in the array must be a method 
  for (const fn of middleware) {
    if (typeof fn !== "function")
      throw new TypeError("Middleware must be composed of functions!");
  }

  //  Return a method , This method is compose Result 
  //  External can call this method to start traversing middleware array 
  //  The form of parameters is the same as that of ordinary middleware , All are context and next
  return function (context, next) {
    return dispatch(0); //  Start middleware execution , Start with the first one in the array 

    //  The method of executing middleware 
    function dispatch(i) {
      let fn = middleware[i]; //  Take out the middleware that needs to be executed 

      //  If i Equal to the array length , Indicates that the array has been executed 
      if (i === middleware.length) {
        fn = next; //  Here let's fn It's equal to what's coming in from the outside next, In fact, it's the finishing line , Such as return 404
      }

      //  If there's no ending from the outside next, It's just resolve
      if (!fn) {
        return Promise.resolve();
      }

      //  Execution middleware , Note that the parameters passed to the middleware should be context and next
      //  To middleware next yes dispatch.bind(null, i + 1)
      //  So the middleware calls next In fact, it calls dispatch(i + 1), That is to execute the next middleware 
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

The main logic of the above code is this line :

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

there fn It's middleware that we write ourselves , For example, at the beginning of the article logger, Let's change it a little bit and see more clearly :

const logger = async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
};

app.use(logger);

Then we compose What's implemented inside is actually :

logger(context, dispatch.bind(null, i + 1));

in other words logger The received next It's actually dispatch.bind(null, i + 1), You call next() When , It's actually calling theta dispatch(i + 1), In this way, the effect of executing the next middleware of array is achieved .

In addition, the middleware is wrapped in a layer before returning Promise.resolve, So we all write middleware ourselves , Whether you use it or not Promise,next After the call, all returned are a Promise, So you can use await next().

koa-compose See the source code here :https://github.com/koajs/compose/blob/master/index.js

app.createContext

Used above this.createContext It's also an example method . This method is based on http.createServer Incoming req and res To build ctx This context , The official source code looks like this :

image-20201029163710087

In this code context,ctx,response,res,request,req,app These variables assign values to each other , I feel dizzy . There's no need to get into this pile of noodles , We just need to get his ideas and skeleton clear , How to carry it ?

  1. First, find out the purpose of his assignment , His purpose is very simple , Just for the convenience of use . You can easily get other variables through one variable , For example, I only have request, But what I want is req, What shall I do? ? After this assignment , Direct use request.req Just go . Other similar , It's hard for me to say whether it's good or not , But it's really convenient to use , The disadvantage is that it is easy to get stuck in the source code .
  2. that request and req What's the difference ? These two variables look so much like , What the hell is it ? This is to say Koa For native req An extension of , We know http.createServer In the callback of req As a description of the request object , You can get the requested header ah ,method Ah, these variables . however Koa Think this req Provided API It fails to work well , So he expanded a little on this basis API, In fact, it's just some grammar sugar , Extended req It becomes request. The original... That was preserved after the extension req, Should also want to provide users with more choices . So the difference between these two variables is request yes Koa Packed req,req Is a native request object .response and res It's the same thing .
  3. since request and response They're just packaged grammar candy , Well, actually Koa You can run without these two variables . So when we carry the skeleton, we can kick these two variables out , Now the skeleton is clear .

Then we kick out response and request Then write down createContext This method :

//  Create context ctx Object function 
createContext(req, res) {
  const context = Object.create(this.context);
  context.app = this;
  context.req = req;
  context.res = res;

  return context;
}

Now the whole world feels fresh ,context What's on it is clear at a glance . But our context It was originally from this.context Of , This variable also has to look at .

app.createContext The corresponding official source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L177

context.js

above this.context It's actually from context.js, So let's start with Application Add this variable to the constructor :

// application.js

const context = require("./context");

//  In the constructor 
constructor() {
	//  Omit other code 
  this.context = context;
}

Then we'll see context.js There's something in it ,context.js It's like this :

const delegate = require("delegates");

module.exports = {
  inspect() {},
  toJSON() {},
  throw() {},
  onerror() {},
};

const proto = module.exports;

delegate(proto, "response")
  .method("set")
  .method("append")
  .access("message")
  .access("body");

delegate(proto, "request")
  .method("acceptsLanguages")
  .method("accepts")
  .access("querystring")
  .access("socket");

In this code context The export is an object proto, The object itself has some methods ,inspect,toJSON And so on. . And then there's a bunch of delegate().method(),delegate().access() And so on. . Um. , What's this for ? You know what this does , We need to see delegates This library :https://github.com/tj/node-delegates, This library is also tj Written by the great God . The general use is like this :

delegate(proto, target).method("set");

The purpose of this line of code is , When you call proto.set() When the method is used , It was actually forwarded to proto[target], What's actually called is proto[target].set(). So is proto Acting right target The interview of .

That's for us context.js What does it mean in it ? Like this line of code :

delegate(proto, "response")
  .method("set");

The purpose of this line of code is , When you call proto.set() when , Actually call proto.response.set(), take proto Switch to ctx Namely : When you call ctx.set() when , What's actually called is ctx.response.set(). The purpose of this is actually to make it easy to use , You can write one less response. and ctx It's not just acting response, And acting for request, So you can also go through ctx.accepts() This calls to ctx.request.accepts(). One ctx It includes response and request, So here context It's also a grammar sugar . Because we've played in front of us response and request These two grammatical sugars ,context As the grammar sugar that packaged these two grammatical sugars , Let's kick it out, too . stay Application In the constructor of the this.context Assign an empty object :

// application.js
constructor() {
	//  Omit other code 
  this.context = {};
}

Now grammar sugar is kicked out , Whole Koa The structure is clearer ,ctx There are only a few required variables :

ctx = {
  app,
  req,
  res
}

context.js The corresponding source code here :https://github.com/koajs/koa/blob/master/lib/context.js

app.handleRequest

Now we ctx and fn It's all constructed , Then we process the request by invoking fn,ctx It was passed to him as a parameter , therefore app.handleRequest The code can be written :

//  Processing specific requests 
handleRequest(ctx, fnMiddleware) {
  const handleResponse = () => respond(ctx);

  //  Call middleware to handle 
  //  After all processing is finished, call handleResponse Return request 
  return fnMiddleware(ctx)
    .then(handleResponse)
    .catch((err) => {
    console.log("Somethis is wrong: ", err);
  });
}

We see compose Library returns fn Although the second parameter is supported to close out , however Koa It didn't use him , If not , After all middleware is executed, it returns an empty promise, So it can be used then Then he dealt with it later . There is only one processing to be done later , It is to return the processing results to the requester , This is the same. respond What needs to be done .

app.handleRequest The corresponding source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L162

respond

respond It's an auxiliary method , It's not in Application In class , All he has to do is return the web request :

function respond(ctx) {
  const res = ctx.res; //  Take out res object 
  const body = ctx.body; //  Take out body

  return res.end(body); //  use res return body
}

Be accomplished

Now we can write our own Koa Replace the official Koa Let's run the example we started with , however logger There will be some problems when the middleware is running , Because he uses syntax sugar in the following line of code :

console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);

there ctx.method and ctx.url In what we build ctx There is no such thing as , But that's okay , Isn't he just one req The grammar of sugar , We from ctx.req Just take it up , So the line above is changed to :

console.log(`${ctx.req.method} ${ctx.req.url} - ${ms}ms`);

summary

Through layer by layer of silk cocoon , We managed to pull out Koa The code skeleton of , I wrote a mini version of Koa.

This mini code has been uploaded GitHub, You can take it down and play with it :https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore

Finally, let's summarize the main points of this paper :

  1. Koa yes Express A new framework written by the original team .
  2. Koa Used JS The new API, such as async and await.
  3. Koa Architecture and Express Make a big difference .
  4. Express The idea is big and comprehensive , A lot of functions are built in , Like routing , Static resources, etc , and Express The middleware is also implemented using the same mechanism of routing , The whole code is more complicated .Express Source code can see my previous article : Handwriting Express.js Source code
  5. Koa It looks clearer ,Koa A kernel library is just a library of its own , Only middleware functions , Requests come through each Middleware in turn , And then come out and return it to the requester , This is what you often hear about “ Onion model ”.
  6. to want to Koa Support other functions , Middleware must be added manually . As a web The server , Routing is a basic function , So we'll come and see... The next article Koa Official routing Library @koa/router, Stay tuned .

Reference material

Koa Official documents :https://github.com/koajs/koa

Koa Source code address :https://github.com/koajs/koa/tree/master/lib

At the end of the article , Thank you for your precious time reading this article , If this article gives you a little help or inspiration , Please don't be stingy with your praise and GitHub Little star , Your support is the motivation for the author to continue to create .

Author's blog GitHub Project address : https://github.com/dennis-jiang/Front-End-Knowledges

Author digs the gold article summary :https://juejin.im/post/5e3ffc85518825494e2772fd

I also made a official account [ The big front of the attack ], No advertising , Don't write about hydrology , Only high quality original , Welcome to your attention ~

版权声明
本文为[A kind of Jiang Pengfei]所创,转载请带上原文链接,感谢