新版webpack4.0指南

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

1、webpack简介

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

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

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

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

3、webpack 核心概念:

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

4、webpack打包流程:

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

5、搭建webpack环境

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

npm init -y

执行完以后,咱们的文件夹中会多出一个package.json文件
图片描述
而后咱们稍加修改react

{
  "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依赖jquery

npm install --save-dev webpack webpack-cli

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

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就能够查看版本号git

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

此时说明咱们webpack安装成功
要想查看webpack之前的各类版本,能够经过以下命令github

npm view webpack versions

6、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模式打包以后代码是不会被压缩的

7、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文档

补充知识:

一、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,这样就不会出现任何的问题了

二、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

8、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

9、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域名
图片描述

10、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官方文档

11、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 访问

12、热模块更新

模块热替换(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的其余参数能够参考 这里

十3、使用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

十4、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的详细介绍能够参考 官网

十5、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

十6、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

十7、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这个工具库单独提取出来了

十8、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中配置自定义名字,发现打包都会报一样的错
图片描述

从上面这些例子中得出结论:
一、 chunks无论设置成什么,webpack作代码分割的时候,都会去匹配cacheGroup这个配置项
二、 chunks设置成async或者all的时候,去处理异步代码,若是想自定义打包后的名字只能经过魔法注释,若是想让打包出来的名字不带vendors,能够把venders设置成false,意思是不让webpack去配置cacheGroup.vendors这个配置项
三、 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根据功能拆分出来的,包含三种状况:
    一、你的项目入口(entry)
    二、经过import()动态引入的代码
    三、经过splitChunks拆分出来的代码

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

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

十9、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文件,点击页面某个地方,能够看到页面显示正常,说明这样写也是没问题的

关于懒加载的其余知识点能够参考 官网

二10、打包分析,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 这种方式来预加载

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

二11、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

二12、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的配置能够参考 官方文档

二十3、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的用法能够参考 官方文档

二十4、环境变量的使用

对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,打包依然正常,说明咱们的环境变量配置好了

相关文章
相关标签/搜索