Refer to Javanese Education – Teacher Bobby’s course;

Plug-ins that will be used in the project:

Esno:

Es6 code can be run, nodeJS node command can only run CMJ code, export, import, such as an error.

Chokidar:

Reference: www.npmjs.com/package/cho… Function, listen for file changes. Do file hot update required.

Esbuild:

Reference: esbuild. Making. IO/getting – sta… Function: Fast package files. Go language, multithreading.

You can learn from a mini-Vite:

Vite development server principle 2, esbuild related knowledge 3, Websocket related knowledge 4, hot update, Chokidar package monitoring file changes

Overall idea:

Take a look at the vite website to see how it works:

What is a Vite?

The official website explains:

Vite (French for “fast”, pronounced /vit/, also known as “veet”) is a new front-end build tool that dramatically improves the front-end development experience. It mainly consists of two parts:

  • A development server that provides rich built-in features based on native ES modules, such as surprisingly fast module hot update (HMR).
  • A set of build instructions that use Rollup to package your code and that are pre-configured to output highly optimized static resources for production.

We are writing a vite development server module today, with a focus on “native ES-based modules”, requiring browsers to be able to support native ESM and native ESM dynamic imports on script tags.

Then go to the official website why choose vite mentioned

1. Vite provides source code in a native ESM way. This essentially lets the browser take over part of the packaging: Vite simply transforms the source code when the browser requests it and makes it available on demand. Code is dynamically imported according to the situation, which is only processed if it is actually used on the current screen. 2. In Vite, HMR is executed on native ESM. When editing a file, Vite only needs to precisely inactivate the chain [1] between the edited module and its nearest HMR boundary (most of the time just the module itself), allowing HMR to keep up to date quickly regardless of the size of the application.

To summarize:

Vite is a front-end development tool that, like WebPack, builds a local server that returns code when you visit it. Unlike WebPack, webpack packs all the files you write into js, CSS, HTML, static files that you can access. But Vite uses the browser’s ESM functionality to dynamically return files, so you don’t need to package your code at startup. HTML, which has a script tag that sets type to module, so the browser will send another request for the file corresponding to the script tag SRC. The Vite server then returns the files based on the request, which is very fast because they are static files with very little logical interaction. Then there is a problem, I write JSX file, browser does not understand ah! The vite server will use esbuild to convert JSX into JS files when requesting JSX files and return them to the browser. Esbuild is written in go and is surprisingly fast.

React/minivite/minivite/minivite/minivite

Take react as an example. Finally, the client needs to get an HTML file containing the relevant files in React. So we need to learn how to create the react – app, refer to: www.jianshu.com/p/68e849768… In simple terms, to run your React code in a browser, you need three packages:

  • React package – Responsible for the core logic of React
  • React-dom package — takes care of the BROWSER’s DOM operations
  • Babel package – Escapes JSX syntax. (Add type=”text/ Babel “to script to write JSX)

Once we introduce these three packages directly into our HTML files, we can happily use React.

<! DOCTYPE html><html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>React</title>
    <! React -->
    <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
    <! React-dom -->
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
    <! -- Babel allows browsers to recognize JSX syntax, if not JSX syntax
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  </head>
  
  <body>
    <div id="app"></div>
    <script type="text/babel">
      // You must add type="text/ Babel ", otherwise JSX syntax is not recognized
      class App extends React.Component {
        render() {
          return (
            <div>
              <h1>Hello World</h1>
            </div>
          );
        }
      }
      ReactDOM.render(<App />.document.getElementById("app"));
    </script>
  </body>
</html>

Copy the code

The main. Js file will be introduced in index.html, the react package will be introduced in main.js, and the root node will be replaced in HTML. Then it can be packaged and sent to the browser (more on this later). In this tutorial, we will use KOA to set up a local server to block import requests from files and return them to esBuild converted files. Note that there are two types of esBuild converted files:

  • The first type is files that do not change, such as react, Babel and other third-party packages
  • One is always changing, like the code files we write ourselves

Therefore, for the files that will not change, we should implement the compiled, and set the cache, and for their own code files will be compiled in real time every time, because the speed of esbuild is very fast, and each package is introduced as a separate module, so it will be much faster than WebPack.

Practice:

Create a basic file directory as follows:



Document Description:

The dev.mand. Js and prett.mand. Js files are used to launch their corresponding files.

Index.html is the file we want to return to the browser, which looks like this:

<! DOCTYPEhtml>
<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>react</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="module" src="/target/main.jsx"></script>
    </body>
</html>
Copy the code

Set up koA server (express can also be used) :

import Koa from "koa";
import koaRouter from "koa-router";
import fs from "fs";
import path from "path";

export async function dev() {
    console.log("div....");
    const app = new Koa();
    const router = new koaRouter();
    app.use((ctx, next) = > {
        console.log("Have a request :", ctx.request.url);
        next();
        console.log("The request over.", ctx.body);
    });
    // The root directory requests to return HTML
    router.get("/".(ctx) = > {
        let htmlPath = path.join(__dirname, ".. /target/index.html");
        let html = fs.readFileSync(htmlPath);
        ctx.set("Content-Type"."text/html");
        ctx.body = html;
    });
    app.use(router.routes());

    app.listen(3030.() = > {
        console.log("app is listen 3030");
    });
}
Copy the code

We can use esno src/dev.command. Js to start the server. And the command line does not support ESNO, so write this instruction to the script tag of package.json file and start with NPM run.)

Esbuild convert JSX files:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App.jsx'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>.document.getElementById('root'))Copy the code

But browsers can’t use JSX files directly, so we converted JSX files using ESbuild and gave them to browsers. Use the transformSync function in esbuild as follows:

require('esbuild').transformSync('<>x</>', {
  jsxFragment: 'Fragment'.// Returns an empty node
  loader: 'jsx', {})code: '/* @__PURE__ */ React.createElement(Fragment, null, "x"); \n'.map: ' '.warnings: []}Copy the code

Conversion function:

import esbuild from "esbuild";

function transformCode(tranObj) {
    return esbuild.transformSync(tranObj.code, {
        loader: tranObj.loader || "js".format: "esm".sourcemap: true}); }export function transformJSX(opts) {
    let tranObj = { code: opts.code };
    tranObj.loader = "jsx";
    let res = transformCode(tranObj);
    return res;
}
Copy the code

Return the converted file to the browser:

JSX /target/main. JSX /target/main. JSX
    router.get("/target/main.jsx".(ctx) = > {
        let filePath = path.join(__dirname, ".. /target/main.jsx");
        let fileText = fs.readFileSync(filePath, "utf-8");
        ctx.set("Content-Type"."application/javascript");
        ctx.body = transformJSX({
            code: fileText,
        }).code;
    });
Copy the code

Caching third-party packages:

Uncaught TypeError: Failed to resolve module specifier “react”. Relative references must start with either “/”, “./”, or “.. /”. This is because the first line of the js file we returned import a react file but the path is not correct. It should start with ‘/’. So the next step is to deal with the imported package. So we can simply look at the import and if it doesn’t start with a ‘/’ or a ‘./’, then treat it as a third-party package and make it a cache. In addition, JSX and other files introduce the import path of the third-party package, we need to convert it into the cache path we set and then return it to the browser. Convert import path:

import esbuild from "esbuild";
import path, { dirname } from "path";
import fs from "fs";

function transformCode(tranObj) {
    return esbuild.transformSync(tranObj.code, {
        loader: tranObj.loader || "js".format: "esm".sourcemap: true}); }export function transformJSX(opts) {
    let tranObj = { code: opts.code };
    tranObj.loader = "jsx";
    let res = transformCode(tranObj);
    let { code } = res;
    // Parse the import of the code string
    // Why parse import?

    // import type { XXXX } from 'xxx.ts';
    // import React from 'react';
    // Use the "react" re to determine whether it is a local file or a tripartite library
    // Local files spell paths
    // The tripartite library is fetched from our pre-compiled cache
    code = code.replace(/\bimport(? ! \s+type)(? :[\w*{}\n\r\t, ]+from\s*)? \s*("([^"]+)"|'([^']+)')/gm.(a, b, c) = > {
        console.log("Regular match :", a, "-- -- -- -- -- -- --", b, "-- -- -- -- -- -- --", c);
        let fromPath = "";
        // Start with a '.' as a local file
        if (c.charAt(0) = = =".") {
            let filePath = path.join(opts.rootPath, c);
            console.log("filePath", filePath, path.dirname(opts.path), fromPath);
            if (fs.existsSync(filePath)) {
                fromPath = path.join(path.dirname(opts.path), c);
                fromPath = fromPath.replace(/\\/g."/");
                return a.replace(b, `"${fromPath}"`); }}else {
          // Todo takes files from third-party libraries from the cache
        }
        return a;
    });
    return { ...res, code };
}

Copy the code

Note: this regular expression /\bimport(? ! \s+type)(? :[\w*{}\n\r\t, ]+from\s*)? ([\ s * (” ^ “] +) “| ‘([^’] +))/gm is divided into three groups a, b, c; A is the entire line of import code, B is the quoted package name, and C is the unquoted package name.

In the previous step, we overwrote the import of the file so that our server can return the file based on the requested path. The third party packages are then processed in the suggor.js file. First we need to cache the third party packages at project startup time. Caching third-party packages:

import { build } from "esbuild";
import { join } from "path";

const appRoot = join(__dirname, "..");
const cache = join(appRoot, "target".".cache");

export async function pretreatment(pkgs = ["react"."react-dom"]) {
    console.log("pretreatment");
    let entrys = pkgs.map((item) = > {
        return join(appRoot, "node_modules", item, "cjs".`${item}.development.js`);
    });

    build({
        entryPoints: entrys,
        bundle: true.sourcemap: true.treeShaking: true.outdir: cache,
        splitting: true.logLevel: "error".metafile: true.format: "esm"}); }Copy the code

Note: The entry path is the path of your local installation package;

To get the cache package, run the pretreatment function.

We can write this in the script tag of the package.json file:"dev": "esno src/prett.command.js && esno src/dev.command.js"

The cache folder contains two packages:



We then return to the transformJSX function to process the third-party package;

        // Start with a '.' as a local file
        if (c.charAt(0) = = =".") {
            let filePath = path.join(opts.rootPath, c);
            console.log("filePath", filePath, path.dirname(opts.path), fromPath, path.dirname("/aa/bb/cc/dd.js"));
            if (fs.existsSync(filePath)) {
                fromPath = path.join(path.dirname(opts.path), c);
                fromPath = fromPath.replace(/\\/g."/");
                return a.replace(b, `"${fromPath}"`); }}else { // ============== add file fetching from cache to avoid packing again
            let filePath = path.join(opts.rootPath, `.cache/${c}/cjs/${c}.development.js`);
            if (fs.existsSync(filePath)) {
                fromPath = path.join(dirname(opts.path), `.cache/${c}/cjs/${c}.development.js`);
                fromPath = fromPath.replace(/\\/g."/");
                return a.replace(b, `"${fromPath}"`); }}return a;
Copy the code

Next, work with the CSS and SVG files.

At this time, a major problem was discovered:

All of our requests were successful, as shown below:



The react script didn’t work and didn’t replace the root element. What’s the problem? We went to see if any errors were reported and found:



It was found that the request to get logo.svg reported this error. I want js file and you give me SVG file. Import from js scripts can only be imported from other JS scripts in the browser. So instead of importing the static file directly, we convert it to a path that will be imported wherever it’s needed. Return this when importing:export default "/target/logo.svg"

Logo.svg is used here <imgsrc={logo}className="App-logo"alt="logo"/>Then the path will be requested to get the real file at the place of use.

So we need to add an identifier to all the places where we import static files, and then import when we request this file, the content-Type that we return is appliction/javascript, And then it says export default “file path”; When the file is used, the browser will send another request for the file and determine whether the static file should be handled.

Make two changes in the code: the transform function:

if (fs.existsSync(filePath)) {
    fromPath = path.join(path.dirname(opts.path), c);
    fromPath = fromPath.replace(/\\/g."/");
    // static files such as SVG add a flag behind the spelling url parameter === new
    if (["svg"].includes(path.extname(fromPath).slice(1))) {
        fromPath += "? import";
    }
    return a.replace(b, `"${fromPath}"`);
}
Copy the code

Dev target request:

router.get("/target/(.*)".(ctx) = > {
        let filePath = path.join(__dirname, "..", ctx.path.slice(1));
        let rootPath = path.join(__dirname, ".. /target");
        console.log("Target request file path:", filePath);

        // query with import marks is static resources ==== new
        if ("import" in ctx.query) {
            ctx.set("Content-Type"."application/javascript");
            ctx.body = `export default "${ctx.path}"`;
            return;
        }
Copy the code

At this point we can finally see the page.

Establish a Websocket link:

The page style CSS file is not working, so let’s put it aside and create a WebSocket link to handle the CSS file and hot update later

Websocket setup consists of two steps: a client and a server. The client uses the native WebScoket class, and the server uses the WS library to create the WebSocket service. Js. Add a script tag to index.html with SRC “@/vite/client” to request it.

Request @ / vite/client:

    // Stuff the client code to the browser, to the HTML
    router.get("/@vite/client".(ctx) = > {
        console.log("get vite client");
        ctx.set("Content-Type"."application/javascript");
        ctx.body = transformCode({
            code: fs.readFileSync(path.join(__dirname, "client.js"), "utf-8"),
        }).code;
        // What is returned here is the actual built-in client code
    });
Copy the code

Client. Js function:

let host = location.host;
console.log("vite client:", host);

// Create WebSocket connection.
const socket = new WebSocket(`ws://${host}`."vite-hmr");

// Connection opened
socket.addEventListener("open".function (event) {
    socket.send("Hello Server!");
});

// Listen for messages
socket.addEventListener("message".function (event) {
    handleServerMessage(event.data);
});

function handleServerMessage(payLoad) {
    let msg = JSON.parse(payLoad);
    console.log("Message from server ", payLoad, "= = = =", msg);
    switch (msg.type) {
        case "connected": {
            console.log("vite websocket connected");
            setInterval(() = > {
                socket.send("ping");
            }, 20000);
            break;
        }
        case "update": {
            console.log("Message update ", msg, msg.updates);
            msg.updates.forEach(async (update) => {
                if (update.type === "js-update") {
                    console.log("[vite] js update....");
                    await import(`/target/${update.path}? t=`);

                    // In this case, only modules should be updated, not completely reloaded.
                    QueueUpdate queueUpdate queueUpdate queueUpdate queueUpdate queueUpdate queueUpdatelocation.reload(); }});break; }}if (msg.type == "update") {}}// Encapsulates some tools and methods for manipulating CSS, since the client is in HTML and can be exported to other modules
const sheetsMap = new Map(a);// Id is the absolute path of the CSS file, and content is the content of the CSS file
export function updateStyle(id, content) {
    let style = sheetsMap.get(id);
    if(! style) { style =document.createElement("style");
        style.setAttribute("type"."text/css");
        style.innerHTML = content;
        document.head.appendChild(style);
    } else {
        style.innerHTML = content;
    }

    sheetsMap.set(id, style);
}

Copy the code

Add websocket service to dev.js:

function createWebSocketServer(httpServer) {
    console.log("create web server:");
    const wss = new WebSocketServer({ noServer: true });
    wss.on("connection".(socket) = > {
        console.log("connected ===");
        socket.send(JSON.stringify({ type: "connected" }));
        socket.on("message", handleSocketMsg);
    });
    wss.on("error".(socket) = > {
        console.error("ws connect error", socket);
    });
    httpServer.on("upgrade".function upgrade(req, socket, head) {
        if (req.headers["sec-websocket-protocol"] = ="vite-hmr") {
            console.log("upgrade".Object.keys(req.headers));
            wss.handleUpgrade(req, socket, head, (ws) = > {
                wss.emit("connection", ws, req); }); }});return {
        send(payLoad) {
            let sendMsg = JSON.stringify(payLoad);
            wss.clients.forEach((client) = > {
                client.send(sendMsg);
            });
        },
        close() {
            console.log("close websocket"); wss.close(); }}; }function handleSocketMsg(data) {
    console.log("received: %s", data);
}
Copy the code

Run createWebSocketServer to get a service instance of webSocket. In dev.js we call:

    // Use the native http.createServer to get the http.Server instance
    // Because the WS library is based on this instance to upgrade the HTTP protocol into the wesocket service
    // Since we are using KOA, we use the app.callback function to get a function that is used by httpServer to handle the request
    let httpServer = http.createServer(app.callback());
    // eslint-disable-next-line no-unused-vars
    const ws = createWebSocketServer(httpServer);

    httpServer.listen(3030.() = > {
        console.log("app is listen 3030");
    });
Copy the code

This way we can get a webSocket link, and we can write some public functions in the client to do some front-end operations. For example, update a CSS file after receiving a request.

Handling CSS files:

The link tag specifies href as the file address and ref as stylesheet. 2. Use the style tag to write the CSS file content directly. Either way we need to create a label. So the question is, how do we create tags in the HTML that we send? Because the HTML file is sent to the browser first, subsequent files are sent by recursive requests in the HTML tag.

So to solve this problem, we can use websocket, send the browser HTML file to add a script tag, inside a Websocket client, and then this can be through the document to create a new style tag, add CSS style. The WebSocket client, as we saw in the previous step, will update the CSS in this step;

Transform add:

export function transformCss(opts) {
    // let filePath = path.join(opts.rootPath, ".." , opts.path);
    // console.log("css path:", path.join(opts.rootPath, ".." , opts.path));
    // The CSS file uses the updateStyle function in client.js to create the style tag to add to the CSS content
    return `
        import { updateStyle } from '/@vite/client'

        const id = "${opts.path}";
        const css = "${opts.code.replace(/"/g."'").replace(/\n/g."")}";

        updateStyle(id, css);
        export default css;
    `.trim();
}
Copy the code

Added to the client.js file

// Encapsulates some tools and methods for manipulating CSS, since the client is in HTML and can be exported to other modules
const sheetsMap = new Map(a);// Id is the absolute path of the CSS file, and content is the content of the CSS file
export function updateStyle(id, content) {
    let style = sheetsMap.get(id);
    if(! style) { style =document.createElement("style");
        style.setAttribute("type"."text/css");
        style.innerHTML = content;
        document.head.appendChild(style);
    } else {
        style.innerHTML = content;
    }

    sheetsMap.set(id, style);
}
Copy the code

Add to dev:

switch (path.extname(ctx.url)) {
case ".svg":
    ctx.set("Content-Type"."image/svg+xml");
    ctx.body = fs.readFileSync(filePath, "utf-8");
    break;
case ".css": / / = = = = = = new
    ctx.set("Content-Type"."application/javascript"); 
    ctx.body = transformCss({
        code: fs.readFileSync(filePath, "utf-8"),
        path: ctx.path,
        rootPath,
    });
    break;
Copy the code

To recap, this step makes a request to the server where a CSS file is imported. The server returns a JS file that imports the updateStyle function from client.js and passes the CSS file contents and file location to the function. In the updateStyle function, check whether there is a corresponding style tag based on the position. If there is, it will be updated; if there is no style tag, it will be generated.

Hot update:

Hot update on file changes required, which we use chokidar npm:www.npmjs.com/package/cho… The idea is to listen to the file changes, change the file name to do processing (is to convert the absolute address of the file to the relative address requested by the browser), through websocket to tell the front end, that module changed, let him re-import.

Listen for file changes:

Dev. Js to add

// Listen for file changes
function watch() {
    return chokidar.watch(targetRootPath, {
        ignored: ["**/node_modules/**"."**/.cache/**"].ignoreInitial: true.ignorePermissionErrors: true.disableGlobbing: true}); }Copy the code

Add watch to the dev function:

    let httpServer = http.createServer(app.callback());
    // eslint-disable-next-line no-unused-vars
    const ws = createWebSocketServer(httpServer);
    // Listening file changed ======== new
    watch().on("change".(filePath) = > {
        console.log("file is change", filePath, targetRootPath);
        handleHMRUpdate(ws, filePath);
    });

    httpServer.listen(3030.() = > {
        console.log("app is listen 3030");
    });
Copy the code

Functions to handle hot updates:

function getShortName(filePath, root) {
    return `${filePath.replace(root, "").replace(/\\/g."/")}`;
    // return path.extname(filePath);
}

// Handle file updates
function handleHMRUpdate(ws, filePath) {
    // let file = fs.readFileSync(filePath);
    const shortFile = getShortName(filePath, targetRootPath);
    console.log("short file:", shortFile);
    let updates = [
        {
            type: "js-update".path: ` /${shortFile}`,},];let sendMsg = {
        type: "update",
        updates,
    };
    ws.send(sendMsg);
}
Copy the code

Handling file changes in client.js:

function handleServerMessage(payLoad) {
    let msg = JSON.parse(payLoad);
    console.log("Message from server ", payLoad, "= = = =", msg);
    switch (msg.type) {
        case "connected": {
            console.log("vite websocket connected");
            setInterval(() = > {
                socket.send("ping");
            }, 20000);
            break;
        }
        case "update": { // ============= new, message type is to update file
            console.log("Message update ", msg, msg.updates);
            msg.updates.forEach(async (update) => {
                if (update.type === "js-update") {
                    console.log("[vite] js update....");
                    await import(`/target/${update.path}? t=`);

                    // In this case, only modules should be updated, not completely reloaded.
                    QueueUpdate queueUpdate queueUpdate queueUpdate queueUpdate queueUpdate queueUpdatelocation.reload(); }});break; }}}Copy the code

Conclusion:

So far, we have achieved mini-Vite, the general idea and source code is about the same, but some details have not done, such as the separate rendering of each module, simple is not “.” to determine whether the beginning of the third party package, whether static file judgment is not comprehensive. It is ok to understand the design idea of Vite as a whole, and also learn about esbuild and Webpack. Full project address: gitee.com/zyl-ll/vite…