vite 源码学习 ⚡

854 阅读4分钟

前言

最近在学习 vite 源码,抱着学习、记录的心态,看到哪就写到哪,十分随意。

vite 是什么

vite 是一个由原生ESM驱动的 Web 开发构建工具。开发环境下使用原生 ESM imports,生产环境下使用Rollup打包。具体介绍:vite github 仓库

CLI

运行npx vite --help会显示vite相关的命令行:

npx vite --help
vite v1.0.0-rc.4

Usage: vite [command] [args] [--options]

Commands:
  vite                       Start server in current directory.
  vite serve [root=cwd]      Start server in target directory.
  vite build [root=cwd]      Build target directory.

Options:
  --help, -h                 [boolean] show help
  --version, -v              [boolean] show version
  --config, -c               [string]  use specified config file
  --port                     [number]  port to use for serve
  --open                     [boolean] open browser on server start
  --base                     [string]  public base path for build (default: /)
  --outDir                   [string]  output directory for build (default: dist)
  --assetsDir                [string]  directory under outDir to place assets in (default: assets)
  --assetsInlineLimit        [number]  static asset base64 inline threshold in bytes (default: 4096)
  --sourcemap                [boolean] output source maps for build (default: false)
  --minify                   [boolean | 'terser' | 'esbuild'] enable/disable minification, or specify
                                       minifier to use. (default: 'terser')
  --mode, -m                 [string]  specify env mode (default: 'development' for dev, 'production' for build)
  --ssr                      [boolean] build for server-side rendering
  --jsx                      ['vue' | 'preact' | 'react']  choose jsx preset (default: 'vue')
  --jsx-factory              [string]  (default: React.createElement)
  --jsx-fragment             [string]  (default: React.Fragment)
  --force                    [boolean] force the optimizer to ignore the cache and re-bundle

虽然上面的结果显示 vite 只有servebuild两个子命令,但实际上还有第三个子命令optimize

vite 没有使用commander这样比较重的 CLI 辅助库,而是使用minimist来帮助解析参数:

const argv = require('minimist')(process.argv.slice(2))

举个例子,像 vite serve --config vite.dev.js --force这样的命令,解析出来的结果是:

argv: {
  _: [ 'serve' ],
  config: 'vite.dev.js',
  force: true,
}

这样就可以通过argv._[0]拿到执行的子命令。

如果通过-c--config参数指定了配置文件的路径,或者当前工作目录下有 vite.config.{js|ts},则会解析该配置文件,然后把 CLI 参数和配置文件做合并,其中 CLI 参数会有更高的优先级:

async function resolveOptions(mode: string) {
  // ...
  // 解析配置文件
  const userConfig = await resolveConfig(mode, argv.config || argv.c)
  // 合并 CLI 参数和配置文件
  if (userConfig) {
    return {
      ...userConfig,
      ...argv // cli options take higher priority
    }
  }
  // ...
}

解析完配置后就会执行相应的子命令:

// 在执行 build 时是 'production',执行其他命令时是 'development'
// 另,command 即上文提到的 argv._[0]
const defaultMode = command === 'build' ? 'production' : 'development'

const envMode = mode || m || defaultMode
// 解析配置
const options = await resolveOptions(envMode)
process.env.NODE_ENV = process.env.NODE_ENV || envMode
if (!options.command || options.command === 'serve') {
  runServe(options)
} else if (options.command === 'build') {
  runBuild(options)
} else if (options.command === 'optimize') {
  runOptimize(options)
} else {
  console.error(chalk.red(`unknown command: ${options.command}`))
  process.exit(1)
}

配置

类型

从 TS 类型上看,vite 有四种配置类型:

  • SharedConfig。server 和 build 共用的配置

  • ServerConfig。dev 服务器的配置,继承 SharedConfig,添加了端口、代理等配置。

  • BuildConfig。打包构建的配置,继承 SharedConfig,添加了 esbuild、rollup 等配置。

  • UserConfig。用户自定义的配置,继承 ServerConfig 和 BuildConfig,添加了 vite 插件的配置。

vite 改动十分频繁,配置项就不一一列出了,而且代码里的注释也十分详细,看这里

解析配置文件

vite 默认会解析配置文件vite.config.jsvite.config.ts

对于使用 Node 模块语法的 JS 配置文件,vite 会直接require

if (!isTS) {
  try {
    config = require(resolvedPath)
  } catch (e) {
    // 如果是 ESM 则会报错,走后续的编译流程
    if (
      !/Cannot use import statement|Unexpected token 'export'/.test(
        e.message
      )
    ) {
      throw e
    }
  }
}

从这里其实可以看出,当使用-c--config时,可以使用JSON作为配置文件,因为 JSON 文件在 Node 里可以被正常require

当配置文件使用 ESM 或 TS 时,vite 会通过 rollup + esbuild 进行编译:

const rollup = require('rollup') as typeof Rollup
const esbuildPlugin = await createEsbuildPlugin({})
const esbuildRenderChunkPlugin = createEsbuildRenderChunkPlugin(
  'es2019',
  false
)
// use node-resolve to support .ts files
const nodeResolve = require('@rollup/plugin-node-resolve').nodeResolve({
  extensions: supportedExts
})
const bundle = await rollup.rollup({
  external: (id: string) =>
    (id[0] !== '.' && !path.isAbsolute(id)) ||
    id.slice(-5, id.length) === '.json',
  input: resolvedPath,
  treeshake: false,
  plugins: [esbuildPlugin, nodeResolve, esbuildRenderChunkPlugin]
})

const {
  output: [{ code }]
} = await bundle.generate({
  exports: 'named',
  format: 'cjs'
})

可能会疑惑为什么使用了两个 esbuild 插件,其实这两个插件不仅用在了编译配置文件,还用在vite build,其中esbuildPlugin用于转换代码,esbuildRenderChunkPlugin用于降低代码版本和压缩。这里给 renderChunk 配置es2019是为了转换配置文件中的可选链语法,相关 PR 在这里

esbuildRenderChunkPlugin原本的名称叫esbuildMinifyPlugin,只用于压缩代码。

经过 rollup 生成的code只是string,还不是最终 vite 需要的配置对象,所以还需要进一步的转换:

config = await loadConfigFromBundledFile(resolvedPath, code)
async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string
): Promise<UserConfig> {
  const extension = path.extname(fileName)
  const defaultLoader = require.extensions[extension]!
  require.extensions[extension] = (module: NodeModule, filename: string) => {
    if (filename === fileName) {
      ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
    } else {
      defaultLoader(module, filename)
    }
  }
  delete require.cache[fileName]
  const raw = require(fileName)
  const config = raw.__esModule ? raw.default : raw
  require.extensions[extension] = defaultLoader
  return config
}

vite 扩展require.extensions某个后缀的 loader,如果命中目标文件,把上一步得到的 rollup code 传给_compile方法。然后再执行require,就能得到想要的配置对象,最后再把require.extensions的后缀 loader 恢复原样。

简单补充下 Node 模块加载的说明,当我们在 require 一个文件时,会根据文件后缀从Module._extensions(即require.extensions)中执行对应的加载方法,例如原生的.js加载方法如下:

Module._extensions['.js'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
};

其中Module.prototype._compile主要用于编译执行代码。关于 Node Module 具体可以看下这里

未完待续...

本文使用 mdnice 排版