前言

在 CSS 中,我們可以用 Media Queries 的方式,分別定義橫向與直向的樣式:

.box {
  width: 300px;
  height: 300px;
  display: none;
}

@media (orientation: landscape) {
  .landscapeBox {
    display: block;
    background: lightblue;
  }
}

@media (orientation: portrait) {
  .portraitBox {
    display: block;
    background: lightcoral;
  }
}

但是,雖然可以隨著直、橫向定義不同的樣式

但卻無法讓元件知道現在的狀態,JS 並不知道目前的裝置方位為何

也就無法將這個值納入元件的 state 去做其他的判斷了

使用套件

我原本打算使用 react-hook-screen-orientation 這個套件來判斷行動裝置的方位

orientationchange 事件

於是我翻了一下這個套件的原始碼,發現它監聽的事件是 orientationchange,以下是部份原始碼:

window.addEventListener("orientationchange", updateOrientation);

於是,我查了一下 MDN 的文件,發現這個事件即將要捨棄了:

Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.

雖說要棄用,但目前這個事件在手機上還是可以正常作用

window.screenorientation 屬性

於是我又在原始碼發現了這一段:

const getOrientation = () => window.screen?.orientation?.type;

在 macOS Safari 的 console 裡輸入 window,發現 window.screen 底下沒有 orientation 這個屬性

所以使用套件,會抓到 undefined 也是很合理的

因此我推測所有使用 Webkit 的瀏覽器一樣沒有 orientation 屬性

測試結果

測試了四個作業系統(Windows、macOS、iOS、Android)、三種瀏覽器(Chrome、Firefox、Safari)之後,得出以下結論:

ChromeFirefoxSafari
MacOSOOX
WindowsOONA
iOSXXX
AndroidOONA

Windows 與 Android 系統上沒有 Safari,所以就不在討論範圍內

果然不出所料,Webkit 系的瀏覽器,全部都拿不到 orientation 屬性

orientation 的相容性

而 MDN 也列出 orientation 屬性對各瀏覽器的支援度,如下表:

browser compatibility

iOS 上的 Chrome、Firefox 應該也屬於 Safari on iOS 那一類

令人驚訝的是,IE11 居然有支援!

雖然行動裝置上也沒有 IE 可以測試,而 Windows 的 IE 整個打不開 CodeSandbox

所以就不實測了

至於 Safari 的結果就 ⋯⋯ 🤦‍♂️

不知道 Safari 什麼時候會支援啊 ⋯⋯

測試裝置與版本

以下是測試結果中使用的裝置,其作業系統及各瀏覽器版本,有需要可以參考一下:

OSChromeFirefoxSafari
MacOSMonterey 12.3.1101.0.4951.64100.015.14
WindowsWin10 21H2100.0.4896.12787.0NA
iOS15.14.1101.0.4951.58100.115.14
Android11101.0.4951.6191.1.0NA

替代方案:使用 matchMedia

因此,就算 orientationchange 事件仍然可以使用

但只要有裝置上的 window.screen 物件沒有 orientation 屬性

就沒辦法用此方法來判斷裝置的方位

於是我查了一下看是否有其他替代方案,在 stack overflow 找到了一些解決方案

而用 window.matchMedia("(orientation: portrait)") 去判斷當前 window 的方位,似乎是可行的方案

並加上監聽 change 事件,即可在當 orientation 改變的時候,得到最新的方位

於是,我把它寫成一個 hook,以便之後方便使用:

import { useCallback, useEffect, useState } from "react";

const getPortraitFromMediaQuery = () => window.matchMedia("(orientation: portrait)");

function UseIsPortrait(): boolean {
  const initialIsPortrait = getPortraitFromMediaQuery().matches;
  const [isPortrait, setIsPortrait] = useState<boolean>(initialIsPortrait);

  const updateOrientation = useCallback((event: MediaQueryListEvent) => {
    console.log("event emitted!!");
    if (event.matches) {
      // Portrait mode
      setIsPortrait(true);
    } else {
      // Landscape
      setIsPortrait(false);
    }
  }, []);

  useEffect(() => {
    const portrait = getPortraitFromMediaQuery();

    // start listening event
    portrait.addEventListener("change", updateOrientation);
    return () => portrait.removeEventListener("change", updateOrientation);
  }, [updateOrientation]);

  useEffect(() => {
    console.log("isPortrait: ", isPortrait);
  }, [isPortrait]);

  return isPortrait;
}

export default UseIsPortrait;

若將 matchMedia 改寫成:window.matchMedia("(orientation: landscape)")

matches 屬性拿到的布林值就會是相反的,就看比較偏好用哪種方式判斷了

後記

window.screen.orientation 確實是一個不錯的判斷方式

只可惜 WebKit 系的瀏覽器不支援,希望以後可以跟進(對 Safari 的成長速度頗為擔心就是了

window.matchMedia("(orientation: landscape)") 目前看來可以取代的方案

美中不足的是,它只能判斷「直」(portrait)與「橫」(landscape)

而無法像 orientation 屬性可以判斷主要橫向(landscape-primary)、次要橫向(landscape-secondary)、主要直向(portrait-primary)、次要橫向(portrait-secondary)四種不同的值(型別為 string

但其實基本的直、橫向判斷,就足以應付大部分的情況了

程式碼全貌

以上的程式碼全貌,可以參考以下的 CodeSandbox

可以試著在手機、平板等裝置上打開,看看不同瀏覽器的差異

最下面的有顏色的方塊是用純 CSS 的方式寫的,方便跟元件的 state 做比較

參考資料