模拟 Taro 实现编译多端样式文件

先看一段 CSS 代码。

css 复制代码
.container {
  color: black;
  padding: 10px;
  
  /* #ifdef h5 */
  background-color: blue;
  font-size: 16px;
  /* #endif */
  
  /* #ifdef weapp */
  background-color: green;
  margin: 5px;
  /* #endif */
  
  border: 1px solid #ccc;
}

/* #ifdef h5 */
.h5-only {
  display: block;
  color: red;
}
/* #endif */

/* #ifndef h5 */
.weapp-only {
  display: flex;
  color: green;
}
/* #endif */

这段 CSS 代码中包含了一些注释节点:

  • #ifdef h5/weapp - 如果定义 h5/weapp
  • #ifndef h5 - 如果未定义 h5
  • #endif - 条件块结束

其中 h5/weapp 分别表示 H5 平台和小程序平台。

实际 Taro 已经支持了很多平台,这里只是拿这两个平台举例子。

如果不进行任何处理,上述代码在两个平台上会生成一模一样的结果,而我们期望两个平台生成不同的 CSS 代码,就像这样:

针对 H5 生成 output.h5.css

css 复制代码
// output.h5.css
.container {
  color: black;
  padding: 10px;
  background-color: blue;
  font-size: 16px;
  
  border: 1px solid #ccc;
}
.h5-only {
  display: block;
  color: red;
}

针对 weapp 生成 output.weapp.css

css 复制代码
// output.weapp.css
.container {
  color: black;
  padding: 10px;
  background-color: green;
  margin: 5px;
  
  border: 1px solid #ccc;
}
.weapp-only {
  display: flex;
  color: green;
}

要想实现上面的效果,我们得请出 PostCSS,大概原理如下:

  1. 使用 PostCSS 解析 CSS 为 AST
  2. 识别条件注释节点
  3. 根据平台移除不满足条件的样式节点
  4. 重新生成 CSS

下面给出详细实现步骤。

先新建两个文件 postcss-ifdef-plugin.jstest-plugin.js 分别用来编写这个插件和测试插件。

编写插件

首先定义整体代码结构,此插件接收一个对象参数 options,调用时可传入具体平台,比如 { platform: 'h5' }。

js 复制代码
// postcss-ifdef-plugin.js
const postcss = require('postcss')

module.exports = postcss.plugin('postcss-ifdef', (options = {}) => {
  // 获取当前构建的目标平台,默认值为 'h5'
  const currentPlatform = options.platform || 'h5'
  
  return (root) => {
      // 核心代码,下面会讲
  }
})

上述代码中的 return 语句是核心代码的实现,大概分为几个步骤:

  1. 定义变量 nodesToRemove 存储所有待删除的节点,方便后面统一处理删除操作。
js 复制代码
const nodesToRemove = []
  1. 定义变量 processingStack 用于处理嵌套的条件块,结构是后进先出的栈(LIFO)
js 复制代码
let processingStack = []
  1. 遍历 AST 中所有节点(深度优先),识别并标记
js 复制代码
root.walk((node) => {
    // 仅处理注释节点(条件编译使用注释语法)
    if (node.type === 'comment') {
        const commentText = node.text.trim()
        
        // 1. 匹配条件开始标记:#ifdef 或 #ifndef
        
        // 匹配 #ifdef h5:[ '#ifdef h5', 'h5', index: 0, input: '#ifdef h5', groups: undefined ]
        // 匹配 #ifdef weapp:['#ifdef weapp', 'weapp', index: 0, input: '#ifdef weapp', groups: undefined]
        const ifdefMatch = commentText.match(/^#ifdef\s+(\w+)/)
        // 匹配 #ifndef h5:['#ifndef h5', 'h5', index: 0, input: '#ifndef h5', groups: undefined]
        const ifndefMatch = commentText.match(/^#ifndef\s+(\w+)/)

        if (ifdefMatch || ifndefMatch) {
          // 解析条件类型和目标平台
          const isIfndef = !!ifndefMatch // 是否为否定条件(ifndef)
          const targetPlatform = isIfndef ? ifndefMatch[1] : ifdefMatch[1] // 目标平台
          
          // 判断当前条件块是否应该保留:
          // - #ifdef平台:当前平台匹配时保留,比如当前平台是 h5, ifdef 平台也是 h5 则保留
          // - #ifndef平台:当前平台不匹配时保留,比如当前平台是 h5, ifndef 平台是 weapp 则保留
          const shouldKeep = isIfndef 
            ? (targetPlatform !== currentPlatform)  // 否定条件:平台不同则保留
            : (targetPlatform === currentPlatform)  // 肯定条件:平台相同则保留
          
          // 将条件块信息压入栈中,记录:
          // - node: 开始注释节点本身
          // - shouldKeep: 是否保留此条件块的内容
          // - startIndex: 在父节点中的位置索引
          // - parent: 父容器节点
          // - endNode: 结束注释节点(暂未找到,初始为null)
          processingStack.push({
            node,                    // 条件开始注释节点
            targetPlatform,          // 目标平台名称
            shouldKeep,              // 是否保留此条件块
            startIndex: node.parent.nodes.indexOf(node), // 在父节点列表中的位置
            parent: node.parent,     // 父容器节点
            endNode: null            // 将在找到匹配的 #endif 时填充
          })
          
          // 无论条件是否满足,条件注释本身都需要删除(不输出到最终 CSS)
          nodesToRemove.push(node)
        }
        
        // 2. 匹配条件结束标记:#endif
        if (commentText === '#endif' && processingStack.length > 0) {
          // 获取栈顶的条件块(最近未匹配的 #ifdef/#ifndef)
          const currentCondition = processingStack[processingStack.length - 1]
          
          // 记录结束节点的信息
          currentCondition.endNode = node          // 结束注释节点
          currentCondition.endIndex = node.parent.nodes.indexOf(node) // 结束位置索引
          
          // 如果此条件块不应该保留(shouldKeep 为 false)
          if (!currentCondition.shouldKeep) {
            const parent = currentCondition.parent   // 条件块的父容器
            const startIdx = currentCondition.startIndex  // 开始位置
            const endIdx = currentCondition.endIndex      // 结束位置
            
            // 标记从 #ifdef/#ifndef 到 #endif 之间的所有节点
            // 注意:不包含开始和结束注释本身(它们已单独标记)
            for (let i = startIdx + 1; i < endIdx; i++) {
              const childNode = parent.nodes[i]
              // 确保节点存在且不是结束注释节点本身
              if (childNode && childNode !== node) {
                nodesToRemove.push(childNode)
              }
            }
          }
          
          // #endif 注释本身也需要删除
          nodesToRemove.push(node)
          // 条件块处理完成,从栈中弹出
          processingStack.pop()
        }
    }
})
  1. 统一删除
js 复制代码
// 遍历所有标记要删除的节点,执行删除操作
nodesToRemove.forEach(node => {
  // 检查节点是否仍有父节点(避免重复删除或已删除的节点)
  if (node && node.parent) {
    node.remove()
  }
})

这里通过举例解释下上述代码为啥要判断 node.parent。

js 复制代码
// 假设 nodesToRemove 数组中包含两个节点 A 和 B
const nodesToRemove = [nodeA, nodeB]

// 情况:A 和 B 是父子关系
// nodeA 是父容器,nodeB 是子节点
// nodeB.parent === nodeA

// 当先删除 nodeA 时:
nodeA.remove() // PostCSS会同时删除 nodeA 和它的所有子节点

// 此时 nodeB 的状态:
console.log(nodeB.parent) // null(因为父节点已被移除)
console.log(nodeB.removed) // true(标记为已删除)

// 如果随后尝试删除 nodeB:
nodeB.remove() // 报错!因为 nodeB 已经没有了 parent 引用

测试插件

写完了插件,可使用 test-plugin.js 文件进行测试。

js 复制代码
// test-plugin.js
const postcss = require('postcss')
const fs = require('fs')
const ifdefPlugin = require('./postcss-ifdef-plugin')

const css = fs.readFileSync('test.css', 'utf8')

// 测试 H5 平台
postcss([ifdefPlugin({ platform: 'h5' })])
  .process(css, { from: undefined })
  .then(result => {
    console.log('=== H5 平台输出 ===')
    console.log(result.css)
    fs.writeFileSync('output.h5.css', result.css)
  })

// 测试 微信小程序平台
postcss([ifdefPlugin({ platform: 'weapp' })])
  .process(css, { from: undefined })
  .then(result => {
    console.log('\n=== 微信小程序平台输出 ===')
    console.log(result.css)
    fs.writeFileSync('output.weapp.css', result.css)
  })

执行 node test-plugin,控制台输出预期结果:

bash 复制代码
=== H5 平台输出 ===
.container {
  color: black;
  padding: 10px;
  background-color: blue;
  font-size: 16px;
  
  border: 1px solid #ccc;
}
.h5-only {
  display: block;
  color: red;
}

=== 微信小程序平台输出 ===
.container {
  color: black;
  padding: 10px;
  background-color: green;
  margin: 5px;
  
  border: 1px solid #ccc;
}
.weapp-only {
  display: flex;
  color: green;
}
相关推荐
0思必得02 小时前
[Web自动化] Selenium处理动态网页
前端·爬虫·python·selenium·自动化
东东5163 小时前
智能社区管理系统的设计与实现ssm+vue
前端·javascript·vue.js·毕业设计·毕设
catino3 小时前
图片、文件的预览
前端·javascript
layman05285 小时前
webpack5 css-loader:从基础到原理
前端·css·webpack
半桔5 小时前
【前端小站】CSS 样式美学:从基础语法到界面精筑的实战宝典
前端·css·html
AI老李5 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·postcss
_OP_CHEN5 小时前
【前端开发之CSS】(一)初识 CSS:网页化妆术的终极指南,新手也能轻松拿捏页面美化!
前端·css·html·网页开发·样式表·界面美化
啊哈一半醒5 小时前
CSS 主流布局
前端·css·css布局·标准流 浮动 定位·flex grid 响应式布局
PHP武器库5 小时前
ULUI:不止于按钮和菜单,一个专注于“业务组件”的纯 CSS 框架
前端·css
电商API_180079052475 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫