學習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 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 onClick={this.handlerAddClick.bind(this)}>提交