「vite4源码」dev模式整体流程浅析(一)

本文基于vite 4.3.0-beta.1版本的源码进行分析

文章内容

  1. vite 本地服务器的创建流程分析
  2. vite 预构建流程分析
  3. vite middlewares拦截请求资源分析
  4. vite 热更新HMR流程分析

1. 入口npm run dev

在项目的package.json中注册对应的scripts命令,当我们运行npm run dev时,本质就是运行了vite

{
    "scripts": {
      "dev": "vite",
    }
}
vite命令是在哪里注册的呢?

node_modules/vite/package.json

{
    "bin": {
        "vite": "bin/vite.js"
    }
}

node_modules/vite/bin/vite.js

#!/usr/bin/env node
const profileIndex = process.argv.indexOf('--profile')

function start() {
  return import('../dist/node/cli.js')
}

if (profileIndex > 0) {
    //...
} else {
  start()
}

最终调用的是打包后的dist/node/cli.js文件

「vite4源码」dev模式整体流程浅析(一)_第1张图片

处理用户的输入后,调用./chunks/dep-f365bad6.jscreateServer()方法,如下面所示,最终调用server.listen()

const { createServer } = await import('./chunks/dep-f365bad6.js').then(function (n) { return n.G; });
const server = await createServer({
    root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    optimizeDeps: { force: options.force },
    server: cleanOptions(options),
});
await server.listen();

createServer()

async function createServer(inlineConfig = {}) {
    const config = await resolveConfig(inlineConfig, 'serve');
    if (isDepsOptimizerEnabled(config, false)) {
        // start optimizer in the background, we still need to await the setup
        await initDepsOptimizer(config);
    }
    const { root, server: serverConfig } = config;
    const httpsOptions = await resolveHttpsConfig(config.server.https);
    const { middlewareMode } = serverConfig;
    const resolvedWatchOptions = resolveChokidarOptions(config, {
        disableGlobbing: true,
        ...serverConfig.watch,
    });
    const middlewares = connect();
    const httpServer = middlewareMode
        ? null
        : await resolveHttpServer(serverConfig, middlewares, httpsOptions);
    const ws = createWebSocketServer(httpServer, config, httpsOptions);

    const watcher = chokidar.watch(
    // config file dependencies and env file might be outside of root
    [root, ...config.configFileDependencies, path$o.join(config.envDir, '.env*')], resolvedWatchOptions);
    const moduleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }));
   
    const server = {
        config,
        middlewares,
        httpServer,
        watcher,
        pluginContainer: container,
        ws,
        moduleGraph,
        ...
    };
    
    const initServer = async () => {
        if (serverInited)
            return;
        if (initingServer)
            return initingServer;
        initingServer = (async function () {
            await container.buildStart({});
            initingServer = undefined;
            serverInited = true;
        })();
        return initingServer;
    };
    if (!middlewareMode && httpServer) {
        // overwrite listen to init optimizer before server start
        const listen = httpServer.listen.bind(httpServer);
        httpServer.listen = (async (port, ...args) => {
            try {
                await initServer();
            }
            catch (e) {
                httpServer.emit('error', e);
                return;
            }
            return listen(port, ...args);
        });
    }
    else {
        await initServer();
    }
    return server;
}

createServer()源码太长,下面将分为多个小点进行分析,对于一些不是该点分析的代码将直接省略:

  • 创建本地node服务器
  • 预构建
  • 请求资源拦截
  • 热更新HMR

createServe思维导图

2. 创建本地node服务器

// 只保留本地node服务器的相关代码
async function createServer(inlineConfig = {}) {
    // 创建http请求
    const middlewares = connect();
    const httpServer = middlewareMode
        ? null
        : await resolveHttpServer(serverConfig, middlewares, httpsOptions);

    const server = {
        config,
        middlewares,
        httpServer,
        watcher,
        pluginContainer: container
        ...,
        async listen(port, isRestart) {
            await startServer(server, port);
            if (httpServer) {
                server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
                if (!isRestart && config.server.open)
                    server.openBrowser();
            }
            return server;
        }
    }
    const initServer = async () => {
        if (serverInited)
            return;
        if (initingServer)
            return initingServer;
        initingServer = (async function () {
            await container.buildStart({});
            initingServer = undefined;
            serverInited = true;
        })();
        return initingServer;
    };
    
    //...
    await initServer();
    return server;
}
上面代码蕴含着多个知识点,我们下面将展开分析

2.1 connect()创建http服务器

Connect模块介绍

Connect是一个Node.js的可扩展HTTP服务框架,用于将各种"middleware"粘合在一起以处理请求

var app = connect();
app.use(function middleware1(req, res, next) {
  // middleware 1
  next();
});
app.use(function middleware2(req, res, next) {
  // middleware 2
  next();
});
var connect = require('connect');
var http = require('http');

var app = connect();

// gzip/deflate outgoing responses
var compression = require('compression');
app.use(compression());

// respond to all requests
app.use(function(req, res){
  res.end('Hello from Connect!\n');
});

//create node.js http server and listen on port
http.createServer(app).listen(3000);

源码分析

会先使用connect()创建middlewares,然后将middlewares作为app属性名传入到resolveHttpServer()
最终也是使用Node.jsHttp模块创建本地服务器

// 只保留本地node服务器的相关代码
async function createServer(inlineConfig = {}) {
    const middlewares = connect();
    const httpServer = middlewareMode
        ? null
        : await resolveHttpServer(serverConfig, middlewares, httpsOptions);
   //...
}
async function resolveHttpServer({ proxy }, app, httpsOptions) {
    if (!httpsOptions) {
        const { createServer } = await import('node:http');
        return createServer(app);
    }
    // #484 fallback to http1 when proxy is needed.
    if (proxy) {
        const { createServer } = await import('node:https');
        return createServer(httpsOptions, app);
    }else {
        const { createSecureServer } = await import('node:http2');
        return createSecureServer({
            // Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
            // errors on large numbers of requests
            maxSessionMemory: 1000,
            ...httpsOptions,
            allowHTTP1: true,
        }, 
        // @ts-expect-error TODO: is this correct?
        app);
    }
}

2.2 启动http服务器

dist/node/cli.js文件的分析中,我们知道
在创建server完成后,我们会调用server.listen()

// dist/node/cli.js
const { createServer } = await import('./chunks/dep-f365bad6.js').then(function (n) { return n.G; });
const server = await createServer({
    root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    optimizeDeps: { force: options.force },
    server: cleanOptions(options),
});
await server.listen();

server.listen()最终调用的也是Node.jsHttp模块的监听方法,即上面Connect模块介绍示例中的http.createServer(app).listen(3000)

async function createServer(inlineConfig = {}) {
    const middlewares = connect();
  const httpServer = middlewareMode
      ? null
      : await resolveHttpServer(serverConfig, middlewares, httpsOptions);
  const server = {
      httpServer,
      //...
      async listen(port, isRestart) {
          await startServer(server, port);
          if (httpServer) {
              server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
              if (!isRestart && config.server.open)
                  server.openBrowser();
          }
          return server;
      }
  };
  }
  async function startServer(server, inlinePort) {
      const httpServer = server.httpServer;
      //...
      await httpServerStart(httpServer, {
          port,
          strictPort: options.strictPort,
          host: hostname.host,
          logger: server.config.logger,
      });
  }
  async function httpServerStart(httpServer, serverOptions) {
      let { port, strictPort, host, logger } = serverOptions;
      return new Promise((resolve, reject) => {
          httpServer.listen(port, host, () => {
              httpServer.removeListener('error', onError);
              resolve(port);
          });
      });
  }

3. 预构建

3.1 预构建的原因

CommonJS 和 UMD 兼容性

在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。
在转换 CommonJS 依赖项时,Vite 会进行智能导入分析,这样即使模块的导出是动态分配的(例如 React),具名导入(named imports)也能正常工作:

// 符合预期
import React, { useState } from 'react'

性能

为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。
有些包将它们的 ES 模块构建为许多单独的文件,彼此导入。例如,lodash-es 有超过 600 个内置模块!当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!即使服务器能够轻松处理它们,但大量请求会导致浏览器端的网络拥塞,使页面加载变得明显缓慢。
通过将 lodash-es 预构建成单个模块,现在我们只需要一个HTTP请求!

注意
依赖预构建仅适用于开发模式,并使用 esbuild 将依赖项转换为 ES 模块。在生产构建中,将使用 @rollup/plugin-commonjs

3.2 预构建整体流程(流程图)

接下来会根据流程图的核心流程进行源码分析

3.3 预构建整体流程(源码整体概述)

Vite 会将预构建的依赖缓存到 node_modules/.vite。它根据几个源来决定是否需要重新运行预构建步骤:

  • 包管理器的 lockfile 内容,例如 package-lock.json,yarn.lock,pnpm-lock.yaml,或者 bun.lockb
  • 补丁文件夹的修改时间
  • 可能在 vite.config.js 相关字段中配置过的
  • NODE_ENV 中的值

只有在上述其中一项发生更改时,才需要重新运行预构建。

我们会先检测是否有预构建的缓存,如果没有缓存,则开始预构建:发现文件依赖并存放于deps,然后将deps打包到node_modules/.vite

async function createServer(inlineConfig = {}) {
    const config = await resolveConfig(inlineConfig, 'serve');
    if (isDepsOptimizerEnabled(config, false)) {
        // start optimizer in the background, we still need to await the setup
        await initDepsOptimizer(config);
    }
    //...
}
async function initDepsOptimizer(config, server) {
    if (!getDepsOptimizer(config, ssr)) {
        await createDepsOptimizer(config, server);
    }
}
async function createDepsOptimizer(config, server) {
    // 第一步:3.4获取缓存
    const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
    // 第二步:3.5没有缓存时进行依赖扫描
    const deps = {};
    discover = discoverProjectDependencies(config);
    const deps = await discover.result;
    // 第三步:3.6没有缓存时进行依赖扫描,然后进行依赖打包到node_modules/.vite
    optimizationResult = runOptimizeDeps(config, knownDeps);
}

3.4 获取缓存loadCachedDepOptimizationMetadata

async function createDepsOptimizer(config, server) {
    // 第一步:3.4获取缓存
    const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
    if (!cachedMetadata) {
        // 第二步:3.5没有缓存时进行依赖扫描
        discover = discoverProjectDependencies(config);
        const deps = await discover.result;
        // 第三步:3.6依赖扫描后进行打包runOptimizeDeps(),存储到node_modules/.vite
        optimizationResult = runOptimizeDeps(config, knownDeps);
    }
}
async function loadCachedDepOptimizationMetadata(config, ssr, force = config.optimizeDeps.force, asCommand = false) {
    const depsCacheDir = getDepsCacheDir(config, ssr);
    if (!force) {
        // 3.4.1 获取_metadata.json文件数据
        let cachedMetadata;
        const cachedMetadataPath = path$o.join(depsCacheDir, '_metadata.json');
        cachedMetadata = parseDepsOptimizerMetadata(await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir);
        // 3.4.2 比对hash值
        if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) {
            return cachedMetadata;
        }
    }
    // 3.4.3 清空缓存
    await fsp.rm(depsCacheDir, { recursive: true, force: true });
}

3.4.1 获取_metadata.json文件数据

通过getDepsCacheDir()获取node_modules/.vite/deps的缓存目录,然后拼接_metadata.json数据,读取文件并且进行简单的整理parseDepsOptimizerMetadata()后形成校验缓存是否过期的数据

const depsCacheDir = getDepsCacheDir(config, ssr);
let cachedMetadata;
const cachedMetadataPath = path$o.join(depsCacheDir, '_metadata.json');
cachedMetadata = parseDepsOptimizerMetadata(await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir);
下面metadata的数据结构是在_metadata.json数据结构的基础上叠加一些数据
function parseDepsOptimizerMetadata(jsonMetadata, depsCacheDir) {
    const { hash, browserHash, optimized, chunks } = JSON.parse(jsonMetadata, (key, value) => {
        if (key === 'file' || key === 'src') {
            return normalizePath$3(path$o.resolve(depsCacheDir, value));
        }
        return value;
    });
    if (!chunks ||
        Object.values(optimized).some((depInfo) => !depInfo.fileHash)) {
        // outdated _metadata.json version, ignore
        return;
    }
    const metadata = {
        hash,
        browserHash,
        optimized: {},
        discovered: {},
        chunks: {},
        depInfoList: [],
    };
    //...处理metadata
    return metadata;
}

3.4.2 比对hash值

if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) {
    return cachedMetadata;
}
最终生成预构建缓存时,_metadata.json中的hash是如何计算的?是根据什么文件得到的hash值?

getDepHash()的逻辑也不复杂,主要的流程为:

  • 先进行lockfileFormats[i]文件是否存在的检测,比如存在yarn.lock,那么就直接返回yarn.lock,赋值给content
  • 检测是否存在patches文件夹,进行content += stat.mtimeMs.toString()
  • 将一些配置数据进行JSON.stringify()添加到content的后面
  • 最终使用content形成对应的hash值,返回该hash

getDepHash()的逻辑总结下来就是:

  • 包管理器的锁文件内容,例如 package-lock.json,yarn.lock,pnpm-lock.yaml,或者 bun.lockb
  • 补丁文件夹的修改时间
  • vite.config.js 中的相关字段
  • NODE_ENV 的值

只有在上述其中一项发生更改时,hash才会发生变化,才需要重新运行预构建

const lockfileFormats = [
    { name: 'package-lock.json', checkPatches: true },
    { name: 'yarn.lock', checkPatches: true },
    { name: 'pnpm-lock.yaml', checkPatches: false },
    { name: 'bun.lockb', checkPatches: true },
];
const lockfileNames = lockfileFormats.map((l) => l.name);

function getDepHash(config, ssr) {
    // 第一部分:获取配置文件初始化content
    const lockfilePath = lookupFile(config.root, lockfileNames);
    let content = lockfilePath ? fs$l.readFileSync(lockfilePath, 'utf-8') : '';
    // 第二部分:检测是否存在patches文件夹,增加content的内容
    if (lockfilePath) {
        //...
        const fullPath = path$o.join(path$o.dirname(lockfilePath), 'patches');
        const stat = tryStatSync(fullPath);
        if (stat?.isDirectory()) {
            content += stat.mtimeMs.toString();
        }
    }
    // 第三部分:将配置添加到content的后面
    const optimizeDeps = getDepOptimizationConfig(config, ssr);
    content += JSON.stringify({
        mode: process.env.NODE_ENV || config.mode,
        //...
    });
    return getHash(content);
}
function getHash(text) {
    return createHash$2('sha256').update(text).digest('hex').substring(0, 8);
}

拿到getDepHash()计算得到的hash,跟目前node_modules/.vite/deps/_metadata.jsonhash属性进行比对,如果一样说明预构建缓存没有任何改变,无需重新预构建,直接使用上次预构建缓存即可

下面是_metadata.json的示例
{
  "hash": "2b04a957",
  "browserHash": "485313cf",
  "optimized": {
    "lodash-es": {
      "src": "../../lodash-es/lodash.js",
      "file": "lodash-es.js",
      "fileHash": "d69f60c8",
      "needsInterop": false
    },
    "vue": {
      "src": "../../vue/dist/vue.runtime.esm-bundler.js",
      "file": "vue.js",
      "fileHash": "98c38b51",
      "needsInterop": false
    }
  },
  "chunks": {}
}

3.4.3 清空缓存

如果缓存过期或者带了force=true参数,代表缓存不可用,使用fsp.rm清空缓存文件夹

"dev": "vite --force"代表不使用缓存
await fsp.rm(depsCacheDir, { recursive: true, force: true });

3.5 没有缓存时进行依赖扫描discoverProjectDependencies()

async function createDepsOptimizer(config, server) {
    // 第一步:3.4获取缓存
    const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
    if (!cachedMetadata) {
        // 第二步:3.5没有缓存时进行依赖扫描
        discover = discoverProjectDependencies(config);
        const deps = await discover.result;
        // 第三步:3.6依赖扫描后进行打包runOptimizeDeps(),存储到node_modules/.vite
        optimizationResult = runOptimizeDeps(config, knownDeps);
    }
}
function discoverProjectDependencies(config) {
    const { cancel, result } = scanImports(config);
    return {
        cancel,
        result: result.then(({ deps, missing }) => {
            const missingIds = Object.keys(missing);
            return deps;
        }),
    };
}
discoverProjectDependencies实际调用就是scanImports
function scanImports(config) {
  // 3.5.1 计算入口文件computeEntries
  const esbuildContext = computeEntries(config).then((computedEntries) => {
        entries = computedEntries;
        // 3.5.2 打包入口文件esbuild插件初始化
        return prepareEsbuildScanner(config, entries, deps, missing, scanContext);
    });
  // 3.5.3 开始打包
  const result = esbuildContext
        .then((context) => {...}
  return {result, cancel}
}

3.5.1 计算入口文件computeEntries()

官方文档关于optimizeDeps.entries可以知道,

  • 默认情况下,Vite 会抓取你的 index.html 来检测需要预构建的依赖项(忽略node_modulesbuild.outDir__tests__coverage
  • 如果指定了build.rollupOptions?.input,即在vite.config.js中配置rollupOptions参数,指定了入口文件,Vite 将转而去抓取这些入口点
  • 如果这两者都不合你意,则可以使用optimizeDeps.entries指定自定义条目——该值需要遵循 fast-glob 模式 ,或者是相对于 Vite 项目根目录的匹配模式数组,可以简单理解为入口文件匹配的正则表达式,可以进行多个文件类型的匹配

如果使用optimizeDeps.entries,注意默认只有 node_modulesbuild.outDir 文件夹会被忽略。如果还需忽略其他文件夹,你可以在模式列表中使用以 ! 为前缀的、用来匹配忽略项的模式
optimizeDeps.entries具体的示例如下所示,详细可以参考 fast-glob 模式

  • file-{1..3}.js — matches files: file-1.js, file-2.js, file-3.js.
  • file-(1|2) — matches files: file-1.js, file-2.js.

本文中我们将直接使用默认的模式,也就是globEntries('**/*.html', config)进行分析,会直接匹配到index.html入口文件

function computeEntries(config) {
    let entries = [];
    const explicitEntryPatterns = config.optimizeDeps.entries;
    const buildInput = config.build.rollupOptions?.input;
    if (explicitEntryPatterns) {
        entries = await globEntries(explicitEntryPatterns, config);
    } else if (buildInput) {
        const resolvePath = (p) => path$o.resolve(config.root, p);
        if (typeof buildInput === 'string') {
            entries = [resolvePath(buildInput)];
        } else if (Array.isArray(buildInput)) {
            entries = buildInput.map(resolvePath);
        } else if (isObject$2(buildInput)) {
            entries = Object.values(buildInput).map(resolvePath);
        }
    } else {
        entries = await globEntries('**/*.html', config);
    }
    entries = entries.filter((entry) => isScannable(entry) && fs$l.existsSync(entry));
    return entries;
}

3.5.2 打包入口文件esbuild插件初始化prepareEsbuildScanner

在上面的分析中,我们执行完3.5.1步骤的computeEntries()后,会执行prepareEsbuildScanner()的插件准备工作

function scanImports(config) {
  // 3.5.1 计算入口文件computeEntries
  const esbuildContext = computeEntries(config).then((computedEntries) => {
        entries = computedEntries;
        // 3.5.2 打包入口文件esbuild插件初始化
        return prepareEsbuildScanner(config, entries, deps, missing, scanContext);
    });
  // 3.5.3 开始打包
  const result = esbuildContext
        .then((context) => {...}
  return {result, cancel}
}
下面将会prepareEsbuildScanner()的流程展开分析

在计算出入口文件后,后面就是启动esbuild插件进行打包,由于打包流程涉及的流程比较复杂,我们在3.5的分析中,只会分析预构建相关的流程部分:

  • 先进行了vite插件的初始化:container = createPluginContainer()
  • 然后将vite插件container作为参数传递到esbuild插件中,后续逻辑需要使用container提供的一些能力
  • 最终进行esbuild打包的初始化,使用3.5.1 计算入口文件computeEntries拿到的入口文件作为stdin,即esbuildinput,然后将刚刚注册的plugin放入到plugins属性中
esbuild相关知识点可以参考【基础】esbuild使用详解或者官方文档
async function prepareEsbuildScanner(config, entries, deps, missing, scanContext) {
    // 第一部分: container初始化
    const container = await createPluginContainer(config);
    if (scanContext?.cancelled)
        return;
    // 第二部分: esbuildScanPlugin()
    const plugin = esbuildScanPlugin(config, container, deps, missing, entries);
    const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {};
    return await esbuild.context({
        absWorkingDir: process.cwd(),
        write: false,
        stdin: {
            contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
            loader: 'js',
        },
        bundle: true,
        format: 'esm',
        logLevel: 'silent',
        plugins: [...plugins, plugin],
        ...esbuildOptions,
    });
}
第一部分createPluginContainer
插件管理类container初始化
async function createServer(inlineConfig = {}) {
    const config = await resolveConfig(inlineConfig, 'serve');
    // config包含了plugins这个属性
    const container = await createPluginContainer(config, moduleGraph, watcher);
}
// ======= 初始化所有plugins =======start
function resolveConfig() {
    resolved.plugins = await resolvePlugins(resolved, prePlugins, normalPlugins, postPlugins);
    return resolved;
}
async function resolvePlugins(config, prePlugins, normalPlugins, postPlugins) {
    return [
        resolvePlugin({ ...}),
        htmlInlineProxyPlugin(config),
        cssPlugin(config),
        ...
    ].filter(Boolean);
}
// ======= 初始化所有plugins =======end
function createPluginContainer(config, moduleGraph, watcher) {
    const { plugins, logger, root, build: { rollupOptions }, } = config;
    const { getSortedPluginHooks, getSortedPlugins } = createPluginHookUtils(plugins);
    const container = {
        async resolveId() {
            //...使用了getSortedPlugins()这个方法,这个方法里有plugins
        }
    }
    return container;
}
function createPluginHookUtils(plugins) {
    function getSortedPlugins(hookName) {
        if (sortedPluginsCache.has(hookName))
            return sortedPluginsCache.get(hookName);
        // 根据hookName,即对象属性名,拼接对应的key-value的plugin
        const sorted = getSortedPluginsByHook(hookName, plugins);
        sortedPluginsCache.set(hookName, sorted);
        return sorted;
    }
}
function getSortedPluginsByHook(hookName, plugins) {
    const pre = [];
    const normal = [];
    const post = [];
    for (const plugin of plugins) {
        const hook = plugin[hookName];
        if (hook) {
            //...pre.push(plugin)
            //...normal.push(plugin)
            //...post.push(plugin) 
        }
    }
    return [...pre, ...normal, ...post];
}

如上面代码所示,在createServer()->resolveConfig()->resolvePlugins()的流程中,会进行vite插件的注册

vite插件具体有什么呢?

所有的插件都放在vite源码的src/node/plugins/**中,每一个插件都会有对应的name,比如下面这个插件vite:css

「vite4源码」dev模式整体流程浅析(一)_第2张图片

常用方法container.resolveId()

从上面插件初始化的分析中,我们可以知道,getSortedPlugins('resolveId')就是检测该插件是否有resolveId这个属性,如果有,则添加到返回的数组集合中,比如有10个插件中有5个插件具有resolveId属性,那么最终getSortedPlugins('resolveId')拿到的就是这5个插件的Array数据

因此container.resolveId()中运行插件的个数不止一个,但并不是每一个插件都能返回对应的结果result,即const result = await handler.call(...)可能为undefined

当有插件处理后result不为undefined时,会直接执行break,然后返回container.resolveId()的结果

//getSortedPlugins最终调用的就是getSortedPluginsByHook
function getSortedPluginsByHook(hookName, plugins) {
    const pre = [];
    const normal = [];
    const post = [];
    for (const plugin of plugins) {
        const hook = plugin[hookName];
        if (hook) {
            //...pre.push(plugin)
            //...normal.push(plugin)
            //...post.push(plugin) 
        }
    }
    return [...pre, ...normal, ...post];
}
async resolveId(rawId, importer = join$2(root, 'index.html'), options) {
    for (const plugin of getSortedPlugins('resolveId')) {
        if (!plugin.resolveId)
            continue;
        if (skip?.has(plugin))
            continue;
        const handler = 'handler' in plugin.resolveId
            ? plugin.resolveId.handler
            : plugin.resolveId;
        const result = await handler.call(...);
        if (!result)
            continue;
        if (typeof result === 'string') {
            id = result;
        } else {
            id = result.id;
            Object.assign(partial, result);
        }
        break;
    }
    if (id) {
        partial.id = isExternalUrl(id) ? id : normalizePath$3(id);
        return partial;
    } else {
        return null;
    }
}
第二部分初始化dep-scan的vite插件
esbuild插件中,提供了两种方法onResolveonLoad
onResolveonLoad的第1个参数为filter(必填)和namespaces(可选)
钩子函数必须提供过滤器filter正则表达式,但也可以选择提供namespaces以进一步限制匹配的路径

按照esbuild插件的onResolve()onLoad()流程进行一系列处理

async function prepareEsbuildScanner(...) {
    const container = await createPluginContainer(config);
    if (scanContext?.cancelled)
        return;
    const plugin = esbuildScanPlugin(...);
    //...省略esbuild打包配置
}
const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/;
function esbuildScanPlugin(config, container, depImports, missing, entries) {
    const resolve = async (id, importer, options) => {
        // 第一部分内容:container.resolveId()
        const resolved = await container.resolveId();
        const res = resolved?.id;
        return res;
    };
    return {
        name: 'vite:dep-scan',
        setup(build) {
            // 第二个部分内容:插件执行流程
            build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
                const resolved = await resolve(path, importer);
                return {
                    path: resolved,
                    namespace: 'html',
                };
            });
            build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {...});
        //....
         }
    }
}

esbuildScanPlugin()的执行逻辑中,如上面代码块注释所示,分为两部分内容:

  • container.resolveId()的具体逻辑,涉及到container的初始化,具体的插件执行等逻辑
  • build.onResolvebuild.onLoad的具体逻辑

下面将使用简单的具体实例按照这两部分内容展开分析:
index.html->main.js->import vue的流程进行分析

index.html文件触发container.resolveId()
当我们执行入口index.html文件的打包解析时,我们通过调试可以知道,我们最终会命中插件:vite:resolve的处理,接下来我们将针对这个插件展开分析

传入参数id就是路径,比如"/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/index.html"或者"/src/main.js"
传入参数importer就是引用它的模块,比如"stdin"或者"/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/index.html"

container.resolveId()逻辑就是根据目前路径的类型,比如是绝对路径、相对路径、模块路或者其他路径类型,然后进行不同的处理,最终返回拼凑好的完整路径

return {
    name: 'vite:resolve',
    async resolveId(id, importer, resolveOpts) {
        //...

        // URL
        // /foo -> /fs-root/foo
        if (asSrc && id[0] === '/' && (rootInRoot || !id.startsWith(root))) {...}
        // relative
        if (id[0] === '.' ||
            ((preferRelative || importer?.endsWith('.html')) &&
                startsWithWordCharRE.test(id))) {...}
        // drive relative fs paths (only windows)
        if (isWindows$4 && id[0] === '/') {...}
        // absolute fs paths
        if (isNonDriveRelativeAbsolutePath(id) &&
            (res = tryFsResolve(id, options))) {...}
        // external
        if (isExternalUrl(id)) {...}
        // data uri: pass through (this only happens during build and will be
        // handled by dedicated plugin)
        if (isDataUrl(id)) {
            return null;
        }
        // bare package imports, perform node resolve
        if (bareImportRE.test(id)) {...}
    }
}
index.html文件触发onResolve和onLoad

一开始我们打包index.html入口文件时

  • 触发filter: htmlTypesRE的筛选,命中onResolve()的处理逻辑,返回namespace: 'html'和整理好的路径path传递给下一个阶段
  • 触发filter: htmlTypesREnamespace: 'html'的筛选条件,命中onLoad()的处理逻辑,使用regex.exec(raw)匹配出index.html中的

    一些原始的文件,比如main.jsmain.cssIndex.vue则直接使用readFile进行内容的读取
    截屏2023-04-07 01.54.53.png
    而一些需要插件进行获取的文件数据,比如Index.vue文件经过transform流程解析

    Index.vue经过vite:vuetransform()转化后的output如下图所示,一共分为4个部分: