关于cli:通过-Vite-的-createapp-学习如何实现一个简易版-CLI

26次阅读

共计 7566 个字符,预计需要花费 19 分钟才能阅读完成。

前言

前段时间,尤雨溪答复了一个宽广网友都好奇的一个问题:Vite 会不会取代 Vue CLI?

答案是:是的!

那么,你开始学 Vite 了吗?用过 Vite 的同学应该都相熟,创立一个 Vite 的我的项目模版是通过 npm init @vitejs/app 的形式。而 npm init 命令是在 npm@6.1.0 开始反对的,实际上它是先帮你装置 Vite 的 @vitejs/create-app 包(package),而后再执行 create-app 命令。

至于 @vitejs/create-app 则是在 Vite 我的项目的 packages/create-app 文件夹下。其整体的目录构造:

// packages/create-app
|———— template-lit-element
|———— template-lit-element-ts
|———— template-preact
|———— template-preact-ts
|———— template-react
|———— template-react-ts
|———— template-vanilla
|———— template-vue
|———— template-vue-ts
index.js
package.json

Vite 的 create-app CLI(以下统称为 create-app CLI)具备的能力不多,目前只反对根底模版的创立,所以全副代码加起来只有 160 行,其整体的架构图:

能够看出的确非常简单,也因而 create-app CLI 是一个很值得入门 学习如何实现简易版 CLI 的例子

那么,接下来本文将会围绕以下两个局部带着大家一起通过 create-app CLI 来学习如何制作一个 简易版的 CLI

  • create-app 中应用到的库(minimistkolorist
  • 逐渐拆解、剖析 create-app CLI 源码

create-app CLI 中应用到的库

create-app CLI 实现用到的库(npm)的确很有意思,既有咱们相熟的 enquirer(用于命令行的提醒),也有不相熟的 minimistkolorist。那么,前面这两者又是拿来干嘛的?上面,咱们就来理解一番~

minimist

minimist 是一个 轻量级 的用于解析命令行参数的工具。说起解析命令行的工具,我想大家很容易想到 commander。相比拟 commander 而言,minimist 则以 轻取胜!因为它只有 32.4 kB,commander 则有 142 kB,即也只有后者的约 1/5。

那么,上面咱们就来看一下 minimist 的根底应用。

例如,此时咱们在命令行中输出:

node index.js my-project

那么,在 index.js 文件中能够应用 minimist 获取到输出的 myproject 参数:

var argv = require('minimist')(process.argv.slice(2));
console.log(argv._[0]); 
// 输入 my-project

这里的 argv 是一个对象,对象中 _ 属性的值则是解析 node index.js 后的参数所造成的数组。

kolorist

kolorist 是一个 轻量级 的使命令行输入带有色调的工具。并且,说起这类工具,我想大家很容易想到的就是 chalk。不过相比拟 chalk 而言,两者包的大小差距并不显著,前者为 49.9 kB,后者为 33.6 kB。不过 kolorist 可能较为小众,npm 的下载量大大不如后者 chalk,相应地 chalk 的 API 也较为详尽。

同样的,上面咱们也来看一下 kolorist 的根底应用。

例如,当此时利用产生异样的时候,须要打印出红色的异样信息告知用户产生异样,咱们能够应用 kolorist 提供的 red 函数:

import {red} from 'kolorist'

console.log(red("Something is wrong"))

又或者,能够应用 kolorist 提供的 stripColors 来间接输入带色彩的字符串:

import {red, stripColors} from 'kolorist'

console.log(stripColors(red("Something is wrong"))

逐渐拆解、剖析 create-app CLI 源码

理解过 CLI 相干常识的同学应该晓得,咱们通常应用的命令是在 package.json 文件的 bin 中配置的。而 create-app CLI 对应的文件根目录下该文件的 bin 配置会是这样:

// pacakges/create-app/package.json
"bin": {
  "create-app": "index.js",
  "cva": "index.js"
}

能够看到 create-app 命令则由这里注册失效,它指向的是当前目录下的 index.js 文件。并且,值得一提的是这里注册了 2 个命令,也就是说咱们还能够应用 cva 命令来创立基于 Vite 的我的项目模版(想不到吧 ????)。

create-app CLI 实现的外围就是在 index.js 文件。那么,上面咱们来看一下 index.js 中代码的实现~

根底依赖引入

下面咱们也提及了 create-app CLI 引入了 minimistenquirekolorist 等依赖,所以首先是引入它们:

const fs = require('fs')
const path = require('path')
const argv = require('minimist')(process.argv.slice(2))
const {prompt} = require('enquirer')
const {
  yellow,
  green,
  cyan,
  magenta,
  lightRed,
  stripColors
} = require('kolorist')

其中,fspath 是 Node 内置的模块,前者用于文件相干操作、后者用于文件门路相干操作。接着就是引入 minimistenquirerkolorist,它们相干的介绍下面曾经提及,这里就不反复阐述~

定义根底模版(色彩)和文件

/packages/create-app 目录中,咱们能够看出 create-app CLI 为咱们提供了 9 种我的项目根底模版 。并且,在命令行交互的时候,每个模版之间的色彩各有不同,即 CLI 会应用 kolorist 提供的色彩函数来 为模版定义好对应的色彩

const TEMPLATES = [yellow('vanilla'),
  green('vue'),
  green('vue-ts'),
  cyan('react'),
  cyan('react-ts'),
  magenta('preact'),
  magenta('preact-ts'),
  lightRed('lit-element'),
  lightRed('lit-element-ts')
]

其次,因为 .gitignore 文件的特殊性,每个我的项目模版下都是先创立的 _gitignore 文件,在后续创立我的项目的时候再替换掉该文件的命名(替换为 .gitignore)。所以,CLI 会事后定义一个对象来 寄存须要重命名的文件

const renameFiles = {_gitignore: '.gitignore'}

定义文件操作相干的工具函数

因为创立我的项目的过程中会波及和文件相干的操作,所以 CLI 外部定义了 3 个工具函数:

copyDir 函数

copyDir 函数用于将某个文件夹 srcDir 中的文件复制到指定文件夹 destDir中。它会先调用 fs.mkdirSync 函数来创立制订的文件夹,而后枚举从 srcDir 文件夹下获取的文件名形成的数组,即 fs.readdirSync(srcDir)

其对应的代码如下:

function copyDir(srcDir, destDir) {fs.mkdirSync(destDir, { recursive: true})
  for (const file of fs.readdirSync(srcDir)) {const srcFile = path.resolve(srcDir, file)
    const destFile = path.resolve(destDir, file)
    copy(srcFile, destFile)
  }
}

copy 函数

copy 函数则用于复制文件或文件夹 src 到指定文件夹 dest。它会先获取 src 的状态 stat,如果 src 是文件夹的话,即 stat.isDirectory()true 时,则会调用下面介绍的 copyDir 函数来复制 src 文件夹下的文件到 dest 文件夹下。反之,src 是文件的话,则间接调用 fs.copyFileSync 函数复制 src 文件到 dest 文件夹下。

其对应的代码如下:

function copy(src, dest) {const stat = fs.statSync(src)
  if (stat.isDirectory()) {copyDir(src, dest)
  } else {fs.copyFileSync(src, dest)
  }
}

emptyDir 函数

emptyDir 函数用于清空 dir 文件夹下的代码。它会先判断 dir 文件夹是否存在,存在则枚举该问文件夹下的文件,结构该文件的门路 abs,调用 fs.unlinkSync 函数来删除该文件,并且当 abs 为文件夹时,则会递归调用 emptyDir 函数删除该文件夹下的文件,而后再调用 fs.rmdirSync 删除该文件夹。

其对应的代码如下:

function emptyDir(dir) {if (!fs.existsSync(dir)) {return}
  for (const file of fs.readdirSync(dir)) {const abs = path.resolve(dir, file)
    if (fs.lstatSync(abs).isDirectory()) {emptyDir(abs)
      fs.rmdirSync(abs)
    } else {fs.unlinkSync(abs)
    }
  }
}

CLI 实现外围函数

CLI 实现外围函数是 init,它负责应用后面咱们所说的那些函数、工具包来实现对应的性能。上面,咱们就来逐点剖析 init 函数实现的过程:

1. 创立我的项目文件夹

通常,咱们能够应用 create-app my-project 命令来指定要创立的我的项目文件夹,即在哪个文件夹下:

let targetDir = argv._[0]
// cwd = process.cwd()
const root = path.join(cwd, targetDir)
console.log(`Scaffolding project in ${root}...`)

其中,argv._[0] 代表 create-app 后的第一个参数,root 是通过 path.join 函数构建的残缺文件门路。而后,在命令行中会输入提醒,告述你脚手架(Scaffolding)我的项目创立的文件门路:

Scaffolding project in /Users/wjc/Documents/project/vite-project...

当然,有时候咱们并不想输出在 create-app 后输出我的项目文件夹,而只是输出 create-app 命令。那么,此时 tagertDir 是不存在的。CLI 则会应用 enquirer 包的 prompt 来在命令行中输入询问:

? project name: > vite-project

你能够在这里输出我的项目文件夹名,又或者间接回车应用 CLI 给的默认我的项目文件夹名。这个过程对应的代码:

if (!targetDir) {const { name} = await prompt({
    type: "input",
    name: "name",
    message: "Project name:",
    initial: "vite-project"
  })
  targetDir = name
}

接着,CLI 会判断该文件夹是否存在以后的工作目录(cwd)下,如果不存在则会应用 fs.mkdirSync 创立一个文件夹:

if (!fs.existsSync(root)) {fs.mkdirSync(root, { recursive: true})
}

反之,如果存在该文件夹,则会判断此时文件夹下 是否存在文件,即应用 fs.readdirSync(root) 获取该文件夹下的文件:

const existing = fs.readdirSync(root)

这里 existing 会是一个数组,如果此时数组长度不为 0,则示意该文件夹下存在文件。那么 CLI 则会询问是否删除该文件夹下的文件:

Target directory vite-project is not empty. 
Remove existing files and continue?(y/n): Y

你能够抉择通过输出 yn 来告知 CLI 是否要清空该目录。并且,如果此时你输出的是 y,即不清空该文件夹,那么整个 CLI 的执行就会退出。这个过程对应的代码:

if (existing.length) {const { yes} = await prompt({
    type: 'confirm',
    name: 'yes',
    initial: 'Y',
    message:
      `Target directory ${targetDir} is not empty.\n` +
      `Remove existing files and continue?`
  })
  if (yes) {emptyDir(root)
  } else {return}
}

2. 确定我的项目模版

在创立好我的项目文件夹后,CLI 会获取 --template 选项,即当咱们输出这样的命令时:

npm init @vitejs/app --template 文件夹名

如果 --template 选项不存在(即 undefined),则会询问要抉择的我的项目模版:

let template = argv.t || argv.template
if (!template) {const { t} = await prompt({
    type: "select",
    name: "t",
    message: "Select a template:",
    choices: TEMPLATES
  })
  template = stripColors(t)
}

因为,TEMPLATES 中只是定义了模版的类型,对比起 packages/create-app 目录下的我的项目模版文件夹命名有点差异(短少 template 前缀)。例如,此时 template 会等于 vue-ts,那么就须要给 template 拼接前缀和构建残缺目录:

const templateDir = path.join(__dirname, `template-${template}`)

所以,当初 templateDir 就会等于 当前工作目录 + template-vue-ts

3. 写入我的项目模版文件

确定完须要创立的我的项目的模版后,CLI 就会读取 用户抉择的我的项目模版文件夹 下的文件,而后将它们一一写入 此时创立的我的项目文件夹 下:

可能有点绕,举个例子,抉择的模版是 vue-ts,本人要创立的我的项目文件夹为 vite-project,那么则是将 create-app/template-vue-ts 文件夹下的文件写到 vite-project 文件夹下。

const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {write(file)
}

因为通过 fs.readdirSync 函数返回的是 该文件夹下的文件名形成的数组,所以这里会通过 for of 枚举该数组,每次枚举会调用 write 函数进行文件的写入。

留神此时会跳过 package.json 文件,之后我会解说为什么须要跳过 package.json 文件。

write 函数则承受两个参数 filecontent,其具备两个能力:

  • 对指定的文件 file 写入指定的内容 content,调用 fs.writeFileSync 函数来实现将内容写入文件
  • 复制模版文件夹下的文件到指定文件夹下,调用后面介绍的 copy 函数来实现文件的复制

write 函数的定义:

const write = (file, content) => {const targetPath = renameFiles[file]
    ? path.join(root, renameFiles[file])
    : path.join(root, file)
  if (content) {fs.writeFileSync(targetPath, content)
  } else {copy(path.join(templateDir, file), targetPath)
  }
}

并且,值得一提的是 targetPath 的获取过程,会针对 file 构建残缺的文件门路,并且兼容解决 _gitignore 文件的状况。

在写入模版内的这些文件后,CLI 就会解决 package.json 文件。之所以独自解决 package.json 文件的起因是每个我的项目模版内的 package.jsonname 都是写死的,而当用户创立我的项目后,name 都应该为该项目标文件夹命名。这个过程对应的代码会是这样:

const pkg = require(path.join(templateDir, `package.json`))
pkg.name = path.basename(root)
write('package.json', JSON.stringify(pkg, null, 2))

其中,path.basename 函数则用于获取一个残缺门路的最初的文件夹名

最初,CLI 会输入一些提醒通知你我的项目曾经创立完结,以及通知你接下来启动我的项目须要运行的命令:

console.log(`\nDone. Now run:\n`)
if (root !== cwd) {console.log(`  cd ${path.relative(cwd, root)}`)
}
console.log(`  npm install (or \`yarn\`)`)
console.log(`  npm run dev (or \`yarn dev\`)`)
console.log()

结语

尽管 Vite 的 create-app CLI 的实现仅仅只有 160 行的代码,然而它也较为全面地思考了创立我的项目的各种场景,并做对应的兼容解决。简而言之,非常 小而美。所以,我置信大家通过学习 Vite 的 create-app CLI 的实现,都应该能够顺手甩出(实现)一个 CLI 的代码 ???? ~

点赞 ????

通过浏览本篇文章,如果有播种的话,能够 点个赞,这将会成为我继续分享的能源,感激~

我是五柳,喜爱翻新、捣鼓源码,专一于源码(Vue 3、Vite)、前端工程化、跨端等技术学习和分享,欢送关注我的 微信公众号:Code center

正文完
 0