Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Suppose we have a target folder with the following file structure

Target ├─ app.CSS ├─ index.css ├─ index.html ├─ index.jsx ├─ logo.svgCopy the code
    // index.html key code
    <script type="module" src="/target/index.jsx"></script>

    // In index.jsx, this is an entry file for the standard 'react' syntax
    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

Next, we’ll get this file to work in the browser, using the core ideas of Vite without the help of familiar WebPack or other CLI tools.

Step by step, implement a simple Vite development version

There are several node modules used here, which can be pre-written in package.json and then installed by NPM install

    {
        // ...
        "devDependencies": {
            "esbuild": "^ 0.9.7"."esno": "^ 0.5.0"."express": "^ 4.17.1"."react": "^ 17.0.0"."react-dom": "^ 17.0.0"."ws": "^ 7.4.5"
        },
        "dependencies": {
            "chokidar": "^ 3.5.2." "}}Copy the code

1. Set up an HTTP service and return our entry fileindex.html

At the target level, create a new SRC directory and create dev.js under SRC

    // dev.js

    import express from "express";
    import { createServer } from "http";
    import { join } from 'path'; // File path related operation API
    import { readFileSync } from "fs"; // File read related operation API

    // target Specifies the absolute path to the folder
    const targetRootPath = join(__dirname, '.. /target'); 

    export async function dev() {
        const app = express();
        // Intercepts the request root path and returns the contents of the index.html file
        app.get('/'.(req, res) = > {
            // Read the index.html file
            const htmlPath = join(targetRootPath, 'index.html');
            let html = readFileSync(htmlPath, 'utf-8');
            // Set the returned content type to text/ HTML
            res.set('Content-Type'.'text/html');
            // Returns the string of the index.html file
            res.send(html);
        });

        // Create a server
        const server = createServer(app);
        const port = 9001;
        // Listen on the port
        server.listen(port, () = > {
            console.log('App is running at http://127.0.0.1:' + port)
        });
    }
Copy the code

Create a new dev.mand.js that imports dev.js and executes the dev() method

    // dev.commamd.js
    import { dev } from './dev';

    dev().catch(console.error);
Copy the code

Create a new dev script in package.json and use the esno module to execute the ES syntax file

    // package.json
    {
        // ...
        "scripts": {
            "dev": "esno src/dev.command.js"}},Copy the code

In the terminal, run the NPM run dev command, open http://127.0.0.1:9001/ in the browser, and check the Network panel. You can find the contents of the index. HTML file. It has returned normally, but the page is still blank.

2. Handle static resources

In the previous step, the index.html was parsed normally and returned, but found that it introduced the index.jsx resource 404

The convention here is that static resource paths all start with /target, so they can be treated uniformly in the route

2.1 Processing Script Files (JS and JSX)

    // Esbuild is used to convert various types of JS files into esM formats that browsers can recognize

    // dev.js
    // ...
    import { transformSync } from 'esbuild'; // Build code


    const transformCode = opts= > {
        return transformSync(opts.code, {
            loader: opts.loader || 'js'.sourcemap: true.format: 'esm'})}const transformJSX = opts= > {
        const ext = extname(opts.path).slice(1); // 'jsx'
        const ret = transformCode({ // jsx -> js
            loader: ext,
            code: opts.code
        });

        let { code } = ret;

        return {
            code
        }
    }

    // target Specifies the absolute path to the folder
    const targetRootPath = join(__dirname, '.. /target'); 

    export async function dev() {
        // ...

        // Intercepts static resource paths and returns resources recognized by the appropriate browser
        app.get('/target/*'.(req, res) = > {

            // req.path -----> /target/index.jsx
            // Full file path
            const filePath = join(__dirname, '.. ', req.path.slice(1));

            // JSX files are processed one by one
            switch (extname(req.path)) {
                case '.jsx': {
                    res.set('Content-Type'.'application/javascript');
                    // This encapsulates a method for converting JSX files to JS files, since browsers cannot parse JSX files
                    res.send(
                        transformJSX({
                            appRoot: targetRootPath,
                            path: req.path,
                            code: readFileSync(filePath, 'utf-8')
                        }).code
                    )
                    break;
                }
                default:
                    break; }})// ...
    }

Copy the code

JSX: import node_modules: import node_modules: import node_modules: import node_modules: import node_modules: import node_modules

Local resource to be imported, concatenate /target prefix, unify its static resource path

/target/. Cache /${moduleName}/index.js, and convert the files in node_modules into ESM modules. /target/. Cache /${moduleName}; Compilation module using ESbuild, faster performance

A transform.js file is extracted to handle file compilation and conversion operations

    // transform.js

    import { transformSync, build } from 'esbuild';
    import { extname, dirname, join } from 'path'
    import { existsSync } from 'fs'


    // Cache the compiled node_modules module to prevent multiple compilations
    let nodeModulesMap = new Map(a);const appRoot = join(__dirname, '.. ')
    const cache = join(appRoot, 'target'.`.cache/`);

    / * * * *@param {Object} Opts compiles configuration objects *@returns Code is the compiled code */
    export const transformCode = opts= > {
        return transformSync(opts.code, {
            loader: opts.loader || 'js'.sourcemap: true.format: 'esm'})}/ * * * *@param {Array}} PKGS the set of node_modules modules to compile */
    const buildNodeModule = async pkgs => {
        const ep = pkgs.reduce((c, n) = > {
            c.push(join(appRoot, "node_modules", n, `index.js`));
            returnc; } []);// console.log(111, ep);

        await build({
            entryPoints: ep,
            bundle: true.format: 'esm'.logLevel: 'error'.splitting: true.sourcemap: true.outdir: cache,
            treeShaking: 'ignore-annotations'.metafile: true.define: {
                "process.env.NODE_ENV": JSON.stringify("development") // Default development mode}})}// Convert js, JSX code to ESM module
    export const transformJSX = async opts => {
        const ext = extname(opts.path).slice(1); // 'jsx'
        const ret = transformCode({ // jsx -> js
            loader: ext,
            code: opts.code
        });

        let { code } = ret;

        // Used to save the node_module that needs to be compiled
        let needbuildModule = [];

        /** * import from local files, node_modules * import React from 'React '; * The react re checks whether it is a local file or a tripartite library */
        code = code.replace(
            /\bimport(? ! \s+type)(? :[\w*{}\n\r\t, ]+from\s*)? \s*("([^"]+)"|'([^']+)')/gm.(a, b, c) = > {
                let from;
                if (c.charAt(0) = = ='. ') { // Local file
                    from = join(dirname(opts.path), c);
                    const filePath = join(opts.appRoot, from);
                    if(! existsSync(filePath)) {if (existsSync(`${filePath}.js`)) {
                            from = `The ${from}.js`}}if (['svg'].includes(extname(from).slice(1))) {
                        from = `The ${from}? import`}}else { // from node_modules
                    from = `/target/.cache/${c}/index.js`;
                    if(! nodeModulesMap.get(c)) { needbuildModule.push(c); nodeModulesMap.set(c,true)}}return a.replace(b, `"The ${from}"`)})// If there are third-party modules that need to be compiled
        if(needbuildModule.length) {
            await buildNodeModule(needbuildModule);
        }
        return {
            ...ret,
            code
        }
    }
Copy the code

2.2 Processing Other Static resource files

As of the previous step, after restarting the NPM run dev command, you should see that the browser has parsed most of the files properly, and the page should display normally. Check the Network panel and find that CSS and SVG files are not processed properly. In the static resource handler, continue adding conditions

    // dev.js
    import { transformCss, transformJSX } from './transform';

    // ...

    // Intercepts static resource paths and returns resources recognized by the appropriate browser
    app.get('/target/*'.async (req, res) => {

        // ...

        // There is a difference between different file processing
        switch (extname(req.path)) {
            
            // ...

            case '.svg':
                // SVG files are actually recognized by browsers
                res.set('Content-Type'.'image/svg+xml');
                res.send(
                    readFileSync(filePath, 'utf-8'))break;
            case ".css":
                // a CSS file that encapsulates a transformCss method that returns type script
                res.set('Content-Type'.'application/javascript');
                res.send(
                    transformCss({
                        path: req.path,
                        code: readFileSync(filePath, 'utf-8')
                    })
                )
                res.send()
                break;
            default:
                break; }})// transform.js

    // ...

    // concatenate a code block that automatically adds style to head
    export const transformCss = opts= > {
        return `
        var insertStyle = function(content) {
            let style = document.createElement('style');
            style.setAttribute('type', 'text/css');
            style.innerHTML = content;
            document.head.appendChild(style);
        }
        const css = "${opts.code.replace(/\n/g.' ')}";
        insertStyle(css);
        insertStyle = null;
        export default css;
        `
    }

Copy the code

Here may also involve other types of files, such as PNG, JSON, less and other files, can be processed according to this method, return the corresponding resources can be, no example;

At this point, after restarting the NPM run dev command, the page should be displayed and running normally

3. Hot update

Hot updates can be an essential feature for development engineers during development, so let’s implement a simple hot update

Question to consider: What is the hot update process like?

Modify the code -> the page automatically refreshes

To be more specific:

Modify the code -> Node finds that the XXX file has changed -> notify the browser that the XXX file has changed -> the browser receives the message -> the browser requests the XXX file again -> the page automatically refreshes

Based on the above flow, it’s easy to think of:

1. The Node application needs to be able to listen for file changes

2. The Node application needs to be able to send messages to the target browser

3. The browser needs to be able to accept messages

4. The browser needs to be able to pull the changed file and refresh it accordingly

So, chokidar is used to listen for file changes, the browser receives messages using WebSocket, and the Node process communicates with the browser using the WS module

Here’s another question: How do I embed WebSocket code in the browser?

In the previous process, we will read the index.html through the Node module, then returned to the browser, so you can read the file content, insert a script tag, in which the corresponding embedded code, not solved ~

For the sake of elegance, we can stuff an ESM module and then intercept the request and return the corresponding code

So first of all, let’s create a new oneclient.jsWebSockte code for the browser

Reload (). There are many other ways to use vue and react. Such as the react – hot – loader, etc

    // client.js

    console.log('[vite] is connecting.... ');

    const host = location.host;

    // The client-server establishes a communication
    const socket = new WebSocket(`ws://${host}`.'vite-hmr');

    // Listen for the traffic, take the data, and do the processing
    socket.addEventListener('message'.async ({ data }) => {
        handleMessage(JSON.parse(data)).catch(console.error);
    })

    async function handleMessage(payload) {
        switch (payload.type) {
            case 'connected':
                console.log('[vite] connected.');

                setInterval(() = > socket.send('ping'), 30000);
                break;
            case 'update':
                payload.updates.forEach(async (update) => {
                    if (update.type === 'js-update') {
                        console.log('[vite] js update.... ');
                        await import(`/target/${update.path}? t=${update.timestamp}`);

                        // mocklocation.reload(); }})break; }}Copy the code

Create a newwebSocket.js, listen for file changes and notify the browser

    import chokidar from 'chokidar'
    import WebSocket from 'ws';
    import { posix } from 'path'

    // Expose the webSocket creation method
    // Create a WebSocket service that encapsulates the send method
    export function createWebSocketServer(server) {
        const wss = new WebSocket.Server({ noServer: true })

        server.on('upgrade'.(req, socket, head) = > {
            if (req.headers['sec-websocket-protocol'= = ='vite-hmr') {
                wss.handleUpgrade(req, socket, head, (ws) = > {
                    wss.emit('connection', ws, req); }); }}); wss.on('connection'.(socket) = > {
            socket.send(JSON.stringify({ type: 'connected' }));
        });

        wss.on('error'.(e) = > {
            if(e.code ! = ='EADDRINUSE') {
                console.error(
                    chalk.red(`WebSocket server error:\n${e.stack || e.message}`)); }});return {
            send(payload) {
                const stringified = JSON.stringify(payload);
                wss.clients.forEach((client) = > {
                    if(client.readyState === WebSocket.OPEN) { client.send(stringified); }}); },close(){ wss.close(); }}},// Expose the method of listening for file changes
    export function watch(targetRootPath) {
        return chokidar.watch(targetRootPath, {
            ignored: ['**/node_modules/**'.'**/.cache/**'].ignoreInitial: true.ignorePermissionErrors: true.disableGlobbing: true})},function getShortName(file, root) {
        return file.startsWith(root + '/')? posix.relative(root, file) : file; }// Expose the file change function
    // The file changes the callback that executes, which actually pushes the change data with websocket
    export function handleHMRUpdate(opts) {
        const { file, ws } = opts;
        const shortFile = getShortName(file, opts.targetRootPath);
        const timestamp = Date.now();
        let updates
        if (shortFile.endsWith('.css') || shortFile.endsWith('.jsx')) {
            updates = [
                {
                    type: 'js-update',
                    timestamp,
                    path: ` /${shortFile}`.acceptedPath: ` /${shortFile}`
                }
            ]
        }

        ws.send({
            type: 'update',
            updates
        })
    }
Copy the code

dev.jsThe corresponding modification

    // dev.js
    import { createWebSocketServer, watch, handleHMRUpdate} from './webSocket'
    // ...

    // Intercepts the request root path and returns the contents of the index.html file
    app.get('/'.(req, res) = > {

        // ...

        // Returns the string of the index.html file
        html = html.replace('<head>'.`\n `).trim()
        res.send(html);
    });

    // Return the client code to the browser in ESM format
    app.get('/@vite/client'.(req, res) = > {
        res.set('Content-Type'.'application/javascript');
        res.send(
            // What is returned here is the actual built-in client code
            transformCode({
                code: readFileSync(join(__dirname, 'client.js'), 'utf-8')
            }).code
        )
    });

    // ...

    const ws = createWebSocketServer(server);

    // Listen for file changes
    watch(targetRootPath).on('change'.async (file) => {
        handleHMRUpdate({ file, ws, targetRootPath });
    })

    const port = 9001;
    // Listen on the port
    server.listen(port, () = > {
        console.log('App is running at http://127.0.0.1:' + port)
    });
Copy the code

conclusion

At this point, a simple Vite development can be said to be complete.

A brief overview of the process:

1.viteBased on esM mechanism

Import content will pull resources through requests, and we can build a service to intercept the return of these requests and return the content we have processed

2. No build process compiled

The entire application is based entirely on Node services, with static resource loading and no compilation and build process, which is sure to be fast.

3. Hot update

The basic principle is: modify the code -> Node finds that the XXX file has changed -> notify the browser that the XXX file has changed -> the browser receives the message -> the browser requests the XXX file again -> the page automatically refreshes

Full Project Path

Github