2.2 webpack

這一節中我們將系統的講解 webpack ,包括:

  • webpack 介紹

    • webpack 是什麼

    • 為什麼引入新的打包工具

    • webpack 核心思想

  • webpack 安裝

  • webpack 使用

    • 命令行調用

    • 配置文件

  • webpack 配置參數

    • entry 和 output

    • 單一入口

    • 多個入口

    • 多個打包目標

  • webpack 支持 Jsx 和 Es6

  • webpack loaders

    • loader 定義

    • loader 功能

    • loader 配置

    • 使用 loader

  • webpack 開發環境與生產環境

  • webpack 分割 vendor 代碼和應用業務代碼

  • webpack develop server

    • 安裝 webpack-dev-server

    • 啟動 webpack-dev-server

    • 代碼監控

    • 自動刷新

    • 熱載入 (hot module replacement)

    • 在 webpack.config.js 中配置 webpack develop server

2.2.1 webpack 介紹

webpack 是什麼

webpack is a module bundler. webpack takes modules with dependencies and generates static assets representing those modules

webpack 是一個模塊打包工具,輸入為包含依賴關係的模塊集,輸出為打包合併的前端靜態資源。在上一節的前端工程化中,已經介紹過,webpack 是同時支持 AMD 和 CommonJs 的模塊定義方式,不僅如此,webpack 可以將任何前端資源視為模塊,如 css,圖片,文本。

為什麼要引入新的打包工具

在 webpack 出現之前,已經有了一些打包工具,如 Browserify, 那為什麼不優化這些工具,而是重複造輪子?

webpack 之前的打包工具工具功能單一,只能完成特定的任務,然而 web 前端工程是複雜的,一個 webapp 對於業務代碼的要求可能有:

  1. 代碼可以分塊,實現按需載入

  2. 首屏載入時間要盡量減少

  3. 需要集成一些第三方庫

對於模塊打包工具,單一的支持 CommonJs 的打包在大型項目中是不夠用的,為了滿足一個大型項目的前端需求,那麼一個打包工具應該包含一些這些功能:

  1. 支持多個 bundler 輸出 -> 解決代碼分塊問題

  2. 非同步載入 -> 按需載入,優化首屏載入時間

  3. 可定製化 -> 可以集成第三方庫,可以定製化打包過程

  4. 其他資源也可以定義為模塊

webpack 的出現正式為了解決這些問題,在 webpack 中,提供了一下這些功能:

  1. 代碼分塊: webpack 有兩種類型的模塊依賴,一種是同步的,一種是非同步的。在打包的過程中可以將代碼輸出為代碼塊(chunk),代碼塊可以實現按需載入。 非同步載入的代碼塊通過分割點(spliting point)來確定。

  2. Loaders: Webpack 本身只會處理 Javascript,為了實現將其他資源也定義為模塊,並轉化為 Javascript, Webpack 定義 loaders , 不同的 loader 可以將對應的資源轉化為 Javascript 模塊。

  3. 智能的模塊解析: webpack 可以很容易將第三方庫轉化為模塊集成到項目代碼中,模塊的依賴可以用表達式的方式(這在其他打包工具中是沒有支持的),這種模塊依賴叫做動態模塊依賴。

  4. 插件系統: webpack 的可定製化在於其插件系統,其本身的很多功能也是通過插件的方式實現,插件系統形成了 webpack 的生態,是的可以使用很多開源的第三方插件。

webpack 核心思想

webpack 的三個核心:

  1. 萬物皆模塊:在 webpack 的世界中,除了 Javascript,其他任何資源都可以當做模塊的方式引用

  2. 按需載入: webapp 的優化關鍵在於代碼體積,當應用體積增大,實現代碼的按需載入是剛需,這也是 webpack 出現的根本原因

  3. 可定製化: 任何一個工具都不可能解決所有問題,提供解決方案才是最可行的,webpack 基於可定製化的理念構建,通過插件系統,配置文件,可以實現大型項目的定製需求。

2.2.2 安裝配置

第一步:Node.js

webpack 是 Node 實現,首先需要到 Node.js 下載安裝最新版本的 Node.js

第二步:webpack-cli

Node.js 安裝好過後,打開命令行終端,通過 npm 命令安裝:

// -g 參數表示全局安裝$ npm install webpack -g

第三步:新建空前端項目

為了使用 webpack,先新建一個空前端項目,創建一個目錄,目錄結構如下:

.├── index.html // 入口 HTML ├── dist // dist 目錄放置編譯過後的文件文件└── src // src 目錄放置源文件 └── index.js // 入口 js

其中 html 內容:

<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Hello React!</title></head><body> <div id="AppRoot"></div> <script src="dist/index.js"></script></body></html>

index.js 內容為:

alert("hello world webpack");

第四步:在項目中安裝 webpack

// 初始化 package.json, 根據提示填寫 package.json 的相關信息$ npm init// 下載 webpack 依賴 // --save-dev 表示將依賴添加到 package.json 中的 "devDependencies" 對象中$ npm install webpack --save-dev

* 第五步:Develop Server 工具 (可選)

dev server 可以實現一個基於 node + express 的前端 server

$ npm install webpack-dev-server --save-dev

2.2.3 webpack 使用

命令行調用

在之前創建的目錄下執行:

$ webpack src/index.js dist/index.js

執行成功過後會出現如下信息:

Hash: 9a8e7e83864a07c0842fVersion: webpack 1.13.1Time: 37ms Asset Size Chunks Chunk Namesindex.js 1.42 kB 0 [emitted] main [0] ./src/index.js 29 bytes {0} [built]

可以查看 dist/index.js 的編譯結果:

/******/ (function(modules) { // webpackBootstrap// .......... UMD 定義內容/******/ })/************************************************************************//******/ ([/* 0 *//***/ function(module, exports) { // index.js 的內容被打包進來 alert("hello world webpack");/***/ }/******/ ]);

在瀏覽器中打開 index.html :

配置文件

以命令執行的方式需要填寫很長的參數,所以 webpack 提供了通過配置的方式執行,在項目目錄下創建 webpack.config.js 如下:

var webpack = require("webpack")module.exports = { entry: "./src/index.js", output: { path: "./dist/", filename: "index.js" }}

執行:

$ webpack

會和通過命令執行有同樣的輸出

2.2.4 webpack 配置

entry 和 output

webpack 的配置中主要的兩個配置 key 是,entry 和 output。

{ entry: [String | Array | Object], // 入口模塊 output: { path: String, // 輸出路徑 filename: String // 輸出名稱或名稱 pattern publicPath: String // 指定靜態資源的位置 ... // 其他配置 }}

單一入口

如果只有一個入口文件,可以有如下幾種配置方式

// 第一種 String { entry: "./src/index.js", output: { path: "./dist/", filename: "index.js" }}// 第二種 Array { entry: ["./src/index.js"], output: { path: "./dist/", filename: "index.js" }}// 第三種 Object{ entry: { index: "./src/index.js", }, output: { path: "./dist/", filename: "index.js" }}

多個入口文件

當存在多個入口時 ,可以使用 Array 的方式,比如依賴第三方庫 bootstrap ,最終 bootstrap 會被追加到打包好的 index.js 中,數組中的最後一個會被 export。

{ entry: ["./src/index.js", "./vendor/bootstrap.min.js"], output: { path: "./dist", filename: "index.js" }}

最終的輸出結果如:

/******/ ([/* 0 *//***/ function(module, exports, __webpack_require__) { __webpack_require__(1); // export 最後一個 module.exports = __webpack_require__(2);/***/ },/* 1 *//***/ function(module, exports) { alert("hello world webpack");/***/ },/* 2 *//***/ function(module, exports) { // bootstrap 的內容被追加到模塊中 console.log("bootstrap file");/***/ }/******/ ])

多個打包目標

上面的例子中都是打包出一個 index.js 文件,如果項目有多個頁面,那麼需要打包出多個文件,webpack 可以用對象的方式配置多個打包文件

{ entry: { index: "./src/index.js", a: "./src/a.js" }, output: { path: "./dist/", filename: "[name].js" }}

最終會打包出:

.├── a.js└── index.js

文件名稱 pattern

  • [name] entry 對應的名稱

  • [hash] webpack 命令執行結果顯示的 Hash 值

  • [chunkhash] chunk 的 hash

為了讓編譯的結果名稱是唯一的,可以利用 hash 。

2.2.5 webpack 支持 Jsx

現在我們已經可以使用 webpack 來打包基於 CommonJs 的 Javascript 模塊了,但是還沒法解析 JSX 語法和 Es6 語法。下面我們將利用 Babel 讓 webpack 能夠解析 Es6 和 Babel

第一步:npm install 依賴模塊

// babel 相關的模塊$ npm install babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-polyfill --save-dev// react 相關的模塊$ npm install react react-dom --save

第二步:webpack.config.js 中添加 babel loader 配置

{ entry: { index: "./src/index.js", a: "./src/a.js" }, output: { path: "./dist/", filename: "[name].js" }, module: { loaders: [{ test: /.js$/, exclude: /node_modules/, loader: "babel", query: { presets: ["es2015", "stage-0", "react"] } }] }}

第三步: 修改 index.js 為 React 的語法

src/index.js 內容改為:

Es6 的知識在後面的章節中講解,目前我們暫時以 Es5 的方式來寫,但是配置已經支持了 Es6 的編譯,熟悉 Es6 的讀者也可以直接寫 Es6

// 通過 require 的方式依賴 React,ReactDOMvar React = require("react");var ReactDOM = require("react-dom");var Hello = React.createClass({ render: function render() { return <div>Hello {this.props.name}</div>; }});ReactDOM.render( <Hello name="World" />, document.getElementById("AppRoot"));

第四步:運行 webpack

$ webpack

執行結果:

Hash: ae2a037c191c18195b6aVersion: webpack 1.13.1Time: 1016ms Asset Size Chunks Chunk Names a.js 1.42 kB 0 [emitted] aindex.js 700 kB 1 [emitted] index + 169 hidden modules

瀏覽器中打開 index.html 會顯示 Hello World

2.2.6 webpack loaders

在配置 JSX 的過程中,使用到了 loader, 前面已經介紹過 webpack 的核心功能包含 loader,通過 loader 可以將任意資源轉化為 javascript 模塊。

loader 定義

Loaders are transformations that are applied on a resource file of your app.

(Loaders 是應用中源碼文件的編譯轉換器)

也就是說在 webpack 中,通過 loader 可以實現 JSX 、Es6、CoffeeScript 等的轉換

loader 功能

  1. loader 管道:在同一種類型的源文件上,可以同時執行多個 loader , loader 的執行方式可以類似管道的方式,管道執行的方式是從右到左的方式loader 可以支持同步和非同步

  2. loader 可以接收配置參數

  3. loader 可以通過正則表達式或者文件後綴指定特定類型的源文件

  4. 插件可以提供給 loader 更多功能

  5. loader 除了做文件轉換以外,還可以創建額外的文件

loader 配置

新增 loader 可以在 webpack.config.js 的 module.loaders 數組中新增一個 loader 配置。

一個 loader 的配置為:

{ // 通過擴展名稱和正則表達式來匹配資源文件 test: String , // 匹配到的資源會應用 loader, loader 可以為 string 也可以為數組 loader: String | Array}

感嘆號和數組可以定義 loader 管道:

{ module: { loaders: [ { test: /.jade$/, loader: "jade" }, // => .jade 文件應用 "jade" loader { test: /.css$/, loader: "style!css" }, { test: /.css$/, loaders: ["style", "css"] }, // => .css 文件應用 "style" 和 "css" loader ] }}

loader 可以配置參數

{ module: { loaders: [ // => url-loader 配置 mimetype=image/png 參數 { test: /.png$/, loader: "url-loader?mimetype=image/png" }, { test: /.png$/, loader: "url-loader", query: { mimetype: "image/png" } } ] }}

使用 loader

第一步: 安裝

loader 和 webpack 一樣都是 Node.js 實現,發布到 npm 當中,需要使用 loader 的時候,只需要

$ npm install xx-loader --save-dev// eg css loader$ npm install css-loader style-loader --save-dev

第二步:修改配置

{ entry: { index: "./src/index.js", a: "./src/a.js" }, output: { path: "./dist/", filename: "[name].js" }, module: { loaders: [{ test: /.js$/, exclude: /node_modules/, loader: "babel", query: { presets: ["es2015", "stage-0", "react"] } }, { test: /.css$/, loader: "style-loader!css-loader" }] }}

第三步:使用

前面我們已經使用過 jsx loader 了, loader 的使用方式有多種

  1. 在配置文件中配置

  2. 顯示的通過 require 調用

  3. 命令行調用

顯示的調用 require 會增加模塊的耦合度,應盡量避免這種方式

以 css-loader 為例子,在項目 src 下面創建一個 css

src/style.css

body { background: red; color: white;}

修改 webpack 配置 entry 添加

entry: { index: ["./src/index.js", "./src/style.css"]}

執行 webpack 命令然後打開 index.html 會看到頁面背景被改為紅色。

最終的編譯結果為:

....function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(171)(); exports.push([module.id, "
body {
background: red;
color: white;
}
"
, ""]);}....

可以看到 css 被轉化為了 javascript, 在頁面中並非調用 <link rel="stylesheet" href=""> 的方式, 而是使用 inline 的<style>.....</style>

另外一種方法是直接 require, 修改 src/index.js:

var css = require("css!./style.css");

編譯結果相同。

2.2.7 webpack 開發環境與生產環境

前端開發環境通常分為兩種,開發環境和生成環境,在開發環境中,可能我們需要日誌輸出,sourcemap ,錯誤報告等功能,在生成環境中,需要做代碼壓縮,hash 值生成。兩種環境在其他的一些配置上也可能不同。

所以為了區分,我們可以創建兩個文件:

  • webpack.config.js // 開發環境

  • webpack.config.prod.js // 生產環境

生產環境 build 用如下命令:

$ webpack --config webpack.config.prod.js

在本章深入 webpack 小節中會更多的介紹生產環境中的優化

2.2.8 webpack 插件

webpack 提供插件機制,可以對每次 build 的結果進行處理。配置 plugin 的方法為在 webpack.config.js 中添加:

{ plugins: [ new BellOnBundlerErrorPlugin() ]}

plugin 也是一個 npm 模塊,安裝一個 plugin :

$ npm install bell-on-bundler-error-plugin --save-dev

2.2.9 webpack 分割 vendor 代碼和應用業務代碼

在上面的 jsx 配置中,我們將 React 和 ReactDOM 一起打包進了項目代碼。為了實現業務代碼和第三方代碼的分離,我們可以利用

CommonsChunkPlugin 插件.

修改 webpack.config.js

{ entry: { index: "./src/index.js", a: "./src/a.js", // 第三方包 vendor: [ "react", "react-dom" ] }, output: { path: "./dist/", filename: "[name].js" }, module: { loaders: [{ test: /.js$/, exclude: /node_modules/, loader: "babel", query: { presets: ["es2015", "stage-0", "react"] } }, { test: /.css$/, loader: "style-loader!css-loader" }] }, plugins: [ new webpack.optimize.CommonsChunkPlugin(/* chunkName= */"vendor", /* filename= */"vendor.bundle.js") ]}

執行 webpack 命令,輸出日誌:

Hash: f1256dc00b9d4bde8f7fVersion: webpack 1.13.1Time: 1459ms Asset Size Chunks Chunk Names a.js 109 bytes 0 [emitted] a index.js 10.9 kB 1 [emitted] indexvendor.bundle.js 702 kB 2 [emitted] vendor [0] multi vendor 40 bytes {2} [built] [0] multi index 40 bytes {1} [built] + 173 hidden modules

index.js 體積變小了,多出了 vendor.bundle.js

2.2.10 webpack develop server

在前端開發的過程中,通常需要啟動一個伺服器,把開發打包好的前端代碼放在伺服器上,通過訪問伺服器訪問並測試(因為可以有些情況需要 ajax 請求)。 webpack 提供了一個基於 node.js Express 的伺服器 - webpack-dev-server 來幫助我們簡化伺服器的搭建,並提供伺服器資源訪問的一些簡單配置。

安裝 webpack-dev-server

$ npm install webpack-dev-server -g

啟動 webpack-dev-server

$ webpack-dev-server --content-base ./

--content-base ./ 參數表示將當前目錄作為 server 根目錄。 命令啟動過後,會在 8080 埠啟動一個 http 服務,通過訪問localhost:8080/index.ht 可以訪問 index.html 內容。

如果訪問提示報錯:

Uncaught ReferenceError: webpackJsonp is not defined

原因是 html 中沒有引用 vendor.bundle.js, 修改 html :

<!-- vendor 必須先於 index.js --><script src="dist/vendor.bundle.js"></script><script src="dist/index.js"></script>

修改 index.html 過後可以看到正確結果

代碼監控

webpack-dev-server 除了提供 server 服務以外, 還會監控源文件的修改,如果源文件改變了,會調用 webpack 重新打包

修改 style.css 中的內容為:

body { background: whitesmoke; color: #333; font-size: 100px;}

可以看到輸出以下日誌:

[168] ./~/react/lib/renderSubtreeIntoContainer.js 466 bytes {2} [built]webpack: bundle is now VALID.webpack: bundle is now INVALID.Hash: cc7d7720b1a0fcbef972Version: webpack 1.13.0Time: 76mschunk {0} a.js (a) 32 bytes {2} + 1 hidden moduleschunk {1} index.js (index) 10.3 kB {2} [170] ./~/css-loader!./src/style.css 230 bytes {1} [built] + 5 hidden moduleschunk {2} vendor.bundle.js (vendor) 665 kB + 168 hidden moduleswebpack: bundle is now VALID.

這個時候說明代碼已經修改了,但是這個時候刷新瀏覽器過後,背景是沒有改變的,原因是 webpack-dev-server 的打包結果是放在內存的,查看 dist/index.js 的內容實際上是沒有改變的,那如何訪問內存中的打包內容呢?

修改 webpack.config.js 的 output.publicPath:

output: { path: "./dist/", filename: "[name].js", publicPath: "/dist" // webpack-dev-server 啟動目錄是 `/`, `/dist` 目錄是打包的目標目錄相對於啟動目錄的路徑 },

重新啟動

$ ctrl + c 結束進程$ webpack-dev-server

修改 style.css 再刷新頁面,修改的內容會反映出來。

自動刷新

上面的配置已經能做到自動監控代碼,每次修改完代碼,刷新瀏覽器就可以看到最新結果,但是 webpack-dev-server 還提供了自動刷新功能,有兩種模式。

Iframe 模式

修改訪問的路徑: localhost:8080/index.ht -> localhost:8080/webpack- 。這個時候每次修改代碼,打包完成過後都會自動刷新頁面。

  • 不需要額外配置,只用修改路徑

  • 應用被嵌入了一個 iframe 內部,頁面頂部可以展示打包進度信息

  • 因為 iframe 的關係,如果應用有多個頁面,無法看到當前應用的 url 信息

inline 模式

啟動 webpack-dev-server 的時候添加 --inline 參數

  • 需要添加 --inline 配置參數

  • 沒有頂部信息提示條,提示信息在控制台中展現

熱載入 (hot module replacement)

webpack-dev-server 還提供了模塊熱載入的方式,在不刷新瀏覽器的條件下,應用最新的代碼更新,啟動 webpack-dev-server 的時候添加 --inline --hot 參數就可以體驗。

$ webpack-dev-server --inline --hot

修改代碼在瀏覽器控制台中會看到這樣的日誌輸出:

[HMR] Waiting for update signal from WDS...vendor.bundle.js:670 [WDS] Hot Module Replacement enabled.2vendor.bundle.js:673 [WDS] App updated. Recompiling...vendor.bundle.js:738 [WDS] App hot update...vendor.bundle.js:8152 [HMR] Checking for updates on the server...vendor.bundle.js:8186 [HMR] Updated modules:vendor.bundle.js:8188 [HMR] - 245vendor.bundle.js:8138 [HMR] App is up to date.

在 webpack.config.js 中配置 webpack develop server

修改 webpack.config.js 添加:

plugins: [ new webpack.optimize.CommonsChunkPlugin( /* chunkName= */"vendor", /* filename= */"vendor.bundle.js", Infinity), // 需要手動添加 HotModuleReplacementPlugin , 命令行的方式會自動添加 new webpack.HotModuleReplacementPlugin()],devServer: { hot: true, inline: true}

不加參數直接執行 webpack-dev-server

$ webpack-dev-server

webpack-dev-server 還提供了其他的一些功能, 如:

  1. 配置 proxy

  2. 訪問 node.js API

  3. 和現有的 node 服務集成

基於這些功能可以實現很多自定義的配置。


推薦閱讀:

React把PropTypes放到一個獨立包
Rx 的編程方式(一)
前端 UI組件化的一些思考
在React.js中使用PureComponent的重要性和使用方式

TAG:React | 前端开发 | 前端工程化 |