前言
最近在学习 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 只有
serve
和build
两个子命令,但实际上还有第三个子命令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.js
或vite.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 排版