Vue的生命周期和前端路由使用

0. 前言

近半年来,我一直从事一个报表管理系统的开发。管理系统是给人用的,但我们团队并没有前端,所以我就兼任了大部分前端开发工作。在这半年的开发工作中,我学习了一些前端内容,在这里做一个总结并分享给大家。

阅读本文,我假设大家是已了解HTML/CSS和JavaScript中级知识的后端开发。

1. Vue的生命周期

1.1 Vue是什么

对于后端开发人员来讲,写前端最不想写的代码就是数据渲染,因为需要使用JS直接操作DOM树,这个过程极其、并且无聊。而Vue是一套用于构建用户界面的JS框架,它类似于React、AngularJS等框架。在写出一些简单的模板后,它能够帮你自动渲染出页面,并且在数据发生改变后,自动重新渲染页面。

Vue官网:vuejs.org

这里是一个简单的demo:

在线演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
</head>

<body>
<div id="app">
{{ message }}
</div>
</body>

<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!"
}
});
</script>
</html>

你可以尝试调整js代码中message的值,页面上显示的内容也会随之改变。

想要了解更多关于Vue的内容,可以查看 官网教程

1.2 Vue生命周期

java开发的同学都知道Servlet,Tomcat,Spring等技术或框架,他们都存在生命周期的概念。为什么会有生命周期的概念?原因是业务代码是被这些技术或框架调度执行的,而且调度器自身代码和业务代码一般会交叉执行;另外业务代码一般无法知晓调度器的状态变更,调度器就需要通过定义不同执行阶段,对外提供扩展点。

Vue也一样,它也是一个框架,因此也需要定义不同执行阶段,方便使用者扩展。

Vue的生命周期:

光看可能也无法有一个直观的感受,所以这里有一个简单的demo:

在线演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
</head>

<body>
<div id="app">
{{ message }}
</div>
</body>

<script type="text/javascript">
let print = function(vue, group) {
console.group(group);
console.log("el", vue.$el);
console.log("data", vue.$data);
console.groupEnd();
};

var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!"
},
beforeCreate() {
print(this, "beforeCreate");
},
created() {
print(this, "created");
},
beforeMount() {
print(this, "beforeMount");
},
mounted() {
print(this, "mounted");
},
beforeUpdate() {
print(this, "beforeUpdate");
},
updated() {
print(this, "updated");
},
beforeDestroy() {
print(this, "beforeDestroy");
},

destroyed() {
print(this, "destroyed");
}
});

setTimeout(() => {
console.warn("准备改变data数据");
app.message = "this is my hello world";

setTimeout(() => {
console.warn("准备销毁app");
app.$destroy();

setTimeout(() => {
console.warn("尝试再改变data数据");
app.message = "is destroy?";
}, 1000);
}, 1000);
}, 1000);

</script>
</html>

在这段代码中,我们在vue的不同阶段都执行打印方法,看看这些阶段的顺序是怎样的,并且是如何触发的。

1.2.1 初始化阶段

在js代码开始执行后,就会触发beforeCreate和created方法。在created阶段,vue会拿到指定data中的数据。

upload successful

1.2.2 挂载阶段

我们对着前面的生命周期图来看,可以看到,我们指定了el并且没有指定template。所以在beforeMount阶段会拿到el对应的html作为模板编译。之后,在mounted阶段,我们看到数据已经被渲染到el中。

upload successful

1.2.3 更新阶段

当我们触发了data数据变更时,则会调用beforeUpdate和updated方法。

upload successful

1.2.4 销毁阶段

当我们调用vue的销毁方法时,则会触发beforeDestroy和destroyed方法。并且在destroy之后,不会再响应data数据的变更。

upload successful

1.2.5 小结

已上,我们简单的过了一下vue的生命周期。由于本位重点不在el和template上,更多有关生命周期的内容可以参考这篇文章 通俗易懂了解Vue组件的生命周期

1.3 Vue组件介绍

了解完Vue的生命周期,我们再来看看Vue组件。什么是Vue组件?你可以理解为Java中的Class。之前咱们写的Vue实例就是Java中直接写main方法,不牵扯类和对象。而Vue组件就是Class类,你可以在main方法中new出不同的Class实例。

来看看这段代码:

在线演示

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
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
</head>

<body>
<div id="app">
<p>{{ message }}</p>
<cp1></cp1>
</div>
</body>

<script type="text/javascript">
let print = function(vue, group) {
console.group(group);
console.log("el", vue.$el);
console.log("data", vue.$data);
console.groupEnd();
};

// 全局注册一个组件
Vue.component("cp1", {
data: function() {
return {
name: "CP1"
};
},
template: "<p>{{ name }}</p>",
beforeCreate() {
print(this, "cp1 beforeCreate");
},
created() {
print(this, "cp1 created");
},
beforeMount() {
print(this, "cp1 beforeMount");
},
mounted() {
print(this, "cp1 mounted");
},
beforeUpdate() {
print(this, "cp1 beforeUpdate");
},
updated() {
print(this, "cp1 updated");
},
beforeDestroy() {
print(this, "cp1 beforeDestroy");
},

destroyed() {
print(this, "cp1 destroyed");
}
});

var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!"
},
beforeCreate() {
print(this, "beforeCreate");
},
created() {
print(this, "created");
},
beforeMount() {
print(this, "beforeMount");
},
mounted() {
print(this, "mounted");
},
beforeUpdate() {
print(this, "beforeUpdate");
},
updated() {
print(this, "updated");
},
beforeDestroy() {
print(this, "beforeDestroy");
},

destroyed() {
print(this, "destroyed");
}
});

setTimeout(() => {
console.warn("准备改变data数据");
app.message = "this is my hello world";

setTimeout(() => {
console.warn("准备销毁app");
app.$destroy();

setTimeout(() => {
console.warn("尝试再改变data数据");
app.message = "is destroy?";
}, 1000);
}, 1000);
}, 1000);

</script>
</html>

在这段代码中,我们注册了一个叫cp1的组件。注册组件需要注意一个事情,就是data返回的必须是个函数,原因是组件可以实例化很多个,多个组件实例之间data数据是要隔离的。另外还需要设置template。

执行完这段代码,可以看到组件在父实例beforeMount之前会先mount,在父实例beforeDestroy之前会先destroy。

如果想要了解更多有关Vue实例的知识,可以查看 官网:深入了解组件

2. 前端路由化开发

2.1 前端路由是什么

路由这个词大家应该都听说过(除非你没用过路由器),那么在一个管理系统中也有路由一说,例如在springmvc中通过不同的uri选择不同的controller生成不同的view。

而前端路由又从何说起?最开始的互联网大多数网页都是直接返回html代码,用户每次交互都是需要跳转刷新页面,整个操作体感不是很好。随着互联网的发展,1996年微软提出iframe标签,从而带来了异步加载和请求元素的概念。在1998年,微软提出了Ajax( Asynchronous JavaScript And XML)的基本概念(XMLHttpRequest的前身)。最终大多数浏览器都提供了XMLHttpRequest的实现。

在有了异步加载技术方案Ajax后,我们发现一个系统可以只有一个页面,通过响应用户的交互,异步加载相关数据并展示在前台。这样,访问这个系统就像是在使用一个本地软件。

这个时候问题来了,当用户噼里啪啦做了一堆查询筛选后,我们的确也给用户展示了相关数据。可用户想把这个网页收藏起来供下次直接访问,或者发送给别人看。问题就出现了,由于一个url对应着一个前端单页,每次用户打开这个url,看到的都是最初的页面,而不是经过噼里啪啦筛选后的页面。

怎么办呢?URL协议的组成部分中有一个hash(锚),修改锚并不会向后端发起请求。所以做前端的同学就开始利用这个锚,把用户的筛选项保存在这个锚上,每当用户打开带有锚的url,js就能根据锚来还原最初用户所做的筛选。

2014年以后,HTML5提供了HistoryAPI,也可以实现路由的功能。

前端路由是什么东西

前端路由的基本原理

2.2 VueRouter介绍

VueRouter是Vue的核心插件,是官方指定的路由管理器,它和Vue深度集成。

这里我们来演示一下VueRouter的简单使用:

在线演示

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

<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.1.3/vue-router.js"></script>
</head>

<body>
<div id="app">
<p>{{ message }}</p>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/cp1">Go to CP1</router-link>
<router-link to="/cp2">Go to CP2</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</body>

<script type="text/javascript">
// cp1组件
const cp1 = {
data: function() {
return {
name: "CP1"
};
},
mounted() {
console.log("cp1 mounted");
},

destroyed() {
console.log("cp1 destroyed");
},
template: "<p>{{ name }}</p>"
};

// cp2组件
const cp2 = {
data: function() {
return {
name: "CP2"
};
},
mounted() {
console.log("cp2 mounted");
},

destroyed() {
console.log("cp2 destroyed");
},
template: "<p>{{ name }}</p>"
};

// 指定路由
const routes = [
{
path: "/cp1",
component: cp1
},
{
path: "/cp2",
component: cp2
}
];

// 生成VueRouter实例
const router = new VueRouter({ routes: routes });

// 在vue中加载vuerouter
var app = new Vue({
router: router,
el: "#app",
data: {
message: "Hello Vue!"
}
});


</script>
</html>

如果你在本地运行代码,分别点击两个a标签会发现分别有CP1CP2出现在页面上,并且浏览器的地址栏中url的锚部分也会变成/cp1cp2

upload successful

可以这么说,当你了解了前端路由的原理,你也可以实现一个你的路由框架。只不过VueRouter是一个完备地、稳定地路由框架,所以直接用它就可以了。

想要了解更多有关VueRouter的知识,可以查看 官方文档

2.3 Vue路由使用技巧

前面2.2节,我们演示了最简单的Vue路由功能。而实际上,要实现2.1节中所说的打开带有锚的页面、自动填充筛选项、查询并渲染数据,还是需要一定的技巧。这里,我来总结一下结合Vue的生命周期,如何实现页面的生命周期管理。

upload successful

如图,当用户发送request,打开页面时,vue会回调created方法。在created方法中,调用自己写的init方法,在init方法中,先将路由查询参数填充到自身data中,然后调用自己写的fetchData方法。在自己写的fetchData方法中,用data中的数据作为参数请求后台,拿到数据后填充到自身data中。vue会自动渲染数据,而当vue监听到select/input/click事件后,调用自己写的parameterChanged方法,在该方法中,push一个新的路由,其中参数是用户新筛选的。最后,使用Vue的监听功能,监听route变化,当route变化后,触发init方法。

已上整个流程,将实现2.1节中所说的用户打开带有锚的页面、自动填充筛选项、查询并渲染数据,同时当用户筛选发生变化时,可以及时调整路由(锚)。

以下是一个简单的实现:

在线演示

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
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.1.3/vue-router.js"></script>
</head>

<body>
<div id="app">
<p>{{ message }}</p>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<a @click="$router.push({
path:'/cp1',
query: {p1:1,p2:2,t:+new Date()}
})"
>TO CP1</a>
<a @click="$router.push({
path:'/cp2',
query: {p1:3,p2:4,t:+new Date()}
})"
>TO CP2</a>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</body>

<script type="text/javascript">
// cp1组件
const cp1 = {
data: function() {
return {
name: "CP1"
};
},
watch: {
$route: "init"
},
methods: {
init() {
console.log("cp1.init 在这里获取前端路由参数,并根据路由参数查询后台数据",this.$route.query);
//拿到路由参数,并填充到自身的data中
//调用query方法
this.query();
},
query(){
// 拿到data中的数据,获取后台数据,并填充到自身data中
this.name = "CP1"+JSON.stringify(this.$route.query);
},
},
created() {
console.log("cp1 created");
this.init();
},

destroyed() {
console.log("cp1 destroyed");
},
template: "<p>{{ name }}</p>"
};
// cp2组件
const cp2 = {
data: function() {
return {
name: "CP2"
};
},
watch: {
$route: "init"
},
methods: {
init() {
console.log("cp2.init 在这里获取前端路由参数,并根据路由参数查询后台数据",this.$route.query);
//拿到路由参数,并填充到自身的data中
//调用query方法
this.query()
},
query(){
// 拿到data中的数据,获取后台数据,并填充到自身data中
this.name = "CP2"+JSON.stringify(this.$route.query);
},
},
created() {
console.log("cp2 created");
this.init();
},

destroyed() {
console.log("cp2 destroyed");
},
template: "<p>{{ name }}</p>"
};
// 指定路由
const routes = [
{
path: "/cp1",
component: cp1
},
{
path: "/cp2",
component: cp2
}
];
// 生成VueRouter实例
const router = new VueRouter({ routes: routes });
// 在vue中加载vuerouter
var app = new Vue({
router: router,
el: "#app",
data: {
message: "Hello Vue!"
}
});


</script>
</html>

先点击CP1,再点击CP2的效果:
upload successful

打开带锚url后的效果:

upload successful

总结

Vue和它的插件Vue-Router能够帮助我们快速实现数据渲染和单页开发。不过,对于不熟悉Vue的同学,快速搭建还有有一定难度。希望通过以上的讲解,能够帮助大家更快的入门Vue前端开发。