本文深入解析 Webpack 中样式加载的实现原理。从 css-loader 和 style-loader 的基础配置入手,演示如何打包 CSS 与 LESS 文件。重点讲解 CSS Modules 的本地作用域机制、全局样式处理及类名组合技巧,并结合 extract-text-webpack-plugin 插件实现生产环境下的样式文件分离输出。
Webpack 样式加载基础原理
在 Webpack 中加载 CSS 需要配合 css-loader 和 style-loader 使用。css-loader 负责处理 @import 和 url 语句,将其转换为标准的 ES6 import 语法。若 @import 指向外部资源,css-loader 会跳过处理,仅对内部资源进行操作。随后,style-loader 将处理后的 CSS 注入到打包文件中。默认情况下,CSS 以 inline 模式存在,并实现了 HMR(模块热替换)接口。虽然 inline 模式便于开发调试,但不适用于生产环境,因为所有样式都会直接输出到页面中。为了生成独立的 CSS 文件,通常需要借助 extract-text-webpack-plugin 插件,下面我们将逐步展开说明。
样式打包与 LESS 支持
一、样式打包基础
1. 安装 css-loader 和 style-loader
npm install css-loader style-loader --save-dev
2. 修改 webpack.config.js 配置文件,增加一个 rules 子节点
module:{
rules:[{
test:/\.css$/,
use: ['style-loader', 'css-loader'],
}]
},
test 正则表达式用于匹配 .css 文件。use 数组中的 loader 执行顺序是从右到左,即先执行 css-loader,再执行 style-loader。这类似于管道机制,数据依次流经各个 loader。配置 loaders: ['style-loader', 'css-loader'] 可理解为执行顺序为 styleLoader(cssLoader(input))。
3. 添加样式文件
在 app/main.css 中定义样式
body {
background: cornsilk;
}
然后在 index.js 中引入该样式文件
import './main.css';
运行 npm start,并在浏览器访问 http://localhost:8080/ 查看效果

此时页面背景色生效,且样式被注入到 header 标签中。修改颜色值后,界面会无刷新更新,这正是 HMR(模块热替换)的功能体现。

样式的更新是通过 webpackHotUpdate 方法实现的。
二、加载 LESS 样式
接下来演示如何加载 LESS 文件,首先需要安装 less-loader
npm install less less-loader --save-dev
修改配置文件以支持 LESS
module:{
rules:[{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
}]
},
创建一个 less.less 文件
@base: #f938ab;
.box-shadow(@style, @c) when (iscolor(@c)) {
-webkit-box-shadow: @style @c;
box-shadow: @style @c;
}
.box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) {
.box-shadow(@style, rgba(0, 0, 0, @alpha));
}
.box {
color: saturate(@base, 5%);
border-color: lighten(@base, 30%);
div { .box-shadow(0 0 5px, 30%) }
}
body {
background: cornsilk;
}
在 index.js 中引入该 LESS 文件
import './less.less';
import component from './component';
var ele=document.createElement("div");
ele.innerHTML="this is an box";
ele.className="box";
document.body.appendChild(ele);
let demoComponent=component();
document.body.appendChild(demoComponent);
编译效果如下

可以看到编译成功。需要注意的是,使用 LESS 时,@import 语句只能指向 .less 文件,若此时 @import main.css 会报错。本节仅对 LESS 进行简单演示,其他样式预处理器的配置同理,后续内容仍基于 CSS 展开。
CSS Modules 作用域与实战
三、理解 CSS 作用域与 CSS Modules
通常 CSS 的作用域是全局的,在母版页中引入多个样式文件时,后加载的样式会覆盖前面的样式,这常给调试带来困扰。CSS Modules 通过 import 引入了本地作用域,从而避免命名空间冲突。Webpack 的 css-loader 支持 CSS Modules,具体配置如下(此处先关闭 HMR):
module:{
rules:[{
test:/\.css$/,
use: ['style-loader', {
loader: 'css-loader',
options: {
modules: true,//让css-loader支持Css Modules。
},
},],
定义一个新的样式文件 main.css
body {
background: cornsilk;
}
.redButton {
background: red;color:yellow;
}
为组件添加样式,首先引入 main.css
import styles from './main.css';
export default function () {
var element = document.createElement('h1');
element.className=styles.redButton;
element.innerHTML = 'Hello webpack';
return element;
}
此时界面已发生变化。

观察右侧生成的样式代码,类名已被修改。回顾整个过程,main.css 中的每个类名都变成了一个模块,在 JS 中可以通过类似获取模块的方式引用。但可能会有疑问:为什么不直接给元素赋值,而要通过 import?这是一个好问题,我们继续新增一个样式。
处理不同样式文件中的同名类
在 other.css 中定义
.redButton {
background:rebeccapurple;color:snow;
}
该文件也包含一个 .redButton 类(但效果为紫色),然后在 index.js 中创建一个 div 元素并应用 redButton 样式。
import './main.css';
import styles from './other.css';
import component from './component';
var ele=document.createElement("div");
ele.innerHTML="this is an other button";
ele.className=styles.redButton;
document.body.appendChild(ele);
let demoComponent=component();
document.body.appendChild(demoComponent);
查看效果

上图说明了两个问题:首先,在 index.js 中引入两个样式文件后,页面输出了两个 style 标签,这略显冗余,但后续会解决;其次,尽管两个文件中都有 .redButton 类,但它们保持独立,避免了命名空间干扰。如果此时直接通过字符串赋值:
element.className="redButton";
将无法获取到样式,因为直接对元素样式赋值默认是全局的。
处理全局样式
若希望某个样式成为全局样式,可以使用 :global 包裹。
在 other.css 中
:global(.redButton) {
background:rebeccapurple;color:snow;
border: 1px solid red;
}
在 main.css 中
:global(.redButton) {
background: red;color:yellow;
}
此时 .redButton 样式将合并。需要直接通过类名获取。
element.className="redButton";

组合样式
修改 other.css,创建 .shadowButton 样式,内部通过 composes 组合 .redButton 类。
.redButton {
background:rebeccapurple;color:snow;
border: 1px solid red;
}
.shadowButton{
composes:redButton;
box-shadow: 0 0 15px black;
}
修改 index.js 代码
var ele=document.createElement("div");
ele.innerHTML="this is an shadowButton button";
console.log(styles);
ele.className=styles.shadowButton;
document.body.appendChild(ele);
查看运行效果

日志打印出 styles 对象,包含两个类名。可见 .shadowButton 是由两个类名组合而成的,div 的 class 属性与下方对应。
生产环境样式文件提取
四、输出独立样式文件
CSS 嵌入页面并非理想状态,我们希望实现分离,将公共部分单独提取。extract-text-webpack-plugin 插件可将多个 CSS 合并为一个文件,但它不支持 HMR(需注释掉 hotOnly: true),因此更适合生产环境。
npm install extract-text-webpack-plugin --save-dev
首先安装 extract-text-webpack-plugin 插件,然后在 webpack.config.js 中进行配置:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractTxtplugin = new ExtractTextPlugin({
filename: '[name].[contenthash:8].css',
});
const commonConfig={
entry: {
app: PATHS.app,
},
output: {
path: PATHS.build,
filename: '[name].js',
},
module:{
rules:[{
test:/\.css$/,
use:extractTxtplugin.extract({
use:'css-loader',
fallback: 'style-loader',
})
}]},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack demo',
}),
extractTxtplugin
],
}
初看此配置可能令人困惑。首先看 fileName,表示最终输出文件的格式为 '[name].[contenthash:8].css'。name 默认对应文件夹名称(这里是 app),contenthash 返回特定内容的哈希值,:8 表示取前 8 位。当然也可以按其他格式命名,例如直接指定文件名。
new ExtractTextPlugin('style.css')
ExtractTextPlugin.extract 本身是一个 loader。fallback: 'style-loader' 的含义是:当有 CSS 未被提取(如外部引入的 CSS)时,使用 style-loader 进行处理。注意此时我们的 index.js 如下:
import './main.css';
import styles from './other.css';
import component from './component';
var ele=document.createElement("div");
ele.innerHTML="this is an box";
ele.className=styles.shadowButton;
document.body.appendChild(ele);
let demoComponent=component();
document.body.appendChild(demoComponent);
//HMR 接口
if(module.hot){
module.hot.accept('./component',()=>{
const nextComponent=component();
document.body.replaceChild(nextComponent,demoComponent);
demoComponent=nextComponent;
})
}
引入了两个 CSS 文件。
执行 npm run build 命令

查看文件夹,得到一个样式文件(若不想看到日志可直接运行 npm build)。

然而,在第三部分使用了 CSS Modules 后发现,other.css 的样式未被打包进来。因此,webpack.config.js 还需进一步修改:
module:{
rules:[{
test:/\.css$/,
use:extractTxtplugin.extract({
use:[ {
loader: 'css-loader',
options: {
modules: true,
},
}],
fallback: 'style-loader',
})
}]},
再次执行 build。

发现两个样式已打包成一个文件。只要内容发生变化,样式文件的名称就会随之改变。更多配置细节可参考 https://www.npmjs.com/package/extract-text-webpack-plugin
小结:本文内容较多,从基本的样式打包、LESS 处理,到 CSS Modules 的认识,最后实现文件打包输出。对新手而言可能略显复杂,工具带来了便利性,也增加了学习成本。在诸多选择和配置中,我们需要找到适合自身的方案,并理解各模块机制,以应对不同需求。本节源码:http://xz.jb51.net:81/201806/yuanma/webpack-ch4_jb51.rar
参考链接:
https://www.npmjs.com/package/css-loader#local-scope
https://survivejs.com/webpack/styling/loading/
https://survivejs.com/webpack/styling/separating-css/
系列文章:
【webpack】-- 入门与解析
【webpack】-- 自动刷新与解析
【webpack】-- 模块热替换
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。