
👆点击“博文视点Broadview”,获取更多书讯

在前端基础建设中,对样式方案的处理是必不可少的。
在本文中,我们将实现一个工程化主题切换功能,并梳理现代前端样式的解决方案。
1
设计一个主题切换工程架构
那么对于前端来说,如何高效地支持深色模式呢?
这里的高效就是指工程化、自动化。在介绍具体方案前,我们先来了解一个必会的前端工程化神器——PostCSS。
▊ PostCSS原理和相关插件能力
简单来说,PostCSS是一款编译CSS的工具。PostCSS具有良好的插件性,其插件也是使用JavaScript编写的,非常有利于开发者进行扩展。
基于前面内容介绍的Babel思想,对比JavaScript的编译器,我们不难猜出PostCSS的工作原理:PostCSS接收一个CSS文件,并提供插件机制,提供给开发者分析、修改CSS规则的能力,具体实现方式也是基于AST技术实现的。
本文介绍的工程化主题切换架构也离不开PostCSS的基础能力。
▊ 架构思路
对于主题切换,社区介绍的方案往往是通过CSS变量(CSS自定义属性)来实现的,这无疑是一个很好的思路,但是作为架构,使用CSS自定义属性只是其中一个环节。站在更高、更中台化的视角思考,我们还需要搞清楚以下内容。
基于以上考虑,以一个超链接样式为例,我们希望做到在开发时编写以下代码。
a { color: cc(GBK05A);}这样就能一劳永逸,直接支持两套主题模式(Light/Dark)。也就是说,在应用编译时,上述代码将被编译为下面这样。
a { color: #646464;}
html[data-theme='dark'] a { color: #808080;}我们来看看在编译时,构建环节完成了什么具体操作。
我们设想,用户点击“切换主题”按钮时,首先通过JavaScript向HTML根节点标签内添加 data-theme为dark的属性值,这时CSS选择器html[data-theme='dark'] a将发挥作用,实现样式切换。
结合图1可以辅助理解上述编译过程。

图1
回到架构设计中,如何在构建时完成CSS的样式编译转换呢?
答案指向了PostCSS。
具体架构设计步骤如下。
• 编写一个名为postcss-theme-colors的PostCSS插件,实现上述编译过程。
• 维护一个色值,结合上例(这里以YML格式为例),配置如下。
GBK05A: [BK05, BK06]
BK05: '#808080'BK06: '#999999'postcss-theme-colors需要完成以下操作。
这里需要补充的是,为了将Dark主题模式色值按照html[data-theme='dark']方式写到HTML根节点上,我们使用了如下两个PostCSS插件。
整体架构设计如图2所示。

图2
2
主题色切换架构实现
▊ PostCSS插件体系
PostCSS具有天生的插件化体系,开发者一般很容易上手插件开发,典型的PostCSS插件编写模板如下。
var postcss = require('postcss');
module.exports = postcss.plugin('pluginname', function (opts) {
opts = opts || {};
// 处理配置项 return function (css, result) { // 转换AST };
})一个PostCSS就是一个Node.js模块,开发者调用postcss.plugin(源码链接定义在postcss.plugin 中)工厂方法返回一个插件实体,如下。
return { postcssPlugin: 'PLUGIN_NAME', /* Root (root, postcss) { // 转换AST } */ /* Declaration (decl, postcss) { } */
/* Declaration: { color: (decl, postcss) { } } */ }}在编写PostCSS 插件时,我们可以直接使用postcss.plugin方法完成实际开发,然后就可以开始动手实现postcss-theme-colors插件了。
▊ 动手实现postcss-theme-colors插件
在PostCSS插件设计中,我们看到了清晰的AST设计痕迹,经过之前的学习,我们应该对AST 不再陌生。根据插件代码骨架加入具体实现逻辑,如下。
const postcss = require('postcss')
const defaults = { function: 'cc', groups: {}, colors: {}, useCustomProperties: false, darkThemeSelector: 'html[data-theme="dark"]', nestingPlugin: null,}
const resolveColor = (options, theme, group, defaultValue) => { const [lightColor, darkColor] = options.groups[group] || [] const color = theme === 'dark' ? darkColor : lightColor if (!color) { return defaultValue }
if (options.useCustomProperties) { return color.startsWith('--') ? 'var(${color})' : 'var(--${color})' }
return options.colors[color] || defaultValue}
module.exports = postcss.plugin('postcss-theme-colors', options => { options = Object.assign({}, defaults, options)
// 获取色值函数(默认为cc) const reGroup = new RegExp('\\b${options.function}\\(([^)]+)\\)', 'g')
return (style, result) => { // 判断PostCSS工作流程中是否使用了某些插件 const hasPlugin = name => name.replace(/^postcss-/, '') === options.nestingPlugin || result.processor.plugins.some(p => p.postcssPlugin === name)
// 获取最终的CSS值 const getValue = (value, theme) => { return value.replace(reGroup, (match, group) => { return resolveColor(options, theme, group, match) }) }
// 遍历CSS声明 style.walkDecls(decl => { const value = decl.value
// 如果不含有色值函数调用,则提前退出 if (!value || !reGroup.test(value)) { return }
const lightValue = getValue(value, 'light') const darkValue = getValue(value, 'dark')
const darkDecl = decl.clone({value: darkValue})
let darkRule
// 使用插件,生成Dark主题模式 if (hasPlugin('postcss-nesting')) { darkRule = postcss.atRule({ name: 'nest', params: '${options.darkThemeSelector} &', }) } else if (hasPlugin('postcss-nested')) { darkRule = postcss.rule({ selector: '${options.darkThemeSelector} &', }) } else { decl.warn(result, 'Plugin(postcss-nesting or postcss-nested) not found') }
// 添加Dark主题模式到目标HTML根节点中 if (darkRule) { darkRule.append(darkDecl) decl.after(darkRule) }
const lightDecl = decl.clone({value: lightValue}) decl.replaceWith(lightDecl) }) }})上面的代码中加入了相关注释,整体逻辑并不难理解。理解了以上源码,postcss-theme-colors插件的使用方式也就呼之欲出了。
const colors = { C01: '#eee', C02: '#111',}
const groups = { G01: ['C01', 'C02'],}
postcss([ require('postcss-theme-colors')({colors, groups}),]).process(css)通过上述操作,我们实现了postcss-theme-colors插件,整体架构也完成了大半。接下来,我们将继续完善,并最终打造出一个更符合基础建设要求的方案。
▊ 架构平台化——色组和色值平台设计
在上面的示例中,我们采用了硬编码(hard coding)方式。
const colors = { C01: '#eee', C02: '#111',}
const groups = { G01: ['C01', 'C02'],}上述代码声明了colors和groups两个变量,并将它们传递给了postcss-theme-colors插件。其中,groups变量声明了色组的概念,比如group1被命名为G01,对应了C01(日间色)、C02(夜间色)两个色值,这样做的好处显而易见。
√ colors维护具体色值。
√ groups维护具体色组。
例如,前面提到了如下的超链接样式声明。
a { color: cc(GBK05A);}在业务开发中,我们直接声明了“使用GBK05A这个色组”。业务开发者不需要关心这个色组在Light和Dark主题模式下分别对应哪些色值。而设计团队可以专门维护色组和色值,最终只提供给开发者色组。
在此基础上,我们完全可以抽象出一个色组和色值平台,方便设计团队更新内容。这个平台可以以JSON或YML等任何形式存储色值和色组的对应关系,方便各个团队协作。
在前面提到的主题切换设计架构图的基础上,我们扩充其为平台化解决方案,如图3所示。

图3

本文节选自《前端架构师:基础建设与架构设计思想》一书,更多前端架构相关内容,请查看本书!


粉丝专享六折,快快扫码抢购吧!

发布:刘恩惠
审核:陈歆懿

如果喜欢本文欢迎 在看丨留言丨分享至朋友圈 三连
热文推荐
新浪微博从 Kafka 到 Pulsar 的演变
什么是语法糖,如何解糖?
入门机器学习?还是先抢救一下数学吧!
OS之巅:系统、性能技术的投入与产出(南北高校、中美专家连线论道)▼点击阅读原文,了解本书详情~
本文分享自 博文视点Broadview 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!