- 如何使用微信小程序·云开发的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 - subscribeMessage开始能发送消息,但后来手动不接收消息,再次开启后,怎就一直接收不了?
"errCode":43101,"errMsg":"openapi.subscribeMessage.send:fail user refuse to accept the msg hint: [FBdZ1a08444924]",我现在微信设置已经允许接收消息了,但调用云函数发送消息的时候,就提示用户拒接接收
2019-11-03 - 一次性订阅消息 如何可以发送多次?
小程序发送一次性订阅消息,发送后在发送第二次就报 43101错误,请问怎样解决
2022-05-16 - 通过wx.joinVoIPChat调用voip-room 后加入的成员不能及时显示画面,求解?
通过 wx.joinVoIPChat API调用成员后, 第一个加入的成员可以正常显示, 第二个成员能显示出黑色占位框,有声音,但不能显示画面,如果占击分享后返回,则可以刷新 出画面 加入时界面 [图片] 点击邀请后界面 [图片] 源码: wxml <view class="view_room" wx:if="{{m1.indexOf(openIdList, cid) > -1}}"> <block wx:for="{{openIdList}}" wx:key="*this"> <voip-room openid="{{item}}" mode="{{selfOpenId === item ? 'camera' : 'video'}}"></voip-room> </block> <block wx:for="{{blankview}}" wx:key="*this"> <view class="view_yq"> <view>+ 邀请</view> <button open-type="share" title="邀请"></button> </view> </block> </view> <view class="room_esc" wx:else> 房间已经被解散! </view> <view style="display:none;color:#fff"> {{openIdList.length}} - {{joinVoIPChat}} <view>{{openIdList}}</view> <view>signature:{{signature}}</view> <view>nonceStr:{{nonceStr}}</view> <view>timeStamp:{{timeStamp}}</view> <view>groupId:{{groupId}}</view> <view>cid:{{cid}}</view> <view>selfOpenId:{{selfOpenId}}</view> <view>session_key:{{session_key}}</view> </view> <view style="height:60px;"></view> <view class="room_menu"> <view bindtap="goMuteConfig" data-v="muteMicrophone"><text class="fal fa-microphone{{room_menu[1]}}"></text>{{room_menu[0]}}</view> <view bindtap="goMuteConfig" data-v="muteEarphone"><text class="fal fa-volume{{room_menu[4]}}"></text>{{room_menu[3]}}</view> <navigator bindtap="goExit" wx:if="{{selfOpenId == cid}}"><text class="fal fa-power-off"></text>结束面试</navigator> <navigator target="miniProgram" open-type="exit" wx:else><text class="fal fa-power-off"></text>退出面试</navigator> </view> <wxs module="m1"> function defineIndexOf(str, val) { return str.indexOf(val); } module.exports = { indexOf: defineIndexOf } </wxs> js const app = getApp() Page({ /** * 页面的初始数据 */ data: { groupId : '', joinVoIPChat:'0', openIdList:[], blankview: ['','','',''], room_menu:['开启话筒', '', false, '开启声音', '', false], yq_img:'' }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { this.setData({ selfOpenId: app.globalData.openid, session_key: app.globalData.session_key, cid: ( options.cid != '' && options.cid != undefined ) ? options.cid : app.globalData.openid, }) wx.showLoading({mask: false }) }, /** * 生命周期函数--监听页面显示 */ onShow: function () { var _this =this; //设置是否保持常亮状态。仅在当前小程序生效,离开小程序后设置失效 wx.setKeepScreenOn({ keepScreenOn: true }) wx.request({ url: app.api_url + 'Room/index/' + app.globalData.openid + '/' + app.globalData.userid, data: {openid:_this.data.cid, session_key:app.globalData.session_key}, header: { 'content-type': 'application/x-www-form-urlencoded' }, method: 'POST', success(res) { _this.setData(res.data) //加入 (创建) 实时语音通话 wx.joinVoIPChat({ signature: res.data.signature, nonceStr: res.data.nonceStr, timeStamp: res.data.timeStamp, groupId: res.data.groupId, roomType: 'video', success(res){ console.log(res) _this.setData({ openIdList: res.openIdList, blankview: Array.apply(null,{length: (4 - res.openIdList.length) }) }) wx.hideLoading(); }, fail(res){ _this.setData({ joinVoIPChat:'fail' + res.errCode + res.errMsg }) } }) //订阅视频画面成员。对于视频房间,当成员超过两人时需进行订阅,否则只能看到最先加入房间的两人画面。 wx.subscribeVoIPVideoMembers({ openIdList: _this.data.openIdList, success(res){ console.log('subscribeVoIPVideoMembers success') } }) //监听实时语音通话成员在线状态变化事件。有成员加入/退出通话时触发回调 wx.onVoIPChatMembersChanged(function(res){ _this.setData({ openIdList: res.openIdList, blankview: Array.apply(null,{length: (4 - res.openIdList.length) }) }) //订阅视频画面成员。对于视频房间,当成员超过两人时需进行订阅,否则只能看到最先加入房间的两人画面。 wx.subscribeVoIPVideoMembers({ openIdList: _this.data.openIdList, success(res){ console.log('subscribeVoIPVideoMembers success_b') } }) }) } }) setInterval(function(){_this.onShow;console.log('setInterval 10')}, 10000); }, goMuteConfig:function(e){ if( e.currentTarget.dataset.v == 'muteMicrophone' ){ if( this.data.room_menu[1] == '' ){ this.setData({room_menu: ['关闭话筒', '-slash', true, this.data.room_menu[3], this.data.room_menu[4], this.data.room_menu[5]]}) }else{ this.setData({room_menu: ['开启话筒', '', false, this.data.room_menu[3], this.data.room_menu[4], this.data.room_menu[5]]}) } }else{ if( this.data.room_menu[4] == '' ){ this.setData({room_menu: [this.data.room_menu[0], this.data.room_menu[1], this.data.room_menu[2],'关闭声音', '-slash', true]}) }else{ this.setData({room_menu: [this.data.room_menu[0], this.data.room_menu[1], this.data.room_menu[2],'开启声音', '', false]}) } } wx.updateVoIPChatMuteConfig({ muteConfig:{ muteMicrophone: this.data.room_menu[2], muteEarphone: this.data.room_menu[5] } }) }, goExit:function(){ wx.exitVoIPChat(); // wx.redirectTo({url: 'index'}) wx.navigateBack({delta:1}) }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { wx.exitVoIPChat(); }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { var _this = this return { title: '邀请您加入远程面试', path: '/pages/call/share?cid=' + app.globalData.openid, imageUrl: _this.data.yq_img, success: function (res) { console.log('转发成功' + app.globalData.openid) } } } })
2020-09-30 - 云开发短信跳小程序(自定义开发版)教程
写在前面如果你想要自主开发,但没有云开发相关经验,可以采用演示视频来学习本教程: [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 URL Scheme 的方式来拉起微信打开主体小程序的。 总之,短信跳转能力的实现分为两个步骤,「配置拉起网页」和「发送短信」。本教程将介绍如何执行操作完成短信跳转小程序的能力。 如果你想要无需写代码就能完成短信跳转小程序的能力,可以参照无代码版教程进行逐步实现。 二、操作指引1、网页创建首先我们需要构建一个基础的网页应用,在任何代码编辑器创建一个 html 文件,在教程这里命名为 index.html 在这个 html 文件中输入如下代码,并根据注释提示更换自己的信息: window.onload = function(){ window.web2weapp.init({ appId: 'wx999999', //替换为自己小程序的AppID gh_ID: 'gh_999999',//替换为自己小程序的原始ID env_ID: 'tcb-env',//替换小程序底下云开发环境ID function: { name:'openMini',//提供UrlScheme服务的云函数名称 data:{} //向这个云函数中传入的自定义参数 }, path: 'pages/index/index.html' //打开小程序时的路径 }) } 以上引入的 web2weapp.js 文件是教程封装的有关拉起微信小程序的极简应用,我们直接引用即可轻松使用。 如果你想进一步学习和修改其中的一些WEB展示信息,可以前往 github 获取源码并做修改。 有关于网页拉起小程序的更多信息可以访问官方文档 如果你只想体验短信跳转功能,在执行完上述文件创建操作后,继续以下步骤。 2、创建服务云函数在上面创建网页的过程中,需要填写一个UrlScheme服务云函数。这个云函数主要用来调用微信服务端能力,获取对应的Scheme信息返回给调用前端。 我们在示例中填写的是 openMini 这个命名的云函数。 我们前往微信开发者工具,定位对应的云开发环境,创建一个云函数,名称叫做 openMini 。 在云函数目录中 index.js 文件替换输入以下代码: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { return cloud.openapi.urlscheme.generate({ jumpWxa: { path: '', // 打开小程序时访问路径,为空则会进入主页 query: '',// 可以使用 event 传入的数据制作特定参数,无需求则为空 }, isExpire: true, //是否到期失效,如果为true需要填写到期时间,默认false expire_time: Math.round(new Date().getTime()/1000) + 3600 //我们设置为当前时间3600秒后,也就是1小时后失效 //无需求可以去掉这两个参数(isExpire,expire_time) }) } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 接下来,我们需要开启云函数的未登录访问权限。进入小程序云开发控制台,转到设置-权限设置,找到下方未登录,选择上几步我们统一操作的那个云开发环境(注意:第一步配置的云开发环境和云函数所在的环境,还有此步操作的环境要一致),勾选打开未登录 [图片] 接下来,前往云函数控制台,点击云函数权限,安全规则最后的修改,在弹出框中按如下配置: [图片] 3、本地测试我们在本地浏览器打开第一步创建的 index.html ;唤出控制台,如果效果如下图则证明成功! 需要注意,此处本地打开需要时HTTP协议,建议使用live server等扩展打开。不要直接在资源管理器打开到浏览器,会有跨域的问题! [图片] 4、上传本地创建好的 index.html 至静态网站托管将本地创建好的 index.html 上传至静态网站托管,在这里静态托管需要是小程序本身的云开发环境里的静态托管。 如果你上传至其他静态托管或者是服务器,你仍然可以使用外部浏览器拉起小程序的能力,但会丧失在微信浏览器用开放标签拉起小程序的功能,也不会享受到云开发短信发送跳转链接的能力。 如果你的目标小程序底下有多个云开发环境,则不需要保证云函数和静态托管在一个环境中,无所谓。 比如你有A、B两个环境,A部署了上述的云函数,但是把 index.html 部署到B的环境静态托管中了,这个是没问题的,符合各项能力要求。只需要保证第一步 index.html 网页中的云开发环境配置是云函数所在环境即可。 部署成功后,你便可以访问静态托管的所在地址了,可以通过手机外部浏览器以及微信内部浏览器测试打开小程序的能力了。 5、短信发送云函数的配置在上面创建 openMini 云函数的环境中再来一个云函数,名字叫 sendsms 。 在此云函数 index.js 中配置如下代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { try { const config = { env: event.env, content: event.content ? event.content : '发布了短信跳转小程序的新能力', path: event.path, phoneNumberList: event.number } const result = await cloud.openapi.cloudbase.sendSms(config) return result } catch (err) { return err } } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 6、测试短信发送能力在小程序代码中,在 app.js 初始化云开发后,调用云函数,示例代码如下: App({ onLaunch: function () { wx.cloud.init({ env:"tcb-env", //短信云函数所在环境ID traceUser: true }) wx.cloud.callFunction({ name:'sendsms', data:{ "env": "tcb-env",//网页上传的静态托管的环境ID "path":"/index.html",//上传的网页相对根目录的地址,如果是根目录则为/index.html "number":[ "+8616599997777" //你要发送短信的目标手机,前面需要添加「+86」 ] },success(res){ console.log(res) } }) } }) 重新编译运行后,在控制台中看到如下输出,即为测试成功: [图片] 你会在发送的目标手机中收到短信,因为短信中包含「退订回复T」字段,可能会触发手机的自动拦截机制,需要手动在拦截短信中查看。 需要注意:你可以把短信云函数和URLScheme云函数分别放置在不同云开发环境中,但必须保证所放置的云开发环境属于你操作的小程序 另外,出于防止滥用考虑,短信发送的云调用能力需要真实小程序用户访问才可以生效,你不能使用云端测试、云开发JS-SDK以及其他非wx.cloud调用方式(微信侧WEB-SDK除外),会提示如下错误: [图片] 如果你想在其他处使用此能力,可以使用服务端API来做正常HTTP调用,具体访问官方文档 7、查看短信监控图表进入 云开发控制台 > 运营分析 > 监控图表 > 短信监控,即可查看短信监控曲线图、短信发送记录。 [图片] 三、总结短信跳转小程序核心是静态网站中配置的可跳转网页,外部浏览器通过URL Scheme 来实现的,这个方式不适用于微信浏览器,需要使用开放标签才可以URL Scheme的生成是云调用能力,需要是目标小程序的云开发环境的云函数中使用才可以。并且生成的URL Scheme只能是自己小程序的打开链接,不能是任意小程序(和开放标签的任意不一致)短信发送能力的体验是每个有免费配额的环境首月100条,如有超过额度的需求可前往开发者工具-云开发控制台-对应按量付费环境-资源包-短信资源包,进行购买。如当前资源包无法满足需求也可通过云开发 工单 提交申请[图片]短信发送也是云调用能力,需要真实小程序用户调用才可以正常触发,其他方式均报错返回参数错误,出于防止滥用考虑云函数和网页的放置可以不在同一个环境中,只需要保证所属小程序一致即可。(需要保证对应环境ID都能接通)如果你不需要短信能力,可以忽略最后两个步骤CMS配置渠道投放、数据统计可参考官方文档
2021-04-07 - getUnlimited生成小程序码失败,失败原因?
环境id: ilearning-demo-ai23i 代码:try { const result = await cloud.openapi.wxacode.getUnlimited({ "scene": 'a=1' }) return result } catch (err) { return err } 日志返回:{ Error: errCode: -501007 invalid parameters | errMsg: wxacode.getUnlimited:fail missing wxCloudApiToken at callGeneralOpenAPI (/var/user/node_modules/wx-server-sdk/index.js:426:19) at process._tickCallback (internal/process/next_tick.js:68:7) errCode: -501007, errMsg: 'wxacode.getUnlimited:fail missing wxCloudApiToken' }
2021-11-05 - 多人音视频通话接口 wx.joinVoIPChat签名问题?
多人音视频对话 用的是这两个接口wx.joinVoIPChat 和组件 voip-room。 1、在调用过程中一直提示这个错误 joinVoIPChat:fail: -10088, -10, join room failed. errType: 4, errCode: -12020, errMsg: invalid signature, -1 2、这个接口是免费试用的吗 服务端签名(php) $data[] = "自己的appid"; $data[] = 'grop_ooo00_tests';//测试房间 这个groupId 需要生成唯一的 使用这个id 签名的人员进入都会进入一个实时语音房间 $data[] = getNonceStr(); $data[] = time(); sort($data,5); // $st = implode('',$data); //拼接成一个字符串 $hash = hash_hmac('sha256', $st,$session_key); //通过 sha256 接口权限已经开通了 [图片]
2020-11-17 - 小程序码无法跳转到指定页面?
[图片] [图片]
2019-12-04