大屏应用在不同分辨率下的适配方案
前言
最近比较忙,好久没来写技术文章了,你说这年头忙点也是好事,说明公司还有钱赚,自然我们也能跟着喝汤,这两天才稍微缓过来一点,最近遇到一些在不同分辨率 下的 web 自适应问题,延伸下去今天想聊的就是,在不同分辨率下的 web 应用如何适配不同的屏幕尺寸,比较具有代表性的就是大屏应用,通常大屏的分辨率可能是 8K,甚至 16K。 我们如何保证应用在不同分辨率下的呈现效果一致就是一项挑战,今天跟大家讲下通常我会使用的几种方案以及他们的优劣。
方案一览
方案 | 原理 |
---|---|
transform scale 缩放方案 | 通过在顶层元素上设置 scale 缩放级别来实现整个内部的子元素缩放 |
rem 方案 | 不使用 px 而使用 rem 来当宽高单位,在不同分辨率计算修改根字体大小来达到缩放 |
viewport 方案 | 不使用 px 而使用 vh 和 vw 来当宽高单位,实现比例式的缩放 |
Scale 方案
第一种方式通过 scale 来缩放元素,先说说实现方式,我最近有接触到一个应用,采用这种方案。 实现起来也比较简单,通过给一个 root 元素设置一个开发用的设计分辨率,然后监听窗口大小变化,根据窗口实际的大小去和你设定的设计分辨率进行计算比例。
比如我设置 1920x1080 的窗口,但是实际分辨率是 2k 显示器时,此时真实的分辨率是 2560x1440,通过计算得出:
2560/1920=1.44 得到宽度的缩放比例
1440/1080=1.2 得到高度的缩放比例
此时去设置元素的 scaleX 和 scaleY 即可得到缩放
代码如下:
<template>
<div
id="screen"
:style="{
width: `${style.width}PX`,
height: `${style.height}PX`,
transform: `${style.transform}`,
}"
>
<div id="app">你的实际应用内容</div>
</div>
</template>
<script>
export default {
data() {
return {
timer: null,
style: {
width: '1920', // 设计设计稿宽度
height: '1080', // 设定设计稿高度
transform: 'scaleY(1) scaleX(1) translate(-50%, -50%)',
},
};
},
mounted() {
let that = this;
that.setScale();
// 窗口改变事件
window.onresize = () => {
this.setScale();
};
},
methods: {
// 获取宽高缩放比例
getScale() {
const w = window.innerWidth / this.style.width;
const h = window.innerHeight / this.style.height;
return { x: w, y: h };
},
setScale() {
// 处理页面的缩放
let scale = this.getScale();
this.style.transform =
'scaleY(' + scale.y + ') scaleX(' + scale.x + ') translate(-50%, -50%)';
// 处理一些ui框架挂载到body下,让这些挂载到body的元素也能缩放,这里用element来举例
const existingStyle = Array.from(document.querySelectorAll('style')).find(
(style) => style.textContent.includes('.handled-el-dialog-transform')
);
if (existingStyle) {
existingStyle.remove();
}
const style = document.createElement('style');
style.textContent = `
.handled-el-dialog-transform{}
.el-dialog {
transform: translate(-50%, -50%) ${
'scaleY(' + scale.y + ') scaleX(' + scale.x + ')'
} !important;
}
.el-popover {
transform: ${
'scaleY(' + scale.y + ') scaleX(' + scale.x + ')'
} !important;
}
`;
document.head.appendChild(style);
},
},
};
</script>
来首先看看在 1080p 的分辨率下的显示效果,记住弹窗和文字的大小
然后来看看再 2k 分辨率下的显示效果,和上面 1080p 是几乎一样的,说明这套方案是可行的
Scale 方案的原理及优劣
首先 Scale 方案的缩放,它是拉伸或者压缩元素的,实际上你看到的所有元素的大小还是原来的大小,比如你弹窗宽度是 1800px,即便你在 4k 分辨率显示和 1080p 显示效果一致, 但在 4k 分辨率时,这个弹窗的宽度数值仍然是 1800px,而是他是通过渲染缩放来等比放大的,所以它会存在一些小问题,比如你见到的和实际的元素位置有偏移, 放大过多时会出现文字模糊,以及可能出现一些不能预期的情况例如滚动条的计算异常, 拿弹窗来说,可能出现在放大后出现滚动条,实际上你肉眼看到的窗口并不需要,这个滚动条的出现就是因为缩放导致的。如果你在 1080p 分辨率下开发,实际要运行在 8k、甚至 16k 的大屏上, 我就不是很建议使用 scale 缩放的方案了。
Rem 方案
Rem 作为一种和 px 一样的大小单位,它也可以用于做自适应分辨率的方案,em 是一种基于当前元素(如果没指定则基于父元素)的 font-size 的一种大小单位, 当有这样一个 div 时,这个 div 的宽度 1em 等价于 20px,高度 2em 等价于 40px。
<div style="font-size:20px;width:1em;heigth:2em"></div>
后来为了让 em 更好用,不用思考在某个元素上 em 等于多少,推出了 root em 单位:rem,他是基于根元素(body 元素)的的 font-size 来确定 1rem 具体是多少 px 的。
<body style="font-size:30px">
<div style="font-size:20px;width:1rem;heigth:2rem"></div>
<div style="font-size:20px;width:1em;heigth:2em"></div>
</body>
在上面的例子中,1rem 等于 30px,而 2rem 等于 60px。
基于 rem 这个可以根据 body 的 font-size 变化的特性,我们也能很容易得通过它来实现页面的自适应。
<template>
<div id="screen" :style="width:1920px;height:1080px">
<div id="app">
<div style="width:800px;height:300px">1</div>
<div style="width:1800px;height:1300px">2</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {};
},
mounted() {
let that = this;
that.setScale();
// 窗口改变事件
window.onresize = () => {
this.setScale();
};
},
methods: {
// 获取宽高缩放比例
getScale() {
const w = window.innerWidth / this.style.width;
return w;
},
setScale() {
// 处理页面的缩放
let scale = this.getScale();
document.body.style.fontSize = `${scale * 16}px`; // 默认body的font-size是16px
},
},
};
</script>
这样的情况下,页面的会随着屏幕自适应,但是有个致命的问题,那就是这个方案只能定义 1rem 等于多少,假如我是标准的 1080p 窗口拉伸到 2k 是没问题的,但是如果我从 1920*1080 的窗口, 我宽高不是等比的放大就会出问题,比如我放到一个带鱼屏上,宽了很多,但是高没有发生变化。此时竖向的尺寸计算都会乱套了,会出现高度和预期不一致,出现竖向滚动条等。
因此,我认为 Rem 方案并不适合用来做大屏应用(如果你不需要纵向自适应,则可以选用此方案),或者做分辨率自适应的场景。但是据我所知,是有人用这种方案实现自适应的。
ViewPort 视口方案
最后,我们来看看 Viewport 视口方案,这个方案比较好用,主流浏览器都支持,可以单独使用 viewport 来实现不同分辨率的自适应,也可以结合 scale 缩放方案一起。我们 来看看这两种方案有什么不一样的。
1.Scale 结合 Viewport 方案
这个方案比较适合要快速实现适配,改动非常小,或者不方便引用 postcss 包来构建应用的情况,这套方案比前面手动计算 scale 的方式要方便很多,性能也会好一些(忽略不计)。
你只需要再 body 上加一个样式,设置好页面的宽高+缩放公式
body {
transform-origin: 0 0;
transform: scaleX(calc(100vw / 1920)) scaleY(calc(100vh/1080));
width: 1920px;
height: 1080px;
overflow: auto;
}
是不是非常的快速,你都不用改其他任何地方,当然因为使用了 scale,所以这套方案和上面的 Scale 方案的原理及优劣 说的问题一样,可能出现一些需要单独处理的样式问题。
2.viewport 方案
这个方案就是,你所有用 px 的地方都使用 vh 和 vw 来替代,当然如果这样的话,开发起来比较费事儿,老应用更是不可能去修改所有的 px 为 vh 和 vw 了,解决办法就是 需要借助一个 postcss 的插件来自动将系统中的所有写了 px 的样式,帮我们转换成 vh 和 vw。
首先安装 postcss 及它的插件
# 安装 PostCSS 核心及加载器
npm install postcss postcss-loader --save-dev
# 安装 px-to-viewport 插件
npm install postcss-px-to-viewport --save-dev
项目根目录创建转换配置文件
module.exports = {
plugins: {
'postcss-px-to-viewport': {
unitToConvert: 'px',
viewportWidth: 1920, // 设计稿宽度
viewportHeight: 1080, // 设计稿高度
unitPrecision: 5,
// 支持对这些属性进行转换,支持通配符['*']转换所有px属性,也可以在这里指定转换哪些属性
propList: [
// 'width',
// 'min-width',
// 'max-width',
// 'height',
// 'min-height',
// 'max-height',
// 'top',
// 'bottom',
// 'left',
// 'right'
],
// 重点修改:使用函数动态指定视口单位
viewportUnit: (prop) => {
if (prop.includes('height')) {
return 'vh'; // 高度相关属性使用vh
}
return 'vw'; // 其他属性使用vw
},
fontViewportUnit: 'vw', //字体使用vw还是vh
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: /node_modules/i,
landscape: false,
},
},
};
这个插件支持的配置很详细,可以参考文档 :
在构建工具中使用 postcss 插件
// webpack.config.js 或 vue.config.js 等
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader', // 确保 postcss-loader 在 css-loader 之后
],
},
],
},
};
项目构建之后你原来的样式:
.test {
width: 100px;
height: 50px;
font-size: 16px;
z-index: 10;
}
构建完成会变成这个样子:
.test {
width: 5.20833vw;
height: 4.62963vh;
font-size: 0.83333vw;
z-index: 10;
}
此时在任何分辨率都能保持一致的显示效果、比例了,这个方案是最现代化的方案,和上面 scale 的方式相比,我觉得这套方案是最优的选择,可以实现和 scale 一样的效果的同时, 还能保持住元素看到的和实际的位置都一致。
总结
最后,rem 方案是不建议选了,它的主要用途不是宽高适配,最后优先考虑纯 viewport+postcss 方案,配置麻烦点,但是维护性好。 不论是 px-to-viewport 插件,还是通过 scale 来实现缩放,总的来说,都是需要一开始预设好你的设计稿尺寸的。