穗腾科技
刀刀
6/20/2025
0 字
0 分钟
一面
Q:讲一下 Vue2 和 Vue3 在 组件通信、响应式、性能优化有什么区别
我的回答
Vue2 响应式原理是
Object.defineProperty
,在 Vue2 中如果给对象添加一个新的属性,是不会触发响应式的,因为没有绑定getter
和setter
;而数组是通过遍历,给每一项绑定响应式,如果层级多需要深度递归绑定。对于数组的方法如push
、pop
、shift
、unshift
、splice
、sort
、reverse
等,Vue2 会对这些方法进行重写,从而实现响应式。Vue3 响应式原理是
Proxy
,它代理是直接代理整个对象,不管是新增还是删除属性,都会触发响应式。Vue2 想要给对象添加一个新属性,或者通过索引改变数组,需要使用
Vue.set
或Vue.delete
,Vue3 不需要。Vue2 组件通信是通过
props
和$emit
实现,父组件拿子组件数据通过this.$refs
,子传父通过this.$emit
触发父组件的事件。Vue3 组件通信是通过props
和emit
实现,父组件想要拿子组件数据需要子组件通过defineExpose
暴露数据,父组件通过ref
拿到子组件实例,再拿到子组件数据。
Q:讲一下 Pinia 对比 Vuex 的优势
我的回答
Pinia 是 useHooks 的形式,Vuex 是组合式 API,参数只能定义在
state
,在mutations
中同步修改值,异步操作只能在actions
中。Pinia 可以直接用ref
定义值,定义函数,直接修改值,还能通过插件实现本地存储
Deepseek 回答
特性 | Vuex | Pinia |
---|---|---|
Vue 版本支持 | Vue 2 & 3 | 专为 Vue 3 设计(兼容 Vue 2) |
TypeScript 支持 | 需要额外声明 | 完美支持(自动类型推断) |
API 设计 | 较复杂(mutations/actions) | 极简 API(actions 统一操作) |
模块化 | 需要 modules 配置 | 自动模块化(文件即模块) |
代码分割 | 需动态注册模块 | 开箱即用(按需加载) |
DevTools 集成 | 支持 更优支持(时间线/编辑等) | |
包体积 | 较大(2.6KB gzip) | 更轻量(1.5KB gzip) |
Composition API | 兼容(但不够自然) | 原生支持(完美契合) |
极简的 API 设计(核心优势)
Pinia 移除了 Vuex 中繁琐的 mutations:
javascript// Vuex 的修改操作 mutations: { SET_USER(state, user) { state.user = user; } }, actions: { async fetchUser({ commit }) { const user = await api.getUser(); commit('SET_USER', user); // 需通过 mutation } } // Pinia 的修改操作(直接通过 action) actions: { async fetchUser() { this.user = await api.getUser(); // 直接赋值 } }
完美的 TypeScript 支持
Pinia 提供全自动类型推断:
typescript// store/user.ts export const useUserStore = defineStore('user', { state: () => ({ name: 'Alice', age: 28 }), getters: { doubleAge(): number { // 自动推断返回类型 return this.age * 2; } } }); // 组件中使用 const store = useUserStore(); store.name // 类型为 string store.doubleAge // 类型为 number
极简的模块化设计
Pinia 采用文件即模块的零配置方案:
textstores/ ├─ user.ts // useUserStore() ├─ product.ts // useProductStore() └─ cart.ts // useCartStore()
更轻量高效
- 包体积减少约 40%
- 无嵌套模块,状态扁平化
- 更好的性能优化
Composition API 原生支持
Pinia 与 Composition API 完美融合:
javascript// 在 setup 中使用 import { useUserStore } from '@/stores/user'; export default { setup() { const userStore = useUserStore(); // 计算属性自动更新 const isAdult = computed(() => userStore.age >= 18); return { userStore, isAdult }; } }
更强大的 DevTools 支持
- 时间线追踪:清晰展示状态变化时序
- Store 热更新:修改代码后保持状态
- 直接编辑状态:调试时实时修改
- Action 日志:记录参数和耗时
Q:讲一下 ts 的 常用配置
我的回答
变量: 类型
来声明类型;as
断言;?
可选;!
非空;T
泛型;Pick
Omit
等。
Q:之前用 JS 如何确保健壮性
我的回答
人为用
if
手写校验判断规则,不符合则return
错误信息。
Deepseek 回答
严格的输入验证
- 验证所有外部输入:用户输入、API响应、文件内容等
- 类型检查:使用 typeof 和 instanceof 确保正确的数据类型
- 范围验证:检查数字、字符串长度等是否在有效范围内
- 模式验证:使用正则表达式验证复杂格式(如邮箱、URL)
全面的错误处理
- 使用 try/catch:捕获同步代码中的错误
- Promise.catch():处理异步操作中的错误
- 全局错误处理:捕获未处理的错误和Promise拒绝
- 自定义错误类型:创建特定错误类以便更好地区分错误
防御性编程
- 空值和未定义检查:使用可选链 ?. 和空值合并 ??
- 参数默认值:为函数参数提供合理的默认值
- 边界条件处理:考虑极端情况和边界值
- 资源清理:使用 finally 块确保资源释放
代码可维护性
- 模块化设计:将代码拆分为小的、单一职责的函数
- 清晰命名:使用有意义的变量和函数名
- 代码注释:解释复杂的逻辑和决策
- 一致性:遵循一致的代码风格和模式
测试策略
- 单元测试:验证单个函数或模块的行为
- 集成测试:测试多个组件的交互
- 端到端测试:模拟真实用户场景
- 边界测试:测试极端值和边界条件
异步健壮性
- Promise处理:正确处理异步操作的成功和失败
- 超时机制:为异步操作设置合理的超时
- 取消支持:提供取消异步操作的能力
- 并发控制:限制同时进行的异步操作数量
安全实践
- 防止注入攻击:清理用户输入,避免直接执行
- 内容安全策略:实施CSP防止XSS攻击
- 避免敏感信息泄露:不在客户端存储敏感数据
- HTTPS:始终通过安全连接传输数据
Q:视频分销项目怎么结果多端兼容性问题
我的回答
小差异用注释
#ifdef
处理
Deepseek 回答
平台检测与适配
- 使用
uni.getSystemInfoSync()
获取当前平台信息 - 条件编译:使用
#ifdef
和#ifndef
处理平台差异 - 环境变量:
process.env.UNI_PLATFORM
获取编译平台 - 自定义适配层:封装平台适配函数
- 使用
API兼容性处理
- 安全调用:检查API是否存在再调用
- API封装:对平台差异API进行统一封装
- 降级方案:为不支持的API提供备选方案
- Promise封装:统一异步API的调用方式
组件兼容性
- 条件渲染:使用条件编译渲染不同组件
- 自定义兼容组件:封装跨平台组件
- 组件属性适配:处理不同平台的属性差异
- 组件扩展文件:.h5.vue, .mp.vue, .app.vue
样式兼容性
- 使用Flex布局:确保布局的响应式
- rpx单位:使用Uniapp的响应式单位
- 条件样式:使用条件编译处理样式差异
- 平台特定样式文件:style.h5.css, style.mp.css
- CSS变量:使用CSS变量管理主题和尺寸
常见兼容性问题及解决方案
问题1:导航栏差异
解决方案:
javascript// 获取导航栏高度 function getNavbarHeight() { const sysInfo = uni.getSystemInfoSync(); // #ifdef MP-WEIXIN const menuButton = wx.getMenuButtonBoundingClientRect(); return menuButton.bottom + menuButton.top - sysInfo.statusBarHeight; // #endif // #ifdef H5 return 44; // H5固定导航栏高度 // #endif // #ifdef APP-PLUS return sysInfo.statusBarHeight + 44; // #endif return 44; // 默认值 }
问题2:支付功能差异
解决方案:
javascriptfunction pay(options) { // #ifdef MP-WEIXIN return uni.requestPayment({ provider: 'wxpay', ...options }); // #endif // #ifdef MP-ALIPAY return uni.requestPayment({ provider: 'alipay', ...options }); // #endif // #ifdef APP-PLUS // iOS应用内支付 if (plus.os.name === 'iOS') { return uni.requestPayment({ provider: 'appleiap', ...options }); } // Android支付 return uni.requestPayment({ provider: 'paymentService', // 需要配置支付服务 ...options }); // #endif // #ifdef H5 // H5支付跳转 window.location.href = options.paymentUrl; // #endif }
问题3:文件系统差异
解决方案:
javascriptasync function readFile(filePath) { // #ifdef H5 // 使用File API const response = await fetch(filePath); return await response.blob(); // #endif // #ifdef MP-WEIXIN return new Promise((resolve, reject) => { wx.getFileSystemManager().readFile({ filePath, encoding: 'binary', success: res => resolve(res.data), fail: reject }); }); // #endif // #ifdef APP-PLUS return new Promise((resolve, reject) => { plus.io.resolveLocalFileSystemURL(filePath, entry => { entry.file(file => { const reader = new plus.io.FileReader(); reader.onload = e => resolve(e.target.result); reader.onerror = reject; reader.readAsArrayBuffer(file); }, reject); }, reject); }); // #endif }
Q:接到新需求时开发前会不会先考虑差异性问题
我的回答
比如开发蓝牙功能,会先去看
uniapp
有没有适配多端,没有的话就去官方看看有没有人有相关问题解决。
Q:怎么优化地图echart图渲染
我的回答
echart
图在左右两侧,通过IntersectionObserver
监听滚动,当滚动到指定位置时,再去渲染那些echart
图。
Deepseek 回答
一、数据层优化(核心瓶颈)
压缩 GeoJSON 数据
使用 mapshaper 简化地理数据(减少 50%-90% 体积):
bashmapshaper input.geojson -simplify 10% -o output.geojson
精度控制:保留小数点后 4 位(经纬度)
案例:某省级地图从 2.3MB → 280KB,加载速度提升 5 倍
按需加载矢量边界
javascript// 动态注册地图(避免全局加载) import simplifiedChina from './china_simplified.json'; echarts.registerMap('china', simplifiedChina); const option = { series: [{ type: 'map', map: 'china' }] };
离散点数据聚合
10万+点位时使用 Grid 聚类:
javascriptseries: [{ type: 'scatter', coordinateSystem: 'geo', cluster: true, data: pointsArray // 原始经纬度数组 }]
二、渲染策略优化
渐进渲染(大数据场景)
javascriptchart.setOption({ series: [{ progressive: 2000, // 每次渲染2000个点 progressiveThreshold: 5000 // 超过5000点启用分片渲染 }] });
分层绘制复杂元素
javascript// 基础轮廓层(静态) { type: 'map', map: 'china', silent: true, itemStyle: { ... } } // 动态数据层(单独控制) { type: 'scatter', zlevel: 2, symbolSize: 8, data: [...] }
WebWorker 异步计算
将路径计算/聚类等 CPU 密集型操作移至 Worker:
javascriptconst worker = new Worker('./geoCluster.js'); worker.postMessage(rawData); worker.onmessage = (e) => chart.setOption({ series: [{ data: e.data }] });
三、配置关键参数
参数 | 推荐值 | 作用 |
---|---|---|
hoverLayerThreshold | 5000 | 超过该点数禁用鼠标事件 |
animation | false 大数据关闭动画 | |
blendMode | 'lighter' | 叠加效果更轻量 |
large | true | 开启大数据模式 |
useDirtyRect | true (v5.4.1+) | 局部渲染(增量更新) |
四、内存泄漏防御 |
// 销毁时释放资源(SPA 必备)
beforeUnmount() {
chart.dispose(); // 销毁实例
echarts.unregisterMap('china'); // 释放地图内存
}
Q:如果有多个地图扎点,如何优化
我的回答
分别渲染,每次渲染20个,分多次渲染
Q:还有其他方法么
我的回答
暂时没想到
Q:如何封装echart图
我的回答
封装成组件,以柱状图为例,x轴y轴等固定的参数直接封装,是否需要单位、数据、柱体颜色宽度、
tooltip
统一props
传入。
Deepseek 回答
echart组件代码示例
// EChartWrapper.vue
<template>
<div ref="chartDom" class="echart-container" :style="{ width, height }"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, shallowRef } from 'vue';
import * as echarts from 'echarts/core';
import {
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components';
import { BarChart, LineChart, PieChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
// 按需注册组件
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
BarChart,
LineChart,
PieChart,
CanvasRenderer
]);
// TypeScript 类型定义
interface Props {
width?: string;
height?: string;
option: echarts.EChartsCoreOption;
theme?: string | object;
autoResize?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
width: '100%',
height: '400px',
autoResize: true,
theme: 'default'
});
const chartDom = ref<HTMLDivElement | null>(null);
const chartInstance = shallowRef<echarts.ECharts | null>(null);
// 初始化图表
const initChart = () => {
if (!chartDom.value) return;
// 销毁旧实例
if (chartInstance.value) {
chartInstance.value.dispose();
}
// 创建新实例
chartInstance.value = echarts.init(
chartDom.value,
props.theme,
{ renderer: 'canvas' } // 强制使用Canvas
);
// 设置配置项
chartInstance.value.setOption(props.option);
// 自动响应式
if (props.autoResize) {
window.addEventListener('resize', handleResize);
}
};
// 响应式调整
const handleResize = () => {
chartInstance.value?.resize();
};
// 监听配置变化
watch(
() => props.option,
(newOption) => {
if (!chartInstance.value) return;
chartInstance.value.setOption(newOption, true); // 第二个参数表示不合并配置
},
{ deep: true }
);
// 生命周期
onMounted(initChart);
onBeforeUnmount(() => {
if (chartInstance.value) {
if (props.autoResize) {
window.removeEventListener('resize', handleResize);
}
chartInstance.value.dispose();
}
});
// 暴露实例方法
defineExpose({
getInstance: () => chartInstance.value,
resize: handleResize
});
</script>
<style scoped>
.echart-container {
background: white;
overflow: hidden;
}
</style>
事件统一处理:
事件统一处理代码
// 事件映射表
const eventMap = {
click: 'click',
mouseover: 'mouseover',
// ...其他事件
};
// 自动绑定事件
const bindEvents = () => {
if (!chartInstance.value) return;
Object.entries(eventMap).forEach(([emitName, echartsEvent]) => {
chartInstance.value?.on(echartsEvent, (params) => {
emit(emitName, params);
});
});
};
// 在initChart中调用
initChart();
bindEvents();
按需加载模块:
按需加载模块代码
// 动态加载图表类型
const loadChartModule = async (type: string) => {
switch (type) {
case 'map':
const [map] = await Promise.all([
import('echarts/charts').then(m => m.MapChart),
import('echarts/components').then(m => m.GeoComponent)
]);
echarts.use([map]);
break;
// 其他类型处理
}
};
// 根据option自动检测
watch(() => props.option, (newOption) => {
const seriesTypes = newOption.series?.map(s => s.type) || [];
seriesTypes.forEach(type => loadChartModule(type));
});
Q:音果项目SKU组件封装的思路,为什么要封装
我的回答
封装成组件,方便快速落地,组件高内聚低耦合
Q:如何迁移项目 Vue2 到 Vue3
我的回答
没做过,想法是先升级到 Vue2.7,可适配 2 和 3,然后逐步迁移。
Deepseek 回答
- 组合式 API 优先迁移javascript
// Before (Vue2 Options API) export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } } // After (Vue3 Composition API) import { ref } from 'vue' export default { setup() { const count = ref(0) const increment = () => { count.value++ } return { count, increment } } }
- 组件迁移路线图
- 关键变更点处理
- 生命周期映射
Vue2 Vue3 说明 beforeCreate setup() 使用 setup 替代 created setup() 使用 setup 替代 beforeMount onBeforeMount mounted onMounted beforeUpdate onBeforeUpdate updated onUpdated beforeDestroy onBeforeUnmount destroyed onUnmounted - API 变更处理js
// 事件总线替代方案(Vue2) // Vue.prototype.$bus = new Vue() // Vue3 使用 mitt 库 import mitt from 'mitt' const emitter = mitt() // 发送事件 emitter.emit('event-name', data) // 监听事件 emitter.on('event-name', (data) => { // 处理事件 })
- 全局 API 变更js
// Vue2 Vue.component('comp', Component) Vue.directive('focus', FocusDirective) // Vue3 const app = createApp(App) app.component('comp', Component) app.directive('focus', FocusDirective)
- v-model 升级vue
<!-- Vue2 --> <ChildComponent v-model="pageTitle" /> <!-- 等价于 --> <ChildComponent :value="pageTitle" @input="pageTitle = $event" /> <!-- Vue3 --> <ChildComponent v-model:title="pageTitle" /> <!-- 等价于 --> <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
- 生命周期映射
- 依赖迁移
- 库迁移
库名称 Vue3 支持状态 替代方案 Vuex 兼容 (v4.x) Pinia (推荐) Vue Router 兼容 (v4.x) Element UI 不兼容 Element Plus - 依赖迁移bash
# 更新 Vue 全家桶 npm uninstall vue vue-router vuex npm install vue@next vue-router@4 vuex@4 # UI 库迁移 npm uninstall element-ui npm install element-plus # 状态管理迁移 npm uninstall vuex npm install pinia
- 库迁移
Q:有没有了解后台管理框架
我的回答
没怎么接触过
Q:拉一个新项目下来,如何设置权限
我的回答
后端返回权限列表,前端通过动态路由添加。组件权限用自定义事件
Deepseek 回答
- 路由级权限控制(Vue示例)javascript
// router.js import { createRouter } from 'vue-router' const routes = [ { path: '/admin', component: () => import('./Admin.vue'), meta: { requiresAuth: true, permissions: ['admin:access'] } }, { path: '/reports', component: () => import('./Reports.vue'), meta: { requiresAuth: true, permissions: ['report:view'] } } ] router.beforeEach((to, from, next) => { const authStore = useAuthStore() // 检查登录状态 if (to.meta.requiresAuth && !authStore.isAuthenticated) { return next('/login') } // 检查权限 if (to.meta.permissions) { const hasPermission = authStore.hasPermissions(to.meta.permissions) if (!hasPermission) return next('/forbidden') } next() })
- 组件级权限控制vue
<template> <div> <button v-if="hasPermission('user:create')">添加用户</button> <button v-if="hasPermission('user:delete')">删除用户</button> </div> </template> <script> import { useAuthStore } from '@/stores/auth' export default { setup() { const authStore = useAuthStore() const hasPermission = (permission) => { return authStore.hasPermission(permission) } return { hasPermission } } } </script>
- 权限指令(高级用法)javascript
// directives/permission.js export default { mounted(el, binding) { const authStore = useAuthStore() const { value } = binding if (!authStore.hasPermission(value)) { el.parentNode?.removeChild(el) } } } // 使用 <button v-permission="'user:delete'">删除</button>
Q:echart图x轴实现滚动
我的回答
dataZoom
Q:vite等构建工具的配置
我的回答
base、publicPath、代理、别名、build打包等
Deepseek 回答
基础配置
javascriptimport { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; // Vue 项目 // import react from '@vitejs/plugin-react'; // React 项目 export default defineConfig({ // 项目根目录(默认 process.cwd()) root: './src', // 公共基础路径(部署子目录时使用) base: '/my-app/', // 插件配置 plugins: [vue()], // 开发服务器配置 server: { port: 3000, // 端口号 open: true, // 自动打开浏览器 proxy: { // 代理配置 '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }, // 构建配置 build: { outDir: '../dist', // 输出目录 assetsDir: 'assets', // 静态资源目录 sourcemap: true, // 生成 sourcemap minify: 'terser', // 代码压缩工具 }, // 解析配置 resolve: { alias: { // 路径别名 '@': path.resolve(__dirname, './src'), 'components': '/src/components' } } });
常用插件配置
javascript// 示例插件(需先安装) import legacy from '@vitejs/plugin-legacy'; // 传统浏览器支持 import svgr from 'vite-plugin-svgr'; // SVG 组件 import checker from 'vite-plugin-checker'; // TypeScript 检查 plugins: [ vue(), legacy({ targets: ['defaults', 'not IE 11'] }), svgr(), checker({ typescript: true }) // TS 类型检查 ]
环境变量配置
创建 .env 文件:
bash.env # 所有环境 .env.local # 本地覆盖(gitignore) .env.development # 开发环境 .env.production # 生产环境
内容格式(以 VITE_ 开头):
envVITE_API_URL=https://api.example.com VITE_DEBUG=true
使用变量:
javascriptconsole.log(import.meta.env.VITE_API_URL);
Q:项目上线跨域问题怎么解决
我的回答
前端设置
ngnix
代理
Deepseek 回答
方案 1:Nginx 反向代理(推荐)
jsserver { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # API 代理 location /api/ { proxy_pass http://backend-server:8080/; # 后端服务器 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # CORS 头(可选,也可在后端设置) add_header 'Access-Control-Allow-Origin' '$http_origin' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' > always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' > always; # 处理 OPTIONS 预检请求 if ($request_method = 'OPTIONS') { return 204; } } }
方案 2:后端配置 CORS 头
以 Node.js (Express) 为例:
javascript// server.js const express = require('express'); const cors = require('cors'); const app = express(); // 基本 CORS 配置 app.use(cors()); // 高级配置 app.use(cors({ origin: ['https://your-frontend.com', 'https://another-domain.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400 })); // 手动设置 CORS 头 app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'https://your-frontend.com'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.header('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); });
方案 3:JSONP(仅限 GET 请求)
javascript// 前端 function jsonpRequest(url, callbackName) { const script = document.createElement('script'); script.src = `${url}?callback=${callbackName}`; document.body.appendChild(script); } // 全局回调函数 window.handleResponse = (data) => { console.log('Received:', data); }; // 发起请求 jsonpRequest('https://api.example.com/data', 'handleResponse');