React 入門-寫個 TodoList 實例

React 是一個用於構建用戶界面的 JavaScript 庫,主要特點有:

  • 聲明式渲染:設計好數據和視圖的關係,數據變化 React 自動渲染,不必親自操作DOM
  • 組件化:頁面切分成多個小部件,通過組裝拼成整體頁面,利於代碼複用

本文通過寫個簡單的 TodoList 實例,不求甚解,熟悉下 React 的開發過程。

1. 安裝 Node.js

Node.js 是一個運行環境,類似 jdk,用以支持在服務端運行 JavaScript。

您可以在這裏下載安裝包:

http://nodejs.cn/download/

以綠色版安裝爲例,將 node-v10.16.1-win-x64.zip 解壓到 E:\software\ 並命名爲 node-v10.16.1

在 Path 環境變量中增加兩項:

E:\software\node-v10.16.1\
E:\software\node-v10.16.1\node_global

在 cmd 中使用 node -v 顯示版本號,表示安裝成功。

Node.js 中有個 npm 軟件包管理器,可以很方便的管理下載和使用第三方開源包,類似 maven,使用 npm -v 顯示版本號,表示 npm 也沒有問題。

綠色版安裝完成後一些必要的配置:

npm config set prefix "E:\software\node-v10.16.1\node_global"

設置全局安裝的模塊存儲路徑

npm config set cache "E:\software\node-v10.16.1\node_cache"

設置下載緩存的存儲路徑

npm config set registry https://registry.npm.taobao.org`

設置 npm 下載源爲淘寶鏡像

簡單使用:

  • npm install xxx: 安裝到項目目錄
  • npm install -g xxx 安裝到全局目錄
  • npm install -save xxx: 安裝到項目目錄,並在 package.json 中的 dependencies 節點記錄依賴
  • npm install --save-dev xxx: 安裝到項目目錄,並在 package.json 中的 devDependencies 節點記錄依賴

2. 腳手架創建項目

React 官方出的腳手架工具 create-react-app ,可以一鍵創建一個 Web 應用程序:

cmd> npm install -g create-react-app
cmd> cd E:
cmd> create-react-app react-todoapp
cmd> cd react-todoapp

腳手架會在當前目錄創建一個 react-todoapp 目錄:

react-todoapp
├── README.md
├── node_modules
├── package.json
├── package-lock.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
    └── setupTests.js

目錄中主要的文件和文件夾說明:

  • README.md: 項目簡介,支持 Markdown 語法
  • node_modules: 項目的依賴包,類似 Maven Repository
  • package.json: 配置項目依賴的第三方包,類似 pom.xml
  • package-lock.json: 鎖定第三方包的版本號,保證 npm install 版本一致
  • public: 公開資源,網站路徑,類似 nginx 的 html 目錄
  • src: 核心組件代碼文件

爲了便於開發,刪除目錄中不必要的文件,最終結構如下:

react-todoapp
├── README.md
├── node_modules
├── package.json
├── package-lock.json
├── .gitignore
├── public
│   └── index.html
└── src
    ├── App.css
    ├── index.js
    ├── TodoApp.js
    ├── TodoItem.js
    └── TodoList.js

接下來,設計與實現一個 TodoList 的例子,我們把所有代碼過一下,敲一遍,先不管爲什麼,跑起來,最後再整理下知識點。

3. 實例 TodoApp

react-todoapp

主要實現功能有:

  • 添加一個待辦事項
  • 刪除一個待辦事項
  • 勾選複選框標記事項已完成

如圖所示,總共將頁面拆分成了三個組件:TodoApp, TodoListTodoItem

3.1 index.js 入口文件

應該可以類比 java 的 main 方法,在 src 目錄新建 index.js 內容如下:

// 引入 React, ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';
// 引入 TodoApp 組件
import TodoApp from './TodoApp';

// 將渲染結果掛在到 root 節點,該節點在 index.html 中
ReactDOM.render(
    <React.StrictMode>
        <TodoApp />
    </React.StrictMode>,
    document.getElementById('root')
);

先導入需要使用的組件(類),然後調用它們提供的方法和服務,有沒有些許眼熟?

3.2 TodoApp.js

TodoApp 設計了頁面整體佈局,它包含全部數據以及操作這些數據的方法,是其他兩個組件的父組件

import React, { Component } from 'react';
import TodoList from './TodoList';
import './app.css';

class TodoApp extends Component {
    constructor(props) { // 構造方法,props 應該是父類的一個成員變量
        super(props);
        this.state = { // 組件狀態數據
            text: '',
            items:[{id: 1, status: 1, text: "去月球"},{id: 2, status: 0, text: "去火星"}]
        };

        // 設置 this 指向,默認 undefined
        this.handleChange   = this.handleChange.bind(this);
        this.handleAdd      = this.handleAdd.bind(this);
        this.handleComplete = this.handleComplete.bind(this);
        this.handleDelete   = this.handleDelete.bind(this);
    }
    // 渲染解析 jsx
    render() {
        return (
            <div className="todo">
                <h3 className="text-center">Todos App</h3>
                <TodoList 
                    items={this.state.items} 
                    handleComplete={this.handleComplete} 
                    handleDelete={this.handleDelete} />

                <input className="input" type="text" placeholder="添加新任務" 
                    value={this.state.text} 
                    onChange={this.handleChange} />

                <button className="btn-add" onClick={this.handleAdd}>添加</button>
            </div>
        );
    }

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

    handleAdd(e) {
        e.preventDefault();
        if (this.state.text.length === 0) {
            return;
        }

        const newItem = {
            id: Date.now(),
            text: this.state.text,
            status: 0
        };

        this.setState({
            items: [...this.state.items, newItem],
            text: ''
        });
    }

    handleComplete(taskid) {
        // 臨時變量,不直接修改原數據
        let items = this.state.items;
        let findItem = items.find(item => item.id === taskid);
        findItem.status = findItem.status === 0 ? 1 : 0;

        this.setState({
            items: items
        });
    }
    
    handleDelete(taskid) {
        let items = this.state.items;
        items = items.filter(item => item.id !== taskid);
        this.setState({
            items: items
        });
    }
}
 
export default TodoApp;

3.3 TodoList.js

TodoList 接收父組件 TodoApp 中的數組,並將其渲染成一個 ul 列表:

import React, { Component } from 'react';
import TodoItem from './TodoItem';

class TodoList extends Component {
    render() {
        return (
            <ul className="list">
                {
                    this.props.items.map((item)=>{
                        return (
                            <TodoItem key={item.id} 
                                taskid={item.id}
                                status = {item.status}
                                text={item.text} 
                                handleComplete={this.props.handleComplete} 
                                handleDelete={this.props.handleDelete} />
                        )
                    })
                }
            </ul>
        );
    }
}
 
export default TodoList;

3.4 TodoItem.js

在 TodoList 遍歷數組時,把每一項元素交給 TodoItem 組件,它會渲染成一個 li 元素:

import React, { Component } from 'react';

class TodoItem extends Component {
    constructor(props) {
        super(props);

        this.taskComplete = this.taskComplete.bind(this);
        this.taskDelete   = this.taskDelete.bind(this);
    }

    render() {
        let isCompleted = this.props.status === 1;
        return (
            <li className={isCompleted?'complete':''}>
                <input type="checkbox" 
                    checked={isCompleted}
                    onChange={this.taskComplete}/>
                <span>{this.props.text}</span>
                <button className="btn-del" onClick={this.taskDelete}>刪除</button>
            </li>
        );
    }

    taskComplete() {
        this.props.handleComplete(this.props.taskid);
    }

    taskDelete() {
        this.props.handleDelete(this.props.taskid);
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.text !== this.props.text || nextProps.status !== this.props.status) {
            return true;
        } else {
            return false;
        }
    }
}
 
export default TodoItem;

這幾個文件寫完之後,進入 react-todoapp 目錄,cmd 運行 npm start,訪問 http://localhost:3000 就能查看最終的結果了。

4. 思考

4.1 JSX

TodoApp 組件在 render 方法渲染時,使用了一個既不是字符串也不是 HTML 的語法,它被稱爲 JSX,是 JavaScript 的語法擴展,使用它可以很方便的創建 DOM。

JSX 看起來像是模板語言,但它具有 JavaScript 的全部功能:

  • 遇到 <> 就當作 HTML 解析
  • 遇到 {} 就當作 JavaScript 解析

4.2 組件通信

這裏主要有兩種通信情況:

  • 父組件向子組件通信
  • 子組件向父組件通信

每個組件都有一個 props 對象,用以訪問組件的屬性,所以,父組件可以向子組件傳遞一組 props 供其使用,就像方法傳參一樣。

子組件向父組件通信,可以利用回調函數:父組件將一個函數作爲 props 傳遞給子組件,子組件調用該回調函數,便可向父組件通信。

回調函數也是增刪一組數據,那麼爲什麼不直接把數據傳給子組件,直接操作?這是因爲 props只讀的,不能修改,改了就會報錯:

TypeError: Cannot assign to read only property 'items' of object '#<Object>'

這樣設計是爲了保證相同的輸入,每次都輸出相同的結果。

4.3 組件狀態

組件分有狀態無狀態,比如 TodoApp 是有狀態的,TodoList 和 TodoItem 是無狀態的。這個狀態 stateprops 類似,也是一組數據,但它是組件內部私有的,其他組件訪問不了。

所以,TodoApp 組件只有在自己內部纔可以對 this.state.items 內部增刪改,就算把它傳給其他組件,也是隻讀的。

在更新 state 時需要注意:

  • 不能直接修改 state:統一使用 setState() 更新
  • setState 是一個異步方法,可傳遞一個函數在執行結束後回調,setState({},()=>{..})
  • setState 會合並更新,就是可以只傳遞變更的部分

組件的 state 可以隨着用戶交互而產生變化,但 props 一旦定義就不再發生改變。

4.4 單向數據流

子組件不能直接修改父組件的數據,數據只能由父組件傳給子組件,更新只能通過回調,這個特性被稱作單向數據流

它保證了組件相同的輸入,每次都是相同的輸出。所有數據都在父組件,代碼易於理解,方便維護。

4.5 其他

import React, { Component } from 'react'; 這是 ES6 解構賦值 的用法,解構賦值是對賦值運算符的擴展,可以將屬性/值從對象/數組中取出,賦值給其他變量。

這個導入就相當於:

import React from 'react';
const Component = React.Component;

類比 Java 的話,可以這樣理解,React 相當於包,Component 相當於包下的類,要使用都要先導入。

5. 總結

簡單寫了下自己的理解,僅供參考!還是要看官方文檔:

此次編寫的 react-todoapp 源碼地址:https://github.com/chuondev/react-todoapp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章