Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

工程化思维处理方案:如何实现应用主题切换功能 #6

Open
BYChoo opened this issue Feb 26, 2021 · 0 comments
Open

Comments

@BYChoo
Copy link
Owner

BYChoo commented Feb 26, 2021

在前端基础建设中,样式方案的处理也必不可少

设计一个主题切换工程架构

随着 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状态管理,同步主题切换的信号

基于以上考虑,以一个超链接样式为例,我们希望做到开发时,编写:

a {
  color: cc(GBK05A);
}

这样的代码,就能一劳永逸直接支持两套主题模式。也就是说,在应用编译上,上述代码预期被编译为下面这样的代码:

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插件
  • 维护一个色值,结合上例子就是:
GBK05A: [BK05, BK06]
BK05: '#808080'
BK06: '#999999'

postcss-theme-colors需要:

  1. 识别 cc() 方法
  2. 读取色值
  3. 通过色值,对 cc() 方法求值,得到两种颜色,分别对应 dark 和 light 模式
  4. 原地编译 CSS 中的颜色为 light 模式色值
  5. 同时 dark 模式色值写到 HTML 节点上

这里需要补充的是,为了将 dark 模式色值按照 html[data-theme='dark'] 方式写到 HTML 节点上,需要使用另外两个PostCSS插件完成:

  • PostCSS Nested
  • PostCSS Nesting

整体架构设计,总结为下图:

描述

主题色架构实现

PostCSS 插件体系

PostCSS具有天生的插件化体系,开发者一般很容易上手插件开发

var postcss = require('postcss');
module.exports = postcss.plugin('pluginname', function (opts) {
  opts = opts || {};
  // Work with options here
  return function (css, result) {
    // Transform the CSS AST
  };
})

上面代码是一个典型的PostCSS插件编写模版。一个PostCSS就是一个Node.js模块,开发者调用 postcss.plugin 工厂方法返回一个插件实体:

return {
    postcssPlugin: 'PLUGIN_NAME',
    /*
    Root (root, postcss) {
      // Transform CSS AST here
    }
    */
    /*
    Declaration (decl, postcss) {
      // The faster way to find Declaration node
    }
    */
    /*
    Declaration: {
      color: (decl, postcss) {
        // The fastest way find Declaration node if you know property name
      }
    }
    */
  }
}

在编写 PostCSS 插件时,我们可以直接使用postcss.plugin方法完成实际开发。接下来,开始动手实现 postcss-theme-colors。

动手实现postcss-theme-colors

具体实现逻辑,代码如下:

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 工作流程中,是否使用了某些 plugins
    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)
    })
  }
})

理解了这部分源码,使用方式也就呼之欲出了:

const colors = {
  C01: '#eee',
  C02: '#111',
}
const groups = {
  G01: ['C01', 'C02'],
}
postcss([
  require('postcss-theme-colors')({colors, groups}),
]).process(css)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant