Webpack 3 從入門到放棄

2021-02-07 SegmentFault

Update (2017.8.27) : 關於 output.publicPath、 devServer.contentBase、 devServer.publicPath的區別。如下:

output.publicPath: 對於這個選項,我們無需關注什麼絕對相對路徑,因為兩種路徑都可以。我們只需要知道一點:這個選項是指定 HTML 文件中資源文件 (字體、圖片、JS文件等) 的 文件名的公共 URL 部分的。在實際情況中,我們首先會通過 output.filename或有些 loader 如 file-loader的 name屬性設置 文件名的原始部分,webpack 將 文件名的原始部分和公共部分結合之後,HTML 文件就能獲取到資源文件了。

devServer.contentBase: 設置靜態資源的根目錄, html-webpack-plugin生成的 html 不是靜態資源。當用 html 文件裡的地址無法找到靜態資源文件時就會去這個目錄下去找。

devServer.publicPath: 指定瀏覽器上訪問所有 打包(bundled)文件 (在 dist裡生成的所有文件) 的根目錄,這個根目錄是相對伺服器地址及埠的,比 devServer.contentBase和 output.publicPath優先。

前言

如果你用過 webpack 且一直用的是 webpack 1,請參考 從v1遷移到v2 (v2 和 v3 差異不大) 對版本變更的內容進行適當的了解,然後再選擇性地閱讀本文。

首先,這篇文章是根據當前最新的 webpack 版本 (即 v3.4.1) 撰寫,較長一段時間內無需擔心過時的問題。其次,這應該會是一篇極長的文章,涵蓋了基本的使用方法,有更高級功能的需求可以參考官方文檔繼續學習。再次,即使是基本的功能,也內容繁多,我儘可能地解釋通俗易懂,將我學習過程中的疑惑和坑一一解釋,如有紕漏,敬請雅正。再次,為了清晰有效地講解,我會演示從零編寫 demo,只要一步步跟著做,就會清晰許多。最後,官方文檔也是個坑爹貨!

Webpack,何許人也?

借用官方的說法:

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

簡言之,webpack 是一個模塊打包器 (module bundler),能夠將任何資源如 JavaScript 文件、CSS 文件、圖片等打包成一個或少數文件。

為什麼要用介個 Webpack?

首先,定義已經說明了 webpack 能將多個資源模塊打包成一個或少數文件,這意味著與以往的發起多個 HTTP 請求來獲得資源相比,現在只需要發起少量的 HTTP 請求。

想了解合併 HTTP 請求的意義,請見這裡:https://www.zhihu.com/question/34401250?from=profilequestioncard

其次,webpack 能將你的資源轉換為最適合瀏覽器的「格式」,提升應用性能。比如只引用被應用使用的資源 (剔除未被使用的代碼),懶加載資源 (只在需要的時候才加載相應的資源)。再次,對於開發階段,webpack 也提供了實時加載和熱加載的功能,大大地節省了開發時間。除此之外,還有許多優秀之處之處值得去挖掘。不過,webpack 最核心的還是打包的功能。

webpack,gulp/grunt,npm,它們有什麼區別?

webpack 是模塊打包器(module bundler),把所有的模塊打包成一個或少量文件,使你只需加載少量文件即可運行整個應用,而無需像之前那樣加載大量的圖片,css文件,js文件,字體文件等等。而gulp/grunt 是自動化構建工具,或者叫任務運行器(task runner),是把你所有重複的手動操作讓代碼來做,例如壓縮JS代碼、CSS代碼,代碼檢查、代碼編譯等等,自動化構建工具並不能把所有模塊打包到一起,也不能構建不同模塊之間的依賴圖。兩者來比較的話,gulp/grunt 無法做模塊打包的事,webpack 雖然有 loader 和 plugin可以做一部分 gulp/grunt 能做的事,但是終究 webpack 的插件還是不如 gulp/grunt 的插件豐富,能做的事比較有限。於是有人兩者結合著用,將 webpack 放到 gulp/grunt 中用。然而,更好的方法是用 npm scripts 取代 gulp/grunt,npm 是 node 的包管理器 (node package manager),用於管理 node 的第三方軟體包,npm 對於任務命令的良好支持讓你最終省卻了編寫任務代碼的必要,取而代之的,是老祖宗的幾個命令行,僅靠幾句命令行就足以完成你的模塊打包和自動化構建的所有需求。

準備開始

先來看看一個 webpack 的一個完備的配置文件,是 介樣 的,當然啦,這裡面有很多配置項是即使到這個軟體被廢棄你也用不上的:),所以無需擔心。

基本配置

開始之前,請確定你已經安裝了當前 Node 的較新版本。

然後執行以下命令以新建我們的 demo 目錄:

$ mkdir webpack-demo && cd webpack-demo && npm init -y

$ npm i --save-dev webpack

$ mkdir src && cd src && touch index.js

我們使用工具函數庫 lodash 來演示我們的 demo。先安裝之:

$ npm i --save lodash

src/index.js

import _ from 'lodash';

function component() {

 const element = document.createElement('div');

 element.innerHTML = _.join(['Hello', 'webpack'], ' ');

 return element;

}

document.body.appendChild(component());

import 和 export 已經是 ES6 的標準,但是仍未得到大多數瀏覽器的支持 (可喜的是, Chrome 61 已經開始默認支持了,見 ES6 modules),不過 webpack 提供了對這個特性的支持,但是除了這個特性,其他的 ES6 特性並不會得到 webpack 的特別支持,如有需要,須藉助 Babel 進行轉譯 (transpile)。

然後新建發布版本目錄:

$ cd .. && mkdir dist && cd dist && touch index.html

dist/index.html

<!DOCTYPE html>

<html>

<head>

   <title>webpack demo</title>

</head>

<body>

   <script src="bundle.js"></script>

</body>

</html>

現在,我們運行 webpack 來打包 index.js 為 bundle.js,本地安裝了 webpack 後可以通過 node_modules/.bin/webpack 來訪問 webpack 的二進位版本。

$ cd ..

$ ./node_modules/.bin/webpack src/index.js dist/bundle.js # 第一個參數是打包的入口文件,第二個參數是打包的出口文件

咻咻咻,大致如下輸出一波:

Hash: de8ed072e2c7b3892179

Version: webpack 3.4.1

Time: 390ms

   Asset    Size  Chunks                    Chunk Names

bundle.js  544 kB       0  [emitted]  [big]  main

  [0] ./src/index.js 225 bytes {0} [built]

  [2] (webpack)/buildin/global.js 509 bytes {0} [built]

  [3] (webpack)/buildin/module.js 517 bytes {0} [built]

   + 1 hidden module

現在,你已經得到了你的第一個打包文件 (bundle.js) 了。

使用配置文件

像上面這樣使用 webpack 應該是最挫的姿勢了,所以我們要使用 webpack 的配置文件來提高我們的姿勢水平。

$ touch webpack.config.js

webpack.config.js

const path = require('path');

module.exports = {

 entry: './src/index.js', // 入口起點,可以指定多個入口起點

 output: { // 輸出,只可指定一個輸出配置

   filename: 'bundle.js', // 輸出文件名

   path: path.resolve(__dirname, 'dist') // 輸出文件所在的目錄

 }

};

執行:

$ ./node_modules/.bin/webpack --config webpack.config.js # `--config` 制定 webpack 的配置文件,默認是 `webpack.config.js`

所以這裡可以省卻 --config webpack.config.js。但是每次都要寫 ./node_modules/.bin/webpack 實在讓人不爽,所以我們要動用 NPM Scripts

package.json

{

 ...

 "scripts": {

   "build": "webpack"

 },

 ...

}

在 npm scripts 中我們可以通過包名直接引用本地安裝的 npm 包的二進位版本,而無需編寫包的整個路徑。

執行:

$ npm run build

一波輸出後便得到了打包文件。

bulid 並不是 npm scripts 的內置屬性,需要使用 npm run 來執行腳本,詳情見 npm run

打包其他類型的文件

因為其他文件和 JS 文件類型不同,要把他們加載到 JS 文件中就需要經過加載器 (loader) 的處理。

加載 CSS

我們需要安裝兩個 loader 來處理 CSS 文件:

$ npm i --save-dev style-loader css-loader

style-loader 通過插入 <style> 標籤將 CSS 加入到 DOM 中,css-loader 會像解釋 import/require() 一樣解釋 @import 和 url()。

const path = require('path');

module.exports = {

 entry: './src/index.js',

 output: {

   filename: 'bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 module: { // 如何處理項目中不同類型的模塊

   rules: [ // 用於規定在不同模塊被創建時如何處理模塊的規則數組

     {

       test: /\.css$/, // 匹配特定文件的正則表達式或正則表達式數組

       use: [ // 應用於模塊的 loader 使用列表

         'style-loader',

         'css-loader'

       ]

     }

   ]

 }

};

我們來創建一個 CSS 文件:

$ cd src && touch style.css

src/style.css

.hello {

 color: red;

}

src/index.js

import _ from 'lodash';

import './style.css'; // 通過`import`引入 CSS 文件

function component() {

 const element = document.createElement('div');

 element.innerHTML = _.join(['Hello', 'webpack'], ' ');

 element.classList.add('hello'); // 在相應元素上添加類名

 return element;

}

document.body.appendChild(component());

執行 npm run build,然後打開 index.html,就可以看到紅色的字體了。CSS 文件此時已經被打包到 bundle.js 中。再打開瀏覽器控制臺,就可以看到 webpack 做了些什麼。

加載圖片

$ npm install --save-dev file-loader

file-loader 指示 webpack 以文件格式發出所需對象並返回文件的公共URL,可用於任何文件的加載。

webpack.config.js

const path = require('path');

module.exports = {

 entry: './src/index.js',

 output: {

   filename: 'bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 module: {

   rules: [

     {

       test: /\.css$/,

       use: [

         'style-loader',

         'css-loader'

       ]

     },

     { // 增加加載圖片的規則

       test: /\.(png|svg|jpg|gif)$/,

       use: [

         'file-loader'

       ]

     }

   ]

 }

};

我們在當前項目的目錄中如下增加圖片:

 webpack-demo

 |- package.json

 |- webpack.config.js

 |- /dist

   |- bundle.js

   |- index.html

 |- /src

+   |- icon.jpg

   |- style.css

   |- index.js

 |- /node_modules

src/index.js

import _ from 'lodash';

import './style.css';

import Icon from './icon.jpg'; // Icon 是圖片的 URL

function component() {

 const element = document.createElement('div');

 element.innerHTML = _.join(['Hello', 'webpack'], ' ');

 element.classList.add('hello');

 const myIcon = new Image();

 myIcon.src = Icon;

 element.appendChild(myIcon);

 return element;

}

document.body.appendChild(component());

src/style.css

.hello {

 color: red;

 background: url(./icon.jpg);

}

再 npm run build之。現在你可以看到單獨的圖片和以圖片為基礎的背景圖了。

加載字體

加載字體用的也是 file-loader。

webpack.config.js

const path = require('path');

module.exports = {

 entry: './src/index.js',

 output: {

   filename: 'bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 module: {

   rules: [

     {

       test: /\.css$/,

       use: [

         'style-loader',

         'css-loader'

       ]

     },

     {

       test: /\.(png|svg|jpg|gif)$/,

       use: [

         'file-loader'

       ]

     },

     { // 增加加載字體的規則

       test: /\.(woff|woff2|eot|ttf|otf)$/,

       use: [

         'file-loader'

       ]

     }

   ]

 }

};

在當前項目的目錄中如下增加字體:

 webpack-demo

 |- package.json

 |- webpack.config.js

 |- /dist

   |- bundle.js

   |- index.html

 |- /src

+   |- my-font.ttf

   |- icon.jpg

   |- style.css

   |- index.js

 |- /node_modules

src/style.css

@font-face {

 font-family: MyFont;

 src: url(./my-font.ttf);

}

.hello {

 color: red;

 background: url(./icon.jpg);

 font-family: MyFont;

}

運行打包命令之後便可以看到打包好的文件和發生改變的頁面。

加載 JSON 文件

因為 webpack 對 JSON 文件的支持是內置的,所以可以直接添加。

src/data.json

{

 "name": "webpack-demo",

 "version": "1.0.0",

 "author": "Sam Yang"

}

src/index.js

import _ from 'lodash';

import './style.css';

import Icon from './icon.jpg';

import Data from './data.json'; // Data 變量包含可直接使用的 JSON 解析得到的對象

function component() {

 const element = document.createElement('div');

 element.innerHTML = _.join(['Hello', 'webpack'], ' ');

 element.classList.add('hello');

 const myIcon = new Image();

 myIcon.src = Icon;

 element.appendChild(myIcon);

 console.log(Data);

 return element;

}

document.body.appendChild(component());

關於其他文件的加載,可以尋求相應的 loader。

輸出管理

前面我們只有一個輸入文件,但現實是我們往往有不止一個輸入文件,這時我們就需要輸入多個入口文件並管理輸出文件。我們在 src 目錄下增加一個 print.js 文件。

src/print.js

export default function printMe() {

 console.log('I get called from print.js!');

}

src/index.js

import _ from 'lodash';

import printMe from './print.js';

// import './style.css';

// import Icon from './icon.jpg';

// import Data from './data.json';

function component() {

 const element = document.createElement('div');

 const btn = document.createElement('button');

 element.innerHTML = _.join(['Hello', 'webpack'], ' ');

 // element.classList.add('hello');

 // const myIcon = new Image();

 // myIcon.src = Icon;

 // element.appendChild(myIcon);

 // console.log(Data);

 btn.innerHTML = 'Click me and check the console!';

 btn.onclick = printMe;

 element.appendChild(btn);

 return element;

}

document.body.appendChild(component());

dist/index.html

<!DOCTYPE html>

<html>

<head>

   <title>webpack demo</title>

   <script src="./print.bundle.js"></script>

</head>

<body>

   <!-- <script src="bundle.js"></script> -->

   <script src="./app.bundle.js"></script>

</body>

</html>

webpack.config.js

const path = require('path');

module.exports = {

 // entry: './src/index.js',

 entry: {

   app: './src/index.js',

   print: './src/print.js'

 },

 output: {

   // filename: 'bundle.js',

   filename: '[name].bundle.js', // 根據入口起點名動態生成 bundle 名,可以使用像 "js/[name]/bundle.js" 這樣的文件夾結構

   path: path.resolve(__dirname, 'dist')

 },

 // ...

};

filename:'[name].bundle.js'中的 [name]會替換為對應的入口起點名,其他可用的替換請參見 output.filename

現在可以打包文件了。但是如果我們修改了入口文件名或增加了入口文件, index.html是不會自動引用新文件的,而手動修改實在太挫。是時候使用插件 (plugin) 來完成這一任務了。我們使用 HtmlWebpackPlugin 自動生成 html 文件。

loader 和 plugin,有什麼區別?loader (加載器),重在「加載」二字,是用於預處理文件的,只用於在加載不同類型的文件時對不同類型的文件做相應的處理。而 plugin (插件),顧名思義,是用來增加 webpack 的功能的,作用於整個 webpack 的構建過程。在 webpack 這個大公司中,loader 是保安大叔,負責對進入公司的不同人員的處理,而 plugin 則是公司裡不同職位的職員,負責公司裡的各種不同業務,每增加一種新型的業務需求,我們就需要增加一種 plugin。

安裝插件:

$ npm i --save-dev html-webpack-plugin

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {

 // entry: './src/index.js',

 entry: {

   app: './src/index.js',

   print: './src/print.js'

 },

 output: {

   // filename: 'bundle.js',

   filename: '[name].bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 plugins: [ // 插件屬性,是插件的實例數組

   new HtmlWebpackPlugin({

     title: 'webpack demo',  // 生成 HTML 文檔的標題

     filename: 'index.html' // 寫入 HTML 文件的文件名,默認 `index.html`

   })

 ],

 // ...

};

你可以先把 dist 文件夾的 index.html文件刪除,然後執行打包命令。咻咻咻,我們看到 dist 目錄下已經自動生成了一個 index.html文件,但即使不刪除原先的 index.html,該插件默認生成的 index.html也會替換原本的 index.html。

此刻,當你細細觀察 dist 目錄時,雖然現在生成了新的打包文件,但原本的打包文件 bundle.js及其他不用的文件仍然存在在 dist 目錄中,所以在每次構建前我們需要晴空 dist 目錄,我們使用 CleanWebpackPlugin 插件。

$ npm i clean-webpack-plugin --save-dev

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {

 // entry: './src/index.js',

 entry: {

   app: './src/index.js',

   print: './src/print.js'

 },

 output: {

   // filename: 'bundle.js',

   filename: '[name].bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 plugins: [

   new HtmlWebpackPlugin({

     title: 'webpack demo',

     filename: 'index.html'

   }),

   new CleanWebpackPlugin(['dist']) // 第一個參數是要清理的目錄的字符串數組

 ],

 // ...

};

打包之,現在,dist 中只存在打包生成的文件。

開發環境

webpack 提供了很多便於開發時使用的功能,來一一看看吧。

使用代碼映射 (source map)

當你的代碼被打包後,如果打包後的代碼發生了錯誤,你很難追蹤到錯誤發生的原始位置,這個時候,我們就需要代碼映射 (source map) 這種工具,它能將編譯後的代碼映射回原始的源碼,你的錯誤是起源於打包前的 b.js的某個位置,代碼映射就能告訴你錯誤是那個模塊的那個位置。webpack 默認提供了 10 種風格的代碼映射,使用它們會明顯影響到構建 (build) 和重構建 (rebuild,每次修改後需要重新構建) 的速度,十種風格的差異可以參看 devtool。關於如何選擇映射風格可以參看 Webpack devtool source map。這裡,我們為了準確顯示錯誤位置,選擇速度較慢的 inline-source-map。

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {

 devtool: 'inline-source-map', // 控制是否生成以及如何生成 source map

 // entry: './src/index.js',

 entry: {

   app: './src/index.js',

   print: './src/print.js'

 },

 // ...

};

現在來手動製造一些錯誤:

src/print.js

 export default function printMe() {

-   console.log('I get called from print.js!');

+   cosnole.log('I get called from print.js!');

 }

打包之後打開 index.html再點擊按鈕,你就會看到控制臺顯示如下報錯:

Uncaught ReferenceError: cosnole is not defined

   at HTMLButtonElement.printMe (print.js:2)

現在,我們很清楚哪裡發生了錯誤,然後輕鬆地改正之。

使用 webpack-dev-server

你一定有這樣的體驗,開發時每次修改代碼保存後都需要重新手動構建代碼並手動刷新瀏覽器以觀察修改效果,這是很麻煩的,所以,我們要實時加載代碼。可喜的是,webpack 提供了對實時加載代碼的支持。我們需要安裝 webpack-dev-server 以獲得支持。

$ npm i --save-dev webpack-dev-server

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {

 devtool: 'inline-source-map',

 devServer: { // 檢測代碼變化並自動重新編譯並自動刷新瀏覽器

   contentBase: path.resolve(__dirname, 'dist') // 設置靜態資源的根目錄

 },

 // entry: './src/index.js',

 entry: {

   app: './src/index.js',

   print: './src/print.js'

 },

 // ...

};

package.json

{

 ...

 "scripts": {

   "build": "webpack",

   "start": "webpack-dev-server --open"

 },

 ...

}

使用 webpack-dev-server 時,webpack 並沒有將所有生成的文件寫入磁碟,而是放在內存中,提供更快的內存內訪問,便於實時更新。

現在,可以直接運行 npm start ( start是 npm scripts 的內置屬性,可直接運行),然後瀏覽器自動加載應用的頁面,默認在 localhost:8080顯示。

模塊熱替換 (HMR, Hot Module Replacement)

webpack 提供了對模塊熱替換 (或者叫熱加載) 的支持。這一特性能夠讓應用運行的時候替換、增加或刪除模塊,而無需進行完全的重載。想進一步地了解其工作機理,可以參見 Hot Module Replacement,但這並不是必需的,你可以選擇跳過機理部分繼續往下閱讀。

模塊熱替換(HMR)只更新發生變更(替換、添加、刪除)的模塊,而無需重新加載整個頁面(實時加載,LiveReload),這樣可以顯著加快開發速度,一旦打開了 webpack-dev-server 的 hot 模式,在試圖重新加載整個頁面之前,熱模式會嘗試使用 HMR 來更新。

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

const webpack = require('webpack'); // 引入 webpack 便於調用其內置插件

module.exports = {

 devtool: 'inline-source-map',

 devServer: {

   contentBase: path.resolve(__dirname, 'dist'),

   hot: true, // 告訴 dev-server 我們在用 HMR

   hotOnly: true // 指定如果熱加載失敗了禁止刷新頁面 (這是 webpack 的默認行為),這樣便於我們知道失敗是因為何種錯誤

 },

 // entry: './src/index.js',

 entry: {

   app: './src/index.js',

   // print: './src/print.js'

 },

 // ...

 plugins: [

   new HtmlWebpackPlugin({

     title: 'webpack demo',

     filename: 'index.html'

   }),

   new CleanWebpackPlugin(['dist']),

   new webpack.HotModuleReplacementPlugin(), // 啟用 HMR

   new webpack.NamedModulesPlugin() // 列印日誌信息時 webpack 默認使用模塊的數字 ID 指代模塊,不便於 debug,這個插件可以將其替換為模塊的真實路徑

 ],

 // ...

};

webpack-dev-server 會為每個入口文件創建一個客戶端腳本,這個腳本會監控該入口文件的依賴模塊的更新,如果該入口文件編寫了 HMR 處理函數,它就能接收依賴模塊的更新,反之,更新會向上冒泡,直到客戶端腳本仍沒有處理函數的話,webpack-dev-server 會重新加載整個頁面。如果入口文件本身發生了更新,因為向上會冒泡到客戶端腳本,並且不存在 HMR 處理函數,所以會導致頁面重載。

我們已經開啟了 HMR 的功能,HMR 的接口已經暴露在 module.hot屬性之下,我們只需要調用 HMR API即可實現熱加載。當「被加載模塊」發生改變時,依賴該模塊的模塊便能檢測到改變並接收改變之後的模塊。

src/index.js

import _ from 'lodash';

import printMe from './print.js';

// import './style.css';

// import Icon from './icon.jpg';

// import Data from './data.json';

function component() {

 const element = document.createElement('div');

 const btn = document.createElement('button');

 element.innerHTML = _.join(['Hello', 'webpack'], ' ');

 // element.classList.add('hello');

 // const myIcon = new Image();

 // myIcon.src = Icon;

 // element.appendChild(myIcon);

 // console.log(Data);

 btn.innerHTML = 'Click me and check the console!';

 btn.onclick = printMe;

 element.appendChild(btn);

 return element;

}

document.body.appendChild(component());

if(module.hot) { // 習慣上我們會檢查是否可以訪問 `module.hot` 屬性

 module.hot.accept('./print.js', function() { // 接受給定依賴模塊的更新,並觸發一個回調函數來對這些更新做出響應

   console.log('Accepting the updated printMe module!');

   printMe();

 });

}

npm start之。為了演示效果,我們做如下修改:

src/print.js

 export default function printMe() {

-   console.log('I get called from print.js!');

+   console.log('Updating print.js...');

 }

我們會看到控制臺列印出的信息中含有以下幾行:

index.js:33 Accepting the updated printMe module!

print.js:2 Updating print.js...

log.js:23 [HMR] Updated modules:

log.js:23 [HMR]  - ./src/print.js

log.js:23 [HMR] App is up to date.

webpack-dev-server 在 inline mode (此為默認模式) 時,會為每個入口起點 (entry) 創建一個客戶端腳本,所以你會在上面的輸出中看到有些信息重複輸出兩次。

但是當你點擊頁面的按鈕時,你會發現控制臺輸出的是舊的 printMe函數輸出的信息,因為 onclick事件綁定的仍是原始的 printMe函數。我們需要在 module.hot.accept裡更新綁定。

src/index.js

import _ from 'lodash';

import printMe from './print.js';

// import './style.css';

// import Icon from './icon.jpg';

// import Data from './data.json';

// ...

// document.body.appendChild(component());

var element = component();

document.body.appendChild(element);

if(module.hot) {

 module.hot.accept('./print.js', function() {

   console.log('Accepting the updated printMe module!');

   // printMe();

   document.body.removeChild(element);

   element = component();

   document.body.appendChild(element);

 });

}

uglifyjs-webpack-plugin 升級到 v0.4.6 時無法正確壓縮 ES6 的代碼,所以上面有些代碼採用 ES5 以暫時方便後面的壓縮,詳見 #49。

模塊熱替換也可以用於樣式的修改,效果跟控制臺修改一樣一樣的。

src/index.js

import _ from 'lodash';

import printMe from './print.js';

import './style.css';

// import Icon from './icon.jpg';

// import Data from './data.json';

// ...

npm start之,做如下修改:

/* ... */

body {

 background-color: yellow;

}

可以發現在不重載頁面的前提下我們對樣式的修改進行了熱加載,棒!

生產環境自動方式

我們只需要運行 webpack-p (相當於 webpack--optimize-minimize--define process.env.NODE_ENV="'production'")這個命令,便可以自動構建生產版本的應用,這個命令會完成以下步驟:

使用 UglifyJsPlugin (webpack.optimize.UglifyJsPlugin) 壓縮 JS 文件 (此插件和 uglifyjs-webpack-plugin 相同)

運行 LoaderOptionsPlugin 插件,這個插件是用來遷移的,見 document

設置 NodeJS 的環境變量,觸發某些 package 包以不同方式編譯

值得一提的是, webpack-p設置的 process.env.NODE_ENV環境變量,是用於編譯後的代碼的,只有在打包後的代碼中,這一環境變量才是有效的。如果在 webpack 配置文件中引用此環境變量,得到的是 undefined,可以參見 #2537。但是,有時我們確實需要在 webpack 配置文件中使用 process.env.NODE_ENV,怎麼辦呢?一個方法是運行 NODE_ENV='production'webpack-p命令,不過這個命令在Windows中是會出問題的。為了解決兼容問題,我們採用 cross-env 解決跨平臺的問題。

$ npm i --save-dev cross-env

package.json

{

 ...

 "scripts": {

   "build": "cross-env NODE_ENV=production webpack -p",

   "start": "webpack-dev-server --open"

 },

 ...

}

現在可以在配置文件中使用 process.env.NODE_ENV了。

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

const webpack = require('webpack');

module.exports = {

 // ...

 output: {

   // filename: 'bundle.js',

   // filename: '[name].bundle.js',

   filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', // 在配置文件中使用`process.env.NODE_ENV`

   path: path.resolve(__dirname, 'dist')

 },

 plugins: [

   new HtmlWebpackPlugin({

     title: 'webpack demo',

     filename: 'index.html'

   }),

   new CleanWebpackPlugin(['dist']),

   // new webpack.HotModuleReplacementPlugin(), // 關閉 HMR 功能

   new webpack.NamedModulesPlugin()

 ],

 // ...

};

[chunkhash]不能和 HMR 一起使用,換句話說,不應該在開發環境中使用 [chunkhash] (或者 [hash]),這會導致許多問題。詳情見 #2393 和 #377。

build 之,我們得到了生產版本的壓縮好的打包文件。

多配置文件配置

有時我們會需要為不同的環境配置不同的配置文件,可以選擇 簡易方法,這裡我們採用較為先進的方法。先準備一個基本的配置文件,包含了所有環境都包含的配置,然後用 webpack-merge 將它和特定環境的配置文件合併並導出,這樣就減少了基本配置的重複。

$ npm i --save-dev webpack-merge

webpack.common.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {

 entry: {

   app: './src/index.js',

   print: './src/print.js'

 },

 output: {

   path: path.resolve(__dirname, 'dist')

 },

 plugins: [

   new HtmlWebpackPlugin({

     title: 'webpack demo',

     filename: 'index.html'

   }),

   new CleanWebpackPlugin(['dist'])

 ],

 module: {

   rules: [

     {

       test: /\.css$/,

       use: [

         'style-loader',

         'css-loader'

       ]

     },

     {

       test: /\.(png|svg|jpg|gif)$/,

       use: [

         'file-loader'

       ]

     },

     {

       test: /\.(woff|woff2|eot|ttf|otf)$/,

       use: [

         'file-loader'

       ]

     }

   ]

 }

};

webpack.dev.js

const path = require('path');

const webpack = require('webpack');

const Merge = require('webpack-merge');

const CommonConfig = require('./webpack.common.js');

module.exports = Merge(CommonConfig, {

 devtool: 'cheap-module-eval-source-map',

 devServer: {

   contentBase: path.resolve(__dirname, 'dist'),

   hot: true,

   hotOnly: true

 },

 output: {

   filename: '[name].bundle.js'

 },

 plugins: [

   new webpack.DefinePlugin({

     'process.env.NODE_ENV': JSON.stringify('development') // 在編譯的代碼裡設置了`process.env.NODE_ENV`變量

   }),

   new webpack.HotModuleReplacementPlugin(),

   new webpack.NamedModulesPlugin()

 ]

});

webpack.prod.js

const path = require('path');

const webpack = require('webpack');

const Merge = require('webpack-merge');

const CommonConfig = require('./webpack.common.js');

module.exports = Merge(CommonConfig, {

 devtool: 'cheap-module-source-map',

 output: {

   filename: '[name].[chunkhash].js'

 },

 plugins: [

   new webpack.DefinePlugin({

     'process.env.NODE_ENV': JSON.stringify('production')

   }),

   new webpack.optimize.UglifyJsPlugin()

 ]

});

package.json

{

 ...

 "scripts": {

   "build": "cross-env NODE_ENV=production webpack -p",

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

   "build:dev": "webpack-dev-server --open --config webpack.dev.js",

   "build:prod": "webpack --progress --config webpack.prod.js"

 },

 ...

}

現在只需執行 npm run build:dev或 npm run build:prod便可以得到開發版或者生產版了!

webpack 命令行選項見 Command Line Interface。

代碼分離入口分離

我們先創建一個新文件:

$ cd src && touch another.js

src/another.js

import _ from 'lodash';

console.log(_.join(['Another', 'module', 'loaded!'], ' '));

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

const webpack = require('webpack');

module.exports = {

 // ...

 entry: {

   app: './src/index.js',

   // print: './src/print.js'

   another: './src/another.js'

 },

 // ...

};

cd..&&npm run build之,我們發現用入口分離的代碼得到了兩個大文件,這是因為兩個入口文件都引入了 lodash,這很大程度上造成了冗餘,在同一個頁面中我們只需要引入一個 lodash就可以了。

抽取相同部分

我們使用 CommonsChunkPlugin 插件來將相同的部分提取出來放到一個單獨的模塊中。

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

const webpack = require('webpack');

module.exports = {

 // devtool: 'inline-source-map',

 // ...

 output: {

   // filename: 'bundle.js',

   filename: '[name].bundle.js',

   // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 plugins: [

   new HtmlWebpackPlugin({

     title: 'webpack demo',

     filename: 'index.html'

   }),

   new CleanWebpackPlugin(['dist']),

   new webpack.optimize.CommonsChunkPlugin({

     name: 'common' // 抽取出的模塊的模塊名

   }),

   // new webpack.HotModuleReplacementPlugin(),

   // new webpack.NamedModulesPlugin()

 ],

 // ...

};

build 之,可以看到結果中包含以下部分:

   app.bundle.js    6.14 kB       0  [emitted]  app

another.bundle.js  185 bytes       1  [emitted]  another

common.bundle.js    73.2 kB       2  [emitted]  common

      index.html  314 bytes          [emitted]

我們把 lodash分離出來了。

動態引入

我們還可以選擇以動態引入的方式來實現代碼分離,藉助 import() 實現之。

webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const CleanWebpackPlugin = require('clean-webpack-plugin');

// const webpack = require('webpack');

module.exports = {

 // ...

 entry: {

   app: './src/index.js',

   // print: './src/print.js'

   // another: './src/another.js'

 },

 output: {

   // filename: 'bundle.js',

   filename: '[name].bundle.js',

   chunkFilename: '[name].bundle.js', // 指定非入口塊文件輸出的名字

   // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',

   path: path.resolve(__dirname, 'dist')

 },

 plugins: [

   new HtmlWebpackPlugin({

     title: 'webpack demo',

     filename: 'index.html'

   }),

   new CleanWebpackPlugin(['dist'])

   // new webpack.optimize.CommonsChunkPlugin({

   //   name: 'common'

   // }),

   // new webpack.HotModuleReplacementPlugin(),

   // new webpack.NamedModulesPlugin()

 ],

 // ...

};

src/index.js

// import _ from 'lodash';

import printMe from './print.js';

// import './style.css';

// import Icon from './icon.jpg';

// import Data from './data.json';

function component() {

 // 此函數原來的內容全部注釋掉...

 return import(/* webpackChunkName: "lodash" */ 'lodash').then(function(_) {

   const element = document.createElement('div');

   const btn = document.createElement('button');

   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

   btn.innerHTML = 'Click me and check the console!';

   btn.onclick = printMe;

   element.appendChild(btn);

   return element;

 }).catch(function(error) {

   console.log('An error occurred while loading the component')

 });

}

// document.body.appendChild(component());

// var element = component();

// document.body.appendChild(element);

// 原本熱加載的部分全部注釋掉...

component().then(function(component) {

  document.body.appendChild(component);

});

注意上面中的 /* webpackChunkName: "lodash" */這段注釋,它並不是可有可無的,它能幫助我們結合 output.chunkFilename把分離出的模塊最終命名為 lodash.bundle.js而非 [id].bundle.js。

現在 build 之看看吧。

結尾的一點廢話

終於寫完了 :),也感謝你能耐心看到這裡。webpack 這個工具的配置還是有些麻煩的。但是呢,某人說這個東東前期會花比較多時間,後期會大大提高你的效率。所以呢,還是拿下這個東東吧。有其他需求的話可以繼續看官方的文檔。遇到困難可以找:

Stack Overflow

Google

Gitter

Webpack Issues

我寫好的 demo 文件放在了這裡:https://github.com/yangkean/webpack-demo

Reference

相關文章推薦

從 0 到 1 搭建 webpack2+vue2 自定義模板詳細教程

webpack多頁應用架構系列(一):一步一步解決架構痛點

入門 Webpack,看這篇就夠了

相關焦點

  • 【webpack】webpack 中最易混淆的 5 個知識點
    「⚠️ 友情提示:本文章不是入門教程,不會費大量筆墨去描寫 webpack 的基礎配置,請讀者配合教程原始碼[2]食用。1.webpack 中,module,chunk 和 bundle 的區別是什麼?
  • webpack基本配置有哪些?如何搭建webpack?
    Webpack的優點是:3.安裝本節作業:安裝webpack環境第二節 webpack基本配置需要在項目的根目錄下創建一個文件 webpack.config.js1. 基本配置-入口:入口起點(entry point)指示 webpack 應該使用哪個模塊,來作為構建其內部依賴圖的開始。
  • Webpack的使用指南-Webpack的常用解決方案
    而use數組代表用哪些loader去處理這些匹配到的文件。此時再運行webpack,打包後的文件bundle.js就包含了css代碼。其中css-loader負責加載css,打包css到js中。而style-loader負責生成:在js運行時,將css代碼通過style標籤注入到dom中。
  • 【Webpack】654- 了不起的 Webpack Scope Hoisting 學習指南
    一、什麼是 Scope HoistingScope Hoisting 是 webpack3 的新功能,直譯為 "「作用域提升」",它可以讓 webpack 打包出來的「代碼文件更小」,「運行更快」。在 JavaScript 中,還有「變量提升」和「函數提升」,JavaScript 會將變量和函數的聲明提升到當前作用域頂部,而「作用域提升」也類似,webpack 將引入到 JS 文件「提升到」它的引入者的頂部。
  • webpack簡單介紹
    webpack是什麼nodeJS開發的網頁打包工具 ——nodeJS的一個模塊webpack的基本使用1) 使用命令行工具初始化項目(npm init -y),得到一個package.json文件(項目配置文件)【
  • webpack教程:如何從頭開始設置 webpack 5
    如果你是從 webpack 4 升級到 webpack 5,這裡有一些注意事項:webpack-dev-server命令現在換成webpack-servefile-loader、raw-loader和url-loader不是必需的,可以使用內置的Asset Modules節點 polyfill 不再可用,例如,如果遇到stream錯誤
  • 看完 Webpack 源碼,我學到了這些
    一種是最常見的 git clone,將 Github 上 webpack 項目 clone 到本地,pull 後與 webpack 官方最新代碼保持一致,一勞永逸。不過作者嘗試第一種方法時,總是 clone 不下來,很大可能是由於 webpack 源文件過大且 github 伺服器 clone 一直很慢。於是退而求其次,使用第二種方法:下載 Webpack 源碼 release 版本。
  • 前端必備技能 webpack - 4. webpack處理CSS資源
    每篇文章純屬個人經驗觀點,如有錯誤疏漏歡迎指正  因為 webpack 本身只具有識別 JS 的能力,所以涉及到其他資源
  • Webpack打包全世界
    到webpack4.0之後,不用自己添加配置文件也可以直接運行了,這個點讚。入口(entry)webpack中提供了多種定義入口屬性的方式。提供單個入口語法和對象語法。不過呢,常用的配置方法是對象方法,用來包含你引入的第三方庫。比如我常用的vue開發框架,帶入到webpack配置文件裡面就需要如下配置。
  • Webpack命令字典大全
    全局進行安裝webpack(熟手不建議)$ npm install webpack -g查看wepback版本信息 相當有用$ npm info webpack卸載wepback>$ npm uninstall webpack安裝指定版本 #=小老鼠$ npm install webpack#1.31.x --save-dev下載webpack插件到node_modules 並在package.json文件中加上webpack的配置內容
  • 擼一個webpack插件!!
    異步*同步*綁定:tapAsync / tapPromise / tap綁定:點擊執行:callAsync / promise執行:電話* 3.call/callAsync執行綁定事件const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);//綁定事件到webapck事件流hook1.tap('
  • Webpack vs Rollup
    開始使用安裝目前webpack最新版本是3.0.0npm i webpack -g npm i webpack@version -g配置在項目添加webpack.config.jsconst path = require('path');const webpack = require('webpack
  • webpack系列---loader
    webpack本身只能打包Javascript文件,對於其他資源例如 css,圖片,或者其他的語法集比如jsx,是沒有辦法加載的。 這就需要對應的loader將資源轉化,加載進來。所謂 loader 只是一個導出為函數的 JavaScript 模塊。
  • 如何編寫一個 Webpack Plugin
    最後執行 Compiler 的 emitAssets 方法把生成的文件輸出到 output 的目錄中。Plugin 作用按我的理解,Webpack 插件的作用就是在 webpack 運行到某個時刻的時候,幫我們做一些事情。
  • 圖解Webpack——實現Plugin
    Plugin是webpack生態系統的重要組成部分,其目的是解決loader無法實現的其他事,可用於執行範圍更廣的任務,為webpack帶來很大的靈活性。目前存在的plugin並不能完全滿足所有的開發需求,所以定製化符合自己需求的plugin成為學習webpack的必經之路。下面將逐步闡述plugin開發中幾個關鍵技術點並實現plugin。
  • webpack4 處理頁面
    HtmlWebpackPlugin 功能詳解對html文件進行加工後輸出到指定目錄。const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = { mode: 'production', entry: ['.
  • 麵包機--從入門到放棄(上篇)
    麵包機-從入門到放棄 目錄1       序(上篇)2       第一部分基礎知識2.1        第一章致烘焙愛好者2.2        麵包機則是掌握這把金鑰匙的有力武器,她優美、高效、從大洋彼岸到十裡洋場,從入門小白到烘焙大神,從家用小灶臺到西點大廚房。到處都有麵包機的身影。本書適合那些從未有過烤麵包經驗的初學者,如果你可以耐著性子看完。保不齊就能烤出好吃的,優雅的麵包。實在編不下去……鬼扯了這麼多是不是覺得目錄和行文風格似曾相識?
  • 重學webpack4之基礎篇
    head 中// use: [loader1,loader2,loader3],loader的處理順序是 3>2>1,從後往前module: { rules: [ { test: /.s?
  • 18款Webpack插件,總會有你想要的!
    將的WebPack中entry配置的相關入口chunk狀語從句:extract-text-webpack-plugin抽取的CSS樣式插入到該插件提供的template或者templateContent配置項指定的內容基礎上生成一個HTML文件,具體插入方式的英文將樣式link插入到head元素中,script插入到head或者body中。
  • 你必須知道的 webpack 插件原理分析
    webpack 自身只支持 js 和 json 這兩種格式的文件,對於其他文件需要通過 loader 將其轉換為 commonJS 規範的文件後,webpack 才能解析到。插件實例在獲取到 compiler 對象後,就可以通過 compiler.plugin (事件名稱, 回調函數) 監聽到 Webpack 廣播出來的事件。並且可以通過 compiler 對象去操作 Webpack。事件流機制webpack 本質上是一種事件流的機制,它的工作流程就是將各個插件串聯起來,而實現這一切的核心就是 Tapable。