前言:经过了近半个月的探索学习,初步揭开了react这一UI库的神秘面纱,后续会深入学习redux以及相关框架,总结下来:react、es6、webpack就跟html、css、js一样是三兄弟,相互之间必须默契配合才能游刃有余地解决相关dealing,这篇文章就先以经典粒子todolist来做个小小的总结吧,这个例子虽然简单,但体现的都是满满的套路和组件化带来的思想


首先附上:todolist在线演示demo

webpack篇

什么是webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用

为什么要使用webpack

编译代码 我们的项目要是用大量的es6语法糖,jsx语法以及模块化的项目结构,而我们写的这些浏览器根本就不认(虽然es6已经支持地很好了,IE除外,但也远远不够),所以,webpack的工作就是把这些代码编译成浏览器认识的代码。就本demo 来说如果不使用,我们就只有勉强通过引入相关js来处理,如下,但实行起来语法糖会千差万别

1
2
3
<script src="https://cdn.bootcss.com/react/15.4.2/react.min.js"></script>
<script src="https://cdn.bootcss.com/react/15.4.2/react-dom.min.js"></script>
<script src="https://cdn.bootcss.com/babel-standalone/6.22.1/babel.min.js"></script>

模块化 为了让一个提高一个项目的耦合性,我们必须将各种各样的组件粒度分细,让开发思路有条不紊,而且模块化也是现在前端开发的必然要求

优化开发环境 目前这对新手的我来说不敢泛泛总结,目前用到的相关配置仅有webpack-server,HotModuleReplacementPlugin,ProvidePlugin等等,等有其他需求再去啃文档吧

让webpack+react+es6项目跑起来的基本依赖

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
{
"scripts": {
"dev": "webpack",
"start": "webpack-dev-server --inline --hot --config webpack.config.js"
},
"dependencies": {
"prop-types": "^15.6.0",
"react": "^16.1.1",
"react-dom": "^16.1.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.7",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^2.30.1",
"less": "^2.7.3",
"less-loader": "^4.0.5",
"style-loader": "^0.19.0",
"url-loader": "^0.6.2",
"webpack": "^3.8.1",
"webpack-dev-middleware": "^1.12.0",
"webpack-dev-server": "^2.9.4",
"webpack-hot-middleware": "^2.20.0"
}
}

让webpack+react+es6项目跑起来的基本配置

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
var path = require("path");
var webpack= require("webpack");
var htmlplugin = require("html-webpack-plugin");
module.exports = {
entry: ["./src/entry.js"],
output: {
path:path.resolve(__dirname, 'result'),
filename: 'src/js/app.js'
},
plugins: [
new htmlplugin({
filename:'index.html',
//可以写成"path/name.html",不过这样好像开不了热刷新,啃了官网也没找到解决的头绪
//webpack总是绕不开热刷新的话题。热替换的功能配置和原理是一大话题,我们还可以通过
//安装express模块来实现热刷新,但配置略复杂没成功,不管了,先这样吧
template: path.resolve(__dirname,"src/index.html")
})
],
module: {
rules: [
{
test : /\.js$/,
use : [{
loader: "babel-loader",
options: {
presets: ['react','env']
}
}],
exclude: [ path.resolve(__dirname,"node_modules") ]
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: "css-loader",
options: {
module: true,
localIdentName: '[name]-[local]_[hash:base64:6]'
}
}
],
exclude: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'src/public/css')
]
},
{
test: /\.css$/,
use : [ 'style-loader','css-loader' ],
include: [
path.resolve( __dirname,'node_modules' ),
path.resolve( __dirname,'src/public/css' )
]
},
{
test: /\.less$/,
use: [
'style-loader',
{
loader: "css-loader",
options: {
module: true,
localIdentName: '[name]-[local]_[hash:base64:6]'
}
},
'less-loader'
],
exclude: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'src/public/css')
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader','less-loader'],
//loader有加载顺序,从右向左,先把less编译成css,再解析css并引入,最后添加到HTML的<head>
include: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'resources/public/css')
]
},
{
test: /\.(jpg|png|gif|jpeg)$/,
use : [ {
loader: 'url-loader',
options: {
limit : 10000,
name: 'src/imgs/[name]_[hash:6].[ext]'
}
} ]
},
{
test: /\.(ttf|eot|svg|woff|woff2)$/,
use: [ {
loader: 'file-loader',
options: { name: 'src/fonts/[name]_[hash:6].[ext]'}
} ]
}
]
},
resolve: {
modules: [path.resolve(__dirname, "src"), "node_modules"]
},
//相当于添加核心模块,当我们要引入src文件夹下资源的时候,就能像引入核心模块一样根据src文件写绝对路径了
devServer: {
inline: true,
hot: true,
publicPath: '/'
}//配置webpack调试服务器热刷新机制
}

还有一些其他很实用并且提高编译效率的配置:

资源压缩:安装相关插件: npm i -D uglifyjs-webpack-plugin ,引入再在plugins里面加new一项就好了,我开始是装了,好像webpack自带了UglifyJsPlugin插件来压缩js代码,后来也没试了,配置如下

1
2
3
4
5
6
7
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]

拆分css代码:总感觉什么都打包在一个看不动的js文件中怪怪的,虽然没什么,但如果需要对css做cdn优化的话,拆!具体做法不是本文的重点,点击传送门

拆分需要编译的文件: 我们在使用的js库如vue或者react等的时候,webpack会将它们一起打包,react和react-dom文件就好几百k,全部打包成一个文件,可想而知,这个文件会很大,用户在首次打开时就往往会出现白屏等待时间过长的问题,这时,我们就需要将这类文件抽离出来。以上是文艺青年的做法,而我就比较牛逼了^ _ ^,为了编译速度,直接跳过了node_modules ,因为好像编译真的很耗电脑性能!

1
2
3
4
5
6
7
8
9
10
{
test : /\.js$/,
use : [{
loader: "babel-loader",
options: {
presets: ['react','env']
}
}],
exclude: [ path.resolve(__dirname,"node_modules") ]
}

必须深化理解的几个es6知识点

结构赋值:在父组件给子组件传递变量的时候会无休止地运用结构赋值,包括对具有编辑器接口的变量进行展开

class类:每个组件的类都继承着”react”这一核心模块中的Component或者是React.Component

es6的模块化: 通过webpack配置文件中resolve的配置简化引用路径

jxs语法及reactDOM组件

关于jsx: 一种跟HTML及其相似的语法,用来描述页面的结构内容,在编译之后,他会被转化成一个对象,react的底层API就会解析这样复杂的对象,再用js语法去构建页面,深刻地体现着用属性描述对象的思想

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
import React, {Component} from 'react';
import React.DOM from 'react-dom';
class Nav extends Component{
constructor(props){
super(props);
this.state={
key1:val1,
key2:val2
}//整个组件的状态机,它描述或记录着该组件不同状态下的样子或数据
this.func1=this.func1.bind(this);
//修改在jsx结构中用事件所绑定函数的this指向,让他指向组件实例
//这样就不用在零参函数中每次都调用箭头函数了
}
func1(){
some coding...
}
render (){
return (
<div className="nav">
<ul>
<li onClick={ ()=>{ change('cardview') } } >首页</li>
<li onClick={ ()=>{ change('imgview') } } >动态</li>
<li onClick={ ()=>{ change('imgview') } }>更多</li>
</ul>
</div>
)
}
}
ReactDOM.render(
<div>
<Nav/>
<Nav/>
</div>
,
document.getElementById("root")//渲染到模板HTML中的dom元素
);

申明组件与调用:组件名称必须大写,组件引用标签可简写:< Nav >< /Nav > === < Nav />,组件内部的结构标签必须闭合,比如:< input type=”button” value=”” >、< img src=”” alt=”” >这在html结构中是可行的,但在jsx语法中必须用“/”闭合:
< input type=”button” value=”” />、< img src=”” alt=”” />。两个以上的组件在一起渲染或共同被父组件引用时,必须使用外层容器包裹在一起,但一般只会渲染一个总的组件,不为啥,组件化该有的美观

变量传递:{变量||表达式}

组件的属性对象:组件内部的属性都挂载在组件实例的props下,即每个属性通过结构this.props对象即可得到,属性值一般为变量,变量用”{}”包裹:key={变量值},因为组件是活的,每一级组件都承担着传递和使用属性变量的功能

组件属性的类型验证: 组件对接收到的属性变量进行验证,包括类型,范围,是否必须等

1
2
3
4
5
6
7
8
9
10
11
import PropTypes from 'prop-types';
//react15.5.0之后需要单独安装:npm i-D prop-types
let datatypes = {
per1:PropTypes.func,
per2:PropTypes.object,
per3:PropTypes.string,
per4:PropTypes.number,
per5:PropTypes.oneOf(['aaa','bbb','ccc'])
}
Nav.propTypes=datatypes;
//挂载到某个组件下,之后该组件从父组件接收到的props都会接收验证

组件的内部状态机: 组件的状态机挂载在类的构造函数里面,它描述或记录着该组件不同状态下的样子或数据,父组件的状态机里的数据可以将状态变量传下去渲染子组件

this.setState(): 刷新状态,属于异步操作,this.setState ( { 要修改的变量(可简写为键名)} ,callback ),当实例对象下的方法要调用其他方法是,要把setState的异步执行考虑进去,因为往往都是在实现状态刷新之后才会执行某些函数,每次刷新状态难度会引起组件的render()方法。重点指出由于调用栈的不同,多个this.setState( )存在异步更新和批量更新的区别:

  • 批量更新:多个setState一起更新,只进行一次render操作

    1
    2
    3
    4
    5
    6
    7
    8
    func1(){
    this.setState({
    key1:val1
    })
    this.setState({
    key2:val2
    })
    }
  • 异步更新:多个setState依次更新,几个更新就进行几次render操作,这里仅仅是几个粒子,因为大部分情况下,更新都是在后端调取数据之后刷新,为调取数据本省就属于异步操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func1(){
    setTimeout( ()=>{
    this.setState({
    key1:val1
    })
    this.setState({
    key2:val2
    })
    },time )
    }

组件的拆分:没什么好说的,根据UI来分就好了,如todolist,拆成header、section、footer

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
class App extends Component{
constructor(props){
super(props);
this.state={
listinfo:[],
inputval:'',
view:'all'
}
this.addlist = this.addlist.bind(this);
this.listdelete = this.listdelete.bind(this);
this.somedelete = this.somedelete.bind(this);
this.valuechange = this.valuechange.bind(this);
this.selectall = this.selectall.bind(this);
this.selectone = this.selectone.bind(this);
this.shiftview = this.shiftview.bind(this);
this.savemodify = this.savemodify.bind(this);
}
savemodify(list,val){
let {listinfo}=this.state;
listinfo=listinfo.map((item,i)=>{
if(list.id===item.id){
item.value=val;
}
return item;
})
}
shiftview(view){
this.setState({
view:view
})
}
valuechange(ev){
this.setState({
inputval:ev.target.value
})
}
addlist(ev){
if (ev.keyCode!==13) return;
let {listinfo,inputval} = this.state;
if( inputval==='' ) return;
let value = inputval.trim();
let alist={};
alist.value = value;
alist.id = new Date().getTime();
alist.isselected = false;
listinfo.push(alist);
this.setState({
listinfo,
inputval:''
});
}
listdelete(list){
// console.log(list);
let {listinfo}= this.state;
listinfo.forEach(function(item,i){
if(item===list){
listinfo.splice(i,1);
}
})
this.setState({listinfo})
}
somedelete(){
let {listinfo}= this.state;
listinfo = listinfo.filter( (item,i)=>{
return item.isselected == false;
} )
this.setState({listinfo})
}
selectone(list){
let {listinfo}=this.state;
listinfo=listinfo.map((item,i)=>{
if (item.id===list.id) {
item.isselected = !item.isselected;
}
return item;
})
this.setState({
listinfo
})
}
selectall(ev){
let {checked}=ev.target;
let {listinfo}=this.state;
listinfo=listinfo.map((item,i)=>{
item.isselected=checked;
return item;
})
this.setState({
listinfo
})
}
render (){
let {addlist,
listdelete,
somedelete,
valuechange,
selectall,
shiftview,
savemodify,
selectone}=this;
let items = null;
let footer = null;
let {listinfo,inputval,view} = this.state;
let leftnum = listinfo.length;
items = listinfo.filter((item,i)=>{
if (item.isselected) {
leftnum--;
}
switch(view){
case 'todo':
return item.isselected===false;
break;
case 'completed':
return item.isselected===true;
break;
default:
return true;
}
})
items = items.map( (item,i)=>{
return(
<Item
listdelete = {listdelete}
list={item}
key={i}
selectone={selectone}
savemodify={savemodify}
/>
);
} )
if (listinfo.length) {
footer=(
<Footer
{...{
showclear:leftnum<listinfo.length,
leftnum:leftnum,
somedelete:somedelete,
shiftview:shiftview,
view:view
}}
/>)
}
return (
<div>
<header className="header">
<h1>todolist</h1>
<input type="text"
className="newtodo"
placeholder="what can i do for you?"
value={inputval}
onChange={valuechange}
onKeyDown = {addlist}
/>
</header>
<section className="listcontent" >
<input type="checkbox"
className="selectall"
checked={leftnum===0&&listinfo.length!=0}
onChange={selectall}
/>
{items}
</section>
{footer}
</div>
)
}
}
class Item extends Component{
constructor(props){
super(props);
this.state={
canedit:false,
val:''
}
this.onedit=this.onedit.bind(this);
this.onblur=this.onblur.bind(this);
this.onenter=this.onenter.bind(this);
this.editchange=this.editchange.bind(this);
}
editchange(ev){
let val=ev.target.value;
this.setState({
val:val
})
}
onedit(list){
let val=list.value;
this.setState({
canedit:true,
val:val
}, ()=>{this.refs.modify.focus() } );
//setState是异步执行的,输入框的自动聚焦必须放在第二个参数里面当回调使用,切记!
}
onenter(ev){
if(ev.keyCode!==13) return;
let {savemodify,list}=this.props;
let {val}=this.state;
// console.log(savemodify);
savemodify(list,val);
this.setState({
canedit:false
})
}
onblur(){
let {savemodify,list}=this.props;
let {val}=this.state;
savemodify(list,val);
this.setState({
canedit:false
})
}
render (){
let {canedit,val}=this.state;
let {onedit,onblur,onenter,editchange}=this;
let {listdelete,list,selectone,savemodify}=this.props;
let editclass='moren';
if (canedit) {
editclass='moren modify';
}
return (
<div className="todo">
<input
type="checkbox"
className="select"
checked={list.isselected}
onClick={(ev)=>{selectone(list)}}
/>
<label
onDoubleClick={()=>{onedit(list)}}
>{list.value}</label>
<input
type="text"
className={editclass}
value={val}
onBlur={onblur}
onChange={editchange}
onKeyDown={onenter}
ref="modify"
/>
<input
type="button"
className="listdelete"
value="删除"

onClick={ev => listdelete(list) }
/>
</div>
)
}
}
let datatypes1 = {
listdelete:PropTypes.func,
list:PropTypes.object,
savemodify:PropTypes.func,
selectone:PropTypes.func
}
Item.propTypes=datatypes1;
class Footer extends Component{
constructor(props){
super(props)
}
render (){
let { leftnum,showclear,somedelete,shiftview,view }=this.props;
let deletebutton = null;
if (showclear) {
deletebutton = (
<input
type="button"
className="completedel"
value="删除已完成项目"
onClick={somedelete}
/>)
}
return (
<div className="footer">
<span><strong>{leftnum} </strong> item left</span>
<ul>
<li
onClick={()=>{shiftview("all")}}
className={view==="all"?"selected":''}
>all</li>
<li
onClick={()=>{shiftview("todo")}}
className={view==="todo"?"selected":''}
>todo</li>
<li
onClick={()=>{shiftview("completed")}}
className={view==="completed"?"selected":''}
>completed</li>
</ul>
{deletebutton}
</div>
)
}
}
let datatypes2 = {
leftnum:PropTypes.number,
somedelete:PropTypes.func,
showclear:PropTypes.bool,
shiftview:PropTypes.func,
view:PropTypes.oneOf(
['all','todo','completed']
)
}
Footer.propTypes=datatypes2;
ReactDOM.render(
<App/>
,
document.getElementById("root")
);

两个常用的数组方法

  • Array.prototype.filter( ),对数组进行过滤,根据每项值进行判断是否返回,可想而知在页面的许多操作都涉及到数组的过滤
1
2
3
array.filter( (item,i)=>{
return judge(item)
} )
  • Array.prototype.map( ),对数组进行改造,根据每项值进行判断是否改造,之后返回新的数组,原数组不变,经常通过状态机里面的数据来改造出批量的组件,结构之并将数据传递下去
1
2
3
4
array.map( (item,i)=>{
item = modify(item);
return item;
} )

通过this.refs访问组件DOM: todolist中最困扰我的就是可编辑功能了,貌似简单确要绑定很多事件函数:首先让input成为受控组件,让输入框在实时输入的时候都可以在状态机里面拿到其值,双击触发onedit事件出现输入框,这一步通过表达式动态添加类名让该input出现,再通过this.refs拿到真实的input在状态刷新的回调中让其自动聚焦,之后实时输入触发内容值改变函数onchange,esc键还原,回车和失焦事件触发父组件的保存函数savemodify,此时又需要调取父组件状态机里的listinfo的值,同时父组件的也要保存函数savemodify传递下去

Next:React.router…