预备知识
-
本地安装卸载包
在项目根目录下运行下面命令
# 安装 npm link # 卸载 npm unlink
-
命令行命令指定运行文件
package.json
文件中bin
字段指定
项目依赖包
-
chalk 打印出彩色字
-
commander 完整的 node.js 命令行解决方案
-
download-git-repo 下载仓库代码
-
handlebars 编译文件
-
inquirer 交互式命令行界面
-
ora 加载动画
-
update-notifier 更新通知
-
figlet 炫酷文字
支持的命令
da --help
da -version
da update
da add
da delete
da list
da create
da site add
da site delete
da site list
命令详情介绍以及项目地址点击传送门
动起来
1. 新建文件夹da-cli
2. 打开命令行工具,进入文件夹,运行初始化仓库
cd ./da-cli
npm init -y
3. 安装依赖
npm i chalk commander download-git-repo handlebars inquirer ora update-notifier figlet -S
- handlebars 不是一定要用,可以使用node读写文件,本项目使用是多介绍一种修改文件内容方案,项目中会使用两种:原生node读写文件、使用handlebars修改文件内容。
- figlet 不是一定要用,主要为了炫酷,装一下
- ora 不是一定要用,为了体验优化
4. 修改package.json
-
脚手架会有多个命令,每个命令会有对应的处理逻辑,一般是会分模块写,这样增加可读性、方便维护
-
命令执行文件,可以由命令根据已经配置的一个文件自己匹配文件,也可以自己注册
例如:
da init
和da add
两个命令(da
是自定义的,必须和bin
中键保持一致)匹配文件模式:(自己编的模式)
// package.json文件 { ... "bin": { "da": "./bin/da.js" }, ... }
./bin/da.js
文件中不会写da init
和da add
两个命令处理逻辑当运行
da init
时,会去bin
文件夹下面找da-init.js
文件(也可以在bin
中增加da-init
键指定执行文件)当运行
da add
时,会去bin
文件夹下面找da-add.js
文件自己注册模式:(自己编的模式)
// package.json文件 { ... "bin": { "da": "./bin/index.js" } ... }
./bin/index.js
文件中会写da init
和da add
两个命令处理逻辑本项目采用自己注册模式
-
package.json
内容{ "name": "da-cli", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { "da": "./bin/index.js" }, "keywords": [], "author": "", "license": "ISC" }
5. 新建文件夹da-cli/bin
和da-cli/lib
,新建文件da-cli/bin/index.js
6. 编写da-cli/bin/index.js
,首先处理兼容问题
#!/usr/bin/env node
惊叹、有病,环境是node
方便记忆
7.实现da -version
,编写da-cli/bin/index.js
#!/usr/bin/env node
const program = require('commander')
program.name('da').version(require('../package').version, '-v -version', 'output the current version').usage('<command> [options]')
program.parse(process.argv)
-
program.parse(process.argv)
必须有这个,一直放到最后一行用于解析命令行参数 -
<>
包裹的参数一般是必传的 -
.name('da')
可以去除 -
此时可以使用以下命令(执行命令前,根项目下运行
npm link
,这样可以全局使用)da -v da -version da -h da -help
8. 实现da update
新建编写da-cli/lib/update/update.js
const updateNotifier = require('update-notifier')
const chalk = require('chalk')
const pkg = require('../../package.json')
const notifier = updateNotifier({
pkg,
updateCheckInterval: 1000,
})
function updateVer() {
if (notifier.update) {
console.log(`New version ${chalk.green(notifier.update.latest)} found, please update`)
notifier.notify()
} else {
console.log('There is no updatable version')
}
}
module.exports = updateVer
编写da-cli/bin/index.js
#!/usr/bin/env node
const program = require('commander')
program.name('da').version(require('../package').version, '-v -version', 'output the current version').usage('<command> [options]')
// update
program
.command('update')
.alias('u')
.description('update version')
.action(() => {
updateVer()
})
program.parse(process.argv)
9. 增加配置文件,方便下面命令实现
新增文件夹da-cli/config
增加da-cli/config/codebase.json
// 存放模板仓库
{
"loading": {
"tplOwner": "lydxwj", // 项目仓库所有者
"tplName": "vue-loading", // 项目仓库名
"site": "gitee" // 项目仓库地址类型
}
}
增加da-cli/config/config.json
{
"sites": { // 全部项目仓库地址类型,支持用户自定义扩展
"github": {
"name": "github",
"value": "github",
"short": "github",
"url": "github:{tplOwner}/{tplName}"
},
"gitlab": {
"name": "gitlab",
"value": "gitlab",
"short": "gitlab",
"url": "gitlab:{tplOwner}/{tplName}"
},
"gitee": {
"name": "gitee",
"value": "gitee",
"short": "gitee",
"url": "direct:https://gitee.com/{tplOwner}/{tplName}"
}
},
"frames": { // 创建项目框架选项,以及对应url,不支持用户自定义扩展
"vue": {
"url": "direct:https://gitee.com/lydxwj/vue-create"
}
}
}
10. 实现da add
(增加模板)
新增文件夹da-cli/lib/template
增加da-cli/lib/template/add.js
const inquirer = require('inquirer')
const chalk = require('chalk')
const fs = require('fs')
// 现有模板,用于判断是否重复添加和新增后的数据拼接
const codeBase = require(`${__dirname}/../../config/codebase`)
// 用于生成模板URL地址类型选择
const sites = require(`${__dirname}/../../config/config`).sites
// 生成模板URL地址类型
const sitesChoices = [];
Object.keys(sites).forEach(item => {
sitesChoices.push(sites[item]);
})
function add() {
// 交互式命令
let question = [
{
name: "name",
type: 'input',
message: "请输入模板名称(英文字母)",
validate (val) {
if (val === '') {
return 'Name is required!'
} else if (!/^[a-zA-Z]+$/.test(val)) {
return 'It can only contain English letters!'
} else if (codeBase[val]) {
return 'Template has already existed!'
} else {
return true
}
}
},
{
type: 'list',
message: '请选择模板仓库地址类型:',
name: 'site',
choices: sitesChoices,
},
{
name: "tplOwner",
type: 'input',
message: "请输入仓库所属",
validate (val) {
if (val === '') return 'The tplOwner is required!'
return true
}
},
{
name: "tplName",
type: 'input',
message: "请输入仓库名",
validate (val) {
if (val === '') return 'The tplName is required!'
return true
}
}
]
inquirer
.prompt(question).then(answers => {
let { name, tplOwner, tplName, site } = answers;
// 拼接增加新的模板
codeBase[name] = {
tplOwner,
tplName,
site
};
// 写文件,更新模板
fs.writeFile(`${__dirname}/../../config/codebase.json`, JSON.stringify(codeBase, null, 2), 'utf-8', err => {
if (err) {
console.log(chalk.red('Error\n'))
console.log(err)
return
}
console.log('\n')
console.log(chalk.green('Added successfully!\n'))
console.log(chalk.grey('The latest template list is: \n'))
// 更新成功并打印最新的模板列表
Object.keys(codeBase).forEach(item => {
console.log(chalk.green(' ' + item + ': \n'));
console.log(' 仓库地址类型:' + sites[codeBase[item].site].name + ' \n');
console.log(' 仓库所属:' + codeBase[item].tplOwner + ' \n');
console.log(' 仓库名:' + codeBase[item].tplName + ' \n');
})
console.log('\n')
})
})
}
module.exports = add
编写da-cli/bin/index.js
...
const tplAdd = require('../lib/template/add')
...
// add a new template
program
.command('add')
.description('add a new template')
.alias('a')
.action(() => {
tplAdd()
})
...
11. 实现da delete
(删除模板)
本命令的实现和上一个十分类似,只是把变量增加键值对变成删除键
增加da-cli/lib/template/delete.js
const inquirer = require('inquirer')
const chalk = require('chalk')
const fs = require('fs')
const codeBase = require(`${__dirname}/../../config/codebase`)
const sites = require(`${__dirname}/../../config/config`).sites
function tplDelete() {
let question = [{
name: "name",
message: "请输入要删除的模板名称",
validate(val) {
if (val === '') {
return 'Name is required!'
} else if (!codeBase[val]) {
return 'Template does not exist!'
} else {
return true
}
}
}]
inquirer
.prompt(question).then(answers => {
let {
name
} = answers;
delete codeBase[name]
fs.writeFile(`${__dirname}/../../config/codebase.json`, JSON.stringify(codeBase, null, 2), 'utf-8', err => {
if (err) {
console.log(chalk.red('Error\n'))
console.log(err)
return
}
console.log('\n')
console.log(chalk.green('Deleted successfully!\n'))
console.log(chalk.grey('The latest template list is: \n'))
Object.keys(codeBase).forEach(item => {
console.log(chalk.green(' ' + item + ': \n'));
console.log(' 仓库地址类型:' + sites[codeBase[item].site].name + ' \n');
console.log(' 仓库所属:' + codeBase[item].tplOwner + ' \n');
console.log(' 仓库名:' + codeBase[item].tplName + ' \n');
})
console.log('\n')
})
})
}
module.exports = tplDelete
编写da-cli/bin/index.js
...
const tplDelete = require('../lib/template/delete')
...
// delete a template
program
.command('delete')
.description('delete a template')
.alias('d')
.action(() => {
tplDelete()
})
...
12. 实现da list
(模板列表)
此功能在增加和删除命令中已经实现
增加da-cli/lib/template/list.js
const chalk = require('chalk')
const codeBase = require(`${__dirname}/../../config/codebase`)
const sites = require(`${__dirname}/../../config/config`).sites
function tplList() {
Object.keys(codeBase).forEach(item => {
console.log(chalk.green(' ' + item + ': \n'));
console.log(' 仓库地址类型:' + sites[codeBase[item].site].name + ' \n');
console.log(' 仓库所属:' + codeBase[item].tplOwner + ' \n');
console.log(' 仓库名:' + codeBase[item].tplName + ' \n');
})
}
module.exports = tplList
编写da-cli/bin/index.js
...
const tplList = require('../lib/template/list')
...
// list all the templates
program
.command('list')
.description('list all the templates')
.alias('l')
.action(() => {
tplList()
})
...
13. 实现da init
(初始化项目)
有了模板,现在就是用起来
增加da-cli/lib/template/init.js
const program = require('commander')
const chalk = require('chalk')
const ora = require('ora')
const download = require('download-git-repo')
const inquirer = require('inquirer')
const path = require('path')
const fs = require('fs')
const codeBase = require(`${__dirname}/../../config/codebase`)
const sites = require(`${__dirname}/../../config/config`).sites
const promptList = [
{
type: 'input',
message: '请输入项目简介',
name: 'description',
default: '项目简介'
},
{
type: 'input',
message: '请输入项目版本',
name: 'version',
default: '1.0.0'
}
]
// 读写文件,修改package的name,description,version
function setProjectDescription(projectName, options) {
const packagePath = `${projectName}/package.json`
const packageJson = fs.readFileSync(packagePath, 'utf-8')
const packageResult = JSON.stringify(Object.assign({}, JSON.parse(packageJson), options), null, 2)
fs.writeFileSync(packagePath, packageResult)
}
function tplInit(templateName, projectName) {
// 判断模板是否存在
if (!codeBase[templateName]) {
console.log(chalk.red('\n Template name does not exit! \n '))
return;
}
if (!projectName) {
console.log(chalk.red('\n Project name should not be empty! \n '))
return program.help();
}
if (program.args.length < 2) return program.help()
// 判断是否已经存在这个文件夹,存在则报错(不判断会造成仓库代码下载不成功)
try {
const stat = fs.statSync(path.resolve('./', projectName));
if (stat.isDirectory()) {
console.log(chalk.red('\n The folder already exists! Please change the project name! \n '))
return;
};
} catch (err) {}
const tplObj = codeBase[templateName];
if (!sites[tplObj.site]) {
console.log(chalk.red('\n Code base address type does not exist! \n '))
return;
}
// 拼接仓库地址
let url = sites[tplObj.site].url;
url = url.replace('{tplOwner}', tplObj.tplOwner);
url = url.replace('{tplName}', tplObj.tplName);
const spinner = ora("Downloading...");
inquirer.prompt(
promptList
).then(answers => {
console.log(chalk.white('\n Start generating... \n'))
spinner.start()
// 下载仓库代码
download(
url,
projectName,
{
clone: url.startsWith('direct:') ? true : false,
},
err => {
if (err) {
spinner.fail()
console.log(chalk.red(`Generation failed. ${err}`))
return
}
const { description, version } = answers
setProjectDescription(projectName, { name: projectName, description, version })
spinner.succeed()
console.log(chalk.cyan('\n Generation completed!'))
console.log(chalk.cyan('\n To get started'))
console.log(chalk.cyan(`\n cd ${projectName}`))
console.log(chalk.cyan(`\n npm install \n`))
}
)
});
}
module.exports = tplInit
编写da-cli/bin/index.js
...
const tplInit = require('../lib/template/init')
...
// generate a new project from a template
program
.command('init')
.arguments('<templateName> <projectName>')
.description('generate a new project from a template', {
templateName: 'existing template name',
projectName: 'folder that does not exist'
})
.alias('i')
.action((templateName, projectName) => {
tplInit(templateName, projectName)
})
...
14. 实现da site list
(URL地址类型列表),da site add
(URL地址类型增加),da site delete
(URL地址类型删除)
因为上面已经实现了类似的,这里就一次搞定三个
新建文件夹da-cli/lib/site
增加da-cli/lib/site/list.js
const chalk = require('chalk')
const sites = require(`${__dirname}/../../config/config`).sites
function siteList() {
Object.keys(sites).forEach(item => {
console.log(chalk.green(' ' + item + ': \n'));
console.log(' 仓库地址类型名:' + sites[item].name + ' \n');
console.log(' 仓库地址URL:' + sites[item].url + ' \n');
})
console.log('\n')
}
module.exports = siteList
增加da-cli/lib/site/add.js
const chalk = require('chalk')
const inquirer = require('inquirer')
const fs = require('fs')
const configJson = require(`${__dirname}/../../config/config`)
const sites = configJson.sites
function siteAdd() {
let question = [
{
name: "siteName",
type: 'input',
message: "请输入仓库地址类型名",
validate (val) {
if (val === '') {
return 'siteName is required!'
} else {
return true
}
}
},
{
name: "siteKey",
type: 'input',
message: "请输入存储仓库地址类型的键(key)",
validate (val) {
if (val === '') return 'siteKey is required!'
if (sites[val]) return 'siteKey has already existed!'
if (!/^[a-zA-Z]+$/.test(val)) {
return 'It can only contain English letters!'
}
return true
}
},
{
name: "siteUrl",
type: 'input',
message: "请输入仓库地址URL",
suffix: '例如:https://my.sitecode.com',
validate (val) {
if (val === '') return 'The siteUrl is required!'
if (!/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(val)) return 'Please enter the correct URL!'
return true
}
}
]
inquirer.prompt(question).then(answers => {
let { siteName, siteKey, siteUrl } = answers;
const newSite = {
name: siteName,
value: siteKey,
short: siteName,
url: 'direct:' + siteUrl + '/{tplOwner}/{tplName}.git'
}
sites[siteKey] = newSite
fs.writeFile(`${__dirname}/../../config/config.json`, JSON.stringify({ ...configJson, sites }, null, 2), 'utf-8', err => {
if (err) {
console.log(chalk.red('Error\n'))
console.log(err)
return
}
console.log('\n')
console.log(chalk.green('Added successfully!\n'))
console.log(chalk.grey('The latest sites list is: \n'))
Object.keys(sites).forEach(item => {
console.log(chalk.green(' ' + item + ': \n'));
console.log(' 仓库地址类型名:' + sites[item].name + ' \n');
console.log(' 仓库地址URL:' + sites[item].url + ' \n');
})
console.log('\n')
})
})
}
module.exports = siteAdd
增加da-cli/lib/site/delete.js
const chalk = require('chalk')
const inquirer = require('inquirer')
const fs = require('fs')
const configJson = require(`${__dirname}/../../config/config`)
const sites = configJson.sites
function siteDelete() {
let question = [
{
name: "siteKey",
type: 'input',
message: "请输入要删除的存储仓库地址类型键(key)",
validate (val) {
if (val === '') {
return 'siteKey is required!'
} else if (!sites[val]) {
return 'siteKey does not exist!'
} else if (val == 'github' || val == 'gitlab' || val == 'gitee') {
return 'no permission!'
}
return true
}
},
]
inquirer.prompt(question).then(answers => {
let { siteKey } = answers;
delete sites[siteKey]
fs.writeFile(`${__dirname}/../../config/config.json`, JSON.stringify({ ...configJson, sites }, null, 2), 'utf-8', err => {
if (err) {
console.log(chalk.red('Error\n'))
console.log(err)
return
}
console.log('\n')
console.log(chalk.green('Deleted successfully!\n'))
console.log(chalk.grey('The latest sites list is: \n'))
Object.keys(sites).forEach(item => {
console.log(chalk.green(' ' + item + ': \n'));
console.log(' 仓库地址类型名:' + sites[item].name + ' \n');
console.log(' 仓库地址URL:' + sites[item].url + ' \n');
})
console.log('\n')
})
})
}
module.exports = siteDelete
编写da-cli/bin/index.js
...
const siteList = require('../lib/site/list')
const siteAdd = require('../lib/site/add')
const siteDelete = require('../lib/site/delete')
...
// 由于是两个指令参数,所以要配合addCommand
// configure site
function makeSiteCommand() {
const site = new program.Command('site');
// list all the sites
site
.alias('s')
.usage('<command>')
.command('list')
.alias('l')
.description('list all the sites')
.action(() => {
siteList()
});
// add a new site
site
.command('add')
.alias('a')
.description('add a new site')
.action(() => {
siteAdd()
});
// delete a site
site
.command('delete')
.alias('d')
.description('delete a site')
.action(() => {
siteDelete()
});
return site;
}
program.addCommand(makeSiteCommand())
...
15. 实现da create
这是最后一个命令,是定制化的生成项目,需要自己建一个用于生成定制的项目。本项目使用的是https://gitee.com/lydxwj/vue-create
,没有什么特别的只是需要修改的文件含有模板引擎的代码,所以本命令使用了模板引擎插件,并且使用了figlet 炫酷文字。
新建文件夹da-cli/lib/create
增加da-cli/lib/create/create.js
const program = require('commander')
const chalk = require('chalk')
const figlet = require('figlet')
const inquirer = require('inquirer')
const ora = require('ora')
const download = require('download-git-repo')
const handlebars = require('handlebars')
const fs = require('fs')
const path = require('path')
const frames = require(`${__dirname}/../../config/config`).frames
// 生成框架选择数组
const framesChoices = [];
Object.keys(frames).forEach(item => {
framesChoices.push(item);
})
const questions = [
{
type: 'list',
message: '请选择框架:',
name: 'frame',
choices: framesChoices,
},
{
type: 'input',
message: '请输入项目简介',
name: 'description',
default: '项目简介'
},
{
type: 'input',
message: '请输入项目版本',
name: 'version',
default: '1.0.0'
}
]
// 修改文件
function renderFiles(targetPath, globalData) {
// 需要修改的文件列表
const fileList = [
`${targetPath}/package.json`,
`${targetPath}/README.md`,
]
return new Promise(async (resolve, reject) => {
for (let i = 0; i < fileList.length; i++) {
try {
const fileContent = await fs.readFileSync(fileList[i], 'utf8')
// 使用模板引擎编译文件
const fileRendered = await handlebars.compile(fileContent)(globalData)
await fs.writeFileSync(fileList[i], fileRendered)
resolve()
} catch (err) {
chalk.red('\n craete project failed. \n')
chalk.red(`\n ${err} \n`)
reject(err)
}
}
})
}
function createPro(projectName) {
if (!projectName) {
console.log(chalk.red('\n Project name should not be empty! \n '))
return program.help()
}
if (program.args.length < 1) return program.help()
try {
const stat = fs.statSync(path.resolve('./', projectName));
if (stat.isDirectory()) {
console.log(chalk.red('\n The folder already exists! Please change the project name! \n '))
return;
};
} catch (err) {}
// 生成炫酷文字
const text = figlet.textSync('da cli', {
font: 'isometric1',
horizontalLayout: 'default',
verticalLayout: 'default',
width: 80,
whitespaceBreak: true
})
console.log(chalk.green(text));
const spinner = ora("Generating...");
inquirer.prompt(questions).then(answers => {
console.log(chalk.white('\n Start generating... \n'))
spinner.start()
let { frame, description, version } = answers;
const url = frames[frame].url;
download(
url,
projectName,
{
clone: url.startsWith('direct:') ? true : false,
},
err => {
if (err) {
spinner.fail()
console.log(chalk.red(`Generation failed. ${err}`))
return
}
renderFiles(projectName, { projectName, description, version }).then(() => {
spinner.succeed()
console.log(chalk.cyan('\n Generation completed!'))
console.log(chalk.cyan('\n To get started'))
console.log(chalk.cyan(`\n cd ${projectName}`))
console.log(chalk.cyan(`\n npm install \n`))
}).catch(() => {
process.exit()
})
}
)
})
}
module.exports = createPro
编写da-cli/bin/index.js
...
const createPro = require('../lib/create/create')
...
// generate an official project
program
.command('create')
.arguments('<projectName>')
.description('generate an official project', {
projectName: 'folder that does not exist'
})
.alias('c')
.action((projectName) => {
createPro(projectName)
})
...
16. 最后增加帮助附加
编写da-cli/bin/index.js
...
program.addHelpText('after', `
Example call:
$ da --help
$ da -version
$ da update
$ da add
$ da delete
$ da list
$ da create
$ da site add
$ da site delete
$ da site list`);
...
完结
以上是整个项目的实现步骤,代码中有少量注释,没有具体详细讲解,如果有什么不懂得欢迎关注公众号询问。