1. 程式人生 > >十分詳細的React入門例項

十分詳細的React入門例項

學習React不是一蹴而就的事情,入門似乎也沒那麼簡單。但一切都是值得的。

今天給大家帶來一個詳細的React的例項,例項並不難,但對於初學者而言,足夠認清React的思考和編寫過程。認真完成這個例項的每一個細節會讓你受益匪淺。接下來我們開始吧!

程式碼下載

預覽

首先說明一下,本例究竟做了什麼。本文實現了一個單頁面人員管理系統的前臺應用。包括以下功能:

  • 人員基本資訊列表;
  • 人員的錄入及刪除;
  • 人員詳細資訊的檢視;
  • 人員資訊的編輯;
  • 根據人員身份進行篩選;
  • 根據人員某些屬性進行排序;
  • 根據人姓名、年齡、身份、性別等關鍵字進行人員搜尋。

頁面預覽如下:

圖1

圖2

為了更好地學習,請先到這裡去感受一下:

程式碼下載

本文構建React元件的時候,使用了es6的語法,最終用webpack打包。最好有相關基礎,我會在相關的地方進行言簡意賅的說明。

第一步:劃分UI Component

React is all about modular, composable components.

React是模組化、元件化的。我們這裡第一步要做的就是將應用劃分成各個元件。我在圖一、圖二的基礎上圈出了我們即將實現的各個元件。結果如圖三、圖四所示:

圖3

圖4

每個圈出的元件功能如下,這是本應用的框架,請大家務必看清楚,其中斜體字是各個元件的名稱:

  • ManageSystem 圖三最外層的紅色方框,這是管理模組的最外層容器,容納整個應用;
  • StaffHeader 圖三最上層藍色方框,該模組接收使用者操作的輸入,包括關鍵字搜尋輸入、篩選條件以及排序方式;
  • StaffItemPanel 圖三中間藍色方框,該模組用於展示所有基於使用者操作(關鍵字搜尋、篩選、排序)結果的條目;
  • StaffFooter 圖三最下層藍色方框,該模組用於新人員的新增;
  • StaffItem 圖三內層的紅色方框,該模組用於展示一條人員的基本資訊,包括刪除和詳情操作的按鈕;
  • StaffDetail 圖四的紅色方框,每當點選StaffItem的’詳情’後會顯示該條目的詳細資訊。該模組用於展示人員的詳細資訊,兼有人員資訊編輯的功能。

為了更清楚地展示框架結構:

ManageSystem

    StaffHeader

    StaffItemPanel

        StaffItem
        StaffItem
        StaffItem...

    StaffFooter

    StaffDetail(只在點選某條目的詳情後展示)

第二步:構建靜態版的React應用

在第一步中我們已經劃分了各個元件,也說明了各個元件的職責。接下來我們分步完成我們的應用,首先我們做一個靜態版的React,只用於render UI元件,但並不包含任何互動。

這個步驟我們只需要參照圖一、圖二去做就好了,絕大部分工作基本上就是使用JSX按部就班地寫html程式碼。這個過程不需要太多思考。每個元件中都僅僅只包含一個render()方法。

需要注意的是,靜態版的應用,資料由父元件通過props屬性向下傳遞,state屬性是用不到的,記住,state僅僅為動態互動而生。

本應用的元件相對較多,我們不妨採用bottom-up的方式,從子元件開始。

好了,我們開始吧。

StaffHeader

首先以StaffHeader為例,建立一個StaffHeader.js檔案。如下:


import React from 'react';
export default class StaffHeader extends React.Component{

    render(){
        return (
          <div>
              <h3 style={{'text-align':'center'}}>人員管理系統</h3>
              <table className="optHeader">
                <tbody>
                  <tr>
                    <td className="headerTd"><input type='text' placeholder='Search...' /></td>
                    <td className="headerTd">
                        <label for='idSelect'>人員篩選</label>
                        <select id='idSelect'>
                            <option value='0'>全部</option>
                            <option value='1'>主任</option>
                            <option value='2'>老師</option>
                            <option value='3'>學生</option>
                            <option value='4'>實習</option>
                        </select>
                    </td>
                    <td>
                        <label for='orderSelect'>排列方式</label>
                        <select id='orderSelect'>
                            <option value='0'>身份</option>
                            <option value='1'>年齡升</option>
                            <option value='2'>年齡降</option>
                        </select>
                    </td>
                  </tr>
                </tbody>
              </table>
          </div>
        );
    }
}

該元件主要用於提供搜尋框,人員篩選下拉框以及排列方式下拉框。沒錯,我們首先就是要搭建一個靜態版的React。呈現的樣子參考圖三最上方的藍色框。當然,為了實現最終的樣式,需要css的配合,css不是本文的關注點,本應用的css也十分簡單,自行檢視原始碼。

StaffItem

StaffItem是每個具體人員的基本資訊元件,用於展示人員的基本資訊並接收使用者的刪除和點選詳情的操作。新建一個StaffItem.js(該元件在StaffItemPanel中被引用):


import React from 'react';
export default class StaffItem extends React.Component{

    render(){
        return (
              <tr
                style={{'cursor': 'pointer'}}
              >
                <td className='itemTd'>{this.props.item.info.name}</td>
                <td className='itemTd'>{this.props.item.info.age}</td>
                <td className='itemTd'>{this.props.item.info.id}</td>
                <td className='itemTd'>{this.props.item.info.sex}</td>
                <td className='itemTd'>
                    <a className="itemBtn">刪除</a>
                    <a className="itemBtn">詳情</a>
                </td>
              </tr>
        );
    }
}

StaffItemPanel

接下來是StaffItemPanel,該元件僅用於展示由父元件傳入的各個人員條目,新建一個StaffItemPanel.js檔案:


import React from 'react';
import StaffItem from './StaffItem.js';
export default class StaffItemPanel extends React.Component{

    render(){
        let items = [];

        if(this.props.items.length == 0) {
            items.push(<tr><th colSpan="5" className="tempEmpty">暫無使用者</th></tr>);
        }else {
            this.props.items.forEach(item => {
                items.push(<StaffItem key={item.key} item={item}/>);
            });
        }

        return (
          <table className='itemPanel'>
            <thead>
                <th className='itemTd'>姓名</th>
                <th className='itemTd'>年齡</th>
                <th className='itemTd'>身份</th>
                <th className='itemTd'>性別</th>
                <th className='itemTd'>操作</th>
            </thead>
            <tbody>{items}</tbody>
          </table>
        );
    }
}

該元件的功能相對簡單,其中


        if(this.props.items.length == 0) {
            items.push(<tr><th colSpan="5" className="tempEmpty">暫無使用者</th></tr>);
        }else {
            this.props.items.forEach(item => {
                items.push(<StaffItem key={item.key} item={item} />);
            });
        }

是為了在暫無條目的時候給出相應的提示,如下圖:

圖5

StaffFooter

StaffFooter元件的功能是新增新人員,新建StaffFooter.js檔案:


import React from 'react';
export default class StaffFooter extends React.Component{

    render(){
        return (
          <div>
            <h4 style={{'text-align':'center'}}>人員新增</h4>
            <hr/>
            <form ref='addForm' className="addForm">
                <div>
                  <label for='staffAddName' style={{'display': 'block'}}>姓名</label>
                  <input ref='addName' id='staffAddName' type='text' placeholder='Your Name'/>
                </div>
                <div>
                  <label for='staffAddAge' style={{'display': 'block'}}>年齡</label>
                  <input ref='addAge' id='staffAddAge' type='text' placeholder='Your Age(0-150)'/>
                </div>
                <div>
                  <label for='staffAddSex' style={{'display': 'block'}}>性別</label>
                  <select ref='addSex' id='staffAddSex'>
                    <option value='男'></option>
                    <option value='女'></option>
                  </select>
                </div>
                <div>
                  <label for='staffAddId' style={{'display': 'block'}}>身份</label>
                  <select ref='addId' id='staffAddId'>
                    <option value='主任'>主任</option>
                    <option value='老師'>老師</option>
                    <option value='學生'>學生</option>
                    <option value='實習'>實習</option>
                  </select>
                </div>
                <div>
                  <label for='staffAddDescrip' style={{'display': 'block'}}>個人描述</label>
                  <textarea ref='addDescrip' id='staffAddDescrip' type='text'></textarea>
                </div>
                <p ref="tips" className='tips' >提交成功</p>
                <p ref='tipsUnDone' className='tips'>請錄入完整的人員資訊</p>
                <p ref='tipsUnAge' className='tips'>請錄入正確的年齡</p>
                <div>
                  <button>提交</button>
                </div>
            </form>
          </div>
        )
    }
}

程式碼看起來比較長,其實就是一個html表單,這個步驟基本都是不需要太多思考的操作,程式碼也沒有任何理解上的難度,記住,我們現在就是要把整個框架搭起來,做一個靜態版的應用!同樣的,呈現出最終的樣式,需要一些css,自行參考原始碼。呈現的樣子見圖三最下面的藍色方框。

StaffDetail

通常情況下,該元件是不顯示的,只有當用戶點選某條目的詳情的時候,我用了一種動畫效果將該元件’浮現出來’。方法就是在css中將該元件的z-index設定為一個很大的值,比如100,然後通過逐漸改變背景透明度的動畫實現浮現的效果。目前我們只需要做一個靜態版的React,尚未實現使用者點選操作的互動,所以這裡只需要建立以下js檔案,並在css中將.overLay的display設定為none就可以了,原始碼中的css檔案已經做好了。


import React from 'react';
export default class StaffDetail extends React.Component{

    render(){
      let staffDetail = this.props.staffDetail;  
      if(!staffDetail)
        return null;

      return (
          <div className="overLay">
            <h4 style={{'text-align':'center'}}>點選'完成'儲存修改,點選'關閉'放棄未儲存修改並退出.</h4>
            <hr/>
            <table ref="editTabel">
              <tbody>
                <tr>
                  <th>姓名</th>
                  <td><input id='staffEditName' type="text" defaultValue={staffDetail.info.name}></input></td>
                </tr>
                <tr>
                  <th>年齡</th>
                  <td><input id='staffEditAge' type="text" defaultValue={staffDetail.info.age}></input></td>
                </tr>
                <tr>
                  <th>性別</th>
                  <td>
                    <select ref='selSex' id='staffEditSex'>
                      <option value="男"></option>
                      <option value="女"></option>
                    </select>
                  </td>
                </tr>
                <tr>
                  <th>身份</th>
                  <td>
                    <select ref="selId" id='staffEditId'>
                      <option value="主任">主任</option>
                      <option value="老師">老師</option>
                      <option value="學生">學生</option>
                      <option value="實習">實習</option>
                    </select>
                  </td>
                </tr>
                <tr>
                  <th>個人描述</th>
                  <td><textarea id='staffEditDescrip' type="text" defaultValue={staffDetail.info.descrip}></textarea></td>
                </tr>
              </tbody>
            </table>
            <p ref='Dtips' className='tips'>修改成功</p>
            <p ref='DtipsUnDone' className='tips'>請錄入完整的人員資訊</p>
            <p ref='DtipsUnAge' className='tips'>請錄入正確的年齡</p>
            <button>完成</button>
            <button>關閉</button>
          </div>
      );
    }
}

和staffFooter類似,這裡主要就是一個表單。

ManageSystem

子元件都已經做好了,接下來就是最外層的容器了。按部就班,新建一個ManageSystem.js:


import React from 'react';
import StaffHeader from './StaffHeader.js';
import StaffItemPanel from './StaffItemPanel.js';
import StaffFooter from './StaffFooter.js';
import StaffDetail from './StaffDetail.js';

var rawData = [{ info: {descrip:'我是一匹來自遠方的狼。', sex: '男', age: 20, name: '張三', id: '主任'}},
               { info: {descrip:'我是一匹來自遠方的狼。', sex: '女', age: 21, name: '趙靜', id: '學生'}},
               { info: {descrip:'我是一匹來自遠方的狼。', sex: '女', age: 22, name: '王二麻', id: '學生'}},
               { info: {descrip:'我是一匹來自遠方的狼。', sex: '女', age: 24, name: '李曉婷', id: '實習'}},
               { info: {descrip:'我是一匹來自遠方的狼。', sex: '男', age: 23, name: '張春田', id: '實習'}},
               { info: {descrip:'我是一匹來自遠方的狼。', sex: '男', age: 22, name: '劉建國', id: '學生'}},
               { info: {descrip:'我是一匹來自遠方的狼。', sex: '男', age: 24, name: '張八', id: '主任'}},
               { info: {descrip:'我是一匹來自遠方的狗。', sex: '男', age: 35, name: '李四', id: '老師'}},
               { info: {descrip:'我是一匹來自遠方的豬。', sex: '男', age: 42, name: '王五', id: '學生'}},
               { info: {descrip:'我是一匹來自遠方的牛。', sex: '男', age: 50, name: '趙六', id: '實習'}},
               { info: {descrip:'我是一匹來自遠方的馬。', sex: '男', age: 60, name: '孫七', id: '實習'}}];

class App extends React.Component {

    render(){
      return (
        <div>
          <StaffHeader/>
          <StaffItemPanel items={rawData} />
          <StaffFooter/>
          <StaffDetail/>
        </div>
      );
    }
}

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

以上程式碼中rawData是演示資料,生產中的資料應該從資料庫獲得,這裡為了簡便,直接生成了11條演示用的資料。

第三步:編譯並打包

在第二步中,我們已經生成了各個component以及subcomponent。主要的任務已經完成了,這一步是做什麼的呢?

簡單地說,上文中我們編寫React Component的過程中,使用了es6和JSX的語法。(特別值得一提的是es6的Module,終於從語言規格上讓Javascript擁有了模組功能。如今js漸入佳境,學習es6是十分重要且值得的!)但這些目前是不能被瀏覽器直接支援的。所以在使用之前,要先經過’編譯’,這個過程我們是使用Babel完成的。

關於Babel,正如其官網所言–Babel is a Javascript compiler.本應用中,它幫我們完成了es6以及JSX的編譯。只不過在本例中babel是以webpack的loader的方式出現的。

關於webpack這裡也不多言了–webpack is a module bundler.請大家自己查閱相關資料。

安裝依賴項

在這裡,首先執行以下命令,安裝開發依賴:

npm install

該命令會自動讀取當前目錄下的package.json檔案,並自行安裝其中的依賴項。檔案內容如下:

{
  "name": "StaffManage",
  "version": "1.0.0",
  "description": "",
  "main": "",
  "scripts": {
    "start": "webpack"
  },
  "author": "WYH",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.14.0",
    "babel-loader": "^6.2.5",
    "babel-preset-es2015": "^6.14.0",
    "babel-preset-react": "^6.11.1",
    "webpack": "^1.13.2"
  }
}

更具體地說,其中的開發依賴項就是

  "devDependencies": {
    "babel-core": "^6.14.0",
    "babel-loader": "^6.2.5",
    "babel-preset-es2015": "^6.14.0",
    "babel-preset-react": "^6.11.1",
    "webpack": "^1.13.2"
  }

編譯打包

安裝開發依賴項後,接下來就是使用webpack打包了,webpack的loader在解析檔案的時候會自動使用babel對檔案進行編譯。配置檔案如下:

module.exports = {
    entry: __dirname + '/src/ManageSystem.js',
    output: {
        path: __dirname + '/build',
        filename: "bundle.js"
    },
    externals: {
        'react': 'React'
    },
    devtool: 'eval-source-map',  //生成source file
    module: {
        loaders: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: {
              presets: ['es2015', 'react']
            }
          }
        ]
    }
};

將第二步中的所有元件都放到當前目錄下的src目錄中,目錄結構可以參考原始碼,然後執行以下命令:

npm start

該命令也是在package.json中指定的。

"scripts": {
  "start": "webpack"
}

好了,在build目錄下應該已經生成bundle.js檔案,這就是我們打包好的檔案,我們只需要在html中引用它就行了。

在當前目錄下生成html檔案如下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <title>人員管理</title>
    <link href="build/style.css" rel="stylesheet" />

  </head>
  <body>
      <div id="app">
      </div>

      <script src="http://cdn.bootcss.com/react/0.13.3/react.min.js"></script>
      <script src="build/bundle.js"></script>
  </body>
</html>

接下來在瀏覽器中開啟index.html看看吧,靜態版的React已經生成了,只是還沒有動態互動而已。至此,已經完成了構建靜態版的React的工作,大框架已經建立,接下來我們讓它動起來!

第四步:新增STAFF類

本文應用涉及的功能有排序,篩選、新增、刪除、修改以及關鍵字搜尋等。功能較多,業務邏輯有些複雜。為了讓React集中精力完成view層的事情,我們這裡新建一個STAFF類來完成業務邏輯。

Javascript中,類的實現是基於其原型繼承機制的。但在es6中,提供了更接近傳統面嚮物件語言的寫法,引入了類(class)的概念。我們可以通過class關鍵字來定義類。實際上,es6的class只是一個語法糖(syntax sugar),它的絕大部分功能,es5均可以做到。而引入的class寫法,是為了讓物件的寫法更加清晰、更加具有面向物件的感覺。

接下來我們新建一個STAFF.js檔案:


class staffItem {
    constructor(item){
        this.info = {};
        this.info.name = item.name;
        this.info.age = item.age || 0;
        this.info.sex = item.sex;
        this.info.id = item.id;
        this.info.descrip = item.descrip || '';
        this.key = ++staffItem.key;
    }
}
staffItem.key = 0;

export default class STAFF {

    constructor(){
        this.allStaff = [
            new staffItem(STAFF.rawData[0]),
            new staffItem(STAFF.rawData[1]),
            new staffItem(STAFF.rawData[2]),
            new staffItem(STAFF.rawData[3]),
            new staffItem(STAFF.rawData[4]),
            new staffItem(STAFF.rawData[5]),
            new staffItem(STAFF.rawData[6]),
            new staffItem(STAFF.rawData[7]),
            new staffItem(STAFF.rawData[8]),
            new staffItem(STAFF.rawData[9]),
            new staffItem(STAFF.rawData[10])
        ];
        this.staff = this.allStaff;
    }
}

STAFF.rawData = [{ descrip:'我是一匹來自遠方的狼。', sex: '男', age: 20, name: '張三', id: '主任'},
                 { descrip:'我是一匹來自遠方的狼。', sex: '女', age: 21, name: '趙靜', id: '學生'},
                 { descrip:'我是一匹來自遠方的狼。', sex: '女', age: 22, name: '王二麻', id: '學生'},
                 { descrip:'我是一匹來自遠方的狼。', sex: '女', age: 24, name: '李曉婷', id: '實習'},
                 { descrip:'我是一匹來自遠方的狼。', sex: '男', age: 23, name: '張春田', id: '實習'},
                 { descrip:'我是一匹來自遠方的狼。', sex: '男', age: 22, name: '劉建國', id: '學生'},
                 { descrip:'我是一匹來自遠方的狼。', sex: '男', age: 24, name: '張八', id: '主任'},
                 { descrip:'我是一匹來自遠方的狗。', sex: '男', age: 35, name: '李四', id: '老師'},
                 { descrip:'我是一匹來自遠方的豬。', sex: '男', age: 42, name: '王五', id: '學生'},
                 { descrip:'我是一匹來自遠方的牛。', sex: '男', age: 50, name: '趙六', id: '實習'},
                 { descrip:'我是一匹來自遠方的馬。', sex: '男', age: 60, name: '孫七', id: '實習'}];

在STAFF.js中我們實際上建立了2個類,為了實現更好的’封裝性’,我們將每一個人員條目單獨作為一個staffItem類,該物件中包含了該人員的所有資訊,在本應用中包含他的姓名、年齡、性別、身份、個人描述等,實踐中我們可以加入類似入職時間,福利薪酬,個人經歷等資訊。另外還有一個key值,它是一個類變數,這個值是唯一標識該staffItem用的。

在第二步,我們在ManageSystem.js中偽造了一些資料,現在我們也把它搬到STAFF中。畢竟React不是存資料用的。

在STAFF類的建構函式中,建立了2個例項變數,一個是allStaff,其中儲存所有staffItem;一個是staff,它是最終需要給React展示的資料,是經過使用者篩選操作、關鍵字搜尋操作之後得到的人員陣列。之所以這麼設計變數也是為了後面的篩選、搜尋等功能。在這裡我們尚無這些操作的邏輯,直接將allStaff賦給staff即可。

好了,接下來在ManageSystem中引入Staff.js,並初始化state:


import React from 'react';
import StaffHeader from './StaffHeader.js';
import StaffItemPanel from './StaffItemPanel.js';
import StaffFooter from './StaffFooter.js';
import StaffDetail from './StaffDetail.js';

import STAFF from './STAFF.js';

class App extends React.Component {

    constructor(){
        super();
        this.state = {
            staff : new Staff
        };
    }

    render(){
      return (
        <div>
          <StaffHeader/>
          <StaffItemPanel items={this.state.staff.staff} />
          <StaffFooter/>
          <StaffDetail/>
        </div>
      );
    }
}

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

在建構函式中,new了一個STAFF類,然後將this.state.staff.staff傳入<StaffItemPanel/>的items屬性。

然後重新編譯打包:

npm start

再次在瀏覽器開啟index.html檔案,雖然還是一個靜態版的React,不過它已經變得更加模組化和’專一’了,結構也更加漂亮。

第五步:完成新增人員功能

關於state

上文說過,state是為互動而生的。React是自上而下的單向資料流,state通常由上層元件擁有並控制,state的變化將觸發建立在該state上的一系列自上而下的元件更新。注意,元件只能update它自己的state,如果下層元件的操作希望改變應用的狀態,形成一個inverse data flow–反向資料流,我們需要從上層元件傳入一個回撥函式。關於state如何確定,可以參考官網一篇文章Thinking in React。接下來我們看人員功能新增是如何完成的。

實現人員新增功能

真正讓React動起來,不妨從新增人員邏輯開始吧,這個功能比較純粹,和其他業務耦合度不高。重新開啟StaffFooter.js,加入部分程式碼:


import React from 'react';
export default class StaffFooter extends React.Component{

    handlerAddClick(evt){
        evt.preventDefault();
        let item = {};
        let addForm = React.findDOMNode(this.refs.addForm);
        let sex = addForm.querySelector('#staffAddSex');
        let id = addForm.querySelector('#staffAddId');

        item.name = addForm.querySelector('#staffAddName').value.trim();
        item.age = addForm.querySelector('#staffAddAge').value.trim();
        item.descrip = addForm.querySelector('#staffAddDescrip').value.trim();
        item.sex = sex.options[sex.selectedIndex].value;
        item.id = id.options[id.selectedIndex].value;

        /*
         *表單驗證
         */
        if(item.name=='' || item.age=='' || item.descrip=='') {
            let tips = React.findDOMNode(this.refs.tipsUnDone);
            tips.style.display = 'block';
            setTimeout(function(){
                tips.style.display = 'none';
            }, 1000);
            return;
        }
        //非負整數
        let numReg = /^\d+$/;
        if(!numReg.test(item.age) || parseInt(item.age)>150) {
            let tips = React.findDOMNode(this.refs.tipsUnAge);
            tips.style.display = 'block';
            setTimeout(function(){
                tips.style.display = 'none';
            }, 1000);
            return;
        }

        this.props.addStaffItem(item);
        addForm.reset();

        //此處應在返回新增成功資訊後確認
        let tips = React.findDOMNode(this.refs.tips);
        tips.style.display = 'block';
        setTimeout(function(){
            tips.style.display = 'none';
        }, 1000);
    }

    render(){
        return (
          <div>
            <h4 style={{'text-align':'center'}}>人員新增</h4>
            <hr/>
            <form ref='addForm' className="addForm">
                <div>
                  <label for='staffAddName' style={{'display': 'block'}}>姓名</label>
                  <input ref='addName' id='staffAddName' type='text' placeholder='Your Name'/>
                </div>
                <div>
                  <label for='staffAddAge' style={{'display': 'block'}}>年齡</label>
                  <input ref='addAge' id='staffAddAge' type='text' placeholder='Your Age(0-150)'/>
                </div>
                <div>
                  <label