如何開發一個可愛的CLI(二)

ULIVZ發表於2018-03-27

本文首發於個人 Github,歡迎 issue / fxxk。

前言

在系列的上一篇《如何開發一個可愛的CLI(一)》中,我給大家講述瞭如何開發一個生成、渲染、轉換樣板檔案(Boilerplate)的簡單腳手架工具。本文,將是愉快的進階環節 —— 如何基於webpack寫一個 “零配置” 的命令列工具(暫且命名為lovely-cli.),實現以下功能:

lovely-cli dev    # 以開發環境,webpack watch的模式啟動一個應用
lovely-cli build  # 以生產環境的模式構建應用
複製程式碼

首先,我要再次強調一個概念。儘管我在第一篇中我有備註:

“由於腳手架的英文 scaffolding 太長,本文我將以更可愛的 CLI 來代替。”
複製程式碼

但仍然有些同學提出疑問(有一位同學回覆我是因為習慣性地滑的太快,所以沒看到...),所以在第二篇的開始,我再次對CLIScafollding的概念全面闡述一次:

  1. CLI,其全稱是 command-line interface,也就是命令列介面。你我常用的git,算是比較著名的一個命令列工具了(你若是用source tree黨,當我沒說):

如何開發一個可愛的CLI(二)

  1. 腳手架,其英譯是 Scafollding,關於它的概念,請首先看這張圖:

如何開發一個可愛的CLI(二)

沒錯,這就是腳手架,在建築領域,無論是大工程還是小工程,都需要各式各樣的腳手架,腳手架工程師首先搭好架子,然後工人們慢慢往裡面堆砌磚頭。

如果你只是建1層樓的平房,你可能只需要一個梯子足矣;如果是10層,上圖的腳手架可能也足夠了;但如果是50層、100層,你的腳手架對結構、承重、安全性將會有更多的考慮(PS:你至少得加個電梯吧。)

回到程式設計,為什麼腳手架這個概念開始在前端領域興起呢?說到底,都是源於工程化的崛起。隨著經濟的發展和人民生活水平的提高,富足的人民群眾,對軟體的體驗需求越來越高,前端的頁面也越來越複雜,十幾年前幾個JS檔案能搞定的頁面需求,現在可能需要幾百,甚至幾千個(哭)。後來,Node出現了,模組化出現了,任務排程和構建工具出現了,前端工程化也就誕生了,而腳手架在其中扮演的角色,和建築工程中一樣 —— 幫你搭建好基本的開發環境,其中包含生成基本專案結構、基本程式碼和開發流程的基本配置和指令碼。

而 vue-cli,首先,它是一個 CLI, 其次,它才是一個 Scafollding,到 3.0 以後,它還算得上一個 build tool。

所以,不要再問我為什麼標題不用 Scafollding 了。

分析

回到我們的需求:

lovely-cli dev    # 以開發環境,webpack watch的模式啟動一個應用
lovely-cli build  # 以生產環境的模式構建應用
複製程式碼

根據需求,我們可以劃分出如下子任務,並提出相應的疑問:

  1. 如何用node寫一個全域性可用的CLI命令 lovely-cli?
  2. 如何支援子命令(dev、build)?
  3. 如何讓 dev 執行 webpack dev,讓 build 執行 webpack build?

實施

前兩條需求,我相信大多數前端/Node程式設計師都已經有一定了解或者非常熟悉了,這裡放上阮老師的文章 《Node.js 命令列程式開發教程》 作為參考,忘記的同學快去複習吧。我快速過一下:

任務一

  1. 新建專案結構如下:
lovely-cli
├── package.json
└── src
    └── index.js
複製程式碼

index.js :

#!/usr/bin/env node

console.log('I am a lovely CLI')
複製程式碼

package.json 的內容如下:

{
  "name": "lovely-cli",
  "version": "0.0.1",
  "description": "A lovely CLI",
  "bin": "src/index.js"
}
複製程式碼

sudo npm link 一下, 看到這個就說明全域性註冊成功了。

/usr/local/bin/lovely-cli -> /usr/local/lib/node_modules/lovely-cli/src/index.js
/usr/local/lib/node_modules/lovely-cli -> /Users/haolchen/Documents/__self__/lovely-cli
複製程式碼

OK,執行一下:

如何開發一個可愛的CLI(二)

任務二

修改一下 index.js 如下:

#!/usr/bin/env node

const command = process.argv[2] // 不懂為什麼這樣寫的同學不夠可愛哦,請回去複習。

if (command === 'dev') {
  console.log('Running in development mode.')
} else if (command === 'build') {
  console.log('Running in production mode.')
} else {
  console.log('I am a lovely CLI.')
}
複製程式碼

執行一下:

如何開發一個可愛的CLI(二)

注:由於handle命令列引數不是本文的重點,本文的演示將均以 vanilla node.js 進行演示。你可能會使用 yargs、commander.js 等等這類元老庫,而我推薦一下可愛的 egoist 出品的 cac

任務三

這算是本文的重點了,我相信有心的同學看到這裡,一定已經有思路了:

  1. 執行 lovely-cli dev => 用開發環境的配置,用 webpack-dev-server 執行起一個webpack app.
  2. 執行 lovely-cli build => 用生成環境的配置,用 webpack 直接 build.

首先 yarn 一下:

yarn add webpack webpack-dev-server -D
複製程式碼

接下來,Just show you the code:

//  20行虛擬碼實現一個 lovely-cli
#!/usr/bin/env node

const command = process.argv[2]
const Webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')

const defaultDevConfig = {} 
const defaultProdConfig = {} 

if (command === 'dev') {
  const compiler = Webpack(defaultDevConfig)
  const devServerOptions = defaultDevConfig.devServer
  const devServer = new WebpackDevServer(compiler, devServerOptions)
  devServer.listen(8080, 'localhost', () => console.log('Starting server on http://localhost:8080'));

} else if (command === 'build') {
  Webpack(defaultProdConfig)

} else {
  console.log('I am a lovely CLI.')
}
複製程式碼

很簡單吧,既然思路都有了,那我們來繼續完善程式碼,寫一個基礎的可用版吧。

首先,完善專案結構:

lovely-cli
├── package.json
├── index.html (新增)
├── package.json
├── src
│   ├── default-webpack-config.js (新增)
│   ├── entry.js (新增)
│   └── index.js
└── dist (新增)
複製程式碼

其中,關於4個新增檔案:

  1. index.html:頁面入口,你也可以用 HtmlWebpackPlugin 解決;
  2. default-webpack-config.js:預設的 webpack 配置;
  3. entry.js:客戶端程式碼入口;
  4. dist:輸出目錄。

首先看一下 default-webpack-config.js:

'use strict'

const { resolve } = require('path')

module.exports = {
  entry: resolve(__dirname, 'entry.js'),
  output: {
    path: resolve(__dirname, '../dist'),
    publicPath: '/dist/', 
    filename: 'bundle.js'
  }
}
複製程式碼

再常規不過的配置了,其中,沒用過 publicPath 作用的同學請參閱 官方文件

接著是 index.html:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Lovely CLI</title>
</head>
<body>
<div id="app"></div>
<script src="/dist/bundle.js"></script>
</body>
</html>
複製程式碼

以及打包入口檔案 entry.js:

const app = document.querySelector('#app')
app.innerHTML = '<h1>Lovely CLI</h1>'
複製程式碼

在看 index.js 檔案做了哪些修改之前,先來體驗一下我們的成果吧:

  1. 執行 lovely-cli dev:

如何開發一個可愛的CLI(二)

開啟瀏覽器:

如何開發一個可愛的CLI(二)

OK,一切按預料執行。

  1. 接著,執行 lovely-cli build:

如何開發一個可愛的CLI(二)

混淆後的檔案也已經生成:

如何開發一個可愛的CLI(二)

最後,讓我們再來看看 CLI 的核心入口檔案做了哪些更改:

#!/usr/bin/env node

const command = process.argv[2]
const Webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')

const defaultConfig = require('./default-webpack-config')

const defaultDevConfig = Object.assign({}, defaultConfig, { mode: 'development' })
const defaultProdConfig = Object.assign({}, defaultConfig, { mode: 'production' })

if (command === 'dev') {
  const compiler = Webpack(defaultDevConfig)
  const devServerOptions = defaultDevConfig.devServer
  const devServer = new WebpackDevServer(compiler, devServerOptions)
  devServer.listen(8080, 'localhost', () => {
    console.log('[Lovely-CLI] Starting server on http://localhost:8080')
  })

} else if (command === 'build') {
  Webpack(defaultProdConfig, function (err, stats) {
    if (err) {
      throw err
    }
    if (stats.hasErrors()) {
      console.log().log('[Lovely-CLI]', stats.toString());
    }
    process.stdout.write(stats.toString({
          colors: true,
          modules: false,
          children: false,
          chunks: false,
          chunkModules: false
        }) + '\n\n')
  })

} else {
  console.log('I am a lovely CLI.')
}
複製程式碼

有實際執行以上程式碼的同學,會發現瀏覽器不會自動檢測變化重新整理,這也是我留給實際動手了的同學的一個思考題,正好趁此機會好好看看一點也不可愛的 webpack 的文件(建議英文)。

最終的程式碼已經放在 Github 上:

lovely-cli

說到這裡,再回過頭來看,上一篇文章中提到的 react-scripts, 你是不是也不陌生了呢?

當然,必須強調,這只是一個玩具(或者說連玩具都算不上),一個實際可用的工具,需要支撐足夠多的場景,正如尤雨溪所說:

“判斷一個開源庫是不是玩具的一個參考是:這個庫有沒有因為實際生產中遇到的特殊情況而做出妥協。”

對於這樣的一個輪子,你可以考慮支援:

  1. 完整的命令列功能,支援 -- 和 - 語法;
  2. 足夠通用的預設配置(webpack4也明白了“約定優於配置”的道理。);
  3. 留出高擴充性的介面(plugin?preset?);
  4. 更漂亮的log頁面(chalk又要登場啦...);
  5. 更優雅的reload(如:修改了webpack配置後根據新的配置重啟dev-server);

當然,還有很多了,只要你想得到。

甜點

老慣例,是時候給大家介紹一些甜點了。

poi

Github - poi

這是官翻:用一個 .js 檔案編寫一個應用程式,無論是 vue 還是 react,poi 會幫你處理所有的開發設定,沒有更多的配置。

poi設計的精妙之處在於提供了 presets 機制,preset可以理解為針對特定場景的一些配置的集合。

在每一個 poi 的 preset 中,可以通過基於 webpack-chain 設計的 API 來修改 webpack 的配置。所有的 preset 會在 app 執行前依次被呼叫。在你我都熟悉的 babel 中,不同的 preset 實際上就是不同的語法轉換 plugin 的一個bundle。

值得開發 Vue 的同學注意的是,Vue、Vue JSX 以及 object-rest-spread 是內建的哦。

目前,poi支援以下預設:有沒有你所熟悉的呢?

如何開發一個可愛的CLI(二)

w7

Github - w7

w7的概念和poi不太一樣,它更像是一個純粹的dev server,沒有任何配置,而做了一件簡單的事,以某個特定埠serve某個html檔案,並監聽該檔案的變化,當檔案發生變化時,會自動重新整理瀏覽器。或許很多人會提出疑問:這難道不是和前端工程化背道而馳嗎?現在還會有人寫純html嗎?

不,相反,我是一名狂熱的前端工程化愛好者。誕生這個專案的緣由實際上還是來源於自己的需求,不知道細心的同學在有沒有發現我部落格倉庫中,每個年份下都有一個tests目錄,這是因為我經常會寫一個相對獨立的前端測試,它不隸屬於某個專案,但我仍然想要它長期存在,我個人習慣以HTML的方式留存,而不是gist。

w7便解決了這個需求,當你只需要做一些簡單的測試時,你並不需要任何構建工具,你只需要一個HTML檔案。雖然只有一個HTML,但w7仍然可以幫你檢測變化並重新整理瀏覽器。

  w7 app.html   
複製程式碼

同時,w7還內建了 vue、react以及rxjs的樣板檔案哦,如果你只是想測試一下一個怎麼用 vue 寫一個計數器,為什麼要花那麼多時間下載 vue-cli,還留下一堆 node_modules 等待清理呢?

  w7 init              # Generate a simple html file with random filename (includes git user name.)
  w7 init --lib vue    # Generate a Counter boilerplate with vue.
  w7 init --lib react  # Generate a Counter boilerplate with React+JSX.
複製程式碼

總結

無論是一個從頭開始的工具(如parcel),還是基於現有成熟方案開發的工具(如 react-scripts 和 poi ),一個好的構建工具,我認為是這樣的:在將 API 設計得足夠簡單的同時,仍然保留其全部的擴充能力。從這一點上看,我認為egoist和她的poi做的相當好。

當然,也請別忘了向實際生產環境妥協。

CLI 系列到此就結束了。接下來幾個月,我會專注在 vue 和 react 的原始碼、對比和思考中,同時也會告訴你如何寫一個包含核心功能的 vue 或者 react 哦,所以下一個系列的標題,就暫定為《以更好的姿勢看待 vue 和 react》吧,敬請關注。

以上,全文終。)

相關文章