跳到內容
關於我 數位花園

部落格

鴨川的各種風貌

曾經一段時間住在京都車站的南側,大概是十條車站那一帶。

因為鄰近鴨川,所以很常去那裡散步或跑步。説到鴨川這條河流,想必去過京都旅遊的人都會有印象。鴨川由南邊一路向北,貫穿京都市的各個著名的觀光景點。京都市内東西向的幾條大路,我想很多人並不陌生。從北邊的「二條」一直到靠近京都車站南邊的「十條」。如果對路名不太有概念也沒什麽關係,因爲我也不是非常地熟門熟路。我自創一個口訣來瞭解全貌:

古城二條、祇園四條、清水五條、前站七條、後站九條

二條最有名的景點大概就是二條城;四條則是祇園、八坂神社、鴨川納涼床,還有商店街、咖啡店、旅館與酒吧;五條非清水寺、產寧坂莫屬;七條則是京都車站的前站,有京都電視塔與 Yodobashi Camera;京都後站大概就是東寺、東福寺、Aeon Mall

或許你好奇問:「那十條有什麼?」

十條是個很平凡的地方,硬要說有什麼地標的話,任天堂總部在這裡,像是一塊龐大的白色石碑佇立在那裡,直到晚上九點多仍是燈火通明。

起初,鴨川給我的印象是一個充滿濃濃京都風、河邊充滿著閒散旅客的地方,或是夏天的「鴨川納涼床」(雖然我從來沒進去坐過)。但實際上那卻只是其中的一小部分而已。或者該說,大部分的景象其實沒有那麼地唯美。

沿著主要幹道往東邊走,就會遇到鴨川。因為過橋之後就要上交流道,路上的車快速疾駛,空氣中充斥著引擎聲。橋的前方有一整區的集合住宅大樓,一整排站開來望著鴨川,我從那個社區旁一個狹窄走道溜進去,迎面而來就是鼎鼎大名的鴨川了。陽光佈滿這個河床,更顯得綠意盎然,河邊的步道緊鄰著水域,也沒什麼欄杆之類的人造物,感覺接近了大自然一點。步道上幾乎沒有人,散步兼散心,想說就這樣一路往北走,能走多遠就走多遠。

潺潺流水聲、映入眼簾的各種鳥類、植物、河床上滿地的礫石,說真的真是另一種享受,像是「我正存在著」的那種感覺。有時會看到零星幾個孩子在河裡戲水、或是大叔們坐在那靜靜地釣魚。平凡無奇的景色,也會來一點不一樣的驚喜。偶爾在河床上看到一堆堆的石碓,各種不同的型態,依然故我地佇立在那裡。起初以為是因爲超自然力量都市傳説之類的緣故造成的(做什麼白日夢),後來查了一下,是稱之為「賽之河原」的傳說故事。

賽之河原 1 的傳説是出自於日本的中世紀時代,但也有種説法是出自於佛教的「法華經」。人們相信比父母親早逝的孩子們,會在陰間 2 的河床 3 ——也就是西院(齋院)的河床上遭遇苦難,他們會在此堆起石頭,然而惡鬼會出現,撞倒那些石碓,並責罵那些孩子們,此時地藏菩薩就會出現拯救那些孩子。「賽」一字,一說是出自於京都的佐比川(「佐比」音同「賽」)或是奈良的狹井川(「狹井」音同「賽」)。

原來是一個帶點悲傷的故事,而不是什麼都市傳說。我不知道做那些石碓的人們是不是都發生這樣的事。每次經過時,雖然沒有認真數過,總感覺有增加新的石碓,卻從來沒有看過正在堆疊石頭的人影。我不禁思索,他們是在何時來這裏堆石頭呢?又是帶著如何的心情?這個故事帶給我一種悲傷但沉靜的感覺,卻隱隱有股强大的意志力。

每從一座橋下穿越,就代表著又往前了一點。有些橋是鐵路專用,有些則是一般道路,給行人、汽車使用。尤其是站在鐵路專用橋的下方,距離大概只有兩公尺左右,往上可以看到陽光從鐵軌之間灑下,與其説是木漏日4,應該稱作是漏日才是。不過原本柔幻似水的詞,用在此確實少了那種意境,反帶了點陽剛的氣息。當電車經過的時候,壓過鐵軌發出的聲響可真是震撼,心想著自己正站在沉重巨大疾駛電車的正下方,那種感覺真是難以言喻。

越往北邊,河邊的步道逐漸變得寬敞,也越來越多人影。有像是音樂系的學生(我猜的),面著河吹著小號或是薩克斯風,我不太懂音樂,但伴隨著鴨川的流水聲與充滿綠意的河床,確實是視覺與聽覺上的享受。還有人帶了小型烤肉架,圍坐著吃肉、喝啤酒。以往我都是用橋來辨認位置的(例如:「啊,三條大橋到了」)但其實從人們在河邊休閒活動,就可以大略知道目前正位於鴨川的哪一段。繼續往北走,就會看到有名的「鴨川納涼床」,上面塞滿了旅客,納涼床附近一帶的河床非常寬廣,但是同樣也擠滿了人,而且一眼望去,不只日本人,外國人的面孔也不在少數。你可以想像是在大安森林公園的草地上,擠滿了人圍坐成一個個圈子那種光景。離開河床,走上去看看旁邊的街道。四條烏丸附近交通特別地繁忙,烏黑得閃閃發亮的計程車到處都是,還有滿滿的人。這裡大概是最熱鬧的地方吧,有各種餐廳、商店街、咖啡店、酒吧,還有百貨公司及旅館,外國人密集度最高的地方,總像是走在大阪梅田車站附近的感覺。

其實從十條走到四條、三條這一帶,腳已經很酸了,雖然只要走回街道上搭個電車就可以輕鬆回家,但我實在不想這麼做。充滿吵雜與人群的商店街會讓我更身心更疲憊。於是我選擇沿著原路回去,我還想要多聽聽鴨川的聲音。天色漸暗,四條烏丸一帶的人卻仍然很多,京都的夜生活才剛開始呢,這裡大概是京都少有的夜生活吧。托觀光客的福,店家才願意營業到比較晚的時間。三條若再繼續走下去,直到出町柳站,那裡是鴨川與今出川的匯流處,因為是交匯處所以水域非常寬廣,有很多觀光客、學生們,還有有名的跳石。

台灣有些地方的河濱公園規劃得很好,非常適合晚間散步或運動,所以我想說也可以在鴨川試試,沒想到出乎意料。吃完晚餐後,我走向通往交流道的橋邊,看到橋的對面,小鋼珠店閃亮的招牌,讓我聯想到美國拉斯維加斯的霓虹招牌,雖然我沒去過拉斯維加斯。一樣走過河邊社區旁狹窄的走道,卻馬上感到異樣。完全沒有路燈,白天走的時候完全沒有注意到,晚上卻異常得明顯,河邊一盞路燈都沒有,幾乎只有微弱的月光,聽起來很浪漫?一點也不。白天都沒什麼人了,更別提晚上,此時也就不逞強說自己不怕黑了,走路的時候總是瞻前顧後、提心吊膽,沒有人還好,看到疑似人影就嚇得半死。鴨川景色依舊,只是籠罩在黑夜裡,整個氛圍就不太一樣了。心中不斷拉扯:「再走一小段?還是掉頭直接回家?」最後在前方一個小出口,還有一盞路燈(當下心裡則稱之為明燈)的位置離開了鴨川。

那次真是有驚無險的經驗,雖然我什麼也沒看到、沒聽到、更沒遇到什麼事,但心裡總有種說不出的不安。當下還冒出「就算在這裡被殺也不會馬上被發現」的想法。才晚上八點多而已,河邊只剩一片漆黑 ⋯⋯ 我想納涼床那邊應該還是車水馬龍、燈火通明吧,彷彿完全不同的世界,若問哪一個比較接近日常的鴨川,我應該會選差點把我嚇死的那一種吧,但是僅限於白天就是了。只要走一次,就能享受鴨川的不同風情、不同的魅力。不過若邀我夜遊鴨川,恕我敬謝不敏。

{/* Ref:

  1. 賽の河原(さいのかわら)

  2. 冥土(めいど)

  3. 河原(かわら)

  4. 木漏れ日:日文中非常有詩意、卻難以翻譯的詞彙之一,字面意思是「從樹葉空隙灑進來的陽光」

E2E 測試導向的開發流程

某天,我們從設計師那裡拿到了設計圖

於是我們開始切版,跟往常一樣,主要注重在畫面的樣式與互動上,此時並不會考慮到 E2E 測試

若開發期間,很幸運地拿到了 QA 的測試案例(或任何寫測試案例的人),我們是否可以在切版的當下,也將未來寫測試案例的情況也加入考慮?

此外,這裡提到的測試案例是指 E2E 測試(自動化測試),而非人工的整合測試

為了讓寫測試的過程能更順利,依據測試案例上的各種 action,例如:點擊某個 button、在 input 輸入文字,或是從某個 div 元素上獲取文字。在這些計畫做事的 DOM 元件上安插屬性(attribute)作為 querySelector 的查詢指標

若手上沒有測試案例又該如何呢?沒有測試案例,就無法準確知道哪些元素會被查詢,只能用推測的。此時腦中閃過一個想法:

如果制定一些規則,然後按照這個規則去添加屬性,是否就可以維持一定的覆蓋率?

因此我根據自己寫測試的經驗,嘗試制定了一些規則。我將會分成以下幾個部分說明:

  1. 元件取向
  2. 位置取向
  3. 狀態取向
  4. 無形資料取向
  5. 結構取向

以下所有程式,我會以 React 元件來解釋。實際上無論框架,最終都會回到 HTML 元素身上

插入 data-test-page 屬性於最外層的 HTML 元素:data-test-page='<頁面元件名稱>'

有了 data-test-page 屬性,我們可以很快地辨認出這個頁面元件的範圍(我更喜歡稱之為「邊界」)

註:屬性名稱全看個人喜好,這裡展示的是我自己覺得不錯的命名方式 🙃

例如我們有一個 home 元件:

function HomePage() {
return <div data-test-page="home">{/* ... */}</div>;
}

插入 data-test-comp 屬性於最外層的 HTML 元素:data-test-comp='<元件名稱>'

有了 data-test-comp 屬性,我們可以很快地辨認出這個基本元件的範圍

function Button() {
return <div data-test-comp="button">{/* ... */}</div>;
}

元件會在不同地方重複使用(這也是我們要抽成元件的初衷,DRY 原則),因此,data-test-comp 就會出現重複的情況,而且如果在相近的地方有多個重複的元件,就不太好快速辨識目標元件

比如說我們在一個頁面上,用了 5 個 button 元件,而測試案例的動作是:「選取其中一個 button,然後點擊它。」此時,我們必須先找出所有 button 的元素,然後在裡面尋找我們要做點擊的那個 button(比對 button 的文字;或是直接指定第幾個 button 元素 ⋯⋯ 等)

這個過程不困難,但我認為這個過程很煩,而且不斷重複在做這個查找的動作。於是我開始思考,是否有更精確、更快速的方式。如果有一個唯一值(或接近唯一的值,亦即很少重複),這個元素尋找的過程將會簡單許多

因此,像是這樣的情境,我們需要另一個表示功能的特徵1 屬性(這個特徵屬性最好是唯一值)

使用 data-test-feat 屬性,用來辨識這個元件在這裡所提供的功能(或目的)

我認為這個屬性比較偏向是非強制性的。倘若只有在元件上標示屬性,仍然可以拿到我們想要的元件。例如:在導覽列(navbar 元件)上的登入 button 及選單裡(menu 元件)的登入 button,分別包含在不同元件裡面。此外,過度使用可能會造成最後很多重複的屬性,唯一性的特徵消失了,精準打擊(aka 準確查詢)的初衷也會隨之化為泡影。因此,要在這之間取得平衡

所以,data-test-feat 屬性大概有兩種加入方式:

有時候,基於元件的樣式設計(有外層決定寬高、背景色 ⋯⋯ 等),我們會在元件的外面包一層容器元素

function HomePage() {
return (
<>
{/* ... */}
<div data-test-feat="confirmBtn">
<Button />
</div>
<div data-test-feat="cancelBtn">
<Button />
</div>
{/* ... */}
</>
);
}

上面的範例,可以很清楚地知道,在 home 頁面裡,有兩個 button:確認 button 及取消 button

如果在元件外面包一層容器元素不是你的菜,可以透過 props 的方式穿進去元件裡:

若沒有外層的容器元素,透過 props 傳進去是另一個選擇:

function HomePage() {
return (
<>
{/* ... */}
<Button dataTestFeat="confirmBtn" />
<Button dataTestFeat="cancelBtn" />
{/* ... */}
</>
);
}

在 button 元件裡:

function Button({ dataTestFeat, btnText, labelText }) {
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-feat={dataTestFeat}>{btnText}</button>
</label>
</div>
);
}

有時候我們想知道某個元件的狀態,假如我們有一個 toggle button 元件:

toggle button

測試案例中,我們想要在點擊 toggle button 之後,確認它會確實從 on 狀態轉變成 off 的狀態

我們當然可以透過辨認 style 的變化得知它的狀態。如果是用 SASS 的話,或許可以很簡單地從 class 上辨認出(端看 class 怎麼設計了)。然而我最近比較常用的是 tailwindcss,此時就會變得不太直覺。以下是我的 toggle button 元件:

function ToggleButton() {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<button
type="button"
className="rounded-full overflow-hidden relative w-16 h-7 shrink-0 text-white font-semibold text-sm uppercase ml-2.5 bg-neutral-500"
onClick={handleChange}
>
{/* here is what the difference by state */}
<div className={`absolute transition left-1.5 top-1/2 -translate-y-1/2 ${isOn && "translate-x-9"}`}>
<div className="rounded-full bg-white w-4 h-4 relative">
<div className="absolute right-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">On</div>
<div className="absolute left-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">Off</div>
</div>
</div>
</button>
);
}

這個元件裡,translate-x-9 是 on 與 off 狀態上差異的 class。我們仍然可以辨識,但會造成修改樣式時後容易抓不到元素的情況。Utility first 樣式庫的優點,在此時卻成了缺點

像這種情況,建議加入表示狀態的屬性:

function Button({ btnText, labelText }) {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-state={isOn} onClick={handleChange}>
{btnText}
</button>
</label>
</div>
);
}

另一個常見的情境是載入狀態(loading),假如有一個列表與搜尋列元件:

function SearchPage() {
// ...some state here
return (
<div data-test-page="searchPage">
<div data-test-comp="searchBar">
<input value={searchValue} data-test-feat="searchInput" />
<button onClick={handleSearch} data-test-feat="searchBtn">
Search
</button>
</div>
<div data-test-comp="list" data-test-loading={isLoading}>
{/* list data here */}
</div>
</div>
);
}

註:基於可讀性,我直接用 HTML 表示

點擊搜尋 button 之後,頁面就會開始 loading 打 API 拿資料,並在結束後,結束 loading 狀態,並更新資料至底下的列表

假如有一個 card 元件要顯示商品資訊,如在元素上存在 product id 的話,就能快速地找到目標商品。然而商品資訊卡片通常不會顯示 product id,此時就必須將這個值加到屬性當中:

function ProductCard({ productId, ...restProps }) {
return (
<div data-test-comp="productCard" data-test-product-id={productId}>
{/* product card content */}
</div>
);
}

至於命名原則,我認為只要夠語意化就行了

在一些特殊情境下,我們會需要用 div 元素去模擬其他元素

這裡舉個例子,用 div 元素去模擬 table 元素的結構。加入 data-test-el 屬性:data-test-el='<模擬的元素名稱>'

function Table() {
return (
<div data-test-el="table">
<div data-test-el="tbody">{/* ... */}</div>
</div>
);
}

或許會有人不太懂為何要這樣做,但我還是可以簡單說明一下

在開發的時候,遇到某些樣式在與 table 有關的元素上(也就是<table><thead><tbody>⋯⋯ 等),在 Safari 瀏覽器下的顯示會不如預期(Safari 的 bug ⋯⋯),因此必須用 div 元素重建整個 table 的結構,因此在加入屬性之後,可以很快地了解整個 table 的結構,藉此增加可讀性

誠如模擬元素,為了增加可讀性,我們也可以在其他地方加入屬性,可以更快地了解整個元件結構。例如一個 dialog 元件:

function Dialog() {
return (
<div data-test-comp="dialog">
<div data-test-el="dialogHeader">this is dialog header</div>
<div data-test-el="dialogBody">this is dialog body</div>
<div data-test-el="dialogFooter">this is dialog footer</div>
</div>
);
}

加入 data-test-el 屬性之後,可以有效率地一眼看出整個元件的結構(在這裡,dialog 分為 dialog header、dialog body、dialog footer 三個區塊)

開發時,加入屬性將會是一個漫長的過程。在開發的過程中,測試案例並非我們的優先考量。制定規則並遵守它,可以讓我們無腦地加入屬性,並將專注力放在實作樣式外觀、操作互動以及資料上。當開發結束後,撰寫測試案例將會更為順利,並感謝以前的自己

最後,說到底這些規則畢竟只是自己的想法,並非什麼業界標準那麼偉大的東西 😎

開發愉快

  1. Feature,我翻譯成「特徵」

機動性與獨立傢俱

我對擁有機動性特質的東西有種莫名地偏好

比起裝潢時就固定位置的系統傢俱,我比較喜歡獨立傢俱

可以隨時改變整個空間的擺設,是一個很大的優點

空間的擺設只要有稍微地變化,就會帶來煥然一新的感覺

雖說沒有要「先查看當日運勢,再依照幸運方位做擺設」如此般地狂熱

但也有像是「今天的陽光特別璀璨,很適合將矮桌挪到窗邊看點書」的雅緻

⋯⋯開玩笑的

一本正經八百地講起了系統傢俱與獨立傢俱之間的差別

但對於租屋族來說,好像也沒有什麼選擇可言

基本上就看房東的屋子是如何就是如何

硬是要塞進大型傢俱,對居住的當事人而言,恐怕也是很困擾的

所幸現在的租屋處,在搬進來前,是幾近空屋的狀態

說是得來不易,我覺得一點也不誇張

然而,我也沒有因此要大顯身手、打造成 Ikea 樣品屋的北歐風,或無印良品風

只有少數幾樣必要的傢俱就足夠了

這讓我想到賈伯斯的一張照片:

他席地而坐在空蕩蕩的房間裡,只有一盞落地燈與黑膠唱片機相伴

steve jobs in living room

身為極簡主義者,適當的空白是很重要的

這句台詞頗適合,但他本人並沒有說過這種話就是了

不過無需將這概念想得太過崇高

其實概念很簡單的:

i'm minimalist

或許,把身為凡人的我與老賈相比,本來就不是個明智的作為

學習摩斯電碼

最近在重溫一款以前很喜歡的遊戲:絕地戰兵(HELLDIVERS

遊戲中有一個叫做「真實傳播器」(Truth Transmitter)的裝置,在任務完成後,它會發出摩斯電碼訊號,其傳輸的訊號是:

··· ··- ·—· · ·-· -····- · ·- ·-· - ···· -··· · ·- -·-· --- -· ·- -·-· - ·· ···- · ·-·-·-

意思是:

SUPER-EARTHBEACONACTIVE.

↓ 真實傳播器 ↓ truth-transmitter

這讓我對摩斯電碼產生興趣

於是就開始在網路上尋找摩斯電碼的學習資源

Google 提供了一個線上的摩斯密碼學習工具,可以在手機或是電腦上開啟,可以立即開始學習,有興趣的同學可以點選連結前往:Morse - Learn

這個學習工具利用圖像學習記憶法,去記憶每一個字幕以及對應的符號,感覺非常地有用

雖然在學習的當中,感覺有點像是在學海軍常用的「北約音標字母NATO phonetic alphabet)」(也就是所謂的 Alpha、Bravo、Charlie、……), 但那些北約音標字母對應的單詞,都是世界通用的,而非只是方便記憶

事實上,我也有找到其他的記憶學習法

學習的過程中,每個字母對應一個單字,然後利用這個單字將其意義圖像化之後,聯想對應的密碼組合,感覺滿有趣的

Google 的這套學習方式,其字母對照表如下:

hello morse

當然,除了英文字母外,還有數字的圖像記憶,這裡就不列出來了,待有興趣的同學自行探索

從前面的對照表中,我們知道基本上是由短訊號、長訊號兩種元素組成

但基本組成元素大致可分為分為五種:

  1. 點(或稱「滴」):短訊號,符號表示為 ·,1 個單位時間(點決定基本單位時間)
  2. 劃(或稱「答」):長訊號,符號表示為 -,時間長度為 3個單位時間
  3. 點劃間隔:在一個字母裡,···--- 之間的間隔,為 1 個單位時間
  4. 字符間隔:3 個單位時間
  5. 單詞間隔:7 個單位時間

借用一下維基百科的範例,「morse code」寫成摩斯電碼,如下:

−− −−− ·−· ··· · −·−· −−− −·· ·
M O R S E C O D E

再來就是三種間隔的差異

從下表可以看出,第一行是時間軸,每個數字代表單位時間

第二行是每個字母的開始與結束範圍

第三行是實際打訊號的動作,= 代表 signal on,. 代表 signal off

1 2 3 4 5 6 7 8
12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
M------ O---------- R------ S---- E C---------- O---------- D------ E
===.===...===.===.===...=.===.=...=.=.=...=.......===.=.===.=...===.===.===...===.=.=...=
^ ^ ^ ^ ^
| dah dit | |
symbol space letter space word space

電碼需要傳輸媒介,才能將要表達的訊號傳出去,接收者才能收到訊息並加以解讀,傳統的摩斯電碼是透過電纜傳輸電子訊號,此外還有其他的傳輸媒介:

  • 有線電路:傳統的摩斯電碼透過叫做「電鍵」(Telegraph Key)的裝置、經由有線電路傳輸電子訊號
  • 無線電波:無線電波透過無線電波傳送兩種類的訊號
  • 可見訊號 1 媒介:利用阿爾迪斯燈(Aldis lamp)日光儀(Heliograph)或是手電筒發送可見光訊號
  • 可聽訊號 2 媒介:例如用車用喇叭發送聲波訊號

Prosign 是 Procedural Sign 的縮寫,是摩斯電碼為了簡化及標準化通訊方式的一種方式

主要會是透過一些大家共識的縮寫來代表個意義,藉此增進通訊效率與準確性

一個簡單的範例是:用 K 代表 “okay, hear you, continue” 的意思

另一個常見的 prosign 的範例是廣為人知的 SOS,摩斯電碼寫成:···---···,因為 prosign 是一個組合代表特定意義(例如這裡是求救訊號),所以字母之間就不會用間隔(··· --- ···

不是所有的 prosign 都是通用的,不同的 prosign 由不同的機構定義

Google 除了有一個學習網頁推廣摩斯電碼之外,還提供了一個摩斯電碼的輸入法供大家使用

gboard

只要在手機上安裝 Gboard 輸入法(App StoreGoogle Play),就可以在裡面新增摩斯電碼輸入法

打字的時候,還會有電報的聲音呢~真的很酷 😆

缺點就是只能打英文

可以趁跟別人聊天的時候多多練習摩斯電碼

雖然一開始應該會奇慢無比吧🤣

其實寫這篇文的當下,我還沒有把所有的摩斯電碼符號學完

只完成了英文字母及 0~9 的數字,剩下的符號部分則還沒開始

其實摩斯電碼不是只有英文,歐洲的一些語言也有各自的版本,像是希臘文、俄文

亞洲的部分,像是阿拉伯文、日文、甚至中文也都有自己的一套摩斯電碼

感覺滿有趣,之後想來研究一下日文的摩斯電碼

PS. 據維基百科,日文摩斯電碼稱之為和文符號Wabun Code), 日文稱為「和文モールス符号

  1. Visual signal

  2. Audible

行動裝置上 100vh 的奇怪行爲

最近遇到一個手機瀏覽器上,在某個特定排版會發生高度有誤的問題

進而發現手機瀏覽器的特殊行爲

查了一下資料,發現這個問題已經存在許久,也有不少的解法

大致分為用 JS 及 CSS 去解決

CSS 的解決方式比較簡單明瞭

想要直接看結果,可以走傳送門:JS 解決方案CSS 解決方案

這個頁面是滿版的,所以 container 的部分佔滿整個 document.body

container 裡面,由上而下包含三個區域:header、body、footer

header 與 footer 分別固定在 container 上緣與下緣,body 撐滿剩下的空間,如果内容超過就會有捲軸滾動

layout 的示意圖如下:

layout

外層的 container 寬高分別是 100vw、100vh,在電腦上看起來沒什麼問題,然而在手機上就不是那麼回事:

100vh-issue-1

可以看到 container 高度是 100vh,卻仍然可以 scroll 的現象,照理來說應該要剛好撐滿才是

備註:可以在手機上點開連結,看一下真實情況


因此,若將 container 裡面的東西加進來,就會造成內外都有 scroll 的情況:

100vh-issue-2


看來 CSS 所抓到的 100vh,跟實際上的 window 高度不一樣,window 比較小,所以造成了 scroll

於是,試著把 container 元素的高度與 window 的高度印出來

var containerElement = document.getElementById("container");
var containerHeight = containerElement.clientHeight;
var heightElement = document.getElementById("container-height");
heightElement.innerText = "container clientHeight: " + containerHeight + "px";
var windowHeightEl = document.getElementById("window-height");
windowHeightEl.innerHTML = "window innerHeight: " + window.innerHeight + "px";

在電腦瀏覽器上,這兩個數字會是一樣的,在手機上卻不同:

different-height

查了一下網路上的資料,大概有兩種方式可以解

一個是用 JS,一個是用 CSS

於是我加上一個 resize 事件,監聽 window 的高度變化,並顯示在畫面上:

var containerElement = document.getElementById("container");
var containerHeight = containerElement.clientHeight;
var heightElement = document.getElementById("container-height");
heightElement.innerText = "container clientHeight: " + containerHeight + "px";
function updateWindowHeight() {
var windowHeightEl = document.getElementById("window-height");
windowHeightEl.innerHTML = "window innerHeight: " + window.innerHeight + "px";
}
updateWindowHeight();
window.addEventListener("resize", updateWindowHeight);

在 iOS Chrome 上,可以看出兩者的高度並不一致:

100vh-issue-3-ios-chrome

iOS 的 Safari,除了 container 與 window 高度不同之外,還發現了另一個現象

100vh-issue-3

在最頂的時候往下滑動,window 的高度是動態的

mind-explosion

在 Android Chrome 上,也出現 window 變化的狀況,但還是有點差異

100vh-issue-3-android-chrome

原來是彩蛋啊!!🥚

既然 100vh 與 window 的高度會不一致,而且在 iOS Safari 與 Android Chrome 上還會不斷改變, 那麼就在監聽 windowresize 事件時, 將 container 元素的高度複寫成當下的 window 高度就行了:

(function () {
function updateWindowHeight() {
var currentWindowHeight = window.innerHeight;
var windowHeightEl = document.getElementById("window-height");
windowHeightEl.innerHTML = "window innerHeight: " + currentWindowHeight + "px";
var containerElement = document.getElementById("container");
containerElement.style.height = currentWindowHeight + "px";
var containerHeight = containerElement.clientHeight;
var heightElement = document.getElementById("container-height");
heightElement.innerText = "container clientHeight: " + containerHeight + "px";
}
updateWindowHeight();
window.addEventListener("resize", updateWindowHeight);
})();

100vh-issue-4

我們可以看到,container 高度與 window 高度,始終保持同步,也就避免出現外層 scroll 的情況發生了

100vh-issue-rotate

旋轉螢幕也不是問題~

既然 container 的高度解決了,再將 container 裡面的 header、body、footer 加進來:

100vh-issue-final

一切看來都非常美好 😎

至於 resize 事件是否要加入 debounce 增進效能,那就看個人選擇了

CSS 的方法有分幾種:

.container {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
max-height: -webkit-fill-available; // 加上 fill-available
background: lavender;
}

但如同前綴 -webkit- 的字面意義,在 Android Chrome 上仍然有問題

但 Windows 及 macOS 上的 Chrome 卻沒事

ChromeSafari
macOSOO
WindowsONA
iOSOO
AndroidXNA

將 html 與 body 的高度設為 100%,底下的 container 就放心地用 100%

完全將在 mobile 上有爭議的 vh 單位移除了,是一個簡潔的方式

html,
body {
margin: 0;
padding: 0;
height: 100%; // 用 100%
width: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%; // 這裡也用 100%
background: lavender;
}

所有平台及裝置的瀏覽器都通過了考驗 💯

ChromeSafari
macOSOO
WindowsONA
iOSOO
AndroidONA

有一個人分享了利用新 CSS 單位的解法,也是一個不錯的選擇:

.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100dvh; // 用單位 dvh
background: lavender;
}

關於 dvh 的示意圖如下:

new unit

PS. 本圖來自 Google IO

但這個似乎還很新,看來支援度沒有太好

簡單測試了一下,在 Safari 可以正常運作,但是 Chrome 卻不認識這個單位

ChromeSafari
macOSXO
WindowsXNA
iOSOO
AndroidXNA

支援度可以參考:“dvh” | Can I use… Support tables for HTML5, CSS3, etc

之後想要來研究一下關於 dvh 這一類給 mobile 專用的單位

ChromeSafari
MacOSOO
WindowsONA
iOSXX
AndroidXNA
  • O:代表 100vh 與 window 高度一致
  • X:代表 100vh 與 window 高度不同
  • NA:此平台無此瀏覽器
ChromeSafari
iOSXO
AndroidONA
  • O:代表 window 高度會隨 scroll 變化
  • X:代表 window 高度固定
  • NA:此平台無此瀏覽器
OSChromeSafari
iOS15.15103.0.5060.6315.15
Android11103.0.5060.71NA

這次因為使用了滿版 layout 搭配 100vh,而發現了這個水很深的問題

iOS Safari 在往下滑動時,window 會不斷改變的現象,確實是大開眼界

總結下來,JS 的解法需要去監聽 resize 事件, 此外大概不可避免要搭配 debounce,會需要寫很多的程式碼, 還需要在適當的時機取消訂閱 resize 事件,避免記憶體洩漏問題, 簡單來說為了效能及資源,都要額外再做其他事

CSS 的三種解法都相對簡單,沒有太多的程式碼

但唯有第二個解法(html、body 設為 100%)通過了所有瀏覽器的考驗

第三個解法(新單位 dvh)雖然現在有相容性問題,但未來的可用性令人期待