vue-cli
在vue的开发的过程中,经常会使用到vue-cli脚手架工具去生成一个项目。在终端运行命令vue create hello-world
后,就会有许多自动的脚本运行。
- 为什么会这样运行呢?
- 我们自己是否也能写一个脚手架工具?
带着这样的疑问,我们先来看看vue-cli。
解读vue-cli
首先我们可以来到vue-cli的安装目录:
mac用户来到路径:/usr/local/lib/node_modules 可以看到(windows可以自行到全局安装的目录下查看)
此时用我们的编辑器 打开@vue文件:
lib内放的是具体各种配置和各种类,对于我们来说,这个目录内的就是所谓的业务逻辑。
bin内放的就是脚本命令的入口,调用lib的入口,入口在package.json内红框定义。我们姑且先放下业务逻辑,来看看这个入口文件。
现在,我们先放下所有的疑虑,我们打开bin/vue.js。
我们可以看到以下内容:
看完之后有什么感觉?咦?怎么跟终端内输出的好像?没错,就是这样,这就是我们使用vue-cli的时候具体的命令。我们去终端输入
vue
:发现了吗,终端内的具体命令全在vue.js内定义过了。
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
program
.command('add <plugin> [pluginOptions]')
.description('install a plugin and invoke its generator in an already created project')
program
.command('invoke <plugin> [pluginOptions]')
好了,剩下代码有兴趣的可以自行打开对应目录读下去,可以学习人家优秀的设计思想。对于本文来说,剩下的许多东西都是业务代码了。我们开头的疑问
为什么会这样运行呢?
已经了解了一个大概了。我们现在来看看我们自己是否也能写一个脚手架工具?
,答案是肯定的。开始动手吧。
手写一个自己的脚手架
先来看看可能需要用到哪些npm的包:
- commander:参数解析
- inquirer:交互式命令行工具,有他就可以实现命令行的选择功能
- chalk:输出文本颜色,为了美丽~
后续可能随着模块的增加,会出现更多需要的包
1. 创建项目
npm init -y # 初始化package.json
2. 创建文件目录
- 在package.json内添加“bin”
- bin下的文件没有格式,且第一行必须是
#! /usr/bin/env node
3. 链接包到全局
npm link # // 取消链接 npm unlink
有时候可能需要在上面命令后面拼接 --force,mac权限问题记得前面sudo
可以去目录:/usr/local/lib/node_modules 查看,发现我们多了一个,同时在这时候终端输入一下试试,我们在package.json下叫的name叫superman-cli
,所以我们的命令就是叫superman-cli
:
好了,基础配置初始化的工作全部结束了!
4. 第一个命令
首先安装包commander
npm install commander --save
在目录bin/superman内
#! /usr/bin/env node
// console.log(1)
const program = require('commander')
program
.version(`Version is ${require('../package.json').version}`)
.description('从0开始 手写脚手架')
.usage('<command> [options]')
program
.parse(process.argv)
测试有效!
然后我们在前面Version命令下加入代码:
program
.command('create <app-name>')
.description('create a new project')
.option('-f, --force', 'Overwrite target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.action((name, cmd) => {
console.log('name', name)
console.log('cmd', cmd)
})
仔细对比我们的代码合终端的输出,我们就可以看到我们写的很多东西都生效了。接下来我们就优化一下
.action
下的参数,毕竟一大堆也不好处理:
program
.command('create <app-name>')
.description('create a new project')
.option('-f, --force', 'Overwrite target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.action((name, cmd) => {
const options = cleanArgs(cmd)
console.log(options)
})
function camelize (str) {
return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '')
}
function cleanArgs (cmd) {
const args = {}
// console.log(cmd)
cmd.options.forEach(o => {
const key = camelize(o.long.replace(/^--/, ''))
// console.log(key)
// console.log(cmd[key])
// console.log(typeof cmd[key])
if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
args[key] = cmd[key]
}
})
return args
}
具体不懂的也可以像我注释的console.log一样,慢慢看就明白了
我们第一个命令已经完成一大半了,接下来就是我们这个create命令具体干什么事情。(在这个文件里,我们只管命令,就像vue-cli一样,这也是我们需要学习的地方,模块如何去处理)
// 在上面.action内补充一行代码
.action((name, cmd) => {
const options = cleanArgs(cmd)
console.log(options)
require('../lib/create')(name, options)
})
同时去lib下创建文件create.js
const path = require('path')
// const fs = require('fs-extra')
async function create (projectName, options) {
console.log(projectName, options)
const cwd = process.cwd(); // 获取当前命令执行时的工作目录
const targetDir = path.join(cwd,projectName); // 目标目录
console.log(targetDir)
}
module.exports = (...args) => {
return create(...args)
}
继续执行superman-cli create hello -f,我们可以得到,force:true
,如果新建的话,将来的目录会是/Users/chenjing/hello
接下来我们尝试创建目录hello,不过我们需要考虑几个问题:
- 是否已经存在目录hello了?(使用fs-extra包)
- 若存在是要删除覆盖还是停止操作?(这里就需要用到插件inquirer啦,进行选择)
npm install fs-extra --save
npm install inquirer --save
直接上代码:
const path = require('path')
const fsextra = require('fs-extra')
const fs = require('fs')
const Inquirer = require('inquirer')
async function create (projectName, options) {
console.log(projectName, options)
const cwd = process.cwd(); // 获取当前命令执行时的工作目录
const targetDir = path.join(cwd,projectName); // 目标目录
console.log(targetDir)
if (fsextra.existsSync(targetDir)) {
if (options.force) {// 如果强制创建 ,删除已有的
await fsextra.remove(targetDir);
console.log('删除成功')
createDir(projectName)
} else {
let { action } = await Inquirer.prompt([
{
name: 'action',
type: 'list',
message: 'Target directory already exists Pick an action:',
choices: [
{name:'Overwrite',value:'overwrite'},
{name:'Cancel',value:false}
]
}
])
if (!action) {
console.log('取消操作')
return
} else if (action === 'overwrite') {
console.log(`\r\nRemoving....`);
await fsextra.remove(targetDir)
console.log('删除成功')
createDir(projectName)
}
}
} else {
createDir(projectName)
}
}
function createDir (projectName) {
fs.mkdir(`./${projectName}`, function (err) {
if (err) {
console.log('创建失败')
} else {
console.log('创建成功')
}
})
}
module.exports = (...args) => {
return create(...args)
}
看效果,先来的一个空目录
superman-cli create hello
superman-cli create hello // 再次
superman-cli create hello -f // 覆盖
4. 小结
本篇的源码github地址
并不是说我们手写脚手架就到此结束了,只是要完整实现一个功能不是一两篇文章可以搞定的。不过我相信写到这里,动手能力强的一定也能体验一把手动模仿vue-cli的爽了。至于能写出什么牛C的脚手架,真的就是个人需求和业务代码堆加。当然可以发散思维后续还可以做许多许多事情。强烈建议阅读vue-cli。或者其他脚手架的源码,都在目录:/usr/local/lib/node_modules 下面,看源码真的是学习最直接的方法了,甚至copy人家的代码到自己的cli内执行。其乐无穷