我正在参加「掘金·启航计划」
本文基于
vite 4.3.0-beta.1版本的源码进行分析
前言
在「vite4源码」dev模式整体流程浅析(一)的文章中,我们已经分析了预构建、请求拦截以及常见的插件源码,在本文中,我们将详细分析vite开发模式下的热更新逻辑
5. 热更新HMR
5.1 服务器启动
启动热更新WebSocketServer服务,启动文件监控
createWebsocketServer()启动websocket服务- 使用
chokidar.watch()监听文件变化
当文件变化时,最终触发handleHMRUpdate()方法
async function createServer(inlineConfig = {}) {
const ws = createWebSocketServer(httpServer, config, httpsOptions);
const watcher = chokidar.watch(
[root, ...config.configFileDependencies, path$o.join(config.envDir, '.env*')],
resolvedWatchOptions);
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);
await onHMRUpdate(file, false);
});
}
const onHMRUpdate = async (file, configOnly) => {
if (serverConfig.hmr !== false) {
await handleHMRUpdate(file, server, configOnly);
}
};
5.2 服务器拦截浏览器请求然后注入代码
5.2.1 拦截index.html注入@vite/client.js
在初始化createServer()中,先注册了中间件middlewares.use(indexHtmlMiddleware(server))
在浏览器加载初始化页面index.html时,会触发indexHtmlMiddleware()的viteIndexHtmlMiddleware()对index.html进行拦截:
- 先使用
fsp.readFile(filename)读取index.html文件内容 - 然后使用
transformIndexHtml(),也就是createDevHtmlTransformFn()重写index.html文件内容 - 最终将重写完成的
index.html文件返回给浏览器进行加载
async function createServer(inlineConfig = {}) {
const middlewares = connect();
const server = {
...
}
server.transformIndexHtml = createDevHtmlTransformFn(server);
if (config.appType === 'spa' || config.appType === 'mpa') {
middlewares.use(indexHtmlMiddleware(server));
}
return server;
}
function indexHtmlMiddleware(server) {
return async function viteIndexHtmlMiddleware(req, res, next) {
if (res.writableEnded) {
return next();
}
const url = req.url && cleanUrl(req.url);
if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
const filename = getHtmlFilename(url, server);
// 读取index.html文件
let html = await fsp.readFile(filename, 'utf-8');
// 改写index.html文件
html = await server.transformIndexHtml(url, html, req.originalUrl);
// 返回index.html文件
return send$1(req, res, html, 'html', {
headers: server.config.server.headers,
});
}
next();
};
}
改写index.html的方法transformIndexHtml()虽然逻辑非常简单,但是代码非常冗长,因此这里不会具体到每一个方法进行分析
核心逻辑为从resolveHtmlTransforms()中拿到很多hooks,然后使用applyHtmlTransforms()遍历所有hook,根据hook(html,ctx)执行结果,进行数据在index.html的插入(插入到<head>或者插入到<body>),然后返回改造后的index.html
function createDevHtmlTransformFn(server) {
const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(server.config.plugins);
return (url, html, originalUrl) => {
return applyHtmlTransforms(html, [
preImportMapHook(server.config),
...preHooks,
htmlEnvHook(server.config),
devHtmlHook,
...normalHooks,
...postHooks,
postImportMapHook(),
], {
path: url,
filename: getHtmlFilename(url, server),
server,
originalUrl,
});
};
}
async function applyHtmlTransforms(html, hooks, ctx) {
for (const hook of hooks) {
const res = await hook(html, ctx);
//...省略对res类型的判断逻辑
html = res.html || html;
tags = res.tags;
//..根据类型tags进行数据的组装,判断是要插入<head>还是插入<body>
html = injectToHead(html, headPrependTags, true);
html = injectToHead(html, headTags);
html = injectToBody(html, bodyPrependTags, true);
html = injectToBody(html, bodyTags);
}
return html;
}
我们经过调试知道,我们
inject的内容是@vite/client,那么是在哪个方法进行注入的呢?
在devHtmlHook()这个hook中,我们进行html的处理,然后返回数据{html, tags}
其中返回的tags数据中就包含了我们的/@vite/client以及对应要插入的位置和一些属性,最终会触发上面分析的applyHtmlTransforms()->injectToHead()方法
const devHtmlHook = async (html, { path: htmlPath, filename, server, originalUrl }) => {
//...
await traverseHtml(html, filename, (node) => {
if (!nodeIsElement(node)) {
return;
}
// 处理<script>标签,添加时间戳?t=xxx,以及触发预加载preTransformRequest()
// 处理<style>标签,添加到styleUrl数组中
// 处理其它attrs标签
});
await Promise.all(styleUrl.map(async ({ start, end, code }, index) => {
const url = `${proxyModulePath}?html-proxy&direct&index=${index}.css`;
// 处理缓存
const mod = await moduleGraph.ensureEntryFromUrl(url, false);
ensureWatchedFile(watcher, mod.file, config.root);
// 转化style数据,触发vite:css插件进行transform()
const result = await server.pluginContainer.transform(code, mod.id);
// 重写s字符串
s.overwrite(start, end, result?.code || '');
}));
html = s.toString();
return {
html,
tags: [
{
tag: 'script',
attrs: {
type: 'module',
src: path$o.posix.join(base, "/@vite/client"),
},
injectTo: 'head-prepend',
},
],
};
};
而injectToHead()的具体代码如下所示,本质也是使用正则表达式进行index.html内容的替换,将对应的tag、type、src添加到指定位置中
function injectToHead(html, tags, prepend = false) {
if (tags.length === 0)
return html;
if (prepend) {
// inject as the first element of head
if (headPrependInjectRE.test(html)) {
return html.replace(headPrependInjectRE, (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}`);
}
}
else {
// inject before head close
if (headInjectRE.test(html)) {
// respect indentation of head tag
return html.replace(headInjectRE, (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}`);
}
// try to inject before the body tag
if (bodyPrependInjectRE.test(html)) {
return html.replace(bodyPrependInjectRE, (match, p1) => `${serializeTags(tags, p1)}\n${match}`);
}
}
// if no head tag is present, we prepend the tag for both prepend and append
return prependInjectFallback(html, tags);
}
function serializeTags(tags, indent = '') {
if (typeof tags === 'string') {
return tags;
}
else if (tags && tags.length) {
return tags.map((tag) => `${indent}${serializeTag(tag, indent)}\n`).join('');
}
return '';
}
插入/@vite/client后改造的index.html为:
5.2.2 vite:import-analysis插件注入热更新代码
name: 'vite:import-analysis',
async transform(source, importer, options) {
let imports;
let exports;
[imports, exports] = parse$e(source);
for (let index = 0; index < imports.length; index++) {
const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex,
n: specifier, a: assertIndex, } = imports[index];
// resolvedId="/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/node_modules/.vite/deps/vue.js?v=da0b3f8b"
// url="/node_modules/.vite/deps/vue.js?v=da0b3f8b"
const [url, resolvedId] = await normalizeUrl(specifier, start);
if (!isDynamicImport) {
// for pre-transforming
staticImportedUrls.add({ url: hmrUrl, id: resolvedId });
}
}
if (hasHMR && !ssr) {
// inject hot context
str().prepend(`import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
`import.meta.hot = __vite__createHotContext(${JSON.stringify(normalizeHmrUrl(importerModule.url))});`);
}
if (config.server.preTransformRequests && staticImportedUrls.size) {
staticImportedUrls.forEach(({ url }) => {
url = removeImportQuery(url);
transformRequest(url, server, { ssr }).catch((e) => {
});
});
}
}
比如Index.vue示例代码中注入:
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/Index.vue");
5.2.3 vite:vue插件注入hot.accept热更新代码
对于每一个.vue文件,都会走vite:vue的插件解析,在对应的transform()转化代码中,会注入对应的import.meta.hot.accept热更新代码
name: 'vite:vue',
async transform(code, id, opt) {
//...
if (!query.vue) {
return transformMain(
code,
filename,
options,
this,
ssr,
customElementFilter(filename)
);
} else {
//...
}
}
async function transformMain(code, filename, options, pluginContext, ssr, asCustomElement) {
//...处理<script>、<style>、<template>,然后放入到output中
const output = [
scriptCode,
templateCode,
stylesCode,
customBlocksCode
];
if (devServer && devServer.config.server.hmr !== false && !ssr && !isProduction) {
output.push(`_sfc_main.__hmrId = ${JSON.stringify(descriptor.id)}`);
output.push(
`typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)`
);
if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
output.push(`export const _rerender_only = true`);
}
output.push(
`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)`,
` }`,
`})`
);
}
//...
let resolvedCode = output.join("\n");
return {
code: resolvedCode
};
}
比如Index.vue文件就注入:
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)
}
})
5.3 @vite/client加载后的执行逻辑
当我们注入
@vite/client到index.html的<script>后,我们会运行@vite/client代码,然后我们会执行什么逻辑呢?
建立客户端的WebSocket,添加常见的事件: open、message、close等
当文件发生改变时,会触发message事件回调,然后触发handleMessage()进行处理
socket = setupWebSocket(socketProtocol, socketHost, fallback);
function setupWebSocket(protocol, hostAndPath, onCloseWithoutOpen) {
const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'vite-hmr');
let isOpened = false;
socket.addEventListener('open', () => {
isOpened = true;
}, { once: true });
// Listen for messages
socket.addEventListener('message', async ({ data }) => {
handleMessage(JSON.parse(data));
});
return socket;
}
5.4 非index.html注入代码,执行局部热更新操作
而在5.2步骤的分析中,我们知道除了在index.html入口文件注入@vite/client后,
我们还在其它文件注入了热更新代码,这些热更新代码主要为createHotContext()和accept()方法,如下所示,从@vite/client获取暴露出来的接口,然后使用@vite/client这些接口进行局部热更新操作
@vite/client加载后有直接运行的代码,进行WebSocket客户端的创建,同时也提供了一些外部可以使用的接口,可以在不同的文件,比如main.js、Index.vue中使用@vite/client提供的外部接口进行局部热更新
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/Index.vue");
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(),在createHotContext()的源码中,我们使用目前文件路径作为key,获取对应的hot对象
从createHotContext()获取hot对象并且赋值给import.meta.hot后,会进行import.meta.hot.accept()的监听,最终触发时会执行acceptDeps()方法,进行当前ownerPath的callbacks收集
那
accept()收集的callbacks什么时候会被触发呢?在下面5.6.1 fetchUpdate将展开分析
function createHotContext(ownerPath) {
function acceptDeps(deps, callback = () => { }) {
const mod = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
const hot = {
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.`);
}
}
};
return hot;
}
5.5 文件改变,服务器处理逻辑
如果改变的文件是"package.json",触发invalidatePackageData(),将"package.json"缓存在packageCache的数据进行删除,不会触发任何热更新逻辑
如果改变的不是"package.json",则会触发onHMRUpdate()->handleHMRUpdate()逻辑
function invalidatePackageData(packageCache, pkgPath) {
packageCache.delete(pkgPath);
const pkgDir = path$o.dirname(pkgPath);
packageCache.forEach((pkg, cacheKey) => {
if (pkg.dir === pkgDir) {
packageCache.delete(cacheKey);
}
});
}
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);
await onHMRUpdate(file, false);
});
const onHMRUpdate = async (file, configOnly) => {
if (serverConfig.hmr !== false) {
await handleHMRUpdate(file, server, configOnly);
}
};
5.5.1 重启服务|全量更新|局部热更新updateModules
async function handleHMRUpdate(file, server, configOnly) {
const { ws, config, moduleGraph } = server;
const shortFile = getShortName(file, config.root);
const fileName = path$o.basename(file);
const isConfig = file === config.configFile;
const isConfigDependency = config.configFileDependencies.some((name) => file === name);
const isEnv = config.inlineConfig.envFile !== false &&
(fileName === '.env' || fileName.startsWith('.env.'));
if (isConfig || isConfigDependency || isEnv) {
await server.restart();
return;
}
if (configOnly) {
return;
}
//normalizedClientDir="dist/client/client.mjs"
if (file.startsWith(normalizedClientDir)) {
ws.send({
type: 'full-reload',
path: '*',
});
return;
}
const mods = moduleGraph.getModulesByFile(file);
// check if any plugin wants to perform custom HMR handling
const timestamp = Date.now();
const hmrContext = {
file,
timestamp,
modules: mods ? [...mods] : [],
read: () => readModifiedFile(file),
server,
};
for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
const filteredModules = await hook(hmrContext);
if (filteredModules) {
hmrContext.modules = filteredModules;
}
}
if (!hmrContext.modules.length) {
// html file cannot be hot updated
if (file.endsWith('.html')) {
ws.send({
type: 'full-reload',
path: config.server.middlewareMode
? '*'
: '/' + normalizePath$3(path$o.relative(config.root, file)),
});
}
return;
}
updateModules(shortFile, hmrContext.modules, timestamp, server);
}
什么情况下需要
server.restart()?isConfig、isConfigDependency、isEnv代表什么意思?
isConfig代表更改的文件是configFile配置文件isConfigDependency代表更改的文件是configFile配置文件的依赖文件isEnv代表更改的文件是.env.xxx文件,当vite.config.js中配置InlineConfig.envFile=false时,会禁用.env文件
如果是上面三种条件中的文件发生改变,则直接重启本地服务器
全量更新的条件是什么?
- (仅限开发)客户端本身不能热更新,
client/client.mjs需要全量更新 - 如果没有模块需要更新,并且变化的是
.html文件,需要全量更新
当不满足上面两种条件时,有对应的模块变化时,触发updateModules()逻辑
5.5.2 寻找热更新边界
注:
acceptedHmrExports在vite 4.3.0-beta.1版本为试验性功能!必须手动配置才能启用!默认不启用!因此一般条件下可以忽略该逻辑产生的热更新!
updateModules()的代码逻辑看起来是比较简单的
- 通过
propagateUpdate()获取是否需要全量更新的标志位 - 同时通过
propagateUpdate()将更新内容放入到boundaries数据中 - 最终将
boundaries塞入updates数组中 ws.send发送updates数据到客户端进行热更新
但是问题来了,
propagateUpdate()到底做了什么?什么情况下hasDeadEnd=true?什么情况下hasDeadEnd=false?从热更新的角度来说,都会存在几个常见的问题:
- 什么类型文件默认开启了热更新?
- 是否存在不需要热更新的文件或者情况?
- 一个文件什么情况需要自己更新?
vite是否有自动注入一些代码?指定某一个模块作为另一个模块热更新的依赖项?
function updateModules(file, modules, timestamp, { config, ws, moduleGraph }, afterInvalidation) {
const updates = [];
const invalidatedModules = new Set();
let needFullReload = false;
for (const mod of modules) {
moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true);
if (needFullReload) {
continue;
}
const boundaries = new Set();
const hasDeadEnd = propagateUpdate(mod, boundaries);
if (hasDeadEnd) {
needFullReload = true;
continue;
}
updates.push(...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update`,
timestamp,
path: normalizeHmrUrl(boundary.url),
explicitImportRequired: boundary.type === 'js'
? isExplicitImportRequired(acceptedVia.url)
: undefined,
acceptedPath: normalizeHmrUrl(acceptedVia.url),
})));
}
//...全量更新或者ws.send()
}
在进行
propagateUpdate()分析之前,有几个比较特殊的变量,我们需要先分析下,才能更好理解propagateUpdate()流程
isSelfAccepting变量解析
isSelfAccepting是什么?isSelfAccepting=true代表什么?
对于vite:css的css文件来说,热更新判断条件如下面代码块所示:
- 不是
CSS modules - 没有携带
inline字段 - 没有携带
html-proxy字段
const thisModule = moduleGraph.getModuleById(id);
if (thisModule) {
// CSS modules cannot self-accept since it exports values
const isSelfAccepting = !modules && !inlineRE.test(id) && !htmlProxyRE.test(id);
}
对于vite:import-analysis,如果存在import.meta.hot.accept(),那么isSelfAccepting=true
name: 'vite:import-analysis',
async transform(source, importer, options) {
if (!imports.length && !this._addedImports) {
importerModule.isSelfAccepting = false;
return source;
}
for (let index = 0; index < imports.length; index++) {
const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex,
n: specifier, a: assertIndex, } = imports[index];
const rawUrl = source.slice(start, end);
// check import.meta usage
if (rawUrl === 'import.meta') {
const prop = source.slice(end, end + 4);
if (prop === '.hot') {
hasHMR = true;
const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0);
if (source.slice(endHot, endHot + 7) === '.accept') {
// further analyze accepted modules
if (source.slice(endHot, endHot + 14) === '.acceptExports') {
lexAcceptedHmrExports(source, source.indexOf('(', endHot + 14) + 1, acceptedExports);
isPartiallySelfAccepting = true;
}else if (lexAcceptedHmrDeps(source, source.indexOf('(', endHot + 7) + 1, acceptedUrls)) {
isSelfAccepting = true;
}
}
}
else if (prop === '.env') {
hasEnv = true;
}
continue;
}
}
}
acceptedHmrExports和importedBindings变量产生缘由分析
为什么需要acceptedHmrExports和importedBindings?这两个变量的作用是什么?是为了什么目的而产生的?
在github.com/vitejs/vite… 和 github.com/vitejs/vite…中,我们可以发现
acceptedHmrExports和importedBindings的相关源码提交记录讨论源码的提交记录是
feat(hmr): experimental.hmrPartialAccept (#7324)
在React的son.jsx文件中,可能存在混合模式,比如下面的代码,export一个组件和一个变量,但是在parent.jsx中只使用Foo这个组件
// son.jsx
export const Foo = () => <div>foo</div>
export const bar = () => 123
在理想情况下,如果我们改变bar这个值,那么son.jsx应该触发热更新重新加载!但是parent.jsx不应该热更新重新加载,因为它所使用的Foo并没有发生改变
// parent.jsx
import { Foo } from './Foo.js'
export const Bar = () => <Foo />
因此需要一个API,在原来的模式:
- 如果某个文件改变,无论什么内容,都会触发该文件的
accept(()=>{开始更新逻辑}) - 监听部分
import依赖库,当import依赖库发生更新时,会触发该文件的accept(()=>{开始更新逻辑})
还要增加一个监听export {xx}对象触发的热更新,也就是:
export const Bar = ...
export const Baz = ...
export default ...
if (import.meta.hot) {
import.meta.hot.acceptExports(['default', 'Bar'], newModule => { ... })
}
当default和Bar发生改变时,会触发上面注册的(newModule)=>{开始更新逻辑}方法的执行
importedBindings变量解析
acceptedHmrExports和importedBindings配套使用!
node.acceptedHmrExports代表目前文件import.meta.hot.acceptExports监听的模块,比如下面的['default', 'Bar']
export const Bar = ...
export const Baz = ...
export default ...
if (import.meta.hot) {
import.meta.hot.acceptExports(['default', 'Bar'], newModule => { ... })
}
importer.importedBindings是在vite:import-analysis中解析import语句时,解析该语句是什么类型,然后添加到importedBindings
// parent.jsx
import { Foo } from './Foo.js'
export const Bar = () => <Foo />
如下面代码所示,我们会往extractImportedBindings()传入
imports[index]:import { Foo } from './Foo.js'importedBindings: 空的Map数据结构
然后我们会解析出namespacedImport、defaultImport、namedImports等数据,然后往importedBindings添加对应的字符串,为:
bindings.add('*')bindings.add('default')bindings.add(name):import的属性名称,比如"Foo"
本质就是解析出import的类型,比如有的是import->export default,有的是import->export const name=""
if (enablePartialAccept && importedBindings) {
extractImportedBindings(
resolvedId,
source,
imports[index],
importedBindings
)
}
async function extractImportedBindings(
id: string,
source: string,
importSpec: ImportSpecifier,
importedBindings: Map<string, Set<string>>
) {
let bindings = importedBindings.get(id)
if (!bindings) {
bindings = new Set < string > ()
importedBindings.set(id, bindings)
}
const isDynamic = importSpec.d > -1
const isMeta = importSpec.d === -2
if (isDynamic || isMeta) {
// this basically means the module will be impacted by any change in its dep
bindings.add('*')
return
}
const exp = source.slice(importSpec.ss, importSpec.se)
const [match0] = findStaticImports(exp)
if (!match0) {
return
}
const parsed = parseStaticImport(match0)
if (!parsed) {
return
}
if (parsed.namespacedImport) {
bindings.add('*')
}
if (parsed.defaultImport) {
bindings.add('default')
}
if (parsed.namedImports) {
for (const name of Object.keys(parsed.namedImports)) {
bindings.add(name)
}
}
}
acceptedHmrExports和acceptedHmrDeps解析
在vite:import-analysis插件中,当我们分析文件的import.meta.hot.accept()时,我们会进行解析source
name: 'vite:import-analysis',
async transform(source, importer, options) {
for (let index = 0; index < imports.length; index++) {
// check import.meta usage
if (rawUrl === 'import.meta') {
const prop = source.slice(end, end + 4);
if (prop === '.hot') {
hasHMR = true;
const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0);
if (source.slice(endHot, endHot + 7) === '.accept') {
// further analyze accepted modules
if (source.slice(endHot, endHot + 14) === '.acceptExports') {
lexAcceptedHmrExports(source, source.indexOf('(', endHot + 14) + 1, acceptedExports);
isPartiallySelfAccepting = true;
} else if (lexAcceptedHmrDeps(source, source.indexOf('(', endHot + 7) + 1, acceptedUrls)) {
isSelfAccepting = true;
}
}
}
continue;
}
}
for (const { url, start, end } of acceptedUrls) {
const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(markExplicitImport(url)), ssr);
normalizedAcceptedUrls.add(normalized);
}
await moduleGraph.updateModuleInfo(importerModule, importedUrls, importedBindings, normalizedAcceptedUrls,
isPartiallySelfAccepting ? acceptedExports : null, isSelfAccepting, ssr);
}
function lexAcceptedHmrDeps(code, start, urls) {
function addDep(index) {
urls.add({
url: currentDep,
start: index - currentDep.length - 1,
end: index + 1,
});
currentDep = '';
}
//...解析code,调用addDep()
}
acceptedHmrDeps变量
通过调试可以知道,当我们使用import.meta.hot.accept(["a", "b"])时,我们可以得到acceptedUrls=[{url: "a"},{url: "b"}],然后触发updateModuleInfo()传入normalizedAcceptedUrls进行赋值
acceptedHmrExports
通过调试可以知道,当我们使用import.meta.hot.acceptExports(["a", "b"])时,我们可以得到acceptedExports=[{url: "a"},{url: "b"}],然后触发updateModuleInfo()传入acceptedExports进行赋值
acceptedHmrExports和acceptedHmrDeps的数据在updateModuleInfo()方法中进行添加
- 在
updateModuleInfo()中,通过字符串"a"经过this.ensureEntryFromUrl(accepted)拿到对应的ModuleNode对象,存入到acceptedHmrDeps中,即mod.acceptedHmrDeps.add(this.ensureEntryFromUrl(acceptedModules[i])) mod.acceptedHmrExports=acceptedExports
async updateModuleInfo(mod, importedModules, importedBindings, acceptedModules, acceptedExports, isSelfAccepting, ssr) {
// update accepted hmr deps
const deps = (mod.acceptedHmrDeps = new Set());
for (const accepted of acceptedModules) {
const dep = typeof accepted === 'string'
? await this.ensureEntryFromUrl(accepted, ssr)
: accepted;
deps.add(dep);
}
// update accepted hmr exports
mod.acceptedHmrExports = acceptedExports;
mod.importedBindings = importedBindings;
return noLongerImported;
}
async ensureEntryFromUrl(rawUrl, ssr, setIsSelfAccepting = true) {
const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr);
let mod = this.idToModuleMap.get(resolvedId);
if (!mod) {
mod = new ModuleNode(url, setIsSelfAccepting);
this.urlToModuleMap.set(url, mod);
mod.id = resolvedId;
this.idToModuleMap.set(resolvedId, mod);
const file = (mod.file = cleanUrl(resolvedId));
let fileMappedModules = this.fileToModulesMap.get(file);
if (!fileMappedModules) {
fileMappedModules = new Set();
this.fileToModulesMap.set(file, fileMappedModules);
}
fileMappedModules.add(mod);
} else if (!this.urlToModuleMap.has(url)) {
this.urlToModuleMap.set(url, mod);
}
return mod;
}
分析完上面4个变量,现在我们可以进行总结
isSelfAccepting: 代表该文件是否具有热更新hot.accept代码,因为如果不写热更新代码,那么这个文件发生变化,是无法被处理的!这个值会根据文件类型的不同而采取不同的逻辑判断importedBindings: 代表该文件中import了什么依赖,以及import使用的是具名函数还是default还是*acceptedHmrDeps: 代表该文件中import.meta.hot.accept(["a", "b"])的内容,比如"a"和"b"acceptedHmrExports: 代表该文件中import.meta.hot.acceptExports(["a", "b"])的内容,比如"a"和"b"现在我们就可以进行
propagateUpdate()的详细分析
propagateUpdate()详细分析
下面代码为propagateUpdate()的所有代码,我们可以分为4个部分进行分析
当propagateUpdate()返回true时,说明无法找到热更新边界,需要全量更新
当propagateUpdate()返回false时,说明已经找到热更新边界并且存放在boundaries中
function propagateUpdate(node, boundaries, currentChain = [node]) {
if (node.id && node.isSelfAccepting === undefined) {
return false;
}
//==========第1部分============
if (node.isSelfAccepting) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
for (const importer of node.importers) {
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
propagateUpdate(importer, boundaries, currentChain.concat(importer));
}
}
return false;
}
//==========第2部分============
if (node.acceptedHmrExports) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
} else {
// 没有文件import目前的node
if (!node.importers.size) {
return true;
}
// 当前node不是CSS类型,但是CSS文件import目前的node,那么直接全量更新
if (!isCSSRequest(node.url) &&
[...node.importers].every((i) => isCSSRequest(i.url))) {
return true;
}
}
//==========第3部分============
for (const importer of node.importers) {
const subChain = currentChain.concat(importer);
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer,
acceptedVia: node,
});
continue;
}
if (node.id && node.acceptedHmrExports && importer.importedBindings) {
const importedBindingsFromNode = importer.importedBindings.get(node.id);
if (importedBindingsFromNode &&
areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
continue;
}
}
// 递归调用直接全量更新
if (currentChain.includes(importer)) {
// circular deps is considered dead end
return true;
}
// 从node向上寻找,递归调用propagateUpdate收集boundaries
if (propagateUpdate(importer, boundaries, subChain)) {
return true;
}
}
return false;
}
第1部分 处理isSelfAccepting
node.isSelfAccepting=true一般发生在.vue、.jsx、.tsx等响应式组件中,代表该文件变化时会触发里面注册的热更新回调方法,然后执行自定义的更新代码
function propagateUpdate(node, boundaries, currentChain = [node]) {
if (node.isSelfAccepting) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
for (const importer of node.importers) {
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
propagateUpdate(importer, boundaries, currentChain.concat(importer));
}
}
return false;
}
//...
}
如果node.isSelfAccepting为true,代表它有accept()方法,比如.vue文件中会注入accept()方法,这个时候只要将目前的node加入到boundaries
同时还要判断node.importers是不是CSS请求链接,如果是的话,要继续向上寻找,再次出发propagateUpdate()收集热更新边界boundaries
源码中注释:像
Tailwind JIT这样的PostCSS插件可能会将任何文件注册为CSS文件的依赖项,因此需要检测node.importers是不是CSS请求,本文对这方面不展开详细的分析,请参考其它文章进行了解
当isSelfAccepting=true,最终propagateUpdate()返回false,代表不用全量更新,热更新边界boundaries加入当前的node,结束其它条件语句的执行
第2部分 处理acceptedHmrExports
function propagateUpdate(node, boundaries, currentChain = [node]) {
//...第1部分
// 第2部分
if (node.acceptedHmrExports) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
} else {
// 没有文件import目前的node
if (!node.importers.size) {
return true;
}
// 当前node不是CSS类型,但是CSS文件import目前的node,那么直接全量更新
if (!isCSSRequest(node.url) &&
[...node.importers].every((i) => isCSSRequest(i.url))) {
return true;
}
}
//...
}
node.acceptedHmrExports: 代表目前文件注入了import.meta.hot.acceptExports(xxx)代码,热更新边界boundaries加入当前的node!node.importers.size: 代表没有其它文件import(引用)了目前的node文件,直接全量更新目前的node文件不是CSS类型,但是其它CSS文件import(引用)了目前的node文件,直接全量更新
第3部分 遍历node.importers
function propagateUpdate(node, boundaries, currentChain = [node]) {
//...第一部分
//...第2部分
// 第3部分
for (const importer of node.importers) {
const subChain = currentChain.concat(importer);
// 逻辑1
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer,
acceptedVia: node,
});
continue;
}
// 逻辑2
if (node.id && node.acceptedHmrExports && importer.importedBindings) {
const importedBindingsFromNode = importer.importedBindings.get(node.id);
if (importedBindingsFromNode &&
areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
continue;
}
}
// 逻辑3
// 递归调用直接全量更新
if (currentChain.includes(importer)) {
// circular deps is considered dead end
return true;
}
// 从node向上寻找,递归调用propagateUpdate收集boundaries
if (propagateUpdate(importer, boundaries, subChain)) {
return true;
}
}
//...
}
function areAllImportsAccepted(importedBindings, acceptedExports) {
for (const binding of importedBindings) {
if (!acceptedExports.has(binding)) {
return false;
}
}
return true;
}
从上面的分析可以知道
acceptedHmrDeps本质就是获取import.meta.hot.accept(xxx)的监听模块acceptedHmrExports本质就是获取import.meta.hot.acceptExports(xxx)的监听模块importedBindings代表目前文件中import的文件的数据
第3部分的代码逻辑主要是遍历当前node.importer,寻找是否需要加入热更新边界boundaries的文件
逻辑1 处理acceptedHmrDeps
如果node.importers[i]注入了import.meta.hot.accept(xxx)的监听代码,并且包含当前文件node(如下面代码块所示), 那么热更新边界boundaries加入当前的node.importers[i]
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer,
acceptedVia: node,
});
continue;
}
// B.js
export const test = "B.js";
// A.js
import {test} from "./B.js";
import.meta.hot.accept("B", (mod)=>{});
逻辑2 处理acceptedHmrExports & importedBindingsFromNode
如下面代码块所示,目前node=B.js,我们改变了B.js的内容,触发了热更新
此时importedBindingsFromNode=["test"],acceptedHmrExports=["test"],触发continue,不触发向上寻找热更新边界的逻辑
if (node.id && node.acceptedHmrExports && importer.importedBindings) {
const importedBindingsFromNode = importer.importedBindings.get(node.id);
if (importedBindingsFromNode &&
areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
continue;
}
}
function areAllImportsAccepted(importedBindings, acceptedExports) {
for (const binding of importedBindings) {
if (!acceptedExports.has(binding)) {
return false;
}
}
return true;
}
// B.js
const test = "B.js3";
import.meta.hot.acceptExports("test", (mod)=>{
console.error("B.js热更新触发");
})
const test1 = "B1.js";
export {test, test1}
// A.js
import {test} from "./B.js";
console.info("A.js", test);
那为什么满足
areAllImportsAccepted就触发continue呢?
acceptExports具体示例分析
通过具体实例明白
acceptExports想要达到的效果
在下面示例代码中,在B.js中,我们监听了export数据:test
- 当我们改变
test变量时,比如从test="B.js"更改为test="B111.js"时,只会触发B.js热更新,然后触发打印B.js热更新触发 - 当我们改变
test1变量,由于B.js中没有监听test1变量,因此会触发B.js热更新 + 向上寻找A.js->向上寻找main.js,最终找到main.js,触发main.js热更新
当B.js监听的
acceptExports的字段(test)跟A.js中import的字段(test1)不一样时,如下面代码所示,会触发向上寻找热更新边界
// main.js
import {AExport} from "./simple/A.js";
import.meta.hot.acceptExports(["aa"]);
// A.js
import {test1} from "./B.js";
console.info("A.js", test1);
export const AExport = "AExport3";
// B.js
const test = "B.js";
import.meta.hot.acceptExports("test", (mod)=>{
console.error("B.js热更新触发");
})
const test1 = "B432.js";
export {test, test1}
如果将B.js监听的acceptExports的字段改为test1,跟A.js中import的字段(test1)一样时,然后改变test1以及改变test,那么最终会发生什么呢?
答:最终只会触发当前文件B.js的热更新,不会触发向上寻找热更新边界(此时areAllImportsAccepted()=true,满足areAllImportsAccepted触发continue,不会触发向上寻找热更新边界)
从这个例子中我们就可以清晰明白acceptExports的作用,我们可以监听部分export变量,从而避免过多文件的无效热更新
那如果目前
node文件acceptExports所有export出去的值,会发生什么?
在上面isSelfAccepting的分析中,我们可以知道,acceptExports代表import.meta.hot.acceptExports(xxx)监听的模块数据
exports代表该文件所exports的数据,比如上面示例B.js的["test", "test1"]
当acceptExports监听的数据已经完全覆盖文件所exports的数据时,会强行设置isSelfAccepting=true
name: 'vite:import-analysis',
async transform(source, importer, options) {
// 当source存在hot.acceptExport字段时,isPartiallySelfAccepting=true
// 当source存在hot.accept字段时,isSelfAccepting=true
if (!isSelfAccepting &&
isPartiallySelfAccepting &&
acceptedExports.size >= exports.length &&
exports.every((e) => acceptedExports.has(e.n))) {
isSelfAccepting = true;
}
}
当isSelfAccepting=true时,当B.js文件发生变化时,就会触发propagateUpdate()的第1部分,热更新边界boundaries加入当前的node,然后直接return false,停止向上处理寻找热更新边界
因此如果目前node文件已经acceptExports所有export出去的值,就可以不向上处理寻找热更新边界
function propagateUpdate(node, boundaries, currentChain = [node]) {
if (node.isSelfAccepting) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
for (const importer of node.importers) {
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
propagateUpdate(importer, boundaries, currentChain.concat(importer));
}
}
return false;
}
//...
}
逻辑3 继续向上找热更新的边界
如果存在循环递归的情况,直接返回true,直接全量更新
// 递归调用直接全量更新
if (currentChain.includes(importer)) {
// circular deps is considered dead end
return true;
}
// 从node向上寻找,递归调用propagateUpdate收集boundaries
if (propagateUpdate(importer, boundaries, subChain)) {
return true;
}
propagateUpdate小结
什么情况下才需要向上找热更新的边界?
现在我们可以根据上面的分析进行总结:
node.isSelfAccepting为false,继续执行下面的条件判断importer.acceptedHmrDeps.has(node),即parent有注入accept("A")监听import {A} from "xxx"的值,不继续向上找热更新的边界(停止propagateUpdate()再次执行)node.acceptedHmrExports为true时,直接将当前node加入到热更新边界中- 已经监听所有
export出去的值,则不继续向上找热更新的边界(停止propagateUpdate()再次执行) - 如果没有监听所有
export出去的值,则继续向上找热更新的边界propagateUpdate(importer, boundaries)
- 已经监听所有
node.acceptedHmrExports为false时,继续向上找热更新的边界propagateUpdate(importer, boundaries)
function propagateUpdate(node, boundaries, currentChain = [node]) {
if (node.id && node.isSelfAccepting === undefined) {
return false;
}
//==========第1部分============
if (node.isSelfAccepting) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
for (const importer of node.importers) {
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
propagateUpdate(importer, boundaries, currentChain.concat(importer));
}
}
return false;
}
//==========第2部分============
if (node.acceptedHmrExports) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
} else {
// 没有文件import目前的node
if (!node.importers.size) {
return true;
}
// 当前node不是CSS类型,但是CSS文件import目前的node,那么直接全量更新
if (!isCSSRequest(node.url) &&
[...node.importers].every((i) => isCSSRequest(i.url))) {
return true;
}
}
//==========第3部分============
for (const importer of node.importers) {
const subChain = currentChain.concat(importer);
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer,
acceptedVia: node,
});
continue;
}
if (node.id && node.acceptedHmrExports && importer.importedBindings) {
const importedBindingsFromNode = importer.importedBindings.get(node.id);
if (importedBindingsFromNode &&
areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
continue;
}
}
// 递归调用直接全量更新
if (currentChain.includes(importer)) {
// circular deps is considered dead end
return true;
}
// 从node向上寻找,递归调用propagateUpdate收集boundaries
if (propagateUpdate(importer, boundaries, subChain)) {
return true;
}
}
return false;
}
5.5.3 全量更新或者发送热更新模块到客户端
function updateModules(file, modules, timestamp, { config, ws, moduleGraph }, afterInvalidation) {
for (const mod of modules) {
//...寻找热更新边界updates,如果找不到,则进行全量更新needFullReload=true
updates.push(...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update`,
timestamp,
path: normalizeHmrUrl(boundary.url),
explicitImportRequired: boundary.type === 'js'
? isExplicitImportRequired(acceptedVia.url)
: undefined,
acceptedPath: normalizeHmrUrl(acceptedVia.url),
})));
}
if (needFullReload) {
// 全量更新
ws.send({
type: 'full-reload',
});
return;
}
if (updates.length === 0) {
// 没有更新,不进行ws.send
return;
}
ws.send({
type: 'update',
updates,
});
}
updates最终的数据结构为:
其中有两个变量需要注意下:path和acceptedPath
path: 取的是boundary.urlacceptedPath: 取的是acceptedVia.url
在寻找热更新边界propagateUpdate()时,如下面代码所示,我们知道
node.isSelfAccepting:path和acceptedPath都为nodenode.acceptedHmrExports:path和acceptedPath都为nodeimporter.acceptedHmrDeps.has(node):path为importer,acceptedPath为node
function propagateUpdate(node, boundaries, currentChain = [node]) {
//==========第1部分============
if (node.isSelfAccepting) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
}
//==========第2部分============
if (node.acceptedHmrExports) {
boundaries.add({
boundary: node,
acceptedVia: node,
});
}
//==========第3部分============
for (const importer of node.importers) {
const subChain = currentChain.concat(importer);
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer,
acceptedVia: node,
});
continue;
}
//...
if (propagateUpdate(importer, boundaries, subChain)) {
return true;
}
}
return false;
}
5.6 文件改变,服务器->客户端触发热更新逻辑
我们从5.3步骤后知道,当文件变化,服务器WebSocket->客户端WebSocket后,会触发handleMessage()的执行
如果update.type为js-update,则触发fetchUpdate(update)方法
如果update.type不为js-update,检测是否存在link标签包含这个要更新模块的路径,如果存在,则重新加载该文件数据(加载新的link,删除旧的link)
Element: after()表示插入新的元素到Elment的后面Element: remove()表示删除该元素
async function handleMessage(payload) {
switch (payload.type) {
case 'update':
notifyListeners('vite:beforeUpdate', payload);
await Promise.all(payload.updates.map(async (update) => {
if (update.type === 'js-update') {
return queueUpdate(fetchUpdate(update));
} else {
const el = Array.from(document.querySelectorAll('link')).find((e) => !outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl));
const newPath = `${base}${searchUrl.slice(1)}${searchUrl.includes('?') ? '&' : '?'}t=${timestamp}`;
if (!el) {
return;
}
// 使用<link href="路径?t=更新时间戳">加载文件
const newLinkTag = el.cloneNode();
newLinkTag.href = new URL(newPath, el.href).href;
const removeOldEl = () => {
el.remove();
console.debug(`[vite] css hot updated: ${searchUrl}`);
resolve();
};
newLinkTag.addEventListener('load', removeOldEl);
outdatedLinkTags.add(el);
el.after(newLinkTag);
}
}));
notifyListeners('vite:afterUpdate', payload);
break;
case 'full-reload':
notifyListeners('vite:beforeFullReload', payload);
//...
location.reload();
break;
}
}
5.6.1 fetchUpdate
在寻找热更新边界
propagateUpdate()时,我们知道
node.isSelfAccepting:path和acceptedPath都为nodenode.acceptedHmrExports:path和acceptedPath都为nodeimporter.acceptedHmrDeps.has(node):path为importer,acceptedPath为node还有可能触发向上找热更新的边界
propagateUpdate(importer, boundaries),此时path为importer,acceptedPath为importer
async function fetchUpdate({ path, acceptedPath, timestamp, explicitImportRequired, }) {
//根据路径拿到之前收集的依赖更新对象
const mod = hotModulesMap.get(path);
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));
// 根据路径重新请求该文件数据
fetchedModule = await import(
/* @vite-ignore */
base +
acceptedPathWithoutQuery.slice(1) +
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${query ? `&${query}` : ''}`);
return () => {
for (const { deps, fn } of qualifiedCallbacks) {
// 将新请求的数据,使用fn(fetchedModule)进行局部热更新
fn(deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)));
}
};
}
如上面代码所示,在fetchUpdate()中,我们会通过hotModulesMap.get(path)拿到关联的mod
那么
hotModulesMap的数据是在哪里初始化的呢?
在5.4步骤的非index.html注入代码分析中,如下面的代码所示,我们知道会在文件中进行hot.accept()的调用
// .vue文件注入代码
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/Index.vue");
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)
}
})
当一个.vue文件使用hot.accept()或者hot.accept(()=>{})时,当监听的文件发生变化时上面代码中meta.hot.accept((mod)=>{})的mod就是上面fetchUpdate()的fetchedModule,const {default}=fetchedModule也就是请求文件的内容(文件进行export default的内容)
当一个文件使用hot.accept("依赖a的路径")或者hot.accept(["依赖a的路径","依赖b的路径"])时,参数会作为deps存入到mod.callbacks中
在寻找热更新边界
propagateUpdate()时,我们知道
node.isSelfAccepting:path和acceptedPath都为nodenode.acceptedHmrExports:path和acceptedPath都为nodeimporter.acceptedHmrDeps.has(node):path为importer,acceptedPath为node还有可能触发向上找热更新的边界
propagateUpdate(importer, boundaries),此时path为importer,acceptedPath为importer
如下面代码所示,当我们通过hotModulesMap.get(path)拿到关联的mod,此时的mod对应的path文件就是代码import.meta.hot.accept或者import.meta.hot.acceptExports所在的文件
async function fetchUpdate({ path, acceptedPath, timestamp, explicitImportRequired, }) {
//根据路径拿到之前收集的依赖更新对象
const mod = hotModulesMap.get(path);
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));
// 根据路径重新请求该文件数据
//...
}
然后通过deps.includes(acceptedPath)进行注册回调的筛选,如果hot.accept有显式注册deps,就会根据deps去筛选
如果hot.accept没有显式注册deps,那么此时deps=[ownerPath],即deps=[path]
比如B.js有一个export test,A.js中进行hot.accept("test"),那么此时
importer.acceptedHmrDeps.has(node):path为importer,acceptedPath为nodedeps就是test的路径,path就是A.js的路径,属性test的路径必定包含当前文件
node的路径最终结果就是找到A.js中注册的
hot.accept("test")进行执行
// @vite/client代码
function createHotContext(ownerPath) {
function acceptDeps(deps, callback = () => { }) {
const mod = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
const hot = {
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.`);
}
},
acceptExports(_, callback) {
acceptDeps([ownerPath], ([mod]) => callback === null || callback === void 0 ? void 0 : callback(mod));
},
};
return hot;
}
那为什么
acceptExports传入的第一个参数不使用呢?直接初始化为[ownerPath]?
我们在上面的propagateUpdate()的分析中,我们知道
node.isSelfAccepting:path和acceptedPath都为nodenode.acceptedHmrExports:path和acceptedPath都为nodeimporter.acceptedHmrDeps.has(node):path为importer,acceptedPath为node
还有可能触发向上找热更新的边界propagateUpdate(importer, boundaries),此时path为importer,acceptedPath为importer
这里的
node代表当前文件的路径!importer代表import当前node文件的那个文件的路径!
从上面的分析中,import.meta.hot.accept(xxx)则可以设置path为importer,acceptedPath为node,即可以在import当前node文件的那个文件中处理当前node文件的热更新
而acceptedHmrExports存在,即import.meta.hot.acceptExports(xxx)存在时,它监听的都是当前node文件的路径,只能在当前node文件中处理当前node文件的热更新,这跟监听export部分数据触发热更新的初衷是符合的,因此acceptExports传入的第一个参数不使用,直接初始化为当前node的文件路径[ownerPath]
import.meta.hot.accept(xxx)不仅仅可以监听export还可以监听import
那上面分析的
acceptedHmrExports变量就代表import.meta.hot.acceptExports(["a", "b"])所监听的值,即acceptedHmrExports=[{url: "a"},{url: "b"}]是怎么来的呢?
为了得到acceptedHmrExports,是直接拿代码去正则表达式获取数据,而不是利用方法调用,如下面代码所示,是通过lexAcceptedHmrExports()拿到acceptExports(["a", "b"])的"a"和"b"
也就是说虽然acceptedHmrExports能够拿到
import.meta.hot.acceptExports(["a", "b"])的值,但仅仅是为了寻找热更新边界时使用只有
path和acceptedPath都为node才会触发实际import.meta.hot.acceptExports(["a", "b"], fn)的fn执行
name: 'vite:import-analysis',
async transform(source, importer, options) {
for (let index = 0; index < imports.length; index++) {
// check import.meta usage
if (rawUrl === 'import.meta') {
const prop = source.slice(end, end + 4);
if (prop === '.hot') {
hasHMR = true;
const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0);
if (source.slice(endHot, endHot + 7) === '.accept') {
// further analyze accepted modules
if (source.slice(endHot, endHot + 14) === '.acceptExports') {
lexAcceptedHmrExports(source, source.indexOf('(', endHot + 14) + 1, acceptedExports);
isPartiallySelfAccepting = true;
} else if (lexAcceptedHmrDeps(source, source.indexOf('(', endHot + 7) + 1, acceptedUrls)) {
isSelfAccepting = true;
}
}
}
continue;
}
}
}
6. 总结
6.1 预构建原理
- 遍历所有的文件,搜集所有裸模块的请求,然后将所有裸模块的请求作为esbuild打包的入口文件,将所有裸模块缓存打包到
.vite/deps文件夹下,在打包过程中,会将commonjs转化为esmodule的形式,本质是使用一个export default包裹着commonjs的代码,同时利用esbuild的打包能力,将多个内置请求合并为一个请求,防止大量请求引起浏览器端的网络堵塞,使页面加载变得非常缓慢 - 在浏览器请求链接时改写所有裸模块的路径指向
.vite/deps - 如果想要重新执行预构建,使用
--force参数或者直接删除node_modeuls/.vite/deps是比较快捷的方式,或者改变一些配置的值可以触发重新预构建
6.2 热更新原理
- 使用
websocket建立客户端和服务端 - 服务端会监听文件变化,然后通过一系列逻辑判断,得出热更新的文件范围,此时的热更新边界的判断依赖于客户端一开始加载文件时,middlewares拦截请求资源,进行文件
transform转化时分析得到的信息(每一个文件都具有一个对象数据ModuleNode),即客户端是否包含import.meta.hot.accept(xxx)这些代码会影响热更新边界的寻找 - 客户端接收服务端的热更新文件范围相关的路径后,进行客户端中热更新代码的调用
6.3 vite与webpack的区别
webpack是先解析依赖,打包构建,形成bundle后再启动开发服务器,当我们修改bundle的其中一个子模块时,我们需要对这个bundle重新打包然后触发热更新,项目越大越复杂,启动时间就越长
vite的核心原理是利用esmodule进行按需加载,先启动开发服务器,然后再进行import模块,无法进行整体依赖的解析和构建打包,同时使用esbuild快速的打包速度进行不会轻易改变node_modules依赖的预构建,提升速度,当文件发生改变时,也会发送对应的文件数据到客户端,进行该文件以及相关文件的热更新,不用重新构建和重新打包,项目越大提升效果越明显
6.4 vite对比webpack优缺点
vite优点
- 快速的服务器启动: 当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务,Vite 以 原生 ESM 方式提供源码,让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码,然后根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理
- 反应快速的热更新: 基于打包器启动时,重建整个包的效率很低,并且更新速度会随着应用体积增长而直线下降,在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新
vite缺点
- 首屏加载较慢: 需要大量http请求和源文件转化操作,首次加载还需要花费时间进行预构建
虽然
vite已经改进预构建不会影响本地服务的启动和运行,但是一些预构建的库,比如react.js,还是得等待预构建完成后才能加载react.js,然后进行整体渲染
- 懒加载较慢: 和首屏加载一样,动态加载的文件仍然需要转化操作,可能会存在大量的http请求(多个业务依赖文件)
- 开发服务器和生产环境构建之间输出和行为可能不一致: 开发环境使用
esbuild,生产环境使用rollup,有一些插件(比如commonjs的转化)需要区分开发环境和生产环境
6.5 vite如何处理Typescript、SCSS等语言
vite:css插件: 调用预处理器依赖库进行转化处理vite:esbuild插件:.ts和.tsx转化.js,用来代替传统的tsc转化功能
Vite 使用 esbuild 将 TypeScript 转译到 JavaScript,约是
tsc速度的 20~30 倍