什么是webpack
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具 。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图 ,然后将你项目中所需的每一个模块组合成一个或多个 bundles ,它们均为静态资源,用于展示你的内容
- 打包: 将不同类型的资源按模块处理进行打包
- 静态:打包最终产出静态资源
- 模块:webpack 支持不同规范的模块化开发,如 ES Module、CommonJS 等可同时使用
安装 webpack
注意:全局安装可以直接使用 webpack 命令打包,但不推荐,避免将项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中, 可能会导致构建失败。
1、安装 webpack 和 webpack-cli, webpack-cli 是在 node 下运行所必须的
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
2、修改 package.json,增加"build": "webpack"
这里可以自行设置入口和出口,如 webpack --entry ./src/main.js --output-path ./build
{
"name": "learn-webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
}
}
3、运行 webpack 进行打包
npm run build
CSS loader
为什么需要 loader
默认情况下无法处理 css 、 less 文件,需要对文件进行转换,loader 就是起到一个转换的作用
css-loader 、 style-loader 、less-loader
css 并非 js 模块, 需要使用 css-loader 识别模块并解析出依赖关系, style-loader 才可以解析并生成样式
安装 css-loader 、 style-loader 、 less-loader
npm i css-loader -D
npm i style-loader -D
npm i less-loader -D
行内 css-loader
import "css-loader!../css/example.css";
配置文件中使用 css-loader 、 style-loader 、和 less-loader
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
},
],
},
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
};
browserslistrc
为什么需要 browserslistrc
目前前端开发均采用工程化方式,不仅需要考虑对各类工具包的兼容性,需要考虑浏览器对 CSS 语法、 JS 语法的兼容性,browserslistrc 可以帮助我们完成目标环境配置。 browserslist 单独是没用的,需要结合 bable 、 autoprefixer 等工具来确定需转译的 JS 特性和需要添加的 CSS 浏览器前缀。它的查询数据来源于 Can I Use
配置 browserslistrc
package.json 增加如下代码,表示兼容市场占有率大于 1% 或最新两个版本或最近24个月有更新维护的浏览器
{
"browserslist": [
">1%",
"last 2 versions",
"not dead",
"not ie <= 8"
]
}
或
添加 .browerslistrc 配置文件书写如下代码
> 1%
last 2 versions
not dead
not ie <= 8
postcss
一个通过 JS 来转换样式的工具,以达到兼容性
安装 postcss
要在命令行中使用 postcss 还需安装 postcss-cli ,并安装 autoprefixer 添加前缀,最后生成待前缀的 css
npm i postcss -D
npm i postcss-cli -D
npx i autoprefixer -D
npx postcss --use autoprefixer -o ret.css ./src/css/test.css
postcss-loader
应该放在 css-loader 之前加载,以加上前缀
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
}
}
importLoaders 属性
在一个 css 文件中导入另一个 css 文件来使用其中的样式时,由于 css 被匹配到后 postcss-loader 先进行工作添加前缀,然后再把代码传递给 css-loader , css-loader 可以处理 @import 导入的内容或者 @import meida 、 url 之类的内容,但并不会再回头交给 postcss-loader 处理,而是直接交给 style-loader 进行展示,这样我们就需要用到 importLoaders 属性指定配置的 css-loader 作用于 @import 的资源之前有多少个 loader
{
loader: "css-loader",
options: {
importLoaders: 1,
},
}
File loader
安装 file-load
npm i file-load -d
使用 file-load
使用 file-loader 会在 /dist 目录中生成导出的图片,他会把文件名称、路径返回并拷贝到打包目录,还要分开请求图片,请求次数变多
img 标签中的图片
function pckImg() {
const oEle = document.createElement("div");
const oImg = document.createElement("img");
oImg.src = require("../img/webpack.png").default;
oEle.appendChild(oImg);
return oEle;
}
document.body.appendChild(pckImg());
{
test: /\.(png|svg|gif|jpg|jpeg)$/,
use: ["file-loader"],
},
use: [
{
loader: "file-loader",
options: {
esModule: false,
},
},
],
import oImgSrc from "../img/webpack.png"
function pckImg() {
const oEle = document.createElement("div");
const oImg = document.createElement("img");
oImg.src = oImgSrc;
oEle.appendChild(oImg);
return oEle;
}
document.body.appendChild(pckImg());
背景图片
import "../css/img.css";
function pckImg() {
const oEle = document.createElement("div");
oEle.className = "bgBox";
return oEle;
}
document.body.appendChild(pckImg());
{
test: /\.(png|svg|gif|jpg|jpeg)$/,
use: [
{
loader: "file-loader",
options: {
esModule: false,
},
}
],
},
{
loader: "css-loader",
options: {
esModule: false,
},
},
设置图片名称于输出
文件名称占位符
{
loader: "file-loader",
options: {
esModule: false,
name: "[name].[hash:6].[ext]",
outputPath: 'img'
},
}
url-loader
安装 url-loader
npm i url-loader -D
使用 url-loader
使用 url-loader 不会在 /dist 目录中生成导出的图片,而是以 base64URL 的方式加载到我们的资源中,减少请求次数。 url-loader 内部也可以调用 file-loader ,通过 limit 控制使用哪种方法
{
test: /\.(png|svg|gif|jpg|jpeg)$/,
use: [
{
loader: "url-loader",
options: {
esModule: false,
name: "img/[name].[hash:6].[ext]",
limit: 25 * 1024,
},
},
],
}
base64URL:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cb2vy3Ri-1642001847935)(C:\Users\HeHao\AppData\Roaming\Typora\typora-user-images\image-20220104222133104.png)]
asset 资源模块(webpack5)
webpack5 后可以直接使用资源类型模块 asset module type 来简化 loader 的使用或替换,无需额外安装, webpack5 已内置
四个 asset 资源模块
-
asset/resource ===> file-loader -
asset/inline ===> url-loader -
asset/source ===> raw-loader -
asset
基本使用
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset/resource",
}
设置 asset 指定输出目录
方法一:设置 output 的 assetModuleFilename 属性
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
assetModuleFilename: "img/[name].[hash:6][ext]",
},
module: {
rules: [
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset/resource",
},
],
},
};
**缺点:**会把字体等资源也输出到 img 文件夹中
方法二:设置 rule 的 generator 属性
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset/resource",
generator: {
filename: "img/[name].[hash:6][ext]"
}
},
],
},
};
根据文件大小阈值选择
使用 asset , 添加 parser
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 20 *1024,
}
}
}
字体图标处理
{
test: /\.(ttf|eot|svg|woff2?)$/,
type: "asset/resource",
generator: {
filename: "font/[name].[hash:3][ext]",
},
},
webpack 插件
loader 和 plugin 的区别
loader:在读取特定内容时,对特定文件类型进行转换
plugin:使用灵活,贯穿 webpack 的整个生命周期,比 loader 可以做的事情更多
插件的本质上是一个类,有自己的构造函数和 apply 方法
第一个插件 —— clean-webpack-plugin
第三方插件,每次打包清空 /dist 目录
安装 clean-webpack-plugin
npm i clean-webpack-plugin -D
使用插件
webpack.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
...
},
plugins: [new CleanWebpackPlugin()],
};
html-webpack-plugin 和 DefinePlugin
自定义 index.html 配置到 ./public 目录
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="gb2312" />
<link rel="icon" href="<%= BASE_URL %>" />
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<body>
<script src="./dist/main.js"></script>
</body>
</html>
webpack.config.js
const {DefinePlugin} = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: "webpack 快速上手",
template: "./public/index.html",
}),
new DefinePlugin({
BASE_URL:
'"https://webpack.docschina.org/favicon.f326220248556af65f41.ico"',
}),
],
copy-webpack-plugin
可以使用它拷贝文件到 ./dist 目录
安装 copy-webpack-plugin
npm i copy-webpack-plugin -D
使用 copy-webpack-plugin
const CopyWebpackPlugin = require("copy-webpack-plugin");
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: "webpack 快速上手",
template: "./public/index.html",
}),
new DefinePlugin({
BASE_URL:
'"https://webpack.docschina.org/favicon.f326220248556af65f41.ico"',
}),
new CopyWebpackPlugin({
patterns: [
{
from: "public",
globOptions: {
ignore: ['**/index.html']
}
},
],
}),
],
babel-loader
babel
为什么需要babel
将 JSX TS ES6+ 转换成浏览器平台可以直接使用的代码
安装 babel core 和 babel cli
npm i @babel/core -D
npm i @babel/cli -D
npm i @babel/plugin-transform-arrow-functions -D
npm i @babel/plugin-transform-block-scoping -D
使用 babel
babel core 需要结合具体的转换插件才能转换为其他兼容性代码。将 ./src 中的所有 js 代码进行箭头函数和作用域转换,转换的文件放到 ./bulid 目录中
npx babel src --out-dir build --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-block-scoping
使用 babel 预设
安装预设,避免麻烦的单独安装和调用
npm i @babel/preset-env -D
使用预设,将 ./src 中的所有 js 代码使用预设进行转换,转换的文件放到 ./bulid 目录中
npx babel src --out-dir build --presets=@babel/preset-env
babel-loader
安装 babel-loader
npm i babel-loader -D
使用 babel-loader
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
],
}
给 babel-loader 指定兼容浏览器
1、根据 browserslistrc 的配置(推荐,但主要是用于 postcss)
2、配置targets,与 browserslistrc 同时存在优先使用该配置
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: "chrome 91",
},
],
],
},
},
],
}
babel-loader 相关配置文件(推荐方法)
简化 babel-loader 配置,避免深层嵌套
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: "chrome 91",
},
],
],
};
polyfill
polyfill 是什么
在遇到 Promise 、 generator 、 symbol 等一些更新语法时 @babel/preset-env 还是无法帮助我们进行转换,所有这个时候需要用到 polyfill 。 注意,在webpack4 默认加入 polyfill 导致产出文件特别大, webpack5 基于优化打包速度考虑已默认移除。
安装 polyfill
这里应该安装为生产依赖,因为生产环境下同样需要依赖它进行转化(babel 7.4.0 开始已被弃用)
npm i @babel/polyfill --save
但是整体来说 @babel/polyfill 包还是过大,如果只需要转换 已正式发布的 ECMAScript 标准进行转换,只需要安装 core-js/stable 和 regenerator-runtime/runtime (转换生成器函数) 两个包即可。
npm i core-js regenerator-runtime
配置 polyfill
在 js 入口文件引入两个包
import "core-js/stable";
import "regenerator-runtime/runtime";
作为 babel 的工具使用,配置babel.config.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3,
},
],
],
};
webpack.config.js 的 js 规则中加入 exclude: /node_modules/ ,因为部分使用的 node_modules 本身已经做了 polyfill ,应该排除
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
],
},
webpack-dev-server
我们目前要打开编写好的网页每次都需要运行 npm run build ,否则无法通过修改代码同步修改 ./dist 中打包的代码,这样流程太过繁琐冗余,需要一种合理的解决方案来实现自动重新编译。
解决方案1—— watch 开发模式 + live server 插件
- 使用 watch:配置 package.json 中 build 为
"build": "webpack --watch" - 使用 webpack 配置文件:修改 webpack.config.json 配置文件,添加
watch: true
缺点:
- 所有源代码都会重新编译
- 每次编译成功都需要进行文件重新读写到 ./dist
- live server 只是 vscode 生态下的
- 不能实现网页的局部刷新
解决方案2:webpack-dev-server
安装 webpack-dev-server
npm i webpack-dev-server -D
基本使用
修改 package.json 增加短命令 "serve": "webpack serve"
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"serve": "webpack serve --config webpack.config.js"
}
开启 serve 静态服务,默认端口为 8080
npm run serve
优点:
- 脱离 vscode live server 插件,生态适用
- 可以实现局部刷新
- 打包全部在内存中进行,不需要进行文件读写
webpack-middleware
webpack-dev-middleware 是一个封装器( wrapper ),它可以把 webpack 处理过的文件发送到一个 server。webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。(webpack-dev-server 使用较多)
安装 core-js、regenerator-runtime 、 express 、 webpack-dev-middleware
npm i core-js regenerator-runtime express webpack-dev-middleware
Server.js 模拟服务端,使用 webpack-middleware
const express = require("express");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpack = require("webpack");
const app = express();
const config = require("./webpack.config.js");
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler));
app.listen(3000, () => {
console.log("服务运行在端口 3000 上");
});
// 开启服务
node ./Server.js
模块热替换 HMR
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。
开启HMR
配置:webpack.config.js
devServer: {
hot: true,
},
为模块开启热更新, index.js ,整个方法也提供了回调函数,在发生更新是运行
import "./title.js";
if (module.hot) {
module.hot.accept(["./title.js"], () => {
console.log("title.js 模块更新了");
});
}
React 组件支持 HMR
React 配置HMR 还需要额外的配置
配置 webpack 使支持 React
安装 @babel/core 、 @babel/preset-react
npm i @babel/preset-react -D
npm i @babel/core -D
在 webpack.config.json 添加 jsx 解析规则
{
test: /\.jsx?$/,
use:['babel-loader']
}
在 babel.config.js 配置 babel
module.exports = {
presets: [
["@babel/preset-env"],
["@babel/preset-react"],
],
};
配置 React HMR
安装 react-refresh-webpack-plugin 和 react-refresh (前者依赖它,配合使用)
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
配置 package.json
plugins: [
new reactRefreshWebpackPlugin()
]
配置 babel.config
module.exports = {
presets: [
["@babel/preset-env"],
["@babel/preset-react"],
],
plugins: [["react-refresh/babel"]],
};
webpack 的几个 path
output 中的 path
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
publicPath: '/',
}
};
devServer 中的 path
webpack.config.js 注意,webpack5 已移除以下属性,可参考 static
const path = require("path");
module.exports = {
devServer: {
publicPath: '/',
contentBase: path.resolve(__dirname, 'public')
watchContentBase: true
},
}
devServer 常用配置
几个常用配置
const path = require("path");
module.exports = {
devServer: {
hotOnly: true,
port: 4000,
open: false,
compress: true,
historyApiFallback
},
}
proxy 设置
进行开发时,前后端可能不在一个端口上,或者需要请求其他地方的数据,那么就会存在浏览器跨域问题,就需要使用 proxy 进行代理
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://https://api.github.com',
pathRewrite: {"^/api" : ""},
changeOrigin: true,
},
},
},
};
resolve 模块解析
配置模块如何被解析
resolve: {
modules: ['node_modules', "lg"],
mainFiles: ['index'],
extensions: [".js", ".json", ".ts", ".jsx", ".vue"]
alias: {
"@": path.resolve(__dirname, "src"),
},
},
mode
提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。
module.exports = {
mode: 'development',
};
默认值为 production
选项 | 描述 |
---|
development | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development . 为模块和 chunk 启用有效的名。(源码阅读友好) | production | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production 。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin ,FlagIncludedChunksPlugin ,ModuleConcatenationPlugin ,NoEmitOnErrorsPlugin 和 TerserPlugin 。(名称替换、删除注释、换行) | none | 不使用任何默认优化选项 |
devtool
source-map
source-map 是一种映射技术,可以根据转换的代码返还成为转化的源代码,这样调试的时候定位到源码中的信息
几种常见的 devtool 配置
适用开发环境:
-
eval :该模式会把每个 module 封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL 。 此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。 -
eval-source-map :每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。 -
cheap-eval-source-map :eval-cheap-source-map - 类似 eval-source-map ,每个模块使用 eval() 执行。这是 " cheap (低开销)" 的 source map ,因为它没有生成列映射 ( column mapping ) ,只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。 -
cheap-module-eval-source-map :类似 eval-cheap-source-map ,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而**,loader source map 会被简化为每行一个映射** ( mapping )
适用生产环境:
- none : (省略 devtool 选项)-不触发 source map , 非常推荐
source-map :生成 index.js.map 文件,,此文件记录了 source map 行列信息如何映射源代码的信息。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它,但是应该将服务器配置为不允许普通用户访问 source map 文件hidden-source-map :source-map 相同,但不会为 bundle 添加引用注释。如果你只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露你的 source map,这个选项会很有用。nosources-source-map : 创建的 source map 不包含 sourcesContent(源代码内容) 。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码。你可以将 source map 文件部署到 web 服务器
tips
验证 devtool 名称时,我们期望使用某种模式, 但注意不要混淆 devtool 字符串的顺序, 模式是: [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map .
eval- : 为每个模块生成 source map 并通过 eval 附加它。建议用于开发,因为改进了重新生成性能。inline- : 将 source map 内联到原始文件中,而不是创建单独的文件。hidden- : 没有添加 source map 的引用。source map 没有部署,但仍然应该生成,例如用于错误报告的目的。nosources- : source map 中不包含源代码。当需要引用原始文件时(需要进一步的配置选项),这可能很有用。
打包环境拆分与合并
修改 package.json 达到传递不同参数调用不同配置的目的
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"serve": "webpack serve",
"build2": "webpack --config ./config/webpack.common.js --env production",
"serve2": "webpack serve --config ./config/webpack.common.js --env development",
}
}
建立三个分离的配置文件和一个 path 路径文件:
path.js 需匹配到的绝对路径都放在这个文件导出,使用时如 resolveApp('./src')
const path = require("path");
const appDir = process.cwd();
const resolveApp = (relativePath) => {
return path.resolve(appDir, relativePath);
};
module.exports = resolveApp;
./config/webpack.common.js
const {merge} = require("webpack-merge");
const prodConfig = require("webpack.prod");
const devConfig = require("webpack.dev");
module.exports = (env) => {
const commonConfig = {
};
const isProduction = env.production;
const config = isPtoduction ? prodConfig : devConfig;
return merge(commonConfig, config)
};
./config/webpack.prod.js / ./config/webpack.dev.js 略
module.exports = (env) => {
return {
};
};
建立三个分离的配置文件和一个 path 路径文件:
path.js 需匹配到的绝对路径都放在这个文件导出,使用时如 resolveApp('./src')
const path = require("path");
const appDir = process.cwd();
const resolveApp = (relativePath) => {
return path.resolve(appDir, relativePath);
};
module.exports = resolveApp;
./config/webpack.common.js
const {merge} = require("webpack-merge");
const prodConfig = require("webpack.prod");
const devConfig = require("webpack.dev");
module.exports = (env) => {
const commonConfig = {
};
const isProduction = env.production;
const config = isPtoduction ? prodConfig : devConfig;
return merge(commonConfig, config)
};
./config/webpack.prod.js / ./config/webpack.dev.js 略
module.exports = (env) => {
return {
};
};
|