微信号:w3cplus_12

介绍:W3CPLUS是一个前端爱好者的家园,W3CPLUS努力打造最优秀的web 前端学习的站点.

JavaScript中的CSS: CSSX

2016-04-22 01:22 大漠

想像一下,一个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:

在把你的代码和实际样式应用到页面之间有一个库,它的主要责任就是创建一个虚拟的样式表,并将其和<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 objectvar 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)。浏览器最后执行编译后的有效代码,如下图所示:

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

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

所以,我要做的是看看如何把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导航菜单,最终效果是这样的:

示例的最终源代码可以在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的默认样式做了处理,所以你看到的效果是这样的:

现在,使用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是用来生成浏览器对应的私有属性。

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

组件最后是要用来和用户交互的。如果我们点击链接,被点击的链接从左边向右边缩进一定的距离,并且给他设置一个背景颜色。在_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 sheetstylesheet = cssx('selected');// clearing all the stylesstylesheet.clear();// adding the stylesstylesheet.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.com

————————————

记述前端那些事,引领web前沿


长按二维码,关注W3cplus



 
W3cplus 更多文章 关于CSS的will-change属性的介绍 宝贝,520可以不送礼物了吧 CSS3的Clip-path属性实战 CSS3 动画案例 Sass基础入门
猜您喜欢 【程序员勇士奖】从今天起不再做一条咸鱼—1024程序员节 iOS病毒XcodeGhost批量检测工具(检测ipa文件) DBA+社群携手SDOUG举办2016迎新春技术分享活动 还屌丝逆袭呢?从来没有一个屌丝能够逆袭!屌丝不能逆袭,人生只能建设!