在第一篇文章【手把手撸脚手架 —— 初识脚手架】中,我们已经搭建了一个简易的脚手架框架,现在我们就实现通过脚手架来下载模板创建一个项目,并把这个脚手架发布到npm。
commander
在之前我们用的是yargs,今天我们换commander来试试:
yarn add commander
复制代码
const commander = require('commander');
const pkg = require("../package.json");
const program = new commander.Command();
program
.version(pkg.version)
.parse(process.argv)
复制代码
至于yargs和commander的区别,大家可以自行google,对我而言就像是js和jquery,commander要简洁一点。
yargs
和commander
的区别不大,可以去查看官方文档。我在文末也附上一些基础用法,可自行选择查看。
inquirer
我们在使用vue-cli
的过程中,一定都见过这个询问界面,作为脚手架和用户通讯的操作方式,这就是用inquirer实现的。
yarn add inquirer
复制代码
const inquirer = require('inquirer');
inquirer
.prompt([
{type: 'input', message: 'your name', name:'yourName', default: 'finget',
validate: function(v) {
return v.startsWith('f')
}
},
{type: 'rawlist', message:'选择性别', name:'sex', choices: ['男','女'], default:'男'},
{type: 'checkbox', message:'选择技术栈', name:'skill', choices: [
{key:'vue', value:'vue'},
{key:'react', value:'react'},
{key:'javascript', value:'js'},
{key:'html', value:'h5'},
{key:'css', value:'css'},
]}
])
.then(answers => {
// Use user feedback for... whatever!!
console.log(answers);
})
.catch(error => {
console.log(error)
if(error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong
}
});
复制代码
inquirer.prompt
可以接收一个数组或者对象,每一个question主要有以下几个属性可用:
- type (input, number, confirm, list, rawlist, expand, checkbox, password, editor)
- name 变量名称
- message 提示信息
- validate [Function] 验证 return true 才能回车生效
- default 默认值
- transform [Function] 类似placehoder,只是展示
- filter [Function] 会改成最后输入的值
- choices [Array] 选项
更多的属性和具体使用demo可以查看官方文档。
我觉得这个inquirer在使用上还是比较简单的,大家可以把它想象成一个动态表单,
type
设置表单类型,name
就是表单绑定的属性,choices
就是下拉选项,message
就是placeholder,validate
就是表单验证…诸如此类!
download-git-repo下载模板
- 新建一个
bin/command-create.js
文件 - 用
inquirer
实现一个create
命令的询问操作 - 用
semver
检验版本号是否合法(这个插件用起来很简单,主要功能就是对版本后的校验比对,可以看一下官方文档)
// bin/command-create.js
const inquirer = require('inquirer');
const semver = require('semver');
const download = require('download-git-repo');
const createCommand = async function(projectName, destination, cmdObj) {
console.log(projectName, destination)
const object = await inquirer
.prompt([{
type: 'input',
name: 'projectName',
message: '请输入项目名称',
default: projectName,
validate: function(v) {
// 规则一:输入的首字符为英文字符
// 规则二:尾字符必须为英文或数字
// 规则三:字符仅允许-和_两种
// Declare function as asynchronous, and save the done callback
const done = this.async();
// Do async stuff
setTimeout(function() {
if (!/^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)) {
done(`请输入合法名称:
规则一:输入的首字符为英文字符
规则二:尾字符必须为英文或数字
规则三:字符仅允许-和_两种
`);
return;
}
done(null, true);
}, 0);
},
filter: (v) => {
return v
}
},
{ type: 'input', message: '请输入作者名称', name: 'authorName' },
{ type: 'input', message: '请输入版本号', name: 'version',
default:'1.0.0',
validate: function(v) {
const done = this.async();
// Do async stuff
setTimeout(function() {
if (!!!semver.valid(v)) {
done(`请输入合法版本号`);
return;
}
done(null, true);
}, 0);
},
filter: (v) => {
if(semver.valid(v)) {
return semver.valid(v)
} else {
return v
}
}
},
])
console.log(object)
}
module.exports = createCommand;
复制代码
这里我们简单获取三个值:projectName
,version
,authorName
.
// bin/index.js
const createCommand = require('./command-create.js')
// command 注册命令
const create = program.command('create [projectName]'); // <必填> [可选]
create
.description('create a project')
.action((projectName, destination, cmdObj) => {
createCommand(projectName, destination, cmdObj)
})
复制代码
在拿到这些信息后我们就可以下载模板了,我们这里使用的是vue3+elementPlus+ts写的一个后台系统模板github.com/FinGet/vue3…
const createCommand = async function(projectName, destination, cmdObj) {
// console.log(projectName, destination)
const projectInfo = await inquirer
.prompt([{
...
....
downloadTemplate(projectInfo);
}
function downloadTemplate(projectInfo) {
if(projectInfo.projectName) {
download('github:FinGet/vue3_elementPlus_ts',projectInfo.projectName, function (err) {
if(!err) {
console.log('下载完成!');
} else {
console.log('下载失败!');
}
})
}
}
复制代码
到这里这个脚手架基本就算完成了,可以下载模板了。接下来做点优化!
写入projectInfo
我们用inquirer
拿到了用户自定义的项目信息,这时我们就该把对应信息写入package.json
中。
- 用
fs.readFile
读取package.json
- 修改对应字段
fs.writeFile
重写package.json
function editFile({ version, projectName, authorName }) {
console.log(version, projectName, authorName)
return new Promise((resolve, reject) => {
// 读取文件
fs.readFile(`${process.cwd()}/${projectName}/package.json`, (err, data) => {
if (err) throw err;
// 获取json数据并修改项目名称和版本号
let _data = JSON.parse(data.toString())
_data.name = projectName;
_data.version = version;
_data.author = authorName;
let str = JSON.stringify(_data, null, 4);
// 写入文件
fs.writeFile(`${process.cwd()}/${projectName}/package.json`, str, function (err) {
if (err) throw err;
})
resolve()
});
})
};
复制代码
ora做一点小动画
ora都是一些简单的api调用,就可以实现动画效果,可自行查看www.npmjs.com/package/ora
yarn add ora
复制代码
const ora = require('ora');
function downloadTemplate(projectInfo) {
const spinner = ora('正在下载模板...').start();
const {projectName, version,authorName} = projectInfo;
if(projectName) {
download('github:FinGet/vue3_elementPlus_ts',projectName, function (err) {
if(!err) {
editFile({version, projectName, authorName }).then(() => {
spinner.succeed('下载完成');
});
} else {
spinner.fail(`下载失败!`);
}
})
}
}
复制代码
发布脚手架
发布脚手架比较简单,只需要执行npm publish
就好了。
注意事项:
- 没有npm账号的话需要去官网www.npmjs.com/ 注册一个
- 第一次发布插件,需要登录npm login
- 插件的名称就是package.json中的那么,如果发布失败,可能是名称冲突了。
发布成功之后就可以用npm安装然后使用了!
附录:commander基础用法
name,usage,version,option,parse
option('-n, --name <items1> [items2]', 'name description', 'default value')
// 手动实例化一个commander
const program = new commander.Command();
program
.name(Object.keys(pkg.bin)[0])
.usage("<command> [options]")
.version(pkg.version)
.option('-d, --debug', '是否开启调试模式', false)
.option('-e, --env <envName>', '获取环境变量名称')
.parse(process.argv)
const options = program.opts();
console.log(options.debug);
复制代码
commander
commander命令注册有两种方式:
- Command API 注册命令
- addCommand API 注册命令
command API 注册命令
// command 注册命令
const clone = program.command('clone <source> [destination]'); // <必填> [可选]
clone
.description('clone a repository')
.option('-f, --force', '是否强制克隆')
.action((source, destination, cmdObj) => {
console.log(source, destination, cmdObj)
})
program.parse(process.argv)
复制代码
addCommand API 注册命令
// addCommand 注册子命令
const service = new commander.Command('service');
service
.command('start [port]')
.action((port) => {
console.log(port)
})
service
.command('stop [port]')
.action((port) => {
console.log(port)
})
program.addCommand(service);
复制代码
这里执行的是test-cli service start 8080
它相当于是 又在my-cli下生成了一个新的service子命令,有分组的那种感觉,可以把同一类命令,放在一个子命令下。但是多个命令不能链式的注册!
监听未知命令
program.on('command:*', function(obj) {
console.log(obj);
console.error('未知的命令:' + obj[0])
const availableCommands = program.commands.map( cmd => cmd.name())
console.log('可用命令:'+ availableCommands.join(','))
})
复制代码
最后
本文只是实现了一个小小的模板下载工具,还有很多地方可以优化,例如:
- 判断当下载目录是否已存在同名文件
- 实现强制下载
- debug 模式
- 像vue-cli一样安装其他的插件
- 怎么让项目自动安装依赖并启动
放上仓库地址gitee.com/finget/test…