[FE] Learn from react-wrap-balancer

Todd-avatar-img
發佈於FE
更新於 發佈於 閱讀時間約 6 分鐘
此套件主要是使字體擁有更好的閱讀體驗,它會將每一行的文字都有差不多的長度,達成更好的閱讀體驗。
https://react-wrap-balancer.vercel.app
raw-image

使用方式

import Balancer from "react-wrap-balancer";

<h1>
<Balancer>My Title</Balancer>
</h1>;

codesandbox

Text Wrap: balance

此套件如果瀏覽器可以使用到原生的 CSS text-wrap: balance  的話,就會使用 native 的方式,不支援才會透過計算來達成。

這是一種 CSS 原生的文字排版方式,用途一樣是為了讓文字更容易閱讀,身為開發者是無法預測最終呈現的文字是什麼(i18n, 文字的字體等),但是瀏覽器可以,所以透過瀏覽器的實作去計算成最容易閱讀的方式。這實作是需要耗費成本的,所以並不推薦將所有的文字都加上,並且瀏覽器也有限制最多只會影響到 6 行的文字。

h1,
h2,
h3,
h4,
h5,
h6,
blockquote {
text-wrap: balance;
}

另外此種方式與 white-space  是衝突的, balance  會造成換行,所以不應該一起使用。

套件中使用 JS 的 CSS.suports  來檢查是否支援使用此方式。

const isTextWrapBalanceSupported = `(self.CSS&&CSS.supports("text-wrap","balance")?1:2)`

useIsomorphicLayoutEffect

常見的 Server Side 和 Client Side 的 effect 實作方式,如果在 Server Side 的話,因為沒有 DOM 元素,所以如果使用 useLayoutEffect  會沒有實際的作用,因此改使用 useEffect 。

export const IS_SERVER = typeof window === "undefined";

export const useIsomorphicLayoutEffect = IS_SERVER
? React.useEffect
: React.useLayoutEffect;

useIdPollyfill

react 中可以使用 useId  來獲取唯一的 ID 值,不過在舊版並沒有此 hook,所以在套件中另外實作了此 hook 的 pollyfill。

let ID = 0

const genId = () => ++IDlet serverHandoffComplete = falsefunction useIdPolyfill() { const [id, setId] = React.useState(serverHandoffComplete ? genId : undefined) useIsomorphicLayoutEffect(() => { if (id === undefined) { setId(genId()) } serverHandoffComplete = true }, []) if (id === undefined) { return id } return `rwb-${id.toString(32)}`}

其中 genId 使用到了 js 的 closure 的概念,它獲取外部的 ID 變數並進行修改。

在使用時檢查開發者目前的 react 是否有支援 useId 的 hook,若沒有的話則使用實作的 useIdPollyfill 。

export function useId() {

const implementation = React.useMemo((): (() => string | number) => {
if ('useId' in React) return React.useId
return useIdPolyfill
}, [])
return implementation()
}

Polymorphic Component

這是一種由外部決定 Component 要使用什麼 HTML Tag 的方式,並透過 TypeScript 讓開發有更好的體驗,可以知道有哪些屬性可以傳遞。

codesandbox

eact 提供了 ComponentPropsWithoutRef 傳入 generic type 獲取該 Component 有哪些的 props 可以傳遞,聽夠過 Omit 移除已經定義的屬性。

interface BalancerOwnProps<

 ElementType extends React.ElementType = React.ElementType

> extends React.HTMLAttributes<HTMLElement> {

...

}



type BalancerProps<ElementType extends React.ElementType> =

 BalancerOwnProps<ElementType> &

   Omit<React.ComponentPropsWithoutRef<ElementType>, keyof BalancerOwnProps>

props nonce

因為此套件會動態的載入 <script> ,而在 CSP(Content Security Policy) 不允許有在 HTML 內的 <script> Tag,除非有設定 nonce 屬性。

const createScriptElement = (
injected: boolean,
nonce?: string,
suffix: string = ''
) => {
if (suffix) {
suffix = `self.${SYMBOL_NATIVE_KEY}!=1&&${suffix}`
}
return (
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
// Calculate the balance initially for SSR
__html:
(injected
? ''
: `self.${SYMBOL_NATIVE_KEY}=self.${SYMBOL_NATIVE_KEY}||${isTextWrapBalanceSupported};self.${SYMBOL_KEY}=${RELAYOUT_STR};`) +
suffix,
}}
nonce={nonce}
/>
)
}

suppressHydrationWarning 是指產生的內容和在 Server Side 產生的不相同的話不需要提示。

在此 Script 會執行

// self.${SYMBOL_NATIVE_KEY}=self.${SYMBOL_NATIVE_KEY}||${isTextWrapBalanceSupported}
self.__wrap_n = self.__wrap_n || isTextWrapBalanceSupported;
// self.${SYMBOL_KEY}=${RELAYOUT_STR};
self.__wrap_b = RELAYOUT_STR
// suffix: self.${SYMBOL_KEY}("${id}",${ratio})
relayout(id, ratio)

relayout

這是此套件計算方式的 function。

const relayout: RelayoutFn = (id, ratio, wrapper) => {

}

傳入的 id , ratio , wrapper ,其中 wrapper 指的是會包住我們內容的 Element,預設是 span 。

  wrapper =
wrapper || document.querySelector<WrapperElement>(`[data-br="${id}"]`)
const container = wrapper.parentElement

const update = (width: number) => (wrapper.style.maxWidth = width + 'px')

// Reset wrapper width
wrapper.style.maxWidth = ''

這一段若是沒有給 wrapper 的話會透過 querySelector 獲取,並重設 wrapper  element 的 maxWith 。

// Get the initial container size
const width = container.clientWidth
const height = container.clientHeight
// Synchronously do binary search and calculate the layout
let lower: number = width / 2 - 0.25
let upper: number = width + 0.5
let middle: number

if (width) {
// Ensure we don't search widths lower than when the text overflows
update(lower)
lower = Math.max(wrapper.scrollWidth, lower)

while (lower + 1 < upper) {
middle = Math.round((lower + upper) / 2)
update(middle)
if (container.clientHeight === height) {
upper = middle
} else {
lower = middle
}
}

// Update the wrapper width
update(upper * ratio + width * (1 - ratio))
}

這一段比較長,主要是使用 binary search 找到最適合的寬度,若是 lowerupper 差不止 1px 就會執行,如果父元素原本的高度(height)和後來的高度(container.clientHeight) 相同會將 upper 設為 middle ,因為在上方已經將 wrapper 的 maxWith 進行更改,所以可能會造成 wrapper 內的‘文字換行造成父元素的高度也改變,若沒有造成換行的話則將 lower 設為 middle 。

之後在依照我們給的 ratio 計算新的 maxWidth ,如果 ratio 為 1 就會使用 upper 的值,0 則使用原本的值。

 if (!wrapper['__wrap_o']) {
if (typeof ResizeObserver !== 'undefined') {
;(wrapper['__wrap_o'] = new ResizeObserver(() => {
self.__wrap_b(0, +wrapper.dataset.brr, wrapper)
})).observe(container)
} else {
// Silently ignore ResizeObserver for production builds
if (process.env.NODE_ENV === 'development') {
console.warn(
'The browser you are using does not support the ResizeObserver API. ' +
'Please consider add polyfill for this API to avoid potential layout shifts or upgrade your browser. ' +
'Read more: https://github.com/shuding/react-wrap-balancer#browser-support-information'
)
}
}
}

之後若沒有 ResizeObserver 的話,建一個新的去監聽 wrapper 的 size 變化重新去執行。

avatar-img
2會員
5內容數
FE Developer
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
你可能也想看
Google News 追蹤
Thumbnail
現代社會跟以前不同了,人人都有一支手機,只要打開就可以獲得各種資訊。過去想要辦卡或是開戶就要跑一趟銀行,然而如今科技快速發展之下,金融App無聲無息地進到你生活中。但同樣的,每一家銀行都有自己的App時,我們又該如何選擇呢?(本文係由國泰世華銀行邀約) 今天我會用不同角度帶大家看這款國泰世華CUB
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
鬧鐘還沒響,手機的震動先把女孩喚醒。她迷迷糊糊地拿起手機,一條訊息跳進眼簾: 「我們到這裡吧。對不起。」 短短的幾個字,讓她原本模糊的意識瞬間清醒。她的手指輕輕顫抖,試圖回撥那熟悉的號碼,但最終卻停在撥號鍵上,手垂下,淚意壓在眼眶深處。她放下手機,只發了一句:「為什麼?」然後關上手機屏
個性化外觀 全新桌布 提供9種配色,讓使用者可以打造獨一無二的桌面。 新樣式對話視窗 對話視窗採全新設計,對於小螢幕還是智慧型手機,擁有更好的支持。 輸入法 繁體中文輸入法預設採用新酷音 更強大檔案管理 改進開啟與存檔對話視窗 更直覺檔案導覽 內部磁碟全部列在檔案側邊欄,方便快
Thumbnail
本篇文章介紹了網頁元素的 Box 佈局,細述 Content Box、Padding Box、Border Box 和 Margin Box 的結構,並探討了 Intrinsic 與 Extrinsic Size 的特性。
Thumbnail
九月份「海光電影院」再次播映《神人之家》,並邀請盧盈良導演與台通主持人李毅誠參與映後座談。電影播映的前一天,盧盈良收到《神人之家》將在今年 11 月於法國上映的消息,他直言一路以來是緣分使然,電影之神為他與他的家人所帶來的變化,是一段非常神奇的旅程。以下文字整理映後座談部分內容,與各位讀者一同分享。
Thumbnail
在程式任何地方都能修改各種react組件狀態的做法分享
Thumbnail
Service Worker 是用於客戶端的攔截器,可以使用 Cache Storage 和 IndexDB,並有自己的生命週期。Web Worker 用於處理可能會使用到大量運算且不希望影響到使用者體驗的任務。Shared Worker 可以在相同 Domain 的不同頁面上共享訊息。
Thumbnail
前言 現在的前端需求已經越來越高,要考慮HTML及CSS的切版美觀程度,以及React以及Flutter所提出的元件(Componet、widget)觀念,也就是將元件模組化,使元件可以更動態的被程式運行,而不用靜態的客製化每一個介面。開發一個好的元件可以提升整體的開發速度,讓任何使用元件的開發者
Thumbnail
摘要四階段:整理主要內容>找出重點>寫摘要>確認
Thumbnail
現代社會跟以前不同了,人人都有一支手機,只要打開就可以獲得各種資訊。過去想要辦卡或是開戶就要跑一趟銀行,然而如今科技快速發展之下,金融App無聲無息地進到你生活中。但同樣的,每一家銀行都有自己的App時,我們又該如何選擇呢?(本文係由國泰世華銀行邀約) 今天我會用不同角度帶大家看這款國泰世華CUB
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
鬧鐘還沒響,手機的震動先把女孩喚醒。她迷迷糊糊地拿起手機,一條訊息跳進眼簾: 「我們到這裡吧。對不起。」 短短的幾個字,讓她原本模糊的意識瞬間清醒。她的手指輕輕顫抖,試圖回撥那熟悉的號碼,但最終卻停在撥號鍵上,手垂下,淚意壓在眼眶深處。她放下手機,只發了一句:「為什麼?」然後關上手機屏
個性化外觀 全新桌布 提供9種配色,讓使用者可以打造獨一無二的桌面。 新樣式對話視窗 對話視窗採全新設計,對於小螢幕還是智慧型手機,擁有更好的支持。 輸入法 繁體中文輸入法預設採用新酷音 更強大檔案管理 改進開啟與存檔對話視窗 更直覺檔案導覽 內部磁碟全部列在檔案側邊欄,方便快
Thumbnail
本篇文章介紹了網頁元素的 Box 佈局,細述 Content Box、Padding Box、Border Box 和 Margin Box 的結構,並探討了 Intrinsic 與 Extrinsic Size 的特性。
Thumbnail
九月份「海光電影院」再次播映《神人之家》,並邀請盧盈良導演與台通主持人李毅誠參與映後座談。電影播映的前一天,盧盈良收到《神人之家》將在今年 11 月於法國上映的消息,他直言一路以來是緣分使然,電影之神為他與他的家人所帶來的變化,是一段非常神奇的旅程。以下文字整理映後座談部分內容,與各位讀者一同分享。
Thumbnail
在程式任何地方都能修改各種react組件狀態的做法分享
Thumbnail
Service Worker 是用於客戶端的攔截器,可以使用 Cache Storage 和 IndexDB,並有自己的生命週期。Web Worker 用於處理可能會使用到大量運算且不希望影響到使用者體驗的任務。Shared Worker 可以在相同 Domain 的不同頁面上共享訊息。
Thumbnail
前言 現在的前端需求已經越來越高,要考慮HTML及CSS的切版美觀程度,以及React以及Flutter所提出的元件(Componet、widget)觀念,也就是將元件模組化,使元件可以更動態的被程式運行,而不用靜態的客製化每一個介面。開發一個好的元件可以提升整體的開發速度,讓任何使用元件的開發者
Thumbnail
摘要四階段:整理主要內容>找出重點>寫摘要>確認