學習React入門最好的例項-TodoList
前言
React 的核心思想是:封裝元件,各個元件維護自己的狀態和 UI,當狀態變更,自動重新渲染整個元件。
最近前端界鬧的沸沸揚揚的技術當屬react了,加上專案需要等等原因,自己也決定花些時間來好好認識下這個東西。然後學習的時候順便花時間寫了一個demo:react-todos, 為了提起興趣,你可以先點這裡去看react-todo
首先react值得拍手稱讚的是它所有的開發都基於元件(component),然後元件和元件之間通過props傳遞方法,每個元件都有一個狀態(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-loader
和babel-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="請輸入你的任務名稱,按Enter鍵確認"/>
</div>
)
}
}
export default TodoHeader; //ES6語法,匯出模組,上文提到的es6文章中有講解
TodoHeader元件的建立方法和App元件的建立方法一樣,內部方法就少了很多了,這裡就定義了一個監聽鍵盤的方法,繫結到了輸入框的keyUp事件上,敲擊Enter鍵的時候就會呼叫父元件傳過來的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的風景真的很優美!
ps: 本人剛開始學習react,如果有理解不對的地方,望各位前輩指出!
參考資料:
- http://www.reqianduan.com/2297.html
- http://wiki.jikexueyuan.com/project/react-tutorial/
- http://gank.io/post/564151c1f1df1210001c9161
相關文章
- React 入門最好的例項-TodoListReact
- React 入門-寫個 TodoList 例項React
- React入門學習例項React
- react-dva學習 --- 用例項來入門React
- React 入門例項教程React
- React入門學習React
- 【深度學習】--DCGAN從入門到例項應用深度學習
- 結合例項學習F#(一) --快速入門
- Redux 入門 Demo:TodoListRedux
- React入門指南(學習筆記)React筆記
- React入門例項參考阮一峰部落格React
- [邊學邊練]用簡單例項學習React單例React
- TypeScript入門例項TypeScript
- Websocet 入門例項Web
- SoapUI入門例項UI
- Flutter 入門例項Flutter
- Kafka入門例項Kafka
- Struts入門例項
- 深度學習、神經網路最好的入門級教程深度學習神經網路
- 通過例項,學習編寫 React 元件的“最佳實踐”React元件
- ActiveMQ 入門及例項MQ
- React從入門到精通學習系列之(1)安裝ReactReact
- 例項:學習XOR
- Makefile例項學習
- opengl簡單入門例項
- Django+MySQL 例項入門DjangoMySql
- Vue專案入門例項Vue
- Jquery入門及例項一jQuery
- Android程式猿的react學習之路-入門指南篇AndroidReact
- Java入門學習注意事項有哪些?Java
- React 系列一 之 TodoListReact
- tail命令學習例項AI
- vue 快速入門的三個小例項Vue
- vue快速入門的三個小例項Vue
- MyBatis基於Maven入門例項MyBatisMaven
- Web Components 入門例項教程Web
- 【Oracle】ASM例項安裝入門OracleASM
- Shell程式設計入門例項程式設計