- 发送邮件,云函数
目的 收集用户反馈,发送到开发者邮箱。 效果[图片] 开发步骤 1.开启邮箱的imap服务(授权码在云函数里用得到) [图片] 2.新建云函数sendMail,并[代码]npm install nodemailer[代码]下载nodemailer插件,代码如下 [图片] 3.页面调用。可以在app.js里写个共通方法。 [图片] app.sendMail("","意见反馈", this.data.inputFeedback) 体验小程序 主页-->小玩艺-->意见反馈 [图片]
2022-04-22 - 如何申请长期订阅消息模板?
【小程序appid】wxbe365e73174ad7ee 【小程序主体】厦门翔业集团有限公司 【申请模板类目】交通服务 -> 航空 【申请模板名称】健康日报填报提醒 【使用场景】 国企疫情防控需要,用于集团公司向员工发送每日健康填报提醒。根据模板编号111—选座登机牌办理提醒进行改动。预计人数一万左右,根据已存在的子公司防疫小程序来看,五百个人都对这个功能很满意,为此申请。 【模板字段】 温馨提示 {{thing4.DATA}} 填报模块 {{name3.DATA}} 【消息示例】 【示例1】 健康日报填报提醒 温馨提示 请打开小程序上报今日的健康信息 填报模块 每日上报 【示例2】 健康日报填报提醒 温馨提示 请打开小程序回复新的公告信息 填报模块 公告信息
2022-04-07 - Node.js mysql 事务和锁的写法
一、安装相关包 npm install access-db npm install dotenv 然后在项目入口文件(如app.js)的最前面引入[代码]require('dotenv').config()[代码] 新建[代码].env[代码]文件,并添加mysql配置。 [代码] MYSQL_HOST=localhost // 必填 MYSQL_USER=root MYSQL_PASSWORD=123456 MYSQL_PORT=3306 MYSQL_DATABASE= // 必填 数据库名 [代码] 二、事务和锁 transaction() 事务处理的异步函数 let { run, begin, rollback, commit, locks } = await mysql.transaction() 参数 类型 必填 说明 begin Function 是 事务开始函数 commit Function 是 事务提交函数 rollback Function 否 回滚事务函数 run Function 是 执行sql语句函数 locks Object 否 上锁类型 locks详情 field value 说明 shared_locks ’ lock in share mode’ 共享锁 Shared Locks (简称 S 锁,属于行锁) exclusive_locks ’ for update’ 排他锁 Exclusive Locks(简称 X 锁,属于行锁) [代码]begin()[代码]为事务开启函数,在[代码]begin()[代码]函数里面执行数据库操作,通过[代码]run()[代码]来执行各个sql语句,并返回结果。当执行报错,或执行的sql结果不满足条件时,可以用[代码]rollback()[代码]函数进行回滚操作。当执行没问题时,记得提交事务,即执行[代码]commit()[代码]函数。 如果需要类似秒杀活动那种业务。那么,还需要对数据进行加锁,直接在返回[代码]sql语句[代码]后面,加上[代码]locks[代码]对应的类型即可。 示例代码: [代码]import {mysql} from 'access-db' /** mysql 的事务 和 锁 */ let {run, begin, rollback, commit, locks} = await mysql.transaction() await begin(async () => { try{ // 需要加锁的时候,就直接在返回的sql语句后面加上相应的锁,注意要将await括起来 let sql1 = (await mysql.get('user', 10, 'sentence')) + locks.exclusive_locks let sql2 = await mysql.update('user', 10, {money: ['incr', -3]}, 'sentence') let sql3 = await mysql.update('user', 12, {money: ['incr', 3]}, 'sentence') let res1 = await run(sql1) if(res1.data.money < 3){ return await rollback() //回滚事务 } await run(sql2) await run(sql3) await commit() //提交事务 }catch(err){ await rollback() //回滚事务 throw new Error(err) } }) [代码]
2021-07-08 - 数据库中,怎么直接求子数据的和?
{"name":"A","pro":[{"name":"A1","total":1},{"name":"A2","total":2},{"name":"A3","total":3}......]} {"name":"C","pro":[{"name":"C1","total":1},{"name":"C2","total":2},......]} {"name":"B","pro":[{"name":"B1","total":1},{"name":"B2","total":2},{"name":"B3","total":3}......]} 上面的数据库中,怎么直接求所有total的和?不用分组。 db.collection('CARS').aggregate().group({ _id: null, total: $.sum('$pro.total') }).end() 这样测试不成功!
2022-03-22 - 如何点一个链接带自定义参数进入到我的小程序?
如何点一个链接带自定义参数进入到我的小程序?
2022-03-23 - 最佳实践丨从 MySQL/MongoDB 迁移数据至 CloudBase 云数据库
迁移说明本篇文章从 MySQL、MongoDB 迁移到云开发数据库,其他数据库迁移也都大同小异。 迁移大致分为以下几步: 从 MySQL、MongoDB 将数据库导出为 JSON 或 CSV 格式创建一个云开发环境到云开发数据库新建一个集合在集合内导入 JSON 或 CSV 格式文件导出一、导出 MySQL 数据下面的流程中,我们使用 Navicat for MySQL 进行导出。您也可以使用其它 MySQL 导出工具。 1、导出为 CSV 格式 选中表后进行导出: [图片] 类型中选择 csv 格式: [图片] 注:在第 4 步时,我们需要勾选包含列的标题 [图片] 导出后的 csv 文件内容 第一行为所有键名,余下的每一行则是与首行键名相对应的键值记录。类似这样: [图片] 2、导出为 JSON 格式 同样的我们将选中的表进行导出为 json 格式: [图片] 剩余步骤全部选择默认即可。 导出后的样子: [图片] 我们将数组去除,最后是这样: [图片] 二、导出 MongoDB 数据首先我们先启动 mongod 服务: [图片] 启动后此终端不要关闭。 1、导出为 CSV 格式 新打开一个终端,输入以下命令: mongoexport -db <数据库> --collection <集合名称> --type csv -f <字段名1[,字段名2]> -o <输出的文件路径> 更详细的参数说明,请参考 MongoDB 文档。 注:导出 csv 格式时需要指定导出的列,否则会出现如下的报错信息:⚠️ csv mode requires a field list 导出后的样子: [图片] 2、导出为 JSON 格式 新打开一个终端,输入以下命令: mongoexport -db <数据库> --collection <集合名称> -o <输出的文件路径> 更详细的参数说明,请参考 MongoDB 文档。 导出后的样子: [图片] 导入1、新建云环境如果已有云环境,可直接跳过这一步打开云开发控制台新建云环境: [图片] 新建环境后耐心等待 2 分钟环境初始化过程。 2、数据库导入点击添加集合来创建一个集合: [图片] 新建之后我们点进去,并进行导入操作: [图片] 选择我们之前导出的 CSV 或 JSON 格式文件。 注意:这里有两种冲突处理模式:Insert 和 Upsert Insert 模式会在导入时总是插入新记录,同一文件不能存在重复的 _id 字段,或与数据库已有记录相同的 _id 字段。如果希望已经存在的数据不被覆盖掉,应该 Insert 模式。Upsert 模式会判断有无该条记录,如果有则更新该条记录,否则就插入一条新记录。如果不希望产生冗余重复的数据,应该使用 Upsert 模式。这里我们选择 Upsert 模式: [图片] 导入过程完毕后,数据库内可以看到导入的数据: [图片] 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 【技术交流群】添加小助手微信号 Tcloudedu1,回复:技术交流 最新资讯关注微信公众号【腾讯云云开发】
2021-04-15 - 微信小程序云开发连接mysql数据库,小程序云函数操作mysql数据库
小程序云开发的功能是越来越强大了,现在小程序云开发可以直接借助云函数来链接mysql数据,操作mysql数据库了,今天就来给大家讲一讲如何使用小程序云开发的云函数来操作mysql数据库。 首先要明确一点,就是小程序云开发的云函数是基于node.js的,所以我们使用node.js的mysql2模块可以直接来链接并操作mysql数据库,所以我们现在要做的就是怎么样在云函数里使用mysql2模块,并且借助这个模块类库来实现mysql数据库的链接。 老规矩,先看效果图 [图片] 我们这里要做的就是在云函数里链接mysql数据库,并返回链接的mysql数据库的版本号。mysql数据库都能成功链接了,后面对mysql的增删改查操作也就是小意思了。所以我们这里先成功的链接mysql数据库才是最重要的。 一,创建小程序并引入云开发 这里我不在做讲解,我之前有讲过小程序云开发的初始化创建,也有录视频讲解,不懂的同学可以移步去看下,云开发项目的创建视频 https://edu.csdn.net/course/play/9604/284440 这里有3点需要注意的 1,一定要在app.js里做云开发环境的初始化 [图片] 2,在project.config.json里配置云函数的目录 [图片] 3,一定要用自己注册的小程序的appid [图片] 二,创建云函数,名字就叫mysql吧。 在我们的cloud,右键创建云函数 [图片] 三,安装mysql2模块依赖 1,右键我们的mysql云函数,点击在终端中打开 [图片] 2,在终端中输入 npm install mysql2 [图片] 需要你电脑安装npm,如果没有安装,请自行百度,网上很多npm的安装教程的。 [图片] 等待我们的mysql2安装成功 四,编写mysql云函数链接mysql数据库 [图片] 完整的代码给大家贴出来 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') //引入mysql操作模块 const mysql = require('mysql2/promise') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { //链接mysql数据库的test库,这里你可以链接你mysql中的任意库 try { const connection = await mysql.createConnection({ host: "你的服务器ip", database: "操作那个数据库", user: "mysql使用后名", password: "mysql密码" }) const [rows, fields] = await connection.execute('SELECT version();') return rows; } catch (err) { console.log("链接错误", err) return err } } [代码] 记得把上面的host,database,user,password 替换成你自己的。 五,上传并部署云函数 [图片] 部署成功 [图片] 这里有一点需要注意,就是你不能用云函数链接你本地mysql数据库,因为上传云函数以后,是上传到里微信服务器,没有办法调用到你本地mysql到,除非你设置下本地mysql可以被外界访问,或者使用你自己服务器上的mysql数据库。 [图片] 这样就可以成功的使用微信小程序链接我们的mysql数据库了。 到这里我们点用自己定义的mysql云函数,就可以成功的链接我们的mysql数据库了。 [图片] 是不是很简单。 更多关于云开发的知识,可以翻看我之前的文章,也可以看我录制的视频讲解 视频讲解 https://edu.csdn.net/course/detail/9604 有任何关于小程序的问题都可以加石头哥微信2501902696(备注小程序) 我们下一节给大家讲解使用小程序云开发实现邮件的发送功能。敬请期待。
2019-08-03 - 微信小程序环境共享,多个小程序共享一个云开发数据库
我们在做小程序开发时,有时候需要多个小程序公用一个数据库,比如我们做一个外卖小程序,要配套一个骑手小程序,这个时候就要两个小程序公用一个云开发环境,公用一个数据库了。所以今天来教下大家如何多个小程序共享一个云开发环境和数据库。 其实官方给的文档很详细了,但是一个细节官方没有讲到,所以就会导致好多同学做多个小程序共享一个云开发环境时,遇到各种各样的问题。 比如下面这样的问题 [图片] 明明感觉自己按照官方要求,该配置的都配置了啊,但是为啥就是出错呢。所以我这里再带大家完整的配置一遍,把里面的一些注意事项也给大家好强调下。 一,准备条件 1-1,必须同一个主体 首先看官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/ [图片] 要共享云开发资源可以 ,但是必须是同一个主体。什么是同一个主体呢,就是两个小程序必须都是你自己的,或者是你公司的。 如果不是同一个主体,会报如下错误 [图片] 1-2,最新的基础库,最新版开发工具 这里记得调到最新的基础库,开发者工具也尽量用最新的 [图片] 开发者工具这里官方是有要求的 [图片] 二,开通环境共享 我这里以两个小程序共享一个数据库为例 小程序A [图片] 小程序B [图片] 大家这里记得我们是小程序A 共享数据库给小程序B 2-1,开通环境共享 开通,使用 1.03.2009140 或以上版本的开发者工具,进入云控制台,到 “设置 - 拓展能力 - 环境共享” 点击开通即开通环境共享能力 [图片] 2-2,开通后授权给别的小程序 [图片] 环境共享开通后将在顶部tab显示环境共享功能,进入 “环境共享” 的页面,点击“添加共享”,即可授权同主体下其他小程序/公众号使用当前小程序下的云开发资源 [图片] 这里填写你要共享小程序的appid,我们这里取小程序B的appid [图片] 授权,选择共享的云环境,默认选中所有环境操作权限,可根据实际使用场景自定义授权。这里建议保持默认即可 [图片] 比如我这里分享给小程序B(编程小石头) [图片] [图片] 2-3,使用共享的云开发环境 我们上面操作好以后,就可以在小程序B的云开发后台看到共享的云开发环境了。将我们的云开发环境切换下就可以查看和使用共享的资源了。 [图片] 可以看到小程序B(编程小石头)可以查看小程序A的数据库了 [图片] 三,请求共享的数据库 我们接下来就在小程序B里调用小程序A的数据库了。官方提示的是调用之前要在小程序A里创建一个如下的云函数,但是我在测试的时候发现不用创建也可以的。 [图片] 所以我们就先不创建cloudbase_auth 云函数,来看看能不能调取到数据。 3-1,初始化云开发环境 我们小程序B想使用小程序A的云开发环境,这里要注意,初始化的时候要如下面注释里写的一样,用小程序A的appid和云开发环境id [图片] 3-2,调用资源方数据 初始化以后不能想正常调用云开发数据库那样了,会报错 [图片] 所以我们这里要改变下使用方法。如下 [图片] 这时候还会报错,是因为我们忽略了官方的一个要求:“ 跨账号调用,必须等待 init 完成”,所以我们必须给init加一个await语法,如下,记得await要结合着async一起使用。 [图片] 可以看到我们成功的请求到了小程序A的数据。直接get的时候记得改下数据库权限奥。 [图片] 代码贴出来给大家,记得改成自己的配置 [代码]Page({ async onLoad() { // 声明新的 cloud 实例 var c1 = new wx.cloud.Cloud({ // 资源方 小程序A的 AppID resourceAppid: 'wx7c54942dfc87f4d8', // 资源方 小程序A的 的云开发环境ID resourceEnv: 'test-ec396a', }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 await c1.init() // wx.cloud.database().collection('xiaoshitou').get() c1.database().collection('xiaoshitou').get() .then(res => { console.log('共享环境请求数据成功', res) }) } }) [代码] 四,调用共享环境的云函数 4-1,调用资源方里的云函数 我们这里在小程序B(编程小石头)里调用小程序A里的云函数试试。 如小程序A里有一个xiaoshitou的云函数 [图片] 可以看到我们可以成功的调用小程序A里的xiaoshitou云函数 [图片] 是不是很简单。今天就给大家讲到这里了,欢迎关注,后面会分享更多小程序开发的知识给大家。
2022-02-24 - 如何实现加入购物车的抛物线效果
一、场景分析 在一些如商城、点餐小程序中实现购物车抛物线效果可以提升界面趣味性增加小程序用户体验。 二、效果预览 效果图压缩后速度有点快,请下载代码片段预览 [图片] 三、实现原理 当用户点击物品时记录当前触摸点,根据触摸点计算抛物线运动的顶点位置,通过触摸点、顶点、购物车的位置计算出抛物线运动轨迹,然后控制 icon 运动。 计算购物车在当前手机内的位置 [代码]/** 设置购物车的坐标位置 **/ wx.getSystemInfo({ success: (res) => { let busPos = {} // x y 坐标分别取屏幕百分之八十的位置 busPos['x'] = res.windowWidth * 0.8 busPos['y'] = res.windowHeight * 0.8 this.setData({ busPos }) } }) [代码] 商品点击事件的处理 点击物品后记录点击的位置,然后根据点击位置计算出抛物线的顶点位置,计算方式为点击位置的上方+150,右边+150(需要根据点击位置是否在购物左边还是右边进行判断)。 根据点击,顶点,购物车三个位置计算出抛物线运动轨迹 以3个控制点为例,点A、B、C、AB 上设置点 D、BC 上设置点 E、DE 连线上设置点 F,则最终的贝塞尔曲线是点F的坐标轨迹; 计算相邻控制点间距; 根据完成时间,计算每次执行时 D 在AB方向上移动的距离,E 在 BC 方向上移动的距离; 时间每递增 100ms,则 D、E 在指定方向上发生位移,F 在 DE 上的位移则可通过 AD/AB = DF/DE 得出; 根据 DE 的正余弦值和 DE 的值计算出F的坐标。 开启定时器,依次按照贝塞尔曲线位置做动画位移 使用定时器将抛物线运动轨迹做动画位移。 定时器执行完动画后将购物车角标+1 老规矩,结尾放代码片段 https://developers.weixin.qq.com/s/PnYfitmG7Hxv
2022-03-04 - 订阅信息恶心至极?没有服务器的我们也能直接发消息到微信(公众号)! [即抄即用,拎包入住]
更新时间(2020/12/2) 大家好,众所周知,今年左右新出的订阅消息对商家和用户的友好度都极低,少了一个直接发信息到微信的最重要的渠道。 [图片] 那么我们动动歪心思,直接发消息到公众号(其实这是不是wx的本意???)。 所有服务号都可以在功能->添加功能插件处看到申请模板消息功能的入口,但只有认证后的服务号才可以申请模板消息的使用权限并获得该权限。 为方便公众号用户方便、快捷地接入小程序服务,公众号用户可复用公众号资质创建小程序。当前每个账号的模板消息的日调用上限为10万次,单个模板没有特殊限制。当账号粉丝数超过10W/100W/1000W时,模板消息的日调用上限会相应提升。这还不美滋滋吗,对于小商家来说等于无限次了。 所以我们一开始最好就在公众号进行微信认证,复用公众号的资质来注册小程序(注意,可以复用5个(好像))。 (我付出了¥300的惨痛代价)。 -------------------------------------------------------------------------------- 以上搞定后,流程走起 重点中的重点:为您的函数申请公网固定ip 有了这个ip之后,没有服务器的我们(穷)就能绕过ip白名单,这不正是真正的云开发精神吗? [图片] 0.用你的小程序账号登陆腾讯云,并在里面新建一个云函数 [图片]>>[图片] 选择你要发消息到公众号的小程序 [图片] [图片] -----------------我是2020/10/23的拎包哥----------------------- 创建并选择角色 记得在 https://console.cloud.tencent.com/cam/role 创建角色 [图片] 勾选tcb,scf [图片] 在搜索框里搜scf,tcb后,有什么策略就勾选什么策略 [图片] [图片] 创建角色名 [图片] 在云函数启用刚刚新建的角色 [图片] ps. 记得做好这一步,不然各种报错 missing authorationo key // 报错:缺失授权键 you are not authorized to xxx // 你没有权限去xxx 1.打开 https://cloud.tencent.com/document/product/583/38198 ,申请白名单 [图片] 2.审核通过后,再跟着步骤走。 [图片] 最后你的云函数会得到一个公网固定ip [图片] 3.开始码云函数的代码 如下,记得把wx-server-sdk和request-promise的包都npm下来。 依赖包可以通过这里上传上来 [图片] 我的做法 把获取的accessToken储存在云开发的数据库里。这样就不用担心access token的生成次数超过限制了 'use strict'; const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const resInfo = db.collection('resInfo') const appid = '公众号的APPID'; // APPID const secret = '公众号的密钥'; // Secret const rp = require('request-promise') exports.main_handler = async (event, context, callback) => { var that = this var options = { url: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + appid + '&secret=' + secret, json: true } return await rp(options) .then(async res1 => { return await resInfo.where({name:'outInfo'}).update({ data: { access_token: res1.access_token } }).then(res2 => { return res2 }) }).catch(err => { return err }) } 公众平台以access_token为接口调用凭据,来调用接口,所有接口的调用需要先获取access_token,access_token在2小时内有效,过期需要重新获取,但1天内获取次数有限,开发者需自行存储,详见获取接口调用凭据(access_token)文档。4.设置触发器(触发器偶尔会失灵,所以最好是59min触发一次)。 [图片] 权限设置 由于云函数的访问不存在openid,所以安全规则必须为任何人可读可写。 [图片] 5.有了可以稳定刷新的access_token后,根据需求挑选你的公众号模板消息,开始你的表演。 例如:做餐饮小程序的朋友都想用户下单后发送订单信息到商户。那么就需要 获取商户的openid 步骤 注:公众号的openid在小程序开发工具就可以查出来 5.1 获取公众号openid列表 https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN 注意我没有加上next_openid = NEXT_OPENID,是为了取出公众号的所有的openidopenid列表在 res.data.openid5.2 for循环openid列表,根据商户的微信信息(nickname,city等等)找出只属于商户的openid https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN --------------------------------------- var openidList = openid列表 for(var i in openidList){ wx.request({ url:'https://api.weixin.qq.com/cgi-bin/user/info?access_token='+ openList[i] + '&lang=zh_CN', success(){ console.log(....) } }) } 这里就不用unionid了,不用浪费时间在上面), 就可以使用只发给商户的模板消息了。 公众号对应文档链接 https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_a_User_List.htmlhttps://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId, -----------------------------------------效果图--------------------------------------- [图片] -------------------------------------------------------------------------------- 哎,差不多了,感觉是有点折腾,如果感觉还是不够直白的,可以指出来我继续补充 。 求点赞,你的评论就是对拎包哥最大的支持。 [图片][图片] ===================更新于2020/10/23======================
2020-12-02 - 小程序 云开发 企业付款到零钱
终于轮到我来装一次b了 之前总是有求于各位神,现在来回馈了。 各位用小程序云开发,要实现退款、企业零钱的可以看过来。 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const config = { appid: '**************', //小程序Appid,填自己的小程序id envName: '*************', // 小程序云开发环境ID mchid: '***********', //商户号,填自己的商户号 pfx: require('fs').readFileSync('./apiclient_cert.p12'),这里是下载的api证书。证书怎么下在呢?网上有 partnerKey: '123111111111111111111111111111111111111111111111111', //此处填商户密钥 notify_url: ' ', //支付回调网址,这里可以随意填一个网址 spbill_create_ip: '127.0.0.1' //不用改 }; const db = cloud.database(); const TcbRouter = require('tcb-router'); //云函数路由 const rq = require('request'); const tenpay = require('tenpay'); //支付核心模块 这里要是报错,直接搜 nps + 报错内容 // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() console.log("提现走到了函数",event) const api = tenpay.init(config); var tixian = event.tixian // 申请企业付款到用户零钱 const orderNumber= 'dlbmoney' + new Date().getTime() + Math.floor(Math.random() * 1000) const datas = { partner_trade_no: orderNumber, openid: wxContext.OPENID, amount: tixian * 100, desc: "订单说明", check_name: "NO_CHECK", //不检查实名 spbill_create_ip:"123.151.79.109" } const result = await api.transfers(datas) return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } }
2021-07-07 - 如何策划一场运营活动?
前言 对于一款产品来说,想要快速提高一些指标,那么做活动肯定是一种必备的运营手段,那么做运营活动需要怎么做呢? 今天主要讲解做活动的 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 - 云托管服务器端接收不到请求参数?
1、小程序端使用以下两种方式请求服务端,在服务端都接收不到code参数 [图片] 2、使用云端调试工具,将参数code拼接到请求url中,后台可以接收到参数,将参数放在body中,后台接收不到参数 [图片]
2021-12-09 - 微信小程序中实现定位以及逆地址解析
前言 在微信小程序开发中,我们可以提前获取用户的地理位置,为用户提供更好的服务,因此我们今天就来实现一下。 一、原理 通过微信小程序的开发文档,我们可以发现 wx.getLoaction 能够获取到用户所在位置的经纬度,并且通过腾讯地图提供的逆地址解析中可以将经纬度信息还原成城市名称。 在实际开发当中,我们可以分为两个部分,一个是腾讯地图key的获取,另一个是微信开发端的编码。 二、腾讯地图key 创建一个腾讯地图的账号。(需要手机号和邮箱号)腾讯地图官网 登录成功之后可以点击右上角的控制台就会出现下图的界面,点击创建应用数量,进入到应用的管理页面。 [图片] 创建一个应用.。(应用名称、应用类型如实填写即可) [图片] 随即在我的应用中会显示刚刚创建的,点击添加key [图片] 信息如实填写就可以了,[代码]注意:启用产品选项要勾选 WebServiceAPI 和 微信小程序[代码],如果忘记勾选的也可以在创建key之后重新编辑配置。[代码]APPID需要到微信小程序网站查阅[代码] [图片] 添加成功之后,在创建好的应用可以看到key。 [图片] 二、编码 1. App.json [代码]"permission": { "scope.userLocation": { "desc": "为了更好的服务体验,我们希望获取你的位置" } } [代码] 2. JavaScript [代码]// 这里我选择在onShow中触发,可以根据具体情况设置触发事件 data: { locationObj: {} } onShow: function () { // 调用定位方法 this.getUserLocation(); }, // 定位方法 getUserLocation: function () { let _this = this wx.getLocation({ type: 'gcj02', // type有两中类型,gcj02 是腾讯地图所能解析的 success: res => { _this.setData({ locationObj: res }) // 调用获取城市名称方法 _this.getCity() } }) }, // 获取定位城市名称方法 getCity: function () { var _this = this wx.request({ url: `https://apis.map.qq.com/ws/geocoder/v1/?key=key填写的位置&location=`+ _this.data.locationObj.latitude + ',' +_this.data.locationObj.longitude, success: res => { console.log(res) // 此处返回的就是需要查询的城市名称 } }) }, [代码] 3. 返回值 逆地址解析之后的返回值如下: [图片] 总结 综上所述,便是今天介绍的微信小程序中定位及逆地址解析的实现方式。更多的参数信息,可以查阅本文末的开发文档链接。 [代码]最后,如果您有更好的方法,欢迎在留言区中分享;或者实际操作中遇到什么问题均可留言或者私信我,感谢您的观看![代码] 微信开发文档:wx.getLocation(Object object) 腾讯开发文档:逆地址解析 原 文 链 接 :JhouXu博客
2021-03-02 - 快速上手微信云托管
微信云托管是为开发者提供的后端服务云原生解决方案,支持托管任意语言及框架的容器化应用,创建环境即可享受能自动扩缩容的容器资源,用户可面向代码/镜像等方式使用,免服务器、免运维。
2022-07-29 - 获取用户位置信息时需填写用途说明
各位开发者:大家下午好。在一些小程序/小游戏的业务逻辑中,有时需要依赖用户所在的地理位置来提供服务,当前开发者可以通过调用 调用 wx.getLocation / wx.authorize 等接口获取用户的地理位置信息或授权。 根据 iOS 系统对用户隐私保护的要求,同时我们也为了让用户可以更好的判断是否要将地理位置信息提供给开发者,故调整为当小程序/小游戏获取用户地理位置信息时,开发者需要填写获取用户地理位置的用途说明。填写的说明将在地理位置授权弹窗中展示,如下图所示: [图片] 具体开发方法如下: 在 app.json 里面增加 permission 属性配置(小游戏需在game.json中配置): [代码][代码]"permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } } [代码][代码]详见 小程序开发文档/小游戏开发文档 可在开发者工具(1.02.1812260及以上版本)中进行调试。 2019年1月14日起新提交发布的版本将会受到此调整的影响。需要各位开发者注意,2019年1月14日起新提交发布的版本若未填写地理位置用途说明,则将无法正常调用地理位置相关接口,请及时填写地理位置用途说明。该调整策略在微信客户端 7.0.0 版本生效。另外,考虑到兼容性等问题,在微信客户端 7.0.0 版本以下的环境中不受此策略影响。 微信团队 2018.12.26
2019-04-28 - 基于lottiejs 动画的下拉刷新/上拉加载组件
当前项目苦于没有合适的(好看的) [代码]pullToRefresh[代码] 解决方案(可能是没仔细去找相关内容),所以做了 refreshable_view_miniprogram 组件: [图片] [图片] 现支持 下拉刷新/加载 滚动刷新/加载 基于 lottie 的加载动画 自定义 [代码]lottie[代码] 动画 刷新/加载打断 加载动画时长控制 Lottie 动画部分 在整个基于 lottie 的动画处理中,都是直接传递 lottiefiles.com 中的 lottie json 文件地址来实现的,你也选择任何你喜爱的动画(不过貌似微信小程序的 canvas 绘制表现会有某些不兼容)。 要求 平台 最低基础库版本 安装说明 状态 WeChat 2.8.3 npm 未全部实机测试 安装说明 使用 [代码]npm install --save refreshable_view_miniprogram[代码] 进行安装,具体可以参考(有个拼写错误,npm 包的名称为refreshable_view_miniprogram, 下划线!) 最后 如若在使用过程中有任何问题务必添加issues告诉我;如果该组件对你有帮助,点赞分享它来帮助更多人~
2021-07-18 - 「笔记」字节跳动小程序如何接入腾讯云CloudBase?
前言 最近在把微信小程序迁移至字节跳动小程序,由于服务端使用了腾讯云 CloudBase,网上搜索了一遍,文章千篇一律,都是复制腾讯云官方1年以前的适配器文档,在经过和腾讯云官方技术人员沟通后终于成功解决问题。 安装 npm i @cloudbase/js-sdk -S npm i @maoyan/cloudbase-adapter-tt_mp -S 使用 由于字节跳动小程序没有提供getAccountInfoSync()接口,无法通过接口获取appId 所以需要将appId设置到字节跳动小程序app对象上。 [代码]App({ onLaunch(options) { this.appId = appId } }) [代码] 腾讯云 CloudBase 安全配置 由于字节跳动小程序使用云开发不享受微信生态下的免鉴权,要在终端应用(如APP、小程序等)中使用云开发的身份验证服务,需要将授权的应用加入白名单,并在SDK使用时传入分配的凭证信息。 腾讯云 CloudBase 登陆授权 为了增加安全性,建议开启匿名登陆。启动匿名登录后,用户将不需要登录即可访问应用。如果有更严格的安全要求,可以自行开启其它身份验证方式。 完整代码 [代码]import tcb from '@cloudbase/js-sdk'; import { adapter } from '@maoyan/cloudbase-adapter-tt_mp'; let app; App({ onLaunch(options) { // appId必须设置 this.appId = "字节跳动小程序的appid"; tcb.useAdapters(adapter); // 腾讯云共享环境初始化 app = tcb.init({ env: '云环境id', appSign: '应用标识', // 需要设置成字节跳动小程序的appid appSecret: { appAccessKeyId: '版本', appAccessKey: '凭证' } }) // 匿名登陆 const auth = app.auth() const loginState = auth.anonymousAuthProvider().signIn() let data = await app.callFunction({ name: "云函数名", data: "参数" }); console.log(data) } }) [代码]
2022-03-03 - 小程序粘性布局组件实现
一、前言 开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。 二、demo演示 如图,实现的组件主要适用于以下几种场景: 吸顶页面最上方; 吸顶与页面有固定距离的位置; 在指定容器内吸顶; 嵌套在scroll-view中吸顶。 [图片] 三、代码演示 其中,粘性组件通过<weimob-sticky></weimob-sticky>调用,参数信息用法如下: 参数 说明 类型 默认值 offset-top 吸顶时与顶部的距离,单位px number 0 z-index 吸顶时的 z-index number 99 container 一个函数,返回容器对应的 NodesRef 节点 function - scroll-top 当前滚动区域的滚动位置,非 null 时会禁用页面滚动事件的监听 number - 滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。 3.1 页面代码 3.1.1 基础用法 [代码]<view class="weimob-block"> <view class="weimob-title">基础用法</view> <view class="weimob-body"> <weimob-sticky> <!-- 需要粘性的部分 --> <button class="margin-left-base" size="mini"> 基础用法 </button> </weimob-sticky> </view> </view> [代码] 3.1.2 吸顶距离 [代码]<view class="weimob-block"> <view class="weimob-title">吸顶距离</view> <view class="weimob-body"> <!-- 吸顶时与顶部的距离,单位px --> <weimob-sticky offset-top="{{ 50 }}"> <!-- 需要粘性的部分 --> <button class="margin-left-top" type="primary" size="mini"> 吸顶距离 </button> </weimob-sticky> </view> </view> [代码] 3.1.3 指定容器 [代码]<view class="weimob-block"> <view class="weimob-title">指定容器</view> <view class="weimob-body"> <!-- 这里需要固定高度 --> <view id="container" style="height: 300rpx;background-color: #fff"> <weimob-sticky container="{{ container }}"> <button size="mini" class="margin-left-special"> 指定容器 </button> </weimob-sticky> </view> </view> </view> [代码] 3.1.4 嵌套在scroll-view使用 [代码]<view class="weimob-block"> <view class="weimob-title">嵌套在 scroll-view 内使用</view> <!-- 这里需要固定高度,scroll-view里的元素高度需要大于其高度 --> <scroll-view bind:scroll="onScroll" scroll-y id="scroller" style="height: 400rpx; background-color: #fff;margin-top: 40rpx;" > <view style="height: 800rpx"> <weimob-sticky scroll-top="{{ scrollTop }}" offset-top="{{ offsetTop }}" > <button size="mini" class="margin-left-scoll"> 嵌套在 scroll-view 内 </button> </weimob-sticky> </view> </scroll-view> </view> [代码] 页面js [代码]Page({ data: { container: null, //一个函数,返回容器对应的 NodesRef 节点 scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听 offsetTop: 0 // 吸顶时与顶部的距离,单位px }, onReady() { // 页面渲染完,获取节点信息 this.setData({ container: () => wx.createSelectorQuery().select('#container'), }); }, onScroll(event) { // 容器滚动时获取节点信息 wx.createSelectorQuery() .select('#scroller') .boundingClientRect((res) => { this.setData({ scrollTop: event.detail.scrollTop, offsetTop: res.top, }); }) .exec(); } }); [代码] 3.2 组件代码 组件wxml [代码]<wxs src="./index.wxs" module="computed" /> <view class="weimob-sticky" style="{{ computed.containerStyle({ fixed, height, zIndex }) }}" > <view class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}" style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}" > <slot /> </view> </view> [代码] 组件wxs 这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。 [代码]function wrapStyle(data) { var style = ""; if (data.transform) { style += 'transform: translate3d(0, ' + data.transform + 'px, 0);' } if (data.fixed) { style += 'top: ' + data.offsetTop + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } function containerStyle(data) { var style = ""; if (data.fixed) { style += 'height: ' + data.height + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } module.exports = { wrapStyle: wrapStyle, containerStyle: containerStyle } [代码] 组件js [代码]import pageScrollMixin from "./page-scroll"; const ROOT_ELEMENT = ".weimob-sticky"; Component({ options: { multipleSlots: true }, properties: { zIndex: { type: Number, value: 99 }, offsetTop: { type: Number, value: 0, observer: "onScroll" }, disabled: { type: Boolean, observer: "onScroll" }, container: { type: null, observer: "onScroll" }, scrollTop: { type: null, observer(val) { this.onScroll({ scrollTop: val }); } } }, data: { height: 0, fixed: false, transform: 0 }, behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) { // 非null时会禁用页面滚动事件的监听 if (this.data.scrollTop != null) { return; } this.onScroll(event); })], lifetimes: { attached() { this.onScroll(); } }, methods: { onScroll({ scrollTop } = {}) { const { container, offsetTop, disabled } = this.data; if (disabled) { this.setDataAfterDiff({ fixed: false, transform: 0 }); return; } this.scrollTop = scrollTop || this.scrollTop; if (typeof container === "function") { // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动; // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定; // 情况三:元素初始化。 // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取 // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取 Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( ([root, container]) => { if (offsetTop + root.height > container.height + container.top) { this.setDataAfterDiff({ fixed: false, transform: container.height - root.height }); } else if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height, transform: 0 }); } else { this.setDataAfterDiff({ fixed: false, transform: 0 }); } }); return; }else{ this.getRect(ROOT_ELEMENT).then(root => { // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶 if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height }); this.transform = 0; } else { this.setDataAfterDiff({ fixed: false }); } return Promise.resolve(); }); } }, setDataAfterDiff(data) { // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。 wx.nextTick(() => { const diff = Object.keys(data).reduce((prev, key) => { const prevCopy = prev; if (data[key] !== this.data[key]) { prevCopy[key] = data[key]; } return prevCopy; }, {}); this.setData(diff); this.triggerEvent("scroll", { scrollTop: this.scrollTop, isFixed: data.fixed || this.data.fixed }); }); }, getContainerRect() { const nodesRef = this.data.container(); return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec()); }, getRect(selector) { return new Promise(resolve => { wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => { resolve(rect); }).exec(); }); } } }); [代码] page-scroll.js 滚动事件在页面进入和离开时共享的pageScrollMixin函数。 [代码]function getCurrentPage() { const pages = getCurrentPages(); return pages[pages.length - 1] || {}; } function onPageScroll(event) { const { weimobPageScroller = [] } = getCurrentPage(); weimobPageScroller.forEach(scroller => { if (typeof scroller === "function" && event) { // @ts-ignore scroller(event); } }); } const pageScrollMixin = scroller => Behavior({ attached() { const page = getCurrentPage(); if (Array.isArray(page.weimobPageScroller)) { page.weimobPageScroller.push(scroller.bind(this)); } else { page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)]; } page.onPageScroll = onPageScroll; }, detached() { const page = getCurrentPage(); page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller); } }); export default pageScrollMixin; [代码] 总结 最后,我将上述代码放在了代码片段中供大家使用了解,https://developers.weixin.qq.com/s/qiym3wmr7znx ,希望能够帮到小伙伴们,欢迎评论区建议或指教哦~
2021-01-26 - 修改小程序swiper组件面板指示点样式
修改当前显示眯的宽度及圆角值,记得加!important;如下所示: .wx-swiper-dot-active{ width:25px !important; border-radius: 5px !important; }
2021-07-16 - 想问问图片展示、上传和下载是不是必须是https?
1、上线后image展示的图片地址是不是必须是https?http能显示出来吗? 2、上线后wx.downloadFile下载的图片地址是不是必须是https? 3、上线后wx.cloud.uploadFile的filePath是不是必须是https?
2021-07-13 - 极简代码之云开发的触底无限加载
js: [代码]const db = wx.cloud.database() const _ = db.command const col = "test" const sql = { _id: _.neq(1) } //获取所有记录 Page({ data: { isEndOfList: false, list: [], limit: 20 //每次拉取数量 }, onLoad: function(options) { this.getData() }, getData: function() { db.collection(col) .where(sql) .skip(this.data.list.length) .limit(this.data.limit) .get() .then(res => { this.setData({ list: [...this.data.list, ...res.data], //合并数据 isEndOfList: res.data.length < this.data.limit ? true : false //判断是否结束 }) }) }, onReachBottom: function() { this.data.isEndOfList || this.getData() } }) [代码] wxml [代码]<view style="height:100px" wx:for='{{list}}' wx:key='none'>{{index}}</view> <view style="padding:15px;text-align:center;color:grey" wx:if='{{list.length>limit}}'> <view wx:if='{{(!isEndOfList)}}'>正在加载数据...</view> <view wx:else>----END----</view> </view> [代码]
2020-06-16 - 使用「官方小程序发券插件」发送「商家券」签名的二三事儿
官方「小程序发券插件API」(≤1.1.3) 签名概要 先来看看「小程序发券插件」所需参数,yaml格式如下: [代码]type: object required: - send_coupon_params - sign - send_coupon_merchant properties: bindcustomevent: type: string description: 自定义事件 send_coupon_params: type: array description: 发券参数 items: type: object required: - stock_id - out_request_no properties: stock_id: type: string description: 批次号 example: abc123 out_request_no: type: string description: 发券凭证 example: '1234567' sign: type: string description: 签名 example: 9A0A8659F005D6984697E2CA0A9CF3B79A0A8659F005D6984697E2CA0A9CF3B7 send_coupon_merchant: type: string description: 发券商户号 example: '10016226' [代码] 转换成数据结构即: [代码]{ bindcustomevent: 'getcoupon', send_coupon_params: [ {out_request_no:'1234567',stock_id:'abc123'} ], send_coupon_merchant: '10016226' } [代码] 签名时,文档说明是需要把上述参数,以[代码]key[代码]下标从0开始拉平成1维对象,即: [代码]{ out_request_no0: '1234567', stock_id0: 'abc123', send_coupon_merchant: '10016226' } [代码] 参数[代码]key[代码]下标以[代码]index[代码]记录了顺序,最大支持十张券刚好是0-9,有点儿 [代码]Array.prototype.flatMap()[代码] 的味道,但不完全是。 转换函数如下: [代码]// 可单独抽离成模块 exports.busiFavorFlat = ({send_coupon_merchant, send_coupon_params = []} = {}) => { return { send_coupon_merchant, ...send_coupon_params.reduce((des, row, idx) => (Object.keys(row).map(one => des[`${one}${idx}`] = row[one]), des), {}), } } [代码] 使用如下: [代码]const responder = { send_coupon_params: [ {out_request_no:'1234567',stock_id:'abc123'}, {out_request_no:'7654321',stock_id:'321cba'}, ], send_coupon_merchant: '10016226' } const waitToSign = busiFavorFlat(responder) [代码] 以 v2版nodejs签名代码为例,把以上输出,代入签名,大功告成,如下~ [代码]// let key = 'exposed_your_key_here_have_risks' responder.sign = signHmacSha256(toSignString(ksort(waitToSign), key), key) // console.info(responder.sign) // 20FB971D442A119C85CA1A49DBDEA61A14944D98AFC1D2B040030C5C8792A83A // 签名及数据结构,以JSON串发送给小程序端 // bindcustomevent是前端事件,由前端决定就好 // server.response.toMiniProgram(JSON.stringify(responder)) [代码]
2020-07-21 - 云函数发送email极简代码
const mailer = require('nodemailer-promise') exports.main = async (event, context) => { let sendEmail = mailer.config({ host: 'smtp.exmail.qq.com', //换成你邮箱的smtp port: 465, secure: true, //检查你邮箱的smpt服务器的设置 auth: { user: 'mailer@mycite.cn', //换成你的邮箱账号和密码 pass: 'Pwd' } }) let message = { from: 'anywords', to: event.to, subject: event.subject, text: event.text, } return await sendEmail(message) } [图片]
2020-10-20 - 小程序上传图片到腾讯云对象存储COS的简单代码
const fs = require('fs') const COS = require('cos-nodejs-sdk-v5') const cos = new COS({ SecretId: 'SecretId', SecretKey: 'SecretKey', }) module.exports = async (ctx) => { const image = ctx.request.files.image //这里获得小程序wx.uploadFile上传的文件,文件标识名为image if(!image) return let ext = image.type.split('/')[1] let path = image.path let key = `image-${Date.now()}.${ext}`;//保存在cos的文件名 let TaskId; function p() { return new Promise((resolve, reject) => { cos.putObject({ Bucket: 'Bucket-1251490133', /* 必须 */ Region: 'cn-north', Key: key, /* 必须 */ Body: fs.createReadStream(path), ContentLength: fs.statSync(path).size }, function (err, data) { console.log(err || data); if (err) { reject(); } else { resolve("url/" + key); } fs.unlinkSync(path); }); }); } try{ ctx.body = await p() }catch (err){ console.log(err) } } [图片]
2020-10-20 - 小程序前后端交互使用JWT
前言 现在很多Web项目都是前后端分离的形式,现在浏览器的功能也是越来越强大,基本上大部分主流的浏览器都有调试模式,也有很多抓包工具,可以很轻松的看到前端请求的URL和发送的数据信息。如果不增加安全验证的话,这种形式的前后端交互时候是很不安全的。 相信很多开发小程序的开发者也不一定都是大神,能够精通前后端,作为小程序的初学者不少人也是根据官方的文档去学习开发的。我自己最开始接触小程序也是从wafer2开始的,那时候腾讯云提供的SDK包含PHP和Node.js,因为对于一直做前端的人来说,Node.js的学习成本比较低,只要会JS基本能看懂,也是从那时候才开始接触Node.js,所以本文主要是基于wafer2的服务端基于Koa2的后端来说(其实这个不重要,Node.js基本都差不多)。 什么是JWT? 根据维基百科的定义,JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。 为什么使用JWT? 首先,这不是一个必选方案。有时候我们的API是其它服务端和小程序公用的,那么就涉及到安全验证的问题了。 微信官方不鼓励小程序一打开就要求必须登陆的方式去获取用户信息,因此我们也不能去校验这个用户是否有权限访问这个接口,但是有的接口又不能让任何人随便去看或者被随意采集。 基于token(令牌)的用户认证 用户输入其登录信息 服务器验证信息是否正确,并返回已签名的token token储在客户端,例如存在local storage或cookie中 之后的HTTP请求都将token添加到请求头里 服务器解码JWT,并且如果令牌有效,则接受请求 一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。 关于JWT的详细介绍网上有很多,这里也就不说了,下面介绍在Koa2框架里的添加方法。 安装依赖 [代码]npm install jsonwebtoken npm install koa-jwt [代码] app.js 引用 [代码]const jwtKoa = require('koa-jwt'); [代码] 设置不需要JWT验证的目录或者文件 [代码]const secret = '设置密钥'; app.use(jwtKoa({secret}).unless({ path: ['/','\/favicon.ico',/^\demo/] })) [代码] 数组中的路径不需要通过jwt验证。 授权 小程序 wx.request 发送网络请求的 referer header 不可设置。 其格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本。 那么我们就可以根据 ctx.header 里的 referer 进行初步的限制,比如指定的 appid 才能生成令牌。 我们在生成令牌的时候可以把简单的信息加入进去,如: [代码]const userToken = { referer: refererArray[2], appid: refererArray[3], version: refererArray[4], data: '此处可传入用户的信息' } [代码] 生成令牌: [代码]const jwt = require('jsonwebtoken'); const secret = '设置密钥'; jwt.sign(userToken, secret, {expiresIn: '2h'}); [代码] expiresIn:为令牌的有效期 这样简单的JWT令牌就生成好了,再通过接口返回给小程序端。 小程序前端如何使用JWT? 很简单,在header里加入下面属性即可。 [代码]authorization: 'Bearer 获取到的令牌' [代码] JWT优点 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而JWT不需要。 无状态 JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT,可以降低服务器查询数据库的次数。 JWT缺点 安全性 由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。 性能 JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的http请求比使用session的开销大得多。 一次性 无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。 (1)无法废弃 通过上面JWT的验证机制可以看出来,一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。 (2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个http请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。
2019-02-20 - 写一个能自定义尺寸、样式的switch
小程序原生的switch不能灵活的修改宽高、样式,很不方便,我这边参考WeUI的开关,写了一个可以自定义尺寸样式的switch组件。 直接上代码:https://gitee.com/piscdong/wechat-switch 效果如下图,可以自定义宽高,可以做成方角的 [图片] 代码分析 这个switch主要的难点就是点击后背景颜色变换的动画,这里用到了css的transition、transform两个属性来实现动画,以及::before和::after两个伪元素。 wxml基本结构为: [代码]<view class="switch"> <view></view> </view> [代码] 父级view是整个switch容器,会用到::before做背景色切换动画,::after做禁用时的灰色遮罩。内部的一个view是来回切换的白点。未选中时默认class是switch,选中时增加一个class:switch_checked。 选中状态到未选中状态背景有一个从中间变大到全部的白色动画,所以需要给父级view设置一个颜色作为背景色。 [代码].switch { ... background: #00c000; position: relative; } [代码] 未选中时::before覆盖整个容器,选中时::before设置[代码]transform: scale(0);[代码],这样选中时白色区域就会缩放到最小,再加上transition实现动画效果。 [代码].switch::before { display: block; content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 9999rpx; background: #fff; transition: all 0.35s cubic-bezier(0.45, 1, 0.4, 1); } .switch_checked::before { transform: scale(0); } [代码] 来回移动的白点,未选中时通过[代码]left: 0;[代码]定位到左侧,选中时将left设置为100%定位到右侧,但是这样白点会完全移出容器范围,所以还需要加上[代码]transform: translateX(-100%);[代码]将白点向左再一定自身宽度的100%,同样加上transition实现动画效果。 [代码].switch view { position: absolute; top: 0; left: 0; width: 60rpx; height: 60rpx; border-radius: 50%; background: #fff; box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.4); transition: all 0.35s cubic-bezier(0.45, 1, 0.4, 1); } .switch_checked view { left: 100%; transform: translateX(-100%); } [代码] 关于“::” 最后搭车说一下“:”和“::”,“:”是伪类,“::”是伪元素。按照我的理解:伪类不会在dom中增加节点,只不过是css选择器的一种特殊效果;伪元素会增加节点,flex布局中会影响到其他元素。 为了保证兼容性,css3是允许伪元素使用单个冒号。
2019-12-24 - (4)获取用户信息
背景 我们发现大部分小程序都会使用 [代码]wx.getUserInfo[代码] 接口,来获取用户信息。原本设计这个接口时,我们希望开发者在真正需要用户信息的情况下才去调取这个接口,但很多开发者会直接调用这个接口,导致用户在使用小程序的时候产生困扰,归结起来有几点: 开发者在小程序首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框获取用户信息,会使得一部分用户点击“拒绝”按钮。 在开发者没有处理用户拒绝弹框的情况下,用户必须授权头像昵称等信息才能继续使用小程序,会导致某些用户放弃使用该小程序。 用户没有很好的方式重新授权,尽管我们增加了[代码]设置[代码]页面,可以让用户选择重新授权,但很多用户并不知道可以这么操作。 此外,我们发现开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致的([代码]wx.getUserInfo[代码] 必须通过 [代码]wx.login[代码] 在后台生成 [代码]session_key[代码]后才能调用)。同时,我们收到开发者的反馈,希望用户进入小程序首页便能获取到用户的 [代码]unionId[代码],以便识别到用户是否以前关注了同主体公众号或使用过同主体的App 。 为了解决以上问题,针对获取用户信息我们更新了三个能力: 1.使用组件来获取用户信息 2.若用户满足一定条件,则可以用[代码]wx.login[代码] 获取到的[代码]code[代码]直接换到[代码]unionId[代码] 3.[代码]wx.getUserInfo[代码] 不需要依赖 [代码]wx.login[代码] 就能调用得到数据 获取用户信息组件介绍 [代码][代码] 组件变化: [代码]open-type [代码]属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码]为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的 [代码]detail[代码] 字段中获取到和 [代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me button>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: 1.API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框; 2.组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框。 通过获取用户信息的组件,就可以解决用户再次授权的问题。 直接获取unionId开发者申请 [代码]userinfo[代码] 授权主要为了获取 [代码]unionid[代码],我们鼓励开发者在不骚扰用户的情况下合理获得[代码]unionid[代码],而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得 [代码]unionid[代码]: 1.在微信开放平台下存在同主体的App、公众号、小程序。 2.用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 这样可让其他同主体的App、公众号、小程序的开发者快速获得已有用户的数据。 不依赖登录的用户信息获取某些工具类的轻量小程序不需要登录行为,但是也想获取用户信息,那么就可以在 [代码]wx.getUserInfo[代码] 的时候加一个参数 [代码]withCredentials: false[代码] 直接获取到用户信息,可以少一次网络请求。 这样可以在不给用户弹窗授权的情况下直接展示用户的信息。 最佳实践 1.调用 [代码]wx.login[代码] 获取 [代码]code[代码],然后从微信后端换取到 [代码]session_key[代码],用于解密 [代码]getUserInfo[代码]返回的敏感数据。 2.使用 [代码]wx.getSetting[代码] 获取用户的授权情况 1) 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息; 2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 3.获取到用户数据后可以进行展示或者发送给自己的后端。 One More Thing 除了获取用户方案介绍之外,再聊一聊很多初次接触微信小程序的开发者所不容易理解的一些概念: 1.关于OpenId和UnionId [代码]OpenId[代码] 是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。 [代码]UnionId[代码] 是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过[代码]UnionId[代码],实现多个小程序、公众号、甚至APP 之间的数据互通了。 同一个用户的这两个 ID 对于同一个小程序来说是永久不变的,就算用户删了小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。 2.关于 getUserInfo 和 login 很多开发者会把 [代码]login[代码] 和 [代码]getUserInfo[代码] 捆绑调用当成登录使用,其实 [代码]login[代码] 已经可以完成登录,[代码]getUserInfo[代码] 只是获取额外的用户信息。 在 [代码]login[代码] 获取到 [代码]code[代码] 后,会发送到开发者后端,开发者后端通过接口去微信后端换取到 [代码]openid[代码] 和[代码]sessionKey[代码](现在会将 [代码]unionid[代码] 也一并返回)后,把自定义登录态 [代码]3rd_session[代码]返回给前端,就已经完成登录行为了。而 [代码]login[代码] 行为是静默,不必授权的,用户不会察觉。 [代码]getUserInfo[代码] 只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,开发者可通过 [代码]unionId[代码] 和其他公众号上已有的用户画像结合来提供历史数据。因此开发者不必在用户刚刚进入小程序的时候就强制要求授权。 可以在官方的文档中看到 [代码]login[代码] 的最佳实践: [图片] Q & A Q1: 为什么 login 的时候不直接返回 openid,而是要用这么复杂的方式来经过后台好几层处理之后才能拿到? A: 为了防止坏人在网络链路上做手脚,所以小程序端请求开发者服务器的的请求都需要二次验证才是可信的。因为我们采取了小程序端只给 [代码]code[代码] ,由服务器端拿着 [代码]code[代码] 和 [代码]AppSecrect[代码] 去微信服务器请求的方式,才会给到开发者对应的[代码]openId[代码] 和用于加解密的 [代码]session_key。[代码] Q2: 既然用户的[代码]openId[代码] 是永远不变的,那么开发者可以使用[代码]openId[代码] 作为用户的登录态么? A: 不行,这是非常危险的行为。因为 [代码]openId[代码] 是不变的,如果有坏人拿着别人的 [代码]openId[代码] 来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的 [代码]第三方session[代码] 来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。 Q3: 是不是用户每次打开小程序都需要重新[代码]login[代码]? A: 不必,可以将登录态存入[代码]storage[代码]中,用户再次登录就可以拿[代码]storage[代码] 里的登录态做正常的业务请求,只有当登录态过期了之后才需要重新[代码]login[代码] 。这样子做一则可以减少用户等待时间,二则可以减少网络带宽。 目前微信的[代码]session_key[代码] 有效期是三天,所以建议开发者设置的登录态有效期要小于这个值。
2018-08-17 - java后台怎么根据openid和session_key生成3rd_session?
如题 求大佬解答
2020-07-02 - 微信卡券退出历史舞台后,继任产品「微信支付代金券」和「微信支付商家券」该如何选择
前言 公众平台在2021年02月26日发布了新公告「微信卡券将不再支持新创建“优惠券”通知」,原文点我直达 ,在微信卡券的「优惠券」退出历史舞台后,对于在微信生态内发放优惠券的需求,微信支付优惠券:商家券或支付券(即代金券)我们该如何去选择呢,且听我细细道来。 一、 产品简介 1.1 公众平台卡券 微信卡券功能是腾讯为商户提供的一套完整的电子卡券解决方案,商户可在法律允许的范围内通过该功能实现电子卡券生成、下发、领取、核销的闭环,并使用对账、卡券管理等配套功能,结合SNS、LBS等能力,更可在多渠道投放,进而拉新留存,沉淀用户。通过实现电子卡券的创建、投放、领取、使用,并配套数据对账、门店管理等功能,连接商家与用户 微信卡券功能可分为API接口功能和公众平台卡券功能,使用两种功能均可实现卡券生成、下发、领取、核销,有开发意愿的商户可使用API接口功能,无开发意愿商户可使用公众平台卡券功能 1.2 微信支付代金券 微信支付代金券是微信支付面向商户提供的一种营销工具。商户创建代金券后,可以发送给用户,当用户使用微信支付交易时,代金券会伴随交易自动核销/抵扣,帮忙商户便捷地落地营销活动 微信支付代金券分为全场券和单品券,全场券是基于商户交易订单进行减价,而单品券是基于订单中指定商品减价(下单时需上传单品信息)。全场券只可以配置满减券,单品券可以配置满减券和换购券。 1.3 微信支付商家券 微信支付商家券是微信支付为商户提供的电子优惠券解决方案,商家可在微信支付允许的范围内通过该功能实现商家优惠券信息生成、下发、领取、核销的闭环,并使用数据对账、券信息查询等配套功能完成商家券的管理操作。 微信支付商家券功能当前暂时仅提供API接口功能,有开发意愿的服务商或者商户可使用API接口完成商家券创建、发放、领取、核销、数据对账等全链路操作。未来微信支付将会为无开发意愿的商家提供商户平台页面配置商家券以及对应管理功能。 二、产品开通 2.1 公众平台卡券 已不支持新开,存量券可以通过接口进行修改、删除,不再支持新创建 2.2 微信支付代金券 微信支付代金券拥有两种产品模式,需要分别开通对应功能 开通预充值代金券权限 登录微信支付商户平台,进入「产品中心」->「我的产品」,开通「预充值代金券」产品权限 [图片] 开通免充值代金券权限 登录微信支付商户平台,进入「产品中心」->「我的产品」,开通「免充值代金券」产品权限 [图片] 注:免充值代金券当前开通还需要进行接口升级验收。 「点我查询升级进度」「点我查看免充值产品功能开通指引」 2.3 微信支付商家券 当前仅支持接口创建,无需特别开通功能,拥有微信支付商户号、微信支付服务商商户号均可使用。 三、产品特色 3.1 公众平台卡券 我可以省略吗? 3.2 微信支付代金券 微信支付代金券支持全场满减券、单品满减券、单品换购券,满足大多数日常营销需求。 营销活动除了基础的制券、发券,还包含使用优惠计算、退款、对账等复杂的流程。微信支付提供了完整的方案,可供商户/服务商使用。 商户可以在任意场景(例如:小程序、H5、APP等)调用API发券,也可以定义活动形式(例如:全员发券、新用户发券、抽奖发券等)。 可插入卡包,在券过期前,用户会收到过期提醒,提升核销转化率。 具备安全防刷能力,阻止黑产的不法行为,为营销资金保驾护航。 3.3 微信支付商家券 微信支付商家券可插入用户微信卡包,在券即将过期前,会收到过期提醒通知。通过消息通知,增强用户对商家券优惠感知,提升券的核销转化率,为商家带来更多的交易收益。 微信支付商家券支持在商家自有场景中发放(例如:商家小程序、H5网页、APP、公众号推文),还支持在微信支付流量场景中做投放(例如:附近发券、支付有礼、朋友圈等渠道)。流量渠道的丰富,极大的提升商家优惠所触及到的用户范围。 具备安全防刷能力,阻止黑产的不法行为,为商家营销资金保驾护航。 商家券可跳转商家自有“公众号”、“小程序”和“营销馆”,让用户更容易连接到商家的服务,获取更多商家品牌信息和活动优惠信息。 四、产品解析 4.1 「券」功能使用对象 4.1.1公众平台卡券 我想省略偷个懒 4.1.2 微信支付代金券 面向微信支付商户、微信支付服务商,使用前需要拥有微信支付商户号或微信支付服务商商户号。 4.1.3微信支付商家券 当前仅面向具备开发能力的微信支付商户、微信支付服务商,使用前需要拥有微信支付商户号或微信支付服务商商户号,并具备开发能力。 4.2 「券」的创建方式 4.2.1公众平台卡券 略······偷个懒 4.2.2 微信支付代金券 支持API创建 支持商户平台自助创建 微信支付商户后台->营销中心->创建全场/单品券 注:创建预充值代金券时,需要事先在商户后台->交易中心->充值/转入,充值代金券的活动预算,否则无法创建。 4.2.3微信支付商家券 当前仅支持API创建 后续会支持商户平台自助创建 「券」的业务模式 4.3 「券」的类型 4.3.1公众平台卡券 略······偷个懒 4.3.2 微信支付代金券 全场满减券 满X元减X元(例如:满10元减5元券),商户可在券的“适用范围”字段里向用户说明,券的适用商品情况(例如:“全场商品适用,特价商品除外”或“XX品牌商品适用”)。 单品营销 基于sku 商品编码(即:barcode id)的营销活劢(发单品券、单品实时立减/折扣等),单品营销是面向商品的营销,营销规则的配置基于商品而变。商品具有“多品类,多品牌,多业务线”特征,单品营销是同商品营销规则的集合。 单品满减券 基于单品,以固定金额进行优惠,如 5 元牛奶代金券,则购买牛奶可优惠 5元;商户可在券的“适用范围”字段里向用户说明,券的适用商品情况(例如:“XX品牌商品适用,不适用与XX商品同享优惠”)。 单品换购券 基于单品,以指定金额购买商品,需用户提前领券。如 5 元牛奶换购券,则凭券可以以 5 元的价格购买牛奶。商户可在券的“适用范围”字段里向用户说明,券的适用商品情况(例如:“XX品牌商品适用,不适用与XX商品”)。 注:以上代金券如部分商品不适用或不叠加可使用订单优惠标记功能,券需要消费者提前领取才可以在支付时自动抵扣(需满足优惠条件)。 4.3.3微信支付商家券 满减券 满X元减X元(例如:满10元减5元券),商户可在券的“适用范围”字段里向用户说明,券的适用商品情况(例如:“全场商品适用,特价商品除外”或“XX品牌商品适用”)。 换购券 满X元换商品,商户可以用换购券包装出时尚百货常见的“小样(单品)免费兑换券”(例如:2元换可乐)或“减至券“;商户可在券的“适用范围”字段里向用户说明,券的适用商品情况(例如:“XX品牌商品适用,不适用与XX商品”)。 折扣券 满X元享受X折,商户可在券的“适用范围”字段里向用户说明,券的适用商品情况(例如:“全场商品适用,特价商品除外”或“XX品牌商品适用”),包装出全场折扣券或者指定单品折扣券。 4.4 「券」的发放 4.4.1公众平台卡券 老规矩,略 4.4.2微信支付代金券 商家自有流量场景 4.4.3微信支付商家券 商家自有流量场景 微信支付流量场景 微信公众平台流量场景
2021-04-12 - 云开发实战:小程序导出Ecxcl表格功能
需求 作为信息收集者需要把用户填写的内容,用Ecxcl表格的方式导出。可以支持在线查看或者复制文件下载链接。 [图片] 查看 excle 表格可以直接在手机上查看表格 复制下载地址可以通过链接在电脑上下载 实现 以收集姓名信息为例: 云函数代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async (event, context) => { try { let { dataList, excelName } = event // 1. 定义excel表格名 let dataCVS = 'test.xlsx' // 2. 定义存储数据的 let alldata = []; // 3. 定义表头 let row = ['姓名']; alldata.push(row); // 4. 循环取出姓名数据 for (let key in dataList) { let arr = []; arr.push(applyList[key].name); alldata.push(arr) } //5. 把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //6. 把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: excelName + ".xlsx", fileContent: buffer, }) } catch (e) { console.error(e) return e } } [代码] 注意上传部署云函数的时候需要先安装[代码]node-xlsx[代码] 安装步骤: 打开命令行 [图片] 输入安装命令:[代码]npm install node-xlsx --save[代码] 小程序调用代码 通用调用云函数生成表格代码部分 [代码]createExcel(type) { // 提示加载中 wx.showLoading({ title: '正在加载中...', }) console.log('请求获取') let data = { dataList: 姓名数据集合, excelName: 文件名 } // 生成excel并存储 wx.cloud.callFunction({ name: 'excel', data: data }).then(res => { // 获取存储文件ID this.getFileUrl(res.result.fileID,type) }) } [代码] 根据不同的type来实现不同的功能 2.1 type为0时查看excel 2.2 type为1时复制地址 [代码] getFileUrl(fileID, type) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL that.setData({ fileUrl: res.fileList[0].tempFileURL }) // 下载文件并且打开文档 if (type == 0) { wx.downloadFile({ url: res.fileList[0].tempFileURL, success: function (res) { const filePath = res.tempFilePath wx.openDocument({ filePath: filePath }) } }) } else if (type == 1) { // 复制地址 wx.setClipboardData({ data: res.fileList[0].tempFileURL }) } } }) }, [代码] 扩展阅读 以上涉及到的API文档地址: node-xlsx wx.setClipboardData wx.uploadFile wx.downloadFile wx.openDocument
2021-02-25 - 「干货分享」一文了解微信优惠券产品(卡券、代金券、商家券)
相信很多产品运营和开发的朋友刚接触到微信营销,听到什么优惠券、卡券、代金券、支付券、商家券,是不是一脸懵逼,我只是想做个优惠而已,要不要这么复杂,这到底该接哪一个?希望这篇文章能让你有一个更加清晰的了解。 不管是卡券、代金券、商家券这些我们都可以统称为“优惠券”,而微信支付代金券有另一种叫法“支付券”,其实支付券还包含立减折扣的。 [图片] 优惠券定义 卡券:是微信公众号提供的一套电子卡券解决方案,实现卡券生成、下发、领取、核销的闭环,并使用对账、卡券管理等配套功能。 微信卡券能力不只包含普通的优惠券(代金券、折扣、兑换、团购、优惠券),还有会员卡、礼品卡、票证(电影票、汽车票、景点门票等)。商户可自行在公众平台或通过 接口 创建卡券,多种渠道投放给用户,用户用券时需核销卡券。 比如100元的订单金额,用户有一张10元代金券,商家先核销这10元代金券,再计算用户实际需要支付金额(90元),支付方式不限制微信支付、其他支付也是可以的。 代金券(即支付券):是微信支付面向商户的一种营销工具,商户创建代金券,可以发送给用户,当用户使用微信支付时,代金券会伴随交易自动核销/抵扣,帮助商户便捷地落地营销活动。 代金券类型包含预充值和免充值两种类型,预充值代金券适用于第三方出资策划的活动,例如:满100减10. 指订单金额100元,用户实付90元,商户实收100元;免充值适用于商户策划的活动,例如:满100减10。 指订单金额100元,用户实付90元(用户领券后,在支付中直接核销10元),商户实收90元。 [图片] 商家券:是微信支付为商户提供的电子优惠券解决方案,商家可在微信支付允许的范围内通过该功能实现商家优惠券信息生成、下发、领取、核销的闭环,并使用数据对账、券信息查询等配套功能完成商家券的管理操作。(目前只提供API接口功能,暂无法在商户平台创建) 其实可以说商家券就是卡券优惠券的升级版,都是商家自主核销,只是他们分属不同的平台,一个是公众号(卡券),一个是微信支付商户平台(商家券)。 [图片] 重要通知 微信卡券-优惠券功能现即将下线,有发券需要的商户尽快升级到“微信支付优惠券”:商家券或支付券(即代金券)。此次模块升级不涉及会员卡、礼品卡、票证产品不影响。https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11614329634x9Pvw&version=&lang=zh_CN&token= 优惠券产品框架 从投放场景、类型、核销看一下优惠券生态圈 [图片] 产品能力对比 内容 卡券(即将下线) 支付券(代金券) 商家券 平台体系 微信公众号 微信支付商户号 微信支付商户号 核销规则 商家核销,不限制微信支付 微信支付自动核销 商家核销,不限制微信支付 卡包 领券后进入卡包 平台发券,自动进入卡包;API发券需申请插卡权限 领券后进入卡包 自定义券码 支持 不支持 支持 营销场景 二维码;公众号消息;朋友圈广告;商家H5/APP/小程序 二维码;朋友圈;商家H5/APP/小程序;平台扫码领券、支付有礼、附近发券等 二维码;朋友圈;商家H5/APP/小程序;平台扫码领券、支付有礼、附近发券等 营销经费 无需充值(垫资) 支持预充值和免充值 无需充值(垫资) 开发能力 后台支持基本创建券与核销,同时支持商家API接口 后台支持创建券发券场景,同时支持API接口 目前只有API接口,无法在后台创建券 注:卡券优惠券产品即将下线,就不要过多关注了,了解一下就好了哈~~ 相关链接 微信卡券产品文档:https://mp.weixin.qq.com/cgi-bin/readtemplate?t=cardticket/faq_tmpl&type=info&token=1472580499&lang=zh_CN 微信卡券接口文档:https://developers.weixin.qq.com/doc/offiaccount/Cards_and_Offer/WeChat_Coupon_Interface.html 微信支付代金券产品文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter5_1_1.shtml 微信支付商家券产品文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter5_2_1.shtml
2021-05-10 - 云调用能力—小程序码的讲解
通过服务端云函数可以获取一个小程序任意页面的小程序码,扫描该小程序码就可以直接进入小程序对应的页面,所有生成的小程序码永久有效,可长期使用。小程序码具有更好的辨识度,且拥有展示“公众号关注组件”等高级能力。当用户扫小程序码打开小程序时,开发者可在小程序内配置公众号关注组件[代码]official-account[代码],用户可以快捷关注公众号。 13.2.1 获取小程序码 1、小程序码的接口说明wxacode.get,适用于需要的码数量较少的业务场景,可接受 path 参数较长,页面路径最大长度 128 字节;生成的小程序码永久有效,个数有限。wxacode.getUnlimited,获取小程序码,适用于需要的码数量极多的业务场景。生成的小程序码,永久有效,数量暂无限制,但是此方法只支持 32 个字符。wxacode.createQRCode(不推荐使用),和 wxacode.get 类似,只是生成的不是小程序码,而是小程序的二维码wxacode.get 和 wxacode.createQRCode 总共生成的码数量限制为 10 万个,也就是究极你的小程序的一生,只能通过这两种方式生成 10 万个小程序码和小程序二维码,不过如果参数相同,是不算次数的,所以 10 万个还是挺多的。 wxacode.get 和 wxacode.getUnlimited 的区别 如果你的小程序页面参数是动态更新的,建议使用 wxacode.getUnlimited,如果你的小程序页面包含了非常多的运营类的参数,32 个字符不够用,或者动态页面较少,那可以使用 wxacode.get,通常用 wxacode.getUnlimited 比较稳妥。 wxacode.getUnlimited 可能 32 个字符不够用,比如想追踪分享小程序码的用户的 openid,比如希望记录更多运营数据,不过即使不够用,也是有替代方法的,就是在数据库里添加一个字段 ID,将你要记录的这些参数与这个简短而独一无二的 ID 对应,这个会浪费一点数据库的性能,不过也在可以接受范围之类。 除此之外,在云调用时传递的参数上,wxacode.get 是必须填写 path 的(path 为小程序的页面路径,即包含 page 和 scene),而 wxacode.getUnlimited 的 page 和 scene 是分开的,可以只填 scene,不必填写 page。 2、新建云函数并添加权限首先我们使用开发者工具,新建一个云函数比如 wxacode,然后在 config.json 里添加如下权限配置(前面已经反复强调权限配置文件 json 的格式),也就是我们在处理云调用时,一定要先添加权限,而且权限文件的格式不能出错。 { "permissions": { "openapi": [ "wxacode.get", "wxacode.getUnlimited" ] } } 然后在 index.js 里添加如下代码,我们先以 wxacode.getUnlimited 这个接口为例获取小程序码,然后再把小程序码上传到云存储里, const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxacodeResult = await cloud.openapi.wxacode.getUnlimited({ scene: "uid=1jigsdff", //只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,不能有空格之类的其他字符 page: "page/index/index", //注意这个必须是已经发布的小程序存在的页面(否则报错),根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面;但是你要填写就不要写错路径 }); const uploadResult = await cloud.uploadFile({ cloudPath: `wxacode.jpg`, fileContent: wxacodeResult.buffer, }); return uploadResult.fileID; }; 而如果是使用 wxacode.get 这个接口,它传递的参数会有所不同, const result = await cloud.openapi.wxacode.get({ path: "page/index/index?uid=1jigsdff", }); 调用这个云函数,就能在云存储里看到生成的 wxacode.jpg 小程序码了。我们可以把集合的某个字段的 id,或者页面 id 等参数写进小程序码里。 13.2.2 通过小程序码进入小程序通过追踪带有参数的小程序码,我们就能知道用户到底是通过我们生成的哪个小程序码进入到小程序的,这个功能应用的场景有很多,尤其是运营上特别有用,比如追踪用户的分享来增加积分或返利,追踪各个渠道的运营效果等等,要完成这样的步骤,除了生成带参数的小程序外,还需小程序能识别该小程序码。 1、小程序码与场景值场景值用来描述用户进入小程序的路径,比如公众号文章的自定义菜单、模板消息、文章等,二维码的扫描、长按、通过识别手机相册的二维码等,微信群聊或单聊等,微信首页顶部搜索框等,也就是用户到底是通过什么方式进入到我们的小程序的,会有一个对应的场景值,扫描小程序码的是 1047,长按图片识别小程序码为 1048,扫描手机相册中选取的小程序码为 1049。 我们可以在 App 生命周期的 onLaunch 和 onShow,或[代码]wx.getLaunchOptionsSync[代码](注意,这个接口是一个对象,不是一个函数) 中获取上述场景值,在下面的 options 对象里就会包含 scene onLaunch (options) { console.log('onLaunch方法',options) }, onShow (options) { console.log('onShow方法',options) }, 在 options 对象里就包含着 scene 这个属性,属性的值即为场景值: path: "" //页面路径 query: {} //页面的参数 referrerInfo: {} //来源小程序、公众号或 App 的 appId scene: 1047 //场景值 shareTicket: //带 shareTicket 的转发可以获取到更多的转发信息,例如群聊的名称以及群的标识 openGId 2、获取小程序码里的参数值得注意的是,使用 cloud.openapi.wxacode.get 和 cloud.openapi.wxacode.getUnlimited 生成的小程序码所带的参数在调试时需要使用开发工具的条件编译自定义参数 scene=xxxx 进行模拟,开发工具模拟时的 scene 的参数值需要进行 encodeURIComponent。 首先我们需要 encodeURIComponent()方法将我们要传递的参数进行编码,比如我们要传递“a=3&b=4&c=5”这样的参数,我们可以直接在控制台里进行编码: encodeURIComponent("a=3&b=4&c=5"); 编码之后的结果为"a%3D3%26b%3D4%26c%3D5",调试时可以添加编译模式,在启动参数里填入 scene=a%3D3%26b%3D4%26c%3D5 小程序码不只是一个技术问题,更多的是涉及到运营,让运营的效果可以量化追踪,是增长黑客、数据运营的关键,场景值可以让我们了解小程序的增长来源;而将一些参数写进小程序码,可以让我们根据参数的不同来采取不同的运营策略,比如广告点击、返利、分销、拼团、分享追踪等等。作为开发人员,可以多和运营交流,让小程序的增长更有效果。
2021-09-10 - 如何解决云开发数据库导出csv文件打开中文乱码问题
如何解决云开发数据库导出csv文件打开中文乱码问题 关键时候还是我知乎厉害,亲测可行 以下是mac系统的解决办法,不用输代码,不用下载额外app,只要excel就可以了~ 1. 打开一个新的excel表格,选择File--->Import[图片] 2. 选择从manager里export出来的csv文件 [图片] 会跳出来一个叫做“text import wizard”的三步骤设置面板;在step1中的file origin选项里,选择Unicode(UTF-8) [图片] 跳过step 2 & 3(不用做任何修改),然后点finish导入csv文件: [图片] 这时候就可以看到没有乱码出现的excel sheet啦! 3. 接下来,选择 File--->Save as [图片] 4. 将file format 选择为CSV UTF-8 (Comma delimited),改好文件名后点save [图片] 5. 打开新保存的csv文件,乱码消失! [图片] 作者:艾米午安 链接:https://www.zhihu.com/question/21869078/answer/428936970 来源:知乎
2020-05-04 - 微信小程序的广告组件,可以在页面内循环调用吗?比如一个列表每隔10个就显示一条广告
如题
2021-06-11 - 微信小程序前端JS实现XML和JSON的格式互转
最近开发小程序微信支付的时候,后端返回了XML格式的数据,我需要吧XML转JSON才可以使用数据,话不多说直接看代码 首先下载需要的JS文件(我是放在CSDN上面的):https://download.csdn.net/download/m0_46156566/19550071[图片] JS相关代码,我的是小程序,其他开发同理引入调用就行了 const X2JS = require('../../utils/x2js/we-x2js'); let x2js = new X2JS(); let xmlStr = '' let myJson = x2js.xml2js(xmlStr); console.log(myJson) => {xml:{appId:wx123456,nonceStr:123456789}} 至于JSON转化为XML同理,把x2js.xml2js(xmlStr);换成x2js.js2xml(xmlStr);就可以了,如果还有不懂的可以在下方留言一起讨论
2021-06-10 - 到目前为止小微商户API进件恢复了吗?
到目前为止小微商户API进件恢复了吗?2021-3-15
2021-03-15 - 【微信支付新人必读】智慧的提问,快速的解答
写在开头 在技术社区里,你技术提问的解答情况,很大程度上取决于你提问的方式与解决此问题的难度。 智慧的提问就是好的提问习惯和好的提问规范的结合,它能让你事半功倍。至少在微信支付社区,它是真实成立的 如果您没有时间读完全文,请务必读完微信支付社区提问智慧 首先来看一下,微信支付社区的智慧提问法则: 微信支付社区提问智慧 好的提问习惯 如果你有以下几个提问习惯,能直接解决大部分你想问的问题~ [代码]1. 尝试在搜索框中搜索答案; 2. 尝试阅读相关官方文档以找到答案; 3. 尝试阅读FAQ以找到答案; 4. 尝试自己检查或试验以找到答案; [代码] 好的提问规范 好的提问规范,能在最快的解决让你得到最优效的解答~ [代码]请用陈述句准确描述问题 1. 标题定位到微信支付具体业务,比如:支付分、代金券、普通支付、合单支付等; 2. 50+字详细准确描述问题的症状: 3. 包含必要的错误信息、期待的结果; 4. 包含必要的截图或代码等细节; 5. 请描述已经尝试过的方法。 [代码] 如果期望得到微信支付技术支持,请参考以下模板:(有敏感信息可私信提供) [代码]1. 请求的具体API接口(提供文档地址和请求的URL): 2. 问题发生时间【必填】 3. 商户号【必填】: 4. 商户订单号【必填】: 5. 相关报错信息文案: 6. 完整的请求和返回参数以及单号: 7. 问题截图或视频: 8. 已经尝试过的方法: [代码] 如果你已做了上述事情,我们会非常乐意回答比较规范的优质提问,也会在最快的时间推送到支持组进行针对性的回答。 当然为了维持社区的内容质量,无效提问(空泛、偏离技术讨论、软文传播、推广引流等内容),我们也有有权进行删除处理。 下面跟着大家看一下通用的技术社区提问智慧,仅供参考~ 通用技术社区提问智慧 量不在多,精炼则灵 简单的将一大堆代码或数据罗列在求助信息中达不到目的。如果你有一个很大且复杂的测试样例让程序崩溃,尝试将其裁剪得越小越好。 描述问题而不是猜测 提问中描述是什么导致了问题是没用的(如果你的诊断理论是对的,或许你就不会来这儿咨询求助了?)。所以,确保只要描述问题的原始症状,而不是你的解释和理论,让社区名人或官方支持来解释和诊断。如果你认为陈述自己的猜测很重要,应清楚地说明这只是你的猜测并描述为什么它们不起作用。 错误的示范: 我在XX时遇到了YY错误,怀疑是ZZ原因,这个问题怎么解决? 智慧的提问: 我组装的电脑(电脑信息)最近在开机20分钟左右,做内核编译时频繁地报SLG11错,但在开头20分钟内从不出问题,重启动不会复位时钟,但整夜关机会。更换所有内存未解决问题,相关的典型编译会话日志附后。 按时间先后罗列问题症状 刚出问题之前发生的事情通常包含有解决问题最有效的线索。所以,问题描述中尽可能的描述在问题出现之前都做了什么。在命令行处理的情况下,有会话日志(如运行脚本工具生成的)并引用相关的若干(如 20)行记录会非常有帮助。 提问应明确 漫无边际的问题通常也被视为没有明确限制的时间无底洞。最有可能给你有用答案的人通常也是最忙的人(假如只是因为他们承担了太多工作的话),这些人对于没有止境的时间无底洞极其敏感,所以他们也倾向于不太喜欢那些漫无边际的问题。 如果你明确了想让回复者做的事(如指点方向、发送代码、检查补丁或其它),你更有可能得到有用的回复。(因为)这样可以让他们集中精力并间接地设定了他们为帮助你需要花费的时间和精力上限。 结语 提问的智慧就是一个敲门砖,它会让你了解到一个事实,为什么那些看起来很牛的人几乎从不提问,似乎他们一进入这个行业就是牛人了。不是的,他们也有问题,但是通常在提问之前就自己解决了;不是因为他们本来就懂得怎么解决,而是解决问题的经历让他们成为牛人;最终,你只会看到网络上多了一篇文章:关于解决 某某 问题的方案。 最后,祝你在微信支付开发的路上,早日晋升为大神~希望未来有一天,您也能将在社区得到的帮助回馈给更多需要帮助的“微信支付新人”。
2020-11-12 - 一个微信如何突破5个绑定限制?
一个微信如何突破5个绑定限制?
2021-06-07 - 答题小程序中,如何将数据库里的排行榜数据导出为excel
在答题小程序中,如何将数据库里的排行榜数据导出为excel? 需要借助第三方工具包,没错,就是node-xlsx了。 [图片] [图片] node-xlsx不仅可以解析Excel文件从中取出数据,还能将数据生成Excel文件,因此我们可以将云数据库里面的数据取出来之后保存为Excel文件,然后再将保存的Excel文件上传到云存储。 1、安装node-xlsx npm install node-xlsx 2、引入node-xlsx const xlsx = require('node-xlsx'); 3、查询数据 const dataList = await db.collection("rank").where({ score:_.gt(0) }).orderBy('score', 'desc').limit(100).get() 4、处理数据并导出为excel const data = dataList.data let sheet = [] let title = ['排名','姓名','支部','答题次数','累计总分'] await sheet.push(title) for(let rowIndex in data){ let rowcontent = [] rowcontent.push(data[rowIndex].rank) rowcontent.push(data[rowIndex].name) rowcontent.push(data[rowIndex].dept) rowcontent.push(data[rowIndex].num) rowcontent.push(data[rowIndex].score) await sheet.push(rowcontent) } const buffer = await xlsx.build([{name: "成绩排行榜", data: sheet}]) 5、在云存储生成excel文档 await cloud.uploadFile({ cloudPath: '成绩排行榜.xlsx', fileContent: buffer, }) [图片] 6、下载到本地 [图片]
2021-06-01 - 400元迁移微信公众号留言功能,迁移全过程
背景:2021年5月27日晚8点30分开始策划迁移公众号,迁移的目的为了是为了【留言】功能,因为从2018年开始注册的公众号都没有留言功能。 [图片] 为了不浪费朋友时间,我直接把他的管理员转移给我微信小号了,方便我来回各种扫码。进入主题,开始迁移公众号,先看官方流程图: [图片] 迁移流程总耗时5天(可以缩短到3天) 开始发起迁移: [图片] 管理员(我的微信小号)扫码验证后出现迁移协议,就是下面这个图 [图片] 输入目标账号原始ID(本公众号的原始ID)发送验证后,在目标账号管理员微信端(我的微信大号)允许即可 [图片] 同意后下一步下载迁移公函 [图片] 下载下来我们需要填写迁移双方基本信息,迁移公函需要双方盖章 [图片] 继续准备资料办理公证书:原公众号营业执照、原公众号后台截图、原法人身份证正反面、目标公众号营业执照、目标公众号后台截图、目标法人身份证正反面,公众号后台截图。 [图片] 准备好后开始办理公函公证书,可以在网上办理某宝1-2百左右。线下的话地图搜索:公证处,提前打电话问问能不能做公众号迁移公函公证书,问好了再去,大约3-5百左右。(下面是我的公证书) [图片] 然后我们上传这些资料去提交即可,最后支付300元。(如果资料传错了,有两次免费修改的机会,两次都错了那继续交300) [图片] 然后我们等着就好了,一般情况2、3天(工作日)就搞定了,期间会打电话确认,然后双方公众号管理员同意迁移,约1个工作日迁移成功。 [图片] 至此公众号迁移功能已完成,整理一下我准备的资料。 [图片] 最后做个timeline 2021年5月27日:和朋友沟通拿号 2021年5月28日:把朋友公众号管理员签到我的微信 2021年5月29日:开始准备迁移所有资料 2021年5月30日:TB线上办理公函公证书100,上传后支付300迁移费 2021年5月31日:迁移电话确认,双方管理员同意,迁移成功
2021-06-01 - 分页下载,每次300行,但仍无法显示超过1500行的记录?
小程序是工厂使用,显示月产量会超过1500行,分页下载每次300行,但发现数组只能显示到1500行超过部分就无法显示了,在手机上测试当下载总数超过1500行时直接显示空白,之前下载的数据也显示不了,在开发工具上模拟不会显示空白但也只能显示1500行,是什么原因 发现一个现象 如果 表格中用 block标签 则最多显示1500行,如果用view标签则最多显示1000行
2021-05-29 - 微信小程序this.setData如何修改对象、数组中的值
在微信小程序的前端开发中,使用this.setData方法修改data中的值,其格式为 this.setData({ '参数名1': 值1, '参数名2': 值2 )} 需要注意的是,如果是简单变量,这里的参数名可以不加引号。 经过测试,可以使用3种方式对data中的对象、数组中的数据进行修改。 假设原数据为: data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, 方式一: 使用['字符串'],例如 this.setData({ ['user_info.age']: 20, ['cars[0]']: 'tesla' }) 方式二: 构造变量,重新赋值,例如 var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) 方式三: 直接使用字符串,此种方式之前不可以,现在可以了,估计小程序库升级了。 注意和第一种方法的对比,推荐还是使用第一种方法。 this.setData({ 'user_info.age': 40, 'cars[0]': 'ford' }) 完整代码: Page({ /** * 页面的初始数据 */ data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, change_data: function(){ console.log('对象-修改前:', this.data.user_info) this.setData({ ['user_info.age']: 20 }) console.log('对象-修改后1:', this.data.user_info) var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) console.log('对象-修改后2:', this.data.user_info) this.setData({ 'user_info.age': 40 }) console.log('对象-修改后3:', this.data.user_info) console.log('数组-修改前:', this.data.cars) this.setData({ ['cars[0]']: 'tesla' }) console.log('数组-修改后1:', this.data.cars) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) console.log('数组-修改后2:', this.data.cars) this.setData({ 'cars[0]': 'ford' }) console.log('数组-修改后3:', this.data.cars) } }) 效果: [图片]
2020-08-26 - 企业小程序流量主收到发票通常需要几天就可以提现?
企业小程序流量主收到发票通常需要几天就可以提现?
2021-05-25 - 非小程序管理员如何删除自己的账号被作为运营人员?
做项目的时候老板把我设置了运营者的身份,现在项目吹了老板也联系不上了。请问要怎么样才能在【联系不上管理员的情况下】把自己从运营身份中删除。小程序、公众号同理,谢谢
2021-05-13 - 写在开源答题小程序300个star之计
写在开源答题小程序300个star之计 [图片] 前几天看到star298个的时候,便计划动手写这篇文章,拖了几天,已经308个star了 很高兴无意的一个举动,能收获大家这么多的认可,当然我也会持续完善下去,目前该开源的答题小程序功能还是相对简单,只有以下几个核心功能 1、考试 2、练习 3、错题记录 4、答题历史 5、答题排名 6、海报生成 [图片] 但是作为一个基本的答题小程序这已经足够了 [图片] 在后续我会持续完善该小程序,后面有几个方向 1、在线创建试卷 2、在线录入试题 3、在线批量导入试题 4、考试报表导出 5、 未来的还很长,对于马上走出象牙塔的我来说,还有很多需要学习的地方 我相信任何一个产品都需要不断的迭代,反复打磨,未来可期
2021-05-13 - 公众平台/小程序服务端API的access_token的内部设计
一、背景 对于使用过公众平台的API功能的开发者来说,access_token绝对不会陌生,它就像一个打开家门的钥匙,只要拿着它,就能使用公众平台绝大部分的API功能。因此,对于开发者而言,access_token的使用方式就变得尤其的重要。在日常API接口的运营中,经常遇到各种的疑问:为什么我的access_token突然非法了?为什么刚刚拿到的access_token,用了10min就过期了?对于这些疑问,我们提供出access_token的设计方案,便于开发者对access_token使用方式上的理解。 对于access_token的获取,可以参考公众平台的官方文档:auth.getAccessToken、获取Access token 二、access_token的内部设计 2.1 access_token的时效性 众所周知,access_token是通过appid和appsecret来生成的。内部设计的步骤如下: (1)开发者通过https请求方式: GET https://API.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,传入appid及apppsecret的参数 (2)公众平台后台会校验appid和哈希(appsecret)是否与存储匹配,若匹配,结合当前时间戳,生成新的access_token。 (3)生成新的access_token的同时,会对老的access_token的过期时间戳更新为当前时间戳。 (4)返回新的access_token给开发者。 这里以图示的方式说明一下,新旧token交替过程: [图片] 从上图需要注意的几点: (1)公众平台存储层只会存储新老两个access_token,意味着假设开发者重复调用3次接口,则会导致最早的access_token立刻失效。 (2)虽然请求新的access_token后,老的access_token过期时间会更新为当前时间,但也不会立刻失效,原理请参考 【2.2 access_token 的逐渐失效性】 (3)出于信息安全考虑,公众平台并不会明文存储appsecret,仅存储appid以及appsecret的哈希值。因此开发者要妥善保管appsecret。当appsecret疑似泄露时,需要及时登录mp.weixin.qq.com重置appsecret。 2.2 access_token 的逐渐失效性 从【access_token的时效性】了解到,当开发者请求获取新的access_token时,老的access_token过期时间会被更新为当前时间,但此时不会立刻失效,因为公众平台会提供【5分钟的新老access_token交替缓冲时间】,因此也称为access_token 的逐渐失效性。 实现的原理是: 1. 由于老的access_token过期时间戳已被刷新,所以在API接口请求期间,带上的access_token解开后,过期时间戳会加上5分钟,然后和当前设备时间进行比对,若超过当前设备时间,判断为失效。 2. 公众平台的设备会保持时钟同步,但设备之间仍然可能会存在1-2分钟的时间差异,所以【5分钟】并非绝对的时间值。当开发者获取到新的access_token后应该尽快切换到新的access_token。 [图片] 从上图需要注意的几点: (1)由于存在设备时间同步的差异,可能会导致开发者遇到拿着老的access_token请求API接口,部分请求成功,部分请求失败的情况,建议开发者获取到新的access_token后尽快使用。 (2)通过理解两个图示,对开发者来说,access_token是相当关键且不能乱调的接口,建议开发者统一管理access_token,以免造成多次请求导致access_token失效。
2021-05-11 - 小程序取消橡皮筋回弹效果解决方案及坑总结
提到ios系统的橡皮筋效果,作为开发者是又爱又恨,有想要这个效果又有不想要的,无奈的是却没有一个简单的开关来设置这个效果是否开启。 最近在开发小程序时也遇到有关于ios橡皮筋回弹的问题,这里分两部分(取消橡皮筋回弹效果和因为这个效果遇到的坑)和大家分享一下。 取消IOS橡皮筋回弹效果的解决方案 1) 页面无滚动区域时,可通过页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]{ "disableScroll":true } [代码] 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo1 2) 页面有滚动区域,滚动区域通过view模拟实现,然后在页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]json文件配置 { "disableScroll":true } view元素模拟实现滚动样式 { height: calc(100vh - 120rpx); //高度必须是固定的值 overflow-y: auto; } [代码] 不足之处在于view元素模拟的滚动区域滚动时不够连贯,没有scroll-view那种原生丝滑般的感觉。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo2 3) 页面有滚动区域,滚动区域使用scroll-view,这时通过disableScroll则无法实现,尝试设置一下scroll-view的scroll-y="{{false}}",上拉或下拉时居然不再触发橡皮筋的回弹啦,当然滚动区域也不能滚动。 小脑袋动一动,解决方法有啦! 通过设置一个变量scrollY动态控制滚动区域的滚动从而阻止回弹。 监听bindscrolltoupper\bindscrolltolower当scroll-view区域滚动到顶部或底部时候设置scrollY:false来关闭页面滚动,从而阻止回弹。 监听bindtouchstart\bindtouchmove 当用户反方向滑动的时候设置scrollY:true,再次开启页面滚动。 [代码]wxml滚动区域属性和事件处理,具体实现请点击测试代码链接 <scroll-view scroll-y="{{scrollY}}" class="list" upper-threshold="5" lower-threshold="5" bindscrolltoupper="bindscrolltoupper" bindscrolltolower="bindscrolltolower" bindtouchstart="touchstart" bindtouchmove="touchmove"> <view class="list-item" wx:for="{{list}}" wx:key="{{index}}">{{item}}</view> </scroll-view> [代码] 相对view模拟实现滚动区域,scroll-view滚动更丝滑,不过每次滚动到底部或顶部的时候,再反向滑动时由于再次开启scroll-view滚动会有操作卡顿的感觉,暂时没想到好的解决方法,有解决的大佬希望提供一下想法,一起学习下。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo3 IOS橡皮筋效果遇到的坑 1) 操作左滑删除组件时上下移动,会触发橡皮筋效果导致页面抖动的问题 这个坑的严重程度看设计师的意愿了,反正我们团队目前是需要解决的,方案类似取消橡皮筋解决方案的第三种 在左滑的时候关闭scroll-view的滚动,取消时再次开启滚动 2) 如果页面顶部有置顶的横向滚动区域scroll-view,当页面滚动到底部时继续上拉会导致置顶头部消失,松开回弹后头部又会出现。 坑是社区里的朋友提出来的,我借了个iphone x 一预览,我嚓,还是真是个奇坑! 微信官方回复已复现正在解决中… 不想继续等下去的,暂时解决方法是 监听页面的滚动区域,当滚动到底部时设置顶部横向滚动scroll-view的scroll-x=false来解决。 写在最后 以上便是我在小程序开发中有关于ios橡皮筋回弹效果的分享,示例代码已上传github,可自行下载体验。 https://github.com/YuniorZen/minicode-debug/tree/master/minicode01 目前微信官方虽说已经着手解决(已两年)此类bug,但哪吒说 我命由我不由天,所以还是我们开发者多分享些解决方案自救来的快。 分享方案如有问题还望不吝指出,没有涉及到的坑也欢迎评论提出,一起学习和解决,后续也会基于此篇不断更新总结。
2021-01-14 - JS中的二进制数据处理
前言 在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。例如在C#里面,就用byte[],标准C里面没有byte类型,但可以通过typedef把byte定义为unsigned char的别名,效果是一样的。JS设计之初似乎就没想过要处理二进制,对于字节的概念可以说是非常非常的模糊。如果要表达字节数组,那么似乎只能用一个普通数组来表示。 然而随着业务需求的逐渐发展,出现了WebGL这样的技术。所谓WebGL,就是指浏览器与显卡之间的通信接口。为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。类型化数组(Typed Array)就是在这种背景下诞生的。而类型化数组是建立在ArrayBuffer对象的基础上的。下面介绍一下Arraybuffer。 一、Arraybuffer1.1 基本概念 ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写。 ❝ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。❝1.2 基本操作「语法」 new ArrayBuffer(length) 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节;返回值:ArrayBuffer 对象;异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常;「示例」 const buffer = new ArrayBuffer(32); buffer.byteLength; // 32 const v = new Int32Array(buffer); ArrayBuffer.isView(v) // true const buffer2 = buffer.slice(0, 1); 上面代码表示实例对象 buffer 占用 32 个字节。 它有实例属性 byteLength ,表示当前实例占用的内存字节长度。 它拥有一个静态方法isView(),这个方法可以用来判断是否为TypedArray实例或DataView实例。 它拥有实例方法 slice(),用来复制一部分内存,使用方式同数组的slice方法。 除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。 二、视图2.1 TypedArray TypedArray一共包含九种类型,每一种都是一个构造函数。(DataView视图不支持Uint8ClampedArray,其他都支持) 名称描述长度(字节)Int8Array8位有符号整数1Uint8Array8位无符号整数1Uint8ClampedArray8位无符号整型固定数组(数值在0~255之间)1Int16Array16位有符号整数2Uint16Array16位无符号整数2Int32Array32位有符号整数4Uint32Array32 位无符号整数4Float32Array32 位 IEEE 浮点数4Float64Array64 位 IEEE 浮点数8 每一种视图都有一个BYTES_PER_ELEMENT常数,表示这种数据类型占据的字节数。 Int8Array.BYTES_PER_ELEMENT // 1 Uint8Array.BYTES_PER_ELEMENT // 1 Int16Array.BYTES_PER_ELEMENT // 2 Uint16Array.BYTES_PER_ELEMENT // 2 Int32Array.BYTES_PER_ELEMENT // 4 Uint32Array.BYTES_PER_ELEMENT // 4 Float32Array.BYTES_PER_ELEMENT // 4 Float64Array.BYTES_PER_ELEMENT // 8 这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,普通数组的操作方法和属性,对TypedArray 数组完全适用。 普通数组与 TypedArray 数组的差异主要在以下方面: [图片] TypedArray和Array之间也可以互相转换 const typedArray = new Uint8Array([1, 2, 3, 4]); const normalArray = Array.apply([], typedArray); 「建立TypedArray视图」 // 创建一个8字节的ArrayBuffer const a = new ArrayBuffer(8); // 创建一个指向a的Int32视图,开始于字节0,直到缓冲区的末尾 const a1 = new Int32Array(a); // 创建一个指向a的Uint8视图,开始于字节4,直到缓冲区的末尾 const a2 = new Uint8Array(a, 4); // 创建一个指向a的Int16视图,开始于字节4,长度为2 const a3 = new Int16Array(a, 4, 2); 上面代码在一段长度为 8 个字节的内存(a)之上,生成了三个视图:a1、a2和a3。 视图的构造函数可以接受三个参数: 第一个参数(必选):视图对应的底层ArrayBuffer对象;第二个参数:视图开始的字节序号,默认从 0 开始;第三个参数:视图包含的数据个数,默认直到本段内存区域结束; 建立了视图以后,就可以进行各种操作了。这里需要明确的是,视图其实就是普通数组,语法完全没有什么不同,只不过它直接针对内存进行操作,而且每个成员都有确定的数据类型。所以,视图就被叫做“类型化数组”。 「TypedArray视图操作」 const buffer = new ArrayBuffer(8); const int16View = new Int16Array(buffer); for (let i = 0; i < int16View.length; i++) { int16View[i] = i * 2; } console.log(int16View) // [0, 2, 4, 6] 上面代码生成一个8字节的ArrayBuffer对象,然后在它的基础上,建立了一个16位整数的视图。由于每个字节占据8位,那么16位就占据了2个字节(1个字节等于8位),所以一共可以写入4个整数,依次为0,2,4,6。 如果在这段数据上接着建立一个8位整数的视图,则可以读出完全不一样的结果。 const int8View = new Int8Array(buffer); for (let i = 0; i < int8View.length; i++) { int8View[i] = i; } console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0] 首先整个ArrayBuffer对象会被分成8段。然后,由于x86体系的计算机都采用小端字节序(具体概念理解请自主查询),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。还可以看到下面这个例子 const buffer = new ArrayBuffer(4); const v1 = new Uint8Array(buffer); v1[0] = 10; v1[1] = 3; v1[2] = 11; v1[3] = 8; console.log(v1) // [10, 3, 11, 8] const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8] console.log(uInt16View) // 计算机采用小端字节序 [0x030a, 0x080b] => [778, 2059] 如果一段数据是大端字节序(大端字节序主要用于数据传输),TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序。 2.2 DataView DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。 ❝ 字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。例如假设上述变量 x 类型为int,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..。❝「语法」 new DataView(buffer [, byteOffset [, byteLength]]) 相关的参数说明如下: buffer:ArrayBuffer 对象 或 SharedArrayBuffer 对象;byteOffset(可选):此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始;异常:此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度;「示例」 const buffer = new ArrayBuffer(16); const view = new DataView(buffer, 0); view.setInt8(1, 68); view.getInt8(1); // 68 如果一次操作(get或者set)两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。DataView的操作方法默认使用大端字节序解读数据,如果需要使用小端字节序解读,必须在操作方法中指定参数为true(get方法的第二个参数和set方法的第三个参数)。 const buffer = new ArrayBuffer(24); const dv = new DataView(buffer); // 1个字节,默认大端字节序 const v1 = dv.getUint8(0); // 小端字节序 const v1 = dv.getUint16(1, true); // 大端字节序 const v2 = dv.getUint16(3, false); // 在第5个字节,以小端字节序写入值为11的32位整数 dv.setInt32(4, 11, true); 对于直接处理ArrayBuffer对象的业务场景不是特别多,特别是写页面比较多的同学。笔者深刻认识并运用的场景,主要是在处理比较复杂且数据量比较大的点云数据,前端接收到的点云数据已经是原始采集数据转换过的二进制数据,前端需要对二进制数据进行解析,运用的解析方法就是上述提到的各种方法。下面介绍一下业务场景中比较常见到的一种二进制表示类型——Blob。 三、Blob3.1 基本介绍 Blob 对象比较常用于文件上传、文件读写操作等。在对文件读写的时候,我们更多的时候只是操作File对象,而File继承了所有Blob的属性。所以在我们看来,File对象可以看作一种特殊的Blob对象。 而Blob 对象与 ArrayBuffer 的区别在于,Blob 对象用于操作二进制文件, ArrayBuffer 用于直接操作内存,所以他们有如下图的关系: [图片] 「语法」 const blob = new Blob(array [, options]); 相关的参数说明如下: array:字符串或二进制对象,表示新生成的Blob实例对象的内容;options(可选):比较常用的属性 type,表示数据的 MIME 类型,默认空字符串;「示例」 const array = ['Hello World! ']; const blob = new Blob(array, {type : 'text/html'}); 「属性和方法」 [图片] 由上图可以看到,Blob对象拥有size和type两个属性,以及多种自有方法。比较常用的方法slice、arrayBuffer等;slice方法主要用来拷贝原来的数据,返回的也是一个Blob实例,这个方法可以用来做切片上传。arrayBuffer方法返回一个 Promise 对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。 const blob = new Blob([]); blob.slice(0, 1); blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/); 3.2 运用场景通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。 Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。 下面是一个Blob的例子,可以看到它很短 blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议。 「文件下载」 [图片] 「图片显示」 [图片] 「切片上传」 [图片] 「本地文件读取」 [图片]
2021-05-10 - 专治按钮效果不明显(扩散动画效果)
效果 [图片] 需求 背景 由于最近自家小程序用户活跃用户下滑,老板看看自家小程序,发现分享按钮不够明显,于是乎有了下面这段对话。 老板:小明,你过来下,看看这个分享按钮不明显 小明:好的,给它点颜色瞧瞧 小明给按钮来了个红色,发给了BOSS BOSS:还是不明显 小明:好的,给它点放大瞧瞧 小明把按钮从原来的60rpx放大到了230rpx,发给了BOSS BOSS:还是不明显 小明:好的,让它动起来! 需求:提高分享率,做个扩散动画效果让这个按钮成为整个页面最靓的仔。 思路分析: 从小到大的变化 从颜色从深到浅 反复进行该动作 动画代码 实用 CSS3 的 animation 属性 代码 [代码].share-btn { width: 200rpx; height: 200rpx; } .share-btn::before { // 省略无关代码 animation: wave 1.5s ease-out infinite; } @keyframes wave { 50%, 75% { width: 230rpx; height: 230rpx; } 80%, 100% { opacity: 0; } } [代码] 分析 我们先来看看 animation 参数: animation: wave 1.5s ease-out infinite; animation: 关键帧名称 动画时长 动画形式 播放次数; ease-out:动画以低速结束 infinite:无限次播放 从参数可以得出来使用了wave这个关键帧参数,1.5完成一次扩散的动画,从快到满的速度,无限重复这个动画。 然后我们再来看看 keyframes 参数: 百分比:动画持续时间的百分比。 属性:CSS样式属性 从参数可以得出来 时间 50%->75% 的时候就会改变大小从200rpx-230rpx。 时间 80%->100% 的时候会改变透明度从0-1。 第一步:原始大小(高度:200,宽度:200,透明度:1) [图片] 第二步:改变大小(高度:230,宽度:230,透明度:1) [图片] 第三步:改变透明度(高度:230,宽度:230,透明度:0) [图片] 第四步:回到第一步 总结 做动画先分析步骤,然后设置 animation 参数。如果你觉得CSS比较麻烦的话,还可以使用小程序提供 Animation 对象实现。 css3 的 animation 的所有参数不局限以上这些,了解更多点击传送门 Animation 对象,了解更多点击传送门
2020-08-17 - H5跳转微信小程序遇到的bug,希望能对大家有帮助
历经百般折磨终于解决了这些bug 官方文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html 首先,需要在公众号配置js接口安全域名,一定要记得配置!! 然后,需要引入JS文件,官方文档里有具体链接,(ps: 不过这边我遇到个坑,如果是直接通过在线引入的话,放到线上后会显示未引入,所以我把他直接拉下来了) 接着最让我头疼的地方来了,就是通过config接口注入权限验证配置的时候 wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 jsApiList: [], // 必填,需要使用的JS接口列表 openTagList: [] // 可选,需要使用的开放标签列表,例如['wx-open-launch-app'] }) 下面这四个参数都是后端返回来的 [图片] 注意:当你请求后端获取这些参数时,需要传一个url过去,你需要获取到当前页面的url,方法 window.location.href.split('#')[0];,一定要确保你传过去的url没有错。 配置完后,你如果开启了调试模式,那么说明你config接口注入权限验证配置成功了 [图片] 如果签名参数出现问题,就会报签名错误,这个错误有可能是后端接收你传的url没有进行处理,一定要跟后端对比一下传入的url,如果url没有错误,有可能是你的没有在公众号配置js接口安全域名,还有就是可能后端这边access_token失效了。 [图片] 还有就是按钮样式失效的问题 这个是微信官方提供的demo [图片] 如果是vue项目建议使用script [图片] 但是不知道为什么我的用两种方式都不行,最后我是加了template,在开放按钮wx-open-launch-weapp上,以及里面的button上写了行内样式然后就生效了,可能是我的css使用了rem单位的原因 以上是我所遇到的问题,尤其是在签名这个地方,百般折磨,发布这篇文章就是希望有伙伴遇到这些问题能够有所帮助。
2021-05-06 - 小程序流量主运营之ECPM怎么提高
目前小程序流量主共有8个广告类型:banner、视频、插屏、前贴、激励、格子、模板、开屏。 首先说下ECPM是什么意思。ECPM指的是每一千次展示可以获得的广告收入。 ECPM是取决于用户点击广告次数,也就是1000次曝光里面点击广告的人数越多,ECPM值越高,不单单是曝光1000次的收益。 也就是说如果你曝光了1000次banner广告,但是没有用户点击,那么你banner广告位的ECPM就是0。 [图片] 然后我们说下今天的主题,ECPM怎么提高。 1、首先你流量主小程序里面的所有广告,都是广告主去投放的。广告主可投放的类型有很多种,根据广告主投放城市、行业等决定。 1-1、比如我在呼和浩特,我把广告投放到呼和浩特,如果我按竞价CPC的投放,一个点击最低1.06,除了微信要扣除流量主收益的50%之外,剩下的50%就是你的收益。 [图片] 1-2、我按合约广告投放,1000次曝光我花费的是15元,微信扣除50%,你的ECPM就是7.5,这么说懂了吧。 [图片] 1-3、所以基于广告主广告投放,你要选好广告,尽量避免第二条里面的1000次曝光这种CPM广告主,这种广告主一般都是大品牌,不在乎你点不点,就是让你看。所以我们要有犀利的眼光去判断屏蔽哪种广告位。 [图片] 2、ECPM的高低取决于你的小程序用户分布年龄 比如30岁IOS用户就是比20岁的IOS用户质量高,30岁左右的IOS用户属于高价值用户,对电商、游戏等付费能力会更强,会比20岁的人群要高,所以我们在开发小程序的时候,尽量往这个年龄段靠。还有30岁的女性电商用户,都是ECPM拉高点的关键用户群体。(此处仅仅是举例分析,无年龄歧视) 所以我们再开发、购买小程序时,选题材、素材的时候可以尽量往这个年龄段靠一下,ECPM会高一些。 3、广告设计体验 广告设计是一门艺术,但是不能违规。要优雅的设计广告,让用户看着舒服,看的清楚,主动去点击广告,这也是广告主投放广告的目的。最近有多人私信和我说买1000多个手机,弄1000多个微信去刷广告。真的是想瞎了心了,如果你是广告主,你投放出去的广告希望用户这样点击吗?再说了,微信广告的风控机制是你承受不住的,微信也得保护广告主投放的利益。如果全部都是这种样点广告,微信生态早就活不下去了。不要想着投机取巧,踏踏实实做流量才是王道。 [图片] 4、站外引流提升ECPM 所有的APP都缺流量,不要以为微信是国民产品就不缺流量。微信小程序也喜欢站外流量引进来,所以大家在引流的时候,可以从其他APP引流,这样你的ECPM也是很高。 [图片] [图片]
2020-12-19 - 网页读取小程序云数据库有使用限制吗?
可以在这里怎么吗
2021-05-04 - 小白入门必看 ‘’微信小程序地图定位开发教程‘’
前言 目前腾讯位置服务提供路线规划、地图选点、地铁图、城市选择器插件四款插件产品,本篇博客主要针对地图选点功能进行实现。 开通腾讯位置服务 1、进入微信公众平台 2、登录进入小程序后台,选择 “开发 - 开发工具 - 腾讯位置服务” [图片] 3、点击 “开通”,进入授权扫码界面 [图片] 4、使用微信扫码进行授权 [图片] 5、绑定开发者账号 [图片] 接入插件 1、在小程序后台,选择 “设置 - 第三方设置 - 插件管理”,点击 “添加插件” [图片] 2、搜索 “腾讯位置服务地图选点” 进行添加 [图片] 开发者密钥配置 1、申请开发者密钥 2、设置KEY的 “启用产品” a、勾选微信小程序,设置授权 APP ID [图片] 授权 APP ID 可以通过 “设置 - 基本设置” 的账号信息进行查看 [图片] b、勾选 “WebService API” [图片] 小程序插件需要使用WebService API的部分服务,所以需要给使用该功能的KEY配置相应权限。 如果填写了域名白名单,需要把[代码]servicewechat.com[代码]域名添加进域名白名单中,否则小程序下将无法正常使用WebServiceAPI服务。 插件的使用 1、引入插件 地图选点appId: wx76a9a06e5b4e693e [代码]// app.json { "plugins": { "chooseLocation": { "version": "1.0.5", "provider": "wx76a9a06e5b4e693e" } } } [代码] 2、设置定位授权 地图选点插件需要小程序提供定位授权才能够正常使用定位功能 [代码]// app.json { "permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序定位" } } } [代码] 3、代码实现 a、js代码 [代码]"use strict"; const chooseLocation = requirePlugin('chooseLocation'); Page({ data: { address: "", locationName: "" }, onShow: function () { // 从地图选点插件返回后,在页面的onShow生命周期函数中能够调用插件接口,取得选点结果对象 // 如果点击确认选点按钮,则返回选点结果对象,否则返回null const location = chooseLocation.getLocation(); if(location){ this.setData({ address: location.address?location.address : "", locationName: location.name?location.name : "" }); } }, //显示地图 showMap() { //使用在腾讯位置服务申请的key(必填) const key = ""; //调用插件的app的名称(必填) const referer = ""; wx.navigateTo({ url: 'plugin://chooseLocation/index?key=' + key + '&referer=' + referer }); } }); [代码] [代码]plugin://chooseLocation/index[代码] 接口参数说明: [图片] b、wxml代码 [代码]<!--index.wxml--> <view class="container"> <button bindtap="showMap">选择位置</button> <view style="margin-top:10px">地址:{{address?address:"暂无"}}</view> <view style="margin-top:10px">名称:{{locationName?locationName:"暂无"}}</view> </view> [代码] 4、效果实现 [图片] 作者:盛夏温暖流年 链接:https://blog.csdn.net/j1231230/article/details/112352787 来源:CSDN 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2021-04-30 - 关于textarea/input层级太高,有内样式或元素对不齐,问题解决
1、如果是textarea或者input层级太高 解决方法:给标签加上always-embed属性(基础库需要2.10.4以上)[图片] 如果兼容低版本基础库,需要使用cover-view写样式覆盖(标签文档:https://developers.weixin.qq.com/miniprogram/dev/component/cover-view.html) 2、如果发现textarea和其它元素需要一排对齐,但是ios有内样式,无法和安卓一样对齐 解决方法:给标签加上disable-default-padding属性(基础库需要2.10.0以上) [图片] 如果需要兼容低版本基础库,需要判断当前是否为ios系统,如果是不加任何padding,否则给textarea加上padding样式,padding: 18rpx 10rpx具体可以根据真机观察给大小。 写文章不容易,有帮助希望点下赞,程序员不难为程序员。
2021-04-27 - css 毛玻璃效果
[图片] css width: 100rpx; height: 130rpx; background: linear-gradient(to right bottom, rgba(44,44,69,0.6), rgba(44,44,69,0.3), rgba(44,44,69,0.2)); backdrop-filter: blur(9px); border-top: 2rpx solid rgba(44,44,69,0.8); border-left: 2rpx solid rgba(44,44,69,0.8); border-radius: 30rpx; padding: 20rpx; box-shadow: 0 9px 15px 0 rgba(25,21,40,0.39),0 -9px 15px 0 rgba(25,21,40,0.39);
2021-04-26 - wx.getUserProfile is not a function ?
用了getUserProfile以后,经常有报错提醒 [图片]
2021-04-19 - 05.适配 wx.getUserProfile 的一点简单想法
先看官方的最新通知 周知:getUserInfo 开发版和体验版 已对齐 getUserInfo 匿名表现,正式版将于 4月13日 正式对齐 getUserInfo 匿名表现。 请开发者使用 getUserProfile 获取用户信息。 小程序登录、用户信息相关接口调整说明:https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801 原先的getUserInfo能力 原先的 getUserInfo 的能力,具体看这里:https://developers.weixin.qq.com/community/develop/doc/000c2424654c40bd9c960e71e5b009?highline=getUserInfo [图片] [图片] 我对 getUserInfo 的理解 我们一般开发者,会用 wx.getUserInfo 来实现 openId 和 用户信息的获取,是非常的便捷的。 并且还能通过 wx.getSetting 获取 "scope.userInfo" 判断用户用户是否已经授权。 wx.getSetting + wx.getUserInfo 能近似完美解决用户登陆授权场景 华丽的分割线 2021-02-04 官方要对小程序登录、用户信息相关接口调整说,具体链接:https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801?blockType=1 这样我们开发者会面临的用户登陆授权的调整,会比较痛,但也必须要去改,因为没办法。 怎么改呢 先不要着急去适配 getUserProfile,先要弄清楚自己的小程序用 getUserInfo 获取那些能力?实现哪些能力? 我的理解,如果是用 wx.getSetting + wx.getUserInfo 来实现获取用户的openId(unioinId) 和用户头像昵称信息的话,可以这样去调整? wx.login 可以拿到用户的 openId,流程如下: [图片] 如果绑定了开发平台,还可以通过 wx.login 静默获取用户的 uninonId [图片] 这个过程是静默的,不需要用户参与。 然后对于需要用户头像信息的时候,在使用 wx.getUserProfile 能力即可。 小结 1.wx.login + wx.getUserProfile 能实现 wx.getUserInfo 的能力,也能满足我们的业务场景。 2.看到官方的调整,先不要着急去调整,先弄清楚要我们的业务场景使用了那些API获取那些能力 3.关于wx.getSetting 返回 "scope.userInfo" 为 undefined。我记得社区有人提问了,等官方回复就好,其实我们可以不依赖这个,原因留给你 4.用户头像更新不及时的问题,我们看看微信聊天记录,当你好友更新头像,当你不点击时,有时候展示的还是老头像,当你点击时,会显示新头像(我们何不借鉴一下,提供给用户更新的能力即可)
2021-04-09 - 小程序被恶意攻击导致数据库和云函数无法访问?
APPID: wxe1087aa998dbd385 小程序刚上线两周,基本上没有用户。 早上8点23分,消息提醒数据读取使用量达到417次,然后持续提醒且读取数量增加很快,直到读取次数达到上限。 接着10点19分,消息提醒数据写入使用量达到279次,还在持续提醒和增加,直到数据写方向也达到上限。 目前小程序已无法正常使用,开发工具也无法查看数据内容是否安全(因为读取使用量达上限了,但这个上限为什么要计算开发工具的使用次数呢?) 在这里有几个问题想问一下懂的朋友或者官方: 1、对于这种存在恶意攻击行为的用户,开发者有没有什么方式可以封掉用户的访问权限; 2、官方提供云开发是方便且降低了开发者门槛,但防攻击机制如何? 3、对于这种问题开发者如何能够快速处理,避免损失呢?
2021-04-11 - 云开发批量上传图片,上传完图片再上传数据库 [即抄即用,拎包入住]
大家好,又是我拎包哥,今天我们来实现在云开发中批量上传图片。 经过Stephen哥的指正,我改用了Promise.all的方法来达到目的。 Promise.all的作用就是等待所包含的promise函数结束后再执行下一步逻辑,非常方便好用!const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, btn() { let promiseMethod = new Array(this.TFP.length) for (let i = 0; i < this.TFP.length; i++) { promiseMethod[i] = wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } Promise.all([...promiseMethod]).then(() => { test.add({ data: { imgList: this.imgList } }) }) } }) --------------------------------------我是分割线-------------------------------------- async await 要点: ctrl c + ctrl v这里用了await阻塞在wx.cloud.uploadFile前面,避免还没上传完图片就往数据库插入数组。减少了then里的代码,美观逼格高。嘻嘻嘻。await wx.cloud.uploadFile不能放在wx.chooseImage里,如果可以的话,请告诉我怎么做,谢谢!欢迎交流,指出错误,我立刻修改么么哒。 标准版 const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, async btn() { this.imgList = [] console.log(this.TFP) for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) 新手最爱一锅炖版(不推荐) 为什么不推荐呢,因为选择图片并不意味着要上传图片,用户还没进行最终的确定操作(不过可以用来了解async await)。 onLoad() { this.imgList = [] wx.chooseImage({ success: async res => { this.TFP = res.tempFilePaths for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) } [图片] ==========================end==========================
2020-05-17 - 无偿资助2021高校微信小程序应用开发赛
参与【2021高校微信小程序应用开发赛】的同学们,如果你们有小程序功能限制等问题,本公司无偿提供资助(企业主体) 后续自己有企业或者其他渠道需要迁移小程序的,本公司全力配合。 前提是:遵循《微信2021高校微信小程序应用开发赛》全部规则。 [图片] 有需要的话,社区私信我。 主页链接: https://developers.weixin.qq.com/community/personal/oCJUsw1gRm1L6jLgiSi9cQtOERTU?from=1002 [图片]
2021-04-05 - add添加数据能添加数组嘛?
[图片] [图片] [图片] 但是却无法在数据库添加成功 [图片] 所以只能用update语句插入数组吗
2021-04-02 - 如何使用云函数随机获取数据库的一条数据并显示在页面?
[图片][图片]这是10条数据,我想实现随机获取一个句子并显示,也就是每日一句的推荐。
2021-03-27 - 求助:小程序管理端如何统计小程序使用记录,比如登录用户名、操作时间、操作页面?非常感谢!
求助专家,小程序管理端目前只有用户数等统计类的指标,如何能统计小程序使用记录,比如登录用户名、操作时间、操作页面?非常感谢!
2021-03-26 - 小程序怎么支付到个人零钱?
体验码(已上线,非具体需求用户请勿支付,如误操作,请联系客服商家动态微信退款) [图片] 下面看下演示 [图片] 开通步骤: 1、首先你得有个营业执照 2、打开微信-我-支付-收付款-二维码收款-收款小账本(或者直接搜索小程序:收款小账本) [图片] 3、补充经营信息,开启商家服务,需要上传营业执照 4、审核通过后,继续收款小账本首页,下单助手、商家动态等去完善店铺信息 5、信息完善后点击【收款小账本】头部店铺名称,然后再点击商家小程序即可打开小商店助手,然后进入我的小程序,就能打开啦 6、剩下的就是店铺活动,各种信息完善了 Tips:此小程序名称格式必须是是【主营业务|地理区域】,然后店铺名称可被全网搜索到
2020-11-22 - 云服务器小程序移植到云开发吗?
云服务器开发的小程序可移植到云开发吗?需要怎么操作。
2021-03-25 - 我可不可以这样做?
我是做会员小程序平台的,我可不可以这样,在扫码进去小程序,积分=钱,然后付款时减去上次积分,在小程序里直接付款到商家的个人微信钱包里。或者有别的好方案,有懂的回答下,万分感谢 [图片]
2021-03-25 - canvas 2d 绘图 代码片段
这是关于如何在小程序开发中 如何使用 canvas 2d 来绘制 海报 的简单的 代码片段 留痕 做记录 https://developers.weixin.qq.com/s/BrjeN2mH74po
2021-03-24 - 求助A环境const db = wx.cloud.database()访问B环境的云数据库如何实现?
我在一个企业下,申请了两个微信appid,分别叫A环境和B环境, 前提是B环境已授权A环境内访问, 现在需要在A环境下,通过云环境共享授权,访问B环境的云数据库 const db = wx.cloud.database({env:'B环境ID'}) 此处,我填写B环境ID,报环境不存在,我填写A环境ID,又报没有数据库,此处该如何配置?
2021-03-21 - 微信小程序转发朋友圈详解
概述点击右上角分享朋友圈[图片] 分享到朋友圈样式[图片] 朋友圈打开样式[图片] 这个功能目前只支持Android(在IOS高版本微信支持朋友圈打开小程序能力,但不能分享)。 用户打开朋友圈分享的小程序,看到不是真正的小程序,而是原本页面的“单页模式”。 什么是“单页模式”?以下是微信官方对于“单页模式”的描述: “单页模式”下,页面顶部固定有导航栏,标题显示为当前页面 JSON 配置的标题。底部固定有操作栏,点击操作栏的“前往小程序”可打开小程序的当前页面。顶部导航栏与底部操作栏均不支持自定义样式。“单页模式”默认运行的是小程序页面内容,但由于页面固定有顶部导航栏与底部操作栏,很可能会影响小程序页面的布局。因此,请开发者特别注意适配“单页模式”的页面交互,以实现流畅完整的交互体验。限制另外,“单页模式”存在着很多限制。以下是官方给出的禁用能力列表: [图片] 限制主要包括以下几点: 页面无登录态,与登录相关的接口,如 [代码]wx.login[代码] 均不可用不允许跳转到其它页面,包括任何跳小程序页面、跳其它小程序、跳微信原生页面若页面包含 tabBar,tabBar 不会渲染,包括自定义 tabBar本地存储与小程序普通模式不共用这些限制,让“单页模式”只适用于内容展示,不适用于有较多交互。 配置针对“单页模式”,新增了单页模式相关配置。目前这个配置里只有一个navigationBarFit属性: [图片] navigationBarFit属性主要是针对原页面设置了自定义导航栏的情况。也就是原页面的json文件中配置了这个属性: { // ... "navigationStyle":"custom" // ... } 给大家看一下普通导航栏和自定义导航栏的区别,下图是普通导航栏页面: [图片] 下图是自定义导航栏页面,我们在原本的导航栏位置使用了banner: [图片] [代码]"navigationStyle":"custom"[代码]这个设置在“单页模式”下也会生效。前文微信官方对“单页模式”的描述有说到“顶部导航栏与底部操作栏均不支持自定义样式”。如果我们在原页面设置了自定义导航栏。那么“单页模式”样式就会变成这样: [图片] 通过设置navigationBarFit为 [代码]squeezed[代码]就可以解决这个问题: { // ... "singlePage": { "navigationBarFit": "squeezed" } // ... } 设置后的样式: [图片] 开发 接下来介绍如何在小程序中实现这个功能。 第一步在需要转发朋友圈的页面中注册用户点击右上角转发功能,这是实现转发朋友圈功能的必要满足条件。 onShareAppMessage: function () { return { title: '转发标题', path: '/pages/home/index', imageUrl: '自定义图片路径' } } 第二步注册分享朋友圈功能(从基础库 [代码]2.11.3[代码] 开始支持): onShareTimeline: function () { return { title: '转发标题', query: 'from=pyq', imageUrl: '自定义图片路径' } } 注意,这里有个问题,分享朋友圈功能不支持自定义页面路径,意味着只能转发当前页面。如果当前页面存在较多“单页模式”限制功能,就可能让我们的页面不能按预期展示。 当页面存在限制功能时,我们存在两个方案,第一个方案,针对“单页模式”做改动,不调用那些限制的功能。第二个方案,另外写一个针对“单页模式”的页面。 这两种方案都需要能判断当前是否正处在小程序“单页模式”。 我们通过判断场景值(场景值用来描述用户进入小程序的路径)是否等于 1154 来判断当前是否正处在小程序“单页模式”。场景值可以在 [代码]App[代码] 的 [代码]onLaunch[代码] 获取。 // app.js App({ // ... onLaunch(options) { const { scene } = options; this.isSinglePage = scene === 1154; } // ... }) 我们将是否正处在“单页模式”的Boolean值放入App实例,方便全局拿到值。 接下来说说两种方案。 第一种方案,在“单页模式”不调用那些限制功能(这是一种不推荐的方案,代码耦合性太强)。举个例子: const app = getApp(); Page({ // ... onLoad() { if (!app.isSinglePage) { wx.login({ // ... }) } } // ... }) 第二种方案,针对“单页模式”另写一个页面。因为分享朋友圈功能并不支持自定义页面路径,我们只能另外写一个组件来作为“单页模式”的内容承载。 将isSinglePage放入页面的初始数据,方便在wxml中拿到: // pages/home/index.js const app = getApp(); Page({ data: { isSinglePage: app.isSinglePage, } // ... }) home-single-page就是分享到朋友圈的内容承载组件: // pages/home/index.json { // ... "usingComponents": { "home-single-page": "components/home-single-page/index" }, } 当“单页模式”时,我们展示 [代码]home-single-page[代码]组件,否则就展示普通页面内容: // pages/home/index.wxml 样式上虽然搞定了,但是在原本的生命周期中可能会调用一些限制功能,或者跑一些其它“单页模式”用不上的内容。我们得停止原本生命周期函数调用。 建议对传入Page的对象进行统一处理,当“单页模式”时,不调用原本的生命周期: // pages/home/index.js import ExtendPage from 'common/extend-page/index' const app = getApp(); ExtendPage({ data: { isSinglePage: app.isSinglePage, } // ... }) ExtendPage函数针对“单页模式”进行统一处理: // common/extend-page/index.js const app = getApp(); const PAGE_LIFE = [ 'onLoad', 'onReady', 'onShow', 'onHide', 'onError', 'onUnload', 'onResize', 'onPullDownRefresh', 'onReachBottom', 'onPageScroll' ]; export default function(option) { let newOption = {}; if(app.isSinglePage) { newOption = PAGE_LIFE.reduce((res, lifeKey) => { if (option[lifeKey]) { res[lifeKey] = undefined; } return res; }, {}) } return Page({ ...option, ...newOption, }); } 在“单页模式”下,我们将原本的生命周期都停止了调用。这样就能很好的将“单页模式”下的页面和普通页面进行解耦。 如果”单页模式“页面比较复杂,需要使用生命周期。我们也可以添加 [代码]singlePageLife[代码]属性,当处在“单页模式”下,就调用 [代码]singlePageLife[代码]内的生命周期: // pages/home/index.js import ExtendPage from 'common/extend-page/index' const app = getApp(); ExtendPage({ data: { isSinglePage: app.isSinglePage, }, singlePageLife: { onLoad() { // ... }, } // ... }) // common/extend-page/index.js const app = getApp(); const PAGE_LIFE = [ 'onLoad', 'onReady', 'onShow', 'onHide', 'onError', 'onUnload', 'onResize', 'onPullDownRefresh', 'onReachBottom', 'onPageScroll' ]; export default function(option) { let newOption = {}; if(app.isSinglePage) { const { singlePageLife } = option; newOption = PAGE_LIFE.reduce((res, lifeKey) => { if (singlePageLife[lifeKey]) { res[lifeKey] = singlePageLife[lifeKey]; } else if(option[lifeKey]) { res[lifeKey] = undefined; } return res; }, {}) } return Page({ ...option, ...newOption, }); } 文章如有疏漏、错误欢迎批评指正。
2020-10-21 - appid更换后其他东西会被影响吗?
我打算把自己的小程序(云开发)从个人主体转换为企业主体。需要重新注册一个小程序号。我是不是在开发者工具里将现有的appid改成新的就好了。云开发的内容会受到影响吗。谢谢
2021-03-20 - 关于小程序切换不同环境的学习总结
关于小程序切换不同环境的学习总结 ~ 今天看到掘金上一篇介绍切换小程序环境的文章收获非常大,总结下,以下文字摘录自原文,并做部分整理 微信小程序目前为止还没有提供API或者具体的配置方式,给我们设置环境变量,所以还得自己想办法。 大概的想法就是设置一个配置文件([代码]/utils/baseData.js[代码]),在需要的地方引入,然后在不同的情况下用node处理配置文件。 具体步骤如下: 设置一个baseData.js文件,在需要的地方引入 这样就完成了第一步,把配置文件引入到项目中 在根目录新建[代码]/env[代码]文件夹 在[代码]/env[代码]文件夹下创建[代码]dev.json[代码]和[代码]prod.json[代码]两个json文件,分别用于存放开发环境和生产环境的常量。 在package.json的script下新增两条命令,用来启动不同的环境 根据命令行参数,选择[代码]/env[代码]下的对应的配置文件 在根目录新建[代码]switch.js[代码]文件,利用node处理配置信息。 * 根据命令行运行参数,修改/config.js 里面的项目配置信息, */ const fs = require('fs') const path = require('path') //源文件 const sourceFiles = { prefix: '/env/', dev: 'dev.json', prod: 'prod.json' } //目标文件 const targetFiles = [{ prefix: '/utils/', filename: 'baseData.js' }] const preText = 'module.exports = ' // 获取命令行参数 const cliArgs = process.argv.splice(2) const env = cliArgs[0] // 判断是否是 prod 环境 const isProd = env.indexOf('prod') > -1 ? true : false // 根据不同环境选择不同的源文件 const sourceFile = isProd ? sourceFiles.prod : sourceFiles.dev // 根据不同环境处理数据 fs.readFile(__dirname + sourceFiles.prefix + sourceFile, (err, data) => { if (err) { throw new Error(`Error occurs when reading file ${sourceFile}.nError detail: ${err}`) process.exit(1) } // 获取源文件中的内容 const targetConfig = JSON.parse(data) // 将获取的内容写入到目标文件中 targetFiles.forEach(function(item, index) { let result = null if (item.filename === 'baseData.js') { result = preText + JSON.stringify(targetConfig, null, 2) } console.log(result) // 写入文件(这里只做简单的强制替换整个文件的内容) fs.writeFile(__dirname + item.prefix + item.filename, result, 'utf8', (err) => { if (err) { throw new Error(`error occurs when reading file ${sourceFile}. Error detail: ${err}`) process.exit(1) } }) }) }) 当我们执行不同的命令时,[代码]switch.js[代码]会动态的将相应的环境配置写入配置文件([代码]/utils/baseData.js[代码])中。所以到这里基本完成我们的目标了。但每次切换环境都要运行npm脚本,也不是很方便。 所以最后一步很关键,就是配置我们的开发者工具,让它在预览和上传等操作前自动执行对应的npm脚本。 在开发者工具本地设置中,可以勾选启用自定义处理命令,如下图 小结: 这样之后,基本就实现了不同环境,使用不同的变量。每次只要更新对应环境的[代码]json[代码]文件,可以说是很方便了。 如需了解详情,可以移步掘金 https://juejin.cn/post/6939778o6439796o6543
2021-03-19 - 微信扫描二维码携带参数跳转小程序问题?
通过微信扫一扫,扫描普通二维码跳转到小程序后,在小程序内部再次扫描该二维码或者性质相同的二维码(二维码绑定商品参数和打开小程序的配置)会重新跳转小程序吗?会在退出小程序重新进入吗?如果有的话,有解决办法吗?万分感谢!!!
2021-03-18 - 公众号开发后台(比如关键字回复)能否使用小程序的云开发?
公众号开发后台(比如关键字回复)能否使用小程序的云开发?
2021-03-16 - 登录系统实现
对于前端来说,登录就是把用户信息提交上去,后续就不用前端去担心了。但是做过一个登陆sdk的项目,发现这里边的逻辑不是那么简单。下面是我对登陆的一些理解分享给大家,感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi session & JWT[代码]http[代码]协议是无状态的,它不能以状态来区分和管理请求和响应。也就是说,如果用户通过账号和密码来进行用户认证后,在下次请求时,用户还需要在再次进行用户认证。因为根据[代码]http[代码]协议,服务端并不知道是哪个用户发起的请求。为了识别当前的用户,服务端与客户端需要约定某个标识表示当前的用户 session为了识别是哪个用户发出的请求,需要在服务端存储一份用户登录的信息,这份登录信息会在响应传递给客户端进行存储,当下次请求的时候客户端会携带登录信息请求服务端,服务端就能够区分请求是哪个用户发起的 下面是示意图: [图片] 在[代码]session[代码]方案中,请求服务端时会携带[代码]session_id[代码],服务端会通过当前的[代码]session_id[代码],去查询数据库当前session是否有效,如果有效后续请求就能够标识当前用户。 如果当前的[代码]session[代码]是无效的或者是不存在的,客户端需要重定向到登录页面,或者提示没有登录 下面是对应的代码: const express = require('express'); const session = require('express-session') const redis = require('redis') const connect = require('connect-redis') const bodyParser = require('body-parser') const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })) const RedisStore = connect(session); const client = redis.createClient({ host: '127.0.0.1', port: 6397 }) app.use(session({ store: new RedisStore({ client, }), secret: 'sec_id', resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 1000 * 60 * 10 } })) app.get('/', (req, res) => { sec = req.session; if (sec.user) { res.json({ user: sec.user }) } else { res.redirect('/login') } }) app.post('/login', (req, res) => { const {pwd, name } = req.body; // 这里为了简便,就写简单点 if (pwd === name) { req.session.user = req.body.name; res.json({ message: 'success' }) } }) 当请求[代码]/[代码]接口的时候,会判断当前[代码]session[代码]是否存在。如果存在,就返回对应的信息;如果不存在,则会重定向到[代码]/login[代码]页面。这个页面登录成功以后,就会设置[代码]session[代码] 上面代码中只考虑了单个服务的场景,但是业务中往往是多个服务,服务域名不一样,由于[代码]cookie[代码]不能跨域,所以[代码]session[代码]的共享会存在一定问题 [图片] 例如有上面场景中,用户首先请求服务[代码]Auth Server[代码],然后生成[代码]session[代码]。当用户再次请求服务[代码]feedback Server[代码]时,由于[代码]session[代码]不共享,就导致服务B拿不到登陆态,就需要重新登录。 session的缺点 [代码]session[代码]用于解决鉴权,存在一些缺点: 多集群支持: 当网站采用集群部署的时候,会遇到多台web服务器之间如何做[代码]session[代码]共享的问题。因为[代码]session[代码]是由单个服务创建,处理请求的服务器可能不是创建[代码]session[代码]的服务器,那么该服务器就无法拿到之前放入到session中的登录凭证之类的信息 性能差: 当流量高峰期时,由于每个请求的用户信息都需要存储在数据库中,对资源会是一种负担 低扩展性:当扩容服务端的时候,[代码]session store[代码]也需要扩容。这会占用额外的资源和增加复杂性 JWT 在[代码]session[代码]服务中,服务器需要维护用户的[代码]session[代码]对象,要么前置一个服务,要么每个服务都从存储层中获取[代码]session[代码]信息,请求量大的时候IO压力大。 相比于[代码]session[代码]服务,把用户信息存放在客户端,每次请求的时候随[代码]cookie[代码]或[代码]http[代码]头部渠道发送到服务器上,就可以让服务器变成无状态的存在,从而减轻服务器的压力。 [图片] 相比于浏览器,[代码]Native App[代码]设置[代码]cookie[代码]没有那么容易,所以服务端需要采用另外一种认证方式。在登录后,服务端会根据登录信息生成一个[代码]token[代码]值,后续的请求客户端请求会携带[代码]token[代码]值进行登录校验。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi [代码]jwt[代码]主要由三部分构成: 头部信息([代码]header[代码])、消息体([代码]payload[代码])和签名([代码]signature[代码]) 头信息指定了[代码]JWT[代码]的签名算法 header = { alg: "HS256", type: "JWT" } [代码]HS256[代码]表示使用了 [代码]HMAC-SHA256[代码] 来生成签名 消息体包含了[代码]JWT[代码]的意图: payload = { "loggedInAs": "admin", "iat": 1422779638 } 未签名的令牌由[代码]base64url[代码]编码的头信息和消息体拼接而成,签名则通过私有的[代码]key[代码]计算而成: key = 'your_key' unsignedToken = encodeBase64(header) + "." + encodeBase64(payload) signature = HAMC-SHA256(key, unsignedToken) 最后在未签名的令牌尾部拼接上[代码]base64url[代码]编码的签名就是JWT了: token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature) 具体实现 首先创建[代码]app.js[代码],用于获取请求参数,还有监听端口等等 // app.js require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser') const cookieParser = require('cookie-parser'); const router = require('./router'); const app = express(); app.use(bodyParser.json()) app.use(cookieParser); app.use(bodyParser.urlencoded({ extended: true })) router(app); app.listen(3001, () => { console.log('server start') }) [代码]dotenv[代码]主要用于配置环境变量,创建[代码].env[代码]文件,下面是本示例的配置: ACCESS_TOKEN_SECRET=swsh23hjddnns ACCESS_TOKEN_LIFE=1200000 然后注册[代码]login[代码]接口,这个接口提交用户信息到[代码]server[代码],后端会用这些信息生成对应的[代码]token[代码],可以直接返回给客户端或者设置[代码]cookie[代码] // user.js const jwt = require('jsonwebtoken') function login(req, res) { const username = req.body.username; const payload = { username, } const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, { algorithm: "HS256", expiresIn: process.env.ACCESS_TOKEN_LIFE }) res.cookie('jwt', accessToken, { secure: true, httpOnly: true, }) res.send(); } 当登录成功以后直接设置客户端的[代码]cookie[代码] 下次请求的时候,服务端直接获取用户的[代码]jwt cookie[代码],判断当前[代码]token[代码]是否是有效的: //middleware.js const jwt = require('jsonwebtoken'); exports.verify = function(req, res, next) { const accessToken = req.cookies.jwt; try { jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET); next(); } catch (error) { console.log(error); return res.status(401).send(); } } 相对于session的方式,jwt具有以下优势: 扩展性好:在分布式部署场景下,session需要数据共享,而jwt不需要 无状态: 不需要在服务端存储任何状态 jwt也存在一些缺点: 无法废弃: 在签发后,在到期之前会始终有效,无法中途废弃。 性能差: session方案中,cookie需要携带的sessionId是一个很短的字符串。但是由于jwt是无状态的,需要携带一些必要的信息,体积会比较大。 安全性:jwt中的payload是base64编码的,没有加密,因此不能存储敏感数据 续签: 传统的cookie续签方案都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。如果要改变jwt的有效时间,就需要签发新的jwt。一种方案是每次请求都更新jwt,这样性能太差了;第二种方案为每个jwt设置过期时间,每次访问刷新jwt的过期时间,就失去了jwt无状态的优势了。 session和jwt的适用场景 适合适用jwt的场景: 有效期短只希望被使用一次 例如在请求服务A的时候,服务A会颁发一个很短过期时间的JWT给浏览器,浏览器可以当前的jwt去请求服务B,服务B则可以通过校验JWT来判断当前用户是否有权操作。 由于jwt具有无法废弃的特性,单点登录和会话管理非常不适合用jwt。 单点登录(SSO) [代码]sso[代码]通常处理的是一个公司的不同应用间的访问登录问题。如企业应用有很多业务子系统,只需要登录一个系统,就可以实现不同子系统间的跳转,而避免了登录操作。 这里举个例子进行说明: 子系统[代码]A[代码]统一到[代码]passport[代码]域名登录,并且在[代码]passport[代码]域名下种上cookie,然后把token加入到url中,重定向到子系统A 回到子系统A后,使用token再次去[代码]passport[代码]验证,如果验证通过返回必要的信息生成系统A的session 当系统A下次请求的时候会当前服务已有[代码]session[代码],不会再去[代码]passport[代码]去权限校验 当访问系统B的时候,由于系统B不存在[代码]session[代码],所以会重定向到[代码]passport[代码]域名,[代码]passport[代码]域名下面已经有cookie了,所以不需要登录,直接把token加入到url中,重定向到子系统B,后续流程和A一样 实现原理 以腾讯为例,腾讯旗下有多个域名,例如: cd.qq.com、tencent.com、jd.cm、music.qq.com 在[代码]cd.qq.com[代码]和[代码]music.qq.com[代码],我们可以设置[代码]cookie[代码]的[代码]domian[代码]为[代码]qq.com[代码]实现[代码]cookie[代码]的共享。 但是如[代码]cd.qq.com[代码]、[代码]tencent.com[代码]二级域名不一致,让所有的域名都能共享一个[代码]cookie[代码]。所以希望有一个通用的服务去承载这个登录服务。例如在腾讯有这样一个域名: [代码]passport.tencent.com[代码]用于专门登录服务的承载。这个时候[代码]cd.qq.com[代码]和[代码]tencent.com[代码]的登录登出都由[代码]sso[代码]([代码]passport.baidu.com[代码])来实现 具体实现 成功登录[代码]SSO[代码]会生成[代码]token[代码]跳转到源页面,此时[代码]SSO[代码]已经有登录状态,但是子系统仍然没有登录态。子系统需要通过[代码]token[代码]设置当前子系统的登录态,并通过当前的[代码]token[代码]请求[代码]passport[代码]服务获取用户的基本信息。 下面主要讲三个部分 [代码]passport[代码]: 登录服务,域名为[代码]passport.com[代码] [代码]system[代码]: 子系统,监听端口[代码]3001[代码]为系统[代码]A[代码],监听端口[代码]3002[代码]为系统[代码]B[代码],域名分别为[代码]a.com[代码]、[代码]b.com[代码] passport服务 [代码]passport[代码]主要有以下几个功能: 统一登录服务获取用户信息校验当前的[代码]token[代码]是否是有效的感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 首先实现登录页面的一些逻辑: // passport.js import express from 'express'; import session from 'express-session'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import connect from 'connect-redis'; import redis from '../redis'; const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser()); app.set('view engine', 'ejs'); app.set('views', `${__dirname}/views`); const RedisStore = connect(session); app.use( session({ store: new RedisStore({ client: redis, }), secret: 'token', resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 1000 * 60 * 10, }, }) ); app.get('/', (req, res) => { const { token } = req.cookies; if (token) { const { from } = req.query; const has_access = await redis.get(token); if (has_access && from) { return res.redirect(`https://${from}?token=${token}`); } // 如果不存在便引导至登录页重新登录 return res.render('index', { query: req.query, }); } return res.render('index', { query: req.query, }); }) app.port('/login', (req, res) => { const { name, pwd, from } = req.body; if (name === pwd) { const token = `${new Date().getTime()}_${ name}`; redis.set(token, name); res.cookie('token', token); if (from) { return res.redirect(`https://${from}?token=${token}`); } } else { console.log('登录失败'); } }) [代码]/[代码]接口首先判断[代码]passport[代码]是否已经有登录成功的[代码]token[代码],如果存在就在去存储中查找当前[代码]token[代码]是否是有效的。如果有效并且参数中携带[代码]from[代码]参数,那么就跳转到原页面并且把生成的[代码]token[代码]值带回到原页面。 下面是[代码]passport[代码]页面的样式: [图片] 登录接口需要做的就是登录成功后设置[代码]passport[代码]域名的[代码]token[代码],然后重定向到之前的页面 子系统实现 import express from 'express'; import axios from 'axios'; import session from 'express-session'; import bodyParser from 'body-parser'; import connect from 'connect-redis'; import cookieParser from 'cookie-parser'; import redisClient from "../redis"; import { argv } from 'yargs'; const app = express(); const RedisStore = connect(session); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser('system')); app.use(session({ store: new RedisStore({ client: redisClient, }), secret: 'system', resave: false, name: 'system_id', saveUninitialized: false, cookie: { httpOnly: true, maxAge: 1000 * 60 * 10 } })) app.get('/', async (req, res) => { const { token } = req.query; const { host } = req.headers; // 如果本站已经存在凭证,便不需要去passport鉴权 if (req.session.user) { return res.send('user success') } // 如果没有本站信息,有没有token,便去passport登录鉴权 if (!token) { return res.redirect(`http://passport.com?from=${host}`) } const {data} = await axios.post('http://127.0.0.1:3000/check',{ token, }) // 验证成功 if (data?.code === 0) { const user = data?.user; req.session.user = user; } else { // 验证失败 return res.redirect(`http://passport.com?from=${host}`) } return res.send('page has token') }) app.listen(argv.port, () => { console.log(argv.port); }) 首先判断当前子系统是否已经登录了,如果当前系统[代码]session[代码]已经存在,就返回[代码]user success[代码]。如果没有登录并且[代码]url[代码]上携带[代码]token[代码]参数,就需要跳转到[代码]passport.com[代码]登录。 如果[代码]token[代码]存在,并且当前子系统没有登录,就需要使用当前页面的[代码]token[代码]去请求[代码]passport[代码]服务,判断这个[代码]token[代码]是否有效的,如果有效就返回相应的信息,并且设置[代码]session[代码]。 这里系统[代码]A[代码]和系统[代码]B[代码]只是监听的接口不同,所以在启动参数中添加变量获取启动端口 passport鉴权服务 app.get('/check', (req, res) => { const { token } = req.query; if (!token) { return res.json({ code: 1 }) } const user = await redis.getAsync(token); if (user) { return res.json({ code: 0, user, }) } else { return res.redirect('passport.com') } }) [代码]check[代码]接口就是判断请求服务的[代码]token[代码]是否是有效的,如果有效就返回对应的用户信息,如果无效就重定向到passport.com重新登录 OAuth [代码]OAuth[代码]协议被广泛应用于第三方授权登录中,借助第三方登录可以让用户规避再次登录的问题。 以[代码]github[代码]授权为例,讲解[代码]OAuth[代码]的授权过程: 访问服务[代码]A[代码],服务[代码]A[代码]没有登录,可以通过[代码]github[代码]第三方登录点击[代码]github[代码],跳转到认证服务器。然后询问是否授权授权完成后,会重定向到服务A的一个路径,并且携带参数[代码]code[代码]服务[代码]A[代码]通过[代码]code[代码]去请求[代码]github[代码],获取到[代码]token[代码]值通过[代码]token[代码]值,再去请求[代码]github[代码]资源服务器获取到你想要的的数据 首先去github-auth申请一个[代码]auth[代码]应用,例如以下: [图片] 执行后会得到对应的[代码]client_id[代码]和[代码]client_secret[代码]。下面是具体的授权代码(启动服务就不写,大同小异): import { AuthorizationCode } from 'simple-oauth2'; const config = { client: { id: 'client_id', secret: 'client_secret' }, auth: { tokenHost: 'https://github.com', tokenPath: '/login/oauth/access_token', authorizePath: '/login/oauth/authorize' } } const client = new AuthorizationCode(config); const authorizationUri = client.authorizeURL({ redirect_uri: 'http://localhost:3000/callback', scope: 'notifications', state: '3(#0/!~' }); app.set('view engine', 'ejs'); app.set('views', `${__dirname}/views`); app.get('/auth', (_, res) => { res.redirect(authorizationUri) }) 上面使用了[代码]simple-oauth2[代码]用于[代码]oauth2[代码]的讲解,当访问[代码]localhost:3000/auth[代码]的时候,服务会自动跳转到[代码]github[代码]的认证地址下面是具体的地址 https://github.com/login/oauth/authorize?response_type=code&client_id=86f4138f17d0c3033ca4&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=notifications&state=3(%230%2F!~ 复制代码 当点击授权后会重定向到[代码]localhost:3000/callback[代码],并且[代码]url[代码]上携带参数[代码]code[代码]。下面是服务端的处理函数 async function getUserInfo(token) { const res = await axios({ method: 'GET', url: 'https://api.github.com/user', headers: { Authorization: `token ${token}` } }) return res.data; } app.get('/callback', async (req, res) => { const { code } = req.query; console.log(code); // 获取token const options = { code, } try { const access = await client.getToken(options); const resp = await getUserInfo(access.token.access_token); return res.status(200).json({ token: access.token, user: resp, }); } catch (error) { } }) 根据[代码]url[代码]上参数[代码]code[代码]获取到[代码]token[代码],然后根据这个[代码]token[代码]去请求[代码]github api[代码]服务,获取到用户信息,通常网站会根据当前获取到的用户信息完成注册、加session等一系列操作。上面代码中,把用户请求数据简单返回给返回给前端,下面是最后返回给前端的数据格式: [图片] 最后就实现了第三方的登录授权 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 作者:B_Cornelius
2021-03-16 - 小技巧!CSS 整块文本溢出省略特性探究
今天的文章很有意思,讲一讲整块文本溢出省略打点的一些有意思的细节。 文本超长打点 我们都知道,到今天(2020/03/06),CSS 提供了两种方式便于我们进行文本超长的打点省略。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 对于单行文本,使用单行省略: { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 而对于多行文本的超长省略,使用 [代码]-webkit-line-clamp[代码] 相关属性,兼容性也已经非常好了: { width: 200px; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } [图片] CodePen Demo -- inline-block 实现整块的溢出打点 问题一:超长文本整块省略 基于上述的超长打点省略方案之下,会有一些变化的需求。譬如,我们有如下结构: Sb Coco FEUIUX Designer前端工程师 [图片] 对于上述超出的情况,我们希望对于超出文本长度的整一块 -- 前端工程师,整体被省略。 如果我们直接使用上述的方案,使用如下的 CSS,结果会是这样,并非我们期待的整块省略: .person-card__desc { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 将 [代码]display: inline[代码] 改为 [代码]display: inline-block[代码] 实现整块省略 这里,如果我们需要实现一整块的省略,只需要将包裹整块标签元素的 [代码]span[代码] 的 [代码]display[代码] 由 [代码]inline[代码] 改为 [代码]inline-block[代码] 即可。 .person-card__desc span { display: inline-block; } [图片] 这样,就可以实现,基于整块的内容的溢出省略了。完整的 Demo,你可以戳这里: CodePen Demo - 整块超长溢出打点省略 问题二:iOS 不支持整块超长溢出打点省略 然而,上述方案并非完美的。经过实测,上述方案在 iOS 和 Safari 下,没能生效,表现为这样: [图片] 查看规范 - CSS Basic User Interface Module Level 3 - text-overflow,究其原因,在于 [代码]text-overflow[代码] 只能对内联元素进行打点省略。(Chrome 对此可能做了一些优化,所以上述非 iOS 和 Safari 的场景是正常的) 所以猜测是因为经过了 [代码]display: inline-block[代码] 的转化后,已经不再是严格意义上的内联元素了。 解决方案,使用多行省略替代单行省略 当然,这里经过试验后,发现还是有解的,我们在开头还提到了一种多行省略的方案,我们将多行省略的代码替换单行省略,只是行数 [代码]-webkit-line-clamp: 2[代码] 改成一行即可 [代码]-webkit-line-clamp: 1[代码]。 .person-card__desc { width: 200px; white-space: normal; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .person-card__desc span { display: inline-block; } 这样,在 iOS/Safari 下也能完美实现整块的超长打点省略: [图片] CodePen Demo -- iOS 下的整块超长溢出打点省略方案 值得注意的是,在使用 [代码] -webkit-line-clamp[代码] 的方案的时候,一定要配合 [代码]white-space: normal[代码] 允许换行,而不是不换行。这一点,非常重要。 这样,我们就实现了全兼容的整块的超长打点省略了。 当然,[代码] -webkit-line-clamp[代码] 本身也是存在一定的兼容性问题的,实际使用的时候还需要具体去取舍。 最后 好了,本文到此结束,一个简单的 CSS 小技巧,希望对你有帮助 :) 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 作者:chokcoco
2021-03-15 - 小程序的云数据库,自己做的后台系统能访问么?
小程序获取的数据需要做整理,统计,不知道云数据库是否支持其他程序访问。没找到客服,求解!!! 谢谢
2021-01-13 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15