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:
在把你的代码和实际样式应用到页面之间有一个库,它的主要责任就是创建一个虚拟的样式表,并将其和<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)。浏览器最后执行编译后的有效代码,如下图所示:
当然,这样做也是需要付出代价的。我们在构建的过程中,需要依赖更多的配置和思考更多事情。但是话又说回来,这样更好的组织代码和让代码更具扩展性。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 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相似的工作流和结果。如果你想了解它是如何工作的,可以看看这个示例。
相关链接
- CSSX language
- CSSX Client-side library
- CSSX-transpiler
- gulp-cssx
- cssx-loader
- Using Vanilla CSS in React Applications
- CSSX playground
扩展阅读
- Write Your CSS with JavaScript
- CSS in JS in CSS
- CSS In JS: It’s a Trap!
- Modularise CSS the React way
- Please, Please Don’t Use “CSS in JS”
本文根据@Krasimir Tsonev的《Finally, CSS In JavaScript! Meet CSSX》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://www.smashingmagazine.com/2016/04/finally-css-javascript-meet-cssx。
如需转载,烦请注明出处:http://www.w3cplus.com/javascript/finally-css-javascript-meet-cssx.html