源码学习~vite用过了嘛,那你知道它是怎么帮你创建项目的吗?

本文将从下载源码开始一步一步解析vite中关于create-vite的源码,了解在我们输入npm create vite之后,vite做了哪些事情。熟悉之后还可以根据具体需求搭建我们自己的脚手架工具等等。

调试准备

克隆完代码之后,可以找到根目录下CONTRIBUTING.md,这个文件中记录了如果想要调试代码该如何操作。如果没有安装pnpm 可以运行npm i -g pnpm进行安装。

按照上面的说明运行之后我们进入packages/create-vite/package.json文件

json 复制代码
{
  "name": "create-vite",
  "version": "5.1.0",
  "type": "module",
  "license": "MIT",
  "author": "Evan You",
  "bin": {
    "create-vite": "index.js",
    "cva": "index.js"
  },
  //......
}

可以看到bin命令中都指向了index.js,所以接下来我们找到这个文件。index.js文件中只写了import './dist/index.mjs'dist/index.mjs是我们上一步pnpm run build之后打包后的文件,无法看到具体的代码。这时候通过同级下的build.config.js可以看到入口文件是src/index.ts。 这个才是我们真正需要看的核心代码,因为是ts文件,所以我们在运行的时候需要使用npx tsx src/index.ts或者npx esnosrc/index.ts来调试。

源码分析

先来看一下index.ts里面的内容,蓝色的是定义的变量名称,下面的是各种函数方法名称。

从中可以找到init()方法,这个就是我们需要看的主方法,里面包含的正是cli工具是如何帮你创建出一个基础项目目录的。接下来我们来逐步分析下里面的代码。

模块引用和基础变量的定义

typescript 复制代码
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// 创建一个子进程并且可以在不同的操作系统中正确地执行命令
import spawn from 'cross-spawn'
// 解析命令行参数的模块
import minimist from 'minimist'
// 创建交互式输入提示的库
import prompts from 'prompts'
// 终端颜色输出的工具
import {
  blue,
  cyan,
  green,
  lightBlue,
  lightGreen,
  lightRed,
  magenta,
  red,
  reset,
  yellow,
} from 'kolorist'

// Avoids autoconversion to number of the project name by defining that the args
// non associated with an option ( _ ) needs to be parsed as a string. See #4606
const argv = minimist<{
  t?: string
  template?: string
}>(process.argv.slice(2), { string: ['_'] })

// 获取当前工作目录
const cwd = process.cwd();

// .... 一些自定义的变量,篇幅过长放到后面讲

const defaultTargetDir = 'vite-project';

async function init() {

  /**
   * formatTargetDir 方法作用是去除反斜杠 (init方法下定义)
   * function formatTargetDir(targetDir: string | undefined) {return targetDir?.trim().replace(/\/+$/g, '')}
   * 获取命令行的第一个参数并格式化
   */
  const argTargetDir = formatTargetDir(argv._[0])

  // 获取命令行中的模板参数 --template 或 -t
  const argTemplate = argv.template || argv.t

  // 获取项目名称
  let targetDir = argTargetDir || defaultTargetDir
  const getProjectName = () =>
    targetDir === '.' ? path.basename(path.resolve()) : targetDir

  //.... 接下文
}

确定项目名称,选择框架的实现

typescript 复制代码
// ...接上文
let result: prompts.Answers<
  'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
>
try {
  result = await prompts(
    [
      // 这里面是关于projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant 这几项配置的检查及提示
      // projectName 项目名称,也是生成的文件夹名称
      // overwrite 如果已存在同目录,是否覆盖原目录
      // framework 框架名称
      // variant 项目的变体, 在选择完框架之后,提供选择框架下的变体 详细内容可查看 FRAMEWORKS 变量
      // ... 代码暂省略,具体可查源码
    ],
    {
      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

上面的代码中,实现的具体功能也就是下图。

根据项目名称创建项目文件的实现

typescript 复制代码
// 解析当前目录的路径(当前路径/{targetDir})
const root = path.join(cwd, targetDir)

if (overwrite === 'yes') {
  // 删除已存在的目录
  emptyDir(root)
} else if (!fs.existsSync(root)) {
  //创建以项目名为名的文件夹
  fs.mkdirSync(root, { recursive: true })
}

根据选择的框架,生成模板文件的实现

typescript 复制代码
// determine template
let template: string = variant || fra mework?.name || argTemplate
let isReactSwc = false
if (template.includes('-swc')) {
  isReactSwc = true
  template = template.replace('-swc', '')
}
// 使用 Node.js 的环境变量 npm_config_user_agent 来获取用户的用户代理,
// 并根据用户代理中的信息解析出使用的包管理器以及版本号,默认使用'npm'
/**
 * 解析出使用的包管理器名称的方法
 * function pkgFromUserAgent(userAgent: string | undefined) {
 *  if (!userAgent) return undefined
 *  const pkgSpec = userAgent.split(' ')[0]
 *  const pkgSpecArr = pkgSpec.split('/')
 *  return {
 *    name: pkgSpecArr[0],
 *    version: pkgSpecArr[1],
 * }}
 */
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')

//判断选择的variants是否包含自定义命令
const { customCommand } =
  FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}
if(customCommand) {
  //....
  // 如果有自定义命令,则使用对应的工具执行命令(npm | pnpm | yarn | bun)
  // 若要查看是否包含自定义命令 可查看FRAMEWORKS变量
}
// 解析模板文件所在的目录路径  也就是同级目录下以template-开头的文件夹
const templateDir = path.resolve(
  fileURLToPath(import.meta.url),
  '../..',
  `template-${template}`,
)
//定义一个write函数,将模板文件的内容写入目标文件
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外的模板文件写入到目标文件夹中
const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
  write(file)
}

// 读取package.json文件内容解析为json对象
// 修改name属性为用户输入的packageName, 这也是为什么需要把package.json单独处理的原因,保证里面的name值为用户输入的值。

const pkg = JSON.parse(
  fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'),
)
pkg.name = packageName || getProjectName()

write('package.json', JSON.stringify(pkg, null, 2) + '\n')
//...后面是一些关于执行完之后的提示信息,此处省略

总结

通过阅读源码,让我感觉很受用的地方有两个。

  • 首先是通过prompts工具对命令行的交互,之前我对命令行交互的部分总觉得比较复杂,但通过了解这个工具,我发现实际上这些操作并不复杂。prompts可以帮助我们轻松地实现与用户的交互,并根据用户的输入执行相应的操作。
  • 其次是对于fs模块的使用。通过模板文件,我们可以方便地读取和写入文件,创建出我们需要的基础文件。我意识到,对文件的操作是一个 CLI 工具的核心。所以,了解并熟练运用fs模块是很重要的。 整个文件阅读下来让我感受到了一个 CLI工具并没有想象中的那么复杂。总的来说,它的整体流程主要包括文件的读写和命令行交互。熟悉了fspath模块的使用后,,那么自己动手实现一个CLI工具也并不是难事。
相关推荐
我穿棉裤了几秒前
解决el-form表单校验时显示的红色星号与文字对齐的问题
前端·javascript·vue.js
超人不会飞_Jay11 分钟前
2026.6.4 Vue用户中心项目笔记
前端·vue.js·笔记
懂懂tty25 分钟前
Vue3 编译优化
前端·javascript·vue.js
踩着两条虫31 分钟前
VTJ.PRO v2.4.0 多人协作与 AI 批量识图实战评测
vue.js·人工智能·低代码·figma
低保和光头哪个先来31 分钟前
源码篇 生命周期
前端·javascript·vue.js
ct9781 小时前
Vue 项目性能优化
前端·vue.js·性能优化
辞忧九千七1 小时前
Vue3 学习:组件通信完全指南
vue.js
LIUAWEIO14 小时前
vue里面下载配置使用zepto vue中怎样使用zepto
javascript·vue.js·es6·zepto
lantian15 小时前
TypeScript 三斜线指令完全指南:从入门到理解为什么不再需要它
前端·javascript·vue.js
布依前端15 小时前
基于 Vue 3 的 Tiptap 富文本编辑器实践:tiptap-editor-vue3 项目介绍
前端·javascript·vue.js