webpack 4 筆記

[toc]

learn webpack4

webpack 用于編譯 JavaScript 模塊窘拯。

本質(zhì)上挚冤,webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)近尚。當(dāng) webpack 處理應(yīng)用程序時(shí)贱田,它會遞歸地構(gòu)建一個(gè)依賴關(guān)系圖(dependency graph)徘六,其中包含應(yīng)用程序需要的每個(gè)模塊锄贼,然后將所有這些模塊打包成一個(gè)或多個(gè) bundle。

本文是學(xué)習(xí) webpack 4 所做的筆記注簿,仍在完善當(dāng)中~

本文所有的代碼都保存在 github倉庫中契吉。

安裝

webpack 的使用是基于 Node 和 NPM 的。

前提條件

在開始之前诡渴,請確保安裝了 Node.js 的最新版本捐晶。使用 Node.js 最新的長期支持版本(LTS - Long Term Support),是理想的起步妄辩。使用舊版本惑灵,你可能遇到各種問題,因?yàn)樗鼈兛赡苋鄙?webpack 功能以及/或者缺少相關(guān) package 包眼耀。

基本安裝

首先我們創(chuàng)建一個(gè)目錄英支,初始化 npm,然后 在本地安裝 webpack哮伟,接著安裝 webpack-cli(此工具用于在命令行中運(yùn)行 webpack):

mkdir webpack-start && cd webpack-start
npm init -y
npm install webpack webpack-cli --save-dev

另外干花,我們還需要調(diào)整 package.json 文件,以便確保我們安裝包是 私有的 (private)楞黄,并且移除 main 入口把敢。這可以防止意外發(fā)布你的代碼。

package.json

  {
    "name": "webpack-demo",
    "version": "1.0.0",
    "description": "",
+   "private": true,
-   "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
+     "build": "webpack"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "webpack": "^4.0.1",
      "webpack-cli": "^2.0.9"
    },
    "dependencies": {}
  }

現(xiàn)在可以開始你的模塊化項(xiàng)目了~

項(xiàng)目結(jié)構(gòu)大概是這樣的:

webpack-start
|- /dist
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- index.js
|- package-lock.json
|- package.json
|- webpack.config.js

完整 demo 文件可在 webpack-study 中的 webpack-start 文件夾查看

使用下一代 ECMAScript

本節(jié)內(nèi)容沿用 webpack-start 文件代碼谅辣。

通過在 webpack 中配置 babel修赞,使用下一代 ECMAScript。

npm install babel-loader  @babel/core  @babel/preset-env --save-dev

安裝之后桑阶,修改配置文件 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: /\.js$/,
+        exclude: /node_modules/,
+        use: {
+          loader: 'babel-loader',
+        }
+      }
     ]
   }
};

這樣就可以在項(xiàng)目中使用下一代 ECMAScript 語法規(guī)則柏副。

但是 babel 默認(rèn)只轉(zhuǎn)換語法,而不轉(zhuǎn)換新的 API蚣录,如需使用新的 API割择,還需要使用對應(yīng)的轉(zhuǎn)換插件 或者 添加 polyfill。

使用轉(zhuǎn)換插件

轉(zhuǎn)換插件適合在組件萎河,類庫項(xiàng)目中使用荔泳。

添加轉(zhuǎn)換插件:

npm install @babel/plugin-transform-runtime --save-dev
 
npm install --save @babel/runtime-corejs2

@babel/runtime-corejs2 可轉(zhuǎn)換 Promise 在 IE 中未定義的問題。

創(chuàng)建 babel 配置文件 .babelrc

{
    "presets": [
        ["@babel/preset-env"]
      ],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "corejs": 2,
          "helpers": true,
          "regenerator": true,
          "useESModules": false
        }
      ]
    ]
  }

完整 demo 文件可在 webpack-study 中的 webpack-ES6/ES6-runtime 文件夾查看虐杯。

使用 @babel/polyfill

@babel/polyfill 適合在業(yè)務(wù)項(xiàng)目中使用玛歌。

添加 polyfill 到生產(chǎn)環(huán)境:

npm install --save @babel/polyfill

創(chuàng)建 babel 配置文件 .babelrc

{
    "presets": [
      ["@babel/preset-env",
        {
          "useBuiltIns": "usage"
        }
      ]
    ]
}

.babelrc 中指定 useBuiltIns: 'usage' 的話,就不用在 webpack.config.js 的 entry 中包含 @babel/polyfill擎椰。

現(xiàn)在你可以在項(xiàng)目中使用新的語法規(guī)則和新的 API 了支子。

babel 文檔: https://babel.docschina.org/docs/en/usage

babel 教程:https://blog.zfanw.com/babel-js/

完整 demo 文件可在 webpack-study 中的 webpack-ES6/ES6-polyfill 文件夾查看。

資源管理

webpack 最出色的功能之一就是达舒,除了 JavaScript值朋,還可以通過 loader 引入任何其他類型的文件叹侄。

也就是說,以上列出的那些 JavaScript 的優(yōu)點(diǎn)(例如顯式依賴)昨登,同樣可以用來構(gòu)建網(wǎng)站或 web 應(yīng)用程序中的所有非 JavaScript 內(nèi)容趾代。

加載 CSS

使用 style-loader 和 css-loader

為了從 JavaScript 模塊中 import 一個(gè) CSS 文件,你需要在 module 配置中 安裝并添加 style-loader 和 css-loader:

npm install --save-dev style-loader css-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$/,
+         exclude: /node_modules/,
+         use: [
+           'style-loader',
+           'css-loader'
+         ]
+       }
+     ]
+   }
  };

webpack 根據(jù)正則表達(dá)式丰辣,來確定應(yīng)該查找哪些文件撒强,并將其提供給指定的 loader。在這種情況下糯俗,以 .css 結(jié)尾的全部文件尿褪,都將被提供給 style-loader 和 css-loader。

這使你可以在依賴于樣式的文件中引入樣式文件 import './style.css'得湘。

現(xiàn)在杖玲,當(dāng)該模塊運(yùn)行時(shí),含有 CSS 字符串的 <style> 標(biāo)簽淘正,將被插入到 html 文件的 <head> 中摆马。

使用 CSS Module

只要在 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$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
-           'css-loader',
+          { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]' } }
        ]
      }
    ]
  },
};

使用 css module 后,在頁面引用樣式需要修改:

src/index.js

import style from "./style.css";

function component() {
  var element = document.createElement("div");

  element.innerHTML = "Asset management";
- element.classList.add('hello');
+ element.classList.add(style.hello);

  return element;
}

document.body.appendChild(component());

重新打包鸿吆,就能看到 calss 名稱已經(jīng)變成類似 style__hello-2uDIX 了囤采。

使用 PostCSS

PostCSS 本身是一個(gè)功能比較單一的工具。它提供了一種方式用 JavaScript 代碼來處理 CSS惩淳。它負(fù)責(zé)把 CSS 代碼解析成抽象語法樹結(jié)構(gòu)(Abstract Syntax Tree蕉毯,AST),再交由插件來進(jìn)行處理思犁。

插件基于 CSS 代碼的 AST 所能進(jìn)行的操作是多種多樣的代虾,比如可以支持變量和混入(mixin),增加瀏覽器相關(guān)的聲明前綴激蹲,或是把使用將來的 CSS 規(guī)范的樣式規(guī)則轉(zhuǎn)譯(transpile)成當(dāng)前的 CSS 規(guī)范支持的格式棉磨。

PostCSS 一般不單獨(dú)使用,而是與已有的構(gòu)建工具進(jìn)行集成学辱。PostCSS 與主流的構(gòu)建工具乘瓤,如 Webpack、Grunt 和 Gulp 都可以進(jìn)行集成策泣。完成集成之后衙傀,選擇滿足功能需求的 PostCSS 插件并進(jìn)行配置。

文檔地址:https://postcss.org/

中文文檔:https://www.postcss.com.cn/

IBM文檔:https://www.ibm.com/developerworks/cn/web/1604-postcss-css/

學(xué)習(xí)指南:https://webdesign.tutsplus.com/series/postcss-deep-dive--cms-889

安裝 PostCSS 并添加插件 autoprefixer 和 postcss-preset-env:

 npm i -D postcss-loader postcss-preset-env autoprefixer   

在 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$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]' } },
+         {
+           loader: 'postcss-loader',
+           options: {
+             ident: 'postcss',
+             plugins: [
+               require('autoprefixer')(),
+               require('postcss-preset-env')(),
+             ]
+           }
+         }
        ]
      }
    ]
  },
};

完整 demo 文件可在 webpack-study 中的 asset-management/css-management 文件夾查看着降。

加載圖片

使用 file-loader

使用 file-loader差油,我們可以輕松地將圖片和 icon 混合到 CSS 中:

npm install --save-dev 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$/,
        exclude: /node_modules/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
            }
          }
        ]
      },
+     {
+       test: /\.(png|svg|jpg|gif)$/,
+       use: ["file-loader"]
+     }
    ]
  }
};

現(xiàn)在可以在頁面(或者 css)中使用 圖片和 icon 了。

現(xiàn)在任洞,當(dāng)你 import MyImage from './my-image.png'蓄喇,該圖像將被處理并添加到 output 目錄,并且 MyImage 變量將包含該圖像在處理后的最終 url交掏。

當(dāng)使用 css-loader 時(shí)妆偏,你的 CSS 中的 url('./my-image.png') 會使用類似的過程去處理。loader 會識別這是一個(gè)本地文件盅弛,并將 './my-image.png' 路徑钱骂,替換為輸出目錄中圖像的最終路徑。

合乎邏輯下一步是挪鹏,壓縮和優(yōu)化你的圖像见秽。

使用 url-loader 和 image-webpack-loader

url-loader 功能類似于 file-loader,但是在文件大刑趾小(單位 byte)低于指定的限制時(shí)解取,可以返回一個(gè) DataURL。

image-webpack-loader 使用 imagemin 壓縮 PNG, JPEG, GIF, SVG 和 WEBP 圖像返顺。

npm install --save-dev image-webpack-loader url-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$/,
        exclude: /node_modules/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
            }
          }
        ]
      },
-     {
-       test: /\.(png|svg|jpg|gif)$/,
-       use: ["file-loader"]
-     },
+     {
+       test: /\.(png|svg|jpg|gif)$/,
+       use: [
+         {
+           loader: "url-loader",
+           options: {
+             limit: 8192,
+             name: 'images/[name]-[hash:5].[ext]'
+           }
+         },
+         "image-webpack-loader"
+       ]
+     }
    ]
  }
};

完整 demo 文件可在 webpack-study 中的 asset-management/iamge-management 文件夾查看禀苦。

加載字體

那么,像字體這樣的其他資源如何處理呢遂鹊?

file-loader 和 url-loader 可以接收并加載任何文件振乏,然后將其輸出到構(gòu)建目錄。這就是說秉扑,我們可以將它們用于任何類型的文件慧邮,包括字體。讓我們更新 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$/,
        exclude: /node_modules/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
            }
          }
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 8192,
              name: 'images/[name]-[hash:5].[ext]'
            }
          },
          "image-webpack-loader"
        ]
      },
+     {
+       test: /\.(woff|woff2|eot|ttf|otf)$/,
+       use: [
+         'url-loader'
+       ]
+     }
    ]
  }
};

完整 demo 文件可在 webpack-study 中的 asset-management/font-management 文件夾查看舟陆。

加載 Iconfont

Iconfont 本質(zhì)上就是字體文件误澳,只要 webpack.config.js 具有在 加載CSS加載字體 添加的 rules,就能加載 Iconfont吨娜。

在需要的頁面引入 iconfont.css 文件就能使用 Iconfont:

import Iconfont from "./asset/font/iconfont.css";

...

// 將圖像添加到我們現(xiàn)有的 div脓匿。
var myIcon = document.createElement("span");;
myIcon.classList.add(Iconfont.iconfont);
myIcon.classList.add(Iconfont['wx-manage-shipin1']);

完整 demo 文件可在 webpack-study 中的 asset-management/font-management 文件夾查看。

加載數(shù)據(jù)

此外宦赠,可以加載的有用資源還有數(shù)據(jù)陪毡,如 JSON 文件,CSV勾扭、TSV 和 XML毡琉。

類似于 NodeJS,JSON 支持實(shí)際上是內(nèi)置的妙色,也就是說 import Data from './data.json' 默認(rèn)將正常運(yùn)行桅滋。

要導(dǎo)入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader丐谋。讓我們處理這三類文件:

npm install --save-dev csv-loader xml-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$/,
        exclude: /node_modules/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
            }
          }
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 8192,
              name: 'images/[name]-[hash:5].[ext]'
            }
          },
          "image-webpack-loader"
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'url-loader'
        ]
      },
+     {
+       test: /\.(csv|tsv)$/,
+       use: [
+         'csv-loader'
+       ]
+     },
+     {
+       test: /\.xml$/,
+       use: [
+         'xml-loader'
+       ]
+     }
    ]
  }
};

完整 demo 文件可在 webpack-study 中的 asset-management/data-management 文件夾查看芍碧。

全局資源

上述所有內(nèi)容中最出色之處是,以這種方式加載資源号俐,你可以以更直觀的方式將模塊和資源組合在一起泌豆。

無需依賴于含有全部資源的 /assets 目錄,而是將資源與代碼組合在一起吏饿。

- |- /assets
+ |– /components
+ |  |– /my-component
+ |  |  |– index.jsx
+ |  |  |– index.css
+ |  |  |– icon.svg
+ |  |  |– img.png

這種配置方式會使你的代碼更具備可移植性踪危,因?yàn)楝F(xiàn)有的統(tǒng)一放置的方式會造成所有資源緊密耦合在一起。假如你想在另一個(gè)項(xiàng)目中使用 /my-component猪落,只需將其復(fù)制或移動(dòng)到 /components 目錄下贞远。

只要你已經(jīng)安裝了任何擴(kuò)展依賴(external dependencies),并且你已經(jīng)在配置中定義過相同的 loader笨忌,那么項(xiàng)目應(yīng)該能夠良好運(yùn)行蓝仲。

但是,假如你無法使用新的開發(fā)方式蜜唾,只能被固定于舊有開發(fā)方式杂曲,或者你有一些在多個(gè)組件(視圖、模板袁余、模塊等)之間共享的資源擎勘。你仍然可以將這些資源存儲在公共目錄(base directory)中,甚至配合使用 alias 來使它們更方便 import 導(dǎo)入颖榜。

接下來我們改造 data-management 項(xiàng)目中的文件棚饵。

完成 加載數(shù)據(jù) 這一小節(jié)后,我們的項(xiàng)目目錄大概是:

data-management
|- /dist
  |- /images
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- /asset
    |- /font
  |- data.xml
  |- icon.jpg
  |- index.js
  |- style.css
|- package-lock.json
|- package.json
|- webpack.config.js

按照上述原則改造如下:

  1. ~/src 文件夾下新建文件夾 components掩完,用于存放我們所有的組件噪漾。
  2. components 文件夾下新建 hello-world 文件夾,用于存放我們的第一個(gè)組件且蓬。
  3. 把存在 ~/src 目錄下的文件 data.xml欣硼、icon.jpgindex.js恶阴、style.css 移動(dòng)到 hello-world 文件夾诈胜。并修改 index.js 文件:
        import style from "./style.css";
    -   import Iconfont from "./asset/font/iconfont.css";
    +   import Iconfont from "../../asset/font/iconfont.css";
        import Icon from "./icon.jpg";
        import Data from './data.xml';
        
        console.log("Data",Data)
        function component() {
          let element = document.createElement("div");
        
          element.innerHTML = "Asset management";
          element.classList.add(style.hello);
        
          return element;
        }
        
        function imageComponent() {
          let element = document.createElement("div");
        
          // 將圖像添加到我們現(xiàn)有的 div。
          let myIcon = new Image();
           myIcon.src = Icon;
        
           element.appendChild(myIcon);
        
          return element;
        }
        
        function iconComponent() {
          let element = document.createElement("div");
        
          // 將圖像添加到我們現(xiàn)有的 div冯事。
          let myIcon = document.createElement("span");;
           myIcon.classList.add(Iconfont.iconfont);
           myIcon.classList.add(Iconfont['wx-manage-shipin1']);
        
           element.appendChild(myIcon);
        
          return element;
        }
        
        function dataComponent() {
          let element = document.createElement("div");
          let str = '';
           
          for(let key in Data.note){
            str += `<p>${key}:${Data.note[key][0]}</p>`
          }
          element.innerHTML = str;
         
          return element;
        }
    -    document.body.appendChild(component());
    -    document.body.appendChild(imageComponent());
    -    document.body.appendChild(iconComponent());
    -    document.body.appendChild(dataComponent());
        
    +   export {
    +     component,
    +     imageComponent,
    +     iconComponent,
    +     dataComponent
    +   }
    
    
  4. ~/src 文件夾下新建文件 index.js(原來的已經(jīng)移入 hello-world 文件夾)焦匈,并添加內(nèi)容:
    import { component,imageComponent,iconComponent,dataComponent} from "./components/hello-world/index.js";
    
    document.body.appendChild(component());
    document.body.appendChild(imageComponent());
    document.body.appendChild(iconComponent());
    document.body.appendChild(dataComponent());
    

調(diào)整完成后我們的項(xiàng)目結(jié)構(gòu)大概是:

data-management
|- /dist
  |- /images
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- /asset
    |- /font
  |- /components
    |- /hello-world
  |- index.js
|- package-lock.json
|- package.json
|- webpack.config.js

這里的 ~/src/asset 文件夾依然存在,它存放的是 Iconfont 文件昵仅,這在我們的整個(gè)項(xiàng)目中都會用到缓熟。當(dāng)然也可以拆分到具體的組件,從而實(shí)現(xiàn)完全沒有全局資源。

輸出管理

本節(jié)代碼沿用 資源管理 代碼并安裝配置好 babel 和 @babel/polyfill够滑。

項(xiàng)目結(jié)構(gòu)大概是這樣的:

output-management
|- /dist
  |- /images
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- /asset
    |- /font
  |- /components
    |- /hello-world
  |- index.js
|- .babelrc
|- package-lock.json
|- package.json
|- webpack.config.js

使用 HtmlWebpackPlugin

從本文開始到現(xiàn)在垦写,我們項(xiàng)目下幾乎所有的文件都動(dòng)過,除了 ~/dist/index.html 這個(gè)文件版述。

用過主流框架的同學(xué)的知道梯澜, ~/dist 目錄下的所有文件都會打包后重新生成寞冯。我們通過 HtmlWebpackPlugin 插件來完成這項(xiàng)任務(wù)渴析。

生成 index.html

安裝插件:

npm install --save-dev html-webpack-plugin

并在配置文件 webpack.config.js 中引入并配置:

    const path = require("path");
+   const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      
      ...
      
+     plugins: [
+       new HtmlWebpackPlugin({
+         title: 'Output Management'
+       })
+     ],

      ...
      
    };

配置完成后,執(zhí)行打包命令 npm run dev吮龄,就會根據(jù)配置生成一個(gè) index.html 文件:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Output Management</title>
  </head>
  <body>
  <script type="text/javascript" src="index.js"></script></body>
</html>

分離入口

現(xiàn)在我們的 ~/src 文件夾下只有一個(gè)入口文件 index.js俭茧,如果存在多個(gè)怎么添加進(jìn)生成的 index.html 文件呢?

首先漓帚,在 ~/src 文件夾下新建入口文件 print.js母债,添加下面內(nèi)容:

function component() {
    let element = document.createElement("div");
  
    element.innerHTML = "print entry";
  
    return element;
  }

  document.body.appendChild(component());

然后修改配置文件 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: 'index.js',
+       filename: '[name].bundle.js',
        path: path.resolve(__dirname, "dist")
      },
      
      ...
      
    };

執(zhí)行打包命令,在重新生成的 index.html 中就添加了多個(gè)入口文件:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Output Management</title>
  </head>
  <body>
  <script type="text/javascript" src="app.bundle.js"></script><script type="text/javascript" src="print.bundle.js"></script></body>
</html>

清理 ~/dist 文件夾

在上一節(jié)的操作中尝抖,我們成功的讓 webpack 可以比較智能的完成了一些任務(wù)毡们,很開心~

然而,當(dāng)我打開 ~/dist 文件夾時(shí)卻發(fā)現(xiàn)里面的文件非常雜亂昧辽,因?yàn)槲覀兠看螛?gòu)建都會生成相應(yīng)代碼衙熔,但是從來沒有清理。

幸運(yùn)的是搅荞,我們可以通過 clean-webpack-plugin 插件在構(gòu)建前清理 /dist 文件夾红氯。

安裝插件:

npm install 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: {
      app: './src/index.js',
      print: './src/print.js'
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
    plugins: [
+     new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Output Management'
      })
    ],
    
    ...
    
  };

現(xiàn)在再次執(zhí)行 npm run build 將會發(fā)現(xiàn)以前生成的文件已經(jīng)被清理干凈了!

完整 demo 可在 output-management 文件夾查看咕痛。

搭建開發(fā)環(huán)境

如果你一直跟隨之前的指南痢甘,應(yīng)該對一些 webpack 基礎(chǔ)知識有著很扎實(shí)的理解。在我們繼續(xù)之前茉贡,先來看看如何建立一個(gè)開發(fā)環(huán)境塞栅,使我們的開發(fā)變得更容易一些。

使用 source map

本小節(jié)沿用 輸出管理 這一節(jié)的代碼腔丧。

當(dāng) webpack 打包源代碼時(shí)放椰,可能會很難追蹤到錯(cuò)誤和警告在源代碼中的原始位置。例如悔据,如果將三個(gè)源文件(a.js, b.js 和 c.js)打包到一個(gè) bundle(bundle.js)中庄敛,而其中一個(gè)源文件包含一個(gè)錯(cuò)誤,那么堆棧跟蹤就會簡單地指向到 bundle.js科汗。這并通常沒有太多幫助藻烤,因?yàn)槟憧赡苄枰獪?zhǔn)確地知道錯(cuò)誤來自于哪個(gè)源文件。

為了更容易地追蹤錯(cuò)誤和警告,JavaScript 提供了 source map 功能怖亭,將編譯后的代碼映射回原始源代碼涎显。如果一個(gè)錯(cuò)誤來自于 b.js,source map 就會明確的告訴你兴猩。

source map 有很多 不同的選項(xiàng) 可用期吓,請務(wù)必仔細(xì)閱讀它們,以便可以根據(jù)需要進(jìn)行配置倾芝。

簡單的說讨勤,在開發(fā)環(huán)境可以使用 "eval" 選項(xiàng)因?yàn)樗芸欤辉谏a(chǎn)環(huán)境晨另,最好 不用 或者使用 "source-map" 選項(xiàng)潭千。

修改配置文件 webpack.config.js 使用 source map:

const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');


module.exports = {

  ...

+ devtool: "eval",
  
  ...
  
};

然后再 print.js 文件中人為的制造一個(gè)錯(cuò)誤:

    function component() {
        let element = document.createElement("div");
      
        element.innerHTML = "print entry";
+       console.lag("asd",asdasd);
      
        return element;
      }
    
    document.body.appendChild(component());

執(zhí)行構(gòu)建 npm run build 后,在瀏覽器打開 ~/build/index.html 文件借尿,控制臺就會輸出一個(gè)錯(cuò)誤:

print.js:4 Uncaught ReferenceError: asdasd is not defined
    at component (print.js:4)
    at eval (print.js:8)
    at Object../src/print.js (print.bundle.js:96)
    at __webpack_require__ (print.bundle.js:20)
    at print.bundle.js:84
    at print.bundle.js:87

告訴我們錯(cuò)誤的文件是 "print.js"刨晴,行數(shù)為 4 。

行數(shù)是錯(cuò)的路翻,實(shí)際在第 5 行狈癞,使用 "source-map" 選項(xiàng)的話,就是正確的:

print.js:5 Uncaught ReferenceError: asdasd is not defined
    at component (print.js:5)
    at Object../src/print.js (print.js:10)
    at __webpack_require__ (bootstrap:19)
    at bootstrap:83
    at bootstrap:83

完整 demo 可在 webpack-dev/source-map 文件夾查看茂契。

使用一個(gè)開發(fā)工具

現(xiàn)在蝶桶,我們每次需要執(zhí)行構(gòu)建的時(shí)候,都需要手動(dòng)運(yùn)行 npm run build 命令账嚎,這有時(shí)會讓我們感到很煩躁~

webpack 中有幾個(gè)不同的選項(xiàng)莫瞬,可以幫助我們在代碼發(fā)生變化后自動(dòng)編譯代碼:

  • webpack's Watch Mode
  • webpack-dev-server
  • webpack-dev-middleware

我們只需要使用其中之一,就能去掉我們的煩惱 ^_^

注意:本小節(jié)每一個(gè)選項(xiàng)都沿用上一小節(jié) 使用 source map 的代碼疼邀。

使用觀察模式

使用觀察模式后旁振,如果項(xiàng)目其中有文件被更新拐袜,代碼將被重新編譯,所以你不必手動(dòng)運(yùn)行整個(gè)構(gòu)建甜攀。

我們添加一個(gè)用于啟動(dòng) webpack 的觀察模式的 npm script 腳本:

   "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
+     "watch": "webpack --watch",
      "build": "webpack"
    },

在命令行中運(yùn)行 npm run watch恒序,就會看到 webpack 編譯代碼歧胁,然而卻不會退出命令行喊巍。這是因?yàn)?script 腳本還在觀察文件玄糟。

現(xiàn)在,我們先移除我們之前引入的錯(cuò)誤:

src/print.js

    function component() {
        let element = document.createElement("div");
      
        element.innerHTML = "print entry";
-       console.lag("asd",asdasd);
      
        return element;
      }
    
    document.body.appendChild(component());

保存文件并檢查終端窗口。應(yīng)該可以看到 webpack 自動(dòng)重新編譯修改后的模塊砍聊!

唯一的缺點(diǎn)是蟹肘,為了看到修改后的實(shí)際效果帘腹,你需要自己刷新瀏覽器阳欲。

完整 demo 可在 webpack-dev/webpack-watch 文件夾查看球化。

使用 webpack-dev-server

webpack-dev-server 為你提供了一個(gè)簡單的 web 服務(wù)器,并且能夠?qū)崟r(shí)重新加載(live reloading)菩浙。

安裝并使用

首先,安裝:

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

然后修改配置文件熄阻,告訴開發(fā)服務(wù)器(dev server)秃殉,在哪里查找文件:

    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: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, "dist")
      },
      devtool: "source-map",
+     devServer: {
+       contentBase: "./dist"
+     },
      
      ...
      
    };

然后在 package.json 文件中添加一個(gè) script 腳本,來啟用開發(fā)服務(wù)器:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
+   "start": "webpack-dev-server --open"
  },

現(xiàn)在吏恭,我們可以在命令行中運(yùn)行 npm start樱哼,就會看到瀏覽器自動(dòng)加載頁面。

如果現(xiàn)在修改和保存任意源文件呼胚,web 服務(wù)器就會自動(dòng)重新加載編譯后的代碼蝇更。試一下!

可以查看 相關(guān)文檔 了解更多關(guān)于 devServer 的配置。

完整 demo 可在 webpack-dev/dev-server/webpack-WDS 文件夾查看纳令。

啟用模塊熱替換

模塊熱替換(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一平绩。它允許在運(yùn)行時(shí)更新各種模塊,而無需進(jìn)行完全刷新笆搓。

配置 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"
      },
      output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, "dist")
      },
      devtool: "source-map",
      devServer: {
        contentBase: "./dist",
+       hot: true
      },
      plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
          title: 'Output Management'
        }),
+       new webpack.NamedModulesPlugin(),
+       new webpack.HotModuleReplacementPlugin()
      ],
      
      ...
      
      
    };

在入口文件 ~/src/index.js 處理模塊的熱替換:

    import { component,imageComponent,iconComponent,dataComponent} from "./components/hello-world/index.js";


-   document.body.appendChild(component());
    document.body.appendChild(imageComponent());
    document.body.appendChild(iconComponent());
    document.body.appendChild(dataComponent());
    
+   let element = component();   // 記錄模塊肤频,方便更新時(shí)移除和替換
+   document.body.appendChild(element);

+   
+    if (module.hot) {
+      module.hot.accept('./components/hello-world/index.js', function() {
+        console.log('Accepting the updated printMe module!');
+   
+        document.body.removeChild(element);
+        element = component();   // 重新渲染頁面后宵荒,更新 component 模塊
+        document.body.appendChild(element);
       })
     }

然后運(yùn)行 npm start 啟動(dòng)項(xiàng)目侠讯,修改 component 模塊內(nèi)容:

./components/hello-world/index.js


    ...
    
    function component() {
      let element = document.createElement("div");
      
-     element.innerHTML = "Asset management";
+     element.innerHTML = "Asset management HMR";
      element.classList.add(style.hello);
    
      return element;
    }
    
    ...

保存文件,可以看到 web 服務(wù)器就會自動(dòng)編譯代碼溜嗜,然后替換瀏覽器中的相應(yīng)模塊。

完整 demo 可在 webpack-dev/dev-server/WDS-HMR 文件夾查看鸿脓。

HMR 修改樣式表

當(dāng)項(xiàng)目中配置了 style-loader 和 css-loader在塔,項(xiàng)目就能模塊熱替換樣式表,無需自己再做任何配置贺待。

使用 webpack-dev-middleware

webpack-dev-middleware 是一個(gè)容器(wrapper)奥此,它可以把 webpack 處理后的文件傳遞給一個(gè)服務(wù)器(server)蠢终。

webpack-dev-server 在內(nèi)部使用了它兜喻,同時(shí),它也可以作為一個(gè)單獨(dú)的包來使用,以便進(jìn)行更多自定義設(shè)置來實(shí)現(xiàn)更多的需求。

接下來是一個(gè) webpack-dev-middleware 配合 express server 的示例。

啟用 webpack-dev-middleware

首先矾利,安裝 express 和 webpack-dev-middleware:

npm install --save-dev express webpack-dev-middleware

接下來我們需要對 webpack 的配置文件 webpack.config.js 做一些調(diào)整男旗,以確保中間件(middleware)功能能夠正確啟用:

    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: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, "dist"),
+       publicPath: '/'
      },
      devtool: "source-map",
      plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
          title: 'Output Management'
        })
      ],
      module: { ···
      }
    };

publicPath 也會在服務(wù)器腳本用到,以確保文件資源能夠在 http://localhost:3000 下正確訪問,我們稍后再設(shè)置端口號。

下一步就是設(shè)置我們自定義的 express 服務(wù):

在項(xiàng)目文件夾 webpack-middleware 下新建文件 server.js

    |- /dist
    |- /node_modules
    |- /src
      |- /asset
      |- /components
        |- /hello-world
      |- index.js
      |- print.js
    |- .babelrc
    |- package-lock.json
    |- package.json
    |- webpack.config.js
+   |- server.js

server.js 添加下列內(nèi)容:

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

接著商佑,在 package.json 添加一個(gè) script笛求,方便我們運(yùn)行服務(wù):

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
+   "server": "node server.js"
  },

添加完成后就可以執(zhí)行 npm run server 命令運(yùn)行程序徒爹。

PS F:\demo\webpack-study\webpack-dev\webpack-middleware> npm run server

> webpack-study@1.0.0 server F:\demo\webpack-study\webpack-dev\webpack-middleware
> node server.js

clean-webpack-plugin: F:\demo\webpack-study\webpack-dev\webpack-middleware\dist has been removed.
Example app listening on port 3000!

打開瀏覽器并輸入 http://localhost:3000,就能看到看到頁面雌贱。

當(dāng)項(xiàng)目中的文件存在修改,在保存后 webpack 就會自動(dòng)編譯文件欣孤,刷新瀏覽器就可以看到編譯后的文件馋没。

完整 demo 可在 webpack-dev/dev-middleware/webpack-middleware 文件夾查看。

使用 webpack-hot-middleware

我們知道如果只使用 webpack-dev-middleware 的話降传,我們必須自己刷新瀏覽器篷朵,那么能不能自動(dòng)刷新呢?答案當(dāng)然是可以搬瑰!

通過使用 webpack-hot-middleware 讓瀏覽器自動(dòng)刷新:

npm install --save-dev webpack-hot-middleware

安裝完成后修改配置文件:

    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"
+       app: ["./src/index.js",'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true&name=app'],
+       print: ["./src/print.js",'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true&name=print']
      },
      output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, "dist"),
        publicPath: '/'
      },
      devtool: "source-map",
      plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
          title: 'Output Management'
        }),
+       new webpack.optimize.OccurrenceOrderPlugin(),
+       new webpack.HotModuleReplacementPlugin(),
+       new webpack.NoEmitOnErrorsPlugin()
      ],
      module: { ···
      }
    };


server.js 中添加:

    const express = require('express');
    const webpack = require('webpack');
    const webpackDevMiddleware = require('webpack-dev-middleware');
    
    const app = express();
    const config = require('./webpack.config.js');
    const compiler = webpack(config);
    
    // Tell express to use the webpack-dev-middleware and use the webpack.config.js
    // configuration file as a base.
    app.use(webpackDevMiddleware(compiler, {
      publicPath: config.output.publicPath
    }));
    
+   app.use(require("webpack-hot-middleware")(compiler, {
+     log: false,
+     heartbeat: 1000,
+   }));
    
    // Serve the files on port 3000.
    app.listen(3000, function () {
      console.log('Example app listening on port 3000!\n');
    });

啟動(dòng)開發(fā)服務(wù)器 npm run server款票,再修改項(xiàng)目下的文件并保存,就能看到瀏覽器已經(jīng)可以自動(dòng)刷新了泽论。

完整 demo 可在 webpack-dev/dev-middleware/hot-middleware 文件夾查看艾少。

配置拆分

本節(jié)我們沿用 使用 webpack-dev-server 這一小節(jié)的代碼。

到這里翼悴,我們已經(jīng)成功使用 webpack 搭建了一個(gè)模塊化項(xiàng)目的開發(fā)環(huán)境缚够,如果我們再把生產(chǎn)環(huán)境也搭建好,就可以進(jìn)入愉快的開發(fā)工作了鹦赎,好開心谍椅。。古话。

但是雏吭,開發(fā)環(huán)境(development)和生產(chǎn)環(huán)境(production)的構(gòu)建目標(biāo)差異很大!

在開發(fā)環(huán)境中陪踩,我們需要具有強(qiáng)大的杖们、具有實(shí)時(shí)重新加載(live reloading)或熱模塊替換(hot module replacement)能力的 source map 和 localhost server。

而在生產(chǎn)環(huán)境中肩狂,我們的目標(biāo)則轉(zhuǎn)向于關(guān)注更小的 bundle摘完,更輕量的 source map,以及更優(yōu)化的資源傻谁,以改善加載時(shí)間孝治。

由于要遵循邏輯分離,我們通常建議為每個(gè)環(huán)境編寫彼此獨(dú)立的 webpack 配置

雖然谈飒,以上我們將生產(chǎn)環(huán)境和開發(fā)環(huán)境做了略微區(qū)分岂座,但是,請注意步绸,我們還是會遵循不重復(fù)原則(Don't repeat yourself - DRY)掺逼,保留一個(gè)“通用”配置妒峦。

為了將這些配置合并在一起浴讯,我們將使用一個(gè)名為 webpack-merge 的工具默穴。通過“通用”配置身坐,我們不必在環(huán)境特定(environment-specific)的配置中重復(fù)代碼坚俗。

安裝:

npm install --save-dev webpack-merge

拆分配置文件:
webpack-merge

  |- /node_modules
  |- /src
  |- .babelrc
  |- package-lock.json
  |- package.json
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js

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: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      title: 'Output Management'
    }),
  ],
  module: { ···
  }
};

webpack.dev.js 文件用于存開發(fā)環(huán)境的配置:

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const webpack = require('webpack');

module.exports = merge(common, {
  devtool: "source-map",
  devServer: {
    contentBase: "./dist",
    hot: true
  },
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin()
  ]
});

webpack.prod.js 文件用于存生產(chǎn)環(huán)境的配置:

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

module.exports = merge(common, {

});

配置文件拆分后棕硫,更改 package.json 文件中的 "script"

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
-   "build": "webpack --mode development",
-   "start": "webpack-dev-server --open"
+   "build": "webpack --config webpack.prod.js",
+   "start": "webpack-dev-server --open --config webpack.dev.js"
  },

接下來可以分別運(yùn)行不同的腳本螟加,查看效果档址!

完整 demo 可在 webpack-merge 文件夾查看祠斧。

搭建生產(chǎn)環(huán)境

本節(jié)沿用上一節(jié) 配置拆分 的代碼闻察。

在生產(chǎn)環(huán)境中,我們的目標(biāo)是如何獲得更小的 bundle琢锋,更輕量的 source map辕漂,以及更優(yōu)化的資源,來改善加載時(shí)間吴超。

tree shaking

tree shaking 是一個(gè)術(shù)語钉嘹,通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊系統(tǒng)中的靜態(tài)結(jié)構(gòu)特性鲸阻,例如 import 和 export跋涣。

在新的 webpack 4 正式版本,擴(kuò)展了這個(gè)檢測能力鸟悴,通過 package.json 的 "sideEffects" 屬性作為標(biāo)記陈辱,向 compiler 提供提示,表明項(xiàng)目中的哪些文件是 "pure(純的 ES2015 模塊)"细诸,由此可以安全地刪除文件中未使用的部分沛贪。

在 ~/src 文件夾下新建一個(gè) math.js 文件:

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

在 ~/src/index.js 入口文件引入并使用 cube() 方法:

    import { component, imageComponent, iconComponent, dataComponent } from "./components/hello-world/index.js";
+   import { cube } from "./math.js";
    
+   function mathComponent() {
+     var element = document.createElement("pre");
+   
+     element.innerHTML = ["Hello webpack!", "5 cubed is equal to " + cube(5)].join("\n\n");
+   
+     return element;
+   }
    
    // document.body.appendChild(component());
    document.body.appendChild(imageComponent());
    document.body.appendChild(iconComponent());
    document.body.appendChild(dataComponent());
+   document.body.appendChild(mathComponent());
    
    let element = component();
    document.body.appendChild(element);
    
    if (module.hot) {
      module.hot.accept("./components/hello-world/index.js", function() {
        console.log("Accepting the updated printMe module!");
    
        document.body.removeChild(element);
        element = component();
        document.body.appendChild(element);
      });
    }

現(xiàn)在如果執(zhí)行構(gòu)建 npm run build 命令,在打包出來的 bundle 文件中仍然會有 square() 方法震贵,盡管并沒有使用它鹏浅。

在 package.json 中添加副作用配置:

  "sideEffects": [
    "*.css"
  ],

安裝插件:

npm install uglifyjs-webpack-plugin --save-dev

修改生產(chǎn)配置文件

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");

module.exports = merge(common, {
  plugins: [
    new UglifyJSPlugin({
      sourceMap: true
    })
  ]
});

現(xiàn)在執(zhí)行構(gòu)建 npm run build 命令,在打包出來的 bundle 文件中就不會有 square() 方法屏歹。

指定環(huán)境

修改開發(fā)環(huán)境配置:

    const merge = require("webpack-merge");
    const common = require("./webpack.common.js");
    const webpack = require('webpack');
    
    module.exports = merge(common, {
      devtool: "source-map",
      devServer: {
        contentBase: "./dist",
        hot: true
      },
      plugins: [
+       new webpack.DefinePlugin({
+         "process.env.NODE_ENV": JSON.stringify("development")
+       }),
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
      ]
    });

修改生產(chǎn)配置文件:

    const merge = require("webpack-merge");
    const common = require("./webpack.common.js");
    const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
    const webpack = require("webpack");
    
    module.exports = merge(common, {
      plugins: [
+       new UglifyJSPlugin({
+         sourceMap: true
+       }),
        new webpack.DefinePlugin({
          "process.env.NODE_ENV": JSON.stringify("production")
        })
      ]
    });

CSS 分離

安裝:

npm i -D extract-text-webpack-plugin@next

接下來把通用配置 CSS loader 配置移動(dòng)到開發(fā)環(huán)境和生產(chǎn)環(huán)境:

webpack.dev.js:

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const webpack = require("webpack");

module.exports = merge(common, {
  devtool: "source-map",
  devServer: {
    contentBase: "./dist",
    hot: true
  },
  plugins: [
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("development")
    }),
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
            }
          }
        ]
      }
    ]
  }
});

webpack.prod.js :

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const webpack = require("webpack");

module.exports = merge(common, {
  plugins: [
    new UglifyJSPlugin({
      sourceMap: true
    }),
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("production")
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
            }
          }
        ]
      }
    ]
  }
});

在生產(chǎn)環(huán)境配置中,加入 extract-text-webpack-plugin 插件配置:

    const merge = require("webpack-merge");
    const common = require("./webpack.common.js");
    const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
+   const ExtractTextPlugin = require("extract-text-webpack-plugin");
    const webpack = require("webpack");
    
    module.exports = merge(common, {
      plugins: [
        new UglifyJSPlugin({
          sourceMap: true
        }),
        new webpack.DefinePlugin({
          "process.env.NODE_ENV": JSON.stringify("production")
        }),
+       new ExtractTextPlugin({
+           filename: '[name].css',
+       })
      ],
+     module: {
+       rules: [
+         {
+           test: /\.css$/,
+           exclude: /node_modules/,
+           use: ExtractTextPlugin.extract({
+             fallback: 'style-loader',
+             use:[
+               { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
+               {
+                 loader: "postcss-loader",
+                 options: {
+                   ident: "postcss",
+                   plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
+                 }
+               }
+             ]
+           })
+         }
+       ]
+     }
    });

在這里遇到一個(gè)問題之碗,假設(shè)我這樣配置插件:

   new ExtractTextPlugin({
       filename: 'css/[name].css',
   })

在執(zhí)行構(gòu)建后蝙眶,css 文件中引入的背景圖地址會變成 css/images/icon-745a8.jpg ,導(dǎo)致背景圖加載失敗幽纷!

所以最終這樣配置:

   new ExtractTextPlugin({
       filename: '[name].css',
   })

完整 demo 可在 webpack-prod 文件夾查看式塌。

優(yōu)化

現(xiàn)在一個(gè)簡單的開發(fā)環(huán)境和生產(chǎn)環(huán)境就配置好了,讓我們進(jìn)入優(yōu)化環(huán)節(jié)吧~

代碼分離

代碼分離是 webpack 中最引人注目的特性之一友浸。此特性能夠把代碼分離到不同的 bundle 中峰尝,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle收恢,以及控制資源加載優(yōu)先級武学,如果使用合理,會極大影響加載時(shí)間伦意。

有三種常用的代碼分離方法:

  • 入口起點(diǎn):使用 entry 配置手動(dòng)地分離代碼火窒。
  • 防止重復(fù):使用 CommonsChunkPlugin 去重和分離 chunk。
  • 動(dòng)態(tài)導(dǎo)入:通過模塊的內(nèi)聯(lián)函數(shù)調(diào)用來分離代碼驮肉。

入口起點(diǎn)

這是迄今為止最簡單熏矿、最直觀的分離代碼的方式。不過离钝,這種方式手動(dòng)配置較多票编,并有一些陷阱,我們將會解決這些問題卵渴。先來看看如何從 main bundle 中分離另一個(gè)模塊:

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

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

webpack.config.js

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

正如前面提到的慧域,這種方法存在一些問題:

  • 如果入口 chunks 之間包含重復(fù)的模塊,那些重復(fù)模塊都會被引入到各個(gè) bundle 中奖恰。
  • 這種方法不夠靈活吊趾,并且不能將核心應(yīng)用程序邏輯進(jìn)行動(dòng)態(tài)拆分代碼。

以上兩點(diǎn)中瑟啃,第一點(diǎn)對我們的示例來說無疑是個(gè)問題论泛,因?yàn)橹拔覀冊?./src/index.js 中也引入過 lodash,這樣就在兩個(gè) bundle 中造成重復(fù)引用蛹屿。

提取公共代碼

SplitChunksPlugin 插件可以將公共的依賴模塊提取到已有的入口 chunk 中屁奏,或者提取到一個(gè)新生成的 chunk。

splitChunks 在 production 模式下自動(dòng)開啟错负。有一些默認(rèn)配置坟瓢,通過 "optimization" 配置參數(shù)詳細(xì)說明:

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const webpack = require("webpack");

module.exports = merge(common, {

  ···
  
  optimization: {
    runtimeChunk: {     // 自動(dòng)拆分 runtime 文件
      name: 'manifest'
    },   
    splitChunks:{
      chunks: 'initial',        // initial(初始塊)、async(按需加載塊)犹撒、all(全部塊)折联,默認(rèn)為 async
      minSize: 30000,           // 形成一個(gè)新代碼塊最小的體積(默認(rèn)是30000)
      minChunks: 1,             // 在分割之前,這個(gè)代碼塊最小應(yīng)該被引用的次數(shù)(默認(rèn)為 1 )  
      maxAsyncRequests: 5,      // 按需加載時(shí)候最大的并行請求數(shù)
      maxInitialRequests: 3,    // 一個(gè)入口最大的并行請求數(shù)
      name:"common",            // 打包的 chunks 的名字(字符串或者函數(shù)识颊,函數(shù)可以根據(jù)條件自定義名字)
      automaticNameDelimiter: '~',  // 打包分隔符
      cacheGroups: {           // 這里開始設(shè)置緩存的 chunks
        vendors: {             
          name: 'vendors',
          chunks: 'all',
          priority: -10,        // 緩存組打包的先后優(yōu)先級(只用于緩存組)
          reuseExistingChunk: true, // 可設(shè)置是否重用該 chunk (只用于緩存組)
          test:/[\\/]node_modules[\\/]/  // 只用于緩存組
        },
        components: {
          test: /components\//,
          name: "components",
          chunks: 'initial',
          enforce: true
        }
      }
    }
  },
  
  ···
  
});

runtimeChunk 的作用是將包含 chunks 映射關(guān)系的 list 單獨(dú)從 app.js 里提取出來诚镰,因?yàn)槊恳粋€(gè) chunk 的 id 基本都是基于內(nèi)容 hash 出來的奕坟,所以你每次改動(dòng)都會影響它,如果不將它提取出來的話清笨,等于 app.js 每次都會改變月杉。緩存就失效了。

配置完成再執(zhí)行構(gòu)建抠艾。lodash 就被提取到 vendors.bundle.js 文件苛萎,在項(xiàng)目中只加載一次。

完整 demo 可在 webpack-optimization/optimization-split 文件夾查看检号。

動(dòng)態(tài)導(dǎo)入

當(dāng)涉及到動(dòng)態(tài)代碼拆分時(shí)腌歉,webpack 提供了兩個(gè)類似的技術(shù)。對于動(dòng)態(tài)導(dǎo)入谨敛,第一種究履,也是優(yōu)先選擇的方式是,使用符合 ECMAScript 提案 的 import() 語法脸狸。第二種最仑,則是使用 webpack 特定的 require.ensure。讓我們先嘗試使用第一種……

首先安裝插件:

 npm install --save-dev @babel/plugin-syntax-dynamic-import

在 .babelrc 文件中添加配置:

    {
        "presets": [
          ["@babel/preset-env",
            {
              "useBuiltIns": "usage"
            }
          ]
        ],
+       "plugins": ["@babel/plugin-syntax-dynamic-import"]
    }

在通用配置 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: {
    filename: "[name].bundle.js",
+   chunkFilename:'[name].bundle.js', // 非入口 chunk 的名稱
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(["dist"]),
    new HtmlWebpackPlugin({
      title: "Output Management"
    })
  ],
  module: {
    ···
  }
};

配置完成后就可在 asyncIndex.js 中嘗試:

    // import _ from 'lodash';
    
+   // 測試動(dòng)態(tài)導(dǎo)入
+   async function getComponent() {
+     var element = document.createElement("div");
+     console.log("開始加載lodash");
+     const _ = await import(/* webpackChunkName: "lodash" */ "lodash");
+     console.log("lodash加載成功");
+     element.innerHTML = _.join(["Hello", "webpack"], " ");
+   
+     return element;
+   }
+   getComponent().then(component => {
+     document.body.appendChild(component);
+   });

懶加載

本節(jié)沿用上一節(jié) 代碼分離 的代碼

懶加載或者按需加載炊甲,是一種很好的優(yōu)化網(wǎng)頁或應(yīng)用的方式泥彤。這種方式實(shí)際上是先把你的代碼在一些邏輯斷點(diǎn)處分離開,然后在一些代碼塊中完成某些操作后卿啡,立即引用或即將引用另外一些新的代碼塊吟吝。這樣加快了應(yīng)用的初始加載速度,減輕了它的總體體積颈娜,因?yàn)槟承┐a塊可能永遠(yuǎn)不會被加載剑逃。

新建 print.js 文件:

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

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
}

然后在 index.js 文件中:

    import { component, imageComponent, iconComponent, dataComponent } from "./components/hello-world/index.js";
    import { cube } from "./math.js";
    import _ from "lodash";
    
    function mathComponent() {
      var element = document.createElement("pre");
    
      // element.innerHTML = ["Hello webpack!", "5 cubed is equal to " + cube(5)].join("\n\n");
      element.innerHTML = _.join(["Hello", "loadsh", cube(5)], " ");
      return element;
    }
    
+   function buttonComponent() {
+     var element = document.createElement("div");
+     var button = document.createElement("button");
+     var br = document.createElement("br");
+     button.innerHTML = "Click me and look at the console!";
+     element.innerHTML = _.join(["Hello", "webpack"], " ");
+     element.appendChild(br);
+     element.appendChild(button);
    
+     // Note that because a network request is involved, some indication
+     // of loading would need to be shown in a production-level site/app.
+     button.onclick = e =>
+       import(/* webpackChunkName: "print" */ "./print").then(module => {
+         var print = module.default;
    
+         print();
+       });
    
+     return element;
+   }
    
    document.body.appendChild(imageComponent());
    document.body.appendChild(iconComponent());
    document.body.appendChild(dataComponent());
    document.body.appendChild(mathComponent());
+   document.body.appendChild(buttonComponent());
    
    // 只有 component() 模塊才是熱更行
    let element = component(); 
    document.body.appendChild(element);
    
    if (module.hot) {
      module.hot.accept("./components/hello-world/index.js", function() {
        console.log("Accepting the updated printMe module!");
    
        document.body.removeChild(element);
        element = component();
        document.body.appendChild(element);
      });
    }

運(yùn)行項(xiàng)目,可以發(fā)現(xiàn)在單擊按鈕之后才加載文件 print.bundle.js 官辽。

完整 demo 可在 webpack-optimization/optimization-split 文件夾查看蛹磺。

緩存

更改生產(chǎn)配置:

    const merge = require("webpack-merge");
    const common = require("./webpack.common.js");
    const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
    const ExtractTextPlugin = require("extract-text-webpack-plugin");
    const webpack = require("webpack");
    
    module.exports = merge(common, {
+      output: {
+       filename: "[name].[chunkhash].js",
+       chunkFilename:'[name].[chunkhash].js', // 非入口 chunk 的名稱
+     },
      plugins: [
        new UglifyJSPlugin({
          sourceMap: true
        }),
        new webpack.DefinePlugin({
          "process.env.NODE_ENV": JSON.stringify("production")
        }),
        new ExtractTextPlugin({
          filename: "[name].css"
        }),
+       new webpack.HashedModuleIdsPlugin()
      ],
      mode: "production",
      optimization: {
        splitChunks:{
          chunks: 'initial',        // initial(初始塊)、async(按需加載塊)同仆、all(全部塊)萤捆,默認(rèn)為async
          minSize: 30000,           // 形成一個(gè)新代碼塊最小的體積(默認(rèn)是30000)
          minChunks: 1,             // 在分割之前,這個(gè)代碼塊最小應(yīng)該被引用的次數(shù)(默認(rèn)為 1 )  
          maxAsyncRequests: 5,      // 按需加載時(shí)候最大的并行請求數(shù)
          maxInitialRequests: 3,    // 一個(gè)入口最大的并行請求數(shù)
          name:"common",            // 打包的 chunks 的名字(字符串或者函數(shù)俗批,函數(shù)可以根據(jù)條件自定義名字)
          automaticNameDelimiter: '~',  // 打包分隔符
          cacheGroups: {           // 這里開始設(shè)置緩存的 chunks
            vendors: {             
              name: 'vendors',
              chunks: 'all',
              priority: -10,        // 緩存組打包的先后優(yōu)先級(只用于緩存組)
              reuseExistingChunk: true, // 可設(shè)置是否重用該 chunk (只用于緩存組)
              test:/[\\/]node_modules[\\/]/  // 只用于緩存組
            },
            components: {
              test: /components\//,
              name: "components",
              chunks: 'initial',
              enforce: true
            }
          }
        },
+       runtimeChunk: {     // 自動(dòng)拆分 runtime 文件
+         name: 'manifest'
+       }
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            exclude: /node_modules/,
            use: ExtractTextPlugin.extract({
              fallback: "style-loader",
              use: [
                { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
                {
                  loader: "postcss-loader",
                  options: {
                    ident: "postcss",
                    plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
                  }
                }
              ]
            })
          }
        ]
      }
    });

完整 demo 可在 webpack-optimization/optimization-cache 文件夾查看俗或。

配置別名

在通用配置下:

  resolve: {
    extensions: [".js", ".css", ".json"],
    alias: {
      asset: __dirname + "/src/asset"
    } //配置別名可以加快webpack查找模塊的速度
  },

完整 demo 可在 webpack-optimization/optimization-cache 文件夾查看。

拆分頁面

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");

module.exports = {
  entry: {
    app: "./src/index.js",
    es6Index: "./src/es6Index.js"
  },
  output: {
    filename: "[name].bundle.js",
    chunkFilename: "[name].bundle.js", // 非入口 chunk 的名稱
    path: path.resolve(__dirname, "dist")
  },
  resolve: {
    extensions: [".js", ".css", ".json"],
    alias: {
      asset: __dirname + "/src/asset"
    } //配置別名可以加快webpack查找模塊的速度
  },
  plugins: [
    new CleanWebpackPlugin(["dist"]),
    new HtmlWebpackPlugin({
      filename: "index.html",
      hash: true,
      chunks: ["app", "vendors", "commons", "manifest"]
    }),
    new HtmlWebpackPlugin({
      filename: "es6Index.html",
      hash: true,
      chunks: ["es6Index", "vendors", "commons", "manifest"]
    })
  ],
    
  ···
    
};

完整 demo 可在 webpack-optimization/optimization-cache 文件夾查看岁忘。

學(xué)習(xí)資料

https://www.webpackjs.com/guides/hot-module-replacement/

https://webxiaoma.com/webpack/entry.html#%E5%8A%A8%E6%80%81%E5%85%A5%E5%8F%A3

https://survivejs.com/webpack/developing/composing-configuration/

一步一步的了解webpack4的splitChunk插件

webpack4 新特性

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辛慰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子干像,更是在濱河造成了極大的恐慌昆雀,老刑警劉巖辱志,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狞膘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)什乙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門挽封,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人臣镣,你說我怎么就攤上這事辅愿。” “怎么了忆某?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵点待,是天一觀的道長。 經(jīng)常有香客問我弃舒,道長癞埠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任聋呢,我火速辦了婚禮苗踪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘削锰。我一直安慰自己通铲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布器贩。 她就那樣靜靜地躺著颅夺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛹稍。 梳的紋絲不亂的頭發(fā)上吧黄,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音稳摄,去河邊找鬼稚字。 笑死,一個(gè)胖子當(dāng)著我的面吹牛厦酬,可吹牛的內(nèi)容都是我干的胆描。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼仗阅,長吁一口氣:“原來是場噩夢啊……” “哼昌讲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起减噪,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤短绸,失蹤者是張志新(化名)和其女友劉穎车吹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體醋闭,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窄驹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了证逻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乐埠。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖囚企,靈堂內(nèi)的尸體忽然破棺而出丈咐,到底是詐尸還是另有隱情,我是刑警寧澤龙宏,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布棵逊,位于F島的核電站,受9級特大地震影響银酗,放射性物質(zhì)發(fā)生泄漏辆影。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一花吟、第九天 我趴在偏房一處隱蔽的房頂上張望秸歧。 院中可真熱鬧,春花似錦衅澈、人聲如沸键菱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽经备。三九已至,卻和暖如春部默,著一層夾襖步出監(jiān)牢的瞬間侵蒙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工傅蹂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纷闺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓份蝴,卻偏偏與公主長得像犁功,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子婚夫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容