# Vue 3 组件及路由

## 概述

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

## 目录结构

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


### 配置 Tailwind CSS

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

```shell
npm install tailwindcss @tailwindcss/vite
```

在 `vite.config.ts` 中增加以下内容：

```typescript
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({  
  plugins: [tailwindcss()]
})
```

在 `/src/styles/index.css` 中引入 Tailwind CSS：

```css
@import "tailwindcss";
```

在 `/src/main.ts` 中增加以下内容：

```typescript
import './styles/index.css'
```

### 配置 TypeScript

```text
src/
├─ types/
│  ├─ xxx.ts
│  ├─ xxx.ts
│  └─ xxx.ts
```

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

```typescript
export type gender = '男' | '女'  
  
export interface User {  
  id: number  
  name: string  
  gender: gender  
}
```

在 `xxx.vue` 中这样使用，使用 ` @ ` 符号表示项目的 ` src ` 目录：

```vue
<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` 的时候会自动创建好：

```typescript
export default defineConfig({  
  resolve: {  
    alias: {  
      '@': fileURLToPath(new URL('./src', import.meta.url)),  
    },  
  },  
})
```

### 配置组件

```text
src/
├─ components/
│  ├─ xxx.vue
│  ├─ xxx.vue
│  └─ xxx.vue
```

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

```vue
<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` 文件中，我想要使用这个组件，可以这样写：

```vue
<template>  
  <SubmitButton></SubmitButton>
</template>  

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

### 配置路由

```text
src/
├─ pages/
│  ├─ xxx.vue
│  ├─ xxx.vue
│  └─ xxx.vue
├─ router/
│  └─ index.ts
```

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

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

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

```shell
npm install vue-router
```

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

```typescript
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` 并使用：

```typescript
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
```

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

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

### 配置状态管理

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

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

Vue 3 会为我们安装 `pinia`，在项目目录下执行命令：

```shell
npm install pinia
```

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

```typescript
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
```

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

## 组件通信

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

### props 属性





## 路由

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

### 页面跳转

在 `/src/pages` 目录下创建 `Home.vue`、`News.vue` 和 `About.vue` 三个组件

```vue
<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` 中写入如下内容：

```typescript
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` 中，需要引入 `RouterLink` 和 `RouterView`：

```vue
<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` 为路由的 `path`，`router-view` 标签为加载页面内容的位置。

### 路由命名

在 `/src/router/index.ts` 中，路由可以指定 `name` 属性：

```typescript
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` 比较长时，这种方式会更加简便：

```vue
<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>
```

### 嵌套路由

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

修改之前的 `News.vue`：

```vue
<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`：

````vue
<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`：

```typescript
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` 中传入。

### 路由传参

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

在`/src/pages/NewsDetail.vue`中稍作修改：

```vue
<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`：

```typescript
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`，可以看到两种传参方式差别不是很大。







---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/vue-3-%E7%BB%84%E4%BB%B6%E5%8F%8A%E8%B7%AF%E7%94%B1/  

