CSS 是前端领域中进化最慢的一块。因为 ES2015/2016 的快速普及和 Babel/Webpack 等工具的迅猛发展,CSS 被远远甩在了后面,逐渐成为大型项目工程化的痛点。也变成了前端走向完全模块化前必须解决的难题。
模块化解决了JS做用域的问题,可是CSS仍是会存在样式覆盖的问题,由于最后打包最终会生成一份文件而不是开发时候的那样分模块分做用域。
因此,咱们今天讨论的是如何在模块化的工程下放心的写CSS样式而不担忧样式覆盖,推荐的方案就是CSS Module。javascript
CSS 模块化重要的是要解决好两个问题:CSS 样式的导入和导出。css
Sass/Less/PostCSS 等前仆后继试图解决 CSS 编程能力弱的问题,结果它们作的也确实优秀,但这并无解决模块化最重要的问题。Facebook 工程师 Vjeux 首先抛出了 React 开发中遇到的一系列 CSS 相关问题。总结以下:前端
CSS 使用全局选择器机制来设置样式,优势是方便重写样式。缺点是全部的样式都是全局生效,样式可能被错误覆盖,所以产生了很是丑陋的 !important,甚至 inline !important 和复杂的选择器权重计数表,提升犯错几率和使用成本。
Web Components 标准中的 Shadow DOM 能完全解决这个问题,但它的作法有点极端,样式完全局部化,形成外部没法重写样式,损失了灵活性。
因为全局污染的问题,多人协同开发时为了不样式冲突,选择器愈来愈复杂,容易造成不一样的命名风格,很难统一。样式变多后,命名将更加混乱。
组件应该相互独立,引入一个组件时,应该只引入它所须要的 CSS 样式。但如今的作法是除了要引入 JS,还要再引入它的 CSS,并且 Saas/Less 很难实现对每一个组件都编译出单独的 CSS,引入全部模块的 CSS 又形成浪费。JS 的模块化已经很是成熟,若是能让 JS 来管理 CSS 依赖是很好的解决办法。
Webpack 的 css-loader 提供了这种能力。
复杂组件要使用 JS 和 CSS 来共同处理样式,就会形成有些变量在 JS 和 CSS 中冗余,Sass/PostCSS/CSS 等都不提供跨 JS 和 CSS 共享变量这种能力。
因为移动端网络的不肯定性,如今对 CSS 压缩已经到了变态的程度。不少压缩工具为了节省一个字节会把 '16px' 转成 '1pc'。但对很是长的 class 名却无能为力,力没有用到刀刃上。
上面的问题若是只凭 CSS 自身是没法解决的,若是是经过 JS 来管理 CSS 就很好解决,所以 Vjuex 给出的解决方案是彻底的 CSS in JS,但这至关于彻底抛弃 CSS,在 JS 中以 Object 语法来写 CSS,估计刚看到的小伙伴都受惊了。直到出现了 CSS Modules。java
CSS 模块化的解决方案有不少,但主要有两类:react
Radium,jsxstyle,react-style 属于这一类。
优势是能给 CSS 提供 JS 一样强大的模块化能力。
缺点是不能利用成熟的 CSS 预处理器(或后处理器),Sass/Less/PostCSS,:hover 和 :active 伪类处理起来复杂。
表明是 CSS Modules。
CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。
发布时依旧编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,能够在 Vue/Angular/jQuery 中使用。
CSS Modules 内部经过 ICSS 来解决样式导入和导出这两个问题。分别对应 :import 和 :export 两个新增的伪类。webpack
:import("path/to/dep.css") { localAlias: keyFromDep; /* ... */ } :export { exportedKey: exportedValue; /* ... */ }
但直接使用这两个关键字编程太麻烦,实际项目中不多会直接使用它们,咱们须要的是用 JS 来管理 CSS 的能力。结合 Webpack 的 css-loader 后,就能够在 CSS 中定义样式,在 JS 中导入。git
启用 CSS Moduleses6
// webpack.config.js css?modules&localIdentName=[name]__[local]-[hash:base64:5]
加上 modules 即为启用,localIdentName 是设置生成样式的命名规则。
也能够这样设置:github
test: /\.less$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]', }, }, ], },
样式文件Button.css:web
.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">Submit</button>
注意到 button--normal-abc53 是 CSS Modules 按照 localIdentName 自动生成的 class 名。其中的 abc53 是按照给定算法生成的序列码。通过这样混淆处理后,class 名基本就是惟一的,大大下降了项目中样式覆盖的概率。同时在生产环境下修改规则,生成更短的class名,能够提升CSS的压缩率。
上例中 console 打印的结果是:
Object { normal: 'button--normal-abc53', disabled: 'button--disabled-def884', }
CSS Modules 对 CSS 中的 class 名都作了处理,使用对象来保存原 class 和混淆后 class 的对应关系。
经过这些简单的处理,CSS Modules 实现了如下几点:
样式默认局部
使用了 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-daf62 button--normal-abc53">Submit</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 个级别,分别是:
综上,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 { }
模块化命名实践:MBC 【仅供参考参考】
M:module 模块(组件)名
B:block 元素块的功能说明
C:custom 自定义内容
如何实现CSS,JS变量共享
注:CSS Modules 中没有变量的概念,这里的 CSS 变量指的是 Sass 中的变量。
上面提到的 :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 数量将成倍上升。
必定要知道,上面之因此称为建议,是由于 CSS Modules 并不强制你必定要这么作。听起来有些矛盾,因为多数 CSS 项目存在深厚的历史遗留问题,过多的限制就意味着增长迁移成本和与外部合做的成本。初期使用中确定须要一些折衷。幸运的是,CSS Modules 这点作的很好:
若是我对一个元素使用多个 class 呢?
没问题,样式照样生效。
如何我在一个 style 文件中使用同名 class 呢?
没问题,这些同名 class 编译后虽然多是随机码,但还是同名的。
若是我在 style 文件中使用伪类,标签选择器等呢?
没问题,全部这些选择器将不被转换,原封不动的出如今编译后的 css 中。也就是说 CSS Modules 只会转换 class 名和 id 选择器名相关的样式。
但注意,上面 3 个“若是”尽可能不要发生。
CSS Modules 结合 React 实践
首先,在 CSS loader中开启CSS Module:
{ loader: 'css-loader', options: { modules: true, localIdentName: '[local]', }, },
在 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.xx,能够试一下 react-css-modules,它经过高阶函数的形式来避免重复输入 styles.xx。
注意⚠️
React 中使用CSS Module,能够设置
1.多个class的状况
// 可使用字符串拼接 className = {style.oneclass+' '+style.twoclass} //可使用es6的字符串模板 className = {`${style['calculator']} ${style['calculator']}`}
2.若是class使用的是连字符可使用数组方式style['box-text']
3.一个class是父组件传下来的
若是一个class是父组件传下来的,在父组件已经使用style转变过了,在子组件中就不须要再style转变一次,例子以下:
//父组件render中 <CalculatorKey className={style['key-0']} onPress={() => this.inputDigit(0)}>0</CalculatorKey> //CalculatorKey 组件render中 const { onPress, className, ...props } = this.props; return ( <PointTarget onPoint={onPress}> <button className={`${style['calculator-key']} ${className}`} {...props}/> </PointTarget> )
子组件CalculatorKey接收到的className已经在父组件中style转变过了
4.显示undefined
若是一个class你没有在样式文件中定义,那么最后显示的就是undefined,而不是一个style事后的没有任何样式的class, 这点很奇怪。
/* HTML */ <template> <h1 @click="clickHandler">{{ msg }}</h1> </template> /* script */ <script> module.exports = { data: function() { return { msg: 'Hello, world!' } }, methods:{ clickHandler(){ alert('hi'); } } } </script> /* scoped CSS */ <style scoped> h1 { color: red; font-size: 46px; } </style>
在Vue 元件档,透过上面这样的方式提供了一个template (HTML)、script 以及带有scoped 的style 样式,也仍然能够保有过去HTML、CSS 与JS 分离的开发体验。但本质上还是all-in-JS 的变种语法糖。
值得一提的是,当style标签加上scoped属性,那么在Vue元件档通过编译后,会自动在元件上打上一个随机的属性 data-v-hash
,再透过CSS Attribute Selector的特性来作到CSS scope的切分,使得即使是在不一样元件档里的h1也能有CSS样式不互相干扰的效果。固然开发起来,比起JSX、或是inline-style等的方式,这种折衷的做法更漂亮。
几点思考:Stop using CSS in JavaScript for web development
参考连接:
1.https://github.com/camsong/bl...
2.https://github.com/ckinmind/R...
3.从Vue 来看CSS 管理方案的发展