自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

CSS-in-JS 的庫是如何工作的?

開發(fā) 前端
CSS-in-JS 的庫是如何工作的?是什么讓 Material UI 選擇了 CSS-in-JS 的方式開發(fā)組件庫?這不禁引起了筆者的好奇,于是決定探索一番,實現(xiàn)一個自己的 CSS-in-JS 庫。

前言

筆者近期學(xué)習(xí) Material UI 的過程中,發(fā)現(xiàn) Material UI 的組件都是使用 CSS-in-JS 的方式編寫的,聯(lián)想到之前在社區(qū)里看到過不少批判 CSS-in-JS 的文章,對此有些驚訝。

CSS-in-JS 的庫是如何工作的?是什么讓 Material UI 選擇了 CSS-in-JS 的方式開發(fā)組件庫?

這不禁引起了筆者的好奇,于是決定探索一番,實現(xiàn)一個自己的 CSS-in-JS 庫。

調(diào)研

目前社區(qū)中流行的 CSS-in-JS 庫主要有兩款:

  •  emotion
  •  styled-components

兩者的 API 基本一致,鑒于 emotion 的源碼中 JavaScript、Flow、TypeScript 三種代碼混在一起,閱讀起來實在是為難筆者,因此果斷放棄了學(xué)習(xí) emotion 的念頭。

那么就以 styled-components 為學(xué)習(xí)對象,看看它是如何工作的。

styled-components 核心能力

使用方式

打開 styled-components 官方文檔[1],點擊導(dǎo)航欄的 Documentation[2],找到 API Reference[3] 一欄,第一個展示的就是 styled-components 的核心 API——styled,用法相信了解 React 的同學(xué)或多或少接觸過:

const Button = styled.div`
background: palevioletred;
border-radius: 3px;
border: none;
color: white;
`;
const TomatoButton = styled(Button)`
background: tomato;
`;

通過在 React 組件中使用模板字符串編寫 CSS 的形式實現(xiàn)一個自帶樣式的 React 組件。

抽絲剝繭

clone styled-components 的 GitHub Repo[4] 到本地,安裝好依賴后用 VS Code 打開,會發(fā)現(xiàn) styled-components 是一個 monorepo,核心包與 Repo 名稱相同:

直接從 src/index.ts 開始查看源碼:

默認(rèn)導(dǎo)出的 styled API 是從 src/constructors/styled.tsx 導(dǎo)出的,那么繼續(xù)向上溯源。

src/constructors/styled.tsx 中的代碼非常簡單,去掉類型并精簡后的代碼如下:
import createStyledComponent from '../models/StyledComponent';
// HTML 標(biāo)簽列表
import domElements from '../utils/domElements';
import constructWithOptions from './constructWithOptions';
// 構(gòu)造基礎(chǔ)的 styled 方法
const baseStyled = (tag) =>
constructWithOptions(createStyledComponent, tag);
const styled = baseStyled;
// 實現(xiàn) HTML 標(biāo)簽的快捷調(diào)用方式
domElements.forEach(domElement => {
styled[domElement] = baseStyled(domElement);
});
export default styled;

從 styled API 的入口可以得知,上面使用方法[5]一節(jié)中,示例代碼:

const Button = styled.div`
background: palevioletred;
border-radius: 3px;
border: none;
color: white;
`;

實際上與以下代碼完全一致:

const Button = styled('div')`
background: palevioletred;
border-radius: 3px;
border: none;
color: white;
`;

styled API 為了方便使用封裝了一個快捷調(diào)用方式,能夠通過 styled[HTMLElement] 的方式快速創(chuàng)建基于 HTML 標(biāo)簽的組件。

接下來,繼續(xù)向上溯源,找到與 styled API 有關(guān)的 baseStyled 的創(chuàng)建方式:

const baseStyled = (tag) =>
constructWithOptions(createStyledComponent, tag);

找到 constructWithOptions 方法所在的 src/constructors/constructWithOptions.ts,去掉類型并精簡后的代碼如下:

import css from './css';
export default function constructWithOptions(componentConstructor, tag, options) {
const templateFunction = (initialStyles, ...interpolations) =>
componentConstructor(tag, options, css(initialStyles, ...interpolations));
return templateFunction;
}

精簡后的代碼變得異常簡單,baseStyled 是由 constructWithOptions 函數(shù)工廠創(chuàng)建并返回的。constructWithOptions 函數(shù)工廠的核心其實是 templateFunction 方法,其調(diào)用了組件的構(gòu)造方法 componentConstructor,返回了一個攜帶樣式的組件。

至此,即將進入 styled-components 的核心,一起抽絲剝繭一探究竟。

核心源碼

constructWithOptions 函數(shù)工廠調(diào)用的組件構(gòu)造方法 componentConstructor 是從外部傳入的,而這個組件構(gòu)造方法就是整個 styled-components 的核心所在。

在上面的源碼里,baseStyled 是將 createStyledComponent 這個組件構(gòu)造方法傳入 componentConstructor 后返回的 templateFunction,templateFunction 的參數(shù)就是通過模板字符串編寫的 CSS 樣式,最終會傳入組件構(gòu)造方法 createStyledComponent 中。

文字描述非?;靵y,畫個圖來梳理一下創(chuàng)建一個帶有樣式的組件的流程:

也就是說,當(dāng)用戶使用 styled API 創(chuàng)建帶有樣式的組件時,本質(zhì)上是在調(diào)用 createStyledComponent 這個組件構(gòu)造函數(shù)。

createStyledComponent 的源碼在 src/models/StyledComponent.ts 中,由于源碼較為復(fù)雜,詳細(xì)閱讀需要移步 GitHub:

styled-components/StyledComponent.ts at main · styled-components/styled-components[6]

從源碼可以得知,createStyledComponent 的返回值是一個帶有樣式的組件 WrappedStyledComponent,在返回這個組件之前對其做了一些處理,大部分都是設(shè)置組件上的一些屬性,可以在查看源碼的時候暫時跳過。

從返回值向上溯源,發(fā)現(xiàn) WrappedStyledComponent 是使用 React.forwardRef 創(chuàng)建的一個組件,這個組件調(diào)用了 useStyledComponentImpl 這個 Hook 并返回了 Hook 的返回值。

繼續(xù)從返回值向上溯源,發(fā)現(xiàn) useStyledComponentImpl Hook 中的大部分代碼都是與我們了解 styled-components 是如何工作這件事情無關(guān)的代碼,都是可以在查看源碼的時候暫時跳過的部分,但是有一個 Hook 的名稱讓筆者覺得就是整個 styled-components 最核心的部分:

const generatedClassName = useInjectedStyle(
componentStyle,
isStatic,
context,
process.env.NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined
);

在撰寫本文之前,筆者大致知道 CSS-in-JS 的庫是通過在運行時解析模板字符串并且動態(tài)創(chuàng)建 <style></style> 標(biāo)簽將樣式插入頁面中實現(xiàn)的,從 useInjectedStyle Hook 的名稱來看,其行為就是動態(tài)創(chuàng)建 <style></style> 標(biāo)簽并插入頁面中。

深入 useInjectedStyle,去掉類型并精簡后的代碼如下:

function useInjectedStyle(componentStyle, isStatic, resolvedAttrs, warnTooManyClasses) {
const styleSheet = useStyleSheet();
const stylis = useStylis();
const className = isStatic
? componentStyle.generateAndInjectStyles(EMPTY_OBJECT, styleSheet, stylis)
: componentStyle.generateAndInjectStyles(resolvedAttrs, styleSheet, stylis);
return className;
}

useInjectedStyle 調(diào)用了從參數(shù)傳入的 componentStyle 上的 generateAndInjectStyles 方法,將樣式傳入其中,返回了樣式對應(yīng)的 className。

進一步查看 componentStyle,是在 createStyledComponent 中被實例化并傳入 useInjectedStyle 的:

const componentStyle = new ComponentStyle(
rules,
styledComponentId,
isTargetStyledComp ? styledComponentTarget.componentStyle : undefined
);

因此 componentStyle 上的 generateAndInjectStyles 實際上是 ComponentStyle 這個類的實例方法,對應(yīng)的源碼比較長也比較復(fù)雜,詳細(xì)閱讀需要移步 GitHub,核心是對模板字符串進行解析,并將類名 hash 后返回 className:

styled-components/ComponentStyle.ts at main · styled-components/styled-components[7]

核心源碼至此就已經(jīng)解析完成了,其余部分的源碼主要是為了提供更多基礎(chǔ)功能以外的 API,提高可用度。

solid-sc 實現(xiàn)

解析完 styled-components 的核心源碼后,回歸到 CSS-in-JS 出現(xiàn)的原因上,最重要的因素可能是 JSX 的出現(xiàn),在前端領(lǐng)域里掀起了一股 All in JS 的浪潮,就此誕生了上文中提到的 CSS-in-JS 的庫。

那么,我們是否可以理解為 CSS-in-JS 與 JSX 屬于一個綁定關(guān)系?

無論這個問題的答案如何,至少能夠使用 JSX 語法的前端框架,都應(yīng)該能夠使用 CSS-in-JS 的技術(shù)方案。

近期筆者也在研究學(xué)習(xí) SolidJS,希望能夠參與到 SolidJS 的生態(tài)建設(shè)當(dāng)中,注意到 SolidJS 社區(qū)中暫時還沒有能夠?qū)?biāo) emotion/styled-components 的 CSS-in-JS 庫,雖然已經(jīng)有能夠滿足大部分需求的 Solid Styled Components 了:

https://github.com/solidjs/solid-styled-components[8]

但是只是會用不是筆者的追求,筆者更希望能夠嘗試自己實現(xiàn)一個,于是就有了 MVP 版本:

learning-styled-components/index.tsx at master · wjq990112/learning-styled-components[9]

不熟悉 SolidJS 的同學(xué)可以暫時將其當(dāng)做 React 來閱讀,核心思路上文其實已經(jīng)解析過了,本質(zhì)上就是將組件上攜帶的樣式在運行時進行解析,給一個獨一無二的 className,然后將其塞到 <style> 標(biāo)簽中。

MVP 版本中只有兩個函數(shù):

const createClassName = (rules: TemplateStringsArray) => {
return () => {
className++;
const style = document.createElement('style');
style.dataset.sc = '';
style.textContent = `.sc-${className}{${rules[0]}}`.trim();
document.head.appendChild(style);
return `sc-${className}`;
};
};
const createStyledComponent: StyledComponentFactories = (
tag: keyof JSX.IntrinsicElements | Component
) => {
return (rules: TemplateStringsArray) => {
const StyledComponent: ParentComponent = (props) => {
const className = createClassName(rules);
const [local, others] = splitProps(props, ['children']);
return (
<Dynamic component={tag} class={className()} {...others}>
{local.children}
</Dynamic>
);
};
return StyledComponent;
};
};

兩個函數(shù)的職責(zé)也非常簡單,一個用于創(chuàng)建 <style> 標(biāo)簽并給樣式生成一個唯一的 className,另一個用于創(chuàng)建經(jīng)過樣式包裹的動態(tài)組件。

當(dāng)然,MVP 版本要成為 SolidJS 社區(qū)中能夠?qū)?biāo) emotion/styled-components 的 CSS-in-JS 庫,還有非常多工作要做,比如:

  •  運行時解析不同方式傳入的 CSS 規(guī)則
  •  緩存相同組件的 className,相同組件復(fù)用樣式
  •  避免多次插入 <style> 標(biāo)簽,多個組件樣式復(fù)用同一個
  •  &etc.

不過,至少開了個好頭,MVP 版本能夠跑通 styled-components 文檔中 Getting Started[10] 部分的示例:

learning-styled-components - StackBlitz[11]

MVP 版本的源代碼在 https://github.com/wjq990112/learning-styled-components 的 master 分支,如果后續(xù)時間和精力允許,會在其他分支上完善,或者單獨開一個 Repo,不管怎么樣,先挖個坑。

最后

在研究 styled-components 的過程中,我發(fā)現(xiàn)社區(qū)中對 CSS-in-JS 的技術(shù)方案意見非常兩極分化,喜歡的同學(xué)非常喜歡,討厭的同學(xué)也非常討厭。

筆者本人也在思考,為什么 CSS-in-JS 會這么流行,以至于 Material UI 也選擇了這樣的技術(shù)方案。

筆者認(rèn)為可能有以下幾個原因:

  •  不需要 CSS 預(yù)處理器實現(xiàn)復(fù)雜的樣式組合
  •  不需要冗長的 CSS 規(guī)則約束 className
  •  構(gòu)建產(chǎn)物中沒有 CSS 文件,不需要單獨引入組件樣式

但 CSS-in-JS 的技術(shù)方案弊端也比較明顯,畢竟需要在運行時解析樣式并動態(tài)插入到頁面中,而且 JS bundle 體積也會增大,加載過程會阻塞頁面渲染。

展望

目前社區(qū)內(nèi)的樣式解決方案多以原子化 CSS 為主,原子化的形式能夠有效減小樣式體積,在編譯階段去掉沒有使用到的樣式,結(jié)合上文中提到的 CSS-in-JS 的技術(shù)方案的弊端,或許將 CSS-in-JS 運行時解析樣式的部分,放到編譯階段進行處理,生成由原子化的樣式組成的 CSS 文件,會是一個值得嘗試的優(yōu)化方向。

責(zé)任編輯:龐桂玉 來源: 大淘寶前端技術(shù)
相關(guān)推薦

2021-02-11 09:01:32

CSS開發(fā) SDK

2022-11-11 08:16:51

2022-11-28 08:50:13

2022-03-22 09:07:34

開發(fā)CSS技術(shù)

2023-11-01 08:36:07

CSSTailwind

2021-02-01 08:36:19

JS引擎V8

2023-01-31 16:43:31

?Node.js事件循環(huán)

2021-05-10 17:20:55

AIOps開發(fā)人員人工智能

2011-08-08 13:45:58

jQuery

2023-03-06 00:27:02

Kubernetesscheduler系統(tǒng)

2024-09-06 17:55:27

Springboot開發(fā)

2020-04-21 14:00:25

HTMLCSSJS

2021-02-23 14:56:12

數(shù)據(jù)庫存儲索引

2021-08-03 14:29:30

ARPANET互聯(lián)網(wǎng)協(xié)議TCP

2023-04-18 14:53:48

2023-04-18 15:09:50

2010-08-02 16:56:03

ICMP協(xié)議

2022-08-12 07:00:00

NFC安全性RFID

2020-09-11 08:41:50

域名系統(tǒng)DNS網(wǎng)絡(luò)

2022-02-11 10:27:28

面部識別算法人工智能
點贊
收藏

51CTO技術(shù)棧公眾號