一、vite是什么?
Vite 是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
- 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
- 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
Vite 意在提供开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并有完整的类型支持。本篇主要分析npm run dev时发生了什么
二、环境准备
1.下载vite源码
从github上下载vite2.7.2的源码,本文下载到了D:\workspace\vue3\vite-2.7.2目录下。下载完成之后cd到vite-2.7.2目录,cmd执行pnpm install(pnpm如何安装),安装完成之后cd到packages/vite,cmd中执行pnpm run dev,构建完成之后就可以将vite命令软链到全局,进入到packages/vite下,执行pnpm link --global,link之后如图:
pnpm 报如下错误的解决方案
pnpm link --global 这个命令报错 Run "pnpm setup" to create it automatically, or set the global-bin-dir setting, or the PNPM_HOME env variable.
The global bin directory should be in the PATH
执行pnpm config delete global-bin-dir 然后在执行pnpm link --global
2.创建vue3项目
使用vite命令创建vue3项目,cmd D:\workspace\vue3中(实际操作时可以创建到任意盘符,此处只是为了下文路径描述的准确,将项目创建在D:\workspace\vue3中),打开cmd执行
npm create vite@latest vitelearn -- --template vue
项目结构如图:
从package.json中删除掉vite,然后执行npm install安装依赖。然后执行pnpm link --global vite,此时会将本地的vite2.7.2的源码依赖进来。
如图node_modules目录下的vite右侧出现了一个箭头。
安装完毕之后执行npm run dev时,node会执行node_modules\.bin\vite.cmd中的脚本,vite.cmd内容:
脚本意思就是判断有没有NODE_PATH这个环境变量,如果有那么执行 C:\Users\EDZ\AppData\Roaming\npm\pnpm-global\5\node_modules\vite\bin\vite.js的vite.js文件,该文件其实就是D:\workspace\vue3\vite-2.7.2\packages\vite\bin\vite.js文件,
vite.js关键代码如下:
代码1
D:\workspace\vue3\vite-2.7.2\packages\vite\bin\vite.js
function start() {
require('../dist/node/cli')
}
if (profileIndex > 0) {
process.argv.splice(profileIndex, 1)
const next = process.argv[profileIndex]
if (next && !next.startsWith('-')) {
process.argv.splice(profileIndex, 1)
}
const inspector = require('inspector')
const session = (global.__vite_profile_session = new inspector.Session())
session.connect()
session.post('Profiler.enable', () => {
session.post('Profiler.start', start)
})
} else {
start()
}
因为执行npm run dev时并没有传递--profile参数,所以profileIndex=-1,也就是直接执行start方法,start方法加载了D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\cli.ts文件,关键代码如下:
代码2
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\cli.ts
cli
.command('[root]') // default command
.alias('serve') // the command is called 'serve' in Vite's API
.alias('dev') // alias to align with the script name
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`)
.option('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle`
)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
// output structure is preserved even after bundling so require()
// is ok here
const { createServer } = await import('./server')
try {
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options)
})
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
await server.listen()
const info = server.config.logger.info
info(
colors.cyan(`\n vite v${require('vite/package.json').version}`) +
colors.green(` dev server running at:\n`),
{
clear: !server.config.logger.hasWarned
}
)
server.printUrls()
// @ts-ignore
if (global.__vite_start_time) {
// @ts-ignore
const startupDuration = performance.now() - global.__vite_start_time
info(
`\n ${colors.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`
)
}
} catch (e) {
createLogger(options.logLevel).error(
colors.red(`error when starting dev server:\n${e.stack}`),
{ error: e }
)
process.exit(1)
}
})
调用createServer方法创建本地服务器
代码3
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\index.ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
const httpsOptions = await resolveHttpsConfig(
config.server.https,
config.cacheDir
)
...省略代码
}
接着看下resolveConfig方法
代码4
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig> {
...省略代码
//指明要使用的配置文件。如果没有设置,Vite 将尝试从项目根目录自动解析。设置为 false 可以禁用自动解析功能
let { configFile } = config
if (configFile !== false) {
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel
)
if (loadResult) {
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
configFileDependencies = loadResult.dependencies
}
}
...省略代码
}
执行npm run dev时没有禁用configFile的自动解析功能,看下loadConfigFromFile方法
代码5
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function loadConfigFromFile(
configEnv: ConfigEnv,
configFile?: string,
configRoot: string = process.cwd(),
logLevel?: LogLevel
): Promise<{
path: string
config: UserConfig
dependencies: string[]
} | null> {
...省略代码
try {
let userConfig: UserConfigExport | undefined
...省略代码
if (!userConfig) {
// Bundle config file and transpile it to cjs using esbuild.
//resolvedPath='D:\\workspace\\vue3\\vitelearn\\vite.config.ts'
const bundled = await bundleConfigFile(resolvedPath)
dependencies = bundled.dependencies
userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
debug(`bundled config file loaded in ${getTime()}`)
}
const config = await (typeof userConfig === 'function'
? userConfig(configEnv)
: userConfig)
if (!isObject(config)) {
throw new Error(`config must export or return an object.`)
}
return {
path: normalizePath(resolvedPath),
config,
dependencies
}
} catch (e) {
...省略代码
}
}
来看下bundleConfigFile方法
代码6
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
async function bundleConfigFile(
fileName: string,
isESM = false
): Promise<{ code: string; dependencies: string[] }> {
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
outfile: 'out.js',
write: false,
platform: 'node',
bundle: true,
format: isESM ? 'esm' : 'cjs',
sourcemap: 'inline',
metafile: true,
plugins: [
{
name: 'externalize-deps',
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
const id = args.path
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true
}
}
})
}
},
{
name: 'replace-import-meta',
setup(build) {
build.onLoad({ filter: /\.[jt]s$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf8')
return {
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents
.replace(
/\bimport\.meta\.url\b/g,
JSON.stringify(`file://${args.path}`)
)
.replace(
/\b__dirname\b/g,
JSON.stringify(path.dirname(args.path))
)
.replace(/\b__filename\b/g, JSON.stringify(args.path))
}
})
}
}
]
})
const { text } = result.outputFiles[0]
return {
code: text,
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
}
}
调用esbuild对vite.config.ts进行编译,text即为编译后的内容,此时result如图:
所以其dependencies为[vite.config.ts],代码6返回了vite.config.ts编译的内容以及vite.config.ts的依赖项,回到代码5,代码继续往下执行
代码8
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function loadConfigFromFile(
configEnv: ConfigEnv,
configFile?: string,
configRoot: string = process.cwd(),
logLevel?: LogLevel
): Promise<{
path: string
config: UserConfig
dependencies: string[]
} | null> {
...省略代码
try {
let userConfig: UserConfigExport | undefined
...省略代码
if (!userConfig) {
// Bundle config file and transpile it to cjs using esbuild.
//resolvedPath='D:\\workspace\\vue3\\vitelearn\\vite.config.ts'
const bundled = await bundleConfigFile(resolvedPath)
//dependencies=['vite.config.ts']
dependencies = bundled.dependencies
userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
debug(`bundled config file loaded in ${getTime()}`)
}
...省略代码
} catch (e) {
...省略代码
}
}
接着看loadConfigFromBundledFile方法
代码9
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
//fileName='D:\\workspace\\vue3\\vitelearn\\vite.config.ts'
//bundledCode='vite.config.ts文件编译后的内容'
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)
}
}
// clear cache in case of server restart
delete require.cache[require.resolve(fileName)]
const raw = require(fileName)
const config = raw.__esModule ? raw.default : raw
require.extensions[extension] = defaultLoader
return config
}
此时config如图:
回到代码4处,代码4继续往下执行
代码10
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig> {
...省略代码
if (configFile !== false) {
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel
)
if (loadResult) {
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
configFileDependencies = loadResult.dependencies
}
}
...省略代码
}
此时loadResult如图:
调用mergeConfig将两个配置项做合并,代码10继续往下执行
代码11
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig> {
...省略代码
//将插件分为前置插件、正常插件、后置插件
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)
//调用每个插件的config方法,vite独有的钩子,此时userPlugins只有vuePlugin一个插件
for (const p of userPlugins) {
if (p.config) {
const res = await p.config(config, configEnv)
if (res) {
config = mergeConfig(config, res)
}
}
}
...省略代码
//注册别名插件
const clientAlias = [
{ find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
{ find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY }
]
// resolve alias with internal client alias
const resolvedAlias = mergeAlias(
// @ts-ignore because @rollup/plugin-alias' type doesn't allow function
// replacement, but its implementation does work with function values.
clientAlias,
config.resolve?.alias || config.alias || []
)
const resolveOptions: ResolvedConfig['resolve'] = {
dedupe: config.dedupe,
...config.resolve,
alias: resolvedAlias
}
...省略代码
//注册一系列插件,例如:resolvePlugin、htmlInlineScriptProxyPlugin、cssPlugin、jsonPlugin、definePlugin、cssPostPlugin、clientInjectionsPlugin、importAnalysisPlugin等
;(resolved.plugins as Plugin[]) = await resolvePlugins(
resolved,
prePlugins,
normalPlugins,
postPlugins
)
// call configResolved hooks
//循环调用用户插件中的configResolved方法
await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))
...省略代码
return resolved
}
回到代码3,代码继续往下执行,主要是对server注册一些中间件以及执行依赖预构建
代码12
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\index.ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
...省略代码
//循环调用每个插件的configureServer方法
for (const plugin of config.plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
...省略代码
if (!middlewareMode && httpServer) {
let isOptimized = false
// overwrite listen to run optimizer before server start
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, ...args: any[]) => {
if (!isOptimized) {
try {
await container.buildStart({})
await runOptimize()
isOptimized = true
} catch (e) {
httpServer.emit('error', e)
return
}
}
return listen(port, ...args)
}) as any
} else {
...省略代码
}
...省略代码
return server
}
看下container.buildStart({})方法
代码13
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts
async buildStart() {
await Promise.all(
plugins.map((plugin) => {
if (plugin.buildStart) {
return plugin.buildStart.call(
new Context(plugin) as any,
container.options as NormalizedInputOptions
)
}
})
)
},
循环调用每个插件的buildStart方法,做一些初始化的工作,代码12继续往下执行,执行runOptimize,也即依赖预构建
代码14
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\index.ts
const runOptimize = async () => {
server._isRunningOptimizer = true
try {
server._optimizeDepsMetadata = await optimizeDeps(
config,
config.server.force || server._forceOptimizeOnRestart
)
} finally {
server._isRunningOptimizer = false
}
server._registerMissingImport = createMissingImporterRegisterFn(server)
}
来看下optimizeDeps方法
代码15
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\index.ts
export async function optimizeDeps(
config: ResolvedConfig,
force = config.server.force,//设置为 true 强制使依赖预构建
asCommand = false,
newDeps?: Record<string, string>, // missing imports encountered after server has started
ssr?: boolean
): Promise<DepOptimizationMetadata | null> {
...省略代码
//先判断D:\\workspace\\vue3\\vitelearn\\node_modules\\.vite目录中是否存在_metadata.json,如果存在说明之前已经编译过了,直接读取缓存,如果不存在那么创建缓存目录
const dataPath = path.join(cacheDir, '_metadata.json')
const mainHash = getDepHash(root, config)
const data: DepOptimizationMetadata = {
hash: mainHash,
browserHash: mainHash,
optimized: {}
}
if (!force) {
let prevData: DepOptimizationMetadata | undefined
try {
prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))
} catch (e) {}
// hash is consistent, no need to re-bundle
if (prevData && prevData.hash === data.hash) {
log('Hash is consistent. Skipping. Use --force to override.')
return prevData
}
}
if (fs.existsSync(cacheDir)) {
emptyDir(cacheDir)
} else {
fs.mkdirSync(cacheDir, { recursive: true })
}
// a hint for Node.js
// all files in the cache directory should be recognized as ES modules
writeFile(
path.resolve(cacheDir, 'package.json'),
JSON.stringify({ type: 'module' })
)
let deps: Record<string, string>, missing: Record<string, string>
if (!newDeps) {
;({ deps, missing } = await scanImports(config))
} else {
deps = newDeps
missing = {}
}
...省略文件
return data
}
因为是初次运行,所以肯定不存在缓存目录。来看下scanImports方法
代码16
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
export async function scanImports(config: ResolvedConfig): Promise<{
deps: Record<string, string>
missing: Record<string, string>
}> {
...省略代码
//默认情况下,Vite 会抓取你的 index.html 来检测需要预构建的依赖项。如果指定了
build.rollupOptions.input,Vite 将转而去抓取这些入口点
const explicitEntryPatterns = config.optimizeDeps.entries
const buildInput = config.build.rollupOptions?.input
if (explicitEntryPatterns) {
entries = await globEntries(explicitEntryPatterns, config)
} else if (buildInput) {
const resolvePath = (p: string) => path.resolve(config.root, p)
if (typeof buildInput === 'string') {
entries = [resolvePath(buildInput)]
} else if (Array.isArray(buildInput)) {
entries = buildInput.map(resolvePath)
} else if (isObject(buildInput)) {
entries = Object.values(buildInput).map(resolvePath)
} else {
throw new Error('invalid rollupOptions.input value.')
}
} else {
entries = await globEntries('**/*.html', config)//抓取index.html来检测需要预构建的依赖项
}
...省略代码
const deps: Record<string, string> = {}
const missing: Record<string, string> = {}
const container = await createPluginContainer(config)
const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {}
await Promise.all(
entries.map((entry) =>
build({
absWorkingDir: process.cwd(),
write: false,
entryPoints: [entry],
bundle: true,
format: 'esm',
logLevel: 'error',
plugins: [...plugins, plugin],
...esbuildOptions
})
)
)
debug(`Scan completed in ${(performance.now() - start).toFixed(2)}ms:`, deps)
return {
deps,
missing
}
}
对入口文件循环进行构建时会回调plugin相关的方法,来看下esbuildScanPlugin方法
代码17
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
}
}
}
构建index.html时先匹配上了
代码18
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
return {
path: await resolve(path, importer),
namespace: 'html'
}
})
...省略代码
}
}
...省略代码
}
看下resolve方法
代码19
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
const seen = new Map<string, string | undefined>()
//id='D:/workspace/vue3/vitelearn/index.html'
//importer=''
const resolve = async (id: string, importer?: string) => {
const key = id + (importer && path.dirname(importer))
//如果已经解析过了那么读缓存中的结果
if (seen.has(key)) {
return seen.get(key)
}
const resolved = await container.resolveId(
id,
importer && normalizePath(importer)
)
const res = resolved?.id
seen.set(key, res)
return res
}
...省略代码
}
然后调用container.resolveId方法
代码20
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts
//rawId='D:/workspace/vue3/vitelearn/index.html'
//importer=''
//options=undefined
async resolveId(rawId, importer = join(root, 'index.html'), options) {
...省略代码
const ctx = new Context()
...省略代码
for (const plugin of plugins) {
...省略代码
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{ ssr }
)
if (!result) continue
...省略代码
break
}
...省略代码
if (id) {
partial.id = isExternalUrl(id) ? id : normalizePath(id)
return partial as PartialResolvedId
} else {
return null
}
}
循环调用每个插件的resolveId方法,被resolvePlugin插件匹配到,看下resolvePlugin的resolveId方法
代码21
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
...省略代码
return {
name: 'vite:resolve',
...省略代码
//id='D:/workspace/vue3/vitelearn/index.html'
//importer=''
resolveId(id, importer, resolveOpts) {
...省略代码
// absolute fs paths
// 绝对路径匹配成功
if (path.isAbsolute(id) && (res = tryFsResolve(id, options))) {
isDebug && debug(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`)
//res='D:/workspace/vue3/vitelearn/index.html'
return res
}
...省略代码
}
//省略代码
}
}
回到代码18,代码18返回如下内容
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
return {
path: await resolve(path, importer),//path='D:/workspace/vue3/vitelearn/index.html'
namespace: 'html'
}
})
...省略代码
}
}
...省略代码
}
代码18继续往下执行,又匹配到如下的规则
代码22
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='D:/workspace/vue3/vitelearn/index.html'
build.onLoad({ filter: htmlTypesRE, namespace: 'html' },async ({ path }) => {
let raw = fs.readFileSync(path, 'utf-8')//读取index.html文件的内容
// Avoid matching the content of the comment
raw = raw.replace(commentRE, '<!---->')
const isHtml = path.endsWith('.html')
const regex = isHtml ? scriptModuleRE : scriptRE
regex.lastIndex = 0
let js = ''
let loader: Loader = 'js'
let match: RegExpExecArray | null
while ((match = regex.exec(raw))) {
const [, openTag, content] = match
const typeMatch = openTag.match(typeRE)
const type = typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
const langMatch = openTag.match(langRE)
const lang = langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
// skip type="application/ld+json" and other non-JS types
if (
type &&
!(
type.includes('javascript') ||
type.includes('ecmascript') ||
type === 'module'
)
) {
continue
}
if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
loader = lang
}
//匹配index.html中的<script type="module" src="/src/main.ts"></script>
const srcMatch = openTag.match(srcRE)
if (srcMatch) {
const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
js += `import ${JSON.stringify(src)}\n`
} else if (content.trim()) {
// There can be module scripts (`<script context="module">` in Svelte and `<script>` in Vue)
// or local scripts (`<script>` in Svelte and `<script setup>` in Vue)
// We need to handle these separately in case variable names are reused between them
const contextMatch = openTag.match(contextRE)
const context =
contextMatch &&
(contextMatch[1] || contextMatch[2] || contextMatch[3])
if (
(path.endsWith('.vue') && setupRE.test(openTag)) ||
(path.endsWith('.svelte') && context !== 'module')
) {
// append imports in TS to prevent esbuild from removing them
// since they may be used in the template
const localContent =
content +
(loader.startsWith('ts') ? extractImportPaths(content) : '')
localScripts[path] = {
loader,
contents: localContent
}
js += `import '${virtualModulePrefix}${path}';\n`
} else {
js += content + '\n'
}
}
}
// `<script>` in Svelte has imports that can be used in the template
// so we handle them here too
if (loader.startsWith('ts') && path.endsWith('.svelte')) {
js += extractImportPaths(js)
}
// This will trigger incorrectly if `export default` is contained
// anywhere in a string. Svelte and Astro files can't have
// `export default` as code so we know if it's encountered it's a
// false positive (e.g. contained in a string)
if (!path.endsWith('.vue') || !js.includes('export default')) {
js += '\nexport default {}'
}
if (js.includes('import.meta.glob')) {
return {
// transformGlob already transforms to js
loader: 'js',
contents: await transformGlob(js, path, config.root, loader)
}
}
//此时js='import /src/main.ts\n\nexport default {}'
//loader='js'
return {
loader,
contents: js
}
...省略代码
}
}
...省略代码
}
返回的contents中有import /src/main.ts内容,然后import /src/main.ts匹配到
代码23
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//id='/src/main.ts'
//importer='D:/workspace/vue3/vitelearn/index.html'
build.onResolve({filter: /.*/}, async ({ path: id, importer }) => {
// use vite resolver to support urls and omitted extensions
const resolved = await resolve(id, importer)
if (resolved) {
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id })
}
const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined
return {
path: path.resolve(cleanUrl(resolved)),
namespace: undefined
}
} else {
// resolve failed... probably unsupported type
return externalUnlessEntry({ path: id })
}
}
)
...省略代码
}
再看下resolve方法
代码24
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
const seen = new Map<string, string | undefined>()
//id='/src/main.ts'
//importer='D:/workspace/vue3/vitelearn/index.html'
const resolve = async (id: string, importer?: string) => {
const key = id + (importer && path.dirname(importer))
if (seen.has(key)) {
return seen.get(key)//如果之前已经解析过路径了,那么直接读取缓存
}
const resolved = await container.resolveId(
id,
importer && normalizePath(importer)
)
const res = resolved?.id
seen.set(key, res)
return res
}
...省略代码
}
然后调用container.resolveId方法
代码25
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts
//rawId='/src/main.ts' importer='D:/workspace/vue3/vitelearn/index.html'
async resolveId(rawId, importer = join(root, 'index.html'), options) {
...省略代码
const ctx = new Context()
...省略代码
for (const plugin of plugins) {
...省略代码
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{ ssr }
)
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(id)
return partial as PartialResolvedId
} else {
return null
}
}
循环调用每个插件的resolveId方法,rawId='/src/main.ts',被D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts的resolvePlugin插件匹配上,看下该插件的resolveId方法
代码26
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
...省略代码
return {
name: 'vite:resolve',
//id='/src/main.ts' importer='D:/workspace/vue3/vitelearn/index.html'
resolveId(id, importer, resolveOpts) {
...省略代码
// id='/src/main.ts'满足该条件
if (asSrc && id.startsWith('/')) {
const fsPath = path.resolve(root, id.slice(1))
if ((res = tryFsResolve(fsPath, options))) {
isDebug && debug(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`)
//res='D:/workspace/vue3/vitelearn/src/main.ts'
return res
}
}
...省略代码
}
...省略代码
}
}
所以最终代码23返回了
build.onResolve(
{
filter: /.*/
},
//id='/src/main.ts'
async ({ path: id, importer }) => {
...省略代码
return {
path: 'D:/workspace/vue3/vitelearn/src/main.ts',
namespace: undefined
}
}
path: 'D:/workspace/vue3/vitelearn/src/main.ts'匹配到如下规则:
代码27
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//id='D:/workspace/vue3/vitelearn/src/main.ts'
build.onLoad({ filter: JS_TYPES_RE }, ({ path: id }) => {
let ext = path.extname(id).slice(1)
if (ext === 'mjs') ext = 'js'
//读取main.ts文件的内容
let contents = fs.readFileSync(id, 'utf-8')
if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) {
contents = config.esbuild.jsxInject + `\n` + contents
}
if (contents.includes('import.meta.glob')) {
return transformGlob(contents, id, config.root, ext as Loader).then(
(contents) => ({
loader: ext as Loader,
contents
})
)
}
return {
loader: ext as Loader,
contents
}
})
}
}
}
代码27返回了如下内容
return {
loader: 'ts',
contents: "
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
"
}
其中import { createApp } from 'vue'会匹配到
代码28
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
//id='vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
build.onResolve(
{
// avoid matching windows volume
filter: /^[\w@][^:]/
},
async ({ path: id, importer }) => {
if (moduleListContains(exclude, id)) {
return externalUnlessEntry({ path: id })
}
if (depImports[id]) {
return externalUnlessEntry({ path: id })
}
const resolved = await resolve(id, importer)
if (resolved) {
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id })
}
if (resolved.includes('node_modules') || include?.includes(id)) {
// dependency or forced included, externalize and stop crawling
if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
depImports[id] = resolved
}
return externalUnlessEntry({ path: id })
} else {
const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined
// linked package, keep crawling
return {
path: path.resolve(resolved),
namespace
}
}
} else {
missing[id] = normalizePath(importer)
}
}
)
...省略代码
}
调用resolve方法
代码29
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
const seen = new Map<string, string | undefined>()
//id='vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
const resolve = async (id: string, importer?: string) => {
const key = id + (importer && path.dirname(importer))
if (seen.has(key)) {
return seen.get(key)
}
const resolved = await container.resolveId(
id,
importer && normalizePath(importer)
)
const res = resolved?.id
seen.set(key, res)
return res
}
...省略代码
}
调用container.resolveId方法
代码30
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts的TransformContext类
//rawId='vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
async resolveId(rawId, importer = join(root, 'index.html'), options) {
...省略代码
const ctx = new Context()
...省略代码
for (const plugin of plugins) {
...省略代码
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{ ssr }
)
if (!result) continue
if (typeof result === 'string') {
id = result
} else {
id = result.id
Object.assign(partial, result)
}
...省略代码
// resolveId() is hookFirst - first non-null result is returned.
break
}
...省略代码
if (id) {
partial.id = isExternalUrl(id) ? id : normalizePath(id)
return partial as PartialResolvedId
} else {
return null
}
}
循环调用每个插件的resolveId方法,rawId='vue'被D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts的resolvePlugin匹配上,看下resolvePlugin的resolveId方法
代码31
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
...省略代码
return {
name: 'vite:resolve',
resolveId(id, importer, resolveOpts) {
...省略代码
if (bareImportRE.test(id)) {
...省略代码
if (
(res = tryNodeResolve(id, importer, options, targetWeb, server, ssr))
) {
return res
}
...省略代码
}
},
...省略代码
}
}
看下tryNodeResolve方法
代码32
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
//id:'vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
export function tryNodeResolve(
id: string,
importer: string | null | undefined,
options: InternalResolveOptions,
targetWeb: boolean,
server?: ViteDevServer,
ssr?: boolean
): PartialResolvedId | undefined {
...省略代码
let pkg: PackageData | undefined
//possiblePkgId=['vue']
const pkgId = possiblePkgIds.reverse().find((pkgId) => {
pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache)!
return pkg
})!
if (!pkg) {
return
}
...省略代码
return { id: resolved }
}
看下resolvePackageData方法
代码33
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\packages.ts
//id='vue'
//basedir='D:/workspace/vue3/vitelearn/src'
export function resolvePackageData(
id: string,
basedir: string,
preserveSymlinks = false,
packageCache?: PackageCache
): PackageData | null {
...省略代码
let pkgPath: string | undefined
try {
//获取vue的package.json文件的路径
//pkgPath='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\package.json'
pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks)
//读取D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\package.json文件的内容,并返回PackageData对象
pkg = loadPackageData(pkgPath, true, packageCache)
if (packageCache) {
packageCache.set(cacheKey!, pkg)
}
return pkg
} catch (e) {
...省略代码
}
return null
}
回到代码32,代码32继续往下执行
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
//id:'vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
export function tryNodeResolve(
id: string,
importer: string | null | undefined,
options: InternalResolveOptions,
targetWeb: boolean,
server?: ViteDevServer,
ssr?: boolean
): PartialResolvedId | undefined {
...省略代码
let pkg: PackageData | undefined
//possiblePkgId=['vue']
const pkgId = possiblePkgIds.reverse().find((pkgId) => {
pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache)!
return pkg
})!
if (!pkg) {
return
}
let resolveId = resolvePackageEntry
let unresolvedId = pkgId
if (unresolvedId !== nestedPath) {
resolveId = resolveDeepImport
unresolvedId = '.' + nestedPath.slice(pkgId.length)
}
let resolved: string | undefined
try {
resolved = resolveId(unresolvedId, pkg, targetWeb, options)
} catch (err) {
if (!options.tryEsmOnly) {
throw err
}
}
...省略代码
return { id: resolved }
}
查看下resolveId方法,resolvePackageEntry赋值给了resolveId,resolvePackageEntry是D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts里面的方法
代码34
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
//id='vue'
export function resolvePackageEntry(
id: string,
{ dir, data, setResolvedCache, getResolvedCache }: PackageData,
targetWeb: boolean,
options: InternalResolveOptions
): string | undefined {
...省略代码
//entryPoint =D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js
//dir='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue'
entryPoint = path.join(dir, entryPoint)//entryPoint='./dist/vue.runtime.esm-bundler.js'
//resolvedEntryPoint=D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js
const resolvedEntryPoint = tryFsResolve(entryPoint, options)
if (resolvedEntryPoint) {
isDebug &&
debug(
`[package entry] ${colors.cyan(id)} -> ${colors.dim(
resolvedEntryPoint
)}`
)
setResolvedCache('.', resolvedEntryPoint, targetWeb)
//resolveEntryPoint='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'
return resolvedEntryPoint
} else {
packageEntryFailure(id)
}
} catch (e) {
packageEntryFailure(id, e.message)
}
}
回到代码32,代码32继续往下执行
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
//id:'vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
export function tryNodeResolve(
id: string,
importer: string | null | undefined,
options: InternalResolveOptions,
targetWeb: boolean,
server?: ViteDevServer,
ssr?: boolean
): PartialResolvedId | undefined {
...省略代码
let pkg: PackageData | undefined
//possiblePkgId=['vue']
const pkgId = possiblePkgIds.reverse().find((pkgId) => {
pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache)!
return pkg
})!
if (!pkg) {
return
}
let resolveId = resolvePackageEntry
let unresolvedId = pkgId
if (unresolvedId !== nestedPath) {
resolveId = resolveDeepImport
unresolvedId = '.' + nestedPath.slice(pkgId.length)
}
let resolved: string | undefined
try {
//resolved ='D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'
resolved = resolveId(unresolvedId, pkg, targetWeb, options)
} catch (err) {
if (!options.tryEsmOnly) {
throw err
}
}
if (!resolved) {
return
}
// link id to pkg for browser field mapping check
idToPkgMap.set(resolved, pkg)//加入缓存
if (isBuild) {
...省略代码
} else {
//server._isRunningOptimizer此时为真
if (
!resolved.includes('node_modules') || // linked
!server || // build
server._isRunningOptimizer || // optimizing
!server._optimizeDepsMetadata
) {
return { id: resolved }
}
...省略代码
}
...省略代码
return { id: resolved }
}
回到代码28处,代码28继续往下执行
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
build.onResolve(
{
// avoid matching windows volume
filter: /^[\w@][^:]/
},
async ({ path: id, importer }) => {
...省略代码
//resolved='D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'
const resolved = await resolve(id, importer)
if (resolved) {
...省略代码
if (resolved.includes('node_modules') || include?.includes(id)) {
if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
//depImports={vue:'D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'}
depImports[id] = resolved
}
return externalUnlessEntry({ path: id })
}
} else {
...省略代码
}
}
)
...省略代码
}
}
}
因为resolveId='D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'含有node_modules,所以代码28返回如下内容
return {
path:'D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js',
external: true
}
回到代码27,代码27返回内容如下:
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='D:\workspace\vue3\vitelearn\src\main.ts'
build.onLoad({ filter: htmlTypesRE, namespace: 'html' },async ({ path }) => {
...省略代码
return {
loader: 'ts',
contents: "
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
"
}
})
...省略代码
}
}
}
其中import { createApp } from 'vue'这条线已经分析完了,然后执行./App.vue的导入分析
import App from './App.vue',匹配到了
代码35
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='./App.vue' importer='D:\\workspace\\vue3\\vitelearn\\src\\main.ts'
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
return {
path: await resolve(path, importer),
namespace: 'html'
}
})
...省略代码
}
}
}
看下resolve方法
代码36
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
const seen = new Map<string, string | undefined>()
//id='./App.vue' importer='D:\\workspace\\vue3\\vitelearn\\src\\main.ts'
const resolve = async (id: string, importer?: string) => {
const key = id + (importer && path.dirname(importer))
if (seen.has(key)) {
return seen.get(key)
}
const resolved = await container.resolveId(
id,
importer && normalizePath(importer)
)
const res = resolved?.id
seen.set(key, res)
return res
}
...省略代码
}
调用container.resolveId方法
代码37
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts
//rawId='./App.vue'
//importer='D:/workspace/vue3/vitelearn/src/main.ts'
//options=undefined
async resolveId(rawId, importer = join(root, 'index.html'), options) {
...省略代码
const ctx = new Context()
...省略代码
for (const plugin of plugins) {
...省略代码
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{ ssr }
)
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(id)
return partial as PartialResolvedId
} else {
return null
}
}
循环调用每个插件的resolveId方法 ,被D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts的resolvePlugin插件匹配到,看下这个插件的resolveId方法
代码38
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
...省略代码
return {
name: 'vite:resolve',
...省略代码
//id='./App.vue' importer='D:/workspace/vue3/vitelearn/src/main.ts'
resolveId(id, importer, resolveOpts) {
...省略代码
//id='./App.vue'以.开头
if (id.startsWith('.') || (preferRelative && /^\w/.test(id))) {
//basedir='D:/workspace/vue3/vitelearn/src'
const basedir = importer ? path.dirname(importer) : process.cwd()
//fsPath='D:\\workspace\\vue3\\vitelearn\\src\\App.vue'
const fsPath = path.resolve(basedir, id)
// handle browser field mapping for relative imports
//normalizedFsPath='D:/workspace/vue3/vitelearn/src/App.vue'
const normalizedFsPath = normalizePath(fsPath)
//pathFromBasedir='/App.vue'
const pathFromBasedir = normalizedFsPath.slice(basedir.length)
...省略代码
if ((res = tryFsResolve(fsPath, options))) {
isDebug && debug(`[relative] ${chalk.cyan(id)} -> ${chalk.dim(res)}`)
//pkg=undefined
const pkg = importer != null && idToPkgMap.get(importer)
if (pkg) {
idToPkgMap.set(res, pkg)
return {
id: res,
moduleSideEffects: pkg.hasSideEffects(res)
}
}
//res='D:/workspace/vue3/vitelearn/src/App.vue'
return res
}
}
...省略代码
},
...省略代码
}
}
回到代码35,代码35返回
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='./App.vue' importer='D:\\workspace\\vue3\\vitelearn\\src\\main.ts'
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
return {
//path='D:/workspace/vue3/vitelearn/src/App.vue'
path: await resolve(path, importer),
namespace: 'html'
}
})
...省略代码
}
}
}
然后匹配到
代码39
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='D:/workspace/vue3/vitelearn/src/App.vue'
build.onLoad({ filter: htmlTypesRE, namespace:'html' }, async ({ path }) => {
//读取App.vue文件的内容
let raw = fs.readFileSync(path, 'utf-8')
// Avoid matching the content of the comment
raw = raw.replace(commentRE, '<!---->')
const isHtml = path.endsWith('.html')
const regex = isHtml ? scriptModuleRE : scriptRE
regex.lastIndex = 0
let js = ''
let loader: Loader = 'js'
let match: RegExpExecArray | null
while ((match = regex.exec(raw))) {
//openTag='<script setup lang="ts">'
const [, openTag, content] = match
...省略代码
//srcRE表达式匹配的是src,而App.vue的并没有src,所以匹配结果为null
const srcMatch = openTag.match(srcRE)
if (srcMatch) {
...省略代码
} else if(content.trim()){
...省略代码
if (
(path.endsWith('.vue') && setupRE.test(openTag)) ||
(path.endsWith('.svelte') && context !== 'module')
) {
const localContent =
content +
(loader.startsWith('ts') ? extractImportPaths(content) : '')
localScripts[path] = {
loader,
contents: localContent
}
js += `import '${virtualModulePrefix}${path}';\n`
}
}
}
...省略代码
return {
loader,
contents: js
}
})
...省略代码
}
}
}
此时
localScripts={
'D:/workspace/vue3/vitelearn/src/App.vue': {
contents:'
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
import './components/HelloWorld.vue'
',
loader:'ts'
}
}
代码39返回如下:
return {
loader:'ts',
contents: '
import 'virtual-module:D:/workspace/vue3/vitelearn/src/App.vue';
export default {}
'
}
import 'virtual-module:D:/workspace/vue3/vitelearn/src/App.vue';又匹配到了
代码40
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
// local scripts (`<script>` in Svelte and `<script setup>` in Vue)
build.onResolve({ filter: virtualModuleRE }, ({ path }) => {
return {
// strip prefix to get valid filesystem path so esbuild can resolve imports in the file
path: path.replace(virtualModulePrefix, ''),
namespace: 'local-script'
}
})
...省略代码
}
}
}
将路径中的virtual-module:字样去掉,代码40返回
return {
path:'D:/workspace/vue3/vitelearn/src/App.vue',
namespace:'local-script'
}
然后去加载D:/workspace/vue3/vitelearn/src/App.vue的内容
代码41
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='D:/workspace/vue3/vitelearn/src/App.vue'
build.onLoad({ filter: /.*/, namespace: 'local-script' }, ({ path }) => {
return localScripts[path]
})
...省略代码
}
}
}
localScripts在代码39处已经赋值过了,所以代码41返回
return {
contents:'
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
import './components/HelloWorld.vue'
',
loader:'ts'
}
其内容import './components/HelloWorld.vue'匹配到了
代码42
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='./components/HelloWorld.vue'
//importer='D:/workspace/vue3/vitelearn/src/App.vue'
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
return {
path: await resolve(path, importer),
namespace: 'html'
}
})
...省略代码
}
}
}
看下resolve方法
代码43
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
const seen = new Map<string, string | undefined>()
//id='./components/HelloWorld.vue'
//importer='D:/workspace/vue3/vitelearn/src/App.vue'
const resolve = async (id: string, importer?: string) => {
const key = id + (importer && path.dirname(importer))
if (seen.has(key)) {
return seen.get(key)
}
const resolved = await container.resolveId(
id,
importer && normalizePath(importer)
)
const res = resolved?.id
seen.set(key, res)
return res
}
...省略代码
}
调用container.resolveId方法
代码44
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts
//rawId='./components/HelloWorld.vue'
//importer='D:/workspace/vue3/vitelearn/src/App.vue'
//options=undefined
async resolveId(rawId, importer = join(root, 'index.html'), options) {
...省略代码
const ctx = new Context()
...省略代码
for (const plugin of plugins) {
...省略代码
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{ ssr }
)
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(id)
return partial as PartialResolvedId
} else {
return null
}
}
循环调用每个插件的resolveId方法,被D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts的resolvePlugin插件匹配,看下resolvePlugin的resolveId方法
代码45
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\plugins\resolve.ts
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
...省略代码
return {
name: 'vite:resolve',
...省略代码
//id='./components/HelloWorld.vue'
resolveId(id, importer, resolveOpts) {
if (id.startsWith('.') || (preferRelative && /^\w/.test(id))) {
//basedir='D:/workspace/vue3/vitelearn/src'
const basedir = importer ? path.dirname(importer) : process.cwd()
//fsPath='D:\\workspace\\vue3\\vitelearn\\src\\components\\HelloWorld.vue'
const fsPath = path.resolve(basedir, id)
// handle browser field mapping for relative imports
//normalizedFsPath='D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue'
const normalizedFsPath = normalizePath(fsPath)
//pathFromBasedir='/components/HelloWorld.vue'
const pathFromBasedir = normalizedFsPath.slice(basedir.length)
...省略代码
if ((res = tryFsResolve(fsPath, options))) {
isDebug && debug(`[relative] ${chalk.cyan(id)} -> ${chalk.dim(res)}`)
//pkg=undefined
const pkg = importer != null && idToPkgMap.get(importer)
if (pkg) {
idToPkgMap.set(res, pkg)
return {
id: res,
moduleSideEffects: pkg.hasSideEffects(res)
}
}
//res='D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue'
return res
}
}
...省略代码
},
...省略代码
}
}
所以代码42返回如下:
代码42
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='./components/HelloWorld.vue'
//importer='D:/workspace/vue3/vitelearn/src/App.vue'
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
return {
//path='D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue'
path: await resolve(path, importer),
namespace: 'html'
}
})
...省略代码
}
}
}
然后去加载D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue文件的内容
代码46
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path="D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue"
build.onLoad({ filter: htmlTypesRE, namespace: 'html' },async ({ path }) ={
//读取HelloWorld.vue文件的内容
let raw = fs.readFileSync(path, 'utf-8')
// Avoid matching the content of the comment
raw = raw.replace(commentRE, '<!---->')
const isHtml = path.endsWith('.html')
const regex = isHtml ? scriptModuleRE : scriptRE
regex.lastIndex = 0
let js = ''
let loader: Loader = 'js'
let match: RegExpExecArray | null
while ((match = regex.exec(raw))) {
//openTag='<script setup lang="ts">'
const [, openTag, content] = match
...省略代码
const srcMatch = openTag.match(srcRE)
if (srcMatch) {
...省略代码
} else if (content.trim()) {
...省略代码
if (
(path.endsWith('.vue') && setupRE.test(openTag)) ||
(path.endsWith('.svelte') && context !== 'module')
) {
const localContent =
content +
(loader.startsWith('ts') ? extractImportPaths(content) : '')
localScripts[path] = {
loader,
contents: localContent
}
js += `import '${virtualModulePrefix}${path}';\n`
} else {
...省略代码
}
}
}
...省略代码
})
...省略代码
return {
loader,
contents: js
}
}
}
}
此时
localScripts={
'D:/workspace/vue3/vitelearn/src/App.vue':{
contents:'
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
import './components/HelloWorld.vue'
',
loader:'ts'
},
'D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue':{
contents:'
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
import 'vue'
',
loader:'ts'
}
}
代码46返回
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path="D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue"
build.onLoad({ filter: htmlTypesRE, namespace: 'html' },async ({ path }) ={
...省略代码
return {
//loader='ts'
loader,
/**
* contents:'
import 'virtual-module:D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue';
export default {}
',
*/
contents: js
}
})
...省略代码
}
}
}
然后匹配到
代码47
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path='virtual-module:D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue'
build.onResolve({ filter: virtualModuleRE }, ({ path }) => {
return {
// strip prefix to get valid filesystem path so esbuild can resolve imports in the file
path: path.replace(virtualModulePrefix, ''),
namespace: 'local-script'
}
})
...省略代码
}
}
}
代码47返回
return {
namespace:'local-script',
path:'D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue'
}
然后匹配到
代码48
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
//path="D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue"
build.onLoad({ filter: /.*/, namespace: 'local-script' }, ({ path }) => {
return localScripts[path]
})
...省略代码
}
}
根据代码46处localScripts对象中的数据,代码48返回的内容如下:
return {
contents:'
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
import 'vue'
',
loader:'ts'
}
import { ref } from 'vue'匹配到
代码49
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string, string>,
missing: Record<string, string>,
entries: string[]
): Plugin {
...省略代码
return {
name: 'vite:dep-scan',
setup(build) {
...省略代码
build.onResolve(
{
// avoid matching windows volume
filter: /^[\w@][^:]/
},
//id='vue' importer='D:/workspace/vue3/vitelearn/src/components/HelloWorld.vue'
async ({ path: id, importer }) => {
if (moduleListContains(exclude, id)) {
return externalUnlessEntry({ path: id })
}
//在编译main.ts时vue已经编译过了,所以直接返回
if (depImports[id]) {
return externalUnlessEntry({ path: id })
}
...省略代码
}
)
...省略代码
}
}
}
代码49返回
return {
external:true,
path:'vue'
}
至此index.html作为entrypoint的依赖都已经收集完毕, 也就是D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts的scanImports方法执行完毕,让我们回到代码16,代码16返回
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\scan.ts
export async function scanImports(config: ResolvedConfig): Promise<{
deps: Record<string, string>
missing: Record<string, string>
}> {
...省略代码
//entries=['D:\\workspace\\vue3\\vitelearn\\index.html']
await Promise.all(
entries.map((entry) =>
build({
absWorkingDir: process.cwd(),
write: false,
entryPoints: [entry],
bundle: true,
format: 'esm',
logLevel: 'error',
plugins: [...plugins, plugin],
...esbuildOptions
})
)
)
debug(`Scan completed in ${(performance.now() - start).toFixed(2)}ms:`, deps)
return {
//deps={vue:D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js}
deps,
//missing={}
missing
}
}
回到代码15,
代码50
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\index.ts
export async function optimizeDeps(
config: ResolvedConfig,
force = config.server.force,
asCommand = false,
newDeps?: Record<string, string>, // missing imports encountered after server has started
ssr?: boolean
): Promise<DepOptimizationMetadata | null> {
...省略代码
let deps: Record<string, string>, missing: Record<string, string>
if (!newDeps) {
//deps={'vue':'D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js',missing:{}}
;({ deps, missing } = await scanImports(config))
} else {
deps = newDeps
missing = {}
}
...省略代码
//flatIdDeps={vue:'D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'}
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: Object.keys(flatIdDeps),
bundle: true,
format: 'esm',
target: config.build.target || undefined,
external: config.optimizeDeps?.exclude,
logLevel: 'error',
splitting: true,
sourcemap: true,
outdir: cacheDir,
ignoreAnnotations: true,
metafile: true,
define,
plugins: [
...plugins,
esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr)
],
...esbuildOptions
})
...省略代码
return data
}
plugins=[],esbuildDepPlugin会返回一个plugin,esBuild对入口文件['vue']进行打包,首先匹配上了
代码51
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
//qualified={vue:'D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'}
export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig,
ssr?: boolean
): Plugin {
...省略代码
return {
name: 'vite:dep-pre-bundle',
setup(build) {
...省略代码
//id:'vue' importer='' kind='entry-point'
build.onResolve({ filter: /^[\w@][^:]/ }, async ({ path: id, importer, kind }) => {
...省略代码
// ensure esbuild uses our resolved entries
let entry: { path: string; namespace: string } | undefined
// if this is an entry, return entry namespace resolve result
//importer=''
if (!importer) {
if ((entry = resolveEntry(id))) return entry
...省略代码
}
...省略代码
})
...省略代码
}
}
}
看下resolveEntry方法
代码52
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
//id='vue'
function resolveEntry(id: string) {
//flatId='vue'
const flatId = flattenId(id)
//qualified={'vue':'D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'}
if (flatId in qualified) {
return {
path: flatId,
namespace: 'dep'
}
}
}
也就是代码51返回
return {
path:'vue',
namespace: 'dep'
}
然后匹配到
代码53
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig,
ssr?: boolean
): Plugin {
...省略代码
return {
name: 'vite:dep-pre-bundle',
setup(build) {
...省略代码
//id='vue'
build.onLoad({ filter: /.*/, namespace: 'dep' }, ({ path: id }) => {
//entryFile='D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js'
const entryFile = qualified[id]
//relativePath='node_modules/vue/dist/vue.runtime.esm-bundler.js'
let relativePath = normalizePath(path.relative(root, entryFile))
if (
!relativePath.startsWith('./') &&
!relativePath.startsWith('../') &&
relativePath !== '.'
) {
//relativePath='./node_modules/vue/dist/vue.runtime.esm-bundler.js'
relativePath = `./${relativePath}`
}
let contents = ''
//exportsData在代码15处已经赋值过,data=[[],['compile'],false,hasReExports:true],第一项为imports的依赖,第二项为exports
const data = exportsData[id]
const [imports, exports] = data
if (!imports.length && !exports.length) {
...省略代码
} else {
...省略代码
if (
data.hasReExports ||
exports.length > 1 ||
exports[0] !== 'default'
) {
contents += `\nexport * from "${relativePath}"`
}
}
//ext='js'
let ext = path.extname(entryFile).slice(1)
return {
loader: ext as Loader,
//contents='\nexport * from "./node_modules/vue/dist/vue.runtime.esm-bundler.js"'
contents,
//root='D:\\workspace\\vue3\\vitelearn'
resolveDir: root
}
})
...省略代码
}
}
}
D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js依赖@vue/runtime-dom,看下runtime.esm-bundler.js的源码:
代码54
D:/workspace/vue3/vitelearn/node_modules/vue/dist/vue.runtime.esm-bundler.js
import { initCustomFormatter, warn } from '@vue/runtime-dom';
export * from '@vue/runtime-dom';
...省略代码
export { compile };
import { initCustomFormatter, warn } from '@vue/runtime-dom';匹配到了
代码55
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig,
ssr?: boolean
): Plugin {
...省略代码
return {
name: 'vite:dep-pre-bundle',
setup(build) {
...省略代码
//id='@vue/runtime-dom'
//importer='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'
//kind='import-statement'
build.onResolve({filter:/^[\w@][^:]/},async({path: id, importer, kind }) => {
...省略代码
const resolved = await resolve(id, importer, kind)
if (resolved) {
if (resolved.startsWith(browserExternalId)) {
return {
path: id,
namespace: 'browser-external'
}
}
if (isExternalUrl(resolved)) {
return {
path: resolved,
external: true
}
}
return {
path: path.resolve(resolved)
}
}
})
...省略代码
}
}
}
来看resolve方法
代码56
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig,
ssr?: boolean
): Plugin {
// default resolver which prefers ESM
const _resolve = config.createResolver({ asSrc: false })
// cjs resolver that prefers Node
const _resolveRequire = config.createResolver({
asSrc: false,
isRequire: true
})
//id='@vue/runtime-dom'
//importer='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'
//kind='import-statement'
const resolve = (
id: string,
importer: string,
kind: ImportKind,
resolveDir?: string
): Promise<string | undefined> => {
let _importer: string
// explicit resolveDir - this is passed only during yarn pnp resolve for
// entries
if (resolveDir) {
_importer = normalizePath(path.join(resolveDir, '*'))
} else {
// map importer ids to file paths for correct resolution
_importer = importer in qualified ? qualified[importer] : importer
}
//resolver=_resolve
const resolver = kind.startsWith('require') ? _resolveRequire : _resolve
return resolver(id, _importer, undefined, ssr)
}
...省略代码
}
看下createResolver的定义
代码58
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig> {
...省略代码
const createResolver: ResolvedConfig['createResolver'] = (options) => {
let aliasContainer: PluginContainer | undefined
let resolverContainer: PluginContainer | undefined
return async (id, importer, aliasOnly, ssr) => {
...省略代码
}
}
}
createResolver其实返回了一个函数
return async (id, importer, aliasOnly, ssr) => {...省略代码}
回到代码56处就是调用了代码58处返回的方法,看下createResolver的具体实现
代码59
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\config.ts
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig> {
...省略代码
const createResolver: ResolvedConfig['createResolver'] = (options) => {
let aliasContainer: PluginContainer | undefined
let resolverContainer: PluginContainer | undefined
//id='@vue/runtime-dom'
//importer='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'
//aliasOnly=undefined
return async (id, importer, aliasOnly, ssr) => {
let container: PluginContainer
if (aliasOnly) {
...省略代码
} else {
container =
resolverContainer ||
(resolverContainer = await createPluginContainer({
...resolved,
plugins: [
//entries:[{ find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
//{ find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY }]
//是在resolveConfig的resolvedAlias进行的赋值
aliasPlugin({ entries: resolved.resolve.alias }),
resolvePlugin({
...resolved.resolve,
root: resolvedRoot,
isProduction,
isBuild: command === 'build',
ssrConfig: resolved.ssr,
asSrc: true,
preferRelative: false,
tryIndex: true,
...options
})
]
}))
}
return (await container.resolveId(id, importer, { ssr }))?.id
}
}
}
此时container就只有两个plugin,一个aliasPlugin一个resolvePlugin,然后调用了container.resolveId方法
代码60
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\server\pluginContainer.ts
class TransformContext extends Context {
...省略代码
//id='@vue/runtime-dom'
//importer='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'
//options={ssr:undefined}
async resolveId(rawId, importer = join(root, 'index.html'), options) {
const skip = options === null || options === void 0 ? void 0 : options.skip;
...省略代码
const ctx = new Context();
...省略代码
for (const plugin of plugins) {
...省略代码
const result = await plugin.resolveId.call(ctx, rawId, importer, { ssr });
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$4(id);
return partial;
}
else {
return null;
}
}
...省略代码
}
循环调用aliasPlugin和resolvePlugin的resovleId方法,被resolvePlugin匹配上,剩余解析过程参考vue的解析过程,最终代码60返回如下
return {
id: 'D:\workspace\vue3\vitelearn\node_modules\@vue\runtime-dom\dist\runtime-dom.esm-bundler.js'
}
回到代码55,代码55返回了
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig,
ssr?: boolean
): Plugin {
...省略代码
return {
name: 'vite:dep-pre-bundle',
setup(build) {
...省略代码
//id='@vue/runtime-dom'
//importer='D:\\workspace\\vue3\\vitelearn\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js'
//kind='import-statement'
build.onResolve({filter:/^[\w@][^:]/},async({path: id, importer, kind }) => {
...省略代码
const resolved = await resolve(id, importer, kind)
if (resolved) {
if (resolved.startsWith(browserExternalId)) {
return {
path: id,
namespace: 'browser-external'
}
}
if (isExternalUrl(resolved)) {
return {
path: resolved,
external: true
}
}
return {
//path='D:/workspace/vue3/vitelearn/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js'
path: path.resolve(resolved)
}
}
})
...省略代码
}
}
}
esbuildDepPlugin插件继续执行,然后匹配到了
代码61
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\esbuildDepPlugin.ts
export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig,
ssr?: boolean
): Plugin {
...省略代码
return {
name: 'vite:dep-pre-bundle',
setup(build) {
...省略代码
//id='@vue/runtime-core'
//importer='D:\\workspace\\vue3\\vitelearn\\node_modules\\@vue\\runtime-dom\\dist\\runtime-dom.esm-bundler.js'
//kind='import-statement'
build.onResolve({filter:/^[\w@][^:]/},async({path:id,importer, kind }) => {
const resolved = await resolve(id, importer, kind)
if (resolved) {
if (resolved.startsWith(browserExternalId)) {
return {
path: id,
namespace: 'browser-external'
}
}
if (isExternalUrl(resolved)) {
return {
path: resolved,
external: true
}
}
return {
path: path.resolve(resolved)
}
}
})
...省略代码
}
}
}
打开D:\\workspace\\vue3\\vitelearn\\node_modules\\@vue\\runtime-dom\\dist\\runtime-dom.esm-bundler.js可以看到runtime-dom依赖了@vue/compiler-core和@vue/shared
@vue/runtime-core的解析过程与vue的解析过程一样,不在重复,代码61返回
return {
path:'D:/workspace/vue3/vitelearn/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js'
}
剩余依赖也同理,至此将D:\workspace\vue3\vitelearn\node_modules\vue\dist\vue.runtime.esm-bundler.js作为入口文件,与之相关联的依赖都已经收集完毕,回到代码50处
D:\workspace\vue3\vite-2.7.2\packages\vite\src\node\optimizer\index.ts
export async function optimizeDeps(
config: ResolvedConfig,
force = config.server.force,
asCommand = false,
newDeps?: Record<string, string>, // missing imports encountered after server has started
ssr?: boolean
): Promise<DepOptimizationMetadata | null> {
...省略代码
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: Object.keys(flatIdDeps),
bundle: true,
format: 'esm',
target: config.build.target || undefined,
external: config.optimizeDeps?.exclude,
logLevel: 'error',
splitting: true,
sourcemap: true,
outdir: cacheDir,
ignoreAnnotations: true,
metafile: true,
define,
plugins: [
...plugins,
esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr)
],
...esbuildOptions
})
const meta = result.metafile!
const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)
for (const id in deps) {
const entry = deps[id]
data.optimized[id] = {
file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
src: entry,
needsInterop: needsInterop(
id,
idToExports[id],
meta.outputs,
cacheDirOutputPath
)
}
//写入缓存文件中
writeFile(dataPath, JSON.stringify(data, null, 2))
return data
}
result结构如图
然后将编译结果缓存到D:\workspace\vue3\vitelearn\node_modules\.vite目录下,至此那些在node_modules中的依赖都已经预编译好了。也即server创建好了,之后就是开启server.listen监听客户端的连接。
总结
npm run dev 其实只是创建了一个本地服务器,然后注册一些中间件,做依赖预构建的工作,整个过程都是一些不怎么耗时的步骤,这也是vite启动快的原因