iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 14
6
Modern Web

從 Hooks 開始,讓你的網頁 React 起來系列 第 14

[Day 14 - 即時天氣] 把 CSS 寫在 JavaScript 中!? - CSS in JS 的使用

感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。

在 React 18 後已經棄用 ReactDOM.render(),改用 ReactDOM.createRoot(),內文中的圖片並未一併修改,煩請讀者留意。

傳統的網頁開發上,我們會把所有的 CSS 樣式寫在一支或多支的 CSS 檔內,接著在 index.html 中透過 <link rel="stylesheet" href="main.css" /> 的方式讓整個網站都能夠套用到這支 CSS 所撰寫的樣式。

上述這種方式因為所有的樣式都是作用在整個網頁的環境下,常會發生不小心命名了同樣的 class 名稱,導致樣式相互影響或彼此覆蓋,又或者發生某些樣式權重不夠的情況而難以調整,因此在 class 的命名上常常需要非常留意小心,進而也出現了許多對於 class 命名的不同規範和設計模式。

為什麼要把 CSS 寫在 JS 中(CSS-in-JS)?

然而,現今前端框架中,因為可以把各個元件給拆分開來,每個不同功能的按鈕都可以是不同的元件,每個元件之間可以是獨立不互相干擾的,所以連 CSS 的樣式也都可以有元件的概念存在,也就是說,在某個元件內所撰寫的樣式,即使有相同 class 的命名,但在最後編譯後這些樣式都只會作用在該元件內,不會干擾到外層或其他元件的樣式

這類把樣式連同元件寫在一起寫法稱作 CSS-in-JS,它的好處在於每個元件都是獨立可被重複使用,你不用再擔心改了 A 卡片的樣式卻不小心連 B 卡片的顏色也變了;你也不用再擔心把以為某支 CSS 檔案是多餘的,砍掉之後卻發生破版的情況,因為現在元件和樣式是綁在一起的,只要這個元件是完整的,那麼放到另一個地方去使用它時,外觀和功能也會是一樣的。

除此之外,既然 CSS 已經被放入的 JS 檔案中,如同把 HTML 寫在 JS 檔中的 JSX 一樣,這些 CSS 的樣式也將可以適用 JS 的語法

透過把 CSS 寫在 JS 中的這種寫法,可以確保特定樣式只會作用在該元件之外,同時還可以把 JS 中的一些邏輯判斷放到 CSS 使用。

現在,就讓我們來看看怎麼使用帶有樣式的元件吧!

提醒:實務上會同時搭配上述兩種做法,有些樣式仍會撰寫在全域環境,讓整個網頁都可以套用到該樣式(例如、版型、主題、字體、...);針對個別元件則在撰寫只用在該元件內的樣式。

CSS-in-JS 套件的選擇

React 中要讓每個元件帶有獨立樣式的做法很多,非常多人選擇使用 styled-components(下圖右側)或 emotion(下圖左側)這兩套工具:

Imgur

styled-components 是從 2016 年就開始的專案,使用人數和 Github 星星數一直都非常多,而 emotion 則自 2017 年開始,可能是由於新版本的 styled-components 一直停留在 beta 而未正式發表的緣故,許多人開始轉而使用 emotion。

npm trends 的分析可以看到雖然 styled-components 仍然擁有最多的星星數,但在 8 月 25 日的時候正式出現死亡交叉,emotion 的下載量正式且持續的超過 styled-components:

Imgur

? 補充:在挑選套件時,可以透過 npm trends 把幾個具有相似功能的套件進行比較,通常可以看出一些趨勢和找到適合自己的。

實際上這兩個套件的用法上非常接近,Styled-Component 因為比較早起步且非常多人使用,網路上已經可以找到許多教學範例,因此在後面的練習中,我們就來嘗試看看後起之秀的 emotion 吧!

Imgur

在 CodeSandbox 中載入 Emotion

昨天我們已經在 CodeSandbox 開啟了一個全新的 React 專案,現在我們可以直接在 CodeSandbox 的左側點選 Add Dependency 來載入和 Emotion 相關的套件,主要有 @emotion/core@emotion/styled 這兩個:

Imgur

建立並使用 Weather App Component

WeatherApp.js

先在 ./src 資料夾中新增一支名為 WeatherApp.js 的檔案,接著透過函式來建立一個名為WeatherApp 的元件,比較不一樣的地方是現在因為我們把不同的組間拆成不同支 JS 檔撰寫,因此在檔案開頭的地方要透過 import 把 React 載入進來,在最後的地方則要透過 export 把這個元件匯出:

// ./src/WeatherApp.js
import React from 'react';

const WeatherApp = () => {
  return <h1>Weather</h1>;
};

export default WeatherApp;

整個畫面會像這樣:

Imgur

index.js

接著到 index.js 這支檔案來把剛剛寫好的 WeatherApp 載入進來,並把多餘不必要的程式碼移除。可以留意到這裡有一句 import './styles.css';,透過這種方式載入的 CSS 因為沒有縮限在某個元件內,因此是會作用到整個網頁的:

// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import WeatherApp from './WeatherApp';

// 這支 CSS 檔的樣式會作用到全域
import './styles.css';

function App() {
  return <WeatherApp />;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

整個畫面會像這樣:

Imgur

styles.css

也就是說,如果有什麼樣式需要作用到整個網頁的話,就可以寫在 ./src/styles.css 這支檔案中。

這裡為了讓各個 HTML 元素在每個瀏覽器上看起來是一致的,我們先在 CodeSandbox 左下角「Add Resources」的地方貼上 https://unpkg.com/normalize.css@8.0.1/normalize.css 以載入 normalize.css

Imgur

接著,為了之後可以把整個版面撐滿,然後再把顯示天氣的區域放在畫面的正中央,因此在 styles.css 的檔案中,可以再加上:

/* ./src/styles.css */
html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
}

#root {
  height: 100%;
  width: 100%;
}

撰寫 WeatherApp 的版面與樣式

接下來,就可以來撰寫 WeatherApp 的版面和樣式了。

打開 ./src/WeatherApp.js,先來撰寫第一個帶有樣式的 component。

傳統寫法

過去在撰寫樣式時我們會像這樣為有需要的 HTML 元素加上 className 來帶入樣式:

const WeatherApp = () => {
  return (
    <div className="container">
      <div className="weather-card">
        <h1>Weather</h1>
      </div>
    </div>
  );
};

然後在 CSS 檔的地方去定義 .container.weather-card 的樣式:

.container {
  background-color: #ededed;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.weather-card {
  min-width: 360px;
  box-shadow: 0 1px 3px 0 #999999;
  background-color: #f9f9f9;
}

使用 emotion 撰寫 styled components

現在當我們要撰寫 CSS-in-js 的寫法時,會像下面的步驟這樣:

  • STEP 1:透過 importemotion 套件載入
  • STEP 2:定義帶有 styled 的 component
    • 第一次看到這種寫法,相信你會覺得非常神秘且不習慣,要建立一個帶有樣式的 <div> 標籤時,只需要使用 styled.div;如果要建立的是 <button> 則是使用 styled.button 其他則以此類推
    • 接著在 styled.div 後面加上兩個反引號(和 Template Literals 用的符號相同),在兩個反引號之間就可以 直接撰寫 CSS 。實際上這裡的 styled.div 是一個函式,而在函式後面直接加上反引號一樣屬於 Template Literals 的一種用法,只是比較少情況會這樣使用。
  • STEP 3:幫剛剛定義好帶有的 styled 的 component 放入 JSX 中即可使用
import React from 'react';

// STEP 1:載入 emotion 的 styled 套件
import styled from '@emotion/styled';

// STEP 2:定義帶有 styled 的 component
const Container = styled.div`
  background-color: #ededed;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const WeatherCard = styled.div`
  position: relative;
  min-width: 360px;
  box-shadow: 0 1px 3px 0 #999999;
  background-color: #f9f9f9;
  box-sizing: border-box;
  padding: 30px 15px;
`;

// STEP 3:把上面定義好的 styled-component 當成元件使用
const WeatherApp = () => {
  return (
    <Container>
      <WeatherCard>
        <h1>Weather</h1>
      </WeatherCard>
    </Container>
  );
};

export default WeatherApp;

若想進一步了解在函式後面直接加上反引號的這種 Template Literal 用法,可以餐考這篇 JavaScript Template Literals and styled-components

完成的畫面會像這樣:

Imgur

CodeSandbox 和 CodePen 一樣可以有 Debug 這種頁面方便除錯,只需要複製右邊網頁上方的網址貼在瀏覽器網址列上即可:

Imgur

在這個頁面打開瀏覽器的開發者工具可以看到,這些帶有樣式的元件,最後都會帶上特殊的 class 名稱,並且套用上所撰寫的 CSS 樣式,而這也就是為什麼不同元件之間的 CSS 樣式不會相互干擾的原因。即使這不同頁面中都定義了一個同樣名為 <Container /> 的 styled-component,但因為它們最終會帶上不同的 class 名稱,因此元件間的樣式並不會相互干擾:

Imgur

今天先讓我們完成到這裡,有需要的話可以到 Weather App - started template @ CodeSandbox 檢視完整的程式碼。明天會使用更多 emotion 來完成樣式並載入 SVG 圖案,把畫面像下面這樣完成,接著再透過中央氣象局的 API 拉取即時的資料來顯示。

Imgur

程式範例

參考資源


上一篇
[Day 13 - 即時天氣] 建立一個即時天氣 App - 前置準備
下一篇
[Day 15 - 即時天氣] 就是這個畫面 - 使用 Emotion 為組件增添 CSS 樣式
系列文
從 Hooks 開始,讓你的網頁 React 起來30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言