“npm create vite“ 是如何实现初始化 Vite 项目?

2023-11-03

欢迎关注我的公号『 前端我废了 』,查看更多文章!!!

前言

我们从 vite 的官方文档中看到,可以使用 npm/yarn/pnpm create 命令来快速初始化一个基于 Vite 的项目;其实很多框架或库都会开发相应的脚手架工具,用于快速初始化项目,例如 create-vite、create-vue、create-react-app 等;这是如何实现的呢?本文将从头分析 create-vite 创建一个 Vite 项目流程的原理。

在这里插入图片描述

从创建项目说起

npm init / create 命令

npm v6 版本给 init 命令添加了别名 create,俩命令一样的

npm init 命令除了可以用来创建 package.json 文件,还可以用来执行一个包的命令;它后面还可以接一个 <initializer> 参数。该命令格式:

npm init <initializer>

参数 initializer 是名为 create-<initializer> 的 npm 包 ( 例如 create-vite ),执行 npm init <initializer> 将会被转换为相应的 npm exec 操作,即会使用 npm exec 命令来运行 create-<initializer> 包中对应命令 create-<initializer>(package.json 的 bin 字段指定),例如:

# 使用 create-vite 包的 create-vite 命令创建一个名为 my-vite-project 的项目
$ npm init vite my-vite-project
# 等同于
$ npm exec create-vite my-vite-project

执行 npm create vite 发生了什么?

当我们执行 npm create vite 时,会先补全包名为 create-vite,转换为使用 npm exec 命令执行,即 npm exec create-vite,接着执行包对应的 create-vite 命令(如果本地未安装 create-vite 包则先安装再执行);了解命令更多信息 initexec

create-vite 原理

create-vite 包源码在 vite 仓库 packages 文件夹下,从 create-vite 包的 package.json 文件的 bin 字段,可以看到配置了两个命令名 create-vitecva 以及对应的映射文件,也就是当我们执行命令时,会去执行对应的映射文件。cva 命令跟 create-vite 命令效果是一样的,我们也可以用 cva 命令来初始化 Vite 项目。

接下来,我们就来分析下 create-vite 的执行流程及源码。

调试准备

  1. 克隆 vite 仓库代码;可以看到 vite 仓库使用 pnpm 作为包管理器,使用 monorepo 方式管理项目,packages 文件夹里面包含 create-vite , vite 和一些 vite 内置插件;
    在这里插入图片描述
  1. 安装依赖;终端进到 packages/create-vite 目录下执行 pnpm i ,也可以直接在根目录使用 pnpm 过滤器选项 --filterpnpm -—filter create-vite i ;

  1. 创建 JavaScript 调试终端;点击进入 VSCode 左侧面板中的运行调试菜单,点击 “JavaScript 调试终端”,就会创建出一个调试终端;
    在这里插入图片描述
  1. 调试;进到 create-vite 包根目录,cd packages/create-vite ,因为源码使用 ts 编写,所以我们需要一个可以运行 ts 的执行器,可以使用 tsx / esno;从 build.config.ts 文件(unbuild 打包工具的配置文件)可以看到入口文件为 src/index.ts ;所以我们在调试终端执行 npx tsx src/index.ts 就可以打断点调试啦。

执行流程

入口文件 src/index.ts 的主函数为 init 函数,init 函数的执行流程图如下:

流程图链接

在这里插入图片描述

源码分析

根据上面的执行流程,我们详细看每个步骤的代码实现(init 函数里面);

1. 判断是否提示用户输入项目名(目录路径)

执行命令时,如果命令行没指定项目名参数,则提示用户输入项目名称(默认值为 vite-project);有则跳过此步骤(例如 npm create vite my-vite-app ,指定项目名为 my-vite-app);

// 用于创建交互提示
import prompts from 'prompts'
// 用于设置输入输出颜色
import { reset, red, blue } from 'kolorist'

// 默认的项目名(目录名)
const defaultTargetDir = 'vite-project'

async function init() {
	// 获取命令行项目名参数, 例如 npm create vite my-vite-project,则 argTargetDir 值为 my-vite-project
	const argTargetDir = formatTargetDir(argv._[0])
	// 目标目录,命令行未指定则使用默认值
	let targetDir = argTargetDir || defaultTargetDir
	// 创建交互提示
	await	prompts(
	    [
	      {
					// 提示用户输入项目名,如果命令行有指定项目名,则 type 赋值为 null,就会跳过此步骤
	        type: argTargetDir ? null : 'text',
	        name: 'projectName',
	        message: reset('Project name:'),
	        initial: defaultTargetDir,
	        onState: (state) => {
	          targetDir = formatTargetDir(state.value) || defaultTargetDir
	        },
	      },
				// 省略其他步骤代码...
			]
		)
}
// 格式化目录名,将结尾斜杠字符 / 去掉
function formatTargetDir(targetDir: string | undefined) {
  return targetDir?.trim().replace(/\/+$/g, '')
}

相关包:

  • prompts 用于创建轻量的、漂亮的、友好的终端交互提示;
  • kolorist 用于设置输入输出颜色

2. 判断当前路径下是否存在相同目录名


不存在则跳过此步骤,存在则提示用户存在相同目录名,并提示是否删除,用户确定删除则继续下一步,否则退出当前执行程序;

async function init() {
	await	prompts(
	    [
				// ...
	      {
          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',
        },
				// 省略其他步骤代码...
			]
		)
}

3. 判断项目名是否为合法包名

在这里插入图片描述

因为项目名后续需要作为包名(package.json 的 name 字段),所以会校验下,如果上一步输入的项目名作为包名不合法,则会自动转为和法治,并提示用户确认;

async function init() {
	// 获取目录名
	const getProjectName = () =>
	   targetDir === '.' ? path.basename(path.resolve()) : targetDir

	await	prompts(
	    [
				// ...
	      {
					// 如何不合法,则提示用户,否则跳过此步骤
          type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
          name: 'packageName',
          message: reset('Package name:'),
          // 转为合法的包名作为初始值
          initial: () => toValidPackageName(getProjectName()),
          validate: (dir) =>
            isValidPackageName(dir) || 'Invalid package.json name',
        },
				// 省略其他步骤代码...
			]
	)
}

// 校验包名是否合法
function isValidPackageName(projectName: string) {
  return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
    projectName,
  )
}
// 转为合法的包名
function toValidPackageName(projectName: string) {
  return projectName
    .trim()
    .toLowerCase() // 转小写
    .replace(/\s+/g, '-') // 空格转为连字符
    .replace(/^[._]/, '') // . _ 字符转为空格
    .replace(/[^a-z\d\-~]+/g, '-') // 非字母 a-z,非数字,非字符 - ~ 转为连字符
}

4. 判断是否提示用户选择框架

在这里插入图片描述
create-vite 还提供了一个命令行选项 --template / -t ,让用户指定使用的模板,如果指定了则判断模板是否存在,存在跳过此步骤,不存在或未指定模板则提示选择;

// 解析命令行参数
import minimist from 'minimist'
import { blue, yellow } from 'kolorist'

// 获取命令行选项值
const argv = minimist<{
  t?: string
  template?: string
}>(process.argv.slice(2), { string: ['_'] })
// 获取当前工作目录路径
const cwd = process.cwd()
// 所有内置模板
const FRAMEWORKS: Framework[] = [
	{
    name: 'vanilla',
    display: 'Vanilla',
    color: yellow,
    variants: [
      {
        name: 'vanilla',
        display: 'JavaScript',
        color: yellow,
      },
      {
        name: 'vanilla-ts',
        display: 'TypeScript',
        color: blue,
      },
    ],
  },
  // 省略其他...
]
// 所有模板
const TEMPLATES = FRAMEWORKS.map(
  (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name],
).reduce((a, b) => a.concat(b), [])

async function init() {
	// create-vite 提供了命令行选项 --template 或 -t 让用户指定模板
	const argTemplate = argv.template || argv.t

	await	prompts(
	    [
				// ...
		    {
					// 若用户通过命令行选项指定了模板且存在此模板,则跳过此步骤,否则提供选择
          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,
            }
          }),
	        },
				// 省略其他步骤代码...
			]
	)
}

相关包:

5. 提示用户选择变体

例如前面选择 vue 框架,则会提供四种变体选择: JavaScript、TypeScript、使用 create-vue 自定义选择集成配置、Nuxt;

// 所有内置模板
const FRAMEWORKS: Framework[] = [
	{
    name: 'vue',
    display: 'Vue',
    color: green,
    variants: [ // 所有变体选项
      {
        name: 'vue',
        display: 'JavaScript',
        color: yellow,
      },
      {
        name: 'vue-ts',
        display: 'TypeScript',
        color: blue,
      },
      {
        name: 'custom-create-vue',
        display: 'Customize with create-vue ↗',
        color: green,
        customCommand: 'npm create vue@latest TARGET_DIR',
      },
      {
        name: 'custom-nuxt',
        display: 'Nuxt ↗',
        color: lightGreen,
        customCommand: 'npm exec nuxi init TARGET_DIR',
      },
    ],
  },
  // 省略其他...
]

async function init() {
	await prompts([
		{
			// 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,
          }
        }),
     },
		// 省略其他步骤代码...
	])

}

6. 删除已有目录或创建新目录

如果第二步骤判断存在已有目录的话,则会递归删除已有目录,否则递归创建新目录(用户在第一步骤中也可以输入路径形式去创建项目目录,例如 a/b);

import fs from 'node:fs'
// 当前目录路径
const cwd = process.cwd()
const defaultTargetDir = 'vite-project'

async function init() {
	const argTargetDir = formatTargetDir(argv._[0])
	let targetDir = argTargetDir || defaultTargetDir

	// 省略其他代码...

	// 终端交互用户的选择结果
	const { overwrite } = result
	// 合成绝对路径
	const root = path.join(cwd, targetDir)
	// 如果用户选择删除,则 overwrite 为 true
	// PS: 其实这里我感觉没必要再做判断了,前面第 2 步骤如果选择取消删除的话,就会退出程序了
	if (overwrite) {
	  // 删除已有目录
	  emptyDir(root)
	} else if (!fs.existsSync(root)) {
	  // 递归创建新目录,因为用户输入的目录也可以是路径,例如 a/b/xxx
	  fs.mkdirSync(root, { recursive: true })
	}
}

function emptyDir(dir: string) {
  if (!fs.existsSync(dir)) {
    return
  }
  for (const file of fs.readdirSync(dir)) {
		// 不删除 .git 目录
    if (file === '.git') {
      continue
    }
		// 同步递归强制删除文件
    fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
  }
}

7. 判断变体选项是否有初始化命令


如果在第五步骤(选择变体)不存在自定义命令配置(customCommand),则跳过此步骤,否则去执行各个框架提供的初始化命令(例如前面框架选择 vue,变体选择 Customize with create-vue ↗, 则使用 create-vue 包的命令去初始化),执行完退出当前程序;

import spawn from 'cross-spawn'
// 默认目录名
const defaultTargetDir = 'vite-project'

async function init() {
	const argTargetDir = formatTargetDir(argv._[0])
	// 通过命令行选项 --template 或 -t 指定的模板
	const argTemplate = argv.template || argv.t
	// 目标目录
	let targetDir = argTargetDir || defaultTargetDir
	// 省略其他代码...
	// 终端交互用户的选择结果
  const { framework, packageName, variant } = result
	// 确定模板
  const template: string = variant || framework?.name || argTemplate
	// 通过 npm 内置环境变量 npm_config_user_agent 解析获取包管理器信息
  const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
  const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')
  // 获取选择的模板变体选项中的自定义命令选项
  const { customCommand } =
    FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}
	// 如果存在自定义命令选项,执行该命令
  if (customCommand) {
		// 生成完整的命令,例如 npm create vite@latest vite-project
    const fullCustomCommand = customCommand
			// 替换 TARGET_DIR 字符串为目标目录
      .replace('TARGET_DIR', targetDir)
			// 替换为用户使用的包管理器
      .replace(/^npm create/, `${pkgManager} create`)
      // Yarn 1.x 版本不支持指定 @latest 标签,所以需去掉
      .replace('@latest', () => (isYarn1 ? '' : '@latest'))
      .replace(/^npm exec/, () => {
        // 优先使用 `pnpm dlx` 或 `yarn dlx` 执行包命令
        if (pkgManager === 'pnpm') {
          return 'pnpm dlx'
        }
        if (pkgManager === 'yarn' && !isYarn1) {
          return 'yarn dlx'
        }
        return 'npm exec'
      })
		// 拆分命令,例如 “npm create vite@latest vite-project” => ["npm", "create", "vite@latest", "vite-project"]
    const [command, ...args] = fullCustomCommand.split(' ')
		// 同步执行命令
    const { status } = spawn.sync(command, args, {
      stdio: 'inherit',
    })
		// 退出当前执行中的程序
    process.exit(status ?? 0)
  }
}
// 通过 npm ua 信息获取使用的包管理器及版本
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],
  }
}

8. 读取模板写入到目标目录

在这里插入图片描述
根据前面用户通过命令行指定或终端交互选择的模板,获取对应模板绝对路径,写入到用户的目录路径;修改 package.json 的 name 字段值为第一步骤输入的项目名;最后提示一些信息,结束流程

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

“npm create vite“ 是如何实现初始化 Vite 项目? 的相关文章

随机推荐

  • 服务器体系结构

    根据服务器的处理器架构 即服务器CPU采用的指令系统 服务器分为CISC架构服务器 RISC架构服务器和VLIW架构服务器 折叠的CISC服务器CISC的英文全称为 复杂指令集计算机 即 复杂指令系统计算机 自从计算机诞生以来 人们一直在使
  • Bitcask存储模型

    大规模分布式存储系统 原理解析与架构实战 读书笔记 最近一直在分析OceanBase的源码 恰巧碰到了OceanBase的核心开发者的新作 大规模分布式存储系统 原理解析与架构实战 看完样章后决定入手 果然物有所值 对于准备学习分布式的同学
  • 测试知识点

    你所熟悉的测试用例设计方法都有哪些 请分别以具体的例子来说明这些方法在测试用例设计工作中的应用 答 有黑盒和白盒两种测试种类 黑盒有等价类划分法 边界分析法 因果图法和错误猜测法 白盒有逻辑覆盖法 循环测试路径选择 基本路径测试 例子 在一
  • 线性代数系列讲解第八篇投影及AX=b(无解情况)求近似解及最小二乘法

    一 投影 1 直线投影到直线 我们会将 b vec b b 投影到 p x a vec p x vec a p xa 我们可以利用发现 e vec e e 和 p vec p p
  • 为什么企业需要私有化专属大模型

    编者按 8月29日凌晨 OpenAI在官网宣布 推出企业版ChatGPT ChatGPT Enterprise 前不久 OpenAI又刚刚发布了针对企业的GPT 3 5 Turbo微调功能 因而引发了一场热烈的讨论 是否仍需要私有化的大模型
  • 【整理三】

    1 说说React生命周期中有哪些坑 如何避免 在代码编写中 遇到的坑往往会有两种 在不恰当的时机调用了不合适的代码 在需要调用时 却忘记了调用 getDerivedStateFromProps 容易编写反模式代码 使受控组件与非受控组件区
  • Python代码扫描:新一代 Python Linter工具Ruff -极力推荐-快的原因是Rust写的?

    目录 RUFF概述 特点 安装和使用 应用场景 配置 编辑器集成 PyCharm 外部工具 Ruff的规则 Ruff速度快的几个原因 最后 注意 后续技术分享 第一时间更新 以及更多更及时的技术资讯和学习技术资料 将在公众号CTO Plus
  • python认证考试-PECP-30-02备考

    前言 进入python institue官网查看认证内容和考试 购买考试凭据 PCEP的政策 PCEP的考试大纲 Python作为目前业界最受欢迎的语言 是大部分数据分析相关从业人员的一项必备技能 对于很多0基础的小白 经常会问的问题就是
  • Nature Machine Intelligence :Nature 机器智能

    官网地址 简称 NML
  • saltstack使用指南:saltstack组件之state状态管理

    saltstack之state状态管理 1 状态的概念及如何撰写一条状态 2 状态配置文件的各个要素 3 使用requisites对状态进行排序控制 一 状态的概念及如何撰写一条状态 远程执行模块的过程类似于一段shell脚本或python
  • 推荐Parallels Desktop虚拟机不为人知的优质功能:端口映射

    我在办公环境下 想要接入公司内部网络 必须报备自己的网卡mac地址 以及个人姓名等信息 非常麻烦 我有一个Linux环境运行的程序 需要临时运行在公司内网 为了避开报备网卡mac地址的麻烦 我把程序放到Parallels Desktop虚拟
  • 同步带长度计算器_同步带轮中心距与同步带长度计算

    同步带轮的节圆直径计算 d Pb Z d 节径Pb 齿距 参考附录表1 Z 齿数 圆周率 3 14159 同步带轮实际外圆直径计算 do d 2 d 节径 节顶距 参考附录表1 同步带轮中心距及同步带节线长计算 D 大带轮的节径d 小带轮节
  • dubbo与zookeeper的关系

    dubbo有很多服务的提供者和消费者 这么多的提供者和消费者需要一个管理中心来管理 这个时候用zookeeper来管理即可 这里的registry就是用zookeeper来实现的 Dubbo建议使用Zookeeper作为服务的注册中心 1
  • FPGA设计进阶2--FPGA时序约束

    Reference 1 xilinx FPGA权威设计指南 2 ASIC集成电路设计 3 综合与时序分析的设计约束实用指南 1 时序检查概念 1 1 基本术语 1 发送沿 Launch Edge 指发送数据的源时钟的活动边沿 2 捕获边沿
  • 在VMware中安装CentOS7

    在VMware中安装CentOS7 一 安装CentOS7 二 安装VMwareTools 三 配置共享文件夹 四 解决CentOS无法访问共享文件夹 一 安装CentOS7 CentOS7 3 1611下载地址 https vault c
  • 学习笔记(十六):商用Wi-Fi的功率延迟分布

    2019 Precise Power Delay Profiling with Commodity Wi Fi 读书笔记 功率延迟分布 Power Delay Profile PDP 刻画多径特性 在动作感知方面意义重大 使用Wi Fi设备
  • 【进阶】Java8新特性的理解与应用

    进阶 Java8新特性的理解与应用 前言 Java 8是Java的一个重大版本 是目前企业中使用最广泛的一个版本 它支持函数式编程 新的Stream API 新的日期 API等一系列新特性 掌握Java8的新特性已经是java程序员的标配
  • FileZilla连接FTP报错421的原因及解决办法

    在使用 FileZilla连接 FTP时报如下错误 421 There are too many connections from your internet address 这是由于FTP限制了客户端的 IP访问连接数量 同一时间内来自同
  • java实现数组穷举 非递归

    穷举String数组 可重载其它数据类型数组 param public static void qiongju String a for int i 0 i
  • “npm create vite“ 是如何实现初始化 Vite 项目?

    欢迎关注我的公号 前端我废了 查看更多文章 前言 我们从 vite 的官方文档中看到 可以使用 npm yarn pnpm create 命令来快速初始化一个基于 Vite 的项目 其实很多框架或库都会开发相应的脚手架工具 用于快速初始化项