webpack已成爲現代Web開發中最重要的工具之一。它是一個用於JavaScript的模塊打包工具,可是它也能夠轉換全部的前端資源,例如HTML和CSS,甚至是圖片。它可讓你更好地控制應用程序所產生的HTTP請求數量、容許你使用其餘資源的特性(例如Jade、Sass和ES6)。webpack還可讓你輕鬆地從npm下載包。css
本文主要針對那些剛接觸webpack的同窗,將介紹初始設置和配置、模塊、加載器、插件、代碼分割和熱模塊替換。html
在繼續學習下面的內容以前須要確保你的電腦中已經安裝了Node.js。前端
使用npm初始化一個新項目並安裝webpack:node
mkdir webpack-demo cd webpack-demo npm init -y npm install webpack@beta --save-dev mkdir src touch index.html src/app.js webpack.config.js
編寫下面這些文件:python
<!-- 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.js const root = document.querySelector('#root') root.innerHTML = `<p>Hello webpack.</p>`
// webpack.config.js const 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
中、使用babel將全部的.js
文件從ES2015轉換成ES5。webpack
爲了讓它能夠轉換到ES5格式的JS文件,咱們須要安裝三個包:babel-core
、webpack加載器babel-loader
和預置babel-preset-es2015
。使用{ modules: false }
讓Tree Shaking 從打包文件中刪除未使用的導出項(exports)以減小文件大小。git
npm install babel-core babel-loader babel-preset-es2015 --save-dev
最後,用如下內容替換package.json
的scripts
部分:github
"scripts": { "start": "webpack --watch", "build": "webpack -p" },
在命令行中運行npm start
將以監視模式啓動webpack,當src
目錄中的.js
文件更改時,它將從新編譯bundle.js。控制檯中的輸出展現了打包文件的信息,注意打包文件的數量和大小十分重要。web
如今當你在瀏覽器中加載index.html頁面時會看到"Hello webpack."。npm
open index.html
打開dist/bundle.js
文件看看webpack都作了哪些事情,頂部是webpack的模塊引導代碼,底部是咱們的模塊。到目前爲止,你可能對於這個尚未一個深入的印象。可是如今你能夠開始編寫ES6模塊,webpack將生成適用於全部瀏覽器的打包文件。
使用 Ctrl + C
中止webpack,運行npm run build
以生產模式編譯咱們的bundle.js
。
注意,打包文件的大小從2.61 kB減小到了585字節。再看一下dist/bundle.js
文件,你會看到大量難懂的代碼,由於咱們使用UglifyJS壓縮了它。這樣的話,咱們可使用更少的代碼達到與以前同樣的效果。
優秀的webpack知道如何使用各類格式的JavaScript模塊,最著名的兩個是:
ES2015 import
語句
CommonJS require()
語句
咱們能夠經過安裝、導入lodash來測試這些格式的模塊。
npm install lodash --save
// src/app.js import {groupBy} from 'lodash/collection' const 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.js const people = [{ manager: 'Jen', name: 'Bob' }, { manager: 'Jen', name: 'Sue' }, { manager: 'Bob', name: 'Shirley' }, { manager: 'Bob', name: 'Terrence' }] export default people
咱們能夠在app.js
中使用相對路徑輕鬆的導入它。
// src/app.js import {groupBy} from 'lodash/collection' import people from './people' const managerGroups = groupBy(people, 'manager') const root = document.querySelector('#root') root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
注意:像lodash/collection
這樣沒有相對路徑的導入是導入安裝在/node_modules
的模塊,你本身的模塊須要一個相似./people
的相對路徑,你能夠以此區分它們。
咱們已經介紹過,你能夠經過配置像babel-loader
這樣的加載器來告訴webpack在遇到不一樣文件類型的導入時該怎麼作。你能夠將多個加載器組合在一塊兒,應用到不少文件轉換中。在JS中導入.sass
文件是一個很是的例子。
這種轉換涉及三個單獨的加載器和node-sass
庫:
npm install css-loader style-loader sass-loader node-sass --save-dev
在配置文件中爲.scss
文件添加新規則。
// webpack.config.js rules: [{ test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] }, { // ... }]
注意: 任什麼時候候更改webpack.config.js
中的任意一個加載規則都須要使用Ctrl + C
和npm start
從新啓動構建。
加載器序列以相反的順序進行處理:
sass-loader
將Sass轉換爲CSS。
css-loader
將CSS解析爲JavaScript並解析全部依賴。
style-loader
將咱們的CSS輸出到文檔中的<style>標籤。
你能夠將它們看做函數調用,將一個加載器的輸出輸入到下一個加載器中。
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); }
你如今能夠直接在JavaScript中導入Sass文件。
// src/app.js import './style.scss' // ...
從新加載index.html,你應該會看到一些樣式。
咱們剛剛在JavaScript中將一個sass文件做爲模塊導入了。
打開dist/bundle.js
並搜索「pre {」
。事實上,咱們的sass已被編譯成一串CSS,並做爲一個模塊保存在咱們的打包文件中。當咱們將這個模塊導入JavaScript中時,style-loader
會將這個字符串輸入到嵌入的<style>
標籤中。
我知道你在想什麼---爲何?
我不會在這裏過多的討論這個問題,但這裏有幾個值得了解的緣由:
你想要包含在項目中的JavaScript組件可能依賴於其餘資源才能正常運行(HTML,CSS,圖片,SVG),若是這些資源均可以打包在一塊兒,那麼導入和使用將會更容易。
消除死代碼:當你的代碼再也不導入JS組件時,CSS也將再也不被導入。生成的打包文件將只會包含執行某些操做的代碼。
CSS模塊:CSS的全局命名空間很難保證對CSS的更改不會產生任何反作用。CSS模塊經過將CSS默認設置爲本地命名空間、提供能夠在JavaScript中引用的惟一類名來更改這一模式。
經過打包、分割代碼等巧妙的方式來減小HTTP請求的數量。
加載器的最後一個例子是使用url-loader
處理圖片。
在標準的HTML文檔中,當瀏覽器遇到<img>
標籤或具備background-image
屬性的元素時將請求圖片。你可使用webpack將圖片存儲爲JavaScript字符串來對小圖片進行優化處理。這樣你就能夠預先加載它們,而且瀏覽器沒必要在之後爲其單獨發起請求。
npm install file-loader url-loader --save-dev
添加一個加載圖片的規則:
// webpack.config.js rules: [{ test: /\.(png|jpg)$/, use: [{ loader: 'url-loader', options: { limit: 10000 } // 將大小小於10kb的圖片轉爲base64字符串 }] }, { // ... }]
使用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.js import codeURL from './code.png' const img = document.createElement('img') img.src = codeURL img.style.backgroundColor = "#2B3A42" img.style.padding = "20px" img.width = 32 document.body.appendChild(img) // ...
這樣將引入一個圖片,其中src屬性包含圖片自己的數據URL。
<img src="data:image/png;base64,iVBO..." style="background: #2B3A42; padding: 20px" width="32">
此外,因爲css-loader
保障了使用url()引用的圖片也能夠經過url-loader
運行,因此能夠直接在CSS中內聯它。
/* 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主頁上的圖片所展現的那樣:
雖然JavaScript是入口點,可是webpack認爲其餘類型資源(如HTML,CSS和SVG)都有本身的依賴關係,因此這些類型的資源應該被視爲構建過程的一部分。
咱們已經看到一個內置的webpack插件的例子,在npm run build
腳本中調用的webpack -p
命令就是使用webpack附帶的UglifyJsPlugin
插件以生產模式壓縮打包文件。
加載器能夠對單個文件運行轉換,插件能夠運行在更大的代碼塊上。
commons-chunk-plugin
是webpack附帶的另外一個核心插件,用於建立一個單獨的模塊,爲多個入口文件分享公共代碼。到目前爲止,咱們一直在使用單個入口文件和單個輸出打包文件。在許多實際場景中,你將受益於將其分解爲多個輸入和輸出文件。
若是你的應用程序有兩個不一樣的區域須要分享某個模塊,例如:用於面向公共應用程序的app.js
、用於管理區域的admin.js
,你能夠像這樣爲其建立單獨的入口點:
// webpack.config.js const 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
注意output.filename
的變化,如今包含了[name]
,它會被替換爲塊名稱。所以咱們能夠從這個配置中獲得兩個輸出文件、也是咱們的兩個入口文件:app.bundle.js
、admin.bundle.js
。
commonschunk
插件生成第三個文件commons.js
,其中包含的是咱們入口文件須要的公共模塊。
// src/app.js import './style.scss' import {groupBy} from 'lodash/collection' import people from './people' const managerGroups = groupBy(people, 'manager') const root = document.querySelector('#root') root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
// src/admin.js import people from './people' const root = document.querySelector('#root') root.innerHTML = `<p>There are ${people.length} people.</p>`
這些入口文件將輸出如下文件:
app.bundle.js包括style
和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
能夠看到它們自動的建立了通用模塊。
另外一個流行的插件是extract-text-webpack-plugin
,可用於將模塊提取到本身的輸出文件中。
下面咱們將修改.scss
規則來編譯Sass,加載CSS,而後將其提取到本身的CSS打包文件中,從而將其從JavaScript打包文件中刪除。
npm install extract-text-webpack-plugin@2.0.0-beta.4 --save-dev
// webpack.config.js const 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
須要一個依賴關係的列表,一個回調和一個可選的模塊的名稱。
若是你的應用中有一段依賴於應用其餘部分不須要的依賴,那最好把它分離成單獨的包。咱們經過添加一個名爲dashboard.js
的新模塊來演示一下,這個模塊須要引入d3
模塊。
npm install d3 --save
// src/dashboard.js import * as d3 from 'd3' console.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 timeout setTimeout(routes.dashboard, 1000)
由於咱們添加了異步加載模塊,因此咱們須要在配置文件中使用一個output.publicPath
屬性,以便讓webpack知道在哪裏獲取它們。
// webpack.config.js const config = { // ... output: { path: path.resolve(__dirname, 'dist'), publicPath: '/dist/', filename: '[name].bundle.js' }, // ... }
從新啓動構建,你會看到一個神祕的新打包文件0.bundle.js
。
webpack爲了提醒你,使用[big]
來突出顯示較大的包,
這個0.bundle.js
將根據須要使用JSONP請求獲取,所以直接從文件系統加載文件不會再加載它。咱們須要運行一個服務器,任何服務器均可以。
python -m SimpleHTTPServer 8001
打開http://localhost:8001/
加載後一秒鐘,你應該看到一個指向咱們動態生成的打包文件 /dist/0.bundle.js
的GET請求和打印到控制檯的「Loaded!」。成功!
實時從新加載能夠經過在文件更改時自動刷新來真正改善開發人員體驗。只需安裝它,並使用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
中的一個樣式,你會切身感覺到這一好處。
若是你對實時從新加載只是印象深入,那麼熱模塊替換(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時,這些將被從打包文件中分離出來。
將NamedModulesPlugin
添加到webpack.config.js
中的插件列表中以改善控制檯中的日誌記錄性能。
plugins: [ new webpack.NamedModulesPlugin(), // ... ]
最後,在頁面中添加一個<input>元素,咱們能夠在輸入框中添加一些文本,以證實在咱們更改模塊的時候不會發生全頁刷新。
<body> <input /> <div id="root"></div> ...
用npm start
重啓服務器來看看熱加載!
在輸入框中輸入「HMR規則」,而後在people.js
中更更名稱,你會發如今不刷新頁面的狀況下發生了內容更新而且輸入框丟失輸入聚焦狀態。
這只是一個簡單的示例,可是但願你能意識到這是很是有用的。對於像React這樣基於組件的開發這更是十分有用的,你有不少「笨」組件須要與其狀態分離,組件能夠在不丟失狀態的狀況下被更新並從新呈現,所以你能夠不斷的得到即時反饋。
更改style.scss中<pre>
元素的背景顏色,你會發現它並無被HMR更新。
pre { background: red; }
事實證實,當你使用style-loader
時,CSS的HMR能夠直接使用而不須要作任何操做。咱們經過將CSS模塊提取到外部的沒法替代的CSS文件中來去除這個關聯。
若是咱們將Sass規則恢復到初始狀態,並從插件列表中刪除extractCSS
,那麼你也能夠看到Sass的熱加載。
{ test: /\.scss$/, loader: ['style-loader', 'css-loader','sass-loader'] }
使用像webpack這樣的模塊打包工具的好處主要是你能夠經過控制資源的構建方式來幫助你提升應用性能。多年來,將文件鏈接起來以減小客戶端上須要的請求數量一直被認爲是最佳實踐。但HTTP / 2如今容許在單個請求中傳送多個文件,所以鏈接文件再也不是具備極端有效性的解決方法,可是它仍然很重要。你的應用程序實際上也能夠從多個擁有單獨緩存的小文件中受益,客戶端能夠獲取單個更改的模塊,而沒必要再次請求存在大部分相同內容的整個包。
我但願這個關於webpack2的介紹對你有所幫助、可以開始使用它來產生很好的效果。圍繞webpack的配置、加載器和插件的學習可能須要一些時間,可是瞭解這個工具的工做原理確定是頗有好處的。