无意中刷掘金看到几篇关于脚手架搭建的文章,并且之前已经有意向自己搭一个像vue-cli的脚手架:很好奇人家 怎么 vue init template 就可以像命令符一样一步一步下去,然后就可以下载到模板。
这里自己去试着去看了一下vue-cli v2版本的源码结合一些资料,简单写了一个用脚手架的形式拉取github的项目
安装
npm i -g g-xsk-cli
使用
初始化
gxc
// 按提示说明走
删除下载项目
gxc del <projectName>
// 或 gxc d <projectName>
接下来就来讲一下写成上面这个东西的步骤
前置知识
- 强大的交互式命令行工具:inquirer
- 编写指令和处理命令行的的工具:commander
- 修改控制台输出内容样式的工具:chalk
- 可执行shell命令的node模块: child_process
- 好看的加载的转圈圈效果:ora
- 网络请求库 : axios
建议先去创建文件去玩一下上面几个工具,挺有意思的!
在源码的example文件里也有我之前尝试过的,可以直接运行
package.json -> bin属性
命令定义属性
"bin": {
gxc: './bin/index.js'
}
并且 /bin/index.js,行首加入一行 #!/usr/bin/env node 指定当前脚本由node.js进行解析
#!/usr/bin/env node
// 开始写
这就是为什么vue-cli能直接跑 vue init template?
秘密就在这里
npm中,在package中设置bin,npm就会对vue创建软连接,路径为node_modules/.bin/vue,而应用
在运行时npm就会把node_modules/.bin加入系统变量,运行时就可以直接 vue init <template>
具体可以看这里
这里我们在调试时,是主动加入的,所以需要 npm lint软连接一波,这样就可以直接运行了
gxc
// 也可以用node ./bin/index.js执行调试
将脚手架上传npm
如何上传脚手架,很简单
1.npm login // 登录 npm账号
2.npm publish // 设置好版本,上传即可
注意 在下载依赖的时候
用 npm i -S inquirer commander chalk ora axios 创建,加入到
"dependencies": {
"axios": "^0.19.2",
"chalk": "^4.0.0",
"commander": "^5.1.0",
"inquirer": "^7.1.0",
"ora": "^4.0.4"
},
因为最后我们全局安装的时候:npm i -g g-sxk-cli 他会自动把dependencies这些依赖下载下来
注意 使用的时候,注意先解绑我们本地的软连接:npm unlink,然后再使用全局下载的 gxc
解析代码
代码还是比较简单的,基本就是一步一步走下去
先来看入口文件
#!/usr/bin/env node
const commander = require('../lib/commander')
const inquirer = require('../lib/inquirer')
const build = require('../lib/run/build')
// 解析参数
commander()
// 获取结果
.then(inquirer)
// 构建
.then(build)
步骤还是比较明确的,接来下的主要步骤都在 lib文件下面,继续...
commander参数处理
主要定义了两个命令
- gxc:下载项目
- gxc d :删除项目
其他的:
- gxc -v:版本
- gxc -h(默认的):获取帮助
// 解析process.argv 参数
const { program } = require('commander')
const { version } = require('../../package.json')
const del = require('../run/delete')
module.exports = function () {
return new Promise((resolve) => {
// 拉取项目
program
.version(version, '-v, --version')
.description('下载github项目')
.action(resolve)
// 删除项目
program
.version(version, '-v, --version')
.command('del <projectName>')
.alias('d')
.description('下载github项目')
.action((projectName) => {
del(projectName)
})
program.parse(process.argv)
})
}
inquirer设置交互命令
先看执行文件:inquirer/index.js
第一步:获取用户输入的github账户对应的所有公共仓库
第二步:抛出用户选择的仓库
const getRepos = require('./getRepos')
const chooseRepo = require('./chooseRepo')
module.exports = async function () {
const repos = await getRepos()
const answer = await chooseRepo(repos)
return answer
}
注意:github API 在 githubApi/index.js中
更多api可以点击这里
getRepos
获取用户输入的github所有公共仓库
- 检查之前是否输入过github账户名,如果为否则走第二步,为是则询问用户是否沿用之前的账户名,用户回答是则直接走第三步,用户回答否则走第二步
- 要求用户输入账户名,拿到结果走下一步
- 拉取仓库列表,请求成功就创建缓存用户名和仓库文件,请求失败则先判断用户是否沿用账户名,如果是则获取上次缓存的仓库文件为结果返回,否则进入下面的错误处理
const inquirer = require('inquirer')
const { repos } = require('../../githubApi')
const util = require('../../util')
const chalk = require('chalk')
const ora = require('ora') // loading效果
// 获取仓库值
module.exports = () => {
let useOldUser = false; // 是否沿用之前账户名
return new Promise((resolve) => {
// 第一步
util.fileIsExist('user.txt')
.then(() => {
// 询问是否需要换账户
inquirer.prompt([{
type: 'confirm',
name: 'change',
message: '监测到您之前的输入过账户名,是否沿用之前账户名?'
}]).then((answer) => {
if (useOldUser = answer.change) {
util.readFile('user.txt').then((user) => {
requestRepos({ user: user.toString() })
})
} else {
getUser(requestRepos)
}
})
})
.catch(getUser.bind(null,requestRepos))
// 第二步
// 获取用户名
function getUser (cb) {
inquirer.prompt([{
type: 'input',
name: 'user',
message: '请输入的您的github账户名'
}]).then(cb)
}
// 第三步
// 获取仓库列表
function requestRepos (answer) {
const spinner = ora(chalk.yellow('正在拉取项目,请稍等...')).start()
repos(answer.user)
.then((res) => {
spinner.stop('')
// 判断是否有仓库存在
if (res.length) {
resolve(res)
} else {
util.consoleBlue('您没有项目可以下载!')
process.exit(1)
}
// 缓存用户名和仓库列表
util.createFile('user.txt', answer.user)
util.createFile('repos.txt', JSON.stringify(res))
})
.catch(err => {
spinner.stop('')
// 尝试获取之前的json输出
if (useOldUser) {
util.fileIsExist('repos.txt').then(() => {
util.readFile('repos.txt').then((res) => {
resolve(JSON.parse(res))
}).catch(err => {
util.consoleRed(err.toString())
process.exit(1)
})
})
}
if (/connect ECONNREFUSED | connect ECONNRESET/.test(err.toString())) {
util.consoleRed(`\n Github当前网络连接不上,请过一会重试!\n ${err.toString()}`)
} else {
util.consoleRed(err.toString())
process.exit(1)
}
})
}
})
}
chooseRepo
拿到用户选择的仓库,抛出
const inquirer = require('inquirer')
const util = require('../../util')
module.exports = function (repos) {
// 处理成prompt需要的格式
const choices = repos.map(item => ({
name: item.name + ':' + item.description,
value: item.clone_url
}))
return new Promise((resolve) => {
inquirer.prompt([
{
type: 'list',
name: 'url',
message: '请选择github项目',
choices
}
]).then((answers) => {
const projectName = choices.filter(item => item.value === answers.url)[0].name
util.consoleBlue(`您即将要下载的项目是:${projectName}`)
resolve(answers)
}).catch(error => {
if(error.isTtyError) {
util.consoleRed('无法在当前环境中呈现提示')
} else {
util.consoleRed(error.toString())
}
process.exit(1)
})
})
}
build构建下载仓库
文件路径在 command/build.js
用child_process 开启一个子进程,运行git clone 下载
// 执行下载
const exec = require('child_process').exec
const ora = require('ora') // loading效果
const chalk = require('chalk')
const util = require('../../util')
module.exports = function (answers) {
const spinner = ora(chalk.yellow('正在下载...')).start()
const buildExec = exec(`git clone ${answers.url}`, (err) => {
if (err) {
spinner.fail(chalk.red('下载中断'))
util.consoleRed(err.toString())
process.exit(1)
} else {
spinner.succeed(chalk.green('下载完成!'))
}
})
}
总结
至此结束,了解了一些工具模块的功能使用,以及自己实现了一个脚手架,感觉挺有成就感的。