转载

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: CSSX

在把你的代码和实际样式应用到页面之间有一个库,它的主要责任就是创建一个虚拟的样式表,并将其和 <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: CSSX

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

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

JavaScript中的CSS: CSSX

所以,我要做的是看看如何把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: CSSX

示例的最终源代码可以在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

现在,使用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: CSSX

组件最后是要用来和用户交互的。如果我们点击链接,被点击的链接从左边向右边缩进一定的距离,并且给他设置一个背景颜色。在 _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相似的工作流和结果。如果你想了解它是如何工作的,可以看看这个 示例 。

原文  http://www.w3cplus.com/javascript/finally-css-javascript-meet-cssx.html
正文到此结束
Loading...