koa study

I have written a lot of demos with koa before, and added an actual online application, but I have never seen its source code.

This time I took the time to look at the source code.

It's actually only 4 files:

  • application.js (main file)
  • context.js (creates a context object for network requests)
  • request.js (request object that wraps koa)
  • response.js (wrapping koa's response object)

alt

Through the package.jsonfile, we can clearly see:

alt

application.jsIt's an entry file, so let's go in and have a look.

alt

core method

  • listen
  • use

Basic usage

const Koa = require('koa');
const app = new Koa();
app.listen(3000);
app.use((ctx, next) => {
      ctx.body = "hello"
})

listen

alt

It's just a service.

There is a debug module that can be used to do some debugging. (The premise is that your environment variable is set to DEBUG, otherwise you will not see the output)

callback function code:

alt

use

The use method, the annotation given by the source code is:

Use the given middleware fn.

In Koa, the entire service is built through one by one middleware.

The implementation of the use method is super simple:

alt

In the above callback function, there is a code:

const fn = componse(this.middleware)

It is used to combine all middleware

middleware

alt

For example, we have this piece of code:

let fn1 = (ctx,next)=>{
    console.log(1);
    next();
    console.log(2);
}
let fn2 = (ctx,next)=>{
    console.log(3);
    next();
    console.log(4);
}
let fn3 = (ctx,next)=>{
    console.log(5);
    next();
    console.log(6);
}

Hope to get: 1, 3, 5, 6, 4, 2 results.

This code is easier:

let fns = [fn1,fn2,fn3]; 
function dispatch(index){
    let middle = fns[index];
    // 判断一下临界点
    if(fns.length === index) return function(){}
    middle({},()=>dispatch(index+1));
}
dispatch(0);

After understanding the way of writing synchronization, when the middleware is written as asyn await, it is easy to write.

function dispatch(index){
    let middle = fns[index];
    if(fns.length === index) return Promise.resolve()
    return Promise.resolve(middle({},()=>dispatch(index+1)))
}

Let's take a look at the code of compose:

alt

The core logic is similar to the above code, but it is more rigorous in logical judgment.

easy mistakes

const Koa = require('koa');
const app = new Koa();
app.listen(3000);
function ajax(){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve("123");
        },3000)
    })
}
app.use(async (ctx,next)=>{
    console.log(1);
    next();
    console.log(2);
});

app.use(async (ctx, next) => {
    ctx.body = await ajax();
})

The above result is not found, because there is no await next in the first middleware.

ctx

alt

Let's look createContextat the source code implementation:

alt

request

request.js

alt

It is to repackage the previous req object.

Advanced syntax is used here: get/set, similar to Object.definePrototype, which can mainly do some logic processing during set.

response

response.js

Similar to how request.js handles it. Here I excerpted a paragraph of body writing:

{
    get body() {
        return this._body;
    },
    set body(val) {
        const original = this._body;
        this._body = val;

        // no content
        if (null == val) {
            if (!statuses.empty[this.status]) this.status = 204;
            this.remove('Content-Type');
            this.remove('Content-Length');
            this.remove('Transfer-Encoding');
            return;
        }

        // set the status
        if (!this._explicitStatus) this.status = 200;

        // set the content-type only if not yet set
        const setType = !this.header['content-type'];

        // string
        if ('string' == typeof val) {
            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 ('function' == typeof val.pipe) {
            onFinish(this.res, destroy.bind(null, val));
            ensureErrorHandler(val, err => this.ctx.onerror(err));

            // overwriting
            if (null != original && original != val) this.remove('Content-Length');

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

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

context

What the context.js file does is more interesting.

alt

It makes a layer of proxy and directly mounts some attribute methods under the request and some attribute methods under the response on the ctx object.

For example, you have to pass ctx.request.urlto get the request path before, but now you only need to write it ctx.url.

delegateThis library, let's take a brief look, mainly look at two methods:

alt

We can simplify it a bit more:

let proto = {

}
function delateGetter(property,name){
    proto.__defineGetter__(name,function(){
        return this[property][name];
    })
}
function delateSetter(property,name){
    proto.__defineSetter__(name,function(val){
        this[property][name] = val;
    })
}
delateGetter('request','query');
delateGetter('request','method')
delateGetter('response','body');
delateSetter('response','body');

I believe that after reading it, I have a clearer understanding of the implementation logic.

Implementation of some middleware

After reading the source code of koa, we can know that koa itself is very small, the implementation is more elegant, and we can achieve what we want by writing middleware.

Commonly used middleware are probably: static, body-parser, router, session, etc.

koa-static

koa-static is a simple static middleware, its source code is here , the core logic implementation is completed by koa-send , but I flipped it, there is no etag processing.

alt

We can also write the simplest static middleware ourselves:

const path = require('path');
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
function static(p){
    return async (ctx,next)=>{
        try{
            p = path.join(p,'.'+ctx.path);
            let stateObj = await stat(p);
            console.log(p);
            if(stateObj.isDirectory()){
                
            }else{
                ctx.body = fs.createReadStream(p);
            }
        }catch(e){
            console.log(e)
            await next();
        }
    }
}    

body-parser

The basic code is as follows:

function bodyParser(){
    return async (ctx,next)=>{
        await new Promise((resolve,reject)=>{
            let buffers = [];
            ctx.req.on('data',function(data){
                buffers.push(data);
            });
            ctx.req.on('end',function(){
                ctx.request.body = Buffer.concat(buffers)
                resolve();
            });
        });
        await next();
    }
}
module.exports = bodyParser;

There Buffer.concat(buffers)are several situations that need to be dealt with, such as form, json, file, etc.

In koa-bodyparser , it co-bodywraps a layer with .

Handling of form and json is relatively easy:

querystring.parse(buff.toString()); // form的处理

JSON.parse(buff.toString()); // json的处理

What needs to be said here is how the file is processed:

alt

Here we need to encapsulate a Buffer.splitmethod to get a few pieces of content in the middle, and then perform the cutting process.

Buffer.prototype.split = function(sep){
    let pos = 0;
    let len = Buffer.from(sep).length;
    let index = -1;
    let arr = [];
    while(-1!=(index = this.indexOf(sep,pos))){
        arr.push(this.slice(pos,index));
        pos = index+len;
    }
    arr.push(this.slice(pos));
    return arr;
}
// 核心实现
let type = ctx.get('content-type');
let buff = Buffer.concat(buffers);
let fields = {}
if(type.includes('multipart/form-data')){
    let sep = '--'+type.split('=')[1];
    let lines = buff.split(sep).slice(1,-1);
    lines.forEach(line=>{
        let [head,content] = line.split('\r\n\r\n');
        head = head.slice(2).toString();
        content = content.slice(0,-2);
        let [,name] = head.match(/name="([^;]*)"/);
        if(head.includes('filename')){
            // 取除了head的部分
            let c = line.slice(head.length+6);
            let p = path.join(uploadDir,Math.random().toString());
            require('fs').writeFileSync(p,c)
            fields[name] = [{path:p}];
        } else {
           fields[name] = content.toString();
        }
    })
}
ctx.request.fields = fields;

Of course, like koa-better-bodythe file processing used in it, split is not used. it uses formidable

alt

alt

The interception operations are all handled in the multipart_parser.js file.

also-router

Basic usage

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa();
var router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

app
  .use(router.routes())
  .use(router.allowedMethods());

principle

There is an article on Nuggets: Interpret and implement a simple koa-router

alt

I will also analyze it according to the idea of ​​​​looking at the source code.

router.routesis to return a middleware:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    if (!matched.route) return next();

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

What it does is that the incoming request will go through router.match, and then push the execution function of the matched route into the array, and merge it back and execute it through the compose(koa-compose) function.

As we wrote router.get/post, all we do is register a route, and then stuff an instance of layer into this.stack:

alt

alt

In addition, like matching special symbols, such as: /:id/:name, it is processed by path-to-regexp .

Understand the above, combined with the source code analysis of the Nuggets, you can basically do it.

Summarize

Koa's things seem to be relatively simple, but there are still many things that have not been analyzed, such as the proxy in the source code.

However, according to the 28 rule, we basically only need to master 80% of the source code implementation.

last of the last

Advertise my blog, welcome to visit: Front-end world of small wings


Author: henryzp
Link: https://juejin.im/post/5ae980adf265da0b7f445d85
Source: Nuggets The
copyright belongs to the author. For commercial reprints, please contact the author for authorization, and for non-commercial reprints, please indicate the source.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325867286&siteId=291194637