- 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - 【文章】优秀文章推送
前言 开发小程序时间不久,看的文章可能大家都看过了,所以此文就送给在小程序开发路上刚刚起跑的一些小伙伴,大佬勿喷 文章来源各大社区或博客 来自掘金 [译] 前端项目中常见的 CSS 问题 [译]一行css代码搞定响应式布局 📝你本可以少写些 if-else 小程序开发经验:多页面数据同步 --腾讯IVWEB团队 手把手教会你小程序登录鉴权 用wxDraw.js制作酷炫的小程序canvas动画『wxDraw 小程序界的zrender』 小程序多端框架全面测评 --凹凸实验室 来自知乎 有用!关于微信小程序,那些开发文档没有告诉你的 开发“小程序”必备书单 来自简书 微信小程序资源整理 小程序的常用居中弹性布局样式整理 最后 看过的文章真的不多,还得加油啊😥!就先分享这些,如果还行记的点赞哦!后期有文章在加吧 欢迎在评论区将你的好文分享一下
2019-06-21 - 【高校征文】| 遇见小程序,发现新乐趣
开篇 时间真的是飞快的,现在是2019/6/4,半年的时间转眼即逝。 我开始接触小程序开发还是18年的寒假,到目前为止时间不算长但也有小一年的时间了,这期间开发了两款小(hen)程(low)序。其中一个小程序是拿去参加2019小程序应用开发赛的(西南赛区),抱着试一试的态度去的(ps:队名也叫try),前段时间在掘金上看到了一个今年参赛的队伍写了一篇介绍自己作品的文章后,的确感觉到了一种可望不可及。 这也让我意识到小程序开发还有很长的路要走,这条路虽长,但是我会坚持走下去,因为就如我标题所说遇见小程序后,它带给了我很多乐趣。昨天看到公众号推送的社区有征文活动的消息,没有犹豫的点进去看了看。然后就开始准备写您所看到的这篇文章。 开篇的废话有点多,后方低能预警🙃(笔者的技术水平有限,可能技术层的分享不多),更多的可能是一些学习历程和经验 路漫漫其修远兮 小程序有趣,但是想要开发一个小程序也不是看一看就能完成的,我们需要去学习它。俗话说:“冰冻三尺非一日之寒”,所以这还要我们始终怀着一颗热爱的心,和坚持不懈的毅力。 那如何学习开发小程序呢?互联网时代的好处就是资源的共享,这改变了我们的生活,当我们想要了解学习某一事物的时候,我们可以打开某搜索引擎进行搜索就能获得很多有用的资源(ps:虽然广告太多了),下面分享一些我学习的经验和资源 学习资源 在那个寒假里我一直想着开发一个自己的小程序,但是当时就是一个白的不能再白的小白!哈哈,啥也不会。于是我便开始各种的在网上找学习教程什么的,这里想说得是学习不仅要有资料还要找到适合自己方法,这很重要。我个人的学习历程如下 [图片] 阅读资料 阅读的资料首推的肯定是官方文档,能把文档读完,其实也差不多了,其他的阅读资料我这收集的不是很多 文档 一篇博客 视频资源 关于视频教学资源,我其实只想推荐一个网站哔哩哔哩,B站真的是一个神奇的网站,”我在B站学编程“ 真的不是一句玩笑😋 哔哩哔哩-搜索微信小程序 自选 我看过的视频 【微信小程序】从入门到入土课程 【微信小程序】腾讯工程师教你开发 微信小程序腾讯官方学习课程-学做小程序 云开发教程 小程序·云开发——私房书柜项目实战 工欲善其事必先利其器 在我们学习了一些小程序的知识的时候,我们就可以到实践阶段了,但 “工欲善其事必先利其器”。作为程序员都有自己最喜欢最趁手的一个IDE,小程序开发也是一样,小程序官方也给开发者准备了一个非常出色的编辑器[代码]微信开发者工具[代码],在我看来它的功能是很强大的,有很多的优点,但是就像人一样没有人十全十美,就目前的开发者工具来说还是有一些缺点,但是官方也一直在改进,这是值得大家期待和赞扬的。 优点 自带模拟器 代码提交方便 云开发 缺点(有些收集,也有个人看法) 工具经常出现莫名的bug,偶尔黑屏 偶尔会有粘贴复制失效(截至目前未发现了) 代码提示较少 作为新手开发者来说,使用微信开发者工具来开发小程序已经是绰绰有余,但是现在对于有了一些经验的开发者来说他们的追求的可能更多的是编程的舒适感,所以现在主流的小程序开发是使用[代码]VScode[代码]+[代码]微信开发者工具[代码]的组合。我在最初学习的小程序的时候一直使用的微信开发者工具,它足以满足我所有的需求,但当我尝试了这个组合之后我觉得有必要推荐一波 推荐理由: VScode拥有极其丰富的插件库,通过这些插件可以提高我们编码的效率。 VScode现在基本上已经是主流编辑器了,所以学习使用它,对以后的工作来说也是有帮助的 VScode的风格也极其舒适 下载地址 VScode 微信开发者工具 VScode基本配置 使用VScode开始开发之路前,我们还需要进行一些配置,但是这里的配置也就是下载一些常用的插件而已,没有很难很复杂得配置,说到插件最常用的是如下几个,安装也很方便,只需要打开vscode的插件市场按名称搜索点击安装就行 minapp [代码]支持微信小程序标签、属性的智能补全, 并且提示中包含文档内容 (同时支持原生小程序、mpvue 和 wepy 框架,并提供 snippets)。 [代码] [图片] wechat-snippet [代码]这个插件主要的功能就是代码辅助, 代码片段自动完成,可以作为上个插件的补充。 [代码] wxml [代码]这款插件用于将wxml代码进行高亮显示, 并且提供代码格式化的功能,可将代码格式化为较易阅读的样式 [代码] 不用担心模拟器的预览问题,当微信开发者工具和VScode里面的项目目录一样的时候,当你在VScode里面保存之后,微信开发者工具的模拟器会自动编译 tips:将微信开发者的代码编辑器关闭把模拟器调大一点效果更佳 界面的开发 基础 详细看过文档和看过教学视频后,相信都明白了小程序的页面构成,这里我要说的主要是需要打牢基础,可以去模仿一些好看的小程序界面,强调的是在这个阶段要自己写代码,不可以CV大法😂,也可以在github上面找一些别人开源的小程序,边自己写,边对着看,有效果的。下面是github一些开源的小程序 私房书柜 微信小程序开发资源汇总 微信小程序开源项目库汇总 后两个项目真的非常不错哦!!基本上有这两个就够了 进阶 当我们有了良好的基础之后,为了开发效率,我们可以使用非常优秀的轮子来帮我们快速的开发出漂亮的界面,我收集的好用的UI框架有这几个,也是我用过的,当然还有很多好的框架,我只是没列出来,用心的小伙伴可以自己去发现哦 WeUI Vant Weapp ColorUI 后端的开发 [图片] 因为本人还处在学习的阶段,后端知识还没有学到,所以这节我不会分享一些主流的后端知识(不会😭),我只是想安利云开发 图源互联网,侵删 云开发 对于新手来说我真的是无脑安利云开发,它给了我们很多的可能。对于高校学生来说开发一个项目,最大的拦路虎可能就是后端,可能租不起服务器,域名问题等等。 而小程序云开发帮我们解决了很多的问题,开通云开发后我们无需去思考服务器的问题,而且还帮我们准备了数据库,我们可以通过云开发的后台可视化界面轻松的管理我们的小程序。虽然云开发是有流量限制的,但是这足以让我们去学习,和创造一些有意思的小程序了。 给我带来了什么 于我个人而言,我的两个小程序都是云开发做的,如果没有云开发可能我的小程序之旅只能止步前端界面了,即使我拥有了服务器,域名,数据库等等,我可能也做不出来一个小程序,因为正如我上面所说的那样,我不会后端技术。就像做饭,有了一堆的好食材,但是却不会做,那也只能眼睁睁的看着。如下是我的感受 体验感 成就感 知识 的确当你做出一个作品的时候你的内心是极有成就感的,这在其中你也会收获一些你还没接触到的知识 为什么用?用什么云? 关于为什么安利云开发,我总结了几点,这些是我觉得的云开发的优点,首先就是免费,这无疑是最cool的一个点,不至于让很多人倒在第一道坎上,其次是它的简单,当一群非常优秀的程序员帮你写好了一堆的API 你只需要调用的时候,的确很爽。云函数,强大的云函数能帮你实现很多必须用服务器才能实现的业务逻辑,而且有了云函数会使你获取openid更加的便捷(此功能只限于微信官方的云开发) 免费 简单 云函数 可视化的管理界面 当然如果开发的项目很大业务逻辑很复杂的话还是需要独立的服务器 关于用什么云 小程序自带的云开发 Bmob云开发 以上两个云是我使用过的,第一个官方的云开发很好用,也不赘述了。第二个是Bmob我觉得也是很赞的一个云,提供免费版,数据库的操作也是很简单,文档也很详细,能够极速上手(绝无广告之嫌,单纯推荐)。 一点点使用经验 当你需要时可以选一个,但我的经验是可以一起使用,当你需要获取用户唯一标识的时候就可以使用官方的云开发提供的云函数,方便快捷,存数据用Bmob,因为当你需要做一些比较复杂的增删改查的时候Bmob的体验要好一点 结语 最近在看《史蒂夫.乔布斯传》,有一句非常著名的苹果广告语 “那些疯狂到以为自己能够改变世界的人,才能真正的改变世界。” 共勉,加油!😬
2020-02-21 - 两种渲染与数据绑定+swiper组件
前言 关于这一篇为什么换标题了?其实这一篇还是我的入门教程系列文章,因为涉及的东西有点多不好起名字,所以我暂时改一个标题😝,这一篇讲的是小程序的数据绑定,和两种特殊的渲染以及<swiper>组件,这一篇讲的东西可以说是很重要了,特别是数据绑定。它可以帮我们将从后台得到的数据绑定到你要展示的位置,所以这些学好,又离我们写一个好的小程序的小目标近了一步,ok,学起来。 数据绑定 数据绑定是很好理解的,通俗一点就是,我想动态的在这展示我的数据,那么我就在这用一个标记把这里标记起来,然后我通过这个标记把数据放过来。没错就是这么简单,语法也简单,就是用两个大括号将变量包裹起来,像这样<view>{{ message }}</view>,这样就声明了这需要一个叫message的数据,那这个数据哪里来?这和 js 中的data有关,可以说这里是我们定义数据的地方,在这可以给标记赋值,然后当页面启动的时候,框架就会知道将data中的数据方法对应的标记位置。当我们想在一个函数中动态传一些数据的话,就是可以使用**this.setData({ })**方法来实现,数据绑定还有很多的玩法建议看文档。下面举一个简单的例子 wxml代码 [代码]<view>你好!!</view> <view>{{value}}</view> [代码] js代码 [代码]Page({ data: { value:"你好小程序" } }) [代码] 效果图 [图片] 条件渲染 wx:if 是小程序的一个控制渲染的属性,也就是符合条件的才会渲染,而不符合条件的就不渲染,它的用法就是<view wx:if="{{condition}}"></view>,判断的条件要写在双引号里面,可以是静态的值,也可以是数据绑定的动态值,也可以是带有判断运算符的值,因为 wx:if 是一个控制属性,需要将它添加到一个标签上。如果要一次性判断多个组件标签,可以使用一个 <block/> 标签将多个组件包装起来,并在上边使用 wx:if 控制属性。(<block/> 并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。引文档)它还有两个兄弟wx:elif 和 wx:else ,这和一些语言的 if 三件套使用是很接近的,意思是一样的,那就不多说了,总之符合条件才会渲染。 wxml代码 [代码]<!--代码来自文档--> <view wx:if="{{condition}}">True</view> <view wx:if="{{length > 5}}">1</view> <view wx:elif="{{length > 2}}">2</view> <view wx:else>3</view> <block wx:if="{{true}}"> <view>view1</view> <view>view2</view> </block> [代码] js代码 [代码]Page({ data: { condition:true } }) [代码] 效果图 [图片] 列表渲染 wx:for 是一个很实用的属性,举个例子,在web页面里面你想要展示三个大小一样的盒子,是不是要复制两次代码,有了wx:for就不用这么麻烦了,直接在view上加上这个属性,循环3次框架就会渲染出三个一样的view,在组件上使用 wx:for 控制属性绑定一个数组(也可以是数值),即可使用数组中各项的数据重复渲染该组件。默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item 如果你不想用默认的,你可以使用 wx:for-item 可以指定数组当前元素的变量名,使用 wx:for-index 可以指定数组当前下标的变量名,wx:for的例子就不举了,文档关于这块写的很好 这里提一下为什么要把swiper组件连在一起讲呢!在swiper组件中会用到这些内容,所以既可以当作练习以上内容,顺便也就把swiper学了 swiper组件 讲真,小程序的组件真的是很方便,如果要是在web端实现一个轮播图那是要费点力气啊!但是通过<swiper>和<swiper-item>(注:swiper-item只能在swiper里面使用)的配合,再加上<swiper>组件的一些属性,轻松就能生成一个漂亮美观的轮播图。当你需要自动滚动的时候只需要设置autoplay为true就行,要设置滚动时间的话只需要使用interva属性就行,lindicator-dots可以控制要不要面板指示点。属性比较多,所以建议看文档。 wxml代码 [代码]<!--我这没有设置滚动时间,默认5秒--> <swiper class="swiper_" autoplay='true' indicator-dots="true" indicator-active-color='orange'> <swiper-item wx:for="{{imgUrls}}" wx:key> <image src='{{item}}'></image> </swiper-item> </swiper> [代码] js代码 [代码]Page({ data: { imgUrls: [ 'https://images.unsplash.com/photo-1551334787-21e6bd3ab135?w=640', 'https://images.unsplash.com/photo-1551214012-84f95e060dee?w=640', 'https://images.unsplash.com/photo-1551446591-142875a901a1?w=640' ] } }) [代码] 效果图 [图片] 结语 谢谢大家的支持!
2019-08-04 - 【小程序学习记录】-icon & rich-text组件
前言 今天星期五啦,终于腾出时间来干自己喜欢的事情了,昨天没有续上,今天接着补上,现在是开始阶段我讲的东西都很简单,所以今天的组件也是很简单的两个组件,<icon>组件和<rich-text>组件。 便捷的<icon> <icon>组件是小程序提供给我们表示图标的,在一个项目中图标是常出现的东西,在HTML中我们要表示一个图标,是很麻烦的,我们要去找图,还要写css样式以达到我们的需求。但是在小程序中就便捷了很多,<icon>提供了一个type属性,只需要提供有效值就可以在页面上显示一个与有效值对应的图标,(有效值:success, success_no_circle, info, warn, waiting, cancel, download, search, clear) 并且大小也不需要我们多操心,<icon>有一个默认的大小23px,一般情况下默认大小是够用的,如果需要修改,那也很简单,只需要给 size 属性一个你想要的大小就行了,噢,对了,还有颜色嘛,这可是很重要的,一个适合的颜色,能够让界面好看很多。这点小程序早想到了,color属性就是帮我们改变icon的颜色的。这些文档比我说的明白,说了这么多,如何使用呢???咱们来看代码 wxml代码 [代码]<!--代码来自文档--> <view class="group"> <block wx:for="{{iconSize}}"> <!--体现大小--> <icon type="success" size="{{item}}" /> <!--icon比较特殊是一个没有闭合标签的组件--> </block> </view> <view class="group"> <block wx:for="{{iconType}}"> <!--体现有效值样式--> <icon type="{{item}}" size="40" /> </block> </view> <view class="group"> <block wx:for="{{iconColor}}"> <!--体现颜色--> <icon type="success" size="40" color="{{item}}" /> </block> </view> [代码] js代码 [代码]Page({ data: { iconSize: [20, 30, 40, 50, 60, 70], iconColor: [ 'red', 'orange', 'yellow', 'green', 'rgb(0,255,255)', 'blue', 'purple' ], iconType: [ 'success', 'success_no_circle', 'info', 'warn', 'waiting', 'cancel', 'download', 'search', 'clear' ] } }) [代码] 截图 [图片] <rich-text>组件 这个组件的作用就是帮我们在页面上展示一些HTML的节点,主要是两种类型的节点,一是文本节点,二是元素节点,默认的是是元素节点,但是也可以通过type属性来选择,type = "node"是元素节点,text 是文本节点,文本节点其实没什么好说的,详情见文档 。<rich-text>有一个很重要的属性是nodes 这个属性官方推荐使用array类型,通过这个nodes属性我们可以给它一个数组,这个数组里面有三个属性,分别是name,attrs,children。分别是HTML节点的名称,是个数组,可以放这个节点相关属性,children是它的子节点。引一段官方的代码 wxml代码 [代码]<rich-text nodes="{{nodes}}" bindtap="tap"></rich-text> [代码] js代码 [代码]// rich-text.js Page({ data: { nodes: [{ name: 'div', attrs: { class: 'div_class', style: 'line-height: 60px; color: red;' }, children: [{ type: 'text', text: 'Hello World!' }] }] }, tap() { console.log('tap') } }) [代码] 截图 [图片] 结束 文档搬运工!打扰了,各位
2019-08-04 - 了解用Bmob云函数获取openid
前言 在开发一些小程序应用的时候为了实现一些特定的业务功能,需要确定是否是同一用户,例如:我们做了一个可以发帖的小程序,A用户发了一篇帖子,B用户也发了一篇帖子,如果没有唯一标识来确认哪个帖子属于谁的话,那么整个就会乱套。而在小程序中openid是用户的唯一标识,也就是说我们可以通过保存openid来知道帖子到底属于谁。但是openid对于用户来说也是很重要的一个信息,所以小程序对于开发者获取openid是比较谨慎的。在了解了openid之后,说说Bmob Bmob是一个做的非常好的云服务平台,提供很棒的后端服务,而且我觉得它非常的便捷,让我们的开发变的简单起来,特别是对于学生党来说,Bmob的免费版真的是一个福音,当然微信小程序官方的云开发也是非常的不错,(这主要和我当时别人推荐的选择有关)简单解释之后言归正传—如何通过Bmob获取openid 其实获取openid不用这么麻烦的,但是想体验一下云函数就试了一下,可以算是经验分享一下 获取流程 获取流程可以先看看官方的这张图 [图片] 这张图告诉我们获取openid的流程粗略的概括为四步 调用wx.login()接口获取到code (code每次都会变) 在通过wx.request()接口将code发送给开发者的服务器 开发者服务器再将登陆凭证和code等信息发送给微信服务器 验证后返回openid 获取的接口 临时登录凭证校验接口其实是一个 HTTPS 接口,开发者服务器可以向通过该接口使用 临时登录凭证code 获取 session_key 和 openid 等。在新的文档里面好像没有看到此接口(有可能是我不细心,各位可以仔细看看) 接口地址 [代码]https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code [代码] 请求参数 [图片] Bmob获取 这里要说的是Bmob的云函数是收费的,所以可以作为学习使用吧,如果有经济实力可以使用 首先通过Bmob的后台创建一个云函数getOpenId,代码示例 [代码]function onRequest(request, response, modules) { var code = request.body.code; var Secret = request.body.secret; var Appid = request.body.appid; var http = modules.oHttp; http( 'https://api.weixin.qq.com/sns/jscode2session?appid=Appid&secret=Secret&js_code=' +code+ '&grant_type=authorization_code', function (error, res, body) { response.send(body); }); } [代码] 其次调用wx.login()获取code,并声明一个对象,用来调用云函数 [代码]wx.login({ success(res) { if (res.code) { let params = { funcName: 'getOpenId', data: { code : res.code , secret : '**************' , appid : '**************' , } } Bmob.functions(params.funcName, params.data).then(function (response) { console.log(response); openid = response.openid; } } } }) [代码] 简单获取方法 直接使用wx.request()向接口发起请求获取openid,此方法需要将https://apiweixin.qq.com配置到安全域名列表里,示例代码 [代码]wx.login({ success: function (res) { console.log(res) if (res.code) { //发起网络请求 wx.request({ url: 'https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code', data: { appid:'****************', secret:'****************', js_code: res.code, grant_type: 'authorization_code' }, success(v){ console.log(v) } }) } else { console.log('登录失败!' + res.errMsg) } } }); [代码] 结语 其实没有必要使用云函数吧!使用简单的方法就好了,了解一下云函数还是很不错的,此文新手看就好,大佬轻喷…喜欢可以点个赞哦嘻嘻
2019-04-13 - 【小程序取值和传值】—你也可能遇到的坑系列
前言 小程序真的很好用,非常的便捷,并且我们可以很轻松的开发属于自己的一款小程序,但是在我们开发写代码的时候难免会遇到一些小坑,然后就是各种的疑问???我整理一些我遇到的坑,说不定你也遇到过哈哈哈 取值和传值 在我们开发一个程序的时候,大概率会涉及到要得到一些节点的值或是需要在页面跳转的时候传一些值过去以完成一些事情,我总结三点 普通的取值 页面传值 from表单取值 虽然看上去很简单但是偶尔会有一些小坑等着我们 普通的取值 通常情况下我们都是先给组件绑定事件,按照文档的说法,如无特殊说明,当组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象。 比如我们给一个组件绑定了一个点击事件叫getValue,然后我们有一个函数如下,其中event就是事件对象 [代码]<view bindtap="getValue"></view> [代码] [代码]getValue:function(event){ // do someing } [代码] 这个event事件对象是基本的对象事件他其中包括了一些属性,对于我们获得想要的值很重要 [图片] 🆗,到这之后我们可以知道target,和currentTarget对我们很重要了,所以接下来继续看看文档给了我们什么信息 target [图片] currentTarget [图片] 初看可能觉得两个没什么区别,但是这就是坑啊,这两个是有一些区别的,target是触发事件的源组件,也就是说你在button上绑定一个事件那么target就是指向这个button不会变的,而currentTarget就不一样,它指向的是触发事件监听的对象, 注意理解 触发事件监听”的对象与“添加(注册)监听事件”的对象是不一样的!前者是能够触发该事件但没有绑定事件,后者指绑定了事件 填坑 1、如果绑定的事件所在组件没有子元素,则用e.target===e.currentTarget一样; 2、如果事件绑定在父元素中,且该父元素有子元素,当用e.currentTarget时,不管点击父元素所在区域还是子元素(当前事件),都正确执行,若用e.target时,点击父元素所在区域无错,点击子元素区域,执行报错,报错的原因是事件没绑定在子元素上,是在父元素上,子元素要用e.currentTarget才正确 上面内容中有引用此博客 https://blog.csdn.net/syleapn/article/details/81289337 官方文档传送 https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html 上面说了很多,但是还没有说到底怎么取值,其实看完上面也差不多了,下面需要提的是dataset 文档的解释: 在组件中可以定义数据,这些数据将会通过事件传递给 SERVICE。 书写方式: 以data-开头,多个单词由连字符-链接,不能有大写(大写会自动转成小写)如data-element-type,最终在 event.currentTarget.dataset 中会将连字符转成驼峰elementType。 简单的理解就是我们可以把想要获取的值通过data-定义到组件上,当我们在触发一个事件的时候我们可以通过event.currentTarget.dataset.XX来获取 [代码]// 像这样 <view> <button bindtap='info' data-value='5555'>点击</button> </view> [代码] [代码]// e和event等价 info:function(e){ console.log('value:'+e.currentTarget.dataset.value) } [代码] 结果:当我们点击按钮的时候就可以得到我们预先定义好的值了 页面传值 页面传值的使用还是比较多的,特别是我们需要做一些详情页面的时候,经常涉及到需要将上一个页面的某一些值带到第二个页面来。这个坑要少一点,我们一步一步来说吧 首先说跳转吧 仅有两种,其他方法欢迎讨论 wx.navigateTo(Object object) navigator 传参数 虽然有两种跳转的方式,但是它们传递参数的方式是一样的,url后拼接?id(参数名字)=要传递的值 注意 (如果多个参数用&分开 &name=value&…….) [代码]<navigator url="pages/detail/detail?value="123"> 跳转 </navigator> [代码] [代码]wx.navigateTo({ url:"pages/detail/detail?value="123", success: function (res) { // success }, fail: function () { // fail }, complete: function () { // complete } }) [代码] 获得参数 在跳转到的界面的一些生命周期的函数中有一个options,它是包含url地址中参数的对象,可以通过它直接点获取。 [代码]onLoad:function(options){ console.log(options.value) } [代码] from表单取值 这里首先要铺垫一下deatil deatil是event事件对象的一个属性,它包括一些额外的信息 ok,接下来要说取值了,常规的做法是通过 <form bindsubmit=“formSubmit”> 与 <button formType=“submit”> 标签配合使用,然后给input一个name属性,我们在js中就可以使用e.deatil.value.name来获取了 [代码]<form bindsubmit="formSubmit"> <input name="detail" placeholder="详情地址" /> <input name="realname" placeholder="收件人姓名" /> <input name="mobile" placeholder="手机号码" type="number"/> <button formType="submit" type="primary">Submit</button> </form> [代码] 对新手来说这里有一点小坑,新手可能会有疑惑 为什么没有给button绑定事件呢,是不是需要在绑定一个事件,其实不用,<form bindsubmit=“formSubmit”> 已经绑定了事件,我们在 js 中只需要写一个叫 formSubmit 的函数就好了 [代码]formSubmit: function(e) { // detail var detail = e.detail.value.detail; // realname var realname = e.detail.value.realname; // mobile var mobile = e.detail.value.mobile; } [代码] 此处有引用 https://www.cnblogs.com/lrgupup/p/7609118.html 结语 好记性不如烂笔头,记录一下自己犯过的一些小错❤
2020-03-24 - 借助小程序云开发实现小程序支付功能(含源码)
我们在做小程序支付相关的开发时,总会遇到这些难题。小程序调用微信支付时,必须要有自己的服务器,有自己的备案域名,有自己的后台开发。这就导致我们做小程序支付时的成本很大。本节就来教大家如何使用小程序云开发实现小程序支付功能的开发。不用搭建自己的服务器,不用有自己的备案域名。只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1,云开发的部署和使用 2,支付相关的云函数开发 3,商品列表 4,订单列表 5,微信支付与支付成功回调 支付成功给用户发送推送消息的功能会在后面讲解。 下面就来教大家如何借助云开发使用小程序支付功能。 支付所需要用到的配置信息 1,小程序appid 2,云开发环境id 3,微信商户号 4,商户密匙 一,准备工作 1,已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中,【设置】 →【开发设置】 下可以获取微信小程序 AppID 和 Secret。 [图片] 2,微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中,【账户中心】→【商户信息】 下可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 下可以设置商户密钥。 [图片] 这里特殊说明下,个人小程序是没有办法使用微信支付的。所以如果想使用微信支付功能,必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3,微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4,开通小程序云开发功能:https://edu.csdn.net/course/play/9604/204526 二,商品列表的实现 效果图如下,由于本节重点是支付的实现,所以这里只简单贴出关键代码。 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view> </view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息,然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看我的历史博客,有写过的) 也有视频讲解: https://edu.csdn.net/course/detail/9604 [图片] 三,支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解。 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1,配置config下的index.js, 这一步所需要做的就是把小程序appid,云开发环境ID,商户id,商户密匙。填进去。 [图片] 2,配置入口云函数 [图片] 详细代码如下,代码里注释很清除了,这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init() const app = require('tcb-admin-node'); const pay = require('./lib/pay'); const { mpAppId, KEY } = require('./config/index'); const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res = require('./lib/res'); const ip = require('ip'); /** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */ exports.main = async function(event, context) { const { type, data, userInfo } = event; const wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database(); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order'); // 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo = `${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; //'127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spbill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }) .get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了。 [图片] 再来看下,支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表,支付状态,支付成功后的回调。 今天就先讲到这里,后面会继续给大家讲解支付的其他功能。比如支付成功后的消息推送,也是可以借助云开发实现的。 由于源码里涉及到一些私密信息,这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言。单独找我要源码也行(微信2501902696) 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 浅谈前端/软件工程师的代码素养
“程序是写给人读的,只是偶尔让计算机执行一下。” ——Donald Ervin Knuth(高德纳) 关于代码素养 我们常常谈到“素养”一词,是指个人在专业领域内实践训练而成的一种修养,在不同的领域中有不同的体现,如在音乐领域中,“音乐素养”是指个人对于音乐的感觉程度,对音高节奏的把控,对不同流派音乐的鉴赏能力等,而在编程领域,也有不同的素养,反映出对基本功、代码整洁度、专业态度等等方面,所谓“代码素养”,简单来说,就是指代码写的是否优雅美观可维护。 绝对完美的代码是不存在的,代码素养并不是指完美主义。在翻译领域有“信,达,雅”的标准,“雅”之所以放在最后,是因为要达到它,需要有比较高的水准和经验积累。类比到编程领域,我们在编程时,第一时间想到的是如何将业务逻辑实现出来,而不是如何把代码优雅地写出来,所以写代码没有所谓的绝对优雅。但是,作为一名专业的前端工程师,确切的说,应该是专业的软件工程师,编写优雅的代码应当是时刻保持的追求,它更像是一个准绳,如同每个人知道自己该做什么,不该做什么,所谓原则,所谓底线,体现出所谓的[代码]“代码素养”[代码]。 破窗理论 破窗理论,原义指窗户破损了,建筑无人照管,人们放任窗户继续破损,最终自己也参与破坏活动,在外墙上涂鸦,任垃圾堆积,最后走向倾颓。 破窗理论在实际中非常容易出现,往往第一个人的代码写的不好,第二个人就会有类似“反正他已经写成这样了,那我也只能这样了”的思想,导致代码越维护越冗杂,最后一刻轰然坍塌,变成无人想去维护的垃圾。 整洁的代码 整洁的代码如同优美的散文,试想读过的一本好书,能够随着作者的笔锋跌宕起伏,充满了画面感,调动了自己的喜怒哀乐。代码虽然没有那样的高潮迭起,但整洁的代码应当充满张力,能够在某一时刻利用这种张力将情节推向高潮。 我更喜欢把写代码类比于写文章讲故事,写代码是创作的过程,作者需要将自己想表达的东西通过代码的形式展现出来,而整洁的代码如同讲故事一般,娓娓道来,引人入胜,不好的代码则让人感觉毫无头绪,通篇不知道在讲什么。 整洁代码原则 在现代化的前端开发中,有很多自动化工具可以帮助我们写出规范的代码,如[代码]eslint[代码],[代码]tslint[代码]等各种辅助校验工具,知名的规范如[代码]google规范[代码]、[代码]airbnb规范[代码]等等也从各个细节方面约束,帮助我们形成合理规范的代码风格。 本小节不再重复语言层面的代码风格,根据实际重构项目,总结出一系列开发过程中需要时刻注意的原则,按照重要程度优先级排列。 1. DRY(Don’t Repeat Yourself) 相信作为一名软件工程师,大家都听说过最基本的DRY原则,很多设计模式,包括面向对象本身,都是在这条原则上做努力。 DRY顾名思义,是指“不要重复自己”,它实际上强调了一个抽象性原则,如果同样或类似的代码片段出现了两次以上,那么应该将它抽象成一个通用方法或文件,在需要使用的地方去依赖引入,确保在改动的时候,只需调整一处,所有的地方都改变过来,而不是到每个地方去找到相应的代码来修改。 在实际工作中,我见过两种在这条原则上各自走向极端的代码: 一种是完全没有抽象概念,重复的代码散落在各处,更奇葩的是,有一部分的抽象,但更多的是重复,比如在common下抽取了一个[代码]data.js[代码]的数据处理文件,部分页面中引用了该文件,而更多页面完全拷贝了该文件中的几个不同方法代码。而作者的意图则是令人啼笑皆非——只用到小部分代码,没必要引入那么整个文件。且不论现代化的前端构建层面可以解决这个问题,即使是引入了整个大文件,这部分多余的代码在gzip之后也不会损失多少性能,但这种到处copy的行为带来后续的维护成本是翻倍的。 对于这种行为还遇到另外一个理由,就是工期时间短,改不动之前的代码,怕造成外网问题,那就拷贝一份相同的逻辑来修改。比如支付逻辑,原有的逻辑为单独的UI浮层+单个支付购买,现在产品提出“打包购买”需求,原有的代码逻辑又比较复杂,出现了“改不动”的现象,于是把UI层和购买逻辑的几个文件整个拷贝过来,修改几下,形成了新的“打包购买”模块,后来产品又提出“按条购买”,按照上述”改不动“原则,又拷贝了一份“按条购买”的模块。这样一来调用处的逻辑就会冗余重复,需要根据不同的购买方式引入不同UI组件和支付逻辑,另外如果新添需求,如支持“分期付款”,那么将改动的是非常多的文件,最可悲的是,最后想要把代码重构为一处统一调用的人,将会面对三份“改不动”的压力,需要众多逻辑中对比分析提取相同之处,工作量已经不能用翻倍来衡量,而这种工作量往往无法得到产品的认同和理解。 另一种极端是过度设计,在写每个逻辑的时候都去抽象,让代码的可读性大大下降,一个简单的for循环都要复用,甚至变量定义,这种代码维护起来也是比较有成本的,还有将迥然不同的逻辑过度抽象,使得抽象方法变得非常复杂,经常“牵一发而动全身”,这种行为也是不可取的。 这也是将该原则排在首位的原因,这种行为导致的重构工作量是最大的,保持良好的代码维护性是一种素养,更是一种责任,如果自己在这方面逃避或偷懒,将把这块工作量翻倍地加在将来别人或自己的身上。 2. SRP(Single Responsibility Principle) SRP也是一个比较著名的设计原则——单一职责,在面向对象的编程中,认为类应该具有单一职责,一个类的改变动机应当只有一个。 对于前端开发来说,最需要贯彻的思想是函数应当保持单一职责,一个函数应当只做一件事,这样一来是保证函数的可复用性,更单一的函数有更强的复用性,二来可以让整体的代码框架更加清晰,细节都封装在一个个小函数中。另外一点也和单一职责有关,就是无副作用的函数,也称纯函数,我们应当尽量保证纯函数的数量,非纯函数是不可避免的,但应当尽量减少它。 把SRP原则排在第二位,因为它非常的重要,没有人愿意看一团乱麻的逻辑,在维护代码时,如果没有一个清晰的逻辑结构,所有的数据定义、数据处理、DOM操作等等一系列细节的代码全部放在一个函数中,导致这个函数非常的冗长,让人本能地产生心理排斥,不愿去查看内部的逻辑。 所有的复杂逻辑放在一个函数中,相信大家看到这样的代码都会眉头一皱: [代码]show: function(a, b) { if (!isInit) { init(); isInit = true; } // reset this.balance = 0; this.isAllBalance = false; var shouldShowLayer = true, preSelectedTermId = 0, needAddress = course.address_state, showTerms, termsObj; var hasPunish = false; this.course = course = course || {}; opt = opt || {}; opt.showMax = opt.showMax || 6; (isIosApp || b.isIAP) && (usekedian = !0, priceSymbol = '<i class="icon-font i-kedian"></i>'), f.splice(b.showMax), layer.show({ $container:b.$container, content:termSelectorTpl({ terms:f, curTermId:b.curTermId || d, name:a.name, hasPunish:h, userInfo:j }, { renderTime:T.render.time.renderCourseTime, renderCourseTime:renderCourseTime, hideUserInfo:b.hideUserInfo, hideTitle:b.hideTitle, hidePayPrice:b.hidePayPrice, confirmText:b.confirmText, sys_time:a.sys_time }), cls:"term-select-new", allowMove:function(a) { return opt.allowMove || ($target.closest('.select-content').length && $('.term-select-new .select-time').height() + $('.term-select-new .select-address').height() + $('.term-select-new .select-discounts').height() > (winWidth > 360 ? 190 : winWidth > 320 ? 175 : 150)); }, afterInit:function(c) { if (needAddress) { that.loadAddress(); // 如果需要地址,且是 app 的话,屏幕可见性切换时需要更新下地址 if (isApp) { $(document).on(visibilityChange, function (e) { // console.log('visibilityChange',document[hidden]); if (!document[hidden]) { // true 参数表示必须刷新 that.loadAddress(true); } }); } } that.afterTermSelect(); $dom.on('click', '.layer-close', function() { setTimeout(function() { !opt.noAutoHide && layer.hide(); }, 100); opt.onCancel && opt.onCancel(); }); $dom.on('click', '.term', function(e) { var $this = $(this); var $terms = $('.term'); if (!$this.hasClass('disabled')) { $terms.removeClass('selected'); $this.addClass('selected'); } that.afterTermSelect(); }); $dom.on('click', '.layer-comfirm', function(e) { var $this = $(this); var termId = $dom.find('.term.selected').data('term-id'); var termName = $dom.find('.term.selected').find('.term-title').html(); var discountId = $dom.find('.discounts-list_item.selected').data('discount-id'); var couId = $dom.find('.discounts-list_item.selected .discounts-coupon').data('cou-id'); var directPay = false; // ios 手Q IAP if (that.toRecharge) { // 需要充值的金额数目 var toRechargePrice = that.curPrice - that.balance; if (isIosApp) { require.async('api', function (api) { api.invoke('api', 'balanceRecharge', { amount: toRechargePrice }); // 充值完成设置回调 api.addEventListener('balanceRechargeCallBack', function(data) { // 支付成功的话 // code=0为成功,其他表示失败 // mode=1表示走充值档位回调,2表示直接充值回调,如果ios 直接充值成功则直接支付 var directPay = data.code === 0 && data.mode === 2; // 执行回调刷新数据 that.toGetBalance(that.course, termId, function() { directPay && $this.trigger('click'); }); }); }); } else { var toRechargePrice = that.curPrice - that.balance; if (that.rechargeMap && Object.keys(that.rechargeMap).indexOf("" + toRechargePrice) > -1) { that.opt.onComfirmClick && that.opt.onComfirmClick(1); iosPay.iosRecharge({ productId: that.rechargeMap[toRechargePrice], count: toRechargePrice, succ: function() { that.toGetBalance(that.course, $('.term.selected').data('term-id')); } }); } else { that.opt.onComfirmClick && that.opt.onComfirmClick(2); // T.jump('/iosRecharge.html?_bid=167&_wv=2147483651'); that.jumpPage('/iosRecharge.html?_bid=167&_wv=2147483651'); } } return; } if (!termId) { require.async(['modules/tip/tip'], function(Tip) { Tip.show(opt.dialogTitle); }); return true; } // check address if (needAddress && !that.addressid) { if (course.must_fill_mailing || !$dom.find('.select-address').hasClass('z-no')) { // 没填地址的话地址框要标红,然后需要滑到视窗让用户看到 var $cnt = $dom.find('.select-content'); var $addressWrap = $dom.find('.select-address_wrapper').addClass('z-err'); var cntRect = $cnt[0].getBoundingClientRect(); var addressBoxRect = $addressWrap[0].getBoundingClientRect(); // console.log('>>>>> ', cntRect, addressBoxRect); if (addressBoxRect.bottom > cntRect.bottom) { $cnt.scrollTop($cnt.scrollTop() + (addressBoxRect.bottom - cntRect.bottom)); } return; } } if (that.isAllBalance && that.opt.onComfirmClick) { that.opt.onComfirmClick(3); } opt.cb && opt.cb(termId, discountId, couId, termName, that.isAllBalance, that.payBalance, that.addressid); setTimeout(function() { !opt.noAutoHide && layer.hide(); }, 300); }); $dom.on('click', '.discounts-list_item', function(e) { var $this = $(this); var $discounts = $('.discounts-list_item'); var isSelected = $this.hasClass('selected'); if (!$this.hasClass('disabled')) { $discounts.removeClass('selected'); $this.addClass(isSelected ? '' : 'selected'); that.setPayPrice(); } }); $dom.on('click', '.address-person .i-edit2, .address-add', function() { var termId = $dom.find('.term.selected').data('term-id'); var courseId = that.course.cid; var src = '/addrEdit.html?_bid=167&_wv=2147483649&ns=1&fr=' + (location.pathname.indexOf('allCourse.html') > -1 ? 4 : location.pathname.indexOf('courseDetail.html') > -1 ? 2 : 3) + '&course_id=' + courseId + '&term_id=' + termId; // T.jump(src); that.jumpPage(src); }).on('click', '.select-address_title .i-right-light', function(e) { var $addressDom = $dom.find('.select-address'); var isOpen = !$addressDom.hasClass('z-no'); if (isOpen) { $addressDom.addClass('z-no'); that.theAddressid = that.addressid; that.addressid = undefined; } else { $addressDom.removeClass('z-no'); that.addressid = that.theAddressid; } }); } }); } else { opt.cb && opt.cb(opt.curTermId || preSelectedTermId); } } [代码] 单一职责并不一定要通过很多函数来完成,也可以用分段达到目的,如同这样: [代码]show(data) { data && this.setData(data); const renderData = { data: this.data, courseData: this.data.courseData, termList: this.termList, userInfo: this.userInfo, addrList: this.addrList, isIAP: this.isIAP, balance: betterDisplayNum(this.balance), curPrice: betterDisplayNum(this.curPrice), curTermId: this.curTermId, discountList: this.discountList, curDisId: this.curDisId, jdSelectId: this.jdSelectId, curAddrId: this.curAddrId }; const formatters = { // formatters termFormatter, priceFormatter, okBtnFormatter, balanceFormatter, priceFormatterWithDiscount }; console.log('[render data]: ', renderData); const html = payLayerTpl(renderData, formatters); // 记录滚动条位置 this._setScrollTop(); // 防止重复append if (this.$view) { this.$view.replaceWith(html); } else { this.$container.append(html); } afterUIRender(() => { this.$view = $('.' + COMPONENT_NAME).show(); this._setContentHeight(); // 动态设置滚动区域的高度 this._restoreScrollTop(); // 恢复滚动位置 this._initEvent(); this._initCountDown(); // 限时折扣倒计时 }); } [代码] 虽然这个函数也没有维持单一职责,但通过“分段”的形式清晰的表明了内部的流程逻辑,这样的代码看起来就会比所有细节揉在一个函数中好很多。 对于单一职责来说,保持起来还是比较困难的,主要在于职责的拆分,有时过于细致的职责拆分也会给阅读带来比较大的困难,对于这种情况,还是拿写作来对比,单一职责相当于文章的一个“段落”,对于文章来说,每个段落都有它的中心思想,可以用一句话描述出来,如果你发现函数的中心思想很模糊,或者需要很多语言去描述它,那也许它已经有很多个职责该拆分了。 3. LKP(Least Knowledge Principle) LKP原则是最小知识原则,又称“迪米特”法则,也就是说,一个对象应该对另一个对象有最少的了解,你内部如何复杂都没关系,我只关心调用的地方。 保持暴露接口的简介易用性也是API设计的通用规则,在实际中发现了这样的一个UI组件: [代码]module.exports = { show: function(course, opt) { // 此处省略一堆逻辑 }, jumpPage: function(url) { // 此处省略一堆逻辑 }, afterTermSelect: function() { // 此处省略一堆逻辑 }, setPrice: function() { // 此处省略一堆逻辑 }, setBalance: function() { // 此处省略一堆逻辑 }, toGetBalance: function(course, curTermId, cb) { // 此处省略一堆逻辑 }, setDiscounts: function(course, curTermId, curPrice) { // 此处省略一堆逻辑 }, filterDiscounts: function(discounts, curPrice) { // 此处省略一堆逻辑 }, isSuitCoupon: function(cou, curPrice) { // 此处省略一堆逻辑 }, setPayPrice: function() { // 此处省略一堆逻辑 }, setTermTips: function(wording) { // 此处省略一堆逻辑 }, loadAddress: function(needUpdate) { // 此处省略一堆逻辑 }, setAddress: function(addressid) { // 此处省略一堆逻辑 } } [代码] 这个UI组件暴露了非常多的方法,有业务逻辑,有视图逻辑,还有工具方法,这时会给维护者带来比较大的困扰,本能的以为这些暴露出去的方法都在被使用,所以想重构其中某些方法都有些不好下手,而实际上,外部调用的方法仅仅是[代码]show[代码]而已。 一个好的封装,无论内部多么复杂,它暴露出来的一定是最简洁实用的接口,而内部逻辑是独立维护的,如上述代码,作为一个UI组件来说,提供最基本的[代码]show/hide[代码]方法即可,有必要时可加入[代码]update[代码]方法自更新,而无需暴露众多细节,造成调用者和维护者的困扰。 4. 可读性基本定理 可读性基本定理——“代码的写法应当使别人理解它所需的时间最小化”。 代码风格和原则不是一概而论的,我们经常需要对一些编码原则和方案进行取舍,例如对于三元表达式的取舍,当我们觉得两种方案都占理时,那么唯一的评判标准就是可读性基本定理,无论写法多么的高超炫技,最好的代码依旧是让人第一时间能够理解的代码。 5. 有意义的名称 代码的可读性绝大部分依赖于变量和函数的命名,一个好的名称能够一针见血地帮助维护者理解逻辑,如同写文章中的“文笔”,文笔优异者总能将故事娓娓道来,引人入胜。 不过要起好名称还是很难的,尤其是我们不是以英语为母语,更是添加了一层障碍,有些人认为纠结在名称上会导致效率变低,开发第一时间应该完成需求的开发。这样说并没有错,我们在开发过程中应当专注于功能逻辑,但不要完全忽视命名,所谓“文笔”是需要锻炼的,思考的越多,命名就会愈加的水到渠成,到后来也就不太会影响工作效率了。 在这里推荐鲍勃大叔提到的童子军规,每一次看自己的代码,都进行一次重构,最简单的重构便是改名,也许一开始觉得命名还比较贴合,但逻辑越写越不符合初始的命名了,当回顾代码时,我们可以顺手对变量和方法进行重新命名,现代编辑工具也很容易做到这一点。 文不对题的命名是最可怕的,如: [代码]function checkTimeConflict(opts) { if (opts.param.passcard || (T.bom.get('autopay') && T.bom.get('term_id'))) { selectToPay({ result: {} }, opts); } else { DB.checkTimeConflict({ param: { course_id: opts.param.courseId, term_id: opts.param.termId }, succ: function(data) { selectToPay(data, opts); }, err: function(data) { dealErr(opts, data); } }); } } [代码] 这个函数被命名为[代码]check*[代码]开头的,本意是检测课程时间是否冲突,但内部逻辑却包含了支付整个流程,此时对于调用者来说,如果不去细看内部逻辑,很有可能就会错误的认为[代码]check[代码]函数没有副作用导致事故发生。 6. 适当的注释维护 注释是一个比较有争议性的话题,有人认为可读的函数变量就很清晰,不需要额外的注释,且注释有不可维护性,如: [代码]// 1-PC, 2-android手QH5, 3-android APP, 4-ios&非手QH5, 5-IOS APP var platform = isAndroidApp ? 3 : isIosApp ? 5 : 4; [代码] 实际上,这个字段的含义早已发生了改变,但由于修改者只修改了逻辑,并没有注意到这一行注释,导致这个老注释提供了错误信息,此时的注释不仅变成了无效注释,甚至会导致维护人的误解,造成bug的产生。 对于这种情况,要么维护注释,要么在注释里面注明接口文档,维护文档,在其他情况下,适当的注释是有必要的,对于复杂的逻辑,如果有一个简练的注释,对于代码可读性的帮助是极大的,但有些不必要的注释可以去掉,注释的取舍关键在于可读性基本定理,如: [代码]const filterFn = (term) => { if (rule.hideEndTerms && term.is_end) { return false; // 隐藏已结束的期 } if (rule.hideSignEndTerms && term.is_out_of_date) { return false; // 隐藏已结束报名的期 } if (rule.hideAppliedTerms && courseUtil.isTermApplied(term)) { return false; // 隐藏已报名的期 } if (rule.hideZeroAllowedTerms && courseUtil.isTermZero(term)) { return false; // 隐藏名额已满的期 } if (rule.productType === productType.PACKAGE) { return false; // 隐藏课程包的班级 } return true; }; [代码] 对于上述逻辑来说,虽然通过变量可以大致猜出功能含义,但一眼看上去就能清晰掌握逻辑结构,归功于注释的简明与清晰。 小结 本文提到的6个代码编写的原则,前三个偏向于代码维护性,后三个偏向于代码可读性,整个可维护性和可读性构成了代码的基本素养。作为一名前端开发工程师,想要拥有良好的代码素养,首先要让自己的代码可维护,不给别人的维护带来巨大的成本和工作量,其次尽量保证代码的美观可读,整洁的代码人见人爱,如同阅读一本好书,令人心情愉悦。 ”代码素养“是一种态度,真正热爱编程的程序员一定不会缺失“代码素养”。我们通常称“写代码”为[代码]“程序设计”[代码],而不是“程序编写”,“设计”一词体现出了我们的代码是一件作品,也许不如“艺术品”那么精致,但也不是什么粗麻烂布,如果在写代码时天马行空,得过且过,抱着只要能实现功能的思想,那这部“作品“是不具有观赏价值的,这不仅仅体现出代码编写者的”不专业”,更是反映出对待编程这件事的态度,代码的整洁程度、可维护性取决于你是否真正“在意”你的代码,每个程序员不一定热爱编程,但请你一定要以“认真”的态度对待自己的专业。 [代码]"clean code"[代码]的作者鲍勃大叔提到,有人曾送给他一条腕带,上面写着“Test Obsessed”,他发觉自己带上后再也无法取下了,不仅是因为腕带很紧,更是因为它也是一条精神上的紧箍咒。在编程时,我们下意识的看下自己的手腕,是否能发现一条隐形的腕带呢?
2019-03-14 - TypeScript入门完全指南(基础篇)
[TOC] 为什么JS需要类型检查 TypeScript的设计目标在这里可以查看到,简单概括为两点: 为JavaScript提供一个可选择的类型检查系统; 为JavaScript提供一个包含将来新特性的版本。 TypeScript的核心价值体现在第一点,第二点可以认为是TypeScript的向后兼容性保证,也是TypeScript必须要做到的。 那么为什么JS需要做静态类型检查呢?在几年前这个问题也许还会存在比较大的争议,在前端日趋复杂的今天,经过像Google、Microsoft、FaceBook这样的大公司实践表明,类型检查对于代码可维护性和可读性是有非常大的帮助的,尤其针对于需要长期维护的规模性系统。 TypeScript优势 在我看来,TypeScript能够带来最直观上的好处有三点: 帮助更好地重构代码; 类型声明本身是最好查阅的文档。 编辑器的智能提示更加友好。 一个好的代码习惯是时常对自己写过的代码进行小的重构,让代码往更可维护的方向去发展。然而对于已经上线的业务代码,往往测试覆盖率不会很高,当我们想要重构时,经常会担心自己的改动会产生各种不可预知的bug。哪怕是一个小的重命名,也有可能照顾不到所有的调用处造成问题。 如果是一个TypeScript项目,这种担心就会大大降低,我们可以依赖于TypeScript的静态检查特性帮助找出一个小的改动(如重命名)带来的其他模块的问题,甚至对于模块文件来说,我们可以直接借助编辑器的能力进行[代码]“一键重命名”[代码]操作。 另外一个问题,如果你接手过一个老项目,肯定会头痛于各种文档的缺失和几乎没有注释的代码,一个好的TypeScript项目,是可以做到代码即文档的,通过声明文件我们可以很好地看出各个字段的含义以及哪些是前端必须字段: [代码]// 砍价用户信息 export interface BargainJoinData { curr_price: number; // 当前价 curr_ts: number; // 当前时间 init_ts: number; // 创建时间 is_bottom_price: number; // 砍到底价 } [代码] TypeScript对开发者是友好的 TypeScript在设计之初,就确定了他们的目标并不是要做多么严格完备的类型强校验系统,而是能够更好地兼容JS,更贴合JS开发者的开发习惯。可以说这是MS的商业战略,也是TS能够成功的关键性因素之一。它对JS的兼容性主要表现为以下三个方面: 隐式的类型推断 [代码]var foo = 123; foo = "456"; // Error: cannot assign `string` to `number` [代码] 当我们对一个变量或函数等进行赋值时,TypeScript能够自动推断类型赋予变量,TypeScript背后有非常强大的自推断算法帮助识别类型,这个特性无疑可以帮助我们简化一些声明,不必像其他语言那样处处是声明,也可以让我们看代码时更加轻松。 结构化的类型 TypeScript旨在让JS开发者更简单地上手,因此将类型设计为“结构化”(Structural)的而非“名义式”(Nominal)的。 什么意思呢?意味着TypeScript的类型并不根据定义的名字绑定,只要是形似的类型,不管名称相不相同,都可以作为兼容类型(这很像所谓的duck typing),也就是说,下面的代码在TypeScript中是完全合法的: [代码]class Foo { method(input: string) { /* ... */ } } class Bar { method(input: string) { /* ... */ } } let test: Foo = new Bar(); // no Error! [代码] 这样实际上可以做到类型的最大化复用,只要形似,对于开发者也是最好理解的。(当然对于这个示例最好的做法是抽出一个公共的interface) 知名的JS库支持 TypeScript有强大的DefinitelyTyped社区支持,目前类型声明文件基本上已经覆盖了90%以上的常用JS库,在编写代码时我们的提示是非常友好的,也能做到安全的类型检查。(在使用第三方库时,可以现在这个项目中检索一下有没有该库的TS声明,直接引入即可) 回顾两个基础知识 在进入正式的TS类型介绍之前,让我们先回顾一下JS的两个基础: 相等性判断 我们都知道,在JS里,两个等号的判断会进行隐式的类型转换,如: [代码]console.log(5 == "5"); // true console.log(0 == ""); // true [代码] 在TS中,因为有了类型声明,因此这两个结果在TS的类型系统中恒为false,因此会有报错: [代码]This condition will always return 'false' since the types '5' and '"5"' have no overlap. [代码] 所以在代码层面,一方面我们要避免这样两个不同类型的比较,另一方面使用全等来代替两个等号,保证在编译期和运行期具有相同的语义。 对于TypeScript而言,只有[代码]null[代码]和[代码]undefined[代码]的隐式转换是合理的: [代码]console.log(undefined == undefined); // true console.log(null == undefined); // true console.log(0 == undefined); // false console.log('' == undefined); // false console.log(false == undefined); // false [代码] 类(Class) 对于ES6的Class,我们本身已经很熟悉了,值得一提的是,目前对于类的静态属性、成员属性等有一个提案——proposal-class-fields已经进入了Stage3,这个提案包含了很多东西,主要是类的静态属性、成员属性、公有属性和私有属性。其中,私有属性的提案在社区内引起了非常大的争议,由于它的丑陋和怪异遭受各路人马的抨击,现TC39委员会已决定重新思考该提案。 现在让我们来看看TypeScript对属性访问控制的情况: 可访问性 public protected private 类本身 是 是 是 子类 是 是 否 类的实例 是 否 否 可以看到,TS中的类成员访问和其他语言非常类似: [代码]class FooBase { public x: number; private y: number; protected z: number; } [代码] 对于类的成员构造函数初始化,TS提供了一个简单的声明方式: [代码]class Foo { constructor(public x:number) { } } [代码] 这段代码和下面是等同的: [代码]class Foo { x: number; constructor(x:number) { this.x = x; } } [代码] TS类型系统基础 基本性准则 在正式了解TypeScript之前,首先要明确两个基本概念: TypeScript的类型系统设计是可选的,意味着JavaScript就是TypeScript。 TypeScript的报错并不会阻止JS代码的生成,你可以渐进式地将JS逐步迁移为TS。 基本语法 [代码]:<TypeAnnotation> [代码] TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。 原始值类型 [代码]var num: number; var str: string; var bool: boolean; [代码] TypeScript支持三种原始值类型的声明,分别是[代码]number[代码]、[代码]string[代码]和[代码]boolean[代码]。 对于这三种原始值,TS同样支持以它们的字面量为类型: [代码]var num: 123; var str: '123'; var bool: true; [代码] 这类字面量类型配合上联合类型还是十分有用的,我们后面再讲。 数组类型 对于数组的声明也非常简单,只需要加上一个中括号声明类型即可: [代码]var boolArray: boolean[]; [代码] 以上就简单地定义了一个布尔类型的数组,大多数情况下,我们数组的元素类型是固定的,如果我们数组内存在不同类型的元素怎么办? 如果元素的个数是已知有限的,可以使用TS的元组类型: [代码]var nameNumber: [string, number]; [代码] 该声明也非常的形象直观,如果元素个数不固定且类型未知,这种情况较为罕见,可直接声明成any类型: [代码]var arr: any[] [代码] 接口类型 接口类型是TypeScript中最常见的组合类型,它能够将不同类型的字段组合在一起形成一个新的类型,这对于JS中的对象声明是十分友好的: [代码]interface Name { first: string; second: string; } var personName:Name = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 上述例子可见,TypeScript对每一个字段都做了检查,若未定义接口声明的字段(非可选),则检查会抛出错误。 内联接口 对于对象来说,我们也可以使用内联接口来快速声明类型: [代码]var personName:{ first: string, second: string } = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 内联接口可以帮助我们快速声明类型,但建议谨慎使用,对于可复用以及一般性的接口声明建议使用interface声明。 索引类型 对于对象而言,我们可以使用中括号的方式去存取值,对TS而言,同样支持相应的索引类型: [代码]interface Foo { [key:string]: number } [代码] 对于索引的key类型,TypeScript只支持[代码]number[代码]和[代码]string[代码]两种类型,且Number是string的一种特殊情况。 对于索引类型,我们在一般化的使用场景上更方便: [代码]interface NestedCSS { color?: string; nest?: { [selector: string]: NestedCSS; } } const example: NestedCSS = { color: 'red', nest: { '.subclass': { color: 'blue' } } } [代码] 类的接口 对于接口而言,另一个重要作用就是类可以实现接口: [代码]interface Point { x: number; y: number; z: number; // New member } class MyPoint implements Point { // ERROR : missing member `z` x: number; y: number; } [代码] 对类而言,实现接口,意味着需要实现接口的所有属性和方法,这和其他语言是类似的。 函数类型 函数是TypeScript中最常见的组成单元: [代码]interface Foo { foo: string; } // Return type annotated as `: Foo` function foo(sample: Foo): Foo { return sample; } [代码] 对于函数而言,本身有参数类型和返回值类型,都可进行声明。 可选参数 对于参数,我们可以声明可选参数,即在声明之后加一个问号: [代码]function foo(bar: number, bas?: string): void { // .. } [代码] void和never类型 另外,上述例子也表明,当函数没有返回值时,可以用[代码]void[代码]来表示。 当一个函数永远不会返回时,我们可以声明返回值类型为[代码]never[代码]: [代码]function bar(): never { throw new Error('never reach'); } [代码] callable和newable 我们还可以使用接口来定义函数,在这种函数实现接口的情形下,我们称这种定义为[代码]callable[代码]: [代码]interface Complex { (bar?: number, ...others: boolean[]): number; } var foo: Complex; [代码] 这种定义方式在可复用的函数声明中非常有用。 callable还有一种特殊的情况,该声明中指定了[代码]new[代码]的方法名,称之为[代码]newable[代码]: [代码]interface CallMeWithNewToGetString { new(): string } var foo: CallMeWithNewToGetString; new foo(); [代码] 这个在构造函数的声明时非常有用。 函数重载 最后,一个函数可以支持多种传参形式,这时候仅仅使用可选参数的约束可能是不够的,如: [代码]unction padding(a: number, b?: number, c?: number, d?: number) { if (b === undefined && c === undefined && d === undefined) { b = c = d = a; } else if (c === undefined && d === undefined) { c = a; d = b; } return { top: a, right: b, bottom: c, left: d }; } [代码] 这个函数可以支持四个参数、两个参数和一个参数,如果我们粗略的将后三个参数都设置为可选参数,那么当传入三个参数时,TS也会认为它是合法的,此时就失去了类型安全,更好的方式是声明函数重载: [代码]function padding(all: number); function padding(topAndBottom: number, leftAndRight: number); function padding(top: number, right: number, bottom: number, left: number); function padding(a: number, b?: number, c?: number, d?: number) { //... } [代码] 函数重载写法也非常简单,就是重复声明不同参数的函数类型,最后一个声明包含了兼容所有重载声明的实现。这样,TS类型系统就能准确的判断出该函数的多态性质了。 使用[代码]callable[代码]的方式也可以声明重载: [代码]interface Padding { (all: number): any (topAndBottom: number, leftAndRight: number): any (top: number, right: number, bottom: number, left: number): any } [代码] 特殊类型 any [代码]any[代码]在TypeScript中是一个比较特殊的类型,声明为[代码]any[代码]类型的变量就像动态语言一样不受约束,好像关闭了TS的类型检查一般。对于[代码]any[代码]类型的变量,可以将其赋予任何类型的值: [代码]var power: any; power = '123'; power = 123; [代码] [代码]any[代码]对于JS代码的迁移是十分友好的,在已经成型的TypeScript项目中,我们要慎用[代码]any[代码]类型,当你设置为[代码]any[代码]时,意味着告诉编辑器不要对它进行任何检查。 null和undefined [代码]null[代码]和[代码]undefined[代码]作为TypeScript的特殊类型,它同样有字面量的含义,之前我们已经了解到。 值得注意的是,[代码]null[代码]和[代码]undefined[代码]可以赋值给任意类型的变量: [代码]var num: number; var str: string; // 赋值给任意类型的变量都是合法的 num = null; str = undefined; [代码] void和never 在函数类型中,我们已经介绍了两种类型,专门修饰函数返回值。 readonly [代码]readonly[代码]是只读属性的修饰符,当我们的属性是只读时,可以用该修饰符加以约束,在类中,用[代码]readonly[代码]修饰的属性仅可以在构造函数中初始化: [代码]class Foo { readonly bar = 1; // OK readonly baz: string; constructor() { this.baz = "hello"; // OK } } [代码] 一个实用场景是在[代码]react[代码]中,[代码]props[代码]和[代码]state[代码]都是只读的: [代码]interface Props { readonly foo: number; } interface State { readonly bar: number; } export class Something extends React.Component<Props,State> { someMethod() { this.props.foo = 123; // ERROR: (props are immutable) this.state.baz = 456; // ERROR: (one should use this.setState) } } [代码] 当然,[代码]React[代码]本身在类的声明时会对传入的[代码]props[代码]和[代码]state[代码]做一层[代码]ReadOnly[代码]的包裹,因此无论我们是否在外面显式声明,赋值给[代码]props[代码]和[代码]state[代码]的行为都是会报错的。 注意,[代码]readonly[代码]听起来和[代码]const[代码]有点像,需要时刻保持一个概念: [代码]readonly[代码]是修饰属性的 [代码]const[代码]是声明变量的 泛型 在更加一般化的场景,我们的类型可能并不固定已知,它和[代码]any[代码]有点像,只不过我们希望在[代码]any[代码]的基础上能够有更近一步的约束,比如: [代码]function reverse<T>(items: T[]): T[] { var toreturn = []; for (let i = items.length - 1; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } [代码] [代码]reverse[代码]函数是一个很好的示例,对于一个通用的函数[代码]reverse[代码]来说,数组元素的类型是未知的,可以是任意类型,但[代码]reverse[代码]函数的返回值也是个数组,它和传入的数组类型是相同的,对于这个约束,我们可以使用泛型,其语法是尖括号,内置泛型变量,多个泛型变量用逗号隔开,泛型变量名称没有限制,一般而言我们以大写字母开头,多个泛型变量使用其语义命名,加上[代码]T[代码]为前缀。 在调用时,可以显示的指定泛型类型: [代码]var reversed = reverse<number>([1, 2, 3]); [代码] 也可以利用TypeScript的类型推断,进行隐式调用: [代码]var reversed = reverse([1, 2, 3]); [代码] 由于我们的参数类型是[代码]T[][代码],而传入的数组类型是一个[代码]number[][代码],此时[代码]T[代码]的类型被TypeScript自动推断为[代码]number[代码]。 对于泛型而言,我们同样可以作用于接口和类: [代码]interface Array<T> { reverse(): T[]; // ... } [代码] 联合类型 在JS中,一个变量的类型可能拥有多个,比如: [代码]function formatCommandline(command: string[]|string) { var line = ''; if (typeof command === 'string') { line = command.trim(); } else { line = command.join(' ').trim(); } } [代码] 此时我们可以使用一个[代码]|[代码]分割符来分割多种类型,对于这种复合类型,我们称之为[代码]联合类型[代码]。 交叉类型 如果说联合类型的语义等同于[代码]或者[代码],那么交叉类型的语义等同于集合中的[代码]并集[代码],下面的[代码]extend[代码]函数是最好的说明: [代码]function extend<T, U>(first: T, second: U): T & U { let result = <T & U> {}; for (let id in first) { result[id] = first[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { result[id] = second[id]; } } return result; } [代码] 该函数最终以[代码]T&U[代码]作为返回值值,该类型既包含了[代码]T[代码]的字段,也包含了[代码]U[代码]的字段,可以看做是两个类型的[代码]并集[代码]。 类型别名 TypeScript为类型的复用提供了更便捷的方式——类型别名。当你想复用类型时,可能在该场景下要为已经声明的类型换一个名字,此时可以使用type关键字来进行类型别名的定义: [代码]interface state { a: 1 } export type userState = state; [代码] 我们同样可以使用type来声明一个类型: [代码]type Text = string | { text: string }; type Coordinates = [number, number]; type Callback = (data: string) => void; [代码] 对于type和interface的取舍: 如果要用交叉类型或联合类型,使用type。 如果要用extend或implement,使用interface。 其余情况可看个人喜好,个人建议type更多应当用于需要起别名时,其他情况尽量使用interface。 枚举类型 对于组织一系列相关值的集合,最好的方式应当是枚举,比如一系列状态集合,一系列归类集合等等。 在TypeScript中,枚举的方式非常简单: [代码]enum Color { Red, Green, Blue } var col = Color.Red; [代码] 默认的枚举值是从0开始,如上述代码,[代码]Red=0[代码],[代码]Green=1[代码]依次类推。 当然我们还可以指定初始值: [代码]enum Color { Red = 3, Green, Blue } [代码] 此时[代码]Red=3[代码], [代码]Green=4[代码]依次类推。 大家知道在JavaScript中是不存在枚举类型的,那么TypeScript的枚举最终转换为JavaScript是什么样呢? [代码]var Color; (function (Color) { Color[Color["Red"] = 0] = "Red"; Color[Color["Green"] = 1] = "Green"; Color[Color["Blue"] = 2] = "Blue"; })(Color || (Color = {})); [代码] 从编译后的代码可以看到,转换为一个key-value的对象后,我们的访问也非常方便: [代码]var red = Color.Red; // 0 var redKey = Color[0]; // 'Red' var redKey = Color[Color.Red]; // 'Red' [代码] 既可以通过key来访问到值,也可以通过值来访问到key。 Flag标识位 对于枚举,有一种很实用的设计模式是使用位运算来标识(Flag)状态: [代码]enum EnvFlags { None = 0, QQ = 1 << 0, Weixin = 1 << 1 } function initShare(flags: EnvFlags) { if (flags & EnvFlags.QQ) { initQQShare(); } if (flags & EnvFlags.Weixin) { initWeixinShare(); } } [代码] 在我们使用标识位时,可以遵循以下规则: 使用 [代码]|=[代码] 增加标志位 使用 [代码]&=[代码] 和 [代码]~[代码]清除标志位 使用 [代码]|[代码] 联合标识位 如: [代码]var flag = EnvFlags.None; flag |= EnvFlags.QQ; // 加入QQ标识位 Flag &= ~EnvFlags.QQ; // 清除QQ标识位 Flag |= EnvFlags.QQ | EnvFlags.Weixin; // 加入QQ和微信标识位 [代码] 常量枚举 在枚举定义加上[代码]const[代码]声明,即可定义一个常量枚举: [代码]enum Color { Red = 3, Green, Blue } [代码] 对于常量枚举,TypeScript在编译后不会产生任何运行时代码,因此在一般情况下,应当优先使用常量枚举,减少不必要代码的产生。 字符串枚举 TypeScript还支持非数字类型的枚举——字符串枚举 [代码]export enum EvidenceTypeEnum { UNKNOWN = '', PASSPORT_VISA = 'passport_visa', PASSPORT = 'passport', SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id', SIGHTED_KEYPASS_CARD = 'sighted_keypass_card', SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card', } [代码] 这类枚举和我们之前使用JavaScript定义常量集合的方式很像,好处在于调试或日志输出时,字符串比数字要包含更多的语义。 命名空间 在没有模块化的时代,我们为了防止全局的命名冲突,经常会以命名空间的形式组织代码: [代码](function(something) { something.foo = 123; })(something || (something = {})) [代码] TypeScript内置了[代码]namespace[代码]变量帮助定义命名空间: [代码]namespace Utility { export function log(msg) { console.log(msg); } export function error(msg) { console.error(msg); } } [代码] 对于我们自己的工程项目而言,一般建议使用ES6模块的方式去组织代码,而命名空间的模式可适用于对一些全局库的声明,如jQuery: [代码]namespace $ { export function ajax(//...) {} } [代码] 当然,命名空间还可以便捷地帮助我们声明静态方法,如和[代码]enum[代码]的结合使用: [代码]enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } namespace Weekday { export function isBusinessDay(day: Weekday) { switch (day) { case Weekday.Saturday: case Weekday.Sunday: return false; default: return true; } } } const mon = Weekday.Monday; const sun = Weekday.Sunday; console.log(Weekday.isBusinessDay(mon)); // true console.log(Weekday.isBusinessDay(sun)); // false [代码] 关于命名规范 变量名、函数和文件名 推荐使用驼峰命名。 [代码]// Bad var FooVar; function BarFunc() { } // Good var fooVar; function barFunc() { } [代码] 类、命名空间 推荐使用帕斯卡命名。 成员变量和方法推荐使用驼峰命名。 [代码]// Bad class foo { } // Good class Foo { } // Bad class Foo { Bar: number; Baz() { } } // Good class Foo { bar: number; baz() { } } [代码] Interface、type 推荐使用帕斯卡命名。 成员字段推荐使用驼峰命名。 [代码]// Bad interface foo { } // Good interface Foo { } // Bad interface Foo { Bar: number; } // Good interface Foo { bar: number; } [代码] 关于模块规范 [代码]export default[代码]的争论 关于是否应该使用[代码]export default[代码]在这里有详尽的讨论,在AirBnb规范中也有[代码]prefer-default-export[代码]这条规则,但我认为在TypeScript中应当尽量不使用[代码]export default[代码]: 关于链接中提到的重命名问题, 甚至自动import,其实export default也是可以做到的,借助编辑器和TypeScript的静态能力。所以这一点还不是关键因素。 不过使用一般化的[代码]export[代码]更让我们容易获得智能提示: [代码]import /* here */ from 'something'; [代码] 在这种情况下,一般编辑器是不会给出智能提示的。 而这种: [代码]import { /* here */ } from 'something'; [代码] 我们可以通过智能提示做到快速引入。 除了这一点外,还有以下几点好处: 对CommonJS是友好的,如果使用export default,在commonJS下需要这样引入: [代码]const {default} = require('module/foo'); [代码] 多了个default无疑感觉非常奇怪。 对动态import是友好的,如果使用export default,还需要显示的通过default字段来访问: [代码]const HighChart = await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js'); Highcharts.default.chart('container', { ... }); // 注意 `.default` [代码] 对于[代码]re-exporting[代码]是友好的,如果使用export default,那么进行[代码]re-export[代码]会比较麻烦: [代码]import Foo from "./foo"; export { Foo } [代码] 相比之下,如果没有[代码]export default[代码],我们可以直接使用: [代码]export * from "./foo" [代码] 实践中的一些坑 实践篇即将到来,敬请期待~
2019-03-18 - 从源码看微信小程序启动过程
一、写作背景 接触小程序一年多,真实体验就是小程序开发门槛相对而言确实比较低。不过小程序的开发方式,一直是开发者吐槽的,如习惯了 Vue,React 开发的开发者经常会吐槽小程序一个 Page 必须由多个文件组成,组件化支持不完善或者说不能非常愉快的开发组件。在以前小项目中没太大感觉,从加入有赞,参与有赞微商城小程序的开发,是真切的体会到对于大型小程序项目开发的复杂性。 有赞从微信小程序内测就开始开发小程序,在不支持自定义组件的时代,只能通过 import 的形式拆分模块或实现组件。在业务复杂的页面,可能会 import 非常多的模块,而相应的 wxss 也需要 import 样式,除了操作繁琐,有时候也难免遗漏。 作为开发者,我们当然希望可以让工作更简单,更愉快,也希望改善我们的开发方式。所以希望能够更了解微信小程序框架,减少不必要的试错,于是有了一次对小程序框架的 debug 之旅。(基础库 1.9.93) 通过三周空余时间的 debug,也算对小程序框架有了一些浅显的认识,达到了最初的目的;对小程序启动,实例,运行等有了真切的体会。这篇文章记录了小程序框架的基本代码结构,启动流程,以及程序实例化过程。 本文的目的是希望把我看到的分享给对小程序感兴趣或者正在开发小程序的读者,主要解答“框架对传入的对象等到底做了什么”。 二、从启动流程一窥小程序框架细节 在开发者工具中使用 help() 方法,可以查看一些指令和方法。使用其中的 openVendor 方法可以打开微信开发者工具在小程序框架所在目录。其中以包括以基础库命名的目录和其他帮助文件,如其中有两个工具 wcc,wcsc。wcc 可把 wxml 转换为对应的 JS 函数 —— $gwx(path, global),wcsc 可将 wxss 转换为 css。而基础库目录包括 WAService.js 和 WAWebview.js 文件。小程序框架在开发者工具中以 WAService.js 命名(WAWebview.js 不知其作用,听说在真机环境使用该文件)。 在开发中工具命令行使用 document.head 可以查看到小程序的启动流程大致如下: [图片] 以小节的方式分别介绍这些流程,小程序是如何处理的(小节编号与图中编号相同)。 1、初始化全局变量 下图是小程序启动是初始化的一些全局的变量: [图片] 那些使用“__”开头,未在文档中提及可使用变量是不建议使用的,wxAppCode 在开发者工具中分为两类值,json 类型和 wxml 类型。以 .json 结尾的,其 key 值为开发者代码中对应的 json 文件的内容,.wxml 结尾的,其 key 值为通过调用 $gwx(’./pages/example/index.wxml’) 将得到一个可执行函数,通过调用这个函数可得到一个标识节点关系的 JSON 树。 [图片] 2、加载框架(WAService.js) 使用工具对 WAService.js 进行格式化后进行 debug。可以发现小程序框架大致由: WeixinJSBridge、 NativeBuffer、 wxConsole、 WeixinWorker、 JavaScript兼容(这部分为猜测)、 Reporter、 wx、 exparser、 virtualDOM、 appServiceEngine 几部分组成。 其中除了 wx 和 WeixinJSBridge 这两个基础 API 集合, exparser, virtualDOM, appServiceEngine 这三部分作为框架的核心, appServiceEngine 提供了框架最基本的接口如 App,Page,Component; exparser 提供了框架底层的能力,如实例化组件,数据变化监听,view 层与逻辑层的交互等;而 virtualDOM 则起着链接 appServiceEngine 和 exparser 的作用,如对开发者传入 Page 方法的对象进行格式化再传入 exparser 的对应方法处理。 框架对外暴露了以下API:Behavior,App,Page,Component,getApp,getCurrentPages,definePlugin,requirePlugin,wx。 3、业务代码的加载 在小程序中,开发者的 JavaScript 代码会被打包为 [代码]define('xxx.js', function(require, module, exports, window, document, frames, self, location, navigator, localStorage, history, Caches, screen, alert, confirm, prompt, fetch, XMLHttpRequest, WebSocket, webkit, WeixinJSCore, Reporter, print, WeixinJSBridge) { 'use strict'; // your code }) [代码] 这里的 define 是在框架中定义的方法,在框架中提供了两个方法:require 和 define 用来定义和使用业务代码。其方式有些像 AMD 规范接口,通过 define 定义一个模块,使用 require 来应用一个模块。但是也有很大区别,首先 define 限制了模块可使用的其他模块,如 window,document;其次 require 在使用模块时只会传入 require 和 module,也就是说参数中的其他模块在定义的模块中都是 undefined,这也是不能在开发者工具中获取一些浏览器环境对象的原因。 在小程序中,JavaScript 代码的加载方式和在浏览器中也有些不同,其加载顺序是首先加载项目中其他 js 文件(非注册程序和注册页面的 js 文件),其次是注册程序的 app.js,然后是自定义组件 js 文件,最后才是注册页面的 js 代码。而且小程序对于在 app.js 以及注册页面的 js 代码都会加载完成后立即使用 require 方法执行模块中的程序。其他的代码则需要在程序中使用 require 方法才会被执行。 下面详细介绍了 app.js,自定义组件,页面 js 代码的处理流程。 4、加载 app.js 与注册程序 在 app.js 加载完成后,小程序会使用 require(‘app.js’) 注册程序,即对 App 方法进行调用,App 方法是对 appServiceEngine.App 方法的引用。 下图是框架对于 App 方法调用时的处理流程: [图片] App 方法根据传入的对象实例化一个 app 实例,其生命周期函数 onLaunch 和 onShow 因为使用不同的方式获取 options的参数。在有些需要根据场景值来实现需求的,或许使用 onShow 中的场景值更合适。 在实际开发过程中发现,在微信顶部唤起小程序和在小程序列表唤起的 options 也是不一样的。在该案例中通过点击分享的小程序进入后,关闭小程序,再通过不同方式进入小程序,通过顶部唤起的还是 options 的 path 属性还是分享出来的 path,但是通过列表中打开直接回到了首页,这里 App 中的 onShow 就会获取到不同的 options。 5、加载自定义组件代码以及注册自定义组件 自定义组件在 app.js 之后被加载,小程序会在这个过程中加载完所有的自定义组件(分包中自定义组件没有有测试过),并且是加载完成后自动注册,只有注册完成后才会加载下一个自定义组件的代码。 下图是框架对于 Component 方法处理流程: [图片] 图中介绍了框架如何对传入 Component 方法的对象的处理,其后面还有很多深入的对于组件实例化的步骤没有在图中表示出来,具体可以在文章最后的附件中查看。 自定义组件在小程序中越来越完善,其拥有的能力也比 Page 更强大,而后面会提到在使用自定义组件的 Page 中,Page 实例也会使用和自定义组件一样的实例化方式,也就是说,他拥有和自定义组件一样的能力。 6、加载页面代码和注册页面 加载页面代码的处理流程和加载自定义组件一样,都是加载完成后先注册页面,然后才会加载下一个页面。 下图是注册一个页面时框架对于 Page 方法的处理流程: [图片] Page 方法会根据是否使用自定义组件做不同的处理。使用自定义组件的 page 对象会被处理为和自定义组件的结构,并在页面实例化时使用不同的处理流程进行实例化。当然对于开发而言没任何不同。 从图中可以发现 Page 传入的(生命周期)代码并不会在这里被执行,可以通过下面小节了解 Page 实例化的详细过程。 7、等待页面 Ready 和 Page 实例化 还记得上面介绍的启动流程中最后一步等待页面 Ready?严格来讲是等待浏览器 Ready,小程序虽然有部分原生的组件,不过本质上还是一个 web 程序。 在小程序中切换页面或打开页面时会触发 onAppRoute 事件,小程序框架通过 wx.onAppRoute 注册页面切换的处理程序,在所有程序就绪后,以 entryPagePath 作为入口使用 appLaunch 的方式进入页面。 下图是处理导航的程序流程: [图片] 从图中可以看出页面的实例化是在进入页面时进行,下图是具体的实例化过程: [图片] 下图是最终可得到 Page 实例: [图片] 可以发现其中多了 onRouteEnd API,实际该接口不会被调用。其中以 component 标记的表示只有在使用了自定义组件时才会有的方法和属性。在前面第 5 小节提到了对于使用自定义组件的页面会按照自定义组件方式解析,这些属性和方法与自定义组件表现一致。 8、关于 setData 小程序框架是一个以数据驱动的框架,当然不能少了对他如何实现数据绑定的探索,下图是 Page 实例的 setData 执行流程: [图片] 其中 component:setData 表示使用自定义组件的 Page 实例的 setData 方法。 三、写在最后 这是一次不完全的小程序框架探索,是在微信开发工具中 debug 的结果。虽然对于实际开发没有什么太大的帮助,但是对框架如何对开发的 js 代码进行处理有了一个很明确的认识,在使用一些 js 特性时可以有明确的感知。如果你还疑惑“小程序框架对传入的对象等到底做了什么”那一定是我表达能力太差,说声对不起。 通过这一次 debug ,也给我引入了新的问题,还希望能够有更多的讨论: · 自定义组件太多启动时会耗时处理自定义组件 · 文件太多会耗时读文件 · 合理的设计分包很重要 当然最后对于框架中已有的能力,还是非常希望微信可以开放更多稳定的接口,并在文档中告知开发者,让开发变得简单一些。
2019-03-05