您現在的位置是:首頁 > 網路遊戲首頁網路遊戲
尤大都推薦的元件庫是如何開發出來的?
- 2022-02-19
3d元素庫怎麼加韌體
「來源: |Vue中文社群 ID:vue_fe」
注意:為了讓篇幅儘可能簡潔一丟丟,在有些地方貼原始碼時,我儘可能貼最能反映要講解內容的原始碼,其他重複性的程式碼就略去了,所以如果你自己嘗試去閱讀原始碼時,可能會發現和文章裡的程式碼有出入。文章跑通 Naive UI 所用到的原始碼倉庫為:https://github。com/pftom/naive-app[1]
簡潔的抽象
前端開發者現在幾乎已經離不開 UI 元件庫了,典型的如 Ant Design、Material Design、以及最近 Vue 生態興起的 Naive UI 等,元件庫提供了簡單、靈活、易用的使用形式,如一個頁面中最常見的 Button 的使用如下:
>安妮薇時報
>
上述幾行簡單的程式碼就可以完成如下有意思的效果:
甚至是,可以一鍵切換面板,如 Dark Mode:
當然還可以處理事件、新增 Icon、處理 Loading 等,透過簡單給定一些 Props,我們就可以擁有一個好看、實用的 Button,相比原始的 HTML 標籤來說,實在是不可同日而語。。。
冰山理論
元件庫在帶來靈活、方便的同時,其內部的原理卻並非如它使用般簡單,就像上述的冰山圖一樣引人深思。
讓我們翻一翻最近的 Vue 元件庫新秀 Naive UI 的 CHANGELOG,就可以窺見編寫一個入門的元件庫大致需要多少時間:
可以看到,2020-03-21 就釋出了 1。x 版本,而在 1。x 之前又是漫長的思考、設計與開發,至今應該差不多兩年有餘。
而為了跑通一個 Naive UI 的 Button,大致需要如下的檔案或程式碼:
。
|_____utils
| |____color
| | |____index。js
| |____vue
| | |____index。js
| | |____flatten。js
| | |____call。js
| | |____get-slot。js
| |____index。js
| |____naive
| | |____warn。js
| | |____index。js
| |____cssr
| | |____create-key。js
| | |____index。js
|_____internal
| |____loading
| | |____index。js
| | |____src
| | | |____Loading。jsx
| | | |____styles
| | | | |____index。cssr。js
| |____index。js
| |____icon-switch-transition
| | |____index。js
| | |____src
| | | |____IconSwitchTransition。jsx
| |____fade-in-expand-transition
| | |____index。js
| | |____src
| | | |____FadeInExpandTransition。jsx
| |____wave
| | |____index。js
| | |____src
| | | |____Wave。jsx
| | | |____styles
| | | | |____index。cssr。js
| |____icon
| | |____index。js
| | |____src
| | | |____Icon。jsx
| | | |____styles
| | | | |____index。cssr。js
|_____styles
| |____common
| | |_____common。js
| | |____light。js
| | |____index。js
| |____transitions
| | |____fade-in-width-expand。cssr。js
| | |____icon-switch。cssr。js
| |____global
| | |____index。cssr。js
|____config-provider
| |____src
| | |____ConfigProvider。js
|____button
| |____styles
| | |_____common。js
| | |____light。js
| | |____index。js
| |____src
| | |____Button。jsx
| | |____styles
| | | |____button。cssr。js
|____assets
| |____logo。png
|_____mixins
| |____use-style。js
| |____use-theme。js
| |____index。js
| |____use-form-item。js
| |____use-config。js
看似困難的背後
雖然跑通一個看似簡單的 背後需要大量的工作,涉及到幾十個檔案的依賴,但對於一個元件庫來說,複雜度是量級近似的,即從一個簡單的 到一個複雜的
,其實在元件庫的領域內,90% 的內容是相似的,所以如果搞懂了 的執行流程,那麼基本可以說搞懂了元件庫近 90% 的內容,剩下的 10% 則是具體元件的具體實現。所以瞭解一個前端元件庫最核心還是需要弄懂一個 跑通背後所需要的各種準備工作,也就是上圖中的第一根高柱,而開發一個元件庫首先也應該專注於設計讓至少一個 Button 跑通的方案。
Button 背後的技術鏈
我們以 Naive UI [2]為研究物件,來詳細剖析其 實現背後的各種原理,原因有比較直觀的 2 點:
其技術棧以 Vite 、Vue3、TypeScript 為主,符合筆者最近的技術棧
相比其他元件庫而言,其在成熟度、知名度和程式碼優秀層面都處於一個相對摺中的水平,不太複雜但又涉及相對比較多的知識,比較適合學習和研究其原理
從模板出發
想了解一個元件,第一件事情當然是瞭解它的骨架了,也就是我們常說的 HTML/JSX 相關內容了,首先看一下 Naive UI 的 Button 元件的模板:
const Button = defineComponent({
name: ‘Button’,
props: {},
setup(props) {},
render() {
// 第一部分
// n
const { $slots, mergedClsPrefix, tag: Component } = this;
const children = flatten(getSlot(this));
return (
ref=“selfRef” // 第二部分 class={[ `${mergedClsPrefix}-button`, `${mergedClsPrefix}-button——${this。type}-type`, { [`${mergedClsPrefix}-button——disabled`]: this。disabled, [`${mergedClsPrefix}-button——block`]: this。block, [`${mergedClsPrefix}-button——pressed`]: this。enterPressed, [`${mergedClsPrefix}-button——dashed`]: !this。text && this。dashed, [`${mergedClsPrefix}-button——color`]: this。color, [`${mergedClsPrefix}-button——ghost`]: this。ghost, // required for button group border collapse }, ]} tabindex={this。mergedFocusable ? 0 : -1} type={this。attrType} style={this。cssVars} disabled={this。disabled} onClick={this。handleClick} onBlur={this。handleBlur} onMousedown={this。handleMouseDown} onKeyup={this。handleKeyUp} onKeydown={this。handleKeyDown} > // 第三部分 {$slots。default && this。iconPlacement === “right” ? (
) : null}
// 第四部分
{{
default: () =>
$slots。icon || this。loading ? (
class={`${mergedClsPrefix}-button__icon`} style={{ margin: !$slots。default ? 0 : “”, }} > {{ default: () => this。loading ? ( clsPrefix={mergedClsPrefix} key=“loading” class={`${mergedClsPrefix}-icon-slot`} strokeWidth={20} /> ) : ( key=“icon” class={`${mergedClsPrefix}-icon-slot`} role=“none” > {renderSlot($slots, “icon”)} ), }}
) : null,
}}
// 第三部分
{$slots。default && this。iconPlacement === “left” ? (
) : null}
// 第五部分
{!this。text ? (
) : null}
// 第六部分
{this。showBorder ? (
aria-hidden
class={`${mergedClsPrefix}-button__border`}
style={this。customColorCssVars}
/>
) : null}
// 第六部分
{this。showBorder ? (
aria-hidden
class={`${mergedClsPrefix}-button__state-border`}
style={this。customColorCssVars}
/>
) : null}
)
}
});
可以看到,上述的主要展示出了 元件的模板部分,基於 Vue3 的 defineComponent 來定義元件,基於 render 方法使用 JSX 的形式來編寫模板,其中模板部分又主要分為 6 部分,在程式碼中以註釋的方式標註出:
主要是取屬性相關,主要有三個屬性:$slots 、mergedClsPrefix 、tag ,其中 $slots 在 Vue 領域內類似孩子節點所屬的物件,mergedClsPrefix 則為整個元件庫的名稱空間字首,在 Naive UI 中這個字首為 n ,tag 則表示此元件應該以什麼樣的標籤進行展示,預設是 ,你也可以換成 ,讓按鈕長得像一個連結
主要是定義 Button 相關的屬性:
其中 class 則根據傳進來的屬性來判定屬於哪種 type:primary 、info 、warning 、success 、error ,以及當前處於什麼狀態:disabled 、block 、pressed 、dashed 、color 、ghost ,根據這些 type 和狀態給予合適的類名,從而為元件定義對應類名所屬的 CSS 樣式
tabIndex 則表示在使用 tab 鍵時,此按鈕是否會被選中,0 表示可被選中,-1 表示不可選中 ;
type 則表示為 button 、 submit 、reset 等按鈕型別,使得按鈕可以被整合進
元件來完成更加複雜的操作,如表單提交的觸發等;style 則是為此元件傳入所需的 CSS Variables,即 CSS變數,而在 setup 函式時,會透過 useTheme (後續會談到)鉤子去掛載 Button 相關的樣式,這些樣式中大量使用 CSS Variables 來自定義元件各種 CSS 屬性,以及處理全域性的主題切換,如 Dark Mode 等
disabled 則是控制此按鈕是否可操作,true 代表被禁用,不可操作,false 代表可操作為預設值
剩下的則是相關的事件處理函式:click 、blur 、mouseup 、keyup 、keydown 等
主要是決定在 iconPlacement 為 left 、right 時,元件孩子節點的展示形式,即圖示在左和右時,孩子節點分佈以 或
標籤的形式展示,當為 right 時,設定為 則是為了更好的處理佈局與定位為圖示相關內容,NFadeInExpandTransition 為控制 Icon 出現和消失的過渡動畫,NIconSwitchTransition 則是控制 loading 形式的 Icon 和其他 Icon 的切換過渡動畫
當按鈕不以 text 節點的形式展示時,其上應該有處理反饋的波紋,透過上述影片也可以看到在點按鈕時會有對應的波紋效果來給出點選反饋,如下圖展示為類文字形式,在點選時就不能出現波紋擴散效果
主要是透過
去模擬元件的邊框:border 和 state-border ,前者主要靜態、預設的處理邊框顏色、寬度等,後者則是處理在不同狀態下:focus 、hover 、active 、pressed 等下的 border 樣式可以透過一個實際的例子看一下這兩者所起的作用:
。n-button 。n-button__border {
border: var(——border);
}
。n-button 。n-button__border, 。n-button 。n-button__state-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: inherit;
transition: border-color 。3s var(——bezier);
pointer-events: none;
}
。n-button:not(。n-button——disabled):hover 。n-button__state-border {
border: var(——border-hover);
}
。n-button:not(。n-button——disabled):pressed 。n-button__state-border {
border: var(——border-pressed);
}
style attribute {
——bezier: ;
——bezier-ease-out: ;
——border: 1px ;
——border-hover: 1px ;
——border-pressed: 1px ;
——border-focus: 1px ;
——border-disabled: 1px ;
}
可以看到 state-border 主要是處理一些會動態變化的效果,如在 hover 、pressed 等狀態下的邊框展示效果,而 border 則負責初始預設的效果。
瞭解了主要模板相關的內容之後,你可能對在講解整個模板中出現頻度最高的一個內容表示疑惑,即:
${mergedClsPrefix}-button
${mergedClsPrefix}-button——${``this``。type}-type
${mergedClsPrefix}-button__content
${mergedClsPrefix}-button——disabled
為什麼會有這麼奇怪的 CSS 類寫法?以及在給元件賦值屬性時:
style``={``this``。cssVars}
一個典型的例子為:
const cssVars = {
// default type
color: “#0000”,
colorHover: “#0000”,
colorPressed: “#0000”,
colorFocus: “#0000”,
colorDisabled: “#0000”,
textColor: textColor2,
}
為什麼需要賦值一堆的 CSS Variables?
如果你對這幾個問題疑惑不解,並想探求其背後的原理,那麼此時你應該舒一口氣,然後保持專注繼續閱讀文章下一部分內容:樣式的組織藝術。
樣式的組織藝術
在元件庫這個領域,絕大部分時間都花在如何更好的、更加自定義的組織整體的樣式系統。
而 Naive UI 這個框架有個有意思的特性,它不使用任何預處理、後處理樣式語言如 Less/Sass/PostCSS 等,而是自造了為框架而生、且框架無關、帶 SSR 特性的類 CSS in JS 的方案:css-render[3],並給予這個方案設計了一套外掛系統,目前主要有兩個外掛:
vue3-ssr[4]
plugin-bem[5]
本文中主要專注於 CSR 方面的講解,所以只會關注 plugin-bem 相關的內容。
css-render 目前的基本使用場景為搭配 plugin-bem 外掛使用,編寫基於 BEM 風格的、易於組織的類 CSS in JS 程式碼,至於這裡為什麼說是 “類” CSS in JS 解決方案,後續會進行講解。
當我們安裝了對應的包之後:
$ npm install ——save-dev css-render @css-render/plugin-bem
可以按照如下形式來使用:
import CssRender from ‘css-render’
import bem from ‘@css-render/plugin-bem’
const cssr = CssRender()
const plugin = bem({
blockPrefix: ‘。ggl-’
})
cssr。use(plugin) // 為 cssr 註冊 bem 外掛
const { cB, cE, cM } = plugin
const style = cB(
‘container’,
[
cE(
‘left, right’,
{
width: ‘50%’
}
),
cM(
‘dark’,
[
cE(
‘left, right’,
{
backgroundColor: ‘black’
}
)
]
)
]
)
// 檢視渲染的 CSS 樣式字串
console。log(style。render())
// 將樣式掛載到 head 標籤裡面,可以提供 options
style。mount(/* options */)
// 刪除掛載的樣式
style。unmount(/* options */)
上述的 Log 的效果如下:
。ggl-container 。ggl-container__left,
。ggl-container 。ggl-container__right {
width: 50%;
}
。ggl-container。ggl-container——dark 。ggl-container__left,
。ggl-container。ggl-container——dark 。ggl-container__right{
background-color: black;
}
可以看到上述程式碼主要使用了 cB 、cE 、 cM 函式來進行各種標籤、樣式的巢狀組合,來達到定義規範 CSS 類和對應樣式的效果,為了更近一步講解這個庫的作用以及它在 Naive UI 中所達到的效果,我們有必要先了解一下什麼是 BEM。
什麼是 BEM?
B(Block)、E(Element)、M(Modifier),即塊、元素與修飾符,是一種廣泛使用的對 HTML/CSS 裡面使用到的類命名規範:
/* 塊 */
。btn {}
/* 依賴塊的元素 */
。btn__price {}
。btn__text {}
/* 修改塊狀態的修飾符 */
。btn——orange {}
。btn——big {}
上述中塊(Block),即 btn ,代表一個抽象的最頂級的新元件,即塊裡面不能包含塊,也被視為一棵樹中的父節點,使用 。btn 表示
元素(Element),即 price 、text ,代表從屬於某個塊,是這個塊的子元素,跟在塊後面,以雙下劃線為間隔,使用 。btn__price 、。btn__text 表示
修飾符(Modifier),即 orange 、big ,用於修改塊的狀態,為塊新增特定的主題或樣式,跟在塊後面,以雙連字元為間隔,使用 。btn——orange 、。btn——big 表示
上述的 CSS 形式反映到 HTML 裡面,會得到如下結構:
¥9。99
訂購
使用這種 BEM 形式的類命名風格基本有如下幾種優點:
可以表示幾乎所有的元素及其從屬關係,且關係明確、語義明確
且即使其他領域的開發者,如客戶端開發,或者設計師們,不瞭解 CSS 語言,也能從這種命名風格里面瞭解元素、元素的層級所屬關係和狀態
搭建了類似的命名結構之後,之後只需要變動少許的類名就可以獲得不同風格的元素,如按鈕:
/* Block */
。btn {
text-decoration: none;
background-color: white;
color: #888;
border-radius: 5px;
display: inline-block;
margin: 10px;
font-size: 18px;
text-transform: uppercase;
font-weight: 600;
padding: 10px 5px;
}
/* Element */
。btn__price {
background-color: white;
color: #fff;
padding-right: 12px;
padding-left: 12px;
margin-right: -10px; /* realign button text padding */
font-weight: 600;
background-color: #333;
opacity: 。4;
border-radius: 5px 0 0 5px;
}
/* Element */
。btn__text {
padding: 0 10px;
border-radius: 0 5px 5px 0;
}
/* Modifier */
。btn——big {
font-size: 28px;
padding: 10px;
font-weight: 400;
}
/* Modifier */
。btn——blue {
border-color: #0074D9;
color: white;
background-color: #0074D9;
}
/* Modifier */
。btn——orange {
border-color: #FF4136;
color: white;
background-color: #FF4136;
}
/* Modifier */
。btn——green {
border-color: #3D9970;
color: white;
background-color: #3D9970;
}
body {
font-family: “fira-sans-2”, sans-serif;
background-color: #ccc;
}
上述只需要修改修飾符 orange 、green 、blue 、big 等,就可以獲得不同的效果:
CSS Render 是如何運作的?
CSS Render 本質上是一個 CSS 生成器,然後提供了 mount 和 unmount API,用於將生成的 CSS 字串掛載到 HTML 模板裡和從 HTML 裡面刪除此 CSS 樣式標籤,它藉助 BEM 命名規範外掛和 CSS Variables 來實現 Sass/Less/CSS-in-JS 形式的方案,可以減少整體 CSS 的重複邏輯和包大小。
瞭解了 BEM 和上述關於 CSS Render 的介紹之後,我們再來回顧一下以下的程式碼:
import CssRender from ‘css-render’
import bem from ‘@css-render/plugin-bem’
const cssr = CssRender()
const plugin = bem({
blockPrefix: ‘。ggl-’
})
cssr。use(plugin) // 為 cssr 註冊 bem 外掛
const { cB, cE, cM } = plugin
const style = cB(
‘container’,
[
cE(
‘left, right’,
{
width: ‘50%’
}
),
cM(
‘dark’,
[
cE(
‘left, right’,
{
backgroundColor: ‘black’
}
)
]
)
]
)
// 檢視渲染的 CSS 樣式字串
console。log(style。render())
// 將樣式掛載到 head 標籤裡面,可以提供 options
style。mount(/* options */)
// 刪除掛載的樣式
style。unmount(/* options */)
上述程式碼主要做了如下工作:
初始化 CSS Render 例項,然後初始化 BEM 外掛例項,併為整體樣式類加上 。ggl- 字首
從 BEM 外掛裡面匯出相關的 cB 、cE 、cM 方法,然後基於這三個方法遵從 BEM 的概念進行樣式類的排列、巢狀、組合來形成我們最終的樣式類和對應的樣式
首先是 cB ,定義某個頂層塊元素為 container
然後是此塊包含兩個子元素,分別是 cE ,代表從屬於父塊的子元素 left 和 right,對應關於 width 的樣式 ;以及 cM ,對父塊進行修飾的修飾符 dark
dark 修飾符又包含一個子元素,屬於 cE ,代表從屬於此修飾符所修飾塊、包含子元素 left 和 right ,對應關於 backgroundColor 的樣式
瞭解了上述的層級巢狀關係之後,我們就可以寫出上述 style 進行 render 之後的效果:
// 。ggl- 字首,以及 cB(‘container’, [cE(‘left, right’, { width: ‘50%’ } )])
。ggl-container 。ggl-container__left,
。ggl-container 。ggl-container__right {
width: 50%;
}
// 。ggl- 字首,以及 cB(‘container’, [cM(‘dark’, [cE(‘left, right’, { backgroundColor: ‘black’ } )])])
。ggl-container。ggl-container——dark 。ggl-container——left,
。ggl-container。ggl-container——dark 。ggl-container__right {
background-color: black;
}
可以看到 cM 定義的修飾符,其實是直接修飾塊,也就是在類生成上會是 。ggl-container。ggl-container——dark 與父塊的類直接寫在一起,屬於修飾關係,而不是從屬關係。
Naive UI 的樣式組織
Naive UI 在樣式組織上主要遵循如下邏輯,依然以 Button 為例:
掛載 CSS Variables,這裡存在預設的變數和使用者傳進來自定義的變數,將 cssVars 傳給標籤的 style 欄位來掛載
掛載 Button 相關基礎樣式、主題(theme)相關的樣式,生成 CSS 類名
掛載全域性預設樣式(這一步在最後,確保全域性預設樣式不會被覆蓋)
透過上面三步走的方式,就可以定義好 Button 相關的所有類、樣式,並透過 CSS Variables 支援主題定製、主題過載等功能。
上述三步走主要是在 setup 函數里面呼叫 useTheme 鉤子,處理 Button 相關樣式掛載和全域性預設樣式掛載,然後處理 CSS Variables 定義和使用:
const Button = defineComponent({
name: “Button”,
props: buttonProps,
setup(props) {
const themeRef = useTheme(
“Button”,
“Button”,
style,
buttonLight,
props,
mergedClsPrefixRef
);
return {
// 定義邊框顏色相關
customColorCssVars: computed(() => {}),
// 定義 字型、邊框、顏色、大小相關
cssVars: computed(() => {}),
}
}
render() {
// 定義 button 相關的 CSS 變數
// 定義邊框顏色獨有的 CSS 變數
}
});
掛載 Buttn 相關樣式
Button 相關樣式掛載與全域性樣式掛載相關的內容存在於 Button 元件的 setup 方法裡面的 useTheme Hooks,useTheme 是一個如下結構的鉤子函式:
/* 全域性 CSS Variables 的型別 */
type ThemeCommonVars = typeof { primaryHover: ‘#36ad6a’, errorHover: ‘#de576d’, 。。。 }
// Button 獨有的 CSS Variable 型別
type ButtonThemeVars = ThemeCommonVars & { /* Button 相關的 CSS Variables 的型別 */ }
// Theme 的型別
interface Theme
// 主題名
name: N
// 主題一些通用的 CSS Variables
common?: ThemeCommonVars
// 相關依賴元件的一些 CSS Variables,如 Form 裡面依賴 Button,對應的 Button
// 需要包含的 CSS Variables 要有限制
peers?: R
// 主題自身的一些個性化的 CSS Variables
self?: (vars: ThemeCommonVars) => T
}
// Button Theme 的型別
type ButtonTheme = Theme<‘Button’, ButtonThemeVars >
interface GlobalThemeWithoutCommon {
Button?: ButtonTheme
Icon?: IconTheme
}
// useTheme 方法傳入 props 的型別
type UseThemeProps
// 主題相關變數,如 darkTheme
theme?: T | undefined
// 主題中可以被過載的變數
themeOverrides?: ExtractThemeOverrides
// 內建主題中可以被過載的變數
builtinThemeOverrides?: ExtractThemeOverrides
}>
// 最終合併的 Theme 的型別
type MergedTheme
? {
common: ThemeCommonVars
self: V
// 相關依賴元件的一些 CSS Variables,如 Form 裡面依賴 Button,對應的 Button
// 需要包含的 CSS Variables 要有限制
peers: W
// 相關依賴元件的一些 CSS Variables,如 Form 裡面依賴 Button,對應的 Button
// 需要包含的 CSS Variables 要有限制,這些 CSS Variables 中可以被過載的變數
peerOverrides: ExtractMergedPeerOverrides
}
: T
useTheme
resolveId: keyof GlobalThemeWithoutCommon,
mountId: string,
style: CNode | undefined,
defaultTheme: Theme
props: UseThemeProps
// n
clsPrefixRef?: Ref
) => ComputedRef
可以看到,useTheme 主要接收 6 個引數:
resolveId 用於定位在全域性樣式主題中的鍵值,這裡是 ‘Button’
mountId 樣式掛載到 head 標籤時,style 的 id
style 元件的 CSS Render 形式生成的樣式標籤、樣式的字串,也就是 Button 相關的類、類與樣式的對應的骨架,裡面是一系列待使用的 CSS Variables
defaultTheme 為 Button 的預設主題相關的 CSS Variables
props 為使用者使用元件時可自定義傳入的屬性,用於覆蓋預設的樣式變數
clsPrefixRef 為整體的樣式類字首,在 Naive UI 中,這個為 n
useTheme 返回一個合併了內建樣式、全域性定義的關於 Button 相關的樣式、使用者自定義樣式三者的樣式合集 ComputedRef
瞭解了 useTheme 鉤子函式的輸入與輸出之後,可以繼續來看一下其函式主體邏輯:
function useTheme(
resolveId,
mountId,
style,
defaultTheme,
props,
clsPrefixRef
) {
if (style) {
const mountStyle = () => {
const clsPrefix = clsPrefixRef?。value;
style。mount({
id: clsPrefix === undefined ? mountId : clsPrefix + mountId,
head: true,
props: {
bPrefix: clsPrefix ? `。${clsPrefix}-` : undefined,
},
});
globalStyle。mount({
id: “naive-ui/global”,
head: true,
});
};
onBeforeMount(mountStyle);
}
const NConfigProvider = inject(configProviderInjectionKey, null);
const mergedThemeRef = computed(() => {
const {
theme: { common: selfCommon, self, peers = {} } = {},
themeOverrides: selfOverrides = {},
builtinThemeOverrides: builtinOverrides = {},
} = props;
const { common: selfCommonOverrides, peers: peersOverrides } =
selfOverrides;
const {
common: globalCommon = undefined,
common: globalSelfCommon = undefined,
self: globalSelf = undefined,
peers: globalPeers = {},
} = {},
} = NConfigProvider?。mergedThemeRef。value || {};
const {
common: globalCommonOverrides = undefined,
= {},
} = NConfigProvider?。mergedThemeOverridesRef。value || {};
const {
common: globalSelfCommonOverrides,
peers: globalPeersOverrides = {},
} = globalSelfOverrides;
const mergedCommon = merge(
{},
selfCommon || globalSelfCommon || globalCommon || defaultTheme。common,
globalCommonOverrides,
globalSelfCommonOverrides,
selfCommonOverrides
);
const mergedSelf = merge(
// {}, executed every time, no need for empty obj
(self || globalSelf || defaultTheme。self)?。(mergedCommon),
builtinOverrides,
globalSelfOverrides,
selfOverrides
);
return {
common: mergedCommon,
self: mergedSelf,
peers: merge({}, defaultTheme。peers, globalPeers, peers),
peerOverrides: merge({}, globalPeersOverrides, peersOverrides),
};
});
return mergedThemeRef;
}
可以看到 useTheme 主體邏輯包含兩個部分:
第一部分為掛載 button 相關的樣式到 clsPrefix + mountId ,包含 button 相關的樣式類骨架,以及掛載全域性通用樣式到 naive-ui/global ,並且這個樣式的掛載過程是在 onBeforeMount 鉤子呼叫時,對應到之前講解的樣式掛載順序就可以理清楚了:
順序為 setup 裡面返回 CSS Variables,然後透過標籤的 style 註冊 CSS Variables
然後掛載 Button 相關的的樣式骨架
然後掛載全域性通用的樣式骨架,確保 Button 相關的樣式骨架不會覆蓋全域性通用的樣式
第二部分為將使用者自定義的主題、內部配置的主題進行整合生成新的主題變數集
使用者自定義的主題 props :包含 theme 、themeOverrides 、builtinThemeOverrides
內部配置的主題 NConfigProvider?。mergedThemeRef。value 與 NConfigProvider?。mergedThemeOverridesRef。value
接下來著重講解關於這兩部的具體程式碼和相關變數的含義。
第一部分中的 button 相關的樣式如下:
import { c, cB, cE, cM, cNotM } from “。。/。。/。。/_utils/cssr”;
import fadeInWidthExpandTransition from “。。/。。/。。/_styles/transitions/fade-in-width-expand。cssr”;
import iconSwitchTransition from “。。/。。/。。/_styles/transitions/icon-switch。cssr”;
export default c([
cB(
“button”,
`
font-weight: var(——font-weight);
line-height: 1;
font-family: inherit;
padding: var(——padding);
// 。。。。 更多的定義
`, [
// border ,邊框相關的樣式類骨架
cM(“color”, [
cE(“border”, {
borderColor: “var(——border-color)”,
}),
cM(“disabled”, [
cE(“border”, {
borderColor: “var(——border-color-disabled)”,
}),
]),
cNotM(“disabled”, [
c(“&:focus”, [
cE(“state-border”, {
borderColor: “var(——border-color-focus)”,
}),
]),
c(“&:hover”, [
cE(“state-border”, {
borderColor: “var(——border-color-hover)”,
}),
]),
c(“&:active”, [
cE(“state-border”, {
borderColor: “var(——border-color-pressed)”,
}),
]),
cM(“pressed”, [
cE(“state-border”, {
borderColor: “var(——border-color-pressed)”,
}),
]),
]),
]),
// icon 相關的樣式類骨架
cE(
“icon”,
`
margin: var(——icon-margin);
margin-left: 0;
height: var(——icon-size);
width: var(——icon-size);
max-width: var(——icon-size);
font-size: var(——icon-size);
position: relative;
flex-shrink: 0;
`,
[
cB(
“icon-slot”,
`
height: var(——icon-size);
width: var(——icon-size);
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
`,
[
iconSwitchTransition({
top: “50%”,
originalTransform: “translateY(-50%)”,
}),
]
),
fadeInWidthExpandTransition(),
]
),
// content 子元素內容相關的樣式類骨架
cE(
“content”,
`
display: flex;
align-items: center;
flex-wrap: nowrap;
`,
[
c(“~”, [
cE(“icon”, {
margin: “var(——icon-margin)”,
marginRight: 0,
}),
]),
]
),
// 更多的關於 backgroundColor、base-wave 點選反饋波紋,icon,content,block 相關的樣式定義
],
// 動畫、過渡相關的樣式類骨架
c(“@keyframes button-wave-spread”, {
from: {
boxShadow: “0 0 0。5px 0 var(——ripple-color)”,
},
to: {
// don‘t use exact 5px since chrome will display the animation with glitches
boxShadow: “0 0 0。5px 4。5px var(——ripple-color)”,
},
}),
c(“@keyframes button-wave-opacity”, {
from: {
opacity: “var(——wave-opacity)”,
},
to: {
opacity: 0,
},
}),
]);
上面的 CSS Render 相關的程式碼最終會產出型別下面的內容:
。n-button {
font-weight: var(——font-weight);
line-height: 1;
font-family: inherit;
padding: var(——padding);
transition:
color 。3s var(——bezier),
background-color 。3s var(——bezier),
opacity 。3s var(——bezier),
border-color 。3s var(——bezier);
}
。n-button。n-button——color 。n-button__border {
border-color: var(——border-color);
}
。n-button。n-button——color。n-button——disabled 。n-button__border {
border-color: var(——border-color-disabled);
}
。n-button。n-button——color:not(。n-button——disabled):focus 。n-button__state-border {
border-color: var(——border-color-focus);
}
。n-button 。n-base-wave {
pointer-events: none;
top: 0;
right: 0;
bottom: 0;
left: 0;
animation-iteration-count: 1;
animation-duration: var(——ripple-duration);
animation-timing-function: var(——bezier-ease-out), var(——bezier-ease-out);
}
。n-button 。n-base-wave。n-base-wave——active {
z-index: 1;
animation-name: button-wave-spread, button-wave-opacity;
}
。n-button 。n-button__border, 。n-button 。n-button__state-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: inherit;
transition: border-color 。3s var(——bezier);
pointer-events: none;
}
。n-button 。n-button__icon {
margin: var(——icon-margin);
margin-left: 0;
height: var(——icon-size);
width: var(——icon-size);
max-width: var(——icon-size);
font-size: var(——icon-size);
position: relative;
flex-shrink: 0;
}
。n-button 。n-button__icon 。n-icon-slot {
height: var(——icon-size);
width: var(——icon-size);
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
}
。n-button 。n-button__icon。fade-in-width-expand-transition-enter-active {
overflow: hidden;
transition:
opacity 。2s cubic-bezier(。4, 0, 。2, 1) 。1s,
max-width 。2s cubic-bezier(。4, 0, 。2, 1),
margin-left 。2s cubic-bezier(。4, 0, 。2, 1),
margin-right 。2s cubic-bezier(。4, 0, 。2, 1);
}
。n-button 。n-button__content {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
。n-button 。n-button__content ~ 。n-button__icon {
margin: var(——icon-margin);
margin-right: 0;
}
。n-button。n-button——block {
display: flex;
width: 100%;
}
。n-button。n-button——dashed 。n-button__border, 。n-button。n-button——dashed 。n-button__state-border {
border-style: dashed !important;
}
。n-button。n-button——disabled {
cursor: not-allowed;
opacity: var(——opacity-disabled);
}
@keyframes button-wave-spread {
from {
box-shadow: 0 0 0。5px 0 var(——ripple-color);
}
to {
box-shadow: 0 0 0。5px 4。5px var(——ripple-color);
}
}
@keyframes button-wave-opacity {
from {
opacity: var(——wave-opacity);
}
to {
opacity: 0;
}
}
可以看到 button 相關的樣式使用 BEM 命名風格處理了各種場景:
border 與 state-border ,關於 disabled、pressed、hover 、active 等狀態下的樣式
。n-button。n-button——color:not(。n-button——disabled):focus 。n-button__state-border {
border-color: var(——border-color-focus);
}
按鈕被點選時出現波紋的樣式 。n-button 。n-base-wave
按鈕中的 icon 相關的樣式 。n-button 。n-button__icon
按鈕中的文字等內容的樣式 。n-button 。n-button__content
同時可以看到在樣式中為各種屬性預留了對應的 CSS Variables,包括 box-shadow 的 ——ripple-color ,icon 寬高的 ——icon-size ,過渡動畫 transition 的 ——bezier ,這些變數是為後面定製各種樣式、主題留出空間。
即在設計一個元件庫的樣式系統時,元件相關的樣式模板使用 BEM 風格提前就定義好,然後對於需要定製的主題相關的變數等透過 CSS Variables 來進行個性化的更改,達到定製主題的效果。
掛載全域性樣式
全域性相關的樣式主要是一些簡單的基礎配置,程式碼如下:
import { c } from “。。/。。/_utils/cssr”;
import commonVariables from “。。/common/_common”;
export default c(
“body”,
`
margin: 0;
font-size: ${commonVariables。fontSize};
font-family: ${commonVariables。fontFamily};
line-height: ${commonVariables。lineHeight};
-webkit-text-size-adjust: 100%;
`,
[
c(
“input”,
`
font-family: inherit;
font-size: inherit;
`
),
]
);
主要為 margin 、font-size 、font-family 、line-height 等相關的內容,是為了相容瀏覽器所必要進行的 CSS 程式碼標準化,比較典型的有 Normalize。css: Make browsers render all elements more consistently。[6]。
其中 commonVariables 如下:
export default {
fontFamily:
’v-sans, system-ui, -apple-system, BlinkMacSystemFont, “Segoe UI”, sans-serif, “Apple Color Emoji”, “Segoe UI Emoji”, “Segoe UI Symbol”‘,
fontFamilyMono: “v-mono, SFMono-Regular, Menlo, Consolas, Courier, monospace”,
fontWeight: “400”,
fontWeightStrong: “500”,
cubicBezierEaseInOut: “cubic-bezier(。4, 0, 。2, 1)”,
cubicBezierEaseOut: “cubic-bezier(0, 0, 。2, 1)”,
cubicBezierEaseIn: “cubic-bezier(。4, 0, 1, 1)”,
borderRadius: “3px”,
borderRadiusSmall: “2px”,
fontSize: “14px”,
fontSizeTiny: “12px”,
fontSizeSmall: “14px”,
fontSizeMedium: “14px”,
fontSizeLarge: “15px”,
fontSizeHuge: “16px”,
lineHeight: “1。6”,
heightTiny: “22px”,
heightSmall: “28px”,
heightMedium: “34px”,
heightLarge: “40px”,
heightHuge: “46px”,
transformDebounceScale: “scale(1)”,
};
上述的通用變數是 UI 元件庫向上構建的一些最基礎的 “原材料”,也是預設不建議修改的、業界的最佳實踐,如定義 size 有 5 類,分別為 tiny 、small 、medium 、large 、huge ,定義字型、程式碼字型 等。
定義與註冊 CSS Variables
這塊的主要程式碼為:
const NConfigProvider = inject(configProviderInjectionKey, null);
const mergedThemeRef = computed(() => {
const {
theme: { common: selfCommon, self, peers = {} } = {},
themeOverrides: selfOverrides = {},
builtinThemeOverrides: builtinOverrides = {},
} = props;
const { common: selfCommonOverrides, peers: peersOverrides } =
selfOverrides;
const {
common: globalCommon = undefined,
[resolveId]: {
common: globalSelfCommon = undefined,
self: globalSelf = undefined,
peers: globalPeers = {},
} = {},
} = NConfigProvider?。mergedThemeRef。value || {};
const {
common: globalCommonOverrides = undefined,
[resolveId]: globalSelfOverrides = {},
} = NConfigProvider?。mergedThemeOverridesRef。value || {};
const {
common: globalSelfCommonOverrides,
peers: globalPeersOverrides = {},
} = globalSelfOverrides;
const mergedCommon = merge(
{},
selfCommon || globalSelfCommon || globalCommon || defaultTheme。common,
globalCommonOverrides,
globalSelfCommonOverrides,
selfCommonOverrides
);
const mergedSelf = merge(
// {}, executed every time, no need for empty obj
(self || globalSelf || defaultTheme。self)?。(mergedCommon),
builtinOverrides,
globalSelfOverrides,
selfOverrides
);
return {
common: mergedCommon,
self: mergedSelf,
peers: merge({}, defaultTheme。peers, globalPeers, peers),
peerOverrides: merge({}, globalPeersOverrides, peersOverrides),
};
首先是從透過 inject 拿到 configProviderInjectionKey 相關的內容,其中 configProviderInjectionKey 相關內容定義在如下:
provide(configProviderInjectionKey, {
mergedRtlRef,
mergedIconsRef,
mergedComponentPropsRef,
mergedBorderedRef,
mergedNamespaceRef,
mergedClsPrefixRef,
mergedLocaleRef: computed(() => {
// xxx
}),
mergedDateLocaleRef: computed(() => {
// xxx
}),
mergedHljsRef: computed(() => {
// 。。。
}),
mergedThemeRef,
mergedThemeOverridesRef
})
可以看到包含 rtl、icon、border、namespace、clsPrefix、locale(國際化)、date、theme、themeOverrides 等幾乎所有的配置,而這裡主要是想拿到主題相關的配置:
mergedThemeRef :可調整的主題,如
import { darkTheme } from ’naive-ui‘
export default {
setup() {
return {
darkTheme
}
}
}
mergedThemeOverridesRef :可調整的主題變數,如
const themeOverrides = {
common: {
primaryColor: ’#FF0000‘
},
Button: {
textColor: ’#FF0000‘
backgroundColor: ’#FFF000‘,
},
Select: {
peers: {
InternalSelection: {
textColor: ’#FF0000‘
}
}
}
// 。。。
}
上述的這兩者有主要包含全域性 common 相關的,以及 Button 中 common 相關的統一變數、self 相關的 Button 自定義的一些變數,以及 Button 在與其他元件使用時涉及相關限制的 peers 變數。
從 useTheme 鉤子函式中返回 themeRef 之後,themeRef 相關的內容會拿來組裝 Button 涉及到的各種樣式,主要從以下四個方向進行處理:
fontProps
colorProps
borderProps
sizeProps
cssVars: computed(() => {
// fontProps
// colorProps
// borderProps
// sizeProps
return {
// 處理 動畫過渡函式、透明度相關的變數
“——bezier”: cubicBezierEaseInOut,
“——bezier-ease-out”: cubicBezierEaseOut,
“——ripple-duration”: rippleDuration,
“——opacity-disabled”: opacityDisabled,
“——wave-opacity”: waveOpacity,
// 處理字型、顏色、邊框、大小相關的變數
。。。fontProps,
。。。colorProps,
。。。borderProps,
。。。sizeProps,
};
});
fontProps 相關程式碼如下:
const theme = themeRef。value;
const {
self,
} = theme;
const {
rippleDuration,
opacityDisabled,
fontWeightText,
fontWeighGhost,
fontWeight,
} = self;
const { dashed, type, ghost, text, color, round, circle } = props;
// font
const fontProps = {
fontWeight: text
? fontWeightText
: ghost
? fontWeighGhost
: fontWeight,
};
主要判斷當 Button 以 text 節點進行展示時、以透明背景進行展示時、標準狀態下的字型相關的 CSS 變數與值。
colorProps 相關程式碼如下
let colorProps = {
“——color”: “initial”,
“——color-hover”: “initial”,
“——color-pressed”: “initial”,
“——color-focus”: “initial”,
“——color-disabled”: “initial”,
“——ripple-color”: “initial”,
“——text-color”: “initial”,
“——text-color-hover”: “initial”,
“——text-color-pressed”: “initial”,
“——text-color-focus”: “initial”,
“——text-color-disabled”: “initial”,
};
if (text) {
const { depth } = props;
const textColor =
color ||
(type === “default” && depth !== undefined
? self[createKey(“textColorTextDepth”, String(depth))]
: self[createKey(“textColorText”, type)]);
colorProps = {
“——color”: “#0000”,
“——color-hover”: “#0000”,
“——color-pressed”: “#0000”,
“——color-focus”: “#0000”,
“——color-disabled”: “#0000”,
“——ripple-color”: “#0000”,
“——text-color”: textColor,
“——text-color-hover”: color
? createHoverColor(color)
: self[createKey(“textColorTextHover”, type)],
“——text-color-pressed”: color
? createPressedColor(color)
: self[createKey(“textColorTextPressed”, type)],
“——text-color-focus”: color
? createHoverColor(color)
: self[createKey(“textColorTextHover”, type)],
“——text-color-disabled”:
color || self[createKey(“textColorTextDisabled”, type)],
};
} elseif (ghost || dashed) {
colorProps = {
“——color”: “#0000”,
“——color-hover”: “#0000”,
“——color-pressed”: “#0000”,
“——color-focus”: “#0000”,
“——color-disabled”: “#0000”,
“——ripple-color”: color || self[createKey(“rippleColor”, type)],
“——text-color”: color || self[createKey(“textColorGhost”, type)],
“——text-color-hover”: color
? createHoverColor(color)
: self[createKey(“textColorGhostHover”, type)],
“——text-color-pressed”: color
? createPressedColor(color)
: self[createKey(“textColorGhostPressed”, type)],
“——text-color-focus”: color
? createHoverColor(color)
: self[createKey(“textColorGhostHover”, type)],
“——text-color-disabled”:
color || self[createKey(“textColorGhostDisabled”, type)],
};
} else {
colorProps = {
“——color”: color || self[createKey(“color”, type)],
“——color-hover”: color
? createHoverColor(color)
: self[createKey(“colorHover”, type)],
“——color-pressed”: color
? createPressedColor(color)
: self[createKey(“colorPressed”, type)],
“——color-focus”: color
? createHoverColor(color)
: self[createKey(“colorFocus”, type)],
“——color-disabled”: color || self[createKey(“colorDisabled”, type)],
“——ripple-color”: color || self[createKey(“rippleColor”, type)],
“——text-color”: color
? self。textColorPrimary
: self[createKey(“textColor”, type)],
“——text-color-hover”: color
? self。textColorHoverPrimary
: self[createKey(“textColorHover”, type)],
“——text-color-pressed”: color
? self。textColorPressedPrimary
: self[createKey(“textColorPressed”, type)],
“——text-color-focus”: color
? self。textColorFocusPrimary
: self[createKey(“textColorFocus”, type)],
“——text-color-disabled”: color
? self。textColorDisabledPrimary
: self[createKey(“textColorDisabled”, type)],
};
}
主要處理在四種形式:普通、text 節點、ghost 背景透明、dashed 虛線形式下,對不同狀態 標準 、pressed 、hover 、focus、 disabled 等處理相關的 CSS 屬性和值
borderProps 相關的程式碼如下:
let borderProps = {
“——border”: “initial”,
“——border-hover”: “initial”,
“——border-pressed”: “initial”,
“——border-focus”: “initial”,
“——border-disabled”: “initial”,
};
if (text) {
borderProps = {
“——border”: “none”,
“——border-hover”: “none”,
“——border-pressed”: “none”,
“——border-focus”: “none”,
“——border-disabled”: “none”,
};
} else {
borderProps = {
“——border”: self[createKey(“border”, type)],
“——border-hover”: self[createKey(“borderHover”, type)],
“——border-pressed”: self[createKey(“borderPressed”, type)],
“——border-focus”: self[createKey(“borderFocus”, type)],
“——border-disabled”: self[createKey(“borderDisabled”, type)],
};
}
主要處理在以 text 形式進行展示和普通形式展示下,五種不同狀態 標準 、pressed 、hover 、focus 、disabled等情況下的處理。
這裡 borderProps 其實主要是定義整個 border 屬性,而邊框顏色相關的屬性其實是透過 setup 裡面的 customColorCssVars 進行定義的,程式碼如下:
customColorCssVars: computed(() => {
const { color } = props;
if (!color) return null;
const hoverColor = createHoverColor(color);
return {
“——border-color”: color,
“——border-color-hover”: hoverColor,
“——border-color-pressed”: createPressedColor(color),
“——border-color-focus”: hoverColor,
“——border-color-disabled”: color,
};
})
sizeProps 相關的程式碼如下:
const sizeProps = {
“——width”: circle && !text ? height : “initial”,
“——height”: text ? “initial” : height,
“——font-size”: fontSize,
“——padding”: circle
? “initial”
: text
? “initial”
: round
? paddingRound
: padding,
“——icon-size”: iconSize,
“——icon-margin”: iconMargin,
“——border-radius”: text
? “initial”
: circle || round
? height
: borderRadius,
};
主要處理 width 、height 、font-size 、padding 、icon 、border 相關的大小內容,其中 margin 在掛載全域性預設樣式的時候進行了處理,預設為 0。
小結
透過上面三步走:
掛載 button 相關的樣式類骨架,留出大量 CSS Variables 用於自定義樣式
掛載全域性預設樣式
組裝、定義相關的 CSS Variables 來填充樣式類骨架
我們就成功應用 CSS Render、 BEM plugin、CSS Variables 完成了 Button 整體樣式的設計,它既易於理解、還易於定製。
不過也值得注意的是,縱觀上述元件中樣式的處理邏輯,只定義在 setup 裡,也少用生命週期相關的鉤子,其實也可以看出 CSS Render 的主要使用場景:即事先將所有的情況都規範好,相關的 CSS Variables 都預設好,然後給出
必要的事件處理也不能少
Naive UI 主要提供了以下幾類事件的處理:
mousedown : handleMouseDown
keyup :handleKeyUp
keydown: handleKeyDown
click : handleClick
blur : handleBlur
可以來分別看一下其中的程式碼:
handleMouseDown :
const handleMouseDown = (e) => {
e。preventDefault();
if (props。disabled) {
return;
}
if (mergedFocusableRef。value) {
selfRef。value?。focus({ preventScroll: true });
}
};
主要處理 disabled 情況下不響應、以及如果可以 focus 情況下,呼叫 selfRef 進行 focus,並激活對應的樣式。
handleKeyUp :
const handleKeyUp = (e) => {
switch (e。code) {
case“Enter”:
case“NumpadEnter”:
if (!props。keyboard) {
e。preventDefault();
return;
}
enterPressedRef。value = false;
void nextTick(() => {
if (!props。disabled) {
selfRef。value?。click();
}
});
}
};
主要處理 Enter 、NumpadEnter 鍵,判斷是否支援鍵盤處理,並在合適的情況下啟用按鈕點選。
handleKeyDown :
const handleKeyDown = (e) => {
switch (e。code) {
case“Enter”:
case“NumpadEnter”:
if (!props。keyboard) return;
e。preventDefault();
enterPressedRef。value = true;
}
};
主要處理 Enter 、NumpadEnter 鍵,判斷是否支援鍵盤處理,並在合適的情況下更新 enterPressedRef 的值,標誌當前是 keydown 過。
handleClick :
const handleClick = (e) => {
if (!props。disabled) {
const { onClick } = props;
if (onClick) call(onClick, e);
if (!props。text) {
const { value } = waveRef;
if (value) {
value。play();
}
}
}
};
根據狀態呼叫對應的點選處理函式,以及非 text 節點下播放按鈕的點選波紋動效。
handleBlur :
const handleBlur = () => {
enterPressedRef。value = false;
};
更新 enterPressedRef 的值,標誌當前是 blur 了。
總結與展望
本文透過一層層、原始碼級剖析了 Naive UI 的 Button 完整過程,可以發現對於元件庫這個領域來說,絕大部分的構思都是花在如何設計可擴充套件的樣式系統上,從 Ant Design、Element UI 使用 Less 來組織樣式系統,再到 Material Design 使用 CSS-in-JS,如 styled-components 來組織樣式系統,再到現在 Naive UI 使用 CSS Render 來組織樣式系統,雖然組織樣式系統的形式繁多,但實際上就我理解而言,在設計樣式類、對應的樣式、樣式的擴充套件和主題定製上應該大體保持相似。
如果你能透過這篇雜亂的文章理解了 Button 執行的整個過程,還保持著對 Naive UI 整體的原始碼、工程化方向建設的興趣,你完全可以按照這個邏輯去理解其他元件的設計原理,正如我在開始放的那張圖一樣,你瞭解整體程式碼的過程中會感覺越來越簡單:
瞭解優秀庫的原始碼設計、研讀大牛的原始碼可以幫助我們瞭解業界最佳實踐、優秀的設計思想和改進編寫程式碼的方式,成為更加優秀的開發者,你我共勉!
參考資料
https://css-tricks。com/bem-101/
https://www。smashingmagazine。com/2018/06/bem-for-beginners/
http://getbem。com/introduction/
https://necolas。github。io/normalize。css/
https://www。naiveui。com/zh-CN/os-theme/components/button
https://github。com/07akioni/css-render
http://www。woshipm。com/ucd/4243012。html
http://getbem。com/introduction/
參考資料
[1]
https://github。com/pftom/naive-app: https://github。com/pftom/naive-app
[2]
Naive UI : https://github。com/TuSimple/naive-ui
[3]
css-render: https://github。com/07akioni/css-render
[4]
vue3-ssr: https://www。npmjs。com/package/@css-render/vue3-ssr
[5]
plugin-bem: https://www。npmjs。com/package/@css-render/plugin-bem
[6]
Normalize。css: Make browsers render all elements more consistently。: https://necolas。github。io/normalize。css/