- 代金券api 返回活动未开始或已结束 ?
["stock_name"] => string(6) "你好" ["comment"] => string(6) "你好" ["belong_merchant"] => string(10) "*******" ["available_begin_time"] => string(29) "2021-03-16T14:08:48.120+08:00" ["available_end_time"] => string(29) "2021-04-15T14:07:48.120+08:00" ["stock_use_rule"] => array(6) { ["max_coupons_per_user"] => int(10) ["max_coupons"] => int(10) ["max_amount"] => int(10) ["max_amount_by_day"] => int(1000) ["natural_person_limit"] => bool(true) ["prevent_api_abuse"] => bool(true) } ["coupon_use_rule"] => array(2) { ["fixed_normal_coupon"] => array(2) { ["coupon_amount"] => int(1) ["transaction_minimum"] => int(1) } ["available_merchants"] => array(1) { [0] => string(10) "*******" } } ["no_cash"] => bool(false) ["stock_type"] => string(6) "NORMAL" ["out_request_no"] => string(24) "161587486816158748686725"
2021-03-16 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - 2020-03-10
- 那些被忽略的盒子模型小知识
那些被忽略的盒子模型小知识 本文是笔者在学习CSS时的一些小白总结 我们知道的盒子模型主要由4个区域组成,分别是内容区域(content),内边距区域(padding),边框区域(border)和外边距区域(margin)。 对于不了解盒子模型的朋友可以移步到这里了解一下。 [图片] Content(内容) 1. 替换元素 替换元素(replaced element),顾名思义就是内容可以被替换的元素。 我们通常会把一些特殊意义的文本替换成图片,比如一个网站的logo。 [图片] 我们会在页面上看到的不是h1标签显示的”Google“文字,而是谷歌logo的图片。使用了content的元素的内容在html标签中是不存在的。这样做就有个好处,当爬虫来访问我们的网站,爬虫可以知道我们这个主站的h1标题是”Google“而不是一个img标签,且在视觉上给用户更好的体验。 2. 伪元素::before和::after [图片] 为了实现上面显示价格,之前写react代码时候会经常这么写,感觉在逻辑上写了好多关联性不大的文本。其实可以利用[代码]::before[代码]和[代码]::after[代码]两个伪元素,把这些与逻辑不相关的写在css里,react dom则专注于数据的表现。 [代码]<div className="price-panel"> <span className="price-panel__price"> ¥ {(totalPrice / 100).toFixed(1)} </span> <span className="price-panel__discount-price"> 已省¥ {(totalDiscountPrice / 100).toFixed(1)} </span> <span className="price-panel__discount"> ( {(discount / 10).toFixed(1)} 折) </span> </div> [代码] 使用了伪元素后的react代码显然更加清晰表示数据。 HTML: [代码]<div className="price-panel"> <span className="price-panel__price"> {(totalPrice / 100).toFixed(1)} </span> <span className="price-panel__discount-price"> {(totalDiscountPrice / 100).toFixed(1)} </span> <span className="price-panel__discount"> {(discount / 10).toFixed(1)} </span> </div> [代码] SCSS: [代码].price-panel { &__price { &::before { content: '¥'; } } &__discount-price { &::before { content: '已省¥'; } } &__discount { &::before { content: '('; } &::after { content: '折)'; } } } [代码] 我们还能使用伪元素帮助实现一些本来需要多个div实现的样式,比如下面这个对话框。 [图片] HTML: [代码]<div class="dialog">Hi,I’m a bubble dialog. Can you see me?</div> [代码] CSS: [代码].dialog { background: #f0f; padding: 10px; border-radius: 10px; color: white; max-width: 250px; position: relative; overflow: visible; } .dialog::after { position: absolute; content: ''; display: inline-block; border-width: 5px 10px; border-style: solid; border-color: transparent transparent #f0f #f0f; width: 0; height: 0; right: -20px; } [代码] Padding(内边距) padding的百分比值是非常有用的。需要注意的padding的百分比值,无论是水平方向还是垂直方向都是相对于父级元素的宽度进行计算的。 如果需要弄一张16:9的等比缩放图片,可以利用padding的这个特性,设置一个[代码]padding-top[代码]或者[代码]padding-bottom[代码]为56.25%即可(100\16*9) [图片] [图片] Margin(外边距) 1. margin合并 块级元素的[代码]margin-top[代码]和[代码]margin-bottom[代码]有时候会合并为单个margin,这种现象叫margin合并。 margin合并发生两个重要元素 必须是块级元素 只发生在垂直方向。 margin合并的场景 1.1 相邻兄弟元素 [图片] 1.2 父级和第一个/最后一个子元素 在实际开发中,父子margin合并很有可能会带给我们麻烦。 如下图所示,div表现出和我们预想不一致的结果。 [图片] 那么怎么才能防止这种父子margin合并导致的和预想不一致问题呢? 解决方法如下(这里直接复制了张鑫旭老师书籍《CSS世界》的原话。): (1)对于margin-top合并(满足一个即可): 父元素设置为BFC 设置[代码]border-top[代码]的值(亲测transparent也可以的) 设置[代码]padding-top[代码]的值 父元素和第一个子元素之间添加内联元素 (2)对于margin-bottom合并(满足一个即可): 父元素设置为BFC 设置[代码]border-bottom[代码](transparent也可以的) 设置[代码]padding-bottom[代码] 父元素和最后一个子元素之间添加一个内联元素 父元素设置[代码]height[代码]、[代码]min-height[代码]或者[代码]max-height[代码] 1.3 空块级元素的margin合并 [图片] 2. margin auto 每当说到[代码]margin:auto[代码],我的第一反应是居中。但这个只是一个浅层应用的表象。 接下来我们去一起看看这个[代码]margin:auto[代码]究竟是‘何方神圣’。 [代码]margin:auto[代码]的填充规则如下: 如果一侧定值,一侧auto,则auto为剩余空间大小。注意auto并不是0的意思。 如果两侧都是auto,则平分剩余的空间 我会疑惑为什么我设置了[代码]margin: auto[代码],却在垂直方向上没有居中。 [图片] 这里《css世界》中给出的答案让人非常容易理解。假如把.son元素的height去掉,.son的高度会自动变成父元素的200px,显然不会,所以无法触发margin: auto。同理,如果把width为200px去掉,确实是会和父元素一样宽。 那么如何让垂直居中呢? 子元素使用绝对定位后设置[代码]margin: auto[代码]即可 [图片] Border(边框) 用border绘制三角形 我们可以利用border color为透明来绘制一些图形,比如三角形 [图片] 注意[代码]border-color[代码]这个属性。 [代码]/* border-color: color; 单值语法 */ border-color: red; /* border-color: vertical horizontal; 双值语法*/ border-color: red #f015ca; /* border-color: top horizontal bottom; 三值语法 */ border-color: red yellow green; /* border-color: top right bottom left; 四值语法 */ border-color: red yellow green blue; [代码] 当然,我们绘制三角形不限于这种等腰三角。 [图片] 这里绘制了一个底边分别是60px和160px的直角三角形。 参考 文章主要参考了张鑫旭老师的《css世界》并根据自己的业务做出的一些实践总结。
2019-04-28 - 2019-04-28
- 版本管理的基本使用 git基本能力详解
前言: 看完此文后, 可以在 github上新建一个 仓库,之后新建一个小程序项目,上传到github的仓库. 一.git基本功能 讲解 拉取: 获得服务器 指定分支代码 到本地的 head分支(当前分支) 抓取:获取服务器分支的 最新修改,不会合并入本地 推送:将本地分支 推送到服务器指定分支 分支: 基于某个本地分支 创建 新分支 合并:将本地的两个分支 进行合并 用于代码提交 就是讲 某个分支 合并到 当前分支(head标识的分支) 分支理解:每个人开发 的 都是自己的 分支,可以是远程的分支,也可以是本地的分支.本地开发完 合并到 主分支就行了. 二.现有项目代码上传到git. 1.打开现有项目. 2.点击版本管理 ->设置->远程->添加 添加你的git地址. 3.抓取->抓取全部 4.点击远程分支-> 查看嵌入记录 ->右键 从提交 新建分支.分支名称你随意 如果 没有 记录 就可以自己新建一个 分支. 5.之后你就可以推送了. [图片] [图片] 三.推荐使用分支的 标准 每个项目做一个远程dev分支,用于主开发分支.代码同步 开发人员获取 远程dev分支,新建本地dev分支. 基于本地dev分支 新建 本地 开发者 分支. 代码获取: 在本地dev分支 获取远程dev分支的变更,之后 merge 本地dev到 本地开发人员的 本地分支上. 代码嵌入,合并:在本地开发人员分支 变更后,嵌入本地开发人员分支. 之后 切换到本地dev分支,在本地dev分支上 拉取远程dev的最新分支代码. 5.1 将开发人员分支 合并到 本地dev分支,之后推送 到远程dev分支,达到代码嵌入的目的. 四.常见问题. [图片] 这个是你 推送之前 没commit导致的. [图片] 2.这个是 你本地 没有 此分支的 head,你在你想要拉取的 远程分支 的 最新嵌入记录上 右键,获取head,就可以了,相当于重置了 head.重置head方法如下: [图片] 3.拉取远程分支可以 点拉取按钮,也可以如下图. [图片] 4.git 仓库的验证方式 还需要你们自己去选择,因为 仓库的 验证 都是 仓库那边决定的. 在 设置->网络和认证 中 设置. [图片]
2019-04-29 - 蓝牙分包写入
前段时间收到一个蓝牙设备,需求是控制板子上面的电机,这对于从未接触过硬件的小白来说无疑是一个挑战,然而我是一个喜欢挑战的蓝人,于是开始了我的研究。 我开始各种搜罗Demo,查看文档及各大论坛。 终于,黄天不负苦心人,我成功的连接上了设备,并且获取到了他的服务及特征值,当然,每台外设可用的服务及特征值都是不一样的,而且有些是不可用的,什么 read、write、notify、indicate 要根据自己的操作需求去看哪个特征值支持。每个服务下面都有不同的特征值,每个特征值下面又分出来几个不同的特征值列表,接下来就是根据自己需求筛选了。 支持列表: read:读取低功耗蓝牙设备的特征值的二进制数据值 write:向低功耗蓝牙设备特征值中写入二进制数据 notify || indicate:启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值 这些都选好后该开始向蓝牙设备写入指令,让他动起来了(想想就有些小激动) 不过根据官方提供的方式转换指令并写入的时候,意外发生了! 我在回调里面打印成功与否的时候,显示成功,但是设备却没人任何反应,于是我又开始找原因(设备通过某个App试过,写入指令后是正常的),又开始了我的搜寻之旅。 之后发现原因是因为我使用的指令转换后超过了20字节,在Api文档中 https://developers.weixin.qq.com/miniprogram/dev/api/wx.writeBLECharacteristicValue.html 标注了建议每次写入不超过20字节,但是也不是强制性的呀,导致我很懵逼,明明返回的是成功。 [图片] 事到如今,只能接着寻找解决方法了,总不能跟他这么耗着呀。接着我的搜寻之旅吧!(废话那么多,重点该来了) 终于,找到种分包的方式,把超过20字节的指令分批发送(当然,没有超过20字节的话,也不影响使用),这边需要注意一下,不能直接连发,需要有一个延迟,然后~完美写入指令,我的小电机动起来了。 整体的流程如下: 打开蓝牙模块 => 搜索蓝牙 => 获取所有已发现的设备 => 连接蓝牙设备 => 获取蓝牙设备的所有服务 => 获取蓝牙设备服务下的所有特征值 => 向蓝牙设备写入指令 => 完成 =>关闭蓝牙模块 行了,不哔哔了。我结合官方提供的Demo修改了一下,添加了一个分包写入,已经打包成代码片段,可以直接使用。 代码片段中服务和特征值这两个地方我写成了固定的,根据自己的需求可以修改下。 [图片] [图片] 核心代码: 延时定时器 格式转换 判断并分包写入 [图片] [图片] 代码片段: https://developers.weixin.qq.com/s/oFJc70mI7o8K 如有不对的地方或者更好的解决方案,还望大佬们及时提出,希望对你们有所帮助。
2019-05-05 - 用 HTM 实现小程序 SVG
写在前面 今天你可以在小程序中使用 Cax 引擎高性能渲染 SVG! SVG 是可缩放矢量图形(Scalable Vector Graphics),基于可扩展标记语言,用于描述二维矢量图形的一种图形格式。它由万维网联盟制定,是一个开放标准。SVG 的优势有很多: SVG 使用 XML 格式定义图形,可通过文本编辑器来创建和修改 SVG 图像可被搜索、索引、脚本化或压缩 SVG 是可伸缩的,且放大图片质量不下降 SVG 图像可在任何的分辨率下被高质量地打印 SVG 可被非常多的工具读取和修改(比如记事本) SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性、可编程星更强 SVG 完全支持 DOM 编程,具有交互性和动态性 而支持上面这些优秀特性的前提是 - 需要支持 SVG 标签。比如在小程序中直接写: [代码]<svg width="300" height="150"> <rect bindtap="tapHandler" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"> </rect> </svg> [代码] 上面定义了 SVG 的结构、样式和点击行为。但是小程序目前不支持 SVG 标签,仅仅支持加载 SVG 之后 作为 background-image 进行展示,如 [代码]background-image: url("data:image/svg+xml.......)[代码],或者 base64 后作为 background-image 的 url。 直接看在小程序种使用案例: [代码]import { html, renderSVG } from '../../cax/cax' Page({ onLoad: function () { renderSVG(html` <svg width="300" height="220"> <rect bindtap="tapHandler" height="110" width="110" style="stroke:#ff0000; fill: #ccccff" transform="translate(100 50) rotate(45 50 50)"> </rect> </svg>`, 'svg-a', this) }, tapHandler: function () { console.log('你点击了 rect') } }) [代码] 其中的 svg-a 对应着 wxml 里 cax-element 的 id: [代码]<view class="container"> <cax-element id="svg-c"></cax-element> </view> [代码] 声明组件依赖 [代码]{ "usingComponents": { "cax-element":"../../cax/index" } } [代码] 小程序中显示效果: [图片] 可以使用 [代码]width[代码],[代码]height[代码],[代码]bounds-x[代码] 和 [代码]bounds-y[代码] 设置绑定事件的范围,比如: [代码]<path width="100" height="100" bounds-x="50" bounds-y="50" /> [代码] 需要注意的是,元素的事件触发的包围盒受自身或者父节点的 transform 影响,所以不是绝对坐标的 rect 触发区域。 再来一个复杂的例子,用 SVG 绘制 Omi 的 logo: [代码]renderSVG(html` <svg width="300" height="220"> <g transform="translate(50,10) scale(0.2 0.2)"> <circle fill="#07C160" cx="512" cy="512" r="512"/> <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/> <circle fill="white" cx="839.36" cy="242.47" r="50"/> </g> </svg>`, 'svg-a', this) [代码] 小程序种显示效果: [图片] 在 omip 和 mps 当中使用 cax 渲染 svg,你可以不用使用 htm。比如在 omip 中实现上面两个例子: [代码] renderSVG( <svg width="300" height="220"> <rect bindtap="tapHandler" height="110" width="110" style="stroke:#ff0000; fill: #ccccff" transform="translate(100 50) rotate(45 50 50)"> </rect> </svg>, 'svg-a', this.$scope) [代码] [代码]renderSVG( <svg width="300" height="220"> <g transform="translate(50,10) scale(0.2 0.2)"> <circle fill="#07C160" cx="512" cy="512" r="512"/> <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/> <circle fill="white" cx="839.36" cy="242.47" r="50"/> </g> </svg>, 'svg-a', this.$scope) [代码] 需要注意的是在 omip 中传递的最后一个参数不是 [代码]this[代码],而是 [代码]this.$scope[代码]。 在 mps 中,更加彻底,你可以单独创建 svg 文件,通过 import 导入。 [代码]//注意这里不能写 test.svg,因为 mps 会把 test.svg 编译成 test.js import testSVG from '../../svg/test' import { renderSVG } from '../../cax/cax' Page({ tapHandler: function(){ this.pause = !this.pause }, onLoad: function () { renderSVG(testSVG, 'svg-a', this) } }) [代码] 比如 test.svg : [代码]<svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg> [代码] 会被 mps 编译成: [代码]const h = (type, props, ...children) => ({ type, props, children }); export default h( "svg", { width: "300", height: "300" }, h("rect", { bindtap: "tapHandler", x: "0", y: "0", height: "110", width: "110", style: "stroke:#ff0000; fill: #0000ff" }) ); [代码] 所以总结一下: 你可以在 mps 中直接使用 import 的 SVG 文件的方式使用 SVG 你可以直接在 omip 中使用 JSX 的使用 SVG 你可以直接在原生小程序当中使用 htm 的方式使用 SVG 这就完了?远没有,看 cax 在小程序中的这个例子: [图片] 详细代码: [代码]renderSVG(html` <svg width="300" height="200"> <path d="M 256,213 C 245,181 206,187 234,262 147,181 169,71.2 233,18 220,56 235,81 283,88 285,78.7 286,69.3 288,60 289,61.3 290,62.7 291,64 291,64 297,63 300,63 303,63 309,64 309,64 310,62.7 311,61.3 312,60 314,69.3 315,78.7 317,88 365,82 380,56 367,18 431,71 453,181 366,262 394,187 356,181 344,213 328,185 309,184 300,284 291,184 272,185 256,213 Z" style="stroke:#ff0000; fill: black"> <animate dur="32s" repeatCount="indefinite" attributeName="d" values="......太长,这里省略 paths........" /> </path> </svg>`, 'svg-c', this) [代码] 再试试著名的 SVG 老虎: [图片] path 太长,就不贴代码了,可以点击这里查看 pasiton 标签 [代码]import { html, renderSVG } from '../../cax/cax' Page({ onLoad: function () { const svg = renderSVG(html` <svg width="200" height="200"> <pasition duration="200" bindtap=${this.changePath} width="100" height="100" from="M28.228,23.986L47.092,5.122c1.172-1.171,1.172-3.071,0-4.242c-1.172-1.172-3.07-1.172-4.242,0L23.986,19.744L5.121,0.88 c-1.172-1.172-3.07-1.172-4.242,0c-1.172,1.171-1.172,3.071,0,4.242l18.865,18.864L0.879,42.85c-1.172,1.171-1.172,3.071,0,4.242 C1.465,47.677,2.233,47.97,3,47.97s1.535-0.293,2.121-0.879l18.865-18.864L42.85,47.091c0.586,0.586,1.354,0.879,2.121,0.879 s1.535-0.293,2.121-0.879c1.172-1.171,1.172-3.071,0-4.242L28.228,23.986z" to="M49.1 23.5H2.1C0.9 23.5 0 24.5 0 25.6s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 24.5 50.3 23.5 49.1 23.5zM49.1 7.8H2.1C0.9 7.8 0 8.8 0 9.9c0 1.1 0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 8.8 50.3 7.8 49.1 7.8zM49.1 39.2H2.1C0.9 39.2 0 40.1 0 41.3s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1S50.3 39.2 49.1 39.2z" from-stroke="red" to-stroke="green" from-fill="blue" to-fill="red" stroke-width="2" /> </svg>`, 'svg-c', this) this.pasitionElement = svg.children[0] }, changePath: function () { this.pasitionElement.toggle() } }) [代码] pasiton 提供了两个 path 和 颜色 相互切换的能力,最常见的场景比如 menu 按钮和 close 按钮点击后 path 的变形。 举个例子,看颜色和 path 同时变化: [图片] 线性运动 这里举一个在 mps 中使用 SVG 的案例: [代码]import { renderSVG, To } from '../../cax/cax' Page({ tapHandler: function(){ this.pause = !this.pause }, onLoad: function () { const svg = renderSVG(html` <svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg>` , 'svg-a', this) const rect = svg.children[0] rect.originX = rect.width/2 rect.originY = rect.height/2 rect.x = svg.stage.width/2 rect.y = svg.stage.height/2 this.pause = false this.interval = setInterval(()=>{ if(!this.pause){ rect.rotation++ svg.stage.update() } },15) }) [代码] 效果如下: [图片] 组合运动 [代码]import { renderSVG, To } from '../../cax/cax' Page({ onLoad: function () { const svg = renderSVG(html` <svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg>` ,'svg-a', this) const rect = svg.children[0] rect.originX = rect.width/2 rect.originY = rect.height rect.x = svg.stage.width/2 rect.y = svg.stage.height/2 var sineInOut = To.easing.sinusoidalInOut To.get(rect) .to().scaleY(0.8, 450, sineInOut).skewX(20, 900, sineInOut) .wait(900) .cycle().start() To.get(rect) .wait(450) .to().scaleY(1, 450, sineInOut) .wait(900) .cycle().start() To.get(rect) .wait(900) .to().scaleY(0.8, 450, sineInOut).skewX(-20, 900, sineInOut) .cycle() .start() To.get(rect) .wait(1350) .to().scaleY(1, 450, sineInOut) .cycle() .start() setInterval(() => { rect.stage.update() }, 16) } }) [代码] 效果如下: [图片] 其他 vscode 安装 lit-html 插件使 htm 的 html[代码]内容[代码] 高亮 还希望小程序 SVG 提供什么功能可以开 issues告诉我们,评估后通过,我们去实现! Cax Github 参考文档
2024-01-04 - request封装
fetch.js [代码]const api = 'www.qq.com' export const Fetch = ({ url = '', data = {}, header = { "content-type": "application/json" }, method = 'GET', api = Api }) => { return new Promise((resolve, reject) => { wx.request({ url: api + url, header: header, method: method, data: data, success: res => { // 成功时的处理 if (res.data.error == 0) { resolve(res.data); } else { reject(res.data); } }, fail: err => { reject(err); } }) }) } [代码] api.js [代码]import { Fetch } from './fetch.js'; export const PostMiniList = data => { return Fetch({ url: '/post/post_mini_list.json', data: data, method: 'POST', header: { 'content-type': 'application/x-www-form-urlencoded' } }) } export const GetMiniList = data => { return Fetch({ url: '/get/get_mini_list.json', data: data }) } [代码] index.js [代码]import { PostMiniList, GetMiniList } from './api.js'; PostMiniList({ a:1, b:2 }).then( res => { // 成功处理 }, err => { // 失败处理 } ) GetMiniList({ a:1, b:2 }).then( res => { // 成功处理 }, err => { // 失败处理 } ) [代码] 把所有api放在api.js里统一管理,利用promise使我们只关注返回的结果
2019-05-06 - 2019-04-17
- 自适应 tabBar 组件 不是底部的哦
https://developers.weixin.qq.com/s/47VZSGmR7Q7w 这是代码片段链接 项目中有好多地方都需要用到 navbar ,一个项目中重复的使用同一段代码感觉很烦人,所以就自己写了一个,适合 2-4 个 tab,【支持多个】多个的话稍微修改一下布局就可以了 ━((′д`)爻(′д`))━!!!-图片传不上去 大家可以 打开代码链接看一下 如果感觉写的还凑合的 帮忙点个赞! 有什么可以改进的也可以 在下方评论
2019-04-17 - 图片实现渐变/透明效果
众所周知,图片等一些盒子都可以利用opacity属性来设置不透明度,但是前两天我朋友忽然给我一个截图,截图效果如下 [图片] 图中红框圈住的位置图片或者说摄像头采集的画面出现了渐变到透明,可以清楚的看到可以看到后面小哥的胳膊,然后问我如何实现这种效果,这下把我难住了(呵 天天给我出难题),我开始在个大论坛开始寻找解决方案; 忽然在前天,日常逛论坛时看到一个文字投影的效果,而后忽然灵机一动就想,能不能变相的实现前两天我想要的那种效果,于是乎赶紧打开编辑器试了下,发现确实可以把我想要的图片或者盒子进行投影并给投影设置上渐变颜色及透明,结果出来了,只不过出来的效果他反了 [图片] 随后利用transform: rotate(180deg);控制他使出倒挂金钩此等功夫,果然不负所望,成功翻转过来 [图片] 但是我想要的只有投影,因为我想要效果目前只能用投影去实现去控制,但是他却本体与投影共同出现了,我不想看到本体,太丑了,怎么办呢,那就给他装个position: absolute; top给他爸爸装个position: relative; overflow: hidden;让他滚出~,结果显而易见,我胜利了; [图片] 我得到了我想要的结果,为了验证结果,我用文字放在他的下方 看看是否透明; [图片] 我真的成功了,哈哈(小开心一会儿),为了再次确认他真是的图片实现了渐变透明,我把渐变的透明度改成了1(也就是不透明) [图片] 事实证明,我真的成功了!!! 吹完牛皮,赶紧附上完成代码: css: [图片] html: [图片] 最终效果图: [图片] 呃…其实核心就是利用投影来完成的-webkit-box-reflect: below 0 linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 1) 100%); https://www.w3cschool.cn/css3/box-reflect.html 当然 肯定有大佬在我之前发现这种实现方式,不过当时我找了很久都没找到实现方式的写法,想了想 就发出来吧,如果有什么不对的地方,或者有其他方式也可以实现同等效果的话 还劳请告知,在下多谢各位大佬了!!!
2019-04-19 - 小程序云函数的高级玩法-路由
一般情况下,一个云函数完成单一的逻辑功能,就是一个类的方法一样,如图: [图片] 但是受限免费用户最多只能使用20个云函数,想要在单一云函数中实现多个复杂的功能就需要通过参数来区别,可读性差,不利于管理。通过路由,尝试将请求归类,一个云函数处理某一类的请求,比如有专门负责处理用户的,或者专门处理支付的云函数。如图: [图片] 为了方便大家试用,腾讯云 Tencent Cloud Base 团队开发了 tcb-router,云函数路由管理库方便大家使用。 基于 koa 风格的小程序·云开发云函数轻量级类路由库,主要用于优化服务端函数处理逻辑 使用 npm install --save tcb-router 云函数端 // 云函数的 index.js const TcbRouter = require(’./router’); exports.main = (event, context) => { const app = new TcbRouter({ event }); [代码]// app.use 表示该中间件会适用于所有的路由 app.use(async (ctx, next) => { ctx.data = {}; await next(); // 执行下一中间件 }); // 路由为数组表示,该中间件适用于 user 和 timer 两个路由 app.router(['user', 'timer'], async (ctx, next) => { ctx.data.company = 'Tencent'; await next(); // 执行下一中间件 }); // 路由为字符串,该中间件只适用于 user 路由 app.router('user', async (ctx, next) => { ctx.data.name = 'heyli'; await next(); // 执行下一中间件 }, async (ctx, next) => { ctx.data.sex = 'male'; await next(); // 执行下一中间件 }, async (ctx) => { ctx.data.city = 'Foshan'; // ctx.body 返回数据到小程序端 ctx.body = { code: 0, data: ctx.data}; }); // 路由为字符串,该中间件只适用于 timer 路由 app.router('timer', async (ctx, next) => { ctx.data.name = 'flytam'; await next(); // 执行下一中间件 }, async (ctx, next) => { ctx.data.sex = await new Promise(resolve => { // 等待500ms,再执行下一中间件 setTimeout(() => { resolve('male'); }, 500); }); await next(); // 执行下一中间件 }, async (ctx)=> { ctx.data.city = 'Taishan'; // ctx.body 返回数据到小程序端 ctx.body = { code: 0, data: ctx.data }; }); return app.serve(); [代码] } tips: 小程序云函数的 node 环境默认支持 async/await 语法,推荐涉及到的异步操作时像 demo 中那样使用 小程序端 // 调用名为 router 的云函数,路由名为 user wx.cloud.callFunction({ // 要调用的云函数名称 name: “router”, // 传递给云函数的参数 data: { $url: “user”, // 要调用的路由的路径,传入准确路径或者通配符* other: “xxx” } }); 完整的实例,请参考我的另一篇博客: 分享使用tcb-router路由开发的云函数短信平台SDK
2019-04-20 - 通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具
登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 async/await、fetch、localStorage、状态管理、GraphQL 等等特性,希望对大家有所帮助。 前端 目录结构 [代码]├── app.js ├── app.json ├── app.wxss ├── common │ └── api │ └── index.js ├── config.js ├── pages │ └── index │ ├── api │ │ └── index.js │ ├── img │ │ ├── btn.png │ │ └── bg.jpg │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── project.config.json ├── store │ ├── action.js │ └── index.js ├── utils │ └── index.js └── vendor ├── event-emitter.js ├── fetch.js ├── fetchql.js ├── http.js ├── promisify.js ├── regenerator.js ├── storage.js └── store.js [代码] 业务代码 app.js [代码]import store from './store/index' const { loginInfo } = store.state App({ store, onLaunch() { // 打开小程序即登陆,无需用户授权可获得 openID if(!loginInfo) store.dispatch('login') }, }) [代码] store/index.js [代码]import Store from '../vendor/store' import localStorage from '../vendor/storage' import actions from './action' const loginInfo = localStorage.getItem('loginInfo') export default new Store({ state: { // 在全局状态中维护登陆信息 loginInfo, }, actions, }) [代码] store/action.js [代码]import regeneratorRuntime from '../vendor/regenerator'; import wx from '../vendor/promisify'; import localStorage from '../vendor/storage' import api from '../common/api/index'; export default { async login({ state }, payload) { const { code } = await wx.loginAsync(); const { authSetting } = await wx.getSettingAsync() // 如果用户曾授权,直接可以拿到 encryptedData const { encryptedData, iv } = authSetting['scope.userInfo'] ? await wx.getUserInfoAsync({ withCredentials: true }) : {}; // 如果用户未曾授权,也可以拿到 openID const { token, userInfo } = await api.login({ code, encryptedData, iv }); // 为接口统一配置 Token getApp().gql.requestObject.headers['Authorization'] = `Bearer ${token}`; // 本地缓存登陆信息 localStorage.setItem('loginInfo', { token, userInfo } ) return { loginInfo: { token, userInfo } } } } [代码] common/api/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' export default { /** * 登录接口 * 如果只有 code,只返回 token,如果有 encryptedData, iv,同时返回用户的昵称和头像 * @param {*} param0 */ async login({ code, encryptedData, iv }) { const query = `query login($code: String!, $encryptedData: String, $iv: String){ login(code:$code, encryptedData:$encryptedData, iv:$iv, appid:$appid){ token userInfo { nickName avatarUrl } } }` const { login: { token, userInfo } } = await getApp().query({ query, variables: { code, encryptedData, iv } }) return { token, userInfo } }, } [代码] pages/index/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' const app = getApp() Page({ data: {}, onLoad(options) { // 将用户登录信息注入到当前页面的 data 中,并且当数据在全局范围内被更新时,都会自动刷新本页面 app.store.mapState(['loginInfo'], this) }, async login({ detail: { errMsg } }) { if (errMsg === 'getUserInfo:fail auth deny') return app.store.dispatch('login') // 继续处理业务 }, }) [代码] pages/index/index.wxml [代码]<view class="container"> <form report-submit="true" bindsubmit="saveFormId"> <button form-type="submit" open-type="getUserInfo" bindgetuserinfo="login">登录</button> </form> </view> [代码] 工具代码 事件处理 vendor/event-emitter.js [代码]const id_Identifier = '__id__'; function randomId() { return Math.random().toString(36).substr(2, 16); } function findIndexById(id) { return this.findIndex(item => item[id_Identifier] === id); } export default class EventEmitter { constructor() { this.events = {} } /** * listen on a event * @param event * @param listener */ on(event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; listener[id_Identifier] = id; container.push(listener); return () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); } }; /** * remove all listen of an event * @param event */ off (event) { this.events[event] = []; }; /** * clear all event listen */ clear () { this.events = {}; }; /** * listen on a event once, if it been trigger, it will cancel the listner * @param event * @param listener */ once (event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; let callback = () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); listener.apply(this, arguments); }; callback[id_Identifier] = id; container.push(callback); }; /** * emit event */ emit () { const { events } = this; const argv = [].slice.call(arguments); const event = argv.shift(); ((events['*'] || []).concat(events[event] || [])).map(listener => self.emitting(event, argv, listener)); }; /** * define emitting * @param event * @param dataArray * @param listener */ emitting (event, dataArray, listener) { listener.apply(this, dataArray); }; } [代码] 封装 wx.request() 接口 vendor/http.js [代码]import EventEmitter from './event-emitter.js'; const DEFAULT_CONFIG = { maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }; class Http extends EventEmitter { constructor(config = DEFAULT_CONFIG) { super(); this.config = config; this.ctx = wx; this.queue = []; this.runningTask = 0; this.maxConcurrent = DEFAULT_CONFIG.maxConcurrent; this.maxConcurrent = config.maxConcurrent; this.requestInterceptor = () => true; this.responseInterceptor = () => true; } create(config = DEFAULT_CONFIG) { return new Http(config); } next() { const queue = this.queue; if (!queue.length || this.runningTask >= this.maxConcurrent) return; const entity = queue.shift(); const config = entity.config; const { requestInterceptor, responseInterceptor } = this; if (requestInterceptor.call(this, config) !== true) { let response = { data: null, errMsg: `Request Interceptor: Request can\'t pass the Interceptor`, statusCode: 0, header: {} }; entity.reject(response); return; } this.emit('request', config); this.runningTask = this.runningTask + 1; let timer = null; let aborted = false; let finished = false; const callBack = { success: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('success', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, fail: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('fail', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, complete: () => { if (aborted) return; this.emit('complete', config, entity.response); this.next(); this.runningTask = this.runningTask - 1; } }; const requestConfig = Object.assign(config, callBack); const task = this.ctx.request(requestConfig); if (this.config.timeout > 0) { timer = setTimeout(() => { if (!finished) { aborted = true; task && task.abort(); this.next(); } }, this.config.timeout); } } request(method, url, data, header, dataType = 'json') { const config = { method, url, data, header: { ...header, ...this.config.header }, dataType: dataType || this.config.dataType }; return new Promise((resolve, reject) => { const entity = { config, resolve, reject, response: null }; this.queue.push(entity); this.next(); }); } head(url, data, header, dataType) { return this.request('HEAD', url, data, header, dataType); } options(url, data, header, dataType) { return this.request('OPTIONS', url, data, header, dataType); } get(url, data, header, dataType) { return this.request('GET', url, data, header, dataType); } post(url, data, header, dataType) { return this.request('POST', url, data, header, dataType); } put(url, data, header, dataType) { return this.request('PUT', url, data, header, dataType); } ['delete'](url, data, header, dataType) { return this.request('DELETE', url, data, header, dataType); } trace(url, data, header, dataType) { return this.request('TRACE', url, data, header, dataType); } connect(url, data, header, dataType) { return this.request('CONNECT', url, data, header, dataType); } setRequestInterceptor(interceptor) { this.requestInterceptor = interceptor; return this; } setResponseInterceptor(interceptor) { this.responseInterceptor = interceptor; return this; } clean() { this.queue = []; } } export default new Http(); [代码] 兼容 fetch 标准 vendor/fetch.js [代码]import http from './http'; const httpClient = http.create({ maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }); function generateResponse(res) { let header = res.header || {}; let config = res.config || {}; return { ok: ((res.statusCode / 200) | 0) === 1, // 200-299 status: res.statusCode, statusText: res.errMsg, url: config.url, clone: () => generateResponse(res), text: () => Promise.resolve( typeof res.data === 'string' ? res.data : JSON.stringify(res.data) ), json: () => { if (typeof res.data === 'object') return Promise.resolve(res.data); let json = {}; try { json = JSON.parse(res.data); } catch (err) { console.error(err); } return json; }, blob: () => Promise.resolve(new Blob([res.data])), headers: { keys: () => Object.keys(header), entries: () => { let all = []; for (let key in header) { if (header.hasOwnProperty(key)) { all.push([key, header[key]]); } } return all; }, get: n => header[n.toLowerCase()], has: n => n.toLowerCase() in header } }; } export default (typeof fetch === 'function' ? fetch.bind() : function(url, options) { options = options || {}; return httpClient .request(options.method || 'get', url, options.body, options.headers) .then(res => Promise.resolve(generateResponse(res))) .catch(res => Promise.reject(generateResponse(res))); }); [代码] GraphQL客户端 vendor/fetchql.js [代码]import fetch from './fetch'; // https://github.com/gucheen/fetchql /** Class to realize fetch interceptors */ class FetchInterceptor { constructor() { this.interceptors = []; /* global fetch */ this.fetch = (...args) => this.interceptorWrapper(fetch, ...args); } /** * add new interceptors * @param {(Object|Object[])} interceptors */ addInterceptors(interceptors) { const removeIndex = []; if (Array.isArray(interceptors)) { interceptors.map((interceptor) => { removeIndex.push(this.interceptors.length); return this.interceptors.push(interceptor); }); } else if (interceptors instanceof Object) { removeIndex.push(this.interceptors.length); this.interceptors.push(interceptors); } this.updateInterceptors(); return () => this.removeInterceptors(removeIndex); } /** * remove interceptors by indexes * @param {number[]} indexes */ removeInterceptors(indexes) { if (Array.isArray(indexes)) { indexes.map(index => this.interceptors.splice(index, 1)); this.updateInterceptors(); } } /** * @private */ updateInterceptors() { this.reversedInterceptors = this.interceptors .reduce((array, interceptor) => [interceptor].concat(array), []); } /** * remove all interceptors */ clearInterceptors() { this.interceptors = []; this.updateInterceptors(); } /** * @private */ interceptorWrapper(fetch, ...args) { let promise = Promise.resolve(args); this.reversedInterceptors.forEach(({ request, requestError }) => { if (request || requestError) { promise = promise.then(() => request(...args), requestError); } }); promise = promise.then(() => fetch(...args)); this.reversedInterceptors.forEach(({ response, responseError }) => { if (response || responseError) { promise = promise.then(response, responseError); } }); return promise; } } /** * GraphQL client with fetch api. * @extends FetchInterceptor */ class FetchQL extends FetchInterceptor { /** * Create a FetchQL instance. * @param {Object} options * @param {String} options.url - the server address of GraphQL * @param {(Object|Object[])=} options.interceptors * @param {{}=} options.headers - request headers * @param {FetchQL~requestQueueChanged=} options.onStart - callback function of a new request queue * @param {FetchQL~requestQueueChanged=} options.onEnd - callback function of request queue finished * @param {Boolean=} options.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) */ constructor({ url, interceptors, headers, onStart, onEnd, omitEmptyVariables = false, requestOptions = {}, }) { super(); this.requestObject = Object.assign( {}, { method: 'POST', headers: Object.assign({}, { Accept: 'application/json', 'Content-Type': 'application/json', }, headers), credentials: 'same-origin', }, requestOptions, ); this.url = url; this.omitEmptyVariables = omitEmptyVariables; // marker for request queue this.requestQueueLength = 0; // using for caching enums' type this.EnumMap = {}; this.callbacks = { onStart, onEnd, }; this.addInterceptors(interceptors); } /** * operate a query * @param {Object} options * @param {String} options.operationName * @param {String} options.query * @param {Object=} options.variables * @param {Object=} options.opts - addition options(will not be passed to server) * @param {Boolean=} options.opts.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) * @returns {Promise} * @memberOf FetchQL */ query({ operationName, query, variables, opts = {}, requestOptions = {}, }) { const options = Object.assign({}, this.requestObject, requestOptions); let vars; if (this.omitEmptyVariables || opts.omitEmptyVariables) { vars = this.doOmitEmptyVariables(variables); } else { vars = variables; } const body = { operationName, query, variables: vars, }; options.body = JSON.stringify(body); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' if (!data) { return reject(errors || [{}]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty) { return reject(errors); } return resolve({ data, errors }); }) )); } /** * get current server address * @returns {String} * @memberOf FetchQL */ getUrl() { return this.url; } /** * setting a new server address * @param {String} url * @memberOf FetchQL */ setUrl(url) { this.url = url; } /** * get information of enum type * @param {String[]} EnumNameList - array of enums' name * @returns {Promise} * @memberOf FetchQL */ getEnumTypes(EnumNameList) { const fullData = {}; // check cache status const unCachedEnumList = EnumNameList.filter((element) => { if (this.EnumMap[element]) { // enum has been cached fullData[element] = this.EnumMap[element]; return false; } return true; }); // immediately return the data if all enums have been cached if (!unCachedEnumList.length) { return new Promise((resolve) => { resolve({ data: fullData }); }); } // build query string for uncached enums const EnumTypeQuery = unCachedEnumList.map(type => ( `${type}: __type(name: "${type}") { ...EnumFragment }` )); const query = ` query { ${EnumTypeQuery.join('\n')} } fragment EnumFragment on __Type { kind description enumValues { name description } }`; const options = Object.assign({}, this.requestObject); options.body = JSON.stringify({ query }); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' and have any errors if (!data) { return reject(errors || [{ message: 'Do not get any data.' }]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty && errors && errors.length) { return reject(errors); } // merge enums' data const passData = Object.assign(fullData, data); // cache new enums' data Object.keys(data).map((key) => { this.EnumMap[key] = data[key]; return key; }); return resolve({ data: passData, errors }); }) )); } /** * calling on a request starting * if the request belong to a new queue, call the 'onStart' method */ onStart() { this.requestQueueLength++; if (this.requestQueueLength > 1 || !this.callbacks.onStart) { return; } this.callbacks.onStart(this.requestQueueLength); } /** * calling on a request ending * if current queue finished, calling the 'onEnd' method */ onEnd() { this.requestQueueLength--; if (this.requestQueueLength || !this.callbacks.onEnd) { return; } this.callbacks.onEnd(this.requestQueueLength); } /** * Callback of requests queue changes.(e.g. new queue or queue finished) * @callback FetchQL~requestQueueChanged * @param {number} queueLength - length of current request queue */ /** * remove empty props(null or '') from object * @param {Object} input * @returns {Object} * @memberOf FetchQL * @private */ doOmitEmptyVariables(input) { const nonEmptyObj = {}; Object.keys(input).map(key => { const value = input[key]; if ((typeof value === 'string' && value.length === 0) || value === null || value === undefined) { return key; } else if (value instanceof Object) { nonEmptyObj[key] = this.doOmitEmptyVariables(value); } else { nonEmptyObj[key] = value; } return key; }); return nonEmptyObj; } } export default FetchQL; [代码] 将wx的异步接口封装成Promise vendor/promisify.js [代码]function promisify(wx) { let wxx = { ...wx }; for (let attr in wxx) { if (!wxx.hasOwnProperty(attr) || typeof wxx[attr] != 'function') continue; // skip over the sync method if (/sync$/i.test(attr)) continue; wxx[attr + 'Async'] = function asyncFunction(argv = {}) { return new Promise(function (resolve, reject) { wxx[attr].call(wxx, { ...argv, ...{ success: res => resolve(res), fail: err => reject(err) } }); }); }; } return wxx; } export default promisify(typeof wx === 'object' ? wx : {}); [代码] localstorage vendor/storage.js [代码]class Storage { constructor(wx) { this.wx = wx; } static get timestamp() { return new Date() / 1000; } static __isExpired(entity) { if (!entity) return true; return Storage.timestamp - (entity.timestamp + entity.expiration) >= 0; } static get __info() { let info = {}; try { info = this.wx.getStorageInfoSync() || info; } catch (err) { console.error(err); } return info; } setItem(key, value, expiration) { const entity = { timestamp: Storage.timestamp, expiration, key, value }; this.wx.setStorageSync(key, JSON.stringify(entity)); return this; } getItem(key) { let entity; try { entity = this.wx.getStorageSync(key); if (entity) { entity = JSON.parse(entity); } else { return null; } } catch (err) { console.error(err); return null; } // 没有设置过期时间, 则直接返回值 if (!entity.expiration) return entity.value; // 已过期 if (Storage.__isExpired(entity)) { this.remove(key); return null; } else { return entity.value; } } removeItem(key) { try { this.wx.removeStorageSync(key); } catch (err) { console.error(err); } return this; } clear() { try { this.wx.clearStorageSync(); } catch (err) { console.error(err); } return this; } get info() { let info = {}; try { info = this.wx.getStorageInfoSync(); } catch (err) { console.error(err); } return info || {}; } get length() { return (this.info.keys || []).length; } } export default new Storage(wx); [代码] 状态管理 vendor/store.js [代码]module.exports = class Store { constructor({ state, actions }) { this.state = state || {} this.actions = actions || {} this.ctxs = [] } // 派发action, 统一返回promise action可以直接返回state dispatch(type, payload) { const update = res => { if (typeof res !== 'object') return this.setState(res) this.ctxs.map(ctx => ctx.setData(res)) return res } if (typeof this.actions[type] !== 'function') return const res = this.actions[type](this, payload) return res.constructor.toString().match(/function\s*([^(]*)/)[1] === 'Promise' ? res.then(update) : new Promise(resolve => resolve(update(res))) } // 修改state的方法 setState(data) { this.state = { ...this.state, ...data } } // 根据keys获取state getState(keys) { return keys.reduce((acc, key) => ({ ...acc, ...{ [key]: this.state[key] } }), {}) } // 映射state到实例中,可在onload或onshow中调用 mapState(keys, ctx) { if (!ctx || typeof ctx.setData !== 'function') return ctx.setData(this.getState(keys)) this.ctxs.push(ctx) } } [代码] 兼容 async/await vendor/regenerator.js [代码]/** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var regeneratorRuntime = (function (exports) { "use strict"; var Op = Object.prototype; var hasOwn = Op.hasOwnProperty; var undefined; // More compressible than void 0. var $Symbol = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = $Symbol.iterator || "@@iterator"; var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; var generator = Object.create(protoGenerator.prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; } exports.wrap = wrap; // Try/catch helper to minimize deoptimizations. Returns a completion // record like context.tryEntries[i].completion. This interface could // have been (and was previously) designed to take a closure to be // invoked without arguments, but in all the cases we care about we // already have an existing method we want to call, so there's no need // to create a new function object. We can even get away with assuming // the method takes exactly one argument, since that happens to be true // in every case, so we don't have to touch the arguments object. The // only additional allocation required is the completion record, which // has a stable shape and so hopefully should be cheap to allocate. function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } var GenStateSuspendedStart = "suspendedStart"; var GenStateSuspendedYield = "suspendedYield"; var GenStateExecuting = "executing"; var GenStateCompleted = "completed"; // Returning this object from the innerFn has the same effect as // breaking out of the dispatch switch statement. var ContinueSentinel = {}; // Dummy constructor functions that we use as the .constructor and // .constructor.prototype properties for functions that return Generator // objects. For full spec compliance, you may wish to configure your // minifier not to mangle the names of these two functions. function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} // This is a polyfill for %IteratorPrototype% for environments that // don't natively support it. var IteratorPrototype = {}; IteratorPrototype[iteratorSymbol] = function () { return this; }; var getProto = Object.getPrototypeOf; var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; } var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; GeneratorFunctionPrototype.constructor = GeneratorFunction; GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; // Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } exports.isGeneratorFunction = function(genFun) { var ctor = typeof genFun === "function" && genFun.constructor; return ctor ? ctor === GeneratorFunction || // For the native GeneratorFunction constructor, the best we can // do is to check its .name property. (ctor.displayName || ctor.name) === "GeneratorFunction" : false; }; exports.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = "GeneratorFunction"; } } genFun.prototype = Object.create(Gp); return genFun; }; // Within the body of any async function, `await x` is transformed to // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test // `hasOwn.call(value, "__await")` to determine if the yielded value is // meant to be awaited. exports.awrap = function(arg) { return { __await: arg }; }; function AsyncIterator(generator) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if (record.type === "throw") { reject(record.arg); } else { var result = record.arg; var value = result.value; if (value && typeof value === "object" && hasOwn.call(value, "__await")) { return Promise.resolve(value.__await).then(function(value) { invoke("next", value, resolve, reject); }, function(err) { invoke("throw", err, resolve, reject); }); } return Promise.resolve(value).then(function(unwrapped) { // When a yielded Promise is resolved, its final value becomes // the .value of the Promise<{value,done}> result for the // current iteration. result.value = unwrapped; resolve(result); }, function(error) { // If a rejected Promise was yielded, throw the rejection back // into the async generator function so it can be handled there. return invoke("throw", error, resolve, reject); }); } } var previousPromise; function enqueue(method, arg) { function callInvokeWithMethodAndArg() { return new Promise(function(resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = // If enqueue has been called before, then we want to wait until // all previous Promises have been resolved before calling invoke, // so that results are always delivered in the correct order. If // enqueue has not been called before, then it is important to // call invoke immediately, without waiting on a callback to fire, // so that the async generator function has the opportunity to do // any necessary setup in a predictable way. This predictability // is why the Promise constructor synchronously invokes its // executor callback, and why async functions synchronously // execute code before the first await. Since we implement simple // async functions in terms of async generators, it is especially // important to get this right, even though it requires care. previousPromise ? previousPromise.then( callInvokeWithMethodAndArg, // Avoid propagating failures to Promises returned by later // invocations of the iterator. callInvokeWithMethodAndArg ) : callInvokeWithMethodAndArg(); } // Define the unified helper method that is used to implement .next, // .throw, and .return (see defineIteratorMethods). this._invoke = enqueue; } defineIteratorMethods(AsyncIterator.prototype); AsyncIterator.prototype[asyncIteratorSymbol] = function () { return this; }; exports.AsyncIterator = AsyncIterator; // Note that simple async functions are implemented on top of // AsyncIterator objects; they just return a Promise for the value of // the final result produced by the iterator. exports.async = function(innerFn, outerFn, self, tryLocsList) { var iter = new AsyncIterator( wrap(innerFn, outerFn, self, tryLocsList) ); return exports.isGeneratorFunction(outerFn) ? iter // If outerFn is a generator, return the full iterator. : iter.next().then(function(result) { return result.done ? result.value : iter.next(); }); }; function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) { if (state === GenStateExecuting) { throw new Error("Generator is already running"); } if (state === GenStateCompleted) { if (method === "throw") { throw arg; } // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); } context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if (context.method === "next") { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === "throw") { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; } context.dispatchException(context.arg); } else if (context.method === "return") { context.abrupt("return", context.arg); } state = GenStateExecuting; var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } else if (record.type === "throw") { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = "throw"; context.arg = record.arg; } } }; } // Call delegate.iterator[context.method](context.arg) and handle the // result, either by returning a { value, done } result from the // delegate iterator, or by modifying context.method and context.arg, // setting context.delegate to null, and returning the ContinueSentinel. function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (method === undefined) { // A .throw or .return when the delegate iterator has no .throw // method always terminates the yield* loop. context.delegate = null; if (context.method === "throw") { // Note: ["return"] must be used for ES3 parsing compatibility. if (delegate.iterator["return"]) { // If the delegate iterator has a return method, give it a // chance to clean up. context.method = "return"; context.arg = undefined; maybeInvokeDelegate(delegate, context); if (context.method === "throw") { // If maybeInvokeDelegate(context) changed context.method from // "return" to "throw", let that override the TypeError below. return ContinueSentinel; } } context.method = "throw"; context.arg = new TypeError( "The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if (record.type === "throw") { context.method = "throw"; context.arg = record.arg; context.delegate = null; return ContinueSentinel; } var info = record.arg; if (! info) { context.method = "throw"; context.arg = new TypeError("iterator result is not an object"); context.delegate = null; return ContinueSentinel; } if (info.done) { // Assign the result of the finished delegate to the temporary // variable specified by delegate.resultName (see delegateYield). context[delegate.resultName] = info.value; // Resume execution at the desired location (see delegateYield). context.next = delegate.nextLoc; // If context.method was "throw" but the delegate handled the // exception, let the outer generator proceed normally. If // context.method was "next", forget context.arg since it has been // "consumed" by the delegate iterator. If context.method was // "return", allow the original .return call to continue in the // outer generator. if (context.method !== "return") { context.method = "next"; context.arg = undefined; } } else { // Re-yield the result returned by the delegate method. return info; } // The delegate iterator is finished, so forget it and continue with // the outer generator. context.delegate = null; return ContinueSentinel; } // Define Generator.prototype.{next,throw,return} in terms of the // unified ._invoke helper method. defineIteratorMethods(Gp); Gp[toStringTagSymbol] = "Generator"; // A Generator should always return itself as the iterator object when the // @@iterator function is called on it. Some browsers' implementations of the // iterator prototype chain incorrectly implement this, causing the Generator // object to not be returned from this call. This ensures that doesn't happen. // See https://github.com/facebook/regenerator/issues/274 for more details. Gp[iteratorSymbol] = function() { return this; }; Gp.toString = function() { return "[object Generator]"; }; function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; if (1 in locs) { entry.catchLoc = locs[1]; } if (2 in locs) { entry.finallyLoc = locs[2]; entry.afterLoc = locs[3]; } this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal"; delete record.arg; entry.completion = record; } function Context(tryLocsList) { // The root entry object (effectively a try statement without a catch // or a finally block) gives us a place to store values thrown from // locations where there is no enclosing try statement. this.tryEntries = [{ tryLoc: "root" }]; tryLocsList.forEach(pushTryEntry, this); this.reset(true); } exports.keys = function(object) { var keys = []; for (var key in object) { keys.push(key); } keys.reverse(); // Rather than returning an object with a next method, we keep // things simple and return the next function itself. return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } // To avoid creating an additional object, we just hang the .value // and .done properties off the next function object itself. This // also ensures that the minifier will not anonymize the function. next.done = true; return next; }; }; function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) { return iteratorMethod.call(iterable); } if (typeof iterable.next === "function") { return iterable; } if (!isNaN(iterable.length)) { var i = -1, next = function next() { while (++i < iterable.length) { if (hasOwn.call(iterable, i)) { next.value = iterable[i]; next.done = false; return next; } } next.value = undefined; next.done = true; return next; }; return next.next = next; } } // Return an iterator with no values. return { next: doneResult }; } exports.values = values; function doneResult() { return { value: undefined, done: true }; } Context.prototype = { constructor: Context, reset: function(skipTempReset) { this.prev = 0; this.next = 0; // Resetting context._sent for legacy support of Babel's // function.sent implementation. this.sent = this._sent = undefined; this.done = false; this.delegate = null; this.method = "next"; this.arg = undefined; this.tryEntries.forEach(resetTryEntry); if (!skipTempReset) { for (var name in this) { // Not sure about the optimal order of these conditions: if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { this[name] = undefined; } } } }, stop: function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; }, dispatchException: function(exception) { if (this.done) { throw exception; } var context = this; function handle(loc, caught) { record.type = "throw"; record.arg = exception; context.next = loc; if (caught) { // If the dispatched exception was caught by a catch block, // then let that catch block handle the exception normally. context.method = "next"; context.arg = undefined; } return !! caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; var record = entry.completion; if (entry.tryLoc === "root") { // Exception thrown outside of any try block that could handle // it, so set the completion value of the entire function to // throw the exception. return handle("end"); } if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"); var hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } else if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else if (hasCatch) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } } else if (hasFinally) { if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else { throw new Error("try statement without catch or finally"); } } } }, abrupt: function(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { // Ignore the finally entry if control is not jumping to a // location outside the try/catch block. finallyEntry = null; } var record = finallyEntry ? finallyEntry.completion : {}; record.type = type; record.arg = arg; if (finallyEntry) { this.method = "next"; this.next = finallyEntry.finallyLoc; return ContinueSentinel; } return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "throw") { throw record.arg; } if (record.type === "break" || record.type === "continue") { this.next = record.arg; } else if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } else if (record.type === "normal" && afterLoc) { this.next = afterLoc; } return ContinueSentinel; }, finish: function(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) { this.complete(entry.completion, entry.afterLoc); resetTryEntry(entry); return ContinueSentinel; } } }, "catch": function(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if (record.type === "throw") { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } // The context.catch method must only be called with a location // argument that corresponds to a known catch block. throw new Error("illegal catch attempt"); }, delegateYield: function(iterable, resultName, nextLoc) { this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }; if (this.method === "next") { // Deliberately forget the last sent value so that we don't // accidentally pass it on to the delegate. this.arg = undefined; } return ContinueSentinel; } }; // Regardless of whether this script is executing as a CommonJS module // or not, return the runtime object so that we can declare the variable // regeneratorRuntime in the outer scope, which allows this module to be // injected easily by `bin/regenerator --include-runtime script.js`. return exports; }( // If this script is executing as a CommonJS module, use module.exports // as the regeneratorRuntime namespace. Otherwise create a new empty // object. Either way, the resulting object will be used to initialize // the regeneratorRuntime variable at the top of this file. typeof module === "object" ? module.exports : {} )); [代码] 后端 [代码]const typeDefs = gql` # schema 下面是根类型,约定是 RootQuery 和 RootMutation schema { query: Query } # 定义具体的 Query 的结构 type Query { # 登陆接口 login(code: String!, encryptedData: String, iv: String): Login } type Login { token: String! userInfo: UserInfo } type UserInfo { nickName: String gender: String avatarUrl: String } `; const resolvers = { Query: { async login(parent, { code, encryptedData, iv }) { const { sessionKey, openId, unionId } = await wxService.code2Session(code); const userInfo = encryptedData && iv ? wxService.decryptData(sessionKey, encryptedData, iv) : { openId, unionId }; if (userInfo.nickName) { userService.createOrUpdateWxUser(userInfo); } const token = await userService.generateJwtToken(userInfo); return { token, userInfo }; }, }, }; [代码]
2019-04-21 - 微信小程序之自定义模态弹窗(带动画)实例
1、基本需求。 实现用户自定义弹框 带动画(动画可做参靠,个人要是觉得不好看可以自定义动画) 获取弹出框的内容,自定义事件获取 2、案例目录结构 二、程序实现具体步骤 1.弹框index.wxml代码 [图片] <!–button–> <view class=“btn” bindtap=“powerDrawer” data-statu=“open”>来点我呀</view> <!–mask–> <view class=“drawer_screen” bindtap=“powerDrawer” data-statu=“close” wx:if="{{showModalStatus}}"></view> <!–content–> <!–使用animation属性指定需要执行的动画–> <view animation="{{animationData}}" class=“drawer_box” wx:if="{{showModalStatus}}"> <!–drawer content–> <view class=“drawer_title”>弹窗标题</view> <view class=“drawer_content”> <view class=“top grid”> <label class=“title col-0”>标题</label> <input class=“input_base input_h30 col-1” name=“rName” value=“可自行定义内容”></input> </view> <view class=“top grid”> <label class=“title col-0”>标题</label> <input class=“input_base input_h30 col-1” name=“mobile” value=“110”></input> </view> <view class=“top grid”> <label class=“title col-0”>标题</label> <input class=“input_base input_h30 col-1” name=“phone” value=“拒绝伸手党”></input> </view> <view class=“top grid”> <label class=“title col-0”>标题</label> <input class=“input_base input_h30 col-1” name=“Email” value=“仅供学习使用”></input> </view> <view class=“top bottom grid”> <label class=“title col-0”>备注</label> <input class=“input_base input_h30 col-1” name=“bz”></input> </view> </view> <view class=“btn_ok” bindtap=“powerDrawer” data-statu=“close”>确定</view> </view> 我这边把效果图放出来吧,需要的,自己看下面地址:http://wxapp.662p.com/thread-3713-1-1.html 这是效果图: [图片]
2019-04-12 - 2020-08-04
- 微信小程序非跳转式组件授权登录
微信小程序非跳转式组件授权登录 首先附上官方文档地址和授权流程 官方地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 流程图: [图片] 大致逻辑:授权 -> 发送code到服务器获取session_key - > 保存在小程序缓存内 -> 调用wx.getUserInfo和session_key获取用户信息 -> 登录成功返回访问token -> 记录登录状态 -> 执行登录成功监听(失败则不监听) 直接上代码,一下均为小程序组件模式有兴趣的可以看下官方文档 创建components(自定义名称)文件夹pages文件夹同级主要放置组件文件 创建 authorize (自定义名称)文件夹 还是一样的创建 对应的authorize.js ,authorize.wxml .authorize.wxss,authorize.json特别注意这里的 authorize.json 文件里面要定义当前页面为组件 [代码]{ "component": true } [代码] 到这里准备工作完成 authorize.js 换成组件的写法,具体参考小程序官方文档,这里展示我定义的 [代码]Component({ //组件的对外属性 说的确实很官方,用过vue组件的就很容易理解这点 //父级向子级传值这里就是接收值得地方 properties:{ //名称要和父级绑定的名称相同 //这里主要是控制自动授权弹框是否显示 true=隐藏 false=显示 iShidden:{ type:Boolean,//定义类型 value: true,//定义默认值 }, //是否自动登录 这里主要用于没有授权是否自动弹出授权提示框 //**用在不自动登录页面但是某些操作需要授权登录** isAuto:{ type: Boolean, value: true, }, }, //组件的内部数据,和 properties 一同用于组件的模板渲染 data:{ cloneIner:null }, //组件所在页面的生命周期声明对象 pageLifetimes:{ //页面隐藏 hide:function(){ //关闭页面时销毁定时器 if(this.data.cloneIner) clearInterval(this.data.clearInterval); }, //页面打开 show:function(){ //打开页面销毁定时器 if (this.data.cloneIner) clearInterval(this.data.clearInterval); }, }, //组件生命周期函数,在组件实例进入页面节点树时执行 attached(){ }, //组件的方法 methods:{ } }); [代码] 注:以下的方法都需写在 methods 内 第一步:未授权用户判断是否执行授权还是直接进行获取用户信息 [代码]//检测登录状态并执行自动登录 setAuthStatus(){ var that = this; that.setErrorCount(); wx.getSetting({ success(res) { //这里会检测是否授权,如果授权了会直接调用自动登录 if (!res.authSetting['scope.userInfo']) { //没有授权不会自动弹出登录框 if (that.data.isAuto === false) return; //自动弹出授权 that.setData({ iShidden: false }); } else { //自动登录 that.setData({ iShidden: true }); if (app.globalData.token) { //这里是授权回调 that.triggerEvent('onLoadFun', app.globalData.token); that.WatchIsLogin(); } else { wx.showLoading({ title: '正在登录中' }); //这里是已授权调用wx.getUserInfo that.getUserInfoBydecryptCode(); } } } }) } [代码] 第二步,没有授权执行打开授权弹出框 [代码] //授权 setUserInfo(e){ var that = this, pdata={}; pdata.userInfo = e.detail.userInfo; pdata.spid = app.globalData.spid; wx.showLoading({ title: '正在登录中' }); wx.login({ success: function (res) { if (!res.code) return app.Tips({ title: '登录失败!' + res.errMsg}); //获取session_key并缓存 that.getSessionKey(res.code, function () { that.getUserInfoBydecryptCode(); }); }, fail() { wx.hideLoading(); } }) }, //从缓存中获取session_key,如果没有则请求服务器再次缓存 getSessionKey(code,successFn,errotFn){ var that=this; wx.checkSession({ success: function (res){ if(wx.getStorageSync('session_key')) successFn && successFn(); else that.setCode(code, successFn, errotFn); }, fail:function(){ that.setCode(code, successFn, errotFn); } }); }, //访问服务器获得session_key 并存入缓存中 setCode(code, successFn, errotFn){ var that = this; app.basePost(app.U({ c: 'Login', a: 'setCode' }), { code: code }, function (res) { wx.setStorageSync('session_key', res.data.session_key); successFn && successFn(res); }, function (res) { if (errotFn) errotFn(res); else return app.Tips({ title: '获取session_key失败' }); }); } [代码] 第三步:执行getUserInfoBydecryptCode 登录获取访问权限 [代码] getUserInfoBydecryptCode: function () { var that = this; var session_key = wx.getStorageSync('session_key') //没有获取到session_key,打开授权页面 //这里必须的判断存在缓存中的session_key是否存在,因为在第一步的时候,判断了 //授权了将自动执行获取用户信息的方法 if (!session_key) { wx.hideLoading(); if(that.data.isAuto) that.setData({ iShidden: false }) return false; }; wx.getUserInfo({ lang: 'zh_CN', success: function (res) { var pdata = res; pdata.userInfo = res.userInfo; pdata.spid = app.globalData.spid;//获取推广人ID pdata.code = app.globalData.code;//获取推广人分享二维码ID if (res.iv) { pdata.iv = encodeURI(res.iv); pdata.encryptedData = res.encryptedData; pdata.session_key = session_key; //获取用户信息生成访问token app.basePost(app.U({ c: 'login', a: 'index' }), { info: pdata},function(res){ if (res.data.status == 0) return app.Tips( { title: '抱歉,您已被禁止登录!' }, { tab: 4, url: '/pages/login-status/login-status' } ); else if(res.data.status==410){ wx.removeStorage({ key:'session_key'}); wx.hideLoading(); if (that.data.iShidden == true) that.setData({ iShidden: false }); return false; } //取消登录提示 wx.hideLoading(); //关闭登录弹出窗口 that.setData({ iShidden: true, ErrorCount:0}); //保存token和记录登录状态 app.globalData.token = res.data.token; app.globalData.isLog = true; //执行登录完成回调 that.triggerEvent('onLoadFun', app.globalData.uid); //监听登录状态 that.WatchIsLogin(); },function(res){ wx.hideLoading(); return app.Tips({title:res.msg}); }); } else { wx.hideLoading(); return app.Tips({ title: '用户信息获取失败!'}); } }, fail: function () { wx.hideLoading(); that.setData({ iShidden: false }); }, }) } [代码] 第四步:监听登录状态 再服务器无法获取到token时,当前页面会一直监听token是否为空,防止无限获取token设置错误次数,终止监听 监听token的用意为:token是服务器返回当前用户的访问凭证,凭证有过期的时候这时候所有的网络请求将无法访问,所以用了一个愚蠢的方法来监听token [代码] //监听登录状态 WatchIsLogin:function(){ this.data.cloneIner=setInterval(function(){ //防止死循环,超过错误次数终止监听 if (this.getErrorCount()) return clearInterval(this.data.clearInterval); if (app.globalData.token == '') this.setAuthStatus(); }.bind(this),800); this.setData({ cloneIner:this.data.cloneIner}); } /** * 处理错误次数,防止死循环 * */ setErrorCount:function(){ if (!this.data.ErrorCount) this.data.ErrorCount=1; else this.data.ErrorCount++; this.setData({ ErrorCount: this.data.ErrorCount}); }, /** * 获取错误次数,是否终止监听 * */ getErrorCount:function(){ return this.data.ErrorCount >= 10 ? true : false; } [代码] 以上就是组件内全部的方法需要在组件生命周期函数内调用第一步的方法检测授权,执行登录 [代码] attached(){ this.setAuthStatus(); } [代码] 注:在网络请求中一定要处理token失效的操作,主要把 app.globalData.token和app.globalData.isLog 设置回空和false 这里附上没有定义的一些app内公用的快捷方法以下的方法最好是写在其他文件里面在app.js里面写一个快捷调用的方法 [代码]/* * post网络请求 * @param string | object 请求地址 * @param object data POST请求数组 * @param callable successCallback 成功执行方法 * @param callable errorCallback 失败执行方法 */ const basePost = function (url, data, successCallback, errorCallback, header) { if (typeof url == 'object') url = U(url); wx.request({ url: url, data: data, dataType : 'json', method: 'POST', header: header, success: function (res) { try{ if (res.data.code == 200) { successCallback && successCallback(res.data); } else { if (res.data.code == 402) getApp().globalData.token = '', getApp().globalData.isLog = false; //返回状态为401时,用户被禁止访问 关闭当前所有页面跳转至用户禁止登录页面 if (res.data.code == 401) return Tips({ title: res.data.msg}, { tab: 4, url:'/pages/login-status/login-status'}); errorCallback && errorCallback(res.data); } } catch (e) { console.log(e); } }, fail: function (res) { errorCallback && errorCallback(res); }, complete: function (res) { } }); } /* * 组装URl *@param object opt */ const U = function (opt, url) { var m = opt.m || 'routine_two', c = opt.c || 'auth_api', a = opt.a || 'index', q = opt.q || '', p = opt.p || {}, params = '', gets = ''; if (url == undefined) url=getApp().globalData.url; params = Object.keys(p).map(function (key) { return key + '/' + p[key]; }).join('/'); gets = Object.keys(q).map(function (key) { return key + '=' + q[key]; }).join('&'); return url + '/' + m + '/' + c + '/' + a + (params == '' ? '' : '/' + params) +'.html'+ (gets == '' ? '' : '?' + gets); } /* * 组装URl *@param object opt */ const U = function (opt, url) { var m = opt.m || 'routine_two', c = opt.c || 'auth_api', a = opt.a || 'index', q = opt.q || '', p = opt.p || {}, params = '', gets = ''; if (url == undefined) url=getApp().globalData.url; params = Object.keys(p).map(function (key) { return key + '/' + p[key]; }).join('/'); gets = Object.keys(q).map(function (key) { return key + '=' + q[key]; }).join('&'); return url + '/' + m + '/' + c + '/' + a + (params == '' ? '' : '/' + params) +'.html'+ (gets == '' ? '' : '?' + gets); } [代码] 代码量有点多,都是能用到的,望大神们多多指点 本小程序后台框架由 http://github.crmeb.net/u/blue 提供 TP5+EasyWeChat技术支持 如果对微信小程序授权不熟悉的可以用 EasyWeChat,确实好用;不是来吹这个EasyWeChat来了,只是个人觉得好用勿喷
2019-05-07 - 2019-04-01
- 小程序保持长连接小经验
大家都知道,小程序的websocket在切入后台5秒左右,会断开链接,或者长时间无数据收发,也会切断链接。 然而心跳数据并不能完全保证链接的正常, 我在这里说一下我的经验,我初中毕业(真的),代码方面写的不好不要见笑。 首先,我为了知道当前的网络是否断了,使用了一个标志位比如 : var NetworkIsOK = false; 当首次链接打开时比如socket的 onOpen 事件!在这个事件回调中,将NetworkIsOK = true; 同理,如果网络出现错误,在错误的回调中将NetworkIsOK = false; 建立一个发送 发送区数据 暂存站 var SendBuffer =[]; 将socket的send 函数包装一下,比如取名SendToServer(data) 在SendToServer中,首先检查网络的状态,如果状态是正常的,则直接调用socket.send()发送数据, 如果不是正常的,则重新链接服务器,并在onOpen事件中检查 SendBuffer.length是否大于 0 ,如果有存入的缓冲数据,则依次发送掉, 以下是部分实际代码,请忽略我垃圾的编程水平! [代码]var[代码] [代码]app = getApp();[代码][代码]/**[代码][代码] [代码][代码]* 与服务器进行通信的所有操作在此进行[代码][代码] [代码][代码]*/[代码][代码]var[代码] [代码]Server = {};[代码][代码]Server.socket = [代码][代码]null[代码][代码]; [代码][代码]//socket连接句柄[代码][代码]Server.isOK = [代码][代码]false[代码][代码]; [代码][代码]//服务器连接状态处理数组[代码][代码]Server.event = []; [代码][代码]//事件注册处理数组[代码][代码]Server.url = [代码][代码]'wss://********'[代码][代码]; //服务器地址[代码] [代码]Server.SendBuffer=[]; [代码][代码]//数据包发送缓冲[代码][代码]/**[代码][代码] [代码][代码]* 初始化操作[代码][代码] [代码][代码]*/[代码][代码]Server.Init=[代码][代码]function[代码][代码](apps){[代码][代码] [代码][代码]console.log([代码][代码]'hello server'[代码][代码]);[代码][代码] [代码][代码]app = apps;[代码][代码] [代码][代码]console.log(app);[代码][代码] [代码][代码]var[代码] [代码]Timer;[代码][代码] [代码][代码]/**执行服务器连接逻辑 */[代码][代码] [代码][代码]Server.socket = wx.connectSocket({[代码][代码] [代码][代码]url:Server.url[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//132.232.87.229[代码][代码] [代码][代码]Server.socket.onOpen([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server open'[代码][代码]);[代码][代码] [代码][代码]Server.isOK=[代码][代码]true[代码][代码]; [代码][代码]//可以通信了[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已连接服务器 '[代码][代码],[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Timer = setInterval([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]Server.startHeart();[代码][代码] [代码][代码]}, 1000 * 50);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onError([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server error'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接错误'[代码][代码],[代码][代码] [代码][代码]icon:[代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onClose([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server close'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接关闭'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onMessage([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//Server.socketPress.onMessage(res);[代码][代码] [代码][代码]var[代码] [代码]message = JSON.parse(res.data);[代码][代码] [代码][代码]if[代码][代码](!message) [代码][代码]return[代码][代码]; [代码][代码]//空数据[代码][代码] [代码][代码]var[代码] [代码]Operator = message.Operator;[代码][代码] [代码][代码]var[代码] [代码]event=Server.event[Operator];[代码][代码] [代码][代码]if[代码][代码](event){[代码][代码] [代码][代码]event(message); [代码][代码]//实际执行[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]} /**服务器重连**/[代码] [代码]Server.reLink=[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]Timer;[代码][代码] [代码][代码]/**执行服务器连接逻辑 */[代码][代码] [代码][代码]Server.socket = wx.connectSocket({[代码][代码] [代码][代码]url:Server.url[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//132.232.87.229[代码][代码] [代码][代码]Server.socket.onOpen([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server open'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]true[代码][代码]; [代码][代码]//可以通信了[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已连接服务器 '[代码][代码],[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.Login();[代码][代码]//重新注册登入[代码][代码] [代码][代码]Timer = setInterval([代码][代码]function[代码][代码](){[代码][代码] [代码][代码]Server.startHeart();[代码][代码] [代码][代码]},1000*50);[代码][代码] [代码][代码]//检查缓冲区是否有未发送数据[代码][代码] [代码][代码]while[代码] [代码](Server.SendBuffer.length>0){[代码][代码] [代码][代码]var[代码] [代码]data = Server.SendBuffer.pop();[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: data[代码][代码] [代码][代码]});[代码][代码] [代码][代码]console.log([代码][代码]'将缓存中的信息发送'[代码][代码],data)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onError([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server error'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接错误'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onClose([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server close'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接关闭'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onMessage([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//Server.socketPress.onMessage(res);[代码][代码] [代码][代码]var[代码] [代码]message = JSON.parse(res.data);[代码][代码] [代码][代码]if[代码] [代码](!message) [代码][代码]return[代码][代码]; [代码][代码]//空数据[代码][代码] [代码][代码]var[代码] [代码]Operator = message.Operator;[代码][代码] [代码][代码]var[代码] [代码]event = Server.event[Operator];[代码][代码] [代码][代码]if[代码] [代码](event) {[代码][代码] [代码][代码]event(message); [代码][代码]//实际执行[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 启动心跳[代码][代码] [代码][代码]*/[代码][代码]Server.startHeart=[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]if[代码] [代码](Server.isOK == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]Server.reLink(); [代码][代码]//重连[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'Heart'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]};[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]// console.log(JsonData);[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: JsonData[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.addEvent([代码][代码]'HeartOK'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]// console.log('心跳OK',message);[代码][代码] [代码][代码]})[代码][代码]}[代码][代码]/**用户登陆 */[代码][代码]Server.Login=[代码][代码]function[代码][代码](ques){[代码][代码] [代码][代码]var[代码] [代码]userOpenId = wx.getStorageSync([代码][代码]'openId'[代码][代码]);[代码][代码] [代码][代码]console.log(app);[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'userLogin'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]userCode: app.userCode, [代码][代码]//用户code[代码][代码] [代码][代码]userInfo: app.userInfo, [代码][代码]//用户信息[代码][代码] [代码][代码]};[代码][代码] [代码][代码]if[代码] [代码](userOpenId) { [代码][代码]//如果缓存中的有效,就用缓存中的openId发过去给服务器[代码][代码] [代码][代码]sendData.userOpenId = userOpenId;[代码][代码] [代码][代码]console.log([代码][代码]'调用缓存openID'[代码][代码]);[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]console.log([代码][代码]"登陆数据:"[代码][代码], JsonData);[代码][代码] [代码][代码]// console.log(JsonData);[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**如果是黑名单用户,禁止使用 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'BanLogin'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]if[代码] [代码](ques) {[代码][代码] [代码][代码]ques();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 注册消息执行处理函数[代码][代码] [代码][代码]*/[代码][代码]Server.addEvent=[代码][代码]function[代码][代码](eventName,eventHandle){[代码][代码] [代码][代码]Server.event[eventName] = eventHandle;[代码][代码]}[代码][代码]/**注册登入成功处理事件 */[代码][代码]Server.addEvent([代码][代码]'LoginOK'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]console.log([代码][代码]'LOGIN OK'[代码][代码]);[代码][代码] [代码][代码]console.log(message);[代码][代码] [代码][代码]wx.setStorageSync([代码][代码]'openId'[代码][代码],message.openId);[代码][代码]});[代码][代码]/**注册登入失败处理事件 */[代码][代码]Server.addEvent([代码][代码]'LoginError'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]content: [代码][代码]'登入失败了!部分功能可能无法使用,可能是网络原因,也可能是没有获得授权'[代码][代码],[代码][代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]//console.log('用户点确定');[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]});[代码][代码]/**[代码][代码] [代码][代码]* 创建帖子[代码][代码] [代码][代码]*/[代码][代码]Server.CreateInvitation=[代码][代码]function[代码][代码](table,image,text,isRichText){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'CreateInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]isRichText:isRichText,[代码][代码]//是否为富文本[代码][代码] [代码][代码]Table:table,[代码][代码]//标题[代码][代码] [代码][代码]Image:image,[代码][代码]//图片地址[代码][代码] [代码][代码]Text:text,[代码][代码]//文本内容[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 从服务器获取贴子[代码][代码] [代码][代码]*/[代码][代码]Server.GetInvitation=[代码][代码]function[代码][代码](id,mode,time,limt,skip,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'GetInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]mode:mode, [代码][代码]//操作模式[代码][代码] [代码][代码]time:time, [代码][代码]//时间查询时使用的时间[代码][代码] [代码][代码]limt:limt, [代码][代码]//分页查询时需要获取的贴子数量[代码][代码] [代码][代码]skip:skip, [代码][代码]//需要跳过的帖子数量[代码][代码] [代码][代码]};[代码][代码] [代码][代码]//var JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'GetInvitationOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'GetInvitationOK'[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]data = message.data; [代码][代码] [代码][代码]var[代码] [代码]serverTime=message.serverTime;[代码][代码] [代码][代码]if[代码][代码](success) success(data,serverTime); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 创建帖子评论[代码][代码] [代码][代码]*/[代码][代码]Server.CreateInvitationComment=[代码][代码]function[代码][代码](_id,text,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'CreateInvitationComment'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:_id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]text:text,[代码][代码]//评论内容[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'CreateInvitationCommentOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'CreateInvitationCommentOK'[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]data = message.data;[代码][代码] [代码][代码]if[代码] [代码](success) success(data); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 给帖子点赞[代码][代码] [代码][代码]*/[代码][代码]Server.LoveInvitation=[代码][代码]function[代码][代码](_id,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'LoveInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:_id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'LoveInvitationOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'LoveInvitationOK'[代码][代码]);[代码][代码] [代码][代码]if[代码] [代码](success) success(); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 向服务器发送数据,数据类型为任意数据,[代码][代码] [代码][代码]* 如果服务器断线,则自动重连服务器,数据被暂存,重连成功后将被发送[代码][代码] [代码][代码]*/[代码][代码]Server.SendData=[代码][代码]function[代码][代码](data){[代码][代码] [代码][代码]if[代码] [代码](Server.isOK == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]Server.reLink(); [代码][代码]//重连[代码][代码] [代码][代码]//将未发送数据存入Buffer[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(data);[代码][代码] [代码][代码]Server.SendBuffer.push(JsonData);[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码]else[代码][代码]{ [代码][代码]//否则直接给服务器发送数据[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(data);[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: JsonData[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码]}[代码][代码]//暴露接口[代码][代码]module.exports.Server = Server;[代码]
2019-03-18 - 2019-03-26
- 2019-03-26
- 2019-03-29
- 2019-03-27
- 2019-03-25
- 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 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] component文件夹是放自定义组件的 pages放页面 public放公共资源如样式表和公共图标 units放各种公共api文件和封装的一些js文件 config.js是配置文件 这么分已经足以满足我的需求,你可以根据自己的项目灵活拆分。 配置文件 我的项目中有个config.js,这个文件是用来配置项目中要用到的一些接口和其它私有字段,我们知道在开发时通常会有测试环境和正式环境,而测试环境跟正式环境的域名可能会不一样,如果不做好配置的话直接写死接口那等到上线的时候一个个改会非常麻烦,所以做好配置是必需的,文件大致如下: [图片] 首先是定义域名,然后在config对象里定义接口名称,getAPI(key)是获取接口方法,最后通过module暴露出去就可以了.引用的时候只要在页面引入 import domain from ‘…/…/config’;,然后wx.request的时候url的获取方式是domain.getAPI(’’) 代码健壮性、容错性 例子 代码的健壮性、容错性也是我们应该要考虑的一点,移动端的项目不像pc端的网络那么稳定,很多时候网络一不稳定就决定我们的项目是否能正常运行,而一个好的项目就一定要有良好的容错性,就是说在网络异常或其它因素导致我们的项目不能运行时程序要有一个友好的反馈,下面是一个网络请求的例子: [图片] 相信多数人请求的方式是这样,包括我以前刚接触小程序的时候也是这样写,这样写不是说不好,而是不太严谨,如果能够正常获取数据那还好,但是一旦请求出现错误那程序可以到此就没法运行下去了,有些比较好的会加上faill失败回调,但也只是请求失败时的判断,在请求成功到获取数据的这段流程内其实是还有一些需要我们判断的,一般我的做法是这样: [图片] 在请求成功后小程序会进行如下判断: 判断是否返回200,是则进行一下步操作,否则抛出错误 判断数据结构是否完整,是则进行一下步操作,否则抛出错误 然后就可以在页面根据情况进行相应的操作了。 定制错误提示码 可以看到上面的截图的错误打印后面会带一个gde0或gde1的英文代码,这个代码是干嘛用的呢,其实是用来报障的,当我们的小程序上线后可能会遇到一些用户发来的报障,一般是通过截图发给我们,之前没有做错误提示码的时候可能只是根据一句错误提示来定位错误,但是很多时候误提示语都是一样的,我们根本不知道是哪里错了,这样一来就不能很快的定位的错误,所以加上这样一个提示码,到时用户一发截图来,我们只要根据这个错误码就能很快的定位错误并解决了,错误提示码建议命名如下: 不宜过长,3个字母左右 唯一性 意义明确 像上面gde表示获取草稿失败,后面加上数字表示是哪一步出错。 模块化 我们组内的大神说过, 模块化的意义在义分治,不在于复用。 之前我以为模块化只是为了可以复用,其实不然,无论模块多么小也是可以模块化,哪怕只是一个简单的样式也一样,并是不为了复用,而是管理起来方便。 很多同学经常将一些公共的样式事js放在app.wxss和app.js里以便调用,这样做其实有一个坏处,就是维护性比较差,如果是比较小的项目还好,项目一大问题就来了。而且项目是会迭代的,不可能总是一个人开发,可能后面会交接给其他人开发,所以会造成的问题就是: app.wxss和app.js里的内容只会越来越多,因为别人不确定哪些是没用的也不敢删,只能往里加东西,造成文件臃肿,不利于维护。 app.wxss和app.js对于每个页面都有效,可读性方面比较差。 所以模块化的意义就出来了,将公共的部分进行模块化统一管理,也便于维护。 样式模块化 公共样式根据上面的目录结构我是放在public里的css里,每个文件命名好说明是哪个部分的模块化,比如下面这个就表示一个按钮的模块化 [图片] 前面说过模块化不在于大小,就算只是一个简单的样式也可以进行模块化,只要在用到的地方import一下就行了,就知道哪里有用到,哪里没有用到,清晰明了。 js模块化 js模块化这里分为两个部分的模块化,一部分是公共js的模块化,另一部分是页面js的模块化即业务与数据的拆分。 公共js模块化 比较常用的公共js有微信登录,弹窗,请求等,一般我是放在units文件夹里,这里经微信弹窗api为例: [图片] 如图是在小程序中经常会用到的弹窗提示,这里进行封装,定义变量,只要在页面中引入就能直接调用了,不用每次都写一大串。比如在请求的时候是这样用的 [图片] toast()就是封装的弹窗api,这样看起来是不是清爽多了! 业务与数据模块化 业务与数据模块化就是指业务和数据分开,互不影响,业务只负责业务,数据只负责数据,可以看到页面会比普通的页面多了一个api.js [图片] 这个文件主要就是用来获取数据的,而index.js主要用来处理数据,这样分工明确,相比以往获取数据和处理数据都在一个页面要好很多,而且我这里获取数据是返回一个promise对象的,也方便处理一些异步操作。 组件化 组件化相信大家都不陌生了,自从小程序支持自定义组件,可以说是大大地提高了开发效率,我们可以将一些公共的部分进行组件化,这部分就不详细介绍,大家可以去看文档。组件化对于我们的项目来说有很大的好处,而且组件化的可移植性强,从一个项目复用到另一个项目基本不需要做什么改动。 总结 这篇文章通过我自己的一些经验来给大家介绍如何优化自己的代码,主要有以下几点 分好项目目录结构 做好接口配置文件 代码健壮性、容错性的处理 定制错误提示码方便定位错误 样式模块化和js模块化 组件化 最后放上项目目录结构的代码片段,大家可以研究一下,有问题一起探讨:https://developers.weixin.qq.com/s/1uVHRDmT7j6l
2019-03-07 - 可能是目前最全的koa源码解析指南
本文将按照以下顺序讲解koa,通过初读到精读的方式,一步一步讲解koa涉及的相关知识。 通过阅读完本文,你将了解以下内容: koa框架核心 类继承在koa中的应用 co的实现原理,是如何将generator转为async函数的 洋葱模型中间件实现原理 koa的统一错误处理机制 委托模式在koa中的应用 一、koa是什么 koa是一个精简的node框架,它主要做了以下事情: 基于node原生req和res为request和response对象赋能,并基于它们封装成一个context对象。 基于async/await(generator)的中间件洋葱模型机制。 koa1和koa2在源码上的区别主要是于对异步中间件的支持方式的不同。 koa1是使用generator、yield)的模式。 koa2使用的是async/await+Promise的模式。下文主要是针对koa2版本源码上的讲解。 二、初读koa源码 如果你看了koa的源码,会发现koa源码其实很简单,共4个文件。 [代码]── lib ├── application.js ├── context.js ├── request.js └── response.js [代码] 这4个文件其实也对应了koa的4个对象: [代码]── lib ├── new Koa() || ctx.app ├── ctx ├── ctx.req || ctx.request └── ctx.res || ctx.response [代码] 下面,我们先初步了解koa的源码内容,读懂它们,可以对koa有一个初步的了解。 2.1 application.js application.js是koa的入口(从koa文件夹下的package.json的main字段(lib/application.js)中可以得知此文件是入口文件),也是核心部分。 下面对核心代码进行了注释。 [代码]/** * 依赖模块,包括但不止于下面的,只列出核心需要关注的内容 */ const response = require('./response'); const compose = require('koa-compose'); const context = require('./context'); const request = require('./request'); const Emitter = require('events'); const convert = require('koa-convert'); /** * 继承Emitter,很重要,说明Application有异步事件的处理能力 */ module.exports = class Application extends Emitter { constructor() { super(); this.middleware = []; // 该数组存放所有通过use函数的引入的中间件函数 this.subdomainOffset = 2; // 需要忽略的域名个数 this.env = process.env.NODE_ENV || 'development'; // 通过context.js、request.js、response.js创建对应的context、request、response。为什么用Object.create下面会讲解 this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } // 创建服务器 listen(...args) { debug('listen'); const server = http.createServer(this.callback()); //this.callback()是需要重点关注的部分,其实对应了http.createServer的参数(req, res)=> {} return server.listen(...args); } /* 通过调用koa应用实例的use函数,形如: app.use(async (ctx, next) => { await next(); }); 来加入中间件 */ use(fn) { if (isGeneratorFunction(fn)) { fn = convert(fn); // 兼容koa1的generator写法,下文会讲解转换原理 } this.middleware.push(fn); // 将传入的函数存放到middleware数组中 return this; } // 返回一个类似(req, res) => {}的函数,该函数会作为参数传递给上文的listen函数中的http.createServer函数,作为请求处理的函数 callback() { // 将所有传入use的函数通过koa-compose组合一下 const fn = compose(this.middleware); const handleRequest = (req, res) => { // 基于req、res封装出更强大的ctx,下文会详细讲解 const ctx = this.createContext(req, res); // 调用app实例上的handleRequest,注意区分本函数handleRequest return this.handleRequest(ctx, fn); }; return handleRequest; } // 处理请求 handleRequest(ctx, fnMiddleware) { // 省略,见下文 } // 基于req、res封装出更强大的ctx createContext(req, res) { // 省略,见下文 } }; [代码] 从上面代码中,我们可以总结出application.js核心其实处理了这4个事情: 1. 启动框架 2. 实现洋葱模型中间件机制 3. 封装高内聚的context 4. 实现异步函数的统一错误处理机制 2.2 context.js [代码]const util = require('util'); const createError = require('http-errors'); const httpAssert = require('http-assert'); const delegate = require('delegates'); const proto = module.exports = { // 省略了一些不甚重要的函数 onerror(err) { // 触发application实例的error事件 this.app.emit('error', err, this); }, }; /* 在application.createContext函数中, 被创建的context对象会挂载基于request.js实现的request对象和基于response.js实现的response对象。 下面2个delegate的作用是让context对象代理request和response的部分属性和方法 */ delegate(proto, 'response') .method('attachment') ... .access('status') ... .getter('writable') ...; delegate(proto, 'request') .method('acceptsLanguages') ... .access('querystring') ... .getter('origin') ...; [代码] 从上面代码中,我们可以总结出context.js核心其实处理了这2个事情: 1. 错误事件处理 2. 代理response对象和request对象的部分属性和方法 2.3 request.js [代码]module.exports = { // 在application.js的createContext函数中,会把node原生的req作为request对象(即request.js封装的对象)的属性 // request对象会基于req封装很多便利的属性和方法 get header() { return this.req.headers; }, set header(val) { this.req.headers = val; }, // 省略了大量类似的工具属性和方法 }; [代码] request对象基于node原生req封装了一系列便利属性和方法,供处理请求时调用。 所以当你访问ctx.request.xxx的时候,实际上是在访问request对象上的赋值器(setter)和取值器(getter)。 2.4 response.js [代码]module.exports = { // 在application.js的createContext函数中,会把node原生的res作为response对象(即response.js封装的对象)的属性 // response对象与request对象类似,基于res封装了一系列便利的属性和方法 get body() { return this._body; }, set body(val) { // 支持string if ('string' == typeof val) { } // 支持buffer if (Buffer.isBuffer(val)) { } // 支持stream if ('function' == typeof val.pipe) { } // 支持json this.remove('Content-Length'); this.type = 'json'; }, } [代码] response对象与request对象类似,就不再赘述。 值得注意的是,返回的body支持Buffer、Stream、String以及最常见的json,如上示例所示。 三、深入理解koa源码 通过上面的阅读,相信对koa有了一个初步认识,但毕竟是走马观花,本着追根问底的学术精神,还需要对大量细节进行揣摩,下文会从初始化、启动应用、处理请求等的角度,来对这过程中比较重要的细节进行讲解及延伸,如果彻底弄懂,会对koa以及ES6、generator、async/await、co、异步中间件等有更深一步的了解。 3.1 初始化 koa实例化: [代码]const Koa = require('koa'); const app = new Koa(); [代码] koa执行源码: [代码]module.exports = class Application extends Emitter { constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); //为什么要使用Object.create? 见下面原因 this.request = Object.create(request); this.response = Object.create(response); if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } } } [代码] 当实例化koa的时候,koa做了以下2件事: 继承Emitter,具备处理异步事件的能力。然而koa是如何处理,现在还不得而知,这里打个问号。 在创建实例过程中,有三个对象作为实例的属性被初始化,分别是context、request、response。还有我们熟悉的存放中间件的数组mddleware。这里需要注意,是使用Object.create(xxx)对this.xxx进行赋值。 Object.create(xxx)作用: 根据xxx创建一个新对象,并且将xxx的属性和方法作为新的对象的proto。 举个例子,代码在demo02: [代码]const a = { name: 'rose', getName: function(){ return 'rose' } }; const b = Object.create(a); console.log('a is ', a); console.log('b is ', b); [代码] 结果如下: [图片] 可以看到,a的属性和方法已经挂载在b的原型(proto)下了。 所以,当执行完 [代码]this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); [代码] 的时候,以context为例,其实是创建一个新对象,使用context对象来提供新创建对象的proto,并且将这个对象赋值给this.context,实现了类继承的作用。为什么不直接用this.context=context呢?我的理解是,这样会导致两者指向同一片内存,而不是实现继承的目的。 3.2 启动应用及处理请求 在实例化koa之后,接下来,使用app.use传入中间件函数, [代码]app.use(async (ctx,next) => { await next(); }); [代码] koa对应执行源码: [代码] use(fn) { if (isGeneratorFunction(fn)) { fn = convert(fn); } this.middleware.push(fn); return this; } [代码] 当我们执行app.use的时候,koa做了这2件事情: 判断是否是generator函数,如果是,使用koa-convert做转换(koa3将不再支持generator)。 所有传入use的方法,会被push到middleware中。 这里做下延伸讲解,如何将generator函数转为类async函数。 如何将generator函数转为类async函数 koa2处于对koa1版本的兼容,中间件函数如果是generator函数的话,会使用koa-convert进行转换为“类async函数”。(不过到第三个版本,该兼容会取消)。 那么究竟是怎么转换的呢? 我们先来想想generator和async有什么区别? 唯一的区别就是async会自动执行,而generator每次都要调用next函数。 所以问题变为,如何让generator自动执行next函数? 回忆一下generator的知识:每次执行generator的next函数时,它会返回一个对象: [代码]{ value: xxx, done: false } [代码] 返回这个对象后,如果能再次执行next,就可以达到自动执行的目的了。 看下面的例子: [代码]function * gen(){ yield new Promise((resolve,reject){ //异步函数1 if(成功){ resolve() }else{ reject(); } }); yield new Promise((resolve,reject){ //异步函数2 if(成功){ resolve() }else{ reject(); } }) } let g = gen(); let ret = g.next(); [代码] 此时ret = { value: Promise实例; done: false};value已经拿到了Promise对象,那就可以自己定义成功/失败的回调函数了。如: [代码]ret.value.then(()=>{ g.next(); }) [代码] 现在就大功告成啦。我们只要找到一个合适的方法让g.next()一直持续下去就可以自动执行了。 所以问题的关键在于yield的value必须是一个Promise。那么我们来看看co是如何把这些都东西都转化为Promise的: [代码]function co(gen) { var ctx = this; // 把上下文转换为当前调用co的对象 var args = slice.call(arguments, 1) // 获取参数 // we wrap everything in a promise to avoid promise chaining, // 不管你的gen是什么,都先用Promise包裹起来 return new Promise(function(resolve, reject) { // 如果gen是函数,则修改gen的this为co中的this对象并执行gen if (typeof gen === 'function') gen = gen.apply(ctx, args); // 因为执行了gen,所以gen现在是一个有next和value的对象,如果gen不存在、或者不是函数则直接返回gen if (!gen || typeof gen.next !== 'function') return resolve(gen); // 执行类似上面示例g.next()的代码 onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); // 执行每一个gen.next() } catch (e) { return reject(e); } next(ret); //把执行得到的返回值传入到next函数中,next函数是自动执行的关键 } function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. */ function next(ret) { // 如果ret.done=true说明迭代已经完毕,返回最后一次迭代的value if (ret.done) return resolve(ret.value); // 无论ret.value是什么,都转换为Promise,并且把上下文指向ctx var value = toPromise.call(ctx, ret.value); // 如果value是一个Promise,则继续在then中调用onFulfilled。相当于从头开始!! if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); } [代码] 请留意上面代码的注释。 从上面代码可以得到这样的结论,co的思想其实就是: 把一个generator封装在一个Promise对象中,然后再这个Promise对象中再次把它的gen.next()也封装出Promise对象,相当于这个子Promise对象完成的时候也重复调用gen.next()。当所有迭代完成时,把父Promise对象resolve掉。这就成了一个类async函数了。 以上就是如何把generator函数转为类async的内容。 好啦,我们继续回来看koa的源码。 当执行完app.use时,服务还没启动,只有当执行到app.listen(3000)时,程序才真正启动。 koa源码: [代码]listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } [代码] 这里使用了node原生http.createServer创建服务器,并把this.callback()作为参数传递进去。可以知道,this.callback()返回的一定是这种形式:(req, res) => {}。继续看下this.callback代码。 [代码]callback() { // compose处理所有中间件函数。洋葱模型实现核心 const fn = compose(this.middleware); // 每次请求执行函数(req, res) => {} const handleRequest = (req, res) => { // 基于req和res封装ctx const ctx = this.createContext(req, res); // 调用handleRequest处理请求 return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // 调用context.js的onerror函数 const onerror = err => ctx.onerror(err); // 处理响应内容 const handleResponse = () => respond(ctx); // 确保一个流在关闭、完成和报错时都会执行响应的回调函数 onFinished(res, onerror); // 中间件执行、统一错误处理机制的关键 return fnMiddleware(ctx).then(handleResponse).catch(onerror); } [代码] 从上面源码可以看到,有这几个细节很关键: 1. compose(this.middleware)做了什么事情(使用了koa-compose包)。 2. 如何实现洋葱式调用的? 3. context是如何处理的?createContext的作用是什么? 4. koa的统一错误处理机制是如何实现的? 下面,来进行一一讲解。 koa-compose和洋葱式调用 先看第一、二个问题。 看看koa-compose的精简源码: [代码]module.exports = compose function compose(middleware) { return function (context, next) { //略 } } [代码] compose函数接收middleware数组作为参数,middleware中每个对象都是async函数,返回一个以context和next作为入参的函数,我们跟源码一样,称其为fnMiddleware。 在外部调用this.handleRequest的最后一行,运行了中间件: [代码]fnMiddleware(ctx).then(handleResponse).catch(onerror); [代码] 我们来看下fnMiddleware究竟是怎么实现的: [代码]function compose (middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } [代码] 解释之前,我们通过一个例子来理解,假设加入了两个中间件。源码在demo01: [代码]const Koa = require('koa'); const app = new Koa(); app.use(async (ctx,next) => { console.log("1-start"); await next(); console.log("1-end"); }); app.use(async (ctx, next) => { console.log("2-start"); await next(); console.log("2-end"); }); app.listen(3000); [代码] 我们逐步执行, 0:fnMiddleware(ctx)运行; 0:执行dispatch(0); 0:进入dispatch函数,下图是各个参数对应的值。 [图片] 从参数的值可以得知,最终会执行这段代码: [代码]return Promise.resolve(fn(context, function next() { return dispatch(i + 1) })) [代码] 此时,fn就是第一个中间件,它是一个async函数,async函数会返回一个Promise对象,Promise.resolve()中若传入一个Promise对象的话,那么Promise.resolve将原封不动地返回这个Promise对象。 0:进入到第一个中间件代码内部,先执行“console.log(“1-start”)” 0:然后执行“await next()”,并开始等待next执行返回 1:进入到next函数后,执行的是dispatch(1),于是老的dispatch(0)压栈,开始从头执行dispatch(1),即把第二个中间件函数交给fn,然后开始执行,这就完成了程序的控制权从第一个中间件到第二个中间件的转移。下图是执行dispatch(1)时函数内变量的值: [图片] 1:进入到第二个中间件代码内部,先执行“console.log(“2-start”)”。然后执行“await next()”并等待next执行返回 2:进入next函数后,主要执行dispatch(2),于是老的dispatch(1)压栈,从头开始执行dispatch(2)。下图是执行dispatch(2)时函数内变量的值: [图片] 所以返回Promise.resolve(),此时第二个中间件的next函数返回了。 2:所以接下来执行“console.log(“2-end”)” 1:由此第二个中间件执行完成,把程序控制权交给第一个中间件。第一个中间件执行“console.log(“1-end”)” 0:终于完成了所有中间件的执行,如果中间没有异常,则返回Promise.resolve(),执行handleResponse回调;如有异常,则返回Promies.reject(err),执行onerror回调。 建议用上面例子进行调试源码来理解,会更加清晰。 至此,回答了上面提到的2个问题: compose(this.middleware)做了什么事情(使用了koa-compose包)。 如何实现洋葱式调用的? 单一context原则 接下来,我们再来看第3个问题context是如何处理的?createContext的作用是什么? context使用node原生的http监听回调函数中的req、res来进一步封装,意味着对于每一个http请求,koa都会创建一个context并共享给所有的全局中间件使用,当所有的中间件执行完后,会将所有的数据统一交给res进行返回。所以,在每个中间件中我们才能取得req的数据进行处理,最后ctx再把要返回的body给res进行返回。 请记住句话:每一个请求都有唯一一个context对象,所有的关于请求和响应的东西都放在其里面。 下面来看context(即ctx)是怎么封装的: [代码]// 单一context原则 createContext(req, res) { const context = Object.create(this.context); // 创建一个对象,使之拥有context的原型方法,后面以此类推 const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.state = {}; return context; } [代码] 本着一个请求一个context的原则,context必须作为一个临时对象存在,所有的东西都必须放进一个对象,因此,从上面源码可以看到,app、req、res属性就此诞生。 请留意以上代码,为什么app、req、res、ctx也存放在了request、和response对象中呢? 使它们同时共享一个app、req、res、ctx,是为了将处理职责进行转移,当用户访问时,只需要ctx就可以获取koa提供的所有数据和方法,而koa会继续将这些职责进行划分,比如request是进一步封装req的,response是进一步封装res的,这样职责得到了分散,降低了耦合度,同时共享所有资源使context具有高内聚的性质,内部元素互相能访问到。 在createContext中,还有这样一行代码: [代码]context.state = {}; [代码] 这里的state是专门负责保存单个请求状态的空对象,可以根据需要来管理内部内容。 异步函数的统一错误处理机制 接下来,我们再来看第四个问题:koa的统一错误处理机制是如何实现的? 回忆一下我们如何在koa中统一处理错误,只需要让koa实例监听onerror事件就可以了。则所有的中间件逻辑错误都会在这里被捕获并处理。如下所示: [代码]app.on('error', err => { log.error('server error', err) }); [代码] 这是怎么做到的呢?核心代码如下(在上面提到的application.js的handleRequest函数中): [代码]handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // application.js也有onerror函数,但这里使用了context的onerror, const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 这里是中间件如果执行出错的话,都能执行到onerror的关键!!! return fnMiddleware(ctx).then(handleResponse).catch(onerror); } [代码] 这里其实会有2个疑问: 1.出错执行的回调函数是context.js的onerror函数,为什么在app上监听onerror事件,就能处理所有中间件的错误呢? 请看下context.js的onerror: [代码]onerror(err) { this.app.emit('error', err, this); } [代码] 这里的this.app是对application的引用,当context.js调用onerror时,其实是触发application实例的error事件 。该事件是基于“Application类继承自EventEmitter”这一事实。 2.如何做到集中处理所有中间件的错误? 我们再来回顾一下洋葱模型的中间件实现源码: [代码]function compose (middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } [代码] 还有外部处理: [代码]// 这里是中间件如果执行出错的话,都能执行到onerror的关键!!! return fnMiddleware(ctx).then(handleResponse).catch(onerror); [代码] 主要涉及这几个知识点: async函数返回一个Promise对象 async函数内部抛出错误,会导致Promise对象变为reject状态。抛出的错误会被catch的回调函数(上面为onerror)捕获到。 await命令后面的Promise对象如果变为reject状态,reject的参数也可以被catch的回调函数(上面为onerror)捕获到。 这样就可以理解为什么koa能实现异步函数的统一错误处理了。 委托模式 最后讲一下koa中使用的设计模式——委托模式。 当我们在使用context对象时,往往会这样使用: ctx.header 获取请求头 ctx.method 获取请求方法 ctx.url 获取请求url 这些对请求参数的获取都得益于context.request的许多属性都被委托在context上了 [代码]delegate(proto, 'request') .method('acceptsLanguages') ... .access('method') ... .getter('URL') .getter('header') ...; [代码] 又比如, ctx.body 设置响应体 ctx.status 设置响应状态码 ctx.redirect() 请求重定向 这些对响应参数的设置都得益于koa中的context.response的许多方法都被委托在context对象上了: [代码]delegate(proto, 'response') .method('redirect') ... .access('status') .access('body') ...; [代码] 至于delegate的使用和源码就不展开了,有兴趣的同学可以看这里 以上就是对koa源码所涉及的所有知识点的解析啦,初次看可能会有点晕,建议结合源码和例子一起多看几次,就会领会到koa框架的简洁和优雅之美所在了~ 参考文章: https://koajs.com https://zhuanlan.zhihu.com/p/34797505 https://zhuanlan.zhihu.com/p/24559011 https://juejin.im/entry/59e747f0f265da431c6f668e https://www.jianshu.com/p/45ec555a6c83 https://juejin.im/post/5b9339136fb9a05d3634ba13 https://www.jianshu.com/p/feb98591a1e5
2019-03-14 - 2019-04-17
- 2021-06-24
- 你不知道的Virtual DOM(一):Virtual Dom介绍
一、前言 目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染效率。那么,什么是 Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的创建过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM。敲单词太累了,下文 Virtual DOM 一律用 VD 表示。 这是 VD 系列文章的开篇,后续还会有更多的文章带你深入了解 VD 的奥秘。 二、VD 是什么 本质上来说,VD 只是一个简单的 JS 对象,并且最少包含 tag、 props和 children三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名(tag)、属性(props)和子元素对象(children)。下面是一个典型的 VD 对象例子: [代码]{ tag: "div", props: {}, children: [ "Hello World", { tag: "ul", props: {}, children: [{ tag: "li", props: { id: 1, class: "li-1" }, children: ["第", 1] }] } ] } [代码] VD 跟 dom 对象有一一对应的关系,上面的 VD 是由以下的 HTML 生成的: [代码]<div> Hello World <ul> <li id="1" class="li-1"> 第1 </li> </ul> </div> [代码] 一个 dom 对象,比如 li,由 tag(li), props({id:1,class:“li-1”})和 children([“第”,1])三个属性来描述。 三、为什么需要 VD 借助 VD,可以达到有效减少页面渲染次数的目的,从而提高渲染效率。我们先来看下页面的更新一般会经过几个阶段: [图片] 从上面的例子中,可以看出页面的呈现会分以下 3 个阶段: JS 计算 生成渲染树 绘制页面 这个例子里面,JS 计算用了 691毫秒,生成渲染树 578毫秒,绘制 73毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高。 通过 VD 的比较,我们可以将多个操作合并成一个批量的操作,从而减少 dom 重排的次数,进而缩短了生成渲染树和绘制所花的时间。至于如何基于 VD 更有效率的更新 dom,是一个很有趣的话题,日后有机会将另写一篇文章介绍。 四、如何实现 VD 与真实 DOM 的映射 我们先从如何生成 VD 说起。借助 JSX 编译器,可以将文件中的 HTML 转化成函数的形式,然后再利用这个函数生成 VD。看下面这个例子: [代码]function render() { return ( <div> Hello World <ul> <li id="1" class="li-1"> 第1 </li> </ul> </div> ); } [代码] 这个函数经过 JSX 编译后,会输出下面的内容: [代码]function render() { return h( 'div', null, 'Hello World', h( 'ul', null, h( 'li', { id: '1', 'class': 'li-1' }, '\u7B2C1' ) ) ); } [代码] 这里的 h 是一个函数,可以起任意的名字。这个名字通过 babel 进行配置: [代码]// .babelrc 文件 { "plugins": [ ["transform-react-jsx", { "pragma": "h" // 这里可配置任意的名称 }] ] } [代码] 接下来,我们只需要定义 h 函数,就能构造出 VD: [代码]function flatten(arr) { return [].concat.apply([], arr); } function h(tag, props, ...children) { return { tag, props: props || {}, children: flatten(children) || [] }; } [代码] h 函数会传入三个或以上的参数,前两个参数一个是标签名,一个是属性对象,从第三个参数开始的其它参数都是 children。children 元素有可能是数组的形式,需要将数组解构一层。比如: [代码]function render() { return ( <ul> <li>0</li> { [1, 2, 3].map( i => ( <li>{i}</li> )) } </ul> ); } // JSX 编译后 function render() { return h( 'ul', null, h( 'li', null, '0' ), /* * 需要将下面这个数组解构出来再放到 children 数组中 */ [1, 2, 3].map(i => h( 'li', null, i )) ); } [代码] 继续之前的例子。执行 h 函数后,最终会得到如下的 VD 对象: [代码]{ tag: "div", props: {}, children: [ "Hello World", { tag: "ul", props: {}, children: [{ tag: "li", props: { id: 1, class: "li-1" }, children: ["第", 1] }] } ] } [代码] 下一步,通过遍历 VD 对象,生成真实的 dom [代码]// 创建 dom 元素 function createElement(vdom) { // 如果 vdom 是字符串或者数字类型,则创建文本节点,比如“Hello World” if (typeof vdom === 'string' || typeof vdom === 'number') { return doc.createTextNode(vdom); } const {tag, props, children} = vdom; // 1. 创建元素 const element = doc.createElement(tag); // 2. 属性赋值 setProps(element, props); // 3. 创建子元素 // appendChild 在执行的时候,会检查当前的 this 是不是 dom 对象,因此要 bind 一下 children.map(createElement) .forEach(element.appendChild.bind(element)); return element; } // 属性赋值 function setProps(element, props) { for (let key in props) { element.setAttribute(key, props[key]); } } [代码] createElement函数执行完后,dom元素就创建完并展示到页面上了(页面比较丑,不要介意…)。 [图片] 五、总结 本文介绍了 VD 的基本概念,并讲解了如何利用 JSX 编译 HTML 标签,然后生成 VD,进而创建真实 dom 的过程。下一篇文章将会实现一个简单的 VD Diff 算法,找出 2 个 VD 的差异并将更新的元素映射到 dom 中去。 PS: 想看完整代码见这里: 代码(https://gist.github.com/dickenslian/86c4e266ae5f2134373376133bec9e3d) 参考链接: The Inner Workings Of Virtual DOM (https://medium.com/@rajaraodv/the-inner-workings-of-virtual-dom-666ee7ad47cf) preact源码学习系列之一:JSX解析与DOM渲染 (https://github.com/youngwind/blog/issues/103)
2019-03-04 - 2019-03-07
- 2019-02-26
- WordPress做一个简约有气质的微信小程序开源分享给大家
小程序官网: https://www.weitimes.com 小程序地址 https://www.wpstorm.cn 联系微信: hackdxd WordPress多端开源小程序,这是丸子团队在发布丸子社区小程序之后,首次打造一款免费简洁的开源小程序。 要维持丸子社区小程序的升级,又要开发一款支持微信和 QQ的小程序前端,真的挺不容易的。 但是,我们还是坚持了下来,希望能够帮助到想要基于 WordPress 程序创建小程序的用户。 这次的开源小程序功能不多,界面清新,简洁。 开源小程序功能: 文章资源实时同步 支持视频在线播放 支持海报生成 支持分享朋友圈 支持搜索关键词文章 支持文章点赞 / 收藏 / 评论 支持查看个人点赞 / 收藏 / 评论文章列 没有太复杂的功能,除了简洁还是简洁,除了清新还是清新,重要的是,如此优雅的小程序,加载性能好,适用性强。 小程序演示截图: [图片] [图片] 我们已经把开源小程序发布到了WordPress小程序主题资源平台上,如果你对小程序有兴趣。 可以访问 WordPress小程序主题资源平台下载: https://www.wpstorm.cn 我们已经把这款开源小程序发布到了开源平台上,如果你对这款小程序有兴趣,可以上以下两个平台下载: 地址; 如何下载开源小程序? 复制以上地址在你的浏览器打开,选择下载即可,了解详情( 记得给我们的开源小程序 Star 哦 _)。 GitHub地址:https://github.com/dchijack/Travel-Mini-Program GiTee地址:https://gitee.com/izol/Travel-Mini-Program 丸子WodPress小程序付费版本(丸子社区/丸子资讯) 强大的后台功能以及简化的小程序设置,几乎不需要更改小程序源码,只要在网站后台简单的配置信息,就能拥有一个漂亮又功能强大的社区小程序。 在线体验扫码: wseywt.jpg 基本功能 网站小程序数据实时同步更新; 首页推荐文章展示; 文章/动态/点赞/评论/收藏/转发; 文章海报生成; 评论跟随; 强大的广告功能; 支持文章内容转换语音朗诵, 采用百度语音/讯飞语音技术,支持用户自行选择平台; 社区功能 小程序端发表社区话题,发布文章; 小程序文章链接分享,可自动获取分享链接的内容,支持的分享链接包括:微信公众号、抖音、微博、B站文章内容; 小程序视频链接分享,可自动获取分享视频地址,支持的分享视频包括:微博视频、抖音、VueVideo及特定的腾讯视频链接; 支持用户关注,可以互相关注,随时可以了解你喜欢的人发布的内容; 积分功能:支持阅读、评论、发布及签到积分,可以设定每天最高可增加积分额度; 新增功能 微信小程序扫码登录网站,专属小程序在线扫码 基于位置信息的动态发布及文章推荐,可以让你快速找到附近的人。 丸子社区新版做了整体UI界面优化 圈子 新增了仿微信视频号功能 新增了发文章功能 新增了解析接口调整 新增了支持拼多多带货一键发布 小程序演示截图: wsVr1H.png wsVfN8.png wsVI3Q.png 一款UI设计精美的自媒体小程序目前支持四端两款模板,完全后台一键式配置,直接使用,基本不懂编程的小白都可以使用。 在线体验扫码NO1: wsmp01.jpg 在线体验扫码NO2: wsmn0I.jpg 基础功能 增加自定义文章类型-公众号推文 支持创建焦点幻灯片图文显示 支持腾讯视频 / 微博视频解析 支持积分功能,签到打卡积分 支持多端积分付费阅读文章 支持 QQ 小程序消息模板通知 支持百度小程序消息模板通知 支持微信小程序关联公众号文章 支持网站后台下载小程序 小程序演示模板NO1: wsVTjs.png wsVbBq.png 小程序演示模板NO2: wsVz34.png wsZpv9.png wsZPD1.png
2021-04-03 - 商城类小程序开发全过程(附源码)
商城类小程序是所有类别小程序中开发的热点,但其相对于其他类别的小程序,具有逻辑复杂、功能需求较大等特点,因此,对开发者具有一定的基础要求。 那我们就止步于前了吗? 当然不! 本文主要讲述商城小程序开发的过程,并附上了源码,希望对各个开发者能有所帮助。 #一、明确项目需求 在开发小程序之前,需要明确,商城类小程序有哪些基本功能和需求。 需求如下: [图片] #二、账号注册 后端开发难度太大,可以直接使用支持小程序开发的后端平台Bmob后端云www.bmob.cn,注册账号。 #三、目录结构设计 [图片] #四、设计与开发 详细的代码不做过多解释,请直接查看源码。 强调一下后端平台使用的过程: [图片] #五、效果图 [图片] [图片] #六、源码下载 源码下载链接 (markdown添加不了附件,只能去这个地址下载源码了,下载是免费的) 如有疑问,可加QQ群:586487943 个人QQ:624235922 源码可以发给你 大家多交流,哈哈
2019-02-27