[FE] Learn from react-wrap-balancer

閱讀時間約 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 變化重新去執行。

2會員
4內容數
FE Developer
留言0
查看全部
發表第一個留言支持創作者!
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
NCEES FE考試主要是美國工程類畢業生,大學畢業都會去考的考試,為未來成為PE做準備,只有通過考試的人才可以成為美國E.I.T.(Engineer in Traine),並通過四年的工作經歷認證,才可以考試成為PE。 <準備教材> 1.NCEES FE Reference Handbook
Thumbnail
兩年前剛出社會的時候也寫過面試心得文:2021 社會新鮮人軟體工程師求職心得分享,不知不覺兩年就過了,自己也不再是這個圈子裡最年輕的那批人,在新手的光環慢慢消失後,職涯的考驗似乎才真的開始...
嗨,我是小七! 本期是個人接觸八維後, 結合理論、各種參考資料和自己觀察的一些綜合成果。 這是談Fe/Fi的第一期,來講講不同的人格如何處理「自己的」情感。 不懂八維、Fe、Fi是什麼的小伙伴也不用擔心, 我會先用白話文大概提一下基本概念。 (=´∀`)人(´∀`=)
Thumbnail
講了這麼多,可能還是很難判斷八維功能。所以下面就提出一些各功能主要使用者,就我觀察中,可能(但不絕對)會實際表現出的具體言行的示例吧。 Fe 外傾情感 vs Fi 內傾情感
Thumbnail
【外傾情感Fe:一 個在男性身上很麻煩的功能】 Fe是個同理心高、適合傾聽、適合協調人際互動的功能。但在男人身上常常會變成一種負面困擾。
Thumbnail
那麼講到差異,外傾情感Fe,講簡單一點,就是比較注意重視人情世故和群體,而內傾情感Fi,則是比較注意重視真實感受和個體。
Thumbnail
走遍所有半路咖啡(𝐻𝑎𝑙𝑓𝑤𝑎𝑦 𝐶𝑜𝑓𝑓𝑒𝑒)分店的我,也可算是忠粉一名。每當朋友問到哪裡有好喝的咖啡,𝐻𝑎𝑙𝑓𝑤𝑎𝑦必定在我的名單上,外賣也好,堂食也好,ℎ𝑎𝑙𝑓𝑤𝑎𝑦總是能保持一貫的水準。香醇均勻、不酸,口感豐厚平順,剛好是我喜歡的口味。
Thumbnail
風花雪月二創的醍醐味就是無解的Time Loop。 《汝、死を招く者にして》的故事就從一張落在クロード房間地板上的神秘紙條開始。
Thumbnail
整本《Love Story》可以說是ディミトリ視角的クロード攻略本,雖然沒有高潮迭起的出生入死,但是不可言說的秘密、不經意洩出的想望,全都透過ディミトリ的質疑、クロード的玩笑,完整地表現出來。
Thumbnail
這樣說好了,迷因比電影還紅的最佳典範:「榮恩!我找到比魔杖還好用的東西了!」為本片打出了第一張好牌,原本以為這部片會成為Straight to DVD的那一種B級片,沒想到竟然上了院線,真是讓人又驚又喜。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
NCEES FE考試主要是美國工程類畢業生,大學畢業都會去考的考試,為未來成為PE做準備,只有通過考試的人才可以成為美國E.I.T.(Engineer in Traine),並通過四年的工作經歷認證,才可以考試成為PE。 <準備教材> 1.NCEES FE Reference Handbook
Thumbnail
兩年前剛出社會的時候也寫過面試心得文:2021 社會新鮮人軟體工程師求職心得分享,不知不覺兩年就過了,自己也不再是這個圈子裡最年輕的那批人,在新手的光環慢慢消失後,職涯的考驗似乎才真的開始...
嗨,我是小七! 本期是個人接觸八維後, 結合理論、各種參考資料和自己觀察的一些綜合成果。 這是談Fe/Fi的第一期,來講講不同的人格如何處理「自己的」情感。 不懂八維、Fe、Fi是什麼的小伙伴也不用擔心, 我會先用白話文大概提一下基本概念。 (=´∀`)人(´∀`=)
Thumbnail
講了這麼多,可能還是很難判斷八維功能。所以下面就提出一些各功能主要使用者,就我觀察中,可能(但不絕對)會實際表現出的具體言行的示例吧。 Fe 外傾情感 vs Fi 內傾情感
Thumbnail
【外傾情感Fe:一 個在男性身上很麻煩的功能】 Fe是個同理心高、適合傾聽、適合協調人際互動的功能。但在男人身上常常會變成一種負面困擾。
Thumbnail
那麼講到差異,外傾情感Fe,講簡單一點,就是比較注意重視人情世故和群體,而內傾情感Fi,則是比較注意重視真實感受和個體。
Thumbnail
走遍所有半路咖啡(𝐻𝑎𝑙𝑓𝑤𝑎𝑦 𝐶𝑜𝑓𝑓𝑒𝑒)分店的我,也可算是忠粉一名。每當朋友問到哪裡有好喝的咖啡,𝐻𝑎𝑙𝑓𝑤𝑎𝑦必定在我的名單上,外賣也好,堂食也好,ℎ𝑎𝑙𝑓𝑤𝑎𝑦總是能保持一貫的水準。香醇均勻、不酸,口感豐厚平順,剛好是我喜歡的口味。
Thumbnail
風花雪月二創的醍醐味就是無解的Time Loop。 《汝、死を招く者にして》的故事就從一張落在クロード房間地板上的神秘紙條開始。
Thumbnail
整本《Love Story》可以說是ディミトリ視角的クロード攻略本,雖然沒有高潮迭起的出生入死,但是不可言說的秘密、不經意洩出的想望,全都透過ディミトリ的質疑、クロード的玩笑,完整地表現出來。
Thumbnail
這樣說好了,迷因比電影還紅的最佳典範:「榮恩!我找到比魔杖還好用的東西了!」為本片打出了第一張好牌,原本以為這部片會成為Straight to DVD的那一種B級片,沒想到竟然上了院線,真是讓人又驚又喜。