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.
-
To run ESM code, the script tag must have the type=”module” attribute.
-
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
-
Why import from esm.sh?
Esm.sh is similar to a CDN, and packages from above have been converted to ESM format by ESBuild.
-
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.
- Start an HTTP server.
- 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.