- 小程序云开发 TypeScript 工程化实践
使用 TypeScript 因为云函数是 node,所以一般是不支持 TypeScript 的。但是 TypeScript 的类型是真的不错,所以决定使用 TypeScript 进行云开发,而且使用 ts 之后可以直接跟前端使用 interface 进行接口定义,不需要另外写文档。 初期:每个函数有个 src 目录,里面存放 ts 文件,每次进行函数发布之前先使用 tsc 进行编译,然后发布到云环境。这个方法有个缺点就是多个云函数直接相同的代码不能通用,只能拷贝多份分布在每个云函数,这样的话每次这些通用函数的 bug 修复需要更新很多云函数。 后期:把所有的云函数提取到另外的目录,使用 Rollup 进行编译到云函数目录并根据依赖动态生成 package.json 文件。这样的话不同的云函数之间可以使用相同的代码。 编译云函数 首先根据云函数名称确定源代码的入口和输出的文件夹路径 module.exports = async function build(functionName, version) { // 输出路径 const distPath = path.join(__dirname, `../cloudfunctions/${functionName}`); // 源文件入口文件 const entryPath = path.join(__dirname, `../cloudfunctions-original/functions/${functionName}/index.ts`); } 然后使用 rollup 的 typescript 和 commjs 插件把源文件编译成 node输出到指定的云函数目录。使用 alias 插件的原因是因为有部分数据同步的工作需要本地运行,然后我写了个 bridge,所以上传云函数的时候替换成 wx-server-sdk,通过编译的时候把 process.env.run 替换为 cloud 来对部分本地运行代码进行删除。 const bundle = await rollup.rollup({ input: entryPath, plugins: [ alias({ entries: [ { find: /^[\s|\S]*bridge\/index$/, replacement: 'wx-server-sdk' }, ] }), replace({ 'process.env.run': JSON.stringify('cloud'), }), typescript({ tsconfig: './tsconfig.json', sourceMap: false, outDir: distPath, include: [ "../cloudfunctions-original/**/*.ts" ], tslib: require.resolve(path.join(__dirname, `../cloudfunctions-original/third-lib/tslib.es6.js`),), }), commonjs({ extensions: ['.ts'], sourceMap: false, }), ], onwarn(warning) { if (warning.code !== 'PLUGIN_WARNING' && warning.code !== 'CANNOT_CALL_NAMESPACE') { console.warn(warning.message); } }, external: [...Object.keys(packageJson.dependencies), 'lodash/fp', 'https', 'fs', 'path'], treeshake: { moduleSideEffects: false, } }); 然后输出到硬盘 const outputOptions = { format: 'cjs', interop: false, dir: distPath, sourcemap: false, preserveModules: true, preserveModulesRoot: entryPath.replace('/index.ts', ''), exports: 'auto', banner: `/* 此文件自动生成,请勿手动修改,源文件位于 cloudfunctions-original/functions/${functionName} */`, } // generate code and a sourcemap const result = await bundle.generate(outputOptions); // write the bundle to disk await bundle.write(outputOptions); 然后根据依赖输出 package.json const functionDeps = flow( flatMap(prop('imports')), map(v => v === 'lodash/fp' ? 'lodash' : v), reject(v => includes(`/${functionName}/`)(v)), )(result.output); const functionPackageJson = { name: functionName, version: version || '1.0.0', ...pick([ 'description', 'main', 'author', 'license', ])(packageJson), dependencies: pick(functionDeps)(packageJson.dependencies), } // write the package.json to disk fs.writeFileSync(`${distPath}/package.json`, JSON.stringify(functionPackageJson, "", "\t")); 创建/更新云函数 上面完成了对单个云函数的打包,接下来我们需要进行批量的 build 和创建/更新云函数。 使用 miniprogram-ci 进行云函数的更新,因为 miniprogram-ci 不支持创建云函数,所以需要开通腾讯云开发,然后使用 @cloudbase/manager-node 进行云函数创建。 首先获取当前环境的所有云函数列表 const manager = new CloudBase({ secretId: process.env.secretId, secretKey: process.env.secretKey, envId: process.env.cloudEnv }) // 因为目前云函数最多 150 个,所以直接指定 150 个 manager.functions.getFunctionList(150) .then((res) => { functionList = res.Functions.map(v => v.FunctionName); console.log(JSON.stringify(functionList)); resolve(); }) .catch((err) => { console.log('err'); reject(err); }); 然后判断云函数是否存在,如果不存在,则进行创建。创建的时候需要注意,如果有 trigger ,可以添加 trigger 字段 try { config = require(`${functionDirName}/${filePath}/config.json`); } catch (error) {} console.log(`开始创建云函数: ${filePath}`) await manager.functions.createFunction({ func: { timeout: 3, name: filePath, installDependency: true, ignore: ['node_modules/'], triggers: config ? config.triggers : [], runtime: 'Nodejs10.15', }, functionPath: `${functionDirName}/${filePath}`, }) 否则,更新云函数 await ci.cloud.uploadFunction({ project: new ci.Project({ appid, type: 'miniProgram', projectPath: functionDirName, privateKeyPath, ignores: ['node_modules/**/*'], }), env: process.env.cloudEnv, name: filePath, path: `${functionDirName}/${filePath}`, remoteNpmInstall: true, }); 批量编译+创建/更新云函数 前面完成了命令式的编译和上传云函数,接下来我们把他们组合起来进行批量处理。 获取所有的云函数名称,因为我们把所有的云函数放到了一个目录,直接读取当前的所有文件夹就可以,然后可以自定义过滤规则。 const dirs = fs.readdirSync(dirName, { withFileTypes: true }) 然后对每个函数调用编译+上传。如果函数比较多,可以考虑使用 node 的 cluster 模块进行多进程处理。 使用 jenkins 进行自动上传云函数 前面已经把批量上传云函数写成了 node 可执行文件,接下来就可以使用 jenkins 进行云函数的发布控制。根据参数进行环境配置和上传云函数进行规则过滤。
2021-06-02 - 如何快速在微信小程序中接入微信对话开放平台
如何快速在微信小程序中接入微信对话开放平台 前言 之前我写了一篇《微信对话开放平台初体验》,链接地址如下: https://developers.weixin.qq.com/community/develop/article/doc/000666072c0ad8f876891815b56013?jumpto=comment&commentid=0000243ff409a0797a89feb535b4 相信看过的朋友,通过这篇文章,会对微信对话开放平台有大致的了解,无论是后台的配置项,还是提供的服务能力,都一目了然。这么好的平台,光看不用实属浪费。微信对话开放平台不光是可以接入微信公众号、微信小程序,还可以接入其他网站。虽然官方的指引文档和视频都有,但在实际开发过程中,仍然会遇到一些问题。本文将为你介绍,如何在微信小程序中,快速接入微信对话开放平台。帮你规避会遇到的各种坑,更顺利的完成微信对话开放平台的接入。 准备工作 你需要有一个自己的小程序,没有的话可以注册一个个人主体的,建议注册账号使用单独的QQ邮箱。 接入小程序插件 查看官方文档 首先进入微信对话开放平台官网(https://openai.weixin.qq.com/)。 [图片] 微信对话开放平台点击右上角的【使用文档】按钮,即可跳转到文档中心的【智能对话】版块,如下图所示: [图片] 我们要做的是小程序接入,需要选中对应的选项卡。 [图片] 这里给一个快捷入口,点下面链接就可以了。 https://developers.weixin.qq.com/doc/aispeech/miniprogram/intro.html 只是这里都只是介绍跟示例,具体怎么操作,需要点击这里【快速接入】,才能看到具体步骤。 [图片] 文档里面写了接入的基本步骤,比方说appid配置、怎么注册插件什么的。只是这个介绍写的过于简略,只看这个远远不够。 细心的你,会注意到这里有一个超链接文字。 [图片] 点击这个【申请使用插件】,你会跳转下面这个链接: https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx8c631f7e9f2465e1 [图片] 这是插件的详情页面,这里每个步骤写的详细多了。按理说看到这里,就不用我多说,照葫芦画瓢都会玩了吧。 就如龙哥(微信之父)所说的那样——生活是不美好的。 接入过程中,仍然会遇到一些坑,稍不留心,在某个步骤卡住,就进行不下去了。下面我会告诉你这些坑在哪,也希望官方能够及时调整文档,修正这些问题。 可能会遇到的坑 小程序后台添加插件(坑指数:1星) 这个有两种方法: 进入小程序后台【设置-第三方平台授权管理】,点击添加插件,搜索「openaiwidget」即可。 访问插件主页https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx8c631f7e9f2465e1&token=&lang=zh_CN 点击添加,会出现【申请成功】提示,页面刷新后会显示【已添加】。 [图片] 其实上面的操作还好,并没有什么坑。但是,当你添加后,小程序的后台不是实时更新的,比方说这种情况,我用方法2添加后,回到小程序后台,搜索会提示已达到上限,而列表是没有更新的。插件列表不同步的情况时有发生,记得添加后多刷新几次小程序后台页面,不然会以为这个插件没有添加成功。 [图片] 添加插件的时候,如果你的按钮是灰色,说明已经达到插件上限。个人小程序插件上限是5个,其他不清楚,如果你有不需要的插件,可以移考虑掉。 [图片] 我个人觉得5个插件是完全不够用的,像我这样的情况,接入的都还只是官方,如果要在这个基础上加非腾讯系第三方,感觉很难。 [图片] appid配置(坑指数:2星) 这个看起来也很简单,查一下appid,复制粘贴也没啥。 [图片] 可你看下面一个文档配置,发现事情并不简单。除了appid,还有个WechatSI要配置。第一次看我一头雾水,不知道是个啥。而且第一个version要自己查,第二个不是最新版本,要么都自己查,要么都是最新的,不知道这个地方上下表述不一致是个什么操作…… [图片] 我把这个名字,用全球最大的搜索引擎搜了一下,发现这个原来是【微信同声传译小程序插件】。 [图片] 这个插件是为了让对话支持语音转文字功能,这样可以让人机对话的交互方式更加丰富,而配置中没有说明。 查版本好也很简单,以【微信同声传译小程序插件】为例,先进入首页: https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx069ba97219f66d99&token=61191740&lang=zh_CN 选择【基本信息】,可以看到更新日志,这里面有最新的版本号。 [图片] 就是appid和版本号这里有点饶,还额外引入了个多的插件。后面的小程序修改app.json,注册组件什么的,相信各位也轻车熟路,文档这部分写的更详细,这里不做赘述。 文档JSON配置书写错误(坑指数:1星) 到了初始化配置,说明你已经搞定了网站的后台配置,可以专注写代码了。可刚写到这里,你发现控制台出现了你最不愿意看到的红色英文字符串。于是你觉得此事必有蹊跷,开始思考报错的原因。代码是从官方那边复制粘贴的,讲道理不应该报错啊。当你再次看这段配置的时候,发现了一个文档上的低级错误——没有加分号。 [图片] 当然这个小问题对你来说是小case了,只是就连这样复制粘贴一把梭,还要卡一下,难免有点不快。希望官方看到可以修正这个文档错误。 没错,我就是微信开放社区的列文虎克(列文虎克是微生物学开拓者)。 页面样式问题(坑指数:3星) 之所以给3颗星,是因为这个样式实在是不好调,可能是因为我有自定义的导航栏,导致我不能直接照搬这个100vh高度的样式。我需要自己写calc,减一个大约100px的高度。 [图片] 其实官方也很贴心,这个高度样式问题官方文档在第8点注意里面也写了,只是光这样是不够的,还是没有彻底解决样式问题。 [图片] 减去顶部导航栏高度后,你还是会看到样式很奇怪,不是下面输入框被挤压,就是上面消息第一条的图片(默认是大幂幂),上面少了半截,如下图所示: 我在这里地方调了很久,发现上面的第3步的配置,里面有很多高度的配置。经过一番研究调试后发现,改动这几处是可以调整页面每个部分的高度。 [图片] guideCardHeight、operateCardHeight、historySize、navHeight这些参数,可以根据需要自定调节,多试几次就知道是怎么回事了。 总结 总体来说,接入还是比较简单的,不需要自己写很多代码,只需要按照文档步骤来,根据实际需要,配置对应的参数,调整下样式基本就OK了。只是接入过程还是会遇到一些小问题,卡在这里也很耽误时间。另外审核这个也比较迷,以前都是最多半天搞定的,这次引入这个插件后,一直显示审核中,因此我也无法将最新版的小程序分享出来给大家体验。 下面放出官方示例,可以直观的体验各个功能模块,感受微信对话开放平台的魅力。 [图片] 彩蛋 我在自己的小程序「EXIF查看器」体验版中,接入了微信对话开放平台插件。我还录了一段20秒左右的演示视频,想看看微信开放社区中,有多少人知道这个对话回复说的是哪个梗。 https://v.qq.com/x/page/y3027wu64bu.html
2019-11-30 - 用小程序·云开发轻松构建二手书商城小程序丨实战
导语 很多大学有个普遍现象,毕业或者搬校区的时候,成堆成堆的书都被随便处理掉,作为过来人,每每想到都十分痛心可惜,而导致这种情况发生的原因,我认为主要还是归结学校原因,一方面没有提供靠谱便利的平台,另一方面,宣传不到位,基于此开发了这款小程序。下面挑了些开发过程中遇到的典型来讲解实现过程,感兴趣可以一览… 一:登录注册页 目前小程序有了详细的登录规范,参考官方示例,本程序的登录入口做了以下处理: 在需要涉及用户信息的部分,进行Modal提示进入,比如:游客发布、购买等 个人中心,未登录默认显示”点击登录“按钮 好了,先来看看登录页面效果图吧: [图片] 手机号获取(相关代码): [代码]<button class="phone" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> <block wx:if="{{phone==''}}"> 请点击获取您的手机号</block> <block wx:if="{{phone!==''}}"> {{phone}}</block> <image wx:if="{{phone==''}}" class="right" src="/images/right.png" /> </button> [代码] [代码] //获取用户手机号 getPhoneNumber: function(e) { let that = this; //判断用户是否授权确认 if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showToast({ title: '获取手机号失败', icon: 'none' }) return; } wx.showLoading({ title: '获取手机号中...', }) wx.login({ success(re) { wx.cloud.callFunction({ name: 'regist', // 对应云函数名 data: { $url: "phone", //云函数路由参数 encryptedData: e.detail.encryptedData, iv: e.detail.iv, code: re.code }, success: res => { wx.hideLoading(); //获取成功,设置手机号码 that.setData({ phone: res.result.data.phoneNumber }) }, }) }, }) }, [代码] 此处仅展示前端部分核心代码,手机号获取涉及到解密过程,需要配合云函数实现,具体的请参考完整demo注册页代码 目前该接口针对非个人开发者,且完成了认证的小程序开放(不包含海外主体)。 常用联系方式的校检: [代码]if (!(/^\w+((.\w+)|(-\w+))@[A-Za-z0-9]+((.|-)[A-Za-z0-9]+).[A-Za-z0-9]+$/.test(email))) { wx.showToast({ title: '请输入常用邮箱', icon: 'none' }); return false; } [代码] 同理相关正则: [代码]//手机号 /^[1][3,4,5,6,7,8,9][0-9]{9}$/ //QQ号 /^\s*[.0-9]{5,11}\s*$/ //微信号 /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/ [代码] 目前常用手机号,似乎就差10和12字段的没有了。 二:发布信息页 [图片] 步骤条实现 发布页有几个小地方值得留意: 顶部的步骤条,随操作流程一直在变。 步骤改变时,有个横向切换动画 价格设置,使用了步进器 刚刚上面之所以说这几个点,因为他们都是同出一源–vant组件 此组件的使用教程可直接看对应官网 https://youzan.github.io/vant-weapp/ 使用组件开发效率会高很多,避免重复工作,同时可以参考部分组件的写法,还是有很多值得学习的地方的。 textarea小注意 步骤二中备注信息那里使用了层级最高的原生组件textarea,这里有个特别使用注意项:如果下面tabbar是自己写的而非使用的自带原生的tabbar,会出现穿透现象,如下图示例: [图片] 我常用的解决办法,通过动态改变textarea的聚焦状况,当点击该区域时,设置聚焦显示真实textarea,当失焦之后,展示为view层,代码如下: [代码] <view class="beibox"> <view wx:if="{{!focus}}" bindtap="focus" >{{beizhu?beizhu:'请输入信息'}}</view> <textarea wx:if="{{focus}}" focus="{{focus}}" bindblur="loose" bindinput="beiInput" value="{{beizhu}}"></textarea> </view> [代码] [代码] data: { beizhu:'', focus: false //默认不聚焦 } //点击聚焦显示textarea隐藏view focus() { let that = this; that.setData({ focus: true }) }, //失焦隐藏textarea显示view loose() { let that = this; that.setData({ focus: false }) }, [代码] 三:首页 [图片] 上面左图是首页的进入后的初始样式,右图是下滑之后的动态页面,关于页面的样式布局方面,使用flex可以轻松搞定,我们重点说下面这点: 监控屏幕滚动实现动态响应 在上图第二张示例图中,随着页面下滑,顶部分类栏也随之置顶,下方也出现了一个返回顶部按钮,实现原理: 监控屏幕下滑高度,当大于我们设定的某个值时,元素进行渲染 这里我们需要使用页面的一个事件处理函数:onPageScroll [代码]//监测屏幕滚动 onPageScroll: function(e) { this.setData({ scrollTop: (e.scrollTop) * (wx.getSystemInfoSync().pixelRatio) }) }, [代码] 函数获取的是页面在垂直方向已滚动的距离(单位px),但我们页面布局使用了rpx计算,所以后面我们乘以设备像素比获取对应的rpx值 在view视图层中通过wx:if或者hidden进行控制显隐,区别在于:wx:if每次隐藏都是销毁了,而hidden只是不呈现,但依旧渲染到页面,具体的使用效果,可查看视图调试处的效果。 下面给个完整的返回顶部示例 [代码]<view class="totop" bindtap="gotop" hidden="{{ scrollTop<500 }}"> <image lazy-load src="/images/top.png" /> </view> [代码] [代码] data: { scrollTop: 0 //初始滚动高度为0 }, //监测屏幕滚动 onPageScroll: function(e) { this.setData({ scrollTop: parseInt((e.scrollTop) * wx.getSystemInfoSync().pixelRatio) }) }, //返回顶部 gotop() { wx.pageScrollTo({ scrollTop: 0 }) }, [代码] 四:详情页面 [图片] 小程序布局只要掌握一个flex,基本上就够了,所以这里不过多阐述样式问题,到时候如果有疑问可查看完整demo,都有注释的。 因为此小程序的使用对象及功用限制,所以和完整的商城相比少了一个购物车功能,支付购买在商品详情页即完成了,这里涉及到两个点,一是下单购买,二是购买之后的通知问题。 小程序内支付提现 不仅仅是支付包括提现,此程序都借助了tenpay这个模块,详细介绍: https://www.npmjs.com/package/tenpay 在小程序中的实例使用,可以参考之前社区之前发布的文章: 10行代码实现小程序支付功能!丨实战 当然,之前文章是教大家如何实现支付,关于提现流程也一样,先去看看tenpay的商户付款到余额的说明,再看一下此程序的相关代码,读一遍准能懂。 发送通知 此程序通知分为两类:短信通知、邮件通知 使用场景:用户下单后,对卖家进行短信+邮件通知,下单后订单状态改变使用邮件通知。 说一点题外话:小程序有一个自带的模板通知,在用户主动触发后7天内能推送模板信息,之前写这个程序的时候慎重考虑过,最后还是舍弃了,毕竟七天时间,不是每本书都那么畅销的。 邮件只需要有一个账户即可,短信通知却是要成本的,当然效果要比邮件好,配置起来的话,难度都一样,我们就以短信为例: 首先去腾讯云申请短信API: https://cloud.tencent.com/product/sms [图片] 按照提示操作,设置好短信签名,模板等。 配置云函数 新建sms云函数,代码如下: [代码] const cloud = require('wx-server-sdk') const QcloudSms = require("qcloudsms_js") const envid = 'zf-shcud'; //云开发环境id const appid = 140000001 // 替换成您申请的云短信 AppID 以及 AppKey const appkey = "abcdefghijkl123445" const templateId = 1234 // 替换成您所申请模板 ID const smsSign = "腾讯云" // 替换成您所申请的签名 cloud.init({ env: envid, }) // 云函数入口函数 exports.main = async (event, context) => new Promise((resolve, reject) => { /*单发短信示例为完整示例,更多功能请直接替换以下代码*/ var qcloudsms = QcloudSms(appid, appkey); var ssender = qcloudsms.SmsSingleSender(); var params = ["测试内容"]; // 获取发送短信的手机号码 var mobile = event.mobile // 获取手机号国家/地区码 var nationcode = event.nationcode ssender.sendWithParam(nationcode, mobile, templateId, params, smsSign, "", "", (err, res, resData) => { /*设置请求回调处理, 这里只是演示,您需要自定义相应处理逻辑*/ if (err) { console.log("err: ", err); reject({ err }) } else { resolve({ res: res.req, resData }) } } ); }) [代码] 提一个小点:在有多个云环境时候,如果涉及到查询云数据库等和云环境有直接干系的操作时候,最好在cloud.init({env: envid})这里声明一下环境,否则有小几率报错。 五、启动页设计 [图片] 启动页也算本程序一个亮点,首次进入就是一张美美的图给人一种身心愉悦之感,下面我们就详细说说这个怎么做: 哪些元素? 全屏背景图 倒计时跳转 说这个之前,大家注意一下整个页面是全屏了的,所以这里我们要配置一下页面参数: 在此页面的.json中这么配置: [代码]{ "navigationStyle":"custom" } [代码] 这就成功全屏了,接着我们来编写页面样式: [代码]<view class="contain"> <view class="go"> <button bindtap="go">跳过{{count}}s</button> </view> <image class="bg" src="{{bgurl}}"></image> </view> [代码] [代码].contain { width: 100%; height: 100%; position: relative; } .bg { position: absolute; left: 0rpx; top: 0rpx; width: 100%; height: 100%; z-index: -1; } .go { position: absolute; right: 30rpx; top: 150rpx; z-index: 9; } .go button { font-size: 28rpx; letter-spacing: 4rpx; border-radius: 30rpx; color: #000; background: rgba(255, 255, 255, 0.781); display: flex; justify-content: center; align-items: center; text-align: center; width: 160rpx; height: 60rpx; } [代码] 样式快速搞定,再来说说js部分。 倒计时功能: [代码]countDown: function() { let that = this; let total = 3;//倒计时总数3秒 this.interval = setInterval(function() { total > 0 && (total--, that.setData({ count: total })), 0 === total && (that.setData({ count: total }), wx.switchTab({ url: "/pages/index/index" }), clearInterval(that.interval)); }, 1e3); }, [代码] 背景图 实现有两种办法,第一是本地路径,第二是引用远程地址(可通过接口动态改变) 第一种好处是直接使用本地图片,加载速度快,第二种可以随时更换启动图,两种办法都试过了,最终我建议还是采用第一种办法,使用本地图片,如果使用远程地址,首次进入会出现短时间白屏,体验不好,当然,你也可以想办法把图片压缩再压缩,那就不存在加载慢了,但分辨率又成了个问题,所以具体如何使用,还是根据产品需求。 总结 纸上得来终觉浅,绝知此事要躬行,以上总结的是开发此程序中我认为遇到的典型问题,实践过程中肯定会有更多有意思的问题的出现,“面向百度”编程是一个方面,但我更建议“面向官方文档”,很多问题其实官方文档中都有很详细的说明和代码示例,如果阅读文档颇感费力,我建议你该静下心来,先熟悉下html,css,javascript相关内容,到时候再回过头来看你会发现“原来如此”。 如果你想要了解更多关于云开发CloudBase相关的技术故事/技术实战经验,请扫码关注【腾讯云云开发】公众号~ [图片]
2019-09-29 - 基础库2.8.3的聚合API是不是有问题?
之前小程序内用聚合统计是没问题的, 这两天升级到2.8.3之后发现统计结果不对. 在开发者工具上把基础库调整到2.8.2之后, 统计结果又正确了. 所以2.8.3的聚合API是不是有什么改动? 2.8.3运行后结果: [图片] 2.8.2运行后结果 [图片] 这是对应方法的代码: [图片]
2019-09-21 - 小程序的源码会不会泄露?
微信小程序发布成功之后。 用户有没有可能通过一些特殊的处理方式,来获取到当前微信小程序的源码? 对于这种类似的问题,腾讯官方技术方面,有没有做什么相应的应对措施? 由于一直没有找到,微信小程序的官方技术支持的人工联系方式。所以在这里把问题发布出来,希望各位做技术的同仁、以及小程序的官方技术人员,能够帮忙。 谢谢!
2018-10-23 - 小程序导出数据到excel表,借助云开发后台实现excel数据的保存
我们在做小程序开发的过程中,可能会有这样的需求,就是把我们云数据库里的数据批量导出到excel表里。如果直接在小程序里写是实现不了的,所以我们要借助小程序的云开发功能了。这里需要用到云函数,云存储和云数据库。可以说通过这一个例子,把我们微信小程序云开发相关的知识都用到了。 老规矩,先看效果图 [图片] 上图就是我们保存用户数据到excel生成的excel文件。 实现思路 1,创建云函数 2,在云函数里读取云数据库里的数据 3,安装node-xlsx类库(node类库) 4,把云数据库里读取到的数据存到excel里 5,把excel存到云存储里并返回对应的云文件地址 6,通过云文件地址下载excel文件 一,创建excel云函数 关于云函数的创建,我这里不多说了。如果你连云函数的创建都不知道,建议你去小程序云开发官方文档去看看。或者看下我录制的云开发入门的视频:https://edu.csdn.net/course/detail/9604 创建云函数时有两点需要注意的,给大家说下 1,一定要把app.js里的环境id换成你自己的 [图片] 2,你的云函数目录要选择你对应的云开发环境(通常这里默认选中的) 不过你这里的云开发环境要和你app.js里的保持一致 [图片] 二,读取云数据库里的数据 我们第一步创建好云函数以后,可以先在云函数里读取我们的云数据库里的数据。 1,先看下我们云数据库里的数据 [图片] 2,编写云函数,读取云数据库里的数据(一定要记得部署云函数) [图片] 3,成功读取到数据 [图片] 把读取user数据表的完整代码给大家贴出来。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: "test-vsbkm" }) // 云函数入口函数 exports.main = async(event, context) => { return await cloud.database().collection('users').get(); } [代码] 三,安装生成excel文件的类库 node-xlsx 通过上面第二步可以看到我们已经成功的拿到需要保存到excel的源数据,我们接下来要做的就是把数据保存到excel 1,安装node-xlsx类库 [图片] 这一步需要我们事先安装node,因为我们要用到npm命令,通过命令行 [代码]npm install node-xlsx [代码] [图片] 可以看出我们安装完成以后,多了一个package-lock.json的文件 [图片] 四,编写把数据保存到excel的代码, 下图是我们的核心代码 [图片] 这里的数据是我们查询的users表的数据,然后通过下面代码遍历数组,然后存入excel。这里需要注意我们的id,name,weixin要和users表里的对应。 [代码] for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } [代码] 还有下面这段代码,是把excel保存到云存储用的 [代码] //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) [代码] 下面把完整的excel里的index.js代码贴给大家,记得把云开发环境id换成你自己的。 [代码]const cloud = require('wx-server-sdk') //这里最好也初始化一下你的云开发环境 cloud.init({ env: "test-vsbkm" }) //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async(event, context) => { try { let {userdata} = event //1,定义excel表格名 let dataCVS = 'test.xlsx' //2,定义存储数据的 let alldata = []; let row = ['id', '姓名', '微信号']; //表属性 alldata.push(row); for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) } catch (e) { console.error(e) return e } } [代码] 五,把excel存到云存储里并返回对应的云文件地址 我们上面已经成功的把数据存到excel里,并把excel文件存到云存储里。可以看下效果。 [图片] 我们这个时候,就可以通过上图的下载地址下载excel文件了。 [图片] 我们打开下载的excel [图片] 其实到这里就差不多实现了基本的把数据保存到excel里的功能了,但是我们要下载excel,总不能每次都去云开发后台吧。所以我们接下来要动态的获取这个下载地址。 六,获取云文件地址下载excel文件 [图片] 通过上图我们可以看出,我们获取下载链接需要用到一个fileID,而这个fileID在我们保存excel到云存储时,有返回,如下图。我们把fileID传给我们获取下载链接的方法即可。 [图片] 1,我们获取到了下载链接,接下来就要把下载链接显示到页面 [图片] 2,代码显示到页面以后,我们就要复制这个链接,方便用户粘贴到浏览器或者微信去下载 [图片] 下面把我这个页面的完整代码贴给大家 [代码]Page({ onLoad: function(options) { let that = this; //读取users表数据 wx.cloud.callFunction({ name: "getUsers", success(res) { console.log("读取成功", res.result.data) that.savaExcel(res.result.data) }, fail(res) { console.log("读取失败", res) } }) }, //把数据保存到excel里,并把excel保存到云存储 savaExcel(userdata) { let that = this wx.cloud.callFunction({ name: "excel", data: { userdata: userdata }, success(res) { console.log("保存成功", res) that.getFileUrl(res.result.fileID) }, fail(res) { console.log("保存失败", res) } }) }, //获取云存储文件下载地址,这个地址有效期一天 getFileUrl(fileID) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL console.log("文件下载链接", res.fileList[0].tempFileURL) that.setData({ fileUrl: res.fileList[0].tempFileURL }) }, fail: err => { // handle error } }) }, //复制excel文件下载链接 copyFileUrl() { let that=this wx.setClipboardData({ data: that.data.fileUrl, success(res) { wx.getClipboardData({ success(res) { console.log("复制成功",res.data) // data } }) } }) } }) [代码] 给大家说下上面代码的步骤。 1,下通过getUsers云函数去云数据库获取数据 2,把获取到的数据通过excel云函数把数据保存到excel,然后把excel保存的云存储。 3,获取云存储里的文件下载链接 4,复制下载链接,到浏览器里下载excel文件。 到这里我们就完整的实现了把数据保存到excel的功能了。 文章有点长,知识点有点多,但是大家把这个搞会以后,就可以完整的学习小程序云开发的:云函数,云数据库,云存储了。可以说这是一个综合的案例。 有什么不懂的地方,或者有疑问的地方,请在文章底部留言,我看到都会及时解答的。后面我还会出一系列关于云开发的文章,敬请关注。
2019-09-07 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 小程序开发另类小技巧 --用户授权篇
小程序开发另类小技巧 --用户授权篇 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 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - 列表中多个 swiper 优化方案探讨
在做一个浏览图片的小程序, 页面中含有多个 swiper, 翻页多了之后滑动卡的很. 但又想在首页中展示大图轮播和自动翻页, 尝试过多个方案, 最终实现如下: 方案1: 减少单条记录中 swiper 个数, 只保留3个. 在 onChange 时切换图片. 优点: 每次只加载一张图片, 提高页面载入速度; 缺点: 切换图片不能平滑过渡, 每次切换图片会显示 loading. 同时在白色背景下会闪烁. 改为黑色背景会好很多. 使用方案1之后, 会大大减少页面中 swiper-item 的数量, 但一旦加载多页后, swiper个数多了还是会卡顿, 尤其是在 android 下, 更加明显. 方案2: 通过wx.createIntersectionObserver()来监测当前页面中显示元素, 使用二维数组分页之后, 可以控制只让当前显示页来使用 swiper 来轮播, 其他已经翻过去的页面都可以设置为组图中的单张图片(或者直接设置成空白占位). 这样可以保证基本上只有一个 pageSize 页面中含有 swiper(最多两个页面, 在翻页交界过程中监测都在当前屏幕中显示), 能大大减少卡顿. 目前使用了这两个方案后, 用 Android手机测试, 滑动基本上就不卡了. 如果还有其他方案, 欢迎大家探讨.
2019-04-05 - CSS3 Animation动画的十二原则
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 [代码] 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) [图片] 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 HTML [代码] [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].one .shape { animation: one 4s infinite ease-out; } .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); } @keyframes one { 0%, 15% { opacity: 0; } 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; } 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; } 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; } } body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 预备动作 (Anticipation) [图片] 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码] HTML [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; } .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); } @keyframes two { 0%, 15% { opacity: 0; transform: none; } 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); } 28%, 38% { transform: translateX(-2em); } 40%, 45% { transform: translateX(-4em); } 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); } 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); } 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; } 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 演出布局 (Staging) [图片] 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 [代码] 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 HTML [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].three .shape.a { transform: translateX(-12em); } .three .shape.c { transform: translateX(12em); } .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; } .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; } @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 26%, 30% { transform: rotateZ(-40deg); } 32.5% { transform: rotateZ(-38deg); } 35% { transform: rotateZ(-42deg); } 37.5% { transform: rotateZ(-38deg); } 40% { transform: rotateZ(-40deg); } 42.5% { transform: rotateZ(-38deg); } 45% { transform: rotateZ(-42deg); } 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 58%, 100% { transform: none; } } @keyframes threeb { 0%, 20% { filter: none; } 40%, 50% { filter: blur(5px); } 65%, 100% { filter: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) [图片] 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 [代码] 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 HTML [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); } .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); } @keyframes four { 0%, 10% { transform: none; } 26%, 30% { transform: rotateZ(-45deg) scale(1.25); } 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); } 50%, 75% { transform: rotateZ(-45deg) scale(1.1); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 跟随和重叠动作 (Follow Through and Overlapping Action) [图片] 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 [代码] 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 HTML [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </div> </article> [代码] CSS [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; } .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); } 15%, 25% { transform: translateX(-12em); opacity: 1; } 85%, 90% { transform: translateX(12em); opacity: 1; } 100% { transform: translateX(12em); opacity: 0; } } @keyframes five-container { 0%, 35% { transform: none; } 50%, 60% { transform: skewX(20deg); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 缓入缓出 (Slow In and Slow Out) [图片] 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 [代码] 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 HTML [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码] CSS [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); } @keyframes six { 0%, 5% { transform: translate(-12em); } 45%, 55% { transform: translate(12em); } 95%, 100% { transform: translate(-12em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 弧线运动 (Arc) [图片] 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 HTML [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </div> </article> [代码] CSS [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; } @keyframes move-right { 0% { transform: translateX(-20em); opacity: 1; } 80% { opacity: 1; } 90%, 100% { transform: translateX(20em); opacity: 0; } } @keyframes bounce { 0% { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] [图片] 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 HTML [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; } .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); } @keyframes sevenb { 100% { transform: rotateZ(360deg); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 次要动作 (Secondary Action) [图片] 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 HTML [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; } .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); } 70%, 100% { transform: translateX(-10em); } } @keyframes eight-shape-b { 0% { transform: none; } 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 32% { transform: translateY(-1.25em); opacity: 1; } 34% { transform: translateY(-1.75em); opacity: 1; } 36%, 38% { transform: translateY(-1.25em); opacity: 1; } 42%, 60% { transform: translateY(-1.5em); opacity: 1; } 75%, 100% { transform: translateY(-8em); opacity: 1; } } @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); } 70%, 100% { transform: translateX(10em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 时间节奏 (Timing) [图片] 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 [代码] 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 HTML [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; } .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; } @keyframes nine { 0%, 10% { transform: translateX(0); } 40%, 60% { transform: rotateZ(90deg); } 90%, 100% { transform: translateX(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 夸张手法 (Exaggeration) [图片] 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 HTML [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码] CSS [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); } @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); } 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); } 70%, 100% { transform: rotateZ(360deg) scale(1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 扎实的描绘 (Solid drawing) [图片] 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 HTML [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </div> </div> </article> [代码] CSS [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; } .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; } .eleven .shape span.front { transform: translateZ(3em); } .eleven .shape span.back { transform: translateZ(-3em); } .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); } .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); } .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); } .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); } @keyframes eleven { 0% { opacity: 0; } 10%, 40% { transform: none; opacity: 1; } 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; } 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 吸引力 (Appeal) [图片] 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码] HTML [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </div> </div> </article> [代码] CSS [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; } .twelve .item { background-color: #1f7bb6; position: absolute; } .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; } .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; } .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; } .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; } @keyframes show-container { 0% { opacity: 0; transform: rotateX(-90deg); } 10% { opacity: 1; transform: none; width: 4em; height: 4em; } 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; } 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; } } @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; } 20%, 85% { opacity: 1; transform: none; } 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); } } @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; } 35%, 80% { transform: none; opacity: 1; } 90%, 100% { opacity: 0; transform: scale(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码]
2019-03-21 - 微信小程序提交审核被拒的常见情况说明
微信不断开放小程序的种种能力和接口之后,小程序的开发以及需求达到了一个增长的高潮期。但是毕竟是新产品,开发者们通常会遇到审核不通过的问题。下面是一些总结,希望对你有用。 除本微信小程序平台常见拒绝情形外,开发者还应遵守《微信小程序平台服务条款》及腾讯公布的相关规则、规范。 帐号基本信息 1.1 小程序名称、简介、logo、服务范围、服务标签、帐号基本信息文字均不得: (1)侵犯他人权益(著作权、商标权、肖像权、名誉权等)。包括但不限于,使用或包含不属于该小程序主体的品牌或商标、标识等内容或与之相似的内容、信息、特殊角标。示例:检查名称、简介中是否含有该小程序不属于该帐号的权益。 (2)含有商业化用语的、热门小程序名称、“国家级”、“最高级”等新广告法明令禁止或其他无关的词语。 (3)含有政治、色情、敏感、暴力血腥、恐怖、其他国家法律法规禁止的词汇及违法内容。 1.2 特别规则 1.2.1 小程序名称、简介: (1)小程序的简介需明确介绍小程序的功能点,不能使用模糊的词义表达,比如:该小程序旨在提高用户的生活品味、该小程序旨在提高用户的购物体验。示例:能在简介中提炼该小程序的几个功能点。 (2)名称、简介的信息表达的意思必须有关联,具有一致性,并应与实际提供的功能一致,不含有与功能无关的搜索热词。示例:简介中能找到小程序名称或者分拆出来的词汇。 (3)小程序名称不能以电话、邮件、日历等广义归纳类、普遍且不具有识别性词语来命名。示例:名称不是是单词汇,必须是两个词以上的组合,当无法判断时,审核人员可主观判断。 1.2.2 小程序头像 logo: (1)小程序头像 logo 清晰度不够时,不予通过。示例:无法看清、分辨、识别图片中包含的各个元素,如:文字、物体、形状等。 (2)小程序头像logo应与名称、简介保持一致 1.2.3 小程序的服务范围和服务标签: 小程序所设置的服务标签,应与所选的服务范围保持一致。标签不能超出服务范围。示例:服务范围是家政,服务标签是美食。 服务类目审核 服务类目是指开发者按照小程序所提供的服务类型和所涉及的服务内容,在平台提供的分类分级表格中选择对应的行业范围。 2.1 小程序的类目要和自身所提供的服务一致。 2.1.1 小程序服务类目所对应的页面中的核心内容必须与该类目一致。 2.1.2 必须保证用户在该页面能使用该服务类目,不得隐藏,不得进行多次跳转。 2.2 小程序的服务类目链接使用正常,不存在违法违规或不符合与腾讯所签署的相关协议、腾讯公布的相关规则、规范等内容。 示例: (1) 小程序服务类目所对应的页面链接不能正常打开。 (2) 小程序服务类目所对应的页面链接加载非法信息。 (3) 小程序服务类目所对应的页面链接加载恶意、色情广告。 (4) 小程序服务类目所对应的页面链接加载侵犯他人权益的内容;含有商业化用语的、热门小程序名称、“国家级”、“最高级”等新广告法明令禁止或其他无关的词语、不含有政治、色情、敏感、暴力血腥、恐怖、其他国家法律法规禁止的词汇及其他违法内容。 小程序整体审核规则 3.1 小程序基本功能审核规范 3.1.1 小程序所实际提供的功能点,需与小程序的简介一致。示例:功能包括但不限于简介中提炼的功能点; 3.1.2 小程序所提供的所有服务类目功能,必须在小程序首页得到体现,即在小程序首页必须能直达或者经过2次点击到达所有本文档2(服务类目审核)中提交的服务类目页面; 3.1.3 小程序实际所提供的服务不得属于尚未开放的服务范围。不应超出小程序平台已开放的类目库范围。示例:游戏尚未开放。 3.1.4 小程序中若存在隐藏或付费功能(比如仅充值可见,仅会员可见等受限功能点),该功能的实现不得含有色情、暴力、政治敏感或其他违法违规内容,开发者提供的测试号需可完整呈现和体验该功能; 3.1.5 小程序的功能应具有使用价值,不能过于简单,示例:只有一个页面,只有一个按钮; 3.1.6 未经腾讯公司授权的情况下,不得在小程序中提供与微信客户端功能相同或类似的功能,示例:小程序功能不能包含朋友圈、漂流瓶等。 3.1.7 在未经允许或未经腾讯公司授权的情况下,不得展示和推荐第三方小程序。示例:不能做小程序导航,不能做小程序链接互推,小程序排行榜等。 3.1.8 小程序功能的使用,无需以关注或使用其他号为条件。示例:使用A小程序时,必须同时使用B小程序 3.2 小程序页面内容审核规范 3.2.1 小程序的页面内容中,存在诱导类行为,包括但不限于诱导分享、诱导添加、诱导关注公众号、诱导下载等,要求用户分享、添加、关注或下载后才可操作的程序,含有明示或暗示用户分享的文案、图片、按钮、浮层、弹窗等的小程序,通过利益诱惑诱导用户分享、传播的小程序,用夸张言语来胁迫、引诱用户分享的小程序,强制或诱导用户添加小程序的,都将会被拒绝; 3.2.2 小程序的页面内容中,主要为营销或广告用途(如内含空白广告位、招商广告等),将会被拒绝;示例:漂浮悬浮广告,含有功能使用的页面中的广告展示比例超过50%,广告遮挡功能。 3.2.3 小程序的页面内容中,存在对用户产生误导、引发用户恐惧心理、严重破坏用户体验或损害用户利益的谣言类等内容的,将会被拒绝; 3.2.4 小程序的页面内容中,不能存在测试类内容;示例:算命,抽签,星座运势等。 3.2.5 小程序的页面内容中不能存在虚假、欺诈类内容,包括但不限于虚假红包、虚假活动、宣传或销售侵害他人合法权益的商品,仿冒腾讯官方或他人业务,其他可能造成微信用户混淆的内容和服务等; 3.2.6 小程序的页面中不能含有传播骚扰信息、广告信息和垃圾信息等内容; 3.2.7 小程序的页面中不得含有可能违反与腾讯签订的、任何形式的服务协议、平台协议、功能协议的内容; 3.2.8 含有发布、传送、传播、储存违反国家法律法规的或含有以下信息内容的,将会被拒绝: 3.2.8.1 反对宪法所确定基本原则的,危害国家安全、泄露国家秘密、颠覆国家政权、破坏国家统一 、损害国家荣誉和利益的小程序; 3.2.8.2 任何带有虚假、欺诈内容等的小程序不予通过; 3.2.8.3 任何召集、推销、鼓动犯罪或有明显侵犯社会善良风俗行为的小程序不予通过; 3.2.8.4 任何包含法律法规禁止传播内容的小程序不予通过; 3.2.8.5 小程序内容包含反政府、反社会或不符合主流政治的行为的,或存在煽动性的涉政言论或国家法律禁止的内容的,或含有散布谣言,扰乱社会秩序,破坏社会稳定信息的,不予通过; 3.2.8.6 小程序内容不能含有色情素材(即旨在激发情欲,对性器官或性行为的明确描述或展示,而无关美学),或存在涉嫌宣扬传播淫秽、色情内容信息,包括暴露图片、挑逗内容等的,或包含非法色情交易的信息; 3.2.8.7 小程序内容不能包含煽动民族仇恨、民族歧视、破坏民族团结的内容、破坏国家宗教政策、宣扬邪教和封建迷信的; 3.2.8.8小程序内容不能包含展示人或动物被杀戮、致残、枪击、针刺或其他伤害的真实图片,描述暴力或虐待儿童的,或包含宣扬暴力血腥内容的,或包含侮辱或者诽谤他人,侵害他人合法权益信息的,将会被拒绝; 3.2.8.9小程序内容不能包含赌博、竞猜和抽奖的。 3.2.9 小程序内的图片上不能含有广告、网址或虚假内容。 3.2.10 小程序代替用户发表、发送、转交任何内容前,必须征得用户明确同意和授权。 3.2.11 小程序的服务提供者必须提供过滤不当内容的措施。示例:设置对发布色情、赌博等涉嫌违法违规的词汇进行过滤提示的措施。 3.2.12 小程序页面中不能存在误导和错误暗示腾讯公司与该小程序有任何合作、投资、背书关系的内容,例如误导和错误暗示腾讯公司是该小程序运营者,或者误导和错误暗示腾讯公司以任何形式表示认可其质量、服务或与其存在合作关而该小程序事实上并非为腾讯公司运营。 3.3 可用性和完整性 3.3.1 提交的小程序须是一个完成品,要求可以打开,可以运行,且不可以是一个测试版。示例:不可运行、存在崩溃、闪退、按钮没有响应、文字表述不完整等。 3.3.2 本身会崩溃,或小程序程序会造成微信客户端崩溃的,将会被拒绝。 3.3.3 存在严重Bug的小程序(如无法添加和打开、无法返回和退出、卡顿严重等),将会被拒绝。 3.3.4 若小程序中存在帐号体系,需提供测试号,包含帐号和密码(可以体验所有功能)。 3.4 用户隐私和数据安全 3.4.1 在收集和使用用户任何数据时,必须明确告知用户该数据的用途,确保经过用户明确同意和授权,并应在用户同意和授权的范围内进行合理使用。在用户注销帐号后应相应删除相关数据。数据包括但不限于获取地理位置、用户通讯录、用户手机号码等。 3.4.2 不得在小程序任何页面请求或诱导用户输入微信用户的用户名或密码。 3.4.3 不得将搜索小程序功能加入小程序。 3.4.4 不得在页面中进行或将通过小程序收集到的用户数据私下进行出售、转交、交易、越权披露或泄露。 3.4.5 不得在未经用户授权同意的情况下,显示用户相关数据,比如:头像,昵称等信息。 3.4.6 小程序不得要求用户降低手机操作系统安全性(如要求iPhone 用户越狱、Android 用户ROOT 等)后,方能使用相关功能。 3.4.7 若小程序有需要追踪用户的地理位置的功能,则必须提供退出该位置追踪的功能和明确指示。 3.5 技术实现规范性 3.5.1 需要提供小程序文档和说明 3.5.2 禁止视频、音乐、语音等多媒体的自动播放 3.5.3 安装或运行其他可执行代码的程序,将会被拒绝 3.5.4 违规加载或更新代码,将会被拒绝 3.5.5 如果小程序有账户系统,必须提供能正常使用且易于发现的“退出”账户选项。 3.6 UI 规范 3.6.1 符合 WeApp UI 规范 3.6.2 小程序页面内的浮层和弹窗可关闭 3.6.3 小程序的界面必须遵守微信的外观和功能,不得提供改变微信外观和功能的产品体验。 3.6.4 小程序的界面不得模仿系统通知或警告诱导用户点击。 3.6.5 小程序头像logo需使用透明或有色背景。若使用白色背景,需使用有色边框。 相关小程序项目案例:https://github.crmeb.net/u/demo
2019-03-20 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 【优化】解决swiper渲染很多图片时的卡顿
相信各位在开发的时候应该有遇到这样一个场景,比如商品的图片浏览,有时图片的浏览会很大,多的时候达几百张或上千张,这样就需要swiper里需要很多swiper-item,如此一来渲染的时候就会很消耗性能,渲染时会有一大段的空白时间,有时还会造成卡顿,体验非常差,下面给大家介绍一下我的解决方案。 首先是wxml结构: [图片] js: [图片] [图片] 主要是利用current属性,swiper里面只放3个swiper-item,要显示的图片放在第二,第一和第三放的是加载的动画背景,步骤如下: 1. 将请求到的数据存入一个数组picListAll内,这里不需要setData,只需要在data外面定义一个变量就行了,以减少渲染性能。 2. 把要显示的图片路径赋值给picUrl, 3. 切换的时候根据bindchange获取current属性,当current改变时判断当前图片在picListAll的index,根据index拿到图片再赋值给picUrl 主要实现步骤就是以上3 步,比较简单,要注意的是当切换到第一张和最后一张的时候要判断一下,把loding动画去掉,请求的时候还可以传入index参数以显示不同的图片,方便从前一页点击图片进入到此页面时能定位到该图片,例子里我是自己mock数据的,只是为了展示,如果你有服务器的话可以弄几百张看看效果,对比直接渲染和用以上方式渲染的差异。当然,这只是我的解决方案,如果各位有更好的方案欢迎一起讨论,一起进步. 系甘先,得闲饮茶 完整代码:https://github.com/HaveYuan/swiper
2019-01-25 - 提问:getImageInfo有同步版本吗?
如题:getImageInfo有同步版本吗?或者没有什么方法使 getImageInfo 同步执行? 如下代码: var newImgs = []; for (var i = 0; i < imgs.length; i++) { // imgs为网络图片路径数组 wx.getImageInfo({ src: imgs[i], success: function (res) { var localImg = res.path; newImgs.push(localImg); } }) } 要求: 获取后的newImgs图片顺序也与imgs 的顺序一致。 有没有什么办法实现?
2018-08-30 - 小程序页面间的值传递
自己写了两个全局方法,用于解决页面间传递参数问题,包括A页面跳转到B页面,B页面返回A页面后更新A页面状态,或者A页面向B页面传递参数等 [代码]/**[代码][代码] [代码][代码]* this.radio对象数据结构[代码][代码] [代码][代码]* {[代码][代码] [代码][代码]* name:{[代码][代码] [代码][代码]* data,//可以是任意数据类型[代码][代码] [代码][代码]* methods//一个数组,用于缓存回掉方法[代码][代码] [代码][代码]* }[代码][代码] [代码][代码]* }[代码][代码] [代码][代码]*/[代码][代码]//接收数据[代码][代码]receive(name, callBack) {[代码][代码] [代码][代码]//初始化存储对象[代码][代码] [代码][代码]if[代码] [代码](![代码][代码]this[代码][代码].radio) [代码][代码]this[代码][代码].radio = {};[代码][代码] [代码][代码]if[代码] [代码](![代码][代码]this[代码][代码].radio[name]) [代码][代码]this[代码][代码].radio[name] = {};[代码][代码] [代码][代码]//如果存储对象指定name的对象中data存在,则直接执行回掉[代码][代码] [代码][代码]if[代码] [代码]([代码][代码]this[代码][代码].radio[name].data) {[代码][代码] [代码][代码]callBack([代码][代码]this[代码][代码].radio[name].data);[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]if[代码] [代码](![代码][代码]this[代码][代码].radio[name].methods) [代码][代码]this[代码][代码].radio[name].methods = [];[代码][代码] [代码][代码]//否则添加到缓存队列中[代码][代码] [代码][代码]this[代码][代码].radio[name].methods.push(callBack);[代码][代码] [代码][代码]}[代码][代码]},[代码][代码]//发送数据[代码][代码]send(name, data) {[代码][代码] [代码][代码]if[代码] [代码](![代码][代码]this[代码][代码].radio) [代码][代码]this[代码][代码].radio = {};[代码][代码] [代码][代码]if[代码] [代码](![代码][代码]this[代码][代码].radio[name]) [代码][代码]this[代码][代码].radio[name] = {};[代码][代码] [代码][代码]//如果方法存在,则执行所有缓存的方法[代码][代码] [代码][代码]if[代码] [代码]([代码][代码]this[代码][代码].radio[name].methods) {[代码][代码] [代码][代码]this[代码][代码].radio[name].methods.forEach((method) => {[代码][代码] [代码][代码]method(data);[代码][代码] [代码][代码]})[代码][代码] [代码][代码]this[代码][代码].radio[name] = [代码][代码]null[代码][代码]; [代码][代码]//清除[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]this[代码][代码].radio[name].data = data;[代码][代码] [代码][代码]}[代码][代码]},[代码] 将上述两个全局方法添加到app.js中,然后就可以使用了,使用方式如下 [代码]var[代码] [代码]app = getApp();[代码] [代码]//接收数据,需要传一个key,和一个回掉函数,[代码][代码]app.receive([代码][代码]'shan'[代码][代码], (res) => {[代码][代码]//res就是send传递的数据,这里是28[代码][代码] [代码][代码]//获取数据后执行的代码[代码][代码]})[代码] [代码]//发送数据,需要传一个key,和要发送的数据,这里发送的数据是一个数字28[代码][代码]app.send([代码][代码]'shan'[代码][代码], 28);[代码]可以在A页面调用receive方法,预先写好回掉函数改变A页面状态,然后跳转到B页面调用send发送数据并返回,这时会执行回掉函数改变A页面的状态 采用发布订阅模式 其原理就是创建一个全局变量radio对象,通过send方法将指定名称的参数存储在radio中,调用receive方法后判断对应名称的参数是否存在,如果存在则直接执行回掉函数,如果不存在就将回掉函数存储在数组中,等待获取数据后执行 可能有问题,欢迎交流
2019-01-17