We know that the Koa class library has the following key features:
- Middleware mechanisms that support the Onion circle model
- Encapsulate request and Response to provide context objects, facilitating HTTP operations
- Asynchronous functions, middleware error handling mechanisms
This article will take you through the steps of using TS to complete a simple framework that implements the core functions of Koa
Step 1: Basic Server running
Objective: To complete a new Koa Server that is fundamentally feasible
- Support app. Listen listening port to start the Server
- App. use adds middleware like handlers
The core code is as follows:
class Koa {
private middleware: middlewareFn = (a)= > {};
constructor() {}
listen(port: number, cb: noop) {
const server = http.createServer((req, res) = > {
this.middleware(req, res);
});
return server.listen(port, cb);
}
use(middlewareFn: middlewareFn) {
this.middleware = middlewareFn;
return this; }}const app = new Koa();
app.use((req, res) = > {
res.writeHead(200);
res.end("A request come in");
});
app.listen(3000, () = > {console.log("Server listen on port 3000");
});
Copy the code
Step 2: Implementation of onion ring middleware mechanism
Goal: Next we need to refine the Listen and Use methods to implement the Onion ring middleware model
As shown in the code below, in this step we want app.use to support the addition of multiple middleware, and the middleware to be executed in the order of onion rings (like deep recursive calls)
app.use(async (req, res, next) => {
console.log("middleware 1 start");
// There are two things to note here:
The next() function must be called only once
// we must use await when calling next
// The specific reason will be explained in detail in the following code implementation
await next();
console.log("middleware 1 end");
});
app.use(async (req, res, next) => {
console.log("middleware 2 start");
await next();
console.log("middleware 2 end");
});
app.use(async (req, res, next) => {
res.writeHead(200);
res.end("An request come in");
await next();
});
app.listen(3000, () = > {console.log("Server listen on port 3000");
});
Copy the code
Let’s take a look at how the onion ring mechanism works:
class Koa {... use(middlewareFn: middlewareFn) {Use uses an array to store all middleware
this.middlewares.push(middlewareFn);
return this;
}
listen(port: number, cb: noop) {
// 2. A function that converts the middleware array into serial calls through composeMiddleware, called in the callback function in createServer
// So the real focus is composeMiddleware. If so, let's look at the implementation of this function
// BTW: Fn is generated when listen is called, which means we can't dynamically add middleware at runtime
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
await fn(req, res);
});
returnserver.listen(port, cb); }}// 3. Core of onion Ring model:
// Input parameter: all collected middleware
// Returns a function that calls the middleware array serially
function composeMiddleware(middlewares: middlewareFn[]) {
return (req: IncomingMessage, res: ServerResponse) = > {
let start = - 1;
// dispatch: triggers the ith middleware execution
function dispatch(i: number) {
// At first, you may not understand why there is such a judgment, but you can look at the whole function to consider the problem
// Start < I, next() should start === I
// If next() is called multiple times, the second and subsequent calls will result in start >= I because start === I was assigned before
if (i <= start) {
return Promise.reject(new Error("next() call more than once!"));
}
if (i >= middlewares.length) {
return Promise.resolve();
}
start = i;
const middleware = middlewares[i];
// Here's the thing!!
// Fetch the i-th middleware execution and pass dispatch(I +1) as next to each of the next middleware
// Now let's review the two questions we raised earlier:
// 1. Why must the next function be called only once in KOA middleware
// You can see that if you do not call next, the next middleware will not be able to trigger, resulting in suspended animation and a timeout
// Calling next multiple times will be executed multiple times by the current middleware
// 2. Why should I await next()
// This is also the core of the onion ring call mechanism, when executing to await next(), next() is executed to wait for the result to be returned, and then proceed further
return middleware(req, res, () => {
return dispatch(i + 1);
});
}
return dispatch(0);
};
}
Copy the code
Step 3: Context provides
Target: Encapsulates the Context and provides convenient operations for request and response
// 1. Define KoaRequest, KoaResponse, KoaContextinterface KoaContext { request? : KoaRequest; response? : KoaResponse; body:String | null;
}
const context: KoaContext = {
get body() {
return this.response! .body; }, set body(body) {this.response!.body = body;
}
};
function composeMiddleware(middlewares: middlewareFn[]) {
return (context: KoaContext) = > {
let start = - 1;
function dispatch(i: number) {
/ /.. Omit other code..
// All middleware accept the context parameter
middleware(context, () => {
return dispatch(i + 1);
});
}
return dispatch(0);
};
}
class Koa {
private context: KoaContext = Object.create(context);
listen(port: number, cb: noop) {
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
// create context object with req and res
Context creates a new object, not assigning it directly to this.context
Since the context fits the request associated with it, it also ensures that each request is a new context object
const context = this.createContext(req, res);
await fn(context);
if (context.response && context.response.res) {
context.response.res.writeHead(200); context.response.res.end(context.body); }});return server.listen(port, cb);
}
// create a context object
createContext(req: IncomingMessage, res: ServerResponse): KoaContext {
// Why use object.create instead of assigning directly?
// Make sure that each request request, response, and context are all new
const request = Object.create(this.request);
const response = Object.create(this.response);
const context = Object.create(this.context);
request.req = req;
response.res = res;
context.request = request;
context.response = response;
returncontext; }}Copy the code
Step 4: Asynchronous function error handling mechanism
Target: Support app.on(“error”) to listen for error events to handle exceptions
Let’s recall how exceptions are handled in Koa. The code might look something like this:
app.use(async (context, next) => {
console.log("middleware 2 start");
// throw new Error(" Error ");
await next();
console.log("middleware 2 end");
});
// KOA unified error handling: listen for error events
app.on("error", (error, context) => {
console.error(` request${context.url}An error has occurred);
});
Copy the code
As you can see from the code above, the core lies in:
- Koa instance APP needs to support event triggering and event listening capabilities
- We need to catch the asynchronous function exception and raise the error event
Let’s see how the code works:
// 1, inherit EventEmitter, increase the ability to trigger events and monitor
class Koa extends EventEmitter {
listen(port: number, cb: noop) {
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
const context = this.createContext(req, res);
// 2. Call fn to await an exception with a try catch
try {
await fn(context);
if (context.response && context.response.res) {
context.response.res.writeHead(200); context.response.res.end(context.body); }}catch (error) {
console.error("Server Error");
// 3, provide more information about context when raising error, aspect log, locate the problem
this.emit("error", error, context); }});returnserver.listen(port, cb); }}Copy the code
conclusion
So far we have TypeScript for the simplified VERSION of the Koa library
- Onion ring middleware mechanism
- Context encapsulates request and Response
- Asynchronous exception error handling mechanism
The complete Demo code can be found in KOA2-Reference
For more exciting articles, welcome to Star in our warehouse and pay attention to our Nuggets. We publish several high quality articles about the big front end every week.
The resources
- In-depth understanding of Koa2 middleware mechanisms
- Probably the most complete koA source code parsing guide to date