04前端初始化

how to

Vue3 项目初始化与路由配置完整笔记

本笔记在保留全部原始代码的基础上,把「从零搭建一个 Vue3 项目」过程中涉及的每一个知识点都展开做了详细讲解:包括清理脚手架、安装并配置各种依赖、目录结构设计、Header 组件、以及路由 / RouterView 嵌套渲染机制(这是本篇重点)。


目录

  1. 项目清理:清空脚手架默认内容
  2. 安装依赖(逐个讲清用途)
  3. Tailwind CSS 配置
  4. main.ts 入口配置
  5. TypeScript 类型提示配置
  6. 目录结构设计
  7. 编写 Header 组件
  8. 路由配置
  9. RouterView 与嵌套路由渲染机制(重点)
  10. 知识点速查总结

一、项目清理:清空脚手架默认内容

1.1 要做什么

create-vue(Vue 官方脚手架)生成的项目里,自带了一堆「示例代码」(欢迎页、示例组件、示例样式)。真正开发前,先把这些「样板 / demo」清掉,得到一个干净的起点。具体操作:

  • 打开 assets 目录:删除 logo.svg清空 main.cssbase.css 两个文件(是清空内容,不是删文件,后面 base.css 还要写 Tailwind 引入)。
  • 删除 components/ 下面的所有文件(那些是脚手架自带的示例组件,如 HelloWorld.vueTheWelcome.vue 等)。
  • router/index.ts:把 routes 数组清空(示例路由不要了,后面重写)。
  • views/ 下的两个文件删除(HomeView.vueAboutView.vue 示例页面)。
  • App.vue:删除所有内容,重新初始化(清成一个空壳,后面重写)。

1.2 为什么要先清理

  • 避免干扰:示例代码里有很多引用关系(App 引 components、router 引 views……),不清理会一直报错、也影响阅读。
  • 明确起点:从空白开始按自己的架构组织代码,思路清晰。
  • 注意先后顺序:因为文件之间有引用,清理时容易出现「A 删了但 B 还在引 A」的报错。合理顺序是从「被引用方」往「引用方」清,或者干脆一次性全清完再重写。比如 App.vue 引了 router,router 引了 views,那清理时把它们一起处理,最后统一重建。

App.vue 重新初始化大致长这样(一个最小空壳):

1
2
3
4
5
<script setup lang="ts"></script>

<template>
  <RouterView />
</template>

只保留一个 <RouterView />(一级路由出口),其余留白,后面再填充。


二、安装依赖(逐个讲清用途)

所有依赖都用 pnpm 安装。先说 pnpm,再逐个讲每个包。

2.1 包管理器 pnpm

  • 包管理器是什么:Node 生态里用来「下载、安装、管理第三方库(npm 包)」的工具。常见的有 npm、yarn、pnpm 三个。
  • pnpm 的特点
    • :安装速度通常比 npm/yarn 快。
    • 省空间:它用「硬链接 + 全局内容寻址存储」的方式管理依赖——同一个版本的包在你电脑上只存一份,各个项目通过链接指向它,不像 npm 每个项目都复制一份,能省下大量磁盘。
    • 依赖更严格(防幽灵依赖):pnpm 的 node_modules 结构是「非扁平」的,只有你在 package.json明确装过的包才能被 import。npm 的扁平结构会把间接依赖也提到顶层,导致你可能「不小心 import 了没装过的包」(叫「幽灵依赖 / phantom dependency」),换个环境就崩。pnpm 从结构上避免了这个问题。
  • pnpm add xxx:安装一个包并写进 package.jsondependencies。(等价于 npm 的 npm install xxx。)

2.2 tailwindcss + @tailwindcss/vite

1
pnpm add tailwindcss @tailwindcss/vite
  • tailwindcss:一个原子化 / 工具类优先(utility-first) 的 CSS 框架。它不给你封装好的「组件」,而是给你成千上万个「小工具类」(如 flexp-4text-centerbg-white),你在 HTML 里直接组合这些类来写样式,几乎不用另写 CSS 文件。
    • 好处:不用给每个元素起 class 名、不用在 HTML 和 CSS 文件间来回跳、样式就近可见、天然响应式、打包时会自动删掉没用到的类(体积小)。
  • @tailwindcss/vite:Tailwind 官方的 Vite 插件。这是 Tailwind v4 的用法(v4 把和构建工具的集成做成了官方插件,配置比 v3 简单很多,不再需要 tailwind.config.jspostcss.config.js 那一套繁琐配置)。

2.3 three + @types/three

1
pnpm add three @types/three
  • three:即 Three.js,基于 WebGL 的 3D 图形库(上一篇笔记里登录页左侧的 3D 模型就是用它做的)。用来在网页里渲染 3D 场景、模型、动画。
  • @types/three:three 的 TypeScript 类型声明包
    • 为什么要单独装类型? 有些库本身是用纯 JavaScript 写的,不自带类型定义。TypeScript 项目里用它们时,编辑器就没有类型提示、也无法做类型检查。社区维护了一个叫 DefinitelyTyped 的仓库,专门为这些库补充类型声明,发布成 @types/xxx 的包。装上 @types/three 后,写 Three.js 代码就有完整的类型提示和检查了。
    • 约定:凡是名字形如 @types/xxx 的,都是「给 xxx 这个库补类型的」,一般装在 devDependencies(只开发时用,打包不需要)。

2.4 axios

1
pnpm add axios
  • axios:最流行的 HTTP 请求库,基于 Promise,浏览器和 Node 都能用。用来向后端发接口请求(GET / POST 等)。
  • 相比浏览器原生的 fetch,axios 的优势:自动把响应转成 JSON、更方便的错误处理、请求/响应拦截器(可统一加 token、统一处理错误)、请求取消、超时设置等。项目里通常会基于 axios 封装一个统一的请求实例(对应目录结构里的 apis/index.ts)。

2.5 @microsoft/fetch-event-source

1
pnpm add @microsoft/fetch-event-source
  • @microsoft/fetch-event-source:微软开源的一个库,用来处理 SSE(Server-Sent Events,服务器发送事件) 这种「服务器持续、单向地往客户端推送数据」的流式场景。
  • 用在哪? 典型就是 AI 对话的「打字机效果」——你问 AI 一个问题,回答是一个字一个字「流」出来的,而不是等全部生成完才一次性显示。这背后就是服务器用 SSE 把生成的内容一段段推过来。(对应目录里的 AI 聊天页 smart/chat。)
  • 为什么不用浏览器原生的 EventSource 原生 EventSource 只支持 GET 请求、不能自定义请求头(没法带 Authorization token)、不能发 POST body。而 AI 对话通常需要 POST 提交问题、还要带鉴权头。@microsoft/fetch-event-source 基于 fetch 实现,支持 POST、自定义 header、更灵活的重连控制,正好补足原生的短板。

2.6 marked

1
pnpm add marked
  • marked:一个把 Markdown 文本转换成 HTML 的解析库,速度快、体积小。
  • 用在哪? 还是 AI 对话场景。大模型返回的内容通常是 Markdown 格式(带 # 标题、**加粗**、代码块、列表等)。直接显示这些原始符号很难看,用 marked 把 Markdown 转成 HTML 再渲染,就能得到排版漂亮的富文本(标题、代码高亮、列表等)。

安全提醒:把「转换后的 HTML」用 v-html 插入页面时要注意 XSS(跨站脚本攻击) 风险——如果内容里混入了恶意 <script>,可能被执行。生产环境建议配合 DOMPurify 之类的库先对 HTML 消毒过滤后再渲染。

2.7 element-plus + @element-plus/icons-vue

1
2
pnpm add element-plus
pnpm add @element-plus/icons-vue
  • element-plus:面向 Vue 3 的一套桌面端 UI 组件库(Element UI 的 Vue3 版)。提供大量开箱即用的组件:按钮、表单、输入框、弹窗、表格、消息提示等。上一篇登录页用到的 el-formel-inputel-buttonElMessage 都来自它。用它能快速搭出美观、统一的界面,不用自己从零写组件。
  • @element-plus/icons-vue:Element Plus 的官方图标库,把图标做成了一个个 Vue 组件(如 HomeFilledSettingStar)。用的时候 import 进来当组件渲染。注意它是单独的包,需要单独安装。

2.8 pinia-plugin-persistedstate

1
pnpm add pinia-plugin-persistedstate
  • 背景Pinia 是 Vue3 的官方状态管理库,存放全局共享状态(如登录用户信息)。但 Pinia 的状态是存在内存里的——页面一刷新,内存清空,状态就没了(比如刷新后「登录用户」又变回未登录)。
  • pinia-plugin-persistedstate:Pinia 的一个持久化插件。它能自动把指定 store 的状态同步保存到 localStorage(或 sessionStorage,页面刷新后再自动读回来恢复。这样用户信息、token 等就能在刷新后保持,不用重新登录。
  • 用法上:先在 main.ts 里 pinia.use(piniaPluginPersistedstate) 启用插件,然后在需要持久化的 store 里开启 persist: true 即可。

小结这一节:这套依赖组合起来就是一个「AI 英语学习 App」的完整技术栈——Tailwind(样式)+ Element Plus(UI 组件)+ Three.js(3D 展示)+ axios(普通接口)+ fetch-event-source(AI 流式对话)+ marked(渲染 AI 返回的 Markdown)+ Pinia 及持久化插件(状态管理 + 刷新不丢失)。


三、Tailwind CSS 配置

分三步:安装(上面已装)、配置 Vite 插件、写 CSS 入口。

3.1 配置 Vite 插件

vite.config.ts 里把 Tailwind 的插件加进 plugins 数组:

1
2
3
4
5
6
7
8
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})

逐点解释:

  • vite.config.ts 是什么:Vite(本项目的构建/开发工具)的配置文件。项目怎么启动、用哪些插件、路径别名、代理等都在这里配。
  • defineConfig(...):Vite 提供的一个工具函数。它本身不做特别的事,主要作用是提供类型提示——把配置对象包进去,编辑器就能对里面的字段自动补全和类型检查。
  • plugins: [...]:Vite 的插件数组。插件用来扩展 Vite 的能力。
  • tailwindcss():调用刚装的 @tailwindcss/vite 插件(注意要调用,加括号)。加进去后,Vite 在构建时就会处理 Tailwind 的那些工具类,把你用到的类编译成真正的 CSS。
  • 注意:实际项目里这个数组通常还有 Vue 插件等,比如 plugins: [vue(), tailwindcss()]。这里为了聚焦 Tailwind 只写了一个。

Tailwind v3 vs v4 的差别(顺带一提,避免看老教程混淆):v3 时代要装 postcssautoprefixer,还要手写 tailwind.config.js 配 content 路径、写 postcss.config.jsv4(本项目用的) 把这些都简化了——一个官方 Vite 插件搞定,配置极少。所以如果你看到教程里让你建 tailwind.config.js,那是 v3 的做法。

3.2 CSS 入口文件

assets/base.css(之前清空的那个)里写:

1
2
3
4
5
@import 'tailwindcss';

html,body,#app{
    @apply h-full w-full;
}

逐行解释:

  • @import 'tailwindcss';:把 Tailwind 的全部样式引入进来。这是 Tailwind v4 的写法——一行 @import 'tailwindcss' 就把「基础重置样式 + 所有工具类」都引进来了。(v3 时代是写三行 @tailwind base; @tailwind components; @tailwind utilities;,v4 合并成了一行 import。)这一行必须放最前面,后面才能用 Tailwind 的功能。
  • html,body,#app { ... }:一条普通的 CSS 选择器,同时选中 htmlbodyid="app" 的元素(#app 就是 Vue 应用挂载的根节点)。
  • @apply h-full w-full;
    • @apply 是 Tailwind 的指令,作用是「在 CSS 里套用 Tailwind 的工具类」。它把工具类对应的样式「展开」写进这条 CSS 规则里。
    • h-full = height: 100%w-full = width: 100%
    • 所以这段等价于给 html/body/#app 都设 height: 100%; width: 100%;
    • 为什么要这么做? 默认情况下 htmlbody#app 的高度是「由内容撑开」,并不占满整个屏幕。想让页面布局(尤其是「满屏高度」的布局,如侧边栏、fixed 定位)正常工作,需要让这三层容器都撑满视口的宽高,形成一条「100% 高度」的传递链。这是做整页布局的经典前置设置。
  • @apply 的意义:它让你能把「一串常用的工具类」封装进一个语义化的 CSS 类里复用,而不用在 HTML 里重复写一长串。不过日常大多数场景直接在 HTML 里写工具类即可,@apply 用于这种「全局基础样式」或「需要复用的样式组合」。

别忘了在 main.ts 里引入这个 css 文件(见下一节 import '@/assets/base.css'),否则样式不会生效。


四、main.ts 入口配置

main.ts 是整个 Vue 应用的入口文件——应用从这里启动,所有全局的东西(插件、样式、UI 库)都在这里注册。

4.1 完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import '@/assets/base.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

app.use(pinia)
app.use(ElementPlus, {
  locale: zhCn,
})
app.use(router)

app.mount('#app')

4.2 三类 import 分别是什么

① 引入 CSS(副作用导入)

1
2
import '@/assets/base.css'
import 'element-plus/dist/index.css'
  • 这种「只写路径、不解构任何东西」的 import 叫副作用导入。它不导入具体的变量,只是「执行这个文件」——对 CSS 来说,就是把这份样式加载进项目、让它生效。
  • @/assets/base.css:我们自己写的 Tailwind 入口(@ 是指向 src 的路径别名)。
  • element-plus/dist/index.css:Element Plus 组件的样式。必须引入,否则 el-button 等组件会「没有样式、丑陋错乱」。

② 引入功能函数 / 库

1
2
3
4
import { createApp } from 'vue'                    // 创建应用实例
import { createPinia } from 'pinia'                // 创建 pinia 实例
import ElementPlus from 'element-plus'             // 组件库本体
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 持久化插件

③ 引入本项目模块

1
2
import App from './App.vue'      // 根组件
import router from './router'    // 路由实例(会自动找 router/index.ts)
  • import router from './router':注意路径是目录 ./router,没写文件名。这是模块解析的默认规则——引入一个目录时,会自动去找该目录下的 index.ts(或 index.js)。所以它实际引的是 router/index.tsexport default 出去的路由实例。

④ 中文语言包

1
import zhCn from 'element-plus/es/locale/lang/zh-cn'
  • Element Plus 默认是英文(比如分页、日期选择器、表格里的文字都是英文)。这里引入它自带的中文(简体)语言包 zhCn,后面配置给组件库,界面文字就变成中文了。这属于 i18n(国际化,internationalization) 的一部分。

4.3 创建应用与 Pinia

1
2
3
const app = createApp(App)      // 用根组件 App 创建 Vue 应用实例
const pinia = createPinia()     // 创建 Pinia(状态管理)实例
pinia.use(piniaPluginPersistedstate)  // 给 Pinia 挂上持久化插件
  • createApp(App):Vue3 的应用创建函数。传入根组件 App,返回一个应用实例 app。之后所有全局配置(装插件、注册全局组件等)都通过这个 app 来做。这是 Vue3 相比 Vue2 的变化——Vue3 是「每个应用一个独立实例」,配置都挂在实例上,不再是全局污染。
  • createPinia():创建一个 Pinia 实例(状态管理的「总仓库」)。
  • pinia.use(piniaPluginPersistedstate):给这个 Pinia 实例安装持久化插件。装完后,各个 store 才能使用 persist 持久化能力。注意顺序:必须先给 pinia 装插件,再把 pinia 交给 app(下一步)。

4.4 注册插件(app.use)与挂载

1
2
3
4
5
app.use(pinia)                       // 启用 Pinia
app.use(ElementPlus, { locale: zhCn }) // 启用 Element Plus,并设为中文
app.use(router)                      // 启用路由

app.mount('#app')                    // 把应用挂载到页面
  • app.use(插件, 选项?):Vue 的插件安装方法。把一个插件注册到应用上,让它的功能在整个应用中可用。
    • app.use(pinia):启用状态管理。
    • app.use(ElementPlus, { locale: zhCn }):启用 Element Plus,第二个参数是配置项{ locale: zhCn } 就是把语言设为前面引入的中文包。这样组件内置文字全变中文。
    • app.use(router):启用 Vue Router,之后才能用 <RouterView><RouterLink>useRouter() 等。
  • app.use 支持链式调用(每次返回 app 自身),也可以写成 app.use(pinia).use(ElementPlus,...).use(router)。分开写更清晰。
  • app.mount('#app')挂载。把整个 Vue 应用「装」到页面上 id="app" 的那个 DOM 元素里(就是 index.html 里的 <div id="app"></div>)。执行这一步后,页面才真正渲染出来。mount 必须放最后——所有插件都 use 完了再挂载。

记住这个固定套路createApp → 创建各种实例/插件 → 一堆 app.use(...) → 最后 app.mount(...)。顺序不能乱,尤其 mount 一定在最后。


五、TypeScript 类型提示配置

tsconfig.app.json 里添加一项配置:

1
2
3
4
5
6
{
  "compilerOptions": {
    // ...其他配置
    "types": ["element-plus/global"]
  }
}
  • tsconfig.app.json 是什么:TypeScript 的配置文件之一。Vite + Vue 项目通常把 TS 配置拆成几个:tsconfig.json(总入口,引用其他两个)、tsconfig.app.json(管应用源码 src)、tsconfig.node.json(管 vite.config.ts 这类 Node 环境代码)。这里改的是管 src 源码的那个。
  • "types": ["element-plus/global"]
    • 作用:让 Element Plus 的组件(<el-button><el-form> 等)在模板里有全局类型提示和检查。加了这个之后,编辑器能识别这些标签、提示它们的属性(props)、发现用错的地方。
    • 为什么需要:Element Plus 组件是全局注册使用的(模板里直接写 <el-xxx>),TypeScript 默认不知道这些全局标签对应什么类型。element-plus/global 这个类型声明就是告诉 TS「这些全局组件长什么样」。
    • 一个坑types 字段一旦手动指定,TypeScript 就只加载你列出的这些类型包,而不再自动加载 node_modules/@types 下的全部。所以如果项目还依赖别的全局类型(如 node),可能需要一并写进数组:"types": ["element-plus/global", "node"]。本例只列了 element-plus/global,按需补充即可。

六、目录结构设计

清理干净后,按功能职责规划 src 目录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
src/
├── apis/               # 统一管理后端接口请求
│   └── index.ts        # 接口初始化与请求实例配置
├── assets/             # 静态资源目录(图片、样式、字体等)
├── components/         # 全局通用公共组件
├── hooks/              # Vue 3 自定义组合式函数(Custom Hooks)
├── layout/             # 页面整体框架布局
│   ├── Content/        # 主体内容区域
│   ├── Header/         # 顶栏组件
│   ├── Profile/        # 用户个人信息
│   └── index.vue       # 布局组件入口
├── router/             # 路由配置目录(定义页面映射关系)
├── stores/             # 全局状态管理目录(如 Pinia / Vuex)
├── views/              # 业务页面组件(具体的路由页面)
└── App.vue             # 项目的顶级根组件

6.1 为什么要这样分目录

「按职责分层」是工程化项目的基本功。 目录清晰,团队协作、后期维护、找代码都更高效。每个目录的职责边界要明确——一看目录名就知道该放什么、该去哪找。逐个说明:

  • apis/接口层。所有和后端通信的请求都集中在这里管理,而不是散落在各个组件里。index.ts 里通常会创建并配置 axios 实例(设置 baseURL、超时、请求/响应拦截器等),再按业务模块拆分成 user.tsword.ts 等。这样接口地址变了、要统一加 token、要统一处理错误时,只改这一层即可。
  • assets/静态资源。图片、全局样式(如 base.css)、字体等。(注意与 public/ 的区别:assets 里的资源会被构建工具处理和打包;public 里的原样输出——上一篇讲过。)
  • components/全局通用组件。那些跨页面复用、不属于某个具体业务页面的组件放这(如上一篇的登录弹窗 Login)。
  • hooks/自定义组合式函数。以 use 开头、封装可复用逻辑的函数(如上一篇的 useLogin)。
  • layout/布局层。放「页面的整体框架骨架」,即那些每个页面都共用的结构(顶栏、侧边、内容区外壳)。里面又细分:
    • Header/:顶栏。
    • Content/:主体内容区(内部藏着第二层路由出口 RouterView,见第九节)。
    • Profile/:用户信息区。
    • index.vue:布局入口,把上面这些拼成完整布局。
  • router/路由配置。定义「URL ↔ 页面组件」的映射关系。
  • stores/状态管理。Pinia 的各个 store(如用户 store)。
  • views/业务页面。真正对应各个路由的「页面级组件」(主页、词库页、AI 页等)。它和 components/ 的区别views 是「页面」(和路由一一对应,是路由的落点);components 是「零件」(被页面或布局组合使用,不直接对应路由)。
  • App.vue根组件,整棵组件树的顶点。

一句话记忆views 装页面、components 装零件、layout 装骨架、apis 管接口、stores 管状态、hooks 管复用逻辑、router 管跳转映射。职责单一、各就各位。


七、编写 Header 组件

Header 是顶栏(layout/Header/index.vue),包含 Logo、导航菜单、积分显示、用户头像。

7.1 完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
    <header class="flex items-center h-20 border-b border-gray-200 justify-center sticky top-0 bg-white z-10">
        <div class="w-[1200px] mx-auto flex items-center justify-between">
            <div
                class="text-2xl font-bold bg-indigo-700 text-white rounded-[10px] px-2 py-1 w-10 flex items-center justify-center h-10 ">
                E</div>
            <div class="text-2xl font-bold">English App</div>
            <div @click="router.push('/')"
                class="flex items-center gap-2 cursor-pointer rounded-[10px] px-2 py-1 text-gray-500">
                <el-icon><HomeFilled /></el-icon> <span>主页</span>
            </div>
            <div @click="router.push('/smart/chat')" class="flex items-center gap-2 cursor-pointer text-gray-500">
                <el-icon><MagicStick /></el-icon> <span>AI</span>
            </div>
            <div @click="router.push('/word-book/index')" class="flex items-center gap-2 cursor-pointer text-gray-500">
                <el-icon><Notebook /></el-icon> <span>词库</span>
            </div>
            <div @click="router.push('/courses/index')" class="flex items-center gap-2 cursor-pointer text-gray-500">
                <el-icon><Reading /></el-icon> <span>课程</span>
            </div>
            <div @click="router.push('/setting/index')" class="flex items-center gap-2 cursor-pointer text-gray-500">
                <el-icon><Setting /></el-icon> <span>设置</span>
            </div>
            <div class="flex items-center gap-2 bg-blue-200 text-blue-700 rounded-full px-2 py-1">
                <el-icon><Sunny /></el-icon> <span class="font-bold text-sm">{{ 0 }}</span>
            </div>
            <div class="flex items-center gap-2 bg-amber-200 text-amber-700 rounded-full px-2 py-1">
                <el-icon><Star /></el-icon> <span class="font-bold text-sm">{{ 0 }}</span>
            </div>
            <div class="flex items-center gap-2 border-l cursor-pointer border-gray-200 pl-4">
                <img class="w-10 h-10 rounded-full ml-2 mr-2"
                    src="https://gips3.baidu.com/it/u=3493347002,3356558679&fm=3074&app=3074&f=PNG?w=2048&h=2048" />
                <span class="text-sm font-bold">{{ '未登录' }}</span>
            </div>
        </div>
    </header>
</template>

<script setup lang="ts">
import { Sunny, Star, HomeFilled, Notebook, MagicStick, Reading, Setting } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
const router = useRouter()
</script>

7.2 脚本部分

1
2
3
import { Sunny, Star, HomeFilled, Notebook, MagicStick, Reading, Setting } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
const router = useRouter()
  • 引入图标:从 @element-plus/icons-vue 按需引入用到的图标组件(HomeFilled 主页、MagicStick 魔法棒代表 AI、Notebook 笔记本代表词库、Reading 阅读代表课程、Setting 设置、Sunny 太阳、Star 星星)。引入后就能在模板里当组件用。
  • useRouter():拿到路由器实例,用于编程式导航(在代码里跳转,而不是点链接)。

7.3 <el-icon> 图标容器

1
<el-icon><HomeFilled /></el-icon>
  • el-icon 是什么:Element Plus 提供的图标容器组件。它本身不是具体图标,而是一个「包裹层」,负责给里面的图标统一控制尺寸、颜色、对齐等。
  • 为什么图标要套一层 el-icon@element-plus/icons-vue 里的图标本质是 SVG 组件。直接用可能大小/颜色不好控制、和文字对齐也不方便。套上 el-icon 后,Element Plus 会帮你处理好这些(默认继承字体大小和颜色、垂直居中对齐文字),用起来更省心、更统一。
  • 用法就是把具体图标组件作为默认插槽放进去:<el-icon><具体图标 /></el-icon>

7.4 编程式导航 router.push

1
2
<div @click="router.push('/')">...主页...</div>
<div @click="router.push('/smart/chat')">...AI...</div>
  • @click="...":Vue 的事件绑定(@clickv-on:click 的简写),点击时执行里面的表达式。
  • router.push('/xxx')编程式导航——用代码控制页面跳转到指定路由。这里点哪个导航项就 push 到对应路径。
  • router.push vs <router-link>(两种导航方式):
    • <router-link to="/xxx">声明式导航,写在模板里,渲染成 <a> 标签,适合「就是个链接」的场景。
    • router.push('/xxx')编程式导航,写在 JS 里,适合「需要在逻辑中跳转」的场景(如登录成功后跳首页、点击某个 div 跳转)。这里因为导航项是 <div> 而非链接、且是点击触发,所以用 router.push
  • router.push 的其他形式:除了传字符串路径,还能传对象 router.push({ name: 'home' })(按路由名跳)、router.push({ path: '/user', query: { id: 1 } })(带查询参数)。相关的还有 router.replace()(替换当前历史记录,不留返回)、router.back()(后退)等。

7.5 关键 Tailwind 类详解

<header> 上的类

1
flex items-center h-20 border-b border-gray-200 justify-center sticky top-0 bg-white z-10
  • flex:弹性布局。
  • items-center:交叉轴(垂直方向)居中——让 header 内元素垂直居中。
  • justify-center:主轴(水平方向)居中——让里面那个 1200px 宽的容器整体水平居中。
  • h-20:高度 5rem(80px)。Tailwind 的间距单位里 1 = 0.25rem = 4px,所以 20 = 80px。
  • border-b:只加下边框(bottom border)。border-gray-200:边框颜色浅灰。合起来是顶栏底部一条浅灰分割线。
  • sticky top-0粘性定位(重点)。position: sticky + top: 0 的组合意思是:正常情况下 header 跟着页面走,但当页面向下滚动、header 即将滚出视口顶部时,它会「粘」在顶部 top: 0 的位置不动了。效果就是顶栏始终吸附在页面顶部可见(常见的「滚动时导航栏固定」效果)。
  • bg-white:白色背景(很重要——粘住后如果没背景色,滚动时下面的内容会透过来叠在一起)。
  • z-10z-index: 10,层叠层级。保证 header 浮在下方内容之上(滚动时内容不会盖住吸顶的 header)。

sticky vs fixed 的区别fixed 是「彻底脱离文档流、死死钉在视口某处」;sticky 是「平时正常在文档流里,滚到临界点才吸附」。做吸顶导航用 sticky top-0 更自然(它会占位、不会像 fixed 那样把下面内容顶上去遮住)。

② 内层容器

1
<div class="w-[1200px] mx-auto flex items-center justify-between">
  • w-[1200px]:固定宽度 1200px([] 任意值语法)。这是定宽居中布局——内容限制在 1200px 内,两侧留白,大屏上不至于拉得太宽。
  • mx-automargin-left: auto; margin-right: auto水平居中这个定宽容器(左右外边距自动均分)。
  • flex items-center justify-between:内部弹性布局、垂直居中;justify-between 让子元素「两端对齐、间隔均分」——Logo 靠左、用户头像靠右,中间导航项均匀分布。

③ Logo 方块

1
<div class="text-2xl font-bold bg-indigo-700 text-white rounded-[10px] px-2 py-1 w-10 h-10 flex items-center justify-center">E</div>
  • text-2xl:字号 1.5remfont-bold:加粗。
  • bg-indigo-700:靛蓝背景。text-white:白字。
  • rounded-[10px]:圆角 10px。
  • px-2 py-1:水平内边距 0.5rem、垂直内边距 0.25remp = padding,x/y 分别是水平/垂直)。
  • w-10 h-10:宽高各 40px,做成一个正方形。
  • flex items-center justify-center:让里面的字母「E」水平垂直都居中。

④ 导航项通用类

1
<div class="flex items-center gap-2 cursor-pointer text-gray-500">
  • flex items-center:让图标和文字在一行垂直居中排列。
  • gap-2:flex/grid 子元素之间的间距 0.5rem(图标和文字之间留点空)。gap 比用 margin 更省心。
  • cursor-pointer:鼠标悬停时变成「手型」指针,提示这个 div 可以点击(因为它不是原生 <a>/<button>,默认不会有手型,需要手动加)。
  • text-gray-500:文字灰色。

⑤ 积分徽标(药丸形)

1
2
3
<div class="flex items-center gap-2 bg-blue-200 text-blue-700 rounded-full px-2 py-1">
  <el-icon><Sunny /></el-icon> <span class="font-bold text-sm">{{ 0 }}</span>
</div>
  • rounded-full完全圆角(半径极大),把矩形变成「胶囊/药丸」形状。配合内边距形成一个圆润的徽标。
  • bg-blue-200 text-blue-700:浅蓝底 + 深蓝字(一种协调的配色手法:同色系,背景浅、文字深)。旁边那个用 bg-amber-200 text-amber-700(琥珀/橙色系)同理。
  • text-sm:小号字。
  • {{ 0 }}:Vue 的插值语法(双大括号),把里面的表达式的值渲染成文本。这里暂时写死 0(占位),将来会替换成从 store 里取的真实数据(如打卡天数、单词数)。

⑥ 用户头像区

1
2
3
4
<div class="flex items-center gap-2 border-l cursor-pointer border-gray-200 pl-4">
  <img class="w-10 h-10 rounded-full ml-2 mr-2" src="https://..." />
  <span class="text-sm font-bold">{{ '未登录' }}</span>
</div>
  • border-l:只加左边框(left border),border-gray-200 浅灰。pl-4:左内边距 1rem。合起来是在头像区左侧加一条竖分割线并留出间距,把它和前面的导航区分隔开。
  • <img> 上的 w-10 h-10 rounded-full:40×40 的圆形头像rounded-full 把方图裁成圆)。ml-2 mr-2:左右外边距。
  • {{ '未登录' }}:同样是插值,暂时写死「未登录」占位,登录后会换成用户名。

本组件目前是「静态占位」版:积分 {{ 0 }}、用户名 {{ '未登录' }} 都是写死的。后续接入 Pinia 用户 store 后,会把它们换成真实的响应式数据({{ userStore.user?.dayNumber }}{{ userStore.user?.name ?? '未登录' }} 之类)。


八、路由配置

路由采用模块化拆分:每个业务模块(home、word-book……)单独一个路由文件,最后在 router/index.ts 里合并。

8.1 layout 布局组件(index.vue)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- layout/index.vue -->
<template>
  <Header />
  <Content />
</template>

<script setup lang="ts">
import Header from "./Header/index.vue";
import Content from "./Content/index.vue";
</script>
  • layout 是所有页面共用的「布局外壳」,由 Header(顶栏)和 Content(内容区)组成。
  • 关键layout 自己不含 RouterView,它只是把 Header 和 Content 拼起来。真正的「二级路由出口」藏在 Content 组件内部(见第九节)。
  • 因为它是每个页面都要的外壳,所以在路由里被设为父路由的 component,各个页面作为它的 children

8.2 Home 路由(静态引入)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// router/home/index.ts
import layout from '@/layout/index.vue'
import Home from '@/views/Home/index.vue'

export default [
  {
    path: '/',
    component: layout,      // 父路由用 layout 布局
    children: [
      {
        path: "/",
        component: Home     // 子路由渲染 Home 页面
      }
    ]
  }
]
  • 这个文件 export default 一个路由记录数组(本模块的路由)。
  • 结构是「一个父路由 + children 子路由」:父路由 path: '/'layout 布局,子路由也是 '/' 渲染 Home。访问 / 时,先渲染 layout,再在其内部渲染 Home。
  • 注意 Home 是「静态引入」import Home from '@/views/Home/index.vue' 在文件顶部直接 import。这意味着 Home 组件的代码会被打包进首屏的主包、应用一启动就加载。

8.3 WordBook 路由(动态引入 / 懒加载)——重点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// router/word-book/index.ts
import layout from '@/layout/index.vue'

export default [
  {
    path: '/word-book',
    component: layout,
    children: [
      {
        path: "index",
        component: () => import('@/views/WordBook/index.vue')  // 动态引入
      }
    ]
  }
]
  • 结构和 home 一样(父 layout + 子页面),但子路由的 component 写法不同
    • Home:component: Home(静态,顶部 import 好的组件)。
    • WordBook:component: () => import('@/views/WordBook/index.vue')动态引入,一个返回 import() 的箭头函数)。

什么是「动态引入 / 路由懒加载」?

  • () => import('...') 是一个函数,里面用了 动态 import()。动态 import 返回一个 Promise,会在被调用时才去异步加载那个模块。
  • 在路由里,component 传这样一个函数,Vue Router 就会等真正访问这个路由时,才去下载对应组件的代码,而不是一开始就全部下载。
  • 构建工具(Vite)看到动态 import,会把这个组件单独拆成一个 JS 文件(chunk),实现代码分割(code splitting)

为什么 Home 用静态、WordBook 用动态?(原文的关键点)

  • Home 是「一进来就展示」的首屏页面。它反正一开始就要用,懒加载没意义(甚至反而多一次请求延迟)。所以直接静态引入,打进主包。
  • WordBook 不是首屏必需——用户不一定会点词库。如果也打进主包,会白白增大首屏体积、拖慢首屏加载。用动态引入后,词库的代码被单独拆出去,只有用户真的点进词库时才下载。这样能减轻首屏加载负担,首屏更快。

一句话总结策略首屏必用的页面 → 静态引入(打进主包,秒开);非首屏、按需访问的页面 → 动态引入懒加载(拆包,用到才下载,减小首屏体积)。这是前端性能优化里非常常用的手段。

8.4 router/index.ts 汇总

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { createRouter, createWebHistory } from 'vue-router'
import home from './home/index'
import wordBook from './word-book/index'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    ...home,     // 主页
    ...wordBook  // 单词本
  ],
})

export default router

8.5 createRouter 与 createWebHistory

1
2
3
4
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [ ... ],
})
  • createRouter(配置):Vue Router 4(配 Vue3)创建路由实例的函数。配置对象里最核心的两项是 history(路由模式)和 routes(路由表)。
  • history: createWebHistory(...):指定路由的历史模式createWebHistory 表示使用 HTML5 History 模式(下一节详解)。
  • import.meta.env.BASE_URL
    • import.meta.envVite 提供的环境变量入口。BASE_URL 是应用部署的「基础路径」(在 vite 配置的 base 决定,默认是 /)。
    • 把它传给 createWebHistory,是为了让路由知道应用挂在什么根路径下。如果你的应用不是部署在域名根目录(比如部署在 https://xxx.com/app/ 下),改了 base,路由才能正确工作。默认 / 时就是根目录。

8.6 History 模式 vs Hash 模式

Vue Router 有两种主要的历史模式:

createWebHistory(History 模式,本项目用)createWebHashHistory(Hash 模式)
URL 形式https://xxx.com/word-book/indexhttps://xxx.com/#/word-book/index
好看程度干净、无 ##,略丑
原理用 HTML5 History API(pushState)改 URL用 URL 里 # 后面的 hash 部分
服务器配置需要后端配合:把所有路径都指回 index.html,否则刷新子路由会 404不需要额外配置(# 后面的内容不会发给服务器)
  • createWebHistory 的注意点:因为 URL 是真实路径(没有 #),用户在 /word-book/index 刷新页面时,浏览器会拿这个路径去请求服务器。而这是个单页应用,服务器上并没有 /word-book/index 这个真实文件,会返回 404。解决办法是在部署时配置服务器(Nginx、Vercel 等),让所有未匹配的路径都返回 index.html,交给前端路由处理。这是 History 模式部署时最常见的坑,务必记住。
  • Hash 模式没这个问题(# 后面的部分浏览器不会发给服务器),但 URL 带 # 不够美观。现代项目多用 History 模式 + 服务器配置。

8.7 嵌套路由 children

1
2
3
4
5
6
7
{
  path: '/word-book',
  component: layout,
  children: [
    { path: 'index', component: () => import('@/views/WordBook/index.vue') }
  ]
}
  • children 让路由形成一棵,而不是一份平铺的列表。父路由(layout)下面挂子路由(具体页面)。
  • 路径拼接规则:子路由的 path 不以 / 开头时是「相对路径」,会拼在父路径后面。这里父 path: '/word-book' + 子 path: 'index' → 实际访问路径是 /word-book/index
  • 如果子路由 path / 开头(像 home 里子路由写的 '/'),则是绝对路径,不拼接父级。(home 里父子都是 '/',所以最终就是 /。)
  • 为什么要嵌套? 因为多个页面(Home、WordBook)共享同一个外壳(layout:Header + Content),把 layout 作为父、页面作为 children,就能实现「外壳固定、内容区随路由切换」——这正是第九节要深入讲的 RouterView 嵌套机制。

8.8 展开运算符合并路由

1
2
3
4
routes: [
  ...home,     // 展开 home 模块导出的数组
  ...wordBook  // 展开 word-book 模块导出的数组
]
  • homewordBook 各自 export default 的是数组
  • ...(展开运算符) 把数组里的元素「摊平」拼进新数组。[...home, ...wordBook] 就是把两个模块的路由记录合并成一份完整的 routes 列表。
  • 好处路由模块化。每个业务模块的路由独立在自己的文件里维护,新增一个模块只需:新建该模块路由文件 → 在 index 里 import 并 ... 进 routes。互不干扰、清晰可扩展。想象一下如果所有路由都堆在一个文件里,几十个页面会非常难维护。

九、RouterView 与嵌套路由渲染机制(重点)

这一节把「路由是怎么一层层渲染出来的」彻底讲清楚。理解它,你就明白为什么切换页面时顶栏 Header 一直不动、只有内容区在换

9.1 路由表长什么样

1
2
3
router/index.ts
├── home/index.ts       → { path: '/',         component: layout, children: [{ path: '/',    component: Home }] }
└── word-book/index.ts  → { path: '/word-book', component: layout, children: [{ path: 'index', component: WordBook }] }

两个模块各自导出一个「路由记录数组」,在 router/index.ts 里用展开运算符 ...home...wordBook 合并成一份完整的 routes 列表,交给 createRouter 生成路由实例。

关键点每一条路由记录都可以有 children,形成一棵,而不是一份平铺列表。layout 是所有页面共用的父级,Home/WordBook 是挂在它下面的子路由。

9.2 RouterView 是「按层级对应」的插槽

先建立核心概念<RouterView /> 是一个占位插槽,它的作用是「在这里渲染当前路由匹配到的组件」。而当路由有嵌套(children)时,就需要多层 RouterView——组件树里有几层嵌套的 <RouterView/>,路由匹配到的链路就能渲染几层。

本项目是两层:

1
2
3
App.vue          ── 第 1 层 <RouterView/>   ← 渲染「父路由」匹配到的组件
  └─ layout       (Header + Content)
       └─ Content ── 第 2 层 <RouterView/>  ← 渲染「子路由」匹配到的组件

特别注意一个容易误解的点layout 本身不含 RouterView,它只是布局壳(Header + Content)。真正的第二层插槽藏在 Content 组件内部。所以视觉上看起来像是「layout 里有 RouterView」,实际上是它的子组件 Content 代为持有这个二级路由出口。

也就是说,Content 组件内部大致长这样:

1
2
3
4
5
6
<!-- layout/Content/index.vue -->
<template>
  <main class="w-[1200px] mx-auto">
    <RouterView />   <!-- 这就是第 2 层路由出口子页面在这里渲染 -->
  </main>
</template>

而根组件 App.vue 里是第一层:

1
2
3
4
<!-- App.vue -->
<template>
  <RouterView />   <!--  1 层路由出口父路由layout在这里渲染 -->
</template>

层级对应关系(务必记住)

  • App.vue 里的 RouterView → 渲染匹配链的第 0 层(父路由 layout)。
  • Content 里的 RouterView → 渲染匹配链的第 1 层(子路由,如 Home / WordBook)。

有几层嵌套路由,就要在对应的组件里放几层 RouterView,一一对应。

9.3 匹配过程:一次性解析整条链,而不是逐层触发

这是理解嵌套路由最关键的一点:访问一个 URL 时,vue-router 不是「先匹配第一层、渲染出来,再去找第二层该渲染什么」这种「走一步看一步」的方式;而是先把整条匹配链路一次性算出来,再自上而下渲染。

以访问 /word-book/index 为例,完整过程:

  1. router 遍历路由表,找到 path: '/word-book' 这条记录,前缀匹配上了。
  2. 继续在它的 children 里找剩余路径 index,找到 { path: 'index', component: WordBook }
  3. 得到匹配链[layout, WordBook]这个数组的顺序,就对应 RouterView 的嵌套层级
  4. 渲染时(自上而下):
    • 第 1 层 RouterView(在 App.vue)拿链路里的第 0 个 → 渲染 layout
    • layout 渲染出 Header + Content
    • 第 2 层 RouterView(在 Content 里)拿链路里的第 1 个 → 渲染 WordBook

访问 / 的情况完全一样,只是匹配链是 [layout, Home]

重点结论不存在「先展示 layout、过一会儿才补上 WordBook」的先后顺序——两层是在同一次渲染里一起确定、一起挂载的。router 是先把「这条 URL 要渲染哪几层、分别是什么组件」整条算清楚,再一次性把整棵子树渲染出来。

为什么强调这点? 很多人误以为嵌套路由是「父先渲染、触发子再渲染」的链式过程,从而在时序上产生错误理解(比如担心「子路由拿不到父的数据」「会闪一下」)。实际上匹配是一次性解析全链,渲染是同步的整体渲染,心里要有这个正确模型。

9.4 为什么切页面时 Header 不会闪烁 / 重建

这是嵌套布局最实用的好处,也是原理的直接体现。

/ 跳到 /word-book/index 时:

  • 旧匹配链:[layout, Home]
  • 新匹配链:[layout, WordBook]

对比两条链,第 0 层组件都是同一个 layout(同一个组件定义的引用)。Vue Router 发现「父层组件没变,只是子层变了」,于是只会:

  • 保留 layout 组件实例不动(连同它里面的 Header 一起,实例不销毁、不重建)。
  • 只把 Content 内部第 2 层 RouterView 渲染的内容Home 换成 WordBook

这就是为什么点击词库按钮,Header 始终存在、只有内容区在切换——本质是同一棵组件树,只重新渲染发生变化的那一层插槽,而不是整个页面推倒重来。

带来的实际收益:

  • 性能好:Header(及其内部状态、事件监听、可能的定时器)不会被反复销毁重建。
  • 体验好:顶栏不闪烁、不跳动,滚动位置等也能保持。
  • 状态保持:如果 Header 里有本地状态(如某个下拉菜单的展开状态),切页面时不会丢失。

延伸:如果连子组件也想在切换时「保活」(不销毁、保留状态),可以用 <KeepAlive>RouterView 包起来:<RouterView v-slot="{ Component }"><KeepAlive><component :is="Component" /></KeepAlive></RouterView>。这样被切走的页面组件会被缓存而非销毁,再切回来时保持原状。本项目未用,了解即可。

9.5 一张图总结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
URL: /word-book/index
router 匹配路由表,一次性得到链路: [ layout , WordBook ]
App.vue  <RouterView/> ①────────► 渲染 layout
                              ┌──────┴───────┐
                              ▼              ▼
                          <Header/>      <Content/>
                                        <RouterView/> ②───────► 渲染 WordBook

① 对应链路第 0 层,② 对应链路第 1 层,两层同时由一次路由匹配结果驱动

  • 换页面(//word-book/index)时,① 处的 layout 不变(Header 稳如泰山),只有 ② 处从 Home 换成 WordBook
  • 这套「父布局固定 + 子内容随路由切换」的结构,是中后台管理系统、多页应用里最经典、最常用的布局方案。

十、知识点速查总结

依赖与工具

名称一句话说明
pnpm快、省磁盘(硬链接共享)、依赖严格(防幽灵依赖)的包管理器
pnpm add xxx安装包并写入 dependencies
tailwindcss原子化 CSS 框架,HTML 里直接组合工具类写样式
@tailwindcss/viteTailwind v4 官方 Vite 插件(配置极简)
three / @types/three3D 图形库 / 它的 TS 类型声明包
@types/xxxDefinitelyTyped 为纯 JS 库补充的类型声明
axiosPromise 版 HTTP 请求库,支持拦截器
@microsoft/fetch-event-sourceSSE 流式(支持 POST/自定义头),做 AI 打字机效果
markedMarkdown 转 HTML,渲染 AI 返回内容(注意 XSS)
element-plus / icons-vueVue3 UI 组件库 / 官方图标包
pinia-plugin-persistedstatePinia 状态持久化到 localStorage,刷新不丢

配置相关

知识点一句话说明
vite.config.ts + defineConfigVite 配置文件,defineConfig 提供类型提示
plugins: [tailwindcss()]注册 Vite 插件(记得加括号调用)
@import 'tailwindcss'v4 引入 Tailwind(放最前面)
@apply在 CSS 里套用 Tailwind 工具类
html,body,#app { h-full w-full }撑满视口高度链,整页布局前置设置
副作用导入 import 'xxx.css'只执行文件(加载样式),不导入变量
import router from './router'引入目录默认找其 index.ts
ElementPlus 中文引入 zhCnapp.use(ElementPlus,{locale:zhCn})
"types":["element-plus/global"]让 el- 组件有全局类型提示(会覆盖自动加载)

Vue 应用启动

知识点一句话说明
createApp(App)用根组件创建应用实例
createPinia() + pinia.use(...)创建状态库并装插件(先装插件再交给 app)
app.use(插件, 选项)注册插件,可链式;第二参为配置
app.mount('#app')挂载到页面,必须最后执行
启动套路createApp → 建实例/装插件 → 多个 use → mount

路由核心

知识点一句话说明
createRouter({history, routes})创建路由实例
createWebHistory()History 模式,URL 无 #部署需服务器回退到 index.html
createWebHashHistory()Hash 模式,URL 带 #,无需服务器配置
import.meta.env.BASE_URLVite 环境变量,应用基础路径
children 嵌套路由路由成树,父布局 + 子页面
子 path 拼接不以 / 开头相对拼父级;以 / 开头是绝对路径
...home 合并路由展开运算符合并各模块路由,实现模块化
静态引入 component: Home打进主包,首屏必用页面用它
动态引入 () => import(...)懒加载/代码分割,非首屏页面用它减小首屏体积
router.push('/x')编程式导航(代码里跳转)
<router-link to>声明式导航(模板里的链接)
<RouterView />路由出口插槽,按层级渲染匹配链对应组件
嵌套渲染机制一次性解析整条匹配链,自上而下同步渲染
父层不变则复用切页面时同一 layout 实例保留,只换变化的子层(Header 不闪)
<KeepAlive>缓存被切走的组件、保活状态(延伸)

Tailwind 常用类(本篇出现)

含义
flex / items-center / justify-center / justify-between弹性布局与对齐
gap-2子元素间距 0.5rem
h-20 / w-10 / h-10高 80px / 宽高 40px(1=4px)
w-[1200px] / rounded-[10px]任意值(方括号语法)
mx-auto水平居中定宽容器
sticky top-0粘性定位,滚动吸顶
z-10层叠层级
border-b / border-l下 / 左 单边边框
rounded-full完全圆角(药丸 / 圆形头像)
cursor-pointer手型指针(给非链接元素加可点提示)
bg-blue-200 / text-blue-700浅底深字同色系配色
px-2 py-1 / pl-4 / ml-2内边距 / 单向内边距 / 外边距

几个「为什么」的重点回顾

  1. 为什么先清理脚手架? 去掉示例干扰、明确起点;清理注意引用先后顺序。
  2. 为什么 three 要另装 @types/three 它是纯 JS 库,需要单独的类型声明才有 TS 提示。
  3. 为什么 AI 对话用 fetch-event-source 而非原生 EventSource? 需要 POST + 自定义头 + 鉴权,原生不支持。
  4. 为什么先给 pinia 装插件再交给 app? 插件要在 store 使用前就绪,顺序不能反。
  5. 为什么 app.mount 放最后? 要等所有插件 use 完,应用配置齐全再渲染上屏。
  6. 为什么 Home 静态、WordBook 动态引入? 首屏必用的静态打进主包秒开;非首屏的懒加载减小首屏体积。
  7. 为什么 History 模式刷新会 404? URL 是真实路径会请求服务器,需配置服务器回退到 index.html。
  8. 为什么切页面 Header 不闪? 嵌套匹配链父层 layout 不变,实例复用,只重渲染变化的子层。

comments powered by Disqus
记录学习与研究