在日常开发中,我们会根据经验沉淀出一些项目模板,在不同在项目中可以进行复用。如果是每次都是通过拷贝代码到新项目的话,这样会比较麻烦,而且容易出错,此时我们就会想能不能将一些模板集成到脚手架(类似vue-cli
, create-react-app
)中,这样我们进行初始化创建就能使用了呢?
比如本人有以下两套开发模板
基于vue-cli4和vant搭建的移动端开发模板 vue-cli4-vant
基于vue-cli4和ant-design-vue构建的后台管理系统简单模板 vue-cli4-vant
希望执行类似 aaa init bbb ccc
这样的命令快速初始化一个项目,无需自己从零开始一步步配置,大大提高了开发效率。
脚手架源码地址 点击这里
安装使用
npm install cm-vcli -g
cm -h
为什么需要脚手架?
- 减少重复性的工作,不再从零创建一个项目,或者复制粘贴另一个项目的代码 。
- 根据动态交互生成项目结构和配置文件,具备更高的灵活性和人性化定制的能力 。
- 有利于多人开发协作,避免了人工传递文件的繁琐。
- 可以集成多套开发模板,根据项目需要选择合适的模板。
第三方库的支持
实现一个脚手架,通常需要以下工具,后续我们将会一一介绍。
- commander: 命令行工具
- download-git-repo: 用来下载远程模板
- inquirer: 交互式命令行工具
- ora: 显示 loading 动画
- chalk: 修改控制台输出内容样式
- log-symbols: 显示出 √ 或 × 等的图标
- handlebars.js 用户提交的信息动态填充到文件中
构建步骤
- 新建一个文件夹,命名为
cm-cli
(我的脚手架命名),在该目录下执行npm init -y
进行初始化,此时就会生产一个package.json
文件。 - 安装第三方工具库
npm install chalk commander download-git-repo inquirer ora log-symbols
- 在根目录下新建一个 bin 文件夹,并在 bin 目录下新建一个无后缀名的 cm 文件,并写上:
#!/usr/bin/env node
console.log('hello world')
这个文件就是整个脚手架的入口文件,我们用 node ./bin/cm
运行一下,在控制台就会打印出 hello world。
当然,每次输入node ./bin/cm
这个命令有点麻烦,我们可以在 package.json 进行命令配置
"bin": {
"cm": "bin/cm"
}
此时我们执行 npm link
将命令挂载到全局,然后再输入 cm
就可以到达刚才node ./bin/cm
的效果了。
- 定义多个命令
我们再 bin 下面的 cm 文件夹来定义多个命令,此时就用到 commander 了。首先我们来看一下 commander 的用法
- usage(): 设置 usage 值
- command(): 定义一个命令名字
- description(): 设置 description 值
- option(): 定义参数,需要设置“关键字”和“描述”,关键字包括“简写”和“全写”两部分,以”,”,”|”,”空格”做分隔。
- parse(): 解析命令行参数 argv
- action(): 注册一个 callback 函数
- version() : 终端输出版本号
根据日常开发需要,我们创建以下几个脚手架命令
- add 新增一个项目模板
- delete 删除一个项目模板
- list 列举所以项目模板
- init 初始化一个项目模板
我们先来编写一下 cm 文件
#!/usr/bin/env node
const program = require('commander')
program.usage('')
program.version(require('../package').version)
program
.command('add')
.description('add a new template')
.action(() => {
require('../commands/add')
})
program
.command('delete')
.description('delete a template')
.action(() => {
require('../commands/delete')
})
program
.command('list')
.description('List the templateList')
.action(() => {
require('../commands/list')
})
program
.command('init')
.description('init a project')
.action(() => {
require('../commands/init')
})
program.parse(process.argv)
然后执行一下 cm -h
,就会看到以下的效果
此时我们再改一下 package.json 的配置
"bin": {
"cm-add": "bin/cm-add",
"cm-delete": "bin/cm-delete",
"cm-list": "bin/cm-list",
"cm-init": "bin/cm-init"
}
然后执行 npm unlink
解绑全局命令,再执行 npm link
重新把命令绑定到全局,这样就可以直接使用 cm add
等命令了。
编写指令
在这里会用到 inquirer 进行命令行交互,我们先来看下 inquirer 的用法,它有以下参数可以配置
- type:表示提问的类型,包括:input, confirm, list, rawlist, expand, checkbox, password, editor;
- name: 存储当前问题回答的变量;
- message:问题的描述;
- default:默认值;
- choices:列表选项,在某些 type 下可用,并且包含一个分隔符(separator);
- validate:对用户的回答进行校验;
- filter:对用户的回答进行过滤处理,返回处理后的值;
- when:根据前面问题的回答,判断当前问题是否需要被回答;
- prefix:修改 message 默认前缀;
- suffix:修改 message 默认后缀。
语法结构如下:
const inquirer = require('inquirer')
const question = [
// 具体交互内容
]
inquirer.prompt(question).then((answers) => {
console.log(answers) // 返回的结果
})
cm add
新增一个项目模板
- 通过命令行交互,让用户输入模板名称和模板的地址
- 将用户输入的模板信息新增写入到
template.json
文件中 - 打印出所有的项目模板
看一下代码
#!/usr/bin/env node
const inquirer = require('inquirer')
const fs = require('fs')
const templateList = require(`${__dirname}/../template`)
const { showTable } = require(`${__dirname}/../util/showTable`)
const symbols = require('log-symbols')
const chalk = require('chalk')
chalk.level = 1
let question = [
{
name: 'name',
type: 'input',
message: '请输入模板名称',
validate(val) {
if (!val) {
return 'Name is required!'
} else if (templateList[val]) {
return 'Template has already existed!'
} else {
return true
}
}
},
{
name: 'url',
type: 'input',
message: '请输入模板地址',
validate(val) {
if (val === '') return 'The url is required!'
return true
}
}
]
inquirer.prompt(question).then((answers) => {
let { name, url } = answers
templateList[name] = url.replace(/[\u0000-\u0019]/g, '') // 过滤 unicode 字符
fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(templateList), 'utf-8', (err) => {
if (err) console.log(chalk.red(symbols.error), chalk.red(err))
console.log('\n')
console.log(chalk.green(symbols.success), chalk.green('Add a template successfully!\n'))
console.log(chalk.green('The latest templateList is: \n'))
showTable(templateList)
})
})
在这里还用到以下两个第三方库,原来美化相互效果:
- chalk:用来修改控制台输出内容样式的,比如颜色
- log-symbols: 显示出 √ 或 × 等的图标
此时,执行 cm add
,并输入项目模板名称和地址,就能看到以下效果了
cm detele
删除一个项目模板,这个就好了解,步骤如下
- 通过命令行交互,让用户输入要删除的项目模板名称
- 删除用户输入的模板数据,然后再将更新的数据写入到
template.json
文件中 - 打印出所有的项目模板
代码如下
#!/usr/bin/env node
const inquirer = require('inquirer')
const fs = require('fs')
const templateList = require(`${__dirname}/../template`)
const { showTable } = require(`${__dirname}/../util/showTable`)
const symbols = require('log-symbols')
const chalk = require('chalk')
chalk.level = 1
let question = [
{
name: 'name',
message: '请输入要删除的模板名称',
validate(val) {
if (!val) {
return 'Name is required!'
} else if (!templateList[val]) {
return 'Template does not exist!'
} else {
return true
}
}
}
]
inquirer.prompt(question).then((answers) => {
let { name } = answers
delete templateList[name]
fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(templateList), 'utf-8', (err) => {
if (err) console.log(chalk.red(symbols.error), chalk.red(err))
console.log('\n')
console.log(chalk.green(symbols.success), chalk.green('Deleted successfully!\n'))
console.log(chalk.green('The latest templateList is: \n'))
showTable(templateList)
})
})
此时,我们执行一下cm delete
,输入要删除的模板,就能看到以下效果了
cm list
列举所有的项目模板,这个就更简单了,直接上代码
#!/usr/bin/env node
const { showTable } = require(`${__dirname}/../util/showTable`)
const templateList = require(`${__dirname}/../template`)
showTable(templateList)
此时,我们执行一下cm list
,输入要删除的模板,就能看到以下效果了
cm init
初始化一个项目模板,这是最重要的一部分,步骤如下
- 通过命令行交互,让用户模板的名称和项目的名称
- 校验模板是否存在,项目名称是否填写
- 开始下载模板,显示加载图标
- 完成模板下载,隐藏加载图标
先来看一下代码
#!/usr/bin/env node
const program = require('commander')
const ora = require('ora')
const download = require('download-git-repo')
const templateList = require(`${__dirname}/../template`)
const symbols = require('log-symbols')
const chalk = require('chalk')
chalk.level = 1
program.usage(' [project-name]')
program.parse(process.argv)
// 当没有输入参数的时候给个提示
if (program.args.length < 1) return program.help()
// 第一个参数是 webpack,第二个参数是 project-name
let templateName = program.args[0]
let projectName = program.args[1]
if (!templateList[templateName]) {
console.log(chalk.red('\n Template does not exit! \n '))
return
}
if (!projectName) {
console.log(chalk.red('\n Project should not be empty! \n '))
return
}
let url = templateList[templateName]
console.log(url)
console.log(chalk.green('\n Start generating... \n'))
// 出现加载图标
const spinner = ora('Downloading...')
spinner.start()
download(`direct:${url}`, `./${projectName}`, { clone: true }, (err) => {
if (err) {
spinner.fail()
console.log(chalk.red(symbols.error), chalk.red(`Generation failed. ${err}`))
return
}
// 结束加载图标
spinner.succeed()
console.log(chalk.green(symbols.success), chalk.green('Generation completed!'))
console.log('\n To get started')
console.log(`\n cd ${projectName} \n`)
})
这里用到 download-git-repo 下载远程模板,它的使用方法如下
const download = require('download-git-repo')
download(repository, destination, options, callback)
- repository 是远程仓库地址
- destination 是存放下载的文件路径,也可以直接写文件名,默认就是当前目录
- options 是一些选项,比如 { clone:boolean } 表示用 http download 还是 git clone 的形式下载。
- callback 是回调函数
此时,我们执行一下cm init app demo
,就能看到以下效果了,根目录下就多了一个 demo 文件夹,就是新拉取的项目模板。
至此,一个前端脚手架就正式完成了。下面我们把它发布到 npm 上。
发布 npm
发布流程
- 执行
npm login
登陆 npm 账号,如果没有账号的先注册一个 - 执行
npm publish
进行发布
发布到 npm 的脚手架名称就是 package.json 的 name 值,要注意的是发布名称不能重复。
发布完之后,我们来验证一下。
- 执行
npm unlink
解绑一下全局命令 - 执行
npm install cm-vcli -g
全局安装脚手架 - 执行
cm -h
此时如果看到以下的效果,就说明脚手架已经发布并安装成功了。