一杯茶的時間,上手 React 框架開發

tuture發表於2020-04-13

React(也被稱為 React.js 或者 ReactJS)是一個用於構建使用者介面的 JavaScript 庫。起源於 Facebook 內部專案,最初用來架設 Instagram 的網站,並於 2013 年 5 月開源。React 效能較高,並且它的宣告式、元件化特性讓編寫程式碼變得簡單,隨著 React 社群的發展,越來越多的人投入 React 的學習和開發,使得 React 不僅可以用來開發 Web 應用,還能開發桌面端應用,TV應用,VR應用,IoT應用等,因此 React 還具有一次學習,隨處編寫的特性。本教程將帶你快速入門 React 開發,通過 20-30 分鐘的學習,你不僅可以瞭解 React 的基礎概念,而且能開發出一個待辦事項小應用,還在想什麼了?馬上學起來吧!本文所有程式碼已放在 GitHub 倉庫中。

此教程屬於 React 前端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵我們繼續創作出更好的教程,持續更新中~

Hello, World

我們將構建什麼?

在這篇教程中,我們將展示給你如何使用 React 構建一個待辦事項應用,下面最終專案的展示成果:

你也可以在這裡看到我們最後構建的結果:最終結果。如果你現在對程式碼還不是很理解,或者你還不熟悉程式碼語法,別擔心!這篇教程的目標就是幫助你理解 React 和它的語法。

我們推薦你在繼續閱讀這篇教程之前先熟悉一下這個待辦事項,你甚至可以嘗試新增幾個待辦事項!你可能注意到當你新增了2個待辦事項之後,會出現不同的顏色;這就是 React 中條件渲染的魅力。

當你熟悉了這個待辦事項之後你就可以關閉它了。在這篇教程的學習中,我們將從一個 Hello World 程式碼模板開始,然後帶領你初始化開發環境,這樣你就可以開始構建這個待辦事項了。

你將學到什麼?

你將學習所有 React 的基礎概念,其中又分為三個部分:

  • 編寫元件相關:包括 JSX 語法、Component、Props
  • 元件的互動:包括 State 和生命週期
  • 元件的渲染:包括列表和 Key、條件渲染
  • 和 DOM & HTML 相關:包括事件處理、表單。

前提條件

我們假設你熟系 HTML 和 JavaScript,但即使你是從其他程式語言轉過來的,你也能看懂這篇教程。我們還假設你對一些程式語言的概念比較熟悉,比如函式、物件、陣列,如果對類瞭解就更好了。

如果你需要複習 JavaScript,我們推薦你閱讀這篇指南。你可能注意到了我們使用了一些 ES6 的特性 – 一個最近的 JavaScript 版本。在這篇教程,我們會使用 arrow functionsclasses,和 const。你可以使用 Babel REPL 來檢查 ES6 程式碼編譯之後的結果。

環境準備

首先準備 Node 開發環境,訪問 Node 官方網站下載並安裝。開啟終端輸入如下命令檢測 Node 是否安裝成功:

node -v # v10.16.0
npm -v # 6.9.0

注意

Windows 使用者需要開啟 cmd 工具,Mac 和 Linux 是終端。

如果上面的命令有輸出且無報錯,那麼代表 Node 環境安裝成功。接下來我們將使用 React 腳手架 – Create React App(簡稱 CRA)來初始化專案,同時這也是官方推薦初始化 React 專案的最佳方式。

在終端中輸入如下命令:

npx create-react-app my-todolist

等待命令執行完成,接著輸入如下命令開啟專案:

cd my-todolist && npm start

CRA 會自動開啟專案並開啟瀏覽器,你應該可以看到下面的結果:

??? 恭喜你!成功建立了第一個 React 應用!

現在 CRA 初始化的專案裡有很多無關的內容,為了開始接下來的學習,我們還需要做一點清理工作。首先在終端中按 ctrl + c 關閉剛剛執行的開發環境,然後在終端中依次輸入如下的命令:

# 進入 src 目錄
cd src

# 如果你在使用 Mac 或者 Linux:
rm -f *

# 或者,你在使用 Windows:
del *

# 然後,建立我們將學習用的 JS 檔案
# 如果你在使用 Mac 或者 Linux:
touch index.js

# 或者,你在使用 Windows
type nul > index.js

# 最後,切回到專案目錄資料夾下
cd ..

此時如果在終端專案目錄下執行 npm start 會報錯,因為我們的 index.js 還沒有內容,我們在終端中使用 ctrl +c 關閉開發伺服器,然後使用編輯器開啟專案,在剛剛建立的 index.js 檔案中加入如下程式碼:

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  render() {
    return <div>Hello, World</div>;
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

我們看到 index.js 裡面的程式碼分為三個部分。

首先是一系列導包,我們匯入了 react 包,並命名為 React,匯入了 react-dom 包並命名為 ReactDOM。對於包含 React 元件(我們將在之後講解)的檔案都必須在檔案開頭匯入 React。

然後我們定義了一個 React 元件,命名為 App,繼承自 React.Component,元件的內容我們將會在後面進行講解。

接著我們使用 ReactDOM 的 render 方法來渲染剛剛定義的 App 元件,render方法接收兩個引數,第一個引數為我們的 React 根級元件,第二個引數接收一個 DOM 節點,代表我們將把和 React 應用掛載到這個 DOM 節點下,進而渲染到瀏覽器中。

注意

上面程式碼的三個部分中,第一部分和第三部分在整篇教程中是不會修改的,同時在編寫任意 React 應用,這兩個部分都是必須的。後面所有涉及到的程式碼修改都是關於第二部分程式碼的修改,或者是在第一部分到第三部分之間插入或刪除程式碼。

儲存程式碼,在終端中使用 npm start 命令開啟開發伺服器,現在瀏覽器應該會顯示如下內容:

準備工作已經就緒!

你可能對上面的程式碼細節還不是很清楚,別擔心,我們將馬上帶你領略 React 的神奇世界!

JSX 語法

首先我們來看一下 React 引以為傲的特性之一 – JSX。它允許我們在 JS 程式碼中使用 XML 語法來編寫使用者介面,使得我們可以充分的利用 JS 的強大特性來操作使用者介面。

一個 React 元件的 render 方法中 return 的內容就為這個元件所將渲染的內容。比如我們現在的程式碼:

render() {
    return <div>Hello, World</div>;
}

這裡的 <div>Hello, World</div> 是一段 JSX 程式碼,它最終會被 Babel 轉譯成下面這段 JS 程式碼:

React.createElement(
  'div',
  null,
  'Hello, World'
)

React.createElement() 接收三個引數:

  • 第一個引數代表 JSX 元素標籤。
  • 第二個引數代表這個 JSX 元素接收的屬性,它是一個物件,這裡因為我們的 div 沒有接收任何屬性,所以它是 null
  • 第三個引數代表 JSX 元素包裹的內容。

React.createElement() 會對引數做一些檢查確保你寫的程式碼不會產生 BUG,它最終會建立一個類似下面的物件:

{
  type: 'div',
  props: {
    children: 'Hello, World'
  }
};

這些物件被稱之為 “React Element”。你可以認為它們描述了你想要在螢幕上看到的內容。React 將會接收這些物件,使用它們來構建 DOM,並且對它們進行更新。

注意

我們推薦你使用 “Babel” 檢視 JSX 的編譯結果。

App 元件最終返回這段 JSX 程式碼,所以我們使用 ReactDOM 的 render 方法渲染 App 元件,最終顯示在螢幕上的就是 Hello, World" 內容。

JSX 作為變數使用

因為 JSX 最終會被編譯成一個 JS 物件,所以我們可以把它當做一個 JS 物件使用,它享有和一個 JS 物件同等的地位,比如可以將其賦值給一個變數,我們修改上面程式碼中的 render 方法如下:

render() {
  const element = <div>Hello, World</div>;
  return element;
}

儲存程式碼,我們發現瀏覽器中渲染的內容和我們之前類似。

在 JSX 中使用變數

我們可以使用大括號 {} 在 JSX 中動態的插入變數值,比如我們修改 render 方法如下:

render() {
  const content = "World";
  const element = <div>Hello, {content}</div>;
  return element;
}

儲存程式碼,發現瀏覽器中效果依然不變。

JSX 中使用 JSX

我們可以在 JSX 中再包含 JSX,這樣我們編寫任意層次的 HTML 結構:

render() {
    const element = <li>Hello, World</li>
    return (
      <div>
        <ul>
          {element}
        </ul>
      </div>
    )
  }

JSX 中新增節點屬性

我們可以像在 HTML 中一樣,給元素標籤加上屬性,只不過我們需要遵守駝峰式命名法則,比如在 HTML 上的屬性 data-index 在 JSX 節點上要寫成 dataIndex

const element = <div dataIndex="0">Hello, World</div>;

注意

在 JSX 中所有的屬性都要更換成駝峰式命名,比如 onclick 要改成 onClick,唯一比較特殊的就是 class,因為在 JS 中 class 是保留字,我們要把 class 改成 className

const element = <div className="app">Hello, World</div>;

實戰

我們使用這一節講解的 JSX 知識,來繼續完成我們的待辦事項應用。

在編輯器中開啟 src/index.js ,對 App 元件做如下改變:

class App extends React.Component {
  render() {
    const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];
    return (
      <ul>
        <li>Hello, {todoList[0]}</li>
        <li>Hello, {todoList[1]}</li>
        <li>Hello, {todoList[2]}</li>
        <li>Hello, {todoList[3]}</li>
      </ul>
    );
  }
}

可以看到,我們使用 const 定義了一個 todoList 陣列常量,並且在 JSX 中使用 {} 進行動態插值,插入了陣列的四個元素。

最後儲存程式碼,瀏覽器中的效果應該是這樣的:

提示

無需關閉剛才使用 npm start 開啟的開發伺服器,修改程式碼後,瀏覽器中的內容將會自動重新整理!

你可能注意到了我們手動獲取了陣列的四個值,然後逐一的用 {} 語法插入到 JSX 中並最終渲染,這樣做還比較原始,我們將在後面列表和 Key 小節中簡化這種寫法。

在這一小節中,我們瞭解了 JSX 的概念,並且實踐了相關的知識。我們還提出了元件的概念,但是並沒有深入講解它,在下一小節中我們將詳細地講解元件的知識。

Component

React 的核心特點之一就是元件化,即我們將巨大的業務邏輯拆分成一個個邏輯清晰的小元件,然後通過組合這些元件來完成業務功能。

React 提供兩種元件寫法:1)函式式元件 2)類元件。

函式式元件

在 React 中,函式式元件會預設接收一個 props 引數,然後返回一段 JSX:

function Todo(props) {
  return <li>Hello, 圖雀</li>;
}

關於 props 我們將在下一節中講解。

類元件

通過繼承自 React.Component 的類來代表一個元件。

class Todo extends React.Component {
  render() {
    return <li>Hello, 圖雀</li>;
  }
}

我們發現,在類元件中,我們需要在 render 方法裡面返回需要渲染的 JSX。

元件組合

我們可以組合不同的元件來完成複雜的業務邏輯:

class App extends React.Component {
  render() {
    return (
      <ul>
        <Todo />
        <Todo />
      </ul>
    );
  }
}

在上面的程式碼中,我們在類元件 App 中使用了我們之前定義的 Todo 元件,我們看到,元件以 <Component /> 的形式使用,比如 Todo 元件使用時為 <Todo />,我們在 Todo 元件沒有子元件時使用這種寫法;當 Todo 元件需要包含子元件時,我們需要寫成下面的形式:

class App extends React.Component {
  render() {
    return (
      <ul>
        <Todo>Hello World</Todo>
        <Todo>Hello Tuture</Todo>>
      </ul>
    );
  }
}

元件渲染

我們在第一節中講到,通過 ReactDOM.render 方法接收兩個引數:1)根元件 2) 待掛載的 DOM 節點,可以將元件的內容渲染到 HTML 中。

ReactDOM.render(<App />, document.getElementById('root'));

實戰

我們運用在這一節中學到的元件知識來繼續完成我們的待辦事項應用。我們編寫一個 Todo 類元件,用於代表單個待辦事項,然後在 App 類元件中使用 Todo 元件。

開啟 src/index.js 檔案,實現 Todo 元件,並調整 App 元件程式碼如下:

class Todo extends React.Component {
  render() {
    return <li>Hello, 圖雀</li>;
  }
}

class App extends React.Component {
  render() {
    const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];
    return (
      <ul>
        <Todo />
        <Todo />
        <Todo />
        <Todo />
      </ul>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

儲存程式碼,然後你應該可以在瀏覽器中看到如下結果:

你可能注意到我們暫時沒有使用之前定義的 todoList 陣列,而是使用了四個相同的 Todo 元件,我們使用繼承自 React.Component 類的形式定義 Todo 元件,然後在元件的 render 中返回了 <li>Hello, 圖雀</li>,所以最終瀏覽器中會渲染四個 "Hello, 圖雀"。並且因為 Todo 元件不需要包含子元件,所以我們寫成了 <Todo /> 的形式。

現在4個待辦事項都是一樣的內容,這有點單調,你可能會想,如果可以像呼叫函式那樣可以通過傳參對元件進行個性化定製就好了,你的想法是對的!我們將在下一節中引出 props,它允許你給元件傳遞內容,從而進行個性化內容定製。

Props

React 為元件提供了 Props,使得在使用元件時,可以給元件傳入屬性進行個性化渲染。

函式式元件中使用 Props

函式式元件預設接收 props 引數,它是一個物件,用於儲存父元件傳遞下來的內容:

function Todo(props) {
  return (
    <li>Hello, {props.content}</li>
  )
}

<Todo content="圖雀" />

我們給 Todo 函式式元件傳遞了一個 content 屬性, 它的值為 "圖雀" ,所有傳遞的屬性都會合並進 props 物件中,然後傳遞給 Todo 元件,這裡 props 物件是這樣的 props = { content: "圖雀" } ,如果我們再傳遞一個屬性:

<Todo content="圖雀" from="圖雀社群" />

最終 props 物件就會變成這樣:props={ content: "圖雀", from: "圖雀社群" }

注意

如果給元件傳遞 key 屬性是不會併入 props 物件中的,所以我們在子元件中也取不到 key 屬性,我們將在列表和 Key 一節 中詳細講解。

類元件中使用 Props

類元件中基本和函式式元件中的 Props 保持一致,除了是通過 this.props 來獲取父元件傳遞下來的屬性內容:

class Todo extends React.Component {
  render() {
    return <li>Hello, {this.props.content}</li>;
  }
}

<Todo content="圖雀" />

實戰

我們運用這一節中學到的 Props 知識來繼續完成我們的待辦事項應用。

開啟 src/index.js 檔案,分別調整 Todo 和 App 元件,修改後程式碼如下:

import React from "react";
import ReactDOM from "react-dom";

class Todo extends React.Component {
  render() {
    return <li>Hello, {this.props.content}</li>;
  }
}

class App extends React.Component {
  render() {
    const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];
    return (
      <ul>
        <Todo content={todoList[0]} />
        <Todo content={todoList[1]} />
        <Todo content={todoList[2]} />
        <Todo content={todoList[3]} />
      </ul>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

注意到我們又重新開始使用之前定義的 todoList 陣列,然後給每個 Todo 元件傳遞一個 content 屬性,分別賦值陣列的每一項,最後在 Todo 元件中使用我們傳遞下來的 content 屬性。

儲存修改的內容,你應該可以在瀏覽器中看到如下的內容:

可以看到,我們的內容又回來了,和我們之前在 JSX 一節 中看到的內容一樣,但是這一次我們成功使用了元件來渲染接收到的 Props 內容。

State 和生命週期

React 通過給類元件提供 State 來創造互動式的內容 – 即內容可以在渲染之後發生變化。

定義 State

通過在類元件中新增 constructor 方法,並在其中定義和初始化 State:

constructor(props) {
    super(props);

    this.state = {
      todoList: ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"]
    };
  }

這裡 constructor 方法接收的 props 屬性就是我們在上一節中講到的那個 props;並且 React 約定每個繼承自 React.Component 的元件在定義 constructor 方法時,要在方法內首行加入 super(props)

接著我們 this.state 來定義元件的 state,並使用 { todoList: ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"] } 物件來初始化 state。

使用 State

我們可以在一個元件中的多處地方通過 this.state 的方式來使用 state,比如我們在這一節中將講到的生命週期函式中,比如在 render 方法中:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todoList: ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"]
    };
  }

  render() {
    return (
      <ul>
        <Todo content={this.state.todoList[0]} />
        <Todo content={this.state.todoList[1]} />
        <Todo content={this.state.todoList[2]} />
        <Todo content={this.state.todoList[3]} />
      </ul>
    );
  }
}

我們通過 this.state.todoList 可以獲取我們在 constructor 方法中定義的 state,可以看到,我們使用 this.state.todoList[0] 的方式替代了之前的 todoList[0]

更新 State

我們通過 this.setState 方法來更新 state,從而使得網頁內容在渲染之後還能變化:

this.setState({ todoList: newTodoList });

注意

關於 this.setState 我們需要注意以下幾點:

1)這裡不能夠通過直接修改 this.state 的方式來更新 state:

// 錯誤的
this.state.todoList = newTodoList;

2)State 的更新是合併更新的:

比如原 state 是這樣的:

constructor(props) {
  super(props);
  this.state = {
    todoList: [],
    nowTodo: '',
  };
}

然後你呼叫 this.setState() 方法來更新 state:

this.setState({ nowTodo: "Hello, 圖雀" });

React 將會合並更新,即將 nowTodo 的新內容合併進原 this.state,當更新之後,我們的 this.state 將會是下面這樣的:

this.state = { todoList: [], nowTodo: "Hello, 圖雀" };

不會因為只單獨設定了 nowTodo 的值,就將 todoList 給覆蓋掉。

生命週期函式

React 提供生命週期函式來追蹤一個元件從建立到銷燬的全過程。主要包含三個方面:

  • 掛載(Mounting)
  • 更新(Updating)
  • 解除安裝(Unmounting)

一個簡化版的生命週期的圖示是這樣的:

注意

React 生命週期相對而言比較複雜,我們這裡不會詳細講解每個部分,上面的圖示使用者可以試著瞭解一下,對它有個大概的印象。

檢視完整版的生命週期圖示請參考這個連結:點選檢視

這裡我們主要講解掛載和解除安裝裡面常用的生命週期函式。

掛載

其中掛載中主要常用的有三個方法:

  • constructor()
  • render()
  • componentDidMount()

constructor() 在元件建立時呼叫,如果你不需要初始化 State ,即不需要 this.state = { ... } 這個過程,那麼你不需要定義這個方法。

render() 方法是掛載時用來渲染內容的方法,每個類元件都需要一個 render 方法。

componentDidMount() 方法是當元件掛載到 DOM 節點中之後會呼叫的一個方法,我們通常在這裡發起一些非同步操作,用於獲取伺服器端的資料等。

解除安裝

解除安裝只有一個方法:

  • componentWillUnmount()

componentWillUnmount 是當元件從 DOM 節點中解除安裝之前會呼叫的方法,我們一般在這裡面銷燬定時器等會導致記憶體洩露的內容。

實戰

我們運用在這一節中學到的 State 和生命週期知識來繼續完成我們的待辦事項應用。

開啟 src/index.js,修改程式碼如下:

import React from "react";
import ReactDOM from "react-dom";

const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];

class Todo extends React.Component {
  render() {
    return <li>Hello, {this.props.content}</li>;
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todoList: []
    };
  }

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({
        todoList: todoList
      });
    }, 2000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    return (
      <ul>
        <Todo content={this.state.todoList[0]} />
        <Todo content={this.state.todoList[1]} />
        <Todo content={this.state.todoList[2]} />
        <Todo content={this.state.todoList[3]} />
      </ul>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

可以看到我們主要改動了五個部分:

  • 將 todoList 移動到元件外面。
  • 定義 constructor 方法,並且通過設定 this.state = { todoList: [] } 來初始化元件的 State,這裡我們將 todoList 初始化為空陣列。
  • 新增 componentDidMount 生命週期方法,當元件掛載到 DOM 節點之後,設定一個時間為 2S 的定時器,並賦值給 this.timer,用於在元件解除安裝時銷燬定時器。等到 2S 之後,使用 this.setState({ todoList: todoList }) 來使用我們剛剛移動到元件外面的 todoList 來更新元件的 this.state.todoList
  • 新增 componentWillUnMount 生命週期方法,在元件解除安裝時,通過 clearTimeout(this.timer) 來清除我們之前設定的定時器,防止出現記憶體洩露。

儲存修改的程式碼,我們應該會看到瀏覽器中有一個內容更新的過程,在元件剛剛建立並掛載時,瀏覽器螢幕上應該是這樣的:

因為我們在 this.state 初始化時,將 todoList 設定為了空陣列,所以一開始 "Hello" 後面的 this.props.content 內容為空,我們就出現了四個 "Hello, "

然後當過了 2S 之後,我們可以看到熟悉的內容出現了:

因為在過了 2S 之後,我們在定時器的回撥函式中將 todoList 設定為了定義在元件外面的那個 todoList 陣列,它有四個元素,所以顯示在瀏覽器上面的內容又是我們之前的樣子。

恭喜你!成功建立了自己第一個互動式的元件!

讀到這裡,你有可能有點累了,試著離開座椅,活動活動,喝杯咖啡,精彩稍後繼續 ?

列表和 Key

目前我們有四個 Todo 元件,我們是一個一個取值然後渲染,這顯得有點原始,並且不可擴充套件,因為當我們的 todoList 陣列很大的時候(比如 100 個元素),一個一個獲取就顯得不切實際了,這個時候我們就需要迴圈介入了。

渲染元件列表

JSX 允許我們渲染一個列表:

render() {
    const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];

    // 請注意:我們這裡在 `map` 遍歷時用了箭頭函式簡潔返回寫法,直接用圓括號`()` 包裹需要返回的 `Todo` 元件,後面也是如此
    const renderTodoList = todoList.map((todo) => (
      <Todo content={todo} />
    ));
    return (
      <ul>
        {renderTodoList}
      </ul>
    );
  }

我們通過對 todoList 進行 map 遍歷,返回了一個 Todo 列表,然後使用 {} 插值語法渲染這個列表。

當然我們可以在 JSX 中使用表示式,所以上面的程式碼可以寫成這樣:

render() {
    const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];
    return (
      <ul>
        {todoList.map((todo) => (
          <Todo content={todo} />
        ))}
      </ul>
    );
  }

加上 Key

React 要求給列表中每個元件加上 key 屬性,用於標誌在列表中這個元件的身份,這樣當列表內容進行了修改:增加或刪除了元素時,React 可以根據 key 屬性高效的對列表元件進行建立和銷燬操作:

render() {
    const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];
    return (
      <ul>
        {todoList.map((todo, index) => (
          <Todo content={todo} key={index} />
        ))}
      </ul>
    );
  }

這裡我們使用了列表的 index 作為元件的 key 值,React 社群推薦的最佳實踐方式是使用列表資料元素的唯一識別符號作為 key 值,如果你的資料是來自資料庫獲取,那麼列表元素資料的主鍵可以作為 key

這裡的 key 值不會作為 props 傳遞給子元件,React 會在編譯元件時將 key 值從 props 中排除,即最終我們的第一個 Todo 元件的 props 如下:

props = { content: "圖雀" }

而不是我們認為的:

props = { content: "圖雀", key: 0 }

實戰

我們運用這一節學到的列表和 Key 的知識來繼續完成我們的待辦事項應用。

開啟 src/index.js,程式碼修改如下:

import React from "react";
import ReactDOM from "react-dom";

const todoList = ["圖雀", "圖雀寫作工具", "圖雀社群", "圖雀文件"];

class Todo extends React.Component {
  render() {
    return <li>Hello, {this.props.content}</li>;
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todoList: []
    };
  }

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({
        todoList: todoList
      });
    }, 2000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    return (
      <ul>
        {this.state.todoList.map((todo, index) => (
          <Todo content={todo} key={index} />
        ))}
      </ul>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

可以看到,我們將之前的手動獲取元素渲染改成了使用內嵌表示式的方式,通過對 this.state.todoList 列表進行 map 操作生成Todo 元件列表,然後使用列表的 index 作為元件的 key 值,最後渲染。

儲存內容,檢視瀏覽器裡面的內容,我們可以看到內容會有一個變化的過程,一開始是這樣的:

你會發現一片空白,然後過了 2S 變成了下面這樣:

這是因為,一開始 this.state.todoList 在 constructor 中初始化時是空陣列, this.state.todoList 進行 map 操作時返回空陣列,所以我們的瀏覽器中沒有內容,當元件掛載之後,等待 2S,我們更新 this.state.todoList 內容,就看到瀏覽器裡面獲得了更新。

條件渲染

在 React 中,我們可以根據不同的情況,渲染不同的內容,這也被成為條件渲染。

if-else 條件渲染

render() {
    if (this.props.content === "圖雀") {
      return <li>你好, {this.props.content}</li>;
    } else {
      return <li>Hello, {this.props.content}</li>;
    }
  }

在上面的程式碼中,我們判斷 this.props.content 的內容,當內容為 "圖雀" 時,我們渲染 "你好, 圖雀",對於其他內容我們依然渲染 "Hello, 圖雀"

三元表示式條件渲染

我們還可以直接在 JSX 中使用三元表示式進行條件渲染:

render() {
    return this.props.content === "圖雀"? (
      <li>你好, {this.props.content}</li>
    ) : (
      <li>Hello, {this.props.content}</li>
    );
  }

當然三元表示式還可以用來條件渲染不同的 React 元素屬性:

render() {
    return (
      <li className={this.state.isClicked ? 'clicked' : 'notclicked'}>Hello, {this.props.content}</li>
    )
  }

上面我們判斷元件的 this.state.isClicked 屬性,如果 this.state.isClicked 屬性為 true,那麼我們最終渲染 "clicked" 類:

render() {
    return (
      <li className="clicked"}>Hello, {this.props.content}</li>
    )
  }

如果 this.state.isClickedfalse,那麼最終渲染 notclicked 類:

render() {
    return (
      <li className="notclicked">Hello, {this.props.content}</li>
    )
  }

實戰

我們運用本節學到的知識繼續完成我們的待辦事項應用。

開啟 src/index.js ,對 Todo 和 App 元件作出如下修改:

class Todo extends React.Component {
  render() {
    if (this.props.index % 2 === 0) {
      return <li style={{ color: "red" }}>Hello, {this.props.content}</li>;
    }

    return <li>Hello, {this.props.content}</li>;
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todoList: []
    };
  }

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({
        todoList: todoList
      });
    }, 2000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    return (
      <ul>
        {this.state.todoList.map((todo, index) => (
          <Todo content={todo} key={index} index={index} />
        ))}
      </ul>
    );
  }
}

我們在首先在 App 元件中給 Todo 元件傳入了一個 index 屬性,然後在 Todo 元件的 render 方法中,對 this.props.index 進行判斷,如果為偶數,那麼渲染一個紅色的文字,如果為奇數則保持不變。

這裡我們通過給 li 元素的 style 屬性賦值一個物件來實現在 JSX 中設定元素的 CSS 屬性,我們可以通過同樣的方式設定任何 CSS 屬性:

// 黑底紅字的 Hello, 圖雀
<li style={{ color: "red", backgroundColor: "black"}}>Hello, 圖雀</li>

注意

這裡有一點需要做一下改變,就是像 background-color 這樣的屬性,要寫成駝峰式 backgroundColor。對應的比如 font-size,也要寫成 fontSize

儲存程式碼,你應該可以在瀏覽器中看到如下內容:

我們看到瀏覽器中的效果確實是在偶數項(陣列中 0 和 2 項)變成了紅色的字型,而(陣列中 1 和 3 項)還是之前的黑色樣式。

事件處理

在 React 元素中處理事件和在 HTML 中類似,就是寫法有點不一樣。

JSX 中的事件處理

這裡的不一樣主要包含以下兩點:

  • React 中的事件要使用駝峰式命名:onClick,而不是全小寫:onclick
  • 在 JSX 中,你傳遞的是一個事件處理函式,而不是一個字串。

在 HTML 中,我們處理事件是這樣的:

<button onclick="handleClick()">點我</button>

在 React 中,我們需要寫成下面這樣:

function Button() {
  function handleClick() {
    console.log('按鈕被點選了');
  }

  return (
    <button onClick={handleClick}>點我</button>
  )
}

注意到我們在上面定義了一個函式式元件,然後返回一個按鈕,並在按鈕上面定義了點選事件和對應的處理方法。

注意

這裡我們的的點選事件使用了駝峰式的 onClick 來命名,並且在 JSX 中傳給事件的屬性是一個函式:handleClick ,而不是之前 HTML 中單純的一個字串:"handleClick()"

合成事件

我們在以前編寫 HTML 的事件處理時,特別是在處理表單時,常常需要禁用瀏覽器的預設屬性。

提示

比如一般表單提交時都會重新整理瀏覽器,但是我們有時候希望提交表單之後不重新整理瀏覽器,所以我們需要禁用瀏覽器的預設屬性。

在 HTML 中我們禁用事件的預設屬性是通過呼叫定義在事件上的 preventDefault 或者設定事件的 cancelBubble

// 當點選某個連結之後,禁止開啟頁面
document.getElementById("myAnchor").addEventListener("click", function(event){
  event.preventDefault()
});

在 JSX 中,事件處理和這個類似:

function Link() {
  function handleClick(event)  {
    event.preventDefault();
    console.log('連結被點選了,但是它不會跳轉頁面,因為預設行為被禁用了');
  }

  return (
    <a onClick={handleClick} href="https://tuture.co">點我</a>
  )
}

實戰

我們運用這一節中學到的知識來繼續完成我們的待辦事項應用。

我們之前的待辦事項的 todoList 陣列都是直接硬編碼在程式碼裡,不可以進行增刪改,這相當死板,一個更真實的 todoList 應該要具備增加功能,這一功能實現需要兩個步驟:

  • 允許使用者輸入新的待辦事項。
  • 將這個輸入的待辦事項加入到現有的 todoList 列表裡面。

在這一小節中,我們將來實現第一個步驟的內容。

開啟 src/index.js ,對 App 元件內容作出如下修改:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      nowTodo: "",
      todoList: []
    };
  }

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({
        todoList: todoList
      });
    }, 2000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  handleChange(e) {
    this.setState({
      nowTodo: e.target.value
    });
  }

  render() {
    return (
      <div>
        <div>
          <input type="text" onChange={e => this.handleChange(e)} />
          <div>{this.state.nowTodo}</div>
        </div>
        <ul>
          {this.state.todoList.map((todo, index) => (
            <Todo content={todo} key={index} index={index} />
          ))}
        </ul>
      </div>
    );
  }
}

可以看到,我們新加入的程式碼主要有四個部分:

  • 首先在 state 裡面新增了一個新的屬性 nowTodo,我們將用它來儲存使用者新輸入的待辦事項。
  • 然後我們在 render 方法裡面,在返回的 JSX 中使用 div 將內容包裹起來,接著加入了一個 div,在裡面加入了一個 input 和 一個 div,input 用於處理使用者的輸入,我們在上面定義了 onChange 事件,事件處理函式是一個箭頭函式,它接收事件 e,然後使用者輸入時,會在函式裡面呼叫 this.handleChange 方法,將事件 e 傳給這個方法。
  • handleChange 裡面通過 this.setState 使用 input 的值來更新 nowTodo 的內容。
  • 最後在 div 裡面使用 {} 插值語法展示 nowTodo 的內容。

儲存程式碼,開啟瀏覽器,你應該可以看到如下的內容:

當你嘗試在輸入框中鍵入內容時,輸入框的下面應會顯示相同的內容:

這是因為當我們在輸入框裡面輸入內容時,我們使用了輸入框的值更新 this.state.nowTodo,然後在輸入框之下展示 this.state.nowTodo 的值。

表單

接下來我們來完成增加新的待辦事項的功能的第二個步驟:允許使用者將新輸入的待辦事項加入到 todoList 列表中。

開啟 src/index.js 檔案,對 App 元件做出如下修改:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      nowTodo: "",
      todoList: []
    };
  }

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({
        todoList: todoList
      });
    }, 2000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  handleChange(e) {
    this.setState({
      nowTodo: e.target.value
    });
  }

  handleSubmit(e) {
    e.preventDefault(e);
    const newTodoList = this.state.todoList.concat(this.state.nowTodo);

    this.setState({
      todoList: newTodoList,
      nowTodo: ""
    });
  }

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <input type="text" onChange={e => this.handleChange(e)} />
          <button type="submit">提交</button>
        </form>
        <ul>
          {this.state.todoList.map((todo, index) => (
            <Todo content={todo} key={index} index={index} />
          ))}
        </ul>
      </div>
    );
  }
}

上面的變化主要有三個部分:

  • 首先我們將 render 方法中返回的 JSX 的最外層的 div 替換成了 form,然後在上面定義了 onSubmit 提交事件,並且通過一個箭頭函式接收事件 e 來進行事件處理,在 form 被提交時,在箭頭函式裡面會呼叫 handleSubmit 方法, 並將 e 傳遞給這個函式。
  • 在 handleSubmit 方法裡面,我們首先呼叫了 e.preventDefault() 方法,來禁止瀏覽器的預設事件處理,這樣我們在提交表單之後,瀏覽器就不會重新整理,之後是將現有的 this.sate.todoList 列表加上新輸入的 nowTodo,最後是使用 this.setState 更新 todoListnowTodo;這樣我們就可以通過輸入內容新增新的待辦事項了。
  • 接著我們將之前的展示 this.state.nowTodo 的 div 替換成 提交按鈕 button,並將 button 的 type 設定為 submit 屬性,表示在點選這個 button 之後,會觸發表單提交;將新輸入的內容加入現有的待辦事項中。

注意

我們在 handleSubmit 方法裡面使用 this.setState 更新狀態時,將 nowTodo 設定為了空字串,代表我們在加入新的待辦事項之後,將清除現有輸入的 nowTodo 待辦事項內容。

儲存程式碼,開啟瀏覽器,在輸入框裡面輸入點東西,你應該可以看到下面的內容:

當你點選提交按鈕之後,新的待辦事項會加入到現有的 todoList 列表中,你應該可以看到下面的內容:

恭喜你!你成功使用 React 完成了一個簡單的待辦事項應用,它可以完成如下的功能:

  • 非同步獲取將要展示的待辦事項:todoList
  • 將待辦事項展示出來
  • 偶數項待辦事項將會展示成紅色
  • 可以新增新的待辦事項

做得好!我們希望你現在已經對 React 的執行機制有了一個比較完整的瞭解,也希望本篇教程能夠為你踏入 React 開發世界提供一個好的開始!感謝你的閱讀!

後記

受限於篇幅,我們的待辦事項還不完整,如果你有額外的時間或者你想要練習你新學到的 React 知識,下面是一些使我們的待辦事項變得完整的一些想法,我們將按實現難度給這些功能排序:

  • 在新增新的待辦事項之後,清空輸入框裡面的內容,方便下一次輸入。這樣涉及到 React 受控元件的知識。
  • 允許對單個事項進行刪除。這涉及到子元件修改父元件的狀態知識。
  • 允許使用者對單個事項進行修改。
  • 允許使用者對待辦事項進行搜尋。

想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

圖雀社群

相關文章