构建工具的必要性

前端无模块化之前 ,所有的js资源都在一个html中统一引入,造成了两种极端情况 :

一种是每个模块文件都单独请求。(造成请求过多问题) 一种是把所有模块打包成一个文件然后只请求一次。(加载缓慢问题)

其他问题 : 多个 js 文件互相依赖时,需要处理好加载顺序

我们通过借助构建工具来解决问题:
我们采用分块传输,按需进行懒加载,也就是增量加载到浏览器中(两种极端方法的中和操作), 这时我们需要构建工具完成 静态分析 编译打包的工作

在整个构建过程中,我们可以对静态资源也进行模块化管理构建 ,可以对项目的依赖项自动处理 , 优化打包后的文件, 等等。。。

构建工具:webpack

Webpack的重要概念:

  • loader : 将各种类型的资源转换成 JavaScript 模块,Webpack 本身只能处理原生的 JavaScript 模块。

  • plugin : 补充 loader 的功能

  • 依赖图: webpack从入口开始递归去处理文件之间的依赖关系,构成的图

  • target : 区分 JavaScript的打包环境

  • runtime : 在模块交互时,连接模块所需的加载和解析逻辑。

  • manifest : 一个集合保存着各模块的要点, 通过 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。

  • modulechunkbundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字: 我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

常用配置项

  1. mode
  2. entry
  3. output
  4. devtool
  5. devServer
  6. optimization(代码优化)
  7. module(配置 loader)
  8. plugins

webpack 术语表

预处理器loader

在引入loader时,可以通过options 提供额外的配置

exclude: 排除被正则匹配到的该模块

必加项 : exclude: /node_modules/, include : /src/

excludeinclude相比 exclude 优先级更高

开发环境

每次手动编译文件很麻烦 , webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码

webpack watch mode : watch 配置项开启 / —watch 命令行

watch 模式下只能对代码进行实时编译,并不能开启本地服务,来运行我们的代码,每次都将操作文件,效率更慢,我们只需要项目编写完成后,去打包生成文件

webpack-dev-server: 命令行 webpack serve (webpack-dev-serve —open)

内部使用 expresswebpack-dev-middleware 中间件搭建的一个服务器,内部使用了一个叫做 memfs 的库,可以将我们打包后的文件夹直接加载到内存中,而不需要频繁操作file system,从而提升编译效率

webpack-dev-middleware : 使用 node 和 该中间件,根据需求可进行更多自定义设置


devServer 参数配置

ContentBase: 指定服务器资源的根目录,静态资源从该目录中找寻,默认使用和webpack-config.js 相同目录。

PublicPath: 如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的 [publicPath](https://webpack.docschina.org/configuration/dev-server/#devserverpublicpath-) 选项进行修改。 默认为’/‘。

不管 PublicPath 怎么设置, 通过路由查找文件时使用的路径都是ContentBase ,而且都是通过在硬盘文件中查找,而不是在内存中查找

publicPath 和 output publicPath 保持一致, 这样静态资源文件才可以请求到, 因为设置完 publicPath, 这个路径就代表了内存中的根目录

热更新

webpack-dev-server v4.0.0 开始,热模块替换是默认开启的。 否则在devServer设置hot:true

热更新开启需要在代码中写更新逻辑 , 否则 webpack-dev-server就算开启热更新,还是会实时刷新页面
企业微信截图_16512124575174.png

**test.js **发生更新把根节点卸载掉然后再生成新节点挂载

在开发其他项目时, 经常手动去写入 module.hot.accpet相关的API是十分麻烦的,然而事实上社区已经针对这些有很成熟的解决方案了:

比如vue开发中,我们使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验
比如react开发中,有react-refresh,实时调整react组件 (ps: react-refresh只能在开发环境使用,如果在生产环境使用会报错)

除了直接配置 webpack 配置文件 ,我们也可以使用 Node.js 来使用 webpack dev server ,写单独的 webpack dev server 逻辑

样式热更新

借助于 style-loader,使用模块热替换来加载 CSS 实际上极其简单。此 loader 在幕后使用了module.hot.accept,在 CSS 依赖模块更新之后,会将其 patch(修补) 到<style> 标签中。

热更新原理

webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket), 通过socket的长连接双方可以通信,针对修改的文件进行更新

生产环境

development(开发环境)production(生产环境) 这两个环境下的构建目标存在着巨大差异:

开发环境: 我们需要强大的 source map 和一个有着 live reloading(实时重新加载) 或 hot module replacement(热模块替换) 能力的 localhost server
生产环境 : 目标则转移至其他方面,关注点在于压缩 bundle、更轻量的 source map、资源优化(让用户更快加载资源,最大限度利用缓存, 资源的压缩),

配置文件:

由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。也可以使用同一个配置文件,不过要在webpack.config.js文件内添加判断条件来使用那个配置

多个 webpack配置下使用一个名为 webpack-merge 的工具,通过引入 common 的配置,使用 merge函数合并 :

  1. const { merge } = require('webpack-merge');
  2. const common = require('./webpack.common.js');
  3. module.exports = merge(common, {
  4. mode: 'development',
  5. devtool: 'inline-source-map',
  6. devServer: {
  7. contentBase: './dist',
  8. },
  9. });


环境变量:

许多 library 通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。
从 webpack v4 开始, 指定 [mode](https://webpack.docschina.org/configuration/mode/) 会自动地配置 [DefinePlugin](https://webpack.docschina.org/plugins/define-plugin), process.env.NODE_ENV 被指定为 mode 值 。

低版本 webpack 手动使用 DefinePlugin
image.png

技术上讲,NODE_ENV 是一个由Node.js 暴露给执行脚本的系统环境变量。然而,与预期相反,在构建脚本 webpack.config.jsprocess.env.NODE_ENV 并没有被设置为 "production"
因此,在 webpack 配置文件中,process.env.NODE_ENV === 'production' 语句不会按照预期执行。

任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量。

source map:

对于 JS文件,针对生产环境用途,选择一个可以快速构建的推荐配置(更多选项请查看 devtool),对于css less scss来说,在loader, options选项中添加。

资源压缩 (uglify) :

移除多余空格, 换行,和不执行的代码, 缩短变量名.. 使代码形式变得更短, 压缩之后的代码基本上不可读,
webpack4 之后压缩代码,默认是 production mode 。

optimization.minimizer 配置项中提供一个/多个压缩工具, optimization.minimize:true 开启压缩功能, 生产模式自动开启

TypeScript 处理

  • 安装 typescript`` ts-loader
  • 配置 tsconfig.json
  • 配置 module.rules 加上 ts-loder

image.png

Ts-loader 使用 tsc (TypeScript 编译器) ,并依赖于您的 tsconfig.json 配置。确保避免将模块设置为“ CommonJS”,否则 webpack将无法改变您的代码。

如果已经使用 Babel-loader 来输出代码,可以使用@Babel/pressing-TypeScript,让 Babel 处理 JavaScript 和 TypeScript 文件,而不是使用额外的加载程序。

TS 中想要启用 source map,我们必须配置 tsconfig.ts
image.png

Ts中导入其他资源, 想要在 TypeScript中使用非代码资源(non-code asset),我们需要告诉 TypeScript 推断导入资源的类型。在项目里创建一个 custom.d.ts文件,这个文件用来表示项目中 TypeScript的自定义类型声明。我们为 .svg文件设置一个声明:
image.png

代码分离(分片):

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

代码分离方法:

  1. 手动配置入口起点, 构建多个bundle 进行加载。但是当重复引入库,不会检测重复导致代码冗余。

  2. 把重复代码提取出来.

方法1 : 手动配置 dependOnoption 选项,这样可以在多个 chunk 之间共享模块 image.png

方法2 : 使用插件 [SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin) 可以将公共的依赖模块提取到一个单独的 chunk中 webpack4 之前 使用 CommonsChunkPlugin 插件 。

image.png

optimization.splitChunks 参数:

chunks : async(默认值) 针对异步资源生效, initial: 只对入口chunk生效, all:对所有的资源生效, 这意味着即使在异步和非异步块之间也可以共享块

minChunks: 拆分之前, 公共模块被共享的最低次数

cacahGroups : 分离chunks时的规则,一般有vendors 和default两种 :vendors 代表在 node_module的区块, default : 代表多次被引用的区块

  1. 当涉及到动态代码拆分时, (主要使用 es6 的 import() / webpack功能 [require.ensure](https://webpack.docschina.org/api/module-methods/#requireensure)
  1. import('lodash')
  2. .then(({ default: _ })=>{})
  3. // 需要 default参数值来获取模块对象,因为 webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值

代码分离插件存在的问题:

提取后的公共模块代码中, 有些 chunk包含 runtime 初始化环境的代码, 导致每次打包影响chunkhash的变化, 而 hash的变化影响 runtime 的代码,导致某些chunk没有更新,缓存也会失效。因此使用 chunk hash 作为资源的版本号优化客户端的缓存,这导致用户频繁的更新资源。

解决方法: 把 runtime的代码提取出来(使用optimization.runtimeChunk ), 通过在提取完公共模块后, 再调用该插件提取一次。提取出runtime 代码后, 还可以利用插件 script-ext-html-webpack-plugin 进行优化,原因每次构建上线后,runtime都要重新请求, 浪费网络资源, 把它直接内联到html中。

缓存

客户端获取资源是比较耗费时间的 , 浏览器使用一种名为缓存的技术, 重复利用浏览器已经获取过的资源, 通过命中缓存,以降低网络流量 。

当开发者更新了 bug , 希望立即更新到用户的浏览器上,而不是使用客户端旧的缓存,最好的办法是改变资源的URL,一个常用的办法时改变文件的名字 : 来迫使客户端重新下载。

通过替换 output.filename 中的 substitutions 设置
image.png

配置之后,不修改文件每次构建也有可能会生成新的文件名(新旧版本情况不同)。所以保险起见,我们将第三方库(library)提取到单独的 vendor chunk 文件中, 因为他不会频繁改变。

可以利用 [SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin/) 插件的 [cacheGroups](https://webpack.docschina.org/plugins/split-chunks-plugin/#splitchunkscachegroups) 选项来实现, 把node_modules 目录的代码提取出单独的一个chunk(所有的第三方库整合成一个chunk)

image.png

每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。

  • 当引入新文件时,我们不希望库的bundle 发生变化,但是 vendor bundle 会随着自身的 module.id 的变化,而发生变化。配置如下属性解决

image.png

提取引导模板 :可使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 来为所有 chunk 创建一个 runtime bundle。

构建性能

目的: 让打包速度更快,资源输出体积更小

  • 减少 loader数量,缩小打包作用域 ,通过使用 include 字段 规定loader应用的目录,缩小范围,或者exclude 字段 , 排除不需要loader 的目录。

  • ProgressPlugin 从 webpack 中删除,可以缩短构建时间。

  • cache 选项: 缓存生成的 webpack 模块和 chunk,来改善构建速度。cache开发模式被设置成 type: 'memory' 而且在 [生产](https://webpack.docschina.org/configuration/mode/#mode-production) 模式 中被禁用

  • 使用 ignorePlugin,他可以完全排除一些模块, 即使被引用也不会被打包, 对于排除一些库的相关文件非常有用, 一些库产生的额外资源我们用不到,但是引用语句在库文件内,我们也无法去掉,这时使用该插件不打包.

  • 使用 DIIplugin插件,对一些不经常改变的公共(第三方)模块,进行预先打包,到工程部署时使用 DiiReferencePlugin来索引打包好的文件, DIIplugin 和 代码分离类似,但是前者会把整个模块拆出来,代码分离会根据规则拆分, 相应的前者需要单独一个配置文件。

  • 使用 happyPack 多线程进行打包(webpack本身是单线程的,只能一个一个的通过依赖关系查找进行转译),适用于转译任务比较重的项目效果明显,对于小项目来说并不明显

Tree Shaking: (缩小chunk体积)

通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

例如,我们仅仅导入了文件中的部分导出内容, 而webpack将把所有内容导入进来,这时需要 tree shaking去除冗余。

它依赖于 ES2015 模块语法的 静态结构 特性。

ES6会在代码编译时确定依赖关系, 可以检测出没有引用过的模块(代码块),webpack进行标记, 在开发环境下仍然可见, 在生产环境 资源压缩时将他们从最终的bundle去掉

Tree Shaking 只对es6 module 有用, 对于通过commonJs 引用进来的没有用处

在工程中使用 babel-loader ,那么一定要通过配置来禁用它的模块依赖解析,因为解析过之后,webpack接受的都是commonjs形式的模块,

在loader 中options:{presets:[@babel/preset-env,{moudle:false}]}

在使用 tree shaking 时必须有 ModuleConcatenationPlugin 的支持,通过设置配置项 mode: "production" 自动启用。否则记得手动引入

标记为无副作用的:

  • 设置 optimization: { usedExports: true} , 低版本 webpack 无此属性

  • package.json 的"sideEffects"属性 设置为 false (所有文件都没有副作用),如果存在副作用的,就设置为数组(副作用文件路径:支持相关文件的相对路径、绝对路径和 glob 模式)

所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中 image.png

压缩输出

副作用标记结束之后, 在最后压缩过程中删除代码才是完整的 shaking
使用命令行-p / --optimize-minimize编译来启用 uglifyjs压缩插件。

webpack 4开始,也可以通过mode 配置选项轻松切换到压缩输出,只需设置为 "**production**"。


**sideEffects****usedExport** 两种标记无用代码的方式

usedExport 针对的层面是单条语句层面。 可以通过 /#PURE/ 注释无副作用语句 sideEffects 针对的层面是文件、模块层面。

sideEffects 打包时直接删除了没有副作用,被引用但没有使用的模块。 usedExports 打包时标记没有使用的语句, 在生产环境下删除多余的语句。

Shimming 预置依赖

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

_shim_另外一个极其有用的使用场景就是:当你希望 polyfill) 扩展浏览器能力,来支持到更多用户时。在这种情况下,你可能只是想要将这些 polyfills 提供给需要修补(patch)的浏览器(也就是实现按需加载)

在依赖全局变量的 第三方模块中 [imports-loader](https://webpack.docschina.org/loaders/imports-loader/) 很有用, [exports-loader](https://webpack.docschina.org/loaders/exports-loader/),将一个全局变量作为一个普通的模块来导出

imports-loader最直接的应用场景,就是你想直接import一个开放的js文件,而不是通过npm去加载这个类库(会有一些类库不支持npm的方式)

  • 加载polyfills:

    最普通的方法 : 引入 [babel-polyfill](https://babel.docschina.org/docs/en/babel-polyfill/) 直接 import ‘babel-polyfill’;使用 import 将其引入到我们的主 bundle 文件:

    最佳实践仍然是,不加选择地和同步地加载所有 polyfill/shim,尽管这会导致额外的 bundle 体积成本。

    但是你仍然可以选择性的加载polyfill, 使用whatwg-fetch 条件性的加载


    优化方案:

    babel-preset-env package 通过 browserslist 来转译那些你浏览器中不支持的特性。这个 preset 使用 [useBuiltIns](https://babel.docschina.org/docs/en/babel-preset-env#usebuiltins) 选项,默认值是 false,这种方式可以将全局 babel-polyfill 导入,改进为更细粒度的 import 格式:babel-preset-env

遗留问题

webpack 4+ 之后为什么需要安装 cli 工具? cli 工具提供了灵活的配置项在控制台 运行webpack

在webpack 3中,webpack本身和它的CLI以前都是在同一个包中,但在第4版之后,他们已经将两者分开来更好地管理它们。

低版本webpack 使用问题

在开发设置中 CleanWebpackPlugin 插件会产生很多冲突, 原因在于, 该插件会在成功构建 build 后(包括编译),删除输出目录的文件.

导致 watch 模式下, 构建成功后,更改编译后, 静态资源 :html文件 和 图片.. 都被删除 解决: cleanStaleWebpackAssets: false 更改该插件选项

在 dev Serve 模式下, 成功构建向内存 写文件而不是硬盘,导致该插件清空了dist目录

解决: 调整 dev Serve 插件的选项 让其构建成功后使文件写入硬盘, 还有设置 cleanStaleWebpackAssets选项, 让其在热更新编译时不会删除静态文件 建议在最后构建的时候再进行清除 使用CleanWebpackPlugin,否则会很多冲突

URL-loader file-loader 的区别:

url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。 url-loader内置了file-loader ,只需要安装一个即可

limit : true 无限制

webpack 常用插件

webpack-dashboard 插件 :可以使控制台中打印的打包有关的信息以列表的形式提供,作为插件添加到webpack配置中,使用webpack-dashboard 模块命令替换原来的webpack启动方式即可

speed-measure-webpack-plugin 可以分析出构建过程的时间, 可以找出构建过程中那个步骤最慢

size-plugin 每次打包后的体积和上次的体积变化值