我正在参与掘金会员专属活动-源码共读第一期,点击参与
本期的源码共读是 《揭秘 create-vite 原理》,这篇文章记录了学习过程中遇到的问题以及解决办法。
create-vite 是什么
create-vite 是一个用于创建 Vite 项目的脚手架工具。它的原理如下:
- create-vite 是一个命令行工具,可以通过 npm 命令来安装和使用。
- 在命令行中运行 create-vite 命令时,会提示用户输入项目名称和其他参数。
- create-vite 会根据用户输入的信息,在当前目录下创建一个新的 Vite 项目。
- 创建过程中,create-vite 会调用 Vite 库中的 API,实现项目的初始化和依赖安装。
- 创建完成后,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
程序入口
获取命令行参数
使用 minimist 包解析用户输入参数
程序执行开始,首先收集命令行用户输入(项目名,模板),修改代码,如下:
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 模块来操作文件系统。主要流程如下:
- 计算文件的目标路径。通过 path.join 方法将文件名和目标目录的路径拼接起来,得到文件的目标路径。
- 判断文件是否存在。如果文件存在,则不执行任何操作;如果文件不存在,则进行下一步。
- 根据参数决定是否拷贝文件。如果 content 参数为空,则表示需要拷贝文件;否则,表示需要将内容写入文件。
- 执行写入操作。如果需要拷贝文件,则调用 copy 方法将源文件拷贝到目标文件;否则,调用 fs.writeFileSync 方法将内容写入文件。
其中,templateDir 变量表示模板文件的目录,renameFiles 变量表示文件重命名的映射表。两者都是通过函数的参数传递进来的。