# 第一个路由

在上一节中,我们任务列表的数据是自己手动造的,那么用户使用的时候怎么添加任务项呢?
这一节,我们将实现一个新增任务项的页面,它会和上一节的任务列表页通过Vue Router (opens new window)进行联动。

功能如下:

  1. 任务列表页,点击新增按钮,跳转到新增任务项页面
  2. 新增任务项页面,点击确认按钮,将新的任务项数据存储在 localStorage 里,并自动返回任务列表页
  3. 每次进入任务列表页时,都自动从 localStorage 里获取任务项列表的数据

# 快速实现新增任务项页面

为了统一管理,咱们先在 src 目录下新建个 views 文件夹来存放所有的页面。
由于新增任务项的页面超级简单,就是个 input 框加确认按钮,并且通过点击确认按钮触发存储数据的事件,就不多废话了:

// todo-list-client/src/views/TodoCreate.vue
<template>
  <div class="l-container">
    <header>
      <h1>新增任务</h1>
    </header>
    <main class="main">
      <input
        v-model="content" 
        type="text" >
      <button 
        class="c-button" 
        @click="save">
        确认
      </button>
    </main>
  </div>
</template>

<script>
export default {
  name: 'TodoCreate',
  data() { 
    return {
      content: ''
    }
  },
  methods: {
    save() {
      let todos = JSON.parse(localStorage.getItem('todos') || '[]')

      // 为了让列表页中新增的任务出现在最上面
      todos.unshift({
        id: todos.length,
        content: this.content,
        completed: false
      })

      localStorage.setItem('todos', JSON.stringify(todos))
    }
  }
 }
</script>

<style scoped>
.l-container {
  margin: 90px 30px 0;
}
.main {
  display: flex;
}
.c-button {
  width: 54px;
  height: 45px;
  padding: 0;
  margin-left: 10px;
  border: none;
  background: #E03535;
  border-radius: 4px;
  color: white;
}
</style>
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

暂时看不到页面效果,这个页面是通过任务列表页跳转进来的。

# CSS 的作用域

新增任务项页面保存后,我们会发现列表页的新增按钮的样式竟然意外的改变了?? 你能找出来问题在哪么?(回忆下上一节出现样式问题的时候,是怎么发现问题所在的)

通过浏览器控制台,我们可以看到,咱们页面的<head></head>标签里有三个内联样式,在最后两个里面各自有.c-button的样式,一个来自 App.vue ,一个来自 TodoCreate.vue 都影响了按钮的样式。解决办法也很简单,就是对 TodoCreate.vue 文件里的样式设置作用域( App.vue 的样式一般我们让它是全局的,不用设置作用域):

<style scoped>
</style>
1
2

原因:

按钮恢复预期样式啦~

# 引入 Vue Router

开始前,请确保Vue Router官方指南 (opens new window)你看完了并大致理解了。

请在脑海中想象一下,有三个盒子:

  • App.vue 盒子
  • <router-view />盒子(待会会出现在改造后 App.vue 的文件里)
  • 页面盒子

App.vue 的盒子里,直接装着 <router-view />盒子。

<router-view />盒子直接装着页面盒子,并且每次只装的下一个页面盒子。也就是说<router-view />这个盒子控制着每次展示哪一个页面。(有时间,我再补个图)

路由改变(浏览器地址栏的 URL 改变),<router-view />盒子就会展示 URL 指向的那个页面。

安装:

npm install vue-router
1

接下来,我们做两件事:

  1. 把我们的第一个页面盒子————任务列表页从 App.vue 盒子 里提取出来
  2. 在 App.vue 盒子里加入 <router-view />盒子

新建一个 TodoList.vue 文件(任务列表页)存放我们 views 目录下。整理完后代码如下:

// todo-list-client/src/views/TodoList.vue
<template>
  <div>
    <header class="header">
      <h1 class="header__title">我的任务</h1>
      <div class="header__body">完成{{ completedTodos.length  }}个 共{{ todos.length }}个</div>
    </header>
    <main class="main">
      <ul class="l-list">
        <li 
          v-for="(todo, index) in todos" 
          :key="todo.id" 
          :class="['l-list__item', { 'is-completed': todo.completed }]">
          <div class="c-checkbox">
            <input 
              v-if="!todo.completed" 
              v-model="todo.completed"
              type="checkbox" 
              class="c-checkbox__input"
              @click="finish(index)">
            <label :class="['c-checkbox__label', { 'is-completed': todo.completed }]">{{ todo.content }}</label>
          </div>
          <div 
            v-if="todo.completed" 
            class="l-list__text">
            {{ todo.completedDate }}
          </div>
        </li>
      </ul>
    </main>
    <footer>
      <button class="c-button">新增</button>
    </footer>
  </section>
</template>
<script>
export default {
  name: 'App',
  data() {
    return {
      todos: [
        {
          id: 1,
          content: '第1个任务',
          completed: false,
          completedDate: ''
        },
        {
          id: 2,
          content: '第2个任务',
          completed: true,
          completedDate: '2020/03/30'
        },
        {
          id: 3,
          content: '第3个任务',
          completed: false,
          completedDate: ''
        },
        {
          id: 4,
          content: '第4个任务',
          completed: true,
          completedDate: '2020/03/31'
        },
        {
          id: 5,
          content: '第5个任务',
          completed: false,
          completedDate: ''
        },
        {
          id: 6,
          content: '第6个任务',
          completed: false,
          completedDate: ''
        },
        {
          id: 7,
          content: '第7个任务',
          completed: false,
          completedDate: ''
        },
      ]
    }
  },
  computed: {
    completedTodos() {
      return this.todos.filter( item => item.completed === true )
    }
  },
  methods: {
    finish(index) {
      this.todos[index].completed = true
      this.todos[index].completedDate = new Date().toLocaleDateString()
    }
  }
}
</script>

<style scoped>
.header {
  position: relative;
  margin: 90px 0 0 70px;
  padding: 30px 0;
}
.header__title {
  color: #2c2c2c;
}
.header__body {
  color: #b9b9b9;
}
.header::after {
  content: ' ';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: #eeeeee;
  transform: scaleY(0.5);
}
.main {
  margin-top: 30px;
}
.l-list {
  padding-inline-start: 0;
  list-style-type: none;
}
.l-list__item {
  padding: 10px 0 10px 30px;
}
.l-list__item.is-completed {
  background: linear-gradient(to right, #F2F2F6, rgba(0,0,0,0));
}
.l-list__text {
  margin: 10px 0 0 40px;
  color: #c5c5c5;
}
.c-checkbox__label {
  margin-left: 27px;
}
.c-checkbox__label.is-completed {
  margin-left: 40px;
  color: #e02f2f;
  text-decoration: line-through;
}
.c-button {
  position: fixed;
  bottom: 35px;
  right: 30px;
  width: 54px;
  height: 54px;
  border: none;
  background: #E03535;
  padding: 0;
  border-radius: 4px;
  color: white;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// todo-list-client/src/App.vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

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

<style>
#app {
  min-width: 375px;
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

现在,我们有了两个页面:任务列表页和新增任务项页。接下来,我们建立路由,将这两个页面联系起来。

因为随着页面的增多,路由也会越来越多,模块化管理会方便很多。所以咱们先直接在src文件夹下新建个router文件夹放路由相关的文件,然后在router文件夹新建个index.js文件存放我们第一个路由信息:

// todo-list-client/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

import TodoList from '../views/TodoList'
import TodoCreate from '../views/TodoCreate'

const routes = [
  { path: '/', component: TodoList },
  { path: '/create', component: TodoCreate }
]
const router = new VueRouter({
  routes
})

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

然后在main.js里引入我们刚刚创建的路由信息,并在整个应用中注入路由

// todo-list-client/src/main.js
import Vue from 'vue'

import 'normalize.css/normalize.css'

import App from './App'
import router from './router/index'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 路由初体验

路由和页面都有了,下面咱们让我们的应用“动起来”!也就是实现本节开头说的那三个功能。

首先是从 localStorage 里读取任务列表的数据(在快速实现新增任务项页面中我们已经把每次新增的数据存到 localStorage 里了)









 






// todo-list-client/src/views/TodoList.vue

// ...
<script>
export default {
  name: 'TodoList',
  data() {
    return {
      todos: JSON.parse(localStorage.getItem('todos') || '[]')
    }
  }
}
// ...
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后是点击列表页新增按钮跳转新增任务项的页面






 
 
 
 
 












 
 
 
 
 




// todo-list-client/src/views/TodoList.vue
<template>
  <div>
    // ...
    <footer>
      <button 
        class="c-button"
        @click="add">
        新增
      </button>
    </footer>
  </div>
</template>

<script>
export default {
  name: 'TodoList',
  data() {
    return {
      todos: []
    }
  },
  methods: {
    add() {
      this.$router.push('/create')
    }
  }
 }
</script>
// ...
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

最后每次新增任务项成功后自动返回到任务列表项,并刷新任务列表页的数据。
其实刷新任务列表页的数据这个功能上面就完成了。因为任务列表页和新增任务页分别是对 localStorage 的读和存,本身就是同步的。但是一般项目中都是请求后端接口获取数据的,而且都是在 Vue 实例的 created 生命周期中完成的,所以我们先提前把这步改下。知识点:Vue 实例的实例生命周期钩子 (opens new window)












 






// todo-list-client/src/views/TodoCreate.vue
// ...
<script>
export default {
  name: 'TodoCreate',
  data() { 
    // ...
  },
  methods: {
    save() {
      // ...
      this.$router.go(-1)
    }
  }
 }
</script>
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17







 


 
 
 





// todo-list-client/src/views/TodoList.vue
// ...
<script>
export default {
  name: 'TodoList',
  data() {
    return {
      todos: []
    }
  },
  created() {
    this.todos = JSON.parse(localStorage.getItem('todos') || '[]')
  }
}
// ...
</script>
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 拓展阅读

在本节里,我们有提到了:

  • Vue 官方风格指南 (opens new window)

    官方的 Vue 特有代码的风格指南。如果在工程中使用 Vue,为了回避错误、小纠结和反模式,该指南是份不错的参考。不过我们也不确信风格指南的所有内容对于所有的团队或工程都是理想的。所以根据过去的经验、周围的技术栈、个人价值观做出有意义的偏差是可取的。

良好的编码习惯可以使你更快更好的阅读社区的代码,也是团队合作时不可忽视的软技能,推荐熟读并运用。

Last Updated: 11/9/2022, 6:38:52 AM