在前端基础建设中,样式方案的处理也必不可少
设计一个主题切换工程架构 随着 iOS 13 引入 Dark Mode(深色模式),各大应用和网站也都开始支持深色模式。相比传统的页面配色方案,深色模式具有较好的降噪性,也能让用户的眼睛看内容更舒适
PostCSS 原理和相关插件能力 简单来说, PostCSS 是一款编译 CSS 的工具。
PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
如上介绍,postCSS具有良好的插件性,其插件也是使用 javascript 编写的,非常有利于开发者拓展。PostCSS的工作原理:PostCSS接收一个CSS文件,并提供了插件机制,提供给开发者分析、修改CSS的规则,具体实现方式也是基于AST技术
架构思路总结 主题切换——社区上介绍的方案往往通过 CSS 变量(CSS 自定义属性)来实现
这无疑是一个很好的思路,但是作为架构来说,使用 CSS 自定义属性——只是其中一个环节。站在更高、更中台化的视觉思考,我们还需要设计:
如何维护不同主题色值
谁来维护不同颜色值
研发和设计之间,如何保持不同颜色值的同步沟通
如何最小化前端工程师的开发量,不需要 hard coding 两份颜色数值
如何做到一键切换时的性能最优
如何配合javascript状态管理,同步主题切换的信号
基于以上考虑,以一个超链接样式为例,我们希望做到开发时,编写:
1 2 3 a { color : cc (GBK05A); }
这样的代码,就能一劳永逸直接支持两套主题模式。也就是说,在应用编译上,上述代码预期被编译为下面这样的代码:
1 2 3 4 5 6 7 a { color : #646464 ; } html [data-theme='dark' ] a { color : #808080 ; }
我们来看看在编译时,构建环节发生了什么:
cc(GBK05A)
这样的声明,被编译为#646464
也就是说,cc
是一个CSS function,而 GBK05A
是一组色值,分别包含了light和dark两种主题的颜色
同时在HTML根节点上,添加属性选择器 data-theme='dark'
,并添加 a
标签color色值样式为 #808080
我们设想,用户点击“切换主题”按钮时,首先通过 Javascript 将 HTML 根节点标签添加 data-theme 为dark的属性值,这时CSS选择器html[data-theme=’dark’] a 将起作用,实现了样式的切换
结合下图理解:
如何在构建时完成 CSS 的样式编译转换呢?答案指向了 PostCSS
首先编写一个名为 postcss-theme-colors 的PostCSS插件
维护一个色值,结合上例子就是:
1 2 3 GBK05A: [BK05, BK06] BK05: '#808080' BK06: '#999999'
postcss-theme-colors需要:
识别 cc()
方法
读取色值
通过色值,对 cc()
方法求值,得到两种颜色,分别对应 dark 和 light 模式
原地编译 CSS 中的颜色为 light 模式色值
同时 dark 模式色值写到 HTML 节点上
这里需要补充的是,为了将 dark 模式色值按照 html[data-theme=’dark’] 方式写到 HTML 节点上,需要使用另外两个PostCSS插件完成:
PostCSS Nested
PostCSS Nesting
整体架构设计,总结为下图:
主题色架构实现 PostCSS 插件体系 PostCSS具有天生的插件化体系,开发者一般很容易上手插件开发
1 2 3 4 5 6 7 8 var postcss = require ('postcss' );module .exports = postcss.plugin('pluginname' , function (opts ) { opts = opts || {}; return function (css, result ) { }; })
上面代码是一个典型的PostCSS插件编写模版。一个PostCSS就是一个Node.js模块,开发者调用 postcss.plugin
工厂方法返回一个插件实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 return { postcssPlugin: 'PLUGIN_NAME' , } }
在编写 PostCSS 插件时,我们可以直接使用postcss.plugin方法完成实际开发。接下来,开始动手实现 postcss-theme-colors。
动手实现postcss-theme-colors 具体实现逻辑,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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) const reGroup = new RegExp (`\\b${options.function } \\(([^)]+)\\)` , 'g' ) return (style, result ) => { const hasPlugin = name => name.replace(/^postcss-/ , '' ) === options.nestingPlugin || result.processor.plugins.some(p => p.postcssPlugin === name) const getValue = (value, theme ) => { return value.replace(reGroup, (match, group ) => { return resolveColor(options, theme, group, match) }) } 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 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` ) } if (darkRule) { darkRule.append(darkDecl) decl.after(darkRule) } const lightDecl = decl.clone({value : lightValue}) decl.replaceWith(lightDecl) }) } })
理解了这部分源码,使用方式也就呼之欲出了:
1 2 3 4 5 6 7 8 9 10 const colors = { C01: '#eee', C02: '#111', } const groups = { G01: ['C01', 'C02'], } postcss([ require('postcss-theme-colors')({colors, groups}), ]).process(css)