Next.js 中的全局與本地樣式
已發表: 2022-03-10我在使用 Next.js 管理複雜的前端項目方面擁有豐富的經驗。 Next.js 對如何組織 JavaScript 代碼有意見,但它沒有關於如何組織 CSS 的內置意見。
在框架內工作後,我發現了一系列組織模式,我認為它們既符合 Next.js 的指導理念,又能實踐最佳 CSS 實踐。 在本文中,我們將一起構建一個網站(一家茶店!)來演示這些模式。
注意:您可能不需要以前的 Next.js 經驗,儘管對 React 有基本的了解並樂於學習一些新的 CSS 技術會很好。
編寫“老式”CSS
在第一次查看 Next.js 時,我們可能會考慮使用某種 CSS-in-JS 庫。 儘管根據項目的不同可能會有好處,但 CSS-in-JS 引入了許多技術考慮。 它需要使用一個新的外部庫,這會增加包的大小。 CSS-in-JS 還可以通過導致額外的渲染和對全局狀態的依賴來產生性能影響。
推薦閱讀: Aggelos Arvanitakis 的“現代 CSS-in-JS 庫在 React 應用程序中的看不見的性能成本”
此外,使用 Next.js 之類的庫的全部意義在於盡可能靜態渲染資源,因此編寫需要在瀏覽器中運行以生成 CSS 的 JS 並沒有多大意義。
在 Next.js 中組織樣式時,我們必須考慮幾個問題:
我們如何適應框架的約定/最佳實踐?
我們如何平衡“全局”樣式問題(字體、顏色、主要佈局等)與“本地”樣式問題(關於單個組件的樣式)?
對於第一個問題,我想出的答案是簡單地編寫好的老式 CSS 。 Next.js 不僅無需額外設置就支持這樣做; 它還產生高性能和靜態的結果。
為了解決第二個問題,我採取的方法可以概括為四部分:
- 設計令牌
- 全局樣式
- 實用程序類
- 組件樣式
我在這裡感謝 Andy Bell 的CUBE CSS (“Composition, Utility, Block, Exception”)理念。 如果您以前沒有聽說過這種組織原則,我建議您查看其官方網站或 Smashing Podcast 上的功能。 我們將從 CUBE CSS 中獲得的原則之一是我們應該接受而不是害怕 CSS 級聯的想法。 讓我們通過將它們應用於網站項目來學習這些技術。
入門
我們將建立一家茶葉店,因為,嗯,茶很好吃。 我們將首先運行yarn create next-app
來創建一個新的 Next.js 項目。 然後,我們將刪除styles/ directory
中的所有內容(都是示例代碼)。
注意:如果你想跟隨完成的項目,你可以在這裡查看。
設計代幣
在幾乎任何 CSS 設置中,將所有全局共享值存儲在 variables 中都有明顯的好處。 如果客戶要求更改顏色,則實施更改是單行的,而不是大量的查找和替換混亂。 因此,我們 Next.js CSS 設置的一個關鍵部分是將所有站點範圍的值存儲為設計標記。
我們將使用內置的 CSS 自定義屬性來存儲這些標記。 (如果你不熟悉這種語法,你可以查看“A Strategy Guide To CSS Custom Properties”。)我應該提到(在一些項目中)我選擇使用 SASS/SCSS 變量來達到這個目的。 我沒有發現任何真正的優勢,所以如果我發現我需要其他SASS 功能(混合、迭代、導入文件等),我通常只會在項目中包含 SASS。 相比之下,CSS 自定義屬性也可以與級聯一起使用,並且可以隨時間更改而不是靜態編譯。 所以,今天,讓我們堅持使用純 CSS 。
在我們的styles/
目錄中,讓我們創建一個新的design_tokens.css文件:
:root { --green: #3FE79E; --dark: #0F0235; --off-white: #F5F5F3; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 1.5rem; --font-size-sm: 0.5rem; --font-size-md: 1rem; --font-size-lg: 2rem; }
當然,這個列表可以而且會隨著時間的推移而增長。 添加此文件後,我們需要跳轉到pages/_app.jsx文件,這是我們所有頁面的主要佈局,並添加:
import '../styles/design_tokens.css'
我喜歡將設計令牌視為在整個項目中保持一致性的粘合劑。 我們將在全球範圍內以及在單個組件內引用這些變量,以確保統一的設計語言。
全局樣式
接下來,讓我們在我們的網站上添加一個頁面! 讓我們進入pages/index.jsx文件(這是我們的主頁)。 我們將刪除所有樣板並添加如下內容:
export default function Home() { return <main> <h1>Soothing Teas</h1> <p>Welcome to our wonderful tea shop.</p> <p>We have been open since 1987 and serve customers with hand-picked oolong teas.</p> </main> }
不幸的是,它看起來很簡單,所以讓我們為基本元素設置一些全局樣式,例如<h1>
標籤。 (我喜歡將這些樣式視為“合理的全局默認值”。)我們可能會在特定情況下覆蓋它們,但它們是一個很好的猜測,如果我們不這樣做,我們會想要什麼。
我將把它放在styles/globals.css文件中(默認來自 Next.js):
*, *::before, *::after { box-sizing: border-box; } body { color: var(--off-white); background-color: var(--dark); } h1 { color: var(--green); font-size: var(--font-size-lg); } p { font-size: var(--font-size-md); } p, article, section { line-height: 1.5; } :focus { outline: 0.15rem dashed var(--off-white); outline-offset: 0.25rem; } main:focus { outline: none; } img { max-width: 100%; }
當然,這個版本是相當基本的,但我的globals.css文件通常最終實際上不需要變得太大。 在這裡,我設計了基本的 HTML 元素(標題、正文、鏈接等)。 無需將這些元素包裝在 React 組件中,也無需為了提供基本樣式而不斷添加類。
我還包括默認瀏覽器樣式的任何重置。 有時,我會使用一些站點範圍的佈局樣式來提供“粘性頁腳”,例如,但它們僅在所有頁面共享相同佈局時才屬於這裡。 否則,它需要在單個組件內進行限定。
我總是包含某種:focus
樣式,以便在聚焦時為鍵盤用戶清楚地指示交互元素。 最好讓它成為網站設計 DNA 中不可或缺的一部分!
現在,我們的網站開始成型:
實用程序類
我們的主頁肯定可以改進的一個方面是文本當前總是延伸到屏幕的兩側,所以讓我們限制它的寬度。 我們在這個頁面上需要這個佈局,但我想我們可能在其他頁面上也需要它。 這是實用程序類的一個很好的用例!
我嘗試謹慎地使用實用程序類,而不是僅僅作為編寫 CSS 的替代品。 我個人對何時向項目添加一個有意義的標準是:
- 我反复需要它;
- 它做好一件事;
- 它適用於一系列不同的組件或頁面。
我認為這個案例符合所有三個條件,所以讓我們創建一個新的 CSS 文件styles/utilities.css並添加:
.lockup { max-width: 90ch; margin: 0 auto; }
然後讓我們將 import '../styles/utilities.css'
添加到我們的pages/_app.jsx中。 最後,讓我們將 pages/index.jsx 中的<main>
標籤更改為<main className="lockup">
。
現在,我們的頁面更加緊密。 因為我們使用了max-width
屬性,所以我們不需要任何媒體查詢來使我們的佈局具有移動響應性。 而且,因為我們使用了ch
度量單位——它大約相當於一個字符的寬度——我們的大小是動態的,與用戶的瀏覽器字體大小有關。
隨著我們網站的發展,我們可以繼續添加更多實用程序類。 我在這裡採取了一種相當實用的方法:如果我正在工作並且發現我需要另一個類來獲得顏色或其他東西,我會添加它。 我不會在陽光下添加所有可能的類——它會膨脹 CSS 文件大小並使我的代碼混亂。 有時,在較大的項目中,我喜歡將內容分解為帶有幾個不同文件的styles/utilities/
目錄; 這取決於項目的需要。
我們可以將實用程序類視為全球共享的通用、重複樣式命令的工具包。 它們有助於防止我們在不同組件之間不斷重寫相同的 CSS。
組件樣式
目前我們已經完成了我們的主頁,但我們仍然需要建立我們網站的一部分:在線商店。 我們的目標是顯示我們想要銷售的所有茶的卡片網格,因此我們需要向我們的網站添加一些組件。
讓我們首先在pages/shop.jsx添加一個新頁面:
export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }
然後,我們需要一些茶來展示。 我們將為每種茶添加名稱、描述和圖像(在 public/ 目錄中):
const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]
注意:這不是一篇關於數據獲取的文章,所以我們採取了簡單的方法,並在文件的開頭定義了一個數組。
接下來,我們需要定義一個組件來顯示我們的茶。 讓我們從創建一個components/
目錄開始(Next.js 默認不創建)。 然後,讓我們添加一個components/TeaList
目錄。 對於任何最終需要多個文件的組件,我通常將所有相關文件放在一個文件夾中。 這樣做可以防止我們的components/
文件夾無法導航。
現在,讓我們添加我們的components/TeaList/TeaList.jsx文件:
import TeaListItem from './TeaListItem' const TeaList = (props) => { const { teas } = props return <ul role="list"> {teas.map(tea => <TeaListItem tea={tea} key={tea.name} />)} </ul> } export default TeaList
這個組件的目的是遍歷我們的茶並為每個茶顯示一個列表項,所以現在讓我們定義我們的components/TeaList/TeaListItem.jsx組件:
import Image from 'next/image' const TeaListItem = (props) => { const { tea } = props return <li> <div> <Image src={tea.image} alt="" objectFit="cover" objectPosition="center" layout="fill" /> </div> <div> <h2>{tea.name}</h2> <p>{tea.description}</p> </div> </li> } export default TeaListItem
請注意,我們使用的是 Next.js 的內置圖像組件。 我將alt
屬性設置為空字符串,因為在這種情況下圖像純粹是裝飾性的; 我們希望避免屏幕閱讀器用戶在此處被冗長的圖像描述所困擾。
最後,讓我們製作一個components/TeaList/index.js文件,這樣我們的組件就可以很容易地從外部導入:
import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList
然後,讓我們通過將 import TeaList from ../components/TeaList
和<TeaList teas={teas} />
元素添加到我們的 Shop 頁面來將它們連接在一起。 現在,我們的茶會出現在列表中,但不會那麼漂亮。
通過 CSS 模塊將樣式與組件放在一起
讓我們從設置卡片樣式開始( TeaListLitem
組件)。 現在,在我們的項目中,我們第一次想要添加特定於一個組件的樣式。 讓我們創建一個新文件components/TeaList/TeaListItem.module.css 。
您可能想知道文件擴展名中的模塊。 這是一個CSS 模塊。 Next.js 支持 CSS 模塊並包含一些關於它們的優秀文檔。 當我們從 CSS 模塊(例如.TeaListItem
)中編寫類名時,它會自動轉換為更像 .TeaListItem 的名稱. TeaListItem_TeaListItem__TFOk_
. TeaListItem_TeaListItem__TFOk_
加上一堆額外的字符。 因此,我們可以使用我們想要的任何類名,而不必擔心它會與我們站點中其他地方的其他類名衝突。
CSS 模塊的另一個優點是性能。 Next.js 包含一個動態導入功能。 next/dynamic 允許我們延遲加載組件,以便它們的代碼僅在需要時加載,而不是增加整個包的大小。 如果我們將必要的本地樣式導入到單個組件中,那麼用戶也可以為動態導入的組件延遲加載 CSS 。 對於大型項目,我們可能會選擇延遲加載大量代碼,並且只預先加載最必要的 JS/CSS。 因此,我通常會為每個需要本地樣式的新組件創建一個新的 CSS 模塊文件。
讓我們首先在我們的文件中添加一些初始樣式:
.TeaListItem { display: flex; flex-direction: column; gap: var(--space-sm); background-color: var(--color, var(--off-white)); color: var(--dark); border-radius: 3px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }
然後,我們可以在TeaListitem
組件中從./TeaListItem.module.css
導入樣式。 style 變量像 JavaScript 對像一樣進來,所以我們可以訪問這個類style.TeaListItem.
注意:我們的類名不需要大寫。 我發現模塊內部(以及外部的小寫)類名的大寫約定在視覺上區分了本地類名和全局類名。
因此,讓我們使用新的本地類並將其分配給TeaListItem
組件中的<li>
:
<li className={style.TeaListComponent}>
您可能想知道背景顏色線(即var(--color, var(--off-white));
)。 這個片段的意思是默認情況下背景將是我們的--off-white
值。 但是,如果我們在卡片上設置--color
自定義屬性,它將覆蓋並選擇該值。
起初,我們希望所有卡片都是--off-white
,但我們可能希望稍後更改個別卡片的值。 這與 React 中的 props 非常相似。 我們可以設置一個默認值,但創建一個插槽,我們可以在特定情況下選擇其他值。 因此,我鼓勵我們考慮 CSS 自定義屬性,例如 CSS 的 props 版本。
樣式仍然看起來不太好,因為我們要確保圖像保留在它們的容器中。 Next.js 的帶有layout="fill"
屬性的 Image 組件獲取position: absolute;
從框架中,所以我們可以通過放置一個容器來限制大小:相對;。
讓我們在TeaListItem.module.css中添加一個新類:
.ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }
然後讓我們在包含我們的<Image>
的<div>
上添加className={styles.ImageContainer}
。 我使用相對“簡單”的名稱,例如ImageContainer
,因為我們在 CSS 模塊中,所以我們不必擔心與外部樣式衝突。
最後,我們想在文本的兩側添加一些填充,所以讓我們添加最後一個類並依賴我們設置為設計標記的間距變量:
.Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }
我們可以將這個類添加到包含我們的名稱和描述的<div>
中。 現在,我們的卡片看起來還不錯:
結合全球和本地風格
接下來,我們希望我們的卡片以網格佈局顯示。 在這種情況下,我們只是處於局部樣式和全局樣式之間的邊界。 我們當然可以直接在TeaList
組件上編寫我們的佈局。 但是,我也可以想像,擁有一個將列表轉換為網格佈局的實用程序類在其他幾個地方可能很有用。
讓我們在這裡採用全局方法並在我們的styles/utilities.css中添加一個新的實用程序類:
.grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr)); gap: var(--space-md); }
現在,我們可以在任何列表中添加.grid
類,我們將獲得一個自動響應的網格佈局。 我們還可以更改--min-item-width
自定義屬性(默認30ch
)來更改每個元素的最小寬度。
注意:記住要考慮像道具這樣的自定義屬性! 如果這個語法看起來不熟悉,你可以查看 Chris Coyier 的“Intrinsically Responsive CSS Grid With minmax()
And min()
”。
由於我們已經在全局範圍內編寫了這種樣式,因此不需要任何花哨的操作就可以將className="grid"
添加到我們的TeaList
組件中。 但是,假設我們想將這種全球風格與一些額外的本地商店結合起來。 例如,我們希望在其中加入更多的“茶美學”,並讓其他每張卡片都有綠色背景。 我們需要做的就是創建一個新的components/TeaList/TeaList.module.css文件:
.TeaList > :nth-child(even) { --color: var(--green); }
還記得我們是如何在TeaListItem
組件上創建--color custom
屬性的嗎? 好了,現在我們可以根據具體情況進行設置了。 請注意,我們仍然可以在 CSS 模塊中使用子選擇器,並且我們選擇在不同模塊中設置樣式的元素並不重要。 因此,我們也可以使用本地組件樣式來影響子組件。 這是一個特性而不是一個錯誤,因為它允許我們利用 CSS 級聯! 如果我們嘗試以其他方式複制這種效果,我們最終可能會得到某種 JavaScript 湯而不是三行 CSS。
那麼,我們如何在TeaList
組件上保留全局.grid
類,同時添加本地.TeaList
類? 這就是語法可能變得有點古怪的地方,因為我們必須通過執行類似style.TeaList
之類的操作來訪問 CSS 模塊之外的.TeaList
類。
一種選擇是使用字符串插值來獲得類似:
<ul role="list" className={`${style.TeaList} grid`}>
在這種小情況下,這可能就足夠了。 如果我們混合和匹配更多的類,我發現這種語法讓我的大腦有點爆炸,所以我有時會選擇使用類名庫。 在這種情況下,我們最終得到一個看起來更合理的列表:
<ul role="list" className={classnames(style.TeaList, "grid")}>
現在,我們已經完成了 Shop 頁面,並且我們已經讓TeaList
組件同時利用了全局和本地樣式。
平衡法
我們現在已經建立了我們的茶館,只使用純 CSS 來處理樣式。 您可能已經註意到,我們不必花費很長時間來處理自定義 Webpack 設置、安裝外部庫等等。 這是因為我們使用 Next.js 開箱即用的模式。 此外,它們鼓勵最佳 CSS 實踐並自然地融入 Next.js 框架架構。
我們的 CSS 組織由四個關鍵部分組成:
- 設計代幣,
- 全球風格,
- 實用程序類,
- 組件樣式。
隨著我們繼續構建我們的網站,我們的設計令牌和實用程序類列表將會增加。 任何添加為實用程序類沒有意義的樣式,我們可以使用 CSS 模塊添加到組件樣式中。 因此,我們可以在局部和全局樣式問題之間找到持續的平衡。 我們還可以生成與 Next.js 站點一起自然增長的高性能、直觀的 CSS 代碼。