vite源码解析记录(一、本地启动 / 解析vite配置)

498 阅读3分钟

前言

vite在本地启动服务的时候,第一步会先解析配置

async function createServer(inlineConfig){
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    ...
}

这个配置可能是来自于inlineCongfig,也有可能来自于vite.config.js配置文件,他们的关系如下

image.png

通过这个关系分析我们知道最终返回经过处理的resolveConfig,主要流程如下

image.png

流程大概就是这个样子,具体实现可以接着往下看

解析vite配置

参考文章:
juejin.cn/post/710491…

获取vite.config配置

这部分主要流程如下,最终的config就是我们的所有的配置

// 命令行传入的配置
let config = inlineConfig;
// 配置文件的依赖,配置文件通过esbuild打包后的metafile,热更新使用
let configFileDependencies = [];

// 获取配置文件中的配置项,与inlineConfig合并
const loadResult = await loadConfigFromFile(configEnv, configFile, config.root, config.logLevel);
config = mergeConfig(loadResult.config, config);
configFile = loadResult.path;
configFileDependencies = loadResult.dependencies;

loadConfigFromFile()

可以看到获取的主要逻辑都集中在loadConfigFromFile这个方法,具体实现如下

async function loadConfigFromFile(configEnv, configFile, configRoot = process.cwd(), logLevel) {
    // 配置文件路径,下面是获取这个路径的步骤,一堆判断不重要
    let resolvedPath;
    ...
    
    // 判断是否是ESM,先看配置文件后缀,再看package.json中的type
    let isESM = false;
    ...
    
    // esbuild打包配置文件,目的是转换 TS 语法和获取参与打包的本地文件依赖dependencies
    // dependencies:获取参与打包的本地文件依赖,可以从打包结果的 meta 数据中拿到。用于配置的热更新,参与打包的文件依赖改变,需要自动重启
    const bundled = await bundleConfigFile(resolvedPath, isESM);
    // 将打包之后的代码转成配置对象
    const userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code, isESM);
    const config = await (typeof userConfig === 'function'
        ? userConfig(configEnv)
        : userConfig);
    return {
        path: normalizePath(resolvedPath),
        config,
        dependencies: bundled.dependencies
    };
}

bundleConfigFile()

bundleConfigFile就是esbuild的使用了,主要就是一些配置的理解,除此之外还有两个内部实现的插件

plugins: [
  {
    name: 'externalize-deps',
    setup(build) {
      // 当引入另外一个模块时,如果匹配 filter 的正则表达式,则执行后面定义的回调
      build.onResolve({ filter: /.*/ }, ({ path: id, importer }) => {
        // 将裸模块设置为external
        // 裸模块:例如 `import { createApp } from "vue"`,vue 就是没有任何路径的裸模块
        // external:热更新时,只需要监听本地配置文件及本地依赖的更改,不需要监听 npm 包的改变
        if (id[0] !== '.' && !path.isAbsolute(id)) {
          return {
            external: true
          }
        }
        // 下面说的是,monorepo环境下,vite做了妥协,如果引用了其他workspace里面的文件,则将这个文件设置为external,即使违背了external的初衷
        // 见 https://github.com/vitejs/vite/pull/9140
        const idFsPath = path.resolve(path.dirname(importer), id)
        const idPkgPath = lookupFile(idFsPath, [`package.json`], {
          pathOnly: true
        })
        if (idPkgPath) {
          const idPkgDir = path.dirname(idPkgPath)
          if (path.relative(idPkgDir, fileName).startsWith('..')) {
            return {
              path: isESM ? pathToFileURL(idFsPath).href : idFsPath,
              external: true
            }
          }
        }
      })
    }
  },
  {
    name: 'inject-file-scope-variables',
    setup(build) {
      // todo
      build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
        const contents = await fs.promises.readFile(args.path, 'utf8')
        const injectValues =
          `const ${dirnameVarName} = ${JSON.stringify(
            path.dirname(args.path)
          )};` +
          `const ${filenameVarName} = ${JSON.stringify(args.path)};` +
          `const ${importMetaUrlVarName} = ${JSON.stringify(
            pathToFileURL(args.path).href
          )};`

        return {
          loader: args.path.endsWith('ts') ? 'ts' : 'js',
          contents: injectValues + contents
        }
      })
    }
  }
]

对于第一个插件,来看一个真实的例子,下面是一个 vite.config.ts 的代码:

// vite.config.ts
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import { vueI18nPlugin } from './CustomBlockPlugin'

export default defineConfig({
  plugins: [
    vuePlugin({
      reactivityTransform: true
    }),
    splitVendorChunkPlugin(),
    vueI18nPlugin
  ]
})

经过 bundleConfigFile 函数的处理(并非 esbuild 的执行结果,bundleConfigFile 函数只取了部分的 esbuild 打包结果),有以下的执行结果:

{
    code: '打包后的 js 代码文本',
    dependencies: ["CustomBlockPlugin.ts", "vite.config.ts"]
}

dependencies 是参与打包的文件(依赖) ,取值为 Object.keys(result.metafile.inputs),裸模块并没有被打包进来

因此,一般情况下,dependencies 只有本地写的配置文件及本地依赖

loadConfigFromBundledFile()

除了bundleConfigFile还有一个方法loadConfigFromBundledFile,这里主要是想讲一下这个方法内部使用了一个自定义导入dynamicImport,代码如下

const dynamicImport = new Function('file', 'return import(file)')

我们知道加载ESM的文件可以使用import来引入,这么做的目的主要是 :使用 new Function 实现的动态 import,在构建打包 vite 源码时,不会被 Rollup 打包到 vite 的构建产物中,因为这里是字符串的import(file),只会在使用的时候去引入这个临时文件

为什么不能一起打包?

  • 配置文件,不属于 vite 源码的一部分,不是 vite 源码的依赖,不能打包到 vite 源码
  • 配置文件在 vite 源码打包过程中,并不存在
  • 配置文件是在 vite 实际运行中,才被动态引入的

这里还要区分 vite 源码打包过程和 vite 打包项目的过程:

  • vite 源码打包:打包产物是 vite 这个工具的代码
  • vite 项目打包:打包产物是项目的代码,该过程才会有 vite 配置文件

解析插件,插件排序,执行插件config hook

vite有两种插件,一种是config.plugins中配置的普通插件,还有一种config.worker.plugins插件,这个专门为了web Worker提供的插件配置。关于web Worker可以看阮一峰的博客

vite在这部分对两种插件做了排序和扁平化,执行config hook

// 解析插件
const rawUserPlugins = (await asyncFlatten(config.plugins || [])).filter((p) => {
    // 过滤假值
    if (!p) {
        return false;
    }
    // 如果没有apply,表示传过来的是对象且没有自定义apply参数
    else if (!p.apply) {
        return true;
    }
    // 如果apply是函数,则表示插件还没有执行,这里调用apply改变上下文并返回结果
    else if (typeof p.apply === 'function') {
        return p.apply({ ...config, mode }, configEnv);
    }
    // 这里就是自定义apply参数,如果apply为'build'则忽略
    else {
        return p.apply === command;
    }
});
// 根据插件的enforce字段进行排序
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)
// 循环vite.config中的插件,如果有config钩子在这里执行
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins];
    for (const p of userPlugins) {
        if (p.config) {
            const res = await p.config(config, configEnv);
            if (res) {
                config = mergeConfig(config, res);
            }
        }
    }

处理别名

// vite内部env和client别名,暂时不知道做什么用
// ENV_ENTRY:'D:\\document\\vue\\ANALYSIS\\vite\\packages\\vite\\dist\\client\\env.mjs'
// CLIENT_ENTRY: 'D:\\document\\vue\\ANALYSIS\\vite\\packages\\vite\\dist\\client\\client.mjs'
const clientAlias = [
    { find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
    { find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY }
];
// 解析vite.config中配置的别名和clientAlias
const resolvedAlias = normalizeAlias(mergeAlias(clientAlias, config.resolve?.alias || []));
const resolveOptions = {
    ...config.resolve,
    alias: resolvedAlias
};

返回最终结果

image.png

加载.env文件

function loadEnv(
  mode, // 模式
  envDir, // .env文件路径
  prefixes = 'VITE_'
) {
  ...
  // 数组化
  prefixes = arraify(prefixes)
  // 环境变量对象
  const env = {}
  const envFiles = [
    /** mode local file */ `.env.${mode}.local`,
    /** mode file */ `.env.${mode}`,
    /** local file */ `.env.local`,
    /** default file */ `.env`
  ]
  ...
  for (const file of envFiles) {
    const path = lookupFile(envDir, [file], { pathOnly: true, rootDir: envDir })
    // 如果环境变量文件存在,使用dotenv解析环境变量并返回
    if (path) {
    ...
    }
  }
  // e.g. { VITE_test:'test111' }
  return env
}

解析baseUrl

开发模式下如果是'',/,./都会转成/,其他情况就是在判断写法合不合规范,例如要以/开头和结尾

解析打包配置

这块打包的时候再看

内部解析器

主要作用:

create an internal resolver to be used in special scenarios, e.g.
optimizer & handling css @imports