纯 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 开发的完整常用操作汇总,涵盖了从项目搭建、响应式数据、组件通信、路由、状态管理到性能优化的核心场景。