React 系列一 之 TodoList

jsliang發表於2019-03-22

Create by jsliang on 2019-3-18 08:37:10
Recently revised in 2019-3-22 11:46:54

Hello 小夥伴們,如果覺得本文還不錯,記得給個 star , 小夥伴們的 star 是我持續更新的動力!GitHub 地址

一 目錄

不折騰的前端,和鹹魚有什麼區別

目錄
一 目錄
二 前言
三 正文
3.1 新建 React 專案
3.2 專案目錄解析
3.3 精簡專案結構
3.4 初探元件
3.5 JSX
3.6 事件及雙向資料繫結
3.7 優化-抽取 CSS
3.8 優化-抽取 JS
四 總結
五 參考文獻

二 前言

返回目錄

通過編寫一個簡單的 TodoList 小 Demo,熟悉 React 的開發流程。

三 正文

返回目錄

Now,開始搞事情。

3.1 新建 React 專案

返回目錄

  1. 下載 Node.js
  2. 安裝 React 腳手架:
    1. npm i create-react-app -g
  3. 開啟新專案:
    1. create-react-app todolist
    2. cd todolist
    3. npm start
  4. 開啟 localhost:3000 檢視頁面

React 系列一 之 TodoList

3.2 專案目錄解析

返回目錄

- todolist
  + node_modules —————————— 專案依賴的第三方的包
  - public ———————————————— 共用檔案
    - favicon.ico        —— 網頁標籤左上角小圖示
    - index.html         —— 網站首頁模板
    - mainfest.json      —— 提供 meta 資訊給專案,並與 serviceWorker.js 相呼應,進行離線 APP 定義
  - src ——————————————————— 專案主要目錄
    - App.css            —— 主元件樣式
    - App.js             —— 主元件入口
    - App.test.js        —— 自動化測試檔案
    - index.css          —— 全域性 css 檔案
    - index.js           —— 所有程式碼的入口
    - logo.svg           —— 頁面的動態圖
    - serviceWorker.js   —— PWA。幫助開發手機 APP 應用,具有快取作用
  - .gitignore ——————————— 配置檔案。git 上傳的時候忽略哪些檔案
  - package-lock.json ———— 鎖定安裝包的版本號,保證其他人在 npm i 的時候使用一致的 node 包
  - package.json ————————— node 包檔案,介紹專案以及說明一些依賴包等
  - README.md ———————————— 專案介紹檔案
複製程式碼

3.3 精簡專案結構

返回目錄

為了方便開發,下面對 creat-react-app 的初始目錄進行精簡:

- todolist
  + node_modules —————————— 專案依賴的第三方的包
  - public ———————————————— 共用檔案
    - favicon.ico        —— 網頁標籤左上角小圖示
    - index.html         —— 網站首頁模板
  - src ——————————————————— 重要的目錄
    - App.js             —— 主元件入口
    - index.js           —— 所有程式碼的入口
  - .gitignore ——————————— 配置檔案。git 上傳的時候忽略哪些檔案
  - package.json ————————— node 包檔案,介紹專案以及說明一些依賴包等
  - README.md ———————————— 專案介紹檔案
複製程式碼

favicon.ico.gitignoreREADME.md 是我們無需理會的,但是其他檔案,我們需要精簡下它們的程式碼:

  1. index.html
程式碼詳情
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>Todolist</title>
  </head>
  <body>
    <noscript>你需要允許在 APP 中執行 JavaScript</noscript>
    <div id="root"></div>
  </body>
</html>
複製程式碼
  1. App.js
程式碼詳情
import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div className="App">
        Hello React!
      </div>
    );
  }
}

export default App;
複製程式碼
  1. index.js
程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼
  1. package.json
程式碼詳情
{
  "name": "todolist",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.8.4",
    "react-dom": "^16.8.4",
    "react-scripts": "2.1.8"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}
複製程式碼

3.4 初探元件

返回目錄

React 系列一 之 TodoList

類似於上圖,在進行頁面開發的時候,我們很容易地使用庖丁解牛的技巧,將頁面進行劃分,然後一部分一部分地將頁面搭建出來。

給個比較官方的說法,就叫頁面元件化:將頁面切成幾個部分,從而有利於頁面的拼裝以及程式碼的維護。

在 create-react-app 的預設配置中,App.js 就是一個元件,一起來看:

App.js

// 1. 引用 React 及其元件
import React, { Component } from 'react';

// 2. 定義一個叫 App 的元件繼承於 Component
class App extends Component {
  render() {
    return (
      <div className="App">
        Hello React!
      </div>
    );
  }
}

// 3. 根據 React 例項,在 App 內部編寫完畢後,匯出這個 App 元件
export default App;
複製程式碼

在上面,我們引用、定義並匯出了這個 App 的元件,然後我們就要使用它:

index.js

// 1. 引入 React、ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';

// 2. 將 App.js 匯入進來
import App from './App';

// 3. 通過 ReactDOM 將 App.js 以虛擬 DOM 的形式渲染/掛載到 root 根節點,該節點在 index.html 中
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼

index.js 告訴我們,它會通過 ReactDom,將 App.js 這個元件掛載到 root 這個節點上,那麼,這個 root 在哪裡呢?我們檢視下 index.html:

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortc ut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>Todolist</title>
  </head>
  <body>
    <noscript>你需要允許在 APP 中執行 JavaScript</noscript>
    <div id="root"></div>
  </body>
</html>
複製程式碼

OK,很容易地我們就捋清楚思路了:我們在 index.html 中定義了個 root 根節點,然後我們通過 index.js,將 App.js 以元件形式渲染到了 index.html 中,從而實現了節點的掛載。

思維發散:我們知道 index.js 和 App.js 的最終結合是掛載到 id="root" 節點上的,如果我們再開一個 index2.js 和 App2.js,掛載到 id="root2" 節點上,行不行呢?亦或者我們開一個 id="root3" 的節點,我們在其中操作 jQuery,是不是也可行?

3.5 JSX

返回目錄

在 create-react-app 的檔案中,不管是 index.js 中的:

ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼

還是 App.js 中的:

class App extends React.Component {
  render() {
    return (
      <div className="App">
        Hello React!
      </div>
    );
  }
}
複製程式碼

等這些有關 DOM 的渲染,都需要用到 JSX,因此需要引入 React:

import React from 'react';
複製程式碼
  • JSX 的定義

那麼,什麼是 JSX 呢?

React 的核心機制之一就是可以在記憶體中建立虛擬的 DOM 元素。React 利用虛擬 DOM 來減少對實際 DOM 的操作從而提升效能。

JSX 就是 JavaScript 和 XML 結合的一種格式。

React 發明了 JSX,利用 HTML 語法來建立虛擬 DOM。當遇到 <,JSX 就當 HTML 解析,遇到 { 就當 JavaScript 解析。

  • JSX 的使用

在 JSX 語法中,如果我們需要使用自己建立的元件,我們直接使用它的定義名即可,例如:

index.js

// 1. 引入 React、ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';

// 2. 將 App.js 匯入進來
import App from './App';

// 3. 通過 ReactDOM 將 App.js 以虛擬 DOM 的形式渲染/掛載到 root 根節點,該節點在 index.html 中
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼

其中第三點即是自定義元件渲染到根節點。

提示:在 React 中,如果需要使用自定義元件,那麼該元件不能小寫開頭 app,而是使用 App 這樣的大寫開頭形式。

3.6 事件及雙向資料繫結

返回目錄

這是我們精簡後的目錄結構:

React 系列一 之 TodoList

我們修改下目錄結構,開始編寫 TodoList:

React 系列一 之 TodoList

首先,我們修改 App.js 為 TodoList.js:

App.js TodoList.js

import React, { Component } from 'react';

class TodoList extends Component {
  render() {
    return (
      <div className="TodoList">
        Hello React!
      </div>
    );
  }
}

export default TodoList;
複製程式碼

然後,我們修改 index.js 中掛載到 index.html 的元件為 TodoList:

index.js

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

import TodoList from './TodoList';

// 3. 通過 ReactDOM 將 App.js 以虛擬 DOM 的形式渲染/掛載到 root 根節點,該節點在 index.html 中
ReactDOM.render(<TodoList />, document.getElementById('root'));
複製程式碼

修改完畢,小夥伴們可以重啟下 3000 埠,檢視下我們的 React 是否能正常啟動。

在此步驟中,我們僅僅修改 App.js 為 TodoList.js,使 index.js 掛載到 root 的是 TodoList.js,除此之外沒進行其他操作。

最後,如果沒有問題,那麼我們進一步編寫 TodoList,獲取到 input 輸入框的值,並渲染到列表中:

TodoList.js

程式碼詳情
// Fragment 是一種佔位符形式,類似於 Vue 的 Template
import React, { Component, Fragment } from 'react';

class TodoList extends Component {

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

  render() {
    return (
      <Fragment>
        <div>
          {/* 單項資料繫結 */}
          <input 
            type="text" 
            value={this.state.inputValue}
            onChange={this.handleInputChange.bind(this)}
          />
          <button>提交</button>
        </div>
        <ul>
          <li>
            <span>吃飯</span>
            <span>X</span>
          </li>
          <li>
            <span>睡覺</span>
            <span>X</span>
          </li>
          <li>
            <span>打豆豆</span>
            <span>X</span>
          </li>
        </ul>
      </Fragment>
    )
  }

  handleInputChange(e) {
    console.log(e.target.value);
    this.setState({
      inputValue: e.target.value
    })
  }

}

export default TodoList;
複製程式碼

我們先檢視演示:

React 系列一 之 TodoList

OK,這樣我們在每輸入一個字元的時候,我們就能立刻獲取到對應的資料,這時候實現了單向資料流:輸入框 -> JS 記憶體。

其中有 3 點需要講解下:

  1. Fragment 是 React 提供的一種佔位符,它像 <input><span> 等標籤一樣,但是它在實際渲染的時候是不會出現的。因為 React 的 JSX 首層必須要有標籤,然後如果使用 <div> 等會佔用一個層級,所以,類似於 Vue 的 Template,React 使用了 Fragment 這種空標籤。
  2. constructor 表示父類的構造方法,在 ES6 中,構造方法 constructor 相當於其建構函式,用來新建父類的 this 物件,而 super(props) 則是用來修正 this 指向的。簡而言之,我們可以在這裡定義資料,並在整個 js 檔案中使用。
  3. onChange 這種寫法,是 React 指定的寫法,例如 onClick 等,在原生 JS 中,使用的是 onclick,而在 React 中,為了區別,需要進行半駝峰編寫事件名字。同時,繫結的 handleInputChange,可以直接在 render 下面進行編寫。

參考文獻:《react 中 constructor() 和 super() 到底是個啥?》

這樣,我們就對 React 的資料及事件有了初步理解,下面我們加下按鈕點選新增列表事件以及點選 X 刪除列表事件。

TodoList.js

程式碼詳情
// Fragment 是一種佔位符形式,類似於 Vue 的 Template
import React, { Component, Fragment } from 'react';

class TodoList extends Component {

  // 建構函式
  constructor(props) {
    super(props);
    // 定義資料
    this.state = {
      inputValue: '',
      list: []
    }
  }

  // 渲染頁面
  render() {
    let closeStyle = {
      fontSize: '1.2em',
      color: 'deepskyblue',
      marginLeft: '10px'
    }
    return (
      <Fragment>
        <div>
          {/* 單項資料繫結 */}
          {/* 在 React 中,繫結時間的,一般為半駝峰形式 */}
          <input 
            type="text" 
            value={this.state.inputValue}
            onChange={this.handleInputChange.bind(this)}
          />
          <button onClick={this.handleBtnClick.bind(this)}>提交</button>
        </div>
        <ul>
          {
            this.state.list.map( (item, index) => {
              return <li key={index}>
                <span>{index}. {item}</span>
                <span style={closeStyle} onClick={this.handleItemDelete.bind(this, index)}>X</span>
              </li>
            })
          }
        </ul>
      </Fragment>
    )
  }

  // 方法體 - 輸入內容
  handleInputChange(e) {
    this.setState({
      inputValue: e.target.value
    })
  }

  // 方法體 - 點選提交
  handleBtnClick() {
    this.setState({
      list: [...this.state.list, this.state.inputValue],
      inputValue: ''
    })
  }

  // 方法體 - 刪除專案
  handleItemDelete(index) {
    const list = [...this.state.list];
    list.splice(index, 1);

    this.setState({
      list: list
    })
  }

}

export default TodoList;
複製程式碼

React 系列一 之 TodoList

在這一部分,我們需要了解 3 個知識點:

  1. rendercloseStyle 這個變數,我們用來定義 CSS 屬性,然後我們通過 style={closeStyle},直接寫了個行內樣式(下面我們會抽離出來)
  2. 關於 JSX 遍歷輸出的形式:
{
  this.state.list.map( (item, index) => {
    return <li key={index}>
      <span>{index}. {item}</span>
      <span style={closeStyle} onClick={this.handleItemDelete.bind(this, index)}>X</span>
    </li>
  })
}
複製程式碼

我們通過 {} 裡面迴圈輸出 DOM 節點。如果你學過 jQuery,那麼可以將它當為拼接字串;如果你學過 Vue,那麼可以將它當成 v-for 變了種寫法。

在這裡我們不用理會為什麼這麼寫,我們先接受這種寫法先。

  1. 關於改變 constructor 中的資料,我們使用:
this.setState({
  list: list
})
複製程式碼

這種形式。其實,這也是有記憶技巧的,要知道我們在定義資料的時候,使用了:

// 定義資料
this.state = {
  inputValue: '',
  list: []
}
複製程式碼

即: this.state,那麼我們需要修改資料,那就是 this.setState 了。

至此,我們的簡易 TodoList 就實現了,下面我們進一步優化,將 CSS 和 JS 進一步抽取。

3.7 優化-抽取 CSS

返回目錄

在上面中,我們提到 closeStyle 是一種行內的寫法,作為一枚 完美程式設計者,我們肯定不能容忍,下面我們開始抽離:

TodoList.js

import React, { Component, Fragment } from 'react';

import './style.css'

// ... 省略中間程式碼

<ul>
{
  this.state.list.map( (item, index) => {
    return <li key={index}>
      <span>{index}. {item}</span>
      <span className="icon-close" onClick={this.handleItemDelete.bind(this, index)}>X</span>
    </li>
  })
}
</ul>
複製程式碼

在這裡,我們需要知道:我們可以通過 import 的形式,直接將 CSS 檔案直接匯入,然後,我們命名 class 的時候,因為 React 怕 JS 的 class 與 HTML 的 class 衝突,所以我們需要使用 className

最後我們再編寫下 CSS 檔案:

.icon-close {
  font-size: 1.2em;
  color: deepskyblue;
  margin-left: 10px;
}
複製程式碼

如此,我們就實現了 CSS 的抽取。

3.8 優化-抽取 JS

返回目錄

在第 4 章關於元件的介紹中,我們講到:一些複雜的 JS 是可以抽取出來,並以元件的形式,嵌入到需要放置的位置的。

那麼,我們在 JSX 越寫越多的情況下,是不是可以將列表渲染那部分抽取出來,從而精簡下 JSX 呢?

答案是可以的,下面我們看下實現:

TodoList.js

程式碼詳情
// Fragment 是一種佔位符形式,類似於 Vue 的 Template
import React, { Component, Fragment } from 'react';

// 引入元件
import TodoItem from './TodoItem';

// 引用樣式
import './style.css';

class TodoList extends Component {

  // 建構函式
  constructor(props) {
    super(props);
    // 定義資料
    this.state = {
      inputValue: '',
      list: []
    }
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
  }

  // 渲染頁面
  render() {
    return (
      <Fragment>
        <div>
          <label htmlFor="insertArea">輸入內容:</label>
          {/* 單項資料繫結 */}
          {/* 在 React 中,繫結時間的,一般為半駝峰形式 */}
          <input 
            id="insertArea"
            type="text" 
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul>
          {/* 精簡 JSX,將部分抽取出來 */}
          { this.getTodoItem() }
        </ul>
      </Fragment>
    )
  }

  // 獲取單獨項
  getTodoItem() {
    return this.state.list.map( (item, index) => {
      return (
        <TodoItem 
          key={index}
          item={item} 
          index={index}
          handleItemDelete={this.handleItemDelete}
        />
      )
    })
  }

  // 方法體 - 輸入內容
  handleInputChange(e) {
    const value = e.target.value;
    this.setState( () => ({
      inputValue: value
    }))
  }

  // 方法體 - 點選提交
  handleBtnClick() {
    const list = this.state.list,
          inputValue = this.state.inputValue;
    this.setState( () => ({
      list: [...list, inputValue],
      inputValue: ''
    }))
  }

  // 方法體 - 刪除專案
  handleItemDelete(index) {
    // immutable - state 不允許做任何改變
    const list = [...this.state.list];
    list.splice(index, 1);

    this.setState( () => ({
      list: list
    }))
  }

}

export default TodoList;
複製程式碼

我們關注下 TodoList.js 的改變:

  1. 我們在 constructor 中,將方法進行了提前定義:
this.handleInputChange = this.handleInputChange.bind(this);
複製程式碼

這樣,我們在下面就不用寫 .bind(this) 形式了。

  1. 我們修改了下 this.setState() 的形式:

原寫法:

this.setState({
  list: list
})
複製程式碼

現寫法:

this.setState( () => ({
  list: list
}))
複製程式碼

因為 React 16 版本進行了更新,使用這種寫法比之前的好,至於好在哪,我們先不關心,以後就用這種寫法了。

  1. 我們引用了元件:
import TodoItem from './TodoItem';
複製程式碼

並且將元件放到方法體:this.getTodoItem() 中,而 this.getTodoItem() 的定義是:

// 獲取單獨項
getTodoItem() {
 return this.state.list.map( (item, index) => {
   return (
     <TodoItem 
       key={index}
       item={item} 
       index={index}
       handleItemDelete={this.handleItemDelete}
     />
   )
 })
}
複製程式碼

在這裡我們可以看到,我們通過自定義值的形式,將資料 keyitemindex 傳遞給了子元件 TodoItem。同時,通過 handleItemDelete,將自己的方法傳遞給了子元件,這樣子元件就可以呼叫父元件的方法了:

TodoItem.js

程式碼詳情
import React, { Component } from 'react'

class TodoItem extends Component {

  constructor(props) {
    super(props);
    // 這種寫法可以節省效能
    this.handleClick = this.handleClick.bind(this);
  }

  render() {
    const { item } = this.props;
    return (
      <li>
        <span>{item}</span>
        <span className="icon-close" onClick={this.handleClick}>X</span>
      </li>
    )
  }

  handleClick() {
    const { handleItemDelete, index } = this.props;
    handleItemDelete(index);
  }

}

export default TodoItem;
複製程式碼

這樣,我們就完成了元件的抽取,並學會了

  • 父元件傳遞值給子元件
  • 子元件呼叫父元件的方法

由此,我們在接下來就可以編寫更豐富健全的專案了。

本文程式碼地址:React 系列原始碼地址

四 總結

返回目錄

在我們學習任意一門語言中,大多就是上手 “Hello World!” 程式設計~

然後做小案例的時候,我們都喜歡來個 TodoList,因為它能講清楚一些有關基礎的知識點。

現在,我們回顧下,我們開發 React 的 TodoList 有啥收穫:

  1. create-react-app 的安裝及開發。
  2. 元件化的思想及在 create-react-app 中關於元件化的應用。
  3. React 關於資料 data 以及方法 methods 的定義及使用,以及如何進行資料雙向繫結。
  4. 將大的元件拆分成小元件,並實現父子元件通訊(父元件傳遞引數給子元件,子元件呼叫父元件的方法)

至此,jsliang 就精通 jQuery、Vue、React 編寫 TodoList 了,哈哈!

五 參考文獻

返回目錄

  1. 《React.Component 與 React.PureComponent(React之效能優化)》
  2. 《visual studio code + react 開發環境搭建》
  3. 《react 中 constructor() 和 super() 到底是個啥?》

jsliang 廣告推送:
也許小夥伴想了解下雲伺服器
或者小夥伴想買一臺雲伺服器
或者小夥伴需要續費雲伺服器
歡迎點選 雲伺服器推廣 檢視!

React 系列一 之 TodoList
React 系列一 之 TodoList

知識共享許可協議
jsliang 的文件庫樑峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的作品創作。
本許可協議授權之外的使用許可權可以從 creativecommons.org/licenses/by… 處獲得。

相關文章