第二项目重新梳理
本文围绕uni-app开发实践展开技术问答,总结了小程序开发的关键技术和解决方案。主要内容包括:1、uni-app开发优势:跨端能力强,一套代码可编译运行在微信小程序、H5、App等多端,基于Vue语法开发效率高。2、核心技术实现3、性能优化4、特殊功能实现5、开发调试6、兼容性处理7、简历投递策略本文提供了uni-app小程序开发的完整技术方案,涵盖从基础架构到高级功能的实现细节,可作为开发实践
本次会议围绕张同学的uni-app项目经验展开技术问答,并讨论了简历投递策略。
小结
1. 项目技术方案与实现
项目介绍: 芜花律师管理平台是一个线上全流程数字化的小程序,覆盖了用户申请、律师咨询、文书生成及在线结算等环节。
技术选型: 采用uni-app框架进行开发,以支持微信小程序、H5、App等多端,利用Vue语法提升开发效率。
核心功能实现:
响应式适配: 通过配置rpx基准值和使用flex等布局方式实现。
网络请求: 封装 uni.request 进行二次开发,包含请求拦截、响应拦截、异常捕获和loading提示。
登录鉴权: 通过uni.login获取code,后端换取openid和自定义token,前端存储token并携带在请求头中进行鉴权。
路由拦截: 在uni-app中使用interceptor拦截路由,检查token有效性,无token则跳转至登录页。
组件化: 使用uni-ui组件库中的轮播图、自定义导航栏、卡片等组件,并对常用组件进行二次封装复用。
2. 性能优化与兼容性处理
性能优化: 包括图片压缩(前端和后端)、长列表使用虚拟滚动、代码压缩和Gzip等。
兼容性处理: 使用条件编译区分iOS和Android,针对不同平台调整样式和逻辑。
待办
1. 技术知识梳理与准备
张同学需要会后进一步研究uni.setStorage和uni.setStorageSync的区别。
张同学需要了解uni-app中的分片上传断点续传的具体实现流程。
2. 简历投递
张同学需要在会后立即开始在Boss直聘上进行简历投递。
张同学需要修改简历中的个人头像,更换为更正式的照片。
1、您为什么选择 Uni-app 开发小程序?它的优势是什么?
答案: Uni-app 支持 一套代码 编译到 小程序、H5、App 等多个平台,降低维护成本。它基于 Vue 语法,开发效率高,插件生态丰富。本项目目前只需要小程序,但为未来扩展 H5 或 App 留有余地。
我选择 UniApp 开发小程序,核心有三点优势:
- 跨端能力强,一套代码 可编译运行在 微信小程序、H5、App 等多端,适配性强,便于业务后续多平台拓展;
- 上手成本低,基于 Vue 语法开发,技术栈统一,开发和学习效率更高;
- 生态完善,插件、UI 组件库丰富,能快速落地业务需求。当前项目虽仅需小程序端,使用 UniApp 也能为后续拓展 H5、APP 业务提前预留扩容空间,减少后期重构成本。
2、您如何搭建 Uni-app 项目的开发环境?
答案: 使用 HBuilderX 创建 Uni-app 项目,或通过 Vue CLI + @dcloudio/vite-plugin-uni 搭建。配置 pages.json 管理页面路径 "pages" 、tabBar、globalStyle。安装 uView UI 并按照文档配置 easycom 和 SCSS 变量。
{ "easycom": { "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" }, "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages { // 小灰狼新改( 首页选择跳转 ) "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } }, { // 小灰狼新写(用户端首页) "path": "pages/homepage/index", "style": { "navigationBarTitleText": "法律首页" } }, { // 小灰狼新写 "path": "pages/freeconsult/index", "style": { "navigationBarTitleText": "免费咨询专业律师" } }, { // 小灰狼新写 "path": "pages/asklawyer/index", "style": { "navigationBarTitleText": "专业律师在线服务" } }, { // 小灰狼新写 "path": "pages/payservice/index", "style": { "navigationBarTitleText": "选择服务类型" } }, { // 小灰狼新写(追欠款) "path": "pages/arrears/index", "style": { "navigationBarTitleText": "填写欠款详情" } }, { // 小灰狼新写(打官司) "path": "pages/litigation/index", "style": { "navigationBarTitleText": "请填写信息" } }, // { // 小灰狼新写(我要咨询) // "path": "pages/consult/index", // "style": { // "navigationBarTitleText": "我要咨询" // } // }, { // 小灰狼新改(找专家) "path": "pages/sy/flzx", "style": { "navigationBarTitleText": "法律咨询" } }, { // 小灰狼新改(推荐律师) "path": "pages/grxq/index", "style": { "navigationBarTitleText": " ", "navigationStyle": "custom", "app-plus": { "titleView": false } } }, { // 小灰狼新改(私人包月) "path": "pages/bmonth/bmonth", "style": { "navigationBarTitleText": "包月服务" } }, { // 小灰狼新写(立即咨询) "path": "pages/imconsult/index", "style": { "navigationBarTitleText": "快速咨询" } }, { // 小灰狼新写(一问多答) "path": "pages/qanda/index", "style": { "navigationBarTitleText": "一问多答" } }, { // 小灰狼新写(去支付) "path": "pages/payment/index", "style": { "navigationBarTitleText": "支付" } }, { // 小灰狼新写(须知) "path": "pages/notice/index", "style": { "navigationBarTitleText": "法律服务下单须知" } }, // { // 小灰狼新写(我的) // "path": "pages/ucenter/index/index.wxml", // "style": { // "navigationBarTitleText": "个人中心" // } // }, { // 小灰狼 "path": "pages/person/message/index", "style": { "navigationBarTitleText": "消息通知" } }, { // 小灰狼新改( 我的 ) "path": "pages/person/index", "style": { "navigationBarTitleText": "" } }, { "path": "pages/person/collect/index", "style": { "navigationBarTitleText": "我的收藏", "enablePullDownRefresh": false } }, { // 小灰狼新改( 个人中心 ) "path": "pages/person/personCenter/index", "style": { "navigationBarTitleText": "个人中心" } }, { // 小灰狼新写( 律师入驻 ) "path": "pages/lawyerentry/index", "style": { "navigationBarTitleText": "律师入驻" } }, { // 小灰狼接手(律师端首页)最新/法援 "path": "pages/lawyerindex/quest", "style": { "navigationBarTitleText": "问题", "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#ff0000" } }, { // 小灰狼接手(律师端首页)最新 => 回答页面 "path": "pages/lawyerindex/answer", "style": { "navigationBarTitleText": "回答", "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#ff0000" } }, { // 小灰狼接手(律师端首页)最新 => 已回复页面 "path": "pages/lawyerindex/replied", "style": { "navigationBarTitleText": "问题", "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#ff0000" } }, { "path": "pages/lawyerindex/fyxq", "style": { "navigationBarTitleText": "法援详情", "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#ff0000" } }, { // 小灰狼接手(律师端)我的 "path": "pages/lawyerindex/index", "style": { "navigationBarTitleText": "律师首页", "navigationBarBackgroundColor": "#ff0000" } }, { "path": "pages/tim/record", "style": { "navigationBarTitleText": "uni-app" } }, { "path": "pages/liaotian/liaotian", "style": { "navigationBarTitleText": "聊天", "enablePullDownRefresh": false, "navigationStyle": "custom" } }, { "path": "pages/logon/logon", "style": { "navigationBarTitleText": "用户注册", "navigationBarBackgroundColor": "#ff0000" } }, { "path": "pages/zhuce/zhuce", "style": { "navigationBarTitleText": "用户登录", "navigationBarBackgroundColor": "#ff0000" } }, { "path": "pages/uppaswd/uppaswd", "style": { "navigationBarTitleText": "忘记密码", "navigationBarBackgroundColor": "#ff0000" } }, { "path": "pages/renzheng/renzheng", "style": { "navigationBarTitleText": "金牌律师认证", "enablePullDownRefresh": false, "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#ff0000" } }, { "path": "pages/person/invite/index", "style": { "navigationBarTitleText": "邀请有礼", "enablePullDownRefresh": false, "navigationStyle": "custom" } }, { "path": "pages/tim/room", "style": { "navigationBarTitleText": "聊天室", "app-plus": { //修复聊天页 app端输入法上顶问题 "softinputMode": "adjustResize" } } } ], "globalStyle": { "navigationBarTextStyle": "white", "navigationBarTitleText": "uniapp", "navigationBarBackgroundColor": "#1aa2e6", "backgroundColor": "#F8F8F8" }, "tabBar": { "backgroundColor": "#fafafa", "borderStyle": "white", "selectedColor": "#21a8e5", "color": "#8f8f8f", "list": [{ "pagePath": "pages/homepage/index", "iconPath": "static/img/icon/home.png", "selectedIconPath": "static/img/icon/home-active.png", "text": "首页" }, { "pagePath": "pages/person/message/index", "iconPath": "static/img/icon/info.png", "selectedIconPath": "static/img/icon/info-active.png", "text": "消息" }, { "pagePath": "pages/person/index", "iconPath": "static/img/icon/my.png", "selectedIconPath": "static/img/icon/my-active.png", "text": "我的" } ] } }搭建 uni-app 开发环境主要有两种方式:
- 快速开发使用 HBuilderX 直接 初始化 uni-app 项目,开箱即用,配置简单、上手快;
- 工程化项目则通过 Vue CLI 或 Vite 结合
@dcloudio/vite-plugin-uni脚手架搭建,适合规范项目与版本管理。项目创建完成后,基础配置:通过
pages.json统一管理页面路由、tabBar、全局样式 与 权限配置;引入 uView 等 UI 组件库,配置 easycom 自动引入、全局 SCSS 变量;同时配置跨端兼容、请求封装、环境变量,统一开发、测试、生产多环境,保证项目稳定迭代。
3、你是如何实现多端响应式适配的?
一、核心:uni-app 官方首选适配方案(全端通用)
1. 单位选型:rpx(最核心)
uni-app 中 rpx 是 跨端适配 的 核心单位,和你说的小程序 rpx 完全一致:
- 750rpx = 屏幕宽度(不管手机是 320px、375px、414px)
- 自动适配 小程序 / App / H5 所有端,无需额外配置
- 设计稿按 750px 宽度做,1:1 写 rpx 即可
✅ 你的项目完全可以 全页面统一用 rpx,这是最省心的方案。
2. 为什么不用 px?
px 是固定像素,大屏手机 / 小屏手机显示大小一样,无法适配,仅用于边框、1px 细线等场景。
二、补充适配方案(配合 rpx 使用)
1. 布局方案:Flex 布局(必须用)
uni-app 全端支持 flex,是适配不同屏幕高度 / 宽度的最优布局:
/* 通用适配样式 */ .container { display: flex; flex-direction: column; min-height: 100vh; /* 占满屏幕高度 */ } .flex-row { display: flex; align-items: center; justify-content: space-between; }
- 适配不同屏幕比例(全面屏 / 非全面屏)
- 自动拉伸 / 压缩,不会出现布局错乱
2. 视口单位:vw /vh
vw:屏幕宽度的 1%vh:屏幕高度的 1%适用场景:
- 高度占满屏幕(如聊天页、登录页)
- 不依赖设计稿的全屏模块
/* 占满屏幕宽度 */ .box { width: 100vw; height: 30vh; }3. 百分比 %
适用:宽度自适应、父容器相对布局
/* 宽度自适应父容器 */ .card { width: 90%; margin: 0 auto; }
三、你的项目专属:uView UI 适配(配置里已开启 easycom)
你配置了:
"easycom": { "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" }uView 自带完美适配:
- 所有组件 默认使用 rpx
- 支持响应式尺寸、响应式主题
- 自动兼容 小程序 / App / H5
✅ 直接用 uView 组件,无需手动改单位,天然适配。
四、pages.json 里的全局样式适配
你已经配置了
globalStyle,这是 全局导航栏 统一适配:"globalStyle": { "navigationBarTextStyle": "white", "navigationBarTitleText": "uniapp", "navigationBarBackgroundColor": "#1aa2e6", "backgroundColor": "#F8F8F8" }
- 统一所有页面 导航栏 样式
- 全端自动渲染,无需手动适配
五、TabBar 自动适配(你已配置)
"tabBar": { "backgroundColor": "#fafafa", "borderStyle": "white", "selectedColor": "#21a8e5", "color": "#8f8f8f", "list": [/* ... */] }
- 小程序 / App 自动渲染原生 TabBar
- 图标 / 文字自动居中适配不同屏幕
- 路径必须和 pages 里一致(你已配置正确)
六、完整最佳实践总结(直接照做)
- 设计稿统一 750px 宽
- 尺寸全部用 rpx
- 布局全部用 flex
- 全屏高度用 100vh
- 自适应宽度用 %
- UI 组件用 uView(自带适配)
- 导航栏 / TabBar 用 pages.json 全局配置
这套方案可以完美适配:
✅ 微信小程序 ✅ 支付宝小程序 ✅ App ✅ H5 ✅ 抖音小程序
总结
- 你的项目 基础配置完全正确(easycom、pages、tabBar、globalStyle 都没问题)
- 多端适配核心就是:rpx + flex + uView,这是 uni-app 行业标准方案
- 不用写复杂媒体查询,不用针对各端单独写样式,全端自动适配
- 你之前说的 rpx / 百分比 / vw、vh / flex 完全正确
4、你是如何封装网络请求的?
一、先看最终目录结构(你说的完全一致)
/common ├── config.js # 环境配置、baseUrl 管理 ├── auth.js # Token 存储、获取、删除 └── request.js # uni.request 二次封装(核心) /apis └── index.js # 所有接口统一管理(调用封装后的request)
二、逐文件实现封装
1. /common/config.js(环境 + baseUrl)
作用:区分开发 / 测试 / 生产环境,统一管理接口域名。
// 根据环境配置不同接口地址 const baseUrl = { // 开发环境 development: "https://api-dev.xxx.com", // 测试环境 test: "https://api-test.xxx.com", // 生产环境 production: "https://api.xxx.com" }; // 当前环境(可自动判断或手动修改) const env = "development"; export default { baseUrl: baseUrl[env], timeout: 10000 // 请求超时时间 };
2. /common/auth.js(Token 管理)
作用:统一处理 Token 存储、获取、清除。
const TOKEN_KEY = "TOKEN"; // 存储 Token export function setToken(token) { uni.setStorageSync(TOKEN_KEY, token); } // 获取 Token export function getToken() { return uni.getStorageSync(TOKEN_KEY) || ""; } // 删除 Token(退出登录) export function removeToken() { uni.removeStorageSync(TOKEN_KEY); }
3. /common/request.js(核心:二次封装 uni.request)
作用:统一请求头、拦截请求 / 响应、处理成功 / 失败、加载提示、错误提示。
import config from "./config.js"; import { getToken } from "./auth.js"; // 封装请求方法 const request = (options) => { // 1. 加载提示(可根据需求开启/关闭) uni.showLoading({ title: "加载中..." }); // 2. 返回 Promise 支持 async/await return new Promise((resolve, reject) => { uni.request({ // 基础配置 url: config.baseUrl + options.url, method: options.method || "GET", data: options.data || {}, timeout: config.timeout, // 3. 请求头(统一携带 Token) header: { "Content-Type": "application/json;charset=UTF-8", token: getToken() // 从auth.js获取 }, // 4. 请求成功 success: (res) => { uni.hideLoading(); const { data } = res; // 状态码统一处理 if (res.statusCode === 200) { // 后端自定义成功码(如 code=200/0) if (data.code === 200) { resolve(data); // 正常返回数据 } else { uni.showToast({ title: data.msg || "请求失败", icon: "none" }); reject(data); } } else { uni.showToast({ title: "服务器异常", icon: "none" }); reject(res); } }, // 5. 请求失败(网络错误/超时) fail: (err) => { uni.hideLoading(); uni.showToast({ title: "网络异常,请重试", icon: "none" }); reject(err); } }); }); }; export default request;
4. /apis/index.js(接口统一管理)
作用:所有接口写在这里,页面直接调用,不写散在页面里。
import request from "../common/request.js"; // 用户模块接口 export const userApi = { // 登录 login(data) { return request({ url: "/user/login", method: "POST", data }); }, // 获取用户信息 getUserInfo() { return request({ url: "/user/info", method: "GET" }); } }; // 商品模块接口 export const goodsApi = { getGoodsList(data) { return request({ url: "/goods/list", data }); } };
三、页面中如何使用(极简调用)
import { userApi } from "@/apis/index.js"; // 登录请求 async login() { try { const res = await userApi.login({ username: "admin", password: "123456" }); console.log("登录成功", res); } catch (err) { console.log("登录失败", err); } }
四、你的封装思路亮点(非常专业)
- 环境分离:config.js 管理 baseUrl,切换环境只需改一个变量
- 权限分离:auth.js 单独管理 Token,高内聚低耦合
- 请求统一:request.js 统一处理加载、请求头、成功、失败
- 接口集中:apis.js 统一管理所有接口,方便维护、查找、修改
- 全端通用:小程序 / App / H5 都能用
总结
你的网络请求封装 完全符合企业级标准,逻辑清晰、结构规范、可维护性极强。我给你的代码就是 直接按照你的思路写出来的完整版,复制到项目里就能用。
5、您如何处理请求的并发和取消?
答案:使用 Promise.all 处理并发请求。对于重复请求,存储请求的 Promise 并返回相同 Promise,或使用 AbortController 取消未完成的请求(Uni-app 支持)。在路由切换时取消无关请求。
你理解的 核心逻辑完全正确,只是 叫法不一样:
- 你说的 Cancel = uni-app 里的 RequestTask.abort ()
- 你说的 合并接口、Promise.all = 处理并发
我直接给你 最标准、最贴合 uni-app / 小程序 的回答,面试一字不差能用。
面试标准答案(背这个)
问:你如何处理请求的并发和取消?
回答:
并发处理 我主要使用 Promise.all 来处理多个接口同时请求,统一等待返回结果后再渲染页面,避免多次 setState 导致页面抖动。另外也会和后端协商,把多个小接口合并成一个接口,减少请求数量,从源头降低并发压力。
请求取消 在 uni-app / 小程序中,uni.request 会返回一个 RequestTask 对象,我会把这个对象保存起来,需要取消时调用 .abort () 方法 即可取消未完成的请求。
常用场景:
- 页面切换时,取消上个页面未完成的请求
- 重复点击提交 时,取消上一次请求
- 搜索防抖 时,取消前一次输入的请求
极简代码示例(你一看就懂)
1. 取消请求(uni-app 版)
// 保存请求任务 let requestTask = null; // 发起请求 requestTask = uni.request({ url: "/api/list", success: (res) => {} }); // 取消请求(你说的 Cancel) requestTask.abort();2. 并发请求
// 同时请求两个接口,一起返回 async getList() { const [res1, res2] = await Promise.all([ api.getBanner(), api.getGoodsList() ]) }
一句话总结(最关键)
- 并发 = Promise.all + 接口合并
- 取消 = RequestTask.abort ()(就是你说的 Cancel,只是名字不同)
你理解的 完全没错,就是小程序 /uni-app 里不叫 Cancel,叫 abort()。
6、您如何实现路由拦截与登录鉴权?
答案: 在 main.js 中使用 uni.addInterceptor 拦截路由跳转,检查是否有 token。如果没有且页面需要登录(如个人中心),则跳转登录页。登录成功后返回原页面。
我直接给你 最标准、最常用、最稳定 的实现方式,和你说的流程完全一致:
- 登录 → 后端返回 Token
- 前端存:
localStorage/sessionStorage+Vuex- 请求拦截器:所有接口自动在请求头携带 Token
- 路由拦截器:未登录自动跳登录页
- 登录成功后:返回原来想访问的页面
一、登录逻辑(你已经在做的)
登录接口成功后:
// 登录成功 loginApi().then(res => { const token = res.data.token; // 1. 存本地持久化 uni.setStorageSync('token', token); // 2. 存 Vuex this.$store.commit('user/SET_TOKEN', token); // 3. 跳回之前想访问的页面 const redirect = uni.getStorageSync('redirectUrl'); if (redirect) { uni.redirectTo({ url: redirect }); uni.removeStorageSync('redirectUrl'); } else { uni.switchTab({ url: '/pages/index/index' }); } })
二、请求拦截器(所有接口自动带 Token)
在
utils/request.js或main.js里配置:uni.addInterceptor('request', { invoke(args) { const token = uni.getStorageSync('token'); // 请求头自动带上 Token if (token) { args.header['Authorization'] = 'Bearer ' + token; // 或你们后端要求的字段:token / tokenKey 等 } } })✅ 作用:所有接口不用手动传 Token,自动携带。
三、路由拦截(核心:鉴权 + 未登录跳转登录)
在 main.js 中写:
// 需要登录才能访问的页面列表 const needLoginPages = [ '/pages/user/user', // 个人中心 '/pages/order/order', // 订单 '/pages/setting/setting' // 设置 ] // 路由拦截 uni.addInterceptor('navigateTo', { invoke(args) { const url = args.url; const hasToken = !!uni.getStorageSync('token'); // 判断:页面需要登录 + 没有Token → 拦截 if (needLoginPages.includes(url) && !hasToken) { // 存下来用户原本想去的页面 uni.setStorageSync('redirectUrl', url); // 跳转到登录页 uni.redirectTo({ url: '/pages/login/login' }); // 终止原跳转 return false; } } }) // 同时拦截:switchTab(底部 tab 跳转) uni.addInterceptor('switchTab', { invoke(args) { const url = args.url; const hasToken = !!uni.getStorageSync('token'); if (needLoginPages.includes(url) && !hasToken) { uni.setStorageSync('redirectUrl', url); uni.redirectTo({ url: '/pages/login/login' }); return false; } } })
四、Vuex 存储 Token(标准结构)
// store/modules/user.js export default { state: { token: uni.getStorageSync('token') || '' }, mutations: { SET_TOKEN(state, token) { state.token = token; }, CLEAR_TOKEN(state) { state.token = ''; uni.removeStorageSync('token'); } } }
五、退出登录
logout() { this.$store.commit('user/CLEAR_TOKEN'); uni.reLaunch({ url: '/pages/login/login' }); }
六、整套流程总结(最清晰的一句话版本)
- 登录:后端返回 Token → 存本地 + Vuex
- 请求:统一拦截 → 自动在请求头带 Token
- 路由:统一拦截 → 需登录页面无 Token → 跳登录
- 登录成功:回到原本想访问的页面
总结
- 路由拦截:
uni.addInterceptor拦截跳转,判断token是否存在- 鉴权:白名单 / 黑名单判断页面是否需要登录
- Token:本地存储(持久化)+ Vuex(状态管理)
- 请求:拦截器自动携带请求头
- 体验:登录后自动返回原页面
7、您如何实现首页轮播图的动态配置?
答案: 使用 swiper 组件(<swiper>),通过 uni.request 获取轮播图列表数据(图片 URL、跳转链接)。动态绑定到 swiper-item,使用 v-for 渲染。设置 autoplay、interval、indicator-dots。
<!-- 表情 --> <swiper class="emoji-swiper" :class="{hidden:hideEmoji}" indicator-dots="true" duration="150"> <swiper-item v-for="(page,pid) in emojiList" :key="pid"> <view v-for="(em,eid) in page" :key="eid" @tap="addEmoji(em)"> <image mode="widthFix" :src="'/static/img/emoji/'+em.url"></image> </view> </swiper-item> </swiper>
使用 uView 组件库的
<u-swiper>轮播图组件,通过uni.request或 封装的请求接口 从后端获取轮播图动态数据(包含图片地址、跳转链接、标题等),将数据赋值给组件的list属性实现 动态渲染;再通过autoplay、duration、indicator-dots等属性配置轮播效果,通过 样式穿透 (::v-deep)修改指示器、圆角、间距 等样式,完成首页轮播图的动态配置。
可直接运行的完整代码(uni-app + uView)
<template> <!-- 1. 使用 uView 轮播图组件,动态绑定 list 数据 --> <u-swiper :list="bannerList" :autoplay="true" :interval="3000" :duration="500" indicator-dots radius="12" height="360rpx" @click="bannerClick" ></u-swiper> </template> <script> export default { data() { return { // 轮播图动态数据(接口赋值) bannerList: [] } }, onLoad() { // 2. 页面加载请求后端接口,获取轮播图数据 this.getBannerList() }, methods: { // 获取轮播图接口 getBannerList() { uni.request({ url: "https://xxx.com/api/banner", // 后端轮播图接口 method: "GET", success: (res) => { // 3. 接口返回数据赋值,实现动态配置 this.bannerList = res.data.list } }) }, // 轮播图点击跳转 bannerClick(item) { uni.navigateTo({ url: item.url // 后端返回的跳转链接 }) } } } </script> <style scoped lang="scss"> /* 4. 样式调整:修改轮播图圆角、指示器样式 */ ::v-deep .u-swiper { margin: 20rpx; } </style>
核心关键点(面试说这 4 点就满分)
- 组件使用:直接使用 uView 的
<u-swiper>组件,不用自己手写 swiper- 动态数据:通过
uni.request请求后端接口,拿到图片、跳转链接- 动态绑定:把接口数据赋值给
list属性,v-for自动渲染- 效果配置:开启自动轮播、设置时间、指示器、圆角、高度,调整样式
总结
- 组件:uView 的 u-swiper(uni-app 最常用)
- 数据:后端接口请求 → 动态赋值
- 配置:autoplay / interval / 指示器 / 圆角 / 高度
- 样式:样式穿透调整 轮播图外观
8、如何实现文件上传(图片、文档)并显示进度?
答案: 使用 uni.chooseImage 选择 图片,调用 uni.uploadFile 上传。通过 onProgressUpdate 监听上传 进度,更新进度条。支持多文件队列上传,失败重试。上传成功后返回文件 URL。
使用
uni.chooseImage选择图片 / 文档,通过uni.uploadFile调用上传接口,利用onProgressUpdate监听上传进度并实时更新进度条;支持多文件队列上传、失败重试功能,上传成功后接收后端返回的文件 URL 进行页面展示。
极简背诵版(一句话)
通过 uni-app 原生
chooseImage选择文件,uploadFile上传,onProgressUpdate监听并显示上传进度,支持多文件、重试,成功返回文件地址。
核心代码(面试说思路用)
// 1. 选择文件 uni.chooseImage({ success: (file) => { // 2. 上传文件 const uploadTask = uni.uploadFile({ url: '上传接口', filePath: file.path, success: (res) => { // 上传成功,拿到文件URL } }) // 3. 监听上传进度 uploadTask.onProgressUpdate((res) => { // res.progress 实时更新进度条 }) } })
总结
- 选择:
uni.chooseImage- 上传:
uni.uploadFile- 进度:
onProgressUpdate- 结果:返回文件 URL,支持多文件、重试
9、你是如何处理小程序的登录流程?
答案: 调用 uni.login 获取 code,传给后端换取 openid 和 session_key,后端返回自定义 token。前端存储 token 到 storage,后续请求携带。用户信息通过 button 的 open-type="getUserInfo" 获取。
我们的小程序是 律师端 + 用户端双端合一 的模式,登录流程在首页处理:首先在页面
onLoad生命周期中调用uni.login获取微信登录 code,将 code 传给后端换取 登录状态;同时通过uni.getUserInfo获取 用户微信信息 并存储。登录完成后,在首页提供 身份选择入口:用户点击 我是律师,就跳转到律师端首页;点击 我是用户,就跳转到普通用户端首页,通过这种方式实现双端身份区分和不同界面展示。
极简背诵版(一句话,更流畅)
小程序是律师、用户双端合一,在首页
onLoad里执行uni.login微信登录获取 code,调用getUserInfo保存用户信息;登录后让用户选择身份,点击对应入口分别进入律师端和用户端的不同页面。
核心流程(面试说这 4 步就满分)
- 生命周期触发:在首页
onLoad自动执行微信登录- 微信授权登录:调用
uni.login拿到code,配合uni.getUserInfo获取用户信息- 信息存储:将用户信息、登录状态本地存储 + 存入 Vuex
- 身份选择跳转:用户点击「我是律师」「我是用户」,进入对应端的首页界面
uni.login({ provider: 'weixin', success: function (loginRes) { console.log(loginRes.authResult); // 获取用户信息 uni.getUserInfo({ provider: 'weixin', success: function (infoRes) { console.log('用户昵称为:' + infoRes.userInfo.nickName); } }); } });关键亮点(面试官很喜欢)
- 双端合一,一套代码区分律师 / 用户两种角色
- 自动登录,进入首页就触发微信授权
- 身份手动选择,界面清晰区分不同权限页面
总结
- 入口:首页 onLoad 生命周期
- 登录:uni.login 获取 code + uni.getUserInfo 获取信息
- 角色:律师端 / 用户端 手动选择 + 界面区分
- 存储:本地存储 + Vuex
10、您如何实现消息模块的 WebSocket 通信?
答案: 使用 uni.connectSocket 建立连接。监听 onOpen、onMessage、onError、onClose。在 onMessage 中解析消息,存入本地数据并更新界面。发送消息调用 uni.sendSocketMessage。实现心跳(定时发送 ping)和 断线重连。
我给你整理一套 生产环境可用、逻辑完整、可直接复用 的 uni-app WebSocket 消息模块实现方案,覆盖你提到的所有核心点:建立连接、监听事件、收发消息、心跳保活、断线重连、在线人数 / 公告等业务扩展。
一、核心实现流程(标准步骤)
- 创建 WebSocket 连接:使用
uni.connectSocket初始化 并 建立长连接- 监听四大核心事件:连接成功 (onOpen)、接收消息 (onMessage)、连接错误 (onError)、连接关闭 (onClose)
- 消息处理:onMessage 解析数据 → 本地存储 / 更新数据 → 刷新页面 UI
- 发送消息:调用
uni.sendSocketMessage主动推送消息- 保活机制:定时发送心跳(ping),防止服务端主动断开
- 容错机制:断线自动重连,保证消息模块稳定性
二、完整可运行代码(封装成工具类,最佳实践)
// utils/socket.js 封装全局 WebSocket 工具类 class Socket { constructor() { // 基础配置 this.socketUrl = "ws://xxx.xxx.xxx:8080/ws"; // 后端 WebSocket 地址 this.socketTask = null; // WebSocket 实例 this.isConnected = false; // 连接状态 this.heartbeatTimer = null; // 心跳定时器 this.reconnectTimer = null; // 重连定时器 this.reconnectCount = 0; // 重连次数 this.maxReconnectCount = 5; // 最大重连次数 } // 1. 初始化并建立 WebSocket 连接 connectSocket() { // 防止重复创建连接 if (this.socketTask) { this.socketTask.close(); } // uni-app 核心:创建连接 this.socketTask = uni.connectSocket({ url: this.socketUrl, success: () => { console.log("WebSocket 初始化成功"); }, fail: (err) => { console.log("WebSocket 初始化失败", err); this.reconnect(); // 初始化失败直接重连 }, }); // 2. 监听连接成功事件 this.socketTask.onOpen(() => { console.log("WebSocket 连接成功"); this.isConnected = true; this.reconnectCount = 0; // 重置重连次数 this.startHeartbeat(); // 开启心跳 }); // 3. 监听接收消息事件(核心) this.socketTask.onMessage((res) => { // 解析后端返回的消息(JSON 格式) const data = JSON.parse(res.data); console.log("收到服务器消息:", data); // 业务处理:根据消息类型处理 switch (data.type) { case "message": // 普通聊天消息 // 存入本地/全局数据,更新页面 UI getApp().globalData.messageList.push(data.content); // 触发页面更新(uni-app 全局事件/页面监听) uni.$emit("updateMessage", data.content); break; case "online": // 在线人数 uni.$emit("updateOnlineCount", data.count); break; case "announcement": // 系统公告 uni.$emit("receiveAnnouncement", data.content); break; case "pong": // 心跳响应 // 收到心跳回复,无需处理 break; } }); // 4. 监听连接错误 this.socketTask.onError((err) => { console.log("WebSocket 连接错误", err); this.isConnected = false; this.reconnect(); // 自动重连 }); // 5. 监听连接关闭 this.socketTask.onClose(() => { console.log("WebSocket 连接关闭"); this.isConnected = false; this.stopHeartbeat(); // 关闭心跳 this.reconnect(); // 自动重连 }); } // 发送消息到服务器 sendMessage(msg) { if (!this.isConnected || !this.socketTask) { uni.showToast({ title: "连接未建立,发送失败", icon: "none" }); return; } // 发送 JSON 格式消息 this.socketTask.send({ data: JSON.stringify(msg), success: () => { console.log("消息发送成功"); }, fail: (err) => { console.log("消息发送失败", err); }, }); } // 开启心跳保活(定时发送 ping,防止断开) startHeartbeat() { this.heartbeatTimer = setInterval(() => { this.sendMessage({ type: "ping" }); }, 30000); // 30秒发送一次心跳 } // 关闭心跳 stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } // 断线重连机制 reconnect() { // 超过最大重连次数,停止重连 if (this.reconnectCount >= this.maxReconnectCount) { uni.showToast({ title: "网络异常,请重新进入页面", icon: "none" }); return; } // 防止重复重连 if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { this.reconnectCount++; console.log(`第 ${this.reconnectCount} 次重连...`); this.connectSocket(); // 重新建立连接 this.reconnectTimer = null; }, 3000); // 3秒重连一次 } // 手动关闭连接(页面卸载时调用) closeSocket() { this.stopHeartbeat(); if (this.socketTask) { this.socketTask.close(); } this.isConnected = false; } } // 导出单例(全局唯一连接) export default new Socket();
三、页面中使用(消息页面)
// pages/chat/chat.vue import socket from "@/utils/socket.js"; export default { onLoad() { // 1. 建立 WebSocket 连接 socket.connectSocket(); // 2. 监听新消息(全局事件) uni.$on("updateMessage", (msg) => { this.messageList.push(msg); }); // 3. 监听在线人数 uni.$on("updateOnlineCount", (count) => { this.onlineCount = count; }); // 4. 监听公告 uni.$on("receiveAnnouncement", (content) => { uni.showModal({ title: "系统公告", content: content, showCancel: false, }); }); }, onUnload() { // 页面卸载,关闭连接 socket.closeSocket(); // 移除全局监听 uni.$off("updateMessage"); uni.$off("updateOnlineCount"); uni.$off("receiveAnnouncement"); }, methods: { // 发送聊天消息 sendChatMsg() { if (!this.inputText) return; socket.sendMessage({ type: "message", content: this.inputText, }); this.inputText = ""; }, }, };
四、核心知识点总结(标准回答)
- 建立连接:使用
uni.connectSocket创建 WebSocket 实例并建立长连接;- 事件监听:监听
onOpen(连接成功)、onMessage(接收消息)、onError(错误)、onClose(关闭) 四大核心事件;- 消息处理:在
onMessage中解析后端数据,根据消息类型更新本地数据并刷新页面 UI;- 发送消息:调用
uni.sendSocketMessage向服务端发送消息;- 心跳保活:定时发送
ping心跳包,避免服务端因空闲断开连接;- 断线重连:连接异常 / 关闭时,自动触发重连机制,保证消息模块稳定性;
- 业务扩展:支持在线人数统计、系统公告、聊天消息等场景。
总结
这套方案是 uni-app 开发消息模块 最标准、最稳定 的 WebSocket 实现方式,覆盖了生产环境必备的 连接管理、消息收发、心跳、重连、业务扩展 全流程,直接复用即可。
11、您如何实现聊天消息的本地缓存?
答案: 将聊天记录存储在 uni.setStorageSync 中,以会话 ID 为 key。每次收到新消息追加,加载会话时先读缓存再请求最新消息。缓存设置最大条数,超出则删除旧消息。
先把结论说清楚:
- uni.setStorageSync:同步 写入,加了
Sync就是 “同步版”。- uni.setStorage:异步 写入,不带
Sync。下面拆开讲(结合你聊天缓存的场景):
一、根本区别:同步 vs 异步
1)uni.setStorageSync (key, data) —— 同步
- 立刻执行,阻塞主线程,写完才往下走。
- 直接传 key、data,没有回调。
- 失败会 抛异常,要用
try-catch包着。try { uni.setStorageSync('chat_123', messages) } catch (e) { console.error('存储失败', e) }2)uni.setStorage ({key, data, success, fail}) —— 异步
- 后台执行,不阻塞主线程。
- 靠
success/fail回调拿到结果。uni.setStorage({ key: 'chat_123', data: messages, success() { console.log('存储成功') }, fail(err) { console.error('存储失败', err) } })
二、为什么聊天缓存常用 setStorageSync?
你面试答案里写的是:
将聊天记录存储在 uni.setStorageSync 中,以会话 ID 为 key。每次收到新消息追加,加载会话时先读缓存再请求最新消息。缓存设置最大条数,超出则删除旧消息。
这么写的原因:
- 代码简单、直观:不用写回调,顺序执行。
- 读的时候也用 getStorageSync:
// 打开会话先读缓存 const list = uni.getStorageSync('chat_123') || []- 聊天记录一般 不算超大体积,同步阻塞影响不大。
三、和 H5 的 localStorage 什么关系?
- H5:
localStorage.setItem(key, value)同步- uni-app:
uni.setStorageSync≈ 对应 H5 localStorage(同步)uni.setStorage≈ 对应 H5 的异步封装(类似封装了一层回调)所以你说的没错:换汤不换药,就是跨端统一了一套 API,名字加了 uni-,同步版多了个 Sync。
四、简单记法
- Sync = 同步,马上写、阻塞、无回调、try-catch
- 无 Sync = 异步,后台写、不阻塞、靠回调
12、如何优化小程序的性能?
我从 图片、长列表、渲染、代码、网络、缓存、分包 七大维度整理,面试脱口就能说,比你刚才说的更完整、专业。
1. 图片资源优化
- 图片压缩、转 WebP 格式,减小体积;
- 采用 图片懒加载,进入视口再加载;
- 合理用图片 CDN,后端做图片自适应裁剪,不用大图缩小展示。
2. 长列表性能优化
- 优先 接口分页,下拉触底加载,不一次性渲染全部数据;
- 超大数据列表用 虚拟列表(虚拟滚动),只渲染可视区域 DOM;
- 减少列表项复杂层级、避免过多绑定事件。
3. 页面渲染 & 数据优化
- 非响应式数据不放在 data 里,直接挂在实例上,减少监听和 diff 开销;
- 减少
setData调用频率,合并多次 setData,避免频繁更新视图;- 精简页面 wxml 结构,减少无用标签、避免多层嵌套。
4. 代码与打包优化
- 开启 Gzip 压缩,减小静态资源体积;
- 清除无用代码、注释、未引用组件,精简项目体积;
- 避免全局冗余组件、全局样式污染。
5. 网络请求优化
- 接口请求 合并请求,减少请求次数;
- 接口做缓存,重复请求优先拿本地缓存,不用每次都调接口;
- 避免定时器、监听事件页面卸载后 不销毁,造成内存泄漏。
6. 分包加载优化
小程序开启 分包拆分,把首页、核心业务放主包,其他页面、组件放分包,减少首屏加载时间。
7. 本地缓存优化
合理使用
uni.setStorage做数据缓存、聊天记录缓存、接口数据缓存,减少重复网络请求,提升页面打开速度。
面试答题话术(直接背这一段就行)
小程序性能优化我主要从几个方面做:第一是图片优化,统一转 WebP 格式、做懒加载,配合后端 CDN 裁剪压缩;第二长列表用分页 + 虚拟滚动,避免一次性渲染大量节点造成卡顿;第三控制 setData 频次,非响应式数据不放进 data,减少视图层 diff 消耗;第四项目开启 Gzip 压缩、做分包加载,拆分主包 和 分包提升首屏速度;同时合理做本地数据缓存,合并接口请求,页面及时销毁定时器 和 监听,避免 内存泄漏。
13、你们是如何实现支付功能的?
我们项目是用 uni-app 开发 App 端 微信支付,整体分为三部分:开通配置、服务器下单、客户端 调起支付。
前期开通与配置
- 先在微信开放平台申请 AppID,提交应用审核;
- 支付申请通过后,拿到商户号(PartnerID),再登录微信商户平台设置 APIv2 密钥;
- 在 uni-app 项目的
manifest.json里,勾选 “微信支付”,配置好 AppID,iOS 平台还要配置 Universal Links,确保和微信开放平台的开发信息一致。服务器端生成预支付订单
- 客户端先请求我们的业务服务器下单;
- 服务器调用微信支付的
统一下单接口,传入订单信息,拿到prepayid(预支付会话 ID);- 再根据 AppID、商户号、prepayid、随机字符串、时间戳等参数,按微信的签名算法生成
sign,把这些参数一起返回给前端。App 端调起支付
- 拿到服务器返回的订单信息后,调用
uni.requestPayment,provider固定为wxpay,orderInfo传完整的订单对象(包含 appid、partnerid、prepayid、package、noncestr、timestamp、sign);- 支付成功 / 失败会走对应的回调,我们再根据结果做订单状态更新和提示。
精简一句话版(面试紧张时说)
我们在 uni-app 里实现 微信支付,主要是:先在 manifest 配置好 微信支付参数,由后端生成并签名预支付订单,前端再用
uni.requestPayment调起支付,处理回调。
补充:关键要点拆解(面试官追问时用)
- 为什么要服务器下单? 签名 和 预支付订单 都必须由服务器生成,不能在前端算,防止密钥泄露、恶意篡改订单金额。
- 前端要做什么? 只负责调
uni.requestPayment,传服务器给的订单信息,处理成功 / 失败回调,更新订单状态。- 和小程序支付的区别? App 支付需要在微信开放平台申请 AppID,还要配置 Universal Links;小程序支付是用小程序 AppID,流程上服务器统一下单、前端调起支付的逻辑是类似的。
14、如何处理 iOS 和 Android 的兼容性问题?
答案:使用条件编译区分平台;iOS 下 input 键盘弹起时使用 scrollIntoView;Android 下注意返回键处理;使用 uni.getSystemInfo 获取系统信息,针对性调整样式或逻辑。
核心一句话
uni-app 确实能一套代码跑安卓 + iOS,但这不代表没有兼容性问题!框架只是 “基础兼容”,真实项目里一定会遇到平台差异,必须手动处理。
1. 为什么明明兼容了,还会有兼容性问题?
uni-app 做的是:
- 一套语法
- 一套代码
- 编译成两个平台的安装包
但它不能 100% 抹平两个系统的底层差异!
比如:
- iOS 键盘弹出方式不一样
- iOS 滚动有 阻尼效果
- Android 返回键逻辑不一样
- 某些 CSS 在两个系统渲染不同
- 组件表现、弹窗层级、输入框行为不同
这些 原生系统差异,框架不可能全部自动处理,所以必须 手动写兼容代码。
2. 你之前说的那些问题,都是真实存在的
你提到的:
- iOS 滑动滞涩
- iOS 布局错乱
- 两端样式显示不一致
这些都是 uni-app 最常见的真实兼容问题,不是框架不好,而是两个手机系统本身就不一样。
3. 那答案里写的方法是干嘛用的?
答案里写的:
- 条件编译区分平台
- iOS 处理 input 键盘
- Android 处理返回键
- 获取系统信息做差异化
这些就是用来解决 “框架没抹平的那部分差异” 的!
简单说:
- 框架负责 90% 通用兼容
- 你负责 10% 平台特殊问题
4. 最通俗的比喻
uni-app 就像 双语翻译器:你说中文,它能自动翻译成英文(安卓)和日文(iOS)。
但:
- 有些句子英文和日文习惯不一样
- 有些表达必须手动调整
- 有些语法必须单独写
翻译器不能 100% 完美,必须人工微调。
最终总结(最清晰版本)
uni-app 可以 一套代码同时支持 安卓 和 iOS,不需要分别开发。但因为两个系统 底层 机制不同,仍然会出现兼容性问题,例如:
- 样式错乱
- 滚动不流畅
- 键盘弹出异常
- 组件表现不一致
所以需要用:
- 条件编译
- 平台专属 CSS
- 获取系统信息
- 平台差异化逻辑
来 解决这些框架无法自动处理的兼容问题。
15、您如何实现下拉刷新和上拉加载更多?
答案: 启用页面配置(enablePullDownRefresh),监听 onPullDownRefresh 事件重置数据并刷新。上拉加载使用 onReachBottom 事件,加载下一页数据。注意设置 total 和 finished 标志。
核心一句话
uni-app 自带下拉刷新、上拉加载,不用装第三方 UI(如 uView)也能做;但复杂业务、好看样式,才会用 uView 组件。
我给你整理成最标准、最清晰的回答
问:你如何实现下拉刷新和上拉加载更多?
标准回答(你直接背)
uni-app 有 内置的下拉刷新 和 上拉加载,不需要依赖第三方 UI 库就能实现:
下拉刷新
- 在 pages.json 里“pages”内开启
enablePullDownRefresh: true- 页面中监听
onPullDownRefresh生命周期- 在里面重新请求接口、刷新列表数据
- 刷新完成后调用
uni.stopPullDownRefresh()停止刷新动画上拉加载更多
- 监听页面生命周期
onReachBottom- 当页面触底时,请求下一页数据
- 拼接进列表
- 根据总页数判断是否加载完毕,避免重复请求
什么时候用 uView?
- 内置的样式比较原生、比较简单
- 如果项目需要 自定义动画、自定义图标、下拉效果更美观,才会使用 uView 的 LoadMore 加载更多 或
pull-refresh组件- 简单业务直接用 uni-app 自带的就够了,性能更好
最关键的总结(你说得完全正确)
- uni-app 自带这两个功能,非常方便
- 不是必须用 uView
- 简单页面用内置,复杂好看的 UI 才用第三方组件
- 内置原生方法性能更快
16、您如何实现自定义导航栏?
答案: 在 pages.json 中设置 navigationStyle: custom,然后使用自定义组件(uni-nav-bar)实现。获取系统状态栏高度(uni.getSystemInfoSync().statusBarHeight)来适配不同机型。
一、核心两步(最重要)
- 关闭原生导航栏 在
pages.json里当前页面配置:"style": { "navigationStyle": "custom" }- 使用自定义导航栏组件 直接用 uni-app 官方自带的:
<uni-nav-bar>不用额外装 uView 也能实现!
二、最精简代码(直接复制用)
<template> <!-- 自定义导航栏 --> <uni-nav-bar title="页面标题" left-text="返回" @clickLeft="back" /> <!-- 页面内容 --> <view>内容区域</view> </template> <script> export default { methods: { back() { uni.navigateBack() } } } </script><view class="nav-wrap"> <uni-nav-bar left-icon="back" title="邀请好友" color="#fff" backgroundColor="transparent" fixed="true" statusBar="true" @clickLeft="handleLeftIcon"></uni-nav-bar> </view>
三、为什么要获取状态栏高度?
因为 手机顶部状态栏高度不一样(iPhone 有刘海、安卓高低不同)。
获取方法:
uni.getSystemInfoSync().statusBarHeight不管是 uni-nav-bar 还是 u-navbar,内部都是自动用这个高度做适配,你不用自己算!
四、面试标准答案(你要的那种)
问:如何实现自定义导航栏?
答:
- 在 pages.json 中"style"设置
navigationStyle: custom关闭原生导航栏;- 使用 uni-app 自带的
<uni-nav-bar>自定义组件;- 组件内部会自动获取
statusBarHeight适配不同机型状态栏高度;- 可配置标题、左侧返回、右侧按钮,完全支持自定义内容。
五、你说的拖拉拽 = 低代码方式
就是在 HBuilderX 里面:
- 直接从 组件库 拖一个
uni-nav-bar到页面- 可视化配置标题、按钮、颜色、样式
- 不用手写太多代码,自动生成
实现非常简单,重点就两步:关闭原生 + 使用自定义组件。
总结(最强记忆版)
- 自定义导航栏 = 关闭原生导航 + 自定义组件
- 官方自带:
<uni-nav-bar>- uView 也能用:
<u-navbar>- 高度适配:自动获取
statusBarHeight- 支持:标题、返回按钮、左右插槽、拖拉拽配置
17、您如何实现全局 loading 和空状态组件?
答案: 封装全局 Loading 组件,通过 uni.showLoading / hideLoading 控制。空状态使用条件渲染,当列表数据为空时展示自定义空状态图及文案。
版本 1(简洁标准版,面试首选)
全局 Loading:对请求进行 Axios / 请求拦截二次封装,请求发起自动开启
uni.showLoading,请求成功 / 失败统一关闭;也可封装全局 Loading 组件,配合 Vuex 全局状态管理,在 App.vue 全局挂载,任意页面可按需调用开启 / 关闭。空状态组件:封装通用空状态 UI 组件,通过条件渲染判断列表数据长度,数据为空、请求失败、无网络时分别展示对应空图片和提示文案,全局页面直接引入复用。
版本 2(贴合你刚才说的思路,更详细)
全局 Loading 实现:
- 把 loading 状态维护在 Vuex / Pinia 全局状态里;
- 在
App.vue中引入全局 Loading 组件,实现全局生效;- 二次封装网络请求,请求拦截自动开启 loading,响应拦截无论成功失败都自动关闭;
- 支持手动在任意页面控制显示隐藏。
空状态实现:
封装可复用的空状态组件,预留图片、文案、按钮插槽,页面通过判断接口返回列表数据是否为空,做条件渲染,无数据时展示空状态组件,实现全局复用。
18、您如何实现小程序的版本更新检测?
答案: 使用 uni.getUpdateManager,监听 onCheckForUpdate、onUpdateReady。有新版本时提示用户重启应用,调用 applyUpdate。
问:你如何实现小程序版本更新检测?
答:
利用小程序提供的
uni.getUpdateManager()获取更新管理器,通过监听onCheckForUpdate检测是否有新版本,再监听onUpdateReady版本下载完成回调,弹窗提示用户更新,确认后调用applyUpdate()强制重启小程序完成版本更新。可直接用的完整代码
// 在App.vue onLaunch 里调用 checkUpdate() { const updateManager = uni.getUpdateManager(); // 检测版本更新 updateManager.onCheckForUpdate(res => { // 有新版本 if (res.hasUpdate) { // 等待新版本下载完成 updateManager.onUpdateReady(() => { uni.showModal({ title: '版本更新', content: '已有新版本,请立即更新', confirmText: '立即更新', success: (res) => { if (res.confirm) { // 重启应用更新 updateManager.applyUpdate(); } } }) }) // 下载失败监听 updateManager.onUpdateFailed(() => { uni.showToast({ title: '版本更新失败', icon: 'none' }) }) } }) }核心要点记 3 个就行
- 核心 API:
uni.getUpdateManager()获取更新管理器- 两个监听:
onCheckForUpdate、onUpdateReady- 重启更新:
applyUpdate()
19、您如何实现自定义组件(如律师卡片)?
答案: 在 components 目录创建 .vue 文件,定义 props(avatar头像, name姓名, rating评分 等),使用 uni-ui 或 uView 组件构建模板。通过 easycom 自动引入,无需手动导入。
问:您如何实现自定义组件?(如律师卡片)答:
- 在项目
components目录下创建独立的.vue组件文件;- 通过
props接收外部数据(头像、姓名、评分、信息等);- 基于 uni-app 内置组件(如
uni-card)或 uView 组件进行 二次封装,统一项目样式;- 组件内部实现点击、关闭、确认等自定义事件,通过
$emit向外抛出;- 利用 uni-app 的
easycom自动引入组件,页面直接使用标签,无需手动导入注册。
超简记忆版(3 句话)
- 创建组件:components 文件夹写 .vue 页面;
- 传参交互:props 接收数据,$emit 触发事件;
- 全局使用:easycom 自动引入,页面直接用标签调用。
核心关键词(必记)
components目录props传值uni-card二次封装$emit事件easycom自动引入
20、您如何实现大文件的断点续传?
答案: 使用分片上传。前端切割文件(如每片2MB),记录已上传片数。上传中断后从断点处继续。后端合并分片。使用 uni.uploadFile 只能传单文件,需自行实现分片逻辑。
大文件断点续传,就是把大文件切成小分片(比如每片 2MB),一片一片上传,记录已经上传成功的分片;断网后重新上传,只传没传完的分片,最后后端把所有分片合并成完整文件。
二、uni-app 里分片上传到底难不难?
一点都不难!
文件切割是 浏览器 / JS 自带能力,不是 uni-app 特殊封装,就用
File.slice()就行,超级简单。uni.uploadFile 本身不支持分片,但 分片逻辑我们自己写 5 行代码就搞定。
三、uni-app 分片上传完整流程(背这个就够)
1. 选文件
用
uni-file-picker选中文件(图片 / 视频都行),拿到file对象。2. 计算分片
定义每片大小:比如 2MB
总片数 = 文件大小 / 每片大小
3. 前端切割文件(核心代码)
// 切割文件,这是原生JS能力,超级简单 const chunk = file.slice(startByte, endByte); // 从第几字节切到第几字节就这一行,就是 分片切割。
4. 循环上传分片
- 每上传一片,记录 已上传成功的序号
- 上传成功一片,再传下一片
5. 断点续传怎么做?
- 上传中断(退出、断网)
- 下次选择同一个文件时
- 先请求后端:这个文件已经传了哪些片?
- 前端 跳过已上传的,只传剩下的分片
6. 所有分片传完
通知后端:合并所有分片 → 生成完整文件
四、你最关心的:文件切割麻烦吗?
完全不麻烦!就一行代码:
// 切割第 index 片 let start = index * chunkSize; let end = Math.min(start + chunkSize, file.size); let chunk = file.slice(start, end); // 切割完成这就是分片。
不是什么复杂算法,就是 按字节截取。
五、面试怎么回答?(标准满分回答)
你可以直接这么说:
大文件断点续传我是用 分片上传 实现的。
前端先用
file.slice()把大文件切成固定大小的分片,比如每片 2MB。然后一片一片上传,记录已经上传成功的分片。
如果上传中断,下次重新上传时,先向后端查询已上传的分片,从断点继续上传剩余分片。
所有分片上传完成后,后端把分片合并成完整文件。
uni-app 里用 uni.uploadFile 上传每个分片,自己封装分片逻辑即可。
六、最简单记忆版(3 句话)
- 大文件 切小片
- 一片一片上传,记录进度
- 断了续传,最后合并
总结
- 文件切割 不麻烦,就
file.slice()一行代码- 断点续传 = 分片上传 + 记录已上传分片
- uni-app 自己封装分片逻辑,不难
21、在处理请求并发时,你遇到过哪些问题?如何解决?
答案: 多个请求同时发起时,loading 会闪烁 或 重复显示。我通过维护一个请求计数器,当计数器从 0 变为 1 时显示 loading,从 1 变为 0 时关闭。另外,对于相同 url 的请求,使用 pending 队列 避免重复请求。
处理接口请求并发时,我实际遇到过 三个典型问题,都做了统一封装解决:
多个请求同时发起,Loading 加载框闪烁、重复显示 / 提前关闭 解决:做 请求计数器,每发起一个请求 计数 + 1,请求结束 计数 - 1;计数器从 0 变 1 才开启 Loading,计数器归 0 再关闭 Loading,避免 并发下 loading 闪屏、提前消失。
同一接口短时间重复发起(比如按钮连点、页面多次触发),造成冗余请求、数据错乱 解决:维护 请求等待队列 Pending Map,把请求 URL 和 参数作为唯一 key,存在 pending 中就不再重复发起;等同个请求结束后再删除缓存,避免重复请求。
并发请求过多,Promise.all 一个失败全部崩溃,全部接口都走失败回调 解决:不用原生
Promise.all,改用 Promise.allSettled,不管单个请求成功失败,都能拿到所有接口结果;再自己遍历状态处理成功 和 失败逻辑,互不影响。
精简版(面试怕忘就背这个)
并发请求我遇到过三个问题:
第一,并发请求 导致 Loading 闪屏,我用 请求计数器 控制显隐;
第二,按钮连点、重复请求接口,我用 Pending 请求队列做防抖节流,拦截重复接口;
第三,Promise.all 一个报错全挂掉,我换成 Promise.allSettled 兼容容错,保证其他正常请求不受影响。
补充:和你之前的知识点衔接
不用单纯只说 Promise.all,面试官要的是:你踩过什么坑 + 怎么解决
不是问你会不会用并发,是问 并发带来的业务问题。
22、你如何实现小程序的登录与用户信息管理?
答案: 调用 uni.login 获取 code 传给后端,后端换取 openid 和 session_key,返回自定义 token。前端存储 token 到 storage。用户信息通过 uni.getUserProfile 获取(需用户授权),但不建议存储敏感信息,应由后端管理。
uni.getUserProfile(OBJECT)
获取用户信息。每次请求都会弹出授权窗口,用户同意后返回 userInfo。
一、小程序登录流程(4 步)
小程序启动自动调用登录 在小程序
onLaunch生命周期里,自动执行uni.login(),拿到微信临时凭证 code。把 code 传给后端 前端把 code 发送到后端接口,后端拿着 code 去微信官方服务器换取用户唯一标识 openid 和 session_key。
后端返回自定义 token 后端不直接返回 openid,而是生成 自定义登录 token 返给前端。
前端存储 token 把 token 存到 localStorage / uni.setStorageSync,作为后续请求的登录凭证。
二、用户信息获取
- 用户头像、昵称不能自动获取,必须 用户主动授权,调用
uni.getUserProfile()弹窗授权。- 授权后拿到昵称、头像,可以传给后端保存。
- 前端把用户信息存在 Vuex / Pinia(全局状态) + 本地缓存,方便页面使用。
三、用户信息管理
token 作为登录核心依据 每次请求接口,在请求头里带上 token,后端校验是否登录。
token 过期处理 后端返回 token 过期时,前端清除缓存,自动重新执行 uni.login 静默登录。
状态管理 使用 Vuex 或 Pinia 维护全局用户登录状态、用户信息,页面直接使用,不用反复获取。
最精简版(怕忘就背这个)
- 小程序启动自动执行
uni.login获取 code,传给后端。- 后端换 openid,返回 自定义 token。
- 前端把 token 存在 storage,Vuex / Pinia 管理用户状态。
- 用户信息必须授权获取,存在全局状态 + 本地缓存。
- 请求带 token,过期自动重新登录。
总结
- 登录核心:
uni.login→ code → 后端 → token → 存储- 用户信息:必须授权获取
- 状态管理:Vuex / Pinia + storage
23、请说明 Uni-app 中的页面通信方式有哪些?
答案:包括:URL 传参(uni.navigateTo 的 url 带参数)、全局数据(getApp().globalData)、Vuex(或 Pinia)、事件总线(uni.$emit / uni.$on)、页面栈获取上一页实例(getCurrentPages)。
uni.navigateTo({ url: `/pages/lawyerindex/answer?listId=${listId}`, });
getCurrentPages()函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,数组中的元素为页面实例,第一个元素为首页,最后一个元素为当前页面。
类型 Array<UniPage>
getCurrentPages()返回了 UniPage 对象数组。每个页面是一个 UniPage 对象,这个对象上有较多方法,比如 获取 / 修改 pageStyle、获取高宽和安全区等。
Uni-app 里常用的 页面 和 组件 通信方式一共有 6 种:
父子组件通信 用 props 父传子,子组件通过 $emit 自定义事件向父组件传值。
深层 / 跨级组件通信(爷孙、隔代) 用 事件总线 EventBus,通过
uni.$emit派发事件,uni.$on监听事件,适合隔代组件传参。也可以用 依赖 注入 provide / inject 跨层透传数据。全局状态管理 用 Vuex / Pinia 做全局共享状态,所有页面、组件都能读写,适合全局用户信息、配置这类数据。
页面跳转 URL 传参 通过
uni.navigateTo跳转时,在 url 后面拼接参数,目标页面在 onLoad 里接收参数。全局全局变量 globalData 用
getApp().globalData挂载全局数据,任何页面直接读写,简单轻量。页面栈通信 通过
getCurrentPages()获取页面栈,直接拿到上一个页面实例,赋值数据、调用页面方法,适合返回上一页刷新数据场景。本地缓存通信 用
uni.setStorageSync / uni.getStorageSync本地存储,页面、组件都能读写,适合持久化简单数据。
精简背诵版(面试直接说)
Uni-app 常用通信方式有:
- 父子组件用 props + $emit;
- 跨级隔代用 EventBus 事件总线 或 provide/inject;
- 全局共享用 Vuex / Pinia;
- 页面跳转用 URL 拼接传参;
- 简易全局变量用 globalData;
- 页面栈用 getCurrentPages 直接操作上一页;
- 持久化通信靠 本地 Storage 缓存。
24、你如何实现消息模块的实时通信?(同10)
答案: 使用 WebSocket(uni.connectSocket)。监听 onOpen、onMessage、onClose,实现心跳包(定时发送 ping)和 断线重连机制。消息发送通过 WebSocket 或 普通 HTTP 接口,接收后存入本地存储或 Vuex 并刷新列表。
uni-app 实现消息实时通信,直接用内置的 WebSocket API(uni.connectSocket),配合 心跳保活 + 断线重连,就能实现聊天、消息推送这类实时功能。
二、你最关心的 2 个问题
1. uni-app 内置 WebSocket 吗?
是的!内置!原生自带!不用装插件!
直接用:
uni.connectSocket()就能创建连接。2. 它是不是原生 WebSocket?
是! 底层就是封装了标准的 WebSocket,用法几乎一样。
三、完整实现流程(背这个就够)
1. 创建连接
调用
uni.connectSocket连接后端 websocket 地址。2. 监听事件(4 个核心)
onOpen:连接成功onMessage:收到新消息(核心)onClose:连接关闭onError:连接失败3. 发送消息
用
wx.sendSocketMessage给后端发消息。4. 心跳包(保活)
每隔几秒(如 30 秒)发一个
ping,防止连接断开。5. 断线重连
如果断网 / 掉线,自动重新调用
connectSocket重连。6. 消息存储
收到消息后,存到 Vuex / Pinia 或 本地缓存,页面自动刷新列表。
四、面试满分回答(直接照着说)
我在项目里是用 uni-app 内置的 WebSocket 实现消息实时通信的。
- 通过
uni.connectSocket创建长连接;- 监听
onMessage接收后端推送的实时消息;- 为了保证连接稳定,加入了 心跳包机制 定时发送 ping;
- 处理网络异常,实现 断线自动重连;
- 收到消息后更新到 Vuex 或 本地存储,页面自动刷新消息列表。
五、超级精简记忆版(3 句话)
- 用 uni.connectSocket 建立长连接
- onMessage 收消息,心跳保活,断线重连
- 收到消息存全局状态,页面刷新
六、和你刚才的代码衔接
你刚才写的跳转:
uni.navigateTo({ url: `/pages/lawyerindex/answer?listId=${listId}`, });消息模块就是:进入页面 → 连接 WebSocket → 收消息 → 显示列表
总结
- uni-app 内置 WebSocket,不用第三方库
- 核心:连接 + 监听消息 + 心跳 + 重连
25、在开发过程当中你是如何调试uni-app这个项目的?
一、基础环境准备
使用 HBuilderX 作为开发工具,它是 uni-app 官方推荐编辑器,内置 编译、运行、调试能力,支持一键发布到 H5、微信小程序、App、支付宝小程序 等 多端。
二、核心调试方式(分端讲解)
1. H5 端(内置浏览器 / 谷歌浏览器,你最常用的方式)
- 运行项目 在 HBuilderX 中选中项目,点击顶部菜单栏
运行 → 运行到浏览器 → 选择 Chrome / 内置浏览器,工具会自动编译项目并打开浏览器页面。- 调试操作 按下
F12打开 开发者工具,核心调试能力:
- 控制台 (Console):查看代码报错、
console.log打印日志,快速定位语法错误、逻辑异常;- 元素 (Elements):查看页面 DOM 结构、样式,实时修改 CSS 调试布局;
- 网络 (Network):监控所有接口请求,查看请求参数、响应数据、请求状态,排查接口调用失败、参数错误、跨域问题;
- 源代码 (Sources):断点调试 JS 代码,逐行执行查看变量值;
- 性能 / 内存:分析页面卡顿、内存泄漏等优化问题。
- 优势:编译速度最快,适合快速调试页面样式、基础逻辑、接口联调。
2. 微信小程序端(必备调试,适配小程序专属特性)
- 运行项目 HBuilderX 中点击
运行 → 运行到小程序模拟器 → 微信开发者工具,工具会自动生成小程序代码,并唤起微信开发者工具。- 调试操作 在微信开发者工具中使用 调试器:
- 专门调试小程序专属 API(如
wx.login、微信支付、小程序路由等);- 查看小程序缓存、授权状态、原生组件渲染问题;
- 排查 H5 端无法复现的小程序兼容性 bug。
- 核心作用:解决 多端差异问题,确保小程序功能正常。
3. App 端(真机 / 模拟器调试)
- 运行项目 点击
运行 → 运行到手机 或 模拟器 → 选择连接的真机 / 安卓模拟器,通过 USB 调试或 WiFi 连接设备。- 调试方式
- 开启 真机调试:在 HBuilderX 中查看控制台日志;
- 使用
vconsole调试面板:在页面中打印日志,查看 App 端的报错、网络请求;- 专门调试原生能力(如摄像头、扫码、定位、本地存储等)。
三、常用辅助调试技巧
- 日志打印:核心调试手段,在关键逻辑处使用
console.log()/console.error()打印变量、接口数据、执行流程;- 断点调试:在浏览器 / 开发者工具中给代码打断点,逐行执行,精准定位逻辑 bug;
- 条件编译调试:针对多端差异代码,单独调试对应平台的逻辑;
- 网络调试:重点排查接口 404、500、参数错误、跨域、超时等问题。
四、整体总结(简洁版)
我在开发中主要用 HBuilderX 开发 uni-app 项目,调试分三步走:
- 优先调试 H5 端:运行到 Chrome 浏览器,F12 打开开发者工具,通过控制台看报错、网络面板查接口、元素面板调样式,效率最高;
- 小程序专属调试:运行到微信开发者工具,解决小程序 API、兼容性问题;
- App 端调试:连接真机 / 模拟器,用真机调试 和 vconsole 排查原生功能问题。全程结合日志打印、断点调试,快速定位并解决页面、逻辑、接口、多端兼容等问题。
总结
- 核心工具:HBuilderX + 浏览器 F12 + 微信开发者工具;
- 主力调试:H5 端 Chrome 调试(样式、接口、基础逻辑);
- 补全调试:小程序 / App 端(专属 API、兼容性、原生功能);
- 核心手段:控制台日志、网络监控、断点调试。
26、你如何实现小程序的日志上报?
答案: 封装 log 函数,收集错误栈、用户操作、设备信息,通过 uni.request 上报到后端。使用 uni.getSystemInfo 获取设备信息。在 Vue.config.errorHandler 和 uni.onError 中捕获错误。
一、核心思路
在 uni-app 小程序里,自动捕获全局错误 + 手动埋点日志,统一封装上报方法,收集关键信息后通过网络请求发送到后端日志服务,用于线上问题排查、用户行为分析。
二、具体实现步骤(最标准方案)
1. 封装统一日志上报方法
创建一个
logReport.js工具类,封装上报逻辑:
- 收集 用户信息、设备信息、页面路径、错误栈、日志类型
- 使用
uni.request异步上报后端(支持小程序 / H5 / App)// 日志上报工具 export const logReport = (logType, content, error = {}) => { // 1. 获取设备信息(uni-app 自带API,小程序可用) const systemInfo = uni.getSystemInfoSync(); // 2. 获取当前页面 const pages = getCurrentPages(); const currentPage = pages.length ? pages[pages.length - 1].route : ''; // 3. 组装上报数据 const logData = { logType, // 日志类型:error/info/action content, // 日志描述 errorStack: error.stack || '', // 错误堆栈 pageUrl: currentPage, // 当前页面 systemInfo, // 设备信息 userId: getUserId(), // 用户ID(自己业务获取) timestamp: Date.now() }; // 4. 上报后端(小程序/uni-app通用) uni.request({ url: 'https://xxx.com/api/log/report', method: 'POST', data: logData, // 不阻塞业务,静默上报 success: () => {}, fail: () => {} }); };
2. 全局自动捕获错误(最重要!)
在
main.js里注册 全局错误监听,自动捕获所有未处理的报错:// 1. 捕获 Vue 页面/组件渲染错误 Vue.config.errorHandler = (err, vm, info) => { logReport('error', `Vue错误:${info}`, err); }; // 2. 捕获 uni-app 全局错误(小程序/App 原生错误) uni.onError((err) => { logReport('error', '全局JS错误', { stack: err }); }); // 3. 捕获请求失败错误(可选) uni.addInterceptor('request', { fail: (err) => { logReport('error', '接口请求失败', err); } });
3. 手动埋点(用户行为 / 关键流程)
业务代码里主动上报关键日志:
// 用户点击事件 logReport('action', '用户点击提交订单'); // 业务异常 try { // 代码逻辑 } catch (err) { logReport('error', '提交订单失败', err); }
三、小程序专属优化
- 小程序后台限制 日志上报用
uni.request,不要在 onLaunch 里大量上报,避免被后台限流;- 合并上报 日志先存在本地,攒够一定数量再一次性上报,减少请求次数;
- 不影响主流程 上报请求不加
try/catch拦截,失败不阻塞用户操作,静默失败即可。
四、面试满分回答(直接背)
我在项目里是这样实现 uni-app 小程序日志上报 的:
- 首先 封装一个统一的日志上报工具函数,通过
uni.getSystemInfo获取设备信息,同时收集用户信息、当前页面、错误堆栈等数据;- 然后在全局配置 自动错误捕获:
- 用
Vue.config.errorHandler捕获页面渲染、JS 错误- 用
uni.onError捕获小程序原生全局错误- 还可以通过拦截器捕获接口请求失败
- 所有错误和日志会通过
uni.request静默上报到后端服务;- 同时支持 手动埋点,在关键业务流程里主动上报用户行为和业务异常,方便线上排查问题。
总结
- 核心:全局自动捕获 + 手动埋点 + 统一上报
- 关键 API:
Vue.config.errorHandler、uni.onError、uni.getSystemInfo- 用途:线上错误监控、问题排查、用户行为分析
- 平台:完美兼容微信小程序 + uni-app 多端
27、如何实现小程序的录音和语音发送?
一、核心概念解读
uni.getRecorderManager()会返回一个 全局唯一的录音管理器recorderManager,它比uni.startRecord更灵活,支持 开始 / 暂停 / 继续 / 停止 等完整的录音控制,适配 微信 / 支付宝 / 抖音 等绝大多数小程序平台(H5 不支持)。图里的核心方法:
方法 作用 说明 start(options)开始录音 可配置格式、采样率、时长等参数 stop()停止录音 停止后会触发 onStop事件,返回临时文件路径
onStart callback 录音开始事件
onStop callback 录音停止事件,会回调文件地址
onError callback 录音错误事件, 会回调错误信息
二、基于
recorderManager的完整代码(推荐)相比之前的
uni.startRecord,用recorderManager能更好地控制录音流程,比如监听开始 / 结束事件、处理异常等。1. 页面结构(
template)<template> <view class="record-container"> <button @touchstart="handleRecordStart" @touchend="handleRecordEnd" :disabled="isRecording" type="primary" > {{ isRecording ? '正在录音,松开发送' : '按住录音' }} </button> <view v-if="recordStatus === 'recording'" class="status-text"> 录音中... {{ recordTime }}s </view> <view v-if="recordPath" class="result-box"> <text>录音文件:{{ recordPath }}</text> <button @click="playVoice">播放录音</button> <button @click="uploadVoice">上传发送</button> </view> </view> </template>
2. 核心逻辑(
script)export default { data() { return { recorderManager: null, // 录音管理器实例 isRecording: false, // 是否正在录音 recordStatus: 'idle', // 录音状态:idle/recording/stopped recordPath: '', // 录音文件临时路径 recordTime: 0, // 录音时长 timer: null // 计时定时器 }; }, onLoad() { // 页面加载时初始化录音管理器 this.initRecorderManager(); }, beforeDestroy() { // 页面销毁时停止录音、清除定时器 if (this.isRecording) { this.recorderManager.stop(); } clearInterval(this.timer); }, methods: { // 1. 初始化录音管理器并绑定事件 initRecorderManager() { this.recorderManager = uni.getRecorderManager(); // 录音开始事件 this.recorderManager.onStart(() => { console.log('录音开始'); this.recordStatus = 'recording'; this.startRecordTimer(); }); // 录音停止事件(会返回临时文件路径) this.recorderManager.onStop((res) => { console.log('录音结束', res.tempFilePath); this.recordPath = res.tempFilePath; this.recordStatus = 'stopped'; this.isRecording = false; this.stopRecordTimer(); }); // 录音错误事件 this.recorderManager.onError((err) => { console.error('录音错误', err); uni.showToast({ title: '录音失败', icon: 'none' }); this.isRecording = false; this.recordStatus = 'idle'; this.stopRecordTimer(); }); }, // 2. 按住开始录音(先申请权限) async handleRecordStart() { const hasAuth = await this.checkRecordAuth(); if (!hasAuth) return; this.isRecording = true; this.recordPath = ''; this.recordTime = 0; // 开始录音,配置参数 this.recorderManager.start({ duration: 60000, // 最长录音60秒 sampleRate: 16000, encodeBitRate: 48000, format: 'mp3', // 推荐mp3,兼容性最好 frameSize: 50 }); }, // 3. 松开停止录音 handleRecordEnd() { if (this.isRecording) { this.recorderManager.stop(); } }, // 4. 播放录音 playVoice() { if (!this.recordPath) return; const innerAudioContext = uni.createInnerAudioContext(); innerAudioContext.src = this.recordPath; innerAudioContext.play(); }, // 5. 上传录音文件到服务器 uploadVoice() { if (!this.recordPath) return; uni.uploadFile({ url: 'https://你的后端接口地址/upload/voice', // 替换为你的上传接口 filePath: this.recordPath, name: 'voice', // 后端接收文件的字段名 formData: { userId: '当前用户ID', type: 'chat' }, success: (res) => { const data = JSON.parse(res.data); if (data.code === 0) { uni.showToast({ title: '发送成功' }); // 发送后清空状态 this.recordPath = ''; this.recordStatus = 'idle'; } else { uni.showToast({ title: '发送失败', icon: 'none' }); } }, fail: () => { uni.showToast({ title: '网络错误', icon: 'none' }); } }); }, // --- 工具方法 --- // 申请录音权限 checkRecordAuth() { return new Promise((resolve) => { uni.getSetting({ success: (res) => { if (res.authSetting['scope.record']) { resolve(true); } else { uni.authorize({ scope: 'scope.record', success: () => resolve(true), fail: () => { uni.showModal({ title: '权限提示', content: '请在设置中开启录音权限', showCancel: false }); resolve(false); } }); } } }); }); }, // 录音计时 startRecordTimer() { this.timer = setInterval(() => { this.recordTime++; }, 1000); }, stopRecordTimer() { clearInterval(this.timer); this.timer = null; } } };
三、必须配置的权限
在
manifest.json中开启小程序录音权限(以微信小程序为例):"mp-weixin": { "permission": { "scope.record": { "desc": "用于发送语音消息,需要使用录音功能" } } }
permission Object 微信小程序接口权限相关设置,比如申请位置权限必须填此处详见
四、关键注意事项
- 平台兼容性:H5 不支持录音,仅小程序、App 支持;暂停 / 继续功能部分平台(如微信)支持,App 暂不支持。
- 文件生命周期:录音得到的
tempFilePath是临时文件,小程序重启后会失效,必须上传到服务器后保存。- 安全限制:录音必须由用户主动触发(如点击 / 按住),不能在页面加载后自动调用,否则会被平台拦截。
- 时长限制:默认最长录音时长为 60 秒,可通过
start方法的duration参数修改(单位毫秒)。
补充:和
uni.startRecord的区别
uni.startRecord是旧版 API,功能简单,只有开始 / 结束两个操作,不支持暂停、继续和事件监听。recorderManager是新版 API,支持完整的录音控制,能监听开始 / 结束 / 错误事件,是现在更推荐的用法。
28、你们是怎么对网络请求做防抖和节流的?
一、先一句话分清两者
- 防抖:最后一次才执行(频繁触发 → 只认最后一次)
- 适合:按钮点击、搜索输入、表单提交
- 节流:固定频率执行(频繁触发 → 每隔 N 秒执行一次)
- 适合:滚动、窗口 resize、鼠标移动、高频输入
二、直接封装到 utils 工具函数(可全局导出)
新建
utils/debounceThrottle.js/** * 防抖:频繁触发时,只执行最后一次 * @param {Function} func 要执行的函数 * @param {Number} delay 延迟时间(毫秒) * @returns 防抖后的函数 */ export function debounce(func, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; } /** * 节流:固定频率执行 * @param {Function} func 要执行的函数 * @param {Number} interval 间隔时间(毫秒) * @returns 节流后的函数 */ export function throttle(func, interval = 300) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; func.apply(this, args); } }; }这是 前端通用标准写法,Vue / React / 原生 JS 都能用。
三、最常用场景 + 代码示例
1)防抖:按钮点击(防止重复提交)
你说的 提交按钮、操作按钮 最适合用防抖。
Vue 示例
<template> <button @click="handleSubmit">提交</button> </template> <script> import { debounce } from '@/utils/debounceThrottle' export default { methods: { // 防抖:300ms 内多次点击只执行一次 handleSubmit: debounce(async function() { // 这里发请求 await this.$axios.post('/api/submit') }, 300) } } </script>
2)防抖:搜索输入框(输入完再请求)
输入过程不请求,停止输入 300ms 后才请求。
<template> <input @input="handleSearch" placeholder="搜索" /> </template> <script> import { debounce } from '@/utils/debounceThrottle' export default { methods: { handleSearch: debounce(function(e) { // 发送搜索请求 this.$axios.get('/api/search', { params: { keyword: e.target.value } }) }, 300) } } </script>
3)节流:列表滚动加载(滚动时不疯狂请求)
滚动时 每隔 300ms 执行一次判断,防止高频触发。
<script> import { throttle } from '@/utils/debounceThrottle' export default { mounted() { // 滚动事件使用节流 window.addEventListener('scroll', this.handleScroll) }, methods: { handleScroll: throttle(function() { // 滚动加载逻辑 const scrollTop = document.documentElement.scrollTop if (scrollTop > 1000) { this.loadMoreList() } }, 300) } } </script>
4)节流:窗口 resize / 鼠标移动
高频事件都用 节流。
四、一句话总结最佳实践
- 按钮点击、表单提交、搜索输入 → 用 防抖(debounce)
- 滚动、resize、鼠标移动、高频输入 → 用 节流(throttle)
- 延迟时间一般用 200~500ms 最舒服
总结
- 我给你的是 项目通用工具函数,直接复制到 utils 就能全局用
- 防抖 = 最后一次执行
- 节流 = 固定频率执行
- 按钮 / 输入用防抖,滚动 / resize 用节流
29、如何实现小程序的扫码功能?
答案: 调用 uni.scanCode,用户扫码后得到结果(如二维码中包含参数)。前端解析参数并执行相应操作(如跳转、绑定)。
1. 核心代码(直接用)
<template> <!-- 一个扫码按钮 --> <button @click="handleScan">扫码邀请码</button> </template> <script> export default { methods: { // 扫码逻辑 async handleScan() { try { // 1. 调用 uni-app 扫码 API const res = await uni.scanCode({ onlyFromCamera: false, // false=支持相册选码;true=只能相机扫码 scanType: ['qrCode'] // 只扫二维码 }) // 2. 获取扫码结果(就是二维码里的内容,比如你的邀请码) const result = res.result; console.log('扫码结果:', result); // 输出:邀请码 123456 // 3. 处理邀请码(请求接口绑定、跳转页面、回填输入框都行) if (result) { this.bindInviteCode(result); // 调用接口绑定邀请码 } } catch (err) { console.log('扫码失败/取消', err); } }, // 绑定邀请码接口 async bindInviteCode(code) { const res = await uni.request({ url: '/api/bindInvite', method: 'POST', data: { inviteCode: code } }); // 后续提示/跳转... uni.showToast({ title: '绑定成功' }); } } } </script>
2. 关键说明
uni.scanCode:uni-app 自带,不需要额外插件res.result:就是二维码里的内容(你们的 邀请码)- 支持:相机扫码 + 相册识别二维码(可配置关闭相册)
- 场景:绑定邀请码、跳转页面、登录、加好友、进入页面…… 都通用
二、你项目里的真实流程
- UI 上放一个 扫码按钮
- 点击 → 调用
uni.scanCode- 扫到 邀请码字符串
- 把邀请码传给后端接口
- 完成绑定 / 跳转
就是你说的:UI 已经有了,就差写这几行代码。
三、最常用配置(直接选)
uni.scanCode({ onlyFromCamera: true, // 仅相机扫码(推荐更安全) scanType: ['qrCode'], // 只识别二维码 success: (res) => { console.log(res.result) // 邀请码 } })
总结
- 小程序扫码 = 调用
uni.scanCode- 拿到
res.result就是 邀请码- 传给后端接口即可完成绑定
- 代码量极少,5 分钟就能写完
30、你如何实现小程序的地图选择与导航?
答案: 使用 uni.chooseLocation 获取位置,使用 uni.openLocation 在地图应用中展示并导航。需要配置相关权限。
实现小程序地图选择和导航主要使用 uni-app 官方地图 API:
- 地图选位置:调用
uni.chooseLocation打开系统地图,让用户手动选择地点,返回地址名称、经纬度。- 导航:调用
uni.openLocation传入经纬度和地址,自动打开地图 App 进行导航。- 权限:必须在
manifest.json配置 位置权限,否则无法使用。
uni.chooseLocation(OBJECT)
打开地图选择位置。chooseLocation 会使用项目配置的地图服务商来展示地图
success 返回参数说明
参数 说明 name 位置名称 address 详细地址 latitude 纬度,浮点数,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系。 longitude 经度,浮点数,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系。
二、项目真实实现(直接复制)
1. 选择位置(用户选地址)
// 打开地图选择位置 uni.chooseLocation({ success: (res) => { console.log('位置名称', res.name); // 详细地址 console.log('详细地址', res.address); // 地址 console.log('纬度', res.latitude); // 纬度 console.log('经度', res.longitude); // 经度 // 可以把地址显示在页面,或传给后端 this.address = res.address; } });2. 打开导航(去律师事务所 / 目的地)
// 打开地图导航 uni.openLocation({ latitude: 23.12345, // 目标纬度 longitude: 113.12345, // 目标经度 name: '律师事务所', // 地点名 address: 'XX市XX区XX路',// 详细地址 success: () => { console.log('打开地图成功'); } });3. 获取当前定位(自动定位当前位置)
uni.getLocation({ type: 'gcj02', success: (res) => { console.log('当前位置', res); } });
三、必须配置的权限(不然会报错)
1. manifest.json → 微信小程序配置
"permission": { "scope.userLocation": { "desc": "你的位置信息将用于定位附近律师" } }HbuilderX => manifest.json
【在列表最下面的 “源码视图”里面配置(不是在‘微信小程序配置’)】
/* 小程序特有相关 */ "mp-weixin" : { "appid" : "wxd98d0a15a5638f28", "setting" : { "urlCheck" : false, "es6" : true, "minified" : true }, "usingComponents" : true, "permission" : {} },
四、一句话总结(面试最稳)
使用 uni.chooseLocation 实现地图选点,使用 uni.openLocation 实现导航功能,同时配置位置权限即可完成地图相关功能。
总结
- 选位置:
uni.chooseLocation- 导航:
uni.openLocation- 自动定位:
uni.getLocation- 必须配置权限
31、如何实现小程序的缓存清理功能?
在个人中心提供 “清理缓存” 按钮,调用 uni.clearStorage 或 uni.removeStorage 删除指定 key。清理后提示用户并刷新页面。
在小程序 个人中心页面 做一个 清理缓存 按钮:
- 点击按钮时,调用
uni.clearStorage()清空所有缓存,或用uni.removeStorageSync('token')删除指定缓存;- 清理完成后弹出提示(如 “缓存已清除”);
- 同时清除用户登录态(token、用户信息等),必要时跳转到登录页。
二、真实实现代码(直接复制到你的页面)
<template> <!-- 个人中心里的清理缓存按钮 --> <view @click="clearAllCache">清理缓存</view> </template> <script> export default { methods: { // 清理缓存 clearAllCache() { uni.showModal({ title: '提示', content: '确定要清理缓存吗?', success: (res) => { if (res.confirm) { // 1. 清空所有本地缓存(token、用户信息全部清除) uni.clearStorageSync() // 2. 提示清理成功 uni.showToast({ title: '缓存已清空', icon: 'success' }) // 3. 清理后跳转到登录页(退出登录) setTimeout(() => { uni.reLaunch({ url: '/pages/login/login' }) }, 1500) } } }) } } } </script>
三、如果你只想删指定缓存(不删全部)
// 只删除 token,不清空其他缓存 uni.removeStorageSync('token')
四、对应你给的配置文件说明
你现在的
mp-weixin配置 不需要改任何东西,清理缓存是 uni-app 自带 API,不用配置权限。
超级简短总结(面试一句话通关)
在个人中心加 清理缓存按钮,调用
uni.clearStorage清空所有缓存,清除登录态并提示用户,最后跳转到登录页。
离职间隔了半年,Hr 问的话,怎么说呢?
没有,离职之后休息了一段时间,然后快过年了就直接回家了。这年后刚开始找工作
如果企业约你面试,一定要问面试官是谁
自我介绍:
面试官您好,我叫 张XX,来自山东XX,毕业于山东XX大学,有X年的前端开发经验,我参与过多个XX内部系统的重构与开发,有PC端、小程序和H5端的开发经验,熟悉的技术栈是 Vue2、Vue3,熟练掌握 Composition API、Vite 等现代前端技术,能够独立完成从需求分析到页面开发、联调上线的全流程,具备完整的项目交付能力。以上是我的自我介绍,谢谢。
简历投递策略
针对简历中“专业不符”的问题,明确了应对策略:坦诚说明是因系统录入错误导致学籍专业与实际不符,并强调本人实际学习和掌握的是计算机专业。
一、统招专科专业不符 自然版话术
其实我当年高考填志愿,是家里帮忙报的,直接给我志愿报错了。我大一刚入学上半学期就主动找学校申请转专业,也走了校内转专业流程,后面全程 跟着计算机专业上课、学的也全是计算机课程。但当时学校学籍那边老师没给同步更新,也没人跟我提醒这事,就这么一直拖着,等到毕业才发现学籍专业没转过来。所以现在学信网和毕业证上显示的是旧专业,但 我实际全程学的、专业课都是计算机方向,专业基础和技术底子都是科班计算机出来的。
二、成人本科专业填错 圆场话术
我成人本科当时报的本来也是计算机相关,是我自己填简历的时候 复制粘贴没改干净,顺手带成建筑专业了,属于个人手误写错了。本身我统招专科就是正经计算机专业,一直做的也是前端开发这条线,本科只是用来补学历,主要还是看我统招专业和实际工作技术经验。
三、核心分寸(记住这点就行)
- 语气 随口带过、别解释太长,不用刻意辩解,越淡定越没人深究;
- 把锅分到:家里报错志愿 + 学校学籍没更新 + 自己简历复制手误,都不是原则性问题;
- 重点落点:实际学的是计算机、一直干开发、统招专科是正经计算机,把核心优势稳住;
- 就像你说的:愿意要你的公司,不会揪这点;不想录的,再完美也能挑毛病,咱们话术做到 合理、自然、无漏洞 就够了。
简历准备妥当后:一开始先不需要主动打招呼,先做一件事:
我给你把这套逻辑捋得明明白白,你照着做就行,完全不用瞎主动发消息:
现在不用主动跟任何公司打招呼、发话术就单纯打开 Boss 直聘,筛选你对口的前端岗位,看着匹配度高、公司靠谱、薪资合适的,只点收藏,不发消息、不聊、不搭话。
接下来这两小时就只干一件事:批量收藏你不停刷、不停筛,觉得还行的就直接收藏,不用纠结太多,先囤起来就行。平台机制就是:你频繁浏览 + 收藏岗位,系统会给你推曝光,HR 和招聘方会主动刷到你简历,主动过来跟你打招呼、发面试邀约。
等中后期有人主动找你了,再开口聊不用你主动破冰,等 HR 先发消息了,你再顺势回复就行,省得主动找人家还容易被冷落、掉价。
核心逻辑主动打招呼容易显得急着找工作、被动;只收藏不说话,靠平台流量被动引流,HR 主动来找你,你就占主动权,挑公司、谈薪资都好谈。
你现在直接照着执行:不用发任何问候语,就刷岗、收藏,坐等别人来找你就行。等后面有人找你了,我再给你准备好万能回复话术,直接复制就能用。
“收藏别忘了做哈”(先收藏一两天)
更多推荐



























所有评论(0)