构建工具的必要性
前端无模块化之前 ,所有的js
资源都在一个html
中统一引入,造成了两种极端情况 :
一种是每个模块文件都单独请求。(造成请求过多问题) 一种是把所有模块打包成一个文件然后只请求一次。(加载缓慢问题)
其他问题 : 多个 js
文件互相依赖时,需要处理好加载顺序
我们通过借助构建工具来解决问题:
我们采用分块传输,按需进行懒加载,也就是增量加载到浏览器中(两种极端方法的中和操作), 这时我们需要构建工具完成 静态分析 编译打包的工作
在整个构建过程中,我们可以对静态资源也进行模块化管理构建 ,可以对项目的依赖项自动处理 , 优化打包后的文件, 等等。。。
构建工具:webpack
Webpack的重要概念:
loader : 将各种类型的资源转换成 JavaScript 模块,Webpack 本身只能处理原生的 JavaScript 模块。
plugin : 补充 loader 的功能
依赖图: webpack从入口开始递归去处理文件之间的依赖关系,构成的图
target : 区分
JavaScript
的打包环境runtime : 在模块交互时,连接模块所需的加载和解析逻辑。
manifest : 一个集合保存着各模块的要点, 通过 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。
module
,chunk
和bundle
其实就是同一份逻辑代码在不同转换场景下的取了三个名字: 我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
常用配置项
- mode
- entry
- output
- devtool
- devServer
- optimization(代码优化)
- module(配置 loader)
- plugins
预处理器loader
在引入loader时,可以通过options 提供额外的配置
exclude
: 排除被正则匹配到的该模块
必加项 : exclude: /node_modules/, include : /src/
exclude
和 include
相比 exclude
优先级更高
开发环境
每次手动编译文件很麻烦 , webpack
提供几种可选方式,帮助你在代码发生变化后自动编译代码
webpack watch mode
: watch 配置项开启 / —watch 命令行
watch
模式下只能对代码进行实时编译,并不能开启本地服务,来运行我们的代码,每次都将操作文件,效率更慢,我们只需要项目编写完成后,去打包生成文件
webpack-dev-server
: 命令行 webpack serve (webpack-dev-serve —open)
内部使用
express
和webpack-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
就算开启热更新,还是会实时刷新页面
当 **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
函数合并 :
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
},
});
环境变量:
许多 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
技术上讲,NODE_ENV
是一个由Node.js
暴露给执行脚本的系统环境变量。然而,与预期相反,在构建脚本 webpack.config.js
中process.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
Ts-loader
使用 tsc (TypeScript 编译器) ,并依赖于您的 tsconfig.json
配置。确保避免将模块设置为“ CommonJS”
,否则 webpack
将无法改变您的代码。
如果已经使用 Babel-loader 来输出代码,可以使用@Babel/pressing-TypeScript,让 Babel 处理 JavaScript 和 TypeScript 文件,而不是使用额外的加载程序。
TS 中想要启用 source map
,我们必须配置 tsconfig.ts
Ts
中导入其他资源, 想要在 TypeScript
中使用非代码资源(non-code asset),我们需要告诉 TypeScript 推断导入资源的类型。在项目里创建一个 custom.d.ts
文件,这个文件用来表示项目中 TypeScript
的自定义类型声明。我们为 .svg
文件设置一个声明:
代码分离(分片):
能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
代码分离方法:
手动配置入口起点, 构建多个bundle 进行加载。但是当重复引入库,不会检测重复导致代码冗余。
把重复代码提取出来.
方法1 : 手动配置 dependOnoption 选项,这样可以在多个 chunk 之间共享模块
方法2 : 使用插件
[SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin)
可以将公共的依赖模块提取到一个单独的chunk
中 webpack4 之前 使用CommonsChunkPlugin
插件 。
optimization.splitChunks
参数:
chunks
: async(默认值) 针对异步资源生效, initial: 只对入口chunk生效, all:对所有的资源生效, 这意味着即使在异步和非异步块之间也可以共享块
minChunks
: 拆分之前, 公共模块被共享的最低次数
cacahGroups
: 分离chunks时的规则,一般有vendors 和default两种 :vendors 代表在 node_module的区块, default : 代表多次被引用的区块
- 当涉及到动态代码拆分时, (主要使用 es6 的
import()
/ webpack功能[require.ensure](https://webpack.docschina.org/api/module-methods/#requireensure)
)
import('lodash')
.then(({ default: _ })=>{})
// 需要 default参数值来获取模块对象,因为 webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值
代码分离插件存在的问题:
提取后的公共模块代码中, 有些 chunk
包含 runtime 初始化环境的代码, 导致每次打包影响chunk
中hash
的变化, 而 hash
的变化影响 runtime 的代码,导致某些chunk没有更新,缓存也会失效。因此使用 chunk hash 作为资源的版本号优化客户端的缓存,这导致用户频繁的更新资源。
解决方法: 把 runtime
的代码提取出来(使用optimization.runtimeChunk ), 通过在提取完公共模块后, 再调用该插件提取一次。提取出runtime 代码后, 还可以利用插件 script-ext-html-webpack-plugin 进行优化,原因每次构建上线后,runtime都要重新请求, 浪费网络资源, 把它直接内联到html中。
缓存
客户端获取资源是比较耗费时间的 , 浏览器使用一种名为缓存的技术, 重复利用浏览器已经获取过的资源, 通过命中缓存,以降低网络流量 。
当开发者更新了 bug , 希望立即更新到用户的浏览器上,而不是使用客户端旧的缓存,最好的办法是改变资源的URL,一个常用的办法时改变文件的名字 : 来迫使客户端重新下载。
通过替换 output.filename
中的 substitutions 设置
配置之后,不修改文件每次构建也有可能会生成新的文件名(新旧版本情况不同)。所以保险起见,我们将第三方库(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)
每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。
- 当引入新文件时,我们不希望库的bundle 发生变化,但是 vendor bundle 会随着自身的 module.id 的变化,而发生变化。配置如下属性解决
提取引导模板 :可使用 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 列表中
压缩输出
副作用标记结束之后, 在最后压缩过程中删除代码才是完整的 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 每次打包后的体积和上次的体积变化值