前段时间,尤雨溪回答了一个广大网友都好奇的一个问题:Vite 会不会取代 Vue CLI?javascript
答案是:是的!前端
那么,你开始学 Vite 了吗?用过 Vite 的同窗应该都熟悉,建立一个 Vite 的项目模版是经过 npm init @vitejs/app
的方式。而 npm init
命令是在 npm@6.1.0
开始支持的,实际上它是先帮你安装 Vite 的 @vitejs/create-app
包(package),而后再执行 create-app
命令。vue
至于 @vitejs/create-app
则是在 Vite 项目的 packages/create-app
文件夹下。其总体的目录结构:java
// 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 行,其总体的架构图:node
能够看出确实很是简单,也所以 create-app
CLI 是一个很值得入门学习如何实现简易版 CLI 的例子。react
那么,接下来本文将会围绕如下两个部分带着你们一块儿经过 create-app
CLI 来学习如何制做一个简易版的 CLI:git
create-app
中使用到的库(minimist
、kolorist
)create-app
CLI 源码create-app
CLI 实现用到的库(npm)确实颇有意思,既有咱们熟悉的 enquirer
(用于命令行的提示),也有不熟悉的 minimist
和 kolorist
。 那么,后面这二者又是拿来干吗的?下面,咱们就来了解一番~npm
minimist
是一个轻量级的用于解析命令行参数的工具。提及解析命令行的工具,我想你们很容易想到 commander
。相比较 commander
而言,minimist
则以轻取胜!由于它只有 32.4 kB,commander
则有 142 kB,即也只有后者的约 1/5。json
那么,下面咱们就来看一下 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
是一个轻量级的使命令行输出带有色彩的工具。而且,提及这类工具,我想你们很容易想到的就是 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"))
了解过 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 引入了 minimist
、enquire
、kolorist
等依赖,因此首先是引入它们:
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')
其中,fs
和 path
是 Node 内置的模块,前者用于文件相关操做、后者用于文件路径相关操做。接着就是引入 minimist
、enquirer
和 kolorist
,它们相关的介绍上面已经说起,这里就不重复论述~
从 /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 实现核心函数是 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
你能够选择经过输入 y
或 n
来告知 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
函数则接受两个参数 file
和 content
,其具有两个能力:
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.json
的 name
都是写死的,而当用户建立项目后,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 三、Vite)、前端工程化、跨端等技术学习和分享,欢迎关注个人 微信公众号:Code center。