NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

鱼和熊掌的故事 - CSS Modules还是BEM

| Comments

首先还是最基础的两个问题:什么是BEM?什么是CSS Modules?

BEM(Block Element Modifer)

https://en.bem.info/

Alt text

BEM的意思就是块(block)、元素(element)、修饰符(modifier),是由Yandex团队提出的一种前端命名方法论。这种巧妙的命名方法让你的CSS类对其他开发者来说更加透明而且更有意义。BEM命名约定更加严格,而且包含更多的信息,它们用于一个团队开发一个耗时的大项目。

1
2
3
.block{}
.block__element{}
.block--modifier{}

.block 代表了更高级别的抽象或组件。
.block__element 代表.block的后代,用于形成一个完整的.block的整体。
.block–modifier代表.block的不同状态或不同版本。
.block__element–modifier代表.element的不同状态或不同版本。

我们用一个搜索栏为例子:

1
2
3
4
<form class="site-search full">
  <input type="text" class="field">
  <input type="Submit" value ="Search" class="button">
</form>

上面的这个CSS类名真是太不精确了,比如field,并不能告诉我们足够的信息。尽管我们可以用它们来完成工作,但它们确实非常含糊不清。用BEM记号法就会是下面这个样子:

1
2
3
4
<form class="site-search site-search--full">
  <input type="text" class="site-search__field">
  <input type="Submit" value="Search" class="site-search__button">
</form>

我们能清晰地看到有个叫.site-search的块,它的内部是一个叫.site-search__field的元素。并且.site-search还有另外一种形态叫.site-search–full。如果搜索栏还有验证,那么它还有一个error状态,可以用site-search__field–error。

虽然上面是个简单地例子,但相信你已经可以看到BEM带来的好处是命名独立,有意义,且可以清晰的表示模块结构。

BEM(或BEM的变体)是一个非常有用,强大,简单的命名约定,以至于让你的前端代码更容易阅读和理解,更容易协作,更容易控制,更加健壮和明确而且更加严密。

CSS Modules

在React中,我们以Web Component的方式实现应用。组件(Component)的概念中有一个很重要特性:完整和自包含,而对于一个完整的Web Component,包含HTML,JAVASCRIPT和CSS。

React通过JSX实现了在JavaScript中写HTML,但是还缺少一个重要的元素:CSS。

在2014年11月的NationJS上Christopher Chedeau谈到了“CSS in JS”的话题。给许多人带了思想上的一个冲击,也让React实现完整Web Component带来的曙光。

现在,已经有了三种最新的,最明智和最可行的实现React样式的方式。

React Style: https://github.com/js-next/react-style
JSX Style: https://github.com/petehunt/jsxstyle
Radium: https://github.com/FormidableLabs/radium

一般情况下,如果项目中有大量的CSS,会出现什么问题?如图

Alt text

Christopher指出,如果你将样式都挪到JavaScript中,这些问题都有很好地解决方案,这点说的没错,但是会引入复杂度,以及不太习惯的CSS使用方式。CSS Modules团队觉得可以让CSS还是保持以前的样子,但是构建时,以style-in-JS的方式实现。

CSS Modules的项目地址: https://github.com/css-modules/css-modules

默认就是局部命名空间

在CSS Modules中每个文件都是独立编译,你可以使用更为简单地类命名方式,而不用担心它会污染全局。

我们以不同状态的Button为例,在没有CSS Modules的情况下,你可能会以Suit/BEM的方式来写CSS。

1
2
3
4
5
/* components/submit-button.css \*/
.Button { /* all styles for Normal \*/ }
.Button--disabled { /* overrides for Disabled \*/ }
.Button--error { /* overrides for Error \*/ }
.Button--in-progress { /* overrides for In Progress \*/}
1
<button class="Button Button--in-progress">Processing...</button>

使用CSS Modules的方式,你就可以用下面这种方式来写:

1
2
3
4
5
/* components/submit-button.css \*/
.normal { /* all styles for Normal \*/ }
.disabled { /* all styles for Disabled \*/ }
.error { /* all styles for Error \*/ }
.inProgress { /* all styles for In Progress \*/

你应该发现,class的前缀button没有了,为什么?因为这个css文件的名字已经叫做submit-button.css,CSS Modules已经知道它的前缀,不需要再次定义了。

1
2
3
/* components/submit-button.js \*/
import styles from './submit-button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

以JavaScript的方式使用CSS。

然后,你再看看生成的最终页面的样子:

1
2
3
<button class="components_submit_button__normal__abc5436">
  Processing...
</button>

最终生成的class的名字是Component Name + Class Name + Base64,保证了层次结构和唯一性。

鱼和熊掌的问题

对比一下BEM和CSS Modules,它们最终的目的都是给你带来独立,唯一和有意义的类命名。CSS Modules允许你以变量的方式将class获取并使用到元素上,并且省略了类名中Component Name前缀(也就是BEM中的B)。

在React中,Component一般会由多个元素组成(除了像Button这样比较简单地组件),就以上面的搜索框为例,在React中以CSS Modules的方式写,就有可能如下:

1
2
3
4
5
6
import styles from './site-search.css';
//省略React代码
<form className={styles.full}>
  <input type="text" className={styles.field}>
  <input type="Submit" value ="Search" class={styles.button}>
</form>  

对比一下BEM方式

1
2
3
4
5
6
import from './site-search-bem.css';//以BEM方式写的样式表
//省略React代码
<form className="site-search site-search--full">
  <input type="text" className="site-search__field">
  <input type="Submit" value="Search" className="site-search__button">
</form>

CSS Modules好像挺完美的,变量的方式传递class,在JavaScript的语境下似乎更有道理,但从本质上来说,与BEM相比,也就只是省略了Block这一个前缀,在这个看似好像很不错的方案下,有没有存在的问题?

问题

测试

在React的环境下,会使用Jest框架对组件进行测试,import styles from ‘./site-search.css’;这一个语句,并不是JavaScript的标准,只是CSS Modules的实现者,帮助你进行了编译,但Jest并不会编译。

所以这个时候,我们会在Jest的PreProcessor中处理这句话,这样才不会有语法解析错误。

1
2
3
4
5
6
7
8
9
10
11
var babel = require('./babel-jest');

module.exports = {
  process: function (src, filename) {
      if (/\.(css|scss)$/.test(filename)) {
          return '';
      } else {
          return babel.process(src, filename);
      }
  }
};

但与此同时,styles会变成空对象,由于我们使用了变量的方式,在测试中,并不能测试className的改变(因为state改变,去改变className)。相反,BEM因为是纯粹的字符串,所以它使可测试的。

在React中引入classnames模块

我们知道,在React中,通过state的改变来决定组件的样式变化,在没有外部力量帮助的情况下,你的代码就会如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* components/submit-button.jsx \*/
import { Component } from 'react';
import styles from './submit-button.css';

export default class SubmitButton extends Component {
  render() {
    let className, text = "Submit"
    if (this.props.store.submissionInProgress) {
      className = styles.inProgress
      text = "Processing..."
    } else if (this.props.store.errorOccurred) {
      className = styles.error
    } else if (!this.props.form.valid) {
      className = styles.disabled
    } else {
      className = styles.normal
    }
    return <button className={className}>{text}</button>
  }
}

这个时候,可以引入classnames模块,例子来自classnames样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var classNames = require('classnames');

var Button = React.createClass({
  // ...
  render () {
    var btnClass = classNames({
      'btn': true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

问题是,classNames中传入的JavaScript对象,不可以用styles.normal作为key,如果要在CSS Modules的环境下,使用classnames,你需要像下面这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import styles from './site-search.css';
var classNames = require('classnames');

var Button = React.createClass({
  // ...
  render () {
    var btnClass = classNames({
      [styles['btn']]: true,
      [styles['btn-pressed']]: this.state.isPressed,
      [styles['btn-over']]: !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

如果你不使用classnames,在使用CSS Modules也是可以用styles[‘normal’]来代替styles.normal。

无意中的发现,结合CSS Modules和BEM方式

通过用styles[‘normal’]来代替styles.normal,在定义CSS的类名时,你依旧可以使用你习惯的CSS类命名方式,比如:BEM。上面的搜索框的例子,就可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
import styles from './site-search.css';
import classNames from 'classnames';
//省略React代码

var fieldClass = classNames({
  [styles['field']]: !this.state.isError,
  [styles['field__error']]: this.state.isError,
});

<form className={styles[full]}>
  <input type="text" className={fieldClass}>
  <input type="Submit" value="Search" className={styles['submit']}>
</form>

这种方式,既保证了CSS Modules的使用,也保留了BEM通过三种基本元素来描述组件样式的方式。

总结

对比两种实现方式,从本质上来讲,并没有区别,否则它们也不可能结合在一起,但是CSS Modules带来的致命伤害是作为一个变量,并不能直接测试(想要测试,可以让Jest的路径指向已经编译过的JavaScript路径)。而纯粹的BEM因为是字符串可以很好地和classnames结合使用,也可以进行测试,所以在选择上占据了很大的优势。

参考资料:
1.http://glenmaddern.com/articles/css-modules
2.http://segmentfault.com/a/1190000000391762
3.https://github.com/css-modules/css-modules
4.https://www.npmjs.com/package/classnames

继续前行!React(一)- 用JavaScript构建Web UI

| Comments

什么是React?

官方网站上是这么写的:

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

React只关心UI,至于怎么路由(route),怎么获取数据(ajax),你可以通过结合其他技术来做。

什么UI,在web中UI指的是HTML和CSS,React通过JavaScript的方式来写HTML和CSS,这就是它所说的“构建用户界面的JavaScript类库”。

用JavaScript绘制UI

1
2
3
4
5
6
7
8
9
var HelloMessage = React.createClass({
  displayName: "HelloMessage",

  render: function render() {
    return React.createElement("div", null, "Hello ", this.props.name);
  }
});

ReactDOM.render(React.createElement(HelloMessage, { name: "John" }), mountNode);

mountNode是一个在页面上的元素,通过document.getElementByXXX得到。

该代码执行得到的结果如下:

Hello John

简单地说,就是它找到页面上的mountNode节点,将Hello John插入,其中John来自于动态传入的JSON数据。

根据上面的解释,是否让你联想到了JavaScript模板引擎,对JavaScript模板引擎不了解的,可以看这篇文章: http://benweizhu.github.io/blog/2015/10/28/js-template-engine/

和JavaScript模板引擎相比,上面这种方式通过JavaScript来编写UI代码,可读性相差甚远,于是React提出了JSX的概念 - JavaScript XML,上面的代码就变成如下:

1
2
3
4
5
6
7
var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

ReactDOM.render(<HelloMessage name="John" />, mountNode);

不管你现在了不了解JSX,但是你应该可以看出来,React所指的用JavaScript构建UI的概念。

HelloMessage就像是一个自定义的新标签,在React的概念中,叫做一个组件。

组件

React的核心概念是组件,广义的理解组件,组件(Component)是对数据和方法的简单封装,能够独立的实现某一种功能。

组件是一种封装,它应该是自包含的,不需要外界的帮助,即可完成自身相应的功能(多个组件相互合作,也是组件本身可以完成相应功能的前提下实现的),比如上面代码中的HelloMessage。

有状态的组件(Props和State):

React不需要你去操作DOM,你只要告诉它,你想要绘制的DOM长什么样?如何去绘制,由它自己来操作。

你只需要根据事件相应来改变组件的状态,当组件状态改变,它会自动重新绘制DOM。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: function() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
    );
  }
});

ReactDOM.render(<Timer />, mountNode);

你改变的是组件的状态this.state.secondsElapsed,DOM的绘制由React自己完成,你只是告诉他画成这样。

参考资料:
1. https://facebook.github.io/react/index.html