基于浏览器环境的css元数据解析方案研究

福州白癜风医院 http://pf.39.net/bdfyy/bjzkbdfyy/140721/4429412.html
序言

目前市面上有很多页面搭建方案,其中一种是基于运行时的lowcode/nocode搭建平台,主要是面向运营、产品及部分开发人员;另外一种则是基于DSL或代码,将可视化能力作为代码编写的辅助能力集成进现有项目中,主要是面向开发人员。AUX辅助工具(下文简称AUX)属于后者。

AUX是架构前端内部研发的一套可视化开发工具,旨在为前后端研发同学提供页面开发的可视化交互和代码生成能力。它侵入性弱,直接对代码进行修改,因此在兼顾易用性的同时产生的代码具有很强的可维护性和二次开发能力。同时,也由于完全基于代码本身,和其他代码平台不同,除了代码没有更加详细的元数据,所以AUX的很大一部分工作,在于努力实现基础的从运行时到编译时的反向处理能力,这样才能保证用户在浏览器中的各种操作能够被分解,并还原到真正的源代码中。

AUX工具提供了很多有趣的功能,其中一个是关于在浏览器中对css样式进行可视化编辑。可视化样式编辑的目的是让开发者能够在开发环境的浏览器端通过编辑器修改页面中的css样式,并实时更新页面中的样式渲染结果,最终在完成编辑后能够直接生成代码并写入用户的项目中。其中,怎么样在浏览器中获取到css代码的元数据,就是首先需要解决的问题,本文将主要围绕这个问题进行讨论。

在浏览器中向用户展现css数据

我们首先明确这个问题的产生原因。在AUX中,css编辑能力的使用逻辑如下:

用户在前端页面中点击想要编辑的元素,点击目标元素后显示选中框;右侧弹出auxcss属性编辑面板,并展示当前元素的样式信息,在面板中可以修改css属性;通过预览功能,页面中元素或组件实时应用样式修改并重新渲染;提交,样式写入到源代码。

css编辑模块的交互示意图

获取样式信息是这个功能的第一步。如何实现页面中css样式信息的提取?为了达到这个目的,方案经历了几次变动,下面说明主要的几种。

用浏览器在js环境提供的现有api

首先作为前端的开发同学,大家都能很容易想到的最直接能获取css规则的方法,就是使用浏览器暴露的api:

使用apiwindow.getComputedStyle(element,[pseudoElt])获取元素的计算样式;使用apiHTMLElement.style获取元素的行内样式信息。

结合这些数据,就可以从计算样式的角度还原出一个元素的css元数据了!

"getComputedStyle"返回值

这个方案的特点:

优点:简单方便,并且从计算样式的角度非常准确;缺点:拿到的元数据是计算结果

?

看到这里,上面的方案好像没什么问题,很好的解决了之前提出的问题,实现起来也比较简便,本文也应该可以结尾了。

但是仔细一想,这个方案得到的是计算后的css样式。

计算后的样式存在什么问题?

问题在于,在样式编辑器开始的设计中,需要提供的能力是让开发者基于代码对css样式进行编辑。

什么是计算样式?浏览器会根据页面上加载到所有css代码规则,计算出对应元素的渲染样式数据,这就是计算样式。

但是计算样式!==css样式代码,如果使用计算样式为样式编辑器提供初始信息,会引起很大的问题。举个例子:在代码中的width:%这样的css样式,经过浏览器的计算后,%会被计算为真正的像素宽度,如"98px"。如果在编辑这个元素时css样式编辑器给用户显示出这个值,会给开发者一个误导——源码里就是硬编码了98px,并且在这个值的基础上修改。然而这是有问题的。

因此,不解决这个问题,后面的一切都无从谈起,样式编辑器的需求意味着必须要找到一个方案来提供某个元素对应的css样式源码,而不是计算样式。

使用浏览器提供的另外一组api

在对这个问题进行更多思考之后,可以想到第二个方案。

这个方案依然是通过浏览器的api去尝试得到css元数据。不同于上个方案,这次的目标是获得计算前的css代码。

简单描述一下思路:通过浏览器apidocument.stylesheets可以获取到整个页面文档的所有CSSStyleSheet实例。其中每个实例对应着浏览器解析出一个css样式表对象,每个样式表中包含多个cssrule。

这些信息代表着开发者通过link等或style元素引入的内联或外部样式表。根据层叠样式表的规范处理这些样式表规则(分析选择器,分析属性等),可以最终计算出应用在某个元素上的属性。并且由于这些属性都是通过原始的样式表规则计算而来,也就可以非常准确的对应到计算前的属性上。

chrome中通过API获取的CSSStylesheet实例

但是相应的这种方式还是存在一些问题,比如:

CSSStyleSheet的cssRules和rules属性受到浏览器CORS策略的限制,不能访问第三方链接来源的css样式表。在开发的场景下的前端工程中,写在用户工程中的样式代码一般都会放在localhost下,但是引用第三方的样式代码在某些情况下也是不可避免的。

考虑某个元素在计算样式时,来源一般分为:

通过link或者style标签声明的样式,通过选择器作用在元素上;在脚本中通过html元素中"style"属性设置上的样式;来自于浏览器的默认样式。很明显,通过本方案,前两者来源都可以获得到,但是对于浏览器的样式就无能为力了。这个问题会导致最终计算出的样式不准确。也就是某些样式属性可能被设置了值,但是没有被解析出来。通过ChromeDevToolsProtocol来获取css元数据

最后一个方案是通过ChromeDevToolsProtocol(下文简称CDP)来获取css元数据。那么ChromeDevToolsProtocol是什么?

TheChromeDevToolsProtocolallowsfortoolstoinstrument,inspect,debugandprofileChromium,ChromeandotherBlink-basedbrowsers.Manyexistingprojectscurrentlyusetheprotocol.TheChromeDevToolsusesthisprotocolandtheteammaintainsitsAPI.

Chromedevtoolsprotocol文档页面

简单来说,ChromeDevToolsProtocol可以用来控制、调试、检查基于chromium内核的浏览器,通常用于对浏览器进行调试,或者制作自动化工具。chrome浏览器内置的devtool以及包括puppeteer在内的很多工具都是在这个协议基础上实现。

通过CDP可以更获取到在脚本运行环境中无法访问的浏览器内核数据,并操作浏览器的行为。我们可以利用CSS.forcePseudoState来给元素施加伪类,通过CSS.getMatchedStylesForNode获取相应Domnode在浏览器中经过选择器解析和计算后的匹配样式表内容。这些内容不仅包括浏览器解析出的来自第三方源(通过link等方式远程引入或者通过style样式表引入等),同时也包含了浏览器本身自带的默认样式。这些都正好解决了前述方案的几个问题。

实现细节

编辑器框架

AUX最终使用了上述的CDP方案来解决css的解析问题。在CDP具体使用方式上,又大概考虑了两种:

通过server使用CDP去控制浏览器;通过Chromeextension使用CDP控制浏览器。最终实现上选择了第二种方式,原因是第一种方式需要在浏览器启动时打开远程debugger端口,对于用户体验不佳。

下图就是最终css样式编辑器的框架:

css编辑工具框架

在整个css编辑器中,用户的选择会触发auxdevtool的选择器,选择器通过bridge通知chromeextension获取某个dom元素的css信息,chromeextension通过CDP获取到css元数据并通过bridge返回给auxdevool,对元数据根据层叠样式表的规则进行运算,样式编辑器就得到了样式代码默认值等更精准的元数据。

样式的具体解析和处理

CSS.getMatchedStylesForNode这个api是css样式解析的核心,下面稍作展开。

首先在CDP的文档中,可以得知,这个api完成的功能是:

ReturnsrequestedstylesforaDOMnodeidentifiedbynodeId.

getMatchedStylesForNode接口

也就是获取一个DOMnode的样式信息,通过接口描述可以得知这些都是chrome内核稍加处理过的css相关信息。返回值包含几个属性,包括:inlineStyle(内联样式)、attributesStyle(属性设置样式)、matchedCSSRules(样式表匹配样式)、pseudoElements(为元素)、inherited(继承样式)等。

在获得这些原始信息并提供给样式编辑器之前,还需要对这些样式进行处理,目的根据css(层叠样式)的规则来运算出诸如:“哪些样式目前处于active的状态”、“某个active的css属性来源是什么”这样的信息。

这部分不做展开,有兴趣的同学可以做深入研究。下面给出部分buildCascade的伪代码:

classCssStyle{constructor(){}.../****

param{CSS.CSSStyle}inlinePayload来自于CDP解析的内联样式*

param{CSS.CSSStyle}attributesPayload来自于CDP解析的属性样式*

param{Array.CSS.RuleMatch}matchedPayload来自于CDP的匹配样式规则*

param{Array.CSS.InheritedStyleEntry}inheritedPayload来自于CDP的继承样式*

return*

memberofCssStyle*/_buildCascade(inlinePayload,attributesPayload,matchedPayload,inheritedPayload,){constnodeCascades=[];constnodeStyles=[];//内联样式拥有最高优先级if(inlinePayload){conststyle=newCSSStyleDeclaration(inlinePayload,Type.Inline,);nodeStyles.push(style);}//以相反的顺序加入rule,满足css定义letaddedAttributesStyle;for(leti=matchedPayload.length-1;i=0;--i){construle=newCSSStyleRule(matchedPayload[i].rule);//在插件注入样式和浏览器样式前插入attributesStyleif((rule.isInjected()

rule.isUserAgent())!addedAttributesStyle){addedAttributesStyle=true;addAttributesStyle.call(this);}nodeStyles.push(rule.style);...}if(!addedAttributesStyle){addAttributesStyle.call(this);}nodeCascades.push(newNodeCascade(this,nodeStyles,false/*isInherited*/),);//顺着node树向上查找并识别继承属性for(leti=0;inheritedPayloadiinheritedPayload.length;++i){//计算继承的内联属性constinheritedStyles=[];constentry=inheritedPayload[i];constinheritedInlineStyle=entry.inlineStyle?newCSSStyleDeclaration(entry.inlineStyle,Type.Inline,):null;if(inheritedInlineStylethis._containsInherited(inheritedInlineStyle)){inheritedStyles.push(inheritedInlineStyle);}//计算每一个父元素匹配的样式规则constinheritedRules=entry.matchedCSSRules

[];for(letj=inheritedRules.length-1;j=0;--j){constinheritedRule=newCSSStyleRule(inheritedRules[j].rule,);if(!this._containsInherited(inheritedRule.style)){continue;}if(containsStyle(nodeStyles,inheritedRule.style)

containsStyle(this._inheritedStyles,inheritedRule.style)){continue;}inheritedStyles.push(inheritedRule.style);this._inheritedStyles.add(inheritedRule.style);}nodeCascades.push(newNodeCascade(this,inheritedStyles,true/*isInherited*/),);}returnnodeCascades;functionaddAttributesStyle(){if(!attributesPayload){return;}conststyle=newCSSStyleDeclaration(attributesPayload,);nodeStyles.push(style);}functioncontainsStyle(styles,query){if(!query.styleSheetId

!query.range){returnfalse;}for(conststyleofstyles){if(query.styleSheetId===style.styleSheetIdstyle.rangequery.range.equal(style.range)){returntrue;}}returnfalse;}}}

上述伪代码描述了计算集联(cascade)的过程。通过合并获取到内联样式、属性样式、匹配样式最终得到一个综合后的样式层叠结构。

一个小问题

在实际的功能实现过程中,遇到了很多小问题,这里说一个:

样式编辑器必须考虑一种情况:在编写代码时,有的元素(如按钮),在不同的状态下(hover,active)会需要不同的css样式。

就像前文说的,应用CDP的CSS.forcePseudoStateapi,可以对页面上的指定元素施加一个伪类。结合这个api,再获取样式信息,就可以实现对一个元素不同状态下样式的元数据获取。

然而这种情况在实际应用中会遇到一些问题。在aux的交互逻辑中,点击某个元素的同时,就会去获取元素的style信息。

问题在于点击时,鼠标是hover在元素上的。这个时候鼠标的hover状态怎么处理?怎么获取元素没有hover伪类状态下的样式?

这个问题就留给读者思考和探索。

总结

本文详细阐述了AUX样式解析模块的设计方案和实现细节。

AUX作为一个可视化页面编辑工具,尝试在浏览器运行时和代码之间建立起桥梁,从一个新的角度制作一款开发人员提效工具。对于这个工具来说,通过运行时元数据还原代码场景的能力尤为重要,也是在开发过程中的难点之一。为了提供最好的开发体验,我们会继续在这个方向进行探索和尝试。

预览时标签不可点收录于话题#个上一篇下一篇

转载请注明:http://www.aierlanlan.com/rzdk/15.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了