- 微信小程序开发之富文本编辑器
微信小程序开发之富文本编辑器 一年多去了,还有这么多人关注这个编辑器,那就索性把这个组件放上去,各位直接引用吧!如果您感觉很好用,很实用,也请大家给点一个赞!前言:富文本在Web开发上的地位大家可想而知,很多地方都需要用到富文本编辑器,比如开发类似新闻管理小程序、商品简介等。微信小程序在基础库2.7.0之后上线了一个editor富文本编辑器组件,这个组件是本次要讲的内容。组件相关的内容大家可以去看官方文档的内容,这里我们就不进行讲解。而我们要做的就是将官方的富文本组件进行二次开发达到一个好用而又实用的地步:https://developers.weixin.qq.com/miniprogram/dev/component/editor.html 先看效果图(以下只是一个基础的实用): [图片] 代码方案: 1.引入组件(组件的下载地址链接:https://pan.baidu.com/s/15D3ejvs30BZPwn94RgyNmw 提取码:hg66) 2、在你需要的使用的页面的JSON文件中引入该组件,引入方法如下: "usingComponents": { "hg-editor":"../../../components/hg-editor/hg-editor(根据自己的放置位置修改,其中/hg-editor/hg-editor是固定的)" } 3、在wxml文件中使用,使用案例如下,可选参数有四个 参数详解: [图片] showTabBar :是否显示工具栏(默认为true,显示,如果改为false则为不显示)placeholder:文本框提示文字,默认为“请输入相关内容”name:是编辑器的name属性,默认为空uploadImageURL:图片的上传地址,默认为空使用属性案例测试: bind:input可以获得用户输入的内容: onInputtingDesc: function (e) { let html = e.detail.html; //相关的html代码 let originText = e.detail.text; //text,不含有任何的html标签 this.setData({ ['topic.text']: html, ['topic.originText']: originText }); } 使用案例: [图片][图片][图片] 您的想法有多大,组件拓展的无限可能就有多大,欢迎各位留言,欢迎各位使用! 好用,就来收藏一下,更新不易,点个赞!
2022-04-22 - weapp-qrcode-canvas-2d在微信小程序中生成二维码,新版canvas-2d接口
weapp-qrcode-canvas-2d weapp-qrcode-canvas-2d 是使用新版canvas-2d接口在微信小程序中生成二维码(外部二维码)的js包。canvas 2d 接口支持同层渲染且性能更佳,建议切换使用,可大幅提升生成图片的速度。 仓库地址 weapp-qrcode-canvas-2d【码云gitee】 weapp-qrcode-canvas-2d【github】 [图片] 测试环境 微信小程序基础库版本:2.10.4 开发者工具版本:Stable 1.03.2101150 Usage 先在 wxml 文件中,创建绘制的 [代码]canvas[代码],并定义好 [代码]width[代码], [代码]height[代码], [代码]id[代码] , [代码]type[代码] ,其中type的值必须为[代码]2d[代码] [代码]<canvas type="2d" style="width: 260px; height: 260px;" id="myQrcode"></canvas> [代码] 安装方法1:直接引入 js 文件 直接引入 js 文件,使用 [代码]drawQrcode()[代码] 绘制二维码 [代码]// 将 dist 目录下,weapp.qrcode.esm.js 复制到项目中。路径根据实际引用的页面路径自行改变 import drawQrcode from '../../utils/weapp.qrcode.esm.js' [代码] 安装方法2:npm安装 [代码]npm install weapp-qrcode-canvas-2d --save [代码] // 然后需要在小程序开发者工具中:构建npm [代码]import drawQrcode from 'weapp-qrcode-canvas-2d' [代码] 安装完成后调用 例子1:没有使用叠加图片 [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node // 调用方法drawQrcode生成二维码 drawQrcode({ canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, background: '#ffffff', foreground: '#000000', text: 'abc', }) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ canvasId: 'myQrcode', canvas: canvas, x: 0, y: 0, width: 260, height: 260, destWidth: 260, destHeight: 260, success(res) { console.log('二维码临时路径:', res.tempFilePath) }, fail(res) { console.error(res) } }) }) [代码] 例子2:使用叠加图片(在二维码中加logo) [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node var img = canvas.createImage(); img.src = "/image/logo.png" img.onload = function () { // img.onload完成后才能调用 drawQrcode方法 var options = { canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, paddingColor: '#fff', background: '#fff', foreground: '#000000', text: '123456789', image: { imageResource: img, width: 80, // 建议不要设置过大,以免影响扫码 height: 80, // 建议不要设置过大,以免影响扫码 round: true // Logo图片是否为圆形 } } drawQrcode(options) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ x: 0, y: 0, width: 260, height: 260, destWidth: 600, destHeight: 600, canvasId: 'myQrcode', canvas: canvas, success(res) { console.log('二维码临时路径为:', res.tempFilePath) }, fail(res) { console.error(res) } }) }; }) [代码] API drawQrcode([options]) options Type: Object 参数 必须 说明 示例 canvas 必须 画布标识,传入 canvas 组件实例 canvasId 非 绘制的[代码]canvasId[代码] [代码]'myQrcode'[代码] text 必须 二维码内容 ‘123456789’ width 非 二维码宽度,与[代码]canvas[代码]的[代码]width[代码]保持一致 260 padding 非 空白内边距 20 paddingColor 非 内边距颜色 默认与background一致 background 非 二维码背景颜色,默认值白色 [代码]'#ffffff'[代码] foreground 非 二维码前景色,默认值黑色 [代码]'#000000'[代码] typeNumber 非 二维码的计算模式,默认值-1 8 correctLevel 非 二维码纠错级别,默认值为高级,取值:[代码]{ L: 1, M: 0, Q: 3, H: 2 }[代码] 1 image 非 在 canvas 上绘制图片,层级高于二维码,v1.1.1+版本支持。具体使用见:例子2 [代码]{imageResource: '', width:80, height: 80, round: true}[代码]
2023-04-02 - 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 一套代码发布多个微信小程序的实践
之前接手了公司的一个微信小程序项目,上线了一段时间之后,需要以这个项目为基础再发布多个小程序。这些小程序的内容基本上都是一样的,只不过它们有不同的名称、主题、图标等等;或者,某几个小程序需要加一些定制化页面,功能等。本文主要记录下我从纯手工复制项目进化到使用命令行工具复制项目的实践过程。这个简单的命令行工具简单粗暴地叫做 quickcopy,文档在这里。 我是使用 Taro 2.2.13 开发小程序的,所以这个工具目前也是在这个环境下开发的。除了 Taro 插件功能 以外,2.x 都可以使用这个工具。 开发工具前 defineConstants 一开始,因为项目也不多,所以我就直接纯手工操作了。比如,我们已经有了一个小程序叫做 小程序A,现在我们需要以这个小程序为基础复制出一个新的小程序,并且在打包之后实现以下 3 个简单的需求: 设置 [代码]config.window.navigationBarTitleText[代码],在 navigation bar 显示各自的小程序名称; 设置 [代码]config.tabBar.selectedColor[代码],在 tabBar 选中时显示不同的颜色; 为新的小程序定制 [代码]config.tabBar[代码] 图标,小程序A 则继续使用原来的图标。 首先,我们使用全局常量来改造 app.jsx 中的 [代码]config[代码]: [代码]// app.jsx config = { tabBar: { // 改造前: selectedColor: '#000' selectedColor: __MAIN_COLOR, list: [ { // 改造前: 'assets/icons/tabbar-home-s.png' selectedIconPath: 'assets/' + __ICON_DIR + '/tabbar-home-s.png' } ] }, window: { // 改造前: navigaionBarTitleText: '小程序A' navigationBarTitleText: __APP_NAME } } [代码] 然后,在 config 目录下分别为这两个小程序创建 Taro 编译配置文件 build-configA.js 和 build-configB.js,写入 [代码]defineConstants[代码]: [代码]// build-configA.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#000'), __ICON_DIR: JSON.stringify('icons') } } // build-configB.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序B'), __MAIN_COLOR: JSON.stringify('#111'), __ICON_DIR: JSON.stringify('icons-b') } } [代码] 最后,编译打包 小程序A 的时候,我需要在 config/index.js 的最后将 build-configA.js 与基础的编译配置 [代码]merge[代码]。当编译打包 小程序B 的时候也是一样。 [代码]module.exports = function(merge) { return merge({}, config, require('./dev'), require('./build-configA.js')) } [代码] 运行这两个小程序,我们就可以看到它们会显示各自的名称与主题色,小程序B 还会显示定制化的 tabBar 图标。 sass.resource 既然在上面的全局常量中我们已经定义了一个主题色 [代码]__MAIN_COLOR[代码],那么,我们肯定也需要为不同的小程序编写不同的主题样式。 首先,在 src/style/themes 目录下分别为两个小程序创建主题样式文件。然后在 build-configA.js 以及 build-configB.js 中进行全局注入: [代码]// build-configA.js sass: { resource: [ 'src/style/themes/themeA.scss', // build-configB.js 中写 src/style/themes/themeB.scss 'src/style/variable.scss', 'src/style/mixins.scss' ] } [代码] 全局注入后也就不需要在样式文件中一次次写 [代码]@import 'xxx.scss'[代码] 了。但在这里需要注意的是,必须完整的列出需要注入的 3 个文件。虽然像 variable.scss 和 mixins.scss 这种样式文件明显可以在所有项目共享,但如果只在 config/index.js 中注入,而在 build-configA.js 或者 build-configB.js 中只注入主题样式文件的话,是行不通的。 [代码]// build-configA.js sass: { resource: ['src/style/themes/themeA.scss'] } // config/index.js sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } // 以上两个配置 `merge` 之后的结果是 sass: { resource: [ 'src/style/themes/themeA.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } [代码] 也就是说,对于数组来说,是按索引位置进行 [代码]merge[代码] 的。 到现在为止,我们实现了为不同的小程序配置不同的名称,icon 以及主题样式。但是本着能偷懒就偷懒的原则,我觉得这些步骤已经有点麻烦了,可以想象下,如果又有新的项目需要发布,我们需要手动做这些事情: 在 config 目录下创建项目的编译配置文件 config-project.js; 如果需要,为新项目建立定制化的 icons 目录; 为新项目创建主题样式文件,并在 config-project.js 中全局注入; 在 config-project.js 编写 [代码]defineConstants[代码],写入不同项目间有差异的常量,其他的常量则写入 config/index.js; 在 config/index.js 合并新项目的编译配置; 目前所有的项目都共享了根目录下的 project.config.json,所以在编译前需要修改 [代码]appid[代码]。 如果哪一天这些项目都需要进行更新,可以想象下: 首先修改 config/index.js 中需要 [代码]merge[代码] 的项目配置路径; 然后修改 project.config.json 中的 [代码]appid[代码]; 最后编译打包; 如此循环; 那么,上面这些步骤是不是可以交给程序来完成呢?为了尽可能偷懒,我就写了一个简单的命令行工具。它可以代替我们完成以下事情: 以 config/index.js 为模版,提取部分编译配置,创建并写入到新项目的 Taro 编译配置文件; 以根目录 project.config.json 为模版,创建新项目的小程序项目配置文件; 创建新项目的主题样式文件,并在编译配置全局注入; 在打包时寻找新项目有没有定制化图标,如果有,则替换。 开发工具后 假定我们已经有了一份现有项目 小程序A 的编译配置: [代码]// config/index.js const config = { projectName: 'projectA', outputRoot: 'dist', copy: { patterns: [ { from: 'src/components/wxs', to: 'dist/components/wxs' }, // ... ] }, sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss', // ... ], projectDirectory: path.resolve(__dirname, '..') }, defineConstants: { HOST: JSON.stringify('www.baidu.com'), APP_NAME: JSON.stringify('小程序A'), MAIN_COLOR: JSON.stringify('#999'), // ... } } [代码] 在这份编译配置里,指定了项目的输出目录是 dist,全局注入了 variable.scss 和 mixins.scss 文件,并指定了 3 个常量。由于 Taro 不会打包 wxs,所以在 [代码]copy.patterns[代码] 手动将 wxs 复制到了输出目录。 在复制项目之前,我们先对编译配置进行一点改造。在 [代码]defineConstants[代码] 中,我们找到那些不同项目间存在差异的常量,在这里就是 [代码]APP_NAME[代码] 和 [代码]MAIN_COLOR[代码],添加双下划线 [代码]__[代码] 作为开头,这样工具就知道这些常量是存在差异的,而剩余的常量在所有项目中都是一样的。然后在 variable.scss 中找到那些与主题有关的变量,这些变量随后需要写入项目各自的主题样式文件中。 对于已存在的项目 projectA,我们最好也进行一次复制操作。这样一来它就可以拥有独立的编译配置,而 config/index.js 不仅会作为一份基础的编译配置被所有项目共享,也会作为创建新项目独立编译配置时的一份模版。 复制项目 以分离已有的 projectA 项目为例(复制新项目也是类似的),在根目录执行: [代码]qc copy projectA wx123456789a [代码] 工具可以代替我们完成这些工作: 创建 Taro 编译配置文件,路径为 config/config-projectA/index.js; 以根目录 project.config.json 为模版创建微信小程序项目配置文件,路径为 config/config-prjectA/project.config.json; [代码]{ "miniprogramRoot": "dist-projectA/", "projectname": "projectA", "appid": "wx123456789a" } [代码] 其余的内容则会与根目录下的 project.config.json 保持一致; 以 src/style、src/styles 以及 src/css 为顺序查找是否存在这些样式目录。如果存在,则在对应目录下创建 themes/projectA.scss 主题样式文件;如果以上几个目录都不存在,则默认在 src/style 下创建。具体的样式则需要手动写入; 从 config/index.js 找到需要全局注入的样式文件,即 [代码]sass.resource[代码],与上一步创建的主题样式文件一同注入到 config/config-projectA/index.js: [代码]sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss', ] } [代码] 主题样式文件会放在第一位,以便 variable.scss 和 mixins.scss 可以依赖主题样式。 从 config/index.js 找到需要复制到输出目录的文件,即 [代码]copy.patterns[代码],修改 [代码]to[代码] 指定的路径; [代码]copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] } [代码] 从 config/index.js 中找到不同项目间具有差异的常量,即 [代码]defineConstants[代码] 中 [代码]__[代码] 开头的常量,并自动添加一个名为 [代码]__PROJECT[代码] 的新常量; [代码]defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('projectA') } [代码] 所以最终的 config/config-projectA/index.js 就像这样: [代码]module.exports = { projectName: 'projectA', outputRoot: 'dist-projectA', defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('prjectA') }, copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] }, sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss' ] } } [代码] 至于上文的 icon 问题,因为 Taro 提供了插件能力,所以我们不再需要像上文一样引入 [代码]__ICON_DIR[代码] 常量并改造 [代码]selectedIconPath[代码]。只需要在 config/index.js 的 [代码]plugins[代码] 中添加 [代码]quickcopy/plugin-copy-assets[代码] 即可。 举个例子,我们原本将 icon 放在 src/assets/icons 目录下,如果我们想为 projectA 指定定制化的 [代码]tabBar.list.selectedIconPath[代码],只需要新建一个名为 src/assets/icons-projectA 的目录,在这个目录下存放 projectA 定制化的 icon 即可。 当打包 projectA 的时候,这个插件会去 assets/icons-projectA 查找是否存在定制化的 icon,如果存在,则使用这个 icon,如果不存在,则使用 assets/icons 中默认的 icon。 其他的 icon 也是同样的道理。 编译前准备 当我们需要编译 projectA 的时候,在根目录执行: [代码]qc prep projectA [代码] 工具会做以下两件事情: 创建 config/build.export.js 文件,并将 config/config-projectA/index.js 导出; [代码]const buildConfig = require('./config-projectA/index') module.exports = buildConfig [代码] 将 config/config-projectA/project.config.json 复制到根目录。 我们只需要在 config/index.js 的最后 [代码]merge[代码] build.export.js,随后在根目录执行 Taro 编译指令。 如何添加定制化页面 也许在未来的有一天,我们接到一个需求,需要为 小程序A 添加一个定制化的页面。我们将这个页面路径添加到 app.jsx 的 [代码]config[代码],但又不希望其他小程序打包的时候把这个页面也打包进去。 一开始我使用的方法简单粗暴:在打包其他小程序的时候把这个页面路径注释起来,在打包 小程序A 的时候再把注释打开。 我们可以借助 babel-plugin-preval(在 Taro 文档中也有提到)以及上文的 [代码]__PROJECT[代码] 常量编写逻辑,来确定哪个项目需要打包定制化页面,哪些项目又不需要打包。 首先,把 [代码]config.pages[代码] 提取出来作为一个独立文件,比如: [代码]// pages.js module.exports = function(project) { const pages = [ 'pages/tabBar/home/index', 'pages/tabBar/profile/index' ] if (project == 'projectA') { pages.push('pages/special/index') } return pages } [代码] 然后改造 app.jsx: [代码]const project = __PROJECT class App extends Component { config = { // 这里使用了 project 而没有直接传入 __PROJECT 是因为我在测试的时候发现直接使用 __PROJECT 编译的时候会报错 pages: preval.require('./pages.js', project) } } [代码] 这样一来我们只需要修改 pages.js 就可以添加定制化页面,不仅避免被不需要的项目打包,也能清楚地看出哪些项目有定制化页面哪些没有。对于 [代码]subpackages[代码] 和 [代码]tabBar.list[代码] 也可以做同样的处理。 最后 这个工具到目前为止是根据公司的业务需求开发的,主要功能也并不多,还是有挺大的局限。我也还在探索如何更方便地打包为不同项目编写的定制化页面,所以这个工具还会继续更新下去。
2020-11-18 - 小程序管理蓝牙设备开发指北
小程序管理蓝牙设备开发记录 前段时间接到一个管理蓝牙设备的需求,要求能搜索并连接指定设备,并读取设备的信息,然后发送指令给设备,让设备运行起来。 允许连接多台同类型的设备,并对设备做分开管理 期间,遇到不少的坑,在此记录下来,希望能对大家有所帮助,有欠缺的地方,还请大家帮忙指正一下,谢谢 话不多说,接下来就进入开发: 1. 初始化 考虑到需要在不同页面都要使用微信的蓝牙接口,并且还需要一些数据的互通,所以我建了一个单例,用于处理微信接口,和设备状态、信息管理 [代码] constructor(config = {}) { if (!manager.instance) { manager.instance = this; this.connectPool = []; this.cachePool = []; this.discoveryPool = []; this.timeout = 5000; this._timer = null; this.adpterStatus = {open: false} } Object.assign(manager.instance, config); if (!this.adpterStatus.open) { this.initBluetoothAdapter() } return manager.instance; } [代码] [代码]connectPool[代码] 设备连接池,用于存储正在连接的设备 <br> [代码]cachePool[代码] 设备缓存池,用于存储连接过的设备 <br> [代码]discoveryPool[代码] 设备发现池,用于存储扫描到的设备 <br> [代码]timeout[代码] 超时时间 <br> [代码]adpterStatus[代码] 蓝牙适配器状态 [代码]{open: '是否打开', available: '是否可用', discovering: '是否正在搜索设备'}[代码] 若适配器打开状态为[代码]false[代码]那么初始化适配器[代码]initBluetoothAdapter[代码]: [代码] /** * 为了方便处理微信的回调,建了一个公共的callBack方法 */ commonCall(success = ()=>{}, fail = ()=>{}, complete = ()=>{}) { return {success, fail, complete} } initBluetoothAdapter() { const that = this; /** * 监听适配器状态,开启监听之前先关闭监听,防止状态重复 * offBluetoothAdapterStateChange 关闭适配器状态监听 * onBluetoothAdapterStateChange 开启适配器状态监听 */ wx.offBluetoothAdapterStateChange(); wx.onBluetoothAdapterStateChange(res => { // 同步适配器状态,TODO做manager工具内的监听,可以参考下一步搜索状态的监听 3. 发现设备 中的 discoveryPoolDidUpdate 方法 Object.assign(that.adpterStatus, res) // TODO 若适配器重新可获取时,重新开启适配器 // 若适配器open = true,开始 -> 2. 设置监听 that.setListener() }); /** * 开启小程序蓝牙适配器,开启之前先关闭,防止状态重复 * closeBluetoothAdapter 关闭蓝牙适配器 * openBluetoothAdapter 开启蓝牙适配器 */ wx.closeBluetoothAdapter(); wx.openBluetoothAdapter(that.commonCall(success => { // 蓝牙适配器初始化成功 })); } [代码] 2. 设置监听 适配器初始化完成后,设置监听,统一处理数据和状态: [代码]onBluetoothDeviceFound[代码] 蓝牙搜索监听 [代码]onBLEConnectionStateChange[代码] 蓝牙设备连接状态监听,并在连接成功的时候 [代码]onBLECharacteristicValueChange[代码] 蓝牙设备特征值变化监听,用户小程序和蓝牙的交互 [代码] setListener() { wx.offBluetoothDeviceFound() wx.onBluetoothDeviceFound(res => { // 设备搜索监听,更新设备,详情请移步 -> 3. 发现设备 }) wx.offBLEConnectionStateChange() wx.onBLEConnectionStateChange(res => { /** * res = { * errorCode: 0 成功 * errorMsg: 错误信息 * connected: 0 断开连接,1 连接成功 * deviceId:连接设备的deivceId * } * * 设备连接状态更新,若连接成功,则开始针对设备进行数据监听,详情请移步 -> 设备交互 */ }) wx.offBLECharacteristicValueChange() wx.onBLECharacteristicValueChange(res => { // 设备特征值发生变化,更新设备数据,详情请移步 -> 设备交互 }) } [代码] 3. 发现设备 [代码] /** * services: 可以通过设备是否具备特定的服务UUID来筛选自己想要的设备 * sCall: 扫描方法调用成功 * fCall:扫描方法调用失败 */ discoveryBluetoothDevices(services, sCall, fCall) { const that = this; // 扫描前清空discoveryPool that.discoveryPool = []; const discoveryCall = that.commonCall(sCall, fCall); discoveryCall.services = services; wx.stopBluetoothDevicesDiscovery(that.commonCall(__=>__, __=>__, () => { wx.startBluetoothDevicesDiscovery(discoveryCall) })) } [代码] 若设备有新设备,则‘设置监听’中的[代码]onBluetoothDeviceFound[代码]会进行新设备上报 调用[代码]updateDevice[代码]方法进行设备过滤和保存 [代码] updateDevice(device) { if (!device) return; if (!device.name) return; if (!device.advertisData) return; const that = this; // 判断设备是否是新设备 if (that.discoveryPool.map(v => v.deviceId).indexOf(device.deviceId) === -1) { // ab2str 见下方备注 device.advertisData = ab2str(device.advertisData) that.discoveryPool.push(device) // 给单例添加提供给外部监听状态的接口,当设备有更新的时候,触发接口回调 that.discoveryPoolDidUpdate && that.discoveryPoolDidUpdate instanceof Function && that.discoveryPoolDidUpdate(that.discoveryPool) // 私有回调 -> 4.连接设备 that._discoveryPoolDidUpdate && that._discoveryPoolDidUpdate instanceof Function && that._discoveryPoolDidUpdate(device) that._timer && clearTimeout(that._timer) } // 超时时间,超时后若无新设备,则关闭Discovery方法 that._timer = setTimeout(() => { wx.stopBluetoothDevicesDiscovery() that._timer && clearTimeout(that._timer) }, that.timeout) } [代码] [代码]备注:[代码]设备广播数据是[代码]ArrayBuffer[代码]的形式,所以通过[代码]ab2str[代码]的方法进行转换,方法见下: [代码] ab2str(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); } [代码] 4. 连接设备 小程序连接设备的接口[代码]wx.createBLEConnection()[代码]接受的值是[代码]deviceId[代码],作为设备的标识符。 但是这个设备Id在iOS和Android手机上,同一设备的值是不同的,所以如果我们要把deviceId保存给后台服务器,下次再拿到,不一定可以直接使用。 所以为了通用性,我们应该跟设备制造商统一一下,让设备广播出自己的特征码,亦或者直接通过[代码]服务UUID[代码]获取到设备的mac地址,用这些不变且唯一的字符串作为保存到后端的设备标识符。 如果以广播中的特征码为唯一标识符,搜索设备并向后台保存的过程中,无需跟设备进行连接操作,本文以这样的方式进行; 如果通过服务UUID获取到设备的mac,保存给后端,需要扫描到设备之后,连接设备,并通过获取设备mac地址的服务UUID,读取到设备的mac,保存到后台。 [代码] connectBluetoothDevice(indentify, sCall, fCall) { const that = this; const connectCall = that.commonCall(sCall, fCall); if (that.adpterStatus.discovering) { for (let i=0; i<that.discoveryPool.length; i++) { if (that.discoveryPool[i].advertisData == indentify) { connectCall.deviceId = that.discoveryPool[i].deviceId connectCall.timeout = that.timeout wx.createBLEConnection(connectCall) } } } else { that.discoveryPoolDidUpdate = null that.discoveryBluetoothDevices([], s => { // 扫描到设备之后,用广播数据进行比对,若一样,获取该设备的deviceId,并连接 that._discoveryPoolDidUpdate = res => { if (res.advertisData == indentify) { connectCall.deviceId = res.deviceId wx.createBLEConnection(connectCall) wx.stopBluetoothDevicesDiscovery() } else { // 检查是否已经停止扫描 console.log('_discoveryPoolDidUpdate', that.adpterStatus) fCall && fCall instanceof Function && fCall("device not found !") } } }) } } [代码] 5. 设备交互 设备交互有三种形式: [代码]read[代码] 程序读取设备的信息 [代码]write[代码] 向设备发指令 [代码]notify[代码] 订阅设备的上报 蓝牙设备出厂的时候,就设置了一些接口,并定义好访问它的服务ID和特征值ID以及访问方式,通过这些可以跟设备做到交互 用前端跟后端交互的方式理解,跟设备进行交互的时候,服务ID和特征值ID 就相当于我们访问接口的api接口,[代码]read[代码]相当于get接口,获取到数据,[代码]write[代码]相当于post接口,数据发送给后台,后台就对应数据逻辑做相应变更,[代码]notify[代码]相当于与服务器建立websocket连接,实时获取服务器发来的数据(单方向) 由于[代码]服务ID[代码]和[代码]特征值ID[代码]都是这样[代码]00002A23-0000-1000-8000-008BF9B054F3[代码]难以记住的串,所以我们建立一个[代码]服务适配器(services-adpter)[代码],它负责配置我们需要用到的服务,如下 [代码] export const serviceAdapter = [ { // 开始设备 serviceName: 'start', serviceUUID: '服务ID', characterUUID: '特征值ID', inFormatter: '入参格式化方法', outFormatter: '出参格式化方法', type: 'write' } ] [代码] 我们传给设备的数据需要转换[代码]二进制数据[代码]和[代码]异或[代码]操作,所以在这里进行配置入参格式化方法和出参格式化方法 接下来就是交互,在设备连接上之后,处理serviceAdapter中的type = read 和 type = notify的任务 处理serviceAdapter任务的顺序为 :处理read任务,对设备进行属性的初始化 -> 处理notify任务,对设备属性进行监听,并设置callBack -> write任务需要主动触发 [代码] // 处理read任务,对设备进行属性的初始化 const readServices = that.getServicesBy('read') readServices.forEach(rs => { console.log('will start read servce:', rs); const call = that.commonCall(success => { console.log('readBLECharacteristicValue success:', success) }) call.deviceId = device.deviceId call.serviceId = rs.serviceUUID call.characteristicId = rs.characterUUID wx.readBLECharacteristicValue(call) }) // 处理notify任务,对设备属性进行监听,并设置callBack const notifyServices = that.getServicesBy('notify') notifyServices.forEach(ns => { console.log('will start notify servce:', ns); const call = that.commonCall(success => { console.log('notifyBLECharacteristicValueChange success:', success) }) call.deviceId = device.deviceId call.serviceId = ns.serviceUUID call.characteristicId = ns.characterUUID wx.notifyBLECharacteristicValueChange(call) }) [代码] write方式,需要用户主动触发 [代码] beginService(indentify, serviceName, params, sCall, fCall) { const that = this for (let i=0; i<serviceAdapter.length; i++) { let adapter = serviceAdapter[i] if (adapter.serviceName == serviceName) { if (adapter.inFormatter && adapter.inFormatter instanceof Function) { const device = this.deviceBy(indentify, 'connect') const call = that.commonCall(sCall, fCall) call.deviceId = device.deviceId call.serviceId = adapter.serviceUUID call.characteristicId = adapter.characterUUID call.value = adapter.inFormatter(params) wx.writeBLECharacteristicValue(call) } return } } } [代码] indentify 设备的唯一标志符 <br> serviceName 需要访问的服务名称 <br> params 发送给设备的数据 <br> 发送数据给用户,并在监听中获取最新的设备信息和状态 自此我们就初步的完成了设备搜索到连接到交互的过程 6. TODOS 为了让工具更加完善,需要增加错误处理,异常抛出,错误重试等操作,这里就不在此赘述了
2021-07-30 - 关于蓝牙打印中文乱码
1、将要打印的内容转换为打印机对应的编码(一般是gbk),可以使用这个库来转: https://github.com/inexorabletash/text-encoding 示例代码如下: [代码]const encoding = require([代码][代码]'../../utils/encoding.js'[代码][代码])[代码][代码]let str = [代码][代码]"你要打印的内容"[代码][代码]let buffer = [代码][代码]new[代码] [代码]encoding.TextEncoder([代码][代码]"gb2312"[代码][代码], { NONSTANDARD_allowLegacyEncoding: [代码][代码]true[代码] [代码]}).encode(str).buffer[代码][代码]wx.writeBLECharacteristicValue({[代码][代码] // 这里的 deviceId 需要在上面的 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取[代码][代码] deviceId: deviceId,[代码][代码] // 这里的 serviceId 需要在上面的 getBLEDeviceServices 接口中获取[代码][代码] serviceId: serviceId,[代码][代码] // 这里的 characteristicId 需要在上面的 getBLEDeviceCharacteristics 接口中获取[代码][代码] characteristicId: characteristicId,[代码][代码] // 这里的value是ArrayBuffer类型[代码][代码] value: buffer,[代码][代码] success: [代码][代码]function[代码] [代码](res) {[代码][代码] console.log([代码][代码]'writeBLECharacteristicValue success'[代码][代码], res)[代码][代码] },[代码][代码] fail: [代码][代码]function[代码] [代码](err) {[代码][代码] console.log([代码][代码]'writeBLECharacteristicValue fail'[代码][代码], err)[代码][代码] }[代码][代码]})[代码] 2、一般来说转完了发现打印中文还会是乱码,这个很有可能是因为你的打印设备需要有对应的打印指令才可以正确打印内容,建议看下对应的编码手册,示例代码如下: [代码]let str = [代码][代码]"! 0 200 200 920 1\n"[代码] [代码]+[代码][代码] [代码][代码]"PAGE-WIDTH 576\n"[代码] [代码]+[代码][代码] [代码][代码]";存根\n"[代码] [代码]+[代码][代码] [代码][代码]";SPEED 0\n"[代码] [代码]+[代码][代码] [代码][代码]";LINE 0 40 544 40 1\n"[代码] [代码]+[代码][代码] [代码][代码]"一维条码\n"[代码] [代码]+[代码][代码] [代码][代码]"SETMAG 0 0\n"[代码] [代码]+[代码][代码] [代码][代码]"SETBOLD 1"[代码][代码] [代码][代码]"ML 32\n"[代码] [代码]+[代码][代码] [代码][代码]"TEXT 24 0 0 420 \n"[代码] [代码]+[代码][代码] [代码][代码]" 上述时间、地点,该机动车未在停车场或道路停车\n"[代码] [代码]+[代码][代码] [代码][代码]"泊位内停放,根据《阳天省实施(中华人民共和国道路\n"[代码] [代码]+[代码][代码] [代码][代码]"交通安全法)办法》第五十二条第一款的规定,对以上\n"[代码] [代码]+[代码][代码] [代码][代码]"事实已作出了图像记录。此告知单及图像记录,将提供\n"[代码] [代码]+[代码][代码] [代码][代码]"给阳天市公安局阳天交通大队审核。\n"[代码] [代码]+[代码][代码] [代码][代码]"ENDML\n"[代码] [代码]+[代码][代码] [代码][代码]"EXT 24 0 0 612 交通警察:\n"[代码] [代码]+[代码][代码] [代码][代码]"TEXT 24 0 48 644 2016年8月1日\n"[代码] [代码]+[代码][代码] [代码][代码]"LEFT\n"[代码] [代码]+[代码][代码] [代码][代码]"B QR 64 708 M 2 U 4 \n"[代码] [代码]+[代码][代码] [代码][代码]"MA,TO: http://a.app.qq.com/o/simple.jsp?pkgname=com.zicox.easyprint\n"[代码] [代码]+[代码][代码] [代码][代码]"ENDQR\n"[代码] [代码]+[代码][代码] [代码][代码]"B QR 362 708 M 2 U 4 \n"[代码] [代码]+[代码][代码] [代码][代码]"MA,TO: http://a.app.qq.com/o/simple.jsp?pkgname=com.zicox.easyprint\n"[代码] [代码]+[代码][代码] [代码][代码]"ENDQR\n"[代码] [代码]+[代码][代码] [代码][代码]"TEXT 24 0 0 848 关注“阳天公安”“阳天交警”\n"[代码] [代码]+[代码][代码] [代码][代码]"TEXT 24 0 344 848 扫描支付宝支付\n"[代码] [代码]+[代码][代码] [代码][代码]"PRINT \n"[代码]类似上面的str,使用1中所说的转码方式,应该就可以正确打印出中文了。 3、关于Android打印: 前面的代码在ios上面应该已经可以正确打印中文了,但是试了多款Android机型,发现还是无法打印,经过实验发现,Android端对单次打印的字节数有限制(不超过20个字节),官方文档其实也提过这一点: [图片] 所以在打印的时候,由于发送指令不完整,导致不能正确打印出所需要的内容。这时候需要对打印的内容进行分包处理,下面是我的做法: [代码] [代码]let buffer = [代码][代码]new[代码] [代码]encoding.TextEncoder([代码][代码]"gb2312"[代码][代码], { NONSTANDARD_allowLegacyEncoding: [代码][代码]true[代码] [代码]}).encode(str).buffer[代码][代码] [代码][代码] sendData(buffer)[代码][代码] [代码]// //发送数据[代码] [代码] [代码][代码]sendData(buffer) {[代码][代码] [代码][代码]let byteLength = buffer.byteLength[代码][代码] [代码][代码]if[代码][代码](byteLength > 0) {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码] [代码][代码]let deviceId = [代码][代码]this[代码][代码].data.deviceId[代码][代码] [代码][代码]let characteristicId = [代码][代码]this[代码][代码].data.characteristicId[代码][代码] [代码][代码]let serviceId = [代码][代码]this[代码][代码].data.serviceId[代码][代码] [代码][代码]wx.writeBLECharacteristicValue({[代码][代码] [代码][代码]// 这里的 deviceId 需要在上面的 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取[代码][代码] [代码][代码]deviceId: deviceId,[代码][代码] [代码][代码]// 这里的 serviceId 需要在上面的 getBLEDeviceServices 接口中获取[代码][代码] [代码][代码]serviceId: serviceId,[代码][代码] [代码][代码]// 这里的 characteristicId 需要在上面的 getBLEDeviceCharacteristics 接口中获取[代码][代码] [代码][代码]characteristicId: characteristicId,[代码][代码] [代码][代码]// 这里的value是ArrayBuffer类型[代码][代码] [代码][代码]value: buffer.slice(0, 20),[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'writeBLECharacteristicValue success'[代码][代码], res)[代码][代码] [代码][代码]if[代码][代码](byteLength > 20) {[代码][代码] [代码][代码]that.sendData(buffer.slice(20, byteLength))[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码] [代码](err) {[代码][代码] [代码][代码]console.log([代码][代码]'writeBLECharacteristicValue fail'[代码][代码], err)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码] 每次取前20个字节,递归进行打印,就可以了。 以上就是蓝牙中文打印相关经验分享,希望对大家有帮助
2018-05-02 - wx.writeBLECharacteristicValue写入指令问题?
[图片][图片] 向蓝牙送指令// 23 23 4 a2 33 63 其中a2怎么写
2021-03-17 - 小程序蓝牙开发框架
wx-simple-bluetooth 名称:wx-simple-bluetooth 适用平台:微信小程序 蓝牙:低功耗蓝牙 这个项目从蓝牙连接、蓝牙协议通信、状态订阅及通知三个层面进行设计,可以很方便的定制您自己的小程序的蓝牙开发。主要功能如下: !!!需要先开启微信开发工具的增强编译!!! 这个项目从蓝牙连接、蓝牙协议通信、状态订阅及通知三个层面进行设计,可以很方便的定制您自己的小程序的蓝牙开发。主要功能如下: 以下是在手机开启了蓝牙功能、GPS以及微信定位权限的情况下: 执行[代码]getAppBLEManager.connect()[代码]会自动扫描周围的蓝牙设备,每[代码]350ms[代码]扫描一次,在该次内连接信号最强的设备。 可设置扫描周边蓝牙设备时,主 [代码]service[代码] 的 [代码]uuid[代码] 列表,以及对应的用于通信的服务id;还可额外添加蓝牙设备名称来进一步筛选设备。 可订阅蓝牙连接状态更新事件,并同步通知前端。 可订阅获取接收到的蓝牙协议事件。依据您的配置,框架内部会自行处理,并只返回最需要的数据给前端。 [代码]注意:目前在发送数据时大于20包的数据会被裁剪为20包[代码]。 已更新为2.x.x版本! 新的版本包括多种新的特性: 提供了完整的示例及较为详细的注释。 重构了蓝牙连接以及重连的整个流程,使其更加稳定和顺畅,也提高了部分场景下重连的速度。[代码](有些蓝牙连接问题是微信兼容或是手机问题,目前是无法解决的。如错误码10003以及部分华为手机蓝牙连接或重连困难。如果您有很好的解决方案,还请联系我,十分感谢)[代码] 只有在连接到蓝牙设备并使用特征值注册了read、write、notify监听后才算连接成功。 在扫描周围设备时,可按自定义的规则过滤多余设备,连接指定设备。详见示例[代码]lb-example-bluetooth-manager.js[代码]。 新增蓝牙协议配置文件,可以很方便的发送和接收蓝牙协议。 优化了蓝牙状态更新和蓝牙协议更新的订阅方式。现在可以更清晰的区分是蓝牙的状态更新还是接收到了新的协议(以及接收到的协议数据是什么),并且会过滤掉与上一条完全相同的通知。 可随时获取到最新的蓝牙连接状态。 各个业务均高度模块化,在深入了解后,可以很方便的拓展。 微信小程序官方蓝牙的部分说明: 小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。 若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。 wx.writeBLECharacteristicValue并行调用多次会存在写失败的可能性。 所以基于这方面的考虑,本框架有以下约束: 必须按照约定的协议格式来制定协议,才能正常使用该框架。 协议一包数据最多20个字节。该框架不支持大于20个字节的协议格式。如果数据超出限制,建议拆分为多次发送。 建议以串行方式执行写操作。 建议先了解清楚小程序的官方蓝牙文档,方便理解框架的使用。 [代码]协议约定格式:[...命令字之前的数据(非必需), 命令字(必需), ...有效数据(非必需 如控制灯光发送255,255,255), 有效数据之后的数据(非必需 如协议结束标志校、验位等) 协议格式示例:[170(帧头), 10(命令字), 1(灯光开启),255,255,255(三个255,白色灯光),233(协议结束标志,有的协议中没有这一位),18(校验位,我胡乱写的)] 有效数据是什么: 在刚刚的这个示例中,帧头、协议结束标志是固定的值,校验位是按固定算法生成的,这些不是有效数据。而1,255,255,255这四个字节是用于控制蓝牙设备的,属于有效数据。 [代码] 该项目如有帮助,希望在GitHub上给个star! 快速集成示例 使用前必须检查手机是否开启蓝牙功能、定位功能、微信一定要给予定位权限(较新的iOS版微信要给予蓝牙权限) 导入项目下的[代码]modules[代码]文件夹到你的项目 1、在小程序页面中设置事件订阅及蓝牙连接、断开示例 [代码]import Toast from "../../view/toast"; import UI from './ui'; import {ConnectState} from "../../modules/bluetooth/lb-bluetooth-state-example"; import {getAppBLEProtocol} from "../../modules/bluetooth/lb-example-bluetooth-protocol"; import {getAppBLEManager} from "../../modules/bluetooth/lb-example-bluetooth-manager"; const app = getApp(); Page({ /** * 页面的初始数据 */ data: { connectState: ConnectState.UNAVAILABLE }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { this.ui = new UI(this); console.log(app); //监听蓝牙连接状态、订阅蓝牙协议接收事件 //多次订阅只会在最新订阅的函数中生效。 //建议在app.js中订阅,以实现全局的事件通知 getAppBLEManager.setBLEListener({ onConnectStateChanged: async (res) => { const {connectState} = res; console.log('蓝牙连接状态更新', res); this.ui.setState({state: connectState}); switch (connectState) { case ConnectState.CONNECTED: //在连接成功后,紧接着设置灯光颜色和亮度 //发送协议,官方提醒并行调用多次会存在写失败的可能性,所以建议使用串行方式来发送 await getAppBLEProtocol.setColorLightAndBrightness({ brightness: 100, red: 255, green: 0, blue: 0 }); break; default: break; } }, /** * 接收到的蓝牙设备传给手机的有效数据,只包含你最关心的那一部分 * protocolState和value具体的内容是在lb-example-bluetooth-protocol.js中定义的 * * @param protocolState 蓝牙协议状态值,string类型,值是固定的几种,详情示例见: * @param value 传递的数据,对应lb-example-bluetooth-protocol.js中的{effectiveData}字段 */ onReceiveData: ({protocolState, value}) => { console.log('蓝牙协议接收到新的 protocolState:', protocolState, 'value:', value); } }); //这里执行连接后,程序会按照你指定的规则(位于getAppBLEManager中的setFilter中指定的),自动连接到距离手机最近的蓝牙设备 getAppBLEManager.connect(); }, /** * 断开连接 * @param e * @returns {Promise<void>} */ async disconnectDevice(e) { // closeAll() 会断开蓝牙连接、关闭适配器 await getAppBLEManager.closeAll(); this.setData({ device: {} }); setTimeout(Toast.success, 0, '已断开连接'); }, /** * 连接到最近的设备 */ connectHiBreathDevice() { getAppBLEManager.connect(); }, async onUnload() { await getAppBLEManager.closeAll(); }, }); [代码] 2、接下来是如何定制你自己的蓝牙业务: 1. 设置你自己的[代码]setFilter[代码]函数参数。 文件位于[代码]./modules/bluetooth/lb-example-bluetooth-manager.js[代码] [代码] import {LBlueToothManager} from "./lb-ble-common-connection/index"; import {getAppBLEProtocol} from "./lb-example-bluetooth-protocol"; /** * 蓝牙连接方式管理类 * 初始化蓝牙连接时需筛选的设备,重写蓝牙连接规则 */ export const getAppBLEManager = new class extends LBlueToothManager { constructor() { super(); //setFilter详情见父类 super.setFilter({ services: ['0000xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'],//必填 targetServiceArray: [{ serviceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',//必填 writeCharacteristicId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxE',//必填 notifyCharacteristicId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxF',//必填 readCharacteristicId: '',//非必填 }], targetDeviceName: '目标蓝牙设备的广播数据段中的 LocalName 数据段,如:smart-voice',//非必填,在判断时是用String.prototype.includes()函数来处理的,所以targetDeviceName不必是全称 scanInterval: 350//扫描周围设备,重复上报的时间间隔,毫秒制,非必填,默认是350ms }); super.initBLEProtocol({bleProtocol: getAppBLEProtocol}); super.setMyFindTargetDeviceNeedConnectedFun({ /** * 重复上报时的过滤规则,并返回过滤结果 * 在执行完该过滤函数,并且该次连接蓝牙有了最终结果后,才会在下一次上报结果回调时,再次执行该函数。 * 所以如果在一次过滤过程中或是连接蓝牙,耗时时间很长,导致本次连接结果还没得到,就接收到了下一次的上报结果,则会忽略下一次{scanFilterRuler}的执行。 * 如果不指定这个函数,则会使用默认的连接规则 * 默认的连接规则详见 lb-ble-common-connection/utils/device-connection-manager.js的{defaultFindTargetDeviceNeedConnectedFun} * @param devices {*}是wx.onBluetoothDeviceFound(cb)中返回的{devices} * @param targetDeviceName {string}是{setFilter}中的配置项 * @returns {{targetDevice: null}|{targetDevice: *}} 最终返回对象{targetDevice},是数组{devices}其中的一个元素;{targetDevice}可返回null,意思是本次扫描结果未找到指定设备 */ scanFilterRuler: ({devices, targetDeviceName}) => { console.log('执行自定义的扫描过滤规则'); const tempFilterArray = []; for (let device of devices) { if (device.localName?.includes(targetDeviceName)) { tempFilterArray.push(device); } } if (tempFilterArray.length) { const device = tempFilterArray.reduce((pre, cur) => { return pre.RSSI > cur.RSSI ? pre : cur; }); return {targetDevice: device}; } return {targetDevice: null}; } }) } /** * 获取本机蓝牙适配器状态 * @returns {Promise<*>} 返回值见小程序官网 wx.getBluetoothAdapterState */ async getBLEAdapterState() { return await super.getBLEAdapterState(); } /** * 获取最新的蓝牙连接状态 * @returns {*} */ getBLELatestConnectState() { return super.getBLELatestConnectState(); } }(); [代码] 2. 按约定的的蓝牙协议格式组装你自己的收发数据。 文件位于[代码]./modules/bluetooth/lb-ble-example-protocol-body[代码] [代码]//send-body.js import {IBLEProtocolSendBody} from "../lb-ble-common-protocol-body/index"; import {HexTools} from "../lb-ble-common-tool/index"; /** * 组装蓝牙协议发送数据示例 * 该框架的蓝牙协议必须按照约定格式来制定,最多20个字节 */ export default class SendBody extends IBLEProtocolSendBody { getDataBeforeCommandData({command, effectiveData} = {}) { //有效数据前的数据 该示例只返回了帧头110 return [110]; } getDataAfterEffectiveData({command, effectiveData} = {}) { //协议结束标志 const endFlag = 233; //该示例中checkSum的生成规则是计算协议从第0个元素累加到结束标志 let checkSum = endFlag + HexTools.hexToNum(command); for (let item of this.getDataBeforeCommandData()) { checkSum += item; } for (let item of effectiveData) { checkSum += item; } //生成有效数据之后的数据 return [endFlag, checkSum]; } } [代码] [代码]//receive-body.js import {IBLEProtocolReceiveBody} from "../lb-ble-common-protocol-body/index"; /** * 组装蓝牙协议接收数据示例 * 该框架的蓝牙协议必须按照约定格式来制定,最多20个字节 */ export default class ReceiveBody extends IBLEProtocolReceiveBody { constructor() { //commandIndex 命令字位置索引 //effectiveDataStartIndex 有效数据开始索引,比如:填写0,{getEffectiveReceiveDataLength}中返回20,则会在{LBlueToothProtocolOperator}的子类{getReceiveAction}实现中,在参数中返回所有数据 super({commandIndex: 1, effectiveDataStartIndex: 0}); } /** * 获取有效数据的字节长度 * 该长度可根据接收到的数据动态获取或是计算,或是写固定值均可 * 有效数据字节长度是指,在协议中由你的业务规定的具有特定含义的值的总字节长度 * 有效数据更多的说明,以及该长度的计算规则示例,见 IBLEProtocolReceiveBody 类的 {getEffectiveReceiveData}函数 * * @param receiveArray 接收到的一整包数据 * @returns {number} 有效数据的字节长度 */ getEffectiveReceiveDataLength({receiveArray}) { return 20; } } [代码] 3. 实现对有效数据的发送、接收处理 位于[代码]modules/bluetooth/lb-example-bluetooth-protocol.js[代码] [代码]import {LBlueToothProtocolOperator} from "./lb-ble-common-protocol-operator/index"; import SendBody from "./lb-ble-example-protocol-body/send-body"; import ReceiveBody from "./lb-ble-example-protocol-body/receive-body"; import {ProtocolState} from "./lb-bluetooth-state-example"; /** * 蓝牙协议管理类 * 在这个类中,以配置的方式来编写读操作和写操作 * 配置方式见下方示例 */ export const getAppBLEProtocol = new class extends LBlueToothProtocolOperator { constructor() { super({protocolSendBody: new SendBody(), protocolReceiveBody: new ReceiveBody()}); } /** * 写操作(仅示例) */ getSendAction() { return { /** * 0x01:设置灯色(写操作) * @param red 0x00 - 0xff * @param green 0x00 - 0xff * @param blue 0x00 - 0xff * @returns {Promise<void>} */ '0x01': async ({red, green, blue}) => { return await this.sendProtocolData({command: '0x01', effectiveData: [red, green, blue]}); }, /** * 0x02:设置灯亮度(写操作) * @param brightness 灯亮度值 0~100 对应最暗和最亮 * @returns {Promise<void>} */ '0x02': async ({brightness}) => { //data中的数据,填写多少个数据都可以,可以像上面的3位,也可以像这条6位。你只要能保证data的数据再加上你其他的数据,数组总长度别超过20个就行。 return await this.sendProtocolData({command: '0x02', effectiveData: [brightness, 255, 255, 255, 255, 255]}); }, } } /** * 读操作(仅示例) * {dataArray}是一个数组,包含了您要接收的有效数据。 * {dataArray}的内容是在lb-ble-example-protocol-body.js中的配置的。 * 是由您配置的 dataStartIndex 和 getEffectiveReceiveDataLength 共同决定的 */ getReceiveAction() { return { /** * 获取设备当前的灯色(读) * 可return蓝牙协议状态protocolState和接收到的数据effectiveData, * 该方法的返回值,只要拥有非空的protocolState,该框架便会同步地通知前端同protocolState类型的消息 * 当然是在你订阅了setBLEListener({onReceiveData})时才会在订阅的地方接收到消息。 */ '0x10': ({dataArray}) => { const [red, green, blue] = dataArray; return {protocolState: ProtocolState.RECEIVE_COLOR, effectiveData: {red, green, blue}}; }, /** * 获取设备当前的灯亮度(读) */ '0x11': ({dataArray}) => { const [brightness] = dataArray; return {protocolState: ProtocolState.RECEIVE_BRIGHTNESS, effectiveData: {brightness}}; }, /** * 接收到设备主动发送的灯光关闭消息 * 模拟的场景是,用户关闭了设备灯光,设备需要主动推送灯光关闭事件给手机 */ '0x12': () => { //你可以不传递effectiveData return {protocolState: ProtocolState.RECEIVE_LIGHT_CLOSE}; }, /** * 接收到蓝牙设备的其他一些数据 */ '0x13': ({dataArray}) => { //do something //你可以不返回任何值 } }; } /** * 设置灯亮度和颜色 * @param brightness * @param red * @param green * @param blue * @returns {Promise<[unknown, unknown]>} */ async setColorLightAndBrightness({brightness, red, green, blue}) { //发送协议,小程序官方提醒并行调用多次会存在写失败的可能性,所以建议使用串行方式来发送,哪种方式由你权衡 //但我这里是并行发送了两条0x01和0x02两条协议,仅演示用 return Promise.all([this.sendAction['0x01']({red, green, blue}), this.sendAction['0x02']({brightness})]); } }(); [代码] 4.(非必须)拓展蓝牙连接和协议状态。 文件位于[代码]modules/bluetooth/lb-bluetooth-state-example.js[代码] [代码]import {CommonConnectState, CommonProtocolState} from "./lb-ble-common-state/index"; //特定的蓝牙设备的协议状态,用于拓展公共的蓝牙协议状态 //使用场景: //在手机接收到蓝牙数据成功或失败后,该框架会生成一条消息,包含了对应的蓝牙协议状态值{protocolState}以及对应的{effectiveData}(effectiveData示例见 lb-example-bluetooth-protocol.js), //在{setBLEListener}的{onReceiveData}回调函数中,对应参数{protocolState}和{value}(value就是effectiveData) const ProtocolState = { ...CommonProtocolState, RECEIVE_COLOR: 'receive_color',//获取到设备的颜色值 RECEIVE_BRIGHTNESS: 'receive_brightness',//获取到设备的亮度 RECEIVE_LIGHT_CLOSE: 'receive_close',//获取到设备灯光关闭事件 }; export { ProtocolState, CommonConnectState as ConnectState }; [代码] 深入了解框架 业务 对应文件夹 示例文件 蓝牙连接 [代码]lb-ble-common-connection[代码](连接、断连、重连事件的处理) [代码]abstract-bluetooth.js[代码](最简单的、调用平台API的连接、断开蓝牙等处理) [代码]base-bluetooth.js[代码](记录连接到的设备的deviceId、特征字、连接状态等信息,处理蓝牙数据的发送、蓝牙重连) [代码]base-bluetooth-imp.js[代码](对蓝牙连接结果的捕获,监听蓝牙扫描周围设备、连接、适配器状态事件并给予相应处理) 蓝牙协议的组装 [代码]lb-ble-common-protocol-body[代码](实现协议收发格式的组装) [代码]i-protocol-receive-body.js[代码] [代码]i-protocol-send-body.js[代码] 蓝牙协议的收发 [代码]lb-ble-common-protocol-operator[代码](处理发送数据和接收数据的代理) [代码]lb-bluetooth-protocol-operator.js[代码] 蓝牙协议的重发 [代码]lb-ble-common-connection[代码] [代码]lb-bluetooth-manager.js[代码](详见[代码]LBlueToothCommonManager[代码]) 蓝牙状态及协议状态 [代码]lb-ble-common-state[代码] [代码]lb-bluetooth-state-example.js[代码],可额外拓展新的状态 蓝牙连接和协议状态事件的订阅 [代码]lb-ble-common-connection/base[代码] [代码]base-bluetooth-imp.js[代码] 下面讲下蓝牙连接和协议状态的分发 蓝牙连接状态事件的分发 文件位于[代码]lb-ble-common-connection/base/base-bluetooth.js[代码] 某一时刻连接状态改变,将新的状态赋值给[代码]latestConnectState[代码]对象。 触发其[代码]setter[代码]函数[代码]set latestConnectState[代码]。 执行[代码]setter[代码]内部的[代码]_onConnectStateChanged[代码]函数回调。 在[代码]getAppBLEManager.setBLEListener[代码]的[代码]onConnectStateChanged({connectState})[代码]函数中接收到连接状态。 蓝牙协议状态事件的分发 [代码]onBLECharacteristicValueChange[代码]位于[代码]lb-ble-common-connection/abstract-bluetooth.js[代码] [代码]receiveOperation[代码]位于[代码]lb-ble-common-protocol-operator/lb-bluetooth-protocol-operator.js[代码] 在[代码]onBLECharacteristicValueChange[代码]函数中,我在接收到数据后,将数据按[代码]receive-body.js[代码]来截取有效数据,并按[代码]lb-example-bluetooth-protocol.js[代码]中[代码]getReceiveAction[代码]的配置方式来处理有效数据,生产出对应的[代码]value, protocolState[代码]。 [代码]filter[代码]是在接收到未知协议时会生成。 [代码] onBLECharacteristicValueChange((res) => { console.log('接收到消息', res); if (!!valueChangeListener) { const {value, protocolState, filter} = this.dealReceiveData({receiveBuffer: res.value}); !filter && valueChangeListener({protocolState, value}); } }); [代码] 这段代码看起来简单,但背后要经历很多流程。 最关键的是这一行[代码]const {value, protocolState, filter} = this.dealReceiveData({receiveBuffer: res.value});[代码]。 下面我详细的讲一下这一行做了哪些事儿: 执行[代码]dealReceiveData[代码]函数处理协议数据。这里的[代码]dealReceiveData[代码],最终交由[代码]lb-bluetooth-manager.js[代码]中的[代码]dealReceiveData[代码]函数来处理数据。 在[代码]dealReceiveData[代码]中执行[代码]this.bluetoothProtocol.receive({receiveBuffer})[代码]来生成有效数据和协议状态。这个[代码]receive[代码]最终交由[代码]receiveOperation[代码]函数执行。 [代码]receiveOperation[代码]在执行时会引用到[代码]LBlueToothProtocolOperator[代码]的子类的配置项[代码]getReceiveAction[代码](子类是[代码]lb-example-bluetooth-protocol.js[代码])。 [代码]getReceiveAction[代码]按开发者自己的实现最终返回约定对象[代码]{protocolState,effectiveData}[代码],该对象返回给[代码]receiveOperation[代码]后进行一次检查(对未在[代码]getReceiveAction[代码]中配置的协议[代码]protocolState[代码]按[代码]CommonProtocolState.UNKNOWN[代码]处理),将该约定对象返回给[代码]dealReceiveData[代码]函数中的局部变量[代码]effectiveData, protocolState[代码]。 [代码]protocolState!==CommonProtocolState.UNKNOWN[代码]的对应对象,会被标记为[代码]filter:true[代码];否则将约定对象返回给[代码]onBLECharacteristicValueChange[代码]函数中的局部变量[代码]value, protocolState[代码]。 以上是这一行代码所做的所有事情。 约定对象,会作为参数传入[代码]valueChangeListener({protocolState, value})[代码]并执行回调。 之后前端就能接收到订阅的事件啦,即在[代码]getAppBLEManager.setBLEListener[代码]的[代码]onReceiveData({protocolState, value})[代码]函数中接收到协议类型和[代码]value[代码]对象。 LINK Document LICENSE 交流 技术交流请加QQ群:821711186 目前该项目已在GitHub开源:点击前往 欢迎打赏 [图片][图片]
2020-03-27 - 蓝牙wx.writeBLECharacteristicValue能发送gb2312指令给单片机?
const val = gb.encodeToGb2312('测试') const num = that.data.value console.log(val); var buffer = util.stringToBytes('+$CCTXA,' + num + ',1,2,A4' + val + '*7C\n'); console.log(buffer); wx.writeBLECharacteristicValue({ deviceId: app.globalData.deviceId, serviceId: app.globalData.serviceId, characteristicId: '0000FFE2-0000-1000-8000-00805F9B34FB', value: buffer, success: function () {}, })
2022-09-16 - 201 商户订单号重复 问题解决
我们在做支付时,如果第一次吊起支付,用户没有支付,下次再进入订单列表重新支付时,就会报这个错误。 201 商户订单号重复 [图片] 解决办法呢,就是每次支付的时候,不管是新下单支付,还是第一次没有支付成功,再次吊起支付,我们都做一个随机的处理,就是每一次提交的outTradeNo都不一样。 pay云函数 [代码]// 云函数代码 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { let orderId = event.outTradeNo.substring(0, 15) + new Date().getTime() await cloud.database().collection('mh-dingdan') .doc(event.outTradeNo) .update({ data: { orderId: orderId } }) const res = await cloud.cloudPay.unifiedOrder({ "body": event.goodName, //商品名称或商品描述 "outTradeNo": orderId, //订单号 "spbillCreateIp": "127.0.0.1", "subMchId": "1615986178", //****** 微信支付商户号 "totalFee": event.totalFee * 100, //支付的金额,单位分 "envId": "cloud1-3g5spw8wbb2af38f", "functionName": "payCallBack" //支付成功的回调 }) return res } [代码] 对应的payCallBack云函数做定当状态更新 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { //订单号 event.outTradeNo return await cloud.database().collection('mh-dingdan') .where({ orderId: event.outTradeNo }) .update({ data: { status: 1 } }).then(res => { return { errcode: 0, errmsg: '支付成功' } }).catch(res => { return res }) } [代码]
2022-03-20 - (已解决)微信支付可以退订单总金额其中一部分金额吗?
需求:活动每个成员先预付活动费,当活动结束后退还部分多出来的费用,也就是多退少补。 困惑:目前使用退款接口填入订单一半的退款金额,但还是会全退,请问微信有途径实现这个需求吗? 下面是我退部分金额的代码,请指教: //小程序端 /** * 退款 */ refund: function () { var that = this; // 获取、设置支付参数 let nonceStr = getRandomNumber() + '' // 随机数 let out_trade_no = this.data.orderId // 商户端订单号 let totalFee = this.data.order.cost * 100 //订单总金额,数据库是以“元”为单位,接口以“分”为单位 let refund_fee = this.data.order.cost/2 * 100 //退款金额,订单总金额的一半 let params = { nonceStr, out_trade_no, totalFee, refund_fee } console.log(params) //请求退款 wx.showLoading() wx.cloud.callFunction({ name: 'gameOrderApi', data: { action: 'refund', params: params, }, success: res => { console.log(res) if (res.result.resultCode == 'SUCCESS' && res.result.returnCode == 'SUCCESS') { wx.showModal({ title: '退款成功', content: '退款金额:' + res.result.refundFee / 100 + ' 元' }) } else { wx.showModal({ title: '错误信息', content: res.result.errCodeDes }) } }, fail: err => { console.log(err) }, complete: () => { wx.hideLoading() } }) }) //云函数gameOrderApi: case 'refund': console.log(event.params) const res = await cloud.cloudPay.refund({ "functionName":"wxpayCallback", "envId":"test-urupk", "subMchId":"1080438595", "nonceStr":event.params.nonceStr, //随机字符串 "out_trade_no": event.params.out_trade_no, //商户订单号 "out_refund_no" : event.params.out_trade_no+"re",//商户退款单号 "totalFee" : event.params.totalFee, //订单总金额(单位:分) "refund_fee":event.params.refund_fee,//申请退款金额 }) console.log(res) return res
2021-05-05 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序云开发:使用云函数实现模糊搜索功能
[图片] 做小程序的时候大家都会需要到搜索的功能,今天就把我这边测试成功的案例给大家。 微信官方文档:获取一个集合的数据如果要获取一个集合的数据,比如获取 todos 集合上的所有记录,可以在集合上调用 [代码]get[代码] 方法获取,但通常不建议这么使用,在小程序中我们需要尽量避免一次性获取过量的数据,只应获取必要的数据。为了防止误操作以及保护小程序体验,小程序端在获取集合数据时服务器一次默认并且最多返回 20 条记录,云函数端这个数字则是 100。开发者可以通过 [代码]limit[代码] 方法指定需要获取的记录数量,但小程序端不能超过 20 条,云函数端不能超过 100 条。话不多说,代码开始: search云函数部分(PS:记得上传云函数) // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database(); // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() let keyWords = event._keyword try { //这里的keyWords是前端小程序访问的参数_keyword return await db.collection('softshow').limit(50).where( db.command.or([{ //使用正则查询,实现对‘name’字段的搜索的模糊查询 name: db.RegExp({ regexp: keyWords, options: 'i', //大小写不区分 }), } //下面可以增加更多的选项,可以做多字段的选择 ]) ).get() } catch (e) { console.log(e) } return { event, } } search.wxml部分代码 <view class="page-box"> <view class="page-content"> <view class="search-wrap radius-border"> <view class="search-type"> <icon class="searchcion" size='20' type='search'></icon> </view> <input class="search-input" bindinput="bindSearchKey" placeholder="请输入关键字" value="{{searchValue}}" /> <view class="search-button"> <view class='sousuo' catchtap="see" style="font-size:32rpx">搜索</view> </view> </view> </view> <!-- 搜索列表 --> <block wx:if='{{obj}}' wx:for='{{obj}}' wx:key=''> <view class="list-box"> <view class="img-wrap"> <image class="img-h5" src="{{item.logo}}"></image> </view> <view class="info"> <view class="title">{{item.name}}</view> </view> </view> </block> </view> search.wxss部分代码 .page-content{ padding-top: 15rpx; margin-bottom: 20px; } .search-wrap{ overflow: hidden; border-radius: 8rpx; height: 40px; display: flex; align-items: center; border-radius: 5px; background-color: #fbfbfb; font-size: 0; line-height: 1; position: relative; } .search-input{ margin: 0 8px; flex: 1; height: 100%; font-size: 16px; } .search-button{ background-color: #00ae65; width: 70px; height: 100%; display: flex; align-items: center; justify-content: center; color: #fff; border-radius:0 5px 5px 0; } .search-type{ width: 40px; height: 100%; display: flex; align-items: center; justify-content: center; color: #333; font-size: 14px; position: relative; background-color: #e4e4e4; } /* 列表样式 */ .list-box{ position: relative; padding: 12px 16px; padding-left: 70px; height: 40px; background-color: #fff; margin-bottom: 10px; } .img-wrap{ position: absolute; left: 16px; top: 12px; width: 40px; height: 40px; border-radius: 3px; overflow: hidden; background-color: #eee; } .img-h5{ position: absolute; width: 100%; height: 100%; background-position: 50%; background-size: cover; background-repeat: no-repeat; background-color: #eee; border-radius: inherit; } .info{ height: 98%; flex-direction: column; justify-content: space-between; } .title{ font-size: 16px; color: #333; line-height: 40px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .zaiyao{ font-size: 13px; color: #999; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } search.js部分代码 // pages/search/search.js var text='' Page({ /** * 页面的初始数据 */ data: { }, see(){ wx.cloud.callFunction({ name: 'search', data: { //this.data.searchKey由页面输入框的内容 _keyword: this.data.searchKey, }, complete: res => { console.log(res) let resources = res.result.data this.setData({ obj: resources }) }, fail: res => { }, }) }, bindSearchKey: function(e) { this.setData({ searchKey: e.detail.value }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, })
2022-03-28 - 云开发http请求的两种写法
对于简单的GET表单请求 可以直接将参数封装在url中 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') // 云函数入口函数 exports.main = async (event, context) => { //qz return new Promise((resolve, reject) => { request({ url: event.URL, method: "POST",//GET json: true, headers: { "content-type": "application/json", "token": event.token }, }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] 对于POST请求 参数不好封装的 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { //这里写普通话成绩查询方式 return new Promise((resolve, reject) => { request({ url: event.url, method: "POST", json: true, headers: { "content-type": "application/json", "token":event.token }, body: event.body }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] body中填写需要的参数 body是json形式 [代码]{ xxx:xxx } [代码] 请求头可以根据自己的需要进行修改。
2019-05-28 - 云函数中。文件怎么转成 base64?
前端录制视频。云函数中需要取出base64传给第三方处理。 前端用 let base64 = wx.getFileSystemManager().readFileSync(i,"base64");又不传不了。base64数据太大了
2020-09-15 - 云开发批量上传图片,上传完图片再上传数据库 [即抄即用,拎包入住]
大家好,又是我拎包哥,今天我们来实现在云开发中批量上传图片。 经过Stephen哥的指正,我改用了Promise.all的方法来达到目的。 Promise.all的作用就是等待所包含的promise函数结束后再执行下一步逻辑,非常方便好用!const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, btn() { let promiseMethod = new Array(this.TFP.length) for (let i = 0; i < this.TFP.length; i++) { promiseMethod[i] = wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } Promise.all([...promiseMethod]).then(() => { test.add({ data: { imgList: this.imgList } }) }) } }) --------------------------------------我是分割线-------------------------------------- async await 要点: ctrl c + ctrl v这里用了await阻塞在wx.cloud.uploadFile前面,避免还没上传完图片就往数据库插入数组。减少了then里的代码,美观逼格高。嘻嘻嘻。await wx.cloud.uploadFile不能放在wx.chooseImage里,如果可以的话,请告诉我怎么做,谢谢!欢迎交流,指出错误,我立刻修改么么哒。 标准版 const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, async btn() { this.imgList = [] console.log(this.TFP) for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) 新手最爱一锅炖版(不推荐) 为什么不推荐呢,因为选择图片并不意味着要上传图片,用户还没进行最终的确定操作(不过可以用来了解async await)。 onLoad() { this.imgList = [] wx.chooseImage({ success: async res => { this.TFP = res.tempFilePaths for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) } [图片] ==========================end==========================
2020-05-17 - 【物流助手】云开发logistics.addOrder,圆通快递报9300501?
通过物流助手生成圆通快递的运单,报错 errCode: 9300501 errMsg: "openapi.logistics.addOrder:fail delivery logic fail rid: 61cf7920-4b20a5a7-47d232c1" appid wx1a87d6c4bee8214c 云函数调用成功 返回结果 {"errCode":9300501,"errMsg":"openapi.logistics.addOrder:fail delivery logic fail rid: 61cf7920-4b20a5a7-47d232c1"} 日志 START REPORT RequestId:7776eb63-6a82-11ec-af27-3e24585222e7 Duration:628ms Memory:256MB MemUsage:26.455406MB END errCode 的合法值: 9300501快递侧逻辑错误,详细原因需要看 delivery_resultcode, 请先确认一下编码方式,python建议 json.dumps(b, ensure_ascii=False),php建议 json_encode($arr, JSON_UNESCAPED_UNICODE) 使用的是云开发。 还请帮我看看,什么情况导致的?
2022-01-05 - 生成运单报:fail delivery logic fai?
通过物流助手生成圆通快递的运单,报错 errCode: 9300501 errMsg: "openapi.logistics.addOrder:fail delivery logic fail rid: 61cf7920-4b20a5a7-47d232c1" appid wx1a87d6c4bee8214c 云函数调用成功 返回结果 {"errCode":9300501,"errMsg":"openapi.logistics.addOrder:fail delivery logic fail rid: 61cf7920-4b20a5a7-47d232c1"} 日志 START REPORT RequestId:7776eb63-6a82-11ec-af27-3e24585222e7 Duration:628ms Memory:256MB MemUsage:26.455406MB END errCode 的合法值: 9300501快递侧逻辑错误,详细原因需要看 delivery_resultcode, 请先确认一下编码方式,python建议 json.dumps(b, ensure_ascii=False),php建议 json_encode($arr, JSON_UNESCAPED_UNICODE) 使用的是云开发。 还请帮我看看,什么情况导致的?
2022-01-01 - 如何批量下载云开发存储文件到本地
有人说打开云开发,存储点击文件有下载地址,这对于少量资源是可以的,下面的方法是下载上万文件的方法: 通过访问官方:https://docs.cloudbase.net/cli-v1/install [图片] [图片] 第四步、云存储文件路径:/qrcode/20220311 ==》需要下载到本地:如 E:\qrcode 1、首先在本地: E:\qrcode\文件内创建一个名为:cloudbaserc.json 的json文件。 { "envId":"你的云开发环境的id" } 2、通过命令访问E盘: e: 3、访问需要下载到本地文件路径 cd qrcode 4、开始下载云端文件到本地,执行命令,对于云端文件路径有子目录可以通过 / 进行访问。 tcb storage download qrcode/20220311 . --dir 5、等等文件下载中,直至文件全部下载。
2022-03-11 - 云开发云函数定时触发器讲解
任何可以产生事件,触发云函数执行的均可以被称为触发器,而定时触发器则是可以处理周期性的事情,比如时报、日报、周报等通知提醒,也可以处理倒计时任务,比如节假日、纪念日以及你可以指定一个具体时间的倒计时任务,除此之外,定时触发器还可以用来周期性处理一些定时任务。比如定期清理一些不必要的数据,定期更新集合内的数据。 13.5.1 定时触发器使用说明1、定时触发器的配置与部署配置了定时触发器的云函数,会在相应时间点被自动触发,云函数的返回结果不会返回给调用方。在对某个云函数使用定时触发器前,首先要保证该云函数在小程序端可以调用成功,更准确的说是能够在不传入参数的情况下在云开发控制台的云端测试能调试成功(小程序端调用有登录态)。 云函数目录里的 config.json 文件可以用来配置权限和定时触发器,如果你的云函数目录下面没有这个配置文件,可以自己创建一个,创建的结构目录如下: test //云函数目录 ├── config.json //权限和定时触发器等的配置文件 ├── index.js //云函数 ├── package.json //云函数的依赖管理 然后再来在配置文件 config.json 里进行类似如何格式的配置,config.json 严格遵循配置文件所要求的格式,比如数组最后一项不能有逗号[代码],[代码];配置文件里不能有注释等 triggers 字段是触发器数组,但是目前云函数只支持一个触发器,即数组只能填写一个,不可添加多个;name 是触发器的名字,最大支持 60 个字符,支持 a-z, A-Z, 0-9, - 和 _,必须以字母开头;type 为触发器类型,timer 是定时触发器config 是触发器的定时配置,里面为 cron 表达式(后面有介绍),cron 有七个必需字段,不能多也不能少(以下为每天早上 9 点到 12 点每隔 5 秒触发一次);{ "triggers": [ { "name": "tomylove", "type": "timer", "config": "*/5 * 9-12 * * * *" } ] } 当我们在修改触发器配置文件 config.json 后,首先鼠标右键 config.json 选择“云函数增量上传:更新文件”,然后再右键 config.json 选择“上传触发器”。这里的“云函数增量上传:更新文件”是让云函数端的触发器文件更新;而“上传触发器”则是让触发器开始生效执行。如果在云函数端的触发器没有更新的情况下就“上传触发器”来执行定时触发,文件可能没有更新,执行的还是旧的触发器内容。当我们想暂停或删除触发器时,可以右键选择“删除触发器”。 2、Cron 表达式语法Cron 表达式有七个必填字段,按空格分隔,既不能多写也不能少写,每一个字段都有它的含义对应着不同的时间点,表达式的取值都为整数且为时间制的范围(注意月在星期的前面): 第一位第二位第三位第四位第五位第六位第七位秒(0-59 )分钟(0-59)小时(0-23)日(1-31)月(1-12或三个字母的英文缩写)星期(0-6或三个字母的英文缩写)年(1970~2099 ) 下面是 cron 表达式的案例,以及我们需要了解一下 cron 表达式里的通配符以及直接写数字的含义: [代码],[代码],表示并集,在时间的表述里是“和”的意思,比如在“小时”字段中, [代码]1,2,3[代码]表示 1 点、2 点和 3 点;[代码]-[代码],指定范围的所有值,在时间的表述里是“到”的意思,比如在“日”字段中,[代码]1-15[代码]包含指定月份的 1 号到 15 号;[代码]*[代码],表示所有值,在时间的表述里是“每”的意思,比如在“小时”字段中,[代码]*[代码]表示每小时;[代码]/[代码],指定步长,在时间的表述里是“隔”的意思,比如在“秒”字段中,[代码]*/5[代码]表示每隔 5 秒;直接写数字,在时间的表述里是“第”(时间点)的意思,比如在“月”字段中,[代码]5[代码]表示每月的第 5 日;//表示每隔5秒触发一次, */5 * * * * * * //表示在每月的1日的凌晨2点触发 0 0 2 1 * * * //表示在周一到周五每天上午10:15触发 0 15 10 * * MON-FRI * //表示在每天上午10点,下午2点,4点触发 0 0 10,14,16 * * * * //表示在每天上午9点到下午5点内每半小时触发 0 */30 9-17 * * * * //表示在每个星期三中午12点触发 0 0 12 * * WED * 定时触发器的 Cron 语法没法实现每隔 90 秒钟或 90 分钟发送一次这样的效果,因为 90 秒超过了秒的时间制上限 60,而 cron 在跨位组合(比如 90 秒需要结合秒和分)上无法覆盖所有的时间;除此之外,云开发的触发器暂时不支持多个定时触发器的叠加;在 Cron 表达式中的“日”和“星期”字段同时指定值时,两者为“或”的关系,即两者的条件均生效;值得一提的是,尽管云函数的时区为 UTC+0 时区,但是定时触发器的时间还是北京时间。 13.5.2 用定时触发器调用云函数定时触发器的使用非常简单,使用开发者工具新建一个云函数比如 trigger,然后在 index.js 里输入以下代码: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { console.log(event); return event; }; 再在 trigger 云函数目录下的 config.json(如果没有这个文件,就创建一个),然后输入以下触发器,为了调试方便,我们可以每隔 5 秒触发一次: { "permissions": { "openapi": [ ] }, "triggers": [ { "name": "tomylove", "type": "timer", "config": "*/5 * * * * * *" } ] } 然后分别右键 index.js 和 config.json,选择“云函数增量上传:更新文件”,然后再来右键 config.json 选择“上传触发器”。云函数就会每隔 5 秒自动触发,相关的日志我们可以在开发者工具的云开发控制台以及腾讯云云开发网页控制台的云函数的日志里查看。 注意小程序端调用 trigger 云函数返回的 event 对象,和使用定时触发器返回的 event 对象的不同,用定时触发器触发云函数是获取不到 openId 的,同时这里有一个 Time 时间是时区为 UTC+0 的时间,比北京时间晚 8 个小时: //在小程序端调用trigger云函数之后返回的event对象 { "userInfo":{ "appId":"wxda99******7046", "openId":"oUL-m5F******buEDsn8" } } //使用定时触发器触发云函数之后返回的event对象 { "Message":"", "Time":"2020-06-11T11:43:35Z", "TriggerName":"tomylove", "Type":"timer", "userInfo":{ "appId":"wxda99********46" } } 13.5.3、定时触发器的应用定时触发器的应用非常广泛,以下仅举一些常用案例,并加以说明: 1、结合消息推送这里的消息推送不仅仅只是指订阅消息,还可以是统一服务消息、公众号的消息(可以用云函数开发微信公众号)、小程序内自己开发的通知(只是用户只有在打开小程序时才能看到)、Email 邮件等等。 比如用户订阅了日报、周报、月报等周期性的通知提醒或者我们需要给用户发送一些汇总信息,就可以固定写一个定时触发器,比如我们需要给指定用户发送工作周报,每周五晚上 17 点 30 分就定时从数据库获取数据发送消息,cron 表达式写法如下: * 30 17 * * FRI * 还可以用来处理一些倒计时(指定时间点)的任务,比如节假日、纪念日以及一些活动时间节点(定时触发器目前只能一个云函数配一个触发器,但是可以提前管理),比如我们希望在六一儿童节的早上 9 点调用云函数给指定用户群体发送消息: 0 0 9 1 6 * * 当然这样的具体时间点显得过于的不灵活,但是如果把时间与云开发数据库结合起来,灵活性就会大很多,比如在运营上每天早上 11 点是你们用户访问最多的时间点,你只需要写一个云函数,把所有的活动都在这个时间点来推送,让定时触发器每天这个时间点都触发,有活动(数据库里有数据)就会发消息,如果没有就不发(云函数调用一次的成本极低)。 如果是实时数据,我们还可以把定时触发器的频率调高,每 5 秒就触发一次,比如我们的数据库只要有最新的数据,就会发消息给指定用户。尽管不是完全的实时,但是 5 秒的频率和实时的差别也就不大了。你也可以根据情况,来调整触发器的频率,毕竟 5 秒和 1 分钟的频率给用户的体验差异并没有太大,但是成本却是 12 倍的关系。 可能你还希望在指定的时间段才触发云函数,比如你只希望在工作日、或者在早上 9 点到晚上 18 点才触发,在指定的时间段才触发既可以让触发更精准不扰民,也可以节约成本,比如下面的触发器就是工作日早上 9 点到 12 点和下午 14 点到 18 点这个时间段,每 5 秒触发一次。 */5 * 9-12,14-18 * MON,TUE,WED,THU,FRI * 从以上案例我们可以了解到,云函数的定时触发可以来自于 cron 表达式的配置,我们可以指定时间点时间段和频率来达到我们想要的效果,同时这个时间“也可以来自于数据库的配置”(伪装),意思是我们可以设置触发器的时间段或频率,如果数据库里有数据就发送,没有数据就不发送,这样就可以达到触发器在时间上的灵活性了。 2、实时获取数据有的时候我们的数据并不是来自于数据库,而是来自于第三方服务,比如前面介绍过的历史上的今天的 API,天气的 API,知乎日报的 API 等等,以及一些 webhook,这些 API 和第三方服务提供的是 json 格式的文件,API 的数据也会随时更新,但是它们更新了却并不会主动通知我们,这时我们可以使用定时触发器向这些 API 发起请求,如果数据出现更新,我们就可以将更新的数据存储到我们的数据库或者进行其他处理,比如企业微信的机器人等机器人通知服务就是如此。 当然定期获取的数据还可以是爬虫,比如我们可以定期抓取指定关键词的新闻或者指定网站的动态,当爬虫获取到了不同的数据的时候,就将最新的动态以机器人消息或者其他方式进行及时的处理。 也就是说,我们无法实时监听到第三方 API 或者网站数据的变动,但是可以用定时触发器来发起请求或者爬虫抓取数据,通过数据的变化来达到“实时”获取数据的目的。 3、自动化处理在数据库的设计里,我们就提到有时候需要对数据库里的数据进行定期的备份与删除等清理维护工作,比如超过一定时间的日志,具有很强时效性的活动数据,以及为了性能考虑而做的虚假删除(数据库性能与优化有介绍)等,毕竟数据库有一定的存储成本而且过多无用数据也会影响数据库的性能,我们可以写一个云函数用定时触发器来执行此类任务。 我们还可以在用户并发比较少的时间段(比如凌晨几点)来处理一些比较耗云函数、数据库性能的任务,比如图片的审核与裁剪、缩略等处理,用户评论是否包含敏感词汇(尽管经过安全处理,但是有时候我们还会设置特别的敏感词),数据的汇总,云存储里废弃文件的删除,用户信息是否完整等等。 也就是说,结合定时触发器,我们可以实现一些任务的自动化处理。 4、密集型任务分流我们知道云函数在处理一些复杂性的任务时是有一些限制的,一是执行时间的限制,建议在设置时执行时间一般不要超过 20s,最长不要超过 60s;二是并发的限制,云函数最大的并发为 1000;三是云函数在查询数据库时一次可以获取最多 1000 条的数据,面对这三个限制,我们应该如何处理密集型的任务呢,比如发送 100 万封邮件,导出几百万条数据到 Excel,发送十万级的订阅消息或消息等等,这个时候就可以使用到定时触发器来处理了。 借助于定时触发器,我们可以将需要耗时较长、对并发要求较高以及数据库请求等的任务进行分批处理,比如我们要给 100 万人发邮件:云函数发起数据库请求,一次只请求 1000 条未发送过邮件的用户(用 where 条件查询某个字段,比如[代码]status:false[代码]),然后将邮件发给 1000 个人(可以参考前面的邮件发送),发完邮件并对这 1000 条数据进行标记(比如使用更新指令将 status 改为 true),这样下次查询未发送过邮件的用户时,就不会重复发送了。通过定时触发器,每 2 秒执行一次发送任务,几十分钟就可以处理完任务。
2021-09-10 - 云开发“分账功能”踩坑记
云开发的分账功能还在公测中,需要去申请开通。如果等了几天还没开通,可以在社区提问,会有官方人员来跟进。 我等了 5 天才开通。 开始之前,首先要搞清楚一个概念:云开发就是一个服务商,所以开发者不需要注册成为支付服务商,就可以使用服务商分账。 以下是我开发过程中遇到的一些坑。可能你会遇到其他坑,记得把调用接口的结果打印出来,看看报的是什么错。 1、文档不全,需要传 profit_sharing 或者 profitSharing 云开发统一下单接口的文档缺少 profit_sharing 参数,需要结合原支付接口文档来看,在分账对接步骤里也有说明。 如果没传这个参数,会报错:“非分账订单不支持分账”。 经过测试发现,这个参数是必需的,可以是 profit_sharing,也可以是 profitSharing,两者都兼容。由于其他参数都是驼峰写法,所以我用了 profitSharing . 2、不需要每次分账都“添加分账接收方” 添加分账接收方,只需要调一次接口即可,添加成功后,不需要每次分账都添加接收方。 有个小提醒,receiver 这个参数不是 JSON,而是JSON 序列化后的字符串,记得用 JSON.stringify 处理。序列化前是对象,不是数组,如果有多个接收方就调用多次接口。 (注意区分一下,在分账接口中,分账接收方的参数是 receivers,不是 receiver。不管单次分账还是多次分账,receivers 参数都是序列化后的字符串,序列化前是对象数组,不是对象) 这里还有个 bug,根据通知指引,我并没有找到这个入口:“商户平台 - 交易中心 - 管理分账接收方”。后面发了个提问帖,找到了这个暗门。登录商户平台,然后访问这个地址:https://pay.weixin.qq.com/index.php/xphp/ccmn_sharing/split_relation_manage 这可能也不是 bug,我猜估计是担心商户会有意或者无意的删除分账接收方。 3、sub_appid 是必填 文档有误,这个参数是必填。 4、PERSONAL_OPENID 和 PERSONAL_SUB_OPENID receivers 的 type 参数,PERSONAL_OPENID 和 PERSONAL_SUB_OPENID,刚开始会有点迷糊。我填的是 PERSONAL_SUB_OPENID,account 填小程序数据库里的用户 openid . 5、支付成功后,需要延时 1 分钟处理 支付成功后如果立即处理分账会报错:“订单处理中,暂时无法分账,请稍后再试”。 分账产品介绍的文档是这么写:“在交易完成后,准实时(建议1分钟后)或30天内调分账接口。” 我单独用了一个云函数来处理分账,setTimeout 设置 59 秒后调用分账接口。 在开发者工具里,进入该云函数的配置设置,把超时时间设置为 60 秒,否则会返回超时。 支付成功后调用该云函数。 2021-9-23 更新: 不能用 setTimeout 处理分账,这样云函数会一直占用内存,超成资源浪费(而且是巨大的浪费)。应改成定时触发,例如每小时触发一次,把已支付的并且支付时间已经过了一分钟的订单找出来,调用分账接口。
2021-09-23 - 请问使用CloudPay.unifiedOrder()怎么获得订单号transaction_id?
因为甲方希望在用户付款之后自动分账,那就需要即时获取用户支付后的订单号。但文档里面没有写相关的返回值,难道只能手动到后台去复制粘贴订单号来做分帐吗?希望能够得到解答,谢谢
2021-11-19 - 小程序云开发--多个小程序协同工作
概述:小程序云开发提供给开发者可以无需搭建服务器,即可使用云端能力来开发微信小程序、小游戏。云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,可以说这个能力可以极大的提高一个前端开发的单兵作战能力。因为个人爱好,闲暇时间也用云开发去开发过几个小程序,作为一个简单系统包括了后台管理小程序和用户使用的小程序,那么问题来了,数据库和云函数只能对应在一个小程序,而且存储在云端文件系统里的图片的访问也只能在当前的小程序中访问,对于另一个小程序怎么去访问数据、访问图片呢? 一、从小程序B访问小程序A的云函数 这里主要利用了云开发提供的通过http接口访问云函数的能力,文档链接 1、开通小程序A的云开发权限 & 新建集合 由于文章篇幅以及内容重点原因,创建小程序、开通云开发权限我们先略过了,我们首先创建一个集合”users“(数据库),设置它的权限为“所有用户可读,仅创建者可写”,这里是为了将来可以通过云函数进行数据的插入,权限链接。 2、新建小程序A的云函数 这个步骤相对简单,在这里我们假设创建了一个"users_add"的一个添加用户的云函数,作用是用于添加用户。主要逻辑是首先判断用户是否已经存在,若存在则返回“已存在”,若不存在则添加用户并返回“成功”。代码完成之后云函数上传并部署。直接上代码吧~ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _users = db.collection('users') // 云函数入口函数 exports.main = async (event, context) => { const user = await _users.where({ openid: event.openid }).get().catch((err) => err); if (!user || user.errMsg != 'collection.get:ok' || user.data.length != 0) { return { errcode: -1001, errmsg: "新增失败" } }else if (user.data.length != 0) { return { errcode: -2001, errmsg: "用户已存在" } } const res = await _users.add({ data: { name: event.name, age: event.age, sex: event.sex, tel: event.tel, openid: event.openid, createtime: new Date().getTime(), } }) if (res && res._id != '') { return { errcode: 0, errmsg: 'success', data: { res: res } } }else{ return { errcode: -9999, errmsg: "新增失败", data: { res: res } } } } 3、小程序B调用小程序A的云函数思路 还是一样创建小程序、开通云开发权限我们先略过了。通过官方文档的说明我们知道,通过http的方式可以访问我们的云函数,官方给予我们的示例是这个样子的:POST https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=ACCESS_TOKEN&env=ENV&name=FUNCTION_NAME,那么我们需要的获取的参数有:access_token(接口调用凭证,获取的文档可以点击此处) 、env(云开发环境ID)、name(云函数名称),这里的access_token比较烦,需要先获取,时效为2小时,官方文档建议我们有个中控服务统一获取和刷新。另外,我们每次调用A小程序的云函数都直接在小程序B里去发这个请求的话,每次都要重复获取token和填写env、name参数,这不符合我们的设计模式。那么还是老办法,我们创建一个云函数,每次小程序B里需要调小程序A的云函数时,先经过中间层即B自己的工具云函数,将方法名称和参数传给该工具云函数,由其完成token的获取、填写env、调小程序A的云函数,将返回的数据返回给小程序。 4、开通小程序B的云开发权限 & 新建集合 & 新建工具云函数 基于此思路我们首先创建一个集合用来记录调用接口凭证的集合"token",并设置其权限;然后新建一个云函数"api",代码如下~ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _token = db.collection('token') const _ = db.command; var rp = require('request-promise') // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() // 获取数据库集合中的“token” const token = await _token.get().catch((err) => err); var access_token = ''; // token不存在、token过期的情况下,重新获取token if (!token || token.errMsg != 'collection.get:ok' || token.data.length == 0 || token.data[0].expires_in < new Date().getTime()) { //删除token里保存的数据 await _token.where({ expires_in: _.gt(0) }).remove() // 获取access_token,真实情况下请替换自己的appid和secret var res = await rp("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wxb1c2c7******0759&secret=9840548c1348*****aa91bc74ae94") .then(function (res) { return res }) .catch(function (err) { return { errcode: -1, errmsg: "获取access_token失败" } }); res = JSON.parse(res); // 将access_token插入到token表中缓存起来 await _token.add({ data: { access_token: res.access_token, expires_in: res.expires_in * 1000 + new Date().getTime() } }) access_token = res.access_token } else {//token存在且未过期,直接取出来使用 access_token = token.data[0].access_token } // 调小程序A的云函数的参数 var options = { method: 'POST', uri: `https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=${access_token}&env=prod-en**h&name=${event.func}`,// 注意env参数,请替换自己的环境id body: event.data, json: true }; options.body.openid = wxContext.OPENID// 加上当前用户的openid var responseData = await rp(options).then(res => res); if (responseData.errcode == 0) { return { errcode: 0, errmsg: 'success', data: JSON.parse(responseData.resp_data) } } else { return { errcode: -3001, errmsg: '接口调用失败', data: responseData && responseData.resp_data ? JSON.parse(responseData.resp_data) : {} } } } 5、小程序B页面中调小程序A的云函数 这个就相对简单了,直接看代码吧~ wx.cloud.callFunction({ name: 'api',// 小程序B自己的云函数 data: { func: 'users_add',// 小程序A的云函数 data: {// 小程序A的云函数需要传递的参数 name: this.data.name, age: this.data.age, sex: this.data.sex, tel: this.data.tel } } }).then(res => { if (res && res.result && res.result.data && res.result.data.errcode == 0) { wx.showToast({ icon: 'none', title: '添加成功', duration: 2000 }) }else{ wx.showToast({ icon: 'none', title: '添加失败', duration: 2000 }) } }) 二、从小程序B访问小程序A上传的图片或文件 一开始的时候是直接使用了小程序A云开发自带的文件上传,文档可以看这里,上传之后返回的是文件id,如cloud://xxx.png,在小程序A里访问是没有问题的,但是在小程序B里访问时就打不开了。。。当然我们可以使用官方提供的方法去换取临时链接(有效期只有2个小时,哎......),想了想每次小程序B去访问A上传的图片还得去转链接,另外小程序B上传的图片怎么储存在小程序A的云数据库里呢?尤其是像上传到同一个集合中的同一个字段名,还得去记录是哪个小程序上传的,每次访问时都得需要换取临时链接,太尴尬了。。。所以,个人推荐使用的是第三方的文件存储服务器,生成一个永久链接,直接保存在数据库中,不管哪个小程序访问都可以直接打开。 三、总结 正如概述中所说的小程序云开发给予开发人员极强的单兵作战能力,我们只需关注业务开发而无需去投入过多的精力关注数据库和运维的内容,开发起来快速且流畅。本文记录里一下本人开发过程中碰到的一些问题,分享出来跟大家一起交流学习,若大家由更好的方法,请联系我,我们一起探讨探讨 : ) ps:云存储的文件能开放个功能让我们生成永久链接该多爽。。。还能当个文件服务器使用,哈哈哈哈~
2020-08-19 - 「笔记」字节跳动小程序如何接入腾讯云CloudBase?
前言 最近在把微信小程序迁移至字节跳动小程序,由于服务端使用了腾讯云 CloudBase,网上搜索了一遍,文章千篇一律,都是复制腾讯云官方1年以前的适配器文档,在经过和腾讯云官方技术人员沟通后终于成功解决问题。 安装 npm i @cloudbase/js-sdk -S npm i @maoyan/cloudbase-adapter-tt_mp -S 使用 由于字节跳动小程序没有提供getAccountInfoSync()接口,无法通过接口获取appId 所以需要将appId设置到字节跳动小程序app对象上。 [代码]App({ onLaunch(options) { this.appId = appId } }) [代码] 腾讯云 CloudBase 安全配置 由于字节跳动小程序使用云开发不享受微信生态下的免鉴权,要在终端应用(如APP、小程序等)中使用云开发的身份验证服务,需要将授权的应用加入白名单,并在SDK使用时传入分配的凭证信息。 腾讯云 CloudBase 登陆授权 为了增加安全性,建议开启匿名登陆。启动匿名登录后,用户将不需要登录即可访问应用。如果有更严格的安全要求,可以自行开启其它身份验证方式。 完整代码 [代码]import tcb from '@cloudbase/js-sdk'; import { adapter } from '@maoyan/cloudbase-adapter-tt_mp'; let app; App({ onLaunch(options) { // appId必须设置 this.appId = "字节跳动小程序的appid"; tcb.useAdapters(adapter); // 腾讯云共享环境初始化 app = tcb.init({ env: '云环境id', appSign: '应用标识', // 需要设置成字节跳动小程序的appid appSecret: { appAccessKeyId: '版本', appAccessKey: '凭证' } }) // 匿名登陆 const auth = app.auth() const loginState = auth.anonymousAuthProvider().signIn() let data = await app.callFunction({ name: "云函数名", data: "参数" }); console.log(data) } }) [代码]
2022-03-03 - [拎包哥] 批量下载云开发云存储的文件到pc端
官方教程 注:第2步和第3部的代码是反过来的。 [图片] 我的步骤: 0.按照官方教程安装tcb脚手架。 1.在桌面新建文件夹hellowWorld,新建cloudbaserc.json,json里的内容为: [图片] { "envId":"你的云开发环境的id" } 2.在cmd输入命令行。 e.g. 下载云存储里的qrCode文件夹到PC端文件夹(桌面的helloWorld文件夹)。 注:--dir在这里的意思是声明前面的 . 是一个文件夹,不需要另外修改。 [图片] tcb storage download qrCode . --dir
2021-09-28