页面跳转是uniApp开发中最基础也最常被问到的功能点。很多初级开发者可能只停留在会用的层面,但作为面试官,我更关注候选人是否理解不同跳转方式的底层差异。uniApp提供了三种核心跳转方式:uni.navigateTo、uni.redirectTo和uni.reLaunch,它们看似简单,实际使用时却有不少门道。
先说说uni.navigateTo,这是最常用的跳转方式。它的特点是保留当前页面,新页面会压入页面栈。在实际项目中,我遇到过页面栈超过10层导致白屏的情况。这时候就需要在跳转前检查页面栈深度:
javascript复制// 检查页面栈深度
const pages = getCurrentPages()
if(pages.length >= 10){
uni.showToast({ title: '页面层级过深', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/detail/detail' })
uni.redirectTo则完全不同,它会关闭当前页面跳转到新页面。这在登录跳转场景特别实用。比如用户从商品页点击购买,发现未登录,这时应该用redirectTo跳转到登录页,而不是navigateTo。否则用户登录后返回,会回到未登录状态的商品页,造成逻辑混乱。
uni.reLaunch是最彻底的跳转方式,它会关闭所有页面打开新页面。适合用在应用内"首页"这样的全局导航点。但要注意,它会导致所有页面状态丢失,包括vuex中的数据。我在实际项目中发现,很多开发者会滥用reLaunch,导致应用状态管理混乱。
数据绑定看似简单,但要做到高性能却需要深入理解原理。uniApp基于Vue的数据绑定机制,但又有其特殊之处。先说基础用法,大家都知道的{{}}插值表达式:
html复制<template>
<view>
<text>{{ message }}</text>
</view>
</template>
但在列表渲染时,很多开发者会忽略key的重要性。没有正确设置key会导致列表更新时DOM无法高效复用。我曾经优化过一个项目,仅仅通过添加正确的key就使列表滚动性能提升了50%:
html复制<template>
<view>
<view v-for="item in list" :key="item.id">
{{ item.name }}
</view>
</view>
</template>
另一个常见问题是数据绑定的性能陷阱。比如在大型表单中,如果每个输入都直接绑定到data中的对象属性,会导致每次输入都触发全量更新。这时可以使用v-model的lazy修饰符,或者手动实现双向绑定:
javascript复制<input :value="formData.name" @input="handleInput('name', $event)">
uni.request是uniApp网络请求的核心API,但直接使用它往往会导致代码难以维护。我在团队中强制要求对网络请求进行二次封装,主要解决以下几个问题:
一个基础的封装示例如下:
javascript复制const http = {
request(options) {
return new Promise((resolve, reject) => {
const defaultOptions = {
header: {
'Authorization': uni.getStorageSync('token')
}
}
uni.request({
...defaultOptions,
...options,
success: (res) => {
if(res.statusCode === 401) {
// 统一处理未登录
uni.navigateTo({ url: '/pages/login/login' })
return
}
resolve(res.data)
},
fail: (err) => {
uni.showToast({ title: '网络错误', icon: 'none' })
reject(err)
}
})
})
}
}
对于高频调用的接口,可以加入缓存机制。我通常会根据接口特点设置不同的缓存策略:
javascript复制const cache = new Map()
async function getDataWithCache(url) {
if(cache.has(url)) {
return cache.get(url)
}
const data = await http.request({ url })
cache.set(url, data)
setTimeout(() => cache.delete(url), 60000) // 1分钟后过期
return data
}
uniApp提供了uni.setStorageSync等同步缓存API,但在实际项目中,我们需要考虑更多场景。首先是缓存大小限制,不同平台有不同的限制(通常5-10MB),超出会导致存储失败。我建议在存储前检查剩余空间:
javascript复制function checkStorageSpace() {
try {
const res = uni.getStorageInfoSync()
if(res.currentSize / res.limitSize > 0.9) {
console.warn('存储空间即将用尽')
return false
}
return true
} catch(e) {
console.error('获取存储信息失败', e)
return false
}
}
其次是结构化数据的存储。很多开发者直接存储JSON字符串,但更好的做法是使用uni.setStorage的异步API配合错误处理:
javascript复制async function saveComplexData(key, data) {
try {
await new Promise((resolve, reject) => {
uni.setStorage({
key,
data: JSON.stringify(data),
success: resolve,
fail: reject
})
})
return true
} catch(e) {
console.error('存储失败', e)
return false
}
}
对于需要频繁更新的数据,可以考虑使用uni.setStorage的加密版本uni.setStorageEncryptedSync(HBuilderX 3.1.0+支持),特别是存储用户敏感信息时。
uniApp的组件系统基于Vue,但有自己的一些特性。首先是组件通信,除了常规的props/emit,在uniApp中还可以利用uni.$on和uni.$emit进行全局事件通信。我在大型项目中会建立专门的事件中心:
javascript复制// event-bus.js
const EventBus = new Vue()
export default EventBus
// 组件A
EventBus.$emit('data-updated', newData)
// 组件B
EventBus.$on('data-updated', (data) => {
// 处理数据
})
另一个常见问题是样式隔离。uniApp默认启用了样式隔离,但有时我们需要在组件中修改父组件样式。可以通过options配置styleIsolation:
javascript复制export default {
options: {
styleIsolation: 'shared' // 表示组件和页面样式可以互相影响
}
}
对于高频使用的UI组件,我建议封装为原生组件以获得更好的性能。uniApp支持将组件编译为原生组件,这需要一些额外配置:
json复制// pages.json
{
"usingComponents": {
"my-native-component": "/components/my-native-component"
}
}
性能是面试中的高频考点,也是实际项目中最容易忽视的环节。首先是图片优化,uniApp的image组件支持懒加载,但需要正确配置:
html复制<image
lazy-load
:src="imageUrl"
mode="aspectFill"
@load="handleImageLoad"
/>
对于长列表,一定要使用scroll-view的优化版本。我通常会封装一个高性能列表组件:
html复制<template>
<scroll-view
scroll-y
:scroll-top="scrollTop"
@scrolltolower="loadMore"
>
<view v-for="(item, index) in list"
:key="item.id"
:id="'item-'+index">
<!-- 列表项内容 -->
</view>
<loading-indicator v-if="loading" />
</scroll-view>
</template>
另一个重要优化点是减少不必要的数据响应。对于大型数据对象,可以使用Object.freeze来阻止Vue的响应式转换:
javascript复制export default {
data() {
return {
largeData: Object.freeze(require('large-data.json'))
}
}
}
uniApp的最大优势是跨平台,但这也带来了兼容性问题。我通常会建立一个平台判断的工具函数:
javascript复制const platform = {
isH5: process.env.VUE_APP_PLATFORM === 'h5',
isWechat: process.env.VUE_APP_PLATFORM === 'mp-weixin',
isApp: process.env.VUE_APP_PLATFORM === 'app'
}
对于平台特定的代码,可以使用条件编译:
javascript复制// #ifdef H5
console.log('这段代码只会在H5平台执行')
// #endif
// #ifdef MP-WEIXIN
console.log('这段代码只会在微信小程序平台执行')
// #endif
在样式兼容方面,我建议使用uniApp提供的CSS变量:
css复制.text {
color: var(--uni-text-color);
font-size: var(--uni-font-size-lg);
}
应用安全往往被初级开发者忽视。首先是接口安全,除了使用HTTPS,我还会在请求头中加入签名:
javascript复制function generateSign(params, timestamp) {
const secret = 'your-secret-key'
const str = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&')
return md5(`${str}×tamp=${timestamp}&secret=${secret}`)
}
对于本地存储的敏感数据,建议使用加密存储:
javascript复制function encryptData(data) {
// 使用crypto-js等库实现加密
return CryptoJS.AES.encrypt(JSON.stringify(data), 'secret-key').toString()
}
function decryptData(encryptedData) {
const bytes = CryptoJS.AES.decrypt(encryptedData, 'secret-key')
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
}
在权限控制方面,我通常会实现一个细粒度的权限检查系统:
javascript复制const permissions = {
'user:create': ['admin'],
'order:delete': ['admin', 'manager']
}
function checkPermission(user, permission) {
return permissions[permission]?.includes(user.role)
}