I am Chenshuang. In this article, I will share my toy-Vite implementation idea and my heart process. I will implement it step by step from the beginning and explain why I do it.

Vite brief introduction

Vite is known as the next generation of front-end development and build tools, its most important feature is “fast”, this is reflected in the Logo and name, Vite (French means “fast”, pronounced /vit/, pronounced the same as “veet”), Vite will be impressed by the “fast”. The main reason why Vite is so “fast” is the use of Native ES Modules in the development environment. To explore the principle, let’s take a look at Native ESM first.

Native ESM

Developers who have written JavaScript may wonder why there are so many modular specifications for JavaScript, AMD, CMD, CJS, ESM, etc. The reason is that JavaScript was never designed to be modular. In order to solve the modularity problem, people invented a lot of modularity specifications, but this also created another problem, which was “chaos”. Finally, the ECMA Committee got tired of it and brought ESM to JavaScript in ES6. There are some differences between ES6 Native ESMs and those written in Webpack. The main differences are as follows.

  1. To run ESM code, the script tag must have the type=”module” attribute.

  2. The import path must be a URL or relative path.

Example:

// You must add type="module" to use the ESM<script type="module">
  // Import path must be url or relative path
	import React from 'http://esm.sh/react'
</script>
Copy the code

Native ESM compatibility

To run the examples below, you need to have a browser compatible with Native ESM. Below is a list of some of the major browsers. Compatibility query address.

React Counter Demo

Now try running an example React counter in a browser using ESM without borrowing any packaging tools.

index.html

// omit other parts<body>
  <div id="root"></div>
  <script type="module" src="./index.js"></script>
</body>
Copy the code

index.js

import React from "http://esm.sh/react";
import ReactDOM from "http://esm.sh/react-dom";
import htm from "http://esm.sh/htm";

const html = htm.bind(React.createElement);

const App = () = > {
  const [count, setCount] = React.useState(0);

  return html`
    <div>
      <div>${count}</div>
      <button onClick=${() => setCount((v) => v + 1)}>add</button>
    </div>
  `;
};

ReactDOM.render(html`<${App}/ > `.document.getElementById("root"));
Copy the code

Note that the File protocol does not run and requires an HTTP Server to be enabled, VS Code’s Live Server extension is recommended. Not surprisingly, you should see a div and a button in the browser.

There are two possible puzzles here

  1. Why import from esm.sh?

    Esm.sh is similar to a CDN, and packages from above have been converted to ESM format by ESBuild.

  2. What library is HTM?

    HTM is called Hyperscript Tagged Markup. JSX can be used instead of JSX because JSX cannot run directly in the browser and does not want to write React. CreateElement. It provides a jSX-like experience, but does not require compilation and runs directly in the browser. For template string syntax, go directly to MDN.

React in the browser without using any packaging tools, as if back to the “slash-and-burn” era 🤣🤣🤣.

This is where you can get a rough idea of how Vite works.

  1. Start an HTTP server.
  2. Intercepts JS requests and compiles syntax not supported by browsers (import directly from node_modules, JSX, etc.).

Vite implementation

Along the same lines, start trying to implement a toy-Vite. File directories are introduced here.

| - toy - vite | - SRC / / toy - vite source | -- index. Js | - demo / / to run the demo | -- index. HTML | -- index. Js | -- package. JsonCopy the code

The demo file is almost identical to the React Counter demo above, except that the library is imported directly from node_Modules and uses JSX.

index.js

// import directly from node_modules
import React from "react";
import ReactDOM from "react-dom";

// Use JSX instead of the HTM library
return (
	<div>
  	<button>-</button>
  	<span>{count}</span>
  	<button>+</button>
  </div>
)
Copy the code

http server

Koa was chosen for HTTP Server in Vite 1.0, and for self-implementation in Vite 2.0, express was chosen.

const express = require("express");
const path = require("path");
const fs = require("fs");

const app = express();

const demoPath = path.resolve(__dirname, ".. /demo");

app.use((req, res) = > {
  const { url } = req;
  let content = fs.readFileSync(path.resolve(demoPath, "." + url)).toString();
  if (url.endsWith("html")) {
    res.type("html");
  } else if (url.endsWith("js")) {
    res.type("js");
  }
  res.send(content);
});

app.listen(8000);
Copy the code

Such a simple HTTP server started well, at this point in the browser enter localhost: 8000 / index, HTML, and open the console, you will see a similar Uncaught SyntaxError: Unexpected token ‘<‘ error: Unexpected token ‘<‘ error: Unexpected token ‘<‘ error: Unexpected token ‘<‘ error: Unexpected token ‘<‘

Conversion JS

In the res. Type (js); Now add the processing of JSX.

content = require("@babel/core").transformSync(content, {
      plugins: ["@babel/plugin-transform-react-jsx"],
}).code;
Copy the code

JSX has been correctly compiled, but the interface still does not render properly. Now the error is Uncaught TypeError: Failed to resolve module specifier “react”. Relative references must start with either “/”, “./”, or “.. /”., remember that the path introduced in Native ESM must be a URL or relative path, which is handled here.

import React from "react";
/ / convert
import React from "/node_moduels/react";
Copy the code

Write a simple re to handle, above the JSX code section, plus

const regex = /from\s*"(.+)"/g;
content = content.replace(regex, `from "/node_modules/$1"`);
Copy the code

Since these two pieces of code will be used again, you can encapsulate them into a transformJs function.

React-dom/demo/node_modules/react/demo/node_modules/react/demo/node_modules/react/demo/node_modules/react Therefore, requests with urls starting with /node_modules need special treatment. Const {url} = req; After add

if (url.startsWith("/node_modules")) {
    res.type("js");
    const pkg = JSON.parse(
      fs
        .readFileSync(path.resolve(demoPath, "." + url, "./package.json"))
        .toString()
    );
    const main = pkg.main;
    let content = fs
      .readFileSync(path.resolve(demoPath, "." + url, main))
      .toString();
    content = transformJs(content);
    res.send(content);
    return;
}
Copy the code

Json file in node_modules, find the main field in the file, index the React file according to the main field value, and send it to the client, open the console again. Uncaught SyntaxError: The requested module ‘/node_modules/react’ does not provide an export named ‘default’, check The content returned by The react request.

React The content returned by the request

'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}
Copy the code

/node_modules/react/demo/node_modules/react/demo/node_modules/react/demo/node_modules/react /react/demo/node_modules/react /react/demo/node_modules/react /react/demo/node_modules/react /react/demo/node_modules/react /react/demo/node_modules/react /react/demo/node_modules/react /react/demo/node_modules/react /react

There’s nothing wrong with the approach so far, but there’s a problem with React not offering an ESM version. To get a feel for it, run a framework that provides an ESM version. Here, preact is selected, and with a few minor changes, you can run successfully.

  • The example in the demo was changed to a Preact implementation

  • @babel/plugin-transform-react-jsx configurepragma as h

  • The package.json main field is replaced with module

The existing code implementation is here, interested in the above can also be modified to run, it is strongly recommended to run the existing implementation to feel.

The existing problems in

In the above implementation, I tried importing Lodash-es and found that hundreds of HTTP requests were made to see the contents of the Lodash-es file

export { default as add } from './add.js';
export { default as after } from './after.js';
export { default as ary } from './ary.js'; . Omit the moreCopy the code

There are hundreds of such export statements, each corresponding to an HTTP request, which is obviously unreasonable.

The main problems with existing implementations are:

  • Dependent libraries must provide ESM format or they won’t work

  • Relying on too many source files results in too many HTTP requests

Use esbuild for dependency prebuilds

Remember importing dependencies from ESm.sh, if you pre-package the dependencies, package them into ESM format, and merge files, you can solve the above problem. We can use esBuild to pre-build HTTP Server before starting it.

async function prebuild() {
  // Read all dependencies in demo
  const pkg = JSON.parse(
    fs.readFileSync(path.resolve(demoPath, "./package.json")).toString()
  );
  const dependencies = Object.keys(pkg.dependencies).map((id) = >
    path.resolve(demoPath, "./node_modules", id)
  );

  // Use esbuild packaging
  await build({
    absWorkingDir: process.cwd(),
    entryPoints: dependencies,
    format: "esm".// The output format is ESM
    bundle: true.// Bundle
    splitting: true.outdir: path.resolve(demoPath, "./node_modules".".toyvite")}); }Copy the code

This is a straightforward pre-build of all the dependencies used in the demo, whereas in Vite we scan the dependencies that are actually used in the project using the regex. After executing prebuild, demo/node_modules/.toyvite can see the packaged product, so when reading the dependent library, just read in the.toyvite directory and continue to modify the code.

if (url.startsWith("/node_modules")) {
      res.type("js");
      const name = url.substr(url.lastIndexOf("/") + 1);
      let content = fs
        .readFileSync(
          path.resolve(
            demoPath,
            "./node_modules/.toyvite/",
            name.endsWith(".js")? name : name +".js"
          )
        )
        .toString();
      content = transformJs(content);
      res.send(content);
      return;
}
Copy the code

Open your browser and successfully run the React code. There is a point where we can continue to optimize. Since we use esbuild, we can also use EsBuild directly in transformJs. Modify the part of the transformJs function that transforms JSX.

content = transformSync(content, {
    jsx: "transform".loader: "jsx",
  }).code;
Copy the code

The full code can be found here.

conclusion

React is just a toy, but you can still use it to learn from Vite. When learning a project, if it is difficult to read the source code directly, you can try to implement a toy version. After understanding the principle, you can look at the source code, perhaps the effect will be better.

Write in the last

The author is currently working in the e-commerce department of Bytedance-Douyin. The team has many HCS in Beijing and Shanghai. If you are interested, you can send your resume to [email protected] or add my wechat suchangvv to contact me for internal promotion. May you all find your dream job.