- 店滴AI开源框架1.0正式发布
官方网站:http://www.wayfirer.com/ 交流社区:http://bbs.wayfirer.com/ 开放接口:http://doc.wayfirer.com 框架亮点: 完成百度AI软硬件对接实现集团化管理实现公司辐射多商户模式的权限管理体系实现多模块开发,独立安装,独立开发实现系统接口/模块接口文档自动生成#欢迎使用店滴AI-餐饮APP/小程序 #### 使用 #####页面:下单首页、收货地址管理、订单管理、下单提交、商品检索 #####完美的swagger api稳定,可以在线测试的接口稳定。 #####使用稳定的YII后台框架,自研系统,方便二次开发,欢迎有开发能力的朋友加入进来一起开发模块进行售卖,官方提供技术支持,推广支持,品牌支持 ####官方地址 ### 持续更新下载地址:http://bbs.wayfirer.com/forum.php?mod=forumdisplay&fid=41 ###官方网址:[http://www.wayfirer.com/](http://www.wayfirer.com/ "http://www.wayfirer.com/") ###接口地址:[http://www.wayfirer.com//index.php?r=doc](http://www.wayfirer.com//index.php?r=doc "http://www.wayfirer.com//index.php?r=doc") ###后台代码GIT:[https://gitee.com/wayfiretech_admin/firetech](https://gitee.com/wayfiretech_admin/firetech "https://gitee.com/wayfiretech_admin/firetech") #### 加群连接 ####qq群:麻烦下载的朋友加下qq交流群:[823429313]( https://jq.qq.com/?_wv=1027&k=5cutnyx "823429313"),可以获取后台管理,接口管理系统,代码开源, 欢迎使用。 ####微信公众号: ![店滴AI](http://www.wayfirer.com/attachment/202003/15/4a0aec77-66cf-3354-9b4a-daf2bab58fb5.png "店滴AI") #### 关于我们 店滴AI:基于AI技术的应用开源管理系统,对接AI有关的软件、硬件,提供基于AI技术的整体解决方案。 我们提供开源的AI管理系统源码,欢迎各界朋友使用。
2020-05-13 - 正确使用JavaScript数组
首先,我们可以简单地认为缩进就是代码复杂性的指标(尽管很粗略)。因为缩进越多代表我们的嵌套越多,因此代码就越复杂。今天就拿数组来做具体的例子,来展示以下如何抛弃循环,减少缩进,正确地使用JavaScript数组。 “…a loop is an imperative control structure that’s hard to reuse and difficult to plug in to other operations. In addition, it implies code that’s constantly changing or mutating in response to new iterations.” -Luis Atencio 循环 我们都知道,循环结构就是会无形地提高代码的复杂性。那我们现在看看在JavaScript上的循环是如何工作的。 在JavaScript上至少有四五种循环的方式,其中最基础的就是[代码]while[代码]循环了。讲例子前,先设定一个函数和数组: [代码]// oodlify :: String -> String function oodlify(s) { return s.replace(/[aeiou]/g, 'oodle'); } const input = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 那么,如果我们现在要使用[代码]oodlify[代码]函数操作一下数组里每个元素的话,如果我们使用[代码]while[代码]循环的话,是这样子的: [代码]let i = 0; const len = input.length; let output = []; while (i < len) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); i = i + 1; } [代码] 这里就有许多无谓的,但是又不得不做的工作。比如用[代码]i[代码]这个计数器来记住当前循环的位置,而且需要把[代码]i[代码]初始化成0,每次循环还要加一;比如要拿[代码]i[代码]和数组的长度[代码]len[代码]对比,这样才知道循环到什么时候停止。 这时为了让清晰一点,我们可以使用JavaScript为我们提供的[代码]for[代码]循环: [代码]const len = input.length; let output = []; for (let i = 0; i < len; i = i + 1) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); } [代码] [代码]for[代码]循环的好处就是把与业务代码无关的计数逻辑放在了括号里面了。 对比起[代码]while[代码]循环虽有一定改进,但是也会发生类似忘记给计数器[代码]i[代码]加一而导致死循环的情况。 现在回想一下我们的最初目的:就只是给数组的每一个元素执行一下[代码]oodlify[代码]函数而已。其实我们真的不想关什么计数器。 因此,[代码]ES2015[代码]就为我们提供了一个全新的可以让我们忽略计数器的循环结构- [代码]for...of[代码]循环 : [代码]let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } [代码] 这个方式是不是简单多了!我们可以注意到,计数器和对比语句都没了。 如果我们这就满足的话,我们的目标也算完成了,代码的确是简洁了不少。 但是其实,我们可以对JavaScript的数组再深入挖掘一下,更上一层楼。 Mapping [代码]for...of[代码]循环的确比[代码]for[代码]循环简洁不少,但是我们仍然写了一些不必要的初始化代码,比如[代码]output[代码]数组,以及把每个操作过后的值push进去。 其实我们有办法写得更简单明了一点的。不过,现在我们来放大一下这个问题先: 如果我们有两个数组需要使用[代码]oodlify[代码]函数操作的话呢? [代码]const fellowship = [ 'frodo', 'sam', 'gandalf', 'aragorn', 'boromir', 'legolas', 'gimli', ]; const band = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 很明显,我们就要这样循环两个数组: [代码]let bandoodle = []; for (let item of band) { let newItem = oodlify(item); bandoodle.push(newItem); } let floodleship = []; for (let item of fellowship) { let newItem = oodlify(item); floodleship.push(newItem); } [代码] 这的确可以完成我们的目标,但是这样写得有点累赘。我们可以重构一下以减少重复的代码。因此我们可以创建一个函数: [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } let bandoodle = oodlifyArray(band); let floodleship = oodlifyArray(fellowship); [代码] 这样是不是好看多了。但是问题来了,如果我们要使用其他函数来操作这个数组的话呢? [代码]function izzlify(s) { return s.replace(/[aeiou]+/g, 'izzle'); } [代码] 这时,我们前面创建的[代码]oodlifyArray[代码]函数帮不了我们了。不过如果我们这时创建[代码]izzlifyArray[代码]函数的话,代码不就又有许多重复的部分了吗? [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } function izzlifyArray(input) { let output = []; for (let item of input) { let newItem = izzlify(item); output.push(newItem); } return output; } [代码] 这两个函数是不是及其相似呢。 如果此时我们将其抽象成一个模式的话呢:我们希望传入一个数组和一个函数,然后映射每个数组元素,最后输出一个数组。这个模式就称为[代码]mapping[代码]: [代码]function map(f, a) { let output = []; for (let item of a) { output.push(f(item)); } return output; } [代码] 其实我们并不需要自己手动写[代码]mapping[代码]函数,因为JavaScript提供了内置的[代码]map[代码]函数给我们使用,此时我们的代码是这样的: [代码]let bandoodle = band.map(oodlify); let floodleship = fellowship.map(oodlify); let bandizzle = band.map(izzlify); let fellowshizzle = fellowship.map(izzlify); [代码] Reducing 此时[代码]map[代码]是很方便了,但是并不能覆盖我们所有的循环需要。 如果此时我们需要累计数组中的所有数组呢。我们假设有一个这样的数组: [代码]const heroes = [ {name: 'Hulk', strength: 90000}, {name: 'Spider-Man', strength: 25000}, {name: 'Hawk Eye', strength: 136}, {name: 'Thor', strength: 100000}, {name: 'Black Widow', strength: 136}, {name: 'Vision', strength: 5000}, {name: 'Scarlet Witch', strength: 60}, {name: 'Mystique', strength: 120}, {name: 'Namora', strength: 75000}, ]; [代码] 如果我们要找到[代码]strength[代码]最大的那个的元素的话,使用[代码]for...of[代码]循环是这样的: [代码]let strongest = {strength: 0}; for (hero of heroes) { if (hero.strength > strongest.strength) { strongest = hero; } } [代码] 如果此时我们想累计一下所有的[代码]strength[代码]的话,循环里面就是这样的了: [代码]let combinedStrength = 0; for (hero of heroes) { combinedStrength += hero.strength; } [代码] 这两个例子我们都需要初始化一个变量来配合我们的操作。合并两个例子的话就是这样的: [代码]function greaterStrength(champion, contender) { return (contender.strength > champion.strength) ? contender : champion; } function addStrength(tally, hero) { return tally + hero.strength; } // 例子 1 const initialStrongest = {strength: 0}; let working = initialStrongest; for (hero of heroes) { working = greaterStrength(working, hero); } const strongest = working; // 例子 2 const initialCombinedStrength = 0; working = initialCombinedStrength; for (hero of heroes) { working = addStrength(working, hero); } const combinedStrength = working; [代码] 此时我们可以抽象成这样一个函数: [代码]function reduce(f, initialVal, a) { let working = initialVal; for (item of a) { working = f(working, item); } return working; } [代码] 其实这个方法JavaScript也提供了内置函数,就是[代码]reduce[代码]函数。这时代码是这样的: [代码]const strongestHero = heroes.reduce(greaterStrength, {strength: 0}); const combinedStrength = heroes.reduce(addStrength, 0); [代码] Filtering 前面的[代码]map[代码]函数是将数组的全部元素执行同个操作之后输出一个同样大小的数组; [代码]reduce[代码]则是将数组的全部值执行操作之后,最终输出一个值。 如果此时我们只是需要提取几个元素到一个数组内呢?为了更好得解释,我们来扩充一下之前的例子: [代码]const heroes = [ {name: 'Hulk', strength: 90000, sex: 'm'}, {name: 'Spider-Man', strength: 25000, sex: 'm'}, {name: 'Hawk Eye', strength: 136, sex: 'm'}, {name: 'Thor', strength: 100000, sex: 'm'}, {name: 'Black Widow', strength: 136, sex: 'f'}, {name: 'Vision', strength: 5000, sex: 'm'}, {name: 'Scarlet Witch', strength: 60, sex: 'f'}, {name: 'Mystique', strength: 120, sex: 'f'}, {name: 'Namora', strength: 75000, sex: 'f'}, ]; [代码] 现在假设我们要做的两件事: 找到[代码]sex = f[代码]的元素 找到[代码]strength > 500[代码]的元素 如果使用[代码]for...of[代码]循环的话,是这样的: [代码]let femaleHeroes = []; for (let hero of heroes) { if (hero.sex === 'f') { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (hero.strength >= 500) { superhumans.push(hero); } } [代码] 由于有重复的地方,那么我们就把不同的地方抽取出来: [代码]function isFemaleHero(hero) { return (hero.sex === 'f'); } function isSuperhuman(hero) { return (hero.strength >= 500); } let femaleHeroes = []; for (let hero of heroes) { if (isFemaleHero(hero)) { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (isSuperhuman(hero)) { superhumans.push(hero); } } [代码] 此时就可以抽象成JavaScript内置的[代码]filter[代码]函数: [代码]function filter(predicate, arr) { let working = []; for (let item of arr) { if (predicate(item)) { working = working.concat(item); } } } const femaleHeroes = filter(isFemaleHero, heroes); const superhumans = filter(isSuperhuman, heroes); [代码] Finding [代码]filter[代码]搞定了,那么如果我们只要找到一个元素呢。 的确,我们同样可以使用[代码]filter[代码]函数完成这个目标,比如: [代码]function isBlackWidow(hero) { return (hero.name === 'Black Widow'); } const blackWidow = heroes.filter(isBlackWidow)[0]; [代码] 当然我们也同样会发现,这样的效率并不高。因为[代码]filter[代码]函数会过滤所有的元素,尽管在前面已经找到了应该要找到的元素。因此我们可以写一个这样的查找函数: [代码]function find(predicate, arr) { for (let item of arr) { if (predicate(item)) { return item; } } } const blackWidow = find(isBlackWidow, heroes); [代码] 正如大家所预期那样,JavaScript也同样提供了内置方法[代码]find[代码]给我们,因此我们最终的代码是这样的: [代码]const blackWidow = heroes.find(isBlackWidow); [代码] 总结 这些JavaScript内置的数组函数就是很好的例子,让我们学会了如何去抽象提取共同部分,以创造一个可以复用的函数。 现在我们可以用内置函数完成几乎所有的数组操作。分析一下,我们可以看出每个函数都有以下特点: 摒弃了循环的控制结构,使代码更容易阅读。 通过使用适当的方法名称描述我们正在使用的方法。 减少了处理整个数组的问题,只需要关注我们的业务代码。 在每种情况下,JavaScript的内置函数都已经将问题分解为使用小的纯函数的解决方案。通过学习这几种内置函数能让我们消除几乎所有的循环结构,这是因为我们写的几乎所有循环都是在处理数组或者构建数组或者两者都有。因此使用内置函数不仅让我们在消除循环的同时,也为我们的代码增加了不少地可维护性。 本文翻译自:JavaScript Without Loops
2020-03-11 - 开源社区小程序「玉帛书」后端已经开源了
社区小程序「玉帛书」服务端API 开源地址 基于eggjs开发(eggjs+mysql+redis) [代码]$ cd api $ npm install $ npm run dev [代码] 具体步骤 手动创建mysql数据库:community 在api目录下运行命令: [代码]npx sequelize db:migrate [代码] 打开config目录下的config.default.js进行相关配置 [代码] /** * 微信小程序配置 */ config.miniprogram = { appid: '', secret: '', }; /** * qq小程序配置 */ config.qqminiprogram = { appid: '', secret: '' }; /** * 公众号配置 */ config.yitao = { appid: '', secret: '', }; /**七牛存储配置 */ config.qiniu = { AccessKey: '', SecretKey: '', bucket: '', }; [代码]
2020-03-08 - 小程序中如何实现表情组件
先上效果图(无图无真相) [图片] 1. 第一步准备表情包素材 我这里用的微博的表情包可以点击下面的链接查看具体JSON格式这里不展示 表情包文件weibo-emotions.js 2. 第二步编写表情组件(基于wepy2.0) 如果不会 wepy 可以先去了解下如果你会vue那非常容易上手 首先我们需要把表情包文件weibo-emotions.js中的JSON文件转换成我们需要的格式 [代码]emojis = [ { id: 编号, value: 表情对应的汉字含义 例如:[偷笑], icon: 表情相对图片路径, url: 表情具体图片路径 } ] [代码] 具体转换方法 [代码]function () { const _emojis = {} for (const key in emotions) { if (emotions.hasOwnProperty(key)) { const ele = emotions[key]; for (const item of ele) { _emojis[item.value] = { id: item.id, value: item.value, icon: item.icon.replace('/', '_'), url: weibo_icon_url + item.icon } } } } return _emojis } [代码] 编写组件的html代码 [代码]<template> <div class="emoji" style="height:{{height}}px;" :hidden="hide"> <scroll-view :scroll-y="true" style="height:{{height}}px;"> <div class="icons"> <div class="img" v-for="img in emojis" :key="img.id" @tap.stop="onTap(img.value)"> <img class="icon-image" :src="img.url" :lazy-load="true" /> </div> </div> <div style="height:148rpx;"></div> </scroll-view> <div class="btn-box"> <div class="btn-del" @tap.stop="onDel"> <div class="icon icon-input-del" /> </div> </div> </div> </template> [代码] html代码中的height变量为键盘的高度,通过props传入 编写组件的css代码 [代码].emoji { position: fixed; bottom: 0px; left: 0px; width: 100%; transition: all 0.3s; z-index: 10005; &::after { content: ' '; position: absolute; left: 0; top: 0; right: 0; height: 1px; border-top: 0.4px solid rgba(235, 237, 245, 0.8); color: rgba(235, 237, 245, 0.8); } .icons { display: flex; flex-wrap: wrap; .img { flex-grow: 1; padding: 20rpx; text-align: left; justify-items: flex-start; .icon-image { width: 48rpx; height: 48rpx; } } } scroll-view { background: #f8f8f8; } .btn-box { right: 0rpx; bottom: 0rpx; position: fixed; background: #f8f8f8; padding: 30rpx; .btn-del { background: #ffffff; padding: 20rpx 30rpx; border-radius: 10rpx; .icon { font-size: 48rpx; } } } .icon-loading { height: 100%; display: flex; justify-content: center; align-items: center; } } [代码] 这里是使用less来编写css样式的,flex布局如果你对flex不是很了解可以看看 这篇文章 组件JS代码比较少 [代码]import { weibo_emojis } from '../common/api'; import wepy from '@wepy/core'; wepy.component({ options: { addGlobalClass: true }, props: { height: Number, hide: Boolean }, data: { emojis: weibo_emojis, }, methods: { onTap(val) { this.$emit('emoji', val); }, onDel() { this.$emit('del'); } } }); [代码] 表情组件基本已经编写完成是不是很简单 那么编写好的组件怎么用呢? 其实也很简单 第一步把组件引入到页面 [代码]<config> { "usingComponents": { "emoji-input": "../components/input-emoji", } } </config> [代码] 第二步把组件加入到页面html代码中 [代码]<emoji-input :height="boardheight" @emoji="onInputEmoji" @del="onDelEmoji" :hide="bottom === 0" /> [代码] 第三步编写onInputEmoji,onDelEmoji方法 [代码] /** * 选择表情 */ onInputEmoji(val) { let str = this.content.split(''); str.splice(this.cursor, 0, val); this.content = str.join(''); if (this.cursor === -1) { this.cursor += val.length + 1; } else { this.cursor += val.length; } this.canSend(); }, /** * 删除表情 */ onDelEmoji() { let str = this.content.split(''); const leftStr = this.content.substring(0, this.cursor); const leftLen = leftStr.length; const rightStr = this.content.substring(this.cursor); const left_left_Index = leftStr.lastIndexOf('['); const left_right_Index = leftStr.lastIndexOf(']'); const right_right_Index = rightStr.indexOf(']'); const right_left_Index = rightStr.indexOf('['); if ( left_right_Index === leftLen - 1 && leftLen - left_left_Index <= 8 && left_left_Index > -1 ) { // "111[不简单]|23[33]"left_left_Index=3,left_right_Index=7,leftLen=8 const len = left_right_Index - left_left_Index + 1; str.splice(this.cursor - len, len); this.cursor -= len; } else if ( left_left_Index > -1 && right_right_Index > -1 && left_right_Index < left_left_Index && right_right_Index <= 6 ) { // left_left_Index:4,left_right_Index:3,right_right_Index:1,right_left_Index:2 // "111[666][不简|单]"right_right_Index=1,left_left_Index=3,leftLen=6 let len = right_right_Index + 1 + (leftLen - left_left_Index); if (len <= 10) { str.splice(this.cursor - (leftLen - left_left_Index), len); this.cursor -= leftLen - left_left_Index; } else { str.splice(this.cursor, 1); this.cursor -= 1; } } else { str.splice(this.cursor, 1); this.cursor -= 1; } this.content = str.join(''); }, [代码] 好了基本就完成了一个表情组件的编写和调用 如果你想看完整的代码请点击这里 如果你想体验可以扫下面的二维码自己去体验下 [图片] 下篇 我们写写怎么实现一个简单的富文本编辑器
2020-03-09 - 小打卡小程序自动化构建及发布流程的工程化实践
原文地址及ppt: https://www.yuque.com/jinxuanzheng/gvhmm5/uy4qu9 目录 这次的分享大概分成三个模块, 分别是为什么做这件事,解决过程中的探索与实践,和我个人对于小程序开发的一些思考 为什么要做这件事? [图片] 首先说下为什么做这件事情,左图这个场景是我们之前的测试场景,首先在一个版本群里,@一下相关人员,丢一张二维码在群里,然后再发一个check list 发布阶段 场景描述 然后会发生些什么呢? 研发会问: 测试通过了么?”稍等“ 隔个5分钟后… 研发再问:”好了么““好了”, 但是很尴尬的有可能跳出一个产品b说我这边还有个功能没测完, 既然有产品A,产品B,那么也有可能有测试A,后端B,这就陷入了一个沟通小循环 [图片] 每天在这个小群里,历史记录里频次最高的发言就是就是"测好了么",“过审了么”,“发版了么”,被我们戏称为现实版的“三次握手”协议,也是这个小群里最“暖心”的问候 发布时间统计概况 [图片] 大概统计了下,每次发版的时长在20分钟左右,按照每天3次发布来计算,每天要花1小时的时间用来进行发布,而且发版并不是一版接一版,它是有间隔的,中间还要计算参与发版流程的同学,例如后端,测试,设计,产品等角色工作被“打断”的成本,作为相关方很难有集中的块状时间去做事情 这样会导致大家对发版这件事情有抵触情绪,我记得当时听到最多的一句话就是,“啊,又发版了?” 这样整体迭代是速度快不起来的, 然而作为创业公司来说,快速迭代,快速试错是一个非常重要的能力,这种状态下明显是一件不太能接受的事情 时间花在了哪? [图片] 可以看左图,这是最开始小打卡最开始的一个发布流程,即修改发布环境 -> 点击上传 -> 填写版本号/版本描述 -> 发布体验码测试 -> 提交审核 -> 点击发布 这是一个流程简洁么?在理想状态下是的,然而现实情况往往要更复杂一些 那么复杂在哪里? 第一点就是check关键信息 提审/提测前是一定要去check关键信息的,为什么?因为是修改发布环境,更改的版本信息是人工来进行修改的,不论多么严谨的人都是有出错的可能性(ps: 其实挺不好意思的,我就干过这个事,而且两次) 第二点是信息同步的问题, 从左图可知,我们是有两个需要经过确认环节的,一个是测试环节,一个是审核环节,这里通常会发生什么事呢? 这里可以一句话概括为“产品/运营等其他相关方很难在第一时间获得发布信息,缺乏有效的通知手段”,其实就是信息同步的问题 查询场景 场景描述 [图片] 以为这就结束了么,在日常工作中,“hey咱们上个版本发了哪些需求”这是一个再寻常不过的问题了,大部分情况下我们会很快给出答案 然而会问这个问题人可就多了,它的上游对接的是客服,技术,产品,市场bd等等部门 根据不同的职责属性,和人数,这个询问次数会被乘以个n次 下面是,根据职责划分,可能会询问这个问题的部门,产品,运营,技术,客服…貌似已经覆盖了大部分的公司职位了 可见是一个长期且重复性相当强的沟通行为 很头痛,但是实际上就是缺了一份更新日志或者说是文档的问题 更新日志需求 [图片] 左图是微信小程序的更新日志文档,大概包含了: 版本号更新日期需求列表可以看到每个迭代发布了什么,什么时候发布的一目了然,自然不需要再来进行询问这个动作 但是我们刚起步没有这份文档的时候怎么办呢?一是根据tag标签去查对应的git log,二是手动去记这么一份wiki 两种做法都存在一定问题: 第一种的问题是除当前项目的开发人员,无法准确获取信息,例如产品/运营/客服同学,首先并不能指望每个人都可以使用git,另一个方面源码私密性也是一个问题 第二种的问题在于 完整性的问题,人为记录更多会选择性的记录,例如里程碑式的业务需求信任度问题,当存在不完整的可能性时,就变成了参考而非答案需要付出额外的人力成本(当然还有可以利用commit message直接生成,但是和团队的提交风格相冲) [图片] 很可惜,没有,所以,很痛 总结 减少无效沟通,避免重复性时间损耗,提高团队效能 --从团队效率上来说,我们并不希望“三次握手”事件的发生保障信息同步能力,确保组织内部信息的一致性和即时性 – 作为公司的主要产品迭代信息是一定要透明化的,不同部门之间合作才不会出现偏差提高版本稳定性,杜绝潜在的发布风险 – 这个下一话会讲最后一个是满足一天多次的发布需求 – 如果按照现在的发布效率来看,1天1-2次就是极限了,不能解决沟通成本问题反而是降效解决方案 概况 [图片] 就小打卡目前的解决方案来说,将问题大概分为了3个模块 自动化构建 - 主要负责打包编译,具备发布体验版的能力更新日志 - 提供版本记录,版本快照能力通讯机制 - 主要负责提供有效的通知手段这里面有一个简单的依赖关系,更新日志依赖构建能力提供原始数据集,通讯机制主要功能是同步消息,但是内容依赖更新日志 总体流程图 [图片] 通过自动化构建,发布体验版并上传到git,git再通过webhook能力打到我们自己的服务器,处理log信息,上传版本快照,将处理后的数据存储到mysql,通过邮件服务,通知到各个方向,最后是视图层进行展现 自动化构建 构建流程需要具备哪些能力? [图片] 首先解决构建问题,首先做事情之前我们要先想一个问题,构建流程需要具备哪些能力? 打包编译的能力 - 解决语法转换/条件编译/代码检查等一系列问题,同时可以在这里定义一些全局常量,例如之前遇到的切换环境的问题发布能力 - 提交体验版的通用做法是点击小程序开发工具的右上角“上传按钮”,其实除了这个方式之外微信提供了另外两种提交方式,命令行和http服务,具体情况可以翻下微信文档版本管理的能力 - 主要是帮助我们规划相关的版本号和版本描述,可以类比npm的version manager最后是要保证一定的扩展性,例如小程序目前支持npm构建功能,如果没有流出扩展空间,是需要去改源代码添加的,并且这套构建流程在一定程度上是需要保障多项目适配的[图片] 这个gif是基于我们自己的一个脚手架工具实现的一个发布流程,可以看到这是一个交互式的导航,输入 xdk-cli publish 这个命令后会询问我们 是否发布正式环境设置一个版本号(这里填写版本号有一个增长逻辑,默认阶段版本号 + 1)填写一个版本描述其实就是一个 切换环境 + 填写版本信息的过程,之后便是进入发布环境 具体方案 [图片] 那么输入 xdk-cli publish 到底做了些什么事呢? 读取项目的本地配置文件,包括脚手架配置和版本管理文件,然后弹出询问命令,填写需要确认的相关信息执行发布钩子上传体验版,执行微信提供的上传命令执行发布后钩子上传体验版成功,返回一个回调这里为了保障扩展性,cli只保留了上传和版本管理的功能,预留了两个钩子函数,分别是发布前后执行,针对于项目定制化的一些task都放在了钩子内,例如babel,lint,sass,小程序的npm功能,也包括git commit, git tag 的提交 好奇cli具体实现的可以微信扫下面这个码,是我之前写的一篇关于搭建cli的文章 带来的收益 左图是优化后的流程,可以看到通过 cli , 我们合并了“修改发布环境 + 点击上传 + 填写版本信息三个环境“,通过直接通过交互命令进行发布, 单从流程角度讲,我们的收益是什么 杜绝了切换线上线下环境问题导致的发布错误版本号填写错误的问题成为历史合并了发布环节,整个提交流程只需要在编辑器中进行即可更新日志 关于更新日志,之前背景中有介绍我们的目标是:**帮助非项目组开发人员快速了解每个迭代的更新信息 ** 三个问题 [图片] 上面是我归纳的三个核心问题,在做这件事之前时一定要想清楚的: 哪些字段需要收录怎么看?在哪里看?什么时候更新日志记录关于第一个问题,哪些字段需要收录? 最基本的有:版本号,版本描述,上线时间,需求列表其次是一些需求的开发人员,审核人员,并且该需求关联的Pr 和 Prd是什么,以及当前版本的状态 怎么看?在哪里看?我们是放在自己内部的线上管理平台上,利用表格的形式展示 什么时候更新日志记录?版本状态出现变动的时候,例如提测,提审,发布,回滚的时候 产出的需求 [图片] 上面的三个问题其实也总结出我们要准备要做些什么。分别是 数据采集,数据存储,状态管理,事件钩子,视图层 数据采集很好理解,收集一些字段数据采集完需要存储到一个线上可访问的数据库,方便我们视图层和后续的一些应用事件钩子,什么时候通知我们需要更新数据,变更状态视图层,承载内容的地方这时候发现本地构建已经很难满足这些需求了,需要一台稳定的线上服务器的来处理这些任务,我们这里是用node来搭建 现有流程 [图片] 这个是我们关于更新日志的现有流程,上面这一层整个是node服务 先是通过构建流程上传代码到git, git通过webhook能力,通知到node服务,收到通知后更新服务器本地代码,提取中间的提交信息和版本信息,压缩上传oss是为了做版本快照,最后将准备好的数据插入mysql 视图层可以根据存储好的数据展示相应内容 案例展示 [图片] 这个图是截的我们管理系统的一张图, 从左到右分别是:版本号,描述,状态,发布时间,还有两个功能选项: 修改状态,下载代码包(其实就是做过处理后的快照) 下面是项目包含的信息:需求类型,需求列表,需求列表的单个item里又有开发人员,审核人员,审核时间,关联pr,和关联的prd 可以看到整个版本信息结构是一目了然的 关键点 [图片] 整个流程最关键的点是怎么整合这样一份完整信息,实际上一份信息是通过 git Msg,Pr Msg, 版本信息,和oss存储信息 合并而成的 为什么git Msg和Pr Msg同属git相关的能力要分开说? [图片] 主要是因为生命周期不同, git Msg依赖于tag hook,tag hook的定义是新的版本诞生,主要是从git log中提取相关版本信息,但是统计维度没办法具象到PR节点,得到更详细的数据,例如Pr标题,开发人员等等 pr Msg依赖于Pr hook,每次提交Pr都会记录相关信息并入库 实际上走的是两套不同的hook服务,入库时也分属不同的两张表,取数时根据prId进行表关联, 看下左图是实际的version信息,这里根据log信息提取出pr_id,推入数组,右图可以理解为Pr表,里面有需求相关的详细信息 具体怎么解析commit msg?npm上很多第三方的包,非常方便 通讯机制 再说最后一个通讯模块,做这个东西的意义是为了解决 “产品/运营等其他相关方很难在第一时间获得发布信息,缺乏有效的通知手段” 这一问题 提取关键词 [图片] 这里可以从这句话里可以提出几个关键词,第一时间,获得发布信息,通知手段,这三个关键词也是我们后续要解决的重点方向 [图片] 首先是关键时间节点,因为具备这个特性所以需要确定几个关键的时间节点进行推送,我这里列的是 测试状态,提审状态,发布状态,回滚状态 这四项 其次是内容详情,肯定是需要有质量的版本信息,关于版本信息刚才讲到的更新日志是一个现成的服务,我们这里直接将它作为数据源,提供必要数据 其次是通知方式,这个就多种多样了,邮件,钉钉,微信等等,我们这里因为用的是企业邮箱而且开发成本比较低,直接用 nodemailer 第三方库搭了个邮件服务 [图片] 关于状态流转 分为两步,手动变更 和 自动变更 手动变更 [图片] 目前阶段来说,手变更是主要变更方式,同样也是兜底方式,大概形式可以看右图,提供了各种状态的按钮,每种状态变更都会通知到node服务中的邮件模块,帮助我们进行消息群发 另一种hack方式 [图片] 自动变更算是一个比较hack的操作,本质上是因为缺少小程序公众平台暂时没有提供hook能力,所以自己这边利用google浏览器的一个叫做油猴的插件在网页上加了这样一个钩子 可以看左图点击提交的时候,会触发脚本绑定的自定义事件,这个事件直接调用我们服务商状态流转的接口帮助我们进行修改,不需要再去后台进行变更 非常期待微信团队后续开放这样一个hook能力,帮助我们不依赖本地客户端实现功能闭环 当前的发布流程 [图片] 这是最新的一套流程,首先是各个组内自测,合并pr,通过命令直接上传体验版,并提交审核,同时发送邮件提醒已经审核,各小组收到审核后进行集成测试,最后邮件提醒当前已发布 新的流程每次发版消耗时间控制在5分钟左右,最主要的是他是一个非阻塞的形式,在发布期间完全可以做自己的事情 对开发人员来说需要关注的环节只有,合并pr,本地执行一次命令,最后在微信公众平台操作提审和发布时的扫码工作对其他人来说,在接到邮件时,直接执行对应需要完成的任务即可[图片] 这套新的流程执行之后,这个测试小群里“三次握手”的场景成为了历史,从最暖的小群,变成了最冷的小群 价值提现 [图片] 首先是解决了我们先前提到一些问题,例如: 拒绝任何形式的“三次握手“,沟通成本降低,无论产品/研发更聚焦于业务本身 - 减少无效沟通具有稳定可靠可追溯的版本记录,确保组织内部信息的一致性邮件服务的送达机制,不再需要人力去传递相关信息,且及时性,完整性得到保障节省下来的资源,足以支撑一天发布多次版本的需求也可以帮助团队小伙伴扩展技术边界 - 接触一些不一样的东西 未来的一些规划 [图片] 一些个人的思考 [图片] 之前有不少同学问我:“小程序开发天花板低,对职业成长不利怎么办?”,最早我也有过类似的想法,经历过一些事情后,总结出了下面的一些点: 在任何一个领域做到“精通”都并不容易,提出这个问题之前首先需要看到“天花板”我们并不是在做小程序,而是在“解决问题”,千万不要要自我设限,认为做小程序开发只能做小程序善于探索边界,主动推动并解决问题形成闭环,解决边界问题才是最有价值的事情工作更重要的是学习做事方法,积累方法论,形成自己的思维框架最后一点追求卓越非常重要,我认识的很多非常厉害并且优秀的人都有这么一项特质 什么是追求卓越?就是做完一件事情,都是要总结复盘思考怎么才能做得更好,哪怕当前的资源并不能支持你在现在的阶段去实现,但是一定要确保自己是有延展性的 当能够把上面5点想清楚的时候我觉得对自己的定位应该不仅仅是小程序开发者了,而是一个互联网从业者
2019-12-24 - 基于koa搭建的开箱即用快速开发框架
简介 https://github.com/shaonianla1997/node-koa node-koa 是由Koa搭建的快速开发框架。 node-koa 可以有效的帮助开发者提高 NodeJs 的开发效率。 node-koa 的特点 [代码]node-koa 秉承了koa的宗旨,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 数据持久化框架:Sequelize 数据库:Mysql 鉴权方案:jwt+basic-auth 密码加密:bcryptjs 参数校验:validator(基于lin-validator) 开发者安装后,只需要负责api的开发,大大提高了开发效率。 [代码] node-koa 的目录结构 app api user.js api示例 lib enum.js js中枚举的仿照实现 help.js api调用成功状态下返回的固定模板 models user.js 模型类 services 用于存放相关的sdk工具 validators 校验器 config config.js 一些全局配置项 core db.js数据库的驱动文件 http-exception.js 请求状态模板参数 init.js !! 用于配置项目的初始化参数 整个app的核心类 lin-validator-v2.js 七月(7yue.pro)开发的参数校验框架 用于参数校验 util.js 配合validator使用 不可删除 middlewares auth.js权限校验的中间件 exception.js全局异常的中间件 static 静态资源文件 快速上手 [代码]# clone the project git clone git@github.com:shaonianla1997/node-koa.git # install dependency npm install or yarn # develop node app.js 方便开发者对参数校验以及鉴权中间件的的使用,请参考api下的接口示例。 项目上线请配置pm2进程守护。 [代码]
2019-12-06 - 小程序简单两栏瀑布流效果
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次放入到高度最低的那一栏。 先上代码:https://developers.weixin.qq.com/s/Fgm5s1mz7Wdm 所谓简单,是指只考虑图片,图片之外的其他元素高度固定,不在考虑范围内。 说一下基本的实现思路: 1、加载列表数据 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 我也考虑过在第二步bindload获取到宽高后就直接插入到栏位中,但是会出现小的图片先加载完先出现到页面中,虽然瀑布流不是普通的列表那样的排序,但是也不能小的图片在上面这样太乱顺序,所以就改成了获取宽高先存储,等所有图片加载完成后再往页面上渲染。 来看看实际的代码 不需要渲染到wxml中的数据,我放到了jsData中,主要是两栏的高度和是否在加载数据的标记。 tempPics是第一次加载的数据,临时存放,用于加载图片宽高 columns是两个栏位的实际展示数据 [代码]jsData: { columnsHeight: [0, 0], isLoading: false }, data: { columns: [ [], [] ], tempPics: [] } [代码] 1、加载列表数据 这一步没什么好说的,主要是触发方式,我的代码里是放在页面加载以及拉到页面底部时触发 [代码]onLoad: function() { this.loadData() }, onReachBottom: function() { this.loadData() } [代码] 加载后将列表数据存到tempPics中,用于页面加载获取宽高 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 [代码]<view class="hide"> <image wx:for="{{tempPics}}" src="{{item.pic}}" bindload="loadPic" binderror="loadPicError" data-index="{{index}}" /> </view> [代码] 主要是image组件的bindload来获取实际宽高,这里还增加了binderror,防止出现图片加载出错的时候卡死 [代码]loadPic: function(e) { var that = this, data = that.data, tempPics = data.tempPics, index = e.currentTarget.dataset.index if (tempPics[index]) { //以750为宽度算出相对应的高度 tempPics[index].height = e.detail.height * 750 / e.detail.width tempPics[index].isLoad = true } that.setData({ tempPics: tempPics }, function() { that.finLoadPic() }) } [代码] 获取到宽高后,以750为宽度计算出相对应的高度并存储,然后增加一个加载完成的标记。加载出错后就强制高度为750,这样展示的时候就是一个正方形。 单个图片加载完成并存储后调用finLoadPic方法来判断所有图片是否都加载完成。 遍历列表,只要有一个图片没有加载完成的标记,就判断为没有加载完成。 加载完成后进入下一步。 [代码]finLoadPic: function() { var that = this, data = that.data, tempPics = data.tempPics, length = tempPics.length, fin = true for (var i = 0; i < length; i++) { if (!tempPics[i].isLoad) { fin = false break } } if (fin) { wx.hideLoading() if (that.jsData.isLoading) { that.jsData.isLoading = false that.renderPage() } } } [代码] 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 这里需要再便利一遍列表,根据当前栏位的高度情况,将图片插入到高度底的那一栏,同时把这一栏高度加上当前图片的高度(不是实际高度,是上一步以750为宽度算出来的高度) [代码]renderPage: function() { var that = this, data = that.data, columns = data.columns, tempPics = data.tempPics, length = tempPics.length, columnsHeight = that.jsData.columnsHeight, index = 0 for (var i = 0; i < length; i++) { index = columnsHeight[1] < columnsHeight[0] ? 1 : 0 columns[index].push(tempPics[i]) columnsHeight[index] += tempPics[i].height } that.setData({ columns: columns, tempPics: [] }) that.jsData.columnsHeight = columnsHeight } [代码] 在wxml中展示的时候image组件的mode要使用widthFix,同时wxss中图片的高度和宽度一样,这样加载出错的图片可以正方形展示 11月21日增加: 根据@杨泉的建议,也尝试了使用wx.getImageInfo来获取图片的宽高(具体代码可以参考评论区),代码也精简了很多。但是实际比较下来速度要比用image组件慢,初步推测原因是[代码]wx.getImageInfo[代码]会返回本地路径,多了写本地临时文件的时间 ps:用到瀑布流的地方,最好能后端直接返回图片的宽高,省去小程序端获取宽高的麻烦 再ps:我个人并不建议小程序端使用瀑布流
2020-01-14 - 使用animation实现列表顺序加载动画
[图片] 之前使用纯transition实现动画时, 发现在部分手机上效果不是很好, 会有不流畅掉帧的现象! 现在换animation方法实现, 不知各位是否有什么高见, 大家一起交流交流 代码片段如下 https://developers.weixin.qq.com/s/pEBv6emG7Cdt
2019-11-29 - 微信小程序官方多端脚手架 - Kbone的起步
kbone的基础安装及介绍,这里不再过多介绍,感兴趣的童鞋可以看看官方的 文章 及 gitbook(文档) 什么是Kbone? Kbone 是由 微信小程序 官方团队推出的 H5 及 微信小程序 同构脚手架,可使同一套代码分别运行在 H5 及 微信小程序 平台,极大减少了开发者(前端)的工作量。 为什么是Kbone? 相信一部分童鞋在之前已经了解过同类型的其他脚手架,如 uni-app 、mpvue、trao 等。既然市面上已经有这么多脚手架了,为什么 微信小程序 官方依然会推出 kbone 呢?难道是他们并不知道有其他的脚手架? 答案是否定的,他们当然也知道其他脚手架,也尝试使用过。 编者之前也就一些问题咨询过相关的开发人员,也就一些问题进行过讨论。在之后也进行过一些尝试。得出结论:在 微信小程序 环境中,Kbone 所编译出来的代码,运行效率高于其他脚手架(不愧是你们,微信小程序的官方开发团队,居然还偷偷做了优化)。 不过目前,Kbone 仅支持编译成 H5 及 微信小程序,如果需要上其他平台,如 支付宝小程序、抖音小程序等等,估计还需要等一等(不过,猜测近段时间不会支持吧)。 这里推荐 祺爸💎 的文章《微信小程序转其他端小程序实战》,也是一个不错的解决方案。仅需要将 Kbone 预先编译成 微信小程序,然后再转成其他小程序即可。 当然,如果你说的是APP,那自己想办法吧。 Kbone起步的一些坑 一、 [图片] 这应该是一个残留bug,之前是需要在微信小程序中使用 npm构建 安装一些库的,但是目前来看已经不需要了,删除 miniprogram-element 文件夹即可 二、 [图片] 这也是一个老问题了,每次修改 Kbone 代码重新编译后,微信开发者工具 均会报这个错误。这是因为在 Kbone 中未配置 微信小程序 的 AppID,在 Kbone 项目中找到 build/miniprogram.config.js ,然后拉到最下面,填写 projectConfig: { appid: ‘’ } 的值即可(需退出编译模式,重新编译) 例: [代码]// 项目配置,会被合并到 project.config.json projectConfig: { projectname: 'kbone-template-vue', appid: 'wxd****7ff****8f4f', } [代码] Kbone的后续 当然,目前 Kbone 依然有很多不足,甚至可以说匪夷所思的地方,但是毕竟是一个新兴脚手架,在可以理解的范围内。Kbone 的团队也在持续优化、维护、升级,相信未来能更好~ 写在后面 至此,Kbone 的项目算是成功搭建,并且正常运行了,如果在使用过程中,有遇到其他问题,欢迎来此文章提出,编者会尽量回复。 最后,推荐一下 pohunchn/kbone-vue 的模版,此模版是基于官方kbone-vue模版所制作的 vue 模版,可使上手更为方便(不用再手动去修改 AppID 啦,也能直接去掉繁琐的 Eslint 啦!)。 [代码]vue init pohunchn/kbone-vue my-app [代码] 执行以上方法即可安装,是不是很简单!
2019-12-03 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 诗词歌赋,样样精通!诗词古语小程序带你领略魅力古风丨实战
1. 小程序功能 古诗词大全 成语大全 成语接龙 诗词飞花令 诗词分享、收藏 诗词接龙 唐诗宋词起名字 百家姓 猜谜语 2. 小程序预览: [图片] 3. 部分截图 首页 [图片] 列表页 [图片] 详情页 分享页 [图片] 唐诗宋词 [图片] 成语接龙 [图片] 4. 项目结构 [代码]. ├── README.md ├── project.config.json // 项目配置文件 ├── cloudfunctions | 云环境 // 存放云函数的目录 │ ├── login // 用户登录云函数 │ │ ├── index.js │ │ └── package.json │ └── collection_get // 数据库查询云函数 │ │ ├── index.js │ │ └── package.json │ └── collection_update // 数据库更新云函数 │ ├── index.js │ └── package.json └── miniprogram ├── images // 存放小程序图片 ├── lib // 配置文件 ├── pages // 小程序各种页面 | ├── index // 首页 | └── menu // 分类页 | └── user // 用户中心 | └── search // 搜索页 | └── list // 列表页 搜索结果页 | └── detail // 详情页 | └── collection // 收藏页 | └── find // 发现页 | └── idiom-jielong // 成语接龙页 | └── poet // 作者页 | └── baijiaxing // 百家姓 | └── xiehouyu // 歇后语 | └── poet // 作者页 | └── suggest // 建议反馈 | └── ... // 其他 ├── style // 样式文件目录 ├── app.js // 小程序入口文件 ├── app.json // 全局配置 └── app.wxss // 全局样式 [代码] 5. 封装云函数操作数据库 本项目是使用的小程序云开发。云开发提供了一个 JSON 数据库,用户可以直接在云端进行数据库增删改查,但是,小程序对用户操作数据的权限进行了一定的限制(例如数据update、一次性get记录的条数限制等),所以,这里主要采用云函数来操作数据库。 查询数据、分页查询 函数根目录上右键,在右键菜单中,选择创建一个新的 Node.js 云函数,我们将该云函数命名为 collection_get。 编辑 index.js: [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { /** * page: 第几页 * num: 每页几条数据 * condition: 查询条件,例如 { name: '李白' } */ const {database, page, num, condition} = event console.log(event) try { return await db.collection(database) .where(condition) .skip(num * (page - 1)) .limit(num) .get() } catch (err) { console.log(err) } } [代码] 使用 collection_get 云函数 例如,按照查询条件[代码]{tags: '唐诗三百首'}[代码]查询诗词列表,每页[代码]num = 10[代码]条数据: [代码]let {list, page, num} = this.data let that = this this.setData({ loading: true }) wx.cloud.callFunction({ name: 'collection_get', data: { database: 'gushici', page, num, condition: { tags: '唐诗三百首' } }, }).then(res => { if(!res.result.data.length) { // 没搜索到 that.setData({ loading: false, isOver: true }) } else { let res_data = res.result.data list.push(...res_data) that.setData({ list, page: page + 1, // 页面加1 loading: false }) } }) .catch(console.error) } [代码] 更新数据 注意,当我们向数据库中添加记录时,系统会自动帮我们为每条记录添加上用户的 [代码]openid[代码] 字段,但如果,数据表是自己用 json/csv 文件导入的,就不存在 [代码]openid[代码] 字段,此时,当更新这个数据表时,系统会认为你不是创建者,所以也就无法更新。 此时,就需要通过云函数更新数据库,新建云函数 collection_update, 编辑 index.js: [代码]// 更新数据 - 根据 _id 更新已打开人数 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command exports.main = async (event, context) => { const { id } = event console.log(event) try { return await db.collection('gushici').doc(id) .update({ data: { opened: _.inc(1) }, }) } catch (e) { console.error(e) } } [代码] 使用 collection_update 云函数 更新某_id数据的打开人数: [代码]let _id = e.currentTarget.dataset.id wx.cloud.callFunction({ name: 'collection_update', data: { id: _id }, }).then(res => { console.log(res.data) }) .catch(console.error) [代码] 6. 数据库模糊查询 小程序云开发可以使用正则表达式进行模糊查询。例如, 根据用户输入关键词,查询标题中存在改关键词的古诗词。 [代码]let database = 'gushici' let condition = { name: { $regex:'.*'+ inputValue, $options: 'i' } } let { list, page, num } = this.data let that = this this.setData({ loading: true }) // 模糊查询 wx.cloud.callFunction({ name: 'collection_get', data: { database, page, num, condition }, }).then(res => { if (!res.result.data.length) { // 没搜索到 that.setData({ loading: false, isOver: true }) } else { let res_data = res.result.data list.push(...res_data) that.setData({ list, loading: false }) } }) .catch(console.error) [代码] 7. 分享或转发功能 小程序中页面触发转发的方式有两种: 1.在小程序的右上角选择转发,需要定义函数 Page.onShareAppMessage,如果当前页面没有定义此事件,则点击后无效果。 2.通过给 [代码]button[代码] 组件设置属性 [代码]open-type="share"[代码],可以在用户点击按钮后触发 Page.onShareAppMessage 事件,如果当前页面没有定义此事件,则点击后无效果。 用户还可以在 Page.onShareAppMessage 事件中自定义转发后显示的标题、图片、路径: [代码]onShareAppMessage(res) { let id = wx.getStorageSync('shareId') if (res.from === 'button') { // 来自页面内转发按钮 console.log(res.target) } return { title: `跟我一起挑战最长的成语接龙吧!`, path: `pages/find/find`, imageUrl: '/images/img.jpg', } }, [代码] 注意:转发成功/失败的 callback 已经被官方废弃,所以理论上小程序是无法得知用户是否将页面分享成功的 8. 用户授权 详情请参考文章:微信小程序之授权 9. 需要注意的几个坑 查询不到数据 数据表中明明有数据,但是 collection.get 到的却为空。解决:可以在云开发控制台中打开数据库权限设置,设置权限。 更新数据失败 collection.update 函数调用成功单返回的却是0行记录被更新,因为小程序端不允许更新没有 openid 字段的数据。解决:可以通过云函数更新数据库。 background 图片 url 不能为本地图片 解决:1:将图片上传到服务器,填写服务器上的图片路径地址。2:将图片转为 base64 编码。 往云数据库中批量导入 json 数据失败 原因:请看文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/import.html 解决:去掉json数据 [代码]{}[代码]之间的逗号, 如果最外层为 [代码][][代码],也必须去掉, 最终形如: [代码]{ "index": "作者_1", "type": "作者", "poet": "李白", "abstract": "李白(701年-762年),字太白,号青莲居士,唐朝浪漫主义诗人,被后人誉为“诗仙”..." } { "index": "作者_2", "type": "作者", "poet": "白居易", "abstract": "白居易(772年-846年),字乐天,号香山居士..." } [代码] 源码链接 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-09 - 自用简单的导航栏(欢迎改造/滑稽)
使用scroll-view横向滚动制作的一款导航栏,只做个个简单渐变切换的效果和下划线(下划线也做了渐变觉得太难看注释了,也可以使用),欢迎各位改造,方便以后多用,也可以自己随便用用,不足也可以指出,此贴如有新想法,新改进会持续更新 原始代码链接:https://developers.weixin.qq.com/s/IffO2gmi7Y9z 增加了放大缩小透明变化:https://developers.weixin.qq.com/s/LtPfmhmD7Y9a
2019-06-21 - 小程序前后端交互使用JWT
前言 现在很多Web项目都是前后端分离的形式,现在浏览器的功能也是越来越强大,基本上大部分主流的浏览器都有调试模式,也有很多抓包工具,可以很轻松的看到前端请求的URL和发送的数据信息。如果不增加安全验证的话,这种形式的前后端交互时候是很不安全的。 相信很多开发小程序的开发者也不一定都是大神,能够精通前后端,作为小程序的初学者不少人也是根据官方的文档去学习开发的。我自己最开始接触小程序也是从wafer2开始的,那时候腾讯云提供的SDK包含PHP和Node.js,因为对于一直做前端的人来说,Node.js的学习成本比较低,只要会JS基本能看懂,也是从那时候才开始接触Node.js,所以本文主要是基于wafer2的服务端基于Koa2的后端来说(其实这个不重要,Node.js基本都差不多)。 什么是JWT? 根据维基百科的定义,JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。 为什么使用JWT? 首先,这不是一个必选方案。有时候我们的API是其它服务端和小程序公用的,那么就涉及到安全验证的问题了。 微信官方不鼓励小程序一打开就要求必须登陆的方式去获取用户信息,因此我们也不能去校验这个用户是否有权限访问这个接口,但是有的接口又不能让任何人随便去看或者被随意采集。 基于token(令牌)的用户认证 用户输入其登录信息 服务器验证信息是否正确,并返回已签名的token token储在客户端,例如存在local storage或cookie中 之后的HTTP请求都将token添加到请求头里 服务器解码JWT,并且如果令牌有效,则接受请求 一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。 关于JWT的详细介绍网上有很多,这里也就不说了,下面介绍在Koa2框架里的添加方法。 安装依赖 [代码]npm install jsonwebtoken npm install koa-jwt [代码] app.js 引用 [代码]const jwtKoa = require('koa-jwt'); [代码] 设置不需要JWT验证的目录或者文件 [代码]const secret = '设置密钥'; app.use(jwtKoa({secret}).unless({ path: ['/','\/favicon.ico',/^\demo/] })) [代码] 数组中的路径不需要通过jwt验证。 授权 小程序 wx.request 发送网络请求的 referer header 不可设置。 其格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本。 那么我们就可以根据 ctx.header 里的 referer 进行初步的限制,比如指定的 appid 才能生成令牌。 我们在生成令牌的时候可以把简单的信息加入进去,如: [代码]const userToken = { referer: refererArray[2], appid: refererArray[3], version: refererArray[4], data: '此处可传入用户的信息' } [代码] 生成令牌: [代码]const jwt = require('jsonwebtoken'); const secret = '设置密钥'; jwt.sign(userToken, secret, {expiresIn: '2h'}); [代码] expiresIn:为令牌的有效期 这样简单的JWT令牌就生成好了,再通过接口返回给小程序端。 小程序前端如何使用JWT? 很简单,在header里加入下面属性即可。 [代码]authorization: 'Bearer 获取到的令牌' [代码] JWT优点 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而JWT不需要。 无状态 JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT,可以降低服务器查询数据库的次数。 JWT缺点 安全性 由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。 性能 JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的http请求比使用session的开销大得多。 一次性 无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。 (1)无法废弃 通过上面JWT的验证机制可以看出来,一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。 (2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个http请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。
2019-02-20 - 【技巧】利用canvas生成朋友圈分享海报
前言 大家好,上次给大家讲了函数防抖和函数节流 https://developers.weixin.qq.com/community/develop/article/doc/000a645d8b8ba0d8722863ef45bc13 今天给大家分享一下利用canvas生成朋友圈分享海报 由于小程序的限制,我们不能很方便地在微信内直接分享小程序到朋友圈,所以普遍的做法是生成一张带有小程序分享码的分享海报,再将海报保存到手机相册,有两种方法可以生成分享海报,第一种是让后台生成然后返回图片链接,这一种方法比较简单,只需要传后台所需要的参数就行了,今天给大家介绍的是第二种方法,用canvas生成分享海报。 效果 [图片] 主要步骤 把海报样式用标签先写好,方便画图时可以比对 用canvas进行画图,canvas要注意定好宽高 canvas利用wx.canvasToTempFilePath这个api将canvas转化为图片 将转化好的图片链接放入image标签里 再利用wx.saveImageToPhotosAlbum保存图片 坑点 用canvas进行画图的时候要注意画出来的图的大小一定要是你用标签写好那个样式的两倍大小,比如你的海报大小是400600的大小,那你用canvas画的时候大小就要是8001200,宽高可以写在样式里,如果你画出来的图跟你海报图是一样的大小的话生成的图片是会很模糊的,所以才需要放大两倍。 画图的时候要注意尺寸的转化,如果你是用rpx做单位的话,就要对单位进行转化,因为canvas提供的方法都是经px为单位的,所以这一点要注意一下,px转rpx的公式是w/750z2,w是手机屏幕宽度screenWidth,可以通过wx.getSystemInfo获取,z是你需要画图的单位,2就是乘以两倍大小。 图片来源问题,因为canvas不支持网络图片画图,所以你的图片要么是固定的,如果不是固定的,那就要用wx.downloadFile下载后得到一个临时路径才行 小程序码问题,小程序需要后台请求接口后返回一个二进制的图片,因为二进制图片canvas也是不支持的,所以也是要用wx.downloadFile下载后得到一个临时路径,或者可以叫后台直接返回一个小程序码的路径给你 这里保存的时候是有个授权提醒的,如果拒绝的话再次点击就没有反应了,所以这里我做了一个判断是否有授权的,如果没有就弹窗提醒,确认的话会打开设置页面,确认授权后再次返回就行了,这里有个坑注意下,就是之前拒绝后再进入设置页面确认授权返回页面时保存图片会不成功,官方还没解决,我是加了个setTimeOut处理的,详情可以看这里 https://developers.weixin.qq.com/community/develop/doc/000c46600780f0fa68d7eac345a400 代码实现 [图片] 这里图片我先用的是网上的链接,实际项目中是后台返回的数据,这个可以自行处理,这里只是为了演示方便,生成临时路径的方法我这里是分别定义了一个方法,其实可以合成一个方法的,只是生成小程序码时如果要传入参数要注意一下。 绘图方法是drawImg,这里截一部分,详细的可以看代码片段 [图片] 不足 由于在实际项目中返回的图片宽高是不固定的,但是canvas画出来的又需要固定宽高,所以分享图会有图片变形的问题,使用drawImage里的参数也不能解决,如果各位有比较好的方案可以一起讨论一下。 代码片段 https://developers.weixin.qq.com/s/3pcsjDmS7M5Y
2019-02-22 - 小程序原生高颜值组件库--ColorUI
[图片] 简介 ColorUI是一个Css类的UI组件库!不是一个Js框架。相比于同类小程序组件库,ColorUI更注重于视觉交互! 浏览GitHub:https://github.com/weilanwl/ColorUI [图片] 如何使用? 先下载源码包 → Github 引入到我的小程序 将 /demo/ 下的 colorui.wxss 和 icon.wxss 复制到小程序的根目录下 在 app.wxss 引入两个文件 [代码]@import "icon.wxss"; @import "colorui.wxss"; [代码] 使用模板全新开发 复制 /template/ 文件夹并重命名为你的项目,微信开发者工具导入为小程序就可以使用ColorUI了 体验沉浸式导航 [图片] App.js 获取系统参数并写入全局参数。 [代码]//App.js App({ onLaunch: function() { wx.getSystemInfo({ success: e => { this.globalData.StatusBar = e.statusBarHeight; let custom = wx.getMenuButtonBoundingClientRect(); this.globalData.Custom = custom; this.globalData.CustomBar = custom.bottom + custom.top - e.statusBarHeight; } }) } }) [代码] Page.js 页面配置获取全局参数。 [代码]//Page.js const app = getApp() Page({ data: { StatusBar: app.globalData.StatusBar, CustomBar: app.globalData.CustomBar, Custom: app.globalData.Custom } }) [代码] Page.wxml 页面构造导航。更多导航样式请下载Demo查阅 操作条组件。 [代码]<view class="cu-custom" style="height:{{CustomBar}}px;"> <view class="cu-bar fixed bg-gradual-pink" style="height:{{CustomBar}}px;padding-top:{{StatusBar}}px;"> <navigator class='action border-custom' open-type="navigateBack" delta="1" hover-class="none" style='width:{{Custom.width}}px;height:{{Custom.height}}px;margin-left:calc(750rpx - {{Custom.right}}px)'> <text class='icon-back'></text> <text class='icon-homefill'></text> </navigator> <view class='content' style='top:{{StatusBar}}px;'>操作条</view> </view> </view> [代码] 自定义系统Tabbar [图片] 按照官方 自定义 tabBar 配置好Tabbar (开发工具和版本库请使用最新版)。 使用ColorUI配置Tabbar只需要更改 Wxml 页的内容即可。 更多Tabbar样式请下载Demo查阅 操作条组件。 /custom-tab-bar/index.wxml [代码] <view class="cu-bar tabbar bg-white shadow"> <view class="action" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab"> <view class='icon-cu-image'> <image src='{{selected === index ? item.selectedIconPath : item.iconPath}}' class='{{selected === index ? "animation" : "animation"}}'></image> </view> <view class='{{selected === index ? "text-green" : "text-gray"}}'>{{item.text}}</view> </view> </view> [代码] 作者叨叨 ColorUI是一个高度自定义的Css样式库,包含了开发常用的元素和组件,元素组件之间也能相互嵌套使用。我也会不定期更新一些扩展到源码。 其实大家都在催我写文档,但这个库源码就在这,所见即所得,粘贴复制就可以得到你想要的页面。当然,文档我还是要写的,也希望大家多多提意见。 现在前端的开发方向基本都是奔着Js方向的,布局和样式大家讨论的有点少。以后我会在开发者社区多聊一聊关于开发中的布局和样式。 [图片] 感谢阅读。
2019-02-26 - 「分享」高性能双列瀑布流极简实现(附示例)❤️
前言 在日常开发过程中,经常会有双列瀑布流场景的需求出现,如商品列表、文章列表等,本文将简单介绍这种情景下如何高效、精准的实现双列瀑布流场景,支持刷新、加载更多等,实现效果如下。 [图片] [图片] 开发思路 瀑布流视图有一种参差的美感,常规列表布局如 flex wrap 等由于存在行高度限制,无法让第二行的 item 对齐上一行最矮处,因此,瀑布流布局时采用双列 scrollview 的 flex 布局。 参差布局的实现,采用代码计算左右两列的高度,然后对左右两列总高度进行比较,新加入的 item 总是排在总高度较小的那列后面。 计算时可以尽可能的缓存高度,例如左右两列高度在每次计算时都缓存起来,有新的 item 加入列表时直接增加左右两列高度即可,不需要重新从头计算。 index.js [代码]const tplWidth = (750 - 24 - 8) / 2; const tplHeight = 595; // plWidth * 1.66 newPhotos.forEach(photo => { const { height, width } = photo let photoHeight = tplWidth if (height > width) { photoHeight = tplHeight photo.display = 'long' } else { photo.display = 'short' } if (leftHeight < rightHeight) { leftList.push(photo) leftHeight += photoHeight } else { rightList.push(photo) rightHeight += photoHeight } }) [代码] index.wxml [代码]<!-- list --> <view class='list'> <!-- left --> <view class='left-list'> <block wx:for="{{leftList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> <!-- right --> <view class='right-list'> <block wx:for="{{rightList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> </view> [代码] index.css [代码].list { display: flex; flex: 1; position: relative; flex-direction: row; justify-content: space-between; padding-left: 12rpx; padding-right: 12rpx; padding-top: 8rpx; } .left-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } .right-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } [代码]
2019-02-27 - 小程序注销能力,已灰度上线
[图片] 一年前,有人疯狂地注册小程序,而今天,有人却渴求着注销小程序。 作者丨程序君 近日,微信小程序注销能力进入灰度测试,开发者可以自行在后台进行小程序的注销。据 腾讯客服(小程序) 的描述,符合条件的小程序可进行自主注销,包括个人、组织、政府等类型。 这样一个看似没有太大“利好”的消息,却引起开发者们的强烈关注。 如何注销小程序 个人类型: 进入小程序后台-设置-原始ID-注销账号。同意协议后,扫码即可进入账号冻结期。 [图片] 组织类型: 与个人不同的是,组织类型在扫码后,要填写对公账户,并进行小额打款验证,小额打款验证期限为10天,打款后验证时间一般为1个工作日。 [图片] 冻结期: 普通小程序、未发布的小游戏冻结期为7天,帐号所有功能不可用,资源仍为占用。而已发布的小游戏冻结期为30天,资源仍为占用。 注销条件: 1.小程序必须是已注册成功的帐号。 2.已开通广告主服务的小程序广告投放账户余额须为零。 3.须自主暂停线上小程序版本服务(除已发布小游戏帐号外)。 账号注销成功后,立即释放绑定邮箱、主体名称、管理员信息(姓名、身份证号、手机号码、微信号)、项目成员信息、关联关系。 原来的昵称有2天的保护期,在此期间,符合命名唯一规则情况下,同一主体下的其他帐号可以使用该名称,主体不一致的,则需要在保护期满后才能申请使用该名称。 注销小程序,为何成热点功能 2017年微信小程序公测,小程序成为互联网的大热点。不少开发者因为占到了一个好名称,即使不去运营,也能获取流量。 一时间,小程序掀起注册热潮,为抢占先机,不少开发者很快把1人最多关联5个小程序的名额用完。程序君当时也是抢占了「婚纱礼服Lite」等5个小程序名称。 随着微信搜索事业部的成立,微信搜索逐渐成熟。运营、服务好的小程序更容易被搜索到,而缺乏运营、服务质量低的小程序已经难以触达用户。以「婚纱礼服Lite」为例,由于缺乏运营,小程序从最高100人/天的搜索访问,到现在,已经无人问津。 [图片] 2年过去了,“小程序躺着赚流量”的时期也随之而去,好的运营配合上好的名称,才是小程序的生存之道。微信依旧坚持“一个身份证之多关联5个小程序”的原则,而小程序的主体迁移功能却开放了。渴望着小程序注销的开发者,有两大类: 供给方:5个关联小程序的额度已经用完,希望把一些跟自身业务关联性不大的小程序名称释放出去,把名称转让给有需要的开发者,同时自己获取新的小程序注册机会。 需求方:小程序没有取到好的名称,而好的名称已经被占用。希望能够通过“主体迁移-注销-名称进入保护期-改名”的方法,重新获取一个好的名称。 小程序生态将更好 当然,除了名称的转让外,小程序注销的意义还有很多。这是给予开发者更好管理小程序的能力,也是完善微信小程序生态的措施。 [图片] 据微信官方数据,2018年微信小程序数量超过100万个,目前的数量或已远远高于这个数据,但数量多不代表质量高。小程序抢注热潮过后,大量名称好,质量低的小程序残留在微信生态中,俨然成为“小程序中的废品”。 一方面,微信开始打造“小程序评价”体系,让用户搜索到的更多是好用的小程序;另一方面,小程序注销让开发者释放无效的小程序,也提升了微信小程序整体质量,让服务真正做到“所需所得,即用即走”。 【晓程序速报】公众号(ID:xcxsubao)回复关键词【注销】,获取更详细的小程序注销流程。
2019-03-04 - 你不知道的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 - 如何监听小程序中的手势事件(缩放、双击、长按、滑动、拖拽)
mina-touch [图片] [代码]mina-touch[代码],一个方便、轻量的 小程序 手势事件监听库 事件库部分逻辑参考[代码]alloyFinger[代码],在此做出声明和感谢 change log: 2019.03.10 优化监听和绘制逻辑,动画不卡顿 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 2020.12.13 更名 mina-touch 2020.12.27 上传 npm 库;优化使用方式;优化 README 支持的事件 支持 pinch 缩放 支持 rotate 旋转 支持 pressMove 拖拽 支持 doubleTap 双击 支持 swipe 滑动 支持 longTap 长按 支持 tap 按 支持 singleTap 单击 扫码体验 [图片] demo 展示 demo1:监听 pressMove 拖拽 手势 查看 demo 代码 [图片] [图片] demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) 查看 demo 代码 [图片] [图片] demo3: 测试监听双击事件 查看 demo 代码 [图片] [图片] demo4: 测试监听长按事件 查看 demo 代码 [图片] [图片] demo 代码 demo 代码地址 mina-tools-client/mina-touch 使用方法 大致可以分为 4 步: npm 安装 mina-touch,开发工具构建 npm 引入 mina-touch onload 实例化 mina-touch wxml 绑定实例 命令行 [代码]npm install mina-touch[代码] 安装完成后,开发工具构建 npm *.js [代码]import MinaTouch from 'mina-touch'; // 1. 引入mina-touch Page({ onLoad: function (options) { // 2. onload实例化mina-touch //会创建this.touch1指向实例对象 new MinaTouch(this, 'touch1', { // 监听事件的回调:multipointStart,doubleTap,longTap,pinch,pressMove,swipe等等 // 具体使用和参数请查看github-README(底部有github地址 }); }, }); [代码] NOTE: 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) *.wxml 在 view 上绑定事件并对应: [代码]<view catchtouchstart="touch1.start" catchtouchmove="touch1.move" catchtouchend="touch1.end" catchtouchcancel="touch1.cancel" > </view> <!-- touchstart -> 实例对象名.start touchmove -> 实例对象名.move touchend -> 实例对象名.end touchcancel -> 实例对象名.cancel --> [代码] NOTE: 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 以上简单几步即可使用 mina-touch 手势库 😊😊😊 具体使用和参数请查看Github https://github.com/Yrobot/mina-touch 如果喜欢mina-touch的话,记得在github点个start哦!🌟🌟🌟
2021-06-24