3
头图

In the previous article, the introduction to Vite started from handwriting a beggar version of Vite (above). We have successfully rendered the page. In this article, we will simply implement the function of hot update.

The so-called hot update is to modify the file without refreshing the page, and a certain part of the page is automatically updated. It sounds simple, but it is still very complicated to achieve a perfect hot update. There are many situations to consider, so This article will only implement a most basic hot update effect.

Create a WebSocket connection

The browser obviously does not know whether the file has been modified, so it needs to be pushed by the backend. Let's first establish a WebSocket connection.

 // app.js
const server = http.createServer(app);
const WebSocket = require("ws");

// 创建WebSocket服务
const createWebSocket = () => {
    // 创建一个服务实例
    const wss = new WebSocket.Server({ noServer: true });// 不用额外创建http服务,直接使用我们自己创建的http服务

    // 接收到http的协议升级请求
    server.on("upgrade", (req, socket, head) => {
        // 当子协议为vite-hmr时就处理http的升级请求
        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" }));
    });

    // 发送消息方法
    const sendMsg = (payload) => {
        const stringified = JSON.stringify(payload, null, 2);

        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(stringified);
            }
        });
    };

    return {
        wss,
        sendMsg,
    };
};
const { wss, sendMsg } = createWebSocket();

server.listen(3000);

WebSocket share a http request with our service, when receiving an upgrade request for the http protocol, determine whether the sub-protocol is vite-hmr , if yes, we will connect the created WebSocket instance. This sub-protocol is defined by ourselves. By setting the sub-protocol, a single server can realize multiple WebSocket connections, and you can connect according to Different protocols handle different types of things. After the server's WebSocket is created, the client also needs to be created, but the client will not have these codes, so we need to manually inject and create a client.js :

 // client.js

// vite-hmr代表自定义的协议字符串
const socket = new WebSocket("ws://localhost:3000/", "vite-hmr");

socket.addEventListener("message", async ({ data }) => {
  const payload = JSON.parse(data);
});

Next we inject this client.js into the html file, before modifying the logic of html file interception:

 // app.js
const clientPublicPath = "/client.js";

app.use(async function (req, res, next) {
    // 提供html页面
    if (req.url === "/index.html") {
        let html = readFile("index.html");
        const devInjectionCode = `\n<script type="module">import "${clientPublicPath}"</script>\n`;
        html = html.replace(/<head>/, `$&${devInjectionCode}`);
        send(res, html, "html");
    }
})

Introduced by import , so we need to intercept this request:

 // app.js
app.use(async function (req, res, next) {
    if (req.url === clientPublicPath) {
        // 提供client.js
        let js = fs.readFileSync(path.join(__dirname, "./client.js"), "utf-8");
        send(res, js, "js");
    }
})

You can see that the connection has been successful.

Monitor file changes

Next, we need to initialize the monitoring of file modifications, and use chokidar to monitor file changes:

 // app.js
const chokidar = require(chokidar);

// 创建文件监听服务
const createFileWatcher = () => {
  const watcher = chokidar.watch(basePath, {
    ignored: [/node_modules/, /\.git/],
    awaitWriteFinish: {
      stabilityThreshold: 100,
      pollInterval: 10,
    },
  });
  return watcher;
};
const watcher = createFileWatcher();

watcher.on("change", (file) => {
    // file文件修改了
})

Build import dependency graph

Why build a dependency graph? It's very simple. For example, if a module changes, it's not enough to just update itself. All modules that depend on it need to be modified. To do this, it is natural to know which modules depend on it.

 // app.js
const importerMap = new Map();
const importeeMap = new Map();

// map : key -> set
// map : 模块 -> 依赖该模块的模块集合
const ensureMapEntry = (map, key) => {
  let entry = map.get(key);
  if (!entry) {
    entry = new Set();
    map.set(key, entry);
  }
  return entry;
};

需要用到的变量和函数就是上面几个, importerMap 模块依赖它的模块的映射; importeeMap用To store the mapping from 模块 to 该模块所依赖的模块 , the main function is to delete modules that are no longer dependent, such as a at first depend on b cimporterMap b -> a c -> a的映射关系, a , delete the dependency on c , then you need to delete the mapping relationship of c -> a from importerMap at the same time, then you can pass importeeMap To get the dependency of the previous a -> [b, c] , and compare it with this dependency a -> [b] , you can find out the dependency c importerMap c c -> a dependencies.

Next, we start to build the dependency graph from the index.html page, index.html The content is as follows:

You can see that it depends on main.js , and modify the method of intercepting html :

 // app.js
app.use(async function (req, res, next) {
    // 提供html页面
    if (req.url === "/index.html") {
        let html = readFile("index.html");
        // 查找模块依赖图
        const scriptRE = /(<script\b[^>]*>)([\s\S]*?)<\/script>/gm;
        const srcRE = /\bsrc=(?:"([^"]+)"|'([^']+)'|([^'"\s]+)\b)/;
        // 找出script标签
        html = html.replace(scriptRE, (matched, openTag) => {
            const srcAttr = openTag.match(srcRE);
            if (srcAttr) {
                // 创建script到html的依赖关系
                const importee = removeQuery(srcAttr[1] || srcAttr[2]);
                ensureMapEntry(importerMap, importee).add(removeQuery(req.url));
            }
            return matched;
        });
        // 注入client.js
        // ...
    }
})

Next, we need to modify the interception method of js and register the dependencies; modify the interception method of Vue single file, and register the dependencies of the js part, because the above In an article, we have extracted the logic of converting bare imports into a public function parseBareImport , so we just need to modify this function:

 // 处理裸导入
// 增加了importer入参,req.url
const parseBareImport = async (js, importer) => {
    await init;
    let parseResult = parseEsModule(js);
    let s = new MagicString(js);
    importer = removeQuery(importer);// ++
    parseResult[0].forEach((item) => {
        let url = "";
        if (item.n[0] !== "." && item.n[0] !== "/") {
            url = `/@module/${item.n}?import`;
        } else {
            url = `${item.n}?import`;
        }
        s.overwrite(item.s, item.e, url);
        // 注册importer模块所以依赖的模块到它的映射关系
        ensureMapEntry(importerMap, removeQuery(url)).add(importer);// ++
    });
    return s.toString();
};

Let's add the previously mentioned logic of removing the relationship that is no longer dependent:

 // 处理裸导入
const parseBareImport = async (js, importer) => {
    // ...
    importer = removeQuery(importer);
    // 上一次的依赖集合
    const prevImportees = importeeMap.get(importer);// ++
    // 这一次的依赖集合
    const currentImportees = new Set();// ++
    importeeMap.set(importer, currentImportees);// ++
    parseResult[0].forEach((item) => {
        // ...
        let importee = removeQuery(url);// ++
        // url -> 依赖
        currentImportees.add(importee);// ++
        // 依赖 -> url
        ensureMapEntry(importerMap, importee).add(importer);
    });
    // 删除不再依赖的关系++
    if (prevImportees) {
        prevImportees.forEach((importee) => {
            if (!currentImportees.has(importee)) {
                // importer不再依赖importee,所以要从importee的依赖集合中删除importer
                const importers = importerMap.get(importee);
                if (importers) {
                    importers.delete(importer);
                }
            }
        });
    }
    return s.toString();
};

Hot update of Vue single file

Let’s first realize the hot update of Vue single file, first listen to the change event of Vue single file:

 // app.js
// 监听文件改变
watcher.on("change", (file) => {
  if (file.endsWith(".vue")) {
    handleVueReload(file);
  }
});

If the modified file ends with .vue Vue js process it. How to deal with it template , style Three parts, we cache the parsed data, when the file is modified, it will be parsed again, and then compared with the previous parsing results, to determine which part of the single file has changed, Finally, different events are sent to the browser, and the front-end pages are used for different processing. For caching, we use lru-cache :

 // app.js
const LRUCache = require("lru-cache");

// 缓存Vue单文件的解析结果
const vueCache = new LRUCache({
  max: 65535,
});

Then modify the Vue single-file interception method and increase the cache:

 // app.js
app.use(async function (req, res, next) {
    if (/\.vue\??[^.]*$/.test(req.url)) {
        // ...
        // vue单文件
        let descriptor = null;
        // 如果存在缓存则直接使用缓存
        let cached = vueCache.get(removeQuery(req.url));
        if (cached) {
            descriptor = cached;
        } else {
            // 否则进行解析,并且将解析结果进行缓存
            descriptor = parseVue(vue).descriptor;
            vueCache.set(removeQuery(req.url), descriptor);
        }
        // ...
    }
})

Then came the handleVueReload method:

 // 处理Vue单文件的热更新
const handleVueReload = (file) => {
  file = filePathToUrl(file);
};

// 处理文件路径到url
const filePathToUrl = (file) => {
  return file.replace(/\\/g, "/").replace(/^\.\.\/test/g, "");
};

We first converted the file path, because the local path was monitored, which is different from the requested url :

 const handleVueReload = (file) => {
  file = filePathToUrl(file);
  // 获取上一次的解析结果
  const prevDescriptor = vueCache.get(file);
  // 从缓存中删除上一次的解析结果
  vueCache.del(file);
  if (!prevDescriptor) {
    return;
  }
  // 解析
  let vue = readFile(file);
  descriptor = parseVue(vue).descriptor;
  vueCache.set(file, descriptor);
};

Then get the cached data, then perform the analysis this time, and update the cache. Next, it is necessary to determine which part has changed.

Hot update template

Let's take a look at a relatively simple template hot update:

 const handleVueReload = (file) => {
    // ...
    // 检查哪部分发生了改变
    const sendRerender = () => {
        sendMsg({
            type: "vue-rerender",
            path: file,
        });
    };
    // template改变了发送rerender事件
    if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
        return sendRerender();
    }
}

// 判断Vue单文件解析后的两个部分是否相同
function isEqualBlock(a, b) {
    if (!a && !b) return true;
    if (!a || !b) return false;
    if (a.src && b.src && a.src === b.src) return true;
    if (a.content !== b.content) return false;
    const keysA = Object.keys(a.attrs);
    const keysB = Object.keys(b.attrs);
    if (keysA.length !== keysB.length) {
        return false;
    }
    return keysA.every((key) => a.attrs[key] === b.attrs[key]);
}

The logic is very simple, when the template part is changed, send a rerender event to the browser, with the modified module url .

Now let's modify the HelloWorld.vue of template and see:

You can see that the message has been successfully received.

Next, you need to modify the client.js file, and add the processing logic after receiving the vue-rerender message.

The file has been updated, the browser definitely needs to request the updated file, Vite uses the import() method, but this method js itself is not available, and the author does not I didn't find where it was injected, so the logic of loading the module can only be simply implemented by myself:

 // client.js
// 回调id
let callbackId = 0;
// 记录回调
const callbackMap = new Map();
// 模块导入后调用的全局方法
window.onModuleCallback = (id, module) => {
  document.body.removeChild(document.getElementById("moduleLoad"));
  // 执行回调
  let callback = callbackMap.get(id);
  if (callback) {
    callback(module);
  }
};

// 加载模块
const loadModule = ({ url, callback }) => {
  // 保存回调
  let id = callbackId++;
  callbackMap.set(id, callback);
  // 创建一个模块类型的script
  let script = document.createElement("script");
  script.type = "module";
  script.id = "moduleLoad";
  script.innerHTML = `
        import * as module from '${url}'
        window.onModuleCallback(${id}, module)
    `;
  document.body.appendChild(script);
};

Because all the modules to be loaded are ES , direct request is not possible, so create a type for module script Let the browser load it, so that you don't have to send the request yourself, you just need to find a way to get the export of the module. This is also very simple. You can create a global function, which is very similar to the principle of jsonp .

Then you can process the vue-rerender message:

 // app.js
socket.addEventListener("message", async ({ data }) => {
    const payload = JSON.parse(data);
    handleMessage(payload);
});

const handleMessage = (payload) => {
    switch (payload.type) {
        case "vue-rerender":
            loadModule({
                url: payload.path + "?type=template&t=" + Date.now(),
                callback: (module) => {
                    window.__VUE_HMR_RUNTIME__.rerender(payload.path, module.render);
                },
            });
            break;
    }
};

It's that simple, let's modify the template of the HelloWorld.vue file to see:

You can see that the page has not been refreshed, but it has been updated. Next, I will explain the principle in detail.

Because we are modifying the template part, the requested url is payload.path + "?type=template , which comes from our request in the previous article Vue the template part of the single file is It is designed like this, why add a timestamp, because if you don't add it, the browser thinks that the module has already been loaded and will not re-request.

The result of the request in the template part is as follows:

A render function is exported, which is actually the rendering function of the HelloWorld.vue component, so we get this function through module.render .

__VUE_HMR_RUNTIME__.rerender是哪里来的呢,其实来自VueVue非生产环境的源码会__VUE_HMR_RUNTIME__ , As the name suggests, it is used for hot update. There are three methods:

rerender is one of them:

 function rerender(id, newRender) {
    const record = map.get(id);
    if (!record)
        return;
    Array.from(record).forEach(instance => {
        if (newRender) {
            instance.render = newRender;// 1
        }
        instance.renderCache = [];
        isHmrUpdating = true;
        instance.update();// 2
        isHmrUpdating = false;
    });
}

The core code is the above 1、2 two lines, directly overwrite the old rendering function of the component with the new rendering function, and then trigger the component update to achieve the effect of hot update.

In addition, I want to explain the involved id , the components that need hot update will be added to map , then how to judge whether a component needs hot update is also very simple, Just add a property to it:

In the mountComponent method, it will judge whether the component exists __hmrId attribute, if it exists, it is considered that it needs to be hot updated, then add it to map , the registration method is as follows :

This __hmrId property needs to be added manually, so we need to modify it before intercepting Vue Single file method:

 // app.js
app.use(async function (req, res, next) {
    if (/\.vue\??[^.]*$/.test(req.url)) {
        // vue单文件
        // ...
        // 添加热更新标志
        code += `\n__script.__hmrId = ${JSON.stringify(removeQuery(req.url))}`;// ++
        // 导出
        code += `\nexport default __script`;
        // ...
    }
})

hot update js

Strike while the iron is hot, let's take a look at the Vue js part of the single file has been modified to perform a hot update.

The basic routine is the same, check whether the part of js has been modified, and if it is modified, send a hot update message to the browser:

 // app.js
const handleVueReload = (file) => {
    const sendReload = () => {
        sendMsg({
            type: "vue-reload",
            path: file,
        });
    };
    // js部分发生了改变发送reload事件
    if (!isEqualBlock(descriptor.script, prevDescriptor.script)) {
        return sendReload();
    }
}

js the part changes, send a vue-reload message, then modify client.js add the processing logic for this message:

 // client.js
const handleMessage = (payload) => {
  switch (payload.type) {
    case "vue-reload":
      loadModule({
        url: payload.path + "?t=" + Date.now(),
        callback: (module) => {
          window.__VUE_HMR_RUNTIME__.reload(payload.path, module.default);
        },
      });
      break;
  }
}

Similar to template hot update, but calling the reload method, which is a little more complicated:

 function reload(id, newComp) {
    const record = map.get(id);
    if (!record)
        return;
    Array.from(record).forEach(instance => {
        const comp = instance.type;
        if (!hmrDirtyComponents.has(comp)) {
            // 更新原组件
            extend(comp, newComp);
            for (const key in comp) {
                if (!(key in newComp)) {
                    delete comp[key];
                }
            }
            // 标记为脏组件,在虚拟DOM树patch的时候会直接替换
            hmrDirtyComponents.add(comp);
            // 重新加载后取消标记组件
            queuePostFlushCb(() => {
                hmrDirtyComponents.delete(comp);
            });
        }
        if (instance.parent) {
            // 强制父实例重新渲染
            queueJob(instance.parent.update);
        }
        else if (instance.appContext.reload) {
            // 通过createApp()装载的根实例具有reload方法
            instance.appContext.reload();
        }
        else if (typeof window !== 'undefined') {
            window.location.reload();
        }
    });
}

You should be able to see its principle through the annotations, by forcing the parent instance to re-render, calling the reload method of the root instance, and re-rendering the component by marking it as a dirty component to achieve the update effect.

style hot update

There are many cases of style update. In addition to modifying the style itself, there are also cases where the scope has been modified and used CSS variables. For simplicity, we only consider modifying the style itself.

According to the introduction of the previous article, Vue The style in the single file is also sent to the browser through the js type, and then dynamically created style tag is inserted into the page, so We need to be able to delete the previously added tag, which requires adding a tag style id , and modify the one we wrote in the previous article insertStyle Method:

 // app.js
// css to js
const cssToJs = (css, id) => {
  return `
    const insertStyle = (css) => {
        // 删除之前的标签++
        if ('${id}') {
          let oldEl = document.getElementById('${id}')
          if (oldEl) document.head.removeChild(oldEl)
        }
        let el = document.createElement('style')
        el.setAttribute('type', 'text/css')
        el.id = '${id}' // ++
        el.innerHTML = css
        document.head.appendChild(el)
    }
    insertStyle(\`${css}\`)
    export default insertStyle
  `;
};

style标签增加一个id ,然后添加之前先删除之前的标签, css removeQuery(req.url) idVue单文件的style请求, removeQuery(req.url) + '-' + index 2b724925165eb2024eb3e17c7adabb9e---作为id , to add index because of one Vue there may be multiple style tags in a single file.

Next, continue to modify the handleVueReload method:

 // app.js
const handleVueReload = (file) => {
    // ...
    // style部分发生了改变
    const prevStyles = prevDescriptor.styles || []
    const nextStyles = descriptor.styles || []
    nextStyles.forEach((_, i) => {
        if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
            sendMsg({
                type: 'style-update',
                path: `${file}?import&type=style&index=${i}`,
            })
        }
    })
}

Traverse the new style data and compare it according to the previous one. If a style block is not available before or is different, then send style-update event, note url need to bring import type=style parameters, which we specified in the previous article.

client.js also need to modify it:

 // client.js
const handleMessage = (payload) => {
    switch (payload.type) {
        case "style-update":
            loadModule({
                url: payload.path + "&t=" + Date.now(),
            });
            break; 
    }
}

It's very simple, just reload the style file with the timestamp.

However, there is still a small problem. For example, there were originally two style blocks. We deleted one, but there are still two blocks on the page. For example, there were two style blocks at the beginning:

Delete the second style block, which is the one that sets the background color:

It can be seen that it still exists. We add it by index, so how many style blocks there are after the update will overwrite the number of style blocks that already existed before, and the last extra ones will not be deleted, so you need to manually Delete tags that are no longer needed:

 // app.js
const handleVueReload = (file) => {
    // ...
    // 删除已经被删掉的样式块
    prevStyles.slice(nextStyles.length).forEach((_, i) => {
        sendMsg({
            type: 'style-remove',
            path: file,
            id: `${file}-${i + nextStyles.length}`
        })
    })
}

Send a style-remove event to notify the page to delete tags that are no longer needed:

 // client.js
const handleMessage = (payload) => {
    switch (payload.type) {
        case "style-remove":
            document.head.removeChild(document.getElementById(payload.id));
            break;
    }
}

It can be seen that it was successfully deleted.

Hot update of common js files

Finally, let's take a look at how to deal with non Vue single file, ordinary js file after the file is updated.

Add a function to handle js hot update:

 // app.js
// 监听文件改变
watcher.on("change", (file) => {
  if (file.endsWith(".vue")) {
    handleVueReload(file);
  } else if (file.endsWith(".js")) {// ++
    handleJsReload(file);// ++
  }
});

Ordinary js hot update needs to use the previous dependency graph data, if a js file is modified, first judge whether it is in the dependency graph, if not, don't use it If yes, then recursively get all the modules that depend on it, because the top-level dependencies of all modules must be index.html , if you simply get all the dependent modules and update them, then each time it is equivalent to refreshing the entire The page is up, so we stipulate that if a dependency is detected as Vue single file, then it means that hot update is supported, otherwise it is equivalent to reaching a dead end and the entire page needs to be refreshed.

 // 处理js文件的热更新
const handleJsReload = (file) => {
  file = filePathToUrl(file);
  // 因为构建依赖图的时候有些是以相对路径引用的,而监听获取到的都是绝对路径,所以稍微兼容一下
  let importers = getImporters(file);
  // 遍历直接依赖
  if (importers && importers.size > 0) {
    // 需要进行热更新的模块
    const hmrBoundaries = new Set();
    // 递归依赖图获取要更新的模块
    const hasDeadEnd = walkImportChain(importers, hmrBoundaries);
    const boundaries = [...hmrBoundaries];
    // 无法热更新,刷新整个页面
    if (hasDeadEnd) {
      sendMsg({
        type: "full-reload",
      });
    } else {
      // 可以热更新
      sendMsg({
        type: "multi",// 可能有多个模块,所以发送一个multi类型的消息
        updates: boundaries.map((boundary) => {
          return {
            type: "vue-reload",
            path: boundary,
          };
        }),
      });
    }
  }
};

// 获取模块的直接依赖模块
const getImporters = (file) => {
  let importers = importerMap.get(file);
  if (!importers || importers.size <= 0) {
    importers = importerMap.get("." + file);
  }
  return importers;
};

Recursively obtain the dependency module of the modified js file, determine whether hot update is supported, and send a hot update event if it supports it, otherwise, send an event to refresh the entire page, because multiple modules may be updated at the same time, so pass type=multi to identify.

Take a look at the recursive method walkImportChain :

 // 递归遍历依赖图
const walkImportChain = (importers, hmrBoundaries, currentChain = []) => {
  for (const importer of importers) {
    if (importer.endsWith(".vue")) {
      // 依赖是Vue单文件那么支持热更新,添加到热更新模块集合里
      hmrBoundaries.add(importer);
    } else {
      // 获取依赖模块的再上层用来模块
      let parentImpoters = getImporters(importer);
      if (!parentImpoters || parentImpoters.size <= 0) {
        // 如果没有上层依赖了,那么代表走到死胡同了
        return true;
      } else if (!currentChain.includes(importer)) {
        // 通过currentChain来存储已经遍历过的模块
        // 递归再上层的依赖
        if (
          walkImportChain(
            parentImpoters,
            hmrBoundaries,
            currentChain.concat(importer)
          )
        ) {
          return true;
        }
      }
    }
  }
  return false;
};

The logic is very simple, that is, when recursively encounters Vue a single file will be stopped, otherwise it will continue to traverse until it reaches the top, which means it has reached a dead end.

Finally, modify it again client.js :

 // client.js
socket.addEventListener("message", async ({ data }) => {
  const payload = JSON.parse(data);
  // 同时需要更新多个模块
  if (payload.type === "multi") {// ++
    payload.updates.forEach(handleMessage);// ++
  } else {
    handleMessage(payload);
  }
});

If the message type is multi , then traverse the updates list and call the processing method in turn:

 // client.js
const handleMessage = (payload) => {
    switch (payload.type) {
        case "full-reload":
            location.reload();
            break;
    }
}

vue-rerender The event already exists before, so just add a method to refresh the entire page.

Test it, App.vue introduce a test.js file:

 // App.vue
<script>
import test from "./test.js";

export default {
  data() {
    return {
      text: "",
    };
  },
  mounted() {
    this.text = test();
  },
};
</script>

<template>
  <div>
    <p>{{ text }}</p>
  </div>
</template>

test.js introduced test2.js :

 // test.js
import test2 from "./test2.js";

export default function () {
  let a = test2();
  let b = "我是测试1";
  return a + " --- " + b;
}

// test2.js
export default function () {
    return '我是测试2'
}

Next modify test2.js Test effect:

You can see that the request was resent, but the page was not updated. Why is this, it is actually a cache problem:

App.vue The two imported files have been requested before, so the browser will directly use the result of the previous request and will not resend the request. How to solve this, it is very simple, you can see the request App.vue url is timestamped, so we can check the request module's url whether there is a timestamp, and if so, put all the module paths it depends on They are also timestamped, which will trigger a re-request. Modify the module path conversion method parseBareImport :

 // app.js
// 处理裸导入
const parseBareImport = async (js, importer) => {
    // ...
    // 检查模块url是否存在时间戳
    let hast = checkQueryExist(importer, "t");// ++
    // ...
    parseResult[0].forEach((item) => {
        let url = "";
        if (item.n[0] !== "." && item.n[0] !== "/") {
            url = `/@module/${item.n}?import${hast ? "&t=" + Date.now() : ""}`;// ++
        } else {
            url = `${item.n}?import${hast ? "&t=" + Date.now() : ""}`;// ++
        }
        // ...
    })
    // ...
}

Let's test it again:

You can see that the update was successful. Finally, let's test the situation of running and refreshing the entire page, and modify the main.js file:

Summarize

This article refers to Vite-1.0.0-rc.5 version and writes a very simple Vite , which simplifies a lot of details and aims to have a basic understanding of Vite and hot updates. There must be something unreasonable or wrong, please point out~

The sample code is at: https://github.com/wanglin2/vite-demo .


街角小林
886 声望773 粉丝