前端脚手架是怎么来的
说起脚手架,大家肯定都用过,vue-cli
, create-react-app
等,它们虽然是基于不同的技术,但功能可以总结为一句话:
- 通过命令行交互的方式进行项目框架的搭建。
既然都用过,那你有没有想过,这样的一个工具,具体是怎么做出来的呢,怎么能实现一个属于自己的脚手架?
现在开始,淦!
我们先来看下脚手架工具的使用流程
- 在命令行输入你的脚手架命令
- 命令行会对你进行一些询问
- 根据提出的问题进行选择,或者输入内容回答
- 根据你的回答,将项目结构初始化到你的目标目录
现在,正式开始,开发一个名叫 cli
的脚手架工具
首先,初始化一个项目目录
npm init -y
目录有了,怎么添加命令呢?
绑定全局命令
这个时候,打开控制台,输入 cli
,回车
因为我们的命令还没有添加到全局,所以这里会报错。
package.json
文件想必大家都很熟悉,在它包含的众多字段中,有一个字段 bin
,没错,就是它:
"bin": {
"cli": "bin/app"
},
复制代码
bin
项用来指定各个内部命令对应的可执行文件的位置。上面代码指定,
cli
命令对应的可执行文件为bin
子目录下的app
文件若包发到
npm
上, 在执行全局安装(npm install cli -g
)时,会将bin
中的命令自动添加到全局
接着创建对应的入口文件:在根目录下创建bin
子目录,然后创建app
文件, 并写入一行我们非常熟悉的内容:
console.log('hello word')
复制代码
那我们在编码过程中想要测试,难道每次都要发布到 npm
然后重新全局安装吗?当然不是,这个时候,你需要了解 npm
的另外一个命令:
npm link
这个命令可以将你当前目录下的 package.json
中的 bin
字段所定义的命令自动添加到全局。
这个时候,控制台执行 cli
,发现报错变了:
这是因为你的命令虽然添加上了,对应的入口文件也找到了,但是这个入口文件用什么解析器解析是不知道的,也就是系统并不知道这个文件是要通过node来解析。
解绑全局命令,重新绑定
执行 npm unlink
将命令解绑,然后在入口文件 app
添加一行代码:
#!/usr/bin/env node
复制代码
这行代码可以用来告诉系统当前文件是以node解析器来执行。
注意这行代码一定要放到文件第一行!
#!/usr/bin/env node
console.log('hello word')
复制代码
执行 npm link
重新绑定命令
执行 cli
命令:
这时,命令对应的入口文件已经可以成功执行了
编写入口文件
现在,我们要为脚手架添加一些可执行的交互命令
首先来了解几个必须的工具:
用来定义交互命令
// 信息声明
program
.version(require('../package').version) // 指定命令版本
.usage('<command> [options]') // 指定命令用法
// 定义命令
program
.command('create <name> [template]') // 创建 create 命令,可指定一个必选参数 name 和一个可选参数 template
.description('基于模板创建一个项目结构') // 命令的描述语
.option('-s, --select', `选择一个模板`)
.option('-t, --template <template>', `指定一个模板`)
.action(async (name, template, options) => { // 获取到相关参数
// do smoething
console.log('name:', name)
console.log('template:', template)
console.log('options:', options)
})
// 解析命令
program.parse(process.argv)
复制代码
命令行执行 cli
,可以看到定义的相关信息
执行 cli create app1 temp1 -s -t temp1
, 可以获取到相关参数:
这时,就可以通过这些参数进行创建项目的逻辑代码编写
编写各命令对应的执行文件
根目录下创建 lib
子目录,用来存放各命令对应的执行文件,然后创建 Create.js
// Create.js
class Create {
constructor(name, template, options) {
console.log('name:', name)
console.log('template:', template)
console.log('options:', options)
}
}
module.exports = Create
复制代码
在 app
中引入并执行
...
program
.command('create <name> [template]')
...
.action(async (name, template, options) => {
new Create(name, template, options);
})
...
复制代码
执行 cli create app1 temp1 -s -t temp1
, 可以进入到 Create.js
定义终端输入的样式
// Create.js
const chalk = require('chalk')
class Create {
constructor(name, template, options) {
console.log('name:', chalk.red(name)) // 终端中 app1 红色显示
console.log('template:', template)
console.log('options:', options)
}
}
module.exports = Create
复制代码
命令行交互工具
// Create.js
const chalk = require('chalk')
const inquirer = require('inquirer')
const fs = require('fs')
class Create {
constructor(name, template, options) {
this.name = name
this.template = template
this.options = options
this.init()
}
async init() {
const isExists = await this.fsAccess(this.name)
if (isExists) { // 如果模板目录存在,则需要进行选择操作
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: `Target directory ${chalk.cyan(this.name)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Rename', value: 'rename' },
{ name: 'Cancel', value: false }
]
}
])
console.log(action)
}
}
// 判断目录是否存在
fsAccess(path) {
return new Promise(resolve => {
fs.access(path, (err) => {
if (!err) {
resolve(true)
} else {
resolve(false)
}
})
})
}
}
module.exports = Create
复制代码
一个命令行loading小工具
fs
扩展工具
这里我们的项目模板采用从远程地址拉取的方式,当然,也可以将模板存放到脚手架中,直接进行复制操作
const ora = require('ora')
const child_process = require('child_process');
const templatePath= 'https://github.com/jquery/jquery.git'
// 拉取项目模板
cloneTemplate() {
const spinner = ora("template clone... \n"); // 生成loading效果
spinner.start();
child_process.exec(`git clone ${templatePath} ${this.name}`, (err) => { // 通过新开子进程的方式执行shell命令,拉取项目模板
if (err) {
spinner.fail()
console.log(chalk.cyan('download error'))
} else {
spinner.succeed()
console.log(chalk.green('download success'))
}
})
}
复制代码
完整代码
// Create.js
const chalk = require('chalk') // 终端样式定义
const inquirer = require('inquirer') // 命令行交互
const fs = require('fs-extra') // 文件操作拓展
const ora = require('ora') // 终端loading效果
const child_process = require('child_process'); // 子进程
const templatePath= 'https://github.com/jquery/jquery.git' // 模板地址
class Create {
constructor(name, template, options) {
this.name = name
this.template = template
this.options = options
this.init()
}
async init() {
const isExists = await this.fsAccess(this.name) // 判断目标目录是否已存在
if (isExists) { // 如果目标目录存在,则需要进行选择操作
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: `Target directory ${chalk.cyan(this.name)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' }, // 重新目录
{ name: 'Rename', value: 'rename' }, // 修改目标目录名称
{ name: 'Cancel', value: false } // 取消操作
]
}
])
if (!action) { // 选择 Cancel, 直接退出进程
process.exit()
} else if (action === 'overwrite') { // 选择 Overwrite, 删除已有的目标目录
await fs.remove(this.name)
this.cloneTemplate();
} else { // 选择 Rename, 需要重新输入目标目录
const { name } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: `please input the new app name:`
}
])
this.name = name;
this.init();
}
} else {
this.cloneTemplate();
}
}
// 判断目录是否存在
fsAccess(path) {
return new Promise(resolve => {
fs.access(path, (err) => {
if (!err) {
resolve(true)
} else {
resolve(false)
}
})
})
}
// 拉取项目模板
cloneTemplate() {
const spinner = ora("template clone... \n");
spinner.start();
child_process.exec(`git clone ${templatePath} ${this.name}`, (err) => {
if (err) {
spinner.fail()
console.log(chalk.cyan('download error'))
} else {
spinner.succeed()
console.log(chalk.green('download success'))
}
})
}
}
module.exports = Create
复制代码
结语
这样,一个简单的脚手架工具就可以使用了,接下来,可以发布到npm
进行全局安装
当然,这只是一个非常简单的demo,但是可以基于这个简单的demo,了解到一个脚手架的基本实现方式,然后根据实际需求,开发出一个适用于自己项目的通用脚手架
END
本文到此结束,有什么问题欢迎各位大佬留言