I am Xiao 17 _, today with you to read the structure and all the source code of KOA, koA is a Web framework with asynchronous middleware. If you don’t know how Koa middleware works, you can start by reading this article: How to understand Koa middleware (Onion model) principles

IO/Readingkoa -…

We will cover all the files in Koa, Koa source contains only four files (cool ~) :

File 1: Application File (application.js)

This is the entry file for Koa.

We typically initialize the KOA server like this:

const Koa = require('koa');
const app = new Koa();
app.listen(3000);
Copy the code

New Koa() actually instantiates a new Application object, which is the constructor in application.js:

module.exports = class Application extends Emitter {
  constructor() {
    super(a);this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context); // from file 2: context.js
    this.request = Object.create(request); // from file 3: request.js
    this.response = Object.create(response); // from file 4: response.js
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect; }}Copy the code

About the Emitter

New Koa() instantiates an Application object that extends Emitter. Extending the Emitter class exposes an eventemitterObject.on () function, which means we can append events to Koa like this:

const app = new Koa();
app.on('event'.(data) = > {
  console.log('an event occurred! ' + data); // an event occurred! 123
});
app.emit('event'.123);
Copy the code

When the EventEmitter object emits an event, all functions attached to that particular event are called synchronously, and any values returned by the called listener are ignored and discarded.

Events | Node. Js v12.4.0 Documentation

About the Object. The create ()

We can also see object.create () in the constructor, which simply creates a new Object, using an existing Object as a prototype for the newly created Object. They have different reference addresses.

Here are some examples:

const person = {
  isHuman: false.printIntroduction: function () {
    console.log(`My name is The ${this.name}. Am I human? The ${this.isHuman}`); }};const me = Object.create(person);
me.name = "Matthew"; // "name" is an attribute of the "me" object, but not of the "person" object
me.isHuman = true; // Inherited attributes can be overridden
me.printIntroduction(); // "My name is Matthew. Am I human? true"
Copy the code

Object.create()

Start the server

After new Koa(), we can look at app.listen(3000). If we use app.listen(3000); Starting the server executes the following code:

listen(. args) {
  debug('listen');
  // Step 1: Call callback() to create an HTTP server
  const server = http.createServer(this.callback());
  // Step 5: After the HTTP server is created, start listening on the port
  returnserver.listen(... args); }callback() {
  // Step 2: Prepare middleware
  const fn = compose(this.middleware);  
  if (!this.listenerCount('error')) 
      this.on('error'.this.onerror);  
  const handleRequest = (req, res) = > {
    // Step 3: createContext, which we'll discuss in more detail
    const ctx = this.createContext(req, res);
    // Step 4: handleRequest, which we'll discuss in more detail
    return this.handleRequest(ctx, fn);
  };  
  return handleRequest;
}
Copy the code

If you want to know how to start an HTTP server without using Koa, here is a normal way to create the server directly using the HTTP package:

const http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('Hello World! ');
    res.end();
}).listen(8080);
Copy the code

About createContext (adding comments to code)

createContext(req, res) {
    // Create a new object based on this.context
    const context = Object.create(this.context);
    // Create a new object and make sure the Request and Response objects are accessible in the context object
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    // Ensure that context, Request, Response, and app objects are accessible to each other
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    // Again, ensure that the Response object is accessible inside the Request object
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    // Returns the context object, which is the CTX object we can use in middleware
    return context;
}
Copy the code

About handleRequest (adding comments to code)

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err= > ctx.onerror(err);
    Respond () when all middleware is complete
    const handleResponse = () = > respond(ctx);
    // If the res from the HTTP package throws an error, call onError
    onFinished(res, onerror);
    The middleware part was covered in the previous article and will not be covered here
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code

Respond (Adds comments to the code)

// Just append ctx.body to res, nothing special here
function respond(ctx) {
  if (false === ctx.respond) return;  
  if(! ctx.writable)return;  
  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;  
  / / ignore the body
  if (statuses.empty[code]) {
    / / remove headers
    ctx.body = null;
    return res.end();
  }  
  if ('HEAD' == ctx.method) {
    if(! res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }  
  // If the body does not exist, return
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if(! res.headersSent) { ctx.type ='text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }  
  // If the body type is buffer, return body directly
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string'= =typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);  
  // JSON encryption body
  body = JSON.stringify(body);
  if(! res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }Copy the code

File 2: Context (context.js)

This file uses a package called delegate to export methods in context.js, and I wrote an article to see how this package works:

This is the bottom of the context.js file:

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')... delegate(proto,'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .access('querystring')
Copy the code

This means that when you access ctx.QueryString, it is actually accessing ctx.request. queryString, and ctX.Request is allocated when createContext is called.

So the delegate basically lets you access the methods in response and Request easily by using CTX in the middleware (since all middleware has CTX as input). Here is an example of the middleware mentioned in the previous article:

// Here is the ctx
app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = 'Hello World';
    await next();
    console.log(4);
});
Copy the code

File 3: Request (request.js)

This is the prototype of CTx.Request. This file lets you access all HTTP request data from this.req, such as headers, IP, host, URL, etc… Here are some examples:

get(field) {
    const req = this.req;
    switch (field = field.toLowerCase()) {
        case 'referer':
        case 'referrer':
            return req.headers.referrer || req.headers.referer || ' ';
        default:
            return req.headers[field] || ' '; }},Copy the code

File 4: Response (response.js)

This is the prototype of CTx.Response. This file gives you access to data in this.res, such as headers and bodies. Here is part of the source code:

set(field, val) {
    if (this.headerSent) return;    
    if (2= =arguments.length) {
      if (Array.isArray(val)) val = val.map(v= > typeof v === 'string' ? v : String(v));
      else if (typeofval ! = ='string') val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (const key in field) {
        this.set(key, field[key]); }}}Copy the code

Thanks for reading and feel free to like and discuss if it’s helpful