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 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 (静态资源模块),替代了之前常用的 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.
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…
3. asset 类型
当 type 设置为'asset'
,就会按照以下的策略去打包文件:
- 如果一个模块大小超过 8 kb(这个值是默认的),就使用
asset/resource
,被打包进输出文件夹中。(类似于 file-loader) - 否则,就使用
asset/inline
,内联到打包文件中。(类似于 url-loader)
区别在于:前者会被单独放进输出文件夹中,后者被处理成 base64 编码字符串内敛进打包出的 JS 文件中。
后者的好处在于减少一次 http 请求,但是过长的字符串也会加重 js 的体积导致加载变慢,因此需要根据实际情况来确定到底采用哪一种方式去处理文件。
注意,当被作为后者处理时,是可以设置编码方式的,例如上面提到的特殊处理。
手动通过 Rule.parser.dataUrlCondition.maxSize
去设置两者的界限:
{
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:将
<style>
标签插入到 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 += '<p class="iconfont icon-mianxingshezhi">This a text.</p>';
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>
标签通过 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);
}
}
控制台展示如下:
上图第二个
<style>
标签内的样式代码就是 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…
autoprefixer:github.com/postcss/aut…
postcss-preset-env:github.com/csstools/po…
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 plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use.
PostCSS Preset Env
PostCSS Preset Env 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
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 时,要通过以下的方式添加样式:
(注意:这里仅对 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…
- github.com/css-modules…
- webpack.js.org/loaders/css…
- webpack.js.org/loaders/css…
小结
以上,是本篇的所有内容。在 Webpack5 中,对于静态资源的处理,我们只要简单地设置 type 就能处理文件,非常方便。但是有些特殊情况还是需要去单独处理的,例如资源输出的路径及文件名的设置、URI 编码格式的设置、转 base64 的文件大小限制的设置。样式文件中,为了防止全局的样式污染,可以开启 CSS Module 来避免。
添加我的微信:enjoy_Mr_cat,共同成长,卷卷群里等你 🤪。
以上,感谢您的阅读~