组件

  • 模板:向外提供特定功能的 js 程序, 一般就是一个 js 文

  • 组件:用来实现局部(特定)功能效果的代码集合(html/css/js/image…..)

  • 模块化:当应用中的 js 都以模块来编写的, 那这个应用就是一个模块化的应用

  • 当应用中的功能都是多组件的方式来编写的, 那这个应用就是一个组件化的应用,。

组件使用的三个步骤:

  1. 创建组件
  2. 注册组件
  3. 使用组件

非单文件组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ahzoo</title>

</head>

<body>
<!-- 提供一个容器 -->
<div id=app>
<!-- 3. 使用组件 -->

全局注册的组件:
<hello></hello>
<br/>
只在app实例注册的组件:
<myschool></myschool>
<br/>
<student></student>
<!-- 写法二:自闭合标签,需要在脚手架环境 -->
<student/>

<h2>----------</h2>

</div>

<!-- 再提供一个容器 -->
<div id=app2>
<!-- 3. 使用组件 -->
全局注册的组件:
<hello></hello>
<br/>
只在app实例注册的组件:
<myschool></myschool>

<h2>----------</h2>

</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script type="text/javascript">

Vue.config.productionTip = false;



// 1. 创建组件(School组件)
const school = Vue.extend({
// el: "#app", //组件定义时,不能写el配置项,因为组件最终都要给一个vm管理,由vm决定组件供谁使用(el配置项需要写在new Vue中)
//组件结构
template: `
<div>
<!--template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点))-->
<h2>学校名称:{{ schoolName }}</h2>
</div>
`,
// 可以使用name指定开发者工具中显示的组件名
name:'School',
data() {
return {
schoolName: '清华大学',
}

}
});
// 1. 创建组件(Student组件)
const student = Vue.extend({
//组件结构
template: `
<div>
<!--template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点))-->
<h2>学生姓名:{{ studentName }}</h2>
<h2>学生年龄:{{ age }}</h2>
</div>
`,
// 可以使用name指定开发者工具中显示的组件名
name:'Student',
data() {
return {
studentName: 'ahzoo',
age: 18
}

}
})

// 1. 创建组件(其它组件)
const hello = Vue.extend({
//组件结构
template: `
<div>
<!--template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点))-->
<h2>学生姓名:{{ message }}</h2>
</div>
`,
// 可以使用name指定开发者工具中显示的组件名
name:'Hello',
data() {
return {
message: 'Hello Vue!'
}

}
})

// 2. 注册组件(全局注册,即所有的VM都能用此组件)
Vue.component('hello', hello)

// 创建VM(vue实例:app2)
var app = new Vue({
el: "#app2",
})

// 创建VM(vue实例:app)
var app = new Vue({
el: "#app",
// 2. 注册组件(局部注册)
components:{
// 格式为 key:value key为最终的组件名,value是组件值;注意key的值不要为大写,因为标签只能为小写
myschool : school,
//也可以不单独设置组件名,直接用创建时的组件名
student

}
})
</script>

</body>

</html>

(与new Vue()的区别:)Vue.extend():使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。并且该组件中的data是一个函数data(),而非一个对象data{}

日常开发中多用 单文件组件

图片

组件的嵌套

图片

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
<body>
<!-- 提供一个容器 -->
<div id=app>


<!-- student组件并未在vm中注册,所以不能写在这里,只能写在注册student的实例(即school)中 -->
<!-- <student></student> -->
<school></school>
<br/>
<b> -----------</b>

</div>


</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script type="text/javascript">

Vue.config.productionTip = false;

// 1. 创建组件(Student组件)
// 使用嵌套组件时,需要提前把被嵌套的组件注册完成,才能被嵌套使用
const student = {
//组件结构
template: `
<div>
<!--template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点))-->
<h2>学生姓名:{{ studentName }}</h2>
<h2>学生年龄:{{ age }}</h2>
</div>
`,
name: 'Student',
data() {
return {
studentName: 'ahzoo',
age: 18
}

}
}

// 1. 创建组件(School组件)
const school = {
//组件结构
template: `
<div>
<!--template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点))-->
<h2>学校名称:{{ schoolName }}</h2>
<br/>
student组件是在school组件中注册的,所以需要写在这里(school组件中):
<student></student>
</div>
`,
name: 'School',
data() {
return {
schoolName: '清华大学',
}

},
components: {
// 将student组件注册到school中
student
}
};



// 创建VM(vue实例:app)
var app = new Vue({
el: "#app",
components: {
school
}
})
</script>

</body>

图片

  • 日常开发中,通常设置一个app组件作为主组件(相当于单文件组件的App.vue):

    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
    <body>
    <!-- 提供一个容器 -->
    <div id=root>

    <!-- 这里只需要写app组件标签即可,或者直接在vm实例中写app标签,就不用在这里写了 -->
    <!-- <app></app> -->

    </div>


    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script type="text/javascript">

    Vue.config.productionTip = false;

    // 1. 创建组件(Student组件)
    // 使用嵌套组件时,需要提前把被嵌套的组件注册完成,才能被嵌套使用
    const student = Vue.extend({
    //组件结构
    template: `
    <div>
    <!--需要一个div标签作为主标签-->
    <h2>学生姓名:{{ studentName }}</h2>
    <h2>学生年龄:{{ age }}</h2>
    </div>
    `,
    name: 'Student',
    data() {
    return {
    studentName: 'ahzoo',
    age: 18
    }

    }
    });

    // 1. 创建组件(School组件)
    const school = Vue.extend({
    //组件结构
    template: `
    <div>
    <h2>学校名称:{{ schoolName }}</h2>
    <br/>
    student组件是在school组件中注册的,所以需要写在这里(school组件中):
    <student></student>
    </div>
    `,
    name: 'School',
    data() {
    return {
    schoolName: '清华大学',
    }

    },
    components: {
    // 将student组件注册到school中
    student
    }
    });

    const hello = Vue.extend({
    template:`<h2> {{ message }} </h2>`,
    data(){
    return{
    message:"Hello Vue!"
    }
    }
    })

    // 一般都会设置一个组件(app组件)作为主组件,统一管理其它子组件
    const app = Vue.extend({
    template: `
    <div>
    <hello></hello>
    <school></school>
    </div>
    `,
    components: {
    hello,
    school
    }
    })


    // 创建VM(vue实例)
    new Vue({
    el: "#root",
    template:`<app></app>`,
    components: {
    // 只需要将主组件注册在vm中即可
    app
    }
    })
    </script>

    </body>

单文件组件

Vue文件的组成:

  1. 模板文件(组件结构)

    1
    2
    3
    <template>
    页面模板
    </template>
  2. JS模板对象(组件交互)

    1
    2
    3
    4
    5
    6
    7
    8
    <script>
    export default {
    data() {return {}},
    methods: {},
    computed: {},
    components: {}
    }
    </script>
  3. 样式(组件样式)

    1
    2
    3
    <style>
    样式定义
    </style>

VSCode默认不识别Vue文件,需要安装Vue插件,推荐安装Vetur

Hello Vue

School.vue(一个标准的Vue组件文件)

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
<template>
<!-- template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点) -->
<div>
<h2> {{ schoolName }} </h2>
<button @click="showThis">点我</button>

</div>

</template>

<script>
//使用export暴露成员(三种方式,一般都是使用默认暴露),供其它模块使用
// @方式一:分别暴露(即每个方法单独暴露)
// export const school = Vue.extend({
//完整写法(Vue.extend()可省略: export const school = Vue.extend({

// @方式三:默认暴露,写法二
export default {

//组件名(name),建议与文件名一致,可省略,但是不建议省略
name:'School',
data(){
return{
schoolName: '清华大学',
}
},
methods:{
showThis(){
//完整写法:
//showThis:funnction(){
alert("Ahzoo")
}
}
}

// @方式二:统一暴露
// export { school, school2, ... }

// @方式三:默认暴露,写法一
// export default school

</script>

<style scoped>
/* 样式标签,用于写CSS样式,可省略 */

</style>

Student.vue(第二个Vue组件文件;由于是单文件组件,所以每个文件代表一个组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<!-- template中必须要有一个标签作为根节点(一般为div标签,且在没有使用Vetur插件时,有且只能由一个根节点) -->


<h2> {{ studentName }}</h2>
<h2> {{ age }}</h2>


</div>
</template>

<script>
export default {
name:'Student',
data(){
return{
studentName: 'ahzoo',
age: 18
}
}
}
</script>

App.vue(用于汇总所有的vue组件)

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
<template>
<div>
<!-- 3. 使用组件 -->
<!-- 需要使用一个div标签作为根元素 -->
<School></School>
<Student></Student>
</div>
</template>

<script>
// 1. 引入所有组件

import School from './School.vue'
import Student from './Student.vue'

export default {
name:'App',
// 2. 注册组件
components:{
School,
Student,
}

}
</script>

main.js (用于注册VM对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//注册VM
import Vue from 'vue'
//引入App.vue
import App from './App.vue'

/*
new Vue({
el:'#app',
// 使用App组件,如果不在这里使用App组件的话就需要在Vue容器中使用
template:'<App></App>',
//注册App组件
components:{App}
})
*/
// 上面为vue1.0的写法,vue 2.0已改为以下写法:
new Vue({
// 使用render直接注册并将App组件放入容器中
render: h => h(App)
}).$mount('#app') // 使用.$mount('#app') 替换el挂载点

index.html (用于充当Vue组件的容器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue组件的容器</title>
</head>
<body>
<!--准备一个容器:-->
<div id="app">

</div>
</body>
</html>

至此,一个简单的Vue项目就写完了,但是并不能直接运行,需要借助Vue脚手架

使用Vue脚手架启动当前项目

将当前Vue单文件组件放在一个Vue脚手架文件中:

School.vue、Student.vue复制在src/components路径下;

App.vue、main.js复制到src路径下;

index.html复制到public路径下

运行vue项目:

1
npm run serve

图片

Vue脚手架

Vue 脚手架(Vue CLI)是 Vue 官方提供的标准化开发工具(开发平台)。

官方文档

安装Vue脚手架

使用命令行终端安装Vue脚手架:

1
$ npm install -g @vue/cli

查看vue是否安装成功:出现版本号即为成功

1
$ vue -V

使用命令行创建

选择路径,创建vue项目

1
$ vue create 项目名

项目名不能包含大写字母,可以用分隔符-

选择创建方式:

vue2、vue3、自定义(上下键切换)

图片

选择npm安装(USE NPM)

图片

安装完成后根据提示运行项目;

图片

1
2
3
$ cd ahzoo

$ npm run sereve

运行成功后,根据提示打开访问地址(http:/localhost:8008),可以看到一个Vue脚手架默认的HelloWorld界面

图片

使用Vue UI创建

在安装完Vue后,使用下面命令,启动Vue UI

1
2
3
4
$ vue ui
🚀 Starting GUI...
🌠 Ready on http://localhost:8000
Auto cleaned 2 projects (folder not found).

命令运行完后会自动打开项目地址(默认为:http://localhost:8000/

在顶部菜单选择创建,选择项目存放路径,点击底部的在此创建新项目

设置项目名(项目名不能为大写,可以使用连接符-),选择包管理器(这里选择的是npm),然后点击下一步

图片

选择预设创建方式,这里选择的是手动(一般直接选默认即可,或者直接使用自己保存的预设);

图片

剩下的配置文件即功能的引入都是可以在创建完成后修改的,不必过于纠结

默认勾选了Choose Vue versionBabelLinter / Formatter

然后可以选择需要的功能,这里勾选了 VuexRouter(路由)以及使用配置文件也勾选上,然后点击下一步;

选择路由是否使用history模式,这里未勾选;Pick a linter / formatter config:**这里选择的是ESLint + Standard config**;然后点击创建项目

图片

提示是否保存为新预设,可自行选择;如果保存为新预设的话,下次创建项目,就可以直接使用该预设;如果需要保存预设的话就直接设置预设名,然后点击保存预设并创建项目,等待项目创建完成即可;

图片

图片

项目创建完成后界面如下:

图片

插件安装

以axios插件为例:

项目创建完成后,点击左侧菜单的插件 –> 添加插件

图片

搜索并安装vue-cli-plugin-axios

图片

等待安装完成后,点击完成安装,等待插件调用;调用完成后,点击继续,完成安装

图片

项目启动

点击左侧菜单的任务,选择service,点击运行,等待项目运行;

然后点击输入,可以看到项目运行地址( http://localhost:8080/ )

图片

项目修改

如果需要修改项目,直接在本地路径修改即可,修改完后重新启动项目

脚手架结构

Vue脚手架生成的默认项目:

图片

结构分析:

图片

通常company文件夹用于存放覆用组件,page文件夹用于存放唯一界面

src/main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import App from './App.vue'
// 下面是一些引入的拓展插件
import './plugins/axios'
import router from './router'
import store from './store'
import './plugins/element.js'

Vue.config.productionTip = false

new Vue({
router,
store,
// 使用render直接注册并将App组件放入容器中
render: h => h(App)
}).$mount('#app') // 使用.$mount('#app') 替换el挂载点

public/index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<!-- 让IE浏览器用最高级别渲染界面 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 开启移动端理想视图窗口 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- BASE_URL指的就是public文件夹路径 -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- 配置网页标题(会自动定位寻找package.json中的配置) -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>

<!-- noscript:如果浏览器不支持JS将显示标签内内容 -->
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

src/App.vue:

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
<template>
<!-- 组件模板 -->

<div id="app">
<img src="./assets/logo.png">
<div>
<p>
If Element is successfully added to this project, you'll see an
<code v-text="'<el-button>'"></code>
below
</p>
<el-button>el-button</el-button>
</div>
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>

<script>
// 组件交互代码

import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'app',
components: {
HelloWorld
}
}
</script>

<style>
/* 组件样式 */
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

修改默认配置

配置文件路径:根目录下的vue.config.js

vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。你也可以使用 package.json 中的 vue 字段,但是注意这种写法需要你严格遵照 JSON 的格式来写。

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
module.exports = {
pages: {
index: {
// page 的入口
entry: 'src/index/main.js',
// 模板来源
template: 'public/index.html',
// 在 dist/index.html 的输出
filename: 'index.html',
// 当使用 title 选项时,
// template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
title: 'Index Page',
// 在这个页面中包含的块,默认情况下会包含
// 提取出来的通用 chunk 和 vendor chunk。
chunks: ['chunk-vendors', 'chunk-common', 'index']
},
// 当使用只有入口的字符串格式时,
// 模板会被推导为 `public/subpage.html`
// 并且如果找不到的话,就回退到 `public/index.html`。
// 输出文件名会被推导为 `subpage.html`。
subpage: 'src/subpage/main.js'
},
// 关闭语法检查
lintOnSave: false,
}

List

todo-list实例

静态界面

src/App.vue:

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
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<TopInput />
<List />
<BottomMenu />
</div>
</div>
</div>
</template>

<script>
import TopInput from "./components/TopInput.vue";
import List from "./components/List.vue";
import BottomMenu from "./components/BottomMenu.vue";

export default {
name: "App",
components: {
TopInput,
List,
BottomMenu,
},
};
</script>

<style>
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>

src/components/TopInput.vue:

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
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
</template>

<script>
export default {};
</script>

<style scoped>

/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}

.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>

src/components/List.vue:

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
<template>
<ul class="todo-main">
<ToDo />
</ul>
</template>

<script>
import ToDo from "./Todo.vue";
export default {
components: {
ToDo,
},
};
</script>

<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}

.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>

src/components/BottomMenu.vue:

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
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span> <span>已完成0</span> / 全部2 </span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>

<script>
export default {};
</script>

<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}

.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}

.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}

.todo-footer button {
float: right;
margin-top: 5px;
}
</style>

src/components/ToDo.vue:

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
<template>
<div>
<li>
<label>
<input type="checkbox" />
<span>xxxxx</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
<li>
<label>
<input type="checkbox" />
<span>yyyy</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</div>
</template>

<script>


export default {
}
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}

li label {
float: left;
cursor: pointer;
}

li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}

li button {
float: right;
display: none;
margin-top: 3px;
}

li:before {
content: initial;
}

li:last-child {
border-bottom: none;
}

</style>


图片

初始化列表

src/components/ToDo.vue:

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
<template>
<div>
<li>
<label>
<input type="checkbox" :checked="todoItem.finish" />
<span>{{ todoItem.title }}</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</div>
</template>

<script>
export default {
// 接收数据
props: ["todoItem"],
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}

li label {
float: left;
cursor: pointer;
}

li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}

li button {
float: right;
display: none;
margin-top: 3px;
}

li:before {
content: initial;
}

li:last-child {
border-bottom: none;
}
</style>


src/components/List.vue:

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
<template>
<ul class="todo-main">
<!-- 使用id作为key , 并将item传递过去-->
<ToDo v-for="item in todos" :key="item.id" :todoItem="item"/>
</ul>
</template>

<script>
import ToDo from "./Todo.vue";
export default {
data(){
return {
todos: [
{ id: '001', title: "吃饭", finish: true },
{ id: '002', title: "睡觉", finish: false },
{ id: '002', title: "打豆豆", finish: false },
]
}
},
components: {
ToDo,
},
};
</script>

<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}

.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>

图片

添加

安装nanoid,用于生成id

1
npm i nanoid

src/App.vue:

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
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 将父模板方法(addTodo)传递给子模板(TopInput) -->
<TopInput :addTodo="addTodo"/>
<!-- 将父模板数据传递给子模板(List) -->
<List :todos="todos"/>
<BottomMenu />
</div>
</div>
</div>
</template>

<script>
import TopInput from "./components/TopInput.vue";
import List from "./components/List.vue";
import BottomMenu from "./components/BottomMenu.vue";

export default {
name: "App",
components: {
TopInput,
List,
BottomMenu,
},
data() {
return {
todos: [
{ id: "001", title: "吃饭", finish: true },
{ id: "002", title: "睡觉", finish: false },
{ id: "003", title: "打豆豆", finish: false },
],
}
},
methods:{
addTodo(addItem){
// 此方法在子模板被调用后,会执行下面的方法
// 在数组左侧添加数据
this.todos.unshift(addItem)
}
}
};
</script>

<style>
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>

src/components/TopInput.vue:

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
<template>
<div class="todo-header">
<input
type="text"
v-model="title"
@keyup.enter="add"
placeholder="请输入你的任务名称,按回车键确认"
/>
</div>
</template>

<script>
// 引入nanoid
import { nanoid } from "nanoid";
export default {
// 接收父模板传递的方法
props: ["addTodo"],
data() {
return {
title: "",
};
},
methods: {
add() {
// 检验数据
if (!this.title) {
return alert("请先输入内容");
}
const inputValue = { id: nanoid(), title: this.title, finish: false };
// 调用父模板方法
this.addTodo(inputValue);
// 添加完后清空输入框
this.title = "";
},
},
};
</script>

<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}

.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(82, 168, 236, 0.6);
}
</style>

src/components/List.vue:

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
<template>
<ul class="todo-main">
<!-- 使用id作为key , 并将item传递过去-->
<ToDo v-for="item in todos" :key="item.id" :todoItem="item" />
</ul>
</template>

<script>
import ToDo from "./Todo.vue";
export default {
props: ["todos"],
components: {
ToDo,
},
};
</script>

<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}

.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>

总结:

将数据和增加数据的方法全放在父模块中,通过父模块将数据和方法传递给需要的子模块

图片

勾选

src/App.vue:

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
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 将父模板方法(addTodo)传递给子模板(TopInput) -->
<TopInput :addTodo="addTodo" />
<!-- 将父模板数据传递给子模板(List)
将checkChange逐层传递给目标模板-->
<List :todos="todos" :checkChange="checkChange"/>
<BottomMenu />
</div>
</div>
</div>
</template>

<script>
import TopInput from "./components/TopInput.vue";
import List from "./components/List.vue";
import BottomMenu from "./components/BottomMenu.vue";

export default {
name: "App",
components: {
TopInput,
List,
BottomMenu,
},
data() {
return {
todos: [
{ id: "001", title: "吃饭", finish: true },
{ id: "002", title: "睡觉", finish: false },
{ id: "003", title: "打豆豆", finish: false },
],
};
},
methods: {
// 添加一个数据
addTodo(addItem) {
console.log(addItem)
// 此方法在子模板被调用后,会执行下面的方法
// 在数组左侧添加数据
this.todos.unshift(addItem);
},
// 修改数据勾选状态
checkChange(id){
this.todos.forEach((todo)=>{
if(todo.id===id){
// 通过id找到目标进行修改
todo.finish = !todo.finish
}
})
console.log(this.todos)
}
},
};
</script>

<style>
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>

src/components/ToDo.vue:

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
<template>
<div>
<li>
<label>
<!-- 将checkChange逐层传递给目标模板 -->
<input
type="checkbox"
:checked="todoItem.finish"
@change="clickCheck(todoItem.id)"
/>
<!-- 这里可以直接使用v-model绑定父模板的选中状态,完成修改,但是这样修改的话,相当于直接修改了props,无法被监测到,所以不建议使用 -->
<!-- <input
type="checkbox"
v-model="todoItem.finish"
/> -->
<span>{{ todoItem.title }}</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</div>
</template>

<script>
export default {
// 接收数据
props: ["todoItem", "checkChange"],
methods: {
clickCheck(id) {
// 调用从父控件传递过来的checkChange方法
this.checkChange(id);
},
},
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}

li label {
float: left;
cursor: pointer;
}

li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}

li button {
float: right;
display: none;
margin-top: 3px;
}

li:before {
content: initial;
}

li:last-child {
border-bottom: none;
}
</style>


src/components/List.vue:

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
<template>
<ul class="todo-main">
<!-- 使用id作为key , 并将item传递过去-->
<ToDo v-for="item in todos" :key="item.id" :todoItem="item" :checkChange="checkChange" />
</ul>
</template>

<script>
import ToDo from "./Todo.vue";
export default {
props: ["todos","checkChange"],
components: {
ToDo,
},
};
</script>

<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}

.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>

删除

src/App.vue:

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
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 将父模板方法(addTodo)传递给子模板(TopInput) -->
<TopInput :addTodo="addTodo" />
<!-- 将父模板数据传递给子模板(List)
将checkChange逐层传递给目标模板-->
<List
:todos="todos"
:checkChange="checkChange"
:deleteTodo="deleteTodo"
/>
<BottomMenu />
</div>
</div>
</div>
</template>

<script>
import TopInput from "./components/TopInput.vue";
import List from "./components/List.vue";
import BottomMenu from "./components/BottomMenu.vue";

export default {
name: "App",
components: {
TopInput,
List,
BottomMenu,
},
data() {
return {
todos: [
{ id: "001", title: "吃饭", finish: true },
{ id: "002", title: "睡觉", finish: false },
{ id: "003", title: "打豆豆", finish: false },
],
};
},
methods: {
// 添加一个数据
addTodo(addItem) {
console.log(addItem);
// 此方法在子模板被调用后,会执行下面的方法
// 在数组左侧添加数据
this.todos.unshift(addItem);
},
// 修改数据勾选状态
checkChange(id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
// 通过id找到目标进行修改
todo.finish = !todo.finish;
}
});
console.log(this.todos);
},
// 删除数据
deleteTodo(id) {
this.todos.shift(id);
console.log("删除数据完成。" + JSON.stringify(this.todos));
},
},
};
</script>

<style>
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>

src/components/ToDo.vue:

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
<template>
<div>
<li>
<label>
<!-- 将checkChange逐层传递给目标模板 -->
<input
type="checkbox"
:checked="todoItem.finish"
@change="clickCheck(todoItem.id)"
/>
<!-- 这里可以直接使用v-model绑定父模板的选中状态,完成修改,但是这样修改的话,相当于直接修改了props,无法被监测到,所以不建议使用 -->
<!-- <input
type="checkbox"
v-model="todoItem.finish"
/> -->
<span>{{ todoItem.title }}</span>
</label>
<button class="btn btn-danger" @click="clickdelete(todoItem.id)">删除</button>
</li>
</div>
</template>

<script>
export default {
// 接收数据
props: ["todoItem", "checkChange","deleteTodo"],
methods: {
clickCheck(id) {
// 调用从父控件传递过来的checkChange方法
this.checkChange(id);
},
clickdelete(id){
this.deleteTodo(id);
}
},
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}

li label {
float: left;
cursor: pointer;
}

li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}

li button {
float: right;
display: none;
margin-top: 3px;
}

li:hover button {
display: block;
}

li:before {
content: initial;
}

li:last-child {
border-bottom: none;
}
</style>


src/components/List.vue:

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
<template>
<ul class="todo-main">
<!-- 使用id作为key , 并将item传递过去-->
<ToDo v-for="item in todos" :key="item.id" :todoItem="item" :checkChange="checkChange" :deleteTodo="deleteTodo"/>
</ul>
</template>

<script>
import ToDo from "./Todo.vue";
export default {
props: ["todos","checkChange","deleteTodo"],
components: {
ToDo,
},
};
</script>

<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}

.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>

图片

底部统计

src/App.vue:

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
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 将父模板方法(addTodo)传递给子模板(TopInput) -->
<TopInput :addTodo="addTodo" />
<!-- 将父模板数据传递给子模板(List)
将checkChange逐层传递给目标模板-->
<List
:todos="todos"
:checkChange="checkChange"
:deleteTodo="deleteTodo"
/>
<BottomMenu :todos="todos"/>
</div>
</div>
</div>
</template>

<script>
import TopInput from "./components/TopInput.vue";
import List from "./components/List.vue";
import BottomMenu from "./components/BottomMenu.vue";

export default {
name: "App",
components: {
TopInput,
List,
BottomMenu,
},
data() {
return {
todos: [
{ id: "001", title: "吃饭", finish: true },
{ id: "002", title: "睡觉", finish: false },
{ id: "003", title: "打豆豆", finish: false },
],
};
},
methods: {
// 添加一个数据
addTodo(addItem) {
console.log(addItem);
// 此方法在子模板被调用后,会执行下面的方法
// 在数组左侧添加数据
this.todos.unshift(addItem);
},
// 修改数据勾选状态
checkChange(id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
// 通过id找到目标进行修改
todo.finish = !todo.finish;
}
});
console.log(this.todos);
},
// 删除数据
deleteTodo(id) {
this.todos.shift(id);
console.log("删除数据完成。" + JSON.stringify(this.todos));
},

},
};
</script>

<style>
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>

src/components/BottomMenu.vue:

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
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span>
<span>已完成{{ finishTodos }}</span> / 全部{{ todos.length }}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>

<script>
export default {
props: ["todos"],
computed: {
finishTodos() {
// 使用reduce进行条件统计,设置初始值为0
// per表示上次(执行reduce方法返回)的值,current表示当前的(遍历的todos的当前的数据)值
return this.todos.reduce((pre, current) => {
// 判断当前数据的finishTodos是否为true,为true则对结果进行加1,否则加0
// 最后结果就是finishTodos为true的数量(数据勾选完成的数量)
return pre + (current.finish ? 1 : 0);
}, 0);
},
},
};
</script>

<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}

.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}

.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}

.todo-footer button {
float: right;
margin-top: 5px;
}
</style>

图片

底部交互

src/App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 将父模板方法(addTodo)传递给子模板(TopInput) -->
<TopInput :addTodo="addTodo" />
<!-- 将父模板数据传递给子模板(List)
将checkChange逐层传递给目标模板-->
<List
:todos="todos"
:checkChange="checkChange"
:deleteTodo="deleteTodo"
/>
<BottomMenu :todos="todos" :checkAll="checkAll" :clearAll="clearAll" />
</div>
</div>
</div>
</template>

<script>
import TopInput from "./components/TopInput.vue";
import List from "./components/List.vue";
import BottomMenu from "./components/BottomMenu.vue";

export default {
name: "App",
components: {
TopInput,
List,
BottomMenu,
},
data() {
return {
todos: [
{ id: "001", title: "吃饭", finish: true },
{ id: "002", title: "睡觉", finish: false },
{ id: "003", title: "打豆豆", finish: false },
],
};
},
methods: {
// 添加一个数据
addTodo(addItem) {
console.log(addItem);
// 此方法在子模板被调用后,会执行下面的方法
// 在数组左侧添加数据
this.todos.unshift(addItem);
},
// 修改数据勾选状态
checkChange(id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
// 通过id找到目标进行修改
todo.finish = !todo.finish;
}
});
console.log(this.todos);
},
// 删除数据
deleteTodo(id) {
this.todos.shift(id);
console.log("删除数据完成。" + JSON.stringify(this.todos));
},
// (取消)全选
checkAll(finish) {
this.todos.forEach((todo) => {
todo.finish = finish;
});
},
// 清除已完成
clearAll() {
this.todos = this.todos.filter((todo) => {
return !todo.finish;
});
},
},
};
</script>

<style>
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>

src/components/BottomMenu.vue:

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
<template>
<div class="todo-footer">
<label>
<input type="checkbox" :checked="isAll" @change="clickAll" />
<!-- 这里可以直接使用v-model绑定选中状态(之前不推荐使用v-model是因为那个是修改prop的值,而这个则不是),但是需要在计算属性中写set方法 -->
<!-- <input type="checkbox" v-model="isAll"/> -->
</label>
<span>
<span>已完成{{ finishTodos }}</span> / 全部{{ todos.length }}
</span>
<button class="btn btn-danger" @click="clickClear">清除已完成任务</button>
</div>
</template>

<script>
export default {
props: ["todos", "checkAll", "clearAll"],
computed: {
// 已完成数据量
finishTodos() {
// 使用reduce进行条件统计,设置初始值为0
// per表示上次(执行reduce方法返回)的值,current表示当前的(遍历的todos的当前的数据)值
return this.todos.reduce((pre, current) => {
// 判断当前数据的finishTodos是否为true,为true则对结果进行加1,否则加0
// 最后结果就是finishTodos为true的数量(数据勾选完成的数量)
return pre + (current.finish ? 1 : 0);
}, 0);
},
// 总数量
total() {
return this.todos.length;
},
isAll() {
// 判断已完成数量和总数量
// 计算属性可以对其它的计算属性进行计算
return this.finishTodos === this.total;
},
},
methods: {
clickAll(event) {
// 这里直接通过event获取当前Dom的数值
this.checkAll(event.target.checked);
},
clickClear() {
this.clearAll();
},
},
};
</script>

<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}

.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}

.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}

.todo-footer button {
float: right;
margin-top: 5px;
}
</style>

图片

代理

跨域:两个请求地址的协议(HTTP/HTTPS)、域名(IP)、端口号(Port)不同,即为跨域;

由于浏览器的同源策略限制,跨域是无法进行数据交换的

代理概念(举例):前端8080端口应用,通过ajax向8080端口的代理服务器请求数据,然后代理服务器再向7777端口的服务器请求数据(同一个服务器之间是不存在跨域的,所以8080端口的代理服务器可以向7777端口的服务器请求数据)

注意:如果请求的数据,在本地存在,就不会通过代理请求。比如我们请求一个public路径下的vue.png资源数据,此时就并不会通过代理服务器,而是直接访问本地的资源数据

新建一个vue脚手架项目;

在该项目中安装axios插件

1
$ npm i axios

启动服务器

准备一个可以进行ajax请求获取数据的服务器;

或者直接使用这个简易的node服务器(提取码:vue2)

运行命令启动node服务器:

1
2
3
$ node server1
服务器1启动成功,请求地址为:http://localhost:7777/ahzoo

图片

配置方式一

配置简单,但不能配置多个代理。

根目录vue.config.js中配置代理:

1
2
3
4
5
6
7
8
9
module.exports = {
// 关闭语法检查
lintOnSave: false,
// 开启代理服务器
devServer: {
// 设置代理服务器地址
proxy: 'http://localhost:7777'
}
}

发送请求:

src/App.vue

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
<template>
<div id="app">
<button @click="searchInfo"> 查询数据 </button>

</div>
</template>

<script>
import axios from 'axios'
export default {

name: 'App',
methods:{
searchInfo(){
// 向代理服务器(8080)请求数据,而不是直接向后端服务器(7777)请求数据
axios.get('http://localhost:8080/ahzoo').then(
response => {
// 获取响应对象response中的数据
console.log("请求成功",response.data)
},
error => {
console.log("请求失败", error.message)
}
)
}
}
}
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

配置方式二(配置多个代理)

可以配置多个代理;

启动第二个服务器:

1
2
3
$ node server2
服务器2启动成功,请求地址为:http://localhost:9999/phone

图片

根目录vue.config.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
module.exports = {
// 关闭语法检查
lintOnSave: false,
// 开启代理服务器 方式一
// devServer: {
// // 设置代理服务器地址
// proxy: 'http://localhost:7777'
// }

// 开启代理服务器 方式二 可以设置多个代理
devServer: {
proxy: {
// '/api' 表示请求前缀为 `/api` 时,就进行下面的代理
'/api': {
target: 'http://localhost:7777',
// 重写请求地址,将所有 `/api` 路径进行正则匹配,替换为空字符,再发送给后端(因为后端的请求地址是 `/ahzoo` 而不是 `/api/ahzoo`
pathRewrite:{'^/api':''},
// ws: true, // 用于支持websocket
// changeOrigin: true // 默认为true。改变真实的请求来源,设为true时,代理服务器将不会把自己的真实请求地址告诉被请求的服务器;反之
},
'/phone': {
target: 'http://localhost:9999'
},
// 用 “|” 表示 “或” 关系:
'/api/t1|/api/t2': {
target: 'http://localhost:9999'
}
}
}
}

发送请求:

src/App.vue

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
<template>
<div id="app">
<button @click="searchInfo"> 查询数据 </button>

<button @click="searchPhoneInfo"> 查询手机数据 </button>
</div>
</template>

<script>
import axios from 'axios'
export default {

name: 'App',
methods:{
searchInfo(){
// 向代理服务器(8080)请求数据,而不是直接向后端服务器(7777)请求数据
axios.get('http://localhost:8080/api/ahzoo').then(
response => {
// 获取响应对象response中的数据
console.log("请求成功",response.data)
},
error => {
console.log("请求失败", error.message)
}
)
},
searchPhoneInfo(){
// 向代理服务器(8080)请求数据,而不是直接向后端服务器(7777)请求数据
axios.get('http://localhost:8080/phone').then(
response => {
// 获取响应对象response中的数据
console.log("请求成功",response.data)
},
error => {
console.log("请求失败", error.message)
}
)
}
}
}
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

图片

插槽

默认插槽

App.vue:

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
<template>
<div class="app">
<Category title="水果" :listData="fruits">
<!-- 此处的img标签将会填充至Category组件的插槽中,如果该组件没有插槽,将不会显示 -->
<img src="https://www.baidu.com/favicon.ico" />
</Category>
<Category title="游戏">
<li v-for="(item, index) in game" :key="index">{{ item }}</li>
</Category>
<Category title="其它" :listData="game" />
</div>
</template>

<script>
import Category from "./components/Category.vue";
export default {
name: "App",
components: {
Category,
},
data() {
return {
fruits: ["苹果", "西瓜"],
game: ["超级玛丽", "拳皇"],
};
},
};
</script>
<style scoped>
.app {
display: flex;
}
.category {
background: beige;
width: 30%;
margin: 10px;
text-align: center;
}
</style>



Category.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="category">
<h3>{{title}}</h3>
<!-- 定义一个插槽,用于使用Category组件时进行填充 -->
<slot>这里可以设置默认值,当使用Category组件时并未传递具体结构,这里的内容就会出现</slot>
</div>
</template>

<script>
export default {
name: "Category",
props: ["title"]
};
</script>

<style>
</style>

图片

具名插槽

就是给每个插槽自定义名称。一个不带 name<slot> 出口会带有隐含的名字“default”。

Category.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="category">
<h3>{{title}}</h3>
<!-- 定义一个插槽,用于使用Category组件时进行填充 -->
<!-- 为插槽设置名字 -->
<slot name="first">1.这里可以设置默认值,当使用Category组件时并未传递具体结构,这里的内容就会出现</slot><br/><br/>
<slot name="second">2.这里可以设置默认值,当使用Category组件时并未传递具体结构,这里的内容就会出现</slot>

</div>
</template>

<script>
export default {
name: "Category",
props: ["title"]
};
</script>

<style>
</style>

App.vue :

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
<template>
<div class="app">
<Category title="网页">
<!-- 此处的img标签将会填充至Category组件的插槽中,如果该组件没有插槽,将不会显示 -->
<!-- 指定插槽的名字 -->
<img slot="first" src="https://www.baidu.com/favicon.ico" />
<a slot="second" href="http://ahzoo.cn">点我跳转主页</a><br />
</Category>
<Category title="游戏">
<li slot="second" v-for="(item, index) in game" :key="index">
{{ item }}
</li>
<!-- 直接使用template指定插槽(并且template标签中可以直接使用v-slot写法),就无需在每个控件都指定插槽了 -->
<template v-slot:second>
<a href="https://store.steampowered.com/">点我跳转Stream</a><br />
<a href="https://www.wegame.com.cn/">点我跳转WeGame</a><br />
</template>
</Category>
<Category title="其它" />
</div>
</template>

<script>
import Category from "./components/Category.vue";
export default {
name: "App",
components: {
Category,
},
data() {
return {
game: ["超级玛丽", "拳皇"],
};
},
};
</script>
<style scoped>
.app {
display: flex;
}
.category {
background: beige;
width: 30%;
margin: 10px;
text-align: center;
padding: 10px;
}
</style>

图片

作用域插槽

即数据有插槽提供者决定,数据的结构(样式)由使用者决定

App.vue:

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
<template>
<div class="app">
<Category title="游戏">
<!-- 由于在插槽中绑定了数据,这里就可以使用template标签包裹,并且指定slot-scope用于接收插槽中绑定的数据,scope名可自定义 -->
<template slot-scope="ahzoo">
<!-- 从接收的数据中获取插槽绑定的数据 -->
{{ ahzoo.msg }}
<ul>
<!-- 从接收的数据中获取插槽绑定的数据 -->
<li v-for="(item, index) in ahzoo.game" :key="index">
{{ item }}
</li>
</ul>
</template>
</Category>
<Category title="游戏">
<!-- slot-scope接收数据时还可以使用结构赋值[其实就是key等于value时(slot-scope不使用自定义名,与接收的参数一样,都为game),可以直接使用value] -->
<template slot-scope="{game}">
<ol>
<li v-for="(item, index) in game" :key="index">
{{ item }}
</li>
</ol>
</template>
</Category>
<Category title="游戏">
<template slot-scope="ahzoo">
<h4>
<li v-for="(item, index) in ahzoo.game" :key="index">
{{ item }}
</li>
</h4>
</template>
</Category>
</div>
</template>

<script>
import Category from "./components/Category.vue";
export default {
name: "App",
components: {
Category,
},
};
</script>
<style scoped>
.app {
display: flex;
}
.category {
background: beige;
width: 30%;
margin: 10px;
text-align: center;
padding: 10px;
}
</style>

Category.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="category">
<h3>{{ title }}</h3>
<!-- 定义一个插槽,用于使用Category组件时进行填充 -->
<!-- 在插槽中绑定数据(可绑定多个数据),当插槽内容被填充时,插槽中绑定的数据就会被传递过去 -->
<slot :game="game" :msg="message">这里是默认内容</slot><br /><br />
</div>
</template>

<script>
export default {
name: "Category",
props: ["title"],
data() {
return {
message: "ahzoo",
game: ["超级玛丽", "拳皇", "扫雷"],
};
},
};
</script>

<style>
</style>

图片

Vuex

图片

Vuex专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信

Vue的使用场景:

  1. 多个组件依赖于同一状态
  2. 来自不同组件的行为需要变更同一状态

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式 (opens new window)就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的**状态 (state)**。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

Vuex的安装与使用

创建一个新的Vue项目;

在Vue项目中运行命令:

1
$ npm i vuex

src/main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from 'vue'
import App from './App.vue'

// 引入store
// import Store from './store/index.js'
//index.js可省略
import store from './store'


Vue.config.productionTip = false


new Vue({
// 注册store:
//key和value相等,直接简写
// store:'store',
store,
render: h => h(App),
}).$mount('#app')

src路径创建一个store文件夹,作为vuex的仓库;并在该文件夹下创建一个index.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
// 该文件用于创建vuex中的核心store

//引入Vue
import Vue from 'vue'
// 引入vuex
import Vuex from 'vuex'

// 使用vuex(必须要在这里使用,因为store中用到了vuex,如果不在这里使用vuex的话,导入store时(会自动执行这个文件)就会找不到vuex)
Vue.use(Vuex)

// 准备actions,用于响应组件中的动作
const actions ={}

// 准备mutations,用于操作数据(state)
const mutations ={}

// 准备state,用于存储数据
const state = {}

// 创建并暴露store实例
export default new Vuex.Store({
// const store = new Vuex.Store({

//这里同样是key:value形式
actions:actions,
//由于key和value相同,所以可以省略
mutations,
state

})

// 暴露store实例
// export default store;

实例:

数字的加减

src/store/index.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
62
// 该文件用于创建vuex中的核心store
//引入Vue
import Vue from 'vue'
// 引入vuex
import Vuex from 'vuex'

// 使用vuex(必须要在这里使用,因为store中用到了vuex,如果不在这里使用vuex的话,导入store时(会自动执行这个文件)就会找不到vuex)
Vue.use(Vuex)

// 准备actions,用于响应组件中的动作
const actions = {
// addNum(context, value) {
// // 由于这里并没有什么业务逻辑,而是直接提交给mutations方法,所以可以省略,直接在根方法中提交给mutations方法
// context.commit('ADDNUM', value)
// },
subNum(context, value) {

console.log(context.state.sumNum)
// 这里模拟一个简单的业务逻辑:数字大于0时,进行减法运算
if (context.state.sumNum > 0) {
context.commit('SUBNUM', value)
}
}
}

// 准备mutations,用于操作数据(state)
const mutations = {
//这里推荐可以直接大写addNum
ADDNUM(state, value) {
//在这里进行数字的操作
state.sumNum += value
},
SUBNUM(state, value) {
// 设置500毫秒后执行
setTimeout(() => {
//在这里进行数字的操作
state.sumNum -= value
}, 500)
}

}

// 准备state,用于存储数据
const state = {
//当前数值的值
sumNum: 0
}
// 创建并暴露store实例
export default new Vuex.Store({
// const store = new Vuex.Store({

//这里同样是key:value形式
actions: actions,
//由于key和value相同,所以可以省略
mutations,
state

})

// 暴露store实例
// export default store;

src/App.vue:

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
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js App" />
</div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue";

export default {
name: "App",
components: {
HelloWorld,
},
mounted() {
console.log("App", this);
},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

src/main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from 'vue'
import App from './App.vue'

// 引入store
// import Store from './store/index.js'
//index.js可省略
import store from './store'


Vue.config.productionTip = false


new Vue({
// 注册store:
//key和value相等,直接简写
// store:'store',
store,
render: h => h(App),
}).$mount('#app')

src/components/HelloWorld.vue:

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
<template>
<div class="hello">
<button @click="increment">+</button>
{{ $store.state.sumNum }}
<button @click="decrement">-</button>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data(){
return{
thatNum:3,
sNum:1
}
},
methods:{
increment(){
// 如果目标dispatch方法中并没有业务逻辑,而是直接提交给了mutations方法(就比如这里的addNum方法),那么就可以不用dispatch方法做中转提交了,而是直接提交给mutations方法
// this.$store.dispatch('addNum', this.thatNum)
// 直接将跳过dispatch方法中的addNum方法,提交给了mutations方法
this.$store.commit('ADDNUM', this.thatNum)
},
decrement(){
this.$store.dispatch("subNum",this.sNum)
}
},
// 钩子(挂载)函数,和el效果相同,这里只是做了个打印效果,所以可省略
mounted(){
console.log('HelloWorld',this)
}
}
</script>

图片

Vuex开发者工具

浏览器打开Vue开发者工具(官方的Vue开发者工具已经内置了Vuex开发者工具);

打开Vue开发者工具后,点击第二个功能菜单,即可进入到Vuex界面;

在这里可以看到具体的Vuex操作时间及操作数据;

鼠标放在历史操作上可以看到三个功能(左上角的三个相同功能为批量操作),从左到右依次为:1.(Commit)提交此次操作 (注意:此操作会将之前的操作全部合并为一个操作)2.(Revert)取消此次操作(注意:此操作会把该操作后面的操作也取消) 3.(Time Travel)回退到此次操作;

底部区域右上角为导入/导出操作;

图片

State与Getter

在src/store/index.js中准备并配置getters:

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
// 该文件用于创建vuex中的核心store
//引入Vue
import Vue from 'vue'
// 引入vuex
import Vuex from 'vuex'

// 使用vuex(必须要在这里使用,因为store中用到了vuex,如果不在这里使用vuex的话,导入store时(会自动执行这个文件)就会找不到vuex)
Vue.use(Vuex)

// 准备actions,用于响应组件中的动作
const actions = {
// addNum(context, value) {
// // 由于这里并没有什么业务逻辑,而是直接提交给mutations方法,所以可以省略,直接在根方法中提交给mutations方法
// context.commit('ADDNUM', value)
// },
subNum(context, value) {

console.log(context.state.sumNum)
// 这里模拟一个简单的业务逻辑:数字大于0时,进行减法运算
if (context.state.sumNum > 0) {
context.commit('SUBNUM', value)
}
}
}

// 准备mutations,用于操作数据(state)
const mutations = {
//这里推荐可以直接大写addNum
ADDNUM(state, value) {
//在这里进行数字的操作
state.sumNum += value
},
SUBNUM(state, value) {
// 设置500毫秒后执行
setTimeout(() => {
//在这里进行数字的操作
state.sumNum -= value
}, 500)
}

}

// 准备state,用于存储数据
const state = {
//当前数值的值
sumNum: 0
}

// 准备getters,用于将state中的数据进行加工
const getters = {
bigSum(state){
return state.sumNum * 10
}
}


// 创建并暴露store实例
export default new Vuex.Store({
actions,
mutations,
state,
//配置getters
getters
})

// 暴露store实例
// export default store;

在src/components/HelloWorld.vue中获取数据:

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
<template>
<div class="hello">
<button @click="increment">+</button>
{{ $store.state.sumNum }}
<button @click="decrement">-</button>
<br/>
<!-- 获取getters中的数据 -->
*10:
{{ $store.getters.bigSum }}
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data(){
return{
thatNum:3,
sNum:1
}
},
methods:{
increment(){
// 如果目标dispatch方法中并没有业务逻辑,而是直接提交给了mutations方法(就比如这里的addNum方法),那么就可以不用dispatch方法做中转提交了,而是直接提交给mutations方法
// this.$store.dispatch('addNum', this.thatNum)
// 直接将跳过dispatch方法中的addNum方法,提交给了mutations方法
this.$store.commit('ADDNUM', this.thatNum)
},
decrement(){
this.$store.dispatch("subNum",this.sNum)
}
},
// 钩子(挂载)函数,和el效果相同;可以获得组件相关的信息这里只是做了个打印效果,所以可省略
mounted(){
console.log('HelloWorld',this)
}
}
</script>

图片

MapState 和 MapGetters

在上一步基础上修改。

src/store/index.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
// 该文件用于创建vuex中的核心store
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 准备actions,用于响应组件中的动作
const actions = {
subNum(context, value) {
console.log(context.state.sumNum)
if (context.state.sumNum > 0) {
context.commit('SUBNUM', value)
}
}
}

// 准备mutations,用于操作数据(state)
const mutations = {
ADDNUM(state, value) {
state.sumNum += value
},
SUBNUM(state, value) {
setTimeout(() => {
state.sumNum -= value
}, 500)
}

}

// 准备state,用于存储数据
const state = {
//当前数值的值
sumNum: 0,
name: 'ahzoo',
id: 999
}

// 准备getters,用于将state中的数据进行加工
const getters = {
bigSum(state){
return state.sumNum * 10
}
}


// 创建并暴露store实例
export default new Vuex.Store({
actions,
mutations,
state,
getters
})


在src/components/HelloWorld.vue:

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
<template>
<div class="hello">
<h3>
<!-- 对应写法一、二 -->
<!-- {{ myId }}<br />
{{ myName }}<br /> -->

<!-- 对应写法三 -->
{{ id }}<br />
{{ name }}<br />
</h3>
<button @click="increment">+</button>
{{ sumNum }}
<button @click="decrement">-</button>
<br />
*10:
{{ bigSum }}
</div>
</template>

<script>
import { mapState,mapGetters } from "vuex";

export default {
name: "HelloWorld",
data() {
return {
thatNum: 3,
sNum: 1
};
},
computed: {
// mapState写法一:
// sum(){
// return this.$store.state.sumNum
// },
// myId(){
// return this.$store.state.id
// },
// myName(){
// return this.$store.state.name
// },

// 借助mapState生成计算属性,从state中读取数据( ... 三个点表示把对象(mapState)中的数据取出来依次展开)
// mapState写法二:
// ...mapState({ sum: 'sumNum', myId: 'id', myName: 'name' }),
// mapState写法三(数组写法):
...mapState(['sumNum', 'id', 'name']),

// big() {
// return this.$store.getters.bigSum;
// },
// mapGetters与mapState的三种写法是一致的,这里直接举例第三种写法,另外两种写法参考mapState
...mapGetters(['bigSum'])
},
methods: {
increment() {
this.$store.commit("ADDNUM", this.thatNum);
},
decrement() {
this.$store.dispatch("subNum", this.sNum);
},
},
// 钩子(挂载)函数,和el效果相同
mounted() {
// mapState写法一:
// mapState({sum:'sumNum',myId:'id',name:'name'})
},
};
</script>



图片

MapActions 和 MapMutations

在上一步基础上修改。

src/components/HelloWorld.vue:

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
<template>
<div class="hello">
<h3>
{{ id }}<br />
{{ name }}<br />
</h3>
<!-- 因为使用了mapMutations,没有指定需要操作的属性,所以需要在这里指定(thatNum) -->
<button @click="increment(thatNum)">+</button>
{{ sumNum }}
<button @click="decrement(sNum)">-</button>
<button @click="positiveDecrement(sNum)"> - (>0) </button>
<br />

<!-- 对应写法三的数组写法,因为此时没有increment和decrement方法,只有ADDNUM和subNum,
当然也可以选择修改mutations里的方法为increment和decrement,只有最终能够保持一致就行 -->
<!-- <button @click="ADDNUM(thatNum)">+</button>
{{ sumNum }}
<button @click="subNum(sNum)">-</button>
<br /> -->

*10:
{{ bigSum }}
</div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";

export default {
name: "HelloWorld",
data() {
return {
thatNum: 3,
sNum: 1,
};
},
computed: {
...mapState(["sumNum", "id", "name"]),
...mapGetters(["bigSum"]),
},
methods: {
// 普通写法
// increment() {
// this.$store.commit("ADDNUM", this.thatNum);
// },
// decrement() {
// this.$store.dispatch("subNum", this.sNum);
// },

// mapMutations同样也是和mapState的三种写法是一致的,这里直接举例第二种对象写法和第三种数组写法
// 借助mapMutations方法会调用commit联系mutations
...mapMutations({ increment: 'ADDNUM', decrement: 'SUBNUM' }),
// ...mapMutations(['ADDNUM','SUBNUM'])

// mapActions同样也是和mapState的三种写法是一致的,这里直接举例第二种对象写法和第三种数组写法
// 借助mapActions方法会调用dispatch联系actions
...mapActions({positiveDecrement:'subNum'}),
// ...mapActions(['subNum'])


},
// 钩子(挂载)函数,和el效果相同
mounted() {
// mapState写法一:
// mapState({sum:'sumNum',myId:'id',name:'name'})
},
};
</script>

MapActions 和 MapMutations使用时,如果需要传递参数,需要在模板绑定事件时就指定传递参数(比如上例的increment方法,就需要指定传递的参数为thatNum)

图片

多组件共享数据

同样在上一步基础修改。

其实就是利用state和getter中的数据是共享的原理。

src/components/Person.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
---------------------------------------------------------------<br />
{{ sumNum }}<br />
id:{{ id }} <br />
最大值是:{{ bigSum }}
</div>
</template>

<script>
import { mapState, mapGetters } from "vuex";

export default {
computed: {
...mapState(["sumNum", "id", "name"]),
...mapGetters(["bigSum"]),
},
};
</script>

<style>
</style>

src/App.vue:

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
<template>
<div id="app">
<HelloWorld/>
<Person/>
</div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue";
import Person from "./components/Person.vue"

export default {
name: "App",
components: {
HelloWorld,
Person
},
mounted() {
console.log("App", this);
},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

路由(Route)

  • 一个路由就是一组映射关系(key - value
  • key 为路径, value 则是 functioncomponent

路由分类:

  • 后端路由:
    • value 是 function, 用于处理客户端提交的请求。
    • 工作过程:服务器接收到一个请求时, 根据请求路径找到匹配的函数来处理请求, 返回响应数据。
  • 前端路由:
    • value 是 component,用于展示页面内容。
    • 工作过程:当浏览器的路径改变时, 对应的组件就会显示。

vue-router:vue 的一个插件库,专门用来实现 SPA 应用

SPA应用:

  1. 单页 Web 应用(single page web application,SPA)
  2. 整个应用只有一个完整的页面
  3. 点击页面中的导航链接不会刷新页面,只会做页面的局部更新
  4. 数据需要通过 ajax

简单使用

创建一个新的Vue项目;

安装路由插件

1
$ npm i vue-router

安装完后引入;

src/main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import App from './App.vue'
// 引入VueRouter
import VueRouter from 'vue-router'
// 引入路由器
import router from './router'

Vue.config.productionTip = false
// 使用VueRouter
Vue.use(VueRouter)

new Vue({
render: h => h(App),
// 路由配置
router
}).$mount('#app')

src/router路径下创建index.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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About.vue'
import Hello from '../components/HelloWorld.vue'


// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About
},
{
path: '/hello',
component: Hello
}
]
})

src/App.vue:

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
<template>
<div id="app">
<div class="menu">

<!-- 使用router-link标签,实现路由的切换 -->
<router-link class="list-group-item" active-class="active" to="/about">关于</router-link>

<router-link class="list-group-item" active-class="active" to="/hello">主页</router-link>

</div>
<div>
<!-- 使用路由视图标签,指定组件的显示位置 -->
主视图
<router-view></router-view>
</div>
</div>
</template>

<script>

export default {
name: "App",
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.menu {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
float: left;
width:30%
}
.list-group-item {
position: relative;
display: block;
padding: 10px 15px;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid #ddd;
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
z-index: 2;
color: #fff;
background-color: #337ab7;
border-color: #337ab7;
}
</style>

src/components/About.vue:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<h2>这是about界面</h2>
</div>
</template>

<script>
export default {
name:'About'
}
</script>

HelloWorld.vue:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="hello">
<h2> hello</h2>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
}
</script>

图片

路由组件一般放在pages路径,非路由组件放在components路径

嵌套(多级)路由

直接在上个项目基础上修改;

配置多级路由规则:

src/router/index.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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About'
import Hello from '../components/HelloWorld'
import Tag from '../pages/Tag'
import Category from '../pages/Category'


// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About,

},
{
path: '/hello',
component: Hello,
// 配置子路由
children: [
{
// 子路由路径不需要加 `/`
path: 'tag',
component: Tag
},
{
path: 'category',
component: Category
}
]
},
]
})

src/pages/Tag.vue:

1
2
3
4
5
6
7
8
9
10
<template>
<h2>标签页</h2>
</template>

<script>
export default {

}
</script>

src/pages/Category.vue:

1
2
3
4
5
6
7
8
9
10
<template>
<h2>分类页</h2>
</template>

<script>
export default {

}
</script>

使用多级路由:

src/components/HelloWorld.vue:

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
<template>
<div class="hello">
<div class="nav-tabs">
<!-- 子路由路径需要带上父路径-->
<router-link class="list-group-item" to="/hello/tag" active-class="active"
>标签</router-link
>
<router-link class="list-group-item" to="/hello/category" active-class="active"
>分类</router-link
>
</div>

<h2>hello界面</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
name: "HelloWorld",
};
</script>
<style scoped>
.nav-tabs {
border-bottom: 1px solid #ddd;
}
.nav-tabs > li {
float: left;
margin-bottom: -1px;
}
.hello {
float: right;
width: 50%;
margin: 30px;
background-color: bisque;
}
</style>

图片

参数传递

除了vuex的参数传递,我们还可以使用更简便的路由传递参数(之前父子组件间的参数传递,默认就是路由的query传递方式)。

query

query传递参数的方式类似于HTTP中的Get方法传递,参数会拼接到网址上

src/pages/Tag.vue中传递参数:

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
<template>
<div>
<h2>标签页</h2>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">

<!-- 写法一: 跳转路由并携带query参数,写死 -->
<!-- 在路径后面添加 ` ? ` ,然后携带需要传递的参数 -->
<!-- <router-link to="/hello/tag/detail?id=999&title=ahzoo">{{ item.title }}</router-link> -->

<!-- 写法二: 跳转路由并携带query参数,:to的字符串写法 -->
<!-- 使用v-bind将后面的解析为表达式,同时使用模板代码“ ` ” 其转为模板,并使用模板语法(${item.id}),获取数据 -->
<!-- <router-link v-bind:to="`/hello/tag/detail?id=${item.id}&title=${item.title}`">{{ item.title }}</router-link> -->

<!-- 写法三: 跳转路由并携带query参数,:to的对象写法 -->
<!-- 使用v-bind将后面的解析为表达式,同时使用模板代码“ ` ” 其转为模板,并使用模板语法(${item.id}),获取数据 -->
<router-link v-bind:to="{
path:'/hello/tag/detail',
query:{
id:item.id,
title:item.title
}
}">
{{ item.title }}
</router-link>


</li>
</ul>
<br/>
<h2> 标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data(){
return{
tagList:[
{id:'001',title:"标签1"},
{id:'002',title:"标签2"},
{id:'003',title:"标签3"},
]
}
}
}
</script>

src/pages/Detail.vue界面接收参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<ul>
<li> 标签ID:{{ $route.query.id }}</li>
<li> 标签名:{{ $route.query.title }}</li>
</ul>
</template>

<script>
export default {
name:'Detail',
// 钩子(挂载)函数,这里只是用于打印调试数据,可省略
mounted(){
console.log(this.$route)
}

}
</script>

src/router/index.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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About'
import Hello from '../components/HelloWorld'
import Tag from '../pages/Tag'
import Category from '../pages/Category'
import DetailPage from '../pages/Datail.vue'

// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About,

},
{
path: '/hello',
component: Hello,
// 配置子路由
children: [
{
// 子路由路径不需要加 `/`
path: 'tag',
component: Tag,
children:[
{
path:'detail',
component: DetailPage
}
]
},
{
path: 'category',
component: Category
}
]
},
]
})

图片

命名路由

打开src/router/index.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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About'
import Hello from '../components/HelloWorld'
import Tag from '../pages/Tag'
import Category from '../pages/Category'
import DetailPage from '../pages/Datail.vue'

// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// 对路由器进行命名
name:'About',
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About,

},
{
path: '/hello',
component: Hello,
// 配置子路由
children: [
{
// 子路由路径不需要加 `/`
path: 'tag',
component: Tag,
children:[
{
// 对路由器进行命名
name:'tagDetail',
path:'detail',
component: DetailPage
}
]
},
{
path: 'category',
component: Category
}
]
},
]
})

修改参数传递的代码,就不用再写路径了,可以直接调用路由名称;

src/pages/Tag.vue

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
<template>
<div>
<h2>标签页</h2>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">
<router-link
v-bind:to="{
// 直接通过路由器名称,调用路由器(注意:如果对路由进行了命名,那就只能使用name调用,不能使用path调用)
name: 'tagDetail',
query: {
id: item.id,
title: item.title,
},
}"
>
{{ item.title }}
</router-link>
</li>
</ul>
<br />
<h2>标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data() {
return {
tagList: [
{ id: "001", title: "标签1" },
{ id: "002", title: "标签2" },
{ id: "003", title: "标签3" },
],
};
},
};
</script>

图片

params

params传递参数的方式类似于HTTP中的Post方法传递

打开src/router/index.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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About'
import Hello from '../components/HelloWorld'
import Tag from '../pages/Tag'
import Category from '../pages/Category'
import DetailPage from '../pages/Datail.vue'

// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// 对路由器进行命名
name:'About',
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About,

},
{
path: '/hello',
component: Hello,
// 配置子路由
children: [
{
// 子路由路径不需要加 `/`
path: 'tag',
component: Tag,
children:[
{
// 对路由器进行命名
name:'tagDetail',
// 通过占位符配置路径规则携带id和title参数
path:'detail/:tagId/:tagTitle',
component: DetailPage
}
]
},
{
path: 'category',
component: Category
}
]
},
]
})

src/pages/Tag.vue中修改为params方式传递:

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
<template>
<div>
<h2>标签页</h2>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">
<!-- 写法一: 跳转路由并携带params参数,写死 -->
<!-- 在路径后面添加携带的参数,与路由器中配置的规则保持一致 -->
<!-- <router-link v-bind:to="`/hello/tag/detail/999/ahzoo`">{{ item.title }}</router-link> -->

<!-- 写法二: 跳转路由并携带params参数 :to的字符串写法-->
<!-- 使用v-bind将后面的解析为表达式,同时使用模板代码“ ` ” 其转为模板,并使用模板语法(${item.id}),获取数据 -->
<!-- <router-link v-bind:to="`/hello/tag/detail/${item.tagId}/${item.tagTitle}`">{{ item.title }}</router-link> -->

<!-- 写法三: 跳转路由并携带params参数,:to的对象写法 -->
<!-- 使用v-bind将后面的解析为表达式,同时使用模板代码“ ` ” 其转为模板,并使用模板语法(${item.tagId}),获取数据 -->
<router-link
v-bind:to="{
//使用params传递参数时,若对路由器进行了命名,就不能使用path,只能用name,否则参数会传不过去
//path: '/hello/tag/detail',
name: 'tagDetail',
params: {
// tagId和tagTitle的名称与路由中配置的占位符保持一致
tagId: item.id,
tagTitle: item.title,
},
}"
>
{{ item.title }}
</router-link>
</li>
</ul>
<br />
<h2>标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data() {
return {
tagList: [
{ id: "001", title: "标签1" },
{ id: "002", title: "标签2" },
{ id: "003", title: "标签3" },
],
};
},
};
</script>

src/pages/Detail.vue中调用params参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<ul>
<!-- 因为在路由中配置的占位符是tagId和tagTitle,所以这里是从params中获取这两个数据 -->
<li> 标签ID:{{ $route.params.tagId }}</li>
<li> 标签名:{{ $route.params.tagTitle }}</li>
</ul>
</template>

<script>
export default {
name:'Detail',
// 钩子(挂载)函数,这里只是用于打印调试数据,可省略
mounted(){
console.log(this.$route)
}

}
</script>

图片

props

props传递参数的方式类似于HTTP中的Post方法传递

写法一、二(适用于params参数)

打开src/router/index.js,配置props

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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About'
import Hello from '../components/HelloWorld'
import Tag from '../pages/Tag'
import Category from '../pages/Category'
import DetailPage from '../pages/Datail.vue'

// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// 对路由器进行命名
name: 'About',
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About,

},
{
path: '/hello',
component: Hello,
// 配置子路由
children: [
{
// 子路由路径不需要加 `/`
path: 'tag',
component: Tag,
children: [
{
// 对路由器进行命名
name: 'tagDetail',
// 通过占位符配置路径规则携带id和title参数
path: 'detail/:id/:title',
component: DetailPage,
// 路由器的配置,写法一:值对应对象,对象中的所有key-value都会以props形式传递给DetailPage(即,在Detail界面进行接收数据)
// props:{a:7,z:'9'}
// 路由器的配置,写法二:若布尔值为真,会把路由器中的params参数以props形式传给DetailPage(即,在Detail界面进行接收数据)
props: true

}
]
},
{
path: 'category',
component: Category
}
]
},
]
})

src/pages/Detail.vue中调用props参数:

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
<template>
<ul>
<!-- 因为在路由中配置的占位符是tagId和tagTitle,所以这里是从params中获取这两个数据 -->
<li> 标签ID:{{ id }}</li>
<li> 标签名:{{ title }}</li>
</ul>
</template>

<script>
export default {
name:'Detail',
// 接收props数据
// 接收方式一传递的数据
// props:['a', 'z'],

// 接收方式二传递的数据
props:['id', 'title'],

// 钩子(挂载)函数,这里只是用于打印调试数据,可省略
mounted(){
console.log(this.$route)
}

}
</script>

src/pages/Tag.vue中传递数据:

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
<template>
<div>
<h2>标签页</h2>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">
<!-- 写法一: 跳转路由并携带params参数,写死 -->
<!-- 在路径后面添加携带的参数,与路由器中配置的规则保持一致 -->
<!-- <router-link v-bind:to="`/hello/tag/detail/999/ahzoo`">{{ item.title }}</router-link> -->

<!-- 写法二: 跳转路由并携带params参数 :to的字符串写法-->
<!-- 使用v-bind将后面的解析为表达式,同时使用模板代码“ ` ” 其转为模板,并使用模板语法(${item.id}),获取数据 -->
<!-- <router-link v-bind:to="`/hello/tag/detail/${item.id}/${item.title}`">{{ item.title }}</router-link> -->

<!-- 写法三: 跳转路由并携带params参数,:to的对象写法 -->
<!-- 使用v-bind将后面的解析为表达式,同时使用模板代码“ ` ” 其转为模板,并使用模板语法(${item.id}),获取数据 -->
<router-link
v-bind:to="{
//使用props传递参数时,若对路由器进行了命名,就不能使用path,只能用name,否则参数会传不过去
//path: '/hello/tag/detail',
name: 'tagDetail',
params: {
// id和title的名称与路由中配置的占位符保持一致
id: item.id,
title: item.title,
},
}"
>
{{ item.title }}
</router-link>
</li>
</ul>
<br />
<h2>标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data() {
return {
tagList: [
{ id: "001", title: "标签1" },
{ id: "002", title: "标签2" },
{ id: "003", title: "标签3" },
],
};
},
};
</script>

写法三:适用于paramsquery参数

query参数为例:

打开src/router/index.js,配置props

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
// 该文件用于创建路由器

// 引入路由
import VueRouter from 'vue-router';
// 引入组件
import About from '../components/About'
import Hello from '../components/HelloWorld'
import Tag from '../pages/Tag'
import Category from '../pages/Category'
import DetailPage from '../pages/Datail.vue'

// 创建并暴露一个路由器
export default new VueRouter({
// 配置路由规则
routes: [
{
// 对路由器进行命名
name: 'About',
// `/about` 为路径
path: '/about',
// About 对应上面引入的组件名
component: About,

},
{
path: '/hello',
component: Hello,
// 配置子路由
children: [
{
// 子路由路径不需要加 `/`
path: 'tag',
component: Tag,
children: [
{
// 对路由器进行命名
name: 'tagDetail',
// path: 'detail/:id/:title',
path: 'detail',
component: DetailPage,
// 路由器的配置,写法三:值对应函数
props($route) {
return {
// id: $route.params.id,
// title: $route.params.title
id: $route.query.id,
title: $route.query.title

}
}

}
]
},
{
path: 'category',
component: Category
}
]
},
]
})

src/pages/Detail.vue中调用props参数:

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
<template>
<ul>
<!-- 因为在路由中配置的占位符是tagId和tagTitle,所以这里是从params中获取这两个数据 -->
<li> 标签ID:{{ id }}</li>
<li> 标签名:{{ title }}</li>
</ul>
</template>

<script>
export default {
name:'Detail',
// 接收props数据
// 接收方式一传递的数据
// props:['a', 'z'],

// 接收方式二传递的数据
props:['id', 'title'],

// 钩子(挂载)函数,这里只是用于打印调试数据,可省略
mounted(){
console.log(this.$route)
}

}
</script>

src/pages/Tag.vue中传递数据:

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
<template>
<div>
<h2>标签页</h2>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">
<router-link
v-bind:to="{
//由于对路由器进行了命名,所以不能用path,只能用name
//path: '/hello/tag/detail',
name: 'tagDetail',
query: {
//params:{
id: item.id,
title: item.title,
},
}"
>
{{ item.title }}
</router-link>
</li>
</ul>
<br />
<h2>标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data() {
return {
tagList: [
{ id: "001", title: "标签1" },
{ id: "002", title: "标签2" },
{ id: "003", title: "标签3" },
],
};
},
};
</script>

如果要使用使用params传参,就只需要修改src/router/index.jssrc/pages/Tag.vue为params模式即可

图片

Vuex

在路由中,我们还可以使用vuex的参数传递:

1
2
3
4
5
6
7
8
9
// 同样是用什么就引入什么
// 引入路由
import store from "../store"

......
// 获取路由中的参数
const state = store.state.sumNum
......

replace

  • 浏览器的历史记录有两种写入模式:1. push模式:追加历史记录;2. replace模式替换当前历史记录

  • 基本写法::replace="true" ,简写:replace

  • 举个例子,我们在浏览器中依次点击了a、b、c、d界面。在没有开启replace模式下,可以通过浏览器的前进后退按钮,在这四个界面中循环切换;如果此时在b、c界面的路由中添加了replace属性,那么进入c界面时,b界面就会被替换掉,进入d界面时,c界面就会被替换点,最终b、c界面都被替换,此时从d界面进行后退操作,就不会回到c界面,而是直接回到a界面,b、c界面从历史记录中被替换移除掉了

继续用之前的项目举例:

在进入Detail.vue界面和进入Tag.vue的界面的路由添加replace属性:

src/pages/Tag.vue

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
<template>
<div>
<h2>标签页</h2>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">
<!-- 添加replace属性 -->
<router-link :replace="true"
v-bind:to="{
//由于对路由器进行了命名,所以不能用path,只能用name
//path: '/hello/tag/detail',
name: 'tagDetail',
query: {
//params:{
id: item.id,
title: item.title,
},
}"
>
{{ item.title }}
</router-link>
</li>
</ul>
<br />
<h2>标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data() {
return {
tagList: [
{ id: "001", title: "标签1" },
{ id: "002", title: "标签2" },
{ id: "003", title: "标签3" },
],
};
},
};
</script>

src/components/HelloWorld.vue

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
<template>
<div class="hello">
<div class="nav-tabs">
<!-- 添加replace属性 -->
<router-link :replace="true" class="list-group-item" to="/hello/tag" active-class="active"
>标签</router-link
>
<router-link class="list-group-item" to="/hello/category" active-class="active"
>分类</router-link
>
</div>

<h2>hello界面</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
name: "HelloWorld",
};
</script>
<style scoped>
.nav-tabs {
border-bottom: 1px solid #ddd;
}
.nav-tabs > li {
float: left;
margin-bottom: -1px;
}
.hello {
float: right;
width: 50%;
margin: 30px;
background-color: bisque;
}
</style>

依次点击:关于–>主页–>标签–>标签1

然后回退,正常顺序应该是:关于<–主页<–标签<–标签1

但是由于进入标签页和进入标签1的路由中加入了replace属性,所以主页界面和标签界面的历史记录都被替换掉了,顺序就变成了:关于<–标签1

图片

编程式路由

在原有项目上进行修改;

src/pages/Tag.vue

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
<template>
<div>
<h2>标签页</h2>
<button @click="forward">返回</button>
<ul>
<li v-for="item in tagList" v-bind:key="item.id">
<router-link
v-bind:to="{
name: 'tagDetail',
query: {
id: item.id,
title: item.title,
},
}"
>
{{ item.title }}
</router-link>
<!-- 路由模式 -->
<button @click="pushShow(item)">push方式查看</button>
<button @click="replaceShow(item)">replace方式查看</button>
</li>
</ul>
<br />
<h2>标签详情:</h2>
<router-view></router-view>
</div>
</template>

<script>
export default {
data() {
return {
tagList: [
{ id: "001", title: "标签1" },
{ id: "002", title: "标签2" },
{ id: "003", title: "标签3" },
],
};
},
methods:{
pushShow(item){
// 使用push模式
//相当于点击路由链接(可以返回到当前路由界面)
this.$router.push({
name: 'tagDetail',
query: {
id: item.id,
title: item.title,
},
})
},
replaceShow(item){
// 使用replace模式
//用新路由替换当前路由(不可以返回到当前路由界面)
this.$router.replace({
name: 'tagDetail',
query: {
id: item.id,
title: item.title,
},
})
},
forward(){
// 请求(返回)上一个记录路由
this.$router.back()
// 请求(返回)上一个记录路由
this.$router.go(-1)
// 请求下一个记录路由
this.$router.go(1)
}
}
};
</script>

图片

路由组件缓存

设置一个输入框,测试路由器缓存

src/pages/Category.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<h2>分类页</h2>
输入分类名:
<input type="text">
</div>
</template>

<script>
export default {
name:'CategoryName'
}
</script>

CategoryName组件缓存

src/components/HelloWorld.vue

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
<template>
<div class="hello">
<div class="nav-tabs">
<!-- 添加replace属性 -->
<router-link class="list-group-item" to="/hello/tag" active-class="active"
>标签</router-link
>
<router-link
class="list-group-item"
to="/hello/category"
active-class="active"
>分类</router-link
>
</div>

<h2>hello界面</h2>

<!-- include中写需要缓存的 组件名 -->
<!-- 如果不写include属性,则所有经过router-view中的组件都会被缓存 -->
<!-- <keep-alive> <router-view></router-view> </keep-alive> -->
<!-- 如果需要写多个组件,直接使用数组格式 -->
<!-- <keep-alive include="['CategoryName', 'Tag']"> -->
<keep-alive include="CategoryName">
<router-view></router-view>
</keep-alive>
</div>
</template>

<script>
export default {
name: "HelloWorld",
};
</script>
<style scoped>
.nav-tabs {
border-bottom: 1px solid #ddd;
}
.nav-tabs > li {
float: left;
margin-bottom: -1px;
}
.hello {
float: right;
width: 50%;
margin: 30px;
background-color: bisque;
}
</style>

测试效果,切换组件时,CategoryName组件输入框中的内容将不会被清除

图片

路由守护

新建一个vue项目

全局路由守卫

前置路由守卫
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
src/main.js
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import router from './router'

Vue.config.productionTip = false
Vue.use(VueRouter)

new Vue({
render: h => h(App),
router,
}).$mount('#app')
src/router/index.js
import VueRouter from 'vue-router'
import A from '../components/a.vue'
import B from '../components/b.vue'
import AA from '../components/aa.vue'
import AB from '../components/ab.vue'

// 创建一个路由器
const router = new VueRouter({
routes: [
{
path: '/a',
component: A,
children: [
{
path: 'aa',
component: AA,
// 在mata(路由元中自定义信息,作为判断是否被路由守护放行的依据)
meta: {
// 设置权限标识为aa
isAuth: "aa"
}
},
{
path: 'ab',
component: AB,
meta: {
// 设置权限标识为ab
isAuth: "ab"
}
}
]
},
{
path: '/b',
component: B
}
]

})

// 全局前置路由守卫
// 在初始化及每一次路由切换时执行箭头函数
router.beforeEach((to, from, next) => {
console.log(to)
//to表示跳转目标,from表示来源目标,next表示放行
// 判断路目标来源路径为/b或者跳转路径为/a或/b,则进行放行
if (from.path === "/b" || to.path === "/a" || to.path === "/b") {
// next放行
next();
} else {
if(to.path==="/a/aa"){
// 获取权限
const isAA = localStorage.getItem("thisAuth")

// 判断权限标识是否为aa,并且当前权限是否为aa
if(to.meta.isAuth==="aa"&&isAA==="true"){
next()
}else{
alert("权限不足")
}
}
else{
console.log(to.path)
}

}

})

// 暴露路由
export default router;

src/components/a.vue

1
2
3
4
5
6
7
8
<template>
<div>
a
<router-link active-class="active" to="/a/aa">去界面aa</router-link>
<router-link active-class="active" to="/a/ab">去界面ab</router-link>
<router-view></router-view>
</div>
</template>

src/components/b.vue

1
2
3
4
5
<template>
<div>
b
</div>
</template>

src/components/aa.vue

1
2
3
4
5
<template>
<div>
aa界面
</div>
</template>

src/components/ab.vue

1
2
3
4
5
<template>
<div>
ab界面
</div>
</template>
后置路由守护
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
src/router/index.js
import VueRouter from 'vue-router'
import A from '../components/a.vue'
import B from '../components/b.vue'
import AA from '../components/aa.vue'
import AB from '../components/ab.vue'

// 创建一个路由器
const router = new VueRouter({
routes: [
{
path: '/a',
component: A,
meta:{
// 定义界面标题
title: "a界面"
},
children: [
{
path: 'aa',
component: AA,
// 在mata(路由元中自定义信息,作为判断是否被路由守护放行的依据)
meta: {
// 设置权限标识为aa
isAuth: "aa",
title: "aa界面"
}
},
{
path: 'ab',
component: AB,
meta: {
// 设置权限标识为ab
isAuth: "ab",
title: "ab界面"
}
}
]
},
{
path: '/b',
component: B,
title: "b界面"
}
]

})

// 全局前置路由守卫
// 在初始化及每一次路由切换时执行箭头函数
router.beforeEach((to, from, next) => {
console.log(to)
//to表示跳转目标,from表示来源目标,next表示放行
// 判断路目标来源路径为/b或者跳转路径为/a或/b,则进行放行
if (from.path === "/b" || to.path === "/a" || to.path === "/b") {
// next放行
next();
} else {
if(to.path==="/a/aa"){
// 获取权限
const isAA = localStorage.getItem("thisAuth")

// 判断权限标识是否为aa,并且当前权限是否为aa
if(to.meta.isAuth==="aa"&&isAA==="true"){
next()
}else{
alert("权限不足")
}
}
else{
console.log(to.path)
}

}

})


// 全局后置路由守护
router.afterEach((to,from)=>{
// 通过路由后置守护设置页面标题(当然前置路由守护也可以)
// 设置界面标题等于在路由中设置的标题,并且默认为'首页'
document.title = to.meta.title || '首页'
console.log(from)
})

// 暴露路由
export default router;

图片

独享路由守卫

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
src/router/index.js
import VueRouter from 'vue-router'
import A from '../components/a.vue'
import B from '../components/b.vue'
import AA from '../components/aa.vue'
import AB from '../components/ab.vue'

// 创建一个路由器
const router = new VueRouter({
routes: [
{
path: '/a',
component: A,
children: [
{
path: 'aa',
component: AA,
// 在mata(路由元中自定义信息,作为判断是否被路由守护放行的依据)
meta: {
// 设置权限标识为aa
isAuth: "aa",
},
// 设置独享路由守卫
beforeEnter: (to, from, next) => {
const isAA = localStorage.getItem("thisAuth")
console.log(from)
// 判断权限标识是否为aa,并且当前权限是否为aa
if (to.meta.isAuth === "aa" && isAA === "true") {
next()
} else {
alert("权限不足")
}
}
},
{
path: 'ab',
component: AB,
}
]
},
{
path: '/b',
component: B,
}
]

})


// 暴露路由
export default router;

组件路由守卫

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
src/router/index.js
import VueRouter from 'vue-router'
import A from '../components/a.vue'
import B from '../components/b.vue'
import AA from '../components/aa.vue'
import AB from '../components/ab.vue'

// 创建一个路由器
const router = new VueRouter({
routes: [
{
path: '/a',
component: A,
children: [
{
path: 'aa',
component: AA,
// 在mata(路由元中自定义信息,作为判断是否被路由守护放行的依据)
meta: {
// 设置权限标识为aa
isAuth: "aa",
},
},
{
path: 'ab',
component: AB,
}
]
},
{
path: '/b',
component: B,
}
]

})


// 暴露路由
export default router;

src/components/aa.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>aa界面</div>
</template>
<script>
export default {
name: "aa",
// 通过路由规则(也就是说只有通过路由跳转时才会执行,如果是用template直接展示就不会执行)进入该组件时被调用
beforeRouteEnter(to, from, next) {
const isAA = localStorage.getItem("thisAuth");
console.log(from);
// 判断权限标识是否为aa,并且当前权限是否为aa
if (to.meta.isAuth === "aa" && isAA === "true") {
next();
} else {
alert("权限不足");
}
},
// 通过路由规则离开该组件时被调用
beforeRouteLeave(to, from, next) {
console.log("离开:", to, from);
next();
},
};
</script>

图片

模式

hash模式

hash模式为路由的默认模式,即路径中会以**#**号加路径的形式存在。

hash模式不会将**#号后面的路径作为值传递给后端,也就是说当浏览器路径为http://localhost/api/#/a/aa时,那么他向后台的请求地址就是#**号(hash)前面的地址,即:http://localhost/api/
图片

history模式

history即路径不带#号,但是会将所有路径作为请求发送给后台,后台可能会出现刷新报错404的问题,因此需要后台进行适配。

开启方式,通过路由器中的mode项配置:

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
import VueRouter from 'vue-router'
import A from '../components/a.vue'
import B from '../components/b.vue'
import AA from '../components/aa.vue'
import AB from '../components/ab.vue'

// 创建一个路由器
const router = new VueRouter({
// 设置路由的工作模式,可选history或者hash,不设置则默认为hash
mode: 'history',
routes: [
{
path: '/a',
component: A,
children: [
{
path: 'aa',
component: AA,
// 在mata(路由元中自定义信息,作为判断是否被路由守护放行的依据)
meta: {
// 设置权限标识为aa
isAuth: "aa",
},
},
{
path: 'ab',
component: AB,
}
]
},
{
path: '/b',
component: B,
}
]

})


// 暴露路由
export default router;

图片