vite hmr实现
上文讲了vite服务端是如何收集依赖的
这次我们讲讲收集依赖之后如何做到热更新
修改文件后,chokidar监听到了文件改动
watcher.on('change', async (file) => {
file = normalizePath$3(file);
if (file.endsWith('/package.json')) {
return invalidatePackageData(packageCache, file);
}
// invalidate module graph cache on file change
moduleGraph.onFileChange(file);
if (serverConfig.hmr !== false) {
try {
await handleHMRUpdate(file, server);
}
catch (err) {
ws.send({
type: 'error',
err: prepareError(err),
});
}
}
});
监听到了
HelloWorld.vue
的改变
然后进到moduleGraph.onFileChange(file)
onFileChange(file) {
const mods = this.getModulesByFile(file);
if (mods) {
const seen = new Set();
mods.forEach((mod) => {
this.invalidateModule(mod, seen);
});
}
}
拿到HelloWorld的mod,进入到invalidateModule
这里有两个mod,一个是HelloWorld.vue,还有一个是它的样式文件
之后递归,将HelloWorld.mod.importers
的模块都收集进seen
之中
invalidateModule(mod, seen = new Set(), timestamp = Date.now(), isHmr = false) {
if (seen.has(mod)) {
return;
}
seen.add(mod);
if (isHmr) {
mod.lastHMRTimestamp = timestamp;
}
else {
// Save the timestamp for this invalidation, so we can avoid caching the result of possible already started
// processing being done for this module
mod.lastInvalidationTimestamp = timestamp;
}
// Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline
// Invalidating the transform result is enough to ensure this module is re-processed next time it is requested
mod.transformResult = null;
mod.ssrTransformResult = null;
mod.ssrModule = null;
mod.ssrError = null;
mod.importers.forEach((importer) => {
if (!importer.acceptedHmrDeps.has(mod)) {
this.invalidateModule(importer, seen, timestamp, isHmr);
}
});
}
最终seen共有四个mod:HelloWorld.style.mod,HelloWorld.mod,App.mod,main.mod
接着进入handleHMRUpdate
初始化上下文
进入handleHotUpdate
插件钩子获取处理结果
这个钩子把HelloWorld.style.mod给过滤掉了
hmrContext.modules 只有HelloWorld.mod
接着执行updateModules
又重新收集了seen,现在去掉了style.mod,只剩下了3个
收集边界
边界只有HelloWorld.vue,并且判断不需要reload
websocket派发更新
首先看客户端的HelloWorld.vue文件
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/components/HelloWorld.vue");
import { ref } from '/node_modules/.vite/deps/vue.js?v=1a7200b1'
const _sfc_main = {
__name: 'HelloWorld',
props: {
msg: String,
},
setup(__props, { expose }) {
expose();
const count = ref(0)
const __returned__ = { count, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
}
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "/node_modules/.vite/deps/vue.js?v=1a7200b1"
const _withScopeId = n => (_pushScopeId("data-v-e17ea971"),n=n(),_popScopeId(),n)
const _hoisted_1 = { class: "card" }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", null, [
/*#__PURE__*/_createTextVNode(" Edit "),
/*#__PURE__*/_createElementVNode("code", null, "components/HelloWorld.vue"),
/*#__PURE__*/_createTextVNode(" to test HMR ")
], -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", null, [
/*#__PURE__*/_createTextVNode(" Check out "),
/*#__PURE__*/_createElementVNode("a", {
href: "https://vuejs.org/guide/quick-start.html#local",
target: "_blank"
}, "create-vue"),
/*#__PURE__*/_createTextVNode(", the official Vue + Vite starter ")
], -1 /* HOISTED */))
const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", null, [
/*#__PURE__*/_createTextVNode(" Install "),
/*#__PURE__*/_createElementVNode("a", {
href: "https://github.com/johnsoncodehk/volar",
target: "_blank"
}, "Volar"),
/*#__PURE__*/_createTextVNode(" in your IDE for a better DX ")
], -1 /* HOISTED */))
const _hoisted_5 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", { class: "read-the-docs" }, "Click on the Vite and Vue logos to learn more", -1 /* HOISTED */))
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString($props.msg), 1 /* TEXT */),
_createElementVNode("div", _hoisted_1, [
_createElementVNode("button", {
type: "button",
onClick: _cache[0] || (_cache[0] = $event => ($setup.count++))
}, "count is " + _toDisplayString($setup.count), 1 /* TEXT */),
_hoisted_2
]),
_hoisted_3,
_hoisted_4,
_hoisted_5
], 64 /* STABLE_FRAGMENT */))
}
import "/src/components/HelloWorld.vue?vue&type=style&index=0&scoped=e17ea971&lang.css"
_sfc_main.__hmrId = "e17ea971"
typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)
import.meta.hot.accept(mod => {
if (!mod) return
const { default: updated, _rerender_only } = mod
if (_rerender_only) {
__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
} else {
__VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
}
})
import _export_sfc from '/@id/__x00__plugin-vue:export-helper'
export default /*#__PURE__*/_export_sfc(_sfc_main, [['render',_sfc_render],['__scopeId',"data-v-e17ea971"],['__file',"c:/Users/mjgao/vite-project/src/components/HelloWorld.vue"]])
注意看,开头注入了createHotContext('/src/components/HelloWorld.vue')
function createHotContext(ownerPath) {
if (!dataMap.has(ownerPath)) {
dataMap.set(ownerPath, {});
}
// when a file is hot updated, a new context is created
// clear its stale callbacks
const mod = hotModulesMap.get(ownerPath);
if (mod) {
mod.callbacks = [];
}
// clear stale custom event listeners
const staleListeners = ctxToListenersMap.get(ownerPath);
if (staleListeners) {
for (const [event, staleFns] of staleListeners) {
const listeners = customListenersMap.get(event);
if (listeners) {
customListenersMap.set(event, listeners.filter((l) => !staleFns.includes(l)));
}
}
}
const newListeners = new Map();
ctxToListenersMap.set(ownerPath, newListeners);
function acceptDeps(deps, callback = () => { }) {
const mod = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
const hot = {
get data() {
return dataMap.get(ownerPath);
},
accept(deps, callback) {
if (typeof deps === 'function' || !deps) {
// self-accept: hot.accept(() => {})
acceptDeps([ownerPath], ([mod]) => deps === null || deps === void 0 ? void 0 : deps(mod));
}
else if (typeof deps === 'string') {
// explicit deps
acceptDeps([deps], ([mod]) => callback === null || callback === void 0 ? void 0 : callback(mod));
}
else if (Array.isArray(deps)) {
acceptDeps(deps, callback);
}
else {
throw new Error(`invalid hot.accept() usage.`);
}
},
// export names (first arg) are irrelevant on the client side, they're
// extracted in the server for propagation
acceptExports(_, callback) {
acceptDeps([ownerPath], ([mod]) => callback === null || callback === void 0 ? void 0 : callback(mod));
},
dispose(cb) {
disposeMap.set(ownerPath, cb);
},
prune(cb) {
pruneMap.set(ownerPath, cb);
},
// Kept for backward compatibility (#11036)
// @ts-expect-error untyped
// eslint-disable-next-line @typescript-eslint/no-empty-function
decline() { },
// tell the server to re-perform hmr propagation from this module as root
invalidate(message) {
notifyListeners('vite:invalidate', { path: ownerPath, message });
this.send('vite:invalidate', { path: ownerPath, message });
console.debug(`[vite] invalidate ${ownerPath}${message ? `: ${message}` : ''}`);
},
// custom events
on(event, cb) {
const addToMap = (map) => {
const existing = map.get(event) || [];
existing.push(cb);
map.set(event, existing);
};
addToMap(customListenersMap);
addToMap(newListeners);
},
send(event, data) {
messageBuffer.push(JSON.stringify({ type: 'custom', event, data }));
sendMessageBuffer();
},
};
return hot;
}
回到HelloWorld的热更新代码
import.meta.hot.accept(mod => {
if (!mod) return
const { default: updated, _rerender_only } = mod
if (_rerender_only) {
__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
} else {
__VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
}
})
这里进到了createHotContext.accept
之中
function acceptDeps(deps, callback = () => { }) {
const mod = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
hotModulesMap
就是从这里收集到的,回调就是HelloWorld的import.meta.hot.accept的回调
客户端收到消息
关键是queueUpdate(fetchUpdate(update))
async function fetchUpdate({ path, acceptedPath, timestamp, explicitImportRequired, }) {
const mod = hotModulesMap.get(path);
if (!mod) {
// In a code-splitting project,
// it is common that the hot-updating module is not loaded yet.
// https://github.com/vitejs/vite/issues/721
return;
}
let fetchedModule;
const isSelfUpdate = path === acceptedPath;
// determine the qualified callbacks before we re-import the modules
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));
if (isSelfUpdate || qualifiedCallbacks.length > 0) {
const disposer = disposeMap.get(acceptedPath);
if (disposer)
await disposer(dataMap.get(acceptedPath));
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`);
try {
fetchedModule = await import(
/* @vite-ignore */
base +
acceptedPathWithoutQuery.slice(1) +
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${query ? `&${query}` : ''}`);
}
catch (e) {
warnFailedFetch(e, acceptedPath);
}
}
return () => {
for (const { deps, fn } of qualifiedCallbacks) {
fn(deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)));
}
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`;
console.debug(`[vite] hot updated: ${loggedPath}`);
};
}
先从hotModulesMap
拿出对应的mod
动态import
发送请求获取最新的改动文件
queueUpdate就是一个任务调度,执行回调
async function queueUpdate(p) {
queued.push(p);
if (!pending) {
pending = true;
await Promise.resolve();
pending = false;
const loading = [...queued];
queued = [];
(await Promise.all(loading)).forEach((fn) => fn && fn());
}
}
这里拿到的mod就是请求回来最新的mod
然后就是vue里面执行最新mod的更新了
转载自:https://juejin.cn/post/7210596158934073405