- 微信小程序更新提醒uniapp
微信小程序更新提醒uniapp 简介 在小程序开发中,版本更新至关重要。为确保用户始终使用最新版本,我们建议在每次打开小程序时进行版本检测。具体方案如下: 1. 启动时版本检测: 我们使用[代码]uni-app[代码]提供的API[代码]uni.getUpdateManager()[代码],API返回全局唯一的版本更新管理器对象: updateManager,用于管理小程序更新。 2. 新版本提示与更新: 如果检测到新版本,弹出提示框告知用户有新版本可用。 提供“立即更新”选项。 用户选择“立即更新”后,小程序自动下载更新内容。 3. 重启应用新版本: 更新完成后,提示用户确认重启小程序以应用新版本。 [图片] [图片] 摘要 :在小程序开发中,版本更新至关重要。本方案利用 [代码]uni-app[代码] 的 [代码]uni.getUpdateManager()[代码] API 在启动时检测版本更新,提示用户并提供立即更新选项,自动下载更新内容,并在更新完成后重启小程序以应用新版本。适用于微信小程序,确保用户始终使用最新版本。以下是实现步骤: 实现步骤 1 创建更新方法 [代码]App.vue[代码]创建updateApp方法用于检查小程序是否有新版本。 [代码]<script setup lang="ts"> import { onLaunch } from '@dcloudio/uni-app' import { useAppStore } from './stores/app' import { useUserStore } from './stores/user' const appStore = useAppStore() const { getUser } = useUserStore() // #ifdef MP-WEIXIN const updateApp = () => { const updateManager = uni.getUpdateManager(); updateManager.onCheckForUpdate(function (res) { // 请求完新版本信息的回调 console.log(res.hasUpdate); }); updateManager.onUpdateReady(function (res) { uni.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success(res) { if (res.confirm) { // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 updateManager.applyUpdate(); } else if (res.cancel) { console.log('用户点击取消,不更新'); } } }); }); updateManager.onUpdateFailed(function (res) { // 新的版本下载失败 uni.showModal({ title: '已经有新版本了哟~', content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~', }) }); } // #endif onLaunch(async () => { await appStore.getConfig() // #ifdef MP-WEIXIN updateApp() // #endif await getUser() }) </script> <style lang="scss"> // </style> [代码] 2 测试 添加编译模式,选择编译设置,选择成功状态进行模拟。 [图片]
2024-10-16 - 个人开发者把小程序发布到 App Store 5个步骤(保姆级教程)
用完即走,小程序已经成为连接用户与服务的重要桥梁,无论是购物、出行还是娱乐,小程序都以其便捷性和高效性赢得了用户的青睐。 那小程序是否有边界,能否拓展到 App? 微信开发者工具的最新升级,为这一问题提供了创新的解答。现在,开发者们可以轻松将已有的小程序代码构建为全功能的 App,而无需从零开始开发,这不仅节省了大量的人力和时间成本,更为小程序开发者们打开了通往 App Store 巨大流量的大门。 在这篇文章中,我们将深入探讨微信开发者工具支持小程序 App 化的五大步骤,分析其潜在价值,并通过实际案例来展示这一过程的效果。 背景:个人开发者,将小程序代码构建为 iOS App,以下为整体流程,适合 iOS 开发 / 上架零基础的同学。 [图片] 缘起 一个周末,突然奇想,我还没有搞过 App,要不搞搞玩玩😄 从 0 开始学还是很慢的,毕竟时间有限,好在了解过提示工程 [代码]《ChatGPT 进阶:提示工程入门 陈颢鹏 李子菡》[代码],问了一下助手 ChatGPT 几个常见的问题。 开发适用于 iPhone 的 App 的流程是什么 注册开发者账号 -> 下载 Xcode -> 学习 Swift -> 设计 + 编码 + 测试 -> TestFlight 内测 -> 准备上架 (准备) -> 提交审核 -> 应用上架 -> 应用更新和维护 很好,请给出可运行的应用实例,完成查询本机 IP 地址 我是一个新手,请问在 XCode 中如何运行你提供的代码 几轮对话后,大约用了 1 个小时,一个 iOS Demo 在模拟器上跑成功了,有点意思😄 不过功能有点简单,几年前用 Vue 写过一个还在跑的网站,同时我知道 ChatGPT 的编程能力,于是我丢了一个问题给他。 [代码]你是一个开发,会 Vue 、iOS 开发(使用 SwiftUI 框架 )两种语言,现在需要你根据 Vue 的代码重写为 iOS 代码,以下是 Vue 代码 [代码] [图片] 笔者是一个运维平台的产品,为了不忘记运维场景和技能,自己维护一个业务场景,开发语言:golang + Vue,部署架构:腾讯云 CLB+TKE+ 服务网格,运营系统:CLS+ 云函数 +Kafka+Doris+Flink+Hadoop+Streamsets。 结果惊奇的发现,真的可以执行,不服不行 👍 [图片] 1. 转折:把小程序代码构建为 iOS App 测试包(1 小时) 如果仅仅只是这样,那么这篇文章标题就该叫“GPT 如何将 Vue 改写为 App”。 万万没想到,过了 2 周从朋友那里获悉 微信开发者工具可以直接将小程序代码构建为 App,就像 Golang 一样,可以通过参数 [代码]GOARCH[代码] 控制构建的程序是在跑在 [代码]amd64[代码],还是 [代码]arm64[代码] CPU 架构上。 [图片] 现实就是这么巧,几年前使用 Vue 开发站点时,同时也开发了同款小程序。 有点意思,参照文档 构建你的第一个应用 花了 1 个小时,在我的 iPhone 上跑了 测试版 的 App. [图片] 此处应该给多端应用的产品和开发点个赞👍🏻 搞到这里,我其实进入了这款的第一个哎哈时刻,确实很爽,因为我不需要花心思用 GPT 来迁移 Vue 程序,直接用微信开发者工具构建为 App 即可,交互完全一致。 另外记录构建过程中遇到的两个问题 问题 1:小程序的图片在 App 中无法渲染 启用 Media SDK 即可 [图片] 问题 2:App 带有 Vconsole 入口 一开始以为在模拟器中才有,最后发现是一个配置,需要自己主动关闭。 [图片] 2. 构建正式包 谁不想在 App Store 能搜到自己的 App 呢,第二步,构建正式包。 2.1 准备苹果开发者账号 在 MacBook Air 或 iPhone 中安装 Apple Developer,然后注册苹果开发者账号 [图片] 一年 688 元会费 [图片] 正常情况下,交完会费后,第二天会收到一封欢迎加入 Apple Developer Program 的邮件,代表苹果开发者账号注册成功。 很遗憾,我注册时提示“未知错误,请再试一次” 找 Apple Developer 客服反馈,最后答复 [代码]由于一个或多个原因,您无法完成 Apple Developer Program 的注册。我们目前无法继续处理您的注册。[代码]。 好吧,估计是被风控命中了,于是找了家人的账号来注册,直接成功😄 2.2 生成 Bundle ID/ 证书 /Profile 生成 App 备案和构建正式包都需要的 Bundle ID/ 证书 /Profile。 生成 Bundle ID Bundle ID 是一个唯一的标识符,用来识别你的应用程序。它通常采用反向域名格式,例如 com.example.myapp。在开发和发布应用程序时,你需要在苹果的开发者账户中注册一个 Bundle ID,这样苹果的服务才能识别出你的应用程序。 参照 文档 生成 Bundle ID。 生成 证书 /Profile 证书(Certificates)用于建立开发者的身份,并确保应用是由已注册的开发者发布的。开发者需要从苹果开发者中心申请证书,用来对应用进行签名,这样 iOS 设备才会信任并运行这个应用程序。 配置文件(Provisioning Profiles)是一个包含证书、应用程序 ID、设备 ID 和其他信息的文件,它告诉 iOS 设备一个应用程序可以被安装和运行。配置文件将应用、开发者和设备联系起来,控制哪些设备可以安装和运行你的应用程序。 参照 文档 生成 iOS 证书和 Provisioning Profile。 [图片] 拓展资料:创建证书签名请求 问题:申请的 iPhone Distribution 证书不受信任 导入 Apple WWDRCA 证书 即可,可能原因:大致是分发的根证书没有导入你的 Mac 上。 更多资料详见 Apple PKI。 [图片] 2.3 备案(10 天 +) App 如果没有备案,在中国大陆将无法上架,这是苹果官方的说明。 中国工业和信息化部(MIIT)要求 App 必须具备有效的互联网信息服务提供者(ICP)备案号,了解更多 [图片] 其实备案比较简单,参照 App 备案 ,使用上一部分申请的 [代码]Bundle ID[代码]、证书(可查看 [代码]公钥[代码]、[代码]签名 MD5 值[代码])即可,不需要把 App 开发完,再来备案。 备案最长需要 20 个工作日,笔者用了 10 个工作日,在一个周五的下午收到了工信部发来的备案通过短信。 2.4 创建移动应用 移动应用是为了让 App 能用上微信的能力(比如分享到朋友圈或发送给朋友、微信登录 / 支付等),在移动应用中同时登记了 Bundle ID 和 Universal Links,这将会传递给下一步的多端框架,这是构建可正式包(采用苹果的分发证书)的必备条件。 先介绍一下 Universal Links。当用户使用 iPhone 手机访问你的网站,同时安装了 App 时,能在网站顶部快速跳转到 App。具体可以看下苹果官方的文档 Supporting associated domains 你需要有一个网站,未来要放 Universal Links 要用到的 [代码]apple-app-site-association[代码] 文件,不过对于我来说,这个功能好像用处不大,我更需要的是当用户用 iPhone 访问网站,引导他去 Apple Store 安装 App. 这里有一个关键信息,如果你不需要微信支付 / 微信登录 / 微信卡券的能力,不需要做开发者认证(开发者认证不能是个人主体) 访问 微信开放平台,创建移动应用,提交审核,几个小时就审核通过了。 [图片] 2.5 绑定多端框架 在 Donut 开发平台 中将 多端应用绑定上一步创建的移动应用,这样可以用到移动应用中登记的 Bundle ID 和 Universal Links,官方这么做比较合理,关键信息必须通过移动应用这关人工审核来起到一定的约束。 [图片] 绑定后,在多端应用中可以看到 Bundle ID 和 Universal Links 了。 [图片] 2.6 准备 App icon 等资料 App Icon 先用工具为你的 App 设计一个 1024px X 1024px 的图标,然后在 App Icon Generator 上生成 iPhone 所有规格的图标,之后在 [代码]project.miniapp.json[代码] 配置。 [图片] 启动图片 App 启动一般需要 2~4 秒,如果没有启动图片是白屏,用户会有点慌,不知道当前 App 是否正在启动,启动图片就是解决这个问题,同时在启动图片中传达 App 的价值主张。 我是直接用 Sketch 设计的,分辨率为 1290px x 2796px,这是兼容性最强的 6.7 寸(iPhone 15 Pro Max/15 Plus/14 Pro Max)手机的分辨率。 考虑到启动图片在不同机型上的兼容性,如果你用 Xcode 开发,苹果官方会推荐使用 Launch Screen Storyboard 隐私信息访问许可描述 小程序虽然没有用到摄像头、麦克风等权限,但多端的 SDK 中有(具体详见 Donut 官方文档 上架应用市场常见问题),所以得提前申明,不然把包通过 [代码]Transporter[代码] 上传后,会收到苹果发出的不合规邮件。 [图片] 以下是根据苹果官方打回的邮件中定义的隐私信息访问许可描述,应该是最基础的了,可以贴到你的 [代码]project.miniapp.json[代码] 文件中(用编辑器打开)。 [代码]{ "privateDescriptions": { "NSBluetoothPeripheralUsageDescription": "为了提供完整的功能,我们的应用程序需要访问蓝牙外设。这将用于与其他设备进行通信和数据交换。我们承诺保护用户隐私和数据安全。", "NSMicrophoneUsageDescription": "为了提供完整的功能,我们的应用程序需要访问麦克风。这将用于录制音频和进行语音交互。我们承诺保护用户隐私和数据安全。", "NSCalendarsUsageDescription": "为了提供完整的功能,我们的应用程序需要访问日历。这将用于提醒和日程管理。我们承诺保护用户隐私和数据安全。", "NSLocationAlwaysAndWhenInUseUsageDescription": "","NSBluetoothAlwaysUsageDescription":" 为了提供完整的功能,我们的应用程序需要始终访问蓝牙外设。这将用于与其他设备进行通信和数据交换。我们承诺保护用户隐私和数据安全。","NSPhotoLibraryUsageDescription":" 为了提供完整的功能,我们的应用程序需要始终访问相册。这将用于 IP 查询时显示 ISP 的图标。我们承诺保护用户隐私和数据安全。","NSCameraUsageDescription":" 为了提供完整的功能,我们的应用程序需要访问摄像头。这将用于录制视频。我们承诺保护用户隐私和数据安全。","NSLocationWhenInUseUsageDescription":" 为了提供完整的功能,我们的应用程序需要在使用时访问位置信息。这将用于提供定位服务和相关功能。我们承诺保护用户隐私和数据安全。" } } [代码] 2.7 构建正式版版本包 参照 打包生成 IPA 生成正式版的版本,注意使用分发证书。 [图片] 报错:file must be in miniprogram project 解决:把 mobileprovision 放在 miniprogram 目录下,因为 profile 不像 App icon 一样会自动上传到 miniprogram/ 目录下。 2.8 使用 Transporter 上传版本 参照 官方文档 上传正式版的 APK 包。 [图片] 遇到问题: Transporter,无法为 App “comxxxx.ipa” 创建临时 .itmsp 软件包。No suitable application records were found. Verify your bundle identifier ‘com.xxxx’ is correct and that you are signed into Xcode with an Apple ID that has access to the app in App Store Connect. [图片] 解决办法:去 App Store Connect 添加 App,绑定 [代码]Bundle id[代码],这样 Transporter 可以验证包在 App Store Connect 中已注册。 3. 使用 TestFlight 测试 在 App Store Connect 的 TestFlight 页面,可以选择内部、外部测试,外部测试版本需要 Apple 官方审核,把 公开链接发给朋友即可。 [图片] 在测试的同时,可以同步准备上架 App Store 的资料了。 4. 准备上架 Apple Store 审核资料 截屏 截屏是用来在 App Store 中显示你的 App 产品介绍页的,具体参照 截屏规范 [图片] 有 [代码]iPhone 15 Plus[代码] 和 [代码]iPhone 8 Plus[代码] 这两款机型就足够了,其他型号的手机能复用,分辨率应该是等比率缩放。 如果你像我一样,没有这两款手机,那用 iOS 模拟器。 Xcode -> 工具栏 Windows -> Devices and Simulators -> Create a new simulator -> Download more simulator runtimes [图片] 在微信开发者工具中运行这两款模拟器,利用模拟器自带截屏工具即可。 隐私政策 找一下常见 App 的隐私政策,在其产品介绍页中可以跳转过去。 如果你有网站就放在网站上,如果没有可以放在腾讯文档上。 [图片] 选择 App 供应的地区范围 哪些地区的用户可以下载你的 App。 [图片] 提交审核 一切准备好了后(包含备案),开启提交审核。 下午 5:35 提交审核,第二天早上 3:40 上架成功。✌🏻 [图片] 5. App Store 的数据 上架后刚好一周,看看最近一周的数据,还不错。 [图片] 这是评分数据 [图片] 6. 引流 二维码引流:草料二维码 通过草料二维码生成 App 的下载链接,放在网站上,引导用户跳转至 App。 Universal Links 参照 Apple 官网文章 Supporting associated domains 准备 Universal Links。 前面已经介绍了这个东东是干嘛的。 准备 [代码]apple-app-site-association[代码] 文件,放在网站的 [代码].well-known[代码] 目录下,完整路径为 [代码]/.well-known/apple-app-site-association[代码] 以下为示例,特别注意的是 [代码]appID[代码] 是由 [代码]团队 ID[代码] + [代码]Bundle ID[代码] 组成。 [代码]{ "applinks":{"apps":[], "details":[ { "appID":"<team_id>.<bundle_id>", "paths":["*"] } ] } } [代码] team_id 从 开发者账户 中获取 [图片] 顶部导航 当用户访问网站时,顶部引导用户跳转到 App 下载页。 等有空了搞搞。 7. 后记 小程序转 App,让个人或企业可以快速拥有 App,获取应用市场的流量,让开发者把精力放在业务逻辑上。 同时在开发小程序的过程中,发现开发者生态会散落在多个地方,比如 github,提供一些小程序模版、组件等能力,无法集中在一个地方比较方便的找到整个开发者生态的能力,和 VSCode 插件生态有点区别。 [图片] 先说 IDE 插件,比如我用 GPT4-Turbo 来写先代码或排查问题会在微信开发者工具和 Web 间跳转,操作流不太顺,如果能在微信开发者工具的插件入口中找到对应的 AI 代码助手,用起来应该很爽。 一旦平台的开放能力放出来,这些能力将源源不断的涌入到这个市场中,而不是作为平台方来集成这些能力,毕竟精力有限,同时还不一定做的最好,用插件可以让用户有更多的选择。 再说说 小程序组件,以大模型为例,目前市场有备案的大模型基座模型有好几家,在小程序开发过程中其实比较缺整体组件(UI + 背后的 API),有点像商场一样,平台方构建开放的能力,引导各个供应商提供开箱即用的能力,让用户可以快速上手,赶上这波大模型的技术趋势。 比如我自己在设计开放能力时的思考,平台专注骨架功能的开发,让开发者能参与到平台的建设中来,把生态盘活起来,最终提升大家研发运营的效率。 最后就是管理后端比较分散,比如 开放平台、donut、we 分析、云测、云托管,云开发,产品矩阵看不清,不容易知道整体的能力,缺少一个集中的控制台。 最后希望小程序越来越好 😄
2024-01-30 - 岁寒之松柏:小程序skyline渲染引擎初尝试
小程序架构介绍 我们都知道小程序本质上是运行在安卓端,苹果端的混合APP,只是微信提供了一套JSBridge,方便用户对一些原生功能和微信相关的功能的进行调用。而微信为了安全和性能的需要,一改以往网络架构中的单线程架构,改为小程序的双线程架构。分别是AppServie 和 Webview 两个线程,我们在小程序中编写的JS代码就是运行在AppService线程的JSCore引擎(类似V8 引擎,一个Js解释器)中,而我们的Wxml和Wxss则会依赖WebView线程进行渲染。 [图片] 目前架构存在的问题 这样的架构虽然已经极大了提高了webview的渲染性能,但是依然会存在一些问题比如: 当页面节点数目过多,很容易发生卡顿 当我们新建一个页面,就要新建一个Webview进行渲染 页面之间共享资源,需要使用Native进行通信,就会消耗更多性能 当AppService(逻辑层)与Webview(视图层)通信也需要依赖Native 所以为了解决这些问题小程序推出Skyline渲染引擎 Skyline引擎介绍 在Skyline环境中,Skyline 会创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。说白了就是之前的样式计算是放到渲染线程来处理,现在把和样式相关的逻辑也放到AppService线程中处理,个人猜测这个渲染线程很有可能很有可能就是flutter,这样的架构就极大减少内存的消耗,和线程上通信时间的消耗。原本wxs中的逻辑,也可以移到Appservice线程中运行 [图片] 使用Skyline引擎的使用步骤 在app.json 文件添加 [代码]"lazyCodeLoading": "requiredComponents"[代码] 属性,这是因为Skyline 依赖按需注入的特性。 [代码] { "pages": [ "pages/index/index", "pages/logs/logs", "pages/test/test" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "sitemapLocation": "sitemap.json", // 在 app.json 文件添加 "lazyCodeLoading": "requiredComponents" } [代码] 在全局或页面配置中声明为 Skyline 渲染,即 app.json 或 page.json 配上[代码]"renderer": "skyline"[代码] Skyline 不支持页面全局滚动,需在页面配置项加上 [代码]"disableScroll": true[代码],在需要滚动的区域使用scroll-view 实现 Skyline 不支持原生导航栏,需在页面配置项加上 [代码]"navigationStyle": "custom"[代码],并自行实现自定义导航栏 [代码] { "usingComponents": {}, // 在 app.json 文件添加或者page页面的json中添加 "disableScroll": true, "navigationStyle": "custom" //也可以放在App.json文件中 "renderer": "skyline" } [代码] 组件适配,参考 Skyline 基础组件支持与差异 WXSS 适配,参考 Skyline WXSS 样式支持与差异 在本地设置中勾选Skyline渲染调试,如果看不到这个选项框,看一下是否在app.json中配置了[代码]"renderer": "skyline"[代码] [图片] Skyline的 worklet 动画介绍 小程序采用双线程架构,渲染线程(UI 线程)和逻辑线程(JS 线程)分离。[代码]JS[代码] 线程不会影响 [代码]UI[代码] 线程的动画表现,如滚动效果。但引入的问题是,[代码]UI[代码] 线程的事件发生后,需跨线程传递到 [代码]JS[代码] 线程,进而触发开发者回调,当做交互动画(如拖动元素)时,这种异步性会带来较大的延迟和不稳定,[代码]worklet[代码] 动画正是为解决这类问题而诞生的,使得小程序可以做到类原生动画般的体验 worklet函数定义 [代码]function helloWorklet() { 'worklet'; //'worklet'声明该函数为work函数,可以在js线程和UI线程中调用 console.log('hello worklet'); } Page({ onLoad(options) { helloWorklet('hello') // print: hello wx.worklet.runOnUI(helloWorklet)() }, }) [代码] 在小程序控制台可以看到如下输出 [图片] 如果看见SkylineGlobal is not defined错误看看是否开启了Skyline渲染调试 [图片] worklet函数间的相互调用 [代码]function slave() { 'worklet'; return "I am slave" } function master() { 'worklet'; const value = slave() console.log(value); } [代码] 从 UI 线程调回到 JS 线程 [代码]const {runOnUI ,runOnJS} = wx.worklet function jsFun(message) { // 普通函数不需要声明为worklet console.log(message) } function uiFun() { 'worklet'; runOnJS(jsFun)('I am from UI') } [代码] 使用shared共享数据 由worklet函数捕获的静态变量,会在编译期间序列化后生成在UI线程的拷贝环境之中,这就导致我们在JS线程中后续更新了变量,但是在UI线程中时得不到最新的数值的。 [代码]const obj = { name: 'skyline'} function someWorklet() { 'worklet' console.log(obj.name) // 输出的仍旧是 skyline } obj.name = 'change name' wx.worklet.runOnUI(someWorklet)() [代码] 因此shyline使用shared来实现线程之间数据的共享 [代码]const { shared, runOnUI } = wx.worklet const offset = shared(0) function someWorklet() { 'worklet' console.log(offset.value) // 输出的是新值 1 } offset.value = 1 runOnUI(someWorklet)() [代码] 简单案例–实现探探的卡片功能 注意:编辑器版本:1.06.2303162 基础库版本:2.30.2 先看效果 [图片] 代码如下 <br> wxml 代码 [代码]<navigation-bar title="探探" /> <view class="page"> <block wx:for="{{containers}}" wx:key="*this"> <pan-gesture-handler data-id="container-{{index}}" onGestureEvent="handlePan"> <view id="container-{{index}}" class="container" style="z-index: {{zIdnexes[index]}};background-image: url({{partContentList[index]}});"> </view> </pan-gesture-handler> </block> </view> [代码] scss代码 [代码].page{ display: flex; justify-content: center; align-items: center; height: 100vh; width: 100vw; position: relative; .container{ height: 80vh; width: 95vw; background-color: burlywood; position: absolute; border-radius: 16rpx; display: flex; justify-content: center; align-items: center; background-size: cover; .image{ display: block; height: 1067rpx; width: 712rpx; margin: 0 0; } } } [代码] 核心逻辑 [代码]import { useAnimation, setAni, Animation, GestureState } from "./method" Page<{ pos: Animation }, any>({ /** * 页面的初始数据 */ data: { containers: [ "burlywood", "blue", "cyan", "black" ], zIdnexes:[], current:0, partContentList:[] }, /** * 生命周期函数--监听页面加载 */ onLoad() { this.initNode() // 当前node的下标 this.active = wx.worklet.shared(0) // 当前contentList的下标 this.current = wx.worklet.shared(0) this.zIndex = 100000 }, initNode() { // 用与保存shared值 this.Nodes = {} // 图片文件 this.contentList = [ "https://i.hexuexiao.cn/up/ca/63/4a/a32912fc26b8445797c8095ab74a63ca.jpg", "https://th.bing.com/th/id/OIP.kSrrRGx6nqOgWzbaEvVD9AHaNK?pid=ImgDet&rs=1", "https://img.zmtc.com/2019/0806/20190806061552744.jpg", "https://img.zmtc.com/2019/0806/20190806061000600.jpg", "https://img.ratoo.net/uploads/allimg/190523/7-1Z5231J058.jpg", "https://th.bing.com/th/id/R.47de9dfcc25d579d84850d4575d24a6a?rik=%2fGkmrewzIEY4Iw&riu=http%3a%2f%2fimg3.redocn.com%2ftupian%2f20150930%2fqizhimeinvlisheyingtu_5034226.jpg&ehk=rG9Ks2QRzj81mZl38gVGmWVAgCHVLWppoDezpfwdxjo%3d&risl=&pid=ImgRaw&r=0", "https://th.bing.com/th/id/R.95f8e6f6bd5b660ae3ad4f3e0d712276?rik=ELKcha%2bE5ryuiw&riu=http%3a%2f%2f222.186.12.239%3a10010%2fwlp_180123%2f003.jpg&ehk=mVN7AzIRR%2fmVPJYWrWOFbEiher3QWtwSdH%2f%2fe4lE7n8%3d&risl=&pid=ImgRaw&r=0" ] this.data.containers.forEach((_: string, index: number) => { if (index == 0) { this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 0 }, this) this.setData({ [`zIdnexes[${index}]`]:100000-index, [`partContentList[${index}]`]:this.contentList[index] }) } else { console.log("10123") this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 20, scale: 0.95 }, this) this.setData({ [`zIdnexes[${index}]`]:100000-index, [`partContentList[${index}]`]:this.contentList[index] }) } }); }, handlePan(evt: any) { "worklet"; console.log(evt) const now = this.Nodes[`#container-${this.active.value}`] as Animation const next = this.Nodes[`#container-${(this.active.value+1)%4}`] as Animation if (evt.state == GestureState.ACTIVE) { // 滑动激活状态 // 设置当前的滑动块 now.x.value += evt.deltaX now.y.value += evt.deltaY now.rotate.value = now.x.value * 10 / 360 // 设置下一个滑动块 let rate = Math.abs(now.x.value) / 150 rate = rate > 1 ? 1 : rate next.y.value = (20 - rate * 20) < 0 ? 0 : (20 - rate * 20) next.scale.value = 0.95 + rate * 0.05 } if (evt.state == GestureState.END) { // 滑动结束 if (Math.abs(now.x.value) < 150) { // 判断是否超过界限值 setAni(now.x, 0) setAni(now.y, 0) setAni(now.rotate, 0) } else if (now.x.value < 0) { // 判断判断左划还是右划 setAni(now.x, -2000) setAni(now.y, -2000) setAni(now.rotate, 0) // 通知js线程进行数据的更新 wx.worklet.runOnJS(this.toNext.bind(this))() } else if (now.x.value > 0) { setAni(now.x, 2000) setAni(now.y, -2000) setAni(now.rotate, 0) wx.worklet.runOnJS(this.toNext.bind(this))() } } }, // 将当前序号的跳转到下一个 toNext(){ const current = this.current.value+1 this.active.value = current%4 this.current.value = current this.setData({ current }) if(current-2>=0){ wx.worklet.runOnUI(this.toReset)((current-2)%4) this.setData({ [`zIdnexes[${(current-2)%4}]`]:99998-current, [`partContentList[${(current-2)%4}]`]:this.contentList[current+2] }) } }, // 将动画归位 toReset(index:number){ "worklet"; const reset = this.Nodes[`#container-${index}`] as Animation setAni(reset.x, 0,0) setAni(reset.y, 20,0) setAni(reset.rotate, 0,0) setAni(reset.scale, 0.95,0) } }) [代码] 参考 skyline worklet 动画
2023-03-20 - 小程序瀑布流的一种实现
在小程序中由于图片组件为固定宽高,所以无法像网页中一样简单的实现瀑布流布局 但是image组件可以设置mode为widthFix,为图片设置固定高度后,可以自适应高度 如果是两栏瀑布流布局 就先定义两个view,让其float: left(高度不会随父组件高度) 获取到新数据后,循环新数据,通过 wx.createSelectorQuery() 获取两个view的高度,将数据中的一项push入矮的一项 不断循环添加子项就能够达到瀑布流的效果 注意事项: 在瀑布流最后的外面要清除浮动,防止影响后面的布局 [图片] 在设置单项时,要在setData的回调方法中再去执行下一项,保证能够让下次获取的高度正确 [图片] 最后效果如下 [图片][图片] 代码片段:https://developers.weixin.qq.com/s/d9MnuWmw7WGm
2023-03-24 - 通过WXS实现回弹的平滑滚动容器
前言 最近在愉快的开发微信小程序的时候碰到了一个体验需求,需要在 Android 侧的滚动也需要带回弹效果,类似于在 Web 端可以使用的 better-scroll,查阅微信小程序内置组件 [代码]scroll-view[代码] 无法满足这种场景,没办法,需求得做呀,只能自己动手撸了! 在微信小程序中,我们可以通过 WXS响应事件 来替代逻辑层处理从而有效的提高交互流畅度,其中使用到的 WXS语法 也是非常类似我们非常熟悉 JavaScript,不过很多的 JavaScript 高级语法在 WXS 模块中不能使用,具体可以点击链接进入微信小程序提供的文档。 思路 以横向滚动为例,内容的宽度大于容器的宽度时可以发生滚动,如图 [图片] 接着通过监听三个触摸事件[代码]touchstart[代码]、[代码]touchmove[代码]、[代码]touchend[代码]来实时的改变 content 的 CSS translate,从而从视觉上达到滚动的目的。 WXS 示例 我们先从一个简单的 WXS 使用示例来了解回顾一下使用方式,WXS 的模块系统类似 CommomJS 规范,使用每个模块内置的 [代码]module[代码] 对象中的 [代码]exports[代码] 属性进行变量、函数导出: [代码]// helper.wxs module.exports = { // 注意 WXS 模块中不支持函数简写 touchstart: function touchstart() { console.log('touchstart called') } } [代码] [代码]<!-- index.wmxl --> <!-- module 为模块名,可按规范任意取名 --> <wxs src="./helper.wxs" module="helper" /> <!-- 与普通的逻辑层事件不同,这里需要加上 {{}} --> <view bind:touchstart="{{ helper.touchstart }}">view</view> [代码] 这样就给 [代码]view[代码] 绑定了一个 [代码]touchstart[代码] 事件,在事件触发后,会在控制台打印出字符串 "touchstart called" 好了,现在正式进入滚动容器的逻辑实现 开工 新建 [代码]scroll.wxml[代码] 文件,准备符合上图中结构的 WXML 内容来构造出一个正确的可以滚动条件 [代码]<!-- scroll.wxml --> <!-- 即图中的 container --> <view class="container" style="width: 100vw;"> <!-- 即图中的 content --> <view class="content" style="display: inline-block; white-space: nowrap;"> <view wx:for="{{ 10 }}" wx:key="index" style="width: 200rpx; height: 300rpx; border: 1px solid; display: inline-block;">{{ item }}</view> </view> </view> [代码] 新建 [代码]scroll.wxs[代码] 文件,里边用于存放我们实现滚动的所有逻辑 接下来进行初始化操作,首先需要获取到 container 和 content 组件实例,在上一节 “WXS 示例” 中我们知道可以通过在组件中触发一个事件来调用 WXS 模块中的方法,但有没有什么方式可以不用等到用户来触发事件就可以执行吗? 通过阅读 WXS 响应事件 文档,可以了解到,另外一种调用 WXS 模块方法就是可以通过 [代码]change:[prop][代码] 监听某一个组件的 Prop 的改变来执行 WXS 模块中指定的方法,且这个方法会立即执行一次,如下面一个示例 [代码]// helper.wxs module.exports = { setup: function setup() { console.log('setup') } } [代码] [代码]<!-- index.wxml --> <wxs src="./helper.wxs" module="helper"></wxs> <!-- 例如我们指定一个 prop 为 prop1,值为 {{ prop1Data }} --> <!-- 通过 change:prop1 语法对这个 prop 的变化进行监听 --> <view prop1="{{ prop1Data }}" change:prop1="{{ helper.setup }}"></view> [代码] [代码]// index.js Page({ data: { prop1Data: {} } }) [代码] 上面示例中,在页面初始化或 [代码]prop1Data[代码] 发生改变时(准确来说是在逻辑层对 [代码]prop1Data[代码] 调用了 [代码]setData[代码] 方法后,即使 [代码]prop1Data[代码] 的内容不变化),都会调用 [代码]hepler.wxs[代码] 模块中的 setup 方法。 现在我们可以通过 [代码]change:prop[代码] 会立即执行一次的特点,来对我们的滚动逻辑进行一次初始化操作 [代码]// scroll.wxs var exports = module.exports // 页面实例 var ownerInstance // container BoundingClientRect var containerRect // content 实例,通过此实例设置 CSS 属性 var slidingContainerInstance // content BoundingClientRect var slidingContainerRect // X方向的最小、最大滚动距离。如 -200 至 0(手势往右时,元素左移,translateX 为负值) var minTranslateX var maxTranslateX = 0 /** * @param newValue 最新的属性值 * @param oldValue 旧的属性值 * @param ownerInstance 页面所在的实例 * @param instance 触发事件的组件实例 */ exports.setup = function setup(newValue, oldValue, _ownerInstance, instance) { ownerInstance = _ownerInstance containerRect = instance.getBoundingClientRect() slidingContainerInstance = ownerInstance.selectComponent('.content') slidingContainerRect = slidingContainerInstance.getBoundingClientRect() minTranslateX = (slidingContainerRect.width - containerRect.width) * -1 } [代码] [代码]<!-- scroll.wxml --> <wxs src="./scroll.wxs" module="scroll" /> <!-- 因本案例只利用 change:[prop] 首次执行的机制,传递的给 _ 的参数是个对象字面量 --> <view class="container" style="width: 100vw;" _="{{ { k: '' } }}" change:_="{{ scroll.setup }}" bind:touchstart="{{ scroll.touchstart }}" bind:touchmove="{{ scroll.touchmove }}" bind:touchend="{{ scroll.touchend }}" > <view class="content" style="display: inline-block; white-space: nowrap;"> <view wx:for="{{ 10 }}" wx:key="index" style="width: 200rpx; height: 300rpx; border: 1px solid; display: inline-block;">{{ item }}</view> </view> </view> [代码] 完成基本的跟随手指移动 [代码]// scroll.wxs var exports = module.exports // 页面实例 var ownerInstance // container BoundingClientRect var containerRect // content 实例,通过此实例设置 CSS 属性 var slidingContainerInstance // content BoundingClientRect var slidingContainerRect // X方向的最小、最大滚动距离。如 -200 至 0(手势往右时,元素左移,translateX 为负值) var minTranslateX var maxTranslateX = 0 /** * @param newValue 最新的属性值 * @param oldValue 旧的属性值 * @param ownerInstance 页面所在的实例 * @param instance 触发事件的组件实例 */ exports.setup = function setup(newValue, oldValue, _ownerInstance, instance) { ownerInstance = _ownerInstance containerRect = instance.getBoundingClientRect() slidingContainerInstance = ownerInstance.selectComponent('.content') slidingContainerRect = slidingContainerInstance.getBoundingClientRect() minTranslateX = (slidingContainerRect.width - containerRect.width) * -1 } // 实时记录 content 位置 var pos = { x: 0 } // 记录每次触摸事件开始时,content 的位置,后续的移动都是基于此值增加或减少 var startPos = { x: 0 } // 记录触摸开始时,手指的位置,后续需要通过比较此值来计算出移动量 var startTouch = { clientX: 0 } function setTranslate(pos0) { slidingContainerInstance.setStyle({ transform: 'translateX(' + pos0.x + 'px)' }) pos.x = pos0.x } exports.touchstart = function touchstart(event) { startTouch.clientX = event.changedTouches[0].clientX startPos.x = pos.x } exports.touchmove = function touchmove(event) { var deltaX = event.changedTouches[0].clientX - startTouch.clientX var x = startPos.x + deltaX setTranslate({ x: x }) } exports.touchend = function touchend() {} [代码] 效果图: [图片] 处理松手后移动超出的情况,需要对其归位: 添加 clamp 工具方法 [代码]// 给出最小、最大、当前值,返回一个在最下-最大范围之间的结果 // 如: -100, 0, -101 => -100 function clamp(min, max, val) { return Math.max(min, Math.min(max, val)) } [代码] 在 touchend 事件中,添加位置校验的逻辑 [代码]// scroll.wxs exports.touchend = function touchend() { setTranslate({ x: clamp(minTranslateX, maxTranslateX, pos.x) }) } [代码] 看看效果: [图片] 回去是能回去了,有点生硬~ 加上松手回弹动画 其中动画可以使用两种实现方式 CSS Transition:在松手后,给 content 元素设置一个 [代码]transition[代码],然后调整 [代码]translateX[代码] 值归位 JS 帧动画:在松手后,利用动画函数不断调整 [代码]translateX[代码] 来进行归位 两种方式通过给相同的动画函数可以达到一样的体验,但 CSS Transition 在我的理解中不太好处理中止的情况,如在动画过程中,又有了新的触摸事件,这里就会产生抖动或未预期到的结果,但 JS 动画可以很简单的应对 因此后续的动画部分打算采用 JS 动画实现,先准备一些动画函数 [代码]// scroll.wxs // 下面内容通过 better-scroll 借鉴 ~ // 可以理解为入参是一个 [0, 1] 的值,返回也是一个 [0, 1] 的值,用来表示进度 var timings = { v1: function (t) { return 1 + --t * t * t * t * t }, v2: function(t) { return t * (2 - t) }, v3: function(t) { return 1 - --t * t * t * t } } [代码] 定义 [代码]moveFromTo[代码] 方法来实现从一个点通过指定的动画函数运动到另一点 [代码]// scroll.wxs /** * @param fromX 起始点xx * @param toX 目标点 x * @param duration 持续时长 * @param timing 动画函数 */ function moveFromTo(fromX, toX, duration, timing) { if (duration === 0) { setTranslate({ x: fromX }) } else { var startTime = Date.now() var disX = toX - fromX var rAFHandler = function rAFHandler() { var progressX = timing(clamp(0, 1, (Date.now() - startTime) / duration)) setTranslate({ x: disX * progressX + fromX }) if (progressX < 1) { ownerInstance.requestAnimationFrame(rAFHandler) } } ownerInstance.requestAnimationFrame(rAFHandler) } } [代码] 调整 touchend 事件处理逻辑,添加归位的动画效果 [代码]// scroll.wxs exports.touchend = function touchend() { moveFromTo( pos.x, clamp(minTranslateX, maxTranslateX, pos.x), 800, timings.v1 ) } [代码] 看看效果: [图片] 看起来达到了目的,再优化一下,在滑动超出边界后,需要给一些阻力,不能滑的“太简单了” 给超边界的滚动加阻力 [代码]// scroll.wxs exports.touchmove = function touchmove(event) { var deltaX = event.changedTouches[0].clientX - startTouch.clientX var x = startPos.x + deltaX // 阻尼因子 var damping = 0.3 if (x > maxTranslateX) { // 手指右滑导致元素左侧超出,超出部分添加阻尼行为 x = maxTranslateX + damping * (x - maxTranslateX) } else if (x < minTranslateX) { // 手指左滑导致元素右侧超出,超出部分添加阻尼行为 x = minTranslateX + damping * (x - minTranslateX) } setTranslate({ x: x }) } [代码] 瞅瞅: [图片] 效果达到了,手指都划出屏幕了,才移动了这么一点距离 到现在已经完成了一个带回弹效果的滚动容器,但还没有做到“平滑”,即在滑动一段距离松手后,需要给 content 一些“惯性”来继续移动一些距离,体验起来就不会那么生硬 加滑动惯性 在这之前,还有一些准备工作需要做 [代码]// scroll.wxs // 记录触摸开始的时间戳 + var startTimeStamp = 0 // 增加动画完成回调 + function moveFromTo(fromX, toX, duration, timing, onComplete) { if (duration === 0) { setTranslate({ x: fromX }) + ownerInstance.requestAnimationFrame(function() { + onComplete && onComplete() + }) } else { var startTime = Date.now() var disX = toX - fromX var rAFHandler = function rAFHandler() { var progressX = timing(clamp(0, 1, (Date.now() - startTime) / duration)) setTranslate({ x: disX * progressX + fromX }) if (progressX < 1) { ownerInstance.requestAnimationFrame(rAFHandler) + } else { + onComplete && onComplete() + } } ownerInstance.requestAnimationFrame(rAFHandler) } } exports.touchstart = function touchstart(event) { startTouch.clientX = event.changedTouches[0].clientX startPos.x = pos.x + startTimeStamp = event.timeStamp } [代码] 因为是在松手后加动量,所以继续处理 touchend [代码]// scroll.wxs exports.touchend = function touchend(event) { // 记录这一轮触摸动作持续的时间 var eventDuration = event.timeStamp - startTimeStamp var finalPos = { x: pos.x } var duration = 0 var timing = timings.v1 var deceleration = 0.0015 // 计算动量,以下计算方式“借鉴”于 better-scroll,有知道使用什么公式的朋友告知以下~ var calculateMomentum = function calculateMomentum(start, end) { var distance = Math.abs(start - end) var speed = distance / eventDuration var dir = end - start > 0 ? 1 : -1 var duration = Math.min(1800, (speed * 2) / deceleration) var delta = Math.pow(speed, 2) / deceleration * dir return { duration: duration, delta: delta } } // 此次滑动目的地还在边界中,可以进行动量动画 if (finalPos.x === clamp(minTranslateX, maxTranslateX, finalPos.x)) { var result = calculateMomentum(startPos.x, pos.x) duration = result.duration finalPos.x += result.delta // 加上动量后,超出了边界,加速运动到目的地,然后触发回弹效果 if (finalPos.x > maxTranslateX || finalPos.x < minTranslateX) { duration = 400 timing = timings.v2 var beyondDis = containerRect.width / 6 if (finalPos.x > maxTranslateX) { finalPos.x = maxTranslateX + beyondDis } else { finalPos.x = minTranslateX + beyondDis * -1 } } } moveFromTo(pos.x, finalPos.x, duration, timing, function () { // 若动量动画导致超出了边界,需要进行位置修正,也就是回弹动画 var correctedPos = { x: clamp(minTranslateX, maxTranslateX, pos.x) } if (correctedPos.x !== pos.x) { moveFromTo( pos.x, correctedPos.x, 800, timings.v1 ) } }) } [代码] 继续看看效果: [图片] 有了有了 只是现在的滚动容器还很“脆弱”,在进行动量动画、回弹动画时,如果手指继续开始一轮新的触摸,就会出现问题,也就是最开始我们在选择 CSS 过渡和 JS 动画考虑到的问题 解决连续触摸滑动问题 在 [代码]moveFromTo[代码] 方法中,添加强制中止的逻辑 [代码]// scroll.wxs + var effect = null function moveFromTo(fromX, toX, duration, timing, onComplete) { + var aborted = false if (duration === 0) { setTranslate({ x: fromX }) ownerInstance.requestAnimationFrame(function () { onComplete && onComplete() }) } else { var startTime = Date.now() var disX = toX - fromX var rAFHandler = function rAFHandler() { + if (aborted) return var progressX = timing(clamp(0, 1, (Date.now() - startTime) / duration)) setTranslate({ x: disX * progressX + fromX }) if (progressX < 1) { ownerInstance.requestAnimationFrame(rAFHandler) } else { onComplete && onComplete() } } ownerInstance.requestAnimationFrame(rAFHandler) } + if (effect) effect() + effect = function abort() { + if (!aborted) aborted = true + } } exports.touchstart = function touchstart(event) { startTouch.clientX = event.changedTouches[0].clientX startPos.x = pos.x startTimeStamp = event.timeStamp + if (effect) { + effect() + effect = null + } } [代码] 体验一下: [图片] 这样一个带回弹的平滑滚动容器就处理的可以使用啦,有问题的地方欢迎大家指出讨论 结尾 完整源码托管在 Github 中:weapp-scroll 其中功能、逻辑更为完善,并同时支持横向、竖向方向的滚动,适合在 Android、PC 场景的使用(毕竟 IOS 侧可以直接使用微信内置组件 [代码]scroll-view[代码]~)。若有帮到希望可以给个星星~ 完~
2023-07-07 - 你想要的微信小程序瀑布流组件库:me-waterfall
介绍 me-waterfall 是一个微信小程序瀑布流组件库,实现简单,侵入性小,贴近 web 端的效果。 线上体验 扫描下方的小程序二维码,体验使用效果: [图片] 安装 方式一:使用 npm 安装(推荐) [代码]npm install me-waterfall [代码] 方式二:下载源码 将源码下载到本地,然后将 [代码]lib[代码] 目录拷贝到自己的项目中。 使用方法 在页面的 [代码]json[代码] 文件或 [代码]app.json[代码] 中引入组件: [代码]{ "usingComponents": { "me-waterfall": "/path/to/me-waterfall/waterfall/index", "me-waterfall-item": "/path/to/me-waterfall/waterfall-item/index" } } [代码] 然后就可以在 [代码]wxml[代码] 中直接使用了: [代码]<me-waterfall> <me-waterfall-item wx:for="{{list}}" wx:key="{{index}}"> <image src="{{item.src}}" style="height:{{item.height}}px;width:100%"/> </me-waterfall-item> </me-waterfall> [代码] API waterfall 组件 props 参数 说明 类型 默认值 是否必须 width 容器宽度,传入后将优先使用此值,呈现速度更快 Number - 否 column 列数 Number 2 否 gap 列与列之间的间距 Number 15 否 methods reflow 重新排列元素,在某些情形下,你可能希望在完成某些操作后对瀑布流进行重新排列,此时可以调用此方法: [代码]const waterfallInstance = this.selectComponent("#waterfall"); waterfallInstance.reflow(); [代码] 外部样式类 参数 说明 类型 默认值 是否必须 custom-class 外部样式类 String - 否 waterfall-item 组件 外部样式类 参数 说明 类型 默认值 是否必须 custom-class 外部样式类 String - 否 关于性能 首先,够用; 其次,由于微信小程序中获取元素尺寸的 api 为回调形式,因此排列内部元素时需要延迟,即等到容器宽度取到之后再进行排列,这会使得瀑布流呈现速度减慢;如果改为传入 [代码]width[代码],呈现速度会更快。 捐赠 如果这个库有帮助,请 Star 这个仓库,让更多人发现它。 当然,也可以鼓励我一下: [图片] 开源协议 本项目基于 MIT 协议。
2022-07-25 - 小程序性能优化实践
小程序性能优化课程基于实际开发场景,由资深开发者分享小程序性能优化的各项能力及应用实践,提升小程序性能表现,满足用户体验。
2024-10-09 - fiddler调试jssdk、小程序抓包
使用fiddler调试jssdk下载地址 https://www.telerik.com/download/fiddler-everywhere 操作流程fiddler配置 [图片] [图片] [图片] 点击options,按图示勾选配置,端口号默认8888 [图片] 右侧面板 AutoResponder 按图示勾选,新增一条规则 // 此处域名为可调试jssdk的地址, 可自定义xxx.edu.dev.faisco.com.cn regex:^http://wx.edu.dev.faisco.com.cn/(?.+)?$ // 此处域名为本机开启的服务地址 http://172.17.1.35:8080/${name} 使用powershell或者cmd,输入ipconfig获取电脑端ip地址 [图片] 手机端设置连接公司wifi,确保手机能直接访问http://172.17.1.35:8080这种本机开启的域名点开wifi设置,新增代理,主机为上一步获取的电脑ip,端口为fiddler设置的端口,默认8888 [图片] 手机访问http://172.17.1.35:8888, 下载fiddler证书并安装 [图片] [图片] 输入第2步正则的域名访问(如:wx.edu.dev.faisco.com.cn),现在访问就相当于访问http://172.17.1.35:8080,可以调试jssdk了 使用fiddler抓包配置信任证书 [图片] 配置网络代理 [图片] 打开电脑版微信小程序,即可抓包 [图片]
2022-06-13 - 添加npm包时无法添加node_modules的问题解决
1.问题: 昨天添加npm包时一直遇到个问题,npm init 和npm i 之后,只生成package.json,不会自动生成node_modules,也就无法构建npm。 2.尝试: 在网上查找各种方案,多次尝试,清缓存,修改config 中的global为false等,一晚上没成功,今天再次调试,终于,现将解决步骤写出,以免下次遇到又懵了。 3.步骤: 第一步 npm初始化,npm init, 然后会生成一个package.json,此时,在project.config.js里面修改如下:"packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./" } ] 第二步 安装想要的组件包npm install weui-miniprogram npm i @vant/weapp -S --production 此时,就会出现node_modules第三步 在工具中找到构建npm,点击完成npm构建
2022-06-09 - 小程序scroll-view翻转后 scroll-into-view的替代方案
背景 腾讯云医小程序有医患聊天会话的场景,由于会话场景存在查询历史消息的场景,小程序中按照常规思路加载历史消息时会出现跳动的问题;跳动的原因是由于在’顶部’插入dom,会使得后面的dom被往后面推,然后重新设置scroll-top或者scrol-into-view从而导页面出现跳动;我们尝试采用【 前端开发中聊天场景的体验优化】文章中的方案处理跳动的场景。该文章的核心观点将scroll-view元素通过设置css样式 transform: rotateX(180deg); 进行翻转,这样将历史消对应的dom结构放在尾部,当添加更多的历史消息(dom)时,由于dom是添加在尾部很优雅的绕过了插入历史消息跳动的场景。但是当我们按照这种方式实现后,发现scroll-view元素提供的scroll-into-view属性不好使了。因此有了本文通过计算scrollTop值设置scrollTop来达到相同目的。 复现该问题的小程序代码片段:代码片段 目前已经反馈给官方(官方已确认是内部组件实现暂不支持翻转的场景 基础知识介绍 计算scrollTop涉及到一些web和小程序的基础知识,后面针对这些基础点进行简单介绍 .scroll-into-view 微信小程序提供的scroll-view元素提供了属性 scroll-into-view,该属性的作用是可以将指定dom滚动到scroll-view可见区域内 [图片] 关于boundingClientRect 下图是MDN解释该属性时提供的,从下图中可以看到top/bottom/left/right的值是元素的左上角和右下角相对于视口左上角的水/垂直距离 [图片] 为了更深入理解这些值。给出了一个简易的demo(代码片段),获取实例元素的的boundingClientRect的值后,可以看到这些值是根据元素的border边界进行计算的 [图片] [图片] [图片] 值得注意的是,当元素处于一个滚动区域内部,left/top值是考虑滚动操作的即包含滚动距离的(参考MDN 另外,当我们把容器元素又或者元素自身设置 transform: rotateX/Y(180deg):不会导致top和bottom的值互换(left与right的值互换); 总会有这样的结论,当dom元素的宽度和高度不为0时,top值一定小于bottom值,left值一定小于right值 关于scrollTop 当一个容器的内容的高度大于其容器高度时,overflow不为visible/hidden时,则会出现滚动条。出现滚动条后,内容区域则可以滚动,此时scrollTop的值是容器可视区域的顶部到内容区域顶部的距离,见下面示意图。 [图片] 值得注意的是,滚动条出现在盒模型中的content区域,见下图滚动条不会覆盖padding/border部分。因此上面说到内容区域高度超过容器高度并不严谨,严谨的说法应该是超过容器的content区域的高度。 [图片] 如果此时给容器设置css样式: transform: rotateX(180deg); 即沿垂直方向翻转180度,scrollTop的值会发生变化吗。 下面我们看下实际的对比效果,为了方便查看滚动条的效果,给滚动条轨道(红色部分)以及滑块(黑色部分)添加了背景色,发现整个元素包括滚动条在内一并进行了翻转。 正常情况(左侧),应用翻转css样式(右侧) [图片] [图片] 翻转后的scrollTop值示意图 [图片] 通过计算scrollTop值来模拟scroll-into-view效果(针对scroll-view翻转的场景j) 由于boundingClinetRect的值是包含border边界的,因此当数据项包含padding,border等区域不会影响这里的计算过程,可以认为下面示意图中的数据项部分的边界是border边界; 由于滚动条是出现在content区域,因此容器元素的的border-top/padding-top不为0时,会影响计算流程,因此这里分为两种情况进行介绍: 2.1 假设scroll-view元素的的border-top/padding-top为0 2.2 假设scroll-view元素的的border-top/padding-top不为0 border-top/padding-top为0的情况 为了方便说明计算过程,我定义三种状态,初始态、中间态、最终态 示意图中的区域说明 白色背景的为视口, 绿色背景的是容器(scroll-view)的可视区域, 灰色区域是内容区域,并且内容区域的高度超过了容器的高度, 红色区域是一个数据项 [图片] 现在的目标是将数据项从初始态滚动到最终态即scroll-into-view的效果:border的上边界与可视区域上边界对齐 第一步:从初始态达到中间态 根据上面关于scrollTop的描述,这里如果scrollTop的值是targetDistance即数据项的底部到内容区域的底部的距离,就可以达到中间态,因此现在的目标是求targetDistance 初始状态的已知变量 初始状态下的的scrollTop值:currentScrollTop (由于容器发生翻转,所以scrollTop视觉上指向容器下方) 数据项的boundingClientRect.bottom为 itemBottom 容器的boundingClientRect.bottom为 contianerBottom 通过示意图很容易得出 [代码]targetDistance = currentScrollTop + (containerBottom - itemBottom) [代码] 第二步:从中间到达最终态 已知变量:容器高度:containerHeight、数据项高度:itemHeight 最终态是数据项的顶部距离容器顶部,从示意图中看到中间态到最终态的scrollTop是减少了的,减少的值其实就是cotainerHeight - itemHeight 经过第一步和第二步我们就可以得到scrollTop的计算公式 [代码]let itemScrollTop = currentScrollTop + containerBottom - itemBottom itemScrollTop -= (containerHeight - itemHeight) => itemScrollTop = currentScrollTop + containerBottom - itemBottom - (containerHeight - itemHeight) [代码] border-top/padding-top不为0的情况 [图片] 根据上面第一种情况的介绍的思路,很容易得到下面结果,不再赘述(X 就是容器padding-top + border-top的值) [代码]let itemScrollTop = currentScrollTop + containerBottom - itemBottom - X itemScrollTop -= (containerHeight - itemHeight - X) => itemScrollTop = currentScrollTop + containerBottom - itemBottom - X - (containerHeight - itemHeight - X) => itemScrollTop = currentScrollTop + containerBottom - itemBottom - (containerHeight - itemHeight) [代码] 【结论】两种情况最终的计算过程是一样的,因此在实现的过程中不需要进行区分 代码实现 代码片段见:https://developers.weixin.qq.com/s/y1X11dmr7AqC 视图层代码 [代码]{{item.content}} #scroll-view { position: absolute; top: 50px; bottom: 50px; width: 100%; background-color: rgba(0, 0, 0, 0.1); // 关键case transform: rotateX(180deg); } [代码] 逻辑层核心代码 [代码]scrollTo () { const itemId = '#item_id_50' const containerId = '#scroll-view' Promise.all([this._queryBoundingClient(itemId), this._getScrollInfo(containerId)]) .then((res = [[[{}]], {}]) => { const [[[ { bottom: itemBottom, height: itemHeight }]], { bottom: containerBottom, scrollTop, height: containerHeight }] = res let itemScrollTop = containerBottom - itemBottom + scrollTop itemScrollTop -= (containerHeight - itemHeight) this.setData({ scrollTop: itemScrollTop }) }) }, _queryBoundingClient (selector) { // 获取目标dom的相关位置/尺寸信息 return new Promise(resolve => { const query = this.createSelectorQuery(); query.selectAll(selector).boundingClientRect(); query.exec(resolve); }) }, _getScrollInfo (idSelector) { // 用来获取容器层相关位置/尺寸信息 return new Promise(resolve => { const query = this.createSelectorQuery() query.select(idSelector).boundingClientRect() query.select(idSelector).scrollOffset() query.exec((res = [{}, {}]) => { const [{ top, bottom, height }, { scrollHeight, scrollTop }] = res const scrollInfo = { scrollTop, scrollHeight, top, bottom, height } resolve(scrollInfo) }) }) } [代码]
2023-03-23 - 开发工具官方TypeScript-基础模板如何构建npm安装包?
开发工具官方TypeScript-基础模板如何构建npm安装包 步骤一 [图片] 步骤二 使用命令 npm i tdesign-miniprogram -S --production 测试。 [图片] 步骤三、 根据错误提示,修改 project.config.json 文件下的两个参数 "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "package.json", "miniprogramNpmDistDir": "./miniprogram" } ] 再次尝试构建,操作成功。 [图片]
2022-04-29 - 小程序隐藏API - onAppRoute(eventListener)
在小程序切换页面或打开页面时会触发onAppRoute 事件,小程序框架通过wx.onAppRoute 可以注册页面切换时的处理程序,一般开发放在app.js的onLunch生命周期中全局注册一次即可,可用于监听页面切换。 onLaunch() { wx.onAppRoute((route) => { console.log(route); }); } 通过对查看route,个人总结如下 [图片] 注:场景值
2022-05-13 - 怎么区分主包还是分包页面呢?
现需要添加页面标识,区分主包还是分包页面,大家有什么好办法呢
2022-04-01 - 关于微信安卓端网页字体适配的通知
为了提供给用户更好的阅读体验,微信安卓版 7.0.10 版本起,网页的字体会跟随微信设置里的字体大小更改而变化。 若调整字体变大或变小后,部分未适配网页的排版会出现显示错乱,建议未进行适配的开发者尽快完成对“ 字体大小” 的适配。 查看网页在字体不同大小下展示效果的方法: 方法1:"设置">"通用">“字体大小">进行字体大小修改后查看对应网页显示效果。 方法2:在微信内访问对应网页右上角”…">底部菜单栏选择调整字体">进行字体大小修改后查看对应网页显示效果。 另外,对于现有的显示问题,我们提供以下方案让开发者临时将字体还原标准大小。同时,开发者可以在页面中提示用户在右上角”…”更多菜单中修改字体到合适的大小。 下列方案可以将字体还原标准大小,但我们仍然建议后续做字体适配来提高用户的阅读体验。 『字体还原标准大小』方案: 我们提供了一个 JSAPI 用于设置字体大小,只需将字体大小等级设置为 2 (标准)即可,代码示例如下: document.addEventListener("WeixinJSBridgeReady", function () { WeixinJSBridge.invoke("setFontSizeCallback", { fontSize: '2' }); }, false); 此外,若页面是用 rem 单位进行排版的(目前该做法更容易导致页面不可用),可以反向重置 font-size 的数值达到还原字体标准大小的目的,此方法在效果上也比较理想。代码示例如下: // 以下代码思路来源网络。同时代码放在 body 标签开头位置效果最佳 var $dom = document.createElement('div'); $dom.style = 'font-size: 10px'; document.body.appendChild($dom); // 计算出放大后的字体 var scaledFontSize = parseInt(window.getComputedStyle($dom, null).getPropertyValue('font-size')); document.body.appendChild($dom); // 计算原字体和放大后字体的比例 var scaleFactor = 10 / scaledFontSize; // 取 html 元素的字体大小 var originRootFontSize = parseInt(window.getComputedStyle(document.documentElement, null).getPropertyValue('font-size')); // 由于设置 font-size 后实际会变大,故 font-size 需设置为更小一级 document.documentElement.style.fontSize = originRootFontSize * scaleFactor * scaleFactor + 'px';
2020-01-14