本文首發於個人 Github,歡迎 issue / fxxk。
前言
在系列的上一篇《如何開發一個可愛的CLI(一)》中,我給大家講述瞭如何開發一個生成、渲染、轉換樣板檔案(Boilerplate)的簡單腳手架工具。本文,將是愉快的進階環節 —— 如何基於webpack
寫一個 “零配置” 的命令列工具(暫且命名為lovely-cli
.),實現以下功能:
lovely-cli dev # 以開發環境,webpack watch的模式啟動一個應用
lovely-cli build # 以生產環境的模式構建應用
複製程式碼
首先,我要再次強調一個概念。儘管我在第一篇中我有備註:
“由於腳手架的英文 scaffolding 太長,本文我將以更可愛的 CLI 來代替。”
複製程式碼
但仍然有些同學提出疑問(有一位同學回覆我是因為習慣性地滑的太快,所以沒看到...),所以在第二篇的開始,我再次對CLI
、Scafollding
的概念全面闡述一次:
- CLI,其全稱是 command-line interface,也就是命令列介面。你我常用的git,算是比較著名的一個命令列工具了(你若是用source tree黨,當我沒說):
- 腳手架,其英譯是 Scafollding,關於它的概念,請首先看這張圖:
沒錯,這就是腳手架,在建築領域,無論是大工程還是小工程,都需要各式各樣的腳手架,腳手架工程師首先搭好架子,然後工人們慢慢往裡面堆砌磚頭。
如果你只是建1層樓的平房,你可能只需要一個梯子足矣;如果是10層,上圖的腳手架可能也足夠了;但如果是50層、100層,你的腳手架對結構、承重、安全性將會有更多的考慮(PS:你至少得加個電梯吧。)
回到程式設計,為什麼腳手架這個概念開始在前端領域興起呢?說到底,都是源於工程化的崛起。隨著經濟的發展和人民生活水平的提高,富足的人民群眾,對軟體的體驗需求越來越高,前端的頁面也越來越複雜,十幾年前幾個JS檔案能搞定的頁面需求,現在可能需要幾百,甚至幾千個(哭)。後來,Node出現了,模組化出現了,任務排程和構建工具出現了,前端工程化也就誕生了,而腳手架在其中扮演的角色,和建築工程中一樣 —— 幫你搭建好基本的開發環境,其中包含生成基本專案結構、基本程式碼和開發流程的基本配置和指令碼。
而 vue-cli,首先,它是一個 CLI, 其次,它才是一個 Scafollding,到 3.0 以後,它還算得上一個 build tool。
所以,不要再問我為什麼標題不用 Scafollding 了。
分析
回到我們的需求:
lovely-cli dev # 以開發環境,webpack watch的模式啟動一個應用
lovely-cli build # 以生產環境的模式構建應用
複製程式碼
根據需求,我們可以劃分出如下子任務,並提出相應的疑問:
- 如何用node寫一個全域性可用的CLI命令 lovely-cli?
- 如何支援子命令(dev、build)?
- 如何讓 dev 執行 webpack dev,讓 build 執行 webpack build?
實施
前兩條需求,我相信大多數前端/Node程式設計師都已經有一定了解或者非常熟悉了,這裡放上阮老師的文章 《Node.js 命令列程式開發教程》 作為參考,忘記的同學快去複習吧。我快速過一下:
任務一
- 新建專案結構如下:
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,執行一下:
任務二
修改一下 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.')
}
複製程式碼
執行一下:
注:由於handle命令列引數不是本文的重點,本文的演示將均以 vanilla node.js 進行演示。你可能會使用 yargs、commander.js 等等這類元老庫,而我推薦一下可愛的 egoist 出品的 cac。
任務三
這算是本文的重點了,我相信有心的同學看到這裡,一定已經有思路了:
- 執行 lovely-cli dev => 用開發環境的配置,用 webpack-dev-server 執行起一個webpack app.
- 執行 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個新增檔案:
- index.html:頁面入口,你也可以用 HtmlWebpackPlugin 解決;
- default-webpack-config.js:預設的 webpack 配置;
- entry.js:客戶端程式碼入口;
- 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 檔案做了哪些修改之前,先來體驗一下我們的成果吧:
- 執行 lovely-cli dev:
開啟瀏覽器:
OK,一切按預料執行。
- 接著,執行 lovely-cli build:
混淆後的檔案也已經生成:
最後,讓我們再來看看 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, 你是不是也不陌生了呢?
當然,必須強調,這只是一個玩具(或者說連玩具都算不上),一個實際可用的工具,需要支撐足夠多的場景,正如尤雨溪所說:
“判斷一個開源庫是不是玩具的一個參考是:這個庫有沒有因為實際生產中遇到的特殊情況而做出妥協。”
對於這樣的一個輪子,你可以考慮支援:
- 完整的命令列功能,支援 -- 和 - 語法;
- 足夠通用的預設配置(webpack4也明白了“約定優於配置”的道理。);
- 留出高擴充性的介面(plugin?preset?);
- 更漂亮的log頁面(chalk又要登場啦...);
- 更優雅的reload(如:修改了webpack配置後根據新的配置重啟dev-server);
當然,還有很多了,只要你想得到。
甜點
老慣例,是時候給大家介紹一些甜點了。
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支援以下預設:有沒有你所熟悉的呢?
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》吧,敬請關注。
以上,全文終。)