iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 19
1
Modern Web

React - DOM界的彼方系列 第 19

Day19: React篇: TodoList程式 + 樣式(Style)

intro

本章的目標是在React元件中的套用樣式(Style),而且可以因狀態改變的作樣式變化,從上一章的程式碼作這些修改都是很簡單的,在React中使用CSS的樣式是個很重要而且有很多議論的主題,本章的重點在後面的說明部份,程式碼部份純粹是個簡單的範例而已。

這個程式最後的呈現結果,會是在網頁上出現一個文字輸入框,當你輸入文字後按下Enter鍵,就會把文字加到下面的列表中,每個項目用滑鼠點按一下,會觸發這個項目的樣式變化。就像下面的動態圖片這樣:

TodoList樣式元件展示

註: 本文章同步放置於Github庫的這裡,所有的程式碼也在裡面。

這支程式其實只是改了一下TodoList元件與TodoItem元件而已,因為整個應用程式的功能還很簡單,所以也就不套其它的樣式進來。

先從TodoList元件看,程式碼整體就請直接看程式檔案,這裡只有說明更動的這些地方而已。

一開始,我們在原本state中記錄的items陣列,在之前的程式中每個陣列成員中記錄一個字串值,現在把它擴充到一個物件值,除了text屬性對應原本的字串值,再多加了一個isCompleted屬性,代表這個項目是不是已經被標記為完成了,所以是個布林值,它的物件結構像下面這樣:

type Item = {
  text: string,
  isCompleted: boolean,
}

原先的handleKeyPress方法,是在按下文字輸入框的Enter鍵,然後加入新項目的程式碼,也要跟著變動,預設剛加入的待辦事項都是未完成的,也就是isCompleted: false,像下面這樣:

const aItem = {text: e.target.value, isCompleted: false }
const newItems = [aItem, ...this.state.items ]

原先的handleRemoveItem方法,現在要改成另一個方法,也就是當滑鼠點按每個項目時,只會作樣式的變化,並不會把這個項目從陣列中移除,下面這是新的handleStylingItem方法程式碼,當然在每個項目要指定的onItemClick屬性也要改成它:

//處理樣式變化其中一個陣列中成員的方法
handleStylingItem = (index: number) => {
  //拷貝一個新陣列
  const newItems = [...this.state.items]

  //切換isCompleted的布林值
  newItems[index].isCompleted = !newItems[index].isCompleted

  //整個陣列重新更新
  this.setState({
    items: newItems,
  })
}

其實這個方法只是切換在陣列成員中的isCompleted屬性,用的是否運算符(!)來切換布林值。這其中依然是用純粹函式的寫法,不會直接變動到原先的陣列值,而是用一個新的陣列來先作拷貝原有的陣列值,然後在這上面更動,之後再指定回去。

在render的部份,你可以看到TodoItem多了一個style屬性,這個屬性的指定值,將會由項目物件中的isCompleted值決定,style屬性將會是個物件值,這裡用的是內嵌樣式的寫法,本章的最後面有一些較為詳盡的說明。另外,onItemClick屬性也指定到新的處理方法handleStylingItem,程式碼如下:

{
  this.state.items.map((item, index) => {
    return <TodoItem
              key={index}
              style={item.isCompleted? {color: 'red', textDecoration: 'line-through'} : {color: 'green'}}
              text={item.text}
              index={index}
              onItemClick={this.handleStylingItem}
            />
  })
}

最後來看TodoItem中的改變,其實就多了一個style屬性,所以在Flow標記,以及propTypes都要加上這個屬性的檢查標記。style屬性是所有的HTML的DOM元素中都有的一個屬性,JSX最後會把物件值轉變為真實套用的內嵌樣式,下面是TodoItem元件的整個程式碼:

//@flow
import React from 'react'

const TodoItem = (props: {text: string, index: number, style: Object, onItemClick: Function}) => {

  const handleClick = () => {
        //實際上呼叫的是由上層元件從props.onItemClick傳入的方法(上層元件的方法)
        props.onItemClick(props.index)
    }

  return <li onClick={handleClick} style={props.style}>{props.text}</li>
}

//加入props的資料類型驗証
TodoItem.propTypes = {
  text: React.PropTypes.string.isRequired,
  index: React.PropTypes.number.isRequired,
  onItemClick: React.PropTypes.func,
  style: React.PropTypes.object,
}

//匯出TodoItem模組
export default TodoItem

其他的詳細說明

內嵌樣式與目前的網頁上的CSS使用方式

內嵌樣式(inline style)是一種很早期的對於DOM元素作樣式定義的撰寫風格,指的就是把CSS樣式的定義,直接插入到元素的style屬性中。長得大概會像下面這樣個範例:

<div style="width: 800px; margin: 1em auto; font: bold 1em/1.2 Verdana, Arial, Helvetica, sans-serif;">
    <div style="float: left; width: 400px; padding: 1em 2em; font-size: 0.9em;">
        some text...
    </div>
</div>

內嵌樣式(inline style)是一個明顯有許多缺點的作法,一看就知道它不容易撰寫也不容易維護。現今對於網頁上DOM元素的CSS樣式定義方式,都是採用分離出CSS定義檔案的作法,獨立出所有的CSS定義,然後對DOM元素使用id或class屬性來指定風格。例如你可以獨立把CSS樣式放在一個獨立的css檔案中:

<link rel="stylesheet" href="css/base.css" type="text/css" media="screen">

或是在HTML檔案中加入<style>標記來定義:

<style>
  #parent{
    width: 800px;
    font: bold 1em/1.2 Verdana, Arial, Helvetica, sans-serif;
  }

  .child{
    float: left;
    width: 400px;
    padding: 1em 2em;
  }
</style>

然後把原本的HTML元素中的屬性改為id或class,像下面這樣:

<div id="parent">
    <div class="child">
        some text...
    </div>
</div>

這樣的作法有很多優點,幾乎是現在所有網站採行的方式,例如以下幾個:

  • 容易集中、組織與管理CSS樣式,可以重覆使用相同的樣式
  • CSS定義可以進行壓縮,減少傳輸大小
  • CSS定義可以被瀏覽器快取,重覆使用,增加速度

不過在經歷過一段使用後,CSS仍然出現了一些本質上的問題,現在我們看到現今很多新的應用技術在這上面出現,例如LESS、SASS、Stylus之類的預先處理器(Preprocessor),PostCSS之類的後續處理器(Preprocessor)與CSS Module,這些技術的出現,都是為了要解決CSS目前面臨的問題。

現在的CSS的使用方式是有什麼問題?大致上有以下幾個:

  • 所有的東西都是全域的: 在CSS的定義中,所有的選擇子(Selectors)都是全域可見的,並沒有沒有全域(global)與本地(local)的差異,這代表要重覆使用同樣的樣式會變得不容易組織,而且也很容易DOM元素子層會嵌套到父母層的CSS定義。

  • CSS檔案愈來愈大: 因為網站的HTML DOM元素樣式愈來愈複雜,CSS通常也會隨著愈來愈大,CSS在進行刪除不必要的定義時會難以取捨,所以總是不斷在新增新的CSS定義,最佳化最後會變成一個令人頭痛的問題。

  • 相依性問題: 如果CSS檔案還有使用其他的外部的CSS,CSS的嵌套就會很容易互相影響到,相依性無法進行有效的管理。

  • 動態樣式: 動態樣式是一個與程式設計相關的議題,因為程式碼與CSS定義的分離後,只能在程式中使用id與class的變更來進行動態樣式的作法,分割得太清楚反而對於程式開發時並不直觀,在測試與除錯時也會造成困難點。

註: 在這份React: CSS in JS簡報中有列出更多的總共7項問題

在JavaScript中的CSS

JavaScript中原本就有完整的定義CSS樣式的許多介面(物件)、屬性與方法,只是之前很少被使用,能看到的有可能只被包在工具函式庫之中。在現在HTML、CSS、JavaScript三者分離,各自撰寫定義檔案,是個簡單而有效率的作法。也因為要分離的很清楚,在JavaScript程式之中很少會直接定義CSS樣式,也就是使用內嵌的樣式。

JavaScript中原本就有一個對應HTML元素實作HTMLElement介面(物件),裡面就有一個style屬性,可以直接定義某個網頁上的元素的樣式,不過它使用了經過調整的CSS屬性名稱,因為JavaScript把CSS的定義屬性名稱要當作物件的屬性識別名來使用,所以與原本的CSS中的定義名稱有些差異,不過9成以上都是改成小駝峰式的命名法而已,例如原本的background-image改為backgroundImage這樣,只有少數幾個例外,像float需要改為cssFloat

註: 在JavaScript中關於CSS與HTML的相關標準,是來自W3C組織所訂定,MDN把它們都歸在Web APIs裡,裡面的內容也非常的很多,有些新的功能是瀏覽器品牌與版本的支援程度都很散亂。

利用這些CSS屬性名稱就可以寫出套用inline style(內嵌樣式)的程式碼,像下面這樣的簡單範例:

function alterStyle(elem) {
  elem.style.background = 'green'
}

function resetStyle(elemId) {
  elem = document.getElementById(elemId)
  elem.style.background = 'white'
}

那對於CSS中的class(類別)又要如何套用?

CSS類別要使用的是HTMLElement介面的上層介面Element,其中有一個className的屬性,給定要套用的CSS類別名稱就行了。用法與style屬性並都差不了多少,但你要事先定義好CSS類別的名稱,像下面的範例這樣:

function changeBgToRed(){
  document.getElementById('d').className = 'bg-red'
}

function changeBgToGreen(){
  document.getElementById('d').className = 'bg-green'
}

註: 會用className作為CSS class(類別)的屬性名稱是因為與原本JS中的關鍵字class相衝突。另一個for屬性是用在label標記上的,因為也與JS中的關鍵字衝突,所以要改用htmlfor

不過因為className屬性的用法過於簡單,它可以指定單一個CSS類別識別名字串,或是以空格分隔的多個CSS類別識別名字串,就這樣而已。後來又有一個新的屬性classList,它可以完全取代className屬性,除了也可以套用多個CSS類別,裡面提供了下面幾個方法,可以更加彈性的套用、移除、切換CSS類別的使用:

  • add: 加入CSS類別名稱,如果已經有了就忽略
  • remove: 移除CSS類別名稱
  • toggle: 在add與remove之間中切換
  • contains: 檢查是否有包含某個CSS類別
function addBoldText(){
  document.getElementById('d').classList.add('bold-text')
}

function removeBoldText(){
  document.getElementById('d').classList.remove('bold-text')
}

function toggleBoldText(){
  document.getElementById('d').classList.toggle('bold-text')
}

classList屬性相當的容易使用,唯一的缺點就是IE瀏覽器對它相當不友善,要在 IE10 以上才能使用。

註: IE9包含以下版本的解決方案請參考Code with classList does not work in IE?

註: 截止目前為止(2016.12),React並不支援classList屬性,但React中的className屬性一樣可以用空格來同時指定多個CSS類別。你可以改用classnames

React元件樣式

內嵌樣式(inline style)

React官網建議這對於元件會採用內嵌樣式的作法完全不意外,React元件是一種元件設計的樣式寫法,為了讓每個元件的外觀樣式獨立,自然會採用內嵌樣式的作法。再者,樣式的改變通常與程式的邏輯有關,也就是以元件的狀態或行為對應的樣式。

內嵌樣式(inline style)上面已經有說明過了,有許多初學者會誤以為這是React自創的寫法,但實際上它是JavaScript中原本一種定義CSS樣式的方式。內嵌樣式可以使用程式碼提供動態的資料,用這種方式可以依不同的情況來套用不同的CSS樣式,一個簡單的範例如下:

const divStyle = {
  color: 'white',
  backgroundImage: 'url(' + imgUrl + ')',
  WebkitTransition: 'all', // 這是新的CSS樣式的定義,注意它的開頭是用大寫'W'
  msTransition: 'all' // 'ms'是指提供商的前綴字
}

ReactDOM.render(<div style={divStyle}>Hello World!</div>, mountNode)

className屬性

React中的className屬性可以用空格來同時指定多個CSS類別,你可以用空格來區分多個不同的CSS類別,用一般的字串值或樣版字串都可以,例如下面的程式碼:

const myClassNames = 'class1 class2 class3'
const cssClassNames = `${error} form-control input-lg round-lg`

<li className={myClassNames}>foo</li>
<input cssClassNames={cssClasses} />

classnames專案

雖然React中提供了HTML元素可使用的className屬性,但它就是像上面說的那個來自Element介面的className屬性。React中並沒有classList這個屬性可用,也沒有裡面的好用方法,為什麼不提供當然是因為這些方法,因為它們都會直接影響到實體DOM元素。在之前React提供了一個cx工具來作類似classList中方法的事情,但現在cx已經棄用,要改用classnames專案來作處理,不過說實在一樣是用來組合(join)或組織CSS類別的作法,classnamesclassList中的方法彈性大多了。

styled-components專案

JavaScript提供的CSS介面畢竟對前端開發工程師來說,是一個很不習慣的語法,雖然它具有程式語言的一些運算的能力,但在社群間有一些新的專案,致力於開發出可以在React中直接寫CSS語法的一些解決方案。styled-components是一個最近才開始的專案,它可以讓你直接在程式碼裡使用一般的CSS定義方式,它用了一個新的ES6特性,稱為樣版字串或樣版字面文字(Template literals),也可以使用在React Native專案上。

最佳實踐

現今CSS中的問題基本上與React關係並不大,React目的自然是把HTML、CSS、JS三者,全部最好都合在React元件中的程式碼裡來撰寫,React官方說明是用內嵌樣式,並不代表開發者就得這樣作,它並不是給了一個最終的答案,而是問了一個好問題,社群上對這個議題提出了許多解決的方案,有人認為React元件用內嵌樣式是絕對合理的,應該就是要這樣作,但也有人認為這個方式是不好的,各種意見都有。

那對你來說,該如何選擇,我先在下面列出幾種CSS樣式在React中的使用選擇:

使用現成的CSS框架

像Bootstrap或Semantic UI這些眾多的現成CSS框架,都可以使用className屬性來指定給React中的元件或DOM元素,直接用就可以了,你不需要另外學什麼特別的語法。

不過,像Bootstrap中有很多動態的效果,都是使用jQuery函式庫為基礎來製作的,它在某些情況也會處理網面上的真實DOM元素,這會與React中的虛擬DOM作法相衝突,建議如果要使用的話,不要使用Bootstrap裡面的這些有JavaScript的元件。

大部份知名的CSS框架都已經有對應的專為React開發所調整過的函式庫,以長期的開發考量來說,你應該用這些專為React設計的框架或函式庫,下面有提供一個列表。

直接使用CSS(或像SASS的預處理器)

你可以用自己撰寫CSS或SASS,然後套用到React之中,Webpack可以搭配使用css-loaderstyle-loader或是sass-loader,讓這些CSS(或SASS)定義檔案,在開發時用import語句匯入到JavaScript程式碼檔案之中使用。

當你在使用create-react-app建立一個新的React專案時,你可能已經發現到裡面預設的範例專案,就有使用這種作法,在其他的樣版文件中都已經幫你作完複雜的設定,所以你也可以照著這樣用。下面是預設的範例的程式碼:

使用為React開發的UI框架

這有很多選擇,現在有很多專案都是在開發專門給React使用的整套UI框架,例如:

使用這些UI框架自然有一些好處,應用程式的整體樣式的設計會一致,而且它們多半都已經設計好很多React相關的UI元件,直接使用就可以了。要說缺點的話,大概就是會被框架或函式庫綁住,因為每套框架設計的方式並不相同,如果要用到其它沒提供的元件你要自己想辦法把樣式套上。

CSS Modules

CSS Module是一個很常被使用的CSS處理技術概念,它可以藉由很多工具來實現,首先它會把原本的CSS定義轉化為更具彈性與可程式化的Interoperable CSS(可交互操作的CSS, ICSS),這是一種基於CSS的超集合,原本的CSS檔案(如果是引用外部檔案的方式),可以在裡面進行更多的定義,例如定義類別合成、全域或本地的作用域等等,這些檔案也可以進行模組化、以及把類別轉換為JavaScript的變數。

CSS Module使用Webpack加上css-loader就可以使用,大部份React的樣版文件,都已經設定與安裝好它。針對React元件還有專門為它打造的React CSS Modules,如果有需要可以再安裝,提供更多彈性的運用。

不過,CSS Module算是一個為了CSS in JavaScript產生問題的解決方案,它可能對初學者來說有點複雜,使用方式還需要再學習。

使用內嵌樣式(inline style)

React社群中已經發展出許多專門使用內嵌樣式的輔助函式庫,例如最有名的是Radium,它大概已經把內嵌樣式發展到一個極致,是一個完全內嵌樣式的解決方案。


那麼對你來說是要選擇哪一種?

基本上,上面是以每種解決方案來區分,但並沒有說限定你一定只能用其中一種,你也可以混用不同種的方式。以真實的開發情況來說,我們會把用到CSS或樣式的情況區分為以下三個種情況:

  1. 版面(Layout) - 在應用中的每個元件的排版位置,或是RWD(響應式網頁設計)等等。
  2. 外觀(Appearance) - 元件或DOM元素看起來的樣子
  3. 狀態或行為的變化(state & Behavior) - 元件或DOM元素因使用者操作或狀態改變時,造成的樣式變化。

這裡面最沒有爭議的是第3種,因為它與React元件中的程式碼的邏輯關係很密切,所以一般就直接用在React中的內嵌樣式來解決最直覺。

第1種情況,你可以直接使用現成的許多版面的CSS解決方案,例如Bootstrap或有更簡單的,它們都可以運作得很好,只要你在元件外用個<div>框住,而且只用它們排版的部份,大部份都是可以正常運作。當然,如果你希望是使用有一致性的,多樣化的與動態的元件,你可以用以這些框架為基礎,專門為React打造的延伸框架。

第2種情況,這要視你的應用與團隊而決定,因為整個應用程式的元件外觀應該要有一致性,或許專門為它寫一個獨立的CSS是一種方式,或是全部採用內嵌樣式的作法。當然,在不使用現有框架中的特效元件的情況下,只套用外觀樣式的部份,也是可以的。例如Bootstrap就有提供線上自訂服務,選擇你只要使用的某些功能。

註: 以上的說明參考這篇stackoverflow問答中的回答。

參考資料


上一篇
Day 18: React篇: JSX語法指引
下一篇
Day 20: React篇: TodoList程式改造 => TodoApp
系列文
React - DOM界的彼方30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言