Hand tear source code to achieve a Koa.

Keywords: Javascript Front-end

Official profile

Koa is a new web framework built by the original team behind Express. It is committed to becoming a smaller, more expressive and more robust cornerstone in the field of web application and API development. By using the async function, KOA helps you discard callback functions and effectively enhances error handling. Instead of binding any middleware, KOA provides a set of elegant methods to help you write server-side applications quickly and happily.

Pre knowledge

node http

Using http to build services

step1: import http

var http = require('http');

Step 2: the createserver method creates an http service

let server=http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/plain'});
    response.end('Hello World');
})

step3: listening port

server.listen(8000);

koa simple usage

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

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

app.listen(3000);

Core directory

  • application.js: entry, export koa.
  • context.js: Context
  • request.js: process request
  • response.js: process response

Export koa class

We briefly introduced the usage of KOA above. We can see that koa has two core functions, a use and a listen. Let's implement these two functions first.
We have learned the usage of http in advance, so things will be easy to do!

koa class

application.js
First define the basic structure of a koa.

class Application {
    use() {

    }

    listen() {
        
    }
}
module.exports = Application

app.listen

It mainly encapsulates http.listen.

class Application {
    callback = (req, res) => {
        res.end('Hello World\n');
    }
    listen() {
        const server = http.createServer(this.callback);
        console.log(...arguments)
        server.listen(...arguments)
    }
}

Test: test.js

const Koa=require('./application.js')

const app=new Koa()

app.listen(3000)

It can be accessed normally

app.use

From the previous knowledge, we can see that app.use receives a callback function and passes in a ctx context, where ctx encapsulates the request and response. In order to be clear and understandable, we do not encapsulate the context first.

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

Then, the simple processing is as follows:

class Application {
    use(fn) {
        this.fn=fn

    }
}

At this time, use receives a function. The execution time of this function is when visiting the website. At this time, you need to pass in this function when creating the http service. The best way is to call it in listen's callbak.

callback = (req, res) => {
    this.fn(req, res)
}

Final code

let http = require('http')
class Application {
    use(fn) {
        this.fn=fn

    }
    callback = (req, res) => {
        this.fn(req, res)
    }
  
    listen() {
        const server = http.createServer(this.callback);
        server.listen(...arguments)
    }
}

module.exports = Application

Test: test.js

const Koa=require('./application.js')

const app=new Koa()

app.use((req, res) => {
    res.end('Hello World\n');
})
app.listen(3000)

It can be accessed normally

Package ctx

Make it clear that each request is independent. For native http requests, the response and request of each request are different. For ctx in koa, it means that each requested ctx is a new ctx.

Structure of ctx

To see the structure of ctx, we first print ctx using the source koa. The final results are as follows. With this structure, we can implement our own ctx.

The following format is the result of console.dir(ctx) (some specific contents are deleted). From the following contents, we can get the structure of ctx..

{
  request: {
    app: Application {
     
    },
    req: IncomingMessage {

    },
    res: ServerResponse {

    },
    ctx: [Circular],
    response: {
      app: [Application],
      req: [IncomingMessage],
      res: [ServerResponse],
      ctx: [Circular],
      request: [Circular]
    },
    originalUrl: '/'
  },
  response: {
    app: Application {

    },
    req: IncomingMessage {
    },
    res: ServerResponse {

    },
    ctx: [Circular],
    request: {
      app: [Application],
      req: [IncomingMessage],
      res: [ServerResponse],
      ctx: [Circular],
      response: [Circular],
      originalUrl: '/'
    }
  },
  app: Application {
  },
  req: IncomingMessage {
  },
  res: ServerResponse {
  },
  originalUrl: '/',
  state: {}
}

context.js

context.js mainly defines the specific structure of context and the methods provided.

Koa Context encapsulates the request and response objects of node into a single object, which provides many useful methods for writing Web applications and API s. These operations are frequently used in HTTP server development. They are added to this level rather than a higher-level framework, which will force the middleware to re implement this common function.

request.js and response.js files

In the core directory, we mentioned these two files, which come in handy at this time. What do these two files achieve? These two files define the structures of ctx.resopnse and ctx.request, which is the result of using dir output above. stay koa Chinese documents You can see the specific structure in, and you can consult it yourself.

The Koa Request object is an abstraction based on the node's native request object and provides many useful functions for HTTP server development.

The Koa Response object is an abstraction based on the node's native response object and provides many useful functions for HTTP server development.

Implement ctx

Define context.js

const context={

}
module.exports=context

Define request.js

const request={

}

module.exports=request

Define response.js

const resposne={

}

module.exports=response

Package ctx in use

We can see from the chapter exporting koa above that when app.use is used, the parameters we pass are (request,response) and the ctx passed by the source koa, so we know that koa creates a ctx when app.use is used.

At the beginning of this chapter, we also mentioned that each ctx requested is a new ctx.

Based on the above two points, we can basically write the following code. (in order to make the code clear and readable, we encapsulated a createcontext function to create the context.)

const Context = require('./context')
const Request = require('./request')
const Response = require('./response')
class Application {
    constructor(){
         this.context = Object.create(Context);
         this.request = Object.create(Request);
         this.response = Object.create(Response);
    }
    use(fn) {
        this.fn = fn

    }
    createContext = (req, res) => {
        const ctx = Object.create(this.context);
        const request = Object.create(this.request);
        const response = Object.create(this.response);
        ctx.app = request.app = response.app = this
        ctx.request = request;
        ctx.request.req = ctx.req = req;

        ctx.response = response;
        ctx.response.res = ctx.res = res;
        ctx.originalUrl = request.originalUrl = req.url
        ctx.state = {}
        return ctx
    }
    callback = (req, res) => {
        let ctx = this.createContext(req, res)
        this.fn(ctx)
    }
    listen() {
        const server = http.createServer(this.callback);
        console.log(...arguments)
        server.listen(...arguments)
    }
}

First, we define a context object in the constructor, which will be defined here because the context attribute is exported by default on Koa's app.

App.context is the prototype from which ctx is created. You can add other attributes for ctx by editing app.context. This is useful for adding ctx to the properties or methods used throughout the application, which may be more effective (no middleware required) and / or simpler (less require()) and more dependent on ctx, which can be considered an anti pattern.
For example, to add a reference to a database from ctx:

app.context.db = db();
app.use(async ctx => {
 console.log(ctx.db);
});

Then, in the callback, we perform secondary encapsulation for response and request.

Let's look at this Code:

>app.context.db = db();
>app.use(async ctx => {
>  console.log(ctx.db);
>});

Before using use again, the context is modified through app.context. When using the use function, do you directly enter the callback function? At this time, this.context has been modified.

test

const Koa=require('./application.js')

const app=new Koa()

app.use((ctx) => {    
    // Test 1
    ctx.response.res.end(" hello my koa")
    // Test 2
    ctx.res.end(" hello my koa")
})
app.listen(3000,()=>{
    console.log('3000')
})

Normal access!

Encapsulate request.js

Clarify the fact that the properties of the request class are set through getter s and setter s. Why is it set like this? The advantage of this setting is that you can easily set and obtain values. Isn't it a little stupid! Please listen to me carefully.

Let's take a look at the properties bound by the request class in Koa, Official link.
Here are some simple examples:

### request.header=
Sets the request header object.

### request.headers
 Request header object. Alias is `request.header`.

### request.headers=
Sets the request header object. Alias is `request.header=`

### request.url
 Get request URL.

### request.url=
Set request URL, yes url Rewriting is useful.

### request.originalUrl
 Get request origin URL. 
  1. Here, each attribute has the function of setting and obtaining, which can be well implemented by using getter and setter.
  2. How do you get each attribute here? Remember what we bound in the request? node http native request (req), right? When we use Object.create and ctx.request.req=req, does the current request object have a req attribute. Can I get it through getter.
     get url () {
        return this.req.url
      },
    
  3. How are each attribute set here? Is it valid if we set the request itself?
    For example, the following structure:
    const request={
        url:'',
        header:{
    
        }
    }
    
    We directly request.url=“ https://juejin.cn/ ”, this will lead to a bug. What is it? Remember where we got our data from req. If you didn't set req.url when you set, can you get this value? So the structure of request is like this.
    const request={
        set url (val) {
            this.req.url = val
        }
        get url () {
            return this.req.url
            },
        }
    

getter of request.socket

Socket means socket here. The concept of socket is not repeated here!

  get socket () {
    return this.req.socket
  },

getter of request.protocol

Return the request protocol, "https" or "http". X-Forwarded-Proto is supported when app.proxy is true.

First judge whether encrypted exists in the socket. If encrypted, it is https,

X-Forwarded-Proto is used to determine the transport protocol (HTTP or HTTPS) used for the connection between the client and the proxy server or load balancing server

X-Forwarded-Proto: https

X-Forwarded-Proto: http

  get protocol () {
    if (this.socket.encrypted) return 'https'
    if (!this.app.proxy) return 'http'
    const proto = this.get('X-Forwarded-Proto')
    return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http'
  },

Here is a get function, which mainly obtains data from the request header according to the field.

 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] || ''
    }
  },

getter of request.host

Gets the Host (hostname:port) when it exists. When app.proxy is true, X-Forwarded-Host is supported; otherwise, Host is used.

get host () {
    const proxy = this.app.proxy
    let host = proxy && this.get('X-Forwarded-Host')
    if (!host) {
      if (this.req.httpVersionMajor >= 2) host = this.get(':authority')
      if (!host) host = this.get('Host')
    }
    if (!host) return ''
    return host.split(/\s*,\s*/, 1)[0]
  },


getter of request.origin

Get the source of the URL, including protocol and host.

For example, I ask: http://localhost:3000/index?a=3 ,

origin returns http://localhost:3000

get origin () {
    return `${this.protocol}://${this.host}`
  },

getter of request.href

Get the complete request url, including protocol, host and url.

href support parsing GET http://example.com/foo

For example, I visit http://localhost:3000/index?a=3
href return http://localhost:3000/index?a=3

 get href () {
   
    if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl
    return this.origin + this.originalUrl
  },

Note: this.originalUrl here has been bound when encapsulating ctx

getter and setter of request.header

Request header object. This is different from node http.IncomingMessage Upper headers Same field

  get header () {
    return this.req.headers
  },

  set header (val) {
    this.req.headers = val
  },

There are many attributes of request. We won't expand it. Anyway, we know the principle. Let's add them slowly.

Encapsulate response.js

Compared with the encapsulation of request, the encapsulation of response is slightly different, because most of the encapsulation of request is getter, while most of the encapsulation of response is setter

In the request section, we explained three reasons for using getter s and setter s. In repose, I think the main reason is to change the object of set.

In fact, it's easy to think about it. For example, we often encounter various states in network requests: 404, 200, etc. These are changed in the http module of node with reposne.status. Suppose we set the response directly in koa, do you think it will be useful? To sum up, KOA's request and respone are the secondary encapsulation of nodehttp module, and the bottom layer is the operation of nodehttp module.

getterh and setter of response.status

Get response status. By default, response.status is set to 404 instead of 200 as the res.statusCode of node.

The default is' 404 '. When is the default here? In fact, it is set to 404 after receiving the request, that is, it is set to 404 when the callback starts. (Note: res.statusCode in http is used to mark the status code, which is encapsulated as status in Koa)

  callback = (req, res) => {
        let ctx = this.createContext(req, res)
        const res = ctx.res
        res.statusCode = 404
        this.fn(ctx)
    }

Implementation of response.status

  get status () {
    return this.res.statusCode
  },

  set status (code) {
    if (this.headerSent) return

    assert(Number.isInteger(code), 'status code must be a number')
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`)
    this._explicitStatus = true
    this.res.statusCode = code
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
    if (this.body && statuses.empty[code]) this.body = null
  },

getter and setter of response.body

First of all, we need to know what body is used for. Body is used to set the response body, that is, to return the content of the response. These contents support the following formats:

  • string write
  • Buffer write
  • Stream pipeline
  • Object | array JSON - string
  • null no content response
  1. In nodehttp, res.end("I am returning content") returns the response content. In koa, we set the response content through CTX. Body = "". Someone here will ask, what is the relationship between ctx.body and resopnse.body. In fact, they are one thing. CTX encapsulates response.body.

  2. In koa, you can return the content by setting ctx.body. In fact, res.end() is used to return the content through res.end(ctx.body). The call timing of res.end is put in the callback here (we will talk about the specific reasons later)

const response = {
    _body: undefined,
    get body() {
        return this._body
    },
    set body(originContent) {
        this.res.statusCode = 200;
        this._body = originContent;
    }
};

Encapsulate context.js

Let's talk about delegates used by Koa first. This is a package that implements the proxy pattern. For Koa, context is the proxy of response and request. The properties and methods of request and response can be obtained directly through ctx.
The following are two main methods used by Koa. In fact, the final effect is consistent with the effect of encapsulating request and response.

__ defineGetter__ Method can bind a function to the specified property of the current object. When the value of that property is read, the bound function will be called.

__ defineSetter__ Method can bind a function to the specified property of the current object. When that property is assigned, the bound function will be called.

(these two methods are obsolete: this feature has been removed from the Web standard. Although some browsers still support it, it may stop supporting it at some time in the future. Please try not to use this feature.)

Delegator.prototype.setter = function (name) {
    var proto = this.proto;
    var target = this.target;
    this.setters.push(name);
    proto.__defineSetter__(name, function (val) {
        return this[target][name] = val;
    });
    return this;
};
Delegator.prototype.getter = function (name) {
    var proto = this.proto;
    var target = this.target;
    this.getters.push(name);
    proto.__defineGetter__(name, function () {
        return this[target][name];
    });
    return this;
};

Here, we separate the core logic of delegates and encapsulate the context

function defineGetter(target, key) {
    context.__defineGetter__(key, function () { 
        return this[target][key]
    })
}
function defineSetter(target, key) {
    context.__defineSetter__(key, function (value) { 
        this[target][key] = value
    })
}
const context = {};

defineGetter('request', 'path')

defineGetter('response', 'body')
)

module.exports = context;

Here we have listed two, and the others will not be repeated.

ctx.body Recap

Above, we talked about response.body and ctx. Through the proxy mode, we got response.body

In Koa's source code, different processing is carried out for different formats of content. Just take a simple look.

response = {
    set body (val) {
        const original = this._body
        this._body = val

        // no content
        if (val == null) {
          if (!statuses.empty[this.status]) {
            if (this.type === 'application/json') {
              this._body = 'null'
              return
            }
            this.status = 204
          }

          return
        }

        // The content exists (the content is set), which is the status code of 200
        if (!this._explicitStatus) this.status = 200



        // String string
        if (typeof val === 'string') {
          if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
          this.length = Buffer.byteLength(val)
          return
        }

        // buffer
        if (Buffer.isBuffer(val)) {
          if (setType) this.type = 'bin'
          this.length = val.length
          return
        }

        // stream
        if (val instanceof Stream) {
          onFinish(this.res, destroy.bind(null, val))
          if (original !== val) {
            val.once('error', err => this.ctx.onerror(err))
            // overwriting
            if (original != null) this.remove('Content-Length')
          }

          if (setType) this.type = 'bin'
          return
        }

        // json
        this.remove('Content-Length')
        this.type = 'json'
      }
  },


ctx.body is finally returned in res.end(), which is called in callback.

We pass in the method we want to execute through app.use, which has the assignment of ctx.body.

app.use((ctx) => {    
    console.log(ctx.request.href)
    ctx.body="123"
})

In the callback, we first create the context, and then we call the incoming method.

 callback = (req, res) => {
        let ctx = this.createContext(req, res)
        this.fn(ctx)
    }

Then, should we call res.end() after the execution of fn, because body is assigned only at this time.

 callback = (req, res) => {
        let ctx = this.createContext(req, res)
        this.fn(ctx)
        let body = ctx.body;
        if (body) {
            res.end(body);
        } else {
            res.end('Not Found')
        }
    }

Summary

So far, the basic content of koa has been realized.
Specifically:

    1. response encapsulates the res ponse of nodehttp through getter and setter
    1. request encapsulates the req of nodehttp through getter and setter
    1. ctx gets the attributes and methods of response and request through the agent

Posted by sdat1333 on Sat, 23 Oct 2021 17:13:49 -0700