如何开发一个 Vue3-cli 的项目初始化程序并发布到 npm 中

499 阅读4分钟

最近有要开发一个新的 Vue3 的需求,过往总是用 vitevue-cli 新建一个项目,太多内容需要重新配置,效率未免太低,为了简化以后的开发,同时也尝试统一一下开发的项目架构,于是自己开发了一个 cli —— xmo-cli

npm地址 www.npmjs.com/package/xmo…

gitee地址 gitee.com/dXmo/xmo-cl…

思路

其实开发一个 cli 的程序难度并不大,实际上就是用命令行提问,然后根据回答执行相应的事宜。作为一个项目初始化的程序,有两种选择,一种是写好模板,再根据 cli 的问答来初始化模板;另一种是直接先写好一个初始的项目,根据 cli 的回答来 clone 不同的项目。

由于我初始化之后的项目规模比较大,所以我选择的是后者。

然后就是选择开发要用到的库,经过整理,开发一个 cli 需要如下库支持,

  • chalk - 自定义命令行 console.log ,改改命令行输出的字体颜色和背景颜色之类的。
  • commander - 读取命令行的参数,例如 rm -rf ./* 这里的 -rf./*就是我所谓的参数。
  • download-git-repo - clone git 项目。
  • inquirer - 在命令行中提问并收集回答。
  • ora - 显示加载中的 loading 效果。

项目规划

准备好库,就能开始

首先新建项目

npm init

得到 package.json ,接着创建目录 binsrcbin 是最终启动的程序的起点,而 src 是其中的抽离出来的细节逻辑。

bin 中创建程序 xmo-cli.mjs ,因为我想要用 ES6 的语法写,所以添加了后缀 .mjs 如果你打算用 require 的形式写则没有这个必要(即直接创建 xmo-cli 不要后缀)。

然后修改 package.json ,添加两个字段。

{
  ...
  "type": "module",
  "bin": {
    "xmo-cli": "./bin/xmo-cli.mjs"
  },
  ...
}

然后对 xmo-cli.mjs 稍作修改以做测试。(第一行指定运行这个程序所用的程序)

#!/usr/bin/env node

console.log('Hello Cli');

这个时候你已经可以运行这个 bin 程序了

./bin/xmo-cli.mjs

也可以在项目根目录的命令行里输入

npm install -g

然后你就可以全局调用 xmo-cli 了。

xmo-cli
# Hello Cli

接着安装相关依赖

npm install chalk commander download-git-repo inquirer ora

具体实现

具体实现中的难点在于学习 commanderinquirer ,而 chalkora download-git-repo 的使用是简单的。如果有不理解的地方,直接看官方文档就好。

除了这几个外部调用的包之外,还用到了自带的 fspath。下面是代码

/bin/xmo-cli.mjs

#!/usr/bin/env node
import commander from 'commander';
import { readFile } from 'fs/promises';

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

const _version = pack.version;
commander.version(_version);

commander
  .command('init [dir]')
  .alias('i')
  .description('vue admin 项目初始化工具')
  .action((dir) => {
    init(dir);
  });

commander.parse(process.argv);

/src/init.js

import { promises } from 'fs';
import Creator from './creator.js';

export const remote = 'direct:https://gitee.com/dXmo/xmo-cli.git';

export function isDirEmpty(dirname) {
  return promises.readdir(dirname).then((files) => {
    return files.length === 0;
  });
}

const init = async (dir) => {
  const project = new Creator();
  await project.init(dir);
};

export default init;

/src/creator.js

import chalk from 'chalk';
import inquirer from 'inquirer';
import { existsSync } from 'fs';
import { remote, isDirEmpty } from './init.js';
import clone from './clone.js';
import { readFile, writeFile } from 'fs/promises';
import path, { dirname } from 'path';

class Creator {
  constructor() {
    // 存储命令行获取的数据,作为demo这里只要这两个;
    this.options = {
      name: '',
      description: '',
    };
  }
  // 初始化;
  async init(dir) {
    console.log(chalk.blueBright('Xmo-cli start creating project'));
    if (dir) {
      console.log(`将在${chalk.green(dir)}文件夹下创建项目。`);
      if (existsSync(dir) && !(await isDirEmpty(dir))) {
        console.log(`${chalk.green(dir)}${chalk.red('目录不为空。')}`);
        return;
      }
    }
    try {
      const { name, description, type } = await this.ask(dir);
      this.options.name = name;
      this.options.description = description;
      if (!dir) {
        dir = name;
        console.log(`将在${chalk.green(dir)}文件夹下创建项目。`);
        if (existsSync(name) && !(await isDirEmpty(name))) {
          console.log(`${chalk.green(dir)}${chalk.red('项目目录不为空。')}`);
          return;
        }
      }
      if (name) {
        await clone(remote + '#' + type, dir);
        await this.write(dir);
        console.log(chalk.greenBright('Success!'), '运行代码,请执行如下指令');
        console.log(chalk.greenBright('   cd ' + dir));
        console.log(chalk.greenBright('   yarn'));
        console.log(chalk.greenBright('   yarn dev'));
      } else {
        console.log(chalk.red('程序提前结束。'));
      }
    } catch (error) {
      console.log(chalk.red(error));
    }
  }
  // 和命令行交互;
  async ask(dir) {
    const initQuestions = (dir) => [
      {
        type: 'input',
        name: 'name',
        message: '请输入项目名称',
        default: dir,
        validate(input) {
          if (!input) {
            return '请输入项目名称!';
          }
          return true;
        },
      },
      {
        type: 'input',
        name: 'description',
        message: '请输入项目描述',
      },
      {
        type: 'list',
        name: 'type',
        message: '请选择项目类型',
        choices: ['mini'],
      },
    ];

    // 返回promise
    return inquirer.prompt(initQuestions(dir));
  }
  // 写数据;
  async write(dir) {
    const pack = JSON.parse(await readFile(path.join(dir, 'package.json')));
    Object.assign(pack, this.options);
    await writeFile(
      path.join(dir, 'package.json'),
      JSON.stringify(pack, null, 2)
    );
  }
}

export default Creator;

/src/clone.js

克隆 git 代码

import { promisify } from 'util';
import ora from 'ora';
import chalk from 'chalk';
import downloadGit from 'download-git-repo';
const download = promisify(downloadGit);

const clone = async function (repo, dir, options = { clone: true }) {
  const process = ora(`开始下载 ${chalk.blue(repo)}`);
  process.start();
  process.color = 'yellow';
  process.text = `正在下载..... ${chalk.yellow(repo)} `;

  try {
    await download(repo, dir, options);
    process.color = 'green';
    process.text = `下载成功 ${chalk.green(repo)} `;
    process.succeed();
  } catch (error) {
    console.log(error);
    process.color = 'red';
    process.text = '下载失败';
    process.fail();
  }
};

export default clone;

总结

代码很短,都是一些很简单的 IO ,我参考了这两个项目

github.com/LNoe-lzy/my…

github.com/l-x-f/vea-c…

成果

image-20210924142527238.png

因为目前暂时只写了一个初始化项目,所以只能初始化构建一种项目类型,后期会再添加。

发布到npm

首先需要在 npm官网 注册一个账号并通过邮箱验证,然后在命令行登录并在项目根目录 publish 就可以发布了。

npm login
npm publish

image-20210924143050889.png

记得写好 README.md 文件,提供一些使用指南。