• Pre-Notify
  • Project directory
  • Express. Js and application. Js
  • HTTP server for app objects
  • Routing function of app object
    • Registered routing
    • Interface implementation
    • Distributed routing
    • Interface implementation
  • router
    • Test Case 1 and functional analysis
    • Function implementation
      • The router and the route
      • layer
      • Registered routing
      • Registration flowchart
      • Routing distribution
      • Distribution flow chart
    • Test Case 2 and functional analysis
    • Function implementation
  • Q
    • Why next recursive traversal and not for?
    • What can we learn from Express’s routing system design?
  • The source code

Pre-Notify

Take a look at my earlier article on a simpler express implementation before reading this article.

Express provides in-depth understanding and concise implementation

Compared to the previous release, this time we will implement all the core features of Express.

It is expected to be divided into: routing (upper and lower), middleware (upper and lower), fried chicken ~

(the better you guys will be, the better you guys will be!)

Project directory

iExpress/
|
|   
| - application.js  # app object
|
| - html.js         # Template engine
|
| - route/
|   | - index.js    # Router entry
|   | - route.js    # Route object
|   | - layer.js    # the router/the route layer
|
| - middle/
|   | - init.js     # Built-in middleware
|
| - test- case / | | - test case file 1 | | -... | · - express. Js# frame entry
Copy the code

Express. Js and application. Js

As we already know in the simpler Express implementation, introducing Express into a project returns a function that, when run, returns an App object. (This app object is a superset of native HTTP.)

The express.js module exports the function that returns the app object when run

let express = require('./express.js');
letapp = express(); // App objects are supersets of native HTTP objects... app.listen(8080); // This calls the native server.listenCopy the code

In the last version, because of the relatively simple functions, only one express.js file was used to complete the task, while in this version, we need to use a module application. Js to store the relevant parts of app

//express.js
const Application = require('./application.js'); //app

function createApplication() {returnnew Application(); //app object} module.exports = createApplication;Copy the code

HTTP server for app objects

One of the most important functions of the app object is to start an HTTP server. We can indirectly call the native.listen method to start a server through the app.listen method.

//application.js
function Application(){}
Application.prototype.listen = function() {function done() {}let server = http.createServer(function(req,res,done){
    	...
    })
    server.listen.apply(server,arguments);
}
Copy the code

Routing function of app object

Another important function of app objects, the Express framework, is to implement routing.

Routing is a shrimp?

The routing function enables the server to respond differently to different request paths and request methods of the client.

To do this we need to do two things: register routes and distribute routes

[Warning] In order to ensure the clarity of the app object as the interface layer, the app object only stores the interface, and the implementation is delegated to the router system (router.js).

Registered routing

When a request comes in, we can determine whether and how the server will respond based on its request mode and request path.

How do we let the server know which requests to respond to and how? That’s all you need to do to register a route.

When the server starts up, we need to record the requests that the server wants to respond to and store them so that the server can respond to those requests individually when they come in.

[warning] Notice that each record corresponds to a request. The record usually contains the request path and request mode of the request. But a request doesn’t necessarily correspond to just one record (middleware, all methods, etc.).

Interface implementation

We register routes by mounting.get,.POST and other methods on app objects.

The. Get method matches the get request, and the. Post method matches the POST request.

There are a total of 33 kinds of request methods, each corresponding to a method under the APP, EMMM… We can’t write it 33 times, can we? So we need to use a methods package to help us reduce the redundancy of the code.

const methods = require('methods'); // This package is a wrapper around HTTP.METHODS, the difference being that the native method names are all uppercase, whereas the latter are all lowercase. methods.forEach(method){ Application.prototype[method] =function(){this._router[method].apply(this._router,slice.call(arguments));returnthis; / / support app. The get (). The get (), post (), listen () ligatures}} / / the code above is the following abbreviated Application. The prototype, the get = fn Application. The prototype. Post = fn .Copy the code

[info] App. get is only an external interface. We delegate all tasks to the router class.

Distributed routing

When the request comes, we need to make the corresponding response according to the recorded routing information. This process is called routing /dispatch

The above is the generalized meaning of route distribution. However, route distribution actually includes two processes, matching routes and distributing routes.

  • Matching Routing When a request comes in, we need to know whether the request is included in the routing information we record. (If not included, typically the server will respond with a cue to the client.)
  • Route Distribution When a route is matched, the callback stored in the matched route information is executed.

Interface implementation

Application.prototype.listen = function() {let self = this;
    
    let server = http.createServer(function(req,res){
        function done(){// No callback res.end(' Cannot${req.method} ${req.url}`); } self._router.handle(req,res, self._router.handle)done); 
    })
    server.listen.apply(server,arguments);
}
Copy the code

router

Test Case 1 and functional analysis

const express = require('.. /lib/express');
const app = express();

app
  .get('/hello'.function(req,res,next){
    res.write('hello,');
    next(); 
  },function(req,res,next){
    res.write('world');
    next();
  })
  .get('/other'.function(req,res,next){
    console.log('Not here.');
    next();
  })
  .get('/hello'.function(req,res,next){
    res.end('! ');
  })
.listen(8080,function() {lettip = `server is running at 8080`; console.log(tip); }); < < < output the hello, world!Copy the code

Compared with the simple version of Express, express supports adding multiple CB packets to a route or adding CB packets to a route separately.

How is this done?

Most importantly, when we store routing information, we organize routes and methods into a two-dimensional data form similar to a two-dimensional array

There are layers of routes in the Router container, and layers of CallBCAK in each layer of route.

After matching a route, we can find the registered callbacks under this route.

Function implementation

The router and the route

Layers of routes are stored in the Router container, and layers of CallBCAK are stored in each layer of route.

First we need to have two constructors to produce the Router and route objects we need.

//router/index.js
function Router(){
    this.stack = [];
}
Copy the code
//router/route.js
function Route(path){
    this.path = path;
    this.stack = [];
    this.methods = {};
}
Copy the code

Next, we create a stack under each object produced by the Router and Route, which is used to hold layers. At this layer, the Router stores layers of routes (instances of routes), while the Route stores layers of methods.

And the objects in each stack would look something like this

//router.stack
[
    {
    	path
        handler
    }
    ,{
    	...
    }
]

//route.stack
[
    {
    	handler	
    }
    ,{
    	...
    }
]
Copy the code

As you can see, both stack objects contain handlers, and the first one also contains a path.

The first contains path, because router-stack traversals match routes, which requires a path comparison.

And both of them need to have a handler property why?

The second stack, Route, is designed to hold callbacks. The first stack is designed to hold callbacks.

When we get a route match, we need to iterate over the route, the route, which means we need a hook to do that when we get a route match, The hook that traverses the Route. stack is the handler that the first stack object holds (the route.dispatch method below).

layer

In the actual project, we encapsulated the objects stored in router.stack and Route. stack into the same object form — layer

On the one hand, it is for semantics, on the other hand, it is for the operation of the layer object (the original routes object and methods object) to be summarized under the Layer object for maintenance.

// router/layer.js
functionLayer(path,handler){ this.path = path; This.handler =handler; this.handler =handler; this.handler =handler; } // Check if the path matches layer.prototype. match =function(path){
    return this.path === path?true:false;
}
Copy the code

Registered routing

// Register route http.methods.foreach (METHOD){let method = METHOD.toLowercase();
    Router.prototype[method] = function(path){
    	letroute = this.route(path); Router.stack = route route[method].apply(route,slice.call(arguments,1)); // Store a layer of callbcak in route.stack}} router.prototype. route =function(path){
    let route = new Route(path);
    letlayer = new Layer(path,route.dispatch.bind(route)); Stack layer.route = route; stack layer.route = route; // To distinguish routing from middleware this.stack.push(layer);return route;
}
Copy the code
// Register callback in route http.methods.foreach (METHOD){let method = METHOD.toLowercase();
    Route.prototype[method] = function() {let handlers = slice.call(arguments);
        this.methods[method] = true; // For fast matchingfor(leti=0; i<handlers.length; ++i){let layer = new Layer('/',handler[i]); layer.method = method; // The callbacks in the route traversal filter this.stack.push(layer) based on the request method; }returnthis; // To support app.route(path).get().post()... }}Copy the code
Registration flowchart

Routing distribution

The whole route distribution is the process of traversing the two-dimensional data structure that we used router.stack and Route. stack.

The process of traversing router.stack is called routing matching and the process of traversing route.stack is called routing distribution.

Matching routes:

// router/index.js

Router.prototype.handle = function(req,res,done) {let self = this,i = 0,{pathname} = url.parse(req.url,true);
    functionNext (err){// Err is mainly used in error middleware, which will be covered in the next chapterif(i>=self.stack.length){
    	    return done;
        }
    	let layer = self.stack[i++];
        if(layer.match(pathName)){// Indicates that the path is successfully matchedif(layer.route){// Indicates a routeif(layer.route.handle_method){route.stack contains callBcak layer.handle_request(req,res,next); }else{ next(err); }}else{// Indicates middleware // Next chapter will skip next(err); }}else{
        	next(err);
        }
    }
    next();
}
Copy the code

Routing distribution

The layer.handle_request method is executed when we finally match the route

// layer.prototype. handle_request = in layer.jsfunction(req,res,next){
    this.handler(req,res,next);
}
Copy the code

This handler is route.dispatch (check the registered route section for those who forgot).

/ / the route. The route in the js. Prototype. Dispatch =function(req,res,out){// Note that out receives next() when route.stack is traversed.let self = this,i =0;
    
    function next(err){
    	if(err){// Indicates that the callback execution error skips the traversal of the current route.stack to be handled by the error middlewarereturn out(err);
        }
    	if(i>=self.stack.length){
    	    returnout(err); Route. stack is traversed. Route. stack is traversed to match the next route.let layer = self.stack[i++];
        if(layer.method === req.method){
    	    self.handle_request();
        }else{
    	    next(err);
        }
    }
    next();
}
Copy the code
Distribution flow chart

Test Case 2 and functional analysis

const express = require('express');
const app = express();

app
  .route('/user')
  .get(function(req,res){
    res.end('get');
  })
  .post(function(req,res){
    res.end('post');
  })
  .put(function(req,res){
    res.end('put');
  })
  .delete(function(req,res){
    res.end('delete');
  })
.listen(3000);
Copy the code

The above is a Resful style excuse, which is actually quite simple to implement if we clear up the above.

Call the.route() method to return our route(route.stack), and then call the.get method to add a different callBcak to the route.

[warning].listen cannot be used with other method names because.get, etc., returns route instead of app

Function implementation

/ / application. The application in the js. Prototype. The route =function(path){
    this.lazyrouter();
    let route = this._router.route(path);
    return route;
}
Copy the code

Also note that route.prototype[method] needs to return route for continuous calls.

So easy~

Q

Why next recursive traversal and not for?

emmm… I would like to say that express source code is designed in this way, well, is that a good answer? ლ (‘ ◉ ❥ ◉ ` ლ)

You can actually use “for”, I’ve tried that,

Modify handle on router/index.js as follows

 let self = this
    ,{pathname} = url.parse(req.url,true);

  for(leti=0; i<self.stack.length; ++i){if(i>=self.stack.length){
      return done(a); }let layer = self.stack[i];
    if(layer.match(pathname)){
      if(! layer.route){ }else{
    
        if(layer.route&&layer.route.handle_method(req.method)){
          // let flag = layer.handle_request(req,res);
    
          for(letj=0; j<layer.route.stack.length; ++j){let handleLayer = layer.route.stack[j];
            if(handleLayer.method === req.method.toLowerCase()){
              handleLayer.handle_request(req,res);
              if(handleLayer.stop){
                return; }}}// go through handleLayer}// quick match successful}// description is a route}// match path}Copy the code

We no longer need to pass next and next arguments when we call methods like.get

app
  .get('/hello'.function(req,res){
    res.write('hello,');
    // this.stop = true;
    this.error = true; // To the error-handling middleware. // next(); // next(); },function(req,res,next){
    res.write('world');
    this.stop = true; // See here!!!!!!!!!!!! Layer traversal will end here // next(); }) .get('/other'.function(req,res){
    console.log('Not here.');
    // next();
  })
  .get('/hello'.function(req,res){
    res.end('! '); }).listen(8080,function() {let tip = `server is running at 8080`;
  console.log(tip);
});
Copy the code

In the above code this.stop=true is equivalent to not calling next(), and not mounting this.stop on the callback is equivalent to calling next().

Route. stack (route.stack) {route.stack (route.stack) {route.stack (route.stack) {route.stack (route.stack) {route.stack (route.stack) {route.stack (route.stack); If set, stop traversal, either for the routing layer(route.stack) or for the callbacks layer(route.stack).

So the question is, is there any reason to iterate with Next?

The answer is: For doesn’t support asynchrony, next does!

Support for asynchrony means that when a callBCAk is executed and its asynchronous results are needed for the next callBCAK to be executed.

B: well… For can’t do this. It doesn’t know whether asynchronous functions are called in the functions it executes, and it doesn’t know which of those asynchronous functions can finish executing.

What can we learn from Express’s routing system design?

emmm… Layer (route or callback) encapsulates the hierarchical operations on each layer and mounts them to this object. Reviewed the original intention of the birth of the class ~

Of course, the next hook recursive traversal is also possible, we know its application scenarios, support asynchronous ~

emmm… What to learn… Not only do we imitate writing a framework, but more importantly, um.. To think! To think! Students, what have learned, to learn to apply… B: well… A: hey!

So WHAT did I learn from this article in the middle of the night? emmm…

The world is so big

The source code

Warehouse address: click to obtain the source code


To be continue…