vite源码学习-启动本地服务

语言: CN / TW / HK
ead>

背景

自从vue3.0面世后,也随即发布了新的构建工具vite,那么接着本次复习的机会就来学习一下

脚手架

我们根据vite官网的说明进行项目脚手架的搭建,搭建后大致如下图结构 ├── README.md ├── index.html ├── node_modules ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ └── main.js └── vite.config.js 我们这里关注到index.html内的代码 ```html

Vite App

`` 通过html的代码,我们得知我们的代码是通过ES module`的方式进行引入的。

我们通过指令npm install安装完依赖后,我们可以通过package.json中的对应的dev启动本地调试服务。 接着我们可以打开本地的http://localhost:3000/查看道我们的应用是否启动成功。

image.png

看到以上基本上就就是成功

学习dev启动服务

回到我们的package.json, 我们看到scripts中的dev对应着vite指令,我们可以在node_modules/.bin/vite找到对应执行,同时我们结束进程,重新通过vscode的挑食能力重新启动服务, 方便我们对于代码进行调试

bin

```js

!/usr/bin/env node

const { performance } = require('perf_hooks')

if (!__dirname.includes('node_modules')) { try { // only available as dev dependency require('source-map-support').install() } catch (e) {} }

global.__vite_start_time = performance.now()

// check debug mode first before requiring the CLI. // 检查各种指令输入 const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg)) const filterIndex = process.argv.findIndex((arg) => /^(?:-f|--filter)$/.test(arg) ) const profileIndex = process.argv.indexOf('--profile')

if (debugIndex > 0) { let value = process.argv[debugIndex + 1] if (!value || value.startsWith('-')) { value = 'vite:*' } else { // support debugging multiple flags with comma-separated list value = value .split(',') .map((v) => vite:${v}) .join(',') } process.env.DEBUG = ${ process.env.DEBUG ? process.env.DEBUG + ',' : '' }${value}

if (filterIndex > 0) { const filter = process.argv[filterIndex + 1] if (filter && !filter.startsWith('-')) { process.env.VITE_DEBUG_FILTER = filter } } }

// 执行node_modules/vite/dist/node/cli.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() }

```

cli.ts

这里我们看到*/dist/*的路径后,就知道我们调试的是打包后的代码,这里就只能对应着源码来看。源码可以自行上github clone下来查看, 我们看到源码中的 packages/vite/src/node/cli.ts ```ts // packages/vite/src/node/cli.ts import { performance } from 'perf_hooks' import { cac } from 'cac' import colors from 'picocolors' import type { BuildOptions } from './build' import type { ServerOptions } from './server' import type { LogLevel } from './logger' import { createLogger } from './logger' import { resolveConfig } from '.'

const cli = cac('vite')

// global options interface GlobalCLIOptions { '--'?: string[] c?: boolean | string config?: string base?: string l?: LogLevel logLevel?: LogLevel clearScreen?: boolean d?: boolean | string debug?: boolean | string f?: string filter?: string m?: string mode?: string }

/* * removing global flags before passing as command specific sub-configs / function cleanOptions( options: Options ): Omit { const ret = { ...options } delete ret['--'] delete ret.c delete ret.config delete ret.base delete ret.l delete ret.logLevel delete ret.clearScreen delete ret.d delete ret.debug delete ret.f delete ret.filter delete ret.m delete ret.mode return ret }

cli .option('-c, --config ', [string] use specified config file) .option('--base ', [string] public base path (default: /)) .option('-l, --logLevel ', [string] info | warn | error | silent) .option('--clearScreen', [boolean] allow/disable clear screen when logging) .option('-d, --debug [feat]', [string | boolean] show debug logs) .option('-f, --filter ', [string] filter debug logs) .option('-m, --mode ', [string] set env mode)

// 我们运行的是vite命令所以我们命中当前逻辑 cli .command('[root]', 'start dev server') // 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 ', [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 // 动态引入server文件 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')
  }
  // 启动服务其中调用startServer方法
  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)
}

})

// 中间省略代码

cli.help() cli.version(require('../../package.json').version)

cli.parse() ```

vite/src/node/server/index.ts

我们看到dev的指令中主要就是通过createServer创建服务,并启动服务。那么我们来看packages/vite/src/node/server/index.ts中的createServer方法中做了什么

createServer

```js // packages/vite/src/node/server/index.ts export async function createServer( inlineConfig: InlineConfig = {} ): Promise { // 格式化数据 const config = await resolveConfig(inlineConfig, 'serve', 'development') // root 项目的根路径 // serverConfig = { // preTransformRequests: true, // fs: { // strict: true, // allow: [ // "", // 跟root一致 // ], // deny: [ // ".env", // ".env.", // ".{crt,pem}", // ], // }, // } const { root, server: serverConfig } = config // config.cacheDir 为 vite的缓存文件的配置项 通常为"node_modules/.vite" // 解析是否包含https相关的配置 const httpsOptions = await resolveHttpsConfig( config.server.https, config.cacheDir ) let { middlewareMode } = serverConfig if (middlewareMode === true) { middlewareMode = 'ssr' } // 创建middlewares其中包含中间件的基础方法以及EventEmitter对应的原型方法 const middlewares = connect() as Connect.Server // 通过node的http模块创建服务 const httpServer = middlewareMode ? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions) // 创建websocket const ws = createWebSocketServer(httpServer, config, httpsOptions) // 获取配置的watch相关配置项 const { ignored = [], ...watchOptions } = serverConfig.watch || {} // 监听跟节点的文件变化 const watcher = chokidar.watch(path.resolve(root), { ignored: [ '/node_modules/', '/.git/', ...(Array.isArray(ignored) ? ignored : [ignored]) ], ignoreInitial: true, ignorePermissionErrors: true, disableGlobbing: true, ...watchOptions }) as FSWatcher // 实例化模块图 const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }) ) // 为插件创建上下文,其中container返回的主要是对应rollup的钩子构建的上下文对象 // 其中包含以下钩子 // buildStart(钩子) // close(钩子) // getModuleInfo(rollup的上下文方法) // load(钩子) // options(钩子) // resolveId(钩子) // transform(钩子) const container = await createPluginContainer(config, moduleGraph, watcher) // 对httpServer上挂在的socket进行采集,当调用closeHttpServer时,循环执行socket的destroy方法并尝试关闭服务 const closeHttpServer = createServerCloseFn(httpServer)

// eslint-disable-next-line prefer-const let exitProcess: () => void

// 声明server const server: ViteDevServer = { // 配置项 config, // 中间件 middlewares, // 服务 httpServer, // 文件监听 watcher, // 插件上下文 pluginContainer: container, // ws ws, // 模块图 moduleGraph, ssrTransform(code: string, inMap: SourceMap | null, url: string) { return ssrTransform(code, inMap, url, { json: { stringify: server.config.json?.stringify } }) }, transformRequest(url, options) { return transformRequest(url, server, options) }, transformIndexHtml: null!, // to be immediately set async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { if (!server._ssrExternals) { let knownImports: string[] = [] const optimizedDeps = server._optimizedDeps if (optimizedDeps) { await optimizedDeps.scanProcessing knownImports = [ ...Object.keys(optimizedDeps.metadata.optimized), ...Object.keys(optimizedDeps.metadata.discovered) ] } server._ssrExternals = resolveSSRExternal(config, knownImports) } return ssrLoadModule( url, server, undefined, undefined, opts?.fixStacktrace ) }, ssrFixStacktrace(e) { if (e.stack) { const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph) rebindErrorStacktrace(e, stacktrace) } }, ssrRewriteStacktrace(stack: string) { return ssrRewriteStacktrace(stack, moduleGraph) }, // 启动服务 listen(port?: number, isRestart?: boolean) { return startServer(server, port, isRestart) }, // 关闭服务 async close() { process.off('SIGTERM', exitProcess)

  if (!middlewareMode && process.env.CI !== 'true') {
    process.stdin.off('end', exitProcess)
  }

  await Promise.all([
    watcher.close(),
    ws.close(),
    container.close(),
    closeHttpServer()
  ])
},
// 打印服务的地址
printUrls() {
  if (httpServer) {
    printCommonServerUrls(httpServer, config.server, config)
  } else {
    throw new Error('cannot print server URLs in middleware mode.')
  }
},
// 重启
async restart(forceOptimize?: boolean) {
  if (!server._restartPromise) {
    server._forceOptimizeOnRestart = !!forceOptimize
    server._restartPromise = restartServer(server).finally(() => {
      server._restartPromise = null
      server._forceOptimizeOnRestart = false
    })
  }
  return server._restartPromise
},

_optimizedDeps: null,
_ssrExternals: null,
_restartPromise: null,
_importGlobMap: new Map(),
_forceOptimizeOnRestart: false,
_pendingRequests: new Map()

} // 生成一个函数用于处理plugins中所有注册的transformIndexHtml钩子 server.transformIndexHtml = createDevHtmlTransformFn(server) // 推出当前进程 exitProcess = async () => { try { await server.close() } finally { process.exit() } }

process.once('SIGTERM', exitProcess)

if (!middlewareMode && process.env.CI !== 'true') { process.stdin.on('end', exitProcess) } const { packageCache } = config const setPackageData = packageCache.set.bind(packageCache) packageCache.set = (id, pkg) => { if (id.endsWith('.json')) { watcher.add(id) } return setPackageData(id, pkg) } // 根部监听器增加事件监听change watcher.on('change', async (file) => { file = normalizePath(file) // 无效化package.json if (file.endsWith('/package.json')) { return invalidatePackageData(packageCache, file) } // invalidate module graph cache on file change // 无效化当前文件 moduleGraph.onFileChange(file) if (serverConfig.hmr !== false) { try { // 尝试进行热更新 await handleHMRUpdate(file, server) } catch (err) { ws.send({ type: 'error', err: prepareError(err) }) } } }) // 根部监听器增加事件监听add watcher.on('add', (file) => { handleFileAddUnlink(normalizePath(file), server) }) // 根部监听器增加事件监听unlink watcher.on('unlink', (file) => { handleFileAddUnlink(normalizePath(file), server) }) // 获取服务启动时的端口 if (!middlewareMode && httpServer) { httpServer.once('listening', () => { // update actual port since this may be different from initial value serverConfig.port = (httpServer.address() as AddressInfo).port }) }

// apply server configuration hooks from plugins // 取出插件中的configureServer周期 const postHooks: ((() => void) | void)[] = [] for (const plugin of config.plugins) { if (plugin.configureServer) { postHooks.push(await plugin.configureServer(server)) } }

// Internal middlewares ------------------------------------------------------

// request timer if (process.env.DEBUG) { middlewares.use(timeMiddleware(root)) }

// cors (enabled by default) const { cors } = serverConfig if (cors !== false) { middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) }

// proxy const { proxy } = serverConfig if (proxy) { middlewares.use(proxyMiddleware(httpServer, proxy, config)) }

// base if (config.base !== '/') { middlewares.use(baseMiddleware(server)) }

// open in editor support middlewares.use('/__open-in-editor', launchEditorMiddleware())

// serve static files under /public // this applies before the transform middleware so that these files are served // as-is without transforms. if (config.publicDir) { middlewares.use(servePublicMiddleware(config.publicDir)) }

// main transform middleware middlewares.use(transformMiddleware(server))

// serve static files middlewares.use(serveRawFsMiddleware(server)) middlewares.use(serveStaticMiddleware(root, server))

// spa fallback if (!middlewareMode || middlewareMode === 'html') { middlewares.use(spaFallbackMiddleware(root)) }

// run post config hooks // This is applied before the html middleware so that user middleware can // serve custom content instead of index.html. // 翻译: // 运行配置后挂钩 // 这在 html 中间件之前应用,以便用户中间件可以 // 提供自定义内容而不是 index.html。 postHooks.forEach((fn) => fn && fn())

if (!middlewareMode || middlewareMode === 'html') { // transform index.html middlewares.use(indexHtmlMiddleware(server)) // handle 404s // Keep the named function. The name is visible in debug logs via DEBUG=connect:dispatcher ... middlewares.use(function vite404Middleware(_, res) { res.statusCode = 404 res.end() }) }

// error handler middlewares.use(errorMiddleware(server, !!middlewareMode))

const initOptimizer = () => { if (!config.optimizeDeps.disabled) { // 创建优化服务,同时进行优化 server._optimizedDeps = createOptimizedDeps(server) } } // 默认配置下middlewareMode为空 if (!middlewareMode && httpServer) {· let isOptimized = false // overwrite listen to init optimizer before server start const listen = httpServer.listen.bind(httpServer) httpServer.listen = (async (port: number, ...args: any[]) => { // 如果没有优化过,在启动前进行优化 if (!isOptimized) { try { // 执行buildStart钩子 await container.buildStart({}) initOptimizer() isOptimized = true } catch (e) { httpServer.emit('error', e) return } } // 然后才进入启动服务的流程 return listen(port, ...args) }) as any } else { await container.buildStart({}) initOptimizer() } // 返回服务 return server }

`` 好的基本的服务创建我们已经了解完了,那么后续我们将启动服务那会涉及到另一个函数startServer`

startServer

```js async function startServer( server: ViteDevServer, inlinePort?: number, isRestart: boolean = false ): Promise { const httpServer = server.httpServer if (!httpServer) { throw new Error('Cannot call server.listen in middleware mode.') } // 取出server的配置,同serverConfig const options = server.config.server // 端口号 const port = inlinePort ?? options.port ?? 3000 const hostname = resolveHostname(options.host) // 协议 const protocol = options.https ? 'https' : 'http' const info = server.config.logger.info const base = server.config.base / * 对httpServer的启动增加了error事件监听,如果是端口号重复++,其他错误就直接抛出 * 同时调用httpServer的listen方法,启动服务 * listen方法内部调用了container.buildStart({}) * 以及预设好优化相关的的依赖 / // 返回启动成功后的对应端口 const serverPort = await httpServerStart(httpServer, { port, strictPort: options.strictPort, host: hostname.host, logger: server.config.logger })

// @ts-ignore const profileSession = global.__vite_profile_session if (profileSession) { profileSession.post('Profiler.stop', (err: any, { profile }: any) => { // Write profile to disk, upload, etc. if (!err) { const outPath = path.resolve('./vite-profile.cpuprofile') fs.writeFileSync(outPath, JSON.stringify(profile)) info( colors.yellow( CPU profile written to ${colors.white(colors.dim(outPath))}\n ) ) } else { throw err } }) } // 如果配置了打开浏览器,那么就自动打开 if (options.open && !isRestart) { const path = typeof options.open === 'string' ? options.open : base openBrowser( path.startsWith('http') ? path : ${protocol}://${hostname.name}:${serverPort}${path}, true, server.config.logger ) }

return server } `` 那么到这里我们的服务就启动成功了。但是启动的过程中有一个关于依赖的重要函数我们忽略了,那就是createOptimizedDeps`我们深入了解一下其中发生了什么

vite/src/node/optimizer/registerMissing.ts

createOptimizedDeps

```ts // 以下省略大量代码 export function createOptimizedDeps(server: ViteDevServer): OptimizedDeps { const { config } = server const { logger } = config

const sessionTimestamp = Date.now().toString() // 读取之前所缓存的元信息 const cachedMetadata = loadCachedDepOptimizationMetadata(config) // 如果之前有元信息,那就使用之前的,如果没有就创建新的元信息 const optimizedDeps: OptimizedDeps = { metadata: cachedMetadata || createOptimizedDepsMetadata(config, sessionTimestamp), registerMissingImport }

let handle: NodeJS.Timeout | undefined let newDepsDiscovered = false

let newDepsToLog: string[] = [] let newDepsToLogHandle: NodeJS.Timeout | undefined const logNewlyDiscoveredDeps = () => { if (newDepsToLog.length) { config.logger.info( colors.green( ✨ new dependencies optimized: ${depsLogString(newDepsToLog)} ), { timestamp: true } ) newDepsToLog = [] } } // 创建新的promise然后将promise的resover跟promise返回回来 let depOptimizationProcessing = newDepOptimizationProcessing() let depOptimizationProcessingQueue: DepOptimizationProcessing[] = [] const resolveEnqueuedProcessingPromises = () => { // Resolve all the processings (including the ones which were delayed) for (const processing of depOptimizationProcessingQueue) { processing.resolve() } depOptimizationProcessingQueue = [] }

let enqueuedRerun: (() => void) | undefined let currentlyProcessing = false

// If there wasn't a cache or it is outdated, perform a fast scan with esbuild // to quickly find project dependencies and do a first optimize run // 翻译: // 如果没有缓存或缓存过时,使用 esbuild 执行快速扫描 // 快速找到项目依赖项并进行第一次优化运行 if (!cachedMetadata) { currentlyProcessing = true // 同上 const scanPhaseProcessing = newDepOptimizationProcessing() optimizedDeps.scanProcessing = scanPhaseProcessing.promise

const warmUp = async () => {
  try {
    debug(colors.green(`scanning for dependencies...`), {
      timestamp: true
    })
    // 取出元信息
    const { metadata } = optimizedDeps
    /*
    *  1. 借助esbuild自定义插件,得到对应的依赖表(比如针对html文件就去找对应的依赖文件)
       2. 如果配置了includes,那就只保留includes中的选项
       3. 生成discovered信息,其中包含源文件的esm路径跟预计生成的优化文件路径以及浏览器hash
    */
    // discovered的数据结构大致情况
    // {
    //   lodash: {
    //     id: "lodash",
    //     file: "/**/**/vue-vite-study/node_modules/.vite/deps/lodash.js",
    //     src: "/**/**/vue-vite-study/node_modules/lodash/lodash.js",
    //     browserHash: "******",
    //   },
    //   vue: {
    //     id: "vue",
    //     file: "/**/**/vue-vite-study/node_modules/.vite/deps/vue.js",
    //     src: "/**/**/vue-vite-study/node_modules/vue/dist/vue.runtime.esm-bundler.js",
    //     browserHash: "******",
    //   },
    // }
    const discovered = await discoverProjectDependencies(
      config,
      sessionTimestamp
    )

    // Respect the scan phase discover order to improve reproducibility
    // 将发现文件加入元信息
    for (const depInfo of Object.values(discovered)) {
      addOptimizedDepInfo(metadata, 'discovered', {
        ...depInfo,
        processing: depOptimizationProcessing.promise
      })
    }

    debug(
      colors.green(
        `dependencies found: ${depsLogString(Object.keys(discovered))}`
      ),
      {
        timestamp: true
      }
    )

    scanPhaseProcessing.resolve()
    optimizedDeps.scanProcessing = undefined
    // 执行优化
    runOptimizer()
  } catch (e) {
    logger.error(e.message)
    if (optimizedDeps.scanProcessing) {
      scanPhaseProcessing.resolve()
      optimizedDeps.scanProcessing = undefined
    }
  }
}

setTimeout(warmUp, 0)

}

async function runOptimizer(isRerun = false) { // Ensure that rerun is called sequentially enqueuedRerun = undefined currentlyProcessing = true

// Ensure that a rerun will not be issued for current discovered deps
if (handle) clearTimeout(handle)

// a succesful completion of the optimizeDeps rerun will end up
// creating new bundled version of all current and discovered deps
// in the cache dir and a new metadata info object assigned
// to optimizeDeps.metadata. A fullReload is only issued if
// the previous bundled dependencies have changed.

// if the rerun fails, optimizeDeps.metadata remains untouched,
// current discovered deps are cleaned, and a fullReload is issued
// 翻译
// 成功完成 optimizeDeps 重新运行将结束
// 创建所有当前和发现的依赖的新捆绑版本
// 在缓存目录中并分配一个新的元数据信息对象
// 优化Deps.metadata。只有在以下情况下才会发出 fullReload

// 1. 之前捆绑的依赖已经改变。
// 2. 如果重新运行失败,optimizeDeps.metadata 保持不变, 清理当前发现的 deps,并发出 fullReload
let { metadata } = optimizedDeps

// All deps, previous known and newly discovered are rebundled,
// respect insertion order to keep the metadata file stable
// 翻译
// 所有的 deps,以前已知的和新发现的都重新打包,
// 尊重插入顺序以保持元数据文件稳定

const newDeps: Record<string, OptimizedDepInfo> = {}

// Clone optimized info objects, fileHash, browserHash may be changed for them
// 拷贝优化过的信息对象,有可能浏览器的hash已经变化
for (const dep of Object.keys(metadata.optimized)) {
  newDeps[dep] = { ...metadata.optimized[dep] }
}
for (const dep of Object.keys(metadata.discovered)) {
  // Clone the discovered info discarding its processing promise
  // 拷贝新发现的信息剔除对应的process promise
  const { processing, ...info } = metadata.discovered[dep]
  newDeps[dep] = info
}

newDepsDiscovered = false

// Add the current depOptimizationProcessing to the queue, these
// promises are going to be resolved once a rerun is committed
// 将当前的 depOptimizationProcessing 添加到队列中,这些一旦提交重新运行,promise 将被解决
depOptimizationProcessingQueue.push(depOptimizationProcessing)

// Create a new promise for the next rerun, discovered missing
// dependencies will be asigned this promise from this point
// 为下一次重新运行创建一个新的 Promise,发现丢失从此时起,依赖项将被分配这个 Promise
depOptimizationProcessing = newDepOptimizationProcessing()

try {
  // 执行优化Deps
  const processingResult = await runOptimizeDeps(config, newDeps)
  // *此时我们的node_modules/.vite/processing中已经生成了对应文件了
  // 取出新的元信息
  const newData = processingResult.metadata

  // After a re-optimization, if the internal bundled chunks change a full page reload
  // is required. If the files are stable, we can avoid the reload that is expensive
  // for large applications. Comparing their fileHash we can find out if it is safe to
  // keep the current browser state.
  // 翻译
  // 重新优化后,如果内部捆绑的块改变了整页重新加载
  // 是必须的。如果文件稳定,我们可以避免昂贵的重新加载
  // 对于大型应用程序。比较他们的 fileHash,我们可以确定它是否安全
  // 保持当前浏览器状态。
  const needsReload =
    metadata.hash !== newData.hash ||
    Object.keys(metadata.optimized).some((dep) => {
      return (
        metadata.optimized[dep].fileHash !== newData.optimized[dep].fileHash
      )
    })

  const commitProcessing = async () => {
    // 写入元数据文件,删除`deps`文件夹并将新的`processing`文件夹重命名为`deps`同步
    await processingResult.commit()

    // While optimizeDeps is running, new missing deps may be discovered,
    // in which case they will keep being added to metadata.discovered
    // 当 optimizeDeps 运行时,可能会发现新的缺失的 deps,\
    // 在这种情况下,它们将继续添加到 metadata.discovered
    for (const id in metadata.discovered) {
      if (!newData.optimized[id]) {
        addOptimizedDepInfo(newData, 'discovered', metadata.discovered[id])
      }
    }

    // If we don't reload the page, we need to keep browserHash stable
    // 如果不加载页面我们需要保证browserHash的稳定
    if (!needsReload) {
      newData.browserHash = metadata.browserHash
      for (const dep in newData.chunks) {
        newData.chunks[dep].browserHash = metadata.browserHash
      }
      for (const dep in newData.optimized) {
        newData.optimized[dep].browserHash = (
          metadata.optimized[dep] || metadata.discovered[dep]
        ).browserHash
      }
    }

    // Commit hash and needsInterop changes to the discovered deps info
    // object. Allow for code to await for the discovered processing promise
    // and use the information in the same object

    for (const o in newData.optimized) {
      const discovered = metadata.discovered[o]
      if (discovered) {
        const optimized = newData.optimized[o]
        discovered.browserHash = optimized.browserHash
        discovered.fileHash = optimized.fileHash
        discovered.needsInterop = optimized.needsInterop
        discovered.processing = undefined
      }
    }

    if (isRerun) {
      newDepsToLog.push(
        ...Object.keys(newData.optimized).filter(
          (dep) => !metadata.optimized[dep]
        )
      )
    }

    metadata = optimizedDeps.metadata = newData
    // 解决当前队列中的promise
    resolveEnqueuedProcessingPromises()
  }

  if (!needsReload) {
    await commitProcessing()

    if (!isDebugEnabled) {
      if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle)
      newDepsToLogHandle = setTimeout(() => {
        newDepsToLogHandle = undefined
        logNewlyDiscoveredDeps()
      }, 2 * debounceMs)
    } else {
      debug(colors.green(`✨ optimized dependencies unchanged`), {
        timestamp: true
      })
    }
  } else {
    if (newDepsDiscovered) {
      // There are newly discovered deps, and another rerun is about to be
      // excecuted. Avoid the current full reload discarding this rerun result
      // We don't resolve the processing promise, as they will be resolved
      // once a rerun is committed
      processingResult.cancel()

      debug(
        colors.green(
          `✨ delaying reload as new dependencies have been found...`
        ),
        {
          timestamp: true
        }
      )
    } else {
      await commitProcessing()

      if (!isDebugEnabled) {
        if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle)
        newDepsToLogHandle = undefined
        logNewlyDiscoveredDeps()
      }

      logger.info(
        colors.green(`✨ optimized dependencies changed. reloading`),
        {
          timestamp: true
        }
      )
      fullReload()
    }
  }
} catch (e) {
  logger.error(
    colors.red(`error while updating dependencies:\n${e.stack}`),
    { timestamp: true, error: e }
  )
  resolveEnqueuedProcessingPromises()

  // Reset missing deps, let the server rediscover the dependencies
  metadata.discovered = {}
}

currentlyProcessing = false
// @ts-ignore
enqueuedRerun?.()

} // 通过ws重新刷新 function fullReload() { // Cached transform results have stale imports (resolved to // old locations) so they need to be invalidated before the page is // reloaded. server.moduleGraph.invalidateAll()

server.ws.send({
  type: 'full-reload',
  path: '*'
})

}

return optimizedDeps }

```

我们发现获取完对应依赖后,是在runOptimizeDeps函数完成的代码优化,我们跟进runOptimizeDeps 进行学习吧

vite/src/node/optimizer/index.ts

runOptimizeDeps

```ts export async function runOptimizeDeps( config: ResolvedConfig, depsInfo: Record ): Promise { config = { ...config, command: 'build' } // 初始化缓存目录 const depsCacheDir = getDepsCacheDir(config) // 初始化过程目录 const processingCacheDir = getProcessingDepsCacheDir(config)

// Create a temporal directory so we don't need to delete optimized deps // until they have been processed. This also avoids leaving the deps cache // directory in a corrupted state if there is an error // 创建一个临时目录,这样我们就不需要删除优化的 deps // 直到它们被处理。这也避免了离开 deps 缓存 // 如果有错误,目录处于损坏状态 if (fs.existsSync(processingCacheDir)) { emptyDir(processingCacheDir) } else { fs.mkdirSync(processingCacheDir, { recursive: true }) }

// a hint for Node.js // all files in the cache directory should be recognized as ES modules // 写入一个package.json writeFile( path.resolve(processingCacheDir, 'package.json'), JSON.stringify({ type: 'module' }) )

const metadata = createOptimizedDepsMetadata(config)

metadata.browserHash = getOptimizedBrowserHash( metadata.hash, depsFromOptimizedDepInfo(depsInfo) )

// We prebundle dependencies with esbuild and cache them, but there is no need // to wait here. Code that needs to access the cached deps needs to await // the optimizedDepInfo.processing promise for each dep

const qualifiedIds = Object.keys(depsInfo)

if (!qualifiedIds.length) { return { metadata, commit() { // Write metadata file, delete deps folder and rename the processing folder to deps return commitProcessingDepsCacheSync() }, cancel } }

// esbuild generates nested directory output with lowest common ancestor base // this is unpredictable and makes it difficult to analyze entry / output // mapping. So what we do here is: // 1. flatten all ids to eliminate slash // 2. in the plugin, read the entry ourselves as virtual files to retain the // path. // 翻译: // esbuild 生成具有最低公共祖先基数的嵌套目录输出 // 这是不可预测的,并且难以分析输入/输出 // 映射。所以我们在这里做的是: // 1. 展平所有 id 以消除斜线 // 2.在插件中,自己读取入口作为虚拟文件保留路径 const flatIdDeps: Record = {} const idToExports: Record = {} const flatIdToExports: Record = {}

const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {} // 初始化 es-module-lexer await init for (const id in depsInfo) { const flatId = flattenId(id) const filePath = (flatIdDeps[flatId] = depsInfo[id].src!) let exportsData: ExportsData if (config.optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { // For custom supported extensions, build the entry file to transform it into JS, // and then parse with es-module-lexer. Note that the bundle option is not true, // so only the entry file is being transformed. // 对于自定义支持的扩展,构建入口文件转换成JS, // 然后用 es-module-lexer 解析。请注意,bundle 选项不是 true, // 所以只有入口文件被转换。 const result = await build({ ...esbuildOptions, plugins, entryPoints: [filePath], write: false, format: 'esm' }) exportsData = parse(result.outputFiles[0].text) as ExportsData } else { // 默认命中逻辑,取出文件内容 const entryContent = fs.readFileSync(filePath, 'utf-8') try { // 取出导出的数据段落 exportsData = parse(entryContent) as ExportsData } catch { const loader = esbuildOptions.loader?.[path.extname(filePath)] || 'jsx' debug( Unable to parse dependency: ${id}. Trying again with a ${loader} transform. ) const transformed = await transformWithEsbuild(entryContent, filePath, { loader }) // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. // This is useful for packages such as Gatsby. esbuildOptions.loader = { '.js': 'jsx', ...esbuildOptions.loader } exportsData = parse(transformed.code) as ExportsData } for (const { ss, se } of exportsData[0]) { const exp = entryContent.slice(ss, se) if (/export\s+*\s+from/.test(exp)) { exportsData.hasReExports = true } } } idToExports[id] = exportsData flatIdToExports[flatId] = exportsData }

const define: Record = { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || config.mode) } // 取出定义的文件中的全局常量 for (const key in config.define) { const value = config.define[key] define[key] = typeof value === 'string' ? value : JSON.stringify(value) }

const start = performance.now() // 通过esbuild对采集的flatIdDeps也就是dep信息进行打包 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: processingCacheDir, ignoreAnnotations: true, metafile: true, define, plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config) ], ...esbuildOptions }) // 输入输出的元信息 const meta = result.metafile!

// the paths in meta.outputs are relative to process.cwd() // 获取相对位置 const processingCacheDirOutputPath = path.relative( process.cwd(), processingCacheDir )

for (const id in depsInfo) { // 得到对应模块的输出 const output = esbuildOutputFromId(meta.outputs, id, processingCacheDir) // 把模块信息增加到元信息中 addOptimizedDepInfo(metadata, 'optimized', { ...depsInfo[id], needsInterop: needsInterop(id, idToExports[id], output), // We only need to hash the output.imports in to check for stability, but adding the hash // and file path gives us a unique hash that may be useful for other things in the future fileHash: getHash( metadata.hash + depsInfo[id].file + JSON.stringify(output.imports) ), browserHash: metadata.browserHash }) } // 寻找输入中不是依赖模块的文件,那它就一定是chalk包 for (const o of Object.keys(meta.outputs)) { if (!o.match(jsMapExtensionRE)) { const id = path .relative(processingCacheDirOutputPath, o) .replace(jsExtensionRE, '') const file = getOptimizedDepPath(id, config) if ( !findOptimizedDepInfoInRecord( metadata.optimized, (depInfo) => depInfo.file === file ) ) { addOptimizedDepInfo(metadata, 'chunks', { id, file, needsInterop: false, browserHash: metadata.browserHash }) } } } // 元信息的地址 const dataPath = path.join(processingCacheDir, '_metadata.json') // 写入原地址 writeFile(dataPath, stringifyOptimizedDepsMetadata(metadata, depsCacheDir))

debug(deps bundled in ${(performance.now() - start).toFixed(2)}ms)

return { metadata, commit() { // Write metadata file, delete deps folder and rename the new processing folder to deps in sync return commitProcessingDepsCacheSync() }, cancel } // 挪动文件夹 async function commitProcessingDepsCacheSync() { // Processing is done, we can now replace the depsCacheDir with processingCacheDir // Rewire the file paths from the temporal processing dir to the final deps cache dir removeDirSync(depsCacheDir) await renameDir(processingCacheDir, depsCacheDir) } // 取消本次的优化 function cancel() { removeDirSync(processingCacheDir) } } ```

总结

那么这里我们就完成了对于整个dev的初始化过程的学习,实际上整个初始化过程总结下来就包含如下流程:

  1. 初始化配置项,包括

    1.1 服务

    1.2 ws

    1.3 插件

    1.4 监控

    等一些其他的相关配置 2. 启动服务

    2.1 启动服务

    2.2 根据入口文件查询依赖

    2.3 构建元信息,通过esbuild的能力优化依赖

那么关于dev的过程命令的过程我们就到这里就结束学习,下面我们学习一下当输入网址访问对应的服务时中间件做了什么事情

下一章:vite源码学习-访问本地服务