1 Native implementation
1.1 Starting a Service
Node up a service how easy, believe that as long as the Internet, can search similar to the following code quickly start a service.
const http = require('http')
const handler = ((req, res) = > {
res.end('Hello World! ')
})
http
.createServer(handler)
.listen(
8888.() = > {
console.log('8888' listening 127.0.0.1.)})Copy the code
Visit 127.0.0.1:8888 and you’ll see the ‘Hello World! . And then you’ll see that if you change the route, if you change the request, you’ll only get this string.
Curl 127.0.0.1:8888 curl curl -X POST http://127.0.0.1:8888 curl 127.0.0.1:8888/aboutCopy the code
At this point, you’ll go to the documentation and discover that there’s something wrong with the reQ you just called back. We can use the Method and URL attributes to return different results for different methods and routes. It’s easy to think of something like this:
const http = require('http')
const handler = ((req, res) = > {
let resData = '404 NOT FOUND! '
const { method, path } = req
switch (path) {
case '/':
if (method === 'get') {
resData = 'Hello World! '
} else if (method === 'post') {
resData = 'Post Method! '
}
break
case '/about':
resData = 'Hello About! '
}
res.end = resData
})
http
.createServer(handler)
.listen(
8888.() = > {
console.log('8888' listening 127.0.0.1.)})Copy the code
But a service can’t have only a few interfaces and methods. You can’t add a branch every time you add one, so it’s easy to decouple the path and method from the handler.
1.2 Policy mode decoupling
How do you decouple it? From the code in the newbie village, we can see that the policy pattern can be used to solve this problem:
const http = require('http')
class Application {
constructor () {
// Collect callbacks for route and method
this.$handlers = new Map()}/ / register handler
register (method, path, handler) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map(a)this.$handlers.set(path, pathInfo)
}
// Register the callback function
pathInfo.set(method, handler)
}
use () {
return (request, response) = > {
const { url: path, method } = request
this.$handlers.has(path) && this.$handlers.get(path).has(method)
? this.$handlers.get(path).get(method)(request, response)
: response.end('404 NOT FOUND! ')}}}const app = new Application()
app.register('GET'.'/'.(req, res) = > {
res.end('Hello World! ')
})
app.register('GET'.'/about'.(req, res) = > {
res.end('Hello About! ')
})
app.register('POST'.'/'.(req, res) = > {
res.end('Post Method! ')
})
http
.createServer(app.use())
.listen(
8888.() = > {
console.log('8888' listening 127.0.0.1.)})Copy the code
1.3 Comply with the DRY principle
But at this point you’ll find:
- If you shake your hand
method
The method is written in lower case becauseHttp.Request.method
It’s all uppercase, it doesn’t match the right onehandler
And return to'404 NOT FOUND'
. - If I want to add some action before the response data, such as adding a timestamp for each request to indicate the time of the request, I have to modify each
register
In thehandler
Function that does not comply with the DRY principle
At this point, modify the code above to implement sequential execution of the handler using Promise.
const http = require('http')
class Application {
constructor() {
// Collect callbacks for route and method
this.$handlers = new Map(a)// Expose get and POST methods
this.get = this.register.bind(this.'GET')
this.post = this.register.bind(this.'POST')}/ / register handler
register(method, path, ... handlers) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map(a)this.$handlers.set(path, pathInfo)
}
// Register the callback function
pathInfo.set(method, handlers)
}
use() {
return (request, response) = > {
const { url: path, method } = request
if (
this.$handlers.has(path) &&
this.$handlers.get(path).has(method)
) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) = > {
return pre.then(() = > {
return new Promise((resolve, reject) = > {
_handler.call({}, request, response, () = > {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end('404 NOT FOUND! ')}}}}const app = new Application()
const addTimestamp = (req, res, next) = > {
setTimeout(() = > {
this.timestamp = Date.now()
next()
}, 3000)
}
app.get('/', addTimestamp, (req, res) = > {
res.end('Hello World! ' + this.timestamp)
})
app.get('/about', addTimestamp, (req, res) = > {
res.end('Hello About! ' + this.timestamp)
})
app.post('/', addTimestamp, (req, res) = > {
res.end('Post Method! ' + this.timestamp)
})
http
.createServer(app.use())
.listen(
8888.() = > {
console.log('8888' listening 127.0.0.1.)})Copy the code
1.4 Lower the user’s mind
But there are still a few minor flaws, users are always creating promises, users may want to be more brainless, so we expose the user to a next method, no matter where we execute next it will go to the next handler, isn’t that beautiful!!
class Application {
// ...
use() {
return (request, response) = > {
const { url: path, method } = request
if (
this.$handlers.has(path) &&
this.$handlers.get(path).has(method)
) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) = > {
return pre.then(() = > {
return new Promise(resolve= > {
// Expose the next method and let the user decide when to go to the next handler
_handler.call({}, request, response, () = > {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end('404 NOT FOUND! ')}}}}// ...
const addTimestamp = (req, res, next) = > {
setTimeout(() = > {
this.timestamp = new Date()
next()
}, 3000)}Copy the code
2 Koa core source code analysis
Along the way, the above code has basically implemented a simple middleware framework that allows users to customize the middleware and then go to the next handler in the business logic through Next (), making it clearer to integrate the business process. But it can only promote the execution of middleware, there is no way to jump out of the middleware to give priority to other middleware. For example, in KOA, a middleware would look something like this:
const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
const middleware2 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
const middleware3 = async (ctx, next) => {
console.log(5);
await next();
console.log(6);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
ctx.body = 'hello world'
})
app.listen(8888)
Copy the code
You can see that the console output is in the order of 1, 3, 5, 6, 4, 2, which is the classic KOA Onion model.
As we go through koA’s source code step by step, we can see that there are only four files in total, which adds up to just over 1000 lines of code if the comments are removed.
file | function |
---|---|
applicaiton.js | An entry to the KOA program that manages and invokes the middleware, handles callbacks to http.createserver, and proxies the requested request and response to the context |
request.js | A wrapper around the Request in the http.createserver callback, various getters, setters, and additional properties |
response.js | A wrapper around the response in the http.createserver callback, various getters, setters, and additional properties |
context.js | Proxy request and Response, and expose some functionality externally |
When creating a Koa instance, Koa doesn’t really do much, setting up some of the instance’s configuration, initializing the middleware queues, and inheriting context, Request, and Response with Object.create.
2.1 the constructor
constructor(options) {
super(a);// The various configurations of the instance, do not worry too much
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
// The most important instance attribute is used to store the middle
this.middleware = [];
// Inherits objects from the other three files
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
Copy the code
Because Koa is only used for middleware integration and listening for request responses, the two instance methods of Koa that we are most interested in are use and Listen. One is used to register middleware and one is used to start services and listen on ports.
2.2 the use
The functionality is as simple as registering middleware and pushing it into the instance properties Middleware list.
use(fn) {
if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
// Use the co library to convert the generator function, v3 will remove it and use promise and async directly... await
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || The '-');
this.middleware.push(fn);
// Middleware app.use(XXX).use(XXX)...
return this;
}
Copy the code
2.3 listen
It is very simple to implement, just call http.createserver directly to create the service and perform some operations of Server.listen directly. A slightly special one is that the createServer passes in arguments that are returned by calling the instance method callback.
listen(. args) {
debug('listen');
// Create a service
const server = http.createServer(this.callback());
// To pass through the parameters, execute the HTTP module server.listen
returnserver.listen(... args); }Copy the code
2.4 the callback
- call
compose
Method to convert all middleware toPromise
Executes, and returns an execution function. - Call the parent class
Emitter
In thelistenerCount
Method to determine whether a user is registerederror
Listener for the event, if noneerror
Event registrationonerror
Methods. - Define the incoming
createServer
This handler has two input arguments, respectivelyrequest
和response
, by callingcreateContext
Methods therequest
和response
Encapsulated intoctx
Object, and then putctx
And the execution function of the first stepfn
The incominghandleRequest
Methods.
callback() {
Koa-compose, the core of the Onion model, will be explained later on when to implement the transition middleware.
const fn = compose(this.middleware);
// From Emitter, if there are no listeners for error events, register the default event listener method onError for error events
if (!this.listenerCount('error')) this.on('error'.this.onerror);
//
const handleRequest = (req, res) = > {
// Call createContext to encapsulate req and RES as CTX objects
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
Copy the code
2.5 createContext
CreateContext encapsulates objects exposed in context, Request, and Response files and adds app, REq, and RES to facilitate CTX access.
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
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;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
Copy the code
2.6 handleRequest
- Get res, default the status to 404
- Define failed callback functions and middleware executes successful callback functions, where the failed callback function is called
context
In theonerror
Function, but eventually trigger app registrationonerror
Functions; Successful callback function callrespond
Method, readctx
Information, write data tores
And respond to the request. - use
on-finished
The module ensures that the corresponding callback function is executed when a stream is closed, completed, and reported as an error. - Executing middleware functions
fnMiddleware
, similar to thePromise.all
When all the middleware processes are successful, the command is executedhandleResponse
Otherwise, an exception is caught.
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err= > ctx.onerror(err);
const handleResponse = () = > respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code
3 Koa-compose
Koa-compose source is very brief:
- First check the validity of the input parameter, and finally return a function.
- This function is used internally
index
Logs the current execution as an identity in the middle and returns the execution from the first middlewaredispatch
Results. If a middleware executes internally multiple timesnext()
Method, the value of I is equal toindex
, an error will be reportedreject
It off. - According to the
index
Takes the middleware from the middleware list and willcontext
和dispatch(i + 1)
Middleware entry parameterctx
和next
Incoming when the middleware executesnext()
Method, the next middleware will be executed in order, and the current middleware will be placed on the execution stack. Finally, when I is equal to the length of the middleware array, i.e. there is no other middleware, the parameter will be enterednext
(undefined in Koa source) to fn, which is undefined and returns nullresolved
The state of thepromise
. - When the core middleware execution is complete, it will automatically trigger
await
Work down, start executing the last middleware, and you end up with an onion model that starts from the outside in and then the inside out.
// The incoming parameter is a list of middleware and the return value is a function
function compose (middleware) {
// Check the validity of the middle
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}/ / core
return function (context, next) {
// Set the initial index value
let index = -1
// Execute dispatch immediately, passing in 0, and return the result
return dispatch(0)
function dispatch (i) {
// Prevent multiple calls to next in the same middleware
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// Take the ith middleware from the middleware list and assign it to fn
let fn = middleware[i]
// Next is always undefined for Koa source code.
if (i === middleware.length) fn = next
// There is no executable middleware
if(! fn)return Promise.resolve()
try {
All implements Promise recursively by exposing the next callback function to ensure that the order of execution of the middleware meets the characteristics of the stack
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
Copy the code
4 Koa-router
Get (‘/:userName’, (res, req) => {/* XXXX */}). At this point you can introduce the KOA-Router middleware and use it as follows.
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/'.async ctx => {
ctx.body = 'Hello World! '
})
router.get('/:userName'.async ctx => {
ctx.body = `Hello ${ctx.params.userName}! `
})
app
.use(router.routes())
.use(router.allowedMethods())
.listen(8888)
Copy the code
The koa-router source code is placed in the lib folder, just two files:
file | function |
---|---|
layer.js | Internally, various regular expressions are used to obtain the corresponding data from the input parameters, storing the requested route, method, corresponding regular match of the route, parameters in the route, and middleware corresponding to the route |
router.js | The specific implementation of the Router provides exposed registration methods such as GET and POST, and middleware for processing routes |
// Register routes and bind middleware
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
const router = this;
const stack = this.stack;
// Support multiple PAth-bound middleware
if (Array.isArray(path)) {
for (let i = 0; i < path.length; i++) {
const curPath = path[i];
router.register.call(router, curPath, methods, middleware, opts);
}
return this;
}
// Create a route
const route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true.name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false.strict: opts.strict || this.opts.strict || false.prefix: opts.prefix || this.opts.prefix || "".ignoreCaptures: opts.ignoreCaptures
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
// Add middleware parameters
for (let i = 0; i < Object.keys(this.params).length; i++) {
const param = Object.keys(this.params)[i];
route.param(param, this.params[param]);
}
stack.push(route);
debug('defined route %s %s', route.methods, route.path);
return route;
};
// Expose methods such as get and POST
for (let i = 0; i < methods.length; i++) {
function setMethodVerb(method) {
Router.prototype[method] = function(name, path, middleware) {
if (typeof path === "string" || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments.2);
} else {
middleware = Array.prototype.slice.call(arguments.1);
path = name;
name = null;
}
this.register(path, [method], middleware, {
name: name
});
return this;
};
}
setMethodVerb(methods[i]);
}
Copy the code
5 Related Documents
- koa onion model
- En.wikipedia.org/wiki/Strate…
- Robdodson. Me/posts/javas…
❤️ Thank you
That is all the content of this sharing. I hope it will help you
Don’t forget to share, like and bookmark your favorite things.