- (14)腾讯视频插件
想要在自己的电商小程序中增加商品介绍视频,但自己搭建视频服务开发成本太大? 想要在餐饮服务中添加菜品视频,但苦于带宽成本过高? 想要在门票预订服务中用视频打动用户,但视频资质申请流程太长? 腾讯视频插件来帮你~ 今天,我们跟大家分享腾讯视频插件的故事。 腾讯视频插件提供了完整视频播放能力,方便开发者可以给用户提供更好的视频体验。 插件基于腾讯视频的CDN节点和成熟视频方案,可以做到为不同地点的用户提供更流畅的播放服务,更清晰的视频,更稳定的播放质量。只需要视频的vid,开发者即可直接在小程序内播放腾讯视频上的内容! 腾讯视频插件使用场景 场景一:电商类小程序 除了对商品的语言描述,实惠的价格,精美的图片,也需要生动直观、360度动态介绍商品,比单一的图片更能引起消费者的好感。 现在,在商品介绍里直接链入相关商品视频。比如,买衣服的电商小程序,可以把电视或电影中明星穿过的同款视频链入,在满足用户感官享受的同时,提升转化率。 场景二:文娱推荐类小程序 不局限于苍白的文字描述,推荐类小程序也可在小程序中添加电影视频、预告片,为用户带来完美的购票、观影体验。开发者无需独立开发视频功能,直接使用“腾讯视频”插件,即可实现视频播放功能。 [图片] 场景三:资讯类小程序 “腾讯视频”插件还可以是内容创作者的一大利器,以游戏攻略类小程序为例。小程序就可以直接用视频的形式展现,一方面清晰明了地展示攻略流程,同时也增加了用户在小程序内的停留时长,进一步引导用户做更多动作。 或者生活居家小技巧类的小程序,比如想教大家一种打结的方式,纯文字的形式即很难说清楚,又浪费内容运营者的时间,这时候直接使用插件链入视频,就能很快地教会大家,给予用户更好的内容体验。 轻松接入腾讯视频插件 “腾讯视频”提供了这么多的功能,使用起来也同样很方便: 1 在“小程序管理后台→设置→第三方服务→插件管理”中查找插件名称“腾讯视频”,并申请使用。 [图片] 2 参考详情页中的 [开发文档],在小程序代码中使用插件。 了解完“腾讯视频”插件后,还想看看其他小程序是如何实际运用的?一起来体验一下 腾讯+ ! [图片] 页面视频服务是由腾讯视频插件提供
2018-08-17 - 使用Puppeteer搭建统一海报渲染服务
背景介绍 有赞微商城包括了 PC 端、H5 端和小程序端,每个端都有绘制分享海报的需求。最早的时候我们是在每个端通过canvas API来绘制的,通过canvas绘制有很多痛点,与本文要讲的海报渲染服务做了一个对比: [图片] 正是因为这些痛点问题,有同事就提出基于Puppeteer实现一个公共的海报渲染服务,使用方只需传入海报图片的html,海报渲染服务绘制一张对应的图片作为返回结果,解决了canvas绘制的各种痛点问题。 一、Puppeteer 是什么 Puppeteer是谷歌官方团队开发的一个 Node 库,它提供了一些高级 API 来通过DevTools协议控制Headless Chrome或Chromium。通俗的说就是提供了一些 API 用来控制浏览器的行为,比如打开网页、模拟输入、点击按钮、屏幕截图等操作,通过这些 API 可以完成很多有趣的事情,比如本文要讲的海报渲染服务,它用到的就是屏幕截图的功能。 二、Puppeteer 能做什么 Puppeteer几乎能实现你能在浏览器上做的任何事情,比如: 生成页面的屏幕截图或pdf 自动化提交表单、模拟键盘输入、自动化单元测试等 网站性能分析:可以抓取并跟踪网站的执行时间轴,帮助分析效率问题 抓取网页内容,也就是我们常说的爬虫 三、海报渲染服务 3.1方案设计 首先我们来看一下海报渲染服务的流程图: [图片] 其实整个流程还是比较简单的,当有一个绘制请求时,首先看之前是否已经绘制过相同的海报了,如果绘制过,就直接从Redis里取出海报图片的 CDN 地址。如果海报未曾绘制过,则先调用Headless Chrome来绘制海报,绘制完后上传到 CDN,最后 CDN 上传完后返回 CDN 地址。整个流程的大致代码实现如下: [代码]const crypto = require('crypto'); const PuppeteerProvider = require('../../lib/PuppeteerProvider'); const oneDay = 24 * 60 * 60; class SnapshotController { /** - 截图接口 * - @param {Object} ctx 上下文 */ async postSnapshotJson(ctx) { const result = await this.handleSnapshot(); ctx.json(0, 'ok', result); } async handleSnapshot() { const { ctx } = this; const { html } = ctx.request.body; // 根据 html 做 sha256 的哈希作为 Redis Key const htmlRedisKey = crypto.createHash('sha256').update(html).digest('hex'); try { // 首先看海报是否有绘制过的 let result = await this.findImageFromCache(htmlRedisKey); // 命中缓存失败 if (!result) { result = await this.generateSnapshot(htmlRedisKey); } return result; } catch (error) { ctx.status = 500; return ctx.throw(500, error.message); } } /** - 判断kv中是否有缓存 * - @param {String} htmlRedisKey kv存储的key */ async findImageFromCache(htmlRedisKey) { } /** - 生成截图 * - @param {String} htmlRedisKey kv存储的key */ async generateSnapshot(htmlRedisKey) { const { ctx } = this; const { html, width = 375, height = 667, quality = 80, ratio = 2, type: imageType = 'jpeg', } = ctx.request.body; this.validator .required(html, '缺少必要参数 html') .required(operatorId, '缺少必要参数 operatorId'); let imgBuffer; try { imgBuffer = await PuppeteerProvider.snapshot({ html, width, height, quality, ratio, imageType }); } catch (err) { // logger } let imgUrl; try { imgUrl = await this.uploadImage(imgBuffer, operatorId); // 将海报图片存在 Redis 里 await ctx.kvdsClient.setex(htmlRedisKey, oneDay, imgUrl); } catch (err) { } return { img: imgUrl || '', type: IMAGE_TYPE_MAP.CDN, }; } /** - 上传图片到七牛 * - @param {Buffer} imgBuffer 图片buffer */ async uploadImage(imgBuffer) { // upload image to cdn and return cdn url } } module.exports = SnapshotController; [代码] 3.2遇到的问题 2.3.1 Chromium启动和执行流程 最开始一个版本我们是直接Puppeteer.launch()返回一个浏览器实例,每次绘制会用单独的一个浏览器实例,这个在使用过程中发现绘制海报会很慢,后面优化时找到了这篇文章:Puppeteer性能优化与执行速度提升,这篇文章提到了两个优化点:1. 优化Chromium启动项;2. 优化Chromium执行流程。 先说优化Chromium启动项,这个就是为了我们启动一个最小化可用的浏览器实例,其他不需要的功能都禁用掉,这样会大大提升启动速度。 [代码]const browser = await puppeteer.launch({ args: [ '–disable-gpu', '–disable-dev-shm-usage', '–disable-setuid-sandbox', '–no-first-run', '–no-sandbox', '–no-zygote', '–single-process' ] }); [代码] 再来说说浏览器的执行流程,最开始我们是每次绘制都会用单独一个浏览器,也就是一对一,这个在压测的时候发现CPU和内存飙升,最后我们改用了复用浏览器标签的方式,每次绘制新建一个标签来绘制。 [代码]const page = await browser.newPage(); page.setContent(html, { waitUntil: 'networkidle0' }); const imageBuffer = await page.screeshot(options); [代码] 3.3.2 networkidle0 最开始我们的海报服务绘制海报时有时候会偶尔出现图片展示不出来的情况,我们排查后发现是因为我们setContent时,使用的是默认的load事件来判断设置内容成功,而我们期望的是所有网络请求成功后才算设置内容成功。 [代码]page.setContent(html); [代码] Puppeteer在setContent和goto等方法里提供了一个waitUntil的参数,它就是用来配置这个判断成功的标准,它提供了四个可选值: load:默认值,load事件触发就算成功 domcontentloaded:domcontentloaded事件触发就算成功 networkidle0:在500ms内没有网络连接时就算成功 networkidle2:在500ms内有不超过2个网络连接时就算成功 我们这里需要用到的就是networkidle0: [代码]page.setContent(html, { waitUntil: 'networkidle0' }); [代码] 当改成networkidle0后,使用方给我们反馈说整个绘制服务变慢了很多,随随便便都2s以上。变慢主要是因为加上networkidle0后,至少需要等待500ms以上,加上绘制的一些其他开销,基本上就需要2s了。所以我们期望这个500ms是可配置的,因为500ms实在太长了,我们的分享海报一般只有几张图片,不需要这么久。但是Puppeteer没有提供相关的参数,还好在issue中早已经有人提出了这个问题:Control networkidle wait time [代码]function waitForNetworkIdle(page, timeout, maxInflightRequests = 0) { page.on('request', onRequestStarted); page.on('requestfinished', onRequestFinished); page.on('requestfailed', onRequestFinished); let inflight = 0; let fulfill; let promise = new Promise(x => fulfill = x); let timeoutId = setTimeout(onTimeoutDone, timeout); return promise; function onTimeoutDone() { page.removeListener('request', onRequestStarted); page.removeListener('requestfinished', onRequestFinished); page.removeListener('requestfailed', onRequestFinished); fulfill(); } function onRequestStarted() { ++inflight; if (inflight > maxInflightRequests) clearTimeout(timeoutId); } function onRequestFinished() { if (inflight === 0) return; --inflight; if (inflight === maxInflightRequests) timeoutId = setTimeout(onTimeoutDone, timeout); } } // Example await Promise.all([ page.goto('https://google.com'), waitForNetworkIdle(page, 500, 0), // equivalent to 'networkidle0' ]); [代码] 利用这个函数我们就可以传入自己想要的超时时间了。 3.2.3 Chromium定时刷新机制 为什么需要定时刷新Chromium呢?总不可能一直用同一个Chromium实例吧,万一变卡或者crash了,就会影响海报的绘制。所以我们需要定时的去刷新当前的浏览器实例。 [代码]class PuppeteerProvider { constructor() { this.browserList = []; } /** - 初始化`puppeteer`实例 */ initBrowserInstance() { Array.from({ length: browserConcurrency }, () => { this.checkBrowserInstance(); }); // 每隔30分钟刷新一下浏览器 this.refreshTimer = setTimeout(() => this.refreshOneBrowser(), thrityMinutes); } /** - 检查是否还需要浏览器实例 */ async checkBrowserInstance() { if (this.needBrowserInstance) { this.browserList.push(this.launchBrowser()); } } /** - 定时刷新浏览器 */ refreshOneBrowser() { clearTimeout(this.refreshTimer); const browserInstance = this.browserList.shift(); this.replaceBrowserInstance(browserInstance); this.checkBrowserInstance(); // 每隔30分钟刷新一下浏览器 this.refreshTimer = setTimeout(() => this.refreshOneBrowser(), thrityMinutes); } /** - 替换单个浏览器实例 * - @param {String} browserInstance 浏览器promise - @param {String} retries 重试次数,超过这个次数直接关闭浏览器 */ async replaceBrowserInstance(browserInstance, retries = 2) { const browser = await browserInstance; const openPages = await browser.pages(); // 因为浏览器会打开一个空白页,如果当前浏览器还有任务在执行,一分钟后再关闭 if (openPages && openPages.length > 1 && retries > 0) { const nextRetries = retries - 1; setTimeout(() => this.replaceBrowserInstance(browserInstance, nextRetries), oneMinute); return; } browser.close(); } launchBrowser(opts = {}, retries = 1) { return PuppeteerHelper.launchBrowser(opts).then(chrome => { return chrome; }).catch(error => { if (retries > 0) { const nextRetries = retries - 1; return this.launchBrowser(opts, nextRetries); } throw error; }); } } [代码] 这里还有一个点,我们给replaceBrowserInstance这个方法加了个重试次数的限制,当超出这个限制后不管有没有任务在进行都会关闭浏览器。这个是防止在某些特殊情况不能关闭掉浏览器,导致内存无法释放的情况。 四、展望 目前海报渲染服务的问题就是qps比较低,因为Chromium消耗最多的资源是CPU,当并发数变高时,CPU也随之变高,就会导致后面的绘制变慢。在4核8G的情况,大概是20qps左右。后面的主要精力就是如何去提升单机的qps,应该还有比较大的空间。还有就是看看能不能增加定时任务,在凌晨机器比较闲的时候提前绘制好一些常用的海报,这样当需要海报时就是直接从redis里取出来了,充分利用了机器的性能,也可以减少海报服务白天的压力。 五、相关链接: Puppeteer 性能优化与执行速度提升:https://blog.it2048.cn/article-puppeteer-speed-up/ Control networkidle wait time:https://github.com/GoogleChrome/puppeteer/issues/1353
2019-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 - 2019-06-28
- 微信小程序分页加载数据~上拉加载更多~小程序云数据库的分页加载
我们在开发小程序时,一个列表里难免会有很多条数据,比如我们一个列表有1000条数据,我们一下加载出来,而不做分页,将会严重影响性能。所以这一节,我们来讲讲小程序分页加载数据的实现。 老规矩,先看效果图 [图片] 可以看到我们每页显示10条数据,当滑动到底部时,会加载第二页的数据,再往下滑动,就加载第三页的数据。由于我们一共21条数据,所以第三页加载完以后,会有一个“已加载全部数据”的提示。 本节知识点 1,小程序分页加载 2,小程序列表显示 3,云数据库的使用 4,云数据库分页请求数据的实现 一,先定义数据 我们做分页数据加载,肯定要先准备好数据,数据已经给大家准备好,如下图,文章末尾会贴出数据源和本节课源码的下载地址。 [图片] 然后把数据导入到我们的云开发的数据库里,关于数据如何导入,这里不再讲解,不知道的同学,请看下面这篇文章。或者去老师历史文章里找一下。 《小程序云开发入门—云数据库数据源的导入与导出》 下面给大家看下我们的数据源,长什么样。其实很简单,就是简单的定义21条数据。 [图片] 然后在看导入到数据库的样子。 [图片] 二,分页请求数据 我们第一步准备好了数据以后,接下来就来讲讲如何在js里做分页加载数据。 首先我们这里用到了小程序云开发数据库的知识点 1,get方法:获取云数据库数据 2,skip方法:跳过前面几条数据,请求后面的数据 3,limit方法:请求多少条数据。 比如下面这段代码,就是跳过前5条,请求从第6条开始往后的10条数据,就是请求6~15的数据,我们做分页加载也就是基于这个原理。 [代码] wx.cloud.database().collection("list") .skip(5) //从第几个数据开始 .limit(10) [代码] 下面把我们index.js的完整代码贴给大家。 [代码]//老师微信:2501902696 let currentPage = 0 // 当前第几页,0代表第一页 let pageSize = 10 //每页显示多少数据 Page({ data: { dataList: [], //放置返回数据的数组 loadMore: false, //"上拉加载"的变量,默认false,隐藏 loadAll: false //“没有数据”的变量,默认false,隐藏 }, //页面显示的事件 onShow() { this.getData() }, //页面上拉触底事件的处理函数 onReachBottom: function() { console.log("上拉触底事件") let that = this if (!that.data.loadMore) { that.setData({ loadMore: true, //加载中 loadAll: false //是否加载完所有数据 }); //加载更多,这里做下延时加载 setTimeout(function() { that.getData() }, 2000) } }, //访问网络,请求数据 getData() { let that = this; //第一次加载数据 if (currentPage == 1) { this.setData({ loadMore: true, //把"上拉加载"的变量设为true,显示 loadAll: false //把“没有数据”设为false,隐藏 }) } //云数据的请求 wx.cloud.database().collection("list") .skip(currentPage * pageSize) //从第几个数据开始 .limit(pageSize) .get({ success(res) { if (res.data && res.data.length > 0) { console.log("请求成功", res.data) currentPage++ //把新请求到的数据添加到dataList里 let list = that.data.dataList.concat(res.data) that.setData({ dataList: list, //获取数据数组 loadMore: false //把"上拉加载"的变量设为false,显示 }); if (res.data.length < pageSize) { that.setData({ loadMore: false, //隐藏加载中。。 loadAll: true //所有数据都加载完了 }); } } else { that.setData({ loadAll: true, //把“没有数据”设为true,显示 loadMore: false //把"上拉加载"的变量设为false,隐藏 }); } }, fail(res) { console.log("请求失败", res) that.setData({ loadAll: false, loadMore: false }); } }) }, }) [代码] 上面的代码就是我们实现分页加载的所有逻辑代码。简单说下代码 1,我们首先进页面时会请求前10条内容 2,10条内容请求成功以后,我们会把请求到的内容加入dataList数组,然后把dataList里的数据显示到页面上。并将currentPage加一,用于请求第二页数据。 3,当用户滑动到底部时,会触发onReachBottom事件,在这个事件里做第二页到请求。然后第二页数据请求成功以后。继续将currentPage加1,这里要记住一定,一定要请求成功以后才将currentPage +1。 三,列表布局和样式 其实index.wxml和index.wxss的代码很简单,给大家把代码贴出来。 1,index.wxml [代码]<scroll-view scroll-y="true" bindscrolltolower="searchScrollLower"> <view class="result-item" wx:for="{{dataList}}" wx:key="item"> <text class="title">{{item.content}}</text> </view> <view class="loading" hidden="{{!loadMore}}">正在载入更多...</view> <view class="loading" hidden="{{!loadAll}}">已加载全部</view> </scroll-view> [代码] 2,index.wxss [代码]page { display: flex; flex-direction: column; height: 100%; } .result-item { display: flex; flex-direction: column; padding: 20rpx 0 20rpx 110rpx; overflow: hidden; border-bottom: 2rpx solid #e5e5e5; } .title { height: 110rpx; } .loading { position: relative; bottom: 5rpx; padding: 10rpx; text-align: center; } [代码] 到这里我们就完整的实现里分页加载功能了。 [图片] 源码和数据源,已经给大家放到网盘里了,有需要的同学请在文章底部留言,或者私信老师。 视频讲解:https://edu.csdn.net/course/detail/9604
2019-11-07 - 小程序自定义单页面、全局导航栏
摘要: 小程序开发技巧。 作者:小白 原文:小程序自定义单页面、全局导航栏 Fundebug经授权转载,版权归原作者所有。 需求 产品说小程序返回到首页不太方便,想添加返回首页按钮,UI说导航栏能不能设置背景图片,因为那样设计挺好看的。 [图片] 需求分析并制定方案 这产品和UI都提需求了,咱也不能反驳哈,所以开始调研,分析可行性方案;1、可以添加悬浮按钮。2、自定义导航栏。 添加悬浮按钮,是看起来是比较简单哈,但是感觉不太优雅,会占据页面的空间,体验也不太好。所以想了下第二种方案,自定义导航栏既可以实现产品的需求还可以满足UI的设计美感,在顶部空白处加上返回首页的按钮,这样和返回按钮还对称(最终如图所示,顶部导航栏是个背景图片,分两块组合起来)。 实现方案 一、实现的前提 1、首先查看文档,看文档里关于自定义导航栏是怎么规定的,有哪些限制;还有小程序自定义导航栏全局配置和单页面配置的微信版本和调试库的最低支持版本。 2、在app.json window 增加 navigationStyle:custom ,顶部导航栏就会消失,只保留右上角胶囊状的按钮,如何修改胶囊的颜色呢;胶囊体目前只支持黑色和白色两种颜色 在app.josn window 加上 “navigationBarTextStyle”:“white/black” 3、还要考虑加返回按钮和返回首页的按钮,适配不同的机型 先说下两种配置方法: ①全局配置navigationStyle: 调试基础库>=1.9.0 微信客户端>=6.6.0 app.json [代码]{ "usingComponents": { "navigationBar": "/components/navigationBar/navigationBar" }, "window": { "navigationStyle": "custom" } } [代码] ②单页面配置navigationStyle 调试基础库>=2.4.3 微信客户端版本>=7.0.0 自定义的页面.json [代码]{ "window": { "navigationStyle": "default" } } { "navigationStyle": "custom", "usingComponents": { "navigationBar": "/components/navigationBar/navigationBar" } } [代码] 两者的区别就是,全局配置放在app.json文件里,单页面配置放在自定义页面配置文件里。 二、实现的步骤 以下说下几个要点: 1、自定义导航栏文本,是否显示返回,是否显示返回首页,导航栏高度 2、statusBarHeight,用来获取手机状态栏的高度,这个需要在全局app.js中的onLaunch,调用wx.getSystemInfo获取,navigationBarHeight+默认的高度,这个是设定整个导航栏的高度, 3、还有注意的,在写样式距离和大小时建议都用px,因小程序右边的胶囊也是用的px,不是rpx。 4、因为自定义导航栏每个页面都要写,所以把导航栏封装了公共组件,这样只需要在每个页面引入即可。 如下是封装的导航栏组件: wxml [代码]<view class="navbar" style="{{'height: ' + navigationBarHeight}}"> <view style="{{'height: ' + statusBarHeight}}"></view> <view class='title-container'> <view class='capsule' wx:if="{{ back || home }}"> <view bindtap='back' wx:if="{{back}}"> <image src='/images/back.png'></image> </view> <view bindtap='backHome' wx:if="{{home}}"> <image src='/images/home.png'></image> </view> </view> <view class='title'>{{text}}</view> </view> </view> <view style="{{'height: ' + navigationBarHeight}};background: white;"></view> [代码] 这里有个需注意的问题,就是一般会出现自定义导航栏,下拉页面,导航栏也随着会下拉,这种问题是因为设置fixed后页面元素整体上移了navigationBarHeight,所以在此组件里设置一个空白view元素占用最上面的navigationBarHeight这块高度 wxss [代码].navbar { width: 100%; background-color: #1797eb; position: fixed; top: 0; left: 0; z-index: 999; } .title-container { height: 40px; display: flex; align-items: center; position: relative; } .capsule { margin-left: 10px; height: 30px; background: rgba(255, 255, 255, 0.6); border: 1px solid #fff; border-radius: 16px; display: flex; align-items: center; } .capsule > view { width: 45px; height: 60%; position: relative; .capsule > view:nth-child(2) { border-left: 1px solid #fff; } .capsule image { width: 50%; height: 100%; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } .title { color: white; position: absolute; top: 6px; left: 104px; right: 104px; height: 30px; line-height: 30px; font-size: 14px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } [代码] js [代码]const app = getApp() Component({ properties: { text: { type: String, value: 'Wechat' }, back: { type: Boolean, value: false }, home: { type: Boolean, value: false } }, data: { statusBarHeight: app.globalData.statusBarHeight + 'px', navigationBarHeight: (app.globalData.statusBarHeight + 44) + 'px' }, methods: { backHome: function () { let pages = getCurrentPages() wx.navigateBack({ delta: pages.length }) }, back: function () { wx.navigateBack({ delta: 1 }) } } }) [代码] json [代码]{ "component": true, "usingComponents": {} } [代码] 最终还需要考虑下版本兼容的问题,毕竟还有一些用户,微信版本并没有更新到最新版本。 首先可以在app.js里面获取下当前用户的微信版本,做下版本比较,如果小于这个版本,设置个全局变量,也可以在组件写个方法,在不同的页面打开显示不同的顶部导航栏,或者可以控制是否显示导航栏,这里就不详细说了。 亲自试了下,在低于7.0版本的微信中,如果采用单页面自定义导航栏,会出现两个导航栏,这时候通过判断版本号不要再渲染自定义的导航栏组件了,在页面的配置文件里写上title名,还有相应的背景色,这样就会显示自带的导航栏了。 总结 小程序开发是有些坑的地方,从不支持自定义导航栏,到支持全局自定义导航栏,再到现在的支持单页面配置,可以看出在慢慢完善。还有底部tabbar,可自己选择配置的太少了,虽然也支持自定义,但是发现自定义写的底部导航组件体验并不好,每次打开页面都会重新渲染底部的按钮,如果全部写成在一个页面里的tab切换,虽然按钮每次不用重新加载了,但是业务多肯定不行,写到一个单页面里东西也太多了。 希望微信能够多添加或放开一些功能,让开发者更好的服务于产品,给用户更好的体验。
2019-06-22 - 小程序组件化开发
一、组件实现方式:template模板和component构造器 除了component,小程序中还有另一种组件化你的方式template模板 区别: 1、template主要是展示,方法则需要在调用的页面中定义。简单来说,如果只是展示,使用template就足够了 2、而component组件则有自己的业务逻辑,可以看做一个独立的page页面。如果涉及到的业务逻辑交互比较多,那就最好使用component组件了。 二、template模板 1、模板定义 建议单独创建template目录,在template目录中创建管理模板文件。 由于模板只有wxml、wxss文件,一个template的模板文件和样式文件只需要命名相同即可,方法则需要在调用的页面中定义 模板文件(wxml):用name区分多个模板 [代码]<template name="packModule"> <view class="packModule">packModule</view> </template> [代码] 模板文件(wxss):自定义模板的样式文件(略),实例中模块相关样式都统一集中在module.wxss中 2、页面引用:(如首页引用) index.wxml: [代码]<!--导入模板--> <import src="./modules/pack.wxml"/> <!--嵌入模板--> <view class="moduleWrap" wx:for="{{moduleInfoList}}" wx:for-item="moduleInfo" wx:key="index"> <!--自由容器模块 里面还有子模块--> <template is="packModule" data="{{moduleInfo}}" wx-if="{{moduleInfo.style == 5}}"></template> <view> [代码] index.wxss: [代码]@import "../../libs/templates/module.wxss"; [代码] 备注: 一个模板文件中可引用多个template,每个template均以name进行区分,页面调用的时候也是以name指向对应的template; template模板没有配置文件(.json)和业务逻辑文件(.js),所以template模板中的变量引用和业务逻辑事件都需要在引用页面的js文件中进行定义; 三、Component组件: [图片] 1. 组件创建: 新建component目录——创建子目录——新建Component(如示例组件:dialog组件) 示例dialog组件也由4个文件构成,与page文件类型相同,但是js文件和json文件与页面有所不同。 dialog.wxml: [代码]<view class="dialog" wx:if="{{ isShow }}"> <!-- 遮罩层 --> <view class="dialog_mask" catchtouchmove="_catchTouch"></view> <!-- 内容 --> <view class="container" catchtouchmove="_catchTouch"> <view class="title" wx:if="{{title}}">{{title}}</view> <view class="content" wx:if="{{content}}"> {{content}} </view> <view class="footer"> <view class="btn cancel_btn" wx:if="{{showCancelButton}}" bindtap="hide" style='background-color:{{globalColor}};'>{{cancelButton}}</view> <view class="btn comfirm_btn" bindtap="comfirm" style='background-color:{{globalColor}};'>{{confirmButton}}</view> </view> </view> </view> [代码] dialog.json: [代码]{ "component": true, "usingComponents": {} } [代码] dialog.wxss:(组件对应 wxss 文件的样式,只对组件wxml内的节点生效) [代码]/* components/dialog/dialog.wxss */ .dialog { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; position: fixed; top:0; z-index: 9999; } ... [代码] dialog.js: [代码]/* * dialog 模态对话框 * Props * 通过事件调用组件 * globalColor:全局色 * Event * show 模态对话框 @param {Object} 配置参数 * hide 模态对话框 * Example * 1、页面中引用dialog:(示例index页面) * 1-1:index.json中声明组件引用 * { * "usingComponents": { * "dialog": "../../components/dialog/dialog" * } * 1-2:index.wxml引用模板 * <dialog id='dialog' global-color="{{globalColor}}"></toast> * 1-3:index页面所引用js文件中,获取组件实例 * onReady: function () { * //获得组件 * this.dialog = this.selectComponent("#dialog"); * } * 2、根据业务条件进行调用: * 2-1、页面中调用: * this.dialog.show({ * title: "提交成功", * content: "描述信息", * cancelButton: "取 消", * showCancelButton:false,//是否显示取消按钮,可缺省,默认不显示 * confirmButton: '确 定', * callback: function () {}//确定按钮回调函数,可缺省,默认只关闭对话框 * }); * 2-1、页面组件中调用: * getCurrentPages()[getCurrentPages().length - 1].dialog.show(...);//传参同上 * 备注:不直接在组件ready中获取dialog组件,会产生多个组件实例,故直接调用页面已有组件实例方法 */ Component({ /** * 组件的属性列表 */ properties: { 'globalColor': String }, /** * 组件的初始数据 */ data: { isShow: false, title: '标题',// 弹窗标题 content: "", // 弹窗内容 cancelButton: '取 消', showCancelButton:false,//是否显示"取消"按钮 confirmButton: '确 定', callback: null //回调函数 }, /** * 组件的方法列表 */ methods: { //隐藏信息提示 hide() { this.setData({ isShow: !this.data.isShow }) }, // 阻止页面滚动 _catchTouch: function () { return; }, //展示信息提示 show(options) { this.setData({ isShow: !this.data.isShow, callback:null }); let _this = this; // 通过options参数配置 if (options) { this.setData(options); } }, // 确定回调 comfirm(){ this.hide(); this.data.callback && this.data.callback();//执行各dialog的回调逻辑 } } }) [代码] 2、页面引用: index.json:(需在json配置文件中进行配置开启使用组件) [代码]{ "usingComponents": { "dialog": "/components/dialog/dialog" } } [代码] index.wxml:(模板文件中引用) [代码]<!-- 自定义dialog组件 --> <dialog id='dialog' global-color="{{globalColor}}"></dialog> [代码] 四、注意点: 1、component组件中扩展自定义节点: 在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点。 默认情况下,一个组件的wxml中只能有一个slot。需要使用多slot时,可以在组件js中声明启用,以不同的 name 来区分。 [代码]Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 } }) [代码] 2、component组件样式注意点: 组件对应 wxss 文件的样式,只对组件wxml内的节点生效。 2-1、组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器。 2-2、组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。 2-3、子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。 2-4、继承样式,如 font 、 color ,会从组件外继承到组件内。 2-5、除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)。 [代码] #a { } /* 在组件中不能使用 */ [a] { } /* 在组件中不能使用 */ button { } /* 在组件中不能使用 */ .a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */ [代码] 五、组件间通信与事件: 1、父组件(调用页面)向子组件传值通讯: 通过properties向自定义组件传递数据 2、子组件向父组件(调用页面)传值通讯: 1、监听事件 自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件 [代码]<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 --> <component-tag-name bindmyevent="onMyEvent" name="{{name}}" /> Page({ data:{ name:"test" }, onMyEvent: function(e){ e.detail // 自定义组件触发事件时提供的detail对象 } }) [代码] 2、触发事件 自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail对象和事件选项: [代码]<!-- 在自定义组件中 --> <view>{{name}}</view> <button bindtap="onTap">点击这个按钮将触发“myevent”事件</button> Component({ properties: { name: { type: String, value: '' } }, methods: { onTap: function(){ var myEventDetail = {} // detail对象,提供给事件监听函数 var myEventOption = {} // 触发事件的选项 this.triggerEvent('myevent', myEventDetail, myEventOption) } } }) [代码] 总结 自定义组件,可以理解为一个自定义的标签,页面的一个片段,可以分为template方式和component组件方式实现;如果是简单的内容展示,逻辑单一,使用template方式即可,但如果每一个组件都有自己的业务逻辑,各自独立,建议使用component组件方式实现,灵活性更高。 参考文献 官方文档
2019-06-20 - 小程序框架对比 ( WePY / mpvue / Taro )
摘要: 对比小程序框架。 原文:小程序第三方框架对比 ( wepy / mpvue / taro ) 公众号:前端小苑 Fundebug经授权转载,版权归原作者所有。 [图片] 众所周知如今市面上端的形态多种多样,手机Web、ReactNative、微信小程序, 支付宝小程序, 快应用等,每一端都是巨大的流量入口,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。但面对目前市面上成熟的小程序第三方框架如何针对自己的需求进行选择也是一个麻烦事,本文针对当前市面上的三大转译框架进行一个综合对比,希望能对大家的技术选择有所帮助,如有哪里不妥的地方希望大家指正。 小程序开发有哪些痛点? 频繁调用 setData及 setData过程中页面跳闪 组件化支持能力太弱(几乎没有) 不能使用 less、scss 等预编译器 request 并发次数限制 为什么使用第三方框架? 只要熟悉vue或react即可快速上手,学习成本低 一套代码可在多端编译运行(微信,支付宝,h5,RN) 支付宝小程序暂不完善 组件化开发,完美解决组件隔离,组件嵌套,组件通信等问题 支持使用第三方 npm 资源 使小程序可支持 Promise,解决回调烦恼 可使用 Generator Function / Class / Async Function 等特性,提升开发效率 对小程序本身的优化,如生命周期的补充,性能的优化等等 支持样式编译器: Scss/Less,模板编译器,代码编译器:Babel/Typescript。 小程序框架对比:WePY / mpvue / Taro 在这里我通过对目前已开源的三种常用小程序框架做一个综合对比, 还有一个叫nanchi的基于react的小程序转译框架,由于没来的及研究暂不做比较。 WePY 腾讯团队开源的一款类vue语法规范的小程序框架,借鉴了Vue的语法风格和功能特性,支持了Vue的诸多特征,比如父子组件、组件之间的通信、computed计算属性、wathcer监听器、props传值、slot槽分发,Mixin混入等。WePY发布的第一个版本是2016年12月份,也就是小程序刚刚推出的时候,到目前为止,WePY已经发布了52个版本, 最新版本为1.7.2; mpvue 美团团队开源的一款使用 Vue.js 开发微信小程序的前端框架。使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小程序提供了代码复用的能力。mpvue在发布后的几天间获得2.7k的star,上升速度飞起,截至目前为止已经有13.7k的star; Taro 京东凹凸实验室开源的一款使用 React.js 开发微信小程序的前端框架。它采用与 React 一致的组件化思想,组件生命周期与 React 保持一致,同时支持使用 JSX 语法,让代码具有更丰富的表现力,使用 Taro 进行开发可以获得和 React 一致的开发体验,同时因为使用了react的原因所以除了能编译h5, 小程序外还可以编译为ReactNative; [图片] [图片] 生命周期 同为vue规范的mpvue和wepy的生命周期和各种方法不尽相同。 WePY wepy生命周期基本与原生小程序相同,再此基础上糅合了一些vue的特性; 对于WePY中的methods属性,因为与Vue中的使用习惯不一致,非常容易造成误解,这里需要特别强调一下:WePY中的methods属性只能声明页面wxml标签的bind、catch事件,不能声明自定义方法,这与Vue中的用法是不一致的。 [图片] [图片] 简单示例Taro与React生命周期完全相同 [图片] 列表渲染 在列表渲染上三者也分别有不同的应用方法 wepy当需要循环渲染WePY组件时(类似于通过wx:for循环渲染原生的wxml标签),必须使用wepy定义的辅助标签。 [图片] mpvue使用v-for与vue一致,只是需要注意一点,嵌套列表渲染,必须指定不同的索引! [图片] taro的列表循环用法基本与react相同,有一点需要注意,在 React 中,JSX 是会编译成普通的 JS 的执行,每一个 JSX 元素,其实会通过 createElement 函数创建成一个 JavaScript 对象(React Element),因此实际上你可以这样写代码 React 也是完全能渲染的: [图片] 但是 Taro 中,JSX 会编译成微信小程序模板字符串,因此你不能把 map 函数生成的模板当做一个数组来处理。当你需要这么做时,应该先处理需要循环的数组,再用处理好的数组来调用 map 函数。例如上例应该写成: [图片] 事件处理 mpvue目前全支持小程序的事件处理器,引入了 Vue.js 的虚拟 DOM ,在前文模版中绑定的事件会被挂在到 vnode 上,同时 compiler 在 wxml 上绑定了小程序的事件,并做了相应的映射,所以在真实点击的时候通过 runtime 中 handleProxy 事件类型分发到 vnode 的事件上,同 Vue 在 WEB 的机制一样,可以做到无损支持。同时还顺便支持了自定义事件和 $emit 机制。 事件映射表,左侧为 WEB 事件,右侧为 小程序 对应事件 [图片] 踩坑注意(官方文档): 列表中没有的原生事件也可以使用例如 bindregionchange 事件直接在 dom 上将bind改为@ @regionchange,同时这个事件也非常特殊,它的 event type 有 begin 和 end 两个,导致我们无法在handleProxy 中区分到底是什么事件,所以你在监听此类事件的时候同时监听事件名和事件类型既 <map @regionchange=“functionName” @end=“functionName” @begin=“functionName”> 小程序能力所致,bind 和 catch 事件同时绑定时候,只会触发 bind ,catch 不会被触发,要避免踩坑 事件修饰符 .stop 的使用会阻止冒泡,但是同时绑定了一个非冒泡事件,会导致该元素上的 catchEventName 失效! .prevent 可以直接干掉,因为小程序里没有什么默认事件,比如submit并不会跳转页面 .capture 支持 1.0.9 .self 没有可以判断的标识 .once也不能做,因为小程序没有 removeEventListener, 虽然可以直接在 handleProxy 中处理,但非常的不优雅,违背了原意,暂不考虑 其他 键值修饰符 等在小程序中压根没键盘,所以… wepy事件绑定区别于vue,根据原生小程序事件提供了语法优化 [图片] Taro 元素的事件处理和 DOM 元素的很相似。但是有一点语法上的不同: Taro 事件绑定属性的命名采用驼峰式写法,而不是小写。 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串 (DOM 元素的写法)。 例如,传统的微信****小程序模板: [图片] Taro 中稍稍有点不同: [图片] 在 Taro 中另一个不同是你不能使用 catchEvent 的方式阻止事件冒泡。你必须明确的使用 stopPropagation。例如,阻止事件冒泡你可以这样写: [图片] request请求 wepy对wx.request做了接受参数的修改,值得一提的是它提供了针对全局的intercapter拦截器。 [图片] 拦截器 [图片] taro对request进行了二次封装,可以使用Taro.request(OBJECT)发起网络请求,支持 Promise 化使用。 [图片] [图片] [图片] mpvue没有对request做特殊优化,与原生相同,可以自己根据需要进行封装 状态管理 wepy 可引用Redux和Mbox,目前wepy的脚手架内已经集成了redux,选择需要即可; mpVue使用vuex; taro使用Redux; 如何选择适合自己的项目 如果只需要做一个微信小程序则根据自己的擅长框架选择mpvue或taro 如果是当前老项目想像向程序迁移同时老项目又是使用vue开发,建议使用mpvue或wepy 如果是老项目使用react开发且需要部分迁移小程序,建议使用taro 如果是新项目且新项目需要同时支持微信小程序和支付宝小程序, 建议使用原生开发,因为目前框架的转译支付宝小程序支持并不是很好,且出了问题不好定位修改, 但如果是小demo不涉及太多逻辑的项目都可以使用框架作为尝鲜; 但如果是涉及太多交互逻辑的则不建议使用框架转译,由于支付宝小程序在视图层基本与小程序一致所以建议手动更改替换部分方法和全局替换一些属性或文件名,如wxml替换为axml这种, 手动转换时间比大概是四比一; 当然如果人手足够一端开发一个是最好的… 时刻前端新鲜技术推送,定期前端精品文章分享,欢迎关注公众号前端小苑。
2019-06-21 - 如何用小程序实现类原生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 - 2019-04-28
- Licia 支持小程序的 JS 工具库
导语 Licia 是一套在开发中实践积累起来的实用 JavaScript 工具库。该库目前拥有超过 300 个模块,同时支持浏览器、node 及小程序运行环境,提供了包括日期格式化、md5、颜色转换等实用模块,可以极大地提高开发效率。 前言 因为小程序运行的是 JavaScript 代码,传统前端所使用的 JS 库理应也能够被用在小程序中才对。然而,经过实际测试,你会发现有相当一部分 npm 包是无法直接在小程序中跑起来的。比如前端工程师十分常用的 lodash,在小程序中引入会报错。 为什么会这样? 主要原因就是绝大部分库的开发者在设计时只会考虑两种运行环境,浏览器和 node,而小程序并不会在其考虑范围内。因此,只要开发者的 JS 代码使用了只有浏览器与 node 中才有的接口,如 DOM 操作、文件读写等,该库就不能正常地运行在小程序环境中。除此之外,假如他们使用了小程序禁用的功能,例如全局变量与动态代码执行,这时候代码跑在小程序环境也会出错。 使用 使用 npm 安装 1、 安装 npm 包 [代码]npm i miniprogram-licia --save [代码] 2、点击开发者工具中的菜单栏:工具 --> 构建 npm 3、直接在代码中引入使用 [代码]const licia = require('miniprogram-licia'); licia.md5('licia'); // -> 'e59f337d85e9a467f1783fab282a41d0' licia.safeGet({a: {b: 1}}, 'a.b'); // -> 1 [代码] 生成定制化 util.js 使用 npm 包的方式会将所有功能引入到代码包中,大概会增加 100 kb 的大小。如果你只想引入所需脚本,可以使用在线工具生成定制化 util 库。 1、访问 https://licia.liriliri.io/builder.html 2、输入需要的模块名,点击生成下载 util.js。 3、将生成的工具库拷贝到小程序项目任意目录下然后直接引入使用。 [代码]const util = require('../lib/util'); util.wx.getStorage({ key: 'test' }).then(res => console.log(res.data)); [代码] 优点 1、目前拥有 270 多个模块可在小程序中正常运行,而 underscore 只有 120 个函数左右。 2、与 lodash 相比增加了不少更加实用的函数,比如 md5、atob、btoa、Emitter、dateFormat 等。 3、可以直接在小程序中引入运行,不像 lodash 需要进行一定的修改才能正常跑在小程序中。 4、定制化生成可以使用更小体积的工具库,这在限制了代码包大小的小程序中十分有用。 附录 这里只简单列出函数及其功能介绍,详细的用法请访问官网查看。 注:模块名右边有小程序图标即表明可以在小程序中使用。 Class: 创建 JavaScript 类。 Color: 颜色转换。 Dispatcher: Flux 调度器。 Emitter: 提供观察者模式的 Event emitter 类。 Enum: Enum 类实现。 JsonTransformer: JSON 转换器。 LinkedList: 双向链表实现。 Logger: 带日志级别的简单日志库。 Lru: 简单 LRU 缓存。 Promise: 轻量 Promise 实现。 PseudoMap: 类似 es6 的 Map,不支持遍历器。 Queue: 队列数据结构。 QuickLru: 不使用链表的 LRU 实现。 ReduceStore: 简单类 redux 状态管理。 Stack: 栈数据结构。 State: 简单状态机。 Store: 内存存储。 Tween: JavaScript 补间动画库。 Url: 简单 url 操作库。 Validator: 对象属性值校验。 abbrev: 计算字符串集的缩写集合。 after: 创建一个函数,只有在调用 n 次后才会调用一次。 allKeys: 获取对象的所有键名,包括自身的及继承的。 arrToMap: 将字符串列表转换为映射。 atob: window.atob,运行在 node 环境时使用 Buffer 进行模拟。 average: 获取数字的平均值。 base64: base64 编解码。 before: 创建一个函数,只能调用少于 n 次。 binarySearch: 二分查找实现。 bind: 创建一个绑定到指定对象的函数。 btoa: window.btoa,运行在 node 环境时使用 Buffer 进行模拟。 bubbleSort: 冒泡排序实现。 bytesToStr: 将字节数组转换为字符串。 callbackify: 将返回 Promise 的函数转换为使用回调的函数。 camelCase: 将字符串转换为驼峰式。 capitalize: 将字符串的第一个字符转换为大写,其余字符转换为小写。 castPath: 将值转换为属性路径数组。 centerAlign: 字符串居中。 char: 根据指定的整数返回 unicode 编码为该整数的字符。 chunk: 将数组拆分为指定长度的子数组。 clamp: 将数字限定于指定区间。 className: 合并 class。 clone: 对指定对象进行浅复制。 cloneDeep: 深复制。 cmpVersion: 比较版本号。 combine: 创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值。 compact: 返回数组的拷贝并移除其中的虚值。 compose: 将多个函数组合成一个函数。 concat: 将多个数组合并成一个数组。 contain: 检查数组中是否有指定值。 convertBase: 对数字进行进制转换。 createAssigner: 用于创建 extend,extendOwn 和 defaults 等模块。 curry: 函数柯里化。 dateFormat: 简单日期格式化。 debounce: 返回函数的防反跳版本。 decodeUriComponent: 类似 decodeURIComponent 函数,只是输入不合法时不抛出错误并尽可能地对其进行解码。 defaults: 填充对象的默认值。 define: 定义一个模块,需要跟 use 模块配合使用。 defineProp: Object.defineProperty(defineProperties) 的快捷方式。 delay: 在指定时长后执行函数。 detectBrowser: 使用 ua 检测浏览器信息。 detectMocha: 检测是否有 mocha 测试框架在运行。 detectOs: 使用 ua 检测操作系统。 difference: 创建一个数组,该数组的元素不存在于给定的其它数组中。 dotCase: 将字符串转换为点式。 each: 遍历集合中的所有元素,用每个元素当做参数调用迭代器。 easing: 缓动函数,参考 http://jqueryui.com/ 。 endWith: 检查字符串是否以指定字符串结尾。 escape: 转义 HTML 字符串,替换 &,<,>,",`,和 ’ 字符。 escapeJsStr: 转义字符串为合法的 JavaScript 字符串字面量。 escapeRegExp: 转义特殊字符用于 RegExp 构造函数。 every: 检查是否集合中的所有元素都能通过真值检测。 extend: 复制多个对象中的所有属性到目标对象上。 extendDeep: 类似 extend,但会递归进行扩展。 extendOwn: 类似 extend,但只复制自己的属性,不包括原型链上的属性。 extractBlockCmts: 从源码中提取块注释。 extractUrls: 从文本中提取 url。 fibonacci: 计算斐波那契数列中某位数字。 fileSize: 将字节数转换为易于阅读的形式。 fill: 在数组指定位置填充指定值。 filter: 遍历集合中的每个元素,返回所有通过真值检测的元素组成的数组。 find: 找到集合中第一个通过真值检测的元素。 findIdx: 返回第一个通过真值检测元素在数组中的位置。 findKey: 返回对象中第一个通过真值检测的属性键名。 findLastIdx: 同 findIdx,只是查找顺序改为从后往前。 flatten: 递归拍平数组。 fnParams: 获取函数的参数名列表。 format: 使用类似于 printf 的方式来格式化字符串。 fraction: 转换数字为分数形式。 freeze: Object.freeze 的快捷方式。 freezeDeep: 递归进行 Object.freeze。 gcd: 使用欧几里德算法求最大公约数。 getUrlParam: 获取 url 参数值。 has: 检查属性是否是对象自身的属性(原型链上的不算)。 hslToRgb: 将 hsl 格式的颜色值转换为 rgb 格式。 identity: 返回传入的第一个参数。 idxOf: 返回指定值第一次在数组中出现的位置。 indent: 对文本的每一行进行缩进处理。 inherits: 使构造函数继承另一个构造函数原型链上的方法。 insertionSort: 插入排序实现。 intersect: 计算所有数组的交集。 intersectRange: 计算两个区间的交集。 invert: 生成一个新对象,该对象的键名和键值进行调换。 isAbsoluteUrl: 检查 url 是否是绝对地址。 isArgs: 检查值是否是参数类型。 isArr: 检查值是否是数组类型。 isArrBuffer: 检查值是否是 ArrayBuffer 类型。 isArrLike: 检查值是否是类数组对象。 isBool: 检查值是否是布尔类型。 isBrowser: 检测是否运行于浏览器环境。 isClose: 检查两个数字是否近似相等。 isDataUrl: 检查字符串是否是有效的 Data Url。 isDate: 检查值是否是 Date 类型。 isEmail: 简单检查值是否是合法的邮件地址。 isEmpty: 检查值是否是空对象或空数组。 isEqual: 对两个对象进行深度比较,如果相等,返回真。 isErr: 检查值是否是 Error 类型。 isEven: 检查数字是否是偶数。 isFinite: 检查值是否是有限数字。 isFn: 检查值是否是函数。 isGeneratorFn: 检查值是否是 Generator 函数。 isInt: 检查值是否是整数。 isJson: 检查值是否是有效的 JSON。 isLeapYear: 检查年份是否是闰年。 isMap: 检查值是否是 Map 对象。 isMatch: 检查对象所有键名和键值是否在指定的对象中。 isMiniProgram: 检测是否运行于微信小程序环境中。 isMobile: 使用 ua 检测是否运行于移动端浏览器。 isNaN: 检测值是否是 NaN。 isNative: 检查值是否是原生函数。 isNil: 检查值是否是 null 或 undefined,等价于 value == null。 isNode: 检测是否运行于 node 环境中。 isNull: 检查值是否是 Null 类型。 isNum: 检测值是否是数字类型。 isNumeric: 检查值是否是数字,包括数字字符串。 isObj: 检查值是否是对象。 isOdd: 检查数字是否是奇数。 isPlainObj: 检查值是否是用 Object 构造函数创建的对象。 isPrime: 检查整数是否是质数。 isPrimitive: 检测值是否是字符串,数字,布尔值或 null。 isPromise: 检查值是否是类 promise 对象。 isRegExp: 检查值是否是正则类型。 isRelative: 检查路径是否是相对路径。 isSet: 检查值是否是 Set 类型。 isSorted: 检查数组是否有序。 isStr: 检查值是否是字符串。 isTypedArr: 检查值是否 TypedArray 类型。 isUndef: 检查值是否是 undefined。 isUrl: 简单检查值是否是有效的 url 地址。 isWeakMap: 检查值是否是 WeakMap 类型。 isWeakSet: 检查值是否是 WeakSet 类型。 kebabCase: 将字符串转换为短横线式。 keyCode: 键码键名转换。 keys: 返回包含对象自身可遍历所有键名的数组。 last: 获取数组的最后一个元素。 linkify: 将文本中的 url 地址转换为超链接。 longest: 获取数组中最长的一项。 lowerCase: 转换字符串为小写。 lpad: 对字符串进行左填充。 ltrim: 删除字符串头部指定字符或空格。 map: 对集合的每个元素调用转换函数生成与之对应的数组。 mapObj: 类似 map,但针对对象,生成一个新对象。 matcher: 传入对象返回函数,如果传入参数中包含该对象则返回真。 max: 获取数字中的最大值。 md5: MD5 算法实现。 memStorage: Web Storage 接口的纯内存实现。 memoize: 缓存函数计算结果。 mergeSort: 归并排序实现。 methods: 获取对象中所有方法名。 min: 获取数字中的最小值。 moment: 简单的类 moment.js 实现。 ms: 时长字符串与毫秒转换库。 negate: 创建一个将原函数结果取反的函数。 nextTick: 能够同时运行在 node 和浏览器端的 next tick 实现。 noop: 一个什么也不做的空函数。 normalizeHeader: 标准化 HTTP 头部名。 normalizePath: 标准化文件路径中的斜杠。 now: 获取当前时间戳。 objToStr: Object.prototype.toString 的别名。 omit: 类似 pick,但结果相反。 once: 创建只能调用一次的函数。 optimizeCb: 用于高效的函数上下文绑定。 pad: 对字符串进行左右填充。 pairs: 将对象转换为包含【键名,键值】对的数组。 parallel: 同时执行多个函数。 parseArgs: 命令行参数简单解析。 partial: 返回局部填充参数的函数,与 bind 模块相似。 pascalCase: 将字符串转换为帕斯卡式。 perfNow: 高精度时间戳。 pick: 过滤对象。 pluck: 提取数组对象中指定属性值,返回一个数组。 precision: 获取数字的精度。 promisify: 转换使用回调的异步函数,使其返回 Promise。 property: 返回一个函数,该函数返回任何传入对象的指定属性。 query: 解析序列化 url 的 query 部分。 quickSort: 快排实现。 raf: requestAnimationFrame 快捷方式。 random: 在给定区间内生成随机数。 randomItem: 随机获取数组中的某项。 range: 创建整数数组。 rc4: RC4 对称加密算法实现。 reduce: 合并多个值成一个值。 reduceRight: 类似于 reduce,只是从后往前合并。 reject: 类似 filter,但结果相反。 remove: 移除集合中所有通过真值检测的元素,返回包含所有删除元素的数组。 repeat: 重复字符串指定次数。 restArgs: 将给定序号后的参数合并成一个数组。 rgbToHsl: 将 rgb 格式的颜色值转换为 hsl 格式。 root: 根对象引用,对于 nodeJs,取 [代码]global[代码] 对象,对于浏览器,取 [代码]window[代码] 对象。 rpad: 对字符串进行右填充。 rtrim: 删除字符串尾部指定字符或空格。 safeCb: 创建回调函数,内部模块使用。 safeDel: 删除对象属性。 safeGet: 获取对象属性值,路径不存在时不报错。 safeSet: 设置对象属性值。 sample: 从集合中随机抽取部分样本。 selectionSort: 选择排序实现。 shuffle: 将数组中元素的顺序打乱。 size: 获取对象的大小或类数组元素的长度。 sleep: 使用 Promise 模拟暂停方法。 slice: 截取数组的一部分生成新数组。 snakeCase: 转换字符串为下划线式。 some: 检查集合中是否有元素通过真值检测。 sortBy: 遍历集合中的元素,将其作为参数调用函数,并以得到的结果为依据对数组进行排序。 spaceCase: 将字符串转换为空格式。 splitCase: 将不同命名式的字符串拆分成数组。 splitPath: 将路径拆分为文件夹路径,文件名和扩展名。 startWith: 检查字符串是否以指定字符串开头。 strHash: 使用 djb2 算法进行字符串哈希。 strToBytes: 将字符串转换为字节数组。 stringify: JSON 序列化,支持循环引用和函数。 stripAnsi: 清除字符串中的 ansi 控制码。 stripCmt: 清除源码中的注释。 stripColor: 清除字符串中的 ansi 颜色控制码。 stripHtmlTag: 清除字符串中的 html 标签。 sum: 计算数字和。 swap: 交换数组中的两项。 template: 将模板字符串编译成函数用于渲染。 throttle: 返回函数的节流阀版本。 timeAgo: 将时间格式化成多久之前的形式。 timeTaken: 获取函数的执行时间。 times: 调用目标函数 n 次。 toArr: 将任意值转换为数组。 toBool: 将任意值转换为布尔值。 toDate: 将任意值转换为日期类型。 toInt: 将任意值转换为整数。 toNum: 将任意值转换为数字。 toSrc: 将函数转换为源码。 toStr: 将任意值转换为字符串。 topoSort: 拓扑排序实现。 trim: 删除字符串两边指定字符或空格。 tryIt: 在 try catch 块中运行函数。 type: 获取 JavaScript 对象的内部类型。 types: 仅用于生成 ts 定义文件。 ucs2: UCS-2 编解码。 unescape: 和 escape 相反,转义 HTML 实体回去。 union: 返回传入所有数组的并集。 uniqId: 生成全局唯一 id。 unique: 返回数组去重后的副本。 unzip: 与 zip 相反。 upperCase: 转换字符串为大写。 upperFirst: 将字符串的第一个字符转换为大写。 use: 使用 define 创建的模块。 utf8: UTF-8 编解码。 values: 返回对象所有的属性值。 vlq: vlq 编解码。 waitUntil: 等待直到条件函数返回真值。 waterfall: 按顺序执行函数序列。 wrap: 将函数封装到包裹函数里面, 并把它作为第一个参数传给包裹函数。 wx: 小程序 wx 对象的 promise 版本。 zip: 将每个数组中相应位置的值合并在一起。
2019-05-07 - 【U计划】弹幕biubiu小程序开发经验分享
弹幕biubiu小程序开发经验分享 Hello,大家好~我们是来自清华大学软件学院大三的弹幕弹幕团队,我是团队的Leader&Developer。我们团队开发的小程序叫作“弹幕biubiu”,主要应用场景是各类晚会现场。你可能已经发现了,我们的团队名和小程序名是不一样的,这是因为我们在确定了团队名称之后,发现这个名字已经被其他的小程序占用了,所以我们只能将小程序换成另一个名字。这也提醒了大家,在开发小程序的时候,一定要先确认自己起的名字没有被使用哦~ 我们的小程序是从去年10月开始开发的,直至今年3月基本完成。之后在今年4月的清华大学软件学院学生节上,我们的小程序作为观众弹幕互动平台被使用,并取得了广泛好评。这一方面说明我们小程序的实用性,另一方面也说明了弹幕互动及其衍生方向依然有很大的发展空间。 [图片] 我们团队的开发选用了敏捷开发的方式,并采用Scrum框架(下图源自清华大学软件学院刘强老师的软件工程课件)。本文将介绍我们团队在开发过程中所做的一些主要工作,希望能够给大家一些启发与帮助。[图片] 1.立项 万事开头难,开发过程中最困难的地方,往往就是在最开始的地方。一个团队中不缺技术人员,而缺少设计人员,也就是“有思想的人”。而且好的想法一定是来源于生活的,如果不仔细观察生活,只是天马行空地构想,是无法获得好的项目主题的。 我们团队在计划开发一个小程序之后,就开始讨论主题。首先我们确定了我们小程序的大致方向。我们发现在每次举办院系学生节时,都需要科协同学用一天的时间去部署弹幕墙,这样效率较低,而且也常常出现弹幕墙宕机的情况,所以我们决定开发一个学生节小程序。之后我们团队通过头脑风暴,将自己设想为学生节举办方与学生节观众,讨论我们可能需要哪些功能,不需要哪些功能等等,从而将项目目标进一步细化。 在这里我们并不能只单纯地讨论,我们需要一个记录者,将所有人的想法记录下来并进行归整,之后再由每个成员进行修改完善,得到我们开发的第一份文档——产品规划文档(弹幕biubiu的产品规划文档)。如果大家对产品规划文档形式不太清楚的话,可以参照上述我们的文档。在文档中我们对于产品的定位、产品的特性以及产品的路线都有了一个明确的描述。 当我们有了产品规划文档之后,立项的过程还没有结束。因为这个文档只是根据自己团队的想法写成的,但是真正的用户会有成千甚至上万人,并不一定每个人都和团队内的成员想法相同。除此之外,市场上可能已经有一些类似的产品。所以我们必须要进行一个调查,明确其他可能的用户的需求以及现有市场上的产品提供的功能。这样的调查有助于我们跳出团队内固有的思维模式,催生出一些新的点子,同时也能避免无用功。在调查结束之后,我们获得了更多的用户需求。我们需要对需求进一步整理细化,写出用户故事(弹幕biubiu的需求获取与用户故事文档),并画出用户故事地图。 [图片] 到此为止,我们基本完成了所有立项工作。这时整个团队应当对自己的项目开发目标有了明确清晰的认知。 2.设计 当明确需求之后,我们就要开始设计工作,也就是进入到敏捷开发的迭代周期中了。设计主要分为两部分: 系统设计 原型设计 系统设计中最重要的工具是UML,即统一建模语言。你可能会用到其中的类图、活动图、用例图等。 2.1 系统设计 系统设计目的是确立技术开发过程中的总纲,这也是开发过程中极为重要的一步,整个开发过程都是围绕系统设计文档展开的。 我们团队开发的小程序使用的是MVC模式,即模型(model)-视图(view)-控制器(controller)。这种模式满足了高内聚、低耦合的程序结构,便于团队开发与管理。 系统设计要求我们首先对于整体系统结构有一个清晰的构想。我们的弹幕小程序的系统架构图如下所示,可以看到这里我们将系统清晰地分为了数层。 [图片] 之后我们就需要对系统的结构进行细化,主要包括两部分内容: 数据存储结构。也就是数据库的设计。我们用一个数据库来管理数据,那么我们需要构造哪些表,每个表中需要保存那些数据等等,都是我们需要考虑的问题。我们团队采用了Mysql+Redis两种数据库相结合的存储模式,保证了数据的读写效率。 前后端接口设计。接口设计可以方便团队前后端开发的分离,提高开发效率。我们团队采用的是Restful的API接口规范,大家可以自行查阅了解该规范的内容。 因为不同的小程序会有不同的系统,所以我这里的设计思路也仅供大家参考,主要的还是要开发团队自己思索并设计,得到最适合自己的系统结构。当然合适的系统架构不意味着在项目的最初就要将所有细节想得十分透彻。一个好的系统架构主要有两方面的特征:稳定的框架与可扩展性。稳定的框架保证了开发过程中无需对代码进行很大程度的调整重构;可扩展性保证了开发人员可以很轻松地将后续的内容添加进系统,而不会影响系统整体的特性。 2.2 原型设计 原型设计,也就是UI设计。我们团队使用的工具是墨刀。墨刀有着丰富的素材库,并且可以设计控件行为,方便团队成员理解交互过程。 [图片] 原型设计往往会是团队中讨论最激烈的环节,因为每个人的审美是不同的,更何况团队中基本都是理工生(sigh…)。我们在原型设计时也进行了多次讨论与修改,才最终确定其样式。 3.开发 3.1 团队管理 自组织团队是敏捷开发的基础,团队被授权自己管理工作进程,并决定如何完成工作。团队成员在开发的过程中需要各司其职,扮演好自己的角色。但是根据著名的“20%定律”,每个团队中总会有20%的成员是free rider,所以这就需要团队的领导者对团队进行良好管理。 在开发的初期,一个团队需要制定自己团队的开发章程,包括每周的开会时间、开会地点、每个成员负责开发的模块、团建安排等,并在之后的开发过程中严格按照章程的规定管理团队。 其次,团队在每次例会时,需要每名成员汇报之前任务的开发进度,对开发过程中遇到的问题进行讨论思考,并确定下一阶段的开发任务。每次的例会都需要指定一名成员记录会议内容,方便团队日后查看。 团队管理中,最主要的就是任务安排的部分。任务安排主要包括两点:明确分工与时间规划。 明确分工是要让大家清楚自己具体是要做什么,其重点在于“细化”。举个例子,如果你和我说,“你去做一个主办方管理网站”,我肯定一头雾水无从下手;但是如果你和我说:“你去实现一个主办方登录的功能,主办方输入用户名和密码就可以跳转到活动列表页面”,那我就可以很快地完成这一个任务。所以明确分工是团队成员,尤其是组织者,需要重点注意的。 时间规划则是让大家有一个紧迫感。做时间规划最好的方法是,组织者先定一个大概的时间,然后所有团队成员进行协商,定出每个人都满意或至少不反对的时间安排。因为团队成员都会有惰性,就算最好的团队也不例外,所以一个明确的时间规划可以让每个人有计划有安排地完成任务。从这个意义上来讲,时间规划也是调动成员热情的不错的方案。 3.2 代码管理与持续集成 我们团队使用git进行代码版本的控制管理,在开发过程中维护了三个代码库,分别对应于系统后端、微信小程序以及弹幕主墙应用程序。我们也利用Github上的Issues、Projects、Wiki等功能辅助我们进行开发。由于我们团队尚未开源,所以这里也不方便向大家展示代码库的具体细节。 此外,我们团队使用Travis CI辅助我们进行代码的持续集成与自动部署,感兴趣的话大家可以自主学习一下它的使用方法。 3.3 文档管理 通过上文我们也发现了,在开发过程中我们会写很多的文档,所以合理的文档管理也是开发中的重点问题。我们团队使用的在线文档工具是石墨文档。石墨文档有三个优点: 有清晰简洁的界面与丰富的功能 支持多成员在线编辑 可以很方便地导出为word文档与pdf文档 我们团队还维护了一个产品文档目录,这样每次要修改或查阅文档时,都有一个很便捷的入口。 [图片] 4.测试 测试主要有三部分:单元测试、功能测试与性能测试。 单元测试。单元测试,就是对软件中最小可测试单元进行检查和验证。不同软件的单元测试是不同的,比如我们在开发后端时,使用的是Python中的Django框架,这一框架是自带单元测试模块的,所以我们只需在test.py中实现所有测试样例即可。单元测试保证了软件最基本的正确性,最佳的模式是开发者在开发的过程中就将单元测试样例写好。(现在微信小程序还没有单元测试的模块,希望之后小程序团队可以在这方面给出接口。) 功能测试。功能测试就是要测试软件系统的各个功能能否正常执行。功能测试的辅助软件有很多,但最简单也最方便的就是人工手动测试,也就是开发者模拟用户的使用场景测试一遍自己的软件系统。 性能测试。软件性能也是评判一个软件好坏的重要依据。就以我们的弹幕小程序为例,虽然现在的学生节晚会只有三百多人,但是如果要拓展到所有晚会时,就不得不考虑高并发的情况。所以我们团队使用Jmeter对于发送弹幕的功能进行了性能测试,并在测试之后通过图片压缩等方式提高了我们小程序的性能。 除此之外,测试环节还包括安全性测试、易用性测试、兼容性测试等。测试过程中大家需要尤其注意的地方是:一定要将测试场景与测试样例想全面。越是严密的测试,软件系统也就相应越理想。 5.分析与维护 在开发与测试结束之后,小程序也就被正式投入使用了。因为用户行为是多种多样的,所以这个时候不出意外会出现一些奇奇怪怪的bug。作为开发者一定要给出一个用户反馈的途径,并且根据用户反馈的问题,制定下一个迭代周期的任务。这样循环往复,直至软件达到预期。下图为我们小程序为用户提供的反馈接口: [图片] 6.总结 以上就是我在开发过程中的一些经验与体会,希望能够给大家一些帮助与启示。弹幕biubiu小程序的开发,对于我来说是一个特别宝贵的经历。在这个过程中我学到了很多新的知识,接触到了很多新的事物,也发现了其他同学很多的优点。同时也很感谢刘强与刘璘两位老师对我们团队的支持与指导,在这里也推荐一下两位老师在学堂在线上的软件工程课程,如果大家感兴趣的话可以去了解学习一下,相信会给你们很大的帮助。 附言 如果大家对我们的小程序感兴趣的话,也可以使用一下呀~ 使用说明 主办方管理网站 应用程序下载链接(也可在主办方管理网站中下载) 小程序二维码 [图片] 大家有什么问题或者建议的话,也欢迎随时与我交流~ 我的Github地址是:https://github.com/JL-Cheng 我的邮箱是:chengjl16@163.com
2019-05-23 - U计划入选项目【清艺管家】开发经历分享
产品简介 清艺管家是我们为学校的艺教中心开发的小程序,旨在用小程序这种更方便的方式让同学们预约使用上我们学校的琴房,也能让琴房利用率更高、管理更规范。因为是给清华大学艺术教育中心使用的,并取"轻易"谐音,所以给这个小程序取名叫清艺管家。 目前正在校内办程序上线的相关手续。 产品设计思路 需求背景 到目前为止,在我们学校使用琴房需要经历以下的流程: [图片] 其中,“办理琴卡"和"打孔"都需要去现场找老师进行办理,且办卡处和琴房不在同一个地方。可以发现这样的琴房使用流程是低效且难以管理的,暂且不论同学在不知道"是否有琴房可供使用"的情况下到现场很可能发现"自己有空的时间没有空闲的琴房”。另外,办卡对于短期使用琴房的同学来说有些不太合理。 通过与艺教中心的老师沟通,我们得知他们目前管理琴房使用记录是纯手工进行,即一名同学使用了一次则写一行记录,记录整理汇总的难度较大。 基于以上旧有预约制度的不便利,我们希望开发“清艺管家”这个系统,此系统能够使用微信小程序向用户显示琴房信息和预约情况,处理用户的预约请求,并让管理老师在现场能够完成预约者身份的验证。 产品定位 通过问卷调研需求、撰写用户故事等方法,我们获取了"用户们"对新系统的大致需求,也给了我们的产品一个较为合适的定位: 对于用户端,微信小程序拥有的很多方便的特性,避免使用浏览器,APP等,只在微信一个应用内就能完成预约过程。所以对于用户端我们优先考虑使用微信小程序。 对于管理端,使用网页的方式呈现,能够尽量呈现多的信息,方便老师进行琴房管理。 目标用户:清华师生及校外可能使用到琴房的人 简介:提供预约琴房服务 产品理念:交互流畅,操作简单,定制化(按时提醒) 主要功能:提供查看使用情况,预约,缴费,查询记录等功能 卖点:简化预约-付款-签到流程,提高管理方管理效率,提升琴房预约系统用户的用户体验 产品设计 以下4幅图分别是我们对"清艺管家"系统管理端、小程序端模块设计和系统设计。 管理端设计 [图片] 小程序端整体设计 [图片] 小程序端各模块设计 [图片] 系统设计 [图片] 原型设计 首先,我们使用墨刀作为我们的原型设计工具,因为免费而且上手难度不高。其次,我们都没有美术或者设计功底,所以没有打算把原型做的多么精致,只是希望能够把原型界面做得简单明了,并且指导之后的前端开发就好了。 我们选择了紫色(因为清华是紫色的)作为我们的主题色,针对一些优先级最高的功能进行了界面设计,下面是我们初期的原型设计。 预约流程 [图片] 订单流程 [图片] 个人界面 [图片] 墨刀原型设计链接 我们真正开发出来的小程序和原型设计相差还是很大的,这个原型设计也是比较粗糙,干脆把链接贴出来: https://free.modao.cc/app/0cfcc5536c6a9ce8fbd81a0ee793120a981ce514#screen=s5A3CC545541540988107569 开发心得 一定要经常与需求方(对于我们来说是艺教中心)沟通需求,因为仅靠前期大致的需求调研往往是不能够全面知悉的,最好一边设计或者开发一边与需求方明确具体需求。 有的时候开发团队闷声开发,奋战几周出来一个产品,会发现与客户想要的完全不是一个东西; 开发过程中可能会有灵光一闪,把新的想法告诉客户之后客户可能会发现这正是他想要的; 敏捷开发正是以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发,现在社会节奏快,客户需求变化也快,我们不能也不应该要求客户一开始就板上钉钉地给出最终方案。 由于是在学期里为学校的机构开发应用,学习压力还是有的,这要求我们小组四个人都明确自己的分工,在确定的时间节点之前保质保量地完成开发和测试工作。我们建议类似的团队在以后的开发中尽量一起开发,虽然可能开发的模块不一样,对开发工作没有直接帮助,但是几个人坐在一块交流更加顺畅,对彼此进度也更加了解,这在团队开发中是很重要的,相当于让彼此都安心吧。(其实是相互鼓励,然后还能约饭哈哈哈~ 要利用好 github 等代码托管仓库进行团队成员工作的分配和协调,作为组长可以多布置 Issue ,如果有人领锅则好,没人领锅就强行甩锅,这样工单管理也比较系统,日后回顾会有成就感。比如下面是我们的 Issue 和 Project 面板: [图片] [图片] 人越多的项目,越有使用CI的必要。有项目经验的同学可能有体会:自己开发了一两天,从远端把代码 Fetch 回来之后发现原来 Work 的代码现在不 Work 了,仔细一看发现是队友写的代码有问题,但是你和队友分别负责两个模块,你不想看也看不懂他写的代码,但是他往主分支里推了没有检验过的代码。这是相当难受的,但是如果配好CI,就可以很快的在这个时候发现问题,并告诉你的队友说:“你的代码都还没 Work 也敢往 master 推?”于是晚饭就有了。 [图片]
2019-05-29 - 如何实现一个6位数的密码输入框
背景: 因为公司业务调整需要做用户支付这一块 开发者需要在小程序上实现一个简单的6位数密码输入框 [图片] 首先想下如何实现该效果: 1.使用input覆盖在框上面,设置letter-spacing达到数字之间间距的效果,实现时发现在input组件上使用letter-spacing无效果 2.循环六个view模拟的框,光标使用动画模拟,一个隐藏的input,点击view框时触发input的Focus属性弹起键盘,同时模拟的光标展示出来,输入值后,input的value长度发生变化,设置光标位置以及模拟的密码小黑圆点 好了,废话不多数,咱们直接上手。 wxml [代码]<view class='container'> <!-- 模拟输入框 --> <view class='pay-box {{focusType ? "focus-border" : ""}}' bindtap="handleFocus" style='width: 604rpx;height: 98rpx'> <block wx:for="{{boxList}}" wx:key="{{index}}"> <view class='password-box {{index === 0 ? "b-l-n":""}}'> <view wx:if="{{(dataLength === item - 1)&& focusType}}" class="cursor"></view> <view wx:if="{{dataLength >= item}}" class="input-black-dot"></view> </view> </block> </view> <!-- 隐藏input框 --> <input value="{{input_value}}" focus="{{isFocus}}" maxlength="6" type="number" class='hidden-input' bindinput="handleSetData" bindfocus="handleUseFocus" bindblur="handleUseFocus" /> </view> [代码] wxss [代码]/* 第一个格子输入框 */ .container .b-l-n { border-left: none; } .pay-box { margin: 0 auto; display: flex; flex-direction: row; border-left: 1px solid #cfd4d3; } /* 支付密码框聚焦的时候 */ .focus-border { border-color: #0c8; } /* 单个格式样式(聚焦的时候) */ .password-box { flex: 1; border: 1px solid #0c8; margin-right: 10rpx; display: flex; align-items: center; justify-content: center; } /* 模拟光标 */ .cursor { width: 2rpx; height: 36rpx; background-color: #0c8; animation: focus 1.2s infinite; } /* 光标动画 */ @keyframes focus { from { opacity: 1; } to { opacity: 0; } } /* 模拟输入的password的黑点 */ .input-black-dot { width: 20rpx; height: 20rpx; background-color: #000; border-radius: 50%; } /* 输入框 */ .hidden-input { margin-top: 200rpx; position: relative; } [代码] JS [代码]Component({ data: { //输入框聚焦状态 isFocus: false, //输入框聚焦样式 是否自动获取焦点 focusType: true, valueData: '', //输入的值 dataLength: '', boxList: [1, 2, 3, 4, 5, 6] }, // 组件属性 properties: { }, // 组件方法 methods: { // 获得焦点时 handleUseFocus() { this.setData({ focusType: true }) }, // 失去焦点时 handleUseBlur() { this.setData({ focusType: false }) }, // 点击6个框聚焦 handleFocus() { this.setData({ isFocus: true }) }, // 获取输入框的值 handleSetData(e) { // 更新数据 this.setData({ dataLength: e.detail.value.length, valueData: e.detail.value }) // 当输入框的值等于6时(发起支付等...) if (e.detail.value.length === 6) { // 通知用户输入数字达到6位数可以发送接口校验密码是否正确 this.triggerEvent('initData', e.detail.value) } } } }) [代码] 实现方式很简单,有点小问题,还有一些后续准备做的优化点,等完善后上线后再来修改一波。 最后附上代码片段: https://developers.weixin.qq.com/s/8CtRqJmT7W8k
2020-07-06 - “比零坊”开发经验分享
本文主要从三个方面介绍开发经验: 立项 设计 开发 1 立项 立项是一个项目的开始部分,在我看来也是最重要的环节,它决定了一个项目的需求、走向等等,整个立项分为两个部分: 项目定位 功能探讨 1.1 项目定位 出于对身边同学消费情况的实际背景,我们团队一致认为急需建立一个大学生消费数据分析平台来帮助大学生树立正确的消费观,培养正确的消费意识,改善大学生的消费结构。下图是我们在团队讨论中形成的项目背景: (1)大一新生不熟悉消费环境。由于刚入学缺乏信息获取的渠道,新生们往往找不到适合自己经济情况的优质商品从而购入了价格过高或者质量很差的商品。 (2)大学生消费意识薄弱。大学生的消费意识不足,自我理财能力不够。 (3)盲目冲动,缺少理性。很多大学生购买物品盲目冲动,购买的自我控制能力较差,总为了追求新鲜的事物,购买了大量没有实用价值的产品。 (4)攀比心理严重。在虚荣心的驱使下过分的追求名牌,导致大学的中心不在学习上。 (5)消费不平衡。前半个月用掉近2/3的生活费,学习仅占10%。 (6)大学生缺少理性消费引导。多数学生在消费决策时都愿意在购买前花时间详细了解商品的信息与价格,但是缺少了解渠道与理性消费引导。 大学生消费现状令人堪忧,消费结构不平衡,急需一个大学生消费数据分析平台来帮助大学生树立正确的消费观,培养正确的消费意识,改善大学生的消费结构。 1.2 功能探讨 我们对市场上的记账软件比如:鲨鱼记账、随手记等进行了使用与调查,团队讨论中结合它们的优点,添加我们自己的亮点,在记账、查阅账单等基础功能之上,添加了统计与推荐功能,下图是项目的功能模块图: [图片] 2 设计 设计工具我们用的是Axure RP8,这是一款比较好用的原型设计工具,我们一开始是团队分工每人设计一个模块,只是规定了小程序颜色,对具体的规范没有说的很详细,导致第一版原型做出来之后,每个人做的千奇百怪…后来我们又重新确定了规范。所以大家在设计的时候,一定要把规范、风格统一再入手。下面是出期设计的原型: [图片][图片] 3 开发 这部分主要分享产品开发的内容,主要包括三个方面:规划记录、开发工具和版本控制。 3.1 规划记录 (1)一周一次会议。开发小组成员和组长轮流分别说出本周的工作成果、发现的问题以及下周工作安排。对于比较重大的问题,由所有成员来一起讨论合适的解决方法。会议之后,组长根据会议内容整理出会议文档。其中,使用todolist来制定详细的下周工作计划,以便跟踪项目进度。 (2)三周一次集成测试。产品的集成测试主要生成最小可运行的程序版本。由于我们是分模块开发,模块之间具有一定联系,集成测试是必不可少的。 [图片][图片] 3.2 开发工具 (1)小程序开发工具使用的是微信开发者工具,简单易用。 (2)后台使用Java EE搭建而成,工具使用的是IntelliJ IDEA社区版。 (3)数据库管理则采用轻量快速的MySQL。 3.3 版本控制 我们使用华为云代码托管作为远程仓库,进行版本控制。小组成员经由Git来提交更新程序代码。程序存在两个版本,一个是随成员修改更新的开发版,另一个则是由组长管理的最小可运行版。最小可运行版本只包含可以稳定运行的程序功能。在三周一次的集成测试后,稳定的程序功能将会被纳入最小可运行版本,其他的则在修改过后进行下一轮集成测试。 本次分享就到这些了~
2019-05-29 - 完美解决小程序session问题
小程序不像web浏览器有cookie机制,在默认使用cookie存sessionid的机制下,后台将无法正常使用session功能,如果正确使用session呢,提供两个方案。 [代码]1、将sessionid通过url进行传递 用户每次登录成功后将生成的sessionid值使用参数回传到客户端, 客户端接到sessionid后保存到本地, 在发起网络请求的底层接口中默认自动带上sessionid=本地存储的sessionid值。 需要配合服务器一起更改,服务器后端默认使用cookie机制 2、无缝对接cookie, 将服务器的set-cookie值保存到本地,再请求的时候模拟浏览器头部信息并带上保存的cookie信息 1)保存cookie值: _XHR('login',{'code':res.code}).then(function( ret ){ ret.header["Set-Cookie"] != undefined && wx.setStorageSync("cookie", ret.header["Set-Cookie"]); }); 2)请求的时候自动带上cookie信息 var header={}; header = { 'content-type': 'application/x-www-form-urlencoded' }; var cookie = wx.getStorageSync("cookie"); if( url != 'login' && !isNull( cookie ) ){ header['cookie'] = cookie; } 将header 赋值到 request的header内 wx.request({ url: qryDomian + url + '.html', data: _data, method: 'POST', header: header, dataType:'json' ...... 第二种方案服务器无需做任务操作。[代码]
2019-05-20 - 自定义标题栏
使用效果 [图片][图片][图片][图片] 使用方法 属性介绍 属性名 类型 默认值 是否必须 说明 menuSrc String ‘’ 否 按钮图片地址 bgImgSrc String ‘’ 否 背景图片地址 bgImgMode String aspectFill 否 背景图片的显示模式 title String ‘’ 否 标题 titleTextColor String ‘’ 否 字体和按钮以及loading图标的颜色,按钮和loading暂时只有黑白2色 backgroundColor String ‘’ 否 整个标题栏的背景颜色 loading Boolean false 否 是否是加载状态 backProxy Boolean false 否 是否重写了返回键 标题栏中属性的默认数据会自动获取json配置以及系统的默认数据,如果不需要动态更改样式,可以在json中设置,组件中同样起作用 事件介绍 属性名 detail NaviBack 返回的逻辑方法 MenuTap 按钮的点击事件 [代码]"usingComponents": { "toolBar": "/component/toolbar" }, [代码] [代码]<toolBar menuSrc='/image/menu_white.png' bindMenuTap='onMenuTap' bgImgSrc='/image/navi-bg.jpg' /> [代码] 高度说明: 为了方便适配,这里给出自定义标题栏的计算公式: const MenuRect = wx.getMenuButtonBoundingClientRect() const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; const height = (MenuRect.top - statusBarHeight) * 2 + MenuRect.height +MenuRect.top Github地址:https://github.com/Aracy/wx-mini-navigationbar
2019-05-21 - 小程序之「 navigateBack 」公共页面案例
navigateBack的用法 [代码] wx.navigateBack({ delta: backSize // backsize表示关闭几页,从页面栈中移除几个(包含本页面) }) [代码] 项目中的需求 「 需求 」:做一个公共结果页,从其他地方跳转到这个页面,最后点击"完成按钮或者右上角返回"后,回到想回到的页面,并且控制目标页面刷新还是不刷新。 「 分析 」:想实现回到想回到的页面,通过对页面栈的管理来实现。 「 举例 」:页面栈中有1-2-3-4-5,5是公用页面,现在想跳到1,那么就可以通过navigateBack的delta值等于4来控制回到1,我们就动态改变delta的值实现动态跳转,navigateBack会让页面出栈,这样就很好地维护了页面栈。 具体思路 通过getCurrentPages( )能够拿到当前小程序页面栈的数组,数组中有个key为route,该值即是栈里页面地址。要跳转到1,4传值给5,5中通过遍历页面栈数组,判断传过来的值是否等于页面栈中的route值,相等的话,记录index,这样即可完成。 最终效果 无论是触发了ios滑屏退出、android物理按钮返回、小程序左上角返回按钮,都会回到目标页面。 但是,经真机测试后发现。点击左上角返回、手机物理返回键、滑屏返回到目标页面,效果上会关闭两个页面。但最终也可以回到目标页面。只能说功能达到了,效果上没那么完美。这没办法。毕竟页面卸载的处理本身就是一个伪处理-_- 代码说话 这是项目中用到的公用页面,就是通过navigateBack来控制跳转 注释写得很清楚了 想要更清晰地了解页面栈是怎样的,就打断点看一看 有问题欢迎留言讨论😄 [代码]Page({ data: { isInvoke: false,// 是否调用过按钮 isUnload: true,// 是否是卸载的生命周期 resultImg: "", // 图标(页面内使用) result: { title: "",// 必传(页面标题) type: 1,// 必传(0:失败 1:成功) url: "", // 必传(跳转的url),传空或者不在页面栈中,跳转到首页或指定页面 // 选传(上面文案布局,成功默认是:成功,失败默认:失败) resultUp: "", // 选传(下面文案布局,不传默认是空 resultDown: "", // 选传(按钮名称,成功默认:完成,失败默认:重试) btnName: "", // 选传(是否刷新目标页,默认刷新) isRefresh: true, } }, /** 页面初始化 */ onLoad: function (options) { }, onShow: function () { this.initData(); }, /** 页面卸载*/ onUnload: function () { // 如果点击了按钮,就不再调用它 if (!this.data.isInvoke) { this.data.isUnload = true this.targetJump() } }, /** 初始化值 */ initData: function () { var result = wx.getStorageSync("result") if (result) { // 页面标题 wx.setNavigationBarTitle({ title: result.title }) var type = result.type this.data.result.url = result.url if (typeof (result.isRefresh) != "undefined") { this.data.result.isRefresh = result.isRefresh } if (type) { // 成功 result.btnName = result.btnName ? result.btnName : "完成" this.data.resultImg = "/resources/success.png" result.resultUp = result.resultUp ? result.resultUp : "成功" } else {// 失败 result.btnName = result.btnName ? result.btnName : "重试" this.data.resultImg = "/resources/failed.png" result.resultUp = result.resultUp ? result.resultUp : "失败" } var btnName = "result.btnName" var resultUp = "result.resultUp" var resultDown = "result.resultDown" this.setData({ resultImg: this.data.resultImg, [resultUp]: result.resultUp, [resultDown]: result.resultDown ? result.resultDown : "", [btnName]: result.btnName }) } }, /** 按钮点击事件 */ btnClick: function () { this.data.isInvoke = true this.data.isUnload = false this.targetJump(); }, /** 目标页面跳转 */ targetJump: function () { // 清除缓存 wx.removeStorageSync("result") var pages = getCurrentPages() // 如果url为空或者不在页面栈中,返回首页 var backSize = 100 var target console.log(pages) for (var i = 0;i < pages.length;i++) { if (this.data.result.url.substring(1) == pages[i].route) { // 自身也在栈中,所以要-1 backSize = pages.length - i - 1; target = pages[i] console.log("target", target) break } } console.log("backSize", backSize) if (backSize == 100) {// 首页或者指定页面 wx.reLaunch({ url: this.data.result.url, }) return } if (this.data.result.isRefresh) { // 刷新目标页面 target.onLoad() } if (this.data.isUnload) {// 用户点击了返回键 // 如果目标页面在页面栈中倒数第二个位置,返回 if (backSize == 1) { return } // 如果只是调用了小程序返回键,因为back键默认会关掉一页 backSize = backSize - 1 } wx.navigateBack({ delta: backSize }) } }) [代码] 以上
2019-05-28