Vite series: handwritten a simple version of vite

As we all know, vite is already the leader of the new generation of web construction tools. It mainly allows front-end developers to quickly get a response without packaging after modifying the code in the local debugging stage, which improves our efficiency in the development stage. Next, in order to deeply understand the principle of vite, we first implement vite without pre built version.

First of all, we should know that vite can execute ESM code directly on the browser without packaging, because the current mainstream browsers have supported ESM module loading, that is, import can be directly recognized by the browser. On the premise of recognition, we only need to add type="module" on the script tag.

The functions we want to achieve are shown in the figure below:

  1. Realize the parsing of vue3 syntax
  2. Realize the analysis of SFC
  3. Realize the parsing of html and js
  4. Finally, a digital addition and subtraction operation function is realized

The first step is to create a node vite folder
The second step is to create an index in the folder HTML file and an entry file, Mian JS and an app Vue file

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module" src="./main.js"></script>
  <script>
    // These values are declared in advance because the following variables are directly used in the source code of the third-party library, so they are declared in advance
    const process = {
      env: {
        NODE_ENV: 'development',
      },
    };
    window.process = process;
    window.development = 'development';
  </script>
</html>
import * as Vue from 'vue';
import App from './App.vue';
Vue.createApp(App).mount('#app');
<template>
  <div>I'm a number now:{{ message }}</div>
  <button v-on:click="handleClick1">increase</button>
  <button v-on:click="handleClick2">reduce</button>
</template>
<script>
export default {
  data() {
    return {
      message: 0,
    };
  },
  methods: {
    handleClick1() {
      this.message += 1;
    },
    handleClick2() {
      this.message -= 1;
    },
  },
};
</script>

These three files are the code for the addition and subtraction digital operation function we want to realize. The rest is to implement a vite. These three files can run normally and see the effect on the browser.

After entering the node vite folder on the command, execute npm init. Next, we install the following packages:

  1. Nodemon ------------- node process management
  2. vue -------------------------- I installed 3.2 twenty
  3. @vue / compiler DOM ------ the version is consistent with that of vue (compile template into render function)
  4. @vue / compiler SFC ------ the version is consistent with that of vue (compile SFC files into json data)
  5. Es module lexer ------ obtain the information of the import statement in the file, which is used to obtain the package loaded through import
  6. Magic string ------------- the path used to replace the third-party package

Let's create server in the node vite directory JS is used to implement the simple version of vite function
package. The JSON code is as follows

{
  "name": "node-vite",
  "version": "1.0.0",
  "description": "",
  "main": "",
  "scripts": {
    "dev": "nodemon ./server.js",
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@vue/compiler-dom": "3.2.20",
    "@vue/compiler-sfc": "3.2.20",
    "es-module-lexer": "^0.9.3",
    "magic-string": "^0.25.7",
    "vue": "3.2.20"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

Enter the server JS let's create a local http server first

const http = require('http');
const server = http.createServer((req, res) => {
});
server.listen(9999);

Our goal is to let the browser access index by default when the browser accesses localhost:9999 HTML, load index.html from the browser After HTML, main.html is parsed by default js, when the browser resolves to import * as Vue from 'vue'; We can't parse the file of vue. What we need to do is to modify the path of vue, and then actively find the source location of vue, take out the js file of the esm version of vue, and then actively return it to the browser. Next, we start processing

const http = require('http');
const url = require('url');
const path = require('path');
const server = http.createServer((req, res) => {
    // Resolve the file name of the access address url
    let pathName = url.parse(req.url).pathname;
    // When the browser directly accesses localhost:9999, it directly returns index HTML file
    if(pathName === '/'){
        pathName = '/index.html'
    }
    // Resolve the suffix type of the file
    let extName = path.extname(pathName);
    let extType = '';
    switch (extName) {
      case '.html':
        extType = 'text/html';
        break;
      case '.js':
        extType = 'application/javascript';
        break;
      case '.css':
        extType = 'text/css';
        break;
      case '.ico':
        extType = 'image/x-icon';
        break;
      case '.vue':
        extType = 'application/javascript';
        break;
      default:
        extType = 'text/html';
    }
    // We will change import * as Vue from 'vue' into import * as Vue from '/@modules/vue';
    // In this way, we will know that the path marked with ` / @ modules' needs to go to node_ Source code found in modules
    if (/\/@modules\//.test(pathName)) {
        // If the resolved path has ` / @ modules', go to node_ Find the source generation of the corresponding library in modules and return it to the browser
        resolveNodeModules(pathName, res);
    } else {
        // Otherwise, parse the regular js file vue file, html file
        resolveModules(pathName, extName, extType, res, req);
    }
});
server.listen(9999);

So far, our server JS file structure is set up. Next, we start to implement the resolveModules() function

const compilerSfc = require('@vue/compiler-sfc');
const compilerDom = require('@vue/compiler-dom');
const fs = require('fs');
function resolveModules(pathName, extName, extType, res, req){
    fs.readFile(`.${pathName}`, 'utf-8', (err, data) => {
        // If there is an error in file parsing, the error will be thrown directly
        if(err){
            throw err;
        }
        // Set content type
        res.writeHead(200, {'Content-Type': `${extType}; charset=utf-8`})

       // The processing of responding to different types of files requested
       if(extName === '.js'){
        // Yes, the suffix is js file processing
        // The rewriteImports function replaces the path where import is introduced into the third-party package. We will implement this function later
        const r = rewriteImports(data);
        res.write(r);
       } else if (extName === '.vue') {
          // Yes, the suffix is File processing for vue (i.e. SFC)
          // Resolve the parameter object of the request url
          const query = querystring.parse(url.parse(req.url).query);
          // Parse sfc into json data through @ Vue / compiler sfc Library
          const ret = compilerSfc.parse(data);
          const { descriptor } = ret;
          if (!query.type) {
            // Parse the script part of sfc file
            const scriptBlock = descriptor.script.content;
            // In the sfc file, we may also use import to import the file, so we need the rewriteImports function to replace the path inside
            const newScriptBlock = rewriteImports(
              scriptBlock.replace('export default', 'const __script = '),
            );
            // Combine the replaced js part with the dynamically introduced render function (compiled from template), and then return to the browser
            const newRet = `
            ${newScriptBlock}
            import { render as __render } from '.${pathName}?type=template'
            __script.render = __render
            export default __script
            `;
            res.write(newRet);
          } else {
            // The browser resolves to 'import {render as _render} from' again/ App. vue? Type = template '` will load the render function
            // Parse the vue file and change the template part into the render function through the @ vue / compiler DOM library
            const templateBlock = descriptor.template.content;
            const compilerTemplateBlockRender = rewriteImports(
              compilerDom.compile(templateBlock, {
                mode: 'module',
              }).code,
            );
            res.write(compilerTemplateBlockRender);
         }
       } else {
        // For other suffixes, such as File processing of html and ico
        // Return directly without any processing
        res.write(data);
       }
       res.end();
    })
}

We begin to implement the rewriteImports() function

// Analysis of ES module lexer parameters
// n indicates the name of the module
// s indicates the starting position of the module name in the import statement
// e indicates the end position of the module name in the import statement
// ss indicates the starting position of the import statement in the source code
// se indicates the end position of the import statement in the source code
// d indicates whether the import statement is a dynamic import. If yes, it is the corresponding start position. Otherwise, it defaults to - 1
const { init, parse } = require('es-module-lexer');
const MagicString = require('magic-string');
function rewriteImports(soure) {
    const imports = parse(soure)[0];
    const magicString = new MagicString(soure);
    if (imports.length) {
    for (let i = 0; i < imports.length; i++) {
      const { s, e } = imports[i];
      let id = soure.substring(s, e);
      if (/^[^\/\.]/.test(id)) {
        // id = `/@modules/${id}`;
        // Modify the path and add the / @ modules prefix
        // magicString.overwrite(s, e, id);
        magicString.overwrite(s, e, `/@modules/${id}`);
      }
    }
    return magicString.toString();
  }
}

The rewriteImports function uses two black magic libraries, ES module lexer and magic string, to replace paths, which is also used in vite
Next, we implement the last function resolveNodeModules on node_ Get the resources of the third-party package in modules and return it to the browser

function resolveNodeModules(pathName, res){
    // Get vue in ` / @ modules/vue '   
    const id = pathName.replace(/\/@modules\//, '');
    // Get the absolute address of the third-party package
    let absolutePath = path.resolve(__dirname, 'node_modules', id);
    // Get the package. Of the third-party package The module field of JSON parses the package address of esm
    const modulePath = require(absolutePath + '/package.json').module;
    const esmPath = path.resolve(absolutePath, modulePath);
    // Read the js content of the esm module
    fs.readFile(esmPath, 'utf-8', (err, data) => {
      if (err) {
        throw err;
      }
      res.writeHead(200, {
        'Content-Type': `application/javascript; charset=utf-8`,
      })
      // Use the rewriteImports function to replace the path of the third-party package introduced in the resource
      const r = rewriteImports(data);
      res.write(r);
      res.end();
    );
}

The above is a simple version of vite. We can execute npm run dev to start the service and see the effect.
From vite2 After 0, youdashen has carried out a series of performance optimizations for vite. The most representative optimization is relying on pre build. I will write a simple vite with pre build version in the next issue. Writing an article also makes me understand the principle of vite more deeply, and I hope it can help you! Later, I also constantly update other source code analysis articles!

Keywords: Javascript Front-end vite

Added by PHPiSean on Tue, 14 Dec 2021 08:51:08 +0200