寫一個屬於你的前端腳手架工具

飛翔荷蘭人發表於2019-03-03

你可能用到過很多前端腳手架工具,有沒有試想過到底如何寫一個屬於你的腳手架呢?

腳手架依賴工具

commander.js 命令列工具

download-git-repo git倉庫程式碼下載

chalk 命令列輸出樣式美化

Inquirer.js 命令列互動

ora 命令列載入中效果

專案搭建

初始化專案

建立專案目錄後執行npm init按照提示完成初始化專案。

安裝依賴

安裝上面我們提到過的這幾個腳手架依賴工具,執行npm install chalk commander download-git-repo inquirer ora --save完成

安裝。

專案結構

專案初始化完成後,建立bin檔案和commands檔案。bin檔案為可執行命令入口目錄,commands則負責編寫一些命令互動。

bin 目錄下 react-cli 檔案

#!/usr/bin/env node
process.env.NODE_PATH = __dirname + `/../node_modules/`
const { resolve } = require(`path`)
const res = command => resolve(__dirname, `../commands/`, command)
const program = require(`commander`)

program.version(require(`../package`).version )

program.usage(`<command>`)

program.command(`init`)
  .option(`-f, --foo`, `enable some foo`)
  .description(`Generate a new project`)
  .alias(`i`)
  .action(() => {
    require(res(`init`))
  })

if(!program.args.length){
  program.help()
}
複製程式碼

建立該react-cli.js為可執行命令入口檔案,並且定義了一個`init`命令,執行命令後會去commands 目錄下尋找對應的init.js檔案

commands 目錄下 init 檔案

const {prompt} = require(`inquirer`)
const program = require(`commander`)
const chalk = require(`chalk`)
const download = require(`download-git-repo`)
const ora = require(`ora`)
const fs = require(`fs`)
const path = require(`path`)

const option =  program.parse(process.argv).args[0]
const defaultName = typeof option === `string` ? option : `react-project`
const tplList = require(`${__dirname}/../templates`)
const tplLists = Object.keys(tplList) || [];
const question = [
  {
    type: `input`,
    name: `name`,
    message: `Project name`,
    default: defaultName,
    filter(val) {
      return val.trim()
    },
    validate(val) {
      const validate = (val.trim().split(" ")).length === 1
      return validate || `Project name is not allowed to have spaces `;
    },
    transformer(val) {
      return val;
    }
  }, {
    type: `list`,
    name: `template`,
    message: `Project template`,
    choices: tplLists,
    default: tplLists[0],
    validate(val) {
      return true;
    },
    transformer(val) {
      return val;
    }
  }, {
    type: `input`,
    name: `description`,
    message: `Project description`,
    default: `React project`,
    validate (val) {
      return true;
    },
    transformer(val) {
      return val;
    }
  }, {
    type: `input`,
    name: `author`,
    message: `Author`,
    default: `project author`,
    validate (val) {
      return true;
    },
    transformer(val) {
      return val;
    }
  }
]
module.exports = prompt(question).then(({name, template, description, author}) => {
  const projectName = name;
  const templateName = template;
  const gitPlace = tplList[templateName][`place`];
  const gitBranch = tplList[templateName][`branch`];
  const spinner = ora(`Downloading please wait...`);
  spinner.start();
  download(`${gitPlace}${gitBranch}`, `./${projectName}`, (err) => {
    if (err) {
      console.log(chalk.red(err))
      process.exit()
    }
    fs.readFile(`./${projectName}/package.json`, `utf8`, function (err, data) {
      if(err) {
        spinner.stop();
        console.error(err);
        return;
      }
      const packageJson = JSON.parse(data);
      packageJson.name = name;
      packageJson.description = description;
      packageJson.author = author;
      var updatePackageJson = JSON.stringify(packageJson, null, 2);
      fs.writeFile(`./${projectName}/package.json`, updatePackageJson, `utf8`, function (err) {
        if(err) {
          spinner.stop();
          console.error(err);
          return;
        } else {
          spinner.stop();
          console.log(chalk.green(`project init successfully!`))
          console.log(`
            ${chalk.bgWhite.black(`   Run Application  `)}
            ${chalk.yellow(`cd ${name}`)}
            ${chalk.yellow(`npm install`)}
            ${chalk.yellow(`npm start`)}
          `);
        }
      });
    });
  })
})
複製程式碼

1.program.parse(process.argv) 可以解析執行init 時候傳入的引數, 我們可以拿到這個引數做為專案建立的目錄名,如果沒有傳入該引數則為其設定一個預設目錄名稱。

2. 命令列互動問答

  • question 陣列為互動命令配置,陣列中每一個物件都對應一個執行命令時候的一個問題
  • type為該提問的型別,name為該問題的名字,可以在後面通過name拿到該問題的使用者輸入答案
  • message為問題的提示
  • default則為使用者沒輸入時的預設為其提供一個答案
  • validate方法可以校驗使用者輸入的內容,返回true時校驗通過,若不正確可以返回對應的字串提示文案
  • transformer為使用者輸入問題答案後將對應的答案展示到問題位置,需要有返回值,返回到字串為展示內容
    具體使用文件

3. 問答結束的回撥

  • prompt方法中then裡的引數是一個物件,可以由此拿到問題由name定義的使用者輸入內容。
  • 根據使用者輸入的內容,可以對應為其生成下載模版,這裡使用download-git-repo工具來下載git倉庫程式碼
  • download方法第一個引數為要下載程式碼倉庫位置,如果為GitHub程式碼倉庫只需要寫使用者名稱和專案名稱即可,如`Hzy0913/react-template`即為下載該倉庫master的程式碼,如果需要切換對應分支則在倉庫地址後面加入對應分支名,如`Hzy0913/react-template#complete`
  • download方法第二個引數為生成下載檔案的檔名,我將他儲存在命令執行目錄下,檔名使用使用者輸入的名字,如引數為`./projectName`,即可在當前執行命令目錄下生成對應的檔名。
  • ora模組可以為我們生成下載時候的旋轉圖示,ora方法傳入的第一個引數為等待時候的提示文案並生成例項,在例項物件上呼叫start()方法開始出現旋轉動畫和提示,stop()方法停止。
  • 模版下載好以後需要為package.json檔案生成使用者自定義輸入的內容,node的fs模組的readFile方法可以幫助我們獲取生成檔案的內容,writeFile則可以寫入內容
  • 最後完成後可以在命令列皮膚上使用console方法給出一些提示內容,chalk 模組可以幫助我們美化輸出內容。

文章中腳手架示例程式碼可以見 build-react-cli

工具測試及釋出

  • 在寫的過程中免不了在我們本地進行測試,因為本身專案為node的工具,我們可以在專案目錄下執行 node bin/react-cli執行。
  • 當然在釋出以後肯定不能使用該命令,此時在釋出前,新增package.json中的bin物件,key為指令碼執行的名字,value為執行目錄,如"bin": {"build-react": "bin/react-cli"} ,即可在輸入build-react的時候等同於執行 node bin/react-cli命令,在我們全域性安裝腳手架的時候,bin物件裡面的內容即可變成全域性可執行命令。
  • 釋出npm包,npm包釋出非常簡單,註冊npm賬號後本地登入即可,在專案目錄下執行npm publish即可釋出,注意包名不能與現有的npm裡的相同、每次釋出新版本的包時需要修改package.json裡的版本號,釋出的包只有在24小時內可以刪除。

最後推薦兩個我寫的腳手架工具

build-react-cli是幫助你快速建立生成react專案的腳手架工具,配置了多種可選擇的不同型別專案模版。

LiveNode超簡單的前端跨域、前後端分離解決方案

相關文章