likes
comments
collection
share

源码嘚吧嘚 - create-vite 原理

作者站长头像
站长
· 阅读数 15

哈喽,大家好,我是 SuperYing。我们今天要聊的主题是 Vite 源码之 create-vite

看完本文您可以收获: 1.了解 create-vite 命令是如何生效的 2.create-vite 源码目录结构 3.create-vite 应用的 node 工具包及用途 4.create-vite 代码初始化整体流程处理细节 5.学有所获,学以致用,心情愉快.....

1.create-vite 命令

1.1 初始化新工程

相信接触过 vite 的小伙伴对于初始化 vite 项目的命令都不会陌生。 如使用 npm 包管理器创建:

npm create vite@latest

控制台执行以上命令,若没有安装 create-vite,会出现如下提示:

源码嘚吧嘚 - create-vite 原理 输入 y 回车即可安装。 安装成功后会继续执行项目创建任务,询问几个项目相关问题(如项目名称 project name,框架 framework 及开发语言等),以根据设置确认代码初始化方式及模板等。 完成以上问题后就创建成功啦,

源码嘚吧嘚 - create-vite 原理

1.2 npm create

之前用过 vueCli 的小伙伴可能会有发现,vite 的创建命令与 vueCli 略有不同。 vite 使用 npm create,而 vueCli 使用 vue(即使用脚手架对外暴露的命令)。

那么 npm create 到底是什么呢? npm create 其实是 npm init 的别名,vite 创建工程的命令改用 npm init 也是可以正常执行的。 执行 npm create vite@latest 命令相当于执行 npm exec create-vite@latest,而 npm exec 的作用则是执行其后面的命令。 到这里就整个串起来了,npm create vite@latest 其实就是在执行 create-vite,也就是 vite 对外暴露的命令。所以这两种方式是等价的,唯一需要注意的就是,使用 npm create 时要求脚手架对外暴露的命令要以 create- 开头。

2.create-vite 目录及工具包

目录结构

create-vite 源码地址:github.com/vitejs/vite…

源码嘚吧嘚 - create-vite 原理

craete-vite 的所有代码都在 src/index.ts 文件中,除此之外的其他目录,则是各个框架的代码模板,如 reactreact-tsvuevue-ts 等等。除开特殊情况,我们初始化的新工程就是将这些模板写入本地目录。

工具包

接下来进入到 src/index.ts 文件,可以发现顶部引入了一些工具包,其中包括 node 内部模块cross-spawnminimist 等等,我们来逐个认识一下。

  • node:fs: file systemnode 内置模块,可以函数形式与文件系统进行交互。
  • node:path: node 内置模块,用于处理文件和目录路径。
  • node:url: node 内置模块,用于 URL 解析。
  • cross-spawn: node spawnspawnSync 的跨平台解决方案。用于生成子进程。
  • minimist: 用于解析命令行参数选项。
  • prompts: 用于命令行交互提示。
  • kolorist: 用于设置命令行文本颜色。

本文暂不对以上的工具包做详细讲解,感兴趣的小伙伴可以自行查阅,或者评论区 call 我。

3.create-vite 代码初始化流程

整个源码的核心是 init 函数,整个过程主要分为以下几步:

  • 命令行参数处理及常量定义。
  • 设置初始化项目信息,包括工程目录,项目名称等。
  • 询问初始项目设置相关问题,如使用框架、开发语言等。
  • 根据用户设置,生成代码。

代码部分已添加注释,可结合注释分步理解。

命令行参数处理及常量定义

// 获取命令行参数
const argv = minimist<{
  t?: string
  template?: string
}>(process.argv.slice(2), { string: ['_'] })
// 命令行执行时进行所在的本地目录
const cwd = process.cwd()
// 初始化功能可选用的框架,如 vue,react 等
// 内容较多此处不全量粘贴了,感兴趣的小伙伴可前往源码查看
const FRAMEWORKS: Framework[] = [
    // ...
]
// 可选用框架对应的模板代码
const TEMPLATES = FRAMEWORKS.map(
  (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name],
).reduce((a, b) => a.concat(b), [])
// 模板代码中需要重命名的文件
const renameFiles: Record<string, string | undefined> = {
  _gitignore: '.gitignore',
}
// 默认代码生成的本地目标目录
const defaultTargetDir = 'vite-project'

初始化项目信息

// 根据命令行传递的参数,获取初始化项目生成的目标目录
const argTargetDir = formatTargetDir(argv._[0])
// 获取命令行设置的拉取代码模板,配置项为 --template 或 -t
const argTemplate = argv.template || argv.t
// 若用户未设置代码生成目录,默认 defaultTargetDir 即 vite-project
let targetDir = argTargetDir || defaultTargetDir
// 若用户设置的代码生成目录为 ‘.’,则默认当前目录,否则为上面的 targetDir
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir

项目设置交互

// 用户设置的最终结果
let result: prompts.Answers<
    'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
  >
  try {
    result = await prompts(
      [
        // 初始化项目名称,若命令行设置了相关参数,则不需要调整
        {
          type: argTargetDir ? null : 'text',
          name: 'projectName',
          message: reset('Project name:'),
          initial: defaultTargetDir,
          onState: (state) => {
            targetDir = formatTargetDir(state.value) || defaultTargetDir
          },
        },
        // 项目初始化目标目录,若目标目录不存在或为空,则不需要设置;否则,需确认是否覆盖;
        {
          type: () =>
            !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
          name: 'overwrite',
          message: () =>
            (targetDir === '.'
              ? 'Current directory'
              : `Target directory "${targetDir}"`) +
            ` is not empty. Remove existing files and continue?`,
        },
        // 若目标目录已存在文件,且上面设置不覆盖,直接退出。
        {
          type: (_, { overwrite }: { overwrite?: boolean }) => {
            if (overwrite === false) {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          },
          name: 'overwriteChecker',
        },
        // 校验 package.json name 是否符合要求
        {
          type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
          name: 'packageName',
          message: reset('Package name:'),
          initial: () => toValidPackageName(getProjectName()),
          validate: (dir) =>
            isValidPackageName(dir) || 'Invalid package.json name',
        },
        // 校验模板是否可用并选择要生成的代码模板所属框架
        {
          type:
            argTemplate && TEMPLATES.includes(argTemplate) ? null : 'select',
          name: 'framework',
          message:
            typeof argTemplate === 'string' && !TEMPLATES.includes(argTemplate)
              ? reset(
                  `"${argTemplate}" isn't a valid template. Please choose from below: `,
                )
              : reset('Select a framework:'),
          initial: 0,
          choices: FRAMEWORKS.map((framework) => {
            const frameworkColor = framework.color
            return {
              title: frameworkColor(framework.display || framework.name),
              value: framework,
            }
          }),
        },
        // 基于所选框架,选择该框架下可用的代码模板
        {
          type: (framework: Framework) =>
            framework && framework.variants ? 'select' : null,
          name: 'variant',
          message: reset('Select a variant:'),
          choices: (framework: Framework) =>
            framework.variants.map((variant) => {
              const variantColor = variant.color
              return {
                title: variantColor(variant.display || variant.name),
                value: variant.name,
              }
            }),
        },
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        },
      },
    )
  } catch (cancelled: any) {
    console.log(cancelled.message)
    return
  }
  // user choice associated with prompts
  // 用户设置最终结果
  const { framework, overwrite, packageName, variant } = result

代码生成

处理目标目录及包管理器

// 处理代码生成目录,若选择覆盖,则清空目录内容;若目录不存在,则新建。
if (overwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root, { recursive: true })
}
// 获取包管理器信息,明确当前使用的包管理,用于后续生成对应的执行命令
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')

自定义代码初始化命令

项目初始化方式有两种情况:自定义命令拉取模板代码。模板如 custom-create-vuecustom-nuxtcustom-svelte-kit 等,定义了自定义命令。 自定义命令处理逻辑如下:

  // 获取当前拉取模板的自定义命令
  const { customCommand } =
    FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}
  // 若存在自定义命令,针对不同的包管理器做命令的兼容性处理
  if (customCommand) {
    // 替换自定义命令中的包管理命令部分替换为用户正在使用的包管理器对应命令。
    // 如若用户使用的 pnpm,自定义命令为 npm create svelte@latest TARGET_DIR 时,替换为 pnpm create svelte@latest TARGET_DIR
    const fullCustomCommand = customCommand
      .replace(/^npm create/, `${pkgManager} create`)
      // Only Yarn 1.x doesn't support `@version` in the `create` command
      // Yarn 1.x 版本不支持在 create 命令中使用 @version
      .replace('@latest', () => (isYarn1 ? '' : '@latest'))
      .replace(/^npm exec/, () => {
        // Prefer `pnpm dlx` or `yarn dlx`
        // 若包管理器为 pnpm,替换为 pnpm dlx
        if (pkgManager === 'pnpm') {
          return 'pnpm dlx'
        }
        // 若包管理器为 yarn 且不是 1.x 版本,替换为 pnpm dlx
        if (pkgManager === 'yarn' && !isYarn1) {
          return 'yarn dlx'
        }
        // Use `npm exec` in all other cases,
        // including Yarn 1.x and other custom npm clients.
        // 其余情况全部使用 npm exec
        return 'npm exec'
      })

    const [command, ...args] = fullCustomCommand.split(' ')
    console.log(fullCustomCommand, command)
    // we replace TARGET_DIR here because targetDir may include a space
    // 使用之前确认的初始化代码目标目录名替换自定义命令中的 TARGET_DIR 占位字符串
    const replacedArgs = args.map((arg) => arg.replace('TARGET_DIR', targetDir))
    // 新开一个子进程,执行对应创建命令
    const { status } = spawn.sync(command, replacedArgs, {
      stdio: 'inherit',
    })
    // 子进程执行完成退出
    process.exit(status ?? 0)
  }

初始化 vite 自有代码模版

此处为项目初始化的第二种方式,模板代码已经在 create-vite 目录下定义,需要根据用户设置修改 package.json name 及重命名 .gitignore 文件,然后将代码写入本地即可。

  // 即将用于初始化的代码模板在 vite 工程的路径
  const templateDir = path.resolve(
    fileURLToPath(import.meta.url),
    '../..',
    `template-${template}`,
  )
  // 写入文件行数,将给定的 file 写入到目标地址
  const write = (file: string, content?: string) => {
    const targetPath = path.join(root, renameFiles[file] ?? file)
    if (content) {
      fs.writeFileSync(targetPath, content)
    } else {
      copy(path.join(templateDir, file), targetPath)
    }
  }
  // 读取模板工程的文件,将除 package.json(因为需要根据用户设置修改 name 配置) 之外的所有文件全部写入目标目录
  const files = fs.readdirSync(templateDir)
  for (const file of files.filter((f) => f !== 'package.json')) {
    write(file)
  }
  // 获取 package.json 内容对象
  const pkg = JSON.parse(
    fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'),
  )
  // 修改 pageage.json name 配置
  pkg.name = packageName || getProjectName()
  // 将 package.json 写入本地
  write('package.json', JSON.stringify(pkg, null, 2))
  if (isReactSwc) {
    setupReactSwc(root, template.endsWith('-ts'))
  }
  // 以下部分均是工程初始化完成后的控制台提示的操作信息
  const cdProjectName = path.relative(cwd, root)
  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(
      `  cd ${
        cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
      }`,
    )
  }
  switch (pkgManager) {
    case 'yarn':
      console.log('  yarn')
      console.log('  yarn dev')
      break
    default:
      console.log(`  ${pkgManager} install`)
      console.log(`  ${pkgManager} run dev`)
      break
  }
  console.log()

总结

以上便是 create-vite 所有的内容了,源码总共就 400+ 行,感兴趣的小伙伴可以去 GitHub 瞅瞅。

感谢阅读,我是 SuperYing,愿共同进步。