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的服务之旅就这样开始了。虽然他们捆绑在一起,但你不需要管理外部样式表。当我尝试这种方法的时候,我碰到了两个问题:
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.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是一样。
正如前面所看到的,那样编写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>;
虽然写法不一样,但最终得到的结果是一样的。
假设我们有一个工具,了解CSSX,并且能产生适当的AST。下一步使用有效的JavaScript编译器。 CSSX-Transpiler 就是需要的编译器。我们仍然使用 babel-generator
,但只有Babel能理解的自定义的CSSX节点。另一个有用的是 babel-types 模块。有大量的实用功能,要是没有他们,我们的工作会变得很困难。
我们来看几个简单的转换。
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 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的一些好处:
把HTML和CSS写在JavaScript中可能看起来很奇怪,但事实是,我们多年来一直这么做。我们预编译模板写在JavaScript中。形成的HTML字符串和使用的内联样式也是写在JavaScript中。所以,为什么不直接使用相同的语法呢?
去年,我一直在使用React,我可以说JSX并不坏。事实上,它可以提高可维护性和缩短一个新项目的开发周期。
我仍然尝试CSSX。我看到了和JSX相似的工作流和结果。如果你想了解它是如何工作的,可以看看这个 示例 。