Vue 3 项目开发常用操作详细汇总

纯 JavaScript 版本 的 Vue 3 项目开发常用操作详细汇总,并补充了更完整的实际场景示例。


一、项目搭建与目录结构

1. 创建项目

bash 复制代码
npm create vue@latest my-project
# 交互选项:No TypeScript, Yes JSX, Yes Router, Yes Pinia

cd my-project
npm install
npm run dev

2. 推荐目录结构

复制代码
src/
├── api/              # 接口请求统一管理
├── assets/           # 静态资源(图片、样式)
├── components/       # 公共组件
│   ├── common/       # 通用基础组件
│   └── business/     # 业务组件
├── composables/      # 组合式函数(复用逻辑)
├── directives/       # 自定义指令
├── router/           # 路由配置
├── stores/           # Pinia 状态管理
├── utils/            # 工具函数
├── views/            # 页面级组件
├── App.vue
└── main.js

二、响应式系统(Composition API)

1. ref 与 reactive 的详细用法

javascript 复制代码
<script setup>
import { ref, reactive, computed } from 'vue'

// ===== ref:用于基本类型和需要替换整个对象的情况 =====
const count = ref(0)
const name = ref('张三')

// 修改 ref 必须通过 .value
const increment = () => {
  count.value++
}

const changeName = () => {
  name.value = '李四'
}

// ===== reactive:用于对象、数组(深层响应式,无需 .value)=====
const user = reactive({
  name: '王五',
  age: 25,
  address: {
    city: '北京',
    district: '朝阳区'
  },
  hobbies: ['读书', '游泳']
})

// 直接修改属性
const updateUser = () => {
  user.age = 26
  user.address.city = '上海'      // 深层对象自动响应
  user.hobbies.push('跑步')       // 数组方法自动响应
}

// 注意:不能整个替换 reactive 对象,会失去响应式
// ❌ 错误:user = { name: '赵六' }  
// ✅ 正确:Object.assign(user, { name: '赵六' })

// ===== computed:计算属性(缓存结果)=====
const doubleCount = computed(() => {
  return count.value * 2
})

// 可写的计算属性(用于 v-model)
const fullName = computed({
  get() {
    return user.firstName + ' ' + user.lastName
  },
  set(newVal) {
    const parts = newVal.split(' ')
    user.firstName = parts[0]
    user.lastName = parts[1] || ''
  }
})
</script>

<template>
  <div>
    <p>count: {{ count }}</p>
    <p>doubleCount: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    
    <p>用户名:{{ user.name }},年龄:{{ user.age }}</p>
    <p>城市:{{ user.address.city }}</p>
    <p>爱好:{{ user.hobbies.join('、') }}</p>
    <button @click="updateUser">更新用户</button>
  </div>
</template>

2. 侦听器 watch 与 watchEffect

javascript 复制代码
<script setup>
import { ref, watch, watchEffect } from 'vue'

const searchText = ref('')
const page = ref(1)
const user = reactive({
  name: '张三',
  profile: {
    age: 20
  }
})

// ===== watch:明确指定依赖,懒执行(初始不执行)=====
watch(searchText, (newVal, oldVal) => {
  console.log('搜索文本变化:', oldVal, '->', newVal)
  // 这里可以调用搜索接口
  fetchData()
})

// 监听多个源
watch([searchText, page], ([newText, newPage], [oldText, oldPage]) => {
  console.log('多个值变化:', newText, newPage)
})

// 深层监听对象(immediate 表示立即执行一次)
watch(
  () => user.profile.age,
  (newVal, oldVal) => {
    console.log('年龄变化:', oldVal, '->', newVal)
  },
  { immediate: true, deep: false }
)

// 深层监听整个对象(开销较大,慎用)
watch(
  () => user,
  (newVal) => {
    console.log('user 对象变化')
  },
  { deep: true }
)

// ===== watchEffect:自动追踪依赖,立即执行 =====
// 适合不需要旧值、自动收集依赖的副作用
watchEffect(() => {
  console.log('当前搜索文本:', searchText.value)
  console.log('当前页码:', page.value)
  // 只要里面用到的响应式变量变化,就会重新执行
})

// 清理副作用(比如取消未完成的请求)
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('延时操作:', searchText.value)
  }, 500)
  
  onCleanup(() => {
    clearTimeout(timer)  // 下次执行前清理上次的 timer
  })
})

const fetchData = () => {
  console.log('执行搜索:', searchText.value)
}
</script>

<template>
  <input v-model="searchText" placeholder="输入搜索内容" />
  <button @click="page++">下一页 {{ page }}</button>
  <input v-model.number="user.profile.age" placeholder="年龄" />
</template>

3. 生命周期钩子

javascript 复制代码
<script setup>
import { 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated, 
  onBeforeUnmount, 
  onUnmounted,
  onActivated,      // KeepAlive 缓存激活时
  onDeactivated     // KeepAlive 缓存停用时
} from 'vue'

const timer = ref(null)

// 挂载前
onBeforeMount(() => {
  console.log('组件即将挂载')
})

// 挂载完成(DOM 已可用)
onMounted(() => {
  console.log('组件已挂载')
  timer.value = setInterval(() => {
    console.log('定时器运行中...')
  }, 1000)
  
  // 可以操作 DOM
  // document.getElementById('box').style.color = 'red'
})

// 更新前
onBeforeUpdate(() => {
  console.log('组件即将更新')
})

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

// 卸载前
onBeforeUnmount(() => {
  console.log('组件即将卸载')
})

// 卸载完成(清理工作)
onUnmounted(() => {
  console.log('组件已卸载')
  clearInterval(timer.value)  // 清理定时器
  // 清理事件监听、WebSocket 连接等
})
</script>

三、组件开发详解

1. <script setup> 基础写法

javascript 复制代码
<!-- ChildComp.vue -->
<script setup>
import { ref } from 'vue'

// 组件内部状态
const localCount = ref(0)

// 暴露给父组件的方法和属性
defineExpose({
  localCount,
  reset() {
    localCount.value = 0
  }
})
</script>

<template>
  <div class="child">
    <p>子组件计数:{{ localCount }}</p>
    <button @click="localCount++">子组件+1</button>
  </div>
</template>

2. Props 与 Emits 完整示例

javascript 复制代码
<!-- UserCard.vue -->
<script setup>
import { computed } from 'vue'

// ===== Props 定义 =====
const props = defineProps({
  // 基础写法
  title: {
    type: String,
    required: true,
    default: '默认标题'
  },
  // 多种类型
  userId: [String, Number],
  // 对象类型
  userInfo: {
    type: Object,
    default: () => ({ name: '匿名', age: 0 })
  },
  // 数组类型
  tags: {
    type: Array,
    default: () => []
  },
  // 自定义校验
  status: {
    type: String,
    validator(value) {
      return ['active', 'inactive', 'pending'].includes(value)
    }
  }
})

// 在 setup 中使用 props(不要解构,会丢失响应式)
const displayTitle = computed(() => {
  return props.title.toUpperCase()
})

// ===== Emits 定义 =====
const emit = defineEmits(['update', 'delete', 'change-status'])

const handleUpdate = () => {
  // 传多个参数
  emit('update', props.userId, { name: '新名字' })
}

const handleDelete = () => {
  emit('delete', props.userId)
}

const handleChangeStatus = (newStatus) => {
  emit('change-status', newStatus)
}
</script>

<template>
  <div class="user-card">
    <h3>{{ displayTitle }}</h3>
    <p>姓名:{{ userInfo.name }}</p>
    <p>年龄:{{ userInfo.age }}</p>
    <p>标签:{{ tags.join(', ') }}</p>
    <p>状态:{{ status }}</p>
    
    <button @click="handleUpdate">更新</button>
    <button @click="handleDelete">删除</button>
    <button @click="handleChangeStatus('active')">激活</button>
  </div>
</template>

父组件使用:

javascript 复制代码
<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'

const userList = ref([
  { id: 1, name: '张三', age: 25, tags: ['VIP', '老客户'] },
  { id: 2, name: '李四', age: 30, tags: ['新客户'] }
])

const handleUpdate = (id, data) => {
  console.log('更新用户:', id, data)
  const user = userList.value.find(u => u.id === id)
  if (user) Object.assign(user, data)
}

const handleDelete = (id) => {
  console.log('删除用户:', id)
  userList.value = userList.value.filter(u => u.id !== id)
}
</script>

<template>
  <UserCard
    v-for="user in userList"
    :key="user.id"
    :title="user.name + ' 的信息'"
    :user-id="user.id"
    :user-info="user"
    :tags="user.tags"
    status="active"
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>

3. v-model 与 defineModel(Vue 3.4+ 推荐)

javascript 复制代码
<!-- MyInput.vue - 自定义输入组件 -->
<script setup>
// Vue 3.4+ 的简洁写法(需要配置 vite.config.js 开启)
const modelValue = defineModel({ default: '' })

// 如果不用 defineModel,传统写法:
// const props = defineProps(['modelValue'])
// const emit = defineEmits(['update:modelValue'])
// const updateValue = (e) => emit('update:modelValue', e.target.value)
</script>

<template>
  <input 
    v-model="modelValue"
    class="my-input"
    placeholder="请输入"
  />
</template>

父组件:

javascript 复制代码
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'

const text = ref('初始值')
</script>

<template>
  <MyInput v-model="text" />
  <p>输入的内容:{{ text }}</p>
</template>

多个 v-model(Vue 3.4+):

javascript 复制代码
<!-- SearchPanel.vue -->
<script setup>
const keyword = defineModel('keyword', { default: '' })
const category = defineModel('category', { default: 'all' })
</script>

<template>
  <input v-model="keyword" placeholder="关键词" />
  <select v-model="category">
    <option value="all">全部</option>
    <option value="tech">科技</option>
    <option value="life">生活</option>
  </select>
</template>

父组件:

javascript 复制代码
<SearchPanel v-model:keyword="searchKey" v-model:category="searchCat" />

4. Provide / Inject 跨层级通信

javascript 复制代码
<!-- 祖先组件 App.vue 或 Layout.vue -->
<script setup>
import { provide, ref, readonly } from 'vue'

const user = ref({
  name: '管理员',
  role: 'admin'
})

const theme = ref('light')

// 提供数据(建议用 readonly 防止子组件直接修改)
provide('user', readonly(user))
provide('theme', theme)

// 提供修改方法
provide('updateTheme', (newTheme) => {
  theme.value = newTheme
})
</script>
javascript 复制代码
<!-- 深层子组件 DeepChild.vue -->
<script setup>
import { inject, computed } from 'vue'

// 接收数据(第二个参数是默认值)
const user = inject('user', { name: '访客', role: 'guest' })
const theme = inject('theme', 'light')
const updateTheme = inject('updateTheme', () => {})

const isAdmin = computed(() => user.value.role === 'admin')

const toggleTheme = () => {
  updateTheme(theme.value === 'light' ? 'dark' : 'light')
}
</script>

<template>
  <div :class="theme">
    <p>当前用户:{{ user.name }}({{ user.role }})</p>
    <p v-if="isAdmin">管理员可见内容</p>
    <button @click="toggleTheme">切换主题:{{ theme }}</button>
  </div>
</template>

5. 插槽(Slots)完整用法

javascript 复制代码
<!-- CardLayout.vue -->
<script setup>
defineProps({
  title: String
})
</script>

<template>
  <div class="card">
    <!-- 具名插槽 -->
    <header v-if="$slots.header || title">
      <slot name="header">
        <!-- 默认内容 -->
        <h2>{{ title }}</h2>
      </slot>
    </header>
    
    <!-- 默认插槽 -->
    <main class="card-body">
      <slot>
        <p>暂无内容</p>
      </slot>
    </main>
    
    <!-- 作用域插槽:向父组件传递数据 -->
    <footer>
      <slot name="footer" :time="new Date().toLocaleString()" :close="closeCard">
        默认页脚
      </slot>
    </footer>
  </div>
</template>

父组件使用插槽:

javascript 复制代码
<script setup>
import { ref } from 'vue'
import CardLayout from './CardLayout.vue'

const items = ref([
  { id: 1, text: '事项 1' },
  { id: 2, text: '事项 2' }
])

const handleClose = () => {
  console.log('关闭卡片')
}
</script>

<template>
  <CardLayout title="我的卡片">
    <!-- 具名插槽 -->
    <template #header>
      <div class="custom-header">
        <h2>自定义标题</h2>
        <button>设置</button>
      </div>
    </template>
    
    <!-- 默认插槽 -->
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </ul>
    
    <!-- 作用域插槽:接收子组件数据 -->
    <template #footer="{ time, close }">
      <div class="custom-footer">
        <span>更新时间:{{ time }}</span>
        <button @click="close">关闭</button>
      </div>
    </template>
  </CardLayout>
</template>

6. 动态组件与异步组件

javascript 复制代码
<script setup>
import { ref, shallowRef, defineAsyncComponent } from 'vue'
import Loading from './Loading.vue'
import ErrorComp from './ErrorComp.vue'

// 动态切换组件
const currentTab = ref('Home')
const tabs = {
  Home: defineAsyncComponent(() => import('./Home.vue')),
  About: defineAsyncComponent(() => import('./About.vue')),
  User: defineAsyncComponent(() => import('./User.vue'))
}

// 异步组件(带加载状态和错误处理)
const AsyncHeavyComp = defineAsyncComponent({
  loader: () => import('./HeavyComp.vue'),
  loadingComponent: Loading,    // 加载中显示
  errorComponent: ErrorComp,    // 加载失败显示
  delay: 200,                   // 延迟显示 loading(避免闪烁)
  timeout: 3000                 // 超时时间
})

// 使用 shallowRef 存储组件(避免深层响应式带来的性能开销)
const dynamicComp = shallowRef(null)

const loadComponent = async (name) => {
  const comp = await import(`./components/${name}.vue`)
  dynamicComp.value = comp.default
}
</script>

<template>
  <div class="tabs">
    <button 
      v-for="(component, name) in tabs" 
      :key="name"
      @click="currentTab = name"
      :class="{ active: currentTab === name }"
    >
      {{ name }}
    </button>
  </div>
  
  <!-- 动态组件 -->
  <KeepAlive>
    <component :is="tabs[currentTab]" />
  </KeepAlive>
  
  <!-- 异步加载的重量级组件 -->
  <AsyncHeavyComp />
  
  <!-- 手动动态加载 -->
  <button @click="loadComponent('Chart')">加载图表组件</button>
  <component :is="dynamicComp" v-if="dynamicComp" />
</template>

四、Vue Router 4 详细配置

1. 路由配置与导航守卫

javascript 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// 路由懒加载
const Home = () => import('@/views/Home.vue')
const User = () => import('@/views/User.vue')
const UserProfile = () => import('@/views/UserProfile.vue')
const NotFound = () => import('@/views/NotFound.vue')

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { 
      title: '首页',
      keepAlive: true 
    }
  },
  {
    path: '/user/:id',
    name: 'User',
    component: User,
    meta: { 
      title: '用户详情',
      requiresAuth: true 
    },
    // 子路由
    children: [
      {
        path: '',           // /user/123
        redirect: { name: 'UserProfile' }
      },
      {
        path: 'profile',    // /user/123/profile
        name: 'UserProfile',
        component: UserProfile
      },
      {
        path: 'posts',      // /user/123/posts
        name: 'UserPosts',
        component: () => import('@/views/UserPosts.vue')
      }
    ]
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { public: true }  // 公开页面,无需登录
  },
  {
    path: '/:pathMatch(.*)*',  // 404 页面
    name: 'NotFound',
    component: NotFound
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition  // 后退时恢复位置
    } else if (to.hash) {
      return { el: to.hash, behavior: 'smooth' }  // 锚点跳转
    } else {
      return { top: 0 }  // 默认置顶
    }
  }
})

// ===== 全局前置守卫 =====
router.beforeEach((to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title || '默认标题'
  
  // 权限校验
  const token = localStorage.getItem('token')
  if (to.meta.requiresAuth && !token) {
    next({ 
      name: 'Login', 
      query: { redirect: to.fullPath }  // 携带原目标地址
    })
  } else {
    next()
  }
})

// ===== 全局解析守卫 =====
router.beforeResolve((to, from, next) => {
  // 在组件内守卫和异步组件解析之后调用
  next()
})

// ===== 全局后置钩子 =====
router.afterEach((to, from) => {
  // 可用于结束 loading
  console.log('路由切换完成:', to.path)
})

export default router

2. 组件内使用路由

javascript 复制代码
<script setup>
import { useRoute, useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref, watch } from 'vue'

const route = useRoute()    // 当前路由信息
const router = useRouter()  // 路由实例

const userId = ref(route.params.id)
const queryTab = ref(route.query.tab || '1')

// 监听路由参数变化(同一组件不同参数时触发)
watch(() => route.params.id, (newId) => {
  userId.value = newId
  fetchUserData(newId)
})

// 编程式导航
const goBack = () => {
  router.back()
}

const goHome = () => {
  router.push('/')
}

const goUser = (id) => {
  // 命名路由 + 参数
  router.push({
    name: 'User',
    params: { id },
    query: { tab: 'profile' }
  })
}

const replaceRoute = () => {
  // 替换当前历史记录(不留痕迹)
  router.replace('/home')
}

// 组件内守卫:离开页面前确认
onBeforeRouteLeave((to, from, next) => {
  const answer = window.confirm('确定要离开吗?有未保存的更改。')
  if (answer) {
    next()
  } else {
    next(false)  // 取消导航
  }
})

// 组件内守卫:同组件路由更新时(如 /user/1 -> /user/2)
onBeforeRouteUpdate((to, from, next) => {
  console.log('路由更新:', to.params.id)
  next()
})

const fetchUserData = (id) => {
  console.log('获取用户数据:', id)
}
</script>

<template>
  <div>
    <p>当前用户ID:{{ userId }}</p>
    <p>当前标签:{{ queryTab }}</p>
    <button @click="goBack">返回</button>
    <button @click="goHome">首页</button>
    <button @click="goUser(999)">去用户999</button>
    
    <!-- 子路由出口 -->
    <router-view />
  </div>
</template>

五、Pinia 状态管理(无 TypeScript)

1. Store 定义

javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi, getUserInfo } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  // ===== State =====
  const token = ref(localStorage.getItem('token') || '')
  const userInfo = ref(null)
  const loading = ref(false)

  // ===== Getters(用 computed)=====
  const isLogin = computed(() => !!token.value)
  const displayName = computed(() => {
    return userInfo.value?.nickname || userInfo.value?.name || '访客'
  })

  // ===== Actions =====
  async function login(form) {
    loading.value = true
    try {
      const res = await loginApi(form)
      token.value = res.token
      userInfo.value = res.userInfo
      localStorage.setItem('token', res.token)
      return res
    } catch (error) {
      throw error
    } finally {
      loading.value = false
    }
  }

  async function fetchUserInfo() {
    if (!token.value) return
    try {
      const res = await getUserInfo()
      userInfo.value = res
    } catch (error) {
      logout()
    }
  }

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

  // 重置所有状态
  function $reset() {
    token.value = ''
    userInfo.value = null
    loading.value = false
  }

  return {
    token,
    userInfo,
    loading,
    isLogin,
    displayName,
    login,
    fetchUserInfo,
    logout,
    $reset
  }
})

2. 选项式写法(传统风格)

javascript 复制代码
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  },
  
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    async asyncIncrement() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    }
  }
})

3. 组件中使用

javascript 复制代码
<script setup>
import { useUserStore, useCounterStore } from '@/stores'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const counterStore = useCounterStore()

// 解构 state 和 getters 保持响应式(必须用 storeToRefs)
const { token, userInfo, isLogin, displayName } = storeToRefs(userStore)
// 方法可以直接解构
const { login, logout } = userStore

// 直接修改 state(Pinia 允许,但建议用 action)
const updateName = () => {
  userStore.userInfo.name = '新名字'  // ✅ 可以直接修改
}

// 使用 $patch 批量修改
const batchUpdate = () => {
  userStore.$patch({
    token: 'new-token',
    userInfo: { name: '批量更新' }
  })
}

// 使用 $patch 函数式写法(推荐用于复杂修改)
const functionalPatch = () => {
  userStore.$patch((state) => {
    state.token = 'func-token'
    state.userInfo.nickname = '函数式更新'
  })
}

// 重置状态
const resetStore = () => {
  userStore.$reset()
}

// 订阅 state 变化(可用于持久化)
userStore.$subscribe((mutation, state) => {
  console.log('store 变化:', mutation)
  localStorage.setItem('user', JSON.stringify(state.userInfo))
})
</script>

<template>
  <div>
    <p>登录状态:{{ isLogin ? '已登录' : '未登录' }}</p>
    <p>用户名:{{ displayName }}</p>
    <p>计数:{{ counterStore.count }}(双倍:{{ counterStore.doubleCount }})</p>
    
    <button @click="counterStore.increment">+1</button>
    <button @click="logout">退出</button>
    <button @click="resetStore">重置</button>
  </div>
</template>

六、模板语法与指令详解

javascript 复制代码
<script setup>
import { ref, computed } from 'vue'

const isShow = ref(true)
const type = ref('A')
const list = ref([
  { id: 1, name: '苹果', price: 5, active: true },
  { id: 2, name: '香蕉', price: 3, active: false },
  { id: 3, name: '橙子', price: 4, active: true }
])
const inputText = ref('')
const htmlContent = ref('<span style="color: red;">红色文本</span>')

const activeList = computed(() => list.value.filter(item => item.active))

const handleClick = (item, event) => {
  console.log('点击了:', item.name)
  console.log('事件对象:', event)
}

const handleSubmit = () => {
  console.log('提交表单:', inputText.value)
}

const onEnter = () => {
  console.log('按下了回车')
}
</script>

<template>
  <div>
    <!-- v-if / v-else-if / v-else(切换时组件会销毁重建) -->
    <div v-if="type === 'A'">类型 A 的内容</div>
    <div v-else-if="type === 'B'">类型 B 的内容</div>
    <div v-else>其他类型</div>

    <!-- v-show(仅切换 display,组件不销毁) -->
    <div v-show="isShow">v-show 控制的内容</div>

    <!-- v-for(务必使用唯一 key,不要用 index) -->
    <ul>
      <li 
        v-for="item in list" 
        :key="item.id"
        :class="{ active: item.active }"
      >
        {{ item.name }} - ¥{{ item.price }}
      </li>
    </ul>

    <!-- v-for 与 v-if 不建议同时使用(Vue 3 中 v-if 优先级更高) -->
    <!-- 推荐用计算属性过滤后再遍历 -->
    <ul>
      <li v-for="item in activeList" :key="item.id">
        {{ item.name }}
      </li>
    </ul>

    <!-- v-for 遍历对象 -->
    <div v-for="(value, key, index) in list[0]" :key="key">
      {{ index + 1 }}. {{ key }}: {{ value }}
    </div>

    <!-- v-for 遍历范围 -->
    <span v-for="n in 5" :key="n">{{ n }}</span>

    <!-- v-model 修饰符 -->
    <input v-model.trim="inputText" placeholder="自动去首尾空格" />
    <input v-model.number="age" type="number" placeholder="自动转数字" />
    <input v-model.lazy="description" placeholder="失焦后更新" />

    <!-- 事件修饰符 -->
    <a href="http://example.com" @click.prevent="handleClick">阻止默认跳转</a>
    <div @click.stop="handleClick">阻止事件冒泡</div>
    <div @click.self="handleClick">只有点击自身才触发(子元素不触发)</div>
    <div @click.once="handleClick">只触发一次</div>
    <form @submit.prevent="handleSubmit">阻止表单默认提交</form>

    <!-- 按键修饰符 -->
    <input @keyup.enter="onEnter" placeholder="回车触发" />
    <input @keyup.esc="onEsc" placeholder="ESC 触发" />
    <input @keydown.ctrl.s="onSave" placeholder="Ctrl+S 保存" />

    <!-- 动态属性 -->
    <div :id="'item-' + item.id">动态 ID</div>
    <div :class="['class-a', item.active ? 'active' : '']">动态类名</div>
    <div :style="{ color: item.active ? 'red' : 'blue', fontSize: '14px' }">动态样式</div>

    <!-- 渲染原始 HTML(慎用,防止 XSS) -->
    <div v-html="htmlContent"></div>

    <!-- 一次性渲染,后续跳过响应式追踪 -->
    <div v-once>静态内容:{{ new Date().toLocaleString() }}</div>

    <!-- 条件缓存(条件不变时跳过子树更新,适合大数据列表) -->
    <div v-memo="[item.id, item.active]">
      复杂内容:{{ item.name }} - {{ item.price }}
    </div>

    <!-- 模板引用 -->
    <input ref="inputRef" />
    <button @click="$refs.inputRef.focus()">聚焦</button>
  </div>
</template>

七、组合式函数(Composables)

1. 常用自定义 Composables

javascript 复制代码
// composables/useMouse.js
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 }
}

// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

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

  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      // toValue 支持 ref 和原始值
      const res = await fetch(toValue(url))
      if (!res.ok) throw new Error(res.statusText)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  // url 变化时自动重新请求
  watchEffect(() => {
    fetchData()
  })

  return { data, error, loading, refresh: fetchData }
}

// composables/useLocalStorage.js
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) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data
}

// composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value.value)

  let timer = null
  watch(value, (newVal) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      debouncedValue.value = newVal
    }, delay)
  })

  return debouncedValue
}

2. 组件中使用

javascript 复制代码
<script setup>
import { ref } from 'vue'
import { useMouse } from '@/composables/useMouse'
import { useFetch } from '@/composables/useFetch'
import { useLocalStorage } from '@/composables/useLocalStorage'
import { useDebounce } from '@/composables/useDebounce'

const { x, y } = useMouse()

const searchText = ref('')
const debouncedSearch = useDebounce(searchText)

// 根据搜索词动态请求
const { data: searchResult, loading } = useFetch(
  computed(() => `https://api.example.com/search?q=${debouncedSearch.value}`)
)

// 持久化主题设置
const theme = useLocalStorage('theme', 'light')
</script>

<template>
  <div>
    <p>鼠标位置:{{ x }}, {{ y }}</p>
    
    <input v-model="searchText" placeholder="搜索..." />
    <p v-if="loading">加载中...</p>
    <ul v-else>
      <li v-for="item in searchResult" :key="item.id">{{ item.name }}</li>
    </ul>
    
    <p>当前主题:{{ theme }}</p>
    <button @click="theme = theme === 'light' ? 'dark' : 'light'">切换主题</button>
  </div>
</template>

八、性能优化常用操作

javascript 复制代码
<script setup>
import { ref, shallowRef, markRaw, computed } from 'vue'

// 1. shallowRef:大数据列表只监听顶层变化
const bigList = shallowRef([
  { id: 1, data: { /* 大量嵌套数据 */ } },
  { id: 2, data: { /* 大量嵌套数据 */ } }
])
// 修改某个项的深层属性不会触发更新(需要手动触发)
const updateItem = (index) => {
  bigList.value[index].data.name = '新名字'  // 不会触发更新
  bigList.value = [...bigList.value]          // 手动触发
}

// 2. markRaw:标记不需要响应式的对象(如第三方库实例)
const chartInstance = markRaw({
  echarts: null,
  init() {
    // 初始化图表
  }
})

// 3. computed 缓存复杂计算
const filteredList = computed(() => {
  // 只有依赖变化时才重新计算
  return bigList.value.filter(item => item.active).sort((a, b) => b.id - a.id)
})

// 4. v-once:静态内容只渲染一次
const staticTime = ref(new Date().toLocaleString())

// 5. 虚拟滚动(长列表优化)
// 安装:npm install vue-virtual-scroller
// 使用:<RecycleScroller :items="list" :item-size="32" v-slot="{ item }">
</script>

<template>
  <div>
    <!-- v-memo:条件不变时跳过子树更新 -->
    <div v-for="item in bigList" :key="item.id" v-memo="[item.id, item.active]">
      <h3>{{ item.id }}</h3>
      <p>大量复杂内容...</p>
    </div>
    
    <!-- v-once 静态内容 -->
    <p v-once>页面加载时间:{{ staticTime }}</p>
    
    <!-- 使用 KeepAlive 缓存组件状态 -->
    <KeepAlive :include="['TabA', 'TabB']" :max="10">
      <component :is="currentTab" />
    </KeepAlive>
  </div>
</template>

九、Vite 配置与常用插件

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'  // 打包分析

export default defineConfig({
  plugins: [
    vue(),
    visualizer({ open: true })  // 打包后自动打开分析页面
  ],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@views': resolve(__dirname, 'src/views'),
      '@stores': resolve(__dirname, 'src/stores')
    }
  },
  
  server: {
    port: 3000,
    open: true,           // 自动打开浏览器
    cors: true,           // 允许跨域
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  build: {
    outDir: 'dist',
    sourcemap: true,
    chunkSizeWarningLimit: 1500,  //  chunk 大小警告阈值
    rollupOptions: {
      output: {
        manualChunks: {
          // 代码分割,按需加载
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus']  // 或其他 UI 库
        }
      }
    }
  }
})

十、main.js 入口文件示例

javascript 复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

// 全局样式
import './assets/styles/main.css'

// 可选:全局引入 UI 库(如 Element Plus)
// import ElementPlus from 'element-plus'
// import 'element-plus/dist/index.css'

const app = createApp(App)

// 全局属性(替代 Vue 2 的 Vue.prototype)
app.config.globalProperties.$formatDate = (date) => {
  return new Date(date).toLocaleDateString()
}

// 全局指令
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

// 全局组件(懒加载建议局部注册)
// app.component('GlobalComp', GlobalComp)

app.use(createPinia())
app.use(router)
// app.use(ElementPlus)

app.mount('#app')

以上就是 Vue 3 纯 JavaScript 开发的完整常用操作汇总,涵盖了从项目搭建、响应式数据、组件通信、路由、状态管理到性能优化的核心场景。

相关推荐
计算机魔术师18 分钟前
我国日均词元调用量突破 500 万亿,中国大模型稳居全球第一梯队
前端
码视野28 分钟前
基于 Spring Boot + Vue3 的【高校化学实验室安全准入考试与危化品配伍排查系统】设计与实现(含PRD/三端高保真源码/大屏)
前端·人工智能·spring boot·后端·安全·vue3
玄魂40 分钟前
VisActor 全新图可视化开源项目:VGraph
前端·数据可视化
宇智波亚索1 小时前
TypeScript 实际应用
前端·javascript·typescript
明飞19871 小时前
C表达式_表达式类型
前端
雪芽蓝域zzs1 小时前
Vue3+Vite 本地数据模拟 JSON 读取和Mock 方案完整讲解总结
前端·vue.js
jjw_zyfx2 小时前
vue3 vite 前端录制浏览器界面视频并保存下载
前端·音视频
南雨北斗2 小时前
vue3项目表单验证 input 添加高亮样式
前端
:-)2 小时前
idea中的vue文件没有高亮显示
前端·javascript·vue.js·ecmascript·intellij-idea