Rem
一、认识
rem
(Root Em
):相对于根元素 (<html>
) 的字体大小。实现思路为: 基于 postcss
和 postcss-pxtorem
通过 Webpack
工程化的构建方式将 px
转化为 rem
后, 通过 动态调整根元素的字体大小 来动态改变页面布局、 实现调整页面字体大小的效果。 Rem
计算规则: 我们为了方面计算, 采用换算单位时固定除数, 动态计算 HTML.fontSize
来实现终端适配。以下是我们的计算逻辑: htmlFontSize = clientWidth / uiWidth * postcssRootValue * scale
。
-
htmlFontSize
: 最终的根字体大小, 它根据屏幕宽度与设计稿宽度的比例乘以我们固定的除数来计算。 -
clientWidth
: 屏幕宽度 -
uiWidth
:UI
稿宽度 -
postcssRootValue
: 换算单位时的除数, 比如我们为了方便计算,规定1rem = 100px
,当UI
稿某个元素为400px
时, 那就在代码中换算成4rem
即可。 -
scale
: 当前用户调整的字体大小,设有default: 1, medium: 1.2, maximum: 1.4
举例: 假如我们的 UI
稿为 750px
, 我们在 iPhone 14 Pro Max 430*932
的终端开发, 为了很方便的适配各种终端以及不同终端尺寸与 UI
稿无缝衔接, 我们需要将 750px
的 UI
稿撑满 430px
宽的屏幕。因此需要知道 屏幕宽度与设计稿宽度 的比例。然后我们换算 px -> rem
时, 固定使用 100
, 所以他们的比例乘 100
就是最终的 font-size
。随后,我们只需要将最后的 font-size
再乘以用户选择的字号倍数即可。
二、实现
注意: 以 UI
稿为 750px
为例进行的 Demo
。如果 UI
稿有变更, 要改变 uiWidth
值。
2.1 App.tsx
import "uno.css";
import "./App.css";
import "./App.scss";
import flexible from "./flexible";
import { useEffect } from "react";
import city1 from "./images/city1.png";
import AppStyle from "./App.module.scss";
function App() {
useEffect(()=>{
flexible(window, document);
}, []);
return (
<div className="app">
App 页面
<div className={AppStyle.div1}></div>
<img className="w-750" src={city1} />
<div className="m-1 color-SubColor font-size-32">嘻嘻哈哈</div>
<div className="p-2 color-Text2 bg-BrandDark font-size-32">哈哈嘻嘻</div>
<div className="box"></div>
<div className="text-ellipsis-2 w-100 font-size-32">
敷设电缆;范德萨范德萨;林凤娇了;附件都说了;就发了;三等奖发了发;啦束带结发;拉数据;
发;苏妲己;啊;发的酸;拉法基;了发的酸;浪费啊;减肥的;是佛i额文峰街道舒服了扩大升级
</div>
<span className="text-ellipsis-2-inline w-100 font-size-32">
敷设电缆;范德萨范德萨;林凤娇了;附件都说了;就发了;三等奖发了发;啦束带结发;拉数据;
发;苏妲己;啊;发的酸;拉法基;了发的酸;浪费啊;减肥的;是佛i额文峰街道舒服了扩大升级
</span>
</div>
);
}
export default App;
2.2 flexible.ts
function flexible(window: Window, document: Document, fontSizeScale = "default") {
const docEl = document.documentElement;
const uiWidth = 750;
const postcssRootValue = 100;
const fontSizeScaleMap: {[key:string]: number} = {
default: 1,
medium: 1.2,
maximum: 1.4
}
function setRemUnit() {
const clientWidth = docEl.clientWidth;
const scale = fontSizeScaleMap[fontSizeScale];
const rem = (clientWidth / uiWidth) * postcssRootValue * scale;
docEl.style.fontSize = rem + "px";
}
setRemUnit();
window.addEventListener("resize", setRemUnit);
window.addEventListener("pageshow", function (e) {
if (e.persisted) {
setRemUnit();
}
});
}
export default flexible;
2.3 uno.config.js
在 UnoCSS
中,需要我们自己换算单位。我们的方案是, 换算单位统一为 1rem = 100px
。所以, 换算逻辑如下:
import { Preset } from "unocss";
const uiWidth = 750;
const getSizeValue = (value: string): string => {
if (!value) {
return "";
}
if (value.includes("-")) {
const values = value.split("-");
return values.map(getSizeValue).join(" ");
}
if (isNaN(Number(value))) {
return value;
}
const remSize = parseFloat(value) / 100;
return remSize ? `${remSize}rem` : "0";
};
const boxSizeValue = [
"border-box",
"content-box",
"clip",
"visible",
"scroll",
].join("|");
export const preset1: Preset = {
name: "preset1",
rules: [
[
/^w-([.\d]+)$/,
([_, num]) => ({ width: getSizeValue(num) }),
{ autocomplete: "w-<num>" },
],
[
/^max-w-([.\d]+)$/,
([, value]) => ({ "max-width": getSizeValue(value) }),
{ autocomplete: "max-w-<num>" },
],
[
/^min-w-([.\d]+)$/,
([, value]) => ({ "min-width": getSizeValue(value) }),
{ autocomplete: "min-w-<num>" },
],
[
/^h-([.\d]+)$/,
([, value]) => ({ height: getSizeValue(value) }),
{ autocomplete: "h-<num>" },
],
[
/^max-h-([.\d]+)$/,
([, value]) => ({ "max-height": getSizeValue(value) }),
{ autocomplete: "max-h-<num>" },
],
[
/^min-h-([.\d]+)$/,
([, value]) => ({ "min-height": getSizeValue(value) }),
{ autocomplete: "min-h-<num>" },
],
[
new RegExp(`^(?:box)-(${boxSizeValue})$`),
([, v]) => (boxSizeValue.includes(v) ? { "box-sizing": v } : undefined),
{
autocomplete: [`(box)-(${boxSizeValue})`],
},
],
[
/^m-([\d]+)$/,
([, value]) => ({ margin: getSizeValue(value) }),
{ autocomplete: "m-<num>" },
],
[
/^m-t-([\d]+)$/,
([, value]) => ({ "margin-top": getSizeValue(value) }),
{ autocomplete: "m-t-<num>" },
],
[
/^m-r-([\d]+)$/,
([, value]) => ({ "margin-right": getSizeValue(value) }),
{ autocomplete: "m-r-<num>" },
],
[
/^m-b-([\d]+)$/,
([, value]) => ({ "margin-bottom": getSizeValue(value) }),
{ autocomplete: "m-b-<num>" },
],
[
/^m-l-([\d]+)$/,
([, value]) => ({ "margin-left": getSizeValue(value) }),
{ autocomplete: "m-l-<num>" },
],
[
/^p-([\d]+)$/,
([, value]) => ({ padding: getSizeValue(value) }),
{ autocomplete: "p-<num>" },
],
[
/^p-t-([\d]+)$/,
([, value]) => ({ "padding-top": getSizeValue(value) }),
{ autocomplete: "p-t-<num>" },
],
[
/^p-r-([\d]+)$/,
([, value]) => ({ "padding-right": getSizeValue(value) }),
{ autocomplete: "p-r-<num>" },
],
[
/^p-b-([\d]+)$/,
([, value]) => ({ "padding-bottom": getSizeValue(value) }),
{ autocomplete: "p-b-<num>" },
],
[
/^p-l-([\d]+)$/,
([, value]) => ({ "padding-left": getSizeValue(value) }),
{ autocomplete: "p-l-<num>" },
],
[
/^font-size-([\d]+)$/,
([, value]) => ({ "font-size": getSizeValue(value) }),
],
[/^font-bold$/, () => ({ "font-weight": "550" })],
[/^bold$/, () => ({ "font-weight": "550" })],
[/^weight-([\d]+)$/, ([, value]) => ({ "font-weight": value })],
[
/^line-height-([0-9].*)$/,
([, value]) => ({ "line-height": getSizeValue(value) }),
],
]
};
2.4 postcss.config.js
module.exports = {
plugins: [
"autoprefixer",
"postcss-preset-env",
[
"postcss-pxtorem",
{
rootValue: 100,
unitPrecision: 5,
propList: ["*"],
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
},
],
],
};
2.5 webpack.config.js
const combineCssLoader = (params = {}) => {
const { modules = false, postcss = true, devMode = true } = params;
const baseLoaders = [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: modules ? { namedExport: false } : false,
},
},
];
if (postcss) {
baseLoaders.push({
loader: "postcss-loader",
options: {
postcssOptions: {
config: Path.resolve(process.cwd(), "./postcss.config.js"),
},
},
});
}
return baseLoaders;
};