从零搭建一个前端cli脚手架并发布到npm

在日常开发中,我们会根据经验沉淀出一些项目模板,在不同在项目中可以进行复用。如果是每次都是通过拷贝代码到新项目的话,这样会比较麻烦,而且容易出错,此时我们就会想能不能将一些模板集成到脚手架(类似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 -gcm -h

为什么需要脚手架?
  1. 减少重复性的工作,不再从零创建一个项目,或者复制粘贴另一个项目的代码 。
  2. 根据动态交互生成项目结构和配置文件,具备更高的灵活性和人性化定制的能力 。
  3. 有利于多人开发协作,避免了人工传递文件的繁琐。
  4. 可以集成多套开发模板,根据项目需要选择合适的模板。
第三方库的支持
实现一个脚手架,通常需要以下工具,后续我们将会一一介绍。
  • commander: 命令行工具
  • download-git-repo: 用来下载远程模板
  • inquirer: 交互式命令行工具
  • ora: 显示 loading 动画
  • chalk: 修改控制台输出内容样式
  • log-symbols: 显示出 √ 或 × 等的图标
  • handlebars.js 用户提交的信息动态填充到文件中
构建步骤
  1. 新建一个文件夹,命名为 cm-cli(我的脚手架命名),在该目录下执行 npm init -y 进行初始化,此时就会生产一个 package.json 文件。
  2. 安装第三方工具库
npm install chalk commander download-git-repo inquirer ora log-symbols

  1. 在根目录下新建一个 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 的效果了。
  1. 定义多个命令
我们再 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,就会看到以下的效果
从零搭建一个前端cli脚手架并发布到npm
文章图片

此时我们再改一下 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 新增一个项目模板
  1. 通过命令行交互,让用户输入模板名称和模板的地址
  2. 将用户输入的模板信息新增写入到template.json文件中
  3. 打印出所有的项目模板
看一下代码
#!/usr/bin/env nodeconst 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 = 1let 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) }) })

【从零搭建一个前端cli脚手架并发布到npm】在这里还用到以下两个第三方库,原来美化相互效果:
  • chalk:用来修改控制台输出内容样式的,比如颜色
  • log-symbols: 显示出 √ 或 × 等的图标
此时,执行 cm add ,并输入项目模板名称和地址,就能看到以下效果了
从零搭建一个前端cli脚手架并发布到npm
文章图片

cm detele 删除一个项目模板,这个就好了解,步骤如下
  1. 通过命令行交互,让用户输入要删除的项目模板名称
  2. 删除用户输入的模板数据,然后再将更新的数据写入到template.json文件中
  3. 打印出所有的项目模板
代码如下
#!/usr/bin/env nodeconst 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 = 1let 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 ,输入要删除的模板,就能看到以下效果了
从零搭建一个前端cli脚手架并发布到npm
文章图片

cm list 列举所有的项目模板,这个就更简单了,直接上代码
#!/usr/bin/env node const { showTable } = require(`${__dirname}/../util/showTable`) const templateList = require(`${__dirname}/../template`)showTable(templateList)

此时,我们执行一下cm list ,输入要删除的模板,就能看到以下效果了
从零搭建一个前端cli脚手架并发布到npm
文章图片

cm init 初始化一个项目模板,这是最重要的一部分,步骤如下
  1. 通过命令行交互,让用户模板的名称和项目的名称
  2. 校验模板是否存在,项目名称是否填写
  3. 开始下载模板,显示加载图标
  4. 完成模板下载,隐藏加载图标
先来看一下代码
#!/usr/bin/env nodeconst 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 = 1program.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(`\ncd ${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 文件夹,就是新拉取的项目模板。
从零搭建一个前端cli脚手架并发布到npm
文章图片

至此,一个前端脚手架就正式完成了。下面我们把它发布到 npm 上。
发布 npm
发布流程
  1. 执行 npm login 登陆 npm 账号,如果没有账号的先注册一个
  2. 执行 npm publish 进行发布
发布到 npm 的脚手架名称就是 package.json 的 name 值,要注意的是发布名称不能重复。
发布完之后,我们来验证一下。
  1. 执行 npm unlink 解绑一下全局命令
  2. 执行 npm install cm-vcli -g 全局安装脚手架
  3. 执行 cm -h
此时如果看到以下的效果,就说明脚手架已经发布并安装成功了。
从零搭建一个前端cli脚手架并发布到npm
文章图片

    推荐阅读