目录
- 前言
- cli脚手架核心功能
- 优化cli脚手架功能
- 发布cli到npm仓库
- 验证脚手架功能
- 总结
一. 前言
本文是专栏《前端工程化》第7篇文章,会持续更新前端工程化方面详细高质量的教程。
日常开发中,我们一直在用各种各样的脚手架,比如vue-cli,@vue/cli, create-react-app, vite还有其他很多社区的脚手架。通过这些脚手架我们可以很快的生成需要的项目模版,可以在此基础上快速的开发项目。
通常在公司会根据公司的业务封装公司的各种项目模版, 为了方便使用,这时候就需要我们自己写一个cli脚手架工具来通过命令行的方式下载对应模版。
本文将用详细的介绍用100行代码从零搭建一个具有完整的功能cli脚手架,从开发到发布到npm仓库使用,支持全局安装和非全局安装方法。相信学完本文章内容后,就可以在自己公司搭建cli脚手架,简化开发流程,也能提升自己的实际工程化能力。
二. cli脚手架核心功能
在工作中会经常使用到cli脚手架,对它的使用方式肯定不陌生,一般cli脚手架都有以下特点:
- 可以通过npm i xxx-cli -g全局安装,然后在命令行直接使用安装的脚手架创建项目。
- 不全局安装可以通过npx xxx-cli直接从npm上访问该脚手架
- 在命令行执行脚手架时,可以在命令行直接传参数,脚手架会把参数传到配置中。
- 脚手架会出来交互式的命令行让我们选择不同的选项,并且获取到选择的内容。
- 最后根据最终选择结果生成下载对应的模版。
有了上面的cli脚手架的核心逻辑,我们就来实现一个cli脚手架。
2.1 创建脚手架项目
window系统推荐用git命令行,有些命令window命令行不支持。
新建一个wuyou-cli文件夹,在文件夹下执行npm init -y生成package.json文件
mkdir wuyou-cli
cd wuyou-cli
npm init -y
2.2 在全局命令行访问项目
在package.json中有一个bin字段,该字段是一个对象,key是全局变量名称,value是触发全局变量时要执行的文件路径。
配置bin对象后发布包到npm上面,全局安装该包时,就会把bin字段下对象的key注册到全局环境变量中。
知道npm包管理的全局变量机制后,就可以进行配置了,在项目中新建bin目录,里面新建cli.js文件。
这个bin/cli.js文件就是触发wuyou-cli全局变量时会执行的文件, 添加简单代码
#! /usr/bin/env node
console.log('wuyou-cli~~~~')
- #! /usr/bin/env node: 该字段是必须的,触发wuyou-cli时告诉操作系统用node环境来执行cli.js文件
此时项目目录结构
wuyou-cli
├─ bin
│ └─ cli.js
└─ package.json
修改package.json文件,新增bin字段, 配置wuyou-cli变量,触发时执行文件地址指向./bin/cli.js
。
"bin": {
"wuyou-cli": "./bin/cli.js"
},
在本地调试的时候可以借助npm link把当前项目中package.json中的bin字段链接到全局变量。
npm link // mac需要加sudo
链接完成执行在命令行后执行wuyou-cli,就可以看到在cli.js中打印的内容了,代表链接成功了。
可以修改一下cli.js中的内容,修改后再次在命令行执行wuyou-cli,可以看到此时打印的内容也是更新了。
此时npm把bin命令链接到全局就成功了,此时如果把代码发布到npm上,执行npm i wuyou-cli -g全局安装wuyou-cli,就会把wuyou-cli自动链接到全局变量中。
2.4 配置自定义交互式命令
在使用cli脚手架的时候,经常会通过脚手架执行一些命令,比如--version
, --help
来查看版本和帮助信息, 以及常见的create或init命令来触发创建项目模版交互,如果自己写的话会比较耗费时间,这个时候可以直接用社区已经封装好的commander来完成这件事,先进行安装:
npm i commander -S
安装好后先简单测试一下,添加一个检测当前脚手架版本的功能,一般可以通过-V
或者--version
来查看。在./bin/cli.js
里面添加代码:
#! /usr/bin/env node
const program = require('commander')
const package = require('../package.json')
// 定义当前版本
program.version(**v${package.version}**)
// 解析用户执行命令传入参数
program.parse(process.argv)
保存代码后在命令行输入:
wuyou-cli --version
会看到命令行出现了版本号。
2.5 处理创建模版命令
接下来就要处理用户创建模版的命令,我们规定当用户在命令行输入wuyou-cli create的时候触发创建模版操作,触发后第一步要输入项目名称,第二步是选择模版,选择完模版后进行创建操作。
需要能处理用户在命令行的输入,就可以用inquirer模块,inquirer是一个交互式命令行工具库,用于与用户进行交互,最新版本需要import引入,所以本文用的^8.2.5
版本,安装依赖:
npm i inquirer@^8.2.5 -S
然后在bin/cli.js里面添加代码
#!/usr/bin/env node
const program = require('commander');
const inquirer = require('inquirer');
const package = require("../package.json");
// 定义当前版本
program.version(**v${package.version}**);
program
.command('create')
.description('创建模版')
.action(async () => {
const { name } = await inquirer.prompt({
type: 'input',
name: 'name',
message: '请输入项目名称:'
})
console.log("项目名称:", name)
});
program.parse(process.argv);
以上代码使用了 commander 库来解析命令行参数,并定义了 create 命令。在 create 命令被调用时,会使用 inquirer 库与用户进行交互,获取用户输入的项目名称。
以上代码中,我们使用了inquirer的input问题类型。根据不同的问题类型,需要提供不同的参数和答案类型,inquirer字段描述:
- type: 问题类型,可以是 input(输入框)、list(列表选择框)、confirm(二选一选择框)等
- name: 问题名称,用于标识答案对象中对应的属性名
- message: 问题描述,将会作为问题提示信息展示给用户
- choices: 选项列表,只有当问题类型为 list 时才需要提供
代码写好后,在命令行执行:
wuyou-cli create
如下图,执行后会出现请输入项目名称,输入名称后按回车,就可以看到拿到了用户输入的项目名称。
2.6 实现选择项目模版
获取到用户输入的项目名称后,后面要做的事情就是让用户选择要下载的模版,然后把模版代码输出到用户输入的项目名称文件夹中。
下载模版有两种方式:
- 把模版代码放在cli脚手架目录里面,然后把模版代码拷贝到用户目标目录中。
- 把模版放在远程git上面,选择完模版从git远程拉取代码到用户目标目录中。
第一种下载模版速度快,但每次模版变动都要升级脚手架版本,不太灵活,所以这里采用第二种方法。
模版要换成真实的git地址,本文模版地址采用我原先三篇工程化文章中的代码模版:
- webpack5-react-ts:【前端工程化】webpack5从零搭建完整的react18+ts开发和打包环境
- react18-vite2-ts :【前端工程化】配置React+ts项目完整的代码及样式格式和git提交规范
- dumi2-demo: 【前端工程化】带你使用dumi2一步一步搭建自己的React组件库和函数库
在bin目录下新建templates.js文件,添加以下代码:
/** 暴露模版代码 */
module.exports = [
{
name: 'webpack5-react-ts',
value: 'https://github.com:guojiongwei/webpack5-react-ts'
},
{
name: 'react18-vite2-ts',
value: 'https://github.com:guojiongwei/react18-vite2-ts'
},
{
name: 'dumi2-demo',
value: 'https://github.com:guojiongwei/dumi2-demo'
}
]
注意模版地址部分,域名github.com和模版地址之间是用冒号:隔开的,不是斜杠/,这个是下一节下载git仓库代码模版所用到的库download-git-repo的规则。
实际项目中要根据自己的需求配置不同的模版,比如gitlab,gitee等,文章后面也会换成接口动态请求。
支持gitee-2023-07-21补充
下载gitee模版需要把路径设置为direct:https://gitee.com/xxx/xxx.git
,然后使用下载模版工具downloadGitRepo时(下面会讲),需要带上{ clone: true }
参数
downloadGitRepo(projectTemplate, dest, { clone: true }, (err) => {})
定义好模版后在bin/cli.js添加选择模版,选择模版是让用户选择列表,需要用inquirer库的list类型:
// ...
const templates = require('./templates.js')
program
.command('create')
.description('创建模版')
.action(async () => {
// ...省略 输入项目名称代码
// 新增选择模版代码
const { template } = await inquirer.prompt({
type: 'list',
name: 'template',
message: '请选择模版:',
choices: templates // 模版列表
})
console.log('模版:', template)
});
program.parse(process.argv);
// ...
代码保存后再次在终端输入wuyou-cli create,就可以选择真实的模版数据,拿到对应项目的git地址。
2.7 实现下载模版
拿到用户选择的模版地址后,就要根据用户输入的项目名称,把指定的项目模版下载到对应项目文件夹中,实现下载git项目模版的功能使用download-git-repo依赖来完成,安装依赖:
npm i download-git-repo -S
download-git-repo的语法
const downloadGitRepo = require('download-git-repo')
downloadGitRepo('项目git地址', '目标文件夹', function(err) {
if (err) {
console.log('下载失败', err)
} else {
console.log('下载成功')
}
})
默认会拉取master分支的代码,如果想从其他分支拉取代码,可以在git地址后面添加
#branch
选择分支。
项目的git地址在选择完模版时可以获取到,现在要获取目标文件夹,目标目录应该是用户执行命令行所在位置下的项目名称文件夹。
可以通过process.cwd()
方法来获取用户执行命令行所在的目录位置,再加上用户输入的项目名称,所以目标文件夹路径应该为:
const path = require('path')
// 目标文件夹 = 用户命令行所在目录 + 项目名称
const dest = path.join(process.cwd(), name)
项目git地址和目标文件夹都拿到后,接下来,我们需要在获取到模版地址后去进行下载模版的操作,修改bin/cli.js文件,添加下载模版代码:
// 引入
const path = require("path")
const downloadGitRepo = require('download-git-repo')
// ...
// 获取目标文件夹
const dest = path.join(process.cwd(), name)
// 开心下载模版
downloadGitRepo(template, dest, (err) => {
if (err) {
console.log('创建模版失败', err)
} else {
console.log('创建模版成功')
}
})
代码写好后,我们再在命令行输入
wuyou-cli create
会出现输入项目名称和选择模版操作,选择完模版后就会自动把模版下载到我们输入到项目名称文件夹里面,如下图:
到这里一个基础的cli命令行脚手架就创建好啦!
三. 优化cli脚手架
上一节已经实现了一个基础的脚手架功能,但一些细节体验不够完善,我们需要完善一下。
3.1 优化下载模版时等待交互
在上面下载模版的时候,由于是从github下载的模版,有时候网络不好,下载时间会久一些,如果什么都不做看着像卡住一样。我们可以添加一下命令行loading动画来提升用户体验。
可以用ora这个库来实现这个功能,ora是一个命令行的loading动画库,最新版本需要import引入,所以本文用的^5.4.1
版本,安装依赖:
npm i ora@^5.4.1 -S
安装好后修改bin/cli.js代码,在downloadGitRepo下载模版之前调用展示loading代码,在下载模版结束后,停止loading展示。
// bin/cli.js
// ...
const ora = require('ora') // 引入ora
// 定义loading
const loading = ora('正在下载模版...')
// 开始loading
loading.start()
// 开始下载模版
downloadGitRepo(template+, dest, (err) => {
if (err) {
loading.fail('创建模版失败:' + err.message) // 失败loading
} else {
loading.succeed('创建模版成功!') // 成功loading
}
})
ora的loading代码添加好后,再次执行wuyou-cli create,输入名称,选择模版,下载的时候就会出现动态的loading和提示语,让用户感知到正在下载模版,提升使用体验。
3.2 添加可操作命令提示
现在我们的cli只有执行wuyou-cli --version和wuyou-cli create创建模版时命令行才有反应,有时候用户想查询都有哪些命令可以操作,所以可以加一个-help
或者-h
命令时查询所有可执行的命令。
program提供了监听--help
操作,在bin/cli.js中配置后用户执行-h
或者 --help
都会触发,会自动把当前program注册的所有命令都打印到控制台,供用户查看。
// bin/cli.js
// ...
program.on('--help', () => {}) // 添加--help
添加好后再在命令输入命令:
wuyou-cli --help
可以看到在控制台可以看到当前cli脚手架支持的command命令以及描述,可以很大的方便用户使用。
3.3 支持从命令行传参数
上面用户输入项目名称和选择模版都是通过wuyou-cli create命令来触发的,也可以通过命令行参数形式直接项目传入名称和模版,就像vite一样,可以直接在命令行输入项目名称和模版名称。
# npm 6.x
npm create vite@latest my-vue-app --template vue
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
# pnpm
pnpm create vite my-vue-app --template vue
实现这个功能依然是使用program库,它提供了获取命令行参数的功能,在create后面加上[projectName]
,可以获取命令传入的项目名称,配置.option
可以获取到通过-t
或--template
传入的模版名称:
program
.command('create [projectName]') // [projectName]是可选 <projectName>是必填
.option('-t, --template', '模版名称') // 配置项 --template xxx
.description('创建项目')
.action(async (projectName, options) => {
// projectName: 项目名称
// options: 配置项 { template: string }
})
知道实现机制后,就可以改bin/cli.js代码了:
// ...
program
.command("create [projectName]")
.description("创建模版")
.option('-t, --template <template>', '模版名称')
.action(async (projectName, options) => {
// 1. 从模版列表中找到对应的模版
let project = templates.find(template => template.name === options.template)
// 2. 如果匹配到模版就赋值,没有匹配到就是undefined
let projectTemplate = project ? project.value : undefined
console.log('命令行参数:', projectName, projectTemplate)
// 3. // 如果用户没有传入名称就交互式输入
if(!projectName) {
const { name } = await inquirer.prompt({
type: "input",
name: "name",
message: "请输入项目名称:",
})
projectName = name // 赋值输入的项目名称
}
console.log("项目名称:", projectName)
// 4. 如果用户没有传入模版就交互式输入
if(!projectTemplate) {
const { template } = await inquirer.prompt({
type: 'list',
name: 'template',
message: '请选择模版:',
choices: templates // 模版列表
})
projectTemplate = template // 赋值选择的项目名称
}
console.log('模版:', projectTemplate)
const dest = path.join(process.cwd(), projectName)
// 5. 开始下载模版
ora('正在下载模版...').start() // 开始loading
downloadGitRepo(projectTemplate, dest, (err) => {
ora().stop() // 结束loading
if (err) {
console.log('创建模版失败', err)
} else {
console.log('创建模版成功')
}
})
})
// ...
代码保存后,开始测试一下。
- 在命令行输入wuyou-cli create,传入项目名称hello和模版webpack5-react-ts:
wuyou-cli create hello -t webpack5-react-ts
看下图可以看到获取到了命令行参数项目和模版名称,跳过了交互输入,直接开始下载模版了。
- 在命令行输入wuyou-cli create,只传入项目名称hello,不传模版:
wuyou-cli create hello
看下图可以看到获取到了命令行参数项目名称,没有获取到模版名称,会进入交互式选模版阶段。
- 只输入wuyou-cli create,项目名称和模版名称都没获取到,就会走输入名称和选择模版流程。
到这一步,我们的cli脚手架就支持从命令行直接传入参数了。
3.4 添加是否覆盖原文件夹
当用户输入的目录所在位置已经有同名的文件夹时,应该提示用户是否覆盖,如果选了覆盖则把原文件夹删除,如果选择不覆盖,就停止所有操作,退出命令行。
根据这个思路可以推断出这个判断应该在用户完项目名称后去做判断,利用fs模块的existsSync方法判断目标文件夹是否存在,如果存在就使用inquirer模块的confirm类型交互来让用户选择是否覆盖。
目标文件夹就是我们上面获取到dest
const dest = path.join(process.cwd(), projectName)
为了删除文件夹方便,我们使用fs-extra,它在fs模块基础上封装了一些方法,比如咱们用到的删除文件夹,安装依赖:
npm i fs-extra -S
安装好后进行代码实现,修改bin/cli.js代码,在下载模版前面面添加:
// ...
const fs = require('fs-extra') // 引入fs-extra
// ...
const dest = path.join(process.cwd(), projectName)
// 判断文件夹是否存在,存在就交互询问用户是否覆盖
if(fs.existsSync(dest)) {
const { force } = await inquirer.prompt({
type: 'confirm',
name: 'force',
message: '目录已存在,是否覆盖?',
})
// 如果覆盖就删除文件夹继续往下执行,否的话就退出进程
force ? fs.removeSync(dest) : process.exit(1)
}
然后再次执行创建命令wuyou-cli create,当目标文件夹已存在时,会出现让用户选择是否覆盖的操作
选择是**(y),则会删除原有文件夹,拉取新模版,如果选否(n)**,则退出命令行,到这里让用户选择是否覆盖原文件夹的操作就处理好了。
3.5 添加模版创建后引导操作
像vite, vue-cli等脚手架在创建完项目后,都会有一个引导提示,比如 cd xxx进入文件夹, npm i安装依赖等等,我们也可以加一下,比较简单,在模版下载成功的回调里面添加打印信息:
downloadGitRepo(projectTemplate, dest, (err) => {
if (err) {
loading.fail('创建模版失败:' + err.message) // 失败loading
} else {
loading.succeed('创建模版成功!') // 成功loading
// 添加引导信息(每个模版可能都不一样,要按照模版具体情况来)
console.log(**\ncd ${projectName}**)
console.log('npm i')
console.log('npm start\n')
}
})
如下图,创建完模版后就有引导提示信息了:
3.6 用接口获取动态模版
上面的模版是固定写死的模版列表,如果新增或者删除模版,就需要修改cli脚手架代码,用户也要跟着升级版本,不太灵活。可以换成接口请求的方式,而像github,gitlab等代码仓库网站,都有提供获取仓库信息等api,比如github的api.github.com,点击链接可以看到很多接口,如下图:
在倒数第二个又一个获取用户仓库列表信息的接口,其中**${user}是用户名称参数,可以通过这个接口查询到对应github用户下所有公开的git**仓库信息,返回一个列表数据,里面有每个仓库的具体信息,点击可以看到返回的结果api.github.com/users/guoji…。
通过这个接口,可以来实现动态获取模版列表的功能,开始写代码,在bin目录下新增api.js文件,添加获取用户下git仓库列表接口:
// bin/api.js
const https = require('https')
/** 获取用户git仓库列表信息 */
function getGitReposList(username) {
return new Promise((resolve, reject) => {
https.request(**https://api.github.com/users/${username}/repos**, {
headers: {
'User-Agent': username
}
}, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk.toString()
})
res.on('end', () => {
const list = JSON.parse(data)
resolve(list.map(item => ({ // 组合成模版所需要的name,value结构
name: item.name,
value: **https://github.com:${username}/${item.name}**
})))
})
res.on('error', (err) => {
reject(err)
})
}).end()
})
}
module.exports = {
getGitReposList
}
写好获取列表数据后,修改bin/cli.js代码,把固定从templates.js引入的模版换成动态的接口数据:
// bin/cli.js
// const templates = require("./templates.js") 删除这一行代码
const { getGitReposList } = require('./api.js') // 新增
// ...
program
.command("create [projectName]")
.description("创建模版")
.option('-t, --template <template>', '模版名称')
.action(async (projectName, options) => {
// 添加获取模版列表接口和loading
const getRepoLoading = ora('获取模版列表...')
getRepoLoading.start()
const templates = await getGitReposList('guojiongwei')
getRepoLoading.succeed('获取模版列表成功!')
// ...
换好后再次执行wuyou-cli create,执行结果如下图,模版列表已经换成了由接口返回的动态数据了。
github可以单独创建一个账号存放模版列表,避免太多非模版仓库展示。
gitlab支持分组,可以通过获取某个分组下面的仓库的api获取到列表。
到这里,一个基础功能完整的cli脚手架就开发好了,可以发布到公共npm或公司自己npm私有库上面进行使用了。
四. 发布cli到npm仓库
发布npm包比较简单,先在package.json中添加一下要发布的文件,本文章项目中只需要把bin目录发布上去就可以了,修改package.json文件,添加files字段,把bin文件夹添加进去:
"files": [
"bin"
]
再在项目里面添加使用文档,添加README.md:
# 学习搭建cli脚手架
## 安装
### 全局安装
$ npm install -g wuyou-cli
# or yarn
$ yarn global add wuyou-cli
### 借助npx
创建模版
$ npx create wuyou-cli <name> [-t|--template]
示例
$ npx create wuyou-cli hello-cli -template dumi2-demo
## 使用
创建模版
$ wuyou-cli create <name> [-t|--template]
示例
$ wuyou-cli create hello-cli -t dumi2-demo
然后去npm官网注册账号,注册好后在命令行通过npm login进行登录,会需要邮箱二级密码验证,登录成功后最后回到项目目录,打开命令行,输入:
npm publish
就可以发布到npm仓库了。
npm包名称不能重复发布,这里记得修改一下package.json里面的name和bin字段,修改后再进行发布。
五. 验证脚手架功能
5.1 全局安装
按照自己文档上写的,先全局安装wuyou-cli:
npm install -g wuyou-cli # mac要加sudo
然后再执行创建操作:
npx wuyou-cli create
就可以看到熟悉的选项出来了,代表我们的脚手架功能可以全局安装使用了。
5.2 非全局借助npx安装
用npx的话,就不用全局安装了,每次可以实时获取到最新的cli脚手架版本,执行命令
wuyou-cli create
出现下图,代表功能也是正常的。
六. 总结
本文介绍了前端搭建CLI脚手架的基本原理和步骤,实现其中大部分常用的功能,并将其发布到npm官方库中使用。同时还有要优化的地方,可以自己尝试一下看看,比如覆盖文件夹操作也支持命令行传参,添加-l命令支持查询模版列表等,可以自己尝试一下。
本文代码仓库地址:github.com/guojiongwei…
npm包地址:www.npmjs.com/wuyou-cli
如果对大家有帮助欢迎收藏,点赞,start。