主流框架在不停的更新换代。在使用了gulp+Jquery半年以上的时候,为了跟上步伐,决定尝试用webpack+react做一个简单的app来熟悉下。

Todo List 就决定是你了!

Webpack

第一次接触webpack的时候觉得这个想法惊为天人!通过各种loader将所有的资源打包,然后插入到html里面:所有非js的资源,都需要通过相对应的loader。而且webpack内还整合很多功能,模块加载啊,ES6支持啊。。。

clipboard.pngWhat is webpack?

安装

安装过程不做赘述,webpack官网有更详细的描述,通过npm安装:$ npm install webpack --save-dev

Loader&Plugins

之前说webpack是把非js资源转换成js,那么就有各式各样的loader,在List of Loaders可以查看列表,但一般来说,会用到如下几种:

1
2
3
css-loader //将css打包
style-loader //将样式输出
url-loader //解析url,小图片会被解析为data-url

除此之外,还有file、json、sass等loader。这里因为使用了React,所以除了这些,还安装了babel-loader。
通过babel,可以将jsx转换成js,也可以使用es6语法。

安装:$ npm install style-loader css-loader url-loader babel-loader sass-loader --save-dev

插件可以提供打包相关的功能:比如将指定打包的CommonsChunkPlugin;压缩js代码的UglifyJsPlugin;限制合并的LimitChunkCountPlugin

具体的插件说明可以在List of Plugins中查看。

Config

要用webpack,需要配置webpack.config.js。在这个配置里面,entry就是这个app的入口在src文件夹,output是编译之后的脚本在dist文件夹。因为这里用了CommonsChunkPlugin,将引用的库单独导出,所以在entry里配置了vendor,编译过之后会生成vendor.js。在module里面可以配置loader,jsx用到babel的es2015和react,scss用到了style、css和sass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var webpack = require('webpack');

module.exports = {
output: {
filename: './dist/[name].js'
},
entry: {
vendor: [
'react',
'react-dom'
],
app: './src/index.jsx'
},

module: {
loaders: [
{test: /\.jsx$/, exclude: /node_modules/, loader: 'babel', query: {presets: ['es2015', 'react']}},
{test: /\.scss$/, exclude: /node_modules/, loaders: ["style", "css", "sass"]}
]
},

plugins: [
new webpack.optimize.CommonsChunkPlugin('vendor', './dist/vendor.js'),
]
}

需要建立一个html文件,将编译过的脚本引入,如果使用脚本分块的话,一定要注意先后依赖:比如vendor.js要在app.js之前。

1
2
<script src="./dist/vendor.js"type="text/javascript"></script>
<script src="./dist/app.js"type="text/javascript"></script>

React

第一次接触React,我是懵逼的。

长时间函数式、关注分离,第一次接触MVVM,表示很纠结、也很方便。纠结的是事件绑定与组建通信的方式;方便的是不用在意界面的修改,而专注于业务。

推荐入门阅读我大神男友的博文:初识 React

JSX

在.jsx文件中,可以直接返回一个元素:

1
2
3
4
5
6
/* 这里的className,是样式class。*/
return (
<div className="content">
Hello, world!
</div>
);

直接写标签,简单易读~!
如果要建组件,要用到React.createClass,注意的一点是组件名一定要首字母大写。
React会建立虚拟DOM树,再之后的更改中会建立另一个DOM树,然后通过有效的比对,在界面上做出相应的更改。

state&props

在react组件中,有stateprops这两个接口。

props是可以为组件组件传递参数,比如:

1
2
3
4
5
6
7
8
var Greating = React.createClass({
render: function(){
return (
<h1>Hello, I'm {this.props.name}! </h1>
);
}
});
React.render(<Greating name="Katherina" />, document.body);

父组件向子组件的参数传递通过props,除此之外可以通过getDefaultProps这个生命周期方法来设置。

state参数是用来实现交互的。起初使用getInitialState获得最初的一个state状态,之后使用setState来控制。

比起手动维护交互时界面的变化,用state就方便多了,然而子组件若是想要改变父组件的state,只能通过事件向父组件传递。

这里就先不举例啦,到Todo List的时候仔细说说。

####生命周期方法####

这次生命周期方法只用到了getInitialState,对于各个方法使用心得和避坑还是参考初识 React

React组件是一个从加载-》更新-》卸载的过程,一共提供10个生命周期方法:

  • getDefaultProps:第一次实例化调用,给组件提供props
  • getInitialState:创建实例时调用,给组件提供初始state
  • componentWillMount:加载组件前调用,可以访问到props和state,并可以修改state
  • render:创建虚拟DOM,每个组件必需的方法
  • componentDidMount:组件加载完成后调用,此时可以通过this.getDOMNode()访问到DOM。
  • componentWillReceiveProps:组件props更新前调用,将props作为参数传入
  • shouldComponentUpdate:组件将要更新时调用,新的 props 和 state 将会作为该方法的参数,该方法应当返回一个布尔值,返回false表示跳过后续的生命周期方法。
  • componentWillUpdate:组件更新之前调用,接收到新的props或者state作为参数,此时不能更新state。
  • componentDidUpdate:组件渲染完成后调用,此时可以访问到新的DOM元素。
  • componentWillUnmount:卸载组件之前调用,可以做一些清理工作,比如清除计时器。

组件加载的时候,会依次调用: (getDefaultProps)、 getInitialStatecomponentWillMountrendercomponentDidMount
其中,只有getDefaultProps是只有第一次实例化才会调用的,之后props会保存下来为组件直接调用。

更新的时候,会依次调用: componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

卸载的时候,会调用componentWillUnmount

TODOS

Todo List是一个经典入门练习。
第一次做webpack+react,因为这两个对我来说都是新东西,所以一开始有点困难。遇到问题也找不到是webpack配置问题,还是react语法问题(对不起,我真的很笨。。。

Anyway,这个入门是在react官网上demo的基础上延伸的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var TodoList = React.createClass({
render: function() {
var createItem = function(itemText) {
return <li>{itemText}</li>;
};
return <ul>{this.props.items.map(createItem)}</ul>;
}
});
var TodoApp = React.createClass({
getInitialState: function() {
return {items: [], text: ''};
},
onChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var nextItems = this.state.items.concat([this.state.text]);
var nextText = '';
this.setState({items: nextItems, text: nextText});
},
render: function() {
return (
<div>
<h3>TODO</h3>
<TodoList items={this.state.items} />
<form onSubmit={this.handleSubmit}>
<input onChange={this.onChange} value={this.state.text} />
<button>{'Add #' + (this.state.items.length + 1)}</button>
</form>
</div>
);
}
});

React.render(<TodoApp />, mountNode);

A JavaScript library for building user interfaces | React

这个demo把一个Todo List的基础功能实现了:可以将新任务添加到任务列表中。

TodoApp是父组件,它用了生命周期方法getInitialState来设置文本框的状态text,列表内的项目items
每次改变文本框内容,setState会改变text的值。在提交的时候,会给items数组多添加一项,同时置空text和文本框。
按钮上面的数字是目前任务条目的个数,当items数组多添加一项的时候,按钮也会随之加1。
TodoApp是父组件中引入了TodoList子组件,并传递了items。在TodoList组建中,对props.itemsmap方法添加了条目。

在此基础上,我除了把界面做漂亮之外(对不起,视觉系的人不能对着毫无样式的页面做功能。。)还增加了勾选完成和删除任务功能。

clipboard.png
——最后效果。(用了sass,需要在入口文件中require

为了让内容记录可以保存,这里使用localStorage来储存。在一开始getInitialState中读取到items,然后每次提交添加、修改和删除条目的时候,都会修改statelocalStorage

1
2
3
4
getInitialState: function() {
var itemsFromStorage = localStorage.getItem('TODOs')?window.JSON.parse(localStorage.getItem('TODOs')):[];
return {items: itemsFromStorage, text: ''};
}

在添加列表条目的基础上,条目上的勾选完成和删除也是修改state。起初添加条目的时候,为条目添加状态 active:

1
2
3
4
5
6
7
8
handleSubmit: function(e) {
e.preventDefault();
var nextItems = this.state.items.concat([{text: this.state.text, status:'active'}]);
var nextText = '';
var itemJson = JSON.stringify(nextItems);
localStorage.setItem('TODOs',itemJson);
this.setState({items: nextItems, text: nextText});
}

之后在勾选完成的时候,会修改status为done:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
changeStatus: function(e) {
var key = e.target.parentNode.getAttribute('id');
var items = this.state.items;
var status = this.state.items[key]['status'];
if (status === 'done'){
items[key]['status'] = 'active';
}else{
items[key]['status'] = 'done';
}
this.changeStates(items);
},
changeStates: function(items) {
this.setState({items: items});
localStorage.setItem('TODOs',JSON.stringify(items));
}

除此之外,删除条目的时候,根据条目的索引,删除state中对应项。

1
2
3
4
5
6
7
8
9
10
removeItem: function(e) {
var key = e.target.parentNode.getAttribute('id');
var items = this.state.items;
items.splice(key, 1);
this.changeStates(items);
},
changeStates: function(items) {
this.setState({items: items});
localStorage.setItem('TODOs',JSON.stringify(items));
}

另外,加了一个用作筛选的选项卡,在子组件中添加一个额外的state:all的时候显示所有;active的时候显示未完成的;complete的时候显示已完成。点击标签的时候,切换state,然后在render的时候做筛选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var TodoList = React.createClass({
changeTab : function(e){
var type = e.target.getAttribute('type');
this.setState({type:type})
},
getInitialState: function() {
return {type:'all'};
},
render: function() {
var style = {};
if(this.props.items.length === 0){
style = {display:'none'};
}

return <div className="itemList">
<div className="clearfix">
{this.props.items.length} items
<nav style={style}>
<a className={this.state.type === 'all'?'navTab active':'navTab'} type='all' onClick={this.changeTab}>ALL</a>
<a className={this.state.type === 'active'?'navTab active':'navTab'} type='active' onClick={this.changeTab}>ACTIVE</a>
<a className={this.state.type === 'done'?'navTab active':'navTab'} type='done' onClick={this.changeTab}>COMPLETE</a>
</nav>
</div>
<ul>
{this.props.items.map((item, index)=>{
if(this.state.type === 'all'||this.state.type === item.status){
return <li key={index} id={index} className={item.status}>
<input type="checkbox" className="toggle" onChange={this.props.changeStatus} checked={item.status === 'done'}/>
<lable>{item.text}</lable>
<button className="remove" onClick={this.props.removeItem}></button>
</li>;
}
})
}
</ul>
</div>;
}
});

最后要说的是组件之间的数据传递。
父组件向子组件传递,通过props

1
<TodoList items={this.state.items} changeStatus={this.changeStatus} removeItem={this.removeItem}/>

而子组件向父组件传递,通过事件。事件定义在父组件,通过props传递到子组件,绑定触发时可以改变父组件的state

下面就是index.jsx的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
var TodoList = React.createClass({
changeTab : function(e){
var type = e.target.getAttribute('type');
this.setState({type:type})
},
getInitialState: function() {
return {type:'all'};
},
render: function() {
var style = {};
if(this.props.items.length === 0){
style = {display:'none'};
}

return <div className="itemList">
<div className="clearfix">
{this.props.items.length} items
<nav style={style}>
<a className={this.state.type === 'all'?'navTab active':'navTab'} type='all' onClick={this.changeTab}>ALL</a>
<a className={this.state.type === 'active'?'navTab active':'navTab'} type='active' onClick={this.changeTab}>ACTIVE</a>
<a className={this.state.type === 'done'?'navTab active':'navTab'} type='done' onClick={this.changeTab}>COMPLETE</a>
</nav>
</div>
<ul>
{this.props.items.map((item, index)=>{
if(this.state.type === 'all'||this.state.type === item.status){
return <li key={index} id={index} className={item.status}>
<input type="checkbox" className="toggle" onChange={this.props.changeStatus} checked={item.status === 'done'}/>
<lable>{item.text}</lable>
<button className="remove" onClick={this.props.removeItem}></button>
</li>;
}
})
}
</ul>
</div>;
}
});
var TodoApp = React.createClass({
getInitialState: function() {
var itemsFromStorage = localStorage.getItem('TODOs')?window.JSON.parse(localStorage.getItem('TODOs')):[];
return {items: itemsFromStorage, text: ''};
},
onChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var nextItems = this.state.items.concat([{text: this.state.text, status:'active'}]);
var nextText = '';
var itemJson = JSON.stringify(nextItems);
localStorage.setItem('TODOs',itemJson);
this.setState({items: nextItems, text: nextText});
},
changeStatus: function(e) {
var key = e.target.parentNode.getAttribute('id');
var items = this.state.items;
var status = this.state.items[key]['status'];
if (status === 'done'){
items[key]['status'] = 'active';
}else{
items[key]['status'] = 'done';
}
this.changeStates(items);
},
changeStates: function(items) {
this.setState({items: items});
localStorage.setItem('TODOs',JSON.stringify(items));
},
removeItem: function(e) {
var key = e.target.parentNode.getAttribute('id');
var items = this.state.items;
items.splice(key, 1);
this.changeStates(items);
},
render: function() {
return (
<div className="pure-u-11-12">
<header>
<h3>TODOs</h3>
</header>
<form onSubmit={this.handleSubmit} className='submitForm'>
<input type='text' className='submitInput pure-u-3-4 pure-u-md-5-6 pure-u-lg-7-8' onChange={this.onChange} value={this.state.text} placeholder="What needs to be done?"/>
<button className='submitBtn pure-u-1-4 pure-u-md-1-6 pure-u-lg-1-8' disabled={this.state.text===''}>{'Add'}</button>
</form>
<TodoList items={this.state.items} changeStatus={this.changeStatus} removeItem={this.removeItem}/>
</div>
);
}
});

完整代码请戳>>github: todolist-react