前端自動化:Node 命令列前端自動構建釋出系統

豬不樂意發表於2019-03-03

目前就我所呆的公司來說,前端的發版都是開發完之後執行編譯,然後通過 ftp 上傳到伺服器中。專案多起來之後,加上測試環境和正式環境的分離,導致管理混亂。而且整個流程也很麻煩,要一步步手動去做。

所以一直就有一個想法,能不能做一個像 Vue Cli 一樣的自動化工具,可以通過命令輸入命令和選擇選項,進行自動化的編譯和發版。說幹就幹立馬就開發了一個 ~~~

GitHub 地址: https://github.com/zuley/zuley-cli

前端自動化:Node 命令列前端自動構建釋出系統

一、技術棧

  • chalk 美化命令列,進行著色等
  • commander 解析使用者命令列輸入
  • inquirer 命令列互動功能,像使用者提問等。
  • node-ssh ssh 模組
  • ora 命令列環境的 loading 效果
  • shelljs 重新包裝了 child_process 子程式模組,呼叫系統命令更方便。

二、建立專案

  1. 直接使用 npm init 建立一個空專案
  2. 在根目錄建立一個不帶字尾的系統檔案 app,作為主入口檔案。

三、簡單命令列示例

在入口檔案中輸入以下程式碼

const program = require(`commander`)
const inquirer = require(`inquirer`)
const chalk = require(`chalk`)
program
  .command(`module`)
  .alias(`m`)
  .description(`建立新的模組`)
  .option(`-n, --name [moduleName]`, `模組名稱`)
  .action(option => {
    console.log(`Hello World`, option.name)
  })
    
program.parse(process.argv)
複製程式碼

執行看效果

$ node app m -n zuley // 輸出:Hello World zuley
複製程式碼

四、以全域性方式執行

上面的示例輸入起來太麻煩,需要進入專案所在目錄才能執行檔案,現在我們需要一個簡單的方式 zuley m -n zuley

1、配置 package.json 中的 bin 欄位

"bin": {
    "zuley": "app"
}
複製程式碼

2、註冊全域性命令

然後執行註冊符號連結,它會把 zuley 這個欄位複製到 npm 全域性模組安裝資料夾 node_modules 內,也就是將 zuley 的路徑加入環境變數 PATH 中。

如果是MAC,則需要加上 sudo 字首,使用管理員許可權。

本文中所有使用了 sudo 的地方,均是MAC系統限制,win使用者需要刪除此句。

評論有人反饋說不加 sudo 也可以,可以先嚐試不加,如果有報錯再加。

$ npm link

# or mac
$ sudo npm link
複製程式碼

3、宣告為可執行應用

在入口檔案的最上方加入宣告,宣告這是一個可執行的應用。

#! /usr/bin/env node

...code
複製程式碼

四、執行命令

$ zuley m -n zuley
複製程式碼

五、專案架構

實際開發中,我們以 src為原始碼目錄,自動釋出系統做為其中的一個模組,放置在模組目錄中。

|-/src/ // 原始碼目錄
|---/modules/ 模組目錄
|-----/Automation/ // 釋出模組目錄
|-------index.js // 模組入口檔案
|-------config.promps.js  // 選項配置檔案
|-------config.service.js // 伺服器配置檔案
|-app  // 入口檔案
複製程式碼

六、在入口檔案 app 中載入自動釋出系統模組

#! /usr/bin/env node

// 匯入自動化任務模組
require(`./src/modules/Automation/index`)
複製程式碼

七、編寫模組入口功能

編寫模組入口檔案 /src/modules/Automation/index.js

1、命令列詢問選項

執行命令之後,我們需要提供命令列介面,讓使用者輸入或者選擇選項來確定接下來的操作。這裡用到了 inquirer 包。

具體用例請去 npm 或者自行搜尋學習。

以下是程式碼片段

// 匯入選項配置
const prompsConfig = require(`./config.promps`)

// 專案名稱
let { name } = await inquirer.prompt(prompsConfig.name)
// 專案渠道
let source = ``
if (prompsConfig.source[name].length > 0) {
  source = await inquirer.prompt(prompsConfig.source[name])
  source = source.source
}
// 專案環境
let { type } = await inquirer.prompt(prompsConfig.type)
    
// 確認選項
log(`請確認你選擇了以下選項`)
log(chalk.green(`專案名稱:`) + chalk.red(TEXTDATA[name]))
log(chalk.green(`專案渠道:`) + chalk.red(TEXTDATA[source]))
log(chalk.green(`專案環境:`) + chalk.red(TEXTDATA[type]))
複製程式碼
前端自動化:Node 命令列前端自動構建釋出系統

2、執行shell命令編譯專案

這裡使用了 shelljs 包。shelljs 重新包裝了 child_process 子程式模組,呼叫系統命令更方便。

本文中所有使用了 sudo 的地方,均是MAC系統限制,win使用者需要刪除此句。

以下是程式碼片段

async function compile (config, type) {
  // 進入專案本地目錄
  shell.cd(config.localPath)
  if (type === `TEST`) {
    log(`測試環境編譯`)
    shell.exec(`sudo npm run test`)
  } else {
    log(`正式環境編譯`)
    shell.exec(`npm run build`)
  }
  log(`編譯完成`)
}
複製程式碼

3、上傳檔案

上傳檔案使用了 node-ssh包,該包封裝了一些簡單易用的方法,具體可以查詢官網或者搜尋教程。

以下是程式碼片段

/**
 * 上傳檔案
 * @param {Object} config 專案配置
 */
async function updateFile (config) {
  // 儲存失敗序列
  let failed = []
  // 儲存成功序列
  let successful = []
  let spinner = ora(`準備上傳檔案`).start()
  // 上傳資料夾
  let status = await ssh.putDirectory(config.localPath + `/dist`, config.remotePath, {
    // 遞迴
    recursive: true,
    // 併發數
    concurrency: 10,
    tick (localPath, remotePath, error) {
      if (error) {
        failed.push(localPath)
      } else {
        spinner.text = `正在上傳檔案:` + localPath
        successful.push(localPath)
      }
    }
  })
  spinner.stop()
  if (status) { 
    log(chalk.green(`完成上傳`))
  } else {
    log(chalk.red(`上傳失敗`))
  }
  if (failed.length > 0) {
    log(`一共有${chalk.red(failed.length)}個上傳失敗的檔案`)
    log(failed)
  }
}
複製程式碼

本例只做簡單的演示,實際應用還需要擴充套件上傳失敗的處理。比如斷點續傳,失敗檔案續傳等。

而且單個上傳會很慢,可以先執行命令壓縮後再上傳,再執行下伺服器命令解壓檔案。

如果上傳失敗,檢查遠端目錄是否有許可權,使用命令修改許可權。

$ chmod -R 777 [目錄]
複製程式碼

4、原始碼

1、index.js

const program = require(`commander`)
const inquirer = require(`inquirer`)
const chalk = require(`chalk`)
const ora = require(`ora`)
const shell = require(`shelljs`)
const node_ssh = require(`node-ssh`)
const ssh = new node_ssh()

// 匯入選項配置
const prompsConfig = require(`./config.promps`)
// 匯入伺服器配置
const serviceConfig = require(`./config.service`)
const log = console.log

// 欄位字典
const TEXTDATA = {
  `A`: `專案A`,
  `B`: `專案B`,
  `PC`: `PC 網站`,
  `WX`: `微信公眾號`,
  `TEST`: `測試環境`,
  `PROD`: `正式環境`,
  ``: `其他`
}

// 新增一個名字為 a 別名為 automation 的命令模組
program
  .command(`a`)
  .alias(`automation`)
  .description(`前端自動化釋出系統`)
  .action(async option => {
    // 專案名稱
    let { name } = await inquirer.prompt(prompsConfig.name)
    // 專案渠道
    let source = ``
    if (prompsConfig.source[name].length > 0) {
      source = await inquirer.prompt(prompsConfig.source[name])
      source = source.source
    }
    // 專案環境
    let { type } = await inquirer.prompt(prompsConfig.type)
    
    // 確認選項
    log(`請確認你選擇了以下選項`)
    log(chalk.green(`專案名稱:`) + chalk.red(TEXTDATA[name]))
    log(chalk.green(`專案渠道:`) + chalk.red(TEXTDATA[source]))
    log(chalk.green(`專案環境:`) + chalk.red(TEXTDATA[type]))
    
    // 獲取配置
    let config = serviceConfig[`${name}-${source}-${type}`]

    log(`使用伺服器配置:${name}-${source}-${type}`)

    // 編譯專案
    compile(config, type)

    // 連線伺服器
    await ConnectService(config)

    // 上傳檔案
    await updateFile(config)
  })

program.parse(process.argv)

/**
 * 連線伺服器
 * @param {Object} config 專案配置
 */
async function ConnectService (config) {
  log(`嘗試連線服務:` + chalk.red(config.service.host))
  let spinner = ora(`正在連線`)
  spinner.start()
  await ssh.connect(config.service)
  spinner.stop()
  log(chalk.green(`成功連線到伺服器`))
}

/**
 * 上傳檔案
 * @param {Object} config 專案配置
 */
async function updateFile (config) {
  // 儲存失敗序列
  let failed = []
  // 儲存成功序列
  let successful = []
  let spinner = ora(`準備上傳檔案`).start()
  // 上傳資料夾
  let status = await ssh.putDirectory(config.localPath + `/dist`, config.remotePath, {
    // 遞迴
    recursive: true,
    // 併發數
    concurrency: 10,
    tick (localPath, remotePath, error) {
      if (error) {
        failed.push(localPath)
      } else {
        spinner.text = `正在上傳檔案:` + localPath
        successful.push(localPath)
      }
    }
  })
  spinner.stop()
  if (status) { 
    log(chalk.green(`完成上傳`))
  } else {
    log(chalk.red(`上傳失敗`))
  }
  if (failed.length > 0) {
    log(`一共有${chalk.red(failed.length)}個上傳失敗的檔案`)
    log(failed)
  }
}

/**
 * 編譯原始碼
 * @param {Object} config 專案配置
 * @param {String} type 編譯型別 TEST or PROD
 */
async function compile (config, type) {
  // 進入專案本地目錄
  shell.cd(config.localPath)
  if (type === `TEST`) {
    log(`測試環境編譯`)
    shell.exec(`sudo npm run test`)
  } else {
    log(`正式環境編譯`)
    shell.exec(`sudo npm run build`)
  }
  log(`編譯完成`)
}
複製程式碼

2、選項配置原始碼:config.promps.js

此檔案配置使用者在命令列中輸入或者選擇的資料,供後續拼接生成 A-WX-TEST 類的欄位。

A-WX-TEST 代表使用者選擇了:專案A-微信-測試環境

程式將通過此拼接欄位,去 config.service.js 中獲取專案配置

/**
 * 自動化模組 - 選項配置檔案
 */
module.exports = {
  // 專案名字
  name: [
    {
      type: `list`,
      name: `name`,
      message: `請選擇要釋出的專案`,
      choices: [
        {
          name: `專案A`,
          value: `A`
        },
        {
          name: `專案B`,
          value: `B`
        }
      ]
    }
  ],
  // 專案渠道
  source: {
    `A`: [
      {
        type: `list`,
        name: `source`,
        message: `請選擇要釋出的渠道`,
        choices: [
          {
            name: `PC網站`,
            value: `PC`
          },
          {
            name: `微信`,
            value: `WX`
          }
        ]
      }
    ],
    `B`: [
      {
        type: `list`,
        name: `source`,
        message: `請選擇要釋出的渠道`,
        choices: [
          {
            name: `PC網站`,
            value: `PC`
          },
          {
            name: `微信`,
            value: `WX`
          }
        ]
      }
    ]
  },
  // 專案環境
  type: [
    {
      type: `list`,
      name: `type`,
      message: `請選擇釋出環境`,
      choices: [
        {
          name: `測試環境`,
          value: `TEST`
        },
        {
          name: `正式環境`,
          value: `PROD`
        }
      ]
    }
  ]
}
複製程式碼

3、伺服器配置原始碼:config.service.js

伺服器配置只配置了兩個作為演示,實際看現實情況補充。

GitHub 上拉取專案測試的時候,記得一定要修改次檔案。

  1. 修改伺服器為自己的伺服器和ssh賬號密碼
  2. 修改專案的本地目錄和遠端目錄
  3. A-WX-TEST 這個欄位代表使用者輸入的選項,具體看 config.promps.js
// 伺服器 A
const serviceA = {
  // 伺服器 IP
  host: `xxx.xxx.xxx.xxx`,
  // ssh 賬號
  username: `xxx`,
  // ss 密碼
  password: `xxxxxx`
}

module.exports = {
  // 專案A,微信測試環境
  `A-WX-TEST`: {
    service: serviceA,
    // 本地目錄
    localPath: `/Users/zuley/ChuDaoNew/minshengjingwu-h5`,
    // 遠端目錄
    remotePath: `/root/html/test`
  },
  // 專案A,微信正式環境
  `A-WX-PROD`: {
    service: serviceA,
    // 本地目錄
    localPath: `/Users/zuley/ChuDaoNew/minshengjingwu-h5`,
    // 遠端目錄
    remotePath: `/root/html/prod`
  }
}

複製程式碼

八、執行自動化釋出

執行以下命令即可啟動自動化

$ zuley a
複製程式碼

輸入有誤想終止命令可輸入

$ Ctrl+C
複製程式碼

九、參考文章

GitHub 地址: https://github.com/zuley/zuley-cli

跟著老司機玩轉Node命令列

chalk:美化命令列,進行著色

commander:解析使用者命令列輸入

inquirer:命令列互動功能,像使用者提問等

node-ssh:ssh 模組

ora:命令列環境的 loading 效果

shelljs:更方便的呼叫系統命令

相關文章