iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
5
Modern Web

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

[Day 15 - 即時天氣] 就是這個畫面 - 使用 Emotion 為組件增添 CSS 樣式

  • 分享至 

  • xImage
  •  

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

老實說這幾天在寫鐵人賽時,發現花最多時間的常常是在找自己喜歡的設計畫面,因為喜歡好看的作品,但卻又不會設計,同時也不想要只是用簡陋的畫面說明程式語法...。好吧!廢話完畢,希望你會喜歡這 30 天中自己做出來的作品。

這次即時天氣的設計畫面主要是參考 imgur 上的圖片,另外則會使用 The Weather is Nice Today 提供的天氣 ICON 來完成。完成的畫面大概會像下面這樣:

Imgur

使用 Emotion 定義基本的 Styled Components

昨天已經把用來撰寫 Styled Components 的套件 emotion 載入專案當中,在 CodeSandbox 中你一樣可以透過 Fork 複製一份昨天的程式碼 Weather App - started template 在繼續開始今天的內容:

Imgur

這裡我們根據下圖拆分成不同的 HTML 區塊:

Imgur

Emotion 中基本 styled components 的使用如同昨天建立 <Container /><WeatherCard /> 的做法,這裡我們就繼續完成 <WeatherCard /> 裡面的結構和樣式,順便複習一下。

以 Location 區塊為例

以 Location 這個區塊為例,我們預期它會是個 div 元素,因此要建立帶有樣式的組件,只需要:

// 定義帶有樣式的 `<Location />` 組件
// 在兩個反引號中放入該 Component 的 CSS 樣式
const Location = styled.div`
  font-size: 28px;
  color: #212121;
  margin-bottom: 20px;
`;

定義好之後,它就是一個 React 組件,可以直接把 <Location /> 放入 JSX 中:

Imgur

而它最後在 HTML 中呈現出來就會是帶有一個特殊 class name 的 <div>,這個 class name(即下圖中的 css-a7vwns)則會對應到剛剛針對 <Location> 所撰寫的 CSS 樣式:

Imgur

完成其他基本的 Styled Components

上面是用 Emotion 建立 Styled Components 最基本的方式。現在就讓我們把其他的 Styled Components 完成,如果你對於 CSS 的內容沒有太大興趣的話,可以直接把下面的程式碼複製到 CodeSandbox 中的 ./src/WeatherApp.js 這隻檔案:

import React from 'react';
import styled from '@emotion/styled';

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;
`;

const Location = styled.div`
  font-size: 28px;
  color: #212121;
  margin-bottom: 20px;
`;

const Description = styled.div`
  font-size: 16px;
  color: #828282;
  margin-bottom: 30px;
`;

const CurrentWeather = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30px;
`;

const Temperature = styled.div`
  color: #757575;
  font-size: 96px;
  font-weight: 300;
  display: flex;
`;

const Celsius = styled.div`
  font-weight: normal;
  font-size: 42px;
`;

const AirFlow = styled.div`
  display: flex;
  align-items: center;
  font-size: 16x;
  font-weight: 300;
  color: #828282;
  margin-bottom: 20px;
`;

const Rain = styled.div`
  display: flex;
  align-items: center;
  font-size: 16x;
  font-weight: 300;
  color: #828282;
`;

const WeatherApp = () => {
  return (
    <Container>
      <WeatherCard>
        <Location>台北市</Location>
        <Description>多雲時晴</Description>
        <CurrentWeather>
          <Temperature>
            23 <Celsius>°C</Celsius>
          </Temperature>
        </CurrentWeather>
        <AirFlow>23 m/h</AirFlow>
        <Rain>48%</Rain>
      </WeatherCard>
    </Container>
  );
};

export default WeatherApp;

此時你應該會看到如下的畫面:

Imgur

目前還未載入任何和天氣有空 ICON。

在 React 中載入 SVG 圖片

上傳 SVG 圖片到 CodeSandbox

這裡為了讓圖片能夠擁有最佳的解析度,關於天氣的圖示我們會使用 SVG 而非 PNG,SVG 屬於向量圖,因此不論縮放到多大的尺寸都可以維持最佳的解析度,而不會出現像是 JPG 或 PNG 放到過大後出現方格的情形。

這裡使用的天氣圖示都來自 The Weather is Nice Today,其中因為有些 SVG 直接上傳到 CodeSandbox 呈現時有些問題,所以會先透過 Sketch 重新轉檔,轉檔後的圖片會放在 dropbox 上提供下載,你可以直接透過拖曳的方式,拉到 CodeSandbox 中。

先在 ./src 資料夾中新增一個 images 的資料夾,接著把 dropbox 上的圖檔下載到本機後,可以透過拖曳的方式把它們拉進去 images 資料夾中:

Imgur

Dropbox 內的天氣圖示會隨專案使用持續增加,但下載連結不會改變,有新圖示時可以在重新下載。

載入 SVG 圖示到 React 中

由於在 CodeSandbox 中是透過 create-react-app 這個工具建立起的 React 開發環境,相關的配置都已經設定,所以要把 SVG 載入 React 中的方式很簡單。

關於 create-react-app 這個建立專案的工具會在最後幾天說明,只需先知道透過這個工具可以快速建立好 React 的開發環境。

第一種方式:ReactComponent

第一種方式是把 SVG 當成一個 React 組件加以載入,因為變成了 React 組件,所以後續如果有需要改變 SVG 的顏色或做動畫都比較靈活。

寫法像這樣:

  • STEP 1:將 ./images/cloudy.svg 匯入,並將該組件命名為 Cloudy ,而 ReactComponent 是 create-react-app 提供的物件
  • STEP 2:在需要的地方就可以使用 <Cloudy />
// STEP 1:使用 import { ReactComponent as xxx } from xxx 載入 SVG
import { ReactComponent as Cloudy } from './images/cloudy.svg';
import { ReactComponent as RainIcon } from './images/rain.svg';

const App = () => (
  <div>
    {/* STEP 2:直接使用該 Component */}
    <Cloudy />
    <RainIcon />
  </div>
);

第二種方式:直接 import SVG 並搭配 img

這種方法因為是把 SVG 以圖檔的形式載入,因此後續比較難去修改 SVG 圖示的顏色、粗細或製作動畫等效果,但若單純只是要以圖檔呈現,使用這種方式較簡便:

  • STEP 1:將 ./images/cloudy.svg 匯入,匯入的內容會變成該圖檔的路徑
  • STEP 2:使用 <img src={...} /> 的方式將 SVG 圖片掛入
// STEP 1:使用 import xxx from xxx 載入 cloudyIcon,會取得該圖檔路徑
import cloudyIcon from './images/cloudy.svg';
import rainIcon from './images/rain.svg';

const App = () => (
  <div>
    {/* STEP 2:透過 src 把 SVG 圖片呈現出來 */}
    <img src={cloudyIcon} alt="cloudy icon" />
    <img src={rainIcon} alt="rain icon" />
  </div>
);

⚠️ 注意:上述這兩種載入 SVG 圖檔的方式都需要使用 create-react-app 來建立專案,或在 WebPack 中有對應的設定才可以。

在即時天氣 App 中載入 SVG 圖片

由於上述第二種直接 import SVG 圖片的方式實際上是取得該圖檔的路徑,但在 CodeSandbox 中該圖檔路徑會是錯誤的,進而有破圖的情況,因此在後面的範例中我們統一都會使用上述的第一種方式。

現在就讓我們來把相關的圖片載入進來:

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

// 載入圖示
import { ReactComponent as CloudyIcon } from './images/cloudy.svg';
import { ReactComponent as AirFlowIcon } from './images/airFlow.svg';
import { ReactComponent as RainIcon } from './images/rain.svg';
import { ReactComponent as RedoIcon } from './images/redo.svg';

// ...

接下來就可以把這些圖式直接當成 Component 來使用,放入 JSX 中:

Imgur

現在的畫面會像這樣子:

Imgur

圖片帶進來後,因為沒有設定高,破版的有點嚴重。

對於在些 SVG 的組件來說,最後渲染到網頁的時候其實就是把 SVG 的程式碼放入 HTML 內,因此一樣透過 CSS 選擇器去選到對應的 SVG 後進行樣式的調整。這裡我們先調整一下 <AirFlow /><Rain /> 的部分。只須在當初定義 styled components 的地方去添加 CSS 修改 SVG 的樣式即可:

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

const AirFlow = styled.div`
  display: flex;
  align-items: center;
  font-size: 16x;
  font-weight: 300;
  color: #828282;
  margin-bottom: 20px;

  svg {
    width: 25px;
    height: auto;
    margin-right: 30px;
  }
`;

const Rain = styled.div`
  display: flex;
  align-items: center;
  font-size: 16x;
  font-weight: 300;
  color: #828282;

  svg {
    width: 25px;
    height: auto;
    margin-right: 30px;
  }
`;

const WeatherApp = () => {
  // ...
};

export default WeatherApp;

其中修改的部份如下:

Imgur

現在的畫面會長得像這樣子:

Imgur

可以看到在風速和雨量的部分大小已經調整好了,但我們還希望天氣的圖示還有「重新整理」的符號可以在小一點,我們接著來看可以怎麼做。

使用 Emotion 調整 Components 樣式

前面我們提到過怎麼使用 Emotion 來建立帶有樣式的 styled components,但 Emotion 不僅可以用來建立組件,還可以將原本就存在的組件添加樣式

舉例來說,剛剛我們透過 import 載入的 SVG 是一個 React 組件,例如,<CloudyIcon /><RedoIcon />。現在如果我們想要為這個原本就存在的組件添加樣式時,可以這麼做:

// 透過 styled(組件) 來把樣式帶入已存在的組件中

const Cloudy = styled(CloudyIcon)`
  /* 在這裡寫入 CSS 樣式 */
  flex-basis: 30%;
`;

const Redo = styled(RedoIcon)`
  /* 在這裡寫入 CSS 樣式 */
  width: 15px;
  height: 15px;
  position: absolute;
  right: 15px;
  bottom: 15px;
  cursor: pointer;
`;

也就是說,原本是在 styled. 後面加上一個 HTML 標籤,現在則是放入一個 React 組件,然後就可以在裡面撰寫 CSS 樣式。

現在我們就把上面透過 Emotion 添加完樣式後的新組件放到 JSX 中,也就是把原本的 <CloudyIcon /> 改成 <Cloudy /> ,原本的 <RedoIcon /> 改成 <Redo />

Imgur

大功告成,終於做出我們像要的即時天氣 App 畫面!

Imgur

明天我們會開始串接中央氣象局的資料,讓畫面不再只是假新聞!

今天完整的程式碼一樣可以到 CodeSandbox 中檢視 Weather APP with Emotion

補充:Emotion 的更多用法

透過 props 將資料帶入 Styled Components 內

既然透過 Emotion 建立的 Styled Components 中仍然是個 React 的組件,就表示可以像第 11 天的內容一樣,透過 props 把資料傳入組件內

舉例來說,現在如果想要將資料 theme 帶入建立好的 <Location /> 內,一樣只要利用像是 HTML 屬性的方式即可:

const WeatherApp = () => {
  return (
    {/* ... */}
      <Location theme="dark">台北市</Location>
    {/* ... */}
  );
};

接著在使用 Emotion 定義 Location 這個 Styled Component 的地方,就可以透過 props 取得傳入的資料:

// 透過 props 取得傳進來的資料
// props 會是 {theme: "dark", children: "台北市"}
const Location = styled.div`
  ${props => console.log(props)}
  font-size: 28px;
  color: #212121;
  margin-bottom: 20px;
`;

這個做有什麼用呢?當我們可以取得外部傳進來的資料時,就可以根據這個資料來決定要呈現的 CSS 樣式,例如,當 themedark 時就把文字顏色改成 #DADADA,否則顯示 #212121,就可以寫成這樣:

// 透過傳進來的資料決定要呈現的樣式
const Location = styled.div`
  font-size: 28px;
  color: ${props => props.theme === 'dark' ? '#dadada' : '#212121'};
  margin-bottom: 20px;
`;

定義許多組件都會共用到的樣式

有時多個組件間還是可能有需要被共用的樣式,像是如果每個按鈕都有固定的外觀,只是不同按鈕組件的顏色有不同時,如果重複在每個按鈕組件都撰寫同樣的 CSS 樣式會變得有點多餘,而且若之後需要修改按鈕的外觀,還得要每支檔案一支一支改,但卻又不想定義 class 樣式來套用時,可以怎麼做呢?

在 Emotion 中可以把撰寫好的 CSS 樣式當作 JavaScript 函式保存起來,步驟如下:

  1. @emotion/core 中匯入 Emotion 提供的 css 函式
  2. 定義帶有 CSS 樣式的函式
  3. 在 Styled Components 中套用定義好的樣式
// STEP 1:匯入 Emotion 提供的 css 函式
import { css } from '@emotion/core';

// STEP 2:將一批 CSS 樣式定義成 JavaScript 函式
const buttonDefault = () => css`
  display: block;
  width: 120px;
  height: 30px;
  font-size: 14px;
  background-color: transparent;
  color: #212121;
`;

// STEP 3 在定義 Styled Components 時載入定義好的 CSS 樣式
// 和 CSS 一樣,同樣的樣式後面寫的會覆蓋前面寫的
const rejectButton = styled.button`
  ${buttonDefault}
  background-color: red;
`

const acceptButton = styled.button`
  ${buttonDefault}
  background-color: green;
`

這種作法不僅能夠讓組件共用某些樣式,一樣能夠透過 props 取得組件帶進來的資料,當有需要切換亮色/暗色主題的時候非常實用:

import { css } from '@emotion/core';

// 在共用樣式的函式中,一樣可以透過 props 取得外部傳來的資料
const buttonDefault = (props) => css`
  display: block;
  width: 120px;
  height: 30px;
  font-size: 14px;
  background-color: transparent;
  color: ${props.theme === 'dark' ? '#dadada' : '#212121'};
`;

const rejectButton = styled.button`
  ${buttonDefault}
  background-color: red;
`;

const acceptButton = styled.button`
  ${buttonDefault}
  background-color: green;
`;

當然 Emotion 的用法還不只這些,這裡只提到一些蠻常用也蠻實用的一些功能,若想完整了解 Emotion 的更多功能,可以進一步參考 Emotion 官方文件

程式範例

參考資源


上一篇
[Day 14 - 即時天氣] 把 CSS 寫在 JavaScript 中!? - CSS in JS 的使用
下一篇
[Day 16 - 即時天氣] 定義並請求組件會使用到的資料 - useState 的更多使用
系列文
從 Hooks 開始,讓你的網頁 React 起來30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
SunAllen
iT邦研究生 1 級 ‧ 2019-10-02 14:21:08

比起內容,畫面更重要!加油!!!/images/emoticon/emoticon07.gif

pjchender iT邦新手 3 級 ‧ 2019-10-03 00:24:04 檢舉

畫面真得很重要啊,可是也花好多時間啊啊啊XDD

0
阿展展展
iT邦好手 1 級 ‧ 2019-10-10 04:26:49

功能完成之後,當然要美美的呈現阿

function work 正常之後 開始進行拉皮、化妝

然後...可能 拉皮的時間變成 function 的時間的兩倍...
/images/emoticon/emoticon26.gif

pjchender iT邦新手 3 級 ‧ 2019-10-10 11:45:48 檢舉

我覺得你真的說到我的心坎裡了/images/emoticon/emoticon06.gif
後面變成花很多間在處理資料XDD/images/emoticon/emoticon02.gif

看來您也是..文章很精美 內容很豐富
然後PO上來時
這邊調一下 那邊空一格
這個要對齊
恩...在多個表情好惹

恩...不行這邊要加一張圖

.
.
.

然後一天又過去了 感謝飛天小女警的努力 (X/images/emoticon/emoticon12.gif

0
falex
iT邦新手 5 級 ‧ 2019-11-01 03:32:58

...上述第二種直接 import SVG 圖片的方式實際上是取得該圖檔的路徑,但在 CodeSandbox 中該圖檔路徑會是錯誤的,僅而有破圖的情況...

好像不是路徑錯誤的問題,因為改用PNG格式的圖片就不會破圖了。

pjchender iT邦新手 3 級 ‧ 2019-11-05 11:19:18 檢舉

謝謝,這個部分我還不太確定實際上在 codesandbox 上會導致破圖的原因,若你有發現的話再歡迎和我分享!

0
samuelWang
iT邦新手 5 級 ‧ 2019-11-13 11:44:23

請問 我會遇到svg檔裡面的css顏色被覆蓋問題,後面載入的svg圖檔會有相同的css clsss名稱,導致畫面顏色出錯

看更多先前的回應...收起先前的回應...
pjchender iT邦新手 3 級 ‧ 2019-11-13 15:46:44 檢舉

你好,請問方便提供 codesandbox 的程式碼嗎?比較能夠知道情況。

發現真的要把svg檔裡面的class名稱改掉,才可以正確地呈現

pjchender iT邦新手 3 級 ‧ 2019-11-21 09:53:26 檢舉

哇!沒想到竟然有這種眉角!
看到你的作品了~哈哈,內容中如果還有發現什麼地方有誤或不夠嚴謹的話再麻煩跟我說。

我要留言

立即登入留言