开发一个属于自己的cli

177 阅读6分钟

背景

鉴于当前开发项目越来越多,随之而来带来一个新的问题,每初始化一个新的项目都要把之前老的项目copy下来,删除git文件,以及一大堆毫无关系的业务文件,工作量大不说还有可能出错,工作效率及其低下,为了提高工作效率以及省去不必要的重复性工作,开发一个前端脚手架势在必行。

实现以下功能

  1. 选择对应的模版并下载
  2. 自动安装项目模版依赖

工具准备

说白了开发一个脚手架其实就是各种工具库的应用,站在前人的肩膀上,加速开发效率

  1. Commander 完整的node js命令行解决方案
  2. Inquirer 通用的交互式命令行用户界面
  3. Ora 命令行加载效果
  4. Download-git-repo 从gitlab或者github下载代码
  5. Shelljs Node js 扩展,用于实现 Unix shell 命令执行
  6. Chalk 美化终端字体样式
  7. cross-spawn 执行项目安装依赖
  8. fs 读取文件
  9. log-symbols node下终端展示图标

脚手架实现

项目结构

├── dist

  ├── ... //生成文件

├── commands

  ├── init.js // 脚手架初始化

├── config

  └── index.js // 常量配置

├──utils

  ├── clone.js // 下载模版

  ├── install.ts // 安装依赖

  ├── constant.js // 常量文件

├── .babelrc //babel配置文件

└── package.json //包管理
  1. 初始化项目 npm init ,如下配置并安装下列依赖
{

    "name": "du-template-cli", // 脚手架发布名称

    "version": "1.0.0", // 发布版本,每发布一次,version必须更改

    "description": "前端脚手架工具",

    "main": "index.js",

    "scripts": {

    "test": "echo \"Error: no test specified\" && exit 1"

    },

    "type": "module",

    "author": "jikaibo",

    "license": "ISC",

    "bin": {

    "du-template-cli": "index.js" // 定义命令名和关联的执行文件,通过bin字段添加

    },

    "dependencies": {

        "chalk": "^4.1.2",

        "commander": "^8.2.0",

        "cross-spawn": "^7.0.3",

        "download-git-repo": "^3.0.2",

        "fs": "0.0.1-security",

        "inquirer": "^8.2.0",

        "log-symbols": "^5.0.0",

        "ora": "^6.0.1",

        "shelljs": "^0.8.4"
    }
}
  1. 项目根目录下新建index.js文件,并将index.js设置为可执行文件
#!/usr/bin/env node

// 注意:这并不是注释,而是声明在node环境下执行此文件,这行必须添加

import Commander from 'commander';

import { VERSION } from './utils/constants.js'

import { initAction } from './commands/init.js'

Commander.usage('<command> [options]');

Commander.version(VERSION).option('-v,--version','查看版本号'); // 查看版本号

Commander

.command('init <name>') // 定义init子命令,<name>为必需参数可在action的function中接收,如需设置非必需参数,可使用中括号

.option('-d, --dev', '获取开发版') // 配置参数,简写和全写中使用,分割

.description('创建项目') // 命令描述说明

.action(initAction)

// 利用commander解析命令行输入,必须写在所有内容最后面

Commander.parse(process.argv);
  1. 新建commands并在此下面新建init.js文件,用于初始化脚手架
import logSymbols from 'log-symbols';

import shell from 'shelljs';

import {clone} from '../utils/clone.js';

import inquirer from 'inquirer'; // 获取用户输入内容

import fs from 'fs'

import chalk from 'chalk'

import { BRANCH, remoteUrlArr } from '../utils/constants.js'

import { questions,installTool } from '../config/index.js'

import { install } from '../utils/install.js'

import path from 'path'

let branch = BRANCH;

let remote = '';

const initAction = async (name, option) => {

    // 0. 检查控制台是否可以运行`git `,

    if (!shell.which('git')) {

        console.log(logSymbols.error, '对不起,git命令不可用!');

        shell.exit(1);

    }

    // 1. 验证输入name是否合法

    if (fs.existsSync(name)) {

        console.log(logSymbols.warning,`已存在项目文件夹${name}!`);

    return;

    }

    if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {

        console.log(logSymbols.error, '项目名称存在非法字符!');

        return;

    }
    // 2. 获取option,确定模板类型(分支)

    if (option.dev) branch = 'develop';

    // 3 模版操作指令

    const answers = await inquirer.prompt(questions)

    const { type } = answers;

    remoteUrlArr.forEach(item => {

        if(item.type === type) {

            remote = item.url;

        }

    })

    let confirm = await inquirer.prompt([

        {

            type: 'confirm',

            message: '确认创建?',

            default: 'Y',

            name: 'isConfirm'

        }

    ]);

    if (!confirm.isConfirm) return false;

    // 4. 下载模板

    await clone(`direct:${remote}#${branch}`, name, { clone: true });

    // 5. 填写模版中的package.json文件基础信息

    const fileName = `${name}/package.json`;

    if(fs.existsSync(fileName)) {

        const data = fs.readFileSync(fileName).toString();

        let json = JSON.parse(data);

        json.name = answers.name;

        json.author = answers.author;

        json.description = answers.description;

        json.stageCode = answers.stageCode

        //修改项目文件夹中 package.json 文件

        fs.writeFileSync(fileName, JSON.stringify(json, null, '\t'), 'utf-8');

    }

    // 6. 安装依赖文件

    const installAnswers = await inquirer.prompt(installTool)

    await install({

        cwd: path.join(process.cwd(), name),

        package: installAnswers.package,

    }).then(() => {

        console.log(chalk.cyan('依赖安装完成'))

        console.log(chalk.cyan('cd'), name)

        console.log(`${chalk.cyan(`${installAnswers.package} start`)}`)

    })

    // 7. 清理文件

    const deleteDir = ['.git', 'docs']; // 需要清理的文件

    const pwd = shell.pwd();

    deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`));

};

export {

    initAction

}

总结:此文件大概做了以下几件事

  • 判断控制台是否安装了git,并对输入的一些字符做了校验。
  • 确定模版分支,由于本项目的前端模版是放在远端的git上,所以确定模版分支,还有另一种做法是将模版放到本工程中,这样做法的优点可以加快模版速度,但不好维护,模版文件修改脚手架要重新发布,这里并不推荐,而是将前端模版单独放到远程git上方便维护。
  • 通过inquirer读取模版操作指令,通过交付命令选择对应的前端模版并下载,目前提供两种前端模版,即React-Admin 和 Vue-Element-Admin。
  • 通过clone方法下载前端模版。
  • 模版下载成功后,向模版中的package.json文件中写入基础信息,比如项目名称(name)、项目描述(description)、作者(author)、项目code(stageCode)
  • 模版文件写入成功后,这时候可以安装模版所需要的依赖文件了
  • 清理文件:清理模版文件中的git信息及docs信息。
  1. 新建utils文件夹并新建clone文件,用于从远端下载模版文件
import download from 'download-git-repo';

import symbols from 'log-symbols'; // 用于输出图标

import ora from 'ora'; // 用于输出loading

import chalk from 'chalk'; // 用于改变文字颜色

const clone = function (remote, name, option) {

    const downSpinner = ora('正在下载模板...').start();

    return new Promise((resolve, reject) => {

        download(remote, name, option, err => {

            if (err) {

                downSpinner.fail();

                console.log(symbols.error, chalk.red(err));

                reject(err);

                return;

            };

        downSpinner.succeed(chalk.green('模板下载成功!'));

        resolve();

        });

   });

};

export {clone}
  1. 在utils下新建install文件,用于下载模版项目依赖
import spawn from 'cross-spawn'

export const install = async (options) => {

const cwd = options.cwd

return new Promise((resolve, reject) => {

    const command = options.package

    const args = ['install', '--save', '--save-exact', '--loglevel', 'error']

    const child = spawn(command, args, {

        cwd,

        stdio: ['pipe', process.stdout, process.stderr],

    })

    child.once('close', (code) => {

        if (code !== 0) {

        reject({ command: `${command} ${args.join(' ')}`,})

        return

    }

    resolve();

    })

    child.once('error', reject)

    })

}
  1. 在utils下新建constants文件,用于保存方法中用到的常量
import { readFile } from 'fs/promises';

const json = JSON.parse(await readFile(new URL('../package.json', import.meta.url)));

const { version } = json;

//当前 package.json 的版本号

export const VERSION = version;

// 拉去模版分支

export const BRANCH = 'master';

// 远端git地址

export const remoteUrlArr = [

    {

        url:'git@github.com:marmelab/react-admin.git',

        type:'react',

    },

    {

        url:'git@github.com:PanJiaChen/vue-element-admin.git',

        type:'vue',

    },
];
  1. 新建一个config文件夹并在此下面新建一个index文件,用于单独保存交互式命令
// 定义需要询问的问题

const questions = [

    {

        type: 'input',

        message: '请输入项目名称:',

        name: 'name',

        validate(val) {

            if (!val) return '模板名称不能为空!';

            if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return '模板名称包含非法字符,请重新输入';

                return true;

            }

    },

    {

        type: 'input',

        message: '请输入项目简介:',

        name: 'description'

    },
    // 项目编码可以不用,此 stageCode 是本公司统一系统注册用的

    {

        type: 'input',

        message: '请输入项目编码:',

        name: 'stageCode'

    },

    {

        type: 'input',

        message: '请输入项目作者:',

        name: 'author'

    },
    {

        type: 'list',

        message: '请选择项目类型:',

        choices: ['react','vue'],

        name: 'type'

    }

];

const installTool = {

    name: 'package',

    type: 'list',

    message: '请选择安装工具',

    choices: ['npm', 'yarn'],

    default: 'npm',

}

export {

    questions,

    installTool

}

到此,一个脚手架基本开发完成了,上述的两个功能已经完成,可以本地测试一下,在项目根目录下执行 npm link可以将du-template-cli命令链接到全局环境中,就可以用du-template-cli来执行相关命令了。

发布到npm

  1. npm adduser: 输入Username、Password、Email完成注册信息

  2. npm login 完成登陆认证

  3. npm publish 完成发布(注意:每次发布版本信息必须修改,即version信息)

  4. 发布完成后就可以通过全局安装命令来安装了,此脚手架已经上传到npm了(du-template-cli)