☰
Vue3+TypeScript练手项目全流程:从零到完整跑通
2026/9/29 13:41:44 网站建设 项目流程

上手 Vue3 和 TypeScript,最尴尬的处境是什么?教程刷了十来遍,文档翻了几大页,网上 demo 也都跑通了,可一旦关掉教程自己想从头搭一个项目,立刻不知道该从哪下手。尤其是 TypeScript,单独学一遍觉得还行,一放进 Vue3 组件里各种类型报错直接把心态搞崩。这篇文章是我用一个 Vue3 + TypeScript 练手项目从零到完整跑通的全程记录,包括项目该做什么、工具链怎么搭、TS 在组件里到底怎么用、实际开发踩了哪些坑,以及练完之后怎么往真实项目升级。内容基本是我实际敲过的代码和踩过的坑,适合刚学完 Vue3 基础、想用 TS 完整做一个项目的新手,也适合写过几个 demo 但没系统梳理过类型写法的同学参考。

1. 练手项目先想清楚:做什么才不算白练

1.1 一个合格的练手项目,要覆盖哪些技术点

练手项目最常见的误区是求大求全。我见过有人第一次上手就直接模仿中后台管理系统,又是权限模块又是多租户,最后写了两个多月还没完成首页。练手项目的核心不是功能多,而是把一条正常的业务闭环跑通。什么叫正常业务闭环?用户进来能登录、能看到数据列表、能搜索、能新增编辑、能删除,退出登录之后路由能拦住未授权访问。这几件事串起来,你对 Vue3 组合式 API、TS 类型约束、Pinia 状态流、路由守卫的理解才是"用过",而不是停留在"看过"。

具体到我这个练手项目,功能清单砍到最后就剩六块:用户登录、路由守卫、列表页搜索分页、新增编辑表单、删除与详情、全局状态管理。另加一个不算复杂的前置业务场景——我选的是"团队待办",因为它的数据模型简单,只有用户、任务、状态、时间,不会像商城那样陷入商品规格、购物车、支付回调的泥潭。这个选择很重要:练手项目的业务复杂度必须压到刚好能体现技术要点,而不是让业务本身成为学习障碍。

我一般建议,项目周期控制在三到四周。第一周做环境搭建和登录模块,第二周做列表、搜索、分页和表单,第三周做状态管理和细节打磨,第四周复盘、补注释、尝试加一个你没写过的小功能,比如"任务拖拽排序"或"数据导出"。按每天两到三小时算,这个节奏比较现实。一开始就把范围定死,每天都能看见进度,这是坚持写完一个练手项目的关键。

1.2 技术栈选型:Vue3 与 TypeScript 为什么适合组合

前端圈一直有 React 和 Vue 的口水战,但对练手来说,选 Vue3 + TS 有几个实打实的好处。第一,Vue3 的组合式 API 天然是函数式组织逻辑,这正好落在 TypeScript 的强项上。选项式 API 里 this 的类型推断在复杂组件里经常绕弯子,而 setup 函数里一切都是变量和函数,props、emit、返回值全走签名,TS 的推导基本不费劲。我自己从 Vue2 加 class component 转过来之后,最大的感受就是"原来 TS 和 Vue 可以这么顺"。

第二,Vue3 官方的 defineProps、defineEmits、ref、computed 都对类型做了第一等支持,不需要一大堆装饰器或者第三方辅助库。第三,生态层面 Element Plus、Naive UI、Vant 这些主流组件库全是 TS 编写,类型是第一公民;相比之下 Vue2 时代想要在组件库里拿到完整类型,很多时候依赖社区补丁,坑相当多。

这里也要说句实在话:如果你连 JavaScript 的数组方法、对象解构、Promise 都还没太搞明白,先别急着上 TypeScript。TS 是 JS 的超集,不是替代品,它的类型标注本身会引入大量新概念,基础不稳的时候会加倍痛苦。我遇到过不少新人,类型报错其实是业务代码写错了,但他以为是 TS 太难。所以推荐顺序是:先能熟练写简单 JS 业务,再上 Vue3 + TS 练手。

1.3 项目目录设计:一开始就把结构立好

很多新手拿到官方脚手架之后,第一反应是把所有东西都塞进 App.vue 或者 views 底下的一个大文件里。这个我特别理解,因为初期写代码最快。但练手项目一定要从第一天就按"功能模块 + 文件职责"来组织目录,不然后面加功能的时候会非常痛苦。

我用的目录结构是这样的:

src/ api/ # 接口定义与请求函数 assets/ # 静态资源 components/ # 通用组件 composables/ # 组合式函数 layouts/ # 布局组件 router/ # 路由配置与守卫 stores/ # Pinia 状态 types/ # 全局类型定义 utils/ # 工具函数 views/ # 页面级组件

这个结构不是标准答案,但它的原则是对的:状态放 stores,接口放 api,类型定义独立放 types,通用逻辑抽成 composables,页面只负责组装。练手阶段就养成这个习惯,等做真实项目的时候会发现收益巨大。反过来如果前期全堆在一起,重构成本会让你直接放弃。

2. 环境准备与项目初始化:把工具链吃透

2.1 Node、包管理器与编辑器选择

开始之前先把环境校准。Vue3 + Vite 项目建议 Node 18 以上,我当前用的 Node 20 LTS,npm 10;你可以用 nvm 管理多个 Node 版本,避免不同项目互相干扰。包管理器方面我推荐 pnpm,理由很直接:Vite 官方脚手架默认支持 pnpm,而且 pnpm 的依赖隔离机制能帮你少踩不少依赖版本错乱的坑。如果你之前只用过 npm,也完全可以从 pnpm 开始,命令几乎没有学习成本,npm install 变成 pnpm install,npm run dev 变成 pnpm dev。

编辑器方面自然是 VS Code,装两个扩展就够了:Volar(现在叫 Vue Language Features)和 TypeScript Vue Plugin。装完之后需要在 VS Code 里把默认格式化工具设置成 Prettier,否则一会儿走 ESLint 一会儿走 Prettier 会很乱。这里有个小坑:如果你同时开了 Volar 的 Takeover 模式,记得把内置 TypeScript 扩展禁用,否则类型提示可能重复或者行为怪怪的。

我先给一条验证环境的命令:

node -v pnpm -v

能正常打印版本号就说明基础环境没问题。如果这里就报错,大概率是没装 Node 或者环境变量没配好,先用搜索引擎查一下对应系统怎么配置环境变量,再继续后面的步骤。

2.2 create-vue 脚手架:每个选项都代表一个决策

初始化项目我直接用官方脚手架,命令是:

pnpm create vue@latest

为什么不用老的 vue-cli?Vue3 官方已经把精力全部放到了 Vite 工具链上,vue-cli 对 Vue3 的支持基本是维护模式,新手没必要学一套即将过时的工具。create-vue 的优点是在交互式命令行里把所有工程化选项问清楚:TypeScript、JSX、Router、Pinia、Vitest、ESLint、Prettier、Vue DevTools。很多人一看到这么多选项就全选,或者全不选,这两个极端都不对。

我的建议是:TS 必选,Router 和 Pinia 选上,ESLint 和 Prettier 选上,Vitest 如果短期内不会写测试可以先不选,JSX 先不选。为什么不选 JSX?因为 Vue3 的模板语法和 TS 配合已经足够好,JSX 是另一套心智模型,练手阶段叠加太多写法反而是负担。注意,这里的选择不是终身的,后面想加测试随时可以装 vitest,但一开始保持精简能让入口不至于太复杂。

创建完项目进入目录,安装依赖,跑起来:

cd vue3-ts-todo pnpm install pnpm dev

浏览器打开 http://localhost:5173,能看到 Vue 官方 logo 页面就算成了。这个时候你会注意到一个细节:脚手架默认生成的内容里带了一些类型声明的示例文件,比如 HelloWorld.vue 里有 defineProps 的类型写法,把它读一遍比看十篇文章都有用。

2.3 初始化后的目录与工具链分工

初始化完成后,工程目录大概是这样的,我整理成了示意:

.vscode/ # 编辑器统一配置 src/ assets/ components/ router/ stores/ views/ App.vue main.ts .env.development # 环境变量示例 .gitignore index.html package.json README.md tsconfig.json tsconfig.app.json tsconfig.node.json vite.config.ts

这个工程里几个文件的分工要搞清楚:入口是 index.html,它通过 script 标签加载 src/main.ts;main.ts 负责创建 Vue 实例、挂载 Router 和 Pinia;Vite 负责开发服务器和打包,配置在 vite.config.ts;TypeScript 的编译配置拆成了三个 tsconfig,其中 tsconfig.app.json 管 src 下的业务代码,tsconfig.node.json 管 vite.config.ts 这类构建脚本。这个拆分是官方推荐的,不要随便删。

我特别提醒一下 tsconfig 里的 paths 配置。很多人初始化之后想用 @/ 这种别名,结果发现不生效,因为 vite.config.ts 里的 alias 和 tsconfig.json 里的 paths 必须同时配置,而且路径要对应。比如:

// vite.config.ts resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } // tsconfig.json 或 tsconfig.app.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }

不过这里有个新变化,我放到后面踩坑章节详细讲,因为 TypeScript 新版对 baseUrl 已经提出弃用警告了。

3. Vue3 + TypeScript 的类型实践:不是装饰,是护栏

3.1 script setup 下的组件类型:props、emit 与暴露

我在练手项目里最常用的组件写法是<script setup lang="ts">,它会自动把顶层的变量和方法暴露给模板,配合 TS 的类型推导非常舒服。先看 props 的类型定义,官方推荐直接使用泛型语法:

<script setup lang="ts"> interface Props { title: string count?: number status: 'todo' | 'done' | 'doing' } const props = defineProps<Props>() // 需要默认值时用 withDefaults withDefaults(defineProps<Props>(), { count: 0 }) </script>

defineProps 的泛型写法有编译期检查,模板里用到不存在的 prop 会直接报错,这比运行时检查前置太多了。defineEmits 同理:

const emit = defineEmits<{ (e: 'update:title', value: string): void (e: 'delete', id: number): void }>()

这里给事件参数加了类型,父组件监听事件时也能拿到类型提示。defineExpose 则用于子组件向父组件暴露方法,配合模板 ref 获取子组件实例的时候类型是完整的,不会再像以前那样拿到一个 any。

3.2 ref、reactive、computed:类型推导与显式标注的边界

组合式 API 的三个核心响应式 API 在 TS 下表现不太一样,理解它们的推断规则能少写很多多余的标注。ref 接收基础值时会自动推导,比如const name = ref('')得到Ref<string>;但如果初始值是空数组或者空对象,你会得到一个不太理想的类型:

// 不推荐:可能被推断成 never[],后面 push 会报错 const list = ref([]) // 推荐:显式标注泛型 interface TodoItem { id: number title: string done: boolean } const list = ref<TodoItem[]>([])

reactive 则用在对象类型上,它会递归地把所有嵌套属性变成响应式,TS 的推导一般可以直接用,但如果你需要定义接口,还是显式标注更清晰。computed 是三者里最省心的,返回值类型会自动推断,基本不需要手动写,除非你要导出一个复杂类型的纯函数。

这里还有一个高频坑:当你从接口拿数据,返回结构是ApiResponse<T>,但你只需要 data 字段,新手经常写const data = res.data as any,这等于把类型护栏拆了。正确做法是先定义接口:

interface ApiResponse<T> { code: number message: string data: T } interface LoginResult { token: string userInfo: UserInfo }

然后请求函数返回Promise<ApiResponse<LoginResult>>,在使用处直接拿到res.data就是LoginResult类型。这个设计能让你从后端数据到页面渲染全链路都有类型,这也是我接下来要专门讲接口封装的原因。

3.3 接口请求封装里的 TS:泛型才是灵魂

练手项目里如果每个页面都直接调用 axios,你会很快发现两件事:一是错误处理代码重复,二是类型全靠手写。正确姿势是做一层薄封装,把所有请求的返回结构统一约束。我的封装思路是这样:

// src/utils/request.ts import axios from 'axios' import type { AxiosInstance, AxiosRequestConfig } from 'axios' export interface ApiResponse<T = unknown> { code: number message: string data: T } const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) service.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( (response) => { const res = response.data as ApiResponse if (res.code !== 200) { return Promise.reject(new Error(res.message)) } return response }, (error) => { return Promise.reject(error) } ) export function request<T>(config: AxiosRequestConfig): Promise<T> { return service.request<ApiResponse<T>>(config).then((response) => response.data.data) }

然后每个接口文件都走这个 request:

// src/api/auth.ts import { request } from '@/utils/request' import type { LoginParams, LoginResult } from '@/types/auth' export function login(data: LoginParams) { return request<LoginResult>({ url: '/auth/login', method: 'post', data }) }

这么做的好处是:业务代码里调用 login 后拿到的返回值天然是 LoginResult,不需要再手写类型断言,也不会因为接口改了字段产生静默错误。

3.4 全局类型声明:env.d.ts 与 types 目录的边界

很多新手会把所有类型都写在 main.ts 或者组件里,这会导致类型文件膨胀且难维护。我的习惯是:业务实体的类型放在 src/types 目录下,按模块拆文件,比如 auth.ts、todo.ts;面向全局的类型声明放在 d.ts 文件里,用 declare global 扩展。

比如 Vite 环境变量默认不被 TS 识别,你直接写import.meta.env.VITE_API_BASE_URL会报错,需要在src/vite-env.d.ts里声明:

/// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_BASE_URL: string } interface ImportMeta { readonly env: ImportMetaEnv }

还有一类是给后端返回的数据模型声明,这部分不建议全用 any,哪怕刚开始写不准类型,也可以先建一个界面大致约束字段。至于什么时候用 any、什么时候应该精确类型,我的原则是:数据边界处尽量精确,内部临时变量可以用自动推导,但不要主动写 any。主动写 any 等于告诉 TS"这里不用你管",一旦数量多了,类型系统就形同虚设。

4. 练手项目核心功能实录:从登录页到 CRUD

4.1 登录页:表单校验、调用接口、保存状态

到了这个阶段,我假设你已经完成了环境和类型基础,接下来就真正开始写业务。登录页虽然简单,但它把表单、接口、状态管理、路由跳转几个环节串起来了,非常适合作为第一个完整功能。

我的登录页结构是:模板部分放表单,script setup 部分负责校验和提交逻辑。校验可以用 Element Plus 自带的 Form 规则,也可以用简单的 if 判断,练手阶段重要的是提交函数里的类型和状态流转:

const loginForm = reactive<LoginForm>({ username: '', password: '' }) const loading = ref(false) async function handleLogin() { if (!loginForm.username || !loginForm.password) { ElMessage.warning('请输入用户名和密码') return } loading.value = true try { const res = await loginApi(loginForm) userStore.setToken(res.token) userStore.setUserInfo(res.userInfo) router.push('/') } catch (e) { // 统一错误提示已经在请求拦截器里做了 } finally { loading.value = false } }

这里有一个很重要的细节:登录成功后 token 是放到 Pinia 还是 localStorage?我的答案是都放——Pinia 管应用内状态,localStorage 做持久化。刷新页面后,在入口重新从 localStorage 恢复 token。如果你只放内存,一刷新登录态就没了,路由守卫立刻把你弹回登录页。

4.2 路由守卫:未登录拦截与动态元信息

路由守卫是登录功能的另一半。练手项目至少要有全局前置守卫,逻辑是:没有 token、目标页需要认证就跳转登录页;有 token 但访问的是登录页就跳回首页。注意这里不要每次都从 localStorage 读 token,最好封装一个工具函数,或者在 store 里写 getter。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.path === '/login' && token) { next({ path: '/' }) } else { next() } })

我建议把 redirect 参数用上,登录成功后再跳回来,这对用户体验很重要。后面如果做权限系统,再扩展到"按角色动态生成路由",练手阶段先不要碰动态路由,那个复杂度不是新手该承受的。

4.3 列表页:搜索、分页、loading 状态机

列表页是练手项目中代码量最大的部分,因为要处理状态、请求、渲染三层。我需要强调一个我犯过很多次的错误:把 loading 只当成一个 boolean,结果接口报错的时候整个页面没有反馈。更好的做法是把请求状态设计成'idle' | 'loading' | 'success' | 'error'这样一个状态机,或者至少区分 loading 和 error。

搜索参数和分页参数建议用一个响应式对象统一管理:

const query = reactive({ keyword: '', status: '' as '' | 'todo' | 'done', page: 1, pageSize: 10, total: 0 }) async function fetchList() { loading.value = true try { const res = await getTodoList({ keyword: query.keyword, status: query.status || undefined, page: query.page, pageSize: query.pageSize }) list.value = res.items query.total = res.total } finally { loading.value = false } }

搜索按钮和分页组件的事件都调用 fetchList,注意分页时要把 page 和 pageSize 回写进 query。这里还有一个容易漏掉的细节:切换分页大小时应该把 page 重置为 1,否则你在第 5 页切成每页 50 条,接口可能会返回空列表。这个逻辑虽然小,但很影响体验。

4.4 表单页:v-model 类型、回显与提交

表单页的核心是 v-model 和类型对齐。Element Plus 的表单组件接收的 value 类型和你提交给接口的类型不一定完全一致,比如日期选择器返回的是数组,但你接口需要的是字符串范围。练手项目我建议在表单页维护一个独立的表单模型:

interface TodoFormModel { title: string description: string priority: 'low' | 'medium' | 'high' dueDate: string | null }

编辑场景还有"回显"问题:从列表页跳过来时带着 id,需要先请求详情再填充表单。这个时候要注意时序,页面 onMounted 里是异步的,模板不能一开始就访问 form.dueDate,否则会报错。最简单的方法是加一个 loaded 标志,数据回来后再渲染表单组件。很多新手遇到"编辑页空白"就是这个问题。

const loaded = ref(false) const form = reactive<TodoFormModel>({ ... }) onMounted(async () => { const id = route.params.id if (id) { const data = await getTodoDetail(Number(id)) Object.assign(form, data) } loaded.value = true })

提交函数则复用登录页的 loading 模式,成功后跳回列表并刷新。到这里,一个完整的登录加 CRUD 闭环就跑通了。

5. 新手必踩的坑:我整理了一份排查清单

5.1 TypeScript 版本升级带来的弃用警告

如果你用的是最新版 create-vue,并且 TypeScript 已经到 5.5 或更高,可能会在终端看到两条黄色警告:选项 baseUrl 已弃用并将停止在 TypeScript 7.0 中运行,以及选项 moduleResolution=node10 已弃用并将停止在 TypeScript 7.0 中运行。这不是你的代码有问题,而是老 tsconfig 写法正处在版本过渡期。

应对方法分两步。第一,moduleResolution 改成 bundler,因为 Vite 项目本身走 esbuild 打包,用 node10 这种老式解析方式是历史包袱。第二,去掉 baseUrl,直接依赖 paths 配合相对路径。我最后稳定下来的 tsconfig 核心配置长这样:

{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "bundler", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }

如果你仍然看到警告,先看一下是不是 create-vue 生成的配置模板还没更新,可以手动把 moduleResolution 调成 "bundler" 试试。注意,只有打包工具真正支持 bundler 语义才能这样改,Vite 项目完全没问题。

5.2 类型断言:能不用就不用,但数据边界例外

我在练手项目里看到最多的类型问题就是乱用 as any。比如从 localStorage 读出 JSON 再转对象,新手习惯写JSON.parse(str) as any,后面的所有代码都失守了。我的建议是:数据边界处定义好接口,让类型错误提早暴露。比如:

interface StoredUser { userId: number name: string avatar: string role: string } function getUserFromStorage(): StoredUser | null { const raw = localStorage.getItem('user') if (!raw) return null try { return JSON.parse(raw) as StoredUser } catch { return null } }

这里的 as StoredUser 是合理的,因为 JSON.parse 在 TS 里只能返回 any,边界处收窄一次是必要的。但不要在业务代码里到处 as,尤其是把类型 A 直接断言成类型 B,那几乎都是在掩盖设计问题。

5.3 响应式丢失、组件不更新与时序问题

练手阶段大家喜欢把接口请求直接写在 setup 顶层,期望页面加载就发请求。但 setup 执行时组件还没挂载,有些 DOM 相关操作会报错。正确做法是把请求放进 onMounted,或者在 setup 顶层请求但不要立即访问 DOM 相关引用。

另一个非常典型的坑是响应式丢失。从 reactive 对象或 Pinia store 里直接解构出来的属性,在 Vue3 里不会保持响应式。很多人写了const { userInfo } = userStore,然后页面怎么都不更新。正确做法是:

// 错误:解构后丢失响应式 const { userInfo } = userStore // 正确:用 storeToRefs 包裹 const { userInfo } = storeToRefs(userStore)

或者直接用userStore.userInfo,这也是响应式的。组件里如果用了 reactive 对象,同样不要直接解构,必要的时候用 toRefs 转换。

还有一类"组件不更新"问题,常见于文件上传组件的成功回调。比如你配置了 on-success 但始终监听不到,这通常不是组件库的 bug,而是你的请求拦截器把响应数据结构改了,成功回调拿到的参数结构和组件库预期不一致。排查思路是打日志看回调参数到底是什么,而不是怀疑组件库有问题。

5.4 第三方类型缺失与组件库类型导入

练手项目一般都会引入一个组件库,比如 Element Plus。正常来说它的类型是齐全的,但偶尔你会遇到"类型上不存在属性 xxx"这样的报错。这时候先检查是不是组件库版本和 Vue 版本不匹配,再看是不是导入路径写错了。Element Plus 的全局类型可以通过在 d.ts 里配置实现按需引入,不需要手动导入每个组件的类型。

另外一个常见困扰:某个老包没有提供 TypeScript 类型定义,import 进来直接 any 报错。处理方式是在项目里新建一个 d.ts 文件用 declare module 补上类型:

declare module 'some-js-lib' { export function init(options: { debug?: boolean }): void }

这属于"接口隔离"的思路,我不建议直接 suppress 或 ignore,因为你后面升级依赖时没有类型会很难受。

5.5 问题排查速查表

我把练手阶段最常见的几类问题汇总成一张表,方便你对照排查:

现象可能原因处理办法
终端出现 baseUrl 弃用警告TypeScript 5.5+ 开始标记旧配置移除 baseUrl,只保留 paths,并改成相对路径
终端出现 moduleResolution=node10 弃用警告老模板默认配置改成 node16 / nodenext,Vite 项目用 bundler
模板里读取不到 props没有用 defineProps 声明检查是否在 script setup 中声明了 props 类型
调用接口返回值没有类型提示请求函数缺少泛型用 request 泛型包装,定义响应接口
解构 store 后页面不更新响应式状态被抽离使用 storeToRefs 或直接访问 store 属性
编辑页回显空白异步回填时序问题加 loaded 标志,数据返回后再渲染表单
数组 push 报错ref 初始值为空数组导致 never[]显式标注 ref<TodoItem[]>([])
on-success 回调不到请求拦截器改了响应结构打印回调参数,调整与组件库的契约

这张表不是万能药,但它覆盖了新手练手项目里频率最高的几个点。遇到问题先对号入座,能省下不少排查时间。

6. 练手完成后,下一步往哪走

6.1 从练手项目到真实项目的差距在哪里

很多新人练完一个项目,觉得下一步就是接外包或者投简历,实际上中间还有不小的距离。练手项目的特点是需求明确、接口自造、没有团队协作,而真实项目至少要面对几个新问题:多人合并代码冲突、接口联调时的字段变动、产品修改需求导致的组件设计变化、旧浏览器兼容、性能优化、错误监控。这些能力不是靠写一个 demo 能获得的,但练手阶段养成的好习惯——类型完整、目录清晰、代码可读——恰恰是解决这些真实问题的底层能力。

如果你想更接近真实项目,可以把练手项目扩展:加一个 mock server 模拟真实接口延迟和失败,加一个单元测试覆盖核心逻辑,把它部署到服务器上用一个真实域名访问。这个过程会迫使你考虑环境变量、跨域、静态资源部署,这些在本地开发环境完全不会出现。

6.2 值得继续深入的方向:测试、工程化与组合式函数

练手项目跑通之后,我个人强烈建议先补 Vitest 单元测试,因为测试能反过来强迫你把代码拆得更合理。一个组件如果很难写测试,往往说明它做的事情太多,这是个很好的重构信号。其次是写自己的组合式函数,比如 useAuth、usePagination,把之前写在组件里的逻辑抽出来,你会发现组件代码一下子瘦身很多,而且对 Vue3 的"逻辑复用"会有更深的体会。

再往后,可以接触 VueUse 这个社区组合式函数库,看看别人怎么设计通用 hooks,但不要照抄,要理解每个函数背后的响应式原理。最后才是 SSR、微前端这些大工程方向,练手阶段不需要碰。

6.3 说点我自己的真实体会

最后分享一点个人经验。我用 Vue3 + TS 练手的时候,最大的瓶颈不是找不到资料,而是资料太多太散,今天看这个教程明天看那个 demo,结果一周下来一个完整功能都没写出来。后来我给自己定了一条规矩:一个练手项目只固定一套技术方案,遇到坑就查这一套方案的官方文档,不看其他分支方案的比较文章。事实证明这个做法非常高效,因为 Vue3 + TS 的方案在官方文档里足够完整,你缺的只是把时间花在写代码上。

写这个项目的过程中还有一个体会:类型报错不可怕,可怕的是不敢开 strict 模式。我一开始也是把严格模式关掉来绕过报错,后来发现那些报错恰恰是代码质量的提示。把 strict 打开,把每个报错都当作一次学习机会,练完一个项目之后你会发现自己对 TS 的理解上了一层。这个练手项目的代码我后来又重构过两遍,每次都有新收获,希望你能比我更快地走到"能用自己的项目去验证想法"那一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询