[译]统一样式语言

原文: [https://medium.com/seek-blog/a-unified-styling-language-d0c208de2660]

原文发表于: 2017-05-23

近几年里我们看到了很多 css in js 的尝试.
大多来自 React 社区. 显然这遭到了很多的吐槽, 尤其是那些非常熟练 css 的人, 觉得这种行为难以置信

"为什么会有人想把 css 写在 js 里?"
"这绝对是糟糕的主意!"
"如果他们学过 css 的话!"

如果这是你的第一反应, 那么请读下去. 我们需要继续讨论为什么把 css 写在 js 里并不那么糟糕, 以及为什么我们要着眼于此.

社区的误解

我从19岁就开始从事 css 专业, 在那个用表格布局的黑暗年代. 按照 CSS Zen Garden 中受到启发, 我将过去的代码迁移到语义标签下并使用样式表布局.
不久之后,我开始痴迷于分离样式,使用不起眼的JavaScript来装饰服务器渲染的标记与客户端交互. 有一个小但是活跃的社区从事着这样的实践, 我们成为了第一批前端工程师.

或许你会认为我会反对 React 的 HTML-in-JS, 因为这和我之前的所做完全相反. 在我的经验里, React组件模型及其服务端渲染能力, 最终可以构建一个复杂的单页应用, 从而让我们可以向用户提供快速, 可达, 渐进增强的应用. 我在 Seek 实践了这样的能力, 通过 React 构建了单页面应用, 即使禁用了 js, 搜索依旧可用, 并且通过服务端渲染在旧的浏览器中可以正常工作

所以让两个社区和平共处的橄榄枝是什么呢, 虽然这种方式并不完美, 没有被你用于生产环境, 甚至不是很有说服力, 但至少可以带给我们思考空间.

Why CSS-in-JS?

希望通过范围控制样式的开发者通常喜欢直接使用 CSS 模块, 而非 CSS-in-JS. 我在工作项目也没有使用 CSS-in-JS.

尽管如此我也实时关注 CSS-in-JS社区, 并且我也认为应当如此.

但是为什么需要 CSS-in-JS 呢?

为了清楚这个问题, 我们将讨论这种做法带来的好处:

  1. 范围约束
  2. 关键样式 (Critical CSS)
  3. 更智能的优化
  4. 包管理
  5. 非浏览器平台样式 (Non-browser styling)

1. 范围约束

普片认为, 构筑有效的 CSS 是一件很困难的事情. 尤其是在一个长期的项目中, 很难找到对应的 CSS.

CSS 社区投入了很大精力想解决这个问题, 通过一些方式增加样式的可维护性, 例如 OOCSS, SMACSS 等. 其中最流行的是来自 YandexBEM, Block Element Modifier.

BEM( 纯 CSS 实现)只是一个命名约定, 通过像是 .Block__element--modifierclass 限制样式. 在 BEM 的项目中, 开发者不得不在所有场合遵循 BEM 规则, 能做到这一点, BEM 是很好用的, 但范围限制只能以纯粹的规范来约束么?

事实上, 大多 CSS-in-JS 都遵循 BEM 的心态, 以不同的方式将样式定位在单个元素上. 例如 glamor, 写起来像是这样:

1
2
3
4
5
6
7
8
import { css } from 'glamor'
const title = css({
fontSize: '1.8em',
fontFamily: 'Comic Sans MS',
color: 'blue'
})
console.log(title)
// → 'css-1pyvz'

请注意, CSS class 并没有在代码中出现. 对样式的引用不再需要在代码中硬编码, 而是由库自动生成. 不用担心选择器全局范围冲突, 也意味着我们不需要手动增加前缀.

选择器的范围与代码所在的范围相匹配. 如果想让这规则在整个应用中都适用, 则需要以 js 模块的方式引入. 这对于长期维护项目是极其强大的, 可以确保样式像其他资源一样能被轻易追踪.

从简单的规范约束到代码约束, 这提高了样式代码的基础质量.

继续之前有个很重要的事情声明

生成的样式是 css, 不是行内样式

早期 CSS-in-JS 类直接将样式绑定在每一个元素上, 元素上的 style 无法代替 css 的所有功能. 新的类库基本上会生成全局的样式, 实时进行插入和删除

JSS 就是一个例子. 使用 JSS 可以实现像是 hover, @media 之类的功能, 直接生成相同的 css 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
const styles = {
button: {
padding: '10px',
'&:hover': {
background: 'blue'
}
},
'@media (min-width: 1024px)': {
button: {
padding: '20px'
}
}
}

这段代码将自动生成对应的 class

1
const { classes } = jss.createStyleSheet(styles).attach()

无论是通过某些框架, 或是简单的 innerHTML, 生成的 class 都可以直接替换之前的 class 字符串

1
2
3
document.body.innerHTML = `
<h1 class="${classes.heading}">Hello World!</h1>
`

这种样式管理可以很方便的整合到其他库中, 比如 react-jss 可以在全局的生命周期注入样式.

1
2
3
4
5
6
7
8
9
import injectSheet from 'react-jss'
const Button = ({ classes, children }) => (
<button className={classes.button}>
<span className={classes.label}>
{children}
</span>
</button>
)
export default injectSheet(styles)(Button)

将样式关联在组件上会使生成的样式与其他代码紧密耦合. 由于 BEM 带来的习惯, 许多 CSS-in-JS 社区认为提取是十分重要的, 在样式关联的过程中丢掉了命名与复用.

一种解决这个问题的新方案是 styled-components.

代替创建样式, styled-components 会直接创建样式关联的组件

1
2
3
4
5
6
import styled from 'styled-components'

const Title = styled.h1`
font-family: Comic Sans MS;
color: blue;
`

我们没有创建一个 class 关联到元素上, 而是直接生成了一个包含样式的组件.

1
<Title>Hello World!</Title>

styled-components 还是使用的 css 字符串, 另一种结构化做的更好的方式是 PayPal 的 Glamorous.

Glamorous提供了和 styled-components 相似的 component-first API, 但用 object 定义样式而非字符串.
这种做法更方便以后减少样式体积或是提高性能

1
2
3
4
5
6
import glamorous from 'glamorous'

const Title = glamorous.h1({
fontFamily: 'Comic Sans MS',
color: 'blue'
})

无论哪种描述样式的语法, 样式与组件都不再仅仅是作用范围的关系, 而是一个密不可分的整体. 当使用像是 React 之类的框架时, 组件就是构建项目的基本单位, 样式则是一种形式的组件. 如果任何东西都可以描述成组件, 为什么还需要单独写样式呢


作为一个用惯了 BEM 的人, 这些变化都不是很大. 但对于习惯了 CSS Modules 的人, 他们并不太愿意为此放弃已经熟练的 CSS 工具与生态环境. 这就是为什么许多项目依旧愿意不改变习惯的基础上通过写大量的 css 解决大多数问题.

2. 关键样式(Critical CSS)

最近相对流行的做法是在文档的顶部引入当前页需要的内联样式来优化页面加载时间. 这与传统加载样式的方式不同, 需要等所有样式都加载完毕才开始进行渲染.

(译注: Critical CSS的概念可以参考理解Critical CSS)

有一些工具可以提取出当前页所需的关键样式, 比如critical. 然而这种做法并没有解决关键样式难以维护或自动化的问题. 这是一个棘手且可选的性能优化问题, 很多项目直接放弃了这一步.

CSS-in-JS 就不一样了

在一个服务端渲染的项目里, 提取关键样式不是一个可选优化, 服务端会严格要求提取出关键的样式.

举个例子, Aphrodite 通过在元素上绑定 class 时调用 CSS 函数跟踪一次渲染所需要哪些样式.

1
2
3
4
5
6
7
import { StyleSheet, css } from 'aphrodite'
const styles = StyleSheet.create({
title: { ... }
})
const Heading = ({ children }) => (
<h1 className={css(styles.heading)}>{ children }</h1>
)

即使样式是通过 js 定义的, 也可以很方便找到当前页面上的全部样式, 并在 server render 时以<style&rt;形式插入文档顶部

如果你观察过 React 的 SSR 项目, 你会发现这种做法非常普遍. 组件通过 js 定义的标记, 会被以 HTML 字符串输出.

如果是以渐进(progressive enhancement)的形式构建程序, 甚至可能不需要客户端上执行任何 JavaScript.

另一方面, 客户端渲染的js则需要脚本去启动你的单页应用, 带入生命周期. 按照指定的方式渲染浏览器.

由于渲染 HTML 和 CSS 的时间是一致的. Aphrodite之类的工具通常会同时计算关键样式并渲染出 HTML. 渲染 React 组件的过程也是相似的.

1
2
3
4
5
const appHtml = `
<div id="root">
${html}
</div>
`

服务端使用 CSS-in-JS, 不但能让客户端没有 JavaScript 的场合下正常工作, 还能渲染的更快.

3. 更智能的优化

人们最近都在尝试一些新的方式去格式化 CSS, 比如雅虎的 Atomic CSS等.
在项目中避免 class 包含”语义”, 而是短小以及用途单一. 例如 Atomic CSS 会用一种近似函数的方式去构建样式表:

1
2
3
<div class="Bgc(#0280ae.5) C(#fff) P(20px)">
Atomic CSS
</div>

这样做是为了缩小样式代码, class可以被尽可能复用. 这种做法可以减少代码文件体积, 并对开发者没有什么影响.

如之前所描述的, CSS-in-JS 的 className 是由程序自动生成的,

代替:

<aside className="sidebar" />

代码会写成:

<aside className={styles.sidebar} />

这种变化看起来很小, 但对标签与样式转换很有帮助. 上面例子中的 styles.sidebar 可以转换为多个 class 名称

1
2
3
<aside className={styles.sidebar} />
// Could easily resolve to this:
<aside className={'class1 class2 class3 class4'} />

如果给每一种样式生成一个对应的 class, 这样能做很多有趣的事情.

我喜欢的一个例子是 Styletron

Styletron 核心API 只做一件事情, 将样式分组, 然后生成对应 className.

1
2
3
4
5
6
7
8
import styletron from 'styletron';
styletron.injectDeclaration({
prop: 'color',
val: 'red',
media: '(min-width: 800px)'
});

// → 'a'

当然 Styletron 还提供了更高级的 API, 比如 injectStyle 用于一次性定义样式集.

1
2
3
4
5
6
7
8
9
10
11
12
import { injectStyle } from 'styletron-utils';
injectStyle(styletron, {
color: 'red',
display: 'inline-block'
});
// → 'a d'

injectStyle(styletron, {
color: 'red',
fontSize: '1.6em'
});
// → 'a e'

注意上面两组 class 的公共部分.

放弃对 class 的直接管理, 仅仅声明我们需要的样式. 由库自己去进行 class 命名的优化.

上图为 使用 Airbnb 风格的 CSS-in-JS 打包大小对比. 通过手动将样式抽象复用的方式现在完全可以自动化实现, 从这里的趋势看出, CSS 原子化现在已经成熟.

4. 包管理

继续往下看之前请先想一想, 我们是如何分享 css 代码的?

可能是手动下载 css 文件, 通过一些前端依赖管理工具, 比如 bower, 现在还可以直接使用npm (感谢 Browserifywebpack), 或是其他一些解包 css 的工具. 前端往往会手动的处理 css 依赖.

无论哪种方式在处理对其他 css 的依赖都不是很理想.

很多人还记得 bowernpm 对依赖处理之间的异同.

Bower 没有耦合任何模块形式, NPM 则采用了 CommonJS. 这导致两者发布差异很大.

Npm 对小型嵌套的依赖非常适用, 而 bower则往往需要引入完整, 庞大的依赖. 每个依赖自身不能很好的处理自己的依赖, 这些往往都需要手动配置.

最终 Npm 上的包迅速增长, 而 Bower 却慢慢被人淡忘…

不幸的是, CSS 社区似乎正在重演 Bower 的道路. 想要解决CSS 嵌套依赖及复杂的层级结构, 我们需要的不仅仅是一个包管理工具, 还需要一种模块式的依赖方法.

那 CSS 需要单独的依赖管理工具吗?

事实上 CSS 与 HTML 关系很像. 如果被问到我们如何分享 HTML, 很容易就能意识到, 我们从不直接共享 HTML, 我们共享的是 HTML-in-JS.

在 jQuery plugins, Angular directives 和 React components 中, 我们都是用小的组件构成大的组件, 每个组件都有自己的 HTML. 每一块都能单独在 npm 上发布. 作为标记语言的 HTML 不够强大到可以这样发布并处理依赖关系, 但是将 HTML 嵌入编程语言中, 我们可以很容易的实现这样的功能.

我们当然也可以像分享 HTML 一样分享 CSS. 我们可以通过 mixins, extends class 或者简单的 Object.assign 或是一种对象分割操作 来处理样式.

1
2
3
4
5
6
const styles = {
...rules,
...moreRules,
fontFamily: 'Comic Sans MS',
color: 'blue'
}

这样我们可以组合并分享样式使用同样的规则, 同样的工具, 同样的基础以及同样的生态体系.

一个比较好的例子是 Polished.

Polished 是 CSS-in-JS 中的 lodash. 他提供了一套完善的混合/色彩函数/快捷操作等工具. 有一点像 JavaScript 版的 Sass. 关键差别在于 Polished生成的代码能更好的组合/测试/分享, 并完全适用于 js 包管理的生态.

5. 非浏览器平台样式

上述的都是 CSS-in-JS 的一些方便之处, 通过传统 CSS 的方式也依然能达到同样的目的. 我将最有趣的也是面向未来的一点放在了最后面. 这一点不是为了当前的 CSS-in-JS, 而是未来设计的一个基础, 这不仅面向前端开发人员, 也面向设计师, 这将彻底改变两个专业的沟通方式.

首先我们需要观察一下 React.


React 的模块是最终渲染之前的中间组件. 在浏览器中它没有直接操作 DOM, 取而代之的是 虚拟 DOM (virtual DOM).

有趣的是, 渲染到 DOM 的并不是 React, 而是 react-dom 库.

不同的环境允许 React 生成不同的对象, JSX 提供的不仅仅是虚拟 DOM, 而是虚拟一切 (virtual whatever).

这是 ReactNative做的事情, 用 JavaScript 语法写原生应用, 最终生成原生组件, 用 TextView 代替 divspan.

最有趣的是, ReactNative 有其独有的一份 StyleSheet API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var styles = StyleSheet.create({
container: {
borderRadius: 4,
borderWidth: 0.5,
borderColor: '#d6d7da',
},
title: {
fontSize: 19,
fontWeight: 'bold',
},
activeTitle: {
color: 'red',
}
})

看起来和普通的样式很像, 有颜色, 字体和边框样式. 这些规则很简单, 很容易映射到普通的 UI 环境. 不过有趣的地方在于转换到原生的语法上.

1
2
3
4
5
var styles = StyleSheet.create({
container: {
display: 'flex'
}
})

在浏览器之外, React Native 也实现了一套 Native 的 flexbox.

这个功能最早作为一个名为 css-layout 的 JavaScript 包被发布, 后来又被重构成了 C.

鉴于其被广泛使用及重要性, 最终它成为了一个独立的品牌: Yoga.

虽然 Yoga是作为将 CSS 移植到非浏览器环境的一个项目, 出于实现成本成本考虑, 它也没有实现所有的 CSS 功能.

这些听起来虽然有所限制, 但是纵观 CSS 历史, 使用 CSS 一直都是挑选出合适的子集.

Yoga 避免了不必要的级联样式, 专注于 flexbox 的实现. 这给跨平台(cross-platform)组件带来了许多可能性.

React Native for Web 用于反过来将 ReactNative 用于 web 中. 使用 webpack 等构建工具可以很好的给包设置别名

1
2
3
4
5
module: {
alias: {
'react-native': 'react-native-web'
}
}

使用 react-native-web 可以将 ReactNative 的组件直接应用在 web 中, 当然也包含其 StyleSheet API.

另一个比较相似的东西是 react-primitives. 它定义了一批在各个平台对应不同实现的原始组件, 用于构建跨平台应用.

微软提供的 ReactXp 库, 用于提供一个支持 web 与原生双方的编码方式, 使用了所谓平台无关风格实现的编码思想.

即使不写 native app, 跨平台的组件减少了环境依赖的限制, 往往会带来意料之外的惊喜.

其中一个惊喜是 airbnb 的 react-sketchapp.

直到 react-sketchapp的到来前, 我们还是不得不将前端开发和设计单独进行.

react-sketchapp 让我们根据 Sketch 的文档, 自动生成跨平台的 react 组件. 这颠覆了开发者和设计师过去的合作方式. 现在, 当我们想改变组件的 UI 时, 我们只需要改变设计, 反之同理.


CSS-in-JS 这种统一语言的组件定义行为, 让我们将关注更好的分离开 – 不是技术而是功能层面的分离. 每一个组件都有自己独立的范围, 然后组合出一个可维护的系统, 使我们能更好的分享我们的组件, 更好的利用开源模块构建复杂的应用.

这一切让人对于这种统一样式语言(unified styling language) 充满了期待, 或许这会给前端社区带来一种从未有过的方法.

当然, 目前来说使用 CSS-in-JS 还需要谨慎. 这一技术还不成熟, 也不一定完全合适于你的项目. 无论如何, 这个技术最近越来越受人们欢迎, 很值得去了解并关注它的发展之路.