从 CSS 到 SCSS 的规模化过渡
有些天真的看法认为, CSS 看起来很容易理解——它没有繁多的编程结构,并且还只是一种用来描述 DOM 外观的声明式语法,而不是一种可执行语言。非常具有讽刺意味的是,功能性的缺失恰让 CSS 难以推断其效果。此外,在选择器所处位置及其处于执行状态时,开发者是不能添加脚本的,从长远来看这也增加了使用 CSS 的风险。
CSS 预处理器向 CSS 中引入了众多高级特性,比如当下风行的迭代就是 CSS 文档规范中所没有的。此类实用性特性通常包括变量、函数、混合宏和作用域,这意味着开发者可以嵌入逻辑操作,以优化 CSS 的编写和执行方式。如果能够正确应用预处理器,就可以在使 CSS 更加模块化和简洁(DRY,Don't Repeat Yourself) ,进而长远地提高代码库的可维护性。
前端基础设施团队 2014 年的一个目标,就是将 Etsy 全部的 CSS 代码过渡到 SCSS。SCSS 是一个成熟稳定、功能丰富的 CSS 预处理器,而且 Etsy 的设计师和开发者已决定将其整合入现有的技术环境中。然而,我们深知转变体量如此庞大的代码库所要付出的努力,也一定是极不寻常的。在 2014 的十月份,我们已经有了散布在 2000+ 个文件中,总计多达 400,000+ 行的代码。
为了和设计团队协同工作,前端团队开始设计将 SCSS 部署到所有开发环境的处理流程,以及该流程的构建管程。本文内容将会涵盖:决策背后的逻辑思考、一次性从 CSS 过渡到 SCSS 的潜在危险,以及如何为保障长远的可维护性,而自建工具实现代码优化。
##为什么钟意 SCSS?
Esty 对 SCSS 潜在能力最大的认可,源于在我们的决策之前,一小拨设计师对 SCSS 为期六个月之多的持续测试。从 Esty 的设计师积极推动代码过渡后,某个产品团队首次将 SCSS 整合进了他们的工作流中。他们定期会面讨论什么是他们所需要的,并开始将其工作详情编写成项目的最佳实践。
正是通过这个产品团队最初的实践,公司其余的部门看到了引入 CSS 预处理器的价值和可行性。当前端基础设施团队策划全公司内部署 SCSS 时,这些设计师所提供的经验完全是无价之宝,并且他们编写的最佳实践,也被吸收到了前端未来开发的 SCSS 编程指南中。
评估完 CSS 预处理器的发展空间后,我们决定使用 SCSS 快速更迭产品。SCSS 是一个极受欢迎的项目,从它活跃的开发者社区就可见一斑。此外它还拥有优秀的文档资源,表述其丰富的功能特性。因为 SCSS 语法是 CSS 的超集,因此开发者无需再学习新的语法,直接就可以上手实践。至于性能,在于 LibSass(一个 C/C++ 的 Sass 引擎端口) 同等条件下,Sass 团队将会优先考虑未来的发展和整体的特性。我们假设,通过使用由 node-sass 提供,绑定在 NodeJS 中的 libsass 可以使我们整合 SCSS 编译流程到当前开发环境中,而不需要考虑编译速度和时间上的损失。
我们也非常兴奋地看到,更大的 SCSS 社区所发布的软件,特别是像 scss-lint 这样的工具。为了编译 SCSS,我们知道从 CSS 过渡到 SCSS,意味着需要在 CSS 代码库上弥补诸多语法上的错误。由于既有 CSS 没有使用统一的代码规范,我们正好将其视为一个机会,通过编写代码规范来建立具有一致性、可执行的样式风格。通过使用定义良好的样式规范和健壮的代码审查,我们可以引用工具使 SCSS 代码持续保持简洁、高效和可维护性。
##新旧 CSS 管程。
我们的资源构建管程,被称为 “builda”(“build assets”的简称)。此前它是一系列的 PHP 脚本,用来处理所有 JS 和 CSS 的合并、压缩和版本控制。当使用其他语言编写(例如压缩工具)的库时,builda 就暂时离开 PHP 环境,交由相关工具执行。在开发者的虚拟机上,builda 对于每个请求可以动态生成 CSS,而在项目构建时,它会保存合并、压缩以及更新版本后的 CSS 文件到存储磁盘上。
我们使用 NodeJS 编写的 SCSS 管程来代替 builda 的 CSS 组件。这里选择 Node 的原因有三。第一,一年前我们就已经使用 Node 重写了 JavaScript 的编译组件,所以我们熟悉发布使用 Node 内置功能的工具和策略。第二,我们发现使用 JavaScript 编写前端工具,在整个组织中方便合作,更方便开发者提交分支整合请求(原文为 pull requests,此处根据 Github 的功能意译)。最后,一份前端开发报告也揭示了 JavaScript 风头正盛,所以使用 JS 编写构建工具,将会让我们在整合第三方代码时保持代码一致性。
该计划中我们最大的担忧就是速度。SCSS 将会增加一个编译步骤到另一个广泛使用的编译进程中,而我们不希望这会延长开发周期和构建时间。幸运的是,我们发现,与标准的 Ruby gem 相比,使用 libsass 至少可以提高十倍的编译速度。
我们致力于确保 SCSS builda 可以从旧代码库无缝升级到新代码库。我们设想,在最喜爱的编辑器下编写 SCSS,在浏览器中自动刷新,并且让虚拟机上的 CSS 渲染器自动执行——一切都还和之前的工作流程一样。在产品上线状态下,该构建管程将会始终为服务器,提供编译、压缩和版本控制的 CSS 文件。
尽管需要一个完全重写 CSS 的服务,但使用健壮的过渡进程和频繁的完备性检测,我们可以避免从 CSS 过渡到 SCSS 的任何中断。在重写之前,以及开发者编写 SCSS 的第一天起,工作流都是稳定不变的。
##转变遗留代码
理论上说,将 CSS 过渡到 SCSS 就像将文件扩展名从 .css
过渡到 .scss
一样简单,但在实践中,这要麻烦的多。
CSS 的难点是什么?它会悄无声息地出错。如果选择器不对或者参数写错了(比如 #000000
写成了 #0000000
),浏览器就会忽略该样式。这些错误将会是我们整个过渡流程的破坏者,因为 SCSS 编译时,一旦出现语法错误,就会完全阻止文件的编译过程。
语法错误还只是一方面的苦难。IE-hacks 有意而为特殊的选择器该如何处理?或者,为了符合我们强制约束在 SCSS 上的代码审查规则,对于遗留的 CSS 又该如何适配?举个例子,我们希望替换每一个 CSS 颜色关键字为十六进制值。
我们的转换策略是在多处修改大量代码。我们会在修正 CSS 的时候搞坏我们的网站吗?我们又是如何自信所有的修正不会制造视觉上的退步?
通常来说,有一些模式可以解决此类问题。一个小型网站可能会纠正语法错误,使用 headless browser 迭代所有页面并为变动的地方创建视觉差异。另外,通过固定大小,进行人工回归分析每个页面,也基本可以确保修复过程渲染那顺利。
不幸的是,这两种可能都不适应我们的规模及相应的测试设备,因为实在有太多不同的页面/测试组合,而且所有的主题都会同时变更。Etsy.com 每封信背后的计算量随时都在改变,大约 1.2M。
我们需要清理所有不正确的 CSS 并在我们给 SCSS 重命名前,强制使用新的代码审查规则,同时,我们还要确保这些修正在没有的时候,不会在网站的各个页面间出现视觉差异。我们分两步解决了这个问题:“修正 SCSS” 和 “SCSS 差异处理”。
##修正 SCSS
我们评估过多种方式来修补 CSS 错误,最初使用了一个宽泛的正则表达式列表,以修复代码中不正确的模式。但这种方式很快就失败了,因为我们的正则表达式列表太复杂,以至于毫无结果。
最终我们选定的方案是:使用解析器将所有的 CSS/SCSS 源代码转换为抽象语法树(AST),以方便稍后我们对特定的节点类型进行转换。对于不熟悉的人来说,可以将 AST 理解为源代码解析后的结构框架。我们使用 Reworkcss CSS 解析器生成 CSS ASTs,并用 gonzales-pe 生成 SCSS ASTs,最后使用自定义的适配器,简化两者在样式和语法上的不同。下面举个例子来说明 AST 生成后的效果,比如 Reworkcss CSS 解析器生成的这个例子就很好。
通过把现有的 CSS/SCSS 解析为 ASTs,我们可以在更加精确的粒度上,对特定类型的错误或选择器进行纠错。让我们回到颜色关键字的例子上来,这种解析方式给了我们一种更简洁的方式替换属性,比如将特定的颜色关键字(black
)转变为等价的十六进制(#000000
)。通过使用 AST,无需冒险在意料之外的位置(比如选择器名字中有此关键字:.black-header
)替换颜色关键字,也无需使用正则表达式遍历所有节点(意译,原文:navigating a jungle of regular expressions),我们就可以执行替换。
简而言之,我们的清整过程如下:
- 为现有 CSS/SCSS 文件生成 AST
- 执行自定义脚本在整个 AST 中辨别和修正属性上的错误/差异。
- 保存并输出为 .scss
- 使用 libsass 编译器编译 .scss 文件,直到成功编译全部文件。
- 重复第二到第四步,必要时手工修正特定的文件。
##SCSS 差异处理
修正 CSS 只完成了全部工作的一半。我们还需要一种方法来确认已清除的 CSS 不会对网站有负面影响,并且该方法还要具有自动检测数千个文件的能力。
现在再次把我们的目光转向 ASTs。ASTs 去掉了源代码的表面,为我们展示了语言的核心结构。因此我们可以得出结论:即使来源的编写形式不一样,只要两个 ASTs 全等,那么它们都会产生同样的 CSS 代码。
我们使用持续集成(Jenkins)服务器执行下列过程,这样每次网站上线后,它都会给提醒我们(上线状态):
- 使用旧版本的 builda 处理源码和未改动的 CSS,生成整合、压缩和版本化的 CSS,然后上线到服务器生成在线网站。最后根据输出文件构建 AST。
- 在第一步执行的同时,进行 SCSS 转换/修正,从 CSS 生成 SCSS 文件。使用 SCSS builda 执行这些 SCSS,生成整合、压缩和版本化的 CSS,最终生成一个 AST。
- 比较两个 ASTs 之间的差异。
- 显示和测试差异。重复第一到第三步,修改执行修正操作的脚本,使用 SCSS builda 或者手动定位 CSS 源码中的错误,直到两个 ASTs 完全相等。
获得相同的 ASTs 之后,我们就有信心处理 Etsy.com 的数千个文件了,而且网站也会在转换为 SCSS 的前后保持一致。将流程集成到 CI 中,可以给我们提供一个快速、安全的方式,发现使用现有代码修正 SCSS 的限制,但又不会影响已经上线的网站。
上线
通过 AST 差异处理获得足够的信心后,我们的下一步是确定网站安全上线的方法。下面就是我们的部署策略:
使用转换前的 CSS 作为来源,我们将 SCSS builda 流程添加到部署管程中。在项目上线前,需要获得 CSS 并进行修正,然后创建 SCSS 文件,继而生成 CSS 文件,最后分发到在线服务器的各个目录。
一旦 SCSS builda 流程正常运行几天(每天 25~50 次推送)后,我们将在基础设施上缓慢增加到 50 % 的用户量,让他们使用最新 的 SCSS builda 生成的页面。整个过程中,我们监控事态统计图。
几天后用户量到达 50 % 时,我们继续增加 Etsy.com 中,可以浏览 SCSS builda 生成的页面的用户量,直到 100 %,同时监控事态统计图。
最后一步是花几个小时,持续发布并转换 CSS 源代码为 SCSS。因为我们的 SCSS builda 流程生成独有的、简洁的 SCSS,所以转换源码就像使用生成的 SCSS 文件替换 CSS 目录下的内容一样简单。
总共 1.2M 行代码的试验性部署后,Etsy.com 终于完全运行在了 SCSS 源码上。
##提高开发者的生产力和代码的可维护性
我们知道,整合类似 SCSS 的新技术到技术库,需要前期就要考虑到沟通、教学和开发工具等工作。暂且只说与构建管程相关的工作,它的重要性就体现在,可以让开发者从入手 SCSS 的那一刻就非常有信心。
###沟通和教学
由最先使用 SCSS 的设计团队总结的样式指南以及最终的成品,向其他部门展示了采用 SCSS 的价值,是推动整个转变过程的关键。通过使用新的样式指南,创建崭新、一致和美丽的页面更加方便,其速度令人印象深刻。在 SCSS 正式启用前,我们和设计师使用邮件和午课来紧密合作,并使用内部 wiki 制作文档。
###开发工具和可维护性
在语法差异之外,还有一些开发者使用 SCSS 会遇见的陷阱和不爽:
- SCSS 编译后,语法错误塞满了编译器,但页面却没有可用的 CSS 来渲染。
- 偶尔你会通过执行看似无害的操作,结果却让 CSS 文件爆炸式增长(这里可以看看
@extend
)。 - 在 SCSS 文件中嵌套
@import
,会让在源文件中追踪特定选择器的过程更加复杂。
我们发现最好的纠正方式就是,将反馈整合到开发环境中。
对于中断的 SCSS,会在文档顶部显示有关遗失或未编译 CSS 文件的错误信息。
为了提高可维护性,整合实时、浏览器内置的 SCSS 代码审查工具是非常有价值的:
由设计师制定的代码审查规则,可以帮助我们保持正确性和一致性,同时它还被用于部署前的整体测试和浏览器内建的代码审查。幸运的是,优秀的开源项目 scss-lint 就有多种配置,而且可以立刻投入使用。
最后,由于 SCSS 文件的嵌套结构,检视文件依赖性的资源映射就必须使用浏览器的开发者工具了。自从 libsass 支持资源映射后,这些操作工作就很简单了。
通过使用 SCSS 构建流程、实时代码审查、资源映射、测试套件升级以及围绕新样式指南的培训,我们最后的内部转换过程就是,推送开发环境的更新到所有开发者的虚拟机上。与 SCSS 产品管程相似的是,开发者环境也需要严格测试和版本迭代,并且要收集来自开发者测试团队的反馈,是想全公司推出该工具的关键。
##总结思考
为复杂系统做彻底变革的关键就是建立自信,将 CSS 过渡到 SCSS 也不例外。我们在整个过程中都很有自信,那是因为我们的修正流程不会产生负面影响网站的 SCSS,并且我们自信创造了正确的工具,可以保持 SCSS 长久性的简洁和可维护性。在整个过程中,通过适当地培训、工具和逻辑审查,我们在将 SCSS 移植到 Etsy 的过程中,实现了对开发者工作流程和产品用户最低程度的干扰。
##备注
- 我们使用 Sass 的 SCSS 格式生成 CSS 语法。对于本文所有的开发设计,其中的 Sass 和 SCSS 都是可以互换的。
- 为了维护我们旧系统中的构建行为,防止导入多余的 CSS,我们新建了 libsass 的分支,以支持 compass-style 的一次性导入方式。
- 图片来自 Daniel Espeset —— Making Maps: The Role of Frontend Infrastructure at Etsy – Fronteers 2014 Presentation (http://talks.desp.in/fronteers2014/)
本文根据@Dan Na的《Transitioning to SCSS at Scale》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://codeascraft.com/2015/02/02/transitioning-to-scss-at-scale。
如需转载,烦请注明出处:https://www.fedev.cn/preprocessor/transitioning-to-scss-at-scale.htmljordan retro 11 mens crimson