vue 响应式布局方案探索
最近我司准备使用 vue3 + vant 重构原本 vue2 + vux 的项目,并且需要适配平板,原本的项目中,我们使用 rem 布局,这次重构时,我们原本准备采用了 vw 的布局方式,但是设计图中,某些页面在平板竖版的时候,需要将页面固定宽度并且内容居中展示,如图,采用 vw 布局时,文字就会变得很大,一番权衡之下,还是采用了 rem 布局,并且在不同视口宽度下,采用不同的根标签大小适配。
那应该如何实现呢,首先,根据 vant 组件库推荐的 rem 布局,需要安装插件 postcss-potorem 和 lib-flexible 这个包,此时的 package.json 相关配置如下:
            
            
              json
              
              
            
          
           "dependencies": {
    "amfe-flexible": "^2.2.1",
  },
  "devDependencies": {
    "postcss-pxtorem": "^6.1.0",
  }在根目录下新建文件 postcss.config.cjs 文件,添加 postcss-pxtorem 配置,可以参考 vant 官网查看 rem布局适配 这里。
            
            
              js
              
              
            
          
          module.exports = {
  map: true,
  plugins: {
    'postcss-pxtorem': {
      rootValue: 37.5,  // 此设计稿尺寸为375px
      propList: ['*'],
      mediaQuery: true
    },
  }
}但是这并不能满足我的需求,即大于一定尺寸时宽度固定。所以我这里删除了依赖 amfe-flexible ,并且在 utils 文件夹目录下新建文件 amfe-flexible.js 文件,将 amfe-flexible 依赖里面的内容拿过来稍作修改,并且在 main.js 文件中引入,这里的意思是,当视口宽度大于 PAD_MIN_WIDTH 这个值时,将根标签的 font-size 固定,否则取 docEl.clientWidth / 10 之后的值赋值给根标签的font-size。
            
            
              js
              
              
            
          
          const PAD_MIN_WIDTH=500
(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1
  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();
  // set 1rem = viewWidth / 10
  function setRemUnit () {
    // 当前按照500px区分 手机 和PAD
    // 手机时按尺寸缩放 PAD 时固定基础尺寸
    if (docEl.clientWidth > PAD_MIN_WIDTH) {
      docEl.style.fontSize = '37.5px'
      return
    }
    const rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
  }
  setRemUnit()
  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })
  // detect 0.5px supports
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
}(window, document))
此时基本已经完成了我所需要的功能,不过还有个小问题,就是在每个 vue 组件中,实际上样式基本都是添加了 scoped 属性的,如果需要写适配,则需要在每个组件中都写一遍媒体查询的设备尺寸的判断,如下这样:
            
            
              css
              
              
            
          
          <style scoped>
@media (max-width: 500px) {  
  /* 手机屏幕样式 */  
}  
  
@media (min-width: 501px) and (max-width: 1024px) {  
  /* 平板屏幕样式 */  
}  
  
</style>
如果设备尺寸的值变了,那么就需要一个个修改每个页面,所以这里准备使用一个变量代替当前位置写死的值,不过百度后发现设备尺寸这里并不支持使用变量的方式。
于是我接着百度了一下,最终找到了 postcss 的一个插件 PostCSS Custom Media ,这个插件是专门用来解决媒体查询中使用变量表达的,但是 ,又又又遇到了问题,在 vite 项目中使用时貌似这个插件不能识别,具体可以看这篇 issue ,顺着这篇文章,我发现了评论区有作者提供的解决办法。
If you're using Modular CSS such as, CSS Modules, postcss-loader or vanilla-extract to name a few, you'll probably notice that custom media queries are not being resolved... To overcome this, we recommend using the PostCSS Global Data plugin.
以上内容在这个issue里面 github.com/vitejs/vite...
于是,新插件 postcss-global-data 火速下载下来,npm install @csstools/postcss-global-data --save-dev 现在我的 package.json 相关的包时这样的。删除了 amfe-flexible 。
            
            
              json
              
              
            
          
          "devDependencies": {
    "@csstools/postcss-global-data": "^2.1.1",
    "postcss-pxtorem": "^6.1.0",
  }此时我的 postcss.config.cjs 文件中是这样的。
            
            
              js
              
              
            
          
          module.exports = {
  map: true,
  plugins: {
    '@csstools/postcss-global-data': {
      files: [
        './src/assets/style/custom-media.css'
      ]
    },
    'postcss-pxtorem': {
      rootValue: 37.5,  // 此设计稿尺寸为375px
      propList: ['*'],
      mediaQuery: true
    },
  }
}./src/assets/style/custom-media.css 这个路径就是将媒体查询中的设备尺寸定义为变量的文件路径,内容如下:
            
            
              css
              
              
            
          
          /* 针对手机的媒体查询 */
@custom-media --phone (width <= 500px);
/* 针对PAD的媒体查询 */
@custom-media --pad (width > 500px);
/* 针对全屏PAD的媒体查询 */
@custom-media --pad-full (width > 900px);但是,后来又遇到了问题,在我的电脑上运行的非常 nice ,同事的电脑上却没有正确的识别设备尺度的临界值,经过多次试错发现,只需要将 px 改为 PX 即可。
            
            
              css
              
              
            
          
          /* 针对手机的媒体查询 */
@custom-media --phone (width <= 500PX);
/* 针对PAD的媒体查询 */
@custom-media --pad (width > 500PX);
/* 针对全屏PAD的媒体查询 */
@custom-media --pad-full (width > 900PX);这里定义了三个变量,在具体的vue文件中使用时,只需要这样写即可:
            
            
              vue
              
              
            
          
          <style scoped>
@media (--phone) {  
  /* 手机样式 */  
}  
  
@media (--pad) {  
  /* 平板样式 */  
}  
    
@media (--pad-full) {  
  /* 平板全屏样式 */  
}  
  
</style>当然,媒体查询也可以判断设备是横屏还是竖屏,不过我司项目是嵌套在软件中使用,具体横屏竖屏由软件开发同事解决,所以这里并未写横屏的媒体查询,不过这个也不难,这样写即可。
            
            
              css
              
              
            
          
          @media (orientation: portrait) {  
  /* 设备为竖屏时的样式 */  
}  
  
@media (orientation: landscape) {  
  /* 设备为横屏时的样式 */  
}  
自此,响应式布局的全部配置搭建已完成,剩下的就是在不同的媒体查询中写样式了。