【寫個工做用的腳手架cli】用腳手架整合模板和配置

學習總結篇,以可否造輪子來衡量學習效果。本篇主要介紹近期使用的一個cli工具

腳手架cli的解決的問題

隨着公司各端的業務進行,前端方面會沉澱出一些通用的解決方案和模板。此時,統一維護和管理就很是有必要了。allen-cli就是基於這樣的場景而誕生的。javascript

這個項目腳手架,最終實現:整合各個模板,一鍵生成模板html

使用示例

目前實現的功能爲:前端

  1. 輸入allen init命令選擇一個腳手架模版進行下載,而後建立對應的app。
  2. 動態選擇構建環境,適配移動端等不一樣狀況。

allen-cli的具體流程

項目的總體結構:
vue

1. 建立項目

npm init建立package.json, 主要加上bin命令java

{
  "bin": {
    "allen": "bin/allen",
    "allen-init": "bin/allen-init"
  },
}

2. 解析參數

一個CLI須要經過命令行輸入各類參數,能夠直接用nodejs的process相關api進行解析,可是更推薦使用commander這個npm包能夠大大簡化解析的過程。node

#!/usr/bin/env node
const program = require('commander')

console.log('version', require('../package').version)

program
  .version(require('../package').version)
    .usage('<command> [項目名稱]')
    .command('init', '建立新項目')
    .parse(process.argv)

3. main主體流程

allen-initgit

// NODE moudle
//  node.js 命令行解決方案
const program = require("commander");

// node.js path模塊
const path = require("path");

// node.js fs模塊
const fs = require("fs");

// 常見的交互式命令行用戶接口的集合
const inquirer = require("inquirer");

// 使用shell模式匹配文件
const glob = require("glob");

// 活動最新的npm包
const latestVersion = require("latest-version");

// node.js 子進程
const spawn = require("child_process").spawn;

// node.js 命令行環境的 loading效果, 和顯示各類狀態的圖標
const ora = require("ora");

// The UNIX command rm -rf for node.
const rm = require("rimraf").sync;

async function main() {
  let projectRoot, templateName
  try {
    // 檢測版本
    let isUpate = await checkVersion();
    // 更新版本
    if (isUpate) await updateCli();
    // 檢測路徑
    projectRoot = await checkDir();
    // 建立路徑
    makeDir(projectRoot)
    // 選擇模板
    let { git } = await selectTemplate();
    // 下載模板
    templateName = await dowload(rootName, git);
    // 本地配置
    let customizePrompt = await getCustomizePrompt(templateName, CONST.CUSTOMIZE_PROMPT)
    // 渲染本地配置
    await render(projectRoot, templateName, customizePrompt);
    // 刪除無用文件
    deleteCusomizePrompt(projectRoot)
    // 構建結束
    afterBuild();
  } catch (err) {
    log.error(`建立失敗:${err.message}`)
    afterError(projectRoot, templateName)
  }
}

3.1 建立文件下載模板

建立文件和選擇模板github

// 建立路徑
function makeDir (projectRoot) {
  if (projectRoot !== ".") {
    fs.mkdirSync(projectName);
  }
}
/**
 * 模板選擇
 */
function selectTemplate() {
  return new Promise((resolve, reject) => {
    let choices = Object.values(templateConfig).map(item => {
      return {
        name: item.name,
        value: item.value
      };
    });
    let config = {
      // type: 'checkbox',
      type: "list",
      message: "請選擇建立項目類型",
      name: "select",
      choices: [new inquirer.Separator("模板類型"), ...choices]
    };
    inquirer.prompt(config).then(data => {
      let { select } = data;
      let { value, git } = templateConfig[select];
      resolve({
        git,
        // templateValue: value
      });
    });
  });
}

下載模板, 用的是download-git-repovue-cli

const download = require('download-git-repo')
const path = require('path')
const ora = require('ora')
const logSymbols = require("log-symbols");
const chalk = require("chalk");
const CONST = require('../conf/const')
module.exports = function (target, url) {
  const spinner = ora(`正在下載項目模板,源地址:${url}`)
  target = path.join(CONST.TEMPLATE_NAME)
  spinner.start()
  return new Promise((resolve,reject) => {
    download(`direct:${url}`,
    target, { clone: true }, (err) => {
      if (err) {
        spinner.fail()
        console.log(logSymbols.fail, chalk.red("模板下載失敗:("));
        reject(err)
      } else {
        spinner.succeed()
        console.log(logSymbols.success, chalk.green("模板下載完畢:)"));
        resolve(target)
      }
    })
  })
}

3.2 界面交互配置

採用的是inquirer的這個庫shell

// 常見的交互式命令行用戶接口的集合
const inquirer = require("inquirer");

3.3 本地配置

若是須要將一些配置放在本地文件,則能夠建立一些本地配置

/**
 * 
 * @param target 模板路徑
 * @param fileName 讀取文件名
 */
function getCustomizePrompt (target, fileName) {
  return new Promise ((resolve) => {
    const filePath = path.join(process.cwd(), target, fileName)
    if(fs.existsSync(filePath)) {
      console.log('讀取模板配置文件')
      let file = require(filePath)
      resolve(file)
    } else {
      console.log('該文件沒有配置文件')
      resolve([])
    }
  })
}

template.json

{
    type: "confirm",
    name: "mobile",
    message: "是否用於移動端?"
  },
  {
    type: "confirm",
    name: "flexible",
    message: "是否使用移動端適配?",
    when: function (answers) {
      return answers.mobile
    }
  },

4. 涉及到的node.js操做

// NODE moudle
//  node.js 命令行解決方案
const program = require("commander");

// node.js path模塊
const path = require("path");

// node.js fs模塊
const fs = require("fs");

// 常見的交互式命令行用戶接口的集合
const inquirer = require("inquirer");

// 使用shell模式匹配文件
const glob = require("glob");

// 活動最新的npm包
const latestVersion = require("latest-version");

// node.js 子進程
const spawn = require("child_process").spawn;

// node.js 命令行環境的 loading效果, 和顯示各類狀態的圖標
const ora = require("ora");

// The UNIX command rm -rf for node.
const rm = require("rimraf").sync;

5. 本地安裝使用

在項目目錄下運行npm i -g,註冊全局命令allen-cli便可使用

C:\Users\XX\AppData\Roaming\npm目錄下會生成相應的可執行文件:

6. npm包allen-cli

一個基本的腳手架CLI就完成了。

歡迎試用:npm i -g allen-cli

相關解析

!/usr/bin/env node

使用過Linux或者Unix的開發者,對於Shebang應該不陌生,它是一個符號的名稱,#!。這個符號一般在Unix系統的基本中第一行開頭中出現,用於指明這個腳本文件的解釋程序。瞭解了Shebang以後就能夠理解,增長這一行是爲了指定用node執行腳本文件

當你輸入一個命令的時候,npm是如何識別並執行對應的文件的呢?

具體的原理阮一峯大神已經在npm scripts 使用指南中介紹過。簡單的理解:

就是輸入命令後,會有在一個新建的shell中執行指定的腳本,在執行這個腳本的時候,咱們須要來指定這個腳本的解釋程序是node。
在一些狀況下,即便你增長了這一行,但仍是可能會碰到一下錯誤,這是爲何呢?

No such file or directory

爲了解決這個問題,首先須要瞭解一下/usr/bin/env。咱們已經知道,Shebang是爲了指定腳本的解釋程序,但是不一樣用戶或者不一樣的腳本解釋器有可能安裝在不一樣的目錄下,系統如何知道要去哪裏找你的解釋程序呢?

/usr/bin/env就是告訴系統能夠在PATH目錄中查找。

因此配置#!/usr/bin/env node, 就是解決了不一樣的用戶node路徑不一樣的問題,可讓系統動態的去查找node來執行你的腳本文件。
看到這裏你應該理解,爲何會出現No such file or directory的錯誤?由於你的node安裝路徑沒有添加到系統的PATH中。因此去進行node環境變量配置就能夠了。

NPM 執行腳本的原理

npm 腳本的原理很是簡單。每當執行npm run,就會自動新建一個 Shell,在這個 Shell 裏面執行指定的腳本命令。所以,只要是 Shell(通常是 Bash)能夠運行的命令,就能夠寫在 npm 腳本里面。

比較特別的是,npm run新建的這個 Shell,會將當前目錄的node_modules/.bin子目錄加入PATH變量,執行結束後,再將PATH變量恢復原樣。

參考連接

相關文章
相關標籤/搜索