Take ten minutes to understand the principles of Vite

Take ten minutes to understand the principles of Vite

Vite is a lighter and faster web application development tool for modern browsers. It is implemented based on the ECMAScript standard native module system ES Module.

His appearance is to solve the problem that webpack cold start time is too long, and Webpack HMR hot update response speed is slow.

A project created with Vite is an ordinary Vue3 application, which has a lot less configuration files and dependencies than an application created based on Vue-cli. The project development dependencies created by Vite are very simple, only Vite and @vue/compiler-sfc, Vite is a running tool, and compiler-sfc is for compiling single-file components ending with .vue.

Vite currently only supports Vue3.0 version by default. When creating a project, it also supports the use of other frameworks such as React by formulating different templates. Vite provides two sub-commands.

# Turn on the server vite serve # Bale vite build Copy code

There is no need to package when starting the service, so the startup speed is very fast. Packaging in a production environment is similar to webpack, which compiles and packs all files together. Vite uses native dynamic import to achieve the requirement for code cutting, so the packaged result can only support modern browsers. If older browsers need to be used, Polyfill can be introduced.

Previously, we used Webpack for packaging because the browser environment does not support modularity, and the module file will generate a large number of http requests. Modularization has been supported in modern browsers, and http2 also solves the problem of multiple file requests. Of course, if your application needs to support IE browser, then it still needs to be packaged. Because IE does not support ES Module.

Projects created by Vite require almost no additional configuration. By default, TS, Less, Sass, Stylus, postcss, etc. are supported, but the corresponding compiler needs to be installed separately. It also supports jsx and web assembly.

The benefit of Vite is to improve the developer's experience in the development process. The web development server does not need to wait to start immediately. The module hot update is almost real-time. The required files are compiled on demand to avoid compiling unused files. Out of the box, avoid the configuration of loader and plugins.

The core functions of Vite include opening a static web server, and being able to compile single-file components, and provide HMR functions.

When starting Vite, the current project directory will be used as the root directory of the static server. The static server will intercept some requests, compile it in real time when requesting a single file, and process modules that other browsers cannot recognize, and implement hmr through websocket.

Let's implement this function ourselves so as to learn its realization principle.

Build a static test server

We first implement a command line tool that can start a static web server. Vite uses KOA internally to implement static servers. (ps: The node command line tool can check my previous article, I won't introduce it here, just post the code directly).

npm init npm install koa koa-send -D Copy code

The entry file of the tool bin is set to the local index.js

#!/usr/bin/env node const Koa = require ( 'koa' ) const send = require ( 'koa-send' ) const app = new Koa() //Turn on the static file server app.use( async (ctx, next) => { //Load static files await send(ctx, ctx.path, { root : process.cwd(), index : 'index.html' } ) await next() }) app.listen( 5000 ) Console .log ( 'server has been launched HTTP://localhost: 5000' ) Copy the code

In this way, a tool for node static server is written.

Handling third-party modules

Our approach is that when a third-party module is used in the code, we can modify the path of the third-party module to give it a logo, and then get the logo from the server to process the module.

1. we need to modify the path of the third-party module. Here we need a new middleware to implement it.

You need to determine whether the file currently returned to the browser is javascript, just look at the content-type in the response header.

If it is javascript, you need to find the module path introduced in this file. ctx.body is the content file returned to the browser. The data here is a stream and needs to be converted into a string for processing.

const stream2string = ( stream ) => { return new Promise ( ( resolve, reject ) => { const chunks = []; stream.on( 'data' , chunk => {chunks.push(chunk)}) stream.on( 'end' , () => {resolve(Buffer.concat(chunks).toString( 'utf-8' ))}) stream.on( 'error' , reject) }) } //Modify the third-party module path app.use( async (ctx, next) => { if (ctx.type === 'application/javascript' ) { const contents = await stream2string(ctx.body); //change the body Modify the path imported in, and re-assign the body to the browser. //import vue from'vue', if it matches from', modify it to from'@modules/ ctx.body = contents.replace( /(from\s+[' "])(?![\.\/])/g , '$1/@modules/' ); } }) Copy code

Then we start to load the third-party module. Here we also need a middleware to determine whether the request path starts with our modified @module, and if it is, go to node_modules to load the corresponding module and return it to the browser.

This middleware must be placed before the static server.

//Load a third-party module app.use( async (ctx, next) => { if (ctx.path.startsWith( '/@modules/' )) { //intercept module name const moduleName = ctx.path.substr( 10 ); } }) Copy code

After getting the module name, you need to get the entry file of the module. What you want to get here is the entry file of the ES Module module. You need to find the package.json of this module and then get the value of the module field in this package.json, which is the entry file. .

//Find the module path const pkgPath = path.join(process.pwd(), 'node_modules' , moduleName, 'package.json' ); const pkg = require (pkgPath); //Re-assign ctx.path, it needs to be re-assigned Set an existing path, because the previous path does not exist ctx.path = path.join( '/node_modules' , moduleName, pkg.module); //execute the next middleware awiat next(); Copy code

In this way, although the browser request comes in with the @modules path, we modify the path path to the path in node_modules before loading, so that when loading, we will go back to node_modules to get the file, and respond to the browser with the loaded content.

//Load a third-party module app.use( async (ctx, next) => { if (ctx.path.startsWith( '/@modules/' )) { //intercept module name const moduleName = ctx.path.substr( 10 ); //Find the module path const pkgPath = path.join(process.pwd(), 'node_modules' , moduleName, 'package.json' ); const pkg = require (pkgPath); //Re-assign ctx.path , Need to reset an existing path, because the previous path does not exist ctx.path = path.join( '/node_modules' , moduleName, pkg.module); //execute the next middleware awiat next(); } }) Copy code

Single file component processing

We said before that browsers cannot handle .vue resources. Browsers can only recognize common resources such as js and css, so other types of resources need to be processed on the server side. When a single file component is requested, the single file component needs to be compiled into a js module on the server and returned to the browser.

When the browser requests a file (App.vue) for the first time, the server compiles the single-file component into an object, loads the component first, and then creates an object.

import Hello from './src/components/Hello.vue' const __script = { name : "App" , components : { Hello } } Copy code

Then go to load the entry file, this time it will tell the server to compile the template of this single-file component and return a render function. Then mount the render function to the component option object just created, and finally export the option object.

import {render as __render} from '/src/App.vue?type=template' __script.rener = __render .__ = hmrId __script '/src/App.vue' Export default __script duplicated code

That is to say, Vite will send two requests, the first request will compile a single-file file, and the second request will compile a single-file template and return a render function.

  1. Compile single file option

Let's first realize the situation of the first request form file. The single-file component needs to be compiled into an option, which is also implemented with a middleware here. This function should only be used when dealing with static servers and before dealing with third-party module paths.

We first need to compile the single-file component. Compiler-sfc is needed here.

//Process the single file component app.use( async (ctx, next) => { if (ctx.path.endsWith( '.vue' )) { //Get the content of the response file and convert it to a string const contents = await streamToString (ctx.body); //The content of the compiled file const {descriptor} = compilerSFC.parse(contents); //Define the status code let code; //If there is no type, it is the first request if (!ctx.query.type) { code = descriptor.script.content; //The code format here is that it needs to be transformed into the look in the vite we posted earlier //import Hello from'./components / Hello.vue' //export default { //name:'App', //components : { //Hello //} //} //Modify the format of the code, replace export default with const __script = code = code.relace( /export\s+default\s+/g , 'const __script = ' ) code += ` import {render as __render} from ' ${ctx.path} ?type=template' __script.rener = __render export default __script ` } //Set the browser response header to js ctx.type = 'application/javascript' //Convert the string into a data stream and pass it to the next middleware. ctx.body = stringToStream(code); } await next() }) const stringToStream = text => { const stream = new Readable(); stream.push(text); stream.push( null ); return stream; } Copy code
npm install @ vue/compiler-sfc -D duplicated code

Then we will deal with the second request of the single-file component. The url of the second request will carry the type=template parameter. We need to compile the single-file component template into the render function.

We must first determine whether there is type=template in the current request

if (!ctx.query.type) { ... } else if (ctx.query.type === 'template' ) { //Get the compiled object code is the render function const templateRender = compilerSFC.compileTemplate({ source : descriptor.template.content }) //Put the render function Assign value to code and return to browser code = templateRender.code } Copy code

Here we have to deal with the process.env in the tool, because these codes will be returned to the browser to run, if not processed, it will default to node, causing the operation to fail. It can be modified in the middleware that modifies the path of the third-party module. After modifying the path, add a modified process.env

//Modify the third-party module path app.use( async (ctx, next) => { if (ctx.type === 'application/javascript' ) { const contents = await stream2string(ctx.body); //change the body Modify the path imported in, and re-assign the body to the browser. //import vue from'vue', if it matches from', modify it to from'@modules/ ctx.body = contents.replace( /(from\s+[' "?!.]) ([///])/G , '$ 1/@ modules/' ) .replace ( /process\.env\.NODE_ENV/g , '" Development "' ); } }) Copy code

So far we have implemented a simplified version of vite. Of course, we only demonstrate the .vue file here. There is no processing for css, less, and other resources, but the methods are similar, and interested students can implement it by themselves. HRM has not been implemented either.

#!/usr/bin/env node const path = require ( 'path' ) const {Readable} = require ( 'stream) const Koa = require(' koa ') const send = require(' koa-send ') const compilerSFC = require(' @vue/compiler-sfc ') const app = new Koa() const stream2string = (stream) => { return new Promise((resolve, reject) => { const chunks = []; stream.on(' data ', chunk => {chunks.push(chunk)}) stream.on(' end ', () => {resolve(Buffer.concat(chunks).toString(' utf- 8 '))}) stream.on(' error ', reject) }) } const stringToStream = text => { const stream = new Readable(); stream.push(text); stream.push(null); return stream; } //Load third-party modules app.use(async (ctx, next) => { if (ctx.path.startsWith('/@modules/')) { //intercept module name const moduleName = ctx.path.substr(10); //find the module path const pkgPath = path.join(process.pwd(), ' node_modules ', moduleName,' package.json '); const pkg = require(pkgPath); //To re-assign ctx.path, you need to reset an existing path, because the previous path does not exist ctx.path = path.join('/node_modules ', moduleName, pkg.module); //Execute the next middleware awiat next(); } }) //Turn on the static file server app.use(async (ctx, next) => { //Load static files await send(ctx, ctx.path, {root: process.cwd(), index: ' index.html '}) await next() }) //Process single file components app.use(async (ctx, next) => { if (ctx.path.endsWith(' .vue ')) { //Get the content of the response file and convert it into a string const contents = await streamToString(ctx.body); //compile file content const {descriptor} = compilerSFC.parse(contents); //Define the status code let code; //No type is the first request if (!ctx.query.type) { code = descriptor.script.content; //The code format here is that it needs to be transformed into the look in the vite we posted earlier //import Hello from ' ./components/Hello.vue ' //export default { //name: ' App ', //components: { //Hello //} //} //Modify the format of the code and replace export default with const __script = code = code.relace(/export\s+default\s+/g, ' const __script = ') code += ` import {render as __render} from ' ${ctx.path}?type=template ' __script.rener = __render export default __script ` } else if (ctx.query.type === ' template ') { //Get the compiled object code is the render function const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }) //Assign the render function to the code and return it to the browser code = templateRender.code } //Set the browser response header to js ctx.type = ' application/javascript ' //Convert the string into a data stream and pass it to the next middleware. ctx.body = stringToStream(code); } await next() }) //Modify the third-party module path app.use(async (ctx, next) => { if (ctx.type === ' application/javascript ') { const contents = await stream2string(ctx.body); //Modify the path imported in the body, re-assign the value to the body and return it to the browser //import vue from ' vue ', matches from' to from '@modules/ ctx.body = contents.replace(/(from\s+[' "])(?![\.\/])/g,'$1/@modules/').replace(/process\.env\.NODE_ENV/g,'" development "'); } }) app.listen(5000) console.log('Server has started http://localhost:5000') Copy code