只听说过CSS in JS,怎么还有JS in CSS?

Souleigh ✨ 等级 1034 0 0

CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css.less之类的文件。将css放在js中使我们更方便的使用js的变量模块化tree-shaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式更容易追溯依赖关系生成唯一的选择器来锁定作用域。尽管CSS in JS不是一个很新的技术,但国内的普及程度并不高。由于Vue和Angular都有属于他们自己的一套定义样式的方案,React本身也没有管用户怎样定义组件的样式[1],所以CSS in JS在React社区的热度比较高。

目前为止实现CSS in JS的第三方库有很多:(http://michelebertoli.github.io/css-in-js/)。像JSS\[2\]、styled-components\[3\]等。在这里我们就不展开赘述了(相关链接已放在下方),这篇文章的重点是`JS in CSS`😀。

JS in CSS又是什么

在上面我们提到CSS in JS就是把CSS写在JavaScript中,那么JS in CSS我们可以推断出就是可以在CSS中使用JavaScript脚本,如下所示。可以在CSS中编写Paint API的功能。还可以访问:ctx,geom。甚至我们还可以编写自己的css自定义属性等。这些功能的实现都基于CSS Houdini[4]。

`.el {  
  --color: cyan;  
  --multiplier: 0.24;  
  --pad: 30;  
  --slant: 20;  
  --background-canvas: (ctx, geom) => {  
    let multiplier = var(--multiplier);  
    let c = `var(--color)`;  
    let pad = var(--pad);  
    let slant = var(--slant);  

    ctx.moveTo(0, 0);  
    ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);  
    ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);  
    ctx.lineTo(0, geom.height);  
    ctx.fillStyle = c;  
    ctx.fill();  
  };  
  background: paint(background-canvas);  
  transition: --multiplier .4s;  
}  
.el:hover {  
  --multiplier: 1;  
}  

Houdini 解决了什么问题

CSS 与 JS的标准制定流程对比

在如今的Web开发中,JavaScript几乎占据了项目代码的大部分。我们可以在项目开发中使用ES 2020、ES2021、甚至提案中的新特性(如:Decorator[5]),即使浏览器尚未支持,也可以编写Polyfill或使用Babel之类的工具进行转译,让我们可以将最新的特性应用到生产环境中(如下图所示)。

只听说过CSS in JS,怎么还有JS in CSS?

JavaScript标准制定流程.png

而CSS就不同了,除了制定CSS标准规范所需的时间外,各家浏览器的版本、实战进度差异更是旷日持久(如下图所示),最多利用PostCSS、Sass等工具來帮我们转译出浏览器能接受的CSS。开发者们能操作的就是通过JS去控制DOMCSSOM来影响页面的变化,但是对于接下來的LayoutPaintComposite就几乎没有控制权了。为了解决上述问题,为了让CSS的魔力不在受到浏览器的限制,Houdini就此诞生。

只听说过CSS in JS,怎么还有JS in CSS?

CSS 标准制定流程.png

CSS Polyfill

我们上文中提到JavaScript中进入提案中的特性我们可以编写Polyfill,只需要很短的时间就可以讲新特性投入到生产环境中。这时,脑海中闪现出的第一个想法就是CSS Polyfill,只要CSS的Polyfill 足够强大,CSS或许也能有JavaScript一样的发展速度,令人可悲的是编写CSS Polyfill异常的困难,并且大多数情况下无法在不破坏性能的情况下进行。这是因为JavaScript是一门动态脚本语言[6]。它带来了极强的扩展性,正是因为这样,我们可以很轻松使用JavaScript做出JavaScript的Polyfill。但是CSS不是动态的,在某些场景下,我们可以在编译时将一种形式的CSS的转换成另一种(如PostCSS[7])。如果你的Polyfill依赖于DOM结构或者某一个元素的布局、定位等,那么我们的Polyfill就无法编译时执行,而需要在浏览器中运行了。不幸的是,在浏览器中实现这种方案非常不容易。

只听说过CSS in JS,怎么还有JS in CSS?

页面渲染流程.png

如上图所示,是从浏览器获取到HTML到渲染在屏幕上的全过程,我们可以看到只有带颜色(粉色、蓝色)的部分是JavaScript可以控制的环节。首先我们根本无法控制浏览器解析HTML与CSS并将其转化为DOMCSSOM的过程,以及Cascade,Layout,Paint,Composite我们也无能为力。整个过程中我们唯一完全可控制的就是DOM,另外CSSOM部分可控。

CSS Houdini草案中提到,这种程度的暴露是不确定的、兼容性不稳定的以及缺乏对关键特性的支持的。比如,在浏览器中的 CSSOM 是不会告诉我们它是如何处理跨域的样式表,而且对于浏览器无法解析的 CSS 语句它的处理方式就是不解析了,也就是说——如果我们要用 CSS polyfill 让浏览器去支持它尚且不支持的属性,那就不能在 CSSOM 这个环节做,我们只能遍历一遍DOM,找到 <style><link rel="stylesheet"> 标签,获取其中的 CSS 样式、解析、重写,最后再加回 DOM 树中。令人尴尬的是,这样DOM树全部刷新了,会导致页面的重新渲染(如下如所示)。

只听说过CSS in JS,怎么还有JS in CSS?

即便如此,有的人可能会说:“除了这种方法,我们也别无选择,更何况对网站的性能也不会造成很大的影响”。那么对于部分网站是这样的。但如果我们的Polyfill是需要对可交互的页面呢?例如scrollresizemousemovekeyup等等,这些事件随时会被触发,那么意味着随时都会导致页面的重新渲染,交互不会像原本那样丝滑,甚至导致页面崩溃,对用户的体验也极其不好。

综上所述,如果我们想让浏览器解析它不认识的样式(低版本浏览器使用grid布局),然而渲染流程我们无法介入,我们也只能通过手动更新DOM的方式,这样会带来很多问题,Houdini的出现正是致力于解决他们。

Houdini API

Houdini是一组底层API,它公开了CSS引擎的各个部分,如下图所示展示了每个环节对应的新API(灰色部分各大浏览器还未实现),从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。Houdini是一群来自Mozilla,Apple,Opera,Microsoft,HP,Intel和Google的工程师组成的工作小组设计而成的。它们使开发者可以直接访问CSS对象模型(CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。

只听说过CSS in JS,怎么还有JS in CSS?

CSS Houdini-API

Properties & Values API

尽管当前已经有了CSS变量,可以让开发者控制属性值,但是无法约束类型或者更严格的定义,CSS Houdini新的API,我们可以扩展css的变量,我们可以定义CSS变量的类型,初始值,继承。它是css变量更强大灵活。

CSS变量现状:

.dom {  
  --my-color: green;  
  --my-color: url('not-a-color'); // 它并不知道当前的变量类型  
  color: var(--my-color);  
}  

Houdini提供了两种自定义属性的注册方式,分别是在js和css中。

CSS.registerProperty({  
  name: '--my-prop', // String 自定义属性名  
  syntax: '<color>', // String 如何去解析当前的属性,即属性类型,默认 *  
  inherits: false, // Boolean 如果是true,子节点将会继承  
  initialValue: '#c0ffee', // String 属性点初始值  
});  

我们还可以在css中注册,也可以达到上面的效果

@property --my-prop {  
  syntax: '<color>';  
  inherits: false;  
  initial-value: #c0ffee;  
}  

这个API中最令人振奋人心的功能是自定义属性上添加动画,像这样:transition: --multiplier 0.4s;,这个功能我们在前面介绍什么是js in css那个demo[8]用使用过。我们还可以使用+使syntax属性支持一个或多个类型,也可以使用|来分割。更多syntax属性值:

属性值 描述
<length> 长度值
<number> 数字
<percentage> 百分比
<length-percentage> 长度或百分比,calc将长度和百分比组成的表达式
<color> 颜色
<image> 图像
<url> 网址
<integer> 整数
<angle> 角度
<time> 时间
<resolution> 分辨率
<transform-list> 转换函数
<custom-ident> ident

Worklets

Worklets是渲染引擎的扩展,从概念上来讲它类似于Web Workers[9],但有几个重要的区别:

  1. 设计为并行,每个Worklets必须始终有两个或更多的实例,它们中的任何一个都可以在被调用时运行

  2. 作用域较小,限制不能访问全局作用域的API(Worklet的函数除外)

  3. 渲染引擎会在需要的时候调用他们,而不是我们手动调用

Worklet是一个JavaScript模块,通过调用worklet的addModule方法(它是个Promise)来添加。比如registerLayout, registerPaint, registerAnimator 我们都需要放在Worklet中

//加载单个  
await demoWorklet.addModule('path/to/script.js');  

// 一次性加载多个worklet  
Promise.all([  
  demoWorklet1.addModule('script1.js'),  
  demoWorklet2.addModule('script2.js'),  
]).then(results => {});  

registerDemoWorklet('name', class {  

  // 每个Worklet可以定义要使用的不同函数  
  // 他们将由渲染引擎在需要时调用  
  process(arg) {  
    return !arg;  
  }  
});  

Worklets的生命周期

只听说过CSS in JS,怎么还有JS in CSS?

Worklets lifecycle

  1. Worklet的生命周期从渲染引擎内开始

  2. 对于JavaScript,渲染引擎启动JavaScript主线程

  3. 然后他将启动多个worklet进程,并且可以运行。这些进程理想情况下是独立于主线程的线程,这样就不会阻塞主线程(但它们也不需要阻塞)

  4. 然后在主线程中加载我们浏览器的JavaScript

  5. 该JavaScript调用 worklet.addModule 并异步加载一个worklet

  6. 加载后,将worklet加载到两个或多个可用的worklet流程中

  7. 当需要时,渲染引擎将通过从加载的Worklet中调用适当的处理函数来执行Worklet。该调用可以针对任何并行的Worklet实例。

Typed OM

Typed OM是对现有的CSSOM的扩展,并实现 Parsing APIProperties & Values API相关的特性。它将css值转化为有意义类型的JavaScript的对象,而不是像现在的字符串。如果我们尝试将字符串类型的值转化为有意义的类型并返回可能会有很大的性能开销,因此这个API可以让我们更高效的使用CSS的值。

现在读取CSS值增加了新的基类CSSStyleValue,他有许多的子类可以更加精准的描述css值的类型:

子类 描述
CSSKeywordValue CSS关键字和其他标识符(如inherit或grid)
CSSPositionValue 位置信息 (x,y)
CSSImageValue 表示图像的值属性的对象
CSSUnitValue 表示为具有单个单位的单个值(例如50px),也可以表示为没有单位的单个值或百分比
CSSMathValue 比较复杂的数值,比如有calc,min和max。这包括子类 CSSMathSum, CSSMathProduct, CSSMathMin, CSSMathMax, CSSMathNegateCSSMathInvert
CSSTransformValue CSS transforms组成的CSSTransformComponent列表,其中包括CSSTranslate, CSSRotate, CSSScale, CSSSkew, CSSSkewX, CSSSkewY, CSSPerspectiveCSSMatrixComponent

使用Typed OM主要有两种方法:

  1. 通过attributeStyleMap设置和获取有类型的行间样式

  2. 通过computedStyleMap获取元素完整的Typed OM样式

使用attributeStyleMap设置并获取

myElement.attributeStyleMap.set('font-size', CSS.em(2));  
myElement.attributeStyleMap.get('font-size'); // CSSUnitValue { value: 2, unit: 'em' }  

myElement.attributeStyleMap.set('opacity', CSS.number(.5));  
myElement.attributeStyleMap.get('opacity'); // CSSUnitValue { value: 0.5, unit: 'number' };  

在线demo[10]

使用computedStyleMap

.foo {  
  transform: translateX(1em) rotate(50deg) skewX(10deg);  
  vertical-align: baseline;  
  width: calc(100% - 3em);  
}  
const cs = document.querySelector('.foo').computedStyleMap();  

cs.get('vertical-align');  
// CSSKeywordValue {  
//  value: 'baseline',  
// }  

cs.get('width');  
// CSSMathSum {  
//   operator: 'sum',  
//   length: 2,  
//   values: CSSNumericArray {  
//     0: CSSUnitValue { value: -90, unit: 'px' },  
//     1: CSSUnitValue { value: 100, unit: 'percent' },  
//   },  
// }  

cs.get('transform');  
// CSSTransformValue {  
//   is2d: true,  
//   length: 3,  
//   0: CSSTranslate {  
//     is2d: true,  
//     x: CSSUnitValue { value: 20, unit: 'px' },  
//     y: CSSUnitValue { value: 0, unit: 'px' },  
//     z: CSSUnitValue { value: 0, unit: 'px' },  
//   },  
//   1: CSSRotate {...},  
//   2: CSSSkewX {...},  
// }  

Layout API

开发者可以通过这个API实现自己的布局算法,我们可以像原生css一样使用我们自定义的布局(像display:flex, display:table)。在 Masonry layout library[11] 上我们可以看到开发者们是有多想实现各种各样的复杂布局,其中一些布局光靠 CSS 是不行的。虽然这些布局会让人耳目一新印象深刻,但是它们的页面性能往往都很差,在一些低端设备上性能问题犹为明显。

CSS Layout API 暴露了一个registerLayout方法给开发者,接收一个布局名(layout name)作为后面在 CSS中使用的属性值,还有一个包含有这个布局逻辑的JavaScript类。

my-div {  
  display: layout(my-layout);  
}  
// layout-worklet.js  
registerLayout('my-layout', class {  
  static get inputProperties() { return ['--foo']; }  

  static get childrenInputProperties() { return ['--bar']; }  

  async intrinsicSizes(children, edges, styleMap) {}  

  async layout(children, edges, constraints, styleMap) {}  
});  
await CSS.layoutWorklet.addModule('layout-worklet.js');  

目前浏览器大部分还不支持

Painting API

我们可以在CSS background-image中使用它,我们可以使用Canvas 2d上下文,根据元素的大小控制图像,还可以使用自定义属性。

await CSS.paintWorklet.addModule('paint-worklet.js');  
registerPaint('sample-paint', class {  
  static get inputProperties() { return ['--foo']; }  

  static get inputArguments() { return ['<color>']; }  

  static get contextOptions() { return {alpha: true}; }  

  paint(ctx, size, props, args) { }  
});  

Animation API

这个API让我们可以控制基于用户输入的关键帧动画,并且以非阻塞的方式。还能更改一个 DOM 元素的属性,不过是不会引起渲染引擎重新计算布局或者样式的属性,比如 transform、opacity 或者滚动条位置(scroll offset)。Animation API的使用方式与 Paint APILayout API略有不同我们还需要通过new一个WorkletAnimation来注册worklet。

// animation-worklet.js  
registerAnimator('sample-animator', class {  
  constructor(options) {  
  }  
  animate(currentTime, effect) {  
    effect.localTime = currentTime;  
  }  
});  
await CSS.animationWorklet.addModule('animation-worklet.js');  

// 需要添加动画的元素  
const elem = document.querySelector('#my-elem');  
const scrollSource = document.scrollingElement;  
const timeRange = 1000;  
const scrollTimeline = new ScrollTimeline({  
  scrollSource,  
  timeRange,  
});  

const effectKeyframes = new KeyframeEffect(  
  elem,  
  // 动画需要绑定的关键帧  
  [  
    {transform: 'scale(1)'},  
    {transform: 'scale(.25)'},  
    {transform: 'scale(1)'}  
  ],  
  {  
    duration: timeRange,  
  },  
);  
new WorkletAnimation(  
  'sample-animator',  
  effectKeyframes,  
  scrollTimeline,  
  {},  
).play();  

关于此API的更多内容:(https://github.com/w3c/css-houdini-drafts/tree/main/css-animation-worklet-1)

Parser API

允许开发者自由扩展 CSS 词法分析器。

解析规则:

const background = window.cssParse.rule("background: green");  
console.log(background.styleMap.get("background").value) // "green"  

const styles = window.cssParse.ruleSet(".foo { background: green; margin: 5px; }");  
console.log(styles.length) // 5  
console.log(styles[0].styleMap.get("margin-top").value) // 5  
console.log(styles[0].styleMap.get("margin-top").type) // "px"  

解析CSS:

const style = fetch("style.css")  
        .then(response => CSS.parseStylesheet(response.body));  
style.then(console.log);  

Font Metrics API

它将提供一些方法来测量在屏幕上呈现的文本元素的尺寸,将允许开发者控制文本元素在屏幕上呈现的方式。使用当前功能很难或无法测量这些值,因此该API将使开发者可以更轻松地创建与文本和字体相关的CSS特性。例如:

  • flex布局: align-items baseline特性。需要知道每一个flex盒子中第一个元素的基线位置。

  • 首字母: 需要知道每个字母的基线高度和字母最大的高度,以及换行内容的基线长度。

  • 单个字形的前进和后退。

  • 换行: 需要访问字体数据,文本的所有样式输入以及布局信息(可用的段落长度等)。

  • 元素中的每一个line boxes都需要一个基线。(line boxes代表包含众多inline boxes的这行)

Houdini 目前进展

只听说过CSS in JS,怎么还有JS in CSS?

Is Houdini ready yet

(https://ishoudinireadyyet.com/)

Houdini 的蓝图

了解到这里,部分开发者可能会说:“我不需要这些花里胡哨的技术,并不能带收益。我只想简简单单的写几个页面,做做普通的Web App,并不想试图干预浏览器的渲染过程从而实现一些实验性或炫酷的功能。”如果这样想的话,我们不妨退一步再去思考。回忆下最近做过的项目,用于实现页面效果所使用到的技术,grid布局方式在考虑兼容老版本浏览器时也不得不放弃。我们想控制浏览器渲染页面的过程并不是仅仅为了炫技,更多的是为了帮助开发者们解决以下两个问题:

  1. 统一各大浏览器的行为

  2. JavaScript一样,在推出新的特性时,我们可以通过Polyfill的形式快速的投入生产环境中。

几年过后再回眸,当主流浏览器完全支持Houdini的时候。我们可以在浏览器上随心所欲的使用任何CSS属性,并且他们都能完美支持。像今天的grid布局在旧版本浏览器支持的并不友好的这类问题,那时我们只需要安装对应的Polyfill就能解决类似的问题。

如在文中发现有误之处,欢迎反馈、纠正。

写在最后

最后,推荐一套TS全系列的教程吧。近期在提升TS,收藏了一套很不错的教程,无偿分享给xdm https://www.yidengxuetang.com/pub-page/index.html 点击文章最后的【阅读原文】直达

Reference

[1] 组件的样式:https://reactjs.org/docs/faq-styling.html

[2] JSS:https://cssinjs.org/

[3] styled-components:https://styled-components.com/

[4] CSS Houdini:https://developer.mozilla.org/en-US/docs/Web/Houdini

[5] Decorator:https://github.com/tc39/proposal-decorators

[6] 动态脚本语言:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript

[7] PostCSS:https://github.com/postcss/postcss

[8] demo:https://codepen.io/xqy279/project/editor/AEbOBq#

[9] Web Workers:https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API

[10] 在线demo:https://codepen.io/xqy279/pen/wvgeNMM

[11] Masonry layout library:https://github.com/desandro/masonry

收藏
评论区

相关推荐

2. web前端开发分享-css,js进阶篇
2. web前端开发分享css,js进阶篇 一,css进阶篇:   等css哪些事儿看了两三遍之后,需要对看过的知识综合应用,这时候需要大量的实践经验, 简单的想法:把qq首页全屏另存为jpg然后
30个前端开发人员必备的顶级工具
在本文中,我为前端Web开发人员汇总了30种顶级工具,从代码编辑器和代码游乐场到CSS生成器,JS库等等。 (https://imghelloworld.osscnbeijing.aliyuncs.com/2e7966318084a45d05a0926cbd749a02.png) 目录 CSS代码生成器 CSS3 Generator
VUE3(七)vue项目抽离.vue文件中的js、css代码
平常再做开发的时候,一般情况下不会将html,js,css代码写到一个文件中。基本上都会写在各自对应的文件中,然后再引入即可。那么在VUE中我们如何抽离vue文件中的js,与css代码呢? 1:抽离javascriptHome.vue<template <div <div :style"{ padding: '24px', back
只听说过CSS in JS,怎么还有JS in CSS?
CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css、.less之类的文件。将css放在js中使我们更方便的使用js的变量、模块化、treeshaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式,更容易追溯依赖关
CSS扩展语言
CSS没有变量?不要诬蔑我们大CSS好不?就像ES一样,你说JS没有模块,ES6给你整一个出来,而在这之前就已经有相关的库出来了。CSS也一样,你说CSS没有变量,CSS3就给你一个var(),变量你值得拥有! // 语法 // <custom-preperty-name> 自定义属性名 // <declaration-value>
JS和CSS加载(渲染)机制不同
一、结论 ==== CSS可以在页面加载完成后随时渲染。举个例子:通过js给某个元素加一个id或者css,只要这个id或者css有对应的样式,此元素的样式就会自动生效。 JS不可以在页面加载完成后生效。最明显的例子就是使用EasyUI的时候,iframe中哪些样式无效(EasyUi是依靠JS进行样式处理的,所以无法运行JS,那么样式也就无法设置。简单点说
SASS入门教程及用法指南
作为前端开发人员,你肯定对css很熟悉,但是你知道css可以自定义吗?大家都知道,js中可以自定义变量,css仅仅是一个标记语言,不是编程语言,因此不可以自定义变量,不可以引用等等。面对这些问题,我们现在来引进一个[SASS](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fwww.haoro
23、Django实战第23天:视频播放页面
打开素材course-play.html,会发现播放页面处了包含播放器,其他和“章节”页面一样。 1、把course-play.html复制到template目录下 2、把下面两段代码拷贝出来 <link rel="stylesheet" type="text/css" href="../css/video-js.min.css">
Array.prototype.slice.call()的理解
最近在看廖雪峰的JS课程,浏览器中的操作DOM的那一章,有这样一道题。 * JavaScript * Swift * HTML * ANSI C * CSS * DirectX <!-- HTML结构 --> <ul id="test-list"> <li>JavaScript</li>
Bootstrap引入在线路径
  如果不想麻烦地安装Bootstrap,可以通过引入在线路径的方式使用bootstrap 样式表文件 <meta rel="stylesheet" src="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> 核心JS文件
Go 1.16 embed特性的简单应用
项目结构如下: └─ui └─embed_ui.go └─dist └─index.html └─static ├─css └─ ... ├─fonts
JavaScript 从select表中获取数据在表格中添加行
<!DOCTYPE html> <html> <head lang="en">     <meta charset="UTF-8">     <title></title>     <link rel="stylesheet" href="css/mian.css"/>     <script src=
JavaScript基础知识
**1:从一个界面跳转到其他界面** window.location.href="b.html" **2:判断变量是否为空** typeof obj == 'undefined' || obj == null || obj == "" **3:html引用公用的css和js** <script type="text/javascript" src=
Javascript基础知识学习(三)
**前言:** javascript是一种轻量的、动态的脚本语言,我们为什么要使用javascript ?对于一个网页的设计,.html用来放置网页的内容,.css则用来设计网页的样式和布局,那么.js它主要是使网页能够产生交互,意思就是能够通过代码动态的修改HTML、操作CSS、响应事件、获取用户计算机的相关信息等。javascript不是所有的浏览器
Subime使用笔记(持续跟进)
Subime使用笔记 ---------- ### 常用插件 * Package Control * Emmet * CSS Format * 格式化CSS代码插件 * DocBlockr * 快速添加代码注释插件 * Side Bar * 增强侧边