React 的核心思想是:封裝組件,各個組件維護本身的狀態和 UI,當狀態變動,自動從新渲染整個組件。css
最近前端界鬧的沸沸揚揚的技術當屬react
了,加上項目須要等等緣由,本身也決定花些時間來好好認識下這個東西。而後花時間本身寫了一個demo:react-todos
, 你能夠先點這裏去看react-todohtml
react首先值得拍手稱讚的是它全部的開發都基於一個組件(component)
,組件和組件之間傳遞方法,並且每一個組件都有一個狀態(state)
,當方法改變了這個狀態值時,整個組件就會重繪
,從而達到刷新,另外,說到重繪就要提到虛擬dom
了,就是用js模擬dom結構,等整個組件的dom更新完畢,才渲染到頁面,簡單來講只更新了相比以前改變了的部分,而不是所有刷新,因此效率很高。前端
你們先新建一個項目文件夾,在裏面建一個項目信息的文件package.json
:node
{ "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" } }
建好以後,運行命令:react
npm install
安裝項目依賴的全部模塊。安裝好以後,另外還有一點,項目數據是存儲在本地瀏覽器的,因此我找到一個小模塊用來操做localStorage,它的原理就是,經過將數據格式化成JSON字符串進行存儲,使用的時候就解析JSON字符串。他的代碼點這裏看localDb能夠看到,你能夠複製一份,放在node_modules的文件夾內。webpack
項目使用的技術方案是:webpack+react+es6
。關於es6的文章,我以前簡單的介紹過,能夠點這裏去看es6,關於webpack的學習,我這裏不詳述了,看之後有時間再出篇文章吧。在項目文件夾下新建一個webpack.config.js
:git
'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:es6
jsx-loader
和babel-loader
來編譯js文件。sass-loader
編譯成css文件。!
鏈接。先來看一下項目的目錄結構,最重要的就是src目錄:github
index.html
是項目的入口頁面。components
文件夾存放項目拆分出來的各個組件文件。vendor
文件夾存放項目依賴的框架,這裏只有react。先來看index.html:web
<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的項目,咱們能夠分爲三個部分:頭部,中間部分,尾部。那咱們就來逐一的分析一下這些組件:
'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上,看的東西稍微有點多。一點點來看:
繼承React.Components的App類
。狀態state
。props傳遞給子組件
。render
。this.state = { //定義組件狀態 todos: this.db.get('todos') || [], isAllChecked: false };
在App組件的構造函數裏,咱們初始化了組件的state,分別有兩個,一個是todos的列表,一個是全部的todos是否全選的狀態。在渲染的時候,咱們會把狀態傳遞到子組件中,若是子組件的某一個方法讓狀態發生了改變,那麼整個組件就會進行重繪。
// 判斷是否全部任務的狀態都完成,同步底部的全選框 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)} />
說明:
/
閉合起來。ES6語法,spread操做符讓代碼簡潔不少,如上述代碼中的TodoFooter:
<TodoFooter {...info} /> //若是不使用spread操做符,就要這樣寫: <TodoFooter isAllchecked={info.isAllChecked} todoCount={info.todoCount} todoDoneCount={info.todoDoneCount}>
React.render(<App/>, document.getElementById('app'));
把上面的App組件的內容渲染到id爲'app'的dom元素裏。
而後咱們再簡單看一下分解出來的三個組件:TodoHeader
, TodoMain
, TodoFooter
。
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()方法
。
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}
,代碼中也作了註釋,能夠洗洗品味一下。
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
。
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的風景真的很優美!
參考資料: