脚手架编写

脚手架编写

Charliexiu Lv2

🛎️脚手架


脚手架框架
  • bin

  • src

    • contant.js

    • create.js

    • main.js

  • package-lock.json

  • package.json

Test


🛠️插件安装

devDependencies & dependencies

脚本名称 脚本作用
commander 读取版本,设定选项(option),开发命令行工具
consolidate express中的模板引擎可以覆盖其他模板
download-git-repo 可以通过git的方式下载模板到本地
ejs 模板库,json生成html和consolidate配合使用
inquirer 命令行交互
metalsmith 批量处理模板
ora 优化加载等待的交互
chalk 美化终端
ncp 判断文件是否存在
axios http库发送请求

Test

1
npm i

编写bin文件

编写www.js

bin文件下创建 www.js 文件

输入

1
2
3
#! /usr/bin/env node

require("../src/main.js")
  • 回到 package.json 添加如下启动项
1
2
3
"bin": {
"xiu": "./bin/www.js"
},

⭐编写src文件


🚀main.js

版本命令行生成以及文件路径选择都在这里编写

1
2
3
const {version} = require('../package.json');
const path = require('path')
const program = require('commander');

编写文件创建和帮助指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mapActions = {
create: {
alias: "c",
description:"create a project",
examples:["xiu create <project-name>"]
},
config: {
alias:"conf",
description:"config project variable",
examples:["xiu config set<k><v>","xiu config get <k>"]
},
"*": {
alias:"",
description:"command not found",
examples:[]
}
};
  • 完成上一步操作后,我们需要逐步进行命令选择

  • 需要使用 Reflect 中的 ownkeys进行每条命令的遍历

  • 最后使用 forEach进行循环操作

1
2
3
4
5
6
7
8
9
10
11
12
13
Reflect.ownKeys(mapActions).forEach((action) => {
program
.command(action)
.alias(mapActions[action].alias)
.description(mapActions[action].description)
.action(() => {
if(action === "*")
console.log(mapActions[action].description)
else{
require(path.resolve(__dirname,action))(...process.argv.slice(3));
}
});
});

command :命令行(对应mapActions中的每一个动作)

alias :别称,也就是mapActions中的alias

description:同样对应mapActions中的description

因为这边是对mapActions进行循环遍历,所以每一个在mapActions中的属性都需要遍历到

  • 最后一行的判断

help事件

1
2
3
4
5
6
7
8
program.on("--help",() => {
console.log("\nExamples:");
Reflect.ownKeys(mapActions).forEach((action)=> {
mapActions[action].examples.forEach((example)=> {
console.log(example)
});
});
});
  • 执行help的时候输出mapActions和命令行自带的option

    Test

版本显示

1
program.version(version).parse(process.argv);

main代码

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
console.log("welcome xiu");

const {version} = require('../package.json');
const path = require('path')
const program = require('commander');
const mapActions = {
create: {
alias: "c",
description:"create a project",
examples:["xiu create <project-name>"]
},
config: {
alias:"conf",
description:"config project variable",
examples:["xiu config set<k><v>","xiu config get <k>"]
},
"*": {
alias:"",
description:"command not found",
examples:[]
}
};
Reflect.ownKeys(mapActions).forEach((action) => {
program
.command(action)
.alias(mapActions[action].alias)
.description(mapActions[action].description)
.action(() => {
if(action === "*")
console.log(mapActions[action].description)
else{
require(path.resolve(__dirname,action))(...process.argv.slice(3));
}
});
});
// help event
program.on("--help",() => {
console.log("\nExamples:");
Reflect.ownKeys(mapActions).forEach((action)=> {
mapActions[action].examples.forEach((example)=> {
console.log(example)
});
});
});
program.version(version).parse(process.argv);

💡create.js

如果纯复刻,建议先去下面把 constant.js写了再回来

模板的选择模板的复制终端的选择终端的样式都在这里实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const axios = require('axios')
const ora = require('ora')
const Inquirer = require('inquirer')
const path = require('path')
// 包装
const {promisify} = require('util')
let downLoadGitRepo = require('download-git-repo')
downLoadGitRepo = promisify(downLoadGitRepo) // 装成ES6
// 复制
let ncp = require('ncp')
ncp = promisify(ncp)
// 复杂选择
const fs = require('fs')
const metalSmith = require('metalsmith')
let {render} = require('consolidate').ejs
render = promisify(render)
const {downloadDirectory} = require('./constant')
const download = require('download-git-repo')
// 美化终端
const chalk = require('chalk')

获取仓库信息

1
2
3
4
5
// 获取仓库信息
const fetchRepoList = async() => {
const {data} = await axios.get("/*请求地址*/ https://api/orgs/repos")
return data
}

抓取版本列表

1
2
3
4
5
// 抓取版本(tag)列表
const fetchTagList = async(repo) => {
const {data} = await axios.get("/*请求地址*/" `https://api/orgs/${repo}/tags`)
return data
}

下载项目

1
2
3
4
5
6
7
8
9
const downLoad = async(repo, tag) => {
let api = `xiu/${repo}`
if(tag) {
api += `#${tag}`
}
const tempdest = `${downloadDirectory}/${repo}`
await downLoadGitRepo(api,tempdest)
return tempdest
}

repo 就是仓库下的模板名称

tag 就是每个模板的版本号

编写加载项

再完成上面几步之前会有一个加载的过程,重复的加载我们可以封装来完成

1
2
3
4
5
6
7
8
const waitFnLoading = (fn,message) => async(...args) => {
// loading 加载
const spinner = ora(message)
spinner.start()
let repos = await fn(...args)
spinner.succeed();
return repos
}

这里有两个函数体变量

(fn,message) , async(…args)

前者用来接收执行的函数和发出的提示

后者用来对执行函数自带参数进行调用

  • 接下来要做的就是导出下载模块

导出下载模块

交互选择

1
2
3
4
5
6
7
8
9
let repos = await waitFnLoading(fetchRepoList,'fetch template...')()
// 交互选择
repos = repos.map(item => item.name)
const {repo} = await Inquirer.prompt({
name: 'repo',
type: 'list',
message: 'please choise a template',
choices : repos, // 选择列表
});

获取对应的版本号

1
2
3
4
5
6
7
8
let tags = await waitFnLoading(fetchTagList,'fetch template tag...')(repo)
tags = tags.map((item) => item.name)
const {tag} = await Inquirer.prompt({
name: 'repo',
type: 'list',
message: 'please choise a tags for template',
choices : tags, // 选择列表
});

交互选择 和 获取对应的版本号 本质上没有差别

都是通过 inquirer 交互页面查看每个版本进行选择

不同的地方是:

异步调用的 加载项不同:

let repos = await waitFnLoading(fetchRepoList,'fetch template...')<mark>()</mark>

let tags = await waitFnLoading(fetchTagList,'fetch template tag...')<mark>(repo)</mark>
  • 下载的项目首先是临时存放到本地,之后再对存放的内容的文件名称和当前路径下的文件匹配有无重复最后实现模板复制到当前路径下。

  • 在一些简单模板下没有ask.js 但是在绝大部分的复杂模板下有 ask.js文件,这就需要我们对ask.js文件进行访问和重编写

这里需要使用到 metalSmith 对模板的内容进行批量处理

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
// 下载项目 返回临时的存放目录
const result = await waitFnLoading(downLoad,'downloading...')(repo,tag)
if(!fs.existsSync(path.join(result,'ask.js'))) {
await ncp(result,path.resolve(proname))
} else {
// 复杂模板需要选择
await new Promise((resolve,reject) => {
metalSmith(__dirname)
.source(result)
.destination(path.resolve(proname))
.use(async(files,metal,done) => {
// files 现在就是所有的文件
const args = require(path.join(result,'ask.js'))
// 选择
const obj = await Inquirer.prompt(args)
const meta = metal.metadata()
Object.assign(meta,obj)
delete files["ask.js"]
done()
})
.use((files,metal,done)=>{
const obj = metal.metadata()
Reflect.ownKeys(files).forEach(async(file)=>{
if(file.includes("js")|| file.includes("json")) {
let content = files[file].contents.toString()
if(content.includes("<%")) {
content = await render(content, obj)
files[file].contents = Buffer.from(content) // 渲染
}
}
})
done()
}).build(err => {
if(err) {
reject()
}else {
resolve()
}
})
})
}
  • done() 相当于 node.js的中间件 next()
1
const result = await waitFnLoading(downLoad,'downloading...')(repo,tag)
  • 这一行代码获取到的result 就是对应模板和版本号之后的结果
1
2
3
if(!fs.existsSync(path.join(result,'ask.js'))) {
await ncp(result,path.resolve(proname))
} else {}
  • 这里的 if..else.. 是对文件中是否存在 ask.js 进行判断

  • 有就是复杂模板 需要重编写

1
2
3
4
5
6
7
8
9
10
11
12
13
metalSmith(__dirname)
.source(result)
.destination(path.resolve(proname))
.use(async(files,metal,done) => {
// files 现在就是所有的文件
const args = require(path.join(result,'ask.js'))
// 选择
const obj = await Inquirer.prompt(args)
const meta = metal.metadata()
Object.assign(meta,obj)
delete files["ask.js"]
done()
})
  • 这一处代码就是对模板下的所有文件匹配找到ask.js然后遍历执行里面所有的问题最后删除 delete files[“ask.js”]
1
2
3
4
5
6
7
8
9
10
11
12
13
.use((files,metal,done)=>{
const obj = metal.metadata()
Reflect.ownKeys(files).forEach(async(file)=>{
if(file.includes("js")|| file.includes("json")) {
let content = files[file].contents.toString()
if(content.includes("<%")) {
content = await render(content, obj)
files[file].contents = Buffer.from(content) // 渲染
}
}
})
done()
})
  • 使用 metalSmith下的use 对刚刚遍历执行的问题中找到以‘<%’开头的选项就是要用户选择或是填写的

  • 这里说明一下,前面删除的 ask.js 为什么这里还可以访问?

  • 原因在于同样都是中间件,中间件之间是可以互相访问变量内容的,所以删除的ask.js在use里同样算是变量未删除。

成功退出

没什么好说的,直接复制改改就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
    console.log(`
${chalk.green('thanks to use my CLI')}
${chalk.white.bold.bgBlue('success download')}
----------------------------------
${chalk.red('version of this')}
${chalk.white(repo,tag)}
${chalk.white('buy me a coofee')}
design by ${chalk.bgBlue.yellow('wuchanghua')}™️
----------------------------------
${chalk.bgWhite.blue('如果你足够充满智慧,加入我,和我一起创造 QQ:1453346832 Email: 1453346832@qq.com')}
${chalk.green('Finish to 100%')}
${chalk.green('Welcome')}
`)

在这里插入图片描述

create代码

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
const axios = require('axios')
const ora = require('ora')
const Inquirer = require('inquirer')
const path = require('path')
// 包装
const {promisify} = require('util')
let downLoadGitRepo = require('download-git-repo')
downLoadGitRepo = promisify(downLoadGitRepo) // 装成ES6
// 复制
let ncp = require('ncp')
ncp = promisify(ncp)
// 复杂选择
const fs = require('fs')
const metalSmith = require('metalsmith')
let {render} = require('consolidate').ejs
render = promisify(render)
const {downloadDirectory} = require('./constant')
const download = require('download-git-repo')
// 美化终端
const chalk = require('chalk')

// 获取仓库信息
const fetchRepoList = async() => {
const {data} = await axios.get("/*请求地址*/ https://api/orgs/repos")
return data
}
// 抓取版本(tag)列表
const fetchTagList = async(repo) => {
const {data} = await axios.get("/*请求地址*/" `https://api/orgs/${repo}/tags`)
return data
}
// 下载项目
const downLoad = async(repo, tag) => {
let api = `xiu/${repo}`
if(tag) {
api += `#${tag}`
}
const tempdest = `${downloadDirectory}/${repo}`
await downLoadGitRepo(api,tempdest)
return tempdest
}
const waitFnLoading = (fn,message) => async(...args) => {
// loading 加载
const spinner = ora(message)
spinner.start()
let repos = await fn(...args)
spinner.succeed();
return repos
}
module.exports = async(proname) => {
let repos = await waitFnLoading(fetchRepoList,'fetch template...')()
// 交互选择
repos = repos.map(item => item.name)
const {repo} = await Inquirer.prompt({
name: 'repo',
type: 'list',
message: 'please choise a template',
choices : repos, // 选择列表
});
// 获取对应的版本号
let tags = await waitFnLoading(fetchTagList,'fetch template tag...')(repo)
tags = tags.map((item) => item.name)
const {tag} = await Inquirer.prompt({
name: 'repo',
type: 'list',
message: 'please choise a tags for template',
choices : tags, // 选择列表
});
// 下载项目 返回临时的存放目录
const result = await waitFnLoading(downLoad,'downloading...')(repo,tag)
if(!fs.existsSync(path.join(result,'ask.js'))) {
await ncp(result,path.resolve(proname))
} else {
// 复杂模板需要选择
await new Promise((resolve,reject) => {
metalSmith(__dirname)
.source(result)
.destination(path.resolve(proname))
.use(async(files,metal,done) => {
// files 现在就是所有的文件
const args = require(path.join(result,'ask.js'))
// 选择
const obj = await Inquirer.prompt(args)
const meta = metal.metadata()
Object.assign(meta,obj)
delete files["ask.js"]
done()
})
.use((files,metal,done)=>{
const obj = metal.metadata()
Reflect.ownKeys(files).forEach(async(file)=>{
if(file.includes("js")|| file.includes("json")) {
let content = files[file].contents.toString()
if(content.includes("<%")) {
content = await render(content, obj)
files[file].contents = Buffer.from(content) // 渲染
}
}
})
done()
}).build(err => {
if(err) {
reject()
}else {
resolve()
}
})
})
}
console.log(`
${chalk.green('thanks to use my CLI')}
${chalk.white.bold.bgBlue('success download')}
----------------------------------
${chalk.red('version of this')}
${chalk.white(repo,tag)}
${chalk.white('buy me a coofee')}
design by ${chalk.bgBlue.yellow('wuchanghua')}™️
----------------------------------
${chalk.bgWhite.blue('如果你足够充满智慧,加入我,和我一起创造 QQ:1453346832 Email: 1453346832@qq.com')}
${chalk.green('Finish to 100%')}
${chalk.green('Welcome')}
`)
}

Test


💡constant.js

  • 在create中用到的地址下载,你会发现无法复刻到本地原因在于 电脑系统版本不同,临时文件存放路径不同

  • 所以这里可以单独对版本进行判断

1
2
3
4
const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'Home' : 'USERPROFILE']}/.template`
module.exports = {
downloadDirectory
}
  • 如果是 ‘darwin’ 那就是 mac 如果不是 其他的都是Windows

🎈完结

祝福你也可以完成自己的脚手架

  • 标题: 脚手架编写
  • 作者: Charliexiu
  • 创建于: 2023-04-08 22:13:27
  • 更新于: 2023-04-19 00:08:57
  • 链接: https://ccharlie-xiu.github.io/2023/04/08/脚手架编写/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。