关于前端:Vite入门从手写一个乞丐版的Vite开始下

32次阅读

共计 15455 个字符,预计需要花费 39 分钟才能阅读完成。

上一篇 Vite 入门从手写一个乞丐版的 Vite 开始(上)咱们曾经胜利的将页面渲染进去了,这一篇咱们来简略的实现一下热更新的性能。

所谓热更新就是批改了文件,不必刷新页面,页面的某个局部就自动更新了,听着仿佛挺简略的,然而要实现一个很欠缺的热更新还是很简单的,要思考的状况很多,所以本文只会实现一个最根底的热更新成果。

创立 WebSocket 连贯

浏览器显然是不晓得文件有没有批改的,所以须要后端进行推送,咱们先来建设一个 WebSocket 连贯。

// 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和咱们的服务共用一个 http 申请,当接管到 http 协定的降级申请后,判断子协定是否是 vite-hmr,是的话咱们就把创立的WebSocket 实例连贯下来,这个子协定是本人定义的,通过设置子协定,单个服务器能够实现多个 WebSocket 连贯,就能够依据不同的协定解决不同类型的事件,服务端的WebSocket 创立实现当前,客户端也须要创立,然而客户端是不会有这些代码的,所以须要咱们手动注入,创立一个文件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);
});

接下来咱们把这个 client.js 注入到 html 文件,批改之前 html 文件拦挡的逻辑:

// 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");
    }
})

通过 import 的形式引入,所以咱们须要拦挡一下这个申请:

// 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");
    }
})

能够看到曾经连贯胜利。

监听文件扭转

接下来咱们要初始化一下对文件批改的监听,监听文件的扭转应用 chokidar:

// 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 文件批改了})

构建导入依赖图

为什么要构建依赖图呢,很简略,比方一个模块扭转了,仅仅更新它本人必定还不够,依赖它的模块都须要批改才对,要做到这一点天然要能晓得哪些模块依赖它才行。

// 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用来寄存 模块 该模块所依赖的模块 的映射,次要作用是用来删除不再依赖的模块,比方 a 一开始依赖 bc,此时 importerMap 外面存在 b -> ac -> a的映射关系,而后我批改了一下 a,删除了对c 的依赖,那么就须要从 importerMap 外面也同时删除 c -> a 的映射关系,这时就能够通过 importeeMap 来获取到之前的 a -> [b, c] 的依赖关系,跟此次的依赖关系 a -> [b] 进行比对,就能够找出不再依赖的 c 模块,而后在 importerMap 里删除 c -> a 的依赖关系。

接下来咱们从 index.html 页面开始构建依赖图,index.html内容如下:

能够看到它依赖了 main.js,批改拦挡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
        // ...
    }
})

接下来咱们须要别离批改 js 的拦挡办法,注册依赖关系;批改 Vue 单文件的拦挡办法,注册 js 局部的依赖关系,因为上一篇文章里咱们曾经把转换裸导入的逻辑都提取成一个公共函数 parseBareImport 了,所以咱们只有批改这个函数就能够了:

// 解决裸导入
// 减少了 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();};

再来减少一下后面提到的去除不再依赖的关系的逻辑:

// 解决裸导入
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();};

Vue 单文件的热更新

先来实现一下 Vue 单文件的热更新,先监听一下 Vue 单文件的扭转事件:

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

如果批改的文件是以 .vue 结尾,那么就进行解决,怎么解决呢,Vue单文件会解析成 jstemplatestyle 三局部,咱们把解析数据缓存起来,当文件批改了当前会再次进行解析,而后别离和上一次的解析后果进行比拟,判断单文件的哪局部发生变化了,最初给浏览器发送不同的事件,由前端页面来进行不同的解决,缓存咱们应用 lru-cache:

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

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

而后批改一下 Vue 单文件的拦挡办法,减少缓存:

// 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);
        }
        // ...
    }
})

而后就来到 handleVueReload 办法了:

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

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

咱们先转换了一下文件门路,因为监听到的是本地门路,和申请的 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);
};

接着获取了一下缓存数据,而后进行了这一次的解析,并更新缓存,接下来就要判断哪一部分产生了扭转。

热更新 template

咱们先来看一下比较简单的模板热更新:

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]);
}

逻辑很简略,当 template 局部产生扭转后向浏览器发送一个 rerender 事件,带上批改模块的url

当初咱们来批改一下 HelloWorld.vuetemplate看看:

能够看到曾经胜利收到了音讯。

接下来须要批改一下 client.js 文件,减少收到 vue-rerender 音讯后的解决逻辑。

文件更新了,浏览器必定须要申请一下更新的文件,Vite应用的是 import() 办法,然而这个办法 js 自身是没有的,另外笔者没有找到是哪里注入的,所以加载模块的逻辑只能本人来简略实现一下:

// 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);
};

因为要加载的都是 ES 模块,间接申请是不行的,所以创立一个 typemodulescript 标签,来让浏览器加载,这样申请都不必本人发,只有把想方法获取到模块的导出就行了,这个也很简略,创立一个全局函数即可,这个很像 jsonp 的原理。

接下来就能够解决 vue-rerender 音讯了:

// 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;
    }
};

就这么简略,咱们来批改一下 HelloWorld.vue 文件的模板来看看:

能够看到没有刷新页面,然而更新了,接下来具体解释一下原理。

因为咱们批改的是模板局部,所以申请的 urlpayload.path + "?type=template,这个源于上一篇文章里咱们申请 Vue 单文件的模板局部是这么设计的,为什么要加个工夫戳呢,因为不加的话浏览器认为这个模块曾经加载过了,是不会从新申请的。

模板局部的申请后果如下:

导出了一个 render 函数,这个其实就是 HelloWorld.vue 组件的渲染函数,所以咱们通过 module.render 来获取这个函数。

__VUE_HMR_RUNTIME__.rerender这个函数是哪里来的呢,其实来自于 VueVue 非生产环境的源码会提供一个 __VUE_HMR_RUNTIME__ 对象,顾名思义就是用于热更新的,有三个办法:

rerender就是其中一个:

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;
    });
}

外围代码就是下面的 1、2 两行,间接用新的渲染函数笼罩组件旧的渲染函数,而后触发组件更新就达到了热更新的成果。

另外要解释一下其中波及到的 id,须要热更新的组件会被增加到map 里,那怎么判断一个组件是不是须要热更新呢,也很简略,给它增加一个属性即可:

mountComponent 办法里会判断组件是否存在 __hmrId 属性,存在则认为是须要进行热更新的,那么就增加到 map 里,注册办法如下:

这个 __hmrId 属性须要咱们手动增加,所以须要批改一下之前拦挡 Vue 单文件的办法:

// 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`;
        // ...
    }
})

热更新 js

趁热打铁,接下来看一下 Vue 单文件中的 js 局部产生了批改怎么进行热更新。

根本套路是一样的,查看两次的 js 局部是否产生了批改了,批改了则向浏览器发送热更新音讯:

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

js局部产生扭转了就发送一个 vue-reload 音讯,接下来批改 client.js 减少对这个音讯的解决逻辑:

// 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;
  }
}

和模板热更新很相似,只不过是调用 reload 办法,这个办法会略微简单一点:

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();
        }
    });
}

通过正文应该能大略看进去它的原理,通过强制父实例从新渲染、调用根实例的 reload 办法、通过标记为脏组件等等形式来从新渲染组件达到更新的成果。

style 热更新

款式更新的状况比拟多,除了批改款式自身,还有作用域批改了、应用到了 CSS 变量等状况,简略起见,咱们只思考批改了款式自身。

依据上一篇的介绍,Vue单文件中的款式也是通过 js 类型发送到浏览器,而后动态创建 style 标签插入到页面,所以咱们须要能删除之前增加的标签,这就须要给增加的 style 标签减少一个 id 了,批改一下上一篇文章里咱们编写的 insertStyle 办法:

// 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) 作为 id;以及Vue 单文件的 style 局部的拦挡申请,减少 removeQuery(req.url) + '-' + index 作为 id,要加上index 是因为一个 Vue 单文件里可能有多个 style 标签。

接下来持续批改 handleVueReload 办法:

// 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}`,
            })
        }
    })
}

遍历新的款式数据,依据之前的进行比照,如果某个款式块之前没有或者不一样那就发送 style-update 事件,留神 url 须要带上 importtype=style参数,这是上一篇里咱们规定的。

client.js也要配套批改一下:

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

很简略,加上工夫戳从新加载一下款式文件即可。

不过还有个小问题,比方原来有两个 style 块,咱们删掉了一个,目前页面上还是存在的,比方一开始存在两个 style 块:

删掉第二个 style 块,也就是设置背景色彩的那个:

能够看到还是存在,咱们是通过索引来增加的,所以更新后有多少个款式块,就会从头笼罩之前曾经存在的多少个款式块,最初多进去的是不会被删除的,所以须要手动删除不再须要的标签:

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

发送一个 style-remove 事件,告诉页面删除不再须要的标签:

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

能够看到被胜利删掉了。

一般 js 文件的热更新

最初咱们来看一下非 Vue 单文件,一般 js 文件更新后要怎么解决。

减少一个解决 js 热更新的函数:

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

一般 js 热更新就须要用到后面的依赖图数据了,如果监听到某个 js 文件批改了,先判断它是否在依赖图中,不是的话就不必管,是的话就递归获取所有依赖它的模块,因为所有模块的最上层依赖必定是 index.html,如果只是简略的获取所有依赖模块再更新,那么每次都相当于要刷新整个页面了,所以咱们规定如果查看到某个依赖是Vue 单文件,那么就代表反对热更新,否则就相当于走到死胡同,须要刷新整个页面。

// 解决 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;
};

递归获取批改的 js 文件的依赖模块,判断是否反对热更新,反对则发送热更新事件,否则发送刷新整个页面事件,因为可能同时要更新多个模块,所以通过 type=multi 来标识。

看一下递归的办法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;
};

逻辑很简略,就是递归遇到 Vue 单文件就进行,否则持续遍历,直到顶端,代表走到死胡同。

最初再来批改一下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);
  }
});

如果音讯类型是 multi,那么就遍历updates 列表顺次调用解决办法:

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

vue-rerender事件之前曾经有了,所以只须要减少一个刷新整个页面的办法即可。

测试一下,App.vue外面引入一个 test.js 文件:

// 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又引入了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'}

接下来批改 test2.js 测试成果:

能够看到从新发送了申请,然而页面并没有更新,这是为什么呢,其实还是缓存问题:

App.vue导入的两个文件之前曾经申请过了,所以浏览器会间接应用之前申请的后果,并不会从新发送申请,这要怎么解决呢,很简略,能够看到申请的 App.vueurl是带了工夫戳的,所以咱们能够查看申请模块的 url 是否存在工夫戳,存在则把它依赖的所有模块门路也都带上工夫戳,这样就会触发从新申请了,批改一下模块门路转换方法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() : ""}`;// ++
        }
        // ...
    })
    // ...
}

再来测试一下:

能够看到胜利更新了。最初咱们再来测试运行刷新整个页面的状况,批改一下 main.js 文件即可:

总结

本文参考 Vite-1.0.0-rc.5 版本写了一个非常简单的 Vite,简化了十分多的细节,旨在对Vite 及热更新有一个根底的意识,其中必定有不合理或谬误之处,欢送指出~

示例代码在:https://github.com/wanglin2/vite-demo。

正文完
 0