Webpack 2 入門
Webpack 2 一旦文檔完成,就將結束 Beta 測試期。不過這並不意味著你現在不能開始使用第 2 版,前提是你知道怎麼配置它。(LCTT 譯註:Webpack 2.1 已經發布。)
Webpack 是什麼
簡單來說,Webpack 是一個 JavaScript 模塊打包器。然而,自從它發布以來,它發展成為了你所有的前端代碼的管理工具(或許是有意的,或許是社區的意願)。
老式的任務運行器的方式:你的標記、樣式和 JavaScript 是分離的。你必須分別管理它們每一個,並且你需要確保每一樣都達到產品級
任務運行器 ,例如 Gulp,可以處理許多不同的 預處理器 和 轉換器 ,但是在所有的情景下,它都需要一個輸入源並將其壓縮到一個編譯好的輸出文件中。然而,它是在每個部分的基礎上這樣做的,而沒有考慮到整個系統。這就造成了開發者的負擔:找到任務運行器所不能處理的地方,並找到適當的方式將所有這些模塊在生產環境中聯合在一起。
Webpack 試圖通過提出一個大膽的想法來減輕開發者的負擔:如果有一部分開發過程可以自動處理依賴關係會怎樣?如果我們可以簡單地寫代碼,讓構建過程最終只根據需求管理自己會怎樣?
Webpack 的方式:如果 Webpack 了解依賴關係,它會僅捆綁我們在生產環境中實際需要的部分
如果你過去幾年一直參與 web 社區,你已經知道解決問題的首選方法:使用 JavaScript 來構建。而且 Webpack 嘗試通過 JavaScript 傳遞依賴關係使得構建過程更加容易。不過這個設計真正的亮點不是簡化代碼管理部分,而是管理層由 100% 有效的 JavaScript 實現(具有 Nodejs 特性)。Webpack 能夠讓你編寫有效的 JavaScript,更好更全面地了解系統。
換句話來說:你不需要為 Webpack 寫代碼。你只需要寫項目代碼。而且 Webpack 就會持續工作(當然需要一些配置)。
簡而言之,如果你曾經遇到過以下任何一種情況:
- 載入有問題的依賴項
- 意外引入一些你不需要在生產中用上的 CSS 樣式表和 JS 庫,使項目膨脹
- 意外的兩次載入(或三次)庫
- 遇到作用域的問題 —— CSS 和 JavaScript 都會有
- 尋找一個讓你在 JavaScript 中使用 Node/Bower 模塊的構建系統,要麼就得依靠一個令人發狂的後端配置才能正確地使用這些模塊
- 需要優化 資產 交付,但你擔心會弄壞一些東西
等等……
那麼你可以從 Webpack 中收益了。它通過讓 JavaScript 輕鬆處理你的依賴關係和載入順序,而不是通過開發者的大腦。最好的部分是,Webpack 甚至可以純粹在伺服器端運行,這意味著你還可以使用 Webpack 構建漸進增強式網站。
第一步
我們將在本教程中使用 Yarn(運行命令 brew install yarn
) 代替 npm
,不過這完全取決於你的喜好,它們做同樣的事情。在我們的項目文件夾中,我們將在終端窗口中運行以下代碼,將 Webpack 2 添加到我們的全局軟體包以及本地項目中:
yarn global add webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.9
yarn add --dev webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.9
我們接著會通過項目根目錄的一個 webpack.config.js
文件來聲明 webpack 的配置:
'use strict';
const webpack = require('webpack');
module.exports = {
context: __dirname + '/src',
entry: {
app: './app.js',
},
output: {
path: __dirname + '/dist',
filename: '[name].bundle.js',
},
};
注意:此處 __dirname
是指你的項目根目錄
記住,Webpack 「知道」你的項目發生了什麼。它通過閱讀你的代碼來實現(別擔心,它簽署了保密協議 😀 )。Webpack 基本上執行以下操作:
- 從
context
文件夾開始…… - ……它查找
entry
下的文件名…… - ……並讀取其內容。每一個
import
(ES6)或require()
(Nodejs)的依賴會在它解析代碼的時候找到,它會在最終構建的時候打包這些依賴項。然後,它會搜索那些依賴項以及那些依賴項所依賴的依賴項,直到它到達「樹」的最底端 —— 只打包它所需要的,沒有其它東西。 - Webpack 從
context
文件夾打包所有東西到output.path
文件夾,使用output.filename
命名模板來為其命名(其中[name]
被替換成來自entry
的對象的鍵)。
所以如果我們的 src/app.js
文件看起來像這樣(假設我們事先運行了 yarn add --dev moment
):
'use strict';
import moment from 'moment';
var rightNow = moment().format('MMMM Do YYYY, h:mm:ss a');
console.log( rightNow );
// "October 23rd 2016, 9:30:24 pm"
我們應該運行:
webpack -p
注意:p
標誌表示「生產」模式,這會壓縮輸出文件。
它會輸出一個 dist/app.bundle.js
,並將當前日期和時間列印到控制台。要注意 Webpack 會自動識別 上面的 'moment'
指代的是什麼(比如說,雖然如果你有一個 moment.js
文件在你的目錄,默認情況下 Webpack 會優先考慮你的 moment
Node 模塊)。
使用多個文件
你可以通過僅僅修改 entry
對象來指定任意數量的 入口 / 輸出點 。
打包多個文件
'use strict';
const webpack = require("webpack");
module.exports = {
context: __dirname + "/src",
entry: {
app: ["./home.js", "./events.js", "./vendor.js"],
},
output: {
path: __dirname + "/dist",
filename: "[name].bundle.js",
},
};
所有文件都會按照數組的順序一起被打包成一個 dist/app.bundle.js
文件。
輸出多個文件
const webpack = require("webpack");
module.exports = {
context: __dirname + "/src",
entry: {
home: "./home.js",
events: "./events.js",
contact: "./contact.js",
},
output: {
path: __dirname + "/dist",
filename: "[name].bundle.js",
},
};
或者,你可以選擇打包成多個 JS 文件以便於分割應用的某些模塊。這將被打包成 3 個文件:dist/home.bundle.js
,dist/events.bundle.js
和 dist/contact.bundle.js
。
高級打包自動化
如果你將你的應用分割成多個 output
輸出項(如果你的應用的一部分有大量你不需要預載入的 JS,這會很有用),你可能會重用這些文件的代碼,因為它將分別解析每個依賴關係。幸運的是,Webpack 有一個內置的 CommonsChunk
插件來處理這個:
module.exports = {
// …
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "commons",
filename: "commons.bundle.js",
minChunks: 2,
}),
],
// …
};
現在,在你的 output
文件中,如果你有任何模塊被載入 2 次以上(通過 minChunks
設置),它會把那個模塊打包到 common.js
文件中,然後你可以將其緩存在客戶端。這將生成一個額外的請求頭,但是你防止了客戶端多次下載同一個庫。因此,在很多情景下,這會大大提升速度。
開發
Webpack 實際上有自己的開發伺服器,所以無論你是開發一個靜態網站還是只是你的網站前端原型,它都是無可挑剔的。要運行那個伺服器,只需要添加一個 devServer
對象到 webpack.config.js
:
module.exports = {
context: __dirname + "/src",
entry: {
app: "./app.js",
},
output: {
filename: "[name].bundle.js",
path: __dirname + "/dist/assets",
publicPath: "/assets", // New
},
devServer: {
contentBase: __dirname + "/src", // New
},
};
現在創建一個包含以下代碼的 src/index.html
文件:
<script src="/assets/app.bundle.js"></script>
……在你的終端中運行:
webpack-dev-server
你的伺服器現在運行在 localhost:8080
。注意 script
標籤裡面的 /assets
是怎麼匹配到 output.publicPath
的 —— 你可以隨意更改它的名稱(如果你需要一個 CDN 的時候這會很有用)。
Webpack 會熱載入所有 JavaScript 更改,而不需要刷新你的瀏覽器。但是,所有 webpack.config.js
文件裡面的更改都需要重新啟動伺服器才能生效。
全局訪問方法
需要在全局空間使用你的函數?在 webpack.config.js
裡面簡單地設置 output.library
:
module.exports = {
output: {
library: 'myClassName',
}
};
……這會將你打包好的文件附加到一個 window.myClassName
實例。因此,使用該命名空間,你可以調用入口文件的可用方法(可以在該文檔中閱讀有關此設置的更多信息)。
載入器
到目前為止,我們所做的一切只涉及 JavaScript。從一開始就使用 JavaScript 是重要的,因為它是 Webpack 唯一支持的語言。事實上我們可以處理幾乎所有文件類型,只要我們將其轉換成 JavaScript。我們用 載入器 來實現這個功能。
載入器可以是 Sass 這樣的預處理器,或者是 Babel 這樣的轉譯器。在 NPM 上,它們通常被命名為 *-loader
,例如 sass-loader
和 babel-loader
。
Babel 和 ES6
如果我們想在項目中通過 Babel 來使用 ES6,我們首先需要在本地安裝合適的載入器:
yarn add --dev babel-loader babel-core babel-preset-es2015
然後將它添加到 webpack.config.js
,讓 Webpack 知道在哪裡使用它。
module.exports = {
// …
module: {
rules: [
{
test: /.js$/,
use: [{
loader: "babel-loader",
options: { presets: ["es2015"] }
}],
},
// Loaders for other file types can go here
],
},
// …
};
Webpack 1 的用戶注意:載入器的核心概念沒有任何改變,但是語法改進了。直到官方文檔完成之前,這可能不是確切的首選語法。
/.js$/
這個正則表達式查找所有以 .js
結尾的待通過 Babel 載入的文件。Webpack 依靠正則檢查給予你完全的控制權 —— 它不限制你的文件擴展名或者假定你的代碼必須以某種方式組織。例如:也許你的 /my_legacy_code/
文件夾下的內容不是用 ES6 寫的,所以你可以修改上述的 test
為 /^((?!my_legacy_folder).).js$/
,這將會排除那個特定的文件夾,不過會用 Babel 處理其餘的文件。
CSS 和 Style 載入器
如果我們只想為我們的應用所需載入 CSS,我們也可以這樣做。假設我們有一個 index.js
文件,我們將從那裡引入:
import styles from './assets/stylesheets/application.css';
我們會得到以下錯誤:你可能需要一個合適的載入器來處理這種類型的文件
。記住,Webpack 只能識別 JavaScript,所以我們必須安裝合適的載入器:
yarn add --dev css-loader style-loader
然後添加一條規則到 webpack.config.js
:
module.exports = {
// …
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
// …
],
},
};
載入器以數組的逆序處理。這意味著 css-loader
會比 style-loader
先執行。
你可能會注意到,即使在生產版本中,這實際上是將你的 CSS 和 JavaScript 打包在一起,style-loader
手動將你的樣式寫到 <head>
。乍一看,它可能看起來有點怪異,但你仔細想想就會發現這就慢慢開始變得更加有意義了。你已經節省了一個頭部請求 —— 節省了一些連接上的時間。如果你用 JavaScript 來載入你的 DOM,無論如何,這從本質上消除了 FOUC。
你還會注意到一個開箱即用的特性 —— Webpack 已經通過將這些文件打包在一起以自動解決你所有的 @import
查詢(而不是依靠 CSS 默認的 import 方式,這會導致無謂的頭部請求以及資源載入緩慢)。
從你的 JS 載入 CSS 是非常驚人的,因為你現在可以用一種新的強大的方式將你的 CSS 模塊化。比如說你要只通過 button.js
來載入 button.css
,這將意味著如果 button.js
從來沒有真正使用過的話,它的 CSS 就不會膨脹我們的生產版本。如果你堅持面向組件的 CSS 實踐,如 SMACSS 或 BEM,你會看到更緊密地結合你的 CSS 和你的標記和 JavaScript 的價值。
CSS 和 Node 模塊
我們可以使用 Webpack 來利用 Node.js 使用 ~
前綴導入 Node 模塊的優勢。如果我們運行 yarn add normalize.css
,我們可以使用:
@import "~normalize.css";
……並且充分利用 NPM 來管理我們的第三方樣式 —— 版本控制、沒有任何副本和粘貼的部分。此外,讓 Webpack 為我們打包 CSS 比起使用 CSS 的默認導入方式有明顯的優勢 —— 節省無謂的頭部請求和載入時間。
更新:這一節和下面一節已經更新為準確的用法,不再使用 CSS 模塊簡單地導入 Node 的模塊。感謝 Albert Fernández 的幫助!
CSS 模塊
你可能聽說過 CSS 模塊,它把 CSS 變成了 SS,消除了 CSS 的 層疊性 。通常它的最適用場景是只有當你使用 JavaScript 構建 DOM 的時候,但實質上,它神奇地將你的 CSS 類放置到載入它的 JavaScript 文件里(在這裡了解更多)。如果你打算使用它,CSS 模塊已經與 css-loader
封裝在一起(yarn add --dev css-loader
):
module.exports = {
// …
module: {
rules: [
{
test: /.css$/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true } }
],
},
// …
],
},
};
注意:對於 css-loader
,我們現在使用 擴展對象語法 來給它傳遞一個選項。你可以使用一個更為精簡的字元串來取代默認選項,正如我們仍然使用了 style-loader
。
值得注意的是,當允許導入 CSS 模塊的時候(例如:@import 'normalize.css';
),你完全可以刪除掉 ~
。但是,當你 @import
你自己的 CSS 的時候,你可能會遇到構建錯誤。如果你遇到「無法找到 ____」的錯誤,嘗試添加一個 resolve
對象到 webpack.config.js
,讓 Webpack 更好地理解你的模塊載入順序。
const path = require("path");
module.exports = {
//…
resolve: {
modules: [path.resolve(__dirname, "src"), "node_modules"]
},
};
我們首先指定源目錄,然後指定 node_modules
。這樣,Webpack 會更好地處理解析,按照既定的順序(分別用你的源目錄和 Node 模塊的目錄替換 "src"
和 "node_modules"
),首先查找我們的源目錄,然後再查找已安裝的 Node 模塊。
Sass
需要使用 Sass?沒問題。安裝:
yarn add --dev sass-loader node-sass
並添加新的規則:
module.exports = {
// …
module: {
rules: [
{
test: /.(sass|scss)$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
]
}
// …
],
},
};
然後當你的 Javascript 對一個 .scss
或 .sass
文件調用 import
方法的時候,Webpack 會處理的。
CSS 獨立打包
或許你在處理漸進增強的問題;或許你因為其它原因需要一個單獨的 CSS 文件。我們可以通過在我們的配置中用 extract-text-webpack-plugin
替換 style-loader
而輕易地做到這一點,這不需要更改任何代碼。以我們的 app.js
文件為例:
import styles from './assets/stylesheets/application.css';
讓我們安裝這個插件到本地(我們需要 2016 年 10 月的測試版本):
yarn add --dev extract-text-webpack-plugin@2.0.0-beta.4
並且添加到 webpack.config.js
:
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
// …
module: {
rules: [
{
test: /.css$/,
use: [
ExtractTextPlugin.extract("css"),
{ loader: "css-loader", options: { modules: true } },
],
},
// …
]
},
plugins: [
new ExtractTextPlugin({
filename: "[name].bundle.css",
allChunks: true,
}),
],
};
現在當運行 webpack -p
的時候,你的 output
目錄還會有一個 app.bundle.css
文件。只需要像往常一樣簡單地在你的 HTML 中向該文件添加一個 <link>
標籤即可。
HTML
正如你可能已經猜到,Webpack 還有一個 [html-loader][6]
插件。但是,當我們用 JavaScript 載入 HTML 時,我們針對不同的場景分成了不同的方法,我無法想出一個單一的例子來為你計划下一步做什麼。通常,你需要載入 HTML 以便於在更大的系統(如 React、Angular、Vue 或 Ember)中使用 JavaScript 風格的標記,如 JSX、Mustache 或 Handlebars。或者你可以使用類似 Pug (以前叫 Jade)或 Haml 這樣的 HTML 預處理器,抑或你可以直接把同樣的 HTML 從你的源代碼目錄推送到你的構建目錄。你怎麼做都行。
教程到此為止了:你可以用 Webpack 載入標記,但是進展到這一步的時候,關於你的架構,你將做出自己的決定,我和 Webpack 都無法左右你。不過參考以上的例子以及搜索 NPM 上適用的載入器應該足夠你發展下去了。
從模塊的角度思考
為了充分使用 Webpack,你必須從模塊的角度來思考:細粒度的、可復用的、用於高效處理每一件事的獨立的處理程序。這意味著採取這樣的方式:
└── js/
└── application.js // 300KB of spaghetti code
將其轉變成這樣:
└── js/
├── components/
│ ├── button.js
│ ├── calendar.js
│ ├── comment.js
│ ├── modal.js
│ ├── tab.js
│ ├── timer.js
│ ├── video.js
│ └── wysiwyg.js
│
└── application.js // ~ 1KB of code; imports from ./components/
結果呈現了整潔的、可復用的代碼。每一個獨立的組件通過 import
來引入自身的依賴,並 export
它想要暴露給其它模塊的部分。結合 Babel 和 ES6,你可以利用 JavaScript 類 來實現更強大的模塊化,而不用考慮它的工作原理。
有關模塊的更多信息,請參閱 Preethi Kasreddy 這篇優秀的文章。
延伸閱讀
via: https://blog.madewithenvy.com/getting-started-with-webpack-2-ed2b86c68783#.oozfpppao
作者:Drew Powers 譯者:OneNewLife 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive