vue 开发配置扫盲帖:什么是 CSS Modules 以及为什么引入 CSS Modules?

2020-02-16 14:30:20 +08:00
 wwwwzf

什么是 CSS Modulse ?

在做 vue 等 Node 项目时,经常会有一项:是否启用 CSS Modules,如下图所示:

那么,什么是 CSS Modules 呢?使用它有什么意义呢?

CSS Modules,直接强译,就是 css 模块的意思。

当 web 研发越来越多时,为了应对 CSS 的凌乱,于是人们想到了用这种方法。 比如有人在 home.vue 中,直接在文件中行内写了一行代码

	<style>
	.btn{font-size:1rem;}
	</style>
	

接下来,这位作者,又在 about.vue 中,同样在文件中行内写了这么一个相同的 css 定义:

	<style>
	.btn{font-size:0.4rem;}
	</style>
	

那么,这两个代码最后编译会不会冲突?有没有办法规避? 需要知道,vue 这样的框架在诞生,背后很大目的就是为了解决中后台开发者不熟悉 css 甚至是切图这些而生,CSS Modules 则能规避这个问题,他会将两个.btn类分别生成.home_btn.about_btn,从而规避。 当然,背后的逻辑会比这个复杂得多,我们慢慢展开讲解。

为什么引入 CSS Modeules

或者可以这么说,CSS Modules 为我们解决了什么痛点。针对以往我写网页样式的经验,具体来说可以归纳为以下几点:

全局样式冲突

过程是这样的:你现在有两个模块,分别为 A、B,你可能会单独针对这两个模块编写自己的样式,例如 a.css 、b.css ,看一下代码

// A.js
import './a.css'
const html = '<h1 class="text">module A</h1>'

// B.js
import './b.css'
const html = '<h1 class="text">module B</h1>'

下面是样式:

/* a.css */
.text {
	color: red;
}

/* b.css */
.text {
	color: blue;
}

导入到入口 APP 中

// App.js
import A from './A.js'
import B from './B.js'

element.innerTHML = 'xxx'

由于样式是统一加载到入口中,因此实际上的样式合在一起(这里暂定为 mix.css )显示顺序为:

/* mix.css */

/* a.css */
.text {
	color: red;
}

/* b.css */
.text {
	color: blue;
}

根据 CSS 的 Layout 规则,因此后面的样式会覆盖掉前面的样式声明,最终有效的就是 text 的颜色为 blue 的那条规则,这就是全局样式覆盖,同理,这在 js 中也同样存在,因此就引入了模块化,在 js 中可以用立即执行函数表达式来隔离出不同的模块

var moduleA = (function(document, undefined){
	// your module code
})(document)

var moduleB = (function(document, undefined){
	// your module code
})(document)

而在 css 中要想引入模块化,那么就只能通过namespace来实现,而这个又会带来新的问题,这个下面会讲到。

嵌套层次过深的选择器

为了解决全局样式的冲突问题,就不得不引入一些特地命名namespac来区分scope,但是往往有些namespace命名得不够清晰,就会造成要想下一个样式不会覆盖,就要再加一个新的namespace来进行区分,最终可能一个元素最终的显示样式类似如以下:

.widget .table .row .cell .content .header .title {
  padding: 10px 20px;
  font-weight: bold;
  font-size: 2rem;
}

在上一个元素的显示上使用了 7 个选择器,总结起来会有以下问题:

会带来代码的冗余

由于 CSS 不能使用类似于 js 的模块化的功能,可能你在一个 css 文件中写了一个公共的样式类,而你在另外一个 css 也需要这样一个样式,这时候,你可能会多写一次,类似于这样的

/* a.css */

.modal {
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 1;
	background-color: rgba(0, 0, 0, 0.7);
}
.text {
	color: red;
}

/* b.css */
.modal {
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 1;
	background-color: rgba(0, 0, 0, 0.7);
}
.text {
	color: blue;
}

那么在合并成 app.css 的时候,就会被编写两遍,虽然样式不会被影响,但是这样实际上也是一种字节浪费,当然,上述的这种情况完全是可以通过公用全局样式来达到目的,但是,这种代码重复通常是在不知情的情况下发生的。

一些解决方案

针对上述的一些问题,也有一些解决方案,具体如下:

CSS 预处理器(Sass/Less 等),Sass,Less 的用法这里不再赘述,如果不清楚,可以自己查阅相关资料去了解一下,其中 sass 的使用文档在这里可以看到 http://code.z01.com/sass

CSS 预处理器最大的好处就是可以支持模块引入,用 js 的方式来编写 CSS,解决了部分 scope 混乱以及代码冗余的问题,但是也不能完全避免。同时,也没有解决全局样式的冲突问题

一个 SASS 的的文件是这样的:

/* app.sass */

@import './reset'
@import './color'
@import './font'

可以实际上编译之后,终究还是一个文件,因此不可避免的会出现冲突样式

BEM ( Block Element Modifier )

There are only two hard problems in Computer Science: cache invalidation and naming things — Phil Karlton

BEM就是为了解决命名冲突以及更好的语义化而生的。

BEM 名词解释 Block:逻辑和页面功能都独立的页面组件,是一个可复用单元,特点如下:

** 命名规则 **

Block作为最小的可复用单元,任意嵌套不会影响功能和外观,命名可以为headermenu等等

<style>
	.header { color: #042; }
</style>

<div class="header">...</div>

Element依附 Block 存在,没有单独的含义,命名上语义尽量接近于 Block,比如 title、item 之类

<style>
	.header { color: #042; }
	.header__title { color: #042; }
</style>

<div class="header">
	<h1 class="header__title">Header</h1>
</div>

Modifier是一个元素的状态显示,例如active、currentselected`

<style>
    .header--color-black { color: #000; }
    .header__title--color-red { color: #f00; }
</style>

<div class="header header--color-black">
    <h1 class="header__title">
        <span class="header__title--color-red">Header</span>
    </h1>
</div>

[说明]

一个完整的示例

<form class="form form--theme-xmas form--simple">
  <input class="form__input" type="text" />
  <input
    class="form__submit form__submit--disabled"
    type="submit" />
</form>
.form { }
.form--theme-xmas { }
.form--simple { }
.form__input { }
.form__submit { }
.form__submit--disabled { }

参考链接:

BEM 解决了模块复用、全局命名冲突等问题,配合预处理 CSS 使用时,也能得到一定程度的扩展,但是它依然有它的问题:

CSS Modules

说了这么多,终于要到正文了

什么是 CSS Modules

根据 CSS Modules 的repo上的话来说是这样的:

CSS files in which all class names and animation names are scoped locally by default.

所以 CSS Modules 并不是一个正式的声明或者是浏览器的一个实现,而是通过构建工具( webpack or Browserify )来使所有的 class 达到 scope 的一个过程。

CSS Modules 解决了什么问题

/* App.css */
.text {
    color: red;
}

/* 编译之后可能是这样的 */
.App__text___3lRY_ {
    color: red;
}

命名唯一,因此保证了全局不会冲突。

可以使用composes来引入自身模块中的样式以及另一个模块的样式:

.serif-font {
  font-family: Georgia, serif;
}

.display {
  composes: serif-font;
  font-size: 30px;
  line-height: 35px;
}

应用到元素上可以这样使用:

import type from "./type.css";

element.innerHTML = 
  `<h1 class="${type.display}">
    This is a heading
  </h1>`;

之后编译出来的模板可能是这样的:

<h1 class="Type__display__0980340 Type__serif__404840">
  Heading title
</h1>

从另一个模块中引入,可以这样写:

.element {
  composes: dark-red from "./colors.css";
  font-size: 30px;
  line-height: 1.2;
}

因为 CSS Modules 只关注与组件本身,组件本身基本都可以使用扁平的类名来写,类似于这样的:

.root {
  composes: box from "shared/styles/layout.css";
  border-style: dotted;
  border-color: green;
}

.text {
  composes: heading from "shared/styles/typography.css";
  font-weight: 200;
  color: green;
}

CSS Modules 怎么用

CSS Modules 不局限于你使用哪个前端库,无论是 React、Vue 还是 Angular,只要你能使用构建工具进行编译打包就可以使用。

下面我使用webpack为例,一步一步引入 CSS Modules.

构建最初始的应用

.
├── build
│   └── bundle.js
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   └── styles
└── webpack.config.js

index.js 作为程序入口,styles 文件夹存放样式文件,配合 webpack.config.js 作为 webpack 配置文件。

// index.js
var html = `<div class="header">
	<h2 class="title">CSS Modules</h2>
</div>`

document.getElementById('container').innerHTML = html;

样式文件:

/* global.css */
* {
	margin: 0;
	padding: 0;
}

.container {
	padding: 20px;
}

/* index.css */
.header {
	font-size: 32px;
}

.title {
	border-bottom: 1px solid #ccc;
	padding-bottom: 20px;
}

模板文件:

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>css modules</title>
</head>
<body>
	<div id="container" class="container"></div>
	<script src="./build/bundle.js"></script>
</body>
</html>

全局安装依赖,配置执行脚本:

npm install webpack webpack-cli --save-dev

package.json

"scripts": {
    "build": "npx webpack && open index.html"
}

在控制台执行npm run build, 得到的结果为:

> css-modules-demo@1.0.0 build /Users/yhhu/Documents/coding/css-modules-demo
> npx webpack && open index.html

Hash: 5810d2ecd760c08cc078
Version: webpack 4.17.1
Time: 78ms
Built at: 2018-08-26 15:09:31
    Asset      Size  Chunks             Chunk Names
bundle.js  3.97 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./src/index.js] 196 bytes {main} [built]

加入样式以及 loaders

package.json 中加入能够处理 css 的 loader

  module: {
    rules: [
      {
        test: /\.js/,
        loader: 'babel-loader',
        include: __dirname + '/src',
        exclude: __dirname + '/src/styles'
     	},
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {         
            }
          }
        ]
      }
    ]
  }

index.js 中引入两个 CSS 文件

// index.js
import './styles/global.css'
import './styles/index.css'

const html = `<div class="header">
	<h2 class="title">CSS Modules</h2>
</div>`

document.getElementById('container').innerHTML = html;

编译之后的执行结果为:

在浏览器中显示为:

提取公有样式

可以看到打包之后的build目录下只有一个bundle.js,我们现在要把样式文件提取出来

./build/
└── bundle.js
npm install --save-dev mini-css-extract-plugin
var MiniCssExtractPlugin = require("mini-css-extract-plugin");

modules: {
    rules: [
        // {
        //   test: /\.css$/,
        //   use: [
        //   	{ loader: "style-loader" },
        //     {
        //     	loader: "css-loader",
        //     	options: {
        
        //     	}
        //     }
        //   ]
        // },
        {
            test: /\.css$/,
            use: [
              {
                loader: MiniCssExtractPlugin.loader,
                options: {
                  publicPath: './build/styles'
                }
              },
              { 
                loader: "css-loader",
                options: {
                    
                }
              }
            ]
        }        
    ]
},
plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
],
<!-- index.html -->

<!DOCTYPE html>
<head>
	<link rel="stylesheet" href="./build/main.css">
</head>
<body>
	<div id="container" class="container"></div>
	<script src="./build/bundle.js"></script>
</body>

可以看到有main.css生成

开启 css modules 功能

默认在css-loader中是不开启css modules功能的,要开启可以设置modules: true即可,更多可以参看官方css-loader使用方法修改webpack.config.js,如下:

{
    test: /\.css$/,
    use: [
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          publicPath: './build/styles'
        }
      },
      { 
        loader: "css-loader",
        options: {
            modules: true
        }
      }
    ]
}        

修改index.js文件中的引用方式:

import './styles/global.css'
import Index from './styles/index.css'

const html = `<div class=${Index.header}>
	<h2 class=${Index.title}>CSS Modules</h2>
</div>`

document.getElementById('container').innerHTML = html;

可以看到,之前都是直接import一个css文件,而现在改成了导出一个对象的形式,我们可以把Index对象打印出来,看看具体是些什么东西:

直接对应我们引用的方式,然后我们再看看生成出来的main.css中具体有哪些东西:

* {
	margin: 0;
	padding: 0;
}

._2BQ9qrIFipNbLIGEytIz5Q {
	padding: 20px;
}
._3Ukt9LHwDhphmidalfey-S {
	font-size: 32px;
}

._3XpLkKvmw0hNfJyl8yU3i4 {
	border-bottom: 1px solid #ccc;
	padding-bottom: 20px;
}

合成一个文件之后,所有的类名都经过了哈希转换,因此确保了类名的唯一性,我们再看看浏览器中inspector中的样式应用,如下:

事实上,container样式我们是不需要转换的,因为我是把它固定写死在了容器上,那我们应该怎么做呢?

全局作用域

要想一个类名不需要被装换,那么可以使用:global(className)来进行包装,这样的类不会被转换,会被原样输出,下面我们修改global.css

/* global.css */
* {
	margin: 0;
	padding: 0;
}

:global(.container) {
	padding: 20px;
}

我们再来看看main.css

就可以发现.container类没有被转换

定义哈希类名

CSS Modules 默认是以[hash:base64]来进行类名转换的,可辨识度不高,因此我们需要自定义

开启自定义,可以使用一个配置参数localIdentName,具体配置如下:

{ 
  loader: "css-loader",
  options: {
  	modules: true,
  	localIdentName: '[path][name]__[local]--[hash:base64:5]'
  }
}

类名组合

如果我们实现类似于Sass的继承功能,我们需要怎么做呢? CSS Modules 中提供了composes关键字让我们来继承另外一个类,修改index.css如下:

.red {
	color: red;
}

.header {
	font-size: 32px;
}

.title {
	composes: red;
	border-bottom: 1px solid #ccc;
	padding-bottom: 20px;
}

我们增加了一个red的类名,在title中实现继承,编译之后的结果为:

发现多了一个src-styles-index__red--1ihPk的类名,正是我们上面继承的那个类

除了在自身模块中继承,我们还可以继承其他文件中的 CSS 规则,具体如下:

我们再styles文件夹下新建一个color.css

/* color.css */
.red {
	color: red;
}

.blue {
	color: blue;
}

然后在index.css文件中导入

/* index.css */
.red {
	color: red;
}

.header {
	font-size: 32px;
}

.title {
	color: green;
	composes: blue from './color.css';
	composes: red;
	border-bottom: 1px solid #ccc;
	padding-bottom: 20px;
}

最终我们会发现文字的颜色为绿色,可见自身模块声明优先级最高,如果把自身申明的color去掉,那么自身引入和从其他文件引入的相同申明又该如何显示呢?

答案是自身引入的声明的优先级会比较高。

总结

至此,所有的 CSS Modules 用法就已经介绍完毕了,至于后续的还有如何应用于ReactVue以及Angular中,相信掌握了上面的内容之后就可以知道怎么写了,如何与预处理器一起使用相信问题也不大。

参考链接

2588 次点击
所在节点    Vue.js
7 条回复
rookiebulls
2020-02-16 14:42:57 +08:00
Vue 的话为啥不直接用 scope
hmxxmh
2020-02-16 14:51:07 +08:00
@rookiebulls 看了前面一点,也是一样的想法
Chingim
2020-02-17 09:57:46 +08:00
vue 根本不需要引入啥 css module
scoped 解决一切烦恼
wwwwzf
2020-02-17 10:24:57 +08:00
根据业务场景不同,没用过不代表没作用。
比如,前端开发者需要掌握 PS 吗?很多会不屑一顾。
再比如,web 开发人员,会打字吗?有人也会不屑一顾。
事实上,有一半以上的从业人员,打 26 个字母都要看着键盘,当然,人们依然有权利选择不屑一顾。

各有所需要,各有作用。
luoway
2020-02-17 11:51:03 +08:00
CSS Modules 和 Vue Scoped 解决的问题是一样的:解决 CSS 作用域的问题。
只是实现方案上不同:
CSS Modules 使用预处理器的方式在 CSS 与 JS 间建立 CSS 类目的关联关系,在保证关联的基础上实现命名唯一;
Vue Scoped 通过在每个 Vue 组件上添加唯一选择器,为对应 CSS 添加选择器后缀,实现 CSS 与选择器一一对应。
因此,优劣势也不同:
CSS Modules 优势是控制精度高,可以复用;劣势是 CSS 不具可读性。
Vue Scoped 优势是原理简单容易理解;劣势是不可通过 JS 控制、不能复用。

如果不是极端复用性要求,Vue 开发者是不需要使用 CSS Modules 的
wwwwzf
2020-02-18 07:37:07 +08:00
@luoway 是的,表述非常精准,也有不同的流派和思想,以及各人僻好。
Hoshinokozo
2020-04-02 10:47:24 +08:00
其实还可以通过类似与 BEM 的方式手动设置作用域,我司的项目就是,每个组件最外层都套个 div,给个独一无二的 class,一般是模块名-组件名__特殊标记,如何保证 class 不重复? webstorm 全局查一下就行了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/645002

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX