本文基于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) 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()办法,进行以后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.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 => { ... })}

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.jsximport { 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.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.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.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最终的数据结构为:


其中有两个变量须要留神下: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原理剖析