從零打造你的前端開發腳手架

南城FE發表於2023-03-13

在實際開發過程中,我們經常都會用到腳手架來構建前端工程專案,常見的主流框架都有自己的腳手架,vue-cli、create-react-app、angular-cli。在大型公司都會有內部定製化的腳手架開發工具,使用腳手架可以大幅提升專案的構建速度,透過命令列的互動,選擇你所需要的配置與整合,可快速完成初始專案的建立。

既然使用了這麼多腳手架建立專案,為何不自己實現一套屬於自己開發習慣的腳手架呢,本文將從0開始搭建一套腳手架開發工具。

什麼是腳手架

腳手架是一類快速形成工程化目錄的工具(command-line-interface, 縮寫:CLI),簡單來說,腳手架就是幫你減少「為重複性工作而做的重複性工作」的工具。

基於我們日常使用過的腳手架得知一個腳手架至少需要實現以下的功能點:

  • 有不同的命令執行操作,比如:
Commands:
  create [options] <app-name>
  add [options] <plugin> [pluginOptions]
  invoke [options] <plugin> [pluginOptions]
  inspect [options] [paths...]
  serve
  build
  ui [options]
  init [options] <template> <app-name>
  config [options] [value]
  outdated [options]
  upgrade [options] [plugin-name]
  migrate [options] [plugin-name]
  info
  help [command]
  • 可以透過命令列互動執行問答列表
? Please pick a preset:
❯ Default ([Vue 3] babel, eslint)
  Default ([Vue 2] babel, eslint)
  Manually select features
  • 最終根據使用者的問答結果生成對應的檔案

必備的知識庫

在完善構建腳手架前,需要引入一些腳手架構建中必須用到的工具庫。

  • commander 可以自定義一些命令列指令,在輸入自定義的命令列的時候,會去執行相應的操作
  • inquirer 可以在命令列詢問使用者問題,並且可以記錄使用者回答選擇的結果
  • fs-extra 是fs的一個擴充套件,提供了非常多的便利API,並且繼承了fs所有方法和為fs方法新增了promise的支援。
  • chalk 可以美化終端的輸出
  • figlet 可以在終端輸出logo
  • ora 控制檯的loading動畫
  • download-git-repo 下載遠端模板

實現過程

建立腳手架

新建一個資料夾,執行 npm init 生成package.json檔案。

npm init -y

建立bin資料夾,bin資料夾存放主要的命令程式入口檔案,並在資料夾下面新建index.js檔案,目錄結構如下:

fe   
├─ bin
│  ├─ index.js          
└─ package.json  

在package.json指定命令的入口檔案為剛剛新建的index.js,bin下面的fe為程式的快速啟動命令。

{
  "name": "fe",
  "version": "1.0.0",
  "main": "index.js",
  "bin": {
    "fe": "./bin/index.js"
  },
  ...
}

修改入口檔案index.js為如下內容,檔案以#!開頭代表這個檔案被當做一個執行檔案來執行,可以當做指令碼執行。後面的/usr/bin/env node代表這個檔案用node執行,node基於使用者安裝根目錄下的環境變數中查詢:

#! /usr/bin/env node

console.log('hello fe cli')

最後將當前命令連結到全域性,即可測試是否正常

npm link

此時在命令列中輸入剛剛的快速啟動命令fe,輸出了列印的日誌。說明執行了index.js中的程式碼,接下來就開始正式搭建腳手架的功能。

這裡我們可以額外擴充套件一下輸出的樣式,當然這個不是必要的,平時安裝依賴包的時候經常會看到不同顏色的輸出和LOGO,主要是用了chalkfiglet,示例程式碼如下:

program
  .on('--help', () => {
    console.log('\r\n' + chalk.white.bgBlueBright.bold(figlet.textSync('nanChengFE', {
      font: 'Standard',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 80,
      whitespaceBreak: true
    })));
    console.log(`\r\nRun ${chalk.cyan(`fe <command> --help`)} for detailed usage of given command\r\n`)
  })

最終輸出的樣式如下,具體的其他LOGO樣式和字型顏色可以看官方文件。

腳手架功能完善

基於commander執行自定義命令指令,具體的使用可以看相關文件,以下實現一個簡單的create命令,傳入引數並列印輸出日誌。

#! /usr/bin/env node
const { Command } = require('commander');
const program = new Command();

program
  .name('fe cli')
  .description('這裡是描述文案')
  .version('1.0.0');

program.command('create <name>')
  .description('建立一個新工程')
  .action((name) => {
    console.log('[ 工程名稱 ] >', name)
  });

program.parse(process.argv);

透過command定義create命令,並要求必傳引數name,再透過action回撥獲取到傳入的引數,後續即可透過使用者輸入的名稱建立專案。

接下來進入建立檔案的過程,在建立檔案的時候需要校驗當前目錄下是否已經存在,如果存在是否需要執行覆蓋的動作。

const path = require('path')
const fs = require('fs-extra')

// 當前命令列執行的目錄
const cwd  = process.cwd();
// 需要建立的目錄
const targetPath  = path.join(cwd, name)

// 目錄是否存在
if (fs.existsSync(targetPath)) {
  // 強制建立
  if (options.force) {
  } else {
    // 詢問使用者是否需要強制建立
  }
} else {
  // 目錄不存在正常建立
}

以上程式碼可以看出options.force命令引數可以直接進入到強制建立的過程。如果還有其他的想法想透過不同的指令做不同的處理,可以參考強制建立的邏輯進行其他的自定義。

上面提到了詢問使用者是否需要強制建立,這需要透過命令列與使用者進行互動,獲取到使用者選擇的資訊,inquirer這個包可以在命令列詢問使用者問題並拿到結果進行後續的邏輯互動。

let { action } = await inquirer.prompt([{
  name: 'action',
  type: 'list',
  message: '目錄已存在,請選擇:',
  choices: [{
    name: '覆蓋',
    value: 'overwrite'
  }, {
    name: '取消',
    value: false
  }]
}])

if (!action) {
  return;
} else if (action === 'overwrite') {
  // 移除已存在的目錄
  await fs.remove(targetPath)
}

接下來繼續詢問要選擇哪個模版,這個github有提供相關的介面,也可以自己維護模版配置的相關介面,核心就是讓使用者選擇需要的模版,比如模版裡有vue,react,小程式等不同場景或框架模版,使用者選擇模版後獲取到對應的遠端模版地址進行下載,這裡會用到兩個新的npm包,ora 可以在控制檯輸出loading動畫,download-git-repo 執行下載遠端模板。示例程式碼如下:

const spinner = ora('遠端倉庫開始下載...');
spinner.start();

// 本地存放地址
const tmp = path.join(
  process.cwd(),
  'templates',
  '工程目錄名稱',
);

// 已存在路徑執行刪除
if (fs.existsSync(tmp)) {
  await fs.remove(tmp)
}

// 下載遠端模版
download(
  '遠端倉庫地址',
  tmp,
  {
    clone: true,
  },
  (err) => {
    if (err) {
      spinner.fail('[ 遠端倉庫下載失敗 ]')
      logger.console.error();
    }
    spinner.succeed('[ 遠端倉庫下載成功 ]')
  }
)

操作過程如下:

如果只是做一個簡單版本的腳手架下載模版到這裡基本就差不多了,選擇對應的框架就下載對應的模版。但是實際開發過程中往往還不夠,不同的框架中還會不同的配置,比如使用vue框架時,是否需要自動加入vuex,vue-router等等。所以在不同的模版中我們還要進行下一步的使用者問詢配置,基於使用者回答的結果建立更符合的工程檔案。

這一段可以參考Vue-cli的原始碼,在不同的模板專案中增加相關的問詢配置,比如在Vue模版中新增了以下prompts配置:

  "name": {
    "type": "string",
    "required": true,
    "message": "填寫專案名稱"
  },
   "description": {
    "type": "string",
    "required": false,
    "message": "填寫專案描述",
    "default": "ZZZ前端vue專案"
  },
  ...

在執行結束下載檔案後繼續執行專案問詢配置列表,效果如下:

最後根據獲取到的使用者回答結果進行工程檔案的生成,比如將獲取到的工程名稱直接填充到模版內容中,根據使用者的選擇是否需要增加vuex的使用等。這塊主要會用到以下幾個依賴。

  • Metalsmith 靜態網站(部落格,專案)的生成器
  • handlerbars 模板編譯器,透過template和json,輸出一個html
  • consolidate 模板引擎整合庫

在vue_cli原始碼中註冊了2個渲染器,類似於vue中的 v-if v-else的條件渲染,這個我們可以根據實際需要擴充套件其他的條件渲染。

// register handlebars helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
  return a === b
    ? opts.fn(this)
    : opts.inverse(this)
})

Handlebars.registerHelper('unless_eq', function (a, b, opts) {
  return a === b
    ? opts.inverse(this)
    : opts.fn(this)
})

在模版檔案中使用對應的標識,結合模板編譯器即可動態生成內容,具體的構建細節有興趣的大家可以看看原始碼或者其他文章詳細的解析。

{{#if_eq userSelectVuex 'vuex'}}
import Vuex from 'vuex'
Vue.use(Vuex)
{{/if_eq}}

列舉這些是想說明如果要改造一個腳手架生成的模版內容我們直接可以找到對應的配置進行增加修改,在使用者問詢處增加需要的場景欄位,獲取到對應的值進行模版內容的動態處理。

總結

到這裡我們實現了一個簡單的開發腳手架,有自定義的快捷命令,可以進行模板選擇,可以根據不同的模板進行下一步的配置選擇,最終生成工程檔案。這只是一個簡單的實現,看vue腳手架的命令就可以看出還有很多功能點可以進一步完善,有興趣的同學可以深入研究。

到此本文就結束了,看完本文如果覺得有用,記得點個贊支援,收藏起來說不定哪天就用上啦~

專注前端開發,分享前端相關技術乾貨,公眾號:南城大前端(ID: nanchengfe)

相關文章