ホームページ >ウェブフロントエンド >htmlチュートリアル >CSS モジュールの詳細な説明と React_html/css_WEB-ITnose での実践
CSS は、フロントエンド分野の中で最も進化が遅い部分です。 ES2015/2016 の急速な人気と、Babel/Webpack などのツールの急速な開発により、CSS は大きく取り残され、大規模プロジェクトのエンジニアリングにおいて徐々に問題点になってきました。これは、フロントエンドが完全なモジュール化に進む前に解決しなければならない難しい問題でもあります。
CSS のモジュール化には多くのソリューションがありますが、主に 2 つのカテゴリがあります。 1 つは、CSS を完全に放棄し、JS または JSON を使用してスタイルを記述することです。 Radium、jsxstyle、react-style がこのカテゴリに分類されます。利点は、CSS に JS と同じ強力なモジュール化機能を提供できることですが、欠点は、成熟した CSS プリプロセッサ (またはポストプロセッサ) Sass/Less/PostCSS を使用できないことと、:hover および :active 疑似クラスが複雑であることです。ハンドル。もう 1 つのタイプは引き続き CSS を使用しますが、スタイルの依存関係を管理するために JS を使用し、CSS モジュールで表されます。 CSS モジュールは、既存の CSS エコシステムと JS モジュール機能の統合を最大限に活用でき、API は非常にシンプルなので学習コストがほとんどかかりません。公開時に個別の JS と CSS がコンパイルされます。 Reactに依存せず、Webpackを使えばVue/Angular/jQueryでも使えます。これが現時点で最良の CSS モジュール ソリューションだと思います。最近プロジェクトでよく使われていますが、その詳細と実際のアイデアを以下に共有します。
CSS のモジュール化は、CSS スタイルのインポートとエクスポートという 2 つの問題を解決するために重要です。エクスポート時にコードを再利用するには、オンデマンドで柔軟にインポートします。グローバルな汚染を避けるために、内部スコープを非表示にする必要があります。 Sass/Less/PostCSS などは、CSS プログラミング機能が弱いという問題を次々に解決しようとしてきましたが、その結果、非常にうまくいきましたが、モジュール性という最も重要な問題は解決できませんでした。 Facebook エンジニアの Vjeux は、React 開発中に遭遇した CSS 関連の一連の問題を最初に指摘しました。私の個人的な見解と合わせてまとめると次のとおりです。
グローバル汚染
CSS はスタイルの設定にグローバル セレクター機構を使用するという利点があります。欠点は、すべてのスタイルがグローバルに有効であり、スタイルがエラーによって上書きされる可能性があることです。その結果、非常に見苦しい !重要な、さらにはインライン !重要な複雑なセレクター重みカウント テーブルが作成され、エラーの可能性と使用コストが増加します。 Web コンポーネント標準の 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 圧縮が異常なレベルに達しています。多くの圧縮ツールは、1 バイトを節約するために「16px」を「1pc」に変換します。ただし、非常に長いクラス名については何もできず、エッジではパワーが使用されません。
上記の問題は CSS だけでは解決できません。CSS を JS で管理すれば簡単に解決できます。 したがって、Vjuex が提供する解決策は完全に JS の CSS ですが、これは と同等です。 CSS を完全に放棄し、Object 構文を使用して JS で CSS を記述したのを見た友人はショックを受けたと思います。 CSS モジュールが登場するまでは。
CSS Modules は、ICSS を介したスタイルのインポートとエクスポートの 2 つの問題を内部的に解決します。 2 つの新しい疑似クラス (それぞれ import と :export) に対応します。
:import("path/to/dep.css") { localAlias: keyFromDep; /* ... */}:export { exportedKey: exportedValue; /* ... */}
しかし、これら 2 つのキーワードを直接使用してプログラムするのは非常に面倒です。必要なのは、JS を使用して CSS を管理する機能です。 Webpack の css-loader と組み合わせると、CSS でスタイルを定義し、JS にインポートできます。
// webpack.config.jscss?modules&localIdentName=[name]__[local]-[hash:base64:5]
モジュールを追加して有効にします。 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 は、localIdentName に従って CSS モジュールによって自動的に生成されるクラス名であることに注意してください。ここで、abc5436 は、指定されたアルゴリズムに従って生成されたシーケンス コードです。このような難読化の後、クラス名は基本的に一意になるため、プロジェクト内でスタイルが適用される可能性が大幅に減少します。同時に、運用環境でルールを変更して短いクラス名を生成すると、CSS の圧縮率を向上させることができます。
上記の例では、コンソールによって出力される結果は次のとおりです:
Object { normal: 'button--normal-abc546', disabled: 'button--disabled-def884',}
CSS モジュールは CSS 内のクラス名を処理し、オブジェクトを使用して元のクラスと難読化されたクラス間の対応を保存します。クラス。 。
通过这些简单的处理,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; }}
对于样式复用,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 语法,编译时会报错。就只能使用预处理器自己的语法来做样式复用了。
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 {}
上面提到的 :export 关键字可以把 CSS 中的 变量输出到 JS 中。下面演示如何在 JS 中读取 Sass 变量:
/* config.scss */$primary-color: #f40;:export { primaryColor: $primary-color;}
/* app.js */import style from 'config.scss';// 会输出 #F40console.log(style.primaryColor);
CSS Modules 是对现有的 CSS 做减法。为了追求简单可控,作者建议遵循如下原则:
不使用选择器,只使用 class 名来定义样式
不层叠多个 class,只使用一个 class 把所有样式定义好
所有样式通过 composes 组合来实现复用
不嵌套
上面两条原则相当于削弱了样式中最灵活的部分,初使用者很难接受。第一条实践起来难度不大,但第二条如果模块状态过多时,class 数量将成倍上升。
一定要知道,上面之所以称为建议,是因为 CSS Modules 并不强制你一定要这么做。听起来有些矛盾,由于多数 CSS 项目存在深厚的历史遗留问题,过多的限制就意味着增加迁移成本和与外部合作的成本。初期使用中肯定需要一些折衷。幸运的是,CSS Modules 这点做的很好:
如果我对一个元素使用多个 class 呢?
没问题,样式照样生效。
如何我在一个 style 文件中使用同名 class 呢?
没问题,这些同名 class 编译后虽然可能是随机码,但仍是同名的。
如果我在 style 文件中使用了 id 选择器,伪类,标签选择器等呢?
没问题,所有这些选择器将不被转换,原封不动的出现在编译后的 css 中。也就是说 CSS Modules 只会转换 class 名相关样式。
但注意,上面 3 个“如果”尽量不要发生。
在 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({ confirm: !this.state.disabled, disabledConfirm: this.state.disabled }); return <div className={styles.root}> <a className={styles.disabledConfirm}>Confirm</a> ... </div> }}
注意,一般把组件最外层节点对应的 class 名称为 root。这里使用了 classnames 库来操作 class 名。
如果你不想频繁的输入 styles.**,可以试一下 react-css-modules,它通过高阶函数的形式来避免重复输入 styles.**。
好的技术方案除了功能强大炫酷,还要能做到现有项目能平滑迁移。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 的实现也属轻量级,未来有标准解决方案后可以低成本迁移。如果你的产品中正好遇到类似问题,非常值得一试。