何为脚手架?在日常开发中,我们创建vue项目所使用的vue-cli,创建react项目的create-react-app…都是非常优秀的脚手架。
那为什么要使用脚手架来创建项目呢?因为在实际开发中,使用脚手架我们可以快速的初始化一个项目,无需自己一步一步配置,可以有效的提升开发效率。
既然已经有这么多优秀的脚手架工具了,为什么还要自己开发一个脚手架呢?其实在实际开发中,有可能这些脚手架不符合我们的实际应用,这时候就需要我们自己根据实际的需求自己去“造轮子”。
一、脚手架运行的基本流程
脚手架就是通过命令交互的方式去渲染对应的模板文件,基本流程如下:
1.通过命令行交互询问用户问题
2.根据用户回答的结果生成文件
例如我们在使用vue-cli创建项目的时候
step1: 运行创建命令
vue create cli-test
step2: 用户选择
step3: 生成符合需求的项目文件
# 忽略部分文件夹
cli-test
├─ index.html
├─ src
│ ├─ App.vue
│ ├─ assets
│ │ └─ logo.png
│ ├─ components
│ │ └─ HelloWorld.vue
│ ├─ main.js
│ └─ router
│ └─ index.js
└─ package.json
二、脚手架插件
脚手架工具需要命令行交互、大型字符打印、logo/输出字符美化、loading效果…,因此需要以下插件来实现这些功能。
名称 | 简介 |
命令行插件 | |
大型字符 - 终端打印大型文字 | |
彩色文字 - 美化终端字符显示 | |
命令行参数输入交互 | |
loading效果 | |
仓库下载 |
下面我们演示这几个插件都在my-cli项目中演示
step1: 我们创建my-cli目录并初始化项目
mkdir my-cli
step2: 初始化项目
cd my-cli && npm init
step3: 创建bin/cli.js文件
mkdir bin && touch bin/cli.js
step4: 将入口文件改成cli.js
// package.json
{
"name": "my-cli",
"version": "1.0.0",
"description": "",
"bin": "./bin/cli.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
step5: 编辑cli.js文件-在文件开始添加 #! /usr/bin/env node这一段代码
#! /usr/bin/env node
// #! 符号的名称叫 Shebang,用于指定脚本的解释程序
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改
// 用于检查入口文件是否正常执行
console.log('my-cli hahaha')
step6: npm link 链接到全局
npm link
当我们在控制台运行my-cli命令的时候,能够输出cli.js文件的内容,说明全局链接成功了
$ my-cli
my-cli hahaha
1. commander
安装依赖
npm i commander figlet chalk inquirer ora download-git-repo
在cli.js中引入
// cli.js
#! /usr/bin/env node
let program = require('commander')
program
.command('create <app-name>')
.description('create a new project')
.action(async (name, options) => {
console.log('project name is ' + name)
})
program.parse()
然后我们运行my-cli create test-xxx
$ my-cli create test-xxx
project name is test-xxx
2. figlet
// cli.js
#! /usr/bin/env node
let figlet = require('figlet')
figlet('my-cli', function (err, data) {
if (err) {
console.log('Something went wrong...')
console.dir(err)
return
}
console.log(data)
})
运行my-cli
$ my-cli
_ _
_ __ ___ _ _ ___| (_)
| '_ ` _ \| | | |_____ / __| | |
| | | | | | |_| |_____| (__| | |
|_| |_| |_|\__, | \___|_|_|
|___/
3. chalk
// cli.js
#! /usr/bin/env node
let figlet = require('figlet')
let chalk = require('chalk')
figlet('my-cli', function (err, data) {
if (err) {
console.log('Something went wrong...')
console.dir(err)
return
}
console.log(chalk.yellow(data))
console.log(chalk.blue(data))
console.log(chalk.red(data))
})
运行my-cli
npm 下载chalk默认下载的是5.x版本,chalk从5.0+版本开始,只支持ESModule格式,chalk是使用export default导出的,chalk之前的版本支持CommomJs格式。所以这里我们要下载5.0以下版本。
4. inquirer
这里我们模仿vue-cli创建项目的命令交互
// cli.js
#! /usr/bin/env node
let program = require('commander')
let { promisify } = require('util')
let asyncFiglet = promisify(require('figlet'))
let chalk = require('chalk')
let inquirer = require('inquirer')
// 日志打印函数
const log = (content) => console.log(chalk.yellow(content))
// 设置版本和参数
program.version(`v${require('../package.json').version}`)
program.option('-n --name <type>', 'output name')
// 打印LOGO
async function printLogo() {
let data = await asyncFiglet('v-cli')
log(data)
}
program
.command('create <app-name>')
.description('create a new project')
.option('-f, --force', 'overwrite target directory if iy exist')
.action(async (name, options) => {
await printLogo()
log('准备创建项目...')
let answer = await inquirer.prompt([
{
name: 'language',
type: 'list',
message: 'Please pick a present:',
choices: [
'Default([Vue 2] babel,eslint)',
'Default(Vue 3)([Vue 3] babel,eslint)',
'Manually select features',
],
},
])
if (answer.language == 'Default([Vue 2] babel,eslint)') {
log('您选择了Vue2版本,即将进入下载模式.')
} else if (answer.language == 'Default(Vue 3)([Vue 3] babel,eslint)') {
log('您选择了Vue3版本,即将进入下载模式.')
} else {
log('你选择了手动配置')
}
})
// 参数解析
program.parse(process.argv)
运行my-cli create my-app
$ my-cli create my-app
_ _
__ __ ___| (_)
\ \ / /____ / __| | |
\ V /_____| (__| | |
\_/ \___|_|_|
准备创建项目...
? Please pick a present: (Use arrow keys)
> Default([Vue 2] babel,eslint)
Default(Vue 3)([Vue 3] babel,eslint)
Manually select features
选择了vue2
? Please pick a present: Default([Vue 2] babel,eslint)
您选择了Vue2版本,即将进入下载模式.
选择了vue3
? Please pick a present: Default(Vue 3)([Vue 3] babel,eslint)
您选择了Vue3版本,即将进入下载模式.
选择了Manually select features
? Please pick a present: Manually select features
你选择了手动配置
和chalk的问题一样,默认是9.x版本,需要降低版本
5. download-git-repo
当我们选择vue2的时候,我们直接下载vue2的默认版本
// cli.js
#! /usr/bin/env node
...
let ora = require('ora')
const spinner = ora('loading')
...
...
if (answer.language == 'Default([Vue 2] babel,eslint)') {
log('您选择了Vue2版本,即将进入下载模式.')
spinner.start()
setTimeout(() => {
spinner.start()
spinner.succeed('下载成功')
}, 3000)
}
...
...
运行my-cli create my-app
$ my-cli create my-app
_ _
__ __ ___| (_)
\ \ / /____ / __| | |
\ V /_____| (__| | |
\_/ \___|_|_|
准备创建项目...
? Please pick a present: (Use arrow keys)
> Default([Vue 2] babel,eslint)
Default(Vue 3)([Vue 3] babel,eslint)
Manually select features
选择了vue2
和chalk的问题一样,默认是6.x版本,需要降低版本
6. download-git-repo
模板克隆
// cli.js
#! /usr/bin/env node
...
let download = require('ora')
download('https://mygitlab.com:flippidippi/download-git-repo-fixture#my-branch', 'test/tmp', { clone: true }, function (err) {
console.log(err ? 'Error' : 'Success')
})
三、创建自己的脚手架
前面我们介绍了几种创建脚手架的插件,下面让我们来利用上面的插件来创建一款自己的插件。
1. 创建项目
参考上面的列子,先创建一个目录结构
my-cli
├─ bin
│ └─ cli.js
└─ package.json
配置package.json文件
{
"name": "my-cli",
"version": "1.0.0",
"description": "",
"bin": "./bin/cli.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^4.1.0",
"commander": "^9.4.1",
"download-git-repo": "^3.0.2",
"figlet": "^1.5.2",
"inquirer": "^8.1.0",
"ora": "^5.4.1"
}
}
使用npm link 链接到全局
npm link
2. 创建启动命令
安装命令行插件commander
npm i commander -S
参考上面的例子,创建脚手架的入口文件,编辑cli.js,创建create命令,并设置脚手架的版本和参数
// cli.js
#! /usr/bin/env node
let program = require('commander')
let create = require('./create')
// 设置版本和参数
program.version(`v${require('../package.json').version}`)
program.option('-n --name <type>', 'output name')
program
.command('create <app-name>')
.description('create a new project')
.option('-f, --force', 'overwrite target directory if iy exist')
.action(async (name, options) => {
create(name, options)
})
// 参数解析
program.parse(process.argv)
当创建的目录已经存在时,我们询问用户是否覆盖本地目录
我们创建create.js文件并在cli.js中调用
// create.js
const path = require('path')
const fs = require('fs-extra')
let inquirer = require('inquirer')
let init = require('./init')
module.exports = async function (name, options) {
// current cmd directory address
const cwd = process.cwd()
// create target directory address
const targetAir = path.join(cwd, name)
if (fs.existsSync(targetAir)) {
if (options.force) {
await fs.remove(targetAir)
} else {
let answer = await inquirer.prompt([
{
name: 'language',
type: 'list',
message: `Target directory ${targetAir} already exists. Pick an action: (Use arrow keys)`,
choices: ['Overwrite', 'Cancel'],
},
])
if (answer.language === 'Overwrite') {
await fs.remove(targetAir)
}
if (answer.language === 'Cancel') {
return
}
}
}
init(name)
}
上面的代码中targetAir代表的是目标目录地址,比如我们在D盘运行my-cli create test,此时targetAir就是D:\test。
之后通过fs.existsSync()方法判断当前目录下是否有test文件。
- 若有,判断创建命令的时候是否带有-f/–force
带有-f/–force,就直接删掉本地文件,并创建新的文件
不带-f/–force,询问用户是否覆盖
- 若无,直接创建新的目录
3. 下载模板
创建init.js文件,并在create.js中调用
// init.js
let { promisify } = require('util')
const ora = require('ora')
const download = promisify(require('download-git-repo'))
let chalk = require('chalk')
let inquirer = require('inquirer')
let asyncFiglet = promisify(require('figlet'))
// 日志打印函数
const log = (content) => console.log(chalk.yellow(content))
// 打印LOGO
async function printLogo() {
let data = await asyncFiglet('v-cli')
log(data)
}
module.exports = async (appName) => {
await printLogo()
log(`🚀 创建项目 ${appName}`)
let answer = await inquirer.prompt([
{
name: 'language',
type: 'list',
message: 'Please pick a present:',
choices: [
'Default([Vue 2] babel,eslint)',
'Default(Vue 3)([Vue 3] babel,eslint)',
'Manually select features',
],
},
])
if (answer.language == 'Default([Vue 2] babel,eslint)') {
const spinner = ora('下载中...').start()
try {
await download(
'direct:https://gitee.com/zjinxiaoliang/jimo-ui.git', // 这里是随便填的笔主的一个开源项目地址
appName,
{ clone: true }
)
spinner.succeed('下载完成')
log(`
下载完成,请执行下面命令启动项目:
===========================
cd ${appName}
yarn 或者 npm init
npm run dev
或者
yarn dev
`)
} catch (error) {
log(`下载失败`, error)
spinner.stop()
}
} else if (answer.language == 'Default(Vue 3)([Vue 3] babel,eslint)') {
log('您选择了Vue3版本,即将进入下载模式.')
} else {
// manually(name)
}
}
当我们执行创建项目的命令
4. 发布到npm
当我们在使用vue-cli之类的脚手架的时候都是, 我们只需要npm全局安装之后,就能够通过命令直接创建项目。那我们应该如何发布到npm官网上呢?
step1: 在git上创建好仓库
step2: 修改package.json
因为,我发布到npm上的时候,发现my-cli已经被占用了,所有我将name改成了jimo-cli
{
"name": "jimo-cli",
"version": "1.0.0",
"private": false,
"description": "",
"main": "/bin/cli.js",
"bin": {
"jimo": "./bin/cli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"files": [
"bin"
],
"keywords": [
"jimo-cli",
"cli",
"脚手架"
],
"author": {
"name": "jinxiaoliang",
"email": "1848815766@qq.com"
},
"license": "ISC",
"dependencies": {
"chalk": "^4.1.0",
"commander": "^9.4.1",
"download-git-repo": "^3.0.2",
"figlet": "^1.5.2",
"fs-extra": "^10.1.0",
"inquirer": "^8.1.0",
"ora": "^5.4.1"
}
}
step3:使用npm publish到npm官网上,发布成功之后,我们到npm官网上面看一下
step4: 我们在本地下载脚手架
step5:然后我们在本地创建项目
到此为止,我们就已经构建好了自己的脚手架了。