本文基于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) returnconst { default: updated, _rerender_only } = modif (_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.jsxexport const Foo = () => <div>foo</div>export const bar = () => 123
在现实状况下,如果咱们扭转bar
这个值,那么son.jsx
应该触发热更新从新加载!然而parent.jsx
不应该热更新从新加载,因为它所应用的Foo
并没有产生扭转
// parent.jsximport { 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.jsximport { 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.jsexport const test = "B.js";// A.jsimport {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.jsconst test = "B.js3";import.meta.hot.acceptExports("test", (mod)=>{ console.error("B.js热更新触发");})const test1 = "B1.js";export {test, test1}// A.jsimport {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收集boundariesif (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.jsimport {AExport} from "./simple/A.js";import.meta.hot.acceptExports(["aa"]);
// A.jsimport {test1} from "./B.js";console.info("A.js", test1);export const AExport = "AExport3";
// B.jsconst test = "B.js";import.meta.hot.acceptExports("test", (mod)=>{ console.error("B.js热更新触发");})// 当acceptExports笼罩了所有export数据时,会强行设置isSelfAccepting=trueconst 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原理剖析