12

此项目总共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文件
图片描述
然后我们稍加修改

{
  "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

<!doctype html>
<html>
  <head>
    <title>起步</title>
  </head>
  <body>
    <script src="./src/index.js"></script>
  </body>
</html>

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

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

dist/index.html

  <!doctype html>
  <html>
   <head>
     <title>起步</title>
   </head>
   <body>
-    <script src="./src/index.js"></script>
+    <script src="main.js"></script>
   </body>
  </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

> webpack-demo@1.0.0 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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>起步</title>
</head>
<body>
 +  <div id="root"></div>
    <script src="./bundle.js"></script>
</body>
</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的形式被引进来的
图片描述

这是因为当你去打包一个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语法
图片描述

安装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语法
图片描述

注意: 在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,打包成功之后刷新页面,显示正常,并且图片样式上会自动添加上了厂商前缀
图片描述

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样式
图片描述

这说明我们通过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字体文件的路径改对
图片描述
然后我们在index.js中添加如下代码

var root = document.getElementById('root');
import './index.scss'
root.innerHTML = '<div class="iconfont iconyanxianbi"></div>'

在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元素
图片描述

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
-   <title>html模板</title>
+   <title><%= htmlWebpackPlugin.options.title %></title> 
</head>
<body>
    <div id="root"></div>
</body>
</html>

然后在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就看不到了
图片描述

详情见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明确告诉我们是哪一个文件的哪一行出错,怎么做呢?
图片描述

现在我们对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还是无法指向源文件图片描述
但是在火狐是可以指向源文件的图片描述

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

此外我们devtool还可以配置inline-source-map,重新打包,刷新页面,可以看到在谷歌中它可以指向源文件
图片描述

但是我们在dist目录中发现,此时并没有main.js.map文件了,其实当我们用inline-source-map时,这个map文件会通过dataUrl的形式直接写在main.js里面
图片描述

此外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,可以看到它帮我们生成了一个服务器地址
图片描述
手动打开这个地址,在控制台看到内容正常打印出来了,如果现在修改和保存任意源文件,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了
图片描述

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这样智能,每次更改源文件之后都需要手动刷新页面才能看到内容的变化
图片描述

几点区别:
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语法了
图片描述

(一)、@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
图片描述
引进了之后我们重新npx webpack,打包之后看main.js一下就变成了1.04Mb了
图片描述
这多的内容就是@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'   // 根据业务代码决定补充什么内容
+                    }]]
          } 
      },

打包发现报错了
图片描述

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

npm install --save core-js@3.0.1

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

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

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

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

重新npx webpack,发现打包出的main.js体积小了不少
图片描述

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了
图片描述
打开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 <div>Hello World</div>
    }
}

ReactDom.render(<App />, 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方法也打包进来
图片描述
我们的业务代码中实际上只用到了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没有生效呢?其实它已经生效了,我们往上面看,可以看到这样的一句话
图片描述
它的意思是这个模块提供了两个方法,但是只有一个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
图片描述
开头讲过,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成功
图片描述

友情提示:
在一些对加载速度敏感的项目中使用第三方库,请注意库的写法是否符合 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')       // 默认打包后只能插入<style>标签内,这个插件可以将css单独打包成文件,以<link>形式引入
+  const PurifyCSS = require('purifycss-webpack');
+  const glob = require('glob-all');

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new webpack.HotModuleReplacementPlugin(),  
+       new MiniCssExtractPlugin({
+           filename: '[name].css'                         // 打包后的css文件名
+       }),     
+       new PurifyCSS({
+           paths: glob.sync([
               // 要做CSS TreeShaking的文件
+               path.resolve(__dirname, './src/*.js')
+           ])
+       })
    ]
}

打包完之后,检查dist/main.css可以看到没有被使用的类样式(box-big和box-small)就没有被打包进去
图片描述

警告
如果项目中有引入第三方 css 库的话,谨慎使用

Tree Shaking部分内容引用这里

此时项目结构为:

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

十六、Development和Production模式的区分打包

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

现在做一些回退处理,删除src中的style.css,并且删除index.js中的部分内容,然后对配置文件进行调整
index.js

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

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

    add(1, 2)

webpack.config.js

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    const webpack = require('webpack');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')   // 这个插件可以保留
-   const PurifyCSS = require('purifycss-webpack');
-   const glob = require('glob-all');

module.exports = {
    mode: 'development', 
    devtool: 'cheap-module-eval-source-map',  
    // mode: 'production', 
    // devtool: 'cheap-module-source-map',  
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        ...      
    },
    module: {
        ...
    },
    optimization: {
         usedExports: true
    },
    plugins: [
        ... 
        new MiniCssExtractPlugin({              // 这个插件保留
            filename: '[name].css'                       
        }),     
-       new PurifyCSS({
-           paths: glob.sync([
-               path.resolve(__dirname, './src/*.js')
-           ])
-       })
    ]
}

回退处理完成之后,我们将webpack.config.js重命名为webpack.dev.js,让它作为开发环境下的配置文件,然后再在同级目录新建一个webpack.prod.js文件,让其作为生产环境下的配置文件,然后将webpack.dev.js文件中的全部内容拷贝一份webpack.prod.js中,并做一些删减
webpack.prod.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')   

module.exports = {
 -   mode: 'development', 
 -   devtool: 'cheap-module-eval-source-map',  
     mode: 'production', 
     devtool: 'cheap-module-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,             
-       hotOnly: true        
-   },
    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: [
                    MiniCssExtractPlugin.loader,
                    // '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(),  
        new MiniCssExtractPlugin({
            filename: '[name].css'                       
        })
    ]
}

接下来,我们打开package.json文件,做一些调整

{
  "name": "webpack-demo",
  "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
-    "start": "webpack-dev-server"
+    "dev": "webpack-dev-server --config webpack.dev.js",
+    "build": "webpack --config webpack.prod.js" 
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}

意思是如果执行npm run dev命令,那么就运行webpack.dev.js这个配置文件,如果执行npm run build命令,那就运行webpack.prod.js这个配置文件

现在验证下,运行npm run dev,打包成功webpack帮我们打开一个localhost:8080这个地址,查看控制台输出3,然后我们把src/index.js中的add(1, 2)改为add(1, 4),保存,返回浏览器收到刷新页面,然后查看控制台,发现打印出了5,如果我们不想手动刷新可以在webpack.dev.js中将hotOnly:true删掉,然后重新npm run dev下

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

如果我们代码需要打包上线了,我们需要在命令行运行npm run build,打包完成之后,在dist目录中我们可以看到main.js已经是压缩过的文件了,我们把这个文件夹丢到线上给后端使用就可以了

但是我们发现,这两个文件中还存在很多重复的代码,我们需要继续优化下,新建一个通用配置,为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。通过“通用”配置,我们不必在环境特定(environment-specific)的配置中重复代码。

安装webpack-merge

npm install --save-dev webpack-merge

然后在同级目录新建一个webpack.common.js的配置文件,然后把那两个文件中公用的代码都提取到这个文件中来
webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')   

module.exports = { 
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, '../dist'),
    },
    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: [
                    MiniCssExtractPlugin.loader,
                    // '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 MiniCssExtractPlugin({
            filename: '[name].css'                       
        })
    ]
}

然后再在这两个文件中通过webpack-merge这个插件对通用配置进行合并
webpack.dev.js

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

const devConfig = {
    mode: 'development', 
    devtool: 'cheap-module-eval-source-map',  
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true          
    },
    optimization: {
        usedExports: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
}

module.exports = merge(commonConfig, devConfig);

webpack.prod.js

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

const prodConfig = {
    mode: 'production', 
    devtool: 'cheap-module-source-map'
}

module.exports = merge(commonConfig, prodConfig);

然后执行npm run dev,打包没有问题,再去执行npm run build,打开dist目录下的index.html文件,也没有问题,说明我们合并成功

为了文件目录简洁,我们在webpack-demo目录下新建一个build文件夹,然后把这三个文件移到这个文件夹中,然后重新修改下packgae.json中的路径

{
  "name": "webpack-demo",
  "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
-    "dev": "webpack-dev-server --config webpack.dev.js",
-    "build": "webpack --config webpack.prod.js"
+    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
+    "build": "webpack --config ./build/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}

重新验证,都是没问题的
此时项目结构如下:

webpack-demo
        |- build
          |- webpack.common.js
          |- webpack.dev.js
          |- webpack.prod.js
        |- dist
          |- index.html
          |- main.js
          |- main.js.map
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
          |- math.js
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js

十七、Code Splitting

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间

(一)、同步代码code splitting

为了能检查打包后的文件内容,我们在package.json中新增一个命令

{
  "name": "webpack-demo",
  "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
+   "dev-build": "webpack --config ./build/webpack.dev.js",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}

然后我们做一些回退处理,现在删除掉src目录下的math.js文件并且去掉index.js文件中的内容,然后去下载一个第三方库lodash

npm install lodash --save

在index.js中添加如下代码
index.js

import _ from 'lodash';

console.log(_.join(['a', 'b', 'c']));

执行命令npm run dev-build,打包成功之后打开dist/index.html文件,检查控制台输出正常
图片描述

假设index.js中的业务逻辑非常长

import _ from 'lodash';

console.log(_.join(['a', 'b', 'c'], '***'));
// 此处省略几千行业务逻辑...
console.log(_.join(['a', 'b', 'c'], '***'));

我们引入了一个工具库(假设有1MB),同时下面又有几千行的业务逻辑(假设也有1MB), webpack打包的时候会统一打包到main.js(现在有2MB),这样势必造成打包后的文件非常大,加载时间会很长,这样还会带来另外一个问题,如果我们修改了我们的业务逻辑,然后重新打包。打包出一个新的2MB的main.js,浏览器每次打开页面,都要先加载 2M 的文件,才能显示业务逻辑,这样会使得加载时间变长,那有没有办法去解决这个问题呢?

在src中新建一个lodash.js

import _ from 'lodash';

// 把lodash挂载到全局window上面
window._ = _;

index.js

- import _ from 'lodash';

  console.log(_.join(['a', 'b', 'c'], '***'));
  // 此处省略几千行业务逻辑...
  console.log(_.join(['a', 'b', 'c'], '***'));

webpack.common.js

module.exports = { 
    entry: {
 +       lodash: './src/lodash.js',
         main: './src/index.js'
    },                        
    ...
}

重新运行npm run dev-build,在dist目录中可以看到打包拆分出来的两个js文件(分别是1MB),然后打开dist中的index.html,现在浏览器就可以并行加载这两个文件了,这样比一次性加载2MB的main.js性能会好点,另外这样打包还有一个好处,就是假如我们的业务逻辑做了变更,现在只需要重新加载main.js就好了,而lodash.js基本不会变更,直接从浏览器缓存中取,这样可以提升加载速度
图片描述

上面是我们自己做的代码分割,其实webpack通过splitChunksPlugins就可以帮我们做code splitting

在 webpack4 之前是使用 commonsChunkPlugin 来拆分公共代码,v4 之后被废弃,并使用 splitChunksPlugin,在使用 splitChunksPlugin 之前,首先要知道 splitChunksPlugin 是 webpack 主模块中的一个细分模块,无需 npm 引入

我们删除掉src/lodash.js文件,然后把index.js还原

import _ from 'lodash';

console.log(_.join(['a', 'b', 'c'], '***'));

然后点开webpack.common.js,加上如下代码

module.exports = { 
    entry: {
 -      lodash: './src/lodash.js',
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, '../dist'),
    },
    ...
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new MiniCssExtractPlugin({
            filename: '[name].css'                       
        })
    ],
+   optimization: {
+       splitChunks: {
+           chunks: 'all'   // 分割所有代码包括同步代码和异步代码,默认chunks:'async'分割异步代码
+       }
+   }
}

重新npm run dev-build,打包成功之后,可以看到dist中生成了两个js文件
图片描述
点开这个vendors~main.js,在最上面可以看到它把lodash这个工具库单独提取出来了,以前我们是自己手动提取,现在我们通过webapck一个简单的配置,它会自动帮我们去做代码分割

(一)、异步代码code splitting

在index.js中我们不仅可以做同步模块的引入还可以做异步模块的引入
index.js

function getComponent() {
    // 异步加载lodash
    return import('lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}


getComponent().then(ele => {
    document.body.appendChild(ele);
})

重新npm run dev-build,发现打包报错了
图片描述

dynamicImport 还是实验性的语法,webpack 不支持,我们需要借助babel的插件进行转换

安装

npm install babel-plugin-dynamic-import-webpack -D
关于这个插件可以参考这里

安装完成之后再在babelrc文件中进行配置即可

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

重新npm run dev-build,可以看到打包成功了,打开index.html文件显示正常

这里分割出0.js,0是以id编号来命名
图片描述
点开这个0.js,在最上面依然可以看到它把lodash这个工具库单独提取出来了

十八、SplitChunksPlugin配置参数详解

上一节最后,我们可以看到打包出来的是0.js,那我们可不可以对这个名字重命名呢?

在这种异步加载的代码中我们有一种语法,叫做“魔法注释”,请看下面具体写法

function getComponent() {
    // 异步加载lodash
    // 意思是异步引入lodash,当做代码分割的时候,给这个lodash库单独进行打包的时候,给它起的名字叫lodash
   return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}

getComponent().then(ele => {
    document.body.appendChild(ele);
})

重新打包npm run dev-build,发现打包出来的还是0.js,这是为什么呢?这是因为我们之前配置的这个插件"plugins": ["dynamic-import-webpack"],并不是官方的插件,它不支持这种“魔法注释”的写法,还现在该怎么呢?

最简单的就是不使用这个插件了,取而代之我们去使用babel官方提供的另一个插件
安装

npm install --save-dev @babel/plugin-syntax-dynamic-import
插件详情请见官网

安装好之后,对babelrc文件进行调整

{
    "presets": [
        ["@babel/preset-env", {
            "targets": {
                "chrome": "67"
                },
            "useBuiltIns": "usage",
            "corejs": 3
            }
        ],
        "@babel/preset-react"
    ],
-   "plugins": ["dynamic-import-webpack"]
+   "plugins": ["@babel/plugin-syntax-dynamic-import"]
} 

重新npm run dev-build,打包成功之后到dist目录中看到名字已经变成了vendors~lodash.js了
图片描述

如果想让打包出来的文件就叫lodash,我们需要在webpack.common.js中改变下配置

 optimization: {
        splitChunks: {
            chunks: 'all',
 +          cacheGroups: {
 +              vendors: false,
 +              default: false
 +          }
        }
    }

重新打包,可以看到打包生成的文件就叫lodash了
图片描述

现在我们将optimization.splitChunks配成一个空对象

 optimization: {
        splitChunks: {
           
        }
    }

然后保存重新打包,可以看到打包依然可以成功运行,只不过lodash名字前面多了一个vendors
图片描述

这是因为如果没有配置任何内容的时候,它会走它内部默认的一套配置流程,具体默认配置参数见下

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

接下来,我们将这些默认配置项配置进webpack.common.js中的optimization.splitChunks中
重新npm run dev-build,打包依然正常,接下来我们一项项解释这些参数有什么作用
首先,我们将cacheGroups.vendors和cacheGroups.default都配置成false

optimization: {
        splitChunks: {
            chunks: 'async',
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors: false,
              default: false
            }
        }
    }

(1)、splitChunks.chunks

默认是‘async’,意思是做代码分割的时候只对异步代码生效。当是字符串时,有效值还可以设置为all和initial

我们把src/index.js中的异步代码先注释,然后同步引入lodash
index.js

// function getComponent() {
//     // 异步加载lodash
//     // 意思是异步引入lodash,当做代码分割的时候,给这个lodash库单独进行打包的时候,给它起的名字叫lodash
//     return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
//         var ele = document.createElement('div');
//         ele.innerHTML = _.join(['Hello', 'World'], '-');
//         return ele;
//     })
// }

// getComponent().then(ele => {
//     document.body.appendChild(ele);
// })

// 同步引入lodash
import _ from 'lodash';
var ele = document.createElement('div');
ele.innerHTML = _.join(['Hello', 'World'], '-');
document.body.appendChild(ele);

重新npm run dev-build,打包完成可以看到dist目录中并没有打包出lodash.js文件,说明对同步代码没有分割成功
图片描述

当把chunks设置为all的时候,意思是对同步代码和异步代码都可以做代码分割,我们看是不是这样的

optimization: {
        splitChunks: {
            chunks: 'all',
            ...
            cacheGroups: {
              vendors: false,
              default: false
            }
        }
    }

改好之后重新打包,打包完成我们发现dist目录中还是没有生成lodash.js,说明代码分割没有生效,这是为什么呢?是因为我们还需要对cacheGroups做一些额外配置

optimization: {
    splitChunks: {
      chunks: 'all',
      ...
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: false
      }
    }
  }

当引入同步的lodash库的时候,设置为all后webpack知道要对同步代码做分割了,但是它会继续往下走,走到cacheGroups的时候,它里面还有个vendors.test配置项,这个配置项会去检测你引入的这个库是不是在node_modules中,很显然我们引入的这个库是通过npm安装的,肯定在node_modules中,那它就符合test这个配置项的要求,于是它会单独把这个lodash打包到vendors这个组里面去
重新再去npm run dev-build,在dist目录中可以看到它此时已经生成了一个vendors~main.js文件了
图片描述
文件前面的vendors指的就是这个库文件符合这个组的要求,所以生成的文件会加上这个组的名字
文件后面的main指的就是入口文件的名字

有的时候我们希望打包出来的文件名不要加上main这个入口名字了,直接把所有引入的库打包到vendors.js这个文件里面,我们可以对vendors这样配置

cacheGroups: {
      vendors:  {
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
 +          filename: 'vendors.js'
         },
      default: false
}

保存重新打包,现在可以看到名字就叫vendors.js了
图片描述

当我们把chunks设置成async或all的时候,让它去处理异步代码并且异步代码中不通过魔法注释去自定义名字

function getComponent() {
    return import('lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}
getComponent().then(ele => {
    document.body.appendChild(ele);
})

// 同步引入lodash
// import _ from 'lodash';
// var ele = document.createElement('div');
// ele.innerHTML = _.join(['Hello', 'World'], '-');
// document.body.appendChild(ele);

而是在vendors.filename中配置自定义名字,发现打包都会报同样的错
图片描述

从上面这些例子中得出结论:
1、 chunks不管设置成什么,webpack做代码分割的时候,都会去匹配cacheGroup这个配置项
2、 chunks设置成async或者all的时候,去处理异步代码,如果想自定义打包后的名字只能通过魔法注释,如果想让打包出来的名字不带vendors,可以把venders设置成false,意思是不让webpack去配置cacheGroup.vendors这个配置项
3、 chunks设置成all的时候,去处理同步代码,必须要给vendors设置配置项,不能是false,否则无法打包出文件,如果还想自定义打包后的名字只能通过vendors.filename来配置

(2)、splitChunks.minSize

意思是超过多少大小就进行压缩,默认是30000即30kb

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 300000000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: false
            }
        }
    }

如果我们把minSize改到非常大,lodash这个库大小肯定是小于这个值的,重新npm run dev-build,发现dist目录中并没有帮我们把lodash分割出来,
图片描述

我们重新再举一个例子,现在我们在src中新建一个test.js
test.js

export default {
    name: 'Hello world'
}

index.js

// function getComponent() {
//     return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
//         var ele = document.createElement('div');
//         ele.innerHTML = _.join(['Hello', 'World'], '-');
//         return ele;
//     })
// }

// getComponent().then(ele => {
//     document.body.appendChild(ele);
// })


// 同步引入lodash
// import _ from 'lodash';
// var ele = document.createElement('div');
// ele.innerHTML = _.join(['Hello', 'World'], '-');
// document.body.appendChild(ele);

import test from './test.js';
console.log(test.name)

然后重新把minSize改回为默认值30000,我们自己写的这个模块是非常小的,估计连1kb都不到,它打包的时候是不会进行代码分割的,我们验证下,重新npm run build,可以看到dist目录中果然没有帮我们做分割
图片描述
那如果我们把minSize改为0

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 0,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: false
            }
        }
    }

我们写的这个模块大小大于0,照理说应该会进行代码分割,我们看是不是这样的?
重新npm run dev-build,我们在dist目录中仍然还是没有看到分割出来的代码,这是为什么呢?
原因是当我们引入这个test模块的时候,它已经符合这个mixSize大于0的要求了,webpack已经知道要对它进行代码分割,但是它会继续往下走,走到cacheGroups的时候,会去匹配vendors中的test规则,发现这个test模块并不在node_modules中,既然无法匹配这个规则所以打包后的文件不会放到vendors.js中去,要放到哪里去webpack自己就不知道而此时default我们又配置的是false,它连默认放到哪里都不知道

现在我们对default这个配置项重新配置下

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 0,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true
              }
            }
        }
    }

保存重新打包,打包之后可以看到它会把这个模块放到以default这个组名字开头的文件里
图片描述
我们也可以在default里自定义打包后的名字

default: {
    // minChunks: 2,
    priority: -20,
    reuseExistingChunk: true,
+   filename: 'common.js'
}

重新打包,这个test模块就会被打包进default这个组里面对应的common.js文件里面
图片描述

(3)、splitChunks.maxSize

使用maxSize告诉webpack尝试将大于maxSize的块拆分成更小的部分。拆解后的文件最小值为minSize,或接近minSize的值。这样做的目的是避免单个文件过大,增加请求数量,达到减少下载时间的目的,但是一般这个值我们不会配置

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
-           maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        }
    }

(4)、splitChunks.minChunks

指的是当一个模块被用了多少次的时候,才对它进行代码分割

现在我们把这个值改为2,但是我们在index.js中只引用了一次,所以按理是不会进行代码分割的,我们验证下
index.js

// function getComponent() {
//     return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
//         var ele = document.createElement('div');
//         ele.innerHTML = _.join(['Hello', 'World'], '-');
//         return ele;
//     })
// }

// getComponent().then(ele => {
//     document.body.appendChild(ele);
// })


// 同步引入lodash
import _ from 'lodash';
var ele = document.createElement('div');
ele.innerHTML = _.join(['Hello', 'World'], '-');
document.body.appendChild(ele);

// import test from './test.js';
// console.log(test.name)

重新npm run dev-build,打包完成查看dist目录,果然就没有帮我们做代码分割了
图片描述

(5)、splitChunks.maxAsyncRequests

默认是5,指的是同时加载的模块数最大是5个

(6)、splitChunks.maxInitialRequests

指入口文件的最大并行请求数,意思是入口文件引入的库如果做代码分割也最多只能分割出3个js文件,超过3个就不会做代码分割了,这些配置一般按照默认配置来即可

(7)、splitChunks.automaticNameDelimiter

意思是打包生成后的文件中间使用什么连接符

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '+',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        }
    }

重新打包,可以看到文件中间的~就变成了+了
图片描述

验证完之后将automaticNameDelimiter: '+'重新改为automaticNameDelimiter: '~'

(7)、splitChunks.name

配置true,意思是将根据块和缓存组密钥自动生成名称,一般采用默认值

(8)、splitChunks.cacheGroups

我们会根据cacheGroups来决定要分割出来的代码到底放到哪个文件里

假如我们同时引入了一个lodash和一个jquery,如果没有这个cacheGroups,那么代码打包的时候会发现lodash大于30kb要做代码分割,会生成一个lodash这样的文件,然后jquery也大于30kb也要做代码分割,会生成一个jquery这样的文件,如果我们要把这两个文件放到一起单独生成一个vendors.js文件,没有cacheGroups就做不到了,它相当于一个缓存组,打包jquery的时候,先把这个文件放到组里缓存着,打包lodash的时候发现lodash也符合这个组的要求,也缓存到这个组里,当所有的模块都分析好之后,然后把所有符合这个组的模块打包到一起去

假设我们引入jquery这个第三方模块,它符合vendors这个组的要求,但是它也符合default这个组的要求,那到底webpack做分割的时候到底是放到vendors这个组里还是放到default这个组里呢?实际上它是通过priority这个优先级来判断的,谁的优先级高就放到谁的组里

假设有a、b两个模块,b模块之前在某个地方已经被引用过了,而且在之前的逻辑中已经打包好了,而a模块又引用了b模块,配置reuseExistingChunk为true,再去打包a的时候,就不会去打包a里面引用的b模块了,a里面用到b就直接去复用之前打包好放到某个地方的b模块,所以这个参数的意思是如果一个模块已经被打包过了,如果再打包的时候就忽略这个模块,直接使用之前被打包好的那个

关于这个插件的参数说明可以参考官网

自此SplitChunksPlugin中的几个基本参数已经讲解完毕,现在对webpack中的三个概念module、chunk和bundle做一下总结

  • module :就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码。
  • chunk :chunk是webpack根据功能拆分出来的,包含三种情况:
    1、你的项目入口(entry)
    2、通过import()动态引入的代码
    3、通过splitChunks拆分出来的代码

chunk包含着module,可能是一对多也可能是一对一。

  • bundle :bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出。
对SplitChunksPlugin参数更多细致的理解可以参考这篇博客

十九、Lazy Loading

懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

首先我们做一些回退处理,删除src中的test.js文件,此时项目结构为

webpack-demo
        |- build
          |- webpack.common.js
          |- webpack.dev.js
          |- webpack.prod.js
        |- dist
          |- index.html
          |- main.js
          |- vendors.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js

然后,我们对index.js中的代码做一些改进
index.js

function getComponent() {
    return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}

document.addEventListener('click', () => {
    getComponent().then(ele => {
        document.body.appendChild(ele);
    })
})

再到webpack.common.js中把vendors.filename注释掉
webpack.common.js

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
               // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        }
    }

重新npm run dev-build,打包好后,打开dist目录下的index.html文件,打开Network,可以看到刚进页面的时候,只加载了index.html和main.js文件,而vendors~main.js文件并没有加载出来
图片描述

而只有我们点了页面中的某个位置之后,这个文件才被加载出来了
图片描述

所以通过这种异步import的方式,可以让lodash在被需要的时候才加载出来,这就是懒加载的概念。

懒加载实际上并不是webpack里面的一个概念,而是ES里面提出的这样一个实验性质的语法,它和webpack本质上关系不大,webpack只不过是能够识别出这种import语法,然后对它引入的代码模块进行代码分割而已

ES6里面引入了async...await的语法,通过这种语法,我们可以对index.js中的代码继续精简下
index.js

async function getComponent() {
    const { default: _ } = await import(/*webpackChunkName:"lodash" */'lodash');
    const ele = document.createElement('div');
    ele.innerHTML = _.join(['Hello', 'World'], '-');
    return ele;
}

document.addEventListener('click', () => {
    getComponent().then(ele => {
        document.body.appendChild(ele);
    })
})

重新打包,然后打开dist/index.html文件,点击页面某个地方,可以看到页面显示正常,说明这样写也是没问题的

关于懒加载的其他知识点可以参考官网

二十、打包分析,preloading和prefetching

(一)、打包分析

首先,我们进入webpack提供打包分析的一个官方网站

复制下面这段代码
图片描述

进入package.json里面,然后把这段代码加入到这个地方
图片描述
意思是在打包的过程中,把整个打包过程中的描述放置到名字叫stats.json的文件中,文件的格式是json

重新npm run dev-build,打包成功之后,可以看到目录中已经生成了stats.json的文件
图片描述

然后我们在官方网站中点开这个http://webpack.github.com/ana...,点击选择文件,将我们刚刚生成的stats.json文件传上去,它会帮我们进行打包代码的分析
图片描述

除了官方提供的工具外,还有很多其他工具供我们使用:

  • webpack-chart: webpack 数据交互饼图。
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户。

(二)、preloading和prefetching

webpack 4.6.0+增加了对prefetching和preloading的支持。

在src/index.js中我们做一些调整

// async function getComponent() {
//     const { default: _ } = await import(/*webpackChunkName:"lodash" */'lodash');
//     const ele = document.createElement('div');
//     ele.innerHTML = _.join(['Hello', 'World'], '-');
//     return ele;
// }

document.addEventListener('click', () => {
    // getComponent().then(ele => {
    //     document.body.appendChild(ele);
    // })
    const ele = document.createElement('div');
    ele.innerHTML = _.join(['Hello', 'World'], '-');
    document.body.appendChild(ele);
})

重新打包npm run build,然后打开dist/index.html,点击页面可以看到打印出‘Hello World’字样,说明我们代码写的没有问题,但是我们这段代码是不是完全没有优化的空间了呢?
我们打开页面的控制台,然后Ctrl+Shift+p,输入Coverage,选择第一个
图片描述

然后点击左侧的录制按钮,再刷新页面
图片描述

可以看到代码使用率是74.7%,点击main.js,可以看到这段代码在我们点击页面前,是没有被利用的,只有点击了页面这段代码才会被执行
图片描述

刚开始加载main.js的时候,这段代码不会执行,不会执行的代码让页面加载的时候就去下载它,实际上会消耗页面加载的性能,webpack希望像这种交互的代码应该把它放到异步加载的模块当中去

我们在src中新建一个click.js的文件
click.js

function handleClick() {
    const ele = document.createElement('div');
    ele.innerHTML = 'Hello World';
    document.body.appendChild(ele);
}

export default handleClick;

index.js

document.addEventListener('click', () => {
   import('./click.js').then(({default: func}) => {
      func();
   })
})

重新npm run build,刷新页面,此时代码使用率就变成了80.2%了,因为我们现在是通过异步的方式引入导致代码变少了
图片描述

然后切换到Network,可以看到此时页面只加载了一个index.html和main.js文件
图片描述

点击页面,可以看到1.js被加载出来了
图片描述
点开1.js,可以看到里面正好是创建div标签,然后往页面挂载这个dom节点的逻辑
图片描述

所以这样去写代码才是让页面加载速度最快的一种正确方式,写高性能前端代码的时候,不光要考虑缓存,还要考虑代码使用率 ,所以 webpack 在打包过程中,是希望我们多写这种异步的代码,才能提升网站性能,这也是webpack默认它的splitChunks.chunks配置项是async的原因了

当然,这也会出现另一个问题,就是当用户点击的时候,才去加载业务模块,如果业务模块比较大的时候,用户点击后并没有立马看到效果,而是要等待几秒,这样体验上也不好,怎么去解决这种问题 ?
如果访问首页的时候,不需要加载详情页的逻辑,等用户首页加载完了以后,页面展示出来了,页面的带宽被释放出来了,网络空闲了,再「偷偷」的去加载详情页的内容,而不是等用户去点击的时候再去加载 这个解决方案就是依赖 webpack 的 Prefetching/Preloading 特性

修改index.js


document.addEventListener('click', () => {
   import(/* webpackPrefetch: true */'./click.js').then(({default: func}) => {
      func();
   })
})

webpackPrefetch: true 会等你主要的 JS 都加载完了之后,网络带宽空闲的时候,它就会预先帮你加载好

保存重新npm run build,然后打开dist/index.html打开控制台中的Network,可以看到我们并没有点击页面,等主要的js文件加载好了之后,页面会偷偷的再去加载好1.js文件
图片描述

然后我们再去点击页面,看到1.js又被加载一遍,但是注意到这次加载的1.js响应速度就不是4ms了,而是1ms,这是因为第一次加载好之后浏览器就缓存起来了,第二次再去加载的时候就直接拿缓存了
图片描述

这里我们使用的是 webpackPrefetch,还有一种是 webpackPreload

区别: Prefetch 会等待核心代码加载完之后,有空闲之后再去加载。Preload 会和核心的代码并行加载,还是不推荐

总结
针对优化,不仅仅是局限于缓存,缓存能带来的代码性能提升是非常有限的,而是如何让代码的使用率最高,有一些交互后才用的代码,可以写到异步组件里面去,通过懒加载的形式,去把代码逻辑加载进来,这样会使得页面访问速度变的更快,如果觉得懒加载会影响用户体验,可以使用 Prefetch 这种方式来预加载

想了解更多这节知识点可以参考官方网站

二十一、CSS文件的代码分割

(一)、filename和chunkFilename的区别

我们在webpack的配置中可能还会经常看见一个这样的配置

module.exports = {
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js', 
+       chunkFilename: '[name].chunk.js',                      
        path: path.resolve(__dirname, '../dist'),
    },
    ...
}

那filename和chunkFilename有什么区别呢?

现在做一些回退处理,删除src/click.js文件,并删除index.js里面的内容,将之前的异步代码加入里面
index.js

async function getComponent() {
   const { default: _ } = await import(/*webpackChunkName:"lodash" */'lodash');
   const ele = document.createElement('div');
   ele.innerHTML = _.join(['Hello', 'World'], '-');
   return ele;
}

document.addEventListener('click', () => {
   getComponent().then(ele => {
       document.body.appendChild(ele);
   })
})

重新npm run dev-build,可以看到dist目录中打包生成的文件名如下,为什么会有一个vendors~lodash.chunk.js文件呢
图片描述
这是因为在entry中我们配置的index.js文件是一个入口文件,入口文件打包生成的文件都会走output中的filename这个配置项,所以index.js在做打包的时候它前面的key值是main,所以最终生成的就是main.js文件;main.js文件中会引入lodash,main.js在打包过程中先执行,然后异步的去加载这个lodash,所以这个lodash并不是一个入口的js文件,而是一个间接异步加载的js文件,打包这样的文件就会走chunkFilename这个配置项

(二)、mini-css-extract-plugin对CSS文件做抽离

接下来,我们介绍如何进行css的代码分割,我们需要借助webpack官网提供的一个插件MiniCssExtractPlugin,此插件将CSS提取到单独的文件中。它为每个包含CSS的JS文件创建一个CSS文件。它支持CSS和SourceMaps的按需加载。

它建立在新的webpack v4功能(模块类型)之上,并且需要webpack 4才能工作,之前的版本一直用的都是extract-text-webpack-plugin。

与extract-text-webpack-plugin相比:

  • 异步加载
  • 没有重复的编译(性能)
  • 更容易使用
  • 特定于CSS

    安装

npm install --save-dev mini-css-extract-plugin

然后我们在webpack.common.js中进行配置,因为我们之前在CSS Tree Shaking中已经对css文件配置过MiniCssExtractPlugin,现在我们对scss文件也配置下
webpack.common.js

 {
          test: /\.scss$/,
          use: [
  -           'style-loader',
  +            MiniCssExtractPlugin.loader,   
               {
                 loader: 'css-loader',
                 options: {
                    importLoaders: 2
                 }
               },
               'postcss-loader',
               'sass-loader'
        ]
   },

配置好之后,在src中新建一个style.css文件
style.css

body {
    background-color: blue;
}

index.js

import './style.css';

npm run dev-build运行下,可以看到style.css文件已经被抽离成main.css文件了,然后打开dist目录下的index.html文件,可以看到页面变成了蓝色

MiniCssExtractPlugin参数中除了可以配置filename还可以配置chunkFilename
webpack.common.js

 new MiniCssExtractPlugin({
            filename: '[name].css',
 +          chunkFilename: '[name].chunk.css'                       
        })

重新npm run dev-build,在dist目录中可以看到生成的还是main.css文件,为什么不是main.chunk.css文件呢?这是因为我们css文件是同步引入的,如果是异步引入就走chunkFilename逻辑,具体可以参见这里的回答
图片描述

在src目录下,我们在去创建一个style1.css文件
style1.css

body {
    font-size: 100px;
}

index.js

  import './style.css';
+ import './style1.css';

重新npm run dev-build,点开dist目录下的main.css文件,可以看到这个插件还会把引入的css文件合并到一起
图片描述

(三)、optimize-css-assets-webpack-plugin对CSS文件做压缩

另外我们还希望打包好的css文件可以做压缩,这里我们需要使用另外一个插件optimize-css-assets-webpack-plugin

安装

npm install --save-dev optimize-css-assets-webpack-plugin
注意
对于webpack v3或更低版本,请使用optimize-css-assets-webpack-plugin@3.2.0。该optimize-css-assets-webpack-plugin@4.0.0版本及以上版本支持webpack v4。

然后在webpack.common.js中去引入
webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')   
+  const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = { 
    ...
    optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        },
+        minimizer: [new OptimizeCSSAssetsPlugin({})]
    }
}

保存重新npm run dev-build,打包完成之后,发现main.css文件并没有被压缩,这里找了很久的原因,终于发现是我们打包命令执行错了,npm run dev-build最终执行的是webpack.dev.js文件,OptimizeCSSAssetsPlugin的压缩功能似乎在开发环境下不起作用;所以我们重新用npm run build,执行打包,打包完之后发现连css文件都看不到了,这是因为我们在前面Tree Shaking中提到过,生产模式下,tree shaking把样式文件忽略了,所以我们还需要在package.json中重新配置下
package.json

{
   + "sideEffects": ["*.css"],
}

配置好后,重新npm run build,再打开dist/main.css就可以看到压缩成功了
图片描述

注意:webpack:production模式默认有配有js压缩,如果这里设置了css压缩,js压缩也要重新设置,因为使用minimizer会自动取消webpack的默认配置,因此此请务必同时指定JS minimalizer

所以我们还需要使用JS压缩的插件terser-webpack-plugin

安装

npm install terser-webpack-plugin --save-dev

安装完之后,再在webpack.common.js中进行配置
webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')   
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+  const TerserJSPlugin = require('terser-webpack-plugin');

module.exports = { 
...
    plugins: [
        ...
        minimizer: [
+           new TerserJSPlugin({}), 
            new OptimizeCSSAssetsPlugin({})
        ]
    }
}

重新npm run build,可以看到main.js已经被压缩了

假如我们在entry中有多个入口,每个入口文件中都引入了css文件
webpack.common.js

 entry: {
        main: './src/index.js',
+       sub: './src/index1.js'
    },  

然后再在src目录下新建一个index1.js和style2.css
index1.js

import './style2.css'

style2.css

body {
    width: 200px;
    height: 200px;
    border: 1px solid yellow;
}

保存重新npm run build,可以看到dist目录中分别生成了一个mian.css和sub.css文件
图片描述

如果想让这些css文件合并成一个css文件怎么办呢?我们可以借助splitChunks.cacheGroups这个功能来实现
,它可以在单个文件中提取所有CSS

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              },
+             styles: {
+               name: 'styles',    
+               test: /\.css$/,
+               chunks: 'all',
+               enforce: true,     //  意思是忽略掉默认的一些参数,只要是css文件就做代码的拆分
+             },
            }
        },
        minimizer: [
            new TerserJSPlugin({}), 
            new OptimizeCSSAssetsPlugin({})
        ]
    }

保存重新npm run build,现在就可以看到只生成了一个CSS文件
图片描述

关于插件mini-css-extract-plugin的其他配置项可以参考官方网站

此时项目结构为:

webpack-demo
        |- build
          |- webpack.common.js
          |- webpack.dev.js
          |- webpack.prod.js
        |- dist
          |- index.html
          |- main.js
          |- styles.chunk.css
          |- styles.chunk.js
          |- sub.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
          |- index1.js
          |- style.css
          |- style1.css
          |- style2.css
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- stats.json

二十二、webpack与浏览器缓存(Caching)

我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到服务器上,客户端(通常是浏览器)就能够访问网站此服务器的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。

这一节通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

先做一些回退处理
webpack.common.js

module.exports = { 
    entry: {
        main: './src/index.js',
-       sub: './src/index1.js'
    }, 
    ...                       
    optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              },
   -          styles: {
   -            name: 'styles',
   -            test: /\.css$/,
   -            chunks: 'all',
   -            enforce: true,
   -          },
            }
        },
    }
}

然后删除src下面的index1.js,style.css,style1.css,style2.css,并清空index.js里面的内容
index.js

import _ from 'lodash';
import $ from 'jquery';

const dom = $('<div>');
dom.html(_.join(['hello world']), ' ');
$('body').append(dom);

因为lodash和jquery是同步引入的,所以我们可以在vendors.filename中自定义下名字

splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
+               filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        },

重新npm run build,打开index.html页面正常显示,页面在第一次打开的时候会去请求两个js文件,一个是main.js一个是vendors.js文件,用户第二次刷新页面的时候,这两个文件实际上已经被缓存进浏览器了,就可以直接到浏览器缓存中拿了,加入我们改了代码
index.js

import _ from 'lodash';
import $ from 'jquery';

const dom = $('<div>');
dom.html(_.join(['hello', 'world'],'-----'));     //  这里改为-----连接
$('body').append(dom);

重新打包,然后把新生成的dist文件上传到服务器,当用户刷新之后会看到改变后的内容吗?其实是不会的,因为我们打包后的名字没有变,还是main.js和vendors.js,用户再刷新页面的时候,发现这两个文件本地有缓存了,就会直接用本地的缓存了,而不会用你新上传上去的这两个文件,这样就会产生问题

为了解决这个问题该怎么做呢?我们需要引入一个概念contenthash

webpack.common.js

output: {
-       filename: '[name].js', 
-       chunkFilename: '[name].chunk.js',                     
        path: path.resolve(__dirname, '../dist'),
    },

webpack.dev.js

const devConfig = {
    ...
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
+   output: {
+       filename: '[name].js', 
+       chunkFilename: '[name].chunk.js',   
+   }
}

webpack.prod.js

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

const prodConfig = {
    mode: 'production', 
    devtool: 'cheap-module-source-map',
+    output: {
+        filename: '[name].[contenthash].js',      // contenthash是和name一样的占位符
+        chunkFilename: '[name].[contenthash].js', 
+    }
}

module.exports = merge(commonConfig, prodConfig);

重新npm run build,可以看到生成的hash值是7fa81f74109755cc2cb0
图片描述

然后我们对index.js什么都不改,重新再npm run build,可以看到生成的hash值还是一样的,源代码没变,打包出来的文件名也没变,所以用户再去请求的时候,还是拿的缓存。假设我们对源代码再次进行修改下
index.js

import _ from 'lodash';
import $ from 'jquery';

const dom = $('<div>');
dom.html(_.join(['hello', 'world'], '======='));   //  ----- 改为=======
$('body').append(dom);

重新npm run build,可以看到hash已经变成bf597aacb0a0afd970fc
图片描述

我们重新把这个最新的dist文件放到服务器上去,用户再去访问页面的时候,就是访问最新的js文件了

更多Caching的配置可以参考官方文档

二十三、Shimming

webpack 编译器(compiler)能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery 中的 $)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方。

在项目中我们经常会使用一些第三方库,这里我们在src中自创一个这样的库jquery.ui.js
src/jquery.ui.js

export function ui() {
    $('body').css('background', 'green');
}

然后我们在index.js中引入这个库

   import _ from 'lodash';
   import $ from 'jquery';
+  import { ui } from './jquery.ui';

+  ui()

   const dom = $('<div>');
   dom.html(_.join(['hello', 'world'], '======='));
   $('body').append(dom);

保存之后,运行npm run dev,可以看到页面直接报错,提示我们“$ is not defined”
图片描述
实际上这里是指jquery.ui.js中的$找不到,那我们会想,在index.js中我们不是引入了$了吗?为什么在jquery.ui.js中找不到呢,原因是因为在webpack中我们是基于模块打包的,模块中的变量是独立于自己这个模块的,所以在jquery.ui.js中想去使用另一个模块的$是不可能的,通过这种方式可以保证,模块与模块之间不会有任何的耦合,不会因为一个模块出错影响到另一个模块

如果想要在jquery.ui.js中去使用$,就必须在上面引入这个变量

+  import $ from 'jquery';

   export function ui() {
      $('body').css('background', 'green');
   }

重新刷新页面,可以看到页面就正常显示绿色了,但是这个库是第三方的,不是我们自己写的,实际上想在这个库的源码中直接这样引入是不现实的,那是不是没办法实现了呢?

先在jquery.ui.js中去掉这个(import $ from 'jquery')引入,然后打开webpack.common.js,我们在这个里面做点配置

+  const webpack = require('webpack');

   module.exports = { 
    ...
      plugins: [
        ...
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[name].chunk.css'                       
        }),
+       new webpack.ProvidePlugin({
+           $: 'jquery'
+       })
    ]
}

意思是webpack如果发现一个模块中使用了$变量,就会在这个模块中自动帮你引入jquery,当这样配置之后就完美的解决了上面的问题了,接下来我们验证下,改了配置文件需要重新npm run dev,可以看到这个时候页面就正常显示了

如果我们还想在这个库中直接使用lodash中的某个方法(如:join方法),可以这样配置

export function ui() {
    $('body').css('background', join(['green'], ''));
}

webpack.common.js

plugins: [
        ...
        new webpack.ProvidePlugin({
            $: 'jquery',
+           join: ['lodash', 'join']
        })
    ],

保存重新npm run dev,页面依然正常显示

接下来介绍Shimming的其它用法
删掉src/jquery.ui.js这个文件,然后删掉index.js中的内容,并在这个里面打印这样一句话
index.js

console.log(this)

刷新页面在控制台可以看到此时打印出来的是一个对象
图片描述

实际上这个this指的是index.js这个模块自身。有的时候我们希望这个this指向的是window,那我们可以借助这个imports-loader来帮我们实现

安装

npm install imports-loader --save-dev

安装好之后,我们再在webpack.common.js中进行配置

{ 
        test: /\.js$/, 
        exclude: /node_modules/, 
 -      loader: 'babel-loader'
 +      use: [{
 +            loader: 'babel-loader'
 +        }, {
 +            loader: 'imports-loader?this=>window'
 +      }]
},

意思是当加载js文件的时候,首先会走imports-loader,它会把这个js文件里面的this改为window,然后再交给babel-loader做js的编译

重新运行npm run dev,打开浏览器控制台,此时this就是指向window了
图片描述

更多Shimming的用法可以参考官方文档

二十四、环境变量的使用

对webpack.dev.js修改

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

const devConfig = {
    mode: 'development', 
    devtool: 'cheap-module-eval-source-map',  
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true          
    },
    optimization: {
        usedExports: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    output: {
        filename: '[name].js', 
        chunkFilename: '[name].chunk.js',   
    }
}

-   module.exports = merge(commonConfig, devConfig);
+   module.exports = devConfig;

对webpack.prod.js修改

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

const prodConfig = {
    mode: 'production', 
    devtool: 'cheap-module-source-map',
    output: {
        filename: '[name].[contenthash].js', 
        chunkFilename: '[name].[contenthash].js', 
    }
}

-  module.exports = merge(commonConfig, prodConfig);
+  module.exports = prodConfig;

对webpack.common.js修改

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');   
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
+  const merge = require('webpack-merge');
+  const devConfig = require('./webpack.dev.js');
+  const prodConfig = require('./webpack.prod.js');

-  module.exports = { 
+  const commonConfig = { 
    ...
}


// 之前导出的是对象,现在我们导出的是一个函数,函数参数我们可以在webpack命令行环境配置中,通过设置 --env进行配置
+ module.exports = env => {
+    if(env && env.production) {
+        return merge(commonConfig, prodConfig);
+    } else {
+        return merge(commonConfig, devConfig);
+    }
+ }

通过上面这样修改,我们打包的时候就需要往webpack.common.js这个配置文件中传递env这样一个全局变量

打开package.json

"scripts": {
-    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js",
+    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.common.js",
-    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
+    "dev": "webpack-dev-server --config ./build/webpack.common.js",
-    "build": "webpack --config ./build/webpack.prod.js"
+    "build": "webpack --env.production --config ./build/webpack.common.js"
  },
如果设置 env 变量,却没有赋值,--env.production 默认将 --env.production 设置为 true。还有其他可以使用的语法。有关详细信息,请查看webpack CLI文档。

保存,我们验证下
执行npm run build,可以看到打包正常
图片描述

执行npm run dev,打包还是正常
图片描述

然后执行npm run dev-build,打包依然正常,说明我们的环境变量配置好了


xiaoxiaohu
68 声望4 粉丝

我感受到的压力,都是来自于我自己不努力不积极而又不甘于现状的恐慌


引用和评论

0 条评论