Webpack打包

2,365 阅读44分钟

一、模块打包工具

模块化很好的解决了在复杂应用开发过程中的组织问题, 但随着我们引入模块化, 应用又会产生一些新的问题:
1、ES Modules 存在环境兼容问题
尽管现在主流浏览器的最新版本都支持这个特性了, 但现在还不能统一所用用户浏览器的使用情况, 所以还是解决兼容性问题;
2、模块文件过多, 网络请求频繁
通过模块化的方式划分的模块文件会比较多, 而前端应用又是运行在浏览器当中的, 每一个模块化文件都需要去服务器请求回来, 这些零散的模块文件会导致浏览器频发发送请求, 从而影响浏览器的工作效率;
3、所有的前端资源都需要模块化
在前端应用的开发过程中, 并不仅仅只有 js 代码需要模块化, 随着应用的日益复杂, HTML、css 这些资源文件同样也会面临相同的问题; 从宏观角度来看, 这些资源文件也都可以看做为前端应用的一个模块, 只不过这些模块的种类和用途和 js 模块不一样;
=> 对于整个开发过程而言, 毋容置疑, 模块化是必要的; 不过我们在原有的基础上引入更好的方案或者工具去解决上述问题或者需求, 让开发者在应用的开发阶段继续享用模块化带来的优势, 又不必担心模块化对生产环境带来的影响;
=> 这里相对更好的方案或者工具提出一些设想, 希望这个方案或者工具满足我们这些设想:

1、编译代码: 将开发阶段编写的包含新特性的代码直接转换为兼容绝大多数环境的代码 => 解决了环境的兼容问题;

2、打包代码: 将散落的模块文件打包到一起, 解决了浏览器中频繁对模块文件发送请求的问题; => 模块的划分, 只是在开发阶段需要, 能够更好的组织代码; 对于运行环境, 是没有必要模块化的, 所以在开发阶段通过模块化编写, 在生产阶段打包到同一个文件中;

3、需要支持不同种类的前端资源类型: 可以将开发阶段说涉及到的样式、图片、字体等资源文件都当做模块使用, 这对于整个前端应用来讲, 就有了统一的模块化方案;

针对于前面两个需求, 可以借助于之前了解过的构建系统配合一些编译工具实现; 对于第三个需求, 就很难通过构建系统 + 编译工具来解决 => 所以就有了前端模块打包工具;

1.1、模块打包工具 - 概要

前端领域目前有些工具就很好的解决了上述问题, 最为主流的就是: webpack、parcel、rollup => 以 webpack 为例, 他的一些核心特性就很好的满足了上面我们的设想;

1、模块打包器(Module bundler): 通过 webpack 可以将零散的代码打包到同一个 JS 文件中;

2、模块加载器(Loader): 代码中对环境有兼容性的代码, 在打包的过程中可以通过 模块加载器 对其进行编译转化;

3、代码拆分(Code Splitting): webpack 可以将应用所有的代码都按照需要打包, 这样一来就不用担心将所有的代码全部打包到一起产生一个较大的文件的问题; webpack 可以将应用加载过程中初运行中所必须的模块打包到一起; 对于其他的模块, 单独存放, 等到工作中实际需要到某个模块, 再去异步加载该模块, 从而实现增量加载(渐进式加载) => 这样就不用担心文件太碎或者文件太大这两个极端的问题;

4、资源模块(Asset Module): 对于前端模块类型的问题, webpack 支持在 JS 中以模块化的方式去载入任意资源的文件: 例如: 在webpack 中可以通过 JS 直接 import css 文件, 这些 css 文件最终会通过 style 标签的形式去工作; 其他类型的文件也可以用类似的方式实现;

所有的打包工具都是以模块化(不单指 JS 模块化, 而是整个前端项目的模块化)为目标; => 让我们在开发阶段更好的享受模块化所带来的的优势, 同时又不必担心模块化对生产环境所产生的的影响;

1.2、Webpack 快速上手

Webpack 作为目前最主流的前端模块打包器, 提供了一整套的前端项目模块化方案, 而不仅仅局限于 JS 的模块化; 通过 Webpack 提供的前端模块方案, 可以轻松的对开发阶段项目所有的资源进行模块化;

项目目录结构: src/heading.js src/index.js index.html

//heading.js
export default () => {
  const element = document.createElement('h2')
  
  element.textContent = 'hello world'
  element.addEventListener('click', () => {
    alert('Hello webpack')
  })
  return element
}
//index.js
import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)
//index.html
<body>
  <script type="module" src="src/index.js"></script>
</body>

运行 index.html => 页面正常工作;
=> 我们通过 Webpack 打包项目

安装 Webpack: 因为 Webpack 是npm 的工具模块 => 初始化 package.json

cnpm init --yes

安装 Webpack 及其核心模块

cnpm i webpack webpack-cli -D

运行 webpack

yarn webpack --version

通过 Webpack 打包 src 目录下的代码: 默认从 src 下的 index.js 开始打包

yarn webpack

打包的结果默认存放到根目录下的 dist 目录

修改 index.html

//index.html
<body>
  <script src="dist/main.js"></script>
</body>

将 webpack 命令添加到 package.json: 以后打包就运行 yarn build

//package.json{  "scripts": {    "build": "webpack"  }}

1.3、Webpack 配置文件

Webpack4 以后的版本支持 0 配置的方式直接启动打包, 打包过程会按照约定将 ‘src/index.js’ 打包到 ‘dist/main.js’; 但是, 大多情况下需要自定义打包的输入路径和输出路径 => 就需要为 Webpack 添加配置文件, 具体做法:
=> 在根目录下添加 webpack.config.js 文件, 该文件是运行在 node 环境下, 编写该 js 时需按照 CommonJs 规范编写代码;

//webpack.config.jsconst path = require('path')//通过导出对象的属性完成相应的配置选项module.exports = {  entry: './src/mian.js', //指定 webpack 打包的输入文件路径(相对路径情况的 ./ 是不能省略的)  output: {    filename: 'main.js', //设置输出文件的名称    path: path.join(__dirname, 'dist'), //设置输出文件的路径 => 该路径必须为绝对路径  }, //指定 webpack 输出文件的属性 => 该属性的值为一个对象}

1.4、Webpack 工作模式

Webpack4 新增了工作模式的用法, 该用法大大简化了 Webpack 配置的复杂程度, 可以理解为针对不同环境的几组预设的配置;

以 1.3 的配置文件为例, 运行 webpack

yarn webpack

运行结果中会输出一个配置警告:

大致的意思是: 没有设置 mode 的属性, Webpack 会默认使用 production 模式去工作;
=> 在 production 模式下, Webpack 会默认启动一些优化插件, 例如: 代码压缩, 该模式对实际生成环境是很好的, 但是打包的文件没法阅读, 对开发阶段来说很不友好;
=> 可以通过 CRI 参数指定打包模式, 具体用法: 为 Webpack 命令传入 --mode 参数 => 该参数有三种取值:

  1. 默认 production: 默认启动优化插件

  2. 开发模式 development: 自动优化打包速度, 添加一些调试过程中需要的辅助;

    yarn webpack --mode development
    
  3. 原始模式 none: 不会做任何额外的处理, 只做原始的打包;

    yarn webpack --mode none
    

关于这三种模式的差异, 可以在官方文档中查看: webpack.js.org/configurati…

除了在命令行中添加 CRI 参数设置工作模式以外, 还可以在 Webpack.config.js 中通过设置 mode 属性的值来设置工作模式;

//webpack.config.jsmodule.exports = {  mode: 'development', //设置 Webpack 的工作模式}

1.5、Webpack 资源模块加载

Webpack 不仅仅是 JS 模块打包工具, 而是前端项目或者前端工程的打包工具 => Webpack 可以打包在前端项目中的任意资源文件 => Ex: 通过 Webpack 打包 css 文件:

以 1.4 为例子: 在 src 下添加 main.css, 编辑 main.css

//main.cssbody{  margin: 0 auto; padding: 0 20px;  max-width: 800px; background: #0f0;}
1.5.1、修改 webpack.config.js
//webpack.config.js/*amend - */entry: './src/main.js'/*amend + */entry: './src/main.css'/*amend end */
1.5.2、运行 webpack 打包命令
yarn webpack

结果: 报错 => 解析模块时遇到了非法字符

原因是: Webpack 默认只解析 JS 文件(会将打包过程中所遇到的所有文件都作为 JS 文件来解析) => 当前并没有配置解析该文件的加载器 => Webpack 可以通过加载器 loader 来解析文件
=> 内部的 loader 只能处理 JS 文件, 我们可以为其他的资源文件添加不同的 loader

1.5.3、解析 CSS 文件, 需要安装 css-loader
cnpm i css-loader -D
1.5.4、在 webpack.config.js 中添加对应的配置
//webpack.config.jsmodule.exports = {  /*amend + */  module: {    rules: [      {        text: /.css$/, //正则表达式, 匹配打包过程的文件路径        use: 'css-loader' //指定匹配到的文件打包所需要的loader      }    ]//针对于其他资源模块打包规则的配置  }  /*amend end */}
1.5.6、重新运行 webpack 重新打包项目, 并启动项目
yarn webpack

大包过程没有问题, 但此时样式并没有作用到页面上 => css-loader 只是将 CSS 资源转换为项目的一个模块, 具体的实现都是将 css 样式 push 到了一个数组数组中 , 这个数组是由 css-loader 的一个模块提供的=> 整个过程并没有使用该数组

1.5.7、这里还需安装一个 style-cloader

style-loader: 将 css-loader 转换后的结果通过 style 标签追加到页面中

cnpm i style-loader -D
1.5.8、修改 webpack.config.js
//webpack.config.jsmodule: {  rules: [    {      test: /.css$/,      /*amend */      use: [        'style-loader',        'css-loader'      ]//这里可以接受一个数组, 数组中加载多个 loader, 执行时是 从后往前 执行    }  ]}

通过 Webpack 重新打包项目, 打包过程中, style-loader 会将 css-loader 转换后的结果通过 style 标签添加都页面中

运行页面, css 样式就作用到页面中了

1.5.9、总结

Loader 是 Webpack 的核心特性, 借助于 Loader 就可以加载任何类型的资源;

1.6、Webpack 导入资源模块

通过以上的探索, 知道可以将 CSS 文件作为项目的打包入口, 但是 Webpack 的打包入口一般还是 JS 文件, 因为打包入口从某种程度上来说是项目的运行入口;
=> 而就目前而言, 前端应用中业务是由 JS 驱动的, 正确的做法还是将 JS 作为打包入口, 然后在 JS 中通过 import 的方式去引入 CSS 文件, 这样 css-loader 任然可以正常工作;

1.6.1、编辑 webpack.config.js
//webpack.config.jsmodule.exports = {  /*amend - */  entry: './src/main.css'  /*amend + */  entry: './src/main.js'  /*amend end */}
1.6.2、编辑 src/main.js
//main.jsimport createHeading from './heading.js'/*amend + */import './main.css'/*amend end */
1.6.3、重新打包
yarn webpack 
1.6.4、运行该项目
1.6.5、为 heading 添加样式

在 src 目录下添加 heading.css, 编辑 heading.css

/* heading.css */.heading{  color: #fff; background: #00f;   padding: 20px; margin: 0 atuo;}

因为我们是定义的 类, 所以要给 heading.js 中的元素添加类名 => 编辑 heading.js

//heading.js/*amend + */import './heading.css'/*amend end */.....element.textContent = 'hello world'/*amend + */element.classList.add('heading')/*amend end */> 重新打包项目​```sheyarn webpack

这样在 JS 文件中去引入 CSS 文件可能会给你带来疑惑, 传统的做法推荐是将 CSS 文件和 JS 文件分开, 单独去维护; 为 Webpack 又建议在 JS 在载入 CSS 文件, 这到底是为什么勒?
=> Webpack 不单单是建议在 JS 文件中引入 CSS 文件, 而是建议在编写代码过程中去引入当前代码所需要的资源文件, 这是因为真正需要资源的不是应用, 而是此时正在编写的代码, 是代码需要正常工作就必须去加载对应的资源 => 这就是 Webpack 的哲学

  1. 逻辑合理, JS 缺失需要这些资源文件
  2. 确保上线资源不缺失, 都是必要的;

学习一个新事物, 不是学会所用的用法你就能提高, 因为这些东西照着文档, 谁都可以做, 很多时候 新事物的实现才是突破点, 能够搞明白新事物为什么这么设计, 你就出道了;

1.7、Webpack 文件资源加载器

目前 Webpack 社区提供了非常多的资源加载器, 基本你能想到的合理的需求都有对应的 loder;

Ex: 文件资源加载器: 大多数的加载器都类似于 css-loader, 都是将资源模块转化为 JS 代码的实现方式工作, 还有一些经常用到的资源文件, 例如: 项目中的图片或者字体, 这些资源没有办法通过 JS 的方式去表示的, 这类的资源文件需要用到文件资源加载器 => file-loader

在 src 目录下添加一张图片: icon.png, 编辑 main.js

import './main.css'/*amend + */import icon from './icon.png'/*amend end */......//这里需要接收该图片模块的默认导出, 该模块的默认导出就是打包后图片的资源, 有了资源路径后, 就可以使用该路径/*amend + */const img = new Image()img.src = icondocument.body.append(img)

安装 file-loader

cnpm i file-loader -D

配置 webpack.config.js

module: {  rules: [    .....,    {    	test: /.png$/,    	use: 'file-loader'    }  ]}

通过 webpack 重新打包, => 打包完成后在 dist 目录下会生成重命名后的导入的图片 => 在 dist/main.js 中是通过刚刚打包生成的名称导出了, 在入口函数中使用了导出的名称作为 img 的 src, 将 img 添加到了 body 中

运行项目 => 在页面中图片不能被正常加载 => 原因: 打包的时候 index.html 并没有生成到 dist 目录, 而是在项目的根目录, 而不是网站的根目录 => webpack 默认 dist 作为网站的根目录 => 解决方案: 通过配置 webpack.config.js 文件修改打包后的文件在网站中的文字

//webpack.config.js.....output: {  filename: 'main.js',  path: path.join(__dirname, 'dist'),  /*amend + */  publicPath: 'dist/' //告诉 Webpack 打包后文件的位置 => 默认为空,表示网站的根目录, 这里的 / 不能省略}

通过 webpack 重新打包项目 => 在 dist/main.js 中将 pubicPath 的值赋值到了 打包文件路径的前面

运行项目 图片就可以显示了

总结: webpack 在打时遇到了图片文件 => 根据配置文件中的配置匹配对应的文件加载器 => 文件加载器将资源文件拷贝到输出目录, 生成资源的路径 => 将该路径作为当前模块的返回值返回(对于项目来说: 所需要的资源就被发布出来了) => 通过模块的导出成员拿到资源的打包后的路径 => 在页面是使用该路径;

1.8、Webpack Data URLs 与 url-loader

其实除了用 file-loader 拷贝物理文件的形式去处理文件以外, 还可以用 Data URLs 表示文件的方式处理文件 => 这种方式也非常常见;

Data URLs : 是一种特殊的 URL 协议, 可以用来直接表示一个文件, 传统的 URL 要求服务器上有对应的文件, 应用通过请求该文件的地址, 得到服务器对应的文件 => 而 Data URLs 是当前 URL 就可以表示当前文件的内容的 URL 协议

=> 这个 URL 的文件就已经包含了文件的内容, 使用该 URL 时, 不会向服务器发送请求
=> Ex: data: text/html; charset=UTF-8, <h1>html content</h1>

浏览器就可以根据上面的语句解析出这是一个 html 类型的内容, 编码为 utf-8, 内容为 <h1>html content</h1>

如果是图片或者字体这一类无法通过文本表示的二进制文件, 可以将文件的内容通过 base64 位编码, 以编码后的结果(字符串) 表示该文件的内容

=> Ex: data: image/png; base64, isdfs......SuqMcc: 表示 png 类型的文件, 文件的编码为 base64, 最后就是该图片文件的 base64 编码 => 一般的 base64 编码较长, 浏览器一样可以解析该编码;

Webpack 打包资源文件一样可以使用这种方式, 通过 Data URLs 就可以通过代码的形式表示任何类型的文件 => 具体的做法: 需要用到一个专门的加载器 => url-loader

安装 url-loader:

cnpm i url-loader -D

编辑 webpack.config.js

....module: {  rules: [   ...,    {    	test: /.png$/,    	use: 'url-loader'    }  ]}

此时, png 文件就会使用 url-loader 将其转化为 Data-URLs 格式文件 => 重新打包项目 => 打包完后, dist 目录下不在会生成图片文件了 => 查看 dist/main.js: 导出的就不再是文件路径, 而是一个完整的 Data-URLs

运行项目: 图片也能正常显示;

这种方式很适合项目中体积比较小的资源, 因为如果资源的体积过大就会造成打包的结果非常大, 从而影响项目的运行速度;
最佳的实践:

  • 项目中的小文件通过 url-loader 转化为 Data-URLs, 减少请求次数
  • 大文件单独提取存放, 提高加载速度

url-loader 可以通过配置选项实现最佳实践, 具体的做法: 配置 webpack.config.js

....module: {  rules: [    ....    {    	test: /.png$|.jpg$/,    	use: {    		loader: 'url-loader',    		options: {// 为 url-loader 添加配置选项    			limit: 10 * 1024 //url-loader 转化的最大的文件体积, 超过该设置的, 任然交给 file-loader 处理;    		}    	}    }  ]}

再次打包, 运行项目

通过以上方式可以实现:

  • 超过 10KB 的文件单独提取存放
  • 小于 10KB 的文件转换为 Data URLs 嵌入代码中

需要注意的是: 通过这种方式使用 url-loader , 必须需要同时安装 file-loader, 因为 url-loader 对于超出大小的文件还是需要 file-loader 提取文件;

1.9、Webpack 常用加载器分类

模块中的资源加载器像工厂里的生产车间, 用来处理和加工打包工程中的资源文件, 除了上面介绍的加载器, 社区中还有很多其他的加载器 => 可以将常用的 loader 大致分为三类:

1..9.1、编译转换类:

会将加载的资源模块转化为 JS 代码, Ex: css-loader: 将 CSS 代码转化为 main.js 中的模块, 从而实现 JS 运行 CSS;

1.9.2、文件操作类

会将加载的资源模块拷贝到输出的目录, 同时又将该文件的访问路径向外导出 => Ex: file-loader

1.9.3、代码检查类

对加载到的资源文件(一般是代码)进行校验的加载器, 该类加载器的目的是为了统一代码的风格, 从而提高代码质量, 该类型加载器一般不会修改生产环境代码;

以上三种就是针对于常用的 loader 做的分类归纳, 后续在接触到一个 loader 后, 要先明确该 loader 是什么类型的加载器, 特点是什么, 使用需要注意什么?

1.10、Webpack 处理 ES2015

由于 Webpack 默认可以处理代码中的 import 和 export, 有人很自然的认为 Webpack 会自动编译 ES6 的代码, 实则不然: Webpack 是对模块进行打包工作; => 因为模块打包需要, 所以 Webpack 会处理 import 和 export, 除此之外, Webpack 并不能转化代码中的其他 ES 特性; => Ex: 打包后的 main.js

在 main.js 中的模块中使用的 const 以及 箭头函数并没有被转换, 如果需要 Webpack 在打包过程中同时处理其他 ES6 特性的转换, 需要为 JS 文件配置额外的编译型 loader => 最常见的 babel-loader

安装 babel-loader: 由于 babel-loader 需要依赖 babel 的核心模块, 此时需要同时安装 @babel/core @babel/preset-env

cnpm i babel-loade @babel/core @babel/preset-env -D

安装完成后 => 为 JS 指点加载器为 babel-loader => 编辑 webpack.config.js

....module: {  rules: [    /*amend + */    {      test: /.js$/,      use:{      	loader: 'babel-loader',      	options: {      		presets: ['@babel/preset-env']    		}    	}    }    /*amend end */    ....  ]}

重新打包项目 => 运行项目

**总结: **

  • webpack 默认只是打包工具, 不会处理代码中的 ES6 及更高版本的新特性, 如果需要处理新特性, 可以通过为 JS 代码配置加载器来实现
  • 加载器可以用来编译转化代码

1.11、Webpack 模块加载方式

在代码中除了 import 能触发加载模块, Webpack 还提供了其他几种加载模块的方式, 我们将 Webpack 中加载模块的方式分为以下几种:

1.11.1、遵循 ES Modules 标准的 import 声明

1.11.2、遵循 CommonJs 标准的 require 函数

如果通过 require 函数载入 ESModules , 需要注意: 对于 ESModules 的默认导出, 需要通过导入 require 模块的导入的结果的 default 属性去获取;

1.11.3、遵循 AMD 标准的 define 函数和 require 函数

Webpack 兼容多种模块化标准, 除非必要的情况下, 否则不要在项目中去混合使用这三个标准, 如果混合使用, 会大大降低项目的可维护性, 项目去统一使用一个标准就行了;

1.11.4、一些 独立的加载器, 在工作时也会处理所加载资源中的导入的模块(Loader 加载的非 JS 也会出发资源资源加载)

Ex: 样式代码中的 @import 指令和 url 函数;
=> HTML 代码中图片标签的 src 属性以及其他标签的某些属性;

1.11.4.1、样式代码中的 url 函数加载资源

在项目中有 css 文件: 如下

body{  min-height: 100vh;  background: #f4f8fh url(background.png);  background-size: cover;}

运行 webpack 命令打包项目;

Webpack 在遇到 css 文件的时候会使用 css-loader 打包该文件, 文件中的 url(background.png) 会被作为资源加入到打包过程中, Webpack 会根据该文件的格式找到相对应的 loader 处理该文件;

1.11.4.2、样式文件中的 import 指令加载资源

在样式文件中除了 url 函数可以加载资源, import 指令也可以加载其他资源 => 在 src 下新建 reset.css 并编辑

body{  margin: 0; padding: 0;}

编辑 main.css: 在其中通过 import 指令载入 reset.css 文件

@imprt url(reset.css);body{  .....}

运行 Webpack 打包命令 => reset.css 的样式被使用

1.11.4.3、HTML 代码中的 img 的 src 属性

在 src 下新建 footer.html, 并编辑

<footer>	<img src="better.png" alt="better" width="256" /></footer>

编辑 src/main.js(在入口文件中需要导入 footer.html 才能参与 Webpack 的打包过程)

import './main.css'import footerHtml from './footer.html'// html 文件默认会将 html 代码作为字符串导出, 需要接受用参数来接受document.write(footerHtml)

Webpack 打包 html 代码, 需要安装 html-loader => 安装 html-loader

cnpm i html-loader -D

配置 webpack.config.js

.....{  test: /.html$/,  use: {		loader: 'html-loader'  }}

通过 Webpack 打包项目 => html 代码中的 img 的图片正常显示在页面中 => 说明 html 代码中 img 的 src 属性也可以触发资源文件的加载;

在 HTML 代码中, 不仅仅只有 img 的 src 属性依赖资源, 其他的标签的某些属性也可能需要依赖资源 => Ex: a 标签的 href 属性 =>
=> 编辑 footer.html

<footer>	....  <a href="better.png">downPng</a></footer>

通过 Webpack 打包项目, 运行项目后, 点击页面中的 a 标签找不到相对应的图片
=> 原因是 html-loader 默认只会处理 img 的 src 属性, 如果想要处理其他属性, 需要通过配置 webpack.config.js

....{  test: /.html$/,  use: {    loader: 'html-loader',    options: {      attrs: ['img: src', 'a: href']//默认当中只用 'img: src'    }  }}

再次通过 Webpack 打包并运行, 点击页面的 a 标签, 就可以找到相对应的图片;

总结: 几乎代码中所有需要引用到的资源, 都会被 Webpack 找出来, 然后根据 webpack.config.js , 交个不同的 loader 处理, 将处理的结果整体打包到输出目录;

1.12、Webpack 核心工作原理

以普通的前端项目为例: 在项目中一般都会散落着各种各样的代码及资源文件,

Webpack 会根据配置找到其中的一个 JS 文件作为入口, 然后顺着入口文件的代码, 根据代码中出现的 import 或者 require 之类的语句, 解析、推断出来文件所依赖的资源模块, 分别解析每个资源模块对应的依赖, 最后就形成了整个项目中所有用到文件直接的依赖关系的依赖树, 有了这个依赖树之后 => Webpack 会递归依赖树 => 找到每个节点对应的资源文件 => 根据配置中的 rules 属性找到模块所对应的加载器 => 加载该模块 => 加载后的结果放入到 bundle.js 中 => 从而实现整个项目的打包; => 在整个过程中 Loader 机制起到了很重要的作用 => Loader 机制是 Webpack 的核心;

1.13、Webpack 开发一个Loader(Webpack Loader 的工作原理)

需求: markdown 文件的加载器 => 在代码中可以导入 markdown 文件
=> 项目目录: src/about.md、src/main.js、src/markdown-loader.js、index.html、package.json、webpack.config.js
=> 初始化文件夹, 安装 webpack

//main.jsimport about from './about.md'console.log(about)
//about.md#关于我我是 lcy668 一个奋斗的前端工程师
<!DOCTYPE html><html lang="zh-CN"><head>   <meta charset="UTF-8">   <meta http-equiv="X-UA-Compatible" content="IE=edge">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>Document</title></head><body>   <script src="dist/bundle.js"></script></body></html>
//webpack.config.jsconst path = require('path')module.exports = {   mode: 'none',   entry: './src/main.js',   output: {         filename: 'bundle.js',      path: path.join(__dirname, 'dist'),      publicPath: 'dist/'   }}

完成 Loader 的开发以后, 可以将该模块发布到 npm 上作为一个独立的模块使用;

Loader 介绍

每个 Webpack 的 Loader 都需要导出一个函数 => 该函数是对加载的资源的处理过程: 输入就是加载的资源文件的内容, 输出就是此次加工以后的结果;

//markdown-loader.jsmodule.exports = source => {  //通过 source 接收输入, 通过 返回值输出  console.log(source);  return 'hello ~'}

在 Webpack.config.js 中添加加载器的规则配置

//webpack.config.js...../*amend + */module: {  rules: [    {      test: /.md$/,      use: './markdown-laoder' //这里不仅仅只可以使用模块的名称, 还可以使用模块的文件路径    }  ]}

使用 webpack 打包项目 => 打印输出 about.md 的内容 以及 hello~ , 但是会报错:

: 需要一个额外的加载器处理当前的加载结果

=> 原因: Webpack 的工作过程有点像工作管道 => 可以在该管道中依次使用多个 loader, 但是要求管道工作之后的结果必须是一段 Js 代码( Result 必须是 JS 代码 )=> 现在的处理是: hello ~ 不是一段 JS 代码, 所以报错;
=> 解决方式: 1、返回结果修改为 JS 代码; 2、再去找其他的 Loader 来处理当前返回的结果;

第一种解决方案: 修改返回结果
//markdown-loader.jsmodule.exports = source => {  console.log(source)  return 'console.log("hello ~")'}

重新打包项目, 正常打包完成;
=> 查看打包后的 bundle.js => webpack 将 loader 处理的结果拼接到模块中(这也解释了为什么 loader 必须要返回 JS 的原因) => 要达到解析 .md 文件, 需要安装可以解析 .md 文件的模块
=> 安装 marked

cnpm i marked -D

安装完成后编辑 markdown-loader.js

//markdown-laoder.jsconst marked = require('marked')module.exports = source => {  //console.log(source)  //return 'console.log("hello ~")'  const html = marked(source)//现在的结果是一段 Html 代码, 如果直接返回 html 会报错;  //正确的做法就是将 html 代码转化为一段 JS 代码, 这里, 希望把这段 html 转化为字符串作为当前模块的导出;  return `module.exports = ${JSON.stringify(html)}`}

运行 Webpack 打包 => 查看打包结果: 此时结果正确;
=> 除了 module.exports 的方式以外, Webpack 允许在返回的代码中使用 esModules 的方式导出; => 这里可以将 返回的语句修改为以下语句:

return `export default ${JSON.stringify(html)}`

结果也是可以的, Webpack 会自动转换代码中的 esModules 代码;

第二种解决方案: 返回一个 html 字符串, 交给下一个 loader 处理

编辑 markdown-loader.js 的返回语句:

return html

安装用于处理 html 的 loader: html-loader

cnpm i html-loader -D

修改 webpack.config.js

//webpack.config.js.....relues: [  {    test: /.md$/,    use: [      'html-loader',      './markdown-loader'    ]  }]

重新通过 webpack 打包项目 => 打包正确

总结: Loader 负责资源文件从输入到输出的转换, 对于同一个资源可以依次使用多个 Loader

1.14、Webpack 插件机制

插件机制是 Webpack 中另外一个核心特性, 他的目的是为了增强Webpack 在项目自动化方面的能力 => 我们知道 Loader 负责实现项目中资源模块的加载, 从而实现整体项目的打包 => 而 Plugin 是用来解决项目中出了资源加载以外的其他一些自动化工作: Ex: 清除 dist 目录、拷贝不需要打包的资源文件到输出目录、压缩打包结果的输出代码 => 有了 plugin 的 Webpack 几乎无所不能的实现了前端工程化中绝大多数经常用到的部分; => 这也造成了很多初学者任务 Webpack 就是前端工程化的原因;

Webpack 常用插件: clean-webpack-plugin

作用: 用来自动清理输出目录的插件 => 在之前的学习中, Webpack 打包都是直接覆盖到 dist 目录, 而打包之前, dist 目录中可能存在一些之前的遗留文件, 再次打包只能覆盖同名的文件, 对于那些已经移除的资源文件就会一直积累在 dist 目录中 => 合理的做法是: 在每次打包之前自动清理 dist 目录 => 这样 dist 中只会存在项目需要的文件 => clean-webpack-plugin 插件就很好的实现了这个需求;

安装 clean-webpack-plugin

cnpm i clean-webpack-plugin -D

编辑 webpack.config.js

//webpack.config.jsconst { CleanWebpackPlugin } = require('clean-webpack-plugin').......,  //绝大多数插件导出的都是一个类型plugins: [  new CleanWebpackPlugin()]

运行 webpack 打包命令 => 之前的文件就不会存在了;

Webpack 常用插件 html-webpack-plugin

自动生成使用打包结果的 HTML 文件, 在这之前 HTML 代码都是通过硬编码的方式单独存放在项目的根目录下 => 这种方式有两个问题:

  1. 在项目发布时需要同时发布根目录下的 HTML 文件和 dist 目录下的所有打包结果, 这样相对麻烦, 而且上线之后还需要确保 HTML 中的路径引用都是正确的;
  2. 如果输出的目录或者是输出的文件名(打包结果的配置发生了变化), HTML 代码中所引用的路径需要手动的修改;

=> 解决上述两个问题的最好的办法就是: 通过 Webpack 自动生成 HTML 文件, 也就是让 HTML 也参与到 Webpack 的构建过程, 在构建过程中 Webpack 知道生成了多少个 bundle, 会自动将打包的 bundle.js 添加到页面中
=> 这样, HTML 也输出到 dist 目录, 上线时只需将 dist 目录发布出去就可以了; => HTML 中对 bundle 的引用是自动注入的, 不需要手动的维护, 从而确保了引用路径的正确性;

=> 具体的实现方式: 借助 html-webpack-plugin 插件实现

安装 html-webpack-plugin

cnpm i html-webpack-plugin -D

修改 webpack.config.js

/*amend + *///html-webpack-plugin 默认导出的就是插件的类型, 不需要解构内部的成员;const HtmlWebpackPlugin = require('html-webpack-plugin')/*amend end */....,plugins: [  .....,  new HtmlWebpackPlugin()]

这样就完成了该插件的配置 => 通过 webpack 重新打包项目 => 打包过程就会自动生成 index.html 输出到 dist 目录
=> 通过 html-webpack-plugin 插件处理后, webpack.config.js 中 output 的 publicPath 就不需要了(删除之前配置中的 publicPath) => 根目录下写死的 index.html 也不需要的(删除)
=> 右后的 HTML 代码都是通过 Webpack 生成出来的;

html-webpack-plugin 选项

有了 html-webpack-plugin 之后就可以动态生成所需要的 HTML 文件, 但是项目的配置任然存在一些改进的地方:

  1. 对于默认生成生成的 HTML 文件的标题是需要修改的;
  2. 很多时候需要自定义页面上的源数据标签和基础的 DOM 结构;

对于简单的自定义可以通过修改 html-webpack-plugin 的属性来实现;

//webpack.config.js.....,  plugins: [    new CleanWebpackPlugin(),    new HtmlWebpackPlugin({      //通过传入对象参数修改 HTML 文件      title: 'WebpackPluginHtml', //设置 html 文件的title,      mate: {//已对象的形式设置 html 中的源数据标签        viewport: 'width=device-width, initial-scale=1.0'      }    })  ]

通过 Webpack 命令再次打包项目

生成的 html 文件的 title 和mate 就会根据配置文件的配置生成
=> 如果需要对该 HTML 文件进行大量的自定义, 更好的做法是在源代码中添加一个用于生成 HTML 文件的模板文件 => 让 html-webpack-plugin 根据模板生成页面 => 在 src 目录下创建 indexTemplate.html => 编辑该模板文件

<!-- 为 index.html 文件生成模板文件 --><!DOCTYPE html><html landg="zh-CN">  <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><%= htmlWebpackPlugin.options.title %></title>  </head>  <body>    <h1> <%= htmlWebpackPlugin.options.csaData %> </h1>  </body></html>

配置 webpack.config.js

.....,  plugins: [    new CleanWebpackPlugin(),    new HtmlWebpackPlugin({      title: 'WebpackPluginTitle',      csaData: '测试数据',      template: './src/indexTemplate.html'    })  ]

通过 webpack 重新打包项目

index.html 根据配置项输出正确的 index.html 文件;
=> 这里注意: 项目中同时使用 html-laoder 和 html-webpack-plugin 模板文件会解析不出来 => 解决方案: 将 模板文件的后缀修改为 .ejs, 同时修改 webpack.config.js 中相应的配置;

html-webpack-plugin 多实例

除了自定义输出内容, 同时输出多个页面文件也是一个非常常见的需求, 除非项目是一个单应用项目, 否则就一定需要多个 HTML 文件
=> 需要输出多个 HTML 文件, 通过 HtmlWebpackPlugin 创建新的实例对象用来创建额外的 HTML 文件;
=> Ex: 创建一个新的实例 => 用于创建 about.html 页面文件

编辑 webpack.config.js

.....,  plugins: [    new CleanWebpackPlugin(),    //用于生成 index.html    new HtmlWebpackPlugin({      title: 'WebpackPluginTitle',      csaData: '测试数据',      template: './src/indexTemplate.html'    }),    //用于生成 about.html    new HtmlWebpackPlugin({      filename: 'about.html' //指定输出的文件名,默认值为 index.html    })  ]

通过 webpack 重新打包项目

在 dist 目录下生成了两个 HTML 文件: index.html、about.html => 根据尝试, 如果需要创建多个页面, 需要在插件列表中使用多个 HtmlWebpackPlugin 的实例对象, 每个对象负责生成一个页面文件;

Webpack 常用插件: copy-webpack-plugin & 总结

在项目中一般含有不需要参与构建过程的资源文件, 他们最终也需要发布到线上: Ex: favicon.ico => 一般这一类的文件统一放在 根目录下的 public 文件中 => 希望 webpack 在打包项目时将 public 文件下的文件一并复制到输出目录 dist
=> 对于这个问题, 解决方案: 借助于 copy-webpack-plugin 插件解决;

安装 copy-webpack-plugin

cnpm i copy-webpack-plugin -D

配置 webpack.config.js

const CopyWebpackPlugin = require('copy-webpack-plugin')......,  plugins: [    ......,    //该类型的构造函数要求出入一个数组: 用于指定需要拷贝的文件路径, 可以是一个通配符、目录、文件的相对路径    new CopyWebpackPlugin({    	patterns: [            { from: "./public", to: "./" }         ]    })  ]

总结: 至此, 我们了解了几个 Webpack 常用的插件, 这几个插件一般的项目中都可以用到; 当你需要其他的插件时, 可以提取需求的关键词去 GitHub 上搜索 => 虽然每个插件的作用不尽相同,但是他们的用法都是类似的

1.15、 Webpack 开发一个插件

相对于 Loader , Plugin 拥有更宽的能力范围, 因为 Loader 只是在加载模块的环节去工作, 而插件的作用范围几乎可以触及到 Webpack 工作的每一个环节;
=> 那插件机制是如何实现的勒? => Webpack 的插件机制就是在软件开发中最常见的钩子机制(有点类似于 Web 当中的事件), 在 Webpack 工作过程中, 有很多的环节, 为了便于插件的扩展, Webpack 几乎给每一个环节都买下了一个钩子, 这样的话, 我们在开发插件的时候就可以通过往这些不同的节点上挂载不同的任务就可以轻松的扩展 Webpack 的能力了; 具体有哪些预先定义好的钩子, 可以参考官方 API 文档 => www.webpack.org/api/compier…

开发一个插件

定义一个插件, 看看具体如何玩钩子上挂载任务, Webpack 要求插件必须是一个函数或者是一个包含 apply 方法的对象 => 一般都会将插件定义为一个类型, 在这个类型中定义一个 apply 方法, 使用时: 通过这个类型构建一个示例, 然后使用;

编辑 webpack.config.js => 开发插件

const ......;//定义一个类class MyPlugin {  //定义一个 apply 方法: 该方法会在 Webpack 启动时自动被调用 => 接受一个 compiler 对象参数, 该参数就是 Webpack 工作过程最核心的一个对象, 里面包含了此次构建的所有配置信息, 我们也是通过该对象去注册钩子函数  //=>这里的需求是该插件可以用来清除 Webpack在打包生成生成的 JS 代码中那些没有必要的注释 => bundle.js 当中去除了这些注释之后,就可以更加容易阅读;  //有了需求之后, 需要明确该插件的执行时机, 也就是要把这个插件挂载在哪个钩子上面; 需求是删除 bundle.js 中没有必要的注释, 意思就是需要 bundle.js 明确之后才可以实施相应的动作;  //在官网中查找 API => 找到 emit 钩子, 根据文档提示, 发现该钩子在 Webpack 即将要玩输出目录输出文件时执行, 非常符合需求;  apply (compiler) {    console.log('myPlugin 启动')    //通过 compiler.hooks 访问到 emit 钩子函数 => 通过 tap 方法注册一个钩子函数: 该函数接受两个参数 => 第一个是插件的名称, 第二个是需要挂载在插件上的函数     compiler.hooks.emit.tap('MyPlugin', compilation => {      // compilation: 可以理解为此次打包的上下文, 所有打包过程中产生的结果都会放到该对象中      // compilation.assets: 获取即将写入目录当中的资源文件信息, 是一个对象      for (const name in compilation.assets) {        console.log(name)        // 通过 name 获取输出的文件名称        //通过 compilation.assets[name].source() 获取输出文件内容        console.log(compilation.assets[name].source())        if (name.endsWith('.js')) {          const contents = compilation.assets[name].source()          const widthoutComments = contents.replace(/\/\*\*+\*\//g, '')          compilation.assets[name] = {            source: () => widthoutComments,//重新赋值输出文件内容            size: () => widthoutComments.length //用来范围新内容的大小, 该方法是 Webpack 内部要求的必需的方法          }        }      }    })  }  }....,plugins: [  .....,  new Myplugin()]

通过 Webpack 命令重新打包项目 => 查看 bundle.js, 对于的注释都被移出掉;

通过开发一个插件的机制: 插件是通过在生命周期的钩子中挂载函数实现扩展, 如果你要深入了解插件机制, 你需要去理解 Webpack 底层的原理

1.16、Webpack 开发体验问题

在此之前我们已经了解了 Webpack 的相关概念和一些基本的用法, 但是以目前的状态应对日常的开发工作还远远不够 => 因为编写源代码 -> 通过 Webpack 打包 -> 运行引用 -> 刷新浏览器

这种周而复始的方式过于原始 => 如果在实际开发中还按照这种方式使用, 必然会大大降低开发效率;
=> 究竟该如何提高我们的开发效率勒?
=> 设想: 理想的开发环境
1. 环境必须能够使用 以 HTTP Server 运行, 而不是以文件的形式预览: 这样做更加接近生产环境的状态; 可以使用 ajax 之类的 API (以文件的形式使用这些 API 是不被支持的);
2. 在环境中: 修改源代码后, Webpack 就可以自动构建 -> 浏览器可以及时显示最新的结果 => 这样大大减小在开发过程中那些重复操作;
3. 希望环境能够提过 Source Map 支持: 这样运行过程中一旦发生错误, 就可以错误的堆栈信息快速定位源代码中的位置 -> 方便开发阶段调试应用;
=> 对于以上的设想需求, Webpack 都已经有相对应的功能实现了;

接下来学习: 如何增强 Webpack 开发体验

Webpack 实现自动编译

目前每次修改完源代码都是通过命令行手动重复运行 Webpack 命令从而得到最新的打包结果, 这种方法特别的麻烦
=> 通过 Webpack CLI 提供的 watch 工作模式解决这个问题 => 在这种模式下, Webpack 会监听文件变化, 自动重新打包

启动 webpack 命令打包时, 在命令行后面添加 --watch 参数

yarn webpack --watch

运行命令之后, Webpack 就会以监听的模式运行, 打包完成后 Webpack CLI 不会立即退出, 他会监听文件的变化, 一旦源文件发生变化, 就会重新打包 , 直到结束这个命令; => 在这种模式下, 开发者只需要专注编码, 不必在手动完成重复的编译工作; => 重新打开一个终端打开 server , 当源代码更新重新打包后刷新浏览器就可以看到最新的效果了;

Webpack 实现自动刷新浏览器

此时的开发体验就是: 完成了源代码的修改, Webpack 就会自动执行编译 => 手动刷新浏览器 => 预览运行结果;
=> 开发者希望编译之后自动刷新浏览器, 开发体验会更上一层楼;
=> 通过 BrowserSync 工具完成浏览器自动属性

全局范围安装 BrowserSync

cnpm i browser-sync -g

通过 --watch 模式启动编译服务 => 通过 browser-sync 启动 HTTP 服务并同时监听 dist 目录下的文件变化

browser-sync dist --files "**/*"

启动之后修改源文件 => webpack CLI 会自动编译源代码到 dist 目录 => browserSync 监听 dist 目录文件有变化 => 重新刷新浏览器;
=> 这样做的弊端:
1. 操作太麻烦了: 因为开发者需要同时使用两个工具;
2. 开发效率降低了: 因为这个过程中 Webpack 会不断将文件写入磁盘, BrowserSync 在从磁盘中读出来 => 这样, 一次都会多出两次磁盘读写操作 => 降低开发效率;

Webpack Dev Server

Webpack Dev Server 是 Webpack 官方推出的一个开发工具, 根据他的名字 -> 他提供用于开发的 HTTP Server, 并且集成 [自动编译] 和[自动刷新浏览器] 等一些列对开发友好的功能, 可以使用该工具直接解决 BrowserSync 的弊端: 将打包结果暂时存放在内存中, 通过内存的读写实现自动编译和刷新浏览器;
=> 这是一个高度集成的工具, 使用起来很简单

安装 Webpack Dev Server

cnpm i webpack-dev-server -D

安装完成后, 该工具为开发者提供了 webpack-dev-server 的 CLI 程序 => 可以直接通过 yarn 运行该 CLI 程序或者将该 CLI 程序定义到 scripts 中;

运行 webpack-dev-server 命令

yarn webpack-dev-server

内部会自动使用 webpack 打包项目应用, 并启动一个 HTTP Server 运行打包结果 => 运行之后会监督代码的变化, 一旦源文件发生变化就会重新打包并更新到浏览器上;
=> Webpack Dev Server 为了开发效率并没有将打包结果放入磁盘中, 而是将打包结果暂时存放在内存中 -> 内部的 HTTP Server 从内存中读取内容 -> 发送给浏览器; => 这样减少了很对磁盘读写操作, 从而大大提高了开发效率;

通过 --open 命令行参数自动开启浏览器

yarn webpack-dev-server --open

**高版本webpack (v4)及以后版本将不支持 webpack-dev-server, 解决方案: 在 package.json 的 scripts 标签中添加命令: **

//package.json.....,"scripts": {  .....,  "devServer": "webpack server --open"//我的命令行}
Webpack Dev Server 静态资源访问

Webpack Dev Server 默认会将构建结果输出的文件全部作为开发服务器的资源资源文件, 也就是说: 只要是通过 Webpack 打包输出的文件都可以被正常访问到 => 还有一些静态资源也需要作为开发服务器的资源被访问的话, 就需要额外的告诉 Webpack Dev Server
=> 具体的方法: 在 Webpack 的配置文件中添加对应配置;

//webpack.config.js....,//添加 devServer 属性 => 专门为 Webpack Dev Server 指定相应的配置 devServer: {   contentBase: './public', //指定额外的静态资源路径: 该属性值可以是个字符串, 也可以是个数组(可以配置一个或者多个路径) }

在 webpack.config.js 中设置了 devServer 属性, 在这之前已经通过 copy-webpack-plugin 插件将静态资源拷贝到了输出目录; => 设置了 devServer 属性, 服务器端就可以直接访问静态资源路径中的静态资源, 那么 就不需要通过 copy-webpack-plugin 拷贝静态资源 => 实际使用 Webpack 时, 一般都会把 copy-webpack-plugin 这样的插件留到上线前的那次打包中使用, 在平时的开发过程中不会使用 copy-webpack-plugin (因为在开发时会频繁重复的执行打包任务 -> 如果拷贝的文件比较多或比较大, 每次都去执行 copy-webpack-plugin , 打包过程的开销就会比较大, 速度也就降低了) => 在开发时不去使用 copy-webpack-plugin , 在上线前的打包中使用 copy-webpack-plugin 的设置在后续会介绍;

注释掉 copy-webpack-plugin

//webpack.config.js....,  //开发阶段最好不要使用 copy-webpack-plugin  // new CopyWebpackPlugin()

再次执行 webpack-dev-server

yarn webpack-dev-server

此次执行完之后, public 里面的文件并没有被拷贝到输出目录, public 里面的文件一样被访问到;

Webpack Dev Server 代理 API 服务

由于开发服务器的缘故, 本地应用会运行在 localhost 的端口上 => 而最终上线后 -> 应用和 API 会布置到同源地址下面 => 这样就会有一个非常常见的问题: 在实际生产环境中, 可以直接访问 API -> 但是开发环境中, 就会发生跨域请求问题
=> 解决方案: 1、使用跨域资源共享(CORS) 解决该问题; => 如果请求的 API 支持 CORS , 这个问题就不成立了 => 并不是任何情况下的 API 都支持 CORS ;
2、在开发服务器配置代理服务( 将接口服务代理到本地的开发服务地址 )

=> Webpack Dev Server 支持配置代理

目标: 将 Github API 代理到开发服务器

Github 接口的 Endpoint(可以理解为接口断点/入口)一般都是在 根目录下, Ex: users 的 Endpoint => api.github.com/users;

配置 webpack.config.js

知道了接口的地址只有, 配置 webpack.config.js

.....,  devServer: {    contentBase: './public',    proxy: {//专门添加代理服务配置, 对象内的每一个属性就是一个代理规则的配置      '/api': {        //http://localhost:8080/api/users -> https://api.github.com/api/users: 请求的路径是什么, 代理的路径地址是会完全一致的 => 而实际的地址为: https://api.github.com/users => 对代理路径的 /api 需要通过重写的方式去掉        target: 'https://api.github.com', //代理的目标路径               pathRewrite: {            '^/api': ''//规则: 将以 /api 为开头的路径替换为 ''          }, //实现代理路径的重写            changeOrigin: true //原因: 默认代理服务器会以实际在浏览器中请求的主机名(localhost:8080) 作为代理请求的主机名 -> 在浏览器端对代理后的地址发起请求, 请求背后还是会去请求到 github 上, 该请求默认的主机名是本地的 localhost => 服务器是需要通过主机名判断请求的来源, 服务器就会将资源发送到来源地址(主机名) => localhost:8080 对于服务器肯定是不认识的 => 所以这里需要修改, 当该属性为 true 时, 请求会以实际代理主机名去发起请求 => 这时主机名就会保持 api.github.com 的原有状态      } //属性的名称就是需要被代理的请求路径前缀 -> 请求以哪个地址开始, 就会触发代理请求 => 属性值: 是为属性名称所匹配的到的代理规则配置        }  }

重新运行 webpack-dev-server

yarn webpack-dev-server

浏览器上请求 localhost:8080/api/users 就会被代理到 github.com/users 接口 => 通过代理之后去请求接口 => 不会出现跨域问题;

1.17、Source Map

通过构建、编译之类的操作, 可以将开发阶段的源代码转化为可以在生产环境运行的代码 => 这是一种进步, 在进步的同时也就意味着运行代码与源代码之间完全不同 => 这种情况下, 调试应用, 或者是运行过程中出现错误 => 这时将无从下手, 这时因为无论是调试还是报错, 都是基于运行代码进行的, 无法定位问题所在问题;
=> Source Map: 解决上述问题最好的办法, 名字就很好的解释了它的作用 -> 源代码地图, 用来映射运行代码于源代码之间的关系 => 一段运行代码通过转换过程中生成的 Source Map 文件就可以逆向定位到源代码

=> 目前很多第三方的库在发布的文件中都会包含 .map 后缀的 Source Map 文件 -> Ex: jquery;

下载 jquery, 查看 jquery.xxxx.min.map

主要有这几个属性:

  1. version: 指的当前文件所使用的 Source Map 标准的版本;
  2. sources: 记录的转换之前源文件的名称, 因为可能是多个文件合并转换为了一个文件, 所以该属性值为数组;
  3. names: 指的源代码中使用的一些成员名称 -> 在压缩代码时, 会将开发阶段有意义的变量名替换为简短的字符, 从而压缩整体代码的体积, 该值中记录的是源代码对应的名称;
  4. mappings: 是整个 Source Map 的核心属性, 他是一个 base64- VLQ 编码的字符串, 记录的是运行代码和源代码之间说对应的映射关系;

有了 Source Map 文件之后, 在运行代码的最后一行通过添加一行注释来引用该 Source Map 文件 => Source Map 特性只是为了帮助开发者更容易调试和定位错误的 -> 对生产环境没有太大的意义 -> 在最新版的 jquery 中已经去掉引用 Source Map 的注释
=> 手动添加 Source Map 文件应用

//运行代码........,  //# sourceMappingURL = xxxxxx

代码中 xxxx: jquery.3.4.1.min.map 为 source Map 文件地址;
=> 这样, 在浏览器中打开开发者工具, 运行代码最后有 Source Map 注释, 就会自动去请求 Source Map 文件, 然后根据文件的内容逆向解析出对应的源代码, 方便开发者的调试 -> 因为有了映射的关系, 如果运行代码中出现了错误, 也就很容易定位到源代码中错误的位置;

Source Map 解决了源代码与运行代码不一致说产生的问题

Webpack 配置 Source Map

Webpack 打包过程同样支持为打包结果生成对应的 Source Map 文件, 用法上也非常简单 => 不过, Webpack 为 Source Map 提供了很多不同的模式, 很多初学者会比较蒙;
=> 接下来, 就研究一下 Webpack 中如何配置使用 Source Map 以及 不同模式之间的差异:

修改 webpack 配置文件

//webpack.config.js//原始代码 ......,//devtool: 用来配置开发过程中的辅助工具 -> Source Map 相关的配置在该属性里配置devtool: 'source-map'

运行 webpack 命令打包项目

yarn webpack

打包完成之后 => 在打包目录下就会层层 bundle.js 和 它对应的 Source Map 文件 => 打开 bundle.js 在其最后一行, 通过注释的方式引用了 bundle.js.map

通过 serve 工具将打包结果运行起来

serve dist

在浏览器里, 就可以通过 Console 里面的提示直接定位到错误所在的源代码位置, 如果你需要调试源代码, 这里就实现了; 如果只是需要使用 Source Map 这里也实现了 -> 但是, 你只是这样使用的话, 实际效果就会差的比较远;
=> 原因: Webpack 支持 12 种不同方式的 Source Map, 每种方式的效率和效果各不相同 -> 很简单的道理: 生成速度快的, 效果会很差; 生成速度慢的, 效果会很好;
=> 具体哪种方式是最适合开发人员的, 还需要继续探索

Webpack 配置 Source Map - eval 模式下的 Source Map

Webpack 配置选项中的 devtool 除了可以使用 source-map 这个值, 还支持很多其他的模式 -> 具体的可以参考:

=> 该表分别从初次构建速度、监视模式重新打包速度、是否适合在生产环境中使用以及所生成的 Source Map 文件的质量四个维度对比了不同模式之间的差异;
=> 表格的对比不够明显, 我们通过不同的尝试来了解不同模式下的差异 -> 从而找到适合自己的最佳实践;

Source Map: eval 模式

eval 是 JS 中的函数, 可以用来运行字符串中的 JS 代码 => eval 运行 JS 代码, 默认情况是运行在临时的虚拟环境中, 开发这可以使用 sourceURL 申明代码所属的文件路径;

=> 开发者可以通过 sourceURL 修改 eval 执行空间的名称, 了解了这个特点以后;

修改 webpack 配置文件

devtool: 'eval'

通过 webpack 重新打包项目

yarn webpack

通过 serve 运行打包结果 dist

serve dist

此时通过 Console 的提示, 可以找到错误出现的文件 -> 但是打开该文件, 确实打包之后的模块代码
=> 因为在 eval 模式下会将每个模块转化过后的代码放在 eval 函数中执行, 并且在 eval 执行函数的最后通过 sourceURL 的方式去说明所对应的文件路径 => 这样的话, 浏览器通过 eval 执行这段代码时就知道代码所对应的源代码是哪个文件 => 从而实现定位错误出现的文件(只能定位文件); => 在 eval 模式下不会生成 Source Map 文件 -> 构建效果很快 -> 效果: 只能定位错误源代码的文件, 不能定位错误的行列信息;

Webpack 不同 devtool 之间的差异

为了可以更好的比较不同模式下的 Source Map 之间的差异, 使用一个新的项目同时创建不同模式下的打包结果, 通过具体的实验横向对比之间的差异;
=> 项目目录: src/heading.js、src/main.js

分别编写 heading.js、main.js

//heading.jsexport default () => {  console.log('Heading ~')  const element = document.createElement('h2')  element.textContent = 'Hello World'  element.classList.add('heading')  element.addEventListener('click', () => {    alert('Hello Webpack')  })    return element}
//main.jsimport createHeading from './heading.js'const heading = createHeading()document.body.append(heading)console.log('main.js running')//故意放置一个错误console.log11('main.js error')

安装 webpack, babel-loader, html-webpack-plugin

cnpm i webpack webpack-cli babel-loader @babel/core @babel/preset-env html-webpack-plugin -D

编辑 webpack 配置文件

//webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin')//Source Map 模式数组const allModes = [  'eval',  'cheap-eval-source-map',  'cheap-module-eval-source-map',  'eval-source-map',  'cheap-source-map',  'cheap-module-source-map',  'inline-cheap-source-map',  'inline-cheap-module-source-map',  'source-map',  'inline-source-map',  'hidden-source-map',  'nosources-source-map']//Webpack 的配置对象可以是数组, 数组中的每个元素都是一个单独的打包配置 -> 可以在一次的打包过程中同时执行多个打包任务module.exports = allModes.map(item => {  return {    devtool: item,    mode: 'none',    entry: './src/main.js',    output: {      filename: `js/${itme}.js`    },    module: {      rules: [        {          test: /\.js$/,          use: {            loader: 'babel-loader', //配置 babel-loader 的目的: 在待会的对比中能够辨别其中一类模式的差异            options: {              presets: ['@babel/preset-env']            }          }        }      ]    },    plugins: [      new HtmlWebpackPlugin({        filename: `${item}.html`      })    ]  }})

通过 webpack 命令打包项目

yarn webpack

通过 serve 运行打包结果

serve dist

此时在页面中看到所有不同模式下的 html, 如果刚刚没有将 JS 输出到一个单独的目录 -> 页面上的文件就会很繁杂;

Webpack 对比不同 devtool 模式

有了之前的打包结果之后 => 就可以一个一个仔细对比每个模式之间的差异;
=> 这里先看几个典型的模式:

eval 模式

将模块代码放到 eval 函数中去执行, 并且通过 sourceURl 标注模块文件的路径;
=> 这种该模式下不会生成 Source Map 文件, 只能定位错误代码的文件;

eval-source-map 模式

该模式也是通过 eval 函数去执行模块代码 => 不同的是: 除了定位错误代码的文件, 还可以定位错误代码具体的行列信息
=> 这种模式相比较 eval 模式: 会生成 Source Map 文件

eval-cheap-source-map 模式

cheap -> 廉价的、便宜的、阉割版的(计算机):该模式虽然也生成了 Source Map 文件, 定位错误文件、定位到错误代码的行信息, 定位不到错误代码列信息 -> 这里的错误代码文件是通过转换后的;

eval-cheap-module-source-map 模式

对比 eval-cheap-source-map 模式:
eval-cheap-module-source-map: 定位错误文件、定位错误代码行信息, 定位不到错误代码列信息; 所定位的错误代码文件和源代码是一模一样的;
cheap-eval-source-map: 定位错误文件、定位错误代码行信息, 定位不到错误代码列信息; 锁定为的错误代码文件是经过 ES6 转换后的结果;

带有 module 的模式, 解析出来的源代码不会经过 ES6 加工的, 是真正的源代码; 不带 module 的模式是经过 ES6 加工后的结果;

总结: 1. eval - 是否使用 eval 执行模块代码;

  1. cheap - Source map 文件是否包含行信息;
  2. module - 是否能够得到 Loader 处理之前的源代码;

其他模式无外乎就是将这几个特点重新排列组合;

cheap-source-map 模式

没有 eval: 没有使用 eval 的方式去执行模块代码;
没有 module: 返回的源代码是经过 ES6 处理后

inline-source-map 模式

和普通的 Source Map 效果是一样的, 只不过该模式下的 Source Map 文件是以 data-url 的方式存在于文件的末尾 => Source Map 模式下的 Source Map 文件是以物理文件的方式存在;
这种模式的弊端: 打包结果的文件体积会打很多;

Hidden-source-map 模式

该模式下在开发者工具里面是看不到 Source Map 的效果的, 但是在打包结果中确实生成了 Source Map 文件 => 在构建中生成了 Source Map 文件, 在代码中并没有通过注释的形式引用 Source Map 文件; => 该模式一般用于开发第三方包 -> 需要去生成 Source Map 文件, 但是不想在代码中直接引用 Source Map 文件, 当使用开发包出现了问题, 手动引入 Source Map 文件;

nosources-source-map 模式

没有源代码模式: 能够看到错误出现的文字, 点击错误信息之后是看不到源代码的; => 同样为开发者提供了错误代码的行列信息;
=> 为了在生产环境中保护源代码不被暴露;

选择合适的 Source Map

虽然 Webpack 支持各种各样的 Source Map 模式, 掌握了这些模式的特点后发现: 一般在应用开发时只会用到其中的几种, 根本没有选择上需要纠结的点 => 这里说一说个人选择经验:
1. 开发模式(开发环境): 选择 cheap-module-eval-source-map, 具体原因:

  1. 代码每行不会超过 80 个字符;

  2. 代码使用框架的情况多 => 代码经过 Loader 转换后的差异较大;

  3. 首次打包速度慢无所谓, 重写打包相对较快;

2.生产模式(生产环境): 选择 none 模式: 因为 1.Source Map 会暴露源代码到生产环境中, 这样会有风险; 2.调试是开发阶段的事情: 应该在开发阶段尽可能把所有问题和隐患都找出来, 而不是到了生成环境去公测 => 如果实在需要, 使用 nosource-source-map 模式;

理解不同模式的差异, 是为了在不同环境中快速选择合适的模式

1.18、Webpack 自动刷新的问题

在此之前, 已经简单了解了 Webpack Dev Server 的基本用法和特性 -> 主要是使用 Webpack 构建的项目提供一个友好的开发环境和可以用来调试的开发服务器; => 使用 Webpack Dev Server 可以让开发过程更加专注于编码 -> 因为它可以见识代码的变化 => 自动进行打包 => 自动刷新同步到浏览器 => 及时预览;
=> 当实际使用这样的特性去完成一些任务时, 会发现一些很不舒服的地方 -> 例如: 一个编辑器的项目, 开发者希望能够及时调试编辑器中文本内容的样式 -> 正常的操作: 先在编辑器中添加一些文本作为展示样例 , 然后修改源代码改变文本的样式 => 问题: 当修改 css 以后, 页面自己添加的样例文字被刷新掉了; => 每次修改完样式, 都需要重新输入测试文字 -> 这样就很繁琐, 让开发着觉得自动刷新很鸡肋(因为每次修改完 css , Webpack Dev Server 监听到文件的变化, 自动打包, 然后刷新页面) => 一旦页面整体刷新, 之前的测试文字就会被刷新掉
=> 问题核心: 自动刷新导致页面状态丢失
=> 希望: 页面不刷新的前提下, 模块也可以及时更新;
=> 解决方案: Webpack HMR

Webpack HMR 介绍

HMR: Hot Module Replacement -> 模块热替换(模块热更新) -> 这里的热和计算机中 热拔插(在一个正在运行的机器上随时插拔设备, 而机器的运行状态不会受插拔设备的影响, 同时插上的设备可以立即开始工作) 的热同义 -> 应用运行过程中实施替换某个模块, 而应用的运行状态不受影响 => 解决了自动刷新导致页面状态丢失的问题 => 热替换只将修改的模块实施替换至应用中, 不必完全刷新应用;

对于项目中除了 CSS 以外的其他项目的修改, HMR 也可以有相同的体验; 不仅如此, 对于项目中的非文本文件, 同样也可以使用热更新;
HMR 是 Webpack 中最强大的功能之一, 同时也是 最受欢迎的的特性 -> 极大程度的提高了开发着的工作效率

开启 HMR

对于 HMR 强大的功能而言, 它的使用并不算特别复杂
=> HMR 已经集成在 webpack-dev-server 中, 在使用中不需要额外安装其他模块

  1. 需要在运行 webpack-dev-server 命令时添加 --hot 命令行参数;

    yarn webpack-dev-server --hot
    
  2. 过配置文件开启: 配置 webpack.config.js 中 devServer 的 hot 属性为 true => 载入 Webpack内置插件: HotModuleReplacementPlugin

    //配置 webpack.config.js 开启 HMRconst webpack = require('webpack')//原 webpack.config.js 配置信息: .....,devServer: {  hot: true},//原 webpack.config.js 配置信息: .....,plugins: [  .....,  new webpack.HotModuleReplacementPlugin()]
    

    执行 webpack-dev-server 命令 启动服务器

    yarn webpack-dev-server --open
    

现在就可以体验 HMR 特性了
=> 修改样式文件, 保存之后, 样式文件就可以以热更新的方式直接作用到页面上
=> 修改 JS 文件, 保存之后 => 发现页面自动刷新了, 并没有热更新的体验;
=> 该如何去实现所有模块的热替换?

HMR 的疑问

通过上面的体验, 发现: 模块化热替换确实提供了非常友好的开发体验, 但是效果却不尽如人意 => 原因: Webpack 中的 HMR 并不是开箱即用 -> Webpack 中的 HMR 需要手动处理模块热替换逻辑(当模块更新后, 如何将更新的模块替换到页面中) -> 这里提出几个问题:
Q1: 为什么央视文件的热更新开箱即用?
=> 样式文件是经过 loader 处理的, 在 style-loader 中已经自动处理了样式文件的热更新;
Q2: 凭什么样式可以自动处理?
=> 样式文件更新后, HMR 只需要及时将样式文件替换到页面中就可以覆盖之前的样式 => 而 JS 模块是没有任何规律的 -> 一个模块可以导出不同类型的对象(对象、字符串、函数) => 对导出成员的使用逻辑也各不相同;
Q3: 我的项目没有手动处理, JS 照样可以热替换?
=> 你使用了某个框架 => 框架下的开发, 每种文件都是有规律的 => 通过脚手架创建的项目内部都集成了 HMR 方案;

HMR APIs

hot-module-replacement-plugin 为 JS 提供了一套用于处理 HMR 的 API => 在代码中使用这套 API 处理当模块更新后如何替换;

使用 HMR APIs 手动更新模块并替换: => 编写 main.js

//mian.js//在 main.js 中 加载了其他的模块, 因为在 main.js 导入了其他模块, 当其他模块更新后就必须替换其他模块;.....,//HMR APIs 为 module 对象提供了 hot 对象;// hot 对象: HMR API 的核心对象, 提供 accept 方法 -> 用于注册模块更新后的处理函数 => 第一参数: 依赖模块的路径; -> 第二个参数: 依赖路径更新后的处理函数;module.hot.accept('./editor.js', () => {  console.log('editor 模块更新了, 需要手动处理热替换逻辑')})

通过 webpack-dev-server 启动项目

yarn webpack-dev-server --open

修改 editor 模块

//editor.js....,console.log(111)

浏览器控制台就会打印 editor 模块更新了..... => 并且不会触发自动刷新页面;
=> 模块的更新被手动处理了, 就不会触发自动刷新了 -> 没有手动处理, 模块更新后就会触发自动刷新;

手动处理 JS 模块热替换

了解了这个 API 后, 这里具体来实现 editor 模块热替换逻辑;
=> 该模块导出的是一个函数, 该模块更新后需要移出原来的元素 => 重新调用函数执行 => 追加到页面上 => 记录新创建的元素(方便以后下次替换找到)

修改 main.js

import createEditor from './editor'import background from './better.png'import './global.css'const editor = createEditor()document.body.appendChild(editor)const img = new Image()img.src = backgrounddocument.body.appendChild(img)let lastEditor = editeormodule.hot.accept('./editor.js', () => {  document.body.removeChild(editor)  const newEditor = createEditor()  document.body.appendChild(newEditor)  lastEditor = newEditor})

修改 editor 模块

保存修改之后, 页面就可以热替换了; => 然后在编辑器中添加一些测试文字 => 保存修改 editor 模块后, 测试文字被刷新掉了 => 原因: 之前的编辑器元素已经被移移除掉了, 之前的状态也就丢失了 => editor 模块的热替换逻辑还需要优化
=> 在替换掉原模块之前将原模块的状态保存下来 => 将编辑器之前的内容存下来 => 替换之后在返回去;

编辑 main.js

.....,let lastEditor = editormodule.hot.accept('./editor.js', () => {  const editorValue = lastEditor.innerHTML  document.body.removeChild(editor)  const newEditor = createEditor()  newEditor.innerHTML = editorValue  document.body.appendChild(newEditor)  lastEditor = newEditor})

这样就解决了文本框状态的丢失问题; => 保存修改 editor 模块后就可以热替换了;

注意: 这并不是一个通用的处理逻辑, 该逻辑只适用于当前的 editor 模块; => 不同的模块有不同的业务逻辑, 不同的业务逻辑的处理热更新的逻辑也不一样

Webpack 处理图片模块热替换

相比较于 JS 模块的热替换, 图片模块的热替换逻辑就会简单的多;

编辑 main.js

import createEditor from './editor'import background from './better.png'import './global.css'const editor = createEditor()document.body.appendChild(editor)const img = new Image()img.src = backgrounddocument.body.appendChild(img)//  以下用于处理 HMR, 与业务代码无关// editor 模块 JS 热替换逻辑let lastEditor = editeormodule.hot.accept('./editor.js', () => {  document.body.removeChild(editor)  const newEditor = createEditor()  document.body.appendChild(newEditor)  lastEditor = newEditor})// 图片模块 热替换module.hot.accept('./better.png', () => {  img.src = background})

修改图片 better.png 就可以实现图片的热替换;

以上就是想应用 针对与两种不同类型资源的热替换的逻辑, 可能你会觉得麻烦, 因为你要写一些额外的代码, 甚至有人会觉得不得不用; => 个人想法: 利大于弊, 对于一个长期开发的项目, 这点额外的工作并不算什么, 如果你能为自己的代码设计规律, 就可以实现一些通用的设计方案; => 如果你使用的是框架开发的话, 使用 HMR 将会十分简单, 因为大部分框架都有成熟的 HMR 方案

HMR 注意事项

刚开始使用 HMR , 肯定会遇到一些问题;

Q1: 处理 HMR 的代码报错会导致自动刷新

在写 HMR 逻辑代码时, 代码发生错误, 会导致页面自动刷新, 逻辑代码里的错误定位不了; => 推荐使用 hotOnly 模式: hot 模式下 -> 如果热替换失败, 会自动回退回去使用自动刷新功能; hotOnly 模式, 热替换失败, 不会使用自动刷新功能;

修改 webpack.config.js

.....,devServer: {  hotOnly: true //hot: true => hotOnly: true}

重新启动 webpack-dev-server

此时热替换逻辑代码的错误就可以定位了;

Q2: 没启用HMR 的情况下, HMR API 报错

在代码中使用了 HMR API, 但是启动 webpack-dev-server 时没有开启 HMR 选项, 此时运行环境就会报错;

原因: module.hot 这个对象是 HMR 这个插件所提供的; => 解决方式: 先添加判断, 然后在书写 HMR 逻辑代码;

//main.js.....,//  以下用于处理 HMR, 与业务代码无关if (module.hot) {  .....,//HMR 逻辑代码}
Q3: 代码中多了一些与业务无关的代码

HMR 逻辑代码与业务本身无关, 对项目会不会有影响 => 压缩代码时会将热替换关闭掉, 并且会移出热替换插件 => 打包项目 => bundle.js 文件中 main.js 模块中关于热替换的逻辑代码是一个 if(false) 的空判断, 这种没有意义的空判断在压缩后也会被替换掉 => 所以, HMR 根本影响不到生产环境;

1.19、Webpack生产环境优化

前面了解到的一些用法和特性都是为了在开发阶段拥有更好的开发体验, 而更好的体验的同时勒, 打包的结果也会变得越来越臃肿 => 这是因为 Webpack 为了实现这些特性会自动往打包结果中添加一些额外的内容 => Ex: Source Map 和 HMR, 为了实现它们, 都会往打包结果中添加额外的代码来实现自己的功能 => 这些代码对于生产环境是冗余的 => 生产环境和开发环境有很大的差异, 在生产环境中注重运行效率, 而开发环境更注重开发效率 => 解决方案就是: 模式(mode), 同时 Webpack 也建议开发者为不同的工作环境创建不同的配置, 以便于打包结果可以适用于不同的生产环境
=> 接下来: 探索生产环境中有哪些值得优化的地方以及一些注意事项?

不同环境下的配置

接下来我们先来尝试为不同的工作环境创建不同的 Webpack 配置 => 创建不同环境下的配置的方式主要有两种:
1. 配置文件根据环境不同导出不同配置;

在配置文件中添加相应的判断条件, 根据环境的不同导出相对应的配置;

2.一个环境对应一个配置文件

为不同的环境单独添加一个配置文件 => 确保每个环境下都有一个对用的配置文件;

配置文件根据环境不同导出不同配置

修改 webpack.config.js

const webpack = require('webpack')const { cleanWebpackPlugin } = require('clean-webpack-plugin')const HtmlWebpackPlugin = require('html-webpack-plugin')const CopyWebpackPlugin = require('copy-webpack-plugin')// Webpack 的配置文件还支持导出一个函数, 在函数中返回配置对象 => 接受两个参数: env -> 通过 CLI 传递的环境名参数; argv -> 运行 CLI 过程中传递的所有参数;module.exports = (env, argv) => {  const config = {.....}//将开发模式的配置文件放在 config 中  if ('production' == env) {//生产环境的 env 就是 production    config.mode = 'production'    config.devtool = false    config.plugins = [      ...config.plugins,      new CleanWebpackPlugin(),      new CopyWebpackPlugin(['public'])    ]  }  return config}

不传递任务命令行参数 直接运行 webpack

yarn webpack

此时 webpack 会以开发模式运行打包 => 打包完成后, dist 目录下并没有 public 目录拷贝过来的文件;

传递命令行参数 --env production 运用 webpack

yarn webpack --env production

此时 Webpack 会以生产模式打包 => 打包完成后, public 目录下的文件已经拷贝到 dist 目录;
=> 当然也可以直接在全局去判断环境变量, 然后直接导出不同的配置;

不同环境对应不同配置文件

通过判断环境名参数返回不同的配置对象这种配置方式只适用于中小型项目 => 因为一旦项目变得复杂, 配置文件也会变得很复杂 => 所以对于大型的项目, 建议使用不同环境对应不同的配置文件;
=> 一般在这种方式下, webpack 一般会有三个配置文件, 其中两个是用来适配不同环境的, 另外一个是一个公共配置 => 因为开发环境和生产环境并不是所有的配置都不同

在项目的根目录下新建 webpack.common.js

用于存放不同环境下的公共配置

const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = {  entry: './src/main.js',  output: {    filename: 'js/bundle.js'  },  module: {    rules: [      {        test: /\.css$/,        use: [          'style-loader',          'css-loader'        ]      },      {        test: /\.(png|jpe?g|gif)$/,        use: {          loader: 'file-loader',          options: {            outputPath: 'img',            name: '[name].[ext]'          }        }      }    ]  },  plugins: [    new HtmlWebpackPlugin({      title: 'projTitle',      template: './src/index.ejs'    }),    new webpack.HotModuleReplacementPlugin()  ]}

在根目录下新建 webpack.dev.js

用于定义开发环境的配置文件;

在根目录下新建 webpack.prod.js

用于定义生产环境的配置文件;

const common = require('./webpack.common')const merge = require('webpack-merge')const { CleanWebpackPlugin } = require('clean-webpack-plugin')const CopyWebpaclPlugin = require('copy-webpack-plugin')//通过 Object.assign() 方法将公共配置对象复制到这里的配置对象中, 并且可以用文件中的一些对象覆盖公共配置对象中的配置; => Object.assign() 方法是完全覆盖前一个对象中的同名属性, 这样的特点对于值类型的属性覆盖是没有问题的, 当时配置中的像 plugins 这样的数组是希望在原有的基础上添加两个插件, Object.assign() 方法就不适合与这里了;// webpack-merge 是专门来解决合并问题的 => 安装 webpack-merge => cnpm i webpack-merge -Dmodule.exports = merge(common, {  mode: 'production',  plugins: [    new CleanWebpackPlugin(),    new CopyWebpackPlugin(['public'])  ]})

运行命令行进行打包

因为项目中已经没哟默认的配置文件, 所以需要指定命令行参数 -- config xxxx 进行打包;

yarn webpack --config webpack.prod.js

也可以将打包命令放在 package.json 的 scripts 中;

Webpack DefinePlugin

在 Webpack 4 中新增的 production 模式下, 内部自动开启了很多优化功能, 对于使用者而言, 开箱即用的体验是非常好的 => 对于学习者而言, 开箱即用会导致忽略很多需要了解的东西, 以至于出现问题后无从下手; 需要深入了解 webpack 的使用, 建议单独研究每一个配置背后的作用; => 这里先来学习其中几个优化配置, 顺便了解一下 Webpack 是如何优化打包结果的;

DefinePlugin 是为代码注入全局成员的, 在 production 模式下回默认启动, 并往代码中注入了 process.env.NODE_ENV 成员, 很多第三方模块都是通过这个成员判断当前的运行环境, 从而决定 例如 打印日志 等操作;
DefinePlugin 是一个内置的插件, 使用需要先导入 Webpack 模块;

编辑 webpack.config.js

const webpack = require('webpack')module.exports = {  mode: 'none',  entry: './src/main.js',  output: {    filename: 'bundle.js'  },  plugins: [    new webpack.DefinePlugin({      //构造函数接受一个对象, 该对象的每一个键值都会被注入到代码中      API_BASE_URL: 'https://api.example.com' //为代码注入 API 服务地址 Ex: https://api.example.com    })  ]}

编辑main.js

//main.jsconsole.log(API_BASE_URL)

运行 webpack 打包

yarn webpack

查看打包结果 => 查看 main.js 模块所对应的函数 => 发现 DefinePlugin 其实就是将注入成员的值直接替换到了代码中 => 而刚刚设置的值内容就是 httpsxxxxx, 字符串中并没有包含引号, 所以替换后代码报错 => 其实 DefinePlugin 的设计并不是只是替换一个数据进来 => 这里所传递的字符串内容实际要求是一段 JS 代码片段 => 这里将 API_BASE_URL 的值修改为 ‘“https:xxxxx”’ => 重新打包 => 现在代码正常;
=> 另外这里还有一个小技巧: 如果需要注入的是一个值 => 可以先通过 JSON.stringify() 的方式将值转换为表示这个值的代码片段;

Webpack 体验 Tree Shaking

Tree Shaking 字面意思就是摇树, 一般伴随着摇树这个动作 树上的枯枝叶会掉下来 => Webpack 中的 tree shaking 也是这个道理 只不过摇掉的是代码中那些没有用到的部分 更专业的说 未引用代码(dead-code) => Webpack 生产模式优化中就有这么一个非常有用的功能 他可以自动检测出代码中那些为引用的代码 然后移出他们;
=> 现在我们就来体验一下:
项目目录: src/components.js src/index.js

//components.jsexport const Button = () => {  return document.createElement('button')  console.log('dead-code')//该段代码是在 return 后, 属于未引用代码}export const Link = () => {  return document.createElement('a')}export const Heading = level => {  return document.createElement('h' + level)}
//index.jsimport { Button } from './components'//这里只导入了 components 中的 Button 成员 就导致了 components 中的 Link 和 Heading 成员未被引用 对于打包后的结果就是冗余的document.body.appendChild(Button())

运行 webpack 以 production 模式打包

yarn webpack --mode production

查看打包结果: 搜索打包结果中的 console.log 、Link 以及 Heading , 会发现未引用的代码并没有被打包 => 这就是 production 模式下的 tree Shaking 工作之后的效果 tree Shaking 会在 production 模式下自动开启;

Webpack 使用 Tree Shaking

需要注意的是: Tree Shaking 并不是 Webpack 中的某一个配置选项 他是一组功能搭配使用后的优化效果 这组功能会在生产模式下自动启用 => 这里介绍一下在其他模式下 如何一步一步的开启 顺便通过这一过程了解 Tree Shaking 的工作过程以及一些其他的优化功能

运行 webpack 以 none 模式打包

yarn webpack

打包结果中 一个模块会对应一个函数 在 components 模块对应的函数中 Link 函数以及 Heading 函数 虽然外部没有引用 但是仍然导出了 => 很明显 这些导出是没有意义的 => 通过一些配置属性将 未引用的代码在打包过程中去掉;

编辑 webpack.config.js

module.exports = {  mode: 'none',  entry: './src/index.js',  output: {    filename: 'bundle.js'  },  optimization: {    //该属性: 集中配置 Webpack 的优化功能    usedExports: true, //在输出结果中 只导出那些外部使用了的成员  }}

重新运行 webpack 以 none 模式打包

yarn webpack

打包结果: components 模块所对应的模块中不在会导出 Link 和 Heading 函数 而且 VScode 也非常友好的将这两个函数的字体变淡 表示他们未被使用 => 此时就可以开启 Webpack 的代码压缩功能 压缩掉这些没有用到的代码

编辑 webpack.config.js

.....,	optimization: {    usedExports: true,    minimize: true  }

再次运行 webpack 打包项目: 此时 bundle.js 中 未引用的代码就都被移出掉了 => 这就是 Tree Shaking 的实现 => 如果把代码理解为一颗大树的话 usedExports 负责标记 枯树叶 minimize 负责摇掉被标记的枯树叶;

Webpack 合并模块

除了 usedExports 以外 还可以使用 concatenateModules 属性继续优化输出文件 => 普通的打包结果是将每一个模块放在一个单独的函数中 如果模块很多 -> 也就意味着在输出文件中有很多模块函数 => concatenateModules 属性作用: 尽可能的将所有模块合并输出到一个函数中

编辑 webpack.config.js

.....,  optimization: {    usedExports: true,    concatenateModules: true,    //minimize: true 为了看到 concatenateModules 的作用 先关闭 minimize  }

运行 webpack 打包项目

打包结果: bundle.js 就不再是一个模块对应一个函数 而是所有的模块都放在了同一个函数中 => concatenateModule 作用: 尽可能的将所有模块合并输出到一个函数中 => 既提升了运行效率 有减少了代码的体积 => 这个特性又被称之为 Scope Hoisting 作用域提升 是 Webpack 3 中添加的特性 => 现在结合 minimize 打包代码的体积又会小很多

Tree-shaking & Babel

由于早期 Webpack 发展非常快 变化比较多 所以查找资料时得到的结果并不适用于当前的版本 => 对于 Tree-shaking 更是如此 很多资料都显示 如果使用了 Tree-shaking 就会导致 babel-loader 失效 => 针对于这个问题 这里统一说明一下:
首先: 需要明白的一点是 -> Tree-shaking 的实现前提是 ES Modules => 也就是说交给 Webpack 打包的代码必须使用 ESM的方式实现的模块化 => 为什么这么说勒 => 我们知道 Webpack 在打包所有模块之前先是将模块根据配置交给不同的 loader 处理 最后将所有 loader 处理之后的结果打包到一起 => 为了转换代码中的 ECMAScript 新特性 很多时候都会选择 babel-loader 处理 JS 而 babel 转换 JS 代码时就可能将代码中的 ES Modules 转换为 CommonJs (这取决于有没有使用转换 ES Modules 的插件 => 例如 preset-env 插件集合就有转换 ESM的插件 => 所以 当 preset-env 插件集合工作时 代码中 ESM 就会被转换为 CommonJs) => 转化后的代码是以 CommonJs 方式组织的代码 所以 Tree-shaking 就不能生效;
我们来尝试一下: 为了更容易分辨结果 optimization 中只开启 usedExports

//webpack.config.js.....,optimization: { usedExports: true}

运行 webpack 打包

yarn webpack

查看打包结果: bundle.js 发现 usedExports 功能正常的工作了 如果开启压缩代码 未引用的代码就会被剔除掉 Tree-shaking 并没有失效 => 这是因为最新版本的 babel-loader 中自动关闭了 ES Modules 转换的插件 => 可以在 node_modules 目录下找到 babel-loader 模块 => 在 lib/injectCaller.js 中已经标识了当前环境是支持 ES Modules 的

=> 找到 preset-env 这个模块 => 在这个模块中 根据刚刚的标识自动禁用了 ES Modules 的转换 => 所以 webpack 打包时 usedExports 生效了(如果你要仔细了解该功能 需要翻看 babel 的源代码) => 也可以在 babel-loader 的 presets 中强制开启转换功能

编辑 webpack.config.js

....,  module: {    relues: [      {      	test: /\.js$/,        use: {          loader: 'babel-loader',          options: {            presets: [              //注意这里是 二维数组: 第一个成员 -> 所使用的的 preset 的名称 第二个成员 -> 给该 preset 定义的配置对象              ['@babel/preset-env', { modules: 'commonjs'/* 默认该属性的值为 auto(根据环境判断是否开启 ESM 插件) */ }]            ]           }        }      }    ]  },....,

再次运行 webpack 打包 => 查看 bundle.js 发现 usedExports 没有生效

=> 即便开启压缩代码 Tree-shaking也不会生效

最新版本的 babel-loader 并不会造成 Tree-shaking 失效 将 webpack.config.js 中 babel-loader 的 presets 的modules 设置为 false => Tree-shaking 就不会失效

Webpack sideEffects
sideEffects 特性介绍

Webpack 4 中还新增了 sideEffects 特性 它允许通过配置的方式标识代码是否有副作用 从而为 Tree-shaking 提供更大的压缩空间 => 副作用: 模块执行时除了导出成员里之外所做的事情 -> sideEffects 一般在开发一个 npm 包时才会用到 用于 npm 包标记是否有副作用 => 但是官网中将 sideEffects 的介绍跟 Tree-shaking 混合到了一起 所以很多人认为他俩是因果关系 => 其实 sideEffects 和 Tree-shaking 并没有很大的关系 => 这里先将 sideEffects 弄明白 你就知道是为什么了
=> 项目目录: src/components/button.js、src/components/heading.js、src/components/index.js、src/components/link.js、src/extend.js、src/index.js

//button.jsexport default () => {  return document.createElement('button')	console.log('dead-code')}
//heading.jsexport default level => {  return document.createElement('h' + level)}
//link.jsexport default () => {  return document.createElement('a')}
//components/index.jsexport { default as Button } from './button'export { default as Link } from './link'export { default as Heading } from './heading'
//src/index.jsimport { Button } from './components'document.body.appendChild(Button())

这样写代码会出现一个问题: 在 src/index.js 中载入的是 components 目录下的 index.js components/index.js 又载入了所有的组件模块 => 这就导致我们只想导入 Button 组件 但是所有的组件模块都会加载运行

=> 运行 webpack 打包(yarn webpack) => 查看打包结果: 发现所有的组件模块都打包进了 bundle.js

=> sideEffects 特性就可以用来解决此类问题

编辑 webpack.config.js

module.exports = {  mode: 'none',  entyr: './src/index.js',  output: {    filename: 'bundle.js'  },  optimization: {    sideEffects: true  }}

sideEffects 特性在 webpack production 模式下自动开启 => 开启该特性后 Webpack 在打包时就会先检查当前代码所属的 package.json 中有没有 sideEffects 的标识 以此来判断该模块是否有副作用 => 如果该模块没有副作用 没有用到的代码就不会被打包

编辑 package.json

{  .....,  "sideEffects": false //标识当前 package.json 所影响的项目中的所有代码都没有副作用 这些没有用到的模块没有副作用 -> 就会被移除掉}

运行 webpack (yarn webpack) 打包 => bundle.js 中那些没有用到的代码就不会被打包=> 注意: 这里设置了两个地方 => webpack.config.js 中的 sideEffects 是用来开启 sideEffects 功能 => 而 package.json 中的 sideEffects 是用来标识当前项目代码是没有副作用的;

sideEffects 注意

使用 sideEffects 这个功能的前提 -> 确定你的代码真的没有副作用 否则 Webpack 打包就会误删那些有副作用的代码
=> 在上面代码的基础上准备 src/extend.js

//extend.js//为 Number 的原型添加一个扩展方法Number.prototype.pad = function (size) {  // 将数字转化为字符串  let result = this + ''  // 在数字前补充指定个数的 0  whild (result.length < size) {    result = '0' + result  }  return result} 

在 extent.js 中并没有向外导出成员 仅仅是在 Number 的原型上挂载了 pad 方法 用来为数字添加前面的导零 => 这时非常常见的基于原型的扩展方法

编辑 src/index.js

import { Button } from './components'import './extend'consoke.log(8.pad(3))document.body.appendChild(Button())

这里为 Number 做扩展的操作就属于 extend 模块的副作用代码(extend.js 中所有的代码) -> 因为导入 extend 后 Number 的原型上都会多一个 pad 方法 这就是副作用 => 此时如果还标识项目中所有的代码都没有副作用的话 -> extent.js 就会被删除

运行 webpack 打包

打包结果中: 刚刚扩展的 pad 方法不会被打包进入输出结果的

=> 因为刚刚扩展的 pad 方法是副作用代码 而package.json 中申明了所有的代码没有副作用

=> 除此之外 代码中载入的 css 模块也是副作用模块 同样会面临刚刚 pad 方法的问题
=> 解决方法: 在 package.json 中关掉副作用 或者 标识当前项目中哪些文件是有副作用的 => 这样, Webpack 打包就不会忽略这些有副作用的模块代码了

编辑 package.json

.....,"sideEffects": [  "./src/extend.js",  "*.css"]

运行 webpack 打包(yarn webpack) => 打包结果中有副作用的模块代码也被打包了

1.20、Webpack 代码分割

通过 Webpack 实现前端项目整体模块化的优势固然很明显 但同样存在一些弊端: 项目中的所有代码最懂都被打包到一起 -> 如果项目非常复杂 模块非常多 打包结果 bundle.js 体积就会特别大 -> 事实情况是: 并不是每个模块在启动时都是必须的 但是这些模块又被全部打包在一起 需要任何一个模块都需要将整体下载下来之后才能使用 -> 会浪费掉很多的流量和带宽 -> 更为合理的方案: 将打包结果按照一定的规则分离到多个 bundle 中 -> 根据应用的运行需要 按需加载模块 -> 这样 大大提高了应用的运行速度和响应效率

开始学习 Webpack 时说过: Webpack 就是将项目中散落的模块合并到一起提高运行效率 -> 这里又说 要将代码分离开 -> 这两个说法冲突吗?
=> 其实这并不冲突 只是物极必反 -> 资源太大了不行太碎了更不行 -> 项目中划分模块的颗粒度一般都会非常细 -> 有时候模块只是提供了小小的工具函数 并不能形成完整的功能单元 -> 如果不把散落的资源合并到一起 就有可能运行一个小小的功能时就会加载非常多的模块 -> 而目前主流的 HTTP 1.1 版本本身都有很多缺陷 Ex: 同域并行请求限制(不能对同一个域名发起很多次的并行请求)、每次请求都会有一定的延迟、请求的 Header 浪费带宽流量(每次请求都会有额外的请求头和响应头) -> 综上所述: 模块打包是必要的 不过在应用越来越大的时候 需要及时的分割代码

=> 为了解决 应用过大 打包体积过大的问题 -> Webpack 支持分包的功能 也可以将这种功能称之为 代码分割 -> 通过将模块按照设计的规则打包到不同的 bundle 中 从而提高应用的运行速度和响应速度-> 目前 Webpack 实现分包的方式主要有两种:
1. 根据业务配置不同的打包入口 -> 也就是会有同时多个打包入口 同时打包 -> 输出多个打包结果
2. 采用 ES Modules 的动态导入功能实现模块的按需加载 -> 这时 Webpack 会将动态导入的模块单独输出到一个 bundle 中

Webpack 多入口打包

多入口打包一般适用于传统的 多页应用程序 -> 最常见的划分规则就是: 一个页面对应一个打包入口 公共部分单独提取

=> 项目结构: src/album.css、src/album.html、src/album.js、src/fetch.js、src/global.js、src/index.css、src/index.html、src/index.js

// src/index.js: 负责实现 index 页面上所有的功能import fetchApi from './fetch'import './global.css'import './index.css'const mainElement = document.querySelector('.main')fetchApi('/posts').then(data => {  data.forEach(item => {    const aritcle = document.createElement('article')    article.className = 'post'        const h2 = document.createElement('h2')    h2.textContent = item.title    article.appendChild(h2)        const paragraph = document.createElement('p')    paragraph.textContent = item.body    article.appendChild(paragraph)        mainElement.appendChild(article)  })})
// src/album.js: 负责实现相册页面的所有功能import fetchApi from './fetch'import './global.css'import './album.css'const mainElement = document.querySelector('.main')fetchApi('/photos?albumId=1').then(data => {  data.forEach(item => {    const section = document.createElement('section')    section.className = 'photo'        const img = document.createElement('img')    img.src = item.thumbnailUrl    section.appendChild(img)        const h2 = document.createElement('h2')    h2.textContent = item.title    section.aooendChild('h2')        mainElement.appendChild(section)  })})
// src/fetch.js: 公共模块 -> 负责用来提供请求 API 的方法export default endpoint => {  return fetch(`https://jsonplaceholder.typicode.com${endpoint}`)  .then(response => response.json())}

这里尝试为这个案例配置多个打包入口

编辑 webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin')const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = {  mode: 'none',  entry: './src/index.js',  output: {    filename: 'bundle.js'  },  module: {    rules: [      {        test: /\.css$/,        use: [          'style-loader',          'css-loader'        ]      }    ]  },  plugins: [    new CleanWebpackPlugin(),    new HtmlWebpackPlugin({      title: 'Multi Entry',      template: './src/index.html',      filename: 'index.html'    }),    new HtmlWebpackPlugin({      title: 'Multi Entry',      template: './src/album.html',      filename: 'album.html'    })  ]}

一般配置文件中的 entry 属性只会配置一个文件路径 如果需要配置多个入口 -> 将 entry 属性的值设置为一个对象 -> 一旦设置为多入口文件 output 属性也需要修改

编辑 webpack.config.js

.....,  entry: {    //该对象中一个键值对就是一个打包入口 -> 键就是入口的名称 值就是入口对应的文件路径    index: './src/index.js',    album: './src/album.js'  },  output: {    filename: '[name].bundle.js' //[name] 会被替换为入口的名称  }

运行 webpack 打包(yarn webpack) -> 此次打包就会有两个入口 -> 打包完成后有两个输出文件 -> 现在还有个问题 -> 打包生成的 html 文件中两个打包结果都被载入了

=> 而我们希望的是一个页面只使用对应的输出文件

修改 webpack.config.js

// html-webpack-plugin 插件默认会输出一个自动注入所有打包结果的 html -> 如果要指定输出 html 文件所引用的 bundle -> 使用 chunks 属性设置 每一个打包入口会形成一个独立的 chunks .....,  plugins: [    new CleanWebpackPlugin(),    new HtmlWebpackPlugin({      title: 'Multi Entry',      template: './src/index.html',      filename: 'index.html',      chunks: ['index']    }),    new HtmlWebpackPlugin({      title: 'Multi Entry',      template: './src/album.html',      filename: 'album.html',      chunks: ['album']    })  ]

运行 webpack 打包(yarn webpack) -> 打包结果正常

Webpack 提取公共模块

多入口打包很容易理解和使用 但是他也存在一个小小的问题: 在不同的打包入口中肯定会有公共模块 Ex: 上面里例子中 index.js 和 album.js 中都使用了 global.css 和 fetch.js -> 这里是示例比较简单 所以重复的影响不会有那么大 -> 如果共同使用的 Jquery 或者 Vue 体积比较大模块 影响就会特别的大 -> 解决方案:
=> 将公共的模块提取到单独的 bundle 中 -> webpack 中实现公共模块的提取也非常简单 -> 只需要在优化配置开启 splitChunks 属性就可以了

编辑 webpack.config.js

.....,  optimization: {    splitChunks: {      chunks: 'all' //将所有的公共模块都提取到单独的 bundle 中    }  }....,

运行 webpack 打包(yarn webpack) -> 输出文件夹中就会多出 album~index.bundle.js -> index.js 和 album.js 两个入口文件的公共模块

Webpack 动态导入

按需加载是开发浏览器应用中非常常见的需求 一般说的按需加载说的是数据 -> 这里说的按需加载说的是: 需要用到某个模块时 再加载这个模块 -> 可以极大的节省贷款和流量
=> Webpack 中支持使用动态导入的方式实现模块按需加载 -> 而且所有动态导入的模块会自动分包
=> 想比较与多入口的方式: -> 动态导入更为灵活 -> 可以通过代码的逻辑控制模块的导入需要和时机 -> 而分包的目的中就有很重要的一点: 让模块实现按需加载 -> 从而提高应用的相应速度

=> 动态导入场景: 在页面的主体区域 如果访问的是文章页 显示的就是文章列表 -> 如果访问的是相册页 显示的就是相册列表

=> 场景代码结构: image-20210628202425458

// src/index.jsimport posts from './posts/posts'import albom from './album/album'const render = () => {  const hash = window.location.hash || '#posts'  const mainElement = document.querySelector('.main')    mainElement.innerHTML = ''  if ('#posts' === hash) {    mainElement.appendChild(posts())  } else if ('#album' === hash) {    mainElement.appendChild(album())  }}render()window.addEventListener('hashchange', render)

上面的代码就会存在浪费的可能性 -> 如果用户打开应用 只是访问了其中一个页面 -> 那另外一个页面组件的加载就是一种浪费 -> 如果动态导入文件就不会存在浪费的问题

修改 src/index.js

// import posts from './posts/posts'// import albom from './album/album'// 动态导入使用的是 ES Modules 的动态导入 -> 在需要导入的地方通过 import() 函数导入指定的路径 -> 该方法返回的是 Promise -> 在该 Promise 的 then 方法中就可以拿到模块对象 moduleconst render = () => {  const hash = window.location.hash || '#posts'  const mainElement = document.querySelector('.main')  mainElement.innerHTML = ''  if ('#posts' === hash) {    // mainElement.appendChild(posts())    // 由于使用的是默认导出 -> 所以需要结构 -> 结果完成后 创建页面的元素    import('./posts/posts').then(({ default: posts }) => {      mainElement.appendChild(posts())    })  } else if ('#album' === hash) {    //mainElement.appendChild(album())    import('./album/album').then(({ default: album }) => {      mainElement.appendChild(album())    })  }}render()window.addEventListener('hashchange', render)

运行 webpack 打包(yarn webpack) -> 打包结果: dist 目录中会多出 三个 JS 文件 -> 这三个文件实际上就是由动态导入自动分包产生的 -> 分别是刚刚导入的两个模块以及两个模块的公共部分所提取出来的 bundle

这就是动态导入在 Webpack 中的使用 -> 整个过程无需配置任何一个地方 只需要按照 ES Modules 动态导入成员的方式导入模块就可以了 -> webpack 会完成自动分包和按需加载
=> 如果你使用的是单页应用开发框架 Ex: React 或者 Vue 那在项目的路由映射组件就可以通过这种动态导入的模式实现按需加载

Webpack 魔法注释

默认通过动态导入产生的 bundle 文件 它的名称就是一个序号 这并没有什么不好的 -> 因为在生产环境中大多数时候是不用关心资源文件的名称是什么 但是如果需要给这些 bundle 命名的话 -> 可以使用 Webpack 所特有的 魔法注释 来实现

编辑 src/index.js

// import posts from './posts/posts'// import albom from './album/album'// 动态导入使用的是 ES Modules 的动态导入 -> 在需要导入的地方通过 import() 函数导入指定的路径 -> 该方法返回的是 Promise -> 在该 Promise 的 then 方法中就可以拿到模块对象 moduleconst render = () => {  const hash = window.location.hash || '#posts'  const mainElement = document.querySelector('.main')  mainElement.innerHTML = ''  if ('#posts' === hash) {    // mainElement.appendChild(posts())    // 由于使用的是默认导出 -> 所以需要结构 -> 结果完成后 创建页面的元素    // 使用 webpack 魔法注释: 在调用 import() 函数时添加一个行内注释参数 -> 该注释有特定的格式: webpackChunkName: 分包名称    import(/* webpackChunkName: 'posts' */,  './posts/posts').then(({ default: posts }) => {      mainElement.appendChild(posts())    })  } else if ('#album' === hash) {    //mainElement.appendChild(album())    import(/* webpackChunkName: album */, './album/album').then(({ default: album }) => {      mainElement.appendChild(album())    })  }}render()window.addEventListener('hashchange', render)

运行 webpack 打包(yarn webpack) -> 此时所生成的 bundle 文件的 name 就会使用刚刚定义的 webpackChunkName -> 如果你的 chunkName 相同的话 相同的 chunkName 会被打包到一个 bundle -> 这样 就可以根据实际情况 灵活组织动态加载的模块的输出文件;

Webpack MiniCssExtractPlugin

MiniCssExtractPlugin 是一个将 CSS 代码从打包结果中提取出来的插件 通过该插件就可以实现 CSS 模块的按需加载

通过命令行安装 mini-css-extract-plugin

cnpm i mini-css-extract-plugin -D

编辑 webpack.config.js

....,const MiniCssExtractPlugin = require('mini-css-extract-plugin').....,  plugins: [    ......,    new MiniCssExtractPlugin()  ]

这样 mini-css-extract-plugin 在工作时就会自动提取代码中的 css 到一个单独的文件中

=> 除此以外 目前使用的样式文件是先交给 css-loader 解析 -> 然后在给 style-loader 处理 style-loader 的作用就是: 将样式通过 style 标签注入到页面中 从而使样式可以工作 -> 使用 MiniCssExtractPlugin 样式文件就会单独存放在文件中 也就不需要 style 标签 -> 而是直接通过 link 的方式引入

=> 所以配置文件中就不在需要 style-loader 了 -> 取而代之 使用的是 MiniCssExtractPlugin 所提供的一个 loader 来实现样式文件通过 link 的方式注入页面

编辑 webpack.config.js

......,const MiniCssExtractPlugin = require('mini-css-extract-plugin')......,  module: {    rules: [      {        test: /\.css$/,        use: [          // style-loader, //将样式文件通过 style 标签注入页面          MiniCssExtractPlugin.loader,          'css-loader'        ]      }    ]  }......,  plugins: [    ......,    new MiniCssExtractPlugin()  ]

运行 webpack 打包(yarn webpack) -> 打包完成后 dist 目录下可以看到提取出来的样式文件 components.css -> 这里需要注意: 如果你的样式文件的体积不是很大的话 提取到单个文件的效果可能会适得其反

=> 个人经验: CSS 超过了 150 KB 才需要考虑是否将他提取到单独文件中 -> 不提取 CSS 嵌入代码中 减少一次请求 效果可能会更好

Webpack OptimizeCssAssetsWebpackPlugin

使用 MiniCssExtractPlugin 插件以后 样式文件就可以被提取到单独的文件中 但是这里同样有一个小问题: 运行 webpack 以生产模式打包(yarn webpack --mode production) -> 输出的样式文件没有被压缩 -> 这是因为 webpack 内置的压缩插件仅仅是针对于 JS 文件的压缩 对于其他的文件压缩都需要额外的插件来支持
=> Webpack 官方推荐了一个 CSS 压缩插件: optimize-css-assets-webpack-plugin

通过命令行安装 optimize-css-assets-webpack-plugin

cnpm i optimize-css-assets-webpack-plugin -D

编辑 webpack.config.js

......,const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')......,  plugins: [    ......,    new OptimizeCssAssetsWebpackPlugin()  ]

运行 webpack 打包(yarn webpack) -> 打包完成后 延时文件就是以压缩的文件输出了
=> 不过这里还有一个额外的小点: 在官方文档中 该插件并不是配置在 plugins 中的 而是配置到了 optimization 的 minimizer 数组中 -> 原因: 如果将该插件配置到 plugins 中 该插件在任何情况下都会正常工作 -> 而配置到 minimizer 数组中 只会在 minimize 这个特性开启时 该插件才会工作
=> 所以 Webpack 建议 像这种压缩类的插件应该配置在 minimizer 数组中

编辑 webpack.config.js

......,const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')......,  optimization: {    minimizer: [      new OptimizeCssAssetsWebpackPlugin()    ]  }  plugins: [    ......,    // new OptimizeCssAssetsWebpackPlugin()  ]

此时打包 如果没有开启压缩代码的功能的话 该插件就不会工作 -> 如果以 production 模式打包 该插件就会自动开启 -> 样式代码就会以压缩的形式输出
=> 这样也有一个小小的问题: 原来可以自动压缩的 JS 代码 现在去不能自动压缩 -> 这是因为设置了 minimizer 这个数组 -> Webpack 认为 如果配置了这个数组 那就是要使用自定义压缩器插件 内部的 JS 压缩器就会被覆盖掉 -> 所以这里需要手动将 JS 压缩器添加回来 -> 内置的 JS 压缩器(压缩插件): terser-webpack-plugin

通过命令行安装 terser-webpack-plugin

cnpm i terser-webpack-plugin -D

将 terser-webpack-plugin 插件手动添加到 minimizer 数组中

// webpack.config.js......,const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')const TerserWebpackPlugin = require('terser-webpack-plugin')......,  optimization: {    minimizer: [      new TerserWebpackPlugin(),      new OptimizeCssAssetsWebpackPlugin()    ]  }  plugins: [    ......,    // new OptimizeCssAssetsWebpackPlugin()  ]

如果以 production 模式打包 JS 和 CSS 文件都会被压缩输出了 如果不开启代码压缩 JS 和 CSS 将不会被压缩输出

Webpack 输出文件名 Hash

一般部署前端的资源文件时 都会启用服务器的静态资源缓存 -> 这样对于用户的浏览器而言 就可以缓存住应用中的静态资源 后续就不需要请求服务器得到这些静态资源文件 -> 这样 整体应用的相应速度都有一个大幅度的提升
=> 不过 开启静态资源的客户端缓存也有一些小小的问题 -> 如果在缓存策略中 缓存失效时间设置过短 效果就不是特别的明显 -> 如果缓存失效时间设置过长 一旦过程应用发生了更新 重新部署之后 又没有办法及时更新到客户端
=> 为了解决这样一个问题: 建议在生产模式下 需要给输出的文件名中添加 Hash值 -> 这样的话 一旦资源文件发生改变 文件名称也会跟着一起变化 -> 对于客户端而言 全新的文件名就是全新的请求 也就解决了缓存的问题 -> 这样就可以将服务端的缓存策略中的失效时间设置非常长 也不用担心文件更新后的问题
=> Webpack 的 filename 属性和绝大多数插件的 filename 属性 都支持通过占位符的方式 来为文件名设置 Hash -> 支持三种 Hash 效果各不相同

  1. 最普通的 Hash: ‘[name]-[hash]’

    该 Hash 是整个项目级别的 -> 也就是 项目中任何一个地方发生改动 打包过程中的 Hash 值都会发生变化

    //webpack.config.jsmodule.exports = {  ......,  output: {  	filename: '[name]-[hask].bundle.js'	},  ......,  plugins: [    ......,    new MIniCssExtractPlugin({    	filename: '[name]-[hash].bundle.js'    })  ]}
    

    运行 webpack 打包: yarn webpack

    打包结果:

    修改项目中的任何一个文件代码 -> 重新打包

    打包结果:

  2. chunkhash

    该 Hash 是 chunk 级别的 -> 就是在打包过程中只要同一路的打包 chunkhash 都是相同的
    => 代码中通过动态导入形成了两路 chunk : 分别是 posts 和 album -> 样式文件是从代码中单独提取出来的 并不是单独的 chunk

    //webpack.config.jsmodule.exports = {  ......,  output: {  	filename: '[name]-[chunkhash].bundle.js'	},  ......,  plugins: [    ......,    new MIniCssExtractPlugin({    	filename: '[name]-[chunkhash].bundle.js'    })  ]}
    

    运行 webpack 打包: yarn webpack

    打包结果:

    main posts 和 album 三者 chunkhash 各不相同 而 css 文件和对应的 JS 文件 chunkhash 是一样的 -> 因为他们是同一路

    修改 index.js -> 重新打包

    此时发现只有 main.bundle.js 的 hash 值法伤了变化 其他的都没有变化

    修改 posts.js -> 重新打包

    此次 posts 所输出的 CSS 和 JS bundle 都会发生变化 -> 因为他们是同一个 chunk -> 至于 mian.bundle 发生变化的原因是: posts.bundle.js 和 posts.bundle.css 文件名发生变化 入口文件中引入的路径也会发生变化 造成了 mian.bundle.js hash 发生变化 -> 所以说 mian.bundle.js hash 是被动发生变化

    => 相比较与 普通 hash -> chunkhash 的控制回更加精确一点

  3. contenthash

    该 hash 是文件级别的 hash -> 他实际上是根据输出文件的内容生成的 hash -> 也就是说只要是不同的文件就会生成不同的 hash 值

    //webpack.config.jsmodule.exports = {  ......,  output: {  	filename: '[name]-[contenthash].bundle.js'	},  ......,  plugins: [    ......,    new MIniCssExtractPlugin({    	filename: '[name]-[contenthash].bundle.js'    })  ]}
    

    修改 index.js -> 重新打包

    同样的只用 main.bunde.js 的 hash 值发生了变化

    修改 posts.css -> 重现打包

    打包结果中 posts.bundle.css 的 hash 值发生了变化 -> main.bundle.js 同样也是因为路径的变化被动发生的变化

    => 相比较与 前面两种 hash: contenthash 应该算是解决缓存问题最好的方式 -> 因为它精确的定位到了文件级别的 hash 只有当文件发生了变化 才有可能更新掉文件名

修改 hash 的长度

如果觉得 20 位的 hash 长度太长的话 webpack 还允许指定 hash 的长度 -> 可以通过 contenthash: 数字 的方式修改 hash 长度 hash

//webpack.config.jsmodule.exports = {  ......,  output: {  	filename: '[name]-[contenthash:8].bundle.js'	},  ......,  plugins: [    ......,    new MiniCssExtractPlugin({    	filename: '[name]-[contenthash:8].bundle.js'    })  ]}

打包结果:

=> 总的来说: 如果控制缓存的话 8位长度的 hash 是最好的选择了