- 小白变大神二:云数据库基础读写工具函数
《小白变大神,微信小程序云开发快速入门与成本控制实战》系列文章 第二篇:云数据库基础读写工具函数 前言 在上一篇文章中,我们创建了一个空页面 Todolist,介绍了数据库权限的基础知识,然后通过在表名称的前面添加 p_ 前缀来区分生产环境和开发环境。 阅读本篇文章之前,强烈建议你先阅读上一篇文章:《小白变大神:一、初识数据库》,并且请根据上一篇文章创建对应的数据库表。 在本篇文章中,我们将继续完善 Todolist 的功能,并提供几个较为基础的数据库读写工具函数。 学习完本篇文章后,你将可直接把读写数据库的代码库应用到你的项目中,提高开发效率。 Todolist 的写入与刷新 使用微信默认样式 考虑到本教程的主要目的是讲解云开发环境的 JavaScript 代码,因此我们在页面UI上使用微信默认方案 WeUI,请向你的 app.json 文件添加下面的配置,这样当你把我的 wxml 代码复制到你的项目中时,你能得到和我一样的页面UI效果。 [代码]// 向app.json文件添加下面的配置,让你的页面UI效果和我的一样 { "useExtendedLib": { "weui": true }, "style": "v2", } [代码] 我假设你已经了解了小程序的 WeUI 和 WXML。 获取代码库 WxMpCloudBooster 建议你在阅读本系列文章时,自己新建一个项目,然后跟着我的步骤在你的电脑上实践。因此,在本篇文章中,你需要先获取 WxMpCloudBooster 库中的代码。 你可以在github代码库:sdjl/WxMpCloudBooster下载,或者使用如下的命令: [代码]# 获取项目 git clone https://github.com/sdjl/WxMpCloudBooster.git # 切换到本篇文章(文章二)对应的代码库 cd WxMpCloudBooster git checkout article2 [代码] 注意:建议 checkout 到 article2,否则你拿到的代码可能和本文中不一致 添加 Todolist 页面UI 获取 WxMpCloudBooster 项目代码并切换到 article2 分支后,请你复制 miniprogram/pages/todo/todo.wxml 文件到你的项目中。刷新页面获得如图所示效果: [图片] 提示:为了减少文章篇幅,我不在文章中放置完整的 wxml 代码,你可以点击这里跳转查看 article2 的代码 数据绑定函数:_inputChange 查看代码库(代码库指WxMpCloudBooster,以后同)中的 miniprogram/pages/todo/todo.js 文件,你会看到如下代码: [代码]'use strict' import utils from '../../utils/utils' Page({ behaviors: utils.behaviors(), data: { todo_list: [], // 未完成事项列表 done_list: [], // 已完成事项列表 new_title: '', // 新增待办事项的内容,与输入框绑定 }, // ... }) [代码] 首先,我们在 data 中定义了一个 new_title 变量,用于存放 <textarea> 输入框中新建 todo 的内容。但是,当我们在 <textarea> 中输入文字时,系统并不会自动更新 data.new_title 的值,因此我们需要在 wxml 中使用 _inputChange 函数来实现数据绑定。代码如下: [代码]<textarea class="weui-textarea" value="{{new_title}}" bind:input="_inputChange" data-field="new_title" placeholder="这里是一个<textarea>输入框" placeholder-class="weui-input__placeholder" /> [代码] _inputChange 函数可以在代码库的 miniprogram/utils/page_behaviors.js 文件中找到: [代码]module.exports = Behavior({ methods: { // page_behaviors.js 中的函数都有下划线 _ _inputChange(e) { const { field } = e.currentTarget.dataset this.setData({ [`${field}`]: e.detail.value }) }, }, }) [代码] 我假设你已经了解了小程序的 事件机制 和 Behavior 为了在 todo.wxml 中使用 _inputChange 函数,我们需要在 todo.js 中引入 behaviors : [代码]Page({ behaviors: utils.behaviors(), }) [代码] 这里的 behaviors 是 Page 的一个内置属性,可在 Page文档中 查看。 在 utils.js 中可查看 behaviors 函数: [代码]const PAGE_BEHAVIORS = require('page_behaviors') const utils = { behaviors(){ return [ PAGE_BEHAVIORS, ] } } [代码] 提醒:当你修改 Behavior 文件时,需要重新编译小程序 这样,当你在 <teatarea> 中输入文字时,data.new_title 的值会自动更新,从而实现了数据绑定。 随着本教程的深入,page_behaviors.js 文件的功能会越来越丰富。 写入数据函数:utils.addDoc 假设你已经根据上一篇文章创建了 todo 和 p_todo 表,且两个表的数据库权限均选择了“自定义安全规则”,并使用了如下的安全配置: [代码]{ "read": "doc._openid == auth.openid", "write": "doc._openid == auth.openid" } [代码] 为了向 todo 表中写入数据,我们需使用 utils.js 中的 addDoc 函数: [代码]addDoc(c, d) { const _ = this return new Promise((resolve, reject) => { _.coll(c).add({ data: d }) .then(res => { resolve(res._id) }) .catch(e => { reject(e) }) }) } [代码] 正如上一篇文章中所说,addDoc 中的 coll 函数会自动根据环境判断使用 todo 还是 p_todo 表。当你在微信开发者工具中运行时,addDoc 会向 todo 表中写入数据,而在生产环境或真机预览时,addDoc 会向 p_todo 表中写入数据,并且以后提供的所有数据库操作函数都会自动判断。 然后在 todo.js 中使用 addDoc 函数: [代码]async addTodo (e) { const _ = this const { new_title } = _.data utils.addDoc('todo', {title: new_title, status: '未完成'}) .then(new_todo_id => { _.updateTodoList() _.setData({ new_title: '' // 清空输入框 }) }) } [代码] 这样就完成了点击“添加 todo”按钮时创建新 todo 的功能。 数据读取函数:utils.docs 与 utils.myDocs 新建 todo 后,需要在 updateTodoList 函数中重新读取 todo_list 列表,并重新渲染页面。 因此我们需要一个读取数据的函数 utils.docs。 但是,在上一篇文章中我们讲到,当你使用“自定义安全规则”且规则中有 auth.openid == doc._openid 时,需要在查询语句中添加 _openid: ‘{openid}’ 条件(否则会抛出没有权限的异常)。 因此,代码库中还提供了 utils.myDocs 函数,此函数专门用于读取上述权限设置的数据。 utils.js 文件中这两个函数的定义如下(完整的代码请查看代码库): [代码]docs ({ c, w = {}, page_num = 0, page_size = 20, only = '', except = '', created = false, order_by = {}, mine = false, } = {}) { // 代码请查看代码库 }, myDocs (args) { args.mine = true return this.docs(args) } [代码] 有了这两个工具函数后,我们就可以在 updateTodoList 函数中重新读取 todo_list,并重新渲染页面。 [代码]async updateTodoList () { const _ = this utils.myDocs({c: 'todo', w: {status: '未完成'} }) .then(docs => { _.setData({ todo_list: docs }) }) } [代码] utils.docs 的参数详解 utils.docs 函数支持多个参数,下面我们来详细解释这些参数的用法: c 参数 c 参数是唯一一个必传的,表示 collection 集合名称。当运行在生产环境时会自动添加 p_ 前缀,因此请勿在这里输入p_前缀,其他数据库操作函数也一样。 w 参数 w 参数表示查询条件 where,如 w: {status: ‘未完成’}。 还可以在 w 中使用“点表示法”,如: [代码]utils.docs( c: 'xxx', w: { 'people[0].name': '张三' } ) [代码] 这里的查询条件规则与官方文档中的规则一致。 page_num 和 page_size 参数 page_num 和 page_size 参数用于分页读取数据,page_num 从0开始,page_size 最大为20(微信限制每次最多读取20条数据)。 我个人认为,微信限制前端每次最多读取20条数据主要是为了避免加载时间过长,从而保障用户体验(毕竟有许多小白什么代码都敢写,可查看 get函数文档)。 在下篇文章中我会提供 utils.allDocs 函数,可实现仅消耗一次调用次数就能读取所有数据。 only 和 except 参数 only 和 except 参数用于控制返回的字段,当你仅需要返回 _id 和 _openid 时,可以这样写: [代码]utils.docs( c: 'xxx', only: '_id, _openid' ) [代码] 同样的,当你不需要返回 _openid 和 created 字段,其他字段都返回时,可以这样写: [代码]utils.docs( c: 'xxx', except: '_openid, created' ) [代码] 但是,当你使用 only 时,无论 only 中是否写了 _id,_id 都会返回。除非你同时使用 except 显性排除了 _id,如: [代码]utils.docs( c: 'xxx', only: 'title, content', except: '_id' ) [代码] 这是因为通常使用 only 时,我们实际上需要 _id 字段,但是每次都写 _id 会很麻烦。 created 参数 created 参数用于控制是否给返回的数据添加创建时间字段,共有4个字段:created、created_str、yymmdd、hhmmss。 通常你并不需要在创建数据时写入当前时间字段,因为我们可以从 _id 中分析出创建这个数据的时间。除非你需要根据此字段进行排序或其他查询操作。 这4个时间字段的格式如下: created:javascript 的 Date 对象 created_str:完整时间字符串,如:‘2024-07-26 12:02:00’ yymmdd:日期,如:‘2024-07-26’ hhmmss:时间,如:‘12:02:00’ 注意:如果数据是在云函数中创建的,需要把云函数的时区设置为 UTC+8(即在云函数中添加 TZ=Asia/Shanghai 配置),这个我们以后再详细讲解。 样例代码如下: [代码]const docs = await utils.docs({ c: 'xxx', created: true, }) console.log(docs[0].created) // Date 时间对象 console.log(docs[0].yymmdd) // '2024-07-26' [代码] order_by 参数 order_by 参数用于控制返回数据的排序,当你仅需根据一个字段升序排序时,可以直接写字段名,如: [代码]utils.docs({ c: 'xxx', order_by: 'rank', }) [代码] 当需要使用降序或多字段排序时,需传入一个对象,如: [代码]utils.docs({ c: 'xxx', order_by: {school: true, grade: 'desc', 'math.score': 0} }) [代码] 以上查询先按 school 升序,再按 grade 降序,最后按 math.score 降序。 在 order_by 中,‘asc’、1 或 true 均表示升序,‘desc’、0 或 false 均表示降序。 mine 参数与 myDocs 函数 由于我们使用了“自定义安全规则”且读取规则为 auth.openid == doc._openid(以后简称“自己的数据”),此时系统要求我们在查询数据时必须在 where 中添加 _openid: ‘{openid}’ 条件,否则会抛出没有权限的异常,如图所示: [图片] 当 mine=true 时,docs 函数会自动添加 _openid: ‘{openid}’ 条件。 但这样在阅读代码时语义不直观,因此建议用 myDocs 代替,myDocs 的参数和功能与 docs 一致,只是 mine 参数默认为 true。 以后还会有许多类似的函数对,如 getDoc、getMyDoc 等。记住一个简单的原则即可:使用 utils 库时,当操作“自己的数据”时,请使用对应的 my 函数。 其他依赖工具函数 utils.docs 依赖了其他 utils 中的函数,部分被依赖的函数如下(具体实现请在代码库中查看): [代码]const utils = { // 判断值是否为undefined或null isNone (i) {}, // 判断值是否为空对象{}、空数组[]、空字符串''、空内容串' '、undefined、null isEmpty (i) {}, // 判断值是否为数组 isArray(i) {}, // 判断值是object但不是数组、null、undefined isObject(i) {}, // 判断值是否为字符串 isString(i) {}, // 拆分字符串,返回数组 // 函数会过滤掉空字符串,并去除两边的空白 split (s, char = ' ') {}, // 判断某个元素是否在数组或对象中 in (item, arr) {}, // 从数据_id中获取时间,返回Date对象 getTimeFromId (id) {}, // 返回时间的年月日字符串,如:'2023-07-01' yymmdd (t) {}, // 返回时间的时分秒字符串,如:'01:02:03' hhmmss (t) {}, // 返回时间的完整字符串,如:'2023-07-01 01:02:03' dateToString (t) {}, } [代码] 随着本系列教程的深入,utils.js 会提供更多的工具函数,希望能帮你提高开发效率。 Todolist 列表显示效果 好了,目前我们已经完成了 todo 的添加功能,当你在输入框中输入文字并点击“添加 todo”按钮时,会在页面中显示新的 todo。效果如图所示: [图片] Todolist 的完成与删除 更新数据函数:utils.updateDoc 与 utils.updateMyDoc utils.js 中提供了数据更新函数,用于更新一个文档,代码如下: [代码]updateDoc (c, id, {_openid, _id, ...d}, {mine = false} = {}) { const _ = this const w = {_id: id} return new Promise((resolve, reject) => { _.coll(c) .where({...w, ...(mine ? {_openid: '{openid}'} : {})}) .limit(1) .update({data: d}) .then(res => { if(res.stats.updated > 0){ resolve(true) } else { resolve(false) } }) .catch(reject) }) }, updateMyDoc (c, id, d) { return this.updateDoc(c, id, d, {mine: true}) } [代码] 同样的,如果要更新“自己的数据”,请使用 updateMyDoc。这两个函数的前三个参数分别表示: c:集合名称 id:文档的 _id d:要更新的数据 这里数据 d 可以使用“点表示法”,如: [代码]utils.updateDoc('todo', todo_id, { 'author.name': '张三' }) .then(res => { if(res === true){ console.log('更新成功') } else { console.log('更新失败') } }) [代码] 注意:只有文档存在且数据有变化时,res 才会返回 true。 在 updateDoc 中排除 _openid 和 _id 字段 系统的 update 函数不允许传入 _openid 和 _id 字段,否则会抛出异常,如图所示: [图片] 但是,在实际开发中,我们经常会先读取一个 doc 文档,然后修改这个文档后使用当前 doc 去更新,如: [代码]const doc = await utils.getDoc('todo', todo_id) doc.status = '已完成' // ... 其他修改doc的代码 utils.updateDoc('todo', todo_id, doc) // 这里的 doc 中包含了 _openid 和 _id 字段 [代码] 此时因为 doc 中包含了 _openid 和 _id 字段,所以会抛出上图所示异常。 为了解决这个问题,我们在 updateDoc 函数中使用了解构赋值 {_openid, _id, …d} 来排除 _openid 和 _id 字段。 现在,你就不需要担心向 updateDoc 和 updateMyDoc 函数传入 _openid 和 _id 了。 为什么不直接 [代码]delete doc._openid[代码] ?,原因一是你可能需要使用 _id,原因二是可能会引起其他代码的 bug。 设置 todo 已完成 为了实现点击 todo 左边的圆圈时把 todo 的状态改为“已完成”,我们先在 todo.wxml 绑定 completeTodo 事件(完整代码请看代码库): [代码]<label wx:for="{{todo_list}}"> <view bind:tap="completeTodo" data-id="{{item._id}}"> <!-- 这里是一个圆圈 --> </view> </label> [代码] 然后在 todo.js 中实现 completeTodo 函数: [代码]async completeTodo (e) { const _ = this const { id } = e.currentTarget.dataset utils.updateMyDoc('todo', id, { status: '已完成' }) .then(() => { _.updateTodoList() }) } [代码] 修改 updateTodoList 函数,把已完成的 todo 放到 data.done_list 中: [代码]async updateTodoList () { const _ = this _.setData({ todo_list: await utils.myDocs({c: 'todo', w: {status: '未完成'} }), done_list: await utils.myDocs({c: 'todo', w: {status: '已完成'} }), }) }, [代码] 点击圆圈后,todo 会从未完成移动到已完成中,效果如图所示: [图片] 提示:这里只是为了演示数据库查询功能,实际开发时不应该在 updateTodoList 中读取数据库,以免消耗“调用次数” 删除数据函数:utils.removeDoc 与 utils.removeMyDoc 删除数据的函数如下: [代码]removeDoc (c, id, {mine = false} = {}) { const _ = this const w = {_id: id} return new Promise((resolve, reject) => { _.coll(c) .where({...w, ...(mine ? {_openid: '{openid}'} : {})}) .limit(1) .remove() .then(async res => { if(res.stats.removed > 0){ resolve(true) } else { resolve(false) } }) .catch(e => { resolve(false) }) }) }, removeMyDoc (c, id) { return this.removeDoc(c, id, {mine: true}) } [代码] 同样删除“自己的数据”时要使用 removeMyDoc 函数。 删除 todo 为了简化本教程的操作,我们直接使用 longtap 事件来删除 todo,你可以在未完成和已完成的 todo 中添加如下代码: [代码]<view class="weui-cell__bd" bind:longtap="deleteTodo" data-id="{{item._id}}" > <view>{{item.title}}</view> </view> [代码] 然后在 todo.js 中实现 deleteTodo 函数: [代码]async deleteTodo (e) { const _ = this const { id } = e.currentTarget.dataset utils.removeMyDoc('todo', id).then(_.updateTodoList) } [代码] 好了,现在你可以长按 todo 来删除它了。 代码库 本系列教程搭配了一个github代码库:sdjl/WxMpCloudBooster,你可以在这里找到文章中的代码: [代码]# 获取项目 git clone https://github.com/sdjl/WxMpCloudBooster.git # 切换到文章二对应的代码库 cd WxMpCloudBooster git checkout article2 [代码] 我每发布一篇文章,就会提交一个 commit,你可以使用 git checkout article + n 来切换到第 n 篇文章对应的代码。 下篇预告 在下篇文章中,我会进一步介绍更多的数据库读写函数,以及提供云函数中操作数据库的版本,并探讨数据库在使用上有哪些限制。届时我们将结束数据库的基础教程,之后会开启其他内容的学习之旅。 我将会在以后的文章中提供更丰富的代码库,你可以把 utils/ 导入到你自己的项目,实现高效率开发,最终变成大神。 本文作者刘永辉,安顺果然赞科技有限公司,转载请注明出处。
07-26 - 「笔记」小程序备案驳回原因整理(不定期更新)
修改字段 待完善原因 修改建议 主体备案证件OR主办单位 证件 主办单位证件涉及前置或专项审批-你单位名称/经营范围/小程序名称/小程序服务内容涉及食品经营 你单位涉及食品相关内容,需要提供《食品经营许可证》、《食品生产许可证》或《仅销售预包装食品经营者备案信息采集表》请通过主体其他补充材料接口上传。 主体备案证件OR主办单位证件 主体证件图片不清晰 请上传清晰完整、不遮挡关键信息/图像、边角齐全、在有效期内的主体有效证件。 主体备案证件OR主办单位证件 主体证件边角不齐全 请上传清晰完整、不遮挡关键信息/图像、边角齐全、在有效期内的主体有效证件。 主体备案证件OR主办单位证件 主体负责人证件非原件彩色扫描件或拍照件 请上传清晰完整、不遮挡关键信息/图像、边角齐全、在有效期内的主体负责人有效证件彩色扫描件或者彩色拍照件。 主体备案证件OR主办单位证件 主办单位证件涉及前置或专项审批-你单位涉及危险化学品 经营范围涉及危化品的,需提供《危险化学品经营许可证》,如实际小程序不涉及的可提供情况说明书,并上传在小程序其他材料位置。 主体备案证件OR主办单位证件 主办单位证件涉及前置或专项审批-北京涉及金融相关关键字 你单位名称/经营范围涉及“金融”相关前置审批关键字。 注:承诺书需上传在小程序其他材料接口。 主体备案证件OR主办单位证件 主办单位证件涉及前置或专项审批-单位名称/经营范围涉及“文化”相关前置审批关键字 你单位名称/经营范围涉及“文化”相关前置审批关键字,如小程序实际经营相关内容,请提供文化和旅游厅审批的《网络文化经营许可证》,前置审批项需选择“文化”;如小程序内容不涉及,需在小程序备注中详细备注小程序从事内容,并承诺不涉及文化前置审批内容。 主体备案证件OR主办单位证件 主办单位证件涉及前置或专项审批-单位名称/经营范国涉及“药品和医疗器械”相关前置审批关键字 你单位名称/经营范围涉及“药品和医疗器械”相关前置审批关键字,如小程序实际经营相关内容,请提供食品药品监督管理局审批的《互联网药品信息服务资格证书》,前置审批项需选择“药品和医疗器械”如小程序内容不涉及,需在小程序备注中详细备注小程序从事内容,并承诺不涉及药品和医疗器械前置审批内容。 主体备案证件OR主办单位证件 主办单位证件涉及前置或专项审批-贵州涉及前置审批关键字 你单位名称、经营范围涉及前置审批关键字,如涉及请配合提供前置审批文件,如实际小程序内容不涉及需配合提供承诺书,承诺书模板下载链接:https://developers.weixin.qq.com/miniprogram/product/record_material.html 注:承诺书需上传在小程序其他材料接口。 主体备案证件OR主办单位证件 你单位名称/经营范围/小程序名称/小程序服务内容涉及食品经营 你单位涉及食品相关内容,需要提供《食品经营许可证》或《预包装食品经营许可证》,请通过主体其他补充材料接口上传。 主体备案证件OR主办单位证件 你单位涉及食品经营 管局要求如涉及食品经营需要提供食品经营许可证,请通过补充材料接口上传。 主体备案证件OR主办单位证件 备案主体涉及特殊关键字 备案主体为律师事务所的,需提供字迹清晰、页面完整的《律师事务所执业许可证》副本进行备案,上传附件应包含许可证副本首页、登记事项首页、最新年审页与变更登记名称、住所页;请通过主体其他补充材料接口传,为了保证清晰度,切勿拼图上传。 主体备案证件OR主办单位证件 备案主体涉及特殊关键字-经营范围涉及“出版”相关关键字 你单位名称/经营范围涉及“出版”相关前置审批关键字,需要提供《互联网出版物许可证》材料。 主体备案证件OR主办单位证件 湖北涉及电子商务或互联网销售 你单位名称/小程序名称/小程序服务内容/经营范围涉及电子商务或百联网销售,如实际小程序内容涉及需配合提供《增值电信业务经营许可证》如不涉及请配合提供“电子商务情况说明书”,说明下模板下载链接:https://developers.weixin.qq.com/miniprogram/product/record_material.html 注:说明书需上传在小程序其他材料接口。 主体备案证件OR主办单位证件 湖南涉及前置审批关键词-涉及出版前置审批关键字 小程序名称、服务内容不涉及前置审批,仅经营范围涉及前置审批关键字,需要备注说明小程序实际从事内容,并需要用户咨询相关前置审批部门后回复无需办理的,需按照格式注明:咨询单位(前置审批主管部门名称) ,电话***,回复无需办理前置审批。(内容主管部门、咨询电话信息,仅供参考,以实际情况为准 咨询部门:湖南省新闻出版局、省电影局 咨询电话:根据用户所在地自助查询) 主体备案证件OR主办单位证件 湖南涉及前置审批关键词-涉及教育、培训等关键字 小程序名称、服务内容不涉及前置审批,仅经营范围涉及前置审批关键字,需要备注说明小程序实际从事内容,并需要用户咨询相关前置审批部门后回复无需办理的,需按照格式注明:咨询单位(前置审批主管部门名称) ,电话***,回复无需办理前置审批。(内容主管部门、咨询电话信息,仅供参考,以实际情况为准 咨询部门:当地教育主管部门 咨询电话:根据所在地自助查询) 主体备案证件OR主办单位证件 湖南涉及前置审批关键词-涉及文化前置审批关键字 小程序名称、服务内容不涉及前置审批,仅经营范围涉及前置审批关键字,需要备注说明小程序实际从事内容,并需要用户咨询相关前警审批部门后回复无需办理的,需按照格式注明:咨询单位(前置审批主管部门名称) ,电话***,回复无需办理前置审批(内容主管部门、咨询电话信息,仅供参考,以实际情况为准。 咨询部门:湖南省文化和旅游厅咨询电话:0731-82213010) 主体备案证件OR主办单位证件 湖南涉及前置审批关键词-涉及金融前置审批关键字 小程序名称、服务内容不涉及前置审批,仅经营范围涉及前置审批关键字,需要备注说明小程序实际从事内容,并需要用户咨询相关前置审批部门后回复无需办理的,需按照格式注明:咨询****单位(前置审批主管部门名称),电话*****,回复无需办理前置审批。 (内容主管部门、咨询电话信息,仅供参考,以实际情况为准 咨询部门:当地金融局 咨询电话:根据所在地自助查询) 主体类型 主体 性质选择错误 你的主体性质选择错误.请根据你提供的证件选择正确的主体性质。 主体类型 主体性质选择错误 你的主体性质选择错误,请根据你提供的证件选择正确的主体性质。 主体补充材料 你提供的补充材料不符合要求 你提供的补充材料不符合要求,请确保你提供的补充材料清晰完整,内容与实际情况相符(包括但不限于材料内容、法人签字、盖章、写日期等信息均符合正常逻辑,且有效期不小于60天)注:如涉及到需要勾选的地方,请根据订单中实际情况勾选,不勾、错勾均不可以。 主体补充材料 补充材料上传位置错误 请将该材料上传至小程序 其他补充材料接口。 主体负责人应急联系方式 应急联系方式不符合要求-天津政企要求:一、应急联系方式需为本单位员工,二、应急联系方式需按要求备注 应急联系方式需为本单位员工,且需要在小程序备注中备注:应急联系电话手机号使用人为XXX公司员工XXX。 主体负责人有效证件类型 主体负责人信息真实性核验不通过-负责人证件号码 不能为其他主体备案过 请提供未备案过的主体负责人信息 主体负责人有效证件类型 负责人证件号码不能为其他主体备案过 请提供末备案过的主体负责人信息。 主体负责人证件 水印遮挡有效字体 或水印内容有误 请上传清晰完、不遮挡关键信息/图像边角齐全、在有效期内的主体负责人有效证件;且水印内容与小程序备案有关。 主体负责人证件 证件不能添加水印/公章 订单中的图片必须为彩色原件拍照件或彩色扫描件,请勿添加水印或公章,请修改后重新提交。 主体负责人证件 请负责人提供补充材料 请主体负责人提交在本单位缴纳至少3个月的社保证明或本行政区域内的居住证。 主办单位名称 主办单位证件涉及前置或专项审批-广东单位名称涉及金融关键字 1、请优先提供金融办等金融监管部门的批文。2、如果确认无法拿不到的金融文件的,需提供一份情况说明书,内容必须写清楚“咨询单位、部门、电话,接电人的答复和态度是什么,以及介绍公司是做什么的,并承诺不利用互联网从事金融服务,不做网贷,不做P2P”等,如果违反需承担关闭小程序、注销备案、主体进入黑名单处罚。 3、法人手写签字或签名章(尽量正楷)、盖单位公章、写日期并上传在主体其他补充材料。 主办单位名称 备案主体涉及特殊关键字-广东涉及非学科类校外培训需提供承诺书 承诺书内容需包含:1)写清楚小程序具体从事内容是什么,并承诺不涉及学科类校外培训活动等。2)单独起一段(文字不得修改):我单位/公司(按实际情况选择不能都保留)承诺末经教育部门批准不从事学科类校外培训活动,如有违背接受被注销备案、关停小程序等处理措施。3)公司落款,加盖公司公章、日期 注:承诺书需上传至小程序其他补充材料接口。 主办单位名称 备案主体涉及特殊关键字-经营范围涉及“电影、电视”关键字 经营范围涉及“电影、电视”关键字请提供《不涉及电影电视情况说明书》,并上传至小程序补充材料接口。承诺内容应包含小程序实际经营内容与用途,并承诺实际不涉及电影、电视节目、影视制作等需主管部门前置审批的相关内容;承诺书须有单位法定代表人签字,加盖公司公章,日期,方视为有效。 主办单位名称 备案主体涉及特殊关键字-经营范围涉及”金融“关键字 经营范围涉及“金融”关键字请提供《不涉及金融情况说明书》,并上传至小程序补充材料接口。承诺内容应包含: 小程序实际经营内容xxxxx与用途xxxxxx,并承诺实际不涉及互联网金融等需主管部门前置审批的相关内容:承诺书须有单位法定代表人签字 ,加盖公司公章,日期方视为有效。 主办单位名称 经营范围涉及“教育”关键字 经营范围涉及“教育”关键字请提供《不涉及教育情况说明书》,并上传至小程序补充材料接口。承诺内容应包含:小程序实际经营内容xxxxx与用途xxxxxx,并承诺实际不步及学科培训、校外培训等需主管部门前置审批的相关内容;承诺书须有单位法定代表人签字,加盖公司公章,日期,方视为有效。 主办单位名称 经营范围涉及“文化”关键字 经营范围涉及“文化”关键字如小程序涉及请配合提供前置审批文件,如不涉及请提供《不涉及文化情况说明书》,并上传至小程序补充材料接口。承诺内容应包含:小程序实际经营内容与用途,并承诺实际不涉及网络文化等需主管部门前置审批的相关内容: 承诺书须有单位法定代表人签字,加盖公司公章,日期,方视为有效。 主办单位名称 经营范围涉及”药品和医疗器械“关键字 经营范围涉及”药品和医疗器械“关键字请提供《不涉及药品和医疗器械情况说明书》,并上传至小程序补充材料接口。承诺内容应包含:小程序实际经营内容xxxxx与用途xxxxxx,并承诺实际不涉及药品、医疗器械等需主管部门前置审批的相关内容;承诺书须有单位法定代表人签字,加盖公司公章日期,方视为有效。 主办单位证件类型 主体证 件类型选择错误 请将主体证件类型修改为和主体证件一致。 主办单位通信地址 主办单位通讯地址不详细 通讯地址需精确到具体的门牌号,例如:xx省xx市xxx县xx路xx号xx号楼xx单元xx室,且不能使用特殊符号(如:2#楼2-3-301),如果已经是最详细的地址,无门牌号的,请在主体备注中说明“通信地址已为最详细”。 人脸核身 活体核验照片衣着不符合要求 请小程序负责人在纯白色背景下(如白色墙体)拍摄,注意背景无杂物、露出清晰的五官和双肩、表情自然、穿着正常应季服装等。 其他 小程序主办者冲突 小程序主办者冲突,修改建议:请核实您在核实已备案成功的信息已当前填写的备案信息否一致后在平台重新提交报备申请。 其他 身份验证未通过 请确保订单中小程序负责人的身份证必须为最新,请修改后重新提交;如确认订单中的身份证均为最新,请配合按照以下流程操作:1、下载CTID APP并使用nfc读卡方式开通网证,然后重新提交订单即可 2、重新提交后仍因此问题多次被退回的,建议咨询证件对应部门。 前置审批材料 小程序服务内容涉及前置审批-四川、广东、上海涉及视频、短剧相关前置审批 涉及视频类请提供《信息网络传播视听节目许可证》,前置审批项选择“广播电视节目”,服务类目选择“休闲娱乐-视频”,《信息网络传播视听节目许可证》上传至前置审批位置。 小程序名称 小 程序名称非纯中文 小程序名称非中文时,必须在小程序备注位置写明小程序中文名称及小程序主要服务内容,并在备注中添加“承诺遵守中华人民共和国法律法规”,填写小程序名称的中文注释。 小程序名称 小程序名称/服务内容涉及教育、校外培训等内容 如小程序内容涉及校外培训等内容必须提供对应资质:学科类培训-教育部门审批的证;文旅部门负责文化艺术类培训机构;体育部门负责体育类培训机构;科技部门负责科技类培训机构:请根据你单位涉及的培训内容,提供正确的资质文件,并上传在小程序其他补充材料接口;如小程序实际内容并不涉及,请修改小程序名称服务内容、备注等信息。 小程序名称 小程序名称不符合个人备案 要求 小程序名称涉及企业/单位/商城等非个人性质,请修改为与实际小程序业务有关的名称;如果你是企业小程序,请使用企业证件进行备案。参考指引:https://developers.weixin.qq.com/miniprogram/product/record/receord_category.html 小程序名称 小程序名称与单位名称无关 你的小程序名称与单位名称无关联或涉及其他单位,请将小程序名称修改为与本单位实际情况一致,且具有实际意义,并在小程序备注中详细描述小程序经营内容。 小程序名称 小程序名称与单位经营范围无关 按照管局要求,小程序名称需要与备案主体性质相符合,通过名称可以看出小程序的具体含义,并在小程序备注中详细描述小程序的涉及内容。 小程序名称 小程序名称与单位经营范围无关 你的小程序名称与企业经营范围无关联,请将小程序名称修改为符合企业经营范围,且具有实际意义,并在小程序备注中详细描述小程序经营内容。 小程序名称 小程序名称涉及前置审批-福建小程序名称涉及剧本杀 小程序名称或备注涉及剧本杀,请提供属地文旅部门下发的备案文件;如不涉及请修改小程序名称或备注。 小程序名称 小程序名称涉及前置审批内容 小程序名称涉及前置审批/专项审批相关关键字(新闻/金融/宗教/医疗器械/网约车/校外培训/广播电影电视节目/文化/出版等),请上传对应前置审批资质;若实际不涉及,请修改为不涉及前置审批关键字的名称。 小程序名称 小程序名称重复 同一个主体下,该App,小程序或快应用上报的名称已存在或已提交备案申请,请勿重复报备;请修改后重新提交。 小程序名称 小程序备案个数较多-备案小程序较多 请配合提供《情况说明书》,内容需包含小程序实际经营内容、承诺“遵守互联网信息服务相关法律法规和行政管理规定,按照备案项目范围提供互联网信息服务,不发布未经许可和法律法规禁止发布的信息”,并法人签字、加盖公司公章写日期,上传至小程序其他材料接口。 小程序名称 非国家级单位小程序命名不符合要求 非国家级单位,不得以中国、中华中央、人民、人大、国家等字头命名。 小程序备注 个人小程序备注不符合要求 小程序备注不符合个人性质,不能涉及企业或经营性等情况,请修改为符合个人性质的备注或删减备注。 小程序备注 四川不涉及前置审批备注要求 小程序主要从事内容为******,承诺不涉及*****等前置审批内容。 小程序备注 小程序不涉及前置审批备注要求-不涉及前置审批备注要求 请补充填写备注,格式参考:“小程序主要从事内容为*******,承诺不涉及*****等前置审批内容” (注:切勿一句话描述为公司旗下产品或公司项目等无实际意义的内容) 小程序备注 小程序名称/小程序服务内容不符合主体性质 小程序名称或小程序服务内容与你当前备案的主体性质不相符,请在备注中详细描述具体含义及小程序后期从事的内容。 小程序备注 小程序备注不符合要求 小程序备注不符合要求,请修改或删减备注。 小程序备注 小程序备注与企业性质不符合 小程序从事业务必须依照营业执照经营范围来开展,不得超范围经营(且不能照抄经营范围填写在备注中,切勿涉及烟草、危化品等不能互联网经营的业务),请详细描述小程序实际从事内容,修改后重新提交。 小程序备注 河南小程序备注 请务必清晰备注小程序实际经营内容。(注:切勿一句话描述为公司旗下产品或公司项目等无实际意义的内容) 小程序补充材料 你提供的补充材料不符合要求 你提供的补充材料不符合要求,请确保你提供的补充材料清晰完整目在有效期内。 小程序负责人姓名 小程序负责人必须为法人 根据管局要求,小程序负责人须为单位的法定代表人,请将小程序负责人信息修改为单位法定代表人的信息。 小程序负责人手机号码 小程序负责人联系方式不能为其他主体备案过 该手机号码已被其他主办单位备案使用,且与你的信息不一致,请提供小程序负责人使用的、未备案过的有效手机号码。 小程序负责人手机号码 手机号码无人接听 请在订单审核期间保持电话畅通并注意接听电话,审核员需与你电话沟通核实备案信息有关情况。 小程序负责人有效证件号码 小程序负责人证件号码不能为其他主体备案过 请提供未备案过的小程序负责人信息,建议本单位/公司具体负责小程序管理、小程序维护的相关人员。 小程序负责人有效证件号码 小程序负责人证件号码不能为其他主体备案过 请提供未备案过的小程序负责人信息,建议本单位/公司具体负责小程序管理、小程序维护的相关人员。 小程序负责人法人授权书 上海小程序负责人授权书不符合要求 请使用上海小程序负责人授权书模板,小程序负责人授权书内容填写与实际负责人信息一致,授权书内容清晰完整,并且需要法人手写签字(尽量正楷)、公章清晰完整、填写日期。授权模板下载链接:https://developers.weixin.qq.com/miniprogram/product/record_material.html 小程序负责人法人授权书 小程序负责人授权书不符合要求 请确保小程序负责人授权书内容填写与实际负责人信息致,授权书内容清晰完整,并且需要法人手写签字(尽量正楷)、公章清晰完整、填写日期。 小程序负责人法人授权书 授权书内容不符合要求-小程序负责人授权书日期不符合要求 授权书必须填写日期,且有效期不小于60天。 小程序负责人电子邮箱 小程序负责人电子邮件不能为其他主体备案过 该手机号码已被其他主办单位备案使用,且与你的信息不一致,请提供小程序负责人使用的、未备案过的电子邮件。 小程序负责人证件 小程序负责人年龄不符合要求 小程序负责人年龄不符合要求(男不大于60周岁,女不大于55周岁),请修改负责人后重新提交。 小程序负责人证件 负责人证件非最新 请确保订单中的身份证必须为最新,请修改后重新提交请上传最新的负责人有效证件。 承诺书 互联网信息服务备案承诺书不符合企业要求 按照属地管局要求,请正确填写承诺书模板信息,请法人签字,并保持正楷签字、使用备案主体公章且清晰、如实写明日期(有效期不小于60天)内容清晰完整。(注:如个体工商户和无公章,需写身份证号、按手印,并需要在主体备注“个体工商户无公章”)(承诺书模板下载链接:https://developers.weixin.qq.com/miniprogram/product/record_material.html) 服务内容类型 小程序服务内容 不符合企业性质 请修改服务内容符合企业性质,需在你单位营业范围内开展工作;参考指引:https://developers.weixin.qq.com/miniprogram/product/record/receord_category.html 服务内容类型 小程序服务内容不符合个人性质 请修改服务内容符合个人性质,或者使用企业证件进行备案;参考指引:https://developers.weixin.qq.com/miniprogram/product/record/receord_category.html 服务内容类型 小程序服务内容不符合企业性质 请修改服务内容符合企业性质需在你单位营业范围内开展工作。 服务内容类型 小程序服务内容不符合企业性质 请修改服务内容符合企业性质,需在你单位营业范围内开展工作;参考指引:https://developers.weixin.qq.com/miniprogram/product/record/receord_category.html 服务内容类型 小程序服务内容选择错误 请根据小程序实际从事内容选择正确的小程序服务内容。 法人授权书 授权书内容不符合要求-主体负责人授权书日期不符合要求 授权书必须填写日期,且有效期不小于60天。 法人授权书 授权书模板不符合要求 你提供的授权书模板不符合要求,请提供符合要求的模板,模板请参考:https://developers.weixin.qq.com/miniprogram/product/record_material.html - 小程序主办者冲突 放弃备案并重提,修改备案类型为新增小程序或无主体新增小程序。 - 其他-身份验证未通过 请确保订单中的主体负责人身份证有效期必须为最新,请修改后重新提交;如确认订单中的身份证均为最新,请配合按照以下流程操作: 1、下载CTID APP并使用nfc读卡方式开通网证,录制网证视频留存,然后重新提交订单;2、重新提交后仍因此问题多次被退回的,将视频发给前端客服,位置小程序发布流程-小程序信息/程序类目 – 查询详情 联系客服按钮。 - 单位名称或证件类型及证件号码与已备案信息不一致 你本次申请备案的主体在工信部备案系统已有备案信息,且与微信平台提交的主体证件信息(单位名称或证件类型及号码)不一致,导致工信部备案系统校验冲突。需要你返回原接入商平台核实后变更主体备案信息,确认与微信平台提交的主体证件信息(单位名称、证件类型、证件号码)保持一致后,重新在微信平台提交小程序备案申请。 - 同一主体同一时间不能有多个流程中的备案 你的主体在其他平台(或接入商)已有等待管局审核中的首次备案申请(示例:当前你的主体有网站APP或小程序正在等待管局审核中),故需要退回当前备案订单待管局审核通过获得主体备案号后,重新在微信平台提交备案申请。 - 同一主体同一时间不能有多个流程中的备案 管局系统驳回:网站主办者冲突(主办者名称或证件类型及号码),请核实后再次报备。修改意见:同一主体同一时间不能有多个流程中的备案,建议放弃当前订单,待你流程中其他订单管局审核有结果了之后,再提交当前订单。 - 备案主体冲突 订单提交失败,请核实你单位是否已取得备案号,并确认之前备案信息是否为最新,如信息不一致,请至原接入商将备案信息变更为最新信息之后,待管局审核通过,再来提交当前订单。 - 小程序主办者冲突 你的主体证件已经在其他平台备案,不能同时在微信平台提交首次备案,请核实后在平台重新提交报备申请。 - 短信核验未通过(自动驳回) 你的订单未完成工信部的短信验证,已被管局系统驳回;请重新提交订单后,在收到工信部系统下发短信验证码的24小时内,按照短信提示的流程进行短信验证。 - 系统校验备案类型错误 您单位之前备案成功过两个网站,您此次订单理应是无主体新增备案,然而您的订单是首次备案,这是系统校验错误,请重新提交订单让系统重新校验正确就好。非常不好意思,请系统又未校验正确,麻烦您再次提交。 - 身份证校验未通过 请确保订单中的身份证必须为最新,请修改后重新提交。 - 其他 根据《中华人民共和国网络安全法》中落实网络实名制要求,请提交法定代表人或者网站负责人在本单位缴纳至少3个月的的社保证明或本行政区域内居住证。 - 其他 主体负责人证件与系统信息不一致。 - 其他 主办单位名称/经营范围涉及前置审批;请核实是否从事相关互联网前置审批服务业务,如从事请出具前置审批文件,如不从事需详细备注。 - 其他 已开通网站打开为违规站,请关闭。 - 其他 请提供相应建站依据。 - 其他 调用公安身份证接口核验证件真实性未通过未通过信息为服务负责人xx:(请确定是不是最新的身份证)。 - 其他 同一个主体下,该App,小程序或快应用上报的名称已存在或已提交备案申请,请勿重复报备。 - 其他 企业名称或申报的小程序服务内容或涉及“游戏”,根据《互联网信息服务管理办法》《网络出版服务管理规定》,请如实填写服务内容并在前置审批栏上传许可文件,如不涉及,请正确选择小程序服务内容并提交小程序服务内容不涉及相关前置审批的情况说明,情况说明需加盖公章。 - 其他 单位名称、经营范围、涉及金融关键词的必须提供金融审批文件,暂时不支持写承诺书。 - 其他 主办单位通用信息地址填写的与实际不符。 - 其他 管局驳回原因:(1003)网站主办者冲突-单位名称或证件类型及证件号码与已备家信息不一致,修改建议:你本次申请备案的主体在工信部备案系统已有备案信息,且与微信平台提交的主体证件信息(单位名称或证件类型及号码)不一致,导致工信部备案系统校验冲突。需要你返回原接入商平台核实后变更主体备案信息,确认与微信平台提交的主体证件信息(单位名称、证件类型、证件号码)保持一致后,重新在微信平台提交小程序备案申请。请查看原接入商中填写的主体证件类型是不是:民办非企业单位主体证件号码,注意大小写是否一致。 备注:不同地区备案要求不完全一致,排序不分先后,以上数据来源于开放社区仅供参考,如有新增驳回原因可以留言补充。
03-05 - 微信小程序-父子组件通讯(传值)
目录 1.父组件传值给子组件 2.子组件传值给父组件 vue、微信小程序都能封装组件,那么就会涉及到一个问题,组件之间的通讯 1.父组件传值给子组件 父组件使用子组件: [代码]<Tabs tabs="{{tabsList}}"> ..... ..... [代码] 组件中: [代码]/** * 组件的属性列表 */ properties: { tabs: { // 类型 type: Array, // 默认值 value: [] } }, // 监听传入的变量,当传入的值发生变化时,触发方法 observers: { 'tabs': function (val) { // val=》就是父组件传入组件中的tabsList数据 console.log(val); } }, data: { }, /** * 子组件的方法列表 */ methods: { //子组件 发生点击事件时触发 handleItemTap (e) { // 1 获取点击的索引 const { index } = e.currentTarget.dataset; // 2 触发 父组件中的事件,传递数据给父组件 把当前点击的index数据传给父组件 this.triggerEvent("tabsItemChange", { index: index }); } } [代码] 这样,就能将tabsList这个数组传给子组件中的tabs变量,以供给子组件使用了 注意,在子组件中得到父组件传入的数据,不用再到data中声明一次,直接通过this.data.tabs就能拿到传入的数据了。 2.子组件传值给父组件 一般在子组件中做了某些操作后,就要返回数据给父组件,最常见的就是:子组件中点击后触发一个方法,返回数据给父组件 [图片] this.triggerEvent(“方法名称”, {key: val }); 这样就能将 {key: val } 这个数据通过一个方法返回给父组件 而父组件中,需要通过bind绑定子组件返回的这个方法,进而拿到传过来的数据 接下来用这个来做演示:this.triggerEvent(“tabsItemChange”, {name: ‘cj’ }); tabsItemChange ====> 就是bind要绑定的方法,至于父组件中要给这个绑定的方法关联什么名字的函数,随便起名字都行,这里我就起名叫做【getSonNameChange】 [代码] <Tabs tabs="{{tabsList}}" bindtabsItemChange="getSonNameChange"> [代码] 然后在父组件的js文件中声明这个函数,获取子组件传过来的数据,注意:是在父组件的js中定义这个函数! [代码] // 获取从子组件传回来的数据 getSonNameChange (e) { // 获取子组件传过来的数据 const { name } = e.detail }, [代码] 此时,就能获取到子组件传给父组件的数据了 注意: 子组件可以直接传一个对象给父组件,这样就不用把每个键值对都写出来了 [代码] handleItemTap (e) { const sonData = { key1: value1, key2: value2, key3: value3 } // 2 触发 父组件中的事件,传递数据给父组件 this.triggerEvent("tabsItemChange", sonData); } [代码]
2023-10-16 - 隐藏【该团队的相关小程序】解决方案
有次看小程序的详细资料的时候,发现有展示该团队的相关小程序 [图片] 想到这个设置应该是在mp后台进行设置的,但是打开MP后台一看,却没有关联任何一个小程序 [图片] 那这到底是为什么? 然后点击【该团队的相关小程序】进去,仔细看看这个页面,除了小程序,底下还有一行小字 以上小程序均由XXXXXXXX及其关联企业提供服务,开发者可在以上小程序中关联你的活动 这个时候,就破案了,是unionid搞的鬼,而且这几个小程序之间存在互相跳转的情况,所以我们可以根据用户的unionid对用户的行为进行记录和分析,进而需要在详细资料里展示出该团队的相关小程序 如果不想展示该团队的相关小程序的话,有两种方式: 1.这几个小程序之间不要有互相跳转的情况 2.在开放平台解除小程序的关联。 比如有5个小程序(A、B、C、D、E)都绑定到了同一个开放平台, 如果A、B、C这三个小程序相互之间有跳转的情况,比如看A小程序的详情,这个时候,在A小程序的详情里就会展示该小程序的相关小程序B和C 并有一行提示【开发者可以再以上小程序中关联你的活动】 但是因为和D、E小程序之间没有相互跳转的联系,所以不会展示D、E小程序 就是因为这一句开发者可以再以上小程序中关联你的活动
2021-06-10 - 自定义tabbar 【恋爱小清单开发总结】
看官方demo的小伙伴知道,自定义tabbar需要在小程序根目录底下建一个名叫custom-tab-bar的组件(我有试过,如果放在components目录里面小程序会识别不了),目前我自己实现的效果是:通过在配置可以切换tab,也可以点击tab后重定向到新页面,支持隐藏tabbar,同时也可以显示右上角文本和小红点。 官方demo里面用的是cover-view,我改成view,因为如果页面有弹窗的话我希望可以盖住tabbar 总结一下有以下注意点: 1、tabbar组件的目录命名需要是custom-tab-bar 2、app.json增加自定义tabbar配置 3、wx.navigateTo不允许跳转到tabb页面 4、进入tab页面时,需要调用tabbar.js手动切换tab 效果图: [图片] 可以扫码体验 [图片] 代码目录如下: [图片] 代码如下: app.json增加自定义tabbar配置 "tabBar": { "custom": true, "color": "#7A7E83", "selectedColor": "#3cc51f", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/love/love", "text": "首页" }, { "pagePath": "pages/tabbar/empty", "text": "礼物说" }, { "pagePath": "pages/tabbar/empty", "text": "恋人圈" }, { "pagePath": "pages/me/me", "text": "我" } ] }, 自定义tabbar组件代码如下 index.js //api.js是我自己对微信接口的一些封装 const api = require('../utils/api.js'); //获取应用实例 const app = getApp(); Component({ data: { isPhoneX: false, selected: 0, hide: false, list: [{ showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/love/love", iconPath: "/images/tabbar/home.png", selectedIconPath: "/images/tabbar/home-select.png", text: "首页" }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/gifts/giftList", iconPath: "/images/tabbar/gift.png", selectedIconPath: "/images/tabbar/gift-select.png", text: "礼物说", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/moments/moments", iconPath: "/images/tabbar/lover-circle.png", selectedIconPath: "/images/tabbar/lover-circle-select.png", text: "恋人圈", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/me/me", iconPath: "/images/tabbar/me.png", selectedIconPath: "/images/tabbar/me-select.png", text: "我" }] }, ready() { // console.error("custom-tab-bar ready"); this.setData({ isPhoneX: app.globalData.device.isPhoneX }) }, methods: { switchTab(e) { const data = e.currentTarget.dataset; console.log("tabBar参数:", data); api.vibrateShort(); if (data.hideTabBar) { api.navigateTo(data.navigatePath); } else { /*this.setData({ selected: data.index }, function () { wx.switchTab({url: data.path}); });*/ /** * 改为直接跳转页面, * 因为发现如果先设置selected的话, * 对应tab图标会先选中,然后页面再跳转, * 会出现图标变成未选中然后马上选中的过程 */ wx.switchTab({url: data.path}); } }, /** * 显示tabbar * @param e */ showTab(e){ this.setData({ hide: false }, function () { console.log("showTab执行完毕"); }); }, /** * 隐藏tabbar * @param e */ hideTab(e){ this.setData({ hide: true }, function () { console.log("hideTab执行完毕"); }); }, /** * 显示小红点 * @param index */ showRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = true; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏小红点 * @param index */ hideRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = false; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 显示tab右上角文本 * @param index * @param text */ showBadge(index, text, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: true, badgeText: text}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏tab右上角文本 * @param index */ hideBadge(index, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: false, badgeText: ""}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } } } }); index.html <view class="footer-tool-bar flex-center {{isPhoneX? 'phx_68':''}}" hidden="{{hide}}"> <view class="tab flex-full {{selected === index ? 'focus':''}}" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" data-navigate-path="{{item.navigatePath}}" data-hide-tab-bar="{{item.hideTabBar}}" data-open-ext-mini-program="{{item.openExtMiniProgram}}" data-ext-mini-program-app-id="{{item.extMiniProgramAppId}}" bindtap="switchTab"> <view class="text"> <view class="dot" wx:if="{{item.showRedDot}}"></view> <view class="badge" wx:if="{{item.showBadge}}">{{item.badgeText}}</view> <image class="icon" src="{{item.selectedIconPath}}" hidden="{{selected !== index}}"></image> <image class="icon" src="{{item.iconPath}}" hidden="{{selected === index}}"></image> </view> </view> </view> index.json { "component": true, "usingComponents": {} } index.wxss @import "/app.wxss"; .footer-tool-bar{ background-color: #fff; height: 100rpx; width: 100%; position: fixed; bottom: 0; z-index: 100; text-align: center; font-size: 24rpx; transition: transform .3s; border-radius: 30rpx 30rpx 0 0; /*padding-bottom: env(safe-area-inset-bottom);*/ box-shadow:0rpx 0rpx 18rpx 8rpx rgba(212, 210, 211, 0.35); } .footer-tool-bar .tab{ color: #242424; height: 100%; line-height: 100rpx; } .footer-tool-bar .focus{ color: #f96e49; font-weight: 500; } .footer-tool-bar .icon{ width: 44rpx; height: 44rpx; margin: 18rpx auto; } .footer-tool-bar .text{ line-height: 80rpx; height: 80rpx; position: relative; display: inline-block; padding: 0rpx 40rpx; box-sizing: border-box; margin: 10rpx auto; } .footer-tool-bar .dot{ position: absolute; top: 16rpx; right: 16rpx; height: 16rpx; width: 16rpx; border-radius: 50%; background-color: #f45551; } .footer-tool-bar .badge{ position: absolute; top: 8rpx; right: 8rpx; height: 30rpx; width: 30rpx; line-height: 30rpx; border-radius: 50%; background-color: #f45551; color: #fff; text-align: center; font-size: 20rpx; font-weight: 450; } .hide{ transform: translateY(100%); } app.wxss(这里的样式文件是我用来存放一些公共样式) /**app.wxss**/ page { background-color: #f5f5f5; height: 100%; -webkit-overflow-scrolling: touch; } .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box; } .blur { filter: blur(80rpx); opacity: 0.65; } .flex-center { display: flex; align-items: center; justify-content: center; } .flex-column { display: flex; /*垂直居中*/ align-items: center; /*水平居中*/ justify-content: center; flex-direction: column; } .flex-start-horizontal{ display: flex; justify-content: flex-start; } .flex-end-horizontal{ display: flex; justify-content: flex-end; } .flex-start-vertical{ display: flex; align-items: flex-start; } .flex-end-vertical{ display: flex; align-items: flex-end; } .flex-wrap { display: flex; flex-wrap: wrap; } .flex-full { flex: 1; } .reset-btn:after { border: none; } .reset-btn { background-color: #ffffff; border-radius: 0; margin: 0; padding: 0; overflow: auto; } .loading{ opacity: 0; transition: opacity 1s; } .load-over{ opacity: 1; } .phx_68{ padding-bottom: 68rpx; } .phx_34{ padding-bottom: 34rpx; } 另外我还对tabbar的操作做了简单的封装: tabbar.js const api = require('/api.js'); /** * 切换tab * @param me * @param index */ const switchTab = function (me, index) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { console.log("切换tab:", index); me.getTabBar().setData({ selected: index }) } }; /** * 显示 tabBar 某一项的右上角的红点 * @param me * @param index */ const showRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showRedDot(index, success, fail); } }; /** * 隐藏 tabBar 某一项的右上角的红点 * @param me * @param index */ const hideRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideRedDot(index, success, fail); } }; /** * 显示tab右上角文本 * @param me * @param index * @param text */ const showBadge = function (me, index, text, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showBadge(index, text, success, fail); } }; /** * 隐藏tab右上角文本 * @param me * @param index */ const hideBadge = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideBadge(index, success, fail); } }; /** * 显示tabbar * @param me * @param success */ const showTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showTab(success); } }; /** * 隐藏tabbar * @param me * @param success */ const hideTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideTab(success); } }; module.exports = { switchTab, showRedDot, hideRedDot, showBadge, hideBadge, showTab, hideTab }; 最后,进入到tab对应页面的时候要手动调用一下swichTab接口,然tabbar聚焦到当前tab /** * 生命周期函数--监听页面显示 */ onShow: function () { tabbar.switchTab(this, this.data.tabIndex);//tabIndex是当前tab的索引 }
2021-11-09 - TDesign 是什么
TDesign 是什么 TDesign 是腾讯各业务团队在服务业务过程中沉淀的一套企业级设计体系。 TDesign 具有统一的 价值观,一致的设计语言和视觉风格,帮助用户形成连续、统一的体验认知。在此基础上,TDesign 提供了开箱即用的 UI 组件库、设计指南 和相关 设计资产,以优雅高效的方式将设计和研发从重复劳动中解放出来,同时方便大家在 TDesign 的基础上扩展,更好的的贴近业务需求。 为什么会有 TDesign 过去,腾讯内部众多团队搭建了不同的设计体系和组件库产品,以满足各自的业务诉求,提升研发效能。这些体系各自独立维护,彼此割裂。并且,随着内部业务的规模不断扩大,这种割裂的局面愈发严重。 为了应对这一情况,腾讯内部建立了 开源协同委员会,参考开源社区的组织方式,将同类项目的不同技术团队聚合在一起,开源共建。TDesign 在这样的背景下应运而生,在腾讯内部以开源协同的方式,共建一个完善、易用的设计体系和组件库产品。 [图片] TDesign 的发展 TDesign 在创建之初就严格按照开源协作的原则运作,包括源代码在内的协作方案讨论、组件设计及 API 制定的过程也完全在公司内源上开放。也得到了公司内开发和设计同学的广泛关注,无论以什么身份参与,TDesign 都同样遵循平等、公开且严格的原则来对待,很多同学从个人项目中试用组件库开始,到提交第一个 [代码]Bug Issue[代码],再到提交第一个 [代码]Feature MR[代码],最后逐步参与到 [代码]MR Review[代码] 和方案制定工作中,成为核心贡献者。在过去的一年中,TDesign 关闭了 1k+ [代码]Issue[代码],进行了 5k+ 次 [代码]CR[代码],保持 每周迭代 发布新版本。 组件库目前支持多个业界主流的开发技术栈,桌面端 Vue2、Vue3 已发布 1.x 版本,桌面端 React 和移动端 Vue3、微信小程序已发布 [代码]Beta[代码] 版本,移动端 React、QQ小程序发布 [代码]Alpha[代码] 内测版本: 桌面端 仓库 描述 状态 tdesign-vue Vue 2.x 技术栈 [代码]1.0 LTS[代码] tdesign-vue-next Vue 3.x 技术栈 [代码]1.0 LTS[代码] tdesign-react React 16.x 技术栈 [代码]1.0 LTS[代码] tdesign-angular 基于 Angular 10 实现 [代码]待上线[代码] 移动端 仓库 描述 状态 tdesign-mobile-vue Vue 3.x 技术栈 [代码]Beta[代码] tdesign-miniprogram 微信小程序 [代码]1.0 LTS[代码] tdesign-mobile-react React 16.x 技术栈 [代码]Alpha[代码] tdesign-flutter 1.17.0 [代码]待上线[代码] 同时,TDesign 与腾讯内部在各自的领域具有丰富行业经验业务团队正协作中,提供更多具有业务属性的组件库产品,未来会有包括政务、零售等多个不同风格及组件类型的组件库产品开放出来,敬请期待! TDesign 后续发展详细规划请参阅 后续计划。 产品特性 完整 TDesign 官方提供了多种业界主流的开发技术栈支持。目前,TDesign 已经支持了 Vue 2、Vue 3、React 和移动端 Vue 3、微信小程序 的开发,其他技术栈如 Augular、Flutter 正在开发中。 为了实现开发与设计之间的高效协同,TDesign 中包含了丰富可复用的设计组件资源,如色彩体系、文字系统、动效设计、图标元素、布局结构等,覆盖支持 Axure、Sketch、Figma、Adobe XD 等各大产品设计软件,将设计和开发者从重复劳动中释放出来。 除了常规设计资源,TDesign 还提供了辅助设计工具如 Sketch 设计插件,也支持在 即时设计、Pixso、墨刀 等市面常用设计工具中使用 TDesign 设计物料。 [图片] 一致 TDesign 将腾讯内部多年设计经验提炼总结为专业的设计指南,其所提供的通用设计解决方案,能够帮助产品经理、设计师、开发者等角色高效完成企业级产品的设计和研发,并保持设计语言和风格的一致,满足用户体验的要求。 基于 TDesign 的设计体系规范,TDesign 同时上线了组件库的桌面端和移动端,提供了多个技术栈实现版本。通过一系列协作流程和辅助工具,保证各技术栈 组件 API 和实现产物一致。借助这些能力,使得项目即便使用了多种不同的技术架构或技术栈,开发者也可通过 TDesign 通用设计组件库进行开发,显著降低学习成本,在构建统一/多端覆盖/跨技术栈的前端应用时更具优势。 [图片] 易用 TDesign 设计体系在形成过程中,提炼了不同业务、场景的设计经验,提供了通用的 设计指南 以降低使用门槛。对于不同企业产品的品牌定制需求,TDesign 支持使用者对设计风格进行扩展,目前已经将设计样式梳理归纳为 Design Token,形成一套企业内部的语义化设计规范,方便后续进行统一的管理和使用扩展。 [图片] 在主题配置方面,TDesign 提供了明亮和 暗色 两种模式,支持一键切换,提升用户的使用体验。后续,TDesign 还会推出针对于不同垂直领域的行业组件,覆盖更多的业务范围。产品团队可以借助内置的行业主题,快速配置对应需求,启动业务开发。 TDesign 同步上线了一款开箱即用的中后台框架 TDesign Starter Kit,开发者可以通过它快速体验组件能功能,也可以将它修改为项目基础脚手架工程,快速实现从 0 到 1 的产品开发上线。 欢迎加入 TDesign 通过对外开源,TDesign 希望将服务范围扩大至外部团队,同时开源也是一个新的起点,借助社区的力量,TDesign 期望获得与同道交流学习的机会,逐步建立起活跃的社区,以便持续打磨完善组件库和相关生态产品。 如果你希望参与 TDesign 的开源共建,请先阅读 《如何贡献》,期待你的参与! ❤️ 感谢 TDesign 所有的贡献者,他们是超过 270 位伙伴们。
2023-03-22 - 小程序自定义tabbar踩坑记
小程序为什么要用自定义tabbar? 我们是为了实现小程序中多个tabbar的效果的。用户进首页的时候,是一个tabbar,在进入到另外的页面的时候,底部的tabbar显示的是另外的一个。这样可以更好的让用户浏览到不同的内容。有点类似于一个主小程序中嵌套了一个子小程序。 现有的一些方案 我们在做这个之前,是有看过其它小程序做的一些效果的。比如小米lite和携程的小程序。 他们的实现的方式是一样的,就是做一个tabbar的自定义组件,然后跳转每个页面的时候用wx.navigateTo方法去跳转。这样是能实现多个tabbar的,当然也是有一些问题的,因为 navigateTo和switchTab的页面加载效果是不一样的。navigateTo是有一个页面的过渡效果的,有一个新页面整体从右侧滑出的动画。但是switchTab是直接出页面的。因为在tabbar上的页面,往往就是需要经常打开的页面,如果有一个跳转页面的滑出动画是会影响用户体验的。所以我们把这个方案做为了一个备选方案。 我们采用的方案 其实微信官方是有一个自定义tabbar的,我们的方案是基于官方的自定义tabbar完成的。 自定义tabbar的地址:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html (在看下面的文章之前,可以先看一下这个例子) 刚开始看,感觉实施起来并不难,而且官方还提供了一个简单的代码片段,但是在这个过程中,遇到了一些问题,以下是记录遇到的问题和解决方法。 我们新建一个js文件用来管理多个tabbar的状态。在router层做了判断,当进入需要显示另一个tabbar的时候,改变这个全局的状态,并且setdata让新的tabbar显示在视图层上。就是要中心化的管理tabbar的状态。 遇到的问题与解决方法 必须在根目录下的指定文件夹 这个自定义tabbar组件必须放在根目录的custom-tab-bar 文件夹下。这样才能被识别。 tabbar上下跳动 当我第一次把官方的例子稍加修改,移植到我们的小程序上的时候,发现tabbar会在真机上,在点击tabbar上图标的时候,上下跳动。当时也差点因为这个原因放弃这个方案。然后在之后的摸索中发现,官方的例子用的是<cover-view>和<cover-image>,改成<view>和<image>标签以后,就没有这种跳动了。当然换为view和image的后,层级会变低,所以要记得给tabbar设置高的层级,否则会被别的内容挡到。 iphonex以上手机底部tab适配 iphonex以上手机因为底部有一个小横条,所以iPhonex以上手机的tabbar的高度是比较高和其它的手机是不一样的。微信小程序原生的tabbar是有这个高度适配的,但是如果换成自定义的tabbar就需要自己适配了。需要自己做一下高度的适配。而且要注意一下开发者工具和真实机型的差距。开发者工具显示iphonex上的tabbar的样子并不是真实机型中看到的,需要注意一下。 tabbar上item数量或小红点 以前在用原生tabbar的时候,有微信的api可以全局的改变某个item上面的数字和小红点。这样在做像购物车上小红点数量改变的时候就可以用这个api。但是在使用了自定义tabbar的时候,就需要自己更新item上的数量或小红点了。而且是全局更新,因为可能在没有tabbar的页面也需要更新。为了实现全局更新,开始没有想到很好的办法,最开始是把tabbar这个组件的setdata方法存到app.js中,然后在需要更新的时候调用setdata。但是这个方法不太好,之后在跟组长讨论后,提示可以用eventhub了。在tabbar的js中来监听数量的改变,在需要改变数量的地方,触发这个event,这样就实现了全局的数量更新。 switchtab加参数 因为我们的页面在tabbar上面,只能用switchtab的方法去跳转到这个页面,但是小程序的switchtab方法是无法带参数的(我其实不太理解为什么不能带参数,但是从二维码或者分享进入tabbar上的页面,又是可以带参数的),我们采用的方法是我们有一个router工具,当检测到要跳到需要带参数的页面的时候,就把解析到的参数加到storage中,然后在页面的onshow的方法中获取。 小程序的基础库问题 自定义tabbar支持的小程序基础库版本是2.5.0,所以我们需要注意一下老基础库版本的兼容性问题。在低基础库的小程序上,自定义tabbar是可以在小程序的后台设置最低的基础库要求。 tabbar闪动 因为在切换tab的时候,必须setdata,所以肯定有闪动的,这个目前还没有找到很好的办法。 最后 小程序自定义tabbar这个特性,感觉使用的小程序也不是很多。我们先进行了一些踩坑,也希望可以帮到使用这个特性的小程序。
2020-12-25 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 微信小程序中安全区域计算和适配
前言 自从iphoneX问世之后,因为iphoneX、iphoneXR和后续全面屏手机设备,因为物理Home键被底部小黑条代替了,这时候很多前端小伙伴在开发的过程都会遇到 “全面屏”和“非全面屏”的兼容性问题,普遍问题就是底部按钮或者选项卡与底部黑线重叠 解释 根据官方解释: 安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)的影响。 具体区域如图展示 [图片] 适配方案 当前有效的解决方式有几种 使用已知底部小黑条高度34px/68rpx来适配 使用苹果官方推出的css函数env()、constant()适配 使用微信官方API,getSystemInfo()中的safeArea对象进行适配 使用已知底部小黑条高度34px/68rpx来适配 这种方式是根据实践得出,通过物理方式测出iPhone底部的小黑条(Home Indicator)高度是34px,实际在开发者工具选中真机获取到高度也是34px,所以直接根据该值,设置margin-bottom、padding-bottom、height也能实现。同时这样做要有一个前提,需要判断当前机型是需要适配安全区域的机型。 但是这种方案相对来说是不推荐使用的。比较是一个比较古老原始的方案 使用苹果官方推出的css函数env()、constant()适配 这种方案是苹果官方推荐使用env(),constant()来适配,开发者不需要管数值具体是多少。 env和constant是IOS11新增特性,有4个预定义变量: safe-area-inset-left:安全区域距离左边边界的距离 safe-area-inset-right:安全区域距离右边边界的距离 safe-area-inset-top:安全区域距离顶部边界的距离 safe-area-inset-bottom :安全距离底部边界的距离 具体用法如下: Tips: constant和env不能调换位置 [代码] padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/ padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/ [代码] 其实利用这个能解决大部分的适配场景了,但是有时候开发需要自定义头部信息,这时候就没办法使用css来解决了 使用微信官方API,getSystemInfo()中的safeArea对象进行适配 通过 wx.getSystemInfo获取到各种安全区域信息,解析出具体的设备类型,通过设备类型做宽高自适应,话不多说,直接上代码 代码实现 [代码] const res = wx.getSystemInfoSync() const result = { ...res, bottomSafeHeight: 0, isIphoneX: false, isMi: false, isIphone: false, isIpad: false, isIOS: false, isHeightPhone: false, } const modelmes = result.model const system = result.system // 判断设备型号 if (modelmes.search('iPhone X') != -1 || modelmes.search('iPhone 11') != -1) { result.isIphoneX = true; } if (modelmes.search('MI') != -1) { result.isMi = true; } if (modelmes.search('iPhone') != -1) { result.isIphone = true; } if (modelmes.search('iPad') > -1) { result.isIpad = true; } let screenWidth = result.screenWidth let screenHeight = result.screenHeight // 宽高比自适应 screenWidth = Math.min(screenWidth, screenHeight) screenHeight = Math.max(screenWidth, screenHeight) const ipadDiff = Math.abs(screenHeight / screenWidth - 1.33333) if (ipadDiff < 0.01) { result.isIpad = true } if (result.isIphone || system.indexOf('iOS') > -1) { result.isIOS = true } const myCanvasWidth = (640 / 375) * result.screenWidth const myCanvasHeight = (1000 / 667) * result.screenHeight const scale = myCanvasWidth / myCanvasHeight if (scale < 0.64) { result.isHeightPhone = true } result.navHeight = result.statusBarHeight + 46 result.pageWidth = result.windowWidth result.pageHeight = result.windowHeight - result.navHeight if (!result.isIOS) { result.bottomSafeHeight = 0 } const capsuleInfo = wx.getMenuButtonBoundingClientRect() // 胶囊热区 = 胶囊和状态栏之间的留白 * 2 (保持胶囊和状态栏上下留白一致) * 2(设计上为了更好看) + 胶囊高度 const navbarHeight = (capsuleInfo.top - result.statusBarHeight) * 4 + capsuleInfo.height // 写入胶囊数据 result.capsuleInfo = capsuleInfo; // 安全区域 const safeArea = result.safeArea // 可视区域高度 - 适配横竖屏场景 const screenHeight = Math.max(result.screenHeight, result.screenWidth) const height = Math.max(safeArea.height, safeArea.width) // 状态栏高度 const statusBarHeight = result.statusBarHeight // 获取底部安全区域高度(全面屏手机) if (safeArea && height && screenHeight) { result.bottomSafeHeight = screenHeight - height - statusBarHeight if (result.bottomSafeHeight < 0) { result.bottomSafeHeight = 0 } } // 设置header高度 result.headerHeight = statusBarHeight + navbarHeight // 导航栏高度 result.navbarHeight = navbarHeight [代码]
2022-11-04 - 那些微信小程序开发踩过的坑
小程序页面栈最多十层 问题:小程序内超过十层路由,你会发现wx.navigateTo跳转不到下一个页面。这是因为使用wx.navigateTo跳转会把当前页面保存到页面栈中,而小程序页面栈最多十层。 解决:超过十层使用redirectTo(重定向)操作 或者参考https://developers.weixin.qq.com/community/develop/article/doc/000a08e12185187bd5cebf3f651013 IOS使用New Date()报错IOS 的 Date 构造函数 不支持2018-04-26这种格式的日期,必须转换为2018/04/26这种格式,可以使用 replace(/-/g, '/')处理image组件使用webp图片时,IOS需要设置webp属性.Android手机在onShow内调用 wx.showModal ,如果不关闭弹窗(直接点击右上角退出小程序),弹窗不会销毁,再次进入页面触发onShow时会出现两次弹窗,IOS正常小程序中使用 web-view打开pdf , IOS 可以正常打开,Android 打开为空白 解决:使用wx.downloadFile和wx.openDocument 在手机相册中选择完图片后直接跳转会出现闪回的现象 原因:在选择完图片后,会重新执行一遍page的onShow生命周期 解决:在选择完图片后,做一个sleep延时1秒,再进行跳转 textarea 层级问题 问题:textarea的placeholder会显示在弹窗的层级之上 解决:使用wx:if 判断当没有值的时候用view代替textarea 最好封装为组件 或者 弹出层使用cover-view组件,而不是view,覆盖住所有原生组件。 小程序的 web-view 中页面跳转后,点击 Android 手机上的物理返回按钮会返回前一个页面。而点击左上角的返回按钮,会直接关闭整个 web-view。有关 web-view 中有背景音乐,后台后无法关闭的问题 https://developers.weixin.qq.com/community/develop/doc/c75139c842a40c67cade23d3f66e7992 var hiddenProperty = 'hidden' in document ? 'hidden' : 'webkitHidden' in document ? 'webkitHidden' : 'mozHidden' in document ? 'mozHidden' : null; if (hiddenProperty) { var visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange'); var onVisibilityChange = function() { if (document[hiddenProperty]) { !MpMovie.video.paused && MpMovie.video.pause(); } }; document.addEventListener(visibilityChangeEvent, onVisibilityChange);
2022-11-07 - 业务数据怎么查,我用云开发高级日志服务
业务错误怎么查,我用云开发高级日志服务 小程序·云开发作为小程序原生的后台开发能力,一直致力于可以更高效地帮助开发者构建性能更好的小程序。而云函数作为云开发的一项基础能力,承载着小程序所有的后台运算逻辑,它就像小程序的大脑一样,每天繁忙地工作着。 而对于开发者而言,如何尽快地定位和排查云函数使用过程中的问题,也成为保障小程序质量的必备功能。而有一部分开发者并不担心这个问题,因为他们选择了一种能力,只要通过简单的开启就可以高效且准确地定位到云函数中的问题。 这就是云开发提供的高级日志服务。那什么是高级日志服务,它又能做什么呢?接下来就让我们一探究竟。 什么是高级日志服务 很多开发者可能都会遇到这样一些问题: [代码]线上的小程序运行地好好的突然出问题了,怎么知道是哪里有异常呢? 根据线上表现大概猜到是哪个功能模块出现了异常,但是不知道上下文调用信息,要如何准确定位问题? [代码] 如果你恰巧使用的是云开发,那不必担心,因为云函数原生就带有日志服务。但是基于之前提供的旧的日志服务,开发者可能还是会遇到一些问题: [代码]我不知道具体的请求 ID 是什么,但是隐约记得一些关键字,这要怎么查询日志信息啊? 我想自定义一些信息打印到日志中,该怎么办? [代码] 小程序·云开发的高级日志服务就是为解决以上所有开发者遇到的问题应运而生的产品能力。基于高级日志服务提供的日志采集和日志检索功能,开发者可以更加高效地发现和解决云函数运行过程中的问题。 高级日志服务能做什么 让我们通过前端开发小 H 的故事来看看,高级日志服务到底能够做什么。 1. 旧框架下的日志服务 之前小 H 经常会为了做小程序而陷入苦恼当中,比如他需要将用户触发订阅消息时的一些数据存储到数据库中,用于后续的消息下发,这就需要有一个完整的服务端才行。但是作为一个前端开发,小 H 对于后台服务的搭建和部署并不熟悉,因此常常陷入困境。 后来他发现了云开发这样一个神器,一键开通就可以具备后台服务开发的能力了。那么现在当他想要存储相关的数据就变得非常简单了。 首先,他定义一个云函数 [代码]subscribe[代码],并在通过该云函数将用户订阅的消息信息存储到小程序·云开发的数据库中。 [代码]exports.main = async (event, context) => { try { const { OPENID } = cloud.getWXContext(); // 在数据库中记录用户的订阅信息 const result = await db.collection('messages').add({ data: { touser: OPENID, // 用户的openid page: 'index', // 订阅消息的页面路径 templateId: event.templateId, // 订阅消息模板ID }, }); return result; } catch (err) { console.log(err); return err; } } [代码] 然后在小程序端调起订阅消息界面的时候触发这个云函数并将对应的信息存储到数据库中。同时小 H 还可以通过云开发提供的原生的日志功能查看每次的调用是否成功,以及具体的调用信息。 [图片] 但是原生的日志中能够写入的数据是非常有限的。而且检索日志的时候只能通过开始时间、结束时间、状态和 [代码]requestID[代码] 进行检索。可是基于业务需求小 H 需要在 [代码]subscribe[代码] 云函数调用的时候需要再打入一些自定义的信息,而且他希望可以对日志按照 [代码]log[代码] / [代码]info[代码] / [代码]warn[代码] / [代码]error[代码] 进行分级,这样在日志查询的时候也可以快速定位到自己想要关注的日志,这该怎么办呢? 当然,小 H 并不只是一个人,正是看到很多开发者有类似的问题,今年我们推出了云开发高级日志的服务。接下来,让我们看看,小 H 是如何使用高级日志服务的。 2. 高级日志服务 首先,小 H 在定义 [代码]subscribe[代码] 函数的时候可以使用 [代码]wx-server-sdk[代码](1.5.0 或以上版本)提供的方法打入一些自定义的日志内容。具体流程为: 通过 [代码]logger()[代码] 方法取得 [代码]log[代码] 对象 调用 [代码]log[代码] 对象上的 [代码]log[代码] / [代码]info[代码] / [代码]warn[代码] / [代码]error[代码] (对应不同 level 的日志等级)方法,传入一个对象作为参数 对象的每一个 [代码]<key, value>[代码] 对都会成为日志一条记录中的一个可检索的键值对,其中 [代码]value[代码] 不论值是什么都会被转成字符串 按照上述改造后, [代码]subscribe[代码] 变成了下面这样: [代码]exports.main = async (event, context) => { const log = cloud.logger(); try { const { OPENID } = cloud.getWXContext(); // 在数据库中记录用户的订阅信息 const result = await db.collection('messages').add({ data: { touser: OPENID, // 用户的openid page: 'index', // 订阅消息的页面路径 templateId: event.templateId, // 订阅消息模板ID }, }); log.info({ action: 'addMessage', touser: OPENID, templateId: event.templateId, }); return result; } catch (err) { log.error({ type: err.name, message: err.message, }); return err; } } [代码] 此时,当这个 [代码]subscribe[代码] 被触发以后,我们就能在高级日志服务中看到这样一条日志记录: [代码]{ "level": "info", "function": "<function_name>", // 执行的云函数名 "requestId": "<request_id>", // Request ID "action": "addMessage", "touser": "<openid>", "templateId": "<template_id>", "src": "app" // logger 打的日志为 app,系统打的日志为 system } [代码] 有了日志以后,日志检索也会变得非常的简单。高级日志不仅提供了全文检索能力,还提供了通过键值检索约束查询范围,让日志的检索变得更加的简单和快捷。 比如,小 H 想知道 [代码]subscribe[代码] 函数的日志,就可以通过: 全文检索:在搜索框中输入 [代码]subscribe[代码] 键值检索:在搜索框中输入 [代码]function:subscribe[代码] 比如,小 H 想知道 [代码]subscribe[代码] 函数且 OPENID 为 [代码]popo[代码] 的日志,就可以通过: 在搜索框中输入 [代码]function:subscribe and touser:popo[代码] 又如,小 H 想知道 level 为 [代码]error[代码] 且错误信息中含有单词 [代码]defined[代码] 或以 [代码]mem[代码] 打头的单词的日志,就可以通过: 在搜索框中输入 [代码]function:subscribe and level:error and (message:defined or message:mem*)[代码] 当然,高级日志服务还提供丰富的查询语法,大家可通过《小程序·云开发高级日志服务》了解详细内容。 [图片] 除了新增的高级日志外,近期小程序·云开发还更新了—— 小程序·云开发能力更新 除了新增的高级日志外,近期小程序·云开发还更新了: 为帮助企业、政府、媒体及其他组织的小程序开发者在新冠肺炎疫情期间共度难关,小程序·云开发推出特殊类型代金券帮助大家以更低地资源成本完成小程序的功能迭代。详情可参考文档《小程序·云开发特殊代金券》 数据库安全规则:提供精细化的控制集合中所有记录的读、写权限的能力,自动拒绝不符合安全规则的前端数据库请求,保障数据安全 自定义告警:提供更加灵活的告警配置,可以使用告警指标、统计周期、比较条件、持续周期、告警频率等参数自由组合告警条件。如:统计周期 [5 分钟],当 [云函数错误次数] [>] [5 次] 且持续 [1] 个周期时告警,[每小时] 告警一次 数据库事务:可以方便开发者更加灵活地使用数据库能力,满足跨多个记录或跨多集合的原子操作的使用诉求,极大地方便了小程序的功能开发
2020-02-12 - 图片安全检测data exceed max size解决方案
最近在重构小程序恋爱小清单,在用云函数做图片的安全检测时报了一个错:cloud.callFunction:fail Error: data exceed max size 也就是图片超过了大小限制。 早期的版本是通过画布将图片缩小(wx.canvasToTempFilePath),接着读取文件流(wx.getFileSystemManager().readFile),然后再提交云函数检测,过程感觉有些繁琐复杂 最近发现其实有更简单的方法,可以借助临时的CDN,传递大数据,最终在云函数端会收到一个CDN地址,接着通过request-promise读取文件流,然后再做安全检测,相比旧版的方法个人感觉简单清爽不少。 参考官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/utils/Cloud.CDN.html 代码如下: 小程序端: const api = require("api.js"); /** * 图片安全检测 * 借助临时CDN传递大数据 * @param filePath 图片的临时文件路径 (本地路径) * @returns {Promise<unknown>} */ const imgSecCheckViaCDN = (filePath) => { return new Promise(function (resolve, reject) { api.callCloudFunction("securityCheck", { type: "imgSecCheckViaCDN", imgData: wx.cloud.CDN({ type: "filePath", filePath, }) }, res => { console.log("图片安全检测结果:", JSON.stringify(res)); const result = res.result; if (result.success) { resolve(result); } else { reject(result); } }, reject); }); } api.js /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 wx.cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; module.exports = {callCloudFunction} 云函数端: // 云函数入口文件 const cloud = require('wx-server-sdk'); const responce = require('easy-responce'); const requestHelper = require('./utils/requestHelper'); const headers = { encoding: null, headers: { "content-type": "application/octet-stream", // "content-type": "video/mpeg4", }, }; // 云函数入口函数 exports.main = async (event, context) => { cloud.init({ env: event.env }); let result = {}; try { const {type, content, imgData} = event; let {buffer} = event; console.log("检测类型:", type, "文本内容:", content, "图片内容:", imgData); switch (type) { case "imgSecCheckViaCDN": const imageResponse = await requestHelper.request(imgData, headers, {}); buffer = imageResponse.body; case "imgSecCheck": result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', // value: Buffer.from(imgBase64, "base64") value: Buffer.from(buffer) } }); break; case "msgSecCheck": result = await cloud.openapi.security.msgSecCheck({content}); break; default: console.log("不支持的检测类型:", type); break; } } catch (e) { console.error(e); result = e; } console.log("检测结果:", result); const {errCode, errMsg} = result; return errCode !== 87014 ? responce.success({errCode}) : responce.fail(errMsg); }; requestHelper.js const rp = require('request-promise'); /** * http请求 * @param url * @param options * @param data * @param autoFollowRedirect * @returns {Promise<unknown>} */ const request = function (url, options, data, autoFollowRedirect = true) { return new Promise(function (resolve, reject) { const p = Object.assign({ json: true, resolveWithFullResponse: true, followRedirect: autoFollowRedirect }, options, data, {url}); console.log("请求参数:", JSON.stringify(p)); return rp(p) .then(async function (repos) { //console.log("获取到最终内容,执行回调函数:", repos); return resolve(repos); }) .catch(async function (err) { if (err && (err.statusCode === 301 || err.statusCode === 302)) { // console.log("停止重定向,重定向信息:", err); console.log("停止重定向"); return resolve(err); } console.error("重定向失败:", err); return reject(err); }); }); } module.exports = {request }
2022-10-21 - 微信小程序通过NFC是否能读取身份证信息?
暂不支持此功能,可参考文档: https://developers.weixin.qq.com/miniprogram/dev/framework/device/nfc.html 暂仅支持 HCE(基于主机的卡模拟)模式,即将安卓手机模拟成实体智能卡。 适用机型:支持 NFC 功能,且系统版本为 Android 5.0 及以上的手机 适用卡范围:符合ISO 14443-4 标准的 CPU 卡
2019-10-09 - 如何使用微信小程序·云开发的Node.js云函数生成Word文档(2021-10-15更新)
编者按 近期一个云开发项目有生成Word文档的需求,经过搜索,发现并没有小程序·云开发有关生成word文档的案例,因为本人还是本科生且非科班出身,一路摸着石头过河,遇到了不少困难,期间还试图向社区的大佬们求助;花了两天时间才搞定这一百行代码,现在分享给大家。 代码有些糙,希望大佬们不要嫌弃。 一、安装云函数依赖officegen、fs 工欲善其事必先利其器,我们知道云函数代码运行在云端Node.js环境中,因此,理论上来说,Node.js能做的事情,小程序·云开发的云函数基本上也能做到。officegen是Github上一款生成微软Office文档的工具,包括.docx、.xlsx、.pptx三种文件,由于我只用了.docx,本文将以Word文件为例。 https://github.com/Ziv-Barber/officegen [图片] 1. 首先我们在微信开发者工具中 新建一个云函数 => 右键云函数名 => 在终端中打开 [图片] 2. npm安装依赖officegen和fs,为了方便本地调试云函数,我们这里也安装wx-server-sdk。 [图片] 代码如下,请逐个安装,如果安装有问题,可以自行搜索“npm”或“npm taobao 镜像” ;这里不再赘述。 npm i officegen npm i fs npm i wx-server-sdk 3. 在云函数index.js开头写下以下代码,引用我们刚刚安装的包。 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); 二、创建Word文档的内容 文档地址: https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md 1. 首先我们根据文档定义(Ctrl CV)两个函数 //文档生成完成后调用,后来其实发现没啥用 // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) //生成文档出现问题时调用 // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) 2. 创建段落API: docx.createP(options) //声明一个创建段落的变量p0bj let pObj = docx.createP(options) //创建一个段落并插入文本 pObj = docx.createP({ align: 'center' //文字对齐方式,center、justify、right;默认为left indentLeft = 1440; // 段落缩进 Indent left 1 inch indentFirstLine = 440; // 首行缩进 }) pObj.addText('你要插入的文字,这里可以时变量', { bold: true, //是否加粗,默认false font_face: 'KaiTi', //字体,这里以“楷体为例”,如果填写了打开文档的电脑没有安装的字体名称,将使用默认字体。能不能用中文,我没试过。 font_size: 19, //字号 color: '595959' //文字颜色 }); 上述例子外,还可以添加下划线、设置斜体、超链接、分页等;还可以编辑页眉和页脚、插入图片等。详见后续代码示例或officegen文档。 3. 插入图片 这里以插入小程序码为例,直接上代码。 要注意的是officegen似乎不支持以buffer形式插入图片,因此要先将图片保存。 //首先定义一个用于保存小程序码图片的函数 //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } //要获取小程序码,首先要修改云函数config.json文件中的云调用权限 { "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } //在云函数main中获取小程序码 //https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', //小程序页面地址,必须是线上版本中存在的页面的完整地址 scene: '', //小程序码参数 width: 240, //小程序码的宽度(是个正方形) }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型,关于路径会在第三部分生成Word文件中解释。 //将图片插入到文档中 pObj = docx.createP() //创建段落 pObj.options.indentFirstLine = 440; //首行缩进 pObj.addImage('/tmp/qr.jpg', { //图片文件路径 cx: 140, //长度 cy: 140 //宽度 }); 三、生成Word文件 文档内容完成后,就可以生成文档了。officegen似乎只能生成文件,没有文件buffer的接口,而要上传到小程序·云开发的云存储中,只能使用Buffer或fs.ReadStream,怎么办呢?先把文件保存下来再读取呗。 首先提一下云函数运行环境 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/mechanism.html 云函数运行在云端 Linux 环境中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间。云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 [代码]/tmp[代码] 目录下提供了一块 [代码]512MB[代码] 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果需要持久化的存储,请使用云存储功能。因此,我们将文件保存在/tmp路径下,文件名随便起,这里我取为exampl.docx。生成文档的代码如下: // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) 理论上来说,我们文档生成完毕后,通过fs.ReadFileStream读取文件调用cloud.uploadFile()即可上传到云存储 const fileStream = fs.createReadStream('/tmp/example.docx') return await cloud.uploadFile({ cloudPath: '/tmp/example.docx', fileContent: fileStream, }) 而在测试过程中我发现,云端测试时,云函数调用超时。而后使用本地调试查看问题出在何处。 云函数本地调试的方法不再赘述,看这里即可。https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/local-debug.html 通过本地调试,发现cloud.uplodaFile()的网络请求始终时挂起(pending)状态,没有传输数据。 [图片] 经过一天的调试,通过监听文件,发现officegen生成文件完成,执行了我们开头复制粘贴的生成文档后执行的docx.on("finalize",)函数,打印文档生成成功的日志后,仍有文件变动,也就是说,文件并没有生成完毕。这就导致了后续步骤的失败。 当时调试的界面我没有保存,就贴一下fs监听文件的代码吧。 let watcherObj = '/tmp/example.docx' //eventType 可以是 'rename' 或 'change'; 当改名或出现或消失的时候触发rename; recursive:是否监听到内层子目录,默认false; try { let myWatcher = fs.watch(watcherObj,{encoding:'utf8',recursive:true},(event,filename) => { if(event == 'change'){ console.log("触发change事件") } console.log(event) //encoding:文件名编码格式,buffer、默认:utf8等;filename有可能为空 if(filename){ console.log('filename: ' + filename) } }) //change 事件会触发多次 myWatcher.on('change',function(err,filename){ console.log(filename + '发生变化'); }); //50秒后 关闭监视 setTimeout(function(){ myWatcher.close() },5000); } catch (error) { console.log('文件不存在!!') } 为解决这一问题,我最先想到了await,结果发现await对officegen生成文档的接口并不起作用;最终我用了最原始的笨办法:用setTimeout等一会儿再读取文件,大佬们有更好的解决方案还请赐教。 return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); //等文件再读1秒 }, 6300); //等文件再写一会儿。根据自己的需求调试后确定等待时长,要预留出一定时间确保文档完全生成完毕。 }) //最终返回内容为文件云存储中的CloudID。 四、完整核心代码 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); cloud.init({ env: '这里填入你的云环境' }) // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } // 云函数入口函数 exports.main = async (event, context) => { var time = new Date() var filePath = 'exportVoluntaryData' var fileName = "zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item._id, width: 240, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // Add a Footer: var footer = docx.getFooter().createP(); footer.addText('XXXX证明_' + item._id, { font_size: 10 }); footer = docx.getFooter().createP(); footer.addText(time.toString(), { font_size: 10 }); //下方开始文档每一页的循环 for (var i in item.volunteerInfo) { //标题 let pObj = docx.createP({ align: 'center' }) pObj.addText('XXX证明', { bold: true,XXX font_face: 'KaiTi', font_size: 19, color: '595959' }); //此处省略了一些正文内容 pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('落款', { font_face: 'FangSong', font_size: 15, color: '595959' }); if (i != ((item.volunteerInfo).length - 1)){ docx.putPageBreak() //分页 } } // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); }, 6300); }) } 本人非计算机相关专业本科生,且本文大部分内容为手打,难免会有差错和疏漏,还请各位指教。 希望本文对你有所帮助。 Soochow University. HaoChen. 2020年2月 ======= 2021-10-15更新 ======= 经过一段时间的使用,上述内容主要存在两点问题:(1)难以判断文件何时生成完毕;(2)连续调用生成文档时,若上一个云函数实例未被销毁,会出现文件内容重复和错乱的问题。 前一段时间进行了更新,因为工作学习忙碌,此次暂不做详解,代码如下。 入口文件index.js// 云函数入口文件 delete require.cache[require.resolve('officegen')]; const cloud = require('wx-server-sdk') var office = require('office.js'); //https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md cloud.init({ env: 'sudaxmt1900' }) const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { return await office.genWord(event); } office.jsconst cloud = require('wx-server-sdk') const fs = require('fs'); function delDir(path) { console.log("delete Dir") let files = []; if (fs.existsSync(path)) { files = fs.readdirSync(path); files.forEach((file, index) => { let curPath = path + "/" + file; if (fs.statSync(curPath).isDirectory()) { delDir(curPath); //递归删除文件夹 } else { fs.unlinkSync(curPath); //删除文件 } }); // fs.rmdirSync(path); // 删除文件夹自身 } } readDocx_fs = function (path) { return new Promise((resolve, reject) => { fs.readFile(path,(err,data)=>{ resolve(data); reject(err); }) }) } //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } exports.genWord = async (event) => { let officegen = require('officegen'); let fs = require('fs'); let docx = officegen('docx'); //ini delDir('/tmp') var item = event.item var filePath = 'exportVoluntaryData' var fileName = "21zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //=========以下建构文档内容========== //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item.id, width: 140, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型 timeBottom = time.getFullYear() + '年' + (time.getMonth() + 1) + '月' + time.getDate() + '日' for (var i in item.volunteerInfo) { let pObj = docx.createP({ align: 'center' }) pObj = docx.createP({ align: 'center' }) pObj.addText('志愿服务时间证明', { bold: true, font_face: 'KaiTi', font_size: 19, color: '595959' }); pObj = docx.createP() pObj = docx.createP({ align: 'justify' }) pObj.options.indentFirstLine = 440; if (item.volunteerInfo[i].academy && item.volunteerInfo[i].major && item.volunteerInfo[i].grade) { var txt = item.volunteerInfo[i].academy + ' ' + item.volunteerInfo[i].major + '专业 ' + item.volunteerInfo[i].grade + ' ' + item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } else { var txt = item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } pObj.addText(txt, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('特此证明。', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('证明人:' + event.tea_info.name + ' ' + event.tea_info.phone, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。核验信息与此证明一致时,此证明不加盖公章仍然有效;若不一致,则以加盖公章的证明为准。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('XXXXX', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP({ align: 'right' }) pObj.addText(timeBottom, { font_face: 'FangSong', font_size: 15, color: '595959' }); // Add a Footer: pObj = docx.createP() pObj = docx.createP() pObj = docx.createP() pObj.addText('XXXXX证明_' + item._id, { font_face: 'FangSong', font_size: 10, color: '808080' }); pObj = docx.createP() pObj.addText(time.toString(), { font_face: 'FangSong', font_size: 10, color: '808080' }); if (i != ((item.volunteerInfo).length - 1)) { docx.putPageBreak() } } //=======================建构文档内容结束========================= // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/' + fileName) return new Promise((resolve, reject) => { docx.generate(out); out.on('close', async function(){ console.log("文件已被关闭,总共写入字节", out.bytesWritten) // console.log('写入的文件路径是'+ out.path); var fileBuf = await readDocx_fs(out.path); var upd = await cloud.uploadFile({ cloudPath: varpath, fileContent: fileBuf, }); console.log(docx) resolve({ event, upd, size: Math.floor(100*out.bytesWritten/1024)/100 + "KB" }) }); out.on('error', (err) => { console.error(err); reject({ errMsg: err }) }); }) }
2021-10-15 - 云函数使用云存储的word模板文件生成新的word
今天遇到一个场景是需要使用云数据库里的数据生成word文件,word文件有模板,在这里做一下经验分享,如果各位大佬有其他方法,欢迎分享,下面直接上过程 1、需要的插件 使用“npm install docx-templates”命令安装 右键云函数,选择“在内建终端中打开”,执行命令 [图片] 2、云函数实现 因为云函数不能直接读取云存储的文件,所以这里先下载然后读取 exports.main = async (event, context) => { const word = cloud.downloadFile({ fileID:'这里使用你云存储的fileID' }) const template = (await word).fileContent const buffer = await createReport({ template, data: { name: '替换的内容', // 这里的key值和你在word模板里面写的要一致,可以有多个 }, cmdDelimiter: ['{', '}'] // 分隔符 }) const time = new Date(); const preDir = time.getFullYear()+"/"+(time.getMonth()+1)+"/"+time.getDate() const stringRandom = require('string-random') const randfilename = stringRandom(32) //随机文件名 const cloudPath = `templates/docx/${preDir}/${randfilename}.docx` //文件 return await cloud.uploadFile({ cloudPath, fileContent: Buffer.from(buffer, 'hex') }) } word模板文件内容(根据自己的实际使用去修改) [图片] 使用后生成的新的文件内容 [图片] 备注:没有找到云函数直接操作云存储的方法,各位大神如果知道,可以发享一下,欢迎评论留言 在解决这个问题搜到的比较有用的文章链接:https://blog.csdn.net/xjc8289555/article/details/118084368这个里面的代码我在使用上发现他的文件读取会有一点问题(云环境的原因),所以做了一些改动,但是确实是帮助到我了
2022-03-18 - [开盖即食]小程序那点事(1)---云开发的一些实战应用分享
[图片] 一、常用云函数 分享一些入门的云函数功能 [代码]const cloud = require('wx-server-sdk') cloud.init() exports.main = async(event, context) => { console.log(event.txt); const { value,type } = event; //返回服务器时间 if(type == "serverTime"){ return Date.now(); } //返回用户各类id if(type == "info"){ const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } //检查内容 if(type == 'checkContent'){ try { let result = await cloud.openapi.security.msgSecCheck({ content: value }); return result; } catch (err){ return err; } } //检查图片 if(type == 'checkImg'){ try { // const result = await cloud.openapi.security.msgSecCheck({ // content: event.txt // }) const result = await cloud.openapi.security.imgSecCheck({ media: { header: { 'Content-Type': 'application/octet-stream' }, contentType: 'image/png', //这里也可动态配置 value: Buffer.from(value) } }) // result 结构 // { errCode: 0, errMsg: 'openapi.templateMessage.send:ok' } console.log(result); return result; } catch (err) { return err } } } [代码] 二、给接口添加签名sign/加密 [图片] 利用云函数来签名接口,防止用户伪造或者绕过小程序直接用postman/脚本/爬虫 调用抓取后端数据。同理,也可以对数据全加密 [代码]// 云函数部分 const cloud = require('wx-server-sdk'); const crypto = require("crypto"); const salt = '996-007-icu-no-overtime'; //加密用的盐,需要跟后端约定好 cloud.init() // 云函数入口函数 exports.main = async (event, context) => { //const wxContext = cloud.getWXContext() let json = event.json; json.salt = salt; //排序 let arr = [] for (let i in json) { arr.push({ key: i, value: json[i] }); }; //首字母排序,你也可以不排,但是考虑到到时候要合后端验证一致,排序方便点 arr.sort((a, b) => { return a.key.charCodeAt(0) - b.key.charCodeAt(0); }); let secret = ''; for (let y = 0; y < arr.length; y++) { secret += arr[y].key + arr[y].value; }; let md5 = crypto.createHash("md5"); // 创建 md5 let md5Sum = md5.update(secret).digest("Hex"); // update 加密 //console.log(md5Sum) return { md5Sum, //secret, } } //页面调用部分 let t = new Date().getTime(); let sign = 'isEmpty'; try { //通过云函数获取加密签名 let result = (await wx.cloud.callFunction({ name: "overTime007", data: { json: { t, characterName, realm, } } })).result; sign = result.md5Sum; } catch (err) { console.log(err); sign = "signFail"; }; [代码] 三、数据库简单增删改查 小程序端和云开发端都可以对数据库进行操作,但云端更安全且权限更大。如果遇到小程序端无法操作的情况,可以先检查下数据权限问题 [图片] [代码]const db = wx.cloud.database(); // 向集合overTime中添加数据 db.collection('overTime').add({ data: { description: 'test', t: new Date(), tags: [ '云开发', '数据库' ], done: false } }).then(res => { console.log(res) }) //删除 db.collection('overTime').doc('t996_007').remove().then(res => { console.log(res) }) //修改 db.collection('overTime').doc('t996_007').update({ data: { description: 'test2', done: true } }).then(res => { console.log(res) }) //查询 db.collection('overTime').get().then(res => { console.log(res.data) }) [代码] 云开发强大在于用简单的函数就能完成很多需要服务器配置的功能,官方也推出低代码,一键生成环境等功能,大大提高了效率,降低开发门槛。 官方相关文档: https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/product/wxcloudrun.html https://developers.weixin.qq.com/community/business/course/00068c2c0106c0667f5b01d015b80d 如有疑问请留言~ 觉得有用,请点个赞哦,我会继续努力分享有用的实战内容~
2022-02-08 - 小程序web-view的H5页面使用localstorage的问题
web-view内部的H5页面,使用localstorage存储数据,部分安卓机型 偶现 此问题: 杀掉小程序的进程,H5页面的localstorage数据会全部丢失。(暂不清楚不杀掉进程,放后台是否也会丢失) 想咨询一下,web-view内部的H5页面使用localstorage,能否实现持久化存储,小程序保存web-view内部网页localstorage数据的时机是什么时候(因为同一机型有时丢失有时不丢失)
2019-03-05 - simple-cloudbase助力小程序云开发
[图片] simple-cloudbase助力小程序云开发 simple-cloudbase 是一套助力于小程序云开发的工具链。 它使用非常的简单,配置也非常少,专注于改善小程序云开发的开发体验。 同时,吸收了现代化的Nodejs项目的特性,使得它支持 [代码]js(cjs,esm)[代码] 和 [代码]typescript[代码] ,还支持路径别名 [代码]alias[代码], 多个云函数共享的 [代码]npm[代码] 包,代码压缩等等特性。 而且,现有的云开发项目,想要迁移进来也是非常简单的。它也能够一键式的生成部署文件,让我们开发者不需要再去微信IDE里,一个一个右键上传并部署。 接下来就让我们看看怎么使用它吧! 快速上手 [代码]# 在你的项目中安装 yarn global add simple-cloudbase # 这里会注册 stcb 指令 ,因为 cloudbase 的 alias 是 tcb,所以 stcb(simple-cloudbase) # 初始化项目 stcb init cloudfunctions cd cloudfunctions # 安装包 yarn # 开发watch模式 yarn dev # 打包项目 yarn build # 生成 cloudbaserc.json 部署文件 yarn gen # 微信云开发部署: yarn global add @cloudbase/cli # tcb 登录到指定的小程序环境 tcb login # 部署云函数 tcb fn deploy [代码] 用这几步,就可以代替微信IDE的操作了,同时也能进行一定的运维管理: 部署成功 [图片] 列出函数 [图片] 删除函数 [图片] 等等其他的功能也都是可以实现的。 默认项目结构 [代码]cloudfunctions # 云开发项目目录 - dist # 编译打包后的函数 - src # 函数源代码 - fn1 # 函数目录 - config.json # 函数的 openapi config - index.[js/ts] # 函数源代码 - fn2 # 另一个函数 - ... - common - index.[js/ts] # 公共的 lib - simple.json # stcb的函数内配置文件 - ... - package.json # package.json - [ts/js]config.json # 设置编译配置和别名 - .env # 部署环境变量文件 - .env.dev # ENV_ID=[你的dev环境] tcb fn deploy --mode dev - .env.prod # ENV_ID=[你的prod环境] tcb fn deploy --mode prod - cloudbaserc.json # 由stcb根据 dist中的函数 stcb gen自动生成 [代码] 设计理念 对于 [代码]serverless[代码] 来说,写配置文件一直是个耗时耗力的工作,这点不论是 [代码]serverless.yml[代码] 还是 [代码]cloudbaserc.json[代码] 都是如此,在没有智能提示的情况下,开发者不得不对照着文档,一遍一遍的改参数部署调试。 所以笔者设定了一个默认的部署配置,动态的去生成配置文件(后续和通过自定义模板覆盖),来帮助缓解这个问题。 还支持了更多的[代码]js[代码],还有别名,函数打包降低体积,适配[代码]Nodejs12.16[代码]环境等等的特性。 这些都是依靠之前对 [代码]serverless[代码] 项目开发总结的经验,一步一步走出来的。 同时也设计支持了 [代码]wxContext[代码] 的本地 [代码]mock[代码],后续还将编写更多的插件,让云开发完成更复杂的工作。 如果您有建议或者意见,或者使用中遇到的各种问题,欢迎来 [代码]Github[代码] 提出。 单函数使用的 router 和 proxy 正在开发中,敬请期待… 附录 文档地址 项目地址
2021-11-12 - 微信公众号H5 localStorage自动被清除
登录成功后会储存token 到localStorage,现在经常有用户反馈会频繁需要登录,我们设置的token是永不过期,绝大部分是华为手机,经过测试,大约一定时间后(30分钟-几个小时不等)localStorage就会丢失
2019-05-16 - CC校友登记小程序(云开发)
项目介绍 但是由于年代久远,学校又经历了多次合并发展,有些校友毕业后流动等原因,目前还有很多校友的信息校友分会没有掌握,这极大地影响了校友分会向更多的校友提供母校发展的最新信息并提供更为贴心的服务, 有鉴于此,校友会决定开展校友信息登记工作。这次信息登记。 各位校友所登记的信息也将仅用于校友相关事宜,不会被用于商业用途,亦不用担心信息外泄等。 同时,恳请各位向同班、同届和认识的其他校友广为推介这次校友信息登记活动,让更多的校友加入校友会这个大家庭。 功能说明 [图片] 特色特点 简约:不臃肿,主打内容极简,功能简洁直击痛点 安全:保护校友的信息安全,隐私内容仅后台管理员后可见。 方便:上传自己的个人信息,方便在需要时取得联系。小程序无需下载APP随用随走。 技术运用 项目使用微信小程序平台进行开发。 使用腾讯云开发技术,免费资源配额, 无需域名和服务器即可搭建。 小程序本身的即用即走,适合小工具的使用场景,也适合程序的开发。 项目效果截图 [图片] [图片] [图片] [图片] [图片] [图片] 项目后台截图 [图片] [图片] [图片] 部署教程: 1 源码导入微信开发者工具 [图片] 2 开通云开发环境 参考微信官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html 在使用云开发能力之前,需要先开通云开发。 在开发者工具的工具栏左侧,点击 “云开发” 按钮即可打开云控制台,根据提示开通云开发,并且创建一个新的云开发环境。 [图片] 每个环境相互隔离,拥有唯一的环境 ID(拷贝此ID,后面配置用到),包含独立的数据库实例、存储空间、云函数配置等资源; 3 云函数及配置 本项目使用到了一个云函数reg_cloud [图片] 在云函数cloudfunctions文件夹下选择云函数reg_cloud , 右键选择在终端中打开,然后执行 npm install –product [图片] [图片] 打开cloudfunctions/reg_cloud/comm/ccmini_config.js文件,配置环境ID和后台管理员手机号码 [图片] 4 客户端配置 打开miniprogram/app.js文件,配置环境ID [图片] 5 云函数配置 在微信开发者工具-》云开发-》云函数-》对指定的函数添加环境变量 [服务端时间时区TZ] =>Asia/Shanghai [函数内存] =>128M [函数超时时间] => 20秒 [图片] 6 设置图片域名信任关系 进入小程序 开发管理=》开发设置=》服务器域名 =》downloadFile合法域名 添加2个域名: 1)你的云存储域名,格式类似:https://1234-test-pi5po-1250248.tcb.qcloud.la 2)微信头像域名:https://thirdwx.qlogo.cn [图片] 7 上传云函数&指定云环境ID [图片] 至此完全部署配置完毕。 gitee源码地址 https://gitee.com/minzonetech/ccreg/ 在线演示: [图片] 如有疑问,欢迎骚扰联系我鸭: 俺的微信: cclinux0730 https://gitee.com/minzonetech/ccreg
2021-08-17 - iOS端中security.imgSecCheck接口报错data exceed max size?
在开发工具中不会出现,真机,手机预览测试都会出。 imgSecCheck接口传的是base64,大小确定在1M以下仍然无法进行下去,而且不会走fail回调。 目前只能先上传至云端,然后传url进行检测,比较耗费时间,希望官方能解决下
2020-08-24 - wx-cropper 基于微信小程序的裁剪功能 可旋转
#微信小程序-Canvas实现对图片裁剪,旋转后裁剪,裁剪框可拖动# [图片] [图片] [图片] 1.使用方法 引入组件 { "usingComponents": { "cropper": "../../components/cropper/index", "nav": "../../components/nav/index" }, "disableScroll": true } 2.目录 [图片] 3.wxml引用 <view class="canvas" style="width: {{canvas.width}}px;height:{{canvas.height}}px;"> <cropper class="cropper" src="{{src}}" rotate="{{rotate}}" bindimgUrl="getUrl" ></cropper> </view> 4.js // Pages/index/index.js Page({ /** * 页面的初始数据 */ data: { src: '', canvas: { width: wx.getSystemInfoSync().windowWidth, height: wx.getSystemInfoSync().windowHeight - 150 - getApp().data.navBarHeight, }, screenRatio: getApp().data.screenRatio, rotate: 0 }, onLoad: function (options) { this.ctx = wx.createCameraContext(); this.cropper = this.selectComponent(".cropper"); }, chooseImage() { let _self = this; wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success (res) { // tempFilePath可以作为img标签的src属性显示图片 const tempFilePaths = res.tempFilePaths[0]; _self.setData({ src: tempFilePaths }) _self.cropper.init(tempFilePaths) } }) }, canvasInit() { // 通过 SelectorQuery 获取 Canvas 节点 const query = query.select('.cropper'); console.log(query.select('#my-canvas')) }, setRotate() { let { rotate } = this.data; this.setData({ rotate: rotate === 270 ? 0 : rotate + 90 }) }, save() { this.cropper.save() }, getUrl(e) { // 裁剪后的照片资源 console.log(e.detail.url) } })
2021-08-11 - 云开发的CMS后台管理系统关联表时,cms上显示的是关联的富文本数据,而云数据库中却是一段编码?
[图片] 为啥,数据库中显示的和cms上的数据不一样,求大佬! span style="color: red; --darkreader-inline-color:#d91616;" data-darkreader-inline-color>只有关联的数据是富文本时才会这样!
2020-12-12 - 小程序调试新方案——使用WeConsole监控console/network/api/component/storage
[图片] 一、背景与简介 在传统的 PC Web 前端开发中,浏览器为开发者提供了体验良好、功能丰富且强大的开发调试工具,比如常见的 Chrome devtools 等,这些调试工具极大的方便了开发者,它们普遍提供查看页面结构、监听网络请求、管理本地数据存储、debugger 代码、使用 Console 快速显示数据等功能。 但是在近几年兴起的微信小程序的前端开发中,却少有类似的体验和功能对标的开发调试工具出现。当然微信小程序的官方也提供了类似的工具,那就是 vConsole,但是相比 PC 端提供的工具来说确实无论是功能和体验都有所欠缺,所以我们开发了 weconsole 来提供更加全面的功能和更好的体验。 基于上述背景,我们想开发一款运行在微信小程序环境上,无论在用户体验还是功能等方面都能媲美 PC 端的前端开发调试工具,当然某些(如 debugger 代码等)受限于技术在当前时期无法实现的功能我们暂且忽略。 我们将这款工具命名为[代码]Weimob Console[代码],简写为[代码]WeConsole[代码]。 项目主页:https://github.com/weimobGroup/WeConsole 二、安装与使用 1、通过 npm 安装 [代码]npm i weconsole -S [代码] 2、普通方式安装 可将 npm 包下载到本地,然后将其中的[代码]dist/full[代码]文件夹拷贝至项目目录中; 3、引用 WeConsole 分为[代码]核心[代码]和[代码]组件[代码]两部分,使用时需要全部引用后方可使用,[代码]核心[代码]负责重写系统变量或方法,以达到全局监控的目的;[代码]组件[代码]负责将监控的数据显示出来。 在[代码]app.js[代码]文件中引用[代码]核心[代码]: [代码]// NPM方式引用 import 'weconsole/init'; // 普通方式引用 import 'xxx/weconsole/init'; [代码] 引入[代码]weconsole/init[代码]后,就是默认将 App、Page、Component、Api、Console 全部重写监控!如果想按需重写,可以使用如下方式进行: [代码]import { replace, restore, showWeConsole, hideWeConsole } from 'weconsole'; // scope可选值:App/Page/Component/Console/Api // 按需替换系统变量或函数以达到监控 replace(scope); // 可还原 restore(scope); // 通过show/hide方法控制显示入口图标 showWeConsole(); [代码] 如果没有显式调用过[代码]showWeConsole/hideWeConsole[代码]方法,组件第一次初始化时,会根据小程序是否[代码]开启调试模式[代码]来决定入口图标的显示性。 在需要的地方引用[代码]组件[代码],需要先将组件注册进[代码]app/page/component.json[代码]中: [代码]// NPM方式引用 "usingComponents": { "weconsole": "weconsole/components/main/index" } // 普通方式引用 "usingComponents": { "weconsole": "xxx/weconsole/components/main/index" } [代码] 然后在[代码]wxml[代码]中使用[代码]<weconsole>[代码]标签进行初始化: [代码]<!-- page/component.wxml --> <weconsole /> [代码] [代码]<weconsole>[代码]标签支持传入以下属性: [代码]properties: { // 组件全屏化后,距离窗口顶部距离 fullTop: String, // 刘海屏机型(如iphone12等)下组件全屏化后,距离窗口顶部距离 adapFullTop: String, } [代码] 4、建议 如果不想将 weconsole 放置在主包中,建议将组件放在分包内使用,利用小程序的 分包异步化 的特性,减少主包大小 三、功能 1、Console 界面如图 1 实时显示[代码]console.log/info/warn/error[代码]记录; [代码]Filter[代码]框输入关键字已进行记录筛选; 使用分类标签[代码]All, Mark, Log, Errors, Warnings...[代码]等进行记录分类显示,分类列表中[代码]All, Mark, Log, Errors, Warnings[代码]为固定项,其他可由配置项[代码]consoleCategoryGetter[代码]产生 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]记录可弹出操作项(如图 2): [代码]复制[代码]:将记录数据执行复制操作,具体形式可使用配置项[代码]copyPolicy[代码]指定,未指定时,将使用[代码]JSON.stringify[代码]序列化数据,将其复制到剪切板 [代码]取消置顶/置顶显示[代码]:将记录取消置顶/置顶显示,最多可置顶三条(置顶无非是想快速找到重要的数据,当重要的数据过多时,就不宜用置顶了,可以使用[代码]标记[代码]功能,然后在使用筛选栏中的[代码]Mark[代码]分类进行筛选显示) [代码]取消留存/留存[代码]:留存是指将记录保留下来,使其不受清除,即点击[代码]🚫[代码]按钮不被清除 [代码]取消全部留存[代码]:取消所有留存的记录 [代码]取消标记/标记[代码]:标记就是将数据添加一个[代码]Mark[代码]的分类,可以通过筛选栏快速分类显示 [代码]取消全部标记[代码]:取消所有标记的记录 [图片] 图 1 [图片] 图 2 2、Api 界面如图 3 实时显示[代码]wx[代码]对象下的相关 api 执行记录 [代码]Filter[代码]框输入关键字已进行记录筛选 使用分类标签[代码]All, Mark, Cloud, xhr...[代码]等进行记录分类显示,分类列表由配置项[代码]apiCategoryList[代码]与[代码]apiCategoryGetter[代码]产生 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]记录可弹出操作项(如图 4): [代码]复制[代码]:将记录数据执行复制操作,具体形式可使用配置项[代码]copyPolicy[代码]置顶,未指定时,将使用系统默认方式序列化数据(具体看实际效果),将其复制到剪切板 其他操作项含义与[代码]Console[代码]功能类似 点击条目可展示详情,如图 5 [图片] 图 3 [图片] 图 4 [图片] 图 5 3、Component 界面如图 6 树结构显示组件实例列表 根是[代码]App[代码] 二级固定为[代码]getCurrentPages[代码]返回的页面实例 三级及更深通过[代码]this.selectOwnerComponent()[代码]进行父实例定位,进而确定层级 点击节点名称(带有下划虚线),可显示组件实例详情,以 JSON 树的方式查看组件的所有数据,如图 7 [图片] 图 6 [图片] 图 7 4、Storage 界面如图 8 显示 Storage 记录 [代码]Filter[代码]框输入关键字已进行记录筛选 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]操作项含义与[代码]Console[代码]功能类似 点击条目后,再点击[代码]❌[代码]按钮可将其删除 点击[代码]Filter[代码]框左侧的[代码]刷新[代码]按钮可刷新全部数据 点击条目显示详情,如图 9 [图片] 图 8 [图片] 图 9 5、其他 界面如图 10 默认显示 系统信息 可通过[代码]customActions[代码]配置项进行界面功能快速定制,也可通过[代码]addCustomAction/removeCustomAction[代码]添加/删除定制项目 几个简单的定制案例如下,效果如图 11: [代码]import { setUIRunConfig } from 'xxx/weconsole/index.js'; setUIRunConfig({ customActions: [ { id: 'test1', title: '显示文本', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.text, handler(): string { return '测试文本'; } }, { id: 'show2', button: '查看2', showMode: WcCustomActionShowMode.text, handler(): string { return '测试文本2'; } } ] }, { id: 'test2', title: '显示JSON', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.json, handler() { return wx; } } ] }, { id: 'test3', title: '显示表格', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.grid, handler(): WcCustomActionGrid { return { cols: [ { title: 'Id', field: 'id', width: 30 }, { title: 'Name', field: 'name', width: 70 } ], data: [ { id: 1, name: 'Tom' }, { id: 2, name: 'Alice' } ] }; } } ] } ] }); [代码] [图片] 图 10 [图片] 图 10 四、API 通过以下方式使用 API [代码]import { showWeConsole, ... } from 'weconsole'; showWeConsole(); [代码] replace(scope:‘App’|‘Page’|‘Component’|‘Api’|‘Console’) 替换系统变量或函数以达到监控,底层控制全局仅替换一次 restore(scope:‘App’|‘Page’|‘Component’|‘Api’|‘Console’) 还原被替换的系统变量或函数,还原后界面将不在显示相关数据 showWeConsole() 显示[代码]WeConsole[代码]入口图标 hideWeConsole() 隐藏[代码]WeConsole[代码]入口图标 setUIConfig(config: Partial<MpUIConfig>) 设置[代码]WeConsole[代码]组件内的相关配置,可接受的配置项及含义如下: [代码]interface MpUIConfig { /**监控小程序API数据后,使用该选项进行该数据的分类值计算,计算后的结果显示在界面上 */ apiCategoryGetter?: MpProductCategoryMap | MpProductCategoryGetter; /**监控Console数据后,使用该选项进行该数据的分类值计算,计算后的结果显示在界面上 */ consoleCategoryGetter?: MpProductCategoryMap | MpProductCategoryGetter; /**API选项卡下显示的数据分类列表,all、mark、other 分类固定存在 */ apiCategoryList?: Array<string | MpNameValue<string>>; /**复制策略,传入复制数据,可通过数据的type字段判断数据哪种类型,比如api/console */ copyPolicy?: MpProductCopyPolicy; /**定制化列表 */ customActions?: WcCustomAction[]; } /**取数据的category字段值对应的prop */ interface MpProductCategoryMap { [prop: string]: string | MpProductCategoryGetter; } interface MpProductCategoryGetter { (product: Partial<MpProduct>): string | string[]; } interface MpProductCopyPolicy { (product: Partial<MpProduct>); } /**定制化 */ interface WcCustomAction { /**标识,需要保持唯一 */ id: string; /**标题 */ title: string; /**默认执行哪个case? */ autoCase?: string; /**该定制化有哪些情况 */ cases: WcCustomActionCase[]; } const enum WcCustomActionShowMode { /**显示JSON树 */ json = 'json', /**显示数据表格 */ grid = 'grid', /** 固定显示<weconsole-customer>组件,该组件需要在app.json中注册,同时需要支持传入data属性,属性值就是case handler执行后的结果 */ component = 'component', /**显示一段文本 */ text = 'text', /**什么都不做 */ none = 'none' } interface WcCustomActionCase { id: string; /**按钮文案 */ button?: string; /**执行逻辑 */ handler: Function; /**显示方式 */ showMode?: WcCustomActionShowMode; } interface WcCustomActionGrid { cols: DataGridCol[]; data: any; } [代码] addCustomAction(action: WcCustomAction) 添加一个定制化项目;当你添加的项目中需要显示你自己的组件时: 请将 case 的[代码]showMode[代码]值设置为[代码]component[代码] 在[代码]app.json[代码]中注册名称为[代码]weconsole-customer[代码]的组件 定制化项目的 case 被执行时,会将执行结果传递给[代码]weconsole-customer[代码]的[代码]data[代码]属性 开发者根据[代码]data[代码]属性中的数据自行判断内部显示逻辑 removeCustomAction(actionId: string) 根据 ID 删除一个定制化项目 getWcControlMpViewInstances():any[] 获取小程序内 weconsole 已经监控到的所有的 App/Page/Component 实例 log(type = “log”, …args) 因为 console 被重写,当你想使用最原始的 console 方法时,可以通过该方式,type 就是 console 的方法名 on/once/off/emit 提供一个事件总线功能,全局事件及相关函数定义如下: [代码]const enum WeConsoleEvents { /**UIConfig对象发生变化时 */ WcUIConfigChange = 'WcUIConfigChange', /**入口图标显示性发生变化时 */ WcVisableChange = 'WcVisableChange', /**CanvasContext准备好时,CanvasContext用于JSON树组件的界面文字宽度计算 */ WcCanvasContextReady = 'WcCanvasContextReady', /**CanvasContext销毁时 */ WcCanvasContextDestory = 'WcCanvasContextDestory', /**主组件的宽高发生变化时 */ WcMainComponentSizeChange = 'WcMainComponentSizeChange' } interface IEventEmitter<T = any> { on(type: string, handler: EventHandler<T>); once(type: string, handler: EventHandler<T>); off(type: string, handler?: EventHandler<T>); emit(type: string, data?: T); } [代码] 五、后续规划 优化包大小 单元测试 体验优化 定制化升级 基于网络通信的界面化 weconsole 标准化 支持 H5 支持其他小程序平台(支付宝/百度/字节跳动) 六、License WeConsole 使用 MIT 协议. 七、声明 生产环境请谨慎使用。
2021-07-14 - #小程序云开发挑战赛#-垃圾问问-微旺网络
应用场景“垃圾问问”是为了方便居民日差查询垃圾分类、了解垃圾分类政策和知识的小程序。 垃圾分类正成为国民生活新时尚,各地都推出了系列垃圾分类新举措。垃圾问问不仅提供了垃圾分类的知识,还可以根据城市切换、垃圾库实时更新。比如宠物粪便、口红等冷门分类,都能在”垃圾问问“里得到答案。贴心的语音查询,更是为不方便输入的用户提供了便利。 目标用户对于某些垃圾不知道如何归类投放的用户。 实现思路本小程序采用基于云开发的原生开发,用到了云数据库存储数据,使用云函数和小程序端进行数据交互。 A. 整体架构图如下: [图片] b. 云函数端 使用tcb-router路由分发,代码结构更利于功能路由的规划,未来功能模块的横向展开。 c. 小程序端 引入mobx支持,划分为vm层(mobx store,page+wxml)以及service层,结构清晰,易于扩展,同时mobx store的存在,也让多page、多组件之间的通讯变的简单。 小程序端架构如下: [图片] 效果截图[图片] [图片] [图片] [图片] [图片] [图片] 作品体验二维码[图片] 功能演示腾讯视频:https://v.qq.com/x/page/f3152chjcwu.html 团队简介本团队的核心成员来自微旺网络科技有限公司,在微信开放社区也积极活跃。
2020-09-16 - H5和小程序间的通信
1.h5向小程序发送消息,根据官方文档,网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。小程序页面通过 bindmessage 绑定的函数读取 post 信息。 2.微信小程序怎么向H5发送消息呢? 目前常用的方法是通过设置webview指向网页的链接(url)拼接参数,然后H5页面截取url中的参数的方式来通信。 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 基于以上两种相互间的通信和传参,现在要解决H5页面A,跳转到H5页面B,在页面B中做了一些操作(这些操作会改变了页面A中的一些数据),然后返回到页面A,希望将这些改变反馈到页面A。 如果页面A跳转到页面B是使用的location.href,操作完成后完返回上一页(页面A)用的是window.history.go(-1)。这时是不会触发visibilitychange事件,可以使用pageshow事件window.addEventListener('pageshow', (event) => { if (event.persisted) { // 是否从缓存读取 } }) 2.第二种方式是使用hashchange来处理的,具体操作如下: 页面B中发送postMessage,增加标识符test,值为test_时间戳,用来告知webview,返回上一页面的时在页面A的url后拼接#test_。(建议使用时间戳,如果是固定值,hashchange之后执行一次) webview中接收页面B发送的消息postMessage,判断是否存在test,如果有则将test的值存入全局变量。在webview的onShow中判断全局变量中是否有test,如果有,修改webview的src,增加hash参数#test_,如果没有则不增加。清除全局变量test。由于只修改了hash部分,页面不会重新刷新。 页面A中绑定hashchange事件,hashchange事件执行自定义逻辑方法,读取hash参数,调用window.history.go(-1),恢复history。
2021-08-03 - 使用 MobX 来管理小程序的跨页面数据
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings [代码] (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: [图片] 详情页如下: [图片] 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list }), }) [代码] 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action }) // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) } }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> [代码] 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 }) // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> [代码] 完整示例 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
2019-11-01 - 挑战答题逻辑细节梳理
挑战答题逻辑细节梳理 其实说挑战答题,我之前确实也做过,确实是挑战但不是PK,也就是10个题,在哪一道做错了,便停在那里,本文说的挑战答题其实是PK答题 这一点要首先说清楚 ~ 挑战答题,准确说2人PK答题模式,是我一直以来都在准备的模块,趁春节几天,把这个2020年的一个缺憾弥补上了, 目前该模块已开发完成,具体的逻辑细节如下所示 1 [图片] 1 [图片] 1 [图片] 1 挑战答题逻辑细节梳理 目前还存在一下几个问题 1、得分跟积分的关系,比如二人对战,10个题,一个人得分100分,一个人得分为90分,那么具体转换到积分上是怎么一个关系,需要进一步考虑 2、目前虽然完成了2人PK对战模式,但是如果做到扩展性更好,比如可以支持5人对战 以上二个问题会在该功能上线后,不断打磨完善,希望把该功能做到更好的交互,更好的体验。 当然目前由于功能刚完成,后面还会参考市面上优秀的挑战答题小程序,吸收好的思路 [图片] 1
2021-02-17 - 假期为啥不用加班,因为小程序云开发上线了这个功能!
作者:Yellowsun 清明小长假就要来了,今天你可以按时下班吗? 开发阿杰早在假期前就计划好了和女友的巴厘岛之旅,也提前订好了机票。可偏偏就在放假前一天,因公司接待需要得紧急上线一个访客预约的小程序解决自主预约及访客通知的需求,由于没有通过小程序发通知的经验沉淀,从阅读文档到产品发布可能需要耗费不少的时间,这使阿杰犯了难,原定于19:00起飞的飞机,他还能赶上吗? 好在继云函数、云存储、云数据库之后,又一提高开发效率的神器——云调用上线了! 啥是云调用? 云调用简单来说是一种免Token调用微信API的能力。传统的微信小程序开发,如果需要调用服务端的API,需要拿着 appid 和 appsecret 换取微信小程序全局唯一后台接口调用凭证 access_token ,而且有效期仅有2小时,有了这个凭证才能开始调用诸如模版消息、客服消息等API。而云调用的诞生,大大简化了调用微信API的鉴权步骤,实现一行代码即调即用。 为什么要用云调用? 微信小程序使用云调用能力后,开发者能够—— 1、一行代码调用API 云调用允许在没有获取 access_token 的情况下调用大部分小程序服务端的API,开发者只需关心业务逻辑本身及调用API的时机,真正实现一行代码调用API。 2、无需担心凭证安全 支持云调用的接口无需获取 access_token 即可调用,换句话说,就是开发者无需关心 access_token 的保管及失效问题,即可获得天然、安全、可靠的接口调用条件,一切接口调用的鉴权机制都交由云开发处理。 如何使用云调用? 阿杰阅读了云调用的文档后,瞬间理解了云调用的实现方法。结合实际产品需求,公司预期的小程序需要在用户输入预约信息提交后,向用户推送模板消息进行通知。 [图片] 就微信小程序推送模板消息,传统实现路径:用户预约成功 - 检查 access_token 是否在有效期内 - 获取 access_token - 调用 templateMessage.send - 推送模板消息。 而云调用实现路径:用户预约成功 - 调用templateMessage.send - 推送模板消息,完全无需关心 access_token 的获取、保管、失效等问题。 结合 wx-server-sdk 提供的 getWXContext 方法获取登录用户的 openid ,调用 templateMessage.send 这个方法,传入接收者 openid 、模板消息内容、模板消息id等参数,即可完成模板消息的下发。发送模板消息的核心代码如下: [图片] 将写好的云函数部署至云端,当用户预约成功后就能收到模板消息通知了。至此大功告成,自测无误提审! [图片] 阿杰巧借云开发的云调用能力,免除了 access_token 获取、校验、保管等相关处理机制的设计,在不到一下午的时间完成了整个项目。最后也如期赶到了机场,阿杰将经过转述给了在机场没等多久的女友,看着女友膜拜的眼神,阿杰拉着女友的手开心地走向了登机口... (故事纯属虚构,如有雷同... 那就是雷同) 巧用云开发,不加班不是梦 云调用的上线将进一步降低微信小程序的开发门槛,提升开发效率。免 access_token 调用 API ,距离早点下班又进了一步! [图片] 除了云调用外,云开发同时还免费提供云函数、云数据库、云存储等Serverless(无服务器计算)能力,助力微信小程序开发者! [图片] (微信开发者工具内置的云开发入口) 目前 微信开发者工具版本 >= 1.02.1903251 且 云函数 wx-server-sdk >= 0.4.0 的开发者可以直接使用云调用能力,具体使用方法详见《微信小程序开发文档》。
2019-04-09 - 1个开发如何撑起一个用户过亿的小程序
作者 LeeHey 2018年12月,腾讯相册累计用户量突破 [代码]1亿[代码],月活 [代码]1200万[代码],阿拉丁指数排行 [代码]Top30[代码],已经成为小程序生态的重量级玩家。 三个多月来,腾讯相册围绕 [代码]在微信分享相册照片[代码]这一核心场景,快速优化和新增一系列社交化功能,配合适当的运营,实现累计用户量突破 [代码]1亿[代码],大大超过预期。 [图片] (9个月,腾讯相册用户量破亿) 可是,谁曾想到,这样一个亿级体量的小程序,竟然是一个开发做出来的?他又是有哪般“绝技”,可以一个人撑起一个用户过亿的小程序? 后台人力紧缺,怎么办? 当我第一次见到腾讯相册小程序的开发David(化名)时,他显得忧心忡忡。 “年底的目标是要过千万的用户,但现在只有几位前端和后台开发。不仅如此,我们的后台开发还不是百分百能够投入到这个项目,大部分时间要抽身支援其它项目,人力非常紧缺。此外,原有后台系统有不少历史包袱,在原有架构上做新的社交化功能开发是不现实的。怎么办? “要不试试 [代码]小程序·云开发[代码]吧,只需要前端就可以把小程序搞起,正好解决我们缺后台的难题。” 于是,David作为腾讯相册前端开发团队的骨干,担当起用 [代码]小程序·云开发[代码]实现腾讯相册小程序社交化功能的重任。 “第一次接触到 [代码]小程序·云开发[代码]时,觉得这个东西(小程序·云开发)理念挺新颖的——— [代码]小程序无服务开发模式[代码]。在一般的小程序开发中,有三大功能小程序开无法绕开后台的帮助,它门分别是数据读取、文件管理以及敏感逻辑的处理(如权限)。因此,传统的开发模式,在小程序端都必须发送请求到后台进行鉴权,并且处理相关的文件或者数据。即使使用 Node 来搭建后端服务,也需要耗费不少的搭基础架构、后期运维的工作量。” [图片] “而 [代码]小程序·云开发[代码]则释放了小程序开发者的手脚,赋予了开发者安全、稳定读取数据、上传文件和控制权限的能力,其它的负载、容灾、监控等,我们小程序开发者只需要关注业务逻辑,专注写好业务逻辑即可,其他的事情完全可以不用操心了!本来我还一筹莫展,了解完 [代码]小程序·云开发[代码]的产品原理以后,我瞬间心里有谱了。” 二维码扫不出来了 [图片] 道路总是不平坦的 ,在腾讯相册小程序通往用户破亿的道路上,困难重重。 由于腾讯相册的二维码需要带上的信息量过大,因此它的二维码显得密密麻麻。这种密集的二维码在某些Android机型下,容易出现无法识别小程序的问题。 这严重制约了腾讯相册小程序分享获客的能力。 [图片] (需要存储name, ownerid, page等大量信息) 这个事情的解决并不难,只需后台开发把数据先存储到数据库中,然后把数据id放到分享链接上,这样,链接便可以转化成32个字符的短链接,让二维码看起来没有那么密集了。 但由于后台人力不足,于是前端开发David利用小程序· 云开发的数据库存储能力,通过调用 [代码]db.collection('qr').add[代码]接口,快速实现数据在数据库中的存储。 [图片] (云开发数据库,格式类似MongoDB) [图片] (云开发数据库索引,可加快数据读取) [图片] 此外,腾讯相册还借住小程序·云开发的云函数能力,生成辨识度更高的小程序码(小程序码文档https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html),用以在朋友圈里传播分享。 [图片] (生成小程序码的云函数逻辑) [图片] (优化后的分享图片和小程序码) 2天上线评论点赞功能 [图片] (评论与点赞功能) 腾讯相册在微信端的核心应用场景是“在微信做分享相册照片”,为了增强腾讯相册用户在微信里的互动,提升用户粘性和留存,腾讯相册决定新增评论与点赞功能,并且把聊天评论就直接在微信聊天窗口里面实现。 在这里,腾讯相册的David面临了两个选择,一是按原开发模式(前台开发-后台开发-前后台联调)做这个功能,面临的问题便是开发周期长、缺后台、迭代速度慢;另一个就是借助云开发的能力,自己上。 为了加快产品迭代速度,David决定采取云开发的开发方式。评论、点赞通过云开发的数据库插入和查询接口,如 [代码]db.collection('comment').add[代码],很快就实现了。 但遇到棘手的问题是,对于一些敏感的操作比如删除和编辑评论、点赞这些敏感操作,还需要到用户的鉴权操作,而这些鉴权信息,都在原有的后台。此时,云函数的路由功能便发挥出作用了。 [图片] (评论点赞逻辑) 用户进行评论点赞的时候,会在小程序端发起请求调用云函数并带上 [代码]openid[代码],云函数用 [代码]openid[代码] 查询原有的后台服务看看该用户是否有权限进行操作,如果用户具有权限,则把评论和点赞的数据都写入云开发的数据库中。 就这样,借住小程序·云开发的能力,腾讯相册仅用2天时间,完成了在传统开发模式下需要1周多工作量的开发工作。 [图片]
2019-02-15 - 云函数云开发控制台设置 TZ 为 Asia/Shanghai无效
如果需要默认 UTC+8,可以配置函数的环境变量,设置 TZ 为 Asia/Shanghai [图片] 云函数已配置,但云开发控制台查看打印的日志还是差8个小时!反复删除重新上传都一样,折腾到快下班了囧 [图片]
2020-12-10 - onShow的问题,getLaunchOptionsSync()二次进入取不到启动参数?
场景如下:a小程序跳转带参进入b小程序;此时小程序b能正确接收到a传来的参数;在b小程序单击返回,返回到a的小程序;再a里面重新单击跳转进入b小程序(此时跳转参数已经变化),小程序b里面接收到的还是上一次参数?请问这种问题怎么解决?在onShow里面使用的getLaunchOptionsSync取启动参数;
2021-01-05 - 最佳实践丨从 MySQL/MongoDB 迁移数据至 CloudBase 云数据库
迁移说明本篇文章从 MySQL、MongoDB 迁移到云开发数据库,其他数据库迁移也都大同小异。 迁移大致分为以下几步: 从 MySQL、MongoDB 将数据库导出为 JSON 或 CSV 格式创建一个云开发环境到云开发数据库新建一个集合在集合内导入 JSON 或 CSV 格式文件导出一、导出 MySQL 数据下面的流程中,我们使用 Navicat for MySQL 进行导出。您也可以使用其它 MySQL 导出工具。 1、导出为 CSV 格式 选中表后进行导出: [图片] 类型中选择 csv 格式: [图片] 注:在第 4 步时,我们需要勾选包含列的标题 [图片] 导出后的 csv 文件内容 第一行为所有键名,余下的每一行则是与首行键名相对应的键值记录。类似这样: [图片] 2、导出为 JSON 格式 同样的我们将选中的表进行导出为 json 格式: [图片] 剩余步骤全部选择默认即可。 导出后的样子: [图片] 我们将数组去除,最后是这样: [图片] 二、导出 MongoDB 数据首先我们先启动 mongod 服务: [图片] 启动后此终端不要关闭。 1、导出为 CSV 格式 新打开一个终端,输入以下命令: mongoexport -db <数据库> --collection <集合名称> --type csv -f <字段名1[,字段名2]> -o <输出的文件路径> 更详细的参数说明,请参考 MongoDB 文档。 注:导出 csv 格式时需要指定导出的列,否则会出现如下的报错信息:⚠️ csv mode requires a field list 导出后的样子: [图片] 2、导出为 JSON 格式 新打开一个终端,输入以下命令: mongoexport -db <数据库> --collection <集合名称> -o <输出的文件路径> 更详细的参数说明,请参考 MongoDB 文档。 导出后的样子: [图片] 导入1、新建云环境如果已有云环境,可直接跳过这一步打开云开发控制台新建云环境: [图片] 新建环境后耐心等待 2 分钟环境初始化过程。 2、数据库导入点击添加集合来创建一个集合: [图片] 新建之后我们点进去,并进行导入操作: [图片] 选择我们之前导出的 CSV 或 JSON 格式文件。 注意:这里有两种冲突处理模式:Insert 和 Upsert Insert 模式会在导入时总是插入新记录,同一文件不能存在重复的 _id 字段,或与数据库已有记录相同的 _id 字段。如果希望已经存在的数据不被覆盖掉,应该 Insert 模式。Upsert 模式会判断有无该条记录,如果有则更新该条记录,否则就插入一条新记录。如果不希望产生冗余重复的数据,应该使用 Upsert 模式。这里我们选择 Upsert 模式: [图片] 导入过程完毕后,数据库内可以看到导入的数据: [图片] 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 【技术交流群】添加小助手微信号 Tcloudedu1,回复:技术交流 最新资讯关注微信公众号【腾讯云云开发】
2021-04-15 - 微信云开发基础能力讲解
云数据库、云存储、云函数、云调用等云开发基础能力的使用介绍 [视频] 点此跳转:小程序云开发训练营活动页面
2021-11-26 - 调用wx.getUserProfile报-12007,如何解决?
根据社区公告的内容,我们根据公告文档中的最佳实践: 1. 安装了对应版本的开发者工具 2. 对登录授权做了向下兼容处理 3. 使用新的wx.getUserProfile 来访问用户授权,获取用户头像与昵称 4. 对授权登录业务功能进行了改造 相关改动,在开发版与体验版功能验收时一切正常,2021-04-13 00:29 分将体验版验收正常的功能发布正式版,正式版报错,打断了授权登录流程。 想知道如何解决体验版与正式版表现不一的问题,让获取用户信息的功能改造能正式发版? [图片] /** * @Description: 基础库2.10.4 2021-04-13 后可以自由调用不需要授权 * @return {Promise} 返回一个pending结束的promise */ _getUserInfo() { return new Promise((resolve, reject) => { wx.getUserInfo({ success(res) { const { rawData, iv, signature, encryptedData } = res const userInfo = { rawData, iv, signature, encryptedData } resolve(userInfo) }, fail(err) { reject(err) } }) }) }, /** * @Description: 微信在2021-04-13进行的调整 基础库2.10.4 以上的用户需要使用wx.getUserProfile 才能获取用户个人信息,地理位置,性别,昵称,头像 * @param {type} {*} * @return {type} {*} */ _getUserProfile({ successCallBack, failCallBack }) { wx.getUserProfile({ lang: 'zh_CN', desc: '更新用户头像与昵称', success: async (res) => { const { userInfo } = res successCallBack(res) }, fail: (err) => { if (isFunction(failCallBack)) { failCallBack(err) } } }) },
2021-04-13 - input隐藏的时候,placeholder仍然显示的问题?
我在使用input的时候,点击input,隐藏表单,显示搜索。但是在搜索页里面input的placeholder没有隐藏,这种情况是很大几率发生的,安卓系统,微信最新版本。 [图片] [图片]
2020-10-12 - 解决textarea的placeholder层级穿透的问题
先说下遇到的问题:之前做过的一个项目改版碰到的病例上传页面发布按钮上一版本是在底部放置的,这一版改为了顶部固定。由于上传页面顶部有两个textarea输入框所以问题就产出了。之前使用的button和view标签布的局页面上滑的时候会被textarea的placeholder穿透。不知道官方什么时候可以解决textarea这个问题。 具体问题如下图: [图片] [图片] 解决方法来源,通过社区各位大佬的回复最终得出以下结论: 1.思路: 通过原生组件去覆盖textarea元素即可 textarea不是原生组件吗 view和button干不过 那我们也找原生组件不就好了吗。所以我就看了下能使用的也就剩cover-view标签了。所以第一种解决方法就是使用原生组件去替换之前的view和button组件。 [图片] 2.思路:通过滑动页面去判断textarea元素的显示和隐藏 使用onPageScroll函数来获取页面的滚动距离 当滚动距离等于textarea元素的top减去固定到顶部的盒子的距离的时候就让textarea元素隐藏或者把textarea的placeholder设置为空也是可以解决穿透问题的。 补充: 3.思路:只用view、input、text等字段去替换textarea元素,来避免textarea的pplaceholder穿透问题 如果有弹窗或者组件被穿透了,可是做一个判断,当弹窗出现的时候设置一个hide字段,并根据字段判断textarea的显示隐藏,并且当textarea隐藏后,用一个样式相同input,或者view组件代替显示原来的textarea,当弹窗消失后,再将textarea显示,将view或者input隐藏掉(注意,给textarea设置一个bindinput的方式将输入的文字显示在view或者input里面,这样基本看不出内部组件类型的变化) [图片] 感谢大佬提供的第三种方案,感觉很不错。 如果还有其他的。欢迎留言。
2020-09-07 - 从源码看微信小程序启动过程
一、写作背景接触小程序一年多,真实体验就是小程序开发门槛相对而言确实比较低。不过小程序的开发方式,一直是开发者吐槽的,如习惯了 Vue,React 开发的开发者经常会吐槽小程序一个 Page 必须由多个文件组成,组件化支持不完善或者说不能非常愉快的开发组件。在以前小项目中没太大感觉,从加入有赞,参与有赞微商城小程序的开发,是真切的体会到对于大型小程序项目开发的复杂性。 有赞从微信小程序内测就开始开发小程序,在不支持自定义组件的时代,只能通过 import 的形式拆分模块或实现组件。在业务复杂的页面,可能会 import 非常多的模块,而相应的 wxss 也需要 import 样式,除了操作繁琐,有时候也难免遗漏。 作为开发者,我们当然希望可以让工作更简单,更愉快,也希望改善我们的开发方式。所以希望能够更了解微信小程序框架,减少不必要的试错,于是有了一次对小程序框架的 debug 之旅。(基础库 1.9.93) 通过三周空余时间的 debug,也算对小程序框架有了一些浅显的认识,达到了最初的目的;对小程序启动,实例,运行等有了真切的体会。这篇文章记录了小程序框架的基本代码结构,启动流程,以及程序实例化过程。 本文的目的是希望把我看到的分享给对小程序感兴趣或者正在开发小程序的读者,主要解答“框架对传入的对象等到底做了什么”。 二、从启动流程一窥小程序框架细节在开发者工具中使用 help() 方法,可以查看一些指令和方法。使用其中的 openVendor 方法可以打开微信开发者工具在小程序框架所在目录。其中以包括以基础库命名的目录和其他帮助文件,如其中有两个工具 wcc,wcsc。wcc 可把 wxml 转换为对应的 JS 函数 —— $gwx(path, global),wcsc 可将 wxss 转换为 css。而基础库目录包括 WAService.js 和 WAWebview.js 文件。小程序框架在开发者工具中以 WAService.js 命名(WAWebview.js 不知其作用,听说在真机环境使用该文件)。 在开发中工具命令行使用 document.head 可以查看到小程序的启动流程大致如下: [图片] 以小节的方式分别介绍这些流程,小程序是如何处理的(小节编号与图中编号相同)。 1、初始化全局变量下图是小程序启动是初始化的一些全局的变量: [图片] 那些使用“__”开头,未在文档中提及可使用变量是不建议使用的,__wxAppCode__ 在开发者工具中分为两类值,json 类型和 wxml 类型。以 .json 结尾的,其 key 值为开发者代码中对应的 json 文件的内容,.wxml 结尾的,其 key 值为通过调用 $gwx('./pages/example/index.wxml') 将得到一个可执行函数,通过调用这个函数可得到一个标识节点关系的 JSON 树。 [图片] 2、加载框架(WAService.js)使用工具对 WAService.js 进行格式化后进行 debug。可以发现小程序框架大致由: [代码]WeixinJSBridge[代码]、 [代码]NativeBuffer[代码]、 [代码]wxConsole[代码]、 [代码]WeixinWorker[代码]、 [代码]JavaScript兼容[代码](这部分为猜测)、 [代码]Reporter[代码]、 [代码]wx[代码]、 [代码]exparser[代码]、 [代码]__virtualDOM__[代码]、 [代码]__appServiceEngine__[代码] 几部分组成。 其中除了 [代码]wx[代码] 和 [代码]WeixinJSBridge[代码] 这两个基础 API 集合, [代码]exparser[代码], [代码]__virtualDOM__[代码], [代码]__appServiceEngine__[代码] 这三部分作为框架的核心, [代码]__appServiceEngine__[代码] 提供了框架最基本的接口如 App,Page,Component; [代码]exparser[代码] 提供了框架底层的能力,如实例化组件,数据变化监听,view 层与逻辑层的交互等;而 [代码]__virtualDOM__[代码] 则起着链接 [代码]__appServiceEngine__[代码] 和 [代码]exparser[代码] 的作用,如对开发者传入 Page 方法的对象进行格式化再传入 exparser 的对应方法处理。 框架对外暴露了以下API:Behavior,App,Page,Component,getApp,getCurrentPages,definePlugin,requirePlugin,wx。 3、业务代码的加载在小程序中,开发者的 JavaScript 代码会被打包为 define( 'xxx.js' , function ( require , module , exports, window, document, frames, self , location, navigator, localStorage, history, Caches , screen, alert, confirm, prompt, fetch, XMLHttpRequest , WebSocket , webkit, WeixinJSCore , Reporter , print , WeixinJSBridge ) { 'use strict' ; // your code }) 这里的 define 是在框架中定义的方法,在框架中提供了两个方法:require 和 define 用来定义和使用业务代码。其方式有些像 AMD 规范接口,通过 define 定义一个模块,使用 require 来应用一个模块。但是也有很大区别,首先 define 限制了模块可使用的其他模块,如 window,document;其次 require 在使用模块时只会传入 require 和 module,也就是说参数中的其他模块在定义的模块中都是 undefined,这也是不能在开发者工具中获取一些浏览器环境对象的原因。 在小程序中,JavaScript 代码的加载方式和在浏览器中也有些不同,其加载顺序是首先加载项目中其他 js 文件(非注册程序和注册页面的 js 文件),其次是注册程序的 app.js,然后是自定义组件 js 文件,最后才是注册页面的 js 代码。而且小程序对于在 app.js 以及注册页面的 js 代码都会加载完成后立即使用 require 方法执行模块中的程序。其他的代码则需要在程序中使用 require 方法才会被执行。 下面详细介绍了 app.js,自定义组件,页面 js 代码的处理流程。 4、加载 app.js 与注册程序在 app.js 加载完成后,小程序会使用 require('app.js') 注册程序,即对 App 方法进行调用,App 方法是对 __appServiceEngine__.App 方法的引用。 下图是框架对于 App 方法调用时的处理流程: [图片] App 方法根据传入的对象实例化一个 app 实例,其生命周期函数 onLaunch 和 onShow 因为使用不同的方式获取 options的参数。在有些需要根据场景值来实现需求的,或许使用 onShow 中的场景值更合适。 在实际开发过程中发现,在微信顶部唤起小程序和在小程序列表唤起的 options 也是不一样的。在该案例中通过点击分享的小程序进入后,关闭小程序,再通过不同方式进入小程序,通过顶部唤起的还是 options 的 path 属性还是分享出来的 path,但是通过列表中打开直接回到了首页,这里 App 中的 onShow 就会获取到不同的 options。 5、加载自定义组件代码以及注册自定义组件自定义组件在 app.js 之后被加载,小程序会在这个过程中加载完所有的自定义组件(分包中自定义组件没有有测试过),并且是加载完成后自动注册,只有注册完成后才会加载下一个自定义组件的代码。 下图是框架对于 Component 方法处理流程: [图片] 图中介绍了框架如何对传入 Component 方法的对象的处理,其后面还有很多深入的对于组件实例化的步骤没有在图中表示出来,具体可以在文章最后的附件中查看。 自定义组件在小程序中越来越完善,其拥有的能力也比 Page 更强大,而后面会提到在使用自定义组件的 Page 中,Page 实例也会使用和自定义组件一样的实例化方式,也就是说,他拥有和自定义组件一样的能力。 6、加载页面代码和注册页面加载页面代码的处理流程和加载自定义组件一样,都是加载完成后先注册页面,然后才会加载下一个页面。 下图是注册一个页面时框架对于 Page 方法的处理流程: [图片] Page 方法会根据是否使用自定义组件做不同的处理。使用自定义组件的 page 对象会被处理为和自定义组件的结构,并在页面实例化时使用不同的处理流程进行实例化。当然对于开发而言没任何不同。 从图中可以发现 Page 传入的(生命周期)代码并不会在这里被执行,可以通过下面小节了解 Page 实例化的详细过程。 7、等待页面 Ready 和 Page 实例化还记得上面介绍的启动流程中最后一步等待页面 Ready?严格来讲是等待浏览器 Ready,小程序虽然有部分原生的组件,不过本质上还是一个 web 程序。 在小程序中切换页面或打开页面时会触发 onAppRoute 事件,小程序框架通过 wx.onAppRoute 注册页面切换的处理程序,在所有程序就绪后,以 entryPagePath 作为入口使用 appLaunch 的方式进入页面。 下图是处理导航的程序流程: [图片] 从图中可以看出页面的实例化是在进入页面时进行,下图是具体的实例化过程: [图片] 下图是最终可得到 Page 实例: [图片] 可以发现其中多了 onRouteEnd API,实际该接口不会被调用。其中以 component 标记的表示只有在使用了自定义组件时才会有的方法和属性。在前面第 5 小节提到了对于使用自定义组件的页面会按照自定义组件方式解析,这些属性和方法与自定义组件表现一致。 8、关于 setData小程序框架是一个以数据驱动的框架,当然不能少了对他如何实现数据绑定的探索,下图是 Page 实例的 setData 执行流程: [图片] 其中 component:setData 表示使用自定义组件的 Page 实例的 setData 方法。 三、写在最后这是一次不完全的小程序框架探索,是在微信开发工具中 debug 的结果。虽然对于实际开发没有什么太大的帮助,但是对框架如何对开发的 js 代码进行处理有了一个很明确的认识,在使用一些 js 特性时可以有明确的感知。如果你还疑惑“小程序框架对传入的对象等到底做了什么”那一定是我表达能力太差,说声对不起。 通过这一次 debug ,也给我引入了新的问题,还希望能够有更多的讨论: 自定义组件太多启动时会耗时处理自定义组件文件太多会耗时读文件合理的设计分包很重要一份在调试过程中的笔记 小程序框架不完全分析.xmind,如果看官有兴趣可以下载看看。当然最后对于框架中已有的能力,还是非常希望微信可以开放更多稳定的接口,并在文档中告知开发者,让开发变得简单一些。
2020-10-22 - 微信小程序转发朋友圈详解
概述点击右上角分享朋友圈[图片] 分享到朋友圈样式[图片] 朋友圈打开样式[图片] 这个功能目前只支持Android(在IOS高版本微信支持朋友圈打开小程序能力,但不能分享)。 用户打开朋友圈分享的小程序,看到不是真正的小程序,而是原本页面的“单页模式”。 什么是“单页模式”?以下是微信官方对于“单页模式”的描述: “单页模式”下,页面顶部固定有导航栏,标题显示为当前页面 JSON 配置的标题。底部固定有操作栏,点击操作栏的“前往小程序”可打开小程序的当前页面。顶部导航栏与底部操作栏均不支持自定义样式。“单页模式”默认运行的是小程序页面内容,但由于页面固定有顶部导航栏与底部操作栏,很可能会影响小程序页面的布局。因此,请开发者特别注意适配“单页模式”的页面交互,以实现流畅完整的交互体验。限制另外,“单页模式”存在着很多限制。以下是官方给出的禁用能力列表: [图片] 限制主要包括以下几点: 页面无登录态,与登录相关的接口,如 [代码]wx.login[代码] 均不可用不允许跳转到其它页面,包括任何跳小程序页面、跳其它小程序、跳微信原生页面若页面包含 tabBar,tabBar 不会渲染,包括自定义 tabBar本地存储与小程序普通模式不共用这些限制,让“单页模式”只适用于内容展示,不适用于有较多交互。 配置针对“单页模式”,新增了单页模式相关配置。目前这个配置里只有一个navigationBarFit属性: [图片] navigationBarFit属性主要是针对原页面设置了自定义导航栏的情况。也就是原页面的json文件中配置了这个属性: { // ... "navigationStyle":"custom" // ... } 给大家看一下普通导航栏和自定义导航栏的区别,下图是普通导航栏页面: [图片] 下图是自定义导航栏页面,我们在原本的导航栏位置使用了banner: [图片] [代码]"navigationStyle":"custom"[代码]这个设置在“单页模式”下也会生效。前文微信官方对“单页模式”的描述有说到“顶部导航栏与底部操作栏均不支持自定义样式”。如果我们在原页面设置了自定义导航栏。那么“单页模式”样式就会变成这样: [图片] 通过设置navigationBarFit为 [代码]squeezed[代码]就可以解决这个问题: { // ... "singlePage": { "navigationBarFit": "squeezed" } // ... } 设置后的样式: [图片] 开发 接下来介绍如何在小程序中实现这个功能。 第一步在需要转发朋友圈的页面中注册用户点击右上角转发功能,这是实现转发朋友圈功能的必要满足条件。 onShareAppMessage: function () { return { title: '转发标题', path: '/pages/home/index', imageUrl: '自定义图片路径' } } 第二步注册分享朋友圈功能(从基础库 [代码]2.11.3[代码] 开始支持): onShareTimeline: function () { return { title: '转发标题', query: 'from=pyq', imageUrl: '自定义图片路径' } } 注意,这里有个问题,分享朋友圈功能不支持自定义页面路径,意味着只能转发当前页面。如果当前页面存在较多“单页模式”限制功能,就可能让我们的页面不能按预期展示。 当页面存在限制功能时,我们存在两个方案,第一个方案,针对“单页模式”做改动,不调用那些限制的功能。第二个方案,另外写一个针对“单页模式”的页面。 这两种方案都需要能判断当前是否正处在小程序“单页模式”。 我们通过判断场景值(场景值用来描述用户进入小程序的路径)是否等于 1154 来判断当前是否正处在小程序“单页模式”。场景值可以在 [代码]App[代码] 的 [代码]onLaunch[代码] 获取。 // app.js App({ // ... onLaunch(options) { const { scene } = options; this.isSinglePage = scene === 1154; } // ... }) 我们将是否正处在“单页模式”的Boolean值放入App实例,方便全局拿到值。 接下来说说两种方案。 第一种方案,在“单页模式”不调用那些限制功能(这是一种不推荐的方案,代码耦合性太强)。举个例子: const app = getApp(); Page({ // ... onLoad() { if (!app.isSinglePage) { wx.login({ // ... }) } } // ... }) 第二种方案,针对“单页模式”另写一个页面。因为分享朋友圈功能并不支持自定义页面路径,我们只能另外写一个组件来作为“单页模式”的内容承载。 将isSinglePage放入页面的初始数据,方便在wxml中拿到: // pages/home/index.js const app = getApp(); Page({ data: { isSinglePage: app.isSinglePage, } // ... }) home-single-page就是分享到朋友圈的内容承载组件: // pages/home/index.json { // ... "usingComponents": { "home-single-page": "components/home-single-page/index" }, } 当“单页模式”时,我们展示 [代码]home-single-page[代码]组件,否则就展示普通页面内容: // pages/home/index.wxml 样式上虽然搞定了,但是在原本的生命周期中可能会调用一些限制功能,或者跑一些其它“单页模式”用不上的内容。我们得停止原本生命周期函数调用。 建议对传入Page的对象进行统一处理,当“单页模式”时,不调用原本的生命周期: // pages/home/index.js import ExtendPage from 'common/extend-page/index' const app = getApp(); ExtendPage({ data: { isSinglePage: app.isSinglePage, } // ... }) ExtendPage函数针对“单页模式”进行统一处理: // common/extend-page/index.js const app = getApp(); const PAGE_LIFE = [ 'onLoad', 'onReady', 'onShow', 'onHide', 'onError', 'onUnload', 'onResize', 'onPullDownRefresh', 'onReachBottom', 'onPageScroll' ]; export default function(option) { let newOption = {}; if(app.isSinglePage) { newOption = PAGE_LIFE.reduce((res, lifeKey) => { if (option[lifeKey]) { res[lifeKey] = undefined; } return res; }, {}) } return Page({ ...option, ...newOption, }); } 在“单页模式”下,我们将原本的生命周期都停止了调用。这样就能很好的将“单页模式”下的页面和普通页面进行解耦。 如果”单页模式“页面比较复杂,需要使用生命周期。我们也可以添加 [代码]singlePageLife[代码]属性,当处在“单页模式”下,就调用 [代码]singlePageLife[代码]内的生命周期: // pages/home/index.js import ExtendPage from 'common/extend-page/index' const app = getApp(); ExtendPage({ data: { isSinglePage: app.isSinglePage, }, singlePageLife: { onLoad() { // ... }, } // ... }) // common/extend-page/index.js const app = getApp(); const PAGE_LIFE = [ 'onLoad', 'onReady', 'onShow', 'onHide', 'onError', 'onUnload', 'onResize', 'onPullDownRefresh', 'onReachBottom', 'onPageScroll' ]; export default function(option) { let newOption = {}; if(app.isSinglePage) { const { singlePageLife } = option; newOption = PAGE_LIFE.reduce((res, lifeKey) => { if (singlePageLife[lifeKey]) { res[lifeKey] = singlePageLife[lifeKey]; } else if(option[lifeKey]) { res[lifeKey] = undefined; } return res; }, {}) } return Page({ ...option, ...newOption, }); } 文章如有疏漏、错误欢迎批评指正。
2020-10-21 - 如何在公众号推文中加入EXCEL表格的链接?
由于EXCEL表格内容大多,如何在公众号推文中加入EXCEL表格的链接?
2020-10-20 - 如何实现一个自定义导航栏
自定义导航栏在刚出的时候已经有很多实现方案了,但是还有大哥在问,那这里再贴下代码及原理: 首先在App.js的 onLaunch中获取当前手机机型头部状态栏的高度,单位为px,存在内存中,操作如下: [代码]onLaunch() { wx.getSystemInfo({ success: (res) => { this.globalData.statusBarHeight = res.statusBarHeight this.globalData.titleBarHeight = wx.getMenuButtonBoundingClientRect().bottom + wx.getMenuButtonBoundingClientRect().top - (res.statusBarHeight * 2) }, failure() { this.globalData.statusBarHeight = 0 this.globalData.titleBarHeight = 0 } }) } [代码] 然后需要在目录下新建个components文件夹,里面存放此次需要演示的文件 navigateTitle WXML 文件如下: [代码]<view class="navigate-container"> <view style="height:{{statusBarHeight}}px"></view> <view class="navigate-bar" style="height:{{titleBarHeight}}px"> <view class="navigate-icon"> <navigator class="navigator-back" open-type="navigateBack" wx:if="{{!isShowHome}}" /> <navigator class="navigator-home" open-type="switchTab" url="/pages/index/index" wx:else /> </view> <view class="navigate-title">{{title}}</view> <view class="navigate-icon"></view> </view> </view> <view class="navigate-line" style="height: {{statusBarHeight + titleBarHeight}}px; width: 100%;"></view> [代码] WXSS文件如下: [代码].navigate-container { position: fixed; top: 0; width: 100%; z-index: 9999; background: #FFF; } .navigate-bar { width: 100%; display: flex; justify-content: space-around; } .navigate-icon { width: 100rpx; height: 100rpx; display: flex; justify-content: space-around; } .navigate-title { width: 550rpx; text-align: center; line-height: 100rpx; font-size: 34rpx; color: #3c3c3c; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /*箭头部分*/ .navigator-back { width: 36rpx; height: 36rpx; align-self: center; } .navigator-back:after { content: ''; display: block; width: 22rpx; height: 22rpx; border-right: 4rpx solid #000; border-top: 4rpx solid #000; transform: rotate(225deg); } .navigator-home { width: 56rpx; height: 56rpx; background: url(https://qiniu-image.qtshe.com/20190301home.png) no-repeat center center; background-size: 100% 100%; align-self: center; } [代码] JS如下: [代码]var app = getApp() Component({ data: { statusBarHeight: '', titleBarHeight: '', isShowHome: false }, properties: { //属性值可以在组件使用时指定 title: { type: String, value: '青团公益' } }, pageLifetimes: { // 组件所在页面的生命周期函数 show() { let pageContext = getCurrentPages() if (pageContext.length > 1) { this.setData({ isShowHome: false }) } else { this.setData({ isShowHome: true }) } } }, attached() { this.setData({ statusBarHeight: app.globalData.statusBarHeight, titleBarHeight: app.globalData.titleBarHeight }) }, methods: {} }) [代码] JSON如下: [代码]{ "component": true } [代码] 如何引用? 需要引用的页面JSON里配置: [代码]"navigationStyle": "custom", "usingComponents": { "navigate-title": "/pages/components/navigateTitle/index" } [代码] WXML [代码]<navigate-title title="青团社" /> [代码] 按上面步骤操作即可实现一个自定义的导航栏。 如何实现通栏的效果默认透明以及滚动更换title为白色背景,如下图所示: [图片] [图片] [图片] [图片] 最后代码片段如下: https://developers.weixin.qq.com/s/wi6Pglmv7s8P。 以下为收集到的社区老哥们的分享: @Yunior: 小程序顶部自定义导航组件实现原理及坑分享 @志军: 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能 @✨o0o有脾气的酸奶💤 [有点炫]自定义navigate+分包+自定义tabbar @安晓苏 分享一个自适应的自定义导航栏组件
2020-03-10 - #小程序云开发挑战赛#-流浪猫速查手册-猫
项目背景 今年5月中下旬,作者在网上发现了一款非常有爱的小程序,叫做《燕园猫速查手册》,因为作者本身也参与流浪猫的救助工作,流浪猫的管理主要是靠记忆,非常不方便管理,于是用业余时间参考《燕园猫速查手册》开发了《流浪猫速查手册》小程序,主要针对的是社会上的流浪猫信息的管理。后续也会开放 [代码]领养[代码] 等模块,致力于倡导大家关爱流浪猫,领养代替购买。 6月1日上线了 [代码]1.0.0[代码] 版本,小程序中只有查看功能,配套也开发了一个 [代码]PC[代码] 管理端,上线后发现志愿者和救助站等人员不习惯使用 [代码]PC[代码] 端操作,也发现了很多细节问题,如流浪猫的隐私保护等。 8月中旬本作者和《燕园猫速查手册》的作者 circle 在微信中聊了聊,重新梳理的整个小程序的功能模块,也趁此机会报名参加了 [代码]云开发挑战赛[代码],重构了 [代码]1.0.0[代码] 版本的代码,并迭代了 [代码]2.0.0[代码] 版本。 应用场景 为了方便管理流浪猫的信息,把流浪猫的信息分享给喜爱猫咪的小伙伴,流浪猫信息的上报等。 目标用户 所有喜欢猫的用户 功能模块 [图片] 实现思路 本项目前端采用 uni-app 框架开发,后端完全采用 微信小程序云开发 实现 业务流程图 [图片] 功能演示 腾讯视频:https://v.qq.com/x/page/w3153x67dxh.html 效果截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 代码链接 GitHub: https://github.com/zhiiee/cats 开源许可 《流浪猫速查手册》的源代码基于 [代码]GPL-3.0[代码] 协议全网开源,可用于商业用途,如果您使用了《流浪猫速查手册》的源代码,那么您的项目必须遵守 [代码]GPL-3.0[代码] 协议进行全网开源,点击 LICENSE 查看许可协议。 体验二维码 线上版本 最新版本还在审核中 可以扫描下面的体验版本申请访问 [图片] 体验版本 同步最新的开发进度(生产环境) [图片] 团队/作者简介 Stephen: 一个热爱宠物,兼职创业,想获得一次天使轮投资,在宠物行业闯出一片天的程序员。
2020-09-20 - 小程序跳转页面加载优化
适应场景: 小程序页面跳转redirect/navigate/其它方式 分析: 从用户触发跳转行为到下一个页面onload生命周期函数内时间差会有500ms左右,如果在页面跳转之后进行onload函数内才开始去加载页面数据,那么这500ms左右的时间就浪费了。 改进: 在页面触发跳转行为的处理函数里结合promise预先加载下个页面的数据,并将promise对象缓存,此时页面跳转和加载数据同时进行,到了目标页面再取出缓存的promise对象进行判断和取数据操作。 效果: 跳转页面加载速度提高了600ms。 示例: 代码结构 [图片] pageManager.js [代码]// 写在utils里的公用方法 const pageList = {}; module.exports = { putData:function(pageName, data){ pageList[pageName] = data; }, getData:function(pageName){ return pageList[pageName]; } } [代码] util.js [代码]const myPromise = fn => obj => { return new Promise((resolve, reject) => { obj.complete = obj.success = (res) => { resolve(res); } obj.fail = (err) => { reject(err); } fn(obj); }) } module.exports = { myPromise : myPromise } [代码] index.js [代码]// 跳转页面 const {myPromise} = require('../../utils/util'); const pageManager = require('../../utils/pageManager'); page({ data: { }, onLoad:function(){ }, gotoPageA:function(){ const PromisePageA = myPromise(wx.request)({ url : '' }).then((res)=>{ return res.data; }) pageManager.putData('pageA',promisePageA); wx.navigateTo({ url: 'pages/pageA/pageA' }) } }) [代码] pageA.js [代码]// 被跳转页面 const util = require('../../utils/util.js'); const pageManager = require('../../utils/pageManager'); const {myPromise} = require('../../utils/util'); Page({ data:{ logs:[] }, onLoad: function(){ const promisePageA = pageManager.getData('pageA'); if(promisePageA){ const resData = promisePageA.then( function(data){ }, function(){ console.log("err"); } ) } } }) [代码]
2019-10-31 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 小程序没有 DOM 接口,原因竟然是……?
拥有丰富的 Web 前端开发经验的工程师小赵今天刚刚来到新的部门,开始从事他之前没有接触过的微信小程序开发。在上手的第一天,他就向同办公室的小程序老手老李请教了自己的问题。 小赵:翻了一圈文档,小程序好像并不提供 DOM 接口?我还以为可以像之前一样用我喜欢的前端框架来做开发呢。老李,你说小程序为什么不给我们提供 DOM 接口呀。 老李:要提供 DOM 接口也没那么容易。你知道小程序的双线程模型吗?(小赵漏出了疑惑的表情)小程序是基于 Web 技术的,这你应该知道,但小程序和普通的移动端网页也不一样。你做了很多前端项目了,应该知道在浏览器里,UI 渲染和 JavaScript 逻辑都是在一个线程中执行的? 小赵:这我知道,在同一个线程中,UI 渲染和 JavaScript 逻辑交替执行,JavaScript 也可以通过 DOM 接口来对渲染进行控制。 老李:小程序使用的是一种两个线程并行执行的模式,叫做双线程模型。像我画的这样,两个线程合力完成小程序的渲染:一个线程专门负责渲染工作,我们一般称之为渲染层;而另外有一个线程执行我们的逻辑代码,我们一般叫做逻辑层。这两个线程同时运行,并通过微信客户端来交换数据。在小程序运行的时候,逻辑层执行我们编写的逻辑,将数据通过 setData 发送到渲染层;而渲染层解析我们的 WXML 和 WXSS,并结合数据渲染出页面。一方面,每个页面对应一个 WebView 渲染层,对于用户来说更加有页面的感觉,体验更好,而且也可以避免单个 WebView 的负担太重;另一方面,将小程序代码运行在独立的线程中的模式有更好的安全表现,允许有像 open-data 这样的组件可以在确保用户隐私的前提下让我们展示用户数据。 [图片] 小赵:怪不得所有和页面有关的改动都只能通过 setData 来完成。但是用两个线程来渲染我们平时用单线程来渲染的 Web 页面,会不会有些「浪费」?而且每一个页面有一个对应的渲染层,那页面变多的时候,岂不是会有很大的开销? 老李: 并不浪费,因为界面的渲染和后台的逻辑处理可以在同一时间运行了,这使得小程序整体的响应速度更快了。而在小程序的运行过程中,逻辑层需要常驻,但渲染层是可以回收的。实际上,当页面栈的层数比较高的时候,栈底页面的渲染层是会被慢慢回收的。 小赵: 原来如此。这么说的话,实际的 DOM 树是存在于渲染层的,逻辑层并不存在,所以逻辑层才没有任何的 DOM 接口,我明白了。但是……既然可以实现像 setData 这样的接口,为什么不能直接把 DOM 接口也代理到逻辑层呢?我觉得小程序可以做一个封装,让我们在逻辑层调用 DOM 接口,在渲染层调用接口后再把结果返回给我们呀。 老李:从理论上来说确实是可以的。但是线程之间的通信是需要时间的呀。将调用发送到渲染层,再将 DOM 调用结果发送回来,这中间由于线程通信发生的时间损耗可能会比这个接口本身需要的时间要多得多。如果以此为基础使用基于 DOM 接口的前端框架,大量的 DOM 调用可能会非常缓慢,让这个设计失去意义。 在实际测试中,如果每次 DOM 调用都进行一次线程通信,耗时大约是同等节点规模直接在渲染层调用的百倍以上;如果忽略通信需要的时间,一个实现良好的基于 DOM 代理的框架可以近似地看成一个动态模板的框架,而动态模板和静态模板相比要慢至少 50% 小赵:原来如此,线程通信的时间确实是我没有考虑到的问题。那现在的小程序框架中难道不存在这个问题吗? 老李: 在现在的小程序框架中,这个问题也是存在的,这也是现在的框架基于静态模板渲染的原因。静态模板可以在运行前就做好打包,直接注入到渲染层,省去线程传输的时间。在运行时,逻辑层只和渲染层进行最少的、必要的数据交换:也就是渲染用的数据,或者说 data 。另一方面,静态模板让两个线程都在启动时就拥有模板相关的所有数据,所以框架也充分利用了这一点,进行了很多优化。 小赵: 怪不得我在文档里发现很多和 setData 有关的性能提示,都提醒尽量减少设置不必要的数据,现在总算是知道为什么了。但是具体到实际开发里的时候,还是总觉得很难每次只设置需要的数据啊,像对象里或者数组里的数据怎么办呢? 老李: 如果只改变了对象里或者数组里的一部分数据,可以通过类似 array[2].message , a.b.c.d 这样的 数据路径 来进行「精准设置」。另外,现在自定义组件也支持 纯数据字段 了,只要在自定义组件的选项中设置好名为 pureDataPattern 的正则表达式, data 中匹配这个正则的字段将成为纯数据字段,例如,你可以用 /^_/ 来指定所有 开头的数据字段为纯数据字段。所有纯数据字段仅仅被记录在逻辑层的 this.data 中,而不会被发送到渲染层,也不参与任何界面渲染过程,节省了传输的时间,这样有助于提升页面更新性能。 小赵:小程序还有这样的功能,受教了。不过说来说去,我还是想在小程序里用我顺手的框架来开发,毕竟这样事半功倍嘛。我在网上搜索了一下,发现现在有很多支持用 Web 框架做小程序开发的框架,但好像都是将模板编译成 WXML,最终由小程序来做渲染,但这样的方法好像兼容性也不是很好。我在想,我们能不能在逻辑层仿造一套 DOM 接口,然后在运行时将 DOM 调用适配成小程序调用? 老李: 你的这个脑洞有一些意思。在逻辑层仿造一套 DOM 接口,直接维护一棵 DOM 树,这当然没问题。但是没有代理 DOM 接口,逻辑层的 DOM 树没法反映到渲染层,因为渲染层具体会出现什么样的组件,是运行时才能知道的,这不就没法生成静态模板了? 小赵:静态模板确实是没法生成了,但我看到小程序的框架支持自定义组件,我是不是可以做一个通用的自定义组件,让它根据传入的参数不同,变成不同的小程序内置组件。而且自定义组件还支持在自己的模板中引用自己,那么我只需要一个这个通用组件,然后从逻辑层用代码去控制当前组件应该渲染成什么内置组件,再根据它是否有子节点去递归引用自己进行渲染就可以了。你看这样可行吗? [图片] 老李: 这样的做法确实可行,而且微信官方已经按照这个思路推出小程序和 Web 端同构的解决方案 Kbone 了。Kbone 的原理就像你刚才说的那样,它提供一个 Webpack 插件,将项目编译成小程序项目;同时提供两个 npm 包,分别提供 DOM 接口模拟和你说的那个通用的自定义组件作为运行时依赖。要不你赶紧试试? 小赵:还有这么好的事,那我终于可以用我喜欢的框架开发小程序了!这么好的框架,为什么不直接内置到小程序的基础库里呀? 老李: 因为这样的功能完全可以用现在已有的基础库功能实现出来呀。Kbone 现在是 npm 包的形式,使得它的功能、问题修复可以随着自己的版本来发布,不需要依赖于基础库的更新和覆盖率,不是挺好的吗? 小赵: 好是好,但我担心的是代码包大小限制的问题。除了我们已经写好的业务逻辑之外,现在还得加上 Kbone,会不会装不下呀? 老李: 原来你是担心这个呀,放心,Kbone 现在已经可以在 扩展库 里一键搞定啦。扩展库是帮我们解决依赖的全新功能,只要在配置项中指定 Kbone 扩展库,就相当于引入了 Kbone 相关的最新版本的 npm 包,这样就不占用小程序的代码包体积了,快试试吧! 小赵:哇,那可太爽了,马上就搞起! 最后 如果你对 Kbone 感兴趣或者有相关问题需要咨询, 欢迎加入 Kbone 技术交流 QQ 群:926335938
2020-01-14 - 小游戏首屏启动优化
一、优化启动的意义 衡量一个游戏好坏的一个很重要的标准就是留存,而启动时间直接决定了第一波玩家的流失率。当用户打开游戏,满怀期待的等待游戏开始。最好的情况是游戏在1-2秒内给与反馈,或者能让用户进行下一步操作。一般首次打开,由于首包需要从服务器下载,都会有一个等待过程。在这个等待的过程中,用户的忍耐度是慢慢降低的。如果游戏在2-5秒之后才进入可用的状态,首屏留存就会受到影响。最后如果游戏超过5秒甚至更久才显示首屏,这时用户的耐心可能完全消失,有一部分用户可能会退出重新进入,但更多的用户会放弃使用。 根据小游戏整体启动留存率分析,Android玩家的首屏打开留存率约为85%。这是什么意思呢,就是玩家从点击小游戏到能看到首屏的渲染界面大约有15%的玩家流失。对于首次玩某款小游戏的玩家,由于本地没有版本缓存,留存率会明显低很多。据统计,仅代码包加载阶段新玩家流失率就达到20%。(以上数据来源微信小游戏性能优化指南) 二、晒数据 以下数据来自我们游戏优化前后的数据对比 [图片] 三、启动性能优化 小游戏启动加载时序 以下是官方给出的启动时序图和优化建议 [图片] [图片] 1.首包优化上面,我们可以完全按照官方的建议,尽量减少首包的大小,由于我们对引擎有定制,所以暂未使用引擎插件能力,我们首包中只存放了引擎及基础的启动代码,大小为1.5M。如果使用引擎插件功能,这个大小可以缩减到300K。 2.我们在首包中仅放入了游戏引擎的代码和一些必要的资源。这时候游戏尚不能完整运行,因为游戏的逻辑代码在子包中,需要进一步的加载。但是这时我们要尽快让游戏给出反馈,也就是显示首屏。首屏的内容绘制我们有两种方案:1)依赖游戏引擎绘制 2)不依赖引擎直接绘制。 1)依赖游戏引擎进行绘制 我们利用引擎进行绘制,要做到资源尽量少,能够满足绘制一个启动图和一个进度条就可以了。 a.对于使用CocosCreator制作的游戏:我们可以在游戏启动的时候, 对于第一个场景那里使用动态创建场景的方式, [图片] 这个动态创建的场景,只使用放在首包里的一些资源。 b.对于使用Laya制作的游戏:我们把原本放在工程代码里的入口代码提取出来,完成Stage的初始化。这样我们就可以做绘制了。 [图片] 2)不依赖引擎直接绘制 在第一种方案中,优化的方向也是尽量减少第一个场景的资源。但是忽略了一个很耗时的过程,引擎初始化。这一步经测试,iOS在100ms以内,安卓在1-2s。如果能把安卓这1-2s的时间优化,想想都兴奋。 为了使首屏等待时间减少到极致,在引擎初始化之前,我们自己来渲染第一帧。我们的游戏使用的Cocos Creator引擎,默认使用WebGL。我们绘制了一个最简单的黑色三角形作为游戏的第一帧。 [代码]//顶点着色器程序 var VSHADER_SOURCE = "attribute vec4 a_Position;" + "void main() {" + //设置坐标 "gl_Position = a_Position; " + "} "; //片元着色器var FSHADER_SOURCE = "void main() {" + //设置颜色 "gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);" + "}";//获取canvas元素 GameGlobal.dycc = wx.createCanvas();//获取绘制二维上下文 var gl = dycc.getContext('webgl'); //编译着色器 var vertShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertShader, VSHADER_SOURCE); gl.compileShader(vertShader); var fragShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragShader, FSHADER_SOURCE); gl.compileShader(fragShader); //合并程序 var shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertShader); gl.attachShader(shaderProgram, fragShader); gl.linkProgram(shaderProgram); gl.useProgram(shaderProgram); //获取坐标点 var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position'); var n = initBuffers(gl,shaderProgram); if(n<0){ console.log('Failed to set the positions'); } // 清除指定<画布>的颜色 gl.clearColor(0.0, 0.0, 0.0, 1.0); // 清空 <canvas> gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, n); function initBuffers(gl,shaderProgram) { var vertices = new Float32Array([ 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 ]); var n = 3;//点的个数 //创建缓冲区对象 var vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.log("Failed to create the butter object"); return -1; } //将缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer); //向缓冲区写入数据 gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW); //获取坐标点 var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position'); //将缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); //连接a_Position变量与分配给它的缓冲区对象 gl.enableVertexAttribArray(a_Position); return n; } [代码] 有了第一帧的绘制,用户可以很快进入到游戏内,不会长时间的在官方白色的加载进度屏那里等待。我们还需要做一些其他处理,不然玩家看到的将是一个黑屏。后续的处理也有两种方案,一种就是如果你对WebGL比较熟悉,可以自行完成更复杂的绘制。另外一种方案就是巧妙利用官方提供的功能。我们这里详细说下后面一种方案。 为了盖住黑屏,我们需要绘制一张启动图。这时候我们使用官方提供的接口[代码]wx.createUserInfoButton[代码]来创建一个满屏的按钮,按钮的背景图就是我们的启动图。但是这时候如果用户点击屏幕,就会造成提示用户授权,这不是我们想要的。接着我们使用官方的另外一个接口[代码]wx.showLoading[代码],创建一个模态加载弹窗就可以解决这个问题了。有了这样一个首屏,剩下的就是尽快加载子包,开始真正的游戏内容。 四、Demo 希望大家也都能探索一下首屏加载的过程。我们提供了一个简单的demo示例,通过链接即可下载到。打开demo中的index.js文件,里面有操作步骤说明。 链接: https://pan.baidu.com/s/18RS9HWpmp5l0V3WGQdNvzw 提取码: bdji 五、结语 以上就是我们的启动优化方案,欢迎各位游戏开发者交流或提出宝贵的建议,对游戏充满热爱的小伙伴也可以选择加入我们大禹网络。HR邮箱:guyifen@dayukeji.com
2019-12-30 - 记录身边优秀的小程序四-微信期末考
本文章题库会不断更新 记录身边优秀的小程序四-微信期末考 微信期末考,谁是最强王者活动题目整理 「群主退群后,哪位群成员自动获得群主位置?」 「撤回消息后多久内可以重新编辑?」 「微信期末考」是微信官方推出的一款答题小程序,有各种各样关于微信的问题。作为微信新商业第一媒体,知晓程序也是出题方之一。 [图片] 001 [图片] 002 [图片] 003 [图片] 004 [图片] 005 [图片] 006 [图片] 007 [图片] 008 [图片] 009 [图片] 010 [图片] 011 [图片] 012 [图片] 013 [图片] 本题库会不断补充 😝
2020-04-13 - 小程序跨页面通信解决方案
场景介绍 在小程序开发过程中,难免会遇到一种情况,当A页面需要用户设置数据 点击进入B页面,在B页面设置成功后返回并将设置的值传递给A页面。但是wx.navigateBack()并不支持返回传参。这种情况下就可以使用onfire.js,onfire.js 是一个很简单的事件分发的 Javascript 库(仅仅 0.9kb),简洁实用。 下载地址 onfire.js下载地址 https://www.bootcdn.cn/onfire.js/ https://gitee.com/mirrors/onfire-js 如何使用 将onfire.js下载下来并放置在开发项目某个目录下,例如根目录lib文件夹内。 在使用页面对应的js文件中引入该文件。 代码如下: A页面 [代码]<!--index.wxml--> <view class="order"> <view class="order-alert">设置您的联系方式</view> <view class="order-mobile" bindtap="setMobile"> <view class="order-mobile__caption">联系方式</view> <view class="order-mobile__content"> <text class="valign-mid"> <text>{{ mobile }}</text> </text> <text class="iconfont order-mobile__content__more"></text> </view> </view> </view> [代码] [代码]//index.js const onfire = require('../../lib/onfire.js') Page({ data: { mobile: '' }, onLoad: function () { // 绑定事件,当名为EventPhoneChange的事件发生的时 onfire.on('EventPhoneChange', e => { this.setData({ mobile: e }) }) }, // 设置手机号 setMobile: function () { wx.navigateTo({ url: '../phone/phone?mobile=' + this.data.mobile, }) }, onUnload: function () { // 取消事件绑定 onfire.un("EventPhoneChange"); } }) [代码] B页面 [代码]<!--phone.wxml--> <view class="phone"> <input class="phone-input" placeholder="手机号码" bindinput="bindinput" value="{{mobile}}"></input> <button class="phone-setting" bindtap="tapSetting">设置</button> </view> [代码] [代码]// phone.js const onfire = require('../../lib/onfire.js') Page({ data: { mobile: '' }, onLoad: function (e) { this.setData({ mobile: e.mobile }) }, tapSetting: function () { let mobile = this.data.mobile; if (!mobile) { wx.showToast({ title: '请填写手机号!', icon: 'none', duration: 2000, }) return; } // 触发名为EventPhoneChange的事件,并且携带变量mobile值。 onfire.fire('EventPhoneChange', mobile) wx.navigateBack() }, bindinput: function (e) { this.setData({ mobile: e.detail.value.trim() }) } }) [代码] 其效果图如下: 图一 [图片] 图二 [图片] 图三 [图片] 相关API 关于onfire.js的API 1.on(event_name, callback) 绑定事件,参数为event_name和callback, 当有名字为event_name的事件发生的时候,callback方法将会被执行。这个方法会返回一个eventObj,这个可以用于使用un(eventObj)方法来取消事件绑定。 2.one(event_name, callback) 绑定(订阅)事件,参数为 event_name with callback. 当被触发一次之后失效。只能被触发一次,一次之后自动失效。 3.fire(event_name, data) 触发名字为event_name的事件,并且赋予变量data为callback方法的输入值。 4.un(eventObj / eventName / function)取消事件绑定。可以仅仅取消绑定一个事件回调方法,也可以直接取消全部的事件; 5.clear() 清空所有事件。 总结 个人对于onfire.js的理解就是一个全局通知,从B页面发出一个通知带上Key和Value,在其他某一个页面监听这个Key值来获取传出来的Value。 其他解决方案 该方案来自摩拜单车小程序 https://juejin.im/post/5da80767f265da5b5f7584dc
2019-12-20 - 在线答题小程序交互设计整理五
在线答题小程序交互设计整理五 ### 本文概述 今天体验的小程序名字是:轻芒答题 [图片] 最近我会体验市面上不同在线答题类小程序,每个小程序仅仅截图,不做分析,每篇文章整理一个小程序,为我的小程序提供设计上的思路 在线答题小程序主要有三个细节: 1. 答题环境 2. 得分环境 3. 错题记录环节 这三部分是文章的重点。 ### 小程序截图 [图片] [图片] [图片] [图片] [图片] [图片] ### 本文总结 待总结
2019-12-24 - 在线答题小程序代码解读
在线答题小程序开发的诱因先来说说我为什么开发在线答题小程序? 九月份的时候,在小程序技术交流的微信群里,有一位朋友扔了一张下面的截图,问大家意见。 [图片] 这个小程序就是在线答题类小程序,这对个人来说,完全可以,没有UGC。 其实,像我们在四五线小城市,每个月三四千的工资已经很好了,而且是被动收入,睡后收入,睡觉的睡。 我当时看到这个信息,很心动,我想每个人都会心动的,当天晚上就在开发者工具上初始化好了项目。 跟很多人一样接下来一段时间,一直没有下文,直到最近,我身边有个在证券公司的朋友,一起吃饭的时候,说起下个周末就要考试了,而且是第二次,不知道能不能通过,很是郁闷,因为在证券公司如果不能通过证券从业资格考试,原则上是不允许转正的。 晚上下班后,我便开始设计数据库结构,题目信息表,科目表(为了扩展其他资格考试),题目类型表(维护单选、多选、判断),答题历史记录表,四张核心表足够了。 接下来我们进入小程序开发正题 小程序采用框架 未采用第三方框架,使用小程序原生框架,未引入任何UI组件库 接口采用PHP YII2框架 小程序实现的功能 ## 目前小程序已经实现的功能有: 选择科目在线答题,答题可以选择单题模式还是列表模式答题结束实时展示分数答题过程,对过题操作进行提示查看分数结束可以查看正确答案答题历史纪录查询,可以查阅当时做题情况也就是说作为一个在线答题系统,基本功能都已闭环。 开发小程序过程中遇到的问题 第一个问题:radio取值问题 在单选选择题的时候,用到以下两个表单组件 [图片] radio-group https://developers.weixin.qq.com/miniprogram/dev/component/radio-group.html radio https://developers.weixin.qq.com/miniprogram/dev/component/radio.html 默认的radio组件事件 wxml文件 {{item.value}} js文件 Page({ data: { items: [ { name: 'USA', value: '美国' }, { name: 'CHN', value: '中国', checked: 'true' }, { name: 'BRA', value: '巴西' }, { name: 'JPN', value: '日本' }, { name: 'ENG', value: '英国' }, { name: 'FRA', value: '法国' }, ] }, radioChange: function (e) { console.log('radio发生change事件,携带value值为:', e.detail.value) } }) 没错,用的就是官方示例代码,我们看到在选择的时候,默认e.detail.value,只能取一个字符串,当时遇到的第一个问题就在这里,如果把这整个选项的信息提取出来,能简单的用{{JSON.stringfy(item)}}吗,当然不可以,因为原生小程序本身不支持。 当时在社区查到解决方案具体可以参考 [单选框radio除了可以传value可以传其他的值吗?] https://developers.weixin.qq.com/community/develop/article/doc/0006ce9771c528ed7b89a6f485bc13 方案就是引入wxs,之前看官方文档,每次到这里,因为不知道这是干什么的,以及解决什么问题,现在明白了,想了解更多关于wxs的内容,也请移步下面两篇文档 [微信小程序wxs有什么用?] https://developers.weixin.qq.com/community/develop/article/doc/0008888a01c69872b689448a051013 [小程序里面精度计算问题] https://developers.weixin.qq.com/community/develop/article/doc/0000ae30ea4da802b989540175b013 第二个问题:每次10道题目是如何选择的 在答题的时候,每次会展示10个题目,这10个题目是从当前科目题库中,随机抽取10个,在题库足够大的情况下,基本可以保证每次进来答题的10个题目跟前面的答题都是不一样的。 小程序截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 代码路径 前端小程序代码,请移步下面 https://gitee.com/xiaofeiyang3369/myexamapp 后端接口用的PHP,代码链接如下,由于我几个小程序都用这个PHP服务,项目代码要远比该小程序的PHP代码要多一些。 https://gitee.com/xiaofeiyang3369/phpapp 如果大家细心,数据库也是可以在线登录的,如果遇到问题,可以微信我。 适用人群 该开源代码适用于小程序初学者,以及大学做在线答题小程序的毕业设计时可以参考。 扫码体验 微信小程序搜索 从业资格题库或者直接扫码 [图片]
2020-06-08 - 小程序如何发送永久模板消息
大家都很清楚模板消息的使命即将结束,具体可查看下文 10月12日,微信在小程序模板消息能力方面公布了一项重大调整。原有的模板消息将升级为「订阅消息」,支持一次性和长期性订阅消息。而模板消息将于2020年1月10日下线。 查看该通知详情请移步 https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401 [图片] 但是模板消息存在的一次表单只能发一次模板消息的限制,在订阅消息方案中并没有解决,那么有没有一种可永久推送消息的实现方案呢,便是本文接下来要讲的内容 关于模板消息转成订阅消息的各种坑,在下面帖子中很详细 https://developers.weixin.qq.com/community/develop/doc/0006088c2940586de249dffbb5b400 在聊具体方案之前,先看看群里讨论的几张截图 [图片] [图片] [图片] [图片] 该方案在微信记账本中实现,微信记账本是腾讯官方推出的一个用于同步账单的小程序 [图片] 可永久推送模板消息方案是: 通过小程序和订阅号绑定,消息通过公众号的模板消息推送发出,只要用户同意推送,我们就可以无限制的发送模板消息。 [图片] [图片] 关于公众号发送模板消息的文档如下所示 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#5 该方案的弊端就是,在使用小程序的过程中,必须同时关注绑定的订阅号,在小程序用户原大于同主体订阅号的情况下,该方案慎用。 但是为了每天能收到推送我们未尝不可以多做一步,关注下订阅号。
2019-11-28 - 如何打开线上版本小程序的调试模式
生产版本的小程序如果出现问题,可以调试一下正式版看看,调试方式如下: 方式1、https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.setEnableDebug.html [代码]wx.setEnableDebug(Object object) 基础库 1.4.0 开始支持,低版本需做兼容处理。 设置是否打开调试开关。此开关对正式版也能生效。 参数 Object object 属性 类型 默认值 必填 说明 enableDebug boolean 是 是否打开调试 success function 否 接口调用成功的回调函数 fail function 否 接口调用失败的回调函数 complete function 否 接口调用结束的回调函数(调用成功、失败都会执行) 示例代码 // 打开调试 wx.setEnableDebug({ enableDebug: true }) // 关闭调试 wx.setEnableDebug({ enableDebug: false }) Tips [代码] 方式2、先在开发版或体验版打开调试,再切到正式版就能看到vConsole
2019-11-29