Vue3 技术博客——Part1




2020-06-03

blog_main_img

搭项目、写页面状态、拆组件、接路由、放 Pinia,再用 Python/FastAPI 做一个 mock 接口,让页面和后端数据跑通

Vue 3 写起来最舒服的地方,是它把“页面长什么样”和“页面怎么动”拆得很清楚:模板负责展示,`

count: {{ count }}

double: {{ doubleCount }}

加一下 这里有几个关键点: - `ref` 适合管理数字、字符串、布尔值这类简单状态 - 在 JavaScript / TypeScript 里访问 `ref` 要写 `.value` - 模板里会自动解包,所以直接写 `{{ count }}` - `computed` 适合写依赖状态计算出来的值 ## `ref`、`reactive`、`computed`、`watch` 怎么选 Vue 的响应式系统很强,但日常用起来可以先记这套:text ref:一个值 reactive:一个对象 computed:从状态推导出新值 watch:状态变化后做额外动作 ![Vue 3 Composition API 关系图](https://zoyblogs.oss-cn-guangzhou.aliyuncs.com/vue3_composition_mxpe.svg) 例子:vue 已完成 保存 `reactive` 的好处是对象字段读写比较自然,但不要随手解构它:ts const form = reactive({ title: 'Vue' }) const { title } = form 这样拿出来的 `title` 会失去响应式连接。需要解构时,可以用 `toRefs`:ts import { reactive, toRefs } from 'vue' const form = reactive({ title: 'Vue' }) const { title } = toRefs(form) ## 模板语法:别把页面写成字符串拼接 Vue 模板的核心就是几类指令。 条件渲染:vue

加载中

{{ error }}

内容已就绪

列表渲染:vue

  • {{ todo.title }}

属性绑定:vue提交 事件绑定:vue保存 表单绑定:vue 这些语法组合起来,足够覆盖大部分业务页面。 ## 组件通信:props 往下,emit 往上 组件拆分后,最常见的问题就是:数据怎么传,动作怎么回。 ![Vue 3 组件通信](https://zoyblogs.oss-cn-guangzhou.aliyuncs.com/vue3_component_qlsn.svg) 子组件 `TodoItem.vue`:vue

{{ todo.title }} 删除

父组件使用它:vue 这个方向很清楚:text 父组件通过 props 给子组件数据 子组件通过 emit 通知父组件发生了什么 子组件不要直接改父组件传来的对象。它应该告诉父组件“用户点了谁”,具体怎么改状态交给父组件处理。 ## `v-model` 也能用在组件上 如果子组件是一个输入框、弹窗、筛选器,`v-model` 会很舒服。 `SearchBox.vue`:vue 父组件:vue `v-model` 的本质就是:text modelValue 传入 update:modelValue 传出 写清楚这个规则,就不会被语法糖绕住。 ## Composable:把可复用逻辑抽出来 如果多个页面都要请求任务列表,可以把逻辑抽成 composable。 `src/composables/useTodos.ts`:ts import { computed, ref } from 'vue' export type Todo = { id: number title: string done: boolean } export function useTodos() { const todos = ref([]) const loading = ref(false) const error = ref('') const undoneCount = computed(() => { return todos.value.filter((todo) => !todo.done).length }) async function loadTodos() { loading.value = true error.value = '' try { const response = await fetch('/api/todos') if (!response.ok) { throw new Error('接口请求失败') } todos.value = await response.json() } catch (err) { error.value = err instanceof Error ? err.message : '未知错误' } finally { loading.value = false } } return { todos, loading, error, undoneCount, loadTodos, } } 页面里使用:vue

加载中

{{ error }}

还有 {{ undoneCount }} 个任务没完成

  • {{ todo.title }}

Composable 的价值是让组件变轻。组件只负责展示和触发动作,数据获取、派生状态、错误处理都放进函数里。 ## Vue Router:页面跳转交给路由 安装项目时如果选择了 Router,会看到 `src/router/index.ts`。 一个常见写法:ts import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: () => import('@/views/HomeView.vue'), }, { path: '/todos/:id', name: 'todo-detail', component: () => import('@/views/TodoDetailView.vue'), props: true, }, ], }) export default router 页面跳转:vue 查看详情 在脚本里跳转:ts import { useRouter } from 'vue-router' const router = useRouter() function goHome() { router.push({ name: 'home' }) } 读取路由参数:ts import { useRoute } from 'vue-router' const route = useRoute() const id = Number(route.params.id) 路由不只是“换页面”,它也是页面状态的一部分。比如筛选条件、分页页码、详情 id,都可以放在 URL 里。 ## Pinia:共享状态别到处传 父子组件传值适合局部场景。跨页面、跨组件共享的数据,更适合交给 Pinia。 `src/stores/todo.ts`:ts import { computed, ref } from 'vue' import { defineStore } from 'pinia' export type Todo = { id: number title: string done: boolean } export const useTodoStore = defineStore('todo', () => { const todos = ref([]) const doneTodos = computed(() => { return todos.value.filter((todo) => todo.done) }) async function loadTodos() { const response = await fetch('/api/todos') todos.value = await response.json() } function toggleTodo(id: number) { const todo = todos.value.find((item) => item.id === id) if (todo) { todo.done = !todo.done } } return { todos, doneTodos, loadTodos, toggleTodo, } }) 页面使用:vue

已完成:{{ doneTodos.length }}

{{ todo.title }} 这里有个细节:从 store 解构状态时,用 `storeToRefs`。这样状态仍然保持响应式。 ## 用 Python/FastAPI 做一个 mock 接口 前端写到接口请求时,可以用 Python 快速起一个小服务。 ![Vue 3 和 Python API 联调](https://zoyblogs.oss-cn-guangzhou.aliyuncs.com/vue3_python_api_rkdt.svg) 安装依赖:bash pip install fastapi "uvicorn[standard]" `api.py`:python from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel class Todo(BaseModel): id: int title: str done: bool = False app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["http://127.0.0.1:5173", "http://localhost:5173"], allow_credentials=True, allow_methods=[""], allow_headers=[""], ) todos = [ Todo(id=1, title="整理 Vue 组件", done=True), Todo(id=2, title="接入 Python 接口"), Todo(id=3, title="拆出 Pinia Store"), ] @app.get("/api/todos") def list_todos() -> list[Todo]: return todos @app.post("/api/todos") def create_todo(todo: Todo) -> Todo: todos.append(todo) return todo 启动接口:bash uvicorn api:app --reload --host 127.0.0.1 --port 8000 前端请求:ts const response = await fetch('http://127.0.0.1:8000/api/todos') const todos = await response.json() 开发联调时,也可以在 Vite 里配置代理,这样前端仍然请求 `/api/todos`。 `vite.config.ts`:ts import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, }, }, }) 这样 Vue 里直接写:ts const response = await fetch('/api/todos') 前端代码更干净,接口地址也不用到处复制。 ## 做一个完整 Todo 页面 `TodoView.vue`:vue

任务看板

未完成:{{ undoneCount }}

添加

加载中

{{ error }}

  • {{ todo.title }}

这个页面已经包含了常见业务页的核心动作: - 从接口加载数据 - 添加本地任务 - 切换完成状态 - 处理加载和错误提示 - 用计算属性展示未完成数量 ## 一些更像工程里的写法 接口请求建议统一封装:ts const API_BASE = import.meta.env.VITE_API_BASE || '' export async function request(url: string, options?: RequestInit): Promise { const response = await fetch(${API_BASE}${url}, { headers: { 'Content-Type': 'application/json', ...options?.headers, }, ...options, }) if (!response.ok) { throw new Error(请求失败:${response.status}) } return response.json() as Promise } 业务接口再单独写:ts import { request } from '@/utils/request' export type Todo = { id: number title: string done: boolean } export function fetchTodos() { return request('/api/todos') } 页面就只关心业务:ts todos.value = await fetchTodos() `` 这比在每个组件里直接写一长串fetch更容易维护。 ## 常见坑,顺手避开ref在脚本里别忘了.value。模板里能自动解包,不代表脚本里也能。reactive不要随便解构。需要解构就用toRefs。 组件里不要直接改 props。子组件应该发事件,父组件负责改状态。 列表渲染别用数组下标当key。数据会新增、删除、排序时,稳定 id 更可靠。 Pinia store 解构状态时用storeToRefs`。否则容易把响应式连接拆掉。 接口地址不要散落在组件里。配置代理或封装请求函数,后面改起来才轻。