手把手帶你擼一個cli工具

你有沒有遇到過在沒有vue-cli、create-react-app這樣子的腳手架的時候一個文件一個文件的去拷貝老項目的配置文件。最近,筆者就在爲組裏的框架去做一套基本的cli工具。通過這邊文章,筆者希望大家都能簡單的去實現一個屬於自己的腳手架工具。

原文鏈接: https://juejin.im/user/57ac15...

做好準備工作

首先,我們需要去新建一個項目並初始化package.json

mkdir my-cli && cd my-cli
npm init

然後我們需要在項目中新建bin文件夾,並將package.json中提供一個bin字段並指向我們的bin文件夾下,這樣通過npm我們就可以實現指令的軟鏈了。

"bin": {
  "mycli": "bin/mycli"
},

在mycli中,我們要在頭部增加這樣一句註釋,作用是"指定由哪個解釋器來執行腳本"

#!/usr/bin/env node

console.log('hello world');

接下來,全局安裝我們這個包,這樣我們就可以直接在本地使用mycli這個指令了。

sudo npm install -g

提供基本模版

既然我們要去做一個初始化項目的cli,那麼項目模版就必不可少了,筆者在這裏提前準備了一個demo的項目目錄模版,這裏就不展開贅述了。
demo項目模版

編寫邏輯

其實核心邏輯很簡單,就是通過控制檯獲取到用戶的一些自定義選項,然後根據選項去從本地或者遠程倉庫拿到我們提前準備好的模版,將配置寫入模版並最後拷貝模版到本地就行了。

我們在src下新增creator.js文件,這個文件導出一個Creator的類。在這個類中現在僅需要三個簡單的方法:init用於初始化、ask用於和命令行交互獲取用戶選擇輸入的數據、write用於調用模版的構建方法去執行拷貝文件寫數據的任務。

class Creator {
  constructor() {
    // 存儲命令行獲取的數據,作爲demo這裏只要這兩個;
    this.options = {
      name: '',
      description: '',
    };
  }
  // 初始化;
  init() {}
  // 和命令行交互;
  ask() {}
  // 拷貝&寫數據;
  write() {}
}

module.exports = Creator;

先去完善init方法,這個方法裏我們僅需要調用ask方法和命令行交互並做一些提示即可(可以通過chalk這個庫去豐富我們的命令行交互色彩)

// ...
init() {
  console.log(chalk.green('my cli 開始'));
  console.log();
  this.ask();
}
// ...

接下來是ask方法,在這個方法中,我們需要根據提示引導用戶輸入問題並獲取用戶的輸入,這裏用到inquirer這個庫來和命令行交互。

// ...
ask() {
  // 問題
  const prompt = [];

  prompt.push({
    type: 'input',
    name: 'name',
    message: '請輸入項目名稱',
    validate(input) {
      if (!input) {
        return '請輸入項目名稱!';
      }

      if (fs.existsSync(input)) {
        return '項目名已重複!'
      }

      return true;
    }
  });

  prompt.push({
    type: 'input',
    name: 'description',
    message: '請輸入項目描述',
  });

  // 返回promise
  return inquirer.prompt(prompt);
}
// ...

修改剛纔的init方法,將ask方法改爲Promise調用。

init() {
  console.log(chalk.green('my cli 開始'));
  console.log();
  this.ask().then((answers) => {
    this.options = Object.assign({}, this.options, answers);
    console.log(this.options);
  });
}

現在我們去命令行試一下,修改bin/mycli文件,然後去運行mycli命令。

#!/usr/bin/env node

const Creator = require('../src/creator.js');

const project = new Creator();

project.init();

執行結果

在和用戶交互完畢並獲取到數據後,我們要做的就是去調用write方法執行拷貝構建了。考慮到日後可能增加很多的模版目錄,不妨我們將每一類的模版拷貝構建工作放到模版中的腳本去做,從而增大可擴展性,新增template/index.js文件

接下來首先根據項目目錄結構創建文件夾(注意區分項目的執行目錄和項目目錄的關係)。

module.exports = function(creator, options, callback) {
  const { name, description } = options;

  // 獲取當前命令的執行目錄,注意和項目目錄區分
  const cwd = process.cwd();
  // 項目目錄
  const projectPath = path.join(cwd, name);
  const buildPath = path.join(projectPath, 'build');
  const pagePath = path.join(projectPath, 'page');
  const srcPath = path.join(projectPath, 'src');

  // 新建項目目錄
  // 同步創建目錄,以免文件目錄不對齊
  fs.mkdirSync(projectPath);
  fs.mkdirSync(buildPath);
  fs.mkdirSync(pagePath);
  fs.mkdirSync(srcPath);

  callback();
}

然後回到creator.js文件,在Creator中的write調用這個方法。

// ...
init() {
  console.log(chalk.green('my cli 開始'));
  console.log();
  this.ask().then((answers) => {
    this.options = Object.assign({}, this.options, answers);

    this.write();
  });
}

// ...

write() {
  console.log(chalk.green('my cli 構建開始'));
  const tplBuilder = require('../template/index.js');
  tplBuilder(this, this.options, () => {
    console.log(chalk.green('my cli 構建完成'));
    console.log();
    console.log(chalk.grey(`開始項目:  cd ${this.options.name } && npm install`));
  });
}
// ...

在開啓文件拷貝寫數據之前,我們需要用到兩個庫mem-fsmem-fs-editor,前者可以幫助我們在內存中創建一個臨時的文件store,後者可以以ejs的形式去編輯我們的文件。

現在constructor中初始化store。

constructor() {
  // 創建內存store
  const store = memFs.create();
  this.fs = memFsEditor.create(store);

  this.options = {
    name: '',
    description: '',
  };

  this.rootPath = path.resolve(__dirname, '../');
  this.tplDirPath = path.join(this.rootPath, 'template');
}

接下來在Creator中增加兩個方法copy和copyTpl分別用於直接拷貝文件和拷貝文件並注入數據。

getTplPath(file) {
  return path.join(this.tplDirPath, file);
}

copyTpl(file, to, data = {}) {
  const tplPath = this.getTplPath(file);
  this.fs.copyTpl(tplPath, to, data);
}

copy(file, to) {
  const tplPath = this.getTplPath(file);
  this.fs.copy(tplPath, to);
}

然後我們根據ejs的語法修改模版中的package.json文件以實現數據注入的功能

{
  "name": "<%= name %>",
  "version": "1.0.0",
  "description": "<%= description %>",
  "main": "index.js",
  "scripts": {},
  "author": "",
  "license": "ISC"
}

回到template/index.js中,對模版中的文件進行相應的拷貝和數據注入操作,最後打印一些可視化的信息。

module.exports = function(creator, options, callback) {
  const { name, description } = options;

  // 獲取當前命令的執行目錄,注意和項目目錄區分
  const cwd = process.cwd();
  // 項目目錄
  const projectPath = path.join(cwd, name);
  const buildPath = path.join(projectPath, 'build');
  const pagePath = path.join(projectPath, 'page');
  const srcPath = path.join(projectPath, 'src');

  // 新建項目目錄
  // 同步創建目錄,以免文件目錄不對齊
  fs.mkdirSync(projectPath);
  fs.mkdirSync(buildPath);
  fs.mkdirSync(pagePath);
  fs.mkdirSync(srcPath);

  creator.copyTpl('packagejson', path.join(projectPath, 'package.json'), {
    name,
    description,
  });

  creator.copy('build/build.js', path.join(buildPath, 'build.js'));

  creator.copy('page/index.html', path.join(pagePath, 'index.html'));

  creator.copy('src/index.js', path.join(srcPath, 'index.js'));

  creator.fs.commit(() => {
    console.log();
    console.log(`${chalk.grey(`創建項目: ${name}`)} ${chalk.green('✔ ')}`);
    console.log(`${chalk.grey(`創建目錄: ${name}/build`)} ${chalk.green('✔ ')}`);
    console.log(`${chalk.grey(`創建目錄: ${name}/page`)} ${chalk.green('✔ ')}`);
    console.log(`${chalk.grey(`創建目錄: ${name}/src`)} ${chalk.green('✔ ')}`);
    console.log(`${chalk.grey(`創建文件: ${name}/build/build.js`)} ${chalk.green('✔ ')}`);
    console.log(`${chalk.grey(`創建文件: ${name}/page/index.html`)} ${chalk.green('✔ ')}`);
    console.log(`${chalk.grey(`創建文件: ${name}/src/index.js`)} ${chalk.green('✔ ')}`);

    callback();
  });
}

執行mycli指令創建項目,一個簡單的cli就完成了。
執行指令

結語

到此,一個簡單的cli就製作完成了,大家可以參考vue-cli、create-react-app等優秀的cli適當的擴展自己的cli工具。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章