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

lzg9527 -
从零搭建一个前端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 -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(): 解析命令行参数 argvaction(): 注册一个 callback 函数version() : 终端输出版本号

根据日常开发需要,我们创建以下几个脚手架命令

add 新增一个项目模板delete 删除一个项目模板list 列举所以项目模板init 初始化一个项目模板

我们先来编写一下 cm 文件

#!/usr/bin/env node
const program = require('commander')

program.usage('<command>')

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,就会看到以下的效果

cm.png

此时我们再改一下 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 ,并输入项目模板名称和地址,就能看到以下效果了

cmadd.png

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 ,输入要删除的模板,就能看到以下效果了

cmdelete.png

cm list

列举所有的项目模板,这个就更简单了,直接上代码

#!/usr/bin/env node
const { showTable } = require(`${__dirname}/../util/showTable`)
const templateList = require(`${__dirname}/../template`)

showTable(templateList)

此时,我们执行一下cm list ,输入要删除的模板,就能看到以下效果了

cmlist.png

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('<template-name> [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 文件夹,就是新拉取的项目模板。

cminit.png

至此,一个前端脚手架就正式完成了。下面我们把它发布到 npm 上。

发布 npm

发布流程

执行 npm login 登陆 npm 账号,如果没有账号的先注册一个执行 npm publish 进行发布

发布到 npm 的脚手架名称就是 package.json 的 name 值,要注意的是发布名称不能重复。

发布完之后,我们来验证一下。

执行 npm unlink 解绑一下全局命令执行 npm install cm-vcli -g 全局安装脚手架执行 cm -h

此时如果看到以下的效果,就说明脚手架已经发布并安装成功了。

cm.png

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

前端javascriptvue.jsnpmnode.js

扩展阅读

加个好友,技术交流

1628738909466805.jpg