Webpack 打包样式资源:style-loader 与 css-loader
打包样式资源
style-loader
把 CSS 插入到 DOM 中。
css-loader
对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样。
url: 启用/禁用url()解析。- 如果设置为
false,css-loader将不会解析url或者image-set中的任何路径。
- 如果设置为
import:启用/禁用@import规则进行处理。sourceMap:启用/禁用生成SourceMapimportLoaders:指定在css-loader之前还要经过几个 loader 处理@import引入的文件。
这个选项是最容易配错的一个。它的含义不是「应用几个 loader」,而是「@import 进来的那个文件,需要回头再走几个前置 loader」。
问题的来源在于:css-loader 处理 @import 时是自己解析、自己内联的,它并不知道被引入的文件原本也需要 sass-loader 或 postcss-loader 处理。所以 a.scss 里 @import './b.scss',b.scss 的内容会被 css-loader 直接塞进来,其中的 Sass 语法就没人编译了。importLoaders: 2 告诉 css-loader:把 b.scss 交回给我前面的 2 个 loader(postcss-loader 和 sass-loader)先处理。
数字怎么数:从 css-loader 往后(也就是配置数组里往下)数几个。
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
// 后面还有 postcss-loader 和 sass-loader 两个,所以是 2
options: { importLoaders: 2 },
},
'postcss-loader',
'sass-loader',
],
}
只有 postcss-loader 时写 1,纯 css-loader 时写 0(默认值)。
实践中的替代做法是改用 @use / @import 让 Sass 自己处理引入——Sass 编译阶段就把文件合并了,轮到 css-loader 时已经是一整份 CSS,没有 @import 需要它解析。所以 SCSS 项目里这个选项其实很少真正起作用,真正需要它的是原生 CSS 里的 @import 配 PostCSS 的场景。
loader 的执行顺序
use 数组是从右到左、从下到上执行的。上面那份配置的实际流程:
b.scss / a.scss
→ sass-loader 编译 Sass 语法,产出 CSS 字符串
→ postcss-loader 跑 autoprefixer 等插件
→ css-loader 解析 @import / url(),产出一个 JS 模块
→ style-loader 产出往 document 插 <style> 的 JS 代码
→ 打进 bundle
理解这个顺序的关键是:每个 loader 的输入是后一个 loader 的输出。css-loader 拿到的必须已经是合法 CSS,所以 sass-loader 必须排在它后面。顺序写反了会报一堆 Unexpected character '@' 之类的语法错误。
style-loader 做了什么
它把 CSS 变成一段运行时 JS:
// style-loader 产出的代码大意
const style = document.createElement('style');
style.textContent = '.app{color:red}';
document.head.appendChild(style);
也就是说 CSS 最终是打进 JS bundle 里、运行时才插入 DOM 的。这带来两个后果:
- 好处是支持 HMR,改 CSS 不刷新页面就能生效。
- 坏处是 CSS 要等 JS 下载执行完才生效,首屏会有一小段无样式内容闪烁(FOUC);而且 JS 体积变大,CSS 无法被浏览器并行下载和独立缓存。
所以生产环境应该换成 mini-css-extract-plugin,把 CSS 抽成独立的 .css 文件:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// 开发用 style-loader(有 HMR),生产用抽取
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }),
],
};
注意 MiniCssExtractPlugin.loader 和 style-loader 是互斥的,同时用会两份样式都生效。
css-loader 的 modules 选项
原文没提但这是 css-loader 最常用的能力之一——CSS Modules,通过类名哈希做样式隔离:
{
loader: 'css-loader',
options: {
modules: {
// 生成的类名格式,生产环境常用纯 hash
localIdentName: '[name]__[local]--[hash:base64:5]',
// 导出的 key 转成驼峰,JS 里可以写 styles.myClass
exportLocalsConvention: 'camelCase',
// 只对 .module.css 结尾的文件启用
auto: /\.module\.\w+$/i,
},
},
}
// 用法
import styles from './App.module.css';
// styles.title 是哈希后的类名,如 'App__title--a1b2c'
element.className = styles.title;
auto 这个选项很有用,配一次就能让 .module.css 走模块化、普通 .css 走全局,不用为两者写两条 rule。
url() 解析的坑
css-loader 会把 url(./img/a.png) 当成一个模块依赖去解析,这意味着路径必须是相对当前 CSS 文件的、真实存在的文件。两种情况会出问题:
/* 会被解析,文件不存在就报 Module not found */
background: url('./img/a.png');
/* 绝对路径和以 / 开头的路径默认不解析,原样输出 */
background: url('/static/a.png');
/* 想让某个 url 不被处理,用 ~ 或配 url.filter */
background: url('~some-pkg/img/a.png'); /* ~ 表示从 node_modules 解析 */
运行时才拼出来的路径(比如 url(var(--icon)))css-loader 处理不了,这类资源要放到 public 目录走绝对路径。需要豁免某些路径时用 url.filter:
{
loader: 'css-loader',
options: {
url: {
filter: (url) => !url.startsWith('/static/'),
},
},
}
webpack 5 里图片资源不需要 loader 了
顺带提一句版本差异:webpack 4 时代处理 url() 引入的图片还要配 file-loader / url-loader,webpack 5 内置了 Asset Modules,直接用 type 声明就行:
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
// 小于 8kb 转 base64 内联,大于则输出文件
dataUrlCondition: { maxSize: 8 * 1024 },
},
}
type: 'asset' 对应旧的 url-loader + limit,'asset/resource' 对应 file-loader,'asset/inline' 对应 url-loader 强制内联。老项目升级 webpack 5 时这几个 loader 应该一并移除,留着会和内置逻辑冲突。

还没有留言,来发表第一条吧~