兩個月前,我曾釋出了一篇基於 webpack 的 React 起步教程。你眼前的這篇文章跟那一篇差不多,只不過不包含 React 那一塊。這篇教程稍微簡單一些,但仍然會有一些棘手的部分。因此,我特意建了一個全新的程式碼倉庫 webpack-library-starter,把建立一個 JavaScript 類庫所需的所有素材都放了進去。
首先,我們說的 “類庫” 是指什麼
在 JavaScript 語境中,我對類庫的定義是 “提供了特定功能的一段代段”。一個類庫只做一件事,並且把這件事做好。在理想情況下,它不依賴其它類庫或框架。jQuery 就是一個很好的例子。React 或者 Vue.js 也可以認為是一個類庫。
一個類庫應該:
用什麼來開發這個類庫並不重要,重要的是我們最終產出的檔案。它只要滿足上述要求就行。儘管如此,我還是比較喜歡用原生 JavaScript 寫成的類庫,因為這樣更方便其它人貢獻程式碼。
目錄結構
我一般選擇如下的目錄結構:
1 2 3 4 5 6 |
+-- lib | +-- library.js | +-- library.min.js +-- src | +-- index.js +-- test |
其中 src
目錄用於存放原始碼檔案,而 lib
目錄用於存放最終編譯的結果。這意味著類庫的入口檔案應該放在 lib
目錄下,而不是 src
目錄下。
起步動作
我確實很喜歡最新的 ES6 規範。但壞訊息是它身上綁了一堆的附加工序。也許將來某一天我們可以擺脫轉譯過程,所寫即所得;但現在還不行。通常我們需要用到 Babel 來完成轉譯這件事。Babel 可以把我們的 ES6 檔案轉換為 ES5 格式,但它並不打算處理打包事宜。或者換句話說,如果我們有以下檔案:
1 2 3 4 |
+-- lib +-- src +-- index.js (es6) +-- helpers.js (es6) |
然後我們用上 Babel,那我們將會得到:
1 2 3 4 5 6 |
+-- lib | +-- index.js (es5) | +-- helpers.js (es5) +-- src +-- index.js (es6) +-- helpers.js (es6) |
或者再換句話說,Babel 並不解析程式碼中的 import
或 require
指令。因此,我們需要一個打包工具,而你應該已經猜到了,我的選擇正是 webpack。最終我想達到的效果是這樣的:
1 2 3 4 5 6 |
+-- lib | +-- library.js (es5) | +-- library.min.js (es5) +-- src +-- index.js (es6) +-- helpers.js (es6) |
npm 命令
在執行任務方面,npm 提供了一套不錯的機制——scripts(指令碼)。我們至少需要註冊以下三個指令碼:
1 2 3 4 5 |
"scripts": { "build": "...", "dev": "...", "test": "..." } |
npm run build
– 這個指令碼用來生成這個類庫的最終壓縮版檔案。npm run dev
– 跟build
類似,但它並不壓縮程式碼;此外還需要啟動一個監視程式。npm run test
– 用來執行測試。
構建開發版本
npm run dev
需要呼叫 webpack 並生成 lib/library.js
檔案。我們從 webpack 的配置檔案開始著手:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// webpack.config.js var webpack = require('webpack'); var path = require('path'); var libraryName = 'library'; var outputFile = libraryName + '.js'; var config = { entry: __dirname + '/src/index.js', devtool: 'source-map', output: { path: __dirname + '/lib', filename: outputFile, library: libraryName, libraryTarget: 'umd', umdNamedDefine: true }, module: { loaders: [ { test: /(\.jsx|\.js)$/, loader: 'babel', exclude: /(node_modules|bower_components)/ }, { test: /(\.jsx|\.js)$/, loader: "eslint-loader", exclude: /node_modules/ } ] }, resolve: { root: path.resolve('./src'), extensions: ['', '.js'] } }; module.exports = config; |
即使你還沒有使用 webpack 的經驗,你或許也可以看明白這個配置檔案做了些什麼。我們定義了這個編譯過程的輸入(entry
)和輸出(output
)。那個 module
屬性指定了每個檔案在處理過程中將被哪些模組處理。在我們的這個例子中,需要用到 Babel 和 ESLint,其中 ESLint 用來校驗程式碼的語法和正確性。
這裡有一個坑,花了我不少的時間。這個坑是關於 library
、libraryTarget
和 umdNamedDefine
屬性的。最開始我沒有把它們寫到配置中,結果編譯結果就成了下面這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
(function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if(installedModules[moduleId]) return installedModules[moduleId].exports; var module = installedModules[moduleId] = { exports: {}, id: moduleId, loaded: false }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.loaded = true; return module.exports; } __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.p = ""; return __webpack_require__(0); })([ function(module, exports) { // ... my code here } ]); |
經過 webpack 編譯之後的檔案差不多都是這個樣子。它採用的方式跟 Browserify 很類似。編譯結果是一個自呼叫的函式,它會接收應用程式中所用到的所有模組。每個模組都被存放到到 modules
陣列中。上面這段程式碼只包含了一個模組,而 __webpack_require__(0)
實際上相當於執行 src/index.js
檔案中的程式碼。
光是得到這樣一個打包檔案,並沒有滿足我們在文章開頭所提到的所有需求,因為我們還沒有匯出任何東西。這個檔案的執行結果在網頁中必定會被丟棄。不過,如果我們加上 library
、libraryTarget
和umdNamedDefine
,就可以讓 webpack 在檔案頂部注入一小段非常漂亮的程式碼片斷:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("library", [], factory); else if(typeof exports === 'object') exports["library"] = factory(); else root["library"] = factory(); })(this, function() { return (function(modules) { ... ... |
把 libraryTarget
設定為 umd
表示採用 通用模組定義 來生成最終結果。而且這段程式碼確實可以識別不同的執行環境,併為我們的類庫提供一個妥當的初始化機制。
構建生產環境所需的版本
對 webpack 來說,開發階段與生產階段之間唯一的區別在於壓縮。執行 npm run build
應該生成一個壓縮版——library.min.js
。webpack 有一個不錯的內建外掛可以做到這一點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// webpack.config.js ... var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; var env = process.env.WEBPACK_ENV; var libraryName = 'library'; var plugins = [], outputFile; if (env === 'build') { plugins.push(new UglifyJsPlugin({ minimize: true })); outputFile = libraryName + '.min.js'; } else { outputFile = libraryName + '.js'; } var config = { entry: __dirname + '/src/index.js', devtool: 'source-map', output: { ... }, module: { ... }, resolve: { ... }, plugins: plugins }; module.exports = config; |
只要我們把
UglifyJsPlugin
加入到 plugins
陣列中,它就可以完成這個任務。此外,還一些事情有待明確。我們還需要某種條件判斷邏輯,來告訴 webpack 需要生成哪一種型別(“開發階段” 還是 “生產階段”)的打包檔案。一個常見的做法是定義一個環境變數,並將它通過命令列傳進去。比如這樣:
1 2 3 4 5 |
// package.json "scripts": { "build": "WEBPACK_ENV=build webpack", "dev": "WEBPACK_ENV=dev webpack --progress --colors --watch" } |
(請留意 --watch
選項。它會讓 webpack 監視檔案變化並持續執行構建任務。)
測試
我通常採用 Mocha 和 Chai 來執行測試——測試環節是這篇起步教程特有的內容。這裡同樣存在一個棘手的問題,就是如何讓 Mocha 正確識別用 ES6 寫的測試檔案。不過謝天謝地,Babel 再次解決了這個問題。
1 2 3 4 5 |
// package.json "scripts": { ... "test": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js" } |
這裡最關鍵的部分在於 --compilers
這個選項。它允許我們在執行測試檔案之前預先處理這個檔案。
其它配置檔案
在最新的 6.x 版本中,Babel 發生了一些重大的變化。現在,在指定哪些程式碼轉換器將被啟用時,我們需要面對一種叫作 presets
的東西。最簡單配置的方法就是寫一個 .babelrc
檔案:
1 2 3 4 |
// .babelrc { "presets": ["es2015"], "plugins": ["babel-plugin-add-module-exports"] } |
ESLint 也需要一個類似的配置檔案,叫作 .eslintrc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// .eslintrc { "ecmaFeatures": { "globalReturn": true, "jsx": true, "modules": true }, "env": { "browser": true, "es6": true, "node": true }, "globals": { "document": false, "escape": false, "navigator": false, "unescape": false, "window": false, "describe": true, "before": true, "it": true, "expect": true, "sinon": true }, "parser": "babel-eslint", "plugins": [], "rules": { // ... lots of lots of rules here } } |
相關連結
這篇起步教程還可以在 GitHub 上找到:github.com/krasimir/webpack-library-starter。
用到的專案如下:
具體依賴如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// package.json "devDependencies": { "babel": "6.3.13", "babel-core": "6.1.18", "babel-eslint": "4.1.3", "babel-loader": "6.1.0", "babel-plugin-add-module-exports": "0.1.2", "babel-preset-es2015": "6.3.13", "chai": "3.4.1", "eslint": "1.7.2", "eslint-loader": "1.1.0", "mocha": "2.3.4", "webpack": "1.12.9" } |
譯註
是不是意猶未盡?其實準確來說,這篇文章是作者對 webpack-library-starter 專案的一個簡要解說,講解了程式碼之外的背景知識。
因此,作為學習者,光讀文章是遠遠不夠的,我們真正需要的是研讀這個專案提供的原始碼,並且動手實際操作和演練,如此方能掌握要領。加油!