From e44b16276d13df6e375b867585335c72ecb0b011 Mon Sep 17 00:00:00 2001 From: xuegan Date: Wed, 5 Feb 2025 19:53:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8Dcssif=E9=97=AE?= =?UTF-8?q?=E9=A2=98&=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strip-conditional-loader.js | 85 +++----- .../test/platform/common/css-if.spec.js | 198 ++++++++++++++++++ 2 files changed, 231 insertions(+), 52 deletions(-) create mode 100644 packages/webpack-plugin/test/platform/common/css-if.spec.js diff --git a/packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js b/packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js index 1a90700d0..b852f3971 100644 --- a/packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js +++ b/packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js @@ -42,56 +42,36 @@ function parse (cssString) { const ast = [] const nodeStack = [] let currentChildren = ast - function pushConditionalNode (nodeType, condition) { - // 获取父节点的 children 数组 - const parentChildren = nodeStack.length > 0 ? nodeStack[nodeStack.length - 1] : ast - const node = new Node(nodeType, condition) - parentChildren.push(node) - // 入栈 - nodeStack.push(parentChildren) - currentChildren = node.children - } tokens.forEach(token => { - switch (token.type) { - case 'text': { - // 生成 Text 节点,保存代码文本 - const textNode = new Node('Text') - textNode.value = token.content - currentChildren.push(textNode) - break - } - case 'if': { - pushConditionalNode('If', token.condition) - break + if (token.type === 'text') { + const node = new Node('Text') + node.value = token.content + currentChildren.push(node) + } else if (token.type === 'if') { + const node = new Node('If', token.condition) + currentChildren.push(node) + nodeStack.push(currentChildren) + currentChildren = node.children + } else if (token.type === 'elif') { + if (nodeStack.length === 0) { + throw new Error('elif without a preceding if') } - case 'elif': { - // 处理 mpx-elif:回到 if 块的父级 children 数组 - if (nodeStack.length === 0) { - throw new Error('elif without a preceding if') - } - currentChildren = nodeStack[nodeStack.length - 1] - pushConditionalNode('Elif', token.condition) - break + currentChildren = nodeStack[nodeStack.length - 1] + const node = new Node('ElseIf', token.condition) + currentChildren.push(node) + currentChildren = node.children + } else if (token.type === 'else') { + if (nodeStack.length === 0) { + throw new Error('else without a preceding if') } - case 'else': { - if (nodeStack.length === 0) { - throw new Error('else without a preceding if') - } - currentChildren = nodeStack[nodeStack.length - 1] - pushConditionalNode('Else', null) - break + currentChildren = nodeStack[nodeStack.length - 1] + const node = new Node('Else') + currentChildren.push(node) + currentChildren = node.children + } else if (token.type === 'end') { + if (nodeStack.length > 0) { + currentChildren = nodeStack.pop() } - case 'end': { - // 结束当前条件块,弹出上一级 children 指针 - if (nodeStack.length > 0) { - currentChildren = nodeStack.pop() - } else { - throw new Error('end without matching if') - } - break - } - default: - break } }) return ast @@ -112,24 +92,25 @@ function evaluateCondition (condition, defs) { function traverseAndEvaluate (ast, defs) { let output = '' - + let batchedIf = false function traverse (nodes) { for (const node of nodes) { - if (node.type === 'Rule') { + if (node.type === 'Text') { output += node.value } else if (node.type === 'If') { // 直接判断 If 节点 + batchedIf = false if (evaluateCondition(node.condition, defs)) { traverse(node.children) + batchedIf = true } - } else if (node.type === 'ElseIf') { + } else if (node.type === 'ElseIf' && !batchedIf) { if (evaluateCondition(node.condition, defs)) { traverse(node.children) - return + batchedIf = true } - } else if (node.type === 'Else') { + } else if (node.type === 'Else' && !batchedIf) { traverse(node.children) - return } } } diff --git a/packages/webpack-plugin/test/platform/common/css-if.spec.js b/packages/webpack-plugin/test/platform/common/css-if.spec.js new file mode 100644 index 000000000..6e8bfc615 --- /dev/null +++ b/packages/webpack-plugin/test/platform/common/css-if.spec.js @@ -0,0 +1,198 @@ +const cssIfLoader = require('../../../lib/style-compiler/strip-conditional-loader') + +describe('css-if webpack loader - 测试用例', () => { + // 测试简单的 if/else 条件 + it('简单条件: 当 isMobile 为 true 时,保留 if 分支内容', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true } }) + } + const inputCSS = ` +/*@mpx-if(isMobile)*/ +.mobile { display: block; } +/*@mpx-else*/ +.desktop { display: block; } +/*@mpx-end*/ + ` + const output = cssIfLoader.call(context, inputCSS) + expect(output).toContain('.mobile') + expect(output).not.toContain('.desktop') + }) + + it('简单条件: 当 isMobile 为 false 时,保留 else 分支内容', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: false } }) + } + const inputCSS = ` +/*@mpx-if(isMobile)*/ +.mobile { display: block; } +/*@mpx-else*/ +.desktop { display: block; } +/*@mpx-end*/ + ` + const output = cssIfLoader.call(context, inputCSS) + expect(output).toContain('.desktop') + expect(output).not.toContain('.mobile') + }) + + // 测试嵌套条件 + it('嵌套条件: 外层 isMobile 为 true 内层 hasFeature 为 true, 输出嵌套 if 分支内容', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true, hasFeature: true } }) + } + const inputCSS = ` +body { margin: 0; } +/*@mpx-if(isMobile)*/ +.mobile { + display: block; + /*@mpx-if(hasFeature)*/ + .feature { color: red; } + /*@mpx-else*/ + .feature { color: blue; } + /*@mpx-end*/ +} +/*@mpx-else*/ +.desktop { display: block; } +/*@mpx-end*/ +header { color: red } + ` + const output = cssIfLoader.call(context, inputCSS) + expect(output).toContain('.mobile') + expect(output).toContain('.feature { color: red; }') + expect(output).toContain('header { color: red }') + expect(output).not.toContain('.feature { color: blue; }') + expect(output).not.toContain('.desktop') + }) + + it('嵌套条件: 外层 isMobile 为 true 内层 hasFeature 为 false, 输出内层 else 分支内容', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true, hasFeature: false } }) + } + const inputCSS = ` +body { margin: 0; } +/*@mpx-if(isMobile)*/ +.mobile { + display: block; + /*@mpx-if(hasFeature)*/ + .feature { color: red; } + /*@mpx-else*/ + .feature { color: blue; } + /*@mpx-end*/ +} +/*@mpx-else*/ +.desktop { display: block; } +/*@mpx-end*/ + ` + const output = cssIfLoader.call(context, inputCSS) + expect(output).toContain('.mobile') + expect(output).toContain('.feature { color: blue; }') + expect(output).not.toContain('.feature { color: red; }') + expect(output).not.toContain('.desktop') + }) + + // 测试多个条件分支:if、elif、else 的情况 + it('多个条件分支: 优先匹配 if 分支,其次 elif,再到 else', () => { + // 测试1: isMobile 为 false,isTablet 为 true,匹配 elif 分支 + let context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: false, isTablet: true } }) + } + const inputCSS = ` +header {} +/*@mpx-if(isMobile)*/ +.mobile { display: block; } +/*@mpx-elif(isTablet)*/ +.tablet { display: block; } +/*@mpx-else*/ +.desktop { display: block; } +/*@mpx-end*/ +body {} + ` + let output = cssIfLoader.call(context, inputCSS) + expect(output).not.toContain('.mobile') + expect(output).toContain('.tablet') + expect(output).not.toContain('.desktop') + expect(output).toContain('header {}') + expect(output).toContain('body {}') + + // 测试2: isMobile 与 isTablet 均为 false,匹配 else 分支 + context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: false, isTablet: false } }) + } + output = cssIfLoader.call(context, inputCSS) + expect(output).not.toContain('.mobile') + expect(output).not.toContain('.tablet') + expect(output).toContain('.desktop') + + // 测试3: isMobile 为 true(优先匹配 if 分支),即使 isTablet 为 true + context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true, isTablet: true } }) + } + output = cssIfLoader.call(context, inputCSS) + expect(output).toContain('.mobile') + expect(output).not.toContain('.tablet') + expect(output).not.toContain('.desktop') + }) + + // 测试多个 if 块在一起的情况 + it('多个 if 块处理: 不同 if 条件处理各自独立', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true, showHeader: false } }) + } + const inputCSS = ` +/*@mpx-if(isMobile)*/ +.mobile { display: block; } +/*@mpx-end*/ + +/*@mpx-if(showHeader)*/ +.header { height: 100px; } +/*@mpx-else*/ +.header { height: 50px; } +/*@mpx-end*/ + ` + const output = cssIfLoader.call(context, inputCSS) + // 第一个 if 块:isMobile 为 true,输出 .mobile + expect(output).toContain('.mobile') + // 第二个 if 块:showHeader 为 false,应该保留 else 分支 + expect(output).not.toContain('height: 100px;') + expect(output).toContain('height: 50px;') + }) + it('多个 if 嵌套 elif 块处理', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true, showHeader: true } }) + } + const inputCSS = ` +/*@mpx-if(isMobile)*/ +.mobile { display: block; } + /*@mpx-if(false)*/ + .test1 {} + /*@mpx-elif(showHeader)*/ + .test2 {} + /*@mpx-end*/ +/*@mpx-end*/ + ` + const output = cssIfLoader.call(context, inputCSS) + expect(output).toContain('.mobile') + expect(output).toContain('.test2') + }) + + it('错误处理: 缺少开始标签', () => { + const context = { + cacheable: jest.fn(), + getMpx: () => ({ defs: { isMobile: true } }) + } + const inputCSS = ` +/*@mpx-elif(isMobile)*/ +.mobile { display: block; } +` + // 预期这种情况下应该抛出异常或给出警告 + expect(() => cssIfLoader.call(context, inputCSS)).toThrow(new Error('elif without a preceding if')) + }) +})