HarmonyOS Web 响应式布局实战:从相对单位到媒体查询完整方案
本文介绍了Web侧实现多设备适配的几种核心方法:相对单位(%、em、rem、vw/vh)实现响应式布局;媒体查询根据不同断点应用不同样式;窗口事件监听实现动态计算;以及宫格布局(CSS Grid)实现灵活排列。这些技术与HarmonyOS原生适配思路类似,但具有Web特有的灵活性和动态计算能力,适用于混合开发、H5页面等跨平台场景,帮助开发者实现"一次开发,多端部署"的目标。
一、今天要干啥
今天聊个实战话题:Web 侧怎么做多设备适配。
之前咱们一直在搞 HarmonyOS 原生应用的多设备适配,用断点、媒体查询、响应式组件那一套。但有些场景得用 Web,比如混合开发、H5 页面、跨平台应用,这时候咋整?
其实 Web 侧的多设备适配跟 HarmonyOS 原生侧思路差不多,核心都是断点划分 + 响应式布局。但 Web 有自己的特色:相对单位更多、媒体查询更灵活、还能用 JavaScript 动态计算。
这篇文章咱们就把 Web 侧的多设备适配能力捋一遍,从相对单位到媒体查询,从宫格布局到自定义弹窗,把实战中用到的方案都整理出来。
二、开干(边做边讲)
2.1 相对单位:响应式开发的基础
Web 开发里控制元素尺寸,得用 CSS 的单位。单位分两类:绝对单位和相对单位。
绝对单位就是像素(px),这玩意儿是个固定值,不管屏幕多大、父元素多大,它都不变。适合那些尺寸固定的元素,比如图标、固定宽度的按钮。
相对单位就不一样了,它会根据其他元素或窗口尺寸动态变化。这就是响应式开发的核心。
常用的相对单位有四种:百分比(%)、em、rem、vw/vh。
百分比这玩意儿最常用,相对于父元素的尺寸。比如父元素宽度 400px,子元素设置 50%,那就是 200px。这东西在响应式设计里用得特别多,让元素大小跟着父元素调整。
.parent {
width: 400px;
}
.child {
width: 50%; /* 200px */
}
em 相对于当前元素的字体大小。如果当前元素字体大小没设置,就继承父元素的。比如父元素字体 16px,子元素设置 1.5em,那就是 24px。这玩意儿适合做文本相关的尺寸控制,调整字体大小就能改布局。
p {
font-size: 16px;
}
span {
font-size: 1.5em; /* 24px */
}
rem 相对于根元素(html)的字体大小。跟 em 类似,但更稳定,因为所有 rem 都基于同一个根元素。全局调整只要改 html 的字体大小就行。
html {
font-size: 16px;
}
p {
font-size: 1rem; /* 16px */
}
span {
font-size: 1.5rem; /* 24px */
}
vw/vh 这玩意儿最厉害,直接相对于视窗(浏览器窗口)。vw 是窗口宽度,vh 是窗口高度。比如窗口宽度 1920px,设置 100vw 就是 1920px。适合那些需要撑满窗口的场景,比如弹窗遮罩层。
.overlay {
width: 100vw; /* 等于视窗宽度 */
height: 100vh; /* 等于视窗高度 */
}
有个事儿得说一下:CSS 里的 px 单位会自动通过设备像素比换算,这让 px 在视觉效果上跟 HarmonyOS 的 vp 单位一样。这个特性消除了设备物理像素的差异,Web 应用迁移到 HarmonyOS 就更方便了。
2.2 媒体查询:断点适配的核心
媒体查询这玩意儿允许你根据设备特性(屏幕尺寸、分辨率、方向等)应用不同的样式规则。这就是 Web 侧断点适配的核心。
在 Web 页面适配 HarmonyOS 侧"一次开发,多端部署"时,横纵向断点对应的尺寸范围要跟 HarmonyOS 侧推荐的断点划分范围保持一致。不过有个细节得注意:Web 侧区分纵向断点用宽高比,HarmonyOS 侧用高宽比。别搞混了。
来看个例子:
@media (840px<=width) {
.article {
font-size: 20px;
}
}
@media (320px<=width<600px) and (min-aspect-ratio: 1/1.2) and (max-aspect-ratio: 1/0.8) {
.article {
font-size: 14px;
}
}
第一段代码:视口宽度不小于 840px 时,article 元素字体大小变成 20px。
第二段代码:视口宽度在 320px 到 600px 之间,宽高比在 1/1.2 到 1/0.8 之间,符合手机上下分屏的小窗口,字体大小变成 14px。
媒体查询能干的事儿不少,咱们来看两个常见场景。
场景一:修改字体大小
这个场景用媒体查询设置不同断点下的字体大小,实现响应式布局。以常见的 sm、md、lg 为例:
.title {
font-size: 14px;
}
@media (320px<=width<600px) {
.title {
font-size: 16px;
}
}
@media (600px<=width<840px) {
.title {
font-size: 18px;
}
}
@media (840px<=width) {
.title {
font-size: 20px;
}
}
这段代码的意思:
- 默认字体大小 14px
- sm 断点(320-600px)时,字体变成 16px
- md 断点(600-840px)时,字体变成 18px
- lg 断点(840px 以上)时,字体变成 20px
这样就能在不同屏幕尺寸上有不同的字体大小,阅读体验更好。
场景二:修改图片宽度
这个场景用媒体查询设置不同断点下的图片宽度,实现响应式布局:
.cover {
width: 100px;
height: 100px;
}
@media (320px<=width<600px) {
.cover {
width: 120px;
height: 120px;
}
}
@media (600px<=width<840px) {
.cover {
width: 160px;
height: 160px;
}
}
@media (840px<=width) {
.cover {
width: 240px;
height: 240px;
}
}
这段代码的意思:
- 默认图片尺寸 100px
- sm 断点时,图片变成 120px
- md 断点时,图片变成 160px
- lg 断点时,图片变成 240px
这样就能在不同屏幕尺寸上有不同的元素尺寸,显示效果更合适。
2.3 窗口事件:动态计算的补充
有些场景媒体查询和相对单位都不够用,得用 JavaScript 动态计算。
window 对象提供了 resize 事件,窗口大小变化时触发。可以用 window.innerWidth 获取窗口宽度,window.innerHeight 获取窗口高度,然后动态调整布局。
来看个例子,等比例修改字体大小:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Font Size on Resize</title>
<style>
html {
font-size: 16px;
}
.content {
padding: 20px;
}
.content h1,
.content p {
margin: 0 0 1em;
}
</style>
</head>
<body>
<div class="content">
<h1>Responsive Font Size Example</h1>
<p>Resize the window to see the font size change.</p>
</div>
</body>
</html>
<script>
const root = document.documentElement;
const initialScale = window.innerWidth / 1920;
root.style.fontSize = `${initialScale * 16}px`;
// Listen for window size change events
window.addEventListener('resize', () => {
const newScale = window.innerWidth / 1920;
root.style.fontSize = `${newScale * 16}px`;
});
</script>
这段代码的意思:
- 初始时,根据窗口宽度计算一个缩放比例(窗口宽度 / 1920)
- 根据缩放比例设置根元素的字体大小(缩放比例 * 16px)
- 窗口大小变化时,重新计算缩放比例并更新字体大小
这样就能实现字体大小随窗口宽度等比例变化,适配效果更灵活。
2.4 宫格布局:网格排列的利器
CSS 提供了 grid 布局,跟 HarmonyOS 的栅格布局类似,把网页内容划分成网格,通过组合不同网格做出各种布局。

宫格布局有几个关键概念得搞清楚:
容器和项目:采用网格布局的区域叫容器,容器内部采用网格定位的子元素叫项目。
行与列:水平区域叫行,垂直区域叫列。
行间距与列间距:两行或两列之间的空白区域。
使用宫格布局的步骤:
第一步:设置容器属性,把容器的 display 属性设置为 grid。
第二步:确定元素的排列方式,包括列宽、行高和间距。
来看个例子,两行三列,列宽和行高都是 100px,行列间距 20px:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo</title>
</head>
<style>
.container {
display: grid;
gap: 20px;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px;
}
.container .grid-item {
background-color: #f6fdf5;
text-align: center;
line-height: 100px;
}
</style>
<body>
<div class="container">
<div class="grid-item">1</div>
<div class="grid-item">2</div>
<div class="grid-item">3</div>
<div class="grid-item">4</div>
<div class="grid-item">5</div>
<div class="grid-item">6</div>
</div>
</body>
</html>
元素个数少的时候,可以逐个书写列宽,比如 grid-template-columns: 100px 100px 100px。但元素个数多的时候,比如十列,这样写可读性就差了。
这时候可以用 repeat() 函数简化书写。这个函数接收两个参数:第一个是重复次数,第二个是要重复的值。
比如上面的代码可以改成:
grid-template-columns: repeat(3, 100px);
效果一样,但代码更简洁。
宫格布局可以结合媒体查询实现不同设备上的最佳体验。通过设置不同断点的排列方式,达到在不同屏幕尺寸上显示不同效果的目的。
来看个例子:
sm 断点下,宫格以 4 列显示,行间距 12px:

@media (320px<=width<600px) {
.grid-functions {
grid-template-columns: repeat(4, 48px);
row-gap: 12px;
}
}
md 断点下,宫格以 6 列显示,行间距 20px:

@media (600px<=width<840px) {
.grid-functions {
grid-template-columns: repeat(6, 48px);
row-gap: 20px;
}
}
lg 断点下,宫格以 8 列显示,行间距 24px:

@media (840px<=width) {
.grid-functions {
grid-template-columns: repeat(8, 48px);
row-gap: 24px;
}
}
这样就能在不同断点下自动调整宫格的列数和间距,适配效果很好。
2.5 自定义弹窗:尺寸适配的细节
大尺寸设备上,弹窗得更大,不然内容太小看不清。通过媒体查询设置不同断点下的弹窗尺寸。
来看个例子:
sm 断点下,弹窗尺寸 328px * 344px:

@media (320px<=width<600px) {
.custom-dialog {
width: 328px;
height: 344px;
}
}
md 断点下,弹窗尺寸 360px * 378px:
@media (600px<=width<800px) {
.custom-dialog {
width: 360px;
height: 378px;
}
}
lg 断点下,弹窗尺寸 393px * 412px:

@media (800px<=width) {
.custom-dialog {
width: 393px;
height: 412px;
}
}
有个细节得注意:不仅弹窗尺寸要响应式适配,弹窗内容也得适配。弹窗内容高度定制,没法提供统一的适配方式,得根据内容自己想办法。
2.6 轮播布局:动态展示的技巧
轮播布局就是轮播图,多张图片轮流播放。原生 Web 没提供直接实现轮播图的组件,得用技巧或第三方组件库。
轮播布局的适配关键点有三个:
控制轮播元素的尺寸:用媒体查询和断点,或窗口事件,设置每个断点下的轮播图尺寸样式。
控制轮播元素的间距:根据排列方式选择方法。用 flex 布局时,推荐用 gap 属性定义间距;其他情况用 margin 属性。
控制每次轮播的位移距离:根据实现方案选择。用 translateX() 时,控制每次增加的步长;用绝对定位时,根据对应的位移属性控制。
来看个 React 实现的轮播图:
const Banner = () => {
const banner = [
{ id: "001", url: "assets/banner01.png" },
{ id: "002", url: "assets/banner02.png" },
{ id: "003", url: "assets/banner03.png" },
{ id: "004", url: "assets/banner04.png" },
];
const [currentIndex, setCurrentIndex] = useState(1);
const [currentDot, setCurrentDot] = useState(0);
const [width, setWidth] = useState<number>(0);
const [singleOffset, setSingleOffset] = useState<number>(0);
const [initOffset, setInitOffset] = useState<number>(0);
const [gap, setGap] = useState(16);
const [animate, setAnimate] = useState("transform 0.5s ease");
const [dotVisible, setDotVisible] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const totalItems = banner.length;
useEffect(() => {
const updateLayout = () => {
const winWidth = window.innerWidth;
if (winWidth < 600) {
setGap(0); // sm 断点下元素间距
setWidth(winWidth - 32); // sm 断点下元素宽度
setSingleOffset(winWidth - 32); // sm 断点下单次位移
setInitOffset(0); // sm 断点下初始偏移
setDotVisible(true);
} else if (winWidth < 840) {
setGap(12); // md 断点下元素间距
setWidth((winWidth - 48 - gap) / 2); // md 断点下元素宽度
setSingleOffset(width + gap); // md 断点下单次位移
setInitOffset(24); // md 断点下初始偏移
setDotVisible(false);
} else {
setGap(16); // lg 断点下元素间距
setWidth((winWidth - 250 - gap) / 2); // lg 断点下元素宽度
setSingleOffset(width + gap); // lg 断点下单次位移
setInitOffset(125); // lg 断点下初始偏移
setDotVisible(false);
}
};
updateLayout();
window.addEventListener("resize", updateLayout);
return () => window.removeEventListener("resize", updateLayout);
}, [gap, width]);
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prev) => prev + 1);
setCurrentDot((p) => (p + 1) % banner.length);
}, 3000);
return () => clearInterval(interval);
});
useEffect(() => {
if (currentIndex === totalItems + 1) {
setTimeout(() => {
setAnimate("none");
setCurrentIndex(1);
setTimeout(() => {
setAnimate("transform 0.5s ease");
}, 50);
}, 550);
}
}, [currentIndex, totalItems]);
return (
<div className="banner-container">
<div
className="banner-wrapper"
ref={wrapperRef}
style={{
transform: `translateX(-${currentIndex * singleOffset - initOffset}px)`,
transition: animate,
gap: `${gap}px`,
}}
>
{[banner[banner.length - 1], ...banner, ...banner].map(
(item, index) => (
<div
style={{
width,
}}
key={`${item.id}-${index}`}
className="banner-item"
>
<img src={item.url} alt={`banner-${item.id}`} />
</div>
)
)}
</div>
{dotVisible ? (
<div className="swiper-dot">
{banner.map((item, index) => (
<div
key={item.id}
className={`dot${currentDot === index ? " dot-active" : ""}`}
></div>
))}
</div>
) : (
<></>
)}
</div>
);
};
export default Banner;
这段代码的意思:
- 根据窗口宽度动态计算轮播图的宽度、间距、位移距离
- sm 断点下,元素间距 0px,元素宽度为窗口宽度减 32px,显示指示点

- md 断点下,元素间距 12px,元素宽度为窗口宽度减 48px减间距再除 2

- lg 断点下,元素间距 16px,元素宽度为窗口宽度减 250px减间距再除 2

- 每 3 秒自动轮播一次
- 使用 translateX 控制位移
这样就能在不同断点下实现不同的轮播效果,适配很灵活。
三、踩了哪些坑
3.1 相对单位选错了
一开始我总觉得百分比最好用,啥都用百分比。后来发现有些场景百分比不合适。
比如弹窗遮罩层,用百分比会有问题。父元素可能不是整个窗口,遮罩层就撑不满窗口。这时候得用 vw/vh,直接相对于视窗。
再比如文本相关的尺寸,用百分比也不好控制。调整父元素字体大小,子元素的文本大小不会跟着变。这时候得用 em 或 rem。
经验就是:得根据场景选择合适的相对单位,别一股脑全用百分比。
3.2 媒体查询断点搞混了
Web 侧和 HarmonyOS 侧的断点划分范围要保持一致,但有个细节容易搞混:纵向断点的宽高比定义不一样。
Web 侧区分纵向断点用宽高比,HarmonyOS 侧用高宽比。
比如手机上下分屏的小窗口,Web 侧判断条件是宽高比在 1/1.2 到 1/0.8 之间:
@media (320px<=width<600px) and (min-aspect-ratio: 1/1.2) and (max-aspect-ratio: 1/0.8) {
...
}
HarmonyOS 侧判断条件是高宽比在某个范围内,这就不一样了。
我一开始没注意这个细节,导致纵向断点的判断条件写错了,适配效果不对。后来仔细看了文档才搞明白。
3.3 弹窗内容没适配
我一开始只适配了弹窗的尺寸,没适配弹窗的内容。结果在大屏设备上,弹窗变大了,但内容还是那么小,看着特别别扭。
后来才知道,弹窗内容也得适配。弹窗内容高度定制,没法提供统一的适配方式,得根据内容自己想办法。
比如弹窗里的图片,得用媒体查询调整尺寸;弹窗里的文本,得用媒体查询调整字体大小;弹窗里的按钮,得用媒体查询调整间距。
这个细节不注意,用户体验就差了。
3.4 轮播图位移距离算错了
轮播图的位移距离得根据元素宽度和间距动态计算。我一开始直接用固定的位移距离,没考虑元素宽度变化。
结果在不同断点下,轮播图的位移距离不对,轮播效果就乱了。
后来改成根据元素宽度加间距动态计算位移距离,效果就对了:
setSingleOffset(width + gap);
这个细节得注意:位移距离必须跟元素宽度和间距匹配,不然轮播效果就错乱。
3.5 窗口 resize 事件没清理
用 JavaScript 监听窗口 resize 事件时,得在组件卸载时清理事件监听,不然会出现意想不到的问题。
我一开始没清理,导致页面切换后,resize 事件还在触发,控制台一堆错误。
后来加了清理逻辑:
window.addEventListener("resize", updateLayout);
return () => window.removeEventListener("resize", updateLayout);
问题就解决了。
四、最终成果
Web 侧的多设备适配方案整理出来了,核心能力有三种:
相对单位:百分比、em、rem、vw/vh,根据场景选择合适的单位。
媒体查询:断点划分 + 响应式布局,设置不同断点下的样式规则。
窗口事件:resize 事件 + 动态计算,用 JavaScript 动态调整布局。
这三种能力可以组合使用,适配效果更灵活。
布局实战方案有三个:
宫格布局:grid 布局 + 媒体查询,设置不同断点下的列数和间距。
自定义弹窗:媒体查询设置不同断点下的弹窗尺寸,内容也得适配。
轮播布局:窗口事件动态计算元素宽度、间距、位移距离,实现不同断点下的轮播效果。
这些方案都是实战中验证过的,可以直接用。
五、经验教训
Web 侧的多设备适配跟 HarmonyOS 原生侧思路差不多,但细节不一样。核心都是断点划分 + 响应式布局,但 Web 有自己的特色:相对单位更多、媒体查询更灵活、还能用 JavaScript 动态计算。
相对单位的选择得根据场景,别一股脑全用百分比。媒体查询的断点划分范围要跟 HarmonyOS 侧保持一致,但纵向断点的宽高比定义不一样,得注意。弹窗的内容也得适配,别只适配弹窗尺寸。轮播图的位移距离得根据元素宽度和间距动态计算,别用固定值。窗口 resize 事件得在组件卸载时清理,不然会出现意想不到的问题。
这些细节不注意,适配效果就差了。注意了,适配效果就好。
Web 侧的多设备适配能力挺强的,用好这些能力,就能实现"一次开发,多端部署"的效果。
更多推荐


所有评论(0)