broken-css – 零运行时的原子化 JSS 方案

broken-css – 零运行时的原子化 JSS 方案

介绍

broken-css 践行了 “正常的编写代码,让编译器负责优化的事情” 的理念,你会使用你熟悉的方式来编写 CSS ,让编译器来自动原子化你的样式,进行复用。

broken-css 核心实现由以下两个库构成

  • @broken-css/core
  • @broken-css/webpack-loader

@broken-css/core

@broken-css/core 做的事情很简单,只是给 @broken-css/webpack-loader 一个信号,告诉这里需要编译, @broken-css/core 源码非常简单,只有三行。

export const css = (_literal: TemplateStringsArray, ..._DOES_NOT_SUPPORT_EXPRESSIONS: never[]): string => {
  throw new SyntaxError('Do not call css on runtime!')
};
复制代码

因为 css … “ 表达式,在编译后会被替换成一段字符串,所以在运行期间,这个函数其实不会被真正执行到,所以并不会抛出这个错误。

@broken-css/webpack-loader

@broken-css/webpack-loader 完成核心步骤,即将代码中 JSS 替换成原子化的 CSS 类名 ,并将编译后的原子化 CSS 导入到相应的 JS 文件中,使得 webpack 接管导入 CSS 的流程,以便复用相关 loader 和 plugin 。

例子

假如我们有两个组件 Foo 和 Bar

// Foo.tsx
import { css } from "@broken-css/core";
import React, { FC } from "react";

const Foo: FC = () => {
  return (
    <div className={css`
      color: red;
      font-size: 24px;
      border: 1px solid black;
      @keyframes shake {
        10%, 90% {
          transform: translate3d(-1px, 0, 0);
        }

        20%, 80% {
          transform: translate3d(2px, 0, 0);
        }

        30%, 50%, 70% {
          transform: translate3d(-4px, 0, 0);
        }

        40%, 60% {
          transform: translate3d(4px, 0, 0);
        }
      }
      &:hover {
        animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
        transform: translate3d(0, 0, 0);
        backface-visibility: hidden;
        perspective: 1000px;
      }
      &::after {
        content: ' after';
        color: brown;
      }
    `}>foo</div>
  );
}

export default Foo;

// Bar.tsx

import { css } from "@broken-css/core";
import React, { FC } from "react";

const Bar: FC = () => {
  return (
    <div className={css`
      color: red;
      font-size: 24px;
      border: 1px black solid;
    `}>bar</div>
  );
}

export default Bar;
复制代码

在编译后会变成

// Foo.tsx
import { css } from "@broken-css/core";
import React, { FC } from "react";

const Foo: FC = () => {
  return <div className={"_0e91 _b38a _43fe _b04b _4b6c"}>foo</div>;
};

export default Foo;
;require("../node_modules/.cache/broken-css-webpack-loader/broken.css");

// Bar.tsx

import { css } from "@broken-css/core";
import React, { FC } from "react";

const Bar: FC = () => {
  return <div className={"_43fe _b04b _f617"}>bar</div>;
};

export default Bar;
;require("../node_modules/.cache/broken-css-webpack-loader/broken.css");

复制代码
/** broken.css **/
@keyframes shake {
    10%, 90% {
        transform: translate3d(-1px, 0, 0);
    }
    20%, 80% {
        transform: translate3d(2px, 0, 0);
    }
    30%, 50%, 70% {
        transform: translate3d(-4px, 0, 0);
    }
    40%, 60% {
        transform: translate3d(4px, 0, 0);
    }
}

._0e91:hover {
    animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
    transform: translate3d(0, 0, 0);
    backface-visibility: hidden;
    perspective: 1000px;
}

._b38a::after {
    content: ' after';
    color: brown;
}

._43fe {color: red;}

._b04b {font-size: 24px;}

._4b6c {border: 1px solid black;}

._f617 {border: 1px black solid;}
复制代码

特性

原子化

如例子所演示的那样,broken-css 会根据样式的内容计算出一个哈希值来表示这个样式规则,同时这个哈希值会被当做类名替换到相应的 JS 文件中,因为哈希是根据其内容计算的,来自两个文件相同的样式计算出的哈希值是一样的,因此在后续的去重步骤中可以将其筛选掉,从而达到复用的目的。

这里需要多说一点的是,对于样式 border: 1px solid black;border: 1px black solid; 来说, broken-css 并不会视为是同一种样式,尽管他们的效果是一样的。如果要做到这种程度的复用,需要考虑太多的边界情况,我希望有一种简单通用的方法来解决这个问题,还在研究中。

体积优势

在使用 broken-css 后,你的 CSS 体积在刚开始不会明显的减少,但随着项目的发展,越来越多的样式存在重复,使得复用的可能性大大增多,体积就会降下来,如果把体积的变化汇成一条线的话,传统的 CSS 体积增长是一条直线,而 broken-css 则是一条曲线。

Untitled.png

配图源于《Atomic CSS-in-JS》

伪类支持

broken-css 支持伪类选择器,对于形如 &::after { ... } 的规则,broken-css 会根据整体样式规则计算出哈希值,然后将 & 替换成对应的哈希值。

// a.js
const cls1 = css`
	color: red;
`

// b.js

const cls2 = css`
	&:hover {
		color: red;
		font-size: 24px;
	}
`
复制代码

应该编译为

// a.js
const cls1 = 'c1'

// b.js

const cls2 = 'c1 c2'
复制代码
.c1, .c1:hover { color: red; }
.c2 { font-size: 24px; }  
复制代码

@规则支持

@ 规则的支持是无需 broken-css 的编译期做任何特殊处理,因此你可以自由的使用动画和媒体查询等规则,broken-css 不会对他们做任何处理,只是简单的解压到最终的 CSS 文件中。

这里我纠结的一点在于要不要做 @keyframes 命名的隔离,但是在后续的思考中发现这并不是简单的事情,例如命名 scope 的范围,是隔离在每一次 css … “ 调用期间,这样的话怎么做复用?全局范围的话,意味着需要维持一张状态表,并且分析全局的 CSS 代码将相应的命名替换掉,同样的很复杂。

CSS 变量

broken-css 将 CSS 变量视作一个普通的样式声明,并没有任何特殊的处理,同样的会根据其内容计算出一个哈希值,并分配一个唯一的类名

const cls1 = css`
	--main-color: red;
	backgroud-color: var(--main-color);
`

const cls2 = css`
	backgroud-color: var(--main-color);
`
复制代码

会被编译为

const cls1 = 'c1 c2'
const cls2 = 'c2'
复制代码
.c1 { --main-color: red; }
.c2 { backgroud-color: var(--main-color); }
复制代码

SSR

由于 broken-css 会在编译后将 JSS 抽离到 CSS 文件中,因此你可以在返回组件生成的 html 片段时,去读取 CSS 文件自行决定用什么形式传输给浏览器。

import { collect } from '@linaria/server';

const css = fs.readFileSync('./dist/styles.css', 'utf8');
const html = ReactDOMServer.renderToString(<App />);
const { critical, other } = collect(html, css);
复制代码

这是 linaria 实现的 SSR 的方式,broken-css 同样适用于这种方式,现在面临的问题是 broken-css 还没有实现类似 collect 形式的用于抽离非首屏的样式的 API ,因此返回的是全量 CSS ,会带有一些冗余样式。实际上如果只是考虑到抽离这个功能,你可以直接使用 @linaria/server ,它并不是依赖于某个特定 JSS 框架的。

智能语法提示

我不太熟悉其他的 IDE ,如果你使用 VSCode ,这个问题可以很完美的解决, broken-css 的 API 形式兼容了 vscode-styled-components 扩展。

后记

broken-css 是我现在在字节实习期间的兴趣项目。项目的灵感起源于 Facebook 分享的他们 如何使用 stylex 来大幅度减少他们的 CSS 体积Linaria ,但那时 stylex 没有开源,同时我也对他们 react-native-like 的 API 形式也颇为不喜,就准备自己尝试写一个出来。心理有了这个想法之后就和当时的导师讨论了下,尽管技术方向和我们当时业务技术栈无关,导师还是鼓励我做一些技术探索。

一周的时间里,花了两三天思考技术方案,再花两三天编写整整个框架,最终在周日把原型整了出来。

完成原型后,我在公司内做了关于 broken-css 的技术分享,在内部论坛上发了一篇技术文章。自此,整个项目就基本上停止开发了。项目停止开发的次要原因是 JSS 和我们业务技术栈方向不契合。我们业务技术栈主要是 Vue ,在 Vue 中使用 JSS 没有什么难度,但是开发体验以及维护难度并不友好。试着体验阅读下以下代码。


<script lang="ts">
import Vue from 'vue'
import { css } from '@broken-css/core`

export default Vue.component('counter', {
	mainCls: css` height: 200px; `,
	countCls: css ` font-size: 20px; color: blue; `,
	data() {
		return {
			count: 0,
		}
	},
	methods: {
		inc() {
			this.count++
		}
	}
})

</script>

<template>
  <div :class="mainCls">
	  count: <span :class="countCls">{{ count }}</span>
		<button :onClick="inc" >Inc</button>
  </div>
</template>
复制代码

主要原因还是我个人对 broken-css 失去了兴趣,因为在实现 broken-css 后,我又接触了很多新的概念,我的脑海中多了很多新的可能性。比如说, Vue3 的 setup 语法使得 Vue 中使用 JSS 的开发体验同样友好, 如何在设计上同时对 Vue 和 React 友好。利用 CSS Variable 来扩展 broken-css 的动态性, Vue3 也做了类似的尝试。 VueConf 上 @HcySunYang 大佬的 “把 Vue SFC 编译到 X” 分享更是给了我很多编译方面的启示。

尽管这些东西同样可以迁移到 broken-css 上实现,但是我很满意 broken-css 现在的实现,尽管有着各种各样的缺点,但是它的确完成了当初设计它的目标。

由于一些原因,主要是代码写的太烂 :),我不想也不能把代码开源出来。如果你对 broken-css 感兴趣的话,欢迎在飞书、微信(yunfeihe)和邮件(i.heyunfei#gmail.com)上找我讨论。

或者,更直接一点,欢迎加入我所在的字节电商广告团队,我们现在实习、校招和社招都有大量的 HC ,有兴趣的同学可直接加我微信。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享