05. Vue3生态实战:Router、Pinia与完整项目

Vue3生态实战:Router、Pinia与完整项目

Vue Ecosystem

恭喜你走到最后一期!前四期我们学习了ES6、Vite、Vue3基础和组合式API。现在,让我们把这些知识整合起来,构建一个完整的Vue3应用。

这一期,我们学习Vue Router(路由)和Pinia(状态管理),然后做一个实战项目。

Vue Router

安装

pnpm add vue-router

基础配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/users/:id',
    name: 'User',
    component: () => import('@/views/User.vue') // 懒加载
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
]

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

export default router
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')
<!-- App.vue -->
<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>
  <router-view />
</template>

路由参数

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

// 获取参数
console.log(route.params.id) // /users/123 → 123
console.log(route.query.page) // /users?page=2 → 2
</script>

编程式导航

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

function goToUser(id) {
  router.push({ name: 'User', params: { id } })
}

function goBack() {
  router.back()
}

function replace() {
  router.replace({ name: 'Home' }) // 不留历史记录
}
</script>

嵌套路由

const routes = [
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue'),
    children: [
      {
        path: '', // /settings
        component: () => import('@/views/SettingsProfile.vue')
      },
      {
        path: 'account', // /settings/account
        component: () => import('@/views/SettingsAccount.vue')
      },
      {
        path: 'security', // /settings/security
        component: () => import('@/views/SettingsSecurity.vue')
      }
    ]
  }
]
<!-- Settings.vue -->
<template>
  <div>
    <h1>Settings</h1>
    <nav>
      <router-link to="/settings">Profile</router-link>
      <router-link to="/settings/account">Account</router-link>
      <router-link to="/settings/security">Security</router-link>
    </nav>
    <router-view />
  </div>
</template>

导航守卫

// 全局前置守卫
router.beforeEach((to, from) => {
  // 需要登录
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// 全局后置守卫
router.afterEach((to) => {
  document.title = to.meta.title || 'My App'
})
// 路由元信息
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: 'Admin Dashboard'
    }
  }
]

Pinia

Pinia是Vue3官方推荐的状态管理库,比Vuex更简单、更强大。

安装

pnpm add pinia

基础配置

// src/stores/index.js
import { createPinia } from 'pinia'

const pinia = createPinia()
export default pinia
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'

const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')

定义Store

// src/stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 组合式API风格(推荐)
export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isLoggedIn,
    userName,
    login,
    logout
  }
})

使用Store

<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构state和getters(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

// actions直接解构
const { login, logout } = userStore

// 直接访问
console.log(userStore.user)
</script>

<template>
  <div v-if="isLoggedIn">
    Welcome, {{ userName }}
    <button @click="logout">Logout</button>
  </div>
  <div v-else>
    <button @click="login">Login</button>
  </div>
</template>

选项式API风格

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),

  getters: {
    doubleCount: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    },

    async fetchCount() {
      const response = await fetch('/api/count')
      this.count = await response.json()
    }
  }
})

持久化

// 使用pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: true // 启用持久化
})

实战项目:任务管理应用

让我们把学到的知识整合起来,做一个任务管理应用。

项目结构

src/
├── main.js
├── App.vue
├── router/
│   └── index.js
├── stores/
│   ├── index.js
│   ├── tasks.js
│   └── user.js
├── composables/
│   └── useLocalStorage.js
├── views/
│   ├── Home.vue
│   ├── Login.vue
│   └── Tasks.vue
└── components/
    ├── TaskItem.vue
    └── TaskForm.vue

任务Store

// src/stores/tasks.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTaskStore = defineStore('tasks', () => {
  const tasks = ref([
    { id: 1, title: 'Learn Vue3', completed: false },
    { id: 2, title: 'Build a project', completed: false }
  ])

  const filter = ref('all') // all, active, completed

  const filteredTasks = computed(() => {
    if (filter.value === 'active') {
      return tasks.value.filter(t => !t.completed)
    }
    if (filter.value === 'completed') {
      return tasks.value.filter(t => t.completed)
    }
    return tasks.value
  })

  const remainingCount = computed(() =>
    tasks.value.filter(t => !t.completed).length
  )

  function addTask(title) {
    tasks.value.push({
      id: Date.now(),
      title,
      completed: false
    })
  }

  function removeTask(id) {
    const index = tasks.value.findIndex(t => t.id === id)
    if (index > -1) {
      tasks.value.splice(index, 1)
    }
  }

  function toggleTask(id) {
    const task = tasks.value.find(t => t.id === id)
    if (task) {
      task.completed = !task.completed
    }
  }

  function clearCompleted() {
    tasks.value = tasks.value.filter(t => !t.completed)
  }

  return {
    tasks,
    filter,
    filteredTasks,
    remainingCount,
    addTask,
    removeTask,
    toggleTask,
    clearCompleted
  }
}, {
  persist: true
})

任务列表组件

<!-- src/views/Tasks.vue -->
<script setup>
import { useTaskStore } from '@/stores/tasks'
import { storeToRefs } from 'pinia'
import TaskItem from '@/components/TaskItem.vue'
import TaskForm from '@/components/TaskForm.vue'

const taskStore = useTaskStore()
const { filteredTasks, filter, remainingCount } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask, clearCompleted } = taskStore
</script>

<template>
  <div class="tasks">
    <h1>Tasks</h1>
    <TaskForm @add="addTask" />

    <ul class="task-list">
      <TaskItem
        v-for="task in filteredTasks"
        :key="task.id"
        :task="task"
        @toggle="toggleTask(task.id)"
        @remove="removeTask(task.id)"
      />
    </ul>

    <div class="filters">
      <button
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
      >
        All
      </button>
      <button
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
      >
        Active
      </button>
      <button
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
      >
        Completed
      </button>
    </div>

    <div class="footer">
      {{ remainingCount }} items left
      <button @click="clearCompleted">Clear completed</button>
    </div>
  </div>
</template>

<style scoped>
.tasks {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.filters button.active {
  font-weight: bold;
}

.task-list {
  list-style: none;
  padding: 0;
}
</style>

任务项组件

<!-- src/components/TaskItem.vue -->
<script setup>
defineProps({
  task: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['toggle', 'remove'])
</script>

<template>
  <li class="task-item" :class="{ completed: task.completed }">
    <input
      type="checkbox"
      :checked="task.completed"
      @change="emit('toggle')"
    />
    <span>{{ task.title }}</span>
    <button @click="emit('remove')">×</button>
  </li>
</template>

<style scoped>
.task-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-item.completed span {
  text-decoration: line-through;
  color: #999;
}
</style>

任务表单组件

<!-- src/components/TaskForm.vue -->
<script setup>
import { ref } from 'vue'

const title = ref('')

const emit = defineEmits(['add'])

function handleSubmit() {
  if (title.value.trim()) {
    emit('add', title.value.trim())
    title.value = ''
  }
}
</script>

<template>
  <form class="task-form" @submit.prevent="handleSubmit">
    <input
      v-model="title"
      type="text"
      placeholder="What needs to be done?"
    />
    <button type="submit">Add</button>
  </form>
</template>

<style scoped>
.task-form {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.task-form input {
  flex: 1;
  padding: 10px;
  font-size: 16px;
}
</style>

项目优化

代码分割

// router/index.js
const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue')
  }
]

懒加载组件

<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('@/components/HeavyComponent.vue')
)
</script>

构建优化

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
})

系列总结

恭喜你完成了现代前端开发到Vue3的完整学习之旅!

第一期:ES6+、Node.js、npm基础
第二期:Vite构建工具
第三期:Vue3响应式和组件
第四期:组合式API和逻辑复用
第五期:Router、Pinia和实战项目

你现在掌握了:

  • 现代JavaScript语法
  • Vite项目构建
  • Vue3核心概念
  • 组合式API和组合式函数
  • Vue Router路由管理
  • Pinia状态管理
  • 完整项目开发流程

下一步

  • 学习TypeScript(Vue3的最佳伴侣)
  • 探索UI框架(Element Plus、Naive UI)
  • 学习服务端渲染(Nuxt 3)
  • 阅读官方文档(https://vuejs.org/

进阶资源


恭喜!你已经掌握了现代前端开发到Vue3的核心技能。去构建你的下一个项目吧!

Views: 15

04. Vue3组合式API:代码组织的艺术

Vue3组合式API:代码组织的艺术

Composition API

如果你写过大型Vue2项目,一定遇到过这样的困扰:

  • 一个组件几千行,data、methods、computed分散各处
  • 想复用一段逻辑,只能用mixin,然后命名冲突
  • TypeScript支持不完善,类型推断总有问题

组合式API(Composition API)就是为了解决这些问题而生的。它让你按功能组织代码,而不是按选项组织。

选项式API vs 组合式API

选项式API(Options API)


export default {
  data() {
    return {
      count: 0,
      user: null
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    fetchUser() {
      // ...
    }
  },
  mounted() {
    this.fetchUser()
  }
}

问题:

  • 相关代码分散在不同选项中
  • 逻辑复用困难(mixin有命名冲突)
  • TypeScript类型推断不好

组合式API(Composition API)


import { ref, computed, onMounted } from 'vue'

// 计数器逻辑
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
  count.value++
}

// 用户逻辑
const user = ref(null)
async function fetchUser() {
  user.value = await fetch('/api/user').then(r => r.json())
}
onMounted(fetchUser)

优势:

  • 相关代码放在一起
  • 逻辑可以提取成独立函数
  • 完美的TypeScript支持

setup函数

``是setup函数的语法糖:

<!-- 等价于 -->

export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}

<!-- 语法糖 -->

const count = ref(0)

所有顶层绑定自动暴露给模板。

Props和Emits


// 定义props
const props = defineProps({
  title: String
})

// 定义emits
const emit = defineEmits(['update'])

// 使用
console.log(props.title)
emit('update', data)

响应式转换

Vue3提供响应式语法糖(实验性):


// 启用后,ref可以省略.value
let count = $ref(0)
count++  // 自动处理

// 解构保持响应性
const { x, y } = $(useMouse())

组合式函数

组合式函数(Composables)是Vue3复用逻辑的核心方式。

基础示例

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}
<!-- 使用 -->

import { useCounter } from './useCounter'

const { count, doubleCount, increment } = useCounter(10)

  <p>Count: {{ count }}</p>
  <p>Double: {{ doubleCount }}</p>
  <button>+1</button>

实用组合式函数

useMouse - 追踪鼠标位置:

import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

useFetch - 数据获取:

import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  async function fetch() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url))
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 支持响应式URL
  watchEffect(fetch)

  return { data, error, loading, refetch: fetch }
}

import { useFetch } from './useFetch'

const { data, loading, error } = useFetch('/api/users')

  <div>Loading...</div>
  <div>Error: {{ error.message }}</div>
  <div>
    <ul>
      <li>
        {{ user.name }}
      </li>
    </ul>
  </div>

useLocalStorage - 本地存储:

import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(
    data,
    (newVal) => {
      if (newVal === null) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newVal))
      }
    },
    { deep: true }
  )

  return data
}

import { useLocalStorage } from './useLocalStorage'

const theme = useLocalStorage('theme', 'light')

  <button>
    Toggle Theme
  </button>

provide/inject

跨层级组件通信:

<!-- 祖先组件 -->

import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
})
<!-- 后代组件(任意层级) -->

import { inject } from 'vue'

const theme = inject('theme')
const toggleTheme = inject('toggleTheme')

  <div>
    <button>Toggle</button>
  </div>

响应式provide

// 使用Symbol避免命名冲突
export const ThemeKey = Symbol('theme')

// 提供默认值
const theme = inject(ThemeKey, 'light')

// 只读
provide(ThemeKey, readonly(theme))

模板引用


import { ref, onMounted } from 'vue'

const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
})

v-for中的ref


const listRef = ref([])

function setRef(el) {
  if (el) {
    listRef.value.push(el)
  }
}

  <div>
    {{ item }}
  </div>

组件ref


import ChildComponent from './Child.vue'

const childRef = ref(null)

function callChildMethod() {
  childRef.value.someMethod()
}

异步组件


import { defineAsyncComponent } from 'vue'

// 异步加载
const AsyncComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 带加载状态
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

最佳实践

命名约定

// 组合式函数以use开头
useCounter()
useMouse()
useFetch()

// 私有函数以_开头或使用__
function _internalHelper() {}

返回值

// 返回响应式引用
export function useCounter() {
  const count = ref(0)
  return { count }  // 不要返回 count.value
}

// 使用时保持响应性
const { count } = useCounter()
// count是ref,仍然是响应式的

清理副作用

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))

  // 或者返回清理函数
  return () => target.removeEventListener(event, callback)
}

// 使用
const cleanup = useEventListener(window, 'resize', handleResize)
cleanup()  // 手动清理

参数处理

// 支持ref和原始值
import { toValue } from 'vue'

export function useFetch(url) {
  // toValue会解包ref
  const finalUrl = toValue(url)

  watchEffect(() => {
    fetch(toValue(url))  // 响应式URL
  })
}

// 使用
const url = ref('/api/users')
const { data } = useFetch(url)  // url变化时自动重新fetch

小结

你现在掌握了:

  • 组合式API的核心思想(按功能组织代码)
  • 组合式函数的编写和使用
  • provide/inject跨层级通信
  • 模板引用和异步组件
  • 最佳实践和命名约定

下一期,我们进入Vue3生态,学习Vue Router路由和Pinia状态管理,完成一个实战项目。


练习任务

  1. 编写一个useCounter组合式函数
  2. 编写一个useDebounce防抖函数
  3. 使用provide/inject实现主题切换
  4. 实现一个异步加载的组件

下期预告:《Vue3生态:Router、Pinia与实战项目》—— 路由怎么配?状态怎么管?来做一个完整项目!

Views: 12

03. Vue3基础:响应式系统和组件入门

Vue3基础:响应式系统和组件入门

Vue Logo

终于进入Vue3的世界了!这一期,我们学习Vue3最核心的概念:响应式系统和组件。

理解这两个概念,你就掌握了Vue3的80%。

响应式系统

什么是响应式?

// 普通变量 - 不是响应式的
let count = 0
count++
// Vue不知道count变了,UI不会更新

// 响应式变量 - Vue会追踪变化
const count = ref(0)
count.value++
// Vue知道count变了,自动更新UI

响应式 = 数据变化时,UI自动更新。

ref vs reactive

Vue3提供两种响应式API:

ref:包装单个值

import { ref } from 'vue'

const count = ref(0)
const message = ref('Hello')
const user = ref({ name: 'Vue' })

// 读取和修改需要.value
console.log(count.value)  // 0
count.value++

reactive:包装对象/数组

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Vue'
  }
})

// 直接访问属性
console.log(state.count)  // 0
state.count++

// 数组
const list = reactive([1, 2, 3])
list.push(4)

怎么选?

场景 推荐
基本类型(数字、字符串) ref
对象 都可以
需要整体替换 ref
解构友好 reactive

我的建议:默认用ref,对象很大时用reactive

模板中自动解包


import { ref } from 'vue'

const count = ref(0)

  <!-- 不需要.value,自动解包 -->
  <p>Count: {{ count }}</p>
  <button>+1</button>

响应式丢失问题

// ❌ 解构会丢失响应性
const state = reactive({ count: 0 })
let { count } = state  // count是普通数字
count++  // 不会触发更新!

// ✅ 使用toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)  // count是ref
count.value++  // 会触发更新

// ✅ 使用toRef(单个属性)
import { toRef } from 'vue'
const count = toRef(state, 'count')

computed

计算属性:基于响应式数据派生新值,自动缓存。


import { ref, computed } from 'vue'

const firstName = ref('Vue')
const lastName = ref('3')

// 只读计算属性
const fullName = computed(() => <code>${firstName.value} ${lastName.value})

// 可写计算属性
const fullName2 = computed({
  get: () => ${firstName.value} ${lastName.value},
  set: (val) => {
    [firstName.value, lastName.value] = val.split(' ')
  }
})

  

{{ fullName }}

watch

侦听数据变化:


import { ref, watch, watchEffect } from 'vue'

const count = ref(0)

// watch:明确指定侦听源
watch(count, (newVal, oldVal) => {
  console.log(<code>count changed: ${oldVal} -> ${newVal})
})

// watch多个源
watch([count, anotherRef], ([newCount, newAnother]) => {
  // ...
})

// watchEffect:自动追踪依赖
watchEffect(() => {
  console.log(count is ${count.value})
})

// 立即执行 + 可停止
const stop = watchEffect(() => {
  // ...
}, { immediate: true })

// 停止侦听
stop()

watch vs watchEffect

  • watch:明确指定侦听源,可访问新旧值
  • watchEffect:自动追踪,适合副作用

组件基础

定义组件


import { ref } from 'vue'

const count = ref(0)

  <button>
    Clicked {{ count }} times
  </button>

button {
  color: blue;
}

``是Vue3的语法糖,自动暴露变量给模板。

Props:父传子

<!-- Child.vue -->

// 定义props
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 使用props
console.log(props.title)

  <h1>{{ title }}</h1>
  <p>Count: {{ count }}</p>
<!-- Parent.vue -->

import Child from './Child.vue'

Emits:子传父

<!-- Child.vue -->

const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: 'Vue' })
}

  <button>Update</button>
<!-- Parent.vue -->

import Child from './Child.vue'

function onUpdate(data) {
  console.log('Received:', data)
}

v-model:双向绑定

<!-- 自定义组件支持v-model -->

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

Vue3.4+可以用defineModel


const modelValue = defineModel()

插槽

<!-- Card.vue -->

  <div class="card">
    <div class="header">

    </div>
    <div class="content">

    </div>
    <div class="footer">

    </div>
  </div>
<!-- 使用 -->

    <h1>Title</h1>

  <p>Content goes here</p>

    <button>OK</button>

作用域插槽:

<!-- List.vue -->

const items = ['Apple', 'Banana', 'Orange']

  <ul>
    <li>

    </li>
  </ul>
<!-- 使用 -->

    {{ index + 1 }}. {{ item }}

模板语法

文本插值


  <p>{{ message }}</p>
  <p>{{ count + 1 }}</p>
  <p>{{ formatDate(date) }}</p>

指令


  <!-- 条件渲染 -->
  <div>Type A</div>
  <div>Type B</div>
  <div>Other</div>

  <!-- 列表渲染 -->
  <ul>
    <li>
      {{ item.name }}
    </li>
  </ul>

  <!-- 事件绑定 -->
  <button>Click</button>
  <button>+1</button>
  ...

  <!-- 属性绑定 -->
  <img />
  <div>...</div>
  <div>...</div>

  <!-- 双向绑定 -->

  ...

  <!-- 显示/隐藏 -->
  <div>...</div>

v-if vs v-show

特性 v-if v-show
渲染方式 条件为假时不渲染 始终渲染,用CSS隐藏
切换成本 高(销毁/重建) 低(只改CSS)
初始成本 低(不渲染) 高(总是渲染)

建议:频繁切换用v-show,条件很少改变用v-if

Class和Style绑定


  <!-- 对象语法 -->
  <div></div>

  <!-- 数组语法 -->
  <div></div>

  <!-- 混合 -->
  <div></div>

  <!-- Style对象 -->
  <div></div>

  <!-- Style数组 -->
  <div></div>

生命周期


import { onMounted, onUpdated, onUnmounted } from 'vue'

onMounted(() => {
  console.log('组件已挂载')
})

onUpdated(() => {
  console.log('组件已更新')
})

onUnmounted(() => {
  console.log('组件已卸载')
})

生命周期钩子:

选项式API 组合式API
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted

小结

你现在掌握了:

  • 响应式系统(ref、reactive、computed、watch)
  • 组件通信(props、emits、v-model、插槽)
  • 模板语法(指令、条件、循环)
  • 生命周期钩子

下一期,我们深入学习组合式API - Vue3最强大的特性。你会学会如何组织代码、复用逻辑、提取组合式函数。


练习任务

  1. 创建一个计数器组件,用ref实现
  2. 创建一个父子组件,通过props和emits通信
  3. 实现一个自定义v-model组件
  4. 使用作用域插槽渲染列表

下期预告:《Vue3组合式API:代码组织的艺术》—— 组合式函数怎么写?如何复用逻辑?

Views: 14

02. Vite快速入门:秒级启动的开发体验

Vite快速入门:秒级启动的开发体验

Vite Logo

如果你用过Webpack,一定经历过这样的痛苦:

  • npm run dev启动要等30秒
  • 改一行代码,热更新要等3秒
  • 配置文件写得想砸键盘

Vite就是为了终结这些痛苦而生的。它的名字来自法语"快"(vite),快到让你怀疑人生。

为什么Vite这么快?

传统打包工具的问题

Webpack的工作方式:

  1. 启动时,从入口文件开始
  2. 递归分析所有依赖
  3. 打包成一个(或几个)大文件
  4. 启动开发服务器

项目越大,依赖越多,打包越慢。

Vite的革命性做法

Vite利用了浏览器的原生ES模块能力:

开发环境

  1. 启动时不打包
  2. 浏览器请求哪个文件,就编译哪个文件
  3. 源码按需编译,秒级启动

生产环境
使用Rollup打包,优化输出

graph LR
    A[浏览器请求] --> B[Vite Dev Server]
    B --> C{已编译?}
    C -->|是| D[返回缓存]
    C -->|否| E[即时编译]
    E --> D

创建第一个Vite项目

# 创建Vue3项目
npm create vite@latest my-vue-app -- --template vue

# 或者用pnpm(推荐)
pnpm create vite my-vue-app --template vue

# 进入项目
cd my-vue-app

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

输出:

  VITE v5.1.0  ready in 234 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

234毫秒!这就是Vite的速度。

项目结构

my-vue-app/
├── index.html          # 入口HTML
├── package.json        # 项目配置
├── vite.config.js      # Vite配置
├── public/             # 静态资源(不处理)
│   └── favicon.ico
└── src/                # 源代码
    ├── main.js         # 入口文件
    ├── App.vue         # 根组件
    └── assets/         # 资源文件
        └── vue.svg

index.html

Vite把index.html作为入口,而不是main.js


    <title>Vite + Vue</title>

    <div id="app"></div>

注意type="module",这告诉浏览器用ES模块方式加载。

main.js

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

App.vue


import { ref } from 'vue'

const count = ref(0)

  <div>
    <h1>Vite + Vue</h1>
    <button>
      Count: {{ count }}
    </button>
  </div>

h1 {
  color: #42b883;
}

Vite配置

基础配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],

  // 开发服务器
  server: {
    port: 3000,          // 端口
    open: true,          // 自动打开浏览器
    host: '0.0.0.0',     // 允许局域网访问
    cors: true           // 启用CORS
  },

  // 构建配置
  build: {
    outDir: 'dist',      // 输出目录
    sourcemap: true      // 生成sourcemap
  }
})

路径别名

import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

使用:

import MyComponent from '@/components/MyComponent.vue'
// 而不是
import MyComponent from '../../../components/MyComponent.vue'

环境变量

# .env.development
VITE_API_URL=http://localhost:3000/api

# .env.production
VITE_API_URL=https://api.example.com

使用:

const apiUrl = import.meta.env.VITE_API_URL

注意:只有VITE_前缀的变量才会暴露给客户端代码。

代理API请求

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

CSS处理

CSS Modules


  <div>
    <h1>Hello</h1>
  </div>

.container {
  padding: 20px;
}
.title {
  color: blue;
}

预处理器

# 安装Sass
pnpm add -D sass

  <div class="container">
    <h1>Hello</h1>
  </div>

$primary-color: #42b883;

.container {
  padding: 20px;

  h1 {
    color: $primary-color;
  }
}

全局CSS

// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: <code>@import "@/styles/variables.scss";
      }
    }
  }
})

静态资源

引入方式

// 直接引入
import logo from '@/assets/logo.png'

// URL形式
const imgUrl = new URL('./assets/logo.png', import.meta.url).href

// 动态引入(Vite特有)
const modules = import.meta.glob('./assets/*.png')
// 返回: { './assets/a.png': () => import('./assets/a.png'), ... }

public目录

public/下的文件不会被处理,直接复制到输出目录:

<img src="/logo.png" />

访问:http://localhost:5173/logo.png

构建优化

代码分割

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
})

分析打包大小

pnpm add -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    visualizer({ open: true })
  ]
})

构建后会生成一个HTML文件,可视化显示每个模块的大小。

TypeScript支持

# 创建TS项目
pnpm create vite my-app --template vue-ts

Vite原生支持TypeScript,无需额外配置(但需要vue-tsc做类型检查)。

pnpm add -D vue-tsc
// package.json
{
  "scripts": {
    "build": "vue-tsc && vite build"
  }
}

常用插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'           // JSX支持
import vueDevTools from 'vite-plugin-vue-devtools'   // Vue DevTools
import compression from 'vite-plugin-compression'    // Gzip压缩
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    vueDevTools(),
    compression(),
    visualizer()
  ]
})

常见问题

Q: 为什么生产构建用Rollup?

A: Rollup生成的代码更干净、更小。开发时用原生ES模块追求速度,生产时用Rollup追求优化。

Q: 如何处理CommonJS模块?

A: Vite会自动转换,大多数情况无需处理。如果有问题:

export default defineConfig({
  optimizeDeps: {
    include: ['some-cjs-module']
  }
})

Q: 热更新不生效?

A: 检查文件是否被正确引入,确保没有语法错误。可以在浏览器控制台查看HMR状态。

Vite vs Webpack

特性 Vite Webpack
启动速度 ⚡ 秒级 🐢 项目大时慢
热更新 ⚡ 毫秒级 🐢 较慢
配置复杂度 📝 极简 😵 复杂
生态成熟度 🌱 快速发展 🌳 非常成熟
生产打包 Rollup Webpack

结论:新项目首选Vite,除非必须用Webpack生态的特定插件。

小结

你现在掌握了:

  • Vite为什么快(原生ES模块)
  • 创建和配置Vite项目
  • 路径别名、环境变量、代理
  • CSS处理(Modules、预处理器)
  • 静态资源引入
  • 构建优化技巧

下一期,我们正式进入Vue3基础。你会学到Vue3的核心概念:响应式、组件、模板语法。


练习任务

  1. 创建一个Vite + Vue3项目
  2. 配置@路径别名
  3. 配置开发服务器代理
  4. 使用Sass编写样式

下期预告:《Vue3基础:响应式系统和组件入门》—— ref和reactive怎么选?组件怎么通信?

Views: 20

01. 现代前端开发入门:从ES6到Vue3的第一步

现代前端开发入门:从ES6到Vue3的第一步

Modern Frontend

你刚决定学习前端开发,打开教程一看:ES6、Node.js、npm、Webpack、Vite、TypeScript...这些名词像天书一样砸过来。别慌,这篇文章帮你理清现代前端的"基础设施",为Vue3开发打下坚实基础。

现代前端开发三件套

在Vue3之前,你需要先掌握三个基础:

  1. ES6+语法 - 现代JavaScript的写法
  2. Node.js + npm - 包管理和构建工具的基础
  3. 模块化开发 - 代码组织方式

让我们一个个攻克。

ES6+必学语法

ES6(ECMAScript 2015)是JavaScript的重大升级,Vue3大量使用这些新特性。

let和const

// ❌ 旧写法
var name = 'Vue'
var name = 'React' // 可以重复声明,容易出bug

// ✅ 新写法
const app = 'Vue3' // 常量,不可重新赋值
let count = 0 // 变量,可以修改
count = 1 // OK
// app = 'React' // 报错!const不能重新赋值

规则:默认用const,需要修改时才用let,永远不用var

箭头函数

// ❌ 旧写法
const add = function(a, b) {
return a + b
}

// ✅ 新写法
const add = (a, b) => {
return a + b
}

// 更简洁:单行可以省略大括号和return
const add = (a, b) => a + b

// 单参数可以省略括号
const double = n => n * 2

关键区别:箭头函数没有自己的this,它会继承外层作用域的this。这在Vue中非常重要。

const obj = {
count: 0,
// ❌ 普通函数,this指向调用者
incrementOld: function() {
setTimeout(function() {
console.log(this.count) // undefined!
}, 1000)
},
// ✅ 箭头函数,this继承外层
incrementNew: function() {
setTimeout(() => {
console.log(this.count) // 0
}, 1000)
}
}

解构赋值

// 对象解构
const user = { name: 'Vue', version: 3, author: 'Evan' }
const { name, version } = user
console.log(name, version) // 'Vue' 3

// 重命名
const { name: framework } = user
console.log(framework) // 'Vue'

// 默认值
const { license = 'MIT' } = user
console.log(license) // 'MIT'

// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5]
console.log(first, second, rest) // 1 2 [3, 4, 5]

展开运算符

// 数组展开
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5]
console.log(arr2) // [1, 2, 3, 4, 5]

// 对象展开(浅拷贝)
const defaults = { theme: 'light', lang: 'zh' }
const settings = { ...defaults, theme: 'dark' }
console.log(settings) // { theme: 'dark', lang: 'zh' }

// 合并对象
const merged = { ...obj1, ...obj2 }

模板字符串

const name = 'Vue'
const version = 3

// ❌ 旧写法
const msg = 'Welcome to ' + name + ' ' + version

// ✅ 新写法
const msg = <code>Welcome to ${name} ${version}

// 多行字符串
const html = `

${name}

`

简写属性

const name = 'Vue'
const version = 3

// ❌ 旧写法
const app = {
name: name,
version: version,
sayHello: function() {
return 'Hello'
}
}

// ✅ 新写法
const app = {
name, // 等同于 name: name
version, // 等同于 version: version
sayHello() { // 方法简写
return 'Hello'
}
}

Promise和async/await

// Promise
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

// async/await(推荐)
async function getUsers() {
try {
const response = await fetch('/api/users')
const data = await response.json()
console.log(data)
} catch (error) {
console.error(error)
}
}

// 并行请求
async function getAll() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
])
return { users, posts }
}

数组方法

const numbers = [1, 2, 3, 4, 5]

// map:转换每个元素
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10]

// filter:筛选元素
const evens = numbers.filter(n => n % 2 === 0) // [2, 4]

// find:查找元素
const found = numbers.find(n => n > 3) // 4

// reduce:累积计算
const sum = numbers.reduce((acc, n) => acc + n, 0) // 15

// some/every:判断
const hasEven = numbers.some(n => n % 2 === 0) // true
const allPositive = numbers.every(n => n > 0) // true

// 链式调用
const result = numbers
.filter(n => n > 2)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0) // 120

可选链和空值合并

const user = {
profile: {
name: 'Vue'
// avatar 可能不存在
}
}

// ❌ 旧写法(容易报错)
const avatar = user && user.profile && user.profile.avatar

// ✅ 可选链
const avatar = user?.profile?.avatar // undefined,不会报错

// ❌ 旧写法
const name = user.name || 'Anonymous' // 空字符串也会用默认值

// ✅ 空值合并
const name = user.name ?? 'Anonymous' // 只有null/undefined才用默认值

Node.js和npm

什么是Node.js?

Node.js让JavaScript可以脱离浏览器运行。它主要用于:

  1. 运行构建工具(Vite、Webpack)
  2. 服务端开发(Express、Nest)
  3. 运行脚本

下载安装:https://nodejs.org/

npm基础命令

# 初始化项目
npm init -y

# 安装依赖
npm install vue # 安装到dependencies
npm install -D vite # 安装到devDependencies
npm install -g pnpm # 全局安装

# 版本管理
npm install vue@3 # 指定版本
npm install vue@latest # 最新版本

# 运行脚本
npm run dev # 开发服务器
npm run build # 构建生产版本

# 其他
npm update # 更新依赖
npm outdated # 检查过时依赖
npm audit # 安全检查

package.json

{
"name": "my-vue-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"vite": "^5.0.0"
}
}

pnpm:更快的替代品

# 安装pnpm
npm install -g pnpm

# 使用(命令基本相同)
pnpm install
pnpm add vue
pnpm dev

pnpm优点:

  • 更快(硬链接,节省磁盘空间)
  • 更严格(避免幽灵依赖)
  • 更安全(更好的依赖管理)

ES模块

import/export

// utils.js - 导出
export const PI = 3.14159

export function add(a, b) {
return a + b
}

export default class Calculator {
// ...
}

// main.js - 导入
import Calculator, { PI, add } from './utils.js'
import * as utils from './utils.js'

动态导入

// 懒加载
const module = await import('./heavy-module.js')

// Vue路由懒加载
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]

开发环境推荐

VSCode必备插件

  1. Vue - Official - Vue语法高亮和智能提示
  2. ESLint - 代码规范检查
  3. Prettier - 代码格式化
  4. Volar - Vue3专用(已集成到Vue - Official)

推荐配置

// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

小结

你现在掌握了:

  • ES6+核心语法(箭头函数、解构、展开、async/await)
  • Node.js和npm基础操作
  • ES模块的import/export
  • 开发环境配置

下一期,我们将学习Vite - Vue3官方推荐的构建工具。它快得让你怀疑人生,配置简单到让你感动流泪。


练习任务

  1. 用解构赋值提取对象属性
  2. 用map/filter/reduce处理数组
  3. 用async/await改写Promise链式调用
  4. 创建一个npm项目,安装vue依赖

下期预告:《Vite快速入门:秒级启动的开发体验》—— 为什么Vite这么快?如何配置Vite项目?

Views: 13