1. 程式人生 > 其它 >從零開始搭建前端腳手架

從零開始搭建前端腳手架

一、功能設計

每個前端小組都會有自己的獨特的業務場景,從這些業務場景從提取公共部分,並打造一個前端專案模版,是非常有必要的

為了能夠基於這個專案模版快速建立一個新專案,就需要腳手架工具登場

所以這裡至少有兩個專案倉庫:前端模版專案、腳手架工具

而對於腳手架工具,它應當具備這樣的功能:輸入一個命令和專案名稱,建立對應的專案目錄,其內容就是模版專案

my-cli create my-project

基於此,腳手架工具的內部邏輯也就很清晰了:

建立一個 create 命令,其行為是拉取模版專案的程式碼(用 git clone 即可實現)

為了實現這個命令,我們需要藉助一個神奇的工具:commander

二、Commander

Commander.js 是完整的 node.js 命令列解決方案

可以通過它建立一個 program 物件,作為程式的主體

const { Command } = require('commander');
const program = new Command();
program.version('0.0.1');

commander 常用的功能有建立選項和命令


選項 option 需要基於短線 " - " 宣告,常見的-h-V 都是選項

每個選項可以定義一個短選項名稱(" - " 後面接單個字元)和一個長選項名稱(" -- "後面接一個或多個單詞)

解析後的選項可以通過.opts()

方法獲取,從而執行對應的操作

比如建立一個-d選項:

program
  .option('-d, --debug', 'output extra debugging')

program.parse(process.argv);

const options = program.opts();
if (options.debug) console.log(options);

還可以對選項定義引數及預設值:

program
  .option('-d, --debug <type>', 'output extra debugging', 'hard')

這裡就對 debug 選項定義了 type 引數,並設定其預設值為 hard

用尖括號 <>定義的引數為必填,用中括號 [] 建立的引數為選填


命令 command 可以通過.command()建立,並通過鏈式呼叫.description().action()定義該命令的描述和具體行為

program
  .command('create <name> [type]')
  .description('create a new project')
  .action((name, type) => {
    console.info('project', name);
  });

和選項類似,命令也可以通過 <> [] 定義引數

除了像上面那樣通過鏈式呼叫建立命令列為(description、action)之外,還可以用單檔案的形式描述命令,不過我沒有跑通


commander 還有更多更強大的功能,這裡就不逐一介紹了,詳情可以參考中文文件

三、正式開始

學習了 commander 之後,就可以著手腳手架的開發了

首先建立腳手架工具的目錄,如 my-cli,然後在目錄下建立一個.gitignore 檔案

# Dependency directories
node_modules/

接著通過npm init建立package.json

建立之後可以刪除其中的 main 和 scripts,然後安裝commander

npm install commander --save

然後建立 bin 目錄及 bin/cli.js 檔案:

#!/usr/bin/env node

const { Command } = require('commander');
const { name, version } = require('../package.json');
const program = new Command();

program.name(name).version(version);

program
  .command("create <project-name>")
  .description("create a new project")
  .action((name) => {
    console.info('project', name);
  });

program.parse();

頂部的#!/usr/bin/env node 是告訴作業系統,用/usr/bin 下的 node 來執行這個指令碼

一個簡單的 cli 命令就建立好了,可以回到根目錄,用 node 執行 cli.js 試試:

node bin/cli.js create my-project

可以看到 create 命令正常執行

但直接通過 node + 路徑 的形式執行程式碼還是太死板了,可以通過npm link將專案掛到全域性,就能像正常的腳手架工具那樣用了

首先需要在 package.json 裡新增 bin 命令:

{
  ...
  "bin": {
    "my-cli": "bin/cli.js"
  }
}

這裡的 my-cli 是包的名稱,後面的路徑需要寫全字尾

然後在根目錄執行npm link就能將 my-cli 連結到全域性

npm link 是一個很好的本地測試的手段,除錯完成後可以通過npm unlink解除安裝

四、完善 create 命令

據文件介紹, commander 支援將命令拆成單檔案進行維護,但我沒有搞出來,最後只好加了一層 map 來拆檔案

在根目錄下新建一個command 目錄,並建立command/create.js 和command/index.js 兩個檔案:

然後將 create 命令的相關邏輯移到 command/create.js 中

// create.js

function createAction(name) {
  console.log('project', name);
}

const create = {
  alias: 'c',
  params: '<project-name>',
  description: 'create a new project',
  action: createAction,
}

module.exports = create;

並在 command/index.js 中匯出

// index.js

const create = require('./create.js');

module.exports = {
  create,
};

最後來改造 bin/cli.js

#!/usr/bin/env node

const { Command } = require('commander');
const { name, version } = require('../package.json');
const commands = require('../command/index.js');
const program = new Command();

program.name(name).version(version);

// 建立命令
Reflect.ownKeys(commands).map((name) => {
  const { params, alias, action, description } = commands[name] || {};
  program.command(`${name} ${params || ''}`) 
    .alias(alias) 
    .description(description) 
    .action((...args) => {
      typeof action === 'function' && action(...args);
    })
});

program.parse(process.argv);

專案的結構基本成型,接下來完善 create 命令

在文章的開頭就已經分析過了,create 命令只需要做一個事情,就是將專案 clone 到當前目錄

const { exec } = require("child_process");

function createAction(name) {
  // 這是模板專案的倉庫地址
  const url = "http://github.com/xxx/template.git";
  // 克隆專案
  exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
    if (error) {
      console.log(error);
      process.exit();
    }
    console.log("Success");
    process.exit();
  });
}

如果模板專案是 github 上的專案,應該沒什麼大問題

而如果模板專案是小組內部的 gitlab 專案,就需要放開模板專案的訪問許可權,至少讓小組人員都能夠訪問

五、優化體驗

在完成了 create 命令之後,我們的腳手架工具就可以算是開發完成

但為了更好的體驗,可以藉助這些工具加以改造:chalkinquirer

1. chalk

它可以在命令列列印彩色文字:

import chalk from 'chalk';

const error = chalk.bold.red;
const warning = chalk.hex('#FFA500');

console.log(chalk.blue('Hello world!'));
console.log(error('Error!'));
console.log(warning('Warning!'));

2.inquirer

這是一個讓使用者與命令列互動的工具

它提供了很多 api,讓使用者可以在程式執行的過程中輸入內容,從而影響程式執行的結果

const inquirer = require('inquirer');

inquirer
  .prompt([
    /* Pass your questions in here */
  ])
  .then((answers) => {
    console.log('success', answers);
  })
  .catch((error) => {
    console.log('error');
  });

在上面的prompt 中配置需要使用者輸入/選擇的(表單)內容

比如讓使用者輸入專案名稱:

.prompt([
  {
    type: "input",
    message: "專案名稱:",
    name: "projectName",
    validate: (val) => {
      // 對輸入的值做判斷
      if (!val || !val.trim()) {
        return chalk.red("專案名不能為空,請重新輸入");
      } else if (val.includes(" ")) {
        return chalk.red("專案名不能包含空格,請重新輸入");
      }
      return true;
    },
  },
])

除了這裡的 input 型別外,inquirer 還提供了很多互動型別,如單選列表list、多選checkbox、確認項confirm 等


在瞭解了 chalk 和inquirer 之後,我們就可以進一步改造 create 命令

npm install inquirer chalk --save
// create.js
const inquirer = require("inquirer");
const chalk = require("chalk");
const { exec } = require("child_process");

function createProject(name) {
  // 這是模板專案的倉庫地址
  const url = "[email protected]:wisewrong/chart-admin.git";
  exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
    if (error) {
      console.log(chalk.red(error));
      process.exit();
    }
    console.log(chalk.green("Success"));
    process.exit();
  });
}

const create = {
  alias: "c",
  params: "[project-name]",
  description: "create a new project",
  action: (project) => {
    project
      ? createProject(project)
      : inquirer
          .prompt([
            {
              type: "input",
              message: "專案名稱:",
              name: "projectName",
              validate: (val) => {
                // 對輸入的值做判斷
                if (!val || !val.trim()) {
                  return chalk.red("專案名不能為空,請重新輸入");
                } else if (val.includes(" ")) {
                  return chalk.red("專案名不能包含空格,請重新輸入");
                }
                return true;
              },
            },
          ])
          .then((answer) => {
            createProject(answer.projectName);
          });
  },
};

module.exports = create;

麻雀雖小五臟俱全,這樣一個簡單的腳手架就開發完了

如果需要釋出到 npm,可以參考我之前的文章《vue-cli 3.x 開發外掛併發布到 npm》

後面我也會繼續完善這個腳手架,新增更多的互動,以及展示進度條(專業挖坑,從來不填...)