Webpack入門

Jimmy Zhang發表於2016-05-10

Webpack入門

簡介

Webpack是一個前端構建工具,本文將簡要介紹它最常用的功能,並建立一個基於webpack的前端開發環境。

範例專案

包含兩個頁面,列表頁list.html和詳情頁detail.html,僅作為Webpack打包的演示,不實際開發功能。範例專案是一個多頁面應用,而非SPA(單頁應用)。我們將使用Webpack來進行前端資源的編譯和打包。

技術選型

  1. 樣式:採用SCSS,因此需要將SCSS轉換為CSS。
  2. 指令碼:採用ES6編寫,因此需要使用Babel將ES6程式碼,轉換為ES5(目前瀏覽器所支援的)。
  3. UI框架:React,因此需要將jsx轉換為js程式碼。

基本使用方法

建立目錄結構

在D盤建一個空資料夾,webpack-tutorial,作為示例專案的根目錄。結構如下:

  • dist
    • js
    • css
  • src
    • jsx
    • js
    • css

dist目錄存放由webpack打包後生成的程式碼,也是.html頁面所引用的檔案;src則是我們編寫的原始碼,通常是無法直接在頁面上引用的,因為當下的瀏覽器還無法完全支援很多新的技術,例如ES6。

安裝Webpack

假設你已經安裝了NodeJS和npm包管理工具,並且版本在3.8.0以上。

開啟命令列工具,定位到D:\webpack-tutorial。首先執行npm init,生成package.json檔案。

接著安裝webpack,開啟命令列工具,執行:

npm install webpack --save-dev

如果是全域性安裝webpack,那麼在本文後面安裝完extract-text-webpack-plugin外掛後,執行時會報錯,所以建議本地安裝。

執行webpack

現在就可以執行webpack了,只不過,現在的Webpack並沒有任何的其他的能力,例如將scss轉為css。

在src/js 資料夾下,建一個common.js,公共指令碼將寫在這個檔案裡,裡面現在只寫一行程式碼:

alert("I'm common");

確認命令列的當前目錄為:D:\webpack-tutorial,然後執行:

webpack src/js/common.js dist/js/common.js

這樣將會在 dist/js資料夾下生成一個common.js檔案。接著在根目錄下建立一個 list.html,然後通過script標籤引用dist/js/common.js。用瀏覽器開啟list.html可以看到頁面彈出提示框。

開啟 dist/js/common.js,可以看到它和 src/js/common.js是不同的,新增了webpack的模組機制,後面引入多個模組時,會看到更詳細地看到它的作用:

/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports) { alert("I'm common"); /***/ } /******/ ]);

可以看到以上程式碼是很不緊湊的,甚至還包含了很多註釋。在生產環境下,我們希望程式碼更加緊湊一些,此時可以使用下面的命令:

webpack src/js/common.js dist/js/common.min.js -p

這樣會生成緊湊的js程式碼:

!function(r){function o(e){if(t[e])return t[e].exports;var n=t[e]={exports:{},id:e,loaded:!1};return r[e].call(n.exports,n,n.exports,o),n.loaded=!0,n.exports}var t={};return o.m=r,o.c=t,o.p="",o(0)}([function(r,o){alert("I'm common")}]);

使用CommonJS構建模組

修改common.js:

var text = "I'm common"; module.exports = text;

然後在src/js下新增另一個檔案list.js:

var text = require("./common"); alert(text); alert("I'm list");

可以看到,list.js依賴於common.js,接著,再次執行webpack:

webpack src/js/list.js dist/js/list.js

修改list.html,現在引用dist/js/list.js(之前是common.js)。開啟list.html,會看到它依次彈出了兩次文字框。現在開啟list.js,會看到它包含了src/common.js和src/list.js中的程式碼。

新增多個模組

假設這個專案需要用到jQuery,開啟命令列工具,安裝它:

npm install jquery -save

這會在webpack-tutorial目錄下生成一個node_modules資料夾,通過npm安裝的的模組都會在這個資料夾下。修改src/js/list.js,讓它引用jquery:

var $ = require("jquery"); // ... 中間無變化 $("body").css("background-color", "#aaaaaa");

使用code splitting

現在建立一個新的detail.html,並在src/js/下建立detail.js:

var $ = require("jquery"); var text = require("./common"); alert(text); alert("I'm detail"); $("body").css("background-color", "#aaaaff");

它和list幾乎是一摸一樣的,僅僅是為了示範多檔案的情況。開啟命令列,執行webpack:

webpack src/js/detail.js dist/js/detail.js

開啟dist/js資料夾,會發現detail.js和list.js檔案體積是一樣大的,這是因為它們都包含了common.js和jquery.js。

jquery.js的體積是比較大的,因此,很有必要將jquery.js打包成一個單獨的檔案,而list.js和detail.js分開打包。在webpack中,可以使用稱作code splitting(程式碼分拆)的技術來實現。

這裡jquery本來就是一個打包好的檔案,不需要打包,只需要引用。但實際專案中,可能會有很多個第三方的類庫或者框架,可以將它們打包到一起。

改寫list.js:

var text = require("./common"); alert(text); alert("I'm list"); require.ensure([], function(){ var $ = require("jquery"); $("body").css("background-color", "#aaaaaa"); }, "jquery");

執行:

webpack src/js/list.js dist/js/list.js --display-modules --display-chunks

注意到後面多了 --display-modules 和 --display-chunks選項,用以顯示檔案所包含的模組。

執行完成後,看到:

Hash: 087ab301df8508cffa3e Version: webpack 1.13.0 Time: 295ms Asset Size Chunks Chunk Names list.js 3.95 kB 0 [emitted] main 1.list.js 267 kB 1 [emitted] jquery chunk {0} list.js (main) 308 bytes [rendered] [0] ./src/js/list.js 248 bytes {0} [built] [1] ./src/js/common.js 60 bytes {0} [built] chunk {1} 1.list.js (jquery) 259 kB {0} [rendered] [2] ./~/jquery/dist/jquery.js 259 kB {1} [built]

可以看到生成了兩個檔案:list.js和1.list.js。其中list.js中包含了 list.js 和 common.js兩個檔案,而1.list.js包含了jquery.js。

注意上面的一列:Chunk Names。1.list.js的chunk名稱是jquery,這個是由上面require.ensure方法的第三個引數定義的。後面在使用配置檔案時會用到這個名稱。

接著使用瀏覽器開啟list.html,用偵錯程式檢視頁面元素,會發現為head部分新插入的script標籤,引用了1.list.js。

Webpack入門
使用Chrome檢視頁面

這裡,我們發現了第一個問題:引用的路徑不對

暫且先不管這個問題,來看一下code splitting的第二個作用:按需載入。修改一下list.js:

var text = require("./common"); alert(text); alert("I'm list"); if(document.getElementById("container")){ require.ensure([], function(){ var $ = require("jquery"); $("body").css("background-color", "#aaaaaa"); }, "jquery"); }

只有當頁面存在container元素的時候,才去載入jquery。因為list.html是一個空頁面,不包含id為container的元素。因此,並不會載入jquery。再次執行webpack,開啟list.html會發現頁面沒有再插入script標籤。

從list.js中刪除掉if判斷語句,再按上面相同的方式改寫一下detail.js,然後對detail.js執行webpack。那麼很快就會發現第二個問題:1.detail.js中也包含了jquery。這樣的話,多個檔案的共同部分只是拆分,而沒有合併。

當使用config配置檔案時,則很好解決上面的問題。除此以外,使用配置檔案也在執行webpack時也省去了很多的輸入。

使用config配置檔案

現在,在專案根目錄下建立一個配置檔案:webpack.config.js。

var src = "./src/js/", dist = "./dist/js/"; module.exports = { entry: { list: src + "list", detail: src + "detail" }, output:{ filename:"[name].js", chunkFilename:"[name].chunk[id].js", path: dist, publicPath: dist } };

這個配置檔案是一個純粹的js檔案,所以你可以寫一些應用邏輯進去。注意到這幾個配置項:

  • entry:輸入,當為物件時,key為chunk的名稱,值為模組路徑。
  • output.filename:輸出的js檔名。
  • output.chunkFilename:輸出的chunk檔名。
  • path:輸出的檔案路徑。
  • publicPath:頁面應用的路徑(即上一小節報錯的路徑)。

配置完成後,執行一下命令列:

D:\webpack-tutorial>webpack --display-modules --display-chunks Hash: 2ba5904a636a8ac64101 Version: webpack 1.13.0 Time: 856ms Asset Size Chunks Chunk Names detail.js 3.96 kB 0 [emitted] detail jquery.chunk1.js 267 kB 1 [emitted] jquery list.js 4.01 kB 2 [emitted] list chunk {0} detail.js (detail) 267 bytes [rendered] [0] ./src/js/detail.js 204 bytes {0} [built] [1] ./src/js/common.js 63 bytes {0} {2} [built] chunk {1} jquery.chunk1.js (jquery) 259 kB {0} {2} [rendered] [2] ./~/jquery/dist/jquery.js 259 kB {1} [built] chunk {2} list.js (list) 311 bytes [rendered] [0] ./src/js/list.js 248 bytes {2} [built] [1] ./src/js/common.js 63 bytes {0} {2} [built]

可以看到生成了三個檔案:jquery.chunk1.js,包含了jquery.js(配置檔案中的[name]即為require.ensure方法中的第三個引數);detail.js和list.js。下面再次開啟list.html,發現上一節的兩個問題都已經解決了。

使用CommonsChunkPlugin

在上面的例子中,我們使用Code Splitting將公共的jquery.js生成到了 jquery.chunk1.js 中,並可以進行按需載入(動態將script標籤插入到頁面head中)。

如果想將公共的js打包到同一個js檔案中,然後手動新增到頁面中,則可以使用CommonsChunkPlugin外掛。

修改webpack.config.js配置檔案:

var webpack = require("webpack"); module.exports = { plugins:[ new webpack.optimize.CommonsChunkPlugin({ name: 'lib', minChunks: 2 }) ] };

為了讓文章緊湊一點,配置檔案中相同的地方沒有再列出來了,下同。

重新執行webpack:

D:\webpack-tutorial>webpack --display-modules --display-chunks Hash: 881d51e5a0d94bc015bc Version: webpack 1.13.0 Time: 858ms Asset Size Chunks Chunk Names detail.js 265 bytes 0 [emitted] detail list.js 262 bytes 1 [emitted] list lib.js 271 kB 2 [emitted] lib chunk {0} detail.js (detail) 155 bytes {2} [rendered] [0] ./src/js/detail.js 155 bytes {0} [built] chunk {1} list.js (list) 152 bytes {2} [rendered] [0] ./src/js/list.js 152 bytes {1} [built] chunk {2} lib.js (lib) 259 kB [rendered] [1] ./src/js/common.js 63 bytes {2} [built] [2] ./~/jquery/dist/jquery.js 259 kB {2} [built]

可以看到生成了三個檔案 detail.js、list.js和 lib.js,其中lib.js包含了 common.js 和 jquery.js。

此時,要記得修改list.html和detail.html,將lib.js手動引入進來:

<script src="dist/js/lib.js"></script> <script src="dist/js/list.js"></script>

如果我們希望只將jquery和其他第三方庫打包到lib中(不包含common.js),則可以這樣配置webpack.config.js:

var webpack = require("webpack"); module.exports = { entry: { lib: ["jquery"], // 其他的庫,可以包含多個 list: src + "list", detail: src + "detail" }, plugins:[ new webpack.optimize.CommonsChunkPlugin({ name: 'lib', minChunks: Infinity }) ] };

執行結果如下:

D:\webpack-tutorial>webpack --display-modules --display-chunks Hash: c0a9b7c514a8a8ad5585 Version: webpack 1.13.0 Time: 839ms Asset Size Chunks Chunk Names detail.js 385 bytes 0 [emitted] detail lib.js 271 kB 1 [emitted] lib list.js 382 bytes 2 [emitted] list chunk {0} detail.js (detail) 218 bytes {1} [rendered] [0] ./src/js/detail.js 155 bytes {0} [built] [1] ./src/js/common.js 63 bytes {0} {2} [built] chunk {1} lib.js (lib) 259 kB [rendered] [0] multi lib 28 bytes {1} [built] [2] ./~/jquery/dist/jquery.js 259 kB {1} [built] chunk {2} list.js (list) 215 bytes {1} [rendered] [0] ./src/js/list.js 152 bytes {2} [built] [1] ./src/js/common.js 63 bytes {0} {2} [built]

可以看到list.js和detail.js中都包含了common.js,而lib.js中包含了第三方的jquery.js。這樣做的好處是:可以把第三方庫整體打包做CDN,而不會受到可能頻繁變動的common.js的影響。

使用 Webpack Loader處理多種資源

Webpack Loader有點類似於一個管道,用來增強Webpack的能力。如果我們需要解析scss程式碼,就需要scss-loader,經過這個管道以後scss就轉換為了css;如果需要解析ES6程式碼,就需要babel-loader,經過這個管道以後ES6就轉換為了ES5。等等,以此類推。

引入樣式

我們先看第一個loader:css loader。它用來將樣式表引入到當前檔案中。

先進行一下準備工作:

  • 在src/css/資料夾下建立一個list.css檔案,裡面就一行程式碼:body{background: #aaa;}
  • 因為現在已經不再演示處理多檔案了,所以在webpack.config.js中刪去detail: src + "detail" 這行。
  • 刪掉src/js/list.js中的所有程式碼

安裝css-loader:

npm install css-loader --save-dev

修改list.js

require("css!../css/list.css");

執行一遍webpack,可以看到dist/js/list.js將list.css包含了進去:

D:\webpack-tutorial>webpack --display-modules --display-chunks Hash: a669b2f88986bbfd0da0 Version: webpack 1.13.0 Time: 585ms Asset Size Chunks Chunk Names list.js 3.29 kB 0 [emitted] list chunk {0} list.js (list) 1.73 kB [rendered] [0] ./src/js/list.js 35 bytes {0} [built] [1] ./~/css-loader!./src/css/list.css 186 bytes {0} [built] [2] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]

這時候,如果開啟list.html,會發現什麼變化都沒有,這是因為css-loader的作用僅僅是將css檔案鏈入到指令碼中,卻不對它做任何處理。

此時,如果希望將樣式的內容渲染到頁面上,則需要安裝另一個loader:style-loader。

顯然,設計原則是:一個loader只幹一件事。

npm install style-loader --save-dev

接著修改list.js

require("style!css!../css/list.css");

這是一個鏈式語法,意思是:先用css-loader載入list.css檔案,然後再用style-loader渲染它。

再次執行webpack,然後開啟list.html,發現頁面已經載入了list.css樣式,頁面背景變成了灰色(#aaa)。

用Chrome瀏覽器檢視頁面原始碼,會發現樣式是嵌入到list.js中去的,而不是通過link標籤引入到頁面中。這樣就會帶來一個問題:頁面閃爍。當你頻繁重新整理頁面時,會發現在指令碼載入完成前,頁面是預設的白色,等到指令碼載入完成後才變成灰色。

對一個Web元件而言,它應當包含HTML結構,CSS樣式表,JS指令碼控制行為,還可能包含字型和圖片。這些統稱為資源assets,它們統統可以通過Webpack打包成一個js檔案。如果是開發一個Web元件,這麼做通常沒有太大問題,因為Web元件只是頁面的一小塊區域。但如果是全域性的樣式,資源載入前帶來的閃爍問題則會影響使用者體驗。
將圖片和HTML打包到js中,也需要相應的loader,這裡就不在演示了。

如此一來,我們希望將css檔案生成到dist/css資料夾中,再通過傳統的方式用link標籤進行引用,而不是生成到list.js指令碼中。現在,重新編輯一下webpack.config.js:

var src = "./src/js/", dist = "./dist/js/"; module.exports = { entry: { list: src + "list" }, output:{ filename:"[name].js", chunkFilename:"[name].chunk[id].js", path: dist, publicPath: dist }, module:{ loaders:[ { test: /\.css$/, loaders:["style", "css"] } ] } };

這裡新增了loaders的配置。然後修改list.js:

require("../css/list.css");

然後執行一遍webpack,會發現沒有任何改變,list.css依然是打包進了list.js中。上面修改配置,不過是將loader的使用規則放到了配置中,並沒有其他變化。

為了將css打包到dist/css資料夾,需要另一個webpack外掛:extract-text-webpack-plugin。開啟命令列,進行安裝:

npm install extract-text-webpack-plugin --save-dev

然後修改webpack.config.js配置檔案:

var src = "./src/js/", dist = "./dist/js/"; var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry: { list: src + "list" }, output:{ filename:"[name].js", chunkFilename:"[name].chunk[id].js", path: dist, publicPath: dist }, module:{ loaders:[ { test: /\.css$/, loader:ExtractTextPlugin.extract("style", "css") } ] }, plugins:[ new ExtractTextPlugin("../css/[name].css", { allChunks:true }) ] };

再次執行webpack,可以看到在dist資料夾下生成了list.css檔案。注意plugins中new ExtractTextPlugin的引數,第一個引數指定了生成的css檔案的位置,這個位置是相對於配置中的output.path(./dist/js/),而不是相當於專案的根目錄(D:\webpack-tutorial)。

修改list.html,在head中引用剛才生成的css檔案,可以看到一切正常:

<link href="dist/css/list.css" rel="stylesheet" />

上面做了這麼多的工作,相當於將list.css從src/css資料夾拷貝一份到dist/css資料夾,好像並沒有太大意義。正常情況下,是在src/css下編寫scss檔案,然後生成到dist/css中。可能你已經猜到了,我們又需要安裝一個loader了:sass-loader。

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

接著再次修改配置檔案:

module:{ loaders:[ { test: /\.css$/, loader:ExtractTextPlugin.extract("style", "css") }, { test: /\.scss$/, loader:ExtractTextPlugin.extract("style", "css!sass") } ] }

在上面多加了一個sass-loader用於處理.scss檔案。接著刪除src/css/list.css,然後建立list.scss檔案:

$bg-main:#aaa; body{ background:$bg-main; }

修改list.js require("../css/list.scss");。然後再次執行webpack,成功後可以看到dist/css下生成的css。如此一來,就可以在專案中編寫scss檔案了。

使用Babel處理ES6

2015年推出了ES6(ES2015),可惜現在瀏覽器的支援很有限。但好在有Babel這樣的神器,可以將ES6轉為現在瀏覽器所支援的ES5。有了上面的經驗,後面就容易很多了,因為都是大同小異的。

先安裝babel-loader和babel-preset-2015:

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

可能很多人會問:babel-preset-es2015是做什麼的?簡單說一下,就是babel本身也是啥都做不了,要藉助preset來完成,目的同樣是為了架構更簡潔吧。

修改webpack.config.js,加入babel-loader:

module:{ loaders:[ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel', query: { presets: ['es2015'] } } ] }

現在,修改list.js,編寫一段ES6程式碼:

var arr = [1,2,3].map(x=> x+1 ); alert(arr);

執行webpack,然後開啟dist/js/list.js,會看到lambda表示式 x=>x+1 已經轉為了匿名函式:

function(module, exports) { "use strict"; var arr = [1, 2, 3].map(function (x) { return x + 1; }); alert(arr); }

引入React

因為我們選用React作為前端UI框架,那麼就需要讓Webapck支援它。不同的是:React是Babel的一個預設,而非Webpack的一個Loader。現在先安裝它:

npm install react react-dom babel-preset-react --save-dev

這裡react和react-dom是React的核心模組;babel-preset-react則是用於babel的preset。

接著修改webpack.config.js:

{ test: /\.jsx?$/, loader: 'babel-loader', exclude:/node_modules/, query:{ presets: ['es2015', "react"] } }

在src/jsx資料夾下建立一個hello.jsx,用於測試React:

var React = require("react"); var ReactDOM = require("react-dom"); var HelloMessage = React.createClass({ render: function() { return <div>Hello {this.props.name}</div>; } }); ReactDOM.render(<HelloMessage name="張子陽" />, document.getElementById("container"));

修改src/js/list.js:

import Hello from "../jsx/hello.jsx";

這裡不可以require,只能import。

修改list.html,在body中加入一行程式碼:<div id="container"></div>。開啟list.html,會看到 “Hello 張子陽”的字樣。

生成的dist/js/list.js會變得非常龐大,因為包含了很多react的程式碼,可以使用前面提到的功能生成多個檔案,這裡就不再演示了。

總結

這篇文章中只能算作流程攻略,而非詳情解說。先掌握最常用的功能,高階功能需要的時候再研究就好了。現在簡短地回顧一下:我們先配置了webpack的環境,通過使用命令列來執行webpack,然後學習了程式碼分拆(Code splitting)、使用配置檔案和提取公共程式碼。最後,通過安裝和配置loader完成了對sass、ES6以及React的支援。下面就開始歡快地編碼吧!

感謝閱讀,希望這篇文章能給你帶來幫助!

相關文章