什么是webpack?
webpack
是一套基于NodeJS的"模块打包工具",在webpack
刚推出的时候就是一个单纯的JS模块打包工具,可以将多个模块的JS文件合并打包到一个文件中。但是随着时间的推移、众多开发者的追捧和众多开发者的贡献,现在webpack
不仅仅能够打包JS模块, 还可以打包CSS/LESS/SCSS/
图片等其它文件。
webpack来打包JS模块
项目上线时将用到的所有模块都合并到一个文件中,可以避免单个导入,使得导入资源变多, 请求次数变多了, 网页性能也就差了。
安装webpack
npm init -y
npm install --save-dev webpack
npm install --save-dev webpack-cli
- 在终端中输入打包的指令
npx webpack index.js
注意点:
index.js 就是需要打包的文件
打包之后的文件会放到 dist 目录中, 名称叫做 main.js
webpack配置文件
// webpack.config.js
const path = require("path");
module.exports = {
/*
mode: 指定打包的模式, 模式有两种,一种是开发模式(development): 不会对打包的JS代码进行压缩;还有一种就是上线(生产)模式(production): 会对打包的JS代码进行压缩;默认生产模式
*/
mode: "development", // "production" | "development"
// entry: 指定需要打包的文件
entry: "./index.js",
// output: 指定打包之后的文件输出的路径和输出的文件名称
output: {
// filename: 指定打包之后的JS文件的名称
filename: "bundle.js",
// path: 指定打包之后的文件存储到什么地方
path: path.resolve(__dirname, "bundle")
}
};
// 执行 npx webpack 打包文件
🪔 配置文件的名称必须叫做: webpack.config.js
, 否则直接输入 npx webpack
打包会出错,如果要使用其它名称, 那么在输入打包命令时候必须通过 --config 指定配置文件名称,npx webpack --config xxx
。也可以在package.json
里配置命令:
"scripts": {
"test": "npx webpack --config index.js"
},
sourcemap
webpack打包后的文件会自动添加很多代码, 在开发过程中非常不利于我们去调试,因为如果运行webpack打包后的代码,错误提示的内容也是打包后文件的内容。所以为了降低调试的难度, 提高错误代码的阅读性, 我们就需要知道打包后代码和打包之前代码的映射关系。只要有了这个映射关系我们就能很好的显示错误提示的内容, 存储这个映射关系的文件我们就称之为sourcemap
。sourcemap
存储打包前代码和打包后代码的影射关系。
⚙️ https://www.webpackjs.com/configuration/devtool/
在webpack.config.js
中添加
devtool: "xxx",
各配置项说明:
eval:
不会单独生成sourcemap文件, 会将映射关系存储到打包的文件中, 并且通过eval存储
优势: 性能最好
缺点: 业务逻辑比较复杂时候提示信息可能不全面不正确
source-map:
会单独生成sourcemap文件, 通过单独文件来存储映射关系
优势: 提示信息全面,可以直接定位到错误代码的行和列
缺点: 打包速度慢
inline:
不会单独生成sourcemap文件, 会将映射关系存储到打包的文件中, 并且通过base64字符串形式存储
cheap:
生成的映射信息只能定位到错误行不能定位到错误列
module:
不仅希望存储我们代码的映射关系, 还希望存储第三方模块映射关系, 以便于第三方模块出错时也能更好的排错
企业开发配置:
development: cheap-module-eval-source-map
只需要行错误信息, 并且包含第三方模块错误信息, 并且不会生成单独sourcemap文件。
production: cheap-module-source-map
只需要行错误信息, 并且包含第三方模块错误信息, 并且会生成单独sourcemap文件。
// webpack.config.js 中添加
devtool: "cheap-module-source-map",
file-loader
1、什么是loader
webapck的本质是一个模块打包工具,所以webpack默认只能处理JS文件,不能处理其他文件,因为其他文件中没有模块的概念, 但是在企业开发中我们除了需要对JS进行打包以外,还有可能需要对图片/CSS等进行打包,所以为了能够让webpack能够对其它的文件类型进行打包,在打包之前就必须将其它类型文件转换为webpack能够识别处理的模块,用于将其它类型文件转换为webpack能够识别处理模块的工具我们就称之为loader
。
2、如何使用loader
webpack中的loader都是用NodeJS编写的,在企业开发中常用的loader已经有现成的,没有必要自己编写,我们只需要安装、配置、使用即可。
2.1、通过npm安装对应的loader
2.2、按照loader作者的要求在webpack进行相关配置
2.3、使用配置好的loader
3.fileloader使用
https://www.webpackjs.com/loaders/file-loader/
安装file-loader,npm install --save-dev file-loader
// 在 webpack.config.js 中配置 file-loader
module: { // module: 告诉 webpack 如何处理 webpack 不能够识别的文件
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
}
]
}
const icon = require("./test.jpg");
// file-loader处理之后我们导入图片拿到的是图片打包之后的地址
console.log(icon); // 6f1d3cc36f54ed3170a108a8d253f8f6.jpg
默认情况下fileloader生成的图片名就是文件内容的 MD5 哈希值,想打包后不修改图片的名称,那么可以新增配置 name: "[name].[ext]"
其它命名规则详见: placeholders
![](/Users/meihao/Library/Application Support/typora-user-images/image-20210327204531564.png)
注意点:默认情况下fileloader会将生成的图片放到dist根目录下面,如果想打包之后放到指定目录下面, 那么可以新增配置 outputPath: "images/"
注意点:如果需要将图片托管到其它服务器, 那么只需在打包之前配置 publicPath: "托管服务器地址"
即可
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]', // 指定打包后文件名称
outputPath: 'images/', // 指定打包后文件存放目录
publicPath: 'http://www.xxx.com/images/' // 指定托管服务器地址(统一替换图片地址)
}
}
]
}
urlloader
url-loader
功能类似于 file-loader
,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL
。
![image-20210327220951235](/Users/meihao/Library/Application Support/typora-user-images/image-20210327220951235.png)
// 配置urlloader
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
name: "[name].[ext]",
outputPath: "/images",
limit: 1024
}
}]
limit
: 指定图片限制的大小,如果被打包的图片超过了限制的大小,就会将图片保存为一个文件;如果被打包的图片没有超过限制的大小,就会将图片转换成base64的字符串。这个也是url-loader
和file-loader
的区别。
💡注意点:对于比较小的图片, 我们将图片转换成base64的字符串之后, 可以提升网页的性能(因为减少了请求的次数),对于比较大的图片,,哪怕我们将图片转换成了base64的字符串之后,也不会提升网页的性能,还有可能降低网页的性能。因为图片如果比较大,那么转换之后的字符串也会比较多,那么网页的体积就会表达,那么访问的速度就会变慢。
css-loader
和图片一样webpack默认能不能处理CSS文件, 所以也需要借助loader将CSS文件转换为webpack能够处理的类型。
// 配置 css-loader
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
css-loader
: 解析css文件中的@import依赖关系(@import "other.css";)
style-loader
: 将webpack处理之后的内容插入到HTML的HEAD代码中
💡loader
特点:单一原则, 一个loader只做一件事情;多个loader会按照从右至左, 从下至上的顺序执行。
// 从右至左
[ 'style-loader', 'css-loader' ]
先执行css-loader解析css文件关系拿到所有内容,
再执行style-loader将内容插入到HTML的HEAD代码中
// 从下至上
[{ loader: "style-loader" }, { loader: "css-loader" }] // 和从右至左一样,两种不同的写法
先执行css-loader解析css文件关系拿到所有内容,
ES6-Module
ES6模块化
在ES6出现之前,JS不像其他语言拥有“模块化”这一概念,于是为了支持JS模块化。我们使用类、立即执行函数或者第三方插件(RequireJS、seaJS)
来实现模块化;但是在ES6出现之后, 上述解决方案都已经被废弃, 因为ES6中正式引入了模块化的概念。
ES6模块化模块和NodeJS中一样,一个文件就是一个模块(相当于一个对象),模块中的数据都是私有的;ES6模块化模块和NodeJS中一样, 可以通过对应的关键字暴露模块中的数据, 可以通过对应的关键字导入模块, 使用模块中暴露的数据。
ES6模块化使用:
常规导出
// 分开导入导出
export xxx;
import {xxx} from "path";
// 一次性导入导出
export {xxx, yyy, zzz};
import {xxx, yyy, zzz} from "path";
💡 注意点: 接收导入变量名必须和导出变量名一致 ,因为导入的时候相当于ES6的解构赋值。如果想修改接收变量名可以通过 xxx as newName方式,变量名被修改后原有变量名自动失效。
默认导入导出
export default xxx;
import xxx from "path";
💡注意点:一个模块只能使用一次默认导出, 多次无效;默认导出时, 导入的名称可以和导出的名称不一致。
less-loader
{
test: /\.less$/,
use: [{ loader: "style-loader"}, { loader: "css-loader" }, { loader: "less-loader" }]
}
sass-loader
{
test: /\.scss$/,
use: [{ loader: "style-loader" // 将 JS 字符串生成为 style 节点
}, {
loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
}, {
loader: "sass-loader" // 将 Sass 编译成 CSS
}]
}
postcss
https://www.postcss.com.cn/ PostCSS
和 sass / less 不同, 它不是CSS预处理器,PostCSS是一个用 JavaScript 工具和插件转换 CSS 代码的工具,PostCSS有许多非常好用的插件。
好用的插件:
autoprefixer (自动补全浏览器前缀)
postcss-pxtorem (自动把px代为转换成rem)
autoprefixer
使用之前先创建postcss-loader
配置文件:
// postcss.config.js // https://github.com/browserslist/browserslist#queries
module.exports = {
plugins: {
"autoprefixer": {
"overrideBrowserslist": [
// "ie >= 8", // 兼容IE7以上浏览器
// "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
// "chrome >= 35", // 兼容谷歌版本号大于35浏览器,
// "opera >= 11.5" // 兼容欧朋版本号大于11.5浏览器,
"chrome >= 36",
]
}
}
};
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader', 'postcss-loader' ]
}
postcss-pxtorem
module.exports = {
plugins: {
"autoprefixer": {
"overrideBrowserslist": [
"ie >= 8", // 兼容IE7以上浏览器
"Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
"chrome >= 35", // 兼容谷歌版本号大于35浏览器,
"opera >= 11.5" // 兼容欧朋版本号大于11.5浏览器,
]
},
"postcss-pxtorem": {
rootValue: 16, // 根元素字体大小
// propList: ['*'] // 可以从px更改到rem的属性
propList: ["height"]
}
}
};
unitPrecision (Number) 允许REM单位增长到的十进制数。
propList ( array ) 可以从px更改到rem的属性。
值需要精确匹配。
使用通配符 * 启用所有属性。 示例:['*']
在单词的开头或者结尾使用 *。 ( ['*position*'] 将匹配 background-position-y )
使用 "与" 属性不匹配。! 示例:['*','letter-spacing']!
将"非"前缀与其他前缀合并。 示例:['*','font*']!
selectorBlackList ( array ) 要忽略和离开的选择器。
如果值为 字符串,它将检查选择器是否包含字符串。
['body'] 将匹配 .body-class.
如果值为 regexp,它将检查选择器是否匹配正则表达式。
[/^body$/] 将匹配 body,但不匹配 .body
replace (Boolean) 替代包含rems的规则,而不是添加回退。
mediaQuery (Boolean) 允许在媒体查询中转换 px。
minPixelValue (Number) 设置要替换的最小像素值。
css模块化
默认情况下通过import "./xxx.css"
导入的样式是全局样式,也就是只要被导入,在其它文件中也可以使用。如果想要导入的CSS文件只在导入的文件中有效,那么就需要开启CSS模块化。
{
loader: "css-loader",
options: {
modules: true // 开启CSS模块化
}
}
// index.css
.size{
height: 200px;
}
// index.js
import cssModule from "./index.css"
console.log(cssModule); // {size: ****}
let oImg = document.createElement("img");
oImg.setAttribute("class", cssModule.size);
iconfont-loader
打包字体图标,字体图标中也用到了url用到了文件,所以我们需要通过file-loader来处理字体图标文件。
{
test: /\.(eot|json|svg|ttf|woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]', // 指定打包后文件名称
outputPath: 'font/', // 指定打包后文件存放目录
}
}
]
},
html-webpack-plugin
插件(plugin
):plugin
用于扩展webpack的功能,loader也是变相的扩展了webpack ,但是它只专注于转化文件这一个领域。而plugin的功能更加的丰富,而不仅局限于资源的加载。
HtmlWebpackPlugin
会在打包结束之后自动创建一个index.html
,并将打包好的JS自动引入到这个文件中。
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 前提
/* plugins: 告诉webpack需要新增一些什么样的功能 */
plugins: [new HtmlWebpackPlugin({
// 指定打包的模板, 如果不指定会自动生成一个空的
template: "./index.html",
minify: {
collapseWhitespace: true, // 告诉htmlplugin打包之后的html文件需要压缩
}
})]
clean-webpack-plugin
webpack-clean-plugin
会在打包之前将我们指定的文件夹清空,应用场景每次打包前将dist
目录清空, 然后再存放新打包的内容,避免新老混淆问题。
webpack-clean-plugin
使用:https://github.com/johnagan/clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [new CleanWebpackPlugin()]
copy-webpack-plugin
在打包项目的时候除了JS/CSS/图片/字体图标
等需要打包以外, 可能还有一些相关的文档也需要打包,文档内容是固定不变的,我们只需要将对应的文件拷贝到打包目录中即可,那么这个时候我们就可以使用copy-plugin
来实现文件的拷贝。
copy-webpack-plugin
使用:https://github.com/johnagan/clean-webpack-plugin
const CopyWebpackPlugin = require('copy-webpack-plugin');
plugins: [new CopyWebpackPlugin([
{from:"doc", to:"./doc"}
])];
mini-css-extract-plugin
mini-css-extract-plugin
是一个专门用于将打包的CSS内容提取到单独文件的插件,前面我们通过style-loader
打包的CSS都是直接插入到head中的。
mini-css-extract-plugin
使用:https://webpack.js.org/plugins/mini-css-extract-plugin/
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 引入
new MiniCssExtractPlugin({ // 增加插件
filename: './css/[name].css',
})
// 替换style-loader
// loader: "style-loader"
loader: MiniCssExtractPlugin.loader // 替换
💡注意点: 如果相关文件资源无法显示, 需要根据打包后的结构手动设置公开路径
options: {
publicPath: "xxx"
}
压缩css代码
安装CSS代码压缩插件:npm install --save-dev optimize-css-assets-webpack-plugin
安装JS代码压缩插件:npm install --save-dev terser-webpack-plugin
// 配置webpack优化项:
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})],
}
// 由于配置了webpack的optimization.minimizer项目会覆盖默认的JS压缩选项,所以JS代码也需要通过插件压缩
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const TerserJSPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
}
}
watch
使用 watch
,webpack 可以监听打包文件变化,当它们修改后会重新编译打包。
watch
在webpack.config.js
相关配置
module.exports = {
watch: true,
watchOptions: { // 进行额外的配置
aggregateTimeout: 300, // 防抖, 和函数防抖一样, 改变过程中不重新打包, 只有改变完成指定时间后才打包
poll: 1000, // 每隔多少时间检查一次变动
ignored: /node_modules/ // 排除一些巨大的文件夹, 不需要监控的文件夹
},
}
webpack-dev-server
webpack-dev-server
和watch
一样可以监听文件变化,webpack-dev-server
可以将我们打包好的程序运行在一个服务器环境下,webpack-dev-server可以解决开发中“开发阶段”的跨域问题。
webpack-dev-server
使用:https://www.npmjs.com/package/webpack-dev-server
devServer: {
contentBase: "./bundle", // 打包后的目录
open: true, // 是否自动在浏览器中打开
port: 8080 // 服务器端口号
},
// package.json 中修改指令
"scripts": {
"test": "npx webpack-dev-server --config webpack.config.js"
},
CORS
利用webpack-dev-server
代理解决跨域问题
devServer: {
contentBase: "./bundle",
open: true,
port: 8080,
/*
proxy: {
// 以下配置的含义:
// 当我们在代码中发送请求到/user的时候, devServer就会自动将我们请求的地址替换为
// http://127.0.0.1:3000/user
"/user": {
target: "http://127.0.0.1:3000",
changeOrigin: true, // 域名跨域
secure: false, // https跨域
},
"/login": {
target: "http://127.0.0.1:3000",
changeOrigin: true, // 域名跨域
secure: false, // https跨域
},
}
*/
proxy: [{
context: ["/user", "/login"],
target: "http://127.0.0.1:3000",
changeOrigin: true, // 域名跨域
secure: false, // https跨域
pathRewrite:{"": "/api"} // 路径重写, 将路径中的api替换为空
}]
/* 注意点: devServer只能解决开发阶段的跨域问题, 并不能解决项目上线之后的跨域问题,项目上线之后是将打包好的文件上传到服务器, 而打包好的文件中并没有devServer */
},
常用配置附录
target:要使用url模块解析的url字符串
forward:要使用url模块解析的url字符串
agent:要传递给http(s).request的对象(请参阅Node的https代理和http代理对象)
ssl:要传递给https.createServer()的对象
ws:true / false,是否代理websockets
xfwd:true / false,添加x-forward标头
secure:true / false,是否验证SSL Certs
toProxy:true / false,传递绝对URL作为路径(对代理代理很有用)
prependPath:true / false,默认值:true - 指定是否要将目标的路径添加到代理路径
ignorePath:true / false,默认值:false - 指定是否要忽略传入请求的代理路径(注意:如果需要,您必须附加/手动)。
localAddress:要为传出连接绑定的本地接口字符串
changeOrigin:true / false,默认值:false - 将主机标头的原点更改为目标URL
HMR-热更新
通过webpack-dev-server
自动打包并没有真正的放到指定的目录中,因为读写磁盘是非常耗时和消耗性能的。所以为了提升性能webpack-dev-server
将转换好的内容直接放到了内存中。
通过webpack-dev-server
可以实现实时监听打包内容的变化,每次打包之后都会自动刷新网页, 但是正是因为每当内容被修改时都会自动刷新网页,所以给我们开发调试带来了很多不便,这时就需要通过HMR
插件来优化调试开发。
热更新监听 css 变化
HMR(HotModuleReplacementPlugin)
热更新插件,会在内容发生改变的时候更新修改的内容但是不会重新刷新网站。HMR(HotModuleReplacementPlugin)
是一个内置插件, 所以不需要任何安装直接引入webpack
模块即可使用。
// webpack.config.js中修改
const Webpack = require("webpack");
devServer: {
...
hot: true, // 开启热更新, 只要开启了热更新就不会自动刷新网页了
hotOnly: true // 哪怕不支持热更新也不要刷新网页
},
plugins: [
new Webpack.HotModuleReplacementPlugin() // 使用插件
]
💡 // 打包CSS规则修改
{
test: /\.css$/,
use:[{
// loader: "style-loader"
loader: MiniCssExtractPlugin.loader, // MiniCssExtractPlugin插件打包提取css为一个文件,配置热更新需要做如下配置:
options:{
hmr: true
}
},
...]
},
热更新监听 js 变化
// index.js
import addSpan from "./test.js";
addSpan();
if(module.hot){ // 判断当前有没有开启热更新
console.log(module)
/*
{
id: "./src/js/index.js"
loaded: true
webpackPolyfill: 1
}
*/
// 告诉热更新需要监听哪一个JS模块的变化,变化后执行什么逻辑
module.hot.accept("./test.js", function () {
addSpan();
});
}
babel
-转换ES678语法
在企业开发中为了兼容一些低级版本的浏览器, 我们需要将ES678高级语法转换为ES5低级语法,否则在低级版本浏览器中我们的程序无法正确执行,默认情况下webpack
是不会将我们的代码转换成ES5
低级语法的, 如果需要转换我们需要使用babel
来转换。
babel
使用:https://babeljs.io/ 安装转换到ES5需要的相关包:npm install --save-dev babel-loader @babel/core @babel/preset-env
. https://babeljs.io/docs/en/babel-preset-env
// webpack.config.js中添加配置规则配置(module.rules)配置
{ // 打包JS规则
test: /\.js$/,
exclude: /node_modules/, // 告诉webpack不处理哪一个文件夹
loader: "babel-loader",
options: {
"presets": [["@babel/preset-env", { // 通过这个插件转化
targets: { // 告诉babel你需要兼容哪些浏览器
"chrome": "58",
},
}]]
}
},
💡在开发中默认情况下babel
会将所有高于ES5
版本的代码都转换为ES5代码,但是有时候可能我们需要兼容的浏览器已经实现了更高版本的代码, 那么这个时候我们就不需要转换。因为如果浏览器本身已经实现了,我们再去转换就会增加代码的体积,就会影响到网页的性能,所以我们通过配置presets
的方式来告诉webpack
我们需要兼容哪些浏览器,然后babel
就会根据我们的配置自动调整转换方案,如果需要兼容的浏览器已经实现了,就不转换了。
polyfill
对于有对应关系的语法而言,通过babel
可以实现自动转换,但是对于没有对应关系的语法(Es678中新增的)而言, 还需要额外的配置。
💡 有对应关系就是指ES5中有对应的概念, 例如: 箭头函数对应普通函数,let
对应var
,这个就叫做有对应关系。没有对应关系就是指E5中根本就没有对应的语法,例如Promise
, includes
等方法是ES678新增的,ES5中根本就没有对应的实现,这个时候就需要再增加一些额外配置,让babel
自己帮我们实现对应的语法。
👉 安装不存在代码的实现包 npm install --save @babel/polyfill
👉 在用到不存在代码的文件中导入包 import "@babel/polyfill";
// webpack.config.js中添加配置规则配置(module.rules)
{
test: /\.js$/,
exclude: /node_modules/, // 告诉webpack不处理哪一个文件夹
loader: "babel-loader",
options: {
"presets": [["@babel/preset-env", {
targets: { // 告诉babel你需要兼容哪些浏览器
"chrome": "58",
},
useBuiltIns: "usage" // 只解析打包我代码里使用的,添加了这个配置,import "@babel/polyfill;不需要了
}]],
}
},
🦠 在文件中导入polyfill模块的弊端:直接导入polyfill
的方式只适用于一般项目开发, 但是如果是在编写一些第三方模块的时候这种方式会出现一些问题,因为这种方式是通过全局变量的方式来注入代码,会污染全局环境. 所以我们再来看一下polyfill
的第二种配置方式。
安装相关依赖:
👉 npm install --save @babel/polyfill
👉 npm install --save-dev @babel/plugin-transform-runtime
👉 npm install --save @babel/runtime
// webpack.config.js中添加配置规则配置(module.rules)
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"presets": [["@babel/preset-env", {
// useBuiltIns: "usage"
}]],
"plugins": [ // 添加如下配置
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
},
// "corejs": false, 还是全局注入,还是会污染全局环境
// "corejs": 2, 则不会污染全局环境
// npm install --save @babel/runtime-corejs2 // 需要安装如下配置
👉 npm install --save @babel/runtime-corejs2
babel-使用技巧
1.1查看错误提示
1.2根据错误信息查询文档
1.3根据文档缺什么安装配置什么
https://babeljs.io/
html-withimg-loader
file-loader
或者url-loader
可以将JS或者CSS中用到的图片打包到指定目录中,但是file-loader
或者url-loader
并不能将HTML中用到的图片打包到指定目录中,需要借助一个名称叫做html-withimg-loader
的加载器来实现HTML中图片的打包。
使用:npm install html-withimg-loader --save
https://www.npmjs.com/package/html-withimg-loader
// webpack.config.js中添加配置规则配置(module.rules)
{
test: /\.(htm|html)$/i,
loader: 'html-withimg-loader'
},
图片压缩
在企业开发中为了提升网页的访问速度, 我们除了会压缩HTML/CSS/JS以外,还会对网页上的图片进行压缩和合并, 压缩可以减少网页体积, 合并可以
安装:
npm install image-webpack-loader --save-dev
npm install img-loader --save-dev
https://www.npmjs.com/package/image-webpack-loader
https://www.npmjs.com/package/img-loader
// 打包图片规则
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1024, // 指定图片限制的大小
name: '[name].[ext]', // 指定打包后文件名称
outputPath: 'images/', // 指定打包后文件存放目录
}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65 // 这个值表示压缩比例,越小压缩与大,越模糊;最大100
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
},
]
},
图片合并
过去为了减少网页请求的次数, 需要"UI设计师"给我们提供精灵图,并且在使用时还需要手动的去设置每张图片的位置,有了webpack之后我们只需要让"UI设计师"给我们提供切割好的图片,我们可以自己合成精灵图, 并且还不用手动去设置图片的位置。
安装:
npm install postcss-sprites --save-dev
npm install webpack-spritesmith --save-dev
https://www.npmjs.com/package/postcss-sprites
https://www.npmjs.com/package/webpack-spritesmith
// postcss.config.js 需要添加如下配置
module.exports = {
plugins: {
... // 其他工具配置
"postcss-sprites": {
spritePath: "./bundle/images", // 告诉webpack合并之后的图片保存到什么地方
groupBy: function (image) { // 告诉webpack合并图片的时候如何分组
// url: '../images/animal/animal1.png',
let path = image.url.substr(0, image.url.lastIndexOf("/"));
// 获取到存放图片对应的文件夹名字 animal
let name = path.substr(path.lastIndexOf("/") + 1);
return Promise.resolve(name);
},
// 设置非png图片才合并
filterBy: function (image) {
let path = image.url;
if(!/\.png$/.test(path)){
return Promise.reject();
}
return Promise.resolve();
}
}
}
};
】
打包合并后的图片:sprite.animal.png Sprite.headers.png
// webpack.config.js 需要设置打包图片的存放路径
// 打包图片规则
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
name: '[name].[ext]',
outputPath: 'images/',
// 指定打包后文件存放目录的公共路径
publicPath: 'http://localhost:9090/images/' //要把图片压缩的配置去掉
// publicPath: 'file:///Users/meihao/code/learning/webpack图片压缩/bundle/images' // 如果不使用devserver,就需要这样配置
}
},
http://localhost:9090/images/sprite.png 打开的合并图片:
图片路径问题
webpack
打包之后给我们的都是相对路径,所以会导致在html中使用的图片能够正常运行,在css中的图片不能正常运行。例如: 打包之后的路径是images/test.jpg
,那么在html中,会去html文件所在路径下找images
,正好能找到所以不报错,但是在css中, 会去css文件所在路径下找images
,找不到所以报错。和上面遇到的问题一样。
解决办法:在开发阶段将publicPath
设置为dev-server
服务器地址,在上线阶段将publicPath
设置为线上服务器地址。
eslint
ESLint 是一个插件化的 javascript 代码检测工具,它可以用于检查常见的 JavaScript 代码错误,也可以进行"代码规范"检查。
安装:
npm install eslint --save-dev
npm install eslint-loader --save-dev
https://www.webpackjs.com/loaders/
http://eslint.cn/
https://eslint.org/docs/user-guide/configuring
// webpack.config.js 需要设置eslint语法检查规则
// 检查编码规范的规则
{
enforce: "pre", // enforce: "pre"作用: 让当前的loader再其它loader之前执行,避开rules的从下至上执行规则。
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve(__dirname, "src"),
loader: 'eslint-loader',
options: {
// eslint options (if necessary)
fix: true // 自动修复
},
},
// 添加 .eslintrc.js 文件
module.exports = {
root: true, // 永远为true
parserOptions: { // parser: 'babel-eslint'
"ecmaVersion": 10, // 默认设置为 3,5(默认), 你可以使用 6、7、8、9 或 10 来指定你想要使用的 ECMAScript 版本
"sourceType": "module", // 设置为 "script" (默认) 或 "module"(如果你的代码是 ECMAScript 模块)。
/*
ecmaFeatures - 这是个对象,表示你想使用的额外的语言特性:
globalReturn - 允许在全局作用域下使用 return 语句
impliedStrict - 启用全局 strict mode (如果 ecmaVersion 是 5 或更高)
jsx - 启用 JSX
*/
"ecmaFeatures": {}
},
// 指定代码运行的宿主环境
env: {
browser: true, // 浏览器
node: true, // node
/*
支持 ES6 语法并不意味着同时支持新的 ES6 全局变量或类型(比如 Set 等新类型)。
对于 ES6 语法,使用 { "parserOptions": { "ecmaVersion": 6 } }
*/
es6: true,
},
extends: [
/*
引入standard代码规范
*/
'standard' // https://github.com/standard/standard/blob/master/docs/RULES-en.md
],
/*
扩展或覆盖规则
*/
rules: {
semi: ["error", "always"], // 强制语句结束添加,分号
indent: ["error", 4], // 强制缩进2个空格
'space-before-function-paren': ['error', 'never'], // 方法名和刮号之间不加空格
"no-unexpected-multiline": "off"
}
};
通过阅读打包错误信息来修复不符合规范语法非常低效,所以我们可以通过webstrom
插件来帮我们完成提,webstrom--setting--eslint--autoxxx
。
配置文件优化
在开发阶段我们为了提升运行效率以及调试效率,一般会通过dev-server来打包,在开发阶段我们为了提升打包效率,不会对打包的内容进行压缩。在上线阶段我们需要拿到真实的打包文件,所以不会通过dev-server来打包,在上线阶段我们为了提升访问的效率,所以在打包时需要对打包的内容进行压缩。所以需要区分开发环境和线上环境。
将公共配置抽取到webpack.config.common.js
中,在dev环境配置
和prod环境配置
中导入common.js
,利用merge
合并即可。
// webpack.config.common.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/bundle.js',
path: path.resolve(__dirname, 'bundle')
},
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve(__dirname, "src"),
loader: 'eslint-loader',
options: {
fix: true
},
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env']],
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
[
'@babel/plugin-transform-runtime',
{
absoluteRuntime: false,
corejs: 2,
helpers: true,
regenerator: true,
useESModules: false
}
]
]
}
},
{
test: /\.(eot|json|svg|ttf|woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'font/'
}
}
]
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
name: '[name].[ext]',
outputPath: 'images/',
publicPath: 'http://127.0.0.1:9090/images'
}
}
]
},
{
test: /\.(htm|html)$/i,
loader: 'html-withimg-loader'
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: true
}
},
{
loader: 'css-loader',
options: {
modules: true // 开启CSS模块化
}
},
{
loader: 'postcss-loader'
}
]
},
{
test: /\.less$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}, {
loader: 'postcss-loader'
}]
},
{
test: /\.scss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'sass-loader'
}, {
loader: 'postcss-loader'
}]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin([{
from: './doc',
to: 'doc'
}]),
new MiniCssExtractPlugin({
filename: 'css/[name].css'
}),
]
};
// webpack.config.dev.js
const Webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Merge = require("webpack-merge");
const CommonConfig = require("./webpack.config.common.js");
const DevConfig = {
devServer: {
contentBase: './bundle',
open: true,
port: 9090,
proxy: [{
context: ['/user', '/login'],
target: 'http://127.0.0.1:3000',
changeOrigin: true, // 域名跨域
secure: false, // https跨域
pathRewrite: { '': '/api' }
}],
hot: true, // 开启热更新, 只要开启了热更新就不会自动刷新网页了
hotOnly: true // 哪怕不支持热更新也不要刷新网页
},
devtool: 'cheap-module-eval-source-map',
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
// 指定打包的模板, 如果不指定会自动生成一个空的
template: './src/index.html',
}),
new Webpack.HotModuleReplacementPlugin()
]
};
module.exports = Merge(CommonConfig, DevConfig);
// webpack.config.prod.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Merge = require("webpack-merge");
const CommonConfig = require("./webpack.config.common.js");
const ProdConfig = {
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
},
devtool: 'cheap-module-source-map',
mode: 'production', // "production" | "development"
plugins: [
new HtmlWebpackPlugin({
// 指定打包的模板, 如果不指定会自动生成一个空的
template: './src/index.html',
minify: {
// 告诉htmlplugin打包之后的html文件需要压缩
collapseWhitespace: true,
}
}),
]
};
module.exports = Merge(CommonConfig, ProdConfig);
Tree-Shaking
过滤掉无用的JS代码和CSS代码,我们称之为Tree-Shaking
。例如: 在a.js中引入了b模块,b模块中有2个方法,但是我只用到了1个方法,默认情况下会将b模块中所有代码都打包到a.js中,为了提升网页性能降低打包体积, 我们可以只将用到的方法打包到a.js中。
webpack中如何开启Tree-Shaking
:https://www.webpackjs.com/guides/tree-shaking/
JS模块Tree-Shaking
开发环境配置:
// webpack.config.dev.js 配置
optimization: {
usedExports: true
},
// package.json 配置
"sideEffects": ["*.css", "*.less", "*.scss"],
生产环境无需进行任何配置,webpack默认已经实现了Tree-Shaking
。
💡注意点:
- 只有ES Modle导入才支持Tree-Shaking
- 任何导入的文件都会受到 tree shaking 的影响。
- 如果在项目中使用类似 css-loader 并导入 CSS 文件,则需要将其添加到 sideEffects 列表中,以免在生产模式中webpack误以为是有副作用的代码,无意中将它删除。
CSS模块Tree-Shaking
安装:
npm i -D purifycss-webpack purify-css glob-all
https://github.com/webpack-contrib/purifycss-webpack
// webpack.config.prod.js
const glob = require('glob-all');
const PurifyCSSPlugin = require('purifycss-webpack');
// 插件新增
plugins: [
new PurifyCSSPlugin({
// 告诉PurifyCSSPlugin需要过滤哪些文件
paths: glob.sync([
path.join(__dirname, 'src/*.html'),
path.join(__dirname, 'src/js/*.js')
])
})
]
Code-Splitting 代码分割
默认情况下webpack会将所有引入的模块都打包到一个文件中,这样就导致了打包后的文件比较大, 以及修改文件后用户需要重新下载所有打包内容问题。
🦠 eg: 在a.js中引入了b.js,那么a.js和b.js都会被打包到bundle.js中,如果a.js有1MB,b.js也有1MB,那么打包之后的文件就有2MB,那么用户第一次打开网页的时候就需要下载2MB的文件。如果我们修改了a.js,但没有修改b.js,重新打包后用户需要重新下载新打包的文件(因为用户本地缓存的是a和b的合体) 这样就导致了每次修改了其中一个文件用户都要重新下载所有内容。
解决方案:将不经常修改的内容打包到另一个文件中,这样每次修改后用户就只用下载修改后的文件,没有被修改的文件由于用户上一次打开已经缓存在了本地就不用下载了,这样性能也提升了。
Code-Splitting
就是将不经常修改的模块打包到单独的文件中,避免每次修改用户都需要重新下载所有内容。
手动分割(了解)
// - 在单独文件中引入模块, 将模块中的内容添加到window上 window.xx = xx;
// - webpack.config.common.js 修改配置文件同时打包多个文件
entry: {
calculate: "./src/js/calculate.js", // 先打包会被先引入
main: "./src/js/index.js",
},
output: {
filename: "js/[name].js", // name 表示的是入口处的key值
path: path.resolve(__dirname, "bundle")
},
自动分割
// webpack.config.common.js
optimization: {
splitChunks: { // 告诉webpack需要对代码进行分割
chunks: "all" // async(只分割异步加载模块)、all(所有导入模块)
}
},
异步加载模块(懒加载模块)
同步加载
:import $ from 'jquery';
例如: 在index.js中导入了10个模块, 那么只要index.js被执行, 就会一次性将10个模块加载进来。
异步加载
: import('jquery').then(({default: $ }) => {使用模块代码});
$用于接收import导入的模块。例如: 在index.js中导入了10个模块, 那么哪怕index.js被执行,也要看是否满足加载条件才去加载。这个语法 ECMAScript 11语法。
// 异步加载 第二种方式
async function getComponment() {
const { default: $ } = await import('jquery');
const $div = $('<div>我是div</div>');
return $div;
}
对于异步加载的模块无需配置, webpack会自动分割。https://webpack.js.org/guides/lazy-loading/
Prefetching-Preloading
异步加载(懒加载)存在一定的弊端,弊端就是用到的时候再加载,那么用户需要等待加载完成后才能使用。例如: 弹出登录框的时候有一些业务逻辑,如果这些业务逻辑使用懒加载的话,那么只有加载完用户才能操作登录框。
💡解决方案:加载完当前需要使用的所有模块之后,在空闲的时间提前把异步加载的模块也加载进来,这样既不会影响到第一次的访问速度,还可以提升异步加载的速度。
Prefetching
:空闲的时候加载,等当前被使用的模块都加载完空闲下来的时候就去加载,不用等到用户用到时再加载。
Preloading
:和其它模块一起加载(了解)
import(/* webpackPrefetch: true */ 'jquery').then(({default: $ }) => {使用模块代码});
async function getComponment() {
const { default: $ } = await import(/* webpackPrefetch: true */ 'jquery');
const $div = $('<div>我是div</div>');
return $div;
}
可以利用魔法注释修改分割代码的名称:
/* webpackChunkName: "jquery" */
import(/* webpackPrefetch: true */ /* webpackChunkName: "jquery" */ 'jquery').then(({default: $ }) => {使用模块代码}); // 最终打包好的模块名为:vendors-jquery.js
长缓存优化
浏览器会自动缓存网页上的资源,以便于提升下次访问的速度,因为浏览器的缓存机制, 也导致文件内容被修改之后只要文件名称没有发生变化,就不会重新去加载修改之后的资源,所以刷新网页后显示的还是修改之前的内容。为了解决这个问题,就需要在打包文件的时候给"文件名称加上内容的hash值"。一旦内容发生了变化, 内容的hash值就会发生变化,文件的名称也会发生变化。一旦文件的名称发生了变化,浏览器就会自动去加载新打包的文件。
有三种加入hash值的方法:
hash
/chunkhash
/contenthash
hash
:根据每次编译打包的内容生成的哈希值,每次打包都不一样,不能很好利用缓存,不推荐。
chunkhash
:根据不同的入口文件(Entry
)进行依赖文件解析、构建对应的chunk
,生成对应的哈希值。在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash
的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。👉注意点:只支持css
和js
, 不支持img
等其它资源。
contenthash
(推荐):根据某个文件内容生成的哈希值,只要某个文件内容发生改变,该文件的contenthash
就会发生变化。👉注意点:在webpack4
中contenthash
和热更新
有冲突, 所以在开发模式想使用contenthash
需要关闭热更新。但是一般情况下我们需要通过hash
解决的是线上代码的内容更新问题,所以开发模式无关紧要。
如果开发模式下要使用contenthash
,需要关闭热更新:
// webpack.config.dev.js
// const Webpack = require('webpack');
devServer: {
...
// hot: true, // 开启热更新, 只要开启了热更新就不会自动刷新网页了
// hotOnly: true // 哪怕不支持热更新也不要刷新网页
}
plugins: [
// new Webpack.HotModuleReplacementPlugin()
]
开启contenthash
// webpack.config.common.js
output: {
...
filename: 'js/[name].[contenthash:8].js', // js输出名字
},
{// 打包字体图标规则
test: /\.(eot|json|svg|ttf|woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[contenthash:8].[ext]',
outputPath: 'font/'
}
}
]
},
{// 打包图片规则
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
name: '[name].[contenthash:8].[ext]', // 指定打包后文件名称
outputPath: 'images/',
publicPath: 'http://127.0.0.1:9090/images'
}
},
]
},
plugins: [
...
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
})
]
SplitChunksPlugin
webpack
在代码分割的时候底层使用的其实是Split-Chunks-Plugin
来实现代码分割的。
相关配置说明:https://www.webpackjs.com/plugins/split-chunks-plugin/
// 默认配置如下:
{
chunks: "async", // 对那些代码进行分割 async(只分割异步加载模块)、all(所有导入模块)、initial(初始块)
minSize: 30000, // 表示被分割的代码体积至少有多大才分割(单位是字节) 30kb
minChunks: 1, // 表示至少被引用多少次数才分割,默认为1
maxAsyncRequests: 5, // 异步加载并发最大请求数(保持默认即可)
maxInitialRequests: 3, // 最大的初始请求数(保持默认即可)
automaticNameDelimiter: '~', // 分割出来的模块命名连接符(xxx~xx.js)
name: true, // 拆分出来块的名字使用指定名称,还是0/1/2...
cacheGroups: { // ***缓存组, 将当前文件中导入的所有模块缓存起来统一处理
vendors: { // 分割从node_modules目录中导入的模块
test: /[\\/]node_modules[\\/]/,
priority: -10 // 优先级, 值越小越优先
},
default: { // 分割从其它地方导入的模块(自己写的模块)
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 如果当前代码块包含的模块已经有了,就不在产生一个新的代码块
}
}
}
// webpack.config.js 中可以根据需要添加
optimization: {
splitChunks: {
chunks: 'all', // 对那些代码进行分割 async(只分割异步加载模块)、all(所有导入模块)
...
}
},
// 第二种配置方式
optimization: {
runtimeChunk: {
name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。
},
splitChunks: {
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
},
}
},
Provide-Plugin
模块各种引入方式:
// 在HTML中全局引入
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
// 特点: 什么地方都可以使用,相当于全局变量
// 通过npm安装通过import局部引入
// 特点: 只能在 import 导入的模块中使用
为了能将npm
安装的组件变成全局的,不用每次在使用的时候import
,就需要使用插件Provide-Plugin
。https://www.webpackjs.com/plugins/provide-plugin/
Provide-Plugin
自动加载模块,而不必到处 import 或 require。默认情况下模块中的数据都是私有的,所以想要使用模块必须先导入模块,如果说在a.js中想要使用jQuery,那么就必须在a.js中导入jQuery模块,如果说在b.js中想要使用jQuery,那么就必须在b.js中导入jQuery模块。
// webpack.config.common.js
plugins: [
...
new Webpack.ProvidePlugin({
$: 'jquery'
})
]
imports-loader
imports-loader
和Provide-Plugin
功能一样可以实现全局导入,但是imports-loader
的功能比Provide-Plugin
更强大,imports-loader
除了可以实现全局导入以外,还可以修改全局this指向。默认情况下模块中的this指向一个空对象,我们可以通过imports-loader
实现让this指向window。
🦠 需要实现全局导入,更推荐使ProvidePlugin
来实现,因为ProvidePlugin
是webpack
内置的官方插件更靠谱。使用imports-loader
修改this
指向,系统会自动将我们的代码放到一个立即执行函数中,这就导致了在打包时候import
不在第一行, 会报错。解决办法就是:无需修改this
指向,直接在模块中使用window
。
// webpack.config.common.js, 打包js规则添加如下规则在下面
{
test: /\.js$/,
exclude: /node_modules/, // 告诉webpack不处理哪一个文件夹
// 以下代码的含义: 但凡在JS中用到了$就去自动加载jQuery
// loader: 'imports-loader?$=jquery'
// 以下代码的含义: 修改模块中this的指向
loader: 'imports-loader?this=>window'
},
resolve
resolve
用于配置导入模块的解析规则,https://www.webpackjs.com/configuration/resolve/ 。
resolve
常用配置:
// 🔑 映射导入路径, 简化导入代码
eg: import 'bootstrap/dist/css/bootstrap.css'; import $ from 'bootstrap';
// 通过修改 webpack.config.common.js ,简化
resolve: {
alias: { // 相当于别名
// 创建 import 或 require 的别名,来确保模块引入变得更简单
bootstrapcss: 'bootstrap/dist/css/bootstrap.css' // 这样,import 'bootstrapcss'; webpack知道去哪里找。
}
},
// 🔑 修改入口查找顺序, 简化导入代码
resolve: {
// 指定模块入口的查找顺序
mainFields: ["style", "main"], // import 导入的包,默认package.json文件中style 或者 main指定了我们需要导入包的路径
// eg: "style": 'dist/css/bootstrap.css''
}
// 🔑 修改查找顺序, 简化导入代码
resolve: {
// 指定导入模块查找顺序,eg: import './test'; 默认先去找对应的.js文件,然后.json, .node
extensions: [".css", ".js"] // 先找.css 然后.js
}
// 🔑 通过import导入模块的时候会先在node_modules中查找,找不到再逐级向上查找,这样在打包的时候非常消耗性能,能不能在打包的时候让webpack只去指定的目录查找。
resolve: {
modules: ["node_modules"], // 指定查找范围, 告诉webpack只在node_modules中查找
}
// webpack.config.common.js
...
module.exports = {
resolve: {
alias: {
bootstrapcss: 'bootstrap/dist/css/bootstrap.css'
}
mainFields: ['style', 'main']
extensions: ['.css', '.js', '.joson'],
modules: ["node_modules"],
...
},
noParse
默认情况下无论我们导入的模块(库)是否依赖于其它模块(库),都会去分析它的依赖关系,但是对于一些独立的模块(库)而言,其根本不存在依赖关系,但是webpack还是会去分析它的依赖关系,这样就大大降低了我们打包的速度。所以对于一些独立的模块(库),我们可以提前告诉webpack不要去分析它的依赖关系,这样就可以提升我们的打包速度。
// webpack.config.common.js
...
module: {
noParse: /jquery/, // webpack 不用解析jquery的依赖关系
rules: [...]
},
IgnorePlugin
IgnorePlugin是webpack的一个内置插件,IgnorePlugin用于忽略第三方包指定目录,让指定目录不被打包进去。
// webpack.config.common.js
plugins: [
new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) // 在打包moment这个库的时候, 将整个locale目录都忽略掉,这个目录存放的都是多语言配置
],
externals
externals
的作用就是将不会发生变化的第三方模块(库)设置为外部扩展,避免将这些内容打包到我们的项目中,从而提升打包速度。
externals
使用:https://www.webpackjs.com/configuration/externals/ ; 手动全局引入第三方模块;在配置文件中告诉webpack这是一个外部扩展库, 不需要打包。
使用externals
需要两步,1、全局导入需要使用的第三方库;2、配置webpack.config.js
;
// index.html
<script src="https://code.jquery.com/jquery-3.4.1.js"></script> // 相当于 window.jQuery;
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script> // window._;
// webpack.config.common.js
externals: {
// 告诉webpack我们在通过import导入jquery的时候,不是导入node_modules中的jquery,而是导入我们全局引入的jquery
jquery: 'jQuery',
lodash: '_'
},
dll动态链接库
dll
动态链接库和externals
功能其实是一样的,都是用于防止重复打包不会发生变化的第三方模块,都是用于提升webpack
打包效率的。只不过externals
不太符合前端的模块化思想,所以就有了dll
动态链接库。
第一步:单独配置一个webpack.config.dll.js
文件打包不会发生变化的第三方库
const path = require('path');
const Webpakc = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendors: ['jquery', 'lodash'] // 要打包的对象
},
output: {
filename: '[name].dll.js', // name就是入口指定的key
path: path.resolve(__dirname, 'dll'),
library: '[name]' // 表示打包的是一个库, 表示将打包的内容通过全局变量暴露出去
},
plugins:[
new Webpakc.DllPlugin({ // DllPlugin作用:在打包第三方库的时候生成一个清单文件
name: '[name]',
path: path.resolve(__dirname, 'dll/[name].manifest.json') // 生成动态库的映射关系
// 因为我们有可能将几个库打包到一个文件中, 所以需要生成一个映射文件方便webpack能够从中找到对应的库
})
]
};
package.json中添加命令:"dll": "npx webpack --config webpack.config.dll.js"
第二步:通过插件将打包好的库引入到界面上
// npm install --save-d add-asset-html-webpack-plugin
// webpack.config.common.js
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html' // 指定打包的模板, 如果不指定会自动生成一个空的
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dll/vendors.dll.js') // 将dll目录下的vendors.dll.js文件插入到上面生成的index.html中
}),
new Webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll/vendors.manifest.json') // 查找映射关系,过滤不需要打包的第三方库
}),
] // AddAssetHtmlPlugin 依赖于 HtmlWebpackPlugin
1 新建一个配置文件, 专门用于打包不会变化的第三方库
2 在打包项目的配置文件中, 通过add-asset-html-webpack-plugin将提前打包好的库插入到html中
3 在专门打包第三方的配置文件中添加生成清单配置
4 在打包项目的配置文件中, 告诉webpack打包第三方库的时候先从哪个清单文件中查询,如果清单包含当前用到的第三方库就不 打包了. 因为已经在html中手动引入了
动态链接库的优势:不用手动插入第三方库到html上;所有第三方库只会被打包一次。
当前动态链接库存在的问题:如果我们提前打包生成了多个文件和清单,那么需要手动增加插入的文件和查询的清单。可以通过NodeJS代码动态添加。
// webpack.config.common.js 中修改
const fs = require('fs');
const plugins = [
new HtmlWebpackPlugin({
template: './src/index.html'
})
];
const dllPath = path.resolve(__dirname, 'dll');
const files = fs.readdirSync(dllPath);
files.forEach(function (file) {
if(file.endsWith(".js")){
plugins.push(new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dll', file)
}));
}else if(file.endsWith(".json")){
plugins.push(new Webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', file)
}));
}
});
module.exports = {
...
plugins: plugins
}
HappyPack
默认情况下webpack打包项目是单线程的,为了提升打包速度,充分发挥多核 CPU 电脑的性能,我们可以通过HappyPack
让webpack实现多线程打包。
HappyPack使用:https://www.npmjs.com/package/happypack npm install --save-dev happypack
// webpack.config.common.js
const HappyPack = require('happypack');
const plugins = [ // 把配置文件中module.exports中的plugins单独提出来,module.exports中的plugins = plugins就行
...
new HappyPack({
id: 'js',
use: [{
test: /\.js$/,
exclude: /node_modules/, // 告诉webpack不处理哪一个文件夹
loader: 'babel-loader',
...
}]
})
];
// 原有打包js改为
{
test: /\.js$/,
exclude: /node_modules/, // 告诉webpack不处理哪一个文件夹
use: 'happypack/loader?id=js' // 告诉webpack只多线程打包js文件
},
打包多页应用
步骤:有多少个界面就指定多少个入口, 并给不同的入口指定不同的名称;有多少个界面就创建多少个HtmlWebpackPlugin,并给不同的界面配置不同的名称;在HtmlWebpackPlugin中通过chunks属性告知需要插入到当前界面的文件。
// webpack.config.common.js
const plugins = [
new HtmlWebpackPlugin({
template: './src/index.html', // 指定打包的模板, 如果不指定会自动生成一个空的
filename: 'index.html', // 生成的文件名字
chunks: ['index', 'vendors~index'] // 生成的html文件需要引入index、vendors~index开头的js文件
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'detail.html',
chunks: ['detail', 'vendors~detail']
}),
];
// webpack.config.common.js 优化上面方法
config.plugins = makePlugins(config);
function makePlugins(config){
const plugins = [
// ...
];
Object.keys(config.entry).forEach(function (key) {
plugins.push(new HtmlWebpackPlugin({
template: './src/index.html',
filename: key + '.html',
chunks: [key, 'vendors~'+key]
}));
});
const dllPath = path.resolve(__dirname, 'dll');
const files = fs.readdirSync(dllPath);
files.forEach(function(file) {
if (file.endsWith('.js')) {
plugins.push(new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dll', file)
}));
} else if (file.endsWith('.json')) {
plugins.push(new Webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', file)
}));
}
});
return plugins;
}
bundle-analyzer
webpack-bundle-analyzer
是一个可视化的打包优化插件,webpack-bundle-analyzer会将打包的结果以图形化界面的方式展示给我们,从webpack-bundle-analyzer生成的图形化界面中我们可以很清楚的知道模块之间的依赖关系、模块大小、模块有没有重复打包、重复引用等,从而针对性的对我们的代码进行优化。
// webpack.config.common.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin(),
...
]
![image-20210402170457102](/Users/meihao/Library/Application Support/typora-user-images/image-20210402170457102.png)