[Vite principle] connect source code interpretation

[Vite principle] connect source code interpretation

The original link of the author's blog: www.yingpengsha.com/vite-yuan-l...

Overview

connect is an extensible HTTP service framework of NodeJS, which can use middleware to extend functions

Both vite and webpack-dev-middleware use connect to build development servers, and Express 's middleware model is also borrowed from connect.

When I read the source code of Vite, I found that Vite uses connect as the core to build a development server, so I have this article. The article is divided into two parts. The main part is an explanation of the source code of connect, and the latter part will briefly explain the design of middleware and how to use connect in vite.

If there are any omissions or improprieties, please feel free to be correct!

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(); }); //The content passed in next() is regarded as an error, and then it will be passed to the middleware that handles the error app.use( function ( req, res, next ) { next( new Error ( 'boom!' )); }); //Error handling middleware //There are four input parameters as middleware for handling errors app.use( function onerror ( err, req, res, next ) { //an error occurred! }); //Service start app.listen( 2000 ) //Or pass the middleware to http to create a service http.createServer(app).listen( 3000 ); copy the code

Source code reading

initialization

createServer

Create a connect instance and return an app function.

const app = connect()
middle
connect
Is
createServer

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

Instance 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 overloading //If the first parameter passed in is not a string, it will be treated as a middleware that only processes the function, and the processing url of the middleware is set to'/' if ( typeof route !== 'string' ) { handle = route; path = '/' ; } //Function overloading //If the passed middleware function has a static function handle, it is directly regarded as another connect instance passed in if ( typeof handle.handle === 'function' ) { var server = handle; server.route = path; handle = function ( req, res, next ) { server.handle(req, res, next); }; } //Function overloading //Directly use the first parameter in createServer, which is the function for processing the request, as the processing function if (handle instanceof http.Server) { handle = handle.listeners( 'request' )[ 0 ]; } //Path formatting //If the path ends with a'/', delete the'/' if (path[path.length- 1 ] === '/' ) { path = path.slice( 0 , -1 ); } //Output debug information debug( 'use %s %s' , path || '/' , handle.name || 'anonymous' ); // Push this .stack.push({ route : path, handle : handle }); return this ; }; Copy code

handle

The function that processes the request, the closure stores some variables, and the core is recursive call

next
Function, by
next
Call the middleware to process the request

function handle ( req, res, out ) { //Middleware subscript var index = 0 ; //The protocol of the requested url plus the host content, eg: https://www.example.com var protohost = getProtohost(req. url) || '' ; //Delete the routing prefix. For example, if some middleware needs routing to match, the corresponding prefix will be deleted and temporarily stored here, and the suffix will be handed over to the corresponding middleware for processing (you can skip Yes , there will be an explanation later) var removed = '' ; //Did you add a leading'/' to the url (you can skip it first, there will be an explanation later) var slashAdded = false ; //Middleware collection var stack = this .stack; //The finishing function called after the middleware is all finished //If the upper application has a finishing function passed down, use its function (for example, the next passed down from the upper layer connect) //Otherwise, use the finishing 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 is prefixed with'/' in the previous middleware, then restore and reset if (slashAdded) { req.url = req.url.substr( 1 ); slashAdded = false ; } //If the last middleware matches the URL with prefix, then restore and reset 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, call the closing function asynchronously and end. if (!layer) { defer(done, err); return ; } //Get the pathname of the current request URL through parseUrl var path = parseUrl(req).pathname || '/' ; //The route corresponding to the middleware var route = layer.route; //If the URL of the current request does not match the route of the middleware, skip directly and end if (path.toLowerCase().substr( 0 , route.length) !== route.toLowerCase()) { return next(err ); } //If the path prefix matches, but the suffix indicates that it is actually a misunderstanding, it is still regarded as a mismatch and skipped, ending //eg:/foo and/fooo do not match var c = path.length> route.length && path[route.length]; if (c && c !== '/' && c !== '.' ) { return next(err); } //Pass the matched route to the middleware, the middleware route is/foo, the requested URL is/foo/bar, then/bar is given to the middleware if (route.length !== 0 && route !== '/' ) { //The routing prefix to be deleted removed = route; //The URL after the route prefix corresponding to the middleware is deleted, and the next middleware will be restored req.url = protohost + req.url.substr(protohost.length + removed.length); //If there is no specific protocol domain name prefix, and the url does not start with a'/', it will be automatically added and marked, and the next middleware will be restored if (!protohost && req.url[ 0 ] !== '/' ) { req.url = '/' + req.url; slashAdded = true ; } } //call middleware call(layer.handle, route, err, req, res, next); } //start next next(); }; Copy code

listen

Use http.createServer to start the service

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

Utility function

defer

Call function asynchronously

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

call

Call middleware function

//handle: the processing function of the current middleware //route: the current route, the default is'/', otherwise it should be the route matched by the current middleware //err: possible error message //req: request //res: response //next: execute the next middleware function call ( handle, route, err, req, res, next ) { //the input parameters of the middleware var arity = handle.length; var error = err; //Is there an error throw var hasError = Boolean (err); //Initial debug 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 that handles errors, call it and end. handle(err, req, res, next); return ; } else if (!hasError && arity < 4 ) { //If there is no error, and the current middleware is a middleware for non-error handling, call it and end. handle(req, res, next); return ; } } catch (e) { //Catch the error and cover the last possible error error = e; } //An error occurred || (There is an error && The current middleware is not a middleware function that handles errors), execute the next middleware next(error); } Copy code

logerror

Functions that output errors

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

getProtohost

Get the url protocol plus 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( '://' ) return fqdnIndex !==- 1 && url.lastIndexOf( '?' , fqdnIndex) ===- 1 ? url.substr( 0 , url.indexOf( '/' , 3 + fqdnIndex)) : undefined ; } Copy code

Analysis of Middleware Processing Mechanism

Example

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 code

Run process

According to the previous code analysis, the operation process of the three middleware can be described as follows: Is it a bit familiar?

Onion model?

Is the middleware of connect an onion model? It can be said that it is, it can be said that it is not.

All in all synchronous logic when, connect the model can achieve the effect of onion

But if there is a link is asynchronous , and you want the logic behind temporarily blocked, waiting for the end of the asynchronous operation, it can only connect linear

The core reason is that next() calls the middleware synchronously , and even if the internal middleware is asynchronously controlled, it will not help. In fact, it is also the difference between Express and the Koa middleware mode. Due to the non-ignorable nature of asynchronous events, the former middleware often It can only be passed linearly, while the latter middleware can implement logical processing in the way of the onion model.

At the same time, I have to admit that Koa's middleware model is indeed better than Express's middleware model.

For the source code analysis of Koa middleware, please refer to the author's article "Koa's Middleware Complete Process + Source Code Analysis"

vite and connect

In the early days of 1.x and 2.x, vite actually used Koa to implement the middleware model. Why did you migrate from Koa to connect? You can understand the reason from 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.

Since the logic processing of vite is gradually inclined to use the hook function of the plug-in from the original middleware processing, the dependence of vite on the middleware mode is gradually reduced, and a more suitable connect is adopted.

How to use connect in vite

The author simplifies the code for creating the development server for Vite , and ignores irrelevant details (such as the judgment of the middlewareMode mode), as long as you know it.

export async function createServer ( inlineConfig: InlineConfig = {} ): Promise < ViteDevServer > { //... //Create a node middleware through connect const middlewares = connect() as Connect.Server //The http/https server will be created according to the configuration of config.server, and the request will be handled by middlewares, similar to the last usage in the basic usage introduction before const httpServer = await resolveHttpServer(serverConfig, middlewares) //... //Time stamp log middleware for debug if (process.env.DEBUG) { middlewares.use(timeMiddleware(root)) } //cors middleware //Corresponding configuration: https://vitejs.bootcss.com/config/#server-cors //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 //Corresponding configuration: https://vitejs.bootcss.com/config/#server-proxy const {proxy} = serverConfig if (proxy) { middlewares.use(proxyMiddleware(httpServer, config)) } //Middleware for processing base url //Corresponding configuration: https://vitejs.bootcss.com/config/#base if (config.base !== '/' ) { middlewares.use(baseMiddleware(server)) } //Open the middleware of the editor //Corresponding library: https://github.com/yyx990803/launch-editor#readme middlewares.use( '/__open-in-editor' , launchEditorMiddleware()) //Reconnect middleware middlewares.use( '/__vite_ping' , ( _, res ) => res.end( 'pong' )) //Escape url middleware middlewares.use(decodeURIMiddleware()) //Middleware for processing files under public middlewares.use(servePublicMiddleware(config.publicDir)) //main transform middleware //! Core content conversion middleware middlewares.use(transformMiddleware(server)) //file processing middleware middlewares.use(serveRawFsMiddleware()) middlewares.use(serveStaticMiddleware(root, config)) //Middleware for processing index.html middlewares.use(indexHtmlMiddleware(server)) //404 middlewares.use( ( _, res ) => { res.statusCode = 404 res.end() }) //error handler //error handling middleware middlewares.use(errorMiddleware(server, middlewareMode)) //... } Copy code

When we use vite to develop, we process our modules from the above various middleware and then return them to the browser for processing.

reference