webpack学习笔记
webpack 基本应用
【注意】只过一遍知识点,不再详细代码演示了。
安装和配置 —— 拆分 dev prod 配置,然后 merge
-
安装 nodejs
-
初始化
npm init -y
-
安装插件
npm i webpack webpack-cli webpack-merge --save-dev
-
新建
src
及其测试 js 代码(包含 ES6 模块化) -
创建配置文件
-
增加
scripts
,运行 -
安装
npm i clean-webpack-plugin --save-dev
-
配置 prod
本地服务和代理
- 新建
index.html
- 安装
npm i html-webpack-plugin --save-dev
,并配置 - 安装
npm i webpack-dev-server --save-dev
,并配置 - 修改
scripts
的dev
,运行
处理 ES6
先引入简单的 babel ,polyfill 以后会单独讲
- 安装
npm i @babel/core @babel/preset-env babel-loader --save-dev
- 配置 webpack module
- 配置
.babelrc
- 运行 dev
处理样式
基本使用
- 安装
npm i style-loader css-loader less-loader less --save-dev
(注意要安装 less) - 配置 webpack module
- 新建 css less 文件,引入 index.js
- 运行 dev
postcss
- 安装
npm i postcss-loader autoprefixer -D
- 新建
postcss.config.js
- 配置 webpack module ,增加
postcss-loader
- 增加 css
transform: rotate(-45deg);
- 运行 dev
处理图片
考虑 base64 格式
- 安装
npm i file-loader url-loader --save-dev
- 分别配置 webpack.dev 和 webpack.prod
- 新建图片文件,并引入到 js 中,插入页面
- 运行 dev
- 运行 build
webpack 题目
webpack 面试题
- 前端代码为何要进行构架和打包?
- module chunk bundle 的区别
- loader 和 plugin 的区别
- 常用的 loader 和 plugin 有哪些
- webpack 性能优化(如上)
- webpack 构建流程简述
- 如何产出多页,如何产出 lib
babel 面试题
- babel 和 webpack 的区别
- babel-runtime 和 babel-polyfill 区别
- 为何 Proxy 无法 polyfill
webpack 高级应用
多入口
- 在src目录下新建
other.html
和other.js
-
修改 entry
image.png -
修改 output
image.png -
修改 HtmlWebpackPlugin
image.png - 运行 dev
- 运行 build
抽离 & 压缩 css 文件
抽离
把原common里面的css、less配置移到dev下,prod的需要配置module和plugin,如下所示:
- 安装
npm i mini-css-extract-plugin -D
- 将之前 common 中的 css 处理,移动到 dev 配置中
-
配置 prod (配置 module ,配置 plugin)
image.png
image.png -
运行 build:dist目录下抽离出对应的css文件
image.png
压缩
- 安装
npm i terser-webpack-plugin optimize-css-assets-webpack-plugin -D
- 配置 prod
抽离公共代码
场景:一般是公共模块或者第三方模块,多处引用如果不提取,可能会存在代码重复冗余。
common和dev基本不用改动,dev能快速用即可,提取可能还稍显麻烦了
- 配置
splitChunks
image.png
image.png
test:/node-module/,表示会进行匹配。比如lodash会安装到node-module里面,当匹配到就符合这个条件
minSize:表示大小限制。比如有的文件很小就几行代码,直接引用即可没必要抽取,所以需要限制一下
minChunks:表示最少复用几次。比如第三方模块,属于可能会在多处引用的情况,所以引用超过一次就提取;但公共模块可能需要2次,如果只在某个地方用到1次就没必要提取
-
修改 HtmlWebpackPlugin 中的 chunks 。重要!!!
image.png -
安装 lodash
npm i lodash --save
做第三方模块的测试,引用 lodash -
运行 build
懒加载
webpack不需要做配置,只需要import引入即可
-
增加
image.pngdynamic-data.js
并动态引入
-
运行 dev 查看效果(看加载 js)
[图片上传中...(image.png-241244-1709365460497-0)]
1.5s之后出现0.js文件 -
运行 build 看打包效果
至此,可以总结一下:
- module:就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码。
- chunk: chunk是webpack根据功能拆分出来的,包含三种情况:
- 你的项目入口(entry)
- 通过import()动态引入的代码
- 通过splitChunks拆分出来的代码
- (chunk包含着module,可能是一对多也可能是一对一)
- bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出。
处理 React 和 vue
处理react:在.babelrc加上"@babel/preset-env"即可
image.png
- vue-loader
- jsx 的编译,babel 已经支持,配置
@babel/preset-react
image.png
module、chunk、bundle区别:
- module:各个源码文件,webpack中一切皆模块。比如js、css文件
- chunk:多模块合并成的,如可以通过entry、 import() 、splitChunk来定义输出
- bundle: 最终输出的文件
常见 loader 和 plugin
webpack 性能优化
注意:
- 以下知识点不再一行一行演示
- 面试的时候也不会问到很细节
- 但不要死记硬背一个概念,一定要知道它的基本原理和配置方式
打包效率
1)优化 babel-loader
- babel-loader cache 未修改的不重新编译
- babel-loader include 明确范围
在 module下的rules配置
{
test: /\.js$/,
use: ['babel-loader?cacheDirectory'], // 开启缓存
include: path.resolve(__dirname, 'src'), // 明确范围
// // 排除范围,include 和 exclude 两者选一个即可
// exclude: path.resolve(__dirname, 'node_modules')
},
2)IgnorePlugin 避免引入哪些模块
以常用的 moment 为例。安装 npm i moment -d
并且 import moment from 'moment'
之后,monent 默认将所有语言的 js 都加载进来,使得打包文件过大。可以通过 ignorePlugin 插件忽略 locale 下的语言文件,不打包进来。
plugins: [
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
import moment from 'moment'
import 'moment/locale/zh-cn' // 这里手动引入中文语言包
moment.locale('zh-cn')
noParse 避免重复打包
module.noParse
配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
module.exports = {
module: {
// 独完整的 `react.min.js` 文件就没有采用模块化
// 忽略对 `react.min.js` 文件的递归解析处理
noParse: [/react\.min\.js$/],
},
};
.min.js基本都是一家模块化处理过的,不需要重新再打包
两者对比一下:
-
IgnorePlugin
直接不引入,代码中不存在 -
noParse
引入,但不再打包编译。比如xxx.min
happyPack 多进程打包
【注意】大型项目,构建速度明显变慢时,作用才能明显。否则,反而会有副作用。
webpack 是基于 nodejs 运行,nodejs 是单线程的,happyPack 可以开启多个进程来进行构建,发挥多核 CPU 的优势。
const path = require('path')
const HappyPack = require('happypack')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
exclude: path.resolve(__dirname, 'node_modules')
}
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// ... 其它配置项
})
]
}
ParallelUglifyPlugin 多进程压缩 js
webpack 默认用内置的 uglifyJS 压缩 js 代码。
大型项目压缩 js 代码时,也可能会慢。可以开启多进程压缩,和 happyPack 同理。
const path = require('path')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};
适用场景:项目较大,打包较慢,开启多进程能提高速度。项目较小的情况下,不建议,
因为相对而言进程有开销,反而降低了打包速度
自动刷新
watch 默认关闭。但 webpack-dev-server 和 webpack-dev-middleware 里 Watch 模式默认开启。
先验证下 webpack 是否能默认自动刷新页面 ???
module.export = {
watch: true, // 开启监听,默认为 false
// 注意,开启监听之后,webpack-dev-server 会自动开启刷新浏览器!!!
// 监听配置
watchOptions: {
ignored: /node_modules/, // 忽略哪些
// 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
// 默认为 300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
// 默认每隔1000毫秒询问一次
poll: 1000
}
}
自动更新
image.png一般开启devServer就会自动更新(页面会自动刷新)
热更新
- 速度更慢
- 网页当前的状态会丢失,如 input 输入的文字,图片要重新加载,vuex 和 redux 中的数据
操作步骤
- 把现有的 watch 注释掉
- 增加以下代码
- 修改 css less 实验 —— 热替换生效
- 修改 js 实验 —— 热替换不生效
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
entry:{
// 为每个入口都注入代理客户端
index:[
'webpack-dev-server/client?http://localhost:8080/',
'webpack/hot/dev-server',
path.join(srcPath, 'index.js')
],
// other 先不改了
},
plugins: [
// 该插件的作用就是实现模块热替换,实际上当启动时带上 `--hot` 参数,会注入该插件,生成 .hot-update.json 文件。
new HotModuleReplacementPlugin(),
],
devServer:{
// 告诉 DevServer 要开启模块热替换模式
hot: true,
}
};
js 热替换不生效,是因为我们要自己增加代码逻辑。
// 增加,开启热更新之后的代码逻辑
if (module.hot) {
module.hot.accept(['./math'], () => {
const sumRes = sum(10, 20)
console.log('sumRes in hot', sumRes)
})
}
最后,热替换切勿用于 prod 环境!!!
DllPlugin
Dll 动态链接库,其中可以包含给其他模块调用的函数和数据。
要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:
- 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
- 页面依赖的所有动态链接库需要被加载。
为什么给 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢?
- 前端依赖于第三方库
vue
react
等 - 其特点是:体积大,构建速度慢,版本升级慢
- 同一个版本,只需要编译一次,之后直接引用即可 —— 不用每次重复构建,提高构建速度
???????
Webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:
- DllPlugin 插件:打包出 dll 文件
- DllReferencePlugin 插件:使用 dll 文件
打包出 dll 的过程
- 增加 webpack.dll.js
- 修改 package.json scripts
"dll": "webpack --config build/webpack.dll.js"
-
npm run dll
并查看输出结果
使用 dll
- 引入
DllReferencePlugin
- babel-loader 中排除
node_modules
- 配置
new DllReferencePlugin({...})
- index.html 中引入
react.dll.js
- 运行 dev
总结 - 提高构建效率的方法
哪些可用于线上,哪些用于线下
-
优化 babel-loader(可用于线上)
-
IgnorePlugin 避免引入哪些模块(可用于线上)
-
noParse 避免重复打包(可用于线上)
-
happyPack 多进程打包(可用于线上)
-
ParallelUglifyPlugin 多进程压缩 js(可用于线上)
-
自动刷新(仅开发环境)
-
热更新(仅开发环境)
-
DllPlugin(仅开发环境)
webpack 性能优化
产出代码优化
使用 production
- 开启压缩代码
- 开启 tree shaking:用来去除无用代码的代码(必须是 ES6 Module 语法才行)
ES6 Module 和 commonjs 的区别
- ES6 Module 是静态引入,编译时引入
- commonjs 是动态引入,执行时引入
// commonjs
let apiList = require('../config/api.js')
if (isDev) {
// 可以动态引入,执行时引入
apiList = require('../config/api_dev.js')
}
import apiList from '../config/api.js'
if (isDev) {
// 编译时报错,只能静态引入
import apiList from '../config/api_dev.js'
}
小图片 base64 编码
image.pngbundle 加 hash
image.png使用 CDN加速
1、配置 publicPath
image.png
产物dist是cdn开头: image.png
2、将产物地址上传到cdn,成功后就能访问了
提取公共改代码
懒加载
使用production
开启mode:production
scope hosting 将多个函数合并到一个函数中
作用:代码体积更小、创建函数作用域更少、代码可读性更好
使用前后的对比,使用的好处、
优化前:
源码:
image.png
优化前打包后产物:
image.png
会各自创建函数作用域,代码量较大
优化后打包产物:
image.png
体积更小
配置如下:
//1.引入插件
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
]
}
同时,考虑到 Scope Hoisting 依赖源码需采用 ES6 模块化语法,还需要配置 mainFields
。因为大部分 Npm 中的第三方库采用了 CommonJS 语法,但部分库会同时提供 ES6 模块化的代码,为了充分发挥 Scope Hoisting 的作用。
webpack 原理和二次开发
这部分是面试的加分项,大家不要深究细节。即便面试被问到,实际工作中应用的概率也非常小。
webpack 本身就是一个工具,只是用来打包构建。而且发展多年已经成熟完善,日常的开发场景都能满足。
webpack 构建流程
几个核心概念
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
Webpack 的构建流程可以分为以下三大阶段:
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
- 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
- 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
开发 loader
以 less-loader
为例,回顾一下使用规则
{
test: /\.less$/,
// 注意顺序
loader: ['style-loader', 'css-loader', 'less-loader']
}
所以,loader 的作用:
- 一个代码转换器,将 less 代码转换为 css 代码
- 再例如 vue-template-compiler
- 再例如 babel 编译 jsx
所以一个 loader 的基本开发模式:
const less = require('node-less')
module.exports = function(source) {
// source 即 less 代码,需要返回 css 代码
return less(source)
}
以上是 loader 的基本开发方式,实际开发中可能还会有更多要求
- 支持 options ,如
url-loader
的使用 - 支持异步 loader
- 缓存策略
// options
const loaderUtils = require('loader-utils')
module.exports = function(source) {
// 获取 loader 的 options
const options = loaderUtils.getOptions(this)
return source // 示例,直接返回 source 了
}
// 异步 loader
module.exports = function(source) {
// 使用异步 loader
const callback = this.async()
// 执行异步函数,如读取文件
someAsyncFn(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast)
})
}
// 缓存
module.exports = function(source) {
// 关闭该 Loader 的缓存功能(webpack 默认开启缓存)
this.cacheable(false)
return source
}
babel
本节主要解决以下问题,相信很多同学都很懵
- babel-polyfill —— 7.4 之后弃用,推荐直接使用 corejs 和 regenerator ??
- babel-runtime
初始化环境
- 安装 babel 插件
npm i @babel/cli @babel/core @babel/preset-env -D
- 新建
.babelrc
,配置 preset-env - 新建
src/index.js
,写一个箭头函数 - 运行
npx babel src/index.js
,看结果
使用 polyfill
什么是 babel-polyfill
-
什么是 polyfill ?—— 即一个补丁,引入以兼容新 API(注意不是新语法,如箭头函数),如搜索“Object.keys polyfill” 和 “Promise polyfill”
-
core-js 集合了所有新 API 的 polyfill 。https://github.com/zloirock/core-js
-
regenerator 是 generator 的 polyfill 。 https://github.com/facebook/regenerator
-
babel-polyfill 即 core-js 和 regenerator 的集合,它只做了一层封装而已。
基本使用
-
src/index.js
中写一个 Promise,打包看结果 -
npm install --save @babel/polyfill
【注意】要--save
- 然后引入
import '@babel/polyfill'
- 再打包,看结果
- 解释:babel 仅仅是处理 ES6 语法,并不关心模模块化的事情。模块化归 webpack 管理
- 全部引入 polyfill ,体积很大
按需加载
- 新增
"useBuiltIns": "usage"
(注意要改写 preset 的 json 结构) - 删掉入口的
import '@babel/polyfill'
- 再打包,看结果
- 提示选择 core-js 的版本,增加
"corejs": 3
- 只引入了 promise 的 polyfill
- 提示选择 core-js 的版本,增加
使用 runtime
babel-polyfill 的问题 —— 会污染全局变量
- 如果是一个网站或者系统,无碍
- 如果是做一个第三方工具,给其他系统使用,则会有问题
- 不能保证其他系统会用 Promise 和 includes 做什么,即便他们用错了,那你也不能偷偷的给更改了
// 源代码
Promise.resolve(100).then(data => data);
[10, 20, 30].includes(20);
// 结果 —— 可以看到,Promise 和 includes 都未改动,因为以注入全局变量了
"use strict";
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
Promise.resolve(100).then(function (data) {
return data;
});
[10, 20, 30].includes(20);
使用 babel-runtime
image.png
npm install --save-dev @babel/plugin-transform-runtime
-
npm install --save @babel/runtime
,注意是--save
- 配置
"plugins": ["@babel/plugin-transform-runtime"]
- 其中
"corejs": 3,
v3 支持 API 如数组 includes ,v2.x 不支持
- 其中
- 删掉
"useBuiltIns": "usage"
- 运行代码
总结
-
babel-polyfill 是什么,core-js 是什么
babel-polyfill是core-js和regenerate的合集,但目前已经废弃 -
babel-polyfill 按需加载的配置
-
babel-polyfill 和 babel-runtime 的不同应用场景
前端为什么要进行打包和构建?
代码层面:
1)体积更小(Tree-Sharking、压缩、合并,加载更快)
2)编译高级语言或语法(TS、ES6+、模块化、scss)
3)兼容性和错误检查(Polyfill、postcss、eslint)
工程化/团队规范方面:
统一高效的开发环境
统一的构建流程和产出标准
集成公司构建规范(提测、上线等)
loader和plugin的区别?
loader模块转换器,如less-》css
plugin扩展插件,如HtmlWebpackPlugin
常用loader和plugin有哪些?
babel和webpack的区别?
- babel-js新语法编译工具,不关心模块化
-webpack-打包工具,是多个loader、plugin的集合
如何产出一个lib?
babel-polyfill和babel-runtime的区别
-babel-polyfill会污染全局,而babel-runtime不会污染全局
产出第三方lib要用babel-runtime
webpack如何实现懒加载
import()