CSS Modules 入门教程

为何引入CSS Modules

或者能够这么说,CSS Modules为咱们解决了什么痛点。针对以往我写网页样式的经验,具体来讲能够概括为如下几点:javascript

全局样式冲突

过程是这样的:你如今有两个模块,分别为A、B,你可能会单独针对这两个模块编写本身的样式,例如a.css、b.css,看一下代码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>'
复制代码

下面是样式:html

/* 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)显示顺序为:java

/* mix.css */

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

/* b.css */
.text {
    color: blue;
}
复制代码

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

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

var moduleB = (function(document, undefined){
    // your module code
})(document)
复制代码

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

嵌套层次过深的选择器

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

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

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

  • 根据CSS选择器的解析规则能够知道,层级越深,比较的次数也就越多。固然在更多的状况下,可能嵌套的层次还会更深,另外,这里单单用了类选择器,而采用类选择器的时候,可能对整个网页的渲染影响更重。
  • 增长了没必要要的字节开销
  • 语义混乱,当文档中出现过多的contenttitle以及item这些通用的类名时,你可能要花上老半天才知道它们究竟是用在哪一个元素上
  • 可扩展性很差,约束越多,扩展性越差

【注】CSS的渲染规则能够参看这篇文章探究 CSS 解析原理web

会带来代码的冗余

因为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的用法这里再也不赘述,若是不清楚,能够本身查阅相关资料去了解一下。

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的实例
  • Element:Block的组成部分,依赖Block存在(出了Block就不能用)

  • [可选]定义Block和Element的外观及行为,就像HTML属性同样,能让同一种Block看起来不同

命名规则

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

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

<div class="header">...</div>
复制代码

Element依附Block存在,没有单独的含义,命名上语义尽可能接近于Block,好比titleitem之类

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

<div class="header">
    <h1 class="header__title">Header</h1>
</div>
复制代码

Modifier是一个元素的状态显示,例如activecurrentselected

<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>
复制代码

【说明】

  • Block彻底独立,能够嵌套,一个header是一个Block,header下的搜索框也能够是一个Block
  • 不可能出现Block__Element-father__Element-son_Modifer这种类名的写法,BEM只有三级
  • Modifier能够加在Block和Element上面
  • Modifier做为一个额外的类名加载Block和Element上面,只是为了改变状态,须要保留本来的class

一个完整的示例

<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使用时,也能获得必定程度的扩展,可是它依然有它的问题:

  • 对于嵌套过深的层次在命名上会给须要语义化体现的元素形成很大的困难
  • 对于多人协做上,须要统一命名规范,这一样也会形成额外的effort

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 解决了什么问题

  • 全局命名冲突,由于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

webpack.config.js中加入可以处理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

在浏览器中显示为:

css-loader

提取公有样式

能够看到打包以后的build目录下只有一个bundle.js,咱们如今要把样式文件提取出来

./build/
└── bundle.js
复制代码
  • 安装依赖
npm install --save-dev mini-css-extract-plugin
复制代码
  • 修改webpack.config.js
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>
复制代码
  • 编译打包

extract

能够看到有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对象打印出来,看看具体是些什么东西:

object

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

* {
	margin: 0;
	padding: 0;
}

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

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

合成一个文件以后,全部的类名都通过了哈希转换,所以确保了类名的惟一性,咱们再看看浏览器中inspector中的样式应用,以下:

no-transform

事实上,container样式咱们是不须要转换的,由于我是把它固定写死在了容器上,那咱们应该怎么作呢?

全局做用域

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

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

:global(.container) {
	padding: 20px;
}
复制代码

咱们再来看看main.css

global

就能够发现.container类没有被转换

定义哈希类名

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

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

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

localIdentName

类名组合

若是咱们实现相似于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中实现继承,编译以后的结果为:

composes-inner

发现多了一个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去掉,那么自身引入和从其余文件引入的相同申明又该如何显示呢?

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

override

总结

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

最后,本文代码仓库为:github.com/Rynxiao/css…

参考连接

相关文章
相关标签/搜索