React入門最好的學習實例-TodoList

来源:http://www.cnblogs.com/jarson-7426/archive/2016/05/24/5524976.html
-Advertisement-
Play Games

前言 React 的核心思想是:封裝組件,各個組件維護自己的狀態和 UI,當狀態變更,自動重新渲染整個組件。 最近前端界鬧的沸沸揚揚的技術當屬 了,加上項目需要等等原因,自己也決定花些時間來好好認識下這個東西。然後花時間自己寫了一個demo: , 你可以先 "點這裡去看react todo" rea ...


前言

React 的核心思想是:封裝組件,各個組件維護自己的狀態和 UI,當狀態變更,自動重新渲染整個組件。

最近前端界鬧的沸沸揚揚的技術當屬react了,加上項目需要等等原因,自己也決定花些時間來好好認識下這個東西。然後花時間自己寫了一個demo:react-todos, 你可以先點這裡去看react-todo

react首先值得拍手稱贊的是它所有的開發都基於一個組件(component),組件和組件之間傳遞方法,而且每個組件都有一個狀態(state),當方法改變了這個狀態值時,整個組件就會重繪,從而達到刷新,另外,說到重繪就要提到虛擬dom了,就是用js模擬dom結構,等整個組件的dom更新完畢,才渲染到頁面,簡單來說只更新了相比之前改變了的部分,而不是全部刷新,所以效率很高。

項目初始化

大家先新建一個項目文件夾,在裡面建一個項目信息的文件package.json:

{
    "name": "react-todos",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "react": "^0.13.3",
        "sass": "^0.5.0"
    },
    "devDependencies": {
        "babel-core": "^5.5.8",
        "babel-loader": "^5.1.4",
        "css-loader": "^0.14.5",
        "file-loader": "^0.8.4",
        "jsx-loader": "^0.13.2",
        "node-libs-browser": "^0.5.2",
        "node-sass": "^3.2.0",
        "sass-loader": "^1.0.2",
        "style-loader": "^0.12.3",
        "url-loader": "^0.5.6",
        "webpack": "^1.9.11"
    }
}

建好之後,運行命令:

npm install

安裝項目依賴的所有模塊。安裝好之後,另外還有一點,項目數據是存儲在本地瀏覽器的,所以我找到一個小模塊用來操作localStorage,它的原理就是,通過將數據格式化成JSON字元串進行存儲,使用的時候就解析JSON字元串。他的代碼點這裡看localDb可以看到,你可以複製一份,放在node_modules的文件夾內。

webpack配置

項目使用的技術方案是:webpack+react+es6。關於es6的文章,我之前簡單的介紹過,可以點這裡去看es6,關於webpack的學習,我這裡不詳述了,看以後有時間再出篇文章吧。在項目文件夾下新建一個webpack.config.js

'use strict';
module.exports = {
    entry: [
        "./src/entry.js"
    ],
    output: {
        path: './out/',
        filename: "bundle.js"
    },
    externals: {
        'react': 'React'
    },
    module: {
        loaders: [
            { test: /\.js$/, loader: "jsx!babel", include: /src/},
            { test: /\.css$/, loader: "style!css"},
            { test: /\.scss$/, loader: "style!css!sass"},
            { test: /\.(png|jpg)$/, loader: 'url?limit=8192'}
        ]
    }
};

上面的文件可以看到:入口文件是在src文件夾里的entry.js,然後輸出文件放在out文件夾的bundle.js里。externals屬性是告訴webpack當遇到require('react')的時候,不去處理並且預設為全局的React變數。這樣子,我們就需要在index.html單獨用src去載入js。最後看看配置的loaders:

  • 因為我們js文件會使用jsx和es6的語法,所以使用jsx-loaderbabel-loader來編譯js文件。
  • scss文件使用sass-loader編譯成css文件。
  • 寫的時候可以省略-loader,多個loader使用!連接。

項目目錄

先來看一下項目的目錄結構,最重要的就是src目錄:

  • index.html是項目的入口頁面。
  • components文件夾存放項目拆分出來的各個組件文件。
  • vendor文件夾存放項目依賴的框架,這裡只有react。

index.html和entry.js

先來看index.html:

<body>
    <header>
        <h1 class="todo-title">React-Todos</h1>
    </header>
    <div class="container todo-container">
        <div id="app"></div>
    </div>
    <script src="./src/vendor/react.min.js"></script>
    <script src="./out/bundle.js"></script>
</body>

entry.js :

'use strict';
require('./styles/main.scss');    // 引入樣式表
require('./components/App');     // 引入組件

webpack會將入口文件進行合併和整理,最後輸出一個bundle.js,所以所有的邏輯都在這個js文件中,因此在index.html中,只需要引入react框架和bundle.js就好了。

分析組件

這個todo的項目,我們可以分為三個部分:頭部,中間部分,尾部。那我們就來逐一的分析一下這些組件:

App

'use strict';
import React from 'react';
import LocalDb from 'localDb';
import TodoHeader from './TodoHeader.js';
import TodoMain from './TodoMain.js';
import TodoFooter from './TodoFooter.js'
//es6寫法
class App extends React.Component { //定義組件,繼承父類
    constructor() { //定義App類的構造函數
        super(); //調用父類的構造函數
        this.db = new LocalDb('ReactDemo');
        this.state = { //定義組件狀態
            todos: this.db.get('todos') || [],
            isAllChecked: false
        };
    }
    // 判斷是否所有任務的狀態都完成,同步底部的全選框
    allChecked() {
        let isAllChecked = false;
        if (this.state.todos.every(todo => todo.isDone)) {
            isAllChecked = true;
        }
        this.setState({   //改變狀態,組件重繪
            todos: this.state.todos,
            isAllChecked: isAllChecked
        });
    }
    // 添加任務,是傳遞給Header組件的方法
    addTodo(todoItem){
        this.state.todos.push(todoItem);  //todo列表
        this.db.set('todos', this.state.todos);
        this.allChecked();
    }
    // 刪除當前的任務,傳遞給TodoItem的方法
    deleteTodo(index){
        this.state.todos.splice(index, 1);
        this.setState({todos: this.state.todos});  //改變狀態
        this.db.set('todos', this.state.todos);
    }
    // 清除已完成的任務,傳遞給Footer組件的方法
    clearDone(){
        let todos = this.state.todos.filter(todo => !todo.isDone);   //過濾掉數組中todo.isDone為true的item。
        this.setState({
            todos: todos,
            isAllChecked: false
        });
        this.db.set('todos', todos);
    }
    // 改變任務狀態,傳遞給TodoItem和Footer組件的方法
    changeTodoState(index, isDone, isChangeAll=false){   //初始化isChangeAll為false
        if(isChangeAll){     //全部操作
            this.setState({
                todos: this.state.todos.map((todo) => {
                    todo.isDone = isDone;
                    return todo;
                }),
                isAllChecked: isDone
            });
        }else{   //操作其中一個todo
            this.state.todos[index].isDone = isDone;
            this.allChecked();
        }
        this.db.set('todos', this.state.todos);
    }
    //組件渲染方法
    render() {
        let info = {
            isAllChecked: this.state.isAllChecked,
            todoCount: this.state.todos.length || 0,
            todoDoneCount: (this.state.todos && this.state.todos.filter((todo) => todo.isDone)).length || 0
        };
        return (
            <div className="todo-wrap">
                <TodoHeader addTodo={this.addTodo.bind(this)} />
                <TodoMain todos={this.state.todos} deleteTodo={this.deleteTodo.bind(this)} changeTodoState={this.changeTodoState.bind(this)} />
                <TodoFooter {...info} changeTodoState={this.changeTodoState.bind(this)} clearDone={this.clearDone.bind(this)} />
            </div>
        );
    }
}
React.render(<App/>, document.getElementById('app'));

我們知道React的主流思想就是,所有的state狀態和方法都是由父組件控制,然後通過props傳遞給子組件,形成一個單方向的數據鏈路,保持各組件的狀態一致。所以我們在這個父組件App上,看的東西稍微有點多。一點點來看:

  • 它採用es6的語法來創建了一個繼承React.Components的App類
  • 然後在構造函數里定義了自己的狀態state
  • 然後定義了很多方法,後面通過props傳遞給子組件
  • 最後定義組件自己的渲染方法render

App狀態

this.state = { //定義組件狀態
    todos: this.db.get('todos') || [],
    isAllChecked: false
};

在App組件的構造函數里,我們初始化了組件的state,分別有兩個,一個是todos的列表,一個是所有的todos是否全選的狀態。在渲染的時候,我們會把狀態傳遞到子組件中,如果子組件的某一個方法讓狀態發生了改變,那麼整個組件就會進行重繪。

App的方法

// 判斷是否所有任務的狀態都完成,同步底部的全選框
allChecked() {}
// 添加任務,是傳遞給Header組件的方法
addTodo(todoItem) {}
// 刪除當前的任務,傳遞給TodoItem的方法
deleteTodo(index) {}
// 清除已完成的任務,傳遞給Footer組件的方法
clearDone() {}
// 改變任務狀態,傳遞給TodoItem和Footer組件的方法
changeTodoState(index, isDone, isChangeAll=false) {}
//組件渲染方法
render() {
    let info = {
        isAllChecked: this.state.isAllChecked,
        todoCount: this.state.todos.length || 0,
        todoDoneCount: (this.state.todos && this.state.todos.filter((todo) => todo.isDone)).length || 0
    };
    return (
        <div className="todo-wrap">
            <TodoHeader addTodo={this.addTodo.bind(this)} />
            <TodoMain todos={this.state.todos} deleteTodo={this.deleteTodo.bind(this)} changeTodoState={this.changeTodoState.bind(this)} />
            <TodoFooter {...info} changeTodoState={this.changeTodoState.bind(this)} clearDone={this.clearDone.bind(this)} />
        </div>
    );
}

從上面的渲染(render)方法可以看出,組件的結構分為三部分,就是上中下。上面的TodoHeader是用來輸入任務的地方,中間的TodoMain是用來展示任務列表的, 下麵的TodoFooter提供一些特殊的方法,比如全選、刪除等。

另外,上面省去function創建函數的方法,是es6的一種語法,關於es6,我之前總結過一篇文章點這裡去看es6

App組件定義的方法,會在渲染的時候傳遞給子組件,比如TodoHeader組件:

<TodoHeader addTodo={this.addTodo.bind(this)} />

說明:

  • 通過props傳遞子組件需要的值和方法。
  • 傳遞方法時一定要bind(this),不然內部this會指向不正確。
  • 子組件的標簽使用的時候一定要使用/閉合起來。
  • ES6語法,spread操作符讓代碼簡潔很多,如上述代碼中的TodoFooter:

    <TodoFooter {...info} />
    //如果不使用spread操作符,就要這樣寫:
    <TodoFooter isAllchecked={info.isAllChecked} todoCount={info.todoCount}     todoDoneCount={info.todoDoneCount}>

渲染App

React.render(<App/>, document.getElementById('app'));

把上面的App組件的內容渲染到id為'app'的dom元素里。

然後我們再簡單看一下分解出來的三個組件:TodoHeader, TodoMain, TodoFooter

TodoHeader組件

class TodoHeader extends React.Component {
    // 綁定鍵盤迴車事件,添加新任務
    handlerKeyUp(e) {
        if(e.keyCode == 13) {
            let value = e.target.value;
            if(!value) return false;
            let newTodoItem = {
                text: value,
                isDone: false
            };
            e.target.value = '';
            this.props.addTodo(newTodoItem);   //使用props調用App組件傳過來的方法。
        }
    }
    render() {
        return (
            <div className="todo-header">
                <input onKeyUp={this.handlerKeyUp.bind(this)} type="text" placeholder="請輸入你的任務名稱,按回車鍵確認"/>
            </div>
        )
    }
}
export default TodoHeader;   //ES6語法,導出模塊,上文提到的es6文章中有講解

TodoHeader組件的創建方法和App組件的創建方法一樣,內部方法就少了很多了,這裡就定義了一個監聽鍵盤的方法,綁定到了輸入框的keyUp事件上,敲擊回車鍵的時候就會調用父組件傳過來的addTodo()方法

TodoMain組件

class TodoMain extends React.Component {
    render() {
        if(this.props.todos.length == 0) {
            return (
                <div className="todo-empty">恭喜您,目前沒有待辦任務!</div>
            )
        } else {
            return (
                <ul className="todo-main">
                    {
                        this.props.todos.map((todo, index) => {
                            //{...this.props} 用來傳遞TodoMain的todos屬性和delete、change方法。
                            return <TodoItem text={todo.text} isDone={todo.isDone} index={index} {...this.props}/>
                        })
                    }
                </ul>
            )
        }
    }
}

TodoMain組件主要是為了把傳遞過來的todos列表遍歷顯示出來,而每一個list又是一個TodoItem組件。這裡又用到了spread操作符{...this.props},代碼中也做了註釋,可以洗洗品味一下。

TodoItem組件

class TodoItem extends React.Component {
    //改變任務是否已完成的狀態
    handlerChange() {
        let isDone = !this.props.isDone;
        this.props.changeTodoState(this.props.index, isDone);
    }
    // 滑鼠移入事件
    handlerMouseOver() {
        React.findDOMNode(this).style.background = '#eee';
        React.findDOMNode(this.refs.delButton).style.display = 'inline-block';
    }
    handlerMouseOut() {
        React.findDOMNode(this).style.background = '#fff';
        React.findDOMNode(this.refs.delButton).style.display = 'none';
    }
    // 刪除當前任務
    handlerDelete(){
        this.props.deleteTodo(this.props.index);
    }
    render() {
        let className = this.props.isDone ? 'task-done' : '';
        return (
            <li onMouseOver={this.handlerMouseOver.bind(this)} onMouseOut={this.handlerMouseOut.bind(this)}>
                <label>
                <input type="checkbox" checked={this.props.isDone} onChange={this.handlerChange.bind(this)} />
                <span className={className}>{this.props.text}</span>
                </label>
                <button ref="delButton" className="btn btn-danger" onClick={this.handlerDelete.bind(this)}>刪除</button>
            </li>
        )
    }
}

TodoItem有這四個方法,我們主要看看新出現的幾點:

  • React.findDOMNode(this)可以獲取當前這個組件標簽。

  • 在元素中定義ref=xxx屬性,就可以通過React.findDOMNode(this.refs.xxx)獲取到這個元素。

  • 給元素定義class類名的時候要使用className

TodoFooter組件

class TodoFooter extends React.Component {
    //改變任務是否已完成的狀態
    handlerSelectAll(e) {
        this.props.changeTodoState(null, e.target.checked, true);    // true表示全部操作。
    }
    //刪除全部已完成的任務
    handlerDeleteDone() {
        this.props.clearDone();
    }
    render() {
        return (
            <div className="todo-footer">
            <label>
                <input type="checkbox" checked={this.props.isAllChecked} onChange={this.handlerSelectAll.bind(this)} />全選
            </label>
            <span><span className="text-success">已完成{this.props.todoDoneCount}</span> / 全部{this.props.todoCount}</span>
            <button className="btn btn-danger" onClick={this.handlerDeleteDone.bind(this)}>清除已完成任務</button>
            </div>
        )
    }
}

todoFooter組件主要用來批量更改狀態和清除已完成的任務,還要顯示任務完成情況,所以代碼很簡單了。

總結

回過頭來再看看這個demo的實現過程,react組件化的思想讓我們編寫代碼的時候思維清晰,便於閱讀。我們通過父組件來控制狀態,並通過props傳遞,來保證組件內的狀態一致,並且我們可以清晰的看到某一個方法該由誰來維護。這是一種全新的前端編碼體驗,相信以後會成為主流。

另外,我們看到代碼中,html直接嵌到js中了,這就是React提出的一種叫JSX的語法。其實入門react本身還是很簡單,只是很多人看到JSX和ES6的語法,就打了退堂鼓了,因為我們被代碼分離“洗腦”太久了。其實,它們就好像是一堵牆,要是我們畏懼這個障礙止步不前,那麼只能停留在原地,如果我們骨氣勇氣爬上去,才發現react的風景真的很優美!

參考資料:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本系列文章實際上就是官網文檔的翻譯加上自己實踐過程中的理解。 伴隨著websites演化至web apps的過程,有三個現象是很明顯的: 頁面中有越來越多的Js。 客戶端能做的事情越來越多。 越來越少的頁面重載(當然也伴隨著更複雜的代碼)。 這些現象導致了什麼?大量的前端代碼。 龐大的代碼庫需要被高 ...
  • 1、jQuery對象轉換成DOM對象 a.var $cr = $("#cr"); //jQuery對象var cr=$cr[0]; //DOM對象alert(cr.checked); //檢測checkbox是否被選中了b.var $cr=$("#cr"); //jQuery對象var cr=$cr ...
  • 很多人對盒子模型搞暈頭了,下麵通過一個簡單的代碼來分析盒子模型的結構! 為了方便方便觀看!在第一個div中畫了一個表格,並將其尺寸設置成與div內容大小一樣!且設置body的margin和padding的屬性都為0px; 本例子採用行內CSS樣式! 代碼如下: 1 2 3 4 5 6 7 8 9 1 ...
  • 在寫上一篇有關apply和call的博文時(閑聊JS中的apply和call),起初我還是擔心大家理解起來比較困難,因為要理解apply調用方式的前提是,至少先理解在JavaScript中函數是什麼?this到底代表什麼意思?等等。不過從大家的反饋來看,我的擔心是多餘的,諸位園友都是高手,理解這些基 ...
  • Atitit.100% 多個子元素自適應佈局屬性 1.1. 原理1 1.2. Table佈局1 1.3. Css佈局1 1.4. 判斷amazui載入完畢2 1.1. 原理 每個子元素平均分配,但是有個min-width... min-width優先 演算法:首先算出平均值,然後與每一個帶min-wi ...
  • align-content 作用: 會設置自由盒內部各個項目在垂直方向排列方式。 條件:必須對父元素設置自由盒屬性display:flex;,並且設置排列方式為橫向排列flex-direction:row;並且設置換行,flex-wrap:wrap;這樣這個屬性的設置才會起作用。 設置對象: 這個屬 ...
  • Vue.js報錯Failed to resolve filter問題原因 金剛 vue Vue.js js javascript 之前使用vue.js寫分頁功能時,寫了一個過濾器,發現一個比較奇怪的錯誤。 console控制台調試的時候 提示錯誤消息: Failed to resolve filte ...
  • 1.前言 上一篇jQuery分析(2)中瞭解了jQuery庫的骨架實現原理,這就好比搖滾音樂,搖滾音樂不是某種音樂他就像一個音樂盒子,裡面包含了各種不同的搖滾風格(山地、朋克、鄉村、流行、硬搖、金屬、迷幻等)。那麼上一篇只是大致瞭解了jQuery的基本形狀,從這篇文章開始會深入jQuery庫的各種函 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...