xixijiang的主页


  • Home

  • Archives

  • Tags

react的5个demo

Posted on 2017-07-23

下面只讲述5个react的实例,虽然仅有5个,但在常用的开发中,几乎会包含大部分的情况,只要熟练掌握这5个demo,相信一定会解决大部分问题。

  • demo都是采用ES6语法写的,不懂的可以学习一下。

DEMO 1 - 最简单的react渲染

代码:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<style type="text/css">
#name{
background: #5bc0de;
color: white;
font-size: 24px;
}
</style>
</head>
<body>
<button id="name">DEMO1</button>
<div id="root"></div>
<script type="text/babel">
class Text extends React.Component {
render(){
return (
<div className="text">
hello, this is rendered by React!
</div>
)
}
}

ReactDOM.render(
<Text/>,
document.getElementById('root')
);
</script>
</body>
</html>

在浏览器中显示的效果如下

这里写图片描述

讲解:

  • 页面中,只有<div id='name'>这里的内容是使用react渲染出来的,代码中这里是空的,依赖下面的js进行渲染
  • 首先看下 class Text extends React.Component 这块,这里是声明一个组件,名字叫做Text,名字随意起,但第一个字母必须大写,用来区分html中原生的标签

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Text extends React.Component {
    render(){
    return (
    <div className="text">
    hello, this is rendered by React!
    </div>
    )
    }
    }
  • 其中参数有很多,但都可以省略,唯有render不可以省略,因为这是用来表述这个插件被加载后,显示的是什么样子,它的返回结果,就是加载在页面上最终的样式。

    1
    2
    3
    4
    ReactDOM.render(
    <Text/>,
    document.getElementById('root')
    );

这段代码是用来渲染react组件的,第一个参数是组件,第二个参数是要渲染的位置。

  • 使用<Text/>的 方式就可以实例化组件,或者写成<Text></Text>,要注意下,react中标签的闭合非常严格,任何标签的关闭与打开必须一一对应,否则会报错。
  • 到目前为止,就完成了一次渲染,将Text组件render函数返回的内容,填充到了id=root的div中。

DEMO 2 - 带有参数的react

往往在使用中,文本的内容并不是写死的,而是需要被我们指定,这样组件才能更通用。下面介绍下,如何向react中传递参数。

###代码:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<style type="text/css">
#name{
background: #5bc0de;
color: white;
font-size: 24px;
}
</style>
</head>
<body>
<button id="name">DEMO1</button>
<div id="root"></div>
<script type="text/babel">
class Text extends React.Component {
render(){
return (
<div className="text">
hello, this is rendered by {this.props.name}! age={this.props.age}
</div>
)
}
}

ReactDOM.render(
<Text name="React" age={11}/>,
document.getElementById('root')
);
</script>
</body>
</html>

在浏览器中显示的效果如下

这里写图片描述

讲解

  • 首先,这个大体上跟第一个demo类似,唯有实例化Text时,多了参数。
  • 当我们传递参数时,写了两种方式,一种是 name="react"另一种是age={11},这两种写法是有区别的,并不仅仅因为一个是str,一个是int。如果是str这种类型,写成 name="xxx"或者name={"xxx"}都是可以的,加了{}的意思就是js中的变量,更加精确了。而后者age={181}是不可以去掉{}的,这样会引起异常,所以这里要注意下,并且建议任何类型都加上{}来确保统一。
  • 当在Text初始化时添加了参数,在组件内部,都收集在this.props中,使用时只要{this.props.name}既可以获取name对应的值,如果取得key并不存在,这里不会报错,只是取到的值是空的。当然可以在getDefaultProps中定义默认的props值,即使在没有传递参数的情况下,也能取到默认值。
  • props中的参数,在初始化传递后,便不能再修改。

DEMO 3 - state,react的核心

state算是react的核心了,任何页面的变化,刷新都是state的变化引起。在react中,只要调用了setState都会引起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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<style type="text/css">
#name{
background: #5bc0de;
color: white;
font-size: 24px;
}
</style>
</head>
<body>
<button id="name">DEMO1</button>
<div id="root"></div>
<script type="text/babel">
class Text extends React.Component {
constructor(props) {
super(props);
this.state = {
name: this.props.name
};
this.keyUp = this.keyUp.bind(this);
}

keyUp(e) {
this.setState({name: e.target.value}, function() {
console.log(this.state.name);
});
}

render(){
return (
<div className="text">
hello, this is rendered by {this.state.name}!
<input type="text" name="" onKeyUp={this.keyUp} />
</div>
)
}
}

ReactDOM.render(
<Text name="react" age={11}/>,
document.getElementById('root')
);
</script>
</body>
</html>

在浏览器中显示的效果如下

这里写图片描述

讲解:

  • 这次组件中多了一个函数,keyUp,keyUp是我自定义的函数,用来响应键盘事件的。
  • 我们先看constructor函数,这里constructor接收props,并采用super(props)去继承父组件的方法和属性,`this.state = {
      name: this.props.name
    };`是初始化state;另外,`this.keyUp = this.keyUp.bind(this);`是绑定this,否则this是react组件支撑实例,而不是组件实例。
    
  • 再看render函数,文字渲染中加了{this.state.name},这个是react内部的状态,可以理解是存储数据的k-v结构,这里的v支持的对象较多。{this.state.name}就是引用状态中的name属性,与props的区别在于,如果state中不存在这个属性,是会报错的,所以我们要在constructor中初始化这个状态的初始值。
  • render中还多了一个,onKeyUp是注册键盘键弹起的事件,当按键按下后弹起,就会触发onKeyUp事件,然后通过绑定的this.keyUp,将事件传递给了自己定义的keyUp函数中。
  • keyUp函数中,使用了this.setState({name: e.target.value}),setState是react中内部的函数,专门用来更新状态的,这里是讲状态中name的值变更为引起事件的value值。
  • 在react中,每次状态的变化,都会引起render函数的重新渲染,这是它自己的机制,我们无需人为处理,当键盘输入内容时,会触发状态变化,导致render重新渲染,渲染的过程会从state中取出变量,所以我们就看到了页面的内容发生了变化。
  • 我们在setState下面加了一个console,通过控制台可以发现,每次打印的值并不是当前输入的值,而是上一次输入的值,这是怎么回事呢?在setState中,这是一个异步处理的函数,并不是同步的,console在setState后立刻执行了,所以这时候状态还没有真正变更完,所以这里取到的状态仍旧是更新前的。这里要特殊注意下。如果需要在更新状态后,再执行操作怎么办呢,setState还有第二个参数,接受一个callback,我们尝试将keyUp中代码改成这样
1
2
3
this.setState({name: e.target.value}, function(){
console.log(this.state.name);
})
  • 这时候log打印出来的只就是我们期望的内容,当每次状态更新成功后,都会调用传进去的callback函数。
  • react中渲染dom有自己的优化方式,首先它在内存中构建一套虚拟的dom,每次更新前将虚拟dom与浏览器中dom对比,只讲有变化的部分进行更新,这样大大的提高了性能。或者我们可以重写函数来控制是否刷新,当然这种方式我们并不提倡。

DEMO 4 - 网络请求触发状态变化

上一节讲到状态变化触发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
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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<style type="text/css">
#name{
background: #5bc0de;
color: white;
font-size: 24px;
}
</style>
</head>
<body>
<button id="name">DEMO1</button>
<div id="root"></div>
<script type="text/babel">
class Text extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
lastGistUrl: '',
dateTime: ''
}
this.request = this.request.bind(this);
}

request() {
$.ajax({
type: 'get',
url: this.props.url,
success: function(result){
var lastGist = result[0];
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url,
dateTime: new Date().getTime()
});
}.bind(this)
});
}
componentDidMount() {
setInterval(this.request, 1000)
}

render(){
return (
<div className="text">
{this.state.username}用户最新的Gist地址为:
<a href={this.state.lastGistUrl}>{this.state.lastGistUrl}</a>
<div>{this.state.dateTime}</div>
</div>
)
}
}

ReactDOM.render(
<Text url="https://api.github.com/users/octocat/gists"/>,
document.getElementById('root')
);
</script>
</body>
</html>

在浏览器中显示的效果如下

这里写图片描述

###讲解:

  • 这个例子中,页面每秒会请求一次网络,将请求到的数据中时间戳更新到状态中。
  • 仍旧是先看代码,相比于上一个例子,这里多了两个函数request和componentDidMount,其中request是请求网络的函数,componentDidMount是react内部的函数,也是react生命周期的一部分,它会在render第一次渲染前执行,而且只会执行一次。
  • 先看request,一个普通的ajax请求,在success回调中,服务器返回result,然后将里面的值赋给state中的username、lastGistUrl属性。这时状态发生了变化,render函数会重新渲染。
  • 为什么success回调函数最后会加一个bind(this)?因为这个函数已经不是react内部的函数了,它是一个外部函数,它里面的this并不是react组件中的this,所以要将外部函数绑定到react中,并能使用react内部的方法,例如setState,就要在函数最后bind(this),这样就完成了绑定。
  • 再看下componentDidMount函数,这个函数在render渲染前会执行,里面的代码也很简单,增加了一个定时器,1秒钟执行一次request。
  • 这里应该在加一个回调,就是定时器在初始化时创建,却没有对应的销毁,所以在组件销毁的时候,应该在这个生命周期中销毁定时器。

DEMO 5 - 组件的嵌套使用

在封装react时,我们往往按照最小单位封装,例如封装一个通用的div,一个通用的span,或者一个通用的table等,所以各自组件对应的方法都会随着组件封装起来,例如div有自己的方法可以更改背景色,span可以有自己的方法更改字体大小,或者table有自己的方法来更新table的内容等~ 这里我们用一个div相互嵌套的例子来查看父子组件如何相互嵌套及调用各自的方法。在下面的例子中,父组件与子组件都有一个方法,来改变自身的背景色,我们实现父子组件相互调用对方的方法,来改变对方的背景色。

代码:

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
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<style type="text/css">
.child{
height: 100px;
width: 200px;
display: inline-block;
}
li{
list-style: none;
display: inline-block;
}
</style>
</head>

<body>
<button id="name">DEMO1</button>
<div id="root"></div>
<script type="text/babel">
class Parent extends React.Component{
constructor(props) {
super(props);
this.state={
background: ''
};
this.child1ChangeColor=this.child1ChangeColor.bind(this);
this.child2ChangeColor=this.child2ChangeColor.bind(this);
this.changeColor=this.changeColor.bind(this);
}
child1ChangeColor(e) {
this.refs['child1'].changeColor(e)
}
child2ChangeColor(e) {
this.refs['child2'].changeColor(e)
}
changeColor(e) {
this.setState({background: $(e.target).css("background-color")});
}
render() {
return (
<div className="parent" style={{background: this.state.background}}>
<br/>
<ul className="list-inline">
<li>对应第一个child</li>
<li><button style={{background:"#286090"}} onClick={this.child1ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#31b0d5"}} onClick={this.child1ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#c9302c"}} onClick={this.child1ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#ec971f"}} onClick={this.child1ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#e6e6e6"}} onClick={this.child1ChangeColor}>&nbsp;</button></li>
</ul>
<ul className="list-inline">
<li>对应第二个child</li>
<li><button href="#" style={{background:"#286090"}} onClick={this.child2ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#31b0d5"}} onClick={this.child2ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#c9302c"}} onClick={this.child2ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#ec971f"}} onClick={this.child2ChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#e6e6e6"}} onClick={this.child2ChangeColor}>&nbsp;</button></li>
</ul>
<hr/>
<Child ref="child1" parentChangeColor={this.changeColor}></Child>
<Child ref="child2" parentChangeColor={this.changeColor}></Child>
</div>
)
}
}

class Child extends React.Component{
constructor(props) {
super(props);
this.state={
background: ''
};
this.changeColor=this.changeColor.bind(this);
}
changeColor(e) {
this.setState({background: $(e.target).css("background-color")});
}
render() {
return (
<div className="child" style={{background: this.state.background}}>
<ul className="list-inline">
<li><button href="#" style={{background:"#286090"}} onClick={this.props.parentChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#31b0d5"}} onClick={this.props.parentChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#c9302c"}} onClick={this.props.parentChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#ec971f"}} onClick={this.props.parentChangeColor}>&nbsp;</button></li>
<li><button href="#" style={{background:"#e6e6e6"}} onClick={this.props.parentChangeColor}>&nbsp;</button></li>
</ul>
</div>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
</script>
</body>

</html>

在浏览器中显示的效果如下

这里写图片描述

讲解:

  • 首先说下,刚打开页面并不是这样的,背景都是白色的。这里的截图是点击各个按钮后变色的样子。
  • 在这个例子中,蓝色的div是一个父组件,它里面包含了两个子组件,分别是红色和橙色,这两个子组件实际上是一模一样的。我们先看下父组件如何调用子组件。
  • 代码中,子组件里面定义了changeColor函数,用来接收onClick事件,并将点击的按钮的data-color属性值作为色值,更改到state中的color属性中,然后触发render来更新背景色。在父组件调用子组件时,我们写了,里面的ref=”child1”就是react中提供的一个属性标签,它与普通的props不同,这里写上ref=”xxx”后,在父组件中,使用this.refs[“child1”]就可以引用对应的子组件,当然这里的ref的值是可以随意定义,只要不重复就好。这样就可以实现组组件引用子组件,然后直接调用里面的方法就好,例如child1ChangeColor中就有this.refs["child1"].changeColor(e);的使用。连起来说下逻辑,在点击父组件中第一列中的按钮后,触发onClick事件,然后onClick事件后,传递到child1ChangeColor后,将事件传递进入,然后再次传递给子组件的changeColor中,因为子组件的changeColor是更改子组件自身的state,所以这时候子组件再次渲染,于是改变了颜色。这就是父组件调用子组件的逻辑。
  • 再说下子组件何如调用父组件的方法,父组件自身也有一个changeColor函数,用来改变自身的背景色。当父组件调用子组件时,,通过props,也就是第二个例子中讲的那样,通过参数的方式传递给子组件,这样子组件中就可以使用this.props.parentChangeColor,来把子组件的onClick事件传递给父组件的changeColor方法中,来改变父组件的背景色。这就是子组件调用父组件函数的方法。
  • 还有一种情况,就是一个父组件下有多个子组件,但是子组件中并没有直接的关系,这时候如果一个子组件调用另一个子组件的方法,就得通过他们共同的父组件来作为中转,在父组件中增加函数来作为中转的函数,来实现子组件间的调用。

github地址:react5个demo

参考:http://blog.csdn.net/iambinger/article/details/51803606

react之生命周期

Posted on 2017-07-23

1. mounting

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

constructor(props)

在mount之前,会调用它;

初始化state的位置;

也可以使用props来初始化state,但要注意当props更新时,state不会更新。那么除了同步props到state,还可以使用state提升。

componentWillMount()

当要mount的时候立即调用,它的调用发生在render()之前,所以这里设置state不会触发重新render。

这是唯一一个当服务器渲染时被调用的生命周期钩子,通常我们建议使用constructor()。

render()

被调用时,检查this.props,this.state,然后返回单个React元素,这个返回的React元素可以表示DOM节点,比如

,也可以表示自定义的组件, 比如。

也可以返回null或者false,表明我们不想渲染,此时ReactDOM.findDOMNode(this)将返回null。

render()函数应该是纯函数,意味着不能修改组件的状态,它返回的是被调用时的结果,并且不能直接和浏览器交互。 如果需要和浏览器交互,需要在componentDidMount()或者其他生命周期中执行。如果shouldComponentUpdate()返回false,那么render将不执行。

componentDidMount()

当组件挂载之后被调用,需要得到DOM节点的操作应该在这里执行。

如果想从远程加载数据,也应在这里执行网络请求。

这里设置state,会触发重新渲染。

2. updating

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate()

componentWillReceiveProps(nextProps)

已经挂载的组件接收新的props之前会调用。

如果需要根据props的变化来更新状态,应该比较this.props和nextProps,并且在这里使用this.setState()执行状态转换。

即使props没有更新,这个方法也可能被调用,所以如果我们想处理变化,一定要比较当前props和下一个props。(当父组件造成子组件重新渲染时会遇到这种情况。)

在mounting期间,react不会用初始的props来调用这个方法。只有在一些组件的props可能更新的时候,才会调用这个方法。

执行this.setState不会触发这个事件。

shouldComponentUpdate(nextProps, nextState)

这个方法能够让react知道组件的输出是否因state或props的改变而受影响。默认就是当state一变化,就会重新渲染,大部分情况下我们使用默认的。

在rendering之前,当收到新的props或者state时,就会调用这个方法。当初始化渲染时,或者当使用forceUpdate()时,该方法不被调用。

返回false不能阻止子组件随他们的state重新渲染。如果返回false,那么componentWillUpdate(), render(), componentDidUpdate()不会被调用。

如果你发现一个组件在表示后很慢(????),可以考虑变成继承式的,从React.PureComponent继承,这意味着该方法此时是shallow prop and state comparision。

如果你想自己手写,那就可以比较this.props和nextprops,以及this.state和nextState,并返回false告诉react这个更新可以跳过。

componentWillUpdate(nextProps, nextState)

在收到新的props或者state后,rendering之前,调用该方法。

在这里可以做一些准备工作,来为更新做准备。

这个方法在初始渲染的时候不会被调用。

这里不能设置this.setState(),如果想根据props来更新state,应该在componentWillReceiveProps(nextProps)执行。

componentDidUpdate

当更新时被调用。

初始渲染的时候不被调用。

当组件更新后,应该在这里操作DOM。

网络请求适合放在这里,只要你比较了当前props和前一个props(当props没变化的时候,进行网络请求可能没有必要)。

3. unmounting

  • componentWillUnmount()

componentWillUnmount()

当卸载一个组件时或者销毁一个组件时,会执行。

可以在这里做一些清除工作,比如清除定时器,取消网络请求,清除在ComponentDidMount中创建的DOM元素。

4. 其他API

  • setState()
  • forceUpdate()

setState(updater, [callback])

认为setState是一个请求,而不是一个立即执行命令,会更好。因为为了更好的性能,react会延时它,并且在一个pass里同时更新好几个组件。所以react不能保证state的变化能立即生效。

setState()通常不立即更新组件。所以在componentDidUpdate或者setState的回调函数中去读取this.state,才能保证读取到的值最新的。

第一个参数是一个updater函数,

(prevState, props) => stateChange

prevState是前一个状态,它不应该直接被改变,相反,应该基于prevState和props建造一个新对象,从而展示变化。

比如,假设我们想增加一个值,

this.setState((prevState, props) => {
      return {counter: prevState.counter + props.step};
});

由updater函数接收的prevState和props是最新的。输出的updater将和prevState进行shallowly merged。

第二个参数是一个可选的回调函数,等setState完成并组建被重新渲染后才会执行。但是我们一般建议使用componentDidUpdate()来代替这个逻辑。

forceUpdate(callback)

component.forceUpdate(callback)

一般只有当state或者props变化时,组件才会重新渲染。但是如果我们想根据其他数据来渲染组件,就可以调用forceUpdate(),从而跳过shouldComponentUpdate()。这会触发子组件正常的生命周期,包括子组件的shouldComponentUpdate()方法。

一般要避免使用。

5. Class properties

  • defaultProps
  • displayName

defaultProps

设置默认属性,即当一个props是undefined时,才有用。如果是null,则不会采取默认值,还是null。

举个例子:

class CustomButton extends React.Component {
      // ...
}

CustomButton.defaultProps = {
      color: 'blue'
};

如果props.color没有提供,则默认就是blue:

render() {
    return <CustomButton /> ; // props.color will be set to blue
  }

如果props.color设置为null,则还是null:

render() {
    return <CustomButton color={null} /> ; // props.color will remain null
  }

displayName

用于debugging信息。JSX会自动设置这个值。

6. instance properties

  • props
  • state

props

this.props包含这个组件的调用者定义的props。

this.props.children是一个特殊的prop,通常是由子标签在JSX表达式中定义的,而不是在它自己的标签中。

state

不要直接修改this.state。

把它当做不可改变的。

状态提升

Posted on 2017-07-23

通常,几个组件拥有相同的数据,此时就需要将这个数据放到最近的公共祖先的状态里。

接下来,我们将创建一个温度计算器,并根据给定水温判断是否沸腾。最终效果如图所示:
result

也就是两个输入框分别对应摄氏温度和华氏温度,只要有一个输入框内容变化,另一个也会相应变化,并判断是否会沸腾。

我们先考虑只有摄氏温度输入框的情况,

class Calculator extends React.Component {
    constructor(props) {
      super(props);
      this.state={
        temperature: ''
      };
      this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e) {
      this.setState({temperature: e.target.value})
    }

    render() {
      return (
        <fieldset>
          <legend>Enter temperature in celsius:</legend>
          <input onChange={this.handleChange} />
          <BoilingVerdict celsius={parseFloat(this.state.temperature)}>
          </BoilingVerdict>
        </fieldset>
      )
    }
  }      
  function BoilingVerdict(props) {
    if (props.celsius >= 100) {
      return <p>The water would boil.</p>;
    }
    return <p>The water would not boil.</p>;
  }

  ReactDOM.render(
    <Calculator />,
    document.getElementById('root')
  );

接着我们再考虑有两个输入框的情况,这时候应该把输入框单独作为一个组件提取出来:

class TemperatureInput extends React.Component {
    constructor(props) {
      super(props);
      this.state={
        temperature: ''
      };
      this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e) {
      this.setState({temperature: e.target.value})
    }

    render() {
      return (
        <fieldset>
          <legend>Enter temperature in {this.props.scale}:</legend>
          <input onChange={this.handleChange} />
          <BoilingVerdict celsius={parseFloat(this.state.temperature)}>
          </BoilingVerdict>
        </fieldset>
      )
    }
  }      

  const scaleName = {
    'c': 'celsius',
    'f': 'Fahrenheit'
  }

  class Calculator extends React.Component {
    render() {
      return (
        <div>
          <TemperatureInput scale={scaleName.c}/>
          <TemperatureInput scale={scaleName.f}/>
        </div>
      )
    }
  }      
  function BoilingVerdict(props) {
    if (props.celsius >= 100) {
      return <p>The water would boil.</p>;
    }
    return <p>The water would not boil.</p>;
  }

  ReactDOM.render(
    <Calculator />,
    document.getElementById('root')
  );

好,现在得到了两个输入框,可是这两个输入框是独立的,一个框的内容不会同步到另一个,由于以前使用vue的时候,我会想到非父子组件通信,也就是通过两个子组件的共同父组件来传递值,react这里也一样,遵循的原则是数据流top-down-flow,所以也不应该考虑一个组件传值给另一个组件,也应该采用公共组件的方式,那么这里我们就应该把我们想要的值放到公共组件Calculator里.

整个工作流程应该是这样的:两个组件都接受父组件的数据,组件一数据变化了,就去触发父组件的事件,这个被触发的事件就去更新父组件的状态,这一更新状态,react就要去重新render了,因为这时组件二的数据也随着变了,所以也就同步显示了。

class TemperatureInput extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e) {
      this.props.onTemperatureChange(e.target.value)
    }

    render() {
      return (
        <fieldset>
          <legend>Enter temperature in {this.props.scale}:</legend>
          <input value={this.props.temperature} onChange={this.handleChange}/>
          <BoilingVerdict celsius={this.props.temperature}>
          </BoilingVerdict>
        </fieldset>
      )
    }
  }      

  const scaleName = {
    'c': 'celsius',
    'f': 'Fahrenheit'
  }

  class Calculator extends React.Component {
    constructor(props) {
      super(props);
      this.state={
        temperature: '',
        scale: 'c'
      };
      this.handleCelsiusChange=this.handleCelsiusChange.bind(this);
      this.handleFahrenheitChange=this.handleFahrenheitChange.bind(this);
    }

    handleCelsiusChange(temperature) {
      this.setState({scale:'c', temperature});
    }
    handleFahrenheitChange(temperature) {
      this.setState({scale:'f', temperature});
    }

    render() {
      const temperature = this.state.temperature;
      const scale = this.state.scale;
      const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
      const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

      return (
        <div>
          <TemperatureInput scale={scaleName.c} temperature={celsius} onTemperatureChange={this.handleCelsiusChange}/>
          <TemperatureInput scale={scaleName.f} temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange}/>
        </div>
      )
    }
  }      
  function BoilingVerdict(props) {
    if (props.celsius >= 100) {
      return <p>The water would boil.</p>;
    }
    return <p>The water would not boil.</p>;
  }

  function toCelsius(fahrenheit) {
    return (fahrenheit - 32) * 5 / 9;
  }

  function toFahrenheit(celsius) {
    return (celsius * 9 / 5) + 32;
  }

  function tryConvert(temperature, convert) {
    const input = parseFloat(temperature);
    if (Number.isNaN(input)) {
      return '';
    }
    const output = convert(input);
    const rounded = Math.round(output * 1000) / 1000;
    return rounded.toString();
  }


  ReactDOM.render(
    <Calculator />,
    document.getElementById('root')
  );

React组件为什么要用super(props)

Posted on 2017-07-23

要在es5中实现继承,首先定义一个父类:

//父类
function sup(name) {
    this.name = name;
}

// 定义父类原型上的方法
sup.prototype.printName = function () {
    console.log(this.name);
}

function sub(name, age) {
    sup.call(this, name) // 调用call方法,继承sup超类属性
    this.age = age;
}

sub.prototype = new sup() // 把子类sub的原型对象指向父类的实例化对象,这样可以继承父类sup原型对象上的属性和方法
sub.prototype.constructor = sub; // 这时会有个问题,子类的constructor属性会指向sup,手动把constructor属性指向子类sub
// 这时候就能在父类的基础上添加属性和方法了
sub.prototype.printAge = function () {
    console.log(this.age)
}

这时候调用父类生成一个实例化对象:

let jack = new sub('jack', 20)
jack.printName() // jack
jack.printAge() // 20

而在es6中实现继承:

class sup {
    constructor(name) {
        this.name = name
    }

    printName() {
        console.log(this.name)
    }
}


class sub extends sup {
    constructor(name, age) {
        super(name)
        this.age = age;
    }

    printAge () {
        console.log(this.age)
    }
}

let jack = new sub('jack', 20)
jack.printName() // jack
jack.printAge() // 20

对比发现,在es5中实现继承:

1、首先得先调用函数的call方法把父类的属性给继承过来

2、通过new关键字继承父类原型的对象上的方法和属性

3、最后再通过手动指定constructor属性指向子类对象

而在es6中实现继承,直接调用super(name),就可以直接继承父类的属性和方法,所以super作用就相当于上述的实现继承的步骤,不过es6提供了super语法糖,简单化了继承的实现

react元素、react组件、react组件支撑实例

Posted on 2017-07-23

React元素是什么

react元素可以理解为我们说的虚拟DOM。(虚拟DOM:是DOM在特定时间段应该是怎样的结构的描述)

react元素仅仅是由一些属性构成的js简单对象,用来描述组件的HTML的结构应该是什么样的,这个对象上不包含任何方法(methods),仅仅只有数据。

如果我们平时使用react+JSX,可能不会涉及到这个概念。但实际上呢,JSX语法实际被编译成了React.createElement(), React.createElement()产生的就是React元素,让我们看一个转换过程的例子:

// 使用JSX语法
var helloworld = <div>hello world!</div>

// 然后是JSX被编译成JS的结果
var helloworld = React.createElement(
    "div",
    null,
    "hello world"
);

// 再是处理成JS简单对象后看起来类似这样
var helloWorld = {
    key: null,
    props: {
        children: "hello world!" // more stuff in here
    },
    ref: null,
    type: 'div'
    // more stuff in here
}

React组件

是React组件类的实例。

一般在JS中我们要得到一个类的实例,通常用new操作符,而React不需要new,而是用ReactDOM.render()把一个React元素渲染成一个特定的DOM元素,并返回一个React组件实例。

ReactDOM.render()干了什么呢?

  • 接收一个虚拟DOM元素,将其渲染成一个真实DOM元素,返回(React元素type指定的)组件实例。
  • 对虚拟DOM执行高效的diff算法。

组件支撑实例是什么

...
// 组件实例
var componentInstance = ReactDOM.render(<CustomForm />, document.getElementById('root'));
// DOM实例
var domInstance = ReactDOM.findDOMNode(componentInstance);

组件实例并不是真实的DOM节点,我们可以使用ReactDOM.findDOMNode(),并将组件实例作为其参数。

组件支撑实例就是引用的真实DOM节点。

总结

JSX语法被转译为React.createElement()调用,最终返回我们称之为“React元素”的JS简单对象,你可以将React元素视为基础构建单元。

React组件实例表示下一个抽象层,ReactDOM.render()接收一个React元素,引用一个真实DOM节点,返回一个React组件实例。该实例可以访问组件类中定义的方法。

React元素和React组件实例都不是真实DOM元素。

渲染组件实例产生的DOM元素称之为组件支撑实例,访问它的主要方式是使用ReactDOM.findeDOMNode().

转载:

https://segmentfault.com/a/1190000009169542

React class与createClass

Posted on 2017-07-23

react创建类之class与createClass区别

以下使用class和createClass两种方式分别写一个组件:

var InputControlES5 = React.createClass({ 
      propTypes: { 
        initialValue: React.PropTypes.string 
      }, 
      defaultProps: { 
        initialValue: '' 
      }, 
      // 设置 initial state 
      getInitialState: function() { 
        return { 
          text: this.props.initialValue || 'placeholder' 
        }; 
      }, 
      handleChange: function(event) { 
        this.setState({ 
          text: event.target.value 
        }); 
      }, 
      render: function() { 
        return (
          <div> Type something:
            <input onChange={this.handleChange} value={this.state.text} /> 
          </div> 
        ); 
      } 
    }); 


    class InputControlES6 extends React.Component { 
      constructor(props) { 
        super(props); 
        // 设置 initial state 
        this.state = { 
          text: props.initialValue || 'placeholder' 
        }; 
        // ES6 类中函数必须手动绑定 this.
        handleChange = this.handleChange.bind(this); 
      } 

      handleChange(event) { 
        this.setState({ 
          text: event.target.value 
        }); 
      } 

      render() { 
        return (
          <div> Type something:
            <input onChange={this.handleChange} value={this.state.text} /> 
          </div> 
        ); 
      } 
    } 

    InputControlES6.propTypes = { 
      initialValue: React.PropTypes.string 
    }; 
    InputControlES6.defaultProps = { 
      initialValue: '' 
    };

区别如下:

函数绑定

createClass在成员函数创建时就已经由react自动绑定好了,需要调用的时候直接使用this.whateverFn即可,函数中的this变量在函数调用时会被正确的设置。

class中,函数不是自动绑定的,必须手动绑定。有三种方法:

  • 在构造函数中绑定
  • 成员函数在定义时使用胖箭头函数
  • 行内绑定:在调用时使用.bind(不推荐)

那么es6 class中如果react没有帮我们绑定this,那么此时的this是哪里来的呢?

有博文说到this本来是指向的生成的Dom节点的div的支撑实例(理解React中es6方法创建组件的this),

构造函数是否调用super方法

class需要接受props作为参数,并调用super(props)。

createClass并不需要这步。

初始化state

createClass初始化state,接受一个getInitialState函数作为参数一部分,这个函数会在组件挂载时被调用一次。

class使用构造函数,在调用super之后,直接设置state即可。

propTypes和defaultProps的位置

createClass中将propTypes和defaultProps作为你传入的对象的属性。

class中,这些变成了类本身的属性,所以他们需要在类定义完成之后被加到类头上。

如果开启了ES7 的属性初始化器(property initializer),可以使用下面的简写法:

class Person extends React.Component { 
      static propTypes = { 
        name: React.PropTypes.string, 
        age: React.PropTypes.string 
      }; 
      static defaultProps = { 
        name: '', 
        age: -1 
      }; 
      ... 
    }

所以该用哪一个呢?

Facebook 已经声明 React.createClass 最终会被 ES6 class 取代,不过他们也说「我们不会废弃 React.createClass,直到我们找到目前 mixin 用例的替代方案,并且在语言中支持类属性初始化器」。

只要有可能,尽量使用无状态函数组件。他们很简单,也会迫使你保持 UI 组件简单。

对于需要 state、生命周期方法、或(通过 refs)访问底层 DOM 节点的复杂组件,使用 class。

不过了解全部三种风格总是有好处的。当你在 StackOverflow 或别的地方查问题的时候,你可能会看到 ES5 和 ES6 两种风格的答案。ES6 风格正在积聚人气,但并不是唯一的风格。

转载自:

http://www.w3cplus.com/react/react-es5-createclass-vs-es6-classes.html

react笔记

Posted on 2017-07-22

1、初识react

angular缺点:作为MVVM框架过重,不适用与移动端开发;UI封装比较复杂,难以重用。

React:

1、不是一个完整的MVC,MVVM框架,重点在于view层

2、react与web components不冲突

3、react比angular“轻”

4、机制:virtual DOM,单向数据绑定

5、组件化的开发思路:高度可重用

应用场景

1、复杂场景下的高性能

2、重用组件库,组件组合

3、“懒”

2、JSX语法(JS XML)

语法糖:对语言功能没有影响,更方便程序员使用,可读性更好

const element = <h1>hello, world.</h1>;

这就是JSX,既不是一个字符串,也不是HTML。

  • JSX更像JS,而不是XML。

  • JSX产生react元素(参见最后一条)

  • JSX可以嵌入表达式

1
2
3
4
5
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
)
  • JSX也是一种表达式
1
2
3
4
5
6
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
  • JSX里可以指定属性
1
const element = <div tabIndex="0"></div>
1
const element = <img src={user.avatarUrl}></img>

注意: 在属性中使用表达式时,花括号两边不能加引号

  • JSX标签元素也可以包含子元素
  • JSX可以阻止XSS攻击
1
2
3
const title = response.potentialMaliciousInput;
// this is safe
const element = <h1>{title}</h1>

默认情况下,react DOM在渲染元素之前,可以escape任何嵌入在JSX中的值,这样你就无法注入任何没有在程序里明确写出来的的东西。在被渲染前,所有的东西都被转换成字符串,这就阻止了XSS攻击。

  • JSX代表Objects

babel会将JSX编译成React.createElement()调用。下面两种写法是等价的:

1
2
3
4
5
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
1
2
3
4
5
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);

React.createElement()会帮助我们做一些检查,得到bug-free的代码,它本质上创建的对象如下:

// 注意,这只是一个简化的结构
const element = {
      type: 'h1',
      props: {
        className: 'greeting',
        children: 'Hello, world'
      }
};

这些对象叫做”React elements”。其实就是屏幕上显示的内容的一种语言描述。

React读取这些对象,并用他们去构建DOM树,并且保持实时更新。

3、如何更新渲染的元素

React element是不可改变的,一旦创建好一个element,就无法改变它的children和attribute。一个element就像电影里的一帧:代表某个时间点的UI。

  • 更新UI的唯一方式就是创建一个新的element,并且传入ReactDOM.render().
    钟表例子:

    function tick() {
        const element = (
            <div>
                  <h1>Hello, world!</h1>
                  <h2>It is {new Date().toLocaleTimeString()}.</h2>
            </div>
          );
        ReactDOM.render(
              element,
              document.getElementById('root')
        );
    }
    
    setInterval(tick, 1000);
    

注意:react应用一般只调用ReactDOM一次,以后将介绍这样的代码是如何被封装到stateful components.

3.1 只在必要的时候调用ReactDOM

React DOM将某个元素及其子元素与原来做比较,只在必要的时候更新DOM。
就拿上面的例子来说,打开控制台查看元素,发现其实只有时间那部分在更新,其他都没有更新。

这大概就是React内部机制之一吧~~

4、Components和Props

组件将UI分成独立的、可复用的块,每个组件相当于一个函数,可以接受任意输入(叫做Props),并返回能够描述屏幕内容的React Elements。

4.1 functional and class components

  • 函数式组件:定义组件最简单的方式就是写一个JS函数

    1
    2
    3
    function Welcome(props) {
    return <h1>hello, {props.name}</h1>
    }
  • class组件:也可以使用class定义组件

    1
    2
    3
    4
    5
    class Welcome extends React.component {
    render() {
    return <h1>hello, {this.props.name}</h1>;
    }
    }

使用class定义的组件具有一些额外的特点。

4.2 渲染组件

react elements出来可以表示DOM 标签,还可以表示用户自定义的组件。

const element = <div />;

const element = <Welcome name="sara" />

第二行中,Welcome是一个用户自定义组件,name=”sara”是属性,它会作为props传递给组件。

function Welcome(props) {
      return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
      element,
      document.getElementById('root')
);

以上代码会在页面上渲染出 “Hello, Sara”。

警告: 组件名必须是大写字母开头!!!

4.3 组件组合

一个组件里可以引用其他组件。

function Welcome(props) {
  return <h1>hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="sara" />
      <Welcome name="ming" />
      <Welcome name="li" />
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

4.4 组件抽象

我们先写一个例子

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar" src={props.author.avatarUrl} alt={props.author.name}/>
        <div className="UserInfo-name">
          {props.author.name}
        </div>
    </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formateDate(props.date)}
      </div>
    </div>
  )
}

function formateDate(props) {
  return props.toLocaleDateString();
}

const comment = {
  date: new Date(),
  text: 'I hope you enjoy learning React!',
  author: {
    name: 'Hello Kitty',
    avatarUrl: 'http://placekitten.com/g/64/64'
  }
};
const element = (
  Comment(comment)
);

ReactDOM.render(
  element,
  document.getElementById('root')
);

上述例子没有将一些功能模块抽取成组件,下面我们抽象成组件:

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formateDate(props.date)}
      </div>
    </div>
  )
}

function formateDate(props) {
  return props.toLocaleDateString();
}

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  )
}
function Avatar(props) {
  return (
    <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
  );

}

const comment = {
  date: new Date(),
  text: 'I hope you enjoy learning React!',
  author: {
    name: 'Hello Kitty',
    avatarUrl: 'http://placekitten.com/g/64/64'
  }
};
const element = (
  Comment(comment)
);

ReactDOM.render(
  element,
  document.getElementById('root')
);

4.5 props是只读的

不要尝试修改props!!!

5、状态和生命周期

在第3部分,我们说了一个钟表的例子,现在我们利用上面讲的组件抽象,可以抽象成这个样子:

function tick() {
    ReactDOM.render(
      <Clock date={new Date()} />,
      document.getElementById('root')
    );
  }

  function Clock(props) {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {props.date.toLocaleTimeString()}.</h2>
      </div>
    )
  }

  setInterval(tick, 1000);

但是这样写之后,Clock并不能控制date,所以我们想把它封装成这个样子:

ReactDOM.render(
      <Clock />,
      document.getElementById('root')
);

这样我们就需要引入了state。

state与props类似, 但是state是组件私有的,并且完全由组件自己控制。

第4部分,我们说过,使用class定义的组件具有一些额外的特点,那么局部state就是它的一个特点,只对class可用的特点。

那么如何将一个函数组件转化成class组件呢?

5.1 将函数转化成class

可遵循以下步骤:

  • 新建一个class扩展React.Component,名字与原函数名字相同
  • 添加一个空方法render() {}
  • 把函数的主体放到render方法里
  • 把props替换成this.props
  • 删除原来的空函数

    function tick() {

      ReactDOM.render(
        <Clock date={new Date()} />,
        document.getElementById('root')
      );
    }
    class Clock extends React.Component {
      render() {
        return (
          <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
          </div>
        )
      }
    }
    
    setInterval(tick, 1000);
    

    这样Clock是一个class,而不是函数。

这就能让我们使用额外的特点,比如局部状态和生命周期钩子。

5.2 给class添加局部状态

  • 把this.props.date替换成this.state.date
  • 给class添加一个class constructor,并给this.state一个初始值。
  • 传递props给base constructor (class组件应当都调用base constructor)
  • 从<Clock />元素中去除date prop

结果如下:

function tick() {
    ReactDOM.render(
      <Clock />,
      document.getElementById('root')
    );
  }
  class Clock extends React.Component {
    constructor(props) {
      super(props);
      this.state = {date: new Date()};
    }

    render() {
      return (
        <div>
          <h1>Hello, world!</h1>
          <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
        </div>
      )
    }
  }

  setInterval(tick, 1000);

当然,经过上述修改后的只是一个静态时钟,时间不会变。

接下来我们给Clock设置自己的定时器。

5.3 给class添加生命周期

当组件被销毁时,他们占用的资源应该被释放。

当Clock第一次被渲染到DOM中时,我们想设置一个定时器,这就是’mounting’。

当Clock对应的DOM被移除时,我们想清除这个定时器,这就是’unmounting’。

class Clock extends React.Component {
    constructor(props) {
      super(props);
      this.state = {date: new Date()};
    }

    componentDidMount() {
      this.timerId = setInterval(() => this.tick(), 1000);
    }

    componentWillUnmount() {
      clearInterval(this.timerId);
    }

    tick() {
      this.setState({date: new Date()});
    }
    render() {
      return (
        <div>
          <h1>Hello, world!</h1>
          <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
        </div>
      )
    }
  }

  ReactDOM.render(
      <Clock />,
      document.getElementById('root')
    );

此时,时钟可以正常走了。

下面我们分析一下整个工作过程:

(1)当被传递给ReactDOM.render(),react调用Clock组件的constructor,然后初始化当前的状态,this.state。

(2)然后react调用Clock组件的render()方法,这时react就知道了该显示什么到屏幕上,react就会更新DOM去匹配Clock的render输出。

(3)当Clock输入插入DOM中时,react调用componentDidMount()生命周期钩子,在这里面,Clock组件让浏览器设置一个定时器去调用tick()函数。

(4)每隔一秒,浏览器调用tick()方法,在这里面,Clock组件通过setState去调度UI的更新。由于setState(),react知道state已经变化了,它就会再次调用render()方法去看看应该如何展示在屏幕上。这时候,render()中的this.state.date变了,react也相应的去更新DOM。

(5)如果组件从DOM中永久移除了,react调用componentWillUnmount()生命周期钩子,去清除定时器。

5.4 正确使用state

5.4.1 不要直接修改state

错误示范:

// wrong
this.state.comment = 'hello';

正确示范,使用setState():

// correct
this.setState({comment: 'hello'});

只有在constructor里才能使用this.state

5.4.2 state的更新可能是异步的

为了性能,react可能会将多个setState()调用放到同一批更新里。

因此this.props和this.state可能更新的不同步,不应该依赖他们的值来计算下一个状态。

比如:下面这个代码就很可能无法更新counter。

// Wrong
this.setState({
      counter: this.state.counter + this.props.increment,
});

我们应该使用setState的第二种方式,即接收一个函数而不是一个对象,这个函数接受两个参数,原先的状态作为第一个参数,实施更新时的props作为第二个参数。

// Correct
this.setState((prevState, props) => ({
      counter: prevState.counter + props.increment
}));

5.4.3 state的更新被合并了

当调用setState()时,react将你提供的对象合并到当前state。

比如,你的state可能包含几个独立的变量:

constructor(props) {
    super(props);
    this.state = {
          posts: [],
          comments: []
    };
  }

然后你用不同的setState()更新他们:

componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

由于合并,this.setState({comments})不影响this.state.posts,但是将完整的替换this.state.comments。

5.5 数据向下流动

也就是说组件不会知道他的props里的数据来自哪里。

这就是“自顶向下”或者“单一”数据流。

由指定组件拥有的任何state,以及被这个state驱动的任何data和UI只能影响其下面的元素。

无论这个组件是有状态的还是无状态的,都被视为这个组件的细节,也都是可能随时间变化的,可以在有状态组件里使用无状态组件,反之亦然。

6. 处理事件

处理react元素的事件与处理DOM元素的事件类似,有以下几点区别:

  • react事件使用camelCase命名,而不是全小写
  • 事件处理器是一个函数,而不是一个字符串

举个例子:

// HTML
<button onclick="activateLaser()">
    Activate Lasers
</button>

// React
<button onClick={activateLaser}>
    Activate Lasers
</button>
  • 不能直接返回false阻止默认事件,必须调用preventDefault。

举个例子:

// HTML
<a href="#" onclick="console.log('the link was clicked.'); return false">
    Click me
</a>

// React
function ActionLink() {
    function handleClick(e) {
        console.log('the link was clicked.');
        e.preventDefaul();
    }
    return (
        <a href="#" onClick={handleClick}>
            Click me
        </a>
    );
}
  • react中一般不需要调用addEventListener来对DOM元素添加监听。

举个例子:

class Toggle extends React.Component {
    constructor(props) {
      super(props);
      this.state = {isToggleOn: true};
       // This binding is necessary to make `this` work in the callback
      this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
      this.setState(prevState => ({
        isToggleOn: !prevState.isToggleOn
      }));
    }
    render() {
      return (
        <button onClick={this.handleClick}>
          {this.state.isToggleOn?'ON':'OFF'}
        </button>
      )
    }
  }

  ReactDOM.render(
      <Toggle />,
      document.getElementById('root')
    );

这里要注意this的绑定,如果我们不在constructor里绑定this给handleClick,那么在调用handleClick时this将会是undefined。这里还有两种写法,

第二种,属性初始化语法

// 采用属性初始化语法
// 方式二
    handleClick = ()=>{
      this.setState(prevState => ({
        isToggleOn: !prevState.isToggleOn
      }));
    }

第三种,使用箭头函数

render() {
      return (
        <button onClick={(e) => this.handleClick(e)}>
          {this.state.isToggleOn?'ON':'OFF'}
        </button>
      )
    }

第三种方式不推荐。因为每次button渲染时都会创建一个新回调函数,当我们把这个回调函数传递给子组件时,子组件会重复渲染。

7. 条件渲染

如果我们有两个组件,需要根据条件渲染其中一个,这时候就可以用if来实现:

比如我们有这么两个组件:

function UserGreeting(props) {
      return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
      return <h1>Please sign up.</h1>;
}

为了实现这两个组件只渲染其中一个,我们创建一个Greeting元素,根据是否登录展示其中一个组件:

function UserGreeting() {
    return <h1>Welcom back!</h1>
  }

  function GuestGreeting() {
    return <h1>Please sign up</h1>
  }

  function Greeting(props) {
    const isLoggedIn = props.isLoggedIn;
    if(props.isLoggedIn) {
      return <UserGreeting />;
    }
    return <GuestGreeting />;
  }

  ReactDOM.render(
      <Greeting isLoggedIn={true} />,
      document.getElementById('root')
    );

7.1 元素变量

我们可以使用变量存储元素。

比如这里我们有两个按钮,表示登录退出:

function LoginButton (props) {
    return (
      <button onClick={props.onClick}>
        Login
      </button>
    )
  }

  function LogoutButton (props) {
    return (
      <button onClick={props.onClick}>
        Logout
      </button>
    )
  }

我们将创建一个状态组件LoginControl.

class LoginControl extends React.Component {
    constructor (props) {
      super(props);
      this.state={
        isLoggedIn: false
      };
      this.handleLogoutClick = this.handleLogoutClick.bind(this);
      this.handleLoginClick = this.handleLoginClick.bind(this);
    }

    handleLogoutClick() {
      this.setState({isLoggedIn: false})
    }

    handleLoginClick() {
      this.setState({isLoggedIn: true})
    }

    render() {
      const isLoggedIn = this.state.isLoggedIn;
      let button = null;
      if(isLoggedIn) {
        button = <LogoutButton onClick={this.handleLogoutClick}/>;
      } else {
        button = <LoginButton onClick={this.handleLoginClick}/>;
      }

      return (
        <div>
          <Greeting isLoggedIn={isLoggedIn}/>
          {button}
        </div>
      );
    }
  }

  function UserGreeting(props) {
    return <h1>Welcome back!</h1>;
  }

  function GuestGreeting(props) {
    return <h1>Please sign up.</h1>;
  }

  function Greeting(props) {
    const isLoggedIn = props.isLoggedIn;
    if (isLoggedIn) {
      return <UserGreeting />;
    }
    return <GuestGreeting />;
  }

  function LoginButton (props) {
    return (
      <button onClick={props.onClick}>
        Login
      </button>
    )
  }

  function LogoutButton (props) {
    return (
      <button onClick={props.onClick}>
        Logout
      </button>
    )
  }

  ReactDOM.render(
      <LoginControl />,
      document.getElementById('root')
    );

当我们使用一个变量,并使用if语句,是一个比较好的方式来渲染一个组件。但有时候,你想去使用一个更短的语法,这里有几种方式:

7.2 inline if with logical && operator

通过{},我们可以嵌入任何表达式到JSX,当然也包括JS逻辑运算符&&,这对于条件渲染时很方便的,
true && expression将取决于expression,

false && expression将取决于false。

function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>
  );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
  <Mailbox unreadMessages={messages} />,
  document.getElementById('root')
);

7.3 inline if-else with Conditional Operator

就是三目运算符:condition ? true: false

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );
}

7.4 阻止元素渲染

在极少数情况下,我们希望组件能够隐藏自己,即使它是由另一个组件渲染的,要实现这点,只需要在它的render函数里return null即可。

function WarningBanner(props) {
  if (!props.warn) {
    return null;
  }

  return (
    <div className="warning">
      Warning!
    </div>
  );
}

class Page extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showWarning: true}
    this.handleToggleClick = this.handleToggleClick.bind(this);
  }

  handleToggleClick() {
    this.setState(prevState => ({
      showWarning: !prevState.showWarning
    }));
  }

  render() {
    return (
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.handleToggleClick}>
          {this.state.showWarning ? 'Hide' : 'Show'}
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <Page />,
  document.getElementById('root')
);

8. lists和keys

8.1 渲染多个组件

使用map()函数:

const number = [1,2,3];
const listItems = number.map((number) => {
  return <li>{number}</li>
});

ReactDOM.render(
    <ul>{listItems}</ul>,
    document.getElementById('root')
  );

8.2 基本的list组件

通常我们会在一个组件内部渲染列表:

上面的例子可以改为:

function NumberList(props) {
    const numbers = props.numbers;
    const listItems = numbers.map((number) => 
      <li>{number}</li>
    );
    return (
      <ul>{listItems}</ul>
    );
  }

  const numbers = [1,2,3];
  ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );

当我们运行这段代码时,发现会有警告,应该给list 元素提供一个key 的属性。“key”是一个特殊的字符串属性,当我们创建元素的list时应该包含这个属性(原因在后面),好,下面我们加上这个属性:

function NumberList(props) {
    const numbers = props.numbers;
    const listItems = numbers.map((number) =>
      <li key={number.toString()}>{number}</li>
    );
    return (
      <ul>{listItems}</ul>
    );
  }

  const numbers = [1,2,3];
  ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );

8.3 Keys

key有助于识别哪些项目已经更改、添加或删除。应该给数组中的元素赋予key,使元素具有稳定的标识:

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
      <li key={number.toString()}>
        {number}
      </li>
);

选择key的最佳方法是使用唯一标识其兄弟之间的列表项的字符串。大多数情况下,将使用数据中的id作为key:

const todoItems = todos.map((todo) =>
      <li key={todo.id}>
        {todo.text}
      </li>
);

如果没有稳定的id,那就使用item的index作为最后的解决方案:

const todoItems = todos.map((todo, index) =>
      // Only do this if items have no stable IDs
      <li key={index}>
        {todo.text}
      </li>
);

不建议使用index作为key,如果items能被重排序

8.4 抽象带key的组件

key只有在数组的上下文里才有意义。

比如,如果想抽象一个ListItem组件,应该让这个key在元素上,而不是在

  • 元素上:
  • 错误的写法:

    function ListItem(props) {
      const value = props.value;
      return (
        // Wrong! There is no need to specify the key here:
        <li key={value.toString()}>
          {value}
        </li>
      );
    }
    
    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        // Wrong! The key should have been specified here:
        <ListItem value={number} />
      );
      return (
        <ul>
          {listItems}
        </ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    

    正确的写法:

    function ListItem(props) {
      // Correct! There is no need to specify the key here:
      return <li>{props.value}</li>;
    }
    
    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        // Correct! Key should be specified inside the array.
        <ListItem key={number.toString()}
                  value={number} />
      );
      return (
        <ul>
          {listItems}
        </ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    

    总结:在map()调用中的元素都需要key。

    8.5 keys在兄弟之间必须是唯一的

    key在他们的兄弟之间必须唯一,全局可以不唯一。

    当我们产生两个不同的数组时,可以用相同的key。

    key只是作为react的提示,但它不能传递到你的组件里,比如 下面的例子:

    const content = posts.map((post) =>
          <Post
            key={post.id}
            id={post.id}
            title={post.title} />
    );
    

    上面例子,post组件能读取到props.id,但不能读取到props.key。

    8.6 把map()嵌入在JSX里

    function NumberList(props) {
          const numbers = props.numbers;
          return (
            <ul>
                  {numbers.map((number) =>
                    <ListItem key={number.toString()}
                  value={number} />
                  )}
            </ul>
          );
    }
    

    采取那种方式取决于代码和自己习惯。

    9. 表单

    nodejs速学笔记(一)

    Posted on 2017-07-21

    Nodejs基础

    安装、运行什么的就不说了,网上一大堆教程。这部分主要是总结一下nodejs是什么,CMD模块系统要注意的。

    • nodejs是一个js脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用node命令
    • 终端下直接输入node命令,可以进入命令行交互模式,很适合用来测试一些JS代码片段,比如正则表达式。
    • NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次。
    • 除非JS模块不能满足需要,否则不要轻易使用二进制模块,否则用户会痛苦无比。

    代码的组织和部署

    模块路径解析规则

    路径解析一直没有研究过,总感觉是个心头病。。

    require函数支持/ 或磁盘符C:开头的绝对路径,也支持./开头的相对路径,绝对路径和相对路径当然不够灵活,我想动动文件位置就得更改一堆路径。

    因此,require函数支持第三种形式的路径,写法类似于foo/bar,并一次按照以下规则解析路径,直到找到模块位置。

    1、内置模块
    如果传递给require函数的是Nodejs内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如:

    require('fs')    
    

    2、node_modules目录
    NodeJS定义了一个特殊的node_modules目录用于存放模块,例如某个模块的绝对路径是/home/user/hello.js, 在该模块中使用require('foo/bar')方式加载模块时,则NodeJS一次尝试使用以下路径。

    /home/user/node_modules/foo/bar
    /home/node_modules/foo/bar
    /node_modules/foo/bar
    

    3、NODE_PATH环境变量
    与PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来指定额外的模块搜索路径,NODE_PATH环境变量中包含一到多个目录路径,路径之间Linux下使用:分割,windows下使用;分割。例如,定义了以下NODE_PATH环境变量:

    NODE_PATH=/home/user/lib:/home/lib
    

    当使用require(‘foo/bar’)的方式加载模块时,则NodeJS依次尝试以下路径。

    /home/user/lib/foo/bar
     /home/lib/foo/bar
    

    包(package)

    JS模块的基本单位是js文件,但复杂些的模块往往由多个子模块组成。这些子模块往往有一个入口模块,在使用时只需要加载入口模块即可。如下目录结构,cat是一个包,里面包含head.js.body.js,main.js,其中main.js是入口模块,加载方式就是require('home/user/lib/cat/main.js'),但是如果我们把main.js改成index.js,引入入口模块的时候就只需要写成require(‘home/user/lib/cat’)的形式,程序会自动寻找index.js的。

    - /home/user/lib/
    - cat/
        head.js
        body.js
        main.js
    

    进而,如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个package.json文件,并在其中指定入口模块的路径。上例中的cat模块可以重构如下:

    - /home/user/lib/
    - cat/
        + doc/
        - lib/
            head.js
            body.js
            main.js
        + tests/
        package.json
    

    其中package.json内容如下:

    {
        "name": "cat",
        "main": "./lib/main.js"
    }
    

    如此一来,就同样可以使用require('/home/user/lib/cat')的方式加载模块。NodeJS会根据包目录下的package.json找到入口模块所在位置。

    命令行程序

    使用NodeJS编写的东西,要么是一个包,要么是一个命令行程序,而前者最终也会用于开发后者。因此我们在部署代码时需要一些技巧,让用户觉得自己是在使用一个命令行程序。

    例如我们用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内实现了所有功能。并且写好后,我们把该程序部署在/home/user/bin/node-echo.js这个位置。为了在任何目录下都能运行该程序,我们需要使用以下终端命令。

    $ node /home/user/bin/node-echo.js Hello World
    Hello World
    

    这种使用方式看起来不怎么像是一个命令行程序,下边的才是我们期望的方式。

    $ node-echo Hello World
    

    Linux

    在Linux系统下,我们可以把JS文件当作shell脚本来运行,从而达到上述目的,具体步骤如下:

    1、在shell脚本中,可以通过#!注释来指定当前脚本使用的解析器。所以我们首先在node-echo.js文件顶部增加以下一行注释,表明当前脚本使用NodeJS解析。

    #! /usr/bin/env node
    

    NodeJS会忽略掉位于JS模块首行的#!注释,不必担心这行注释是非法语句。

    2、然后,我们使用以下命令赋予node-echo.js文件执行权限。

    $ chmod +x /home/user/bin/node-echo.js
    

    这样处理后,我们就可以在任何目录下使用node-echo命令了。

    windows

    在Windows系统下的做法完全不同,我们得靠.cmd文件来解决问题。假设node-echo.js存放在C:\Users\user\bin目录,并且该目录已经添加到PATH环境变量里了。接下来需要在该目录下新建一个名为node-echo.cmd的文件,文件内容如下:

    @node "C:\User\user\bin\node-echo.js" %*
    

    这样处理后,我们就可以在任何目录下使用node-echo命令了。

    工程目录

    了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例,一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看起来像下边这样。

    - /home/user/workspace/node-echo/   # 工程目录
        - bin/                          # 存放命令行相关代码
            node-echo
        + doc/                          # 存放文档
        - lib/                          # 存放API相关代码
            echo.js
        - node_modules/                 # 存放三方包
            + argv/
        + tests/                        # 存放测试用例
        package.json                    # 元数据文件
        README.md                       # 说明文件
    

    NPM

    会用就行了,不求甚解哈哈~~

    下载第三方包

    比如上边例子中的argv,就可以在工程目录下打开终端,使用以下命令来下载三方包。

    $ npm install argv
    ...
    argv@0.0.2 node_modules\argv
    

    下载好之后,argv包就放在了工程目录下的node_modules目录中,因此在代码中只需要通过require('argv')的方式就好,无需指定三方包路径。

    以上命令默认下载最新版三方包,如果想要下载指定版本的话,可以在包名后边加上@<version>,例如通过以下命令可下载0.0.1版的argv。

    $ npm install argv@0.0.1
    ...
    argv@0.0.1 node_modules\argv
    

    如果使用到的三方包比较多,在终端下一个包一条命令地安装未免太人肉了。因此NPM对package.json的字段做了扩展,允许在其中申明三方包依赖。因此,上边例子中的package.json可以改写如下:

    {
        "name": "node-echo",
        "main": "./lib/echo.js",
        "dependencies": {
            "argv": "0.0.2"
        }
    }
    

    这样处理后,在工程目录下就可以使用npm install命令批量安装三方包了。更重要的是,当以后node-echo也上传到了NPM服务器,别人下载这个包时,NPM会根据包中申明的三方包依赖自动下载进一步依赖的三方包。例如,使用npm install node-echo命令时,NPM会自动创建以下目录结构。

    - project/
        - node_modules/
            - node-echo/
                   - node_modules/
                    + argv/
                ...
        ...
    

    如此一来,用户只需关心自己直接使用的三方包,不需要自己去解决所有包的依赖关系。

    安装命令行程序

    从NPM服务上下载安装一个命令行程序的方法与三方包类似。例如上例中的node-echo提供了命令行使用方式,只要node-echo自己配置好了相关的package.json字段,对于用户而言,只需要使用以下命令安装程序。

    $ npm install node-echo -g
    

    参数中的-g表示全局安装,因此node-echo会默认安装到以下位置,并且NPM会自动创建好Linux系统下需要的软链文件或Windows系统下需要的.cmd文件。

    - /usr/local/               # Linux系统下
            - lib/node_modules/
                + node-echo/
                ...
            - bin/
                node-echo
                ...
            ...
    
        - %APPDATA%\npm\            # Windows系统下
            - node_modules\
                + node-echo\
                ...
            node-echo.cmd
            ...
    

    发布代码

    第一次使用NPM发布代码前需要注册一个账号。终端下运行npm adduser,之后按照提示做即可。账号搞定后,接着我们需要编辑package.json文件,加入NPM必需的字段。接着上边node-echo的例子,package.json里必要的字段如下。

    {
            "name": "node-echo",           # 包名,在NPM服务器上须要保持唯一
            "version": "1.0.0",            # 当前版本号
            "dependencies": {              # 三方包依赖,需要指定包名和版本号
                "argv": "0.0.2"
              },
            "main": "./lib/echo.js",       # 入口模块位置
            "bin" : {
                "node-echo": "./bin/node-echo"      # 命令行程序名和主模块位置
            }
        }
    

    之后,我们就可以在package.json所在目录下运行npm publish发布代码了。

    版本号

    使用NPM下载和发布代码时都会接触到版本号。NPM使用语义版本号来管理代码,这里简单介绍一下。

    语义版本号分为X.Y.Z三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。

    • 如果只是修复bug,需要更新Z位。

    • 如果是新增了功能,但是向下兼容,需要更新Y位。

    • 如果有大变动,向下不兼容,需要更新X位。
      版本号有了这个保证后,在申明三方包依赖时,除了可依赖于一个固定版本号外,还可依赖于某个范围的版本号。例如”argv”: “0.0.x”表示依赖于0.0.x系列的最新版argv。NPM支持的所有版本号范围指定方式可以查看官方文档。

    灵机一点

    除了本章介绍的部分外,NPM还提供了很多功能,package.json里也有很多其它有用的字段。除了可以在npmjs.org/doc/查看官方文档外,这里再介绍一些NPM常用命令。

    NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。

    使用npm help 可查看某条命令的详细帮助,例如npm help install。

    在package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。

    使用npm update <package>可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。

    使用npm update <package> -g可以把全局安装的对应命令行程序更新至最新版。

    使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。

    使用npm unpublish <package>@<version>可以撤销发布自己发布过的某个版本代码。

    小结

    本章介绍了使用NodeJS编写代码前需要做的准备工作,总结起来有以下几点:

    • 编写代码前先规划好目录结构,才能做到有条不紊。

    • 稍大些的程序可以将代码拆分为多个模块管理,更大些的程序可以使用包来组织模块。

    • 合理使用node_modules和NODE_PATH来解耦包的使用方式和物理路径。

    • 使用NPM加入NodeJS生态圈互通有无。

    • 想到了心仪的包名时请提前在NPM上抢注。

    文件操作

    文件拷贝

    NodeJS提供了基本的文件操作API,但是像文件拷贝这种高级功能就没有提供,因此我们先拿文件拷贝程序练手。与copy命令类似,我们的程序需要能接受源文件路径与目标文件路径两个参数。

    小文件拷贝

    我们使用NodeJS内置的fs模块简单实现这个程序如下。

    var fs = require('fs');
    
    function copy(src, dst) {
        fs.writeFileSync(dst, fs.readFileSync(src));
    }
    
    function main(argv) {
        copy(argv[0], argv[1]);
    }
    
    main(process.argv.slice(2));
    

    以上程序使用fs.readFileSync从源路径读取文件内容,并使用fs.writeFileSync将文件内容写入目标路径。

    process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。
    

    大文件拷贝

    上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完成拷贝。因此上边的程序需要改造如下。

    var fs = require('fs');
    
    function copy(src, dst) {
        fs.createReadStream(src).pipe(fs.createWriteStream(dst));
    }
    
    function main(argv) {
        copy(argv[0], argv[1]);
    }
    
    main(process.argv.slice(2));
    

    以上程序使用fs.createReadStream创建了一个源文件的只读数据流,并使用fs.createWriteStream创建了一个目标文件的只写数据流,并且用pipe方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。

    API走马观花

    我们先大致看看NodeJS提供了哪些和文件操作有关的API。这里并不逐一介绍每个API的使用方法,官方文档已经做得很好了。

    Buffer(数据块)

    S语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。

    • 除了可以读取文件得到Buffer的实例外,还能够直接构造,例如:

      var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

    • 有length属性

    • 可以用下标访问: bin[0]
    • 与字符串能够互相转化

      • 使用指定编码将二进制数据转化为字符串
        var str = bin.toString('utf-8'); // => "hello"
      • 将字符串转换为指定编码下的二进制数据:
        var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>
    • Buffer不是只读的(字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。),更像是可以做指针操作的C语言数组。例如,可以用[index]方式直接修改某个位置的字节。

      • bin[0] = 0x48;
    • 而.slice方法也不是返回一个新的Buffer,而更像是返回了指向原Buffer中间的某个位置的指针

      `[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]

       ^           ^
       |           |
      bin     bin.slice(2)`
      

      因此对.slice方法返回的Buffer的修改会作用于原Buffer,例如:

      var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
      var sub = bin.slice(2);
      
      sub[0] = 0x65;
      console.log(bin); // => <Buffer 68 65 65 6c 6f> ```
      

      也因此,如果想要拷贝一份Buffer,得首先创建一个新的Buffer,并通过.copy方法把原Buffer中的数据复制过去。这个类似于申请一块新的内存,并把已有内存中的数据复制过去。以下是一个例子。

      var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
      var dup = new Buffer(bin.length);
      
      bin.copy(dup);
      dup[0] = 0x48;
      console.log(bin); // => <Buffer 68 65 6c 6c 6f>
      console.log(dup); // => <Buffer 48 65 65 6c 6f>
      

    总之,Buffer将JS的数据处理能力从字符串扩展到了任意二进制数据。

    Stream(数据流)

    当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种Stream来提供对数据流的操作。

    以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下:

    var rs = fs.createReadStream(pathname);
    
        rs.on('data', function (chunk) {
            doSomething(chunk);
        });
    
        rs.on('end', function () {
            cleanUp();
        });
    

    Stream基于事件机制工作,所有Stream的实例都继承于NodeJS提供的EventEmitter。

    上边的代码中data事件会源源不断地被触发,不管doSomething函数是否处理得过来。代码可以继续做如下改造,以解决这个问题。

    var rs = fs.createReadStream(src);
    
        rs.on('data', function (chunk) {
            rs.pause();
            doSomething(chunk, function () {
                rs.resume();
            });
        });
    
        rs.on('end', function () {
            cleanUp();
        });
    

    以上代码给doSomething函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。

    此外,我们也可以为数据目标创建一个只写数据流,示例如下:

    var rs = fs.createReadStream(src);
        var ws = fs.createWriteStream(dst);
    
        rs.on('data', function (chunk) {
            ws.write(chunk);
        });
    
        rs.on('end', function () {
            ws.end();
        });
    

    我们把doSomething换成了往只写数据流里写入数据后,以上代码看起来就像是一个文件拷贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流内部的缓存会爆仓。我们可以根据.write方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据drain事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。因此代码可以改造如下:

    var rs = fs.createReadStream(src);
        var ws = fs.createWriteStream(dst);
    
        rs.on('data', function (chunk) {
            if (ws.write(chunk) === false) {
                rs.pause();
            }
        });
    
        rs.on('end', function () {
            ws.end();
        });
    
        ws.on('drain', function () {
            rs.resume();
        });
    

    以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场景很多,例如上边的大文件拷贝程序,NodeJS直接提供了.pipe方法来做这件事情,其内部实现方式与上边的代码类似。

    File System(文件系统)

    NodeJS通过fs内置模块提供对文件的操作。fs模块提供的API基本上可以分为以下三类:

    • 文件属性读写。

    其中常用的有fs.stat、fs.chmod、fs.chown等等。

    • 文件内容读写。

    其中常用的有fs.readFile、fs.readdir、fs.writeFile、fs.mkdir等等。

    • 底层文件操作。

    其中常用的有fs.open、fs.read、fs.write、fs.close等等。

    NodeJS最精华的异步IO模型在fs模块里有着充分的体现,例如上边提到的这些API都通过回调函数传递结果。以fs.readFile为例:

    fs.readFile(pathname, function (err, data) {
        if (err) {
            // Deal with error.
        } else {
            // Deal with data.
        }
    });
    

    如上边代码所示,基本上所有fs模块API的回调参数都有两个。第一个参数在有错误发生时等于异常对象,第二个参数始终用于返回API方法执行结果。

    此外,fs模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作更方便时的情况。同步API除了方法名的末尾多了一个Sync之外,异常对象与执行结果的传递方式也有相应变化。同样以fs.readFileSync为例:

    try {
        var data = fs.readFileSync(pathname);
        // Deal with data.
    } catch (err) {
        // Deal with error.
    }
    

    Path(路径)

    操作文件时难免不与文件路径打交道。NodeJS提供了path内置模块来简化路径相关操作,并提升代码可读性。以下分别介绍几个常用的API。

    • path.normalize

      将传入的路径转换为标准路径,具体讲的话,除了解析路径中的.与..外,还能去掉多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。以下是一个例子:

      var cache = {};
      
      function store(key, value) {
          cache[path.normalize(key)] = value;
      }
      
      store('foo/bar', 1);
      store('foo//baz//../bar', 2);
      console.log(cache);  // => { "foo/bar": 2 }
      

    **坑出没注意**: 标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\\/g, '/')再替换一下标准路径。

    • path.join

      将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。以下是一个例子:

      path.join('foo/', 'baz/', '../bar'); // => "foo/bar"
      
    • path.extname

      当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。以下是一个例子:

      path.extname('foo/bar.js'); // => ".js"
      

    遍历目录

    遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS文件时,就需要遍历整个目录。

    • 递归算法
      遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通过不断缩小问题的规模来解决问题。以下示例说明了这种方法。

      function factorial(n) {
          if (n === 1) {
              return 1;
          } else {
              return n * factorial(n - 1);
          }
      }
      

    **陷阱**:使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。

    • 遍历算法

      目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。

         A
       / \
      B   C
        / \   \
      D   E   F
      
    • 同步遍历
      了解了必要的算法后,我们可以简单地实现以下目录遍历函数。

      function travel(dir, callback) {
          fs.readdirSync(dir).forEach(function (file) {
              var pathname = path.join(dir, file);
      
              if (fs.statSync(pathname).isDirectory()) {
                  travel(pathname, callback);
              } else {
                  callback(pathname);
              }
          });
      }
      

    可以看到,该函数以某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断和处理。因此假设有以下目录:

    - /home/user/
        - foo/
            x.js
        - bar/
            y.js
        z.css
    

    使用以下代码遍历该目录时,得到的输入如下。

    travel('/home/user', function (pathname) {
            console.log(pathname);
        });
    
        ------------------------
        /home/user/foo/x.js
        /home/user/bar/y.js
        /home/user/z.css
    
    • 异步遍历

      如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理完全相同。travel函数的异步版本如下。

      function travel(dir, callback, finish) {
          fs.readdir(dir, function (err, files) {
              (function next(i) {
                  if (i < files.length) {
                      var pathname = path.join(dir, files[i]);
      
                      fs.stat(pathname, function (err, stats) {
                          if (stats.isDirectory()) {
                              travel(pathname, callback, function () {
                                  next(i + 1);
                              });
                          } else {
                              callback(pathname, function () {
                                  next(i + 1);
                              });
                          }
                      });
                  } else {
                      finish && finish();
                  }
              }(0));
          });
      }
      

    这里不详细介绍异步遍历函数的编写技巧,在后续章节中会详细介绍这个。总之我们可以看到异步编程还是蛮复杂的。

    文本编码

    使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。我们常用的文本编码有UTF8和GBK两种,并且UTF8文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。
    
    • BOM的移除

      BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符(”\uFEFF”),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:

      Bytes      Encoding
      ----------------------------
      FE FF       UTF16BE
      FF FE       UTF16LE
      EF BB BF    UTF8
      

    因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误。因此,使用NodeJS读取文本文件时,一般需要去掉BOM。例如,以下代码实现了识别和去除UTF8 BOM的功能。

    function readText(pathname) {
            var bin = fs.readFileSync(pathname);
    
            if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
                bin = bin.slice(3);
            }
    
            return bin.toString('utf-8');
        }
    
    • GBK转UTF8

      NodeJS支持在读取文本文件时,或者在Buffer转换为字符串时指定文本编码,但遗憾的是,GBK编码不在NodeJS自身支持范围内。因此,一般我们借助iconv-lite这个三方包来转换编码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。

      var iconv = require('iconv-lite');
      
      function readGBKText(pathname) {
          var bin = fs.readFileSync(pathname);
      
          return iconv.decode(bin, 'gbk');
      }
      
    • 单字节编码

      有时候,我们无法预知需要读取的文件采用哪种编码,因此也就无法指定正确的编码。比如我们要处理的某些CSS文件中,有的用GBK编码,有的用UTF8编码。虽然可以一定程度可以根据文件的字节内容猜测出文本编码,但这里要介绍的是有些局限,但是要简单得多的一种技术。

      首先我们知道,如果一个文本文件只包含英文字符,比如Hello World,那无论用GBK编码或是UTF8编码读取这个文件都是没问题的。这是因为在这些编码下,ASCII0~128范围内字符都使用相同的单字节编码。

      反过来讲,即使一个文本文件中有中文等字符,如果我们需要处理的字符仅在ASCII0~128范围内,比如除了注释和字符串以外的JS代码,我们就可以统一使用单字节编码来读取文件,不用关心文件的实际编码是GBK还是UTF8。以下示例说明了这种方法。

      1. GBK编码源文件内容:
          var foo = '中文';
      2. 对应字节:
          76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B
      3. 使用单字节编码读取后得到的内容:
          var foo = '{乱码}{乱码}{乱码}{乱码}';
      4. 替换内容:
          var bar = '{乱码}{乱码}{乱码}{乱码}';
      5. 使用单字节编码保存后对应字节:
          76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B
      6. 使用GBK编码读取后得到内容:
          var bar = '中文';
      

    模板引擎

    Posted on 2017-07-21

    模板原理

    模板的诞生是为了将显示与数据分离,模板技术多种多样,但其本质是将模板文件和数据通过模板引擎生成最终的HTML代码。

    模板引擎就是利用正则表达式识别模板标识,并利用数据替换其中的标识符。比如:

    Hello, <%= name%>
    

    数据是{name: '木的树'},那么通过模板引擎解析后,我们希望得到Hello, 木的树。模板的前半部分是普通字符串,后半部分是模板标识,我们需要将其中的标识符替换为表达式。模板的渲染过程如下:

    //字符串替换的思想
    function tmpl(str, obj) {
        if (typeof str === 'string') {
            return str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return obj[key];
            });
            }
    }
    
    var str = "Hello, <%= name%>";
    var obj = {name: "Lzz"};
    

    模板引擎

    引擎核心

    上面我们演示是简单的字符串替换,但对于模板引擎来说,要做的事情更复杂些。通常需要以下几个步骤:

    • 利用正则表达式分解出普通字符串和模板标识符,<%=%>的正则表达式为/<%=\s*([^%>]+)\s*%>/g.
    • 将模板标识符转换成普通的语言表达式
    • 生成待执行语句
    • 将数据填入执行,生成最终的字符串

    Demo代码如下:

    //编译的思想
    function tmpl(str, obj) {
        if (typeof str === 'string') {
            var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + obj." + key; // 在函数字符串中利用'包裹正常字符串
            });
            console.log(tm); // Hello, ' + obj.name
            tm = "return '" + tm; //"'Hello' + obj.name"
            console.log(tm); // return 'Hello, ' + obj.name
            var compile = new Function('obj', tm);
            console.log(compile(obj)); // Hello, Lzz
            return compile(obj);
        }
    }
    
    var str = "Hello, <%= name%>";
    var obj = {name: "Lzz"}; // Hello, Lzz
    tmpl(str,obj);
    

    模板编译

    上述代码中有如下部分:

    tm = "return '" + tm; //"'Hello' + obj.name"
    var compile = new Function('obj', tm);
    

    为了能够与数据一起执行生成字符串,我们需要将原始的模板字符串转换成一个函数对象。这个过程称为模板编译。模板编译使用了new Function(), 这里通过它创建了一个函数对象,语法如下:

    new Function(arg1, arg2,..., functionbody)
    

    Function()构造函数接受多个参数,最后一个参数作为函数体的内容,其之前的参数全部作为生成的新函数的参数。需要注意的是Function的参数全部是字符串类型,函数体部分对于字符串跟函数表达式一定要区分清楚,初学者往往在对函数体字符串中的普通字符串和表达式的拼接上犯错。一定要将函数体字符串和内部字符串正确拼接,如:

    new Function('obj', "return 'Hello,' + obj.name")
    

    模板编译过程中每次都要利用Function重新生成一个函数,浪费CPU。为此我们可以将函数缓存起来,代码如下:

    //模板预编译
      var tmpl = (function(){
          var cache = {};
          return function(str, obj){
              if (!typeof str === 'string') {
                  return;
              }
              var compile = cache[str];
              if (!cache[str]) {
                  var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                      var key = arguments[1];
                      return "' + obj." + key;
                  });
                  tm = "return '" + tm; //"'Hello' + obj.name"
                  compile = new Function('obj', tm);
                  cache[str] = compile;
              }
              return compile(obj); //预编译情况下应该返回compile函数
          }
      }());
      var str = "Hello, <%= name%>";
      var obj = {name: "Lzz"};
      tmpl(str, obj);
    

    利用with

    with 语句可以方便地用来引用某个特定对象中已有的属性,但是不能用来给对象添加属性。要给对象创建新的属性,必须明确地引用该对象。

    with(object instance)  
    {  
            //代码块  
    }  
    

    有时候,在一个程序代码中,多次需要使用某对象的属性或方法,照以前的写法,都是通过:对象.属性或者对象.方法这样的方式来分别获得该对象的属性和方法,着实有点麻烦,学习了with语句后,可以通过类似如下的方式来实现:

    with(objInstance)  
    {  
       var str = 属性1;  
    .....  
    } 
    

    去除了多次写对象名的麻烦。

    利用with我们可以不用把模板标识符转换成obj.name,只需要保持name标识符即可。

    // 利用with使得变量自己寻找对象, 找不到的视为普通字符串
    // 貌似return后面不能直接跟with
    //模板预编译
    var tmpl = (function(){
        var cache = {};
        return function(str, obj){
            if (!typeof str === 'string') {
                return;
            }
            var compile = cache[str];
            if (!cache[str]) {
                var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                    var key = arguments[1];
                    return "' + " + key;
                });
                tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "; } return tmp;"; //"'Hello' + obj.name"
                compile = new Function('obj', tm);
                cache[str] = compile;
            }
            return compile(obj); //预编译情况下应该返回compile函数
        }
    }());
    var str = "Hello, <%= name%>";
    var obj = {name: "LZZ"};
    tmpl(str, obj);
    

    XSS漏洞

    如果上面的obj变成var obj = {name: "<script>alert(\"XSS\")</script>"};,那么最终生成的结果就会变成:

    "Hello, <script>alert("XSS")</script>"
    

    为此我们需要堵上这个漏洞,基本就是要将形成HTML标签的字符转换成安全的字符,这些字符通常是&, <, >, “, ‘。转换函数如下:

    var strip = function(html) {
        return String(html)
        .replace(/&/g, '&amp;')//&
        .replace(/</g, '&lt;')//左尖号
        .replace(/>/g, '&gt;')//右尖号
        .replace(/"/g, '&quot;')//双引号"
        .replace(/'/g, '&#039;');//IE下不支持&apos;'
    }
    

    这样下来,模板引擎应该变成这样:

    var tmpl = (function() {
          var cache = {};
          var strip = function(html) {
              return String(html)
                  .replace(/&/g, '&amp;') //&
                  .replace(/</g, '&lt;') //左尖号
                  .replace(/>/g, '&gt;') //右尖号
                  .replace(/"/g, '&quot;') //双引号"
                  .replace(/'/g, '&#039;'); //IE下不支持&apos;'
          }
          return function(str, obj) {
              if (!typeof str === 'string') {
                  return;
              }
              var compile = cache[str];
              if (!cache[str]) {
                  //var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                  //    var key = arguments[1];
                  //    return "' + strip(" + key + ")";
                  //});
                  var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                      var code = arguments[1];
                      return "' + strip(" + code + ")"; //利用escape包裹code
                  }).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                      var key = arguments[1];
                      return "' + " + key;
                  });
                  tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "; } return tmp;"; //"'Hello' + obj.name"
                  compile = new Function('obj', 'strip', tm);
                  cache[str] = compile;
              }
              return compile(obj, strip); //预编译情况下应该返回compile函数
          }
      }());
    
      var str = "<%= name%>";
      var obj = { name: "<script>alert(\"XSS\")</script>"}; 
      tmpl(str, obj);
    

    这时候我们得到如下结果:

    "&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;"
    

    模板逻辑

    功能稍微强大的模板引擎,都允许在模板中添加一部分逻辑来控制页面的最终渲染。如:

    var str = "<%for(var i = 0; i < 3; i++){%>name is <%= name%=""> <%}%>"; <="" code="">

    这里我们用<%%>代表逻辑代码<%=%>代表模板中需要替换的标识符。我们的模板代码变成了如下所示:

    //模板逻辑
      var tmpl = (function() {
        var cache = {};
        var strip = function(html) {
            return String(html)
                .replace(/&/g, '&amp;') //&
                .replace(/</g, '&lt;') //左尖号
                .replace(/>/g, '&gt;') //右尖号
                .replace(/"/g, '&quot;') //双引号"
                .replace(/'/g, '&#039;'); //IE下不支持&apos;'
        }
        return function(str, obj) {
            debugger;
            if (!typeof str === 'string') {
                return;
            }
            var compile = cache[str];
            if (!cache[str]) {
                var tm = str.replace(/<%\s*([^=][^%>]*)\s*%>/g, function() {
                    var key = arguments[1];
                    console.log("';" + key + " tmp+='");
                    return "';" + key + " tmp+='"; // 逻辑代码需要一块块的拼接起来,为的是拼接成一段合理的函数字符串传递给new Function
                }).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                    var code = arguments[1];
                    return "' + strip(" + code + ") +'"; //利用escape包裹code ,加入模板逻辑时要注意,保证拼接成正确的函数字符串
                }).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                    var key = arguments[1];
                    return "' + " + key + "+ '"; //加入模板逻辑时要注意,保证拼接成正确的函数字符串
                });
                debugger;
                tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "'; } return tmp;"; //"'Hello' + obj.name"
                compile = new Function('obj', 'strip', tm);
                cache[str] = compile;
            }
            return compile(obj, strip); //预编译情况下应该返回compile函数
        }
      }());
    
      var str = "<%for(var i = 0; i < 3; i++){%>name is <%= name%=""> <%}%>"; var="" obj="{name:" "<script>alert(\"xss\")<="" script>"};="" tmpl(str,="" obj);="" <="" code="">

    第一步,我们将模板中的逻辑表达式找出来,用的正则表达式是/<%\s*([^=][^%>]*)\s*%>/g

    str.replace(/<%\s*([^=][^%>]*)\s*%>/g, function() {
                var key = arguments[1];
                return "';" + key + " tmp+='"; // 逻辑代码需要一块块的拼接起来,为的是拼接成一段合理的函数字符串传递给new Function
            })
    

    注意在拼接时,为了防止函数字符串中的字符串没有闭合对表达式造成影响,我们在key前后都加了’来保证其中的字符串闭合。
    第二步, 对可能存在的HTML标签进行转义

    .replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var code = arguments[1];
                return "' + strip(" + code + ") +'"; //利用escape包裹code ,加入模板逻辑时要注意,保证拼接成正确的函数字符串
            })
    

    同样需要注意前后的字符串闭合
    第三步,像先前一样处理模板标识符

    .replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + " + key + "+ '";//加入模板逻辑时要注意,保证拼接成正确的函数字符串
            })
    

    仍然要注意其中的字符串闭合问题。

    模板引擎是一个系统的问题,复杂模板还支持模板嵌套,这里就不介绍了,希望此文能够抛砖引玉,让大火带来更好的干货!

    转载自:

    http://www.cnblogs.com/dojo-lzz/p/5518474.html

    线上监控系统的设计

    Posted on 2017-07-20

    1. 确定监控指标(埋点)

    1.1 页面性能

    为什么要监控性能?

    这是一个最基本的问题,为什么要关注和监控前端性能?对于公司来说,性能在一定程度上与利益直接相关。国外有很多这方面的调研数据:

    性能 收益
    Google 延迟 400ms 搜索量下降 0.59%
    Bing 延迟 2s 收入下降 4.3%
    Yahoo 延迟 400ms 流量下降 5-9%
    Mozilla 页面打开减少 2.2s 下载量提升 15.4%
    Netflix 开启 Gzip 性能提升 13.25% 带宽减少50%

    为什么性能会影响公司的收益呢?根本原因还是在于性能影响了用户体验。加载的延迟、操作的卡顿等都会影响用户的使用体验。尤其是移动端,用户对页面响应延迟和连接中断的容忍度很低。想象一下你拿着手机打开一个网页想看到某个信息却加载半天的心情,你很可能选择直接离开换一个网页。谷歌也将页面加载速度作为 SEO 的一个权重,页面加载速度对用户体验和 SEO 的影响的调研有很多。

    尽管性能很重要,开发迭代过程中难免会有所忽视,性能会伴随产品的迭代而有所衰减。特别在移动端,网络一直是一个很大的瓶颈,而页面却越来越大,功能越来越复杂。并没有简单的几条黄金规则就可以搞定性能优化工作,我们需要一套性能监控系统持续监控、评估、预警页面性能状况、发现瓶颈,指导优化工作的进行。

    有哪些可用工具

    Page Speed

    Page Speed 是谷歌开发的分析和优化网页的工具,可以作为浏览器插件使用。工具基于一系列优化规则对网站进行检测,对于未通过的规则会给出详细的建议。与此类似的工具还有 Yslow 等,推荐使用gtmetrix网站同时查看多个分析工具的结果,如下图所示:

    WebPagetest

    WebPageTest 是一款非常优秀的网页前端性能测试工具,已开源。可以使用在线版,也可以自己搭建。国内也有利用 WebPagetest 搭建的性能测试平台,推荐使用阿里测 (以下示例使用阿里测进行测试)。

    使用 WebPagetest,你可以详细掌握网站加载过程中的瀑布流、性能得分、元素分布、视图分析等数据。其中比较直观的视图分析功能可以直接看到页面加载各个阶段的截屏:

    上图直观地展现了浏览类网站两个重要的时间点:白屏时间和首屏时间,即用户多久能在页面中看到内容,以及多久首屏渲染完成(包含图片等元素加载完成)。这两个时间点直接决定了用户需要等待多久才能看到自己想看到的信息。谷歌优化建议中也提到减少非首屏使用的 css 及 js,尽快让首屏呈现。

    PhantomJS

    PhantomJS轻松地将监控带入了自动化的行列。Phantom JS 是一个服务器端的 JavaScript API 的 WebKit,基于它可以轻松实现 web 自动化测试。PhantomJS 需要一定编程工作,但也更灵活。官方文档中已经有一个完整的获取网页加载 har 文件的示例,具体说明可以查看此文档,国内也有不少关于此工具的介绍。另外新浪@貘吃馍香开发的类似工具berserkJS也挺不错,还贴心的提供了首屏统计的功能,具体文章可以查看此处。

    为什么要监控真实访问性能

    到此肯定有同学问,既然有这么多优秀的工具,为什么要监控线上用户真实访问性能呢?

    我们发现,工具模拟测试会在一定程度上与真实情况偏离,有时无法反映性能的波动情况。另外除了白屏首屏之类的基础指标,产品线同样关注产品相关的指标,例如广告可见、搜索可用、签到可用等,这些功能直接与页面 JS 加载相关,通过工具较难模拟。

    为了持续监控不同网络环境下用户访问情况与页面各功能可用状况,我们选择在页面中植入 JS 来监控线上真实用户访问性能,同时利用已有的分析工具作为辅助,形成一套完整多元的数据监控体系,为产品线的评估与优化提供可靠的数据。

    关于不同监控方式的简单对比可以查看下表:

    类型 优点 缺点 示例
    非侵入式 指标齐全、客户端主动监测、竞品监控 无法知道性能影响用户数、采样少容易失真、无法监控复杂应用与细分功能 Pagespeed、PhantomJS、UAQ
    侵入式 真实海量用户数据、能监控复杂应用与业务功能、用户点击与区域渲染 需插入脚本统计、网络指标不全、无法监控竞品 DP 、Google 统计

    前端的数据有哪些

    前端的数据其实有很多,从大众普遍关注的 PV、UV、广告点击量,到客户端的网络环境、登陆状态,再到浏览器、操作系统信息,最后到页面性能、JS 异常,这些数据都可以在前端收集到。数据很多、很杂,不进行很好的分类肯定会导致统计混乱,也不利于统计代码的组织,下面就对几种普遍的数据需求进行了分类:

    1、访问

    访问数据是基于用户每次在浏览器上打开目标页面来统计的,它是以 PV 为粒度的统计,一个 PV 只统计一次访问数据。访问数据可以算作是最基础、覆盖面最广的一种统计,可以统计到很多的指标项,下面列出了一些较为常见的指标项:

    • PV/UV:最基础的 PV(页面访问数量)、UV(独立访问用户数量)

      1、PV(page view)即页面浏览量或点击量,是衡量一个网站或网页用户访问量。具体的说,PV值就是所有访问者在24小时(0点到24点)内看了某个网站多少个页面或某个网页多少次。PV是指页面刷新的次数,每一次页面刷新,就算做一次PV流量。
        度量方法就是从浏览器发出一个对网络服务器的请求(Request),网络服务器接到这个请求后,会将该请求对应的一个网页(Page)发送给浏览器,从而产生了一个PV。那么在这里只要是这个请求发送给了浏览器,无论这个页面是否完全打开(下载完成),那么都是应当计为1个PV。

      实例代码:

      通过创建img src ,来请求服务的,后台接到对应的请求进行相应的处理即可记录数据了。

      //   统计pv 和uv
      api.picLog = function (clickFlag){
          window.setTimeout(function(){
              var playclick = new Image();
              playclick.src="http://*****/logtjsj/commsj/commjstj/www2016/" + clickFlag + ".jpg";
          },300);
      };
      

      2、UV(unique visitor)即独立访客数,指访问某个站点或点击某个网页的不同IP地址的人数。在同一天内,UV只记录第一次进入网站的具有独立IP的访问者,在同一天内再次访问该网站则不计数。

    • 页面来源:页面的 refer,可以定位页面的入口

    • 操作系统:了解用户的 OS 状况,帮助分析用户群体的特征,特别是移动端,iOS 和 Android 的分布就更有意义了
    • 浏览器:可以统计到各种浏览器的占比,对于是否继续兼容 IE6、新技术(HTML5、CSS3 等)的运用等调研提供参考价值
    • 分辨率:对页面设计提供参考,特别是响应式设计
    • 登录率:百度也开始看重登陆,登陆用户具有更高的分析价值,引导用户登陆是非常重要的
    • 地域分布:访问用户在地理位置上的分布,可以针对不同地域做运营、活动等
    • 网络类型:wifi/3G/2G,为产品是否需要适配不同网络环境做决策
    • 访问时段:掌握用户访问时间的分布,引导消峰填谷、节省带宽
    • 停留时长:判断页面内容是否具有吸引力,对于需要长时间阅读的页面比较有意义
    • 到达深度:和停留时长类似,例如百度百科,用户浏览时的页面到达深度直接反映词条的质量
    2、性能

    页面 DOM 结构越来越复杂,但是又要追求用户体验,这就对页面的性能提出了更高的要求。性能的监控数据主要是用来衡量页面的流畅程度,也有一些主要的指标:

    • 白屏时间:即用户多久能在页面中看到内容,用户从打开页面开始到页面开始有东西呈现为止,这过程中占用的时间就是白屏时间
    • 首屏时间:多久首屏渲染完成(包含图片等元素加载完成),用户浏览器首屏内所有内容都呈现出来所花费的时间
    • 用户可操作时间:用户可以进行正常的点击、输入等操作
    • 总下载时间:页面所有资源都加载完成并呈现出来所花的时间,即页面 onload 的时间
    • 自定义的时间点:对于开发人员来说,完全可以自定义一些时间点,例如:某个组件 init 完成的时间、某个重要模块加载的时间等等
    • 网络指标(DNS、TCP、首字节、html传输时间)
    3、点击

    在用户的所有操作中,点击应该是最为主要的一个行为,包含了:pc 端鼠标的 click,移动端手指的 touch。用户的每次点击都是一次诉求,从点击数据中可以挖掘的信息其实有很多,下面只列出了我们目前所关注的指标:

    • 页面总点击量
    • 人均点击量:对于导航类的网页,这项指标是非常重要的
    • 流出 url:同样,导航类的网页,直接了解网页导流的去向
    • 点击时间:用户的所有点击行为,在时间上的分布,反映了用户点击操作的习惯
    • 首次点击时间:同上,但是只统计用户的第一次点击,如果该时间偏大,是否就表明页面很卡导致用户长时间不能点击呢?
    • 点击热力图:根据用户点击的位置,我们可以画出整个页面的点击热力图,可以很直观的了解到页面的热点区域
    4、异常

    这里的异常是指 JS 的异常,用户的浏览器上报 JS 的 bug,这会极大地降低用户体验,对于浏览器型号、版本满天飞的今天,再 NB 的程序员也难免会有擦枪走火的时候,当然 QA 能够覆盖到大部分的 bug,但肯定也会有一些 bug 在线上出现。JS 的异常捕获只有两种方式:window.onerror、try/catch,关于我们是如何做的将在后续的文章中有详细的描述,这里只列出捕获到异常时,一般需要采集哪些信息(主要用来 debug 异常):

    • 异常的提示信息:这是识别一个异常的最重要依据,如:’e.src’ 为空或不是对象
    • JS 文件名
    • 异常所在行
    • 发生异常的浏览器
    • 堆栈信息:必要的时候需要函数调用的堆栈信息,但是注意堆栈信息可能会比较大,需要截取
    5、其他

    除了上面提到的 4 类基本的数据统计需求,我们当然还可以根据实际情况来定义一些其他的统计需求,如用户浏览器对 canvas 的支持程度,再比如比较特殊的 – 用户进行轮播图翻页的次数,这些数据统计需求都是前端能够满足的,每一项统计的结果都体现了前端数据的价值。

    1.2 用户规模

    • App下载量(只针对App)
      • 每日下载App总数
    • 注册激活用户总数
      • 每日新增注册用户数
      • 注册转化率
    • 日均活跃用户数

    1.3 市场运营

    • 活跃用户比例
    • 用户主要来源
    • 留存率
      • 使用留存(日、周、月)
      • 购买留存(日、周、月)
    • 浏览情况
      • 人均浏览页面量
      • 人均浏览时长
      • 访问次数
      • 访问频率
    • 互动情况
      • 每日评论用户数
      • 交互发反馈次数(收藏、分享、喜欢等功能)

    1.4 商业效果

    • 日均流水
    • 订单转化率
    • 客单价

    2. 确定采集数据的方式(数据统计)

    如何采集

    在前端,通过注入 JS 脚本,使用一些 JS API(如:!!window.localStorage 就可以检验浏览器是否支持 localStorage)或者监听一些事件(如:click、window.onerror、onload 等)就可以得到数据。捕获到这些数据之后,需要将数据发送回服务器端,一般都是采用访问一个固定的 url,把数据作为该 url 的 query string,如:http://www.baidu.com/u.gif?data1=hello&data2=hi。

    在实践的过程中我们抽离了一套用于前端统计的框架alog,方便开发者书写自己的统计脚本,具体的使用方法和 API 见github。下面就使用 alog来简单说明如何进行前端数据的采集:

    例如:你需要统计页面的 PV,顺便加上页面来源(refer)

    // 加载 alog,alog 是支持异步的
    void function(e,t,n,a,o,i,m){
    e.alogObjectName=o,e[o]=e[o]||function(){(e[o].q=e[o].q||[]).push(arguments)},e[o].l=e[o].l||+new Date,i=t.createElement(n),i.asyn=1,i.src=a,m=t.getElementsByTagName(n)[0],m.parentNode.insertBefore(i,m)
    }(window,document,"script","http://uxrp.github.io/alog/dist/alog.min.js","alog");
    
    // 定义一个统计模块 pv
    alog('define', 'pv', function(){ 
       var pvTracker = alog.tracker('pv');
       pvTracker.set('ref', document.referrer); // 设定 ref 参数
       return pvTracker;
    });
    
    // 创建一个 pv 统计模块的实例
    alog('pv.create', {
        postUrl: 'http://localhost/u.gif' // 指定上传数据的 url 地址
    });
    
    // 上传数据
    alog('pv.send', "pageview"); // 指明是 pageview
    

    在页面上部署上面的代码,浏览器将会发送下面的 http 请求:

    http://localhost/u.gif?t=pageview&ref=yourRefer
    

    再例如:JS 异常的采集,需要进行事件监听

    // 加载 alog
    void function(e,t,n,a,o,i,m){
    e.alogObjectName=o,e[o]=e[o]||function(){(e[o].q=e[o].q||[]).push(arguments)},e[o].l=e[o].l||+new Date,i=t.createElement(n),i.asyn=1,i.src=a,m=t.getElementsByTagName(n)[0],m.parentNode.insertBefore(i,m)
    }(window,document,"script","http://uxrp.github.io/alog/dist/alog.min.js","alog");
    
    // 定义一个统计模块 err
    alog('define', 'err', function(){ 
       var errTracker = alog.tracker('err');
       window.onerror = function(message, file, line) { //监听 window.onerror
            errTracker.send('err', {msg:message, js:file, ln:line});
        };
       return errTracker;
    });
    
    // 创建一个 err 统计模块的实例
    alog('err.create', {
        postUrl: 'http://localhost/u.gif'
    });
    

    这时,只要页面中 JS 发生异常,就会发送如下面的 HTTP 请求

    http://localhost/u.gif?t=err&msg=errMessage&js=jsFileName&ln=errLine
    

    2.1 确定统计起点

    技术实现:

    在用户输入 URL 或者点击链接的时候就开始统计,可以借用Navigation Timing API或者cookie时间戳。

    2.2 统计白屏时间

    技术实现:

    通过获取头部资源加载完的时刻来近似统计白屏时间。

    尽管并不精确,但却考虑了影响白屏的主要因素:首字节时间和头部资源加载时间。

    头部资源加载时间

    技术实现:

    在浏览器 head 内底部加一句 JS, 统计头部资源加载结束点。

    <!DOCTYPE HTML>
    <html>
       <head>
           <meta charset="UTF-8"/>
       <script>
         var start_time = +new Date; //测试时间起点,实际统计起点为 DNS 查询
       </script>
       <!-- 3s 后这个 js 才会返回 -->
       <script src="script.PHP"></script>  
       <script>
         var end_time = +new Date; //时间终点
         var headtime = end_time - start_time; //头部资源加载时间    
         console.log(headtime);
       </script>
       </head>
       <body>    
       <p>在头部资源加载完之前页面将是白屏</p> 
       </body>
    </html>
    

    2.3 统计首屏时间

    通过统计首屏内图片的加载时间便可以获取首屏渲染完成的时间。

    技术实现

    统计流程如下:

    首屏位置调用 API 开始统计 -> 绑定首屏内所有图片的 load 事件 -> 页面加载完后判断图片是否在首屏内,找出加载最慢的一张 -> 首屏时间
    

    这是同步加载情况下的简单统计逻辑,另外需要注意的几点:

    • 页面存在 iframe 的情况下也需要判断加载时间
    • gif 图片在 IE 上可能重复触发 load 事件需排除
    • 异步渲染的情况下应在异步获取数据插入之后再计算首屏
    • css 重要背景图片可以通过 JS 请求图片 url 来统计(浏览器不会重复加载)
    • 没有图片则以统计 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
      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
      function getOffsetTop(ele) {
      var offsetTop = ele.offsetTop;
      if (ele.offsetParent !== null) {
      offsetTop += getOffsetTop(ele.offsetParent);
      }
      return offsetTop;
      }

      var firstScreenHeight = win.screen.height;
      var firstScreenImgs = [];
      var isFindLastImg = false;
      var allImgLoaded = false;
      var t = setInterval(function() {
      var i, img;
      if (isFindLastImg) {
      if (firstScreenImgs.length) {
      for (i = 0; i < firstScreenImgs.length; i++) {
      img = firstScreenImgs[i];
      if (!img.complete) {
      allImgLoaded = false;
      break;
      } else {
      allImgLoaded = true;
      }
      }
      } else {
      allImgLoaded = true;
      }
      if (allImgLoaded) {
      collect.add({
      firstScreenLoaded: startTime - Date.now()
      });
      clearInterval(t);
      }
      } else {
      var imgs = body.querySelector('img');
      for (i = 0; i < imgs.length; i++) {
      img = imgs[i];
      var imgOffsetTop = getOffsetTop(img);
      if (imgOffsetTop > firstScreenHeight) {
      isFindLastImg = true;
      break;
      } else if (imgOffsetTop <= firstScreenHeight && !img.hasPushed) {
      img.hasPushed = 1;
      firstScreenImgs.push(img);
      }
      }
      }
      }, 0);



      doc.addEventListener('DOMContentLoaded', function() {
      var imgs = body.querySelector('img');
      if (!imgs.length) {
      isFindLastImg = true;
      }
      });

      win.addEventListener('load', function() {
      allImgLoaded = true; isFindLastImg = true; if (t) { clearInterval(t); } collect.log(collect.global); });

    2.4 统计用户可操作时间

    技术实现:

    用户可操作默认可以统计domready时间。

    2.5 统计总下载时间

    技术实现:

    总下载时间默认可以统计onload时间,这样可以统计同步加载的资源全部加载完的耗时。如果页面中存在很多异步渲染,可以将异步渲染全部完成的时间作为总下载时间。

    2.6 网络指标

    2.6.1 网络类型判断

    技术实现:

    可以通过测速的方式来判断不同 IP 段对应的网络。测速例如比较经典的有 facebook 的方案。

    2.6.2 网络耗时统计

    技术实现:

    网络耗时数据可以借助一些工具(如Navigation Timing 接口)获取,可以获取页面所有静态资源的加载耗时。通过此接口可以轻松获取 DNS、TCP、首字节、html 传输等耗时。

    3. 数据分析

    可以从多个维度去分析数据。大数据处理需要借助 Hadoop、Hive 等方式,而对于普通站点则任意一种后端语言处理即可。

    3.1 均值与分布

    均值与分布是数据处理中最常见的两种方式。因为它能直观的表示指标的趋势与分布状况,方便进行评估、瓶颈发现与告警。处理过程中应去除异常值,例如明显超过阈值的脏数据等。

    3.2 多维分析

    为了方便挖掘性能可能的瓶颈,需要从多维的角度对数据进行分析。

    例如移动端最重要的维度就是网络,数据处理上除了总体数据,还需要根据网络类型对数据进行分析。

    常见的维度还有系统、浏览器、地域运营商等。我们还可以根据自身产品的特点来确定一些维度,例如页面长度分布、简版炫版等。

    需要注意的是维度并不是越多越好,需要根据产品的特点及终端来确定。维度是为了方便查找性能瓶颈。

    4. 数据可视化

    技术实现:

    采用Highcharts,Echarts制作可视化图表。

    Echart可以统计的有:

    浏览器的占比情况、用户登陆情况、用户地理位置分布、浏览器的多天占比波动情况、搜索词排行等等。

    5. 智能告警

    5.1 安全监测

    • 文字监测
    • 链接监测:监测网站的链接,对暗链进行扫描
    • 图片监测
    • 脚本监测
    • 挂马监测

    5.2 性能监测

    监控服务器性能(进程是否存活、内存、cpu、网卡)变化,超过阈值即报警

    5.3 诊断功能

    监测网站内部的错误页面,内部链接错误等监测。

    5.4 报表功能

    可以导出报表。

    5.5 报警方式

    邮件、短信、微信等方式。

    网站统计中的数据收集原理及实现_埋点统计

    网站数据统计分析工具是网站站长和运营人员经常使用的一种工具,比较常用的有谷歌分析、百度统计和腾讯分析等等。所有这些统计分析工具的第一步都是网站访问数据的收集。目前主流的数据收集方式基本都是基于javascript的。

    数据收集原理分析

    简单来说,网站统计分析工具需要收集到用户浏览目标网站的行为(如打开某网页、点击某按钮、将商品加入购物车等)及行为附加数据(如某下单行为产生的订单金额等)。早期的网站统计往往只收集一种用户行为:页面的打开。而后用户在页面中的行为均无法收集。这种收集策略能满足基本的流量分析、来源分析、内容分析及访客属性等常用分析视角,但是,随着ajax技术的广泛使用及电子商务网站对于电子商务目标的统计分析的需求越来越强烈,这种传统的收集策略已经显得力不能及。

    后来,Google在其产品谷歌分析中创新性的引入了可定制的数据收集脚本,用户通过谷歌分析定义好的可扩展接口,只需编写少量的javascript代码就可以实现自定义事件和自定义指标的跟踪和分析。目前百度统计、搜狗分析等产品均照搬了谷歌分析的模式。

    其实说起来两种数据收集模式的基本原理和流程是一致的,只是后一种通过javascript收集到了更多的信息。下面看一下现在各种网站统计工具的数据收集基本原理。

    流程概览

    首先通过一幅图总体看一下数据收集的基本流程。

    图1. 网站统计数据收集基本流程

    首先,用户的行为会触发浏览器对被统计页面的一个http请求,这里姑且先认为行为就是打开网页。当网页被打开,页面中的埋点javascript片段会被执行,用过相关工具的朋友应该知道,一般网站统计工具都会要求用户在网页中加入一小段javascript代码,这个代码片段一般会动态创建一个script标签,并将src指向一个单独的js文件,此时这个单独的js文件(图1中绿色节点)会被浏览器请求到并执行,这个js往往就是真正的数据收集脚本。数据收集完成后,js会请求一个后端的数据收集脚本(图1中的backend),这个脚本一般是一个伪装成图片的动态脚本程序,可能由php、python或其它服务端语言编写,js会将收集到的数据通过http参数的方式传递给后端脚本,后端脚本解析参数并按固定格式记录到访问日志,同时可能会在http响应中给客户端种植一些用于追踪的cookie。

    上面是一个数据收集的大概流程,下面以谷歌分析为例,对每一个阶段进行一个相对详细的分析。

    埋点脚本执行阶段

    若要使用谷歌分析(以下简称GA),需要在页面中插入一段它提供的javascript片段,这个片段往往被称为埋点代码。下面是我的博客中所放置的谷歌分析埋点代码截图:

    图2. 谷歌分析埋点代码

    其中_gaq是GA的的全局数组,用于放置各种配置,其中每一条配置的格式为:

    _gaq.push(['Action', 'param1', 'param2', ...]);
    

    Action指定配置动作,后面是相关的参数列表。GA给的默认埋点代码会给出两条预置配置,_setAccount用于设置网站标识ID,这个标识ID是在注册GA时分配的。_trackPageview告诉GA跟踪一次页面访问。更多配置请参考:https://developers.google.com/analytics/devguides/collection/gajs/。实际上,这个_gaq是被当做一个FIFO队列来用的,配置代码不必出现在埋点代码之前,具体请参考上述链接的说明。

    就本文来说,_gaq的机制不是重点,重点是后面匿名函数的代码,这才是埋点代码真正要做的。这段代码的主要目的就是引入一个外部的js文件(ga.js),方式是通过document.createElement方法创建一个script并根据协议(http或https)将src指向对应的ga.js,最后将这个element插入页面的dom树上。

    注意ga.async = true的意思是异步调用外部js文件,即不阻塞浏览器的解析,待外部js下载完成后异步执行。这个属性是HTML5新引入的。

    数据收集脚本执行阶段

    数据收集脚本(ga.js)被请求后会被执行,这个脚本一般要做如下几件事:

    1、通过浏览器内置javascript对象收集信息,如页面title(通过document.title)、referrer(上一跳url,通过document.referrer)、用户显示器分辨率(通过windows.screen)、cookie信息(通过document.cookie)等等一些信息。

    2、解析_gaq收集配置信息。这里面可能会包括用户自定义的事件跟踪、业务数据(如电子商务网站的商品编号等)等。

    3、将上面两步收集的数据按预定义格式解析并拼接。

    4、请求一个后端脚本,将信息放在http request参数中携带给后端脚本。

    这里唯一的问题是步骤4,javascript请求后端脚本常用的方法是ajax,但是ajax是不能跨域请求的。这里ga.js在被统计网站的域内执行,而后端脚本在另外的域(GA的后端统计脚本是http://www.google-analytics.com/__utm.gif),ajax行不通。一种通用的方法是js脚本创建一个Image对象,将Image对象的src属性指向后端脚本并携带参数,此时即实现了跨域请求后端。这也是后端脚本为什么通常伪装成gif文件的原因。通过http抓包可以看到ga.js对__utm.gif的请求:

    图3. 后端脚本请求的http包

    可以看到ga.js在请求__utm.gif时带了很多信息,例如utmsr=1280×1024是屏幕分辨率,utmac=UA-35712773-1是_gaq中解析出的我的GA标识ID等等。

    值得注意的是,__utm.gif未必只会在埋点代码执行时被请求,如果用_trackEvent配置了事件跟踪,则在事件发生时也会请求这个脚本。

    由于ga.js经过了压缩和混淆,可读性很差,我们就不分析了,具体后面实现阶段我会实现一个功能类似的脚本。

    后端脚本执行阶段

    GA的__utm.gif是一个伪装成gif的脚本。这种后端脚本一般要完成以下几件事情:

    1、解析http请求参数的到信息。

    2、从服务器(WebServer)中获取一些客户端无法获取的信息,如访客ip等。

    3、将信息按格式写入log。

    5、生成一副1×1的空gif图片作为响应内容并将响应头的Content-type设为image/gif。

    5、在响应头中通过Set-cookie设置一些需要的cookie信息。

    之所以要设置cookie是因为如果要跟踪唯一访客,通常做法是如果在请求时发现客户端没有指定的跟踪cookie,则根据规则生成一个全局唯一的cookie并种植给用户,否则Set-cookie中放置获取到的跟踪cookie以保持同一用户cookie不变(见图4)。

    图4. 通过cookie跟踪唯一用户的原理

    这种做法虽然不是完美的(例如用户清掉cookie或更换浏览器会被认为是两个用户),但是是目前被广泛使用的手段。注意,如果没有跨站跟踪同一用户的需求,可以通过js将cookie种植在被统计站点的域下(GA是这么做的),如果要全网统一定位,则通过后端脚本种植在服务端域下(我们待会的实现会这么做)。

    系统的设计实现

    根据上述原理,我自己搭建了一个访问日志收集系统。总体来说,搭建这个系统要做如下的事:

    图5. 访问数据收集系统工作分解

    下面详述每一步的实现。我将这个系统叫做MyAnalytics。

    确定收集的信息

    为了简单起见,我不打算实现GA的完整数据收集模型,而是收集以下信息。

    埋点代码

    埋点代码我将借鉴GA的模式,但是目前不会将配置对象作为一个FIFO队列用。一个埋点代码的模板如下:

    <script type="text/javascript"> 
        var _maq = _maq || []; 
        _maq.push(['_setAccount', '网站标识']);   
        (function() {     
            var ma = document.createElement('script'); 
            ma.type = 'text/javascript'; 
            ma.async = true;     
            ma.src = ('https:' == document.location.protocol ? 'https://analytics' : 'http://analytics') + '.codinglabs.org/ma.js';                 var s = document.getElementsByTagName('script')[0];             s.parentNode.insertBefore(ma, s); 
        })(); 
    </script>
    

    这里我启用了二级域名analytics.codinglabs.org,统计脚本的名称为ma.js。当然这里有一点小问题,因为我并没有https的服务器,所以如果一个https站点部署了代码会有问题,不过这里我们先忽略吧。

    前端统计脚本

    我写了一个不是很完善但能完成基本工作的统计脚本ma.js:

    (function () {     var params = {};     //Document对象数据     if(document) {         params.domain = document.domain || '';          params.url = document.URL || '';          params.title = document.title || '';          params.referrer = document.referrer || '';      }        //Window对象数据     if(window && window.screen) {         params.sh = window.screen.height || 0;         params.sw = window.screen.width || 0;         params.cd = window.screen.colorDepth || 0;     }        //navigator对象数据     if(navigator) {         params.lang = navigator.language || '';      }        //解析_maq配置     if(_maq) {         for(var i in _maq) {             switch(_maq[i][0]) {                 case '_setAccount':                     params.account = _maq[i][1];                     break;                 default:                     break;             }            }        }        //拼接参数串     var args = '';      for(var i in params) {         if(args != '') {             args += '&';         }            args += i + '=' + encodeURIComponent(params[i]);     }          //通过Image对象请求后端脚本     var img = new Image(1, 1);      img.src = 'http://analytics.codinglabs.org/1.gif?' + args; })();
    

    整个脚

    参考自:

    http://blog.csdn.net/ddcowboy/article/details/55511304

    http://fex.baidu.com/blog/2014/05/front_end-data/

    http://blog.csdn.net/u010427666/article/details/52173219

    原理:

    http://www.ithao123.cn/content-1820695.html

    1…456…8
    xixijiang

    xixijiang

    切莫停下前进的脚步

    74 posts
    1 categories
    12 tags
    © 2019 xixijiang
    Powered by Hexo
    Theme - NexT.Muse