个人案例
- #小程序云开发挑战赛#-魅力单词-未央队
1. 引言 背单词十分枯燥乏味,为了给广大英语学习者提供一个比较好的背英语模式和应用,魅力单词应运而生。 基于微信小程序,天然低门槛、易上手。 2. 应用场景 课间、地铁上、旅途中,任何碎片化时间。 3. 作品简介 魅力单词是一款面向学生的背单词小程序,提供了丰富的单词词库以及好玩的背单词模式。 4. 目标用户 初高中生、大学生,英语爱好者。 5. 实现思路 本小程序基于云开发,用到了云数据库存储数据,使用云函数获取词库列表,随机获取单词词条,图片列表。 按批次拉取数据缓存到内存及localStorage中,实时对比输入内容与当前单词,基于图片奖励,继续循环下个单词。 图片数据:来源于微博。 单词词库:来源于百词斩。 6.流程图 [图片] 7. 效果图 [图片] [图片] [图片] [图片] 8. 功能代码展示 云函数 [图片] 数据库 [图片] 页面源码 [图片] [图片] 9. 作品二维码 [图片] 作品代码仓库地址:请私信 10. 团队简介 团队名称:未央队 团队成员:曹禄丰
2022-03-14 - #小程序云开发挑战赛#-情侣券-想做就做
应用场景 灵感源于 [图片] 目标用户 情侣,夫妻 原型图 [图片] 架构图 [图片] 效果截图 主流程 [图片] 模块 模版 [图片] 我的 [图片] 项目演示视频 地址:https://v.qq.com/x/page/v3153g8zs5p.html 功能代码展示 云函数代码(卡券云函数) [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() if (event.action && ticketHelper[event.action]) { const result = await ticketHelper[event.action](wxContext, event) return result } return { message: 'This action was not found', error: -1, } } const db = cloud.database(); const ticketHelper = { // 添加卡券 async addTicket(context, params) { params.ticket._openid = context.OPENID let res = await db.collection('tickets').add({ data: params.ticket }) return res }, // 查看我的卡券 async queryMyTicket(context, params) { const { OPENID } = context let res = await db.collection('tickets').where({ _openid: OPENID }).orderBy('createdAt', 'desc').get() return res }, // 查看卡券详情 async queryCurrentTicket(context, params) { let res = await db.collection('tickets').doc(params.ticketId).get() return res }, // 删除卡券 async removeTicket(context, params) { let res = await db.collection('tickets').doc(params.ticketId).remove() return res }, } [代码] 页面代码(首页) [代码]<!--index.wxml--> <view class="container"> <view class="top-img"> <image class="top-img" src="../../images/top_img.png"></image> </view> <view class="ticket-list"> <view bindtap="toAdd" class="create"> + 添加自定义模版 </view> <view wx:for="{{myTemplates}}" class="custom" style="background-image: url({{item.backgroundurl}});" bindtap="toInfo" data-item="{{item}}"> <view class="custom-title" style="color:{{item.color}}">{{item.title}}</view> <view class="custom-info" style="color:{{item.color}}">{{item.info}}</view> </view> </view> <view class="ticket-list"> <view wx:for="{{templates}}" bindtap="toInfo" data-item="{{item}}" class="ticket-item {{index%2==0?'ticket-pink-bg':'ticket-blue-bg'}}"> <text class="ticket-title">{{item.title}}</text> <view class="ticket-info {{index%2==0?'ticket-pink-info':'ticket-blue-info'}} ">{{item.info}}</view> </view> </view> </view> [代码] 逻辑代码(首页) [代码] // 首页 const app = getApp() import { queryPrivateTemplate, queryPublicTemplate } from '../../api/template' Page({ data: { }, onLoad: function (opt) { // 领取路径跳转 if (opt.type == 1) { wx.navigateTo({ url: '/pages/giveList/giveList?ticket=' + opt.ticket, }) } // 查询公用的卡券模版 queryPublicTemplate().then(res => { this.setData({ templates: res.result.data, }) }); }, onShow() { // 查询私有的卡券模版 queryPrivateTemplate().then(res => { this.setData({ myTemplates: res.result.data, }) }); }, // 去添加模版 toAdd() { if (app.authorized !== true) { wx.navigateTo({ url: '/pages/authorize/authorize' }); return; } wx.navigateTo({ url: '/pages/selectBackground/selectBackground', }) }, // 查看详情 toInfo(res) { // 没有授权就先去授权 if (app.authorized !== true) { wx.navigateTo({ url: '/pages/authorize/authorize' }); return; } let item = res.currentTarget.dataset.item var templateInfo = JSON.stringify(item); wx.navigateTo({ url: '/pages/add/add?action=update&templateInfo=' + templateInfo }) } }) [代码] 样式代码(app.js) [代码].container{ width: 100vw; /* height: 100vh; */ background-color: #FFFFFF; display: flex; flex-direction: column; align-items: center; } page{ background: #f6f6f6; --color-p: #F58B98; } button { padding-left: 0rpx; padding-right: 0rpx; border-radius: 0rpx; } button::after { border: none; } /* 底部按钮 */ .next-btn { position: absolute; bottom: 0rpx; left: 0rpx; width: 750rpx; height: 120rpx; background: var(--color-p); text-align: center; line-height: 120rpx; font-size: 30rpx; font-family: PingFang SC; font-weight: 600; color: #FFFFFF; } /* 已使用样式 */ .image-gray { -webkit-filter: grayscale(100%); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); filter: gray; opacity:0.9; } /* 所有输入框,未输入文字的样式 */ .placeholder { font-size: 26rpx; font-family: PingFang SC; font-weight: 400; color: #CCCCCC; } /* 卡券样式:主页、添加页、我收到的、我赠送的、详情页 */ .custom { width: 670rpx; height: 240rpx; margin-top: 25rpx; background: #FFF7F7; border-radius: 20rpx; background-size: 100% 100%; position: relative; } .custom-title { margin-left: 40rpx; padding-top: 40rpx; font-size: 40rpx; font-weight: 600; color: #FFFFFF; } .custom-info { margin-left: 40rpx; padding-top: 25rpx; font-size: 26rpx; font-weight: 600; color: #FFFFFF; } /* 头部样式:选择背景、添加页、详情页 */ .top { width: 690rpx; display: flex; flex-direction: row; margin: 20rpx 30rpx; align-items: center; justify-content: space-between } .top-title { font-size: 34rpx; font-family: PingFang SC; font-weight: 600; color: #2A2A2A; } .top-right-text { font-size: 24rpx; font-family: PingFang SC; font-weight: 400; color: var(--color-p); } [代码] 作品体验二维码 [图片] 团队简介 夫妻档 陈宇明:负责产品与开发 王丝雨:负责设计与交互 觉得不错那就【点赞】支持一下
2020-09-20 - Gitter - 高颜值 GitHub 小程序客户端诞生记
0. 前言 嗯,可能一进来大部分人都会觉得,为什么还会有人重复造轮子,GitHub第三方客户端都已经烂大街啦。确实,一开始我自己也是这么觉得的,也问过自己是否真的有意义再去做这样一个项目。思考再三,以下原因也决定了我愿意去做一个让自己满意的GitHub第三方客户端。 对于时常关注GitHub Trending列表的笔者来说,迫切需要一个更简单的方式随时随地去跟随GitHub最新的技术潮流; 已有的一些GitHub小程序客户端颜值与功能并不能满足笔者的要求; 听说iOS开发没人要了,掌握一门新的开发技能,又何尝不可? 其实也没那么多原因,既然想做,那就去做,开心最重要。 1. Gitter [图片] GitHub:https://github.com/huangjianke/Gitter,可能是目前颜值最高的GitHub小程序客户端,欢迎star 数据来源:GitHub API v3 目前实现的功能有: 实时查看Trending 显示用户列表 仓库和用户的搜索 仓库:详情展示、README.md展示、Star/Unstar、Fork、Contributors展示、查看仓库文件内容 开发者:Follow/Unfollow、显示用户的followers/following Issue:查看issue列表、新增issue、新增issue评论 分享仓库、开发者 … Gitter的初衷并不是想把网页端所有功能照搬到小程序上,因为那样的体验并不会很友好,比如说,笔者自己也不想在手机上阅读代码,那将会是一件很痛苦的事。 在保证用户体验的前提下,让用户用更简单的方式得到自己想要的,这是一件有趣的事。 2. 探索篇 技术选型 第一次觉得,在茫茫前端的世界里,自己是那么渺小。 当决定去做这个项目的时候,就开始了马不停蹄的技术选型,但摆在自己面前的选择是那么的多,也不得不感慨,前端的世界,真的很精彩。 原生开发:抱着学习的心态,希望尝试下非原生开发体验; WePY:之前用这个框架已经开发过一个小程序,诗词墨客,同样是抱着学习的心态,尝试下其他框架; mpvue:用Vue的方式去开发小程序,个人觉得文档并不是很齐全,加上近期维护比较少,可能是趋于稳定了? Taro:用React的方式去开发小程序,Taro团队的小伙伴维护真的很勤快,也很耐心的解答大家疑问,文档也比较齐全,开发体验也很棒,还可以一键生成多端运行的代码(暂没尝试) 货比三家,经过一段时间的尝试及踩坑,综合自己目前的能力,最终确定了Gitter的技术选型: Taro + Taro UI + Redux + 云开发 Node.js 页面设计 其实,作为一名Coder,曾经一直想找个UI设计师妹子做老婆的(肯定有和我一样想法的Coder),多搭配啊。现在想想,code不是生活的全部,现在的我一样很幸福。 话回正题,没有设计师老婆页面设计怎么办?毕竟笔者想要的是一款高颜值的GitHub小程序。 嗯,不慌,默默的拿出了笔者沉寂已久的Photoshop和Sketch。不敢说自己的设计能力如何,Gitter的设计至少是能让笔者自己心情愉悦的,倘若哪位设计爱好者想对Gitter的设计进行改良,欢迎欢迎,十二分的欢迎! 3. 开发篇 Talk is cheap. Show me the code. 作为一篇技术性文章,怎可能少得了代码。 在这里主要写写几个踩坑点,作为一个前端小白,相信各位读者均是笔者的前辈,还望多多指教! Trending 进入开发阶段没多久,就遇到了第一个坑。GitHub居然没有提供Trending列表的API!!! 也没有过多的去想GitHub为什么不提供这个API,只想着怎么去尽快填好这个坑。一开始尝试使用Scrapy写一个爬虫对网页端的Trending列表信息进行定时爬取及存储供小程序端使用,但最终还是放弃了这个做法,因为笔者并没有服务器与已经备案好的域名,小程序的云开发也只支持Node.js的部署。 开源的力量还是强大,最终找到了github-trending-api,稍作修改,成功部署到小程序云开发后台,在此,感谢原作者的努力。 Trending列表云函数 [代码]// 云函数入口函数 exports.main = async (event, context) => { const { type, language, since } = event let res = null; let date = new Date() const cacheKey = `repositories::${language || 'nolang'}::${since || 'daily'}`; const cacheData = await db.collection('repositories').where({ cacheKey: cacheKey }).orderBy('cacheDate', 'desc').get() if (cacheData.data.length !== 0 && ((date.getTime() - cacheData.data[0].cacheDate) < 1800 * 1000)) { res = JSON.parse(cacheData.data[0].content) } else { res = await fetchRepositories({ language, since }); await db.collection('repositories').add({ data: { cacheDate: date.getTime(), cacheKey: cacheKey, content: JSON.stringify(res) } }) } return { data: res } } [代码] Markdown解析 嗯,这是一个大坑。 在做技术调研的时候,发现小程序端Markdown解析主要有以下方案: wxParse:作者最后一次提交已是两年前了,经过自己的尝试,也确实发现已经不适合如README.md的解析 wemark:一款很优秀的微信小程序Markdown渲染库,但经过笔者尝试之后,发现对README.md的解析并不完美 towxml:目前发现是微信小程序最完美的Markdown渲染库,已经能近乎完美的对README.md进行解析并展示 在Markdown解析这一块,最终采用的也是towxml,但发现在解析性能这一块,目前并不是很优秀,对一些比较大的数据解析也超出了小程序所能承受的范围,还好贴心的作者(sbfkcel)提供了服务端的支持,在此感谢作者的努力! Markdown解析云函数 [代码]const Towxml = require('towxml'); const towxml = new Towxml(); // 云函数入口函数 exports.main = async (event, context) => { const { func, type, content } = event let res if (func === 'parse') { if (type === 'markdown') { res = await towxml.toJson(content || '', 'markdown'); } else { res = await towxml.toJson(content || '', 'html'); } } return { data: res } } [代码] markdown.js组件 [代码]// 云函数解析markdown parseReadme() { const { md, base } = this.props let that = this wx.cloud.callFunction({ // 要调用的云函数名称 name: 'parse', // 传递给云函数的event参数 data: { func: 'parse', type: 'markdown', content: md, } }).then(res => { let data = res.result.data if (base && base.length > 0) { data = render.initData(data, {base: base, app: this.$scope}) } that.setState({ fail: false, data: data }) }).catch(err => { console.log('cloud', err) that.setState({ fail: true }) }) } [代码] [代码]// Markdown渲染 render() { const { data } = this.state return ( <View> { data ? ( <View> <import src='../towxml/entry.wxml' /> <template is='entry' data='{{...data}}' /> </View> ) : ( <View className='loading'> <AtActivityIndicator size={20} color='#2d8cf0' content='loading...' /> </View> ) } </View> ) } [代码] Redux 其实,笔者在该项目中,对Redux的使用并不多。一开始,笔者觉得所有的接口请求都应该通过Redux操作,后面才发现,并不是所有的操作都必须使用Redux,最后,在本项目中,只有获取个人信息的时候使用了Redux。 [代码]// 获取个人信息 export const getUserInfo = createApiAction(USERINFO, (params) => api.get('/user', params)) [代码] [代码]// action export function createApiAction(actionType, func = () => {}) { return ( params = {}, callback = { success: () => {}, failed: () => {} }, customActionType = actionType, ) => async (dispatch) => { try { dispatch({ type: `${customActionType }_request`, params }); const data = await func(params); dispatch({ type: customActionType, params, payload: data }); callback.success && callback.success({ payload: data }) return data } catch (e) { dispatch({ type: `${customActionType }_failure`, params, payload: e }) callback.failed && callback.failed({ payload: e }) } } } [代码] [代码]getUserInfo() { if (hasLogin()) { userAction.getUserInfo().then(()=>{ Taro.hideLoading() Taro.stopPullDownRefresh() }) } else { Taro.hideLoading() Taro.stopPullDownRefresh() } } const mapStateToProps = (state, ownProps) => { return { userInfo: state.user.userInfo } } export default connect(mapStateToProps)(Index) [代码] [代码]// reducers export default function user (state = INITIAL_STATE, action) { switch (action.type) { case USERINFO: return { ...state, userInfo: action.payload.data } default: return state } } [代码] 目前,笔者对Redux还是处于一知半解的状态,嗯,学习的路还很长。 有需要的同学可以前往开源仓库查看相应的完整源码,还请多多指教。 4. 结语篇 当Gitter第一个版本通过审核的时候,心情是很激动的,就像自己的孩子一样,看着他一点一点的长大,笔者也很享受这样一个项目从无到有的过程,在此,对那些帮助过笔者的人一并表示感谢。 当然,目前功能和体验上可能有些不大完善,也希望大家能提供一些宝贵的意见,Gitter走向完美的路上希望有你! 最后,希望Gitter小程序能对你有所帮助!
2019-02-21 - 一次安全可靠的通信——HTTPS原理
我们知道小程序的wx.request网络接口只支持HTTPS协议(文档-小程序网络说明),为什么HTTPS协议就比HTTP安全呢?一次安全可靠的通信应该包含什么东西呢,这篇文章我会尝试讲清楚这些细节。 Alice与Bob的通信 我们以Alice与Bob一次通信来贯穿全文,一开始他们都是用明文的形式在网络传输通信内容。 [图片] 嗅探以及篡改 如果在他们的通信链路出现了一个Hacker,由于通信内容都是明文可见,所以Hacker可以嗅探看到这些内容,也可以篡改这些内容。 [图片] 公众号的文章之前就遇到很多被挟持篡改了内容,插入广告。 [图片] 加密解密 既然明文有问题,那就需要对明文进行加密处理,让中间人看不懂内容,于是乎要对原来的内容变成一段看不懂的内容,称为加密,反之则是解密。而本质其实就是一种数学运算的逆运算,类似加法减法,例如发送方可以将 abcd…xyz 每个字母+1映射成 bcd…yza,使得原文的字母变成看不懂的序列,而接收方只需要将每个字母-1就可以恢复成原来的序列,当然这种做法规律太容易被破解了,后边会有个案例示意图。 [图片] 对称加密 如果对2个二进制数A和B进行异或运算得到结果C, 那C和B再异或一次就会回到A,所以异或也可以作为加密解密的运算。 [图片] 把操作数A作为明文,操作数B作为密钥,结果C作为密文。可以看到加密解密运用同一个密钥B,把这种加解密都用同一个密钥的方式叫做对称加密。 [图片] 可以看到简单的异或加密/解密操作,需要密钥跟明文位数相同。为了克服这个缺点,需要改进一下,把明文进行分组,每组长度跟密钥一致,分别做异或操作就可以得到密文分片,再合并到一起就得到密文了。 [图片] 但是这种简单分组的模式也是很容易发现规律,可以从下图看到,中间采用对原图进行DES的ECB模式加密(就是上边提到简单分组的模式) [图片] 很明显,原图一些特征在加密后还是暴露无遗,因此需要再改进一把。一般的思路就是将上次分组运算的结果/中间结果参与到下次分组的运算中去,使得更随机混乱,更难破解。以下图片来自维基百科: [图片] 经过改良后,Alice与Bob如果能提前拿到一个对称加密的密钥,他们就可以通过加密明文来保证他们说话内容不会被Hacker看到了。 [图片] 非对称加密 刚刚还引发另一个问题,这个对称加密用到的密钥怎么互相告知呢?如果在传输真正的数据之前,先把密钥传过去,那Hacker还是能嗅探到,那之后就了无秘密了。于是乎出现另外一种手段: [图片] 这就是非对称加密,任何人都可以通过拿到Bob公开的公钥对内容进行加密,然后只有Bob自己私有的钥匙才能解密还原出原来内容。 [图片] RSA就是这样一个算法,具体数学证明利用了大质数乘法难以分解、费马小定理等数学理论支撑它难以破解。相对于前边的对称加密来说,其需要做乘法模除等操作,性能效率比对称加密差很多。 [图片] 由于非对称加密的性能低,因此我们用它来先协商对称加密的密钥即可,后续真正通信的内容还是用对称加密的手段,提高整体的性能。 [图片] 认证 上边虽然解决了密钥配送的问题,但是中间人还是可以欺骗双方,只要在Alice像Bob要公钥的时候,Hacker把自己公钥给了Alice,而Alice是不知道这个事情的,以为一直都是Bob跟她在通信。 [图片] 要怎么证明现在传过来的公钥就是Bob给的呢?在危险的网络环境下,还是没有解决这个问题。 [图片] 一般我们现实生活是怎么证明Bob就是Bob呢?一般都是政府给我们每个人发一个身份证(假设身份证没法伪造),我只要看到Bob身份证,就证明Bob就是Bob。 网络也可以这么做,如果有个大家都信任的组织CA给每个人出证明,那Alice只要拿到这个证明,检查一下是不是CA制作的Bob证书就可以证明Bob是Bob。所以这个证书里边需要有两个重要的东西:Bob的公钥+CA做的数字签名。 [图片] 前边说到用公钥进行加密,只有拥有私钥的人才能解密。数字证书有点反过来:用私钥进行加密,用公钥进行解密。CA用自己的私钥对Bob的信息(包含Bob公钥)进行加密,由于Alice无条件信任CA,所以已经提前知道CA的公钥,当她收到Bob证书的时候,只要用CA的公钥对Bob证书内容进行解密,发现能否成功解开(还需要校验完整性),此时说明Bob就是Bob,那之后用证书里边的Bob公钥来走之前的流程,就解决了中间人欺骗这个问题了。 这种方式也是一种防抵赖的方式,让对方把消息做一个数字签名,只要我收到消息,用对方的公钥成功解开校验这个签名,说明这个消息必然是对方发给我的,对方不可以抵赖这个行为,因为只有他才拥有做数字签名的私钥。 [图片] CA其实是有多级关系,顶层有个根CA,只要他信任B,B信任C,C信任D,那我们基本就可以认为D是可信的。 [图片] 完整性 上边基本上已经解决了保密性和认证,还有一个完整性没有保障。虽然Hacker还是看不懂内容,但是Hacker可以随便篡改通信内容的几个bit位,此时Bob解密看到的可能是很乱的内容,但是他也不知道这个究竟是Alice真实发的内容,还是被别人偷偷改了的内容。 [图片] 单向Hash函数可以把输入变成一个定长的输出串,其特点就是无法从这个输出还原回输入内容,并且不同的输入几乎不可能产生相同的输出,即便你要特意去找也非常难找到这样的输入(抗碰撞性),因此Alice只要将明文内容做一个Hash运算得到一个Hash值,并一起加密传递过去给Bob。Hacker即便篡改了内容,Bob解密之后发现拿到的内容以及对应计算出来的Hash值与传递过来的不一致,说明这个包的完整性被破坏了。 [图片] 一次安全可靠的通信 总结一下,安全可靠的保障: 对称加密以及非对称加密来解决:保密性 数字签名:认证、不可抵赖 单向Hash算法:完整性 来一张完整的图: [图片]
2019-02-20