# 一些常见的webpack插件
# loaders
webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。
# file-loader
- file-loader 可以解析项目中的 url 引入(不仅限于 css),根据我们的配置,将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。
- 默认情况下,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名。
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
loader: "file-loader",
options: {
name: "[name]_[hash].[ext]",
outputPath: "images/"
}
}
}
];
# url-loader
- url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
- url-loader 把资源文件转换为 URL,file-loader 也是一样的功能。不同之处在于 url-loader 更加灵活,它可以把小文件转换为 base64 格式的 URL,从而减少网络请求次数。url-loader 依赖 file-loader。
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
loader: "url-loader",
options: {
name: "[name]_[hash].[ext]",
outputPath: "images/",
limit: 204800
}
}
}
];
# css-loader
- 只负责加载 css 模块,不会将加载的 css 样式应用到 html
- importLoaders 用于指定在 css-loader 前应用的 loader 的数量
- 查询参数 modules 会启用 CSS 模块规范
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
];
}
# style-loader
- 负责将 css-loader 加载到的 css 样式动态的添加到 html-head-style 标签中
- 一般建议将 style-loader 与 css-loader 结合使用
# sass-loader
# 安装
yarn add sass-loader node-sass webpack --dev
- node-sass 和 webpack 是 sass-loader 的 peerDependency,因此能够精确控制它们的版本。
- loader 执行顺序:从下至上,从右至左
- 通过将 style-loader 和 css-loader 与 sass-loader 链式调用,可以立刻将样式作用在 DOM 元素。
// webpack.config.js
module.exports = {
...
module: {
rules: [{
test: /\.scss$/,
use: [{
loader: "style-loader" // 将 JS 字符串生成为 style 节点
}, {
loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
}, {
loader: "sass-loader" // 将 Sass 编译成 CSS
}]
}]
}
};
# postcss-loader
- webpack4 中使用 postcss-loader 代替 autoprefixer,给 css3 样式加浏览器前缀。具体可参考
https://blog.csdn.net/u014628388/article/details/82593185
// webpack.config.js
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
'postcss-loader'
],
}
//postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')({ browsers: ['last 2 versions'] }),
],
};
# plugins
plugin 可以在 webpack 运行到某个时刻的时候,帮你做一些事情
# HtmlWebpackPlugin
- HtmlWebpackPlugin 会在打包结束后,自动生成一个 html 文件,并把打包生成的 js 自动引入到这个 html 文件中
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
],
};
# clean-webpack-plugin
- clean-webpack-plugin 插件用来清除残留打包文件,特别是文件末尾添加了 hash 之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多。
- 新版本中的 clean-webpack-plugin 仅接受一个对象,默认不需要传任何参数。具体可参考
https://blog.csdn.net/qq_23521659/article/details/88353708
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
...
plugins: [
new CleanWebpackPlugin()
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
# SplitChunksPlugin
- 具体概念可参考
https://juejin.im/post/5af15e895188256715479a9a
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
# MiniCssExtractPlugin
将 CSS 提取为独立的文件的插件,对每个包含 css 的 js 文件都会创建一个 CSS 文件,支持按需加载 css 和 sourceMap
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: "[name].css",
chunkFilename: "[id].css"
})
],
module: {
rules: [
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: "css-loader",
options: {
importLoaders: 2 // 用于指定在 css-loader 前应用的 loader 的数量
// modules: true // 查询参数 modules 会启用 CSS 模块规范
}
},
"sass-loader",
"postcss-loader"
]
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
"css-loader",
"postcss-loader"
]
}
]
}
};
# CssMinimizerWebpackPlugin
主要用来移除无用的css代码及css代码压缩
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
module: {
loaders: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
optimization: {
minimize: true,
minimizer: [
// For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
// `...`,
new CssMinimizerPlugin(),
],
},
};
# devtool
# source map
source map 就是对打包生成的代码与源代码的一种映射,主要是为了方便定位问题和排查问题。devtool 关键有 eval、cheap、module、inline 和 source-map 这几块,具体可参考文档:
https://www.webpackjs.com/configuration/devtool/
- development 环境参考配置:
'cheap-module-eval-source-map'
- production 环境参考配置:
'cheap-module-source-map'
# webpack-dev-server
webpack-dev-server 提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。具体可参考
https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-server
# 接口代理(请求转发)
(个人还是比较喜欢使用whistle进行代理)如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。dev-server 使用了非常强大的
http-proxy-middleware
包。常用于接口请求转发。具体参考https://www.webpackjs.com/configuration/dev-server/#devserver-proxy
devServer: {
contentBase: "./dist",
open: true,
hot: true,
hotOnly: true,
proxy: {
"/api": {
target: "https://other-server.example.com",
pathRewrite: {"^/api" : ""},
secure: false,
bypass: function(req, res, proxyOptions) {
if (req.headers.accept.indexOf("html") !== -1) {
console.log("Skipping proxy for browser request.");
return "/index.html";
}
}
}
}
},
# 解决单页面路由问题
当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html 通过传入以下启用:
historyApiFallback: true;
通过传入一个对象,比如使用 rewrites 这个选项,此行为可进一步地控制:
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: "/views/landing.html" },
{ from: /^\/subpage/, to: "/views/subpage.html" },
{ from: /./, to: "/views/404.html" }
];
}
# webpack-dev-middleware
webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求
// server.js
// 使用webpack-dev-middleware
// https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-middleware
const express = require("express");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const config = require("./webpack.config.js");
const complier = webpack(config);
const app = express();
app.use(
webpackDevMiddleware(complier, {
publicPath: config.output.publicPath
})
);
app.listen(3000, () => {
console.log("server is running");
});
# Hot Module Replacement
模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。
// webpack.config.js
...
const webpack = require('webpack');
...
devServer: {
contentBase: './dist',
open: true,
hot: true,
hotOnly: true
},
plugins: [
...
new webpack.HotModuleReplacementPlugin()
],
如果已经通过 HotModuleReplacementPlugin 启用了模块热替换(Hot Module Replacement),则它的接口将被暴露在 module.hot 属性下面。通常,用户先要检查这个接口是否可访问,然后再开始使用它。
// index.js
if (module.hot) {
module.hot.accept("./library.js", function() {
// 使用更新过的 library 模块执行某些操作...
});
}
# bundle 分析
借助一些官方推荐的可视化分析工具,可对打包后的模块进行分析以及优化
webpack-chart
: webpack 数据交互饼图webpack-visualizer
: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的webpack-bundle-analyzer
: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户
# Preloading、Prefetching
prefetch:会等待核心代码加载完成后,页面带宽空闲后再去加载 prefectch 对应的文件;preload:和主文件一起去加载
- 可以使用谷歌浏览器 Coverage 工具查看代码覆盖率(ctrl+shift+p > show coverage)
- 使用异步引入 js 的方式可以提高 js 的使用率,所以 webpack 建议我们多使用异步引入的方式,这也是 splitChunks.chunks 的默认值是"async"的原因
- 使用魔法注释 /_ webpackPrefetch: true _/ ,这样在主要 js 加载完,带宽有空闲时,会自动下载需要引入的 js
- 使用魔法注释 /_ webpackPreload: true _/,区别是 webpackPrefetch 会等到主业务文件加载完,带宽有空闲时再去下载 js,而 preload 是和主业务文件一起加载的
# babel
# babel 编译 es6、jsx 等
- @babel/core babel 核心模块
- @babel/preset-env 编译 es6 等; 它可以根据开发者的配置,按需加载插件
- @babel/preset-react 转换 jsx
- @babel/plugin-transform-runtime 避免 polyfill 污染全局变量,减少打包体积
- @babel/polyfill es6 内置方法和函数转化垫片
- @babel/runtime
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
];
}
新建.babelrc 文件
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-transform-runtime"]
}
# 按需引入 polyfill
在 src 下的 index.js 中全局引入@babel/polyfill 并写入 es6 语法,但是这样有一个缺点: 全局引入@babel/polyfill 的这种方式可能会导入代码中不需要的 polyfill,从而使打包体积更大,修改.babelrc 配置
`yarn add core-js@2 @babel/runtime-corejs2 --dev`
{
"presets": [
[
"@babel/preset-env", {
"useBuiltIns": "usage"
}
],
"@babel/preset-react"
],
"plugins": ["@babel/plugin-transform-runtime"]
}
这就配置好了按需引入。配置了按需引入 polyfill 后,用到 es6 以上的函数,babel 会自动导入相关的 polyfill,这样能大大减少打包编译后的体积。
# babel-runtime 和 babel-polyfill 的区别
参考
https://www.jianshu.com/p/73ba084795ce
- babel-polyfill 会”加载整个 polyfill 库”,针对编译的代码中新的 API 进行处理,并且在代码中插入一些帮助函数
- babel-polyfill 解决了 Babel 不转换新 API 的问题,但是直接在代码中插入帮助函数,会导致污染了全局环境,并且不同的代码文件中包含重复的代码,导致编译后的代码体积变大。 Babel 为了解决这个问题,提供了单独的包 babel-runtime 用以提供编译模块的工具函数, 启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数
- babel-runtime 适合在组件,类库项目中使用,而 babel-polyfill 适合在业务项目中使用。
# 高级概念
# tree shaking(js)
tree shaking 可清除代码中无用的 js 代码,只支持 import 方式引入,不支持 commonjs 的方式引入 mode 是 production 的无需配置,下面的配置是针对 development 的
// webpack.config.js
optimization: {
usedExports: true
}
// package.json
"sideEffects": false,
# Code Spliting
代码分割,和 webpack 无关
- 同步代码(需在 webpack.config.js 中配置 optimization)
// index.js
import _ from 'lodash';
console.log(_.join(['a','b','c'], '****'))
// 在webpack.base.js里做相关配置
optimization: {
splitChunks: {
chunks: 'all'
}
},
- 异步代码(无需任何配置,但需安装
@babel/plugin-syntax-dynamic-import
包)
// index.js
function getComponent() {
return import("lodash").then(({ default: _ }) => {
const element = document.createElement("div");
element.innerHTML = _.join(["Jack", "Cool"], "-");
return element;
});
}
getComponent().then(el => {
document.body.appendChild(el);
});
# Caching(缓存)
通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash] 替换可以用于在文件名中包含一个构建相关(build-specific)的 hash,但是更好的方式是使用 [contenthash] 替换,当文件内容发生变化时,[contenthash]也会发生变化
output: {
filename: "[name].[contenthash].js",
chunkFilename: '[name].[contenthash].chunk.js'
}
# Shimming
webpack 编译器(compiler)能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery 中的 $)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方
- shimming 全局变量(第三方库)(ProvidePlugin 相当于一个垫片)
const path = require('path');
+ const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
- }
+ },
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash'
+ })
+ ]
};
- 细粒度 shimming(this 指向 window)(需要安装 imports-loader 依赖)
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: require.resolve('index.js'),
+ use: 'imports-loader?this=>window'
+ }
+ ]
+ },
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join']
})
]
};
# 环境变量
webpack 命令行环境选项 --env 允许您传入任意数量的环境变量。您的环境变量将可访问 webpack.config.js。例如,--env.production 或--env.NODE_ENV=local
webpack --env.NODE_ENV=local --env.production --progress
使用环境变量必须对 webpack 配置进行一项更改。通常,module.exports 指向配置对象。要使用该 env 变量,必须转换 module.exports 为函数:
// webpack.config.js
const path = require("path");
module.exports = env => {
// Use env.<YOUR VARIABLE> here:
console.log("NODE_ENV: ", env.NODE_ENV); // 'local'
console.log("Production: ", env.production); // true
return {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
}
};
};
# library 打包配置
除了打包应用程序代码,webpack 还可以用于打包 JavaScript library 用户应该能够通过以下方式访问 library:
- ES2015 模块。例如 import library from 'library'
- CommonJS 模块。例如 require('library')
- 全局变量,当通过 script 脚本引入时
我们打包的 library 中可能会用到一些第三方库,诸如 lodash。现在,如果执行 webpack,你会发现创建了一个非常巨大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash 当作 peerDependency。也就是说,用户应该已经将 lodash 安装好。因此,你可以放弃对外部 library 的控制,而是将控制权让给使用 library 的用户。这可以使用 externals 配置来完成:
// webpack.config.js
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js'
- }
+ },
+ externals: {
+ lodash: {
+ commonjs: 'lodash',
+ commonjs2: 'lodash',
+ amd: 'lodash',
+ root: '_'
+ }
+ }
};
对于用途广泛的 library,我们希望它能够兼容不同的环境,例如 CommonJS,AMD,Node.js 或者作为一个全局变量。为了让你的 library 能够在各种用户环境(consumption)中可用,需要在 output 中添加 library 属性:
// webpack.config.js
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
- filename: 'library.js'
+ filename: 'library.js',
+ library: 'library'
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
当你在 import 引入模块时,这可以将你的 library bundle 暴露为名为 webpackNumbers 的全局变量。为了让 library 和其他环境兼容,还需要在配置文件中添加 libraryTarget 属性。这是可以控制 library 如何以不同方式暴露的选项。
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'library.js',
+ library: 'library',
+ libraryTarget: 'umd'
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
我们还需要通过设置 package.json 中的 main 字段,添加生成 bundle 的文件路径。
// package.json
{
...
"main": "dist/library.js",
...
}
# PWA 打包配置
渐进式网络应用程序(Progressive Web Application - PWA),是一种可以提供类似于原生应用程序(native app)体验的网络应用程序(web app)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的网络技术来实现的 添加 workbox-webpack-plugin 插件,并调整 webpack.config.js 文件:
npm install workbox-webpack-plugin --save-dev
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
- title: 'Output Management'
+ title: 'Progressive Web Application'
- })
+ }),
+ new WorkboxPlugin.GenerateSW({
+ // 这些选项帮助 ServiceWorkers 快速启用
+ // 不允许遗留任何“旧的” ServiceWorkers
+ clientsClaim: true,
+ skipWaiting: true
+ })
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
注册 Service Worker
import _ from 'lodash';
import printMe from './print.js';
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/sw.js').then(registration => {
+ console.log('SW registered: ', registration);
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError);
+ });
+ });
+ }
现在来进行测试。停止服务器并刷新页面。如果浏览器能够支持 Service Worker,你应该可以看到你的应用程序还在正常运行。然而,服务器已经停止了服务,此刻是 Service Worker 在提供服务。
# TypeScript 打包配置
可参考
https://www.webpackjs.com/guides/typescript/
或https://webpack.js.org/guides/typescript/
- 安装 ts 依赖
npm install --save-dev typescript ts-loader
- 增加 tsconfig.json 配置文件
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}
- webpack.config.js 添加对 ts/tsx 语法支持(ts-loader)
const path = require("path");
module.exports = {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/
}
]
},
resolve: {
extensions: [".tsx", ".ts", ".js"]
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
}
};
- 当从 npm 安装第三方库时,一定要牢记同时安装这个库的类型声明文件。可以从 TypeSearch 中找到并安装这些第三方库的类型声明文件。如
npm install --save-dev @types/lodash
- 本文链接: https://mrgaogang.github.io/javascript/performance/%E4%B8%80%E4%BA%9B%E5%B8%B8%E8%A7%81%E7%9A%84webpack%E6%8F%92%E4%BB%B6.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!