共计 37493 个字符,预计需要花费 94 分钟才能阅读完成。
本文基于
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 的起因
在 https://github.com/vitejs/vite/discussions/7309 和 https://github.com/vitejs/vite/pull/7324 中,咱们能够发现
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 />
如上面代码所示,咱们会传入 imports[index]
为import {Foo} from './Foo.js'
,importedBindings
是一个空的 Map
数据结构
而后咱们会解析出 namespacedImport
、defaultImport
、namedImports
等数据,而后往 importedBindings
增加对应的字符串,为:
bindings.add('*')
bindings.add('default')
bindings.add(name)
:import
的属性名称,比方"Foo"
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;
}
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)
的监听模块(如上面代码块所示), 那么热更新边界 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);
export const AExport = "AExport";
那为什么满足
areAllImportsAccepted
就触发continue
呢?意思就是如果目前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"
的值,不持续向上找热更新的边界-
node.acceptedHmrExports
为true
时,间接将以后node
退出到热更新边界中- 曾经监听所有
export
进来的值,则不持续向上找热更新的边界 - 如果没有监听所有
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;
}
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
热更新
从这个例子中咱们就能够清晰明确 acceptExports
的作用,咱们能够监听局部 export
变量,从而防止过多文件的有效热更新
当监听的
acceptExports
的字段跟 import 的字段不一样时,会触发向上寻找热更新边界
当监听的acceptExports
的字段跟 import 的字段一样时,只会触发以后文件的热更新
// 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 热更新触发");
})
// 当 acceptExports 笼罩了所有 export 数据时,会强行设置 isSelfAccepting=true
const test1 = "B432.js";
export {test, test1}
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.url
acceptedPath
: 取的是acceptedVia.url
在寻找热更新边界 propagateUpdate()
时,如上面代码所示,咱们晓得
node.isSelfAccepting
:path
和acceptedPath
都为node
node.acceptedHmrExports
:path
和acceptedPath
都为node
importer.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
都为node
node.acceptedHmrExports
:path
和acceptedPath
都为node
importer.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
文件应用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
中
如下面代码所示,当咱们通过 hotModulesMap.get(path)
拿到关联的 mod
,此时的mod
对应的 path
文件所注册的 import.meta.hot.accept
或者 import.meta.hot.acceptExports
的回调
而后通过 deps.includes(acceptedPath)
进行注册回调的筛选,如果 hot.accept
有显式注册 deps
,就会依据deps
去筛选
如果 hot.accept
没有显式注册deps
,那么此时deps
=[ownerPath]
,即deps
=[path]
// .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)
}
})
// @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
都为node
node.acceptedHmrExports
:path
和acceptedPath
都为node
importer.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"
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
建设客户端和服务端 - 服务端会监听文件变动,而后通过一系列逻辑判断,得出热更新的文件范畴,此时的热更新边界的判断依赖于
transform
文件内容时的剖析,每一个文件都具备一个对象数据ModuleNode
- 客户端接管服务端的热更新文件范畴相干的门路后,进行客户端中热更新代码的调用
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 倍
参考文章
- Vite 源码剖析,是时候弄清楚 Vite 的原理了
- Vite 原理及源码解析
- Vite 原理剖析