- 元初框架 -- 真正的微信小程序原生SPA开发框架核心概念详解
元初框架核心概念详解 元初框架是一个为微信小程序设计的应用框架,它提供了一套完整的架构模式和工具集,用于构建复杂的小程序应用。以下是对框架核心概念的详细解析: 1. 应用配置 - application.js [代码]application.js[代码] 是整个应用的配置中心,定义了应用的基本信息和全局设置。 [代码]appInfo:{ version: 'v2.0.1', latestVersion: '', envVersion:'develop', appId:'APP_ID', // ... 其他配置 ... }, startPage: 'journal', startParam: {}, // ... 其他配置 ... theme:'default' [代码] 主要配置项包括: 版本信息:控制应用版本号和环境版本 启动页面:定义应用启动时加载的第一个页面 主题设置:支持多种主题模式(默认、蓝色、微信、深色、粉色) 这些配置为整个应用提供了统一的参数管理,使得应用行为可以通过配置文件集中控制。 2. 数据管理 - db 数据库模块是框架的核心组件之一,负责数据的存储、查询和管理。 2.1 数据源结构 [代码]lib/db/db.js[代码] 定义了应用的初始数据结构,包括页面配置、账户信息、交易记录等: [代码]module.exports = { "pages": [ // 页面定义 { "index": 20, "name": 'config', "title": '系统设置', "status": 1, "icon": 'setting-fill', "uri": "config", "path": 'pages/views/constant/config/index', // ... 其他页面属性 ... }, // ... 其他页面 ... ], // 其他数据集合 "transactionEntry":[ // 交易记录 ] } [代码] 2.2 数据库API [代码]lib/db/api.js[代码] 提供了操作数据的接口: [代码]class Database { // 数据查询方法 async addCollection(collectionName) { if (dataSource[collectionName]) { return `Collection "${collectionName}" already exists.`; } dataSource[collectionName] = []; return `Collection "${collectionName}" has been added successfully.`; } // ... 其他数据操作方法 ... } module.exports = { db(){ return new Database() } }; [代码] 数据库模块实现了类似MongoDB的集合操作模式,支持数据的增删改查,为应用提供了统一的数据访问层。 3. 路由系统 - router 路由系统负责页面间的导航和状态管理,是框架的核心功能之一。 3.1 路由存储 - routeStore [代码]lib/router/routeStore.js[代码] 管理路由状态和页面配置: [代码]export const routeStore = observable({ pages: [], currentPage: {}, stacks: [], locker: false, tabbar: { tabItems: [] }, // ... 其他状态 ... // 初始化第一个页面 _initFirstPage() { let curPage = {} const startPage = wx.getStorageSync('startPage') || application.page.startPage // ... 页面初始化逻辑 ... } // ... 其他方法 ... }) [代码] 3.2 路由控制器 - router.js [代码]lib/router/router.js[代码] 实现了页面导航的核心逻辑: [代码]class Router { // 页面导航 to(target, success) { this._navigate(target); // ... 导航逻辑 ... } // 页面回退 back(delta = 1) { // ... 回退逻辑 ... } // 切换Tab页 switchTab(target) { // ... Tab切换逻辑 ... } // ... 其他方法 ... } // 创建单例实例 const router = new Router(); module.exports = router; [代码] 路由系统实现了以下核心功能: 页面导航:支持普通页面和Tab页面的跳转 页面栈管理:维护页面历史记录,支持回退操作 路由锁机制:防止快速连续导航造成的问题 参数传递:支持页面间数据传递 4. 状态管理 - store 框架采用MobX进行状态管理,实现了响应式的数据流。 4.1 全局状态 - globalStore [代码]export const globalStore = observable({ ledger: {}, sysInfo: {}, userInfo: {}, theme: 'light', setLedger: action(function (ledger) { // ... 设置账本逻辑 ... }), // ... 其他方法 ... }) [代码] 4.2 功能模块状态 框架为不同功能模块提供了专门的状态管理: 账户管理:[代码]accountsStore.js[代码] 交易记录:[代码]transactionStore.js[代码] 通知管理:[代码]noticeStore.js[代码] 记账功能:[代码]postingStore.js[代码] 这些状态存储通过 [代码]store/store.js[代码] 统一导出: [代码]import { configure } from 'mobx-miniprogram' export { globalStore } from './globalStore' // ... 其他状态导出 ... export { routeStore as routerStore} from '@/lib/router/routeStore' configure({ enforceActions: "observed" }); [代码] 5. 主应用页面 - main [代码]pages/main/index.js[代码] 是应用的主入口页面,负责初始化框架和管理全局UI状态: [代码]Page({ data: { loading: true, // ... 其他数据 ... }, onLoad() { // 创建状态绑定 this.storeBindings = createStoreBindings(this, { store: globalStore, fields: [ 'userInfo', 'theme'], }); // ... 初始化逻辑 ... }, // 获取用户信息 async getUserInfo() { // ... 获取用户数据和账本信息 ... }, // 路由初始化 routeInit(userInfo) { routerStore.init(userInfo) .then(res => { log.info("路由初始化成功:", res) }).catch(e => { log.error("路由加载失败:", e) }) }, // ... 其他方法 ... }) [代码] 主页面的WXML模板使用条件渲染,根据当前路由状态显示不同的页面组件: [代码]<page data-theme="{{theme}}" data-weui-theme="{{theme}}"> <view class="page"> <!-- 加载状态 --> <view class="page bg-page" wx:if="{{loading}}"> <!-- ... 加载UI ... --> </view> <!-- 主内容区 --> <view class="page bg-page" wx:else> <!-- 根据当前页面名称条件渲染不同组件 --> <gen-index-page wx:if="{{currentPage.name ==='index'}}" page="{{currentPage}}" ></gen-index-page> <!-- ... 其他页面组件 ... --> </view> <!-- 全局UI组件 --> <gen-notice-bar <!-- ... 属性 ... --> ></gen-notice-bar> <!-- 底部标签栏 --> <tabbar wx:if="{{(currentPage && (currentPage.tab)) && !loading}}" items="{{tabItems}}" border active-index="{{tabActive||0}}" bind:switchTab="switchTab" /> </view> </page> [代码] 6. 框架工作流程 元初框架的工作流程如下: 应用启动:加载 [代码]application.js[代码] 配置 初始化主页面:[代码]pages/main/index.js[代码] 的 [代码]onLoad[代码] 方法执行 状态绑定:通过 MobX 将全局状态绑定到页面 用户认证:检查登录状态,获取用户信息 数据加载:获取账本信息和其他必要数据 路由初始化:初始化路由系统,加载起始页面 UI渲染:根据当前路由状态渲染对应的页面组件 用户交互:通过路由系统处理页面导航和状态变更 7. 框架特点 模块化设计:各功能模块高度解耦,便于维护和扩展 响应式状态管理:使用 MobX 实现数据与UI的自动同步 单页应用模式:主页面作为容器,动态加载不同功能组件 集中式路由:统一的路由管理,简化页面导航 主题支持:内置多主题切换功能,提升用户体验 权限控制:基于用户角色的权限管理系统 丝滑切换页面:有效解决了微信原生路由跳转白屏现象,并可实现多样化自定义跳转与切换动画 元初框架通过这些核心概念和组件,为小程序开发提供了一套完整的解决方案,使开发者能够更高效地构建复杂的应用。 效果预览 略 上一篇,突破限制:微信小程序SPA架构的创新之路,帮助开发者更好地理解和应用这一创新架构。 作者:万能复式记账小程序开发者
03-15 - 🚀 小程序初始化异步问题优雅解决!发布-订阅模式实战分享 🌈✨
开发小伙伴们 👋,是不是也踩过这个坑👇:小程序刚启动时在 app.js 做全局初始化,但首页 onLoad 取不到初始化后的数据,导致页面逻辑出错! 我用 Promise + 发布订阅模式,完美解决!💥 🌟 小程序启动时,我需要在 onLaunch() 做这些事: ✅ 检查小程序更新 ✅ 拉取基础配置 ✅ 获取用户或系统状态 BUT!初始化是 异步 的,而首页 index.js 的 onLoad() 生命周期太快,可能比初始化还早一步。直接用 getApp() 拿不到数据,页面全空 ❌。💡 解决方案: 用发布订阅模式通知页面 —— 初始化完成再动! 🔥 实现核心: 1️⃣ Promise 封装初始化,明确完成时机 2️⃣ 自己实现简易版 EventBus,支持 on / emit 3️⃣ 初始化成功后 emit 发布事件,页面收到通知,再用数据 📄 步骤拆解: 1️⃣ 封装初始化函数 📌 重点:重试机制,确保 getApp() 可用。 // utils/appInit.js function initFun() { const app = getApp(); app.globalData.pageInit = true; } function appInit(maxRetry = 10, interval = 250) { return new Promise((resolve, reject) => { let retryCount = 0; const checkAppReady = () => { wx.nextTick(() => { if (typeof getApp === 'function' && getApp() !== undefined) { initFun(); resolve('初始化成功'); } else if (retryCount < maxRetry) { retryCount++; setTimeout(checkAppReady, interval); } else { reject('初始化失败'); } }); }; checkAppReady(); }); } export default appInit; 2️⃣ 实现简易版 EventBus 📌 功能:订阅 on / 发布 emit / 取消 off,页面间轻松通信。 // utils/eventBus.js class EventBus { constructor() { this.events = {}; } on(event, callback) { if (typeof event !== 'string' || typeof callback !== 'function') return; if (!this.events[event]) { this.events[event] = []; } if (!this.events[event].includes(callback)) { this.events[event].push(callback); } } emit(event, data) { if (!this.events[event] || this.events[event].length === 0) return; this.events[event].forEach(cb => { try { cb(data); } catch (err) { console.error(`"${event}" 回调出错`, err); } }); } off(event, callback) { if (!this.events[event]) return; this.events[event] = this.events[event].filter(fn => fn !== callback); } } const eventBus = new EventBus(); export default eventBus; 3️⃣ App.js 使用 📌 关键点:✅ 先订阅,再初始化,防止订阅晚了漏事件。 // app.js import appInit from '/utils/appInit'; import eventBus from '/utils/eventBus'; App({ globalData: {}, onLaunch() { // 先订阅,保证其他页面后续能接收 eventBus.on('initDone', () => {}); appInit(10).then(() => { eventBus.emit('initDone', { init: true }); }).catch(err => { console.error('App 初始化失败', err); }); }, }); 4️⃣ 首页 index.js 监听初始化完成 📌 确保数据 ready,再做业务处理! // pages/index/index.js import eventBus from '/utils/eventBus'; Page({ onLoad() { eventBus.on('initDone', () => { const app = getApp(); console.log('收到初始化完成通知', app.globalData); // 使用初始化数据 }); }, onUnload() { // 页面销毁时解绑,防止内存泄漏 eventBus.off('initDone', ()=>{}); } });
03-20 - 小程序上也能放烟花
快过年了,贴一个放烟花的自定义组件 之前从某网站扒下来改造适配了一下小程序,整体效果还可以,欢迎大家下载体验 先看看动画效果,由于文件大小有限,实际效果可运行代码片段或者前往小程序[恋爱小清单]首页左上角查看 [图片] 源码片段在这里,欢迎下载体验 https://developers.weixin.qq.com/s/3AOc71mH7lX5
01-08 - 微信小程序营销活动发放红包实践
微信小程序营销活动发放红包实践 刚过去的春节,承办了一个喜迎新春的工会活动,具体有以下几个环节,达到一定积分可以进行抽奖,抽奖有微信红包 [图片] [图片] [图片] 之前的时候,是通过直接商家转账到零钱,赶巧,年前微信这块做了改动商家转账到零钱这个产品直接回收(没法走线下批量发放)了,不让用了,后面发现现金红包这个产品可以用 [图片] 批量下发使用的模版如下所示 [图片] 目前活动已经顺利结束了,使用该产品发放红包有个问题,因为红包未被领取,会自动退回,而不是直接到账的, [图片] [图片] 这个时候,用户的补发就会成为问题 目前没有找到一种可以批量导出未领取用户列表的方式
02-07 - 从基础学习:如何重写 Page 加入路由守卫
1.函数的基础 在 JavaScript 中,函数是一种特殊的对象,可以被赋值、传递和修改。 [代码]function greet() { console.log("Hello!"); } greet(); // 输出:Hello! [代码] 2.函数的重写 我们可以通过重新赋值来 重写 一个函数。 [代码]function greet() { console.log("Hello!"); } greet(); // 输出:Hello! // 重写 greet 函数 greet = function () { console.log("Hi!"); }; greet(); // 输出:Hi! [代码] 第一次调用 [代码]greet()[代码],输出 “Hello!”。 重写 greet 函数后,再次调用 [代码]greet()[代码],输出 “Hi!”。 3.保存原始函数 在重写函数时,通常需要保存原始函数,以便在重写后的函数中调用它。 [代码]function greet() { console.log("Hello!"); } // 保存原始函数 const originalGreet = greet; // 重写 greet 函数 greet = function () { console.log("Before greeting..."); originalGreet(); // 调用原始函数 console.log("After greeting..."); }; greet(); [代码] 输出: [代码]Before greeting... Hello! After greeting... [代码] 4.应用到 Page 函数 在微信小程序中,Page 是一个全局函数,用于注册页面。我们可以通过重写 Page 函数,在页面注册时插入自定义逻辑。 [代码]// 保存原始的 Page 函数 const originalPage = Page; // 重写 Page 函数 Page = function (obj) { console.log("Page is being registered..."); // 调用原始的 Page 函数 originalPage(obj); }; // 注册页面 Page({ onLoad() { console.log("Page loaded!"); } }); [代码] 输出: [代码]Page is being registered... Page loaded! [代码] 5.扩展页面生命周期 通过重写 Page 函数,我们可以在页面生命周期中插入自定义逻辑。 [代码]// 保存原始的 Page 函数 const originalPage = Page; // 重写 Page 函数 Page = function (obj) { // 保存原始的 onLoad 函数 const originalOnLoad = obj.onLoad; // 重写 onLoad 函数 obj.onLoad = function (options) { console.log("Before onLoad..."); if (originalOnLoad) { originalOnLoad.call(this, options); // 调用原始的 onLoad 函数 } console.log("After onLoad..."); }; // 调用原始的 Page 函数 originalPage(obj); }; // 注册页面 Page({ onLoad() { console.log("Page loaded!"); } }); [代码] 输出: [代码]Before onLoad... Page loaded! After onLoad... [代码] 6.使用场景:加入路由守卫 [代码]let data; function init(_data) { data = _data; let oldPage = Page; // 保存原始的 Page 函数 Page = function (obj) { // 重写 onShow let oldOnShow = obj.onShow; obj.onShow = function () { routerBeforeEach(() => { if (oldOnShow) { oldOnShow.call(this); // 调用原始的 onShow 函数 } }); }; // 重写 onHide let oldOnHide = obj.onHide; obj.onHide = function () { routerBeforeEach(() => { if (oldOnHide) { oldOnHide.call(this); // 调用原始的 onHide 函数 } }); }; return oldPage(obj); // 调用原始的 Page 函数 }; } [代码] 7.总结 重写函数:通过重新赋值函数,可以在函数执行前后插入自定义逻辑。 保存原始函数:在重写函数时,通常需要保存原始函数,以便在需要时调用它。 应用到 Page:通过重写 Page 函数,可以在页面生命周期中插入路由守卫逻辑。
02-16 - 小程序备案各省份管局联系电话
其它地方整理的,仅供参考,以实际为准 北京通信管理局 010-63310094天津通信管理局 022-60351158河北通信管理局 0311-81582202山西通信管理局 0351-8788032内蒙古通信管理局 0471-6684287 0471-6680579辽宁通信管理局 024-86581199 024-86581402吉林通信管理局 0431-88925397黑龙江通信管理局 0451-53610153上海通信管理局 021-63905006江苏通信管理局 025-58500033浙江通信管理局 0571-87078277安徽通信管理局 0551-65680622 0551-65680633福建通信管理局 0591-28355716江西通信管理局 0791-6207387 0791-6218176山东通信管理局 0531-82092828河南通信管理局 0371-65795120湖北通信管理局 027-87796833027-87796001湖南通信管理局 0731-82260326广东通信管理局 020-87628386广西通信管理局 0771-2628426 0771-2628411 0771-2628420海南通信管理局 0898-66550106重庆通信管理局 023-68583855四川通信管理局 028-87015272 028-87012203贵州通信管理局 0851-85611000云南通信管理局 0871-63557966西藏通信管理局 0891-6329494陕西通信管理局 029-965107甘肃通信管理局 0931-4501253 0931-4501254青海通信管理局 0971-8208948宁夏通信管理局 0951-619863531新疆通信管理局 0991-5832086其它地方整理的,仅供参考,以实际为准
2024-12-21 - 基于微信小程序的 wx.request 的高级封装
weReq是什么 [代码]weReq[代码] 是基于微信小程序的 [代码]wx.request[代码] 的高级封装,提供全局和外部拦截器的管理,支持自动登录等功能,旨在简化微信小程序网络请求的处理流程,提升开发者的使用体验。 特性 支持 Promise API:该类支持 [代码]async/await[代码] 和 [代码]then[代码],使得异步操作更加简洁,避免了回调地狱的复杂性,提升了代码可读性。 拦截请求和响应:内置多种拦截器机制允许开发者在请求和响应阶段进行一系列处理,仅返回业务数据,简化了对返回信息的操作。 自带 Loading:基于微信的wx.showLoading的方法在发起请求时自动显示加载提示,确保请求完成后自动隐藏,提升交互体验。 自动登录:在登录态过期时,系统会自动尝试重新登录,整个过程对开发者是透明的,无需手动处理,对用户是无感知的,提高了用户体验。 下载与安装 源码下载引入 步骤1:点击这里下载 [代码]weReq[代码] 源码,将解压后的文件夹中的weReq.min拷贝到小程序项目中的 [代码]utils[代码] 目录下。引用 [代码]weReq[代码] 并初始化。 步骤2:import Request from ‘…/utils/weReq.min.js’; npm安装 使用 npm 构建前,请先阅读微信官方的 npm 支持 步骤1:npm install we-req 步骤2: 构建 npm,点击开发者工具中的菜单栏:工具 --> 构建 npm 步骤3:完成前两步骤就可以引入啦,import Request from ‘we-req’ 使用示例 [代码]import Request from 'we-req'; //或者 import Request from '../utils/weReq.min.js'; const weReq = Request.init({ baseURL: 'https://api.example.com', timeout:3000, ... }); // 发起 GET 请求 weReq.get({ url: '/endpoint' }) .then(response => { console.log(response); }) .catch(error => { console.error(error); }); // 发起 POST 请求 weReq.post({ url: '/endpoint',data:{name:"我是小明"} }) .then(response => { console.log(response); }) .catch(error => { console.error(error); }); [代码] 更多丰富示例可以查看demo,链接quick-start。 weReq API 创建一个实例 [代码]import Request from 'we-req' const weReq = Request.init({ baseURL: BASE_URL, timeout: TIME_OUT, ...config }) [代码] 实例方法 以下是可用的实例方法,实例方法的配置将与实例的配置合并,如果有相同的字段,实例方法传过来的字段替换实例方法的字段。 weReq#request(config) weReq#get(config) weReq#delete(config) weReq#post(config) 请求配置(config) 部分参数请参考小程序本身配置, 传送门。 参数 类型 默认值 必填 说明 url string 是 开发者服务器接口地址 data string/object/ArrayBuffer 否 请求的参数 baseURL string 否 会自动添加到 url 前,除非 url 是一个绝对 URL,它可以通过设置一个 [代码]baseURL[代码] 便于后面的请求方法传递相对 URL,不用每次请求带上域名。 timeout number 3000 否 超时时间,单位为毫秒。默认值为 60000 method string GET 否 HTTP 请求方法 headers object application/json 否 请求发送时候的请求头 loading boolean/string false 否 请求过程页面是否展示全屏的加载框,默认文字是加载中,当值为字符串时,将替换loading的文字 interceptors object false 否 拦截器,在请求前或响应的时候对数据做拦截,可以对请求参数做一些处理或对响应数据做处理,比如:自定义加载框、发送前可以对 config 进行修改、在收到响应后可以对 res 进行处理或转换等。 reLoginConfig object false 否 登录管理器,对一些开发者,需要拿到小程序登录凭证,带到给服务器结合并返回服务器的数据做处理,当session_key过期的时候会自动登录,并重新请求session_key过期的时候的网络请求,做到用户无感知登录过程。 全屏加载框(loading) 简单来说,减少每次网络请求前,都要手写一遍,网络正在加载中。 示例代码 [代码]import Request from 'we-req' const weReq = Request.init({ baseURL: 'https://wx.mock.com/api/', timeout: 3000, //开启全局加载弹窗 loading: true }) //发起 POST 请求,自带加载框 weReq .post({ url: '/endpoint', data: { name: '我是小明' } }) .then((response) => { console.log(response) }) .catch((error) => { console.error(error) }) // 当我某个请求突然不想用全局加载框了,也可去掉 weReq .post({ url: '/endpoint', loading: false, data: { name: '我是小明' } }) .then((response) => { console.log(response) }) .catch((error) => { console.error(error) }) [代码] 拦截器(interceptors) 拦截器,顾名思义,在请求前或响应的时候对数据做拦截,可以对请求参数做一些处理或对响应数据做处理,比如:实现自定义加载框、发送前可以对 config 进行修改、在收到响应后可以对 res 进行处理或转换等。 interceptors参数对象说明 参数 类型 默认值 必填 说明 requestSuccessFn Function 否 请求前拦截器 requestFailFn Function 否 请求失败拦截器 responseSuccessFn Function 否 响应成功拦截器 responseFailFn Function 否 响应失败拦截器 示例代码 [代码]import Request from 'we-req'; const weReq = Request.init({ baseURL: 'https://wx.mock.com/api/', timeout: 3000, interceptors: { // 请求成功拦截器 requestSuccessFn: (config) => { // 在请求发送前可以对 config 进行修改 return config; }, // 请求失败拦截器 requestFailFn: (err) => { // 可选:处理请求失败的情况,如记录日志或提示用户 }, // 响应成功拦截器 responseSuccessFn: async (res) => { // 在收到响应后可以对 res 进行处理或转换 return res; }, // 响应失败拦截器 responseFailFn: (err) => { // 可选:处理响应失败的情况,如显示错误信息或重试请求 }, }); [代码] 自动登录(autoLoginConfig) 简单来说,小程序登录是通过 [代码]code[代码] 换取 [代码]session_key[代码] 的过程,过通过 Header 携带 [代码]sessionId[代码]发送后端。 当 [代码]session_key[代码] 过期时,我们需要用新的 [代码]code[代码] 获取新的 [代码]session_key[代码],然后继续发起请求。 那么,问题来了,因为每个请求都需要携带 [代码]session_key[代码],如果用户在访问某个页面的时候时突然 [代码]session_key[代码] 过期了,那该页面数据就获取不了了,无法渲染,页面出现空白,在这种情况下,我们需要重新获取用户的 [代码]code[代码],以换取新的 [代码]session_key[代码],并重新获取页面数据的方法,重新渲染页面数据。 [代码]weReq[代码] 的自动登录就是为此而生,我们约定登录过期状态(默认是 [代码]res.code === -220[代码],可根据服务器判断调整),当检测到过期时,[代码]weReq[代码] 会自动调用 [代码]wx.login[代码] 重新获取 [代码]code[代码],再通过 [代码]code[代码] 调用登录接口获取新的 [代码]sessionId[代码],通过 Header 携带 [代码]sessionId[代码],最后重新发起sessionId过期之前的所有网络的请求。 这样,用户将无感知地自动登录,并自动重新调用该网络请求,这些操作都不用自己重新写是不是很方便哩。 autoLoginConfig参数对象说明 参数 类型 默认值 必填 说明 reTokenConfig Object 是 当token过期的时候,调用该方法,这里是填调用该方法的配置 isTokenExpiredFn Function 是 判断token过期的状态码,这个根据自己服务器代码返回来判断,举例我这边的的状态码为-220为过期,当所请求返回-220的时候则会自动调用登录网络请求接口 reLoginLimit Number 3 否 当重试请求登录接口返回失败次数超过这个次数,将不再重试登录请求。 reTokenConfig参数对象说明 参数 类型 默认值 必填 说明 url String 是 获取token的网络请求url ,它可以通过设置一个 [代码]baseURL[代码] 便于后面的请求方法传递相对 URL,不用每次请求带上域名,如果是绝对url将默认以绝对url优先 method Function 是 GET|POST,如果是GET请求,code将会自动拼接到url后面,如果是POST请求将会放入data传到后端 codeKey String code 否 决定你的code传入的key的变量名字,如GET请求时/code?=123456,POST请求为{code:“123456”} data Object 否 其他参数 success Function 否 请求成功的回调,可在此存储你需要的业务,如:存储token到本地存储 示例代码 [代码]import Request from 'we-req' const weReq = Request.init({ baseURL: 'https://wx.mock.com/api/', //获取本地token,放到请求拦截器,每次网络请求前带上token interceptors: { requestSuccessFn: (config) => { const key = wx.getStorageSync('token') config.header = {} config.header.Authorization = `Bearer ${key}` config.data = { ...config.data } return config } }, reLoginConfig: { // 当token过期的时候刷新token并存储到本地 reTokenConfig: { url: '/refresh_token', method: 'POST', data: {}, success: (res) => { // 根据自己服务器返回数据实现自己的业务 if (res.data.data.token) { wx.setStorageSync('token', res.data.data.token) } } }, // 判断网络请求token过期的状态码,这个根据自己业务代码返回来判断,我这边的状态码为-220为过期,则会自动刷新token isTokenExpiredFn: (res) => { return res.code === -220 } } }) [代码] demo 更多丰富示例可以查看demo,链接quick-start。
2024-12-24 - setData 数组 更新单个或多个对象
setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。 ✅ 页面或组件的 data 字段,应用来存放和页面或组件渲染相关的数据(即直接在 wxml 中出现的字段);✅ 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用 setData 设置并使用 observers 监听变化;✅ 页面或组件渲染无关的数据,应挂在非 data 的字段下,如 [代码]this.userData = {userId: 'xxx'}[代码];❌ 避免在 data 中包含渲染无关的业务数据;❌ 避免使用 data 在页面或组件方法间进行数据共享;❌ 避免滥用 纯数据字段 来保存可以使用非 data 字段保存的数据。3.2 控制 setData 的频率每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用 [代码]setData[代码],会导致以下后果: 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。因此,开发者在调用 setData 时要注意: ✅ 仅在需要进行页面内容更新时调用 setData;✅ 对连续的 setData 调用尽可能的进行合并;❌ 避免不必要的 setData;❌ 避免以过高的频率持续调用 setData,例如毫秒级的倒计时;❌ 避免在 onPageScroll 回调中每次都调用 setData。 let item = e.currentTarget.dataset.item; item.status = !item.status let setListItems = {} for (let i = 0; i < this.data.list.length; i++) { let listItem = this.data.list[i]; if (listItem?.id === item.id) { setListItems = Object.assign({ [`list[${i}].status`]: item.status }, setListItems) } } var arr = Object.keys(setListItems); if (arr.length > 0) { this.setData(setListItems); } console.log(this.data.list)
2024-09-25 - safe-area-inset-bottom 设置最小安全距离
ios底部存在安全距离可通过设置 safe-area-inset-bottom 某些安卓safe-area-inset-bottom 为 0 .safe-area { padding-bottom: constant(safe-area-inset-bottom); /* 兼容 IOS<11.2 */ padding-bottom: env(safe-area-inset-bottom); /* 兼容 IOS>11.2 */ } 有些场景用以上方式设置ios显示正常。 安卓底部会很奇怪(按钮贴到底部了) [图片] 解决方案: 利用 padding不能为负数 上代码 .safe-area { padding-bottom: calc(constant(safe-area-inset-bottom) - 32rpx); /* 兼容 IOS<11.2 */ padding-bottom: calc(env(safe-area-inset-bottom) - 32rpx); /* 兼容 IOS>11.2 */ &::after { content: ''; display: block; height: 32rpx; } } 这样底部间距就能自由控制拉,还不需要用js控制(完美~)! [图片]
2024-11-26 - 微信小程序签字板、手写签名、涂鸦组件
bao-wecom 小程序自定义签字板、手写签名、涂鸦组件。 背景 在一次小程序项目中,需要开发一个用户签字的功能,所以就开发这个签字板组件。 案例demo [图片] 使用方法 1. 安装组件 [代码]npm install --save bao-wecom [代码] 2. 构建 npm 点击开发者工具中的菜单栏:工具 --> 构建 npm [图片] 3. 签字板使用 在页面的 json 配置文件中添加 bao-wecom-signature 自定义组件的配置 [代码]{ "usingComponents": { "bao-wecom-signature": "bao-wecom/signature/index" }, "pageOrientation": "landscape",//横屏 "navigationStyle": "custom" //自定义头 } [代码] WXML 文件中引用 bao-wecom-signature [代码]<bao-wecom-signature></bao-wecom-signature> [代码] bao-wecom-signature 的属性介绍如下: 字段名 类型 必填 描述 title String 否 签字版标题 color String 否 设置笔画线条的颜色,默认为#1A1A1A ,hex格式 boardColor String 否 设置画板的颜色,默认为#ffffff ,hex格式 backgroundColor String 否 设置背景的颜色,默认为#ffa500 ,hex格式 size Number 否 设置笔画线条的粗细,默认为8 fileType String 否 设置导出图片的格式,默认为png quality Number 否 生成图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 buttonList Array 否 设置显示的按钮,默认为[“cancel”, “setting”, “reset”, “preview”, “save”, “confirm”],cancel:取消按钮,setting:设置按钮,reset:重写按钮,preview:预览按钮,save:保存按钮,confirm:确认按钮。 bao-wecom-color-picker 的事件介绍如下: 事件名 描述 confirm 点击确定时触发,返回值{ path: 图片本地路径, width: 宽度, height: 高度 } cancel 点击取消时触发 如果觉得作者不易,打赏一下呗 [图片]
2024-10-30 - 小程序自定义tabbar,根据权限显示不同的tabbar
在小程序开发者,自定义tabbar,根据权限角色不同,显示不同的tabbar,经常遇到,这里给出一个解决方案。 1、修改app.json [代码]"tabBar": { "custom":true } [代码] 2、在项目根目录创建一个文件夹 custom-tab-bar,组件,如图 [图片] custom-tab-bar/index.js [代码]Component({ /** * 组件的属性列表 */ properties: { }, /** * 组件的初始数据 */ data: { active: -1, list: [ { value: 0, show: true, label: '首页', icon: 'home', url: '/pages/index/index' }, { value: 1, show: true, label: '订单', icon: 'task', url: '/pages/order/index/index' }, { value: 2, show: true, label: '我的', icon: 'user', url: '/pages/my/index/index' }, ], }, /** * 组件的方法列表 */ methods: { onChange({detail: {value}}) { const {list} = this.data; console.log('value', value) this.setData({ active: value, }); wx.switchTab({ url: list[value].url, }) }, init() { const page = getCurrentPages().pop(); let urls = this.data.list.map(v => v.url); let active = urls.findIndex(v => v === `/${page.route}`); console.log('active',active) this.setData({ active }); }, toggleMenu(role) { // 在这里处理具体的权限业务逻辑 // 下面是伪代码 let {list} = this.data; if (role == 1) { list[1].show = false; this.setData({list}); } }, } }) [代码] custom-tab-bar/index.json [代码]{ "component": true, "usingComponents": { "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar", "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item" } } [代码] custom-tab-bar/index.wxml [代码]<t-tab-bar t-class="t-tab-bar" value="{{active}}" bindchange="onChange" theme="tag" split="{{false}}"> <block wx:for="{{list}}"> <t-tab-bar-item wx:if="{{item.show}}" wx:key="index" value="{{item.value}}" icon="{{item.icon}}"> {{item.label}} </t-tab-bar-item> </block> </t-tab-bar> [代码] 3、在tabbar关联页面初始化tabbar组件的init方法 上面我们在tabbar写了三个路由,那么在这三个相对应的页面,都要调用一下下面的代码 [代码]onShow() { this.getTabBar().init() }, [代码] 通过以上步骤已经实现了自定义tabbar的逻辑。 关于角色权限下的tabbar 在有些场景下,我们需要权限来控制显示tabbar,这个时候,我们可以在首页,请求数据,然后调用[代码]this.getTabBar().toggleMenu();[代码] 来改变要显示的菜单,这样我们根据角色权限的tabbar就完成了。
2024-06-26 - 手把手教你备案微信小程序(非个人主体备案)
备案材料准备 在提交备案前,请务必提前准备好备案所需材料,以免由于材料更新问题,导致备案需延期提交。下面将会带大家详细了解备案材料的要求,这样后续在提交时就能避免因为材料问题而导致失败。 材料示例及注意事项 [图片] 注:所有上传材料大小应不超过2M,分辨率不低于720* 1280 ,仅支持JPG、JPEG、PNG 格式 若想查看更多小程序备案材料示例,详情可查看文档 备案信息填写 备案材料准备好后,就可以前往【小程序管理后台-设置-小程序备案】提交备案申请了。下面将会详细教大家如何进行备案信息的填写,一共分为五个部分:主办单位信息填写、主体负责人信息填写、小程序信息填写、小程序管理员信息填写和上传其他信息材料。 1.主办单位信息填写 [图片] [图片] 填写说明 常见报错/问题 解决方案 ①选择地区:选择与证件地址相一致的省市区信息 该主体已在XX完成备案,请修改备案省份或注销备案主体重新备案 请核实该主体是否有在其他省份备案过,由于同主体在所有平台的备案省份必须保持一致,需修改备案省份或注销备案主体重新备案 ②主办者性质:默认与小程序主体认证信息相一致 / / ③证件类型:默认与小程序主体认证信息相一致 / / ④上传证件:按要求提供最新版证件 营业执照有效期不足 请联系工商部门更新证件有效期 ⑤企业名称:填写证件相对应名称信息 主办者与小程序主体不一致 请核实填写企业名称是否与小程序主体名称、上传营业执照名称相一致 ⑤企业名称:填写证件相对应名称信息 营业执照名称为空或者* 号 请联系工商部门更新企业名称信息 ⑥证件住所:填写证件相对应经营场所信息 【主体证件住所】工商数据对比不通过 请参考文档进行排查 ⑦证件号码:填写证件相对应统一社会信用代码信息 未查询到企业信息,请检查主体证件号是否有误 请核实填写的是否为统一社会信用代码,若无,请联系工商部门更新证件信息,不能填写其他如工商注册号等 ⑧通讯地址:填写当前主体所在的实际通讯地址(无需填写省、市、区) 通讯地址未能精确到门牌号 若无具体门牌号,需要在备注中说明情况 ⑨备注(选填):针对主体信息进行补充说明,如有可填写 / / 注:若为新建企业或近期有做信息变更,可能会存在企业工商数据更新延迟的情况,建议过段时间(5~15个工作日)再进行重试,否则无法正常发起验证流程。 2.主体负责人信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择主体负责人证件类型信息 / / ②上传证件:按要求提供最新版证件 / / ③负责人名称:通过上传证件自动识别,有误可自行修改 主体负责人与法定代表人不一致,且备案所在地不支持法定代表人授权 请核实填写的主体负责人是否为法人,需与营业执照信息一致,由于所属地区不支持授权,只能填写法人信息 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【主体负责人证件号码】企业工商四要素核验失败 请核实填写的主体负责人名称、证件号信息是否正确 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:主体负责人手机号码 【主体负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:主体负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:主体负责人的应急电话 【主体负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:主体负责人的电子邮箱 / / 3.小程序信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①服务内容标识:根据小程序实际运营内容选择合适的即可 小程序服务内容类型数目不能超过5个 服务内容标识是通信管局对各个行业的分类,平台部分行业类目与管局行业类目名称不完全不一致,建议根据备案小程序实际运营内容尽可能选择对应的服务内容标识,最多选择5个。 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 如从事XXX业务,请上传前置审批文件 小程序实际运营内容涉及前置审批项,需上传对应的审批文件 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 前置审批项必须选择“以上都不涉及” 小程序实际运营内容不涉及前置审批项,需要选择"以上都不涉及” ③备注(必填):具体描述小程序实际经营内容,主要服务内容 请在小程序备注按格式填写 请核实是否有根据备注格式进行填写,仅自行补充带星号内容即可。 4.小程序管理员信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择小程序负责人证件类型信息(目前仅支持身份证) / / ②上传证件:按要求提供最新版证件(目前仅支持身份证) / / ③负责人名称:通过上传证件自动识别,有误可自行修改 【小程序负责人姓名】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【小程序负责人证件号码】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:小程序负责人手机号码 【小程序负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:小程序负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:小程序负责人的应急电话 【小程序负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:小程序负责人的电子邮箱 / / ⑩负责人人脸核身:小程序管理员(小程序负责人)需使用微信APP扫码,完成人脸核身 当前场景仅支持居民身份证 核实小程序管理员证件是否为大陆居民身份证,目前港澳台管理员无法进行人脸核身,建议先更换小程序管理员为中国大陆地区的人员,作为备案小程序负责人。 5.上传其他信息材料 [图片] 填写说明 常见报错/问题 解决方案 互联网信息服务承诺书:1. 广东地区:下载页面提供的模版文件,填写完整后上传提交;2. 非广东地区:点击阅读确认后提交 承诺书需加盖公章,但个体户没有公章 若个体工商户无公章,需要主体负责人手写日期+签名+盖手印+身份证号码,同时请在主体备注处备注“个体工商户无公章”。注:江苏、宁夏、福建地区,须刻章后提交备案,不接受负责人手印。 以上信息都填写完毕,就可点击提交,后续的备案审核流程可参考: [图片] 如有其他相关疑问,欢迎随时参与社区讨论。
2024-05-28 - 原生小程序自定义导航栏
微信原生小程序导航栏 navigation-bar 使用说明 [图片][图片] 1、安装插件,并在小程序开发工具中构建 npm [代码]npm i navigationbar-wx [代码] 2、app.js 中得 globalData 内写上如下: [代码]import { navigationbarWx } from "navigationbar-wx/config.js"; navigationbarWx.init({ title: "小程序默认名称", homePath: "/pages/index/index",//默认路径,如果相同可以忽略这个参数 }) //备注:init 可初始化的字段如下字段说明 [代码] 3、app.json 中配置插件 [代码]"usingComponents": { "navigationbar-wx": "navigationbar-wx" } [代码] 4、页面使用-传参如下说明 [代码]<!-- 可直接使用 --> <navigationbar-wx /> <!-- 也可传参使用 --> <navigationbar-wx title="" logo="" color="" bgColor="" bgImg="" iconColor="" hideHome="" hideSeat="" /> [代码] 字段说明 字段 必传 类型 默认值 说明 title 否 String 小程序默认标题 当前页面标题 logo 否 String 无 可传入一个图片 URL 作为标题,logo 优先级大于 title color 否 String #000000 字体颜色,默认黑色 bgColor 否 String #ffffff 导航栏颜色,默认白色 bgImg 否 String 无 导航栏整体图片,背景图优先级大于背景颜色 iconColor 否 String black 左边按钮颜色,只支持 black、white 两种类型 hideHome 否 Boolean false 当没有上一级页面时,是否隐藏小房子 hideSeat 否 Boolean false 导航栏底部占位,默认占位不隐藏 homePath 否 String /pages/index/index 页面如果设置这个参数优先级高于 init 时传入的路径 github 地址 github 地址
2024-06-17 - input adjust-position 键盘弹起 + 自定义导航 解决固定定位偏移top方案
input adjust-position 键盘弹起 + 自定义导航 解决flex定位问题 关键词:adjust-position,键盘弹起 ;"navigationStyle": "custom"; 解决方案:当键盘弹起时,通过已知值计算top偏移; 主要获取数值 键盘高度,窗口高度,获取焦点的输入框的top偏移(主要); 窗口高度 - 键盘高度 = 可视的高度 输入框Top偏移(主要是这个,在不同位置) - 可视的高度 + 输入框高度 = 相对窗口顶部(top)偏移 wxml 部分 <view class="navigation-bar" style="{{keyboardHeight > 0?'top:'+OffsetTop +'px':''}}"> <navigation-bar /> <!--自定义导航--> </view> .navigation-bar{ position: fixed; top:0; left:0; widt:100%; } <view> <view style="height:600px"></view> <!-- 非必需,看个人需要;cursor-spacing 指定光标与键盘的距离,取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离 --> <!-- 必须 id --> <!-- 必须 bindfocus bindblur 指向同一个事件--> <input cursor-spacing="20" id="input-phone" bindfocus="getTelIptHeight" bindblur="getTelIptHeight" type="text" value="{{phone}}" bindinput="phoneInput" placeholder="请输入手机号" placeholder-class="input-placeholder" /> </view> js事件部分 const selectDom = (createSelectorQuery, select) => { return new Promise((resolve, reject) => { try { createSelectorQuery.select(select).boundingClientRect((e) => { resolve(e) }).exec(); } catch (error) { console.log(error); } }) } { data:{ OffsetTop:0, }, getTelIptHeight(e) { console.log("height---------", e); let keyboardHeight = e?.detail?.height || 0 this.setData({ keyboardHeight: keyboardHeight }) // 自定义导航栏逻辑计算 const windowHeight = wx.getSystemInfoSync().windowHeight; // 获取窗口高度 const query = wx.createSelectorQuery(); // selectDom 自己疯装的 Promise.all([selectDom(query, '#' + e.currentTarget.id)]).then((rect) => { console.log(rect[0], windowHeight) const inputTop = rect[0]?.top; // 获取输入框的 top偏移 const bottomOffs = windowHeight - keyboardHeight; // 可视窗口高度 //输入框偏移位置 - 可视窗口高度 + 自身高度 = 相对窗口顶部(top)偏移 const Offset = (inputTop - bottomOffs) + rect[0]?.height; console.log('Offset', Offset) this.setData({ OffsetTop: Offset }) }) } }
2024-04-16 - css实现无缝滚动字幕
直接看代码片段吧:https://developers.weixin.qq.com/s/Ae0lsgme7SOG 代码不复杂,就不多解释了 js主要是在一开始设置一下css,后续全都靠css来实现效果 为什么要用css来实现,而不是js。主要是实际使用下来,css比js在时间控制上更精准,而且不需要用到setTimeout,也就没必要考虑clearTimeout 同样原理还可以做出纵向的滚动字幕,代码片段:https://developers.weixin.qq.com/s/9p1H6gmk75OM
2024-01-16 - 深入理解小程序——生命周期
生命周期类型 不像其他框架,小程序是有页面(page) 和组件(Component) 两个概念,所以可以理解有两种生命周期。不过,你也可以使用组件也注册页面,然后在 [代码]lifetimes[代码] 字段里声明组件的生命周期,在 [代码]pageLifetimes[代码] 里声明页面的生命周期。 页面的生命周期有: onLoad onShow onReady onHide onUnload 而组件的生命周期有: created attached ready moved detached error 这里其实还有一个 linked 生命周期,存在父子关系时会触发 条件渲染的影响 组件可以通过 [代码]wx:if[代码] 和 [代码]hidden[代码] 来控制渲染的,这里对生命周期的触发也有影响。 如定义这么一个组件 [代码]log[代码]: [代码]Component({ lifetimes: { attached() { console.log('log attached') } } }) [代码] 使用 wx:if 然后在页面中使用 [代码]wx:if[代码] 条件渲染: [代码]<view wx:if="{{false}}"> <log /> </view> [代码] 此时不会触发 [代码]attached[代码],因此控制台没有输出。 使用 hidden 反而如果使用 [代码]hidden[代码] 条件渲染: [代码]<view hidden="{{true}}"> <log /> </view> [代码] 此时反而控制台会输出 [代码]log attached[代码]。 两者差异 其实两者的差异在于,[代码]hidden[代码] 会正常渲染 DOM,而 [代码]wx:if[代码] 则不会渲染。 如果组件的父元素使用 [代码]hidden[代码] 进行隐藏,那么此时 [代码]created[代码]、[代码]attached[代码]、[代码]ready[代码] 生命周期均会正常触发。如果在这些生命周期里获取子元素的尺寸,则所有值均返回 0。 如 TDesign 里面的 [代码]swipe-cell[代码] 需要计算 left 和 right 区域的大小;[代码]tabs[代码] 需要计算下划线的位置。 解决方案 比较简单的处理方式:建议用户使用 [代码]wx:if[代码] 而不是 [代码]hidden[代码],不过这明显是治标不治本的方案。 前文也提到了,问题的根本是没有正确地获取到元素的尺寸,因此可以在获取元素尺寸的地方做兼容处理。异常触发的条件则是 [代码]width == 0 && right == 0[代码] 知道在哪里需要兼容处理之后,需要解决的则是:如何在可以获取到正确的尺寸的时候重新获取尺寸呢? 此时可以使用 [代码]wx.createIntersectionObserver[代码] 这个 API。当 [代码]hidden = false[代码] 的时候,组件会重新出现在视图里,Observer 就会被触发,此时重新获取尺寸就能得到正确的尺寸信息了。以下是简易的封装: [代码]const getObserver = (context, selector) => { return new Promise((resolve, reject) => { wx.createIntersectionObserver(context) .relativeToViewport() .observe(selector, (res) => { resolve(res); }); }); }; const getRect = function (context:, selector) { return new Promise((resolve, reject) => { wx.createSelectorQuery() .in(context) .select(selector) .boundingClientRect((rect) => { if (rect) { resolve(rect); } else { reject(rect); } }) .exec(); }); }; export const getRectFinally = (context, selector) => { return new Promise((resolve, reject) => { getRect(context, selector).then(rect => { if (rect.width === 0 && rect.height === 0) { getObserver(context, selector).then(res => { resolve(res) }).catch(reject) } else { resolve(rect) } }).catch(reject) }) } [代码] 父子组件的影响 当存在父子组件的时候,可能很多人根本不知道各种生命周期的触发顺序。 之前 TDesign 的 [代码]cell-group[代码] 有个错误的实现,在 linked 生命周期里获取子元素进行操作: [代码]Component({ relations: { '../cell/cell': { type: 'child', linked() { this.updateLastChid(); }, }, }, updateLastChid() { const items = this.$children; items.forEach((child, index) => child.setData({ isLastChild: index === items.length - 1 })); }, }) [代码] 其实,存在父子组件的时候,生命周期是这么触发的: 父组件 created 子组件 created 父组件 attached 子组件 attached 父组件 linked(触发多次,次数 = 子组件数量) 子组件 linked 父组件 ready 子组件 ready 因此如果是这么使用 [代码]t-cell-group[代码]: [代码]<t-cell-group> <t-cell title="cell1" /> <t-cell title="cell2" /> <t-cell title="cell3" /> </t-cell-group> [代码] 那么子组件 cell 的 [代码]setData[代码] 触发次数为:1 + 2 + 3 = 6 次。 但其实开发者的预期应该是 1 次,所以 [代码]updateLastChid[代码] 应该放在父组件的 ready 方法里才符合预期。 总结 以上是在小程序开发的过程中,常遇到的问题。但如果没有像 TDesign 组件库这样深入开发小程序,可能并不会去深入钻研生命周期的细节。但在日常的业务开发当中,如果开发者能够清晰地理解各种生命周期的本质,在遇到其他问题的时候,也能比较快速地定位问题的关键点。 如果能到达这样的效果,也是笔者写下这篇文章的初心。 更多内容关注:https://github.com/LeeJim
2023-08-18 - 🎆我们开源啦 | 基于Skyline开发的组件库🚀
我们开源啦,希望可以给大家的开发之旅带来一些灵感。我后溪的小程序也都会基于这个组件库开发,并且会保持组件库的更新与维护。 我是第一次进行开源,肯定会有错漏,欢迎大家指正,我会以最快的时间响应修改。 Skyline UI 组件库 前言 Skyline 是微信小程序推出的一个类原生的渲染引擎,其使用更精简高效的渲染管线,性能比 WebView 更优异,并且带来诸多增强特性,如 Worklet 动画、手势系统、自定义路由、共享元素等。 使用这个组件库的前提是:通过微信小程序原生+skyline框架开发,所以目前我们不保证兼容webview框架(也就是电脑端与低版本的微信),但后续会进行系统性的兼容。 使用 Skyline UI前,请确保你已经学习过微信官方的 微信小程序开发文档 和 Skyline 渲染引擎文档 。 背景 随着Skyline 渲染引擎 1.1.0 版本发布,我们所运营的小程序也平稳的渡过了阵痛期,团队使用Skyline也越来得心应手,所以接下来,团队的开发重心全面偏向Skyline渲染框架,考虑有大量的UI交互重复,我们决定基于Skyline开发了这个UI组件库。 但团队力量有限,这个新生的组件可能有很多的不尽如人意,所以希望能以开源的方式吸引更多开发者使用Skyline框架,如果这个框架不适合你,也可以借鉴其思路。 Gitee Gitee仓库 在线预览 以下是目前两个使用该框架的小程序 SkylineUI组件库 [图片] NONZERO COFFEE [图片] 开始使用 UI库结构 Skyline UI组件库 依赖于以下四部分,具体使用参考以下的具体说明 utils工具库: 其中包含了UI库自定义的一个工具类SkyUtils,它包含了组件中所含的各种函数,非常重要。 各组件元素:sky-*(组件名) skywxss样式库:其中包含深浅色色彩、文字字体、布局等样式wxss 在小程序中引入 UI库 一、直接下载引入 点击下载组件包 将src下所有文件复制到您项目根目录下的components文件夹中,没有的话请自行新建。 二、npm引入 1.在小程序项目中,可以通过 npm 的方式引入 SkylineUI组件库 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 2.安装组件库 [代码]npm install jieyue-ui-com [代码] 3.npm 命令执行完后,需要在开发者工具的项目中点菜单栏中的 工具 - 构建 npm 两种引入方式的不同可能导致后续使用时,引用组件的路径不同,请注意区别 1.直接引入components文件夹内,引用地址通常是 ‘./components/‘ 2.npm引入,组件引用地址通常是’./miniprogram_npm/jieyue-ui-com/’ 如何使用 1.在app.js文件中初始化工具类,并且添加两个全局变量 [代码]// app.js App({ onLaunch() { ;(async ()=>{ // 全局注册工具类SkyUtils // 这里默认npm引用,地址为'./components/utils/skyUtils',如果是直接引用组件,地址可能是'./components/utils/skyUtils',后面不再说明 const SkyUtils = await import('./components/utils/skyUtils'); wx.SkyUtils = SkyUtils.default; // 初始化设备与系统数据 wx.SkyUtils.skyInit() // 小程序自动更新方法 wx.SkyUtils.versionUpdate() })() }, globalData: { sky_system:{}, sky_menu:{} }, }) [代码] 2.在app.wxss文件中引入样式文件 [代码]//wxss * _dark.wxss 是适配深色模式的色彩变量 @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor_dark.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfontline.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfont.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyother.wxss'; [代码] 3.page.json中引用组件 [代码]//page.json { "usingComponents": { "sky-text":"/miniprogram_npm/jieyue-ui-com/sky-text/sky-text" } } [代码] 4.页面中使用 [代码] // wxml <sky-text content="文本内容" max-lines="2" fade></sky-text> [代码] 5.其他组件具体使用请参考组件包中的redeme.md 适配深色模式 如果您在开发时,全部使用我们预设好的颜色变量,那么可以自动适配深色模式。 [代码].page{ background-color: var(--bg-l0); } [代码] [代码] <view style="background-color: var(--bg-l0)"></view> <view style="background-color: {{color}}"></view> [代码] [代码] Page({ data: { color: "var(--bg-l0)" } }) [代码]
2024-01-09 - 用小程序wxs做一个时间格式化的过滤器
为什么用wxs格式化时间呢? 使用小程序的wxs格式化时间可以使代码更简洁、易读,并且可以避免在多个地方使用相同的时间格式化代码,提高了代码的可维护性和可重用性。此外,wxs运行在渲染层,可以减少渲染层和逻辑层之间的通信开销,提高小程序的性能。 只需要三步,就可以做一个格式化时间的过滤器(代码亲测可用) 1 在小程序的工程目录中,创建一个名为“filters”的文件夹,然后在该文件夹下创建一个名为“timeFilter.wxs”的文件,用于实现时间格式化过滤器。 2 在“timeFilter.wxs”文件中,定义一个名为“timeFormat”的函数,用于格式化时间。该函数接收一个时间参数(可以是当前时间、时间戳、毫秒时间戳或Date格式的时间),以及一个格式化字符串参数(例如“yyyy-MM-dd HH:mm:ss”),返回格式化后的时间字符串。 function timeFormat(timestamp, format) { if (!format) { format = "yyyy-MM-dd hh:mm:ss"; } var realDate if ((timestamp + '').length == 10 || (timestamp + '').length == 13) { if ((timestamp + '').length == 10) timestamp = timestamp * 1000; timestamp = parseInt(timestamp); realDate = getDate(timestamp); } else { timestamp = timestamp.replace(getRegExp("T", 'g'), ' '); timestamp = timestamp.replace(getRegExp("-", 'g'), '/'); realDate = timestamp; } var realDate = getDate(timestamp); function timeFormat(num) { return num < 10 ? '0' + num : num; } var date = [ ["M+", timeFormat(realDate.getMonth() + 1)], ["d+", timeFormat(realDate.getDate())], ["h+", timeFormat(realDate.getHours())], ["m+", timeFormat(realDate.getMinutes())], ["s+", timeFormat(realDate.getSeconds())], ["q+", Math.floor((realDate.getMonth() + 3) / 3)], ["S+", realDate.getMilliseconds()], ]; var reg1 = regYear.exec(format); if (reg1) { format = format.replace(reg1[1], (realDate.getFullYear() + '').substring(4 - reg1[1].length)); } for (var i = 0; i < date.length; i++) { var k = date[i][0]; var v = date[i][1]; var reg2 = getRegExp("(" + k + ")").exec(format); if (reg2) { format = format.replace(reg2[1], reg2[1].length == 1 ? v : ("00" + v).substring(("" + v).length)); } } return format; } var regYear = getRegExp("(y+)", "i"); module.exports = { timeFormat: timeFormat }; 3 在需要使用过滤器的页面的.wxml文件中,引入“timeFilter.wxs”文件,使用<wxs>标签将其定义为一个模块,然后在需要格式化时间的地方使用过滤器。 <wxs module="timeFilter" src="../test/fimeFilter.wxs"></wxs> <!-- 格式化时间戳 --> <view>{{timeFilter.timeFormat(1613509857, 'yyyy-MM-dd hh:mm:ss')}}</view> <!-- 格式化毫秒时间戳 --> <view>{{timeFilter.timeFormat(1613509857123, 'yyyy-MM-dd hh:mm:ss')}}</view> <!-- 格式化Date格式的时间 --> <view>{{timeFilter.timeFormat('2022-02-12T12:00:00', 'yyyy-MM-dd hh:mm:ss')}}</view> <view>{{timeFilter.timeFormat('2022/02/12 12:00:00', 'yyyy-MM-dd hh:mm:ss')}}</view> 当然,小程序的wxs不仅仅可以格式化时间,还可以用于实现一些数据的计算、格式化、过滤等功能,主要包括以下方面: 数据的格式化:可以将时间、货币、数字等数据进行格式化,例如将时间转换为指定格式的字符串,将数字保留指定位数的小数等。数据的计算:可以进行简单的数学运算,例如加减乘除、求余数等。数据的过滤:可以实现对数据的筛选和过滤,例如过滤出符合条件的数组元素、去除字符串中的空格等。数据的操作:可以对数据进行一些简单的操作,例如获取数组的长度、获取字符串的子串等。 需要注意的是,小程序的wxs并不是完整的JavaScript语言,它是在JavaScript的基础上进行了一定的限制和扩展。因此,在使用wxs时需要遵守小程序的规范和限制,例如不能使用全局变量、不能直接访问页面的DOM元素等。 更多关于wxs的详细说明,可以移步官方文档 《WXS介绍》
2023-02-15 - Skyline 转场动画轻松实现
在之前的 Skyline|小程序页面转场动画 文章中,Skyline 支持了自定义路由,开发者可以根据业务的需求来自行编写页面转场动画。 文章发布之后,我们收到不少开发者的反馈,对于“半屏”打开的转场动画是十分常见的,希望官方可以内置该能力。 为了降低开发成本,从基础库 v3.1.0 开始,Skyline 预设了一些常见的路由动画效果~ routeType 动画效果 wx://bottom-sheet 向上半屏弹窗,前一个页面不变 wx://upwards 向上进入页面,前一个页面不变 wx://zoom 放大进入页面,前一个页面不变 wx://cupertino-modal 向上打开页面至胶囊下面的位置,前一个页面收缩下沉 wx://cupertino-modal-inside 被 wx://cupertino-modal 打开的页面需要继续使用 wx://cupertino-modal 打开效果 wx://modal-navigation 被 wx://cupertino-modal 或 wx://modal 打开的页面向左进入页面的效果,前一个页面不变 wx://modal 向上打开页面至胶囊下面的位置,前一个页面不变 对于以上的 routeType,使用起来非常简单,因为动画效果已经由基础库内置了,所以开发者直接使用即可 [代码]// 演示使用 wx://modal 进入页面 wx.navigateTo({ url: 'xxx', routeType: 'wx://modal' }) [代码] 让我们来看看动画效果吧~ 1、向上半屏弹窗 & 向上进入页面 & 放大进入页面 除了默认的向左进入页面外,基础库内置了向上半屏弹窗 & 向上进入页面 & 放大进入页面 这几个动画效果,开发者可以根据业务自身情况来使用 [图片] 2、向上打开页面至胶囊下面的位置 这个动画效果在原生 APP 中的使用十分常见,对于前一个页面的处理和后面页面打开的动画效果略有不同 我们先来看 wx://cupertino-modal 的路由效果(图左)向上打开页面至胶囊下面的位置,前一个页面收缩下沉 在使用 wx://cupertino-modal 打开的页面之后,有两种页面打开形式 · wx://cupertino-modal-inside(图中):新页面打开方式同 wx://cupertino-modal 一致 · wx://modal-navigation(图右):新页面向左进入,前一个页面不变 [图片] 前一个页面的效果除了下沉收缩,也支持保持不变,使用起来就比较简单 使用 wx://modal 向上打开页面至胶囊下面的位置,前一个页面不变。新页面需要使用 wx://modal-navigation 向左进入,前一个页面不变 [图片] 预设路由使用简单,直接在 wx.navigateTo 配置参数即可,如果想要更丰富、更多的自定义的路由效果,大家可以使用 自定义路由 来自行定制~
2023-10-23 - 微信小程序-图片宽高自适应设置
微信小程序中,有很做组件都是有默认宽高的,比如,image组件默认宽度320px、⾼度240px,这些默认设置常常会对我们的页面布局造成影响 <!-- mode="widthFix" 让图片占满宽度,等比例缩放高度 --> <image src="xxxx" mode="widthFix"></image> 所以,在使用image标签的时候,最常用的方法就是设置其 mode,例如用widthFix,实现图片占满宽度,同时按照宽度来拉伸图片高度,这个mode是最常用的image模式。 有时候需要根据图片的宽高来计算出image标签的高度,这就需要用到一个公式了(目的是为了让展示图片根据原图进行等比例的缩放) [图片] 而且,容器.width基本都会设置为100%(100vw),再加上能查看到图片的宽高,就可以根据下面的公式计算出应该设置的容器的高度,进而展现出最完整的图片 [图片] 例如:设置容器的高度就能在wxss样式中通过 【 calc 】 来计算出了 [图片] 这样展示出来的图片就是按照原图的比例缩放的,不会造成超出或者拉伸。
2023-10-17 - 微信小程序-封装请求API——promise方式
目录 第一步:在app.js同级目录下,创建一个文件夹 第二步:封装wx.request方法成promise对象 第三步:页面中引用封装的请求API 1.设置基础请求路径 2.解构传入的参数 3.根据不同的url接口添加不同的header 4.添加请求发起时页面loading效果 完整的封装的API 的 js文件 微信小程序原生的请求API就是wx.request [代码]wx.request({ url: 'example.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 有时候不能很好的适配我们的开发需求,比如我们要加一些基础url路径、请求前后的loading效果、不同接口名称下的header。而且,在success回调方法里写请求成功后的操作,看起来代码不太清晰。接下来讲一下封装的逻辑,完整代码放在最后。 第一步:在app.js同级目录下,创建一个文件夹 [图片] 在utils文件夹里新建一个service.js文件,用来放封装的wx.request方法 第二步:封装wx.request方法成promise对象 使用promise对象能很好的解决回调地狱,在.then(res=>{}).catch(err=>{})中能很清晰地看出代码的逻辑 [代码]export const 返回出去的方法名 = (parmas) => { // 返回一个promise对象 return new Promise((resolve, reject) => { wx.request({ url: parmas.url, //仅为示例,并非真实的接口地址 data:parmas.data, header: { 'content-type': 'application/json' // 默认值 }, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) } [代码] 注意:success: (result) => {} 使用箭头函数,防止出现this指向错误 这样,就算是封装了一个最简单、最基础(简陋)的请求API了,在需要使用这个方法的页面的js文件中,引入它 第三步:页面中引用封装的请求API [代码]/** * 小程序中要引用方法,哪个页面要用,就在哪个页面引入 */ import { cjRequest } from "../../utils/service"; Page({ /** * 页面的初始数据 */ data: { .......... .......... getGoodsList () { //因为返回的是promise对象,所以通过.then来获取resolve出来的请求成功的返回数据 cjRequest({ url: "https://xxxtest/goods/search", data: this.QueryParams }) .then((res) => { console.log(res); }) } [代码] 现在,来详细扩展一下封装的请求API 1.设置基础请求路径 [代码]// 基础url const baseUrl = "https://xxxtest" [代码] 这样,就可以简化调用这个方法时url的参数内容了,也方便统一修改开发环境地址、生产环境地址 [代码]// url: "/goods/search" ==》 url中不用再写前面的一长串了 cjRequest({ url: "/goods/search", data: this.QueryParams }).then((res) => { console.log(res); }) [代码] 2.解构传入的参数 我们可以通过ES6中的扩展运算符,将传入到封装方法里的参数直接全部解构出来,不用再一个个获取赋值给对应的键值对了 …parmas 直接解构出传入的参数 [代码]export const 返回出去的方法名 = (parmas) => { //设置基础请求头 const baseUrl = "https://xxxtest" // 返回一个promise对象 return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其解构出的,传入的url:xxx参数 url: baseUrl + parmas.url, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) } [代码] 3.根据不同的url接口添加不同的header header中的Authorization字段一般存储token,用来做身份验证,但有些请求不需要携带token,所以这个封装的API中需要能根据传入的url来判断什么时候该给请求添加上token,什么时候不用可以添加。 [代码]``` export const 返回出去的方法名 = (parmas) => { /** * 根据不同的url接口,来设置不同的header请求头 ** 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token ** { ...parmas.header } ==> 先解构出传进来的header对象,然后再往这个对象里面添加 Authorization字段数据,这样即使有传入header的其他字段也能保留下来 * 如果传入的parmas参数中没有header,那myHeader就是个空的对象 {} 因为啥都没有 */ let myHeader = { ...parmas.header }; //通过includes方法查找字符串中是否包含指定内容,进而判断是否要添加token if (parmas.url.includes("/neddToken/")) { // 往myHeader这个对象里插入键值对 带上Storage中存储的token myHeader["Authorization"] = wx.getStorageSync("token"); } //设置基础请求头 const baseUrl = "https://xxxtest" // 返回一个promise对象 return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其解构出的,传入的url:xxx参数 url: baseUrl + parmas.url, /** * !可以设置上默认的content-type,然后再扩展出传入的myHeader,如果传入的myHeader 为空,那header就还是默认的content-type一个键值对 * !{ 'content-type': 'application/json', ...myHeader } ==》 扩展出myHeader这 个对象中的键值对; */ header: { 'content-type': 'application/json', ...myHeader }, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) [代码] } [代码][代码] 此时,在使用这个封装的API的时候,就可以对header进行设置了,例如: [代码] ``` cjRequest({ url: '/neddToken/home/swiperdata', // 使用的时候也可以传入一些header的字段 header: { 'content-type': 'application/json', 'Date': 'Tue, 15 Nov 2021 08:12:31 GMT' }, method: 'GET', }).then((result) => { console.log(result) }) [代码] [代码][代码] 此时,因为请求url中含有【neddToken】,就会在header中插入token [图片] 此时,这个请求的header中就会添加上token(也就是Authorization这个字段了) [图片] 4.添加请求发起时页面loading效果 当页面在加载数据的时候,最好要有一个loading的提示,同时有遮罩,防止用户乱点 所以,就需要在封装的API中加入微信小程序的wx.showLoading遮罩层了 [代码] // 显示加载中loading效果 wx.showLoading({ title: "加载中", mask: true //开启蒙版遮罩 }); ... ... // 关闭正在等待loading效果 wx.hideLoading(); [代码] 如果直接在封装的API的开始加上loading,在请求结束加上隐藏loading效果,那乍看一下,好像没错,但是细想一下,如果一个页面同时触发了多个请求呢?比如打开一个页面,同时加载多个模块,需要从不同的接口请求数据,那就会使用多次这个封装的API。 此时,就会出现,第一个请求结束,直接关闭了loading效果,而后面几个请求就没有loading效果的遮罩了。 所以,需要在封装的js文件中设置一个全局变量,每次调用这个封装的文件时,就对这个变量++,每次请求结束,返回数据出去的时候,就对这个变量–,最后判断一下这个变量是否为0(也就是所有请求的结束了),在决定是否关闭loading效果 [图片] [图片] 完整的封装的API 的 js文件 [代码]// 同时发送异步代码的次数 let ajaxTimes = 0; export const cjRequest = (parmas) => { // 当有地方调用请求方法的时候,就增加全局变量,用于判断有几个请求了 ajaxTimes++; // 显示加载中loading效果 wx.showLoading({ title: "加载中", mask: true //开启蒙版遮罩 }); /** * 根据不同的url接口,来设置不同的header请求头 ** 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token ** { ...parmas.header } ==> 先解构出传进来的header对象,然后再往这个对象里面添加Authorization字段数据,这样即使有传入header的其他字段也能保留下来 *? 如果传入的parmas参数中没有header,那myHeader就是个空的对象 {} 因为啥都没有 */ let myHeader = { ...parmas.header }; if (parmas.url.includes("/neddToken/")) { // 往myHeader这个对象里插入键值对 带上Storage中存储的token myHeader["Authorization"] = wx.getStorageSync("token"); } // 基础url const baseUrl = "https://api-hmugo-web.itheima.net/api/public/v1" return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其传入的url:xxx参数 url: baseUrl + parmas.url, /** * !可以设置上默认的content-type,然后再扩展出传入的myHeader,如果传入的myHeader为空,那header就还是默认的content-type一个键值对 * !{ 'content-type': 'application/json', ...myHeader } ==》 扩展出myHeader这个对象中的键值对; */ header: { 'content-type': 'application/json', ...myHeader }, success: (result) => { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, // 不管请求成功还是失败,都会触发 complete: () => { /** * !loading效果同时被多个请求触发是可以显示一个的,但是关闭loading一旦被第一个请求完成后关闭,后面的请求触发的loading效果就没了 * !所以,需要通过全局设置一个变量,来监听同时触发了几个请求,当最后一个请求完成后,再关闭loading * ?每次结束请求后,就减少全局变量,当为0时,就表示这是最后一个请求了 */ ajaxTimes--; // 此时就可以关闭loading效果了 if (ajaxTimes === 0) { // 关闭正在等待loading效果 wx.hideLoading(); } } }); }) } [代码] 如果需要按照原生微信请求的格式来封装(调用时要传各种方法、参数),请参考: 微信小程序-封装请求API——原生方式_五速无头怪的博客-CSDN博客[图片]https://blog.csdn.net/black_cat7/article/details/120697607 参考:黑马微信商场小程序 黑马程序员微信小程序开发前端教程_零基础玩转微信小程序_哔哩哔哩_bilibili[图片]https://www.bilibili.com/video/BV1nE41117BQ?p=3
2023-10-18 - Skyline | 它超赞耶!Skyline 商用初体验 -- NONZERO COFFEE
抛开软件功能不谈,在交互体验上,打造一款能和原生APP相媲美的微信小程序,是我这样的微信开发者的梦想。想在技术上想实现原生的功能,开发成本是非常巨大的。但感谢微信小程序推出的渲染引擎Skyline,让我可以轻松实现原生app的交互体验。 目前,小程序已上线运行半月,虽有bug,但瑕不掩瑜。如果您对于使用webView开发还是Skyline开发还有疑虑的话,希望以下介绍对于有选择困难症的同学们有所帮助。接下来,我想借助自己开发的小程序,向大家推荐与介绍Skyline渲染引擎(当然也希望向大家推荐我所开发的小程序 -- 「 NONZERO COFFEE 」)。 如有错误或遗漏,欢迎在评论区批评指正,不胜感激。 小程序效果演示 [图片] 对小程序感兴趣的也可以加我好友讨论技术: [图片] 开发文档中对于Skyline的介绍,有以下几个重要的增强特性:worklet 动画、手势系统、自定义路由(预设路由)、共享元素动画。对于他们的使用和说明,我就不过多说明了,具体可以参照:https://mp.weixin.qq.com/s/dRz2PnkwHxYVL2kCexQ7WQ。 接下来我会介绍这些特性在小程序中,是怎么变成我的奇淫巧技,让小程序也可以更加优美。 worklet 动画:丝滑与复杂的页面元素展示 当页面滚动时,我们通常会希望根据页面不同的滚动状态或位置,灵活的展示或改变某些元素的状态,那么借助scroll-view的worklet:onscrollupdate时间和worklet动画可以完美实现效果~ [图片] 1、展示中可以看到背景图片会随着下拉放大,但是上滑时却是跟随上滑,完美实现不同状态 2、为了优化顾客点单体验,主要菜单栏(点单、外卖、商城)需要在上滑至离开屏幕时,顶部需要弹出备用菜单栏. 以上效果如果通过简单的wx:if判断,那么动画效果势必会很突兀,如果使用worklet 动画的timing()与spring()函数,可以轻松实现动画的流畅效果。 附上简单代码: 这里是置顶的点单菜单栏 这里是页面内容和 onLoad(options) { // 背景图片的高度和缩放 this.bgimgTop = shared(0) this.bgimgSca = shared(1) // 顶部点单栏透明度 this.topfunOp = shared(0) }, onReady() { // 背景图片动画监听 this.applyAnimatedStyle('.bgimg', () => { 'worklet' return {top: `${this.bgimgTop.value}px`,transform:`scale(${this.bgimgSca.value},${this.bgimgSca.value})`} }) // 顶部点单栏动画监听 this.applyAnimatedStyle('.fixtopview', () => { 'worklet' return {opacity: `${this.topfunOp.value}`} }) }, scrolling(e){ "worklet" let scrollTop = e.detail.scrollTop if(scrollTop <= 0){ //向下滚动 this.bgimgTop.value = 0 this.bgimgSca.value = 1 + (-scrollTop / 800) this.topfunOp.value = 0 }else{ //向上滑动 this.bgimgTop.value = - scrollTop this.bgimgSca.value = 1 let opTemp = (scrollTop / 200) this.topfunOp.value = opTemp >= 1 ? 1 : opTemp } }, Tip:由于代码量较大,以下说明不会附上详细代码,待后续整理好源码,会开源的 手势系统:提升用户留存率,神之一手 [图片] 这个实例呢,和第一个非常相似,是因为其中也 用到了worklet动画,换句话说,如果使用了Skyline框架,那么worklet将萦绕整个开发进展,它即代替了wss和css动画。 另外,更重要的是这个实例运用了手势系统,概而述之,就是使用<pan-gesture-handler><vertical-drag-gesture-handler>去更精细化的代理使用和监听<scroll-view>组件。 一、首页由<pan-gesture-handler>判断页面状态,状态1:往上拖动,不处理滚动事件,而是负责顶部搜索按钮和搜索框的动画效果、背景图片的缩放效果、以及整个容器的高度变化;状态2:向下滑动,则处理滚动事件 二、当判断向下滑动,处理滚动事件时,先由<vertical-drag-gesture-handler>判断<scroll-view>是否触顶,接下来也有两种状态,状态1:触顶,则改变由<vertical-drag-gesture-handler>接管,处理容器下滑的动画;状态2:没有触顶,则是<scroll-view>的正常滑动。 自定义路由 :小程序焕然一新,媲美原生 示例一: [图片] 这个详情弹出页的效果和webview下的 <page-container>组件的效果基本相似,如果这里简单的使用跳转新页面,则不符合UI设计稿,使用页面弹出组件时,又会发生用户的返回操作直接退出商品列表页,所以这里使用的自定义路由,并且还利用了worlklet和手势系统,实现了手势下滑与左滑时,如果下滑距离和下滑速度足够,则退出,不足够,则回弹恢复。 示例二: [图片] 这里的自定义路由,效果不明显,但是恰恰就需要这种效果。简单来说就是用户点击搜索框,需要跳转到搜索页面,但是设计稿中需要这种跳转是无感的,而自定义路由中将transitionDuration跳转时间设置为10或者更短,并且将handlePrimaryAnimation跳转效果设置成空值,即可完美实现。以下是简单的自定义路由配置代码: const ShopSearchRouteBuilder = (customRouteContext) => { console.info('skyline: half page route build') /** * 1. 手势拖动时采用原始值 * 2. 页面进入时采用 curve 曲线生成的值 * 3. 页面返回时采用 reverseCurve 生成的值 */ const handlePrimaryAnimation = () => { 'worklet' return { } } return { opaque: false, handlePrimaryAnimation, transitionDuration: 10, reverseTransitionDuration: 10, barrierDismissible:true, canTransitionTo: false, canTransitionFrom: false, } } 共享元素 :我的奇奇怪怪的使用 [图片] 如果没有看出这个例子的共享元素是如何使用的,那么我用webview的页面来分解一下。 [图片] [图片] 这里的商品列表页和购物车其实是两个页面,但是底部的购物车按钮元素,又需要在两个页面跳转时,无感展示,所以这里是最适合使用共享元素的,我个人认为,这比跳转页面时使详情图片在不同页面间飞跃更加高级和实用。 但是既然说到了图片飞跃的效果,下面我也来展示几个实用例子: [图片] [图片] 这里的两个例子也都是列表页跳转至详情页时,实例图片的飞跃。 当然小程序中的动画效果可不止以上罗列的这么几个,总而言之,使用了Skyline之后,以上的动画效果在小程序中随处可见,如果不是本人的代码能力有限,我想小程序的整体效果完全可以媲美APP的原生效果。 希望以上这些能够为即将进行微信小程序开发,而苦恼于是否使用Skyline的同学们,带来新的思路。 Skyline的bug:背刺与惊喜 新技术需要有勇气去使用,去克服遇到的一切问题我遇到了很多bug,虽然他总会在我意料不到的地方背刺我一刀,但是Skyline的效果实在让我欲罢不能,万分庆幸的是我真的要非常感谢微信团队的老师们,给了我很多建议与帮助。 还没有经过老师同意上图,暂时先打个码。 [图片] [图片] 好了,接下来说一下,现在还没有解决的bug,希望微信团队能尽快修复。 1、小程序使用的是自定义tabbar,在ios下,相隔较长时间再重新打开小程序,会出现白屏的现象,是完全不显示任何元素,但vconsole是可以看到各生命周期已经正常允许了的。这里实在不好上传相关代码,因为首页代码很复杂。还关联了全局状态管理。 与其他同学沟通过,发现并不是我的单一案例,也有人复现了。 解决方案:万幸我使用的是自定义tabbar,小程序启动首页被我设置成了某一个单页面,然后从单页面跳转到tabbar首页,并且这个单页面还能设置成宣传海报首页,这也算是一个自洽的解决方法。 2、wx.switchTab()方法:前提依旧是自定义tabbar,假设tabbar有A、B、C、D首先进入了tabbar首页A页面,然后跳转到其他页面,如果这时使用wx.switchTab()跳转至B\C\D页面,会出现tabbar栏不出现的情况。 解决方案:全部跳转至A首页,或者tabbar页面底部安置一个假的tabbar栏。 3、最麻烦的问题:如果我不考虑兼容的问题,那么即代表我需要抛弃一部分没有升级微信版本习惯的用户,因为低版本的微信无法正常使用Skyline框架开发的小程序。 解决方案:如果您的开发成本和时间足够,完全可以慢慢兼容、如果还在纠结,建议您单页面慢慢升级Skyline,Skyline支持单页面渲染(这可真是太棒了),但是对于我这种有代码强迫症的人来说,这是硬伤,我不能忍受一个页面是webview,另一个是skyline,我只能慢慢熬过用户更新的阵痛期。 最后,我还想说几句话: 1、开源这件事,我已经做好了准备,但是开源成本太大,我还没考虑好是否花时间进行开源,总之,有需要的同学,可以写个评论支持下。 2、开发: (1)前端:有80%以上的图片与文字,都是通过后台数据渲染的,也就是说部署后,基本不需要修改小程序代码,但是自由度极高。 (2)后台框架:考虑到是小项目,只使用了java的springboot,数据库使用的是mySql,缓存则也是通过代码实现的(不使用redis,是因为云托管不支持,单独买太贵了),存储使用的腾讯的COS。 (3)后台部署:我抛弃了传统服务器,拥抱了微信云托管。 (4)管理admin页面:使用的是antd admin(vue2),部署方面同样也是使用的云托管的静态资源存储。
2023-10-10 - 小程序隐私协议开发指南仿微信读书授权DEOM示例
一:效果预览图 [图片] 二:代码片段 https://developers.weixin.qq.com/s/UuaRwcm874LW 三:参考文献 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html 一、功能介绍涉及处理用户个人信息的小程序开发者,需通过弹窗等明显方式提示用户阅读隐私政策等收集使用规则。 为规范开发者的用户个人信息处理行为,保障用户合法权益,微信要求开发者主动同步微信当前用户已阅读并同意小程序的隐私政策等收集使用规则,方可调用微信提供的隐私接口。 特别注意: 以下指南中涉及的 getPrivacySetting、onNeedPrivacyAuthorization、requirePrivacyAuthorize 等接口,目前仍处于 Beta 调试阶段,目前并未按预期返回正确的结果。开发者目前可以先阅读本指南文档和接口文档进行理解,平台将会尽快正式上线这些接口以及调试方法,上线后会在本指南文档和相关公告中进行说明。 2023.08.22更新: 以下指南中涉及的 getPrivacySetting、onNeedPrivacyAuthorization、requirePrivacyAuthorize 等接口目前可以正常接入调试。调试说明: 在 2023年9月15号之前,在 app.json 中配置 [代码]__usePrivacyCheck__: true[代码] 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。在 2023年9月15号之后,不论 app.json 中是否有配置 [代码]__usePrivacyCheck__[代码],隐私相关功能都会启用。 <view wx:if="{{showPrivacy}}"> <view>隐私弹窗内容....</view> <button bindtap="handleOpenPrivacyContract">查看隐私协议</button> <button id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button> </view> 四:参考示例 [图片] 五:注意事项 如果使用了 button 组件 open-type 设置了 getPhoneNumber 属性, bindgetphonenumber 。当你新增了属性功能 "__usePrivacyCheck__": true, 那么 授权 wx.getPrivacySetting 调用成功,但是当你未授权或取消时, 切换使用 button 组件 open-type = getPhoneNumber ,可能调用失败,可以参考如下设置代码 ,避免出现 {errno: 104, errMsg: "privacy permission is not authorized"} openType:'getPhoneNumber|agreePrivacyAuthorization' // 示例 if(wx.getPrivacySetting) { this.setData({ openType:'getPhoneNumber|agreePrivacyAuthorization' }) }else { this.setData({ openType:'getPhoneNumber' }) }
2023-09-13 - 【日常杂记】小程序源生icon不垂直居中问题
[图片] 原生icon不居中怎么解决? <icon class="icon-small" type="circle" size="40rpx"></icon> 你会发现 无论是 flex布局 还是 vertical-align: middle;都无法解决莫名其妙多出来的高度 这时候可以这样:直接写死高度 [图片] [图片]
2023-09-18 - 体验skyline模式,写一个国庆头像生成小程序
小程序演示 [图片] 截图 [图片] [图片] 上手指南 该小程序使用了微信的Skyline渲染引擎,效率更高体验更好,可以实现类似于flutter Hero动画(见上图效果)。头像生成使用的是Snapshot进行合成,这个东西很方便,不用使用canvas进行繁琐的绘制处理,只要把布局写好就行,类似html2canvas。 官方[代码]onChooseAvatar[代码]接口获取头像的接口实在太模糊没法使用,因此换成了[代码]wx.chooseMedia[代码] 注意:由于使用了一些新特效所以项目无法在WebView下运行,需使用Skyline引擎,如果考虑到用户兼容性的慎用。 开发前的配置要求 微信版本库 3.0.0 安装步骤 克隆或下载本项目到目录中使用微信开发者工具导入即可 [代码]git clone https://github.com/QQOQ/mp-avatar.git [代码] 修改根目录下的[代码]env.config.js[代码]文件,里面配置你的接口地址 在[代码]app.js[代码]处有一个请求素材的接口,可以修改此处为你的接口,返回格式如下: [代码]{ "default_template": "https://51porn.oss-cn-hangzhou.aliyuncs.com/hat5.png", "default_avatar": "https://51porn.oss-cn-hangzhou.aliyuncs.com/demo.jpg", "template_list": [ "https://51porn.oss-cn-hangzhou.aliyuncs.com/hat0.png", "https://51porn.oss-cn-hangzhou.aliyuncs.com/hat1.png" ] } [代码] [代码]default_template: 默认模板 default_avatar: 默认头像 template_list: 模板列表 [代码] 特别说明 该项目素材均来自于互联网,如有侵权请联系本人删除。
2023-09-27 - 推荐 7 款小程序产品运营必备的官方小程序
前言 接触小程序从2018年到现在已经5年多了,从最开始进入公司管理小程序团队,到后来全职做一名小程序独立开发,再到现在创业组建一个小团队,大大小小的小程序累计加起来差不多100多个,在这个过程中这 7 个官方小程序一直在用。 小程序 以下均为微信官方小程序,直接在微信搜一搜名字即可查到。 微信指数 [图片] 微信指数小程序对我来说使用频率最高了,无论是小程序方向的决策还是公众号文章内容方向的决策我都会先查找一下微信指数,如果内容有热度才能去做,否则做了也白做。 [图片] 使用非常简单,只需要输入关键词即可。能够查看这个词的指数趋势以及数据来源。 [图片] 小程序助手 [图片] 可以看作简化版的小程序管理后台,我用的最多的是审核管理。很多时候代码提交审核了,版本通过了,我人在外面,不方便用电脑,这个时候我就直接用这个小程序进行版本发布,非常方便。 [图片] We分析 [图片] 当小程序上线后,数据分析是必不可少的。从最开始的小程序数据助手到现在的We分析,我几乎每天都会查看数据分析数据来源,这样可以及时的去调整我的运用策略和发现程序问题。 [图片] 微信开放社区 [图片] 只要涉及到微信相关的问题,大部分都能在这个社区得到解决,在这里有很多常见问题的解决方案也有官方最新动态以及提出问题后能得到官方人员和贡献者的解答。 [图片] 我也是贡献者之一,截止到现在回答了3000多个问题和分享了100多篇文章。 [图片] 公众平台助手 [图片] 我主要是用它来查看公众号的数据统计。 [图片] 如果你有发布公众号的需求可以下载官方的【订阅号助手】App。 流量主服务助手 [图片] 小程序盈利模式无法就是广告模式或者付费模式,如果是广告那就用这个小程序。 [图片] 微信支付商家助手 [图片] 付费模式就用这个,在这里还可以直接解决客户投诉问题。 [图片] 最后 你在小程序开发过程中还有什么实用的小程序?欢迎留言一起讨论
2023-09-24 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 2023品牌新媒体矩阵营销洞察报告:流量内卷下,如何寻找增长新引擎?
[图片] 近年来,随着移动互联网的发展渗透,短视频、直播的兴起,新消费/新零售、兴趣电商/社交电商等的驱动下,布局线上渠道已成为绝大多数品牌的必然选择。 2022年,越来越多的品牌加入到自运营、自播的行列中,并且从单一的平台逐渐拓展到多平台,矩阵化打造,实现从品牌曝光、内容种草到消费转化、用户沉淀的营销闭环。 各短视频、电商平台也陆续推出诸多政策与活动,鼓励、扶持品牌商家自播,助推品牌在社媒平台长效经营。 然而面对线上流量红利的逐渐消退、获客成本的不断上涨,品牌如何通过布局矩阵,完成渠道建设,实现全域经营、业绩增长?从流量到留量,如何深挖用户价值,实现高效转化?面对海量的新媒体运营资产,如何科学、智能、精细化管控,赋能数字化转型、提升运营效率、降低运营成本,从而实现品牌企业社媒运营的持续发展? 为此,果集·云略推出《2023品牌新媒体矩阵营销洞察报告》,希望能给各品牌企业带来些许参考。 【云略Pro】公众号回复“品牌矩阵” 获取完整版报告 1、行业发展概况 (1)用户规模不断增长 电商市场前景可观 据中国互联网络信息中心(CNNIC)发布的第51次《中国互联网络发展状况统计报告》显示,截至2022年12月,我国网民规模达10.67亿,其中,短视频用户规模达10.12亿,同比增长7770万,占网民整体的94.8%。网络直播用户规模达7.51亿,同比增长4728万,占网民整体的70.3% 另一方面,据国家统计局数据显示,2022年,全国网上零售额137853亿元,比上年增长4.0%。其中,实物商品网上零售额119642亿元,增长6.2%,占社会消费品零售总额的比重增长至27.2%。 2022年中国直播电商市场规模超过3.4万亿,年增长率为53%,预计2023年规模将超4.9万亿。眼下疫情全面放开,经济逐步恢复,而伴随着短视频、直播用户规模的逐年增长,未来社媒电商市场前景可期。 (2)电商市场规模同比上涨84% 社媒平台持续火热 2022年,社媒平台持续火热。抖音、快手整体交易规模超万亿,同比上升84%,销量同比增长91%。 随着疫情的全面放开,2023年Q1再创新高,交易规模同比上升60%。视频号直播带货规模同样保持高速增长,销售额同比增长超8倍。小红书电商主播数量同比增长 337%,直播场次同比增长 214%。 (3)AI / 5G / VR 等新技术 为行业发展注入新的动力 得益于新技术的发展,给社媒电商行业带来了更多可能。如可运用在文案内容、模特图片制作、短视频制作、智能客服等方面,更好地实现降本增效。 此外,AI 与虚拟主播的结合,也日渐成为一种趋势。此前爆火的柳夜熙就深受大家的喜欢,也证明了其可能性。 2023年,AI 浪潮风起云涌,而社媒电商与 AI 等新技术的结合,也将为行业带来新的发展点与机遇,打破行业壁垒,实现新的突破。 [图片] 2、品牌布局矩阵的必要性 (1)用户消费时长增长 短视频直播成品牌营销新阵地 短视频、直播行业飞速发展,用户规模和消费时长不断攀升。尤其是短视频,近年来持续领跑用户时长占比,且持续保持较高增长率。短视频、直播日益成为企业树立品牌形象、内容营销种草、获取用户心智、业绩提效增长的重要渠道,从而实现可持续发展。 [图片] (2)品牌加速入场 多平台矩阵运营成共 随着互联网行业不断向纵深发展,内容形态与营销场景也更加多元化。越来越多的品牌跑步入场,深耕社媒营销,建立多平台营销矩阵,借助社媒平台的全域态势助力品牌增长。 据果集数据显示,目前有近23%的品牌选择跨平台运营。其中抖音与其他平台的组合是品牌首选的。 [图片] (3)品牌自播成风口 直播场次与销售业绩稳定增长 电商直播日益成为品牌社媒运营的重点。越来越多的品牌并不单纯地依赖和追求头部主播的强带货效应,而是逐渐发力品牌自播,通过店铺的常态化运营,做好客群关系的维护,及时获取用户反馈,以便更好优化选品与内容策略,打造品牌长效经营的阵地。 据果集数据显示,2022年全年抖音、快手品牌自播场次较2021年增长62.48%,品牌自播GMV增长56.68%。 (4)多渠道品牌营销推广 实现从种草到购买闭环 在移动互联网时代,市场细分越来越垂直,充分借助社媒平台,拓展多渠道营销,多触点发力,构建多维度营销矩阵,沉淀与品牌调性相符的渠道价值,实现销量增长、营销破圈、用户沉淀等核心诉求,才能更好做到品效合一。 3、品牌矩阵营销洞察 品牌案例—— YAYA 鸭鸭 (1)建立品牌直播矩阵 承接流量圈定精准人群 2020年6月,鸭鸭品牌入驻抖音,依靠与达人直播合作的模式快速实现冷启动。2020年11月,开始尝试品牌自播。2021年5月打造品牌店铺矩阵,通过裂变式店群,圈定精准人群,发力品牌自播。同年9月,入驻快手并开始直播。到了2022年,觉察到视频号电商的机会后,鸭鸭在视频号也开设了品牌自播。 [图片] (2)平台差异化定位 品牌自播贡献主要销售额 YAYA鸭鸭在社媒平台采取的是“自播+达播”的模式,但也会根据各平台的属性差异,运营的侧重点也略有不同。 比如抖音平台,前期依靠达人直播分销冷启动,但到了后期品牌自播的投入不断增加;而在快手上,“老铁”的私域特征更为突出,在信任电商下,达人分销比重会更多。 (3)多元场域联动提升品牌认知 主推明星单品打造超级爆款 [图片] 品牌案例—— Midea 美的 (1)线上数字化转型 实现品牌全域协同经营 近年来,随着社媒平台的快速发展,用户的注意力和消费习惯也逐渐往线上迁移。不少传统商家开始线上化、数字化转型,由原来单一的线下门店经营转向线上线下双驱动的模式,加速全渠道布局,努力寻找生意增长的新引擎。 从全网布局来看,美的品牌账号覆盖主要社媒平台、主要品类,“种+拔”一体化全链路布局,通过跨平台矩阵、账号矩阵的多种营销场景,实现品牌全域协同经营。 [图片] (2)加速布局品牌自运营体系 探索生意增长新引擎 高速发展的直播电商,让不少商家找到了新的发展机会。近年来,美的品牌加速布局直播电商自运营体系搭建,发力内容场景、货架场景、营销场景3大锚点,整合全场景,打造流量闭环渠道,实现深度转化与长效增长。 2022年全年同比2021年全年,美的品牌在抖快GMV增长66.64%,尤其是品牌号自播带来的销售明显增长,抖音渠道品牌号自播占比从37%增加至82%。 [图片] (3)破圈营销打造年轻潮流生活方式 节点营销引爆热点话题带动销量提升 [图片] 4、品牌矩阵搭建策略 与解决方案 什么是品牌新媒体矩阵? 能够触达目标群体,满足品牌销量增长、营销破圈、用户沉淀等核心诉求的多种新媒体渠道组合。 (1)常见的账号矩阵搭建结构 [图片] [图片] (2)品牌矩阵搭建策略 [图片] (3)品牌自播体系搭建策略 [图片] (4)各社媒平台特点 [图片] (5)组合拳出击 实现品效域合一 [图片] (6)品牌矩阵布局存在的问题 及解决方案 [图片] [图片] 因为篇幅有限,推文仅展示部分数据,【云略Pro】公众号后台回复“品牌矩阵”,可获取完整版PDF报告。
2023-06-30 - We分析 · 小程序推广分析 能力更新
推广分析是We分析推出的一项经营工具。可以帮助商户监测带参数的小程序页面路径数据,并便捷生成带参数推广素材。将推广素材投放至不同的推广渠道和应用场景后,可在We分析上查看不同推广计划的引流效果数据。 功能路径:【We分析-经营工具-推广分析】 [图片] 应用场景 [图片] 能力更新 1. 推广交易数据监测:访问数据基础上支持统计通过推广引流人群的交易数据,商户可自定义归因区间,确定合理的统计口径。例如,归因区间设定的为7天,该用户在访问推广计划后7天内所发生的交易转化都会归功于该推广计划。(仅开通支付的商户支持该能力) [图片] 2.素材类型拓展:创建推广计划后,根据需求可生成以下类型素材,支持批量生成。 [图片] 3.推广标签设置:可对推广计划所属标签进行标记(例如,为所有朋友圈广告渠道的计划打标),每个推广最多添加3个标签,可在推广看板中根据标签进行数据查看,对比分析不同标签的推广效果。 [图片] 4. 人群分析优化: (1)推广人群画像优化,支持查看引流整体人群、引流新用户、交易用户的基础画像; (2)可快捷创建推广计划的引流人群包,在【画像洞察】、【行为分析】中对引流群体做进一步的分析。 [图片] 详细可查看开放文档:使用指引。【推广分析】持续迭代中,关于【推广分析】的需求与反馈可加入下方交流群与我们反馈。 [图片]
2023-06-14 - 如何实现动态生成好友分享图
本文章采用官方最新的 Canvas.createImage() 来实现下动态生成好友分享图,可以拿来即用。展示效果如下(其中蓝框中文案和红框头像为插入的文本、图像。背景图也支持动态更换): [图片] 小程序demo案例:https://developers.weixin.qq.com/s/nJtr4QmL7RD3 一、市面案例缺陷:翻阅了目前市面上的小程序动态生成好友分享图,大部分还是使用已废弃的『wx.createSelectorQuer』接口来实现。目前小程序已无法很好支持。 二、主要有以下几个关键点需要注意下: 做好友分享图要考虑5:4的比例使用 wx.getImageInfo 一定要考虑图片失败的场景,然后采用兜底图片。相同的逻辑在complete中执行。要区分 wx.createImage ,这个是小游戏用来创建图片对象的。小程序要用Canvas.createImage()。也不是使用 new Image!!!使用的时候一定要在 canvas 类型中注明 type 是 2d 的 canvas[图片] 三、优化知识点: 如何用 async in image loading:https://stackoverflow.com/questions/46399223/async-await-in-image-loading ----- 采用await img.decode() 或者 img.onload = () => resolve() 如何隐藏Canvas:https://developers.weixin.qq.com/community/develop/doc/1aadfacdd9f38584881e0c50db2bcda1 ----- position:fixed;left:100%;
2023-06-19 - 如何策划一场运营活动?
前言 对于一款产品来说,想要快速提高一些指标,那么做活动肯定是一种必备的运营手段,那么做运营活动需要怎么做呢? 今天主要讲解做活动的 3 个步骤:活动策划、活动实施、活动复盘,让大家来了解做活动的整体框架。 活动策划 不要为了做活动而做好活动,第一步是搞清楚活动目标,常见的活动目标无非就是这 3 点: 拉新:让更多新的用户知道你的产品 促活:让用户提高产品使用频率和时长 转化:让用户付费,提高当前产品收益 比如:要推广一个课程,价格是 100 元,销售额目标达到 10w。 先来拆一下这个活动目标,销售额达到 10w,属于转化变现环节。量化活动目标,按照课程的售价,要达到 10w 的销售额,需要有 1000 个付费学员,按以往 10% 的付费转化率来算,需要拉新 10000 左右的目标用户。 再根据拉新用户数量=渠道用户数*拉新转化率,拉新转化率的平均数据是 5~10%,那么反推得出,我们要投放的种子流量池需要在 10~15w。 最终得出 3 个关键环节数据,我们需要投放 10~15w 的流量池,拉新 10000 用户,转化 1000 用户,才能完成销售额 10w 的目标。 确定要目标接下就是确定活动方案包含但不局限于,活动目标、活动主题、用户参与路径、目标受众类型、活动亮点等。 确定完活动方案,接下来就要输出更为细节的内容了。如文案内容、海报设计等。在写文案、做海报之前,多看竞品、看头部机构、看大平台同类主题,他们的文案是怎样写的,海报是怎样设计的,整理出来再优化。 最后,做一个时间进度排期,确定每天的工作内容,每天确认是否完成,进行检查。 活动实施 当方案确认完后,那就需要找资源合作方,确保流量是足够的,找合作进行联合推广。确定完合作方,确定宣发日期以及要宣发的内容,避免到活动宣发的时候,对方没有排期而耽误了宣发工作。 活动正式推广宣发时,先不要所有渠道全部推送,建议先推送到某个渠道,看看用户的反馈,如果发现问题,则及时优化,再推广至其他渠道。 使用AB测试,分别让组成成分相同或相似的用户,随机访问 AB 两个版本活动,收集用户数据,最后分析评估出最好的版本,投入使用。 如:将两张海报分别发布到 4 个社群,看不同海报的活动参与人数、拉新人数、推广人数、活动裂变率等,各个数据,来敲定最终的活动海报。 当用户决定付费时,会经历一段犹豫纠结的周期。作为运营,我们要做的就是缩短用户的决策周期,让用户尽快完成付费转化动作。 如:推出了一个 40 元的限时优惠券,同时为了让付费后的用户再分享,又结合了分销,这样用户领取优惠券低价购买后,还可以继续分销挣佣金。 等到 40 元限时优惠券到期后,接着再推出20元限时券,很多用户这个时候发现原来优惠券的确有时间限制,且金额在减少,所以就会赶快购买。 限时优惠券,是很多商家都会用的策略,特别是零售和电商行业,优惠只会让用户感受到低价,但是限时却可以触发用户的厌恶损失心理,从而加快完成付费环节。 活动复盘 活动策划前的第一步是要对目标进行拆解,所以我们在复盘活动时,第一步就需要先回顾目标拆解的每一个指标,然后看每一指标的实际结果,包括完成率、目标差异等。在这一步尽可能根据实际情况来统计。 当我们把最终的数据结果对应到目标拆解的每一环节后,我们就能知道哪一步没有完成目标,哪一步超额完成,以及各自的差异点,然后我们就要来分析这其中的原因。 这一步需要将前面分析的结论沉淀记录下来,提炼有规律的因素,后续做活动策划时做参考。将好的环节做成可执行的规则模版,不足的环节总结教训避免下次再出现。 小结 想要做好一次运营活动需要做到以下 3 个步骤: 活动策划:明确目的、确认方案、准备材料 活动实施:准备资源、观察过程、提高转化 活动复盘:回顾目标、数据分析、总结经验
2022-02-12 - 微信小程序如何实现页面传参?
前言 只要你的小程序超过一个页面那么可能会需要涉及到页面参数的传递,下面我总结了 4 种页面方法。 路径传递 通过在url后面拼接参数,参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔;如 ‘path?key=value&key2=value2’。 案例:A页面带参数跳转到B页面 A页面跳转代码 [代码]goB(){ wx.navigateTo({ url: '/pages/B/index?id=value', }) }, [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', options.id) } [代码] 上面的案例是字符串参数,但是很多情况下需要传递对象,如下方代码。 [代码]Page({ data: { userInfo:{ name:'cym', age:16 } }, goB(){ wx.navigateTo({ url: '/pages/B/index?id='+this.data.userInfo, }) }, }) [代码] 如果使用上面同样的方式结构,输出的结果是:[object Object] 这个时候需要先把对象通过JSON.stringify(obj)将 object 对象转换为 JSON 字符串进行参数传递,再到接收页面通过JSON.parse解析使用。 A页面跳转代码 [代码] goB(){ let userStr = JSON.stringify(this.data.userInfo) wx.navigateTo({ url: '/pages/B/index?id='+userStr, }) } [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', JSON.parse(options.id)) } [代码] 全局变量 通过App全局对象存放全局变量。 app.js代码 [代码]App({ // 存放对象的全局变量 globalData:{}, }) [代码] A页面跳转代码 [代码]// 获取App对象 const app = getApp() Page({ /** * 页面的初始数据 */ data: { userInfo: { name: 'cym', age: 16 } }, goB() { app.globalData.userInfo = this.data.userInfo wx.navigateTo({ url: '/pages/B/index', }) }, }) [代码] B页面接收代码 [代码]// 获取全局对象 const app = getApp() Page({ onLoad: function (options) { console.log(app.globalData.userInfo) } }) [代码] 存放在 App 全局变量里面,可以被多个页面使用,直接从 App 对象获取即可。这个数据是保持在内测中,每次小程序销毁就没有了。 数据缓存 通过存储到数据缓存中。 A页面跳转代码 [代码] goB() { wx.setStorageSync('userInfo', this.data.userInfo) wx.navigateTo({ url: '/pages/B/index', }) } [代码] B页面接收代码 [代码] onLoad: function (options) { let userInfo = wx.getStorageSync('userInfo', this.data.userInfo) console.log(userInfo) } [代码] 存放在数据缓存里面,可以被多个页面使用,直接用 getStorageSync 获取即可。这个数据是保持在数据缓存中,除非清楚数据缓存或者删除小程序否则一直存在。 事件通信 通过事件通信通道。 A页面跳转代码 [代码]goB() { wx.navigateTo({ url: '/pages/B/index', success:(res)=>{ // 发送一个事件 res.eventChannel.emit('toB',{ userInfo: this.data.userInfo }) } }) } [代码] B页面接收代码 [代码]onLoad: function (options) { // 获取所有打开的EventChannel事件 const eventChannel = this.getOpenerEventChannel(); // 监听 index页面定义的 toB 事件 eventChannel.on('toB', (res) => { console.log(res.userInfo) }) } [代码] 总结 大家可以针对具体业务场景来进行选择合适自己的传参方式。
2022-02-19 - 做微信小程序产品,需要注意这 3 点!
前言 微信小程序已经成为了越来越多企业和个人的选择,因为它可以为用户提供便捷、快速的服务和体验。然而,要想在微信小程序市场上脱颖而出,需要考虑以下 3 点。 1.明确目标受众 在开发微信小程序产品之前,需要确定你的目标受众是谁,并了解他们的需求和行为模式。这有助于在设计和开发产品时提供更好的用户体验和满足他们的需求。 在明确受众的同时需要还需要明确市场需求大小,可以通过【微信指数】小程序来查询需求量。 例:在微信指数小程序输入「微信小程序」查看指数趋势。 [图片] 2.界面与交互 微信小程序的界面设计非常重要,因为它是用户与你的产品进行交互的第一印象。一个好的界面设计应该清晰、简洁、易于导航,,同时提供有用的功能和信息。可以学习微信设计指南,以及参考官方小程序的界面设计,如:微信指数,微信支付,腾讯系列小程序。 微信小程序产品的用户体验至关重要。在开发产品时,要考虑到用户使用的便捷性以及整体交互,尽量减少用户在使用过程中遇到的障碍和问题。 3.最小可行性产品 做微信小程序我不推荐做的像App那样功能全面,一个小程序最好只解决一个问题。第一个版本不需要很完善,做个最小可行性产品(mvp)就可以上线快速验证。 [图片] 在此需要注意安全性和稳定性,微信小程序产品的安全性和稳定性是至关重要的。在开发产品时,要考虑到数据的安全性和系统的稳定性,并采取措施来确保用户的数据不会被恶意攻击或泄露,这个是最基本的保障。 通过上线后用户数据反馈产品需要不断的更新优化,持续提高其性能和用户体验。 推荐使用PDCA循环:Plan(计划)、Do(执行)、Check(检查)和Act(处理) P(Plan)计划,包括方针和目标的确定,以及活动规划的制定。 D(Do)执行,根据已知的信息,设计具体的方法、方案和计划布局;再根据设计和布局,进行具体运作,实现计划中的内容。 C(Check)检查,总结执行计划的结果,分清哪些对了,哪些错了,明确效果,找出问题。 A(Act)处理,对总结检查的结果进行处理,对成功的经验加以肯定,并予以标准化;对于失败的教训也要总结,引起重视。对于没有解决的问题,应提交给下一个PDCA循环中去解决。 以上四个过程不是运行一次就结束,而是周而复始的进行,一个循环完了,解决一些问题,未解决的问题进入下一个循环,这样阶梯式上升的。
2023-05-03 - Skyline|电商小程序 留住用户秘诀
你是否也收到这样的用户反馈? 商品列表滚动区域太小,很难找到想要的商品。头部的搜索广告占据了半个屏幕,挤占了实际空间。在我手机这样小的屏幕上,展示区域太小了,能否把它放大点?[图片] 在电商页面中,我们需要向用户展示众多的商品、广告等信息。 然而,如何在有限的屏幕空间中更好地展示它们,是一个需要我们深入思考的问题。 由于小程序 webview 渲染框架在技术存在一定的局限性,我们需要在不同的设计之间进行抉择。 当广告具有较高的优先级时,我们会考虑突出广告的展示,同时减小商品列表在界面中所占比例。当商品列表具有较高的优先级时,我们会考虑优先展示商品,而放弃广告的展示。[图片] 但是当广告和商品列表同样重要的情况下,要怎么办呢? 常见的一种设计方式是设计一个隐藏按钮,当用户不想看广告的时候把广告隐藏掉,隐藏之后商品列表就有更多展示空间 [图片] 但是这种情况也只是针对愿意手动点击隐藏按钮的情况下,还是有一定的局限性。 那么,有没有办法做到在无形中隐藏广告呢? [图片] 说到这里,当然是可以的啦✌️ 1、吸顶布局 + worklet 轻松实现 在常见的电商小程序首页,通常是顶部展示类目、接着展示商品详情,商品详情顶部也有热门等等的分类。 当页面滚动的时候,我们希望商品详情热门分类可以吸在顶部,便于切换。 [图片] 这里我们用到了 scroll-view 的 sticky-header、sticky-section 吸顶布局容器即可轻松实现。 ... ... ... 当 scroll-view 滚动的时候,根据滚动位置把搜索框放到标题的位置,可以再节省一点空间 attached() { // nav-bar 隐藏或展示 this.applyAnimatedStyle('.nav-bar', () => { 'worklet' return { opacity: this.navBarOpactiy.value } }) // 改变搜索框宽度 this.applyAnimatedStyle('.search', () => { 'worklet' return { width: `${this.searchBarWidth.value}%`, } }) }, // scroll-view 监听函数 handleScrollUpdate(evt) { 'worklet' const maxDistance = 60 const scrollTop = clamp(evt.detail.scrollTop, 0, maxDistance) const progress = scrollTop / maxDistance const EasingFn = Easing.cubicBezier(0.4, 0.0, 0.2, 1.0) this.searchBarWidth.value = lerp(100, 70, EasingFn(progress)) this.navBarOpactiy.value = lerp(1, 0, progress) }, 2、手势 + worklet 操作更灵活 小程序新渲染框架支持了手势系统,手势在这里可以发挥大作用~ 我们可以使用手势协商让小程序页面中的广告、商品等无缝切换和更好的展示 [图片] // .wxml ... // .js handlePan(e) { 'worklet' ... if (e.state === GestureState.ACTIVE) { if (this._interactionState.value === InteractionState.UNFOLD) { // 展开状态下,往上滑才折叠起来 if (e.absoluteY - this._startY.value < 0) { this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(0.0, { duration: 250 }, () => { 'worklet' this._interactionState.value = InteractionState.RESET }) } } else { // 其它情况,跟随手指滑动 this._translY.value = e.absoluteY - this._startY.value } } // 其他状态下的处理 ... }, 加入手势之后,除了可以隐藏广告,我们还可以将一些头部信息隐藏 在用户查看商品列表时,隐藏大部分无用信息,将商品列表展示区域最大化 // 最外层 .page 往上挪 this.applyAnimatedStyle('.page', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) return { transform: `translateY(${translY}px)` } }) // 改变 .navigation-bar 背景色 this.applyAnimatedStyle('.navigation-bar', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const opacity = translY / -this._tabsTop.value return { backgroundColor: `rgba(255, 255, 255, ${opacity})` } }) // 输入框:改变宽度并且展示 this.applyAnimatedStyle('.search-input', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const percentage = translY / -this._tabsTop.value return { width: `${percentage * 60 + 40}%`, opacity: percentage, } }) 除此之外,我们这里可以利用手势来展示商家的一些信息 当在商品列表往下拉到顶部时,触发整个列表下拉展示出商家信息 再往上滑动则商品列表重新展示~ [图片] // 商品详情往下拉 this.applyAnimatedStyle('.main', () => { 'worklet' const translY = clamp(this._translY.value, 0, Number.MAX_VALUE) console.log(222, translY) return { transform: `translateY(${translY}px)` } }) // 简单的 header 渐隐 // 商品详情展示时,仅显示简单的 header:学堂名称和几个标签 this.applyAnimatedStyle('.header-shop-info-simple', () => { 'worklet' const min = 50 const max = 100 const translY = clamp(this._translY.value, min, max) - min return { opacity: 1 - (translY / (max - min)) } }) // 复杂的 header 渐显 // 商品详情下拉,显示复杂 header:展示热门活动、公告等等信息 this.applyAnimatedStyle('.header-shop-info-detail', () => { 'worklet' const min = 100 const max = 150 const translY = clamp(this._translY.value, min, max) - min return { opacity: translY / (max - min) } }) 加入手势动画之后,我们的页面展示对比之前有了以下的优势: 更加自然:更符合用户操作习惯,用户自然滚动屏幕时不会感到突兀更节省空间:滚动隐藏更为灵活、省空间,使页面更清爽更高效的展示:可以将要展示的内容更好的展示,无需做取舍 借助手势动画,我们可以优化小程序界面展示、提升用户体验,从而获得更高的商业价值。 如果你也想更好的留住用户,mark 下这个 源码 [ 瀑布流页面 / 分类页面 ] 直接接到到你的小程序吧~
2023-08-03 - 小程序自定义tabbar以及隐藏tabbar
[图片] 分析: 选中时显示背景和文字,图标变换为选中状态的图标 默认状态下,进入小程序显示的是中间的 “地图” 选中 “预约”时,隐藏tabbar ,且导航栏显示 返回 按钮,点击返回 “地图” 整体效果 [图片][图片][图片] 整体目录 [图片] 设置 app.json 在 [代码]app.json[代码]中 配置tabbar 中的 “custom”: true [代码]{ "pages": [ "custom-pages/custom-active/index", "custom-pages/custom-order/index", "custom-pages/custom-my/index" ], "entryPagePath": "custom-pages/custom-active/index", "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "自定义tabbar", "navigationBarTextStyle": "black" }, "tabBar": { "custom": true, "list": [ { "pagePath": "custom-pages/custom-my/index", "text": "我的", "iconPath": "/images/tabbar/my.png" }, { "pagePath": "custom-pages/custom-active/index", "text": "活动" }, { "pagePath": "custom-pages/custom-order/index", "text": "预约" } ] }, "usingComponents": {}, } [代码] 在项目根目录下,添加 custom-tab-bar custom-tab-bar为component 编写 tabBar 代码 用自定义组件的方式编写即可,该自定义组件完全接管 tabBar 的渲染。另外,自定义组件新增 getTabBar 接口,可获取当前页面下的自定义 tabBar 组件实例 在根目录下新建文件夹名为 custom-pages ,存放自定义tabbar的页面 custom-pages -> custom-active -> custom-my -> custom-order [图片] custom-tab-bar代码 [代码]Component({ data: { // tabbar 的列表 tabbarLists:[ { pagePath: "/custom-pages/custom-my/index", text: "我的", iconPath: "/images/tabbar/my.png", selectedIconPath: "/images/tabbar/my_active.png" }, { pagePath: "/custom-pages/custom-active/index", text: "地图", iconPath: "/images/tabbar/map.png", selectedIconPath: "/images/tabbar/map_active.png" }, { pagePath: "/custom-pages/custom-order/index", text: "预约", iconPath: "/images/tabbar/order.png", selectedIconPath: "/images/tabbar/order_active.png" } ], active:null, //设为数字,会产生tabbar闪烁 isShow:true //控制显示隐藏tabbar }, methods: { switchTab(e){ const { index,url } = e.currentTarget.dataset; wx.switchTab({url}) } } }) [代码] [代码]<view class="tab-bar" wx:if="{{isShow}}"> <block wx:for="{{tabbarLists}}" wx:key="item"> <view class="tab-bar-item" data-index="{{index}}" data-url="{{item.pagePath}}" bindtap="switchTab" > <image class="icon" src="{{active == index?item.selectedIconPath:item.iconPath}}"></image> <view class="text {{active == index?'active':''}}">{{item.text}}</view> <!-- 设置选中状态下的背景 --> <view wx:if="{{active == index}}" class="bg-item"></view> </view> </block> </view> [代码] [代码].tab-bar{ height: 48px; background-color: #ffffff; display: flex; border-top: 1rpx solid #f9f9f9; box-shadow: 0 0 5rpx #f8f8f8; } .tab-bar-item{ display: flex; flex: 1; justify-content: center; align-items: center; position: relative; } .icon{ width: 27px; height: 27px; } .text{ font-size: 26rpx; padding: 0 5rpx; letter-spacing: 0.1rem; display: none; } .active{ color: #10B981; display: block; font-weight: 800; } .bg-item{ position: absolute; width: 80%; top: 15rpx; bottom: 15rpx; border-radius: 40rpx; background-color:rgba(52, 211, 153,0.15); } [代码] tabbar的page页 一共3个tabbar的page页:custom-active、custom-my、custom-order custom-active 页 核心代码 [代码]Page({ onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ active: 1 }) } } }) [代码] custom-my 核心代码 [代码]Page({ onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ active: 0 }) } } }) [代码] custom-order 该page页要隐藏 tabbar ,所以要将 isShow设置为false; 使用 custom-navigator 自定义组件 显示 头部导航 [代码]<!-- 使用自定义组件导航头 --> <custom-navigator title="{{title}}"> </custom-navigator> [代码] [代码]Page({ data: { title:"预约" }, onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ active: 2, isShow:false }) } } }) [代码] [代码]{ "usingComponents": { "custom-navigator":"/components/custom-navigator/custom-navigator" }, "navigationStyle": "custom" } [代码] custom-navigator 自定义组件 [代码]<view class="status"></view> <view class="navigator"> <view class="icon"> <image bindtap="gobackTap class="icon-image" src="/images/back.png"></image> </view> <view class="text">{{title}}</view> <view class="right"></view> </view> [代码] [代码].status{ height: 20px; } .navigator{ height: 44px; background-color: #ffffff; display: flex; flex-direction: row; align-items: center; justify-content: center; } .icon{ width: 40px; height: inherit; display: flex; align-items: center; justify-content: center; } .icon-image{ width: 27rpx; height: 27rpx; border: 1rpx solid #cccccc; padding: 8rpx; border-radius: 20rpx; } .text{ color: #333333; flex: 1; text-align: center; font-size: 28rpx; } .right{ color: #333333; width: 40px; } [代码] [代码]Component({ properties: { title:{ type:String, value:"标题" } }, methods: { gobackTap(e){ wx.switchTab({ url: '/custom-pages/custom-active/index', }) } } }) [代码]
2023-05-12 - 用做App的思路去做微信小程序可行吗?
前言 最近在和一位创业的朋友沟通做一款产品,这位朋友一直用app的思路想做个微信小程序。我的建议是用做App的思路去做微信小程序是不可行的,我们要考虑两边的区别,利用不同生态的优势才行。 微信小程序和App的区别 入口不同:微信小程序是通过二维码、搜索、公众号等方式触达用户,而App则是通过应用商店等方式独立下载和安装。 开发语言不同:App通常使用Object-C、Java、Swift等专门的编程语言,而微信小程序则是基于JavaScript开发的,所以开发门槛相对较低。App通常是由公司内部的团队或者外包给第三方开发公司进行开发,而微信小程序是由微信官方提供的开发工具和开发文档,个人或者小团队也可以进行开发。 用户群体不同:App面向的用户群体更广泛,而微信小程序主要面向微信用户,同时也可以吸引一些不使用微信的用户。 功能和体验不同:App通常具有更丰富的功能和更好的用户体验,而微信小程序则更注重轻量化和快速触达用户。 推广方式:微信小程序可以通过微信公众号、小程序卡片、朋友圈等多种方式进行推广,而App则需要通过应用商店等渠道进行推广。app的营销难度大,用户很少会主动去分享,但是小程序可以通过分享直接转发,并且小程序可以通过一些营销的方式来进行裂变,极大的提高了营销的效率。 生态环境:微信小程序在微信生态可以围绕公众号、视频号、企业微信、微信群去做,而App生态更开放。 功能限制:微信小程序的功能相对来说比较有限,不能与手机系统进行深度交互。而APP则可以实现更多的功能,与手机系统进行深度交互。 总结 微信小程序:开发成本更低,获客成本更低,使用成本更低 App:留存更高,生态更开放,体验可以做到更好,限制更少 做微信小程序就要充分利用微信生态的优势,围绕公众号、视频号、企业微信、微信群来做用户留存裂变。 总的来说,微信小程序和App都有自己的优势和特点,创业者可以根据自己资源/产品属性/用户人群等多方面的情况来选择适合自己的平台。
2023-05-05 - 答题积分小程序云开发实战-界面交互篇:积分排名页布局样式与逻辑交互开发
微信小程序云开发实战-答题积分赛小程序 界面交互篇:积分排名页布局样式与逻辑交互开发积分排名页效果图[图片] 积分排名布局与样式实现这个积分排名页的页面布局,设计上是比较美观的,主要展示排名、用户头像昵称、积分信息。我曾搭建的消防安全知识答题、网络安全知识答题、安全生产知识答题等,都是使用这种方式实现的。 页面布局在rank.wxml中,编写布局代码: <view class="mw-page"> <view class="menu menu-avatar cu-list"> <view class="cu-item"> <view class="cu-avatar lg round"> <image class="avatar" src="/images/0.png" mode="widthFix"></image> </view> <view class='content'> <view class='text-gray'> <text class="icon-upstagefill text-yellow"></text> 第<text class="text-yellow text-xl">1</text>名 </view> <view class='text-sm text-grey'>姑苏洛言</view> </view> <view class='action'> <view class='text-xl text-yellow'>20积分</view> </view> </view> </view> </view> 页面样式在rank.wxss中,编写样式代码: page{ background-color: #fff; } .mw-page .menu.cu-list { padding: 20rpx; } .mw-page .menu.cu-list .cu-item { border: 2rpx solid #ddd; border-radius: 10rpx; margin-bottom: 20rpx; } 页面预览保存后,可以在模拟器预览效果或者手机微信扫码后预览。 [图片] 列表渲染觉得太少了,看不出效果? 在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。 [图片] <view class="mw-page"> <view class="menu menu-avatar cu-list"> <view class="cu-item" wx:for="{{6}}" wx:key="index"> <view class="cu-avatar lg round"> <image class="avatar" src="/images/0.png" mode="widthFix"></image> </view> <view class='content'> <view class='text-gray'> <text class="{{index+1 <= 3?'icon-upstagefill text-yellow':'icon-medalfill text-gray'}}"></text> 第<text class="{{index+1 <= 3?'text-yellow':'text-gray'}} text-xl">{{index+1}}</text>名 </view> <view class='text-sm text-grey'>姑苏洛言</view> </view> <view class='action'> <view class='text-xl text-yellow'>20积分</view> </view> </view> </view> </view> 注意:默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item。 列表效果预览保存后,可以在模拟器预览效果或者手机微信扫码后预览。 [图片]
2023-05-06 - 火遍全网3个月,淄博如何打好“从流量到留量”的关键一战?
[图片] 淄博,从3月火到了5月,在“五一”前热度达到顶峰,且还在持续爆火中。 在抖音上,#淄博烧烤 的话题播放量高达200多亿,单日最高播放量近18亿,也有不少3、4月份爆火的话题,播放量超10亿。 [图片] ▲ 图片来源:果集 · 飞瓜数据 小红书近30天与“淄博”相关的笔记也在增长,#淄博烧烤、#淄博话题浏览量均在4亿左右。 [图片] [图片] ▲ 图片来源:果集 · 千瓜数据 公众号上与“淄博烧烤”相关的文章,据不完全统计,阅读10W+至少有250多篇。 名副其实火遍全网。 而在线下,“进淄赶烤”的人蜂拥而至、络绎不绝。 相关数据显示,3月淄博共计接待游客480万人次,而淄博人口才470万。 刚刚过去的“五一”假期,淄博站客运累计发送旅客超24万人次,较2019年同期增长8.5万人次,增幅55%。旅客到达量约24万人次。 “木鸟”民宿发布的《2023五一假期民宿消费报告》显示,在民宿订单增幅Top10城市榜单中,淄博、威海、福州分列前三位,“顶流”淄博民宿同比2019年订单增幅超78倍。 据微信5月4日发布的《2023“五一”游玩井喷数据报告》,“五一”期间,淄博旅游业消费额环比4月增长73%,而游客在淄博本地中小商户日均消费金额环比4月增长也达到了近40%。 [图片] ▲ 图片来源:「微信派」公众号 说淄博顶流,大概也不为过。即便你的身体没有在“进淄赶烤”的路上,心灵也被网上“小饼烤炉加蘸料,灵魂烧烤三件套”刷屏。 一个烧烤带火了一个工业城市,淄博如何出圈?为什么能成为“顶流”?它的爆火带给互联网人、营销人哪些启发? 1、短视频造神 社交平台种草打卡 淄博烧烤的爆火,要从一个温暖的故事讲起。 去年疫情期间,山东大学1万多名大学生被安排在淄博隔离,淄博政府好吃好喝招待他们。临走前,还请所有学生吃了一顿烧烤,并且邀请他们来年再来。今年春天,这些学生回来了。 更重要的是,这些大学生在各种社交平台上分享、打卡淄博烧烤。#大学生组团到淄博吃烧烤 登上抖音同城热搜,目前话题播放量高达3.4亿,还有各种与之相关的话题。 [图片] ▲ 图片来源:抖音截图 随后央视新闻等各大媒体纷纷报道,淄博烧烤“灵魂三件套”开始走红。 将淄博烧烤推上更高热度的,则是千万粉美食博主“B太”的打卡视频。他专注美食餐饮行业的打假。但4月8日发布的视频中,“B太”去了10家美食店,没有一家缺斤少两,并且视频中还展现了好客山东的热情。目前该视频的点赞有368万。 [图片] ▲ 图片来源:B太抖音截图 从百度搜索指数和微信指数趋势也可以明显看到,自4月8日开始,淄博的热度一路狂飙。随着淄博各种宠粉政策的推出以及热情真诚的服务,淄博被“烤”红了。 [图片] ▲ 图片来源:微信指数 央视新闻记者到淄博采访的时候提到:印象最深的不是烧烤,而是一种不同以往的采访体验。当我们在记录别人的同时,也在被别人记录着。 正如卢克文所说:淄博烧烤的红火,是网络时代独有的,小城经济发掘爆炸式现象。 最新数据显示,截至2022年12月,我国短视频用户规模达10.12亿,网络直播用户规模达7.51亿。 短视频直播时代,无论是普普通通的网民,还是拥有一定话语权、粉丝量的网红博主;无论是蹭流量,还是随手记录,这些KOL、KOC的主动传播,有力推动淄博的走红。 2、全盘布局 精准营销 淄博政府的流量承接可谓“快、准、狠”。 首先是政府部门的联动。不仅仅是文旅局,商务局、公安局、市场监管局等各政府部门“总动员”,并且“五一”期间,党政机关全员不休假,充当服务志愿者,做好一条龙服务。 淄博烧烤高铁专列、21条定制公交、成立淄博市烧烤协会、发布淄博烧烤地图、举办淄博烧烤节、72小时昼夜施工改造道路……各种高效“宠粉”举措接连推出。 此次爆火因大学生而起,而淄博政府也面向这批受众,推出了专门的举措,精准营销。 全市38处青年驿站为符合条件的来淄求职、就业的青年学生提供每年3次、每次2晚的免费入住,来淄实习、游玩、访友的青年学生可享受每年4次、每次5天的半价入住。 大学生作为年轻群体代表,也是在网上最为活跃的群体,且近来大学生拉练式的特种兵旅游走红,为淄博的流量传播创造了条件。 [图片] ▲ 图片来源:抖音截图 淄博政府很好地抓住了关键目标用户,不仅吸引他们来旅游,更是吸引外来人才,将流量充分沉淀转化。 当然,有的网友提出要185的小哥接待,淄博政府真的满足了用户的个性化需求。 普通民众也在努力。本地人不吃烧烤让位、出门不打车还为游客免费接送、商铺为游客提供免费手机充电,还有0.85米志愿者喝着奶帮看行李,甚至有夫妻连架都不敢吵了,生怕影响淄博形象。 [图片]▲ 图片来源:网络 有网友调侃:470万人的淄博,有400万人都是客服。全城都在“为淄博的荣誉而战”。 从上至下,由点到面,淄博不仅承接住了流量,还很好地维护和提升了品牌形象,将“淄博烧烤”城市名片和淄博城市形象的良好一面,展现地淋漓尽致。 有的营销活动做不好,可能活动方案是一方面,各部门执行到位、默契配合也很重要,一个环节出错,就有可能满盘皆输。 清晰了解用户画像,针对目标受众战略布局,实施精准营销,并且“用户至上”,想用户所想,把服务做到极致,做好口碑营销。 3、从流量到留量 从爆红到长红 当下全国不少景区都出现宰客现象,今年“五一”前夕,也爆出了不少“酒店刺客”的新闻,而淄博不宰客、不缺斤少两、酒店不涨价反而降价退钱给用户。 淄博所做的不过是回归服务的初心与本质,只有真诚不辜负真诚,而真诚是永远的“必杀技”。 [图片] ▲ 图片来源:网络 是淄博烧烤特别好吃吗?当然也并不全是。全国各地的人民千里迢迢赶去淄博,为的可能不是烧烤,而是感受这座真诚、热情好客、充满烟火气的城市带给人们不一样体验。 正如吴晓波所提到的,淄博烧烤正在兑现人们对自由市场的平民式想象:物美价平的商品、畅快淋漓的消费体验、童叟无欺的市场环境、谦卑和气的“小政府”。 烧烤带火了淄博,成为了这座城市的“引流”利器。但淄博不止于烧烤。 淄博在做的,是穿透烧烤,激活整座城市的活力,努力将“流量”变为“留量”,将“爆红”转化为“长红”。 无论是淄博的烧烤、旅游,还是我们所处的行业,所做的从来都不是一次性买卖,不能为了一时的蝇头小利,而失去初心与本质,做出有损品牌形象、用户满意度大打折扣的行为。 一夜爆红是偶然,但是昙花一现还是勇立潮头,要靠正向内容的持续输出、产品与服务的不断打磨、品牌形象的持续打造、用户口碑的不断积累,只有这样才能带来充分的长尾效应,稳定可持续发展。
2023-05-06 - 小程序静默登录设计
简介 本文主要分享个人平时开发微信小程序时登录设计的思路,目前微信小程序的登录机制与以前的小程序登录机制或网页的登录机制不一样 一. 静默登录 小程序目前的登录方式采用的是静默登录的形式,即用户不需要进行任何相关授权或其他操作就可以很流畅的体验整个应用,不像以前的小程序应用很多功能都需要手动去判断用户是否已经登录过再去开放某些入口或功能(当然目前也有不少小程序改成绑定手机号的形式去搞这种需求) 静默登录目前有以下几点好处: 不需要特意去增加一个登录界面或者登录弹窗去实现登录功能 减少前端对一些特定按钮进行登录状态判断的工作量 用户不需要进行额外登录操作才能完美体验整个小程序 更好的保障用户隐私安全,当然这是对 TX 有好处(毕竟很多小程序喜欢采用强制登录来获取用户信息) 二. 静默登录流程 前端调用 [代码]wx.login[代码] 这个 [代码]api[代码] 获取临时登录凭证([代码]code[代码]) 前端调用服务端登录接口传递 [代码]code[代码] 给服务端 服务端通过小程序 [代码]appId[代码] 和小程序秘钥向微信服务器获取 [代码]openId[代码] 和 [代码]session_key[代码] 服务端将 [代码]openId[代码] 和 [代码]session_key[代码] 进行关联同时产生一个带有默认名称和默认头像的新用户 服务端将自定义登录的信息返回给前端,下次请求服务端接口时把登录信息带上 三. 静默登录设计思路 前端调用 [代码]wx.login[代码] 是不需要进行任何相关操作的,所以触发登录完全是由前端去决定的,而进行登录不能以指定页面作为参考点,需要考虑到用户分享从其他页面进入的情况 现在需要一种,目前个人思考出有两种方案: 每个页面都套用一个 [代码]layout[代码] 自定义组件,在该组件的生命周期进行登录触发,然后触发完毕后在组件抛出一个登录后的回调函数,接着页面接收这个回调函数然后在这个函数调用业务 [代码]api[代码] 在每次请求服务端 [代码]api[代码] 之前,先进行登录,等待登录处理完毕之后再进行请求服务端 两者比较之后目前是采用第二种方式去进行登录流程设计,因为静默登录完全跟用户没有任何关系,只是单纯与服务端进行交流,所以不应该与页面有关系,应该是跟请求服务端 [代码]api[代码] 有关系 四. 静默登录设计实现 由于采用 [代码]api[代码] 拦截方式实现登录设计,登录的代码都放在 [代码]api[代码] 请求模块去实现,以下是实现思路: 在页面中调用服务端 [代码]api[代码],首先判断是否已经登录过,如果没有登录则先触发登录再请求服务端 [代码]api[代码],如果登录则直接请求 考虑到一个页面可能会同时调用多个服务端 [代码]api[代码],需要避免同时触发多次登录 [代码]// 登录后的信息 const token = { // ... } // 请求封装 const _request = <T = unknown>(apiLink: string, params?: Record<string, any>): Promise<T | null> => { return new Promise((resolve, reject) => { wx.request({ url: apiLink, data: params, success: (res) => { resolve(res.data) }, fail: (err) => { reject(null) } }) }) } // 记录登录状态 let _loginRecord: any = null // 请求拦截 const apiRequest = async <T = unknown>(apiLink: string, params?: Record<string, any>): Promise<T | null> => { // 如果一个页面同时触发多个 api 请求,那么只公用一个登录的 Promise 状态,这样就不会多次触发登录 if (!_loginRecord) { _loginRecord = _request('xxxxx/login', { code: 'xxxx' }) } const loginResult = await _loginRecord /** * 这里根据业务需求去定制登录报错时的情况,我目前是 * 先弹窗提示,然后用户确认后再重新请求 api * if (!loginResult) { _loginRecord = null return null } return await _request(apiLink, params) } export default apiRequest [代码] 五. 最后 以上是我个人常用的微信小程序静默登录设计思路,如果有小伙伴有更好的设计思路,希望能够分享出来学习一下
2023-05-09 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - 日历组件WeCalendar;支持折叠周模式;滑动;自定义标记
miniprogram-wecalendar 一个支持滑动、自定义折叠、标记日期、轻快的小程序日期组件 使用ESbuild 构建,现在的快速响应的 [图片] [图片] [图片] [图片] [图片] 英文 README 展示 [图片] 安装 [代码]npm i miniprogram-wecalendar [代码] or [代码]yarn add miniprogram-wecalendar [代码] 使用 在使用地方 [代码]page.json[代码] 或者 [代码]app.json[代码] 中使用 👇🏻 [代码]{ "usingComponents": { "WeCalendar": "miniprogram-wecalendar" } } [代码] 在 wxml 加入使用 👇🏻 [代码]<WeCalendar markCalendarList="{{markCalendarList}}" isToday="{{true}}" bind:onRangeDate="onRangeDate" bind:onSelect="onSelect" /> [代码] WeCalendar的参数 Property Type Default required Description isToday Boolean False 0 是否展示今天俺妞icon markCalendarList [代码]Array[{ date: YYYY-MM-DD pointColor: #ccc }][代码] [] 0 标记日历的数组,支持自定义颜色 defaultDate String: YYYY-MM-DD Null 0 默认时间 showFolding Boolean True 0 日历折叠功能 weeekLayer Number 1 0 日历折叠的等级 | 行数 WeCalendar 的方法 Property Type Description onSelect Function Callback 日历每天的点击事件 onRangeDate Function Callback 日历渲染的日期范围 举个例子 🌰 onSelect [代码]onSelect: (e) => { const {day} = e.detail // ... } [代码] onRangeDate [代码]onRangeDate: (e) => { const {beginTime, endTime} = e.detail // ... } [代码] 开发启动 [代码]npm run dev [代码] 用微信小程序开发工具打开[代码]demo[代码]文件夹即可,更改[代码]src[代码]下面的文件会自动构建
2023-04-19 - setData动态key的使用以及使用建议
相信每个小程序开发者用的最多的函数非setData莫属,setData是小程序开发中使用最频繁、也是最容易引发性能问题的接口。 本篇内容主要讲小程序setData的用法、注意事项、使用建议以及动态key的写法。 setData介绍 Page.prototype.setData(Object data, Function callback) [代码]setData 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)。 [代码] 字段 类型 必填 描述 最低版本 data Object 是 这次要改变的数据 callback Function 否 setData引起的界面更新渲染完毕后的回调函数 1.5.0 Object 以 key: value 的形式表示,将 this.data 中的 key 对应的值改变成 value。 注意事项 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。 仅支持设置可 JSON 化的数据。 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。 请不要把 data 中任何一项的 value 设为 undefined ,否则这一项将不被设置并可能遗留一些潜在问题。 示例代码: 在开发者工具中预览效果 使用建议 1. data 应只包括渲染相关的数据 setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。 ✅ 页面或组件的 d- ata 字段,应用来存放和页面或组件渲染相关的数据(即直接在 wxml 中出现的字段); ✅ 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用 setData 设置并使用 observers 监听变化; ✅ 页面或组件渲染无关的数据,应挂在非 data 的字段下,如 this.userData = {userId: ‘xxx’}; ❌ 避免在 data 中包含渲染无关的业务数据; ❌ 避免使用 data 在页面或组件方法间进行数据共享; ❌ 避免滥用 纯数据字段 来保存可以使用非 data 字段保存的数据。 2. 控制 setData 的频率 每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用 setData,会导致以下后果: 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换; 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟; 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。 因此,开发者在调用 setData 时要注意: ✅ 仅在需要进行页面内容更新时调用 setData; ✅ 对连续的 setData 调用尽可能的进行合并; ❌ 避免不必要的 setData; ❌ 避免以过高的频率持续调用 setData,例如毫秒级的倒计时; ❌ 避免在 onPageScroll 回调中每次都调用 setData。 3.setData 应只传发生变化的数据 setData 的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。 ✅ setData 应只传入发生变化的字段; ✅ 建议以数据路径形式改变数组中的某一项或对象的某个属性,如 This.Setdata({‘Array[2].Message’: ‘Newval’, ‘A.B.C.D’: ‘Newval’}),而不是每次都更新整个对象或数组; ❌ 不要在 setData 中偷懒一次性传所有data:this.setData(this.data)。 修改数组动态key的写法以及修改对象的某个属性 在开发者工具中预览效果 动态修改数组的某个元素 用一个中括号修饰表达式即可 [代码]clickItem(e){ let item = e.currentTarget.dataset.item let index = e.currentTarget.dataset.index // 第一种 可以只改某个数组的元素 // item.selected =!this.data.list[index].selected // this.setData({ // ['list['+ index + ']']: item // }) //第二种 也可以直接修改某个数组的某个属性 this.setData({ ['list['+ index + '].selected']: !this.data.list[index].selected }) }, [代码] 以数据路径形式改变数组中的某一项或对象的某个属性 [代码]clickObject(e){ this.setData({ 'user.clickNum': this.data.user.clickNum + 1 }) }, [代码] 更多内容可以仔细查看官方文档: setData 函数详解(在文档的底部) 合理使用 setData
2023-04-24 - 运用小程序Skyline技术构建无缝用户体验 —— 同程旅行酒店最佳实践分享
[图片] 动效衔接设计与小程序渲染框架 1、什么是动效衔接设计? 随着互联网技术和设计理念的不断发展,动效设计成为现代 UI 设计中不可或缺的一部分。其中,动效衔接是非常重要的一环。动效衔接设计是指通过巧妙的动效设计,将不同的 UI 元素在动画过程中自然、流畅地衔接起来,从而增强用户的交互体验和视觉感受。在实际应用中,动效衔接设计主要应用于界面转场、信息提示、状态变化等方面,通过顺畅的衔接,降低用户因白屏等待而产生的焦虑。 2、动效衔接设计的意义 (1)极大提高用户体验,让用户感受到界面的流畅和自然,从而增加用户对产品的好感度。 (2)降低用户的操作认知成本,帮助用户更好地理解执行操作后所带来的结果,从而减少用户对产品的困惑。 (3)强化视觉层,让用户更好地区分不同的信息和元素,从而增强视觉层次感。 (4)增加界面的美感度,让界面更加生动有趣,从而提升整体的美感和设计价值。 (5)提升品牌的认知度,让产品更加具有特色和独特性,从而提高品牌的认知度和市场竞争力。 3、什么是小程序渲染框架Skyline? 为了进一步优化小程序性能,小程序在原 webview 渲染引擎之外最新推出 小程序渲染框架Skyline,其使用更精简高效的渲染管线,并拥有诸多增强特性,让它拥有更接近原生渲染的性能体验。新的增强特性有 worklet 动画系统、手势系统、自定义路由、共享元素动画,而且许多常用的组件如 scroll-view、swiper 都有了更高性能的实现。 [图片] [图片] 实践理念和场景拆解 1、动效衔接设计的核心原则 简单而清晰的动效设计,需要遵守以下几个原则: (1)一致性:动效衔接应该与整体设计风格保持一致。包括颜色、字体、动画速度等方面。 (2)可预测性:用户能够感知动画元素的变化关联性,从而增加用户对产品的掌控感和对界面的理解。 (3)反馈性:动效衔接与用户操作相响应,从而帮助用户理解他们的操作所带来的结果。 (4)视觉层次:动效衔接遵循视觉层次原则,让用户区分页面中的上下关系以及三维物理世界的关系层次,给用户清晰的层级区分感知,提高用户体验。 (5)自然性:动效衔接符合物理规律,例如重力、加速度等,从而增强动画的真实感和用户体验。 2、理念孵化与使用场景拆解 以提炼的动态感受为出发点,理性的层面给予了我们大致的产品体验感知,为我们动效理念的建成提供了框架。对此我们将继续从感性层面出发,找寻可传递真实感受的运动现象并加以组合提炼。 本次 同程旅行小程序 以酒店预订链路中核心的相册页面进行应用场景,在用户操作图片的过程中运用小程序渲染框架承接。 (1)将整个过程进行了拆解,首先为了退出时行动的路径更加清晰,做了一个响应设计,当界面向右滑动退出的过程中,相册图片进行缩小,在缩放过程中,会根据位移距离控制缩放的比例,同时蒙层的透明度以及毛玻璃效果也跟随手指移动变化,和相册列表页在视觉上呈现 XY 轴以及上下层级的空间关系,在缩小到一定的比例时,触发震动效果,松手退出到相册列表页。 (2)在交互结束时,图片退回相册列表页原始位置,在返回路径的过程中,根据交互结束时的定位点,来判断运动的方向和距离,计算运动加速度,以及模拟运动加速度带来的惯性回弹的方向和角度变化,加强与模拟真实物理世界的运动定律,和视觉上的动态感知。 结合自然世界的运动规律来看,把页面进入的元素比作是行驶的汽车,用户当作是正在斑马线上行驶的人,将马路作为页面空间。若汽车采用的是缓入运动(加速)的话,马路上的行人则看到的是一辆不断加速向他行驶过来的车辆。因为担心车辆高速的逼近导致刹车不及时的情况,行人便会本能的作出躲闪的反应。其实页面也是一个道理,进入的元素使用加速运动出现过冲的运动感知会让用户体验时产生不适。 [图片] 小程序渲染框架技术开发实践过程剖析 1、开发自定义路由实现此交互,需要 自定义路由动画,因为小程序渲染框架的页面支持自定义跳转动画。当使用自定义路由后,页面跳转时指定路由类型,就会触发自定义路由动画,而不再是默认的从右往左的动画,此处的实现可以使得页面跳转时,没有默认的路由动画,页面将直接以透明的方式渲染在屏幕上,由开发者自己控制页面内元素的动画展示方式,具体实现如下: (1)在图片查看页面配置文件 index.json 中声明 { "backgroundColor": "#00000000", "backgroundColorContent": "#00000000", // 设置客户端页面背景为透明 "navigationStyle": "custom", "renderer": "skyline", // skyline渲染引擎 "disableScroll": true, "usingComponents": { } } (2)在 wxss 中,设置图片查看页面的 page 节点为透明背景 page { background: transparent; } (3)在 js 中,使用 wx.router.addRouteBuilder(routeType, fn) 来声明自定义路由动画 wx.router.addRouteBuilder('myCustomRoute', function (params) { const handlePrimaryAnimation = () => { 'worklet'; return { // 可在此处,根据 params.primaryAnimation.value 的值,来设置页面的动画效果 backgroundColor: `rgba(0,0,0,${ params.primaryAnimation.value })` }; }; return { opaque: false, handlePrimaryAnimation, barrierColor: '', barrierDismissible: false, transitionDuration: 320, reverseTransitionDuration: 250, canTransitionTo: true, canTransitionFrom: false }; }) (4)在图片列表页面中,使用 x.navigateTo 来跳转页面,并且设置 routeType 为 myCustomRoute wx.navigateTo({ url: '/pages/skyline-image-viewer/index?index=0', routeType: 'myCustomRoute' }) [图片] 需配置页面的渲染引擎为 Skyline,并且在跳转时使用 routeType 就可以实现让页面在跳转时没有默认的路由动画。 2、共享元素穿越在连续的页面跳转时,页面间 key 相同的 share-element 节点将产生飞跃特效,还可自定义插值方式和动画曲线,通常作用于图片。为保证动画效果,前后页面的 share-element 子节点结构应该尽量保持一致 <share-element key="share-key"> <view> you code here </view> <!-- 需要注意,share-element 内要求只有一个根节点 --> </share-element> [图片] 这时,界面的表现像上面视频一样,是一个连续的动画状态,这完全是由 share-element 来控制的,share-element 的动画原理如下图所示: [图片] 3、接入手势组件,实现图片放大、缩小、平移在图片查看页面有如下结构: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <share-element key="{{ shareKey }}" class="current-item"> <image src="{{ src }}"/> </share-element> </scale-gesture-handle> 这里,我们使用小程序渲染框架提供的 手势组件 <scale-gesture-handle>,来实现图片的放大、缩小、平移等手势交互。 注意,所有声明为 worklet 指令的方法它们运行在UI线程,不要在方法中修改普通的变量,因为跨线程的关系,只能修改使用 wx.worklet.shared 声明的变量。 const GestureState = { POSSIBLE: 0, // 此时手势未识别 BEGIN: 1, // 手势已识别 ACTIVE: 2, // 连续手势活跃状态 END: 3, // 手势终止 CANCELLED: 4 // 手势取消 }; Component({ attached() { this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.sharScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.sharScale.value})` } }); // 页面所需的数据,需要在 attached 事件里初始化完毕,使其可以参与首帧渲染 this.setData({ src: '...', shareKey: '...' }); }, methods: { // 当手势组件识别到手势时,触发此回调 onScaleGestureHandle(e) { 'worklet'; const { state } = e; // 在worklet函数里,不要使用 const {} = this 对this解构 const shareX = this.shareX; const shareY = this.shareY; const sharScale = this.sharScale; if (state === GestureState.BEGIN) { // 手势已经识别,此时,可以获取到手势的初始值 } else if (state === GestureState.ACTIVE) { // 手势活跃状态,此时,可以获取到手势的变化值,如平移的距离、缩放的比例等 // 将当前变化的值,设置到 `shared` 变量,就可以改变元素的样式,类似于vue3的数据驱动 shareX.value += e.focalDeltaX; shareY.value += e.focalDeltaY; sharScale.value = e.scale; } else if (state === GestureState.END || state === GestureState.CANCELLED) { // 手势终止或取消,此时,可以获取到手势的最终值 } } } }) [图片] 4、手势协商(解决手势冲突) 上面的 demo 简单演示如何使用手势组件来做图片交互,但是在图片查看页面中,我们还有其他的手势交互,如图片的左右滑动切换等,一般我们会使用 <swiper> 组件来实现,但是 <swiper>组件的内部实现和 <scale-gesture-handle> 组件,都会监听手势事件,手势组件的事件不支持冒泡的,就会导致下面结构横时: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </scale-gesture-handle> 使用手势横向滑动时,会优先触发 swiper 的横向切换事件,而无法触发 <scale-gesture-handle> 的手势事件了,这在图片放大时的图片横向移动产生了冲突。此时就需要使用手势协商来解决手势冲突。 什么是手势协商? 手势协商指的是:当页面同时有多个手势交互时,需通过一定的约定来决定哪些手势事件应该被执行,哪些需要被忽略。 小程序渲染框架解决手势冲突的方式,主要是通过手势组件的 tag、simultaneous-handlers、native-view 和 should-response-on-move 来实现 tag:手势组件的标识,用于区分不同的手势组件simultaneous-handlers:手势组件的协商者,表示需要同时触发事件的手势组件的标识should-response-on-move:参与手势时间的派发过程,返回 false时,表示该手势时间不会继续派发native-view:用当前手势组件来代理原生组件内部的手势事件,如<swiper>组件内部的手势事件<swiper> 的内部也是使用了 <horizontal-drag-gesture-handler>手势组件,但是我们不能直接在<swiper>上设置tag来使其参与手势协商,需要用相同的手势组件通过native-view=swiper将其内部的事件代理出来,使其可以参与协商<!-- <scale-gesture-handle> 缩放手势 --> <!-- <horizontal-drag-gesture-handler> 横向拖动手势 --> <!-- 通过 simultaneous-handlers=tag 来声明多个手势应该同时触发 --> <scale-gesture-handle tag="scale" simultaneous-handlers="{{['swiper']}}" worklet:ongesture="onScaleGestureHandle"> <!-- 此处使用 native-view=swiper 代理内部的手势组件 --> <!-- 通过 should-response-on-move=fn 来参与`事件派发`过程,决定手势的事件是否应该派发 --> <horizontal-drag-gesture-handler tag="swiper" native-view="swiper" simultaneous-handlers="{{['scale']}}" worklet:should-response-on-move="shouldResponseOnMove"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </horizontal-drag-gesture-handler> </scale-gesture-handle> const GuestureMode = { INIT: 0, SCALE: 1, SWIPE: 2, MOVE: 3 // ... }; Component({ attached() { this.GuestureModeShared = wx.worklet.shared(GuestureMode.INIT); this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.shareScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.shareScale.value})` } }); // ... }, methods: { onScaleGestureHandle(e) { 'worklet'; const { state } = e; if (state === GestureState.BEGIN) { this.GuestureModeShared.value = GuestureMode.INIT; } else if (state === GestureState.ACTIVE) { if(this.GuestureModeShared.value === GuestureMode.INIT) { this.gestureBefore(e); // 手势类型未知时,判断手势类型 } else { this.gestureHandle(e); // 手势类型已知时,处理手势事件 } } else if (state === GestureState.END || state === GestureState.CANCELLED) { this.GuestureModeShared.value = GuestureMode.INIT; } }, // 判断手势类型 gestureBefore(e) { 'worklet'; const { focalDeltaX, focalDeltaY, scale } = e; if (Math.abs(focalDeltaX) > Math.abs(focalDeltaY)) { this.GuestureModeShared.value = GuestureMode.SWIPE; } else if (scale > 1) { this.GuestureModeShared.value = GuestureMode.SCALE; } else { this.GuestureModeShared.value = GuestureMode.MOVE; } }, // 处理手势事件 gestureHandle(e) { 'worklet'; if (this.GuestureModeShared.value === GuestureMode.SCALE) { this.shareScale.value = e.scale; } else if (this.GuestureModeShared.value === GuestureMode.SWIPE) { // swiper 切换模式时,这里什么都不用做 } else if (this.GuestureModeShared.value === GuestureMode.MOVE) { this.shareX.value += e.focalDeltaX; this.shareY.value += e.focalDeltaY; } }, // 用于判断手势事件是否应该派发 shouldResponseOnMove(e) { 'worklet'; return this.GuestureModeShared.value === GuestureMode.SWIPE; // 当模式为SWIPE时,才响应手势事件 } } }) [图片] 通过上面的代码,我们实现了手势协商,当用户在图片上进行滑动的操作时,总是会触发 <scale-gesture-handler> 的手势事件,通过对图片当前状态的判断来决定应该触发哪种手势,我们通过此种协商让 <horizontal-drag-gesture-handle> 手势在合适的时机触发,以此避免手势冲突。 5、使用小程序渲染框架时需要注意的一些地方作为一款新的渲染优化方式,开发者使用小程序渲染框架需要注意以下内容,以保证渲染的效果和性能。 (1)自定义路由时首帧渲染&首帧性能优化 小程序渲染框架的首帧渲染对共享元素动画非常重要,若共享元素节点的key 错过首帧设置的话,可能会丢失飞跃动画,所以在使用小程序渲染框架时,共享元素的 key 应该尽量在 attached 中或之前设置到页面,并且在首帧渲染时,应尽可能的减少 UI 层的渲染工作 如下: 1)所需要的数据应尽可能使用提前计算好,避免构建页面时等待太久影响响应速度 2)首次设置的数据应该尽可能的少,避免首次渲染时,页面上的元素过多,导致首帧渲染时间过长,导致动画卡顿(如:不要同时初始化太多的 <swiper-item>) 3)确保首帧渲染时,共享元素的 key 正确的设置,避免在首帧渲染时,由于找不到对应的共享元素,导致动画丢失,看不到飞跃动画 4)由于手势事件触发频繁,应尽量避免大量需要的计算的逻辑高频执行,容易导致机器发烫,或者导致动画卡顿 **worklet 函数的使用** worklet 函数的使用有一些限制,主要是由于它是在 UI 线程执行的,所以 worklet 函数中的 this 并非是页面的 this 实例, 里面所使用到的变量也是通过特殊的 babel 插件转换到UI线程的,需要与逻辑层共用的变量都需要用 wx.worklet.shared 将它声明成共享变量,在 UI 线程调用逻辑层的函数需要使用 wx.worklet.runOnJS (2)与 web 规范的差异 虽然小程序渲染框架尽可能的与 web 规范保持一致,但是由底层渲染引擎的限制,还是有一些差异,如: 1)display: flex 的默认朝向是 column,而不是 row,这需要开发者注意,官方后续会支持 block 布局方式 2)暂不支持 css 伪元素,如 ::after、::before,官方正在支持中 3)position 仅支持 absolute、relative,不支持 sticky,实现滚动吸附的效果需用 sticky-* 组件来配合 scroll-view 实现 ** <share-element> 在非小程序渲染框架运行环境里的表现是什么** 在非小程序渲染框架的运行环境内,<share-element> 组件会被视为一个 <view> 组件,需要做好布局的兼容 6、何时使用小程序渲染框架开发时,请确保小程序开发者工具版本是 最新版 nightly,sdk 版本在 2.30.2+,具体限制可参考 文档。 这些新特性的引入,使得小程序渲染框架在小程序开发中的优势更加明显,开发者可以更加便捷地实现各种复杂的交互效果,并且达到接近原生APP的体验。 [图片] 未来展望 1、个性化产品形态:将会根据不同的用户需求和场景,设计出更加符合用户喜好和习惯的动效衔接,进行组件化调用。 2、更加自然和真实的动效衔接:动效衔接将会更加贴近自然规律和真实物理效应,从而增强动画的真实感和用户体验。 3、更加智能化和自适应的动效衔接:动效衔接将会根据用户的操作行为和使用习惯,自适应调整动画效果,从而提高用户体验和产品效果。 4、扩大产品、设计与开发的协作效应:设计对动效的把控、产品对用户的洞察以及开发对新技术的应用,才可以发挥最大化的协作效应。 附1:本文作者 同程旅行研发工程师 同程旅行体验设计师 同程旅行产品经理 附2:代码片段 相册小程序代码片段(请使用 PC 端浏览器打开):https://developers.weixin.qq.com/s/E979jCmP7oHG 附3:UE标注 [图片] 附4:AB 实验效果 AB 实验显著win0.23% [图片]
2023-04-28 - We分析 · 小程序数据分析平台
内含平台介绍、接入指引及系列课程,帮助您快速了解、上手平台,精细化分析小程序数据,驱动业务决策。
2022-11-21 - 昨天开源了一个前端样式库-Vaa,希望能减少代码量(MIT协议,免费商用)
想法 2022年12月份的时候萌生了这个想法,到现在开源,花了两个月时间。 自己也在一边使用一边完善种,直到昨天感觉第一个版本应该是差不多了,于是开始开源,希望能够给大家带来些许帮助。 小巧精致,仅45.8kb 地址:https://github.com/vaaagle/VaaWxss 核心 Flex是这个样式库的核心,我们将一些常用样式通过比较规范的规则,包成了‘类’ 我们经常需要定义宽高,于是重复的定义类然后在类中设置,至少是3行代码 .demo{ height:50px; } 现在vaa里面预设了常用宽高和其它常用样式的类,如 fs-12、fw-600、z-index-1、radius-16、w-100p、h-60、f-c-c等,您只需要了解命名规则然后去使用即可,无需再去定义大量属性重复的类。 我们的预想是,您在看页面的时候,在脑海里设计实现方式的时候,就可以通过拼凑这些类的方式直接实现您设计的效果,比如: 现在有个需要实现一个按钮,那怎么设计? 构思:文本元素(text)+ 字符串居中 + 手机屏幕70%的宽度 + 40px高度 + 圆角 + 文字颜色白色 + 背景颜色蓝色 + 字体大小14px + 距离顶部10px 实现: <text class="w-70p h-40 radius-20 c-fff c-bg-blue2 fs-14 mt10">提交</text> [图片] 使用前提 需要您对flex布局有些了解 [图片]
2023-02-17 - 微盟移动端组件库 Titian Mobile 对外开源
消费互联网时代,纷繁的应用需求催生了产品和功能的指数级增长。在复杂多元的研发场景下,高效快速、保质保量、可持续交付的研发模式,方能支撑高速扩张的业务需求。 01. Titian 是什么? 近日,微盟重磅推出多渠道移动端组件库——Titian。作为移动端前台的“底层”能力,它伴随了“微盟 WOS -新商业操作系统”的研发历程,经历了从无序到有序,从稚嫩到成熟的蜕变,并助力了需要不断实现的“产品一体化、体验一体化、服务一体化、数据一体化和开放一体化”的 WOS 终极形态。 [图片] 经过一年多的不断进化,Titian 取得了大量核心业务应用及有效性验证。Titian 提供了清晰统一的 API ,满足多渠道小程序与多前端框架能力,极大的降低了研发成本与提升需求流转效率。同时,Titian 也是体验良好的重要保障,遵循用户习惯、维持用户心智,为业务提供了有序一致的产品体验。 [图片] 02.设计理念体验设计远不止于核心任务,更需要沉淀与延续设计模式和理念,为后续的设计产出质量与效率提升做基奠。为此,微盟Titian在完成核心任务的同时并行落地了“移动端设计体系”。包含以下三部分:分别为“设计理念”、“视觉语言”、“设计规范”。体系是设计在执行时的规则、标准与指导。 [图片] 针对微盟产研、微盟生态开发者、外部行业开发者3类用户群体,微盟Titian在提升效率、普适通用的核心思路下,基于“有序、普适、多元”的设计理念与价值观,打造具有自身特色的移动端组件库。 · 体验有序一致 即内外的有序,内在拥有统一的标准、规则及模式。外在有统一的设计语言让用户体验有序一致。 · 场景应用普适性 沉淀于微盟核心的 SaaS 业务,对“交易场景”拥有良好的普适性。既能满足当下需求,也能拓展延伸至更广泛的应用场景。 · 品牌调性多元化 丰富多元的场景选择性,满足不同品牌调性展现。贴合用户心智,维持品牌认知。在赋能品牌的同时,开发者更能探索出无限可能。 03.Titian 提供的能力 3.1 完善的基础组件 Titian 目前提供了 60+ 移动端基础组件,足以支撑绝大多数的业务需求。 [图片] 3.2 更丰富的多端小程序支持 借力微盟自研的小程序多渠道转码工具(即将开放),Titian小程序组件库可在微信、支付宝、小红书、快手等渠道的小程序顺畅运行。 3.3 React 和 Vue 同步支持 Titian H5 基于 Web Components 能力,Titian H5 提供 React 和 Vue 3.0 两套组件库。该两套组件库底层互通, API 一致,满足业务方丰富的使用场景。 3.4 H5 与小程序 API 统一小程序与 H5 组件库两者采用统一的 API 设计,方便开发者接入。 3.5 二次开发能力 在 Titian 丰富的基础组件上,用户可以快速开发满足自身需求的定制组件。在微盟已经有大量的案例落地,满足了丰富的业务需求。 [图片] 3.6 多维度主题切换 在确保品牌风格统一的前提下,Titian基于品牌调性提炼了具有共性的视觉特征,分别为颜色、图标、圆角。并用这些特征组合为“通用”、“潮流”、“亲和”套风格(后续会持续增加),让开发者随心选择,让产品视觉更贴合品牌调性,保持品牌识别度,维持C端用户对品牌的心智认知。 3.7 开箱即用的体验 Titian 提供交互式的示例,对于组件最常用的功能,可以直接配置相关属性。业务方可以直接上手操作,简单直观。 04. 开发工具 · 自研打包工具基于 esbuild , Titian 自研了小程序打包工具,让编译流程更加快速,开发体验更加友好。 · VS Code 提示插件 帮助用户在 VS Code 中快捷使用 Titian 组件。 · WXML 格式化工具一个 prettier 格式化插件,方便统一代码风格。 · Figma 设计组件Titian提供了一整套完整的设计组件资源,接入引用即可立即使用,方便快速搭建页面,设计组件覆盖代码能实现的所有能力,做到了设计-研发闭环。 · UED 验收工具对于 Titian 组件,打开验收工具配置后,长按即可变得半透明。可以帮助 UI 快速分辨出 Titian 组件。 05. 优秀案例 经过1年的迭代与不断调优,Titian已为微盟企业内部及微盟商城、CRM、CMS、商户助手、微盟客等客户,累积提供了5000+的移动端场景的应用及验证。 [图片] [图片] [图片] [图片] [图片] [图片] [图片] 未来,微盟将在 Titian 组件库的基础上做更多的探索,开放优化可视化建站平台、D2C 设计图转代码工具、物料市场等新工具新平台,敬请大家多多关注~ 反馈和共建请访问Titian UI - 多渠道移动端组件库PC端官网,进行体验! https://titian.design.weimob.com/ github链接:https://github.com/weimob-tech/titian-design 微盟云开发者可通过微盟云开发者工具中的Titian使用文档指引,查找使用方式! https://doc.weimobcloud.com/word?menuId=53&childMenuId=58&tag=3764
2023-02-07 - Skyline|在小程序实现原生相册的效果
相册在日常生活中经常使用到,如手机自带相册、朋友圈、商品展示图、评论贴图等等,都经常用到相册的能力。 👇下面演示 iOS 原生相册、朋友圈等相册使用效果,我们可以看到图片切换非常顺滑,视觉焦点不变。 [图片] 😭 但是在小程序中,页面切换会有明显的切换感。用户焦点会丢失,缺少视觉关联性。 [图片] 共享元素🔥 为了丰富用户交互效果、提升用户体验、增强视觉关联性,小程序支持了页面间的共享元素 下图展示有无共享元素的页面切换效果,可以看出使用共享元素之后,转场动画更灵活 [图片] 共享元素 经常作用在图片上,例如上面示例中的相册效果,是那么共享元素动画要怎么实现呢? 在页面跳转时,两个页面 key 相同的 share-element 组件则会产生飞跃的过渡效果 [图片] 在上一篇文章中,我们学习了 页面转场动画,共享元素动画跟页面转场动画是类似的,同样是在页面切换间的动画。 动画进度、时间 与 路由进度、时间保持一致(非自定义路由也支持共享元素动画) 在共享元素飞跃的过程中,前后页面图片的裁剪方式(mode) 可能不一致 这种情况下容易导致图片突然跳变,所以我们需要在飞跃的过程中改变图片的大小来保证平滑飞跃 [图片] 在共享元素动画进行的过程中,share-element 可以收到 onFrame 表示动画帧回调 我们可以在帧回调中处理内部元素的显示 例如:我们这里通过在帧回调中改变图片宽高来达到平滑飞跃的效果 // .wxml // .js // 初始化 attached() { this.aspectRatio = shared(0) this.curRect = shared(undefined) // 绑定 worklet 动画 this.applyAnimatedStyle('.img', () => { 'worklet' const curRect = this.curRect.value return { left: `${curRect.left}px`, top: `${curRect.top}px`, width: `${curRect.width}px`, height: `${curRect.height}px` } }) }, // 获取图片初始宽高比 onImageLoad(e) { const { width, height } = e.detail this.aspectRatio.value = width / height }, // 动画帧回调,调整图片大小 onFrame(data) { 'worklet' // 当前帧容器的宽高、进度等信息 const { begin, end, progress, direction } = data ... // 根据图片初始宽高比、共享元素容器、动画进度等计算出变化过程中的值 this.curRect.value = { left = lerp(begin.left, end.left, t), top = lerp(begin.top, end.top, t), width = lerp(begin.width, end.width, t), height = lerp(begin.height, end.height, t), } } 更多共享元素动画原理请查看 官方文档 手势搭配打开图片之后,我们经常需要用到手势来操作图片,如缩放、移动、双击等等 [图片] 我们上次学过的 手势系统 又派上用场啦 通过监听手势事件配合 worklet 函数即可在小程序实现图片预览效果 👇 下面演示缩放手势的处理,除了缩放之外,相册在手势处理上还有很多复杂的逻辑,包括惯性、边界逻辑判断等 点击查看更多相册相关的手势操作 // .wxml // 绑定缩放手势 let sharedValues = this.sharedValues ?? [] // .js // 绑定缩放 this.applyAnimatedStyle('#image', () => { 'worklet' // worklet 函数,sharedValues 变化时,函数会立即执行 return { transform: `scale(${sharedValues[SCALE].value})` } }) // 监听缩放 onScale(evt) { 'worklet' // 连续的手势状态 && 双指放缩 if (evt.state === GestureState.ACTIVE && evt.pointerCount === 2) { // 计算出当前真正的缩放值 sharedValues[SCALE].value = evt.scale / sharedValues[TEMP_LAST_SCALE].value sharedValues[TEMP_LAST_SCALE].value = evt.scale } } 最后,我们来看下小程序实现出来的相册跟原生相册的使用对比,在小程序也可以顺滑的实现类原生的效果啦~ [图片] 目前,同程旅行 已经上线了共享元素结合手势的相册效果,mark这个 相册源码 直接接入到你的小程序吧~ [视频]
2023-08-03 - 微信小程序可视化电影选座组件
推荐一款可视化电影选座组件,具体使用方法请看原文链接:https://juejin.cn/post/6996913047725932575 [图片] gitee地址:https://gitee.com/jensmith/source-coding 原文地址:https://juejin.cn/post/6996913047725932575
2022-04-12 - 这个库能轻松解决99%的异步和逻辑加载时机问题(异步篇)
[图片] 你是否纠结过底层业务逻辑(登陆、获取用户信息等)到底是放app.js的onLaunch还是page的onLoad里比较好,或者因为异步问题被迫放在了onload,我们来分析一下优劣 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 分析 onLaunch处理 优点:底层业务逻辑集中并且只需写一次,比较好维护 缺点:目前没有一个理想的方案来解决onLaunch和onLoad的异步问题,包括注册回调、重写onLoad、请求拦截等。 onLoad处理 优点:因为不涉及跨页面通知,因此异步逻辑比较好处理 缺点:每个页面都得写一次底层业务逻辑,非常繁琐,而且既然是公用的底层业务逻辑,分散在每个页面的onLoad里,好像也不大对劲。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 抉择 按照高内聚低耦合的原则,那逻辑和数据放onLaunch里肯定的,不应该和普通page逻辑耦合在一起,通用的数据和逻辑应该在入口去处理,执行一次到处使用,就像vue的main.js一样,会注册一些技术层的基础设施(路由、状态管理等插件),那业务层的基础设施不就是token、用户信息、所在位置等逻辑吗? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 想象中的最佳实践 那我们的目标就是如何满足两者的优点,避免两者的缺点,做到真正的“高内聚低耦合” 1.保持底层业务逻辑写在入口app.js,避免耦合page里的逻辑 2.能在任何page里第一时间拿到globalData数据 3.使用方便,做到在业务开发中无感知,不需要写额外的调用、通知等代码 4.无任何副作用,不会影响其他功能,比如重写阻塞onLoad 5.灵活可配,适用以后此类任何业务 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 梦想成真先看一段代码 ⬇️ // page.js export default { name: 'Home', onLoadLogin(){ //登录成功(拿到token) && 页面初始化完成 //Tips:适用于某页面发送的请求依赖token的场景 }, onLoadUser(){ //页面初始化完成 && 获取用户信息完成 //Tips:适用于页面初始化时需要用到用户信息去做判断再走页面逻辑的场景 }, onReadyUser(){ //dom渲染完成 && 获取用户信息完成 //Tips:适用于首次进入页面需要在canvas上渲染头像的类似场景 }, onReadyShow(){ //小程序内页面渲染完成 && 页面显示 //Tips:适用于需要获取小程序组件或者dom,并且每次页面显示都会执行的场景 }, } 应该懂什么意思了吧?是不是你理想中的样子,使用起来跟没有似的 ⬆️ 这段示例代码满足了上面的第2、3、4条目标 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 再来看一段 ⬇️ // app.js // 配置自定义钩子,所有钩子都可以随意组合搭配使用,执行机制类似于Promise.all(但不是用Promise实现的) CustomHook.install({ 'Login':{ // 自定义钩子名称、必须大写字母开头 name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ // 自定义钩子名称 name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } //依赖globalData中数据 }, globalData) 怎么样,是不是很棒,依赖globalData,名字可配,连触发规则都可配,而且还附加了可随意组合的功能(意外还解决了页面内逻辑执行时机问题,在下篇讲) ⬆️ 这段示例代码满足了上面的第1、5条目标。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 是不是跃跃欲试了,那就赶紧试试,好用回来告诉我! ⬇️(公司内部已接入两年了很稳定) GitHub:https://github.com/1977474741/spa-custom-hooks [图片]
2023-07-07 - 什么小程序需要教育相关类目?
什么内容或服务的小程序需要补充教育相关类目? [图片] 1、培训机构:1-职业培训 2-课外辅导,小程序涉及以学历教育或成人教育为目的、有线下教学场景的教育培训机构,提供职业培训、课外辅导、语言培训等教育培训业务,需补充教育-培训机构 所需资质:需提供区、县级教育部门颁发的《民办学校办学许可证》及营业执照(或事业单位法人证书、民办非企业单位登记证书)(18年新法规要求,仅针对新增帐号) 案例:下图小程序涉及提供在线教育培训服务,需补充教育-培训机构。 [图片] 2、教育信息服务:小程序内提供教育政策、考试通知等教育信息服务,需补充教育-教育信息服务。 案例:小程序涉及提供高考成绩查询服务,需补充教育-教育信息服务。 [图片] 3、学历教育(学校):适用于小程序提供初等、中等和高等学历教育等服务,需补充教育-学历教育(学校)类目。 所需资质(2选1): 1)公立学校:由教育行政部门出具的审批设立证明或《事业单位法人证书》 2)私立学校:《民办学校办学许可证》 案例:如下图小程序涉及提供专升本学历教育服务,需补充教育-学历教育(学校类目。 [图片] 4、驾校培训:适用于小程序涉及提供驾校在线付费培训服务,需补充教育-驾校培训类目。 所需资质:《机动车驾驶员培训备案》 案例:如下图小程序涉及提供付费学车培训套餐服务,需补充教育-驾校培训类目。 [图片] 5、驾校平台:适用于小程序涉及为驾校主体提供入驻渠道,提供多家驾校在线培训等服务,需补充教育-驾校平台类目。 所需资质(同时提供): 1、两家或以上的驾校培训公司的合作协议 2、驾校培训机构的《机动车驾驶员培训备案》 3、《非经营性互联网信息服务备案核准》 4、《小程序开发者承诺函》(注:《小程序开发者承诺函》在申请类目详情页处含示例模板) 6、教育平台:适用于小程序涉及为学历教育、成人教育的培训机构提供入驻渠道、提供多家教育培训等服务,需补充教育-教育平台类目。 所需资质(二选一): 1、《非经营性互联网信息服务备案核准》、两家或以上的培训机构合作协议、及培训机构的《民办学校办学许可证》 2、《小程序开发者承诺函》、及其他平台良好经营情况的证明材料(如app store 评分超过50万人及app内提供该类目的服务内容截图) 7、素质教育:适用于小程序涉及艺术培训、语言培训等为目的、有线下教学场景的培训机构,及为艺术、语言培训机构提供入驻渠道等服务 注:若提供多家艺术教育培训等服务,需补充“教育-素质教育”类目。 8、婴幼儿教育:适用于小程序0~6岁年龄阶段的婴幼儿教育服务,需补充教育-婴幼儿教育类目。 案例:小程序涉及提供婴幼儿教育服务,需补充教育-婴幼儿教育类目。 [图片] 9、教育-在线教育:小程序涉及提供教育答题类服务,需补充教育-在线教育类目。 案例:下图小程序涉及提供教育类答题服务,需补充教育-在线教育类目。 [图片] 10、教育装备:小程序提供教育教学活动所需的教具、学具、器材、设施、场所及其配置服务,需补充教育-教育装备类目 案例:下图小程序涉及提供科学实验、电子演示器教育设备配置服务,需补充教育-教育装备类目 [图片] 11、出国留学:小程序内涉及提供境外教育服务,需补充教育-出国留学类目。 案例:如下图小程序涉及提供国外留学签证服务,需补充教育-出国留学类目。 [图片] 12、特殊人群教育:小程序内提供特殊人群方面相关的教育服务,需补充教育-特殊人群教育类目。 案例:如图小程序涉及提供自闭症儿童教育服务,需补充教育-特殊人群教育类目。 [图片] 13、在线视频课程:小程序内提供教育行业提供,网课、在线培训、讲座等教育类直播,需补充教育-在线视频课程类目。 所需资质(5选1): 1、《事业单位法人证书》(适用公立学校) 2、区、县级教育部门颁发的《民办学校办学许可证》(适用培训机构) 3、《信息网络传播视听节目许可证》 4、全国校外线上培训管理服务平台备案 5、教育部门的批准文件 案例:如下图小程序涉及提供小学生作文思维提升在线视频课程培训服务,需补充教育-在线视频课程类目。 [图片] 14、素质教育:小程序内提供以艺术培训、语言培训等为目的、有线下教学场景的培训机构,及为艺术、语言培训机构提供入驻渠道等服务,需补充教育素质教育类目。 案例:如下图小程序涉及提供线下门店的拉丁舞蹈培训服务,需补充教育素质教育类目。 [图片] 15、教育平台:小程序内涉及为学历教育、成人教育的培训机构提供入驻渠道、提供多家教育培训等服务,需补充教育-教育平台服务类目。 所需资质(需同时提供): 1)《非经营性互联网信息服务备案核准》 2)两家或以上的培训机构合作协议,及培训机构的《民办学校办学许可证》 案例:如下小程序涉及为职业资格、技能培训、电脑课程等培训机构提供入住渠道,需补充教育-教育平台服务类目。 [图片]
2021-12-03