Author’s blog link: www.yingpengsha.com/vite-yuan-l…

An overview of the

Connect is an extensible HTTP service framework for NodeJS that extends functionality using middleware

Vite and Webpack-dev-Middleware both use Connect to build development servers, and Express’s middleware pattern borrows from Connect.

This article came about when I read the source code of Vite and found that Vite uses Connect as the core to build the development server. This article is divided into two parts. The main part is about the source code of Connect, and the rest of the article is about middleware design and how Vite uses Connect.

If there are omissions, please do not hesitate to correct them!

The basic use

const connect = require('connect');
const http = require('http');

const app = connect();

// Basic middleware usage
app.use(function middleware1(req, res, next) {
  next();
});

// URL matching middleware
app.use('/foo'.function fooMiddleware(req, res, next) {
  // req.url starts with "/foo"
  next();
});

// Something passed in next() is treated as an error and passed on to the middleware that handles the error
app.use(function (req, res, next) {
  next(new Error('boom! '));
});

// Error handling middleware
// Middleware with four input parameters is considered to handle errors
app.use(function onerror(err, req, res, next) {
  // an error occurred!
});

// The service starts
app.listen(2000)
// Or pass the middleware to HTTP to create the service
http.createServer(app).listen(3000);
Copy the code

The source code to read

Initialize the

createServer

Create a Connect instance that returns an app function.

Connect in const app = connect() is createServer

function createServer() {
  // Declare the instance app
  // To call app is to call the static function handle on app
  function app(req, res, next){ app.handle(req, res, next); }
  // Mount static properties from proto to app
  merge(app, proto);
  // Attach static properties from EventEmitter to app
  merge(app, EventEmitter.prototype);
  // Set the default route to '/'
  app.route = '/';
  // Initializes the stack of default middleware
  app.stack = [];
  return app;
}
Copy the code

Examples of function

Stores static properties/methods on the instance

use

Add middleware to app.Stack

function use(route, fn) {
  var handle = fn;
  var path = route;

  // Function overload
  // If the first argument passed in is not a string, the middleware is treated as a function-only middleware and the processing URL for that middleware is set to '/'
  if (typeofroute ! = ='string') {
    handle = route;
    path = '/';
  }

  // Function overload
  // If the middleware function passed in has a static function handle, it is assumed that another connect instance was passed in
  if (typeof handle.handle === 'function') {
    var server = handle;
    server.route = path;
    handle = function (req, res, next) {
      server.handle(req, res, next);
    };
  }

  // Function overload
  // Take the first parameter in createServer, the function that handles the request, as a handler
  if (handle instanceof http.Server) {
    handle = handle.listeners('request') [0];
  }

  // Format the path
  // Delete '/' if path ends in '/'
  if (path[path.length - 1= = ='/') {
    path = path.slice(0, -1);
  }

  // Displays debugging information
  debug('use %s %s', path || '/', handle.name || 'anonymous');
  / / into the stack
  this.stack.push({ route: path, handle: handle });

  return this;
};
Copy the code

handle

The closure stores some variables. The core is to recursively call the next function, which calls the middleware to handle the request

function handle(req, res, out) {
  // Middleware subscript
  var index = 0;
  // Protocol of the requested URL plus host content, eg: https://www.example.com
  var protohost = getProtohost(req.url) || ' ';
  // Delete route prefixes. For example, if some middleware needs to match routes, the corresponding route prefixes will be deleted and temporarily exist here, while the suffixes will be handed over to the corresponding middleware for processing (can be skipped first, explained later).
  var removed = ' ';
  // Whether the url is appended with a leading '/'
  var slashAdded = false;
  // Middleware collection
  var stack = this.stack;

  // The last function to be called when the middleware is finished
  // If the upper application has a terminating function passed down, use its function (such as next, which is passed down from connect)
  // Instead use the closing function generated by finalHandler
  var done = out || finalhandler(req, res, {
    env: env,
    onerror: logerror
  });

  // Store its original URL
  req.originalUrl = req.originalUrl || req.url;

  // Define the next function
  function next(err) {
    // If the URL was prefixed with '/' in the previous middleware, restore and reset
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // If the URL is prefix matched on the previous middleware, restore and reset it
    if(removed.length ! = =0) {
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = ' ';
    }

    // Get the current middleware
    var layer = stack[index++];

    // If there is no middleware left, call the terminating function asynchronously, end.
    if(! layer) { defer(done, err);return;
    }

    // Get the pathname of the current requested URL through parseUrl
    var path = parseUrl(req).pathname || '/';
    // Route to the middleware
    var route = layer.route;

    // If the requested URL does not match the middleware route, skip it
    if (path.toLowerCase().substr(0, route.length) ! == route.toLowerCase()) {return next(err);
    }

    // If the path prefix matches, but the suffix indicates a misunderstanding, the path is still considered mismatched and skipped
    // eg: /foo does not match /fooo
    var c = path.length > route.length && path[route.length];
    if(c && c ! = ='/'&& c ! = ='. ') {
      return next(err);
    }

    // Passes the matched route to the middleware, whose route is /foo. The requested URL is /foo/bar, and /bar is passed to the middleware
    if(route.length ! = =0&& route ! = ='/') {
      // Route prefix to be deleted
      removed = route;
      // Delete the URL after the route prefix of the middleware, the next middleware will restore it
      req.url = protohost + req.url.substr(protohost.length + removed.length);

      // If there is no protocol-specific domain name prefix and the URL does not start with '/', it is automatically supplemented and marked, which will be restored in the next middleware process
      if(! protohost && req.url[0]! = ='/') {
        req.url = '/' + req.url;
        slashAdded = true; }}// Call the middleware
    call(layer.handle, route, err, req, res, next);
  }

  / / start next
  next();
};
Copy the code

listen

Start the service using http.createserver

proto.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
Copy the code

Tool function

defer

Asynchronously calling a function

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments))}Copy the code

call

Call middleware functions

// handle: the handler function of the current middleware
// route: indicates the current route. The default value is '/'. Otherwise, it indicates the route matched by the current middleware
// err: indicates possible error information
/ / the req: request
/ / res: response
// next: executes the next middleware
function call(handle, route, err, req, res, next) {
  // The number of entries in the middleware
  var arity = handle.length;
  var error = err;
  // Whether an error was thrown
  var hasError = Boolean(err);

  // Initial debugging information
  debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);

  try {
    if (hasError && arity === 4) {
    	// If an error is thrown and the current middleware is the middleware to handle the error, the call ends.
      handle(err, req, res, next);
      return;
    } else if(! hasError && arity <4) {
      // If there are no errors and the current middleware is not used to handle errors, the call ends.
      handle(req, res, next);
      return; }}catch (e) {
    // Catch the error and overwrite a possible error
    error = e;
  }

  / / an error | | (incorrect && current middleware is not middleware functions of handling errors, and execution of a middleware
  next(error);
}
Copy the code

logerror

Output the wrong function

function logerror(err) {
  // env = process.env.NODE_ENV || 'development'
  if(env ! = ='test') console.error(err.stack || err.toString());
}
Copy the code

getProtohost

Access url agreement and domain name eg: http://www.example.com/foo = > http://www.example.com

function getProtohost(url) {
  if (url.length === 0 || url[0= = ='/') {
    return undefined;
  }

  var fqdnIndex = url.indexOf(': / /')

  returnfqdnIndex ! = = -1 && url.lastIndexOf('? ', fqdnIndex) === -1
    ? url.substr(0, url.indexOf('/'.3 + fqdnIndex))
    : undefined;
}
Copy the code

Analysis of middleware processing mechanism

The sample

const connect = require('connect')
const app = connect()

app.use(function m1(req, res, next) {
  console.log('m1 start ->');
  next();
  console.log('m1 end <-');
});

app.use(function m2(req, res, next) {
  console.log('m2 start ->');
  next();
  console.log('m2 end <-');
});

app.use(function m3(req, res, next) {
  console.log('m3 service... ');
});

app.listen(4000)
Copy the code

Run the process

According to the previous code analysis, the running flow of the three middleware can be roughly described as follows:Does that look familiar?

The onion model?

Is connect’s middleware an Onion model? Yes and no.

Connect can implement the onion model when all the logic is synchronous

However, if a link is asynchronous, and you want the logic to temporarily block until the asynchrony ends, connect can only be linear

The core reason is that next() calls the middleware synchronously, even though asynchronous control is carried out inside the real-time middleware. In fact, this is also the difference between Express and Koa middleware. Due to the non-negligible nature of asynchronous events, the former middleware can only be delivered linearly. The middleware of the latter implements logic processing in the manner of the Onion model.

It is also worth acknowledging that Koa’s middleware pattern is better than Express’s

Koa middleware source analysis, you can see the author of this article “Koa middleware complete process + source analysis”

Vite and connect

In the early days of 1.x and 2.x, Vite actually used Koa to implement middleware patterns. Why Koa was migrated to Connect can be seen in the last paragraph of Migration from V1.

Since most of the logic should be done via plugin hooks instead of middlewares, the need for middlewares is greatly reduced. The internal server app is now a good old connect instance instead of Koa.

As the logic of Vite moves from middleware to plug-in hook functions, vite becomes less dependent on middleware patterns in favor of the more appropriate Connect.

How do YOU use Connect in Vite

I’ve simplified the code that vite uses to create the development server, leaving out irrelevant details (such as the middlewareMode judgment) and just letting it go

export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
	// ...
  
  // Create a Node middleware with connect
  const middlewares = connect() as Connect.Server
  
  // An HTTP/HTTPS server is created based on the config.server configuration and the request is handled by Middlewares, similar to the last usage in the basic usage introduction
  const httpServer = await resolveHttpServer(serverConfig, middlewares)
  
  // ...
  
  // debug uses the timestamp logging middleware
  if (process.env.DEBUG) {
    middlewares.use(timeMiddleware(root))
  }
  
  // CORS middleware
  / / the corresponding configuration: https://vitejs.bootcss.com/config/#server-cors
  / / the corresponding library: https://www.npmjs.com/package/cors
  const { cors } = serverConfig
  if(cors ! = =false) {
    middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
  }
  
  // proxy
  // Middleware that handles proxy configuration
  / / the corresponding configuration: https://vitejs.bootcss.com/config/#server-proxy
  const { proxy } = serverConfig
  if (proxy) {
    middlewares.use(proxyMiddleware(httpServer, config))
  }
  
  // The middleware that handles base urls
  / / the corresponding configuration: https://vitejs.bootcss.com/config/#base
  if(config.base ! = ='/') {
    middlewares.use(baseMiddleware(server))
  }

  // Open the editor's middleware
  / / the corresponding library: https://github.com/yyx990803/launch-editor#readme
  middlewares.use('/__open-in-editor', launchEditorMiddleware())

  // Reconnect the middleware
  middlewares.use('/__vite_ping'.(_, res) = > res.end('pong'))

  // Escape url middleware
  middlewares.use(decodeURIMiddleware())
  
  // Middleware to process files under public
  middlewares.use(servePublicMiddleware(config.publicDir))
  
  // main transform middleware
  / /! Core content transformation middleware
  middlewares.use(transformMiddleware(server))
  
  // File processing middleware
  middlewares.use(serveRawFsMiddleware())
  middlewares.use(serveStaticMiddleware(root, config))
  
	// Middleware to process index.html
  middlewares.use(indexHtmlMiddleware(server))
  
  // All the above are not working properly when 404
  middlewares.use((_, res) = > {
    res.statusCode = 404
    res.end()
  })
  
  // error handler
  // Error handling middleware
  middlewares.use(errorMiddleware(server, middlewareMode))
  
  // ...
}
Copy the code

When we use Vite, it is the above middleware that processes our modules and returns them to the browser for processing.

reference

  • “Koa middleware complete process + source code Analysis”