源码学习~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工具也并不是难事。
相关推荐
十一吖i5 分钟前
前端将后端返回的文件下载到本地
vue.js·elementplus
光影少年6 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
mosen8682 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
Gavin_9154 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
Devil枫9 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
GIS程序媛—椰子10 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
毕业设计制作和分享11 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
程序媛小果11 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot