Webpack 样式加载原理与实战:从基础到 CSS Modules

作者:微光时间:2026-10-22 15:19:35浏览:559

本文深入解析 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/ 查看效果

Webpack 样式加载基础原理

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

Webpack 样式加载基础原理

样式的更新是通过 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);

编译效果如下

Webpack 样式加载基础原理

可以看到编译成功。需要注意的是,使用 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;
}

此时界面已发生变化。

Webpack 样式加载基础原理

观察右侧生成的样式代码,类名已被修改。回顾整个过程,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);

查看效果

Webpack 样式加载基础原理

上图说明了两个问题:首先,在 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";

Webpack 样式加载基础原理

组合样式

修改 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);

查看运行效果

Webpack 样式加载基础原理

日志打印出 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 命令

Webpack 样式加载基础原理

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

Webpack 样式加载基础原理

然而,在第三部分使用了 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。

Webpack 样式加载基础原理

发现两个样式已打包成一个文件。只要内容发生变化,样式文件的名称就会随之改变。更多配置细节可参考 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】-- 模块热替换

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章