16px
1rem
等于html的font-size
值目标元素尺寸(rem) = 设计稿尺寸(px) / 根字体大小(px)
安装postcss-pxtorem
npm install postcss-pxtorem --save-dev
配置postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 16, // 根字体大小
propList: ['*'], // 需要转换的属性,*表示全部
exclude: /node_modules/i // 排除node_modules目录
}
}
}
创建rem适配工具
// utils/rem.js
export function setRemUnit(designWidth = 1920) {
const docEl = document.documentElement;
const clientWidth = docEl.clientWidth || window.innerWidth;
if (!clientWidth) return;
// 计算根字体大小,例如设计稿1920px,根字体设为10px
const fontSize = (clientWidth / designWidth) * 10;
// 设置根字体大小
docEl.style.fontSize = `${fontSize}px`;
}
// 初始化
export function initRem(designWidth = 1920) {
setRemUnit(designWidth);
// 监听窗口大小变化
window.addEventListener('resize', () => {
setRemUnit(designWidth);
});
// 监听页面方向变化
window.addEventListener('orientationchange', () => {
setRemUnit(designWidth);
});
}
在main.js中初始化
import { initRem } from './utils/rem';
// 假设设计稿宽度为1920px
initRem(1920);
配置viewport
<meta name="viewport" content="width=device-width, initial-scale=1.0">
rem与vw结合
/* 基于视口宽度的根字体大小 */
html {
font-size: 1vw; /* 1vw = 视口宽度的1% */
}
/* 设计稿1920px,100px对应到rem为5.2rem */
.element {
width: 5.2rem; /* 相当于100px在1920px设计稿中的大小 */
}
/* 针对不同尺寸屏幕的调整 */
@media (max-width: 1600px) {
html {
font-size: 0.8333vw; /* 1600/1920=0.8333 */
}
}
@media (max-width: 1366px) {
html {
font-size: 0.7115vw; /* 1366/1920=0.7115 */
}
}
// utils/rem.js
export function pxToRem(px, designWidth = 1920) {
return `${px / (designWidth / 10)}rem`;
}
<!-- 基础卡片组件 -->
<template>
<div class="card">
<h3 class="title">{{ title }}</h3>
<div class="content">
<slot />
</div>
</div>
</template>
<style scoped>
.card {
width: 25rem; /* 相当于设计稿中的500px */
height: 18.75rem; /* 相当于设计稿中的375px */
padding: 1.25rem; /* 相当于设计稿中的25px */
border-radius: 0.5rem; /* 相当于设计稿中的10px */
background-color: white;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
.title {
font-size: 1.25rem; /* 相当于设计稿中的20px */
margin-bottom: 0.75rem; /* 相当于设计稿中的15px */
}
</style>
<!-- 大屏布局示例 -->
<template>
<div class="dashboard">
<Header />
<div class="main-content">
<Sidebar />
<div class="content-area">
<div class="row">
<Card title="数据概览">
<Chart type="line" :data="overviewData" />
</Card>
<Card title="实时监控">
<Chart type="bar" :data="monitorData" />
</Card>
</div>
<div class="row">
<Card title="地域分布" :span="2">
<Map :data="regionData" />
</Card>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dashboard {
display: flex;
flex-direction: column;
height: 100vh;
}
.main-content {
display: flex;
flex: 1;
}
.sidebar {
width: 15rem; /* 相当于设计稿中的300px */
background-color: #f5f5f5;
}
.content-area {
flex: 1;
padding: 1.25rem; /* 相当于设计稿中的25px */
}
.row {
display: flex;
gap: 1.25rem; /* 相当于设计稿中的25px */
margin-bottom: 1.25rem; /* 相当于设计稿中的25px */
}
.card {
flex: 1;
}
.card[span="2"] {
flex: 2;
}
</style>
<!-- 图表组件 -->
<template>
<div class="chart-container">
<canvas ref="chartCanvas"></canvas>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import Chart from 'chart.js/auto';
const props = defineProps({
type: {
type: String,
default: 'line'
},
data: {
type: Object,
required: true
}
});
const chartCanvas = ref(null);
let chartInstance = null;
onMounted(() => {
initChart();
});
watch(() => props.data, () => {
updateChart();
});
const initChart = () => {
if (chartInstance) {
chartInstance.destroy();
}
const ctx = chartCanvas.value.getContext('2d');
chartInstance = new Chart(ctx, {
type: props.type,
data: props.data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
};
const updateChart = () => {
if (chartInstance) {
chartInstance.data = props.data;
chartInstance.update();
}
};
</script>
<style scoped>
.chart-container {
width: 100%;
height: 18.75rem; /* 相当于设计稿中的375px */
}
</style>
字体大小问题
/* 使用px与rem混合方案 */
body {
font-size: 16px; /* 基础字体使用px */
}
h1 {
font-size: 1.5rem; /* 标题使用rem */
}
小数像素问题
// 优化根字体大小计算
export function setRemUnit(designWidth = 1920) {
const docEl = document.documentElement;
const clientWidth = docEl.clientWidth || window.innerWidth;
if (!clientWidth) return;
// 避免小数像素问题,设置最小字体单位
const baseFontSize = 10;
const fontSize = Math.round((clientWidth / designWidth) * baseFontSize);
docEl.style.fontSize = `${fontSize}px`;
}
/* 结合flex与rem */
.container {
display: flex;
flex-wrap: wrap;
gap: 1.25rem; /* 相当于设计稿中的25px */
}
.item {
flex: 0 0 calc(33.33% - 0.833rem); /* 相当于设计稿中的25px间隙 */
min-width: 18.75rem; /* 相当于设计稿中的375px */
}
// 按需加载重型组件
const HeavyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
timeout: 3000
});
// utils/rem.js
export function setRemUnit(designWidth = 1920) {
// ...原有代码...
}
// 添加节流函数
const throttle = (fn, delay) => {
let timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
};
};
export function initRem(designWidth = 1920) {
setRemUnit(designWidth);
// 使用节流处理resize事件
window.addEventListener('resize', throttle(() => {
setRemUnit(designWidth);
}, 100));
// 监听页面方向变化
window.addEventListener('orientationchange', () => {
setRemUnit(designWidth);
});
}
<!-- 虚拟滚动列表 -->
<template>
<div class="virtual-list" ref="listRef">
<div class="list-content" :style="{ height: contentHeight }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{ top: item.top, height: itemHeight + 'px' }"
class="list-item"
>
{{ item.content }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
const props = defineProps({
items: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 40
},
visibleCount: {
type: Number,
default: 10
}
});
const listRef = ref(null);
const startIndex = ref(0);
const contentHeight = computed(() => {
return `${props.items.length * props.itemHeight}px`;
});
const visibleItems = computed(() => {
return props.items.slice(startIndex.value, startIndex.value + props.visibleCount).map((item, index) => {
return {
...item,
top: `${(startIndex.value + index) * props.itemHeight}px`
};
});
});
const handleScroll = () => {
if (!listRef.value) return;
const scrollTop = listRef.value.scrollTop;
const newStartIndex = Math.floor(scrollTop / props.itemHeight);
if (newStartIndex !== startIndex.value) {
startIndex.value = newStartIndex;
}
};
onMounted(() => {
if (listRef.value) {
listRef.value.addEventListener('scroll', handleScroll);
}
});
</script>
// 测试工具:模拟不同分辨率
const testResolutions = [
{ width: 1920, height: 1080 },
{ width: 1600, height: 900 },
{ width: 1366, height: 768 },
{ width: 1280, height: 720 }
];
const simulateResolution = (resolution) => {
document.documentElement.style.width = `${resolution.width}px`;
document.documentElement.style.height = `${resolution.height}px`;
window.dispatchEvent(new Event('resize'));
};
// 使用方法
testResolutions.forEach((resolution, index) => {
setTimeout(() => {
simulateResolution(resolution);
console.log(`测试分辨率: ${resolution.width}x${resolution.height}`);
}, index * 3000);
});
// 使用Performance API监控渲染性能
const monitorPerformance = () => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name === 'render') {
console.log('渲染时间:', entry.duration, 'ms');
}
});
});
observer.observe({ entryTypes: ['measure'] });
// 在关键渲染点标记
performance.mark('render-start');
// 渲染操作...
performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');
};
通过本文提供的方案,你可以在Vue项目中高效地实现rem布局与大屏自适应。关键技术点包括:
这种方案特别适合数据可视化大屏、监控系统等需要适应多种屏幕尺寸的应用场景。根据实际项目需求,你可以进一步扩展其功能,如添加暗黑模式、多语言支持等。