Uniapp 速查文档
Uniapp 速查文档
没错,又在拓展新的框架,新的知识,这次是
Uniapp。
我当前只做了小程序这一端,客户端这边是支持了
ios和安卓的app(app端自己打包再套到自己的壳子里的,我这边负责把一些不兼容的样式,以及登录授权相关的支持,并不是直接用uniapp直接发布app) 。其他端后续看情况再支持。
一、关于生命周期
以下是 UniApp 中完整的生命周期分类及说明,包含 应用生命周期、页面生命周期 和 组件生命周期
1、应用生命周期(App.vue)
| 生命周期 | 触发时机 | 使用场景 |
|---|---|---|
onLaunch |
当uni-app初始化完成时触发(全局只触发一次) |
获取设备信息、初始化全局数据、检查登录状态 |
onShow |
当uni-app启动,或从后台进入前台显示时触发 |
统计活跃用户、检查版本更新 |
onHide |
当uni-app从前台进入后台时触发 |
保存应用状态、停止定时任务 |
onError |
当uni-app报错时触发 |
错误监控和上报 |
onUniNViewMessage |
当nvue页面发送消息时触发(仅App端) |
nvue与vue页面通信 |
2、页面生命周期(页面级)
| 生命周期 | 触发时机 | 使用场景 |
|---|---|---|
onInit |
页面初始化时触发(仅百度小程序) | 百度小程序专用初始化逻辑 |
onLoad |
页面加载时触发(一个页面只会调用一次) | 接收路由参数、初始化页面数据 |
onShow |
页面显示/切入前台时触发 | 刷新数据(如返回页面时刷新列表) |
onReady |
页面初次渲染完成时触发(一个页面只会调用一次) | 操作DOM(如初始化图表) |
onHide |
页面隐藏/切入后台时触发(如跳转到其他页面) | 暂停定时器、保存草稿 |
onUnload |
页面卸载时触发(如关闭页面或redirectTo) | 清除定时器、解绑全局事件 |
onResize |
窗口尺寸变化时触发(仅App、微信小程序) | 响应式布局调整 |
onPullDownRefresh |
下拉刷新时触发 | 重新加载数据 |
onReachBottom |
页面上拉触底时触发 | 加载更多数据 |
onTabItemTap |
点击当前tab页时触发(需要是tabbar页面) | 统计tab点击行为 |
onShareAppMessage |
用户点击右上角分享时触发 | 自定义分享内容 |
onPageScroll |
页面滚动时触发 | 实现滚动动画、吸顶效果 |
onNavigationBarButtonTap |
点击导航栏按钮时触发(仅App、H5) | 处理自定义导航栏按钮点击 |
onBackPress |
页面返回时触发(仅App、H5) | 拦截返回操作(如提示保存) |
3、组件生命周期(Vue组件级)
| 生命周期 | 触发时机 | 使用场景 |
|---|---|---|
beforeCreate |
实例初始化之后,数据观测之前 | 极少使用 |
created |
实例创建完成(可访问data/methods,但DOM未生成) | 请求初始数据 |
beforeMount |
挂载开始之前被调用 | 极少使用 |
mounted |
挂载完成后调用(DOM已渲染) | 操作DOM、初始化第三方库 |
beforeUpdate |
数据更新时调用(虚拟DOM重新渲染和打补丁之前) | 获取更新前的DOM状态 |
updated |
数据更新导致虚拟DOM重新渲染后调用 | 执行依赖新DOM的操作 |
beforeUnmount |
在一个组件实例被卸载之前调用,这个钩子在服务端渲染时不会被调用 | 当这个钩子被调用时,组件实例依然还保有全部的功能。 |
unmounted |
在一个组件实例被卸载之后调用 | 可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。 |
关于Vue3的其他相关知识,可以参考这篇:vue3 实战笔记,期待能帮到你。
4、完整生命周期执行顺序
Vue3 页面及组件生命周期流程图
![]()
场景:首次打开页面
1. 应用生命周期
App.onLaunch → App.onShow
2. 页面生命周期
Page.onLoad → Page.onShow → Page.onReady
3. 组件生命周期
组件.beforeCreate → 组件.created → 组件.beforeMount → 组件.mounted
场景:页面跳转(A → B)
A.onHide → B.onLoad → B.onShow → B.onReady
场景:返回上一页
B.onUnload → A.onShow
4、跨平台差异说明
因为uniapp 兼容的平台较多,所以就会有关于跨平台的兼容性差异
| 生命周期 | 支持平台 | 特殊说明 |
|---|---|---|
onInit |
仅百度小程序 | 其他平台用onLoad替代 |
onResize |
App、微信小程序 | H5需监听window.resize |
onBackPress |
App、H5 | 微信小程序需用wx.onAppCapture返回事件 |
onNavigationBarButtonTap |
App、H5 | 微信小程序需自定义导航栏 |
5、关于在不同生命周期中的最佳实践建议
-
数据请求:
- 首次加载:
onLoad+created - 返回刷新:
onShow
- 首次加载:
-
DOM操作:
- 页面级:
onReady - 组件级:
mounted
- 页面级:
-
资源释放:
- 页面级:
onUnload - 组件级:
beforeDestroy
- 页面级:
二、项目开发
1、 配置tabBar
tabBar就是在小程序的底部菜单,也相当于一级导航,以我当前这个小程序为例子,我只需要两个tab,配置两个就可以。
![]()
示例代码:
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#2F88FF",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/img/home_before2.png",
"selectedIconPath": "static/img/home_after2.png",
"text": "首页"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/img/user_before2.png",
"selectedIconPath": "static/img/user_after2.png",
"text": "我的"
}
]
},
2、关于页面跳转
uni.navigateTo(OBJECT)
保留当前页面,跳转到应用内的某个页面,使用
uni.navigateBack可以返回到原页面。 原文链接
基础使用,就是从一个页面跳转到另一个页面
uni.navigateTo({
url: "/pages/my-history/my-history",
});
复杂示例: 从A页面跳转B页面,携带参数(可从url带参数,也可以用方法)
- A页面
// 可直接通过地址栏传递参数
uni.navigateTo({
url: `/pages/formContent/formContent?form=${permissionCode}`,
success: (res) => {
// 也可以在跳转成功后传递一些参数
res.eventChannel.emit('acceptDataFromIndexPage', {
toolDetail: tool,
});
},
});
- B页面
<template>
<view>
B 页面
</view>
</template>
<script setup>
import {
ref,
onMounted,
getCurrentInstance
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import FormComponent from '@/components/FormComponent.vue';
import {
formConfigs
} from '@/config/formConfig.js';
onLoad((options) => {
// 获取路由参数中的 formKey
const formKey = `/pages/formContent/formContent?form=${options.form}`;
console.log(formKey)
});
onMounted(() => {
const instance = getCurrentInstance().proxy;
const eventChannel = instance.getOpenerEventChannel();
// 接收自A页面跳转成功传递的数据
eventChannel.on('acceptDataFromIndexPage', (data) => {
console.log('Received data:', data);
});
});
</script>
uni.redirectTo(OBJECT)
关闭当前页面,跳转到应用内的某个页面。原文链接
uni.reLaunch(OBJECT)
关闭所有页面,打开到应用内的某个页面。原文链接
uni.switchTab(OBJECT)
跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。原文链接
tabBar也就是你在项目里配置的底部操作区域,这个跳转就不和跳转路由一样了,需要使用特定的方法,和上面几个跳转方法一样,这个url同样是你的文件存放地址:
例如:
uni.switchTab({
url: '/pages/index/index',
});
3、页面通讯
我们除了可以用
vue的通讯工具,还可以用uniapp自带的通讯工具
uni.$emit(eventName,OBJECT)
触发全局的自定义事件,附加参数都会传给监听器回调函数。 原文链接
uni.$on(eventName,callback)
监听全局的自定义事件,事件由
uni.$emit触发,回调函数会接收事件触发函数的传入参数。 原文链接
4、判断当前的设备类型
有些时候我们是需要根据不同的设备来写一些样式或者区分请求不同接口,那就需要知道当前的设备类型
// 存储设备信息
let deviceInfo = {
platform: "devtools",
detail: {},
};
const appInfo = uni.getAppBaseInfo();
deviceInfo.platform = appInfo.uniPlatform;
deviceInfo.detail = appInfo;
uni.setStorage({
key: "deviceInfo",
data: deviceInfo,
});
console.log("当前运行环境-登录", deviceInfo);
在chrome 浏览器运行
![]()
在微信小程序中运行
![]()
在app中运行
<Weex>[log][WXBridgeContext.mm:1323](http://WXBridgeContext.mm:1323), jsLog:
运行平台:---COMMA------BEGIN:JSON---
{
"platform":"app",
"detail":
{"appId":"__UNI__3DD2483",
"appName":"uni-ui-demo",
"appVersion":"1.0.2",
"appVersionCode":"10020",
"appWgtVersion":"1.0.0",
"appLanguage":"zh-Hans",
"enableDebug":false,
"language":"zh-Hans-CN",
"SDKVersion":"",
"theme":"light",
"version":"1.9.9.81527",
"isUniAppX":false,
"uniPlatform":"app",
"uniRuntimeVersion":"4.45",
"uniCompileVersion":"4.45",
"uniCompilerVersion":"4.45"
}
}
5、条件编译
// #ifdef APP-PLUS
plus.globalEvent.addEventListener('plusMessage', (msg) => {
uni.showToast({
title: 'postMessage0',
icon: "error",
duration: 2000
});
const result = msg.data.args.data;
if (result.name == 'postMessage') {
uni.showToast({
title: 'postMessage1',
icon: "error",
duration: 2000
});
console.log('postMessage', msg);
uni.$emit('webviewCode', msg);
}
});
// #endif
6、关于封装请求接口
请求实体
import { HttpResponse } from "./common";
import { useLoginStore } from "../store/loginStore";
const request = (
baseUrl: string,
url: string,
method: "POST" | "GET",
data = {},
header = {},
): Promise<HttpResponse> => {
return new Promise((resolve, reject) => {
const loginStore = useLoginStore();
uni.getNetworkType({
success: function (res) {
if (res.networkType === "none") {
uni.showToast({
title: "当前网络不可用,请检查网络设置",
icon: "none",
duration: 2000,
});
reject(new Error("网络不可用")); // 拒绝请求
} else {
const deviceInfo = uni.getStorageSync("deviceInfo");
const token = uni.getStorageSync("token");
// 我的自定义参数
const otherHeader = {
token: token?.jwt,
};
uni.request({
url: `${baseUrl}${url}`,
method: method,
data: data,
header: {
"Content-Type": "application/json",
...(!baseUrl.includes("sso") ? otherHeader : {}),
...header,
},
success: (res: any) => {
// 以下的判断根据自己的业务处理
if (res.data.code === "0" || res.data.code === 1) {
resolve(res.data);
} else if (res.data.code === 401) {
uni?.hideLoading();
console.log("登录失效,请登录");
// 清空本地数据
loginStore.deleteAllStorageData();
reject(res.data);
} else if (res.data.code === 10006 && res.data.msg === "账号已过期") {
uni?.hideLoading();
console.log("账号已过期");
}
else if (res.data.code === undefined && res.data) {
// 后端没有返回 code 的情况
resolve(res.data);
console.log(res.data);
} else {
// 非 2xx 状态码处理
uni.showToast({
title: res?.data.msg || "请求报错,请重试",
icon: "none",
duration: 2000,
});
reject(res);
console.log(res.data);
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: "请求失败,请稍后再试",
icon: "none",
duration: 2000,
});
reject(err);
},
});
}
},
fail: function () {
console.error("获取网络状态失败");
reject(new Error("获取网络状态失败"));
},
});
});
};
// 封装 GET 请求
const get = (baseUrl: string, url: string, data = {}, header = {}): Promise<HttpResponse> => {
return request(baseUrl, url, "GET", data, header);
};
// 封装 POST 请求
const post = (baseUrl: string, url: string, data = {}, header = {}): Promise<HttpResponse> => {
return request(baseUrl, url, "POST", data, header);
};
export { get, post };
接口请求示例:
import { post } from '../request';
import { _INDEXURL } from '../config';
import { MenuVersion, HttpResponse } from '../common'
// 请求示例:
// 后端接口地址: https://xxxxx.cn/api/aigc/menu/recent/useList
// 拼接完整地址:使用历史
const useList = (params : { version : MenuVersion }) : Promise<HttpResponse<any>> => {
const url = '/menu/recent/useList';
return post(_INDEXURL, url, params);
};
export default useList;
上面的_INDEXURL是请求的域名前缀,用变量代替,后期好替换管理,并且我有很多不同的前缀,如下:
const _SSOURL_1 = `${config.SSOURL}/sso/1.0/sso`;
const _SSOURL_3 = `${config.SSOURL}/sso/3.0/sso`;
const _SSOURL_4 = `${config.SSOURL}/sso/4.0/sso`;
const _INDEXURL = `${config.INDEXURL}/api/aigc`;
而这个SSOURL也是根据当前的环境区分使用的是测试还是线上接口,如下这样配置:
const config: Record<string, any> = {
test: {
SSOURL: "https://test.xxx.cn",
INDEXURL: "https://test.xxx.cn",
SOCKET_URL: "wss://test.xxx.cn",
ENVVERSION: "trial",
MINPRROGRAM: 2,
},
production: {
SSOURL: "https://xxx.xxx.cn",
INDEXURL: "https://xxx.xxx.cn",
SOCKET_URL: "wss://xxx.xxx.cn",
ENVVERSION: "release",
MINPRROGRAM: 0,
},
};
返回数据基础类型
interface HttpResponse<T = any> {
data: { token: string };
code: number;
msg?: string;
success?: boolean;
value?: T;
rs?: {
[key: string]: any;
};
}
7、注册全局组件
我的项目里有且不止一个全局的组件,我不想在每个页面都引入
把需要全局引入的组件放在一个文件里
<template>
<!-- 公共模板,如有需要在全局添加的组件,可以在这里添加 -->
<view>
<!-- 登录弹框 -->
<LoginModal />
<!-- 购买会员弹框 -->
<BuyMembershipDialog />
</view>
</template>
<script setup>
import LoginModal from "@/components/login/loginModal.vue";
import BuyMembershipDialog from "@/components/modal/BuyMembershipDialog.vue";
</script>
在这里注册到全局:
import { createSSRApp } from "vue";
import App from "./App.vue";
import share from "@/utils/share.js";
import CommonTemplate from "@/components/common/common-template.vue";
export function createApp() {
const app = createSSRApp(App);
// 全局组件
app.component("CommonTemplate", CommonTemplate);
app.use(share); // 全局混入
return {
app,
};
}
8、关于微信授权登录
微信授权流程
获取微信登录授权码
code-> 拿着授权码去换取unionID和openID-> 通过两个id后端查询库里是否存在绑定关系,存在绑定关系则调用登录接口,不存在绑定关系则跳转到绑定手机号页面,进行手机号的绑定
![]()
![]()
使用获取手机号组件来获取微信手机号 文档地址,这个接口需要由自己的服务端来转发,服务端文档地址
每个小程序账号将有
1000次体验额度,用于开发、调试和体验。该1000次的体验额度为正式版、体验版和开发版小程序共用,超额后,体验版和开发版小程序调用同正式版小程序一样,均收费。这个组件是收费的,标准单价为:每次组件调用成功,收费0.03元。
<button class="btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumberFn">
授权
</button>
具体授权
const getPhoneNumberFn = async (e) => {
const {
detail: { code, errMsg },
} = e;
if (errMsg === "getPhoneNumber:ok") {
dialogClose();
uni.showLoading({ title: "授权中..." });
await getPhoneNumber({ code }).then((res) => {
// 这里拿到手机号去绑定
autoBindPhoneAndLoginFn(res?.value?.purePhoneNumber);
});
} else {
// 用户拒绝授权
uni.showToast({
title: "您已拒绝授权",
icon: "none",
});
}
};
获取微信登录授权的授权码
// 获取授权码
uni.login({
provider: "weixin",
onlyAuthorize: true, // 微信登录仅请求授权认证
success: async function (event) {
const { code } = event;
const deviceInfo = uni.getStorageSync("deviceInfo");
if (deviceInfo.platform === "mp-weixin") {
await miniAppLogin(code);
} else {
// app 平台配置
await appLoginFn(code);
}
},
fail: function (err) {
uni.showToast({
title: "授权失败,请重试",
icon: "none",
});
isLoading.value = false;
},
});
注意: 我现在的项目是同时运行在小程序和客户端(安卓& ios app上的),所以在拿到微信的 code 码之后,需要区分是在小程序登录还是app 登录,再调用不同的方法
小程序登录: miniAppLogin 换取openID 和 unionID
const miniAppLogin = async (code) => {
await wxLogin({ code }).then((res) => {
const unionID = res?.value?.unionid;
const openID = res?.value?.openid;
if (unionID) {
unionId.value = unionID;
openId.value = openID;
uni.setStorageSync("openId", openID);
// 调用接口查状态,如果是首次登录,则弹出是否同意协议,否则直接登录
getWxBindingStatus({ unionId: unionID, openId: openID }).then((val) => {
const status = val?.rs?.result;
// false:未绑定 true:已绑定
if (status) {
handleGetticket();
} else {
alertDialog.value.open();
isLoading.value = false;
}
});
}
});
};
app 的登录流程
9、关于apple授权登录
const handleAppleLogin = () => {
uni.login({
provider: "apple",
success: async (loginRes) => {
console.log("apple登录成功", loginRes);
const token = loginRes.authResult.access_token;
// 拿着token去请求后端,查看这个账户有没有绑定过
await appleConnect({ access_token: token })
.then((res) => {
// 执行后端登录逻辑
handleWxLoginSuccess(res);
uni.hideLoading();
})
.catch((error) => {
console.log(error, "appleConnect 失败");
if (error.code === "5005" && JSON.parse(error.msg)) {
// 弹绑定手机号页面,去绑定手机号
openId.value = JSON.parse(error.msg)?.openId;
handleLoginChangeSuccess("third");
}
});
},
fail: function (err) {
console.log("apple授权失败", err);
uni.showToast({
title: "登录授权失败,请重试",
icon: "none",
});
},
});
};
如果是首次授权,会弹出一个确认框来确认是否用本apple账号登录
如果不是首次授权登录,则不会弹这个框,直接登录了
![]()
10、小程序支付
小程序调用支付还是蛮简单的,文档地址: uniapp微信小程序支付, 微信小程序官网支付文档
uniapp 小程序支付
参数示例(仅作为示例,非真实参数信息):
uni.requestPayment({
provider: 'wxpay',
timeStamp: String(Date.now()),
nonceStr: 'A1B2C3D4E5',
package: 'prepay_id=wx20180101abcdefg',
signType: 'MD5',
paySign: '',
success: function (res) {
console.log('success:' + JSON.stringify(res));
},
fail: function (err) {
console.log('fail:' + JSON.stringify(err));
}
});
当点击了购买以后,先调用后端接口下单
const handleOrderPay = async (openId, type) => {
await orderPay({
goodsId: selectedGoodsId.value,
openId,
}).then(({ value }) => {
const { appId, ...res } = value;
getPayModal(res);
});
};
使用uniapp的方法调起支付弹框,参数参参考上面的示例或文档:
const getPayModal = (value) => {
uni.requestPayment({
provider: "wxpay",
...value,
success: function (res) {
// 支付成功后会有回调,这里是支付成功后的一些提示和操作
// 建议:提示+跳转到指定页面
uni.showToast({
title: "支付成功",
icon: "success",
});
uni.switchTab({url:"/pages/mine/mine"})
},
fail: function (err) {
console.log("支付失败fail:" + JSON.stringify(err));
uni.showToast({
title: "支付失败",
icon: "error",
});
},
});
};
原生微信小程序接入支付
如果你用的是原生的微信小程序,文档地址,同样也是前端调用方法就可以调起支付的弹框(前提依然是要先下单),参考下面的示例:
wx.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success (res) { },
fail (res) { }
})
最后,关于一些异常bug
关于在ios手机收到验证码后,使用自动填充时,短信验证码回填两次的问题
限制验证码输入框的内容长度,查阅了社区说这个是
ios系统的bug,暂时先用maxLength来限制 代码如下:
<input
class="input-password"
v-model="verificationCode"
placeholder="输入验证码"
type="number"
inputmode="numeric"
maxlength="6"
/>
textarea 输入框中间有内容,从中间插入光标,一直按删除键,当删除完前面的内容后,光标会跳到末尾,导致误删末尾的内容
我的项目背景: 既需要正常的用户输入,又要拿到上次用户输入进行回填,如果不需要数据回填,就不需要
v-model就不需要数据同步,直接从@input里拿数据就行,我猜测大概是组件数据通信延迟导致的问题。
第一种解决方案:
html:
<textarea
v-model="essayContent"
@input="onInput"
></textarea>
js:
// 当光标已经删除到开头时,阻止默认删除行为
const onInput = (e) => {
const selectionStart = e.detail.cursor; // 获取光标位置
if (selectionStart === 0) {
e.preventDefault(); // 阻止默认删除行为
return;
}
};
我们项目还用到了:uni-ui ,我尝试了项目中用到的 uni-easyinput 组件,同样也有这个问题,示例如下:
<uni-easyinput
type="textarea"
:maxlength="4000"
v-model="baseFormData.introduction"
placeholder="请输入提示词"
></uni-easyinput>
第二种解决方案:
不用v-model用value绑定,然后在 @blur的时候给 value绑定的值赋值,如下:
html:
<textarea
:value="formData.content"
@blur="onBlur"
></textarea>
js:
const onBlur = (e) => {
formData.content = e.target.value;
};
这样不用v-model同样也可以在formData.content拿到数据
写到这里还没有结束,最近又收到了其他的工作安排,后面会继续更新,一方面对于自己来说可以当作一个笔记本,另一方面也给掘金的好友们提供一些思路,
祝大家开发顺顺利利。