Skip to content
On this page

我们知道, package.json 文件的 bin 字段是用来指定要运行的命令及运行命令所要执行的文件

vite 命令执行的文件

vite 的配置如下:

json
{
  "bin": {
    "vite": "bin/vite.js"
  }
}

好了,我们知道启动 dev-server 的 vite 命令,实际运行的就是 bin/vite.js 文件,那我们进去看看:

1. 入口:vite/bin/vite.js

判断当前目录是否包含 node_modules 目录,若不包含,则需要 source-map 支持

js
#!/usr/bin/env node // 固定格式,用来指定命令运行的环境(node)

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

2. 检查 process.argv 是否是 debug 模式

js
// 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))

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 = value

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

3. 导入编译后的 cli 脚本

js
const profileIndex = process.argv.indexOf('--profile')

function start() {
  require('../dist/node/cli')
}

// 如果配置了 profile 参数
if (profileIndex > 0) {
  // 截掉 '--profile'
  process.argv.splice(profileIndex, 1)
  const next = process.argv[profileIndex]
  if (next && !next.startsWith('-')) {
    process.argv.splice(profileIndex, 1)
  }
  // 创建会话连接(webkit inspector)
  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()
}

4. 进入 cli.js,源码:vite/src/node/cli.ts

ts
import { cac } from 'cac' /* 一个简单且强大的命令行库,比 commander 更轻量 */
import chalk from 'chalk' /* 美化控制台日志记录 */
import { performance } from 'perf_hooks' /* node.js 收集性能指标的对象 */
import { BuildOptions } from './build' /* 打包 📦 配置项 */
import { ServerOptions } from './server' /* 启动服务的配置项 */
import { createLogger, LogLevel } from './logger' /* 打印日志 */
import { resolveConfig } from '.' /* 处理配置项的方法 */
import { preview } from './preview' /* 打包结果预览方法 */

// 使用 cac 生成 cli 实例
const cli = cac('vite')

5. 全局 cli 选项及清理选项

ts
// 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
 * 删除全局 cli 配置
 */
function cleanOptions<Options extends GlobalCLIOptions>(
  options: Options
): Omit<Options, keyof GlobalCLIOptions> {
  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
}

6. cli 参数定义

这里我们就只研究一下开发环境下的功能实现,生产环境/预览环境后续再考虑吧~

ts
// 下面是一些核心参数,如 -c 指定配置文件等
cli
  .option('-c, --config <file>', `[string] use specified config file`)
  .option('--base <path>', `[string] public base path (default: /)`)
  .option('-l, --logLevel <level>', `[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 <filter>', `[string] filter debug logs`)
  .option('-m, --mode <mode>', `[string] set env mode`)

// dev 开发环境,主要针对 server 的参数配置
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(devAction)
/* 这里将 action 函数抽出来单独看,免得篇幅过长,拎不清注意点 */

cli.help() // 命令行使用 -h / --help 参数,输出帮助信息
cli.version(require('../../package.json').version) // vite 版本号

cli.parse() // 解析命令行参数

devAction 函数

ts
const devAction = async (
  root: string,
  options: ServerOptions & GlobalCLIOptions
) => {
  // output structure is preserved even after bundling so require()
  // is ok here
  const { createServer } = await import('./server') // 导入创建 dev-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)
    })
    // native Node http server instance or null
    if (!server.httpServer) {
      throw new Error('HTTP server not available')
    }
    await server.listen() // 启动 dev-server
    const info = server.config.logger.info // info 日志

    info(
      chalk.cyan(`\n  vite v${require('vite/package.json').version}`) +
        chalk.green(` dev server running at:\n`),
      {
        clear: !server.config.logger.hasWarned
      }
    )
    // 调用 logger.ts 的 printCommonServerUrls 方法打印 dev-server 运行的 url
    server.printUrls()

    // __vite_start_time 在执行 vite 命令时
    // 赋值为 performance.now(),记录启动服务的开始时间
    if (global.__vite_start_time) {
      // 服务启动时间(ms)
      const startupDuration = performance.now() - global.__vite_start_time
      info(`\n  ${chalk.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`)
    }
  } catch (e) {
    // 打印服务启动失败的日志
    createLogger(options.logLevel).error(
      chalk.red(`error when starting dev server:\n${e.stack}`),
      { error: e }
    )
    // 结束进程
    process.exit(1)
  }
}

7. createServer

ts
// vite/src/node/server/index.ts
export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
  // 这个方法里,大概做了下面这些事:
  // 1. 处理服务的启动配置
  const config = await resolveConfig(inlineConfig, 'serve', 'development')
  const httpsOptions = await resolveHttpsConfig(config.server.https)

  // 2. 检查 config.server.middlewareMode 是否 === true,若是将其置为 'ssr'
  // connect 包创建连接(方便使用中间件来扩展的 node http server 框架)
  const middlewares = connect() as Connect.Server
  const httpServer = middlewareMode
    ? null
    : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
  // resolveHttpServer 方法用来区分使用 http / https / http2 (npm package)来
  // 创建 server 并返回赋值给 httpServer

  // 3. 创建 webSocket 实例
  const ws = createWebSocketServer(httpServer, config, httpsOptions)

  // 4. 使用 chokidar 监听 文件 修改
  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

  // 5. 创建 pluginContainer,创建模块映射表
  const container = await createPluginContainer(config, moduleGraph, watcher)

  const moduleGraph: ModuleGraph = new ModuleGraph(url =>
    container.resolveId(url)
  )
  const closeHttpServer = createServerCloseFn(httpServer)

  // 6. 创建 server 对象
  const server: ViteServer = {
    config,
    middlewares,
    get app() {
      // 过期的 api
      return middlewares
    },
    httpServer,
    watcher,
    pluginContainer: container,
    ws,
    moduleGraph,
    transformWithEsbuild,
    transformRequest(url, options) {
      return transformRequest(url, server, options)
    },
    transformIndexHtml: null!, // to be immediately set
    ssrLoadModule(url) {
      // ... 服务端渲染加载 module
    },
    listen(port?: number, isRestart?: boolean) {
      // 启动 dev-server 方法
      return startServer(server, port, isRestart)
    },
    async close() {
      // 退出 dev-server
      // watcher,ws,container,httpServer
    },
    printUrls() {
      // 打印日志
    },
    async restart(forceOptimize: boolean) {
      // 调用 restartServer 方法,重启 dev-server
    }
    // _xxxx  一些私有属性
  }

  // 立即创建转换 html 的方法
  server.transformIndexHtml = createDevHtmlTransformFn(server)

  // 7. package 缓存
  const { packageCache } = config
  const setPackageData = packageCache.set.bind(packageCache)
  packageCache.set = (id, pkg) => {
    if (id.endsWith('.json')) {
      watcher.add(id)
    }
    return setPackageData(id, pkg)
  }

  // 8. watcher 监听文件变化:change, add, unlink
  // 具体可见 【热更新机制】
  watcher.on('change', async file => {
    /* ...other code */
  })

  // 9. 一堆中间件的应用
  // /node/server/middlewares/*.ts

  return server // 返回创建的 server 对象
}

MIT