- 如何查找微信公众号主页并复制链接
随意复制该微信公众号的一篇文章链接在浏览器打开查看源代码 找到biz=*****编码,(全局搜一下__biz=)例如biz=MzAxMDE1MTk3Nw== https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzAxMDE1MTk3Nw== 将 biz= 后面的*****编码替换成你刚刚找到的编码,就可以获取到该公众号的主页链接啦。
2022-04-01 - wxss 样式不生效?
首页(index)下的wxss样式无法生效, 其他页面的wxss正常 电脑重启过,不行;软件重启过,不行;微信开发者工具更新到最新版本(当前版本:1.05.2102010),还是不行;
2021-02-25 - 【已解决】wxml使用三元运算符设置class不支持多个class同时指定?
// 指定单个class,正常 <view class="{{showStudioMenu ? studioMenu : tabs}}" /> // 指定多个class,报错 <view class="{{showStudioMenu ? tabbar studioMenu : tabbar tabs}}" /> 报错信息: [ WXML 文件编译错误] Error 2: ./custom-tab-bar/index.wxml:1:12: Bad attr `class` with message: unexpected token `studioMenu`. (env: macOS,mp,1.05.2204180; lib: 2.24.0) [ WXML 文件编译错误] ./custom-tab-bar/index.wxml Bad attr `class` with message > 1 | | ^ 2 | 3 | 4 | at files://custom-tab-bar/index.wxml#1(env: macOS,mp,1.05.2204180; lib: 2.24.0) WAServiceMainContext.js:2 Uncaught FrameworkError Unexpected token '<' SyntaxError: Unexpected token '<'(env: macOS,mp,1.05.2204180; lib: 2.24.0) SyntaxError: Unexpected token '<'(env: macOS,mp,1.05.2204180; lib: 2.24.0) ----------------------- 已解决,不管是单个class还是多个class都需要加单引号,否则即使不报错,样式也会丢失。 正确用法: <view class="{{showStudioMenu ? 'tabbar studioMenu' : 'tabbar tabs'}}" />
2022-04-28 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - 使用云开发CMS能力实现简易商场
源码 点此领取 技术栈 云开发 CloudBase:云端一体化的 Serverless 后端服务解决方案。Taro:一套遵循 React 语法规范的 多端开发 解决方案开发工具 建议提前安装好 微信开发者工具Node LTS 版本VS Code 编辑器CloudBase VS Code 插件需求分析 只考虑基本的功能: 商品列表与下单:展示商品信息,创建订单订单列表:展示订单列表 资源准备 1. 在微信开发者工具中开通云开发,请选择按量付费 如果你的环境是预付费,请到设置中,将支付方式转换为按量付费 [图片] 2. 安装 CMS 系统 (1)更新到最新的 Nightly 版本工具,在工具顶部 Tab 栏中,点击「更多」-「内容管理」。 [图片] (2)点击开通,勾选同意协议后,点击确定。 [图片] (3)开通内容管理需要填写管理员账号,填写账号后,点击「确定」完成。 [图片] (4)开通拓展需要一定时间,请耐心等待。 (5)完成后,点击「更多」-「内容管理」,即可看到内容管理的入口和相关信息。点击访问地址,即可在弹出的窗口中进行内容管理的相关配置。 [图片] 3. 登录 CMS 系统,创建资源 CloudBase CMS 已经部署在当前环境下的静态网站托管中,访问地址的格式如下:云开发静态托管默认域名/部署路径,例如 https://envid.ap-shanghai.app.tcloudbase.com/tcb-cms/(结尾有 / 符号)。默认域名可以访问控制台查看。 打开 CloudBase CMS 后,你需要先登录,账号密码为安装时设置的管理员账号和密码。 在开始管理内容数据前,我们需要先创建一个项目。CloudBase CMS 使用项目划分不同类的内容,便于区分内容数据用途,进行权限管理。 首先,我们需要点击新建项目下方的创建新项目按钮,创建一个名为小商店,Id 为 shop 的项目。 [图片] 创建完项目后,点击项目卡片,进入项目的管理页面,我们会看到项目的欢迎页面。 [图片] 创建商品类型,管理商品信息 创建一个名称为商品的内容模型,数据库名为 goods,即将商品数据存储到 goods 数据集合中。如果新建内容的时候指定的集合不存在,CloudBase CMS 会自动新建集合。 [图片] 在创建完内容模型后,我们会得到一个空的内容模型。接下来,我们需要为商品添加商品名称,商品图片,价格,库存数量等字段。 为商品添加商品名称属性,因为商品名称通常是比较短的文字,所以我们可以选择单行字符串字段,点击右侧的单行字符串卡片,填写商品名称的字段信息。除了基本的名称,数据库字段名之外,我们还可以为此字段添加其他的限制,如最大长度,限制填写商品名称时的最大长度,创建商品时,是否必需填写商品等。 [图片] 类似的,我们可以创建数字类型的价格字段以及库存数量,图片类型的商品图片字段。在创建图片字段时,考虑到商品的图片可能有多张,我们可以打开允许多个内容按钮,表明可以上传多张图片。 [图片] 创建的 goods 数据库集合的结构如下: [图片] 同上,类似的创建一个名称为订单列表,数据库集合名为 order 的内容模型,来管理订单信息。创建的 order 数据库集合的结构如下: [图片] 添加一个商品 [图片] 创建项目 1、拉取模板 # 安装 taro cli 工具 npm install -g @tarojs/cli@2.2.7 # 拉取模板 git clone https://github.com/TencentCloudBase/cloudbase-minishop.git 使用微信开发者工具导入项目,进入 client 目录,安装依赖: npm i 项目目录 cloud/functions 包含写好的微信支付的两个云函数, pay 和接收支付消息推送的 pay-callback 云函数。使用时需使用微信开发者工具上传这两个云函数。 2、项目目录 . ├── client // 小程序源码 │ ├── config │ └── src │ ├── assets │ ├── components │ └── pages │ ├── index │ └── order-list └── cloud // 云开发相关源码 │ └── functions │ ├── pay │ └── pay-callback ├── cloudbaserc.json // 云开发配置 ├── project.config.json // 小程序配置 微信支付下单流程 1、小程序调用云函数,在云函数中调用统一下单接口,参数中带上接收异步支付结果的云函数名和其所在云环境 Id。 const cloud = require("wx-server-sdk"); const res = await cloud.cloudPay.unifiedOrder({ envId: '', subMchId: '', body: "商品名", totalFee: 100, outTradeNo: '订单号', spbillCreateIp: "127.0.0.1", functionName: "pay-callback" }); // 返回 res.payment 支付结果回调的云函数必须返回如下一个对象,否则会视为回调不成功,云函数会收到重复的支付回调。 { errcode: '', errmsg: '', } 2、统一下单接口返回的成功结果对象中有 payment 字段,该字段即是小程序端发起支付的接口(wx.requestPayment)所需的所有信息。 3、小程序端拿到云函数结果,调用 wx.requestPayemnt 发起支付 wx.requestPayment({ ...payment, success (res) { }, fail (res) { }tt })https://docs.cloudbase.net/ 4、支付完成后,在统一下单接口中配置的云函数将收到支付结果通知。 多端支持 - 跨平台 小程序Web 相关文献 云开发文档 云开发微信支付 支付接口
2021-09-10 - 服务创建和部署发布
[视频] 学习微信云托管服务的创建和管理,测试和灰度发布流程,服务相关设置的指导。
2022-03-24 - 小程序video组件在开发者工具可以播放,在手机上无法播放问题终极解决方案
如果你的服务器是Apache2.4,不是的请绕行! 究其根本原因:就是apache没有配置mp4视频不要进行gzip压缩。 在apache的配置文件中加一行: SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|zip|mp4)$ no-gzip dont-vary 再重启apache就可以了。 看了一些帖子,也有问这个问题的,竟然没有给出解决方案,微信官方你们看着办吧!
2017-11-15 - 内容安全检测图片API:openapi.security.imgSecCheck完美解决方案。
背景需求: 我个人做了一款小程序的小游戏,本质是小程序。里面有个自定义图片的功能。用户从本地相册选一张图片进行裁剪,之后保存到缓存中或者上传到服务器。然后用户再用这张图片作为素材进行其它操作。这里就涉及到内容安全了,提交审核没有通过也是因为这个没有做内容安全。防止一些色情低俗的事情发生。 正文: 思路:相册选图片 --> 裁剪小的图片 --> 内容安全检测 --> 通过 --> 裁剪大的图片 --> 保存。 失败的原因:绝大多数是因为检测图片不能大于1M,而导致超时,或者是errCode:-1,又或者是其它问题。 [图片] [图片] 核心代码图片: [代码]默认裁剪小尺寸图片 (我的业务需求是正方形图片,也可动态计算宽高比例) [代码] [图片] 检测图片 部分iOS不兼容encoding: ‘ucs2’。注释掉就好了 [图片] [图片] 云函数 [图片] 测试情况: 正常图片不含违法违规,测试20次,全部通过。小程序上线后暂无发现检测失败情况。百度搜索的“人体油画”等等均可通过。 PS:第一次写经验分享哈,看不懂可以问我。体验一下我的小程序想问我这个小程序其它的功能点也可以喔! 技术会迭代更新,用到的技术会有时效性,看编辑时间,可能当时的技术现在不适用了
2020-10-22 - 动手打造更强更好用的微信开发者工具-编辑器扩展篇
1. 写在前面 1.1 微信开发者工具现状 具备一些基本的通用IDE功能,但是第三方的支持扩展需要加强。 1.2 开发者工具自带的编辑器扩展功能 可能很多老铁没用过官方的微信开发者工具的编辑器扩展(我一般称为编辑器插件)。官方把这块功能也隐藏得很深,也没有相关文档介绍,但是预留了相关的入口。合理利用第三方编辑器插件,可以极大的提升开发效率。下面先来看看官方预留的编辑器插件入口: [图片] (图一) 2. 几个不错插件安装效果 2.1 标签高亮插件-vincaslt.highlight-matching-tag [图片] 功能:可以把当前行对应的标签开头和结尾高亮起来,让开发者一目了然 2.2 小程序开发助手插件-overtrue.miniapp-helper [图片] 功能:必须要说的这个是纯国产的插件,里面的代码片段功能很全,具体介绍:小程序开发助手 - Visual Studio Marketplace https://marketplace.visualstudio.com/items?itemName=overtrue.miniapp-helper 2.3 minapp插件-qiu8310.minapp-vscode [图片] 功能:这个是今天的明星插件,里面的跳转功能很强,可以在wxml里CMD+点击对应变量/方法和CSS样式名称直接跳转到对应的js/wxss文件对应的地方。具体的下面是官方介绍: 标签名与属性自动补全 根据组件已有的属性,自动筛选出对应支持的属性集合 属性值自动补全 点击模板文件中的函数或属性跳转到 js/ts 定义的地方(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 样式名自动补全(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 在 vue 模板文件中也能自动补全,同时支持 pug 语言 支持 link(纯 wxml 或 pug 文件才支持,vue 文件不支持) 自定义组件自动补全(纯 wxml 文件才支持,vue 或 pug 文件不支持) 模板文件中 js 变量高亮(纯 wxml 或 pug 文件才支持,vue 文件不支持) 内置 snippets 支持 emmet 写法 wxml 格式化 3. DIY添加适合自己的插件 3.1 添加插件功能简介 仔细研究过微信开发者工具的人可能知道或者了解,其实微信开发者工具编辑器跟微软的开源编辑器vsCode「颇有渊源」。再深入研究发现,vsCode的插件完全可以无缝移植到微信开发者工具编辑器里来,所以今天的内容就是移植vsCode的插件到微信开发者工具。咱们先看看微信开发者工具自带的「管理编辑器扩展」功能(图1标注为2的地方) [图片](图二) 3.2 插件添加具体步骤 3.2.1 安装插件,获取插件文件 安装vsCode并安装你需要移植的插件,必须要说的是vsCode的插件非常多,好的插件也很多。相关安装,搜索插件教程建议大家百度相关教程。或者直接下载vsCode亲自体验,插件安装过程还是非常简单的。 3.2.2 复制插件文件夹 找到vsCode相关插件的安装文件夹: 操作系统 安装路径 windows %USERPROFILE%.vscode\extensions macOS ~/.vscode/extensions Linux ~/.vscode/extensions 复制对应插件文件夹到微信开发者工具的「打开编辑器扩展目录」(图一标注为1的地方) 3.2.3 添加插件配置文件 新版开发者工具直接进入图形设置,扩展设置里勾选对应插件即可。如下图: [图片] 旧版操作方法:进入微信开发者工具的「管理编辑器扩展」功能页面,在尾端加入对应添加的插件名称。以以上3个介绍的插件为例,在原来的尾端加入: “vincaslt.highlight-matching-tag”, “overtrue.miniapp-helper”, “qiu8310.minapp-vscode” 3.2.4 见证奇迹 重启微信开发者工具,见证插件带来的编码便利吧! 4 需要注意的 vsCode的插件很多,小程序相关的也越来越多了,但是插件质量参差不齐,所以安装时建议选择「标星」star比较多的插件。
2020-05-02 - 小程序读取excel表格数据,并存储到云数据库
最近一直比较忙,答应大家的小程序解析excel一直没有写出来,今天终于忙里偷闲,有机会把这篇文章写出来给大家了。 老规矩先看效果图 [图片] 效果其实很简单,就是把excel里的数据解析出来,然后存到云数据库里。说起来很简单。但是真的做起来的时候,发现其中要用到的东西还是很多的。不信。。。。 那来看下流程图 流程图 [图片] 通过流程图,我看看到我们这里使用了云函数,云存储,云数据库。 流程图主要实现下面几个步骤 1,使用wx.chooseMessageFile选择要解析的excel表格 2,通过wx.cloud.uploadFile上传excel文件到云存储 3,云存储返回一个fileid 给我们 4,定义一个excel云函数 5,把第3步返回的fileid传递给excel云函数 6,在excel云函数里解析excel,并把数据添加到云数据库。 可以看到最神秘,最重要的就是我们的excel云函数。 所以我们先把前5步实现了,后面重点讲解下我们的excel云函数。 一,选择并上传excel表格文件到云存储 这里我们使用到了云开发,使用云开发必须要先注册一个小程序,并给自己的小程序开通云开发功能。这个知识点我讲过很多遍了,还不知道怎么开通并使用云开发的同学,去翻下我前面的文章,或者看下我录的讲解视频《5小时入门小程序云开发》 1,先定义我们的页面 页面很简单,就是一个按钮如下图,点击按钮时调用chooseExcel方法,选择excel [图片] 对应的wxml代码如下 [图片] 2,编写文件选择和文件上传方法 [图片] 上图的chooseExcel就是我们的excel文件选择方法。 uploadExcel就是我们的文件上传方法,上传成功以后会返回一个fildID。我们把fildID传递给我们的jiexi方法,jiexi方法如下 3 把fildID传递给云函数 [图片] 二,解下来就是定义我们的云函数了。 1,首先我们要新建云函数 [图片] 如果你还不知道如何新建云函数,可以翻看下我之前写的文章,也可以看我录的视频《5小时入门小程序云开发》 如下图所示的excel就是我们创建的云函数 [图片] 2,安装node-xlsx依赖库 [图片] 如上图所示,右键excel,然后点击在终端中打开。 打开终端后, 输入 npm install node-xlsx 安装依赖。可以看到下图安装中的进度条 [图片] 这一步需要你电脑上安装过node.js并配置npm命令。 3,安装node-xlsx依赖库完成 [图片] 三,编写云函数 我把完整的代码贴出来给大家 [代码]const cloud = require('wx-server-sdk') cloud.init() var xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async(event, context) => { let { fileID } = event //1,通过fileID下载云存储里的excel文件 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] //用来存储所有的添加数据操作 //2,解析excel文件里的数据 var sheets = xlsx.parse(buffer); //获取到所有sheets sheets.forEach(function(sheet) { console.log(sheet['name']); for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; //第几行数据 if (rowId > 0 && row) { //第一行是表格标题,所有我们要从第2行开始读 //3,把解析到的数据存到excelList数据表里 const promise = db.collection('users') .add({ data: { name: row[0], //姓名 age: row[1], //年龄 address: row[2], //地址 wechat: row[3] //wechat } }) tasks.push(promise) } } }); // 等待所有数据添加完成 let result = await Promise.all(tasks).then(res => { return res }).catch(function(err) { return err }) return result } [代码] 上面代码里注释的很清楚了,我这里就不在啰嗦了。 有几点注意的给大家说下 1,要先创建数据表 [图片] 2,有时候如果老是解析失败,可能是有的电脑需要在云函数里也要初始化云开发环境 [图片] 四,解析并上传成功 如我的表格里有下面三条数据 [图片] 点击上传按钮,并选择我们的表格文件 [图片] 上传成功的返回如下,可以看出我们添加了3条数据到数据库 [图片] 添加成功效果图如下 [图片] 到这里我们就完整的实现了小程序上传excel数据到数据库的功能了。 再来带大家看下流程图 [图片] 如果你有遇到问题,可以在底部留言,我看到后会及时解答。后面我会写更多小程序云开发实战的文章出来。也会录制本节的视频出来,敬请关注。
2019-11-12 - 免费ICP备案攻略。不花1分钱拥有一台云服务器并顺利ICP备案。
写在前面: 大家不要将ICP证和ICP备案搞混了。 ICP证指的是【电信增值业务经营许可证】,这个资质需要企业主体至少100万注金,去工信部办理,比较难办理;社交-交友需要ICP证。 而ICP备案,【非经营性互联网信息服务备案核准】仅仅是指企业主体的域名备案,可以简单的按以下步骤免费办理成功,其他社交类目如社区、论坛、笔记等,只需要ICP备案即可。 1、在腾讯云注册一个账号并认证企业主体(不吹不黑,开发小程序当然首选腾讯云,好用)。http://www.qcloud.com/ 如果你是个人主体,就不要往下看了,没必要折腾了。 2、找到腾讯云免费活动页:https://cloud.tencent.com/act/free?from=10107 3、选择一款云服务器,180天免费试用。 云服务器申请成功后,它的使命就完成了,没用了,让它自生自灭吧。 在整个备案过程中,也不需要部署网站(域名都没有备案,哪来的网站?)。 [图片] 云服务器180天到期后,可以自己决定是否续费,每个月也才99元,促销期甚至更低,完全可以接受吧。 备案成功后,该服务器就没什么作用了,让它180天后自然欠费销毁得了。 服务器销毁后会有什么影响?答:没有任何影响。 但是。。。。。 你备案的域名最后还得指向一个网站,因为腾讯云会应工信部的要求定期检查网站是否合规,所以你还是要建一个简单的网站,(备案期间,可以暂时不管网站的事,等将来需要的时候再管理)。 至于有多简单,答,多简单都行。此时你可以在七牛、腾讯云、阿里云租点免费的对象存储空间,做个简单的网站。 4、在进行ICP备案之前,你需要在腾讯云注册你的域名地址,如果你已有域名,但不在腾讯云,建议先将要域名过户到腾讯云的账号上。 5、进入控制台,开始ICP备案,这个流程就不介绍了,因为完全一看就懂。而且现在使用备案小程序后,不需要幕布或现场拍照了,极其方便,大家跟着流程走就一点问题没有,有人脸识别和在线拍一段小视频。另外,大家可以随便作,随便填,填错或者填得不合适也不用怕,会有专门的备案客服打电话告诉你哪哪要改,还会告诉你应该怎么填才更容易通过工信部的审核,客服的态度好得发指。 仅说一点其中的几个小坑: a、人脸识别的时候,白色背景、白色背景、白色背景,笔者在人脸识别的时候,满世界找白墙,结果还被打回来重拍了3次。 b、网站用途一律写:公司官网,好通过工信部审核。 6、腾讯云提交资料到工信部审核。这是一个漫长的让人无语的等待,20-30天。笔者最近两次都是20天才过审;不要幻想会有可能提前完成审核,这是政府部门在审核,提前完成说明某政府人员的工作安排有问题,会犯错误的。 7、备案成功后,会有短信通知你,但是,你需要去工信部网站查询结果,并将结果切屏拷贝下来,因为小程序类目审核需要上传这张图片。http://beian.miit.gov.cn/publish/query/indexFirst.action [图片] 把上面这张图片保存好,小程序类目审核的时候需要上传。收到通知后,如果在这里查不到结果,也别急,据说需要24小时。 8、接下来是小程序上线审核。 因为ICP备案的小程序内容肯定涉及到社交,最后小程序上线时还要提交到工信部审核,还需要7天左右的时间,加上前面ICP备案的时间,加起来怎么也得30-40天。大家估计时间,别影响小程序上线。这7天也是政府部门在审核,不要幻想会提前。 9、计算一下时间: 腾讯云注册账号和认证:1-3天; 域名备案:腾讯云环节:1-3天; 域名备案:工信部环节:20-30天; 小程序添加服务类目:社交类目审核:1-3天; 小程序上线审核:腾讯环节:1-2天; 小程序上线审核:工信部环节:7+天; 总天数:30-40天; 10、节省时间的一些建议: 在开发小程序之前,就开始备案工作,小程序可以同时开发,相互不影响; 在开发完成之前一、两星期之内,先发布一版小程序,别管功能是不是完整,能通过审核就行,这样会有7天的等待类目审核的时间,这个时间里,小程序可以照常开发,不影响进度; 只要是社交类,基本需要有文字和图片安全检查功能,别忘了加上,别到时审核通过不了。 11、结束。 [图片]
2021-01-19 - 如何使用云开发的 HTTP API 上传文件 | 云开发
云开发是目前不少人在开发小程序时的选择,随着业务的增长,开发者也开始追求在更多的平台使用云开发。 今天,我们来看一看,如何在网页中直接使用云开发的 HTTP API 上传文件。 业务流程说明 由于云开发的存储使用的是腾讯云的对象存储 COS,因此,在上传时,存在一个上传流程的问题。具体的上传流程我绘制了一个时序图,你可以通过这个时序图来了解具体的上传流程。 [图片] 具体实现 接下来我们来实现一下具体的上传步骤,主要来说,有以下三步: 请求微信的接口,获取到 AccessToken 使用 AccessToken 接口,换取 COS 上传参数 使用 COS 的 API 接口上传文件 1. 获取 Access Token 首先,我们需要获取 Access Token,这里我们需要用到微信官方文档的 [代码]getAccessToken[代码] 这个接口,已经小程序的 AppID 和 Secret 。 Appid 和 Secret 可以在小程序后台的开发设置中获取,这里不再赘述。 由于 Access Token 不允许在前端进行获取,需要大家根据自己的语言编写代码获取 Token ,并发送到前端。这里我给几个常见语言的例子 Java 你需要将下方的 APPID 和 SECRET 替换为你自己的 APPID 和 SECRET [代码]import java.io.IOException; import org.apache.http.client.fluent.*; public class SendRequest { public static void main(String[] args) { sendRequest(); } private static void sendRequest() { // 获取 Code (GET ) try { // Create request Content content = Request.Get("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET") // Fetch request and return content .execute().returnContent(); // Print content System.out.println(content); } catch (IOException e) { System.out.println(e); } } } [代码] PHP 你需要将下方的 APPID 和 SECRET 替换为你自己的 APPID 和 SECRET [代码]<?php // get cURL resource $ch = curl_init(); // set url curl_setopt($ch, CURLOPT_URL, 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET'); // set method curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); // return the transfer as a string curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // send the request and save response to $response $response = curl_exec($ch); // stop if fails if (!$response) { die('Error: "' . curl_error($ch) . '" - Code: ' . curl_errno($ch)); } echo 'HTTP Status Code: ' . curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL; echo 'Response Body: ' . $response . PHP_EOL; // close curl resource to free up system resources curl_close($ch); [代码] Ruby 你需要将下方的 APPID 和 SECRET 替换为你自己的 APPID 和 SECRET [代码]require 'net/http' require 'net/https' # 获取 Code (GET ) def send_request uri = URI('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET') # Create client http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_PEER # Create Request req = Net::HTTP::Get.new(uri) # Fetch Request res = http.request(req) puts "Response HTTP Status Code: #{res.code}" puts "Response HTTP Response Body: #{res.body}" rescue StandardError => e puts "HTTP Request failed (#{e.message})" end [代码] 2. 获取 COS 上传参数 当我们拿到了 Access Token 以后,就可以向云开发的接口发送请求,获取 COS 的上传参数了。同样,这个接口也需要在服务端请求。 [图片] 需要注意的是,这个接口有三个参数,其中的 access token 需要放在 url 里,另外两个参数需要放在 post 的 body 里。 具体可以参考下面的代码,这里我用的参数如下: [代码]{ "env": "rcn", "path": "testfile" } [代码] Java 这里将 Token 替换为上一个环节中生成的 Token [代码]import java.io.IOException; import org.apache.http.client.fluent.*; import org.apache.http.entity.ContentType; public class SendRequest { public static void main(String[] args) { sendRequest(); } private static void sendRequest() { // 调用上传接口 (POST ) try { // Create request Content content = Request.Post("https://api.weixin.qq.com/tcb/uploadfile?access_token=TOKEN") // Add headers .addHeader("Content-Type", "application/json; charset=utf-8") // Add body .bodyString("{\"env\": \"rcn\",\"path\": \"testfile\"}", ContentType.APPLICATION_JSON) // Fetch request and return content .execute().returnContent(); // Print content System.out.println(content); } catch (IOException e) { System.out.println(e); } } } [代码] PHP 这里将 Token 替换为上一个环节中生成的 Token [代码]<?php // get cURL resource $ch = curl_init(); // set url curl_setopt($ch, CURLOPT_URL, 'https://api.weixin.qq.com/tcb/uploadfile?access_token=TOKEN'); // set method curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); // return the transfer as a string curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // set headers curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json; charset=utf-8', ]); // json body $json_array = [ 'env' => 'rcn', 'path' => 'testfile' ]; $body = json_encode($json_array); // set body curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // send the request and save response to $response $response = curl_exec($ch); // stop if fails if (!$response) { die('Error: "' . curl_error($ch) . '" - Code: ' . curl_errno($ch)); } echo 'HTTP Status Code: ' . curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL; echo 'Response Body: ' . $response . PHP_EOL; // close curl resource to free up system resources curl_close($ch); [代码] Ruby 这里将 Token 替换为上一个环节中生成的 Token [代码]require 'net/http' require 'net/https' require 'json' # 调用上传接口 (POST ) def send_request uri = URI('https://api.weixin.qq.com/tcb/uploadfile?access_token=TOKEN') # Create client http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_PEER dict = { "env" => "rcn", "path" => "testfile" } body = JSON.dump(dict) # Create Request req = Net::HTTP::Post.new(uri) # Add headers req.add_field "Content-Type", "application/json; charset=utf-8" # Set body req.body = body # Fetch Request res = http.request(req) puts "Response HTTP Status Code: #{res.code}" puts "Response HTTP Response Body: #{res.body}" rescue StandardError => e puts "HTTP Request failed (#{e.message})" end [代码] 3. 上传文件到 COS 的 API 在第二步的 API 请求成功后,你需要构建一个请求,来上传文件。这部分需要确保你的各项参数是正确的。 [图片] 这里我给出一个使用 Jquery 上传的示例,你可以对照我的代码,看一下你自己的代码应该是什么样的。 [代码]// 上传文件 (POST https://cos.ap-shanghai.myqcloud.com/7263-rcn-1258788140/testfile) var formData = new FormData(); formData.append("file", "123"); formData.append("x-cos-meta-fileid", "HBPXgrO3M5Wijh6eLuVVX+iG11rk4mNdQum5Lg10PFCDuk7sQN12Y4t0fG36qkajY6iZGe3GSIo/5ykMG4hdsu1HUJ5OZccS7VA4961UZiJ27pOrXRBtgxYjssuMW52+pyXLIO0sYHvHCQn8EZE="); formData.append("x-cos-security-token", "oKPjfeWGvRFzSR45azxJoZEzcMozJOWecd700261215792a61eefe56427ac91f6OlXUai4_iST1mbzYo8ph_GCgGejJNvtA1V483_SpzpnsPLeQc43b1MKj3yI5Dr0zT-jfYFB33-fe52ZMdZhEl3dCuJN7buFKPOAQqwEjFlr7gSyOrP5ox350qslAnjKHxORAQVNMR3PwiA1uxMe3tjtOjQKTF-xu1kxTowczdtwPYYC7F-IqbQFZ9q_yIQGNzIKs_-67tIeLl0Zv7iUkCE3KcmJUG9isgjpnCe_BSUsgw6EvBmrYU7eP-AIPuFPJZyC7Ocum11PGr3H4HU2QuZYp1AOWI48izl2wSJyu3CZZGxjsZ2CNHomNw7KvGKvR3OISx74Zzr41mZc7P6RNYPzK6jeE3K3rwWuolKtaqnlPiUQLd9i_9HScv2xBbgD4MHwGZB6jgb3DgYlenCtFlQ"); formData.append("Signature", "q-sign-algorithm=sha1&q-ak=AKIDtxMKuQ9CIgWF-aSrFKfNzPpijPhLSYQ8q-fsI2KfFqA104Sba16xKmbT6RgYpHD-&q-sign-time=1581727591;1581728491&q-key-time=1581727591;1581728491&q-header-list=&q-url-param-list=&q-signature=62f0d5f90dec5153a59b28820f77255aa9baa37e"); formData.append("key", "testfile"); jQuery.ajax({ url: "https://cos.ap-shanghai.myqcloud.com/7263-rcn-1258788140/testfile", type: "POST", headers: { "Content-Type": "multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__", }, processData: false, contentType: false, data: formData, }) .done(function(data, textStatus, jqXHR) { console.log("HTTP Request Succeeded: " + jqXHR.status); console.log(data); }) .fail(function(jqXHR, textStatus, errorThrown) { console.log("HTTP Request Failed"); }) .always(function() { /* ... */ }); [代码] 请求成功后,你会获得一个 HTTP Status Code 为 204 的 Response ,则说明你上传成功了。 总结 最后,我们来进行总结,想要使用云开发的 HTTP API,你需要先请求 Access Token 接口,获取 Token ,并基于此,获取 COS 上传接口。最后,在前端构建 FormData ,上传文件到真正的 COS 中去。最后,当你获得一个 204 的返回码时,就说明文件上传成功了。 扫描下方二维码,关注我的微信公众号,获取每日微信开发知识 [图片] 或添加我的个人微信:huanchengbai,和我取得联系
2020-02-15 - 用__wxConfig.envVersion区分小程序体验版,开发板,正式版
在开发过程中,通常测试版和正式版的api的根路径不同,需要在发布时手动去更改路径,这就显得很繁琐,然后官方也没有给出相应的判断环境的api,其实小程序是预设了这个api的,只是不知道为什么没有公布出来,这个api就是 __wxConfig 关键点 — __wxConfig 在控制台中打印__wxConfig可以得到一下数据 [图片] 其中的envVersion为运行环境,有以下几个值 envVersion: ‘develop’, //开发版 envVersion: ‘trial’, //体验版 envVersion: ‘release’, //正式版 其中的platform为运行的平台 有Android ios devtools 等 之前一直不知道微信小程序可以用__wxConfig.envVersion区分小程序体验版,开发板,正式版 目前在官方文档没有查到相关资料,但是亲测可用 [代码] envVersion 类型为字符串 envVersion: 'develop', //开发版 envVersion: 'trial', //体验版 envVersion: 'release', //正式版 [代码] 具体代码可参考如下截图 [图片] 20191120 其实在我们的开发过程中是不需要这个变量的,因为我们开发版、体验版、和生产版是三个不同的小程序,所以不需要根据环境变量来区分 20191121摘自社区帖子 [代码]const env = typeof __wxConfig !== "undefined" ? __wxConfig.envVersion || "release" : "release"; const isProd = env === "release"; const protocol = isProd ? "https://" : "http://"; const baseApi = { develop: "testapi.com", trial: 'readyapi.com', release: "api.com" }; export const api = protocol + baseApi[env]; [代码]
2019-11-20 - wx.previewImage()方法无法预览本地图片
- 当前 Bug 的表现(可附上截图) 调用wx.previewImage()方法,无法预览本地图片(包含绝对路径和相对路径) 预览网路图片则可以,详见代码片段 出现问题基础版本库:目前测了2.1.1以上的都无法预览 [图片] - 提供一个最简复现 Demo 代码片段: wechatide://minicode/hFNmUxm97Z28
2018-09-19 - 小程序AR识别,三行代码实现Camera数据毫秒级转base64图片
关键词:小程序AR 图片 base64 相机 Camera onCameraFrame Canvas ArrayBuffer Uint8Array Uint8ClampedArray upng-js 核心步骤: 1 相机原始图像数据frame.data,即ArrayBuffer数组,转成Uint8Array数组 2 Uint8Array数组转成Uint8ClampedArray数组 3 wx.canvasPutImageData(Uint8ClampedArray) 详细流程如下: 最近因为项目需求,需要上传base64去做AR识别功能,和大家一起分享讨论下具体的实现方式。 首先说下实现原理,通过Camera的onCameraFrame获取实时帧数据,将实时帧数据添加到Canvas上,然后将Canvas保存为临时图片,再将临时图片转换为base64。 贴上核心实现代码: wxml: js: var nCounter = 0; openCamera: function (res) { var that = this var camera_ctx = wx.createCameraContext() listener = camera_ctx.onCameraFrame((frame) => { // nCounter等于30 是因为一开始相机会有一个对焦的过程,如果一开始获取数据,就是模糊的图片 if (nCounter == 30) { console.log(frame.data instanceof ArrayBuffer, frame.width, frame.height) var data = new Uint8Array(frame.data); var clamped = new Uint8ClampedArray(data); // 实时帧数据添加到Canvas上 wx.canvasPutImageData({ canvasId: 'myCanvas', x: 0, y: 0, width: frame.width, height: frame.height, data: clamped, success(res) { // 转换临时文件 wx.canvasToTempFilePath({ x: 0, y: 0, width: frame.width, height: frame.height, canvasId: 'myCanvas', fileType: 'jpg', destWidth: frame.width, destHeight: frame.height, // 精度修改 quality: 0.8, success(res) { // 临时文件转base64 wx.getFileSystemManager().readFile({ filePath: res.tempFilePath, //选择图片返回的相对路径 encoding: 'base64', //编码格式 success: res => { // 保存base64 that.data.mybase64 = res.data; } }) }, fail(res) { console.log(res); } }, that) } }) } nCounter++ // console.log(nCounter); if (nCounter >= 100) { nCounter = 0 } }) listener.start() } 目前网上有两种转换方式并对比下: 1:upng-js等第三方转码js库,将相机流转换成base64,一般需要1-2s左右 [图片] 2.使用canvas将相机流转变base64,都是使用js或者小程序官方的api进行转换,一般转换时间在1秒以下: [图片] 重点说明下: 如何使用wx.canvasPutImageData()将相机流添加canvas,我们查看该官方api,添加的data类型为:Uint8ClampedArray [图片] 而我们通过onCameraFrame获取的data类型为:ArrayBuffer [图片] 所有两者类型不一致,就需要转换,将ArrayBuffer=>Uint8Array=>Uint8ClampedArray var data = new Uint8Array(frame.data); var clamped = new Uint8ClampedArray(data); 成功的把onCameraFrame获取实时帧数据转换并canvasPutImageData在canvas上,并通过canvasToTempFilePath获取临时文件,如何获取临时文件getFileSystemManager转换为base64,传入云端进行AR识别,就大功告成! 技术分享来自于:北京晞翼科技有限公司 技术作者:le3d618、xiaoz0816 微信商务联系:le3d618
2020-04-30 - 云调用实现内容安全【文本、图片】
应用场景: 解决小程序输入内容违规,导致小程序被封风险,或者微信官方检查到小程序未使用安全审核机制,则警告要求使用,否则封禁搜索功能。 核心代码: 云函数端: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { try { let result = ''; if(event.content){ result = await cloud.openapi.security.msgSecCheck({ content: event.content }); }else if(event.base64){ result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/jpeg', value: Buffer.from(event.base64, 'base64') } }) } return { result } } catch (error) { return { error } } } 小程序端: //文本安全检测 wx.cloud.callFunction({ name: "secCheck", data: { content: "花里胡哨", } }).then((res) => { console.log('msgSecCheck =', res) }) //图片安全检测 wx.chooseImage({ count: "1", complete: (res) => { wx.getFileSystemManager().readFile({ filePath: res.tempFilePaths[0], encoding: "base64", success: (res) => { wx.cloud.callFunction({ name: "secCheck", data: { base64: res.data, } }).then((res) => { console.log('imgSecCheck =', res) }) } }); }, }) 说明提示: 由于代码片段不支持云开发,故无法放代码片段,使用过程中有什么问题,欢迎讨论。
2020-05-07 - 云函数抓取网络图片转存到云存储
const cloud = require(‘wx-server-sdk’) const fs = require(‘fs’) const path = require(‘path’) const rp = require(‘request-promise’); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { var options = { url: “xxxx”, encoding: null, //如果不加这个选项,保存的图片会显示不出来,request默认返回utf8格式 }; let resultValue = await rp(options); cloud.uploadFile({ cloudPath: ‘8787.jpg’, fileContent: resultValue, }) }
2019-11-27 - 新能力 | 云开发CMS内容管理系统,5分钟搞定小程序管理后台
小程序·云开发的云调用能力,让用户可以免鉴权快速调用微信的开放能力,极大节约了开发成本。现在,大家期待已久的云开发 CMS 内容管理系统,终于上线啦!顺便提示,接下来还可以二次开发哦! 云开发 CMS 管理系统是什么? 云开发 CMS 内容管理系统是云开发提供的一个扩展程序,可以在云开发控制台一键安装在自己的云开发环境中,方便开发人员和内容运营者随时随地管理小程序 / Web 等多端云开发内容数据。不用编写代码就可以使用,还提供了 PC /移动端浏览器访问支持,支持文本、富文本、图片、文件、关联类型等多种类型的可视化编辑。 [图片] 先来看看云开发CMS的"庐山真面目" 首先我们通过几张截图来直观感受一下 CMS 内容管理系统扩展: 图1 云开发控制台的安装界面截图 [图片] 图2 安装并配置好内容的 CMS 内容管理系统界面演示 [图片] 图3 CMS 内容管理系统界面的移动端演示 [图片] 云开发 CMS 内容管理系统有哪些功能特性 ? 特性 介绍 免开发 基于后台建模配置生成内容管理界面,无须编写代码 多端适配 支持 PC/移动端访问和管理内容 功能丰富 支持文本、富文本、图片、文件 等多种类型内容的可视化编辑,并且支持内容关联 权限控制 系统基于管理员/运营者两种身份角色的访问控制 外部系统集成 支持 Webhook 接口,可以用于在运营修改修改内容后通知外部系统,如自动构建静态网站、发送通知等 数据源兼容 支持管理小程序/ Web / 移动端的云开发数据,支持管理已有数据集合,也可以在 CMS 后台创建新的内容和数据集合 部署简单 可在云开发控制台扩展管理界面一键部署和升级 什么场景下适合使用 CMS ? 1. 适用于需要为小程序应用增加一个运营管理后台的业务 小程序应用有偏运营方面的文章编辑和发布、运营活动配置、素材管理等数据管理需求,使用 CMS 扩展之后,不用手动线上修改 db 数据,也不用投入人力物力开发管理后台,可以随时随地使用自己环境下部署的 CMS 内容管理系统来管理,同时还支持区分管理员和运营者的身份权限。 2. 适用于快速开发内容型的网站应用、小程序应用等场景 CMS 内容管理系统还可以帮助开发者提升开发网站应用、小程序应用的效率,省去一部分后端开发工作。例如安装了CMS 扩展之后,解决了内容和数据的管理和生产问题,直接可以结合前端应用框架读取 db 数据进行渲染。例如基于 CMS 可以快速开发博客、企业官网等小程序/网站应用,最后悄悄透露一下,云开发的官网 (http://cloudbase.net/) 就是基于 CMS 扩展 + Next.js + 云开发静态托管搭建和部署的。 如何安装和使用 CMS ? 第一步:切换为按量付费 由于 CMS 扩展需要用到静态网站托管资源,必须在按量计费的环境下才可以部署,因此首先要切换计费方式为按量付费。 1. 微信小程序开发者 登录微信开发者工具-云开发控制台 在【云开发控制台】-【设置】-【环境设置】-【支付方式】中点击切换【按量付费】即可。 注意:这里需要先保证腾讯云账户中是有充值金额的哦~ [图片] 2. 腾讯云开发者 登录腾讯云云开发控制台 在【云开发 CloudBase 控制台】-【环境】-【资源购买】-【计费模式】中点击【切换按量付费】即可。 [图片] 第二步:在腾讯云控制台安装扩展 登录腾讯云控制台 微信小程序开发者需要使用微信公众号登录! [图片] 在【云开发 CloudBase 控制台】-【扩展能力】-【扩展管理】中找到 CMS内容管理系统 扩展进行安装 安装时需要进行资源的授权和扩展程序的配置,比如管理员和运营者的账号密码配置等,同时需要提供自定义登录的密钥,可以点击自定义登录密钥旁边的小图标了解如何填写。 [图片] 第三步:使用 CMS 内容管理系统 完成【CMS内容管理系统】的安装以后,然后访问该扩展的管理页,可以在【扩展运行方式】Tab 查看使用指引,依照文档完成 CMS 的使用,下面简单介绍一下快速上手的步骤,更多细节可以参考运行方式。 [图片] 访问 CMS 系统 CMS 扩展已经部署在当前环境下的静态网站托管中,访问路径为“静态托管的默认域名+安装设置的部署路径” 访问地址的格式如下: [代码]云开发静态托管默认域名/部署路径[代码],例如 [代码]https://xxxx.tcloudbaseapp.com/tcb-cms/[代码] 账号登录 打开 CMS 系统后首先会提示需要登录,我们首先使用使用安装扩展时设置的管理员账号和密码进行登录 内容建模 登录成功后,首先需要进行内容的建模设置,例如我们想为自己的博客应用(小程序/网站)来生成管理界面。 假设当前已有一个管理 文章的数据库集合 [代码]articles[代码],我们可以在 CMS 管理后台新建一个 “文章” 内容(如果新建内容的时候指定的集合名不存在,CMS 扩展会自动新建集合)来生成“文章”类型的内容管理界面。 假设数据库集合 [代码]articles[代码] 的结构如下: 字段名 类型 描述 _id ID 文章唯一 id name String 文章标题 cover String 封面图,这里存放云开发的存储的文件的 cloudID content String 文章内容,采用 markdown 格式 author ID 作者的用户 id createTime DateTime 创建时间 updateTime DateTime 更新时间 tag String[] 标签,例如 [代码]["serverless","cms"][代码] category String[] 分类,例如 [代码]["前端","开发"][代码] 我们在“内容设置”中点击“新建”来创建“文章”类型时,可以对照上面的集合数据把字段类型和字段的限制进行配置,例如封面图可以直接选择 “图片”字段类型,文章内容可以直接选择 “Markdown” 类型,这样在生成的管理界面里可以直接上传图片和通过编辑器编写文章,保存在数据库集合的时候,依然会保存为数据库支持的类型,图片会存储为云存储的 CloudID, 内容会存储为字符串等。 [图片] 创建并保存之后会自动刷新生成”文章“的运营界面 管理内容 接下来就可以进行运营管理内容操作了,可以使用运营者身份登录,对新创建的“文章”进行操作,我们可以新建一篇文章。 [图片] 文章发布成功后,即可在文章列表中看到这篇文章 [图片] 使用内容数据 采用 CMS 管理的内容,依然可以通过云开发各端 SDK 进行访问(需要注意的是在前端访问时,需要正确设置数据库的安全规则设置,例如设置为所有用户可读,仅创建者可写)。 例如,在上面的例子里,我们需要在云函数中获取文章的标签是 [代码]CloudBase[代码] 的最新 10 条文章,可以采用以下代码来获取数据: [代码]db.collection("articles") .where({ tag: "CloudBase" }) .orderBy("createTime", "desc") .limit(10) .get(); [代码] 获取到内容数据就可以在各种场景使用了,比如在小程序/ Web 中构建应用和网站,具体的CMS + 应用开发的实践可以关注后期我们的实践教程。 [图片] 后续,云开发CMS内容管理系统将支持二次开发,用户可以自由定制自己的管理后台。云开发将始终坚持,为开发者提供一站式云服务! [图片] 最后,小编赠上《5分钟部署云开发CMS系统》教程,帮助大家快快上车! 视频链接: https://v.qq.com/x/page/f09687on1qv.html 文档链接 :(CMS 内容管理系统链接) https://cloud.tencent.com/document/product/876/44547
2020-09-14 - 小程序canvas绘制base64的二维码图片
从后台获取二维码base64码 success: res => { // console.log(res.data.data) var imgSrc = res.data.data;//base64编码 var save = wx.getFileSystemManager(); var number = Math.random(); //保存本地图片文件路径,也是绘图路径 let filePath = wx.env.USER_DATA_PATH + '/pic' + number + '.png' save.writeFile({ filePath: filePath, data: imgSrc, encoding: 'base64' success: res2 => { wx.saveImageToPhotosAlbum({ filePath: filePath, success: function (res2) { console.log(res2) // wx.showToast({ // title: '保存成功', // }) if (res.data.data) { //这是封装的模板,赋值 that.setData({ shareThree: { avatar: userInfo.avatarUrl, nickname: userInfo.nickName, awardMoney: that.data.positionArr.jobName, showShareModel: true ErWeiMa: filePath, awardContent: that.data.positionArr.nature + ' | ' + that.data.positionArr.city } }) } } }) } }) } 另外,如果是从后台返回直接的图片路径,就需要先下载,用下载后返回的路径绘图 //下载二维码 //console.log(that.data.ErWeiMa) // wx.downloadFile({ // url: that.data.ErWeiMa, // success: function (res) { // //console.log(res) // if (res.statusCode == 200) { // that.setData({ //设置ctx.drawImage(that.data.ErWeiMa, *,*,*)地址 // QRPath: res.tempFilePath // }) // that.drawImage(); // } // }, // fail: function () { // that.showErrorModel('网络错误'); // } // })
2019-12-25 - 二维码base64转图片
var base64 = res.data.replace(/[\r\n]/g, "") //res.data是后端返给你的base64,有时候会存在换行符, // 小程序端解析不了,你需要自己做处理 var array = wx.base64ToArrayBuffer(base64) const req = wx.getFileSystemManager(); const FILE_BASE_NAME = 'mine_base64'; const filePath = wx.env.USER_DATA_PATH + '/' + FILE_BASE_NAME + '.png'; req.writeFile({ filePath, data: array, encoding: 'binary', success() { console.log(filePath) that.setData({ errormsg: '', code: filePath //图片地址 }) }, fail() { }, });
2020-05-21 - 微信服务平台MCN内容服务专区上线公告
微信服务平台MCN内容服务专区于6月30日正式对外开放,可以连接商家和内容服务商,为商家提供直播、视频、图文、线下活动等相关服务,满足方案策划、内容制作等代运营需求。 商家可在平台下单,选择合适的服务商联系合作。 当前服务内容包括: 直播策划直播培训达人代播直播设备租赁图文/视频策划公众号代运营品牌营销策划 商家可通过以下方式与服务商取得联系: 1.商家可通过企业主页右上角的【合作联系】与服务商进行联系,咨询服务相关事宜。 [图片] 2.商家可选择具体的服务和方案,通过服务主页右上角的【合作联系】与机构联系,或通过服务详情页的【我要预约】填写需求细节进行预约下单。服务商将在收到预约后与商家联系,落实合作内容。 [图片] [图片] 平台在陆续接入更多优质的内容服务商和服务。 MCN内容服务专区 微信内容服务商开放入驻公告及申请流程
2020-07-01 - 小程序内用户帐号登录规范调整和优化建议
为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范。本公告所称“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。具体规范要求如下: 1.服务范围开放的小程序 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 包括但不限于打开小程序后立即跳转提示登录或打开小程序后立即强制弹窗要求登录,都属于违反上述要求的情况; 以下反面示例,在用户打开小程序后立刻弹出授权登录页; [图片] 建议修改为如下正面示例形式:在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项。 [图片] 2.服务范围特定的小程序 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序; 下图案例为正面示例:校友管理系统,符合规范要求。 [图片] 3.仅提供注册功能小程序 对于线上仅提供注册功能,其他服务均需以其他方式提供的小程序,可在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或帐号登录。如ETC注册申请、信用卡申请; 如下反面示例,用户在进入时未获取任何信息,首页直接强制弹框要求登录注册ETC,这是不符合规范的。 [图片] 建议修改为如下正面示例所示形式:允许在首页说明注册功能后,提供登录或注册按钮供用户主动选择点击登录。 [图片] 4.提供可取消或拒绝登录选项 任何小程序调用帐号登录功能,应当为用户清晰提供可取消或拒绝的选项按钮,不得以任何方式强制用户进行帐号登录。 如下图所示反面示例,到需要登录环节直接跳转登录页面,用户只能选择点击登录或退出小程序,这不符合登录规范要求。 [图片] 建议修改为下图正面示例形式,在需帐号登录的环节,为用户主动点击登录,并提供可取消按钮,不强制登录。 [图片] 针对以上登录规范要求,平台希望开发者们能相应地调整小程序的帐号登录功能。如未满足登录规范要求,从2019年9月1日开始,平台将会在后续的代码审核环节进行规则提示和修改要求反馈。
2019-07-20 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 小程序与小游戏获取用户信息接口调整,请开发者注意升级。
为优化用户体验,使用 wx.getUserInfo 接口直接弹出授权框的开发方式将逐步不再支持。从2018年4月30日开始,小程序与小游戏的体验版、开发版调用 wx.getUserInfo 接口,将无法弹出授权询问框,默认调用失败。正式版暂不受影响。开发者可使用以下方式获取或展示用户信息: 一、小程序: 1、使用 button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息。 详情参考文档: https://developers.weixin.qq.com/miniprogram/dev/component/button.html 2、使用 open-data 展示用户基本信息。 详情参考文档: https://developers.weixin.qq.com/miniprogram/dev/component/open-data.html 二、小游戏: 1、使用用户信息按钮 UserInfoButton。 详情参考文档: https://developers.weixin.qq.com/minigame/dev/document/open-api/user-info/wx.createUserInfoButton.html 2、开放数据域下的展示用户信息。 详细参考文档: https://developers.weixin.qq.com/minigame/dev/document/open-api/data/wx.getUserInfo.html 请各位开发者注意及时调整接口。
2018-04-16 - 社区每周 | 小程序文档支持在线交互式预览、上两周社区问题反馈(09.02-09.13)
各位微信开发者: 以下是上两周小程序相关能力更新及我们在社区收到的问题反馈、需求的处理进度,希望同大家一同打造小程序生态。 「微信官方文档」支持在线交互式预览微信官方文档 部分组件“示例代码”已从“静态代码+静态贴图预览”模式升级为“代码编辑器+交互式预览”,实现阅读示例代码时可在浏览器端同步体验示例效果。目前该功能已覆盖大部分小程序 表单组件。我们正在逐渐丰富使用该模式的内容与功能,以进一步提升开发者阅读文档的效率与体验。 各位开发者浏览文档体验该功能时若有任何意见建议,欢迎在本帖下方留言反馈,我们会及时关注与评估改进。 [图片] 上两周问题反馈和处理进度(09.02-09.13)修复中的问题onPageScroll 在 iOS 端需要 touchend 才有反馈的问题 查看详情 点击地图标点 markers,同时触发了标点的点击事件和点击地图的问题 查看详情 cover-view 下 writing-mode 真机失效的问题 查看详情 小程序 web-view 中,通过手势不能1次返回上一级页面的问题 查看详情 新访问人数为 20 亿的问题 查看详情 video 组件播放 m3u8 格式地址,在华为 P20 中播放一会儿之后会自动停止的问题 查看详情 小程序文档拓展能力中国际化里的链接问题 查看详情 处于橡皮筋效果时 SetData 会使页面跳动闪屏的问题 查看详情 为什么我画布canvas的内容在开发工具上正常,真机调试的问题 查看详情 微信开放工具报错 Unexpected end of JSON input 的问题 查看详情 子域在开发工具上可以显示 但是在手机上不显示的问题 查看详情 小程序服务商助手的问题 查看详情 canvas 绘制闪烁的问题 查看详情 小程序分享 title 字段带回车键分享标题显示错误的问题 查看详情 wx.onError 捕获不到错误的问题 查看详情 wx.reLaunch 无法重载 webview 页面的问题 查看详情 onShareAppMessage 在模拟器点右上角分享不触发的问题 查看详情 播放临时文件中的 mp3,获取音乐播放时间导致开发工具帧率骤降无法正常工作的问题 查看详情 开发者工具上传代码提示报错的问题 查看详情 JSON.parse 的问题 查看详情 云开发 arrayElemAt 文档示例代码有误的问题 查看详情 iPhone 手机调用 wx.joinVoIPChat 出现问题 查看详情 浮窗中的小程序跳转 wx.navigateBackMiniProg 会关闭所有小程序的问题 查看详情 微信小游戏设置横屏时,iOS 预览黑屏,竖屏时正常显示的问题 查看详情 开发者工具和 Android 真机测试正常,唯独 iPhone 无法正常使用的问题 查看详情 setUserCloudStorage 无法增加 KVDlist 里面的字段的问题 查看详情 iOS 报 at line undefined in undefined 的问题 查看详情 原生 tabBar 在没有文本时可以垂直居中的问题 查看详情 iOS 长按 web-view 中的小程序码无法跳转的问题 查看详情 灰度上线实际用户比例远高于设定值的问题 查看详情 小程序地图,点击 bindmarkertap 会触发 bindtap 的问题 查看详情 微信 android7.0.6 版不能播放 aac 文件的问题 查看详情 video 组件播放完某些视频后,进度条停留在接近结束位置的问题 查看详情 开放域获取未授权用户的数据做排行榜出问题 查看详情 OpenDataContext-wx.getUserInfo 无法获取非好友信息的问题 查看详情 出现报错,打不开小程序的问题 查看详情 数据周期性更新,报错 fail no permission 的问题 查看详情 用户打开页面报错,第一次出现如下图,查了一下不是小程序内部的代码的问题 查看详情 微信进入页面失败的问题 查看详情 图片地址返回301的时候, 相同地址放入 wx.previewImage 无法显示的问题 查看详情 web-view在横竖屏切换后,屏幕高度发生问题 查看详情 css 属性的问题 查看详情 wx.request 和 socket,首次连接和请求特别慢的问题 查看详情 微信小程序打开文档和图片的问题 查看详情 showToast 在导航主页闪动问题 查看详情 1907300开发者工具,getContext('webgl') 返回 null 的问题 查看详情 editor 设置格式后失去焦点同时关闭键盘的问题 查看详情 游戏中匀速运动物体,抖动厉害的问题 查看详情 调用 getUnlimited 接口生成小程序码的问题 查看详情 自定义 tabBar 位置渲染错误的问题 查看详情 App.onError 捕获到 webviewScriptError 错误的问题 查看详情 调用 queryquota 接口一直响应 system error hint 的问题 查看详情 iPhone 触底继续上拉导致置顶内容消失的问题 查看详情 UDP 发包服务端收到的数据和实际发送数据不一致什的问题 查看详情 基础库随着微信更新版本以后,已上线的小程序出现BUG 查看详情 UDP 发送 ArrayBuffer 在开发者工具上无效但真机预览有效的问题 查看详情 animation 动画在真机上移动出现问题 查看详情 scroll-view 包含的自定义组件中 fixed 元素层级问题 查看详情 Uncaught TypeError: Illegal invocation 报错 查看详情 调用 readdirSync 报错 查看详情 Android map 组件首次加载网络图片 marker 会显示为一个红色标记的问题 查看详情 demo 里面的 currentTime = 0 在真机上不起作用的问题 查看详情 需求反馈跟进迭代中scanCode 支持离线模式的需求 查看详情 审核通过之后,支持手机端一键发布的需求 查看详情 需求评估中开发者工具考虑支持 jsdoc 风格的api 说明的需求 查看详情 云开发后台时候能支持 aggregate 的需求 查看详情 通过 API 获取系统通知、用户反馈接口的需求 查看详情 wx.chooseMessageFile 能获取到图片网络路径的需求 查看详情 命令行调用-上传代码,能读取 project.config 的需求 查看详情 开发者工具能用 vue 的插件的需求 查看详情 希望微信提供一个全局的类似 movable-area 的组件 查看详情 startRecord() 能支持实时反馈录制时长的需求 查看详情 小程序审核有个进度条之类的需求 查看详情 audio 可以支持倍速播放的需求 查看详情 小程序运营后台查看每个接口的 request 成功率的需求 查看详情 手机版小程序助手中的体验版,能否默认采用后台设置的路径的需求 查看详情 希望 editor 组件出一个上传音视频的功能 查看详情 云开发本地调试界面建议 查看详情 开发者工具中的体验评分, 体验完评分报告的导出功能的需求 查看详情 云开发希望可以接收消息 wxa_media_check ,用于安全内容检测的需求 查看详情 调试面板的 network 的 cloud 增加数据库 watch 请求的需求 查看详情 image组件后续支持全景照片的需求 查看详情 微信小程序云开发的需求 查看详情 关于“实时数据推送”的几点建议 查看详情 picker 能多选的需求 查看详情 后续云开发会开放服务端的实时数据推送功能的需求 查看详情 IDE 中触发编译时机可以在内存监听到 diff 触发更新的需求 查看详情 OCR 插件返回处理过的的身份证图片地址的需求 查看详情 微信开发者工具全局替换功能的需求 查看详情 开放小程序的键盘功能的需求 查看详情 wx.previewImage 支持横屏的需求 查看详情 template 和 block 标签快捷补充的需求 查看详情 小程序自动化测试 sdk 提供截图功能的需求 查看详情 发布已通过审核的小程序接口,返回目前发布的小程序代码模板 ID 的需求 查看详情 提几个关于onCameraFrame接口(及其相关)的若干个改进建议 查看详情 建议在添加其他账号中新增一个备注填写的需求 查看详情 official-account 支持自定义样式的需求 查看详情 支持 webm 的需求 查看详情 微信团队 2019.09.19
2019-09-19 - 微信OCR能力-证件识别
能力应用 微信提供的OCR证件识别接口能力,可应用于特定行业或场景,需要用户提供证件信息才可以继续使用服务,且需要基于用户的实体证件。开发者通过拍照后,调用api传入,识别证件对应信息。 接口介绍 2.1接口功能 本接口提供基于小程序/H5的身份证OCR识别。 2.2接口限制及申请 1、已认证的订阅号、服务号、企业号、小程序可购买后调用,属于正式开放状态。(2020年4月1日起,每日支持100次免费调用,更多调用次数,需购买后使用) 2、接口购买及指引,可点击此处查看详细描述。 2.3支持证件 身份证、银行卡、行驶证 2.4接口文档 详细接口文档,可以点击此处查阅。 2.5使用TIPS 此接口包含后台接口及小程序插件。后台接口,可搭配小程序/H5的拍照、相册选照等一起使用。可完成证件照片的采集、上传、识别、信息返回等流程,用于需要基于实体证件,采集照片或文字信息等的业务场景。 另提供OCR身份证、银行卡、行驶证识别小程序插件服务,可在微信开放社区插件模块中查看,详细介绍可以点击此处查阅。
2020-03-30 - wxss 编译错误
wxss 编译错误,错误信息:ErrorFileCount[2] path `style/stylesheet.css` not found from `./app.wxss`. path `fonts/font-awesome.min.css` not found from `./app.wxss`. [图片] [图片] [图片]
2017-03-01 - HQChart教程 - 如何快速的创建一个K线图
HQChart介绍 HQChart(https://github.com/jones2000/HQChart)是根据c++行情客户端软件移植到js平台上的一个项目。由金融图形库和麦语法(分析加语法)脚本执行器组成,所有指标计算都在前端完成,后台api只需要提供基础的行情数据就可以。 支持小程序,h5, 分2套代码完成,因为个个平台对canvas的支持和性能是不一样的,所有独立2套代码,最大的发挥个个平台canvas的性能。 支持手势操作(放大缩小,左右移动,十字光标) 。。。。。其他功能我就不介绍了,有兴趣的可以上github上看,上面有详细的使用教程和设计方案 创建K线图步骤 在WXML里面增加一个’canvas’元素, 并设置canvas-id, 定义好手势事件,bindtouchstart,bindtouchmove,bindtouchend [代码]<view class="container"> <canvas class="historychart" canvas-id="historychart" style="height:{{Height}}px; width:{{Width}}px;" bindtouchstart='historytouchstart' bindtouchmove='historytouchmove' bindtouchend='historytouchend'/> </view> [代码] 在对应的页面js文件里面 import HQChart组件 (wechathqchart/umychart.wechat.3.0.js) [代码]import { JSCommon } from "../wechathqchart/umychart.wechat.3.0.js"; [代码] 在page类里面创建HQChart组件,设置参数并绑定到’canvas’元素上 [代码]Page( { data: { Height: 0, Width: 0, }, HistoryChart: null, HistoryOption: { Type: '历史K线图', //如果要横屏类型使用:历史K线图横屏 Windows: //窗口指标 [ { Index: 'MA' }, { Index: 'VOL'}, { Index: 'RSI' } ], //ColorIndex: { Index: '五彩K线-双飞乌鸦' }, //五彩K线 //TradeIndex: { Index: '交易系统-BIAS' }, //交易系统 Symbol: "000001.sz", IsAutoUpate: true, //是自动更新数据 IsShowCorssCursorInfo: true, //是否显示十字光标的刻度信息 CorssCursorTouchEnd:true, //手离开屏幕 隐藏十字光标 KLine: { DragMode: 1, //拖拽模式 0 禁止拖拽 1 数据拖拽 2 区间选择 Right: 1, //复权 0 不复权 1 前复权 2 后复权 Period: 0, //周期 0 日线 1 周线 2 月线 3 年线 MaxReqeustDataCount: 1000, //数据个数 PageSize: 30, //一屏显示多少数据 Info: ["业绩预告", "公告", "互动易", "调研", "大宗交易", "龙虎榜"], //信息地雷 //Policy: ["30天地量", "20日均线,上穿62日均线"], //策略信息 InfoDrawType:0, DrawType: 0, }, //叠加股票 Overlay: [ //{Symbol:'000001.sz'} ], KLineTitle: //标题设置 { IsShowName: true, //显示股票名称 IsShowSettingInfo: true, //显示周期/复权 }, Border: //边框 { Left: 1, //左边间距 Right: 1, //右边间距 Top:30, Bottom:25, }, Frame: //子框架设置 [ { SplitCount: 3 }, { SplitCount: 2 }, { SplitCount: 2 }, ], ExtendChart: //扩展图形 [ { Name: 'KLineTooltip' } ], }, onLoad: function () { var self = this // 获取系统信息 wx.getSystemInfo({ success: function (res) { console.log(res); // 可使用窗口宽度、高度 //console.log('height=' + res.windowHeight); //console.log('width=' + res.windowWidth); self.setData({ Height: res.windowHeight, Width: res.windowWidth, Title: { Display:'block'}}); } }); }, onReady: function () { //创建历史K线类 var element = new JSCommon.JSCanvasElement(); element.ID = 'historychart'; element.Height = this.data.Height; //高度宽度需要手动绑定!! 微信没有元素类 element.Width = this.data.Width; this.HistoryChart = JSCommon.JSChart.Init(element); //把画布绑定到行情模块中 this.HistoryChart.SetOption(this.HistoryOption); }, //把画图事件绑定到hqchart控件上 historytouchstart: function (event) { if (this.HistoryChart) this.HistoryChart.OnTouchStart(event); }, historytouchmove: function (event) { if (this.HistoryChart) this.HistoryChart.OnTouchMove(event); }, historytouchend: function (event) { if (this.HistoryChart) this.HistoryChart.OnTouchEnd(event); }, }) [代码] 这样1个K线图+指标图就完成了 效果图 [图片] [图片] HQChart代码地址 地址: https://github.com/jones2000/HQChart
2019-06-15 - 小程序入门018~小程序列表背景颜色交替显示 表格背景颜色交替
最近做小程序,有这个一个需求,就是列表里的条目背景色要实现交替颜色显示。比如奇数列显示红色,偶数列显示绿色。比如像下面这样。 [图片][图片] 经过一番研究,发现借助css3的nth-child() 选择器可以很好的实现,颜色交替效果。 如我们定义如下index.wxml [代码]<!--index.wxml--> <view class='root' wx:for="{{list}}" wx:key="item"> <view class='item'>{{item}}</view> </view> [代码]在index.js里定义如下数据。 [代码]//index.js Page({ data: { list: [1, 2, 3, 4, 5, 6, 7, 8, 9] } }) [代码]实现如下效果1,奇数列红色,偶数列绿色背景,定义如下wxss [代码]/**index.wxss**/ .root:nth-child(1n+0) { background: red; } .root:nth-child(2n+0) { background: green; } .item { width: 100%; height: 40px; } [代码]效果图如下 [图片][图片] 2,三种颜色的交替 [图片][图片] 可以看到我们借助nth-child() 选择器可以很好的实现列表中背景颜色交替实现的效果。 有任何关于小程序的问题可以微信交流 2501902696(备注小程序)
2019-06-12 - wepy 2.x开发小程序中遇到的一些注意事项
1、wx小程序中,只有text标签可以解析 、\n。 在view中会直接打印出出来。 2、使用原生的setData会导致数据不同步,若使用,取数据时需要注意先同步数据 3、使用小程序textarea组件,解决穿透问题时,可以在下方组件使用cover-view,子组件支持使用button。 4、7.0.0以前版本cover-view使用存在bug,如出现问题,建议客户更新微信 但是不能给button直接设置动态标题,标题需要使用cover-view再包一层才生效,否则动态标题展示不全;没有动态参数的标题可以直接设置 5、new Date将字符串转化成时间的时候,ios系统,日期格式需要是’/‘隔开的,否则会转化失败,安卓系统’-‘或者’/'都可兼容
2019-06-13 - 随手写的一个css头像轮播,代码较菜,不喜勿喷.哈哈哈
[图片] 大概就长这样样子,可以向左自动轮播的那种,主要就是用到了css的延迟transition-delay这个属性,然后配合定时器做的一个css的轮播.哈哈哈,然后没了,欢迎各位改造,指出里面的错误.觉得想用的朋友就拿去用吧 代码链接:https://developers.weixin.qq.com/s/ox93u6m17W9X
2019-06-13 - 使用Puppeteer搭建统一海报渲染服务
背景介绍 有赞微商城包括了 PC 端、H5 端和小程序端,每个端都有绘制分享海报的需求。最早的时候我们是在每个端通过canvas API来绘制的,通过canvas绘制有很多痛点,与本文要讲的海报渲染服务做了一个对比: [图片] 正是因为这些痛点问题,有同事就提出基于Puppeteer实现一个公共的海报渲染服务,使用方只需传入海报图片的html,海报渲染服务绘制一张对应的图片作为返回结果,解决了canvas绘制的各种痛点问题。 一、Puppeteer 是什么 Puppeteer是谷歌官方团队开发的一个 Node 库,它提供了一些高级 API 来通过DevTools协议控制Headless Chrome或Chromium。通俗的说就是提供了一些 API 用来控制浏览器的行为,比如打开网页、模拟输入、点击按钮、屏幕截图等操作,通过这些 API 可以完成很多有趣的事情,比如本文要讲的海报渲染服务,它用到的就是屏幕截图的功能。 二、Puppeteer 能做什么 Puppeteer几乎能实现你能在浏览器上做的任何事情,比如: 生成页面的屏幕截图或pdf 自动化提交表单、模拟键盘输入、自动化单元测试等 网站性能分析:可以抓取并跟踪网站的执行时间轴,帮助分析效率问题 抓取网页内容,也就是我们常说的爬虫 三、海报渲染服务 3.1方案设计 首先我们来看一下海报渲染服务的流程图: [图片] 其实整个流程还是比较简单的,当有一个绘制请求时,首先看之前是否已经绘制过相同的海报了,如果绘制过,就直接从Redis里取出海报图片的 CDN 地址。如果海报未曾绘制过,则先调用Headless Chrome来绘制海报,绘制完后上传到 CDN,最后 CDN 上传完后返回 CDN 地址。整个流程的大致代码实现如下: [代码]const crypto = require('crypto'); const PuppeteerProvider = require('../../lib/PuppeteerProvider'); const oneDay = 24 * 60 * 60; class SnapshotController { /** - 截图接口 * - @param {Object} ctx 上下文 */ async postSnapshotJson(ctx) { const result = await this.handleSnapshot(); ctx.json(0, 'ok', result); } async handleSnapshot() { const { ctx } = this; const { html } = ctx.request.body; // 根据 html 做 sha256 的哈希作为 Redis Key const htmlRedisKey = crypto.createHash('sha256').update(html).digest('hex'); try { // 首先看海报是否有绘制过的 let result = await this.findImageFromCache(htmlRedisKey); // 命中缓存失败 if (!result) { result = await this.generateSnapshot(htmlRedisKey); } return result; } catch (error) { ctx.status = 500; return ctx.throw(500, error.message); } } /** - 判断kv中是否有缓存 * - @param {String} htmlRedisKey kv存储的key */ async findImageFromCache(htmlRedisKey) { } /** - 生成截图 * - @param {String} htmlRedisKey kv存储的key */ async generateSnapshot(htmlRedisKey) { const { ctx } = this; const { html, width = 375, height = 667, quality = 80, ratio = 2, type: imageType = 'jpeg', } = ctx.request.body; this.validator .required(html, '缺少必要参数 html') .required(operatorId, '缺少必要参数 operatorId'); let imgBuffer; try { imgBuffer = await PuppeteerProvider.snapshot({ html, width, height, quality, ratio, imageType }); } catch (err) { // logger } let imgUrl; try { imgUrl = await this.uploadImage(imgBuffer, operatorId); // 将海报图片存在 Redis 里 await ctx.kvdsClient.setex(htmlRedisKey, oneDay, imgUrl); } catch (err) { } return { img: imgUrl || '', type: IMAGE_TYPE_MAP.CDN, }; } /** - 上传图片到七牛 * - @param {Buffer} imgBuffer 图片buffer */ async uploadImage(imgBuffer) { // upload image to cdn and return cdn url } } module.exports = SnapshotController; [代码] 3.2遇到的问题 2.3.1 Chromium启动和执行流程 最开始一个版本我们是直接Puppeteer.launch()返回一个浏览器实例,每次绘制会用单独的一个浏览器实例,这个在使用过程中发现绘制海报会很慢,后面优化时找到了这篇文章:Puppeteer性能优化与执行速度提升,这篇文章提到了两个优化点:1. 优化Chromium启动项;2. 优化Chromium执行流程。 先说优化Chromium启动项,这个就是为了我们启动一个最小化可用的浏览器实例,其他不需要的功能都禁用掉,这样会大大提升启动速度。 [代码]const browser = await puppeteer.launch({ args: [ '–disable-gpu', '–disable-dev-shm-usage', '–disable-setuid-sandbox', '–no-first-run', '–no-sandbox', '–no-zygote', '–single-process' ] }); [代码] 再来说说浏览器的执行流程,最开始我们是每次绘制都会用单独一个浏览器,也就是一对一,这个在压测的时候发现CPU和内存飙升,最后我们改用了复用浏览器标签的方式,每次绘制新建一个标签来绘制。 [代码]const page = await browser.newPage(); page.setContent(html, { waitUntil: 'networkidle0' }); const imageBuffer = await page.screeshot(options); [代码] 3.3.2 networkidle0 最开始我们的海报服务绘制海报时有时候会偶尔出现图片展示不出来的情况,我们排查后发现是因为我们setContent时,使用的是默认的load事件来判断设置内容成功,而我们期望的是所有网络请求成功后才算设置内容成功。 [代码]page.setContent(html); [代码] Puppeteer在setContent和goto等方法里提供了一个waitUntil的参数,它就是用来配置这个判断成功的标准,它提供了四个可选值: load:默认值,load事件触发就算成功 domcontentloaded:domcontentloaded事件触发就算成功 networkidle0:在500ms内没有网络连接时就算成功 networkidle2:在500ms内有不超过2个网络连接时就算成功 我们这里需要用到的就是networkidle0: [代码]page.setContent(html, { waitUntil: 'networkidle0' }); [代码] 当改成networkidle0后,使用方给我们反馈说整个绘制服务变慢了很多,随随便便都2s以上。变慢主要是因为加上networkidle0后,至少需要等待500ms以上,加上绘制的一些其他开销,基本上就需要2s了。所以我们期望这个500ms是可配置的,因为500ms实在太长了,我们的分享海报一般只有几张图片,不需要这么久。但是Puppeteer没有提供相关的参数,还好在issue中早已经有人提出了这个问题:Control networkidle wait time [代码]function waitForNetworkIdle(page, timeout, maxInflightRequests = 0) { page.on('request', onRequestStarted); page.on('requestfinished', onRequestFinished); page.on('requestfailed', onRequestFinished); let inflight = 0; let fulfill; let promise = new Promise(x => fulfill = x); let timeoutId = setTimeout(onTimeoutDone, timeout); return promise; function onTimeoutDone() { page.removeListener('request', onRequestStarted); page.removeListener('requestfinished', onRequestFinished); page.removeListener('requestfailed', onRequestFinished); fulfill(); } function onRequestStarted() { ++inflight; if (inflight > maxInflightRequests) clearTimeout(timeoutId); } function onRequestFinished() { if (inflight === 0) return; --inflight; if (inflight === maxInflightRequests) timeoutId = setTimeout(onTimeoutDone, timeout); } } // Example await Promise.all([ page.goto('https://google.com'), waitForNetworkIdle(page, 500, 0), // equivalent to 'networkidle0' ]); [代码] 利用这个函数我们就可以传入自己想要的超时时间了。 3.2.3 Chromium定时刷新机制 为什么需要定时刷新Chromium呢?总不可能一直用同一个Chromium实例吧,万一变卡或者crash了,就会影响海报的绘制。所以我们需要定时的去刷新当前的浏览器实例。 [代码]class PuppeteerProvider { constructor() { this.browserList = []; } /** - 初始化`puppeteer`实例 */ initBrowserInstance() { Array.from({ length: browserConcurrency }, () => { this.checkBrowserInstance(); }); // 每隔30分钟刷新一下浏览器 this.refreshTimer = setTimeout(() => this.refreshOneBrowser(), thrityMinutes); } /** - 检查是否还需要浏览器实例 */ async checkBrowserInstance() { if (this.needBrowserInstance) { this.browserList.push(this.launchBrowser()); } } /** - 定时刷新浏览器 */ refreshOneBrowser() { clearTimeout(this.refreshTimer); const browserInstance = this.browserList.shift(); this.replaceBrowserInstance(browserInstance); this.checkBrowserInstance(); // 每隔30分钟刷新一下浏览器 this.refreshTimer = setTimeout(() => this.refreshOneBrowser(), thrityMinutes); } /** - 替换单个浏览器实例 * - @param {String} browserInstance 浏览器promise - @param {String} retries 重试次数,超过这个次数直接关闭浏览器 */ async replaceBrowserInstance(browserInstance, retries = 2) { const browser = await browserInstance; const openPages = await browser.pages(); // 因为浏览器会打开一个空白页,如果当前浏览器还有任务在执行,一分钟后再关闭 if (openPages && openPages.length > 1 && retries > 0) { const nextRetries = retries - 1; setTimeout(() => this.replaceBrowserInstance(browserInstance, nextRetries), oneMinute); return; } browser.close(); } launchBrowser(opts = {}, retries = 1) { return PuppeteerHelper.launchBrowser(opts).then(chrome => { return chrome; }).catch(error => { if (retries > 0) { const nextRetries = retries - 1; return this.launchBrowser(opts, nextRetries); } throw error; }); } } [代码] 这里还有一个点,我们给replaceBrowserInstance这个方法加了个重试次数的限制,当超出这个限制后不管有没有任务在进行都会关闭浏览器。这个是防止在某些特殊情况不能关闭掉浏览器,导致内存无法释放的情况。 四、展望 目前海报渲染服务的问题就是qps比较低,因为Chromium消耗最多的资源是CPU,当并发数变高时,CPU也随之变高,就会导致后面的绘制变慢。在4核8G的情况,大概是20qps左右。后面的主要精力就是如何去提升单机的qps,应该还有比较大的空间。还有就是看看能不能增加定时任务,在凌晨机器比较闲的时候提前绘制好一些常用的海报,这样当需要海报时就是直接从redis里取出来了,充分利用了机器的性能,也可以减少海报服务白天的压力。 五、相关链接: Puppeteer 性能优化与执行速度提升:https://blog.it2048.cn/article-puppeteer-speed-up/ Control networkidle wait time:https://github.com/GoogleChrome/puppeteer/issues/1353
2019-06-13 - 小程序数据生命周期
结合自己在平时的开发中遇到的各种问题,和浏览各种问题的解决方案总结出一些自己在日常开发中常用的技巧和知点,希望各位不吝斧正。 1.短生命周期数据存储 以小程序启动到彻底关闭为周期的的数据建议存储在app.js文件夹中,引用app.js: [代码]const app =getApp(); [代码] 假设Value是在小程序本次生命周期中经常使用到的一个数据,比如说请求API的Token,动态的令牌等。那么就可以把这个值赋值到全局变量中去。实际上,并不是只有app.js中的globalData是全局变量,可以自己定义数据集。 [代码]App({ eduOS:{ token:'' }, ... }) [代码] 对于app.js里面的token进行赋值操作很简单,只要页面引用了app.js [代码]app.eduOS.token = Value; [代码] 这个数据在小程序的本次启动到彻底关闭后台的周期中就是长期存在的了,还可以根据需要进行修改,Value可以是对象。 2.长生命周期或者隐私数据存储 这种数据的显著特点是在小程序关闭再次重启后依然存在,或者涉及到用户的隐私信息但是需要复用,这种时候可以用本地缓存来解决这种问题。 本地缓存的生命周期: 小程序被开始使用 -----> 小程序被彻底从使用列表中移除。 小程序设置缓存的方式: [代码]wx.setStorage({ key: 'educookie', data: { xh: that.data.xh, pwd: that.data.pwd } }) [代码] 小程序获取缓存的方式: [代码] var that = this; wx.getStorage({ key: 'educookie', success: function(res) { that.setData({xh:res.data.xh,pwd:res.data.pwd}); }, }) [代码] 比如保存用户的登陆态信息,但是不能保存用户的隐私数据,就可以采用这种方式。 或者是一个非时效性的数据,可以通过这种方式进行存储。 3.动态信息或配置信息存储 保存用户的配置信息,在更换手机时能迅速完成配置同步。 商家小程序推荐商品修改,或者是内容修正,或者是增加活动,不可能每次都要重写然后再次让小程序进行审核。 对此,可以在后端服务器中保存这个信息。 以一个小程序的轮播广告牌为例: [代码]{ ad1:'imgurl1', ad2:'imgurl2', ad3:'imgurl3' } [代码] 把这个数据存放在后台服务器,每一次刷新该页面都请求一次后台数据,对内容进行修改。 [代码]wx.request({ url:'XXX', data:{}, success(res){ that.setData({ adList:res.data }) } }) [代码] 类似这种方式,完成对一些数据的动态控制或者是云同步。 4.页面间数据传递 页面间之间的数据传递一般是简单的,这种类型的数据的生命周期是一次性的,用完即删。 [代码]wx.navigatorTo({ url:'../index/index?param1=value1¶m2=value2' }) [代码] [代码]//在index页面获取 onLoad(options){ console.log(options.param1);//value1 } [代码] 可以参照Http请求中的Get表单传参方式来写页面之间的传参。 如果需要传送的数据太多,可通过Map<key ,Storge>进行传递 [代码]wx.setStorge({ key:'xxx', data:mydata }) [代码] 传递参数只需要传递一个key,在其他界面通过这个key再次去获取本地缓存,对于这种类型的数据,建议使用完后即时的删除缓存。 [代码]wx.removeStorage({ key: 'xxx', success(res) { console.log(res) } }) [代码] 返回携带参数方法 [代码]wx.navigateBack({ }) [代码] 共两种方式 *1. 全局变量 和 Storage * [代码]const app = getApp(); page({ app.globalData.isBackvalue = ture;//确定是返回的值 app.globalData.tmpData = value;//你要传递的值,也可以设置缓存 }) [代码] 在上一个页面获取 [代码]const app = getApp(); ... onShow(){ if(app.globalData.isBackValue){ this.setData({ XXX:app.globalData.tmpData }) //或者是通过获取缓存的方法进行赋值 } } [代码] 2. 页面栈 [代码]var allpages = getCurrentPages();//获取全部页面数据 //栈的下标从 0 开始,进入页面第一个加载的页面数据是 allpages[0],当前页面是allpages[allpages.length - 1], 上一个页面是allpages[allpages.length - 2] var prepagedata = allpages[allpages.length - 2].data;//获取上一页面的数据。 var prepage = allpages[allpages.length - 2];//获取上一页面,包括数据和方法 //设置数据方法 prepage.setData({ XXX:value //XXX 是上个页面data中的参数,value是要设置的值 }) //调用函数方法,prepage中必须定义callfunction函数才可以调用 prepage.callfunction(); [代码] 开发联系Q 1025584691
2019-06-30 - 小程序之「 navigateBack 」公共页面案例
navigateBack的用法 [代码] wx.navigateBack({ delta: backSize // backsize表示关闭几页,从页面栈中移除几个(包含本页面) }) [代码] 项目中的需求 「 需求 」:做一个公共结果页,从其他地方跳转到这个页面,最后点击"完成按钮或者右上角返回"后,回到想回到的页面,并且控制目标页面刷新还是不刷新。 「 分析 」:想实现回到想回到的页面,通过对页面栈的管理来实现。 「 举例 」:页面栈中有1-2-3-4-5,5是公用页面,现在想跳到1,那么就可以通过navigateBack的delta值等于4来控制回到1,我们就动态改变delta的值实现动态跳转,navigateBack会让页面出栈,这样就很好地维护了页面栈。 具体思路 通过getCurrentPages( )能够拿到当前小程序页面栈的数组,数组中有个key为route,该值即是栈里页面地址。要跳转到1,4传值给5,5中通过遍历页面栈数组,判断传过来的值是否等于页面栈中的route值,相等的话,记录index,这样即可完成。 最终效果 无论是触发了ios滑屏退出、android物理按钮返回、小程序左上角返回按钮,都会回到目标页面。 但是,经真机测试后发现。点击左上角返回、手机物理返回键、滑屏返回到目标页面,效果上会关闭两个页面。但最终也可以回到目标页面。只能说功能达到了,效果上没那么完美。这没办法。毕竟页面卸载的处理本身就是一个伪处理-_- 代码说话 这是项目中用到的公用页面,就是通过navigateBack来控制跳转 注释写得很清楚了 想要更清晰地了解页面栈是怎样的,就打断点看一看 有问题欢迎留言讨论😄 [代码]Page({ data: { isInvoke: false,// 是否调用过按钮 isUnload: true,// 是否是卸载的生命周期 resultImg: "", // 图标(页面内使用) result: { title: "",// 必传(页面标题) type: 1,// 必传(0:失败 1:成功) url: "", // 必传(跳转的url),传空或者不在页面栈中,跳转到首页或指定页面 // 选传(上面文案布局,成功默认是:成功,失败默认:失败) resultUp: "", // 选传(下面文案布局,不传默认是空 resultDown: "", // 选传(按钮名称,成功默认:完成,失败默认:重试) btnName: "", // 选传(是否刷新目标页,默认刷新) isRefresh: true, } }, /** 页面初始化 */ onLoad: function (options) { }, onShow: function () { this.initData(); }, /** 页面卸载*/ onUnload: function () { // 如果点击了按钮,就不再调用它 if (!this.data.isInvoke) { this.data.isUnload = true this.targetJump() } }, /** 初始化值 */ initData: function () { var result = wx.getStorageSync("result") if (result) { // 页面标题 wx.setNavigationBarTitle({ title: result.title }) var type = result.type this.data.result.url = result.url if (typeof (result.isRefresh) != "undefined") { this.data.result.isRefresh = result.isRefresh } if (type) { // 成功 result.btnName = result.btnName ? result.btnName : "完成" this.data.resultImg = "/resources/success.png" result.resultUp = result.resultUp ? result.resultUp : "成功" } else {// 失败 result.btnName = result.btnName ? result.btnName : "重试" this.data.resultImg = "/resources/failed.png" result.resultUp = result.resultUp ? result.resultUp : "失败" } var btnName = "result.btnName" var resultUp = "result.resultUp" var resultDown = "result.resultDown" this.setData({ resultImg: this.data.resultImg, [resultUp]: result.resultUp, [resultDown]: result.resultDown ? result.resultDown : "", [btnName]: result.btnName }) } }, /** 按钮点击事件 */ btnClick: function () { this.data.isInvoke = true this.data.isUnload = false this.targetJump(); }, /** 目标页面跳转 */ targetJump: function () { // 清除缓存 wx.removeStorageSync("result") var pages = getCurrentPages() // 如果url为空或者不在页面栈中,返回首页 var backSize = 100 var target console.log(pages) for (var i = 0;i < pages.length;i++) { if (this.data.result.url.substring(1) == pages[i].route) { // 自身也在栈中,所以要-1 backSize = pages.length - i - 1; target = pages[i] console.log("target", target) break } } console.log("backSize", backSize) if (backSize == 100) {// 首页或者指定页面 wx.reLaunch({ url: this.data.result.url, }) return } if (this.data.result.isRefresh) { // 刷新目标页面 target.onLoad() } if (this.data.isUnload) {// 用户点击了返回键 // 如果目标页面在页面栈中倒数第二个位置,返回 if (backSize == 1) { return } // 如果只是调用了小程序返回键,因为back键默认会关掉一页 backSize = backSize - 1 } wx.navigateBack({ delta: backSize }) } }) [代码] 以上
2019-05-28 - 借助云开发实现小程序模版消息推送(不用搭建服务器就可以实现微信消息推送)
上一节给大家将了借助云开发实现小程序支付功能,那么我们就要想了,能不能借助云开发实现小程序消息推送功能呢? 还别说,云开发还真能实现推送的功能。 一直关注我的同学肯定知道老师之前也写过借助java后台实现小程序消息推送的文章。 我们借助java后台虽然也能轻松的实现消息推送。但是呢?用java开发后台推送,必须要搭建服务器,学习java代码,部署java代码当然你就是做java开发的,或者学习过java,这没什么。 但是作为小程序开发人员来说,用java显得太重了。 传送门: 《借助小程序云开发实现小程序支付功能(含源码)》 《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 下面就来教大家如何借助云开发实现小程序模版消息的推送功能。 老规矩,先看效果图 [图片] 下面来讲实现步骤 一,定义推送的云函数 由于我们的云推送功能只能在云函数里调用,所以我们这里必须要在云函数里实现推送功能。 1,首先我们定义一个云函数push0524。 如果你还不知道如何使用云开发,如何定义云函数,去翻下老师之前的文章。有写的。 [图片] 把完整的代码贴给大家 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { console.log(event) return sendTemplateMessage(event) } //小程序模版消息推送 async function sendTemplateMessage(event) { const { OPENID } = cloud.getWXContext() // 接下来将新增模板、发送模板消息、然后删除模板 // 注意:新增模板然后再删除并不是建议的做法,此处只是为了演示,模板 ID 应在添加后保存起来后续使用 const addResult = await cloud.openapi.templateMessage.addTemplate({ id: 'AT0002', keywordIdList: [3, 4, 5] }) const templateId = addResult.templateId //新增的模版id const sendResult = await cloud.openapi.templateMessage.send({ touser: OPENID, templateId, formId: event.formId, page: 'pages/index/index', data: { keyword1: { value: '云开发实现推送', }, keyword2: { value: '2019 年 5 月 24 日', }, keyword3: { value: '编程小石头', }, } }) //删除模版id await cloud.openapi.templateMessage.deleteTemplate({ templateId, }) return sendResult } [代码] 上面代码所实现的就是 1,创建模版,拿到模版id 2,使用模版ID,填充模版消息,发送模版 3,删除模版。 我们正常开发时,模版都是在小程序后台获取到的。这里是为例演示方便。所以正常开发时,只需要实现第二步就行了。 推送的关键代码就是这个方法: cloud.openapi.templateMessage.send 通常我们定义完push0524云函数以后,如果直接调用的话,会报错误的。 [图片] 来看下这个错误,看到红色框里的permission就知道,肯定是权限的问题。所以我们在定义完云函数以后,要在push0524云函数下面添加权限配置页面。如下图 [图片] 重要的就是这个: “templateMessage.send”, 推送权限。因为推送是云开发给我们提供的,我们这里调用时,必须配置相关权限,才能使用的。 到这里我们的推送功能就实现了。下面我们来验证下。 二,验证云开发推送 验证其实很简单,和我们之前的《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 类似。只不过一个是在java后台推送,一个是在小城里推送。下面我们简单写个小程序里验证推送的demo。 功能很简单 1,获取formid,因为推送必须有formid的 2,点击调用push0524实现推送 [图片] 简单的贴下代码 [图片] [图片] 需要注意的一点:我们测试时,必须要真机测试。因为模拟器没法获取到formid的。 [图片] 我们在推送成功的success回调中打印下log。如果log中出现,send:ok字样,就代表我们推送成功了。来看下推送成功的效果。 微信聊天列表接收到了消息提醒 [图片] 消息内容 [图片] 到这里我们就用云开发实现完整的消息推送功能了。是不是很简单。 有任何关于编程的问题都可以加老师微信 2501902696(备注小程序)也可以找老师索要完整源码。 编程小石头码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 借助小程序云开发实现小程序支付功能(含源码)
我们在做小程序支付相关的开发时,总会遇到这些难题。小程序调用微信支付时,必须要有自己的服务器,有自己的备案域名,有自己的后台开发。这就导致我们做小程序支付时的成本很大。本节就来教大家如何使用小程序云开发实现小程序支付功能的开发。不用搭建自己的服务器,不用有自己的备案域名。只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1,云开发的部署和使用 2,支付相关的云函数开发 3,商品列表 4,订单列表 5,微信支付与支付成功回调 支付成功给用户发送推送消息的功能会在后面讲解。 下面就来教大家如何借助云开发使用小程序支付功能。 支付所需要用到的配置信息 1,小程序appid 2,云开发环境id 3,微信商户号 4,商户密匙 一,准备工作 1,已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中,【设置】 →【开发设置】 下可以获取微信小程序 AppID 和 Secret。 [图片] 2,微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中,【账户中心】→【商户信息】 下可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 下可以设置商户密钥。 [图片] 这里特殊说明下,个人小程序是没有办法使用微信支付的。所以如果想使用微信支付功能,必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3,微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4,开通小程序云开发功能:https://edu.csdn.net/course/play/9604/204526 二,商品列表的实现 效果图如下,由于本节重点是支付的实现,所以这里只简单贴出关键代码。 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view> </view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息,然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看我的历史博客,有写过的) 也有视频讲解: https://edu.csdn.net/course/detail/9604 [图片] 三,支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解。 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1,配置config下的index.js, 这一步所需要做的就是把小程序appid,云开发环境ID,商户id,商户密匙。填进去。 [图片] 2,配置入口云函数 [图片] 详细代码如下,代码里注释很清除了,这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init() const app = require('tcb-admin-node'); const pay = require('./lib/pay'); const { mpAppId, KEY } = require('./config/index'); const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res = require('./lib/res'); const ip = require('ip'); /** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */ exports.main = async function(event, context) { const { type, data, userInfo } = event; const wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database(); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order'); // 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo = `${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; //'127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spbill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }) .get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了。 [图片] 再来看下,支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表,支付状态,支付成功后的回调。 今天就先讲到这里,后面会继续给大家讲解支付的其他功能。比如支付成功后的消息推送,也是可以借助云开发实现的。 由于源码里涉及到一些私密信息,这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言。单独找我要源码也行(微信2501902696) 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 自定义标题栏
使用效果 [图片][图片][图片][图片] 使用方法 属性介绍 属性名 类型 默认值 是否必须 说明 menuSrc String ‘’ 否 按钮图片地址 bgImgSrc String ‘’ 否 背景图片地址 bgImgMode String aspectFill 否 背景图片的显示模式 title String ‘’ 否 标题 titleTextColor String ‘’ 否 字体和按钮以及loading图标的颜色,按钮和loading暂时只有黑白2色 backgroundColor String ‘’ 否 整个标题栏的背景颜色 loading Boolean false 否 是否是加载状态 backProxy Boolean false 否 是否重写了返回键 标题栏中属性的默认数据会自动获取json配置以及系统的默认数据,如果不需要动态更改样式,可以在json中设置,组件中同样起作用 事件介绍 属性名 detail NaviBack 返回的逻辑方法 MenuTap 按钮的点击事件 [代码]"usingComponents": { "toolBar": "/component/toolbar" }, [代码] [代码]<toolBar menuSrc='/image/menu_white.png' bindMenuTap='onMenuTap' bgImgSrc='/image/navi-bg.jpg' /> [代码] 高度说明: 为了方便适配,这里给出自定义标题栏的计算公式: const MenuRect = wx.getMenuButtonBoundingClientRect() const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; const height = (MenuRect.top - statusBarHeight) * 2 + MenuRect.height +MenuRect.top Github地址:https://github.com/Aracy/wx-mini-navigationbar
2019-05-21 - DatePicker 年月日时分秒 任你选
DatePicker 微信上的时间选择,有的时候你会发现,你不能同时选择日期和时间,而且时间不能选到秒。DatePicker让你想选什么选什么… Mode DatePicker分为四个mode:YMDhms(年月日时分秒)、YMD(年月日)、MD(月日)、hm(时分)。 我自己觉得用起来很爽快。 效果图 mode:YMDhms (年月日时分秒) [图片] mode:YMD(年月日) [图片] mode:MD (月日) [图片] mode:hm (时分) [图片] gitHub地址
2019-05-11 - 小程序转支付宝小程序工具:wx2my
背景目前市面上有很多微信小程序,同时开发者开发完微信小程序后,希望可以同时发布到支付宝小程序平台上,可惜微信小程序并不能直接发布到支付宝平台上,两个平台小程序不兼容。因此开发者需要对微信小程序代码进行修改,调整成支付宝小程序代码。 庆幸的事两种小程序代码有很多相似之处,手动修改比较繁琐,因此小程序助手孕育而生。达到自动把微信小程序代码转换成支付宝小程序。不过由于两种小程序功能和api等的不一致,转换后生成的支付宝小程序并不能直接运行起来,还需要进行代码检查,手动的修改无法转换的部分。 地址 vscode插件: wx2my(微信小程序转支付宝小程序) cli命令工具: wx2my npm地址 使用文档: wx2my 语雀地址 目标 快速转换微信小程序为支付宝小程序,达到快速转换,降低转换成本,这样可以早点下班。 视频教程[视频] 能力 文件名转换app文件名转换: 微信小程序 --> wx2my --> 支付宝小程序 app.json app.json app.js app.js app.wxss app.acss page页面、自定义组件文件名转换: 微信小程序 --> wx2my --> 微信小程序 index.json index.json index.js index.js index.wxml index.axml index.wxss index.acss 其他类型文件名转换: 微信小程序 --> wx2my --> 支付宝小程序 parse.wxs parse.sjs 其他类型文件(图片、视频等) 直接复制,不转换 文件内容转换app.json 转换 app.json文件为整个小程序配置文件,不过微信小程序app.json和支付宝小程序在app.json配置文件支持的能力不完全一致,部分一致的但名称不一致的配置,转换工具会分析并转换出来。 转换方式: navigationBarTitleText --> defaultTitle enablePullDownRefresh --> pullRefresh navigationBarBackgroundColor --> titleBarColor ...等 其中微信小程序支持,支付宝小程序不支持的,需要开发者自己手动修改,如:networkTimeout、functionalPages、workers等 全局组件转换 微信小程序支持全局组件,即在app.json中添加usingComponents字段,这样在小程序内的页面或自定义组件中可以直接使用全局组件而无需再声明。 转换方式: 转换工具会分析小程序中所有页面和组件,找到那些使用了全局组件的页面和组件,并把全局组件声明在页面或组件的json文件中,当做普通组件引用和使用。同时把全局组件的声明删除。 wxml文件转换 转换逻辑是以wx:xxx开头的,替换为a:xxx方式。 a. 事件相关的转换,微信中 bindeventname 或 bind:eventname 转换为 onEventname, 如下: 转换前: <page bindtap="showName" bind:input = "actionName" catchchange="catchchange"bindtouchend="onTouchEnd"></page> 转换后: <page onTap="showName" onInput = "actionName" catchChange="catchchange" onTouchEnd="onTouchEnd"></page> b: 循环语句转换, 如下: 转换前: <view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName" wx:key="unique">{{idx}}: {{itemName.message}}</view> 转换后: <view a:for="{{array}}" a:for-index="idx" a:for-item="itemName" a:key="unique"> {{idx}}: {{itemName.message}}</view> c: wxs代码转换,微信小程序中的wxs功能对应支付宝小程序中的sjs功能,微信wxml中支持引用外部wxs文件和内联wxs代码,支付宝中只支持引用外部文件方式使用sjs,不支持内联sjs代码。 转换方式:转换工具分享所有wxml文件,找到wxs内联代码,提取wxs的内联代码,生成sjs文件,并使用外部引用的方式引入sjs文件,如下: 转换前: <wxs src="../wxs/utils.wxs" module="utils" /><wxs src="../wxs/utils.wxs" module="utils"> </wxs><wxs module="parse"> module.exports.getMax = getMax;</wxs> 转换后: <import-sjs from="../wxs/utils.sjs" name="utils" /><import-sjs from="../wxs/utils.sjs" name="utils"/><import-sjs from="./parse.sjs" name="sjsTest" />并在同级目录下创建了 [代码]parse.sjs[代码] 文件,并转换wxs的CommonJS为ESM parse.sjs文件内容: export default { getMax }; d: 无法替换完成的,在转换后的支付宝小程序的代码中,插入注释代码,提醒开发者并需要开发者手动检查修改。如下: 转换前: <cover-image class="img" src="/path/to/icon_play" bindload="bindload" binderror="binderror" aria-role="xxx"aria-label="xxx"/> 转换后: <cover-image class="img" src="/path/to/icon_play" bindload="bindload" binderror="binderror" aria-role="xxx"aria-label="xxx"/><!-- WX2MY: 属性bindload、binderror、aria-role、aria-label不被支持,请调整 --> 出现这种情况,开发者可以手动的搜索 [代码]WX2MY:[代码] 关键字,查找需要修改的代码 js文件转换 转换工具对api相关的调用转换使用了桥接文件 [代码]wx2my.js[代码] ,在所有js文件顶部引入wx2my.js文件,对api的调用,使用桥接函数,桥接函数对api参数不一致的地方在函数内部进行处理,如下: 转换前: wx.request(opts) 转换后: wx2my.request(opts) [代码]wx[代码] 转换为 [代码]wx2my[代码] ,其中wx2my为前进函数对外的方法 桥接函数中 [代码]request[代码] 的方法如下: [图片]
2019-04-17 - 使用云开发接入阿里云短信SDK,实现自给自足!
发送手机短信验证码 按需求自行修改函数内容 1.前往阿里云申请短信服务 1.短信服务 > 国内消息 > 添加签名 [图片] 1.短信服务 > 国内消息 > 添加模板 [图片] 2.引入 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const Core = require('@alicloud/pop-core'); const accessKeyId = 'xxx' // 你的appid const accessKeySecret = 'xxx' // 你的secret const SignName = 'xxx' // 你的签名 const TemplateCode = 'xxx' // 你的模版CODE var client = new Core({ accessKeyId, accessKeySecret, endpoint: 'https://dysmsapi.aliyuncs.com', apiVersion: '2017-05-25' }) let params = { SignNameJson: JSON.stringify([SignName]), TemplateCode: TemplateCode, } cloud.init({ env: 'xxx' // 你的环境id }) // 云函数入口函数 /** * 发送模板消息 */ exports.main = async(event, context) => { let { OPENID, APPID, UNIONID } = cloud.getWXContext() const db = cloud.database() return new Promise(async(resolve, reject) => { try { if(!event.phone) throw {code: 7322, data: [],info: '手机不能为空!'} if(!/^[1][3,4,5,6,7,8,9][0-9]{9}$/.test(event.phone)) throw {code: 7321, data: [],info: '手机号码格式错误!'} // 获取数据 let { data } = await db.collection('sms-record').where({ phone: event.phone, openid: OPENID, is_used: 1 }).orderBy('created_at', 'desc').skip(0).limit(1).get(), code = null // 计算时间 if(data.length != 0 && (Number(new Date()) - Number(new Date(data[0].created_at))) < 60000) { throw {code: 7323, data: [],info: '一分钟内,不能重复发送!'} } else if(data.length != 0 && (Number(new Date()) - Number(new Date(data[0].created_at))) < 1800000){ code = data[0].code } else { // 生成六位随机数 code = Math.floor(Math.random() * 900000) + 100000 } //发送短信 let { Code } = await client.request('SendBatchSms', Object.assign({ PhoneNumberJson: JSON.stringify([event.phone]), TemplateParamJson: JSON.stringify([{code}]) },params), { method: 'POST' }) if(Code !== 'OK') throw {code: 7321, data: [],info: '发送短信失败!'} // 新增数据 await db.collection('sms-record').add({ data: { phone: event.phone, code, openid: OPENID, is_used: 1, created_at: db.serverDate() } }) resolve({ code: 0, data: [], info: '操作成功!' }) } catch (error) { console.log(error) if(!error.code) reject(error) resolve(error) } }) } [代码] 3.参数 属性 类型 默认值 必填 说明 phone string 是 国内手机号码 4.使用 [代码] // 返回Promise wx.cloud.callFunction({ name: 'sendSms', data: { phone } }).then(res => { console.log(res) }) async sendSms(){ try { let { data } = await wx.cloud.callFunction({ name: 'sendSms', data: { phone } }) console.log(data) } catch (error) { console.log(error) } } [代码] 当然你也可以使用旧版的sdk 更多云函数模板 另外求个流量 和 star 模板使用云开发实现,接入百度AI平台API图像识别系统,无需另外搭建服务器,只需修改文件内配置项 一款方便快捷识别AI,可根据您拍摄或相册中照片识别出您所需要知道的物种(植物,动物,图文,菜品类型),相关知识,帮助您了解该物种,打开新世界! [图片]
2019-04-26 - 你根本不懂rebase-使用rebase打造可读的git graph
git graph 可读指什么? 这里的可读,主要指的是能够通过看git graph了解每一次版本更迭,每一次hotfix的修改记录.反映到分支上面,有两个要求: 每个分支的历史修改可读(单个分支的层面) 每个分支的分叉合并可读(多个分支的层面) rebase是什么,它是更优雅的merge吗? rebase翻译做[代码]变(re)基(base)[代码]. 讲rebase的文章经常会引用三张图: [图片] 原本的两个分支 [图片] 通过merge的结果 [图片] 通过rebase的结果 用来说明git rebase和git merge的区别的时候确实是足够了,但是 git rabase的用途并非是合并分支,它与merge根本不是同样的性质.(注意,这里的说法是[代码]并非是[代码],不是[代码]并非只是[代码],因为虽然有时rebase替代了merge的工作,但其原理和性质完全不一样.) rebase还有以下几种用处: [代码]git pull —-rebase[代码]处理同一分支上的冲突(如果你能理解其实这是[代码]git fetch&&git rebase[代码]两个操作,并且理解远程分支和本地分支的区分的话,那么其实他跟单纯的rebase用法没什么区别,但是因为其场景不一样,所以单独拆分出来讲) [代码]git rebase -i[代码]修改commit记录 实质上: merge是对目前分叉的两条分支的合并 rebase是对[代码]当前分支[代码]记录基于任何[代码]commit节点[代码](不限于当前分支上的节点)的变更. rebase的[代码]base[代码]不能理解为分叉的基点,而是整个git库中存在的所有commit节点: 在[代码]git pull —-rebase[代码]的时候,这个[代码]当前分支[代码]是本地分支,[代码]commit节点[代码]是远程分支的head 在[代码]git rebase master[代码]的时候,这个[代码]当前分支[代码]是feature分支,[代码]commit[代码]节点是master分支的head 在[代码]git rebase -i[代码]的时候,这个[代码]当前分支[代码]就是当前工作分支,[代码]commit节点[代码]是在 -i后注明的commit rebase是怎么工作的? 上面我们已经说到了: [代码]rebase[代码]是对[代码]当前分支[代码]记录基于任何[代码]commit节点[代码](不限于当前分支上的节点)的变更. 怎么做到呢?我没有深入研究它真的是如何实现的,以下步骤一定是不对的,但足够让你理解rebase干了什么. 我们标注出了两个重点,[代码]当前分支[代码]和[代码]commit节点[代码]. 把[代码]当前分支[代码]branch-A从头到尾列出来,从数据结构的角度来说这是一个链表 把[代码]commit节点[代码]所在的分支branch-B从头到尾列出来,同样是一个链表 找到这两个链表最近相同的节点n 把A在n之后的所有节点拆下来构成L 把B在n之后的所有节点中存在的diff信息都汇总起来构成d 对于L中的每一个节点,把他的diff信息拿出来,看看d中有没有冲突,如果有没法自动处理的冲突抛出错误,等待用户自己处理 可选地,对于[代码]rebase -i[代码]来说,还可以一次取多个节点或者按照不同顺序取,你有更大的处理自由 没冲突和处理完冲突的节点,改一个hash放到branch-B的[代码]commit节点[代码]之后 你可以把之前我们说到的三种rebase用处套在以上步骤看看,是否能够理解. rebase很危险对吗? 对,很危险. 不过就像小马过河一样,光听别人说是没用的,我们需要明白为什么有人说危险,有人说不危险.我看到很多文章说rebase有问题,但他们的说法其实并不让人信服,很多时候只是他们不会用. 很多人听说过一个golden rule,在文末有链接,但是很少有人会明白真正的原因.让我们一层层地剖析: 其他人git push的时候会对比较本地分支和远程分支的区别,把不同的地方推上去 如果远程分支被修改了,那么其他人的本地分支和远程分支就会出现分叉(另外还可能造成其他人之前已经推送的工作被覆盖) 当出现分叉的时候,意味着其他人需要处理冲突,也就是说,你对于远程历史记录的修改使得[代码]冲突扩散到了其他人身上[代码] 所以我们尽量不能修改远程分支,不能[代码]把别人fetch回去的改掉[代码],因为他们的工作就是基于fetch回去的分支开展的(往前推进是必须的,其实也修改了远程分支,所以才会merge产生冲突,但是这个冲突是无法避免的) 针对上面说的这一条,git也做了限制,如果你触犯了上面的原则,会在push的时候被阻挡,但是通过加一个[代码]-f[代码]可以强推 实际上不止rebase这样,任何修改远程分支历史的操作都会造成冲突,并且这个冲突需要所有人都解决一遍. 但是分析还是太长了,记不住怎么办? 只需要记住[代码]-f[代码],只要你不使用[代码]-f[代码],那么就是安全的. 不过仅是安全,并不能保证优雅,如果要使git graph可读,那你还得多想想: 怎么让自己的commit历史清晰(每个commit反应了一个单位的工作,前后顺序合理) 怎么让每次hotfix和feature所做的工作和顺序清晰 rebase如何让git graph可读? 我们还是说回之前提到的三个用法: git rebase master 在把分支合并回master的时候,用[代码]git rebase master[代码]代替[代码]git merge master[代码].(注意,只在合并之前使用,否则多人协作会遇到冲突) 这样的好处有两个: log里不会出现一个[代码]Merge branch 'master' into hotfix/xxx[代码]的节点 master分支上在这次merge之前已经被提交的[代码]上一次工作[代码]和这一次工作的顺序更清晰,因为rebase会让这次feature的分叉节点改到上一次工作后.对于master分支来说,我们并不关心checkout新的feature的顺序,我们[代码]更关心merge新的feature的顺序[代码]. [图片] 比如这里,使用merge master导致的紫色的分叉在提交之前与master多了一次连接,而且主线上在紫色分叉合并之前还经历了一次合并,这个时间顺序并不清晰. 那么在master分支上合并也用rebase吗?不是.因为我们需要master上的分叉让我们更明白master上的改变(所以使用-no-ff).实际上,不管你采用任何git flow模型,我都建议你对不太重要的分支合并采用rebase,对重要的分支合并采用merge.这样会让主干的更改更清晰,而分支不会扩散地太远. git pull —-rebase 多人在同一分支上工作的时候(包含master分支和多人合作的feature等分支),在git pull的时候会遇到冲突,git pull的默认行为是[代码]git fetch&git merge[代码],merge的对象是远程分支和本地分支. 它的好处基本上与上一条无异,还多了一条: 使用merge行为的pull会将其他人的工作作为外来的分叉,从而在graph上产生一个新的分叉, 并且其他人这一段时间所做的所有的工作都会在graph上被抬升出去,如果这段时间其他人做的工作很多,graph的主线会变得丧失了主线的意义(因为它太单薄了,很多工作根本没反应上来). [图片] 比如这里,本来左数第二条玫红色的才是主线,因为不规范地在master上直接提交了一次commit并且采用merge方式的pull做了合并导致主线被抬升到了外层.而这次不规范的commit却成了主线. git rebase -i 使用这条命令可以修改分支的记录,比如觉得之前的commit修改内容不够单元化,像是[代码]修改了文案1为文案2[代码],[代码]修改了文案2为文案3[代码],这种记录对于master分支来说是没必要关注的信息,最好通过[代码]git commit --amend[代码]或者rebase的方式修改掉. 不过并不推荐在提交之前手动做一次整个分支的squash,如果是rebase方式合并的话,也许更有意义.工蜂(腾讯内部的code平台)提供了merge request的标题和内容功能,所以没必要做squash,完全可以不必太聚合,以便反应真实的信息. 为了不影响别人,只用它修改未push的commit,或者如果一条分支只有一个人,你也可以修改已经push的commit. 对于这条命令的更多功能,可以再去查阅其他文章. 可读的graph应该长什么样? 先说一个原则,看graph要先看主线,主线要清晰,再看分叉上信息,这与我们的工作流程是一致的. [图片] 绿色的hotfix或者feature分支每次不是只允许提交一次commit,只是这一段都是一些小更改. 这看起来有点可笑,一点都不高级.说了这么多做了这么多难道只是为了得到这么简单的图? 没错,[代码]为了让东西变简单,本来就要付出很多代价[代码],我们所做的就是要让东西变简单,比如努力工作是为了让赚钱变简单,努力提升是为了让工作变简单.让事情变复杂只会让事情不可控. 当然具体如何还是要取决于你采用的git flow,但是原则很简单: [代码]每个分叉的子分叉尽量是一个串联一个,内部尽量不要再有自己的提交.[代码] 为什么我认为这样的git graph可读性好,因为它把我们的工作也拍平了,不在乎每个工作的开始时间和持续时间,只关心这个工作的完成时间. 假如一个项目需求1是1月1号启动,2月1号上线,需求2是1月20号启动,2月10号上线.1月10号修了一个bug,2月3号修了一个bug. 听起来是不是很绕? 如果你的git graph显示的也是这样的信息,可读性一定不好,所以我们要做的git graph应该反应的是如下信息: 1月10号修补bug 2月1号上线需求1 2月3号修补bug 2月10号上线需求2 rebase的缺点是什么? (这里并不讨论rebase可能带来的冲突问题,有很多文章都会讲,上面也已经提到了rebase的危险性,这里只讨论rebase对于git graph的缺点.实际上,冲突只是rebase不恰当使用导致的问题,而非rebase本身的问题.) 当然也有人会说,工作的开始时间也很重要呀,因为它反映了当时工作开展的基础条件.对,这是rebase master的弊端.他让记录清晰,也让记录丢失了一些信息.记录的加工让可读性变得更好,也让信息量变少了. git rebase 让git graph发生了变化,[代码]每次分叉的检出和并入之间不会再有任何节点[代码].(因为合并到master采取的是merge行为.否则根本没有分叉) [图片] 也就是这种情况不会再出现.因为每次总是[代码]rebase master[代码],把自己的起点抬了上去.[代码]git rebase实际上让检出信息没有意义,换取了主分支分叉的清晰.[代码] 另外值得注意的是,不要在同一个分支上混用[代码]rebase[代码]和[代码]merge[代码](包括pull 的默认merge行为),因为rebase之后的[代码]commit hash[代码]被改变了,再merge的时候两个分支的共同起点被提前了,merge之后的git graph上会出现一左一右两串同样commit信息的一段历史. 如果rebase没有缺点,那么也就没有争议.是否使用rebase也要看真实的需求是什么. 这篇文章要干什么? 通过rebase让git graph更可读.目的和原则我们都已经说过了,没必要再重新说一遍. 多有谬误之处,还望不吝赐教!
2019-04-29 - 巧用云调用,实现【共享名片夹】小程序
原创: 锋少 一、前言 从一个较早的小程序开发者到第一批使用小程序·云开发的开发者,这期间一直在关注关于小程序各方面的更新,同时也用小程序·云开发做了几款产品,其中包括上次分享的随手记Lite小程序,比较上次,这次分享的技术点相对更加全面和实用一些。 涉及的技术点有: 数据上传、数据更新、分页读取、数据删除,AI智能名片识别读取。 单图上传、多图上传,图片URL获取,带参小成码生成。 下发模板消息,云调用使用。 二、主要功能 创建电子名片:信息存储,图片上传,名片读取(AI智能名片识别) 转发电子名片:专属名片海报(带参小程序码生成) 电子名片被访问:下发模板消息(云调用) 三、功能实现 3.1、准备工作 1、注册微信小程序账号: 方式一:直接注册(https://mp.weixin.qq.com/wxopen/waregister?action=step1) 方式二:已经有微信公众号(已认证)朋友可以直接【登录公众号】 -> 【小程序管理】 -> 【添加】->【快速注册并认证小程序】,注册完成后,找到小程序的 AppID和 AppSecret [图片] 2、下载微信开发者工具、创建项目 ,打开开发者工具,键入项目目录、项目名称、刚才的 AppID,此时项目创建成功,然后点击开发者工具上方的【云开发】开通云开发。 3.2功能实现一:【创建电子名片】 信息存储,图片上传,名片读取(AI智能名片识别) 1.功能简要描述 对于一个名片的小程序,第一步肯定是创建电子名片,除此之外,可以用传统信息录入的方式创建名片,同时也支持纸质名片的识别读取,快速创建名片,这里本地需要导入 [代码]mapping.js[代码]框架,接下来以纸质名片识别为例。 2.核心代码 [代码] // 上传名片后获取零时链接 getTempFileURL() { wx.cloud.getTempFileURL({ fileList: [{ fileID: this.data.fileID, }], }).then(res => { console.log('获取成功', res); if (res.fileList.length) { this.setData({ coverImage: res.fileList[0].tempFileURL }, () => { this.parseNameCard(); }); } else { Toast('获取图片地址失败'); } }).catch(err => { Toast('获取图片地址失败'); }); }, // 读取名片 parseNameCard() { wx.cloud.callFunction({ name: 'parseCard', data: { url: this.data.coverImage } }).then(res => { if (res.result.data.length == 0) { Toast('解析失败,请上传【纸质名片】或【手动创建】'); return; } let data = this.transformMapping(res.result.data); wx.setStorageSync("parseCardData", data) Toast('解析成功'); }).catch(err => { console.error('解析失败,请上传【纸质名片】或【手动创建】', err); Toast('解析失败,请上传【纸质名片】或【手动创建】'); }); }, // 名片数据解析 transformMapping(data) { let record = {}; let returnData = []; data.map((item) => { let name = null; if (mapping.hasOwnProperty(item.item)) { name = mapping[item.item]; // 写入英文名 item.name = name; } return item; }); // 过滤重复的字段 data.forEach((item) => { if (!record.hasOwnProperty(item.item)) { returnData.push(item); record[item.item] = true; } }); return returnData; }, [代码] 3.3功能实现二:【转发电子名片】 专属名片海报(带参小程序码生成) 1.功能简要描述:转发电子名片有两种方式。 1.以小程序的形式直接转发给好友或微信群。 2.生成专属名片海报分享到朋友圈长按进入对应的电子名片页面。名片海报上除了有对应用户的姓名之外,还有专属的名片小程序码,效果如下: [图片] 2.核心代码 [代码]const cloud = require('wx-server-sdk') const axios = require('axios') var rp = require('request-promise'); cloud.init() // 云函数入口函数,小程序端传过来页面和名片id exports.main = async (event, context) => { console.log(event) try { const resultValue = await rp('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=appid&secret=secret') const token = JSON.parse(resultValue).access_token; const response = await axios({ method: 'post', url: 'https://api.weixin.qq.com/wxa/getwxacodeunlimit', responseType: 'stream', params: { access_token: token, }, data: { page: event.page, width: 300, scene: event.id, }, }); return await cloud.uploadFile({ cloudPath: 'xcxcodeimages/' + Date.now() + '.png', fileContent: response.data, }); } catch (err) { console.log('>>>>>> ERROR:', err) } } [代码] 3.4功能实现三:【电子名片被访问】 下发模板消息(云调用) 1.功能简要描述 用户名片被访问的时候,用户者会收到【客户来访提醒】的模板消息,同时提醒用户完善名片信息。 2.核心代码 [代码]const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { try { const result = await cloud.openapi.templateMessage.send({ touser: event.toUser, page: "pages/index/index", data: { keyword1: { value: event.visitDate }, keyword2: { value: "刚刚有人深度访问了您的名片,经常完善名片信息,更容易被查找和访问。" }, }, templateId: 'templateId', formId: event.formId, }) return result } catch (err) { throw err } } [代码] 四、总结 和传统的小程序 + WEB后台开发模式比起来,云开发在精力和人力上真的是节省了很多,这能使开发者将大部分精力和时间放到功能的开发上。 云开发上线时间不算太长,但逐步有新的功能开放出来,比如云控制台数据的导入导出、云调用等,希望小程序·云开发开放出更多的接口和功能… 五、项目预览 [图片]
2019-05-05 - 影响到微信小程序排名的因素有哪些?
随着腾讯官方不断的赋予微信小程序新的功能,这个新功能也越来越受到大家的关注.那么在众多小程序中如何让自己的小程序脱颖而出,成为广大运营者关心的问题.今天就来跟大家聊聊关于小程序排名的那些事. 先来说说小程序展现方式的分类: 1.附近小程序 大家熟知这个功能的主要原因是腾讯给出了一个比较诱人的推广方式,附近1-5公里的免费展示.这个对于很多商家来说是特别有吸引力的.但是由于腾讯的审核 机制问题,如果只有一个店的商家就只能在一个固定的位置进行展示.所覆盖范围有限.这里的排名规则很简单,就是根据微信用户与店面的地理位置进行判断.离 店越近展示就越靠前.人工干预的因素几乎没有可能,这里就不做过多阐述了. 2.搜索小程序 附近小程序的位置的局限性会影响到小程序的流量,针对这种情况微信小程序给出了一个辅助功能,关键词搜索功能.这是小程序流量来源的另一个重要途径. 具体可分为微信搜索和小程序搜索,很多人就会问这两者有什么区别么?是的,有区别的.下面我就来重点说说这两个搜索方式的. 先来说说微信搜索,点击微信搜索 下面会出来一个小程序选项,里面展示的小程序为近期你访问过的小程序 以花店为例,会出来之前我们使用过的小程序’周边花店’;影响微信搜索的主要因素,大家很容易看到.以使用过的小程序方式展现.需要用户有打开微信小程序的记录 再来说说小程序搜索,小程序搜索的具体操作方式其实和我们打开小程序的步骤差不多,这里面的展示和排名是我们今天要重点去说的,影响小程序搜索排名的因素总结下来有以下几点: 【1】上线时间(占比5%): 微信小程序的上线时间,上线时间越早,排名越靠前 目前微信小程序关键词占位非常重要,越早发布竞争越小,而且还可以做到霸-屏的效果 【2】关键字频次(10%): 描述中完全匹配出现关键词次数越多,排名越靠前 【3】标题(35%): 标题中关键词出现1次,且整体标题的字数越短,排名越靠前 【4】使用量(占比50%) 微信小程序用户使用数量越多,排名越靠前 如果你提前布局小程序各个方面,想必你的小程序排名远远超过你的预想值。 反之你要退后布局,你的排名就很难靠前,用户找到您的概率就会很低。 根据以上因素,不难发现决定小程序排名的重要因素在于用户的使用量,运营者在日常推广中可以针对性的去推广小程序。 最后分享一下我收集的小程序: https://www.sucaihuo.com/source/0-0-266-0-0-0
2019-05-09 - 小程序需要https域名,不会配置HTTPS?给我5分钟,手把手教你
本文针对不会配置HTTPS或者小白开发着,请开发者社区的大佬们自动忽略。非广告,心得分享,勿喷,谢谢。 👇 推荐一个小程序商城,全开源,码云GVP项目,有兴趣的可以了解一下:【点击下载】 👇 👇 正文开始 01、关于 FreeSSL.cn FreeSSL.cn 是一个免费提供 HTTPS 证书申请、HTTPS 证书管理和 HTTPS 证书到期提醒服务的网站,旨在推进 HTTPS 证书的普及与应用,简化证书申请的流程。 当然了,我看重的不是免费(微笑~),而是 FreeSSL 使用起来非常人性化。我是一个计算机常识非常薄弱的程序员(羞愧一下),但通过 FreeSSL,我竟然可以独自完成 Tomcat 的 HTTPS 配置! 很多年以前,公司要做华夏银行的接口对接,需要 HTTPS 访问,大概花了 3000 块买的证书,最后证书还有问题,HTTPS 也没搞定。总之,坑的很! FreeSSL.cn 有很大的不同,申请非常便捷,优点很多,值得推荐一波。毕竟再也不用邮件、电话各种联系了(也许时代进步了)。 100% 永久免费;这要感谢 Let’s Encrypt 与 TrustAsia 提供的免费 SSL 证书。 在 HTTPS 证书到期前,FreeSSL.cn 会及时地提醒更换证书,免费的服务。 私钥不在网络中传播,确保 HTTPS 证书的安全。 02、使用 FreeSSL 申请证书 第一步,填写域名,点击「创建免费的 SSL 证书」 [图片] 第二步,填写邮箱,点击「创建」 [图片] 1)证书类型默认为 RSA RSA 和 ECC 有什么区别呢?可以通过下面几段文字了解一下。 HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能,可以有效防止数据被监听或篡改,还能抵御 MITM(中间人)攻击。TLS 在实施加密过程中,需要用到非对称密钥交换和对称内容加密两大算法。 对称内容加密强度非常高,加解密速度也很快,只是无法安全地生成和保管密钥。在 TLS 协议中,应用数据都是经过对称加密后传输的,传输中所使用的对称密钥,则是在握手阶段通过非对称密钥交换而来。常见的 AES-GCM、ChaCha20-Poly1305,都是对称加密算法。 非对称密钥交换能在不安全的数据通道中,产生只有通信双方才知道的对称加密密钥。目前最常用的密钥交换算法有 RSA 和 ECDHE:RSA 历史悠久,支持度好,但不支持 PFS(Perfect Forward Secrecy);而 ECDHE 是使用了 ECC(椭圆曲线)的 DH(Diffie-Hellman)算法,计算速度快,支持 PFS。 2)验证类型默认为 DNS DNS 和文件验证有什么区别呢?我们再来一起了解下。 首先,我们需要明白一点,CA(Certificate Authority,证书颁发机构) 需要验证我们是否拥有该域名,这样才给我们颁发证书。 文件验证(HTTP):CA 将通过访问特定 URL 地址来验证我们是否拥有域名的所有权。因此,我们需要下载给定的验证文件,并上传到您的服务器。 DNS 验证:CA 将通过查询 DNS 的 TXT 记录来确定我们对该域名的所有权。我们只需要在域名管理平台将生成的 TXT 记录名与记录值添加到该域名下,等待大约 1 分钟即可验证成功。 所以,如果对服务器操作方便的话,可以选择文件验证;如果对域名的服务器操作比较方便的话,可以选择 DNS 验证。如果两个都方便的话,请随意选啦。 3)CSR生成默认为离线生成 离线生成、浏览器生成 和 我有 CSR 又有什么区别呢?来,我们继续了解一下。 离线生成 推荐!!!:私钥在本地加密存储,更安全;公钥自动合成,支持常见证书格式转换,方便部署;支持部分 WebServer 的一键部署,非常便捷。 离线生成的时候,需要先安装 KeyManager,可以提供安全便捷的 SSL 证书申请和管理。下载地址如下: https://keymanager.org/ Windows 的话,安装的时候要选择“以管理员身份运行”。 浏览器生成:在浏览器支持 Web Cryptography 的情况下,会使用浏览器根据用户的信息生成 CSR 文件。 Web Cryptography,网络密码学,用于在 Web 应用程序中执行基本加密操作的 JavaScript API。很多浏览器并不支持 我有 CSR:可以粘贴自己的 CSR,然后创建。 第三步,选择离线生成,打开 KeyManager 填写密码后点击「开始」,稍等片刻。 第四步,返回浏览器,点击「下一步」,出现如下界面。 [图片] 第五步,下载文件,并上传至服务器指定目录下。 第六步,点击「验证」,通过后,出现以下界面。 [图片] 第七步,点击「保存到KeyManager」,可以看到证书状态变成了已颁发。 03、为 Tomcat 配置 jks 格式证书 第一步,导出证书。假如服务器选择的 Tomcat,需要导出 Java keystone (简拼为 jks)格式的证书。 [图片] 注意:私钥的密码在配置 Tomcat 的时候用到。 [图片] 第二步,上传证书至服务器。 第三步,配置 Tomcat 的 server.xml。 [代码] <Connector port="81" protocol="HTTP/1.1" maxThreads="250" maxHttpHeaderSize="8192" acceptCount="100" connectionTimeout="60000" keepAliveTimeout="200000" redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,application/xml,application/json,text/javascript,application/javascript,text/css,text/plain,text/json,image/png,image/gif"/> <Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="443" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="/home/backup/qingmiaokeji.cn.jks" keystorePass="Chenmo" clientAuth="false" sslProtocol="TLS" useBodyEncodingForURI="true" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,application/xml,application/json,text/javascript,application/javascript,text/css,text/plain,text/json,image/png,image/gif" /> [代码] 其中 keystorePass 为导出证书时私钥的加密密码。 第四步,重启 Tomcat,并在浏览器地址栏中输入 https://你的域名/ 进行测试。 注意到没,浏览器地址栏前面有一个绿色的安全锁,这说明 HTTPS 配置成功了!好了,为自己鼓个掌! 04、最后 你有没有订个五分钟的时间沙漏?如果超过五分钟 HTTPS 还没有配置成功,你过来揍我!反正你又打不来我!我在CRMEB等你! 👇 👇 👇 最后亿遍,再次发一下我的项目:全开源啊!公众号+小程序啊!商城系统啊!免费啊!了解一下啊→→→点我点我!
2019-05-10 - 小程序地图学习之获取位置 获取经纬度 获取地名 获取地址
我们在做小程序开发时,难免会遇到地图相关的开发,而小程序已经为我们提供的比较完善的地图组件。我们只需要调用相关的api就可以实现大致的功能。如:获取经纬度,获取位置,获取地址,获取地名。结下来就具体给大家讲解。 老规矩先看效果图 [图片] 接下来我们就来看看具体实现步骤 一,定义一个按钮来调用位置获取的api [代码]<!--index.wxml--> <button bindtap='getLocation'>获取位置信息</button> <text>{{jingwei}}</text> <text>{{address}}</text> <text>{{name}}</text> [代码] 二,调用获取地理位置的方法 [代码]//index.js Page({ getLocation() { let that = this; wx.chooseLocation({ success: function(res) { console.log(res) var latitude = res.latitude var longitude = res.longitude; that.setData({ jingwei: "经纬度:" + longitude + ", " + latitude, address: " 地址:" + res.address, name: " 地名:" + res.name }) } }); } }) [代码] 其实到这里我们就可以实现获取经纬度,获取位置信息的功能了。 但是呢??现在小程序调用用户位置信息时,需要用户授权,如下图,如果用户点击了拒绝,我们就没有办法调用地图获取位置信息了。 [图片] 所以呢,我们要想实现一个完整的获取用户位置信息的功能,就要在监测到用户拒绝的位置权限时,引导用户去重新授权。这样才是一个友好的健壮的程序。下面就来教大家如何引导用户去打开授权。 三,在app.json里注册位置权限 [图片] 上图红色框里就是我们的位置权限的注册代码,app.json的完整代码如下。 [代码]{ "pages": [ "pages/index/index", "pages/setting/setting" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } }, "sitemapLocation": "sitemap.json" } [代码] 四,定义检查位置权限是否打开的方法 [代码] //校验位置权限是否打开 checkLocation() { let that = this; //选择位置,需要用户授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.userLocation']) { wx.authorize({ scope: 'scope.userLocation', success() { wx.showToast({ //这里提示失败原因 title: '授权成功!', duration: 1500 }) }, fail() { that.showSettingToast('需要授权位置信息'); } }) } } }) }, [代码] 这个方法就是来检查用户的位置权限是否授权,如果没有授权,就弹窗提示用户去授权页授权。弹窗代码如下: [代码] // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, [代码] 至此就可以实现一个完整的获取用户位置信息的小程序了,index.js完整代码如下 [代码]//index.js Page({ getLocation() { this.checkLocation(); let that = this; wx.chooseLocation({ success: function(res) { console.log(res) var latitude = res.latitude var longitude = res.longitude; that.setData({ jingwei: "经纬度:" + longitude + ", " + latitude, address: " 地址:" + res.address, name: " 地名:" + res.name }) } }); }, //校验位置权限是否打开 checkLocation() { let that = this; //选择位置,需要用户授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.userLocation']) { wx.authorize({ scope: 'scope.userLocation', success() { wx.showToast({ //这里提示失败原因 title: '授权成功!', duration: 1500 }) }, fail() { that.showSettingToast('需要授权位置信息'); } }) } } }) }, // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, }) [代码] 从代码中可以看到,我们在用户拒绝授权时的提示框,点击会跳转到setting页,setting也是我们自己的页面,但是这个页面特别简单。就定义一个button。 [代码]<!--pages/setting/setting.wxml--> <button class="button" open-type="openSetting" type='primary'> 打开授权设置页 </button> [代码] 为什么要这么做呢,因为微信不允许我们直接打开权限设置页,必须通过button组件提供的开发能力去到设置页,这里的开放能力就是open-type=“openSetting” 中的openSetting。我们点击按钮后就到了权限设置页。 [图片] 这样就可以引导用户再次授权了。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 完整的源码可以加老师微信获取,也可以关注下面老师公号,回复“地图源码” 获取。 [图片]
2019-05-08 - 微信小程序swiper高度动态适配(子元素高度不固定)
示例代码地址 https://github.com/s568774056/swipe.git 对于整页都是swiper的情况下。例如下面这张图: [图片] 则可以使用如下css [代码] [代码] [代码]swiper,swiper-item{[代码] [代码] [代码][代码]height[代码][代码]: [代码][代码]100[代码][代码]vh [代码][代码]!important[代码][代码];[代码][代码]}[代码] [代码] [代码] [代码]或者 [代码] [代码] [代码] [代码][代码] [代码][代码] swiper,swiper-item{ height: calc(100vh - 75rpx) !important; } [代码][代码] [代码] [代码] 对于swiper占据部分高度的情况下。 [图片] 使用如下代码 原理为在[代码]swiper-item[代码][代码][代码]的最上面和最下面插入空view,并利用wx api获取两个之间的高度差,然后设置给[代码]swiper[代码]。 细节方面需要自己调整下。为什么小程序不把这个组件做好呢?还得自己计算- -! <swiper class='hide' bindanimationfinish="swiperChange" style="height:{{swiper_height}};" current="{{isIndex}}"> <swiper-item wx:for="{{roomList}}" wx:for-item='room' wx:for-index="index"> <view id="start{{index}}" class='start-view'></view> <block wx:for="{{imgUrls}}" wx:for-item='path' wx:for-index="img-index"> <image mode="aspectFill" src="{{path}}" /> </block> <view id="end{{index}}" class='start-view'></view> </swiper-item> </swiper> [代码][代码][代码][代码] swiper { margin-top: 45rpx; } Page({ data: { roomList: ['Room1', 'Room2', 'Room3'], imgUrls: [ 'https://images.unsplash.com/photo-1551334787-21e6bd3ab135?w=640', 'https://images.unsplash.com/photo-1551214012-84f95e060dee?w=640', 'https://images.unsplash.com/photo-1551446591-142875a901a1?w=640' ], swiper_height: 0, isIndex:0 }, onLoad: function () { this.autoHeight(); }, changeNavBar: function (e) { this.setData({ isIndex: e.detail }); }, swiperChange: function (e) { this.setData({ isIndex: e.detail.current }); this.autoHeight(); }, autoHeight() { let { isIndex } = this.data; wx.createSelectorQuery() .select('#end' + isIndex).boundingClientRect() .select('#start' + isIndex).boundingClientRect().exec(rect => { let _space = rect[0].top - rect[1].top; _space = _space + 'px'; this.setData({ swiper_height: _space }); }) } }) 参考文章https://developers.weixin.qq.com/community/develop/doc/00008aaf4a473056d1c69a8b253c04
2019-09-04 - 关于使用了层级过高的组件后,使用Input被遮挡的问题解决方法!(我真是个天才~~~~)
用cover-view 来显示,input来输入,通过bindinput来赋值,用cover-view来覆盖input的位置,同时把cover-view的背景颜色设置透明,这样既不会被其它组件盖住,也能显示出input的光标出来。
2019-05-08 - Licia 支持小程序的 JS 工具库
导语 Licia 是一套在开发中实践积累起来的实用 JavaScript 工具库。该库目前拥有超过 300 个模块,同时支持浏览器、node 及小程序运行环境,提供了包括日期格式化、md5、颜色转换等实用模块,可以极大地提高开发效率。 前言 因为小程序运行的是 JavaScript 代码,传统前端所使用的 JS 库理应也能够被用在小程序中才对。然而,经过实际测试,你会发现有相当一部分 npm 包是无法直接在小程序中跑起来的。比如前端工程师十分常用的 lodash,在小程序中引入会报错。 为什么会这样? 主要原因就是绝大部分库的开发者在设计时只会考虑两种运行环境,浏览器和 node,而小程序并不会在其考虑范围内。因此,只要开发者的 JS 代码使用了只有浏览器与 node 中才有的接口,如 DOM 操作、文件读写等,该库就不能正常地运行在小程序环境中。除此之外,假如他们使用了小程序禁用的功能,例如全局变量与动态代码执行,这时候代码跑在小程序环境也会出错。 使用 使用 npm 安装 1、 安装 npm 包 [代码]npm i miniprogram-licia --save [代码] 2、点击开发者工具中的菜单栏:工具 --> 构建 npm 3、直接在代码中引入使用 [代码]const licia = require('miniprogram-licia'); licia.md5('licia'); // -> 'e59f337d85e9a467f1783fab282a41d0' licia.safeGet({a: {b: 1}}, 'a.b'); // -> 1 [代码] 生成定制化 util.js 使用 npm 包的方式会将所有功能引入到代码包中,大概会增加 100 kb 的大小。如果你只想引入所需脚本,可以使用在线工具生成定制化 util 库。 1、访问 https://licia.liriliri.io/builder.html 2、输入需要的模块名,点击生成下载 util.js。 3、将生成的工具库拷贝到小程序项目任意目录下然后直接引入使用。 [代码]const util = require('../lib/util'); util.wx.getStorage({ key: 'test' }).then(res => console.log(res.data)); [代码] 优点 1、目前拥有 270 多个模块可在小程序中正常运行,而 underscore 只有 120 个函数左右。 2、与 lodash 相比增加了不少更加实用的函数,比如 md5、atob、btoa、Emitter、dateFormat 等。 3、可以直接在小程序中引入运行,不像 lodash 需要进行一定的修改才能正常跑在小程序中。 4、定制化生成可以使用更小体积的工具库,这在限制了代码包大小的小程序中十分有用。 附录 这里只简单列出函数及其功能介绍,详细的用法请访问官网查看。 注:模块名右边有小程序图标即表明可以在小程序中使用。 Class: 创建 JavaScript 类。 Color: 颜色转换。 Dispatcher: Flux 调度器。 Emitter: 提供观察者模式的 Event emitter 类。 Enum: Enum 类实现。 JsonTransformer: JSON 转换器。 LinkedList: 双向链表实现。 Logger: 带日志级别的简单日志库。 Lru: 简单 LRU 缓存。 Promise: 轻量 Promise 实现。 PseudoMap: 类似 es6 的 Map,不支持遍历器。 Queue: 队列数据结构。 QuickLru: 不使用链表的 LRU 实现。 ReduceStore: 简单类 redux 状态管理。 Stack: 栈数据结构。 State: 简单状态机。 Store: 内存存储。 Tween: JavaScript 补间动画库。 Url: 简单 url 操作库。 Validator: 对象属性值校验。 abbrev: 计算字符串集的缩写集合。 after: 创建一个函数,只有在调用 n 次后才会调用一次。 allKeys: 获取对象的所有键名,包括自身的及继承的。 arrToMap: 将字符串列表转换为映射。 atob: window.atob,运行在 node 环境时使用 Buffer 进行模拟。 average: 获取数字的平均值。 base64: base64 编解码。 before: 创建一个函数,只能调用少于 n 次。 binarySearch: 二分查找实现。 bind: 创建一个绑定到指定对象的函数。 btoa: window.btoa,运行在 node 环境时使用 Buffer 进行模拟。 bubbleSort: 冒泡排序实现。 bytesToStr: 将字节数组转换为字符串。 callbackify: 将返回 Promise 的函数转换为使用回调的函数。 camelCase: 将字符串转换为驼峰式。 capitalize: 将字符串的第一个字符转换为大写,其余字符转换为小写。 castPath: 将值转换为属性路径数组。 centerAlign: 字符串居中。 char: 根据指定的整数返回 unicode 编码为该整数的字符。 chunk: 将数组拆分为指定长度的子数组。 clamp: 将数字限定于指定区间。 className: 合并 class。 clone: 对指定对象进行浅复制。 cloneDeep: 深复制。 cmpVersion: 比较版本号。 combine: 创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值。 compact: 返回数组的拷贝并移除其中的虚值。 compose: 将多个函数组合成一个函数。 concat: 将多个数组合并成一个数组。 contain: 检查数组中是否有指定值。 convertBase: 对数字进行进制转换。 createAssigner: 用于创建 extend,extendOwn 和 defaults 等模块。 curry: 函数柯里化。 dateFormat: 简单日期格式化。 debounce: 返回函数的防反跳版本。 decodeUriComponent: 类似 decodeURIComponent 函数,只是输入不合法时不抛出错误并尽可能地对其进行解码。 defaults: 填充对象的默认值。 define: 定义一个模块,需要跟 use 模块配合使用。 defineProp: Object.defineProperty(defineProperties) 的快捷方式。 delay: 在指定时长后执行函数。 detectBrowser: 使用 ua 检测浏览器信息。 detectMocha: 检测是否有 mocha 测试框架在运行。 detectOs: 使用 ua 检测操作系统。 difference: 创建一个数组,该数组的元素不存在于给定的其它数组中。 dotCase: 将字符串转换为点式。 each: 遍历集合中的所有元素,用每个元素当做参数调用迭代器。 easing: 缓动函数,参考 http://jqueryui.com/ 。 endWith: 检查字符串是否以指定字符串结尾。 escape: 转义 HTML 字符串,替换 &,<,>,",`,和 ’ 字符。 escapeJsStr: 转义字符串为合法的 JavaScript 字符串字面量。 escapeRegExp: 转义特殊字符用于 RegExp 构造函数。 every: 检查是否集合中的所有元素都能通过真值检测。 extend: 复制多个对象中的所有属性到目标对象上。 extendDeep: 类似 extend,但会递归进行扩展。 extendOwn: 类似 extend,但只复制自己的属性,不包括原型链上的属性。 extractBlockCmts: 从源码中提取块注释。 extractUrls: 从文本中提取 url。 fibonacci: 计算斐波那契数列中某位数字。 fileSize: 将字节数转换为易于阅读的形式。 fill: 在数组指定位置填充指定值。 filter: 遍历集合中的每个元素,返回所有通过真值检测的元素组成的数组。 find: 找到集合中第一个通过真值检测的元素。 findIdx: 返回第一个通过真值检测元素在数组中的位置。 findKey: 返回对象中第一个通过真值检测的属性键名。 findLastIdx: 同 findIdx,只是查找顺序改为从后往前。 flatten: 递归拍平数组。 fnParams: 获取函数的参数名列表。 format: 使用类似于 printf 的方式来格式化字符串。 fraction: 转换数字为分数形式。 freeze: Object.freeze 的快捷方式。 freezeDeep: 递归进行 Object.freeze。 gcd: 使用欧几里德算法求最大公约数。 getUrlParam: 获取 url 参数值。 has: 检查属性是否是对象自身的属性(原型链上的不算)。 hslToRgb: 将 hsl 格式的颜色值转换为 rgb 格式。 identity: 返回传入的第一个参数。 idxOf: 返回指定值第一次在数组中出现的位置。 indent: 对文本的每一行进行缩进处理。 inherits: 使构造函数继承另一个构造函数原型链上的方法。 insertionSort: 插入排序实现。 intersect: 计算所有数组的交集。 intersectRange: 计算两个区间的交集。 invert: 生成一个新对象,该对象的键名和键值进行调换。 isAbsoluteUrl: 检查 url 是否是绝对地址。 isArgs: 检查值是否是参数类型。 isArr: 检查值是否是数组类型。 isArrBuffer: 检查值是否是 ArrayBuffer 类型。 isArrLike: 检查值是否是类数组对象。 isBool: 检查值是否是布尔类型。 isBrowser: 检测是否运行于浏览器环境。 isClose: 检查两个数字是否近似相等。 isDataUrl: 检查字符串是否是有效的 Data Url。 isDate: 检查值是否是 Date 类型。 isEmail: 简单检查值是否是合法的邮件地址。 isEmpty: 检查值是否是空对象或空数组。 isEqual: 对两个对象进行深度比较,如果相等,返回真。 isErr: 检查值是否是 Error 类型。 isEven: 检查数字是否是偶数。 isFinite: 检查值是否是有限数字。 isFn: 检查值是否是函数。 isGeneratorFn: 检查值是否是 Generator 函数。 isInt: 检查值是否是整数。 isJson: 检查值是否是有效的 JSON。 isLeapYear: 检查年份是否是闰年。 isMap: 检查值是否是 Map 对象。 isMatch: 检查对象所有键名和键值是否在指定的对象中。 isMiniProgram: 检测是否运行于微信小程序环境中。 isMobile: 使用 ua 检测是否运行于移动端浏览器。 isNaN: 检测值是否是 NaN。 isNative: 检查值是否是原生函数。 isNil: 检查值是否是 null 或 undefined,等价于 value == null。 isNode: 检测是否运行于 node 环境中。 isNull: 检查值是否是 Null 类型。 isNum: 检测值是否是数字类型。 isNumeric: 检查值是否是数字,包括数字字符串。 isObj: 检查值是否是对象。 isOdd: 检查数字是否是奇数。 isPlainObj: 检查值是否是用 Object 构造函数创建的对象。 isPrime: 检查整数是否是质数。 isPrimitive: 检测值是否是字符串,数字,布尔值或 null。 isPromise: 检查值是否是类 promise 对象。 isRegExp: 检查值是否是正则类型。 isRelative: 检查路径是否是相对路径。 isSet: 检查值是否是 Set 类型。 isSorted: 检查数组是否有序。 isStr: 检查值是否是字符串。 isTypedArr: 检查值是否 TypedArray 类型。 isUndef: 检查值是否是 undefined。 isUrl: 简单检查值是否是有效的 url 地址。 isWeakMap: 检查值是否是 WeakMap 类型。 isWeakSet: 检查值是否是 WeakSet 类型。 kebabCase: 将字符串转换为短横线式。 keyCode: 键码键名转换。 keys: 返回包含对象自身可遍历所有键名的数组。 last: 获取数组的最后一个元素。 linkify: 将文本中的 url 地址转换为超链接。 longest: 获取数组中最长的一项。 lowerCase: 转换字符串为小写。 lpad: 对字符串进行左填充。 ltrim: 删除字符串头部指定字符或空格。 map: 对集合的每个元素调用转换函数生成与之对应的数组。 mapObj: 类似 map,但针对对象,生成一个新对象。 matcher: 传入对象返回函数,如果传入参数中包含该对象则返回真。 max: 获取数字中的最大值。 md5: MD5 算法实现。 memStorage: Web Storage 接口的纯内存实现。 memoize: 缓存函数计算结果。 mergeSort: 归并排序实现。 methods: 获取对象中所有方法名。 min: 获取数字中的最小值。 moment: 简单的类 moment.js 实现。 ms: 时长字符串与毫秒转换库。 negate: 创建一个将原函数结果取反的函数。 nextTick: 能够同时运行在 node 和浏览器端的 next tick 实现。 noop: 一个什么也不做的空函数。 normalizeHeader: 标准化 HTTP 头部名。 normalizePath: 标准化文件路径中的斜杠。 now: 获取当前时间戳。 objToStr: Object.prototype.toString 的别名。 omit: 类似 pick,但结果相反。 once: 创建只能调用一次的函数。 optimizeCb: 用于高效的函数上下文绑定。 pad: 对字符串进行左右填充。 pairs: 将对象转换为包含【键名,键值】对的数组。 parallel: 同时执行多个函数。 parseArgs: 命令行参数简单解析。 partial: 返回局部填充参数的函数,与 bind 模块相似。 pascalCase: 将字符串转换为帕斯卡式。 perfNow: 高精度时间戳。 pick: 过滤对象。 pluck: 提取数组对象中指定属性值,返回一个数组。 precision: 获取数字的精度。 promisify: 转换使用回调的异步函数,使其返回 Promise。 property: 返回一个函数,该函数返回任何传入对象的指定属性。 query: 解析序列化 url 的 query 部分。 quickSort: 快排实现。 raf: requestAnimationFrame 快捷方式。 random: 在给定区间内生成随机数。 randomItem: 随机获取数组中的某项。 range: 创建整数数组。 rc4: RC4 对称加密算法实现。 reduce: 合并多个值成一个值。 reduceRight: 类似于 reduce,只是从后往前合并。 reject: 类似 filter,但结果相反。 remove: 移除集合中所有通过真值检测的元素,返回包含所有删除元素的数组。 repeat: 重复字符串指定次数。 restArgs: 将给定序号后的参数合并成一个数组。 rgbToHsl: 将 rgb 格式的颜色值转换为 hsl 格式。 root: 根对象引用,对于 nodeJs,取 [代码]global[代码] 对象,对于浏览器,取 [代码]window[代码] 对象。 rpad: 对字符串进行右填充。 rtrim: 删除字符串尾部指定字符或空格。 safeCb: 创建回调函数,内部模块使用。 safeDel: 删除对象属性。 safeGet: 获取对象属性值,路径不存在时不报错。 safeSet: 设置对象属性值。 sample: 从集合中随机抽取部分样本。 selectionSort: 选择排序实现。 shuffle: 将数组中元素的顺序打乱。 size: 获取对象的大小或类数组元素的长度。 sleep: 使用 Promise 模拟暂停方法。 slice: 截取数组的一部分生成新数组。 snakeCase: 转换字符串为下划线式。 some: 检查集合中是否有元素通过真值检测。 sortBy: 遍历集合中的元素,将其作为参数调用函数,并以得到的结果为依据对数组进行排序。 spaceCase: 将字符串转换为空格式。 splitCase: 将不同命名式的字符串拆分成数组。 splitPath: 将路径拆分为文件夹路径,文件名和扩展名。 startWith: 检查字符串是否以指定字符串开头。 strHash: 使用 djb2 算法进行字符串哈希。 strToBytes: 将字符串转换为字节数组。 stringify: JSON 序列化,支持循环引用和函数。 stripAnsi: 清除字符串中的 ansi 控制码。 stripCmt: 清除源码中的注释。 stripColor: 清除字符串中的 ansi 颜色控制码。 stripHtmlTag: 清除字符串中的 html 标签。 sum: 计算数字和。 swap: 交换数组中的两项。 template: 将模板字符串编译成函数用于渲染。 throttle: 返回函数的节流阀版本。 timeAgo: 将时间格式化成多久之前的形式。 timeTaken: 获取函数的执行时间。 times: 调用目标函数 n 次。 toArr: 将任意值转换为数组。 toBool: 将任意值转换为布尔值。 toDate: 将任意值转换为日期类型。 toInt: 将任意值转换为整数。 toNum: 将任意值转换为数字。 toSrc: 将函数转换为源码。 toStr: 将任意值转换为字符串。 topoSort: 拓扑排序实现。 trim: 删除字符串两边指定字符或空格。 tryIt: 在 try catch 块中运行函数。 type: 获取 JavaScript 对象的内部类型。 types: 仅用于生成 ts 定义文件。 ucs2: UCS-2 编解码。 unescape: 和 escape 相反,转义 HTML 实体回去。 union: 返回传入所有数组的并集。 uniqId: 生成全局唯一 id。 unique: 返回数组去重后的副本。 unzip: 与 zip 相反。 upperCase: 转换字符串为大写。 upperFirst: 将字符串的第一个字符转换为大写。 use: 使用 define 创建的模块。 utf8: UTF-8 编解码。 values: 返回对象所有的属性值。 vlq: vlq 编解码。 waitUntil: 等待直到条件函数返回真值。 waterfall: 按顺序执行函数序列。 wrap: 将函数封装到包裹函数里面, 并把它作为第一个参数传给包裹函数。 wx: 小程序 wx 对象的 promise 版本。 zip: 将每个数组中相应位置的值合并在一起。
2019-05-07 - 微信小程序插件 fail url not in domain list
[图片] 开发微信小程序插件,审核失败,上线的时候在微信开发工具上测试过,没有任何问题。 [图片] 但是,真机测试确实出现了问题,前提是真机预览“关闭调试功能“就会出现问题。 真机预览,关闭调试功能,wxrequest请求报错 fail url not in domain list 小程序插件服务器请求域名已经设置 小程序设置服务器请求域名也已经设置 微信开发者工具测试没有问题 并且真机预览,打开调试功能的时候,也会正常请求。 [图片] 打开调试,请求正常 [图片] 代码片段 wechatide://minicode/Y77DTYmF6LZe appid wx245c121cc7c4028d 微信版本 6.6.6 测试过的机型 iphone 5 10.3.3 小米 note2 iphone 5s 插件域名设置 [图片] 和微信小程序服务器域名设置 [图片]
2018-06-06 - vivo x9 组件textarea 设置 show-confirm-bar失效
vivo x9 组件textarea 设置 show-confirm-bar失效 设置成false,完成工具条还是存在 https://developers.weixin.qq.com/s/D6GOt8mt7T4s
2018-11-23 - 自定义导航栏所有机型的适配方案
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 思路 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码] [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 扩展 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 再扩展 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! 代码片段 https://developers.weixin.qq.com/s/Q79g6kmo7w5J
2019-02-25 - 实现小程序canvas拖拽功能
组件地址 https://github.com/jasondu/wx-comp-canvas-drag 实现效果 [图片] 如何实现 使用canvas 使用movable-view标签 由于movable-view无法实现旋转,所以选择使用canvas 需要解决的问题 如何将多个元素渲染到canvas上 如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层 如何实现拖拽元素 如何缩放、旋转、删除元素 看起来挺简单的嘛,就把上面这几个问题解决了,就可以实现功能了;接下来我们一一解决。 如何将多个元素渲染到canvas上 定义一个DragGraph类,传入元素的各种属性(坐标、尺寸…)实例化后推入一个渲染数组里,然后再循环这个数组调用实例中的渲染方法,这样就可以把多个元素渲染到canvas上了。 如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层 在DragGraph类中定义了判断点击位置的方法,我们在canvas上绑定touchstart事件,将手指的坐标传入上面的方法,我们就可以知道手指是点击到元素本身,还是删除图标或者变换大小的图标上了,这个方法具体怎么判断后面会讲解。 通过循环渲染数组判断是非点击到哪个元素到,如果点击中了多个元素,也就是多个元素重叠,那第一个元素就是最上层的元素啦。 ###如何实现拖拽元素 通过上面我们可以判断手指是否在元素上,当touchstart事件触发时我们记录当前的手指坐标,当touchmove事件触发时,我们也知道这时的坐标,两个坐标取差值,就可以得出元素位移的距离啦,修改这个元素实例的x和y,再重新循环渲染渲染数组就可以实现拖拽的功能。 如何缩放、旋转、删除元素 这一步相对比较难一点,我会通过示意图跟大家讲解。 我们先讲缩放和旋转 [图片] 通过touchstart和touchmove我们可以获得旋转前的旋转后的坐标,图中的线A为元素的中点和旋转前点的连线;线B为元素中点和旋转后点的连线;我们只需要求A和B两条线的夹角就可以知道元素旋转的角度。缩放尺寸为A和B两条线长度之差。 计算旋转角度的代码如下: [代码]const centerX = (this.x + this.w) / 2; // 中点坐标 const centerY = (this.y + this.h) / 2; // 中点坐标 const diffXBefore = px - centerX; // 旋转前坐标 const diffYBefore = py - centerY; // 旋转前坐标 const diffXAfter = x - centerX; // 旋转后坐标 const diffYAfter = y - centerY; // 旋转后坐标 const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180; const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180; // 旋转的角度 this.rotate = currentGraph.rotate + angleAfter - angleBefore; [代码] 计算缩放尺寸的代码如下: [代码]// 放大 或 缩小 this.x = currentGraph.x - (x - px); this.y = currentGraph.y - (x - px); [代码]
2019-02-20 - 小程序构建骨架屏的探索
首屏 一般情况下,在首屏数据未拿到之前,为了提升用户的体验,会在页面上展示一个loading的图层,类似下面这个 [图片] 其中除了菊花图以外网上还流传这各种各样的loading动画,在PC端上几乎要统一江湖了,不过最近在移动端上面看到不同于菊花图的加载方式,就是这篇文章需要分享的Skeleton Screen,中文称之为"骨架屏" 概念 A skeleton screen is essentially a blank version of a page into which information is gradually loaded. 在H5中,骨架屏其实已经不是什么新奇的概念了,网上也有各种方案生成对应的骨架屏,包括我们经常使用的知乎、饿了么、美团等APP都有应用骨架屏这个概念 图片来源网络,侵删 [图片] 方案 先从H5生成骨架屏方案开始说起,总的来说H5生成骨架屏的方案有2种 完全靠手写HTML和CSS方式给每个页面定制一套骨架屏 利用预渲染的方式生成静态骨架屏 第一套方案,毫无疑问是最简单最直白的方式,缺点也很明显,假如页面布局有修改的话,那么除了修改业务代码之外还需要额外修改骨架屏,增加了维护的成本。 第二套方案,一定程度上改善了第一套方案带来的维护成本增加的缺点,主要还是使用工具预渲染页面,获取到DOM节点和样式,保留页面结构,覆盖样式,生成灰色块盖在原有文本、图片或者是canvas等节点上面,最后将生成的HTML和CSS打包出来,就是一个带有骨架屏的页面。最后再利用webpack工具将生成的骨架屏插入到HTML里面,详细的话可以看看饿了么的分享,这里就不多描述了。 调研了下H5生成骨架屏的方案,对于小程序生成骨架屏的方案也有了一个大致的想法,主要有2个难点需要实现 预渲染 获取节点 预渲染 再说回饿了么提供的骨架屏的方案,使用 puppeteer 渲染页面(或者使用服务端渲染,vue或者react都有提供相应的方案),拿到DOM节点和样式,这里有一点需要注意的是,页面的渲染是需要初始化的数据,数据的来源可以是初始化的data(vue)或者mock数据,当然小程序是无法直接使用 puppeteer 来做预渲染(有另外的方案可以实现),需要利用小程序初始化的 data + template 渲染之后得到一个初始化结构作为骨架屏的结构 [代码]//index.js Page({ data: { motto: 'Hello World', userInfo: { avatarUrl: 'https://wx.qlogo.cn/mmopen/vi_32/SYiaiba5faeraYBoQCWdsBX4hSjFKiawzhIpnXjejDtjmiaFqMqhIlRBqR7IVdbKE51npeF6X1cXxtDQD2bzehgqMA/132', nickName: 'jay' }, lists: [ 'aslkdnoakjbsnfkajbfk', 'qwrwfhbfdvndgndghndeghsdfh', 'qweqwtefhfhgmjfgjdfghaefdhsdfgdfh', ], showSkeleton: true }, onLoad: function () { const that = this; setTimeout(() => { that.setData({ showSkeleton: false }) }, 3000) } }) //index.wxml <view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 有了上面的 data + template 之后,就有了一个初始化的页面结构,接下来就需要拿到节点信息 节点 小程序基础库1.4.0之后小程序基础库提供了一组新的API,可用于获取节点信息,具体API戳这里。 跟H5方式一样,根据class或者id获取节点信息,不同的是只能获取到当前的节点信息,无法获取到其父或者子节点信息,所以只能手动给需要渲染骨架屏的节点添加相应的class或者id [代码]<view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 约定2个特殊的class作为获取节点信息的标记[代码]skeleton-rect[代码]和[代码]skeleton-radius[代码],在页面中获取相应的[代码]top[代码]、[代码]left[代码]、[代码]width[代码]、[代码]height[代码]进行骨架屏的绘制 结果 [图片] 具体的调用方式和源码,请看 github ,最后求start 总结 上文有说到小程序也可以使用 page-skeleton-webpack-plugin 方式一样生成骨架屏,最重要的一点就是需要将小程序跑在chrome上面,后面的流程就一样了,至于怎么将小程序跑在chrome上面呢?可以利用 wept ,缺点就是目前作者已经停止维护这个工具了,不支持新版小程序的API。 说回来我这个生成骨架屏的方案,其实跟 page-skeleton-webpack-plugin 有点相似,不同的是,page-skeleton-webpack-plugin 采用离线渲染的方式生成静态骨架屏插入路由中,而我采用运行时先渲染页面默认结构,然后根据默认结构再绘制骨架屏。从性能角度出发确实不如 page-skeleton-webpack-plugin,但是也差不了多少了,主要还是小程序并没有提供类似服务端渲染的方案。目前从使用上来讲,还是有点小麻烦,需要默认数据撑开页面结构,需要给相应的节点添加class,后面有时间再研究下有没有更好的方案吧~~~
2019-02-20 - Wxml2Canvas -- 快速生成小程序分享图通用方案
Wxml2Canvas库,可以将指定的wxml节点直接转换成canvas元素,并且保存成分享图,极大地提升了绘制分享图的效率。目前被应用于微信游戏圈、王者荣耀、刺激战场助手等小程序中。 github地址:https://github.com/wg-front/wxml2canvas 一、背景 随着小程序应用的日渐成熟,多处场景需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 对于简单的分享图,比如固定大小的背景图加几行简短文字构成的分享小图,我们可以利用官方提供的canvas接口将元素直接绘制, 虽然繁琐了些,但能满足基本要求。 对于复杂的分享图,比如用户在微信游戏圈发表完话题后,需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制。又如王者荣耀助手小程序,需要将十人对局的详细战绩绘制成分享图,包含英雄数据、装备、技能、对局结果等信息,要绘制100多张图片和大量的文字信息,如果依旧使用官方的接口一步一步绘制,对开发者来说简直就是一场噩梦。我们急需一种通用、高效的方式完成上述的工作。 在这样的背景下,wxml2cavnas诞生了,作为一种分享图绘制的通用方案,它不仅能快速的绘制简单的固定小图,还能直接将wxml元素真实地转换成canvas元素,并且适配各种机型。无论是复杂的图文混排的富文本内容,还是展现形式多样的战绩结果页,都可以利用wxml2cavnas完美地快速绘制并生成所期望的分享图片。 二、Wxml2Canvas介绍及示例 1. 介绍 Wxml2Cavnas库,是一个生成小程序分享图的通用方案,提供了两种绘制方式: 封装基础图形的绘制接口,包括矩形、圆形、线条、图片、圆角图片、纯文本等,使用时只需要声明元素类型并提供关键数据即可,不需要再关注canvas的具体绘制过程; wxml直接转换成canvas元素,使用时传入待绘制的wxml节点的class类名,并且声明绘制此节点的类型(图片、文字等),会自动读取此节点的computedStyle,利用这些数据完成元素的绘制。 2. 生成图示例 下面是两张极端复杂的分享图。 2.1 游戏圈话题 [图片] 点击查看完整长图 2.2.2 王者荣耀战绩 [图片] 点击查看完整大图 三、小程序的特性及局限 小程序提供了如下特性,可供我们便捷使用: measureText接口能直接测量出文本的宽度; SelectorQuery可以查询到节点对应的computedStyle。 利用第一条,我们在绘制超长文本时便于文本的省略或者换行,从而避免文字溢出。 利用第二条,我们可以根据class类名,直接拿到节点的样式,然后将style转换成canvas可识别的内容。 但是和html的canvas相比,小程序的canvas局限性很多。主要体现在如下几点: 不支持base64图片; 图片必须下载到本地后才能绘制到画布上; 图片域名需要在管理平台加入downFile安全域名; canvas属于原生组件,在移动端会置于最顶层; 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容以及图片节点的链接。 针对以上问题,我们需要将base64图片转换jpg或png格式的图片,实现图片的统一下载逻辑,并且离屏绘制内容。针对第五条,好在SelectorQuery可以获取到节点的dataset属性,所以我们需要在待绘制的节点上显示地声明其类型(imgae、text等),并且显示地传入文本内容或图片链接,后文会有示例。 四、Wxml2Canvas使用方式 1. 初始化 首先在wxml中创建canvas节点,指定宽高: [代码] <canvas canvas-id="share" style="height: {{ height * zoom }}px; width: {{ width * zoom }}px;"> </canvas> [代码] 引入代码库,创建DrawImage实例,并传入如下参数: [代码] let DrawImage = require('./wxml2canvas/index.js'); let zoom = this.device.windowWidth / 375; let width = 375; let height = width * 3; let drawImage = new DrawImage({ element: 'share', // canvas节点的id, obj: this, // 在组件中使用时,需要传入当前组件的this width: width, // 宽高 height: height, background: '#161C3A', // 默认背景色 gradientBackground: { // 默认的渐变背景色,与background互斥 color: ['#17326b', '#340821'], line: [0, 0, 0, height] }, progress (percent) { // 绘制进度 }, finish (url) { // 画完后返回url }, error (res) { console.log(res); // 画失败的原因 } }); [代码] 所有的数字参数均以iphone6为基准,其中参数width和height决定了canvas画布的大小,规定值是在iphone6机型下的固定数值; zoom参数的作用是控制画布的缩放比例,如果要求画布自适应,则应传入 windowWidth / 375,windowWidth为手机屏幕的宽度。 2. 传入数据,生成图片 执行绘制操作: [代码] drawImage.draw(data, this); [代码] 执行绘制时需要传入数据data,数据的格式分为两种,下面展开介绍。 2.1 基础图形 第一种为基础的图形、图文绘制,直接使用官方提供接口,下面代码是一个基本的格式: [代码] let data = { list: [{ type: 'image', url: 'https://xxx', class: 'background_image', // delay: true, x: 0, y: 0, style: { width: width, height: width } }, { type: 'text', text: '文字', class: 'title', x: 0, y: 0, style: { fontSize: 14, lineHeight: 20, color: '#353535', fontFamily: 'PingFangSC-Regular' } }] } [代码] 如上,type声明了要元素的类型,有image、text、rect、line、circle、redius_image(圆角图)等,能满足绝大多数情况。 class类名指定了使用的样式,需要在style中写出,符合css样式规范。 delay参数用来异步绘制元素,会把此元素放在第二个循环中绘制。 x,y用来指定元素的起始坐标。 将css样式与元素分离的目的是便于管理与复用。 此种方式每个元素都相互独立,互不影响,能够满足自由度要求高的情况,可控性高。 2.2 wxml转换 第二种方式为指定wxml元素,自动获取,下面是示例: [代码] let data = { list: [{ type: 'wxml', class: '.panel .draw_canvas', limit: '.panel' x: 0, y: 0 }] } [代码] 如上,type声明为wxml时,会查找所有类名为draw_canvas的节点,并且加入到绘制队列中。 class传入的第一个类名限定了查询的范围,可以不传,第二个用来指定查找的节点,可以定义为任意不影响样式展现的通用类名。 limit属性用来限定相对位置,例如,一个文本的位置(left, top) = (50, 80), class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80)绘制。 由于小程序节点元素查询接口的局限,无法直接获取节点的文本内容和图片标签的src属性,也无法直接区分是文本还是图片,但是可以获取到dataset,所以我们需要在节点上显示地声明data-type来指明类型,再声明data-text传入文字或data-url传入图片链接。下面是个示例: [代码] <view class="panel"> <view class="panel__img draw_canvas" data-type="image" data-url="https://xxx"></view> <view class="panel__text draw_canvas" data-type="text" data-text="文字">文字</view> </view> [代码] 如上,会查询到两个节点符合条件,第一个为image图片,第二个为text文本,利用SelectorQuery查询它们的computedStyle,分别得到left、top、width、height等数据后,转换成canvas支持的格式,完成绘制。 除此之外,下面的示例功能更加丰富: [代码] <view class="panel"> <view class="panel__text draw_canvas" data-type="background-image" data-radius="1" data-shadow="" data-border="2px solid #000"></view> <view class="panel__text draw_canvas" data-type="text" data-background="#ffffff" data-padding="2 3 0 0" data-delay="1" data-left="10" data-top="10" data-maxlength="4" data-text="这是个文字">这是个文字</view> </view> [代码] 如上,第一个data-type为background-image,表示读取此节点的背景图片,因为可以通过computedStyle直接获取图片链接,所以不需要显示传入url。声明data-radius属性,表示要将此图绘成乘圆形图片。data-border属性表示要绘制图片的边框,虽然也可以通过computedStyle直接获取,但是为了避免非预期的结果,还是要声明传入,border格式应符合css标准。此外,图片的box-shadow等样式都会根据声明绘制出来。 第二个文本节点,声明了data-background,则会根据节点的位置属性给文字增加背景。 data-padding属性用来修正背景的位置和宽高。data-delay属性用来延迟绘制,可以根据值的大小,来控制元素的层级,data-left和data-top用来修正位置,支持负值。data-maxlength用来限制文本的最大长度,超长时会截取并追加’…’。 此外,data-type还有inline-text,inline-image等行内元素的绘制,其实现较为复杂,会在后文介绍。 五、Wxml2Canvas实现原理 1. 绘制流程 整个绘制流程如下: [图片] 因为小程序的限制,只能在画布上绘制本地图片,所以统一先对图片提前下载,然后再绘制,为了避免图片重复下载,内部维护一个图片列表,会对相同的图片链接去重,减少等待时间。 2. 基本图形的实现 基础图形的绘制比较简单,内部实现只是对基础能力的封装,使用者不用再关注canvas的绘制过程,只需要提供关键数据即可,下面是一个图片绘制的实现示例: [代码] function drawImage (item, style) { if(item.delay) { this.asyncList.push({item, style}); }else { if(item.y < 0) { item.y = this.height + item.y * zoom - style.height * zoom; }else { item.y = item.y * zoom; } if(item.x < 0) { item.x = this.width + item.x * zoom - style.width * zoom; }else { item.x = item.x * zoom; } ctx.drawImage(item.url, item.x, item.y, style.width * zoom, style.height * zoom); ctx.draw(true); } } [代码] 如上,x,y值坐标支持传入负值,表示从画布的底部和右侧计算位置。 3. Wxml转Canvas元素的实现 3.1 computedStyle的获取 首先需要获取wxml的样式,代码示例如下: [代码] query.selectAll(`${item.class}`).fields({ dataset: true, size: true, rect: true, computedStyle: ['width', 'height', ...] }, (res) => { self.drawWxml(res); }) [代码] 3.2 块级元素的绘制 对于声明为image、text的元素,默认为块级元素,它们的绘制都是独立进行的,不需要考虑其他的元素的影响,以wxml节点为圆形的image为例,下面是部分代码: [代码] if(sub.dataset.type === 'image') { let r = sub.width / 2; let x = sub.left + item.x * zoom; let y = sub.top + item.y * zoom; let leftFix = +sub.dataset.left || 0; let topFix = +sub.dataset.top || 0; let borderWidth = sub.borderWidth || 0; let borderColor = sub.borderColor; // 如果是圆形图片 if(sub.dataset.radius) { // 绘制圆形的border if(borderWidth) { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setStrokeStyle(borderColor) ctx.setLineWidth(borderWidth) ctx.stroke() ctx.closePath() } // 绘制圆形图片的阴影 if(sub.boxShadow !== 'none') { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setFillStyle(borderColor); setBoxShadow(sub.boxShadow); ctx.fill() ctx.closePath() } // 最后绘制圆形图片 ctx.save(); ctx.beginPath(); ctx.arc((x + r), (y + r) - limitTop, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(url, x + leftFix * zoom, y + topFix * zoom, sub.width, sub.height); ctx.closePath(); ctx.restore(); }else { // 常规图片 } } [代码] 如上,块级元素的绘制和基础图形的绘制差异不大,理解起来也很容易,不再多述。 3.3 令人头疼的行内元素的绘制 当wxml的data-type声明为inline-image或者inline-text时,我们认为是行内元素。行内元素的绘制是一个难点,因为元素之前存在关联,所以不得不考虑各种临界情况。下面展开细述。 3.3.1 纯文本换行 对于长度超过一行的行内元素,需要计算出合适的换行位置,下图所示的是两种临界情况: [图片] [图片] 如上图所示,第一种情况为最后一行只有一个文字,第二种情况最后一行的文字长度和宽度相同。虽然长度不同,但都可通过下面代码绘制: [代码] let lineNum = Math.ceil(measureWidth(text) / maxWidth); // 文字行数 let sinleLineLength = Math.floor(text.length / lineNume); // 向下取整,保证多于实际每行字数 let currentIndex = 0; // 记录文字的索引位置 for(let i = 0; i < lineNum; i++) { let offset = 0; // singleLineLength并不是精确的每行文字数,要校正 let endIndex = currentIndex + sinleLineLength + offset; let single = text.substring(currentIndex, endIndex); // 截取本行文字 let singleWidth = measureWidth(single); // 超长时,左移一位,直至正好 while(singleWidth > maxWidth) { offset--; endIndex = currentIndex + sinleLineLength + offset; single = text.substring(currentIndex, endIndex); singleWidth = measureWidth(single); } currentIndex = endIndex; ctx.fillText(single, item.x, item.y + i * style.lineHeight); } // 绘制剩余的 if(currentIndex < text.length) { let last = text.substring(currentIndex, text.length); ctx.fillText(last, item.x, item.y + lineNum * style.lineHeight); } [代码] 为了避免计算太多次,首先算出大致的行数,求出每行的文字数,然后移位索引下标,求出实际的每行的字数,再下移一行继续绘制,直到结束。 3.3.2 非换行的图文混排 [图片] 上图是一个包含表情图片和加粗文字的混排内容,当使用Wxml2Canvas查询元素时,会将第一行的内容分为五部分: 文本内容:这是段文字; 表情图片:发呆表情(非系统表情,image节点展现); 表情图片:发呆表情; 文本内容:这也; 加粗文本内容:是一段文字,这也是文字。 对于这种情况,执行查询computedStyle后,会返回相同的top值。我们把top值相同的元素聚合在一起,认为它们是同一行内容,事实也是如此。因为表情大小的差异以及其他影响,默认规定top值在±2的范围内都是同一行内容。然后将top值的聚合结果按照left的大小从左往右排列,再一一绘制,即可完美还原此种情况。 3.3.3 换行的图文混排 当混排内容出现了换行情况时,如下图所示: [图片] 此时的加粗内容占据了两行,当我们依旧根据top值归类时,却发现加粗文字的left值取的是第二行的left值。这就导致加粗文字和第一部分的文字的top值和left值相同,如果直接绘制,两部分会发生重叠。 为了避免这种尴尬的情况,我们可以利用加粗文字的height值与第一部分文字的height值比较,显然前者是后者的两倍,可以得知加粗部分出现了换行情况,直接将其放在同组top列表的最后位置。换行的部分根据lineHeight下移绘制,同时做记录。 最后一部分的文本内容也出现了换行情况,同样无法得到真正的起始left值,并且其top值与上一部分换行后的top值相同。此时应该将他的left值追加加粗换行部分的宽度,正好得到真正的left值,最后再绘制。 大多数的行内元素的展现形式都能以上述的逻辑完美还原。 六、总结 基于基础图形封装和wxml转换这两种绘制方式,可以满足绝大多数的场景,能够极大地减少工作量,而不需要再关注内部实现。在实际使用中,二者并非孤立存在,而更多的是一起使用。 [图片] 如上图所示,对于列表内容我们利用wxml读取绘制,对于下部的白色区域,不是wxml节点内容,我们可以使用基础图形绘制方式实现。二者的结合更加灵活高效。 目前Wxml2Canvas已经在公司内部开源,不久会放到github上,同时也在不断完善中,旨在实现更多的样式展现与提升稳定性和绘制速度。 如果有更好的建议与想法,请联系我。
2019-02-28 - 【转】如何在微信小程序里面实现跨页面通信?
我们在处理业务需求的时候,常常会遇到一些情况,在二级或者三级页面进行某些操作或者变更后,需要将结果通知到上级页面去。比如: 选择了某些配置项,点击保存后,外部页面能够立即变更 在上传头像页面,上传完毕后,外部页面的头像能够立即显示为新头像。 所以,这个时候就涉及到如何在页面之间通信的问题了。 跨页面通信进一步说其实就是一个程序内部的事件通知机制问题,在其他平台或者OS上都一些相应的实现,比如: iOS SDK自带的 NotificationCenter Android 平台著名的第三方库 EventBus 目前微信小程序官方SDK还没有提供 Event API 来帮助开发者实现页面间通信,所以我们今天来看看,自己如何实现这样一个简单的小工具。 说到这里就不得不说“云梦”的微信小程序版本了,在小程序开始公测后,我们也在第一时间将“云梦”的基本功能移植到了小程序平台上。 整个过程相当顺利,除了小程序的IDE还不是太稳定外,基本上没啥大问题。 开发过程和React-Native基本相似,大概一天时间就搞定了。 Quick And Dirty我们知道,在小程序里面一个页面的变化,是通过调用 setData 函数来实现的。所以想做到在二级页面里让一级页面产生变化,最 Quick And Dirty 的做法就是把一级页面的 this 传入到二级页面去,这样我们在二级页面调用 page1.setData(…) 就可以立即引发外部的变化。 但是这并不是一个好的方案,不仅产生了页面的耦合,而且也并不能处理复杂的数据逻辑,因为二级页面不并清楚也不应该关心一级页面想怎么处理当前数据。所以二级页面只应该把变更后的数据通知给一级页面即可,至于一级页面是想刷新界面,还是想本地存储或者发起网络通信,别人都不需知晓了。 简单的Callback如果只是想把数据通知给外部页面,那应该怎么做呢? 我们来看看第二个方案,如果想产生一个通知,这里就需要用到 callback 机制了。 即关心数据变化的页面,注册一个 callback 函数到一个公共的地方;而数据变更者在变更数据后,将新的数据放入同一个公共的地方;在放入数据时,同时调用这个 callback 函数,让 callback 函数实现者接收到这个变化。 哪这个公共的地方在哪里呢? 第一反应就是 app.js 里面,因为小程序提供了一个 API 叫做 getApp(),让 page 初始化时,可以通过以下代码: var app = getApp()来获取 app 实例,从而实现全局的数据共享,并且微信也很贴心的在 Demo 代码里面留了一个 globalData 字段,以暗示开发者这里是可以用来存储全局数据的。 App({ ... globalData:{ userInfo:null } ...})基于 app.js 方案的伪代码如下: //app.jsApp({ addListener: function(callback) { this.callback = callback; }, setChangedData: function(data) { this.data = data; if(this.callback != null) { this.callback(data); } } })然后我们在一级页面的 onLoad中 调用 addListener: //page1.jsvar app = getApp()Page({ onLoad: function () { app.addListener(function(changedData) { that.setData({ data: changedData }); }); } })在二级页面数据变更的地方调用: //page2.jsvar app = getApp()Page({ onBtnPress: function() { app.setChangedData('page2-data'); } })一个基本合格的方案以上就是跨页面通信的最基本原理,不过这也是一个很 dirty 的方案,因为上面的代码只能支持一种 Event 的通知,而且也不能针对这个 Event 添加多个监听者(比如有多个页面需要同时知道某数据变更)。 让我们来看看一个基本合格的 Event 管理器应该具备怎样的能力? 支持多种 Event 的通知 支持对某一 Event 可以添加多个监听者 支持对某一 Event 可以移除某一监听者 将 Event 的存储和管理放在一个单独模块中,可以被所有文件全局引用 根据以上的描述,我们来设计一个新的 Event 模块,对应上面的能力,它应该具有如下三个函数: on 函数,用来向管理器中添加一个 Event 的 Callback,且每一个 Event 必须有全局唯一的 EventName,函数内部通过一个数组来保存同一 Event 的多个 Callback remove 函数,用来向管理器移除一个 Event 的 Callback emit 函数,用来触发一个 Event 我们在小程序的 utils 目录中,新建一个 event.js 文件,来作为一个独立的模块,伪代码如下: //event.jsvar events = {};function on(name, callback) { var callbacks = events[name]; addToCallbacks(callbacks, callback); }function remove(name, callback) { var callbacks = events[name]; removeFromCallbacks(callbacks, callback); }function emit(name, data) { var callbacks = events[name]; emitToEveryCallback(callbacks, data); }exports.on = on;exports.remove = remove;exports.emit = emit;我们来看看在一二级页面应该如何来使用这个 Event 模块 在二级页面中触发事件: //page2.jsvar event = require('../../utils/event.js');Page({ onBtnPress: function() { event.emit('DataChanged', 'page2-data'); } });在一级页面的 onLoad 中监听事件,onUnload 中取消监听: //page1.jsvar event = require('../../utils/event.js');Page({ onLoad: function() { var that = this; event.on('DataChanged', function(changedData) { that.setData({ data: changedData }); }); }, onUnload: function() { event.remove('DataChanged', ...); } });咦,似乎哪里不对? remove 需要接受两个参数,第一个是 EventName,第二个是 Callback,但是我们的 Callback 以匿名函数的方式写在了 event.on(...) 的调用语句里面 好吧,那我们不得不修改一下语句的调用方式: //page1.jsvar event = require('../../utils/event.js');Page({ onDataChanged: function(changedData) { this.setData({ data: changedData }) }, onLoad: function() { event.on('DataChanged', this.onDataChanged); }, onUnload: function() { event.remove('DataChanged', this.onDataChanged); } });这样就 OK 了么?NO NO NO NO 熟悉 Javascript this 这个大坑的朋友们一定会知道,在 onDataChanged 这个函数中调用的 this 并不是我们 Page 中的那个 this,所以根本不可能调用到 this.setData(....),于是我们用 bind 大法稍微调整一下: onLoad: function() { event.on('DataChanged', this.onDataChanged.bind(this)); }onUnload: function() { event.remove('DataChanged', this.onDataChanged.bind(this)); }现在OK了么?NO NO NO NO!如果大伙敲代码试试,就会发现依然还是不行! 因为 this.onDataChanged.bind(this)会产生一个新的匿名函数,即 bind的 返回值是一个函数,那么在 onLoad 和 onUnload 里面,各自调用了 bind 大法,从而产生了各自的匿名函数,也就是说 event.remove(...) 塞进去的那个函数,并不是 event.on(...) 塞进去的那个函数,这样就造成了 remove 时无法正确匹配。removeFromCallbacks 的伪代码大致如下: function removeFromCallbacks(callbacks, callback) { var newCallbacks = []; for(var item in callbacks) { if(item != callback) { newCallbacks.push(item); } } return newCallbacks; }所以我们会发现 remove 传入的 callback 永远无法在 callbacks 数组中被匹配到,从而也就无法正确移除了。 最终的代码实现当 EventName + Callback 无法唯一决定需要移除的监听者时,那么自然想到的就是再增加一个 key 值,我们可以用Page自身的某个特性来做 key,比如 page name ,新的 remove 原型如下: function remove(eventName, pageName, callback);pageName 是一个字符串,如果开发者不能做到全局内 page name 唯一的话(比如开发者一不小心写错了),那就可能会出现后来监听者冲掉前面监听者的情况,从而造成无法收到通知的 bug。 所以这里看起来还是用 page 的 this 做 key 比较靠谱,修改后的函数原型如下: function on(name, self, callback); function remove(name, self, callback);让我们来看看内部具体怎么实现。以下是一个完整的 on 函数实现: function on(name, self, callback) { var tuple = [self, callback]; var callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.push(tuple); } else { events[name] = [tuple]; } }第二行我们将 self (即 page 的 this)和 callback 合并成一个 tuple 第三行从 events 容器中,取出该 EventName 下的监听者数组 callbacks 如果该数组存在,则将 tuple 加入数组;如果不存在,则新建一个数组。 remove的完整实现: function remove(name, self) { var callbacks = events[name]; if (Array.isArray(callbacks)) { events[name] = callbacks.filter((tuple) => { return tuple[0] != self; }); } }第二行从 events 容器中,取出该 EventName 下的监听者数组 callbacks 如果 callbacks 不存在,则直接返回 如果存在,则调用 callbacks.filter(fn) 方法 filter 方法的含义是通过 fn 来决定是否过滤掉 callbacks 中的每一个项。fn 返回 true 则保留,fn 返回 false 则过滤掉。所以我们调用 callbacks.filter(fn) 后,callbacks 中的每一个 tuple 都会被依次判定。 fn的定义为: (tuple) => { return tuple[0] != self; }tuple 中的第一个元素 self 和 remove 传入的 self 相比较,如果不相等则返回 true 被保留,如果相等则返回 false 被过滤掉。 callbacks.filter(fn) 会返回一个新的数组,然后重新写入 events[name],最终达到移除callbacks中某一项的逻辑。 最后再来看看emit的实现: function emit(name, data) { var callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.map((tuple) => { var self = tuple[0]; var callback = tuple[1]; callback.call(self, data); }); } }第二行从 events 容器中,取出该 EventName 下的监听者数组 callbacks 如果 callbacks 不存在,则直接返回 如果存在,则调用 callbacks.map(fn) 方法 和 filter 的用法类似,map 函数的作用相当于 for 循环,依次取出 callbacks 中的每一个项,然后对其执行 fn(tuple),从其名字就可以看出 map 就是映射变换的意思,将 item 变换为另外一种东西,这个映射关系就是fn。 fn 的定义为: (tuple) => { var self = tuple[0]; var callback = tuple[1]; callback.call(self, data); }对传入的 tuple,分别取出 self 和 callback,然后调用 Javascript 的 call大法: fn.call(this, args)从而最终实现调用到监听者的目的。 讲到这里就基本上差不多了,因为 Event 模块持有了 Page 的 this,所以一定要在 Page 的 Unload 函数中调用 event.remove(…),不然会造成内存泄露。 源代码event.js 的完整源代码和Demo请见 https://github.com/danneyyang/weapp-event 原作者: danneyyang
2016-11-25 - 小程序自定义事件
事件的作用: 事件可用于不同页面间的数据交互,例如A页面监听了名为“test”的事件,当事件发生时改变页面标题。 B页面,比如点击某个按钮触发并广播了“test”事件。此时A页面的标题将改变。 实现: -- 在app.js 中定义事件 eventArr: [], // 事件数组 ,用于存储事件 addEventListener: function (eventName, cb) { // 注册自定义事件 var len = this.eventArr.length if (len < 1) { this.eventArr.push({ eventName: eventName, cbArray: [{ callback: cb }] }) } else { for (var i = 0; i < len; i++) { if (this.eventArr[i].eventName == eventName) { this.eventArr[i].cbArray.push({ callback: cb }) return } } this.eventArr.push({ eventName: eventName, cbArray: [{ callback: cb }] }) } }, sendEvent: function (eventName, paramObj) { // 触发事件 var len = this.eventArr.length for (var i = 0; i < len; i++) { if (this.eventArr[i].eventName == eventName) { var pa = paramObj || {} for (var j = 0; j < this.eventArr[i].cbArray.length; j++) { this.eventArr[i].cbArray[j].callback(pa) } break } } }, removeEvent: function (eventName) { // 移除事件 var len = this.eventArr.length for (var i = 0; i < len; i++) { if (this.eventArr[i].eventName == eventName) { this.eventArr.splice(i, 1) break } } } -- 在其他任意页面监听事件(建议在onLoad中监听) app.addEventListener('test',function(res){ // 收到事件后的处理逻辑,res为sendEvent的第二个参数 }.bind(this)) -- 触发事件 app.sendEvent('test',{title:'xiapu'})
2019-02-13 - 跪求官方出个富文本编辑器吧!!!
算我求求你们了。赶紧出一个吧!
2018-12-24 - 微信开发工具公众号模拟器无法正常点击
[图片] 公众号开发 别的同事使用是正常的,而我这边点击事件总是有问题,事件的触发,总是需要在按钮的上面不远处才能点击到。 模拟器外的其他程序点击均没有问题。重装开发工具,仍有这个问题。
2018-09-12