- 《小程序云开发挑战赛获奖作品》 附体验二维码.附链接
校园赛道 [图片] 职业赛道 [图片] 校园赛道1. QSCamera微信小程序QSCamera面向浙江大学校级学生组织求是潮的摄影部及视频团队两个部门,用于部门内或跨部门的器材借还登记、借用记录查询、当前器材状态查询等场景,本文从12个方面全面的介绍了该款小程序 https://developers.weixin.qq.com/community/develop/article/doc/0006a8dd4f89684f45fad49fc56c13 [图片] 2.BJUT活动助手BJUT活动助手 Activity Helper,一个基于北京工业大学校园内的各项信息开发的一款集报名、统计、汇总、浏览于一体的活动助手小程序。 https://developers.weixin.qq.com/community/develop/article/doc/0000a40f544cc8ecaefadfaab56813 [图片] 3. 校园二手购 为了方便广大学生用户,不用在贴吧等地方发布二手交易,而是有一个官方的、专门的平台来提供给学生。小程序的使用者可以在校园二手购平台上发布闲置的物品,以供大家购买。用户可以浏览全部发布的商品,在自己喜欢的商品下面留言以取得和售主的联系,在双方都谈妥之后,就可以进行线下交易。 https://developers.weixin.qq.com/community/develop/article/doc/000e0676d484384bb1fa76e2156413 [图片] 4.大学校园闲置物品交易平台在大学校园闲置物品交易平台中,大学生能够在不涉及线上支付的情况下安全快捷地出售与购买二手物品。 https://developers.weixin.qq.com/community/develop/article/doc/000a04e79a4778f919fa2674e56413 5. 校拍我们的校拍小程序和同名公众号主要是为了展示用户的学校的校园美景以及分享各种摄影入门小技巧,吸引更多的摄影爱好者来使用我们的小程序,通过发布自己拍的美景以及视频来更好的宣传用户的学校。 https://developers.weixin.qq.com/community/develop/article/doc/0008ce45f6c5d010c0faaab4c5b013 [图片] 职业赛道1. 情侣券恋爱必备 https://developers.weixin.qq.com/community/develop/article/doc/000002afb882701ca0fa756585b813 [图片] 2. 宝宝约玩 实现线下绘本馆线上线下一体化,将常规绘本馆的会员体系、仓储管理线上化,同时增加线上借阅服务及社区能力,实现绘本馆的线上线下一体化服务,为绘本馆打造借阅一站式服务解决方案。 https://developers.weixin.qq.com/community/develop/article/doc/000c6a6abb4cd81ab1faad17c5d013 [图片] 3.我存我存小程序,留住世间一切美好.实现线下绘本馆线上线下一体化,将常规绘本馆的会员体系、仓储管理线上化,同时增加线上借阅服务及社区能力,实现绘本馆的线上线下一体化服务,为绘本馆打造借阅一站式服务解决方案。 https://developers.weixin.qq.com/community/develop/article/doc/000228e8948d68897bfa0d2f75b413 [图片] 4. 魅力单词魅力单词是一款面向学生的背单词小程序,提供了丰富的单词词库以及好玩的背单词模式。 https://developers.weixin.qq.com/community/develop/article/doc/0000ca7a0806d0e584fa11f055f413 [图片] 5. 自助心理成长利用自己的力量,觉察自己的内心,应用心理学知识,使我们此刻更心安,未来更幸福 https://developers.weixin.qq.com/community/develop/article/doc/0000ceafce43c010c7fa7cf4d58c13 [图片] 感谢您的阅读~
2020-11-06 - 干货满满:个人小程序广告日入1k实战操作
众所周知,个人小程序受限于微信的政策,很多内容或者类目无法去做,对于变现,大家的目光基本也就聚焦在广告变现这一条路上。 随着小程序各种广告的开放,个人小程序的变现之路似乎也越来越明朗。然而,现实的情况却是,月底结算的时候,流量主们看着结算单上的两位数,欲哭无泪。 那么,对于个人小程序,广告变现的路子该如何去走? 如何才能实现收益最大化? 如何才能广告月入30k,当上CEO,赢取白富美,走上人生巅峰? **下面是满满的干货,一定要看到最后一行!** 1. 做什么方向?做什么内容?起什么名字? 以变现为目的的小程序,名字很重要,一定要和其他热门小程序相似、相近、相仿! 内容? 内容从来不是重点,只需要记住一点, 什么火做什么,什么话题热做什么! 以这个小程序为例: [图片] 看明白了吧,短视频现在多火啊,做他准没错! 说到这里,有人发现问题了,视频类目明明是个人不允许做的啊,这怎么办? 这个问题待会说。 2. 善用广告 目前可用的广告基本有banner,开屏,激励视频,贴片,这几种广告形式一定要在小程序中运用的淋漓尽致! 仍以此小程序为例: [图片] [图片] [图片] a. 打开小程序就开屏广告,切换tab就开屏广告,没事滚滚屏也给你来一个,不怕你不点,反正不小心也会点到 b. 视频流里面能放多少个就放多少个,万一不小心点到了呢? c. 视频的贴片广告一定要加的,不然就浪费了 d. 视频下方的相关视频一定要做,想看可以,先看一下激励视频再说。反正客户群体主要是大爷大妈,有的是时间,不在乎这15秒到30秒。 3. 善用分享 凡是目所能及的地方,一定要加上分享,分享朋友圈这种不仅要加还要做动态效,让用户记得去点。 [图片] 4. 善于召回用户 一定要用好小程序的下发通知功能,收集formid,没事就给用户发个通知什么的,不愁他不回头(至于formid怎么收集,不在本教程讨论范畴之内,自行搜索)。 [图片] 6. 绕审(敲黑板!划重点!) 说了这么多,一定有人要说了,你这小程序类目不对,广告乱搞,怎么可能过审啊? 重点来了,绕审,这个一定要做。 众所周知,微信的审核是 有人工环节的,只需要在提审期间,让审核人员看到你想让他看到的,不就行了么? 这个也很简单,审核期间,接口返回的数据做点手脚,页面上加个判断不就是了。 [图片] 看到了吧,审核期间所有视频都变成了图片,点击查看大图而已。 完美解决!! 好了说了这么多,对于月入30K是不是有点心得了???如果有兴趣,继续往下看 -------------------- -------------------- -------------------- 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!!
2019-09-10 - 小程序将小程序码与图片结合生成海报分享朋友圈
样例参考(瑞幸咖啡小程序) [图片][图片][图片] 需求分析 服务器端会返回不确认的图片资源到前端 前端将返回的每张图片都要贴上小程序码 将贴上小程序码的图片使用 swiper 组件轮播 用户点击保存时,将图片保存至相册。 至于点击保存如何保存至相册(wx.saveImageToPhotosAlbuml 了解一下,注意一下授权问题即可) 碰到的问题 canvas为原生组件, 而原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。 [代码]采用方法: 通过定位,将其移除到不在可视范围,如iphone6 .canvas { position: relative; left: -375px; } [代码] 绘制多张图片时, 一个canvas 标签只能对应画一张图片 (目前我测试的是这样,如有其它方法,欢迎评论交流) [代码]采用方法: 1. html端循环要生成的图片张数,对应循环出多个 canvas 组件,分别设置不同的 canvasId 区分 2. 封装一个绘制海报的函数,返回一个Promise对象,用于后面绘制完所有的图片,统一赋值渲染,避免多次触发数据更新 3. js端 循环执行一次 绘制海豹函数,存于一个数组列表 4. 使用 Promise.all 函数,统一绘制完毕将生产图片路径赋值 html: <canvas v-for="u in m.urlList" :key="u" :canvas-id="'poster'+index" class="canvas" style="width:100%; height:100%;"> </canvas> <swiper :indicator-dots="true" :circular="true" :autoplay="true" indicator-color="rgba(255,255,255,.2)" indicator-active-color="#fff" class="swiper"> <swiper-item v-for="f in m.filePaths" :key="index"> <image :src="f"> </swiper-item> </swiper> js: // 数据 (mpvue 开发) function data(){ return { m : { urlLIst: [ // 图片资源 '/static/poster0.jpg', '/static/poster1.jpg' ], filePaths: [], // 生成图片(贴上小程序码) } } } try { let res = wx.getSystemInfoSync(); // 同步获取系统信息 let w = res.windowWidth; // 手机可用区域宽度 let h = res.windowHeight; // 手机可用区域高度 let codeUrl = '/static/code.jpg'; // 小程序码 let drawList = []; // 用于保存绘制海报图Promise对象 m.urlList.forEach( (u, i)=> { // 传入canvas组件ID,图片路径(测试使用的是本地路径) drawList.push(drawPoster('poster'+i, u, codeUrl)); }) // 统一更新数据 Promise.all(drawList).then((valuse)=>{ m.filePaths = valuse; }); // 封装绘制图片函数 function drawPoster(canvasId, bgUrl, codeUrl){ return new Promise( (resolve, reject) => { // 创建画布实例 let ctx = wx.createCanvasContext(canvasId); // 绘制背景图: 图片路径,x坐标,y坐标,宽,高 ctx.drawImage(bgUrl, 0, 0, w, h); // 绘制小程序码 ctx.drawImage(codeUrl, w-120, h-120, 100, 100); // 绘制 ctx.draw(false, ()=>{ // 该通过函数将canvas绘制导出为图片 wx.canvasToTempFilePath({ x: 0, y: 0, width: w, height: h, canvasId: canvasId, success(res){ resolve(res.tempFilePath); } }); }); } }catch(e){ // 自己封装了一成 wx.$toast(e); } [代码] 最终demo效果图 [图片][图片] 在社区中暂未看到多张海报实现的方案,如果有更好的实现方案,欢迎交流
2019-07-30 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 【周刊-1】三年大厂面试官-面试题精选及答案
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把期间我和我的同事常问的面试题和答案汇总在我 Github 的 Weekly-FE-Interview 中。希望对大家有所帮助。 如果你在bat面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把题目汇总并将面试要点和答案写好放在周刊里,大家一起共同进步和成长,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 常见面试题精选 以下是十道大厂一面的时候常见的面试题,如果全部理解并且弄透,在一面或者电话面的时候基本上能中1~2题。小伙伴可以先不急着看答案,先自己尝试着思考一下和自己实现一下,然后再看答案。 第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 解析:第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 第 2 题:讲解一下HTTPS的工作原理 解析:第 2 题:讲解一下HTTPS的工作原理 第 3 题:讲解一下https对称加密和非对称加密。 解析:第 3 题:讲解一下https对称加密和非对称加密 第 4 题:如何遍历一个dom树 解析:第 4 题:如何遍历一个dom树 第 5 题:new操作符都做了什么 解析:第 5 题:new操作符都做了什么 第 6 题:手写代码,简单实现call 解析:第 6 题:手写代码,简单实现call 第 7 题:手写代码,简单实现apply 解析:第 7 题:手写代码,简单实现apply 第 8 题:手写代码,简单实现bind 解析:第 8 题:手写代码,简单实现bind 第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 解析:第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 第 10 题:简单手写实现promise 解析:第 10 题:简单手写实现promise 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。
2019-04-08 - CSS 火焰?不在话下
正文从下面开始。 今天的小技巧是使用纯 CSS 生成火焰,逼真一点的火焰。 嗯,长什么样子?在 CodePen 上输入关键字 [代码]CSS Fire[代码],能找到这样的: [图片] 或者这样的: [图片] 我们希望,仅仅使用 CSS ,效果能再更进一步吗?能不能是这样子: [图片] 如何实现 嗯,我们需要使用 [代码]filter[代码] + [代码]mix-blend-mode[代码] 的组合来完成。 很多 CSS 华而不实的效果都是 [代码]filter[代码] + [代码]mix-blend-mode[代码],很有意思,但是业务中根本用不上,当然多了解了解总没坏处。 如上图,整个蜡烛的骨架, 除去火焰的部分很简单,掠过不讲。主要来看看火焰这一块如何生成,并且如何赋予动画效果。 Step 1: filter blur && filter contrast 模糊滤镜叠加对比度滤镜产生的融合效果。 单独将两个滤镜拿出来,它们的作用分别是: [代码]filter: blur()[代码]: 给图像设置高斯模糊效果。 [代码]filter: contrast()[代码]: 调整图像的对比度。 但是,当他们“合体”的时候,产生了奇妙的融合现象。 先来看一个简单的例子: [图片] 仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。 利用上述 [代码]filter blur & filter contrast[代码],我们要先生成一个类似火焰形状的三角形。(略去过程) 这里类似火焰形状的三角形的具体实现过程,在这篇文章有详细的讲解:你所不知道的 CSS 滤镜技巧与细节 [图片] 父元素添加 [代码]filter: blur(5px) contrast(20)[代码],会变成这样: [图片] Step 2: 火焰粒子动画 看着已经有点样子了,接下来是火焰动画,我们先去掉父元素的 [代码]filter: blur(5px) contrast(20)[代码] ,然后继续 。 这里也是利用了 [代码]filter[代码] 的融合效果,我们在上述火焰中,利用 SASS 随机均匀分布大量大小不一的圆形棕色 div ,隐匿在火焰三角内部,大概是这样: [图片] 接下来,我们再利用 SASS,给中间每个小圆赋予一个从下往上逐渐消失的动画,并且均匀赋予不同的 [代码]animation-delay[代码],看起来会是这样: [图片] OK,最重要的一步,我们再把父元素的 [代码]filter: blur(5px) contrast(20)[代码] 打开,神奇的火焰效果就出来了: [图片] Step 3: mix-blend-mode 润色 当然,上述效果已经很不错了。经过各种尝试,调整参数,最后我发现加上 [代码]mix-blend-mode: screen[代码] 混合模式,效果更好,得到头图上面的最终效果如下: [图片] 完整源码在我的 CodePen 上:CodePen Demo – CSS Fire 另外一些效果 当然,掌握了这种方法后,这种生成火焰的技巧也可以迁移到其他效果去。下图是我鼓捣到另外一个小 Demo,当 hover 到元素的时候,产生火焰效果: [图片] CodePen Demo – Hover Fire 嗯,这些其实都是对滤镜及混合模式的一些搭配运用。按照惯例,肯定有人会留言喷了,整这些花里胡哨的有什么用,性能又不好,业务中敢上不把你的腿给打骨折。 [图片] 于我而言,虚心接受各种批评质疑及各种不同的观点,当然我是觉得搞技术一方面是实用,另一方面是兴趣使然,自娱自乐。希望喷子绕道~ 回到正题,了解了这种黏糊糊湿答答的技巧后,还可以折腾出其他很多有意思的效果,当然可能需要更多的去尝试,如下面使用一个标签实现的滴水效果: [图片] CodePen Demo – 单标签实现滴水效果 值得注意的细节点 动画虽然美好,但是具体使用的过程中,仍然有一些需要注意的地方: CSS 滤镜可以给同个元素同时定义多个,例如 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] ,但是滤镜的先后顺序不同产生的效果也是不一样的; 也就是说,使用 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] 和 [代码]filter: brightness(1.5) contrast(150%) blur(5px)[代码] 处理同一张图片,得到的效果是不一样的,原因在于滤镜的色值处理算法对图片处理的先后顺序。 滤镜动画需要大量的计算,不断的重绘页面,属于非常消耗性能的动画,使用时要注意使用场景。记得开启硬件加速及合理使用分层技术; [代码]blur()[代码] 混合 [代码]contrast()[代码] 滤镜效果,设置不同的颜色会产生不同的效果,这个颜色叠加的具体算法暂时没有找到很具体的规则细则,使用时比较好的方法是多尝试不同颜色,观察取最好的效果; 细心的读者会发现上述效果都是基于黑色底色进行的,动手尝试将底色改为白色,效果会大打折扣。 最后 本文只是简单的介绍了整个思路过程,许多 CSS 代码细节,调试过程没有展现出来。主要几个 CSS 属性默认大家已经掌握了大概,阅读后可以自行去了解补充更多细节: [代码]filter[代码] [代码]mix-blend-mode[代码] 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。 最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS: [图片]
2019-04-26 - 微信小程序 封装HTTP请求系列
1.util内封装的函数 function format(id, data) { var key = “XXXXXX”//这部分是xxtea加密的时候所需要的key data = JSON.stringify(data);//这部分是加密 data = base64_encode(data);//这部分是加密 data = xxtea_encrypt(data, key);//这部分是加密 return new Promise((resolve, reject) => {//这里是请求函数的封装为了做到没有一些乱七八糟的报错 这里使用了Promise wx.request({//这里是请求话不多说 url: url, data: data, header: { ‘Content-Type’: ‘application/json;charset=utf-8’ }, method: “POST”, success: function(res) {//正确的时候 res = xxtea_decrypt(res.data, key);//这里是解密 res = base64_decode(res);//这里是解密 res = JSON.parse(res);//这里是解密 resolve(res)//Promise返回成功 }, fail: function(res) {//错误的时候 reject(res);//Promise返回失败 } }) }) } 以上就是在util中封装好的请求函数 不要在意名字 提醒:莫忘记在module中声明该函数 不会声明 说明你不配用 下面是如何调用该函数并针对返回结果进行处理 util.format(url, data) .then(res => { console.log(“请求成功时调用该函数”) console.log(res); }) .catch(res => { console.log(“请求失败时调用该函数”) }) }, 以上就是函数的调用 记得声明 util 使用该函数的目的就是 ↓↓↓ 懒 减少调接口 加密解密以及一些乱七八糟的操作的时候的简化 emmmmmmmmm 就这样 诸位江湖再见 告辞
2019-04-25 - 「插屏广告组件」向非游戏类小程序流量主开放
小程序插屏广告指小程序在特定场景切换时以卡片方式弹出的广告形式。当用户触发流量主指定场景时,插屏广告就会自动向用户展现,目前向非游戏类目全量流量主开放,小游戏类目即将启动内测。 此外激励式视频广告也向非游戏类小程序流量主开放,详见《激励式视频广告开通指引》。 [图片] [图片][图片] [图片][图片][图片][图片][图片] [图片] [图片] [图片]
2019-04-25