企业级前端场景题集合
本文收集了企业开发中常见的前端场景题,主要以 Vue 3 Composition API 视角实现,涵盖不同类型、功能和方向的实际问题。
1. 防止重复请求/按钮重复点击
需求分析
在表单提交、数据保存等场景中,用户可能因网络延迟、误操作等原因重复点击按钮,导致多次发送相同请求,引发数据不一致或性能问题。
实现方案
创建一个通用的 useThrottleFn Hook 来限制函数的执行频率:
vue
<template>
<button @click="handleSubmit" :disabled="loading">
{{ loading ? '提交中...' : '提交' }}
</button>
</template>
<script setup>
import { ref } from 'vue'
import { useThrottleFn } from './hooks'
const loading = ref(false)
const submitForm = async () => {
if (loading.value) return
loading.value = true
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('表单提交成功')
} catch (error) {
console.error('表单提交失败', error)
} finally {
loading.value = false
}
}
// 使用节流 Hook,限制 2 秒内只能执行一次
const handleSubmit = useThrottleFn(submitForm, 2000)
</script>javascript
// hooks.js
import { ref } from 'vue'
export function useThrottleFn(fn, delay) {
const lastCall = ref(0)
return function(...args) {
const now = Date.now()
if (now - lastCall.value < delay) {
return
}
lastCall.value = now
return fn.apply(this, args)
}
}进阶方案:结合 Axios 拦截器
javascript
// api.js
import axios from 'axios'
const pendingRequests = new Map()
const generateKey = (config) => {
return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
}
// 请求拦截器
axios.interceptors.request.use(config => {
const key = generateKey(config)
// 如果请求已存在,取消之前的请求
if (pendingRequests.has(key)) {
pendingRequests.get(key).cancel()
}
// 创建新的取消令牌
const source = axios.CancelToken.source()
config.cancelToken = source.token
pendingRequests.set(key, source)
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器
axios.interceptors.response.use(response => {
const key = generateKey(response.config)
pendingRequests.delete(key)
return response
}, error => {
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message)
} else {
const key = generateKey(error.config)
pendingRequests.delete(key)
}
return Promise.reject(error)
})
export default axios2. 无限滚动加载
需求分析
在列表页中,为了提升性能和用户体验,需要实现滚动到底部自动加载更多数据的功能。
实现方案
vue
<template>
<div class="infinite-scroll-container" ref="containerRef" @scroll="handleScroll">
<div v-for="item in list" :key="item.id" class="list-item">
{{ item.content }}
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-if="noMore" class="no-more">没有更多数据了</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
const containerRef = ref(null)
const list = ref([])
const page = ref(1)
const pageSize = ref(10)
const loading = ref(false)
const noMore = ref(false)
const hasMore = computed(() => {
return !loading.value && !noMore.value
})
const loadData = async () => {
if (!hasMore.value) return
loading.value = true
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000))
const newData = Array.from({ length: pageSize.value }, (_, i) => ({
id: (page.value - 1) * pageSize.value + i + 1,
content: `第 ${page.value} 页,第 ${i + 1} 条数据`
}))
list.value = [...list.value, ...newData]
page.value++
// 模拟数据加载完毕
if (page.value > 5) {
noMore.value = true
}
} catch (error) {
console.error('加载数据失败', error)
} finally {
loading.value = false
}
}
const handleScroll = () => {
if (!containerRef.value || !hasMore.value) return
const { scrollTop, clientHeight, scrollHeight } = containerRef.value
// 当滚动到底部 50px 内时加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
loadData()
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.infinite-scroll-container {
height: 400px;
overflow-y: auto;
border: 1px solid #ccc;
}
.list-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.loading, .no-more {
text-align: center;
padding: 20px;
color: #666;
}
</style>3. 表单验证与提交
需求分析
实现一个包含多种输入类型的表单,支持实时验证、错误提示和表单提交功能。
实现方案
vue
<template>
<form @submit.prevent="handleSubmit">
<div class="form-item">
<label>用户名</label>
<input v-model="formData.username" @blur="validateField('username')" />
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>
<div class="form-item">
<label>邮箱</label>
<input v-model="formData.email" @blur="validateField('email')" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<div class="form-item">
<label>密码</label>
<input type="password" v-model="formData.password" @blur="validateField('password')" />
<span v-if="errors.password" class="error">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="loading">提交</button>
</form>
</template>
<script setup>
import { ref, reactive } from 'vue'
const formData = reactive({
username: '',
email: '',
password: ''
})
const errors = reactive({
username: '',
email: '',
password: ''
})
const loading = ref(false)
const validateField = (field) => {
switch (field) {
case 'username':
if (!formData.username.trim()) {
errors.username = '用户名不能为空'
} else if (formData.username.length < 3) {
errors.username = '用户名至少3个字符'
} else {
errors.username = ''
}
break
case 'email':
if (!formData.email.trim()) {
errors.email = '邮箱不能为空'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
errors.email = '邮箱格式不正确'
} else {
errors.email = ''
}
break
case 'password':
if (!formData.password) {
errors.password = '密码不能为空'
} else if (formData.password.length < 6) {
errors.password = '密码至少6个字符'
} else {
errors.password = ''
}
break
}
}
const validateForm = () => {
let isValid = true
Object.keys(formData).forEach(field => {
validateField(field)
if (errors[field]) {
isValid = false
}
})
return isValid
}
const handleSubmit = async () => {
if (!validateForm() || loading.value) return
loading.value = true
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('表单提交成功', formData)
} catch (error) {
console.error('表单提交失败', error)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.form-item {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input {
width: 300px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
color: red;
font-size: 12px;
margin-top: 5px;
display: block;
}
button {
padding: 10px 20px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>4. 数据状态管理(Pinia)
需求分析
在中大型应用中,需要一个集中的状态管理方案来管理共享数据,如用户信息、全局配置等。
实现方案
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const userInfo = ref(null)
const token = ref(localStorage.getItem('token'))
// Getters
const isLogin = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name || '')
// Actions
const login = async (credentials) => {
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000))
const mockUser = {
id: 1,
name: '张三',
email: 'zhangsan@example.com'
}
const mockToken = 'mock-jwt-token'
userInfo.value = mockUser
token.value = mockToken
localStorage.setItem('token', mockToken)
return true
} catch (error) {
console.error('登录失败', error)
return false
}
}
const logout = () => {
userInfo.value = null
token.value = null
localStorage.removeItem('token')
}
const fetchUserInfo = async () => {
if (!token.value) return
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000))
const mockUser = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
avatar: 'https://example.com/avatar.jpg'
}
userInfo.value = mockUser
} catch (error) {
console.error('获取用户信息失败', error)
logout()
}
}
return {
userInfo,
token,
isLogin,
userName,
login,
logout,
fetchUserInfo
}
})vue
<!-- 使用示例 -->
<template>
<div>
<div v-if="userStore.isLogin">
<p>欢迎,{{ userStore.userName }}!</p>
<button @click="userStore.logout">退出登录</button>
</div>
<div v-else>
<button @click="handleLogin">登录</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useUserStore } from '../stores/user'
const userStore = useUserStore()
const handleLogin = async () => {
await userStore.login({
username: 'zhangsan',
password: '123456'
})
}
onMounted(() => {
// 页面加载时获取用户信息
userStore.fetchUserInfo()
})
</script>5. 响应式数据监听与处理
需求分析
在某些场景下,需要监听数据的变化并执行相应的逻辑,如表单数据变化时自动保存草稿。
实现方案
vue
<template>
<div>
<h3>自动保存草稿</h3>
<textarea v-model="content" rows="10" cols="50"></textarea>
<p v-if="saveStatus" class="save-status">{{ saveStatus }}</p>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
const content = ref('')
const saveStatus = ref('')
// 模拟从本地存储加载草稿
onMounted(() => {
const savedDraft = localStorage.getItem('article-draft')
if (savedDraft) {
content.value = savedDraft
}
})
// 使用 watch 监听数据变化,并使用 debounce 优化保存频率
const debouncedSave = debounce(async (newValue) => {
saveStatus.value = '正在保存...'
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 500))
localStorage.setItem('article-draft', newValue)
saveStatus.value = '保存成功'
// 3秒后清除保存状态
setTimeout(() => {
saveStatus.value = ''
}, 3000)
} catch (error) {
console.error('保存失败', error)
saveStatus.value = '保存失败'
}
}, 1000)
watch(content, (newValue) => {
debouncedSave(newValue)
})
// 防抖函数
function debounce(fn, delay) {
let timer = null
return function(...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
</script>
<style scoped>
.save-status {
color: #42b983;
font-size: 14px;
margin-top: 10px;
}
</style>6. 组件通信(Props/Emits/Provide/Inject)
需求分析
实现父子组件、跨层级组件之间的通信,如导航菜单与内容区域的联动。
实现方案
vue
<!-- 父组件 App.vue -->
<template>
<div class="app-container">
<Sidebar @select="handleSelect" :selectedKey="selectedKey" />
<Content :content="currentContent" />
</div>
</template>
<script setup>
import { ref, reactive, provide } from 'vue'
import Sidebar from './components/Sidebar.vue'
import Content from './components/Content.vue'
const selectedKey = ref('home')
const contentMap = reactive({
home: '首页内容',
about: '关于我们',
contact: '联系我们'
})
const currentContent = ref(contentMap[selectedKey.value])
// 提供全局配置
provide('appConfig', {
theme: 'light',
version: '1.0.0'
})
const handleSelect = (key) => {
selectedKey.value = key
currentContent.value = contentMap[key]
}
</script>
<style scoped>
.app-container {
display: flex;
}
</style>vue
<!-- 子组件 Sidebar.vue -->
<template>
<div class="sidebar">
<div
v-for="item in menuItems"
:key="item.key"
@click="handleClick(item.key)"
:class="{ active: selectedKey === item.key }"
class="menu-item"
>
{{ item.label }}
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
selectedKey: {
type: String,
default: 'home'
}
})
const emit = defineEmits(['select'])
const menuItems = [
{ key: 'home', label: '首页' },
{ key: 'about', label: '关于我们' },
{ key: 'contact', label: '联系我们' }
]
const handleClick = (key) => {
emit('select', key)
}
</script>
<style scoped>
.sidebar {
width: 200px;
background-color: #f5f5f5;
padding: 20px;
}
.menu-item {
padding: 10px;
cursor: pointer;
margin-bottom: 10px;
border-radius: 4px;
}
.menu-item:hover {
background-color: #e0e0e0;
}
.active {
background-color: #42b983;
color: white;
}
</style>vue
<!-- 子组件 Content.vue -->
<template>
<div class="content">
<h2>{{ content }}</h2>
<p>当前主题: {{ appConfig.theme }}</p>
<p>版本号: {{ appConfig.version }}</p>
</div>
</template>
<script setup>
import { defineProps, inject } from 'vue'
const props = defineProps({
content: {
type: String,
default: ''
}
})
// 注入全局配置
const appConfig = inject('appConfig')
</script>
<style scoped>
.content {
flex: 1;
padding: 20px;
}
</style>7. 图片懒加载
需求分析
在长列表中,为了提升页面加载性能,需要实现图片的懒加载功能,即当图片进入视口时才加载。
实现方案
vue
<template>
<div class="image-list">
<div v-for="item in imageList" :key="item.id" class="image-item">
<img
v-lazy="item.url"
:alt="item.alt"
@load="handleImageLoad"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, directive } from 'vue'
const imageList = ref([
{ id: 1, url: 'https://picsum.photos/300/200?random=1', alt: '图片1' },
{ id: 2, url: 'https://picsum.photos/300/200?random=2', alt: '图片2' },
{ id: 3, url: 'https://picsum.photos/300/200?random=3', alt: '图片3' },
{ id: 4, url: 'https://picsum.photos/300/200?random=4', alt: '图片4' },
{ id: 5, url: 'https://picsum.photos/300/200?random=5', alt: '图片5' },
{ id: 6, url: 'https://picsum.photos/300/200?random=6', alt: '图片6' },
{ id: 7, url: 'https://picsum.photos/300/200?random=7', alt: '图片7' },
{ id: 8, url: 'https://picsum.photos/300/200?random=8', alt: '图片8' },
{ id: 9, url: 'https://picsum.photos/300/200?random=9', alt: '图片9' },
{ id: 10, url: 'https://picsum.photos/300/200?random=10', alt: '图片10' }
])
const loadedCount = ref(0)
// 自定义图片懒加载指令
const vLazy = directive('lazy', {
mounted(el, binding) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 图片进入视口,加载图片
el.src = binding.value
observer.unobserve(el)
}
})
},
{
threshold: 0.1 // 当图片 10% 进入视口时触发
}
)
// 初始占位图
el.src = 'data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22300%22 height=%22200%22%3E%3Crect width=%22100%25%22 height=%22100%25%22 fill=%22%23f5f5f5%22/%3E%3C/svg%3E'
observer.observe(el)
}
})
const handleImageLoad = () => {
loadedCount.value++
console.log(`已加载 ${loadedCount.value}/${imageList.value.length} 张图片`)
}
onMounted(() => {
console.log('图片列表组件挂载完成')
})
</script>
<style scoped>
.image-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
}
.image-item {
aspect-ratio: 3/2;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
img:hover {
transform: scale(1.05);
}
</style>8. 国际化(i18n)
需求分析
实现应用的多语言切换功能,支持中英文切换。
实现方案
javascript
// i18n/index.js
import { createI18n } from 'vue-i18n'
// 语言包
const messages = {
en: {
hello: 'Hello',
welcome: 'Welcome to our application',
language: 'Language',
home: 'Home',
about: 'About',
contact: 'Contact'
},
zh: {
hello: '你好',
welcome: '欢迎使用我们的应用',
language: '语言',
home: '首页',
about: '关于我们',
contact: '联系我们'
}
}
const i18n = createI18n({
locale: localStorage.getItem('language') || 'zh', // 默认语言
messages,
legacy: false, // 使用 Composition API
globalInjection: true // 全局注入 $t
})
export default i18nvue
<!-- App.vue -->
<template>
<div class="app">
<div class="language-switcher">
<button @click="switchLanguage('zh')" :class="{ active: currentLocale === 'zh' }">中文</button>
<button @click="switchLanguage('en')" :class="{ active: currentLocale === 'en' }">English</button>
</div>
<h1>{{ $t('hello') }}</h1>
<p>{{ $t('welcome') }}</p>
<nav>
<router-link :to="'/home'">{{ $t('home') }}</router-link>
<router-link :to="'/about'">{{ $t('about') }}</router-link>
<router-link :to="'/contact'">{{ $t('contact') }}</router-link>
</nav>
<router-view />
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const currentLocale = computed(() => locale.value)
const switchLanguage = (lang) => {
locale.value = lang
localStorage.setItem('language', lang)
}
</script>
<style scoped>
.language-switcher {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 5px 10px;
border: 1px solid #ccc;
background-color: white;
cursor: pointer;
}
button.active {
background-color: #42b983;
color: white;
border-color: #42b983;
}
nav {
display: flex;
gap: 20px;
margin-top: 20px;
}
a {
text-decoration: none;
color: #42b983;
}
</style>总结
本文收集了企业开发中常见的前端场景题,主要以 Vue 3 Composition API 视角实现,涵盖了以下几类场景:
- 性能优化类:防止重复请求、无限滚动、图片懒加载
- 用户交互类:表单验证与提交、按钮状态管理
- 状态管理类:Pinia 数据管理
- 组件通信类:Props/Emits/Provide/Inject
- 国际化类:i18n 多语言切换
这些场景题涵盖了前端开发的核心知识点,通过实际的代码实现可以更好地理解和掌握 Vue 3 的使用技巧。