全部文章

css开发回顾与展望

2018-06-16 @sunderls

react css component

近期参与新项目开发,在开始技术选择阶段中JS使用React没有啥疑问,不过对于CSS如何处理产生了疑问。 首先目前公司内部使用的是SASS,但是内部规则过于陈旧麻烦,所以想要更加简单的CSS Module + PostCSS, 一开始觉得这样很好,不过其他还有styled-component等的解决方案,陷入了深深的沉思,决定回归初心分心一下。

首先CSS是为了干什么的

CSS,层级样式表,为了文档设定样式的。为啥要单独搞一个逻辑呢?这是为了为了第一个“C”-层级。 设想一下,所有的元素都是inline的话:

  1. 重复的样式会多的不行 - 比如一个通用的按钮样式
  2. 无法批量覆盖 - 比如换一个主题的话就会变得不可实现

所以单独抽离出一个样式表,按照CSS的规则可以实现样式的复用和覆盖。CSS主要包含两部分:

  1. 选择器 - id, className, tag等,来指定匹配的元素
  2. 属性 - 设定显示时的细节属性值

CSS的问题

CSS的出现本来是解决问题的,但是随着项目越来越负责,CSS本身的使用也会出现一些列问题。 根据个人工作经验,我总结为以下几个问题:

1. 命名难

inline style本身并不是被推荐的,应该是处于浏览器性能问题。但是CSS本身并没有命名空间概念, 类名的话可能出现重复冲突,所以css命名是个难题。为了解决这个问题出现了一些convention,比如

  1. BEM: 按照Block,Element, Modifier的规则来命名。
  2. Atomic: 把css规则拆分成足够小的部分,避免环境依存,像bootstrap那样。

通过这些规则,可以避免冲突,也可以让团队有一个共同的参照标准,但是并不能根治问题。 BEM非常啰嗦,还是会花时间思考命名,比如.header__logo--large。Atomic则是命名简单,html上会出现多个className,比如class="margin-top-20 padding-right-20"等。

2. 浏览器兼容,CSS版本兼容

CSS不断升级,浏览器的支持情况不同,导致对于某个单独的属性,可能需要添加:

  1. 不同vendor prefix来支持不同浏览器,比如-webkit-transformtransform
  2. 不同的值来支持不同的实现,比如display:flex, display:flexbox

3. CSS升级慢,写起来麻烦啰嗦

比如一个简单的问题,网站的基本颜色值,可能需要在各种按钮,边界线等地方使用。 可以在需要的地方都写一遍,但是有一天突然颜色变了,这就需要在各个地方去查找一遍更改回来,这非常麻烦。 我们需要一种可以定义变量的方法,幸运的是最新的css支持了variable。比如:

:root {
    --main-bg-color: coral; 
}

#div1 {
    background-color: var(--main-bg-color); 
}

#div2 {
    background-color: var(--main-bg-color);
}

这里有两个问题:

  1. 老浏览器不支持
  2. 语法有点蛋疼。

预处理 css preprocessor

人总是习惯偷懒,以上问题考虑之后自然想到能不能有一种更简单的书写方式。于是有了SASS, LESS等CSS预处理工具。

@main-bg-color: coral

#div1:
    background-color: @main-bg-color

#div2:
    background-color: @main-bg-color

这明显简单的多了。现在的预处理工具还有插件系统,帮助处理sprite图等,已经是CSS的基本工具之一了。

但是,可以看出,预处理工具不解决命名问题,另外新手上路将变得更复杂。

JS程序员和CSS程序员的分工

我掌握的信息可能不足以说明问题,据我的感觉CSS程序员和JS程序员分工的情况应该是普遍存在的,小公司两边都干的情况也不少。 这其中至少有以下几方面的原因:

  1. CSS和JS差距比较大: CSS是描述性的,需要的知识是UI的结构各种细节的实现经验等。而JS是逻辑性的,主要处理的业务逻辑。所以本身二者是可以分开的,专业性差距大。
  2. CSS和JS有各自不同的钻研方向。CSS更加偏重于如何实现更好的UI,而JS则是如何更快更稳健的开发业务逻辑,以及用JS实现更多的目的。
  3. 开发流程需要:CSS对设计负责,JS对业务逻辑负责。导致开发的时候CSS和JS可以在商量好的情况下同时进行。

分工带来了好处,也带来了弊端:

  1. 沟通成本: 虽然可以事先沟通,但是难免遇到某个实现上的分歧带来的效率地下。处于人情世故可能互相迁就导致代码质量下降。
  2. 命名意义不同:markup根据自己的理解定义的名字和JS以及API中的属性名不一致。虽然这只是个代码整洁性上的问题。

babel之于JS,postCSS之于CSS

与其用CoffeeScript带来新的语法,不如用Babel的plugin用上最新的ES语法。这得益于JS的不断前进带来的。 Sass就像CoffeScript,而对于CSS而言,我以为postCSS就是CSS的Babel。

postCSS会得到css的AST,然后通过postcss-preset-env就可以用上最新的CSS语法,用autoprefixer可以自动加上vendor prefix。

嗯,看上去非常不错。本质上和SASS基本一样,但是面向的是未来的语法。, 个人以为sass等预处理可以完全被postCSS替代,对于新手也更友好。

但是 postCSS并不直接解决命名以及CSS和JS程序员的合作问题。

css-modules 让命名变得更简单

CSS Module 是指默认样式是local的css文件,编译的时候会转换为icss的一种中间格式,这种格式在JS的module环境下会转换为object格式,方便调用。

使用例子: style.css

.className {
  color: green;
}
import styles from "./style.css";
element.innerHTML = '<div class="' + styles.className + '">';

可以看到css module有点像JSON。

另外通过composes可以扩展,有点像sass中的%extends

.className {
  color: green;
  background: red;
}

.otherClassName {
  composes: className;
  color: yellow;
}

通过css modules再也不用为命名冲突发愁了,因为一个css文件的作用域是本地的,可以安全的在不同的css文件中定义一个都叫title的className。 这是如何实现的呢?当然是通过编译系统,在最终输出成编译后的结果的时候,生成了唯一的类名。

使用css modules的话,很自然的出现了以下的文件组织结构

components/
    header/
        index.css
        index.js
    footer/
        index.css
        index.js
    ...

这样的好处是js和css得到了分离,代码比较清楚明白。我也是用过,感觉还行,除了有两点问题:

  1. 一些global的样式,无处安置,只能放在单独一个css/下面import,感觉有点奇怪
  2. css需要import到js中,然后用className={styles.title}这样的利用方式。。真的写多了发现很累。
  3. composes也比较啰嗦。比如一个公用的.btn类,为了增加一个margin,就需要在利用的地方,新增一个比如.btn{composes: btn from common}这种代码。。有点累。

这种情况在vue component中依然存在,比如这样一个component

<template>
<button class="btn">button</button>
</template
<style>
.btn {
    color: red;
}
</style>

我们还是需要添加一个.btn来锚点。

为啥不直接在js中写css呢?css-in-js

这出现的比较早了,当时我一直没有觉得这个有多好,因为写法上不方便,一直都没有尝试。

Object方式 🙅‍♂️

import CommonStyle from './commonStyle';

const style = {
    ...CommonStyle.button,
    color: 'red'
};

export default function Component() {
    return <div><button style={style}>button</button></div>
}

这是最基本的inline方式了,以object为单元直接通过style来控制。

通过 radium可以解决:hover等的支持问题,并且style还被扩展为支持数组,像react-native那样。

import Radium from 'radium';
import CommonStyle from './commonStyle';

function Component() {
    return <div><button style={[CommonStyle.button, {color: 'red'}]}>button</button></div>
}

export default Radium(Component);

另外还可以通过其他的工具实现更多的基于object的语法支持,比如j2c, glamor, jss

这样写起来,会出现很多styleXXX的Object,以及 style={styleXXX}的语句。 与其这样,还不如分开,像css module那样还整洁一些。

Styled Components

上述多次出现的style={styleXXX}问题,可以通过styled components解决。与其说这是一项技术,说其是一种思想或者写法说不定更好。

注意到目前为止,我们都试图通过className或者style来把样式和目标元素对应起来,只要还是这样思考,这中间的这个锚点就必然存在, 命名问题就必然存在,而直接inline显然不行,代码不可读。styled component抛弃了css的组件化概念,而是完全基于js的组件概念。 原来所有css锚点的位置,全部转化为单独的带有样式的component - styled component

用styled component来写上述例子的话:

import styled from 'styled-components';
import CommonButton from './CommonButton';

const Button = styled(CommonButton)`
    color: red;
`;

function Component() {
    return <div><Button>button</Button></div>
}

export default Radium(Component);

wow,代码可读性变得好很多!当然理解第四行干了什么需要画个几十分钟,建议参考The magic behind 💅 styled-components

其思想归结为一点就是通过component自身来实现css的组件化,消除css和component之间的mapping。本质上讲,其和下面的写法没有什么不同。

import CommonButton from './CommonButton';

const Button = (props) => <CommonButton style={{color: 'red'}}>{props.children}</CommonButton>;

export default function Component() {
    return <div><Button>button</Button></div>
}

略微精妙的是它巧妙的利用了Tagged Template Literals, 让中间component的声明变得巧妙。

style component看上去已经和有那么回事了。styled-components, styled-jss, glamorous, emotion, styletron 都是基于此的解决方案。

但是如何和css engineer分工协作是一个问题。

css半分离

回顾以下上面的vue.js的例子,为什么react不能这样呢?当然可以,稍微区别的是react原则上和vuejs不一样没有template这一说, 所以在实现上稍微有区别,比如styled-jsx

import CommonStyle from './commonStyle';

export default function Component() {
    return <div>
        <button style={style} className="button">button</button>
        <style jsx>{`
            .button: {
                color: 'red' !important;
            }
        `}</style>
    </div>
}

我只能说,这太难看了!!!!!!!!!

总结

回顾一下css开发的问题,我们发现到目前为止styled components是最好的解决办法。如果对于css和js文件分离有要求的话css-modules+postCSS值得一试。