JavaScript中的CSS: CSSX

发布于 大漠

JavaScript是一种美妙的语言。它丰富、动态,和Web紧密耦合在一起。JavaScript的一切概念听起来不那么疯狂了。首先,我们在JavaScript中写后端逻辑,然后Facebook JSX的出现,把HTML也写在JavaScript中。那么为什么CSS不做同样的事情呢?

想像一下,一个Web组件都在一个.js文件中,这个文件包含了一切:HTML结构、CSS样式和一些逻辑。仍然会有基本的样式表,但动态的CSS将使用JavaScript来处理。现在这样做是能做的,并实现它的一个方法称为CSSX。CSSX是我用了近一个月的业余时间写的一个项目,它是具有挑战性的、有趣的,而且在这个项目中我学到很多新东西。它的最终结果就是变成一个工具,允许你在JavaScript中写CSS。

类似于JSX,CSSX也提供了封装好的API。开始在一个组件中能看到所有部分,这已经是一个很大的进步了。关注分离发展也有多年了,但Web也正在改变。通常我们的工作完全是在浏览中进行,这样Facebook提出的JSX方法就变得很有意义。当一切都在同一个地方的时候更有助于理解。我们也常常把部分的HTML和JavaScript结合在一起,通过混合在一起,只是做一些显式绑定。如此一来,HTML在JavaScript能正常工作,那么JavaScript也一定可以会CSS工作。

概念

我思考怎么把CSS放到JavaScript的时间可以追溯到2013年。当时我创建一个库,开始只是把它作为CSS预处理器,但后来我把它转换成一个客户端工具。这个想法其实很简单:把对象转换为有效的CSS,然后运用到Web页面。把JavaScript为CSS的服务之旅就这样开始了。虽然他们捆绑在一起,但你不需要管理外部样式表。当我尝试这种方法的时候,我碰到了两个问题:

  • 第一个问题文本没有样式(FOUT)。如果我们依靠JavaScript提供CSS,那么页面在得到样式之前,用户看到的内容是没有样式的,这样就会导致布局混乱和导致用户的体验非常的糟糕。
  • 第二个问题就是没有样式表。样式写在JavaScript中的应用示例有很多,但大多数都是内联样式。换句话说,他们只要是用来修改DOM元素的style。这样写是没问题,但我们并不需要给所有元素都写样式和改变自己的属性。另外不是所有属性样式都可以放到内联样式中,比如说媒体查询和伪类。

我的目标就变成了如何解决这两个问题,刚开始我整理了一个解决方案。下图演示了如何在JavaScript中写CSS:

JavaScript中的CSS

在把你的代码和实际样式应用到页面之间有一个库,它的主要责任就是创建一个虚拟的样式表,并将其和<style>标记关联起来。然后,它将提供一个PAI来管理CSS规则。每一个与JavaScript交互的样式表都将镜像映射到<style>标记中。使用这种方法,可以将要动态改变样式风格和JavaScript控件紧密的耦合在一起。你也不需要定义新的CSS类,因为你在运行的时候,就动态的生成了需要的CSS规则。

我更喜欢生成和注入的内联样式不是大规模的。这在技术上是容易实现的,但它只是不成规模。如果CSS在JavaScript中,我们能够控制它像一个真正的样式表,可以定义样式、添加、删除和更新样式,这些变化就像在一个应用到页面的静态样式文件中一样。

FOUT问题是一个取舍问题。问题是:我们应该把我们的CSS写在JavaScript还是**什么CSS可以当作JavaScript中的一部分?**当然,排版、网格和颜色都应该放在一个静态文件中,这样浏览器可以尽快的渲染。然而有很多的东西不需要立即就渲染,比如像.is-clicked.is-actived这样的状态类对应的样式。在单页面Web应用的世界中,一切由JavaScript写入的都可以使用JavaScript来写样式。因为它没有出现之前,我们有整个JavaScript包。在大型应用程序中,不同的块让它们尽可能的分开显得非常重要。单个组件的依赖关系越少越好。在客户端的观点中,HTML和CSS很难依赖JavaScript。如果没有他们,内容就不会显示。他们分组将会让项目的复杂性减少很多。

基于上述这些原因,我开始写CSSX客户端库。

CSSX简介

要让CSSX可用,需要先在你的页面中加载cssx.min.js文件和使用npm install cssx安装npm模块。如果你有build处理,那么你对npm包会有兴趣。

在Github上提供了一个在线演示的DEMO,在那里你可以看到CSSX的一些效果。

CSSX客户端在运行时需要CSSX的注入。后面我们看到基他模块可以支持CSS的语法糖。直到那时,我们才开始关注只提供JavaScript API。

这有一个非常简单的示例,注册一个样式表的规则:

var sheet = cssx();
sheet.add('p > a', {
  'font-size': '20px'
});

如果我们要在浏览器中运行,那么需要在文档的<head>添加一个新的<style>标签:

<style id="_cssx1" type="text/css">p > a{font-size:20px;}</style>

add方法接受一个选择器和作为对象的CSS属性。虽然他能工作,但他就是一个静态的声明。几乎没有使用JavaScript做任何处理,这样我们完全可以将这些样式添加到外部的CSS文件中。让我们把代码修改成:

var sheet = cssx();
var rule = sheet.add('p > a');
var setFontSize = function (size) {
  return { 'font-size': size + 'px' };
};

rule.update(setFontSize(20));
…
rule.update(setFontSize(24));

现在还有一件事。现在能够动态的更改font-size的值。上面代码的结果是这样的的:

p > a {
  font-size: 24px;
}

现在CSS在JavaScript写就变成了对象。使用JavaScript语言的特点构建它们。默认使用工厂函数和基类的扩展定义一个变量变得非常简单。封装、可重用性、模块化,这些特点都具有了。

CSSX有一个简单的API,主要是因为JavaScript很灵活。CSS就留给开发人员自己去组成,而公开的功能主要围绕实际生产的样式风格。例如,在写CSS时,倾向于成组去创建,比如布局结构、页头、侧边栏和页脚等。下面的代码演示了使用CSSX对象规则:

var sheet = cssx();

// `header` is a CSSX rule object
var header = sheet.add('.header');

header.descendant('nav', { margin: '10px' });
header.descendant('nav a', { float: 'left' });
header.descendant('.hero', { 'font-size': '3em' });

对应的结果:

.header nav {
  margin: 10px;
}
.header nav a {
  float: left;
}
.header .hero {
  font-size: 3em;
}

我们可以使用header.d来替代header.descendant。恼人的是写全.descendant需要时间,所以可以使用.d快捷方式来替代。

我们还有另一个类似于descendant的方法:nested。它不是改变选择器,而是CSSX定义的一个嵌套。例如下面的示例:

var smallScreen = sheet.add('@media all and (max-width: 320px)');
smallScreen.nested('body', { 'font-size': '10px' });

/* results in
@media all and (max-width: 320px) {
  body {
    font-size: 10px;
  }
}
*/

这个API可以用来创建媒体查询或@keyframes。在理论上,这个非常类似Sass的语法功能。还有,也可以使用.n这样的缩写来替代.nested

到目前为止,已经看到了如何生成有效的CSS,并且应用于页面。然而这样写样式需要很多时间,即使我们的代码具有良好的结构,它和写CSS是一样。

在JavaScript中写CSS的语法

正如前面所看到的,那样编写CSS并不好,主要是因为我们不得不用引号将每一个都括起来。我们可以做一些优化,比如说使用驼峰写法,为不的单位创建不同的帮手,但这样依旧让CSS不够简洁和简单。这样在JavaScript中写CSS也很容易导致意外的错误。好吧,那么我们想要的语法是什么?JSX创建,对吗?可是它没有。在JavaScript中没有实际的HTML标记,那这又发生了什么?其实是JSX在构建的时候编译了(更准确的说是transpile)。浏览器最后执行编译后的有效代码,如下图所示:

JavaScript中的CSS

当然,这样做也是需要付出代价的。我们在构建的过程中,需要依赖更多的配置和思考更多事情。但是话又说回来,这样更好的组织代码和让代码更具扩展性。JSX仅仅是通过管理HTML模板复杂性,让我们的生活看起来更美好而以。

但对于CSS,类似JSX正是我想要的。我开始研究Bable,因为它是JSX官方使用的编译器。它使用Bablon模块来解析代码并将代码转换到一个抽像的语法树(AST)。然后使用babel-generator 解析语法树,把它变成有效的JavaScript代码。这就Babel解析的JSX。它里面使用的一些ES6特性,浏览同样还不支持。

JavaScript中的CSS

所以,我要做的是看看如何把Babylon理解JSX的方式运用到CSS中。模块是这样的写的,因此它请允许外部扩展。事实上,几乎所有都可以改变。JSX是一个插件,我真想为此CSSX创建一个类似的插件。

我知道AST是非常重要,也非常有用,但我从示花时间去学习。它基本上是一个花时间阅读的过程,就是一个接一个代码块(或标记)。我们有一大堆的东西需要转换成一个个有意义的标记。如果是公认的,定个一个上下文和一个接一个从上向下解析,直到退出为止。当然,也有许多需要覆盖的情况。有趣的是我花了几周时间认真的阅读和理解,才知道我们不能扩展解析器。

在一开始的时候我就犯了一个致命的错误:要实现一个类似JSX的插件。真的无法告诉你写了多少次CSSX,但每一次我都无法完全覆盖CSS语法和打破JavaScript语法。后来我才意识到这其实和JSX完全不同。这才让我开始去扩展CSS的需要。测试驱动开发的方法非常有用。我应该提到Babylon已经做了超过2100次测试。这绝对是一个合理的考虑,考虑到模块理解这样一个丰富和动态的JavaScript语法所需要的时间与测试。

我必须做一些有趣的设计决策。首先我尝试着解析下面这样的代码:

var styles = {
  margin: 0,
  padding: 0
}

直到我决定运行插件在Babylon中做测试时一切都很顺利。解析器通常从这段代码中产生ObjectExpression节点,但是我在做别的事情,这才让我意识到这是CSSX。我有效的打破了JavaScript语法。没有办法找到,直到解析了整个区块,这也就是为什么我决定使用另一个语法:

var styles = cssx({
  margin: 0;
  padding: 0;
});

我们明确表示,我们写的是CSSX表达式。当我们有一个明确的接口之后,调整解析器就变得容易多了。JSX没有这个问题,因为HTML基本上没有接近JavaScript,所以还没有这样的冲突。

使用CSSX(...)符号表示在用CSSX,但后来意识到,可以将它换成<style>...</style>。这是一个廉价的开关,每次解析器在处理代码之前,只需要运行一个简单的正则来替换:

code = code.replace(/<style>/g, 'cssx(').replace(/<\/style>/g, ')');

这有且于我们像下面一样写代码:

var styles = <style>{
  margin: 0;
  padding: 0;
}</style>;

虽然写法不一样,但最终得到的结果是一样的。

开始在JavaScript中写CSS

假设我们有一个工具,了解CSSX,并且能产生适当的AST。下一步使用有效的JavaScript编译器。CSSX-Transpiler就是需要的编译器。我们仍然使用babel-generator,但只有Babel能理解的自定义的CSSX节点。另一个有用的是babel-types模块。有大量的实用功能,要是没有他们,我们的工作会变得很困难。

CSSX表达式的类型

我们来看几个简单的转换。

var styles = <style>{
  font-size: 20px;
  padding: 0;
}</style>;

转换后的代码如下:

var styles = (function () {
  var _2 = {};
  _2['padding'] = '0';
  _2['font-size'] = '20px';
  return _2;
}.apply(this));

这是第一个类型,制作了一个简单的对象。相当于上面的代码是这样的:

var styles = {
  'font-size': '20px',
  'padding': '0'
};

回忆一下上面介绍的,你将看到,这正是我们需要的CSSX客户端库。如果我们有很多的操作,那么最好是使用CSS的基本功能。

第二个表达式包含了更多的信息。它包括整个CSS规则:选择器和属性:

var sheet = <style>
  .header > nav {
    font-size: 20px;
    padding: 0;
  }
</style>;

转换后:

var sheet = (function () {
  var _2 = {};
  _2['padding'] = '0';
  _2['font-size'] = '20px';

  var _1 = cssx('_1');

  _1.add('.header > nav', _2);

  return _1;
}.apply(this));

请注意,我们定义了一个新的样式表cssx('_1')。需要说明一下,如果这段代码运行两次,不会创建一个额外的<style>标记。将会使用相同的一个,那是因为cssx()接收相同的ID(_1),所以返回的是相同的样式表对象。

如果我们增加更多的CSS规则,会看到更多的_1.add()行。

动态改变

如前面所述,在JavaScript中编写CSS的主要好处是获取广泛的工具,如定义一个函数,得到一个数字和输出一个字体大小的样式规则。我很难定义这些动态的语法部分,在JSX中使用括号容易包装代码,但在CSSX做这样的事情将是一件麻烦事,因为括号和其他东西易引起冲突。我们总是在定义CSS规则时使用它们。所以我最初使用的是``符号:

var size = 20;
var styles = <style>
  .header > nav {
    font-size: `size + 2`px;
    padding: 0;
  }
</style>;

对应的结果:

.header > nav {
  padding: 0;
  font-size: 22px;
}

我们可以使用动态的部分无处不在。

var size = 20;
var prop = 'size';
var selector = 'header';
var styles = <style>
  .`selector` > nav {
    font-`prop`: `size + 2`px;
    padding: 0;
  }
</style>;

类似于JSX,JavaSript代码转换为有效的代码:

var size = 20;
var prop = 'size';
var selector = 'header';
var styles = (function () {
  var _2 = {};
  _2['padding'] = '0';
  _2["font-" + prop] = size + 2 + "px";

  var _1 = cssx('_1');

  _1.add("." + selector + " > nav", _2);

  return _1;
}.apply(this));

我需要提到在transpiled中的self-invoking函数代码是需要保持在正确的域内。我们内部所谓的动态表达式的代码应该使用在正确的上下文。否则,可能会请求访问未定义的变量或访问全局变量。使用闭包的另一个原因是避免与应用程序的其他部分产生冲突。

得到一些反馈后,我决定支持两种动态表达式的语法规则。固定需要的代码尽量定义在CSSX内部,现在还可以使用{{...}}<%...%>

var size = 20;
var styles = <style>
  .header > nav {
    font-size: px;
    padding: 0;
  }
</style>;

示例展示

让我们来创建一个真实的东西,看看CSSX在实践中是如何工作的。因为CSSX由JSX启发而来,那我们将创建一个React导航菜单,最终效果是这样的:

JavaScript中的CSS

示例的最终源代码可以在Github上找到。简单的方式是你可以直接下载源文件和安装npm依赖包,然后运行npm run让JavaScript运行编译,在浏览中打开example/index.html文件,你就可以看到效果。

基本工作

我们已经证实CSSX并不意味着所有的CSS都可以写在JavaScript中。它应该只包含那些动态的部分。这个示例的基本CSS样式如下:

body {
  font-family: Helvetica, Tahoma;
  font-size: 18px;
}
ul {
  list-style: none;
  max-width: 200px;
}
ul, li {
  margin: 0;
  padding: 0;
}
li {
  margin-bottom: 4px;
}

我们的导航由一个无序列表项组成,每个li包含一个<a>标记,表示是可点击区域。

导航组件

如果你不熟悉React也不用担心。相同的代码也可以应用在其他的框架。重要的是我们理解如何使用CSSX来写导航的样式风格和定义他们的行为。

要做的第一件事就,就是在页面上呈现这些链接。假设列表项目中有一个items属性。我们可以使用<li>标记,做一个循环:

class Navigation extends React.Component {
  constructor(props) {
    super(props);
    this.state = { color: '#2276BF' };
  }
  componentWillMount() {
    // Create our style sheet here
  }
  render() {
    return <ul>{ this._getItems() }</ul>;
  }
  _getItems() {
    return this.props.items.map((item, i) => {
      return (
        <li key={ i }>
          <a className='btn' onClick={ this._handleClick.bind(this, i) }>
            { item }
          </a>
        </li>
      )
    })
  }
  _handleClick(index) {
    // Handle link's click here
  }
}

我们在组件状态上设置一个color变量,稍后要使用它。因为在运行时生成的样式,可以进一步通过编写一个函数返回颜色。注意,在JavaScript中写CSS,我们不再生生一个静态的CSS。

事实上,组件准备渲染。

const ITEMS = [
  'React',
  'Angular',
  'Vue',
  'Ember',
  'Knockout',
  'Vanilla'
];

ReactDOM.render(
  <Navigation items={ ITEMS } />,
  document.querySelector('body')
);

浏览器只显示ITEMS。在静态的CSS中我们已经对无序列表ul的默认样式做了处理,所以你看到的效果是这样的:

JavaScript中的CSS

现在,使用CSSX定义一些初步的样式,让其看来起更像列表。这里创建了一个componentWillMount函数,因为页面组件触发之前的方法。

componentWillMount() {
  var color = this.state.color;
  <style>
    li {
      padding-left: 0;
      (w)transition: padding-left 300ms ease;
    }
    .btn {
      display: block;
      cursor: pointer;
      padding: 0.6em 1em;
      border-bottom: solid 2px `color`;
      border-radius: 6px;        
      background-color: `shadeColor(color, 0.5)`;
      (w)transition: background-color 400ms ease;
    }
    .btn:hover {
      background-color: `shadeColor(color, 0.2)`;
    }
  </style>;
}

注意,现在使用CSSX表达式定义了底部边框的颜色和背景色。shadeColor是一个辅助函数,它接受一个十六进制格式颜色和第二个参数设置颜色的透明度(介于1-1)。这并不是真正重要的。这段代码的结果是一个新的样式表注入到了页面的<head>当中。下面CSS真正我们需要的:

li {
  padding-left: 0;
  transition: padding-left 300ms ease;
  -webkit-transition: padding-left 300ms ease;
}
.btn {
  background-color: #91bbdf;
  border-radius: 6px;
  border-bottom: solid 2px #2276BF;
  padding: 0.6em 1em;
  cursor: pointer;
  display: block;
  transition: background-color 400ms ease;
  -webkit-transition: background-color 400ms ease;
}
.btn:hover {
  background-color: #4e91cc;
}

属性前面的w是用来生成浏览器对应的私有属性。

现在我们的导航看起来不再是简单的文本:

JavaScript中的CSS

组件最后是要用来和用户交互的。如果我们点击链接,被点击的链接从左边向右边缩进一定的距离,并且给他设置一个背景颜色。在_handleClick函数中,我们会收到点击项的索引值,因此,可以使用CSS的:nth-child选择器来写样式:

_handleClick(index) {
  <style>
    li:nth-child({{ index + 1 }}) {
      padding-left: 2em;
    }
    li:nth-child({{ index + 1 }}) .btn {
      background-color: {{ this.state.color }};
    }
  </style>;
}

虽然能工作,但还存在一点问题。点击其他项目,那么前一个被点击的项目没有恢复到初始状态。例如,我们的文档可能包含:

li:nth-child(4) {
  padding-left: 2em;
}
li:nth-child(4) .btn {
  background-color: #2276BF;
}
li:nth-child(3) {
  padding-left: 2em;
}
li:nth-child(3) .btn {
  background-color: #2276BF;
}

所以,必须清楚点击项之前的样式。

var stylesheet, row;

// creating a new style sheet
stylesheet = cssx('selected');

// clearing all the styles
stylesheet.clear();

// adding the styles
stylesheet.add(
  <style>
  li:nth-child({{ index + 1 }}) {
    padding-left: 2em;
  }
  li:nth-child({{ index + 1 }}) .btn {
    background-color: {{ this.state.color }};
  }
  </style>
);

现在变成这样:

cssx('selected')
  .clear()
  .add(
    <style>
      li:nth-child({{ index + 1 }}) {
        padding-left: 2em;
      }
      li:nth-child({{ index + 1 }}) .btn {
        background-color: {{ this.state.color }};
      }
    </style>
  );

注意,指一个ID设置selected样式。这是很重要的;否则,每次都得到不同的样式表。

这样一来就可以看到前面展示的GIF动画展示的导航效果。

有这样的一个简单的示例,我们可以了解到CSSX的一些好处:

  • 不需要额外处理一些CSS类名
  • 没有和DOM做交互,不需要添加或删除CSS类
  • 真正的动态编写CSS,和组件的逻辑紧密耦合在一起

总结

把HTML和CSS写在JavaScript中可能看起来很奇怪,但事实是,我们多年来一直这么做。我们预编译模板写在JavaScript中。形成的HTML字符串和使用的内联样式也是写在JavaScript中。所以,为什么不直接使用相同的语法呢?

去年,我一直在使用React,我可以说JSX并不坏。事实上,它可以提高可维护性和缩短一个新项目的开发周期。

我仍然尝试CSSX。我看到了和JSX相似的工作流和结果。如果你想了解它是如何工作的,可以看看这个示例

相关链接

扩展阅读

本文根据@Krasimir Tsonev的《Finally, CSS In JavaScript! Meet CSSX》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://www.smashingmagazine.com/2016/04/finally-css-javascript-meet-cssx

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。中国Drupal社区核心成员之一。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/finally-css-javascript-meet-cssx.htmlDunk Low Pro Sb Loden Dark Black Loden 304292-301