Uniapp 速查文档

Uniapp 速查文档

没错,又在拓展新的框架,新的知识,这次是Uniapp
我当前只做了小程序这一端,客户端这边是支持了ios和安卓的appapp端自己打包再套到自己的壳子里的,我这边负责把一些不兼容的样式,以及登录授权相关的支持,并不是直接用uniapp直接发布app) 。其他端后续看情况再支持。


一、关于生命周期

以下是 UniApp 中完整的生命周期分类及说明,包含 应用生命周期页面生命周期组件生命周期


1、应用生命周期(App.vue)

生命周期 触发时机 使用场景
onLaunch uni-app初始化完成时触发(全局只触发一次) 获取设备信息、初始化全局数据、检查登录状态
onShow uni-app启动,或从后台进入前台显示时触发 统计活跃用户、检查版本更新
onHide uni-app从前台进入后台时触发 保存应用状态、停止定时任务
onError uni-app报错时触发 错误监控和上报
onUniNViewMessage nvue页面发送消息时触发(仅App端) nvuevue页面通信

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组件级)

vue生命周期官方文档

生命周期 触发时机 使用场景
beforeCreate 实例初始化之后,数据观测之前 极少使用
created 实例创建完成(可访问data/methods,但DOM未生成) 请求初始数据
beforeMount 挂载开始之前被调用 极少使用
mounted 挂载完成后调用(DOM已渲染) 操作DOM、初始化第三方库
beforeUpdate 数据更新时调用(虚拟DOM重新渲染和打补丁之前) 获取更新前的DOM状态
updated 数据更新导致虚拟DOM重新渲染后调用 执行依赖新DOM的操作
beforeUnmount 在一个组件实例被卸载之前调用,这个钩子在服务端渲染时不会被调用 当这个钩子被调用时,组件实例依然还保有全部的功能。
unmounted 在一个组件实例被卸载之后调用 可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

关于Vue3的其他相关知识,可以参考这篇:vue3 实战笔记,期待能帮到你。


4、完整生命周期执行顺序

Vue3 页面及组件生命周期流程图

场景:首次打开页面

markdown 复制代码
1. 应用生命周期
   App.onLaunch → App.onShow

2. 页面生命周期
   Page.onLoad → Page.onShow → Page.onReady

3. 组件生命周期
   组件.beforeCreate → 组件.created → 组件.beforeMount → 组件.mounted

场景:页面跳转(A → B)

css 复制代码
A.onHide → B.onLoad → B.onShow → B.onReady

场景:返回上一页

css 复制代码
B.onUnload → A.onShow

4、跨平台差异说明

因为uniapp 兼容的平台较多,所以就会有关于跨平台的兼容性差异

生命周期 支持平台 特殊说明
onInit 仅百度小程序 其他平台用onLoad替代
onResize App、微信小程序 H5需监听window.resize
onBackPress App、H5 微信小程序需用wx.onAppCapture返回事件
onNavigationBarButtonTap App、H5 微信小程序需自定义导航栏

5、关于在不同生命周期中的最佳实践建议

  1. 数据请求

    • 首次加载:onLoad + created
    • 返回刷新:onShow
  2. DOM操作

    • 页面级:onReady
    • 组件级:mounted
  3. 资源释放

    • 页面级:onUnload
    • 组件级:beforeDestroy

二、项目开发

1、 配置tabBar

tabBar就是在小程序的底部菜单,也相当于一级导航,以我当前这个小程序为例子,我只需要两个tab,配置两个就可以。

示例代码:

css 复制代码
  "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可以返回到原页面。 原文链接

基础使用,就是从一个页面跳转到另一个页面

css 复制代码
 uni.navigateTo({
    url: "/pages/my-history/my-history",
  });

复杂示例: 从A页面跳转B页面,携带参数(可从url带参数,也可以用方法)

  • A页面
javascript 复制代码
// 可直接通过地址栏传递参数
uni.navigateTo({
  url: `/pages/formContent/formContent?form=${permissionCode}`,
  success: (res) => {
  // 也可以在跳转成功后传递一些参数
    res.eventChannel.emit('acceptDataFromIndexPage', {
      toolDetail: tool,
    });
  },
});
  • B页面
xml 复制代码
<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同样是你的文件存放地址:

例如:

css 复制代码
 uni.switchTab({
   url: '/pages/index/index',
 });

3、页面通讯

我们除了可以用vue的通讯工具,还可以用uniapp自带的通讯工具

uni.$emit(eventName,OBJECT)

触发全局的自定义事件,附加参数都会传给监听器回调函数。 原文链接

uni.$on(eventName,callback)

监听全局的自定义事件,事件由 uni.$emit 触发,回调函数会接收事件触发函数的传入参数。 原文链接


参考链接

4、判断当前的设备类型

有些时候我们是需要根据不同的设备来写一些样式或者区分请求不同接口,那就需要知道当前的设备类型

ini 复制代码
  // 存储设备信息
  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中运行

c 复制代码
<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、条件编译

php 复制代码
// #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、关于封装请求接口

请求实体

typescript 复制代码
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 };

接口请求示例:

typescript 复制代码
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是请求的域名前缀,用变量代替,后期好替换管理,并且我有很多不同的前缀,如下:

ini 复制代码
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也是根据当前的环境区分使用的是测试还是线上接口,如下这样配置:

css 复制代码
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,
   
  },
};

返回数据基础类型

css 复制代码
interface HttpResponse<T = any> {
  data: { token: string };
  code: number;
  msg?: string;
  success?: boolean;
  value?: T;
  rs?: {
    [key: string]: any;
  };
}

7、注册全局组件

我的项目里有且不止一个全局的组件,我不想在每个页面都引入

把需要全局引入的组件放在一个文件里

xml 复制代码
<template>
  <!-- 公共模板,如有需要在全局添加的组件,可以在这里添加 -->
  <view>
    <!-- 登录弹框 -->
    <LoginModal />
    <!-- 购买会员弹框 -->
    <BuyMembershipDialog />
  </view>
</template>

<script setup>

import LoginModal from "@/components/login/loginModal.vue";
import BuyMembershipDialog from "@/components/modal/BuyMembershipDialog.vue";

</script>

在这里注册到全局:

javascript 复制代码
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-> 拿着授权码去换取unionIDopenID -> 通过两个id后端查询库里是否存在绑定关系,存在绑定关系则调用登录接口,不存在绑定关系则跳转到绑定手机号页面,进行手机号的绑定

使用获取手机号组件来获取微信手机号 文档地址,这个接口需要由自己的服务端来转发,服务端文档地址

每个小程序账号将有1000次体验额度,用于开发、调试和体验。该1000次的体验额度为正式版、体验版和开发版小程序共用,超额后,体验版和开发版小程序调用同正式版小程序一样,均收费。这个组件是收费的,标准单价为:每次组件调用成功,收费0.03元

kotlin 复制代码
<button class="btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumberFn">
 授权
</button>

具体授权

javascript 复制代码
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",
    });
  }
};

获取微信登录授权的授权码

javascript 复制代码
  // 获取授权码
  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 换取openIDunionID

ini 复制代码
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授权登录

javascript 复制代码
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 小程序支付

参数示例(仅作为示例,非真实参数信息):

javascript 复制代码
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));
        }
});

当点击了购买以后,先调用后端接口下单

typescript 复制代码
const handleOrderPay = async (openId, type) => {
    await orderPay({
      goodsId: selectedGoodsId.value,
      openId,
    }).then(({ value }) => {
      const { appId, ...res } = value;
      getPayModal(res);
    });
};

使用uniapp的方法调起支付弹框,参数参参考上面的示例或文档:

php 复制代码
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",
      });
    },
  });
};

原生微信小程序接入支付

如果你用的是原生的微信小程序,文档地址,同样也是前端调用方法就可以调起支付的弹框(前提依然是要先下单),参考下面的示例:

php 复制代码
wx.requestPayment({
  timeStamp: '',
  nonceStr: '',
  package: '',
  signType: 'MD5',
  paySign: '',
  success (res) { },
  fail (res) { }
})

最后,关于一些异常bug

关于在ios手机收到验证码后,使用自动填充时,短信验证码回填两次的问题

限制验证码输入框的内容长度,查阅了社区说这个是ios系统的bug,暂时先用maxLength来限制 代码如下:

ini 复制代码
<input
    class="input-password"
    v-model="verificationCode"
    placeholder="输入验证码"
    type="number"
    inputmode="numeric"
    maxlength="6"
  />

textarea 输入框中间有内容,从中间插入光标,一直按删除键,当删除完前面的内容后,光标会跳到末尾,导致误删末尾的内容

我的项目背景: 既需要正常的用户输入,又要拿到上次用户输入进行回填,如果不需要数据回填,就不需要v-model 就不需要数据同步,直接从@input 里拿数据就行,我猜测大概是组件数据通信延迟导致的问题。

第一种解决方案:

html:

ini 复制代码
  <textarea
        v-model="essayContent"
        @input="onInput"
      ></textarea>

js:

ini 复制代码
// 当光标已经删除到开头时,阻止默认删除行为
const onInput = (e) => {
  const selectionStart = e.detail.cursor; // 获取光标位置
  if (selectionStart === 0) {
    e.preventDefault(); // 阻止默认删除行为
    return;
  }
};

我们项目还用到了:uni-ui ,我尝试了项目中用到的 uni-easyinput 组件,同样也有这个问题,示例如下:

ini 复制代码
 <uni-easyinput
    type="textarea"
    :maxlength="4000"
    v-model="baseFormData.introduction"
    placeholder="请输入提示词"
 ></uni-easyinput>

第二种解决方案:

不用v-modelvalue绑定,然后在 @blur的时候给 value绑定的值赋值,如下:

html:

ini 复制代码
 <textarea
  :value="formData.content"
  @blur="onBlur"
></textarea>

js:

ini 复制代码
const onBlur = (e) => {
  formData.content = e.target.value;
};

这样不用v-model同样也可以在formData.content拿到数据


写到这里还没有结束,最近又收到了其他的工作安排,后面会继续更新,一方面对于自己来说可以当作一个笔记本,另一方面也给掘金的好友们提供一些思路,

祝大家开发顺顺利利。

相关推荐
琉-璃6 小时前
vue3+ts 任意组件间的通信 mitt的使用
前端·javascript·vue.js
FogLetter6 小时前
React Fiber 机制:让渲染变得“有礼貌”的魔法
前端·react.js
不想说话的麋鹿6 小时前
「项目前言」从配置程序员到动手造轮子:我用Vue3+NestJS复刻低代码平台的初衷
前端·程序员·全栈
JunpengHu6 小时前
esri-leaflet介绍
前端
zm4357 小时前
bpmn.js 自定义绘制流程图节点
前端·bpmn-js
小杨梅君7 小时前
探索现代 CSS 色彩:从 HSL 到 OKLCH,打造动态色阶
前端·javascript·css
刺客_Andy7 小时前
React 第五十一节 Router中useOutletContext的使用详解及注意事项
前端·javascript·react.js
2501_915918417 小时前
App 上架苹果商店全流程详解 从开发者账号申请到开心上架(Appuploader)跨平台免 Mac 上传实战指南
macos·ios·小程序·uni-app·objective-c·cocoa·iphone
宁雨桥7 小时前
基于 Debian 服务器的前端项目部署完整教程
服务器·前端·debian