前言

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的风景真的很优美!

参考资料:

React入门最好的学习实例-TodoList的更多相关文章

  1. React入门基础(学习笔记)

    这篇博客是我通过阅读React官方文档的教程总结的学习笔记,翻译可能存在误差,如有疑问请参见http://reactjs.cn/react/docs/tutorial.html . 一.所需文件 在编 ...

  2. React 入门-写个 TodoList 实例

    React 是一个用于构建用户界面的 JavaScript 库,主要特点有: 声明式渲染:设计好数据和视图的关系,数据变化 React 自动渲染,不必亲自操作DOM 组件化:页面切分成多个小部件,通过 ...

  3. React 入门实例教程

    现在最热门的前端框架,毫无疑问是 React . 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React 起源于 Face ...

  4. 2015年最热门前端框架React 入门实例教程

    现在最热门的前端框架,毫无疑问是 React . 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React 起源于 Face ...

  5. React入门实例教程

    文章转自:阮一峰 现在最热门的前端框架,毫无疑问是 React . 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React ...

  6. react入门学习及总结

    前言 不知不觉一年又过去了,新的一年又到来,2019应该要好好思考,好好学点有用的东西,规划下自己今后的学习方向,不要再像以前那样感觉很迷茫. react简单介绍 官网及中文文档 https://re ...

  7. React 入门学习笔记整理目录

    React 入门学习笔记整理(一)--搭建环境 React 入门学习笔记整理(二)-- JSX简介与语法 React 入门学习笔记整理(三)-- 组件 React 入门学习笔记整理(四)-- 事件 R ...

  8. React 入门实例教程(转载)

    现在最热门的前端框架,毫无疑问是 React . 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React 起源于 Face ...

  9. 【转】react入门实例教程

    作者: 阮一峰 日期: 2015年3月31日 写在前面:原文链接http://www.ruanyifeng.com/blog/2015/03/react.html    github地址https:/ ...

随机推荐

  1. test markdown

    Markdown和Haroopad介绍文档 [TOC "float:right"] 什么是Markdown Markdown 是一种轻量级标记语言,创始人为约翰·格鲁伯(John ...

  2. 使用未付费的账号真机调试 iOS 程序,过几天后程序一打开就会闪退

    使用未付费的苹果开发者账号真机调试 iOS 程序,过几天后程序一打开就会闪退.   解决办法: 删除 Provisioning Profile,重新配置一次. 终极解决办法:花钱购买苹果开发者账号. ...

  3. Owin的URL编码怎么搞?以前都是HttpUtility.UrlEncode之类的,现在连system.web都没了,肿么办?

    Owin的URL编码怎么搞?以前都是HttpUtility.UrlEncode之类的,现在连system.web都没了,肿么办? 编码: Uri.EscapeDataString(name) 解码: ...

  4. 【源码】谷歌代理~WPF简单小软件-2015-10-15首发 (2016-03-01已更新源)

    蛋疼,昨天把代理去了后才发现,原来咱们连谷歌应用都访问不了,,,用别人的总觉得不怎么安全,然后今天早上就编了个小软件干掉他这限制==>  GoogleProxy.exe [主要目的:为了能在线安 ...

  5. OpenCASCADE Make Primitives-Sphere

    OpenCASCADE Make Primitives-Sphere eryar@163.com Abstract. The sphere is the simplest topology shape ...

  6. Ubuntu 14.04 64bit 安装tensorflow(GPU版本)

    本博客主要用于在Ubuntu14.04 64bit 操作系统上搭建google开源的深度学习框架tensorflow. 0.安装CUDA和cuDNN 如果要安装GPU版本的tensorflow,就必须 ...

  7. Android Drawable、Bitmap、byte[]之间的转换

    转自http://blog.csdn.net/june5253/article/details/7826597 1.Bitmap-->Drawable Bitmap drawable2Bitma ...

  8. 【开源】OSharp3.0框架解说系列(6.2):操作日志与数据日志

    OSharp是什么? OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现.与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现.依 ...

  9. ASP.NET MVC之Session State性能问题(七)

    前言 这一节翻译一篇有关Session State性能问题的文章,非一字一句翻译. 话题 不知道我们在真实环境中是否用到了Session State特性,它主要用来当在同一浏览器发出多个请求时来存储数 ...

  10. The network bridge on device VMnet0 is not running

    The network bridge on device VMnet0 is not running. The virtual machine will not be able to communic ...