標籤:

翻譯 | webpack2的入門手冊

背景

一直對webpack的打包流程很感興趣,但是無奈官網文檔實在太多,搜出來的大部分文章要麼偏理論要麼純粹講過程不講原理,最近終於找到一篇入門文章,文章對於初學者講的很清晰,但是由於是英文的,而且我沒有找到這篇文章對應的中文翻譯版,所以本文主要是對那篇文章進行翻譯,介紹一下webpack2的入門知識。

註:本人翻譯水平有限,如果有錯誤,歡迎指正。

原文地址:A Beginner』s Guide to Webpack 2 and Module Bundling

原文作者:Mark Brown

譯文作者:Allen Gong

webpack2入門手冊(譯文)

Webpack是一個模塊打包機

Webpack已然成為當前web開發最重要的工具之一。首先它是一個Javascript的打包工具,但同時他也能打包包括HTML,CSS,甚至是圖片等形式的資源。它能更好的控制你正在編寫的App的HTTP請求,並且允許你去使用更多的資源(如Jade,Sass以及ES6)。Webpack同時允許你更容易的從npm獲取安裝包。

這篇文章主要面向那些對於webpack完全陌生的同學,內容將包括初始安裝和配置,模塊,模塊載入器,插件,代碼拆分以及模塊熱替換(HMR,hot module replacement)。如果你覺得入門視頻比較有用的話,我推薦Glen Maddern的Webpack初體驗作為開始學習的起點,會讓你理解為什麼webpack如此特殊。

為了更加後續的閱讀,請確保先安裝了Node.js,安裝可以參考Node.js安裝教程。你也在Github上下載到對應的Demo。

安裝

讓我們用npm和webpack新建一個項目吧:

mkdir webpack-democd webpack-demonpm init -ynpm install webpack@beta --save-devmkdir srctouch index.html src/app.js webpack.config.js

編輯以下文件:

<!-- index.html --><!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Hello webpack</title> </head> <body> <div id="root"></div> <script src="dist/bundle.js"></script> </body></html>

// src/app.jsconst root = document.querySelector(#root)root.innerHTML = `<p>Hello webpack.</p>`

// webpack.config.jsconst webpack = require(webpack)const path = require(path)const config = { context: path.resolve(__dirname, src), entry: ./app.js, output: { path: path.resolve(__dirname, dist), filename: bundle.js }, module: { rules: [{ test: /.js$/, include: path.resolve(__dirname, src), use: [{ loader: babel-loader, options: { presets: [ [es2015, { modules: false }] ] } }] }] }}module.exports = config

以上的設置只是通用配置,它會指導你的webpack將我們的入口文件src/app.js編譯輸入為/dist/bundle.js,並且所有的.js文件都將通過Babel從ES2015轉換為ES5。

為了讓這個項目能運行起來,我們需要安裝三個安裝包,babel-core,webpack的載入器babel-loader以及預處理模塊babel-preset-es2015,這些模塊都是為了支持Javascript的編寫。{ modules: false }可以確保使用Tree Shaking去去除掉不必要的模塊,同時會降低文件大小。

npm install babel-core babel-loader babel-preset-es2015 --save-dev

最後使用下面代碼更新package.json:

"scripts": { "start": "webpack --watch", "build": "webpack -p"},

運行npm start將會以觀察模式啟動webpack,在這種模式下,會持續監聽我們src文件夾下的.js文件。控制台的輸出結果顯示了生成的打包後的文件,我們應該持續關注生成的文件的大小和數量。

現在你可以在瀏覽器中訪問index.html,將會看到「Hello webpack.」

open index.html

打開dist/bundle.js看看webpack到底做了什麼事,在文件的頂部是bootstrapping模塊的代碼,在它下面是我們自己的模塊。你可能目前還沒有什麼感覺webpack好處,但是你現在可以編寫ES6代碼並且webpack將會把各個模塊打成生產所需要的包,這樣所有瀏覽器都能訪問。

使用Ctrl + C停止webpack的服務,運行npm run build,編譯成生成環境所需要的包。

注意:包的大小從2.61 kB降到了585 bytes 重新看看dist/bundle.js,你會發現代碼變得一團糟,UglifyJS對打包後的代碼進行了壓縮,運行起來是沒有差別的,但同時字元數是相當少的。

模塊

對於外部模塊,webpack有多種方式去引入,其中比較重要的兩種是:

  • ES2015的import方法
  • CommonJS的require()方法

我們可以通過安裝lodash來測試上述方式,並且導入到 app.js中。

npm install lodash --save

// src/app.jsimport {groupBy} from lodash/collectionconst people = [{ manager: Jen, name: Bob}, { manager: Jen, name: Sue}, { manager: Bob, name: Shirley}, { manager: Bob, name: Terrence}]const managerGroups = groupBy(people, manager)const root = document.querySelector(#root)root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`

運行npm start重啟webpack並刷新index.html,你會在頁面上看到一個按照manager分好組人名的數組。 接下來讓我們把這個數組部分單獨放在people.js這個模塊里。

// src/people.jsconst people = [{ manager: Jen, name: Bob}, { manager: Jen, name: Sue}, { manager: Bob, name: Shirley}, { manager: Bob, name: Terrence}]export default people

我們可以以相對路徑的方式將模塊導入到app.js

// src/app.jsimport {groupBy} from lodash/collectionimport people from ./peopleconst managerGroups = groupBy(people, manager)const root = document.querySelector(#root)root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`

注意:導入像lodash/collection這種不使用相對路徑的,是那些通過npm安裝的,從/node_modules中引入,你自定義的模塊則需要像./people相對路徑的方式引入,通過這種方式可以對兩種模塊進行區分。

載入器

我們已經介紹了babel-loader,它是眾多loader中的一種,能夠告訴webpack當遇到不同的文件時如何處理。比較好的方式是將loader進行串聯,載入到一個載入器中,我們通過從Javascript中引入Sass包來看看loader是如何進行工作的。

Sass

這個轉換器包括了三個單獨的載入器和node-sass庫:

npm install css-loader style-loader sass-loader node-sass --save-dev

在配置文件中為.scss引入新的規則:

// webpack.config.jsrules: [{ test: /.scss$/, use: [ style-loader, css-loader, sass-loader ]}, { // ...}]

注意:不管什麼時候你改變了webpack.config.js中的載入規則,你都需要通過Ctrl + C然後npm start的方式重啟webpack。

loader以倒序的方式運行:

  • sass-loader轉換Sass成CSS
  • css-loader將CSS解析Javascript並解決依賴包問題
  • style-loader將CSS導出成<tag>便簽放在document下

你可以將上述過程想像成函數的調用關係,一個函數運行的結果作為另一個函數的輸入:

styleLoader(cssLoader(sassLoader(source)))

接下來讓我們增加一個Sass源文件:

/* src/style.scss */$bluegrey: #2B3A42;pre { padding: 20px; background: $bluegrey; color: #dedede; text-shadow: 0 1px 1px rgba(#000, .5);}

現在你可以在你的app.js中直接引入Sass文件:

// src/app.jsimport ./style.scss// ...

刷新index.html你會看到樣式發生了變化。

Javascript中的CSS

我們剛剛把Sass作為一個模塊引入到我們的入口文件中。

打開dist/bundle.js,搜索pre {。事實上,Sass已經被編譯成一段CSS的字元串,並以模塊的形式存在。當我們在我們的Javascript文件中導入這個模塊時,style-loader就會將其編譯輸出成內嵌的<style>標籤。

我知道你在想什麼?為什麼要這麼做?

關於這個問題,我在這個話題中不想說太多,但下面幾個原因值得思考一下:

  • 如果你想在項目中引入一個Javascript組件並正常運行,可能需要依賴很多其他資源(如HTML, CSS, Images, SVG),如果我們將所有資源打包到一起,將會非常易於引入和使用。
  • 去除寫死的代碼:當一個JS組件不再被代碼引入到項目中,對應的CSS也不會被引入進來。而最終打包後的結果也只會包含那些被引用的部分。
  • CSS模塊:由於全局CSS命名空間的存在,使得改變CSS後是否有副作用不得而知。CSS模塊默認情況下將CSS設成本地,並顯示你在Javascript中可以引用的唯一類名。
  • 通過捆綁/分割代碼的巧妙方式減少HTTP請求的數量。

圖片

最後一個關於loader的例子是關於處理圖片的url-loader。

在標準HTML文檔中,圖片通過<img>標籤或者background-image屬性獲得。但是通過webpack,一些小圖片可以以字元串的形式存儲在Javascript中。通過這種方式,你可以在預載入的時候就獲取到圖片,從而不需要單獨的請求去請求圖片。

npm install file-loader url-loader --save-dev

在配置文件中增加一條圖片的規則:

// webpack.config.jsrules: [{ test: /.(png|jpg)$/, use: [{ loader: url-loader, options: { limit: 10000 } // Convert images < 10k to base64 strings }]}, { // ...}]

通過Ctrl + C和npm start重啟服務。

通過下面的命令下載一個測試圖片:

curl https://raw.githubusercontent.com/sitepoint-editors/webpack-demo/master/src/code.png --output src/code.png

現在可以在app.js中載入圖片資源:

// src/app.jsimport codeURL from ./code.pngconst img = document.createElement(img)img.src = codeURLimg.style.backgroundColor = "#2B3A42"img.style.padding = "20px"img.width = 32document.body.appendChild(img)// ...

這樣頁面中多了一個img,它的src屬性包含了圖片自身的data URI。

<img src="data:image/png;base64,iVBO..." style="background: #2B3A42; padding: 20px" width="32">

同時,因為css-loader的緣故,通過url()屬性引入的圖片,也通過url-loader轉換成行內元素。

/* src/style.scss */pre { background: $bluegrey url(code.png) no-repeat center center / 32px 32px;}

編譯後變成:

pre { background: #2b3a42 url("data:image/png;base64,iVBO...") no-repeat scroll center center / 32px 32px;}

模塊到靜態資源

現在你可以webpack是如何幫助你對將你項目中一系列的依賴資源進行打包處理的,下面這張圖是webpack官網主頁上的。

雖然Javascript是入口文件,但是webpack還是傾向於你的其他類型的資源像HTML, CSS, and SVG能有自己的依賴,把它們作為構建包的一部分。

插件

我們已經看過了webpack其中一個構建插件的例子,使用UglifyJsPlugin的npm run build腳本可以調用webpack -p,它的作用是與webpack搭配壓縮生成後的包。

當loader在單個文件上操作相應變換時,插件可以在各個大型代碼塊上交叉運行。

公共代碼

commons-chunk-plugin是另一個核心插件,搭配webpack用來創建在多個入口文件中使用的擁有公共代碼的單文件模塊。到目前為止,我們使用的都是單一入口和單一出口文件。但是很多real-world scenarios中更好的方法是使用多文件入口和多文件出口。

如果你在你的應用中有兩個完全獨立的領域但是卻擁有共同的模塊,舉個例子,app.js是面向用戶的,admin.js是面向管理員的,你就可以為他們單獨創建不同的入口文件,就像下面這樣:

// webpack.config.jsconst webpack = require(webpack)const path = require(path)const extractCommons = new webpack.optimize.CommonsChunkPlugin({ name: commons, filename: commons.js})const config = { context: path.resolve(__dirname, src), entry: { app: ./app.js, admin: ./admin.js }, output: { path: path.resolve(__dirname, dist), filename: [name].bundle.js }, module: { // ... }, plugins: [ extractCommons ]}module.exports = config

注意對於結果文件,現在包含了名字,這樣我們區分出兩個不同的結果文件對應不同的入口文件:app.bundle.js和admin.bundle.js。

commonschunk插件生成了第三個文件commons.js,他包含了我們入口文件的公共模塊。

// src/app.jsimport ./style.scssimport {groupBy} from lodash/collectionimport people from ./peopleconst managerGroups = groupBy(people, manager)const root = document.querySelector(#root)root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`

// src/admin.jsimport people from ./peopleconst root = document.querySelector(#root)root.innerHTML = `<p>There are ${people.length} people.</p>`

這些入口文件將會產生下列文件:

  • app.bundle.js:包括樣式和lodash/collection模塊
  • admin.bundle.js:不包含任何額外模塊
  • commons.js:包含了我們公共的people模塊

我們可以在兩個入口文件中都引入公共模塊:

<!-- index.html --><!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Hello webpack</title> </head> <body> <div id="root"></div> <script src="dist/commons.js"></script> <script src="dist/app.bundle.js"></script> </body></html>

<!-- admin.html --><!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Hello webpack</title> </head> <body> <div id="root"></div> <script src="dist/commons.js"></script> <script src="dist/admin.bundle.js"></script> </body></html>

試試在瀏覽器中重新載入index.html和admin.html,看看自動生成的公共模塊部分。

抽取CSS

另一個受歡迎的插件是extract-text-webpack-plugin,它的用途是抽取模塊到對應的結果文件中。

下面我們在配置文件中修改.scss的規則編譯成對應的Sass文件,載入CSS,接著把他們抽取到各自的CSS包中,這樣就可以把它們從Javascript包中移除。

npm install extract-text-webpack-plugin@2.0.0-beta.4 --save-dev

// webpack.config.jsconst ExtractTextPlugin = require(extract-text-webpack-plugin)const extractCSS = new ExtractTextPlugin([name].bundle.css)const config = { // ... module: { rules: [{ test: /.scss$/, loader: extractCSS.extract([css-loader,sass-loader]) }, { // ... }] }, plugins: [ extractCSS, // ... ]}

重啟webpack你會看到一個新的打包後的文件app.bundle.css,你可以照例直接引用它。

<!-- index.html --><!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Hello webpack</title> <link rel="stylesheet" href="dist/app.bundle.css"> </head> <body> <div id="root"></div> <script src="dist/commons.js"></script> <script src="dist/app.bundle.js"></script> </body></html>

刷新頁面,確認CSS已經被編譯過了,並從app.bundle.js移到了app.bundle.css,成功了!

代碼拆分

我們已經看了幾種代碼拆分的方法:

  • 手動創建單獨的入口文件
  • 自動將公共代碼拆分到公共模塊中
  • 使用extract-text-webpack-plugin從編譯後的代碼中抽取出來

拆分包還有其他方法:System.import和require.ensure。通過在這些函數中包含代碼段,你可以創建一個在運行時按需載入的模塊。這個從根本上提高了性能,因為在啟動過程中不需要把所有東西都發送到客戶端。System.import將模塊名作為參數,並返回一個Promise對象。require.ensure獲取依賴關係的列表,回調函數以及可選的模塊名。

如果應用程序的某一部分具有很大的依賴關係,則應用程序的其餘部分就不需要了,最好的方式就是拆分到各個模塊中去。我們通過新建一個需要依賴d3的模塊dashboard.js來證明這點。

npm install d3 --save

// src/dashboard.jsimport * as d3 from d3console.log(Loaded!, d3)export const draw = () => { console.log(Draw!)}

在app.js的頂部引入dashboard.js:

// ...const routes = { dashboard: () => { System.import(./dashboard).then((dashboard) => { dashboard.draw() }).catch((err) => { console.log("Chunk loading failed") }) }}// demo async loading with a timeoutsetTimeout(routes.dashboard, 1000)

因為我們載入了非同步模塊,我們需要在配置文件中增加output.publicPath屬性,因此webpack知道去哪裡獲取。

// webpack.config.jsconst config = { // ... output: { path: path.resolve(__dirname, dist), publicPath: /dist/, filename: [name].bundle.js }, // ...}

運行npm run build操作,你會看到一個新的神秘的打包文件0.bundle.js。

注意webpack為了保持誠實,通過凸現[big]的包來讓你保持關注。 這個0.bundle.js將會通過JSONP的請求按需載入,所以從文件目錄中獲取將不在有效,我們需要啟動一個服務來獲取文件。

python -m SimpleHTTPServer 8001

打開瀏覽器,輸入http://localhost:8001/ 載入一秒鐘後,你會獲得一個GET請求,我們動態生成了/dist/0.bundle.js文件,在控制台上列印除了"Loaded!",成功!

webpack開發伺服器

當文件改變時,實時地重新載入能提高開發者的開發效率。只要安裝它,並且以webpack-dev-server的形式啟動,就可以體驗啦。

npm install webpack-dev-server@2.2.0-rc.0 --save-dev

修改package.json中的start腳本:

"start": "webpack-dev-server --inline",

重新運行npm start,在瀏覽器中打開http://localhost:8080

試著去改變src目錄中任何文件,如改變people.js中的任意一個名字,或者style.scss中的任意樣式,去看看它如何實時改變。

熱模塊替換(熱更新)

如果你對實時重新載入印象深刻,那麼hot module replacement(HMR)一定會讓你吃驚不已。

現在是2017年了,你在工作中已經可以在單頁面應用中使用全局狀態了。在開發過程中,你可能會對組件進行許多小的修改,並且希望能在瀏覽器中看到修改後生成的結果,這樣可以實時去更改。但是通過刷新頁面或者實時熱更新並不能改變全局的狀態,你就必須重頭開始。但是HMR永遠地改變了這一問題。

最後對package.json中的start腳本做修改:

"start": "webpack-dev-server --inline --hot",

在app.js中告訴webpack去接受這個模塊以及對應依賴的熱更新。

if (module.hot) { module.hot.accept()}// ...

注意:webpack-dev-server --hot設置了module.hot為true,但只是在開發過程中。當以生產模式打包時,module.hot被設成了false,這樣這些包就被從結果中抽離了。

在webpack.config.js中增加一個NamedModulesPlugin插件,去改善控制台的記錄功能。

plugins: [ new webpack.NamedModulesPlugin(), // ...]

最後我們在頁面中增加一個<input>元素,我們可以在裡面增加一些文字,用來確保我們更改自己模塊時頁面不會刷新。

<body> <input /> <div id="root"></div> ...

運行npm start重啟服務,觀察熱更新如何工作吧。

為了實驗,在input框中輸入「HMR Rules」,接著改變一個people.js中的名字,你會發現頁面在不刷新也能做出修改,而忽略input的狀態。

這只是一個簡單的例子,但是希望你能看到其廣泛的用途。在諸如React的開發模式中,你可能有很多"啞巴"組件是與他們的狀態分離開的,通過熱更新,這些組件將不會失去狀態,也能實時更新,因此你將獲得及時的反饋。

熱更新CSS

修改style.scss文件中<pre>元素的背景顏色,你發現他並沒有被HMR替換。

pre { background: red;}

事實證明當你使用style-loader時,CSS的熱更新將會免費為你提供而不需要你做任何特殊處理。我們只需要斷開CSS模塊與最終抽取的包之間的鏈接,這個包是無法被替換的。

如果我們將Sass規則恢復到原始狀態,並從插件列表中刪除extractCSS,那麼您也可以看到Sass的熱重新載入。

{ test: /.scss$/, loader: [style-loader, css-loader,sass-loader]}

HTTP/2

使用像webpack這樣的模塊打包工具的主要好處之一是,您可以通過控制資源的構建方式以及在客戶端上的獲取方式,從而幫助你提高性能。多年以來,它被認為是最佳實踐,通過連接文件減少客戶端請求。現在還是有效,但是HTTP2在單一請求中發送多文件,因此連接文件的方式不不再是"銀彈"。你的應用程序實際上可以從多個小文件單獨緩存,但客戶端可以獲取單個更改的模塊,而不必再次獲取大部分相同內容的整個包。

Webpack的創始人Tobias Koppers的撰寫了一篇內容豐富的帖子,解釋了為什麼打包仍然很重要,即使在HTTP/2時代。

想了解更多請參考webpack &amp;amp;amp; HTTP/2

寫在結尾的話

我真心希望你已經發現這個介紹webpack 2的文章對你有幫助,並能夠開始很好使用它。圍繞webpack的配置,載入程序和插件可能需要一些時間,但是了解這個工具的工作原理後會對你有很大幫助。

文檔仍在進行更新中,但如果您想將現有的Webpack1項目移到Webpack2,則可以參考Migrating from v1 to v2。

webpack是否是你打包的選擇,從評論中你就可以知曉。

本文由Scott Molinari,Joan Yin和Joyce Echessa進行了同行評審。 感謝SitePoint的同行評議人員,使SitePoint內容成為最棒的內容!

本文翻譯自A Beginner』s Guide to Webpack 2 and Module Bundling 翻譯者:Allen Gong


推薦閱讀:

webpack:從入門到真實項目配置
如果HTTP2普及了,Webpack、Rollup這種打包工具還有意義嗎?
webpack必知必會
前端調用 GraphQL API,從未如此方便!
webpack打包之 緩存

TAG:webpack |