新版webpack4.0指南

此项目总共24节,主要参考资料如下:
视频:https://coding.imooc.com/lear...
博客:https://itxiaohao.github.io/b...
文章:
https://webpack.js.org/
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://www.cnblogs.com/kwzm/...

一、webpack简介

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

二、WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务
Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件
如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件

三、webpack 核心概念:

  • Entry :入口
  • Module:模块,webpack中一切皆是模块
  • Chunk: 代码库,一个chunk由十多个模块组合而成,用于代码合并与分割
  • Loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • Plugin: 扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • Output: 输出结果

四、webpack打包流程:

webpack启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有Module.每找到一个Module,就会根据配置的Loader去找出对应的转换规则,对Module进行转换后,再解析出当前的Module依赖的Module.这些模块会以Entry为单位进行分组,一个Entry和其所有依赖的Module被分到一个组也就是一个Chunk。最好Webpack会把所有Chunk转换成文件输出。在整个流程中Webpack会在恰当的时机执行Plugin里定义的逻辑

五、搭建webpack环境

1.webpack是基于node环境的,所以使用webpack之前需要先安装node.js文件
2.安装完node.js之后可以在cmd命令行通过node -v 查看node是否安装成功,出现版本号即安装成功;然后通过npm -v 查看node中的包管理器是否安装成功,如果出现版本号,也说明安装成功
3.新建webpack-demo文件夹,然后cd进入这个文件目录,执行如下命令初始化npm

npm init -y

执行完之后,我们的文件夹中会多出一个package.json文件
新版webpack4.0指南_第1张图片
然后我们稍加修改

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,                     
  "scripts": {
    
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

"private"设置为 true, 表示私有的,不会被发布到npm的线上仓库中去
删除"main":"index.js"这行,意思是我们这个项目不会被外部引用,只是自己来用,没必要暴露一个js文件,这可以防止意外发布你的代码
4.package.json文件已经就绪,接下来安装webpack依赖

npm install --save-dev webpack webpack-cli

我们不是全局安装而是安装在项目内,此时在命令行输入webpack -v 查看版本号会显示出错

PS E:\Code\webpack4.0\webpack-demo> webpack -v
webpack : 无法将“webpack”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ webpack -v
+ ~~~~~~~
    + CategoryInfo          : ObjectNotFound: (webpack:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

但是没关系,node提供了一个npx命令,通过命令npx webpack -v就可以查看版本号

PS E:\Code\webpack4.0\webpack-demo> npx webpack -v
4.29.6

此时说明我们webpack安装成功
要想查看webpack以前的各种版本,可以通过如下命令

npm view webpack versions        

六、webpack的配置文件

现在我们将创建以下目录结构、文件和内容:

webpack-demo
  |- package-lock.json
  |- package.json
+ |- index.html
+ |- /src
+   |- index.js

src/index.js

function component() {
    var element = document.createElement('div');
    element.innerHTML = 'hello webapck';
  
    return element;
  }
  
  document.body.appendChild(component());

index.html



  
    起步
  
  
    
  

然后,我们稍微调整下目录结构,将“源”代码(/src)从我们的“分发”代码(/dist)中分离出来。“源”代码是用于书写和编辑的代码。“分发”代码是构建过程产生的代码最小化和优化后的“输出”目录,最终将在浏览器中加载:

webpack-demo
  |- package-lock.json
  |- package.json
+ |- /dist
+    |- index.html
- |- index.html
+ |- /src
+   |- index.js

dist/index.html

  
  
   
     起步
   
   
-    
+    
   
  

执行 npx webpack,会将我们的脚本作为入口起点,然后 输出 为 main.js。Node 8.2+ 版本提供的 npx 命令,可以运行在初始安装的 webpack 包(package)的 webpack 二进制文件(./node_modules/.bin/webpack):

PS E:\Code\webpack4.0\webpack-demo> npx webpack
Hash: 12bb1db463f0190f063f
Version: webpack 4.29.6
Time: 409ms
Built at: 2019-03-27 11:46:08
  Asset   Size  Chunks             Chunk Names
main.js  1 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 191 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

在浏览器中打开 index.html,如果一切访问都正常,你应该能看到以下文本:'Hello webpack'

从上面可以看到,我们并没有在文件中配置webpack的配置文件,为何也能打包成功呢?这是因为webpack内部提供了一套默认配置,所以我们打包的时候用的是它的默认配置文件,如果我们想自定义这个配置文件里面的内容,该怎么做呢?

我们增加一个webpack.config.js配置文件

webpack-demo
  |- package-lock.json
  |- package.json
+ |- webpack.config.js
  |- /dist
    |- index.html
    |- main.js
  |- /src
    |- index.js

webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',                         // 入口文件
    output: {
        filename: 'bundle.js',                       // 打包好之后的名字,之前默认是叫main.js 这里我们改为bundle.js
        path: path.resolve(__dirname, 'dist')        // 打包好的文件应该放到哪个文件夹下
    }
}

现在,让我们通过新配置文件再次执行构建

PS E:\Code\webpack4.0\webpack-demo> npx webpack
Hash: ececbdb7c981b95af3a3
Version: webpack 4.29.6
Time: 130ms
Built at: 2019-03-27 14:20:10
    Asset   Size  Chunks             Chunk Names
bundle.js  1 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 191 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

此时项目结构应该是

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

当我们运行npx webapck时,webpack并不知道如何去打包,于是它就是会找默认的配置文件,找到webpack.config.js这个文件,然后根据这个文件中配置的入口和出口打包了,假设我们这个配置文件的名字不是这个默认的名字,而是叫webpack.aaa.js,现在我们重新运行npx webpack,这个时候它就不会执行这个webpack.aaa.js这个文件了,而是会去走它内部的一套流程,打包出来的还是main.js而不是bundle.js,如果我们任然想输出bundle.js,这时我们可以执行如下命令

PS E:\Code\webpack4.0\webpack-demo> npx webpack --config webpack.aaa.js
Hash: ececbdb7c981b95af3a3
Version: webpack 4.29.6
Time: 116ms
Built at: 2019-03-27 14:45:53
    Asset   Size  Chunks             Chunk Names
bundle.js  1 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 191 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
如果 webpack.config.js 存在,则 webpack 命令将默认选择使用它。我们在这里使用 --config 选项只是向你表明,可以传递任何名称的配置文件。这对于需要拆分成多个文件的复杂配置是非常有用

测试完之后,我们把webpack.aaa.js文件还原成webpack.config.js

考虑到用 npx这种方式来运行本地的 webpack 不是特别方便,我们可以设置一个快捷方式。在 package.json 添加一个 npm 脚本(npm script):
package.json

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
+    "bundle": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0"
  }
}

意思是当我们运行bundle这个命令,它就会自动帮我们执行webpack这个命令,现在,可以使用 npm run bundle命令,来替代我们之前使用的 npx 命令

PS E:\Code\webpack4.0\webpack-demo> npm run bundle

> [email protected] bundle E:\Code\webpack4.0\webpack-demo
> webpack

Hash: 12bb1db463f0190f063f
Version: webpack 4.29.6
Time: 241ms
Built at: 2019-03-27 14:54:39
  Asset   Size  Chunks             Chunk Names
main.js  1 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 191 bytes {0} [built]

现在,我们已经实现了一个基本的构建过程,此刻你的项目应该和如下类似:

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

细节补充:
我们在之前打包的时候会发现命令行会出现如下警告

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concep...

是因为我们没有给打包设置模式,现在我们在webpack.config.js中设置mode
webpack.config.js

const path = require('path');

module.exports = {
    mode: 'production',                              // 不写的mode,默认就是生产模式
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                       
        path: path.resolve(__dirname, 'dist')       
}

重新打包,发现警告消失了,其实,这里mode除了可以设置production外还可以设置成development,设置development模式打包之后代码是不会被压缩的

七、webpack中loader

loader可以说是webpack最核心的部分,loader简单来说就是一个导出为函数的JavaScript模块,webpack会配置文件申明的倒序调用loader,传入资源文件,经loader处理后传给下一loader或者webpack处理, 通俗点理解就是,webpack自身只理解JavaScript,loader可以让webpack能够去处理那些非JavaScript文件

(一)、使用loader打包图片

安装file-loader

npm install file-loader --save-dev

webpack.config.js

    const path = require('path');

    module.exports = {
        mode: 'development',                             
        entry: './src/index.js',                         
        output: {
            filename: 'bundle.js',                      
            path: path.resolve(__dirname, 'dist')     
        },
+       module: {
+            rules: [                      // module.rules 允许你在 webpack 配置中指定多个 loader
+                {
+                    test: /\.(png|svg|jpg|gif)$/,
+                    use: [
+                        'file-loader'        // 这里其实是  {loader: 'file-loader'}的简写
+                    ]
+                }
+            ]
+        }
    }

往src目录下添加一张图片(如:04.jpg),然后删除index.js里面的内容,添加如下内容:

import avatar from './04.jpg';

var img = new Image();
img.src = avatar;

var root = document.getElementById('root');
root.append(img);

/dist/index.html




    
    
    
    起步


 +  

最后执行npm run bundle打包,会发现dist目录下多出了一张图片,现在目录结构如下

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

打开index.html文件,图片显示正常,说明我们已经打包成功

如果我们想自定义打包后图片的名字该如何处理呢?
webpack.config.js

module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        // [name]: 资源的基本名称    [ext]: 资源扩展名
     +                  options: {
     +                      name: '[name].[ext]'  
     +                  }
                    }
                ]
            }
        ]
    }

删除掉dist目录下的bundle.js和c613962b1e741b4139150622b2371cd9.jpg,然后重新执行npm run bundle,打开index.html文件仍然正常显示,现在dist目录下如下

|- /dist
  |- bundle.js
  |- 04.jpg
  |- index.html

现在我们图片是打包到dist目录下,如果我们想图片打包到别的目录下,可以通过outputPath这个属性来配置

module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
           +                outputPath: 'images/'              
                        }
                    }
                ]
            }
        ]
    }

删除掉dist目录下的bundle.js和04.jpg,然后重新执行npm run bundle,打开index.html文件仍然正常显示,现在dist目录下如下

|- /dist
  |-images
    |- 04.jpg
  |- bundle.js
  |- index.html
其实file-loader还有许多其它的参数,具体可以参见 file-loader文档

接下来,我们介绍一个和file-loader很类似的url-loader ,url-loader除了可以做file-loader的 工作之外 ,它还能做一个额外的事情
安装url-loader

npm install --save-dev url-loader

然后我们把dist目录下的images文件和bundle.js文件删掉,用url-loader替换掉file-loader
webpack.config.js

   {
 -     loader: 'file-loader',
 +     loader: 'url-loader'
       options: {
                 name: '[name].[ext]',
                 outputPath: 'images/'
               }
      }

然后重新执行npm run bundle,打包正常,但是我们发现图片并没有打包进dist目录下

|- /dist
  |- bundle.js
  |- index.html

打开index.html,发现图片还是能正常显示,是不是很奇怪,这到底是怎么回事呢?
我们打开控制台,发现图片地址是以base64的形式被引进来的
新版webpack4.0指南_第2张图片

这是因为当你去打包一个jpg格式的图片的时候,用了url-loader,它会把你图片转换成一个base64的字符串,然后直接放到bundle.js文件里面,而不是生成一个图片文件
但是如果这个loader这么用,其实是不合理的,虽然图片被打包进js里面,加载好js 图片自然就出来,它不用再去额外请求一个图片的地址了,省了一次http请求,但是带来的问题是什么呢?如果这个文件特别大,打包生成的js文件也就会特别的大,那么你加载这个js的时间就会很长,那么url-loader的最佳使用方式是什么?如果图片非常小只有1-2kb,那么图片被打包进js文件是个非常好的选择,如果图片很大,那就应该像file-loader一样,把图片打包到dist目录下,不要打包到bundle.js里,这样更合适

其实我们在options里再配置个参数limit就可以实现这个功能

{
   loader: 'url-loader',
   options: {
       name: '[name].[ext]',
       outputPath: 'images/',
 +     limit: 2048
     }
 }

意思是,如果你的图片大小超过了2048个字节的话,那么就会像file-loader一样,打包到dist目录下生成一个图片;但是如果图片小于2048个字节也就是小于2kb的时候,url-loader会直接把这个图片变成一个base64的字符串放到bundle.js中

接下来验证下,我们04.jpg图片是1.58M肯定大于20kb,执行npm run bundle打包,果然在dist目录下生成了图片

|- /dist
  |-images
    |- 04.jpg
  |- bundle.js
  |- index.html

然后我们删除掉images文件和bundle.js文件,再把limit值改为900000000,1.58M肯定小于这个值,再重新执行打包,发现图片被打包进bundle里面了

|- /dist
  |- bundle.js
  |- index.html
其实url-loader还有许多其它的参数,具体可以参见 url-loader文档

(二)、使用loader打包样式

安装style-loader和css-loader

npm install --save-dev style-loader css-loader

webpack.config.js

const path = require('path');

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
  +         {
  +           test: /\.css$/,
  +           use: ['style-loader', 'css-loader']
  +         }
        ]
    }
}

然后在src中新建一个index.css文件
src/index.css

.avatar {
    width: 150px;
    height: 150px;
}

src/index.js

  import avatar from './04.jpg';
+ import './index.css';

  var img = new Image();
  img.src = avatar;
+ img.classList.add('avatar')

var root = document.getElementById('root');
root.append(img);

重新运行npm run bundle,再次在浏览器中打开 index.html,你应该看到图片大小已经变成150*150了,检查页面,并查看页面的 head 标签。它应该包含我们在 index.js 中导入的 style 块元素 ,那么问题来了,为什么需要两个loader来处理呢?这是因为它们两个分工不同,css-loader会帮我们分析出所有css文件之间的关系, 最终把这些css文件合并成一段css,style-loader在得到css-loader生成的内容之后,style-loader会把这段内容挂载到页面的head部分

如果我们项目中用的是sass或者less该如何处理呢?
现在我们把src中的index.css改为index.scss文件
src/index.scss

body{
    .avatar {
        width: 150px;
        height: 150px;
    }
}

index.js

import avatar from './04.jpg';
- import './index.css';
+ import 'index.scss'

var img = new Image();
img.src = avatar;
img.classList.add('avatar')

var root = document.getElementById('root');
root.append(img);

webpack.config.js

 {
 -    test: /\.css$/,
 +    test: /\.scss$/,
      use: ['style-loader', 'css-loader']
 }

最后我们执行npm run bundle,打包成功刷新页面,发现图片又变回原来的大小,我们打开控制台head部分,发现style中的语法并不是css语法,而是原始的scss语法,所以浏览器当然是不能识别了,所以我们在打包scss文件时还需要借助其他额外的loader,帮助我们把scss语法翻译成css语法
新版webpack4.0指南_第3张图片

安装sass-loader和node-sass, node-sass是sass-loader的依赖,所以也需要一并安装

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

安装完成之后,再在webpack.config.js中配置sass-loader

 {
      test: /\.scss$/,
      use: [
          'style-loader',       // 将 JS 字符串生成为 style 节点
          'css-loader',         // 将 CSS 转化成 CommonJS 模块
+          'sass-loader'         // 将 Sass 编译成 CSS
      ]
 }

执行npm run bundle,刷新页面发现图片又变回150*150了,检查head,可以看到sass语法已经被编译成css语法
新版webpack4.0指南_第4张图片

注意: 在webpack的配置里面loader是有顺序的,执行顺序是 从下到上,从右到左,所以当我们去打包一个sass文件的时候,首先会执行sass-loader,对sass代码进行翻译,翻译成css代码之后给到css-loader,然后css-loader把所有的css合并成一个css模块,最后被style-loader挂载到页面的head中去

其实css-loader和sass-loader还有许多其它的参数,具体可以参见css-loader文档sass-loader文档

有时候我们写C3的新特性的时候,往往需要在这样写,目的是为了兼容不同版本浏览器

div {
    transform: translate(150px,150px);
    -ms-transform: translate(150px,150px);
    -moz-transform: translate(150px,150px);
    -webkit-transform: translate(150px,150px);
}

但是这样写起来会很麻烦,我们可不可以通过loader来自动为属性添加厂商前缀呢?答案肯定是可以的,接下来为大家介绍一个postcss-loader
安装postcss-loader

npm i -D postcss-loader

index.scss

body{
    .avatar {
        width: 150px;
        height: 150px;
 +      transform: translate(150px,150px)
    }
}

然后再在webpack-demo目录下创建一个postcss.config.js文件
postcss.config.js

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

这里我们还需要安装下autoprefixer

npm install autoprefixer -D

安装完成之后,我们在webpack.config.js中配置postcss-loader
webpack.config.js

  {
      test: /\.scss$/,
      use: [
          'style-loader',       // 将 JS 字符串生成为 style 节点
          'css-loader',         // 将 CSS 转化成 CommonJS 模块
+          'postcss-loader',
          'sass-loader',        // 将 Sass 编译成 CSS
      ]
 }

重新npm run bundle,打包成功之后刷新页面,显示正常,并且图片样式上会自动添加上了厂商前缀
新版webpack4.0指南_第5张图片

postcss-loader其他的参数使用具体见 postcss-loader文档

补充知识:

1、importLoader参数

如果我们在scss文件中又去引入了一个额外的scss文件,这种情况webpack该如何去处理呢?
首先我们在src中新建一个avatar.scss文件
src/avatar.scss

body {
    .abc {
        border: 5px solid red;
    }
}

index.scss

+ @import './avatar.scss';

   body{
        .avatar {
            width: 150px;
            height: 150px;
            transform: translate(150px,150px)
        }
    }

webpack打包的时候对于index.js中引入的index.scss文件,它会依次调用postcss-loader,sass-loader, css-loader,style-loader,但是它在打包index.scss文件的时候,它里面又通过import语法额外引入了一个avatar.scss文件,那么有可能这块的引入在打包的时候,就不会去走sass-loader和postcss-loader了,而是直接去走css-loader和style-loader了,如果我们希望在index.scss里面引入的avatar.scss文件也可以走sass-loader和postcss-loader,那该怎么办呢?这时我们需要在css-loader里面配置一个importLoaders参数
webpack.config.js

{
                test: /\.scss$/,
                use: [
                    'style-loader', 
   -                'css-loader'
   +                {
   +                    loader: 'css-loader',
   +                    options: {
       // 查询参数 importLoaders,用于配置「css-loader 作用于 @import 的资源之前」有多少个 loader
   +                         importLoaders: 2     // 0 => 无 loader(默认); 1 => postcss-loader; 2 => postcss-loader, sass-loader
   +                     }
   +                  },
                    'postcss-loader',
                    'sass-loader'
                ]
            }

意思就是你通过@import引入的scss文件在打包之前也要去走两个loader,也就是postcss-loader和sass-loader;这种语法就能保证无论你是在js里面直接去引入scss文件,还是在scss文件里再去引用别的scss文件,都会从下到上执行所有的loader,这样就不会出现任何的问题了

2、css模块化打包

在src下创建一个createAvatar.js文件
createAvatar.js

import avatar from './04.jpg';

function createAvatar() {
    var img = new Image();
    img.src = avatar;
    img.classList.add('avatar')

    var root = document.getElementById('root');
    root.append(img);
}

export default createAvatar;

index.js

import avatar from './04.jpg';
import './index.scss';
+ import createAvatar from './createAvatar';

+ createAvatar()

var img = new Image();
img.src = avatar;
img.classList.add('avatar')

var root = document.getElementById('root');
root.append(img);

重新执行npm run bundle,打包成功之后刷新页面,页面会正常显示两张图片,并且这两张图片都有avatar样式
新版webpack4.0指南_第6张图片

这说明我们通过import './index.scss'这种形式引入的css文件,相当于是全局的,如果我们一不小心改了这个文件里面的样式,很可能会影响到另一个文件里面的样式,很容易出现样式冲突的问题,这样就引出了css 模块化的概念,让css文件只作用于当前这个模块

我们在webpack.config.js中的css-loader中引入modules参数
webpack.config.js

{
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
     +                      modules: true               //  意思是开启css的模块化打包
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            }

然后我们在index.js中修改scss的引入
index.js

  import avatar from './04.jpg';
- import './index.scss';
+ import style from './index.scss';
+ import createAvatar from './createAvatar';

+ createAvatar()

  var img = new Image();
  img.src = avatar;
- img.classList.add('avatar')
+ img.classList.add(style.avatar)
  var root = document.getElementById('root');
  root.append(img);

然后重新打包,刷新页面,你会发现只有当前文件中的这个图片有样式,而通过createAvatar引入的这个图片是没有样式的

此时目录结构如下

webpack-demo
|- /dist
  |- images
    |- 04.jpg
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- 04.jpg
  |- avatar.scss
  |- createAvatar.js
  |- index.scss
  |- index.js
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

(三)、使用loader打包字体文件

首先删除index.js和index.scss里面的内容,然后删除dist目录下的imags文件夹和bundle.js
然后删除04.jpg和createAvatar.js,avatar.scss文件
现在目录结构如下:

webpack-demo
|- /dist
  |- index.html
|- /node_modules
|- /src
  |- index.scss
  |- index.js
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

首先我们在src中新建一个font文件夹
然后我们从IconFont中下载两个图标到本地,然后解压到文件夹,把文件夹中的.eot,.svg,.ttf,.woff,.woff2字体文件复制到font文件夹下
最后把解压文件夹中的iconfont.css文件里面的内容复制到index.scss文件中
接着我们 把index.scss中的iconfont字体文件的路径改对
新版webpack4.0指南_第7张图片
然后我们在index.js中添加如下代码

var root = document.getElementById('root');
import './index.scss'
root.innerHTML = '
'

在webpack.config.js中去掉css模块化配置并且在webpack中添加打包字体文件的loader

const path = require('path');

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
-                           modules: true
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
+           {
+              test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
+              use: ['file-loader']
+           }
        ]
    }
}

执行npm run bundle,打包成功之后刷新页面,字体图标已经生效
此时目录结构:

webpack-demo
|- /dist
  |- 4bba583098563e64f4b12ab1d27cd516.eot
  |- 7db708ac7335b8e8596a04a93c5501cd.ttf
  |- 0052329c35318bbe484b99b3d3e5aa47.woff
  |- 54718bd06e7ee6c87b9e2f41c96851ea.svg
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- font
    |- iconfont.eot
    |- iconfont.svg
    |- iconfont.ttf
    |- iconfont.woff
    |- iconfont.woff2
  |- index.scss
  |- index.js
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

八、webpack中plugins

插件是 webpack 生态系统的重要组成部分,为社区用户提供了一种强大方式来直接触及 webpack 的编译过程(compilation process)。插件能够 钩入(hook) 到在每个编译(compilation)中触发的所有关键事件。在编译的每一步,插件都具备完全访问 compiler 对象的能力,如果情况合适,还可以访问当前 compilation 对象。

(一)、html-webpack-plugin

在之前的项目中我们dist目录中的index.html文件是我们手动创建的,如果我们每次打包都自己手动创建那就太麻烦了,所以我们需要借助html-webpack-plugin这个插件,该插件会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。 你可以让插件为你生成一个HTML文件,使用lodash模板提供你自己的模板,或使用你自己的loader

安装

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

首先我们删除整个dist文件夹,然后再在webpack.config.js中配置这个插件

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

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
+   plugins: [new HtmlWebpackPlugin()]
}

最后执行npm run bundle,打包完成后会看到dist目录下webpack自动帮我们生成了一个index.html文件
但是我们会发现我们直接打开这个index.html文件字体图标并没有显示出来,这是因为我们在src/index.js中获取过root这个dom节点,但是我们打包生成的index.html中没有给我们自动生成一个这样的dom元素
新版webpack4.0指南_第8张图片

接下来,我们在src中创建一个index.html模板
index.html




    
    
    
-   html模板
+   <%= htmlWebpackPlugin.options.title %> 


    

然后在webpack.config.js中对HtmlWebpackPlugin重新配置下
webpack.confiig.js

plugins: [new HtmlWebpackPlugin(
+       {
+          template: 'src/index.html',           //意思是打包的时候以哪个html文件为模板
+          filename: 'index.html',               // 默认情况下生成的html文件叫index.html,可以自定义
+          title: 'test App',   // 为打包后的index.html配置title,这里配置后,在src中的index.html模板中就不能写死了,需要<%= htmlWebpackPlugin.options.title %>这样写才能生效
+          minify: {
+                collapseWhitespace: true        // 把生成的index.html文件的内容的没用空格去掉
+            }
+       }
    )]

重新删除dist目录,避免干扰,然后再去打包,打包完成之后打开dist目录中的index.html文件,可以看到字体图标能正常显示了

其他参数配置请见 html-webpack-plugin官方文档

(二)、clean-webpack-plugin

假如我们想改变打包生成之后的js文件名,比如我们不想叫bundle.js了而是想叫dist.js
webpack.config.js

output: {
-       filename: 'bundle.js',  
+       filename: 'dist.js',                    
        path: path.resolve(__dirname, 'dist')        
    },

重新npm run bundle,可以看到dist目录下会出多一个新打包出来的dist.js文件,但是上一次打包的bundle.js还是依然存在,我们希望的是,每次打包的时候,能帮我们先把dist目录先删除,然后重新生成,要实现这个功能我们就需要借助clean-webpack-plugin这个插件,这个插件不是官方推荐的,而是一个第三方插件

安装Webpack

npm install clean-webpack-plugin -D

webpack.confiig.js

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

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'dist.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin(
        {
            template: 'src/index.html',
            filename: 'index.html',
            minify: {
                collapseWhitespace: true
            }
        }
+    ),  new CleanWebpackPlugin()]
}

配置完了之后重新打包,发现之前打包生成的bundle.js就看不到了
新版webpack4.0指南_第9张图片

详情见 clean-webpack-plugin官方文档

此时目录结构如下

webpack-demo
|- /dist
  |- 4bba583098563e64f4b12ab1d27cd516.eot
  |- 7db708ac7335b8e8596a04a93c5501cd.ttf
  |- 0052329c35318bbe484b99b3d3e5aa47.woff
  |- 54718bd06e7ee6c87b9e2f41c96851ea.svg
  |- dist.js
  |- index.html
|- /node_modules
|- /src
  |- font
    |- iconfont.eot
    |- iconfont.svg
    |- iconfont.ttf
    |- iconfont.woff
    |- iconfont.woff2
  |- index.scss
  |- index.js
  |- index.html
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

九、Entry与Output的基础配置

entry顾名思义就是打包的入口文件
在webpack.config.js中entry对应的是一个字符串,其实它是下面这种方式的简写

entry: {
    main: './src/index.js'
}

默认打包输出的文件是main.js
假如我们有这样一个需求,我们需要将src/index.js文件打包两次,第一次打包到一个main.js中,第二次打包到一个sub.js中

-  entry: './src/index.js'
+  entry: {
+        main: './src/index.js',
+        sub: './src/index.js'
+    }, 
+  output: {
        filename: 'dist.js',                      
        path: path.resolve(__dirname, 'dist')        
    },

执行npm run bundle,我们会发现打包出错了,这是因为我们打包要生成两个文件一个叫main一个叫sub,最终都会起名叫dist.js,这样的话名字就冲突了,想要解决这个问题,我们就需要把output中的filename替换成一个占位符,而不是一个固定的名字

output: {
-         filename: 'dist.js',
+        filename: '[name].js',      //  这里name指的就是前面entry中对应的main和sub                   
         path: path.resolve(__dirname, 'dist')        
    },
这里占位符还有很多具体可以见 output参数

重新npm run bundle打包,打包完成之后我们发现dist目录中既有main.js也有sub.js文件,并且index.html中把main.js和sub.js同时都引入进来了

有的时候可能会有这样一种场景,打包完成之后我们会把这些打包好的js文件托管到CDN上,这时output.publicPath 是很重要的选项。如果指定了一个错误的值,则在加载这些资源时会收到 404 错误

output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
+       publicPath: 'http://cdn.com.cn'        
    },

重新打包,然后查看dist中的index.html,可以看到注入进来的js文件中每个文件前面都自动带有cdn域名
图片描述

十、SourceMap的配置

当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会简单地指向到 bundle.js。这并通常没有太多帮助,因为你可能需要准确地知道错误来自于哪个源文件。

为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你

现在我们做一些回退处理,将目录中dist目录删掉,然后把src中的font文件夹和index.scss删掉,并且清空index.js里面的内容
此时目录如下

webpack-demo
|- /node_modules
|- /src
  |- index.js
  |- index.html
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

然后对webpack.config.js做稍许修改

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

module.exports = {
    mode: 'development',          
+   devtool: 'none',           // 我们现在是开发模式,这个模式下,默认sourcemap已经被配置进去了,所以需要关掉              
    entry: {
        main: './src/index.js',
-       sub: './src/index.js'
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
-       publicPath: 'http://cdn.com.cn'        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin(
        {
            template: 'src/index.html',
            title: 'test App',
            filename: 'index.html',
            minify: {
                collapseWhitespace: true
            }
        }
    ), new CleanWebpackPlugin()]
}

然后再在src/index.js中生成一个错误

cosnole.error('I get error!');

重新打包,然后打开dist目录中的index.html文件,然后再控制台可以看到错误,但是我们只能看到这个错误来自于打包后的main.js里面,并不知道这个错误来自于源文件的哪一行里面,这对于我们代码调试非常不友好,我们需要webpack明确告诉我们是哪一个文件的哪一行出错,怎么做呢?
新版webpack4.0指南_第10张图片

现在我们对webpack.config.js中的devtool重新改下

    mode: 'development', 
-   devtool: 'none',
+   devtool: 'source-map',                            
    entry: {
        main: './src/index.js',
    },                       

然后npm run bundle,刷新页面,可以看到如果用source-map,在dist目录下会多出一个main.js.map文件,这个map文件中是一些映射的对应关系,它可以对我们源代码和打包后的代码做一个映射,

注意: 在谷歌浏览器中source-map还是无法指向源文件 新版webpack4.0指南_第11张图片
但是在火狐是可以指向源文件的 新版webpack4.0指南_第12张图片

官方文档中也提到source map在Chrome中有一些问题,具体看这里

此外我们devtool还可以配置inline-source-map,重新打包,刷新页面,可以看到在谷歌中它可以指向源文件
新版webpack4.0指南_第13张图片

但是我们在dist目录中发现,此时并没有main.js.map文件了,其实当我们用inline-source-map时,这个map文件会通过dataUrl的形式直接写在main.js里面
新版webpack4.0指南_第14张图片

此外devtool还可以配置inline-cheap-source-map,它类似于inline-source-map,唯一的区别就是inline-source-map会帮我们把错误代码精确到源文件的第几行第几个字符,但是我们一般只需要知道在哪一行就可以了,这样的一种映射它比较耗费性能,而加个cheap之后意思就是只需要映射哪一行出错就可以了,所以相对而言它的打包速度会快些

但是inline-cheap-source-map这个配置只会针对于我们的业务代码进行映射,比如这里我们的index.js文件和打包后的main.js做映射,它不会管引入的其他第三方模块之间的映射,如果我们想让webpack不仅管业务代码还管第三方模块错误代码之间的映射,那么我们可以配置这个inline-cheap-module-source-map

除此之外,我们还可以配置devtool:eval, eval是打包速度最快的一种方式,性能最好的一种,但是针对比较复杂的代码情况下,用eval可能提示出来的内容并不全面

最佳实践:在development模式,用cheap-module-eval-source-map; 在production模式下,用cheap-module-source-map

devtool还有许多其他参数,具体可以见devtool官方文档

十一、webpack-dev-server

每次我们改变代码之后,都会重新npm run bundle,然后手动打开dist目录下的index.html查看,才能实现代码的重新编译运行,实际上这种方式会导致我们的开发效率非常低下,我们希望我们改了src下的源代码dist目录自动重新打包

要想实现这种功能,有三种方法:

(一)、修改package.json配置

"scripts": {
     "bundle": "webpack"
 +   "watch": "webpack --watch"  // 意思是webpack会监听打包的文件,只要打包的文件发生变化,就会自动重新打包
  },

重新执行npm run watch,然后我们把src/index.html代码改下

-   cosnole.error('I get error!');
+   console.log('哈哈哈')

不用重新打包,我们刷新页面就可以看到控制台已经打印出了‘哈哈哈’字样

(二)、dev-server

有时候我们需要命令不仅能帮我们实现自动打包还能第一次运行的时候帮我们自动打开浏览器页面同时还能模拟一些服务器的功能,这时我们可以借助webpack-dev-server这个工具
webpack-dev-server 为你提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。它并不真实打包文件,只是在内存中生成
安装

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

修改webpack.config.js配置

+   devServer: {
+     contentBase: './dist'
+   },

以上配置告知 webpack-dev-server,在 localhost:8080 下建立服务,将 dist 目录下的文件,作为可访问文件。

让我们添加一个 script 脚本,可以直接运行开发服务器(dev server):
package.json

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "bundle": "webpack",
    "watch": "webpack --watch",
+   "start": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^9.5.1",
    "clean-webpack-plugin": "^2.0.1",
    "css-loader": "^2.1.1",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-server": "^3.3.1"
  }
}

现在,我们可以在命令行中运行 npm run start,可以看到它帮我们生成了一个服务器地址
新版webpack4.0指南_第15张图片
手动打开这个地址,在控制台看到内容正常打印出来了,如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码

devServer中我们还可以配置open参数:

   devServer: {
     contentBase: './dist',
     open: true     // 执行npm run start时会自动打开页面,而不需要我们手动打开地址,它等同于我们在package.json中"start": "webpack-dev-server --open" 这个命令  
     },

如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用
在 localhost:3000 上有后端服务的话,你可以这样启用代理:

devServer: {
        contentBase: './dist',
        open: true,
+       proxy: {
+           '/api:': 'http://localhost:3000'
+       }
    },

请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users

还可以设置端口号

devServer: {
        contentBase: './dist',
        open: true,
+       port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        }
    },

可以看到我们端口号已经变成了8080了
新版webpack4.0指南_第16张图片

webpack-dev-server还有其他很多参数,具体见 devServer官方文档

(三)、使用 webpack-dev-middleware

如果不用webpack-dev-server,我们可以通过webpack-dev-middlewar结合express手动写一个这样的服务
webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求,接下来是一个 webpack-dev-middleware 配合 express server 的示例

首先,安装 express 和 webpack-dev-middleware

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

接下来我们需要对 webpack 的配置文件做一些调整,以确保中间件(middleware)功能能够正确启用:

output: {
        filename: '[name].js',                           
        path: path.resolve(__dirname, 'dist'),
 +      publicPath: '/'         // 表示所有打包生成的文件之间的引用都加一个根路径
    },

publicPath 也会在服务器脚本用到,以确保文件资源能够在 http://localhost:3000 下正确访问

接下来,我们新建一个server.js文件

    webpack-demo
        |- dist
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- package.json
        |- package-lock.json
        |- postcss.config.js
+        |- server.js
        |- webpack.config.js

server.js

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const complier = webpack(config)   // 用webpack结合这个配置文件随时进行代码的编译

const app = express();
app.use(webpackDevMiddleware(complier, {
    publicPath: config.output.publicPath
}))


app.listen(3000, () => {
    console.log('server is running')
})

现在,添加一个 npm script,以使我们更方便地运行服务:
package.json

"scripts": {
    "bundle": "webpack",
    "watch": "webpack --watch",
    "start": "webpack-dev-server",
+   "server": "node server.js"
  },

执行npm run server,将会有类似如下信息输出,说明node服务器已经运行,并且已经帮我们打包好文件,然后我们打开localhost:3000,可以看到控制台打印正常,但是这个服务没有webpack-dev-server这样智能,每次更改源文件之后都需要手动刷新页面才能看到内容的变化
新版webpack4.0指南_第17张图片

几点区别:
output.publicPath: 是指打包后的html文件加载其他css/js时,加上publicPath这个路径。
devServer.contentBase: 是指以哪个目录为静态服务
devServer.publicPath: 此路径下的打包文件可在浏览器中访问,假设服务器运行在 http://localhost:8080 并且 output.filename 被设置为 bundle.js。默认 publicPath 是 "/",所以你的包(bundle)可以通过 http://localhost:8080/bundle.js 访问,可以修改 publicPath,将 bundle 放在一个目录publicPath: "/assets/",你的包现在可以通过 http://localhost:8080/assets/bundle.js 访问

十二、热模块更新

模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新

现在做一些回退处理
package.json

"scripts": {
-    "bundle": "webpack",
-    "watch": "webpack --watch",
     "start": "webpack-dev-server",
-    "server": "node server.js"
  },

删除掉server.js文件,并且对webpack.config.js做一些修改

    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
  -     publicPath: '/'
    },
    module: {
        rules: [
           ...
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
+           {
+                test: /\.css$/,
+                use: [
+                    'style-loader',
+                    'css-loader', 
+                    'postcss-loader',
+               ]
+           },
            ...
        ]
    }
}

然后我们去掉src/index.js中的内容,然后重新添加新的内容

import './style.css';

var btn = document.createElement('button');
btn.innerHTML = '新增';
document.body.appendChild(btn);

btn.onclick = function() {
    var div = document.createElement('div');
    div.innerHTML = 'item';
    document.body.appendChild(div)
}

同时在src目录下新增一个style.css文件

div:nth-of-type(odd) {
    background-color: yellow;
}

重新npm run start,会看到页面上多了一个新增按钮,点击新增按钮,页面会出现item,并且奇数的item背景色是黄色;
现在我们把style.css中的背景色改为blue,保存,回到页面,webpack-dev-server发现代码改变了,它会帮我们重新打包编译并且重新刷新页面,导致页面上的这些item全部都没有了,如果我们想测试这些item背景色是否改变,就需要重新点击按钮,每次这样的话就会很麻烦, 我们希望当我们改变样式代码的时候,不要帮我们刷新页面,只是把样式代码替换掉就可以了,之前页面渲染出来的这些东西不要动,这个时候就可以借助HMR的这个功能来帮我们实现

打开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 = {
    mode: 'development', 
    devtool: 'heap-module-eval-source-map',                            
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist')
    },
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
+       hot: true,               // 启用 webpack 的模块热替换特性
+       hotOnly: true            // 即使HMR功能不生效,也不让浏览器自动刷新
    },
    module: {
        ...
    },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
+       new webpack.HotModuleReplacementPlugin()        // webapck内置插件
    ]
}

重新运行npm run start,然后点击新增,背景色变成蓝色了,然后我们到style.css中将blue变成red,回到页面可以看到背景为蓝色的地方已经全部替换成了红色,而页面并没有全部刷新,只是有样式改变的地方局部进行了刷新

那么HMR在js中有什么好处呢?接下来看下面这个例子
在src中新增一个counter.js和number.js文件
counter.js

function counter() {
    var div = document.createElement('div');
    div.setAttribute('id', 'counter');
    div.innerHTML = 1;
    div.onclick = function() {
        div.innerHTML = parseInt(div.innerHTML, 10) + 1;
    }
    document.body.appendChild(div)
}

export default counter;

number.js

function number() {
    var div = document.createElement('div');
    div.setAttribute('id', 'number');
    div.innerHTML = 1000;
    document.body.appendChild(div)
}
export default number;

index.js

// import './style.css';

// var btn = document.createElement('button');
// btn.innerHTML = '新增';
// document.body.appendChild(btn);

// btn.onclick = function() {
//     var div = document.createElement('div');
//     div.innerHTML = 'item';
//     document.body.appendChild(div)
// }

import counter from './counter.js';
import number from './number.js';
counter();
number();

然后我们把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 = {
    mode: 'development', 
    devtool: 'heap-module-eval-source-map',                            
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist')
    },
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        // hot: true,               // 启用 webpack 的模块热替换特性
        // hotOnly: true            // 即使HMR功能不生效,也不让浏览器自动刷新
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader', 
                    'postcss-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        // new webpack.HotModuleReplacementPlugin()        // webapck内置插件
    ]
}

重新npm run start,页面上可以看到一个1一个1000,我们点击1这个地方让这个数字一直加到某个值如:8,然后我们回到number.js中,把div.innerHTML = 1000 改为div.innerHTML = 2000,保存,回到页面,我们发现之前加到8的数字又重新变回1了,这是因为我们改了代码,webpack重新编译重新刷新页面了,我们希望下面这个数字改变了不要影响我上面加好了的数字,现在借助HMR就可以实现我们的目标

现在我们把webpack.config.js中之前注释的代码全部放开,我们重新npm run bundle;回到页面,我们把上面的这个1点成某个值如:10,然后我们回到number.js中,把div.innerHTML = 2000 改为div.innerHTML = 3000,保存,回到页面,发现页面2000并没有变成3000,这是因为代码虽然重新编译了,但是index.js中number()没有被重新执行,此时我们需要在index.js中增加点代码:
src/index.js

// import './style.css';

// var btn = document.createElement('button');
// btn.innerHTML = '新增';
// document.body.appendChild(btn);

// btn.onclick = function() {
//     var div = document.createElement('div');
//     div.innerHTML = 'item';
//     document.body.appendChild(div)
// }

import counter from './counter.js';
import number from './number.js';
counter();
number();

+ if(module.hot) {
     // 如果number这个文件发生了变化,那么就会执行后面这个函数,让number()重新执行下
+    module.hot.accept('./number', () => {
         // 获取之前的元素,删除它
+        let abc= document.getElementById('number');
+        document.body.removeChild(abc);
+        number();
+    })         
+  }

做完这步重新npm run start,然后回到页面,把1点成某个值如:10,然后我们回到number.js中,把div.innerHTML = 3000 改为div.innerHTML = 4000,保存,回到页面,此时可以看到此时3000已经变成4000了,但是上面的10还是10,没有变成1,说明热模块更新已经成功
那为什么上面的样式文件的改变,可以不用写if(module.hot){...}这样的代码,就能达到热模块更新的效果呢?这是因为style-loader已经内置了这样的功能,当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept 来修补(patch) style标签,像其他loader也有这个功能,比如:vue-loader 此 loader 支持用于 vue 组件的 HMR,提供开箱即用体验

关于热模块替换可以参考 热模块替换官方文档
module.hot的其他参数可以参考 这里

十三、使用babel处理ES6语法

对之前的项目目录进行简化,删除src下的counter.js, number.js, style.css, 然后把index.js中的内容全部清除
此时目录结构

webpack-demo
        |- dist
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

然后再在index.js中写一点ES6的语法
index.js

const arr = [
    new Promise(() => {}),
    new Promise(() => {})
];

arr.map(item => {
    console.log(item)
})

重新npm run start,编译成功之后,打开console,可以看到Promise被打印出来了,说明ES6语法运行是没有任何问题的,这是因为谷歌浏览器对ES6语法是支持的,但是有很多低版本浏览器比如IE,对ES6是不支持的,我们就需要把它转换成ES5语法,要实现这种转换我们需要借助babel

安装

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

安装完成之后,在webpack.config.js中增加babel配置规则
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 = {
    module: {
        rules: [
+            { 
+                test: /\.js$/, 
+                exclude: /node_modules/,   // 如果js文件在node_modules里面,就不使用这个babel-loader了,node_module里面的js实际上是一些第三方代码,没必要对这些代码进行ES6转ES5操作
+                loader: "babel-loader" 
+            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            ...
        ]
    },
    ...
}

当我们使用babel-loader处理js文件的时候,实际上这个babel-loader只是webpack和babel做通信的一个桥梁,我们配置了Babel但它并不会帮你把ES6语法翻译成ES5的语法,所以还需要借助@babel/preset-env这个模块
安装@babel/preset-env

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

然后再在webpack.config.js中重新进行配置

{ 
       test: /\.js$/, 
       exclude: /node_modules/, 
       loader: "babel-loader",
  +     options: {
  +              presets: ["@babel/preset-env"]
  +         } 
       },

然后我们通过npx webpack进行打包,打包完成之后打开在dist目录下打开main.js文件,在最下面可以看到之前写的ES6语法已经被翻译成ES5语法了
新版webpack4.0指南_第18张图片

(一)、@babel/polyfill : ES6 内置方法和函数转化垫片

但是光做到这样还不够,因为像Promise,map这些新的语法变量和方法在低版本浏览器中还是不存在的,所以我们不仅要使用@babel/preset-env做语法上的转换,还需要把这些新的语法变量和方法补充到低版本浏览器里,这里我们借助@babel/polyfill

使用 @babel/polyfill 的原因
Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。必须使用 @babel/polyfill,为当前环境提供一个垫片。
所谓垫片也就是垫平不同浏览器或者不同环境下的差异

安装

npm install --save @babel/polyfill

index.js

+ import "@babel/polyfill";

 const arr = [
    new Promise(() => {}),
    new Promise(() => {})
 ];

 arr.map(item => {
    console.log(item)
 })

没引进@babel/polyfill之前我们打包,main.js的大小只有29.5kb
新版webpack4.0指南_第19张图片
引进了之后我们重新npx webpack,打包之后看main.js一下就变成了1.04Mb了
新版webpack4.0指南_第20张图片
这多的内容就是@babel/polyfill弥补的的一些低版本浏览器不存在的内容

我们现在只用了Promise和map语法,其他的ES6的语法我们在这里并没有用到,实际上这样引入@babel/polyfill,它会把其他ES6的补充语法一并打包到main.js中了,我们可以继续优化下
webpack.config.js

      { 
          test: /\.js$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader',
          options: {
-                 presets: ['@babel/preset-env']
+                 presets: [['@babel/preset-env', {
+                        useBuiltIns: 'usage'   // 根据业务代码决定补充什么内容
+                    }]]
          } 
      },

打包发现报错了
新版webpack4.0指南_第21张图片

这里其实我们还需要安装一个core-js,具体原因可以参考这里

npm install --save [email protected]

安装完成之后,对presets重新配置

presets: [['@babel/preset-env', {
         useBuiltIns: 'usage',
 +       corejs: 3
        }]]

配置了useBuiltIns: 'usage'了之后,polyfill在需要的时候会自动导入,所以可以把全局引入的这段代码注释掉了

// 全局引入
// import "@babel/polyfill";

重新npx webpack,发现打包出的main.js体积小了不少
新版webpack4.0指南_第22张图片

presets 里面还可以配置targets参数

{ 
          test: /\.js$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader',
          options: {
                   presets: [
                        ['@babel/preset-env', {
       +                    targets: {
       +                         chrome: "67"
       +                    },
                            useBuiltIns: 'usage',
                            corejs: 3
                    }]
                ]
           }
     },

这段代码意思是webpack打包的时候会判断Chrome浏览器67以上的版本是否兼容ES6,如果兼容它打包的时候就不会做ES6转ES5,如果不兼容就会对ES6转ES5操作

现在验证下,我用的谷歌版本是73.0.3683.103,是兼容ES6新的api的,所以它不会通过@babel/polyfill对这些新的api进行转化了,重新npx webpack,可以看到因为没有用到@babel/polyfill,打包体积又变回了之前的29.6kb了
新版webpack4.0指南_第23张图片
打开dist目录下的main.js,到最下面可以看到webapck确实没有对Promise和map这些ES6语法进行转化

@babel/polyfill的详细介绍可以参考 官网

(二)、@babel/plugin-transform-runtime : 避免 polyfill 污染全局变量,减小打包体积

但是这样配置也不是所有的场景都适用的,比如你在开发一个类库或者开发一个第三方模块或者组件库的时候,实际上用@babel/polyfill这种方案是有问题的,因为它在注入这些Promise和map方法的时候,它会通过全局变量的形式注入,会污染全局环境,所以我们需要换一种配置方式,使用@babel/plugin-transform-runtime

使用 @babel/plugin-transform-runtime 的原因
Babel 使用非常小的助手来完成常见功能。默认情况下,这将添加到需要它的每个文件中。这种重复有时是不必要的,尤其是当你的应用程序分布在多个文件上的时候。 transform-runtime 可以重复使用 Babel 注入的程序代码来节省代码,减小体积。

index.js中我们注释掉import "@babel/polyfill"这段代码

// import "@babel/polyfill";

const arr = [
    new Promise(() => {}),
    new Promise(() => {})
];

arr.map(item => {
    console.log(item)
})

安装

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

安装完成之后我们在webpack.config.js重新进行配置

{ 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: 'babel-loader',
        options: {
               // presets: [
               //     ['@babel/preset-env', {
               //         targets: {
               //             chrome: "67"
               //         },
               //         useBuiltIns: 'usage',
               //         corejs: 3
               //     }]
               // ]
+              'plugins': [
+                     ['@babel/plugin-transform-runtime', {
+                         'absoluteRuntime': false,
+                         'corejs': 2,
+                         'helpers': true,
+                         'regenerator': true,
+                         'useESModules': false
+                    }]
+                ]
           } 
   },

打包npx webpack,发现报错了,这是因为我们配置了'corejs': 2,所以还需要额外安装一个包

npm install --save @babel/runtime-corejs2

安装完成之后,重新npx webpack,这样打包就没有任何问题了

注意: 如果你写的只是业务代码的时候,那你配置的时候只需要配置presets:[['@babel/preset-env',{...}]]这段代码,并且在业务代码前面引入import "@babel/polyfill"就可以了;
如果你写的是一个库相关的代码的时候,你需要使用@babel/plugin-transform-runtime这个插件,它的好处是不会污染全局环境,所以当你写类库的时候不去污染全局环境是一个更好的方案

@babel/plugin-transform-runtime的详细介绍可以参考官网

知识补充点:
我们看到babel对应的配置项会非常多,也非常长,我们可以在根目录下创建一个.babelrc文件,然后把options对应的这个对象剪切到.babelrc文件中
.babelrc

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

然后去掉webpack.config.js中的options

{ 
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: 'babel-loader',
   -            options: {
   -                 // presets: [
   -                 //     ['@babel/preset-env', {
   -                 //         targets: {
   -                 //             chrome: '67'
   -                 //         },
   -                 //         useBuiltIns: 'usage',
   -                 //         corejs: 3
   -                 //     }]
   -                 // ]
   -                 'plugins': [
   -                     ['@babel/plugin-transform-runtime', {
   -                         'absoluteRuntime': false,
   -                         'corejs': 2,
   -                         'helpers': true,
   -                         'regenerator': true,
   -                         'useESModules': false
   -                     }]
   -  
   -                 ]
   -             } 
            },

保存,重新打包npx webpack,可以看到依然可以正常打包
此时目录结构为

webpack-demo
        |- dist
          |- index.html
          |- main.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

十四、webpack实现对React框架代码的打包

首先做一些回退处理,我们现在是写的业务代码,所以在babelrc文件配置@babel/preset-env

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

+    "presets": [
+        ["@babel/preset-env", {
+            "targets": {
+                "chrome": "67"
+            },
+            "useBuiltIns": "usage",
+            "corejs": 3
+        }]
+    ]
} 

然后安装React包

npm install react react-dom --save

index.js

import React, {Component} from 'react';
import ReactDom from 'react-dom';

class App extends Component {
    render() {
        return 
Hello World
} } ReactDom.render(, document.getElementById('root'))

执行npm run start,然后打开页面控制台,发现页面报错,其实是浏览器不识别React这种jsx语法,我们我们需要借助@babel/preset-react这个工具来实现对React的打包
安装

npm install --save-dev @babel/preset-react

安装完成之后,在babelrc中进行配置
.babelrc

{
    "presets": [
        ["@babel/preset-env", {
            "targets": {
                "chrome": "67"
                },
            "useBuiltIns": "usage",
            "corejs": 3
            }
        ],
+       "@babel/preset-react"
    ]
} 

重新npm run start,此时页面显示正常了

@babel/preset-react的详细介绍可以参考 官网

十五、Tree Shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。

新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

(一)、JS Tree Shaking

在我们的项目中添加一个新的通用模块文件 src/math.js,此文件导出两个函数:

webpack-demo
        |- dist
          |- index.html
          |- main.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
+          |- math.js
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

math.js

export const add = (a, b) => {
    console.log(a + b)
}

export const minus = (a, b) => {
    console.log(a - b)
}

index.js

import { add } from './math.js';

add(1,2)

然后打包npx webpack,在控制台可以看到输出3,说明代码已经正确运行了,实际上在index.js里面我们引入了add方法,但是我们并没有引入minus方法,但是在打包的时候可以看到在main.js中webpack不仅把add方法打包进来了,还把minus方法也打包进来
新版webpack4.0指南_第24张图片
我们的业务代码中实际上只用到了add方法,如果把minus方法也打包进来是没有必要的,会使我们的main.js文件变的很大,最理想的打包方式是我们引入什么就帮我们打包什么,所以我们需要借助tree shaking功能

注意:tree shaking只支持ES Module这种模块的引入,如果使用这种CommonJs的引入方式require('./math.js'),tree shaking是不支持的

在development模式下,默认是没有tree shaking这个功能,要想加上需要这样配置

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

module.exports = {
    mode: 'development', 
    devtool: 'heap-module-eval-source-map',                            
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true,               // 启用 webpack 的模块热替换特性
        hotOnly: true            // 即使HMR功能不生效,也不让浏览器自动刷新
    },
    module: {
        rules: [
            { 
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: 'babel-loader'
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader', 
                    'postcss-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
+   optimization: {
+       usedExports: true
+   },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new webpack.HotModuleReplacementPlugin()        // webapck内置插件
    ]
}

接着在package.json里面加上sideEffects属性为false,意思是tree shaking对所有模块都做tree shaking,没有要特殊处理的东西

{
  "name": "webpack-demo",
+ "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "start": "webpack
    -dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.4.3",
    "@babel/plugin-transform-runtime": "^7.4.3",
    "@babel/preset-env": "^7.4.3",
    "@babel/preset-react": "^7.0.0",
    "autoprefixer": "^9.5.1",
    "babel-loader": "^8.0.5",
    "clean-webpack-plugin": "^2.0.1",
    "css-loader": "^2.1.1",
    "express": "^4.16.4",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-middleware": "^3.6.2",
    "webpack-dev-server": "^3.3.1"
  },
  "dependencies": {
    "@babel/polyfill": "^7.4.3",
    "@babel/runtime": "^7.4.3",
    "@babel/runtime-corejs2": "^7.4.3",
    "core-js": "^3.0.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

但是假如我们引入了import "@babel/polyfill"这样的包,就需要特殊处理了。这个模块实际上并没有导出任何的内容,在它的内部实际上是在window对象上绑定了一些全局变量,比如说Promise(window.promise)这些东西,所以它没有直接导出模块,如果用了tree shaking,发现这个模块没有导出任何内容,就会打包的时候直接把这个@babel/polyfill给忽略掉了,但是我们又是需要这个模块的,所以打包的时候就会出问题了,所以我们需要对这样的模块做一个特殊的设置,如果不希望对@babel/polyfill这样的模块进行tree shaking,我们可以在package.json中这样设置“sideEffects”: ["@babel/polyfill"]

除了@babel/polyfill这样的文件需要特殊处理外,还有我们引入的css文件(如:import './style.css'),实际上只要引入一个模块,tree shaking就会去看这个模块导出了什么,你引入了什么,如果没有用到的打包的时候就会帮你忽略掉,style.css显然没有导出任何内容,如果这样写,tree shaking解析的时候就会把这个样式忽略掉,样式就不会生效了,所以我们还需要这样添加“sideEffects”: ["*.css"],意思是任何的css文件都不要tree shaking

对于上面这段话的实践验证:
development模式下,不管设置"sideEffects": false 还是 "sideEffects": ["*.css"],style.css都不会被tree shaking,页面样式还是会生效,结论就是,开发模式下,对于样式文件tree shaking是不生效的
production模式下,"sideEffects": false页面样式不生效,说明样式文件被tree shaking了;然后设置"sideEffects": ["*.css"]页面样式生效,说明样式文件没有被tree shaking,结论就是,生产模式下,对于样式文件tree shaking是生效的

配置好了之后重新npx webpack,然后打开main.js可以看到minus方法任然被打包进来,那是不是tree shaking没有生效呢?其实它已经生效了,我们往上面看,可以看到这样的一句话
新版webpack4.0指南_第25张图片
它的意思是这个模块提供了两个方法,但是只有一个add方法被使用了,使用了tree shaking的webpack打包的时候已经知道哪些方法被使用了,故作出这样的提示,那为什么没有帮我们把没有用到的代码去除掉呢? 这是因为在development模式下,我们可能需要做一些调试,如果删除掉了,那我们做调试的时候可能就找不到具体位置了,所以开发环境下,tree shaking还会保留这些无用代码

如果是production环境下,我们对webpack.json.js文件进行调整下

module.exports = {
    // mode: 'development', 
    // devtool: 'cheap-module-eval-source-map',  
+   mode: 'production', 
+   devtool: 'cheap-module-source-map',  
   ...
    // optimization: {   //  在production模式下,tree shaking一些配置自动就配置好了,所以这里不需要写了
    //     usedExports: true
    // },
    ...
}

重新npx webapck,打开main.js,因为是线上代码webpack做了压缩,我们搜索console.log可以看到只能搜到一个,说明webpack去掉了minus方法
图片描述

如何处理第三方JS库?
对于经常使用的第三方库(例如 jQuery、lodash 等等),如何实现 Tree Shaking ?
以lodash.js为例,进行介绍
安装lodash.js

npm install lodash --save

index.js

import { add } from './math.js';


+  import { chunk } from 'lodash'
+  console.log(chunk([1, 2, 3], 2))


add(1, 2)

执行npx webpack,如下图所示,打包后大小为77.3kb,显然只引用了一个函数,不应该这么大。并没有进行tree shaking
新版webpack4.0指南_第26张图片
开头讲过,js tree shaking 利用的是 ES 的模块系统。而 lodash.js 没有使用 CommonJS 或者 ES6 的写法。所以,安装对应的模块系统即可

安装 lodash.js 的 ES 写法的版本:

npm install lodash-es --save

修改下index.js

import { add } from './math.js';

- import { chunk } from 'lodash'
+ import { chunk } from 'lodash-es'
console.log(chunk([1, 2, 3], 2))

add(1, 2)

再次npx webpack,只有1.04kb了,显然,tree shaking成功
新版webpack4.0指南_第27张图片

友情提示:
在一些对加载速度敏感的项目中使用第三方库,请注意库的写法是否符合 ES 模板系统规范,以方便 webpack 进行 tree shaking。

(二)、CSS Tree Shaking

在src中新增一个style.css文件
style.css

.box {
  height: 200px;
  width: 200px;
  border-radius: 3px;
  background: green;
}

.box--big {
  height: 300px;
  width: 300px;
  border-radius: 5px;
  background: red;
}

.box-small {
  height: 100px;
  width: 100px;
  border-radius: 2px;
  background: yellow;
}

index.js

   import { add } from './math.js';
+  import './style.css';

-  import { chunk } from 'lodash-es'
-  console.log(chunk([1, 2, 3], 2))

+  var root = document.getElementById('root')
+  var div = document.createElement('div')
+  div.className = 'box'
+  root.appendChild(div)

   add(1, 2)

PurifyCSS 将帮助我们进行 CSS Tree Shaking 操作。为了能准确指明要进行 Tree Shaking 的 CSS 文件,还有 glob-all (另一个第三方库)。 glob-all 的作用就是帮助 PurifyCSS 进行路径处理,定位要做 Tree Shaking 的路径文件。
安装依赖

 npm install glob-all purify-css purifycss-webpack --save-dev

为了配合PurifyCSS 这个插件,我们还需要额外安装一个mini-css-extract-plugin这个插件

npm install --save-dev mini-css-extract-plugin
mini-css-extract-plugin更多参数配置请参考 这里

然后更改配置文件

+  const MiniCssExtractPlugin = require('mini-css-extract-plugin')       // 默认打包后只能插入