UNPKG

postcss-mobile-forever

Version:

PostCSS 伸缩视图转换插件。To adapt different displays by one mobile viewport.

871 lines (807 loc) 40.2 kB
const { removeDuplicateDecls, mergeRules, createRegArrayChecker, createIncludeFunc, createExcludeFunc, isMatchedStr, createContainingBlockWidthDecls, hasNoneRootContainingBlockComment, hasRootContainingBlockComment, hasIgnoreComments, convertNoFixedMediaQuery, convertMaxMobile, convertMobile, convertFixedMediaQuery, hasApplyWithoutConvertComment, isMatchedSelectorProperty, hasWritingModeComment, convertPropValue, convertMaxMobile_FIXED_LR, convertMaxMobile_FIXED, convertRem, convertRem_FIXED_LR, convertRem_FIXED, } = require("./src/logic-helper"); const { createPropListMatcher } = require("./src/prop-list-matcher"); const { appendMediaRadioPxOrReplaceMobileVwFromPx, appendDemoContent, appendCentreRoot, appendCentreBody, appendDisplaysRule, appendCSSVar, extractFile, appendSiders, appendRemFontSize, } = require("./src/css-generator"); const { PLUGIN_NAME, demoModeSelector, lengthProps, applyComment, rootCBComment, notRootCBComment, ignoreNextComment, ignorePrevComment, verticalComment, fullW, fullH, autoOverflow, } = require("./src/constants"); const { pxToMediaQueryPx_noUnit, vwToMediaQueryPx_noUnit, percentToMediaQueryPx_FIXED_noUnit } = require("./src/unit-transfer"); const path = require('path'); const { bookObj } = require("./src/utils"); const { /** 用于验证字符串是否为“数字px”的形式 */ preflightReg, varTestReg, } = require("./src/regexs"); const defaults = { /** 页面最外层选择器,如 `#app`、`.root-class` */ appSelector: "#app", /** 标准视图宽度 */ viewportWidth: 750, /** rem 模式的基准宽度 */ basicRemWidth: null, /** 视图展示的最大宽度,单位会转换成诸如 min(vw, px) 的形式 */ maxDisplayWidth: null, /** 打开媒体查询,打开后将自动关闭 maxDisplayWidth */ enableMediaQuery: false, /** 桌面端宽度 */ desktopWidth: 600, /** 移动端横屏宽度 */ landscapeWidth: 425, /** 宽度断点,视图大于这个宽度,则页面使用桌面端宽度 */ minDesktopDisplayWidth: null, /** 高度断点,视图小于这个高度,并满足一定条件,则页面使用移动端横屏宽度 */ maxLandscapeDisplayHeight: 640, /** 用于指定应用根元素作为包含块 */ appContainingBlock: "calc", // manual | auto | calc /** 指定 appContainingBlock auto 后,指定包含块的选择器名称,应是 appSelector 父元素 */ necessarySelectorWhenAuto: "body", /** 在页面外层展示边框吗 */ border: false, /** 不做桌面端的适配 */ disableDesktop: false, /** 不做移动端横屏的适配 */ disableLandscape: false, /** 不做视口单位转换 */ disableMobile: false, /** 排除文件 */ exclude: null, /** 包含文件 */ include: null, /** 单位精确到小数点后几位? */ unitPrecision: 3, /** 选择器黑名单列表 */ selectorBlackList: [], /** 属性黑名单列表 */ propertyBlackList: {}, /** 属性值的黑名单列表 */ valueBlackList: [], /** 是否处理某个属性? */ propList: ['*'], /** 包含块是根元素的选择器列表 */ rootContainingBlockSelectorList: [], /** 纵向书写模式的选择器列表 */ verticalWritingSelectorList: [], /** 移动端竖屏转换单位 */ mobileUnit: "vw", /** 侧边内容配置 */ side: { /** 侧边宽度 */ width: null, /** 上下左右间隔 */ gap: 18, /** 左上选择器 */ selector1: null, /** 右上选择器 */ selector2: null, /** 右下选择器 */ selector3: null, /** 左下选择器 */ selector4: null, width1: null, width2: null, width3: null, width4: null, }, /** 自定义注释名称 */ comment: { /** 直接添加进屏幕媒体查询,不转换 */ applyWithoutConvert: applyComment, /** 包含块注释 */ rootContainingBlock: rootCBComment, /** 非包含块注释 */ notRootContainingBlock: notRootCBComment, /** 忽略选择器内的转换 */ ignoreNext: ignoreNextComment, /** 忽略本行转换 */ ignoreLine: ignorePrevComment, /** 纵向书写模式 */ verticalWritingMode: verticalComment, }, /** 添加标识,用于调试 */ demoMode: false, /** 和长度有关的自定义属性 */ customLengthProperty: { /** 根包含块属性,用于 left 和 right */ rootContainingBlockList_LR: [], /** 根包含块属性,用于非 left 和 right 的属性 */ rootContainingBlockList_NOT_LR: [], /** 祖先包含块属性 */ ancestorContainingBlockList: [], /** 关闭自动添加到桌面端和横屏,设置了以上三个任意选项后,该值强制为 true */ disableAutoApply: false, }, /** 实验性功能 */ experimental: { /** 是否拆分桌面端和横屏样式文件,提取移动端、桌面端和横屏代码,使用 `@import` 引入 */ extract: false, /** 视图展示的最小宽度 */ minDisplayWidth: null, } }; const TYPE_REG = "regex"; const TYPE_ARY = "array"; /** 检查是否是正则类型或包含正则的数组 */ const checkRegExpOrArray = createRegArrayChecker(TYPE_REG, TYPE_ARY); /** 如果不包含,则返回 true,不转换 */ const hasNoIncludeFile = createIncludeFunc(TYPE_REG, TYPE_ARY); /** 如果排除,则返回 true,不转换 */ const hasExcludeFile = createExcludeFunc(TYPE_REG, TYPE_ARY); /** * 视口类型可以分为 3 种,分别是移动端竖屏、移动端横屏以及桌面端。 * * 插件 postcss-px-to-viewport 用于解决移动端竖屏适配问题。 * 本插件用于解决在只有 1 套 UI 的情况下,适配移动端竖屏、横屏和桌面端的问题。 * * 通过媒体查询设置在移动端横屏和桌面端两种情况下的 app 视口宽度,根据视口宽度和设计图 * 宽度的比例,将两种情况的 px 元素的比例计算后的尺寸放入媒体查询中。 * * 以上是本插件的一种模式,即媒体查询模式,这种模式生成代码量大,因此插件提供 * 了另一种生成代码量小、功能效果近似的模式,也即 max-vw-mode。 */ module.exports = (options = {}) => { const opts = { ...defaults, ...options, side: { ...defaults.side, ...options.side, }, comment: { ...defaults.comment, ...options.comment, }, customLengthProperty: { ...defaults.customLengthProperty, ...options.customLengthProperty, }, experimental: { ...defaults.experimental, ...options.experimental, }, }; const { viewportWidth, enableMediaQuery, desktopWidth, landscapeWidth, appSelector, border, disableMobile, minDesktopDisplayWidth, maxLandscapeDisplayHeight, include, exclude, unitPrecision, side, demoMode, selectorBlackList, propertyBlackList, valueBlackList, rootContainingBlockSelectorList, verticalWritingSelectorList, propList, maxDisplayWidth, comment, mobileUnit, customLengthProperty, experimental, appContainingBlock, necessarySelectorWhenAuto, basicRemWidth, } = opts; const disableDesktop = enableMediaQuery ? opts.disableDesktop : true; const disableLandscape = enableMediaQuery ? opts.disableLandscape : true; const { extract, minDisplayWidth } = experimental || {}; const { width: sideWidth, width1: sideW1, width2: sideW2, width3: sideW3, width4: sideW4, gap: sideGap, selector1: side1, selector2: side2, selector3: side3, selector4: side4 } = side; const { applyWithoutConvert: AWC_CMT, rootContainingBlock: RCB_CMT, notRootContainingBlock: NRCB_CMT, ignoreNext: IN_CMT, ignoreLine: IL_CMT, verticalWritingMode: VWM_CMT } = comment; const { rootContainingBlockList_LR, rootContainingBlockList_NOT_LR, ancestorContainingBlockList, disableAutoApply } = customLengthProperty; const _minDesktopDisplayWidth = minDesktopDisplayWidth == null ? desktopWidth : minDesktopDisplayWidth; const excludeType = checkRegExpOrArray(opts, "exclude"); const includeType = checkRegExpOrArray(opts, "include"); const satisfyPropList = createPropListMatcher(propList); /** 需要添加到桌面端和横屏的 css 变量 */ const expectedLengthVars = [...new Set([].concat(rootContainingBlockList_LR, rootContainingBlockList_NOT_LR, ancestorContainingBlockList))].filter(e => e != null); const defaultViewportWidth = typeof viewportWidth === "function" ? viewportWidth('') : viewportWidth; return { postcssPlugin: PLUGIN_NAME, prepare(result) { const file = result.root && result.root.source && result.root.source.input.file || ''; const from = result.opts.from; // 包含文件 if(hasNoIncludeFile(include, file, includeType)) return; // 排除文件 if(hasExcludeFile(exclude, file, excludeType)) return; /** 桌面端视图下的媒体查询 */ let desktopViewAtRule = null; /** 移动端横屏下的媒体查询 */ let landScapeViewAtRule = null; /** 桌面端和移动端横屏公共的媒体查询,用于节省代码体积 */ let sharedAtRule = null; /** 用于在开启 border 后的 dvh 检测,如果浏览器支持,则应用 dvh,`@supports (min-height: 100dvh)` */ let dvhAtRule = null; /** 当前选择器是否是 fixed 布局 */ let hadFixed = null; /** 当前选择器是否是纵向书写模式 */ let isVerticalWritingMode = false; /** 当前选择器 */ let selector = null; // 是否动态视图宽度? const isDynamicViewportWidth = typeof viewportWidth === "function"; /** 视图宽度 */ const _viewportWidth = isDynamicViewportWidth ? viewportWidth(file) : viewportWidth; /** 选择器在黑名单吗 */ let blackListedSelector = null; /** 是否添加过调试代码了? */ let addedDemo = false; /** 依赖根包含块宽度的属性 */ let containingBlockWidthDeclsMap = null; /** 不是被选择器包裹的属性不处理,例如 @font-face 中的属性 */ let walkedRule = false; /** 媒体查询模式,media-query mode */ const mqMode = enableMediaQuery === true; /** rem 模式,相比 max-vw 模式有更小的产包 */ const remMode = !mqMode && (mobileUnit === "rem" || basicRemWidth != null); /** max-vw 模式 */ const maxVwMode = !mqMode && !remMode && (maxDisplayWidth != null); /** vw 模式 */ const vwMode = !mqMode && !remMode && !maxVwMode; /** rem 模式的基准宽度 */ const _basicRemWidth = remMode ? basicRemWidth == null ? defaultViewportWidth : basicRemWidth : null; /** rem 模式下,基准宽度和当前视图宽度的比值,用于将视图统一转换为基准视图宽度 */ const remRatio = _basicRemWidth / _viewportWidth; const fontViewportUnit = remMode ? "rem" : mobileUnit; /** 忽略矫正 fixed 定位 */ const ignoreToCorrectFixed = ["manual", "auto"].includes(appContainingBlock); /** 需要添加到应用根元素的样式,该样式用于指定根元素为包含块 */ const autoAppContainingBlock = appContainingBlock === "auto"; /** 忽略转换的 at 规则 */ let ignoreAtRule = false; /** 是否是 keyframes 规则 */ let isKeyframesAtRule = false; let desktopKeyframesAtRule = null; let landscapeKeyframesAtRule = null; let siders = [{ atRule: null, selector: side1, width: sideW1 ?? sideWidth, gap: sideGap, }, { atRule: null, selector: side2, width: sideW2 ?? sideWidth, gap: sideGap, }, { atRule: null, selector: side3, width: sideW3 ?? sideWidth, gap: sideGap, }, { atRule: null, selector: side4, width: sideW4 ?? sideWidth, gap: sideGap, }]; // { atRule, selector, width, gap } /** 一个选择器内优先级最高的各个属性 */ const priorityProps = new Map(); /** 给侧边用的存储宽度属性的 */ let widthValForSiders = null; if (maxVwMode || vwMode) { return { Once(_, postcss) { /** 检测 dvh 支不支持,支持就应用,不然移动端有的浏览器的 vh 会导致滚动 */ dvhAtRule = postcss.atRule({ name: "supports", params: "(min-height: 100dvh)", nodes: [] }); }, AtRule: initContainingBlockWidthDeclsMap, Rule, Declaration, RuleExit: transformContainingBlockWidthDecls, AtRuleExit: transformContainingBlockWidthDecls, OnceExit(css) { const appendedDvh = dvhAtRule.nodes.length > 0; if (appendedDvh) css.append(dvhAtRule); } } } let maxRemAtRule = []; let hasHtmlRule = false; if (remMode) { return { Once(_, postcss) { dvhAtRule = postcss.atRule({ name: "supports", params: "(min-height: 100dvh)", nodes: [] }); }, AtRule: initContainingBlockWidthDeclsMap, Rule, Declaration, RuleExit: transformContainingBlockWidthDecls, AtRuleExit: transformContainingBlockWidthDecls, OnceExit(css, postcss) { const appendedDvh = dvhAtRule.nodes.length > 0; if (appendedDvh) css.append(dvhAtRule); if (hasHtmlRule) { const _maxDisplayWidth = [].concat(maxDisplayWidth); maxRemAtRule = _maxDisplayWidth.map(w => { const mediaRule = postcss.atRule({ name: "media", params: `(min-width: ${w}px)`, }); const htmlRule = postcss.rule({ selector: "html", }).append(bookObj({ prop: "font-size", value: `${w * 100 / _basicRemWidth}px`, important: true, })); mediaRule.append(htmlRule); return mediaRule; }); maxRemAtRule.forEach(atRule => css.append(atRule)); } } } } return { Once(_, postcss) { /** 桌面端视图下的媒体查询 */ desktopViewAtRule = postcss.atRule({ name: "media", params: `(min-width: ${_minDesktopDisplayWidth}px) and (min-height: ${maxLandscapeDisplayHeight}px)`, nodes: [] }); /** 移动端横屏下的媒体查询 */ const landscapeMediaStr_1 = `(min-width: ${_minDesktopDisplayWidth}px) and (max-height: ${maxLandscapeDisplayHeight}px)`; const landscapeMediaStr_2 = `(max-width: ${_minDesktopDisplayWidth}px) and (min-width: ${landscapeWidth}px) and (orientation: landscape)`; landScapeViewAtRule = postcss.atRule({ name: "media", params: `${landscapeMediaStr_1}, ${landscapeMediaStr_2}`, nodes: [] }); /** 桌面端和移动端横屏公共的媒体查询,用于节省代码体积 */ sharedAtRule = postcss.atRule({ name: "media", params: `(min-width: ${_minDesktopDisplayWidth}px), (orientation: landscape) and (max-width: ${_minDesktopDisplayWidth}px) and (min-width: ${landscapeWidth}px)`, nodes: [] }); /** 检测 dvh 支不支持,支持就应用,不然移动端有的浏览器的 vh 会导致滚动 */ dvhAtRule = postcss.atRule({ name: "supports", params: "(min-height: 100dvh)", nodes: [] }); }, AtRule(atRule, postcss) { isKeyframesAtRule = atRule.name === "keyframes"; ignoreAtRule = !isKeyframesAtRule; if (isKeyframesAtRule) { const params = atRule.params; desktopKeyframesAtRule = postcss.atRule({ name: "keyframes", params, nodes: [] }); landscapeKeyframesAtRule = postcss.atRule({ name: "keyframes", params, nodes: [] }); } }, AtRuleExit() { ignoreAtRule = false; isKeyframesAtRule = false; const appendedDesktopKeyframes = desktopKeyframesAtRule != null && desktopKeyframesAtRule.nodes.length > 0; const appendedLandcapeKeyframes = landscapeKeyframesAtRule != null && landscapeKeyframesAtRule.nodes.length > 0; if (appendedDesktopKeyframes) { desktopViewAtRule.append(desktopKeyframesAtRule); desktopKeyframesAtRule = null; } if (appendedLandcapeKeyframes) { landScapeViewAtRule.append(landscapeKeyframesAtRule); landscapeKeyframesAtRule = null; } }, Rule, Declaration(decl, postcss) { const prop = decl.prop; const val = decl.value; if (prop === "width") widthValForSiders = val; if (!walkedRule) return; // 不是 Rule 的属性则不转换 if (ignoreAtRule) return; // 不转换媒体查询中的属性 if (decl.book) return; // 被标记过不转换 if (blackListedSelector) return; // 属性在黑名单选择器中,不进行转换 if (!satisfyPropList(prop)) return; if (isMatchedSelectorProperty(propertyBlackList, selector, prop)) return; // 属性是否在黑名单中 if (isMatchedStr(valueBlackList, val)) return; // 属性值是否在黑名单中 if (prop === "position" && val === "fixed" && !ignoreToCorrectFixed) return hadFixed = true; if (hasIgnoreComments(decl, result, IN_CMT, IL_CMT)) return; // 如果有标注不转换注释,直接添加到桌面端和横屏,不进行转换 if (hasApplyWithoutConvertComment(decl, result, AWC_CMT)) { appendDisplaysRule(!disableDesktop, !disableLandscape, prop, val, decl.important, selector, postcss, { sharedAtRule, desktopViewAtRule, landScapeViewAtRule, isShare: priorityProps.get(prop) === decl, }); return; } // 是否忽略百分比转换?如果不忽略,则需要收集哪些属性会涉及到根包含块 if (!ignoreToCorrectFixed) { // 该属性是用于设置根包含块的变量属性 const isRootContainingBlockProp = rootContainingBlockList_LR.includes(prop) || rootContainingBlockList_NOT_LR.includes(prop); isRootContainingBlockProp && (hadFixed = true); // 受 fixed 布局影响的,需要在 ruleExit 中计算的属性 if (containingBlockWidthDeclsMap.has(prop) || isRootContainingBlockProp) { const important = decl.important; const mapDecl = containingBlockWidthDeclsMap.get(prop); if (mapDecl == null || important || !mapDecl.important) containingBlockWidthDeclsMap.set(prop, decl); return; } } // 预检正则,判断是否符合转换条件 if (preflightReg.test(val)) { const important = decl.important; // 添加桌面端、移动端媒体查询 appendMediaRadioPxOrReplaceMobileVwFromPx(postcss, selector, prop, val, disableDesktop, disableLandscape, disableMobile, { desktopViewAtRule, landScapeViewAtRule, sharedAtRule, important, decl, matchPercentage: false, expectedLengthVars, disableAutoApply, isLastProp: priorityProps.get(prop) === decl, isKeyframesAtRule, desktopKeyframesAtRule, landscapeKeyframesAtRule, convertMobile: (number, unit) => convertMobile(prop, number, unit, _viewportWidth, unitPrecision, fontViewportUnit, mobileUnit), convertDesktop: (number, unit, numberStr) => convertNoFixedMediaQuery(number, desktopWidth, _viewportWidth, unitPrecision, unit, numberStr), convertLandscape: (number, unit, numberStr) => convertNoFixedMediaQuery(number, landscapeWidth, _viewportWidth, unitPrecision, unit, numberStr), }); } else if ( // 值是指定的变量名称,则加入进桌面端和横屏的媒体查询 (expectedLengthVars.length > 0 && expectedLengthVars.some(varStr => val.includes(varStr))) || // 默认行为,未指定长度变量列表,属性和长度有关,并且值包含变量 val(...),则加入进桌面端和横屏的媒体查询 (expectedLengthVars.length === 0 && !disableAutoApply && lengthProps.includes(prop) && varTestReg.test(val))) { const enabledDesktop = !disableDesktop; const enabledLandscape = !disableLandscape; appendCSSVar(enabledDesktop, enabledLandscape, prop, val, decl.important, selector, postcss, { sharedAtRule, desktopViewAtRule, landScapeViewAtRule, isLastProp: priorityProps.get(prop) === decl, }); } }, RuleExit(rule, postcss) { if (!walkedRule) return; if (ignoreAtRule) return; if (blackListedSelector) return; // 转换受 fixed 影响的属性的媒体查询值 containingBlockWidthDeclsMap.forEach((decl, prop) => { if (decl == null) return; const { value: val, important } = decl; const leftOrRight = prop === "left" || prop === "right" || rootContainingBlockList_LR.includes(prop); appendMediaRadioPxOrReplaceMobileVwFromPx(postcss, selector, prop, val, disableDesktop, disableLandscape, disableMobile, { desktopViewAtRule, landScapeViewAtRule, sharedAtRule, important, decl, matchPercentage: hadFixed, expectedLengthVars, disableAutoApply, isLastProp: priorityProps.get(prop) === decl, isKeyframesAtRule, desktopKeyframesAtRule, landscapeKeyframesAtRule, convertMobile: (number, unit) => convertMobile(prop, number, unit, _viewportWidth, unitPrecision, fontViewportUnit, mobileUnit), convertDesktop: (number, unit, numberStr) => convertFixedMediaQuery(number, desktopWidth, _viewportWidth, unitPrecision, unit, numberStr, hadFixed, leftOrRight), convertLandscape: (number, unit, numberStr) => convertFixedMediaQuery(number, landscapeWidth, _viewportWidth, unitPrecision, unit, numberStr, hadFixed, leftOrRight), }); }); containingBlockWidthDeclsMap = new Map(); // 自动获取并转换 sider 的宽度 let foundSide = null; if (widthValForSiders && (foundSide = siders.find(side => side.selector === selector))) { if (foundSide.width == null) { let convertedSideVal = null; const pxVal = widthValForSiders.match(/(.*?)(?=px$)/)?.[1]; if (pxVal != null) { convertedSideVal = pxToMediaQueryPx_noUnit(+pxVal, _viewportWidth, desktopWidth, unitPrecision); } else { const vwVal = widthValForSiders.match(/(.*?)(?=vw$)/)?.[1]; if (vwVal != null) { convertedSideVal = vwToMediaQueryPx_noUnit(+vwVal, desktopWidth, unitPrecision); } else { const percVal = hadFixed ? widthValForSiders.match(/(.*?)(?=%$)/)?.[1] : null; if (percVal != null) { convertedSideVal = percentToMediaQueryPx_FIXED_noUnit(+percVal, desktopWidth, unitPrecision); } } } foundSide.width = convertedSideVal; } } walkedRule = false; !addedDemo && demoMode && demoModeSelector === selector && (appendDemoContent(postcss, demoModeSelector, rule, desktopViewAtRule, landScapeViewAtRule, disableDesktop, disableLandscape, disableMobile), addedDemo = true); }, OnceExit(css, postcss) { const appendedDesktop = desktopViewAtRule.nodes.length > 0; const appendedLandscape = landScapeViewAtRule.nodes.length > 0; const appendedShared = sharedAtRule.nodes.length > 0; const appendedDvh = dvhAtRule.nodes.length > 0; if (extract) { /** * 清空 css 内容,并拆分为 `@import` * ```css * @import url(mobile.css) * @import url(landscape.css) screen and ... * @import url(desktop.css) screen and ... * ``` */ const matched = (file || '').match(/([^/\\]+)\.(\w+)(?:\?.+)?$/); const name = matched && matched[1] || "UNEXPECTED_FILE"; const ext = matched && matched[2] || "css"; const mobileFile = `mobile.${name}.${ext}`; const desktopFile = `desktop.${name}.${ext}`; const landscapeFile = `landscape.${name}.${ext}`; const sharedFile = `shared.${name}.${ext}`; /** 应用根目录 */ const rootDir = process.cwd(); /** 当前 css 文件路径 */ const curFilePath = from || ''; /** 目标文件夹 */ const targetDir = path.join(__dirname, ".temp"); /** 目标文件文件夹 */ const targetFileDir = path.join(targetDir, curFilePath.replace(/[^/\\]*$/, '').replace(rootDir, '')); const newSharedFilePath = path.join(targetFileDir, sharedFile); const atImportShared = postcss.atRule({ name: "import", params: `url(${newSharedFilePath}) ${sharedAtRule.params}` }); if (appendedDesktop) { const sidersMedia = appendSiders(postcss, siders, _minDesktopDisplayWidth, maxLandscapeDisplayHeight); if (sidersMedia.length > 0) css.append(sidersMedia); // 侧边样式添加入移动端样式文件中,移动端样式文件也就是主样式文件 } if (appendedShared) css.append(atImportShared); const mobileCss = css.toString(); // without media query const mobilePromise = extractFile(mobileCss, mobileFile, targetFileDir); // 提取移动端 css atImportShared.remove(); let sharedPromise = Promise.resolve(); if (appendedShared) { mergeRules(sharedAtRule); // 合并相同选择器中的内容 removeDuplicateDecls(sharedAtRule); // 移除重复属性 const sharedCss = postcss.root().append(sharedAtRule.nodes).toString(); // without media query sharedPromise = extractFile(sharedCss, sharedFile, targetFileDir); // 提取公共 css } let desktopPromise = Promise.resolve(); if (appendedDesktop) { const desktopCssRules = postcss.root(); // without media query mergeRules(desktopViewAtRule); // 合并相同选择器中的内容 removeDuplicateDecls(desktopViewAtRule); // 移除重复属性 desktopCssRules.append(desktopViewAtRule.nodes); // if (appendedShared) desktopCssRules.prepend(atImportShared); const desktopCss = desktopCssRules.toString(); desktopPromise = extractFile(desktopCss, desktopFile, targetFileDir); // 提取桌面端 css } let landscapePromise = Promise.resolve(); if (appendedLandscape) { const landscapeCssRules = postcss.root(); // without media query mergeRules(landScapeViewAtRule); // 合并相同选择器中的内容 removeDuplicateDecls(landScapeViewAtRule); // 移除重复属性 landscapeCssRules.append(landScapeViewAtRule.nodes); // if (appendedShared) landscapeCssRules.prepend(atImportShared); const landscapeCss = landscapeCssRules.toString(); landscapePromise = extractFile(landscapeCss, landscapeFile, targetFileDir); // 提取横屏 css } // 清空文件内容,并替换为 @import,导入移动端、桌面端和横屏 const atImportMobile = postcss.atRule({ name: "import", params: `url(${path.join(targetFileDir, mobileFile)})` }); const atImportDesktop = postcss.atRule({ name: "import", params: `url(${path.join(targetFileDir, desktopFile)}) ${desktopViewAtRule.params}` }); const atImportLandscape = postcss.atRule({ name: "import", params: `url(${path.join(targetFileDir, landscapeFile)}) ${landScapeViewAtRule.params}` }); css.walkRules(rule => { rule.removeAll(); }); if (appendedLandscape) css.prepend(atImportLandscape); if (appendedDesktop) css.prepend(atImportDesktop); css.prepend(atImportMobile); return Promise.all([mobilePromise, desktopPromise, landscapePromise, sharedPromise]); } else { if (appendedDesktop) { mergeRules(desktopViewAtRule); // 合并相同选择器中的内容 removeDuplicateDecls(desktopViewAtRule); // 移除重复属性 css.append(desktopViewAtRule); // 样式中添加桌面端媒体查询 const sidersMedia = appendSiders(postcss, siders, _minDesktopDisplayWidth, maxLandscapeDisplayHeight); if (sidersMedia.length > 0) { css.append(sidersMedia); } } if (appendedLandscape) { mergeRules(landScapeViewAtRule); removeDuplicateDecls(landScapeViewAtRule); // 移除重复属性 css.append(landScapeViewAtRule); // 样式中添加横屏媒体查询 } if (appendedShared) { mergeRules(sharedAtRule); removeDuplicateDecls(sharedAtRule); // 移除重复属性 css.append(sharedAtRule); // 样式中添加公共媒体查询 } } if (appendedDvh) css.append(dvhAtRule); }, }; function Rule(rule, postcss) { if (rule.processedLimitedCentreWidth || rule.processedAutoAppContainingBlock) return; // 对于用 maxDisplayWidth 来限制宽度的根元素,会在原来的选择器内添加属性,这会导致重新执行这个选择器,这里对已经处理过的做标记判断,防止死循环 if (mqMode) { // 验证当前选择器在媒体查询中吗,不对选择器中的内容转换 if (ignoreAtRule) return ; walkedRule = true; widthValForSiders = null; } selector = rule.selector; if (isMatchedStr(selectorBlackList, selector)) return blackListedSelector = true; hadFixed = false; isVerticalWritingMode = false; blackListedSelector = false; if (remMode && !hasHtmlRule) { hasHtmlRule = selector === "html"; hasHtmlRule && appendRemFontSize(rule, _basicRemWidth); } // 设置页面最外层 class 的最大宽度,并居中 if (selector === appSelector) { if (autoAppContainingBlock) { rule.prepend(bookObj(fullW), bookObj(fullH), bookObj(autoOverflow)); rule.processedAutoAppContainingBlock = true; } else appendCentreRoot(postcss, selector, disableDesktop, disableLandscape, border, { rule, desktopViewAtRule, landScapeViewAtRule, sharedAtRule, dvhAtRule, desktopWidth, landscapeWidth, maxWidthMode: maxVwMode || remMode, maxDisplayWidth, minDisplayWidth, }); } if (selector === necessarySelectorWhenAuto && autoAppContainingBlock) { appendCentreBody(postcss, selector, disableDesktop, disableLandscape, border, { rule, desktopViewAtRule, landScapeViewAtRule, sharedAtRule, desktopWidth, landscapeWidth, dvhAtRule, maxWidthMode: maxVwMode || remMode, maxDisplayWidth, minDisplayWidth, }); } // 标记优先级最高的各个属性 mqMode && priorityProps.clear(); rule.walkDecls(decl => { const { prop, value: val } = decl; if (mqMode) { const mapProp = priorityProps.get(prop); if (mapProp == null || decl.important || !mapProp.important) { priorityProps.set(prop, decl); } } if (!isVerticalWritingMode && prop === "writing-mode" && ["vertical-rl", "vertical-lr", "sideways-rl", "sideways-lr", "tb", "tb-rl"].includes(val)) isVerticalWritingMode = true; }); if (!ignoreToCorrectFixed) if (hasRootContainingBlockComment(rule, RCB_CMT) || // 有标志*根包含块*的注释吗? isMatchedStr(rootContainingBlockSelectorList, selector)) hadFixed = true; if (hasWritingModeComment(rule, VWM_CMT) || // 有标志*纵向书写模式*的注释吗? isMatchedStr(verticalWritingSelectorList, selector)) isVerticalWritingMode = true; initContainingBlockWidthDeclsMap(rule); } function initContainingBlockWidthDeclsMap(typeRule) { if (ignoreToCorrectFixed || hasNoneRootContainingBlockComment(typeRule, NRCB_CMT)) // 有标志*非根包含块*的注释吗?或者指定了忽略转换百分比单位 containingBlockWidthDeclsMap = new Map(); else containingBlockWidthDeclsMap = createContainingBlockWidthDecls(isVerticalWritingMode); } function transformContainingBlockWidthDecls() { if (blackListedSelector) return; containingBlockWidthDeclsMap.forEach((decl, prop) => { if (decl == null) return; const val = decl.value; const leftOrRight = prop === "left" || prop === "right" || rootContainingBlockList_LR.includes(prop); const { mobile } = convertPropValue(prop, val, { enabledMobile: true, matchPercentage: hadFixed, convertMobile: (number, unit, numberStr) => { if (remMode) { if (hadFixed) { if (leftOrRight) return convertRem_FIXED_LR(number, unit, _basicRemWidth, unitPrecision, numberStr, remRatio); return convertRem_FIXED(number, unit, _basicRemWidth, unitPrecision, mobileUnit, fontViewportUnit, prop, numberStr, remRatio); } return convertRem(number, unit, _basicRemWidth, unitPrecision, mobileUnit, fontViewportUnit, prop, remRatio); } else if (maxVwMode) { if (hadFixed) { if (leftOrRight) return convertMaxMobile_FIXED_LR(number, unit, maxDisplayWidth, _viewportWidth, unitPrecision, numberStr); return convertMaxMobile_FIXED(number, unit, maxDisplayWidth, _viewportWidth, unitPrecision, mobileUnit, fontViewportUnit, prop, numberStr, minDisplayWidth); } return convertMaxMobile(number, unit, maxDisplayWidth, _viewportWidth, unitPrecision, mobileUnit, fontViewportUnit, prop, numberStr, minDisplayWidth); } return convertMobile(prop, number, unit, _viewportWidth, unitPrecision, fontViewportUnit, mobileUnit); }, }); decl.book = true; decl.value = mobile; }); containingBlockWidthDeclsMap = new Map(); } function Declaration(decl) { const { prop, value: val } = decl; if (decl.book) return; // 被标记过不转换 if (blackListedSelector) return; // 属性在黑名单选择器中,不进行转换 if (!satisfyPropList(prop)) return ; if (isMatchedSelectorProperty(propertyBlackList, selector, prop)) return; // 属性是否在黑名单中 if (isMatchedStr(valueBlackList, val)) return; // 属性值是否在黑名单中 if (prop === "position" && val === "fixed" && !ignoreToCorrectFixed) return hadFixed = true; if (hasIgnoreComments(decl, result, IN_CMT, IL_CMT)) return; // 如果有标注不转换注释,不进行转换 if (hasApplyWithoutConvertComment(decl, result, AWC_CMT)) return ; // 是否忽略百分比转换?如果不忽略,则需要收集哪些属性会涉及到根包含块 if (!ignoreToCorrectFixed) { // 该属性是用于设置根包含块的变量属性 const isRootContainingBlockProp = rootContainingBlockList_LR.includes(prop) || rootContainingBlockList_NOT_LR.includes(prop); isRootContainingBlockProp && (hadFixed = true); // 受 fixed 布局影响的,需要在 ruleExit 中计算的属性 if (containingBlockWidthDeclsMap.has(prop) || isRootContainingBlockProp) { const important = decl.important; const mapDecl = containingBlockWidthDeclsMap.get(prop); if (mapDecl == null || important || !mapDecl.important) containingBlockWidthDeclsMap.set(prop, decl); return; } } if (preflightReg.test(val)) { const { mobile } = convertPropValue(prop, val, { enabledMobile: true, matchPercentage: false, convertMobile(number, unit, numberStr) { if (remMode) return convertRem(number, unit, _basicRemWidth, unitPrecision, mobileUnit, fontViewportUnit, prop, remRatio); else if (maxVwMode) return convertMaxMobile(number, unit, maxDisplayWidth, _viewportWidth, unitPrecision, mobileUnit, fontViewportUnit, prop, numberStr, minDisplayWidth); else return convertMobile(prop, number, unit, _viewportWidth, unitPrecision, fontViewportUnit, mobileUnit); }, }); decl.book = true; decl.value = mobile; } } }, }; }; module.exports.postcss = true; /** * 用于替换 webpack css-loader 配置中的 getLocalIdent。需要引入 css-loader * 导出的 defaultGetLocalIdent 函数。 * * 开启 extract 选项后,桌面端和横屏的媒体查询会被分割为单独的文件,文件的路 * 径和源文件相异,这会导致生成的样式选择器 hash 值不同,这个函数用来修改新生成 * 文件的路径字符串,用于在桌面端和横屏的媒体查询文件里的选择器 hash 保持和源文 * 件一致。 * * 举例: * ```javascript * const { defaultGetLocalIdent } = require("css-loader"); * const { remakeExtractedGetLocalIdent } = require("postcss-mobile-forever"); * * module.exports = { * module: { * rules: [{ * test: /\.css$/, * use: [{ * loader: "css-loader", * options: { * modules: { * localIdentName: "[path][name]__[local]", * getLocalIdent: remakeExtractedGetLocalIdent({ defaultGetLocalIdent }), // <----- 这里 * }, * }, * }, "postcss-loader"], * }], * }, * } * ``` **/ module.exports.remakeExtractedGetLocalIdent = function({ defaultGetLocalIdent, getLocalIdent }) { return (context, localIdentName, localName, options) => { const { resourcePath, } = context; const aStr = __dirname.replace(process.cwd(), ''); // '/node_modules/postcss-mobile-forever' const bStr = resourcePath.replace(aStr + '/.temp', ''); // remove '/node_modules/postcss-mobile-forever/.temp' const cStr = bStr.replace(/(?<=[\\/])(?:landscape|desktop|mobile|shared)\.([^\\/]*)$/, (_, file) => file); // remove 'landscape\.|desktop\.|mobile\.|shared.' const newContext = { ...context, resourcePath: cStr, // remade resource path }; if (getLocalIdent) { return getLocalIdent(newContext, localIdentName, localName, options); } else { const localIdent = defaultGetLocalIdent(newContext, localIdentName, localName, options); return localIdent.replace(/\[local\]/gi, localName); } } };