「Webpack5 专题(二)」静态资源的处理
一、前言
上一篇讲解了 webpack5 的基础配置。这一篇文章将会介绍如何利用 webpack 中的 Loaders 对静态资源做处理,Loaders 是 webpack 的核心概念之一。
静态资源主要包括以下几方面的内容:
样式文件(CSS) 图片(Images) 字体(Fonts) 数据文件(Data) ...
文件结构:
webpack
|- /dist
|- index.html
|- /src
|- /assets
|- /fonts
|- font-a.otf
|- font-b.ttf
|- font-c.woff
|- font-d.woff2
|- font-e.svg
|- /icons
|- icon-a.png
|- incon-b.svg
|- /images
|- img-a.png
|- img-b.jpg
|- img-gif.jpg
|- /js
|- js-a.js
|- /others
|- o-a.txt
|- o-b.xml
|- /styles
|- global.scss
|- reset.css
|- /components
|- content.js
|- header.js
|- sidebar.js
|- index.js
|- package.json
|- webpack.config.js
复制代码
assets 文件夹中存放了若干静态文件(例如:字体、图标、图片、文本、样式以及 JS 文件),中间根据文件的不同,又分了几个文件夹。这里为了方便起见,文件名均已作简化处理。
index.js
// js 模块
const Header = require("./components/header");
const Sidebar = require("./components/sidebar");
const Content = require("./components/content");
// 图片模块 (这 5 个都是小图)
const icon1 = require("./assets/icons/mac-os.png");
const icon2 = require("./assets/icons/apple-tv.png");
const icon3 = require("./assets/icons/apple-contacts.png");
const iconSvg1 = require("./assets/icons/arrow-up.svg");
const iconSvg2 = require("./assets/icons/arrow-down.svg");
// 图片模块 (这 3 个都是大图)
const dog1 = require("./assets/images/animal-dog-1.png");
const avatar1 = require("./assets/images/avatar-1.jpg");
const cat1 = require("./assets/images/express-cat-1.gif");
// 数据模块
const fileTxt = require("./assets/data/notes.txt");
const fileXml = require("./assets/data/hello.xml");
// 字体模块
const font = require("./assets/fonts/Calibre-Regular.otf");
const iconFont1 = require("./assets/fonts/iconfont.ttf");
const iconFont2 = require("./assets/fonts/iconfont.woff");
const iconFont3 = require("./assets/fonts/iconfont.woff2");
// 样式模块
// const reset = require('./assets/styles/reset.css');
// const global = require('./assets/styles/global.scss');
const dom = document.getElementById("root");
// header
new Header(dom);
// side-bar
new Sidebar(dom);
// content
new Content(dom);
复制代码
引入了各种文件模块并放入变量中,每一个模块在打包以后都有值,可以正常使用。
二、Loaders — 加载器
webpack enables use of loaders[2] to preprocess files. This allows you to bundle any static resource way beyond JavaScript. You can easily write your own loaders using Node.js.
简单来说,loaders 可以帮我们预处理任何静态文件并打包。
例如:图片文件,样式文件、html 文件,甚至是一些数据文件:txt 文件、xml 文件……
那么,如何配置呢?
三、Asset Modules — 静态资源模块
根据 Webpack5 的文档,它简化了之前版本对于文件方面的配置,提供了 Asset Modules[3] (静态资源模块),替代了之前常用的 raw-loader、url-loader、file-loader。
也就是说不用再安装这几个 loader 了,直接通过一行type: 'asset'
搞定,书写更加方便!(注意 module 中 type 的设置!)
// webpack 配置文件
const path = require("path");
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
// *** 模块选项中匹配的文件会通过 loaders 来转换!
module: {
rules: [
// 图片文件
{
test: /\.(jpe?g|png|gif|svg)$/i,
type: "asset", // 一般会转换为 "asset/resource"
},
// 字体文件
{
test: /\.(otf|eot|woff2?|ttf|svg)$/i,
type: "asset", // 一般会转换为 "asset/inline"
},
// 数据文件
{
test: /\.(txt|xml)$/i,
type: "asset", // 一般会转换成 "asset/source"
},
],
},
};
复制代码
每次打包后,文件都会根据 output 的配置放进输出文件夹,但是对于压缩处理的文件则不会被打包到文件夹,只会以base64 编码格式或者 URI 编码等格式存在于 bundle.js 的源码中。
源码中的 base64
源码中的 URI
默认情况下,当文件小于 8 kb 则会采用 base64 编码,那么上面出现的 URI 编码怎么设置的呢?
1. 特殊处理
某些文件需要特殊处理。例如,SVG 文件转换为 URI 编码后,与 base64 相比,体积会更小;
处理 txt、xml 文件 type: 'asset/source'
更好,它会导出资源的源码,这样就能看到全貌,而非 base64 格式的编码字符串。
最小化 SVG 文件需要安装一个第三方库:mini-svg-data-uri[4].
npm i -D mini-svg-data-uri
复制代码
安装之后,重新配置如下:
// webpack 配置文件
const path = require("path");
const miniSVGDataURI = require("mini-svg-data-uri");
module.exports = {
// ...
module: {
rules: [
// 图片文件
{
test: /\.(jpe?g|png|gif)$/i,
type: "asset",
},
// svg文件
{
test: /\.svg$/i,
type: "asset/inline",
generator: {
dataUrl(content) {
content = content.toString();
return miniSVGDataURI(content);
},
},
},
// 字体文件
{
test: /\.(otf|eot|woff2?|ttf|svg)$/i,
type: "asset",
},
// 数据文件
{
test: /\.(txt|xml)$/i,
type: "asset/source",
},
],
},
};
复制代码
至此,打包完成。现在,看看这些文件模块究竟被打包成什么数据了,在 index.js 文件中添加如下代码:
// js 模块
const Header = require("./components/header");
const Sidebar = require("./components/sidebar");
const Content = require("./components/content");
// 图片模块 (这 5 个都是小图)
const icon1 = require("./assets/icons/mac-os.png");
const icon2 = require("./assets/icons/apple-tv.png");
const icon3 = require("./assets/icons/apple-contacts.png");
const iconSvg1 = require("./assets/icons/arrow-up.svg");
const iconSvg2 = require("./assets/icons/arrow-down.svg");
// 图片模块 (这 3 个都是大图)
const dog1 = require("./assets/images/animal-dog-1.png");
const avatar1 = require("./assets/images/avatar-1.jpg");
const cat1 = require("./assets/images/express-cat-1.gif");
// 其他文件模块
const fileTxt = require("./assets/data/notes.txt");
const fileXml = require("./assets/data/hello.xml");
// 字体模块
const font = require("./assets/fonts/Calibre-Regular.otf");
const iconFont1 = require("./assets/fonts/iconfont.ttf");
const iconFont2 = require("./assets/fonts/iconfont.woff");
const iconFont3 = require("./assets/fonts/iconfont.woff2");
// 样式模块
// const reset = require('./assets/styles/reset.css');
// const global = require('./assets/styles/global.scss');
// 在控制台中打印这些模块:
console.log("icon-png: ", icon1);
console.log("icon-svg: ", iconSvg1);
console.log("png: ", dog1);
console.log("jpg: ", avatar1);
console.log("gif: ", cat1);
console.log("txt: ", fileTxt);
console.log("xml: ", fileXml);
console.log("font: ", font);
console.log("iconFont: ", iconFont1);
const dom = document.getElementById("root");
// 插入一张图片
const img = new Image();
img.src = dog1; // 图片模块 dog1 作为 img 变量的 src 属性值
img.width = 200;
root.append(img);
// header
new Header(dom);
// side-bar
new Sidebar(dom);
// content
new Content(dom);
复制代码
浏览器页面展示如下:
可以看到 SVG 文件 (icon-svg) 是被处理成 URI 编码的!
2. 配置静态文件名
默认情况下,打包以后的文件是散在 dist 文件夹中的,难以区分和维护。
现在,需要将他们分门别类地放进对应的文件夹中,就需要对文件名做统一的管理。
webpack.config.js
const path = require("path");
const miniSVGDataURI = require("mini-svg-data-uri");
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
// 静态文件打包后的路径及文件名(默认是走全局的,如果有独立的设置就按照自己独立的设置来。)
assetModuleFilename: "assets/[name]_[hash][ext]",
},
module: {
rules: [
// 图片文件
{
test: /\.(jpe?g|png|gif)$/i,
type: "asset",
generator: {
filename: "images/[name]_[hash][ext]", // 独立的配置
},
},
// svg 文件
{
test: /\.svg$/i,
type: "asset",
generator: {
dataUrl(content) {
content = content.toString();
return miniSVGDataURI(content);
},
},
},
// 字体文件
{
test: /\.(otf|eot|woff2?|ttf|svg)$/i,
type: "asset",
generator: {
filename: "fonts/[name]_[hash][ext]",
},
},
// 数据文件
{
test: /\.(txt|xml)$/i,
type: "asset/source", // exports the source code of the asset
},
],
},
};
复制代码
第 23 - 25 行,用于单独配置静态文件名;
generator: {
filename: 'images/[name]_[hash][ext]' // 单独配置打包的路径及文件名
},
复制代码
第 12 行,assetModuleFilename: 'assets/[name][ext]',
用于设置全局的静态文件路径及文件名。如果文件模块没有单独进行配置,就按照这个来设置文件名。
其中,[name] 表示原来的文件名,[hash] 表示散列值,[ext] 表示文件后缀。这些都属于占位符(placeholders),在 webpack4 中有提到:v4.webpack.js.org/loaders/fil…[5]
3. asset 类型
当 type 设置为'asset'
,就会按照以下的策略去打包文件:
如果一个模块大小超过 8 kb(这个值是默认的),就使用 asset/resource
,被打包进输出文件夹中。(类似于 file-loader)否则,就使用 asset/inline
,内联到打包文件中。(类似于 url-loader)
区别在于:前者会被单独放进输出文件夹中,后者被处理成 base64 编码字符串内敛进打包出的 JS 文件中。
后者的好处在于减少一次 http 请求,但是过长的字符串也会加重 js 的体积导致加载变慢,因此需要根据实际情况来确定到底采用哪一种方式去处理文件。
注意,当被作为后者处理时,是可以设置编码方式的,例如上面提到的特殊处理。
手动通过 `Rule.parser.dataUrlCondition.maxSize`[6] 去设置两者的界限:
{
test: /\.(jpe?g|png|gif)$/i,
type: 'asset',
generator: {
filename: 'images/[name]_[hash][ext]',
},
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8kb (低于8kb都会压缩成 base64)
}
},
},
{
test: /\.svg$/i,
type: 'asset',
generator: {
filename: 'icons/[name]_[hash][ext]',
dataUrl(content) {
content = content.toString();
return miniSVGDataURI(content); // 通过插件提供的编码算法处理文件
}
},
parser: {
dataUrlCondition: {
maxSize: 2 * 1024 // 2kb (低于2kb都会压缩)
}
},
},
复制代码
另外,除了asset/resource
、asset/inline
,还有一个上文提到的,用于处理 txt、xml 文件的asset/source
,它相当于 webpack4 的 raw-loader。
三、样式文件的处理
1. style-loader & css-loader
style-loader 和 css-loader 是相辅相成的。
style-loader:将 标签插入到 DOM 中。
css-loader:解析通过 @import、url()、import/require() 这些方式引入的样式文件。
安装 loaders:
npm install --save-dev css-loader style-loader
复制代码
webpack.config.js
module.exports = {
module: {
rules: [
// ...
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
复制代码
注意两个 loader 的位置,要反过来写。
index.js
// ...
const reset = require("./assets/styles/reset.css");
console.log("css: ", reset);
import "./assets/styles/reset.css";
const dom = document.getElementById("root");
dom.innerHTML += 'This a text.
';
复制代码
reset.css
@font-face {
font-family: "iconfont";
/* Project id 1947684 */
src: url("../fonts/iconfont.woff2?t=1627306378388") format("woff2"), url("../fonts/iconfont.woff?t=1627306378388") format("woff"),
url("../fonts/iconfont.ttf?t=1627306378388") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-mianxingshezhi:before {
content: "\e6ad";
}
/* ... */
复制代码
解释:reset.css 中引入了字体图标(来自 iconfont),index.js 中通过import
的方式引入了这个 css 文件,并使用了这个字体图标。
其中,
import './assets/styles/reset.css'
、
onst reset = require('./assets/styles/reset.css');
这两段语句背后的 CSS 代码就是通过 css-loader 去解析的。
控制台展示如下:
上图中,在 CSS 样式被 css-loader 解析完成后, 标签通过 style-loader 插入到 DOM 中。
2. sass-loader
sass-loader:加载一个 Sass/SCSS 文件,并编译成 CSS。
安装:
npm install sass-loader sass webpack --save-dev
复制代码
webpack.config.js
module.exports = {
module: {
rules: [
// ...
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
],
},
};
复制代码
index.js
const global = require("./assets/styles/global.scss");
console.log("scss: ", global);
import "./assets/styles/global.scss";
const dom = document.getElementById("root");
// insert an image
const img = new Image();
img.src = dog1;
// img.width = 200;
img.classList.add("avatar");
root.append(img);
复制代码
global.scss
// 自定义变量
$color: #ff4200;
$fs: 14px;
$ls: 1.2;
// 自定义mixin
@mixin size($w, $h: $w) {
width: $w;
height: $h;
}
body {
font-size: $fs;
background-color: #eaeaea;
.avatar {
@include size(150px);
transform: translateX(50px);
}
}
复制代码
控制台展示如下:
上图第二个 标签内的样式代码就是 global.scss 中转译成 css,并通过 style-loader 插入到 DOM 中的。
3. postcss-loader
PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
postcss 仓库:github.com/postcss/pos…[7]
autoprefixer:github.com/postcss/aut…[8]
postcss-preset-env:github.com/csstools/po…[9]
a) 安装
安装:
npm install --save-dev postcss-loader postcss
复制代码
webpack.config.js
module.exports = {
module: {
rules: [
// ...
{
test: /\.s?css$/i,
use: ["style-loader", "css-loader", "sass-loader", "postcss-loader"],
},
],
},
};
复制代码
注意,postcss-loader 要放在最后。
b) 配置
Autoprefixer
PostCSS[10] plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use[11].
PostCSS Preset Env
PostCSS Preset Env[12] lets you convert modern CSS into something most browsers can understand, determining the polyfills you need based on your targeted browsers or runtime environments.
PostCSS SCSS Syntax
A SCSS[13] parser for PostCSS[14].
This module does not compile SCSS. It simply parses mixins as custom at-rules & variables as properties, so that PostCSS plugins can then transform SCSS source code alongside CSS.
插件需要安装一下:
npm install postcss-preset-env autoprefixer postcss-scss --save-dev
复制代码
在根目录下,新建:postcss.config.js
module.exports = {
syntax: "postcss-scss",
plugins: [require("autoprefixer"), "postcss-preset-env"],
};
复制代码
webpack.config.js 不需要修改。
四、补充:css-loader 的 options
不同的 loader 都会有各自的 options,css-loaders 有两个实用的 options。
importLoaders:The option importLoaders
allows you to configure how many loaders beforecss-loader
should be applied to@import
ed resources and CSS modules/ICSS imports. 简单来说,就是允许你在执行 css-loader 前,让某些资源要经过前面的 loaders 去处理的个数。modules:Allows to enable/disable CSS Modules or ICSS and setup configuration. 允许你开启 CSS Module。
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.sc?ss$/i,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 2,
// 0 => no loaders (default);
// 1 => postcss-loader;
// 2 => postcss-loader, sass-loader
modules: true, // 默认是 false ***
},
},
"sass-loader",
"postcss-loader",
],
},
],
},
};
复制代码
注意,上面 importLoaders 设置为 2,表示匹配到样式文件时,都会去执行一开始的两个 loader,也就是 'sass-loader' 以及 'postcss-loader'.
至于 modules 设置为 true,则保证了样式模块的独立性,不会被互相覆盖。具体看下面的例子:
a) 不设置 modules 选项
src/assets/styles/global.scss
// 自定义变量
$color: #ff4200;
$fs: 14px;
$ls: 1.2;
// 自定义mixin
@mixin size($w, $h: $w) {
width: $w;
height: $h;
}
body {
font-size: $fs;
background-color: #eaeaea;
.avatar {
@include size(150px);
transform: translateX(50px);
}
}
复制代码
src/assets/js/createImg.js
import avatar from "../images/avatar-2.jpg";
import dog1 from "../images/animal-dog-1.png";
function createImg(root) {
const img = new Image();
img.src = dog1;
img.classList.add("avatar"); // 添加 avatar 类名
root.append(img);
}
export default createImg;
复制代码
src/index.js
import "./assets/styles/reset.css";
import "./assets/styles/global.scss";
import dog1 from "./assets/images/animal-dog-1.png";
import createImg from "./assets/js/createImg.js";
const dom = document.getElementById("root");
// 第一张图的创建
createImg(dom);
// 第二张图的创建
// insert an image
const img = new Image();
img.src = dog1;
img.classList.add("avatar"); // // 添加 avatar 类名
root.append(img);
复制代码
在这种情况下,import './assets/styles/global.scss'
这一句对全局的文件都有效果,因此它的后一句 import createImg from './assets/js/createImg.js';
就会受到影响。影响的结果就是,createImg.js
文件中的第 7 行代码的样式添加,添加的就是 global.scss 中的第 15 行的选择器 'avatar' 的样式。
打包后,结果如图所示:
b) 将 modules 设置为 true
设置完后,代码不变的情况下,打包完成后,查看结果如图所示:
前后两张秋田犬均变成了超大的图,原因是样式没有作用到图片上。
当应用 CSS Module[15] 时,要通过以下的方式添加样式:
(注意:这里仅对 index.js 中的代码作修改,使之恢复,而 createImg.js 模块中的代码不变,从而形成对比。)
src/index.js
import "./assets/styles/global.scss";
import style from "./assets/styles/global.scss"; // 自定义变量名称,作为 css module 导入
import dog1 from "./assets/images/animal-dog-1.png";
import createImg from "./assets/js/createImg.js";
const dom = document.getElementById("root");
// 第一张图的创建
createImg(dom);
// 第二张图的创建
// insert an image
const img = new Image();
img.src = dog1;
// css module 的使用:通过自定义变量名称 style 加 . 的形式访问到 avatar 类名
img.classList.add(style.avatar);
root.append(img);
复制代码
打包后,结果如图所示:
通过对比,就可以发现,通过模块方式设置的秋田犬的样式是有的,而第一张则没有。
这种利用 CSS Module 的方式来添加样式的方法,解决了全局覆盖同名样式(一般是类名和 id)的尴尬情况。
阅读参考:
github.com/css-modules…[16] github.com/css-modules…[17] webpack.js.org/loaders/css…[18] webpack.js.org/loaders/css…[19]
小结
以上,是本篇的所有内容。在 Webpack5 中,对于静态资源的处理,我们只要简单地设置 type 就能处理文件,非常方便。但是有些特殊情况还是需要去单独处理的,例如资源输出的路径及文件名的设置、URI 编码格式的设置、转 base64 的文件大小限制的设置。样式文件中,为了防止全局的样式污染,可以开启 CSS Module 来避免。
关于本文
本系列来自:EricKnight
https://juejin.cn/user/2154698521972423/posts