Webpack 3,從入門到放棄

原文首發於:Webpack 3,從入門到放棄css

Update (2017.8.27) : 關於 output.publicPathdevServer.contentBasedevServer.publicPath的區別。以下:html

  • output.publicPath: 對於這個選項,咱們無需關注什麼絕對相對路徑,由於兩種路徑均可以。咱們只須要知道一點:這個選項是指定 HTML 文件中資源文件 (字體、圖片、JS文件等) 的文件名的公共 URL 部分的。在實際狀況中,咱們首先會經過output.filename或有些 loader 如file-loadername屬性設置文件名的原始部分,webpack 將文件名的原始部分和公共部分結合以後,HTML 文件就能獲取到資源文件了。
  • devServer.contentBase: 設置靜態資源的根目錄,html-webpack-plugin生成的 html 不是靜態資源。當用 html 文件裏的地址沒法找到靜態資源文件時就會去這個目錄下去找。
  • devServer.publicPath: 指定瀏覽器上訪問全部 打包(bundled)文件 (在dist裏生成的全部文件) 的根目錄,這個根目錄是相對服務器地址及端口的,比devServer.contentBaseoutput.publicPath優先。

前言

Tips
若是你用過 webpack 且一直用的是 webpack 1,請參考 從v1遷移到v2 (v2 和 v3 差別不大) 對版本變動的內容進行適當的瞭解,而後再選擇性地閱讀本文。node

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

Webpack,何許人也?

借用官方的說法: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.git

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

爲何要用介個 Webpack?

首先,定義已經說明了 webpack 能將多個資源模塊打包成一個或少數文件,這意味着與以往的發起多個 HTTP 請求來得到資源相比,如今只須要發起少許的 HTTP 請求。github

Tips
想了解合併 HTTP 請求的意義,請見 這裏web

其次,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());

Tips
importexport 已是 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.jsbundle.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"
  },
  ...
}

Tips
npm scripts 中咱們能夠經過包名直接引用本地安裝的 npm 包的二進制版本,而無需編寫包的整個路徑。

執行:

$ npm run build

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

Tips
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')
  },
  // ...
};

Tips
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"
  },
  ...
}

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

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

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

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

Tips
模塊熱替換(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,這個插件能夠將其替換爲模塊的真實路徑
  ],
  // ...
};

Tips
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.

Tips
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);
  });
}

Tips
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()
  ],
  // ...
};

Tips
[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:devnpm run build:prod即可以獲得開發版或者生產版了!

Tips
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);
 });

Tips
注意上面中的/* webpackChunkName: "lodash" */這段註釋,它並非無關緊要的,它能幫助咱們結合output.chunkFilename把分離出的模塊最終命名爲lodash.bundle.js而非[id].bundle.js

如今 build 之看看吧。

懶加載 (lazy loading)

既然有了import(),咱們能夠選擇在須要的時候才加載相應的模塊,減小了應用初始化時加載大量暫不須要的模塊的壓力,這能讓咱們的應用更高效地運行。

src/print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');

export default function printMe() {
  // console.log('Updating print.js...');
  console.log('Button Clicked: Here\'s "some text"!');
}

src/index.js

import _ from 'lodash';
// 其餘引入註釋...

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);

  btn.onclick = function() {
    import(/* webpackChunkName: "print" */ './print')
    .then(function(module) {
      const printMe = module.default; // 引入模塊的默認函數

      printMe();
    });
  };
    
  return element;

  // 本來的動態引入註釋...
}

document.body.appendChild(component());
// var element = component();
// document.body.appendChild(element);

// 熱加載部分註釋

// component().then(function(component) {
//    document.body.appendChild(component);
//  });

構建之,控制檯此時並沒有輸出,點擊按鈕,會看到控制檯以下輸出:

print.bundle.js:1 The print.js module has loaded! See the network tab in dev tools...
print.bundle.js:1 Button Clicked: Here's "some text"!

說明 print 模塊只在咱們點擊時才引入了,すっげえ!

緩存 (caching)

瀏覽器在初次加載網站時,會下載不少文件,爲了較少下載大量資源的壓力,瀏覽器會對資源進行緩存 (caching),這樣瀏覽器即可以更迅速地加載網站,可是咱們須要在文件內容發生改變時更新文件。

咱們能夠在輸出文件名上下手腳:

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].[chunkhash].js',
    // chunkFilename: '[name].bundle.js',
    // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  // ...
};

Tips
[chunkhash] 是內容相關的,只要內容發生了改變,構建後文件名的 hash 就會發生改變。

還有一個要點是提取出第三方庫放到單獨模塊中,由於它們是不太可能頻繁發生改變的,因此無需屢次加載這些模塊,提取的方法用 CommonsChunkPlugin 插件,這個插件上文中提到過,指定入口文件名時它會提取改入口文件爲單個文件,不指定則會提取 webpack 的運行時代碼。

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',
    vendor: [ // 第三方庫能夠統一放在這個入口一塊兒合併
      'lodash'
    ]
    // print: './src/print.js'
    // another: './src/another.js'
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].[chunkhash].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: 'vendor' // 將 vendor 入口處的代碼放入 vendor 模塊
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime' // 將 webpack 自身的運行時代碼放在 runtime 模塊
    })
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

Tips
包含 vendor 的 CommonsChunkPlugin 實例必須在包含 runtime 的以前,不然會報錯。

src/index.js

// import _ from 'lodash';
// ...

// ...

若是咱們在 src 下新建一個文件h.js,再在index.js中引入它,保存,構建之,咱們發現有些沒改變的模塊的 hash 也發生了改變,這是由於加入h.js後它們的module.id變了,但這明顯是不合理的。在開發環境,咱們能夠用 NamedModulesPlugin 將 id 換成具體路徑名。而在生產環境,咱們可使用 HashedModuleIdsPlugin

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 = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new webpack.HashedModuleIdsPlugin(), // 替換掉原來的`module.id`
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

再來執行剛纔那波操做,就會發現無關修改的模塊 hash 未變了。

Shimming

Tips
你能夠將 shim 簡單理解爲是用於兼容 API 的小型庫。

使用 jQuery 時咱們習慣性地使用$jQuery變量,每次都使用const $ = require(「jquery」)引入的話太麻煩,若是能直接把這兩個變量設置爲全局變量豈不美滋滋?這樣就能夠在每一個模塊中直接使用這兩個變量了。爲了兼容這一作法,咱們使用 ProvidePlugin 插件爲咱們完成這一任務。

$ npm i --save jquery

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 = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new webpack.ProvidePlugin({ // 設置全局變量
      $: 'jquery',
      jQuery: 'jquery'
    }),
    new webpack.HashedModuleIdsPlugin(),
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

src/print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');
console.log($('title').text()); // 使用 jQuery

export default function printMe() {
  // console.log('Updating print.js...');
  console.log('Button Clicked: Here\'s "some text"!');
}

build,點擊頁面按鈕,成功了。

另外,若是你須要在某些模塊加載時設置該模塊的全局變量,請看 這裏

結尾的一點廢話

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

我寫好的 demo 文件放在了這裏

Reference

相關文章
相關標籤/搜索