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