搭建cli脚手架

大家都会用到@vue/cli或者create-react-app这种脚手架,但是每个公司都或多或少有一些自己的基础框架。每次都从头搭建一个项目比较麻烦,所以准备搭建一个简单的脚手架方便生成项目代码。

前期搭建准备

脚手架用到了以下几个库:

  • chalr: 用于命令行绘制彩色文字
  • commander: 用于提供指令,例如vue/cli的vue create appname就是一个指令
  • download: 下载
  • fs-extra: 好用的文件处理
  • ora: 用于加载动画
  • inquirer: 用于命令行交互,例如vue/cli在命令行让选择vue版本,css处理器等交互功能。

项目的目录结构如下:

cli

|____ bin # bin目录

| |____ cli # 入口文件,可以没有后缀名

|____ lib # lib目录

| | ____ create.js # 创建项目过程中的主要交互逻辑

| | ____ Creator.js # Creator类

| | ____ Downloader.js # Downloader类

| ____ package.json

目前的脚手架功能其实没必要这么复杂,但是后续还要有新功能加入,所以暂时搞成这个样子了。

package.json中配置bin属性。

1
2
3
4
5
{
"bin": {
"name": "./bin/cli"
}
}

这么配置之后就是调用name指令会执行bin/cli这个文件。比如你要用vue create appname来创建项目就配置name为vue这样就可以。

脚手架文件

脚手架是以nodejs运行的,在代码的头部加上\#! /usr/bin/env node标明是以nodejs运行的。

bin/cli

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#! /usr/bin/env node

const program = require("commander");
const chalk = require("chalk")

// 定义指令
program
.command(`create <app-name>`)
.description(`create a new project`)
.option('-f, --force', 'overwrite target directory if it exists')
.action((name, cmd) => {
require('../lib/create')(name, cmd)
})

program.on('--help', function(){
console.log()
console.log(`Run ${chalk.cyan('脚手架name <command> --help')} for details`)
console.log()
})

program
.version(`脚手架名字 ${require("../package.json").version}`)
.usage(`<command> [option]`)
// 解析用户执行命令传入的参数
program.parse(process.argv);

这里只是搭建了基础功能,还有更多功能可以自行扩展

  • command 指令
  • description 描述
  • option 额外选项,比如这里的-f就是强制替换同名目录

拉取git仓库创建项目

我们在调用create 指令之后调用了lib/create.js这个文件

  • lib/create.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const path = require("path")
const fse = require("fs-extra")
const Inquirer = require("inquirer")
const Creator = require("./Creator")
const ora = require("ora")
module.exports = async function create(projectName, options){
// 当前工作目录
const cwd = process.cwd()
// 目标目录
const targetDir = path.join(cwd, projectName)
// 如果已存在同名目录
if(fse.existsSync(targetDir)){
// 如果是强制创建项目 --force , -f
if(options.force){
// 移除已有目录
await fse.remove(targetDir)
}else{
// 提示用户是否确认覆盖
let {action} = await Inquirer.prompt([ // 询问方式
{
name: "action",
type: "list",
message: "Target directory already exists, Please pick an action",
choices: [ // 选项
{name: "Overwrite", value: "overwrite"},
{name: "Cancel", value: false}
]
}
])
// 如果cancel直接退出
if(!action){
return;
}else if(action === "overwrite"){
let spinner = ora("wating for remove...")
spinner.start()
await fse.remove(targetDir)
spinner.succeed("remove success")
}
}
}
// 正式创建流程
const creator = new Creator(projectName, targetDir)
// 开始创建
creator.create()
}
  • lib/Creator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const Inquirer = require("inquirer")
const path = require("path")
const Downloader = require("./Downloader")
const fse = require("fs-extra")
const fs = require("fs")
const chalk = require("chalk")
class Creator {
constructor(projectName, targetDir){
this.name = projectName
this.target = targetDir
}
async create(){
let repo = await this.fetchRepo()
await this.download(repo)
}
/**
* @author zhaolei_hu
* @description 查询仓库列表
* @returns {string} repo 选择的仓库
*/
async fetchRepo(){
// 这里可以动态获取,我这边不做演示,写死
let {repo} = await Inquirer.prompt({
name: "repo",
type: "list",
choices: ["vue2", "vue3"],
message: "Please choice a template to create project"
})
return repo
}
/**
* @author zhaolei_hu
* @description 下载
* @param {string} repo
*/
async download(repo){
let requestUrl = `仓库地址`
let downloader = new Downloader(requestUrl, path.resolve(process.cwd(), this.name), this.name)
downloader.download(this.downloadResHandler, this)
}
/**
* @author zhaolei_hu
* @description 下载结果处理
* @param {*} err
*/
async downloadResHandler(err){
if(err){
return;
}
let dir = path.resolve(process.cwd(), this.name)
await this.editName(dir, this.name)
}
/**
* @author zhaolei_hu
* @description 修改package.json的name为项目名
* @param {string} dir
* @param {string} name
*/
async editName(dir, name){
try {
const packageObj = await fse.readJson(path.resolve(dir, "package.json"))
packageObj.name = name
await fse.outputFile(path.resolve(dir, "package.json"), JSON.stringify(packageObj, "", "\t"))
this.drawMessage(name)
} catch (err) {
console.error(err)
}
}
/**
* @author zhaolei_hu
* @description 绘制提示信息
*/
drawMessage(name){
let logo = fs.readFileSync(path.resolve(__dirname, "logo.txt"), "utf-8")
console.log(logo)
console.log(chalk.hex("#ccc").bold("🎉 Successfully created project."))
console.log(chalk.hex("#ccc").bold("👉 Get started with the following commands:"))
console.log()
console.log(' ' + chalk.hex("#666").bold("$") + " " + chalk.hex("#60d1dd").bold('cd '+name))
console.log(' ' + chalk.hex("#666").bold("$") + " " + chalk.hex("#60d1dd").bold('npm install'))
console.log(' ' + chalk.hex("#666").bold("$") + " " + chalk.hex("#60d1dd").bold('npm run serve'))
console.log()
}
}

module.exports = Creator
  • lib/Downloader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const downloadUrl = require('download')
const ora = require("ora")
class Downloader {
/**
* @author zhaolei_hu
* @param {string} requestUrl 仓库地址
* @param {string} targetDir 存储地址
* @param {string} 项目名
*/
constructor(requestUrl, targetDir, name){
this.requestUrl = requestUrl
this.targetDir = targetDir
this.name = name
}
/**
* @author zhaolei_hu
* @description download
* @param {Function} fn
* @param {Creator} creator Creator实例,保证this指向问题
*/
async download(fn,creator){
let spinner = ora("please wait for a moment, downloading...")
spinner.start()
let downloadOptions = {
extract: true,
strip: 1,
mode: '666',
headers: {
accept: 'application/zip',
}
}
downloadUrl(this.requestUrl, this.targetDir, downloadOptions)
.then(function () {
spinner.succeed("download success");
fn.call(creator)
})
.catch(function (err) {
spinner.fail("download failed, please retry...")
fn.call(creator, err)
})
}
}
module.exports = Downloader

这里的代码也并不复杂,看一下注释应该就没问题,基本流程就是选择要下载的仓库,然后去git拉取仓库下来,然后可以对package.json做一些额外配置,比如改项目名等等。。。

  • fs-extra可以很方便对处理json
  • JSON.stringify(xx,xx, “\t”)中传入制表符为了格式化json文件
作者

胡兆磊

发布于

2022-09-16

更新于

2022-10-23

许可协议