Vite is a lighter, faster Web application development tool for modern browsers. It is based on the ECMAScript standard native Module system ES Module implementation.

His appearance is to solve the webpack cold start time is too long, in addition to webpack HMR hot update slow response.

A project created using Vite is a normal Vue3 application with fewer configuration files and dependencies than a VUE-CLI-based application. Vite creates a very simple project development dependency, just Vite and @vue/ compiler-sFC, Vite is a running tool, compiler-sFC is to compile. Vue end of the single-file component.

Vite currently only supports Vue3.0 by default. You can create projects using different templates and other frameworks such as React. Vite provides two subcommands.

# Start vite serve # Pack vite buildCopy the code

There is no need to pack to start the service, so the startup speed is extremely fast. Packaging in production is similar to WebPack, which compiles and packs all files together. For code cutting requirements Vite uses native dynamic import to achieve, so packaging results can only support modern browsers, if older browsers need to use Polyfill.

We used Webpack packaging earlier because the browser environment does not support modularity and because module files generate a lot of HTTP requests. Modularity is already supported in modern browsers, and HTTP2 also solves the problem of multiple file requests. Of course, if your application needs to support Internet Explorer, it will need to be packaged. Because IE does not support ES Module.

Vite creates projects that require little extra configuration and support TS, Less, Sass, Stylus, PostCSS by default, but require a separate compiler to install. It also supports JSX and Web Assembly.

The benefits of Vite are to improve the experience of developers in the development process. The Web development server can be started immediately without waiting, the module hot update is almost real-time, and the required files are compiled on demand, avoiding the compilation of unnecessary files, and out of the box, avoiding the configuration of Loader and plugins.

The core features of Vite include starting a static Web server and being able to compile single-file components and provide HMR functionality.

When vite is started, the current project directory will be used as the root directory of the static server. The static server will intercept part of the request, compile in real time when requesting a single file, and process other modules that are not recognized by the browser. HMR is implemented through WebSocket.

Let’s implement this feature ourselves to learn how it works.

Set up a static test server

Let’s start by implementing a command-line tool that can start a static Web server. Vite uses KOA internally to implement static servers. The ps: Node command line tool can be viewed in my previous article, but I won’t cover it here, and will post the code directly.

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

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

#! /usr/bin/env node

const Koa = require('koa')
const send = require('koa-send')

const app = new Koa()

// Start 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 started http://localhost:5000')
Copy the code

You have written a tool for node static servers.

Handling third-party modules

What we do is when we use a third party module in our code, we can change the path of the third party module, give it an identity, and then retrieve that identity from the server to deal with the module.

First we need to modify the path of the third party module, here we need a new middleware to implement.

To determine if the file currently being 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 that needs to be converted to 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 path of the third-party module
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // Modify the import path in body, reassign the value to body and return it to the browser
        // import vue from 'vue' to from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(? ! [\.\/])/g.'$1/@modules/'); }})Copy the code

Then we start loading third-party modules, again requiring middleware to determine if the request path starts with the @module we modified, and if so, load the corresponding module from node_modules and return it to the browser.

This middleware is placed before the static server.

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

After obtaining the Module name, you need to obtain the import file of the Module. Here, you need to obtain the import file of the ES Module. You need to find the package.json of the Module first and then obtain the value of the Module field in the package.json, which is the import file.

// Find the module path
const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
const pkg = require(pkgPath);
// To reassign 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();
Copy the code

So the browser requests the @modules path, but we change the path to node_modules before loading, so that when loading, we go back to node_modules to get the file, and send the loaded content to the browser.

// Load the third-party module
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // Intercepts the 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 reassign 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 middlewareawiat next(); }})Copy the code

Single-file component processing

As mentioned earlier, the browser can’t handle. Vue resources. The browser can only recognize js, CSS and other common resources, so other types of resources need to be handled on the server side. When requesting a single file component, you need to compile the single file component into a JS module on the server and return it to the browser.

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

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

It then loads the entry file, this time telling the server to compile the single-file component’s template and returning a render function. Then mount the Render function to the newly created component option object and export the option object.

import { render as __render } from '/src/App.vue? type=template'
__script.rener = __render
__script.__hmrId = '/src/App.vue'
export default __script
Copy the code

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

  1. Compile single file option

Let’s first implement the first request for a single file. The single-file component needs to be compiled into an option, again with a middleware implementation. This function should be used before dealing with static servers only, before dealing with third-party module paths.

We first need to compile the single-file component. This is where compiler-SFC comes in.

// Handle single-file components
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);
        // Compile the file contents
        const { descriptor } = compilerSFC.parse(contents);
        // Define the status code
        let code;
        // If type does not exist, it is the first request
        if(! ctx.query.type) { code = descriptor.script.content;// The format of the code here is that it needs to be modified to look like the vite we posted earlier
            // import Hello from './components/Hello.vue'
            // export default {
            // name: 'App',
            // components: {
            // Hello
            / /}
            / /}
            // Reformat code to 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 data to be passed 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 the code
npm install @vue/compiler-sfc -D
Copy the code

We then process the second request from the single-file component, which takes the type=template parameter to the URL, and we need to compile the single-file component template into the render function.

We first need to determine whether the current request contains type=template

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

We also need to deal with process.env in the tool here, because this code will be returned to the browser and will default to Node if not handled, causing it to fail. This can be done in middleware where the path of a third party module is changed. After the path is changed, add a change to process.env

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

So far we have implemented a simplified version of Vite, of course, here we only demonstrated. Vue files, CSS, less, other resources are not processed, but the method is similar, interested students can implement their own. HRM has not been achieved 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; Use (async (CTX, next) => {if (ctx.path.startswith ('/@modules/')) {// moduleName = ctx.path.substr(10); // Find module path const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json'); const pkg = require(pkgPath); Ctx.path = path.join(' ctx.path = path.join(' ctx.path = path.join(' ctx.path = path.join('/node_modules', moduleName, pkg.module); // Execute the next middleware awiat next(); Use (async (CTX, next) => {// Load static file await send(CTX, ctx.path, {root: process.cwd(), index: 'index.html'}) await next()}) app.use(async (CTX, next) => {if (ctx.path.endswith (').vueConst contents = await streamToString(ctx.body); ')) {const contents = await streamToString(ctx.body); // Compilersfc.parse (contents); const {descriptor} = compilersfc.parse (contents); // define the status code. // If (! ctx.query.type) { code = descriptor.script.content; // Import Hello from '; // Import Hello from '; // Import Hello from ';./components/Hello.vue' // export default { // name: 'App', // components: {// Hello // // // // // // // // // // // // // // // // // // // // // // // // /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 to render function const templateRender = compilerSFC.com pileTemplate ({source: Descriptor. The template. The content}) / / will be assigned to render function code is returned to the browser. Code = templateRender code} / browser/set response headers for js CTX. Type ="application/javascript'// Converts the string into data to be passed to the next middleware. ctx.body = stringToStream(code); } await next()}) app.use(async (CTX, next) => {if (ctx.type === ')application/javascript') { const contents = await stream2string(ctx.body); // import vue from '// import vue from'vue', matches to from 'Modified tofrom '@modules/ ctx.body = contents.replace(/(from\s+['("])? ! [\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"'); }}) app.listen(5000) console.log(' Server started http://localhost:5000')Copy the code