使用ResizeObserver製作響應式Vue元件

chen小白發表於2018-07-18

使用ResizeObserver製作響應式Vue元件

前言

  • 一提到製作響應式元件或佈局,腦海裡首先想到的是通過@media查詢來控制,但是有一個問題,它能滿足你的需求麼?大多數情況下可以很好的解決問題,有時也會不靈驗。已一個例子作為說明。
  • 假設你要建立一個postItem元件,在大屏上post是這樣的顯示效果
  • image
  • 在手機上我需要這樣的效果
  • image
  • 第一反應就是想到媒體查詢,根據頁面的寬度來控制樣式,於是就有了下面的樣式。
@media only screen and (max-width: 576px) {
  .post__item {
    flex-direction: column;
  }
  
  .post__image {
    flex: 0 auto;
    height: auto;
  }
}
複製程式碼
  • 元件具有重用性,不是一次買賣,某天需求變了要在一個頁面上根據post的類別來顯示,效果如下
  • image
  • @media查詢的最大問題是,元件響應性基於螢幕大小,但應基於其自身大小,這時原來的媒體查詢就不靈驗了。在這種情況下,元件佈局僅取決於它們。這些元件應該是原子的,獨立地確定它們自己的大小並使其適應佈局。構建響應式元件,ResizeObserver是個不錯的選則。

介紹ResizeObserver

  • ResizeObserver:是一項新的功能,監聽元素的內容矩形大小的變更,並通知做出相應的反應。和document.onresize的功能很相似。
const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const cr = entry.contentRect;
    console.log('Element:', entry.target);
    console.log(`Element size: ${cr.width}px x ${cr.height}px`);
    console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
  })
})

observer.observe(someElement)
複製程式碼
  • 瀏覽器的支援性,如圖所示
  • image
  • 雖然目前主流瀏覽器對ResizeObserver不支援,慶幸的是,ResizeObserver有基於MutationObserver的polyfill,而主流瀏覽器對MutationObserv是支援的
  • image

使用

  • 作為元件
<template>
  <Responsive :breakpoints="{
    small: el => el.width <= 500
  }">
    <div slot-scope="el" :class="['post__item', { small: el.is.small }]">
      <img class="post__image" :src="post.image" />
      <div class="post__text">{{post.text}}</div>
    </div>
  </Responsive>
</template>

<script>
import { Responsive } from "vue-responsive-components"
export default {
  props: ['post'],
  components: { Responsive }
}
</script>

<style lang="scss">
.post__item {
  display: flex;
}
.post__image {
  flex: 0 0 200px;
  height: 200px;
}
.post__item.small {
  flex-direction: column;
  
  .post__image {
    flex: 0 auto;
    height: auto;
  }
}
</style>
複製程式碼
  • 作為指令
<template>
  <!-- Will add/remove .small if the width is less / greater -->
  <div class="post__item" v-responsive="{ small: el => el.width <= 500 }">
    <img class="post__image" :src="post.image" />
    <div class="post__text">{{post.text}}</div>
  </div>
</template>

<script>
import { ResponsiveDirective } from "vue-responsive-components"
export default {
  props: ["post"],
  directives: {
    responsive: ResponsiveDirective
  }
}
</script>
複製程式碼

外掛程式碼實現

  • npm install resize-observer-polyfill --save-dev
  • npm install loadsh --save-dev
import throttle from "lodash.throttle"
import ResizeObserver from "resize-observer-polyfill"

export const ResponsiveMixin = {
  data() {
    return {
      el: {
        width: 0,
        height: 0,
        is: {}
      }
    }
  },
  mounted() {
    if (
      typeof process === "undefined" ||
      (!process.server && (this.breakpoints || this.$options.breakpoints))
    ) {
      this.$nextTick(() => {
        const handleResize = throttle(entries => {
          const cr = entries[0].contentRect
          ;(this.el.width = cr.width), (this.el.height = cr.height)
          const conds = Object.assign(
            {},
            this.breakpoints || {},
            this.$options.breakpoints || {}
          )
          for (const breakpoint in conds) {
            this.$set(this.el.is, breakpoint, conds[breakpoint](this.el))
          }
        }, 200)

        const observer = new ResizeObserver(handleResize)
        if (this.$el instanceof Element) {
          observer.observe(this.$el)
        }
      })
    }
  }
}

export const Responsive = {
  data() {
    return { init: false }
  },
  props: {
    noHide: { type: Boolean, default: false },
    breakpoints: { type: Object, default: undefined }
  },
  mixins: [ResponsiveMixin],
  render(h) {
    const slot =
      (this.$scopedSlots.default && this.$scopedSlots.default(this.el)) ||
      this.$slots.default

    return !this.noHide && !this.init
      ? h(
          "div",
          {
            style: { visibility: "hidden" }
          },
          [slot]
        )
      : slot
  },
  mounted() {
    this.init = true
  }
}

export const ResponsiveDirective = {
  inserted(el, conds) {
    if (typeof process === "undefined" || !process.server) {
      const handleResize = throttle(entries => {
        const cr = entries[0].contentRect
        for (const breakpoint in conds.value) {
          if (conds.value[breakpoint](cr)) {
            el.classList.add(breakpoint)
          } else {
            el.classList.remove(breakpoint)
          }
        }
      }, 200)

      const observer = new ResizeObserver(handleResize)
      observer.observe(el)
    }
  }
}

export const VueResponsiveComponents = Vue => {
  Vue.component("Responsive", Responsive)
  Vue.directive("responsive", ResponsiveDirective)
}
複製程式碼

總結

  • ResizeObserver對響應式佈局提供了一種新穎的解決思路,讓元件保持了原子性、獨立性,同樣的思路也適用於angular、react等元件寫法(注意需根據angular、react語法規則更改)。

參考文件

相關文章