- [填坑手册]小程序web-view组件实战与踩坑
[图片] 首先,根据官网文档可以知道 只有非个人 的小程序才可以使用web-view组件,如果你的个人开发者,可以跳过这篇文章。 [图片] 一、使用web-view以及它的好处 1、己方账号(第三方)与小程序openId/UnionId的关联绑定,实现免登陆 比如你是某门户网站S,你要识别自己小程序上的用户与网站用户的关系,你可以通过三种方法绑定关系,公众号,小程序源生,小程序web-view内嵌跳转三种方法 2、内嵌H5的富文本,减少重复开发 比如你是门户网站,社区,以往有大量的新闻和帖子,里面带了各种css样式的富文本,小程序源生是无法直接读取的,需要大量转化,这时候直接内嵌这些H5新闻,大大降低开发成本 3、热更新,减少发布审核 某些需要经常更新的内容、公告、活动页,内嵌H5可以减少频繁提交小程序审核 二、小程序功能赋权 为H5提供各种小程序才有的功能,比如录音,扫一扫等。 注意事项 多场景判断,建议使用官方API: wx.miniProgram.getEnv H5唤醒一些小程序API有一定的延时,0.3~1秒 请调用小程序专用的JSSDK,同一个jssdk,但是webview的功能收到限制,和之前微信打开H5有所不同 小程序自动获取加载H5的title H5中iframe的url必须也是业务域名 web-view一定是撑满全屏的,自定义顶部菜单,悬浮的都没用 三、小程序和H5之前的互相通讯 1、 从小程序 ==>> h5 小程序控制H5,可以直接用src路径传参的形式,比如 [代码]<!-- 小程序端 HTML --> <web-view src="//URL?a=param1&b=param2"></web-view> [代码] 避免在链接中带有中文字符,在 iOS 中会有打开白屏的问题,建议加一下 encodeURIComponent。 2、 从 H5 ==>> 小程序 [图片] 这里我们知道bindmessage是小程序用来监听H5的推送的内容,但是这里不大不小的坑!就是它的三个出发场景: 小程序后退:使用接口名 wx.miniProgram.navigateTo,wx.miniProgram.navigateBack,wx.miniProgram.switchTab 等切换小程序页面/场景的API时候都会出发 分享:这个就是当你点分享小程序的时候,会接受到H5之前发送的postMessage 组件销毁,web-view组件销毁,类似 wx.miniProgram.redirectTo 都会触发。 [代码]<!-- 小程序端 HTML --> <web-view bindmessage="handleGetMessage" src="{{openUrl}}"></web-view> [代码] [代码]// 小程序端 JS --> Page({ /** * 页面的初始数据 */ data: { openUrl: "", }, /** * 获取请求数据 */ handleGetMessage: function (e) { console.log(e.detail.data); } }, }) [代码] [代码]<!-- h5端 HTML和JS --> <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> <script> wx.miniProgram.postMessage({ data: { link: "//test.com", title: "一起学习,一起进步" } }); //wx.miniProgram.redirectTo({ // url:"/pages/inner/index?source=123" //}) wx.miniProgram.navigateBack({delta: 1}) </script> [代码] 注意事项 那些H5控制小程序的跳转路径必须是“/”开头,如 “/pages/xxx/xxx”,且路径必须在app.json里有,地址错误的话,有时不报错。 postMessage的json必须是data开始,不然接收不到数据。 [图片] 四、bindmessage接收到消息有3个重要特性(重点) 接收可以是H5之前几分钟前发送postMessage,不一定是即刻发出的。 之前发出的 postMessage的DATA信息会累加,当触发bindmessage接收的时候是一个数组。 [图片] 当bindmessage 再次 接收到数据,之前发送的数据不会被清空,将累加一起返回,获取时要判断好数组的角标 [图片] 五、Tips 1、在IDE工具中如何调试H5 [图片] 可以在 web-view 组件上通过右键 - 调试,打开 web-view 组件的调试。 2、内嵌H5缓存问题 web-view加载的H5具有很重的缓存,如果需要调试,可以通过在url后面加时间戳的方式解决。 3、小程序关闭,H5背景音乐仍然在播放问题 小程序已经关闭,但是H5自带的背景音乐仍然在手机后台播放的问题。这里可以利用一个属性: visibilitychange:页面可见性状态 简单的说,浏览器标签页被隐藏或显示的时候会触发visibilitychange事件。 [代码]var statusBeforeHide = true; //初始化页面的状态 var music = document.getElementById("xxx"); // 更改音乐播放状态 function setChangeMusic() { if (document[hiddenProperty]) { // 页面隐藏 if (statusBeforeHide) { music.pause(); // 暂停 } } else { // 页面显示 if (statusBeforeHide) { music.play() //如果statusBeforeHide是true, 继续播放 } } } let hiddenProperty = ('hidden' in document) ? 'hidden' : ('webkitHidden' in document) ? 'webkitHidden' : ('mozHidden' in document) ? 'mozHidden' : null; if (hiddenProperty) { let visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange'); let onVisibilityChange = () => { //console.log('visibilityChange'); setChangeMusic(); }; document.addEventListener(visibilityChangeEvent, onVisibilityChange); } else { console.log("不支持这个api"); } [代码] 总结,web-view还是非常实用的组件,且用且珍惜~ 往期回顾: 小程序自定义头部导航栏“完美”解决方案 小程序Canvas生成海报(一) 小程序新版订阅消息+云开发实战与跳坑
2021-09-13 - 技术选型:关于小程序webview中内嵌H5的重要提示
虽然小程序提供了不少原生能力,但有时我们想在小程序中使用web生态的能力时或者对接第三方活动页面时,会使用webview组件来承载网页,但这里有一些小程序中关于webview的重要提示,在了解之后可以进一步决定某些业务场景下还是否应该使用H5内嵌小程序实现业务。 提示: 1.小程序中的webview中的H5 ,需要添加web-view可信域名,需要在对应的网页的部署根目录中添加相应校验文件,如果对接的是第三方无法添加校验文件的,则无法打开。 2.小程序中的webview如需使用微信相关能力,须配置jssdk,须提供微信公众号账号主体,并按文档进行正确的页面授权和配置(需后端配合)。 3.小程序中的webview有较强的缓存(社区相关问题较多),需要用户手动清除通用存储并删除小程序才可以,如需H5每次部署后,不再有缓存,可寻找后端修改Nginx配置为无缓存 4.小程序中的webview中无论使用原生input拍照或jssdk提供的拍照能力,当IOS内存不足总会出现H5重新加载的情况,当前无解决方案(社区相关问题较多)。 参考JS-SDK说明文档web-view | 微信开放文档微信h5页面,调用拍照后,页面刷新 | 微信开放社区web-view嵌入的h5页面缓存严重,如何清除? | 微信开放社区
2023-11-27 - web-view嵌套h5页面的小程序应该使用哪个微信支付方式?
支付方式
03-19 - 请问web-view内嵌的小程序怎么做微信支付啊,有谁知道的嘛?
微信支付
03-11 - 小程序使用微信支付,钱到哪里?如何退款提现,多久到账?一文读懂微信支付!
关于“钱”的事情,我相信是所有商家都很关注的事情!今天我们就整理了一些关于微信支付,大家最关心,和平时询问的最多问题,进行讲解!顾客通过小程序进行下单,钱是到哪里?如果申请退款,需要多久退款?这一系列的问题,我们一文搞定! 如何登录微信支付? 打开网址(https://pay.weixin.qq.com/)可通过扫码或者账号密码进行登录,登录成功后,选择绑定了小程序的商户号点击进入! [图片] 小程序到账的资金去哪里了? 1、钱到哪儿? 一般我们申请微信支付,需要绑定银行卡,主体为个体工商户时使用的是法人个人银行卡或对公账户;主体为企业时,使用的是对公银行账户!客户使用小程序微信支付的钱,会实时到商家自己的商户号里,默认会在早上8点自动提现至商户号绑定的提现账户中! [图片] 2、如何查看每日收支明细? 在我们系统后台,点击财务-微信对账,即可查看每日每笔订单的金额、类型和账单状态! [图片] 3、如何接收微信支付消息通知? 启用“微信支付商家助手”-选择登录收款商户号-收款记录,商家可以打开公众号通知或收款语音播报,这样就可以接收来自微信支付的消息通知,帮助商家更好地使用微信支付做生意! [图片] 4、到账如何提醒通知? 客户下单后,商家会通过微信支付后台、微信支付商户助手等方式收取交易概括,显示金额、日期等交易信息,支持语音播报! [图片] 如何修改结算账户? 1、登录来一间后台-设置-支付管理-支付列表 [图片] 2、点击修改结算账户,即可修改 [图片] 结算需要多久到账? 1、结算方式 结算方式分两种,一种是自动结算,一种是手动结算(【交易中心】-【提现/转出】-输入提现金额,进行提现)!一般我们默认是自动结算,自动提现规则显示:一般是T+1(默认是次日早上八点),次日发起自动提现! [图片] 2、自动结算 关闭设置自动结算内容:产品中心 - 我的产品 - 资金解决方案 - 自动提现 [图片] 退款需要多久?如何查询退款记录? 1、退款操作 客户下单后,选择退款,商家可在来一间系统后台,查看退款详情,金额返回时间根据支付方式而定。 [图片] 2、退款查询 来一间系统后台 > 财务>退款处理。查看实时退款详情,退款状态 [图片] 订单取消退不了款或退款失败怎么办? 1、如果订单是刚刚取消的,系统会有一个处理流程,稍等几分钟,系统会自动退还款项。 2、如果订单已经取消很久,几天过去还是没有退款的情况。 首先确认账户里面有没有余额,如果没有,需要充值余额。再重新申请退款【微信对账】-【退款处理】-重新退款 [图片] 如何查看支付商户号以及管理员? 1、当忘记自己小程序的商户号,以及不记得之前的管理员是谁时。可以登录系统后台在【设置】-【支付管理】-【支付列表】中查看。 点击资料详情,选择【超级管理员】可以查看管理员是谁。 [图片] 如果需要变更,登录商户支付后台去进行修改【账户中心】-【个人信息】-修改 [图片] 如果需要多个人管理商户号,在商户支付后台【账户中心】-【账户设置】-【员工账号管理】相关权限进行新增配置 [图片] 小程序提示交易受限怎么办? 提示交易受限:当微信支付失败提示“交易受限”时,我们需要解除支付限制,具体路径是:微信公众号关注微信支付——在帮助中心点击交易不了——点击解除支付限制。 微信支付提现交易停滞怎么办? 这种情况通常是因为小程序太长时间没有使用,长时间没有流水所导致的!可以登录“微信支付商家助手”小程序(操作路径:“微信支付商家助手”小程序—>风险处理—>违约处理记录)处理“交易停滞”单据。 [图片] 是否支持支付宝等其他支付方式? 微信小程序是基于微信生态开发的内容,目前是无法使用支付宝或其他支付方式的,可以使用微信支付,或者微信绑定信用卡、银行卡等卡片,同时支持跳转云闪付app进行支付! 以上就是我们整理的关于微信支付常见问题,希望能够帮助各位商家更好地了解微信支付~如果大家在使用过程中遇到文中没有提及到的问题,欢迎大家咨询我们,我们会及时为大家解答疑惑!
2023-11-28 - 小程序如何换绑管理员?
小程序在注册流程中绑定管理员,在使用过程中如需更换管理员,可在小程序后台换绑,流程如下: 一、登录小程序后台,进入“用户身份-管理员”,点击修改 [图片] 二、原管理员验证身份 个人主体帐号:原管理员扫码验证身份 [图片] 非个人主体帐号:填写原管理员身份证号,并使用原管理员绑定的微信号扫码验证身份 [图片] 三、填写新管理员信息,完成绑定 个人主体帐号:管理员姓名与身份证号必须与主体一致,不可修改。可更换绑定的微信号。要求管理员微信号绑定了银行卡且与主体身份一致。 [图片] 非个人主体帐号:可重新填写新管理员姓名、身份证号、手机,并绑定微信号。 [图片]
2020-03-18 - 视频号审核人员极其不作为,只会应付差事!!!
事情是这样的,最近ai绘画以及ai制作的视频非常火爆,就想着在视频号发布视频。 于是就创建了账号 [ 甜妹AI ],发布的内容是写实风格的ai美女跳舞视频,刚开始一切都非常顺利,几天就涨粉3000+,并且发布的内容多次收到平台的推荐。 这种日子没过多久,就收到了 出镜人身份验证的通知。由于视频中的出镜人物为ai,我按照要求 录制视频 说出指定内容以及视频内容来源,并提交了我本人的身份证 以及 创作期间的截图、证明材料能内容。 最后等来的结果是 出镜人身份验证 未通过。也没有任何的申诉入口了。然后我就在视频号内的 反馈与帮助内 尝试联系客服。得到的回复依旧是提交的材料无法验证出镜人身份。 到这里我依旧没有选择放弃,我尝试在微信客服 公众号 联系人工,联系上人工之后 客服让我提交相应的证明材料,传到百度网盘发给他,他再帮忙提交给审核,我提交了当时制作时候的整个工程文件以及当时创作过程中我保留的一些截图 ,大小近2G,内含了ai逐帧绘制的原图,结果在第二天又收到了同样的回复,最主要的是,我分享的网盘 他既没有下载 也没有浏览,百度网盘可以看到分享连接的 浏览次数 下载次数 以及 转存次数。 [图片][图片][图片] [图片][图片] 前前后后折腾了好几天,现在账号依旧处于限制状态,审核难道看不出来ai与真人吗,提交材料根本不看,多次联系得到的回复内容也是一模一样,没有丝毫的差别。 无语。
01-18 - 小程序备案周期与客服沟通结果
兄弟们,经过我三天的客服接入等待,目前大概知道了流程,可以参考下。 第一步,平台审核1-2工作日,这个应该都是正常的。 第二部,提交管局收到短信等待管局审核也是正常 问题来了,出在了 1-2中间!!!! 初审只是你的信息过了审核,但是并没有提交到管局!!! 现在大部分都是等待提交管局审核。 所以 慢慢等就行了。 [图片] [图片] [图片] [图片]
2023-09-22 - 微信开放平台,更换主体之后,客户的unionid是否会发生改变?
微信开放平台,更换主体之后,客户的unionid是否会发生改变?
2023-09-20 - 个人小程序备案流程
根据官方专员的社区回复,“新增(未上架)小程序9月4日开放备案入口;存量(已上架)小程序9月19日开放备案入口” 我找了自己一个未上架的小程序,操作了一遍备案流程。 整理如下。 一,备案入口 小程序首页就可以看到。 [图片] 二,去备案 点按钮后,会弹出二维码窗口,要求管理员微信扫码确认。 1,验证备案类型,会让你选择地区,然后上传身份证。 [图片] 2,填写主体信息 [图片] 3,填写小程序信息 [图片] 这一步,需要管理员手机扫码,微信授权,以及人脸核验。 [图片][图片][图片] 4,上传材料 这一步的材料,承诺书是系统默认生成的,不用自己写。你也可以预览承诺书。 [图片] 5,然后就是材料预览 自己复核一下填写是否有问题。,没问题就提交。 6,备案完成。 进入平台审核中状态。 [图片] [图片]
2023-09-04 - 适配新版获取昵称头像、隐私保护协议接口
蛮记录一下,虽然官方新版获取昵称、头像的api上线已久 目前的做法是封装了一个auth.js用来专门获取用户信息,想要实现的效果是 1、若用户没有授权过昵称、头像,则弹出授权弹窗 2、若用户有授权过,则直接返回昵称头像等其他用户信息 3、允许更新用户信息,也就是不管之前有没有授权过昵称头像,都会再次弹出授权弹窗 4、支持旧版api,主要是考虑到部分用户的微信客户端可能长期未更新,不支持新版api 另外也适配了最近的隐私接口功能。因为获取昵称组件是一个input,不会触发wx.onNeedPrivacyAuthorization,所以这里是给input加了一个catch:touchstart="requirePrivacyAuthorize",在requirePrivacyAuthorize里面调用wx.requirePrivacyAuthorize,从而触发wx.onNeedPrivacyAuthorization。具体代码可以看下improveUserProfile.wxml、improveUserProfile.js 主要代码如下,核心方法 authProcess: auth.js const api = require('/api.js'); const file = require("/file.js"); /** * 保存或者更新用户信息到云端 * @param userInfo * @returns {Promise<unknown>} * @private */ const _saveOrUpdateUserInfo = function (userInfo) { return new Promise((resolve, reject) => { api.router.callCloudUserCenterFunction("UserInfoHandler/saveOrUpdateUserInfo", userInfo, res => { console.log("保存用户信息到云端完毕=>", JSON.stringify(res)); resolve(res); }, res => { console.error("保存用户信息到云端失败=>", res); reject(); } ); }); }; /** * 发起后端请求前先获取用户信息 * 使用新版获取用户信息接口 * @param app * @param basePage * @param update 等于true时表示更新用户信息 * @returns {Promise<any>} */ const authProcess = function (app, basePage, update) { return new Promise(function (resolve, reject) { if (update) { console.log("更新用户信息,重新授权"); _getUserProfile(app, basePage).then(res => resolve(res)).catch(res => reject(res)); } else if (!app.globalData.userInfo.authEver) { console.log("没有用户授权信息,开始授权"); _getUserProfile(app, basePage).then(res => resolve(res)).catch(res => reject(res)); } else { console.log("用户有授权过,无需重新授权"); resolve(app.globalData.userInfo); } }); }; /** * 使用新版接口获取用户信息 * 兼容新版头像昵称填写能力 * @param app * @param basePage * @private */ const _getUserProfile = function (app, basePage) { return new Promise(function (resolve, reject) { const supportChooseAvatar = wx.canIUse('button.open-type.chooseAvatar'); if (supportChooseAvatar) { console.warn("支持头像昵称填写能力"); const improveUserProfile = basePage.improveUserProfile || basePage.selectComponent('#improveUserProfile'); const toast = basePage.toast || basePage.selectComponent('#toast'); if (!improveUserProfile) { console.error("完善用户资料组件缺失,无法继续"); reject("完善用户资料组件缺失"); } else if (!toast) { console.error("toast组件缺失,无法继续"); reject("toast组件缺失"); } else { improveUserProfile.show(({nickName, avatarUrl}) => { const {openid, avatarFileId: oldAvatarFileId} = app.globalData.userInfo; const targetPath = _getHeaderPath(openid); console.log("上传获取到的临时头像:", avatarUrl); toast.showLoadingToast({text: "开始上传头像"}); file.uploadCloudFile(targetPath, avatarUrl).then(fileId => { file.getTempFileURL(fileId).then(fileUrl => { toast.hideLoadingToast(); _getUserProfileCallback(app, basePage, { userInfo: { nickName, avatarUrl: fileUrl, avatarFileId: fileId } }).then(userInfo => resolve(userInfo)); if (oldAvatarFileId) { console.log("删除旧的云储存头像文件", oldAvatarFileId); file.deleteCloudFile([oldAvatarFileId]).then(); } }).catch(res => { toast.hideLoadingToast(); toast.showToast({text: "保存头像链接失败"}); reject("保存头像链接失败"); }) }).catch(res => { toast.hideLoadingToast(); toast.showToast({text: "头像上传失败"}); reject("头像上传失败"); }); file.onUploadProgressUpdate(progress => { toast.showLoadingToast({text: "上传了" + progress + "%"}); }); }, res => { console.log("用户拒绝授权信息:", res); reject(res); }); //reject("需要使用新api授权用户信息"); } } else { console.warn("不支持头像昵称填写能力"); api.getUserProfile( res => { _getUserProfileCallback(app, basePage, res).then(userInfo => resolve(userInfo)); }, res => { console.error("获取用户信息失败:", res); reject(res); } ); //reject("需要使用旧api授权用户信息"); } }); }; /** * 获取到用户信息的后续逻辑 * @param app * @param basePage * @param result * @returns {Promise<unknown>} * @private */ const _getUserProfileCallback = (app, basePage, result) => { return new Promise(function (resolve, reject) { console.log("获取用户信息结果:", JSON.stringify(result)); let {userInfo} = result; //authEver=true时后端做更新操作,false时新增操作 const {authEver} = app.globalData.userInfo || {}; userInfo = Object.assign(app.globalData.userInfo || {}, userInfo, {ready: true, authEver: true}); basePage.setData({ userInfo, }, () => { _saveOrUpdateUserInfo(Object.assign({}, userInfo, {authEver})).then(res => { console.log("用户信息保存完毕,更新后的用户信息:", res); const result = res.result; if (result.success) { userInfo = Object.assign(app.globalData.userInfo || {}, userInfo); basePage.setData({ userInfo, }); } resolve(userInfo); }).catch(res => reject()); }); }); } /** * 获取头像地址 * 不同项目可以自行调整 * @param openid * @returns {string} * @private */ const _getHeaderPath = (openid) => { return "images/" + openid + "/avatarUrl_" + new Date().getTime() + ".webp"; } module.exports = { setUserInfo2Data, authProcess }; 昵称、头像授权弹窗自定义组件 improveUserProfile.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="improve-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo"> <image class="icon" src="xxxx"></image> </view> <view class="mini-name">恋爱小清单 申请</view> </view> <view class="tips"> <view class="tip">获取你的昵称、头像</view> <view class="use-for">用于个人中心、共享清单、恋人圈以及分享时展示</view> </view> <form catchsubmit="saveUserInfo"> <view class="info-item flex-center"> <view class="label">头像</view> <view class="input-content flex-full more"> <button class="reset-btn flex-column-left" style="height:100%" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar"> <view class="choose-avatar before" wx:if="{{!avatarUrl}}"> <image class="icon" src="/images/me/choose-avatar.png"></image> </view> <image class="choose-avatar after" src="{{avatarUrl}}" wx:else></image> </button> </view> </view> <view class="info-item flex-center no-border"> <view class="label">昵称</view> <input class="input-content flex-full more" model:value="{{nickName}}" type="nickname" placeholder="输入昵称" placeholder-class="edit-placeholder" focus="{{nickNameFocus}}" always-embed="{{false}}" adjust-position="{{true}}" hold-keyboard="{{false}}" name="nickName" catch:touchstart="requirePrivacyAuthorize"></input> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" form-type="reset" bindtap="close">拒绝</button> <button class="save reset-btn" form-type="submit">保存</button> </view> <!--隐藏参数区域开始--> <input disabled style="display:none;" name="avatarUrl" value="{{avatarUrl}}"></input> <!--隐藏参数区域结束--> </form> </view> </view> <!--toast--> <toast id="toast"></toast> improveUserProfile.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 5000; transition: opacity .3s; } .improve-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 5100; background-color: #fff; box-sizing: border-box; padding-top: 60rpx; padding-left: 50rpx; padding-right: 50rpx; border-radius: 30rpx 30rpx 0 0; transform: translateY(100%); transition: transform .3s, bottom .3s; } .cover-fade-in{ opacity: 0.7; } .slade-in{ transform: translateY(0); bottom: 0rpx; } .improve-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .improve-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .improve-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .improve-box .tips{ margin-bottom: 20rpx; } .improve-box .tips .tip{ color: #121212; font-size: 34rpx; font-weight: 500; margin-bottom: 12rpx; } .improve-box .tips .use-for{ color: #606266; font-size: 28rpx; margin-bottom: 20rpx; } .improve-box .info-item{ /*margin-bottom: 30rpx;*/ border-bottom: 1rpx solid #f0f0f0; } .improve-box .label{ margin-right: 40rpx; font-size: 32rpx; color: #404040; padding: 30rpx 0 30rpx; } .improve-box .choose-avatar.after{ width: 48rpx; height: 48rpx; display: block; border-radius: 10rpx; } .improve-box .choose-avatar.before{ background-color: #f0f0f0; width: 48rpx; height: 48rpx; display: block; border-radius: 10rpx; padding: 8rpx; box-sizing: border-box; } .improve-box .choose-avatar.before .icon{ width: 100%; height: 100%; display: block; } .improve-box .input-content{ width: 100%; font-size: 32rpx; min-height: 80rpx; height: 80rpx; line-height: 80rpx; border-radius: 10rpx; padding-left: 20rpx; box-sizing: border-box; /*border-bottom: 1rpx solid #dcdee1;*/ position: relative; padding-right: 30rpx; } .edit-placeholder{ color: #c7c7cc; font-size: 32rpx; } .improve-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .improve-box .buttons .save{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #fff; /*background:#fff linear-gradient(90deg,rgba(246, 120, 121,.9) 10%, rgb(246, 120, 121));*/ background-color: #07c160; margin-left: -50rpx; } .improve-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } .more:after{ content: " "; transform: rotate(45deg); height: 14rpx; width: 14rpx; border-width: 2rpx 2rpx 0 0; border-color: #b2b2b2; border-style: solid; position: absolute; margin-top: -8rpx; right: 4rpx; top: 50%; transition: transform .3s; } improveUserProfile.js //获取应用实例 const app = getApp(); const api = require('../../utils/api.js'); const tabbar = require('../../utils/tabbar.js'); Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 avatarUrl: "", nickNameFocus: false, }, attached() { this.toast = this.selectComponent('#toast'); }, observers: { 'show': function (show) { if (!show) { console.log("获取用户昵称头像弹窗关闭,昵称输入框状态复位"); this.setData({ nickNameFocus: false }) } } }, /** * 组件的方法列表 */ methods: { show(successCallback, failCallback) { const device = app.getSystemInfo(); this.setData({ show: true, device, successCallback, failCallback }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); }) tabbar.hideTab(this); }, hide() { this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) tabbar.showTab(that); }, 300) }) }, close() { const {failCallback} = this.data; typeof failCallback == 'function' && failCallback("用户拒绝授权"); this.hide(); }, onChooseAvatar(e) { console.log("选择头像:", e); const {avatarUrl} = e.detail; this.setData({ avatarUrl }) }, /** * 模拟隐私接口调用,并触发隐私弹窗逻辑 */ requirePrivacyAuthorize() { if (wx.requirePrivacyAuthorize) { console.warn("当前基础库支持api wx.requirePrivacyAuthorize"); wx.requirePrivacyAuthorize({ success: res => { console.log('用户同意了隐私协议 或 无需用户同意隐私协议'); //用户同意隐私协议后给昵称输入框聚焦 this.setData({ nickNameFocus: true }) }, fail: res => { console.log('用户拒绝了隐私协议'); } }) } else { console.warn("当前基础库不支持api wx.requirePrivacyAuthorize"); this.setData({ nickNameFocus: true }) } }, saveUserInfo(e) { console.log("保存用户资料:", e); const {nickName, avatarUrl} = e.detail.value; if (!avatarUrl) { this.toast.showToast({text: "请选择头像"}); return; } if (!nickName) { this.toast.showToast({text: "请输入昵称"}); return; } const {successCallback} = this.data; typeof successCallback == 'function' && successCallback({nickName, avatarUrl}); this.hide(); } } }); 弹窗效果 [图片]
2023-08-30 - 隐私协议授权的一种解决思路
现状 登录页使用了获取手机号接口,登录页为单独的页面(非弹窗),所有登录需求都会跳转到此页。 [图片] 分析 小程序提供两种接入方式:一种是主动查询授权状态,如果状态为未授权则进入授权流程,已授权则无需处理;另一种是被动监听何时需要授权,触发事件后进入授权流程,未触发则表示已授权。在登录的流程中,主动查询就是先发起授权后点击登录按钮,被动监听就是先点击登录按钮后发起授权。个人感觉,主动查询的方式先告知用户需要采集隐私数据,比较符合用户习惯,而被动监听的方式事前未告知用户需要授权,且打断了登录的流程,用户体验不佳,因此采取主动查询的方式。 官方的示例采用了弹窗授权的方式,但无论是在页面上覆盖一个模拟弹窗组件,还是使用page-container,控制弹窗都不能像VUE那样优雅,复用也不方便。鉴于登录页也是单独的页面,考虑到以后可能还有其它页面需要授权,因此授权页也采用单独的页面(PS.skyline的自定义路由可以很优雅地实现弹窗,但目前还没有适配,暂不考虑,之后对授权页进行改造即可)。 思路 在登录页的onLoad中主动查询授权状态,如果状态为未授权则调用授权接口跳转到授权页,授权页回调授权状态,如果授权失败则登录也失败。 [图片] 实现 [代码]// 封装的登录接口 function requestLogin() { return new Promise(async (resolve, reject) => { wx.navigateTo({ url: '/pages/user/user-login/user-login', // 登录页 events: { login: (loginStatus) => { // 登录状态回调 if (loginStatus) { // 成功 resolve() } else { // 失败 reject(new Error('login fail')) } } }, fail: () => reject(new Error('navigate fail')) }) }) } // 封装的授权接口 function requestProvicy() { return new Promise(async (resolve, reject) => { wx.getPrivacySetting({ success: (res) => { if (res.needAuthorization) { // 需要授权 wx.navigateTo({ url: '/pages/user/user-login/user-login', // 登录页 events: { privacy: (privacyStatus) => { // 授权状态回调 if (loginStatus) { // 成功 resolve() } else { // 失败 reject(new Error('privacy fail')) } } }, fail: () => reject(new Error('navigate fail')) }) } else { // 无需授权 resolve() } }, fail: () => reject(new Error('get privacy fail')) }) }) } // 第一步:调用封装的登录接口 // 需要登录才能进入的页面 Page({ onLoad: async function(options) { try { await wx.$auth.requestLogin() // 登录失败抛出错误 } catch { return wx.$router.back() // 返回上一页 } // 业务代码 } }) // 第二步:获取手机号登录 // 登录页 <!-- wxml > <button open-type="getPhoneNumber" bindgetphonenumber="handleGetPhoneNumber">获取手机号</button> Page({ handleGetPhoneNumber: async function(e) { const code = e.detail.code // 手机号code // 调用自己后台的登录接口 this._loginStatus = true // 登录成功状态,在onUnload回调 wx.$router.back() // 返回上一页 }, onLoad: async function() { try { await wx.$auth.requestProvicy() // 授权失败抛出错误 } catch { return wx.$router.back() // 返回上一页 } } onUnload: function() { const eventChannel = this.getOpenerEventChannel() if (!eventChannel) return eventChannel.emit('login', !!this._loginStatus) // 回调登录结果 } }) // 第三步:授权 // 授权页 <!-- wxml > <button id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">我已阅读并同意隐私协议</button> Page({ handleAgreePrivacyAuthorization: async function() { this._privacyStatus = true // 授权成功状态,在onUnload回调 wx.$router.back() // 返回上一页 }, onUnload: function() { const eventChannel = this.getOpenerEventChannel() if (!eventChannel) return eventChannel.emit('privacy', !!this._privacyStatus) // 回调登录结果 } }) [代码] 最终效果 未登录的状态下打开一个需要登录才能使用的页面 [图片] 可以进入小程序实际体验一下,欢迎各位一起讨论 [图片]
2023-09-01 - 关于新版隐私协议接口wx.onNeedPrivacyAuthorization的适配解读以及实现代码
官方公告地址: https://developers.weixin.qq.com/community/develop/doc/00042e3ef54940ce8520e38db61801 目前,开发工具或者体验版的小程序,调试基础库如果是2.33.0及以上就得适配了,线上版本9月15日之后生效,所以这之前需要尽快改完,发布一版,否则到了9月15号之后 线上就会生效报错了。 其实改起来也很简单,以下是实现步骤和代码: 1、首先看一下这个网址,里边包含涉及到的隐私的接口,这些接口都要适配一下 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html [图片] 在以上接口用到的页面,需要画一下类似上边的弹窗(这个弹窗可以全局定义个组件,方便多个页面共用),然后里边蓝字可以点击后调用wx.openPrivacyContract(Object object)接口即可,会自动跳转打开隐私协议页面。 拒绝按钮可以加一个点击事件,然后在事件里这样写 [图片] 同意按钮比较特殊,布局需要用button这样写,记得给button加一个Id [图片] 然后在handleAgreePrivacyAuthorization里就可以获取到点击事件,这样写 [图片] 2、最后需要在onLoad或者onShow里加上以下监听代码,在这里边让自定义的隐私弹窗显示出来即可。 [图片] 以上代码加上就可以了,如果业务逻辑用到了需要判断是否授权过,可以加上 wx.getPrivacySetting(Object object)去获取是否授权过,用不到可以不加这个判断。
2023-08-16 - 自从官方公布“获取手机号收费标准”,我还有的困惑。你还有哪些困惑?一起交流交流
首先,手机号是微信用户信息,把用户信息外流还收钱是很很难看的态度。至少我认识的一群开发者都是骂声一片,想知道为啥要还要么干。 其次真的要按腾讯官方的改,还有很多定义不清楚的地方,开发无从下手。具体我收到的和自己发现还不清楚的如下: 首先社区一位资深从业者,给出了一些犀利的问题: 1.充值购买次数后会,如果小程序被封禁了,充值的金额是否可退款。 2.购买数量是否支持按量付费? 3.如果次数用完了,未购买新的次数,用户端的表现是什么? 该问题原文:https://developers.weixin.qq.com/community/develop/doc/00088205530478420fff168c85b000 其次的我的疑问: 1、能不能手机上付钱,很多用户不会用微信小程序管理后台,支付可更方便。 2、额度实时手机查看,能不能有预警机制,和统计,让购买者有个预期。 3、这个一份钱都没便宜 套餐个p?是不是对套餐有啥误会?产品要不在考虑下在发布? [图片] 4、文档这里,旧的可以用,那么旧的是走快速手机号额度还是实时的额度呢?不升级如何扣除额度问题怎么理解? https://developers.weixin.qq.com/community/develop/doc/000686660085681a21ff380c056400 除了上文你还有哪些困惑,欢迎评论交流
2023-06-28 - 手机号快速验证组件要收费了,那么小程序代开发平台怎么帮客户把这个费用支付上?
手机号快速验证组件要收费了,如果是自己开发的小程序该交钱交钱,但是我们这种代开发小程序平台,有很多的客户,他们不懂如何支付费用,我们怎么帮客户把这个费用支付了呢?难道是挨个登录每一个客户的小程序后台给他们充钱么?有谁知道如何解决。感谢大佬们回复。
2023-06-27 - 微信公众平台用户信息相关接口调整公告
微信公众平台为开发者提供了用户授权登录功能及相关接口,以便开发者为用户提供便捷的使用体验。 根据相关法律法规,为进一步规范开发者调用用户信息相关接口或功能,保障用户合法权益,平台将对用户信息相关功能及接口进行调整,具体如下: 一、相关接口调整: 1、小程序与小游戏获取用户信息相关接口:不再返回用户性别及地区信息; 能力参考:小程序用户信息 、小游戏用户信息; 2、 公众号用户信息获取接口:不再返回用户性别及地区信息; 能力参考:公众号用户信息; 3、 Open平台授权接口:包括App授权登录、公众号H5授权登录、网站扫码授权登录,不再返回用户性别及地区信息; 能力参考:移动应用、网站应用、第三方平台; 本次改造调整生效后,所述涉及字段返回值将按如下规则生效: [图片] *注:字段名均保持不变,小程序与小游戏获取用户信息接口“用户性别”字段名为gender;Open 平台授权接口“用户性别”字段名为 sex 二、相关功能调整: 1、 公众号个性化菜单功能:不再提供基于性别/地区设置个性化菜单的能力; 能力参考:个性化菜单接口; 2、公众号后台粉丝列表:粉丝列表等不再展示用户的性别信息; 涉及功能页面:公众平台帐号管理后台-用户管理/留言管理/赞赏/视频弹幕/消息; 3、服务号粉丝列表筛选:不再提供基于地区筛选粉丝的能力; 涉及功能页面:公众平台帐号管理后台-对话能力-客户管理。 平台预计10月20日完成调整,请开发者及时进行调整适配,避免影响相关服务及用户体验。 微信团队 2021年09月26日
2023-09-26 - 修改微信用户信息后,getUserInfo信息什么时候更新?
更新时间:2小时
2020-09-04 - 小程序web-view嵌入h5页面,是否可以分享到朋友圈?
小程序web-view嵌入h5页面,是否可以分享到朋友圈
2021-07-14 - 关于小程序使用webview标题栏动态展示问题开发实践
关于小程序使用webview标题栏动态展示问题开发实践 问题描述 使用webview开发的小程序,小程序的标题栏根据webview中H5的标题展示,随着H5 页面路由不同,页面标题展示不同 H5使用VUE开发 [图片] [图片] [图片] [图片] [图片] 问题总结 小程序可以正常展示H5 document 的title 实现 方式1 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>标题写这里</title> </head> <body> </body> </html> 方式2 document.title 备注 开发者工具展示时有问题,只展示首次页面的document的title,后面title变更不会体现到小程序的标题上来 真机正常的 参考文章 小程序web-view页面标题不会根据H5设置的标题做出改变?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00048a2ab7457067af0deaeac56c00 关于小程序使用webview标题栏动态展示问题开发实践
2023-05-25 - webView报错the permission value is offline verifying
the permission value is offline verifying这个错误是因为config没有正确执行,或者是调用的JSAPI没有传入config的jsApiList参数中。建议按如下顺序检查: 确认config正确通过。 如果是在页面加载好时就调用了JSAPI,则必须写在wx.ready的回调中。 确认config的jsApiList参数包含了这个JSAPI。 可参考:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#%E9%99%84%E5%BD%951-JS-SDK%E4%BD%BF%E7%94%A8%E6%9D%83%E9%99%90%E7%AD%BE%E5%90%8D%E7%AE%97%E6%B3%95
2019-09-24 - 单页模式的浏览量会被统计到we分析吗?
小程序分享到朋友圈后的单页模式,当用户浏览阅读后,we分析里面会有统计吗?能按人去重吗
2023-02-16 - 社区每周 | 关于规范公众号模板消息的公告及上周问题反馈(3.20-3.24)
各位微信开发者: 以下是关于规范公众号模板消息的公告及上周我们在社区收到的问题反馈的处理进度,希望与大家一同打造更好的小程序生态! 关于规范公众号模板消息的公告 为了减少不恰当的模板消息对用户带来的困扰,平台对公众号模板消息进行如下调整: 1)去除自定义颜色、表情符号;(表情符号由替换为 “□” 改为直接去除) 2)去除尾部/备注内容; 3)去除首行内容。 [图片] 此措施不会影响模板消息下发,自 2023 年 5 月 4 日起,当公众号进行模板消息下发时,平台会自动去掉相关内容。 如有服务通知类模板需求,建议前往 微信公众平台,在「模板消息 > 模板库 > 类目模板库」新增对应模板。 详情可查看原公告:《关于规范公众号模板消息的公告》、《关于规范公众号模板消息的再次公告》 上周问题反馈与处理进度(3.13-3.17) 修复中的问题调用 chooseMedia 接口时,选择顺序和发送顺序出现不一致的问题 查看详情 调用 wx.onAudioInterruptionBegin 接口时,iOS 系统出现监听失败的问题 查看详情 微信团队 2023.04.03
2023-09-18 - 关于规范公众号模板消息的公告
公众号的模板消息能力旨在方便开发者为用户提供服务凭证通知。近期平台发现,部分公众号下发的模板消息涉及包括但不限于商品营销、服务推广等内容。且利用颜色/表情符号诱导用户查看、点击。上述行为损害了用户消息接收体验,给用户带来极大困扰,引起大量用户投诉。 违规案例1:消息主要为服务凭证通知内容,但尾部/备注字段下发与服务凭证无关的营销推广内容,并结合自定义颜色、emoji表情符号诱导用户点击。 [图片] 违规案例2:利用正常服务模板下发纯营销推广内容,实际内容与模板服务场景不一致,并结合自定义颜色、emoji表情符号诱导用户点击。 [图片] 为进一步规范模板消息下发行为,给用户提供更优质的消息接收体验。2023年3月30日起,平台将会进一步规范模板消息内容,规则如下: 1、限制部分消息内容展示(运营者可暂不修改,继续使用,发送时会自动过滤相关内容展示。) 1)尾部/备注字段不支持展示; 2)自定义颜色内容展示为默认颜色; 3)emoji表情符号展示为“□”。 [图片] 2、拦截纯营销类消息下发 对于纯为营销推广的模板消息,平台将会进行拦截。 3、加大对发送纯营销类消息帐号的限制力度 平台除了拦截纯营销类消息外,还会对发送该类消息的帐号采取限制添加模板、限制模板跳转等能力封禁措施。 以上规范生效时间如下: 即日起,平台会逐步落实纯营销类消息的拦截措施; 自2023年3月30日开始,去除尾部/备注内容、自定义颜色、表情符号。 模板消息为服务场景下产生的即时服务通知,希望开发者们合理、正确使用模板消息相关接口能力,优化内容展示形态,避免下发营销推广内容,共同维护模板消息良好的消息生态,共同提升用户模板消息相关的服务体验。
2023-09-18 - 国外手机号授权登录国内小程序失败?
[图片][图片]
2019-08-22 - 小程序开通云闪付支付后常见问题Q&A
看完记得点赞收藏好评一键三连 Q1:小程序如何开通云闪付支付功能? A1:超级管理员扫码登录商户平台「点我访问」,点击「产品中心」->「开发配置」->「支付方式配置」->「开通“云闪付付款”功能」 小提示:支付方式配置在页面最底部 [图片] Q2:开通“云闪付付款”功能后小程序需要做开发对接吗? A2:不需要,开通“云闪付付款”功能后,商户号绑定的小程序默认就支持云闪付付款了,无需做任何开发对接,原有系统无需调整。 Q3:用户使用云闪付付款,商户收款手续费是多少? A3:与商户号原费率保持一致,举例:用户在商户A小程序使用云闪付支付100元,商户A当前费率为0.6%,则应收手续费应收为1000.6%=0.6元。 Q4:用户使用云闪付付款,商户收款资金在什么时间结算到商家银行卡? A4:用户使用云闪付付款实时到微信支付商户号,根据商户号原结算周期结算。举例:用户在商户A小程序使用云闪付支付100元,商户A当前结算周期为T+1并开通自动提现功能,则在用户付款时间后一个工作日自动提现到商户银行卡。 Q5:用户使用云闪付付款的订单应如何进行查询? A5:1.在商户后台通过交易账单中的「付款银行」字段来检索对应的订单 扫码登录商户平台-交易中心-交易账单-下载交易账单,「付款银行字段」取值为:UPQUICKPASS_CREDIT和UPQUICKPASS_DEBIT的,即为用户使用云闪付付款的交易 [图片] [图片] 2.微信端可以通过商家助手小程序查看,访问「微信支付商家助手小程序」,点击「收款记录」,付款人展示为“云闪付用户”的,即为用户使用云闪付进行付款的交易。[图片] Q6:小程序“云闪付付款”功能是否支持服务商模式? A5:支持 Q7:某个小程序可以单独关闭用户使用云闪付付款功能吗? A7:可以,商户可以在商户平台指定小程序关闭该功能,并查看已关闭的小程序列表。超级管理员扫码登录商户平台「点我访问」,点击「产品中心」->「开发配置」->「支付方式配置」->「新增关闭云闪付APPID」,添加成功后该小程序用户将不再支持云闪付付款 小提示:支付方式配置在页面最底部 [图片] Q8:用户在小程序使用云闪付付款是否支持云闪付优惠? A8:支持云闪付通用优惠或全场优惠 Q9:用户使用云闪付付款后支付结果通知没有「attach」字段返回是什么原因? A9:已知问题,已排期修复 Q10 :商户后台开通了“云闪付付款”功能,为什么小程序支付时没有云闪付付款功能入口呢? A:1、手机需要安装云闪付APP 2、云闪付付款选项需要调起微信支付后才会让用户选择,当小程序自身功能有多项支付选择时,需要选择「微信支付」后才可会有云闪付付款选择[图片] [图片] Q11:用户在小程序使用“云闪付”付款后,服务商还有技术服务费吗? A:满足基础技术服务费和行业政策要求的,保持不变 Q12:商户后台开通配置云闪付后,支付时没有云闪付付款选择如何排查? A: 1.商户在后台关闭了此功能; 2.用户手机有安装云闪付app; 3.如属于以下场景,也不会展示云闪付:境外交易、指定身份支付、未成年支付、支付中签约; 4.商户号为新开通商户或近期无交易,要有稳定的流水后才会开启入口; 5.开通了“自助清关”产品不会展示云闪付; 6.电商收付通托管模式的商户不会展示云闪付; 7.小微商户(注:商业版小微灰度内测阶段)不会展示云闪付。 如有更多疑问可以跟帖回复,也可以拨打95017进行咨询
2021-12-31 - 公众号名字为新注册的公众号 改名总是不成功?
在注册公众号时名称已经输入 但是注册好了以后名称总是就变成了 新注册的公众号 然后需要改名字 需要上传资料审核 问了其他伙伴 他们都没有这种问题 是什么原因呢?
2022-09-24 - 朋友圈被折叠?别慌,4套话术模板和禁用词清单来了!
最近微信真的越来越严格了,群折叠之后是微信朋友圈折叠,完全不给发广告的活路。 其实朋友圈折叠功能早就有,但是这次升级可厉害了:之前都是针对文字内容的折叠,这次连图片也折叠! 更绝的是,如果你短时间发布多条广告信息,还可能被折叠收录到一个合集页,进一步减少曝光,来个「叠中叠」。 [图片] ▲图源网络 什么意思?比如你辛辛苦苦排了 6 、7 条产品的九宫格+文案,发到朋友圈就等收钱了,结果一发傻眼了,全被折叠成一条「XXX等 6 条内容」的文案,连一张图都不给你曝光!是不是气死? 还能怎么办?微信爸爸说了算,咱们改还不行! 要是你感觉自己的朋友圈内容也可能被折叠,接下来的内容你真的要看看,转发给做私域的伙伴一起学,别等朋友圈真被折叠才拍大腿后悔! 什么样的朋友圈 最可能被折叠? 直接上微信团队的官方标准: 「朋友圈是用户分享和关注朋友们生活点滴的空间,一直以来批量或高频次发送营销性信息的行为,存在侵扰用户安宁、减损用户朋友圈体验的问题,(因此)被较多用户投诉。」 划重点了朋友们:批量!高频次!发送营销性信息! 用人话再解释一遍: ① 多次发布重复或相似内容的用户,内容会被折叠! 比如你手上有大量的客服微信号,复制粘贴同样的带货素材到朋友圈发布,一旦同样的内容发布超过一定次数,折叠你没商量! ② 每天频繁发布广告的用户,内容会被折叠! 比如你在微信做一次促销活动,为了宣传短时间大量发布促销相关的朋友圈内容,也等着被折叠吧,而且要告诉你:一旦被盯上,后续即使降低发广告的频次或者发送生活内容,也同样会被折叠。 [图片] ▲生活动态的朋友圈也被折叠 如何避开折叠 安全发圈? 问题出现了,怎么解决? 我熬夜刷了一礼拜朋友圈,发现不被折叠的 3 种方法: ① 企微!企微!企微! 早就劝大家做私域一定要用企微,从来没像今天这么笃定过!还在用个微的商家抓紧转! 我观察了朋友圈所有的企微号,没发现一个朋友圈被折叠的。个微卷成团,企微稳如山! 比如下图,同一时间,同样是发送广告信息,某房产个人号的内容被折叠,而用了「推荐款」、「限时限量」等明显营销词汇的企微号反而没有被折叠。 [图片] 这绝对不是个例,同样的其他企微号,有一个算一个,朋友圈都没有被折叠的情况。 [图片] 这说明什么朋友们?朋友圈折叠功能对企微开了后门啊!你做私域不用企微,就是浪费官方倾斜的资源!个微收拾收拾,赶紧把流量导到企微,越快越好! 当然已经做企微的商家也别骄傲,企微朋友圈右上角有我不感兴趣按钮,乱发朋友圈打扰用户,被用户点了直接屏蔽 7 天,比折叠还惨。 [图片] ② 营销素材,用视频号发 你要是实在用不了企微,又觉得自己折叠风险很大,还有一招,发视频号! 我观察到目前折叠的朋友圈,都是文案+图片、或者图片的形式,但是视频号就很神奇,它不折叠! [图片] 凡是微信鼓励的就是我们要做的,朋友圈就别玩九宫格了,直接做成视频号分享,短视频带货不香吗?评论区还可以带小程序的短链,曝光转化直接一步到位。 ③ 4 套防折叠必杀话术模板 如果你不想转企微,也不想做视频,OK,我还有一招必杀给你。发文案+图片的朋友圈也能防折叠。 首先复盘一下折叠的逻辑是什么:批量或高频次发送营销性信息的行为。 硬广的套路已经玩不通了,朋友圈还可以来软的,广告没人爱看还容易被折叠,但是段子人人爱看,生活分享人人爱看啊,那就这么干,分享 4 种防折叠还能带货的朋友圈话术模板: 1、 生活分享型 这类内容玩的就是种草逻辑,用得好的商家真的太多了。 比如看到蛋糕烘焙行业的一个商家,朋友圈真的太生活化了,不看昵称还以为就是在减肥、在家做蛋糕的身边朋友。 [图片] 生活分享型的文案模板:生活经历描述+产品结合+感想。 比如你是母婴食品的商家,朋友圈可以是「夏天宝宝胃口总不好,太让人头疼了。灵机一动用XX麦片给他做了凉爽的水果粥,孩子一口气吃完还要一碗,我太机智了哈哈。」 2、 场景描述 这类文案的重点是要告诉用户:用了你的产品,生活到底会有啥变化? 比如看到一个房产人的朋友圈很有意思: 通篇没有提到买房,但是营造了在这里生活的惬意场景。 [图片] 这类内容完全可以复制,比如酒类商家描述用户劳累一天后,躺在沙发上微醺的场景、或者蛋糕商家描述用户早起后给自己做一顿精致早餐的场景,通过场景化的描述,让产品融入其中种草给用户。 3、 干货内容 传递有价值的信息永远不会过时,干货内容朋友圈不用多讲了。 比如做母婴品牌的德轩母婴,创始人每天会在朋友圈发布育儿相关的干货知识,以及用户咨询的解答截图,潜移默化树立起专家的人设,让用户有问题第一时间就能想到咨询她,评论区要是再带上干货相关的小程序商品链接,完美。 [图片] 4、 互动话题 除以上的内容以外,最后一种模板就是互动讨论。 举个例子,拼图商家图益会把拼图主题和朋友圈话题巧妙的结合起来,晒美食主题的拼图朋友圈,文案配「你的火锅必点菜是什么」来引发粉丝讨论,就是不错的互动方式。 如果你是美妆商家,互动话题可以是「你包里必备的化妆品是什么?」,如果是知识付费商家,可以是「你最近一次看书是什么时候?」等等,结合图片软性植入做营销。 [图片] 朋友圈已经被折叠 那就做好这两点 如果你朋友圈已经被折叠了,那上面的方法对你用处都不大了,我只强调两点,亡羊补牢。 ① 做好一句话营销 被折叠的内容只展示前 12 个字,最后的曝光机会了,你得把握好! 一定把朋友圈的精华都集中在前面,比如「早春少女感爆款穿搭合集」、「熬夜人群 10 种护肝食品」等,把利益点集中在前面讲解,吸引用户点开看。 ② 打造朋友圈橱窗 朋友圈被折叠了,还有朋友圈主页可以做营销。 比如看过一个商家每次朋友圈只发一张图不发文案,连续发 9 次把自己的朋友圈主页排成橱窗的样式,如下图: [图片] 用户点进个人主页看朋友圈和逛商城一样,排版样式相当美观,其实也不失为一种不错的营销方式,被折叠也不怕朋友圈没用了。 最后再分享下我总结的折叠高风险关键词: 1、 和营销直接相关的词汇:秒杀、特价、红包、顾客、价格、打折、包邮、买家、优惠、团购、代购、下单等等。 2、 各行业的高频词:蛋糕、好房、女装、口红等,也要注意不能在朋友圈频繁出现。
2022-02-24 - 开放标签wx-open-launch-weapp的一些问题认知
先上代码: 普通HTML版本为:(由于使用代码选项,保存无法显示,所以只能文本显示了) <wx-open-launch-weapp id="launch-btn" username="gh_xxxxxxxx" path="pages/home/index?user=123&action=abc" > <script type="text/wxtag-template"> <style>.btn { padding: 12px }</style> <button class="btn">打开小程序</button> </script> </wx-open-launch-weapp> <script> var btn = document.getElementById('launch-btn'); btn.addEventListener('launch', function (e) { console.log('success'); }); btn.addEventListener('error', function (e) { console.log('fail', e.detail); }); </script> 如果是使用框架,则为: <div class="test-position" <wx-open-launch-weapp id="launch-btn" username="gh_** *" path="pages/index/index.html?user=123&action=abc"> <template> <style>.btn { padding: 12px; height: 100px; width: 120px; }</style> <button class="btn">打开小程序-测试方法二</button> </template> </wx-open-launch-weapp> </div> 如果框架写上还不行,可以试试在文件main.js中,写上: Vue.config.ignoredElements = ['wx-open-launch-app', 'wx-open-launch-weapp']; 代码就写完了,说说需要注意的几点吧: 1、在开放标签中,<template>或者<script>里面的写样式,千万不要使用定位position,如果非要用就在最外层的div里面写,例如我这里的class=‘test-position’这里定位; 2、如果你觉得里面写样式不好写,可以在里面样式style写opacity:0;,这样的话开放标签只是用来填充,大小自己控制就行; 3、如果你跳转之后显示页面不存在,请检查下path的路径结尾是否写上了.html; 4、开发工具是无法测试的,只能使用手机测试,如果你在手机分享功能正常,说明你初始化授权没问题,记得要在初始化授权写上开放标签openTagList: ['wx-open-launch-weapp'];如果手机发现按钮不见了,初始化授权是ok的,只是标签写法出来问题;请查看其他注意点; 微信版本要求为:7.0.12及以上。 系统版本要求为:iOS 10.3及以上、Android 5.0及以上。 5、如果还不行,然后初始化授权也是成功的,请质疑一下后台初始化授权信息的jssdk中,APPID是否你想要的公众号,眼见为实; 6、如果还不行,请移步到微信官网查看是否有其他问题:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html
2021-05-18 - 获取用户信息
背景 我们发现大部分小程序都会使用 [代码]wx.getUserInfo[代码] 接口,来获取用户信息。原本设计这个接口时,我们希望开发者在真正需要用户信息的情况下才去调取这个接口,但很多开发者会直接调用这个接口,导致用户在使用小程序的时候产生困扰,归结起来有几点: 开发者在小程序首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框获取用户信息,会使得一部分用户点击“拒绝”按钮。 在开发者没有处理用户拒绝弹框的情况下,用户必须授权头像昵称等信息才能继续使用小程序,会导致某些用户放弃使用该小程序。 用户没有很好的方式重新授权,尽管我们增加了[代码]设置[代码]页面,可以让用户选择重新授权,但很多用户并不知道可以这么操作。 此外,我们发现开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致的([代码]wx.getUserInfo[代码] 必须通过 [代码]wx.login[代码] 在后台生成 [代码]session_key[代码]后才能调用)。同时,我们收到开发者的反馈,希望用户进入小程序首页便能获取到用户的 [代码]unionId[代码],以便识别到用户是否以前关注了同主体公众号或使用过同主体的App 。 为了解决以上问题,针对获取用户信息我们更新了三个能力: 1.使用组件来获取用户信息 2.若用户满足一定条件,则可以用[代码]wx.login[代码] 获取到的[代码]code[代码]直接换到[代码]unionId[代码] 3.[代码]wx.getUserInfo[代码] 不需要依赖 [代码]wx.login[代码] 就能调用得到数据 获取用户信息组件介绍 [代码][代码]组件变化: [代码]open-type [代码]属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码]为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的 [代码]detail[代码] 字段中获取到和 [代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me button>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: 1.API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框; 2.组件 [代码][代码][代码][代码]由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框。 通过获取用户信息的组件,就可以解决用户再次授权的问题。 直接获取unionId开发者申请 [代码]userinfo[代码] 授权主要为了获取 [代码]unionid[代码],我们鼓励开发者在不骚扰用户的情况下合理获得[代码]unionid[代码],而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得[代码]unionid[代码]: 1.在微信开放平台下存在同主体的App、公众号、小程序。 2.用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 这样可让其他同主体的App、公众号、小程序的开发者快速获得已有用户的数据。 不依赖登录的用户信息获取某些工具类的轻量小程序不需要登录行为,但是也想获取用户信息,那么就可以在 [代码]wx.getUserInfo[代码] 的时候加一个参数[代码]withCredentials: false[代码] 直接获取到用户信息,可以少一次网络请求。 这样可以在不给用户弹窗授权的情况下直接展示用户的信息。 最佳实践 1.调用 [代码]wx.login[代码] 获取 [代码]code[代码],然后从微信后端换取到 [代码]session_key[代码],用于解密 [代码]getUserInfo[代码]返回的敏感数据。 2.使用 [代码]wx.getSetting[代码] 获取用户的授权情况 1) 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息; 2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 3.获取到用户数据后可以进行展示或者发送给自己的后端。 One More Thing 除了获取用户方案介绍之外,再聊一聊很多初次接触微信小程序的开发者所不容易理解的一些概念: 1.关于OpenId和UnionId [代码]OpenId[代码] 是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。 [代码]UnionId[代码] 是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过[代码]UnionId[代码],实现多个小程序、公众号、甚至APP 之间的数据互通了。 同一个用户的这两个 ID 对于同一个小程序来说是永久不变的,就算用户删了小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。 2.关于 getUserInfo 和 login 很多开发者会把 [代码]login[代码] 和 [代码]getUserInfo[代码] 捆绑调用当成登录使用,其实 [代码]login[代码] 已经可以完成登录,[代码]getUserInfo[代码] 只是获取额外的用户信息。 在 [代码]login[代码] 获取到 [代码]code[代码] 后,会发送到开发者后端,开发者后端通过接口去微信后端换取到 [代码]openid[代码] 和[代码]sessionKey[代码](现在会将[代码]unionid[代码] 也一并返回)后,把自定义登录态 [代码]3rd_session[代码]返回给前端,就已经完成登录行为了。而 [代码]login[代码] 行为是静默,不必授权的,用户不会察觉。 [代码]getUserInfo[代码] 只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,开发者可通过 [代码]unionId[代码] 和其他公众号上已有的用户画像结合来提供历史数据。因此开发者不必在用户刚刚进入小程序的时候就强制要求授权。 可以在官方的文档中看到 [代码]login[代码] 的最佳实践: [图片] Q & A Q1: 为什么 login 的时候不直接返回 openid,而是要用这么复杂的方式来经过后台好几层处理之后才能拿到? A: 为了防止坏人在网络链路上做手脚,所以小程序端请求开发者服务器的的请求都需要二次验证才是可信的。因为我们采取了小程序端只给 [代码]code[代码] ,由服务器端拿着 [代码]code[代码] 和 [代码]AppSecrect[代码] 去微信服务器请求的方式,才会给到开发者对应的[代码]openId[代码] 和用于加解密的[代码]session_key。[代码] Q2: 既然用户的[代码]openId[代码] 是永远不变的,那么开发者可以使用[代码]openId[代码] 作为用户的登录态么? A: 不行,这是非常危险的行为。因为 [代码]openId[代码] 是不变的,如果有坏人拿着别人的 [代码]openId[代码] 来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的 [代码]第三方session[代码] 来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。 Q3: 是不是用户每次打开小程序都需要重新[代码]login[代码]? A: 不必,可以将登录态存入[代码]storage[代码]中,用户再次登录就可以拿[代码]storage[代码] 里的登录态做正常的业务请求,只有当登录态过期了之后才需要重新[代码]login[代码] 。这样子做一则可以减少用户等待时间,二则可以减少网络带宽。 目前微信的[代码]session_key[代码] 有效期是三天,所以建议开发者设置的登录态有效期要小于这个值。
2018-09-25 - (12)微信同声传译插件
我 微信界的同传扛把子! 识别你的语音? Of course! 我就是你的知己! 翻译你的中文? No problem! 我的英语好着呢! 听懂她的英文? So easy! 你想听我就敢译! What’s more! 我还能读给你听! 如何快速实现语音转文字、文本翻译、语音合成等等能力? 不用着急,让微信同声传译插件来帮你! 今天我们的小故事想和大家分享微信同声传译插件的故事。 1 应用场景 大家好,我叫“微信同声传译”插件,是由微信智聆语音团队、微信翻译团队与公众平台联合推出的同传开放接口,可通过语音转文字、文本翻译、语音合成接口,为开发者赋能。下面介绍我的应用场景: 场景1: 英语口语、听力双管齐下,没有外教也能学好英语! 把我放在小程序里,我既可以将口语转为文字,也可以帮助用户判断发音是否标准纯正,是最佳听众和英语学习道路上的良师益友; 场景2: 出国旅游,英语蹩脚怎么办? 这时我可以应用到翻译小程序里,说中文,译英文,再也不怕出国交流难。 场景3: 看剧时想要吐槽!可是手里有零食,打字不方便怎么办? 只要在小程序里有我,用户就可以按住按钮说话,语音转文字,帮你发送弹幕! [图片] 微信同声传译插件目前开放了以下三个接口——语音识别接口、语音合成接口、文本翻译接口。 通过这个插件,插件使用者可以轻松实现语音读取识别、文本转语音和中英文文本转换,避免这类需求的重复开发工作。 [图片] 2 如何使用插件? 插件接入流程 >>> 开发者在小程序插件中搜索“同声传译”、“语音识别”可以搜索到微信同声传译,添加使用后按使用文档接入使用,同时小程序开发者还可通过 https://github.com/Tencent/Face2FaceTranslator 查看我们开源的插件使用案例。 [图片] 1 登录小程序后台管理,进入“设置”; [图片] 2 点击第三方服务; [图片] 3 点击添加插件; [图片] 4 输入“同声传译”或者“语音识别”、“翻译”等搜索关键字搜索插件并添加,同时可以点击“查看详情”,获取插件的详细介绍、接入说明和开发文档。 [图片] 3 插件简介 1 插件名称 微信同声传译 2 插件AppID wx069ba97219f66d99 3 插件使用指南 微信同声传译插件设计之初,采用和小程序官方流式录音API、网络请求API类似的javascript调用接口,小程序开发者可以快速接入使用此插件。小程序“面对面翻译”使用了这些接口,并且已经开源 [开源地址],便于开发者快速开发。 [图片] (面对面翻译小程序) 具体使用接口可以查看微信同声传译插件使用文档。
2018-08-17 - 如何在微信内外部浏览器唤起小程序
微信外部浏览器唤起微信小程序目的:通过发送短信召回流失用户。官方文档地址https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html步骤一该API我们主要用到的配置如下:jump_wxa:跳转到的目标小程序信息。该对象内包含两个字段。path:通过scheme码进入的小程序页面路径,必须是已经发布的小程序存在的页面,不可携带queryquery:通过scheme码进入小程序时的query。步骤二需要注意的是,query必传,然而如果我们需要跳转的页面不需要参数,也需要配置上query,默认我们填写的是from=sms, 用于统计是从短信渠道来的。步骤三与后端约定access_token 通过前端传递appKey进行区分,传哪个微信小程序的appKey 就生成对应小程序的access_token。步骤四支持设置scheme的过期时间,默认永久,我们的应用场景很少,暂不详说。步骤五代码里操作如下,由后端聚合参数信息。let postData = { appKey: 'QTSHE_MINI_APP', path: 'pages/partdetails/partdetails', query: 'partJobId=123456' || 'a=1' } this.$axios.post('xxx', postData).then((res) => { if (res.success) { const url = res.data.openlink // 将scheme转为我们平台的短链接,否则在安卓手机上无法打开微信小程序,会默认打开浏览器搜索。 this.$axios.get(`xxx`, {url}).then((res) => { if (res.success) { this.shortLink = res.data // 将weixin://xxxx 转换为 https://b.qtshe.com/xxx } }) } else { this.$Message.error('获取失败,请稍后重试') } }).catch((err) => { console.log(err) this.$Message.error('获取失败,请稍后重试') }) 微信内部浏览器唤起小程序官方文档地址https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html步骤一首先需要登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”,未配置会导致wx.config签名失败,导致无法使用开放标签。步骤二在需要调用JS接口的页面引入如下JS文件:https://res.wx.qq.com/open/js/jweixin-1.6.0.js,1.6.0版本内才增加了上述标签,低于该版本的都无法显示。步骤三通过config接口注入权限验证配置并申请所需开放标签, 在wx.config里增加openTagList标签,内置两个开放标签 [代码]wx-open-launch-app[代码] 微信h5唤起本地已经安装的app,以及 [代码]wx-open-launch-weapp[代码] 微信h5唤起小程序,操作如下:wx.config({ debug: false, appId: window.g_info.wx_appid, // 全局变量appId timestamp: data.data.timestamp, // 调用微信接口返回的 nonceStr: data.data.nonceStr, // 调用微信接口返回的 signature: data.data.signature, // 调用微信接口返回的 jsApiList: ['openLocation', 'getLocation'], // 这里需要任意填写api,不能为空 openTagList: [ 'wx-open-launch-app', 'wx-open-launch-weapp' ] }) 注意点对于Vue等视图框架,为了避免template标签冲突的问题,可使用[代码]<script type="text/wxtag-template"></script>[代码]进行代替,来包裹插槽模版和样式。页面中与布局和定位相关的样式,如[代码]position: fixed; top -100;[代码]等,尽量不要写在插槽模版的节点中,请声明在标签或其父节点上;对于有CSP要求的页面,需要添加白名单frame-src https://*.qq.com webcompt:,才能在页面中正常使用开放标签。// Vue里操作代码如下,如果需要写样式,最好是外部包一个div进行样式编写,内部的内容宽高100%即可,调试样式可使用微信开发者工具的网页模式: .quick-btn { // 这里写样式 width: 3.43rem; height: 0.96rem; display: flex; img { width: 100%; display: block; } } <div class="quick-btn"> <img src="https://qiniu-image.qtshe.com/20220317quick-apply.png" alt="" /> </div> 示例效果[图片] 为了方便运营同学,做了个工具给他们使用。 [图片] 2023年4.11号新版本后的改版方案: 首先做一个落地页:https://b.qtshe.com/1DF43E 代码如下: import toast from 'toast' export default { created() { this.init() }, methods: { // 获取微信scheme地址,并且主动跳转一次 init() { this.$axios.post(`https://xxx/你们的接口地址/user/device/scheme`, this.$route.query).then(res => { if (res.success) { window.location.href = res.data } else { toast(res.msg) } }).catch(() => { toast('服务器错误,请稍后重试') }) } } } 原理:通过地址栏配置需要跳转的参数,h5页面拿到参数后通过接口转换为weixin://xxxx 开头的scheme协议地址,并且主动location.href一次。这样能保证用户每次打开都是新的scheme地址,针对一天50w数量上限的问题,可尝试同一个用户在固定时间内相同的参数,返回相同的scheme地址。 以上限制只存在于除微信外的外部浏览器,微信容器里没有以上的限制。 2023年12月18日后方案: 官方公告如下: https://developers.weixin.qq.com/community/develop/doc/00024e32cbc36055c0c0a34b066401 ,没有外部浏览器、微信环境的限制,可以被多人打开。 使用方式通过明文拼接: weixin://dl/business/?appid=*APPID*&path=*PATH*&query=*QUERY*&env_version=*ENV_VERSION* 示例: // query需要encode编码,ENV_VERSION为控制打开开发版、体验版、线上版本 weixin://dl/business/?appid=wxa7b0b022472e55e6&path=pages/partdetails/partdetails&query=partJobId%3D123456 通过明文 URL Scheme 拉起的小程序(页面)必须要提前在「小程序管理后台 -> 设置 -> 隐私与安全 -> 明文 scheme 拉起此小程序」中进行声明; 小程序:配置能够通过明文 scheme 进入的小程序页面[图片] 这一点很烦很烦,希望能支持下 默认可打开所有的,不然每次都得去配置。
08-07 - web-view打开嵌入腾讯视频的页面提示iframe不支持引用非业务域名
现在的策略是允许公众号文章内嵌iframe,对于第三方的iframe会检查业务域名。可理解为: 1、打开公众号文章,文章内嵌iframe含有腾讯视频这种情况就是支持的。 2、打开开发者自己的业务域名,网页内嵌iframe含有腾讯视频这种情况就是不支持的。
2019-11-20 - 小程序在第三方平台提交审核报‘9400001’,errmsg: "该开发小程序已开通小程序直播?
[图片] 具体情况这样,请大佬帮忙
2020-04-17 - 小程序服务商助手的自述
相信每位小程序服务商的同学,都遇过这些问题: “小程序需要紧急上线,提审后每20分钟看一次消息,却收到了驳回通知。” “提审不通过,我应该给小程序选什么类目?” “本月的提审额度用完了,想增加当月的提审额度,我还有机会吗?” “刚提审的几千个小程序出bug了,开发哥哥不在身边怎么办?” ...... 当遇到这些问题时,找内部技术同学要排期,因为不熟悉平台规则,在反复的信息查询和文字沟通中,花了大量的时间,问题也还没解决。 为了解决服务商的“痛点”,平台把我(小程序服务商助手)推出,服务商可以在小程序内查询很多实用的数据和功能:查看审核信息、加急次数、服务商的新规则新能力、查找小程序的数据和功能、咨询平台客服等等。 虽然我已经上线了半年,用过的服务商都说方便。但还是有不少服务商同学不知道我的存在,所以借着这个机会,让我来好好向大家介绍一下我的各个功能模块吧。 业务实时晴雨表,竞争风险指向标 [首页业务能力看板]使用了六项维度辅助服务商概览业务状况,每项维度都有对应的打分和评价细则,服务商可以全方位评估自己的业务现状,辅助定位发展策略。各项星级指数越高,意味着服务商对应的业务能力越好。 [图片] 精细运营资源,数据驱动增长 [数据页]分别为状态概览,调用次数,授权关系和类目占比。覆盖了服务商分析旗下小程序的最重要的维度,服务商可以通过手机,清晰的查看、分析小程序的表现,从而精细化运营,合理分配技术服务资源。 [图片] 移动管理百宝箱,合作沟通驱动轮 [我的主页]是一个助手小程序的核心管理功能集成页,功能覆盖服务商成员信息管理,小程序审核业务管理,公告通知和咨询反馈渠道,是目前服务商助手小程序使用频率最高的页面。 [图片] 【成员管理】:权限管理&成员更新 [成员管理]主要管理有权限查看和办理业务的成员名单,管理员默认为服务商open账号的管理员,通过微信号添加新的管理员,成为了开放平台账号成员后,即可进入“服务商助手”小程序,使用小程序的业务功能。 [图片] 【审核管理】:审核资源管家&加急撤回功能 [审核管理功能]可以满足服务商运营管理人员对小程序代码审核业务的核心管理需求,包括审核资源查看和管理、小程序一键加急/撤回操作。 1. 审核资源查看和管理: 包括服务商审核资源使用量/余量(本月提审、加急quota分配情况及对应剩余次数),相关审核进度/结果数据展示,审核队列明细和查询功能,历史审核合格率和驳回原因分布等。 注意:审核资源受服务商提审质量和线上表现影响,请服务商们务必做好小程序内容把控,减少提审驳回和线上违规情况。 [图片] 进入审核队列,可以浏览审核中、审核冻结、审核驳回的小程序明细。同时可以按小程序名称、appid、模板id检索服务商小程序的审核状态和结果。 [图片] 2. 加急/撤回功能 小程序一键加急后,该审核单将以最高优先级安排审核,满足服务商特殊场景需要快速被审核的需求;撤回后,可以继续修改小程序后再次提审,同时可以对队列中的某个或者多个分别或者批量进行加急/撤回审核的操作。 [图片] 更多审核专区的知识,可以从页面底部的《审核专区使用手册》去了解。 智能和人工客服系统 [客服咨询]是服务商对平台最直接最便利的沟通反馈渠道。 一般的规则类知识类问题,客服会提供丰富的运营知识库给到服务商去查阅,较为复杂的流程功能或技术类问题,则通过工作日早9点到下午6点在线的人工客服来对接,流转到内部处理。 [图片] 重要消息公告牌 [公告栏]是平台向服务商同步重要信息的首要渠道。 很多政策变更、规则更新、重大活动都将通过小程序内的红点和微信模板消息提醒告知目标服务商。服务商可以第一时间掌握最新的平台信息,不再慢人一步。 [图片] 除了以上已有的功能,平台还在下半年为广大服务商同学准备了更多方便的功能,方便服务商同学清楚自己在平台的表现和权益、便捷管理自己服务的小程序、了解最新的平台运营规则和行业知识等等,我们一起敬请期待吧。 同时,欢迎广大服务商同学前来体验「小程序服务商助手」,了解自己的业务数据和表现。如对“我”有任何问题或者服务商运营的意见,可以通过客服通道和公告专区反馈。平台将根据大家的意见建议,不断完善,让我成为大家最好用的服务商管理小助手。以后,请多多指教。
2020-07-15 - 【问题排查】小程序闪退
在使用小程序的时候,偶然会发生闪退。这里来讲一下闪退的问题该如何排查。 版本排查 发生闪退的时候,首先,要确认下 版本 是不是最新的。如果不是,建议更新版本再重试。旧版本的问题会在新版本进行修复哦。 微信版本: 微信官网 基础库版本:基础库更新日志小程序自查 确认版本都是最新情况下,还是有闪退的问题的话,建议先进行小程序自查~ 一般情况下,闪退是因为内存使用过多导致的,小程序侧可以通过基础库提供 wx.onMemoryWarning 接口来监听内存不足的告警,当收到告警时,通过回收一些不必要资源避免进一步加剧内存紧张。 反馈官方 如果问题还是会出现的话建议反馈给官方处理,需要附带上以下信息点协助排查(划重点:完整的提供信息才可以加速问题处理进度哦!!!) 示例: 系统及微信版本号:安卓7.0.17、IOS 7.0.17(出现问题的时候,建议两端都测试,给出有问题的case)必现 or 偶现:必现可复现场景:代码片段 或者 线上小程序复现步骤:进入首页,点击添加按钮等等,推荐录制复现的 视频(重点)进行上传。上传日志:提供微信号,复现时间点(操作步骤:手机微信那里上传下日志: 我 -> 设置 -> 帮助与反馈:右上角扳手 -> 上报日志,选择出现问题的日期,上传日志)
2020-11-03 - 第三方代注册小程序支持快速设置登录邮箱和密码
近期平台对第三方服务商代注册小程序的流程进行了优化,用户通过第三方服务商成功创建小程序后,将收到设置小程序登录邮箱和密码的微信消息提醒。管理员可通过点击提醒内的“前往设置邮箱密码”或直接搜索「小程序助手」小程序进行邮箱密码的设置。 温馨提示:所设置的邮箱需要是未绑定任何个人微信、公众号、小程序、微信开放平台的邮箱 详细操作流程如下: 1.打开“小程序助手”小程序,选择对应的小程序 [图片] 2.点击“登录邮箱”设置项 [图片] 3.填写邮箱以及设置小程序密码 [图片] 4.系统会发送验证邮件到邮箱 [图片][图片] 5.打开邮件中的链接,确认验证 [图片] 6.邮箱设置成功,可以在官网:mp.weixin.qq.com用所设置的邮箱和密码登录小程序后台 [图片] 如有疑惑,可微信搜索小程序服务商助手-我的-咨询反馈。
2020-07-03 - 微信小程序+webview+百度地图问题的终结答案
1、问题现象 [图片] 2、当前的反馈帖子 https://developers.weixin.qq.com/community/develop/doc/000eca6d5443c05f57ea3a84854400?highLine=webview%2520%2520%25E7%2599%25BE%25E5%25BA%25A6%25E5%259C%25B0%25E5%259B%25BE https://developers.weixin.qq.com/community/develop/doc/000a4c01754120c0f038259dc56800 https://developers.weixin.qq.com/community/develop/doc/000cc0bfab8258a6d3d70f4c751800 3、当前的结论 官方未回答,因为这是百度地图的功能。开发者也是迷茫为什么,该怎么办。 4、百度的开发文档和官方问答分析 http://lbsyun.baidu.com/index.php?title=wxjsapi http://lbsyun.baidu.com/index.php?title=FAQ/wxjsapi [图片] 5、结论: 小程序内web-view能否用百度地图,取决于你引用百度地图后是否有调用百度服务器。如果只是用来渲染地图类或本地交互类的是可以支持的,但是如果是打开网页直接调了百度地图的服务器,就会通不过微信服务器的校验。 如果需要再小程序内调百度地图的服务,只能用原生地图的开发模式,先注册百度地图-微信小程序类型的ak,并下载js文件后,给小程序配置服务器百度地图的request域名后,再调用。 综上所述:百度地图在小程序web-view模式,是无法调用百度地图服务。如果有此需求可以自己规避下,或者切换技术架构或实现方案。
2020-10-15 - 一个营业执照可以注册多少个开放平台账号?
50个。
2019-12-27 - 服务商政策|流程|能力|接口变更通知(2020.11.16)
一、小程序服务商助手监管专区上线通知平台在“小程序服务商助手”小程序新增了 【监管专区】 ,包括服务商旗下小程序违规数据与违规队列展示,支持服务商快速查询小程序违规与申诉情况。详情请参考: https://developers.weixin.qq.com/community/minihome/doc/0002026140063083364baae0250001 二、公众号、小程序回调出口IP变更通知为了提高公众号、小程序回调出口网络质量,微信callback IP地址计划灰度切换到以下腾讯云网络出口网段,详情请参考: https://developers.weixin.qq.com/community/develop/doc/0006c02b1403a0dc693b4132c51001?blockType=1 三、微信卡券将不再支持新申请开通使用“优惠券”功能因“微信卡券>优惠券”产品能力未来将统一升级为“微信支付优惠券”,12月10日0点起,“微信卡券>优惠券”功能将不再支持新商户开通,该功能后续将陆续下线。其他微信卡券功能暂无变化。详情请参考: https://developers.weixin.qq.com/community/develop/doc/000a8aeccf45284fc83bc22d251c01?blockType=1 四、服务平台开放入驻及原有服务升级通知为了更好地为用户提供服务,服务平台在近期进行了改版升级。第三方服务商可在服务平台上传对应行业的小程序开发服务,通过审核后即可进行服务展示。详情请参考: https://developers.weixin.qq.com/community/develop/doc/0004a24516cfd01ec02baefae51401 五、小程序类目资质更新[图片] 备注:绿色字体为小程序类目的更新调整,详情请参考小程序开放的服务类目 第三方快速创建的小程序可选择的类目参考第三方平台-快速创建小程序接口-类目参考表 https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Fast_Registration_Interface_document.html
2020-11-16 - 小程序开发服务商入驻微信服务平台指引攻略
概述欢迎申请成为微信小程序开发服务商:微信小程序开发服务是微信开放平台第三方服务商的一项服务能力。 成为微信小程序开发服务商你可以创建小程序代开发服务并发布到服务平台。 商家可在服务平台小程序开发专区选购你的服务,并通过授权的方式获得你所提供的服务。 为创建SaaS小程序代开发服务你需完成以下步骤:1. 创建第三方平台 2. 登录服务平台 3. 创建小程序开发服务 内容发布并通过审核后,即可通过小程序开发服务专区向用户提供服务。 审核内容将包括:服务商在所选行业的服务能力、服务标题、服务简介、服务头图、服务效果图,服务功能和套餐的完整性,服务价格的合理性。 入驻基本标准(1)已在微信开放平台通过开发者资质认证,成为微信第三方服务商; (2)具备微信小程序开发能力和服务经验 (入驻服务所选行业下,活跃小程序案例不少于5个); (3)服务商半年内无违规记录; (4)服务商可在对应行业提供完整的小程序行业解决方案 (对应行业所提交的案例展示小程序需具备平台规定的核心场景之一)。 *详细服务上传审核标准 服务创建规则1. 单个服务对应单个行业,如服务商服务多个行业,则需分别上传多个服务; 2. 单个行业对应上传的服务内容需关联该行业解决方案,不得出现无关/其他行业的内容。 接入流程:一、创建第三方平台(如有可跳过该步骤)第三方平台是服务商创建服务并获取授权的基础体系。第三方平台的申请和上线流程参照第三方平台的申请和上线流程。 注:注册第三方流程根据开发模式选择平台型服务商或定制化服务商 平台型服务商注册说明 定制化服务商说明文档 *服务商需通过第三方平台账号与小程序建立授权关系,并满足入驻标准方可上架服务 二、登录服务平台1. 使用第三方平台管理员微信登陆服务平台(该微信号需同时为与第三方企业主体一致的小程序管理员) [图片] 2. 若无企业主页,点击“创建新的企业主页” 若有企业主页,则请查看步骤(三) [图片] 3. 若无可创建的企业主页,进入微信公众平台“创建新的企业小程序”(企业小程序创建流程),创建成功后,再回到服务平台继续创建企业主页。 [图片] 4. 创建企业主页:选择以某个公司为主体的小程序(每个主体只能创建一个企业主页) [图片] [图片] 5. 选择企业主页的类型 [图片] 6. 确认企业主页的主体 [图片] 7. 完善企业资料并提交 [图片] 三、创建小程序开发服务1. 点击右上角头像,进入“我的主页” [图片] 2. 创建服务,选择“小程序开发” [图片] 3. 填写信息并提交 [图片] 4. 提交审核 在“我的主页”点击“提交审核”,审核通过后服务将展示在服务平台 审核需时1-3个工作日,服务商可在「我的主页」服务列表状态栏查看当前审核状态 [图片] 5. 服务上架
2020-12-02 - 第三方开放平台 ,全网发布提示:暂时没有测试资源,请稍等一段时间再提交?
因为有其他第三方平台提交了检测,测试资源被占用了,过段时间再尝试提交即可。
2019-12-27 - 服务商助手审核专区使用手册
本月可提审次数 显示服务商本月剩余可提审次数和分配的总quota值,每月1号更新 本月可加急次数 显示服务商本月剩余可加急次数和分配的总加急次数,每月1号更新 以上审核资源受服务商线上表现(小程序内容安全、活跃度、平台配合度)影响,各服务商务必做好小程序商户提供内容素材的管理,减少提审驳回情况和线上违规情况 更新时间为1号早上6点,0-6点显示的是上一月quota,6点后更新为本月 审核数据 服务商可以查看近7天、本月、上月的每日审核量、合格率和期间平均合格率、驳回原因分布 平台合格率为所选择时间周期内的合格率,合格率为重要运营指标,对服务商的资源分配有较强影响,请注意提升 一期合格率存在一定的误差,仅供参考,后续会不断完善,提升这里的数据准确度 审核队列 可以对当前审核队列中的各个审核单进行管理 审核中:提审后等待审核的单,审核时效一般为7天内,表现良好的服务商有更快审核速度 审核冻结:大批量提审未通过预检,被延后审核,建议服务商基于预检不通过原因撤回整改后再次提审,审核时效>15-30个工作日,冻结不及时撤回将被平台追加处罚 审核驳回:近7天被驳回的审核单 注意,审核队列只保留最近一次审核状态的单,如近7天被驳回的单再次提审,将从审核驳回队列中消失,进入审核中队列或审核冻结队列 审核队列支持按小程序名称,小程序appid,模板id进行搜索 搜索时,将在全部审核队列中搜索。小程序名搜索的优先级高于模板id,如”幸运58”的小程序和模板id为5828的100个小程序同时存在时,搜索58将只显示“幸运58”,搜索582才会显示模板5828 appid只支持精确匹配,名称和模板id支持模糊匹配 搜索模板id时,可以按全部单和只看审核冻结单进行筛选,方便进行批量操作 目前一页可展示100个审核单,如需查看更多,请点击页面底部的“加载更多审核单”,加载第二页,以此类推 小程序加急 服务商对于审核中和审核冻结的小程序可以进行加急操作,加急后,该审核单将优先安排审核,原本被冻结的状态也会解除 加急额度属于平台稀有资源,请谨慎使用,严禁任何形式的转卖转赠,一经发现将对quota和加急次数进行一年的处罚 单个加急:可以在小程序的明细页点击加急按钮进行 多个加急:可以点击审核队列的加急按钮,选择多个小程序进行加急 不可加急的类型:需要被报备网信办等机构的审核,无法加急,提示加急失败,加急额度返还 小程序撤回 当发现bug,或者预检不通过导致大批小程序被冻结时,服务商可以操作撤回这批小程序,整改后再次提审。 对大批量问题提审单撤回有助于提升服务商合格率,降低对平台资源消耗。预检不通过的冻结单未及时撤回将会被处罚 单个撤回:可以在小程序的明细页点击撤回按钮进行 批量撤回:可以点击审核队列的撤销审核按钮,选择多个小程序进行撤回,批量操作时,可以基于模板去筛选,可以按照只看冻结去筛选 推荐操作流程:当服务商需要对某个模板id,如5234模板id中全部提审单或全部冻结审核单进行撤回时,建议点击撤销审核按钮,进入选择审核单页面,搜索模板5234,在进入的模板页,点击筛选选项(全部/只看审核冻结),然后点击全选,可以看到本次要撤回的小程序个数,最后点击确定 批量撤回在单数较多时,将生成一个后台异步任务执行,执行结束后将批量撤回结果反馈回来,大批量的撤回可能要等待较久时间 不可撤回的类型: 1,单个小程序每月撤回次数上限为10次,超过时不可撤回 2,已经被其他渠道(如API接口)撤回的单,不可重复撤回 3,其他 客服专区 本次审核专区将为部分服务商开放客服专区入口,客服专区包括机器人客服为服务商提供常用知识库,以及人工客服解决服务商具体问题 开放范围:基于服务商每月quota排名进行分配,一期开放100个,后续将不断放开,直至覆盖全部服务商 客服工作时间:工作日:9:00-12:00,14:00-18:00,特殊时期以平台公告为准 客服问题范围:小程序运营规则、服务商运营政策、提审/审核/线上处罚常见问题咨询、服务商助手体验bug问题等 使用过程中任何问题都可以通过客服入口留言,如无客服入口可以通过开放社区进行反馈,我们将以帮助服务商提高获取信息和办理业务的效率为核心目标,不断完善服务商助手小程序。 最后更新时间:2020年3月5号
2020-11-27 - 第三方服务商提审限额机制优化说明
第三方服务商按月按提审表现分配提审限额(quota)的机制上线以来,服务商对提审质量的重视度不断上升,大盘提审合格率也不断提升。为了保障服务商提审效率,携手与服务商共同发展,平台近期将完善quota机制,更科学合理地分配服务商提审额度。 一、提审限额分配机制优化 服务商表现排名,会综合服务商的以下表现进行评价。同时平台考察服务商表现维度更全面(11月在服务商助手小程序的“表现与权益”专区上线) 月quota分配优化前:月quota基于近3月提审单数、服务商表现排名分配月quota分配优化后:月quota基于近6月提审单数、服务商表现排名分配新服务商支持:新第三方服务商每月最少可获得50个quota。*新第三方服务商定义:自上线之日起一年内称之为新第三方服务商 安全:提审质量、线上违规情况、低风险小程序数量、高风险小程序数量等 规模:授权小程序个数、主体个数、月新增小程序个数等 活跃:高活跃小程序个数及占比、不活跃小程序个数及占比 支付:支付金额、笔数、支付活跃小程序个数及占比 体验:体验差小程序个数及占比 [图片] 二、quota自助调额范围优化: 优化前:服务商每次申请临时额度为月初分配提审额度的15%优化后:月初分配的quota作为调额范围的划分标准,具体调整范围如下图:[图片] 特别注意: 服务商在可选范围内按需求自助调额,自助调整次数以及调额范围将基于提审合格率、quota消耗率等服务商表现分配,分配规则如下: 自然年内若存在超过三个月(不含)提审合格率低于80%,将无法申请临时调额quota消耗率作为第三方服务商画像分值的一个重要参考项,请按需进行quota调额申请,若quota消耗率存在超过三个月(不含)低于60%,服务商的画像分值排序将会降低,影响后续审核的优先级以及quota等权益分配。quota消耗率指当月quota使用值/(当月quota分配值+临时申请quota总值)*100% [图片] 平台将持续为服务商提供高效快捷审核服务,若因业务发展等情况存在超额提审需求的服务商还可以通过“小程序服务商助手-我的-咨询反馈”联系人工客服进行申请更多临时额度,客服工作时间:工作日:9:00-12:00,14:00-18:00,特殊时期以平台公告为准。 临时额度人工申请周期为1-3天,请服务商提前规划好提审额度的需求。 微信开放平台运营团队 2020.11.4
2022-06-01 - 响应式原理解析
Vue响应式 前言 数据响应式系统是Vue最显著的一个特性,我们通过Vue官方文档回顾一下。 数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。 现在是时候深入一下了! 本文针对响应式系统的原理进行一个详细介绍。 响应式是什么 我们先来看一个例子 [代码]<div id="app"> <p>{{color}}</p> <button @click="changeColor">change color!</button> </div> new Vue({ el: '#app', data() { color: 'blue' }, methods: { changeColor() { this.color = 'yellow'; } } }) [代码] 当我们点击按钮的时候,视图的p标签文本就会从 [代码]blue[代码]改变成[代码]yellow[代码]。 Vue要完成这次更新,其实需要做两件事情: 监听数据[代码]color[代码]的变化。 当数据[代码]color[代码]更新变化时,自动通知依赖该数据的视图。 换成专业那么一点点点的名词就是利用[代码]数据劫持/数据代理[代码]去进行[代码]依赖收集[代码]、[代码]发布订阅模式[代码]。 我们只需要记住一句话:在getter中收集依赖,在setter中触发依赖 如何追踪侦测数据的变化 首先有个问题,如何侦测一个对象的变化? 目前来说,侦测对象变化有两种方法。大家都知道的! Object.defineProperty vue 2.x就是使用[代码]Object.defineProperty[代码]来数据响应式系统的。但是用此方法来来侦测变化会有很多缺陷。例如: Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应; Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。 … 本文也是利用Object.defineProperty来介绍响应式系统。 Proxy vue3就是通过proxy实现响应式系统的。而且在国庆期间已经发布pre-alpha版本。 相比旧的[代码]Object.defineProperty[代码], [代码]proxy[代码]可以代理整个对象,并且提供了多个[代码]traps[代码],可以实现诸多功能。此外,Proxy支持代理数组的变化等等 当然proxy也有一个致命的缺点,就是无法通过[代码]polyfill[代码]模拟,兼容性较差。 依赖收集的重要角色 Dep Watcher [代码]Dep[代码]、[代码]Watcher[代码]是数据响应式中两个比较重要的角色。 收集依赖的地方 Dep 因为在视图模板上可能有多处地方都引用同一个数据,所以要有一个地方去存放数据的依赖,这个地方就是Dep。 Dep主要维护一个依赖的数组,当我们利用render函数生成VNode的时候,会触发数据的getter,然后则会把依赖push到Dep的依赖数组中。 依赖是Watcher! 我们可以把[代码]Watcher[代码]理解成一个中介的角色,数据发生变化时,会触发数据的setter,然后通过遍历Dep中的依赖Watcher,然后Watcher通知额外的操作,额外的操作可能是更新视图、更新computed、更新watch等等… 原理实现 还是那句话:在getter中收集依赖,在setter中触发依赖。 下面我们看看代码: 代码有点长,下面会有步骤来讲解一次。 [代码]/* * 劫持数据的getter、setter */ function defineReactive(data, key, val) { // 1-1:把color数据变成响应式 const dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { // 3. 因为模板编译watcher访问到了color,从而触发get方法,触发了收集依赖的方法 dep.depend(); return val; }, set(newVal) { if (val === newVal) { return; } val = newVal; // 4-1. 假设我们通过 `this.color = 'yellow';`去更改`color`的值,就会触发set方法。 dep.notify(); } }); } /* * dep类,收集依赖,和触发依赖 */ class Dep { constructor() { this.subs = []; // 收集依赖的数组 } // 收集依赖 depend() { // 3-1. 通过外部的变量来添加到color的依赖中. if (window.target && !this.subs.includes(window.target)) { this.subs.push(window.target); } } // 通知依赖更新 notify() { // 4-2. 遍历 this.subs.forEach(watcher => { watcher.update(); }); } } /** * 数据与外部的中介 */ class Watcher { constructor(expr, cb) { this.cb = cb; this.expr = expr; // 2-1. 这里触发了get方法 this.value = this.get(); } get() { // 2-2. 这里把自己(watcher)赋值给了外部其中的一个变量 window.target = this; // 2-3. data[this.expr]触发了color的get const value = data[this.expr]; window.target = undefined; return value; } update() { this.value = this.get(); this.cb(this.value); } } [代码] 下面我们来走一次流程。 括号里面的1-1,2-2是对应代码的执行点。 1. 把数据变成响应式 利用[代码]defineReactive[代码]把color数据变成响应式(1-1),执行过这个方法后,我们调用[代码]console.log(this.color)[代码]的时候可以触发get方法。同理当我们[代码]this.color = 'yellow'[代码]。 注意:在[代码]Object.defineProperty[代码]上面初始化一个存放依赖的dep,这里其实是把dep作为数据[代码]color[代码]的一个私有变量,让get和set的方法可以访问到,也是我们经常说的闭包。 2. 编译模板创建watcher 假设我们现在编译模板遇到[代码]{{color}}[代码]。 Vue就会创建一个Watchter,伪代码如下: [代码]new Watcher('color', () => { // 当color发生变化的时候,会触发这里的方法。 }); [代码] 这里高能!! Watcher的构造函数里面调用了[代码]get()[代码]方法(2-2),把自己(watcher)赋值给了一个外部变量。 然后再触发get方法(2-3)。 3. get中收集依赖 因为模板编译watcher访问到了color,从而触发get方法,触发了收集依赖的方法。 进入到[代码]dep.depend[代码]方法中(3-1),这里因为在Watcher中把自己存到了外部变量中,所以在dep.depend方法中可以收集到依赖。 现在,依赖就被收集了。 4. 通过setter触发依赖 假设我们通过 [代码]this.color = 'yellow';[代码]去更改[代码]color[代码]的值,就会触发set方法,执行dep.notify(4-1)。 会遍历依赖数组,从而去触发Watcher的cb方法。 cb就是上面伪代码[代码]new Watcher[代码]的那个回调函数。 只要回调函数里面运行了操作dom方法,或者触发了diff算法更新dom,都可以把视图进行更新。 响应式简易流程大概就是这样了… 侦测数据变化的类型 其实数据监听变化有两种类型,一种是“推”(push),另一种是“拉”(pull)。 React和Angular中的变化侦测都属于“拉”,就是说在数据发生变化的时候,它不知道哪个数据变了,然后会发送一个信号给框架,框架收到信号后,会进行一个暴力比对来找出那些dom节点需要重新渲染。Angular中使用的是脏检查,在React使用虚拟dom的diff。 vue的数据监听属于“推”。当数据发生变化时,就知道哪个数据发生变化了,从而去更新有此数据依赖的视图。 因此,框架知道的数据更新信息越多,也就可以进行更细粒度的更新。比如,直接通过dom api操作dom。 相对“拉”的力度是最粗的。看到这里,是不是觉得vue的更新效率最快。 我们看看下面的例子 [代码]<template> <div>{{a}}</div> <div>{{b}}</div> </template> [代码] 这里我们看出模板只有a、b两个数据依赖,也就是说我们要创建两个闭包dep去存放两个watcher依赖,我们知道闭包的缺点就是内存泄露。如果有1000个数据依赖在模板上,每个数据所绑定的依赖就越多,依赖追踪在内存上的开销就会越大。 所以,从Vue.js2.0开始,它引入了虚拟dom,一个状态所绑定的依赖不再是具体的dom节点,而是一个组件,即一个组件一个Watcher。 这样状态变化后,会通知到组件,组件内部再使用虚拟 dom进行比对。这可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。但并不是引入虚拟dom后,渲染速度变快了。准确的来说,应该是80%的场景下变得更快了,而剩下的20%反而变慢了。 个人觉得,鱼和熊掌不可兼得。 “推”是牺牲内存来换更新速度。 “拉”则是牺牲更新速度来获取内存。 总结 Vue响应式的灵魂:在getter中收集依赖,在setter中触发依赖。 [图片] 我们再看看图,回顾一下整个流程。 通过[代码]defineReactive[代码],遍历data里面的属性,把数据的getter/setter劫持,用来收集和触发依赖。 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到Dep依赖中。 当数据发生变化时,会触发setter,从而Dep会向依赖(Wachter)发送通知。 Watcher收到通知后,会向外界发送通知,变化通知到外界后可能接触视图更新,也有可能触发用户的某个回调函数等等。 参考文献:《深入浅出Vue.js》
2019-12-31 - 个人项目学习笔记
前言:看完了比赛项目,感觉像是经历了一场头脑风暴,项目的起名、涉足行业、内容、UI、架构,以及业务设计等,感到都有很多学习的地方。打算做一个学习笔记贴。 一、分类 *项目有点多,我自己做了一下分类,以便查找。 记账与备忘: 《大学生记账本》 《咸鱼记账》 《KrisQin记账本》 《MY备忘》 《家庭多用记事本》 《ygjtools》 《乐考吧》 《日程管家》 《YAccount记账助手》 《随变记账》 《待办事项工具》 《小婷和小天一起记账》 《初心日历》 《寝记账》 《智慧账本》 失物招领: 《爱心收发室》 《帮寻小站》 《月见》 《长大寻物》 《悦寻失物招领》 《校园寻回》 《失全拾美》 健身类: 《健身助手力量日记》 《活力健身房》 《RedPoint红点》 《健身小程序简介》 音乐影视: 《云享Music》 《king电影》 《無音不泉》 《电影周周荐》 AI识别: 《鹦鹉AI端侧识别》 《ai视觉测试》 《图文识别》 《AI物以类聚》 《AI写诗》 医疗健康: 《人体生理指标》 《吃药小助》 《己目》 《体重MM》 《菲特日记》 《医医查》 《全国核酸检测资质医院查询》 《糖友饮食助手》 《每日戒糖》 《自助心理成长》 《预约挂号小程序》 《蓝医先生》 社团活动: 《阮薇薇点名啦》 《山大clubs》 《素拓百分百》 《小小微距》 《娱乐投票小程序》 《活动栏》 《薇科技弹幕墙》 《头马报名》 《滑伴》 《科联答题》 《招新Plus》 《文艺比赛小行家》 《布告》 《BJUT活动助手》 《准到聚餐》 学习工具: 《错题小本本》 《为高考加分》 《分录英雄》 《口算助手》 《答案sou》 《教资易取》 《快刷题库answer question》 《拾一英语》 《魅力单词》 《魔方训练计时器》 《focusair》 《Y计算器》 《高级工匠心录》 《成长课程表》 《“倾听者”综合型语音评价系统》 《小青考证》 《IAI CDS》 教育培训: 《来这儿学》 《微学堂(在线学习平台)》 《大学生资源共享平台》 《袋鼠培培》 《宝贝积分管理》 时间管理: 《西瓜清单》 《语音倒计时器》 《西红柿时间管理》 《Do More打卡小程序》 《倒计时》 《tomato clock》 《step by step》 《BT清单》 《tusake Today》 《叮咚倒数日》 《FTodoList》 校园管理: 《班级价值分》 《校园缺勤录》 《教务小助手》 《工程课表》 《云迎新》 《CAN课程表》 《简单的课表小程序》 《运动会管理系统》 《重邮课后小程序》 《高校信息共享平台》 《校园简单易》 《中北请假助手》 《知侬》 《ITD智慧校园》 《We广油》 《江大电服》 校园介绍: 《阿里嘻嘻》 《民大新生助手》 《北邮宣讲通》 《志愿校园》 《浙里淘志愿》 《云校知》 《校拍》 校园社区: 《北院守夜人》 《校园墙》 《AIB校友会》 《校园小唤》 《xcu许院生活》 物流快递: 《高校联盟-快递代取》 《速派递》 《物流小程序》 预约与邀请: 《天翊图书馆预约》 《农大饭食》 《课室助手》 《会议邀请函》 《weSport》 《哪天约》 《定约》 《易约行》 《自闭间预定》 《简约约拍》 《实验室设备预约助手》 《QSCamera》 《预约班车》 《心暖农侬》 《书香长大》 《私约团课》 情侣婚礼: 《趣婚礼》 《小酒馆》 《恋人小清单》 《云表白》 《情侣券》 《旅梦恋爱》 《快表白》 《恋爱空间》 购物商城: 《微信云商城》 《吃否CHIFOU》 《云端商城小程序》 《购物》 《柠檬商城》 《武冈微商城》 《狗头的店,狗头管理》 《林林的妙妙屋》 《芳甸鲜花商城》 《优鲜配送联盟》 《汇尤e家》 《云开发带后台商城系统》 《预付费机票销售小程序》 《微购收单》 知识普及: 《科普小程序》 《百词百科》 《BOSS百科》 《球员搜搜》 《火查查》 《急速查病》 《趣答星球》 《铁路生涯》 《趣酿》 《吃吃等你》 《男人买菜》 《诗华社》 《天天诗词》 《心跑道》 程序员: 《sentry 小程序客户端》 《GitPark》 《码农SHOW营》 《OTP动态验证码》 《微源库》 《LE编程》 《一起来学计组叭》 《统一运维平台》 《见字如面》 图像处理: 《莉龙美颜工具》 《图像复原微信小程序》 《Hi头像》 《我是主角》 《修补匠》 《祥云》 《抽屉表情》 《人脸识别虚拟仿真实验》 《照片时光机》 语言翻译: 《CEnews》 《多源在线翻译》 《汉泰小词典》 《识译小程序》 社区周边: 《社区速修》 《虚拟社区》 《简物业》 《租户在线》 《美今管家》 《顾家》 《雨中送伞》 《盲小鹿》 树洞与留言: 《海豚时光瓶》 《树洞》 《苦海匿舟》 《深大小树洞》 《LMSH7TH》 简历与工作: 《个人简历Plus》 《快速找工作》 《猿宝典》 《云线名片》 《InterviewHub》 《企业招聘》 《JF校园云招聘平台》 《普罗名特》 《校园招聘》 资讯与娱乐: 《Killkinfe》 《开心小杜》 《旅小布短视频》 《心灵鸡汤大全》 《轨道nighty night》 《拯救不开心》 《大宗交易数据查询分析助手》 《糗事》 旅行: 《我的旅行箱》 《云航助手》 《宝塔出行》 《PicGo图旅》 城市宣传与服务: 《数字余杭》 《哏儿通》 《联系群众客户端》 《城市预警系统》 《郑州限行查询》 《佤山行》 商业工具: 《软著助手》 《义思丽代办平台》 《契约farm》 办事工具: 《省计数字监理》 《OA外勤管家》 《报工小助手》 《make的测评程序》 《实验室管理小程序》 《星河意见箱》 《梦凡云OA》 《微助helper》 《群消息公示》 《安全帽智慧监控小程序》 地图打卡: 《高级打卡鸡》 《摄影地图游客版》 《同学在哪儿》 《每日步数打卡》 《生活智打卡》 《打卡日历》 《Mayday Online》 《嘿!我在这儿!》 《心里有树》 《地图留言》 《印纪》 天气与日历: 《一眼天气》 《历史日历》 《7日天气》 《历史上的今天TIH》 《实用小工具》 游戏娱乐: 《趣味游乐城》 《假如生命很短暂》 《磁力积木3D预览》 《MusicColorBlock-Detail》 《消灭癌细胞》 《红小包抽奖》 《大师请提笔》 《画画的北鼻》 《东方小游戏》 存储与分享: 《酷传CoolTran》 《我存》 《次元乌托邦云网盘》 《闪加》 《悦分享》 《云享坊》 生活工具: 《WiFi生成码》 《古老的API小工具》 《日常工具box》 《柠檬收纳》 《买它or not》 《我车呢》 《微信小程序工具箱》 《小记易》 《电魔方智能家居》 《缸中之鱼导购系统》 《格式转换工厂》 《小神助手》 《我家的WIFI》 二手交易与租赁: 《大学校园闲置物品交易平台》 《宝宝约玩》 《精简之校园二手交易平台》 《学辰ing》 《二手市场》 《虾麦》 《校园二手购》 《零工哥》 《易珠》 《瓜大e拼车》 外卖点单收银: 《来一杯a》 《美食屋》 《外卖系统》 《便利下单助手》 《云智慧收银》 《超市Boss助手(零售助手)》 《为特餐饮助手》 《Holly食刻》 《微信自助点餐小程序》 《餐饮流水记账》 《seven取餐小程序》 《校内外卖》 《秀食餐饮小程序》 《校云通》 日记博客论坛: 《博客系统》 《天天读书》 《myVlog》 《红推》 《论坛小程序》 《一瞬相册》 《席博》 《一只书匣》 《社交平台》 《图迹圈》 《MallBook》 《青存纪》 《酒肆 家谱》 《比斯兔u》 《校园书友》 《CC交个朋友》 《点滴互助》 《广大搜搜》 《社交点评》 《迷你论坛》 《Simple Note 短记》 垃圾分类: 《垃圾问问》 《垃圾分类小程序》 《垃圾分类赢好礼》 宠物: 《萌宠创造营》 《宠幸治疗》 《宠物营地》 《流浪猫速查手册》 《泊宠》 物联网: 《lononiot》 《LoRa智能家居管理》 《温湿度实时监控及开关控制小demo的设计》 《HomeAssistant》 《流量计设备性能测试平台》 疫情防控: 《行程助手Plus》 《每天都要上报体温》 《校园疫情管理小程序》 《CUMTB疫情管控期间学生外出申请系统》 《疫简签》 地摊: 《地摊生活》 《逛逛地摊》 《迷你小摊》 二、学习笔记 (一)名词解释 1,分录: 会计分录亦称“记账公式”,简称“分录”。它根据复式记账原理的要求,对每笔经济业务列出相对应的双方账户及其金额的一种记录。 2,文玩: 指的是文房四宝及其衍生出来的各种文房器玩。这些文具造型各异,雕琢精细,可用可赏,使之成为书房里、书案上陈设的工艺美术品。 3,sentry: Sentry是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。 4,GitHub: 是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。 5,软著: 全称是计算机软件著作权,是指软件的开发者或者其他权利人依据有关著作权法律的规定,对于软件作品所享有的各项专有权利。 6,打卡: 网络流行词,原指上下班时刷卡记录考勤。现衍生指到了某个地方或拥有某个事物(一般会向他人展示)。网红、圣地打卡。 7,番茄工作法: 是简单易行的时间管理方法。使用番茄工作法,选择一个待完成的任务,将番茄时间设为25分钟,专注工作,中途不允许做任何与该任务无关的事,直到番茄时钟响起,然后进行短暂休息一下(5分钟就行),然后再开始下一个番茄。每4个番茄时段多休息一会儿。 8,码农: 可以指在程序设计某个专业领域中的专业人士,或是从事软体撰写,程序开发、维护的专业人员。但一般Coder特指进行编写代码的编码员。 9,树洞: 来源于童话故事《皇帝长了驴耳朵》,意思是一个可以袒露心声的地方,是指可以将秘密告诉它而绝对不会担心会泄露出去的地方。 10,AI: 全程人工智能(Artificial Intelligence),英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。 11,OA: 办公自动化(Office Automation,简称OA)是将现代化办公和计算机技术结合起来的一种新型的办公方式。办公自动化没有统一的定义,凡是在传统的办公室中采用各种新技术、新机器、新设备从事办公业务,都属于办公自动化的领域。 12,素拓: “素质拓展训练”的简称。素质拓展起源于国外风行了几十年的户外体验式训练,通过设计独特的富有思想性、挑战性和趣味性的户外活动,培训人们积极进取的人生态度和团队合作精神,是一种现代人和现代组织全新的学习方法和训练方式。 13,磁力积木: 由若干个不同形状的积木单体组成。在各个单体的边沿嵌有磁铁或磁片,磁铁上履盖有一层搪瓷,利用磁力使各单体紧密连接在一起。 14,教资: 指教师资格证考试,是由教育部考试中心官方设定的教师资格考试。 15,Instagram: 又叫照片墙,是一款运行在移动端上的社交应用,以一种快速、美妙和有趣的方式将你随时抓拍下的图片彼此分享。 16,Redpoint: 攀岩术语,是指事前曾练习爬过该路线,以先锋攀登的方式、无坠落地完攀该路线。 17,头马: 是Toastmasters的中文简称,于1924年在美国加州成立。是一个非盈利性的、由会员自行管理的组织,目前已在全球一百多个国家成立了上万个俱乐部。 *上述名词介绍来自百度百科与知乎。 (二)出现的学校 河北科技大学(河北-石家庄) 电子科技大学(四川-成都) 重庆邮电大学(重庆) 哈尔滨理工大学(黑龙江-哈尔滨) 桂林航天工业学院理学院(广西-桂林) 河南理工大学(河南-焦作) 河北北方学院(河北-张家口) 北方民族大学(宁夏-银川) 山西大学(山西-太原) 西安交通大学(陕西-西安) 西安电子科技大学(陕西-西安) 华中科技大学(湖北-武汉) 深圳大学(广东-深圳) 浙江大学(浙江-杭州) 湖南大学(湖南-长沙) 武汉大学(湖北-武汉) 广东技术师范大学(广东-广州) 西北民族大学(甘肃-兰州) 北京邮电大学(北京) 中国民航大学(天津) 广东机电职业技术学院(广东-广州) 长江大学(湖北-荆州) 华南理工大学(广东-广州) 包头铁道职业技术学院(内蒙古-包头) 重庆工程职业技术学院(重庆) 江苏大学(江苏-镇江) 南京邮电大学(江苏-南京) 华南理工大学广州学院(广东-广州) 长安大学(陕西-西安) 泉州师范学院(福建-泉州) 桂林电子科技大学(广西-桂林) 广西医科大学(广西-南宁) 华南农业大学(广东-广州) 山西农业大学(山西-晋中、太原) 南京大学金陵学院(江苏-南京) 广东石油化工学院(广东-茂名) 兰州交通大学(甘肃-兰州) 东华理工大学(江西-南昌、抚州) 中山大学南方学院(广东-广州) 广州大学(广东-广州) 中国矿业大学(北京) 南京工业大学(江苏-南京) 中北大学(山西-太原) 华中农业大学(湖北-武汉) 东莞理工学院(广东-东莞) 广东工业大学(广东-广州) 上海电机学院(上海) 南宁职业技术学院(广西-南宁) 台州职业技术学院(浙江-台州) 福州大学(福建-福州) 厦门理工学院(福建-厦门) 美国纽约大学(美国) 英国曼彻斯特大学(英国) 昆明理工大学(云南-昆明) 天津城建大学(天津) 北京工业大学(北京) 广东建设职业技术学院(广东-广州) 湖北师范大学(湖北-黄石) 许昌学院(河南-许昌) 西北工业大学(陕西-西安) (三)个人认为的特别题材 动物保护-鹦鹉AI端侧识别 健康管理-吃药小助 学习工具-口算助手 商业工具-软著助手 图像处理-摄影地图游客版 AI换脸-我是主角 个性服务-雨中送伞 情侣生活-恋人小清单 心情宣泄-苦海匿舟 走失找回-月见 AR躲猫猫-萌宠创造 文艺共鸣-轨道nighty night 心里健康-心暖农侬 模拟红包-红小包抽奖 攀岩健身-RedPoint 职校沟通-铁路生涯 酿酒乐趣-趣酿 盲人助力-盲小鹿 历史日历-历史上的今天 消防检查-火查查 情侣福音-情侣券 家庭互助-顾家 宠物关注-流浪猫速查手册 停车助手-我车呢 诗歌创作-AI写诗 智能家居-LoRa智能家居管理 智能招领-悦寻失物招领 你画我猜-画画的北鼻 随手反馈-城市预警系统 仿真识别-人脸识别虚拟仿真实验 智慧校园-ITD智慧校园 活动协调-哪天约 家庭物联-HomeAssistant 地热监测-流量计设备性能测试平台 (四)官方公布的复赛名单 校园赛道: [图片] 职业赛道: [图片] (五)官方公布的决赛名单 校园赛道: [图片] 职业赛道: [图片] (六)最终决赛成绩 校园赛道: [图片] 职业赛道: [图片]
2020-11-14 - 小程序快速注册免认证费教程
功能描述 快速创建小程序功能优化了小程序注册认证的流程,采用法人人脸识别方式替代小额打款等认证流程,极大的减轻了小程序主体、类目资质信息收集的人力成本。第三方只需收集法人姓名、法人微信、企业名称、企业代码信息四个信息,便可以向企业法人下发一条模板消息来采集法人人脸信息,完成全部注册、认证流程。快速创建小程序接口能帮助第三方迅速拓展线下商户,拓展商户的服务范围,占领小程序线下商业先机。 通过该接口创建小程序默认“已认证”。为降低接入小程序的成本门槛,通过该接口创建的小程序无需交300元认证费。 注:该功能只能创建线下类目小程序,创建线上类目小程序将被驳回,且影响第三方调用该接口的quota。 小程序类目参考: 使用教程注册流程第三方平台1. 第三方平台需具有以下权限集(更新权限集后,需通过审核并全网发布后才可生效) 2. 全网发布必须审核通过[图片] 3. 微擎或独立版平台配置微信开放平台信息 提交申请1. 收集法人微信、法人姓名、企业名称、信用代码四个商户信息外加第三方客服电话,方便商家与第三方联系(建议填写第三方客服电话);企业名称需与工商部门登记信息一致;法人姓名与绑定微信银行卡的姓名一致。信息收集时要确保四个信息的对应关系,否则接口无法成功调用。 2. 登录应用系统,填写对应信息(【小程序】->【快速注册】[图片] 3. 注册记录显示已经提交的注册信息,查询进度功能由于微信官方接口限制,每条记录半个小时以内只能查询一次。 法人认证 通过法人&企业主体校验,微信平台向法人微信下发模板消息。法人需在24小时内点击消息,进行身份证信息与人脸识别信息收集;[图片] 注意事项1. 注意保证主体信息与工商部门登记一致。如:广州和广州市;国家企业信用信息公示系统:http://www.gsxt.gov.cn/index.html 2. 微信号填写错误,需正确引导获取位置“微信”-“我”(不能使用手机号、QQ号) 3. 确保微信号主人和微信支付绑定银行卡的主人姓名一致。核实用户是否有改过名字,或者近期有做身份证升级(从15位身份证升级成18位身份证)。 4. 刚提交任务不会马上收到,会有几分钟或者十几分钟延迟(实际时间取决于信息收集的准确程度) 5. 若提示法人验证失败,查看服务器消息状态码(status),调整信息,重新提交任务。 登录信息完善法人信息认证通过后,用户需通过微信公众平台找回账号密码,管理小程序。 获取小程序的原始ID 获取方式:关注公众平台安全助手。点击菜单栏【绑定查询】,选择微信号绑定账号,选择小程序,可以看到新认证的小程序的原始ID。 注:这个新注册的小程序没有头像和名称,而且后方还带有已授权第三方的字样。 找回账号密码访问网址:https://mp.weixin.qq.com/,进入微信公众平台后台的登录界面,点击 找回账号密码,选择找回账号,输入获取到的原始ID。 [图片] [图片] [图片] 按照要求填写企业信息、公户信息、管理员信息以及验证邮箱,管理员会收到一条打款验证的模板消息,根据微信提供的账号和金额给微信打款 [图片] 1. 打款成功后会受到一条提示模板消息,同时,验证邮箱内会收到一条重置密码的链接。点击链接进行密码重置。成功获取小程序的账号密码,登陆后填写基本信息,而且小程序是已经认证的! 注意事项1. 提前准备一个未注册过小程序公众号的邮箱,找回密码后这个邮箱将会作为小程序的登录邮箱! 2. 没有公户的商家请根据微信提示进行验证。 3. 选择类目时必须选择线下类目
2020-07-05 - 微信开放平台账号如何迁移?
微信开放平台账号如何迁移,原来公司已注销,先想把原公司账号的应用迁移到新注册公司的账号上。麻烦问一下如何操作?
2020-10-14 - 小程序内怎么调试web-view?
开发工具上在web-view页面内点击鼠标右键有个调试的选项 需要在真机上调试需要自行引入vconsole:https://github.com/Tencent/vConsole/blob/dev/README_CN.md
2019-10-09 - ios 调用wx.opendocument 没有反应调用成功了为什么打不开文件? 安卓的可以
微信v1.02.1910120 [图片]
2019-11-18 - H5如何跳转微信小程序?
之前遇到一个需求,就是要从H5跳转到小程序里,但是微信之前一直没有提供接口做跳转,我们只能做降级方案,在要跳转小程序的地方做了一个弹窗,弹窗里面放小程序码,引导用户长按识别小程序码,然后跳转到小程序内,整个流程非常之长,转化率可想而知也是很低的。 今天刚好看到有人技术群里面问了这个问题,于是我就去看了下微信的文档,发现微信偷偷的更新的这个接口,可以让微信浏览器下的H5跳转到小程序内。 相关文档在这边: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html 用的是JS-SDK的接口,需要使用到js-sdk-1.6.0的版本才有支持,https://res.wx.qq.com/open/js/jweixin-1.6.0.js wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 jsApiList: [], // 必填,需要使用的JS接口列表 openTagList: [] // 可选,需要使用的开放标签列表,例如['wx-open-launch-app'] }); 在wx.config下面多了一项openTagList,开放标签列表,目前支持配置wx-open-launch-weapp,wx-open-launch-app wx-open-launch-weapp 指H5跳转小程序 wx-open-launch-app 指H5跳转app 我们主要介绍的是wx-open-launch-weapp H5跳转小程序 先上才艺: [图片][图片][图片] html代码如下: var btn = document.getElementById('launch-btn'); btn.addEventListener('launch', function (e) { console.log('success'); }); btn.addEventListener('error', function (e) { console.log('fail', e.detail); }); username为小程序的原始id,path对应的是小程序的链接地址。之前有写过微信H5的应该会知道怎么把这段代码嵌入到之前的代码里面。 目前此功能仅开放给已认证的服务号,网页的域名要在服务号的“JS接口安全域名”下。 亲测<wx-open-launch-weapp>可以跳转到任意合法合规的小程序,是任意的小程序都能跳转!!!!这个接口真开放(不怕人干坏事?) PS: 有个坑,官方文件说的path是/a/b/c?d=1&e=2#fg,类似的这样的链接格式,但是我自己亲测如果直接使用/a/b/c?d=1&e=2#fg这样格式的链接会报页面不存在,然后我想到了小程序那边复制链接的时候会在链接后面加上.html,于是挖槽的事情发生了,把path链接格式换成/a/b/c.html?d=1&e=2#fg这样就能正常访问,不知道是微信故意这样设计的还是bug,有待考证。 然后这个接口真的可以干好多坏事,希望大家能用正确的价值观来正确使用此接口。 微信开放标签有最低的微信版本要求,以及最低的系统版本要求。 如果开发过程中出现以下情况的,要确认一下,微信版本要求为:7.0.12及以上。 系统版本要求为:iOS 10.3及以上、Android 5.0及以上。 [图片]
2020-07-09 - 手机授权拒绝时候的返回信息?
正常情况下手机授权点击拒绝的时候是这样的 e.detail.errMsg=getPhoneNumber : faili user deny; 但是我在huwwei-vtr-al00 这款机型上 点击拒绝的时候返回是e.detail.errMsg=getPhoneNumber : faili : user deny; 请问这个是bug还是以后的判定需要加上这个判定条件? 环境是体验版本
2019-08-09 - Kbone原理解析 & 小程序技术选型
Kbone原理解析 官方介绍 “Kbone 是一个致力于微信小程序和 Web 端同构的解决方案。” Web端框架基本原理 首先我们来看下普通Web端框架,以Vue框架为例,一份Vue模板对应一个组件,在代码构建阶段编译成调用Dom接口的JS函数,执行此JS函数就会创建出组件对应的Dom树,从而渲染到浏览器页面上。 [图片] 然而,小程序是双线程的,并没有Dom树的概念,逻辑层和视图层完全分离,逻辑层是一个纯粹的JSCore,开发者可以编写JS脚本,但是无法直接调用Dom/Bom的api, 没有任何浏览器相关的实现。 在小程序中,视图层和逻辑层的交互是通过数据和时间驱动的。 因此,要实现跨端同构,问题是:怎么将web端代码转为小程序代码? 业界常规做法 目前业界流行的第三方跨端框架们,常规做法都是:静态编译兼容。 [图片] 原理是把代码语法分析一遍,然后将其中的模板部分翻译成对应的跨端需求的模板(微信小程序、支付宝小程序、H5、APP等)。 静态编译最大的局限性是无法保证转换的完整性,因为Vue模板和WXML模板的语法并不是直接对等的,Vue的特性设计也和小程序的设计无法划等号,这自然就导致了部分Vue特性的丢失。 比如像Vue中的v-html指令、ref获取Dom节点、过滤器等就通通用不了。 除了Vue自身的特性外,一些原本依赖Dom/Bom接口的Vue插件页无法使用,例如Vue-Router。 Kbone的做法 Kbone是通过提供 适配器 的方式来实现同构,即运行时兼容,而非静态编译。 Kbone的适配器核心包含两个部分: miniprogram-render: 仿造Dom/Bom接口,构造仿造Dom树; miniprogram-element: 监听仿造Dom树变化,渲染到页面,同时监听用户行为,触发事件。 仿造Dom树和浏览器的运行时对比: 仿造Dom树: 利用内置组件和自定义组件的自引用来进行递归,创建组件树。 如图,自定义custom-dom为递归自引用组件: [图片] 递归的终止条件是遇到特定节点、文本节点或者children空节点。 在创建出组件树后,将Dom节点和自定义组件实例进行绑定,以便后续的Dom更新和操作。 [图片] kbone这里还对节点数进行了优化: 如果一个dom节点对应一个自定义组件的话,就会创建很多自定义组件,这样会很浪费开销,这里做了子树的合并,也就是说3层才创建一个自定义组件,节省开销。 优化前:17个dom=17个自定义组件; 优化后:17个dom=4个自定义组件,蓝色那个是单节点,会合并到上面的树; [图片] dom 子树作为自定义组件渲染的层级数是可以通过配置传入,理论上层级越多,使用自定义组件数量越少,性能也就越好。 一棵很大的 Dom 树,一次性 setData 到视图层,可能会超过 setData 的大小限制(1024kB),拆分成多棵子 Dom 树,然后分批的 setData 到视图层,可以节省开销。 事件监听 小程序的事件是视图层到逻辑层的通讯方式,事件绑定在组件上,当被触发时,就会执行逻辑层中对应的事件处理函数。 小程序的捕获冒泡是在视图层view端,因此逻辑层在整个捕获冒泡流程中各个节点接收到的事件不是同一个对象,小程序事件的捕获冒泡和阻止冒泡等操作必须在WXML模板中生命,无法使用接口实现。 [图片] 为了能够让web端和小程序端的事件系统行为一致,kbone除了仿造了一份Dom树外,也把整个事件系统仿造了一份,即在仿造Dom树上进行捕获冒泡。 当自定义组件监听到用户的操作后,就将事件发往仿造Dom树,后续自定义组件监听到的同一个事件的冒泡就直接忽略。 当触发改节点,仿造Dom树接收到事件后,再进行捕获和冒泡,让事件在各个节点触发。 [图片] Kbone的优势 支持多个前端框架:Vue、React、Preact 等 支持更为完整的前端框架特性: Vue 中的 v-html 指令、Vue-router 插件等 提供了常用的 dom/bom 接口 可以使用小程序本身的特性: live-player 内置组件、分包功能等 提供一些 Dom 扩展接口:getComputedStyle 接口等 Kbone实践 脚手架kbone-cli 官方已经提供了kbone-cli可以用来快速开发: 用npm全局安装kbone-cli [图片] 可以根据自己的技术栈选择不同的开发模板:React/Vue/Omi/Preact [图片] 然后就可以愉快的进行开发啦~ [图片] 生成的demo项目结构如下: [图片] demo中包含了多页跳转、vue-router、vuex等的使用示例,以及mp-webpack-plugin的配置示例。 对于多页面的应用,在 Web 端可以直接通过 a 标签或者 location 对象进行跳转,但是在小程序中则行不通。同时 Web 端的页面 url 实现和小程序页面路由也是完全不一样的。 Demo示例对比 [图片] 其中,有一部分两端差异的业务逻辑功能,也给出了3中不同的解决方案: 利用vue-improve-loader,在构建时对dom树节点进行删减,在需要提出的节点加上check-reduce属性 利用reduce-loader,将业务中不需要被打包的代码进行去除,使用行内loader和环境变量来判断 使用样式隐藏,即设置不需要显示的节点样式为 display:none 其他问题 在实际开发中,还会碰到一些细节,例如: 多页面开发:修改webpack和mp-webpack-plugin配置 小程序内置组件: 部分用html标签代替,其他用wx-component + behavior标签 小程序自定义组件:修改mp-webpack-plugin配置,补充wxCustomComponents字段,将自定义组件放入组件根目录,使用自定义组件 自定义app.js和app.wxss:监听app的生命周期,修改webpack配置补充app.js的构建入口,修改插件配置的generate.app字段,补充app.js 扩展dom/bom对象和API:使用 window.$$extend追加方法 代码优化:用reduce-loader做体积精简,dom树精简用vue-improve-loader 区分环境实现不同功能:process.env.isMiniprogram 更新迭代 kbone由于目前在快速发展期,更新迭代非常迅速,以下特性是对比了8月份的版本和11月份版本,可以看出已经解决了近2/3的问题。 [图片] 小程序技术选型 详细了解了kbone之后,我们来分析下小程序技术框架到底应该怎么选? kbone & 小程序原生 已有web版,需要小程序版:kbone 跨平台需求(web + 小程序):kbone 对性能特别苛刻 or 追求稳定 or 要用最新功能:小程序原生 页面节点数量特别多( 1000 节点以上),且对渲染性能要求高:静态模板转义方案(第三方框架:mpvue/taro等) 第三方框架 MpVue :不推荐再用了,坑越来越多,内部也表示之后不会投入太多维护 WePY 1.7.x :不推荐再用了:1.7.x 的版本在最初的设计上的缺陷导致遗留了很多比较严重的问题 WePY 2.0:现在还是 alpha 阶段,内外部有一些小程序在跑,体验和反馈还可以。但依然 issue 比较多。害怕踩坑的也不推荐使用 Taro: 也还是有不少问题,但相对来说应该是比 mpVue 和 WePY 更稳定一点 Uni-app:mpvue的衍生版,跨端 (官方示例有6端) 支持的很好,在H5端实现了一套微信模拟器,可以尝试,是目前唯一支持app端的商用方案,有独立的编辑器HBuildX Chameleon: 统一的多态协议,渐进式跨端,提供脚手架命令工具,规划比较宏大 Omi :基于Taro完成的框架,kbone有支持omi的模板 Nanachi: 基于react的编译期间的转义框架 总结 没有跨端需求,只需要微信小程序 ==> 小程序原生 web端转小程序 or 两端 or 想要尝鲜 ==> kbone 多端 or Vue 技术栈 ==> uni-app 多端 or React 技术栈 ==> taro 不介意学习新技术栈 ==> wepy 2.0 or chaemeleon 写在最后 小程序在非常快速的更新迭代,就算是原生框架也还是有一些坑的,因此没有哪种框架是百分之百完美,需要根据业务具体需求以及自身技术栈偏好来进行选择。 文章中提及到的部分第三方框架只是参考了官方文档,没有逐个一一尝试,有问题麻烦指出,鞠躬~~ [图片]
2022-03-10 - kbone,十分钟让 Vue 项目同时支持小程序
什么是kbone 微信小程序开发过程中,许多开发者会遇到 小程序 与 Web 端一起的需求,由于 小程序 与 Web 端的运行环境不同,开发者往往需要维护两套类似的代码,这对开发者来说比较耗费力气,并且会出现不同步的情况。 为了解决上述问题,微信小程序推出了同构解决方案 [代码]kbone[代码] 来解决此问题。 那么,[代码]kbone[代码] 要怎么使用呢?这里我们将通过一个 [代码]todo[代码] 的例子来跟大家讲解。 基本结构 首先,我们来看下一个基本的 kbone 项目的目录结构(这里的 [代码]todo[代码] 是基于 [代码]Vue[代码] 的示例,[代码]kbone[代码] 也有 [代码]React[代码],[代码]Preact[代码],[代码]Omi[代码] 等版本,详情可移步 kbone github)。 因为 kbone 是为了解决 小程序 与 Web 端的问题,所以每个目录下的配置都会有两份(小程序 与 Web 端各一份) [图片] 入口 不管是 小程序 端还是 Web 端,都需要入口文件。在 [代码]src/index[代码] 目录下,[代码]main.js[代码] 为 Web 端用主入口,[代码]main.mp.js[代码] 则为 小程序 端用主入口。 当然,Web 端会比 小程序 多一个入口页面,即 [代码]index.html[代码](位于根目录下)。 [图片] 下面两段代码分别是 小程序端 入口与 Web 端入口的代码,可以看到 小程序端的入口代码封装在 [代码]createApp[代码] 函数里面(这里固定即可),内部会比 Web 端多一个创建 [代码]app[代码] 节点的操作,其他的基本就是一致的。 [代码]// 小程序端入口 import Vue from 'vue' import todo from './todo.vue' export default function createApp() { // 创建app节点用于绑定 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) return new Vue({ el: '#app', render: h => h(todo) }) } [代码] [代码]// web端入口 import Vue from 'vue' import todo from './todo.vue' new Vue({ el: '#app', render: h => h(todo) }) [代码] todo.vue 在上面的入口图可以看到,源码目录中,除了入口文件分开之前,页面文件就是共用的了,这里直接使用 Vue 的写法即可,不用做特殊的适应。 配置 写完代码之后,我们要怎么跑项目呢?这时,配置就派上用场啦。 Web 端配置为正常的 Vue 配置,小程序端配置与 Web 端配置的唯一不同就是需要引入 [代码]mp-webpack-plugin[代码] 插件来将 Vue 组件转化为小程序代码。 [图片] 构建代码 接着,我们需要构建代码,让代码可以运行到各自的运行环境中去。构建完成后,生产代码会位于 dist 目录中。 [代码]// 构建 web 端代码 // 目标代码在 dist/web npm run build // 构建小程序端代码 // 目标代码在 dist/mp npm run mp [代码] 小程序端 的构建会比 Web 端的构建多一个步骤,就是 npm 构建。 进入 [代码]dist/mp[代码] 目录,执行 [代码]npm install[代码] 安装依赖,用开发者工具将 [代码]dist/mp[代码] 目录作为小程序项目导入之后,点击工具栏下的 [代码]构建 npm[代码],即可预览效果。 效果 最后,我们来看一下 todo 的效果。kbone 初体验,done~ todo 代码可到 kbone/demo13 自提。 [图片] 最后 如果你想了解更多 kbone 相关的使用及详情,可移步 kbone github。 如有疑问,可到 Kbone小主页 发帖沟通。
2020-04-22 - 支付中签约,获取预支付id报错签名错误?
签名校验通过了,结果还是报错<xml><return_code><![CDATA[FAIL]]></return_code> <return_msg><![CDATA[签名错误]]></return_msg> </xml> 调用的是:https://api.mch.weixin.qq.com/pay/contractorder JSAPI方式,openId有传 appid:wx4ef75de5609f6986 [图片]
2020-01-08 - 社区常见相关问题总结贴--新增外部链接管理规范、模版消息公告
9月份社区为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范、然后总结下关于新规以及社区的常见问题、不定时增加社区重要公告 增加小程序违规公告:https://developers.weixin.qq.com/community/operate 增加小程序模版消息公告:https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401?blockType=1 增加微信外部链接内容管理规范:https://weixin.qq.com/cgi-bin/readtemplate?t=weixin_external_links_content_management_specification&from=timeline&isappinstalled=0 相关链接模版消息公告 新规范链接 类目资质 恶意对抗平台规则的违规行为公告 小程序修改名称说明 小程序账号相关问题 小程序常见违规整改处理方案 微信外部链接内容管理规范 常见问题 服务范围开放的小程序? 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 若小程序属于第一种服务范围开放的小程序,还是建议可以在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项 服务范围特定的小程序? 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序 只用于公司内部使用的小程序,应该怎么改? 公司内部小程序,只限公司内部使用。如果是公司内部人员,授权微信会自动登录,或者后台数据库存在的手机号码,利用手机验证码登录也可以,这个并没有账号密码登录模式。 你好,经核实,贵方小程序功能无法体验,建议增加一种登录方式(如:账号、密码),并提供可登录体验的测试账号信息,填写在 版本描述处提交,以便审核人员及时体验到小程序功能 经核实,贵方小程序打开即要求授权信息,且点击取消授权后仍强制授权,为企业内部工具,建议在小程序的登录页面明确介绍小程序的具体功能,并且在登录及授权界面为用户提供接受/拒绝登录的权利,由用户自主选择是否进一步授权登录。 类似本地化生活服务的小程序只开通了部分城市,审核被拒绝? 经核实,贵方小程序打开提示:该地区暂未开放,功能无法体验,建议增加手动定位,并在版本描述中写明已上架正式内容的城市,以便审核人员审核。 审核团队的测试微信或手机号并未在我们数据库存在,导致审核失败? 或者类似的问题:我们可以提供测试手机号,或者测试微信账号密码,但是这都涉及到手机验证码,请问当你们登录的时候,我如何把验证码发送给你们? 请将测试信息填写在版本描述处提交审核,审核人员不能对外联系,所以请提供一个写死的验证码,感谢您的支持和理解! 因为小程序特殊性,用户打开必须要获取用户位置。地理位置授权影响登陆规范要求? 你好,如小程序仅要求地理位置授权,暂不属于帐号登录规范要求内 关键字搜索排序与审核通过后搜索不到小程序? 系统会根据query和小程序的相关性来判断召回和排序,后续我们会优化搜索策略,感谢反馈。有异议,提供appid、搜索词、搜索入口、搜索页面截图 刚审核通过发布后搜索不到是有延迟的,大概半天左右的延迟。如果着急可以发帖,注意发帖规范、附上小程序的APPID 仅提供注册服务的小程序,审核拒绝? 你好,如果你的小程序仅提供线上注册功能,后续服务是需要以其他方式提供的话,可以在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或登录。 然后官方加了一句⬇️。说明让用户体验小程序功能是很重要的。 而如果你的小程序除了线上注册外,还同时提供其他线上服务,建议先让用户体验、知悉小程序的功能后,再要求用户进行注册、登录。 审核好几天了,官方可以加急审核吗?? 你好,暂不支持加急审核,请耐心等待审核结果。根据社区规定,提审时间为7个工作日内的催审问题暂不予反馈,故此贴隐藏 关于 企业信息或法定代表人信息不一致? 建议发帖联系客服。注意发帖格式 等15个工作日,平台是拉取的工商局的数据。 可以联系客服工作人员吗,急在线等。 联系客服工作人员的正确姿势 是想咨询什么问题呢?不方便发帖咨询的话可以私信哦(进入个人主页私信功能) 审核超过7天了还是没有消息?? 审核时间为7个工作日,和7天还是有点不一样。 审核超过7个工作日,请刷新页面注意查看站内信,点击小🔔查看最新审核消息。 为什么审核我的小程序这么久,别人可以那么快? 官方整理回答 为什么你以前可以2-3小时通过审核,是因为你在运营/性能/用户等指标都达到优秀,所以符合小程序评测——优秀的标准,因此拥有急速审核的权益。常见问题见:https://kf.qq.com/faq/190108BJnmUN190108RrEnqE.html 你在某一次提交版本之后,小程序的性能存在问题,被我们检测到,所以便失去急速审核的权益,如果你需要急速审核,请你优化好小程序的性能再重新提交,你们可以自己在开发阶段利用工具的体验评分面板先自查一次,详细见https://developers.weixin.qq.com/miniprogram/dev/devtools/audits.html 最后补充一点,如果没有达到优秀的标准,日常承诺处理的审核时间是7个工作日内,如果没有超过7个工作日,催提审是不受理的 好消息是:官方加急审核的权益已经在开发,估计不久后会上线。 同一开放平台账号,绑定的移动应用和公众号,获取的unionid为什么不一致? 只要绑在一个开发者帐号下,即使主体不一样,也允许获取到统一的unionID。绑定同一个微信开放平台帐号下,同一个用户的unionID如果不同的,原因只能是开发者搞混openid。openid要对应所属的AppID,才会相同。 举个例子: 1. 小程序AppID:wxc104eb635b8cxxxx ——帐号A, 公众号AppID:wx311a2a9a8e1dxxxx ——帐号B, 2.核实帐号A和帐号B 绑定同一个微信开放平台帐号是:xxxxxx@sina.com ,所以用一个用户的unionID相同, 3.而开发者所反馈的出现unionID不同,原因是:所提供的openid不属于帐号A,也不属于帐号B,而是属于帐号C或帐号D,而帐号C或帐号D并没有绑定在同一个微信开放平台帐号下,所以unionID不同。
2020-01-10 - 小程序没有 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 - 开放平台设置业务类别 41033 invaid register type h
开放平台提小程序设置类别 api /cgi-bin/wxopen/addcategory?access_token=" appid wx84b73785711a9888 返回 "message": "invaid register type hint: [ufXwka05541894]", "errcode": 41033 请问如何解决
2019-04-30 - 第三方平台代小程序提交审核需要设置类目,代小程序添加类目接口在哪里?
第三方平台代小程序提交审核需要设置类目,代小程序添加类目接口在哪里。获取授权的小程序的服务类目为空,如何为授权的小程序添加类目,能否给出相应的接口文档, 该小程序不是第三方平台创建的,所以小程序基本信息设置 的接口都用不了,请问如何处理
2019-08-21 - vue-cli 3.x 快速搭建 vue + ts + kbone + kbone-ui + 云开发 项目
使用的工具和库 @vue/cli v4.1.1 vue-cli-plugin-kbone 0.4.14 kbone-ui 0.5.8 创建项目 关于 [代码]vue-cli 3.x[代码] 的使用这里不做介绍,请移步至Vue CLI官方文档查看 使用 [代码]vue-cli 3.x[代码] 可视化页面根据自己的需求创建一个 [代码]ts[代码] 空项目即可 因为我们是用 [代码]ts[代码] 开发微信小程序,所以需要安装微信小程序对应的 [代码]types[代码] [代码]miniprogram-api-typings[代码], 安装好后需要在 [代码]tsconfig.json[代码] 配置文件中的 [代码]types[代码] 节点添加对应的配置,最好是再加一个忽略校验 [代码]*.d.ts[代码] 的配置项,因为引入的 [代码]types[代码] 在编译时也会校验 [代码]{ "compilerOptions": { ... "skipLibCheck": true, "types": [ ... "miniprogram-api-typings" ], ... }, ... } [代码] 集成kbone [代码]kbone[代码] 提供了 [代码]vue-cli 3.x[代码] 的插件,可以一键集成到 [代码]vue-cli 3.x[代码] 创建的项目中。 在刚刚创建好的空项目中选择 [代码]插件[代码] -> [代码]添加插件[代码] 搜索 [代码]vue-cli-plugin-kbone[代码] 点击安装即可 安装完成后会弹出配置页面 [代码]AppId[代码] 填写自己的小程序 [代码]AppId[代码] [代码]项目名[代码] 填写自己的项目名 [代码]Kbone 入口文件名称[代码] 填写为 [代码]main.mp.ts[代码] [代码]是否需要输出 app.js、project.config.json 等非页面相关文件[代码] 选择 [代码]不输出 project.config.json[代码] [代码]选择 app.wxss 输出配置[代码] 选择 [代码]输出默认标签样式[代码] [代码]选择是否自动构建依赖包[代码] 根据自己使用的工具选择 [代码]npm[代码] 或 [代码]yarn[代码] [代码]是否需要使用 rem[代码] 建议开启 其他的配置项默认即可,然后点击完成 插件会自动生成 [代码]kbone[代码] 的入口文件、配置文件、以及三条命令脚本 入口文件 入口文件就是我们刚才输入的 [代码]main.mp.ts[代码] ,位置在 [代码]src[代码] 目录下,根据自己创建项目时所安装的库进行修改 例如我在创建时使用了 [代码]router[代码] 和 [代码]vuex[代码] 这些需要在入口文件引入的,所以也需要在 [代码]main.mp.ts[代码] 中引入 [代码]import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false // 需要将创建根组件实例的逻辑封装成方法 export default function createApp () { // 在小程序中如果要注入到 id 为 app 的 dom 节点上,需要主动创建 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) return new Vue({ router, store, render: h => h(App) }).$mount('#app') } [代码] 配置文件 配置文件为 [代码]miniprogram.config.js[代码] , 位置在项目根目录,其中的内容自行按需修改,对应的配置项可参考 [代码]kbone[代码] 官方文档 命令脚本 插件会在 [代码]package.json[代码] 中生成的三条命令脚本,分别是 [代码]build:mp[代码] 、 [代码]dev:mp[代码] 和 [代码]mp[代码] 因为我们后面需要再集成 [代码]云开发[代码] 所以命令需要做一点点修改,将打包目录修改为 [代码]dist/mp/miniprogram[代码] [代码]build:mp[代码] 改为 [代码]"build:mp": "cross-env MP_ENV=miniprogram vue-cli-service build --mode production --dest ./dist/mp/miniprogram/common" [代码] [代码]mp[代码] 改为 [代码]"mp": "cross-env MP_ENV=miniprogram vue-cli-service build --mode development --dest ./dist/mp/miniprogram/common --watch" [代码] 集成kbone-ui 万众期待的 [代码]kbone-ui[代码] 终于发布了一部分出来,官方文档 可以继续使用 [代码]vue-cli 3.x[代码] 的可视化页面安装,也可以使用命令行安装 安装完成后在 [代码]web[代码] 端的入口文件 [代码]main.ts[代码] 和 [代码]kbone[代码] 入口文件 [代码]main.mp.ts[代码] 中引入 [代码]kbobe-ui[代码] main.ts [代码]import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import KboneUI from 'kbone-ui' import 'kbone-ui/lib/weui/weui.css' Vue.use(KboneUI) Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app') [代码] main.mp.ts [代码]import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import KboneUI from 'kbone-ui' import 'kbone-ui/lib/weui/weui.css' Vue.use(KboneUI) Vue.config.productionTip = false // 需要将创建根组件实例的逻辑封装成方法 export default function createApp () { // 在小程序中如果要注入到 id 为 app 的 dom 节点上,需要主动创建 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) return new Vue({ router, store, render: h => h(App) }).$mount('#app') } [代码] 集成云开发 完成上面步骤项目大致是可以跑起来了,但是 [代码]kbone[代码] 是不支持 [代码]scoped[代码] 样式的,所以在项目中用到 [代码]scoped[代码] 样式的地方需要自己再改一下,这里就不细说了 还有最重要的一点就是我们在集成 [代码]kbone[代码] 时选择的是 [代码]不输出 project.config.json[代码] 也就意味着打包的时候不会生成 [代码]project.config.json[代码] 文件,原因也是因为 [代码]云开发[代码] 中需要在 [代码]project.config.json[代码] 中配置 [代码]miniprogramRoot[代码] 和 [代码]cloudfunctionRoot[代码] ,所以我们需要手动建一个 [代码]project.config.json[代码] 文件在项目根目录,如果不会建,那就在开发者工具中新建一个 [代码]云开发[代码] 项目,从里面拷一个吧 [代码]project.config.json[代码] 文件创建完后,需要再在项目根目录创建一个 [代码]cloudfunctions[代码] 文件夹,用于存放 [代码]云开发[代码] 的代码,关于 [代码]云开发[代码] 的代码都放在这个文件夹下面就可以了,云开发文档 上面的步骤完成后,还有最后一个关键步骤,配置 [代码]webpack[代码] 在打包项目时将 [代码]云开发[代码] 相关的代码也打包进去 在项目根目录创建一个 [代码]vue.config.js[代码] 文件,添加以下代码 [代码]const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { configureWebpack: config => { if (process.env.MP_ENV === 'miniprogram') { config.plugins.push(new CopyWebpackPlugin([ { from: path.resolve(__dirname, './cloudfunctions'), to: path.resolve(__dirname, './dist/mp/cloudfunctions'), toType: 'dir' }, { from: path.resolve(__dirname, './project.config.json'), to: path.resolve(__dirname, './dist/mp/project.config.json'), toType: 'file' } ])) } } } [代码] 其配置的意思就是如果当前运行的环境变量是 [代码]miniprogram[代码] 那么就将 [代码]cloudfunctions[代码] 文件夹和 [代码]project.config.json[代码] 文件拷贝到小程序的打包目录,到这里就全部集成完了,可以开心的去码代码了 DEMO项目https://github.com/stephenml/kbone-ts-template
2020-04-27 - 有食品经营许可证,要上线小吃快餐还需要其他资质证明吗?
我们有食品经营许可证,经营项目是“预包装食品销售(含冷藏冷冻食品),散装食品销售(不含冷藏冷冻食品)”,现在要上线小吃快餐,还需要其他资质证明吗?
2019-12-26 - 获取用户信息方案介绍
背景 小程序一个比较重要的能力就是获取用户信息,也就是使用 [代码]wx.getUserInfo[代码] 接口。我们发现几乎所有的小程序都会调用这个接口。虽然我们在设计文档上有提出最好的设计是在真正要用户信息的情况下才去获取用户信息,不过很多开发者并没有按照我们的期望去做,导致用户在使用的时候有很多困扰。 归结起来有几点: 开发者在首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框有会使得一部分用户放弃小程序的使用。 开发者没有处理用户拒绝弹框的情况,有部分小程序强制要求用户授权头像昵称等信息才能继续使用小程序。 用户没有很好的方式重新授权,虽然在前几个版本我们增加了[代码]设置[代码]页面可以让用户选择重新授权,但是操作还是不够便捷。 开发者希望进到首页就获取到用户的[代码]unionId[代码],以便和之前已经关注了公众号的用户画像关联起来。 开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致: [代码]getUserInfo[代码]必须通过[代码]wx.login[代码] 在后台生成[代码]session_key[代码] 后才能调用。 为了解决以上几点,我们更新了三个能力: 使用组件来获取用户信息,用户拒绝授权后也可以重新弹窗再次授权 若用户满足一定条件(下文有详细介绍),则可以用[代码]wx.login[代码] 获取到的code直接换到[代码]unionId[代码] [代码]wx.getUserInfo[代码] 不依赖 [代码]wx.login[代码] 就能调用得到数据。 获取用户信息组件介绍[代码][代码] 组件变化: [代码]open-type[代码] 属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码] 为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的[代码]detail[代码]字段中获取到和[代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<[代码][代码]button[代码] [代码]open-type[代码][代码]=[代码][代码]"getUserInfo"[代码] [代码]bindgetuserinfo[代码][代码]=[代码][代码]"userInfoHandler"[代码][代码]> Click me </[代码][代码]button[代码][代码]>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框 组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框 直接获取unionId考虑很多场景下,业务方申请userinfo授权主要为了获取unionid。我们鼓励开发者在不骚扰用户的情况下合理获得unionid,而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得unionid。 在微信开放平台下存在同主体的App、公众号、小程序。 用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 getUserInfo 和 login很多开发者会把login和getUserInfo捆绑调用当成登录使用,其实login已经可以完成登录,可以建立账号体系了,getUserInfo只是获取额外的用户信息。 在login获取到code,然后发送到开发者后端,开发者后端再通过接口去微信后端换取到openid和sessionKey(并且现在会将unionid也一并返回)之后,然后把3rd_session返回给前端,就已经完成登录行为。而login行为是静默,不必授权的,不会对用户造成骚扰。 getUserInfo只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,通过unionId和其他公众号上已有的用户画像结合起来提供历史数据。所以不必在刚刚进入小程序的时候就强制要求授权。 推荐使用方法调用[代码]wx.login[代码] 获取[代码]code[代码],然后从微信后端换取到[代码]sessionKey[代码],用于解密[代码]getUserInfo[代码]返回的敏感数据。 使用[代码]wx.getSetting[代码] 获取用户的授权情况 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 获取到用户数据后可以进行展示或者发送给自己的后端。 文档中的quickStart已经更新 特别注意为了给用户提供更好的小程序环境,我们约定在一段时间后(具体时间会做通知),若还出现以下情况(包括但不限于),将无法通过审核 初次打开小程序就弹框授权用户信息 未处理用户拒绝授权的情况 强制要求用户授权 已经上线的小程序不会受到影响。 FAQ Q: 除了 UserInfo 呢,比如说位置信息 --- ’风の诺言 . A: 其他授权信息不像用户信息那么高频繁,也基本是在使用时候才申请授权,所以没有同 UserInfo 一起给出。我们会先看看 UserInfo 的使用情况再结合具体场景我们会给出相应的方案 Q: 后台要维护用户信息 --- Azleal 我们的小程序业务是功能都需要授权才能使用的(也就是必须拿到unionid获取用户信息) --- elemeNT 我在小程序与服务号的数据需要互通,通过unionId来确定用户的唯一性,如果在用户进入小程序后不强制他授权,单凭一个openid来存储他的用户数据,在用户下次从服务号进入时。不就会产生重复数据吗?就没做到数据互通了 --- ﺭ并向你吐了趴口水ﺭ五年. 另外看到官方提到 要强制推行,我想说我们目前所有用户是通过unionid注册的。那么这些用户就不得不使用 openid重新登录 、注册一遍。更重要的是,之前他们的相关数据都会对应不上(因为你们也不允许强制用户登录授权) --- 羊毛 现在这种方案,不能满足我们的需求,我们的小程序,必须一进入就要获取他的信息,然后加载他的数据; --- 韩文 A: 调用`wx.login`已经可以获取到用户的登录态,已经可以做用户账号的管理。 UserInfo 中带的 UnionId 是额外的信息,没有它完全可以完成登录 对于需要和开发平台绑定的业务进行数据互通的情况,一个新用户进来没有互通数据的情况下也是可以体验到所有业务,那么对于没有授权unionId的用户,可以将其当成是新用户,当真正授权unionId之后再做绑定完全是可以的 Q: 我需要确保用户的唯一性,这样就必须取unionID,否则用户删除了小程序,或者换了设备, 下次再进来这个小程序,该用什么来区分是上次来过的用户呢?? --- WEI+ A: 如果你本身没有其他公众号、App、小程序,那么也就没必要拿到unionid,因为unionid是打通你在开放平台下所有应用的标识 如果只有一个小程序,用 openid 足以, openid 是一个用户对于一个小程序的标识,永远不变 Q: wx.getUserInfo 是网络请求,如果使用了 open-type = "getUserInfo",是否每次点击都会调接口? --- SouthernBox A: 是的,open-type="getUserInfo" 的作用以及内部实现基本和 wx.getUserInfo 一样 区别是一个开发者主动(拒绝一次不再弹窗),一个是用户主动(拒绝任意次都可以重新弹窗) Q: 比如有一个创建按钮,用户点击一次授权了,我已经获取到用户信息,再次点击就没必要再调用 getUserInfo 去网络请求了。 --- SouthernBox A: 可以参考文中 quickStart 的做法,如果已经授权了,那就可以把按钮隐藏,之后的授权直接用API wx.getUserInfo 调用(因为已经授权,所以也不会弹窗),用户也不会再点了 Q: 小程序是不是必须要用微信自带的授权才可以登录 ,能否不使用授权方式登录,用自己系统的api接口数据实现?这个会不会涉及到审核不过的问题??麻烦解答一下 谢谢了。 --- WEI+ A: 自己做登录不会涉及到审核问题。 不过不建议在没有原有账号体系的情况下让用户在小程序内注册,过重的行为会损失用户。 Q: 在小程序中有一个"我的"页面,这是属于会员页,如果用户要进入这个页面就必须授权。交互方式就是在用户未授权情况下整个页面只显示一个授权获取用户信息的button 按钮,这个需要用户自己去触发,算不算强制授权? --- ﺭ并向你吐了趴口水ﺭ五年. A: 强制授权是说如果用户如果不授权基本信息,连最基础的浏览功能都不提供(当然这个也是要分具体的业务场景,不会限制得太死板) 可以有更好的交互,参考下主流App,在未登录的时候点击【我的】页面,也不会直接要求登录,而是展示了一定的页面结构,同时给一个登录按钮(例如【携程】【京东】等),之后再在这个页面做操作的话可以弹一个登录页面或按钮提示用户登录是完全可以的。 上述所说的登录只是用户感知上的登录,从业务逻辑上用户其实在 wx.login 的时候已经完成登录了。 Q: 看了很多评论,有些人还是不知道为什么官方要这样做,我作为一个商家角度来说下。 --- Mr.J 1. 比如我们要做一些户外推广的二唯码,用户只看到了你的图片宣传单,扫描二唯码一打开就提示“需要获取你的个人信息,您是否允许”,你不要当自己是开发者当自己是一个正常人,看到这个提示我相信很多人的第一反应就是拒绝。如果第一步已经把你拒之门外,谈何营销? 2. 没有小程序之前,我们在公众号有很多用户,都绑定了unionid,有小程序之后我们考虑怎么让用户接受小程序,可以静默登录我觉得非常好,从公众号过来的用户可以直接就登录了,没有任何提示,完美的对接,这是一个很好的体验。 A: 说得很好,我们的这些改造不仅是为了开发者,同时也是为了这个生态下的用户考虑。希望开发者们也能站在用户的角度去思考怎么做一个产品。 Q: 我不明白为什么login 给多个unionid 为什么不行? unionid也不能算是个人信息吧,给多个unionid可以更方便开发者,而且很多情况下就不用调用getUserInfo了 --- candyTong 我们提个建议,能否直接开放unionid呢?这样也许会有许多小程序不需要再弹窗了。既一定程度保障了用户体验,也照顾到了我们开发者的体验。 --- 羊毛 A: 如果直接开放了unionid,就会出现这种情况:当你作为一个用户进入一个小程序,这个小程序并没要求你授权就直接把你的头像昵称显示出来(它之前把unionId对应的头像昵称都存了下来),但是这个小程序主体(open平台主体和公众平台主体并不相同)相关的任何一个应用你从来没用过,你会不会觉得很奇怪并且很不舒服,觉得自己在微信内的用户信息没有丝毫的保障? Q: 那有推荐的比较好的例子么?对于必须使用用户头像、昵称这些信息的小程序而言 --- 亚里士朱德 A: 首先,没有什么逻辑是一定要使用用户的头像、昵称才能work的。对于这个case,完全可以先用默认头像、匿名昵称先做替代,用户点击默认头像后就可以弹出授权信息,非常的水到渠成。 Q: 之前看了这个帖子一直在思考,如果是一进去需要回去用户的地理位置信息显示到地图上的呢?这样算不算是一进去就弹窗授权获取用户信息? --- 吴俊绩🤔 A: 地图的情况和获取用户信息不同,我们目前还没对地图的授权请求有所调整。当前不受上述策略的影响 Q: 对于开发者而言,小程序与公众号是同级的,只是不同的入口 但是这样的设计,公众号与小程序成了主从关系咯 --- log琥珀① A: 并无什么主从关系,只是多一个渠道让开发者可以更方便的获取到已经是该主体下用户的unionId
2017-08-07 - 在小程序里,识别小程序码,没有反应请问在修复了吗?
在小程序里,识别小程序码,本来识别小程序码或者独立扫码会打开本程序的其他页面,请问在修复了吗? https://developers.weixin.qq.com/community/develop/doc/00080205234788ed7099799765bc00
2019-12-18 - 小程序代码包总包上限提升至12M、新增内置扩展库支持
小程序代码包总包上限提升至12M为了让开发者开发出功能更丰富的小程序,小程序或小游戏代码包总包上限由8M提升到12M。建议开发者优化小程序性能并将每个分包做得尽可能小,以便提升用户的打开速度,优化用户体验。 开发者可登录开发者工具,在详情-项目配置中点击刷新按钮,获取最新的配置信息。 [图片] 内置扩展库支持扩展库是将官方提供的一系列扩展能力打包而成的一些类库。为方便开发者使用,我们在最新的nightly版开发者工具支持了内置扩展库,开发者只需在app.json配置文件声明引用指定扩展库即可,无需自行引入相关 npm 包且不计入小程序代码包大小,目前支持了kbone和WeUI两种扩展库,更多详情请参考文档。 欢迎各位开发者接入,如有问题,可在本帖下方留言或在社区发帖反馈。 微信团队 2019.11.22
2019-11-22 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 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 - 怎样识别字符串中的手机号,手机号高亮显示,并且可以点击这个手机号,拨打电话?
我们的需求是这样的》 客户留了一段备注信息,里面可能会有手机号(可能是多个号码),怎么才能识别出手机号,并且手机号要能高亮显示,当用户看到手机号的时候,直接点击手机号就可以拨打电话了,这样的功能App里面很好实现,但是小程序怎么实现呢。
2019-11-04 - 微信API服务器IP地址更新公告
各位开发者: 由于内部网络设备裁撤,api.weixin.qq.com(sz/hk.api.weixin.qq.com)需要更新部分接入点IP,详细如下: 深圳移动vip变更:183.232.103.145;120.198.199.239 -> 112.60.0.226;112.60.0.235 深圳电信vip变更:183.61.49.149 -> 183.3.234.152 深圳联通vip变更:58.251.61.149 -> 58.251.80.204 深圳cap vip变更:182.254.106.119 -> 121.51.166.37 香港vip变更:203.205.147.177;103.7.30.34 -> 203.205.239.94;203.205.239.82;103.7.30.16 此次变更不影响直接使用DNS解析微信域名的业务,如有业务仍在尝试旧的接入IP(通过代码写死或防火墙配置),请及时更新至新的接入IP,裁撤截止日期2019年10月17日,届时旧IP会停止服务,返回指引文档信息。 请不要通过配置hosts的方式访问api.weixin.qq.com,以免微信侧后续更新出口IP时出现接口访问失败。 特殊情况如配置防火墙等,请改造成通过接口获取api.weixin.qq.com当前所有出口IP(同一时间不会所有IP都变更,请尝试可用IP),并更新至hosts或防火墙等。 获取api.weixin.qq.com当前所有接入IP请参考官方文档。 微信团队 2019.09.24
2019-09-24 - 小程序模板消息能力调整通知
小程序模板消息能力在帮助小程序实现服务闭环的同时,也存在一些问题,如: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰; 2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求。 为提升小程序模板消息能力的使用体验,我们对模板消息的下发条件进行了调整,由用户自主订阅所需消息。 一次性订阅消息 一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 [图片] (一次性订阅示例) 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 调整计划 小程序订阅消息接口上线后,原先的模板消息接口将停止使用,详情如下: 1. 开发者可登录小程序管理后台开启订阅消息功能,接口开发可参考文档:《小程序订阅消息》 2. 开发者使用订阅消息能力时,需遵循运营规范,不可用奖励或其它形式强制用户订阅,不可下发与用户预期不符或违反国家法律法规的内容。具体可参考文档:《小程序订阅消息接口运营规范》 3. 原有的小程序模板消息接口将于 2020 年 1 月 10 日下线,届时将无法使用此接口发送模板消息,请各位开发者注意及时调整接口。 微信团队 2019.10.12
2019-10-13 - 小程序没流量?也许你忘了做这7件事
2019年,移动用户增速放缓、下沉市场释放增长红利、巨头APP流量优势凸显……商家寻找用户和流量出现,或许就在微信、抖音、百度和支付宝的小程序之中。 8月14日,《中国移动互联网2019半年大报告》显示,微信小程序500万以上月活跃用户的数量正在快速增长,由133个增至180个,超过100 万的达到883个;同时,小程序快速向中老年用户渗透,50岁以上用户增长了22%;此外,小程序的人均使用个数和支付人数分别有156%和109%的增 长,用户使用小程序的习惯正在不断加深。抢占用户流量,小程序已成为各行业商家必争的赛道。 商家使用小程序并不一定需要复杂的运营技巧,只要做好几项基础的引流工作,就能快速吸引线上、线下用户,提升销量。 1、符合搜索习惯的名称 微信搜索和“附近的小程序”是用户获取小程序的两个重要渠道,与来自扫码、分享、有朋友信任背书不同,通过搜索展示的小程序,越是符合用户搜索习惯、直白表达出商品和服务的特点的小程序名称就能吸引用户打开。特别是在“人无我有”阶段,微信搜索导流效果显著。 2、重视小程序页面标题 在新改版的微信中,即将上线“动态”功能,直接可以展示小程序商品、服务的页面。在这种像信息流一样的展示方式下,小程序页面标题越吸引人,就越能吸引用户打开自家的小程序。 3、实体店二维码海报引流 实体店的线下客流是小程序的重要流量池,既然没有额外人力、精力,商家也需要在门店、橱窗、收银台等显著位置放上带有小程序码的易拉宝、海报,并设置新用户优惠券、首单优惠等促销方式,引导用户进入小程序体验购物,从而在非营业时间也能产生订单。 小程序下载:https://www.sucaihuo.com/source/0-0-266-0-0-0 4、公众号菜单和图文 有公众号的商家应当做三件事情:1)小程序绑定公众号;2)在自定义菜单中设置小程序;3)图文消息中插入小程序,最好是以卡片的形式,更容易吸引点击。 5、宣传单页、卡片等物料 无论是线上电商还是线下实体店商家,都应当不放过每次接触用户的机会,来推广小程序。比如在宣传单页、配送车辆、实体会员卡、商品包装等上面印上小程序码,设置扫码优惠活动,吸引用户进入小程序。 6、小程序下拉菜单 下拉菜单是快速进入微信小程序的便捷方式,也是收藏常用小程序的重要入口,但并不是每个用户都熟悉。商家需要做的就是,通过发微信红包等奖励方式,引导用户将自家小程序放进“我的小程序”,这样可以有更高的概率让用户回到小程序。 7、建用户微信群 社交营销是小程序的最大优势,特别是对高频消费的快消品商家来说,创建用户微信群,并在用户群分享小程序活动,是最有效的活动营销方式之一。在微信群中,可以定时推出秒杀引流、拼团裂变、积分兑换福利等活动,培养用户小程序下单习惯。 据悉,微信官方正在基于多维度的小程序质量评估机制,给予优质小程序更多的展现入口。对商家而言,越早投入小程序运营、提供越优质的商品和服务,就越有可能获得更多的微信流量支持。小程序已成为企业最重要的获客渠道,目前来看,没有之一。
2019-09-01 - 微信登录能力介绍
为了便于用户便捷使用App、网站、移动端网页、小程序的服务,微信提供不同的技术方案,便于开发者在不同终端平台的服务中接入微信登录。 通过这个教程,开发者可以了解平台提供的针对各终端平台的微信登录能力,并可以根据实际使用场景合理选择接入方式。 以下为几类型微信登录的功能说明: 类型授权域/接口 用户侧使用流程 接入流程 App 接入微信SDK,并调用snsapi_userinfo (1)在App内选择使用微信登录 (2)拉起微信客户端,打开用户授权页,完成登录授权(1)注册微信开放平台(open.weixin.qq.com)帐号,并完成开发者资质认证 (2)申请【App移动应用】并审核通过后可以使用,查看开发文档网站应用 snsapi_login (1)用户使用微信“扫一扫”,在PC端扫码 (2)客户端打开授权页,完成登录授权 (1)注册微信开放平台(open.weixin.qq.com)帐号,并完成开发者资质认证 (2)申请【网站应用】并审核通过后可以使用,查看开发文档 微信客户端内H5使用公众号的登录能力: snsapi_base snsapi_userinfosnsapi_base:静默授权 snsapi_userinfo: (1)用户在H5内点击登录,唤起授权弹窗 (2)用户侧完成登录授权(1)注册微信公众号,选择“服务号”类型,并完成微信认证 (2)在公众号管理后台设置回调域名 (3)接入微信登录能力,查看开发文档 小程序wx.login wx.getUserInfowx.login:静默授权,开发者可获取openid wx.getUserInfo: (1)用户在小程序内点击组件,唤起登录窗口(2)用户侧完成登录授权 (1)注册小程序 (2)接入微信登录功能,查看开发文档,查看登录流程设计指引 开发者在不同使用场景下接入微信登录,应该注册符合要求的帐号并使用对应的登录能力。 【常见问题】 Q1: 在调用微信登录时,出现了“此帐号并没有这些scope的权限,错误码:10005”,是什么原因? A:对于场景与帐号属性、能力项不对应时(如在移动端网页中使用网站应用的AppID调用登录能力),将会出现以下的错误提示:此帐号并没有这些scope的权限,错误码:10005 [图片] Q2:我的服务同时有App、官网、公众号、小程序,那我怎么打通用户数据? A:对于多平台的服务,若开发者希望能识别用户身份,例如:希望用户在小程序内也能查看到在App内购买的商品订单,则可以通过平台提供的UnionID机制来实现用户身份识别。
2019-03-06 - 第三方服务商复用公众号资质生产的小程序如何注销?
第三方服务商复用公众号资质生产的小程序如何注销?这些小程序没有自己的后台,不用了又只能放在那里霸占名额~公司主体的小程序名额不够哇~
2019-08-13 - 一场比赛,一窥小程序的有限与无限
本文参加2019年「大赛文章征集」征文活动 大家好,我是华南赛区二等奖「Resser 阅见」小程序的开发团队DeveSA的队长。继上一篇高校微信小程序大赛经验分享杂谈流水账般介绍小程序的开发流程,这次我们来谈谈更Deep一点的——小程序的有限与无限。 「有限」和「无限」乍一看是不是很玄乎?让我稍稍剧透一下,这里的「有限」指的是小程序开发和运营上受到的限制,而「无限」则是小程序与生俱来的裂变能力和发展前景。本文的目的不只是简单地罗列这些Pros&Cons,也会根据在比赛中汲取到的经验分享给大家如何越过这些Cons并利用好Pros。 技术层面 wx.request 我相信每一个开发过小程序的developer,都记得大明湖畔的[代码]wx.request[代码]。曾几何时,比赛群里都是大家讨论如何在ddl前搞好备案,又或是ddl前一天匆匆忙忙设置好调试模式。 [代码]wx.request[代码]是微信提供的网络请求接口,限制诸多,有: 只支持HTTPS协议 不能使用IP地址或localhost 不支持8080以外端口 请求的域名必须经过ICP备案 用来鉴权的[代码]api.weixin.qq.com[代码]不能配置为服务器域名 Header中不能设置User-Agent 20个域名限制 而我们开发的「阅见」本质上是RSS阅读器,请求的feed链接都是用户自定义的外链,这些外链有HTTP的,也有没有备案的,数量更是无限的。这不就完美和[代码]wx.request[代码]的限制撞车了吗? 在这些限制下,很多同学第一反应是匆忙给自己的服务器上证书、上备案,却也不免遇上使用的第三方开放API(如[代码]http://api.github.com[代码])没上备案的问题。这就引出了更高阶的解决方法——在自己已备案的域名上设置[代码]tracker[代码]域名转发。然而,还有一种更优雅的方法——云函数。 也就是,原本从小程序到目标服务器的过程(上图)中间加入了云函数(下图)。 [图片] [图片] 借助于云函数与小程序得天独厚的血缘关系,小程序配置和调用云函数都十分方便。 云函数使用原生的[代码]request[代码]对传入的网址进行请求: [代码]const request = require('request'); exports.main = (event, context) => { return new Promise((resolve, reject) => { request(event.options, (error, res, body) => { if (error) return reject(error); resolve(res); }) }); } [代码] 小程序端则将原本的[代码]wx.request[代码]封装一下,代码有点长,只给个思路: [代码]wx.vrequest = function (options) { //将options转发到params,过程省略 return new Promise((RES, REJ) => { wx.cloud.callFunction({ //请求云函数 name: 'v-request', data: { options: params }, success: res => { const { result } = res; options.success && options.success(result); RES(result); }, fail: err => { //错误回调 REJ(err); }, complete: options.complete }) }) } [代码] 具体部署可以参考Github上guren-cloud/v-request WXML与富文本解析 解决了[代码]wx.request[代码]的问题,「阅见」遇到的第二个问题就是很多网站不支持RSS全文输出,因此我们计划让「阅见」能得到文章原文的HTML内容输出,再经过富文本渲染,呈现在小程序上。 小程序采用WXML,微信设计的一套标签语言,然而,网页上的富文本都是通过HTML呈现出来的,这样的话,当小程序想显示网页内容时,只能调用[代码]web-view[代码]了,而[代码]web-view[代码]不支持个人类型的小程序…… Butttt,多亏了小程序良好的生态环境,有很多开发者为小程序编写插件和库,其中,有关富文本渲染的就有wxParse和Towxml。其中wxParse已经两年没有更新了,虽然也有各路大神fork改良,但由于RSS获取到的文章千差万别,支持性不是很好(比如代码显示以及[代码]ruby[代码]标签等)。这里我强烈推荐Towxml,除了这个渲染库比较新之外,Towxml还有许多优良特性,比如,官方号称的“极致的中文排版优化”。 Thanks to Towxml,在「阅见」中,用户可以看到排版优良的订阅内容,甚至还能看视频哦。 [图片] 除了Towxml,「阅见」还借助了URL2Article提供的API提取正文内容,其能精准识别网页的正文部分,提取的内容不含广告导航等非正文内容。 [图片] 资源占用 说起小程序,你能想到最合适的形容词是什么?对我来说,就是「小」,这个「小」不只提现在小程序「触手可及、用完即走」的轻量,还有对开发者实在的限制——打包体积不能超过2M,本地缓存上限10M。 [图片] 这对于在大内存时代动辄上百兆代码的coder们来说,无疑是极大的限制。Butttt,在实际编写小程序时,只要有良好的编程习惯,这2M和10M是完全够用的,因为代码包经过了GZIP压缩后压缩率可以达到80%。 话虽如此,在「阅见」中,主要操作都是在小程序端完成的,所以相应的逻辑代码很多,在开发后期我们也的确遇到了代码包超过2M的情况。下面给一些减小代码包的体积的Tips: 小程序中只保存图标需要的图片文件,除tabbar外图标都使用[代码]·svg[代码]矢量格式,不仅体积相比[代码].png[代码]等传统点阵格式小了很多,而且也有矢量图永远高清的优点。 其他静态图片保存在服务器上或托管在图床上,七牛云就是个不错的选择,每个月还有免费的10G CDN流量,访问速度比云开发中的云储存快了不少。 删除测试使用的[代码]console.log()[代码]语句 压缩CSS代码 如果使用了第三方UI框架库,可以删除没有使用到的组件 如果以上Tips还不能解决代码包体积超过限制的问题,可以使用分包。 大部分小程序都会由某几个功能组成,通常这几个功能之间是独立的,但会依赖一些公共的逻辑,并且这些功能通常会对应某几个独立的页面。那么小程序代码的打包,大可不必一定要打成一个,可以按照功能的划分,拆分成几个分包,当需要用到某个功能时,才加载这个功能对应的分包。 使用分包方式加载小程序,能使小程序承载更多的功能与服务;而对用户而言,可以获得更快的加载速度,同时在不影响启动速度前提下使用更多功能。 目前小程序分包大小有以下限制: 整个小程序所有分包大小不超过 8M 单个分包/主包大小不能超过 2M 除了代码包2M的限制外,别忘了还有10M的缓存限制。在刚开发小程序时,我在所有跨页面传参使用的都是[代码]wx.setStorageSync[代码]和[代码]wx.getStorageSync[代码],不仅拖慢了程序的运行速度,还占用了宝贵了n/10M。其实,对于不需要存储在本地的跨页面传参,完全可以用[代码]app.globalData[代码]来代替: [代码]app.globalData[代码]是全局变量,下次进入的时候,就要重新获取。 [代码]Storage[代码]是本地缓存,除非缓存被清除不需要重新获取。 运营规范 登陆 相信大家对下图的warning都不陌生: [图片] 在之前,小程序可以直接通过[代码]wx.getUserInfo[代码]调起授权弹窗,这一操作虽然很便利,但却被一些开发者滥用,有些小程序一打开就要求授权甚至不授权就不能使用。在考虑到这一点的基础上,[代码]wx.getUserInfo[代码]不再显示授权弹窗,只能用button组件的开放功能调用。引用官方的话: 用户使用登录功能就像“面基”,第一印象很重要。这几个小改动在提升小程序使用的流畅体验、避免用户对数据采集授权担忧的同时,也将驱动用户更乐意尝试使用小程序服务。 因此,好的选择是在用户第一次登陆时“微妙地提示”用户授权登陆,并且在用户未授权时也可以访问到小程序的公共内容。 模版消息 我们知道,iOS上有专门的消息推送机制,而安卓上只能通过让应用后台运行实现24小时接受消息。借助于微信的装机率和使用率,小程序不需要时刻保持后台运行也能让用户接受到消息,这靠的就是微信的模版消息功能。Butttt,为了防止此功能的滥用,模版消息的发送有两个触发条件: 支付:当用户在小程序内完成过支付行为,可允许开发者向用户在7天内推送有限条数的模板消息(1次支付可下发3条,多次支付下发条数独立,互相不影响) 提交表单:当用户在小程序内发生过提交表单行为且该表单声明为要发模板消息的,开发者需要向用户提供服务时,可允许开发者向用户在7天内推送有限条数的模板消息(1次提交表单可下发1条,多次提交下发条数独立,相互不影响) 所以开发者难免会遇到模版消息发送次数不够用的窘况。但根据第二点,小程序每获得一次[代码]formId[代码],相当于就多了一条模版消息的「命」。简单来说,我们可以将小程序的表单组件进行封装,把用户的交互点击的[代码]bindtap[代码]事件替换为[代码]bindsubmit[代码],乔装一下小程序中其他功能按钮,当用户点击这些按钮时,就能获取到更多的[代码]formId[代码]。 ⚠️ 不过即便如此,开发者也应该遵守小程序运营规范,不要滥用模板消息。 更新:根据7月25日「微信广告小程序流量主大会」消息,微信小程序将上线「一次性订阅消息」的能力,只要经用户确认,超过七天的模版消息也能送达用户。 总结 小程序脱胎于微信,出生时便自带「降低门槛、充实能力、场景流量、提高转化、交易变现」等强有力的Buff,在电商零售、政务平台多方面可以说是全面赋能,有无限的发展潜能。小程序在方便用户,降低用户使用门槛的同时,也为开发者们提供了诸多上手即用的API和组件。如此强有力的工具想要长青,自然需要在开发、运营上施加限制,此之谓「有限」,而施加限制的目的是建立友好的微信生态体系,实现用户、微信、开发者三方互利共赢,此之谓「无限」。
2019-07-26 - 有赞功夫:一个To B行业产品经理的自我修养
产品经理已经成为互联网行业最吃香的职业之一,不过大多数人提到这个职业最先想到的还是微信、今日头条、淘宝、支付宝等C端产品,它们有海量用户,也创造了海量的产品经理岗位。而一个不容忽视的趋势是:以腾讯为代表的诸多互联网公司正在加重To B业务,未来To B行业的产品经理将扮演更重要的角色。 ToB行业产品经理工作与To C 行业产品经理有哪些区别?要面对哪些挑战?需要怎样的职业素养?以下是有赞美业产品总监功夫关于SaaS产品的心得分享。 有赞作为一个商家服务公司,主要面向零售商家提供SaaS产品,成立7年来已积累了有赞微商城、有赞零售、有赞美业、有赞餐饮、有赞小程序等SaaS软件产品,帮商家网上开店、网上营销、管理客户、获取订单。 功夫曾在房天下(搜房网)担任产品副总监,参与房产家居的O2O业务,后在阿里巴巴负责个性化推荐业务,有近6年的产品经历,无论在To C还是To B的产品,都有着丰富的经验。这次分享中涉及到有赞产品经理的工作理念、判断逻辑以及一些很具体的产品需求场景,对SaaS行业或者To B行业的产品经理来说都是很好的参考。 以下是功夫分享内容的整理:(采访与整理 :三节课产品学院行业研究员张成翼) 一. 为什么需要SaaS? SaaS的产生,从一开始就是为了追求效率,能够让“专业人做专业事”,进而提升企业经营效率,它的产生,本质上是技术在商家经营过程应用程度不断提升的一种体现。现有的SaaS形式,是在不断演化之后产生的结果。 接下来,我们以有赞美业的SaaS为例,来看看商家经营的解决方案演化过程—— 1.记账 这是比较早期的阶段,商家是通过一个厚厚的本子来管理店里面的经营情况,甚至到现在还有很多商家是用这种解决方案。 2. IT化 然后,一部分追求效率的商家开始使用单机不联网的ERP去管理店里面的经营情况,相比原来数据不易丢失,也能省不少时间。与此同时,商家初步具备了事“后”分析的能力,比如美业商家经营里有个很重要的场景——预约。 通常的场景大概是这样的:顾客打电话给前台预约某个时间做服务,前台打开系统录入后,告知技师客户姓名以及具体的服务时间和项目。店长在晚上复盘能看到今天每个时间段的客户预约量,接下来她可能会和老板商量要不要在明天采取一系列动作。 3. 互联网化 这是当前处在的阶段,美团用了7年的时间让商家开始接受了互联网的工具,知道怎么在线上找流量进而引导进店。商家开始使用微信公众号和小程序客服功能和他的顾客进行互动,从顾客的角度上来讲体验变得更好了,因为不再是冷冰冰的买卖关系。 这个阶段解决方案创造的价值是,让商家具备了事“中”反应的能力,还是以刚才的预约为例:顾客打开公众号或小程序预约时,能看到商家门店里的动态流量,当她提交信息完成预约后,前台的预约看板里会以可视化的形式展示出来,与此同时,技师也会收到实时提醒。店长随时随地打开App能知道哪个房间、哪个技师处于空闲状态,进而决定是否开启一波限时促销的活动。 4. 智能化 这是不久的将来一定会发生的,当然前提条件是有了足够多数据和信息的积累,比如说颗粒度足够丰富的“活”数据、平台间的数据可以实时共享等等。不过,到那个时候商家一定能具备事“前”分析的能力,比如,一个新顾客进店后,装在店里面的人脸识别系统会调取出这个顾客在其他店的消费记录,以及别的商家给她打上的标签,营销顾问再基于这些信息做针对性的营销。 在不同的阶段,其实提供的也是不同的产品,在最早的阶段,你只要提供一个账本就可以,而到第二个阶段,用友、金蝶等产品是主流;第三个阶段,也是当前,商家开始逐步接受使用互联网产品,这也是为什么SaaS开始在这个阶段兴起。 所以,SaaS的产生,其实是技术在商家经营过程应用程度不断提升的一种体现。而从中长期来看,SaaS针对其所服务的行业,是有可能提供领先于行业的解决方案,为整个行业赋能。 虽然我们还无法得知它未来终极的产品形态究竟如何,不过,从目前阿里、有赞等公司开始尝试的实践当中来看,基于数据智能和网络智能提供的服务将是重要的趋势,这也是曾鸣教授在《智能商业》一书中,为我们描绘的图景。 二. 对于产品经理来说,To C与To B的工作有什么区别?或者说,提出了怎样的挑战? 首先,从事SaaS的产品经理本质上仍旧是产品经理。换言之,对于大多数产品经理都适用的工作方法和评判标准,对于SaaS产品经理来说,也是适用的。 比如说,对于SaaS产品经理,或者借用现在一个时髦的概念,从事产业互联网的产品经理与消费互联网的产品经理相比,其工作基本思路与流程并无二致,都是基于场景进行设计。其核心思考的思路都是“用户,场景,需求”,大家都需要写PRD,用AXURE画原型,这一点是通用的。 但是,具体到SaaS的场景当中去,有许多SaaS产品中遇到的典型问题,在To C场景中是前所未有的,比如说: To C的产品经理,天然是用户,天然就能更好地理解用户的需求,To B的产品经理,大多数情况下不是用户,毕竟,对于大多数产品经理来说,很少会有自己开设一家企业,并经营3-5年的经历。这就意味着,To B的产品经理大多数情况下,并不真正了解用户,就更难提理解了; To C的产品,决策链通常是『点』状的,产品可能某个单点功能足够优秀,就能够获得用户,而To B的产品,决策链更多情况下是『环』状的,员工,老板之间可能各有诉求,二者的诉求有时甚至可能是冲突的,这时候,产品需要切实的解决用户问题,创造价值,用户才有可能真正的buy in; To C的产品,试错成本很低,很多时候,如果一个功能效果不好很容易就可以回滚,但是在To B的场景下,这是不允许的,一个功能一旦上线,商家在上面形成业务数据,就几乎不可能回滚了。这就意味着,SaaS产品经理在做产品设计时,前期需要扎扎实实的产品功能想清楚。 这几点差异的背后,以及很多时候我们提到的关于To B与To C的不同,究其根本,很重要的原因是在To B的场景下,产品经理们对于他们所服务的用户的相关信息输入是不够的。 对于To B,或者说SaaS来说,很多行业中通用的解决方案和思考方式,都是隐性知识,是需要你在行业中长期实践才能了解到,否则,很多只是在逻辑上或者理念上合理的解决方案,实践中就会遇到很多挑战。 也因此,对于To B产品的产品经理来说,不仅需要互联网产品的基本设计方法,还需要对其所服务行业有足够深刻的认知。他们不仅仅要关注用户体验,更多的时候,是要关注用户价值,关注在这个行业中究竟应该如何高效解决问题。 如果做不到这一点,简单的将过去在To C领域使用的方法直接套用到在To B领域,将会面临着很大的挑战。这也是刚刚转行To B的产品经理,时常会遇到的挑战。 对于有赞而言也是如此,过去在C端产品非常优秀的产品经理,在进入有赞之后,可能两三个月都没法很好的进入状态。对于他们来说,很多过去的工作方法和理念都需要抛弃,从更多的角度理解产品设计这件事,掌握更多关于SaaS产品的设计方法和理念,才能够更好的适应这个环境和过程。 也是在这个过程中,有赞内部也实践出一套方法论,帮助产品经理在面对商家,这个有些特殊的用户群体时,能够更快的进入状态,产出更高效的解决方案。 接下来,我们就借助有赞在发展过程中的一些实际案例,聊一聊有赞的产品设计方法。 三. to B产品如何进行基于业务场景的设计? 正如我们之前所说,基于场景的产品设计这一基本思路,无论是在C端还是在B端,都是适用的。 不过,有赞在实践过程中发现,在面向企业用户进行产品设计时,需要注意和回答的很多问题,是与C端不同的。 简单来看,是要依次回答这样三个问题 —— 1.业务场景究竟是什么? 如何定义业务场景?这是进行产品设计时第一个回答问题。 在定义场景时,不仅仅是简单的说一句用户在什么情况下使用这个功能,这样的描述还是太简单,对于业务场景的描述需要更有颗粒度,“我要把这个产品给你描述完,立马你就有画面感的,才叫场景“。 怎样才是有画面感的场景描述呐呢?关键在于,能够包含七个要素,分别是—— 用户、环境、动机、目标、介质,交互,动作。 只有包含这七个要素,才能够叫做有“画面感”的场景描述。你对业务场景的理解,能否到达这个颗粒度,会对你的产品设计结果带来很大差异。 在有赞内部,发生过很多次类似的事情,产品经理们因为对于业务场景理解的不同,会提出完全不同的解决方案。 比如说,现在让你给有赞设计一个店铺装修功能,你会如何做? 通用的解决方案可能是: 给商家做好基础功能,商家在后台可以自由的搭配界面,拼装颜色,调整图片大小尺寸。他能够把有赞店铺像装修淘宝店铺,京东店铺一样特别好看,这样,无论什么时候来,不同的店铺都能够有不同的主题。 这个方案确实是有效,他也确实是”店铺装修“,但它真的是最适合用户的解决方案吗? 要回答这个问题,仍旧应该是回到原点——清晰描述客户的业务场景。 这个过程,其实就是思考什么样的用户需要这个功能?什么时候需要这个功能?他想要这个功能解决什么问题,实现怎样的效果?通过怎样的方式解决这个问题?为了解决这个问题他需要使用怎样的介质?这个过程中要执行怎样的动作? 回到店铺装修这个问题上,对于有赞的商家来说,他们使用店铺装修功能的场景可能是,头部的20个品牌商家,在春节期间,为了决定突出品牌调性,获得更多的用户认可,同时为了增强跟顾客之间的这种互动,提高大家对他的好感度,所以他决定在春节期间上线一些新的活动。同时把它整个的页面设计的更符合春节的这种气息一些。于是它在后台里面选择了一套模板,并安排美工,设计一些符合这种气息的这种图片上传到后台里面,来达到这样的效果。 在这个回答当中,大致将刚刚提到的七个要素体现出来。这时,我们会发现,其实特别自由的店铺装修功能可能并不最合适,节庆期间的活动,如果店铺装修太过复杂,只会让商家感到反感。他们需要把大量的时间花在店铺装修上,针对于时间就是金钱的大促来说并不值当。而且,这样繁复的装修,在日常使用当中却又使用不到。 最终,这个功能的方案是,只设置针对不同节庆的基础模板,商家可以自己上传图片,符合节庆的氛围。但是,店铺的具体样式并不能调整。 当然,具体的功能设计,当然还有很多考量,我们这里只是简单的讨论,业务场景理解对于产品设计的价值。 2.产品价值是什么? 对产品的业务场景有了基本的理解之后,第二步可能需要理解一下,你的产品或者功能,对于用户的价值究竟是什么? 一般而言,对于To B类的产品,用户价值这件事无非两点 : 商家的经营效率是否提高? 商家的经营效果是否提高? 用户使用你的产品,工作效率是不是提高了、是不是能够赚到更多的钱,这两条是评判SaaS产品的核心标准。 如果在这两条中,一条都不占,那么,你这个需求可能就没法上了。 有赞内部之前经常有类似的需求,对于他们自己来说是有价值的,但对于商家而言,可能并不能提供价值,这时,也是考验产品经理的产品设计水平的时刻。 还是以有赞为例。 有赞运营曾向产品提出过这样一个需求,希望在商家注册或者使用付费功能时,能够填写自己的经营类目。 比如说,你是一家水果店,那么就在注册时,填写自己的类目为零售。你是一家按摩店,就填写自己的类目为服务。 之所以需要增加这一步,是为了有赞能够对于自己服务的商家有更全面的了解,之后能够提供更为优质的服务。对于商家来说,凭空在注册流程中增加这一步,短期之内他们的经营效率没有太大的帮助,而且,反而会增加操作成本。 但是,另一方面,这个需求也确实对于有赞有价值,所以也不可能直接把运营的需求拒绝掉。 这个时候,就需要产品的同学能够提出更有效的解决方案。 最后采取的方案是,让内部的数据同学,把同一商家ID下面的商品名称数据进行分析,进而得出商家的类目。 商家在使用有赞SaaS时,一定会录入商品名称,比如说水果店就会有苹果,香蕉,哈密瓜等商品名,便利店可能就会有各种饮料,零食等商品。这些商品的名称基本都是独有的。 数据同学可以在后台抽样选择20个商品,看看商品名称都落在哪个类目,进而推断商家所处的类目,这样,不仅能够得出不同商家的经营类目,而且从结果上看,反而比商家自己选择自己的经营类目更加精准一些,对于运营的同学更有价值。 而这样的案例,其实对于有赞来说时常发生。对于SaaS产品,很多功能或许是需要做的,但是,究竟是否可以做,则需要反复思考,这个功能是不是真正能够为用户提升价值,如果不能,我们倾向于不要做,这也是在SaaS产品中第二个关键的问题。 3.如何判断功能的优先级? 最后,当你开始有大量的需求之后,应该如何判断产品功能优先级? 针对这个问题的解决方案,无论是在C端还是在SaaS,判断准则都差异不大。 大致的判断标准基本都是公司战略重心、行业发展趋势、资源限制以及收入等要素来考虑。 关于如何进行宏观的考量,此处就不再过多赘述了。 四. 一个优秀SaaS产品经理的判断标准 对于产品经理而言,其实无论是在C端还是B端,能力要求都是近似的,大约都是这样一个框架—— 1. 首先是需求分析产生的能力。 我们对于业务场景的理解,对于产品价值的判断,这些前置性的思考,其实都是基于产品的理解和思考。这是一个基础能力,通过这种方法,形成产品需求; 2. 第二个就是产品设计的能力。 很多C端的产品经理,会将产品设计能力定义为UI设计能力,交互逻辑的设计能力以及一些流程或者细节的优化。坦率来说,我认为这个能力在SaaS解决方案里面是最不重要的。对于SaaS而言,最重要的是能够提供可用的解决方案,产业互联网的第一优先级就是可用。没有做到可用,但是足够好看,是无法解决本质问题的。所以,这里的产品设计能力,特指的是能够针对所服务的行业提出可行的解决方案的能力; 3. 第三个是项目跟进的能力, 就是你是否有能力主导整个项目,它能够如期按时交付,能够组织大家所有的参与者按照设定的目标推进并最终实现它,甚至是提前上线等等; 4. 最后一个就是对行业的敏感能力, 这是一个很重要的能力点,为什么说它重要?因为对于前三个能力来说,其实都有清晰的方法论,是可以训练,可以交付的,包括三节课正在开设的课程都是在解决这个问题。 但是,行业敏感度很难进行批量的训练,它有一个前提是你对这个行业要有足够的热爱,如果做不到这一点,后续都很困难。在热爱的基础上,可能需要你学习和训练一些信息获取的能力、分析调研的能力、以及全局的思考能力等等,通过对行业的掌握,最终获得这项能力。
2019-08-29 - 微信小程序码获取-从频繁失败到成功率100%
早期实现方案 1. 方案实现 通过微信的appSecret获取小程序accessToken并缓存 微信小程序上很多操作都需要使用accessToken,比如用户授权手机号,当然也包括获取小程序码 通过微信提供的api获取到对应的小程序码,由于http接口直接返回的是图片本身,所以考虑将图片上传七牛服务器并获取图片链接,最后使用图片的链接来展示或保存小程序码 2. 方案优点 由于上传了小程序码,对于一些跳转固定页面和参数的码可以将图片链接存到数据库,以供用户下次分享使用,无需重复获取 3. 存在的问题 稳定性很差,获取小程序码的失败率比较高,甚至会出现一个时间段内完全获取不到码的情况 接口效率不好,由于每次都会存在图片上传,而且上传本身又比较耗时,导致服务器压力巨大且频繁出现慢接口,可能会影响到项目中的其他服务 改造后方案 1. 方案实现 获取小程序码后不再上传七牛,直接通过图片流的方式返回给前端 2. 方案优点 取消了图片的上传操作,接口效率大幅提升,提高了小程序码的获取成功率,也减轻了服务的压力 3. 存在的问题 依旧存在小程序码获取失败的情况 4. 问题排查 经排查日志发现是accessToken失效导致,缓存的accessToken失效时间远比微信规定的失效时间短,那究竟又是什么情况会导致accessToken失效呢?经讨论和实验发现以下三点: 我们微信的appSecret授权给第三方网站使用(比如阿拉丁),他们也有获取小程序码的服务,运营可以通过阿拉丁获取小程序码,这就会导致阿拉丁使用我们的appSecret获取accessToken,以至于我们缓存中的accessToken失效。 后端缓存中的accessToken存入和获取的逻辑存在缺陷,每当从缓存读取accessToken时,若缓存不命中,则通过微信api获取新的accessToken然后再存入缓存,这个逻辑容易导致缓存穿透,即当多个请求都没有命中缓存时,只有一个线程能通过微信api拿到新的accessToken,其他线程都拿不到。 当一个accessToken存在时间比较长时,手动调用微信api获取小程序码,会看到微信的api也会存在概率获取不到码的情况,但是一个全新生成的accessToken则不会有这种情况,至少在10分钟之内非常稳定。 最终的方案 1. 方案实现 通知运营不要再使用阿拉丁的生成小程序码的功能,若有这方面需求可以找技术帮忙获取。 缓存中的accessToken有效时间缩短至5分钟,保证每次使用的accessToken都能稳定获取小程序码。 修改accessToken的获取机制,由定时器来获取accessToken并更新缓存,定时器每4分钟执行一次,以确保每个请求都能命中缓存,若定时器出现异常,则回退之前的逻辑(请求没有命中缓存,通过微信api重新获取accessToken)。 最终效果 这一个方案上线后,线上再也没有出现小程序码没有获取成功的情况,观察日志也没再出现获取失败的情况,目前已经两周保持100%成功率了。 [图片]
2019-06-04 - 【微信小程序】性能优化
为什么要做性能优化? 一切性能优化都是为了体验优化 1. 使用小程序时,是否会经常遇到如下问题? 打开是一直白屏 打开是loading态,转好几圈 我的页面点了怎么跳转这么慢? 我的列表怎么越滑越卡? 2. 我们优化的方向有哪些? 启动加载性能 渲染性能 3. 启动加载性能 1. 首次加载 你是否见过小程序首次加载时是这样的图? [图片] 这张图中的三种状态对应的都是什么呢? 小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:[代码]下载小程序代码包[代码]、[代码]加载小程序代码包[代码]、[代码]初始化小程序首页[代码]。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2. 加载顺序 小程序加载的顺序是如何? 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过2,我们知道了,问题1中第一张图是[代码]资源准备[代码](代码包下载);第二张图是[代码]业务代码的注入以及落地页首次渲染[代码];第三张图是[代码]落地页数据请求时的loading态[代码](部分小程序存在) 3. 控制包大小 提升体验最直接的方法是控制小程序包的大小,这是最显而易见的 勾选开发者工具中“上传代码时,压缩代码”选项; 及时清理无用的代码和资源文件(包括无用的日志代码) 减少资源包中的图片等资源的数量和大小(理论上除了小icon,其他图片资源从网络下载),图片资源压缩率有限 从开发者的角度看,控制代码包大小有助于减少小程序的启动时间。对低于1MB的代码包,其下载时间可以控制在929ms(iOS)、1500ms(Android)内。 4. 采用分包加载机制 根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; [图片] 使用分包时需要注意代码和资源文件目录的划分。启动时需要访问的页面及其依赖的资源文件应放在主包中。 5 采用分包预加载技术 在4的基础上,当用户点击到子包的目录时,还是有一个代码包下载的过程,这会感觉到明显的卡顿,所以子包也不建议拆的太大,当然我们可以采用子包预加载技术,并不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转; [图片] 这种基于配置的子包预加载技术,是可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;是灵活可控的 6. 采用独立分包技术 目前很多小程序[代码]主包+子包[代码](2M+6M)的方式,但是在做很多运营活动时,我们会发现活动(红包)是在子包里,但是运营、产品投放的落地页链接是子包链接,这是的用户在直达落地时,必须先下载主包内容(一般比较大),在下载子包内容(相对主包,较小),这使得在用户停留时间比较短的小程序场景中,用户体验不是很好,而且浪费了很大部分流量; [图片] 可以采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源; 7. 首屏加载的优化建议 7.1 提前请求 异步请求可以在页面onLoad就加载,不需要等页面ready后在异步请求数据;当然,如果能在前置页面点击跳转时预请求当前页的核心异步请求,效果会更好; 7.2 利用缓存 利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新,这不仅优化了性能,在无网环境下,用户也能很顺畅的使用到关键服务; 7.3 避免白屏 可以在前置页面将一些有用的字段带到当前页,进行首次渲染(列表页的某些数据–> 详情页),没有数据的模块可以进行骨架屏的占位,使用户不会等待的很焦虑,甚至走了; 7.4 及时反馈 及时的对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了,无响应 渲染性能优化 1. 小程序渲染原理 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 分析这个流程不难得知:页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 2. 避免使用不当setData 在数据传输时,逻辑层会执行一次[代码]JSON.stringify[代码]来去除掉[代码]setData[代码]数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将[代码]setData[代码]所设置的数据字段与[代码]data[代码]合并,使开发者可以用[代码]this.data[代码]读取到变更后的数据。因此,为了提升数据更新的性能,开发者在执行[代码]setData[代码]调用时,最好遵循以下原则: 2.1 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; [图片] 2.2 数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用[代码]setData[代码]来设置这些数据; [图片] 2.3 与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下 [图片] 提升数据更新性能方式的代码示例 [代码]Page({ onShow: function() { // 不要频繁调用setData this.setData({ a: 1 }) this.setData({ b: 2 }) // 绝大多数时候可优化为 this.setData({ a: 1, b: 2 }) // 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外 this.setData({ myData: { a: '这个字符串在WXML中用到了', b: '这个字符串未在WXML中用到,而且它很长…………………………' } }) // 可以优化为 this.setData({ 'myData.a': '这个字符串在WXML中用到了' }) this._myData = { b: '这个字符串未在WXML中用到,而且它很长…………………………' } } }) [代码] 利用setData进行列表局部刷新 在一个列表中,有[代码]n[代码]条数据,采用上拉加载更多的方式,假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果 解决方法 1、可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来) 2、说到重点了,就是利用[代码]setData[代码]局部刷新 [代码]> a.将点赞的`id`传过去,知道点的是那一条数据, 将点赞的`id`传过去,知道点的是那一条数据 [代码] [代码]<view wx:if="{{!item.status}}" class="btn" data-id="{{index}}" bindtap="couponTap">立即领取</view> [代码] [代码]> b.重新获取数据,查找相对应id的那条数据的下标(`index`是不会改变的) > c.用setData进行局部刷新 [代码] [代码]this.setData({ list[index] = newList[index] }) [代码] 其实这个小操作对刚刚接触到微信小程序的人来说应该是不容易发现的,不理解setData还有这样的写法。 2.4 切勿在后台页面进行setData 在一些页面会进行一些操作,而到页面跳转后,代码逻辑还在执行,此时多个[代码]webview[代码]是共享一个js进程;后台的[代码]setData[代码]操作会抢占前台页面的渲染资源; [图片] [图片] 3. 用户事件使用不当 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的[代码]bind[代码]和[代码]catch[代码]),从而减少通信的数据量和次数; 2.事件绑定时需要传输[代码]target[代码]和[代码]currentTarget[代码]的[代码]dataset[代码],因而不要在节点的[代码]data[代码]前缀属性中放置过大的数据。 [图片] 4. 视图层渲染原理 4.1首次渲染 初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的WXML片段上生成节点树。节点树也就是在开发者工具WXML面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。 [图片] 在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而减少WXML中节点的数量可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。 简化WXML代码的例子 [代码]<view data-my-data="{{myData}}"> <!-- 这个 view 和下一行的 view 可以合并 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> <text> <!-- 这个 text 通常是没必要的 --> {{myText}} </text> </view> </view> <!-- 可以简化为 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> {{myText}} </view> [代码] 4.2 重渲染 初始渲染完毕后,视图层可以多次应用[代码]setData[代码]的数据。每次应用[代码]setData[代码]数据时,都会执行重渲染来更新界面。初始渲染中得到的data和当前节点树会保留下来用于重渲染。每次重渲染时,将[代码]data[代码]和[代码]setData[代码]数据套用在WXML片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,将[代码]setData[代码]数据合并到[代码]data[代码]中,并用新节点树替换旧节点树,用于下一次重渲染。 [图片] 在进行当前节点树与新节点树的比较时,会着重比较[代码]setData[代码]数据影响到的节点属性。因而,去掉不必要设置的数据、减少[代码]setData[代码]的数据量也有助于提升这一个步骤的性能。 5. 使用自定义组件 自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响;比如一些运营活动的定时模块可以单独抽出来,做成一个定时组件,定时组件的更新并不会影响页面上其他元素的更新;各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。 [图片] 6. 避免不当的使用onPageScroll 每一次事件监听都是一次视图到逻辑的通信过程,所以只在必要的时候监听pageSrcoll [图片] 总结 小程序启动加载性能 控制代码包的大小 分包加载 首屏体验(预请求,利用缓存,避免白屏,及时反馈 小程序渲染性能 避免不当的使用setData 合理利用事件通信 避免不当的使用onPageScroll 优化视图节点 使用自定义组件
2019-03-07 - 该appid认证主体与特约商户主体不一致
该appid认证主体与特约商户主体不一致,怎么能一致
2019-08-26 - 小程序审核:名称规则调整及优化建议
微信小程序的开发者们在发布小程序之前,需要给每个小程序起好一个名称,设置好logo、简介及类目信息,然后才能进行代码提审。 这些账号的基础信息是用户对一个小程序的初步认知,平台希望这些信息能够给予用户准确预期每个小程序的功能和内容,也希望每个小程序的名称是具有独特性的,减少误导、混淆用户理解的可能性。 因此有以下几点名称规则调整及优化建议,希望开发者可以了解并遵循,后续可能会在名称审核、代码审核或已发布运营时,收到平台的审核通知或建议修改通知。 l 微信小程序的名称建议是品牌、商标或具有辨识度的短词 在微信小程序发布后,用户会通过搜索、传播等场景使用小程序的功能和服务。选择品牌、商标或具有辨识度的短词作为小程序的名称,可以帮助用户快速建立与小程序的记忆与关系。 名称为“印象笔记”会比“笔记”能更好的让用户记忆和理解,并使用“印象”品牌词进行搜索或传播。 [图片] l 不能直接使用功能性描述、广义归纳类的词汇作为名称 功能性描述的词汇虽然可传达出小程序所要提供的相关功能,但直接使用此类词汇,缺乏显著性且不具有识别、区分小程序的作用。 如名称为“牙科”的账号,用户能直接理解小程序的内容可能与“牙科”相关,但对这个小程序账号是没有建立认知和关系的。 [图片] l 不能同时使用地域性词汇与广义归纳类词汇进行命名 使用“地域+广义归纳类”词汇进行命名,同样存在指向性过于宽泛的问题。 如以下案例, 名称包含的地域词:“四川”、归纳类词:“系统门窗”,并不会让用户对账号的功能产生有效的预期。以下示例是不允许的: [图片] 但在政务民生类服务场景,“广州地铁”、“广州公积金”可以被用户理解并接受属于政府或相关机构提供的服务和资讯,因此“地域+广义归纳类”词汇可被政府民生场景使用。 l 不建议使用具有营销属性的词汇进行命名 营销属性词汇如:免费、促销、清仓、打折等,鉴于活动规则普遍附有限制条件,在名称中使用,通常带有过于夸大的表达效果,并非是小程序的核心功能和用户的全部认知,存在误导的可能性。以下示例是不允许的: [图片] l 商品销售类小程序,需要包含品牌名称,不直接使用产品分类进行命名 商品销售类小程序,若仅使用产品分类进行命名,无法建立易于记忆、易于传播的品牌形象。如名称为“女鞋男鞋女包批发生产”,用户并不会因“女鞋”“男鞋”“女包”就会进行选择,反而“品牌+女装”才是用户真正需要的。以下示例是不允许的。 [图片] l 不在名称中堆砌热门搜索关键词 名称中堆砌多个关键词,过于冗长的热门搜索关键词堆砌名称无法建立品牌记忆,目的普遍是在搜索结果上获得更多的曝光。以下示例是不允许的: [图片] 结语 此次名称规则的调整和优化建议,平台希望开发者们能相应地调整自己的小程序名称。如未满足名称规范,平台会在后续的小程序名称设置、名称审核、代码审核流程进行规则提示和修改要求。并会在搜索场景优化需求基础上,对无辨识度的账号名称进行搜索策略调整。 微信小程序完整的名称规则可具体参照《微信小程序平台运营规范》2. 基本信息规范,除以上规则与示例之外,当然还包括了不得使用违法违规的名称、不得使用侵权他人的品牌词进行命名、在命中敏感类目如医疗词时提交主体资质等规则。 平台希望小程序名称的优化调整,可以使得每个被开发者精心创造出来的小程序都能让用户在使用时、搜索时、传播时逐渐接受并建立对产品品牌的认知。
2023-08-17 - 小程序审核:名称规则调整及优化建议
微信小程序的开发者们在发布小程序之前,需要给每个小程序起好一个名称,设置好logo、简介及类目信息,然后才能进行代码提审。 这些帐号的基础信息是用户对一个小程序的初步认知,平台希望这些信息能够给予用户准确预期每个小程序的功能和内容,也希望每个小程序的名称是具有独特性的,减少误导、混淆用户理解的可能性。 因此有以下几点名称规则调整及优化建议,希望开发者可以了解并遵循,后续可能会在名称审核、代码审核或已发布运营时,收到平台的审核通知或建议修改通知。 l 微信小程序的名称建议是品牌、商标或具有辨识度的短词 在微信小程序发布后,用户会通过搜索、传播等场景使用小程序的功能和服务。选择品牌、商标或具有辨识度的短词作为小程序的名称,可以帮助用户快速建立与小程序的记忆与关系。 名称为“印象笔记”会比“笔记”能更好的让用户记忆和理解,并使用“印象”品牌词进行搜索或传播。 [图片] l 不能直接使用功能性描述、广义归纳类的词汇作为名称 功能性描述的词汇虽然可传达出小程序所要提供的相关功能,但直接使用此类词汇,缺乏显著性且不具有识别、区分小程序的作用。 如名称为“牙科”的帐号,用户能直接理解小程序的内容可能与“牙科”相关,但对这个小程序帐号是没有建立认知和关系的。 [图片] l 不能同时使用地域性词汇与广义归纳类词汇进行命名 使用“地域+广义归纳类”词汇进行命名,同样存在指向性过于宽泛的问题。 如以下案例, 名称包含的地域词:“四川”、归纳类词:“系统门窗”,并不会让用户对帐号的功能产生有效的预期。以下示例是不允许的: [图片] 但在政务民生类服务场景,“广州地铁”、“广州公积金”可以被用户理解并接受属于政府或相关机构提供的服务和资讯,因此“地域+广义归纳类”词汇可被政府民生场景使用。 l 不建议使用具有营销属性的词汇进行命名 营销属性词汇如:免费、促销、清仓、打折等,鉴于活动规则普遍附有限制条件,在名称中使用,通常带有过于夸大的表达效果,并非是小程序的核心功能和用户的全部认知,存在误导的可能性。以下示例是不允许的: [图片] l 商品销售类小程序,需要包含品牌名称,不直接使用产品分类进行命名 商品销售类小程序,若仅使用产品分类进行命名,无法建立易于记忆、易于传播的品牌形象。如名称为“女鞋男鞋女包批发生产”,用户并不会因“女鞋”“男鞋”“女包”就会进行选择,反而“品牌+女装”才是用户真正需要的。以下示例是不允许的。 [图片] l 不在名称中堆砌热门搜索关键词 名称中堆砌多个关键词,过于冗长的热门搜索关键词堆砌名称无法建立品牌记忆,目的普遍是在搜索结果上获得更多的曝光。以下示例是不允许的: [图片] 结语 此次名称规则的调整和优化建议,平台希望开发者们能相应地调整自己的小程序名称。如未满足名称规范,平台会在后续的小程序名称设置、名称审核、代码审核流程进行规则提示和修改要求。并会在搜索场景优化需求基础上,对无辨识度的帐号名称进行搜索策略调整。 微信小程序完整的名称规则可具体参照《微信小程序平台运营规范》2. 基本信息规范,除以上规则与示例之外,当然还包括了不得使用违法违规的名称、不得使用侵权他人的品牌词进行命名、在命中敏感类目如医疗词时提交主体资质等规则。 平台希望小程序名称的优化调整,可以使得每个被开发者精心创造出来的小程序都能让用户在使用时、搜索时、传播时逐渐接受并建立对产品品牌的认知。
2019-05-14 - 使用微信sdk时,updateAppMessageShareData报错
用vue脚手架开发微信分享功能,我在main.js封装了一个方法shareList [图片] 然后在组件的生命周期函数create()执行,这个shareList方法 [图片] 但在开发者工具中,却提示 updateAppMessageShareData is not defined [图片]
2019-08-20 - 社区突出贡献者激励计划
各位微信开放社区的用户: 大家下午好。 社区从上线到现在,我们看到了很多积极、热心的开发同学、运营同学在社区帮我们回答问题、分享一些经验,成为了“社区网红”,我们希望能看见更多的网红出现。因此,为了共同营造社区良好的问答、分享氛围,自2018.11.01起,我们会开始以月为时间单位,记录各位用户(个人/企业)在社区的贡献,并评选出社区每月突出贡献者。 激励措施 当选社区突出贡献者的用户,将有机会获得以下奖励: (1) 小程序极速审核通道,开发者参与开发的小程序将可享受一个月快速审核的奖励。 (2) 官方沟通群,我们希望及时听见你的反馈和建议 (3) 微信周边礼品,如 WeStore 礼品、微信公开课Pro门票 评选准则时间:每个自然月的贡献统计,下个月将清零重新计算。维度: 1.加分项,例如: 1)在社区的回答、经验分享、案例分享获得的点赞 2)高质量的回答被置顶 3)在社区内发表文章,并被评为“精选” 2.减分项,例如: 1)广告、重复问题刷屏、与社区无关帖子(如诗词等)将会被删除,会扣除相应的分数 如果帖子被删帖,该个人/企业用户将无法参与本月突出贡献者评选,请大家在社区不要提供无效的内容。 2)不被认可的回答,将会扣除相应的分数 除每月评选社区突出贡献者之外,我们将组织更多的活动,提供更多给大家展示的舞台。 我们希望看见各位个人/企业用户在社区的智慧和力量,和我们一同打造充满热情的微信开放社区! 关于社区的任何想法,欢迎大家留言给我们。 微信团队 2018.10.24
2019-04-14 - 海外主体小程序是否可以实现跨境支付?
服饰类的海外主体小程序,选择商家自营售卖产品, 是否可以调用微信支付实现跨境支付? 谢谢~~ 补充下问题,从微信支付接口开发文档里面看来,境外商户的支付产品是不支持小程序的↓ 除了使用第三方支付之外,是否还有什么办法在海外主体小程序里调起微信支付呢? [图片]
2019-08-16 - 小程序内用户帐号登录规范调整和优化建议
为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范。本公告所称“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。具体规范要求如下: 1.服务范围开放的小程序 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 包括但不限于打开小程序后立即跳转提示登录或打开小程序后立即强制弹窗要求登录,都属于违反上述要求的情况; 以下反面示例,在用户打开小程序后立刻弹出授权登录页; [图片] 建议修改为如下正面示例形式:在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项。 [图片] 2.服务范围特定的小程序 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序; 下图案例为正面示例:校友管理系统,符合规范要求。 [图片] 3.仅提供注册功能小程序 对于线上仅提供注册功能,其他服务均需以其他方式提供的小程序,可在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或帐号登录。如ETC注册申请、信用卡申请; 如下反面示例,用户在进入时未获取任何信息,首页直接强制弹框要求登录注册ETC,这是不符合规范的。 [图片] 建议修改为如下正面示例所示形式:允许在首页说明注册功能后,提供登录或注册按钮供用户主动选择点击登录。 [图片] 4.提供可取消或拒绝登录选项 任何小程序调用帐号登录功能,应当为用户清晰提供可取消或拒绝的选项按钮,不得以任何方式强制用户进行帐号登录。 如下图所示反面示例,到需要登录环节直接跳转登录页面,用户只能选择点击登录或退出小程序,这不符合登录规范要求。 [图片] 建议修改为下图正面示例形式,在需帐号登录的环节,为用户主动点击登录,并提供可取消按钮,不强制登录。 [图片] 针对以上登录规范要求,平台希望开发者们能相应地调整小程序的帐号登录功能。如未满足登录规范要求,从2019年9月1日开始,平台将会在后续的代码审核环节进行规则提示和修改要求反馈。
2019-07-20 - 小程序webview组件,小程序和webview交互,小程序内联h5页面,小程序webview内网页实现微信支付
小程序支持webview以后,我们开发的好多h5页面,就可以直接在小程序里使用了,比如我们开发的微信商城,文章详情页,商品详情页,就可以开发一套,多处使用了。我们今天来讲一讲。在小程序的webview里实现微信支付功能。因为微信不允许在小程序的webview里直接调起微信支付。所以我们这节课就要涉及到小程序和webview的交互了。 老规矩先看效果。 因为这里涉及的东西比较多,录gif太多,没法上传,我就录制了一段视频出来。 https://v.qq.com/x/page/t0913iprnay.html 原理 先说下实现原理吧,实现原理就是我们在webview的h5页面里实现下单功能,然后点击支付按钮,我们点击支付按钮的时候会跳转到小程序页面,把订单号,订单总金额,传递到小程序里,然后小程序里使用订单号和订单金额去调起微信支付,实现付款,付款成功或者失败时都会有回调。我们再把对应的回调传递给webview,刷新webview里的订单和支付状态。 一,定义webview显示h5页面 关于webview的使用,我就不做讲解了,官方文档里写的很清楚,用起来也很简单。https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html [图片] webview很简单,就是用一个webview组件,显示我们的网页。 二,定义h5页面 我这里启动一个本地服务器,用来展示一个简单的h5页面。 [图片] 上图是我在浏览器里显示的效果。 接下来我们在小程序的webview里显示这个页面,也很简单,只需要把我们的src定义为我们的本地网页链接就可以了。 [图片] 这里有一点需要注意 因为我们是本地链接,我们需要到开发者工具里把这一项给勾选。 [图片] 三,来看下h5页面代码 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>小程序内嵌webview</title> <style> .btn { font-size: 70px; color: red; } </style> </head> <body> <h1>我是webview里的h5页面</h1> <a id="desc" class="btn" onclick="jumpPay()">点击支付</a> <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> <script> console.log(location.href); let payOk = getQueryVariable("payOk"); console.log("payOk", payOk) if(payOk){//支付成功 document.getElementById('desc').innerText="支持成功" document.getElementById('desc').style.color="green" }else{ document.getElementById('desc').innerText="点击支付" } //获取url里携带的参数 function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } return (false); } function jumpPay() { let orderId = Date.now();//这里用当前时间戳做订单号(后面使用你自己真实的订单号) let money = 1;//订单总金额(单位分) let payData = {orderId: orderId, money: money}; let payDataStr = JSON.stringify(payData);//因为要吧参数传递给小程序,所以这里需要转为字符串 const url = `../wePay/wePay?payDataStr=${payDataStr}`; wx.miniProgram.navigateTo({ url: url }); // console.log("点击了去支付", url) console.log("点击了去支付") } </script> </body> </html> [代码] h5代码这里不做具体讲解,只简单说下。我们就是在点击支付按钮时,用当前时间戳做为订单号(因为订单号要保证唯一),然后传一个订单金额(单位分),这里节约起见,就传1分钱吧,花的是自己的钱,心疼。。。。 关键点说一下 1, 必须引入jweixin,才可以实现h5跳转小程序。 <script type=“text/javascript” src=“https://res.wx.qq.com/open/js/jweixin-1.3.2.js”></script> 2,跳转到小程序页面的方法 [代码]const url = `../wePay/wePay?payDataStr=${payDataStr}`; wx.miniProgram.navigateTo({ url: url }); [代码] 这里要和你小程序的页面保持一致。payDataStr是我们携带的参数 [图片] 四,小程序支付页 来看下我们的小程序支付页 [图片] 小程序支付页功能很简单,就是来接收我们h5传过订单号和订单金额。然后去调起微信支付,实现支付。支付成功和支付失败都有对应的回调。 [图片] 支付我们这里实用的小程序云开发来实现的支付,核心代码只有10行。由于支付不是本节的重点,所以这里不做具体讲解。感兴趣的同学可以去看我写的文章和我录的视频 小程序支付文章:https://www.jianshu.com/p/2b391df055a9 小程序支付视频:https://edu.csdn.net/course/play/25701/310742 下面把小程序接收参数和支付的完整代码贴出来给大家 [代码]Page({ //h5传过来的参数 onLoad: function(options) { console.log("webview传过来的参数", options) //字符串转对象 let payData = JSON.parse(options.payDataStr) console.log("orderId", payData.orderId) let that = this; wx.cloud.callFunction({ name: "pay", data: { orderId: payData.orderId, money: payData.money }, success(res) { console.log("获取成功", res) that.goPay(res.result); }, fail(err) { console.log("获取失败", err) } }) }, //微信支付 goPay(payData) { wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success(res) { console.log("支付成功", res) //你可以在这里支付成功以后,再跳会webview页,并把支付成功状态传回去 wx.navigateTo({ url: '../webview/webview?payOk=true', }) }, fail(res) { console.log("支付失败", res) } }) } }) [代码] 代码里注释很清楚,这里有一点,就是我们支付成功后,需要告诉h5我们支付成功了,通知h5去刷新订单或者支付状态。 到这里我们就完整的实现了小程序webview展示h5页面,并且做到了h5和小程序的交互,实现了小程序webview的支付功能。 是不是很简单呢。 源码地址 1,关注“编程小石头”公号,回复“webview”即可获取源码 2,也可以到我github下载源码 https://github.com/qiushi123/xiaochengxu_demos [图片]
2019-08-15 - 不用买域名、不用备案、不用配置https快速部署Node.js微信小程序商城
生产环境推荐使用阿里云服务器,阿里云限时优惠券领取 最近更新时间:2019-03-09 原文链接:https://nideshop.com/ 本文档为开源微信小程序商城 NideShop 项目的安装部署教程,欢迎 star NideShop 商城 api 服务端:https://github.com/tumobi/nideshop NideShop 微信小程序客户端: https://github.com/tumobi/nideshop-mini-program NideShop 后台管理客户端: https://github.com/tumobi/nideshop-admin 一、购买新浪云SAE 为什么选择SAE?免费二级域名和支持https访问,不用备案,可用做微信小程序服务器。 SAE推荐链接:http://sae.sina.com.cn/ 选择对应的部署环境 开发言语:容器虚拟机 操作系统:centos 操作系统版本:7.6.1810 单实例配置:高级 II 型 (不建议选择基础型,因为配置低可能会导致安装失败 ) 二级域名:testnideshop(不可以重复,最终域名为:testnideshop.applinzi.com) 应用名称:测试NideShop (这里可随便填写) 文中出现 testnideshop.applinzi.com 的地方,请替换为你配置的二级域名 [图片] 二、使用 Web 终端登录云容器 找到新创建的容器,直接点击下图位置中的 终端 使用 [代码]Web终端[代码] 登录。如果需要打开多个 [代码]Web终端[代码] ,则多次点击 终端。 [图片] 三、安装基本软件 [代码]yum update -y yum install -y epel-release yum install -y curl vim net-tools git [代码] 四、通过 nvm 安装 Node.js 安装 nvm [代码]curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash [代码] 创建文件 .bashrc 并追加内容 [代码]cat>>.bashrc<<EOF export NVM_DIR="/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" EOF [代码] 使用 .bashrc 生效 [代码]source /.bashrc [代码] 安装 Node.js [代码]nvm install v10 node -v [代码] 五、开启共享型MySQL并导入数据 下载项目 nideshop.sql 文件 访问源码地址:https://github.com/tumobi/nideshop,根据下图下载源码,解压。 [图片] 开启共享型 MySQL 数据库 直接点击 服务开关 开启共享型 MySQL [图片] 导入数据库 创建导入任务,并选择源码目录下的 nideshop.sql 文件 [图片] 查看导入结果 导入任务需要一定的执行时间,直到状态为 执行成功 [图片] 六、部署 NideShop 小程序商城 安装 ThinkJS [代码]npm install -g think-cli thinkjs --version [代码] 下载 NideShop 的源码 [代码]mkdir /var/www cd /var/www git clone https://github.com/tumobi/nideshop [代码] 安装依赖 [代码]cd /var/www/nideshop npm install [代码] 替换数据库配置 直接复制以下内容执行 [代码]cat>src/common/config/database.js<<EOF const mysql = require('think-model-mysql'); module.exports = { handle: mysql, database: 'app_' + process.env.APPNAME, prefix: 'nideshop_', encoding: 'utf8mb4', host: process.env.MYSQL_HOST, port: process.env.MYSQL_PORT, user: process.env.ACCESSKEY, password: process.env.SECRETKEY, dateStrings: true }; EOF [代码] 修改 NideShop 运行的主机和端口 更改默认监听的主机 [代码]sed -i "N;2ahost:'0.0.0.0'," src/common/config/config.js [代码] 更改运行的端口 [代码]sed -i "N;2aport:5050," src/common/config/config.js [代码] 编译项目 [代码]npm run compile [代码] 通过 pm2 进行线上部署 安装 pm2 [代码]npm install -g pm2 [代码] 启动 pm2 [代码]pm2 startOrReload pm2.json [代码] 浏览器访问测试 https://testnideshop.applinzi.com/ 注意:需要把 testnideshop 替换为你创建时填写的二级域名 七 修改NideShop微信小程序的配置 微信小程序商城GitHub: https://github.com/tumobi/nideshop-mini-program 打开文件config/api.js,修改ApiRootUrl为自己的域名,注意 https 和后面的 api/ 不能少 [代码]const ApiRootUrl = 'https://testnideshop.applinzi.com/api/'; [代码] 八 微信小程序端运行效果图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] 如使用的是阿里云服务器,请参考另一篇文章:CentOS 7.3 下部署基于 Node.js的微信小程序商城
2019-03-14 - 个体户申请微信小程序,交易时recAccount过长?
[图片]最后一步了,打款过去[图片],说这个账户太长了,这怎么汇款给腾讯啊;25位的账户,后面10位加上去是不是就超过了最大长度啊
2019-08-13 - 截图组件(修正版,上篇删了,有bug)
刚才发的截图时发生错位,现在重新发 https://developers.weixin.qq.com/s/hzVM3BmU7Paz 以后有时间会出一整套原生的框架,开源 开源 开源,希望大神指点一二
2019-08-14 - 纵向 slider
因为官方的 slider 只有横向的,所以按照官方的slider写了个纵向的,用 wxs 处理的事件(出了这么久了,却没有怎么用过。。)。代码片段如下 https://developers.weixin.qq.com/s/qYLgcDmC7Air 没什么难点,本来想着几行就写完了。。意料之外的多了些代码。。主要有几点想吐槽的 wxs 里的getState是用来存公共临时变量的。为什么不直接在 wxs里定义一个变量呢?因为如果有多个组件的话,这些变量会共享。另外还要注意下面的情况 [代码] var state = getState() state.value = 1 // 正确 state = {value: 1} // 错误 [代码] 点击事件是加到线条上的,线条本身太窄了,加了padding扩大点。另外,头尾两个位置应该是用户最常用的点击,但是却不怎么好点到,甚至根本点不到,所以在两头各加了2个view来直接设置最大最小值。 想点着圆点滑动的时候,偶尔会点到滚动条上,结果造成页面滑动,所以在最外面加了 touchmove 绑定了空方法。 为什么这个组件需要有 show-value 的配置。。感觉特别鸡肋。。 早先发到群里的那一版不太好,没优化,而且value的设置忘了减去 min了。。哈哈哈。。抱歉。。
2020-07-06 - Mock服务插件在接口测试中的设计与应用
引言 在日常的接口测试中,测试人员常常会遇到以下几种令人头疼的情况: 场景一:依赖的接口状态不稳定,导致集成 CI 常常失败,需要耗费大量时间排查非被测目标本身之外的环境问题 场景二:做异常测试时构造异常数据成本高、难度大,某些异常数据甚至无法通过正常途径构造 场景三:被测目标开发进度先于依赖模块,当测试需要先行介入接口测试,但依赖模块接口尚且不通 面对以上痛点,我们需要做什么? 一、概述 1.1 Mock 定义 Mock 是测试过程中中常见的一种技术,即对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,从而把测试与测试边界以外的对象隔离开。 引用《淘宝网-接口测试白皮书》中的对 Mock 的定义 在测试当中,mock 是指使用各种技术手段模拟出各种需要的资源以供测试使用。 被 mock 的资源通常有以下特征: 被测目标依赖该资源 该资源可能因为各种原因不稳定、返回结果不断变化或者并不总是能够获取到 该资源跟被测目标本身质量无关 这些资源可能是一个外部或底层接口、一个系统、一组数据对象或者是一整套目标软件 的工作环境等。通过 mock 避免对外部真实资源的依赖实现对被测目标的孤立测试,从而大 大降低测试的难度,节约测试成本。 1.2 Mock 分类 测试中的 Mock 可以大致分为两类:mock 数据和 mock 服务。 Mock 数据即 mock 一个对象,写入一些预期的值,通过它进行自己想要的测试。主要适用于单元测试,例如常见的 EasyMock、Mockito 等。 Mock 服务即 mock 一个 sever,构造一个依赖的服务并给予他预期的服务返回值,适用范围广,更加适合我们的集成测试。因此我们就 mock server 这种模式进行设计与开发。 二、需求分析 2.1 核心场景 场景一: 小明要测试一个普通商品创建订单接口 create,在创建订单的过程中会交易系统会调用店铺系统查询店铺状态接口 queryShop 和营销系统查询营销活动接口 queryUmp,但这两个接口不太稳定,导致订单创建用例常常失败,小明希望每次调用这两个接口都能直接返回200不影响下单流程; 场景二: 小李也在测试订单创建接口,并且依赖店铺查询接口 queryShop 设计了三种店铺状态下的下单场景:店铺正常营业、店铺已打烊、店铺已过试用期未缴费。但是通过正常途径构造测试数据成本很高,需要创建三个店铺->设置店铺状态->创建商品->创建订单。小李希望三个用例调用店铺 query 接口时能返回预期的三个结果; 场景三: 碰巧小红也在测试订单创建接口,他们的用例都集成在同一个 CI 中,但是小红的用例中商品参加了某个营销活动,她希望自己的用例访问营销活动查询接口 queryUmp 时能返回正常的结果,不被 mock 用例所影响。 2.2 需求整理 根据以上三个场景,加之适用于有赞测试环境模式,可归纳为大致六个需求: 1、调用依赖接口时能够返回指定值( mock 的基本功能) 2、支持同时 mock 多个服务 3、多个测试用例依赖同一个接口,运行时支持返回不同结果 4、支持集成在日常 CI 中使用 5、在一个测试套件中只有部分用例需要 mock 接口 A,其他用例则需要正常的结果返回 6、支持有赞 service chain 路由环境模式调用 三、设计思路 3.1 架构设计 整体设计架构图如下: [图片] [图片] Mock 插件设计分为两个部分: 1、bit-mocker 工程作为 jar 包引入我们的接口测试工程中,其中包含两个 Testng 的监听器。IMethodInterceptor 将普通用例与需要 mock 用例分组,优先执行 mock 用例。TestListenerAdapter 监听用例执行过程,在用例开始执行前获取所有要 mock 的服务列表,调用批量注册 mock 服务,在 mock 用例执行完毕后注销服务; 2、trade-mocker-service 工程作为 mock 服务的载体,承担着注册服务、提供服务、泛化调用、注销服务的功能。 3.2 实现方法 前提: 由于有赞及大部分互联网公司均使用Dubbo框架进行项目开发,本文mock服务实现也是基于并适用于Dubbo框架。 现根据上述需求列表依次介绍实现细节: 1. 调用依赖接口时返回指定值(mock的基本功能) ——如下图所示,实现mock基本功能需要做两件事情:创建 provider、接收请求并返回期望 response: [图片] - 创建 provider 上图是一个极其简化的调用图,而众所周知 Dubbo 是通过将 provider 注册到注册中心,再由 consumer 订阅服务,通过注册中心返回到地址来实现调用的。因此我们需要将需要 mock 的服务注册到注册中心才算完成创建。 第一步:将需要 mock 的应用根据 groupId 和 artifactId 从 maven 仓库中拉取到最新版本的 jar 包到本地服务器上。 第二步:本地暴露 mock 服务。配置 Dubbo Provider 有4种方式:XML配置,properties 方式配置,API 调用方式配置,注解方式配置。由于我们的出发点是能够灵活的运用于接口测试中,势必要通过入参来决定 mock 什么服务,因此在 provider 的配置方式上选择了自由度更高的代码配置,而非更加常见的 XML 配置。配置代码如下: [代码]//从入参中获取所有需要暴露的服务List,依次进行暴露 mockServiceModel.getApplications().stream().forEach(application -> application.getServices().stream().forEach(service -> { ServiceConfig serviceConfig = new ServiceConfig(); serviceConfig.setApplication(new ApplicationConfig(application.getApplication())); serviceConfig.setProtocol(new ProtocolConfig(PROTOCOL, application.getPort())); //暂时没有设置注册中心,下一步单独注册 serviceConfig.setRegistry(new RegistryConfig("N/A")); serviceConfig.setInterface(service); //设置泛化调用的实现类 serviceConfig.setRef(new MockGenericService(map)); //设置拦截器 serviceConfig.setFilter("mockFilter"); //服务暴露 serviceConfig.export(); //获取暴露后的dubbo服务ip及端口号 URL exportUrl = (URL) serviceConfig.getExportedUrls().get(0); application.setIp(exportUrl.getIp()); application.setPort(exportUrl.getPort()); } [代码] 第三步:将服务注册到注册中心。有赞目前使用的注册中心是 ETCD,可以直接通过拼接 URL 的方式进行调用注册,但无论是 ETCD、ZooKeeper 或是其他注册中心,实现方式大同小异。 - 接收请求并返回期望 response 第一步:设置调用拦截 filter。在上面配置 Dubbo Provider 的代码中我们已经将拦截器 mockFilter、泛化服务 MockGenericService 设置好了。拦截器和泛化服务都是实现自 Dubbo 原生接口,他们需要搭配使用,相辅相成。先看一下拦截器 mockFilter 实现于 Dubbo 的 Filter 接口。 在 MockFilter 中我们从 Invocation 里获取到客户端的调用方法、参数类型、参数值数组,放进服务端的 RpcInvocation 中,然后修改 RpcInvocation 中的方法、参数类型,将请求转发到我们暴露服务的唯一实现类 MockGenericService。 [代码]public class MockFilter implements Filter { private static final Class<?>[] GENERIC_PARAMETER_TYPES = new Class<?>[] {String.class, String[].class, Object[].class}; @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { RpcInvocation rpcInvocation = (RpcInvocation) invocation; //获取调用参数、参数类型、请求方法,设置到RpcInvocation中 rpcInvocation.setArguments(new Object[] { invocation.getAttachment("interface") + "." + invocation.getMethodName(), Arrays.stream(invocation.getParameterTypes()).map(clazz -> clazz.getName()).toArray(String[]::new), invocation.getArguments()}); rpcInvocation.setParameterTypes(GENERIC_PARAMETER_TYPES); rpcInvocation.setMethodName(Constants.$INVOKE); //调用泛化服务 return invoker.invoke(rpcInvocation); } } [代码] 第二步:实现泛化调用,返回预设的 response。我们的泛化服务实现类 MockGenericService 实现自 Dubbo 的 GenericService 接口。该接口可以说是 mock 功能的好搭档,目前业界很多 mock 方法也都是通过该接口来实现的。 在 MockGenericService 中我们根据 method 从预存好的 response map 中取出匹配的返回值,转换成该方法的返回参数 type。利用拦截器和泛化调用这一套组合拳,mock 的基本功能就实现了。以下为部分代码实现: [代码]public class MockGenericService implements GenericService { //存放mock方法与返回参数 private Map<String, Object> map; public MockGenericService(Map<String, Object> map) { this.map = map; } @Override public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException { //根据调用方法找到预设的mock返回参数 Object value = map.get(method); Gson gson = new Gson(); if (value instanceof Map) { value = gson.toJson(value); } String interfaceName = method.substring(0, method.lastIndexOf(".")); String methodName = method.substring(method.lastIndexOf(".") + 1); try { Class[] parameterClasses = new Class[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { parameterClasses[i] = Class.forName(parameterTypes[i]); } //将返回参数转化为该调用方法的返回参数类型 Type methodReturnType = Class.forName(interfaceName).getDeclaredMethod(methodName, parameterClasses).getGenericReturnType(); return new Gson().fromJson(value.toString(), methodReturnType); } catch (ClassNotFoundException e) { throw new MockException(e); } catch (NoSuchMethodException e) { throw new MockException(e); } } } [代码] 2. 支持同时 mock 多个服务 ——想要在集成 CI 中使用,势必会有越来越多的接口需要 mock。我们将需要 mock 的服务以 List 的类型去触发服务的注册和暴露,事实上上面的代码中已经是这样做了。 3. 多个测试用例依赖同一个接口,运行时支持返回不同结果 ——此时 mock 的服务已经暴露在注册中心,可是每一个方法调用时却想要得到不同的结果,因此我们需要在泛化服务中做改造。 利用我们平时写接口测试常用的框架 Testng 中的监听器(后面还会详细说到)监听每一个接口测试用例 的 onTestStart 时刻,在该用例调用测试之前将 mock 数据塞入到泛化服务中的 response map 中,那么随后请求过来的时候就能获取到在当前用例刚开始的时候最新插入的返回值。下一个请求又做同样的事情,周而复始保证每个请求都能得到对应测试用例预设的返回值; 4. 支持集成在日常 CI 中使用 ——为了尽量简化使用者的操作步骤,我们用监听器+自定义注解的形式实现快速 mock。 首先看一下上面已经提到的过的 Testng 中的监听器,Testng 自带多种类型的监听器。我们的需求是能够把控用例执行节奏,并在各个执行节点中插入需要做的方法,因此我们选择的监听器是 TestListenerAdapter,实现自 ITestListener。 在所有接口测试用例执行前,也就是 onStart 时获取所有需要 mock 的服务列表进行批量注册。 在执行每一个具体测试方法前 onTestStart,我们需要将该用例对应的 mock 返回值塞入 mock 服务的泛化调用中。 这时我们已完成了大部分的 mock 工作,但还差一个完美的 ending,那就是把注册的 mock 服务全部下线,并 kill 对应进程。这么做一是为了不影响其他人在测试环境中的正常使用,二是维护 mock 服务器的稳定性。这部分内容则放在所有测试运行完毕之后 onFinish。 我们看一下具体的代码实现: [代码]public class MockableListener extends TestListenerAdapter { @Override public void onStart(ITestContext testContext) { super.onStart(testContext); Set<Class> testClasses = Arrays.stream(testContext.getAllTestMethods()) .map(testMethod -> testMethod.getTestClass().getRealClass()) .collect(Collectors.toSet()); //初始化mock服务,包括设置sc,注册mock服务 initMockService(testContext, testClasses); //添加一个jvm关闭的钩子,其他情况退出时也会调用注销mock服务 Runtime.getRuntime().addShutdownHook(new Thread(() -> killMock(testContext))); } @Override public void onTestStart(ITestResult result) { super.onTestStart(result); Method method = result.getMethod().getConstructorOrMethod().getMethod(); ITestContext iTestContext = result.getTestContext(); //设置mock返回值 mockData(iTestContext ,method); } @Override public void onFinish(ITestContext testContext) { super.onFinish(testContext); //下线服务并kill对应进程 killMock(testContext); } } [代码] 再来看一下自定义注解,我们一共设置了两个自定义注解 @Mock、@MockData。 在测试类前添加 @Mock 注解,注解着中填写需要 mock 服务的 application、services、groupId、artifactId,在解析到注解中填写的内容后去拉取对应 jar 包,注册服务。 在测试方法前添加 @MockData,里面填上具体想要 mock 的方法及对应返回参数。在每一次调用测试方法时都会读取该注解内的内容塞入 mock 服务的泛化服务中等待调用时返回。 5. 一个测试套件中只有部分用例需要 mock 接口 A,其他用例则需要正常的结果返回 ——在之前的设计中,当 mock 服务注册到注册中心后,它会一直存活在注册中心,直到所有测试结束才会注销,这样势必会影响正常用例的执行结果。当时我设计了三个方案: 1、增加判断条件,如果该方法不需要 mock,则在泛化服务中将请求转发至基础环境,再将正常返回值返回给调用方 2、调整服务注册与下线的时间点,需要 mock 服务时立即 register,当前用例执行完毕立刻 unRigister 3、控制 Testng 中用例执行顺序,将需要 mock 的测试方法放在最前面执行,执行完毕后统一下线 对比了三个方案,方案1改造量大逻辑复杂,方案2对性能损耗过大且易造成不稳定现象,最终选择了方案3。运用Testng中控制测试顺序的监听器 IMethodInterceptor 加以实现。 首先将接口测试用例分为 mock 用例与普通用例两组,优先执行 mock 用例,代码如下。然后在上方的 TestListenerAdapter 监听器中 onTestStart 状态节点增加 mock 用例是否执行完毕判断,如果是,则下线 mock 服务。 [代码] @Override public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) { List<IMethodInstance> result = new ArrayList<>(); methods.forEach(method -> { Test test = method.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class); Set<String> groups = new HashSet<>(); Arrays.stream(test.groups()).forEach(group -> groups.add(group)); //将mock用例与普通用例分组,设置执行顺序 if (groups.contains("mock")) { result.add(0, method); } else { result.add(method); } }); return result; } [代码] 6. 支持有赞 service chain 路由环境模式调用 ——有赞环境具体实现和运行逻辑可以参考有赞环境解决方案,本文仅简单介绍与 mock 实际应用息息相关的部分就是测试环境多环境实现:全链路标识透传 service chain 方案,下方简称 sc。 当客服端发起调用时如果带了 sc 标,那么无论什么业务应用,何种协议、框架,都必须将源端的 sc 标识透传下去,在 RPC 调用过程中每一次的调用都会匹配 provider 是否有对应带 sc 标的服务,如果有,则请求到指定的带 sc 标服务上;如果没有,则默认走到不带任何 sc标的基础环境; 为了适用于有赞 sc 环境方案,我们也是利用 TestListenerAdapter 监听器,在实例化测试类之后和调用任何配置方法之前,自动查询配置文件,以及是否手工 set 过 sc 值,如果有,则在我们 mock 服务信息中添加对应的 sc 信息,并注册到 etcd 上;如果没有,则自动生成一个虚拟 sc,以防影响基础环境的正常调用。sc 环境中调用关系简化图如下: [图片] 需要注意的是,若该 sc 环境中已有正常服务 A,再 mock 一个服务 A,会导致同一个 sc 上有两个服务,此时调用会因为均衡负载的原理随机调用。所以该场景不支持也不允许。 四、整体实现 为了将整个实现细节流程串在一起便于大家理解,将全流程调用及实现画在以下时序图中: [图片] 五、实际应用 5.1 插件应用 操作步骤: Step0: 引入 jar 包,并按原步骤编写执行接口测试 Step1: 测试类前添加 @Listeners({MockableListener.class}) 注解 Step2: 测试类前添加 @Mock 注解,填写mock的应用和接口以及接口所属的 groupId、artifactId Step3: 测试方法前添加 @MockData 注解,填写 mock 接口的方法,以及 mock 数据对应的方法 Step4: 按原步骤添加业务校验 指定sc: 如配置文件中没有指定 sc 环境则在 @Mock 中会随机生成 sc 如在指定 sc 环境跑接口用例,mock 服务会使用当前 sc 标识,导致同一个 sc 有两个相同服务注册,所以禁止 mock 当前 sc 环境中已有的服务! mock多服务: 测试类前添加多个 @Mock 注解 mock信息: 增加一个返回值为需要mock的数据的方法,与@MockData注解中方法名对应 5.2 Demo 展示 以需求分析中的场景二为例,测试一个创建订单接口 create,mock 店铺查询接口 queryShop 返回店铺已打烊,校验结果为下单失败。 [代码]//添加监听器 @Listeners({MockableListener.class}) //添加 @Mock 注解及 mock 应用、接口信息 @Mock(application = "shop-prod", services = "com.youzan.shopcenter.xxx.xxx.xxx", groupId = "com.youzan.shopcenter", artifactId = "shopprod-api") public class MockdemoTest extends BaseTradeTest { //添加 @MockData 注解及 mock 方法和对应的返回参数 @MockData(mockMethod = "com.youzan.shopcenter.xxx.xxx.xxx.queryShop", dataMethod = "mockData") @Test public void testMock1(){ // 下单数据构造 OrderCreationDTO orderCreationDTO = JSON.parseObject(NormalCreate, OrderCreationDTO.class); // 调用订单创建接口 PlainResult<OrderCreationVO> rsCreate = tradeService.create(orderCreationDTO); //校验订单创建结果与mock是否一致 Assert.assertEquals(rsCreate.getMessage(),"该店铺已打烊,暂无法购买,请联系商家。"); } // mock 返回参数构造 public static PlainResult<List<ShopAbilityInfo>> mockData() { PlainResult<List<ShopAbilityInfo>> result = new PlainResult<List<ShopAbilityInfo>>(); result.setCode(200); result.setMessage("successful"); ShopAbilityInfo shopAbilityInfo = new ShopAbilityInfo(); shopAbilityInfo.setKdtId(28945646); shopAbilityInfo.setAbilityCode("trade_online_order_ability"); //设置店铺打烊 shopAbilityInfo.setInvalidCode("SHOP_PROD_RELATION_INVALID"); result.setData(shopAbilityInfo); return result; } } [代码] 5.3 Mock 使用原则 使用时不能过度依赖 mock,而需要从 mock 的必要性以及投入产出比考虑,可以基于以下两个原则考虑: 只对构造步骤复杂、构造耗时较长、不稳定的依赖对象/服务进行mock。 依赖对象/服务的逻辑正确性与稳定性与我们测试的对象无关。 六、展望 Mock 插件还有许多地方可以优化和拓展,例如更加精准的控制 mock 服务对用例的影响,支持更多类型的 mock。希望大家能多提宝贵意见,一起探讨与学习!
2019-06-27 - 如何写出一手好的小程序之多端架构篇
本文大致需要 14m+ 的阅读时间。 简述小程序的通信体系 为了大家能更好的开发出一些高质量、高性能的小程序,这里带大家理解一下小程序在不同端上架构体系的区分,更好的让大家理解小程序一些特有的代码写作方式。 整个小程序开发生态主要可以分为两部分: 桌面 nwjs 的微信开发者工具(PC 端) 移动 APP 的正式运行环境 一开始的考虑是使用双线程模型来解决安全和可控性问题。不过,随着开发的复杂度提升,原有的双线程通信耗时对于一些高性能的小程序来说,变得有些不可接受。也就是每次更新 UI 都是通过 webview 来手动调用 API 实现更新。原始的基础架构,可以参考官方图: [图片] 不过上面那张图其实有点误导行为,因为,webview 渲染执行在手机端上其实是内核来操作的,webview 只是内核暴露的一下 DOM/BOM 接口而已。所以,这里就有一个性能突破点就是,JSCore 能否通过 Native 层直接拿到内核的相关接口?答案是可以的,所以上面那种图其实可以简单的再进行一下相关划分,新的如图所示: [图片] 简单来说就是,内核改改,然后将规范的 webview 接口,选择性的抽一份给 JsCore 调用。但是,有个限制是 Android 端比较自由,通过 V8 提供 plugin 机制可以这么做,而 IOS 上,苹果爸爸是不允许的,除非你用的是 IOS 原生组件,这样的话就会扯到同层渲染这个逻辑。其实他们的底层内容都是一致的。 后面为了大家能更好理解在小程序具体开发过程中,手机端调试和在开发者工具调试的大致区分,下面我们来分析一下两者各自的执行逻辑。 tl;dr 开发者工具 通信体系 (只能采用双向通信) 即,所有指令都是通过 appservice <=> nwjs 中间层 <=> webview Native 端运行的通信体系: 小程序基础通信:双向通信-- ( core <=> webview <=> intermedia <=> appservice ) 高阶组件通信:单向通信体系 ( appservice <= android/Swift => core) JSCore 具体执行 appservice 的逻辑内容 开发者工具的通信模式 一开始考虑到安全可控的原因使用的是双线程模型,简单来说你的所有 JS 执行都是在 JSCore 中完成的,无论是绑定的事件、属性、DOM操作等,都是。 开发者工具,主要是运行在 PC 端,它内部是使用 nwjs 来做,不过为了更好的理解,这里,直接按照 nwjs 的大致技术来讲。开发者工具使用的架构是 基于 nwjs 来管理一个 webviewPool,通过 webviewPool 中,实现 appservice_webview 和 content_webview。 所以在小程序上的一些性能难点,开发者工具上并不会构成很大的问题。比如说,不会有 canvas 元素上不能放置 div,video 元素不能设置自定义控件等。整个架构如图: [图片] 当你打开开发者工具时,你第一眼看见的其实是 appservice_webview 中的 [代码]Console[代码] 内容。 [图片] content_webview 对外其实没必要暴露出来,因为里面执行的小程序底层的基础库和 开发者实际写的代码关系不大。大家理解的话,可以就把显示的 WXML 假想为 content_webview。 [图片] 当你在实际预览页面执行逻辑时,都是通过 content_webview 把对应触发的信令事件传递给 service_webview。因为是双线程通信,这里只要涉及到 DOM 事件处理或者其他数据通信的都是异步的,这点在写代码的时候,其实非常重要。 如果在开发时,需要什么困难,欢迎联系:开发者专区 | 微信开放社区 IOS/Android 协议分析 前面简单了解了开发者工具上,小程序模拟的架构。而实际运行到手机上,里面的架构设计可能又会有所不同。主要的原因有: IOS 和 Android 对于 webview 的渲染逻辑不同 手机上性能瓶颈,JS 原始不适合高性能计算 video 等特殊元素上不能被其他 div 覆盖 … 一开始做小程序的双线程架构和开发者工具比较类似,content_webview 控制页面渲染,appservice 在手机上使用 JSCore 来进行执行。它的默认架构图其实就是这个: [图片] 但是,随着用户量的满满增多,对小程序的期望也就越高: 小程序的性能是被狗吃了么? 小程序打开速度能快一点么? 小程序的包大小为什么这么小? … 这些,我们都知道,所以都在慢慢一点一点的优化。考虑到原生 webview 的渲染性能很差,组内大神 rex 提出了使用同层渲染来解决性能问题。这个办法,不仅搞定了 video 上不能覆盖其他元素,也提高了一下组件渲染的性能。 开发者在手机上具体开发时,对于某些 高阶组件,像 video、canvas 之类的,需要注意它们的通信架构和上面的双线程通信来说,有了一些本质上的区别。为了性能,这里底层使用的是原生组件来进行渲染。这里的通信成本其实就回归到 native 和 appservice 的通信。 为了大家更好的理解 appservice 和 native 的关系,这里顺便简单介绍一下 JSCore 的相关执行方法。 JSCore 深入浅出 在 IOS 和 Android 上,都提供了 JSCore 这项工程技术,目的是为了独立运行 JS 代码,而且还提供了 JSCore 和 Native 通信的接口。这就意味着,通过 Native 调起一个 JSCore,可以很好的实现 Native 逻辑代码的日常变更,而不需要过分的依靠发版本来解决对应的问题,其实如果不是特别严谨,也可以直接说是一种 "热更新" 机制。 在 Android 和 IOS 平台都提供了各自运行的 JSCore,在国内大环境下运行的工程库为: Anroid: 国内平台较为分裂,不过由于其使用的都是 Google 的 Android 平台,所以,大部分都是基于 chromium 内核基础上,加上中间层来实现的。在腾讯内部通常使用的是 V8 JSCore。 IOS: 在 IOS 平台上,由于是一整个生态闭源,在使用时,只能是基于系统内嵌的 webkit 引擎来执行,提供 webkit-JavaScriptCore 来完成。 这里我们主要以具有官方文档的 webkit-JavaScriptCore 来进行讲解。 JSCore 核心基础 普遍意义上的 JSCore 执行架构可以分为三部分 JSVirtualMachine、JSContext、JSValue。由这三者构成了 JSCore 的执行内容。具体解释参考如下: JSVirtualMachine: 它通过实例化一个 VM 环境来执行 js 代码,如果你有多个 js 需要执行,就需要实例化多个 VM。并且需要注意这几个 VM 之间是不能相互交互的,因为容易出现 GC 问题。 JSContext: jsContext 是 js代码执行的上下文对象,相当于一个 webview 中的 window 对象。在同一个 VM 中,你可以传递不同的 Context。 JSValue: 和 WASM 类似,JsValue 主要就是为了解决 JS 数据类型和 swift 数据类型之间的相互映射。也就是说任何挂载在 jsContext 的内容都是 JSValue 类型,swift 在内部自动实现了和 JS 之间的类型转换。 大体内容可以参考这张架构图: [图片] 当然,除了正常的执行逻辑的上述是三个架构体外,还有提供接口协议的类架构。 JSExport: 它 是 JSCore 里面,用来暴露 native 接口的一个 protocol。简单来说,它会直接将 native 的相关属性和方法,直接转换成 prototype object 上的方法和属性。 简单执行 JS 脚本 使用 JSCore 可以在一个上下文环境中执行 JS 代码。首先你需要导入 JSCore: [代码]import JavaScriptCore //记得导入JavaScriptCore [代码] 然后利用 Context 挂载的 evaluateScript 方法,像 new Function(xxx) 一样传递字符串进行执行。 [代码]let contet:JSContext = JSContext() // 实例化 JSContext context.evaluateScript("function combine(firstName, lastName) { return firstName + lastName; }") let name = context.evaluateScript("combine('villain', 'hr')") print(name) //villainhr // 在 swift 中获取 JS 中定义的方法 let combine = context.objectForKeyedSubscript("combine") // 传入参数调用: // 因为 function 传入参数实际上就是一个 arguemnts[fake Array],在 swift 中就需要写成 Array 的形式 let name2 = combine.callWithArguments(["jimmy","tian"]).toString() print(name2) // jimmytian [代码] 如果你想执行一个本地打进去 JS 文件的话,则需要在 swift 里面解析出 JS 文件的路径,并转换为 String 对象。这里可以直接使用 swift 提供的系统接口,Bundle 和 String 对象来对文件进行转换。 [代码]lazy var context: JSContext? = { let context = JSContext() // 1 guard let commonJSPath = Bundle.main.path(forResource: "common", ofType: "js") else { // 利用 Bundle 加载本地 js 文件内容 print("Unable to read resource files.") return nil } // 2 do { let common = try String(contentsOfFile: commonJSPath, encoding: String.Encoding.utf8) // 读取文件 _ = context?.evaluateScript(common) // 使用 evaluate 直接执行 JS 文件 } catch (let error) { print("Error while processing script file: \(error)") } return context }() [代码] JSExport 接口的暴露 JSExport 是 JSCore 里面,用来暴露 native 接口的一个 protocol,能够使 JS 代码直接调用 native 的接口。简单来说,它会直接将 native 的相关属性和方法,直接转换成 prototype object 上的方法和属性。 那在 JS 代码中,如何执行 Swift 的代码呢?最简单的方式是直接使用 JSExport 的方式来实现 class 的传递。通过 JSExport 生成的 class,实际上就是在 JSContext 里面传递一个全局变量(变量名和 swift 定义的一致)。这个全局变量其实就是一个原型 prototype。而 swift 其实就是通过 context?.setObject(xxx) API ,来给 JSContext 导入一个全局的 Object 接口对象。 那应该如何使用该 JSExport 协议呢? 首先定义需要 export 的 protocol,比如,这里我们直接定义一个分享协议接口: [代码]@objc protocol WXShareProtocol: JSExport { // js调用App的微信分享功能 演示字典参数的使用 func wxShare(callback:(share)->Void) // setShareInfo func wxSetShareMsg(dict: [String: AnyObject]) // 调用系统的 alert 内容 func showAlert(title: String,msg:String) } [代码] 在 protocol 中定义的都是 public 方法,需要暴露给 JS 代码直接使用的,没有在 protocol 里面声明的都算是 私有 属性。接着我们定义一下具体 WXShareInface 的实现: [代码]@objc class WXShareInterface: NSObject, WXShareProtocol { weak var controller: UIViewController? weak var jsContext: JSContext? var shareObj:[String:AnyObject] func wxShare(_ succ:()->{}) { // 调起微信分享逻辑 //... // 成功分享回调 succ() } func setShareMsg(dict:[String:AnyObject]){ self.shareObj = ["name":dict.name,"msg":dict.msg] // ... } func showAlert(title: String, message: String) { let alert = AlertController(title: title, message: message, preferredStyle: .Alert) // 设置 alert 类型 alert.addAction(AlertAction(title: "确定", style: .Default, handler: nil)) // 弹出消息 self.controller?.presentViewController(alert, animated: true, completion: nil) } // 当用户内容改变时,触发 JS 中的 userInfoChange 方法。 // 该方法是,swift 中私有的,不会保留给 JSExport func userChange(userInfo:[String:AnyObject]) { let jsHandlerFunc = self.jsContext?.objectForKeyedSubscript("\(userInfoChange)") let dict = ["name": userInfo.name, "age": userInfo.age] jsHandlerFunc?.callWithArguments([dict]) } } [代码] 类是已经定义好了,但是我们需要将当前的类和 JSContext 进行绑定。具体步骤是将当前的 Class 转换为 Object 类型注入到 JSContext 中。 [代码]lazy var context: JSContext? = { let context = JSContext() let shareModel = WXShareInterface() do { // 注入 WXShare Class 对象,之后在 JSContext 就可以直接通过 window.WXShare 调用 swift 里面的对象 context?.setObject(shareModel, forKeyedSubscript: "WXShare" as (NSCopying & NSObjectProtocol)!) } catch (let error) { print("Error while processing script file: \(error)") } return context }() [代码] 这样就完成了将 swift 类注入到 JSContext 的步骤,余下的只是调用问题。这里主要考虑到你 JS 执行的位置。比如,你可以直接通过 JSCore 执行 JS,或者直接将 JSContext 和 webview 的 Context 绑定在一起。 直接本地执行 JS 的话,我们需要先加载本地的 js 文件,然后执行。现在本地有一个 share.js 文件: [代码]// share.js 文件 WXShare.setShareMsg({ name:"villainhr", msg:"Learn how to interact with JS in swift" }); WXShare.wxShare(()=>{ console.log("the sharing action has done"); }) [代码] 然后,我们需要像之前一样加载它并执行: [代码]// swift native 代码 // swift 代码 func init(){ guard let shareJSPath = Bundle.main.path(forResource:"common",ofType:"js") else{ return } do{ // 加载当前 shareJS 并使用 JSCore 解析执行 let shareJS = try String(contentsOfFile: shareJSPath, encoding: String.Encoding.utf8) self.context?.evaluateScript(shareJS) } catch(let error){ print(error) } } [代码] 如果你想直接将当前的 WXShareInterface 绑定到 Webview Context 中的话,前面实例的 Context 就需要直接修改为 webview 的 Context。对于 UIWebview 可以直接获得当前 webview 的Context,但是 WKWebview 已经没有了直接获取 context 的接口,wkwebview 更推崇使用前文的 scriptMessageHandler 来做 jsbridge。当然,获取 wkwebview 中的 context 也不是没有办法,可以通过 KVO 的 trick 方式来拿到。 [代码]// 在 webview 加载完成时,注入相关的接口 func webViewDidFinishLoad(webView: UIWebView) { // 加载当前 View 中的 JSContext self.jsContext = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext let model = WXShareInterface() model.controller = self model.jsContext = self.jsContext // 将 webview 的 jsContext 和 Interface 绑定 self.jsContext.setObject(model, forKeyedSubscript: "WXShare") // 打开远程 URL 网页 // guard let url = URL(string: "https://www.villainhr.com") else { // return //} // 如果没有加载远程 URL,可以直接加载 // let request = URLRequest(url: url) // webView.load(request) // 在 jsContext 中直接以 html 的形式解析 js 代码 // let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html") // self.jsContext.evaluateScript(try? String(contentsOfURL: url!, encoding: NSUTF8StringEncoding)) // 监听当前 jsContext 的异常 self.jsContext.exceptionHandler = { (context, exception) in print("exception:", exception) } } [代码] 然后,我们可以直接通过上面的 share.js 调用 native 的接口。 原生组件的通信 JSCore 实际上就是在 native 的一个线程中执行,它里面没有 DOM、BOM 等接口,它的执行和 nodeJS 的环境比较类似。简单来说,它就是 ECMAJavaScript 的解析器,不涉及任何环境。 在 JSCore 中,和原生组件的通信其实也就是 native 中两个线程之间的通信。对于一些高性能组件来说,这个通信时延已经减少很多了。 那两个之间通信,是传递什么呢? 就是 事件,DOM 操作等。在同层渲染中,这些信息其实都是内核在管理。所以,这里的通信架构其实就变为: [图片] Native Layer 在 Native 中,可以通过一些手段能够在内核中设置 proxy,能很好的捕获用户在 UI 界面上触发的事件,这里由于涉及太深的原生知识,我就不过多介绍了。简单来说就是,用户的一些 touch 事件,可以直接通过 内核暴露的接口,在 Native Layer 中触发对应的事件。这里,我们可以大致理解内核和 Native Layer 之间的关系,但是实际渲染的 webview 和内核有是什么关系呢? 在实际渲染的 webview 中,里面的内容其实是小程序的基础库 JS 和 HTML/CSS 文件。内核通过执行这些文件,会在内部自己维护一个渲染树,这个渲染树,其实和 webview 中 HTML 内容一一对应。上面也说过,Native Layer 也可以和内核进行交互,但这里就会存在一个 线程不安全的现象,有两个线程同时操作一个内核,很可能会造成泄露。所以,这里 Native Layer 也有一些限制,即,它不能直接操作页面的渲染树,只能在已有的渲染树上去做节点类型的替换。 最后总结 这篇文章的主要目的,是让大家更加了解一下小程序架构模式在开发者工具和手机端上的不同,更好的开发出一些高性能、优质的小程序应用。这也是小程序中心一直在做的事情。最后,总结一下前面将的几个重要的点: 开发者工具只有双线程架构,通过 appservice_webview 和 content_webview 的通信,实现小程序手机端的模拟。 手机端上,会根据组件性能要求的不能对应优化使用不同的通信架构。 正常 div 渲染,使用 JSCore 和 webview 的双线程通信 video/map/canvas 等高阶组件,通常是利用内核的接口,实现同层渲染。通信模式就直接简化为 内核 <=> Native <=> appservice。(速度贼快) 参考: 教程 | 《小程序开发指南》
2019-02-19 - 小程序类目简介
为了能让C端用户更快速直接了解小程序的功能及内容,以及基于部分行业的法律法规、平台管理的需要,小程序提供的服务需要与小程序所选的类目一致。 一、类目的选择 (1)类目根据主体的不同,开放范围分为个人、组织、境外。 (2)基于法律法规、平台管理的需要,部分类目需要提交相关的资质并经过审核后,才能申请成功。无资质要求的类目选择后即刻生效。 (3)建议开发者根据小程序自身服务内容对照类目适用范围申请相应的类目,以避免后期提交代码审核/小程序上架后出现类目不符等问题被驳回或按违规处理。 (4)小程序类目对应资质、部分示例模板等内容详情可参考:点击查看。 二、小程序类目配置指引 (1)初始化类目配置介绍: 登录【微信公众平台】 ->【点击立即注册】 ->【选择账号类型:小程序进入注册详情页】 ->【填写账号信息,点击注册】 ->【点击登录邮箱,进行账号激活】 ->【激活后进行用户信息登记】 ->【小程序发布流程】 ->【小程序类目,点击去补充,添加类目】 ->【提交】 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] (2)小程序类目后台配置介绍: 登录【微信公众平台】 ->【小程序发布流程】 ->【小程序类目,点击查看详情】 ->【选择添加服务类目并上传类目资质】 ->【提交】 [图片] [图片] [图片] 三、提前准备小程序类目资质 小程序不同类目对应的资质不同,建议开发者可根据微信开放的“小程序开放的服务类目”表查看对应资质,并提前做好申请相关资质准备,且资质需在有效期内,避免提交类目审核时因资质不符或过期等情况被驳回。 四、类目审核常见问题 1.一个小程序可选 5个 服务类目,且一个月有 5次 修改类目机会; 2.为满足部分开发者业务拓展的诉求,小程序类目已达5个且满足一定条件的账号可以申请【更多类目】。 更多类目申请条件(同时满足): (1)小程序的5个类目中至少有1个资质类目。类目资质可见开放的服务类目表; (2)小程序近7天每天访问人数100人及以上; (3)小程序近3个月无违规行为; (4)本月仍有添加服务类目的次数。 申请方法:登录【微信公众平台小程序】 ->【小程序发布流程】 ->【小程序类目,点击查看详情】 ->【申请更多类目】 ->【提交】 [图片] 3.类目审核时长:一般情况为1 - 7天,如出现审核量大、内容复杂等特殊情况可能延长;如有紧急申请需求,且有加急额度情况下,在选择需提交资质的类目时,可同时勾选【审核加急】按钮,详情参考:【小程序类目审核加急通道开放】 4.部分常见驳回原因: 情况1: 提交资质主体与小程序注册主体不一致 案例:小程序(主体为123)申请【医疗服务-其他私立医疗机构】类目,提交资质主体为345,即为主体不一致 建议:以有资质的主体申请小程序 或 用小程序主体申请相关资质 情况2:提交资质已过期 案例:小程序申请【医疗服务-其他私立医疗机构】类目,提交《医疗执业许可证》,许可有效期为2016.09.12,即为过期 建议:提交许可资质在有效期内且有效期大于3个月及以上 情况3:提交资质模糊 案例:小程序申请【社交-社区/论坛】类目,提交的《非经营性互联网信息服务备案核准》资质模糊,审核无法辨别 建议:请确保提交资质清晰,可供审核辨别 情况4:提交虚假资质 案例:小程序(主体为123)申请【社交-社区/论坛】类目,提交主体一致的《非经营性互联网信息服务备案核准》资质,审核人员经可信渠道核实其资质主体为456,将判断小程序提交虚假资质 建议:以有资质的主体申请小程序 或 用小程序主体申请相关资质 温馨提醒: 审核中如发现小程序提交虚假资质,平台将视违规情节采取限制、甚至停止提供相关功能服务等措施,如涉及违法行为,可能追究其民事、刑事等相关法律责任,请各位开发者勿在“危险”的边缘试探哦~
09-09 - 小程序服务商开发者,搭建一个mvp第三方平台必备的开发功能模块构成
我们在搭建小程序第三方平台过程发现官方提供的有很多接口,但不是每个接口都需要开发者用代码去实现。比如代码包管理的删除接口,就可以通过登录open.weixin.qq.com里,在列表看着更加详细的代码包版本介绍通过官方提供的界面就可以删除。即如果不是特殊需要,此接口可以不做开发。 那么我们搭建一个小程序服务商第三方平台需要哪些必要的功能模块呢? 必备功能模块概况 首先用一张图总概括下搭建一个mvp小程序服务商第三方平台必备的开发功能模块 [图片] 第三方平台权限集 先用一张图表明第三方平台账号在授权后可用的权限集的部分。 [图片] 只有平台先申请某一个权限,经过审核且全网发布后,旗下小程序才能在授权时选择某权限。有以下等 A>=B, B>=C;也就是第三方平台即使有每个微信提供的每个权限集,通过管理员授权后也不一定有全部权权限。原因: 1 单一授权被授权给其他第三方平台账号,或管理员授权时选择不把某一个权限授权给平台。比如开发和代码管理权限集 2 旗下小程序自身主体资质性质自带,不能拥有某个api权限。案例参考3 3 最终授权成功的权限集里,每个权限下都有多个api,所以在用某个api前需要看是由有权限。比如开发和代码管理权限集下,setwebviewdomain这个api不支持个人主体资质的appid调用 4 授权成功后,用token就可以用第三方平台提供的api代调用实现业务,或代调用小程序开发文档的api(标志参数需要access_token)里的接口,来实现产品开发。 授权及回调域名验证 ####### 说明 ####### 发起域名必须要在合法登记的域名名单内,且用户授权同意后的跳转url必须要在资料登记。体现在第三方平台创建时的以下两项配置中。 ① 授权发起页域名 ② 授权事件接收URL [图片] 授权回调URL源码 [代码] public void ProcessRequest(HttpContext context) { Request = context.Request; Response = context.Response; string signature = Request.QueryString["signature"]; string msg_signature = Request.QueryString["msg_signature"]; string timestamp = Request.QueryString["timestamp"]; string nonce = Request.QueryString["nonce"]; #region Response body // 微信并不会因为返回失败就再发送一次消息,相反如果不返回success, 微信会延迟推送 string ResMsg = "success"; if (Request.HttpMethod == "POST") { using (Stream stream = Request.InputStream) { Byte[] postBytes = new Byte[stream.Length]; stream.Read(postBytes, 0, (Int32)stream.Length); string postString = Encoding.UTF8.GetString(postBytes); if (postString.Contains("<AppId>")) { XElement xdoc = XElement.Parse(postString); CurAppID = xdoc.Element("AppId").Value.Trim(); LoadAppInfo(CurAppID); string postStringXmlSrc = string.Empty; Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(CurAppToken, CurAppEncodingAESKey, CurAppID); int ret = wxcpt.DecryptMsg(msg_signature, timestamp, nonce, postString, ref postStringXmlSrc); if (ret == 0) { xdoc = XElement.Parse(postStringXmlSrc); if (xdoc != null) { string InfoType = xdoc.Element("InfoType").Value.Trim(); if (InfoType == "component_verify_ticket") { string componentVerifyTicket = xdoc.Element("ComponentVerifyTicket").Value.Trim(); WeixinDataHelper.UpdateComponentVerifyTicket(CurAppID, componentVerifyTicket); } else if (InfoType == "unauthorized") { string authorizedAppId = xdoc.Element("AuthorizerAppid").Value.Trim(); WeixinDataHelper.Unauthorized(CurAppID, authorizedAppId); } else { // 微信平台上填写的授权URL 目前就支持这两种 InfoType } } } } } } else if (Request.HttpMethod == "GET") { ResMsg = Request.QueryString["echostr"]; } ResponseEnd(ResMsg); #endregion } [代码] 各种票据有效性维护机制 1 授权后得到授权码(authorization_code ) [代码]需要用授权码去调用接口换取令牌,并保存。 [代码] 2 获取令牌和刷新令牌 [代码]用授权码获得令牌authorizer_access_token和刷新令牌authorizer_refresh_token。 需要保存令牌和刷新令牌。 [代码] 3 刷新令牌authorizer_access_token [代码]用刷新令牌定期去微信网关拉取令牌,维持令牌的有效性,保证后期代实现接口时令牌有效性。约1h左右的时间去刷新一次令牌。刷新令牌服务需要有重试机制,因为瞬时网络原因会返回失败,需要重试。 [代码] 消息与事件处理平台 1 第三方平台component_verify_ticke更新 [代码]平台审核通过后,每隔10分钟定时推送一次component_verify_ticket,开发者需要保存在数据库。再授权场景获取预授权码时需要用到这个有效的ticket。 [代码] 2 授权状态变更(成功,变更,取消) 3 代码审核通知消息 4 注意: [代码]① 这里的消息时加密的需要先解密。 ② 有开发者反馈说不知道返回信息时旗下哪个appid,这里补充下,appid是再请求头的request参数里直接返回的。 ③ 消息与通知解密部分代码 [代码] [代码] public void ProcessRequest(HttpContext context) { HttpRequest Request = context.Request; HttpResponse Response = context.Response; // 所属的已授权公众号的appid string AppID = Request.QueryString["AppID"]; string reqSignature = Request.QueryString["signature"]; string reqMsgSignature = Request.QueryString["msg_signature"]; string reqTimestamp = Request.QueryString["timestamp"]; string reqNonce = Request.QueryString["nonce"]; if (string.IsNullOrEmpty(AppID) || !WeixinHelper.ValidateWeixinInterface(reqSignature, WeixinResources.ComponentAppToken, reqTimestamp, reqNonce)) { ResponseEnd(Response, string.Empty,AppID); return; } if (AppID == WeixinResources.AutoTestAppID) { string _tmsg = new WeixinAutoTestHandler(Request, Response, WeixinResources.ComponentAppID).GetMsg(); ResponseEnd(Response, _tmsg,AppID); return; } #region Response body string ResMsg = string.Empty; if (Request.HttpMethod == "POST") { using (Stream stream = Request.InputStream) { Byte[] postBytes = new Byte[stream.Length]; stream.Read(postBytes, 0, (Int32)stream.Length); string postString = Encoding.UTF8.GetString(postBytes); string Msg = string.Empty; // 解密 Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(WeixinResources.ComponentAppToken, WeixinResources.ComponentAppEncodingAESKey, WeixinResources.ComponentAppID); int ret = 0; ret = wxcpt.DecryptMsg(reqMsgSignature, reqTimestamp, reqNonce, postString, ref Msg); if (ret != 0) { ResponseEnd(Response, string.Empty,AppID); return; } // 生成响应消息 string resMsg = WeixinHelper.ReturnMessageAsThirdPlatform(AppID, Msg); // 加密消息 string EncryptMsg = string.Empty; ret = wxcpt.EncryptMsg(resMsg, reqTimestamp, reqNonce, ref EncryptMsg); if (ret != 0) { EncryptMsg = string.Empty; } ResMsg = EncryptMsg; } } else if (Request.HttpMethod == "GET") { ResMsg = Request.QueryString["echostr"]; } ResponseEnd(Response, ResMsg,AppID); #endregion } [代码] [代码]public static string ReturnMessageAsThirdPlatform(string AppID, string requestMsg) { string responseContent = string.Empty; XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(requestMsg); XmlNode MsgType = xmlDoc.SelectSingleNode("/xml/MsgType"); if (MsgType != null) { switch (MsgType.InnerText) { case "event": responseContent = EventHandleAsThirdPlatform(AppID, xmlDoc); //事件处理 break; case "text": case "image": case "voice": case "video": case "shortvideo": case "location": responseContent = TextHandleAsThirdPlatform(AppID, xmlDoc); //消息处理 break; default: break; } } return responseContent; } [代码] 代小程序域名设置机制 1 小程序服务器域名 为授权小程序设置服务器域名 requestdomain request合法域名 wsrequestdomain socket合法域名 uploaddomain uploadFile合法域名 downloaddomain downloadFile合法域名 2 小程序业务域名 为授权小程序提供业务域名 web-view的合法域名 第三方平台先登记后,在给旗下小程序授权 旗下小程序可以使用配置域名的子域名作为业务域名 代小程序代码包管理机制 1 提交代码包 详细关于代码包管理参考https://developers.weixin.qq.com/community/develop/article/doc/000622ad764e48a45419e25b151813 ①ext.json的配置项更改体现在代码包配置json数据里。ext_json字段对应ext.json里的字段,这里template_id关联第三方平台》小程序模板管理里的模板ID。 ② 那么要提交小程序的appid呢?还是通过ext_json字段来配置。ext_jsonjson字符串里的extAppid就是第三方平台账号旗下授权,本次要提交代码的小程序appid,通过上传代码时候的来指定extAppid就可以给一个小程序代提交代码。 [图片] [图片] 2 提交审核 3 收到审核通知审核通过后提交上线 4 查询最近一次提交审核进度 5 回退到上一个版本 第三方代码模板管理 1 创建小程序账号,授权给平台账号,并绑定到第三方平台账号的开发小程序列表 2 用开发小程序appid创建项目的开发代码包模板 3 用开发小程序appid提交代码到草稿箱 4从草稿箱添加到模板库 附加总结稿 [图片]
2020-11-05 - 怎么知道用户有没有把小程序收藏到“我的小程序”中?
- 需求的场景描述(希望解决的问题) 一个工具类小程序,希望知道在载入的时候,用户是不是已经新增小程序到“我的小程序”中了。 - 希望提供的能力 有没有这样的接口获取这个信息?或者在用户点击“新增到我的小程序”的时候,触发事件可以捕获的? 或者通过进入来源的scene值可以判断呢? 目前只看到Android系统,如果从桌面小程序图标进入,scene值为1023,收藏到“我的小程序”后再进入,scene值和从“最近打开”进入的scene值一样是1089。1103和1104已经被废弃了。。。
2018-09-20 - 微信小游戏用什么测试占用手机的cpu以及内存??
微信小游戏用什么测试占用手机的cpu以及内存??求大神指导
2019-07-31 - 升级后完全不能真机调试了吗
[图片] 升级后完全不能真机调试了吗 [图片]
2019-07-26 - GBK如何转成utf-8
- 电脑端正常,手机端乱码!编码格式gbk,请求解决,如何手动转换?[图片] [图片] - 手机端,请求返回自动将gbk转成utf-8 - - http请求编码为gbk的时候就会乱码,没有自动转换,能不能让我手动转换?
2018-05-25 - 小程序云开发实战 - 口袋工具之“历史上的今天”
口袋工具y 前言 本项目是一个基于云开发的小程序。 本文选取项目中的一个页面 – 历史上的今天 来做一个云开发的分享,会涉及云函数和云数据库。 由于是实战项目,关于小程序的基础知识请移步官方文档,本文不再赘述。 项目预览 微信搜索: [代码]口袋工具y[代码] 扫一扫: [图片] 前期遇到的问题 数据来源:没有数据,寸步难行呀 如何解决数据来源 编写爬虫将需要的数据爬取并保存下来 找一些提供数据的平台,如阿凡达数据、聚合数据等等。 本项目选择第二种方式,并最终选择了聚合数据平台API。 项目开始 新建项目 新建项目,配置好名称、目录、AppID等信息,后端服务选择小程序·云开发,点击新建。 关于AppID: 请自行修改为你注册的小程序AppID。 [图片] 点击新建即可完成项目初始化,得到一个云开发模板: [图片] 目录结构: [代码]+-- cloudfunctions|[指定的环境] // 存放云函数的目录 +-- miniprogram // 小程序代码编写目录 |-- README.md // 项目描述文件 |-- project.config.json // 项目配置文件 [代码] 新建云开发环境 点击左上角菜单项 [代码]云开发[代码] [图片] 点击创建资源环境,环境名称及环境ID请自行设置: [图片] 点击确定即可完成创建 编写云函数 1. 新建云函数 在目录 [代码]cloudfunctions[代码] 上右键 新建云函数,填入新建云函数的名称(如todayInHistory) 回车或失去焦点即会自动创建并上传。 2. 安装依赖 云函数目前执行环境仅支持node,所以需要使用js来编写云函数的逻辑。 在控制台中进入该云函数的目录,执行 [代码]npm i -S axios [代码] 本项目使用axios来执行请求的发送,可以使用其他如request-promise等等的库来替换 3. 编写云函数 新建 [代码]config.js[代码] 文件,添加代码如下: [代码]exports.key = YOUR_JUHE_KEY // 在聚合数据平台申请的key exports.baseUrl = 'http://v.juhe.cn/todayOnhistory/queryEvent.php' [代码] 打开 ·index.js· 文件,编写代码: [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const axios = require('axios') cloud.init() const db = cloud.database() // 聚合数据 const { baseUrl, key } = require('./config') // 云函数入口函数 exports.main = async(event, context) => { const { month, day } = event const resp = await axios.get(baseUrl, { params: { key, date: `${month}/${day}` } }).then(res => { return res.data }) return resp.result } [代码] 编写页面 1. 新建页面 在开发小程序的过程中,新建一个页面是很常见的操作,有两个非常方便的方式: 在 [代码]app.json[代码] 文件中,在pages项添加我们需要的页面路径,直接保存即可。如: [代码]"pages": [ "pages/today-in-history/index" ] [代码] 在 [代码]pages[代码] 目录下新建目录 [代码]today-in-history[代码] ,在新建的目录上 [代码]右键[代码] -> [代码]新建page[代码] , 填入名称如[代码]index[代码] , 回车即可完成页面下四个文件的创建 2. 编写 [代码]index.wxml[代码] [代码]<view class="container"> <view class="header full-width"> <view>{{year}}年{{month}}月{{day}}日</view> </view> <view class="content full-width"> <view class="list-view"> <block wx:for="{{list}}" wx:key="index"> <view class="item-title">{{item.title}}</view> <view class="item-date">{{item.date}}</view> </block> </view> </view> </view> [代码] 3. 编写 [代码]index.js[代码] [代码]// pages/today-in-history/index.js Page({ /** * 页面的初始数据 */ data: { year: 1990, month: 1, day: 1, list: [] }, /** * 生命周期函数--监听页面加载 */ onLoad: function() { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const day = now.getDate(); this.setData({ year, month, day }); this.doGetList(); }, /** * 执行数据获取 */ doGetList: function() { const { month, day } = this.data; wx.cloud.callFunction({ name: 'todayInHistory', data: { month, day } }).then(res => { let list = res.result.reverse(); this.setData({ list }); }) .catch(console.error) } }) [代码] 4. 编写 [代码]index.wxss[代码] [代码]/* pages/today-in-history/index.wxss */ .container { padding-bottom: 20rpx; background-color: #E8D3A9; } .header { display: flex; justify-content: space-around; align-items: center; height: 80rpx; color: #FFF; } .content { flex: 1; } .list-view { height: 100%; display: flex; flex-direction: column; padding: 0 20rpx; } .list-item { display: flex; flex-direction: column; border-radius: 10rpx; padding: 16rpx 0; box-sizing: border-box; margin-top: 20rpx; background-color: #fff; text-align: center; box-shadow: 1px 1px 5px 1px rgb(207, 207, 207); } .item-title { font-size: 36rpx; padding: 10rpx 16rpx; color: #262626; line-height: 48rpx; } [代码] 5. 效果预览 到这里我们完成了 [代码]历史上的今天[代码] 的列表页,效果如下: [图片] 添加日期选择器 1. 引入 vantweapp 项目中使用 wantweapp 的部分组件 安装 [代码]# npm npm i vant-weapp -S --production # yarn yarn add vant-weapp --production [代码] 构建npm 点击开发者工具菜单项 [代码]工具[代码] -> [代码]构建npm[代码] 程序将自动构建已安装的依赖 2. 在app.json引入组件 [代码] "usingComponents": { "van-datetime-picker": "/miniprogram_npm/vant-weapp/datetime-picker/index", "van-popup": "/miniprogram_npm/vant-weapp/popup/index", "van-toast": "/miniprogram_npm/vant-weapp/toast/index" } [代码] 3. 修改 index.wxml 添加下面的代码 [代码]<view class="full-width"> <van-popup show="{{ show }}" position="bottom"> <van-datetime-picker type="date" value="{{ currentDate }}" bind:cancel="onCancel" bind:confirm="onConfirm" /> </van-popup> </view> <van-toast id="van-toast" /> [代码] 4. 修改 index.js 引入 Toast [代码]import Toast from '../../miniprogram_npm/vant-weapp/toast/toast'; [代码] data 添加 属性 [代码]data: { year: 1990, month: 1, day: 1, list: [], show: false, currentDate: Date.now() } [代码] 添加 监听方法 [代码]/** * 监听日期选择 */ onChangeDate: function() { this.setData({ show: true }); }, /** * 监听取消 */ onCancel: function() { this.setData({ show: false }); }, /** * 监听确定 */ onConfirm: function(event) { const date = new Date(event.detail); const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); this.setData({ year, month, day, show: false }); this.doGetList(); } [代码] 最后修改 doGetList ,添加loading [代码]/** * 执行数据获取 */ doGetList: function() { const { month, day } = this.data; Toast.loading({ mask: true, message: '加载中...' }); wx.cloud.callFunction({ name: 'todayInHistory', data: { month, day } }).then(res => { let list = res.result.reverse(); this.setData({ list }); Toast.clear(); }) .catch(console.error) } [代码] 5. 效果如下 [图片] [图片] 补充 由于聚合数据平台API非会员调用次数有限(100次/天),明显是不太够用的。因此,我们可以考虑在请求到数据时,将数据存在云数据库中,其实也就实现了一个类似爬虫的功能啦。流程如下::梵 [图片] 代码实现: 修改 [代码]cloudfunctions/todayInHistory/index.js[代码] [代码]// ... 省略其他无需改动的代码 exports.main = async(event, context) => { const { month, day } = event const ret = await db.collection('todayInHistory').where({ date: `${month}/${day}` }).get() if (ret.data.length > 0) { return ret.data[0].result } const resp = await axios.get(baseUrl, { params: { key, date: `${month}/${day}` } }).then(res => { return res.data }) await db.collection('todayInHistory').add({ data: { date: `${month}/${day}`, result: resp.result } }) return resp.result } ··· [代码] 结语 目前只开发了两个小功能 [代码]历史上的今天[代码] 和 [代码]周公解梦[代码] ,后续会继续开发新的功能,希望可以做成一个小工具集合,这也是 [代码]口袋工具[代码] 这个名称的由来。 感谢各位读者的阅读,由于本人水平有限,文章中如有错误或不妥之处,请不吝赐教! 如果你喜欢这篇文章或是这个项目,不妨进去GitHub链接点个Star支持下 today。 源码链接 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended
2019-07-30 - 付款时出现“调用支付JSAPI缺少参数:appld”
第三方系统与公众号后台配置时重置了密钥,现第三方系统可以正常进行支付操作,但导致我们原系统出了问题,用户付款时出现:“调用支付JSAPI缺少参数:appld”字样,新的第三方系统用户支付则没有问题,请问如何解决同一个公众号后台能配置两个系统的问题呢?
2019-07-15 - H5纯签约开发问题汇总
H5纯签约接口调用成功后,拿到跳转签约页面URL,访问URL页面显示:"发起签约页面非法"; 签约查询失败,我用的是plan_id+contract_code模式查询,参数如下: <xml> <appid>(私密)</appid> <contract_code>55745910</contract_code> <mch_id>(私密)</mch_id> <contract_id></contract_id> <plan_id>127585</plan_id> <sign>(私密)</sign> <version>1.0</version> </xml> 查询结果如下: <xml> <return_code> <![CDATA[SUCCESS]]> </return_code> <result_code> <![CDATA[FAIL]]> </result_code> <err_code>-25</err_code> <err_code_des> <![CDATA[RESULT NULL]]> </err_code_des> </xml> RESULT NULL 开发文档显示查询为空,传入正确查询参数,我比对了下参数,都没问题,求知道的小伙伴指点下
2019-07-22 - 消息推送 接收的表情emoji不支持
接收到的emoji 都是这种字符串 \/::D\/::D\/::D,这需要如何转化
2019-01-28 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 小程序开发另类小技巧 --用户授权篇
小程序开发另类小技巧 --用户授权篇 getUserInfo较为特殊,不包含在本文范围内,主要针对需要授权的功能性api,例如:wx.startRecord,wx.saveImageToPhotosAlbum, wx.getLocation 原文地址:https://www.yuque.com/jinxuanzheng/gvhmm5/arexcn 仓库地址:https://github.com/jinxuanzheng01/weapp-auth-demo 背景 小程序内如果要调用部分接口需要用户进行授权,例如获取地理位置信息,收获地址,录音等等,但是小程序对于这些需要授权的接口并不是特别友好,最明显的有两点: 如果用户已拒绝授权,则不会出现弹窗,而是直接进入接口 fail 回调, 没有统一的错误信息提示,例如错误码 一般情况而言,每次授权时都应该激活弹窗进行提示,是否进行授权,例如: [图片] 而小程序内只有第一次进行授权时才会主动激活弹窗(微信提供的),其他情况下都会直接走fail回调,微信文档也在句末添加了一句请开发者兼容用户拒绝授权的场景, 这种未做兼容的情况下如果用户想要使用录音功能,第一次点击拒绝授权,那么之后无论如何也无法再次开启录音权限**,很明显不符合我们的预期。 所以我们需要一个可以进行二次授权的解决方案 常见处理方法 官方demo 下面这段代码是微信官方提供的授权代码, 可以看到也并没有兼容拒绝过授权的场景查询是否授权(即无法再次调起授权) [代码]// 可以通过 wx.getSetting 先查询一下用户是否授权了 "scope.record" 这个 scope wx.getSetting({ success(res) { if (!res.authSetting['scope.record']) { wx.authorize({ scope: 'scope.record', success () { // 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问 wx.startRecord() } }) } } }) [代码] 一般处理方式 那么正常情况下我们该怎么做呢?以地理位置信息授权为例: [代码]wx.getLocation({ success(res) { console.log('success', res); }, fail(err) { // 检查是否是因为未授权引起的错误 wx.getSetting({ success (res) { // 当未授权时直接调用modal窗进行提示 !res.authSetting['scope.userLocation'] && wx.showModal({ content: '您暂未开启权限,是否开启', confirmColor: '#72bd4a', success: res => { // 用户确认授权后,进入设置列表 if (res.confirm) { wx.openSetting({ success(res){ // 查看设置结果 console.log(!!res.authSetting['scope.userLocation'] ? '设置成功' : '设置失败'); }, }); } } }); } }); } }); [代码] 上面代码,有些同学可能会对在fail回调里直接使用wx.getSetting有些疑问,这里主要是因为 微信返回的错误信息没有一个统一code errMsg又在不同平台有不同的表现 从埋点数据得出结论,调用这些api接口出错率基本集中在未授权的状态下 这里为了方便就直接调用权限检查了 ,也可以稍微封装一下,方便扩展和复用,变成: [代码] bindGetLocation(e) { let that = this; wx.getLocation({ success(res) { console.log('success', res); }, fail(err) { that.__authorization('scope.userLocation'); } }); }, bindGetAddress(e) { let that = this; wx.chooseAddress({ success(res) { console.log('success', res); }, fail(err) { that.__authorization('scope.address'); } }); }, __authorization(scope) { /** 为了节省行数,不细写了,可以参考上面的fail回调,大致替换了下变量res.authSetting[scope] **/ } [代码] 看上去好像没有什么问题,fail里只引入了一行代码, 这里如果只针对较少页面的话我认为已经够用了,毕竟**‘如非必要,勿增实体’,但是对于小打卡这个小程序来说可能涉及到的页面,需要调用的场景偏多**,我并不希望每次都人工去调用这些方法,毕竟人总会犯错 梳理目标 上文已经提到了背景和常见的处理方法,那么梳理一下我们的目标,我们到底是为了解决什么问题?列了下大致为下面三点: 兼容用户拒绝授权的场景,即提供二次授权 解决多场景,多页面调用没有统一规范的问题 在底层解决,业务层不需要关心二次授权的问题 扩展wx[funcName]方法 为了节省认知成本和减少出错概率,我希望他是这个api默认携带的功能,也就是说因未授权出现错误时自动调起是否开启授权的弹窗 为了实现这个功能,我们可能需要对wx的原生api进行一层包装了(关于页面的包装可以看:如何基于微信原生构建应用级小程序底层架构) 为wx.getLocation添加自己的方法 这里需要注意的一点是直接使用常见的装饰模式是会出现报错,因为wx这个对象在设置属性时没有设置set方法,这里需要单独处理一下 [代码]// 直接装饰,会报错 Cannot set property getLocation of #<Object> which has only a getter let $getLocation = wx.getLocation; wx.getLocation = function (obj) { $getLocation(obj); }; // 需要做一些小处理 wx = {...wx}; // 对wx对象重新赋值 let $getLocation = wx.getLocation; wx.getLocation = function (obj) { console.log('调用了wx.getLocation'); $getLocation(obj); }; // 再次调用时会在控制台打印出 '调用了wx.getLocation' 字样 wx.getLocation() [代码] 劫持fail方法 第一步我们已经控制了wx.getLocation这个api,接下来就是对于fail方法的劫持,因为我们需要在fail里加入我们自己的授权逻辑 [代码]// 方法劫持 wx.getLocation = function (obj) { let originFail = obj.fail; obj.fail = async function (errMsg) { // 0 => 已授权 1 => 拒绝授权 2 => 授权成功 let authState = await authorization('scope.userLocation'); // 已授权报错说明并不是权限问题引起,所以继续抛出错误 // 拒绝授权,走已有逻辑,继续排除错误 authState !== 2 && originFail(errMsg); }; $getLocation(obj); }; // 定义检查授权方法 function authorization(scope) { return new Promise((resolve, reject) => { wx.getSetting({ success (res) { !res.authSetting[scope] ? wx.showModal({ content: '您暂未开启权限,是否开启', confirmColor: '#72bd4a', success: res => { if (res.confirm) { wx.openSetting({ success(res){ !!res.authSetting[scope] ? resolve(2) : resolve(1) }, }); }else { resolve(1); } } }) : resolve(0); } }) }); } // 业务代码中的调用 bindGetLocation(e) { let that = this; wx.getLocation({ type: 'wgs84', success(res) { console.log('success', res); }, fail(err) { console.warn('fail', err); } }); } [代码] 可以看到现在已实现的功能已经达到了我们最开始的预期,即因授权报错作为了wx.getLocation默认携带的功能,我们在业务代码里再也不需要处理任何再次授权的逻辑 也意味着wx.getLocation这个api不论在任何页面,组件,出现频次如何,**我们都不需要关心它的授权逻辑(**效果本来想贴gif图的,后面发现有图点大,具体效果去git仓库跑一下demo吧) 让我们再优化一波 上面所述大致是整个原理的一个思路,但是应用到实际项目中还需要考虑到整体的扩展性和维护成本,那么就让我们再来优化一波 代码包结构: 本质上只要在app.js这个启动文件内,引用./x-wxx/index文件对原有的wx对象进行覆盖即可 [图片] **简单的代码逻辑: ** [代码]// 大致流程: //app.js wx = require('./x-wxx/index'); // 入口处引入文件 // x-wxx/index const apiExtend = require('./lib/api-extend'); module.exports = (function (wxx) { // 对原有方法进行扩展 wxx = {...wxx}; for (let key in wxx) { !!apiExtend[key] && (()=> { // 缓存原有函数 let originFunc = wxx[key]; // 装饰扩展的函数 wxx[key] = (...args) => apiExtend[key](...args, originFunc); })(); } return wxx; })(wx); // lib/api-extend const Func = require('./Func'); (function (exports) { // 需要扩展的api(类似于config) // 获取权限 exports.authorize = function (opts, done) { // 当调用为"确认授权方法时"直接执行,避免死循环 if (opts.$callee === 'isCheckAuthApiSetting') { console.log('optsopts', opts); done(opts); return; } Func.isCheckAuthApiSetting(opts.scope, () => done(opts)); }; // 选择地址 exports.chooseAddress = function (opts, done) { Func.isCheckAuthApiSetting('scope.address', () => done(opts)); }; // 获取位置信息 exports.getLocation = function (opts, done) { Func.isCheckAuthApiSetting('scope.userLocation', () => done(opts)); }; // 保存到相册 exports.saveImageToPhotosAlbum = function (opts, done) { Func.isCheckAuthApiSetting('scope.writePhotosAlbum', () => done(opts)); } // ...more })(module.exports); [代码] 更多的玩法 可以看到我们无论后续扩展任何的微信api,都只需要在lib/api-extend.js 配置即可,这里不仅仅局限于授权,也可以做一些日志,传参的调整,例如: [代码] // 读取本地缓存(同步) exports.getStorageSync = (key, done) => { let storage = null; try { storage = done(key); } catch (e) { wx.$logger.error('getStorageSync', {msg: e.type}); } return storage; }; [代码] 这样是不是很方便呢,至于Func.isCheckAuthApiSetting这个方法具体实现,为了节省文章行数请自行去git仓库里查看吧 关于音频授权 录音授权略为特殊,以wx.getRecorderManager为例,它并不能直接调起录音授权,所以并不能直接用上述的这种方法,不过我们可以曲线救国,达到类似的效果,还记得我们对于wx.authorize的包装么,本质上我们是可以直接使用它来进行授权的,比如将它用在我们已经封装好的录音管理器的start方法进行校验 [代码]wx.authorize({ scope: 'scope.record' }); [代码] 实际上,为方便统一管理,Func.isCheckAuthApiSetting方法其实都是使用wx.authorize来实现授权的 [代码]exports.isCheckAuthApiSetting = async function(type, cb) { // 简单的类型校验 if(!type && typeof type !== 'string') return; // 声明 let err, result; // 获取本地配置项 [err, result] = await to(getSetting()); // 这里可以做一层缓存,检查缓存的状态,如果已授权可以不必再次走下面的流程,直接return出去即可 if (err) { return cb('fail'); } // 当授权成功时,直接执行 if (result.authSetting[type]) { return cb('success'); } // 调用获取权限 [err, result] = await to(authorize({scope: type, $callee: 'isCheckAuthApiSetting'})); if (!err) { return cb('success'); } } [代码] 关于用户授权 用户授权极为特殊,因为微信将wx.getUserInfo升级了一版,没有办法直接唤起了,详见《公告》,所以需要单独处理,关于这里会拆出单独的一篇文章来写一些有趣的玩法 总结 最后稍微总结下,通过上述的方案,我们解决了最开始目标的同时,也为wx这个对象上的方法提供了统一的装饰接口(lib/api-extend文件),便于后续其他行为的操作比如埋点,日志,参数校验 还是那么一句话吧,小程序不管和web开发有多少不同,本质上都是在js环境上进行开发的,希望小程序的社区环境更加活跃,带来更多有趣的东西
2019-06-14 - 无法打开小程序,请稍后再试(错误码104)
长按删除小程序也不行,切换wifi和4g也都不行 微信版本:7.0.4 appId: wx4663ba1e3403df53 [图片]
2019-07-12 - 自用简单的导航栏(欢迎改造/滑稽)
使用scroll-view横向滚动制作的一款导航栏,只做个个简单渐变切换的效果和下划线(下划线也做了渐变觉得太难看注释了,也可以使用),欢迎各位改造,方便以后多用,也可以自己随便用用,不足也可以指出,此贴如有新想法,新改进会持续更新 原始代码链接:https://developers.weixin.qq.com/s/IffO2gmi7Y9z 增加了放大缩小透明变化:https://developers.weixin.qq.com/s/LtPfmhmD7Y9a
2019-06-21 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 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 - 系统信息中的窗口高度
今天发现,在app.js 中执行 wx.getSystemInfoSync() 获取系统信息的 windowHeight是减去 tabbar 的。 在没有tabbar的 页面执行 后获取的高度是算上tabbar高度的。 这样原来想着在 globalData里放上 一次系统信息就可以调用的想法在这个 windowHeight 上是不行的。
2019-05-31 - 小程序图片上传,存储,获取,显示
我们在做小程序开发时,难免会遇到图片上传的功能,我们如果自己搭建图片服务器的话,成功太大了,并且还要写后台程序来接收上传的图片,还要有存储服务器。好在小程序云开发为我们提供了云存储的功能,这样我们就可以轻松的实现小程序图片的上传和存储。 老规矩,先看效果图 [图片] 本节知识点 1,小程序图片的选取 2,小程序图片的上传 3,小程序图片的存储 4,获取云端图片并显示 下面就来具体讲解下具体实现步骤 图片的选择和上传 index.wxml文件如下 [代码] <view class='item_root' bindtap='chuantupian'> <text>{{zhaopian}}</text> <view class='right_arrow' /> </view> [代码] index.js文件如下 [代码] //上传图片 chuantupian() { let that = this; let timestamp = (new Date()).valueOf(); wx.chooseImage({ success: chooseResult => { wx.showLoading({ title: '上传中。。。', }) // 将图片上传至云存储空间 wx.cloud.uploadFile({ // 指定上传到的云路径 cloudPath: timestamp + '.png', // 指定要上传的文件的小程序临时文件路径 filePath: chooseResult.tempFilePaths[0], // 成功回调 success: res => { console.log('上传成功', res) wx.hideLoading() wx.showToast({ title: '上传图片成功', }) if (res.fileID) { that.setData({ zhaopian: '图片如下', imgUrl: res.fileID }) } }, }) }, }) }, [代码] 到这里其实我们就可以实现图片的选取和上传功能了。 下面讲讲具体是如何实现的 首先我们通过wx.chooseImage来获取相册里的图片 再获取照片成功后,我们用当前时间戳命名图片,然后使用 wx.cloud.uploadFile方法来实现图片的上传 在上传成功后,会有如下回调。下图中的filenId就是我们在云存储中的路径,可以直接用这个路径来获取图片并显示的。 [图片] 到这里我们就轻松的实现了小程序图片上传的功能,是不是很简单。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 视频讲解地址:https://edu.csdn.net/course/play/9604/281187 [图片]
2019-06-11 - 永远对微信小程序保持尊重——小程序心得体会和开发经验
开篇 我第一次接触小程序时,还清楚的记得是在2017年的春节期间。当我升级最新版的微信后,开始摸索着新的功能变化。在发现页有一个叫小程序的入口,点进去有一列的“应用”。当我打开一个叫“亲戚关系计算器”的小程序后,简单的使用,然后退出,再去寻找其他的小程序,猫眼电影,自选股,滴滴出行…… 当我尝试着去探索更多小程序的过程中,我突然发现,微信正在发展为一个超级的应用流量入口。而微信中的小程序也可以轻松的坐拥10亿的可用用户人群。 那时候过年,我做的唯一一件事情就是细细的读了小程序的开发文档,心想他可以具有多大的活力与动力,能够引发多大的改变。而我能不能适应他,去随着他的发展,带动自己的腾飞。 很遗憾,当时的小程序并不对个人开发者开放,没有办法注册一个小程序,只能在开发者工具上写一些小的应用,去熟悉小程序的开发。 3月27日,小程序重要更新,其中之一就是支持个人开发者注册小程序。那时候的我,在学校上大一,用一个运营公众号的微信号注册了第一个小程序,并做了一些实验性的开发,并上线。 从这个过程中,我开始细细的体会小程序的优势以及不足,小程序适用于那些领域,小程序适合怎么推广。小程序适合那些行业领域的应用。现在看来,其实从小程序提供的能力,就可以依稀端详出小程序所致力的场景与应用领域。 2018年1月,在我历经半年多的思考和衡量下,做出第一款真正属于自己的小程序——GS比赛计分。去尝试探索线上小程序和线下场景的交融,在这过程中有顾虑,有大胆创新,但都为我更深层次理解小程序有很大的帮助。 目前,自己已经做过10余款小程序,除了GS比赛计分开发时间很长,其他的小程序都是一个月左右的时间完工。比如2019年2月的高校课程助手;2019年3月的数据查询助手;2019年4月的数据汇总助手。 我接下来会从小程序的需求分析与应用设计,小程序的开发,小程序的运营3篇来讲述我对于微信小程序的独自见解与经验。希望能为更多的学生开发者有所启发和借鉴。 需求分析与应用设计 要清楚的认识到小程序对自己需求的最大赋能,需要从最初去理解小程序的定位,微信团队对于小程序的定义是这样的: 微信小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。 通过对小程序提供能力的分析,不难看出。小程序相对于APP来说,在降低开发门槛的同时能够满足最普遍的应用需求,适合生活服务类线下商铺以及非刚需低频应用领域。使得微信通过小程序作为生态的建立者和维护者,赋能商家和应用者。以一种生态触角的状态来迅速的捕捉最大化生态红利。 当清楚的理解小程序的定位后,那么就需要合理的筛选需求并进行应用的功能设计。我将会使用【GS比赛计分】作为例子进行分析说明。 1.用实质的问题引出明确需求,并确定解决问题的功能边界 首先思考的是,自己需要做的功能都有哪些,使用人群是谁?使用场景是什么?需要数据的实时性吗? 一开始设计【GS比赛计分】的时候,目的是为了解决现在的中小型比赛使用人工计分的失效性差,出错率高,人工和时间成本昂贵的问题。程序的目的是将比赛计分的相关人员用互联网工具联系起来,以提升计分效率。 [图片] 当明晰了产品目标后,开始考虑使用人群。一般来看,大多数的比赛计分都是由两种角色构成的,一个是评委(分数确定者),另一个是工作人员(分数汇总者)。 传统的计分过程是通过现场工作人员将评委的分数以纸质的形式送达计分人员。在这一过程中就有大量的成本浪费了(一个是人力成本,另一个是时间成本)。另外,计分人员以Excel或计算器和笔记的形式进行比分汇总,在这一过程中又存在大量的成本浪费(人力成本和时间成本甚至还有错误风险)。 [图片] 所以横观所有的主观评分的线下比赛,无一例外都存在比赛结束后有长时间的节目或视频热场环节,其实这都是在为人工计分的缓慢争取时间。 所以,【GS比赛计分】的使用人群和使用场景就确定下来了,与传统人工计分的过程类似,只不过需要互联网赋能,解决时间和人力成本。需要数据的实时交互。 【GS比赛计分】作为解决现象问题的工具,首先需要做的就是不要违背现象中事件的发生次序。所以功能完全参照比赛计分过程来设计。具体功能如下,提交成绩,撤回成绩,弃权处理,实时的选手分数,实时的选手名次,清楚评委数据,解绑评委,重启比赛,结束比赛,比赛内置会话。 2.对程序平台进行横向比对,明确小程序的优势和劣势 当程序的功能确定好后,不要着急着手界面设计,功能模块的组合,交互设计这些过程。当你在进行这些过程之前,需要认真的去考虑你的应用是不是适合在小程序上开发。 小程序的火爆,注定有许多人盲目的进入这一领域,但不是所有的应用都适合小程序的推广模式,也不是所有应用都能吃得起小程序的运行效率的。所以在确定开发小程序之前,正确的认识的合适与不合适,避免让自己的成本白白的付出。 如何确定适不适合用小程序来做呢?我们可以让小程序作为一个互联网平台,把应用带入到互联网的每一个平台中去,进行横向的对比,去找出各自的优点与不足,当做出客观的判断后,应用在小程序上相比于其他平台有没有优势就很明显了。 还是以【GS比赛计分】举例。我所横向对比平台包括PC、原生APP、Web平台、轻应用、小程序。首先从满足功能来说,比赛计分需要很稳定的实时性,所以我将Web平台排除(因为复杂的比赛环境,不同的设备浏览器难免会有问题)。 然后将剩下的平台做分析。PC平台使用可能性太小(原因:比赛现场成本太高昂,租借笔记本可能都不是现实的);原生APP不适合(原因:评委评分前需要下载APP,比赛结束后没什么用了,需要把这个APP卸载。而且IOS和安卓两大平台增加了研发成本);轻应用有阻碍(原因:百度轻应用由于装机量小,覆盖人群不多。支付宝小程序没有社交关系链,无法有效的推送给需要的人。产业联盟轻应用,苹果用户怎么考虑?) [图片] 经过一系列的对比,很明显。对于【GS比赛计分】来讲,微信小程序是最好的应用平台。同时微信小程序仍然可以和不同的平台进行联系,所以可扩展性,功能延展性都是最佳的。 3.应用设计要满足即用即走的理念 当确定小程序是最理想的应用平台时,我们需要对小程序进行便利化的设计。这就需要抛弃一部分原生应用开发或Web开发的一些设计理念。追求“极致、简约”的风格设计。 在【GS比赛计分】的详细设计时,我考虑到,以个人账户的形式去下发比赛的流程是行不通的,既然服务于比赛,那么就以比赛为最基本的账户组成单元,明确一个比赛ID。而同一个比赛的不同角色,以不同的IK进行区别,而角色的设置包含在比赛设计中,由比赛创建者在创建比赛时自行创建。 那么小程序的使用场景就出来了。以比赛现场的告知或者微信聊天的分享,告知比赛参与角色其ID和IK,就可以让角色快速的进入绑定角色并使用程序。免去注册的一系列烦恼。 同时在微信用户面前,这个账户体系是平权的,任何用户在得知ID和IK的情况下都可以进入(账户绑定情况下不可以进入),一定程度上免除密码和忘记密码,注销账户这一系列的麻烦。 当完成比赛后,程序已经完成了自己所有的任务时,使用者可以直接退出程序,不需要注销比赛等操作。真正做到即走。 在使用过程中,要清楚的考虑用户的使用过程,从而做一些保险机制。微信小程序运行在微信上,微信是社交工具,就免不了用户会退出小程序甚至微信去做一些其他的事情。所以【GS比赛计分】在设计时要保证用户回来要用到自己想要的,在程序设计中有中间状态界面能够保证用户可以迅速进入使用状态。 4.对于很大的系统,要把最适合小程序部分拿出来,而不是全部 现在的【GS比赛计分】其实是一个大的生态系统,结合有线下的网络接口,展示接口,线上小程序,web平台。每一个部分都承载着自己独特的应用价值。 比如Web平台就承载比赛管理的任务,创建比赛,上传比赛文件,选手图片,设置选手(名称、介绍、手机号、图片、出场顺序),设置评分项(名称、权值、预置分数),设置评委(名称、权值,IK)。从实际的分析来看,比赛管理最适合在PC端进行,不管是文件还是图片,公认都是PC上传比较容易。 在最初设计的时候,我错误的把系统分成了多个小程序进行系统搭建,在实际使用过程中造成了重大的缺陷和用户流失。最大的表现是,我开发了【GS比赛创建】小程序,作为比赛的入口程序。从而造成比赛数量增速缓慢,大的使用场景无法突破,老用户意见上升等一系列问题。不得不注销了【GS比赛创建】小程序,并进行很大的架构调整。【GS比赛计分】暂停使用,造成大量的用户流失。 [图片] 所以,当设计多场景,前后联动性很强的应用时,需要将功能进行使用划分,每一个划分需要找最适合的平台。最适合小程序的部分,就做好与其他平台的无缝结合。 5.小程序的应用场景和机会 目前来看,小程序的应用场景主要包括支付场景,比如扫码支付、快消餐饮、移动购物、交通出行等等,工具类小程序能更碎片化、垂直化地满足细分的应用场景需求。 根据微信的最近更新变化来看,公众号和小程序的协同作用将越发明显,公众号的作用也将进一步放大,因此未来发展的机会可能在这几方面。 内容营销,小程序能通过更完善更精准的服务进而提高用户黏性。具体来看包括各大知识付费的小程序以及在教育风口上的小程序。当然,这些小程序也可同时开发APP(按微信对小程序的开发步骤来看)从而实现用户沉淀。 具有支付场景的各类商家。包括传统的已有一定客户群的商家提供更方便的服务或者实现线上线下联动以及新零售。 工具类小程序。包括共享经济领域,这类小程序即用即走,轻量化,便捷化。 天生具有社交属性的小程序。比如抽奖、互赠礼品、拼团减价、社交性的小游戏以及帮拿快递等。 小程序的开发经验 1. 微信小程序开发文档是最好的学习文档 很多同学喜欢看视频教程,或者买一本小程序开发书来学习。觉得这么学会加深理解更加容易上手,而官方文档干巴巴不好学。现在的微信小程序能力更新速度很快,当一个教程或书出来的时候,其实已经过时了。建议同学去微信公开课中学习微信小程序的入门教程,开发入门后,根据自己的开发需要,自行阅读官方文档来学习。 [图片] 2. 必须了解小程序的运行原理 微信小程序是运行在微信中的,所以运行速度并不能和原生媲美。但是在开发小程序的过程中,可以用良好的编程思路来追求程序的高效运行水平。但前提是,你需要对小程序的运行环境有所理解,需要知道在开发环境和真实环境(IOS和Android)下的运行差别。大部分的开发坑都是因为不同运行环境造成的。 官方的声明:微信小程序运行在三端:iOS、Android 和 用于调试的开发者工具;在 iOS 上,小程序的 javascript代码是运行在JavaScriptCore中;在Android上,小程序的javascript代码是通过 X5 内核来解析;在开发工具上,小程序的javascript代码是运行在nwjs(chrome内核)中。 微信小程序的运行环境类似 ReactNative ,而不是纯 Html5。两者最大的不同在于,ReactNative 的界面是由原生控件渲染出来的,而 Html5 的界面是由浏览器内核渲染出来的。两者在性能上有较大的差异,从而表现出来微信小程序要比h5页面好很多。 3.安利小程序云开发 云开发为开发者提供完整的云端支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。目前提供三大基础能力支持: 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码 数据库:一个既可在小程序前端操作,也能在云函数中读写的 JSON 数据库 存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理 云开发使得小程序开发门槛进一步降低,甚至偏前端的开发人员也可以独立开发小程序了。另外,云开发中云函数支持去请求第三方服务接口。所以云开发最大程度上提供了小程序所需基本API的构建,另外可以通过第三方服务请求能力扩展API能力。 [图片] 而且云开发的数据库和存储完全可以与云函数构建一个小型的后端平台。并发能力中等,适合大部分的小程序业务API的构建。 4. 养成良好的小程序代码习惯 微信小程序的代码文件分为4种,WXML、WXSS、JS、JSON。 JSON文件是小程序的配置文件,APP.JSON是小程序全局配置,另外每个页面也会有配置文件。建议页面中的JSON文件只填写页面需要配置的(比如页面标题),而不变化的不要写到里面(比如标题风格,背景颜色等等)。 JS主要是javascript语法,建议以模块的形式对通用的方法进行封装,尽可能的利用好代码,不要有大量重复的方法语句。如果多个页面需要,可以单独建立js文件,在需要的页面进行引入。 WXSS是样式文件,遵从css的编写规则,尽可能的少些样式表,多多利用已经写过的样式,如果多个页面需要,可以单独建立wxss或者写到app.wxss中 WXML是页面结构文件。如果多个页面中有相同的结构,可以单独封装为模块,而模块中的逻辑方面也尽可能遵从代码最大效益化。 总之,开发代码简洁,会使得编译后的小程序包很小,使加载速度更加快速。 5. 小程序填坑总结: 首先,微信开发者社区是好的小程序开发交流平台,从中可以获得大多数问题的解决方法: https://developers.weixin.qq.com/community/develop/question 另外,许多人常用搜索引擎来直接搜索问题的解决方法,一般大部分的错误或者问题都会在开发文档上写的很清楚,只不过很少有人去注意到,推荐几个小程序填坑的集合文章: https://blog.csdn.net/weixin_42448956/article/details/82414225 https://www.cnblogs.com/shaoting/p/6051261.html https://www.imooc.com/article/36148 https://www.cnblogs.com/wangking/p/6946438.html https://www.jianshu.com/p/4362e52f5c49 小程序的运营 小程序运营属于软件产品运营的一种类别。从产品生命期来看,小程序运营分别为研发期、种子期、成长期、成熟期、衰退期。另外由于小程序在运行模式和定位的不同,表现在推广形式上与平常的软件产品有很大的差别。接下来,我会用【GS比赛计分】【数据查询助手】小程序为例子佐证分享小程序的运营经验。 1. 小程序研发期,搞清楚产品的定位以及目标用户 当你不是运营小程序的产品策划者,你需要首先要搞清楚产品的定位以及目标用户。(这也是多数互联网公司将产品策划和运营作为一个岗位的原因)。在整个产品的研发期,需要跟进产品的每一个细微功能点,要明晰产品的使用用户;还要时刻去观察产品的领域有没有竞品的出现,用户习惯有什么改变;要时刻去衡量产品的竞争优势,为之后的发布运营做好准备。 【数据查询助手】是提供自定义信息查询服务的小程序。任何微信用户均可以上传自定义的数据(报表,成绩单等任何表格数据)建立查询。 [图片] 当在产品研发的一个月,我不断的探索小程序领域,APP领域,Web领域有没有相同的功能产品或服务。甚至将问卷系统(腾讯问卷、问卷星、问卷网)作为潜在的竞争对手。 另外我还制作了web版和原生APP的Demo去体验他们与小程序体验的不同。去确定微信小程序是很好的适应平台。从而为之后的运营做足了准备,提升了自己的信心。 2. 小程序种子期,要充分利用体验版过程 小程序体验版相当于其他应用平台的内测版。体验版可以更高层次的模仿真实的用户环境。在这一阶段更容易发现用户间连接要求高的应用缺陷。同时可以在安全的范围内去聆听用户的真是使用反馈。 【GS比赛计分】开展了长达1个月的体验版。邀请了20个核心用户去体验。由于应用需要与服务端建立实时连接。用户不同的设备,不同网络环境对程序的稳定性做出了很大的考验。在这一阶段修复了不少场景不同导致的错误或者效率低下问题。 另外一开始推出的小程序界面设计只遵从了功能设计,没有很好的考虑真实的使用场景。所以在这一阶段,我最大程度上听取体验用户的建议,对整个界面进行改版,使交互更加的亲近用户。 3. 小程序成长期,明晰获取用户的手段和推广手段,最大化的成长: 当小程序功能稳定后,到了成长阶段,用户使用是最核心的任务。获取用户的时候,必须先让对方了解自己的产品,建立认知,将产品介绍给用户,让用户进入小程序之后,想方设法让产品与用户产生交互,让用户不断体验产品,让活动始终覆盖用户,让用户对产品认可,要完成产品与用户的关系构建。 从现在许多小程序的运营手段来看,基本上都围绕着社交裂变和线下推广的方式来提升小程序的获客表现。而小程序由于较低的开发成本,较快的更新速度,以及较低的试错成本,使得多数很强势的小程序都采用功能矩阵发展模式,快速实现功能及迭代。 小程序获取用户的手段主要由下: 朋友圈分享(包括图片二维码、广告直接进入); 聊天好友推荐转发; 线下二维码(包含商家推广、广告推广); 微信搜索(一般由其他社交平台或者用户需求引起); 线上识别二维码(线上推广,文章推广,或其他社交平台的推广); 其他小程序的跳转(互相推荐) 公众号跳转(公众号运营推广) APP跳转(一般是APP延展的简洁功能推广,或者轻量级触达用户形式的推广) 小程序发掘社交推广的手段主要由下: 社交立减金,实现社交裂变; 社交比拼玩法,引导社交裂变; 互动加入分享按钮,提醒用户转发; 设计同伴环境,鼓励社群传播; 设计任务玩法,领取奖励; 设计福利,鼓励好友助力; 聚焦核心功能,促进口碑传播; 【数据查询助手】从产品定位上就自带社交裂变的元素,当查询创建者创建了一个查询后,他可以根据要分享的人群情况选择多种分享方法。如果是企业微信或者微信工作群,那么可以直接分享小程序到聊天窗口。其他的用户可以直接进入小程序进行查询。如果是线下的查询(推广会,现场发布等),可以通过小程序二维码的方式进入查询界面查询。对于其他社交平台来讲,可以用二维码来做分享,如果是常用用户(添加到我饿小程序或者桌面作为常用工具的用户),可以直接通过复制文字(含查询码)然后进入应用的方式快速开始查询。 同时每一个查询者,都可能是潜在的查询创建者和程序推广者。所以要在这一阶段不断的优化体验流程,尽可能做更多事情覆盖多场景的查询(比如微信搜一搜直接搜索查询码,直接查询),去吸引用户,留住用户。 由于小程序用完即走的理念,导致许多工具类小程序(不含深度融合线下和社交的)用户的留存普遍较低。既然工具类就是服务用户,那么就把小程序慢慢的做成一种用户习惯,从习惯的养成变为行业应用的转化。从转化中寻找切入点,进而挖掘可以创造价值的功能产品。 所以,做小程序不要过分贪图大规模的使用率,大批量的用户。他本身是一种服务理念的触达和养成,你需要在用户心里养成使用习惯,而不要上来糖衣炮弹把用户打蒙,甚至反感。这样小程序的生态就被搞乱了。 4. 小程序成熟期,稳定期最重要的就是小版本的迭代更新: 刚才讲到,小程序有较低的开发成本,较快的更新速度,以及较低的试错成本。所以在小程序成熟期需要根据用户数据不断的去更正调整功能,去保持较高的运营分数。在产品功能中,适时的进行用户付费转化。 付费转化一方面可以拉开用户层次,对小程序的用户是一种活水作用,提升用户的使用粘性,容易过滤最核心的用户,提供更加好的产品服务。如果在成长期很好的进行用户习惯的养成,这个过程会更加自然。反之,应用将会更快的进入衰退期。 付费转化的方式主要由几类: 电商类:主要靠活动、优惠刺激(现实抢购、秒杀、预约、限时满减、显示商品库存和抢购人数); 游戏类:游戏道具(向朋友求助、每日签到、社区活动等方式免费获得,但数量有限,且都是一些级别较低的道具)高价值道具付费、皮肤售卖、游戏币购买; 内容类:付费文章、阅读币购买、付费课程; 5.小程序衰退期:适时舍弃,进行新产品开始推出 小程序开发周期短,很多时候应用分析不够透彻,更多的是一些商业或推广试错。导致许多小程序还没有进入成长期就进入晚期了。这一类小程序直接舍弃就好,不需要什么转化了,这也不算小程序的衰退期(没有盛何来衰)。 对于经历过成熟期的产品主要有几个原因导致进入衰退期: 市场中有新的创新型产品,导致冲击衰退。 自身功能设计有缺陷,导致用户流失。 对用户了解不透彻,付费转化失败,用户迁移到同类产品。 产品质量下降,不能适应用户行为的变化。 由于运营组织的原因,产品运营出现危机。 在这一阶段,运营已经无能为力。那么就进行产品的复盘,积极的去投入到新产品或者新领域的探究上面去。小程序的开发成本较低,所以可以有很多机会去重新塑造一个好的产品。 6.总结,谈谈自己的想法: 现在多种多样的互联网产品不断产生,产品竞争异常激烈。互联网产品的运营手段也是推陈出新,花样繁多。在这里我不给大家分享一些运营花样,因为每个小程序都是独特的,应该有自己独特的运营手段,具体是什么,希望产品者和运营者本身用热爱小程序的心去发现和实践。 永远保持对产品的尊重,对用户的尊敬是每个产品人最重要的事情。如果对应用足够热爱,你会厌恶他被污染,他被别人嫌弃;你会尽自己的可能让他变得更加本真,你会合理的去运营突破,去帮助产品走向更高的位置。 如果你没有爱你的产品,再出色的产品也只是昙花一现而已,并不能给你带来任何长远的意义。 这是最好的小程序运营指南:https://developers.weixin.qq.com/miniprogram/product。 [图片]
2019-06-23 - 小程序与APP和公众号哪个好?
近期有好多客户咨询我做APP,我就很恼火,你预算一点点,还想盖高楼,真是难啊!俗话说,巧妇难为无米之炊。你给那点钱,我也很难帮到客户,就重点给他推荐小程序的好处,客户最终也愿意听我的建议,低成本开发小程序,前期试错迭代,探索模式清晰后再做APP开发。APP的历史很久了,上线近两年的微信小程序功能与APP相近,而公众号同样基于微信,性能又与小程序相近,小程序和APP还有公众号到底有什么区别呢?下面我简单整理了自己总结的东西,和大神一起来讨论一下。 一、小程序和公众号的区别 在电商行业发展或者准备进入电商行业中发展的企业商家,在微信电商不断发展的现在,大多会选择进入这个市场去发展。特别是这一年多来,微信小程序的火热,让更多商家看到它的商机与市场,纷纷投入到小程序商城的开发当中。不过,即使开发完毕,还需要考虑更为重要的问题,就是如何去推广这个平台来曝光引流、转化,而保障它的发展的问题。 二、小程序和APP的区别 小程序虽然被喻为原生APP,但是开发小程序与开发APP所需要的技术不是在一个层次上的。小程序的开发,是基于微信官方提供开发指引与工具基础上去开发,相当有一个大致的框架作为基础。而APP开发,所有的框架内容都要从基础开始搭建,技术难度与开发周期都在小程序之上,这是区别之一。 区别之二,在于它们的功能实现与技术维护上。小程序虽然能实现企业商家的大部分功能需求,但是依旧有一些功能是它无法实现而APP开发可以实现的。比如一些物联网项目的需求功能,就只能在APP上才能真正实现软硬件对接。另外,在开发之后的后期维护上,APP比小程序维护所需技术更深层次,它需要针对不同类型的系统做兼容性开发、维护以及系统升级,以保障它能顺畅运行。而小程序的维护,有微信官方的支持,维护成本更低、周期更短,且流程更加简单一些。 三、如何保障小程序的发展问题? 1.线下场景投放参数码推广 小程序商城虽然作为一个线上的电商平台,但不一定它的推广就要局限于线上,可以通过小程序码、参数二维码的投放,将线下场景也用于推广引流,让线下的流量也能通过扫码而引到线上。 2.与公众号关联推广 微信小程序可以与公众号相互关联这一点,是明显让这两个平台得以互补的设定。小程序难以将客户真实留存到平台中,而公众号的粉丝体系恰好可以弥补这一功能,实现粉丝的沉淀。而开发小程序商城之后,与公众号进行关联,可通过图文、自定义菜单等,赋予小程序入口,将公号的粉丝引到小程序中,同时对商城进行推广与曝光。这种在公众号原有基础上推广小程序商城的做法,也是比较有效的。 3.利用砍价、拼团等工具去实现裂变 像拼多多这类型的商城,除了基本的商品销售之外,其实更让它名声大燥的,是它的拼团与砍价的玩法。拼团更省钱,砍价更是可以0元购物,这些对于消费者来说都是直接的利益体现,所以他们更愿意去主动转发分享这个平台,而在他们分享的过程中,商城也就得到推广。所以,微信小程序商城可以借助这种模式,用砍价、拼团等工具,去实现裂变传播。 4.利用软文做内容营销 内容营销是一种比较常见的推广方式,小程序商城可以利用这种手段,编辑软文进行营销推广并刺激转化,将主推的产品、主推的服务以软广的形式去介绍出去,在实现的曝光的同时,也用价值抓住用户眼球,刺激用户实现消费。 APP、公众号和小程序上线的时候不同,功能也有所不一样,发展的方向也不同,但不可否认的是对我们的生活有着很大的影响,为我们提供了便利。至于企业要怎么选择,要根据自身的发展需求,从基础出发,契合企业的发展需求。 个人觉得还是小程序更有前途,它是一个轻APP,基于微信小程序快速部署,也有成熟的第三方系统,可以直接拿来用,腾讯官方也会不断丰富小程序的基础功能,对我们来说是一大好处。利用第三方的成熟系统开发小程序也是不错的选择,我们自己研发的小程序商城系统现在开源出来,希望能够帮助更多开发者快速开发,而不被客户催促加班着急上火! **源码下载地址:http://github.crmeb.net/u/demo
2019-12-23 - 小打卡|如何组件化拆分一个200+页面的小程序
大家好,我是小打卡的前端唐驰。刚才金轩正同学分享了基于原生小程序底层架构,在此基础上我为大家分享下如何拆分一个200+页面的小程序,主要通过以下几点来聊一聊小打卡在组件化路上的一些实践 1.背景 2.组件与方案 3.组件间通讯 4.基于组件我们做了哪些事 [图片] [图片] 1. 其实一开始小打卡是没有引入组件化的,因为微信最开始是不支持组件化的。当时js代码已经4k+行了,各种功能代码,有用的没有用的,不知道干什么的代码就躺在那里,一动不动。举个例子,一个头像点击跳转的逻辑搜索了下,遍布在各个页面。修改起来可想而知的胆战心惊。另一个原因就是当时由于业务功能直线上升,很快我们就遇到了代码包超包了。在微信还没有实现分包之前,我们就只能一个一个页面的去review剔除代码,效率极低。这也是促成我们决定寻求出路的原因之一。可是删代码删功能是不能解决问题,期间我们也考虑过h5的方式,跑了demo之后却发现h5方式的多次渲染, 与加载首页白屏,尽管有各种服务端渲染方案,但是我们一致觉得为了用户体验,放弃了。 [图片] 2. 对于小打卡来说,我们不能再任由项目裸奔了,需要一种开发方式来进行约束,主要是有几个诉求: 在之前的项目上,为了方便。功能与功能之间的耦合程度极其的高,各种为了使用方便而随意修改某一个方法。 1.降低页面上各个功能点的耦合程度 我们不希望同一个功能点同样的代码在页面肆意copy,这样带来了极高的维护成本。以至后面无法维护。并且功能的复用不希望是copy,前端与后端不同的是不仅是单单的逻辑复用、更有布局、样式等。 2.提供代码的可复用性、可维护性 对于一个程序员来说,如果你打开一个代码文件。映入眼帘的是密密麻麻的代码,行数达到好几千K行,我相信大家的第一反应是抗拒的,更别说去修改代码,天知道会改出什么问题。 3.降低单一文件的复杂度 4.如果是公共功能的化我们还希望它能够有自己的作用域,保持自己的独立性。 [图片] 3. 根据以上几点,我们用一个页面举例,如何去拆分一个页面,首先我们需要有以下几点认识: 决定一个页面如何组件化的前提是该页面的功能是否是有全局都需要的功能模块 功能模块是否需要与页面其他模块强耦合 单个功能模块逻辑是否过于复杂(占用代码空间过大)——>单纯是为了页面代码的可读性。 不是全拆成组件就是最好的,不能为了组件而组件化 [图片] 4. 说了这么多,其实我们应该首先应该了解下,组件的特性? 专一性(一个组件只干一件事情,或者某一类事情。)功能的高度内聚 比如说右侧的feed集上的头像、它是一个组件、就负责显示头像跟跳转,其他的事它都不参与 可配置(能够适应通过设置属性值的方式来输出不同的东西)输入影响部分输出 然后我们同时可以设置头像组件上的size属性来设置头像在不同页面下的大小样式 生命周期(组件可以在自身或者说所在页面的生命周期内可以做不同的事情)比如可以在组件生成的时候进行数的初始化、属性值的类型校验、组件销毁时并同时销毁定时器等其他任务 事件传递 (既然要让组件与页面保持独立性,那么组件与页面的通讯交互就得需要一个标准) 右侧的feed组件其实是一个组件集合、我们通过组合不同的组件然后就形成了feed组件。就跟搭积木一样、只需要引入组件就行了。特别方便。 [图片] 5. 说到组件,那么小程序早期的不支持自定义组件开发这就很让人头疼、同样的feed组件我们经历了几乎三个版本的大改动、从最开始的直接写在页面里,后台使用template方式、再到后来的自定义组件方式。所以我们的演进步骤就成了page->template->component, 这儿列了一个表格对比了下几种组件化方式的对比。 可以看到,include的方式其实是最鸡肋的,include的方式其实实际意义上我理解成更多的是代码的切割,并且还不能将(template、wxs)分出去、所以这种方式我们直接pass掉了, 而template的方式其实是我们曾经主力使用的方式、到现在我们也还在使用、相对于include来说,template有了独立的作用域、虽然css、跟js还是与页面共享的。但是已经可以做一些比较简单的事情了。 对于component来说,完完全全的组件,满足了组件的所有要求。 [图片] 6. 先说说template的方式吧,举个列子,这个是我们的使用template构建的头像组件。跟写页面的方式很像、同样是js、wxss、wxml组成。用名称来命名。但是由于微信当时没有很方的方式去引用这些文件,或者说没有一种方法可以打包供我们很方便的使用。但是比起之前直接copy代码的方式、这样通过引用的方式使用其实感觉已经好了很多了。 [图片] [图片] 7. 具体的使用方式我画了张图,对应组件内文件与页面文件的对应方式、这里对于js的引用其实我们是做了一些小动作, 我们在调用Page方法前做了一次page方法与组件方法的check,因为在page代码里我们不能保证所有的方法名不会跟组件内的方法名不会冲突,所以我们做了这个一个检查、 然后mix函数还做了另一个事情就是将page方法与组件方法合并。然后对于mix函数其实我们还可以做很多事情、、比如规范生命周期回调函数放在一个对象内,然后我们自己定义的方法放在另一个对象里,就跟vue一样。 But,在经历了一段template组件化的时间后,我们又觉得这个方式还是有点烂,为什么呢?在使用时仍然不能避免引入众多的文件、虽然我们对js文件做了处理,但是wxss的样式仍然会被污染、js与page仍然共享作用域。并不能成为一个真正的标准组件。好在后来,微信上了自定义组件的功能,接下来聊聊这个标准的微信自定义组件吧。 8. 微信提供了自定义组件的功能后我们也第一时间跟进了,相对于template这种方式来说,现在是真正的独立于页面存在。使用也比之前更为方便与简洁,右图是我们对component的一个项目目录划分。我们将component划分为了公共组件与页面组件、为什么会有页面组件, 1.是为了降低页面代码的复杂度 2.为了好看。 公共组件就不说了,一定是最基础、最通用的组件。 [图片] 9. 转向component方式后有一个问题逐渐便凸显出来了,由于组件的独立作用域,组件间的通讯就成了一个问题,接下来聊一聊组件的事件传递。微信最开始的时候提供了一种triggerEvent的方式,可是这样的方式似乎并不能满足我们某些场景下的需求。后来又提供了page下selectComponent方法来直接操作组件内部的属性与方法。然后还有就是基于我们自己的事件广播机制。这几种方式构成了小打卡现目前最主要的组件与page、组件与组件间的数据交互方式 [图片] 10. 先来说说triggerEvent模式,微信在自定义组件上可以自定义监听函数。我们在组件内将需要向外抛出的事件统一通过this.triggerEvent(‘invoke’,{handler:’fun’,data:{}})这个方法来执行。其中invoke对应了我们绑定在组件标签上的监听函数。而将需要外部执行的方法与数据通过数据的方式传给监听函数。而在page上面我通过统一的监听回调函数去自动执行需要执行的方法、这里的trigger与event都不要我们去手写在组件与page创建的时候底层就已经帮我们预置了,我们只需要关注业务开发就行。这是对于一部分需要page与组件交互的模式。而对于我们想直接操作组件方法而不需要反馈的模式就得使用selectComponent的模式 [图片] 11. 一个简单的列子:全局的toast组件。在需要弹出toast的时候我们想直接调用就行、不用在通过传值给组件、然后由组件来执行显示或隐藏。这类组件我们在组件目录里新增了一个lib的文件。在page里只需要引入这个lib文件然后就可以直接调用toast组件。lib主要是对this.selectCompent与执行逻辑的一个封装。 [图片] 12. 事件发布订阅模式:基于底层的eventBus。简化后我们用在了组件与组件之间的通讯上、特点是简单。 [图片] 13. 解决了组件间的通讯问题,可是对于公共组件的引用仍然让我们觉得麻烦与不畅快、所以我们构建了全局通用模版、它是干什么的呢。它提供给了一些基础的全局组件、比如自定义导航头、toast、loading等等。小打卡所有的页面都通过slot的方式插入到这个模版组件x-page下面。这样就解决了我们需要在每个页面引入公共组件的问题。另一个问题使用自定义导航栏的时定位起点会有状态栏下移动到屏幕左上方。会造成布局的错误。通过x-page可以很好解决这个问题而不用重新布局。并且通信问题也不用担心,都是由x-page组件作为中台来对内对外进行分发与执行。 [图片] [图片] [图片] 14. 通过以上小打卡的开发模式就基本形成。要做的事情还有很多,更多组件的玩儿法,对于现在或者将来我们正在做的。 是构建小打卡的组件与基础sdk的仓库。 拆分组件开发与业务开发。 通过npm包管理的方式来应对越来越多的小程序平台的开发。 或者通过形成小程序插件的方式供其他小伙伴使用。 [图片] [图片] 以上就是我今天分享的内容。谢谢。
2019-04-26 - 有赞前端质量保障体系
前言 最近一年多一直在做前端的一些测试,从小程序到店铺装修,基本都是纯前端的工作,刚开始从后端测试转为前端测试的时候,对前端东西茫然无感,而且团队内没有人做过纯前端的测试工作,只能一边踩坑一边总结经验,然后将容易出现问题的点形成体系、不断总结摸索,最终形成了目前的一套前端测试解决方案。在此,将有赞的前端质量保障体系进行总结,希望和大家一起交流。 先来全局看下有赞前端的技术架构和针对每个不同的层次,主要做了哪些保障质量的事情: [图片] [图片] 有赞的 Node 技术架构分为业务层、基础框架层、通用组件和基础服务层,我们日常比较关注的是基础框架、通用组件和业务层代码。Node 业务层做了两件事情,一是提供页面渲染的 client 层,用于和 C 端用户交互,包括样式、行为 js 等;二是提供数据服务的 server 层,用于组装后台提供的各种接口,完成面向 C 端的接口封装。 对于每个不同的层,我们都做了一些事情来保障质量,包括: 针对整个业务层的 UI 自动化、核心接口|页面拨测; 针对 client 层的 sentry 报警; 针对 server 层的接口测试、业务报警; 针对基础框架和通用组件的单元测试; 针对通用组件变更的版本变更报警; 针对线上发布的流程规范、用例维护等。 下面就来分别讲一下这几个维度的质量保障工作。 一、UI 自动化 很多人会认为,UI 自动化维护成本高、性价比低,但是为什么在有赞的前端质量保证体系中放在了最前面呢? 前端重用户交互,单纯的接口测试、单元测试不能真实反映用户的操作路径,并且从以往的经验中总结得出,因为各种不可控因素导致的发布 A 功能而 B 功能无法使用,特别是核心简单场景的不可用时有出现,所以每次发布一个应用前,都会将此应用提供的核心功能执行一遍,那随着业务的不断积累,需要回归的测试场景也越来越多,导致回归的工作量巨大。为了降低人力成本,我们亟需通过自动化手段释放劳动力,所以将核心流程回归的 UI 自动化提到了最核心地位。 当然,UI 自动化的最大痛点确实是维护成本,为降低维护成本,我们将页面分为组件维度、页面维度,并提供统一的包来处理公用组件、特殊页面的通用逻辑,封装通用方法等,例如初始化浏览器信息、环境选择、登录、多网点切换、点击、输入、获取元素内容等等,业务回归用例只需要关注自己的用例操作步骤即可。 1、框架选择 – puppeteer[1],它是由 Chrome 维护的 Node 库,基于 DevTools 协议来驱动 chrome 或者 chromium 浏览器运行,支持 headless 和 non-headless 两种方式。官网提供了非常丰富的文档,简单易学。 UI 自动化框架有很多种,包括 selenium、phantom;对比后发现 puppeteer 比较轻量,只需要增加一个 npm 包即可使用;它是基于事件驱动的方式,比 selenium 的等待轮询更稳当、性能更佳;另外,它是 chrome 原生支持,能提供所有 chrome 支持的 api,同时我们的业务场景只需要覆盖 chrome,所以它是最好的选择。 – mocha[2] + mochawesome[3],mocha 是比较主流的测试框架,支持 beforeEach、before、afterEach、after 等钩子函数,assert 断言,测试套件,用例编排等。 mochawesome 是 mocha 测试框架的第三方插件,支持生成漂亮的 html/css 报告。 js 测试框架同样有很多可以选择,mocha、ava、Jtest 等等,选择 mocha 是因为它更灵活,很多配置可以结合第三方库,比如 report 就是结合了 mochawesome 来生成好看的 html 报告;断言可以用 powser-assert 替代。 2、脚本编写 封装基础库 封装 pc 端、h5 端浏览器的初始化过程 封装 pc 端、h5 端登录统一处理 封装页面模型和组件模型 封装上传组件、日期组件、select 组件等的统一操作方法 封装 input、click、hover、tap、scrollTo、hover、isElementShow、isElementExist、getElementVariable 等方法 提供根据 “html 标签>>页面文字” 形式获取页面元素及操作方法的统一支持 封装 baseTest,增加用例开始、结束后的统一操作 封装 assert,增加断言日志记录 业务用例 安装基础库 编排业务用例 3、执行逻辑 分环境执行 增加预上线环境代码变更触发、线上环境自动执行 监控源码变更 增加 gitlab webhook,监控开发源码合并 master 时自动在预上线环境执行 增加 gitlab webhook,监控测试用例变更时自动在生产环境执行 每日定时执行 增加 crontab,每日定时执行线上环境 [图片] [图片] [图片] [图片] 二、接口测试 接口测试主要针对于 Node 的 server 层,根据我们的开发规范,Node 不做复杂的业务逻辑,但是需要将服务化应用提供 dubbo 接口进行一次转换,或将多个 dubbo 接口组合起来,提供一个可供 h5/小程序渲染数据的 http 接口,转化过程就带来了各种数据的获取、组合、转换,形成了新的端到端接口。这个时候单单靠服务化接口的自动化已经不能保障对上层接口的全覆盖,所以我们针对 Node 接口也进行自动化测试。为了使用测试内部统一的测试框架,我们通过 java 去请求 Node 提供的 http 接口,那么当用例都写好之后,该如何评判接口测试的质量?是否完全覆盖了全部业务逻辑呢?此时就需要一个行之有效的方法来获取到测试的覆盖情况,以检查有哪些场景是接口测试中未覆盖的,做到更好的查漏补缺。 – istanbul[4] 是业界比较易用的 js 覆盖率工具,它利用模块加载的钩子计算语句、行、方法和分支覆盖率,以便在执行测试用例时透明的增加覆盖率。它支持所有类型的 js 覆盖率,包括单元测试、服务端功能测试以及浏览器测试。 但是,我们的接口用例写在 Java 代码中,通过 Http 请求的方式到达 Node 服务器,非 js 单测,也非浏览器功能测试,如何才能获取到 Node 接口的覆盖率呢? 解决办法是增加 cover 参数:–handle-sigint,通过增加 --handle-sigint 参数启动服务,当服务接收到一个 SIGINT 信号(linux 中 SIGINT 关联了 Ctrl+C),会通知 istanbul 生成覆盖率。这个命令非常适合我们,并且因此形成了我们接口覆盖率的一个模型: [代码]1. istanbule --handle-sigint 启动服务 2. 执行测试用例 3. 发送 SIGINT结束istanbule,得到覆盖率 [代码] 最终,解决了我们的 Node 接口覆盖率问题,并通过 jenkins 持续集成来自动构建 [图片] [图片] [图片] 当然,在获取覆盖率的时候有需求文件是不需要统计的,可以通过在根路径下增加 .istanbule.yml 文件的方式,来排除或者指定需要统计覆盖率的文件 [代码]verbose: false instrumentation: root: . extensions: - .js default-excludes: true excludes:['**/common/**','**/app/constants/**','**/lib/**'] embed-source: false variable: __coverage__ compact: true preserve-comments: false complete-copy: false save-baseline: false baseline-file: ./coverage/coverage-baseline.json include-all-sources: false include-pid: false es-modules: false reporting: print: summary reports: - lcov dir: ./coverage watermarks: statements: [50, 80] lines: [50, 80] functions: [50, 80] branches: [50, 80] report-config: clover: {file: clover.xml} cobertura: {file: cobertura-coverage.xml} json: {file: coverage-final.json} json-summary: {file: coverage-summary.json} lcovonly: {file: lcov.info} teamcity: {file: null, blockName: Code Coverage Summary} text: {file: null, maxCols: 0} text-lcov: {file: lcov.info} text-summary: {file: null} hooks: hook-run-in-context: false post-require-hook: null handle-sigint: false check: global: statements: 0 lines: 0 branches: 0 functions: 0 excludes: [] each: statements: 0 lines: 0 branches: 0 functions: 0 excludes: [] [代码] 三、单元测试 单元测试在测试分层中处于金字塔最底层的位置,单元测试做的比较到位的情况下,能过滤掉大部分的问题,并且提早发现 bug,也可以降低 bug 成本。推行一段时间的单测后发现,在有赞的 Node 框架中,业务层的 server 端只做接口组装,client 端面向浏览器,都不太适合做单元测试,所以我们只针对基础框架和通用组件进行单测,保障基础服务可以通过单测排除大部分的问题。比如基础框架中店铺通用信息服务,单测检查店铺信息获取;比如页面级商品组件,单测检查商品组件渲染的 html 是否和原来一致。 单测方案试行了两个框架: Jest[5] ava[6] 比较推荐的是 Jest 方案,它支持 Matchers 方式断言;支持 Snapshot Testing,可测试组件类代码渲染的 html 是否正确;支持多种 mock,包括 mock 方法实现、mock 定时器、mock 依赖的 module 等;支持 istanbule,可以方便的获取覆盖率。 总之,前端的单测方案也越来越成熟,需要前端开发人员更加关注 js 单测,将 bug 扼杀在摇篮中。 四、基础库变更报警 上面我们已经对基础服务和基础组件进行了单元测试,但是单测也不能完全保证基础库的变更完全没有问题,伴随着业务层引入新版本的基础库,bug 会进一步带入到业务层,最终影响 C 端用户的正常使用。那如何保障每次业务层引入新版本的基础库之后能做到全面的回归?如何让业务测试同学对基础库变更更加敏感呢?针对这种情况,我们着手做了一个基础库版本变更的小工具。实现思路如下: [代码]1. 对比一次 master 代码的提交或 merge 请求,判断 package.json 中是否有特定基础库版本变更 2. 将对应基础库的前后两个版本的代码对比发送到测试负责人 3. 根据 changelog 判断此次回归的用例范围 4. 增加 gitlab webhook,只有合并到合并发布分支或者 master 分支的代码才触发检查 [代码] 这个小工具的引入能及时通知测试人员针对什么需求改动了基础组件,以及这次基础组件的升级主要影响了哪些方面,这样能避免相对黑盒的测试。 第一版实现了最简功能,后续再深挖需求,可以做到前端代码变更的精准测试。 [图片] 五、sentry 报警 在刚接触前端测试的时候,js 的报错没有任何追踪,对于排查问题和定位问题有很大困扰。因此我们着手引入了 sentry 报警监控,用于监控线上环境 js 的运行情况。 – sentry[7] 是一款开源的错误追踪工具,它可以帮助开发者实时监控和修复崩溃。 开始我们接入的方式比较简单粗暴,直接全局接入,带来的问题是报警信息非常庞大,全局上报后 info、warn 信息都会打出来。 更改后,使用 sentry 的姿势是: sentry 的全局信息上报,并进行筛选 错误类型: TypeError 或者 ReferenceError 错误出现用户 > 1k 错误出现在 js 文件中 出现错误的店铺 > 2家 增加核心业务异常流程的主动上报 最终将筛选后的错误信息通过邮件的形式发送给告警接收人,在固定的时间集中修复。 [图片] [图片] 六、业务报警 除了 sentry 监控报警,Node 接口层的业务报警同样是必不可少的一部分,它能及时发现 Node 提供的接口中存在的业务异常。这部分是开发和运维同学做的,包括在 Node 框架底层接入日志系统;在业务层正确的上报错误级别、错误内容、错误堆栈信息;在日志系统增加合理的告警策略,超过阈值之后短信、电话告警,以便于及时发现问题、排查问题。 业务告警是最能快速反应生产环境问题的一环,如果某次发布之后发生告警,我们第一时间选择回滚,以保证线上的稳定性。 七、约定规范 除了上述的一些测试和告警手段之外,我们也做了一些流程规范、用例维护等基础建设,包括: 发布规范 多个日常分支合并发布 限制发布时间 规范发布流程 整理自测核心检查要点 基线用例库 不同业务 P0 核心用例定期更新 项目用例定期更新到业务回归用例库 线上问题场景及时更新到回归用例库 目前有赞的前端测试套路基本就是这样,当然有些平时的努力没有完全展开,例如接口测试中增加返回值结构体对比;增加线上接口或页面的拨测[8];给开发进行自测用例设计培训等等。也还有很多新功能探索中,如接入流量对比引擎,将线上流量导到预上线环境,在代码上线前进行对比测试;增加UI自动化的截图对比;探索小程序的UI自动化等等。 参考链接 [1] https://github.com/GoogleChrome/puppeteer [2] https://www.npmjs.com/package/mocha [3] https://www.npmjs.com/package/mochawesome [4] https://github.com/gotwarlost/istanbul [5] https://github.com/facebook/jest [6] https://github.com/avajs/ava [7] https://docs.sentry.io [8] https://tech.youzan.com/youzan-online-active-testing/
2019-04-25 - 【周刊-2】三年大厂面试官-前端面试题(偏难)
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把阿里和腾讯常问的面试题与答案汇总在我的Github中。希望对大家有所帮助,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 如果你在阿里和腾讯面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把答案和考点都列出来,公布在下一期的面试周刊里。 面试题精选 大家如果去阿里和腾讯面试过,就会发现,在网上刷了很多的前端面试题,但是去大厂面试的时候还是一头雾水,那是因为那些在网上一搜就能搜出来的题,大厂的面试官基本看不上,他们都会问一些开放题,在回答开放题的过程中,就能摸清你知识技能的广度和深度,所以本期会加入几道我在面试候选人常用的开放题,供大家学习和思考。 我把下面每道题的难度高低,和对标了阿里和腾讯的多少职级,都写上去了,大家可以参考一下自己是什么职级。 第 1 题:如何劫持https的请求,提供思路 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 很多人在google上搜索“前端面试 + https详解”,把答案倒背如流,但是问到如何劫持https请求的时候就一脸懵逼,是因为还是停留在https理论性阶段。 想告诉大家的是,就算是https,也不是绝对的安全,以下提供一个本地劫持https请求的简单思路。 模拟中间人攻击,以百度为例 先用OpenSSL查看下证书,直接调用openssl库识别目标服务器支持的SSL/TLS cipher suite [代码] openssl s_client -connect www.baidu.com:443 [代码] 用sslcan识别ssl配置错误,过期协议,过时cipher suite和hash算法 [代码] sslscan -tlsall www.baidu.com:443 [代码] 分析证书详细数据 [代码] sslscan -show-certificate --no-ciphersuites www.baidu.com:443 [代码] 生成一个证书 [代码] openssl req -new -x509 -days 1096 -key ca.key -out ca.crt [代码] 开启路由功能 [代码] sysctl -w net.ipv4.ip_forward=1 [代码] 写转发规则,将80、443端口进行转发给8080和8443端口 [代码] iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443 [代码] 最后使用arpspoof进行arp欺骗 如果你有更好的想法或疑问,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/11 第 2 题:前端如何进行seo优化 难度:阿里p5、腾讯t21 合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页 重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取 重要内容不要用js输出:爬虫不会执行js获取内容 少用iframe:搜索引擎不会抓取iframe中的内容 非装饰性图片必须加alt 提高网站速度:网站速度是搜索引擎排序的一个重要指标 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/12 第 3 题:前后端分离的项目如何seo 难度:阿里p6 ~ p6+、腾讯t22 ~ t23 使用prerender。但是回答prerender,面试官肯定会问你,如果不用prerender,让你直接去实现,好的,请看下面的第二个答案。 先去 https://www.baidu.com/robots.txt 找出常见的爬虫,然后在nginx上判断来访问页面用户的User-Agent是否是爬虫,如果是爬虫,就用nginx方向代理到我们自己用nodejs + puppeteer实现的爬虫服务器上,然后用你的爬虫服务器爬自己的前后端分离的前端项目页面,增加扒页面的接收延时,保证异步渲染的接口数据返回,最后得到了页面的数据,返还给来访问的爬虫即可。 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/13 第 4 题:简单实现async/await中的async函数 难度:阿里p6 ~ p6+、腾讯t22 ~ t23 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里 [代码]function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch (e) { return reject(e); } if (next.done) { return resolve(next.value); } Promise.resolve(next.value).then( function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); } ); } step(function() { return gen.next(undefined); }); }); } [代码] 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/14 第 5 题:1000-div问题 难度:阿里p5 ~ p6、腾讯t21 ~ t22 一次性插入1000个div,如何优化插入的性能 使用Fragment [代码] var fragment = document.createDocumentFragment(); fragment.appendChild(elem); [代码] 向1000个并排的div元素中,插入一个平级的div元素,如何优化插入的性能 先display:none 然后插入 再display:block 赋予key,然后使用virtual-dom,先render,然后diff,最后patch 脱离文档流,用GPU去渲染,开启硬件加速 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/15 第 6 题:(开放题)2万小球问题:在浏览器端,用js存储2万个小球的信息,包含小球的大小,位置,颜色等,如何做到对这2万条小球信息进行最优检索和存储 难度:阿里p7、腾讯t31 你面试阿里和腾讯,能否上p7和t31,就看你对开放题能答有多深和多广。 这题目考察你如何在浏览器端中进行大数据的存储优化和检索优化。 如果你仅仅只是答用数组对象存储了2万个小球信息,然后用for循环去遍历进行索引,那是远远不够的。 这题要往深一点走,用特殊的数据结构和算法进行存储和索引。 然后进行存储和速度的一个权衡和对比,最终给出你认为的最优解。 我提供几个能触及阿里p7和腾讯t31级别的思路: 用ArrayBuffer实现极致存储 哈夫曼编码 + 字典查询树实现更优索引 用bit-map实现大数据筛查 用hash索引实现简单快捷的检索 用IndexedDB实现动态存储扩充浏览器端虚拟容量 用iframe的漏洞实现浏览器端localStorage无限存储,实现2千万小球信息存储 这种开放题答案不唯一,也不会要你现场手敲代码去实现,但是思路一定要行得通,并且是能打动面试官的思路,如果大家有更好的idea,欢迎大家到我的github里补充:https://github.com/airuikun/Weekly-FE-Interview/issues/16 第 7 题:(开放题)接上一题如何尽可能流畅的实现这2万小球在浏览器中,以直线运动的动效显示出来 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 这题考察对大数据的动画显示优化,当然方法有很多种。 但是你有没有用到浏览器的高级api? 你还有没有用到浏览器的专门针对动画的引擎? 或者你对3D的实践和优化,都可以给面试官展示出来。 提供几个思路: 使用GPU硬件加速 使用webGL 使用assembly辅助计算,然后在浏览器端控制动画帧频 用web worker实现javascript多线程,分块处理小球 用单链表树算法和携程机制,实现任务动态分割和任务暂停、恢复、回滚,动态渲染和处理小球 如果大家有更好的idea,欢迎大家到我的github里补充:https://github.com/airuikun/Weekly-FE-Interview/issues/17 第 8 题:(开放题)100亿排序问题:内存不足,一次只允许你装载和操作1亿条数据,如何对100亿条数据进行排序。 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 这题是考察算法和实际问题结合的一个问题 众所周知,腾讯玩的是社交,用户量极大。很多场景的数据量都是百亿甚至千亿级别。 那么如何对这些数据进行高效的操作呢,可以通过这题考察出来。 以前老听说很多人问,前端学算法没有用,考算法都是垃圾,面不出候选人的能力 其实。。。老哥实话告诉你,当你在做前端需要用到crc32、并查集、字典树、哈夫曼编码、LZ77之类东西的时候 已经是涉及到框架实现和极致优化层面了 那时你就已经到了另外一个前端高阶境界了 所以不要抵触算法,可能只是我们目前的眼界和能力,还没触及到那个层级 我前面已经公布了两道开放题的答案了,相信大家已经有所参悟。我觉得在思考开放题的过程中,会有很多意想不到的成长,所以我建议这道题大家可以尝试自己思考一下。本题答案会在周五公布到我的github上。 对应的github地址为:https://github.com/airuikun/Weekly-FE-Interview/issues/18 第 9 题:(开放题)a.b.c.d和a[‘b’][‘c’][‘d’],哪个性能更高 难度:阿里p7 ~ p7+、腾讯t31 ~ t32 别看这题,题目上每个字都能看懂,但是里面涉及到的知识,暗藏杀鸡 这题要往深处走,会涉及ast抽象语法树、编译原理、v8内核对原生js实现问题 直接对标阿里p7 ~ p7+和腾讯t31 ~ t32职级,我觉得这个题是这篇文章里最难的一道题,所以我放在了开放题中的最后一题 大家多多思考,本题答案会在周五公布到我的github上 对应的github地址为:https://github.com/airuikun/Weekly-FE-Interview/issues/19 第 10 题:git时光机问题 难度:阿里p5 ~ p6+、腾讯t21 ~ t23 现在大厂,已经全部都是用git了,基本没人使用svn了 很多面试候选人对git只会commit、pull、push 但是有没有使用过reflog、cherry-pick等等,这些都很能体现出来你对代码管理的灵活程度和代码质量管理。 针对git时光机经典问题,我专门写了一个文章,轻松搞笑通俗易懂,大家可以看一下,放松放松,同时也能学到对git的时光机操作《git时光机》 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。 交流 欢迎关注我的微信公众号,微信扫下面二维码或搜索“前端屌丝”,讲述了一个前端屌丝逆袭的心路历程,共勉。 [图片]
2019-04-17 - Vue 服务端渲染实践 ——Web应用首屏耗时最优化方案
随着各大前端框架的诞生和演变,[代码]SPA[代码]开始流行,单页面应用的优势在于可以不重新加载整个页面的情况下,通过[代码]ajax[代码]和服务器通信,实现整个[代码]Web[代码]应用拒不更新,带来了极致的用户体验。然而,对于需要[代码]SEO[代码]、追求极致的首屏性能的应用,前端渲染的[代码]SPA[代码]是糟糕的。好在[代码]Vue 2.0[代码]后是支持服务端渲染的,零零散散花费了两三周事件,通过改造现有项目,基本完成了在现有项目中实践了[代码]Vue[代码]服务端渲染。 关于Vue服务端渲染的原理、搭建,官方文档已经讲的比较详细了,因此,本文不是抄袭文档,而是文档的补充。特别是对于如何与现有项目进行很好的结合,还是需要费很大功夫的。本文主要对我所在的项目中进行[代码]Vue[代码]服务端渲染的改造过程进行阐述,加上一些个人的理解,作为分享与学习。 概述 本文主要分以下几个方面: 什么是服务端渲染?服务端渲染的原理是什么? 如何在基于[代码]Koa[代码]的[代码]Web Server Frame[代码]上配置服务端渲染? 基本用法 [代码]Webpack[代码]配置 开发环境搭建 渲染中间件配置 如何对现有项目进行改造? 基本目录改造; 在服务端用[代码]vue-router[代码]分割代码; 在服务端预拉取数据; 客户端托管全局状态; 常见问题的解决方案; 什么是服务端渲染?服务端渲染的原理是什么? [代码]Vue.js[代码]是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出[代码]Vue[代码]组件,进行生成[代码]DOM[代码]和操作[代码]DOM[代码]。然而,也可以将同一个组件渲染为服务器端的[代码]HTML[代码]字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。 上面这段话是源自Vue服务端渲染文档的解释,用通俗的话来说,大概可以这么理解: 服务端渲染的目的是:性能优势。 在服务端生成对应的[代码]HTML[代码]字符串,客户端接收到对应的[代码]HTML[代码]字符串,能立即渲染[代码]DOM[代码],最高效的首屏耗时。此外,由于服务端直接生成了对应的[代码]HTML[代码]字符串,对[代码]SEO[代码]也非常友好; 服务端渲染的本质是:生成应用程序的“快照”。将[代码]Vue[代码]及对应库运行在服务端,此时,[代码]Web Server Frame[代码]实际上是作为代理服务器去访问接口服务器来预拉取数据,从而将拉取到的数据作为[代码]Vue[代码]组件的初始状态。 服务端渲染的原理是:虚拟[代码]DOM[代码]。在[代码]Web Server Frame[代码]作为代理服务器去访问接口服务器来预拉取数据后,这是服务端初始化组件需要用到的数据,此后,组件的[代码]beforeCreate[代码]和[代码]created[代码]生命周期会在服务端调用,初始化对应的组件后,[代码]Vue[代码]启用虚拟[代码]DOM[代码]形成初始化的[代码]HTML[代码]字符串。之后,交由客户端托管。实现前后端同构应用。 如何在基于[代码]Koa[代码]的[代码]Web Server Frame[代码]上配置服务端渲染? 基本用法 需要用到[代码]Vue[代码]服务端渲染对应库[代码]vue-server-renderer[代码],通过[代码]npm[代码]安装: [代码]npm install vue vue-server-renderer --save [代码] 最简单的,首先渲染一个[代码]Vue[代码]实例: [代码] // 第 1 步:创建一个 Vue 实例 const Vue = require('vue'); const app = new Vue({ template: `<div>Hello World</div>` }); // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer(); // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) { throw err; } console.log(html); // => <div data-server-rendered="true">Hello World</div> }); [代码] 与服务器集成: [代码] module.exports = async function(ctx) { ctx.status = 200; let html = ''; try { // ... html = await renderer.renderToString(app, ctx); } catch (err) { ctx.logger('Vue SSR Render error', JSON.stringify(err)); html = await ctx.getErrorPage(err); // 渲染出错的页面 } ctx.body = html; } [代码] 使用页面模板: 当你在渲染[代码]Vue[代码]应用程序时,[代码]renderer[代码]只从应用程序生成[代码]HTML[代码]标记。在这个示例中,我们必须用一个额外的[代码]HTML[代码]页面包裹容器,来包裹生成的[代码]HTML[代码]标记。 为了简化这些,你可以直接在创建[代码]renderer[代码]时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中: [代码]<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html> [代码] 然后,我们可以读取和传输文件到[代码]Vue renderer[代码]中: [代码]const tpl = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8'); const renderer = vssr.createRenderer({ template: tpl, }); [代码] Webpack配置 然而在实际项目中,不止上述例子那么简单,需要考虑很多方面:路由、数据预取、组件化、全局状态等,所以服务端渲染不是只用一个简单的模板,然后加上使用[代码]vue-server-renderer[代码]完成的,如下面的示意图所示: [图片] 如示意图所示,一般的[代码]Vue[代码]服务端渲染项目,有两个项目入口文件,分别为[代码]entry-client.js[代码]和[代码]entry-server.js[代码],一个仅运行在客户端,一个仅运行在服务端,经过[代码]Webpack[代码]打包后,会生成两个[代码]Bundle[代码],服务端的[代码]Bundle[代码]会用于在服务端使用虚拟[代码]DOM[代码]生成应用程序的“快照”,客户端的[代码]Bundle[代码]会在浏览器执行。 因此,我们需要两个[代码]Webpack[代码]配置,分别命名为[代码]webpack.client.config.js[代码]和[代码]webpack.server.config.js[代码],分别用于生成客户端[代码]Bundle[代码]与服务端[代码]Bundle[代码],分别命名为[代码]vue-ssr-client-manifest.json[代码]与[代码]vue-ssr-server-bundle.json[代码],关于如何配置,[代码]Vue[代码]官方有相关示例vue-hackernews-2.0 开发环境搭建 我所在的项目使用[代码]Koa[代码]作为[代码]Web Server Frame[代码],项目使用koa-webpack进行开发环境的构建。如果是在产品环境下,会生成[代码]vue-ssr-client-manifest.json[代码]与[代码]vue-ssr-server-bundle.json[代码],包含对应的[代码]Bundle[代码],提供客户端和服务端引用,而在开发环境下,一般情况下放在内存中。使用[代码]memory-fs[代码]模块进行读取。 [代码]const fs = require('fs') const path = require( 'path' ); const webpack = require( 'webpack' ); const koaWpDevMiddleware = require( 'koa-webpack' ); const MFS = require('memory-fs'); const appSSR = require('./../../app.ssr.js'); let wpConfig; let clientConfig, serverConfig; let wpCompiler; let clientCompiler, serverCompiler; let clientManifest; let bundle; // 生成服务端bundle的webpack配置 if ((fs.existsSync(path.resolve(cwd,'webpack.server.config.js')))) { serverConfig = require(path.resolve(cwd, 'webpack.server.config.js')); serverCompiler = webpack( serverConfig ); } // 生成客户端clientManifest的webpack配置 if ((fs.existsSync(path.resolve(cwd,'webpack.client.config.js')))) { clientConfig = require(path.resolve(cwd, 'webpack.client.config.js')); clientCompiler = webpack(clientConfig); } if (serverCompiler && clientCompiler) { let publicPath = clientCompiler.output && clientCompiler.output.publicPath; const koaDevMiddleware = await koaWpDevMiddleware({ compiler: clientCompiler, devMiddleware: { publicPath, serverSideRender: true }, }); app.use(koaDevMiddleware); // 服务端渲染生成clientManifest app.use(async (ctx, next) => { const stats = ctx.state.webpackStats.toJson(); const assetsByChunkName = stats.assetsByChunkName; stats.errors.forEach(err => console.error(err)); stats.warnings.forEach(err => console.warn(err)); if (stats.errors.length) { console.error(stats.errors); return; } // 生成的clientManifest放到appSSR模块,应用程序可以直接读取 let fileSystem = koaDevMiddleware.devMiddleware.fileSystem; clientManifest = JSON.parse(fileSystem.readFileSync(path.resolve(cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8')); appSSR.clientManifest = clientManifest; await next(); }); // 服务端渲染的server bundle 存储到内存里 const mfs = new MFS(); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, (err, stats) => { if (err) { throw err; } stats = stats.toJson(); if (stats.errors.length) { console.error(stats.errors); return; } // 生成的bundle放到appSSR模块,应用程序可以直接读取 bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8')); appSSR.bundle = bundle; }); } [代码] 渲染中间件配置 产品环境下,打包后的客户端和服务端的[代码]Bundle[代码]会存储为[代码]vue-ssr-client-manifest.json[代码]与[代码]vue-ssr-server-bundle.json[代码],通过文件流模块[代码]fs[代码]读取即可,但在开发环境下,我创建了一个[代码]appSSR[代码]模块,在发生代码更改时,会触发[代码]Webpack[代码]热更新,[代码]appSSR[代码]对应的[代码]bundle[代码]也会更新,[代码]appSSR[代码]模块代码如下所示: [代码]let clientManifest; let bundle; const appSSR = { get bundle() { return bundle; }, set bundle(val) { bundle = val; }, get clientManifest() { return clientManifest; }, set clientManifest(val) { clientManifest = val; } }; module.exports = appSSR; [代码] 通过引入[代码]appSSR[代码]模块,在开发环境下,就可以拿到[代码]clientManifest[代码]和[代码]ssrBundle[代码],项目的渲染中间件如下: [代码]const fs = require('fs'); const path = require('path'); const ejs = require('ejs'); const vue = require('vue'); const vssr = require('vue-server-renderer'); const createBundleRenderer = vssr.createBundleRenderer; const dirname = process.cwd(); const siteInfo = require('./../../core/siteinfo.js').get(); const env = siteInfo.env; let bundle; let clientManifest; if (env === 'development') { // 开发环境下,通过appSSR模块,拿到clientManifest和ssrBundle let appSSR = require('./../../core/app.ssr.js'); bundle = appSSR.bundle; clientManifest = appSSR.clientManifest; } else { bundle = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-server-bundle.json'), 'utf-8')); clientManifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'), 'utf-8')); } module.exports = async function(ctx) { ctx.status = 200; let html; let context = await ctx.getTplContext(); ctx.logger('进入SSR,context为: ', JSON.stringify(context)); const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8'); const renderer = createBundleRenderer(bundle, { runInNewContext: false, template: tpl, // (可选)页面模板 clientManifest: clientManifest // (可选)客户端构建 manifest }); ctx.logger('createBundleRenderer renderer:', JSON.stringify(renderer)); try { html = await renderer.renderToString({ ...context, url: context.CTX.url, }); } catch(err) { ctx.logger('SSR renderToString 失败: ', JSON.stringify(err)); console.error(err); } ctx.body = html; }; [代码] 如何对现有项目进行改造? 基本目录改造 使用[代码]Webpack[代码]来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用[代码]Webpack[代码]支持的所有功能。 一个基本项目可能像是这样: [代码]src ├── components │ ├── Foo.vue │ ├── Bar.vue │ └── Baz.vue ├── frame │ ├── app.js # 通用 entry(universal entry) │ ├── entry-client.js # 仅运行于浏览器 │ ├── entry-server.js # 仅运行于服务器 │ └── index.vue # 项目入口组件 ├── pages ├── routers └── store [代码] [代码]app.js[代码]是我们应用程序的「通用[代码]entry[代码]」。在纯客户端应用程序中,我们将在此文件中创建根[代码]Vue[代码]实例,并直接挂载到[代码]DOM[代码]。但是,对于服务器端渲染([代码]SSR[代码]),责任转移到纯客户端[代码]entry[代码]文件。[代码]app.js[代码]简单地使用[代码]export[代码]导出一个[代码]createApp[代码]函数: [代码]import Router from '~ut/router'; import { sync } from 'vuex-router-sync'; const Vue = require('vue'); const { createStore } = require('./../store'); import ElementUI from 'element-ui'; import vueScroll from 'vue-scroll'; import '~mstyle/ai-scan/src/assets/less/common.less'; import Frame from './index.vue'; import breastRouter from './../routers/breast'; import lungRouter from './../routers/lung'; import List from './../page/list/index.vue'; import DicomView from '~cmpt/dicom-view'; Vue.use(vueScroll); Vue.use(ElementUI); function createVueInstance(routes, ctx) { let siteinfo; if (ctx) { siteinfo = ctx.siteinfo; } else { siteinfo = window.SiteInfo; } const router = Router({ base: siteinfo.path, mode: 'history', routes: [routes], }); const store = createStore({ ctx }); // 把路由注入到vuex中 sync(store, router); const app = new Vue({ router, render: function(h) { return h(Frame); }, store, }); return { app, router, store }; } function createVueInstanceNoRouter(component, ctx) { const componentIns = Vue.extend(component); const store = createStore({ ctx }); const app = new Vue({ store, render: h => h(component), }); return { app, store }; } module.exports = function createApp(ctx) { // 通过typeof window是否为'undefined'判断当前是客户端还是服务端 // 针对不同页面创建不同的Vue实例 if (typeof window !== 'undefined') { // 客户端 if (/list/.test(window.location.pathname)) { return createVueInstanceNoRouter(List); } else if (/breast/.test(window.location.pathname)) { DicomView.init('image', 'imaging'); return createVueInstance(breastRouter); } else if (/lung/.test(window.location.pathname)) { DicomView.init('series', 'imaging'); return createVueInstance(lungRouter); } else if (/pacs/.test(window.location.pathname)) { DicomView.init('series', 'imaging'); return createVueInstance(pacsRouter); } else if (/esophagus/.test(window.location.pathname)) { return createVueInstance(esophagusRouter); } else { return void console.log('The path does not match any router rule'); // do nothing } } else { // 服务端 if (/list/.test(ctx.path)) { return createVueInstanceNoRouter(List, ctx); } else if (/lung/.test(ctx.path)) { return createVueInstance(lungRouter, ctx); } else if (/pacs/.test(ctx.path)) { return createVueInstance(pacsRouter); } else if (/esophagus/.test(ctx.path)) { return createVueInstance(esophagusRouter); } else if (/breast/.test(ctx.path)) { return createVueInstance(breastRouter, ctx); } else { return void console.log('The path does not match any router rule'); // do nothing } } } [代码] 注:在我所在的项目中,需要动态判断是否需要注册[代码]DicomView[代码],只有在客户端才初始化[代码]DicomView[代码],由于[代码]Node.js[代码]环境没有[代码]window[代码]对象,对于代码运行环境的判断,可以通过[代码]typeof window === 'undefined'[代码]来进行判断。 避免创建单例 如[代码]Vue SSR[代码]文档所述: 当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例。同样的规则也适用于 router、store 和 event bus 实例。你不应该直接从模块导出并将其导入到应用程序中,而是需要在 createApp 中创建一个新的实例,并从根 Vue 实例注入。 如上代码所述,[代码]createApp[代码]方法通过返回一个返回值创建[代码]Vue[代码]实例的对象的函数调用,在函数[代码]createVueInstance[代码]中,为每一个请求创建了[代码]Vue[代码],[代码]Vue Router[代码],[代码]Vuex[代码]实例。并暴露给[代码]entry-client[代码]和[代码]entry-server[代码]模块。 在客户端[代码]entry-client.js[代码]只需创建应用程序,并且将其挂载到[代码]DOM[代码]中: [代码]import { createApp } from './app'; // 客户端特定引导逻辑…… const { app } = createApp(); // 这里假定 App.vue 模板中根元素具有 `id="app"` app.$mount('#app'); [代码] 服务端[代码]entry-server.js[代码]使用[代码]default export[代码] 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配和数据预取逻辑: [代码]import { createApp } from './app'; export default context => { const { app } = createApp(); return app; } [代码] 在服务端用[代码]vue-router[代码]分割代码 与[代码]Vue[代码]实例一样,也需要创建单例的[代码]vueRouter[代码]对象。对于每个请求,都需要创建一个新的[代码]vueRouter[代码]实例: [代码]function createVueInstance(routes, ctx) { let siteinfo; if (ctx) { siteinfo = ctx.siteinfo; } else { siteinfo = window.SiteInfo; } const router = Router({ base: siteinfo.path, mode: 'history', routes: [routes], }); const store = createStore({ ctx }); // 把路由注入到vuex中 sync(store, router); const app = new Vue({ router, render: function(h) { return h(Frame); }, store, }); return { app, router, store }; } [代码] 同时,需要在[代码]entry-server.js[代码]中实现服务器端路由逻辑,使用[代码]router.getMatchedComponents[代码]方法获取到当前路由匹配的组件,如果当前路由没有匹配到相应的组件,则[代码]reject[代码]到[代码]404[代码]页面,否则[代码]resolve[代码]整个[代码]app[代码],用于[代码]Vue[代码]渲染虚拟[代码]DOM[代码],并使用对应模板生成对应的[代码]HTML[代码]字符串。 [代码]const createApp = require('./app'); module.exports = context => { return new Promise((resolve, reject) => { // ... // 设置服务器端 router 的位置 router.push(context.url); // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject('匹配不到的路由,执行 reject 函数,并返回 404'); } // Promise 应该 resolve 应用程序实例,以便它可以渲染 resolve(app); }, reject); }); } [代码] 在服务端预拉取数据 在[代码]Vue[代码]服务端渲染,本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。服务端[代码]Web Server Frame[代码]作为代理服务器,在服务端对接口服务发起请求,并将数据拼装到全局[代码]Vuex[代码]状态中。 另一个需要关注的问题是在客户端,在挂载到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。 目前较好的解决方案是,给路由匹配的一级子组件一个[代码]asyncData[代码],在[代码]asyncData[代码]方法中,[代码]dispatch[代码]对应的[代码]action[代码]。[代码]asyncData[代码]是我们约定的函数名,表示渲染组件需要预先执行它获取初始数据,它返回一个[代码]Promise[代码],以便我们在后端渲染的时候可以知道什么时候该操作完成。注意,由于此函数会在组件实例化之前调用,所以它无法访问[代码]this[代码]。需要将[代码]store[代码]和路由信息作为参数传递进去: 举个例子: [代码]<!-- Lung.vue --> <template> <div></div> </template> <script> export default { // ... async asyncData({ store, route }) { return Promise.all([ store.dispatch('getUserInfo'), store.dispatch('lung/getSeriesByStudyId', { studyId: route.params.id, aiEngine: route.params.aiEngine, }, { root:true }), store.dispatch('lung/getDicomViewConfig', { root:true }), store.dispatch('lung/getDialogWindow', { root:true }), ]); }, // ... } </script> [代码] 在[代码]entry-server.js[代码]中,我们可以通过路由获得与[代码]router.getMatchedComponents()[代码]相匹配的组件,如果组件暴露出[代码]asyncData[代码],我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文中。 [代码]const createApp = require('./app'); module.exports = context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp(context.CTX); // 针对没有Vue router 的Vue实例,在项目中为列表页,直接resolve app if (!router) { resolve(app); } // 设置服务器端 router 的位置 router.push(context.CTX.url.replace('/imaging', '')); // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject('匹配不到的路由,执行 reject 函数,并返回 404'); } Promise.all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute, }); } })).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文,并且 `template` 选项用于 renderer 时, // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 context.state = store.state; resolve(app); }).catch(reject); }, reject); }); } [代码] 客户端托管全局状态 当服务端使用模板进行渲染时,[代码]context.state[代码]将作为[代码]window.__INITIAL_STATE__[代码]状态,自动嵌入到最终的[代码]HTML[代码] 中。而在客户端,在挂载到应用程序之前,[代码]store[代码]就应该获取到状态,最终我们的[代码]entry-client.js[代码]被改造为如下所示: [代码]import createApp from './app'; const { app, router, store } = createApp(); // 客户端把初始化的store替换为window.__INITIAL_STATE__ if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__); } if (router) { router.onReady(() => { app.$mount('#app') }); } else { app.$mount('#app'); } [代码] 常见问题的解决方案 至此,基本的代码改造也已经完成了,下面说的是一些常见问题的解决方案: 在服务端没有[代码]window[代码]、[代码]location[代码]对象: 对于旧项目迁移到[代码]SSR[代码]肯定会经历的问题,一般为在项目入口处或是[代码]created[代码]、[代码]beforeCreate[代码]生命周期使用了[代码]DOM[代码]操作,或是获取了[代码]location[代码]对象,通用的解决方案一般为判断执行环境,通过[代码]typeof window[代码]是否为[代码]'undefined'[代码],如果遇到必须使用[代码]location[代码]对象的地方用于获取[代码]url[代码]中的相关参数,在[代码]ctx[代码]对象中也可以找到对应参数。 [代码]vue-router[代码]报错[代码]Uncaught TypeError: _Vue.extend is not _Vue function[代码],没有找到[代码]_Vue[代码]实例的问题: 通过查看[代码]Vue-router[代码]源码发现没有手动调用[代码]Vue.use(Vue-Router);[代码]。没有调用[代码]Vue.use(Vue-Router);[代码]在浏览器端没有出现问题,但在服务端就会出现问题。对应的[代码]Vue-router[代码]源码所示: [代码]VueRouter.prototype.init = function init (app /* Vue component instance */) { var this$1 = this; process.env.NODE_ENV !== 'production' && assert( install.installed, "not installed. Make sure to call `Vue.use(VueRouter)` " + "before creating root instance." ); // ... } [代码] 服务端无法获取[代码]hash[代码]路由的参数 由于[代码]hash[代码]路由的参数,会导致[代码]vue-router[代码]不起效果,对于使用了[代码]vue-router[代码]的前后端同构应用,必须换为[代码]history[代码]路由。 接口处获取不到[代码]cookie[代码]的问题: 由于客户端每次请求都会对应地把[代码]cookie[代码]带给接口侧,而服务端[代码]Web Server Frame[代码]作为代理服务器,并不会每次维持[代码]cookie[代码],所以需要我们手动把 [代码]cookie[代码]透传给接口侧,常用的解决方案是,将[代码]ctx[代码]挂载到全局状态中,当发起异步请求时,手动带上[代码]cookie[代码],如下代码所示: [代码]// createStore.js // 在创建全局状态的函数`createStore`时,将`ctx`挂载到全局状态 export function createStore({ ctx }) { return new Vuex.Store({ state: { ...state, ctx, }, getters, actions, mutations, modules: { // ... }, plugins: debug ? [createLogger()] : [], }); } [代码] 当发起异步请求时,手动带上[代码]cookie[代码],项目中使用的是[代码]Axios[代码]: [代码]// actions.js // ... const actions = { async getUserInfo({ commit, state }) { let requestParams = { params: { random: tool.createRandomString(8, true), }, headers: { 'X-Requested-With': 'XMLHttpRequest', }, }; // 手动带上cookie if (state.ctx.request.headers.cookie) { requestParams.headers.Cookie = state.ctx.request.headers.cookie; } // ... let userInfoRes = await Axios.get(`${requestUrlOrigin}${url.GET_USERINFO}`, requestParams); commit(globalTypes.SET_USERINFO, { userInfo: userInfoRes.data, }); } }; // ... [代码] 接口请求时报[代码]connect ECONNREFUSED 127.0.0.1:80[代码]的问题 原因是改造之前,使用客户端渲染时,使用了[代码]devServer.proxy[代码]代理配置来解决跨域问题,而服务端作为代理服务器对接口发起异步请求时,不会读取对应的[代码]webpack[代码]配置,对于服务端而言会对应请求当前域下的对应[代码]path[代码]下的接口。 解决方案为去除[代码]webpack[代码]的[代码]devServer.proxy[代码]配置,对于接口请求带上对应的[代码]origin[代码]即可: [代码]let requestUrlOrigin; if (state.ctx.URL.hostname === 'localhost' || state.ctx.URL.hostname === '127.0.0.1') { // 本地开发,转到localhost:9000 requestUrlOrigin = constParams.CONST_LOCAL_WEBSERVER_ORIGIN; } else { // 测试环境、正式环境、盒子 requestUrlOrigin = state.ctx.URL.origin; } let userInfoRes = await Axios.get(`${requestUrlOrigin}${url.GET_USERINFO}`, requestParams); [代码] 对于[代码]vue-router[代码]配置项有[代码]base[代码]参数时,初始化时匹配不到对应路由的问题 在官方示例中的[代码]entry-server.js[代码]: [代码]// entry-server.js import { createApp } from './app'; export default context => { // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, // 以便服务器能够等待所有的内容在渲染前, // 就已经准备就绪。 return new Promise((resolve, reject) => { const { app, router } = createApp(); // 设置服务器端 router 的位置 router.push(context.url); // ... }); } [代码] 原因是设置服务器端[代码]router[代码]的位置时,[代码]context.url[代码]为访问页面的[代码]url[代码],并带上了[代码]base[代码],在[代码]router.push[代码]时应该去除[代码]base[代码],如下所示: [代码]router.push(context.url.replace('/base', '')); [代码] 小结 本文为笔者通过对现有项目进行改造,给现有项目加上[代码]Vue[代码]服务端渲染的实践过程的总结。 首先阐述了什么是[代码]Vue[代码]服务端渲染,其目的、本质及原理,通过在服务端使用[代码]Vue[代码]的虚拟[代码]DOM[代码],形成初始化的[代码]HTML[代码]字符串,即应用程序的“快照”。带来极大的性能优势,包括[代码]SEO[代码]优势和首屏渲染的极速体验。之后阐述了[代码]Vue[代码]服务端渲染的基本用法,即两个入口、两个[代码]webpack[代码]配置,分别作用于客户端和服务端,分别生成[代码]vue-ssr-client-manifest.json[代码]与[代码]vue-ssr-server-bundle.json[代码]作为打包结果。最后通过对现有项目的改造过程,包括对路由进行改造、数据预获取和状态初始化,并解释了在[代码]Vue[代码]服务端渲染项目改造过程中的常见问题,帮助我们进行现有项目往[代码]Vue[代码]服务端渲染的迁移。
2019-04-16 - 小程序canvas的那些事
背景 业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二: 服务端合成:直接返回给前端图片URL 客户端合成:客户端利用canvas绘制 在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。 下面主要谈谈**客户端(canvas)**合成的过程。 实现思路 小程序端发起请求,获取需动态展示的数据; 利用canvas绘制画布; 导出图片保存到相册。 小技巧&那些坑 理想很丰满,现实很骨感。 实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。 promise化 画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。 [代码] let promise1 = new Promise((resolve, reject) => { this.getData(resolve, reject) }); let promise2 = new Promise((resolve, reject) => { this.getSystemInfo(resolve, reject) }); Promise.all([promise1, promise2]).then(() => { this.drawCanvas() }).catch(err => { console.log(err) }); [代码] 自适应 1、为了在各个机型下保持大小自适应,需要计算出缩放比: [代码] getSystemInfo(resolve, reject) { try { const res = wx.getSystemInfoSync() //缓存系统信息 systemInfo = res //这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375 zoom = res.windowWidth / 750 * 1 resolve() } catch (e) { // Do something when catch error reject("获取机型失败") } } [代码] 2、绘制时进行按缩放比进行缩放,如: [代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom) [代码] 绘制网络图片 经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制: [代码]wx.downloadFile({ url: imgUrl, success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom) } } }) [代码] 绘制base64图片 因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。 解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持): 1、调用fileManager.writeFile存储base64到本地; 2、绘制本地图片。 实现代码如下: [代码]// 先获得一个文件实例 fileManager = wx.getFileSystemManager() // 把图片base64格式转存到本地,用于canvas绘制 fileManager.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`, data: self.data.qrcode, encoding: 'base64', success: () => { //此处需先调用wx.getImageInfo,方可绘制成功 wx.getImageInfo({ src: `${wx.env.USER_DATA_PATH}/qrcode.png`, success: () => { //绘制二维码 ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom) ctx.draw() } }) } }) [代码] 保存到本地相册 wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。 [代码]canvas2Img(e) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum'] === undefined) { //呼起授权界面 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { save() } }) } else if (res.authSetting['scope.writePhotosAlbum'] === false) { //引导拒绝过授权的用户授权 wx.showModal({ title: '温馨提示', content: '需要您授权保存到相册的权限', success: res => { if (res.confirm) { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { save() } } }) } } }) } else { save() } } }) function save() { wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg', success: res => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showModal({ content: '保存成功', showCancel: false, confirmText: '确认' }) }, fail: res => { if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') { wx.showModal({ content: '保存到相册失败', showCancel: false }) } } }) }, fail: () => { wx.showModal({ content: '保存到相册失败', showCancel: false }) } }) } [代码] 导出清晰图片 wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。 原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。 这里应该乘以设备像素比,实现如下: [代码]wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg' )} [代码] 特殊字体的绘制 研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。 实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。 [代码]drawNum(num, x, y, w, h) { return new Promise(function (resolve, reject) { //这里存储0-9的图片CDN链接 let numMap = [] wx.downloadFile({ url: numMap[num], success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom) resolve() } }, fail: () => { reject() } }) }) } [代码] 安卓机型图片绘制锯齿化问题 测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。 总结 个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点: 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等); 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。 ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。
2019-03-07 - 小程序多端框架全面测评
最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11日)已发布稳定版为标准进行讨论。 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: [代码]uni-app[代码] > [代码]Taro[代码] > [代码]chameleon[代码] > [代码]WePY[代码]、[代码]mpvue[代码] [图片] 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码] 则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: [代码]chameleon[代码] > [代码]Taro[代码]、[代码]uni-app[代码] > [代码]mpvue[代码]、[代码]WePY[代码] [图片] 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: [代码]WePY[代码] > [代码]uni-app[代码] 、[代码]taro[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] [图片] 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: [代码]Taro[代码] > [代码]mpvue[代码] 、 [代码]uni-app[代码] > [代码]WePY[代码] > [代码]chameleon[代码] 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: [代码]uni-app[代码] > [代码]Taro[代码]、[代码]WePY[代码]、[代码]mpvue[代码] > [代码]chameleon[代码] [图片] 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: [代码]Taro[代码] > [代码]WePY[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] > [代码]uni-app[代码] 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码] 不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-03-19 - PNG图片压缩原理--屌丝的眼泪
背景 今天凌晨一点,突然有个人加我的qq,一看竟然是十年前被我删掉的初恋。。。。 因为之前在qq空间有太多的互动,所以qq推荐好友里面经常推荐我俩互相认识。。。。谜之尴尬 [图片] 同意好友申请以后,仔细看了她这十年间所有的qq动态和照片。 她变美了,会打扮了,以前瘦瘦的身材配上现在的装扮和妆容,已经是超越我认知的女神了。 而我依然碌碌无为,逐渐臃肿的身体加上日益上扬的发际线,每天为生活操劳和奔波,还穷。 用一句话形容现在的感受就是: “妳已经登上更高的巅峰 而我只能望着妳远去的背影”。 默默点了根烟,把她长得好看的照片都保存了下来。 咦?发现每一张照片都是.png的图片格式。 png??png的图片我们每天都在用,可是png到底是什么,它的压缩原理是什么? 很好,接下来我将会给大家一一阐述。 什么是PNG PNG的全称叫便携式网络图型(Portable Network Graphics)是目前最流行的网络传输和展示的图片格式,原因有如下几点: [代码]无损压缩[代码]:PNG图片采取了基于LZ77派生算法对文件进行压缩,使得它压缩比率更高,生成的文件体积更小,并且不损失数据。 [代码]体积小[代码]:它利用特殊的编码方法标记重复出现的数据,使得同样格式的图片,PNG图片文件的体积更小。网络通讯中因受带宽制约,在保证图片清晰、逼真的前提下,优先选择PNG格式的图片。 [代码]支持透明效果[代码]:PNG支持对原图像定义256个透明层次,使得图像的边缘能与任何背景平滑融合,这种功能是GIF和JPEG没有的。 PNG类型 PNG图片主要有三个类型,分别为 PNG 8/ PNG 24 / PNG 32。 [代码]PNG 8[代码]:PNG 8中的8,其实指的是8bits,相当于用28(2的8次方)大小来存储一张图片的颜色种类,28等于256,也就是说PNG 8能存储256种颜色,一张图片如果颜色种类很少,将它设置成PNG 8得图片类型是非常适合的。 [代码]PNG 24[代码]:PNG 24中的24,相当于3乘以8 等于 24,就是用三个8bits分别去表示 R(红)、G(绿)、B(蓝)。R(0255),G(0255),B(0~255),可以表达256乘以256乘以256=16777216种颜色的图片,这样PNG 24就能比PNG 8表示色彩更丰富的图片。但是所占用的空间相对就更大了。 [代码]PNG 32[代码]:PNG 32中的32,相当于PNG 24 加上 8bits的透明颜色通道,就相当于R(红)、G(绿)、B(蓝)、A(透明)。R(0255),G(0255),B(0255),A(0255)。比PNG 24多了一个A(透明),也就是说PNG 32能表示跟PNG 24一样多的色彩,并且还支持256种透明的颜色,能表示更加丰富的图片颜色类型。 怎么说呢,总的来说,PNG 8/ PNG 24 / PNG 32就相当于我们屌丝心中,把女神分为三类: [代码]一类女神 = PNG 8[代码]:屌丝舔狗们见到第一类女神,顿时会觉得心情愉悦、笑逐颜开,屌丝发黑的印堂逐渐舒展,确认过眼神,是心动的感觉。 [代码]二类女神 = PNG 24[代码]:第二类女神开始厉害了,会给屌丝们一种菊花一紧、振聋发聩的心弦震撼,接触多了第二类女神能让屌丝每天精神抖擞,延年益寿。 [代码]三类女神 = PNG 32[代码]:在第三类女神面前,所有的语言都显得苍白无力。那是一种看了让屌丝上下通透、手眼通天的至尊级存在。超凡脱俗、天神下凡都不足以描摹她美色的二分之一。我曾经只有在梦里才见到过。 哎。。。我的初恋,看着她现在的照片,应该是触及PNG 24这一等级了。 PNG图片数据结构 PNG图片的数据结构其实跟http请求的结构很像,都是一个数据头,后面跟着很多的数据块,如下图所示: [图片] 如果你用vim的查看编码模式打开一张png图片,会是下面这个样子: [图片] 握草,第一眼看到这一坨坨十六进制编码是不是感觉和女神的心思一样晦涩难懂? 老弟 莫慌,讲实话,如果撩妹纸有那一坨坨乱码那么简单,哥哥我早就妻妾成群啦。 接下来我就一一讲解这一堆十六进制编码的含义。 [代码]8950 4e47 0d0a 1a0a[代码]:这个是PNG图片的头,所有的PNG图片的头都是这一串编码,图片软件通过这串编码判定这个文件是不是PNG格式的图片。 [代码]0000 000d[代码]:是iHDR数据块的长度,为13。 [代码]4948 4452[代码]:是数据块的type,为IHDR,之后紧跟着是data。 [代码]0000 02bc[代码]:是图片的宽度。 [代码]0000 03a5[代码]:是高度。 以此类推,每一段十六进制编码就代表着一个特定的含义。下面其他的就不一一分析了,太多了,小伙伴们自己去查吧。 什么样的PNG图片更适合压缩 常规的png图片,颜色越单一,颜色值越少,压缩率就越大,比如下面这张图: [图片] 它仅仅由红色和绿色构成,如果用0代表红色,用1代表绿色,那用数字表示这张图就是下面这个样子: 00000000000000000 00000000000000000 00000000000000000 1111111111111111111111111 1111111111111111111111111 1111111111111111111111111 我们可以看到,这张图片是用了大量重复的数字,我们可以将重复的数字去掉,直接用数组形式的[0, 1]就可以直接表示出这张图片了,仅仅用两个数字,就能表示出一张很大的图片,这样就极大的压缩了一张png图片。 所以!颜色越单一,颜色值越少,颜色差异越小的png图片,压缩率就越大,体积就越小。 PNG的压缩 PNG图片的压缩,分两个阶段: [代码]预解析(Prediction)[代码]:这个阶段就是对png图片进行一个预处理,处理后让它更方便后续的压缩。说白了,就是一个女神,在化妆前,会先打底,先涂乳液和精华,方便后续上妆、美白、眼影、打光等等。 [代码]压缩(Compression)[代码]:执行Deflate压缩,该算法结合了 LZ77 算法和 Huffman 算法对图片进行编码。 预解析(Prediction) png图片用差分编码(Delta encoding)对图片进行预处理,处理每一个的像素点中每条通道的值,差分编码主要有几种: 不过滤 X-A X-B X-(A+B)/2(又称平均值) Paeth推断(这种比较复杂) 假设,一张png图片如下: [图片] 这张图片是一个红色逐渐增强的渐变色图,它的红色从左到右逐渐加强,映射成数组的值为[1,2,3,4,5,6,7,8],使用X-A的差分编码的话,那就是: [2-1=1, 3-2=1, 4-3=1, 5-4=1, 6-5=1, 7-6=1, 8-7=1] 得到的结果为 [1,1,1,1,1,1,1] 最后的[1,1,1,1,1,1,1]这个结果出现了大量的重复数字,这样就非常适合进行压缩。 这就是为什么渐变色图片、颜色值变化不大并且颜色单一的图片更容易压缩的原理。 差分编码的目的,就是尽可能的将png图片数据值转换成一组重复的、低的值,这样的值更容易被压缩。 最后还要注意的是,差分编码处理的是每一个的像素点中每条颜色通道的值,R(红)、G(绿)、B(蓝)、A(透明)四个颜色通道的值分别进行处理。 压缩(Compression) 压缩阶段会将预处理阶段得到的结果进行Deflate压缩,它由 Huffman 编码 和 LZ77压缩构成。 如前面所说,Deflate压缩会标记图片所有的重复数据,并记录数据特征和结构,会得到一个压缩比最大的png图片 编码数据。 Deflate是一种压缩数据流的算法. 任何需要流式压缩的地方都可以用。 还有就是我们前面说过,一个png图片,是由很多的数据块构成的,但是数据块里面的一些信息其实是没有用的,比如用Photoshop保存了一张png图片,图片里就会有一个区块记录“这张图片是由photshop创建的”,很多类似这些信息都是无用的,如果用photoshop的“导出web格式”就能去掉这些无用信息。导出web格式前后对比效果如下图所示: [图片] 可以看到,导出web格式,去除了很多无用信息后,图片明显小了很多。 结语 以上就是我对png的理解了,写的不好,就像一个支离破碎的中老年,杂乱无章。 想起那年跟初恋分手的原因 是因为怕影响到学习。。。可是分开后成绩也还是很烂,不仅错过了女神,而且到现在也依然一事无成。 如今中年已至,身上背负着巨大的房贷,家里还有嗷嗷待哺的孩子,看着身旁呼噜声轰天熟睡中的妻子,突然也就想开了。 就像鲁迅说的: “爱情就像在海滩上捡贝壳,不要捡最大的, 也不要捡最漂亮的,要捡就捡自己最喜欢的, 最重要的是捡到了自己喜欢的 就永远不要再去海边了。” 。。。。。。 凌晨四点写完文章 不知不觉睡着了 梦回到十年前的那个夏天 我们都笑的很甜 看着你哭泣的脸 微笑着对我说再见 再见 作者:第一名的小蝌蚪 github: 文章会第一时间分享在前端屌丝心路历程,欢迎star或者watch,感恩
2019-03-22 - docker快速入门
1. 介绍docker是什么Docker使用go基于linux lxc(linux containers)技术实现的开源容器,诞生于2013年年初,最开始叫dotcloud公司,13年年底改名为docker inc。 2017年下载次数达到了百亿次,估值达13亿美元,通过对应用封装(Packaging)、分发(Distribution)、部署(Deployment)、运行(Runtime)全生命周期管理,达到“一次封装,到处运行” [图片] 为何使用docker?Docker直译码头工人,将各种大小和形状的物品装进船里。这对从事软件行业的人来说,听起来很熟悉,花了大量时间和精力把一个应用放在另一个应用里 [图片] docker出现之前,对不同环境的安装、配置、维护工作量很多,如部署,配置文件,crontab,依赖等等。 使用docker,无需关心环境,只需要一些配置就能构建镜像,而部署则用一条run命令 [图片] 虚拟机 vs 容器 虚拟机需要有额外的虚拟机管理应用和虚拟机操作系统层,操作系统层不仅占用空间而且运行速度也相对慢 docker容器是在本机操作系统层面上实现虚拟化,因此很轻量,速度接近原生系统速度 [图片] 虚拟机启动速度是分钟级别,性能较弱、内存和硬盘占用大,一个物理机最多跑几十个虚拟机,但它的隔离性比较好。 docker启停都是秒级实现,内存和硬盘占用非常小,单机支持上千个容器,在ibm服务器上可运行上万个容器 容器跟虚机相比,有着巨大的优势 [图片] docker优点 只关心应用:以往我们需要关心操作系统、软件、项目,有了docker我们可以只关心应用而不是操作系统,docker发展迅速,基于docker的paas平台也层出不穷,使得我们能更方便的使用docker 快速交付:docker可在秒级提供沙箱环境,开发,测试,运维使用完全相同的环境来部署代码 微服务:docker有助于将一个复杂系统分解,让用户用更离散的方式思考服务 离线开发:将服务编排在笔记本中移动办公,使用docker可在本机秒级别启动一个本地开发环境 降低调试成本:在测试和上线时产生无效的类、有问题的依赖、缺少的配置等问题,docker可让一个问题调试和环境重现变得更简单 CD:docker让持续交付实现变得更容易,特别是对于蓝绿部署就更简单。 第一版上线时,需要上第二版新功能,两个版本功能会有冲突,这时用docker实现蓝绿部署就非常方便了 如:可以部署两个版本同时在线,新版本测试没问题了把老版本流量切到新版本就可以了 迁移:可以很快的迁移到其他云或服务器 与传统虚拟机方式相比,容器化方式在很多场景下都是存在极为明显的优势。无论是开发、测试、运维都应该尽快掌握docker,尽早享受其带来的巨大便利 [图片] 容器化方式在很多场景下都有极大的优势。无论是开发、测试、运维都应该尽快掌握docker,尽早享受其带来的巨大便利 [图片] 概念再来了解docker非常关键的概念,这样才能理解docker容器整个生命周期 [图片] 概念—镜像 镜像(类)=文件系统+数据,我常常用开发语言中的类比作镜像,对象比作容器 镜像由多个层加上一些docker元数据组成,容器运行着由镜像定义的系统 [图片] 概念—容器容器(对象)=镜像运行实例 容器是镜像的运行实例,可以使用同一个镜像运行多个实例。如图所示,一个ubuntu docker镜像产生了三个ubuntu容器,docker利用容器运行和隔离应用 [图片] 从读写角度来说,镜像是只读的,容器是在镜像上添加了一层可读写的文件系统 [图片] [图片] 概念—层层=文件变更集合 像传统虚机应用,每个应用都需要拷贝一份文件副本,运行成百上千上磁盘空间会迅速耗光,而docker采用写时复制来减少磁盘空间,当一个运行中的容器要写入一个文件时,它会把该文件复制到新区域来记录这次的修改,在执行docker提交时将这次修改记录下并产生一个新的层。docker分层解决大规模使用容器时碰到的磁盘和效率问题 [图片] 概念—仓库docker借鉴了大量git优秀的经验。docker仓库分公有库和私有库,最大的公开仓库是docker hub,国内也有很多仓库源 [图片] 2. 创建第一个docker应用通过创建一个docker应用来看看docker是怎么方便使用的 创建docker镜像方式 创建docker有四种方式 [图片] 但最常用的docker命令+手工提交和Dockerfile的方式 [图片] 对于我们来说Dockerfile是最常用也是最有用的 “dockerfile” [图片] 那创建一个docker应用只需要三步:编写dockerfile、构建镜像、运行容器 编写dockerfile那我们就开始用dockerfile来创建一个应用 Dockerfile是包含一系列命令的文本文件,这个文件包含6条命令 1、FROM是使用php官方镜像,左边是镜像名字,右边是标签名字,标签名字不写默认是latest 2、声明维护人员 3、RUN运行一条linux命令,我们把php代码重定向到/tmp/index.php 4、EXPOSE声明要开放的端口 5、WORKDIR启动容器后默认目录 6、CMD容器启动后,默认执行的命令,相当于应用的入口,用php自带的webserver监听8000 [图片] 构建镜像使用docker build命令生成镜像,—tag指定镜像的名字,左边是名字,右边是标签,最后有个.表示在当前目录查找Dockerfile 可以看到,每个命令都会有个输入输出,输入是命令,输出是给到层的id,所以,基本上每个命令都会产生一个层 最后提示镜像构建成功,并打上镜像标签 [图片] 运行容器第三,使用docker run命令运行镜像,-p将容器的8000端口映射到本机8000端口,—name给容器起个名字 用curl对本机8000端口请求,服务器返回当前时间,说明我们构建的容器运行成功了 [图片] 请求本地8000端口,服务器返回当前时间 [图片] dockerfile常用命令其实Dockerfile常用命令就5个:from、add、run、workdir、cmd 创建docker应用步骤编写dockerfile 构建镜像 运行容器 使用docker应用步骤拉取镜像 运行容器 dockerfile最佳实践精简镜像用途 尽量让每个镜像的用途单一 选择合适基础镜像 选择以alpine、busybox等基础的镜像 busybox:号称操作系统里的瑞士军刀,只有……这么大,但却有一百多常用命令 如果你的目标是小而精,busybox是首选,因为它已经精简到没有bash,使用的是ash,一个兼容posix的shell [图片] Alpine:你的目标是小但是又有一些工具的话,可以选择alpine,它是一个面向安全的轻量linux发行版,它关注安全、性能和资源效能,比busybox功能更完善,还提供apk查询和安装软件包,大小只有2-3兆 [图片] 很多官方的镜像都有alpine的镜像,像刚刚使用的php镜像 [图片] 提供维护者信息 正确使用版本 使用明确的版本号,而非依赖于默认的latest,避免环境不一致导致的问题 [图片] 删除临时文件 如安装软件后的安装包,如上图2、3步骤 提高生成速度 如内容不变的指令尽量放在前面,这样可以复用 减少镜像层数 多条命令写在一起,使生成的镜像层数少,如上图2、3步骤 恰当使用multi-stage 保证最终生成镜像最小化 3. 常用命令search想使用一个镜像,用这个命令就可以了,默认按评分排序 official如果是ok表示是官方镜像 Auto标示它是否用dickerfile进行自动化镜像构建 [图片] pull一旦确定一个镜像,通过对其名称执行docker pull来下载 标签默认是latest,严格来讲,镜像的仓库名还应该添加仓库地址的,默认是registry.hub.docker.com Docker images命令查找下载的镜像 [图片] run使用docker run运行一个容器,it表示用交互式方式运行,最后表示要执行的命令 [图片] 其实更常用的方式是以后台方式来执行,这时用d参数在后台运行 运行后用exec命令进去到容器 [图片] tagDocker tag给镜像一个新tag名字 Docker images查看centos镜像,把centos:latest打上centos:yeedomliu,这时再看会有3个centos,latest和yeedomliu的镜像id是相同的 把centos:yeedomliu删除,再查看latest还会存在,最后用rmi命令删除latest就会真正把latest镜像删除掉 如果相同镜像存在多个标签,只有最后一次的rmi命令会真正删除镜像 [图片] psPs可以查看运行中的容器 [图片] rmi删除一个镜像,同一个镜像id的不同标签的镜像,使用rmi删除最后一个镜像才会真正删除这个镜像 [图片] rm删除docker容器,如果运行中的容器需要加-f [图片] diff容器启动后文件变化情况 [图片] logs查看容器运行后的日志 [图片] cp我们想从容器里面拷贝文件到宿主机,或相反的过程就可以用到cp命令 [图片] container prune随着使用docker时间越长,停止状态下的容器会越来越多,这些都会占据磁盘空间 [图片] image prune未被打标签的镜像可以用image prune命令清理 [图片] system prune/df如果你觉得刚刚两条命令执行起来麻烦,可以用docker system prune一条命令搞定 另外用system df查看docker磁盘空间 [图片] 4. 实战了解了docker基础知识后,可进入相对实战的环节 本地开发 常见问题 架构 优化 本地开发 我们的项目使用了很多服务,如redis/mysql/mongodb等等,如果一个个运行起来,还加上配置,容易出手,也比较麻烦 kitematic:与使用命令行管理本地容器相比,你更想使用图形工具对容器管理,官方推出的容器管理工具,通过它可以查找镜像、创建容器、配置、启停容器等管理 [图片] [图片] 这是配置容器端口和宿主机端口,目录,网络等映射界面 [图片] docker-composecompose定位是“定义和运行多个docker容器的应用”,前身fig,目前仍然兼容fig格式的模板文件。 一条命令可以把一个复杂的应用启动起来 日常工作中,经常碰到多个容器相互完成某项任务 [图片] docker-compose示例1 默认模板文件名叫docker-compose.yml,结构很简单,每个顶级元素为服务名称,次级信息为配置信息 这里使用了redis/mongodb/mysql/nginx镜像,分别给它们映射了本地目录、端口、密码等信息,nginx镜像需要使用redis/mysql等服务,用links命令连接进来 [图片] docker-compose示例2 如果在本地开发,每个项目都可以像之前说的那样配置,这里提供了另外一种做法 我把公共的资源在一开始就启动,每个项目里只启动nginx镜像并关联其它的服务即可 公共服务compose [图片] 项目compose [图片] 常见问题 主进程:docker启动第一个进程称主进程,就是id为1的进程,这个进程退出就意味着容器退出,所以想要使docker作为服务使用,这个进程是不能退出的 expose命令是声明暴露的端口,运行时用-P才会生效。一般ports命令是做真正的端口映射,比较常用 架构 安装了docker的主机,一般在一个私有网络上 1、调用docker客户端可以从守护进程获取信息或发送指令 2、docker守护进程使用http协议接收来自docker客户端的请求 3、私有docker注册中心存储docker镜像 4、docker hub是由docker公司运营的最大的公共注册中心 互联网上也存在其他公共的注册中心 调用 Docker客户端可以从守护进程获取信息或给它发送指令。守护进程是一个服务器,它使用 HTTP协议接收来自客户端的请求并返回响应。相应地,它会向其他服务发起请求来发送和接收镜像,使用的同样是 HTTP协议。该服务器将接收来自命令行客户端或被授权连接的任何人的请求。守护进程还负责在幕后处理用户的镜像和容器,而客户端充当的是用户与 REST风格 API之间的媒介。 理解这张图的关键在于,当用户在自己的机器上运行 Docker时,与其进行交互的可能是自己机器上的另一个进程,或者甚至是运行在内部网络或互联网上的服务。 [图片] 优化 使用小镜像:一般来说,使用小的镜像都相对比较优秀,如官方的镜像基本上都有基于alpine的镜像 事后清理:删除镜像里软件包或一些临时文件,减小镜像大小 命令写一行:多个命令尽量写在一起有助于减少层数,也会减少镜像的大小 脚本安装:使用脚本进行初始化时,可以有效减少dockerfile的命令,同时带来另外的问题,可读性不好并且构建镜像时缓存不了 扁平化镜像:构建镜像过程中,可能会涉及到一些敏感信息,或者用了上面的办法镜像依然很大,可以试试这个办法 docker export 容器名或容器id | docker import - 镜像标签 multi-stage:从docker 17.05版本开始,docker支持multi-stage(多阶段构建),特别适合编译型语言,如我在一个镜像下编译,在另外一个很小的系统运行,如下图,go项目在golang环境下编译,在alpine环境下运行 [图片]
2019-03-27 - 如何打造一份0 bug 的js代码!
说到程序开发,bug总是如影随形,开发过程中50%的时间在debug,30%是在修之前发布了的bug,毫不奇怪。 如何把bug见到最少,甚至是0 bug呢?看似遥不可及,但实际是可以追求的,方法就是完整科学的测试,事实上,测试和使用是证明代码没bug的唯一方式。 测试又分为白盒测试跟黑盒测试,一般来说,产品上线前经过测试同学测试做的功能测试都是黑盒测试,毕竟测试同学不可能了解所有的代码,而能做白盒测试的,都是最了解代码的人,也就是写代码的程序员。而只有最了解代码的人也才能写出最完善的测试用例。而单元测试就是白盒测试里面最重要的方法之一。 很多程序员,特别是前端程序员是没有写单元测试的习惯。诚然,写单元测试其实是比较费劲的事情,特别在前端领域,涉及大量ui及交互操作,写单元测试尤为困难。但是近年来js模块化越演越烈,模块化的同时也使得写单元测试变得更容易了。 本文就致力于探讨在当前的开发环境下做单元测试的一些实践方法。 先介绍下被测试的对象 —— 一个js工具库,这个公共库有几个比较重要的标签:ES6,移动端,浏览器端。 从最简单的讲起吧,js的测试框架其实不算少,比较有名的有mocha,Jasmine,Jest等,基本用法都比较简单明了,看看官方文档就大概能写出来一些测试用例了。下面我主要使用比较强大mocha来作为主要的测试框架。 下面是我项目中的一份test.js: [代码]import * as lib from './index'; import chai from 'chai'; let expect = chai.expect; describe('testcase',function(){ it('single one',function(){ let a = 'aer'; expect(a).to.be.a('string'); }); it('test /common/getUrlParam',function(){ let ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com#a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','https://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('b','http://vip.qq.com?a=1&b=2'); expect(ret).to.equal('2'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1#a=2'); expect(ret).to.equal('2'); }); }); [代码] ES6 这份test.js包含两个测试用例,第一用例是用于测试的,单纯试用下测试框架跟断言库的功能,第二个用例是对库文件中一个模块的一个方法的测试用例,估计前端大佬们看看方法名大概都能看懂是什么东西,我就不多说了。写好test.js后就跑了试试看,运行mocha,然后马上就报错了: [代码]JUSTYNCHEN-MC0:gxh-lib-es6 justynchen$ ./node_modules/.bin/mocha /path/to/your/project/somelib/test.js:1 (function (exports, require, module, __filename, __dirname) { import * as lib from './index'; ^ SyntaxError: Unexpected token * at new Script (vm.js:79:7) at createScript (vm.js:251:10) at Object.runInThisContext (vm.js:303:10) at Module._compile (internal/modules/cjs/loader.js:656:28) ... [代码] 我当前的node版本是v10.13.0(对,近日node的LTS版本已经升级到10.x了,没升的同学赶紧玩玩吧),按理说是默认支持import的。仔细一看报错,原来mocha不是直接执行test.js,而是把test.js的内容放到了一个沙箱里面执行的。那就有点蛋疼了,就算node新版本已经支持了也没法直接使用。第一想法就是先把代码编译了,然后再做测试,可是测试的代码都是编译后的代码,就算测试出什么问题,还要经过sourcemap才能找到源码中出错的位置,想想都蛋疼。 官方当然是不会这么蠢的,稍微找了下官方的方案,不难找到对es6的支持。babel提供了一个register,给到不同的应用去做转换,mocha同样也可以使用这个register先转换然后再跑。命令就变成了这个: [代码]mocha --require babel-core/register [代码] 同时,package.json里面的选项也要加上babel的选项: [代码]"babel": { "presets": [ "stage-3", "latest" ], }, [代码] 当然,相关的包(babel,babel-core)也要同时装上,大家都懂的后面我就不提了,缺啥装啥就对了,后面提到的工具如没特别说明都是指npm包。 到这里相关的资料还比较好找,接下来就是干货了。 browser 运行上面改造过的mocha命令,是不是就ok了呢?当然没那么顺利,这里遇到了第二个坑: [代码]./node_modules/.bin/mocha --require babel-core/register /path/to/your/project/somelib/common/cache.js:15 var storage = window[storageType]; ^ ReferenceError: window is not defined at initStorage (/Users/justynchen/....../cache.js:10:16) at Object.<anonymous> (/Users/justynchen/....../cache.js:41:16) at Module._compile (internal/modules/cjs/loader.js:688:30) [代码] 前面也说了这份库是给移动端浏览器用的,其中就免不了使用一些浏览器的API,这些API在node里面都是不存在的。解决方案有两个: 直接不测试使用了浏览器API的代码,使用前先做检测并return掉。 找一个模拟浏览器的环境,让浏览器的API也能正常执行。 方案一是我们不愿意看到的,特别是一份浏览器用的库,不测试浏览器相关的特性那跟咸鱼有什么区别【手动狗头】。那就按方案二的思路想走吧,想到node模拟浏览器的环境,脑中浮现的第一个名词估计大部分人跟我都一样 —— electron。作为业界最著名的“没有界面的浏览器”,用在这里再合适不过了。但是该怎么用呢,稍作搜索,果然已经有前人做了相应的工作,有一个electron-mocha的工具刚好就是把这两个东西合了起来。 然后命令就变成了这样: [代码]electron-mocha --renderer --require babel-core/register [代码] 然后终于得到了我们想要的结果: [代码]JUSTYNCHEN-MC0:somelib justynchen$ ./node_modules/.bin/electron-mocha --renderer --require babel-core/register testcase ✓ single one ✓ test /common/getUrlParam 2 passing (37ms) [代码] 完美~(请自动脑补金星脸) 可是,就这么完了是否有点意犹未尽? 是的,就是缺了点什么东西,说好的0 bug呢,写了测试用例就能保证0 bug了么?肯定不是的,如果有的地方就是有bug,只是用例没写好,并没有覆盖到有问题的地方怎么办?只有写“全”了的测试用例才能保证0 bug。如何确保用例写全了呢?请往下看。 代码测试覆盖率 这里引入一个概念,叫代码测试覆盖率,大概意思就是说,你的测试用例到底覆盖了多少的代码。理想情况下,肯定只有100%覆盖所有代码的用例,才能说自己经过测试的代码是0 bug的,当然现实中100%总是很难的,一般覆盖到90%以上已经是比较理想的情况了。 JS也有统计代码测试覆盖率的库 —— Istanbul。这库名也很有意思,库名直译是伊斯坦布尔,没错,就是那个正常中国人可能名字都没听说过的中东城市。这个地方有个特产是毯子,然后这个库的作者就想,覆盖就是毯子该做的事情嘛,脑洞一开就把库名起作Istanbul了。 继续“稍微读下文档”,哦,原来这个库有一个命令行工具,nyc,装上然后放到执行命令的前面就能做覆盖率统计了。然后就有了下面的命令 [代码]nyc --reporter=lcov --reporter=text electron-mocha --renderer --require babel-core/register [代码] 其中reporter是定制化报告的内容,默认是text,lcov就是生成一个网页版的覆盖率报告。 然而,跑完之后是酱婶儿的 [代码]JUSTYNCHEN-MC0:somelib justynchen$ ./node_modules/.bin/nyc ./node_modules/.bin/electron-mocha --renderer --require babel-core/register testcase ✓ single one ✓ test /common/getUrlParam ✓ aidMaker test 3 passing (18ms) ----------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ----------|----------|----------|----------|----------|-------------------| All files | 0 | 0 | 0 | 0 | | ----------|----------|----------|----------|----------|-------------------| [代码] 苍天大地啊,为啥啥都没有。。。。 这里就又开始苦逼的查资料环节,不得不吐槽一下,这个资料是真不好查,中文资料没有就算了,反正早习惯了,文档翻遍了还是没有。。。那就过分了。然后查了下别的资料,基本都是mocha跟Istanbul一起用的,也没有electron-mocha相关的。 最后还是找到了github的issue里面,果然有人是跟我有类似问题,找了几个提了没啥回音的,终于找到一个maintainer的回复。里面指向了一个插件 —— babel-plugin-istanbul。 皇天不负有心人,得益于之前已经引入了babel,这里只要加上这个插件就ok了,照例先装包,package.json里面的babel配置加上这个参数 [代码]"babel": { "presets": [ "stage-3", "latest" ], "env": { "test": { "plugins": [ "istanbul" ] } } }, [代码] 然后按照插件的README改一下命令,最终得到了这个 [代码]cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text electron-mocha --renderer --require babel-core/register [代码] 然后一跑。。。发现,x你x的。。。跟前面的输出一毛一样,啥的没有!!! 冷静一下,再回头看看maintainer回复的原话 Basically, yes. Note that if you’re using babel already you can now just use the excellent istanbul-plugin to instrument your code. If you do this, you really only need to write out the __coverage__ object after the tests have run. (I write them out for both renderer and main thread tests, then combine them using nyc report from the command line). 里面提到了一个奇怪的参数__coverage__,又查一波资料,在这个项目的某些commit comment里面看到这个__coverage__的蛛丝马迹,这东西好像是一个全局参数,那这个参数有啥用咧?管他有用没用,打出来看看再说,然后就在test.js里面吧这个参数打了下,发现,卧槽,还真有,而且里面不就是覆盖率的数据么???? 嗯,有数据。。。怎么生成报告呢?作者写的语焉不详。。。啥叫write out这个参数然后配合nyc report命令就能用了,write到啥地方啊,咋配合啊!!! 这个时候就想到了Istanbul的一些特性,其实它是会在测试后生成一个.nyc_output的文件夹的,打开一看,里面不就是一些json么!那是不是直接write进去就好了呢?文件该叫啥名字咧,原来的文件都是hash命名的,这hash哪来的呀。不管了写了再说,然后得到如下test.js [代码]import * as lib from './index'; import chai from 'chai'; import fs from 'fs'; let expect = chai.expect; describe('testcase',function(){ it('single one',function(){ let a = 'aer'; expect(a).to.be.a('string'); }); it('test /common/getUrlParam',function(){ let ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com#a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','https://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('b','http://vip.qq.com?a=1&b=2'); expect(ret).to.equal('2'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1#a=2'); expect(ret).to.equal('2'); }); after(function() { fs.writeFileSync('./.nyc_output/coverage.json',JSON.stringify(__coverage__)); }); }); [代码] 终于生效了 [代码]JUSTYNCHEN-MC0:somelib justynchen$ tnpm run test > @tencent/somelib@1.0.1 test /path/to/your/project/..... > cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text electron-mocha --renderer --require babel-core/register testcase ✓ single one ✓ test /common/getUrlParam 3 passing (26ms) ----------------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ----------------------|----------|----------|----------|----------|-------------------| All files | 5.96 | 3.3 | 3.6 | 6.02 | | gxh-lib-es6 | 0 | 0 | 0 | 0 | | index.js | 0 | 0 | 0 | 0 | | gxh-lib-es6/business | 16.89 | 8.84 | 3.45 | 16.89 | | aid-maker.js | 80 | 66.67 | 100 | 80 | 325,340,341,344 | cgi-handler.js | 0 | 0 | 0 | 0 |... 57,159,160,161 | index.js | 0 | 0 | 0 | 0 | | pay.js | 0 | 0 | 0 | 0 |... 86,88,89,91,97 | .... [代码] 至此,终于可以说出那句 完美~ 附录 整体架构图 [图片] 后记:整个单元测试的技术其实都没什么困难的,基本上都有库可以用,主要把时间都花在了查询资料上面。写本文的时候好像是遇到问题马上就找到了解决方案,其实真实情况是,几乎遇到每个坑都会试了至少一两个走不通的方案,最后才找到正确的方案的,所以对于不经常关注社区的人来说,单靠文档是很难解决所有的问题的。单从前端领域来看,前端的技术日新月异,再完善的文档都很快会跟不上发展的步伐,还是要靠多关注社区的动向,甚至多参与社区的讨论和建设才不至于在需要用某些技术的时候无从下手。
2019-03-29 - 小程序自定义组件知多少
自定义组件 why 代码的复用 在起初小程序只支持 Page 的时候,就会有这样蛋疼的问题:多个页面有相同的组件,每个页面都要复制粘贴一遍,每次改动都要全局搜索一遍,还说不准哪里改漏了就出翔了。 组件化设计 在前端项目中,组件化是很常见的方式,某块通用能力的抽象和设计,是一个必备的技能。组件的管理、数据的管理、应用状态的管理,这些在我们设计的过程中都是需要去思考的。当然你也可以说我就堆代码就好了,不过一个真正的码农是不允许自己这么随便的! 所以,组件化是现代前端必须掌握的生存技能! 自定义组件的实现 一切都从 Virtual DOM 说起 前面《解剖小程序的 setData》有讲过,基于小程序的双线程设计,视图层(Webview 线程)和逻辑层(JS 线程)之间通信(表现为 setData),是基于虚拟 DOM 来实现数据通信和模版更新的。 自定义组件一样的双线程,所以一样滴基于 Virtual DOM 来实现通信。那在这里,Virtual DOM 的一些基本知识(包括生成 VD 对象、Diff 更新等),就不过多介绍啦~ Shadow DOM 模型 基于 Virtual DOM,我们知道在这样的设计里,需要一个框架来支撑维护整个页面的节点树相关信息,包括节点的属性、事件绑定等。在小程序里,Exparser 承担了这个角色。 前面《关于小程序的基础库》也讲过,Exparser 的主要特点包括: 基于 Shadow DOM 模型 可在纯 JS 环境中运行 Shadow DOM 是什么呢,它就是我们在写代码时候写的自定义组件、内置组件、原生组件等。Shadow DOM 为 Web 组件中的 DOM 和 CSS 提供了封装。Shadow DOM 使得这些东西与主文档的 DOM 保持分离。 简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装 HTML 组件(类似 vue 组件,将 html,css,js 独立部分提取)。 例如我们定义了一个自定义组件叫[代码]<my-component>[代码],你在开发者工具可以见到: [图片] [代码]#shadow-root[代码]称为影子根,DOM 子树的根节点,和文档的主要 DOM 树分开渲染。可以看到它在[代码]<my-component>[代码]里面,换句话说,[代码]#shadow-root[代码]寄生在[代码]<my-component>[代码]上。[代码]#shadow-root[代码]可以嵌套,形成节点树,即称为影子树(Shadow Tree)。 像这样: [图片] Shadow Tree 拼接 既然组件是基于 Shadow DOM,那组件的嵌套关系,其实也就是 Shadow DOM 的嵌套,也可称为 Shadow Tree 的拼接。 Shadow Tree 拼接是怎么做的呢?一切又得从模版引擎讲起。 我们知道,Virtual DOM 机制会将节点解析成一个对象,那这个对象要怎么生成真正的 DOM 节点呢?数据变更又是怎么更新到界面的呢?这大概就是模版引擎做的事情了。 《前端模板引擎》里有详细描述模版引擎的机制,通常来说主要有这些: DOM 节点的创建和管理:[代码]appendChild[代码]/[代码]insertBefore[代码]/[代码]removeChild[代码]/[代码]replaceChild[代码]等 DOM 节点的关系(嵌套的处理):[代码]parentNode[代码]/[代码]childNodes[代码] 通常创建后的 DOM 节点会保存一个映射,在更新的时候取到映射,然后进行处理(通常包括替换节点、改变内容[代码]innerHTML[代码]、移动删除新增节点、修改节点属性[代码]setAttribute[代码]) 在上面的图我们也可以看到,在 Shadow Tree 拼接的过程中,有些节点并不会最终生成 DOM 节点,例如[代码]<slot>[代码]这种。 但是,常用的前端模版引擎,能直接用在小程序里吗? 双线程的难题 自定义组件渲染流程 双线程的设计,给小程序带来了很多便利,安全性管控力都拥有了,当然什么鬼东西都可以比作一把双刃剑,双线程也不例外。 我们知道,小程序分为 Webview 和 JS 双线程,逻辑层里是没法拿到真正的 DOM 节点,也没法随便动态变更页面的。那在这种情况下,我们要怎么去使用映射来更新模版呢(因为我们压根拿不到 Webview 节点的映射)? 所以在双线程下,其实两个线程都需要保存一份节点信息。这份节点信息怎么来的呢?其实就是我们需要在创建组件的时候,通过事件通知的方式,分别在逻辑层和视图层创建一份节点信息。 同时,视图层里的组件是有层级关系的,但是 JS 里没有怎么办?为了维护好父子嵌套等节点关系,所以我们在 逻辑层也需要维护一棵 Shadow Tree。 那么我们自定义组件的渲染流程大概是: 组件创建。 逻辑层:先是 wxml + js 生成一个 JS 对象(因为需要访问组件实例 this 呀),然后是 JS 其中节点部分生成 Virtual DOM,拼接 Shadow Tree 什么的,最后通过底层通信通知到 视图层 视图层:拿到节点信息,然后吭哧吭哧开始创建 Shadow DOM,拼接 Shadow Tree 什么的,最后生成真实 DOM,并保留下映射关系 组件更新。 这时候我们知道,不管是逻辑层,还是视图层,都维护了一份 Shadow Tree,要怎么保证他们之间保持一致呢? 让 JS 和 Webview 的组件保持一致 为了让两边的 Shadow Tree 保持一致,可以使用同步队列来传递信息。(这样就不会漏掉啦) 同步队列可以,每次变动我们就往队列里塞东西就好了。不过这样还会有个问题,我们也知道 setData 其实在实际项目里是使用比较频繁的,要是像 Component 的 observer 里做了 setData 这类型的操作,那不是每次变动会导致一大堆的 setDate?这样通信效率会很低吧? 所以,其实可以把一次操作里的所有 setData 都整到一次通信里,通过排序保证好顺序就好啦。 Page 和 Component Component 是 Page 的超集 事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含[代码]usingComponents[代码]定义段。 来自官方文档-Component 所以,基于 Component 是 Page 的超集,那么其实组件的渲染流程、方式,其实跟页面没多大区别,应该可以一个方式去理解就差不多啦。 页面渲染 既然页面就是组件,那其实页面的渲染流程跟组件的渲染流程基本保持一致。 视图层渲染,可以参考7.4 视图层渲染说明。 结束语 其实很多新框架新工具出来的时候,经常会让人眼前一亮,觉得哇好厉害,哇好高大上。 但其实更多时候,我们需要挖掘新事物的核心,其实大多数都是在原有的事物上增加了个新视角,从不一样的视角看,看到的就不一样了呢。作为一名码农,我们要看到不变的共性,变化的趋势。
2019-04-01 - 揭开JS无埋点技术的神秘面纱
一、背景 相信很多人都接触过**“埋点”这个概念,无论是前端还是后端开发,我们都可以使用这门技术来生产出一些运营性质的原始数据(接口耗时、程序安装/启动、用户交互行为等等),然后分析它们得到一些抽象指标(例如留存率、转化率),进而决定产品运营或者代码优化的方向。现在业界有许多比较知名数据平台,比如Google Analytics、Facebook Pixel、Mixpanel、GrowingIO、诸葛IO、TalkingData、神策数据等数不胜数一大票,这些平台有单纯做数据分析的,也有服务于特定领域例如广告监测转化的,都提供了多端(Android、iOS、Web、小程序、ReactNative)的埋点SDK和比较全面的BI服务。这一两年,不少平台都开始宣传一种叫“无埋点”**的技术,下面以Web端为例,揭开它的神秘面纱。 二、什么是无埋点? **“无埋点”在国外一些平台被叫做[代码]Codeless Tracking[代码],顾名思义就是可以写“更少”的埋点代码。而“代码埋点”**一般需要开发人员编写代码,监听某个html元素的产生的事件,然后调用上报数据的接口,发送数据。而无埋点则可以由非技术人员(例如运营、产品),在可视化的工具中作出配置,然后就可以将html元素中产生的行为上报到后台。下面是Mixpanel平台的可视化工具的截图。 [图片] 在这个工具里,需要首先输入页面的url,页面加载完成后,会出现可视化配置的工具条。点击创建事件,就可以进入元素选择模式,用鼠标点击页面上的某个元素(例如button、a这些element),就可以在弹出的对话框里面,设置这个事件的名称(比如叫[代码]TEST[代码])。保存这个配置之后,如果页面在浏览器中被浏览,刚才配置的那个按钮发生点击时,就会向后台上报一个[代码]TEST[代码]事件。我们还可以设置上报[代码]TEST[代码]事件的时候,带上一些属性(properties),这些属性同样也是在页面中用鼠标去选择,然后保存起来的。 看到这里,首先从产品层面上,我们比较具体的了解到“无埋点”到底是干什么的了,无埋点就是用可视化工具配置页面中需要被监测的元素,并设置这个元素产生行为的时候需要上报的数据。但是还有非常关键的一点必须提到,要让“无埋点”工作起来,页面里面还是必须嵌入了一段JS SDK的基础代码,只是不需要再去调用SDK具体的数据上报接口罢了。 所以,“无埋点”技术的关键是: 操作可视化配置工具,保存配置 SDK基础代码如何根据配置上报行为 下面介绍一下如何实现这两个关键。 三、关键技术 1. 基础代码 和代码埋点一样,要让“无埋点”工作起来,网页里也必须有一段“基础代码”。 [代码]<!-- start Mixpanel --><script type="text/javascript">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+"=([^&]*)")))?l[1]:null};g&&c(g,"state")&&(i=JSON.parse(decodeURIComponent(c(g,"state"))),"mpeditor"===i.action&&(b.sessionStorage.setItem("_mpcehash",g),history.replaceState(i.desiredHash||"",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(".");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments, 0)))}}var d=a;"undefined"!==typeof f?d=a[f]=[]:f="mixpanel";d.people=d.people||[];d.toString=function(b){var a="mixpanel";"mixpanel"!==f&&(a+="."+f);b||(a+=" (stub)");return a};d.people.toString=function(){return d.toString(1)+".people (stub)"};k="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" "); for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn4.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn4.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn4.mxpnl.com/libs/mixpanel-2-latest.min.js";c=e.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]); mixpanel.init("46042714e64a7536dde6f02af1aec923");</script><!-- end Mixpanel --> [代码] 上面是Mixpanel平台的基础代码,不同平台家的这段基础代码,大同小异,都是一段IIFE形式的、压缩过的js代码,执行完成之后,在head里面插入了一个新的script标签,异步去下载真正的核心SDK代码下来工作。所以并不是基础代码可以根据配置上报行为,而是基础代码会下载一段**“更大”**的SDK核心代码,这段代码才是SDK真正的功能实现。 这样子做的好处是,基础代码很短,加载的时候不会影响到网页的性能,而且核心SDK代码的更新也不需要用户去更新这段基础代码。 2. 页面的唯一标识 在配置元素行为的时候,需要唯一标识一个页面,这样才能保证A页面的配置,不会下发给在B页面,不会导致B页面产生出A页面里配置的行为。在Web里面标识页面靠的是url,url由protocol、domain、port、path和参数组成,存储配置的时候要将url的参数提出来再存。而url的参数位置是可以变化的,比如urlA([代码]http://a.b.com/c.html?pa=1&pb=2[代码])和urlB([代码]http://a.b.com/c.html?pb=2&pa=1[代码])虽然[代码]urlA !== urlB[代码],但是其实它们是一个页面。 3. 元素的唯一标识 唯一标识页面后,接下来就要唯一标识页面里面的元素,这样才能保证A页面中配置的元素A1可以被SDK找到,从而监听它产生的事件。 在html里面,元素是以DOM Tree组织的,如果沿着元素A1出发,一直向上记录它的parent和它在parent中的index,直到根节点body,那么就可以得到元素A1在DOM Tree中的唯一路径。 html的元素还会拥有很多属性,例如css class、id可以用来定位元素。通过Chrome开发者工具可以看到Mixpanel的可视化工具在配置元素的时候,使用的是https://github.com/Autarc/optimal-select这个库来生成element的唯一标识的。而Github上还有https://github.com/rowthan/whats-element这样的库,也可以生成元素在DOM Tree中的唯一标识。 此外,还有平台在标识元素的时候,采用了[代码]xpath[代码],这也是一个思路。 4. 如何查找元素 上面说到元素可以有唯一标识,那么有了唯一标识,就可以利用它的原理,找到这个元素。有一个很好用的API是[代码]document.querySelector()[代码],这个API可以根据CSS选择器找到对应的元素。此外,根据元素的标识方法,还可以使用[代码]document.getElementById()[代码]、[代码]document.getElementByName()[代码]来实现元素的查找。 这里需要重点强调的是,如果页面在配置完成之后又发生了修改,导致DOM Tree发生变化,此时需要被监测的元素的唯一标识可能也会发生改变。很可能导致根据之前的配置无法找到该元素了,或者找到的并不是我们希望监测的元素,从而导致产生的事件数量发生比较明显的变化。为了数据的稳定性和准确性,应该设有相应的监测告警处理这种case,并提示用户去重新配置页面。我个人认为这是无埋点最大的缺点。 5. 标记元素时的高亮效果和可视化交互实现 这是一个比较细节的点,其实熟悉js的大牛们都知道,有无数种方式去实现鼠标移动到元素上时的[代码]类hover[代码]效果,点击元素后弹出一个对话框,让用户输入配置的信息也so easy。但是我想说的是,一旦我们采用向页面中动态添加元素的方式去实现可视化工具的交互界面,那么有可能会破坏掉页面原来的DOM Tree结构。从而导致生成元素唯一标识的时候出现误差,所以这里必须要好好处理,保证生成的元素标识不会受到影响。 Mixpanel采用了[代码]CustomElement[代码]和[代码]ShadowDOM[代码],把可视化工具所有的功能都用自定义的[代码]Web Component[代码]实现了,虽然目前只有Chrome支持[代码]Web Component[代码],但是真的有点叼。。这样自定义的元素和交互不会对用户的网页DOM产生影响。当然,如果你的可视化工具实现做的很轻,比如只是将用户的网页放在一个[代码]iframe[代码]里面,大部分交互都交给iframe的parent页面去处理,那也可以在配置的时候,最小程度的破坏用户的网页了。 6. 配置工具中如何控制页面的跳转 当进入可视化配置状态时,我们可以让用户点击一个元素,然后弹一个对话框,让用户对这个元素进行配置。此时,如果这个元素本身的[代码]click[代码]行为是页面跳转呢?我们应该怎么处理? 这里本质上是一个交互设计的问题。在可视化配置工具中,应该有两种基本交互操作。一种是让用户选中某一个元素,进行配置;另一种,是让用户可以触发页面原有的行为。 为什么要有第二种交互?因为我们的工具肯定要支持用户进行二级页面的可视化配置对不对?或者说,用户的页面中可能会弹出一个对话框,对话框里面有一个按钮,用户对监测这个按钮,对它做配置,对不对?简单来说,就是用户页面中原有的点击行为,可能会导致页面结构产生变化,例如跳转,页面内弹出对话框等等。 那问题就好解了,除了点击,再设计一种交互来支持用户网页中原有的点击行为不就好了。用“右键点击”或者“按住shift+点击”之类都可以。反正不要再和网页默认的交互很容易产生冲突的方式就行。 最后再提一下,之前想很久没有想明白,如何能够能防止用户点击的时候页面产生跳转。后来才知道,DOM的事件流分三个阶段:捕获、目标、冒泡。所以为了避免用户的点击产身页面跳转,给document在捕获阶段加一个listener,拦截掉这个事件的继续分发就行了。 [图片] 简单的示例代码如下: [代码]document.addEventListener('click', e => { // 如果是按住shift的点击,那么保持原有的行为 if (e.shiftKey) { return; } // 如果是单纯的点击,那么拦截分发 e.preventDefault(); e.stopImmediatePropagation(); // 获取元素的唯一标识,然后让用户进行配置等等 this._selectElement(e.target); }, true); // useCapture必须为true [代码] 四、总结 可以看到“无埋点”并不是零侵入,用户的网页中依然需要加载SDK的代码(除非你是浏览器厂商,可以在加载网页的时候,给网页加inject基础代码)。只是每一个行为事件的上报代码不需要开发人员手动编写,而是由运营人员用可视化工具配置,所以叫它**“可视化埋点”**也许更加合适。我们知道数据采集是数据分析的基础和先决条件,数据采集做不好,其他的东西都是空中楼阁。 这里可以小结一下“无埋点”技术的优劣。无埋点的好处是技术成本低,对用户非常友好,不需要重新部署,配置完成就可以生效。但是其缺点也非常明显,不具有代码埋点的灵活性和深度,只能采集到用户肉眼可见的数据,无法获取内存里的数据,同时也无法适应页面结构的变化,所以在实际生产中,要选择性地在合适的地方使用无埋点技术。 多扯一点产品设计和技术方案的选择,产品上是否可以支持采集内存数据呢?当然可以,比如微信小程序的“自定义分析”,就可以支持上报页面[代码]data[代码]下面的属性,这时虽然同样是可视化配置,运营人员肯定不会知道代码里面的变量名字,必须得有开发人员参与配置才行。关于页面结构发生变化之后的数据丢失,也是有方案可以破的。比如Mixpanel平台的Codeless Tracking,实际上采集了页面中所有页面的点击事件上报,然后在后台再去根据用户的配置计算转化数量。这样做的好处就是如果页面变化后,用户接到告警,修改了配置,那么用于数据上报方案是全量的,所以平台是由能力将过去的数据回溯出来的。而上面我们说的根据配置下发,查找监测指定元素,再上报数据的方案属于按需上报,数据出现误差是无法回溯的。不过全量上报数据大家也知道,太不友好了,这个数据量太大,不仅前端消耗资源多,如果为了做数据回溯,后台的存储压力也会加大,而存储的数据大部分还是无效的,这个成本有点高了。 五、参考资料 JS埋点技术分析 https://github.com/Autarc/optimal-select https://github.com/rowthan/whats-element https://www.zhihu.com/question/38000812
2019-04-01 - 开发者工具 Nightly Build 近期更新:云调用、云函数本地调试、全新云开发控制台
开发者工具 Nightly Build 版本 近期包含了如下云开发能力的更新: 云调用 云调用 是云开发提供的基于云函数使用小程序开放接口的能力,目前覆盖服务端调用的场景,后续将会陆续开放开放数据调用、消息推送、支付等其他多种使用场景。 云调用需要在云函数中通过 wx-server-sdk 使用。在云函数中调用服务端接口不再需要换取 access_token,只要是在从小程序端触发的云函数中发起的云调用都经过微信自动鉴权,可直接在 SDK 中发起调用。目前支持云调用的服务端开放接口列表可在 服务端接口列表页 查看。 想快速体验云调用和查看代码示例,可下载最新 Nightly Build 版的开发者工具,新建云开发快速启动模板项目,在其中新增了云调用的部分: [图片] 云函数本地调试 IDE 新增了云函数本地调试功能,方便开发者在本地进行云函数调试,提高开发效率。云函数开发可以不再需要频繁上传测试,只需用 IDE 在本地调试完成再上传到现网验证。开发者可在云函数根目录右键选择本地调试来开启本地调试界面。 详细内容可参考文档。 [图片] 全新的云开发控制台 云开发控制台经过全新设计和改版,优化交互和视觉体验,功能分类更加清晰、各项功能更加易用。 [图片] [图片] 可以在开发者工具 Nightly Build 版本 下载并体验上述功能。如果有遇到任何使用问题可以在本帖提出。
2019-04-02 - Service Worker学习与实践(一)——离线缓存
什么是[代码]Service Worker[代码] [代码]Service Worker[代码]本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步[代码]API[代码]。 [代码]Service Worker[代码]的本质是一个[代码]Web Worker[代码],它独立于[代码]JavaScript[代码]主线程,因此它不能直接访问[代码]DOM[代码],也不能直接访问[代码]window[代码]对象,但是,[代码]Service Worker[代码]可以访问[代码]navigator[代码]对象,也可以通过消息传递的方式(postMessage)与[代码]JavaScript[代码]主线程进行通信。 [代码]Service Worker[代码]是一个网络代理,它可以控制[代码]Web[代码]页面的所有网络请求。 [代码]Service Worker[代码]具有自身的生命周期,使用好[代码]Service Worker[代码]的关键是灵活控制其生命周期。 [代码]Service Worker[代码]的作用 用于浏览器缓存 实现离线[代码]Web APP[代码] 消息推送 [代码]Service Worker[代码]兼容性 [代码]Service Worker[代码]是现代浏览器的一个高级特性,它依赖于[代码]fetch API[代码]、[代码]Cache Storage[代码]、[代码]Promise[代码]等,其中,[代码]Cache[代码]提供了[代码]Request / Response[代码]对象对的存储机制,[代码]Cache Storage[代码]存储多个[代码]Cache[代码]。 [图片] 示例 在了解[代码]Service Worker[代码]的原理之前,先来看一段[代码]Service Worker[代码]的示例: [代码]self.importScripts('./serviceworker-cache-polyfill.js'); var urlsToCache = [ '/', '/index.js', '/style.css', '/favicon.ico', ]; var CACHE_NAME = 'counterxing'; self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; } return fetch(event.request); }) ); }); self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); [代码] 下面开始逐段逐段地分析,揭开[代码]Service Worker[代码]的神秘面纱: [代码]polyfill[代码] 首先看第一行:[代码]self.importScripts('./serviceworker-cache-polyfill.js');[代码],这里引入了Cache API的一个polyfill,这个[代码]polyfill[代码]支持使得在较低版本的浏览器下也可以使用[代码]Cache Storage API[代码]。想要实现[代码]Service Worker[代码]的功能,一般都需要搭配[代码]Cache API[代码]代理网络请求到缓存中。 在[代码]Service Worker[代码]线程中,使用[代码]importScripts[代码]引入[代码]polyfill[代码]脚本,目的是对低版本浏览器的兼容。 [代码]Cache Resources List[代码] And [代码]Cache Name[代码] 之后,使用一个[代码]urlsToCache[代码]列表来声明需要缓存的静态资源,再使用一个变量[代码]CACHE_NAME[代码]来确定当前缓存的[代码]Cache Storage Name[代码],这里可以理解成[代码]Cache Storage[代码]是一个[代码]DB[代码],而[代码]CACHE_NAME[代码]则是[代码]DB[代码]名: [代码]var urlsToCache = [ '/', '/index.js', '/style.css', '/favicon.ico', ]; var CACHE_NAME = 'counterxing'; [代码] [代码]Lifecycle[代码] [代码]Service Worker[代码]独立于浏览器[代码]JavaScript[代码]主线程,有它自己独立的生命周期。 如果需要在网站上安装[代码]Service Worker[代码],则需要在[代码]JavaScript[代码]主线程中使用以下代码引入[代码]Service Worker[代码]。 [代码]if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { console.log('成功安装', registration.scope); }).catch(function(err) { console.log(err); }); } [代码] 此处,一定要注意[代码]sw.js[代码]文件的路径,在我的示例中,处于当前域根目录下,这意味着,[代码]Service Worker[代码]和网站是同源的,可以为当前网站的所有请求做代理,如果[代码]Service Worker[代码]被注册到[代码]/imaging/sw.js[代码]下,那只能代理[代码]/imaging[代码]下的网络请求。 可以使用[代码]Chrome[代码]控制台,查看当前页面的[代码]Service Worker[代码]情况: [图片] 安装完成后,[代码]Service Worker[代码]会经历以下生命周期: 下载([代码]download[代码]) 安装([代码]install[代码]) 激活([代码]activate[代码]) 用户首次访问[代码]Service Worker[代码]控制的网站或页面时,[代码]Service Worker[代码]会立刻被下载。之后至少每[代码]24[代码]小时它会被下载一次。它可能被更频繁地下载,不过每[代码]24[代码]小时一定会被下载一次,以避免不良脚本长时间生效。 在下载完成后,开始安装[代码]Service Worker[代码],在安装阶段,通常需要缓存一些我们预先声明的静态资源,在我们的示例中,通过[代码]urlsToCache[代码]预先声明。 在安装完成后,会开始进行激活,浏览器会尝试下载[代码]Service Worker[代码]脚本文件,下载成功后,会与前一次已缓存的[代码]Service Worker[代码]脚本文件做对比,如果与前一次的[代码]Service Worker[代码]脚本文件不同,证明[代码]Service Worker[代码]已经更新,会触发[代码]activate[代码]事件。完成激活。 如图所示,为[代码]Service Worker[代码]大致的生命周期: [图片] [代码]install[代码] 在安装完成后,尝试缓存一些静态资源: [代码]self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); [代码] 首先,[代码]self.skipWaiting()[代码]执行,告知浏览器直接跳过等待阶段,淘汰过期的[代码]sw.js[代码]的[代码]Service Worker[代码]脚本,直接开始尝试激活新的[代码]Service Worker[代码]。 然后使用[代码]caches.open[代码]打开一个[代码]Cache[代码],打开后,通过[代码]cache.addAll[代码]尝试缓存我们预先声明的静态文件。 监听[代码]fetch[代码],代理网络请求 页面的所有网络请求,都会通过[代码]Service Worker[代码]的[代码]fetch[代码]事件触发,[代码]Service Worker[代码]通过[代码]caches.match[代码]尝试从[代码]Cache[代码]中查找缓存,缓存如果命中,则直接返回缓存中的[代码]response[代码],否则,创建一个真实的网络请求。 [代码]self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; } return fetch(event.request); }) ); }); [代码] 如果我们需要在请求过程中,再向[代码]Cache Storage[代码]中添加新的缓存,可以通过[代码]cache.put[代码]方法添加,看以下例子: [代码]self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // 缓存命中 if (response) { return response; } // 注意,这里必须使用clone方法克隆这个请求 // 原因是response是一个Stream,为了让浏览器跟缓存都使用这个response // 必须克隆这个response,一份到浏览器,一份到缓存中缓存。 // 只能被消费一次,想要再次消费,必须clone一次 var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // 必须是有效请求,必须是同源响应,第三方的请求,因为不可控,最好不要缓存 if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // 消费过一次,又需要再克隆一次 var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); }); [代码] 在项目中,一定要注意控制缓存,接口请求一般是不推荐缓存的。所以在我自己的项目中,并没有在这里做动态的缓存方案。 [代码]activate[代码] [代码]Service Worker[代码]总有需要更新的一天,随着版本迭代,某一天,我们需要把新版本的功能发布上线,此时需要淘汰掉旧的缓存,旧的[代码]Service Worker[代码]和[代码]Cache Storage[代码]如何淘汰呢? [代码]self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); [代码] 首先有一个白名单,白名单中的[代码]Cache[代码]是不被淘汰的。 之后通过[代码]caches.keys()[代码]拿到所有的[代码]Cache Storage[代码],把不在白名单中的[代码]Cache[代码]淘汰。 淘汰使用[代码]caches.delete()[代码]方法。它接收[代码]cacheName[代码]作为参数,删除该[代码]cacheName[代码]所有缓存。 sw-precache-webpack-plugin sw-precache-webpack-plugin是一个[代码]webpack plugin[代码],可以通过配置的方式在[代码]webpack[代码]打包时生成我们想要的[代码]sw.js[代码]的[代码]Service Worker[代码]脚本。 一个最简单的配置如下: [代码]var path = require('path'); var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const PUBLIC_PATH = 'https://www.my-project-name.com/'; // webpack needs the trailing slash for output.publicPath module.exports = { entry: { main: path.resolve(__dirname, 'src/index'), }, output: { path: path.resolve(__dirname, 'src/bundles/'), filename: '[name]-[hash].js', publicPath: PUBLIC_PATH, }, plugins: [ new SWPrecacheWebpackPlugin( { cacheId: 'my-project-name', dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', minify: true, navigateFallback: PUBLIC_PATH + 'index.html', staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], } ), ], } [代码] 在执行[代码]webpack[代码]打包后,会生成一个名为[代码]service-worker.js[代码]文件,用于缓存[代码]webpack[代码]打包后的静态文件。 一个最简单的示例。 [代码]Service Worker Cache[代码] VS [代码]Http Cache[代码] 对比起[代码]Http Header[代码]缓存,[代码]Service Worker[代码]配合[代码]Cache Storage[代码]也有自己的优势: 缓存与更新并存:每次更新版本,借助[代码]Service Worker[代码]可以立马使用缓存返回,但与此同时可以发起请求,校验是否有新版本更新。 无侵入式:[代码]hash[代码]值实在是太难看了。 不易被冲掉:[代码]Http[代码]缓存容易被冲掉,也容易过期,而[代码]Cache Storage[代码]则不容易被冲掉。也没有过期时间的说法。 离线:借助[代码]Service Worker[代码]可以实现离线访问应用。 但是缺点是,由于[代码]Service Worker[代码]依赖于[代码]fetch API[代码]、依赖于[代码]Promise[代码]、[代码]Cache Storage[代码]等,兼容性不太好。 后话 本文只是简单总结了[代码]Service Worker[代码]的基本使用和使用[代码]Service Worker[代码]做客户端缓存的简单方式,然而,[代码]Service Worker[代码]的作用远不止于此,例如:借助[代码]Service Worker[代码]做离线应用、用于做网络应用的推送(可参考push-notifications)等。 甚至可以借助[代码]Service Worker[代码],对接口进行缓存,在我所在的项目中,其实并不会做的这么复杂。不过做接口缓存的好处是支持离线访问,对离线状态下也能正常访问我们的[代码]Web[代码]应用。 [代码]Cache Storage[代码]和[代码]Service Worker[代码]总是分不开的。[代码]Service Worker[代码]的最佳用法其实就是配合[代码]Cache Storage[代码]做离线缓存。借助于[代码]Service Worker[代码],可以轻松实现对网络请求的控制,对于不同的网络请求,采取不同的策略。例如对于[代码]Cache[代码]的策略,其实也是存在多种情况。例如可以优先使用网络请求,在网络请求失败时再使用缓存、亦可以同时使用缓存和网络请求,一方面检查请求,一方面有检查缓存,然后看两个谁快,就用谁。 优化方向:目前我所负责的DICOM项目,虽然还没有用上[代码]Service Worker[代码],但前面经过不断地优化迭代,通过从增加http层的缓存、无损压缩图像的替换、有损压缩图像的渐进加载、更换DICOM解压缩策略、使用indexed DB缓存CT图像、首屏可见速度已经从20多秒降低到5秒左右,内存占用从700M以上降低到250M左右。后期还会一直深挖这一块。主要方向之一就是service worker的替换,全站缓存静态资源。此外,高优先级的则是DICOM无损图像解压算法的最优选择与优化、cornerstone的jpg图像展示。 项目优化还在继续,力求极致性能和用户体验~
2019-04-11 - 小程序构建骨架屏的探索
首屏 一般情况下,在首屏数据未拿到之前,为了提升用户的体验,会在页面上展示一个loading的图层,类似下面这个 [图片] 其中除了菊花图以外网上还流传这各种各样的loading动画,在PC端上几乎要统一江湖了,不过最近在移动端上面看到不同于菊花图的加载方式,就是这篇文章需要分享的Skeleton Screen,中文称之为"骨架屏" 概念 A skeleton screen is essentially a blank version of a page into which information is gradually loaded. 在H5中,骨架屏其实已经不是什么新奇的概念了,网上也有各种方案生成对应的骨架屏,包括我们经常使用的知乎、饿了么、美团等APP都有应用骨架屏这个概念 图片来源网络,侵删 [图片] 方案 先从H5生成骨架屏方案开始说起,总的来说H5生成骨架屏的方案有2种 完全靠手写HTML和CSS方式给每个页面定制一套骨架屏 利用预渲染的方式生成静态骨架屏 第一套方案,毫无疑问是最简单最直白的方式,缺点也很明显,假如页面布局有修改的话,那么除了修改业务代码之外还需要额外修改骨架屏,增加了维护的成本。 第二套方案,一定程度上改善了第一套方案带来的维护成本增加的缺点,主要还是使用工具预渲染页面,获取到DOM节点和样式,保留页面结构,覆盖样式,生成灰色块盖在原有文本、图片或者是canvas等节点上面,最后将生成的HTML和CSS打包出来,就是一个带有骨架屏的页面。最后再利用webpack工具将生成的骨架屏插入到HTML里面,详细的话可以看看饿了么的分享,这里就不多描述了。 调研了下H5生成骨架屏的方案,对于小程序生成骨架屏的方案也有了一个大致的想法,主要有2个难点需要实现 预渲染 获取节点 预渲染 再说回饿了么提供的骨架屏的方案,使用 puppeteer 渲染页面(或者使用服务端渲染,vue或者react都有提供相应的方案),拿到DOM节点和样式,这里有一点需要注意的是,页面的渲染是需要初始化的数据,数据的来源可以是初始化的data(vue)或者mock数据,当然小程序是无法直接使用 puppeteer 来做预渲染(有另外的方案可以实现),需要利用小程序初始化的 data + template 渲染之后得到一个初始化结构作为骨架屏的结构 [代码]//index.js Page({ data: { motto: 'Hello World', userInfo: { avatarUrl: 'https://wx.qlogo.cn/mmopen/vi_32/SYiaiba5faeraYBoQCWdsBX4hSjFKiawzhIpnXjejDtjmiaFqMqhIlRBqR7IVdbKE51npeF6X1cXxtDQD2bzehgqMA/132', nickName: 'jay' }, lists: [ 'aslkdnoakjbsnfkajbfk', 'qwrwfhbfdvndgndghndeghsdfh', 'qweqwtefhfhgmjfgjdfghaefdhsdfgdfh', ], showSkeleton: true }, onLoad: function () { const that = this; setTimeout(() => { that.setData({ showSkeleton: false }) }, 3000) } }) //index.wxml <view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 有了上面的 data + template 之后,就有了一个初始化的页面结构,接下来就需要拿到节点信息 节点 小程序基础库1.4.0之后小程序基础库提供了一组新的API,可用于获取节点信息,具体API戳这里。 跟H5方式一样,根据class或者id获取节点信息,不同的是只能获取到当前的节点信息,无法获取到其父或者子节点信息,所以只能手动给需要渲染骨架屏的节点添加相应的class或者id [代码]<view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 约定2个特殊的class作为获取节点信息的标记[代码]skeleton-rect[代码]和[代码]skeleton-radius[代码],在页面中获取相应的[代码]top[代码]、[代码]left[代码]、[代码]width[代码]、[代码]height[代码]进行骨架屏的绘制 结果 [图片] 具体的调用方式和源码,请看 github ,最后求start 总结 上文有说到小程序也可以使用 page-skeleton-webpack-plugin 方式一样生成骨架屏,最重要的一点就是需要将小程序跑在chrome上面,后面的流程就一样了,至于怎么将小程序跑在chrome上面呢?可以利用 wept ,缺点就是目前作者已经停止维护这个工具了,不支持新版小程序的API。 说回来我这个生成骨架屏的方案,其实跟 page-skeleton-webpack-plugin 有点相似,不同的是,page-skeleton-webpack-plugin 采用离线渲染的方式生成静态骨架屏插入路由中,而我采用运行时先渲染页面默认结构,然后根据默认结构再绘制骨架屏。从性能角度出发确实不如 page-skeleton-webpack-plugin,但是也差不了多少了,主要还是小程序并没有提供类似服务端渲染的方案。目前从使用上来讲,还是有点小麻烦,需要默认数据撑开页面结构,需要给相应的节点添加class,后面有时间再研究下有没有更好的方案吧~~~
2019-02-20 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] component文件夹是放自定义组件的 pages放页面 public放公共资源如样式表和公共图标 units放各种公共api文件和封装的一些js文件 config.js是配置文件 这么分已经足以满足我的需求,你可以根据自己的项目灵活拆分。 配置文件 我的项目中有个config.js,这个文件是用来配置项目中要用到的一些接口和其它私有字段,我们知道在开发时通常会有测试环境和正式环境,而测试环境跟正式环境的域名可能会不一样,如果不做好配置的话直接写死接口那等到上线的时候一个个改会非常麻烦,所以做好配置是必需的,文件大致如下: [图片] 首先是定义域名,然后在config对象里定义接口名称,getAPI(key)是获取接口方法,最后通过module暴露出去就可以了.引用的时候只要在页面引入 import domain from ‘…/…/config’;,然后wx.request的时候url的获取方式是domain.getAPI(’’) 代码健壮性、容错性 例子 代码的健壮性、容错性也是我们应该要考虑的一点,移动端的项目不像pc端的网络那么稳定,很多时候网络一不稳定就决定我们的项目是否能正常运行,而一个好的项目就一定要有良好的容错性,就是说在网络异常或其它因素导致我们的项目不能运行时程序要有一个友好的反馈,下面是一个网络请求的例子: [图片] 相信多数人请求的方式是这样,包括我以前刚接触小程序的时候也是这样写,这样写不是说不好,而是不太严谨,如果能够正常获取数据那还好,但是一旦请求出现错误那程序可以到此就没法运行下去了,有些比较好的会加上faill失败回调,但也只是请求失败时的判断,在请求成功到获取数据的这段流程内其实是还有一些需要我们判断的,一般我的做法是这样: [图片] 在请求成功后小程序会进行如下判断: 判断是否返回200,是则进行一下步操作,否则抛出错误 判断数据结构是否完整,是则进行一下步操作,否则抛出错误 然后就可以在页面根据情况进行相应的操作了。 定制错误提示码 可以看到上面的截图的错误打印后面会带一个gde0或gde1的英文代码,这个代码是干嘛用的呢,其实是用来报障的,当我们的小程序上线后可能会遇到一些用户发来的报障,一般是通过截图发给我们,之前没有做错误提示码的时候可能只是根据一句错误提示来定位错误,但是很多时候误提示语都是一样的,我们根本不知道是哪里错了,这样一来就不能很快的定位的错误,所以加上这样一个提示码,到时用户一发截图来,我们只要根据这个错误码就能很快的定位错误并解决了,错误提示码建议命名如下: 不宜过长,3个字母左右 唯一性 意义明确 像上面gde表示获取草稿失败,后面加上数字表示是哪一步出错。 模块化 我们组内的大神说过, 模块化的意义在义分治,不在于复用。 之前我以为模块化只是为了可以复用,其实不然,无论模块多么小也是可以模块化,哪怕只是一个简单的样式也一样,并是不为了复用,而是管理起来方便。 很多同学经常将一些公共的样式事js放在app.wxss和app.js里以便调用,这样做其实有一个坏处,就是维护性比较差,如果是比较小的项目还好,项目一大问题就来了。而且项目是会迭代的,不可能总是一个人开发,可能后面会交接给其他人开发,所以会造成的问题就是: app.wxss和app.js里的内容只会越来越多,因为别人不确定哪些是没用的也不敢删,只能往里加东西,造成文件臃肿,不利于维护。 app.wxss和app.js对于每个页面都有效,可读性方面比较差。 所以模块化的意义就出来了,将公共的部分进行模块化统一管理,也便于维护。 样式模块化 公共样式根据上面的目录结构我是放在public里的css里,每个文件命名好说明是哪个部分的模块化,比如下面这个就表示一个按钮的模块化 [图片] 前面说过模块化不在于大小,就算只是一个简单的样式也可以进行模块化,只要在用到的地方import一下就行了,就知道哪里有用到,哪里没有用到,清晰明了。 js模块化 js模块化这里分为两个部分的模块化,一部分是公共js的模块化,另一部分是页面js的模块化即业务与数据的拆分。 公共js模块化 比较常用的公共js有微信登录,弹窗,请求等,一般我是放在units文件夹里,这里经微信弹窗api为例: [图片] 如图是在小程序中经常会用到的弹窗提示,这里进行封装,定义变量,只要在页面中引入就能直接调用了,不用每次都写一大串。比如在请求的时候是这样用的 [图片] toast()就是封装的弹窗api,这样看起来是不是清爽多了! 业务与数据模块化 业务与数据模块化就是指业务和数据分开,互不影响,业务只负责业务,数据只负责数据,可以看到页面会比普通的页面多了一个api.js [图片] 这个文件主要就是用来获取数据的,而index.js主要用来处理数据,这样分工明确,相比以往获取数据和处理数据都在一个页面要好很多,而且我这里获取数据是返回一个promise对象的,也方便处理一些异步操作。 组件化 组件化相信大家都不陌生了,自从小程序支持自定义组件,可以说是大大地提高了开发效率,我们可以将一些公共的部分进行组件化,这部分就不详细介绍,大家可以去看文档。组件化对于我们的项目来说有很大的好处,而且组件化的可移植性强,从一个项目复用到另一个项目基本不需要做什么改动。 总结 这篇文章通过我自己的一些经验来给大家介绍如何优化自己的代码,主要有以下几点 分好项目目录结构 做好接口配置文件 代码健壮性、容错性的处理 定制错误提示码方便定位错误 样式模块化和js模块化 组件化 最后放上项目目录结构的代码片段,大家可以研究一下,有问题一起探讨:https://developers.weixin.qq.com/s/1uVHRDmT7j6l
2019-03-07 - 小程序架构设计(二)
接着上篇文章《小程序架构设计(一)》 前边我们说到采用Web+离线包的方式可以解决很多问题,但是遗留了一个安全问题有待解决。 经过了一番讨论,我们决定把开发者的JS逻辑代码放到单独的线程去运行,因为不在Webview线程里,所以这个环境没有Webview任何接口,自然的开发者就没法直接操作Dom,也就没法动态去更改界面,“管控”的问题得以解决。 还存在一个问题:开发者没法操作Dom,如果用户交互需要界面变化的话,开发者就没办法动态变化界面了。所以我们要找到一个办法:不直接操作Dom也能做到界面更新。 其实Facebook早有方案解决这个问题,就是上篇文章提到的React。React引入了Virtual Dom的概念(后文简称VD),业务侧只需要改变数据即可引起界面变化,相关原理后边再写篇文章来分享。 至此小程序双线程的模型就定下来了:渲染层(Webview)+逻辑层(JSCore) [图片] 其中渲染层用了Webview进行渲染,开发者的JS逻辑运行在一个独立的JSCore线程。 渲染层提供了带有数据绑定语法的WXML,逻辑层提供了setData等等API,开发者需要进行界面变化时,只需要通过setData把变化的数据传进去,小程序框架就会进行Dom Diff等流程最后把正确的结果更新在Dom树上。 [图片] 可以看到在开发者的逻辑下层,还需要有一层小程序框架的支持(数据通信、API、VD算法等等),我们把它称为基础库。 我们在两个线程各自注入了一份基础库,渲染层的基础库含有VD的处理以及底层组件系统的机制,对上层提供一些内置组件,例如video、image等等。逻辑层的基础库主要会提供给上层一些API,例如大家经常用到的wx.login、wx.getSystemInfo等等。 解决了渲染问题,我们还要看一下用户在和界面交互时的问题。 [图片] 用户在屏幕点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过setData引起界面变化,整个过程需要四次通信。对于一些强交互(例如拖动视频进度条)的场景,这样的处理流程会导致用户的操作很卡。 对于这种强交互的场景,我们引入了原生组件,这样用户和原生组件的交互可以节省两次通信。 [图片] 正如上图所示,原生组件和Webview不是在同一层级进行渲染,原生组件其实是叠在Webview之上,想必大家都遇到过这个问题,video、input、map等等原生组件总是盖在其他组件之上,这就是这个设计带来的问题。 我们也很重视这个问题,经过了一段时间的努力,我们攻克了这个难题,把原生组件渲染到Webview里,从而实现同层渲染。目前video组件已经完成同层渲染的全量发布,详细可以看我们之前的公告:同层渲染公测。 为了让开发者可以更好的开发小程序,我们在后来还引入了自定义组件和插件的概念,我们后续会有相关的文章再介绍这两块的设计,希望大家关注我们社区的文章板块。 [图片] 以上就是小程序架构设计的历史。
2019-02-27 - 跨界 - Omi 发布多端统一框架 Omip 打通小程序与 Web
Omip 今天,Omi 不仅仅可以开发桌面 Web、移动 H5,还可以直接开发小程序!直接开发小程序!直接开发小程序! → omijs.org → Github 地址 Omi 简介 Omi 框架是腾讯研发的下一代前端框架, 基于 Web Components 规范设计的组件化框架,可以开发 PC Web、移动端 H5,也可以直接使用 Omi 开发小程序。Omi 服务于腾讯的 H5 页面, PC 网站以及腾讯内部的一些管理系统和小程序等。自去年年底开源以来,该项目共获得 Star 数 7000+,贡献者 40+。Omi 借助京东 O2Team 优秀的 taro 多端统一框架,以及 Omi 开发团队和社区贡献者近期的共同努力,使 Omi 打通了小程序与 Web。细心的用户会发现,Omi 的 slogan 从 下一代 Web 框架 变更为 下一代前端框架, 因为 Omip 的加入,Omi 生于 Web 却能脱离 Web。 [图片] 同样的语法,同样的书写格式,运行在不同的平台、不同的环境,除了一些平台特有的API,几乎不用任何改动! 老的 Omi 项目做一些极其微小的改动(平台特性相关)就能跑在安卓/IOS的小程序里。 Learn Once, Write Anywhere Write Once, Run Anywhere [图片] Omip 特性 一次学习,多处开发,一次开发,多处运行 使用 JSX,表达能力和编程体验大于模板 支持使用 npm/yarn 安装管理第三方依赖 支持使用 ES6+ 支持使用 CSS 预编译器 小程序 API 优化,异步 API Promise 化 超轻量的依赖包,顺从小程序标签和组件的设计 快速开始 [代码]npm i omi-cli -g omi init-p my-app cd my-app npm start //开发小程序 npm run dev:h5 //开发 h5 npm run build:h5 //发布 h5 [代码] 把小程序目录设置到 dist 目录就可以愉快地调试了! node 版本要求 >= 8 也支持一条命令 [代码]npx omi-cli init-p my-app[代码] (npm v5.2.0+) 当然也支持 TypeScript: [代码]omi init-p-ts my-app [代码] TypeScript 的其他命令和上面一样,也支持小程序和 h5 SPA 开发。 Omip 多端示例 [图片] [图片] Omi 其他 [图片] [图片] 社区化发展,欢迎加入并贡献社区 目前 Omi 的贡献者遍布国内外各大公司(中国、韩国、美国、土耳其),Omi 共接受了快 40 位贡献者的文档和代码提交,核心贡献者共 11 名。欢迎有想法有能力有激情的开发者加入贡献者行列并最终能够进入 Omi Team。 你可以从这几个方面贡献: 1.翻译文档,目前有中文、英文和韩文,欢迎其他语言版本的翻译加入 2.提交补丁代码优化 Omi 3.积极参与 Issue 的讨论,如答疑解惑、提供想法或报告无法解决的错误 4.贡献案例,可以是管理后台、PC 网站、移动端 H5等等 5.完善文档,可以反复修正文档,让其更易懂,上手更快 6.扩展 Omi 生态,编写 Omi 自定义组件 7.分享与 Omi 的故事 8.写 Omi 相关的 blog 我们非常欢迎开发者们为腾讯开源贡献一份力量,相应也将给予贡献者激励以表认可与感谢。参见腾讯贡献者激励计划 Omi 交流群 欢迎加入Omi交流群,群聊号码:256426170,也可扫码加入: [图片] 感谢 感谢京东 O2Team taro 项目 感谢京东 O2Team taro 团队 Github https://github.com/Tencent/omi
2019-03-20 - 从源码看微信小程序启动过程
一、写作背景 接触小程序一年多,真实体验就是小程序开发门槛相对而言确实比较低。不过小程序的开发方式,一直是开发者吐槽的,如习惯了 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 ,也给我引入了新的问题,还希望能够有更多的讨论: · 自定义组件太多启动时会耗时处理自定义组件 · 文件太多会耗时读文件 · 合理的设计分包很重要 当然最后对于框架中已有的能力,还是非常希望微信可以开放更多稳定的接口,并在文档中告知开发者,让开发变得简单一些。
2019-03-05 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/article/doc/000040a5dc4518005d2842fdf51c13 [代码]今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 [代码] 首先先来理解一下两者的概念和区别: [代码] 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ [代码] 适用场景 函数防抖 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 函数节流 滚动加载,加载更多或滚到底部监听 搜索联想功能 实现原理 [代码] 函数防抖 [代码] [代码]const _.debounce = (func, wait) => { let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }; [代码] [代码] 函数节流 [代码] [代码]const throttle = (func, wait) => { let last = 0; return () => { const current_time = +new Date(); if (current_time - last > wait) { func.apply(this, arguments); last = +new Date(); } }; }; [代码] [代码] 上面两个方法都是比较常见的,算是简化版的函数 [代码] lodash中的 Debounce 、Throttle [代码] lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51[代码]
2019-02-22 - 【技巧】swiper仿tab切换
大家好,上次给大家分享了swiper多图片的解决方案:https://developers.weixin.qq.com/community/develop/article/doc/0008aa4fdbce08405c288f37951813 今天再给大家分享一个关于swiper的小技巧,利用swiper仿tab切换。 相信大家在app或浏览器上阅读新闻时,比如今日头条,会有这样一个场景,左右滑动的时候可以切换不同栏目,体验非常好,但是小程序好像没有提供相关组件,如果想实现这种效果该怎么做呢今天就给大家介绍一下在小程序里是怎么实现的。 首先先看下效果 [图片] 实现原理很简单,利用小程序swiper再配合scroll-view就能实现,不过这里面有几点需要注意一下: 1.scroll-view一定要给一个高度,不然会有问题; 2.切换的时候只显示当前的swiper-item里的内容,其它swiper-item里的内容可以先隐藏掉,这是因为如果你的swiper-item里的图片太多的话可能会造成页面回收,因为新闻列表大多是图文列表,而tab经常是不止两个的,可能是7、8个或更多,如果每个tab都显示的话到时上拉加载页面会非常庞大,所以这里我建议不用显示的内容先隐藏,记住是swiper-item里的内容不是swiper-item,到时切换回来时再重新渲染,如果你要保存滚动的位置还要做其它的一些处理,这里就不仔细讲解了; 3.这里适用的是整个页面都是tab切换的,如果只是在页面的某处实现tab切换,还要考虑高度的问题,加载数据的时候根据数据个数长度来计算高度,每次加载数据都要计算高度,切换到不同的tab也是,这部分比较麻烦,因为要计算,不过并不难,只要 计算正确的话是没有问题的; 大概就是这样,基本实现思路,大家可以根据这个思路去拓展,在上面加上自己的功能,over! 代码片段:https://developers.weixin.qq.com/s/89OO1smX736d
2019-02-26 - 【优化】解决swiper渲染很多图片时的卡顿
相信各位在开发的时候应该有遇到这样一个场景,比如商品的图片浏览,有时图片的浏览会很大,多的时候达几百张或上千张,这样就需要swiper里需要很多swiper-item,如此一来渲染的时候就会很消耗性能,渲染时会有一大段的空白时间,有时还会造成卡顿,体验非常差,下面给大家介绍一下我的解决方案。 首先是wxml结构: [图片] js: [图片] [图片] 主要是利用current属性,swiper里面只放3个swiper-item,要显示的图片放在第二,第一和第三放的是加载的动画背景,步骤如下: 将请求到的数据存入一个数组picListAll内,这里不需要setData,只需要在data外面定义一个变量就行了,以减少渲染性能; 把要显示的图片路径赋值给picUrl; 切换的时候根据bindchange获取current属性,当current改变时判断当前图片在picListAll的index,根据index拿到图片再赋值给picUrl; 主要实现步骤就是以上3 步,比较简单,要注意的是当切换到第一张和最后一张的时候要判断一下,把loding动画去掉,请求的时候还可以传入index参数以显示不同的图片,方便从前一页点击图片进入到此页面时能定位到该图片,例子里我是自己mock数据的,只是为了展示,如果你有服务器的话可以弄几百张看看效果,对比直接渲染和用以上方式渲染的差异。当然,这只是我的解决方案,如果各位有更好的方案欢迎一起讨论,一起进步。 完整代码:https://github.com/HaveYuan/swiper
2019-02-20 - 一次安全可靠的通信——HTTPS原理
我们知道小程序的wx.request网络接口只支持HTTPS协议(文档-小程序网络说明),为什么HTTPS协议就比HTTP安全呢?一次安全可靠的通信应该包含什么东西呢,这篇文章我会尝试讲清楚这些细节。 Alice与Bob的通信 我们以Alice与Bob一次通信来贯穿全文,一开始他们都是用明文的形式在网络传输通信内容。 [图片] 嗅探以及篡改 如果在他们的通信链路出现了一个Hacker,由于通信内容都是明文可见,所以Hacker可以嗅探看到这些内容,也可以篡改这些内容。 [图片] 公众号的文章之前就遇到很多被挟持篡改了内容,插入广告。 [图片] 加密解密 既然明文有问题,那就需要对明文进行加密处理,让中间人看不懂内容,于是乎要对原来的内容变成一段看不懂的内容,称为加密,反之则是解密。而本质其实就是一种数学运算的逆运算,类似加法减法,例如发送方可以将 abcd…xyz 每个字母+1映射成 bcd…yza,使得原文的字母变成看不懂的序列,而接收方只需要将每个字母-1就可以恢复成原来的序列,当然这种做法规律太容易被破解了,后边会有个案例示意图。 [图片] 对称加密 如果对2个二进制数A和B进行异或运算得到结果C, 那C和B再异或一次就会回到A,所以异或也可以作为加密解密的运算。 [图片] 把操作数A作为明文,操作数B作为密钥,结果C作为密文。可以看到加密解密运用同一个密钥B,把这种加解密都用同一个密钥的方式叫做对称加密。 [图片] 可以看到简单的异或加密/解密操作,需要密钥跟明文位数相同。为了克服这个缺点,需要改进一下,把明文进行分组,每组长度跟密钥一致,分别做异或操作就可以得到密文分片,再合并到一起就得到密文了。 [图片] 但是这种简单分组的模式也是很容易发现规律,可以从下图看到,中间采用对原图进行DES的ECB模式加密(就是上边提到简单分组的模式) [图片] 很明显,原图一些特征在加密后还是暴露无遗,因此需要再改进一把。一般的思路就是将上次分组运算的结果/中间结果参与到下次分组的运算中去,使得更随机混乱,更难破解。以下图片来自维基百科: [图片] 经过改良后,Alice与Bob如果能提前拿到一个对称加密的密钥,他们就可以通过加密明文来保证他们说话内容不会被Hacker看到了。 [图片] 非对称加密 刚刚还引发另一个问题,这个对称加密用到的密钥怎么互相告知呢?如果在传输真正的数据之前,先把密钥传过去,那Hacker还是能嗅探到,那之后就了无秘密了。于是乎出现另外一种手段: [图片] 这就是非对称加密,任何人都可以通过拿到Bob公开的公钥对内容进行加密,然后只有Bob自己私有的钥匙才能解密还原出原来内容。 [图片] RSA就是这样一个算法,具体数学证明利用了大质数乘法难以分解、费马小定理等数学理论支撑它难以破解。相对于前边的对称加密来说,其需要做乘法模除等操作,性能效率比对称加密差很多。 [图片] 由于非对称加密的性能低,因此我们用它来先协商对称加密的密钥即可,后续真正通信的内容还是用对称加密的手段,提高整体的性能。 [图片] 认证 上边虽然解决了密钥配送的问题,但是中间人还是可以欺骗双方,只要在Alice像Bob要公钥的时候,Hacker把自己公钥给了Alice,而Alice是不知道这个事情的,以为一直都是Bob跟她在通信。 [图片] 要怎么证明现在传过来的公钥就是Bob给的呢?在危险的网络环境下,还是没有解决这个问题。 [图片] 一般我们现实生活是怎么证明Bob就是Bob呢?一般都是政府给我们每个人发一个身份证(假设身份证没法伪造),我只要看到Bob身份证,就证明Bob就是Bob。 网络也可以这么做,如果有个大家都信任的组织CA给每个人出证明,那Alice只要拿到这个证明,检查一下是不是CA制作的Bob证书就可以证明Bob是Bob。所以这个证书里边需要有两个重要的东西:Bob的公钥+CA做的数字签名。 [图片] 前边说到用公钥进行加密,只有拥有私钥的人才能解密。数字证书有点反过来:用私钥进行加密,用公钥进行解密。CA用自己的私钥对Bob的信息(包含Bob公钥)进行加密,由于Alice无条件信任CA,所以已经提前知道CA的公钥,当她收到Bob证书的时候,只要用CA的公钥对Bob证书内容进行解密,发现能否成功解开(还需要校验完整性),此时说明Bob就是Bob,那之后用证书里边的Bob公钥来走之前的流程,就解决了中间人欺骗这个问题了。 这种方式也是一种防抵赖的方式,让对方把消息做一个数字签名,只要我收到消息,用对方的公钥成功解开校验这个签名,说明这个消息必然是对方发给我的,对方不可以抵赖这个行为,因为只有他才拥有做数字签名的私钥。 [图片] CA其实是有多级关系,顶层有个根CA,只要他信任B,B信任C,C信任D,那我们基本就可以认为D是可信的。 [图片] 完整性 上边基本上已经解决了保密性和认证,还有一个完整性没有保障。虽然Hacker还是看不懂内容,但是Hacker可以随便篡改通信内容的几个bit位,此时Bob解密看到的可能是很乱的内容,但是他也不知道这个究竟是Alice真实发的内容,还是被别人偷偷改了的内容。 [图片] 单向Hash函数可以把输入变成一个定长的输出串,其特点就是无法从这个输出还原回输入内容,并且不同的输入几乎不可能产生相同的输出,即便你要特意去找也非常难找到这样的输入(抗碰撞性),因此Alice只要将明文内容做一个Hash运算得到一个Hash值,并一起加密传递过去给Bob。Hacker即便篡改了内容,Bob解密之后发现拿到的内容以及对应计算出来的Hash值与传递过来的不一致,说明这个包的完整性被破坏了。 [图片] 一次安全可靠的通信 总结一下,安全可靠的保障: 对称加密以及非对称加密来解决:保密性 数字签名:认证、不可抵赖 单向Hash算法:完整性 来一张完整的图: [图片]
2019-02-20 - 小程序前后端交互使用JWT
前言 现在很多Web项目都是前后端分离的形式,现在浏览器的功能也是越来越强大,基本上大部分主流的浏览器都有调试模式,也有很多抓包工具,可以很轻松的看到前端请求的URL和发送的数据信息。如果不增加安全验证的话,这种形式的前后端交互时候是很不安全的。 相信很多开发小程序的开发者也不一定都是大神,能够精通前后端,作为小程序的初学者不少人也是根据官方的文档去学习开发的。我自己最开始接触小程序也是从wafer2开始的,那时候腾讯云提供的SDK包含PHP和Node.js,因为对于一直做前端的人来说,Node.js的学习成本比较低,只要会JS基本能看懂,也是从那时候才开始接触Node.js,所以本文主要是基于wafer2的服务端基于Koa2的后端来说(其实这个不重要,Node.js基本都差不多)。 什么是JWT? 根据维基百科的定义,JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。 为什么使用JWT? 首先,这不是一个必选方案。有时候我们的API是其它服务端和小程序公用的,那么就涉及到安全验证的问题了。 微信官方不鼓励小程序一打开就要求必须登陆的方式去获取用户信息,因此我们也不能去校验这个用户是否有权限访问这个接口,但是有的接口又不能让任何人随便去看或者被随意采集。 基于token(令牌)的用户认证 用户输入其登录信息 服务器验证信息是否正确,并返回已签名的token token储在客户端,例如存在local storage或cookie中 之后的HTTP请求都将token添加到请求头里 服务器解码JWT,并且如果令牌有效,则接受请求 一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。 关于JWT的详细介绍网上有很多,这里也就不说了,下面介绍在Koa2框架里的添加方法。 安装依赖 [代码]npm install jsonwebtoken npm install koa-jwt [代码] app.js 引用 [代码]const jwtKoa = require('koa-jwt'); [代码] 设置不需要JWT验证的目录或者文件 [代码]const secret = '设置密钥'; app.use(jwtKoa({secret}).unless({ path: ['/','\/favicon.ico',/^\demo/] })) [代码] 数组中的路径不需要通过jwt验证。 授权 小程序 wx.request 发送网络请求的 referer header 不可设置。 其格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本。 那么我们就可以根据 ctx.header 里的 referer 进行初步的限制,比如指定的 appid 才能生成令牌。 我们在生成令牌的时候可以把简单的信息加入进去,如: [代码]const userToken = { referer: refererArray[2], appid: refererArray[3], version: refererArray[4], data: '此处可传入用户的信息' } [代码] 生成令牌: [代码]const jwt = require('jsonwebtoken'); const secret = '设置密钥'; jwt.sign(userToken, secret, {expiresIn: '2h'}); [代码] expiresIn:为令牌的有效期 这样简单的JWT令牌就生成好了,再通过接口返回给小程序端。 小程序前端如何使用JWT? 很简单,在header里加入下面属性即可。 [代码]authorization: 'Bearer 获取到的令牌' [代码] JWT优点 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而JWT不需要。 无状态 JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT,可以降低服务器查询数据库的次数。 JWT缺点 安全性 由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。 性能 JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的http请求比使用session的开销大得多。 一次性 无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。 (1)无法废弃 通过上面JWT的验证机制可以看出来,一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。 (2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个http请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。
2019-02-20 - 1个开发如何撑起一个过亿用户的小程序
2018年12月,腾讯相册累计用户量突破[代码]1亿[代码],月活1200万,阿拉丁指数排行 [代码]Top 30[代码],已经成为小程序生态的重量级玩家。 三个多月来,腾讯相册围绕【在微信分享相册照片】这一核心场景,快速优化和新增一系列社交化功能,配合适当的运营,实现累计用户量突破[代码]1亿[代码],大大超过预期。 [图片] (腾讯相册用户量破亿) 可是,谁曾想到,这样一个亿级体量的小程序,竟然是一个开发做出来的?他又是有哪般“绝技”,可以一个人撑起一个用户过亿的小程序? 后台人力紧缺,怎么办? 当我第一次见到腾讯相册小程序的开发David(化名)时,他显得忧心忡忡。 “年底的目标是要过千万的用户,但现在只有几位前端和后台开发。不仅如此,我们的后台开发还不是百分百能够投入到这个项目,大部分时间要抽身支援其它项目,人力非常紧缺。此外,原有后台系统有不少历史包袱,在原有架构上做新的社交化功能开发是不现实的。怎么办? “要不试试’小程序·云开发’吧,只需要前端就可以把小程序搞起,正好解决我们缺后台的难题。” 于是,David作为腾讯相册前端开发团队的骨干,担当起用小程序·云开发实现腾讯相册小程序社交化功能的重任。 “第一次接触到’小程序·云开发‘时,觉得这个东西(小程序·云开发)理念挺新颖的———小程序无服务开发模式。在一般的小程序开发中,有三大功能小程序开无法绕开后台的帮助,它门分别是数据读取、文件管理以及敏感逻辑的处理(如权限)。因此,传统的开发模式,在小程序端都必须发送请求到后台进行鉴权,并且处理相关的文件或者数据。即使使用 Node 来搭建后端服务,也需要耗费不少的搭基础架构、后期运维的工作量。” [图片] “而小程序·云开发则释放了小程序开发者的手脚,赋予了开发者安全、稳定读取数据、上传文件和控制权限的能力,其它的负载、容灾、监控等,我们小程序开发者只需要关注业务逻辑,专注写好业务逻辑即可,其他的事情完全可以不用操心了!本来我还一筹莫展,了解完’小程序·云开发‘的产品原理以后,我瞬间心里有谱了。” 二维码扫不出来了 [图片] 道路总是不平坦的 ,在腾讯相册小程序通往用户破亿的道路上,困难重重。 由于腾讯相册的二维码需要带上的信息量过大,因此它的二维码显得密密麻麻。这种密集的二维码在某些Android机型下,容易出现无法识别小程序的问题。 这严重制约了腾讯相册小程序分享获客的能力。 [图片] (需要存储name, ownerid, page等大量信息) 这个事情的解决并不难,只需后台开发把数据先存储到数据库中,然后把数据id放到分享链接上,这样,链接便可以转化成32个字符的短链接,让二维码看起来没有那么密集了。 但由于后台人力不足,于是前端开发David利用小程序· 云开发的数据库存储能力,通过调用db.collection(‘qr’).add接口,快速实现数据在数据库中的存储。 [图片] (云开发数据库,格式类似MongoDB) [图片] (云开发数据库索引,可加快数据读取) [图片] 此外,腾讯相册还借住小程序·云开发的云函数能力,生成辨识度更高的小程序码(小程序码文档),用以在朋友圈里传播分享。 [图片] (生成小程序码的云函数逻辑) [图片] (优化后的分享图片和小程序码) 2天上线评论点赞功能 [图片] (评论与点赞功能) 腾讯相册在微信端的核心应用场景是“在微信做分享相册照片”,为了增强腾讯相册用户在微信里的互动,提升用户粘性和留存,腾讯相册决定新增评论与点赞功能,并且把聊天评论就直接在微信聊天窗口里面实现。 在这里,腾讯相册的David面临了两个选择,一是按原开发模式(前台开发-后台开发-前后台联调)做这个功能,面临的问题便是开发周期长、缺后台、迭代速度慢;另一个就是借助云开发的能力,亲自上阵。 为了加快产品迭代速度,David决定采取云开发的开发方式。评论、点赞通过云开发的数据库插入和查询接口,如db.collection(‘comment’).add,很快就实现了。 但遇到棘手的问题是,对于一些敏感的操作比如删除和编辑评论、点赞这些敏感操作,还需要到用户的鉴权操作,而这些鉴权信息,都在原有的后台。此时,云函数的路由功能便发挥出作用了。 [图片] (评论点赞逻辑) 用户进行评论点赞的时候,会在小程序端发起请求调用云函数并带上 [代码]openid[代码],云函数用 [代码]openid[代码] 查询原有的后台服务看看该用户是否有权限进行操作,如果用户具有权限,则把评论和点赞的数据都写入云开发的数据库中。 就这样,借住小程序·云开发的能力,腾讯相册仅用2天时间,完成了在传统开发模式下需要1周多工作量的开发工作。 原有开发模式 云开发全栈开发 工作量 后台1周(微信登录态校验+业务逻辑server开发)+ 前后台联调1天 1 - 2天,无需联调 什么是小程序·云开发? 小程序·云开发是基于腾讯云研发的全新 云开发 Tencent Cloud Base(简称 TCB) 服务,腾讯云与微信力推的这套云开发服务的诞生,恰逢其时地帮助腾讯相册走出开发效率的瓶颈。 [图片] (基于腾讯云的云开发) 小程序云目前提供三大基础能力支持: 云函数:充当了后台的角色,开发者可以在上面用 Node (后续还会支持 PHP, Python 等)写后台逻辑,跟微信私有协议天然鉴权,可以云函数里直接获取 [代码]appid[代码], [代码]openid[代码], [代码]unionid[代码] 等重要鉴权信息,大大简化了小程序后台的开发工作量。 数据库:一个既可在小程序前端操作,也能在云函数中读写的文档型数据库,提供控制台可视化管理 文件存储:在小程序前端和云函数里都可以直接上传/下载云端文件,提供控制台可视化管理 如果你是全新开发的小程序,架构非常轻量简单,如下图。 [图片] 如果你是已有的小程序,部份需要跟原有后台交互的功能,完全可把云函数作为路由,节省获取openid 等用户信息的逻辑,如下图: [图片] 相关资料 小程序·云开发解决方案 小程序·云开发文档
2019-02-19 - 微信小程序音视频与WebRTC互通的技术思路和实践
概述 本文介绍了微信小程序视音视频和WebRTC的技术特征、差异等,并针对两者的技术差异分享和总结了微信小程序视音视频和WebRTC互通的实现思路以及技术方案。 关于作者 rexchang(常青):腾讯视频云终端技术总监,2008 年毕业加入腾讯,一直从事客户端研发相关工作,先后参与过 PC QQ、手机QQ、QQ物联 等产品项目,目前在腾讯视频云团队负责音视频终端解决方案的优化和落地工作。 分别介绍一下小程序音视频和WebRTC 小程序音视频是什么? 2017年腾讯视频云团队跟微信团队联合,将视频云 SDK 跟微信小程序整合在一起,并通过 <live-pusher> 和 <live-player> 两个标签的形式开放内部的功能。通过这两个标签,开发者可以实现在线直播、低延时监控、双人视频通话以及多人视频会议等功能 [图片] WebRTC又是什么? WebRTC(Web Real-Time Communication),是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌收购 GIPS 公司而获得的一项技术,在 Chrome 浏览器上无需安装插件,通过 javascript 就可以编写实时音视频通话程序。 小程序音视频和WebRTC的区别在哪里? 如果您跟我一样是一个实用主义者,那我就简单从实用主义角度说一下我的结论:小程序搞定了手机,WebRTC拿下了PC。 如果你对技术原理比较感兴趣,那我们就可以从多个技术的角度去列举两者的区别,下面是一张详细对比的表格: [图片] (区别一):内部原理 小程序音视频是将腾讯视频云的 liteavsdk 嵌入到微信内部实现的,然后通过 <live-pusher> 和 <live-player> 两个标签将 SDK 内部的音视频能力开放出来。所以小程序的标签起到了开发者 API 的作用,而内部的 SDK 则是真正用来实现音视频功能。 WebRTC 由谷歌收购 GIPS 得来(这里不得不提一下,我加入腾讯时所在的第一个团队就是 QQ 团队,当时 QQ 的音视频还是购买的 GIPS 公司的产品,不过由于技术支持不能满足需求,后来我们就转为自研路线了)。所以其技术被完整的保留并且加入到了 Google 的 Chrome 浏览器内核当中。而且最近苹果也已经开始在 Safari 浏览器中支持 WebRTC 的相关能力。 (区别二):传输协议 小程序音视频在直播场景使用了 RTMP 推流协议以及 FLV 播放协议,这两种协议都已经有多年的沉淀而且在互联网上的资料也是汗牛充栋。 小程序音视频在视频通话场景则使用了经过 UDP 改造的 RTMP 协议,相比于普通 RTMP 有更强的抗弱网能力和更低的卡顿率。 小程序音视频内核 LiteAVSDK 的抗弱网演示 WebRTC的底层则是使用RTP和RTCP两种数据协议,其中RTP主要用于音视频数据传输,而 RTCP 则用于传输控制。 (区别三):移动端碎片化问题 小程序音视频由于是微信统一实现的,而且微信团队每个版本都尽量要求功能对齐,否则宁可不上,所以在碎片化问题上基本不存在。 相比之下,WebRTC在这里则要尴尬的多,一方面Android系统的碎片化本身让WebRTC的具体表现呈现“百花齐放”的景象,同时,iOS 目前的内嵌WebView(也就是在微信等APP里打开的各种内嵌网页)不支持WebRTC也还是个很麻烦的问题。 (区别四):未来扩展性 小程序音视频跟随微信的版本发布,有什么问题一般是当前代码流修正,然后跟随下一个版本发布,所以一般一个功能点(比如给 pusher 加一个美颜的功能)或者一个问题点(比如不支持手势放大)从确立到最终实现(或解决)仅需要一个月的时间,而且微信APP新版本的覆盖速度也确实挺快。 相比之下,WebRTC则不是一个团队或者一家公司的问题了,因为它现在已经走标准路线,所以每一个新特性都是先确定标准,然后再推动浏览器厂商(包括苹果)进行跟随。 (区别五): 桌面浏览器 在前面几个问题的分析上,我的观点都倾向于小程序音视频。确实,在目前国内的移动领域里,谷歌和苹果都不能一家说了算,真正说了算的还是微信。 但是在桌面浏览器这个部分,Chrome目前在PC浏览器市场上留到地位的存在决定了 WebRTC 的优势就很大了,开发者可以在不安装插件的情况下就可以实现自己想要的功能。 所以,实现同 Chrome 浏览器的音视频互通,成为了小程序音视频的一个必不可少的能力特性。 互联互通 小程序音视频和WebRTC支架并非零和博弈,双方都有自己的优势和不足,实现两者的互通就能实现 1 + 1 > 2 的效果。 PC 端 用户可以使用 Chrome 浏览器直接使用音视频能力,免去安装桌面应用程序的痛苦。 移动端 用户可以使用微信小程序直接使用音视频能力,减少安装App的等待时间。 两者结合,可以将原本局限于小应用场景下的音视频能力扩展到各行各业中。 当然,要实现互联互通,并不是特别容易,首先,我们需要对 WebRTC 协议本身有一个全面的了解: 剖析WebRTC 就像结婚一样,既然你决定要选择另一个人作为人生下半辈子的伴侣,那你肯定会先深入地了解一下TA这个人,比如性格,脾气,爱好等各个方面。 同样,我们要想很好的将小程序音视频和WebRTC打通,那也必须要多了解一下WebRTC,对其知根知底,方能和平相处。 WebRTC 的设计思路是open的 WebRTC 的接口设计一开始就尽可能把内部细节更多的暴露出来,而不是简单封装一套傻瓜式的接口。这种方案的好处是二次开发的灵活度比较高,比如您可以发现 WebRTC 的 API 可以灵活到操作很多连接细节。 但任何事情都有另一面,WebRTC的学习成本并不低,虽然Google做了很多浅显易懂的PPT来教你怎么 Getting Start,但真要完整的学进去,还是需要静下心来,慢慢啃下去。 WebRTC 有多种后台接入方案 说WebRTC喜欢迁就比人,也是一种比喻,WebRTC所支持的后台架构非常多(比如 Mixer, Mesh,Router),而且谷歌认为这些后台实现方案并不需要给出什么限制和标准,因此也就没有提供统一的后台解决方案。 这种开放式的设计思路非常好,但副作用就是实现成本高。在真刀真枪的项目落地时,没有踩坑经验的开发者就很容易被这种技术门槛挡在门外。尤其是想要将 WebRTC 真正应用到企业级解决方案中,面对录制和存档的刚性需求,就需要花费大量时间进行定制开发。 互通方案 了解到 WebRTC 的这些特点后,我们的互通方案也就比较清晰了: 首先,小程序音视频的特点是接口简单,快速上手,这是小程序的优势;而这一点恰恰是WebRTC的劣势,所以我们没有必要在小程序端为WebRTC暴露十几组接口函数,而是继续采用小程序音视频的<live-pusher> 和 <live-player> 标签来解决问题。 其次,WebRTC 的后台没有官方实现,那就意味着这里有很大的发挥空间,腾讯视频云就可以实现一套WebRTC后台并将其同小程序音视频所使用RTMP后台进行打通。简单来说,腾讯视频云要在小程序音视频和WebRTC之间充当红娘(更确切的说,应该是翻译员)的角色。 但是看过《新闻联播》里国家领导人之间谈话镜头的人都知道,这种翻译是会影响交流速度的。小程序音视频和WebRTC之间互通,中间引入一个翻译员,是不是通讯延时也就增加了? 其实不会,因为小程序音视频和WebRTC的视频编码标准在常规应用场景中是一致的,都是H.264标准,只是音频格式不同而已。这就意味着,翻译员要做的事情很少,两边基本都能听懂对方在说什么,所以延时不会增加多少。 协议握手 下图所展示的就是腾讯视频云在小程序音视频和WebRTC互通问题上所采取的方案: [图片] (1)首先,微信端的小程序通过腾讯视频云SDK将音视频流推送到腾讯云 RTMP 服务器。 (2)其次,腾讯云 RTMP 服务器的会对音视频数据进行初步的转化处理,然后透传给腾讯视频云的实时音视频后台集群。 (3)再次,实时音视频后台会再次将数据交给一个叫做 WebRTC-Proxy 的模块,就在这里, WebRTC-Proxy 要将来自小程序音视频的音视频数据翻译成 WebRTC 理解的“语言”。 (4)最后,在PC上的Chrome浏览器,就可以通过浏览器内置的WebRTC模块跟 WebRTC-Proxy 通讯,进而看到小程序端的视频影像。 (5)上面的四个过程倒过来,就可以实现双向视频通话;而将腾讯视频云作为星型结构的中心节点,多个端(不管是小程序还是Chrome浏览器)都接入进来,那就可以形成多人音视频解决方案。 打通房间逻辑 仅仅完成了音视频数据在小程序和WebRTC之间的握手还远远不够,因为在一次成功的音视频通话背后,不仅仅是把一端的音视频数据传递到另一端这么简单,还有状态的同步和成员间的状态协同。 比如多人视频通话中,涉及到呼叫和接通的流程,其中一方如果挂断了,其他人要收到挂断的通知。同时,如果有新的参与者加入,那么其他人也要收到相应的通知。WebRTC 中有很多组件,比如 RTCPeerConnection 就负责处理网络连接中各种密密麻麻的逻辑细节。但是 WebRTC 的接口中引入的新名词非常多,对于初学者来说还是有一定的门槛,为了简化这里的逻辑,我们引入一个叫做“房间”的概念。 所谓房间(Room),就是把同时参与视频通话的各方圈在一起的一个东西。比如双人通话中,通话中的两个人 A 和 B 就可以认为在一个房间中。再比如在多人通话中,通话中的五个人(A B C D E)也可以认为是在一个房间里。 有了房间的概念,那我们就可以对刚才说的状态协同用两个简单的动作描述一下:如果有一个人加入了视频通话,那么就可以理解为他/她已经进房(EnterRoom)了;如果有一个退出了视频通话,那么就可以理解为他/她已经离开房间(LeaveRoom)了。而房间的门板上始终写着:“目前在房间里有哪几个人”。 有了房间的概念,我们就可以将小程序的两个简单的<live-pusher> 和 <live-player>标签,同 WebRTC 那一套复杂的 API 进行功能上的对齐,我们甚至不需要修改我们在第一版中定义的接口,就可以达成这个目标: <live-pusher> 标签:代表房间中的“我”。 <live-player> 标签:代表房间中的“其他人”。 内部逻辑细节 [图片] (1)<live-pusher> 的 url 接口不再传递 rtmp:// 协议的推流地址,而是传递 room:// 协议的推流地址。 (2)<live-pusher> 标签在 start 成功之后,就相当于成功进入一个 room,之后,您可以通过 onPushEvent (PUSH_EVT_ROOM_USERLIST = 1020) 事件,收到房间里还有那些人的信息。在视频通话期间,房间内各个成员的进进出出,也都会通过这个事件通知给您的小程序代码。 (3)ROOM_USERLIST 里每一项都是一个二元组(如果是 1v1 的视频通话,ROOM_USERLIST 里只会有一个人): userid 和 playurl。 userid 代表是哪个用户, playurl 则是这个用户远程画面的播放地址。您要做的只是使用 <live-player> 标签播放这些远程画面的图像和声音而已。 (4)在 WebRTC 这一端,您可以参考我们的 webrtc API,这套 API 相对于 WebRTC 原生的 API,更适合初学者使用。 呃… 您可能会说:“你这也叫简单呀,我感觉还是要写几十行代码,能不能真的做到像一个标签一样简单呢?” 好吧,其实上面四步是我们第一个版本的接入流程,就在我们昨晚这套方案之后,小程序团队刚好推出了自定义组件的机制,于是,我们有了更好的接入方案。 能不能更简单? 如果您希望一天内就打通 webrtc 和 小程序音视频 的互通,那么我推荐您不要从零开始,因为那会耗费您太多时间去踩坑和 bugfix,推荐您直接使用我们封装好的 <webrtc-room> ,这套方案既可以帮助您完成快速接入,又能满足一定的定制需求。 另外,不要忘记在微信=>发现=>小程序=>腾讯云视频云,体验一下腾讯云官方 Demo 中的 WebRTC 互通效果哦。 [图片] <webrtc-room> 功能说明 <webrtc-room> 标签是基于 <live-pusher> 和 <live-player> 实现的用于 WebRTC 互通的自定义组件。用于实现跟 Chrome 和 App SDK 之间的视频通话功能。 版本要求 微信 6.6.6 版本开始支持。 Demo体验 (1) Chrome: 用谷歌浏览器打开 体验页面。 (2) 微信端:发现=>小程序=>搜索“腾讯视频云”,点击 视频通话 页卡,输入相同的房间号。 对接资料 源码地址 源码说明 小程序端源码 Github Chrome端源码 Github 属性定义 属性 类型 默认值 说明 template String ‘float’ 必要,标识组件使用的界面模版。 demo中内置 bigsmall,float,grid三种布局 sdkAppID String 必要,开通实时音视频服务创建应用后分配的 sdkAppID userID String 必要,用户 ID userSig String 必要,身份签名,相当于登录密码的作用 roomID Number 必要,房间号 beauty Number 0 可选, 美颜指数,取值 0 - 9,数值越大效果越明显 whiteness String 0 可选, 美白指数,取值 0 - 9,数值越大效果越明显 muted Boolean false 可选,true 静音 false 不静音 debug Boolean false 可选,true 打印推流 debug 信息 fales 不打印推流 debug 信息 bindRoomEvent Function 必要,监听 <webrtc-room> 组件返回的事件 enableIM Boolean false 可选,是否启用IM bindIMEvent Function 当IM开启时必要,监听 IM 返回的事件 aspect String 9:16 可选, 宽高比3:4, 9:16 minBitrate String 200 可选,最小码率,该数值决定了画面最差的清晰度表现 maxBitrate String 400 可选,最大码率,该数值决定了画面最好的清晰度表现 autoplay Boolean false 可选,进入房间后是否自动显示远程画面 enableCamera Boolean true 可选,开启\关闭摄像头 pureAudioPushMode Number 可选,纯音频推流模式 recordId Number 可选,自动录制时业务自定义id enableCamera Boolean true 是否开启摄像头 smallViewLeft String ‘1vw’ 小窗口距离大画面左边的距离,只在template设置为bigsmall有效 smallViewTop String ‘1vw’ 小窗口距离大画面顶部的距离,只在template设置为bigsmall有效 smallViewWidth String ‘30vw’ 小窗口宽度,只在template设置为bigsmall有效 smallViewHeight String ‘40vw’ 小窗口高度,只在template设置为bigsmall有效 waitingImg String 当微信切到后台时的垫片图片 loadingImg String 画面loading图片 操作接口 <webrtc-room> 组件包含如下操作接口,您需要先通过 selectComponent 获取 <webrtc-room> 标签的引用,之后就可以进行相应的操作了。 函数名 说明 start() 启动 pause() 暂停 resume() 恢复 stop() 停止 switchCamera() 切换摄像头 sendC2CTextMsg(receiveUser, msg, succ, fail) 发送C2C文本消息 sendC2CCustomMsg(receiveUser, msgObj, succ, fail) 发送C2C自定义消息 sendGroupTextMsg(msg, succ, fail) 发送群组文本消息 sendGroupCustomMsg(msgObj, succ, fail) 发送群组自定义消息 [代码]var webrtcroom = this.selectComponent("#webrtcroomid") webrtcroom.pause(); [代码] 事件通知 <webrtc-room> 标签通过 onRoomEvent 返回内部事件,通过 onIMEvent返回 IM 消息事件,事件参数格式如下: [代码]"detail": { "tag": "事件tag标识,具有唯一性", "code": "事件代码", "detail": "对应事件的详细参数" } [代码] 如何快速接入? 上面说了很多细节的技术原理和内部细节,如果您想要快速尝试一下,建议您阅读下面三篇文章就可以了。 一分钟跑通demo 我们准备了一个简单上手的小程序音视频Demo,输入房间号即可开始通话,这篇文章主要介绍如何把Demo快速地 run 起来。 一分钟集成组件 这篇文章主要介绍如何快速地将 <webrtc-room> 组件集成到您的小程序工程项目中。 快速调通基本功能 这篇文档就主要介绍如何二次开发了,它介绍了<webrtc-room> 中主要 API 的使用。 总结 本篇文章主要介绍了小程序音视频和Chrome浏览中重要的WebRTC技术的互通方案,希望能对您的项目开发有所帮助,期待您的反馈。
2019-02-20 - 小程序码二进制流如何生成二维码图片?
- 需求的场景描述(希望解决的问题) getWXACodeUnlimit接口从微信后台获取到了小程序码的二进制流。 如何用 获得的小程序码二进制流 生成 可见的小程序图片? 试过file.writeFile() 生成的文件,不能用图片编辑器查看 - 希望提供的能力 ++++++++++++++++++++++++++++++ 分割线 +++++++++++++++++++++++++++++++ 最后还是没有解决,我是用了第三方的后台,直接生成的
2019-01-08 - 「微信开发者·代码管理」功能上线
各位开发者: 大家好。 近期我们上线了微信开发者·代码管理 的功能,这是为开发者提供的一项基于 Git 的代码管理服务。方便微信开发者进行代码推送、拉取、版本管理和多人协作,并且可以简单的将仓库设置为私有或者公开。 系统会为每个小程序自动创建一个专属项目组,用户无需单独开通。后续登录过微信开发者·代码管理的小程序开发者会自动成为该项目组成员。 微信开发者·代码管理的登录用户必须同时满足以下两个条件: 1.该用户必须为微信开发者,即微信小程序、订阅号以及服务号的开发者; 2.该用户必须为实名认证的用户。 微信开发者·代码管理的登录方式包括: 1.通过微信开发者工具版本管理中的“代码管理”或工具栏中的“微信开发者·代码管理”快速进入。 2.通过浏览器微信扫码登录:https://git.weixin.qq.com 。 具有登录权限的用户,可在小程序专属项目组中创建远程仓库或创建其他非小程序专属项目组。 在微信开发者工具版本管理中添加远程仓库时,系统会自动拉取该小程序专属项目组下已创建远程仓库列表。用户可点击选择相应仓库,系统会自动填充该仓库的 URL 和名称,方便用户添加。 [图片] 详细内容可参考 文档。 微信团队 2019.01.28
2019-01-28 - 这文档是敌特派来的人写的吗?目的就是为了整死我们搬砖工吗?
事情是这样的。 我司一小破程序,打开时类似这样,显示一个logo,一个标题 [图片] 经过一个2秒的动画效果,logo和标题就移动到上面部分了,同时渐显出来一个loading组件,这些都是使用小程序的Animation API实现的。 [图片] [图片] 现在需求来了。 我们想在首屏渲染后。在图标往上移的动画执行周期中,将背景色缓慢从蓝色变为白色。 (别问为什么要变背景色,我们准备待会加完班拿上弹弓组团去打设计师家玻璃了) [图片] 有朋友会说了,这不是很简单嘛,弄个定时器去替换class不就行了? 我只想说,no no no。朋友,我们搬砖就要有搬砖的样子嘛。 什么时间搬,搬多少,什么时间停,都要严谨嘛。 天真的我,想当然的就拍着胸脯向BOSS表示小意思啦。 [图片] naive的我心里想着 肯定会有动画执行开始和结束一个callback接口的嘛 然鹅,、翻遍了小程序文档里关于动画的各个段落之后才发现 [图片] 神马?? 我不信!一定是我的眼刚刚瞎了,我要再看一遍。 [图片] [图片] [图片] [图片] [图片] PS 看,多么言简意赅的文档! 在看多了外面那些"妖艳贱货"的文档后,如此小清新的文档,还真让我这老司机虎躯一震。 // TODO 我当即在心里暗暗发誓,我一定要强烈建议我司将此文档规范引进并在我司大范围实践,太他【文明用语】高效了。 END PS 在我不懈的努力下 在某毒找到了一篇关于动画重置的实例 [图片] [图片] 哦也,三七三十一,一定是我聋了才没看见这么大个接口 同事心里还在做自我批判,怎么能轻易的就甩锅给腾讯爸爸。 祭出我的Ctrl+F大法 [图片] 果然。还是我太天真。竟然没有搜到 0/0? 在经过了一番苦苦的某毒搜索之后,猛然意识到,或许是我姿势不对? [图片] 谢天谢地,博客园诚不我欺。确实有这个东东。 我默默的打开了唯一的一条搜索结果学习了起来。你猜怎么着? [图片] 我发现了腾讯爸爸藏起来的彩蛋。 哇,没想到小程序团队这么调皮。 在动画相关的所有文档里,竟然半个字都没提有这几个事件。保密工作做的很到位。表扬。5星好评。 [图片] 根据文档,照猫画虎。 [图片] [图片] 控制台没有任何反应 [图片] 一定是我姿势不对,我换换姿势。 [图片] [图片] 一顿操作猛如虎,然鹅发现并没有什么卵用。 [图片] [图片] [图片] 我盯着这条说明,默默的给自己点上了一根烟后陷入了痛苦的沉思。 期间我尝试了各种姿势,都没有找到关于WXSS animation到底是个什么鬼。 我只知道有Animation这个动画API。或许他俩是一个东西? 但是为什么Animation里没有关于它的只言片语? [图片] 既然Animation里没有写,肯定是另外一套体系吧? 灵光一闪, oh no,别又是腾讯爸爸调皮了把文档藏起来了吧。 [图片] [图片] 经过地毯式的搜索及换遍了各种姿势想要跟我的小程序互动一把后。 [图片] [图片] [图片] 我选择死亡。 [图片] [图片] 我想起那天夕阳下调的微信小程序,那是我逝去的青春。。。 IDE: v1.02.1901230 Library: 2.4.2
2019-01-28 - (15)真机定位问题技巧
开发者在开发小程序的时候可能会碰到一些这样的问题: 问题1 开发者工具上看效果没问题,但是在真机上测试不行? 问题2 有用户遇到小程序功能无法使用的问题,但无法快速定位解决? 今天我们的小故事与大家分享一些真机定位的技巧,可以解决上面两个问题。 1 vConsole开发利器和远程调试功能 针对问题1,我们提供了 vConsole 开发利器和远程调试功能,可以协助开发者在定位真机上的问题。 vConsole 的有四个Tab面板,可以先看下 Log 面板,看是否有异常信息,异常类型 thirdScriptError 是框架捕捉到的开发者的代码执行的异常,可以优先处理异常信息看是否可以解决问题。Log 面板可以看到异常出现的文件和行数。 [图片] 除了异常日志,开发者还可以通过 console.log 接口在一些关键执行路径上打日志来定位问题,这些日志会呈现在 Log 面板上。 vConsole 默认是不开启的,可以通过下面2个方法来开启: 1 开发版和体验版可以点击小程序页面右上角的...按钮打开的菜单项“打开调试”来开启 vConsole。 2 正式版没有“打开调试”的菜单项,可以先通过开发版和体验版来开启 vConsole,然后再打开正式版。或者可以预埋一个隐藏操作,比如连续点击某个 Button 多次,然后调用 API 接口 wx.setEnableDebug 来打开。 vConsole 虽然强大,但在手机上查看大量的日志信息不方便,此外,vConsole 没有断点调试、无法修改样式,定位复杂问题需要花费比较多的时间。 小程序的业务逻辑运行在 AppService 层,页面渲染在 WebView 运行,并通过微信客户端通信,因此,我们想到了可以让 AppService 运行在开发者工具,页面渲染还是在手机 WebView,两者通过网络来通信,这样借助开发者工具的调试能力,就可以实现远程调试功能。 远程调试窗口通过手机客户端扫描开发者工具上生成的二维码来打开,无需像普通手机 H5 页面调试一样,需要在手机端进行一些设置。 [图片] 打开的远程调试界面和开发者工具的模拟器的调试界面很像,需要注意的是,要在 Console 里对小程序进行调试,需要将调试的上下文切换到 VM Context 1 。 [图片] 更多的远程调试的使用方法请参考使用文档。 2 意见反馈能力 对于问题2,小程序的使用反馈来自用户投诉,这种情况用户无法联系到开发者。我们遇见过有小程序功能出现问题,用户无法使用,但投诉无门的情况,而这些问题,开发者也没有途径去收集以及处理,这就导致了小程序服务质量下降,用户流失。 为此,我们开发了“意见反馈”功能,当出现问题时,开发者可以引导用户使用“意见反馈”进行反馈,并上传日志来辅助开发者定位问题。操作过程如下: 引导用户进入小程序帐号详情页面,具体可以在小程序界面点击右上角...按钮,选择关于菜单。接着在帐号详情页面点击右上角...按钮,选择意见反馈菜单进入页面。页面可以上传图片和日志,建议用户上传异常情况的截图,以及勾选允许开发者使用小程序日志选项上传日志,反馈信息越详细,越有助于定位问题。 [图片] [图片] 如果觉得上面的操作步骤太麻烦,开发者可以通过在页面 WXML 添加下面的按钮,用户点击按钮可以直接打开“意见反馈”页面。 [图片] 开发者需要定时处理用户的反馈,这样才能保证小程序的质量。开发者可以登录小程序管理后台,进入左侧菜单客服反馈,就可以看到用户的反馈内容以及下载日志来辅助定位问题。 [图片] 为了保证日志信息足够详细,开发者需要用下面的接口在代码的关键执行路径上写日志。 [图片] wx.getLogManager 接口的更详细使用请参考文档。 希望通过这些小技巧,可以帮助大家顺畅地开发小程序。
2019-04-29 - 获取用户位置信息时需填写用途说明
各位开发者:大家下午好。在一些小程序/小游戏的业务逻辑中,有时需要依赖用户所在的地理位置来提供服务,当前开发者可以通过调用 调用 wx.getLocation / wx.authorize 等接口获取用户的地理位置信息或授权。 根据 iOS 系统对用户隐私保护的要求,同时我们也为了让用户可以更好的判断是否要将地理位置信息提供给开发者,故调整为当小程序/小游戏获取用户地理位置信息时,开发者需要填写获取用户地理位置的用途说明。填写的说明将在地理位置授权弹窗中展示,如下图所示: [图片] 具体开发方法如下: 在 app.json 里面增加 permission 属性配置(小游戏需在game.json中配置): [代码][代码]"permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } } [代码][代码]详见 小程序开发文档/小游戏开发文档 可在开发者工具(1.02.1812260及以上版本)中进行调试。 2019年1月14日起新提交发布的版本将会受到此调整的影响。需要各位开发者注意,2019年1月14日起新提交发布的版本若未填写地理位置用途说明,则将无法正常调用地理位置相关接口,请及时填写地理位置用途说明。该调整策略在微信客户端 7.0.0 版本生效。另外,考虑到兼容性等问题,在微信客户端 7.0.0 版本以下的环境中不受此策略影响。 微信团队 2018.12.26
2019-04-28 - 社区经验分享与问题总结
1. 插件开发中,使用自定义组件需用到相对路径 "usingComponents":{ "alert":"../../components/alert/alert" } 注:选择自定义组件需要加上in(this) wx.createSelectorQuery().in(this).select('.xxxxx') 2. 获取小程序码相关 通过接口生成的小程序码page必须是已经发布的 生成小程序码后可在客户端开发工具中,通过二维码编译进行测试scene参数 [图片] 3. swiper组件current不重置问题(bug) 如:通过arr=[1,2]遍历swiper-item组件,当swiper滑动到current=1时,setData({arr:[2]});此时swiper会出现空白。 原因:current未重置为0,需自己去设置current。 https://developers.weixin.qq.com/community/develop/doc/00066c8beacfa05f24d7d144056800 4. 模板消息相关 时效性:1次支付可下发3条,1次提交表单可下发1条。(7天内有效) 对应性:发送消息的对象openId和formId是匹配的。 获取方式:发起支付或表单提交 5. 分包加载大小限制问题:使用分包加载时,如果在分包中使用插件,插件大小只会算在分包大小2MB与整包8MB内,不算入主包2MB。(之前算在了主包内,目前已修复)。 6. 获取unionId(包括openId)流程(前提:小程序 或 其主体公众号 与 微信开发平台账号关联) 开放平台关联同主体的公众号且 用户已经关注公众号: wx.login()=> 获取到code,后端通过appid+appserect+code,拿到openId+ session_key+unionId 开放平台关联小程序: 用户授权后通过wx. getUserInfo(需要授权)获取iv、encryptedData,然后解密(需要用到上面的 session_key),appid+ session_key + encryptedData + iv解密 得到unionId、openId及用户信息 [图片] 注:如果再次获取code会导致之前的session_key过期 7. h5与小程序跳转问题 公众号=>小程序:公众号自定义菜单可配置跳转到小程序 小程序=>h5:webview(需配置业务域名、webview不支持个人账号) 注:目前不支持h5与小程序的直接跳转 8. canvas原生组件覆盖自定义弹层的解决方案 用css样式控制器显示或隐藏,如hidden 纯显示性的canvas可以生成图片之后展示 9. 自定义弹层背景滚动问题 方法一:打开的函数中,如果自定义弹框当前显示,则isScroll设为true,否则设为false <scroll-viewclass="scanInvoice_content" height="100%"scroll-y="{{isScroll}}"> //设置Page的overflow-y属性值为hidden </scroll-view> 方法二:事件捕获,顶层加上catchtouchmove 10. 小程序图片分享截取变形或显示不全:保持分享的图片是5:4。 11. 授权问题 wx.authorize可以对除scope.userinfo之外的权限进行授权,scope.userinfo需要用<button open-type="getUserInfo"/>组件进行授权。 统一小程序下的用户拒绝授权之后会直接进入失败回调,这种情况可使用wx.openSetting引导用户授权。(用户手动删除小程序才会重新提示授权) 12. 数据绑定是双括号内只能是data里面的变量或者wxs里声明的函数。 13.不要用wx.request去访问微信接口,官方限制且不能把[代码]api.weixin.qq.com[代码]配置为服务器域名 基础库版本:v2.4.0 欢迎更新指正
2018-12-28 - AI人脸应用
这款颜值检测小程序使用了腾讯开放人脸识别API,本项目适合刚入门的同学练手,熟悉整个框架,整体实现如下: 后台定义接口调用函数,返回前端渲染需要的数据格式,以及构造汉化字典。 通过小程序提供的云函数编写后台逻辑,调用接口放在云函数处理即可。 前端开发根据自己的审美或者请人设计,想自己弄得可以参考material design的规范,或者去GitHub使用现成的UI组件。建议自己动手写写前端,个人经验,写过后会对数据结构设计,前后端分离有个很直观的印象,关键是能提高个人审美(手动围笑) 这里还需要的是对数据的持久化处理,可以选用云开发的数据库,也可以区别的平台,建议是找接口封装简单易用的。 最后是交互设计,这个要尽量简单明了,比如就提供一个上传图片按钮,不能占地方。比如对于图片的处理,用卡片类展示,最好使用瀑布流可以展示完全,用其他的对称的方案,后台需要做很多图像裁剪,影响速度和观感。比如历史记录入口,不推荐用tabbar的导航方式,减少了可视面积,建议是浮层提供交互,虽然路径会深一层,但是图片展示效果会好很多 下面上图片,给想上手的同学提供一点思路和灵感,扫码体验下,需要源码参照的同学,请在小程序内扫码联系作者。 [图片] [图片] [图片] [图片] [图片]
2019-01-06 - webview手指滑动事件问题
业务逻辑:用webview来展示H5,在H5页面里面添加手指滑动事件的判断,touchstart时记录手指按下的位置 starY = e.touches[0].pageY,touchend时记录手指离开时的位置endY = e.changedTouches[0].pageY,当endY - starY > 0时,表示手指向下滑动,反之向上滑动,=0时未滑动。 场景与表现: 1,开发工具,H5页面不超过屏幕高度,按照业务逻辑正常执行,手机上也一样 2,开发工具,H5页面高度超出屏幕高度,按照业务逻辑正常执行,但是在手机上,手指滑动频率比较正常时没问题,当手指滑动频率比较快的时候,用上面业务逻辑得到的结果就会出现异常,比如:明明是向下滑动,判断出的结果却是向上 希望官方大大帮我看下,为什么会出现这种情况
2018-12-24 - 官方大大进来一下,web-view问题
window.onload = function(){ let isApp = window.__wxjs_environment === 'miniprogram'; console.log(isApp) } h5页面嵌套在小程序里面,ios true 安卓 false
2018-12-21 - 小程序web-view组件、音频播放功能调整说明
各位开发者: 下午好。 从微信6.7.2客户端版本开始,以下功能的逻辑将进行调整,请开发者注意适配: 1.为便于用户方便使用小程序,web-view 组件在小程序全屏模式下,将统一显示导航栏 自该版本起,[代码]navigationStyle:custom[代码] 对 [代码]<web-view>[代码] 组件无效。 开发者在全屏模式下,无需再自行绘制 web-view 导航栏。 可详见文档: https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html 2.背景音频管理器 [代码]backgroundAudioManager[代码]在退出小程序后不再默认继续播放 近期发现部分小程序退出后仍在播放无意义的音频,引起用户疑惑。 因此自该版本起,若需要在小程序切后台后继续播放音频,需要在 app.json 中配置 [代码]requiredBackgroundModes[代码] 属性。 开发版和体验版上可以直接生效,正式版还需通过审核。 详见文档: https://developers.weixin.qq.com/miniprogram/dev/framework/config.html#%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE 请大家及时调整。 微信团队 2018.08.23
2018-09-25 - 我是如何学习小程序的
前段时间和大家一起分享了一篇关于学习方法内容《大牛与搬运工的差距——学习方法的力量》。我们将学习过程分成八步,并借鉴了敏捷开发的迭代思想,以达到自我迭代学习的效果。行胜于言,理论结合实践才是王道,所以本文我将基于前面的学习方法,分享我是如何学习微信小程序的。 关于小程序小程序是继订阅号、服务号、企业号后,16年腾讯新推出的又一种开发能力。小程序无需下载安装,可以在微信内便捷地获取和传播,可谓是一款“即扫即用,用完即扔”的便捷应用,同时具有近似APP般的出色使用体验。 坦白讲,此刻和大家分享小程序学习,已经有点后知后觉的意味了。小程序刚出来时,和很多人一样,我其实也是持有怀疑、观望的态度,即便到目前为止,我接触的小程序恐怕就是上次玩的那个“跳一跳”了。 最近也是因为想在我们的一款建站系统的开源项目中关联小程序,才决定学习了解下小程序,接下来我们就马上开启小程序的学习之旅。 1.确立目标我学习小程序的目标很明确,总的来说就两个: 1.学习小程序相关技术,能独立开发制作一款小程序。 2.将小程序与我们的开源项目(蝉知建站系统)关联打通。 2.拆分任务我们通过逆向分解,将每个目标拆分成具体的可执行任务。这个过程其实就是将目标细分整理成具体的小目标或问题,然后将其关联到一个个可执行的任务,目的是完成目标或解决问题。 完成我的两个目标,需要具体学习哪些知识,完成哪些事项呢?第一个目标需要学习小程序的代码框架结构、运行机制、组件用法、小程序后台操作以及小程序开发工具的使用等。第二个目标则需要学习小程序的API、插件、相关接口调用,以及蝉知建站系统这边的微信模块代码。 梳理之后,就变成了一个个需要完成的具体任务。这里我借助待办任务管理工具,将需要做的任务都罗列出来,方便后面执行跟踪。 [图片] 3.搜集知识资源学习过程中必然少不了各种学习资料,在搜集知识资源时主要侧重三方面:书籍、官方文档和网络技术文章。 我首先找的就是微信的官方文档——《小程序开发文档》,内容涵盖了快速入门的简易教程、组件、框架、API、开发者工具等内容,既权威又全面。其次书籍我选的也是由腾讯官方微信架构师熊普江编写的《小程序,巧应用:微信小程序开发实战 第2版》。学习过程中以这两份材料为主要资源,除此之外,还在CSDN、掘金、博客园等IT技术门户里查阅一些小程序相关的“干货”。 对于知识资源的搜集,还是要提醒大家注意一点,切勿做“收藏控”。很多朋友对于书籍、各种教程、网络文章等资源,购买了下载了收藏了,然后,就没有然后了,最终真的沦落为“收藏品”。而我们真正要做的是要把知识“收藏”并沉淀到我们脑子里。 [图片] 4.分配自身资源其实就是时间管理。我们时间、精力等各方面资源有限,所有的知识不可能平均分配有限资源。对于我们搜集的知识资源,要分清主次,然后使用不同的时间管理策略进行学习。 前面梳理了一些需要执行的学习任务,并整理了大量的学习资源,这一步要做的就是将这些任务、资源结合自身的实际情况,合理规划分配时间精力。每个人的情况不同,所以这一步因人而异,这里我还是借助待办工具,做一个具体可执行性的学习计划。 [图片] 5.勤学学习效率与效果取决于执行力。之前文章介绍过学习过程中的一些方法论,这里我简要地介绍一下学习小程序时的大体过程。 a.快速泛读 对于小程序的基本的代码框架结构、运行机制、后台管理操作以及开发工具的使用这些内容,都是快速的过了一遍。熟悉核心概念与基本的操作流程,具体的细节问题在后面的使用操作时在深入研究。 b.深入解读 根据小程序的MVVM开发模式,深入学习其视图层与逻辑层相关知识,包括常用的函数、事件、组件、API等的使用。并结合开发工具实践操作,通过做个demo案例来进一步了解这些知识点的运用。 c.问题攻读 对于学习过程中遇到的问题,先查阅资料或谷歌来解决。比如小程序里的WXML、WXSS与HTML、CSS还是有些区别的,包括JS的使用也有别于PC端的DOM操作机制。如果有难以解决的问题,可以先记下,或许后面会柳暗花明或找他人帮助。 这一步学习的正确姿势是在实践操作中发现问题,然后带着问题找答案。 6.建立知识体系不要以为这一步可有可无,建立知识体系有助于帮我们理清各个概念和知识点之间的关联,逐渐形成系统的知识体系。基于前面的大量泛读和深入解读,我们可以逐步构建出一个知识体系。这里我借助xmind脑图工具,整理了一下小程序的知识体系。 [图片] 7.好问基于前面问题攻读后还有未解决的问题,就要求教他人获取帮助了。提问也是一门艺术,这仍是一个学习交流的过程,而不是让别人去解决原本你该解决的问题。 向别人提问求助时,要讲问题的来龙去脉讲清楚,如怎么引起的,自己尝试做了哪些操作等等。这些年在众多技术交流社群和平台,见过很多奇葩的提问方式,这里列几个,与大家共勉。 a.直抛结果型 直接甩出一张截图或错误提醒,并附上一句:怎么解决?怎么破?跪求科普…… b.不懂就问型 不动脑,不动手,碰到问题就问。这种选手即便帮他解决了问题,他也学不到东西。 c.懒人型 与前者类似,但更滑稽可笑,看到这种问题我个人向来直接无视。比如“这个问题是这样操作么?”,“我这样整对不对?”,“是点这里么?”…… 8.分享复述 其实即便不提“费曼技巧”,我们很多人在学习时都会用到分享复述这个方法。无论是演讲、录视频、还是像我此刻这样写文章,其实都是在向他人阐述自己对知识的理解和观点,然后大家会给自己一些反馈,比如你的错误、问题,接着自己再回归到知识里完善不缺“漏洞”,如此迭代学习。 小结我将自己学习小程序的过程分为八步: 1.确立目标:没有目标的学习是不会有结果和效果的。 2.拆分任务:将目标分解成具体可执行的学习任务。 3.搜集知识资源:查阅官方文档、购买书籍、搜集网络干货文章。 4.分配自身资源:合理分配时间精力,制定学习计划。 5.勤学:强大的执行力是学习的根本保障。 6.建立知识体系:将学习的知识在脑海里建立系统模型。 7.好问:避免孤立学习,但好问也要会问。 8.分享复述:检验学习成果,提高学习效果的最好方法。 作者:GavinHsueh,专注项目管理、办公自动化、网站建设等web应用开发领域。
2018-06-19 - wxml里面数据绑定组建一个对象报错
- 当前 Bug 的表现(可附上截图) [图片] 我是在api文档看到应该有这个功能的,这还是api上面的demo改了下,不知道为什么不行,是不是我写错了,还是我犯了啥低级错误?? - 预期表现 - 复现路径 - 提供一个最简复现 Demo [图片]
2018-12-20 - 【重大bug】小程序导致微信闪退,bug必现
- 当前 Bug 的表现(可附上截图) 在页面中setData操作较频繁,但也不是特别频繁,看点击速度,最多也就是10秒进行setData20次,然后在iPhoneX真机中就直接闪退,把微信也给强退了,微信还让提交错误日志,闪退了几次之后还给警告了。安卓测试过的机型都没有出现闪退的现象,iPhone6S也没问题,iPhone7也出问题了。 - 预期表现 - 复现路径 - 提供一个最简复现 Demo
2018-12-13 - image标签缓存图片问题
- 当前 Bug 的表现(可附上截图) image标签中的网络图片在服务器端更换后,客户端图片并未更新,无论删除小程序还是清理微信缓存都不行。在开发者工具中是可以被更新的。基于http缓存策略,通过设置cache-control:no-store响应头强制关闭缓存也无果 - 预期表现 在服务器关闭缓存策略的情况下,客户端能够实现服务端更改图片后客户端响应更新后的图片 - 复现路径 任意一个服务端图片地址,图片更换后必现
2018-11-06 - image标签是不是缓存了图片
小程序上我使用image标签 路径是一张网络图片 我把服务器上的网络图片替换之后(名称不变) 小程序显示的还是原来的图片。。。 怎么回事???
2017-10-13 - 产品经理又提需求了
要求:小程序里面个人动态有更新的情况下,手机会响铃提醒用户去打开小程序,能打电话最好 我该怎么办???
2018-11-07 - 开发者工具bug!!!隐藏到屏幕侧边里面了!!亟待解决该问题
亟待解决!!!
2018-10-24 - java 后台发送鉴黄api access_token 状态码41001
appid: wxd25bb1943daf726f token: access_token=13_KMCAfVoLQuGLVC4pr5sAE_spy7xLnDMBYYeKKOkhK7fPD-v7dAc_uKC3HtG4mjEbt9P_5tPsbxmkbV1hGP7B7_9y0XuFjrbevr8xd--Gf12GjYIXP7rXhn1YGPUGIFcAHANPM 发送的post请求方式: [图片] 返回值: [图片]
2018-08-30 - 用nodejs封装几个常用人工智能接口
1.语音语音合成:接口:https://reptile.akeyn.com/voice/text2audio?content=合成的文字 例子:https://reptile.akeyn.com/voice/text2audio?content=我们在广州天河 语音识别:接口:https://reptile.akeyn.com/voice/recognition?url=mp3地址 例子:https://reptile.akeyn.com/voice/recognition?url=http://img.bazhuay.com/152896248842554.mp3 2.文字识别识别银行卡:接口:https://reptile.akeyn.com/ocr/bankcard?url=银行卡图片路径 例子:https://reptile.akeyn.com/ocr/bankcard?url=https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=888049859,2568911766&fm=27&gp=0.jpg 识别身份证:接口:https://reptile.akeyn.com/ocr/idcard?url=身份证图片路径 例子:https://reptile.akeyn.com/ocr/idcard?url=https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3264366825,616996887&fm=27&gp=0.jpg 识别营业执照证:接口:https://reptile.akeyn.com/ocr/idcard?url=营业执照图片路径 例子:https://reptile.akeyn.com/ocr/business?url=https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1528787767575&di=2092bea8f13a7d36980bcfea48672a77&imgtype=0&src=http://d.hiphotos.baidu.com/zhidao/pic/item/3b292df5e0fe9925cfa37f6b37a85edf8db17159.jpg 识别带文字的图片:接口:https://reptile.akeyn.com/ocr/general?url=有文字的图片路径 例子:https://reptile.akeyn.com/ocr/general?url=https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1528795796906&di=c969a302739ae5fa138619fbd1589428&imgtype=0&src=http://g.hiphotos.baidu.com/zhidao/pic/item/6159252dd42a283402bc167753b5c9ea15cebfb7.jpg 3.图片识别识别动物:接口:https://reptile.akeyn.com/image/animal?url=动物图片路径 例子:https://reptile.akeyn.com/image/animal?url=https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=741061832,2708652394&fm=27&gp=0.jpg 识别植物:接口:https://reptile.akeyn.com/image/plant?url=植物图片路径 例子:https://reptile.akeyn.com/image/plant?url=https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3121747171,2429610755&fm=27&gp=0.jpg 识别菜肴:接口:https://reptile.akeyn.com/image/dish?url=菜肴图片路径 例子:https://reptile.akeyn.com/image/dish?url=https://gss0.bdstatic.com/94o3dSag_xI4khGkpoWK1HF6hhy/baike/w%3D268%3Bg%3D0/sign=d21700a6ecfe9925cb0c6e560c9339e2/4d086e061d950a7bea3656a706d162d9f2d3c956.jpg 识别车型:接口:https://reptile.akeyn.com/image/car?url=汽车图片路径 例子:https://reptile.akeyn.com/image/car?url=https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=808288941,469095564&fm=27&gp=0.jpg 识别品牌:接口:https://reptile.akeyn.com/image/logo?url=logo图片路径 例子:https://reptile.akeyn.com/image/logo?url=https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=4067228399,201083271&fm=27&gp=0.jpg
2018-06-19 - 小程序上线后出现vConsole按钮
当小程序正式上线后,打开生产版也出现了vConsole按钮,多半是因为曾经打开过体验版,并且在体验版上打开了调试模式。这样打开生产版的时候,也允许调试窗口。解决方法就是,再次进入体验版,点击右上角三个点,关闭调试模式。这样生产上也就关闭了。 不知道是小程序故意的还是bug,可能是为了方便生产调试?但是我的体验版也可以做成和生产环境一模一样的啊。所以。。。。
2018-06-25 - 微信小程序中腾讯视频插件的使用。
首先要登录微信公众平台,在你的小程序后台的设置-第三方服务—插件管理,搜索插件并点击添加,之后等待插件开发者的同意申请。(备注:如果你的申请迟迟未通过,可以在https://github.com/tvfe/txv-miniprogram-plugin的Issues里new issue一条,填写你的小程序名称,你的qq/微信号,申请时间。然后提交,等待通过,亲测很快就通过了使用申请。) 然后可以download下https://github.com/tvfe/txv-miniprogram-plugin上的腾讯视频小程序播放插件在本地,用通过申请的小程序appid打开下载的代码包在微信开发者工具中查看,可以看到一个简单的demo。预览扫描二维码可以在手机上测试。 ———————————————————————————————————————————————————— 补充:vid的获得。 在腾讯视频上登录你的qq或者微信,然后实名认证通过后,上传视频审核通过后,点击你的视频播放,在视频左下有个分享,点击后复制html代码到你的剪切板等地,然后在https的路径里找到vid,这个vid就是你视频的唯一vid值。之后把它放到组件txv-video的vid里。 使用: 1、先在你的小程序的app.json里声明使用插件的 "plugins": { "myPlugin": { "version": "1.0.0", "provider": "wxxxxxxxxxxxxxxxxx" } }, 使用腾讯视频插件就对应 "plugins": { "tencentvideo": { "version": "1.1.1", "provider": "wxa75efa648b60994b" } } 2、在使用插件的页面的js文件里 const txvContext = requirePlugin("tencentvideo"); 3、使用插件的自定义组件,在json中定义需要引入的自定义组件。使用plugin://协议 "usingComponents": { "txv-video": "plugin://tencentvideo/video" } 4、页面上<txv-video vid="xxxxxxxx" playerid="txv1"></txv-video> 5、去看插件的使用方法,比如暂停,播放等。 先在onload中 this.txvContext = txvContext.getTxvContext('txv1'); 然后在你的具体的函数中调用需要的方法,如 this.txvContext.pause()
2018-06-29 - 每周社区 | 上周社区问题反馈以及功能优化更新
各位微信开发者: 大家下午好。从本周开始,我们每周收到的 问题反馈、处理进度、社区功能更新 将以公告形式同步给大家,希望和大家一同打造小程序的生态。 一、上周问题反馈和处理进度(06.25-06.29) (一)修复中的问题: 关于微信小程序设置过多监听事件后报错 查看反馈 @font-face引入新字体,仅在IOS下且为外部字体时生效。查看反馈 查看反馈 cover-view 出现 background 无法覆盖 padding 区域 查看详情 iOS下,切换input组件,键盘会重新推出且位置计算有误 查看详情 意见反馈日志读写太频繁 查看详情 安卓全屏模式下,web-view没有占满屏幕,底部有部分留白 查看详情 工具中showToast icon='none'时无法触摸穿透 查看详情 iOS页面中多个video标签,其中一个全屏退出后video展示异常 查看详情 安卓光标移动后无法移回最后 查看详情 chooseLocation 工具上偶现只返回了经纬度 查看详情 开发工具将窗体独立之后,页面刷新/切换后不渲染 查看详情 wx.pageScrollTo导致fixed元素闪动 查看详情 小程序插件页面无法配置导航栏样式 查看详情 工具上分包预下载接口分包大小计算有误 查看详情 setBackgroundColor报无权限 接口上线后未放开权限位 查看详情 工具全局查找的查找范围不包含wxs文件 查看详情 iOS下 textarea的padding无法置0 查看详情 (二)修复完成,待客户端版本上线 安卓textarea adjust-position 设置失效 查看详情 statusBarHeight安卓返回0 查看详情 安卓text长按复制失效 查看详情 (三)已修复的问题 touch事件手机上timestamp位置不一致 查看详情 (四)需求反馈 webview 业务域名 优化 查看详情 支持自定义下拉刷新圆点的位置 查看详情 开发工具素材管理支持svg 查看详情 待讨论评估支持需求: 提供微信版本和公共库版本的对应关系数据,其他类似反馈 查看详情 uploadFile支持多文件上传 查看详情 (二)近期社区能力更新 我们在社区收到了大家希望增加”关注问题“、”收藏“帖子和通知消息回答定位的需求,近期已新增优化如下能力: 1. 增加“关注问题”的能力,关注有消息变动,会收到消息通知 2. 所有帖子新增“收藏”功能 3. 在个人主页上有所体现关注和收藏功能 4. 优化消息通知回答定位 [图片] [图片] 三、《诚邀开发者共同营造社区》的反馈 1.关于社区的官方回答 我们每天都有安排相关的同学值班回复处理大家的问题。 但是我们希望通过这些改造,促进开发者们之间的交流, 问题和回答在大家思维碰撞中产生火花。而bug和需求类,由我们来处理。 [图片] 2.关于通知中心的反馈 已优化上线该问题,可以具体定位到该问题的回答、该帖子的评论。 [图片] 3.关于已知问题 关于已知问题,大家可以关注社区“已知问题”模块,看我们的已知问题和相关修复。 [图片] 4.关于社区的评论bug 大家反馈符号超过了编辑框,帮助我们发现了一个bug,我们已经修复了这个问题。谢谢大家的反馈。 [图片] 5.关于交流群 我们希望所有的交流可以在社区完成,方便更多的开发者看见这类问题。 [图片] 感谢大家的反馈,也希望大家和我们一起共同营造社区的良好氛围。 微信团队 2018.07.03
2018-07-03