什么是css模块化?

为了理解css模块化思想,我们首先了解下,什么是模块化,在百度百科上的解释是,在系统的结构中,模块是可组合、分解和更换的单元。模块化是一种处理复杂系统分解成为更好的可管理模块的方式。它可以通过在不同组件设定不同的功能,把一个问题分解成多个小的独立、互相作用的组件,来处理复杂、大型的软件。看完模块化,是不是有种拼图的即视感,可以把大图分成各个小图,然后把小图拼成大图,分与合的艺术感。那么css模块化思想,也就是在css编写环境中,用上模块化的思想,把一个大的项目,分解成独立的组件,不同的组件负责不同的功能,最后把模块组装,就成了我们要完成的项目了。

css模块化有什么好处?

当做一个大项目,几个人团队合作开发,结果看不懂彼此的代码,怎么办,当面对前人已经写好代码,需要修改,可是无处下手,怎么办.当代码耦合,修改费时费力,怎么办,当需要迭代,面对庞大的代码,牵一发动全身的悲催时刻,怎么办,这个时候,模块化思想就是救星了。css写法特别的灵活,也因为灵活,所以容易耦合在一起,这时候就需要进行模块化的分离。那么css模块化的好处多多,列举了一些如下:

  •     提高代码重用率
  •     提高开发效率、减少沟通成本
  •     提高页面容错
  •     降低耦合
  •     降低发布风险
  •     减少Bug定位时间和Fix成本
  •     更好的实现快速迭代
  •     便于代码维护

CSS 模块化的解决方案有很多,但主要有两类。一类是彻底抛弃 CSS,使用 JS 或 JSON 来写样式。Radiumjsxstylereact-style 属于这一类。优点是能给 CSS 提供 JS 同样强大的模块化能力;缺点是不能利用成熟的 CSS 预处理器(或后处理器) Sass/Less/PostCSS,:hover 和 :active 伪类处理起来复杂。另一类是依旧使用 CSS,但使用 JS 来管理样式依赖,代表是 CSS Modules。CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。发布时依旧编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。是我认为目前最好的 CSS 模块化解决方案。近期在项目中大量使用,下面具体分享下实践中的细节和想法。

CSS 模块化遇到了哪些问题?

CSS 模块化重要的是要解决好两个问题:CSS 样式的导入和导出。灵活按需导入以便复用代码;导出时要能够隐藏内部作用域,以免造成全局污染。Sass/Less/PostCSS 等前仆后继试图解决 CSS 编程能力弱的问题,结果它们做的也确实优秀,但这并没有解决模块化最重要的问题。Facebook 工程师 Vjeux 首先抛出了 React 开发中遇到的一系列 CSS 相关问题。加上我个人的看法,总结如下:

  1. 全局污染
    CSS 使用全局选择器机制来设置样式,优点是方便重写样式。缺点是所有的样式都是全局生效,样式可能被错误覆盖,因此产生了非常丑陋的 !important,甚至 inline !important 和复杂的选择器权重计数表,提高犯错概率和使用成本。Web Components 标准中的 Shadow DOM 能彻底解决这个问题,但它的做法有点极端,样式彻底局部化,造成外部无法重写样式,损失了灵活性。

  2. 命名混乱 由于全局污染的问题,多人协同开发时为了避免样式冲突,选择器越来越复杂,容易形成不同的命名风格,很难统一。样式变多后,命名将更加混乱。

  3. 依赖管理不彻底
    组件应该相互独立,引入一个组件时,应该只引入它所需要的 CSS 样式。但现在的做法是除了要引入 JS,还要再引入它的 CSS,而且 Saas/Less 很难实现对每个组件都编译出单独的 CSS,引入所有模块的 CSS 又造成浪费。JS 的模块化已经非常成熟,如果能让 JS 来管理 CSS 依赖是很好的解决办法。Webpack 的 css-loader 提供了这种能力。

  4. 无法共享变量 复杂组件要使用 JS 和 CSS 来共同处理样式,就会造成有些变量在 JS 和 CSS 中冗余,Sass/PostCSS/CSS 等都不提供跨 JS 和 CSS 共享变量这种能力。

  5. 代码压缩不彻底 由于移动端网络的不确定性,现在对 CSS 压缩已经到了变态的程度。很多压缩工具为了节省一个字节会把 '16px' 转成 '1pc'。但对非常长的 class 名却无能为力,力没有用到刀刃上。

上面的问题如果只凭 CSS 自身是无法解决的,如果是通过 JS 来管理 CSS 就很好解决,因此 Vjuex 给出的解决方案是完全的 CSS in JS,但这相当于完全抛弃 CSS,在 JS 中以 Object 语法来写 CSS,估计刚看到的小伙伴都受惊了。直到出现了 CSS Modules。

CSS Modules 模块化方案

CSS Modules 内部通过 ICSS 来解决样式导入和导出这两个问题。分别对应 :import 和 :export 两个新增的伪类。

:import("path/to/dep.css") {
  localAlias: keyFromDep;
  /* ... */
}
:export {
  exportedKey: exportedValue;
  /* ... */
}

但直接使用这两个关键字编程太麻烦,实际项目中很少会直接使用它们,我们需要的是用 JS 来管理 CSS 的能力。结合 Webpack 的 css-loader 后,就可以在 CSS 中定义样式,在 JS 中导入。

启用 CSS Modules

// webpack.config.js
css?modules&localIdentName=[name]__[local]-[hash:base64:5]

加上 modules 即为启用,localIdentName 是设置生成样式的命名规则。

/* components/Button.css */
.normal { /* normal 相关的所有样式 */ }
.disabled { /* disabled 相关的所有样式 */ }
/* components/Button.js */
import styles from './Button.css';

console.log(styles);

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

生成的 HTML 是

<button class="button--normal-abc53"> Processing... </button>

注意到 button--normal-abc5436 是 CSS Modules 按照 localIdentName 自动生成的 class 名。其中的 abc5436 是按照给定算法生成的序列码。经过这样混淆处理后,class 名基本就是唯一的,大大降低了项目中样式覆盖的几率。同时在生产环境下修改规则,生成更短的 class 名,可以提高 CSS 的压缩率。

上例中 console 打印的结果是:

Object {
  normal: 'button--normal-abc546',
  disabled: 'button--disabled-def884',
}

CSS Modules 对 CSS 中的 class 名都做了处理,使用对象来保存原 class 和混淆后 class 的对应关系。

通过这些简单的处理,CSS Modules 实现了以下几点:

  • 所有样式都是 local 的,解决了命名冲突和全局污染问题

  • class 名生成规则配置灵活,可以此来压缩 class 名

  • 只需引用组件的 JS 就能搞定组件所有的 JS 和 CSS

  • 依然是 CSS,几乎 0 学习成本

样式默认局部

使用了 CSS Modules 后,就相当于给每个 class 名外加加了一个 :local,以此来实现样式的局部化,如果你想切换到全局模式,使用对应的 :global

.normal {
  color: green;
}

/* 以上与下面等价 */
:local(.normal) {
  color: green;
}

/* 定义全局样式 */
:global(.btn) {
  color: red;
}

/* 定义多个全局样式 */
:global {
  .link {
    color: green;
  }
  .box {
    color: yellow;
  }
}

Compose 来组合样式

对于样式复用,CSS Modules 只提供了唯一的方式来处理:composes 组合

/* components/Button.css */
.base { /* 所有通用的样式 */ }

.normal {
  composes: base;
  /* normal 其它样式 */
}

.disabled {
  composes: base;
  /* disabled 其它样式 */
}
import styles from './Button.css';

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

生成的 HTML 变为

<button class="button--base-abc53 button--normal-abc53"> Processing... </button>

由于在 .normal 中 composes 了 .base,编译后会 normal 会变成两个 class。

composes 还可以组合外部文件中的样式。

/* settings.css */
.primary-color {
  color: #f40;
}

/* components/Button.css */
.base { /* 所有通用的样式 */ }

.primary {
  composes: base;
  composes: $primary-color from './settings.css';
  /* primary 其它样式 */
}

对于大多数项目,有了 composes 后已经不再需要 Sass/Less/PostCSS。但如果你想用的话,由于 composes 不是标准的 CSS 语法,编译时会报错。就只能使用预处理器自己的语法来做样式复用了。

class 命名技巧

CSS Modules 的命名规范是从 BEM 扩展而来。BEM 把样式名分为 3 个级别,分别是:

  • Block:对应模块名,如 Dialog

  • Element:对应模块中的节点名 Confirm Button

  • Modifier:对应节点相关的状态,如 disabled、highlight

综上,BEM 最终得到的 class 名为 dialog__confirm-button--highlight。使用双符号 __ 和 -- 是为了和区块内单词间的分隔符区分开来。虽然看起来有点奇怪,但 BEM 被非常多的大型项目和团队采用。我们实践下来也很认可这种命名方法。

CSS Modules 中 CSS 文件名恰好对应 Block 名,只需要再考虑 Element 和 Modifier。BEM 对应到 CSS Modules 的做法是:

/* .dialog.css */
.ConfirmButton--disabled {
}

你也可以不遵循完整的命名规范,使用 camelCase 的写法把 Block 和 Modifier 放到一起:

/* .dialog.css */
.disabledConfirmButton {
}

如何实现CSS,JS变量共享

上面提到的 :export 关键字可以把 CSS 中的 变量输出到 JS 中。下面演示如何在 JS 中读取 Sass 变量:

/* config.scss */
$primary-color: #f40;

:export {
  primaryColor: $primary-color;
}
/* app.js */
import style from 'config.scss';

// 会输出 #F40
console.log(style.primaryColor);

CSS Modules 使用技巧

CSS Modules 是对现有的 CSS 做减法。为了追求简单可控,作者建议遵循如下原则:

  • 不使用选择器,只使用 class 名来定义样式

  • 不层叠多个 class,只使用一个 class 把所有样式定义好

  • 所有样式通过 composes 组合来实现复用

  • 不嵌套

上面两条原则相当于削弱了样式中最灵活的部分,初使用者很难接受。第一条实践起来难度不大,但第二条如果模块状态过多时,class 数量将成倍上升。

一定要知道,上面之所以称为建议,是因为 CSS Modules 并不强制你一定要这么做。听起来有些矛盾,由于多数 CSS 项目存在深厚的历史遗留问题,过多的限制就意味着增加迁移成本和与外部合作的成本。初期使用中肯定需要一些折衷。幸运的是,CSS Modules 这点做的很好:

如果我对一个元素使用多个 class 呢?

没问题,样式照样生效。

如何我在一个 style 文件中使用同名 class 呢?

没问题,这些同名 class 编译后虽然可能是随机码,但仍是同名的。

如果我在 style 文件中使用了 id 选择器,伪类,标签选择器等呢?
没问题,所有这些选择器将不被转换,原封不动的出现在编译后的 css 中。也就是说 CSS Modules 只会转换 class 名相关样式。

但注意,上面 3 个“如果”尽量不要发生。

CSS Modules 结合 React 实践

在 className 处直接使用 css 中 class 名即可。

/* dialog.css */
.root {}
.confirm {}
.disabledConfirm {}
import classNames from 'classnames';
import styles from './dialog.css';

export default class Dialog extends React.Component {
  render() {
    const cx = classNames({
      [styles.confirm]: !this.state.disabled,
      [styles.disabledConfirm]: this.state.disabled
    });

    return <div className={styles.root}>
      <a className={cx}>Confirm</a>
      ...
    </div>
  }
}

注意,一般把组件最外层节点对应的 class 名称为 root。这里使用了 classnames 库来操作 class 名。
如果你不想频繁的输入 styles.**,可以试一下 react-css-modules,它通过高阶函数的形式来避免重复输入 styles.**

CSS Modules 结合历史遗留项目实践

好的技术方案除了功能强大炫酷,还要能做到现有项目能平滑迁移。CSS Modules 在这一点上表现的非常灵活。

外部如何覆盖局部样式

当生成混淆的 class 名后,可以解决命名冲突,但因为无法预知最终 class 名,不能通过一般选择器覆盖。我们现在项目中的实践是可以给组件关键节点加上 data-role 属性,然后通过属性选择器来覆盖样式。

// dialog.js
  return <div className={styles.root} data-role='dialog-root'>
      <a className={styles.disabledConfirm} data-role='dialog-confirm-btn'>Confirm</a>
      ...
  </div>
// dialog.css
[data-role="dialog-root"] {
  // override style
}

因为 CSS Modules 只会转变类选择器,所以这里的属性选择器不需要添加 :global

如何与全局样式共存

前端项目不可避免会引入 normalize.css 或其它一类全局 css 文件。使用 Webpack 可以让全局样式和 CSS Modules 的局部样式和谐共存。下面是我们项目中使用的 webpack 部分配置代码:

module: {
  loaders: [{
    test: /\.jsx?$/,
    loader: 'babel'
  }, {
    test: /\.scss$/,
    exclude: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true'
  }, {
    test: /\.scss$/,
    include: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css!sass?sourceMap=true'
  }]
}
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'

/* src/views/Component.js */
// 以下为组件相关样式
import './Component.scss';

目录结构如下:

src
├── app.js
├── styles
│   ├── app.scss
│   └── normalize.scss
└── views
    ├── Component.js
    └── Component.scss

这样所有全局的样式都放到 src/styles/app.scss 中引入就可以了。其它所有目录包括 src/views 中的样式都是局部的。

总结

CSS Modules 很好的解决了 CSS 目前面临的模块化难题。支持与 Sass/Less/PostCSS 等搭配使用,能充分利用现有技术积累。同时也能和全局样式灵活搭配,便于项目中逐步迁移至 CSS Modules。CSS Modules 的实现也属轻量级,未来有标准解决方案后可以低成本迁移。如果你的产品中正好遇到类似问题,非常值得一试。

本文参照了:http://www.cnblogs.com/WebShare-hilda/p/4686067.html

https://segmentfault.com/a/1190000004300065

css模块化及CSS Modules使用详解的更多相关文章

  1. CSS进阶内容—浮动和定位详解

    CSS进阶内容-浮动和定位详解 我们在学习了CSS的基本知识和盒子之后,就该了解一下网页的整体构成了 当然如果没有学习之前的知识,可以到我的主页中查看之前的文章:秋落雨微凉 - 博客园 CSS的三种布 ...

  2. 《网页设计基础——CSS的四种引入方式详解》

    网页设计基础--CSS的四种引入方式详解     一.行内式:   规则: 1. 行内式是所有样式方法中最为直接的一种,它直接对HTML的标记使用style属性,然后将CSS代码直接写在其中.   格 ...

  3. ES6 模块化(Module)export和import详解 export default

    ES6 模块化(Module)export和import详解 - CSDN博客 https://blog.csdn.net/pcaxb/article/details/53670097 微信小程序笔记 ...

  4. CSS魔法堂:Position定位详解

    一.Position各属性值详解   1.  static :默认值,元素将按照正常文档流规则排列.   2.  relative :相对定位,元素仍然处于正常文档流当中,但可以通过left.top. ...

  5. css控制UL LI 的样式详解

    用<ul>设置导航 <style> #menu ul {list-style:none;margin:0px;} #menu ul li {float:left;} </ ...

  6. css控制UL LI 的样式详解(推荐)

    代码如下: <div id="menu"> <ul> <li><a href="#">首页</a>& ...

  7. CSS学习笔记(9)--详解CSS中:nth-child的用法

    详解CSS中:nth-child的用法 前端的哥们想必都接触过css中一个神奇的玩意,可以轻松选取你想要的标签并给与修改添加样式,是不是很给力,它就是“:nth-child”. 下面我将用几个典型的实 ...

  8. CSS系列(8) CSS后代选择器和子选择器详解

    一.CSS后代选择器详解 1,  生动介绍基本概念 一个标签嵌B在另一个标签A内部,B就是A的后代. 而且,B的后代也是A的后代,这就叫“子子孙孙无穷尽也”. 比如: <div> < ...

  9. html/css基础篇——link和@inport详解以及脚本执行顺序探讨

    先说一说两者之间的异同 两者都可以引用外部CSS的方式,现在主流浏览器两者都支持(ps:@import是CSS2.1提出的),但是存在一定的区别: 1.link是XHTML标签,除了加载CSS外,还可 ...

随机推荐

  1. ●Splay的一些题

    ●个人感觉: 代码长: 函数多: (很套路): (很强的Splay,无愧于“区间王”) ●NOI2005维修数列 一个可以当模板学习的题,包含了众多操作(函数): 区间插入,删除,更新,翻转,询问信息 ...

  2. hdu 5493 (树状数组)

    题意:在一个队列中,你知道一个人在他左边或者右边比他高的人的个数,求字典序最小的答案 思路:先将人按  矮-->高 排序,然后算出在每个人前面需要预留的位置.树状数组(也可以线段树)解决时,先二 ...

  3. bzoj3825 NOI2017 游戏

    题目背景 狂野飙车是小 L 最喜欢的游戏.与其他业余玩家不同的是,小 L 在玩游戏之余,还精于研究游戏的设计,因此他有着与众不同的游戏策略. 题目描述 小 L 计划进行nn 场游戏,每场游戏使用一张地 ...

  4. 例10-4 uva10791(唯一分解)

    题意:求最小公倍数为n的数的和的最小值. 如12:(3,4),(2,6),(1,12)最小为7 要想a1,a2,a3……an的和最小,要保证他们两两互质,只要存在不互质的两个数,就一定可以近一步优化 ...

  5. Codeforces Round #407 (Div. 2)

    来自FallDream的博客,未经允许,请勿转载,谢谢. ------------------------------------------------------ A.Anastasia and ...

  6. 关于HttpClient重试策略的研究

    一.背景 由于工作上的业务本人经常与第三方系统交互,所以经常会使用HttpClient与第三方进行通信.对于交易类的接口,订单状态是至关重要的. 这就牵扯到一系列问题: HttpClient是否有默认 ...

  7. JSP 基本语法

    1 JSP 的由来 servlet产生后,存在很大的问题,为了表现页面的效果,需要输出大量的HTML 语句,表现为一个个字符串,不仅利于开发,也不利于后期的维护,由此产生了JSP.主要用于将Servl ...

  8. 从零安装Scrapy心得 | Install Python Scrapy from scratch

    1. 介绍 Scrapy,是基于python的网络爬虫框架,它能从网络上爬下来信息,是data获取的一个好方式.于是想安装下看看. 进到它的官网,安装的介绍页面 https://docs.scrapy ...

  9. C语言程序设计第二次作业——

    1,编译过程过程中的错误缺引号和分号并且拼写错误. 正确结果: 2,编译过程 改正错误: 正确结果: 3,利用SIZEOF运算符求出的数据类型所占字节大小: 4,在头文件LIMITS.H中相关的编译 ...

  10. Java 中 json字符串转换为类

    使用到alibaba.fastjson包 具体实现 JSONObject jsonObject = JSONObject.parseObject(msg); SmsSenderStatus smsSe ...