美文网首页
自定义PostCSS插件实现主题切换

自定义PostCSS插件实现主题切换

作者: smartzheng | 来源:发表于2021-05-06 09:47 被阅读0次

    对于主题切换这一话题,社区上介绍的方案往往通过CSS 变量(CSS 自定义属性)来实现,但其自动化程度以及可维护性都较差。

    PostCSS 可以接收一个CSS 文件,并提供了插件机制,提供给开发者分析、修改CSS的规则,具体实现方式也是基于 AST 技术,利用这一特点,我们可以实现一套更为自动化的主题切换功能。

    假设我们这样编写CSS(定义一个CSS方法cc,下文会解释其作用):

    a {
        color: cc(G01);
    }
    

    假设:

    • 借助PostCSS的能力,根据不同的主题,最终输出不同的CSS样式,比如日间模式a标签的色值为#eee,而夜间模式色值为#111

    • 这里我们可以通过在HTML根节点加上属性选择器data-theme='dark'来动态改变当前页面主题是否为夜间主题。

    所以需要做到的是,我们最终需要生成一份如下的CSS样式:

    a {
        color: #eee;
    }
    
    html[data-theme="dark"] a {
        color: #111;
    }
    

    实现步骤大致如下:

    • 首先编写一个名为 postcss-theme-colorsPostCSS 插件,实现上述编译过程。

    • 维护一个色值,结合上例(这里以 JSON格式为例)就是:

    {
      C01: '#eee',
      C02: '#111'
    }
    

    postcss-theme-colors 需要:

    • 识别cc()方法;

    • 读取色值;

    • 通过色值,对cc()方法求值,得到两种颜色,分别对应 dark 和 light 模式;

    • 原地编译 CSS 中的颜色为日间模式色值;

    • 同时将dark模式色值写到HTML 节点上。(通过PostCSS Nested或者PostCSS Nesting插件完成)

    先简要介绍一下PostCSS的原理:

    PostCSS自身只包括了CSS分析器CSS节点树APIsource map生成器CSS节点拼接器,而基于PostCSS的插件都是使用了CSS节点树API来实现的。

    我们都知道CSS的组成如下:

    element {
      prop1 : rule1 rule2 ...;
      prop2 : rule1 rule2 ...;
      prop2 : rule1 rule2 ...;
      ...
    }
    

    也就是一条一条的样式规则组成,每一条样式规则包含一个或多个属性跟值。所以PostCSS的执行过程大致如下:

    1. Parser 利用CSS分析器读取CSS字符内容,得到一个完整的节点树
    2. Plugin 对上面拿到的节点树利用CSS节点树API进行一系列的转换操作
    3. Plugin 利用CSS节点拼接器将上面转换之后的节点树重新组成CSS字符
    4. Stringifier 在上面转换期间可利用source map生成器表明转换前后字符的对应关系

    PostCss插件要做的就是拿到节点树上的CSS属性声明,通过转换拼接为新的CSS字符串;这里我们需要的功能的编写方式如下,可以参考 PostCSS 8 插件

     module.exports = (opts = {}) => {
       return {
         postcssPlugin: 'postcss-dark-theme-class', // 插件名
         Once (root, { result }) { // root为根节点树,Once方法会在该节点下的所有子元素被处理之前调用
            root.walkDecls(decl=>{...}) // 遍历CSS声明
         }
       }
     }
     module.exports.postcss = true // 声明导出为postcss插件
    

    post-theme-color实现如下:

    const postcss = require('postcss')
    
    const defaults = {
      function: 'cc', // 自定义CSS方法名
      groups: {}, // 存储色值分组
      colors: {}, // 存储所有色值
      useCustomProperties: false,// 是否使用自定义属性
      darkThemeSelector: 'html[data-theme="dark"]', // 夜间模式选择器
      nestingPlugin: null // 添加选择器的插件
    }
    
    /**
     * 计算最终色值
     * @param options
     * @param theme
     * @param group
     * @param defaultValue
     * @returns {string|*}
     */
    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 = options => {
      options = Object.assign({}, defaults, options)
      // 获取色值函数(默认为 cc())
      const reGroup = new RegExp(`\\b${options.function}\\(([^)]+)\\)`, 'g')
      return {
        postcssPlugin: 'postcss-theme-colors', // 定义插件名
        Once(root, { 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 声明
          root.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)
          })
        }
      }
    }
    module.exports.postcss = true
    

    插件使用:

    • 定义CSS源文件source.css
    a {
        color: cc(G01);
    }
    
    • 定义色值:
    const colors = {
      C01: '#eee',
      C02: '#111',
      C03: '#fff',
      C04: '#222',
    }
    
    • 定义模式色值分组:
    const groups = {
      G01: ['C01', 'C02'], 
      G02: ['C03', 'C04'],
    }
    
    • 执行转换:
    const css = fs.readFileSync('source.css')
    postcss([
      require('./postcss-theme-colors')({ colors, groups }),
      require('postcss-nested')
    ]).process(css).then(res => {
      fs.writeFileSync('index.css', res.css)
    })
    

    执行完成后即可在index.css生成如下代码:

    a {
        color: #eee;
    }
    
    html[data-theme="dark"] a {
        color: #111;
    }
    

    相关代码含义已在注释中详细注明。通过post-theme-colors插件,后续主题维护只需维护colorsgroups两个对象即可,可以通过JSON或者YML进行维护。

    源代码请查看:https://github.com/smartzheng/arch-demos/tree/master/post-theme-colors

    相关文章

      网友评论

          本文标题:自定义PostCSS插件实现主题切换

          本文链接:https://www.haomeiwen.com/subject/unvrdltx.html