乐趣区

关于前端:vite4源码dev模式整体流程浅析二

本文基于 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 内容的替换,将对应的 tagtypesrc 增加到指定地位中

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/clientindex.html<script> 后,咱们会运行 @vite/client 代码,而后咱们会执行什么逻辑呢?

建设客户端的 WebSocket,增加常见的事件: openmessageclose
当文件产生扭转时,会触发 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.jsIndex.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() 办法,进行以后 ownerPathcallbacks收集

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()isConfigisConfigDependencyisEnv 代表什么意思?

  • isConfig代表更改的文件是 configFile 配置文件
  • isConfigDependency代表更改的文件是 configFile 配置文件的依赖文件
  • isEnv代表更改的文件是 .env.xxx 文件,当 vite.config.js 中配置 InlineConfig.envFile=false 时,会禁用 .env 文件

如果是下面三种条件中的文件产生扭转,则间接重启本地服务器

全量更新的条件是什么?

  • (仅限开发)客户端自身不能热更新,满足 client/client.mjs 就是全量更新的条件须要全量更新
  • 如果没有模块须要更新,并且变动的是 .html 文件,须要全量更新

当不满足下面两种条件时,有对应的模块变动时,触发 updateModules() 逻辑

5.5.2 寻找热更新边界

注:acceptedHmrExportsvite 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 中,咱们能够发现 acceptedHmrExportsimportedBindings的相干源码提交记录探讨
源码的提交记录是feat(hmr): experimental.hmrPartialAccept (#7324)

Reactson.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 => {...})
}

defaultBar产生扭转时,会触发下面注册的 (newModule)=>{开始更新逻辑} 办法的执行

importedBindings 解析

acceptedHmrExportsimportedBindings 配套应用!

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 数据结构
而后咱们会解析出 namespacedImportdefaultImportnamedImports 等数据,而后往 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 进行赋值

acceptedHmrExportsacceptedHmrDeps 的数据在 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.isSelfAcceptingtrue,代表它有 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.isSelfAcceptingfalse,继续执行上面的条件判断
  • importer.acceptedHmrDeps.has(node),即 parent 有注入 accept("A") 监听 import {A} from "xxx" 的值,不持续向上找热更新的边界
  • node.acceptedHmrExportstrue 时,间接将以后 node 退出到热更新边界中

    • 曾经监听所有 export 进来的值,则不持续向上找热更新的边界
    • 如果没有监听所有 export 进来的值,则持续向上找热更新的边界propagateUpdate(importer, boundaries)
  • node.acceptedHmrExportsfalse 时,持续向上找热更新的边界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最终的数据结构为:

其中有两个变量须要留神下:pathacceptedPath

  • path: 取的是boundary.url
  • acceptedPath: 取的是acceptedVia.url

在寻找热更新边界 propagateUpdate() 时,如上面代码所示,咱们晓得

  • node.isSelfAccepting: pathacceptedPath 都为node
  • node.acceptedHmrExports: pathacceptedPath 都为node
  • importer.acceptedHmrDeps.has(node): pathimporteracceptedPathnode
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.typejs-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: pathacceptedPath 都为node
  • node.acceptedHmrExports: pathacceptedPath 都为node
  • importer.acceptedHmrDeps.has(node): pathimporteracceptedPathnode

还有可能触发向上找热更新的边界 propagateUpdate(importer, boundaries),此时pathimporteracceptedPathimporter

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()fetchedModuleconst {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: pathacceptedPath 都为node
  • node.acceptedHmrExports: pathacceptedPath 都为node
  • importer.acceptedHmrDeps.has(node): pathimporteracceptedPathnode

还有可能触发向上找热更新的边界 propagateUpdate(importer, boundaries),此时pathimporteracceptedPathimporter

这里的 node 代表以后文件的门路!importer代表 import 以后 node 文件的那个文件的门路!


从下面的剖析中,import.meta.hot.accept(xxx)则能够设置 pathimporteracceptedPathnode,即能够在 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 预构建原理

  1. 遍历所有的文件,收集所有裸模块的申请,而后将所有裸模块的申请作为 esbuild 打包的入口文件,将所有裸模块缓存打包到 .vite/deps 文件夹下,在打包过程中,会将 commonjs 转化为 esmodule 的模式,实质是应用一个 export default 包裹着 commonjs 的代码,同时利用 esbuild 的打包能力,将多个内置申请合并为一个申请,避免大量申请引起浏览器端的网络梗塞,使页面加载变得十分迟缓
  2. 在浏览器申请链接时改写所有裸模块的门路指向.vite/deps
  3. 如果想要从新执行预构建,应用 --force 参数或者间接删除 node_modeuls/.vite/deps 是比拟快捷的形式,或者扭转一些配置的值能够触发从新预构建

6.2 热更新原理

  1. 应用 websocket 建设客户端和服务端
  2. 服务端会监听文件变动,而后通过一系列逻辑判断,得出热更新的文件范畴,此时的热更新边界的判断依赖于 transform 文件内容时的剖析,每一个文件都具备一个对象数据ModuleNode
  3. 客户端接管服务端的热更新文件范畴相干的门路后,进行客户端中热更新代码的调用

6.3 vite 与 webpack 的区别

webpack是先解析依赖,打包构建,造成 bundle 后再启动开发服务器,当咱们批改 bundle 的其中一个子模块时,咱们须要对这个 bundle 从新打包而后触发热更新,我的项目越大越简单,启动工夫就越长
vite 的外围原理是利用 esmodule 进行按需加载,先启动开发服务器,而后再进行 import 模块,无奈进行整体依赖的解析和构建打包,同时应用 esbuild 疾速的打包速度进行不会轻易扭转 node_modules 依赖的预构建,晋升速度,当文件产生扭转时,也会发送对应的文件数据到客户端,进行该文件以及相干文件的热更新,不必从新构建和从新打包,我的项目越大晋升成果越显著

6.4 vite 比照 webpack 优缺点

vite 长处

  1. 疾速的服务器启动: 当冷启动开发服务器时,基于打包器的形式启动必须优先抓取并构建你的整个利用,而后能力提供服务,Vite 以 原生 ESM 形式提供源码,让浏览器接管了打包程序的局部工作:Vite 只须要在浏览器申请源码时进行转换并按需提供源码,而后依据情景动静导入代码,即只在以后屏幕上理论应用时才会被解决
  2. 反馈疾速的热更新: 基于打包器启动时,重建整个包的效率很低,并且更新速度会随着利用体积增长而直线降落,在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只须要准确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块自身),使得无论利用大小如何,HMR 始终能放弃疾速更新

vite 毛病

  1. 首屏加载较慢: 须要大量 http 申请和源文件转化操作,首次加载还须要破费工夫进行预构建

    尽管 vite 曾经改良预构建不会影响本地服务的启动和运行,然而一些预构建的库,比方react.js,还是得期待预构建实现后能力加载react.js,而后进行整体渲染

  2. 懒加载较慢: 和首屏加载一样,动静加载的文件依然须要转化操作,可能会存在大量的 http 申请(多个业务依赖文件)
  3. 开发服务器和生产环境构建之间输入和行为可能不统一: 开发环境应用 esbuild,生产环境应用rollup,有一些插件(比方commonjs 的转化)须要辨别开发环境和生产环境

6.5 vite如何解决 Typescript、SCSS 等语言

  • vite:css插件: 调用预处理器依赖库进行转化解决
  • vite:esbuild插件: .ts.tsx 转化 .js,用来代替传统的tsc 转化性能

Vite 应用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍

参考文章

  1. Vite 源码剖析,是时候弄清楚 Vite 的原理了
  2. Vite 原理及源码解析
  3. Vite 原理剖析
退出移动版