100vw和window.innerWidth并不相等的原因

333 阅读3分钟

问题原因

  1. 移动端浏览器的特殊性
  • 在移动端浏览器中,100vw 包含了滚动条的宽度
  • window.innerWidth 不包含滚动条宽度
  • 某些 App 内的 WebView 可能会对视口进行自定义处理
  1. App 内 WebView 的特殊处理
  • 一些 App 会修改 viewport 的 meta 设置
  • WebView 可能会添加自己的导航栏或工具栏
  • 部分 App 会对 WebView 进行缩放处理

解决方案

  1. 使用 CSS 变量动态设置视口宽度
/**
 * 视口宽度管理工具
 */
export class ViewportManager {
    constructor() {
        this.viewportWidth = 0;
        this.init();
    }

    init() {
        // 初始化视口宽度
        this.setViewportWidth();
        // 监听resize事件
        window.addEventListener('resize', this.handleResize.bind(this));
    }

    setViewportWidth() {
        // 获取真实视口宽度
        const realWidth = window.innerWidth;
        this.viewportWidth = realWidth;
        
        // 设置CSS变量
        document.documentElement.style.setProperty('--vw', `${realWidth / 100}px`);
    }

    handleResize() {
        this.setViewportWidth();
    }

    // 获取当前视口宽度
    getViewportWidth() {
        return this.viewportWidth;
    }
}
  1. 创建适配工具函数
/**
 * 移动端适配工具类
 */
export class MobileAdapter {
    constructor(designWidth = 750) {
        this.designWidth = designWidth;
        this.viewportManager = new ViewportManager();
    }

    /**
     * px 转换为 vw 单位
     * @param {number} px - 像素值
     * @returns {string} vw值
     */
    px2vw(px) {
        return `${(px / this.designWidth) * 100}vw`;
    }

    /**
     * 获取实际像素值
     * @param {number} px - 设计稿像素值
     * @returns {number} 实际像素值
     */
    getRealPx(px) {
        const vw = this.viewportManager.getViewportWidth() / 100;
        return (px / this.designWidth) * 100 * vw;
    }
}
  1. 创建 LESS 混入
// 基准设计稿宽度
@design-width: 750;

// px 转 vw 混入
.px2vw(@property, @px) {
    @{property}: calc(@px / @design-width * 100 * var(--vw));
}

// 尺寸设置混入
.size(@width, @height) {
    .px2vw(width, @width);
    .px2vw(height, @height);
}

// 字体大小混入
.font-size(@px) {
    .px2vw(font-size, @px);
}
  1. 创建适配组件
import {ViewportManager} from '../utils/viewport';
import {MobileAdapter} from '../utils/adapter';

export class ViewportAdapter {
    constructor() {
        this.viewportManager = new ViewportManager();
        this.mobileAdapter = new MobileAdapter();
        this.init();
    }

    init() {
        // 设置 viewport meta
        this.setViewportMeta();
        // 初始化事件监听
        this.initEventListeners();
    }

    setViewportMeta() {
        const meta = document.createElement('meta');
        meta.setAttribute('name', 'viewport');
        meta.setAttribute('content', 
            'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
        );
        document.head.appendChild(meta);
    }

    initEventListeners() {
        // 监听屏幕旋转
        window.addEventListener('orientationchange', () => {
            setTimeout(() => {
                this.viewportManager.setViewportWidth();
            }, 300);
        });

        // 监听页面加载完成
        window.addEventListener('load', () => {
            this.viewportManager.setViewportWidth();
        });
    }
}
  1. 使用示例
import {ViewportAdapter} from '../components/ViewportAdapter';
import {MobileAdapter} from '../utils/adapter';

class Demo {
    constructor() {
        // 初始化视口适配
        this.viewportAdapter = new ViewportAdapter();
        this.mobileAdapter = new MobileAdapter();
        
        this.init();
    }

    init() {
        // 示例:动态设置元素宽度
        const element = document.querySelector('.demo-element');
        const width = this.mobileAdapter.getRealPx(200);
        element.style.width = `${width}px`;
    }
}
  1. 样式应用示例
@import './mixins.less';

.demo-container {
    .size(750, 1334);
    background: #fff;
    
    .header {
        .size(750, 88);
        .px2vw(padding-left, 32);
        .px2vw(padding-right, 32);
        
        &-title {
            .font-size(32);
            color: #333;
        }
    }
    
    .content {
        .px2vw(margin-top, 20);
        .px2vw(padding, 32);
        
        &-item {
            .size(686, 200);
            .px2vw(margin-bottom, 20);
            .px2vw(border-radius, 16);
            background: #f5f5f5;
            
            &-title {
                .font-size(28);
                .px2vw(line-height, 40);
                color: #666;
            }
        }
    }
}
  1. HTML 结构示例
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>移动端适配示例</title>
</head>
<body>
    <div class="demo-container">
        <header class="header">
            <h1 class="header-title">移动端适配示例</h1>
        </header>
        
        <main class="content">
            <div class="content-item">
                <h2 class="content-item-title">内容区域</h2>
            </div>
        </main>
    </div>
    
    <script>
        // 初始化适配
        new Demo();
    </script>
</body>
</html>

核心解决思路

  1. 使用 CSS 变量存储真实视口宽度单位
  2. 通过 JavaScript 动态计算和更新视口宽度
  3. 使用 LESS 混入统一管理单位转换
  4. 提供完整的适配工具类和组件
  5. 监听相关事件确保适配的实时性

优点

  1. 精确控制视口宽度
  2. 适应不同 App 内 WebView 的特殊处理
  3. 统一的开发体验
  4. 良好的可维护性
  5. 支持动态适配

注意事项

  1. 需要在页面初始化时就执行适配逻辑
  2. 注意性能优化,避免频繁计算
  3. 考虑兼容性问题
  4. 注意屏幕旋转的处理

这套解决方案可以有效解决移动端 WebView 中 100vw 与 window.innerWidth 不一致的问题,同时提供了完整的适配方案。