弹窗组件
组件结构➡️插槽➡️样式➡️显示和隐藏的逻辑➡️事件处理
子组件:
<template>
<div v-if="visible" class="modal-overlay">
<div class="modal-container">
<div class="modal-header">
<h3>{{ title }}</h3>
<button @click="close" class="close-btn">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<button @click="close" class="cancel-btn">取消</button>
<button @click="confirm" class="confimr-btn">确定</button>
</div>
</div>
</div>
</template>
父组件:
<template>
<div>
<button @click="showModal = true" class="open-modal-btn">打开弹窗</button>
<Modal v-model:visible="showModal" title="自定义标题" @confirm="handleConfirm" @cancel="handleCancel">
<h3>通过插槽传入的内容</h3>
<p>可以在这放置任何自定义内容</p>
</Modal>
</div>
</template>
首页白屏
1.资源路径错误
打开vite.config.ts
export default defineConfig({
pluginls: [vue()],
// 判断是生产环境还是开发环境,使用不同的资源路径格式
base: process.env.NODE_ENV === 'production' ? './' : '/'
})
2.路由配置错误
确保路由组件使用动态导入(import())并正确处理错误
const routes = [
{
path: '/',
// 要配置一个error错误页面,资源没加载出来的时候指向error 404 页面
component: () => import('@/views/Home.vue').catch(() => import('@/views/Error.vue')),
},
];
排查流程:
打开开发者工具,检查Console和Network面板,定位错误类型
验证配置一致性
检查环境变量,确保生产环境变量正确定义并注入
后端接口返回的超大树形结构数据
1.虚拟滚动(最佳方案)
vue3的插件,只渲染当前可见的部分
<!-- :items 要渲染的节点数组 -->
<!-- :item-size 固定每个节点的高度为 32px,用于计算滚动条位置和渲染区域 -->
<!-- key-field 指定节点对象的唯一标识字段为 id -->
<!-- v-slot 定义作用域插槽,接收当前渲染的节点数据,并解构出 item 对象供内部使用 -->
<RecycleScroller
class="scroller"
:items="visibleNodes"
:item-size="32"
key-field="id"
v-slot="{ item }"
>
<!-- 动态计算左内边距,根据节点的层级 level 实现缩进效果(每级缩进 20px) -->
<!-- 点击节点时调用 toggleExpand 方法,传入当前节点,用于展开/折叠子节点 -->
<div
class="tree-node"
:style="{ paddingLeft: `${item.level * 20}px` }"
@click="toggleExpand(item)"
>
<!-- 只有当节点存在子节点(item.children 为真)时才显示展开/折叠图标 -->
<!-- 根据节点的 expanded 状态显示不同图标:展开时显示 '-',折叠时显示 '+' -->
<span v-if="item.children" class="expand-icon">
{{ item.expanded ? '-' : '+' }}
</span>
{{ item.label }}
</div>
</RecycleScroller>
2.懒加载
只加载当前展开节点的子节点,减少初始数据量
// 定义一个异步函数 loadChildren,用于加载树节点的子节点,并切换展开/折叠状态
const loadChildren = async (node: TreeNode) => {
// 检查当前节点是否尚未加载子节点:如果没有 children 属性且未标记为已加载(loaded 为 false)
if (!node.children && !node.loaded) {
// 发起异步请求,获取该节点的子节点数据,URL 中携带当前节点的 id
const response = await fetch(`/api/tree/children?id=${node.id}`)
// 将响应的 JSON 数据解析并赋值给节点的 children 属性
node.children = await response.json()
// 标记该节点已加载过数据,避免重复请求
node.loaded = true
}
// 切换节点的展开/折叠状态:如果原来是展开则折叠,原来是折叠则展开
node.expanded = !node.expanded
}
3.数据分片处理(需要后端接口支持)
// 定义每个数据块的大小,即每次请求加载的节点数量
const chunkSize = 1000
// 记录当前已加载的块索引(从0开始),用于分页请求
let currentChunk = 0
// 创建一个响应式引用 loadedData,初始值为空数组,用于存储所有已加载的树节点
const loadedData = ref < TreeNode[] > ([])
// 定义一个异步函数,用于加载下一个数据块并更新 loadedData
const loadNextChunk = async () => {
// 向服务器发起 GET 请求,获取指定块索引和大小的节点数据
const response = await fetch(`/api/tree?chunk=${currentChunk}&size=${chunkSize}`)
// 解析响应的 JSON 数据,得到当前块的节点数组
const chunk = await response.json()
// 将新加载的节点数组合并到现有的 loadedData 数组中(使用展开运算符创建新数组)
loadedData.value = [...loadedData.value, ...chunk]
// 块索引加1,准备加载下一个块
currentChunk++
}
4.Web Worker...
浏览器类型
检查是移动端还是PC端
1.userAgent
function detectDevice() {
// 使用JavaScript的userAgent方法检测用户设备
const userAgent = navigator.userAgent.toLowerCase()
// 用正则判断是什么设备
const isMobile = /iphone|ipad|ipod|android|mobile|phone/i.test(userAgent)
return isMobile ? 'mobile' : 'pc'
}
2.屏幕尺寸检测
userAgent检测不准确或检测不出来的备选方案
function detectDevice() {
// 使用JavaScript的userAgent方法检测用户设备
const userAgent = navigator.userAgent.toLowerCase()
// 用正则判断是什么设备
const isMobile = /iphone|ipad|ipod|android|mobile|phone/i.test(userAgent)
const isSmallScreen = window.innerWidt < 768 // 768px 常见的移动端分界点
return (isMobile || isSmallScreen) ? 'mobile' : 'pc'
}
扩展:延迟重定向(避免影响SEO)
setTimeout(() => {
const isMobile = /iphone|ipad|ipod|android|mobile|phone/i.test(navigator.userAgent)
window.location.href = isMobile ? '/h5-app' : '/web-app'
}, 100)
安全:防止其他人调试前端代码
核心防御技术
高级代码混淆:Obfuscator 开源工具
动态Debugger陷阱
setInterval(() => {
// 记录当前高精度时间(毫秒)
const start = performance.now()
// 触发断点:如果开发者工具打开,脚本会在此暂停
debugger
// 记录暂停结束后的时间
const end = performance.now()
// 如果暂停时间超过100毫秒(说明debugger生效了)
if (end - start > 100)
// 立即跳转到空白页,使页面无法继续调试
window.location.href = 'about:blank'
// 每50毫秒执行一次检测
}, 50)
开发者工具检测
通过window.outerWidth - window.innerWidth 或 window.outerHeight - window.innerHeight判断是否打开了调试工具(差值通常 > 160px)
快捷键禁用 右键菜单禁用...
进阶防御策略
代码分片和动态加载
将关键代码拆分为多个片段,通过异步请求或import()动态加载,减少静态分析可能
反调试库集成
工具:disable-devtool 或 console-ban
使用JavaScript实现轻量级本地存储方案,用于保存和读取用户偏好设置
当localStorage存放超过5m容量时,会自动寻找时间久且使用频率不高的存储进行清除
// 存储前缀,避免命名冲突
prefix: 'app_prefs_'
// 存
function setLocal (key, value) {
try {
// 将值转换成字符串
const serializedValue = JSON.stringify(value)
// 值大小限制在5m内
localStorage.setItem(this.prefix + key, serializedValue)
} catch (error) {
console.error('保存偏好设置失败:', error)
}
}
// 取
function getLocal (key, defaultValue = null) {
try {
// 读取本地存储的值
const serializedValue = localStorage.getItem(this.prefix + key)
// 如果没读取到则返回null
if (serializedValue === null) return defaultValue
// 将值转换成对象 然后返回
return JSON.parse(serializedValue)
} catch (error) {
console.error('读取偏好设置失败:' + error)
return defaultValue
}
}
// 删
function removeLocal () {
// 获取 localStorage 中所有键的数组,并遍历每个键
Object.keys(localStorage).forEach(key => {
// 检查当前键是否以当前对象的 prefix 属性开头
if (key.startsWith(this.prefix)) {
localStorage.removeItem(key)
}
})
}
判断一个点是否在Canvas的图形内
function isPointInRect(x, y, rect) {
return x >= rect.x && x <= rect.x + rect.width &&
y >= rect.y && y <= rect.y + rect.height
}
const rect = { x: 10, y: 10, width: 100, height: 50 }
console.log(isPointInRect(30, 30, rect)) // true
console.log(isPointInRect(150, 30, rect)) // false
防止按钮重复提交
// 禁用提交按钮 如果提交时网络断开,按钮会一直禁用 需要刷新等手动处理
form.addEventListener('submit', (e) => {
e.preventDefault()
submitBtn.disabled = true
submitBtn.textContent = '提交中...'
setTimeout(() => {
console.log('提交成功')
submitBtn.textContent = '提交成功'
}, 2000)
})
// 前端存储提交记录
form.addEventListener('submit', (e) => {
e.preventDefault()
// 临时存储一个提交了的值
const hasSubmitted = sessionStorage.getItem('formSubmitted')
if(hasSubmitted) {
alert('请勿重复提交')
return
}
sessionStorage.setItem('formSubmitted', 'true')
console.log('表单提交')
})
与其他岗位的沟通
统一接口规范和文档
制定标准:与后端团队约定统一的接口规范(如 RESTful、GraphQL),明确请求/响应格式(如 JSON 结构、状态码定义)。
文档先行:使用工具(如 Swagger、YAPI、Apifox)维护实时更新的接口文档,要求后端在开发前提供清晰的接口定义,包括:
- 请求方法(GET/POST等)
- 路径(URL)
- 参数(Query/Body/Header)及示例
- 响应数据结构及示例
- 错误码说明
版本控制:对接口文档进行版本管理,避免因接口变更导致前后端冲突。
接口环境隔离与配置
在Vue项目中使用 .env 文件区分不同环境(开发 测试 预发布等)
使用 mockjs 或 axios-mock-adapter 模拟后端接口
接口联调阶段划分
-
并行开发:前端基于 Mock 数据开发界面,后端实现接口逻辑。
-
初步联调:后端接口完成后,前端切换到真实环境测试,记录问题并反馈。
-
回归测试:后端修复问题后,前端再次验证接口。
-
定期沟通...
项目从0到1搭建
-
使用 Vite 创建项目
- Assets 静态资源
- Components 全局可复用组件
- Composables 组合式函数
- Router 路由配置
- Stores 状态管理
- Styles 全局样式
- Utils 工具函数(路由守卫)
- Views 页面级组件
-
安装必要插件
- vue-router
- Element-plus
- Axios
-
项目配置
-
export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, './src') // 用 @ 代替 ./src } }, server: { port: 3000, // 指定端口号 open: true // 服务器启动后,自动在默认浏览器中打开应用程序 }, build: { outDir: 'dist', sourcemap: true } })
-
-
路由配置
-
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' const routes: Array<RouteRecordRaw> = [ { path: '/', name: 'Home', component: () => import(@/views/HomeView.vue) }, { path: '/about', name: 'About', component: () => import(@/views/AboutView.vue) } ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) export default router
-
-
Pinia 状态管理
-
import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })
-
-
主入口文件
-
import { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' import { ElementPlus } from 'element-plus' import 'element-plus/dist/index.css' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app')
-
多并发处理
- 请求合并与批量处理
// 定义一个异步函数 fetchMultipleData
// 接收一个 apis 参数
// apis 是一个包含多个接口配置的对象数组
async function fetchMultipleData(apis) {
// 使用 try...catch 捕获可能出现的错误
try {
// 使用 map 遍历 apis 数组,对每个接口调用 fetch 函数,返回一个 Promise 数组
// fetch 的第二个参数是请求配置
// 这里使用了 api 对象中的 method 属性(如 'GET'、'POST' 等)
const requests = apis.map(api => fetch(api.url, { method: api.method }))
// 使用 Promise.all 等待所有 fetch 请求完成
// 返回一个包含所有 Response 对象的数组
const responses = await Promise.all(requests)
// 再次使用 Promise.all 等待所有响应的 json() 方法解析完成
// 返回一个包含所有解析后数据的数组
// 注意:responses.map(res => res.json()) 会生成一组 Promise
// 所以需要用 Promise.all 等待
return await Promise.all(responses.map(res => res.json()))
} catch (error) {
console.log(error)
throw error
}
}
import { useData } from './composables/useData';
export default {
setup() {
const { data, loading, error } = useData(fetchMultipleData([
{ url: '/api/users', method: 'GET' },
{ url: '/api/products', method: 'GET' }
]));
return { data, loading, error };
}
}
防抖节流
防抖
在用户停止输入一段时间后才触发请求,避免频繁请求,适用于搜索输入框
<!-- lodash的debounce -->
<template>
<input v-model="keyword" @input="handleInput" />
</template>
<script setup>
import { ref } from 'vue'
import { debounce } from 'lodash'
const keyword = ref('')
// 使用 debounce 包装搜索函数
const search = debounce(() => {
console.log('搜索关键词:', keyword.value)
}, 500)
const handleInput = () => {
search()
}
// 组件卸载时自动取消(如果使用 setup 语法糖,需在 onUnmounted 中处理)
import { onUnmounted } from 'vue'
onUnmounted(() => {
search.cancel()
})
</script>
<!-- 手写防抖 -->
<script>
const handleInput = (e) => {
clearTimeout(timer)
timer = setTimeout(() => {
const query = e.target.value.trim()
if(query) {
console.log('发送搜索请求:', query)
// 实际API的调用
// fetch(`/api/search?q=${query}`)
}
}, 500)
}
</script>
节流
在固定时间间隔内最多触发一次请求,适用于滚动加载
<!-- lodash的throttle -->
<template>
<div @scroll="handleScroll">...</div>
</template>
<script setup>
import { throttle } from 'lodash'
import { onUnmounted } from 'vue'
const onScroll = () => {
console.log('滚动触发')
}
const throttledScroll = throttle(onScroll, 200)
const handleScroll = () => {
throttledScroll()
}
onUnmounted(() => {
throttledScroll.cancel()
})
</script>
<!-- 手写节流 -->
<script>
let lastTime = 0
const handleInput = (e) => {
const now = Date.now()
if(now - lastTime < 500) return
lastTime = now
const query = e.target.value.trim()
if(query) {
console.log('发送搜索请求:', query)
// 实际API的调用
// fetch(`/api/search?q=${query}`)
}
}
</script>
Vue列表组件的key的作用
- 高效更新虚拟 DOM
- 保持组件状态(勾选框等)
- 避免渲染错误
正确使用key:
v-for ➡️ 避免使用数组索引作为key,除非列表是静态而且不会重新排序
['1', '2', '3'].map(parseInt)
// 输出:
// [1, NaN, NaN]
// map有三个参数 当前值 下标 原数组 parseInt只能接收两个参数也就是 map 的前两个参数
// string是需要转换成数字的字符串 radix是代表需要转成多少进制 2~36
// parseInt(string, radix)
// 第一轮遍历
parseInt('1', 0) // radix为0时默认是十进制 返回 1
// 第二轮遍历
parseInt('2', 1) // radix为1 要求在2~36 返回NaN
// 第三轮遍历
parseInt('3', 2) // radix为2,二进制,但 3 不在有效范围内 二进制有效数字只能是0和1 返回NaN
// 最后返回 [1, NaN, NaN]
setTimeout Promise Async/Await 的区别
// setTimeout:宏任务
// 延时输出,延时时间自定义
// 无法直接处理异步操作的结果或错误
setTimeout(() => {
console.log(1)
}, 500)
// Promise:微任务
// 封装异步操作,提供链式调用和状态管理
// Async/Await
// 基于Promise的语法糖,用同步风格编写异步代码
Async Await如何通过同步的方式实现异步
npm安装机制,为什么输入npm i 可以自动安装对应模块
Object.prototype.toString.call() instance of Array.isArray()
// Object.prototype.toString.call()
// 通用性强,但不够简洁,还需要额外处理返回值
const arr = [1, 2, 3]
console.log(Object.prototype.toString.call(arr)) // "[object, Array]"
// instanceof
// 写法简单,性能比Object.prototype.toString.call()更快
const arr = [1, 2, 3]
console.log(arr instanceof Array) // true
// Array.isArray()
// 写法简单,JS官方推荐
const arr = [1, 2, 3]
console.log(Array.isArray(arr))
回流和重绘 优化
重绘:元素的外观属性改变,不影响布局时
回流:元素的几何属性(宽高/边距/位置等)或内容(文本/图片尺寸)改变,一定会触发重绘
优化方案:
-
批量修改 DOM
-
避免频繁读取布局属性
-
防抖节流
const和let的变量存储位置
存储在当前的词法环境中,是JavaScript引擎内部的数据结构
Vue3的双向数据绑定 底层如何实现互相改变
reactive() ref() 使用Proxy包装对象,拦截所有读写的操作
访问响应式数据时,触发 Proxy 的 get 拦截器
get 拦截器会调用 track() 函数,收集依赖
修改响应式数据时,触发 Proxy 的 set 拦截器
set 拦截器调用 trigger() 函数,重新渲染组件
<input v-model="state.count" />
<!-- = -->
<input :value="state.count" @input="state.count = $event.target.value" />
在Vue中,子组件为什么不能修改父组件传过来的数据
-
单向数据流:
- 可预测性:数据变化唯一来源是父组件,便于调试
- 可维护性:避免子组件意外修改导致数据混乱
可以通过事件通知父组件修改:
// 子组件
props: ['value'],
methods: {
updateValue() {
this.$emit('input', 'new value')
}
}
// 父组件
<ChildComponent :value="parentValue" @input="parentValue = $event" />
HTTPS握手过程
暂时无法在飞书文档外展示此内容
apply()和call() 区别
function introduce(city, age) {
console.log(this.name + ' 在' + city + ',年龄' + age)
}
const user = { name: '小明' }
// 一个一个传
introduce.call(user, '北京', 20) // 小明 在北京,年龄20
// 数组传
introduce.apply(user, ['上海', 22]) // 小明 在上海,年龄22
// call 通常更快
// apply需要将数组参数结构为独立参数,而call直接传递参数
// 需要动态生成参数列表时可以直接使用apply
// ES6 的优化方案
// 使用展开运算符可以让call接受数组参数,同时保持性能优势
Vue 中的 Object.defineProperty 有什么缺陷
-
无法自动监听对象新增或删除的属性
- 对于已经初始化的对象,通过直接赋值新增属性(比如:obj.newData = value)或者删除(delete obj.a),Object.defineProperty无法触发响应式更新
-
对数组的监听不完整
- 通过索引修改数组元素(arr[0] = newValue)或者修改数组长度(arr.length = newLength)都无法触发更新
-
深度监听性能消耗大
- 对嵌套对象进行响应式处理时,需递归遍历所有属性并逐个定义getter/setter,若对象层级深或数据量大,初始化阶段会消耗大量性能。(原因:Object.defineProperty 递归遍历是同步且强制的,无论属性是否被访问了,都需要提前劫持)
三种元素隐藏方式
-
Opacity: 0(需要保留交互或动画)
- 视觉隐藏:透明不可见,但保留布局
- 交互性:仍然可以响应点击,悬停等事件
- 渲染:元素会被渲染到页面上,占用GPU资源
-
Visibility: hidden(需要保留布局但隐藏内容)
- 视觉隐藏:不可见,但保留布局
- 交互性:不响应任何事件
- 渲染:元素会被渲染,但浏览器不绘制内容
-
Display: none(需要彻底移除布局而且没有交互)
- 视觉隐藏:不可见,不保留布局,后续元素会填补位置
- 交互性:不响应任何事件(完全从 DOM 中移除渲染)
- 渲染:元素不会被渲染,不占用GPU资源