create-vite 是什么

create-vite 是一个用于创建 Vite 项目的脚手架工具。它的原理如下:

  1. create-vite 是一个命令行工具,可以通过 npm 命令来安装和使用。
  2. 在命令行中运行 create-vite 命令时,会提示用户输入项目名称和其他参数。
  3. create-vite 会根据用户输入的信息,在当前目录下创建一个新的 Vite 项目。
  4. 创建过程中,create-vite 会调用 Vite 库中的 API,实现项目的初始化和依赖安装。
  5. 创建完成后,create-vite 会打印项目信息,并告知用户如何运行项目。

综上所述,create-vite 原理是通过命令行交互和调用 Vite 库的 API 来实现项目的创建和初始化。它是一个方便快捷的工具,可以帮助开发者快速创建 Vite 项目,提高开发效率。

学习目标

  • 理解 npm create vite <your project name> [--template vue] 命令生成 vite 项目的过程
  • 分析 create-vite 源码

准备工作

克隆 vitejs/vite 仓库

项目克隆后可以首先查看 CONTRIBUTING.md 文件,CONTRIBUTING.md 文件中有使用 VSCode 调试源码的详细步骤。

git clone https://github.com/vitejs/vite.git

初始化环境

Vite repo 是一个使用 pnpm 工作区的 monorepo (一个git管理多个项目)。项目使用 pnpm 管理依赖,安装 pnpm 后执行命令

pnpm i

如何调试

进入到 vite\packages\create-vite\src 目录下,执行命令:

npx tsx .\index.ts

在此期间查看控制台交互并在代码中进行断点调试

流程分析

  • 获取命令行参数
  • 处理参数
    • 有无指定项目名
    • 有无指定项目模板
    • 项目名是否重复
    • 校验项目名是否合法
  • 提示用户选择框架
  • 读取模板写入目录

源码解读

程序入口

create-vite 程序入口

image.png

获取命令行参数

使用 minimist 包解析用户输入参数

image.png

程序执行开始,首先收集命令行用户输入(项目名,模板),修改代码,如下:

async function init() {
  // formatTargetDir 函数格式化目标文件夹的路径,使其不包含多余的空格和斜杠。
  // argv._[0] 是命令行的第一个参数
  const argTargetDir = formatTargetDir(argv._[0])
  console.log('argv: ', argv)
  ...
}

修改代码后执行程序,查看 argv 输出了什么

npx esno index.ts demo1

控制台输出:

argv:  { _: [ 'demo1' ] }

用户输入交互

prompts 是一个轻量简便用户交互命令行提示工具。

prompts 中有如下几种可能发生的交互:

  • projectName:获取项目名
  • overwrite:重名的情况询问用户是否覆盖
  • overwriteChecker:检查用户操作
  • packageName:提示用户输入包名(toValidPackageName函数会检查包名是否合法)
  • framework:选择框架
  • variant:选择语言变体(js,ts..)

检查包名是否合法

function isValidPackageName(projectName: string) {
  return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
    projectName
  )
}

在 JavaScript 中,包名的格式一般遵循以下规则:

  • 包名可以包含字母、数字、破折号、点、星号和波浪线等字符,不能包含空格或其他特殊字符。
  • 包名必须以字母或数字开头,不能以破折号或波浪线开头。
  • 包名中可以包含点,表示层级关系。例如,lodash.isarray 表示 lodash 包中 isarray 子包。
  • 包名中可以包含波浪线,表示包名中的单词间的分隔。例如,is-array 表示一个叫 is-array 的包。
  • 包名前可以添加 @ 符号和用户名,表示这是一个用户发布的包。例如,@babel/core 表示 babel 用户发布的 core 包。

模板拷贝

当收集完用户的输入后,开始根据参数进行文件创建,package.json 文件修改等操作。

  const templateDir = path.resolve(
    fileURLToPath(import.meta.url),
    '../..',
    `template-${template}`
  )

  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)
    }
  }

函数的实现方式是通过 fs 模块来操作文件系统。主要流程如下:

  1. 计算文件的目标路径。通过 path.join 方法将文件名和目标目录的路径拼接起来,得到文件的目标路径。
  2. 判断文件是否存在。如果文件存在,则不执行任何操作;如果文件不存在,则进行下一步。
  3. 根据参数决定是否拷贝文件。如果 content 参数为空,则表示需要拷贝文件;否则,表示需要将内容写入文件。
  4. 执行写入操作。如果需要拷贝文件,则调用 copy 方法将源文件拷贝到目标文件;否则,调用 fs.writeFileSync 方法将内容写入文件。

其中,templateDir 变量表示模板文件的目录,renameFiles 变量表示文件重命名的映射表。两者都是通过函数的参数传递进来的。