UniApp前端面试
一、基础信息
1. 什么是UniApp?它的核心特点是什么?与传统前端框架(Vue/React)、原生开发(iOS/Android)、其他跨端框架(Flutter/React Native/Taro)的区别是什么?
答案:
-
定义:UniApp是DCloud推出的基于Vue.js的跨端开发框架,支持“一套代码”编译为iOS、Android、微信小程序、H5、支付宝小程序等10+平台。
-
核心特点:
① 跨端一致性:一套代码多端运行,降低多端开发成本;
② Vue生态兼容:复用Vue语法、组件化、Vuex/Pinia,学习成本低;
③ 原生能力调用:通过
uniAPI直接调用各端原生能力(如支付、定位);④ 性能贴近原生:App端支持nvue(原生渲染),小程序端复用原生渲染逻辑。
-
区别:
-
与Vue/React:Vue/React是单端框架,UniApp是跨端框架(基于Vue扩展跨端能力);
-
与原生开发:原生需分别编写iOS/Android代码,UniApp一套代码覆盖多端,但复杂原生能力需插件补充;
-
与Flutter:Flutter是自绘引擎(UI一致性强),UniApp适配各端原生渲染(更贴合平台特性);
-
与React Native:RN通过JS桥接原生组件,UniApp编译为各端原生代码(小程序端无桥接损耗);
-
与Taro:Taro基于React语法,UniApp基于Vue语法,且UniApp对小程序生态支持更深度。
-
2. UniApp支持哪些端的开发?不同端的打包流程有什么差异?
答案:
-
支持端:App(iOS/Android)、微信/支付宝/百度/字节跳动/QQ等小程序、H5、快应用、桌面端(Windows/macOS)。
-
打包流程差异:
① H5端:直接运行或打包为静态资源(HTML/CSS/JS),部署到服务器即可;
② 小程序端:HBuilderX中选择对应平台“发行”,生成小程序代码包,导入各平台开发者工具后上传审核;
③ App端:先配置
manifest.json(应用名称、权限、SDK),选择“发行-原生App-云打包/本地打包”:-
云打包:直接生成apk/ipa安装包(无需本地SDK);
-
本地打包:需安装Android Studio/Xcode,生成原生工程后编译打包;④ 桌面端:需安装UniApp桌面端打包插件,编译为exe/dmg文件。
-
3. UniApp的项目结构是怎样的?pages.json/manifest.json/App.vue/main.js分别有什么作用?
答案:
-
核心结构:
├── pages/ // 页面目录(每个页面一个子目录,含.vue/.json/.js/.wxss等) ├── static/ // 静态资源(图片、字体等,编译时不压缩) ├── components/ // 自定义组件目录 ├── uni_modules/ // 插件市场下载的模块 ├── pages.json // 页面路由、全局配置 ├── manifest.json // 应用配置(名称、权限、各端参数) ├── App.vue // 应用入口(全局样式、应用生命周期) ├── main.js // Vue实例创建、全局挂载 ├── uni.scss // 全局样式变量(如主题色) └── unpackage/ // 打包输出目录 -
文件作用:
-
pages.json:配置页面路由(pages数组)、tabbar、全局导航栏样式、下拉刷新、分包加载等; -
manifest.json:配置应用名称、图标、版本号、权限(如相机/定位)、各端特有配置(如小程序appid、App的SDK密钥); -
App.vue:应用级根组件,包含onLaunch/onShow/onHide等应用生命周期,可定义全局样式; -
main.js:创建Vue实例(createApp/createSSRApp),挂载全局组件、状态管理(Vuex/Pinia)、全局拦截器等。
-
4. UniApp中easycom组件自动引入机制的原理是什么?如何配置?
答案:
-
原理:
easycom是UniApp的组件自动引入机制,无需手动import和Vue.component注册,框架会根据约定的目录结构自动扫描、注册组件,降低组件使用成本。 -
约定规则:
① 组件需放在
components目录下(或自定义目录),且组件目录名与组件文件名一致(如components/uni-button/uni-button.vue);② 页面中直接使用组件标签(如
<uni-button>),框架会自动匹配并引入对应组件。 -
配置方式(
pages.json):{ "easycom": { "autoscan": true, // 开启自动扫描 "custom": { // 自定义匹配规则:正则匹配组件名 → 组件路径 "^uni-(.*)": "@/components/uni-$1/uni-$1.vue", "^my-(.*)": "@/components/my-components/my-$1.vue" } } } -
注意:
easycom默认开启,无需手动配置即可使用官方组件或符合约定的自定义组件。
5. UniApp的条件编译如何使用?请举例说明(如区分H5和小程序、区分iOS和Android)。
答案:
-
条件编译:通过
#ifdef(存在某平台)/#ifndef(不存在某平台)+ 平台标识,实现不同平台的代码差异化编译(非运行时判断),打包时会剔除无用代码,减少体积。 -
常用平台标识:
-
APP-PLUS:App端;APP-IOS:iOS端;APP-ANDROID:Android端; -
MP-WEIXIN:微信小程序;MP-ALIPAY:支付宝小程序;H5:H5端; -
MP:所有小程序平台;PLUS:所有App平台。
-
-
示例:
<!-- 模板中区分H5和微信小程序 --> <view> #ifdef H5 <button @click="openH5Pay">H5支付</button> #endif #ifdef MP-WEIXIN <button @click="openWxPay">微信支付</button> #endif </view> <script> export default { methods: { getDeviceInfo() { // 脚本中区分iOS和Android #ifdef APP-IOS uni.showToast({title: 'iOS设备'}); #endif #ifdef APP-ANDROID uni.showToast({title: 'Android设备'}); #endif } } } </script> <style> /* 样式中区分App和H5 */ #ifdef APP-PLUS .container {padding: 20px;} #endif #ifdef H5 .container {padding: 10px;} #endif </style>
6. UniApp中全局样式和页面样式的优先级是怎样的?如何修改原生组件的样式(如导航栏、tabbar)?
答案:
-
样式优先级:页面内局部样式 >
App.vue全局样式 >uni.scss全局变量样式;行内样式 > 带!important的样式 > 普通样式;小程序端不支持*选择器,且原生组件(如picker)样式需用组件自身属性或深度选择器修改。 -
修改原生组件样式:① 导航栏:在
pages.json中配置(全局/页面级):// 全局导航栏 { "globalStyle": { "navigationBarTitleText": "标题", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "backgroundColor": "#f5f5f5" }, // 页面级导航栏(覆盖全局) "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页", "enablePullDownRefresh": true } } ] }② tabbar:在
pages.json中配置:"tabBar": { "color": "#666666", // 未选中颜色 "selectedColor": "#ff5722", // 选中颜色 "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tab/home.png", "selectedIconPath": "static/tab/home-active.png" }, { "pagePath": "pages/my/my", "text": "我的", "iconPath": "static/tab/my.png", "selectedIconPath": "static/tab/my-active.png" } ] }③ 原生组件(如
input/picker):通过组件属性(如style/class)或深度选择器(::v-deep/>>>)修改:::v-deep .uni-input-input { font-size: 14px; color: #333; }
7. UniApp与Vue的语法有哪些差异?(如指令、生命周期、API调用)
答案:
-
指令差异:
-
UniApp新增
@tap(替代@click,兼容各端点击事件,小程序无click事件); -
不支持Vue的
v-html(小程序限制),需用rich-text组件渲染富文本; -
v-for必须加key(强制要求,否则报错),且不支持v-for与v-if同级使用。
-
-
生命周期差异:
-
UniApp新增应用生命周期(
onLaunch/onShow/onHide)和页面生命周期(onLoad/onShow/onReady/onPullDownRefresh); -
Vue的
created/mounted在UniApp中仍可用,但页面渲染完成建议用onReady(小程序端mounted可能早于页面渲染)。
-
-
API差异:
-
路由跳转:UniApp用
uni.navigateTo/uni.switchTab等,替代Vue的$router.push; -
数据请求:UniApp用
uni.request,替代axios/fetch(H5端可兼容axios,但需处理跨域); -
本地存储:UniApp用
uni.setStorage/uni.getStorage,替代localStorage(小程序无localStorage)。
-
二、生命周期类
1. UniApp的生命周期分为哪几类?(应用生命周期、页面生命周期、组件生命周期)
答案:
UniApp的生命周期分为三类:
-
应用生命周期:在
App.vue中定义,对应应用的启动、显示、隐藏等全局状态,常用钩子:-
onLaunch:应用初始化完成(全局只触发一次); -
onShow:应用前台显示(如从后台切回、首次启动); -
onHide:应用后台隐藏(如切到桌面); -
onError:应用报错时触发。
-
-
页面生命周期:在页面.vue文件中定义,对应页面的加载、显示、卸载等,常用钩子:
-
onLoad:页面加载(接收跳转参数,仅触发一次); -
onShow:页面显示(每次打开页面都触发); -
onReady:页面初次渲染完成(可操作DOM/原生组件); -
onHide:页面隐藏(如跳转到其他页面); -
onUnload:页面卸载(如返回上一页); -
onPullDownRefresh:下拉刷新触发; -
onReachBottom:上拉触底触发。
-
-
组件生命周期:与Vue组件生命周期一致(Vue2/Vue3略有差异),常用钩子:
-
Vue2:
beforeCreate→created→beforeMount→mounted→beforeUpdate→updated→beforeDestroy→destroyed; -
Vue3:
setup→onBeforeMount→onMounted→onBeforeUpdate→onUpdated→onBeforeUnmount→onUnmounted; -
UniApp新增
onInit(组件初始化,仅Vue3支持,比created更早)。
-
2. UniApp的应用生命周期(如onLaunch/onShow/onHide)与页面生命周期(如onLoad/onShow/onReady)的执行顺序是怎样的?
答案:
-
应用首次启动:
App.vue onLaunch→ 首页onLoad→ 首页onShow→ 首页onReady→App.vue onShow -
应用切换后台/前台:
-
前台→后台:当前页面
onHide→App.vue onHide; -
后台→前台:
App.vue onShow→ 当前页面onShow;
-
-
页面跳转(A→B):A页面
onHide→ B页面onLoad→ B页面onShow→ B页面onReady; -
页面返回(B→A):B页面
onUnload→ A页面onShow。
3. UniApp的页面生命周期与Vue的生命周期(created/mounted)有什么关联?执行时机有何不同?
答案:
-
关联:UniApp页面生命周期基于Vue生命周期扩展,执行顺序为:
onLoad→beforeCreate→created→onShow→beforeMount→mounted→onReady -
执行时机差异:
-
onLoad:页面加载时触发,可接收跳转参数(如options.id),仅执行一次; -
created:Vue实例创建完成(未挂载DOM),此时可访问数据,但无法操作DOM; -
mounted:Vue实例挂载完成(DOM已生成),但小程序端可能存在“虚拟DOM已挂载,原生组件未渲染完成”的情况; -
onReady:页面原生渲染完成(小程序端对应Page.onReady),此时操作原生组件(如地图、视频)更安全,比mounted更晚触发。
-
4. 小程序端和App端的UniApp生命周期有哪些差异?(如小程序的onUnload vs App的onBackPress)
答案:
-
页面卸载:
-
小程序端:页面返回时触发
onUnload(页面实例销毁); -
App端:页面返回时先触发
onHide,只有关闭应用或跳转到非栈内页面才触发onUnload。
-
-
返回键监听:
-
小程序端:无物理返回键,无法监听返回事件;
-
App端:支持
onBackPress钩子(监听物理返回键/导航栏返回按钮),返回true可阻止默认返回:onBackPress(options) { // options.from:返回来源('backbutton'=物理键,'navigateBack'=API调用) if (this.isEditing) { uni.showModal({ title: '提示', content: '是否放弃编辑?', success: (res) => { if (res.confirm) uni.navigateBack(); } }); return true; // 阻止默认返回 } return false; // 允许返回 }
-
-
应用退出:
-
小程序端:无
onExit钩子,直接退出; -
App端:可通过
plus.runtime.quit()主动退出,或监听onUniNViewMessage处理退出逻辑。
-
5. 自定义组件的生命周期与页面生命周期的区别是什么?组件中如何监听页面的生命周期?
答案:
-
区别:
-
页面生命周期包含UniApp特有钩子(如
onLoad/onShow),组件生命周期与Vue一致(无页面钩子); -
页面生命周期由框架直接触发,组件生命周期依赖页面(页面卸载时组件触发
destroyed/onUnmounted)。
-
-
组件监听页面生命周期:
① 通过
uni.$on/uni.$emit全局事件:// 页面中触发 onShow() { uni.$emit('pageShow', '首页显示'); } // 组件中监听 mounted() { uni.$on('pageShow', (data) => { console.log(data); // 首页显示 }); }, unmounted() { uni.$off('pageShow'); // 解绑事件,避免内存泄漏 }② 通过
@hook:生命周期监听:// 组件中监听页面onShow mounted() { this.$parent.$on('hook:onShow', () => { console.log('页面显示'); }); }③ 使用
uni.requireNativePlugin('uni-page-lifecycle')(原生插件,仅App端支持)。
6. 页面跳转时(如navigateTo),原页面和新页面的生命周期执行顺序是怎样的?
答案:
以页面A通过uni.navigateTo跳转到页面B为例:
-
页面A触发
onHide(页面隐藏); -
页面B触发
onLoad(页面加载,接收参数); -
页面B触发
onShow(页面显示); -
页面B触发
onReady(页面渲染完成);
若从页面B返回页面A:
-
页面B触发
onUnload(页面卸载); -
页面A触发
onShow(页面重新显示)。
若使用uni.redirectTo(关闭当前页跳转):
页面A触发onUnload → 页面B触发onLoad→onShow→onReady。
7. onPullDownRefresh和onReachBottom的触发条件是什么?如何配置?
答案:
-
onPullDownRefresh(下拉刷新):-
触发条件:页面下拉时触发(需先开启下拉刷新配置);
-
配置方式:
① 全局开启:
pages.json的globalStyle中设置"enablePullDownRefresh": true;② 页面级开启:页面
style中设置"enablePullDownRefresh": true; -
使用示例:
onPullDownRefresh() { // 刷新数据 this.loadData().then(() => { uni.stopPullDownRefresh(); // 停止下拉刷新动画 }); }
-
-
onReachBottom(上拉触底):-
触发条件:页面滚动到底部时触发(可配置触底距离);
-
配置方式:
① 全局配置:
pages.json的globalStyle中设置"onReachBottomDistance": 50(触底距离,单位px);② 页面级配置:页面
style中设置"onReachBottomDistance": 50; -
使用示例:
onReachBottom() { if (!this.isLoading && this.hasMore) { this.pageNum++; this.loadMoreData(); // 加载下一页 } }
-
三、跨端实现与原理类
1. UniApp的跨端原理是什么?(编译器层面:将Vue代码编译为各端原生代码;运行时层面:统一的JS引擎和渲染层)
答案:
UniApp的跨端核心是**“编译器+运行时框架”**双层架构:
-
编译器层面:将Vue代码编译为各端原生代码/配置:
① 小程序端:将
.vue文件拆分为wxml(模板)、wxss(样式)、js(逻辑),生成小程序配置文件(app.json/page.json);② App端:Vue代码编译为JS,结合nvue(原生Vue)编译为Android的Java/iOS的OC代码;
③ H5端:编译为标准HTML/CSS/JS(类似Vue CLI打包)。
-
运行时层面:提供统一的运行时框架,屏蔽各端差异:
① 统一API层:
uniAPI(如uni.request/uni.navigateTo)封装各端原生API,调用时自动适配(如uni.request在H5端用fetch,小程序端用wx.request);② 统一渲染层:App端支持nvue(基于weex,原生渲染)和vue(webview渲染),小程序/H5端用各端原生渲染引擎;
③ 统一JS引擎:App端用V8/JSCore,小程序端用各平台内置JS引擎,保证逻辑层一致。
2. UniApp的“一套代码多端运行”是如何实现的?与Taro的编译原理有什么不同?
答案:
-
UniApp实现“一套代码多端运行”的核心:
① 语法抽象:基于Vue语法,抽象出跨端通用的组件(如
<view>/<text>)和API(uni前缀),编译时映射为各端原生组件/API;② 条件编译:通过
#ifdef等语法剔除非目标平台代码,保证打包产物纯净;③ 运行时适配:运行时框架根据当前平台自动切换API实现,无需开发者关注底层差异。
-
与Taro的编译原理差异:
-
语法基础:Taro基于React/JSX语法,UniApp基于Vue/SFC(单文件组件)语法;
-
编译方式:Taro是“转译式”(将JSX转译为各端代码,如微信小程序的
wxml);UniApp是“编译+适配”(Vue代码直接编译为各端代码,结合运行时适配); -
小程序支持:UniApp对小程序生态支持更深度(如兼容小程序原生组件/插件),Taro需额外适配;
-
App端性能:UniApp支持nvue原生渲染,Taro App端基于React Native(桥接渲染,性能略低)。
-
3. UniApp中“条件编译”和“运行时判断”的区别是什么?分别适用于什么场景?
答案:
| 特性 | 条件编译 | 运行时判断 |
|---|---|---|
| 执行阶段 | 编译阶段(打包时) | 运行阶段(代码执行时) |
| 代码体积 | 剔除无用代码,体积更小 | 保留所有平台代码,体积更大 |
| 灵活性 | 低(需提前确定平台) | 高(可动态判断) |
| 实现方式 | #ifdef/#ifndef+平台标识 | uni.getSystemInfoSync()判断 |
-
条件编译适用场景:差异化功能模块(如H5端的支付逻辑、小程序端的分享逻辑)、原生组件替换(如App端用nvue组件,H5端用web组件);
-
运行时判断适用场景:小范围逻辑差异(如提示语、样式微调)、需动态获取平台信息的场景(如根据设备类型调整布局)。
-
示例对比:
// 条件编译(编译时剔除其他平台代码) #ifdef MP-WEIXIN uni.showMenuButton(); #endif // 运行时判断(保留所有代码,运行时分支执行) const platform = uni.getSystemInfoSync().platform; if (platform === 'weixin') { uni.showMenuButton(); }
4. UniApp的渲染层和逻辑层是分离的吗?(如小程序的双线程模型)这种架构带来的优缺点是什么?
答案:
-
渲染层与逻辑层分离情况:
-
小程序端:采用双线程模型(逻辑层JS引擎+渲染层WebView/原生组件),渲染层与逻辑层分离,通过数据通信同步状态;
-
App端(vue页面):基于webview,渲染层与逻辑层在同一线程;App端(nvue页面):原生渲染,逻辑层与渲染层分离;
-
H5端:渲染层与逻辑层不分离(同标准web)。
-
-
分离架构的优点:
① 安全性高:逻辑层运行在独立线程,避免渲染层DOM操作影响逻辑层;
② 稳定性强:渲染层崩溃不影响逻辑层;
③ 小程序端:可限制逻辑层性能消耗,避免卡顿。
-
分离架构的缺点:
① 通信开销:渲染层与逻辑层需通过数据传递同步状态(如小程序的
setData),频繁通信会导致性能损耗;② 开发限制:逻辑层无法直接操作DOM(需通过数据驱动),原生组件交互需通过桥接;
③ 调试复杂:双线程模型增加调试难度(如断点需分别调试逻辑层和渲染层)。
5. UniApp如何调用原生SDK(如iOS的高德地图SDK、Android的支付SDK)?
答案:
UniApp调用原生SDK有三种方式:
-
方式1:使用插件市场的原生插件直接安装插件市场中已封装好的SDK插件(如高德地图、支付宝支付),通过
uni.requireNativePlugin调用:const amap = uni.requireNativePlugin('AMap-Location'); amap.getLocation((res) => { console.log(res.latitude, res.longitude); }); -
方式2:开发自定义原生插件若需自定义SDK调用,需开发原生插件:
① Android端:用Java/Kotlin编写插件,注册到UniApp插件框架;
② iOS端:用OC/Swift编写插件,集成SDK并暴露API;
③ 插件打包后导入UniApp项目,通过
uni.requireNativePlugin调用。 -
方式3:App端通过
plus** API调用原生能力**App端可直接使用plusAPI调用部分原生SDK(如定位、文件操作):// App端调用原生定位 plus.geolocation.getCurrentPosition((res) => { console.log(res.coords.latitude); });
6. UniApp的原生插件(Native Plugin)开发流程是怎样的?如何在项目中集成?
答案:
-
原生插件开发流程(以Android为例):
① 创建Android Library项目,集成目标SDK(如高德地图);
② 实现
UniPluginModule接口,暴露JS调用的方法(用@UniJSMethod注解):public class MyPlugin extends UniPluginModule { @UniJSMethod(uiThread = true) public void getSDKVersion(JSCallback callback) { String version = "1.0.0"; // SDK版本 callback.invoke(version); } }③ 配置插件清单(
plugin.json),声明插件名称、类名、方法;④ 打包为aar文件,iOS端打包为.framework文件。 -
项目集成原生插件:
① 将插件文件放入项目
nativeplugins目录;② 在
manifest.json中配置插件:"app-plus": { "nativePlugins": { "MyPlugin": { "plugins": [ { "type": "module", "name": "MyPlugin", "class": "com.example.myplugin.MyPlugin" } ] } } }③ 在代码中调用:
const myPlugin = uni.requireNativePlugin('MyPlugin'); myPlugin.getSDKVersion((res) => { console.log(res); });
7. UniApp中nvue(native vue)与普通vue页面的区别是什么?适用场景有哪些?
答案:
- nvue与普通vue页面的核心区别:
| 特性 | nvue页面 | 普通vue页面 |
|---|---|---|
| 渲染方式 | 原生渲染(Android/iOS原生组件) | WebView渲染(HTML/CSS) |
| 性能 | 高(接近原生App) | 中(依赖WebView性能) |
| 样式支持 | 仅支持flex布局,样式限制多 | 支持标准CSS(Flex/Grid等) |
| 组件支持 | 仅支持nvue内置组件 | 支持所有UniApp组件+Web组件 |
| 适用端 | 仅App端 | 所有端(App/小程序/H5) |
-
nvue适用场景:
① App端高性能页面(如长列表、动画密集的页面);
② 需要与原生组件深度交互的页面(如地图、视频播放);
③ 对流畅度要求高的页面(如电商商品详情、直播页面)。
-
普通vue页面适用场景:
① 多端兼容需求(需同时支持小程序/H5);
② 样式复杂的页面(如富文本、自定义布局);
③ 快速开发的页面(无需关注原生渲染细节)。
四、组件与API类
1. UniApp的组件分为哪几类?(内置组件、自定义组件、原生组件)请举例说明常用内置组件的差异(如view vs div、text vs span)。
答案:
-
组件分类:
① 内置组件:UniApp提供的跨端通用组件(如
<view>/<text>/<button>/<image>);② 自定义组件:开发者编写的可复用组件(如
<my-card>/<uni-button>);③ 原生组件:各端原生组件(如小程序的
<canvas>/<video>,App端的<map>)。 -
常用内置组件差异:
-
<view>vs<div>:<view>是UniApp的块级容器组件,对应H5的<div>、小程序的<view>,支持Flex布局,跨端兼容;<div>仅H5端支持,小程序/App端不识别。 -
<text>vs<span>:<text>是行内文本组件,对应H5的<span>,仅支持文本渲染(小程序限制),可通过selectable属性开启文本选择;<span>仅H5端支持。 -
<image>vs<img>:<image>支持跨端图片渲染,内置懒加载(lazy-load)、图片裁剪(mode属性,如widthFix/aspectFill);<img>仅H5端支持,无跨端适配能力。 -
<button>vs HTML<button>:UniApp的<button>支持跨端样式适配(如小程序的open-type属性),HTML<button>仅H5端支持。
-
2. 如何开发UniApp的自定义组件?全局注册和局部注册的区别是什么?组件间如何通信?(props/ on/$emit`)
答案:
-
开发自定义组件步骤:
① 在
components目录下创建组件目录(如my-card),新建my-card.vue文件;② 编写组件逻辑(模板/样式/脚本):
<template> <view class="my-card">{{ title }}</view> </template> <script> export default { props: { title: { type: String, default: '默认标题' } } } </script> <style scoped> .my-card {padding: 10px; border: 1px solid #eee;} </style>③ 注册并使用组件(通过
easycom自动注册,或手动注册)。 -
全局注册vs局部注册:
-
全局注册:在
main.js中通过app.component('my-card', MyCard)注册,所有页面/组件可直接使用,适合高频组件; -
局部注册:在页面/组件的
components选项中注册,仅当前页面/组件可用,适合低频组件(减少全局内存占用)。
-
-
组件间通信方式:
①
props/$emit:父传子用props,子传父用$emit:// 父组件 <my-card :title="parentTitle" @change="handleChange"></my-card> // 子组件 this.$emit('change', '新标题');② 全局事件总线(
uni.$on/$emit):跨组件/页面通信:// 发送事件 uni.$emit('globalEvent', '数据'); // 接收事件 uni.$on('globalEvent', (data) => {console.log(data);});③ 状态管理(Vuex/Pinia):全局数据共享(如用户信息、购物车);
④
$parent/$children:直接访问父子组件实例(不推荐,耦合度高);⑤
provide/inject:跨层级组件通信(父组件provide提供数据,子组件inject注入数据)。
3. UniApp中slot(插槽)的使用与Vue有差异吗?如何实现作用域插槽?
答案:
-
slot使用差异:UniApp的slot基本兼容Vue语法,但小程序端有以下限制:① 不支持
slot的动态名称(如slot="dynamicName");、② 小程序端
slot嵌套深度有限(建议不超过3层);③ 作用域插槽在小程序端需用
template包裹。 -
作用域插槽实现:子组件通过
slotProps传递数据,父组件通过v-slot接收:<!-- 子组件(my-list.vue) --> <template> <view> <slot v-for="item in list" :item="item" :index="index"></slot> </view> </template> <script> export default { data() { return { list: ['A', 'B', 'C'] }; } } </script> <!-- 父组件使用 --> <template> <my-list> <template v-slot="slotProps"> <view>{{ slotProps.index }}-{{ slotProps.item }}</view> </template> </my-list> </template>(注:小程序端需用
template包裹作用域插槽内容,H5端可直接使用)
4. UniApp的API与微信小程序的API有什么关系?如何调用各端的原生API(如小程序的wx.request、App的plus API)?
答案:
-
UniApp API与微信小程序API的关系:UniApp的
uniAPI大量借鉴微信小程序API的设计(如uni.navigateTo对应wx.navigateTo,uni.request对应wx.request),但做了跨端适配(支持多平台);微信小程序API仅适用于微信小程序端,uniAPI适用于所有端。 -
调用各端原生API的方式:
① 通过
uni** API调用**(推荐,跨端兼容):uni.request({ url: 'https://api.example.com', method: 'GET', success: (res) => {console.log(res.data);} });② 条件编译调用原生API(仅特定端可用):
// 微信小程序端调用wx API #ifdef MP-WEIXIN wx.getStorage({key: 'token', success: (res) => {}}); #endif // App端调用plus API #ifdef APP-PLUS plus.screen.lockOrientation('portrait-primary'); #endif③ 通过原生插件调用:复杂原生API(如高德地图SDK)需通过原生插件封装后调用。
5. UniApp中网络请求的方式有哪些?(uni.request、axios适配、封装请求拦截器)如何处理跨域问题(H5端)?
答案:
-
网络请求方式:①
uni.request(官方推荐,跨端兼容):uni.request({ url: 'https://api.example.com/user', method: 'POST', data: {id: 1}, header: {'Content-Type': 'application/json'}, success: (res) => {console.log(res.data);}, fail: (err) => {console.error(err);} });② 封装请求拦截器(统一处理token、错误):
// utils/request.js export default function request(options) { // 请求拦截:添加token options.header = options.header || {}; const token = uni.getStorageSync('token'); if (token) { options.header.Authorization = `Bearer ${token}`; } // 返回Promise return new Promise((resolve, reject) => { uni.request({ ...options, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else { reject(res); } }, fail: (err) => { reject(err); } }); }); }③ Axios适配(仅H5端):需配置跨域代理,不推荐跨端使用。
-
H5端跨域问题处理:① 开发环境:在
manifest.json中配置代理:"h5": { "devServer": { "proxy": { "/api": { "target": "https://api.example.com", "changeOrigin": true, "pathRewrite": {"^/api": ""} } } } }② 生产环境:
-
后端配置CORS(
Access-Control-Allow-Origin: *); -
前端部署在与后端同域名下,或通过Nginx反向代理。
-
6. UniApp中如何实现文件上传/下载?(uni.uploadFile/uni.downloadFile)需要注意哪些问题?
答案:
-
文件上传(
uni.uploadFile):uni.chooseImage({ count: 1, success: (res) => { const tempFilePath = res.tempFilePaths[0]; uni.uploadFile({ url: 'https://api.example.com/upload', // 上传接口 filePath: tempFilePath, name: 'file', // 后端接收文件的字段名 formData: {user: 'test'}, // 额外参数 success: (uploadRes) => { console.log(uploadRes.data); // 上传结果 }, fail: (err) => { console.error(err); } }); } }); -
文件下载(
uni.downloadFile):uni.downloadFile({ url: 'https://example.com/file.pdf', // 下载链接 success: (res) => { if (res.statusCode === 200) { // 保存文件(App端) #ifdef APP-PLUS plus.io.resolveLocalFileSystemURL(res.tempFilePath, (entry) => { entry.copyTo(plus.io.PRIVATE_DOC, 'file.pdf', (newEntry) => { console.log('保存成功:', newEntry.fullPath); }); }); #endif // 打开文件(小程序端) #ifdef MP-WEIXIN uni.openDocument({ filePath: res.tempFilePath, showMenu: true }); #endif } } }); -
注意问题:
① 上传文件的
name字段需与后端一致;② 小程序端需配置upload/download域名白名单(在小程序管理后台);
③ App端需申请文件读写权限(
manifest.json中配置);④ 大文件上传建议分片上传(通过
uni.uploadFile分块发送)。
7. UniApp的本地存储API(uni.setStorage/uni.getStorage)与浏览器的localStorage有什么区别?如何处理大容量数据存储?
答案:
- 核心区别:
| 特性 | uni.setStorage/uni.getStorage | 浏览器localStorage |
|---|---|---|
| 跨端支持 | 支持所有端(App/小程序/H5) | 仅H5端支持 |
| 存储类型 | 支持异步/同步(uni.setStorageSync) | 仅同步 |
| 存储大小 | 小程序端约10MB,App端无限制 | 约5MB |
| 数据格式 | 自动序列化/反序列化(支持对象) | 仅支持字符串 |
-
大容量数据存储方案:
① App端:使用
plus.io操作本地文件(如JSON文件存储):#ifdef APP-PLUS // 写入文件 plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => { fs.root.getFile('bigData.json', {create: true}, (fileEntry) => { fileEntry.createWriter((writer) => { writer.write(JSON.stringify(bigData)); }); }); }); // 读取文件 plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => { fs.root.getFile('bigData.json', {create: false}, (fileEntry) => { fileEntry.file((file) => { const reader = new FileReader(); reader.onload = () => { const data = JSON.parse(reader.result); }; reader.readAsText(file); }); }); }); #endif② 小程序端:使用云开发数据库(如微信云开发)或分块存储(将大对象拆分为多个key存储);
③ H5端:使用
IndexedDB(支持大容量结构化数据存储)。
8. 如何在UniApp中使用地图、支付、分享等原生能力?(如调用微信支付、支付宝支付)
答案:
-
地图能力:
① 使用UniApp内置
<map>组件(跨端兼容):<map :latitude="39.9042" :longitude="116.4074" :markers="markers"></map>② App端使用原生地图SDK(如高德/百度地图):通过插件市场插件或自定义原生插件调用。
-
支付能力:
① 微信小程序支付:
#ifdef MP-WEIXIN uni.request({ url: 'https://api.example.com/getWxPayParams', // 后端获取支付参数 success: (res) => { wx.requestPayment({ ...res.data, success: (payRes) => {console.log('支付成功');}, fail: (err) => {console.error('支付失败');} }); } }); #endif② App端支付宝支付:通过
uni-app支付插件或plus.payment调用:#ifdef APP-PLUS plus.payment.request(channel, payInfo, (res) => { console.log('支付成功'); }, (err) => { console.error('支付失败'); }); #endif -
分享能力:
① 使用
uni.share(跨端兼容):uni.share({ provider: 'weixin', type: 0, title: '分享标题', imageUrl: 'https://example.com/img.png', success: (res) => {console.log('分享成功');} });② 小程序端使用
wx.showShareMenu自定义分享内容:#ifdef MP-WEIXIN wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] });
五、路由与页面跳转类
1. UniApp的路由配置文件是哪个?(pages.json)如何配置tabbar、页面路径、导航栏样式?
答案:
-
路由配置文件:
pages.json是UniApp的路由配置文件,负责页面路径、全局样式、tabbar等配置。 -
配置页面路径:
{ "pages": [ { "path": "pages/index/index", // 页面路径(必填) "style": { // 页面级样式(可选,覆盖全局) "navigationBarTitleText": "首页", "enablePullDownRefresh": true } }, { "path": "pages/detail/detail", "style": { "navigationBarTitleText": "详情页" } } ] } -
配置tabbar:
{ "tabBar": { "color": "#666666", // 未选中文字颜色 "selectedColor": "#ff5722", // 选中文字颜色 "borderStyle": "black", // 边框样式 "backgroundColor": "#ffffff", // 背景色 "list": [ { "pagePath": "pages/index/index", // tab页路径(必须在pages数组中) "text": "首页", // tab文字 "iconPath": "static/tab/home.png", // 未选中图标 "selectedIconPath": "static/tab/home-active.png" // 选中图标 }, { "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "static/tab/cart.png", "selectedIconPath": "static/tab/cart-active.png" }, { "pagePath": "pages/my/my", "text": "我的", "iconPath": "static/tab/my.png", "selectedIconPath": "static/tab/my-active.png" } ] } } -
配置导航栏样式:
{ "globalStyle": { "navigationBarBackgroundColor": "#ffffff", // 导航栏背景色 "navigationBarTextStyle": "black", // 文字颜色(black/white) "navigationBarTitleText": "UniApp", // 全局标题 "backgroundColor": "#f5f5f5", // 页面背景色 "app-plus": { "titleNView": { // App端原生导航栏配置 "backgroundColor": "#ffffff", "titleText": "UniApp", "titleColor": "#333333" } } } }
2. UniApp的页面跳转方式有哪些?(uni.navigateTo/uni.redirectTo/uni.switchTab/uni.reLaunch/uni.navigateBack)各自的使用场景和区别是什么?
答案:
UniApp提供5种页面跳转方式,核心区别在于是否保留当前页面和是否跳转到tabbar页:
| 跳转方式 | 保留当前页 | 关闭当前页 | 支持tabbar页 | 适用场景 |
|---|---|---|---|---|
uni.navigateTo | ✅ | ❌ | ❌ | 普通页面跳转(如列表→详情) |
uni.redirectTo | ❌ | ✅ | ❌ | 替换当前页(如登录→首页) |
uni.switchTab | ❌ | ✅ | ✅ | 跳转到tabbar页(如首页→我的) |
uni.reLaunch | ❌ | ✅(所有页) | ✅ | 重启应用(如退出登录→首页) |
uni.navigateBack | - | ✅(当前页) | ❌ | 返回上一页(如详情→列表) |
-
示例:
// 保留当前页,跳转到详情页(带参数) uni.navigateTo({ url: '/pages/detail/detail?id=1&name=test' }); // 关闭当前页,跳转到首页 uni.redirectTo({ url: '/pages/index/index' }); // 跳转到tabbar的“我的”页面 uni.switchTab({ url: '/pages/my/my' }); // 关闭所有页,跳转到首页 uni.reLaunch({ url: '/pages/index/index' }); // 返回上一页(delta=2返回上两页) uni.navigateBack({ delta: 1 });
3. 页面跳转时如何传递参数?接收参数的方式有哪些?如果参数过大(如对象/数组),如何处理?
答案:
-
传递参数的方式:
① URL拼接(适用于小参数,如字符串/数字):
uni.navigateTo({ url: `/pages/detail/detail?id=1&status=active&list=${JSON.stringify([1,2,3])}` });② 全局状态管理(Vuex/Pinia,适用于大参数):
// 存储参数 store.commit('setDetailData', bigData); // 跳转 uni.navigateTo({url: '/pages/detail/detail'});③ 本地存储(
uni.setStorageSync,适用于超大参数):// 存储参数 uni.setStorageSync('detailData', bigData); // 跳转 uni.navigateTo({url: '/pages/detail/detail'}); -
接收参数的方式:
① URL参数:在目标页
onLoad中接收:onLoad(options) { const id = options.id; // 1 const status = options.status; // 'active' const list = JSON.parse(options.list); // [1,2,3] }② 全局状态管理:在目标页直接获取:
onLoad() { const bigData = store.state.detailData; }③ 本地存储:在目标页读取后删除(避免残留):
onLoad() { const bigData = uni.getStorageSync('detailData'); uni.removeStorageSync('detailData'); // 读取后删除 } -
大参数处理建议:优先使用Vuex/Pinia(内存存储,效率高),超大参数(如图片二进制数据)使用本地文件存储(App端)或云存储(小程序端)。
4. 如何实现页面返回时刷新数据?(如通过onShow、事件总线、Vuex/Pinia)
答案:
-
方式1:利用
onShow生命周期(简单场景):列表页在onShow中重新加载数据(返回时自动触发):// 列表页(pages/list/list.vue) onShow() { this.loadListData(); // 返回时重新加载列表 } -
方式2:事件总线(
uni.$on/$emit)(精准刷新):// 列表页(监听事件) onLoad() { uni.$on('refreshList', () => { this.loadListData(); }); }, onUnload() { uni.$off('refreshList'); // 解绑事件 } // 详情页(触发事件) onUnload() { uni.$emit('refreshList'); // 返回前触发刷新 } -
方式3:Vuex/Pinia状态同步(复杂场景):详情页修改数据后更新状态,列表页监听状态变化:
// 详情页 store.commit('updateItem', newData); // 列表页 watch: { 'store.state.listData': { handler() { this.loadListData(); }, deep: true } }
5. Tabbar页面和非Tabbar页面的跳转限制是什么?(如navigateTo无法跳转到tabbar页面)
答案:
-
核心限制:
①
uni.navigateTo/uni.redirectTo无法跳转到tabbar页面(会报错);② 跳转到tabbar页面必须使用
uni.switchTab(会关闭所有非tabbar页面);③ tabbar页面之间跳转只能用
uni.switchTab(不能用navigateTo)。 -
示例:
// 错误:navigateTo跳转到tabbar页 uni.navigateTo({url: '/pages/my/my'}); // 小程序端报错,App端无响应 // 正确:switchTab跳转到tabbar页 uni.switchTab({url: '/pages/my/my'}); -
特殊需求处理(保留当前页跳转到tabbar页):先通过
uni.navigateTo跳转到非tabbar的“我的”页面,再在该页面用uni.switchTab跳转到tabbar页(不推荐,易导致页面栈混乱)。
6. UniApp中路由拦截如何实现?(如登录验证、权限控制)
答案:
UniApp通过uni.addInterceptor实现路由拦截(支持navigateTo/redirectTo/switchTab等所有跳转方式):
-
登录验证拦截示例:
// main.js uni.addInterceptor('navigateTo', { // 跳转前拦截 invoke(options) { // 需要登录的页面路径(如/pay、/my/order) const needLoginPages = ['/pages/pay/pay', '/pages/my/order/order']; const isNeedLogin = needLoginPages.some(page => options.url.includes(page)); if (isNeedLogin) { const token = uni.getStorageSync('token'); if (!token) { // 未登录,跳转到登录页 uni.navigateTo({url: '/pages/login/login'}); // 阻止原跳转 return false; } } }, // 跳转后回调 success(options) { console.log('跳转成功:', options.url); } }); // 拦截switchTab(如跳转到“我的”页需登录) uni.addInterceptor('switchTab', { invoke(options) { if (options.url.includes('/pages/my/my')) { const token = uni.getStorageSync('token'); if (!token) { uni.navigateTo({url: '/pages/login/login'}); return false; } } } }); -
权限控制拦截示例:
uni.addInterceptor('navigateTo', { invoke(options) { if (options.url.includes('/admin')) { const userRole = uni.getStorageSync('userRole'); if (userRole !== 'admin') { uni.showToast({title: '无权限访问', icon: 'none'}); return false; } } } });
六、性能优化类
1. UniApp项目的性能优化有哪些常用方法?(分包加载、图片优化、数据懒加载、组件复用)
答案:
UniApp性能优化需分端针对性处理,核心方法如下:
-
分包加载:小程序端主包≤2M,将非核心页拆分为分包,按需加载(减少首屏加载时间);
-
图片优化:
① 使用
image组件的lazy-load属性(懒加载);② 采用webp格式(体积比jpg小30%);
③ 小程序端使用CDN加速,App端使用本地图片(常用图标);
-
列表优化:
① 长列表用
recycle-view(回收复用组件,减少DOM节点);② 分页加载(
onReachBottom加载下一页),避免一次性渲染大量数据;③
v-for加key(避免DOM复用错误),且不与v-if同级使用; -
组件优化:
① 高频组件全局注册(减少重复初始化);
② 复杂组件按需加载(
import()动态导入); -
数据优化:
① 减少
setData次数(小程序端),合并数据更新;② 避免在
onShow中执行复杂计算(影响页面切换流畅度); -
启动优化:
①
App.vue的onLaunch中延迟加载非必要数据(如统计、广告);② App端使用启动图(
splashscreen)掩盖加载过程; -
样式优化:
① 减少样式层级(小程序端样式解析效率低);
② 避免使用
!important(增加样式计算开销)。
2. 什么是UniApp的分包加载?如何配置主包和分包?分包的大小限制是什么(如微信小程序主包≤2M)?
答案:
-
分包加载:将小程序代码拆分为主包(包含首页、tabbar页、公共代码)和分包(其他页面),用户打开分包页面时才下载分包,减少首屏加载时间,突破小程序体积限制。
-
配置方式(
pages.json):{ "pages": [ // 主包页面(首页、tabbar页) "pages/index/index", "pages/my/my" ], "subPackages": [ { "root": "pages/sub1", // 分包1根目录 "pages": [ "detail/detail", // 分包页面路径:pages/sub1/detail/detail "list/list" ] }, { "root": "pages/sub2", // 分包2根目录 "pages": [ "pay/pay", "order/order" ] } ], "preloadRule": { // 预加载规则:进入首页时预加载sub1分包 "pages/index/index": { "network": "all", // 网络类型(all/wifi) "packages": ["pages/sub1"] } } } -
分包大小限制:
-
微信小程序:主包≤2M,所有包合计≤20M;
-
支付宝小程序:主包≤2M,所有包合计≤16M;
-
字节跳动小程序:主包≤2M,所有包合计≤12M。
-
3. 如何优化UniApp的启动速度?(如减少启动页耗时、预加载数据、优化首屏渲染)
答案:
-
减少启动页耗时:
① 简化
App.vue的onLaunch逻辑(避免同步请求、复杂计算);② App端配置启动图(
manifest.json),掩盖加载过程:"app-plus": { "splashscreen": { "alwaysShowBeforeRender": true, "waiting": true, "autoclose": false, // 手动关闭启动图 "bgColor": "#ffffff", "image": "static/splash.png" } }加载完成后手动关闭:
// App.vue onLaunch #ifdef APP-PLUS setTimeout(() => { plus.navigator.closeSplashscreen(); }, 1000); #endif -
预加载数据:
① 首屏数据提前请求(如在
onLaunch中请求首页数据,存储到Vuex);② 分包预加载(
preloadRule),提前下载常用分包。 -
优化首屏渲染:
① 首屏只渲染核心内容(如骨架屏),非核心内容延迟渲染;
② 减少首屏组件数量(隐藏非必要组件);
③ 小程序端开启“按需注入”(微信开发者工具→详情→本地设置)。
4. 列表渲染时(如v-for)如何优化性能?(如加key、虚拟列表、分段加载)
答案:
-
基础优化:
①
v-for必须加唯一key(避免DOM复用错误,推荐用id,不用index):<view v-for="item in list" :key="item.id">{{ item.name }}</view>② 避免
v-for与v-if同级使用(会遍历所有元素后再判断,效率低):<!-- 错误 --> <view v-for="item in list" :key="item.id" v-if="item.show">...</view> <!-- 正确 --> <view v-for="item in filteredList" :key="item.id">...</view> -
分段加载:通过
onReachBottom实现分页加载,每次加载10-20条数据:data() { return { list: [], pageNum: 1, pageSize: 10, hasMore: true }; }, onLoad() { this.loadData(); }, onReachBottom() { if (this.hasMore) { this.pageNum++; this.loadData(); } }, methods: { async loadData() { const res = await request({ url: '/api/list', data: {pageNum: this.pageNum, pageSize: this.pageSize} }); if (res.data.length < this.pageSize) { this.hasMore = false; } this.list = [...this.list, ...res.data]; } } -
虚拟列表(长列表优化):使用
recycle-view组件(UniApp官方推荐),仅渲染可视区域内的元素:<recycle-view class="list" :data="list" height="100%"> <view slot-scope="props" class="item"> {{ props.item.name }} </view> </recycle-view>
5. UniApp中图片优化的策略有哪些?(如懒加载、使用webp格式、图片压缩、CDN加速)
答案:
-
懒加载:
image组件添加lazy-load属性(小程序/App端默认支持,H5端需配置):<image :src="imgUrl" lazy-load mode="widthFix"></image> -
格式优化:优先使用webp格式(体积比jpg/png小30%-50%),小程序/App端自动兼容:
<image :src="imgUrl.replace('.jpg', '.webp')" lazy-load></image> -
图片压缩:
① 服务端压缩(如七牛云/阿里云OSS的图片处理接口,指定宽度/质量):
<image src="https://example.com/img.jpg?imageView2/1/w/300/h/200/q/80"></image>② 前端压缩(上传时压缩图片):
uni.chooseImage({ count: 1, sizeType: ['compressed'], // 压缩图片 success: (res) => { /* 上传 */ } }); -
CDN加速:图片部署到CDN(如阿里云CDN、腾讯云CDN),减少图片加载时间。
-
本地图片缓存:App端使用
plus.cache缓存图片,避免重复下载:#ifdef APP-PLUS plus.cache.saveFile(imgUrl, (res) => { this.localImgUrl = res.target; // 本地缓存路径 }); #endif
6. 如何减少UniApp的内存占用?(如及时销毁定时器、取消事件监听、避免大数组渲染)
答案:
-
及时销毁定时器/订阅:在页面
onUnload或组件unmounted中清除定时器、解绑事件:onLoad() { this.timer = setInterval(() => { /* 逻辑 */ }, 1000); uni.$on('event', this.handleEvent); }, onUnload() { clearInterval(this.timer); // 销毁定时器 uni.$off('event', this.handleEvent); // 解绑事件 } -
避免大数组渲染:
① 分页加载数据(如每次加载20条);
② 长列表使用虚拟列表(仅渲染可视区域);
③ 不用的数据及时置为
null(释放内存):onUnload() { this.list = null; // 清空大数组 } -
优化图片内存:
① 小程序端避免同时渲染大量图片(限制≤50张);
② App端使用
image组件的resize属性压缩图片尺寸。 -
减少组件嵌套:组件嵌套深度≤5层(减少虚拟DOM树复杂度,降低内存占用)。
7. H5端的性能优化与小程序/App端的优化有什么差异?
答案:
H5端、小程序端、App端的运行环境不同,性能优化侧重点差异显著:
| 优化维度 | H5端优化重点 | 小程序端优化重点 | App端优化重点 |
|---|---|---|---|
| 渲染层 | 优化DOM操作、CSS渲染、WebView性能 | 减少setData次数、优化WXML渲染 | 选择nvue原生渲染、优化原生组件交互 |
| 资源加载 | 开启HTTP/2、gzip压缩、懒加载资源 | 分包加载、配置CDN域名白名单 |
七、状态管理类
1. UniApp中如何实现全局状态管理?(Vuex/Pinia、全局变量、uni.$emit/$on)
答案:
UniApp支持多种全局状态管理方案,根据复杂度选择:
-
Vuex/Pinia(推荐,复杂应用):
-
Vuex:
① 创建
store/index.js:import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { token: '', userInfo: {} }, mutations: { SET_TOKEN(state, token) { state.token = token; }, SET_USER(state, user) { state.userInfo = user; } }, actions: { login({ commit }, data) { commit('SET_TOKEN', data.token); commit('SET_USER', data.user); } }, getters: { isLogin: state => !!state.token } });② 在
main.js挂载:import store from './store'; const app = new Vue({ store, ...App });③ 组件中使用:
this.$store.commit('SET_TOKEN', 'xxx'); this.$store.dispatch('login', {token: 'xxx', user: {name: 'test'}}); console.log(this.$store.getters.isLogin); -
Pinia(Vue3推荐):
① 创建
stores/user.js:import { defineStore } from 'pinia'; export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: {} }), actions: { login(data) { this.token = data.token; this.userInfo = data.user; } }, getters: { isLogin: state => !!state.token } });② 组件中使用:
import { useUserStore } from '@/stores/user'; const userStore = useUserStore(); userStore.login({token: 'xxx', user: {name: 'test'}}); console.log(userStore.isLogin);
-
-
全局变量(简单场景):在
App.vue中定义全局变量,通过getApp().globalData访问:// App.vue onLaunch() { getApp().globalData = { token: '', userInfo: {} }; } // 组件中使用 const app = getApp(); app.globalData.token = 'xxx'; -
uni.$emit/$on(临时状态传递):适合跨组件临时通信,不适合长期存储状态(页面卸载需解绑)。
2. Vuex/Pinia在UniApp中的使用与在Vue项目中有什么区别?如何处理多端兼容?
答案:
-
使用区别:
① 核心语法一致(Vuex的
state/mutations/actions、Pinia的defineStore),无本质差异;② UniApp中需注意小程序端的异步限制:Vuex的
actions中避免使用过多嵌套异步(可能导致性能问题);③ Pinia在UniApp中需确保Vue版本兼容(Vue3项目直接使用,Vue2需安装
pinia-plugin-persistedstate兼容)。 -
多端兼容处理:
① 状态持久化:
-
Vuex使用
vuex-persistedstate插件,Pinia使用pinia-plugin-persistedstate,将状态保存到uni.setStorage:// Pinia持久化配置 import { createPersistedState } from 'pinia-plugin-persistedstate'; const pinia = createPinia(); pinia.use(createPersistedState({ storage: { getItem: key => uni.getStorageSync(key), setItem: (key, value) => uni.setStorageSync(key, value), removeItem: key => uni.removeStorageSync(key) } }));
② 避免存储非序列化数据(如函数、DOM对象),否则小程序端会报错。
-
3. 如何实现状态的持久化存储?(结合uni.setStorage、Vuex插件)
答案:
-
Vuex持久化:使用
vuex-persistedstate插件,将状态同步到本地存储:// store/index.js import createPersistedState from 'vuex-persistedstate'; const store = new Vuex.Store({ // ...其他配置 plugins: [ createPersistedState({ storage: { getItem: key => uni.getStorageSync(key), setItem: (key, value) => uni.setStorageSync(key, value), removeItem: key => uni.removeStorageSync(key) }, paths: ['token', 'userInfo'] // 只持久化指定状态 }) ] }); -
Pinia持久化:使用
pinia-plugin-persistedstate插件:// main.js(Vue3) import { createPinia } from 'pinia'; import { createPersistedState } from 'pinia-plugin-persistedstate'; const pinia = createPinia(); pinia.use(createPersistedState({ storage: { getItem: (key) => uni.getStorageSync(key), setItem: (key, value) => uni.setStorageSync(key, value), }, })); app.use(pinia); // stores/user.js(直接配置persist) export const useUserStore = defineStore('user', { state: () => ({ token: '' }), persist: true // 自动持久化 }); -
手动持久化:在状态变更时手动调用
uni.setStorage:// Vuex mutation中 SET_TOKEN(state, token) { state.token = token; uni.setStorageSync('token', token); } // 初始化时读取 state: { token: uni.getStorageSync('token') || '' }
4. 全局状态和页面状态的边界是什么?如何避免状态污染?
答案:
-
边界划分:
① 全局状态:跨页面/组件共享的数据(如用户信息、token、全局配置);
② 页面状态:仅当前页面使用的数据(如表单输入、分页参数、临时列表)。
-
避免状态污染:
① 全局状态仅存储必要数据,不存储页面临时数据;
② Vuex/Pinia使用模块化(Vuex的
modules、Pinia的多store):// Vuex模块化 const userModule = { namespaced: true, // 开启命名空间 state: { token: '' }, mutations: { SET_TOKEN(state, token) { state.token = token; } } }; const store = new Vuex.Store({ modules: { user: userModule, cart: cartModule } }); // 调用时指定模块 this.$store.commit('user/SET_TOKEN', 'xxx');③ 页面卸载时清空临时状态:
onUnload() { this.formData = {}; // 清空页面表单数据 }④ 避免直接修改全局状态(通过mutation/actions修改)。
八、UI框架与组件库类
1. UniApp常用的UI组件库有哪些?(uni-ui、uView、ColorUI)各自的特点和适用场景是什么?
答案:
-
uni-ui(DCloud官方):
-
特点:轻量、跨端兼容性最好(适配所有端)、与UniApp深度集成;
-
组件数量:约30+常用组件(日历、表单、列表等);
-
适用场景:追求轻量、跨端一致性的项目。
-
-
uView(uView UI,Vue2/Vue3):
-
特点:组件丰富(100+)、样式美观、文档完善、支持主题定制;
-
组件覆盖:表单、导航、媒体、交互等全品类;
-
适用场景:中大型项目(需丰富组件支持)。
-
-
ColorUI:
-
特点:纯CSS组件库(无JS逻辑)、样式精美、体积小;
-
适用场景:需自定义交互逻辑,仅需UI样式的项目。
-
-
其他库:
-
NutUI(京东):适配小程序,电商场景友好;
-
Vant Weapp(有赞):小程序原生组件库,UniApp可通过条件编译使用。
-
2. 如何在UniApp中引入和使用uView组件库?需要注意哪些配置?
答案:
-
安装uView(Vue3版):
① npm安装:
npm install uview-plus② 配置
main.js:import uviewPlus from 'uview-plus'; app.use(uviewPlus);③ 配置
uni.scss(全局样式):@import 'uview-plus/theme.scss';④ 配置
pages.json(easycom自动引入):{ "easycom": { "^u-(.*)": "uview-plus/components/u-$1/u-$1.vue" } } -
使用示例:
<template> <u-button type="primary" @click="handleClick">按钮</u-button> <u-form :model="form" :rules="rules" ref="formRef"> <u-form-item label="用户名" prop="username"> <u-input v-model="form.username"></u-input> </u-form-item> </u-form> </template> -
注意事项:
① Vue2和Vue3版本需区分(uView 2.x对应Vue2,uView Plus对应Vue3);
② 小程序端需确保组件路径配置正确(easycom规则匹配);
③ 自定义主题需覆盖
uni.scss中的变量(如$u-primary-color: #ff5722;)。
3. 如何自定义UniApp UI组件的样式?(如覆盖uView组件的默认样式、使用深度选择器)
答案:
-
覆盖全局样式:在
uni.scss中修改UI库的全局变量(如uView的主题色):// uView全局变量覆盖 $u-primary-color: #ff5722; // 主色 $u-button-border-radius: 8px; // 按钮圆角 -
局部样式覆盖(深度选择器):小程序/H5端使用
::v-deep(Vue2)或:deep()(Vue3)穿透组件样式:/* Vue3 */ :deep(.u-button__content) { font-size: 16px; } /* Vue2 */ ::v-deep(.u-input__inner) { height: 44px; } -
自定义组件class:多数UI组件支持
custom-class属性,直接添加自定义样式:<u-button custom-class="my-btn">按钮</u-button> <style> .my-btn { background: #007aff; } </style>
4. 如何开发UniApp的自定义组件库?(发布到npm、支持easycom自动引入)
答案:
-
开发步骤:
① 创建组件目录结构:
my-components/ ├── components/ │ ├── my-button/ │ │ ├── my-button.vue │ │ └── index.js │ └── my-card/ ├── index.js └── package.json② 编写组件(
my-button.vue):<template> <button class="my-button" @click="$emit('click')">{{ text }}</button> </template> <script> export default { name: 'MyButton', props: { text: String } } </script>③ 配置
index.js(批量导出):import MyButton from './components/my-button/my-button.vue'; import MyCard from './components/my-card/my-card.vue'; const components = [MyButton, MyCard]; const install = (app) => { components.forEach(component => { app.component(component.name, component); }); }; export default { install }; export { MyButton, MyCard }; -
发布到npm:
① 配置
package.json:{ "name": "my-uniapp-components", "version": "1.0.0", "main": "index.js", "keywords": ["uniapp", "components"] }② 登录npm并发布:
npm login npm publish -
支持easycom自动引入:在用户项目的
pages.json中配置:{ "easycom": { "autoscan": true, "custom": { "^my-(.*)": "my-uniapp-components/components/my-$1/my-$1.vue" } } }
九、调试与发布类
1. UniApp的调试方式有哪些?(HBuilderX调试、小程序开发者工具调试、真机调试)
答案:
-
HBuilderX调试:
① 模拟器调试:HBuilderX内置模拟器(支持App/H5/小程序),点击“运行→运行到模拟器”;
② 控制台调试:HBuilderX底部“控制台”查看日志、网络请求、错误信息;
③ 断点调试:在代码行号处点击添加断点,运行后触发断点可查看变量、调用栈。
-
小程序开发者工具调试:
① 运行到小程序模拟器:HBuilderX中“运行→运行到小程序模拟器→微信开发者工具”;
② 打开微信开发者工具,导入生成的小程序项目,使用其调试工具(Console、Network、WXML);
③ 真机调试:微信开发者工具中“预览→真机调试”,扫码在手机上调试。
-
App真机调试:
① 安卓真机:开启USB调试,连接电脑,HBuilderX中“运行→运行到手机或模拟器→选择设备”;
② iOS真机:需配置开发者证书,通过Xcode或HBuilderX的iOS真机运行功能;
③ 远程调试:App端通过
uni.showDebugger()开启调试面板,或使用vconsole插件(H5/App端):#ifdef H5 import VConsole from 'vconsole'; new VConsole(); #endif
2. 如何在UniApp中调试网络请求?(查看请求参数、响应数据、错误信息)
答案:
-
HBuilderX网络调试:底部“控制台→网络”标签,可查看所有
uni.request/uni.uploadFile的请求URL、参数、响应数据、状态码; -
小程序开发者工具调试:打开“Network”面板,筛选“Request”,查看请求详情(Headers、Response、Preview);
-
封装请求日志:在请求拦截器中打印日志:
function request(options) { console.log('请求参数:', options); return new Promise((resolve, reject) => { uni.request({ ...options, success: (res) => { console.log('响应数据:', res); if (res.statusCode !== 200) { console.error('请求错误:', res); reject(res); } else { resolve(res.data); } }, fail: (err) => { console.error('请求失败:', err); reject(err); } }); }); }
3. UniApp的发布流程是什么?(H5发布、小程序发布、App发布)
答案:
-
H5发布:
① HBuilderX中“发行→网站-H5手机版”;
② 配置发布参数(如域名、标题),选择输出目录,生成静态资源;
③ 将生成的文件上传到服务器(如Nginx、阿里云OSS)。
-
小程序发布:
① HBuilderX中“发行→小程序-微信”(或对应平台);
② 生成小程序代码包,打开微信开发者工具,导入项目;
③ 在微信开发者工具中“上传→填写版本号/描述→上传成功”;
④ 登录微信公众平台,在“版本管理”中提交审核,审核通过后发布。
-
App发布:
① 云打包:HBuilderX中“发行→原生App-云打包”,配置应用名称、图标、权限,选择Android/iOS,生成安装包;
② 本地打包:
-
安卓:生成Android Studio工程,编译为apk/aab包,上传到应用商店(华为、小米、应用宝);
-
iOS:生成Xcode工程,配置证书,打包为ipa文件,上传到App Store Connect审核发布;③ 应用商店审核:需提供应用截图、隐私政策、资质文件(如ICP备案、软著)。
-
4. 小程序发布时需要注意哪些审核规范?(如内容合规、功能合规、隐私政策)
答案:
-
内容合规:
① 不得包含敏感内容(色情、暴力、政治);
② 不得使用夸大/虚假宣传词汇(“最佳”“国家级”);
③ 小程序名称、图标、简介需与内容一致,不得侵权。
-
功能合规:
① 不得包含违规功能(赌博、虚拟货币、医疗美容);
② 不得强制用户分享/关注才能使用功能;
③ 不得诱导用户下载App(小程序内禁止App推广)。
-
隐私政策:
① 收集用户数据(手机号、位置、相册)需提前告知,并提供隐私政策页面;
② 隐私政策需明确数据用途、存储期限、第三方共享情况;
③ 需获得用户授权(如弹窗同意隐私政策)。
-
其他规范:
① 小程序需有实际功能(不得为空壳);
② 性能达标(首屏加载≤5秒,无明显卡顿);
③ 电商类小程序需提供ICP备案、食品经营许可证(如卖食品)。
5. App打包时如何配置权限、图标、启动图?(manifest.json配置)
答案:
-
权限配置:在
manifest.json的“App权限配置”中勾选所需权限(如相机、定位、存储),或手动编辑:"app-plus": { "android": { "permissions": [ "<uses-permission android:name="android.permission.CAMERA"/>", "<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>" ] }, "ios": { "plist": { "NSCameraUsageDescription": "需要相机权限用于拍照", "NSLocationWhenInUseUsageDescription": "需要位置权限用于定位" } } } -
图标配置:在
manifest.json的“App图标配置”中上传图标(或放置到static/logo目录),支持不同分辨率:"app-plus": { "icons": { "android": { "mdpi": "static/logo/android/mdpi.png", "hdpi": "static/logo/android/hdpi.png" }, "ios": { "appstore": "static/logo/ios/appstore.png", "iphone": "static/logo/ios/iphone.png" } } } -
启动图配置:在
manifest.json的“App启动图配置”中上传启动图:"app-plus": { "splashscreen": { "image": "static/splash.png", "android": { "hdpi": "static/splash/android/hdpi.png" }, "ios": { "iphone": "static/splash/ios/iphone.png" } } }
6. 如何处理UniApp的版本更新?(小程序自动更新、App热更新/整包更新)
答案:
-
小程序版本更新:微信小程序会自动检测更新(用户下次打开时触发),或手动检测:
// app.vue onLaunch #ifdef MP-WEIXIN const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate((res) => { if (res.hasUpdate) { updateManager.onUpdateReady(() => { wx.showModal({ title: '更新提示', content: '新版本已准备好,是否重启?', success: (res) => { if (res.confirm) updateManager.applyUpdate(); } }); }); } }); #endif -
App热更新(wgt包更新):无需应用商店审核,直接更新代码:
// 检测更新 uni.request({ url: 'https://api.example.com/version', success: (res) => { const serverVersion = res.data.version; const localVersion = plus.runtime.version; if (serverVersion > localVersion) { // 下载wgt包 uni.downloadFile({ url: res.data.wgtUrl, success: (downloadRes) => { // 安装更新 plus.runtime.install(downloadRes.tempFilePath, {force: true}, () => { plus.runtime.restart(); }); } }); } } }); -
App整包更新:版本差异较大时,引导用户下载新安装包:
wx.showModal({ title: '版本更新', content: '发现新版本,请前往应用商店下载', showCancel: false, success: () => { plus.runtime.openURL('https://app.example.com/download'); } });
十、兼容性与适配类
1. UniApp如何适配不同尺寸的屏幕?(rpx、flex、媒体查询)
答案:
-
rpx单位(推荐):rpx是UniApp的自适应单位,1rpx = 屏幕宽度/750(设计稿以750px宽度为准),自动适配不同屏幕:
.box { width: 375rpx; /* 屏幕宽度的一半 */ height: 100rpx; font-size: 28rpx; } -
flex布局:弹性布局适配不同屏幕,如均分容器:
<view class="flex-container"> <view class="item">1</view> <view class="item">2</view> <view class="item">3</view> </view> <style> .flex-container { display: flex; width: 100%; } .item { flex: 1; /* 均分宽度 */ height: 100rpx; } </style> -
媒体查询:针对不同屏幕宽度定制样式:
/* 小屏手机 */ @media (max-width: 375px) { .title { font-size: 24rpx; } } /* 大屏手机 */ @media (min-width: 414px) { .title { font-size: 32rpx; } } -
动态计算:通过
uni.getSystemInfoSync()获取屏幕信息:onLoad() { const sysInfo = uni.getSystemInfoSync(); this.screenWidth = sysInfo.screenWidth; this.screenHeight = sysInfo.screenHeight; this.statusBarHeight = sysInfo.statusBarHeight; // 状态栏高度 }
2. iOS和Android端的兼容性差异有哪些?如何处理?
答案:
-
常见差异:
① 状态栏高度:iOS刘海屏状态栏高度44px,Android通常24-48px;
② 键盘行为:iOS键盘弹出时挤压页面,Android可能覆盖输入框;
③ 样式渲染:iOS支持
-webkit-appearance,Android对部分CSS属性支持差异;④ 权限申请:iOS需在
plist中声明权限描述,Android在AndroidManifest.xml中声明。 -
处理方案:① 动态计算状态栏高度:
const sysInfo = uni.getSystemInfoSync(); this.navBarHeight = sysInfo.statusBarHeight + (sysInfo.platform === 'ios' ? 44 : 48);② 监听键盘高度调整布局:
uni.onKeyboardHeightChange((res) => { this.inputBottom = res.height; // 输入框底部间距 });③ 条件编译适配样式:
#ifdef APP-IOS .button { border-radius: 8px; } #endif #ifdef APP-ANDROID .button { border-radius: 4px; } #endif
3. 小程序端的兼容性限制有哪些?(如API限制、样式限制、性能限制)
答案:
-
API限制:
① 小程序不支持
window/document对象(H5端可用);② 部分
uniAPI在小程序端有差异(如uni.share需配置分享权限);③ 网络请求需配置域名白名单(小程序管理后台)。
-
样式限制:
① 不支持
*选择器、:after/:before伪元素(部分小程序支持);② 行内样式不支持
!important;③ 背景图片需使用网络地址或base64(本地图片需转base64)。
-
性能限制:
① 主包体积≤2M,所有包合计≤20M(微信小程序);
② 页面节点数≤1000,WXML层级≤30层;
③
setData单次数据≤1024KB,调用频率≤20次/秒。 -
处理方案:
① 条件编译屏蔽小程序不支持的API:
#ifdef H5 document.title = '标题'; #endif② 分包加载减少主包体积;③ 优化WXML结构,减少节点层级。
4. H5端的兼容性问题有哪些?(如浏览器兼容、跨域、路由模式)
答案:
-
浏览器兼容:
① 低版本浏览器(如IE)不支持ES6+语法,需配置babel转译:
// babel.config.js module.exports = { presets: [['@babel/preset-env', { targets: { chrome: '58', ios: '10' } }]] };② 部分CSS属性(如
flex-wrap)需加前缀:.box { display: -webkit-flex; display: flex; -webkit-flex-wrap: wrap; flex-wrap: wrap; } -
跨域问题:开发环境配置代理(
manifest.json),生产环境后端配置CORS:"h5": { "devServer": { "proxy": { "/api": { "target": "https://api.example.com", "changeOrigin": true, "pathRewrite": {"^/api": ""} } } } } -
路由模式:H5端支持
hash和history模式,history模式需后端配置重定向:"h5": { "router": { "mode": "history", "base": "/app/" } }
十一、实战场景类
1. 如何实现UniApp的登录功能?(账号密码登录、微信登录、Token存储)
答案:
-
账号密码登录:
// 登录请求 async login() { const res = await request({ url: '/api/login', method: 'POST', data: { username: this.username, password: this.password } }); // 存储Token和用户信息 uni.setStorageSync('token', res.token); this.$store.commit('SET_TOKEN', res.token); this.$store.commit('SET_USER', res.user); // 跳转到首页 uni.switchTab({ url: '/pages/index/index' }); } -
微信登录(小程序端):
async wxLogin() { // 获取微信code const wxRes = await uni.login({ provider: 'weixin' }); // 后端换取Token const res = await request({ url: '/api/wxlogin', method: 'POST', data: { code: wxRes.code } }); // 存储状态 uni.setStorageSync('token', res.token); uni.switchTab({ url: '/pages/index/index' }); } -
登录状态校验:在
App.vue的onLaunch中校验Token:onLaunch() { const token = uni.getStorageSync('token'); if (!token) { uni.navigateTo({ url: '/pages/login/login' }); } }
2. 如何实现下拉刷新和上拉加载更多?
答案:
-
下拉刷新:
① 配置
pages.json开启下拉刷新:{ "pages": [ { "path": "pages/list/list", "style": { "enablePullDownRefresh": true } } ] }② 页面中实现逻辑:
onPullDownRefresh() { this.pageNum = 1; this.list = []; this.loadData().then(() => { uni.stopPullDownRefresh(); // 停止刷新动画 }); } -
上拉加载更多:
① 配置触底距离:
{ "pages": [ { "path": "pages/list/list", "style": { "onReachBottomDistance": 50 } } ] }② 页面中实现逻辑:
data() { return { list: [], pageNum: 1, hasMore: true, isLoading: false }; }, onReachBottom() { if (!this.isLoading && this.hasMore) { this.isLoading = true; this.pageNum++; this.loadData().finally(() => { this.isLoading = false; }); } }, async loadData() { const res = await request({ url: '/api/list', data: { pageNum: this.pageNum } }); if (res.data.length === 0) { this.hasMore = false; return; } this.list = [...this.list, ...res.data]; }
3. 如何实现图片预览、上传、裁剪功能?
答案:
-
图片预览:
previewImage(current) { uni.previewImage({ current: current, // 当前显示图片的链接 urls: this.imageList // 所有图片链接列表 }); } -
图片上传:
chooseImage() { uni.chooseImage({ count: 3, // 最多选择3张 sizeType: ['compressed'], // 压缩图片 sourceType: ['album', 'camera'], // 相册/相机 success: (res) => { // 上传图片 res.tempFilePaths.forEach((path) => { uni.uploadFile({ url: '/api/upload', filePath: path, name: 'file', success: (uploadRes) => { this.uploadedImages.push(JSON.parse(uploadRes.data).url); } }); }); } }); } -
图片裁剪(App端):使用
uni.chooseImage结合第三方裁剪插件(如uni-image-cropper):<uni-image-cropper ref="cropper" :src="tempImage" @confirm="onCropConfirm"></uni-image-cropper> <script> chooseAndCrop() { uni.chooseImage({ success: (res) => { this.tempImage = res.tempFilePaths[0]; this.showCropper = true; // 显示裁剪组件 } }); }, onCropConfirm(e) { // e.path为裁剪后的图片路径 uni.uploadFile({ url: '/api/upload', filePath: e.path, name: 'file' }); } </script>
4. 如何实现小程序的分享功能?(好友分享、朋友圈分享)
答案:
-
页面内分享按钮:
<button open-type="share" @click="onShare">分享</button> -
自定义分享内容:
onShareAppMessage(res) { if (res.from === 'button') { // 按钮触发的分享 return { title: '自定义标题', path: '/pages/detail/detail?id=1', imageUrl: 'https://example.com/share.jpg' }; } // 右上角菜单触发的分享 return { title: '默认标题', path: '/pages/index/index' }; }, // 朋友圈分享(仅微信小程序) onShareTimeline() { return { title: '朋友圈分享标题', imageUrl: 'https://example.com/share.jpg' }; } -
主动触发分享:
share() { uni.share({ provider: 'weixin', type: 0, // 0=好友,1=朋友圈 title: '分享标题', imageUrl: 'https://example.com/share.jpg', success: () => { uni.showToast({ title: '分享成功' }); } }); }
总结
UniApp作为跨端开发框架,核心优势是“一套代码多端运行”,但需掌握其生命周期、跨端适配、性能优化等关键点:
-
跨端原理:通过编译器将Vue代码编译为各端原生代码,运行时通过
uniAPI适配差异; -
性能优化:分包加载、虚拟列表、图片懒加载是提升体验的核心手段;
-
状态管理:复杂项目优先使用Vuex/Pinia,结合本地存储实现持久化;
-
调试发布:分端调试(模拟器/真机/开发者工具),严格遵循各平台审核规范。
掌握以上内容,可高效开发稳定、高性能的UniApp应用。