CSS Modules 入门及 React 中实践

lvchongyi 8年前
   <h2>写在前面</h2>    <p>读文先看此图,能先有个大体概念:</p>    <p><img src="https://simg.open-open.com/show/126ff4cc8523965c3fa9c054384574d7.png"></p>    <h2>CSS Modules介绍</h2>    <p>CSS Modules是什么东西呢?首先,让我们从官方文档入手:</p>    <p><a href="/misc/goto?guid=4958965462431997440" rel="nofollow,noindex">GitHub – css-modules/css-modules: Documentation about css-modules</a></p>    <p>CSS模块是一个CSS文件,其中所有类名和动画名称在本地默认范围. CSS模块就是所有的类名都只有局部作用域的CSS文件。</p>    <p>所以CSS Modules既不是官方标准,也不是浏览器的特性,而是在构建步骤(例如使用Webpack或Browserify)中对CSS类名选择器限定作用域的一种方式(通过hash实现类似于命名空间的方法)。</p>    <p>It doesn’t really matter in the end (although shorter class names mean shorter stylesheets) because the point is that they are dynamically generated, unique, and mapped to the correct styles.在使用CSS模块时,类名是动态生成的,唯一的,并准确对应到源文件中的各个类的样式。</p>    <p>这也是实现样式作用域的原理。它们被限定在特定的模板里。例如我们在buttons.js里引入buttons.css文件,并使用.btn的样式,在其他组件里是不会被.btn影响的,除非它也引入了buttons.css.</p>    <p>可我们是出于什么目的把CSS和HTML文件搞得这么零碎呢?我们为什么要使用CSS模块呢?</p>    <h2>为什么我们需要CSS模块化</h2>    <h3>CSS全局作用域问题</h3>    <p>CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。相信写css的人都会遇到样式冲突(污染)的问题。</p>    <p>于是一般这么做(笔者都做过):</p>    <p>* class命名写长一点吧,降低冲突的几率</p>    <p>* 加个父元素的选择器,限制范围</p>    <p>* 重新命名个class吧,比较保险</p>    <p>所以亟待解决的问题就是css局部作用域避免全局样式冲突(污染)的问题</p>    <h3>JS CSS无法共享变量</h3>    <p>复杂组件要使用 JS 和 CSS 来共同处理样式,就会造成有些变量在 JS 和 CSS 中冗余,CSS预处理器/后处理器 等都不提供跨 JS 和 CSS 共享变量这种能力。</p>    <h3>健壮并且扩展方便的CSS</h3>    <p>作为有追求的工程师,编写健壮并且扩展方便的CSS一直是我们的目标。那么如何定义健壮并且扩展方便?有三个要点:</p>    <ul>     <li>面向组件 – 处理 UI 复杂性的最佳实践就是将 UI 分割成一个个的小组件 Locality_of_reference 。如果你正在使用一个合理的框架,JavaScript 方面就将原生支持(组件化)。举个例子,React 就鼓励高度组件化和分割。我们希望有一个 CSS 架构去匹配。</li>     <li>沙箱化(Sandboxed) – 如果一个组件的样式会对其他组件产生不必要以及意想不到的影响,那么将 UI 分割成组件并没有什么用。就这方面而言,CSS的全局作用域会给你造成负担。</li>     <li>方便 – 我们想要所有好的东西,并且不想产生更多的工作。也就是说,我们不想因为采用这个架构而让我们的开发者体验变得更糟。可能的话,我们想开发者体验变得更好。</li>    </ul>    <h2>CSS模块化方案分类</h2>    <p>CSS 模块化的解决方案有很多,但主要有三类。</p>    <h3>CSS 命名约定</h3>    <p>规范化CSS的模块化解决方案(比如BEM BEM — Block Element Modifier ,OOCSS,AMCSS,SMACSS,SUITCSS)</p>    <p>但存在以下问题:</p>    <p>* JS CSS之间依然没有打通变量和选择器等</p>    <p>* 复杂的命名</p>    <h3>CSS in JS</h3>    <p>彻底抛弃 CSS,用 JavaScript 写 CSS 规则,并内联样式。 React: CSS in JS // Speaker Deck 。Radium,react-style 属于这一类。但存在以下问题:</p>    <p>* 无法使用伪类,媒体查询等</p>    <p>* 样式代码也会出现大量重复。</p>    <p>* 不能利用成熟的 CSS 预处理器(或后处理器)</p>    <p>使用JS 来管理样式模块</p>    <p>使用JS编译原生的CSS文件,使其具备模块化的能力,代表是 CSS Modules <a href="/misc/goto?guid=4958965462431997440" rel="nofollow,noindex">GitHub – css-modules/css-modules: Documentation about css-modules</a> 。</p>    <p>CSS Modules 能最大化地结合现有 CSS 生态(预处理器/后处理器等)和 JS 模块化能力,几乎零学习成本。只要你使用 Webpack,可以在任何项目中使用。是笔者认为目前最好的 CSS 模块化解决方案。</p>    <h2>CSS Modules 使用教程</h2>    <h3>启用 CSS Modules</h3>    <pre>  <code class="language-css">// webpack.config.js  css?modules&localIdentName=[name]__[local]-[hash:base64:5]     </code></pre>    <p>加上 modules 即为启用, localIdentName 是设置生成样式的命名规则。</p>    <pre>  <code class="language-css">/* components/Button.css */  .normal { /* normal 相关的所有样式 */ }     </code></pre>    <pre>  <code class="language-css">// components/Button.js  importstylesfrom './Button.css';  console.log(styles);  buttonElem.outerHTML = `<buttonclass=${styles.normal}>Submit</button>`     </code></pre>    <p>生成的 HTML 是</p>    <pre>  <code class="language-css"><buttonclass="button--normal-abc53">Submit</button>     </code></pre>    <p>注意到 button--normal-abc53 是 CSS Modules 按照 localIdentName 自动生成的 class 名。其中的 abc53 是按照给定算法生成的序列码。经过这样混淆处理后,class 名基本就是唯一的,大大降低了项目中样式覆盖的几率。同时在生产环境下修改规则,生成更短的 class 名,可以提高 CSS 的压缩率。</p>    <p>上例中 console 打印的结果是:</p>    <pre>  <code class="language-css">Object {    normal: 'button--normal-abc53',    disabled: 'button--disabled-def886',  }     </code></pre>    <p>CSS Modules 对 CSS 中的 class 名都做了处理,使用对象来保存原 class 和混淆后 class 的对应关系。</p>    <p>通过这些简单的处理,CSS Modules 实现了以下几点:</p>    <p>* 所有样式都是局部作用域 的,解决了全局污染问题</p>    <p>* class 名生成规则配置灵活,可以此来压缩 class 名</p>    <p>* 只需引用组件的 JS 就能搞定组件所有的 JS 和 CSS</p>    <p>* 依然是 CSS,几乎 0 学习成本</p>    <h2>CSS Modules 在React中的实践</h2>    <p>那么我们在React中怎么使用?</p>    <h3>手动引用解决</h3>    <p>在 className 处直接使用 css 中 class 名即可。</p>    <pre>  <code class="language-css">importReactfrom 'react';  importstylesfrom './table.css';     exportdefault class Tableextends React.Component {      render () {          return <divclassName={styles.table}>              <divclassName={styles.row}>              </div>          </div>;      }  }     </code></pre>    <p>渲染出来的组件出来</p>    <pre>  <code class="language-css"><divclass="table__table___32osj">      <divclass="table__row___2w27N">      </div>  </div>     </code></pre>    <h3>react-css-modules</h3>    <p>如果你不想频繁的输入 styles.** ,有一个 <a href="/misc/goto?guid=4959719542567804731" rel="nofollow,noindex">GitHub – gajus/react-css-modules: Seamless mapping of class names to CSS modules inside of React components.</a> ,它通过高阶函数的形式来生成 className ,不过不推荐使用,后文会提到。</p>    <p>API也很简单,给组件外包一个CSSModules即可。</p>    <pre>  <code class="language-css">importReactfrom 'react';  importCSSModulesfrom 'react-css-modules';  importstylesfrom './table.css';     class Tableextends React.Component {      render () {          return <divstyleName='table'>          </div>;      }  }     exportdefault CSSModules(Table, styles);     </code></pre>    <p>不过这样我们可以看到,它是需要运行时的依赖,而且需要在运行时才获取className,性能损耗大,那么有没有方便又接近无损的方法呢?答案是有的,使用babel插件 babel-plugin-react-css-modules <a href="/misc/goto?guid=4959741832799438326" rel="nofollow,noindex">GitHub – gajus/babel-plugin-react-css-modules: Transforms styleName to className using compile time CSS module resolution.</a> 把 className 获取前置到编译阶段。</p>    <h3>babel-plugin-react-css-modules</h3>    <p>babel-plugin-react-css-modules 可以实现使用 styleName 属性自动加载CSS模块。我们通过该babel插件来进行语法树解析并最终生成 className 。</p>    <p>来看看组件的写法,现在你只需要把 className 换成 styleName 即可获得CSS局部作用域的能力了,是不是非常简单。</p>    <pre>  <code class="language-css">importReactfrom 'react';  importstylesfrom './table.css';     class Tableextends React.Component {      render () {          return <divstyleName='table'>          </div>;      }  }     exportdefault Table;     </code></pre>    <p>工作原理</p>    <p>那么该babel插件是怎么工作的呢?让我们从官方文档入手:</p>    <p><a href="/misc/goto?guid=4959741832888084282" rel="nofollow,noindex">GitHub – gajus/babel-plugin-react-css-modules: Transforms styleName to className using compile time CSS module resolution.</a></p>    <p>笔者不才 ,稍作翻译如下:</p>    <p>1. 构建每个文件的所有样式表导入的索引(导入具有 .css 或 .scss 扩展名的文件)。</p>    <p>2. 使用 postcss 解析匹配到的css文件</p>    <p>3. 遍历所有 JSX 元素声明</p>    <p>4. 把 styleName 属性解析成匿名和命名的局部css模块引用</p>    <p>5. 查找与CSS模块引用相匹配的CSS类名称:</p>    <p>* 如果 styleName 的值是一个字符串字面值,生成一个字符串字面值。</p>    <p>* 如果是JSXExpressionContainer,在运行时使用helper函数来构建如果 styleName 的值是一个 jSXExpressionContainer , 使用辅助函数([ getClassName ]在运行时构造 className 值。</p>    <p>6. 从元素上移除 styleName 属性。</p>    <p>7. 将生成的 className 添加到现有的 className 值中(如果不存在则创建 className 属性)。</p>    <p>使用实例</p>    <p>在成熟的项目中,一般都会用到CSS预处理器或者后处理器。</p>    <p>这里以使用了 stylus CSS预处理器为例子,我们来看下如何使用。</p>    <ul>     <li>安装依赖</li>    </ul>    <pre>  <code class="language-css">npminstall -save-devsugerssbabel-plugin-react-css-modules      </code></pre>    <ul>     <li>编写Webpack配置</li>    </ul>    <pre>  <code class="language-css">// webpack.config.js  module: {    loaders: [{      test: /\.js?$/,      loader: [['babel-plugin-react-css-modules',{            generateScopedName:'[name]__[local]',            filetypes: {                ".styl": "sugerss"            }      }]]    }, {      test: /\.module.styl$/,      loader: 'style!css?modules&localIdentName=[name]__[local]!styl?sourceMap=true'    }, {      test: /\.styl$/,      loader: 'style!css!styl?sourceMap=true'    }]  }     </code></pre>    <ul>     <li>组件写法</li>    </ul>    <pre>  <code class="language-css">importReactfrom 'react';  import './table.module.styl';     class Tableextends React.Component {      render () {          return <divstyleName='table'>          </div>;      }  }     exportdefault Table;     </code></pre>    <p>如上,你可以通过配置Webpack中module.loaders的test路径 Webpack-module-loaders-configuration ,来区分样式文件是否需要CSS模块化。</p>    <p>搭配 sugerss 这个 postcss 插件作为 stylus 的语法加载器,来支持babel插件 babel-plugin-react-css-modules 的语法解析。</p>    <p>最后我们回过头来看下,我们React组件只需要把 className 换成 styleName ,搭配以上构建配置,即可实现CSS模块化 。</p>    <h2>最后</h2>    <p>CSS Modules 很好的解决了 CSS 目前面临的模块化难题。支持与 CSS处理器搭配使用,能充分利用现有技术积累。如果你的产品中正好遇到类似问题,非常值得一试。</p>    <p>希望大家都能写出健壮并且可扩展的CSS,以上。</p>    <p> </p>    <p>来自:http://www.alloyteam.com/2017/03/getting-started-with-css-modules-and-react-in-practice/</p>    <p> </p>