自定义封装一个命令行脚手架

背景

我们用 vue,react,或是 vite,搭建项目时,大都是执行一个命令行,然后终端出现模板,让你选择,你输入项目名称,选择模板,选择一些配置项,是否需要安装等等,最后得到一个初始化的模板,然后在上面进行业务的开发。

实现一个这样的命令行脚手架,其实不难,借助一些第三方库,我们就能做到

为啥要自己自定义封装这么一个命令行脚手架

。减少重复性的工作,不再需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。

。根据交互动态生成项目结构和配置文件等。

。多人协作更为方便,不需要把文件传来传去,ctrl + C/V。

快速讲解下实现的过程

  1. 在github仓库先建立好模板
  2. 用户通过命令交互的方式下载不同的模板
  3. 经过用户自定义的选择,将模板引擎渲染定制项目模板
  4. 模板变动,只需更新模板即可,不需要用户更新脚手架

用到的第三方库

js库 说明
commander.js 可以自动的解析命令和参数,用于处理用户输入的命令
download-git-repo 下载并提取 git 仓库,用于下载项目模板
inquirer.js 通用的命令行用户界面集合,用于和用户进行交互
handlebars.js 模板引擎,将用户提交的信息动态填充到文件中
ora 下载过程久的话,可以用于显示下载中的动画效果
chalk 可以给终端的字体加上颜色
log-symbols 可以在终端上显示出√或x等的图标

再放下各个插件的版本,避免因为插件的高低版本不同,导致同样的代码运行起来频频报错

json 复制代码
  "dependencies": {
    "chalk": "^2.4.1",
    "commander": "^12.0.0",
    "download-git-repo": "^3.0.2",
    "handlebars": "^4.0.11",
    "inquirer": "^6.1.0",
    "log-symbols": "^2.2.0",
    "ora": "^3.0.0"
  }

这里可能大家会有个好奇,package.json 中有 dependencies ,还有 devDependencies,我为什么要把这些插件安装在 dependencies中,而不是安装在 devDependencies,我为什么 npm i xx -S 而不是 npm i xx -D

这里我不展开详细讲 dependenciesdevDependencies的区别,我就快速简单说一下:

  1. devDependencies 里面平时放的都是工具类的插件,在生产环境中不会再用到
    • 举个栗子:比如 Babel 这种,我不希望它在生产环境中还在,因为生产环境的代码是已经打包过之后,已经从 es6 处理成 es5 了,不需要再用Babel了
  2. dependencies 里面平时放的也是工具类的插件,但是这类插件在生产环境中也依然会用到
    • 举个栗子:比如echarts, 我最后代码打包之后,在生产环境中要可视化展示一些数据,需要用到图表,那echarts, 就是在生产环境中也要用到的

那问题来了:我开发自定义的脚手架,为什么扯到了dependenciesdevDependencies?它两的区别我也不是不知道。

公布答案:主要原因是因为开发脚手架,我在本地用这两个没啥区别,最后发现问题是出在发包之后下载安装

我本地调的没问题,我当时的依赖都是放在devDependencies里的,发包之后

bash 复制代码
# 发包之后进行安装
npm install --global my-hahaha-cli

经过排查,才发现,问题就是出在 devDependencies 上,当我把我的包卸载掉之后,把所有的依赖都转移到dependencies上。

bash 复制代码
# 卸载自己安装的包
npm uninstall --global my-hahaha-cli

# 重新发布之后,再次安装
npm install --global my-hahaha-cli

这个时候,依赖才自动安装上了。

本地环境

nodejs 14.18.0

因为我是本地用 nvm管理多版本的node, 同时也安装了 yarn, 今天复盘整理笔记的时候,之前是用 npm install来安装的,后来习惯性的用了 yarn 结果报错了,这里需要注意一下

简单介绍下各个插件

commander

作用:可以自动的解析命令和参数,用于处理用户输入的命令

node原生有 process.argv 这个属性,也能用户输入的命令。 但是这个属性是从第三个开始才是用户输入的,比较麻烦, 所以我们还是使用 commander

bash 复制代码
npm install commander@12.0.0 -S

download-git-repo

作用:这个包可以帮助我们下载 github仓库中的包到本地

bash 复制代码
npm install download-git-repo@3.0.2 -S

使用这个库时,有个注意点

使用 download-git-repo 这个第三方包 进行模板的下载,先在自己的github上创建几个公开的git仓库,用 down-git-repo的时候,要注意一个问题,他的download方法的第一个参数 要写 git仓库的地址,但是这个地址一般来讲,是 https://github.com/github用户名/仓库名,但是在用这个第三方库download的时候,https://github.com后面跟紧的是 :(冒号)然后才是 github用户名/仓库名 再跟着 #分支名

否则,会报一个128 没有权限的问题

inquirer

作用:主要是 实现向导作用,实现一问一答的交互方式

handlebars

作用:主要是 当作模板引擎来处理字符串,再配合 node自带的 fs模块 将处理好的字符串 替换掉原来的模板package.json

inquirrerhandlebars配合使用


视觉美化的第三方库

ora

作用:控制台下的loading效果,加载中...

hash 复制代码
npm install ora@3.0.0 -S

chalk

作用:为打印信息加上样式,比如成功信息为绿色,失败信息为红色,这样子会让用户更加容易分辨同时也让终端的显示更加的好看

hash 复制代码
npm install chalk@2.4.1 -S

log-symbols

hash 复制代码
npm install log-symbols@2.2.0 -S

作用:各种日志级别的彩色符号,给终端的提示信息前面加上 ✔,× 的符号

过程及代码

  1. 先在github上建立3个项目模板

如法炮制,再新建2个

然后,以第一个my-template1 为例,新建一个package.json,给里面的字段设置成动态的

github这边的已经设置好了,然后就是编码环节

  1. 这里我直接放代码了,讲解都写在了注释里了

index.js

js 复制代码
#!/usr/bin/env node

//使用Node开发命令行工具所执行的 Javascript 脚本必须在顶部加入 #!/usr/bin/env node  声明
// console.log("hello demo")

// 1. 获取用户输入命令 (原生获取命令行参数的方法)
// console.log(process.argv)



const { Command } = require('commander');
const download = require('download-git-repo')
const chalk = require('chalk')
const logSymbols = require('log-symbols')
const inquirer = require('inquirer')
const fs = require('fs')
const handlebars = require('handlebars')
const ora = require('ora')

const program = new Command();
program
  .version('0.1.0');

const templates = {
    'tpl-obj1': {
        url: 'https://github.com/OhBadWorld/my-template1',
        downloadUrl: 'https://github.com:OhBadWorld/my-template1#main',
        description: '基于vue2搭建的obj1模板'
    },
    'tpl-obj2': {
        url: 'https://github.com/OhBadWorld/my-template2',
        downloadUrl: 'https://github.com:OhBadWorld/my-template2#main',
        description: '基于vue2搭建的obj2模板'
    },
    'tpl-obj3': {
        url: 'https://github.com/OhBadWorld/my-template3',
        downloadUrl: 'http://github.com:OhBadWorld/my-template3#main',
        description: '基于vue2搭建的obj3模板'
    }
}
// mycli init a a-name   基于 a模板进行初始化
// mycli init b b-name   基于 b模板进行初始化
// mycli init c c-name   基于 c模板进行初始化

program
  .command('init <template-name> <project-name>')
  .description('初始化项目模板')
  .option('-s, --setup_mode [mode]', 'Which setup mode to use')
  .action((templateName, projectName) => {
    // 下载之前做 loading 提示
    const spinner = ora('正在下载模板...').start();
    // 根据模板名下载对应的模板到本地并取名为 projcetName
    // console.log(templates[templateName], projectName);
    // download
    //      第一个参数: 仓库地址
    //      第二个参数: 下载路径
    const { downloadUrl } = templates[templateName]
    download(downloadUrl, projectName, { clone: true }, (err)=>{
        if (err) {
          spinner.fail() // 下载失败提示
          console.log(logSymbols.error, chalk.red(`${templateName} download fail`))
          return
        }
        spinner.succeed() // 下载成功提示
        // 把项目下的 package.json 文件读取出来
        // 使用向导的方式采集用户输入的值
        // 使用模板引擎把用户输入的数据解析到 package.json 文件中
        // 解析完毕,把解析之后的结架重新写入package.json 文件中
        inquirer.prompt([
          {
            type: 'input',
            name: 'name',
            message:'请输入项目名称'
          },
          {
            type: 'input',
            name: 'description',
            message:'请输入项目简介'
          },
          {
            type: 'input',
            name: 'author',
            message:'请输入作者名称'
          },
        ]).then((answers)=>{
          const packagePath = `${projectName}/package.json`
          // 把采集到的用户输入的数据解析替换到 package.json 文件中
          const packageContent = fs.readFileSync(packagePath, 'utf8')
          // 通过 handlebars进行模板解析,作用 编译并替换
          const packageResult = handlebars.compile(packageContent)(answers)
          fs.writeFileSync(packagePath, packageResult)
          console.log(logSymbols.success, chalk.green(`模板 ${templateName} init success`))
        })
    })
  });

program
  .command('list')
  .description('查看所有可用模板')
  .action(() => {
    for (let key in templates) {
        console.log(`
        ${key}   ${templates[key].description}
        `)
    }
  })

program.parse();

package.json

json 复制代码
{
  "name": "my-cli",
  "version": "1.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "type": "commonjs",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bin": {
    "my": "index.js"
  },
  "dependencies": {
    "chalk": "^2.4.1",
    "commander": "^12.0.0",
    "download-git-repo": "^3.0.2",
    "handlebars": "^4.0.11",
    "inquirer": "^6.1.0",
    "log-symbols": "^2.2.0",
    "ora": "^3.0.0"
  }
}
  1. 本地开发,npm link 命令

你本地开发,你要用自定义的脚手架命令,需要在项目的当前目录下,在终端执行npm link命令,这样,你在任何位置目录的终端里,输入 my,都可以执行到你自定义的命令。

如果你想换个命令名,不叫my ,叫my123,也可以,你还是去在项目的当前目录下,在终端,先执行npm unlink , 再执行 npm link 即可。

你本地开发好了,那就在项目的当前目录下,在终端执行npm unlink即可。

发包

前置条件:将你本地的 npm源 设置成 npm官网的镜像源

在这里,需要把你的npm的镜像源设置成 npm官方网站的镜像源

因为 平时我们开发,下载依赖包,由于npm官网在国外,受到网络的传输速度的影响,平时我们都是在淘宝镜像上下载依赖的。但是你要发包,是发布在npm官网上,所以就要切换成 npm官网的依赖

bash 复制代码
查看npm的配置
npm config list

设置官方源
npm config set registry https://registry.npmjs.org/

设置国内镜像源
npm config set registry http://registry.npmmirror.com/

这里推荐使用 nrm 来管理 镜像源,可以参考我这篇文章:npm 与 nrm

  1. 首先需要先登录 打开你的终端,任何位置的终端都行,输入 npm login,然后输入你的账号密码 (账号邮箱可以看见,密码是看不见的,你照旧输入就行)
  1. 进入你的脚手架目录,打开终端,输入 npm publish 命令
    • 在输入命令之前,你先查下,你要发布的包名,在npm官网上有没有重名的,可能会存在冲突
    • 莽一波,果然失败了

就是包名重复了

这下发布成功了

bash 复制代码
# 下载依赖
npm install --global my-hahaha-cli

# 卸载依赖
npm uninstall --global my-hahaha-cli
相关推荐
霍先生的虚拟宇宙网络16 分钟前
webp 网页如何录屏?
开发语言·前端·javascript
jessezappy37 分钟前
jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js
前端·word·jquery·filesaver·word-export
旧林8431 小时前
第八章 利用CSS制作导航菜单
前端·css
Roc.Chang1 小时前
macos 使用 nvm 管理 node 并自定义安装目录
macos·node.js·nvm
yngsqq1 小时前
c#使用高版本8.0步骤
java·前端·c#
Smile丶凉轩2 小时前
微服务即时通讯系统的实现(服务端)----(1)
c++·git·微服务·github
Myli_ing2 小时前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风2 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
软件小伟2 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾2 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js