一. 为什么
我们在开发过程中,特别是在业务代码的开发时,代码结构一般都是比较固定的,所以开发新的业务节点,都会去拷贝之前写的节点,改吧改吧就行了。
或者我们需要写一个自己的项目,需要新建一个工程,一般也会拷贝之前写过的项目,这些项目代码结果和配置自己比较熟悉,用起来比较顺手。
在这个过程中出现了很多问题
- 需要手动去拷贝,比较麻烦
- 拷过去的带有原来的代码,清除起来比较麻烦
- 换了电脑之前的项目都没了
所以需要写一个类似create-react-app的工具,可以直接下载一个初始化的代码块或者项目。
二. 创建项目
1. 初始化npm项目
2. 在package.json中添加bin命令
"bin": {
"peach": "./bin/peach.js"
},
3. 创建bin/peach.js文件
在文件头添加下面的代码,这里一定要添加,声明在node环境执行,否则后面的执行会出错。
#!/usr/bin/env node --harmony
4. 安装commander
npm install commander
5. 先写一个最简单的查看版本的代码
//bin/peach.js
#!/usr/bin/env node --harmony
var program = require('commander');
program
.version(require('../package.json').version)
.parse(process.argv)
其中version默认参数是 --version和-V,也可以自定义参数比如
.version(require('../package.json').version, '-v, -V, --version')
6. 本地测试
通过npm link将命令link到全局,就可以通过peach命令执行了
npm link
测试发现报错了,执行了ps1命令,我们希望是运行node命令
根据提示查了一下,需要以管理员身份运行以下指令,并且选择A。
set-executionpolicy remotesigned
D:\test\peach-cli> peach -v
1.0.0
没有问题,下来直接修改文件就可以,不用每次都link,如果要去掉link,只需要执行unlink命令即可。
npm unlink
7. 添加初始化命令
#!/usr/bin/env node --harmony
var program = require('commander');
program
.version(require('../package.json').version, '-v, -V, --version')
program
.command('init')
.description('初始化')
.alias('i')
.action(() => {
require('../lib/init')()
})
program.parse(process.argv)
其中command为命令名称,alias为别名,action是执行脚本
创建../lib/init.js文件
module.exports = () => {
console.log('init');
}
尝试执行peach init或者peach i
D:\test\peach-cli> peach i
init
8. 拷贝模板到当前目录
在init中编写拷贝模板的代码,暂时先写死所有参数,创建一个测试的模板
const fs = require('fs')
const path = require('path')
module.exports = () => {
copyTempl();
/**
* 复制模板文件
*/
function copyTempl() {
let desPath = path.join(__dirname, '../template/react-temp')
copyDir(desPath, 'peach-test')
console.log('执行完毕');
}
/**
* 复制
* @param src
* @param dist
* @param callback
*/
function copyDir(src, dist, callback) {
fs.access(dist, function (err) {
if (err) {
// 目录不存在时创建目录
fs.mkdirSync(dist, {
recursive: true
});
}
_copy(null, src, dist);
});
const _copy = (err, src, dist) => {
if (err) {
callback(err);
} else {
let dir = fs.readdirSync(src, 'utf-8');
for (let j of dir) {
var _src = src + '/' + j;
var _dist = dist + '/' + j;
let stat = fs.statSync(_src);
if (stat.isDirectory()) {
copyDir(_src, _dist, callback);
} else {
fs.writeFileSync(_dist, fs.readFileSync(_src, {
encoding: 'utf-8'
}));
}
}
}
}
}
}
9. 随便找个目录测试一下
D:\test> peach i
执行完毕
模板拷贝成功。
10. 用户交互
现在我们的中项目名和可选模板还都是写死的,需要让用户可以自己选择,这个时候需要inquirer来进行交互。
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const tempList = ['react-temp', 'vue-temp']
module.exports = () => {
//inquirer.prompt返回的是一个promise对象
createQuestion().then(res => {
const {filename, templName} = res;
copyTempl(filename, templName)
})
function createQuestion() {
const questions = [{
type: 'input',
name: 'filename',
message: '请输入要创建的文件名:',
validate: function (value) {
var pass = value.match(/\w+/);
if (pass) {
return true;
}
return '请输入正确的文件名称(数字文字下划线)';
}
},
{
type: 'rawlist',
name: 'templName',
message: '请选择模板?',
choices: tempList
}
];
return inquirer.prompt(questions)
}
//copyTempl();
/**
* 复制模板文件
*/
function copyTempl(filename, tempName) {
let desPath = path.join(__dirname, '../template/'+tempName)
copyDir(desPath, filename)
console.log('执行完毕');
}
/**
* 复制
* @param src
* @param dist
* @param callback
*/
function copyDir(src, dist, callback) {
fs.access(dist, function (err) {
if (err) {
// 目录不存在时创建目录
fs.mkdirSync(dist, {
recursive: true
});
}
_copy(null, src, dist);
});
const _copy = (err, src, dist) => {
if (err) {
callback(err);
} else {
let dir = fs.readdirSync(src, 'utf-8');
for (let j of dir) {
var _src = src + '/' + j;
var _dist = dist + '/' + j;
let stat = fs.statSync(_src);
if (stat.isDirectory()) {
copyDir(_src, _dist, callback);
} else {
fs.writeFileSync(_dist, fs.readFileSync(_src, {
encoding: 'utf-8'
}));
}
}
}
}
}
}
测试一下,没有问题。
11. 优化一下代码结构
将具体的执行方法放在utils里面,init中只调用执行函数,将支持的模板也放在配置文件中。
// ./utils.js
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const tempList = require('./tempList.json');
module.exports = {
createQuestion() {
const questions = [{
type: 'input',
name: 'filename',
message: '请输入要创建的文件名:',
validate: function (value) {
var pass = value.match(/\w+/);
if (pass) {
return true;
}
return '请输入正确的文件名称(数字文字下划线)';
}
},
{
type: 'rawlist',
name: 'templName',
message: '请选择模板?',
choices: tempList
}
];
return inquirer.prompt(questions)
},
/**
* 复制模板文件
*/
copyTempl(filename, tempName) {
let desPath = path.join(__dirname, '../template/' + tempName)
this.copyDir(desPath, filename)
console.log('执行完毕');
},
/**
* 复制
* @param src
* @param dist
* @param callback
*/
copyDir(src, dist, callback) {
fs.access(dist, function (err) {
if (err) {
// 目录不存在时创建目录
fs.mkdirSync(dist, {
recursive: true
});
}
_copy(null, src, dist);
});
const _copy = (err, src, dist) => {
if (err) {
callback(err);
} else {
let dir = fs.readdirSync(src, 'utf-8');
for (let j of dir) {
var _src = src + '/' + j;
var _dist = dist + '/' + j;
let stat = fs.statSync(_src);
if (stat.isDirectory()) {
this.copyDir(_src, _dist, callback);
} else {
fs.writeFileSync(_dist, fs.readFileSync(_src, {
encoding: 'utf-8'
}));
}
}
}
}
}
}
// init.js
const Utils = require('./utils');
module.exports = () => {
Utils.createQuestion().then(res => {
const {
filename,
templName
} = res;
Utils.copyTempl(filename, templName)
})
}
// tempList.json
[{ "name": "react", "value": "react-temp" }, { "name": "vue", "value": "vue-temp" }]
测试一下也没有问题。
12. 添加list指令,
用户可以查看所有支持的列表。在bin/peach.js中添加
创建lib/list.js文件
const list = require('./tempList.json')
module.exports = () => {
console.log(
`现支持模板列表:\n${list.map(item => item.value).join('\n')}`
)
}
测试没有问题
13. 添加help指令
当用户输入--help或者什么都不熟的时候,提示用户使用方法。
// bin/peach.js
if (!program.args.length) {
program.help()
}
测试一下, commander会自动根据你的配置,生成出使用说明。
14. 发布
现在基本框架完成了,我们上传npm试试。
1 切换到npm下,不能用淘宝镜像。
2 npm login登录
3 npm publish
上传的时候报错,说是版本问题,增加的版本还是报同样的错,去www.npmjs.com/发现是peach-cli这个包已经被人占用了,只能改名字为gbg-peach-cli再上传。
上传成功到官网去查一下,能查到,说明没有问题。
15. 安装测试
npm unlink当前项目,获取到本地npm下面删除相关的命令脚本和文件。全局安装cli
npm i gbg-peach-cli -g
这样一个最简单的模板下载工具就完成了。
三. 从git下载
上述完成的只能下载预设的模板,如果需要更改模板什么的,就需要重新发布,所以改为从Git clone项目。
1. 添加git地址
修改tempList.json,将原来的下载模板改为git地址。
[{
"name": "react",
"value": "git@github.com:guobaogang/wechat-yatzy-server.git"
},
{
"name": "vue",
"value": "git@github.com:guobaogang/vue-js-tmp.git"
}
]
2. git clone方法
在utils.js中增加克隆git项目的方法,child_process为nodejs内置方法,不用安装。
// lib/utils.js
const exec = require('child_process').exec
/**
* 克隆git项目
*/
gitClone(filename, gitUrl, branch = 'master') {
let cmdStr = `git clone ${gitUrl} ${filename} && cd ${filename} && git checkout ${branch}`;
exec(cmdStr, (error, stdout, stderr) => {
if (error) {
console.log(error)
process.exit();
}
console.log('克隆完成');
})
},
init.js中修改调用方法
// lib/init.js
const Utils = require('./utils');
module.exports = () => {
Utils.createQuestion().then(res => {
const {
filename,
templName
} = res;
Utils.gitClone(filename, templName)
})
}
测试一下,可以下载成功
D:\test> peach i
? 请输入要创建的文件名: peach-test2
? 请选择模板? react
克隆完成
四. 一些细节处理
1. 删除.git
克隆成功之后代码中有.git,这个是不需要的,得删除
安装rimraf插件,克隆成功后删除.git。
注意:虽然前面有cd到文件夹,但是当前运行环境并没有真的cd到该文件夹,所以路径并不是./.git
// lib/untis.js
const rimraf = require('rimraf')
gitClone(filename, gitUrl, branch = 'master') {
let cmdStr = `git clone ${gitUrl} ${filename} &&
cd ${filename} && git checkout ${branch}`;
exec(cmdStr, (error, stdout, stderr) => {
if (error) {
console.log(error)
process.exit();
}
rimraf.sync(`./${filename}/.git`)
//注意:虽然前面有cd到文件夹,但是当前运行环境并没有真的cd到该文件夹,所以路径并不是./.git
console.log('克隆完成');
})
}
2. 运行提示
由于网络或者Git仓库过大等原因,clone的时候时间会比较长,需要有一个正在运行的提示。
// lib/utils.js
const CLI = require('clui'),
Spinner = CLI.Spinner;
gitClone(filename, gitUrl, branch = 'master') {
let cmdStr = `git clone ${gitUrl} ${filename} && cd ${filename} && git checkout ${branch}`;
let countdown = new Spinner('努力加载中,请稍后... ', ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']);
countdown.start();
exec(cmdStr, (error, stdout, stderr) => {
if (error) {
console.log(error)
process.exit();
}
rimraf.sync(`./${filename}/.git`)
console.log('克隆完成');
countdown.stop()
})
}
测试一下,效果可以。
3. 美化输出
提示文字有点难看,可以使用chalk和figlet插件优化一下
比如讲克隆完成的文字改为红色。
// lib/utils.js
const chalk = require('chalk');
console.log(chalk.red('\n克隆完成'));
比如在使用init命令的时候,输出一个peach-cli的大写文字
//lib/init.js
const Utils = require('./utils');
const chalk = require('chalk');
const figlet = require('figlet');
module.exports = () => {
console.log(
chalk.green(
figlet.textSync('PEACH-CLI', { horizontalLayout: 'full' })
)
)
Utils.createQuestion().then(res => {
const {
filename,
templName
} = res;
Utils.gitClone(filename, templName)
})
}
看一下效果
clui,chalk,figlet的用法还有很多,这块只是最简单的用法,大家可以尝试做出更多的效果。
4. 检查更新版本
有时候我们更新了版本,用户在使用的时候并不知道,为了保持版本最新状态,最好在使用的时候检查一下版本,如果有新版本,可以提示用户更新。
总的来说这个功能可能比较鸡肋,根据自己需求加吧。
这个功能需要用到很多插件。
- request: 获取工具信息
- co: 异步控制
- co-prompt: 等待用户输入
- semver: 版本检查和对比
好了,开始写代码,先写一个检查版本的方法。
//lib/checkVersion.js
const request = require('request')
const semver = require('semver')
const co = require('co')
const prompt = require('co-prompt')
const chalk = require('chalk')
const packageConfig = require('../package.json')
module.exports = done => {
request({
//由于网络等原因,此处改为https://registry.npm.taobao.org/gbg-peach-cli
//否则经常超时,查不到版本号
url: 'https://registry.npmjs.org/gbg-peach-cli',
//为了用户体验,这里时间不能太长
timeout: 1000
}, (err, res, body) => {
if (!err && res.statusCode === 200) {
const latestVersion = JSON.parse(body)['dist-tags'].latest
const localVersion = packageConfig.version
if (semver.lt(localVersion, latestVersion)) {
console.log()
console.log(chalk.yellow(' A newer version of peach-cli is available.'))
console.log()
console.log(' latest: ' + chalk.green(latestVersion))
console.log(' installed: ' + chalk.red(localVersion))
console.log()
co(function* () {
let update = yield prompt('Do you want to update the package ? [Y/N]')
if (update.toLowerCase() === 'y' || update.toLowerCase() === 'yes') {
console.log('有新版本');
} else if (update.toLowerCase() === 'n' || update.toLowerCase() === 'no') {
done()
}
})
} else {
done()
}
} else {
done()
}
})
}
在init中调用
// lib/init.js
const check = require('./checkVersion');
module.exports = () => {
check(() => {
...
})
}
测试一下
没有问题,下来再写一个自动更新的方法就可以了。
5. 自动更新
// lib/update.js
const exec = require('child_process').exec
const co = require('co')
const chalk = require('chalk')
const CLI = require('clui'),
Spinner = CLI.Spinner;
module.exports = () => {
return co(function* () {
let cmdStr = `npm install gbg-peach-cli -g`;
let countdown = new Spinner('更新中,请稍后... ', ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']);
countdown.start();
yield(function () {
return new Promise(function (resolve, reject) {
exec(cmdStr, (error, stdout, stderr) => {
if (error) {
console.log(error)
process.exit();
}
console.log(chalk.yellow('更新完成!'))
// 处理指定文件夹
countdown.stop();
resolve()
})
})
})()
})
}
在checkversion中调用
// lib/checkVersion.js
...
if (flag.toLowerCase() === 'y' || flag.toLowerCase() === 'yes') {
update().then(function(){
done()
})
}
...
测试一下,没有问题