本次会议围绕张同学的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 开发小程序,核心有三点优势:

  1. 跨端能力强,一套代码 可编译运行在 微信小程序、H5、App 等多端,适配性强,便于业务后续多平台拓展;
  2. 上手成本低,基于 Vue 语法开发,技术栈统一,开发和学习效率更高;
  3. 生态完善,插件、UI 组件库丰富,能快速落地业务需求。当前项目虽仅需小程序端,使用 UniApp 也能为后续拓展 H5、APP 业务提前预留扩容空间,减少后期重构成本。

2、您如何搭建 Uni-app 项目的开发环境?

    答案: 使用 HBuilderX 创建 Uni-app 项目,或通过 Vue CLI + @dcloudio/vite-plugin-uni 搭建。配置 pages.json 管理页面路径 "pages" 、tabBarglobalStyle。安装 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 开发环境主要有两种方式:

  1. 快速开发使用 HBuilderX 直接 初始化 uni-app 项目,开箱即用,配置简单、上手快;
  2. 工程化项目则通过 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 自带完美适配:

  1. 所有组件 默认使用 rpx
  2. 支持响应式尺寸、响应式主题
  3. 自动兼容 小程序 / 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 里一致(你已配置正确)

六、完整最佳实践总结(直接照做)

  1. 设计稿统一 750px 宽
  2. 尺寸全部用 rpx
  3. 布局全部用 flex
  4. 全屏高度用 100vh
  5. 自适应宽度用 %
  6. UI 组件用 uView(自带适配)
  7. 导航栏 / TabBar 用 pages.json 全局配置

这套方案可以完美适配:

✅ 微信小程序 ✅ 支付宝小程序 ✅ App ✅ H5 ✅ 抖音小程序


总结

  1. 你的项目 基础配置完全正确(easycom、pages、tabBar、globalStyle 都没问题)
  2. 多端适配核心就是:rpx + flex + uView,这是 uni-app 行业标准方案
  3. 不用写复杂媒体查询,不用针对各端单独写样式,全端自动适配
  4. 你之前说的 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);
  }
}

四、你的封装思路亮点(非常专业)

  1. 环境分离:config.js 管理 baseUrl,切换环境只需改一个变量
  2. 权限分离:auth.js 单独管理 Token,高内聚低耦合
  3. 请求统一:request.js 统一处理加载、请求头、成功、失败
  4. 接口集中:apis.js 统一管理所有接口,方便维护、查找、修改
  5. 全端通用:小程序 / App / H5 都能用

总结

    你的网络请求封装 完全符合企业级标准,逻辑清晰、结构规范、可维护性极强。我给你的代码就是 直接按照你的思路写出来的完整版,复制到项目里就能用。

5、您如何处理请求的并发和取消?

    答案:使用 Promise.all 处理并发请求。对于重复请求,存储请求的 Promise 并返回相同 Promise,或使用 AbortController 取消未完成的请求(Uni-app 支持)。在路由切换时取消无关请求。


你理解的 核心逻辑完全正确,只是 叫法不一样

  • 你说的 Cancel = uni-app 里的 RequestTask.abort ()
  • 你说的 合并接口、Promise.all = 处理并发

我直接给你 最标准、最贴合 uni-app / 小程序 的回答,面试一字不差能用。


面试标准答案(背这个)

问:你如何处理请求的并发和取消?

回答:

  1. 并发处理    我主要使用 Promise.all 来处理多个接口同时请求,统一等待返回结果后再渲染页面,避免多次 setState 导致页面抖动。另外也会和后端协商,把多个小接口合并成一个接口,减少请求数量,从源头降低并发压力。

  2. 请求取消    在 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。如果没有且页面需要登录(如个人中心),则跳转登录页。登录成功后返回原页面。


我直接给你 最标准、最常用、最稳定 的实现方式,和你说的流程完全一致:

  1. 登录 → 后端返回 Token
  2. 前端存:localStorage / sessionStorage + Vuex
  3. 请求拦截器:所有接口自动在请求头携带 Token
  4. 路由拦截器:未登录自动跳登录页
  5. 登录成功后:返回原来想访问的页面

一、登录逻辑(你已经在做的)

登录接口成功后:

// 登录成功
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.jsmain.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' });
}

六、整套流程总结(最清晰的一句话版本)

  1. 登录:后端返回 Token → 存本地 + Vuex
  2. 请求:统一拦截 → 自动在请求头带 Token
  3. 路由:统一拦截 → 需登录页面无 Token → 跳登录
  4. 登录成功:回到原本想访问的页面

总结

  1. 路由拦截uni.addInterceptor 拦截跳转,判断 token 是否存在
  2. 鉴权:白名单 / 黑名单判断页面是否需要登录
  3. Token:本地存储(持久化)+ Vuex(状态管理)
  4. 请求:拦截器自动携带请求头
  5. 体验:登录后自动返回原页面

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 属性实现 动态渲染;再通过 autoplaydurationindicator-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 点就满分)

  1. 组件使用:直接使用 uView 的 <u-swiper> 组件,不用自己手写 swiper
  2. 动态数据:通过 uni.request 请求后端接口,拿到图片、跳转链接
  3. 动态绑定:把接口数据赋值给 list 属性,v-for 自动渲染
  4. 效果配置:开启自动轮播、设置时间、指示器、圆角、高度,调整样式

总结

  1. 组件:uView 的 u-swiper(uni-app 最常用)
  2. 数据:后端接口请求 → 动态赋值
  3. 配置:autoplay / interval / 指示器 / 圆角 / 高度
  4. 样式:样式穿透调整 轮播图外观

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 实时更新进度条
    })
  }
})

总结

  1. 选择uni.chooseImage
  2. 上传uni.uploadFile
  3. 进度onProgressUpdate
  4. 结果:返回文件 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 步就满分)

  1. 生命周期触发:在首页 onLoad 自动执行微信登录
  2. 微信授权登录:调用 uni.login 拿到 code,配合 uni.getUserInfo 获取用户信息
  3. 信息存储:将用户信息、登录状态本地存储 + 存入 Vuex
  4. 身份选择跳转:用户点击「我是律师」「我是用户」,进入对应端的首页界面
uni.login({
  provider: 'weixin',
  success: function (loginRes) {
    console.log(loginRes.authResult);
    // 获取用户信息
    uni.getUserInfo({
      provider: 'weixin',
      success: function (infoRes) {
        console.log('用户昵称为:' + infoRes.userInfo.nickName);
      }
    });
  }
});

关键亮点(面试官很喜欢)

  • 双端合一,一套代码区分律师 / 用户两种角色
  • 自动登录,进入首页就触发微信授权
  • 身份手动选择,界面清晰区分不同权限页面

总结

  1. 入口:首页 onLoad 生命周期
  2. 登录:uni.login 获取 code + uni.getUserInfo 获取信息
  3. 角色:律师端 / 用户端 手动选择 + 界面区分
  4. 存储:本地存储 + Vuex

10、您如何实现消息模块的 WebSocket 通信?

    答案: 使用 uni.connectSocket 建立连接。监听 onOpen、onMessage、onError、onClose。在 onMessage 中解析消息,存入本地数据并更新界面。发送消息调用 uni.sendSocketMessage。实现心跳(定时发送 ping)和 断线重连。


    我给你整理一套 生产环境可用、逻辑完整、可直接复用 的 uni-app WebSocket 消息模块实现方案,覆盖你提到的所有核心点:建立连接、监听事件、收发消息、心跳保活、断线重连、在线人数 / 公告等业务扩展。

一、核心实现流程(标准步骤)

  1. 创建 WebSocket 连接:使用 uni.connectSocket 初始化 并 建立长连接
  2. 监听四大核心事件:连接成功 (onOpen)、接收消息 (onMessage)、连接错误 (onError)、连接关闭 (onClose)
  3. 消息处理:onMessage 解析数据 → 本地存储 / 更新数据 → 刷新页面 UI
  4. 发送消息:调用 uni.sendSocketMessage 主动推送消息
  5. 保活机制:定时发送心跳(ping),防止服务端主动断开
  6. 容错机制:断线自动重连,保证消息模块稳定性

二、完整可运行代码(封装成工具类,最佳实践)

// 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 = "";
    },
  },
};

四、核心知识点总结(标准回答)

  1. 建立连接:使用 uni.connectSocket 创建 WebSocket 实例并建立长连接;
  2. 事件监听:监听 onOpen(连接成功)、onMessage(接收消息)、onError(错误)、onClose(关闭) 四大核心事件;
  3. 消息处理:在 onMessage 中解析后端数据,根据消息类型更新本地数据并刷新页面 UI;
  4. 发送消息:调用 uni.sendSocketMessage 向服务端发送消息;
  5. 心跳保活:定时发送 ping 心跳包,避免服务端因空闲断开连接;
  6. 断线重连:连接异常 / 关闭时,自动触发重连机制,保证消息模块稳定性;
  7. 业务扩展:支持在线人数统计、系统公告、聊天消息等场景。

总结

    这套方案是 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。每次收到新消息追加,加载会话时先读缓存再请求最新消息。缓存设置最大条数,超出则删除旧消息。

这么写的原因:

  1. 代码简单、直观:不用写回调,顺序执行。
  2. 读的时候也用 getStorageSync
    // 打开会话先读缓存
    const list = uni.getStorageSync('chat_123') || []
    
  3. 聊天记录一般 不算超大体积,同步阻塞影响不大。

三、和 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. 图片资源优化

  1. 图片压缩、转 WebP 格式,减小体积;
  2. 采用 图片懒加载,进入视口再加载;
  3. 合理用图片 CDN,后端做图片自适应裁剪,不用大图缩小展示。

2. 长列表性能优化

  1. 优先 接口分页,下拉触底加载,不一次性渲染全部数据;
  2. 超大数据列表用 虚拟列表(虚拟滚动),只渲染可视区域 DOM;
  3. 减少列表项复杂层级、避免过多绑定事件。

3. 页面渲染 & 数据优化

  1. 非响应式数据不放在 data 里,直接挂在实例上,减少监听和 diff 开销;
  2. 减少 setData 调用频率,合并多次 setData,避免频繁更新视图;
  3. 精简页面 wxml 结构,减少无用标签、避免多层嵌套。

4. 代码与打包优化

  1. 开启 Gzip 压缩,减小静态资源体积;
  2. 清除无用代码、注释、未引用组件,精简项目体积;
  3. 避免全局冗余组件、全局样式污染。

5. 网络请求优化

  1. 接口请求 合并请求,减少请求次数;
  2. 接口做缓存,重复请求优先拿本地缓存,不用每次都调接口;
  3. 避免定时器、监听事件页面卸载后 不销毁,造成内存泄漏。

6. 分包加载优化

    小程序开启 分包拆分,把首页、核心业务放主包,其他页面、组件放分包,减少首屏加载时间。

7. 本地缓存优化

    合理使用 uni.setStorage 做数据缓存、聊天记录缓存、接口数据缓存,减少重复网络请求,提升页面打开速度。


面试答题话术(直接背这一段就行)

    小程序性能优化我主要从几个方面做:第一是图片优化,统一转 WebP 格式、做懒加载,配合后端 CDN 裁剪压缩;第二长列表用分页 + 虚拟滚动,避免一次性渲染大量节点造成卡顿;第三控制 setData 频次,非响应式数据不放进 data,减少视图层 diff 消耗;第四项目开启 Gzip 压缩、做分包加载,拆分主包 和 分包提升首屏速度;同时合理做本地数据缓存,合并接口请求,页面及时销毁定时器 和 监听,避免 内存泄漏

13、你们是如何实现支付功能的?

    我们项目是用 uni-app 开发 App 端 微信支付,整体分为三部分:开通配置、服务器下单、客户端 调起支付

  1. 前期开通与配置

    • 先在微信开放平台申请 AppID,提交应用审核;
    • 支付申请通过后,拿到商户号(PartnerID),再登录微信商户平台设置 APIv2 密钥;
    • 在 uni-app 项目的 manifest.json 里,勾选 “微信支付”,配置好 AppID,iOS 平台还要配置 Universal Links,确保和微信开放平台的开发信息一致。
  2. 服务器端生成预支付订单

    • 客户端先请求我们的业务服务器下单;
    • 服务器调用微信支付的 统一下单 接口,传入订单信息,拿到 prepayid(预支付会话 ID);
    • 再根据 AppID、商户号、prepayid、随机字符串、时间戳等参数,按微信的签名算法生成 sign,把这些参数一起返回给前端。
  3. App 端调起支付

    • 拿到服务器返回的订单信息后,调用 uni.requestPaymentprovider 固定为 wxpayorderInfo 传完整的订单对象(包含 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 库就能实现:

  1. 下拉刷新

    • 在 pages.json 里“pages”内开启 enablePullDownRefresh: true
    • 页面中监听 onPullDownRefresh 生命周期
    • 在里面重新请求接口、刷新列表数据
    • 刷新完成后调用 uni.stopPullDownRefresh() 停止刷新动画
  2. 上拉加载更多

    • 监听页面生命周期 onReachBottom
    • 当页面触底时,请求下一页数据
    • 拼接进列表
    • 根据总页数判断是否加载完毕,避免重复请求
  3. 什么时候用 uView?

    • 内置的样式比较原生、比较简单
    • 如果项目需要 自定义动画、自定义图标、下拉效果更美观,才会使用 uView 的 LoadMore 加载更多 或 pull-refresh 组件
    • 简单业务直接用 uni-app 自带的就够了,性能更好

最关键的总结(你说得完全正确)

  • uni-app 自带这两个功能,非常方便
  • 不是必须用 uView
  • 简单页面用内置,复杂好看的 UI 才用第三方组件
  • 内置原生方法性能更快

16、您如何实现自定义导航栏?

    答案: 在 pages.json 中设置 navigationStyle: custom,然后使用自定义组件(uni-nav-bar)实现。获取系统状态栏高度(uni.getSystemInfoSync().statusBarHeight)来适配不同机型。


一、核心两步(最重要)

  1. 关闭原生导航栏    pages.json 里当前页面配置:
    "style": {
      "navigationStyle": "custom"
    }
    
  2. 使用自定义导航栏组件    直接用 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,内部都是自动用这个高度做适配,你不用自己算!


四、面试标准答案(你要的那种)

问:如何实现自定义导航栏?

答:

  1. 在 pages.json 中"style"设置 navigationStyle: custom 关闭原生导航栏;
  2. 使用 uni-app 自带的 <uni-nav-bar> 自定义组件;
  3. 组件内部会自动获取 statusBarHeight 适配不同机型状态栏高度;
  4. 可配置标题、左侧返回、右侧按钮,完全支持自定义内容。

五、你说的拖拉拽 = 低代码方式

就是在 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 实现:

  1. 把 loading 状态维护在 Vuex / Pinia 全局状态里;
  2. 在 App.vue 中引入全局 Loading 组件,实现全局生效;
  3. 二次封装网络请求,请求拦截自动开启 loading,响应拦截无论成功失败都自动关闭;
  4. 支持手动在任意页面控制显示隐藏。

空状态实现:

封装可复用的空状态组件,预留图片、文案、按钮插槽,页面通过判断接口返回列表数据是否为空,做条件渲染,无数据时展示空状态组件,实现全局复用。

18、您如何实现小程序的版本更新检测?

    答案: 使用 uni.getUpdateManager,监听 onCheckForUpdateonUpdateReady。有新版本时提示用户重启应用,调用 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 个就行

  1. 核心 API:uni.getUpdateManager() 获取更新管理器
  2. 两个监听:onCheckForUpdateonUpdateReady
  3. 重启更新:applyUpdate()

19、您如何实现自定义组件(如律师卡片)?

    答案: 在 components 目录创建 .vue 文件,定义 props(avatar头像, name姓名, rating评分 等),使用 uni-ui 或 uView 组件构建模板。通过 easycom 自动引入,无需手动导入。


问:您如何实现自定义组件?(如律师卡片)答:

  1. 在项目 components 目录下创建独立的 .vue 组件文件;
  2. 通过 props 接收外部数据(头像、姓名、评分、信息等);
  3. 基于 uni-app 内置组件(如 uni-card)或 uView 组件进行 二次封装,统一项目样式;
  4. 组件内部实现点击、关闭、确认等自定义事件,通过 $emit 向外抛出;
  5. 利用 uni-app 的 easycom 自动引入组件,页面直接使用标签,无需手动导入注册。

超简记忆版(3 句话)

  1. 创建组件:components 文件夹写 .vue 页面;
  2. 传参交互:props 接收数据,$emit 触发事件;
  3. 全局使用: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 句话)

  1. 大文件 切小片
  2. 一片一片上传,记录进度
  3. 断了续传,最后合并

总结

  • 文件切割 不麻烦,就 file.slice() 一行代码
  • 断点续传 = 分片上传 + 记录已上传分片
  • uni-app 自己封装分片逻辑,不难

21、在处理请求并发时,你遇到过哪些问题?如何解决?

    答案: 多个请求同时发起时,loading 会闪烁 或 重复显示。我通过维护一个请求计数器,当计数器从 0 变为 1 时显示 loading,从 1 变为 0 时关闭。另外,对于相同 url 的请求,使用 pending 队列 避免重复请求。


处理接口请求并发时,我实际遇到过 三个典型问题,都做了统一封装解决:

  1. 多个请求同时发起,Loading 加载框闪烁、重复显示 / 提前关闭    解决:做 请求计数器,每发起一个请求 计数 + 1,请求结束 计数 - 1;计数器从 0 变 1 才开启 Loading,计数器归 0 再关闭 Loading,避免 并发下 loading 闪屏、提前消失。

  2. 同一接口短时间重复发起(比如按钮连点、页面多次触发),造成冗余请求、数据错乱    解决:维护 请求等待队列 Pending Map,把请求 URL 和 参数作为唯一 key,存在 pending 中就不再重复发起;等同个请求结束后再删除缓存,避免重复请求。

  3. 并发请求过多,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 步)

  1. 小程序启动自动调用登录    在小程序 onLaunch 生命周期里,自动执行 uni.login(),拿到微信临时凭证 code

  2. 把 code 传给后端    前端把 code 发送到后端接口,后端拿着 code 去微信官方服务器换取用户唯一标识 openidsession_key

  3. 后端返回自定义 token    后端不直接返回 openid,而是生成 自定义登录 token 返给前端。

  4. 前端存储 token    把 token 存到 localStorage / uni.setStorageSync,作为后续请求的登录凭证。


二、用户信息获取

  1. 用户头像、昵称不能自动获取,必须 用户主动授权,调用 uni.getUserProfile() 弹窗授权。
  2. 授权后拿到昵称、头像,可以传给后端保存
  3. 前端把用户信息存在 Vuex / Pinia(全局状态) + 本地缓存,方便页面使用。

三、用户信息管理

  1. token 作为登录核心依据    每次请求接口,在请求头里带上 token,后端校验是否登录。

  2. token 过期处理    后端返回 token 过期时,前端清除缓存,自动重新执行 uni.login 静默登录

  3. 状态管理    使用 Vuex 或 Pinia 维护全局用户登录状态、用户信息,页面直接使用,不用反复获取。


最精简版(怕忘就背这个)

  1. 小程序启动自动执行 uni.login 获取 code,传给后端。
  2. 后端换 openid,返回 自定义 token
  3. 前端把 token 存在 storage,Vuex / Pinia 管理用户状态。
  4. 用户信息必须授权获取,存在全局状态 + 本地缓存。
  5. 请求带 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 种

  1. 父子组件通信    props 父传子,子组件通过 $emit 自定义事件向父组件传值。

  2. 深层 / 跨级组件通信(爷孙、隔代)  用 事件总线 EventBus,通过 uni.$emit 派发事件,uni.$on 监听事件,适合隔代组件传参。也可以用 依赖 注入 provide / inject 跨层透传数据。

  3. 全局状态管理     Vuex / Pinia 做全局共享状态,所有页面、组件都能读写,适合全局用户信息、配置这类数据。

  4. 页面跳转 URL 传参    通过 uni.navigateTo 跳转时,在 url 后面拼接参数,目标页面在 onLoad 里接收参数。

  5. 全局全局变量 globalData     getApp().globalData 挂载全局数据,任何页面直接读写,简单轻量。

  6. 页面栈通信    通过 getCurrentPages() 获取页面栈,直接拿到上一个页面实例,赋值数据、调用页面方法,适合返回上一页刷新数据场景。

  7. 本地缓存通信    uni.setStorageSync / uni.getStorageSync 本地存储,页面、组件都能读写,适合持久化简单数据。


精简背诵版(面试直接说)

Uni-app 常用通信方式有:

  1. 父子组件用   props + $emit
  2. 跨级隔代用   EventBus 事件总线provide/inject
  3. 全局共享用   Vuex / Pinia
  4. 页面跳转用   URL 拼接传参
  5. 简易全局变量用 globalData
  6. 页面栈用   getCurrentPages 直接操作上一页;
  7. 持久化通信靠 本地 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 实现消息实时通信的。

  1. 通过 uni.connectSocket 创建长连接;
  2. 监听 onMessage 接收后端推送的实时消息;
  3. 为了保证连接稳定,加入了 心跳包机制 定时发送 ping;
  4. 处理网络异常,实现 断线自动重连
  5. 收到消息后更新到 Vuex 或 本地存储,页面自动刷新消息列表。

五、超级精简记忆版(3 句话)

  1. 用 uni.connectSocket 建立长连接
  2. onMessage 收消息,心跳保活,断线重连
  3. 收到消息存全局状态,页面刷新

六、和你刚才的代码衔接

你刚才写的跳转:

uni.navigateTo({
  url: `/pages/lawyerindex/answer?listId=${listId}`,
});

消息模块就是:进入页面 → 连接 WebSocket → 收消息 → 显示列表


总结

  • uni-app 内置 WebSocket,不用第三方库
  • 核心:连接 + 监听消息 + 心跳 + 重连

25、在开发过程当中你是如何调试uni-app这个项目的?

一、基础环境准备

    使用 HBuilderX 作为开发工具,它是 uni-app 官方推荐编辑器,内置 编译、运行、调试能力,支持一键发布到 H5、微信小程序、App、支付宝小程序 等 多端。


二、核心调试方式(分端讲解)

1. H5 端(内置浏览器 / 谷歌浏览器,你最常用的方式)

  1. 运行项目    在 HBuilderX 中选中项目,点击顶部菜单栏 运行 → 运行到浏览器 → 选择 Chrome / 内置浏览器,工具会自动编译项目并打开浏览器页面。
  2. 调试操作    按下 F12 打开 开发者工具,核心调试能力:
    • 控制台 (Console):查看代码报错、console.log 打印日志,快速定位语法错误、逻辑异常;
    • 元素 (Elements):查看页面 DOM 结构、样式,实时修改 CSS 调试布局;
    • 网络 (Network):监控所有接口请求,查看请求参数、响应数据、请求状态,排查接口调用失败、参数错误、跨域问题;
    • 源代码 (Sources):断点调试 JS 代码,逐行执行查看变量值;
    • 性能 / 内存:分析页面卡顿、内存泄漏等优化问题。
  3. 优势:编译速度最快,适合快速调试页面样式、基础逻辑、接口联调。

2. 微信小程序端(必备调试,适配小程序专属特性)

  1. 运行项目    HBuilderX 中点击 运行 → 运行到小程序模拟器 → 微信开发者工具,工具会自动生成小程序代码,并唤起微信开发者工具。
  2. 调试操作    在微信开发者工具中使用 调试器
    • 专门调试小程序专属 API(如 wx.login、微信支付、小程序路由等);
    • 查看小程序缓存、授权状态、原生组件渲染问题;
    • 排查 H5 端无法复现的小程序兼容性 bug。
  3. 核心作用:解决 多端差异问题,确保小程序功能正常。

3. App 端(真机 / 模拟器调试)

  1. 运行项目    点击 运行 → 运行到手机 或 模拟器 → 选择连接的真机 / 安卓模拟器,通过 USB 调试或 WiFi 连接设备。
  2. 调试方式
    • 开启 真机调试:在 HBuilderX 中查看控制台日志;
    • 使用 vconsole 调试面板:在页面中打印日志,查看 App 端的报错、网络请求;
    • 专门调试原生能力(如摄像头、扫码、定位、本地存储等)。

三、常用辅助调试技巧

  1. 日志打印:核心调试手段,在关键逻辑处使用 console.log() console.error() 打印变量、接口数据、执行流程;
  2. 断点调试:在浏览器 / 开发者工具中给代码打断点,逐行执行,精准定位逻辑 bug;
  3. 条件编译调试:针对多端差异代码,单独调试对应平台的逻辑;
  4. 网络调试:重点排查接口 404、500、参数错误、跨域、超时等问题。

四、整体总结(简洁版)

我在开发中主要用 HBuilderX 开发 uni-app 项目,调试分三步走:

  1. 优先调试 H5 端:运行到 Chrome 浏览器,F12 打开开发者工具,通过控制台看报错、网络面板查接口、元素面板调样式,效率最高;
  2. 小程序专属调试:运行到微信开发者工具,解决小程序 API、兼容性问题;
  3. App 端调试:连接真机 / 模拟器,用真机调试 和 vconsole 排查原生功能问题。全程结合日志打印、断点调试,快速定位并解决页面、逻辑、接口、多端兼容等问题。

总结

  1. 核心工具:HBuilderX + 浏览器 F12 + 微信开发者工具
  2. 主力调试:H5 端 Chrome 调试(样式、接口、基础逻辑);
  3. 补全调试:小程序 / App 端(专属 API、兼容性、原生功能);
  4. 核心手段:控制台日志、网络监控、断点调试。

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);
}

三、小程序专属优化

  1. 小程序后台限制    日志上报用 uni.request不要在 onLaunch 里大量上报,避免被后台限流;
  2. 合并上报    日志先存在本地,攒够一定数量再一次性上报,减少请求次数;
  3. 不影响主流程    上报请求不加 try/catch 拦截,失败不阻塞用户操作,静默失败即可。

四、面试满分回答(直接背)

我在项目里是这样实现 uni-app 小程序日志上报 的:

  1. 首先 封装一个统一的日志上报工具函数,通过 uni.getSystemInfo 获取设备信息,同时收集用户信息、当前页面、错误堆栈等数据;
  2. 然后在全局配置 自动错误捕获
    • Vue.config.errorHandler 捕获页面渲染、JS 错误
    • uni.onError 捕获小程序原生全局错误
    • 还可以通过拦截器捕获接口请求失败
  3. 所有错误和日志会通过 uni.request 静默上报到后端服务
  4. 同时支持 手动埋点,在关键业务流程里主动上报用户行为和业务异常,方便线上排查问题。

总结

  • 核心:全局自动捕获 + 手动埋点 + 统一上报
  • 关键 API:Vue.config.errorHandleruni.onErroruni.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 微信小程序接口权限相关设置,比如申请位置权限必须填此处详见


四、关键注意事项

  1. 平台兼容性:H5 不支持录音,仅小程序、App 支持;暂停 / 继续功能部分平台(如微信)支持,App 暂不支持。
  2. 文件生命周期:录音得到的 tempFilePath 是临时文件,小程序重启后会失效,必须上传到服务器后保存。
  3. 安全限制:录音必须由用户主动触发(如点击 / 按住),不能在页面加载后自动调用,否则会被平台拦截。
  4. 时长限制:默认最长录音时长为 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:就是二维码里的内容(你们的 邀请码
  • 支持:相机扫码 + 相册识别二维码(可配置关闭相册)
  • 场景:绑定邀请码、跳转页面、登录、加好友、进入页面…… 都通用

二、你项目里的真实流程

  1. UI 上放一个 扫码按钮
  2. 点击 → 调用 uni.scanCode
  3. 扫到 邀请码字符串
  4. 把邀请码传给后端接口
  5. 完成绑定 / 跳转

就是你说的: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

  1. 地图选位置:调用 uni.chooseLocation 打开系统地图,让用户手动选择地点,返回地址名称、经纬度。
  2. 导航:调用 uni.openLocation 传入经纬度和地址,自动打开地图 App 进行导航。
  3. 权限:必须在 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。清理后提示用户并刷新页面。


在小程序 个人中心页面 做一个 清理缓存 按钮:

  1. 点击按钮时,调用 uni.clearStorage() 清空所有缓存,或用 uni.removeStorageSync('token') 删除指定缓存;
  2. 清理完成后弹出提示(如 “缓存已清除”);
  3. 同时清除用户登录态(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 等现代前端技术,能够独立完成从需求分析到页面开发、联调上线的全流程,具备完整的项目交付能力。以上是我的自我介绍,谢谢。


简历投递策略

针对简历中“专业不符”的问题,明确了应对策略:坦诚说明是因系统录入错误导致学籍专业与实际不符,并强调本人实际学习和掌握的是计算机专业。

一、统招专科专业不符 自然版话术

    其实我当年高考填志愿,是家里帮忙报的,直接给我志愿报错了。我大一刚入学上半学期就主动找学校申请转专业,也走了校内转专业流程,后面全程 跟着计算机专业上课、学的也全是计算机课程。但当时学校学籍那边老师没给同步更新,也没人跟我提醒这事,就这么一直拖着,等到毕业才发现学籍专业没转过来。所以现在学信网和毕业证上显示的是旧专业,但 我实际全程学的、专业课都是计算机方向,专业基础和技术底子都是科班计算机出来的。

二、成人本科专业填错 圆场话术

    我成人本科当时报的本来也是计算机相关,是我自己填简历的时候 复制粘贴没改干净,顺手带成建筑专业了,属于个人手误写错了。本身我统招专科就是正经计算机专业,一直做的也是前端开发这条线,本科只是用来补学历,主要还是看我统招专业和实际工作技术经验。

三、核心分寸(记住这点就行)

  1. 语气 随口带过、别解释太长,不用刻意辩解,越淡定越没人深究;
  2. 把锅分到:家里报错志愿 + 学校学籍没更新 + 自己简历复制手误,都不是原则性问题;
  3. 重点落点:实际学的是计算机、一直干开发、统招专科是正经计算机,把核心优势稳住;
  4. 就像你说的:愿意要你的公司,不会揪这点;不想录的,再完美也能挑毛病,咱们话术做到 合理、自然、无漏洞 就够了。

简历准备妥当后:一开始先不需要主动打招呼,先做一件事:

我给你把这套逻辑捋得明明白白,你照着做就行,完全不用瞎主动发消息:

  1. 现在不用主动跟任何公司打招呼、发话术就单纯打开 Boss 直聘,筛选你对口的前端岗位,看着匹配度高、公司靠谱、薪资合适的,只点收藏,不发消息、不聊、不搭话

  2. 接下来这两小时就只干一件事:批量收藏你不停刷、不停筛,觉得还行的就直接收藏,不用纠结太多,先囤起来就行。平台机制就是:你频繁浏览 + 收藏岗位,系统会给你推曝光,HR 和招聘方会主动刷到你简历,主动过来跟你打招呼、发面试邀约。

  3. 等中后期有人主动找你了,再开口聊不用你主动破冰,等 HR 先发消息了,你再顺势回复就行,省得主动找人家还容易被冷落、掉价。

  4. 核心逻辑主动打招呼容易显得急着找工作、被动;只收藏不说话,靠平台流量被动引流,HR 主动来找你,你就占主动权,挑公司、谈薪资都好谈。

你现在直接照着执行:不用发任何问候语,就刷岗、收藏,坐等别人来找你就行。等后面有人找你了,我再给你准备好万能回复话术,直接复制就能用。

“收藏别忘了做哈”(先收藏一两天)

Logo

一站式 AI 云服务平台

更多推荐