常见的自适应处理
目前常见的方案大概有 3 种
- vw / vh
- rem + vw/vh
- scale(transform)该项目选择这个
vw/vh核心原理
- 视口单位定义:
-
vw
(Viewport Width) :1vw = 视口宽度的 1%(如视口宽度 1920px,则 1vw = 19.2px)。vh
(Viewport Height) :1vh = 视口高度的 1%(如视口高度 1080px,则 1vh = 10.8px)。- 优势 :直接与屏幕尺寸绑定,无需中间层计算(如
rem
需要动态设置根字体大小)。
- 布局逻辑:
-
- 元素尺寸 :用
vw
定义宽度、vh
定义高度,元素会随视口大小自动缩放。 - 字体和间距 :同样使用
vw
/vh
,确保文字和间距的自适应。 - 比例控制 :结合
calc()
或aspect-ratio
保持元素宽高比。
- 元素尺寸 :用
- 注意事项:
-
- 极端屏幕比例 :超宽或超高屏幕可能导致元素拉伸,需设置
min-width
/max-width
约束。 - 字体可读性 :过小的视口可能导致文字过小,可用
clamp()
函数设置动态范围。
- 极端屏幕比例 :超宽或超高屏幕可能导致元素拉伸,需设置
代码示例:
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
.dashboard {
width: 100vw;
height: 100vh;
padding: 2vh 2vw; /* 内边距自适应 */
background: #1a1a1a;
display: flex;
flex-direction: column;
}
.header {
font-size: clamp(20px, 2.5vw, 40px); /* 动态字体大小 */
color: white;
margin-bottom: 3vh;
}
.content {
flex: 1;
display: flex;
gap: 1.5vw; /* 间距自适应 */
}
.chart {
width: 70vw;
height: 80vh;
background: #2a2a2a;
aspect-ratio: 16/9; /* 保持 16:9 宽高比 */
}
.info-panel {
width: 25vw;
height: 80vh;
background: #2a2a2a;
overflow: auto;
}
</style>
</head>
<body>
<div class="dashboard">
<div class="header">数据看板</div>
<div class="content">
<div class="chart"></div>
<div class="info-panel"></div>
</div>
</div>
</body>
</html>
缺点其实一目了然,纯CSS处理,每个位置都需要自己去处理,页面布局稍微一复杂,肯定会比较的麻烦。
rem + vw/vh核心原理
纯CSS处理
rem
+ vw
/vh
是一种混合布局方案,结合了 rem
的灵活性和 vw
/vh
的视口响应能力。其核心原理是 动态设置根字体大小( 1rem
的值)为视口宽度的百分比 ,从而让所有基于 rem
的布局自动适配屏幕尺寸
- 动态根字体大小
- 将
html
的font-size
设置为1vw
(或更复杂的视口比例),使1rem = 1%视口宽度
。
css
html {
font-size: 0.625vw; /* 设计稿 1920px 时,1rem = 0.625% * 1920 ≈ 12px */
}
- 优势:
-
- 所有使用
rem
的尺寸会随视口宽度自动缩放。 - 避免纯
vw
单位在复杂布局中计算繁琐的问题(如font-size: 0.8vw
可改为font-size: 1.28rem
)。
- 所有使用
- 视口单位补充
- 对需要严格依赖视口尺寸的元素(如全屏容器、保持宽高比的元素),直接使用
vw
/vh
。 - 对字体、间距、内边距等使用
rem
,保证整体布局比例协调。
- 极端值约束
- 通过
clamp()
或媒体查询限制根字体大小的最小值和最大值,避免超小/超大屏的极端情况。
代码示例
设计稿尺寸为 1920x1080
:
- 根字体基准值:
12px
(即1rem = 12px
)。 - 容器宽度占视口 90%,高度占视口 80%。
- 字体和间距按
rem
缩放。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
/* 核心:通过视口宽度动态设置 rem */
html {
/* 1920px 设计稿下,1rem = 12px → 12px / 1920px = 0.625vw */
font-size: 0.625vw;
/* 设置最小/最大字体(可选) */
font-size: clamp(12px, 0.625vw, 24px);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.dashboard {
width: 90vw; /* 直接依赖视口宽度 */
height: 80vh; /* 直接依赖视口高度 */
margin: 5vh auto; /* 上下边距为视口高度的 5% */
background: #f0f0f0;
padding: 2rem; /* 内边距按 rem 缩放 */
}
.chart-container {
width: 100%;
height: 60vh; /* 直接依赖视口高度 */
background: #2a2a2a;
margin-bottom: 1.5rem; /* 按 rem 缩放 */
}
.info-box {
width: 100%;
padding: 1rem; /* 按 rem 缩放 */
background: #fff;
border-radius: 0.5rem; /* 按 rem 缩放 */
}
.info-text {
font-size: 1.2rem; /* 按 rem 缩放(设计稿 14.4px) */
color: #333;
}
</style>
</head>
<body>
<div class="dashboard">
<div class="chart-container"></div>
<div class="info-box">
<p class="info-text">当前数据:2023</p>
</div>
</div>
</body>
</html>
关键逻辑
- 根字体计算公式
ini
// 设计稿基准值:1rem = 12px,设计稿宽度 1920px
// 换算公式:fontSize = (基准值 / 设计稿宽度) * 100vw
fontSize = (12 / 1920) * 100vw = 0.625vw
- 单位选择规则
- 使用
rem
:字体、内边距、外边距、边框圆角等需要按比例缩放的属性。 - 使用
vw
/vh
:容器尺寸、定位偏移(如top: 10vh
)、需要严格保持视口比例的元素。
- 极端值处理
css
/* 最小根字体 12px,最大 24px */
html {
font-size: clamp(12px, 0.625vw, 24px);
}
- 超小屏 (如 320px 手机):
0.625vw = 2px
→ 强制限制为12px
。 - 超大屏 (如 3840px):
0.625vw = 24px
→ 不再继续放大。
动态rem + JS + Sass/Less
不过,现在只是很简单的页面,实际使用1rem=12px这种换算的话,在对应设计稿的时候,很难换算,而且很容易出现小数,比如:
设计稿px值 / 基准值(12px) = rem值
---> 设计稿100px ---> 100/12 ≈ 8.333rem
所以实际中,一般都是动态rem + JS + Sass/Less方案
核心逻辑:
- 基准值设定 :定义
1rem = 设计稿宽度的1%
(1920px → 19.2px)
-
- 但为方便计算,放大100倍 --->
1rem = 100px
- 但为方便计算,放大100倍 --->
动态计算:通过JS实时计算缩放比例
- scale = Math.min(当前宽/设计宽, 当前高/设计高)
- 尺寸转换:设计稿元素尺寸直接除以100得到rem值
-
- 示例:设计稿500px →
5rem
- 示例:设计稿500px →
ini
// 当屏幕宽度=设计稿宽度时
scale = 1 → fontSize = 100px
// 当屏幕宽度=960px时
scale = 960/1920 = 0.5 → fontSize = 50px
代码示例:
html
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>专业数据大屏</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<div class="dashboard">
<!-- 头部 -->
<header class="dashboard-header">
<h1 class="title">智慧运营中心</h1>
<div class="time-info">2025年03月 | 实时更新</div>
</header>
<!-- 主内容 -->
<main class="dashboard-content">
<section class="chart-container left">
<div class="chart-card">
<div class="chart-title">业务增长趋势</div>
<div class="chart-area"></div>
</div>
</section>
<section class="chart-container right">
<div class="chart-card">
<div class="chart-title">地域分布热力</div>
<div class="chart-area"></div>
</div>
</section>
</main>
</div>
<script>
// 动态适配逻辑(保持原有)
const DESIGN_WIDTH = 1920,
DESIGN_HEIGHT = 1080;
function adjustRootFont() {
const scale = Math.min(
window.innerWidth / DESIGN_WIDTH,
window.innerHeight / DESIGN_HEIGHT
);
document.documentElement.style.fontSize = `${scale * 100}px`;
}
// 初始化监听
window.addEventListener("resize", () => {
requestAnimationFrame(() => {
adjustRootFont();
});
});
adjustRootFont();
</script>
</body>
</html>
sass
css
@use "sass:math";
// 设计系统参数
$design-width: 1920;
$design-height: 1080;
$base-rem: 100; // 1rem = 100px
$color-primary: #2a85ff;
$color-bg: #1a1d28;
$color-card: #252a38;
// 单位转换
@function px2rem($px) {
@return math.div($px, $base-rem) * 1rem;
}
// 混合器:卡片效果
@mixin neumorphism-card {
background: $color-card;
border-radius: px2rem(12);
box-shadow:
0 px2rem(8) px2rem(24) rgba(0,0,0,0.15),
0 0 px2rem(2) rgba(255,255,255,0.1);
}
/* 基础重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
html {
font-size: 100px; // JS动态覆盖
}
body {
background: $color-bg;
color: #fff;
overflow: hidden;
}
/* 主容器 */
.dashboard {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
padding: px2rem(20);
}
/* 头部区域 */
.dashboard-header {
height: px2rem(80);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 px2rem(40);
margin-bottom: px2rem(20);
.title {
font-size: px2rem(36);
font-weight: bold;
background: linear-gradient(135deg, #6b8dd6, $color-primary);
background-clip: text;
}
.time-info {
font-size: px2rem(24);
color: rgba(255,255,255,0.8);
}
}
/* 主内容区 */
.dashboard-content {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: px2rem(20);
overflow: hidden;
.chart-container {
height: 100%;
@include neumorphism-card;
padding: px2rem(20);
.chart-card {
height: 100%;
display: flex;
flex-direction: column;
.chart-title {
font-size: px2rem(24);
margin-bottom: px2rem(16);
padding-left: px2rem(10);
border-left: px2rem(4) solid $color-primary;
}
.chart-area {
flex: 1;
background: rgba(255,255,255,0.05);
border-radius: px2rem(8);
}
}
}
}
/* 响应式适配 */
@media (max-aspect-ratio: 16/9) {
.dashboard-content {
grid-template-columns: 1fr;
// 增加以下优化代码
gap: px2rem(10); // 缩小间距
padding-bottom: px2rem(20); // 底部留白
.chart-container {
height: auto; // 取消固定高度
min-height: px2rem(400); // 设置最小高度
flex: 1; // 启用弹性填充
}
}
// 添加超小屏幕适配
@media (max-height: 600px) {
.dashboard-header {
height: px2rem(60);
padding: 0 px2rem(20);
.title {
font-size: px2rem(24);
}
.time-info {
font-size: px2rem(18);
}
}
.chart-container {
min-height: px2rem(300) !important;
.chart-area {
height: px2rem(200) !important;
}
}
}
}
当然sass需要经过编译,引入相关包进行处理即可
csharp
pnpm init
pnpm add sass
scale核心原理
其实就是使用 transform: scale()
通过数学计算保持内容宽高比例不变,同时通过缩放使内容始终充满目标容器
假设设计稿尺寸为 1920x1080
,目标屏幕尺寸为 1440x900
,缩放过程如下:
- 计算宽高比例
-
- 宽度比例:
1440/1920 = 0.75
- 高度比例:
900/1080 ≈ 0.833
- 宽度比例:
- 取最小比例 为了保证内容完整显示且不溢出,取
0.75
作为缩放比例(以宽度为基准) - 应用缩放 将内容缩小到
75%
,此时实际渲染尺寸为:
-
- 宽度:
1920 * 0.75 = 1440
(完美匹配目标宽度) - 高度:
1080 * 0.75 = 810
(小于目标高度 900,上下留黑边)
- 宽度:
- 居中定位 通过
transform
或者其他属性进行调整位置,使内容垂直/水平居中
代码示例:
xml
<!DOCTYPE html>
<html>
<head>
<style>
#design-container {
width: 1920px;
height: 1080px;
position: fixed;
transform-origin: 0 0; /* 依然保持左上角基准点 */
background: #f0f0f0;
}
.dashboard-item {
width: 500px;
height: 300px;
background: #3498db;
position: absolute;
left: 200px;
top: 150px;
}
</style>
</head>
<body>
<div id="design-container">
<div class="dashboard-item"></div>
</div>
<script>
function adjustScale() {
const designWidth = 1920;
const designHeight = 1080;
const container = document.getElementById("design-container");
// 获取当前视口尺寸
const currentWidth = window.innerWidth;
const currentHeight = window.innerHeight;
// 计算缩放比例(取最小值保证内容完整)
const scale = Math.min(
currentWidth / designWidth,
currentHeight / designHeight
);
// 计算垂直居中补偿量
const offsetY = (currentHeight - designHeight * scale) / (2 * scale);
// 计算水平居中补偿量
const offsetX = (currentWidth - designWidth * scale) / (2 * scale);
// 计算居中
// container.style.left = (currentWidth - designWidth * scale) / 2 + "px";
// container.style.top = (currentHeight - designHeight * scale) / 2 + "px";
// 应用变换:先缩放 -> 后平移
container.style.transform = `
scale(${scale})
translate(${offsetX}px, ${offsetY}px)
`;
}
// 初始化与响应式
adjustScale();
window.addEventListener("resize", adjustScale);
</script>
</body>
</html>
关键点解析
transform-origin: 0 0
确保缩放以左上角为基准点,便于后续定位计算
比例计算逻辑
- scale = Math.min(当前宽度/设计宽度, 当前高度/设计高度)
这种算法保证内容始终完整显示,可能出现黑边但不会溢出
- 垂直/水平补偿量公式
ini
offsetY = (当前视口高度 - 设计高度×缩放比例) / (2 × 缩放比例)
offsetX = (当前视口宽度 - 设计宽度×缩放比例) / (2 × 缩放比例)
-
- 为什么除以缩放比例 ?因为
translate
的移动距离是基于缩放后的坐标系。例如:
- 为什么除以缩放比例 ?因为
-
-
- 如果缩放比例为 0.5,
translateY(100px)
实际上会移动 50px 的物理像素 - 需要反向补偿:
目标物理像素移动量 / scale
- 如果缩放比例为 0.5,
-
当然,简单点,其实也可以直接使用margin或者left、top等属性
使用scale方案是现在一些中小项目比较常用的方案,主要是代码简单方便,封装之后,基本不会涉及到后续一些处理,但是也有非常突出的一些问题:
- 当大屏跟 ui 稿的比例不一样时,会出现周边留白情况
- 缩放后可能文本模糊
- 地图上的点位会出现偏移/点击位置不准
- 在使用第三方组件时,比如下拉框等不会缩放
方案对比表
维度 | Scale 方案 | vw/vh 方案 | rem + vw/vh 方案 |
---|---|---|---|
实现原理 | 整体坐标系缩放 | 完全依赖视口百分比 | 动态基准单位 + 视口辅助 |
代码复杂度 | ★☆☆ (极简) | ★★☆ (中等) | ★★★ (较高) |
布局精细度 | 全局粗放控制 | 中等粒度 | 高精度控制 |
字体清晰度 | 缩放后可能模糊 | 原生清晰 | 原生清晰 |
滚动支持 | 需特殊处理(坐标系破坏) | 天然支持 | 天然支持 |
交互事件精度 | 需反向计算(点击位置偏移) | 精准 | 精准 |
浏览器兼容性 | IE9+ | IE9+ (部分单位需polyfill) | IE9+ (需rem polyfill) |
性能表现 | 高(GPU加速) | 中(重绘影响) | 中(单位计算开销) |
维护成本 | 低(单点控制) | 中(分散调整) | 高(需预处理器配合) |