Vue 3 组件及路由

第 1 节 概述

在复杂的前端项目中,会有多个页面和模块,使用 Vue 3 构建复杂项目时,为了方便实现页面的模块化解耦以及统一管理,需要使用组件和路由。

第 2 节 目录结构

一个复杂的 Vue 3 项目会由多个组件构成,这些组件组成了如下的目录结构:

2.1 配置 Tailwind CSS

在项目目录下使用命令进行安装:

1
npm install tailwindcss @tailwindcss/vite

vite.config.ts 中增加以下内容:

1
2
3
4
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({  
  plugins: [tailwindcss()]
})

/src/styles/index.css 中引入 Tailwind CSS:

1
@import "tailwindcss";

/src/main.ts 中增加以下内容:

1
import './styles/index.css'

2.2 配置 TypeScript

1
2
3
4
5
src/
├─ types/
│  ├─ xxx.ts
│  ├─ xxx.ts
│  └─ xxx.ts

TypeScript 的类型定义放在 /src/types 目录下,需要新增类型时,创建 xxx.ts 文件,比如创建一个 user.ts 文件:

1
2
3
4
5
6
7
export type gender = '男' | '女'  
  
export interface User {  
  id: number  
  name: string  
  gender: gender  
}

xxx.vue 中这样使用,使用 @ 符号表示项目的 src 目录:

1
2
3
4
5
6
7
8
9
<script lang="ts" setup>  
import { RouterLink, RouterView } from 'vue-router'  
import type { User } from '@/types/user'  
const person: User = {  
  id: 1,  
  name: '张三',  
  gender: '男',  
}  
</script>

@ 符号的定义在 vite.config.ts 中, 使用 npm create vue@latest 的时候会自动创建好:

1
2
3
4
5
6
7
export default defineConfig({  
  resolve: {  
    alias: {  
      '@': fileURLToPath(new URL('./src', import.meta.url)),  
    },  
  },  
})

2.3 配置组件

1
2
3
4
5
src/
├─ components/
│  ├─ xxx.vue
│  ├─ xxx.vue
│  └─ xxx.vue

通用的小组件放在 /scr/components 目录下,文件名是 xxx.vue,Vue 3 要求组件名首字母大写,并且至少要有两个单词组成,比如,创建 SubmitButton.vue 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <button
    class="px-4 py-2 rounded bg-blue-500 text-white text-sm hover:bg-blue-600 transition"
    @click="handleClick"
  >
    提交
  </button>
</template>

<script setup lang="ts">
function handleClick() {
  console.log('SubmitButton 被点击了')
}
</script>

xxx.vue 文件中,我想要使用这个组件,可以这样写:

1
2
3
4
5
6
7
<template>  
  <SubmitButton></SubmitButton>
</template>  

<script lang="ts" setup>  
import SubmitButton from '@/components/SubmitButton.vue'  
</script>

2.4 配置路由

1
2
3
4
5
6
7
src/
├─ pages/
│  ├─ xxx.vue
│  ├─ xxx.vue
│  └─ xxx.vue
├─ router/
│  └─ index.ts

页面组件需要配置单独的 URL,路由(Router)就是管理页面跳转和 URL 映射的机制,所以我们需要通过路由来配置这些组件。

使用 npm create vue@latest 的时候可以选择是否配置路由,Vue 3 会自动为我们配置路由。

Vue 3 会为我们安装 vue-router,在项目目录下执行命令:

1
npm install vue-router

Vue 3 会创建 /src/router/index.ts 文件,并且进行初始化,导出一个 router 对象:

1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from 'vue-router'  

const router = createRouter({  
  history: createWebHistory(import.meta.env.BASE_URL),  
  routes: [],  
})  

export default router

Vue 3 会在 /src/main.ts 中导入 router 并使用:

1
2
3
4
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

Vue 3 的页面组件需要放到 /src/pages 文件夹中。

vue-router 的具体使用方法在后面的章节讲述。

2.5 配置状态管理

Vue 3 中有多种组件之间的通信方式,使用 Pinia 是其中一种方法,Pinia 是一个状态管理库,可以实现多个组件共享数据,当其状态改变时,相关组件自动更新。

使用 npm create vue@latest 的时候可以选择是否配置状态管理,Vue 3 会自动为我们配置状态管理。

Vue 3 会为我们安装 pinia,在项目目录下执行命令:

1
npm install pinia

Vue 3 会在 /src/main.ts 中导入 pinia 并使用:

1
2
3
4
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')

pinia 的具体使用方法在后面的章节讲述。

第 3 节 组件通信

通常,父组件和子组件之间存在数据交互,Vue 3 中实现了这一交互,可以很方便实现组件之间的通信。

3.1 props 属性

第 4 节 路由

以一个简单的新闻网页为例说明如何构建路由,网页有导航栏和内容区域两部分,导航栏有主页、新闻和关于三个标签,点击导航栏的标签,内容区域加载对应的页面。

4.1 页面跳转

/src/pages 目录下创建 Home.vueNews.vueAbout.vue 三个组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <div>
  	<h1 class="text-2xl font-bold">首页</h1>
    <p>欢迎来到首页</p>
  </div>
</template>

<template>
  <div>
  	<h1 class="text-2xl font-bold">新闻</h1>
    <p>这里是新闻页面</p>
  </div>
</template>

<template>
  <div>
  	<h1 class="text-2xl font-bold">关于</h1>
    <p>这里是关于页面</p>
  </div>
</template>

/src/router/index.ts 中写入如下内容:

 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
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'

const routes = [  
  {
    path: '/',
    redirect: '/home',
  },
  {
    path: '/home',
    component: Home,
  },
  {
    path: '/news',
    component: News,
  },  
  {  
    path: '/about',
    component: About,  
  },  
] 

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

然后在 xxx.vue 中,需要引入 RouterLinkRouterView

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
  <div>
    <nav class="p-4 bg-gray-200 flex gap-10 justify-center">
      <router-link class="hover:text-blue-500" to="/">首页</router-link>
      <router-link class="hover:text-blue-500" to="/news">新闻</router-link>
      <router-link class="hover:text-blue-500" to="/about">关于</router-link>
    </nav>

    <div class="p-4">
      <router-view />
    </div>
  </div>
</template>

<script lang="ts" setup>
import { RouterLink, RouterView } from 'vue-router'
</script>

router-link 标签中需要指定属性 to 为路由的 pathrouter-view 标签为加载页面内容的位置。

4.2 路由命名

/src/router/index.ts 中,路由可以指定 name 属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const routes = [  
  {  
    path: '/',  
    redirect: '/home',  
  },  
  {  
    path: '/home',  
    name: 'home',  
    component: Home,  
  },  
  {  
    path: '/news',  
    name: 'news',  
    component: News,  
  },  
  {  
    path: '/about',  
    name: 'about',  
    component: About,  
  },  
]

这样在 xxx.vue 中就可以使用 :to,通过 name 来查找 path,当 path 比较长时,这种方式会更加简便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>
  <div>
    <nav class="p-4 bg-gray-200 flex gap-10 justify-center">
      <router-link :to="{ name: 'home' }" class="hover:text-blue-500">首页</router-link>
      <router-link :to="{ name: 'news' }" class="hover:text-blue-500">新闻</router-link>
      <router-link :to="{ name: 'about' }" class="hover:text-blue-500">关于</router-link>
    </nav>

    <div class="p-4">
      <router-view />
    </div>
  </div>
</template>

4.3 嵌套路由

路由可以嵌套为多级,比如新闻页面下可以有新闻的详情页面。

修改之前的 News.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>
  <div class="flex gap-4">
    <div class="w-1/4 bg-gray-100 p-4 rounded">
      <h2 class="text-xl font-bold mb-2">新闻列表</h2>
      <ul class="space-y-2">
        <li v-for="item in newsList" :key="item.id">
          <router-link
            :to="{ name: 'news-detail', params: { id: item.id } }"
            class="block text-blue-500 hover:underline"
          >
            {{ item.title }}
          </router-link>
        </li>
      </ul>
    </div>

    <div class="flex-1 bg-white p-4 rounded shadow">
      <router-view />
      <!-- 如果没有选择新闻显示默认提示 -->
      <template>
        <p class="text-gray-400">请选择一条新闻查看详情</p>
      </template>
    </div>
  </div>
</template>

<script setup lang="ts">
const newsList = [
  { id: 1, title: '新闻 1' },
  { id: 2, title: '新闻 2' },
  { id: 3, title: '新闻 3' },
]
</script>

/src/pages 目录下创建 NewsDetail.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <div>
    <h2 class="text-xl font-bold">新闻详情页</h2>
    <p>新闻 ID: {{ newsId }}</p>
  </div>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()
const newsId = computed(() => route.params.id)
</script>

修改/src/router/index.ts

 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
const routes = [
  {
    path: '/',
    redirect: { name: 'home' },
  },
  {
    path: '/home',
    name: 'home',
    component: Home,
  },
  {
    path: '/news',
    name: 'news',
    component: News,
    children: [
      {
        path: ':id',
        name: 'news-detail',
        component: NewsDetail,
      },
    ],
  },
  {
    path: '/about',
    name: 'about',
    component: About,
  },
]

/news 下新增 children 属性,并定义 path:id, 这样新闻详情的 path 就会是多级嵌套的叠加,变成 /news/:id,这里的 :id 为动态参数,在 news.vue 中传入。

4.4 路由传参

路由的传参方式有两种,一种是使用 query 传参,一种是使用 params 传参,上面的例子中使用的就是 params 传参,稍作修改就可以变成使用 query 传参。

/src/pages/NewsDetail.vue中稍作修改:

1
2
3
4
5
6
<router-link
  :to="{ name: 'news-detail', query: { id: item.id } }"
  class="block text-blue-500 hover:underline"
>
  {{ item.title }}
</router-link>

然后修改/src/router/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const routes = [
  {
    path: '/news',
    name: 'news',
    component: News,
    children: [
      {
        path: '',
        name: 'news-detail',
        component: NewsDetail,
      },
    ],
  }
]

使用query传参时,path形式类似/news?id=1&x=x&y=y,使用params传参时path形式类似/news/1/x/y,可以看到两种传参方式差别不是很大。