编程一件非常简单、有趣而很容易让人有成就感的事情,云开发训练营致力于让更多的人能够快速学会小程序的云开发,并做出实际的作品,享受到技术创造的魅力。学习这本手册,不需要任何前置的编程知识,可以说零基础也能学会。本教程也同样适合有一定基础的程序员来学习小程序的云开发。
可能有不少同学在大学期间接触过C、Java等编程语言,学了很久也没有做出东西来,或者自己也曾尝试过自学编程,但是感觉也没能坚持下来,过段时间学的也很快忘记了,就误以为编程太难或需要学习很久,其实是你学习方法和方向的问题。小程序·云开发是最值得新手、技术爱好者入门编程的方向之一。
我之前从来没有接触过编程,能够学会吗?
可以的,只要你会最基本的电脑操作(比如软件怎么下载安装,会基础的打字、会上网、了解基础的Office办公软件等),你就可以学会。
学编程是不是对英语、数学有要求,我基础不好能学吗?
编程有很多方向,有的确实对数学、英语这些有很高的要求,但是小程序的云开发对数学基本没有要求,英语只需要看到单词不头疼,最好有初中英语水平就可以。
编程学起来难吗,我逻辑能力不强,真的能学会吗?
就论逻辑而言,小程序云开发的难度比高中数学的难度要低很多,你高中数学都能够学下来,就不可能学不会。
编程是不是要学很久才行,我学多久能够做东西出来?
两天的业余时间(5~6个小时左右)就可以做出展示的东西(demo)出来,十天的业余时间大致可以入门,一个月的时间就能独立开发一个完整的项目。
学编程是不是一定要用电脑?
是,如果没有电脑,不自己动手实战,只看书,就不要学了。
为什么不是视频?我要按照文档来自学吗?
是的,因为视频要讲的内容都写成了详细的文字说明和操作步骤,你可以按照自己学习的进度和节奏来学,更重要的是培养实战的习惯。
小程序我知道,但是云开发是啥?小程序与云开发到底什么关系?前端是啥?前端和后端是怎么联系的?开发一个小程序到底要学哪些技术?…在你想的太多,做的太少的时候,没有人有义务回答你。停止发问,立即开始动手实战,一切的问题只有探索之后才能明白。
用云开发来开发小程序,可以让你独立(一个人就可以)开发出一个完整(横跨前端后端)、而又实用的小程序项目,这就是小程序·云开发的产品力。云开发可以大大降低技术的学习成本和开发成本,但并不意味着它只能开发中小型的项目,即使是亿级流量的小程序也同样推荐使用云开发。
也许你是一个文科生,之前从前接触过编程,但是希望通过技术实现自己的创意;
也需你做着产品、市场、运营等相关的工作,想学习一门对工作、生活有一定帮助的技术;
也许你是一个创业者,希望快速实践自己的想法,使用小程序来验证Idea的可行性;
也许你是一个技术爱好者,希望能够了解Serverless新技术趋势,或开发一些小程序项目赚点广告费或作为副业;
云开发,作为一个基于Serverless的云端一体化产品方案,一项普惠技术,致力于降低技术实践门槛,让创意更容易实现。为此,云开发特推出零基础训练营,帮助更多人实现技术梦。
训练营,基于微信群展开,倡导“翻转课堂”教学理念,强调个人驱动力和朋辈互助,每月一期,随到随学,有问必答。
目前,训练营已举办超过6次,累计帮助2000+编程爱好者实现从0到1上手编程开发 ,并通过云开发实现自己的技术梦。
技术入门,难在心理畏惧。云开发训练营,零基础可学,无须技术基础。
还等什么?扫码关注云开发公众号,回复【训练营】,赶紧加入云开发训练营吧!
学习技术不要光看视频、教材,一定要动手实践。只有实战,技术才会变得简单。小程序相比其他编程语言来说,可以让我们更快做出一些技术产品。
小程序的开发有两样东西必不可少,一个是小程序开发的技术文档;还有一个就是小程序的开发者工具。
开发者工具:小程序开发者工具下载地址
大家可以根据自己电脑的操作系统来下载对应的稳定版安装包进行安装。就和我们写Word、PPT文档要用Office的软件一样,我们要在开发者工具上多多动手,技术才能掌握的更加真切。
技术文档:小程序技术文档
技术文档大家先只需要花五分钟左右的时间了解大致的结构即可,先按照我们的教学步骤学完之后再来看也不迟。官方的小程序技术文档过于全面而且详细,对于初学者或者零基础的朋友来说,我们会引导大家如何循序渐进的学习文档里的技术知识。
多看技术文档和多用开发工具也是我们学习其他编程语言或技术最为重要的两点,凡是脱离技术文档和开发工具看视频、看文章以及搜集再多的资料都是舍本求末的错误做法,而这也是很多初学者的一个通病。
值得注意的是小程序的开发功能更新非常频繁,很多网上的教程内容都比较过时,而只有技术文档才是同步最新的。无论你是初学者还是高手,技术文档都是我们技术开发的基础与落脚点,常读常新。
小程序的注册非常方便,打开小程序注册页面,按照要求填入个人的信息,验证邮箱和手机号,扫描二维码绑定你的微信号即可,3分钟左右的时间即可搞定。
注册页面:小程序注册页面
注册小程序时不能使用注册过微信公众号、微信开放平台的邮箱哦,也就是需要你使用一个其他邮箱才行。
当我们注册成功后,就可以自动登入到小程序的后台管理页面啦,如果你不小心关掉了后台页面,也可以点击小程序后台管理登录页进行登录。
后台管理页:小程序后台管理登录页
小程序和微信公众号的登录页都是同一个页面,他们会根据你的不同的注册邮箱来进行跳转。
进入到小程序的后台管理页后,点击左侧菜单的开发进入设置页,然后再点击开发设置,在开发者ID里就可以看到AppID(小程序ID),这个待会我们有用。
注意小程序的ID(AppID)不是你注册的邮箱和用户名,你需要到后台查看才行哦~
安装完开发者工具之后,我们使用微信扫码登录开发者工具,然后使用开发者工具新建一个小程序的项目,
点击新建确认之后就能在开发者工具的模拟器里看到一个简单的Hello World模板小程序,在编辑器里看到这个小程序的源代码。
小任务: 分别点击开发者工具工具栏上的模拟器、编辑器、调试器,以及下面的手机下拉框、显示百分比,看看有什么效果。找到开发者工具的菜单栏,在项目菜单栏里找到查看所有项目,在设置菜单栏里找到外观设置,切换一下主题、调试器主题(深色、浅色)。
接下来,我们点击开发者工具的工具栏里的预览图标,就会弹出一个二维码,使用你的手机微信扫描这个二维码就能在微信里看到这个小程序啦。以后我们要自己开发一个小程序都可以按照上面的操作新建一个模板小程序,然后在这个的基础上修改开发。
如果你没有使用微信登录开发者工具,以及你的微信不是该小程序的开发者是没法预览的哦。这个Hello World模板小程序非常简单,但是它的文件结构却是完整的。
点击微信开发者工具的“云开发”图标,在弹出框里点击“开通”,同意协议后,会弹出创建环境的对话框。这时会要求你输入环境名称和环境ID,以及当前云开发的基础环境配额(基础配额免费,而且足够你使用哦)。
建议你环境名称可以使用 xly、环境ID自动生成即可,当你的云开发环境出现问题的时候,你可以提供你的环境ID,云开发团队会有专人为你解答。
按照对话框提示的要求填写完之后,点击创建,会初始化环境,环境初始化成功后会自动弹出云开发控制台,这样我们的云开发服务就开通啦。大家可以花两分钟左右的时间熟悉一下云开发控制台的界面。
在了解以下知识时,大家只需要结合开发者工具的编辑器对照着介绍,一一展开文件夹、用编辑器查看文件的源代码,大致浏览一下即可。这就是实战学习的方法(和看书、看视频的学习方法不同),千万不要死记硬背哦,你以后用多了自然就记住啦~
小程序的文件结构
在开发者工具的编辑器里可以看到小程序源文件的根目录下有app.js、app.json和app.wxss,这是小程序必不可少的三个主体文件,下面我们来大致了解一下小程序文件结构(只需要大致了解就可以啦~不理解也没有关系)。
小任务:在结合开发者工具实战了解了上面的知识之后,你明白了哪个文件夹是小程序的根目录吗?
小程序的页面组成
在每一个页面文件夹里都有四个文件,这四个文件的名称都是一样的,它们分别为:
在前面我们已经提到,app.json可以对整个小程序进行全局配置,而配置的依据就需要我们参考技术文档了。
技术文档:小程序全局配置
打开上面的小程序全局配置技术文档,里面会有很多你看不懂的名称,这是非常正常的,大家也不需要记,只需要花两三分钟时间快速浏览一下即可,后面我们会教大家如何结合技术文档来实战学习。
json语法
在对小程序进行配置之前,可以使用开发者工具打开app.json文件,对照着下面的json语法来进行理解:
注意,这里所有的标点符号都需要是英文状态下的,也就是我们经常听说的全角半角里的半角状态,不然会报错哦。很多之前没有接触过编程的童鞋经常会犯这样的错误,一定要多多注意!当我们要输入编程里的标点符号时,一定要先确认一下,你的输入法是汉语形态,还是字母形态,如果输的是汉字形态,一定要切换哦~
设置小程序窗口表现
使用开发者工具打开app.json文件,可以看到如下代码里有一个window的字段名(如前面所说,字段名要用双引号””包着),它的值是一个对象(如前面所说,{}大括号里的就是对象),可见对象可以是一组数据的集合,这个集合里包含着几条数据。
"window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" },
这些就是window配置项,可用于设置小程序的状态栏、导航条、标题、窗口背景色。
小任务:打开小程序全局配置查看backgroundTextStyle、navigationBarBackgroundColor、navigationBarTitleText、navigationBarTextStyle的配置描述(大致了解即可)。
使用开发者工具的编辑器将以上属性的值改成如下代码(这里的backgroundTextStyle只有在设置了下拉刷新样式时才会比较明显,以后会介绍)
"window": { "backgroundTextStyle": "dark", "navigationBarBackgroundColor": "#1772cb", "navigationBarTitleText": "云开发技术训练营", "navigationBarTextStyle": "white" },
添加完成之后记得保存代码哦,文件修改没有保存会在标签页有一个小的绿点。可以使用快捷键(同时按)Ctrl和S来保存(Mac电脑为Command和S)。
然后点击开发者工具的编译图标,就能看到更新之后的效果啦,也可以点击预览,使用手机微信扫描生成的二维码查看实际效果。
小任务: navigationBarBackgroundColor值是 #F8F8F8, #1772cb,这是十六进制颜色值,它是一个非常基础而且用途范围极广的计算机概念,大家可以搜索了解一下:1、如何使用电脑版微信、QQ的截图工具取色(取色颜色会有一点偏差);2、RGB颜色与十六进制颜色如何转换;
新建小程序页面
新建页面的方法有两种,一种是使用开发者工具在pages文件夹下新建;还有一种是通过app.json的pages配置项来新建,我们先来看第2种方法。
通过app.json新建页面
pages配置项是设置页面的路径,也就是我们在小程序里写的每一个页面都需要填写在这里。使用开发者工具打开app.json文件,在pages配置项里新建一个home页面(页面名称可以是任意英文名),代码如下:
"pages/home/home","pages/index/index","pages/logs/logs"
大家写的时候可以回顾一下json语法,每个页面后都记得要用逗号,隔开,如果你的文件代码写错了,开发者工具会报错。
在模拟器就能看到我们新建的这个首页了,会显示如下内容:
pages/home/home.wxml
大家再来看看小程序的文件夹结构,是不是在pages文件夹下面多了一个home的文件夹?而且这个文件夹还自动新建了四个页面文件。
我们删掉文件目录下的index和logs文件夹,然后把app.json的pages配置项修改为:
"pages": [ "pages/home/home", "pages/list/list", "pages/partner/partner", "pages/more/more"],
也就是我们删掉了index和logs页面配置项的同时,又新增了三个页面(list、partner、more,这三个页面名称大家可以根据自己需要来命名)。
小任务:这些新建的页面文件都在电脑上的什么呢?比如在开发者工具右键点击home文件夹或者home.wxml,选择“在硬盘打开”就可以看到该文件在我们电脑的文件夹里什么的位置啦
相信通过前面的学习,大家对一个完整的小程序的文件结构有了一个大致的了解,对小程序的开发者工具也有了一定的认识,那这节我们来开始动手写一下小程序的代码。
我们在开发者工具里打开之前修改的模板小程序home文件夹下的home.wxml,里面有如下代码
<!--pages/home/home.wxml--><text>pages/home/home.wxml</text>
这个第1行,是一句注释,也就是一句说明,不会显示在小程序的前端,第2行就是一个<text>组件。
接下来我们会广泛使用到小程序的<view>组件。比如我们在上面的代码下面加一下下面的代码,大家再来看效果:
<view> <view> <view>WXML 模板</view> <view>从事过网页编程的人知道,网页编程采用的是 HTML + CSS + JS 这样的组合,其中 HTML 是用来描述当前这个页面的结构,CSS 用来描述页面的样子,JS 通常是用来处理这个页面和用户的交互。</view> </view></view>
大家可以结合上面的代码,来了解一下组件的基本写法
我们可以把这个页面写的更加复杂一点。
<view> <view> <view> <view>技术学习说明</view> <view>技术和我们以往所接触的一些知识有很大的不同,比如英文非常强调词汇量,需要你多说多背;数学需要你记住公式,反复练习;在教学的方式上也有很大的不同,以前都是有专门的老师手把手教你,而且还有同学交流。那要学好技术,应该依循什么样的学习方法和教学方法呢? </view> </view> <view> <view> <view>自学而非手把手</view> <view>技术的方向众多,而且知识点也是非常庞杂,需要你具备一定的自学能力,所谓自学能力就是要求你遇到问题能够勤于思考,擅于搜索,能够不断实践探索。在实际工作中,也要求你能根据技术文档可以迅速掌握前沿的技术,而同事不会有时间教你,如果没有自学能力,是很难胜任很多工作的。 </view> </view> <view> <view>查阅文档而非死记知识点</view> <view>在高中一学期一门课只有很薄的一本书,老师会反复讲解知识点,强化你的记忆,考试也是闭卷;而技术一个很小的分支,内容就有几千页甚至更多,强记知识点显然不合适。学习技术要像查词典一样来查阅技术文档,你只需要掌握基本的语法和用法,在编程的时候随时查阅,就像你不需要背诵上万的单词也能知道它的意思用法一样,所以技术文档是学习技术最为重要的参考资料。 </view> </view> <view> <view>实战而非不动手的看书</view> <view>技术是最强调结果的技能,你看了再多书,如果不知道技术成品是怎么写出来的,都是枉然。很多朋友有收集癖,下载了很多电子资源,收藏了很多高赞的技术文章,但是却没有动手去消化去理解,把时间和精力都浪费了。不动手在开发者工具里去写代码,不动手配置开发环境,缺乏实战的经验,都是阻碍你学好技术的坏习惯。 </view> </view> <view> <view>搜索而非做伸手党</view> <view>在以前,我们遇到技术问题,我们可以问老师问同学,于是很多人把这种不良的习惯也带到了技术的学习当中,遇到问题也总喜欢求助于别人。技术的方向众多,环境复杂,问题也是很多,如果你总是依赖别人的解答,是很难学好技术的。一定要学会在网上通过搜索遇到的问题来找答案。 </view> </view> <view> <view>团队协作,而非单打独斗</view> <view>一个产品涉及的技术非常多,需要很多人来一起配合才能开发好,所以学习技术的时候我们需要了解一些代码规范、工作的流程、项目管理等,在技术方面也会有API接口,接口文档这些,还要懂得如何和同职业的同事以及不同角色的人如设计师等一起配合,而不是自己一个人单打独斗。 </view> </view> <view> <view>系统的指导而非茫然无序</view> <view>学好技术最依赖你自学,但是如果没有人指导你,你可能会像苍蝇一样陷入众多资料中茫然无序,抓不住一个技术的重点,也不清楚哪些技术才值得你深入学习,最好是有经验的人可以指点你应该看什么,学什么,什么才是重点,当然不是手把手教你。 </view> </view> </view> </view> </view>
大家是不是已经发现我们写的小程序页面有点丑?那我们需要对这个小程序页面进行美化。但是我们的代码里面<view>组件这么多,要是不对每个组件进行区分,就很难对每个组件进行美化了。
id与class选择器
这个时候我们就要先了解一下选择器的概念。选择器是用来干什么的呢?从名字里我们就可以看出来,就是为了选择。比如学校有1000个人,我们要选择出其中一个人,那我们可以给所有人一个学号,这个学号是唯一的,我们可以称这个学号为id,用于精准的选择;还有的时候我们需要对一群人进行分类选择,比如整个班级或者所有男生,这个班级、性别,我们可以称为class,用于分类选择。
在wxss技术文档这里有关于选择器的描述。
技术文档:WXSS技术文档
给组件增加属性
比如上面的view组件实在太多了,为了区分它们,我们给他们加一些属性,这样我们就可以用选择器选择它们了。
<view id="wxmlinfo"> <view class="content"> <view class="title">WXML 模板</view> <view class="desc">从事过网页编程的人知道,网页编程采用的是 HTML + CSS + JS 这样的组合,其中 HTML 是用来描述当前这个页面的结构,CSS 用来描述页面的样子,JS 通常是用来处理这个页面和用户的交互。 </view> </view></view>
以及比较复杂的那一段代码的view组件也加上属性。给组件添加属性在外观上并不会有什么效果哦~
<view id="studyweapp"> <view class="content"> <view class="header"> <view class="title">技术学习说明</view> <view class="desc">技术和我们以往所接触的一些知识有很大的不同,比如英文非常强调词汇量,需要你多说多背;数学需要你记住公式,反复练习;在教学的方式上也有很大的不同,以前都是有专门的老师手把手教你,而且还有同学交流。 那要学好技术,应该依循什么样的学习方法和教学方法呢? </view> </view> <view class="lists"> <view class="item"> <view class="item-title">自学而非手把手</view> <view class="item-desc">技术的方向众多,而且知识点也是非常庞杂,需要你具备一定的自学能力,所谓自学能力就是要求你遇到问题能够勤于思考,擅于搜索,能够不断实践探索。在实际工作中,也要求你能根据技术文档可以迅速掌握前沿的技术,而同事不会有时间教你,如果没有自学能力,是很难胜任很多工作的。 </view> </view> <view class="item"> <view class="item-title">查阅文档而非死记知识点</view> <view class="item-desc">在高中一学期一门课只有很薄的一本书,老师会反复讲解知识点,强化你的记忆,考试也是闭卷;而技术一个很小的分支,内容就有几千页甚至更多,强记知识点显然不合适。学习技术要像查词典一样来查阅技术文档,你只需要掌握基本的语法和用法,在编程的时候随时查阅,就像你不需要背诵上万的单词也能知道它的意思用法一样,所以技术文档是学习技术最为重要的参考资料。 </view> </view> <view class="item"> <view class="item-title">实战而非不动手的看书</view> <view class="item-desc">技术是最强调结果的技能,你看了再多书,如果不知道技术成品是怎么写出来的,都是枉然。很多朋友有收集癖,下载了很多电子资源,收藏了很多高赞的技术文章,但是却没有动手去消化去理解,把时间和精力都浪费了。不动手在开发者工具里去写代码,不动手配置开发环境,缺乏实战的经验,都是阻碍你学好技术的坏习惯。 </view> </view> <view class="item"> <view class="item-title">搜索而非做伸手党</view> <view class="item-desc">在以前,我们遇到技术问题,我们可以问老师问同学,于是很多人把这种不良的习惯也带到了技术的学习当中,遇到问题也总喜欢求助于别人。技术的方向众多,环境复杂,问题也是很多,如果你总是依赖别人的解答,是很难学好技术的。一定要学会在网上通过搜索遇到的问题来找答案。 </view> </view> <view class="item"> <view class="item-title">团队协作,而非单打独斗</view> <view class="item-desc">一个产品涉及的技术非常多,需要很多人来一起配合才能开发好,所以学习技术的时候我们需要了解一些代码规范、工作的流程、项目管理等,在技术方面也会有API接口,接口文档这些,还要懂得如何和同职业的同事以及不同角色的人如设计师等一起配合,而不是自己一个人单打独斗。</view> </view> <view class="item"> <view class="item-title">系统的指导而非茫然无序</view> <view class="item-desc">学好技术最依赖你自学,但是如果没有人指导你,你可能会像苍蝇一样陷入众多资料中茫然无序,抓不住一个技术的重点,也不清楚哪些技术才值得你深入学习,最好是有经验的人可以指点你应该看什么,学什么,什么才是重点,当然不是手把手教你。 </view> </view> </view> </view> </view>
大家在学习的过程中,要随时在开发者工具的模拟器上查看效果,也要经常用微信扫码预览所生成的二维码来查看效果,千万不要只看教程怎么说,而是自己要动手去实战。
给wxml文件的组件加了选择器之后,我们就可以在wxss文件里给指定的某个<view>组件以及某类<view>组件添加一些美化了,这里我们需要编辑home.wxss文件。wxss美化的知识和css是一样的,所以小程序的技术文档里面没有,大家可以看一下w3shool的CSS参考手册。CSS文件的作用就是来美化组件的。
技术文档:CSS参考手册
这里大家只需要了解CSS的字体属性、文本属性、背景属性、边框属性、盒模型
CSS涉及的知识点非常多,现在大家也只需要知道有这些概念即可,学技术千万不要在没有看到实际效果的情况下来死记概念。概念没有记住一点关系都没有,因为大家可以随时来查文档。接下来我们也会有实际的例子让大家看到效果,大家想深入学习的时候可以回头再看这些文档。
下面这些关于CSS的基础知识点,大家可以结合我们是如何调整Word、PPT的样式的来理解,比如我们是怎么调整文本的大小、颜色、粗细等等的,添加样式的时候要注意随手实战了解了效果再说。
比如class为title的<view>组件里面的文字是标题,我们需要对标题的字体进行加大、加粗以及居中处理,这时候我们就可以在home.wxss文件里加入以下代码,然后大家看看有什么效果。
.title{ font-size: 20px; font-weight: 600; text-align: center; }
通过 .title
这个选择器,我们就选择到了类class为 title
的<view>
组件,然后就可以精准地对它进行美化,对它的美化代码不会用在其他组件上了。
大家也要留意css的写法,这里font-size,font-weight,text-align外面称之为属性,冒号:后面的我们称之为值,属性:值这一整个我们叫做声明,每个声明我们用分号;隔开。大家不要记这些概念,知道是个什么东西就行了。
class为item-title的<view>组件里面的文字是一个列表的标题,我们希望它和其他文字的样式有所不同,不过这个标题要比title的字体小一些;比其他文字更粗,但是比title更细;颜色我们可以添加一个彩色字体,
.item-title{ font-size:18px; font-weight:500; color: #c60;}
我们希望描述类的文字颜色浅一点,不要那么黑,我们可以换一个浅一点的颜色,我们在home.wxss下面继续加代码.
.desc,.item-desc{ color: #333;}
大家注意,我们这里有两个选择器,一个是desc,一个是item-desc,当我们希望两个不同的选择器有相同的css代码时,可以简化一起写,然后用逗号,隔开。
除了标题(class为title和item-title的<view>组件)我们都给他们加了字体大小,我们希望所有的文字大小、行间距有一个统一的设定。
#wxmlinfo,#studyweapp{ font-size:16px; font-family: -apple-system-font,Helvetica Neue,Helvetica,sans-serif; line-height: 1.6;}
为了大家方便查阅技术文档,我们把这些常用的css技术文档整理到以下表格,方便大家更深入的去学习。
字体属性 | 备注 |
---|---|
font-family | 规定文本的字体系列。 |
font-size | 规定文本的字体尺寸。 |
font-weight | 规定字体的粗细。 |
文本属性 | 备注 |
---|---|
color | 设置文本的颜色。 |
line-height | 设置行高。 |
text-align | 规定文本的水平对齐方式。 |
大家有没有发现段落之间的距离、文字之间的距离,以及与边框之间的距离都比较拥挤?这个时候就需要用到盒子模型啦。盒子模型就像一个长方形的盒子,它有长度、高度、也有边框,以及内边距与外边距。我们通过实战来了解一下。
长度、高度、边框我们比较好理解,那这个内边距和外边距是什么意思呢?
内边距就是这个长方形的边框与长方形里面的内容之间的距离,有上边距,右边距、下边距、左边距这个四个内边距,分别为padding-top,padding-right,padding-bottom,padding-left。注意是上、右、下、左,这样一个顺时针。
那外边距就是这个长方形的边框与长方形外面的内容之间的距离,同样也有上边距margin-top,右边距margin-right,下边距margin-bottom,左边距margin-left这个四个边距。同样也是上、右、下、左这个顺时针。
比如我们给id为wxmlinfo和studyweapp的<view>组件加一个内部距,让文字
#wxmlinfo,#studyweapp{ padding-top:20px; padding-right:15px; padding-bottom:20px; padding-left:15px;}
padding的简写 上面这四个padding是可以做一定的简写的,关于padding的简写,大家可以去阅读以下技术文档,多用就会了,CSS padding属性 ,在这里有四个padding简写的案例,比如上面的这四条声明可以简写成padding:20px 15px。大家可以业余深入了解一下。
class为title的view组件是标题,我们希望它和下面的文字距离大一点,我们可以添加以下样式:
.title,.item-title{ margin-bottom:0.9em; }
这里咋又冒出了一个em的单位,em是相对于当前字体尺寸而言的单位,如果当前你的字体大小为16px,那1em为16px;如果当前你的字体大小为18px,那1em为18px。
为了让class为item-title的<view>组件,也就是列表的标题更加突出,我们可以给它左边加一个边框,
.item-title{ border-left: 3px solid #c60; padding-left: 15px;}
这样,小程序的一篇文章的样式看起来就算马马虎虎可以接受啦~为了便于大家查阅,我们也把盒子模型的三类属性整合在了一起
内边距属性 | 备注 |
---|---|
padding | 在一个声明中设置所有内边距属性。 |
padding-top | 设置元素的上内边距。 |
padding-right | 设置元素的右内边距。 |
padding-bottom | 设置元素的下内边距。 |
padding-left | 设置元素的左内边距。 |
外边距属性 | 备注 |
---|---|
margin | 在一个声明中设置所有外边距属性。 |
margin-top | 设置元素的上外边距。 |
margin-right | 设置元素的右外边距 |
margin-bottom | 设置元素的下外边距。 |
margin-left | 设置元素的左外边距。 |
边框属性 | 备注 |
---|---|
border | 在一个声明中设置所有的边框属性。比如border:1px solid #ccc; |
border-top | 在一个声明中设置所有的上边框属性。 |
border-right | 在一个声明中设置所有的右边框属性。 |
border-bottom | 在一个声明中设置所有的下边框属性。 |
border-left | 在一个声明中设置所有的左边框属性。 |
border-width | 设置四条边框的宽度。 |
border-style | 设置四条边框的样式。 |
border-color | 设置四条边框的颜色。 |
border-radius | 简写属性,设置所有四个 border-*-radius 属性。 |
box-shadow | 向方框添加一个或多个阴影。 |
更多的设计样式,大家可以根据上面的技术文档,在开发者工具里像做实验一样的来测试学习。
前几节的内容让我们的小程序有了文字,但小程序的内容形式还不够丰富,比如没有链接,没有图片等元素,而这些元素在小程序里也都是通过组件来实现的。
在小程序里,我们是通过navigator组件来给页面添加链接的。有些页面在我们打开小程序的时候就可以看得到,还有些则需要我们通过点击链接进行页面切换才可以看得到,这些我们可以称之为二级页面。
技术文档:[navigator组件技术文档]( https://www.51coolma.cn/weixinapp/weixinapp-navigator.html )
二级页面
为了让二级页面与tabBar的页面有更加清晰的结构关系,我们可以在tabBar对应的页面文件夹下面新建要跳转的页面。比如我们的第一个tabBar是home,凡是home会跳转的二级页面,我们都建在home文件夹里。
我们同样在pages配置项里新建一个页面imgshow,名称大家可以自定义~这样pages配置项的内容如下:
"pages/home/home", "pages/home/imgshow/imgshow", "pages/list/list", "pages/partner/partner", "pages/more/more"
然后我们再来在home页面的home.wxml加入以下代码
<view class="index-link"> <navigator url="./../home/imgshow/imgshow" class="item-link">让小程序显示图片</navigator></view>
在上面的代码中,我们把navigator组件嵌套在view组件里,当然不嵌套也是可以的。要写一个非常复杂的页面,就会经常用到这种嵌套。
由于navigator组件没有添加样式,所以在视觉上看不出它是一个可以点击的链接,我们在home.wxss里给它添加一个样式:
.item-link{ margin: 20px; padding:10px 15px; background-color: #4ea6ec; color: #fff; border-radius: 4px;}
url是页面跳转链接,大家注意这个路径的写法,我们也可以把上面的链接形式写成以下代码:
/pages/home/imgshow/imgshow
这两个路径都是指向imgshow页面。
小任务:为什么页面的路径有两个imgshow?比如把路径写成
/pages/home/imglist
对应的是什么页面?在pages配置项添加一下看看效果。
相对路径
大家注意我们之前使用的路径基本都是相对路径,相对路径使用“/”字符作为目录的分隔字符,
小任务:你知道当前根目录是什么吗?/pages/home/imgshow/imgshow和./../home/imgshow/imgshow这两个的写法你明白它们为啥指向的是同一个路径了吗?
要管理好图片资源、链接(页面)资源、音频资源、视频资源、wxss样式资源等等内部与外部资源,就一定要掌握路径方面的知识。我们之后也会经常运用这个知识。
绝对路径
那什么是绝对路径呢?网络链接比如 :
7n.51coolma.cn/attachments/knowledge/202006/77455.png
这个就是绝对路径,还有C:*Windows*System32,这种从盘符开始的路径也是绝对路径。通常相对路径用的会比较多一些。
一个好看的网页怎么可能少得了图片呢?小程序添加图片是通过image组件的方式。
技术文档:image组件技术文档
我们首先把要显示的图片放到小程序的image文件夹里,然后再在imgshow页面下的imgshow.wxml添加以下代码:
<view id="imgsection"> <view class="title">小程序显示图片</view> <view class="imglist"> <image class="imgicon" src="/image/icon-tab1.png"></image> </view></view>
注意图片的链接是我们之前的tab图标链接,也就是这个链接来源于小程序的本地文件夹。可能你的图片命名会有所不同,主要根据情况修改。
这样我们的图片就在小程序里显示出来啦~~
如果我们不对图片的样式比如高度和宽度进行处理,图片显示就会变形。这是因为小程序会给图片增加一个默认的宽度和高度,宽度为300px,高度为225px。
图片光显示出来还是不够的,很多时候我们会对图片显示出来的大小有要求,或者对它的外边距有要求;利用之前学到的知识,我们也可以给image组件加一些 css样式。比如我们在imgshow.wxss里面添加
.imglist{ text-align: center;}.imglist .imgicon{ width: 200px; height: 200px; margin: 20px;}
我们可以把图片放在小程序的本地文件夹里,也可以把图片放在网上。那如何把一张图片以链接的方式让其他人看到呢?这个时候就需要一个专门的存储图片的服务器(图床)了。
免费的图床:腾讯云对象存储COS
由于我们之前注册过小程序,可以选择其他登录方式里的微信公众号登录,登录后点击右上角控制台,即可进入后台,在工具栏里下拉云产品,找到存储下面的对象存储,在左侧菜单存储桶列表创建存储桶,只需注意将访问权限改为公有读私有写,其他按说明自行操作。
创建好存储桶bucket,然后大家可以把图片上传到对象存储服务器里面,并分享链接并在imgshow.wmxl测试一下:
<view class="imglist"> <image class="imgitem" src="7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image></view>
上面的网络图片是变形的,为了让图片不变形,那我们需要给图片添加一个wxss样式,这里就有一个问题,这张图片的宽度为1684px,高度为998px,而手机的宽度却没有这么高的像素。我们想让图片在手机里完整显示而不变形该怎么处理呢?方法之一是我们可以使用尺寸单位rpx。
技术文档:尺寸单位rpx
在小程序里,所有的手机屏幕的宽度都为750rpx,我们可以把图片等比缩小。比如给图片添加样式:
.imglist .imgitem{ width: 700rpx; height: 415rpx; margin: 20rpx;}
有了rpx这个尺寸单位,我们可以确定一个元素在小程序里的精准位置和精准大小,不过这个尺寸单位处理图片起来经常需要换算挺麻烦的,我们来看下面的处理方法。
由于我们的图片可能尺寸大小不一,或者由于iPhone、安卓手机的尺寸大小不一以及我们对图片显示的要求不一,为了让我们的图片显示正常,小程序需要对图片进行一些裁剪。
小程序是通过mode的方式来对图片进行裁剪的,大家可以去阅读一下image组件关于13种mode模式的说明。
技术文档:image组件技术文档
如果我们想处理好上面的图片,我们该怎么处理呢?按照技术文档,我们可以给image组件添加一个widthFix模式:宽度不变,高度自动变化,保持原图宽高比不变。
<view class="imglist"> <image class="imgitem" mode="widthFix" src="7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image></view>
然后给图片添加wxss样式:
.imglist .imgitem{ width: 100%;}
也就是说设置图片的宽度为百分比样式,而高度则自动变化,保持原图宽高比不变。
百分比是网页、移动端等用来布局以及定义大小的一个非常重要的单位,大家要多学多练多分析哦~
当然还会有这样的一个要求,我们希望图片全屏显示,但是设计师却只给图片预留了一个很小的高度,这样我们就必须对图片进行一定的裁剪了,我们可以在imgshow.wxml这样写。
<view class="imglist"> <image class="imgfull" mode="center" src="7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> </view>
而在imgshow.wxss里面添加一些样式
.imglist .imgfull{ width: 100%; height: 100px;}
大家可以在开发者工具以及通过扫描开发者工具预览生成的二维码在手机上体验一下,并把这里的mode=”center”,换成其他12个模式来了解一下,不同的模式对图片裁剪的影响。
图片的处理是一个非常重要的知识点,需要大家多多实践,但是原理和核心知识点都在wxss的样式处理和小程序image组件里,大家可以根据实际需求来应用。
背景属性也是属于CSS方面的知识,所谓背景属性就是给组件添加一些颜色背景或者图片背景。由于css的背景属性尤其是当我们想用一张图片作为组件的背景时,也会涉及到背景图片的位置与裁剪,这个和小程序image组件的裁剪多少有一些相通之处,所以我们就把CSS的背景属性放到这里来讲一下~
以下是我们经常会使用到的css背景属性以及相对应的技术文档,和之前我们强调的一样,技术文档是来翻阅和深入学习的,大家可以先用背景属性做出一些效果再说~
背景属性 | 备注 |
---|---|
background | 在一个声明中设置所有的背景属性。 |
background-color | 设置元素的背景颜色。 |
background-image | 设置元素的背景图像。 |
background-size | 规定背景图片的尺寸。 |
background-repeat | 设置是否及如何重复背景图像。 |
比如我们可以给我们之前写好的home页面,id为wxmlinfo的view组件加一个背景颜色以及id为studyweapp的view组件添加一个背景图片:
#wxmlinfo{ background-color: #dae7d9;}#studyweapp{ background-image: url(https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/bg.png); background-size: cover; background-repeat: no-repeat;}
大家注意,写在wxss里的图片只能来自服务器或者图床,不能放在小程序的文件结构里,这是小程序的一个规定。
我们经常在一些app里看到很多图片它有圆角或者阴影,那这个是怎么实现的呢?这些效果是通过css的边框属性来实现的。
大家可以在小程序的image文件夹添加一张深色背景的图片(如果小程序的背景是深色的,图片背景是白色也是可以的)。我们给之前添加的image组件加一个圆角和阴影样式,在imgshow.wxss添加以下代码:
.imglist .img{ border-radius: 8px; box-shadow: 5px 8px 30px rgba(53,178,225,0.26);}
图片有了圆角,有了阴影就有了一些现代感啦。
这里用到了一个颜色就是rgba颜色值。RGB前面我们要求大家查过,RGBA(R,G,B,A)的R是红色值,G是绿色值,B是蓝色值,R,G,B的值取值范围是0~255,A是Alpha透明度,取值0~1之间,越靠近0越透明。
我们来重新回顾一下边框属border-radius和box-shadow,大家可以点击链接查看技术文档的详情。
除了圆角,我们经常会有把图片做成圆形的需求,我们来看具体的例子。首先在wxml文件里输入以下代码,添加一个logo图片,
<view class="imglist"> <image class="circle" mode="widthFix" src="https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/logo.jpg" rel="external nofollow" ></image></view>
然后在与之对应的wxss文件里添加相应的css样式,
.imglist .circle{ width: 200px; height: 200px; border-radius: 100%;}
也就是我们只需要定义了图片长宽之后,再来定义一下border-radius为100%即可把图片做成圆形。
前面我们学习了Navigator组件里添加一段文字,实现点击文字进行链接的跳转,Navigator组件还可以嵌套view组件,比如我们点击某块的内容会进行一个跳转。和view组件一样, Navigator组件也是可以嵌套的。
比如我们在home.wxml里输入以下代码:
<view class="event-list"> <navigator url="/pages/home/imgshow/imgshow" class="event-link"> <view class="event-img"> <image mode="widthFix" src="https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/weapp.jpg" rel="external nofollow" ></image> </view> <view class="event-content"> <view class="event-title">零基础学会小程序开发</view> <view class="event-desc">通过两天集中的学习,你会循序渐进的开发出一些具有实际应用场景的小程序。 </view> <view class="event-box"> <view class="event-address">深圳南山</view> <view class="event-time">2018年9月22日-23日</view> </view> </view> </navigator></view>
在home.wxss里输入以下样式:
.event-list{ background-color: #fafbfc; padding: 20px 0; }.event-link{ margin: 10px; border-radius: 5px; background-color: #fff; box-shadow:5rpx 8rpx 10rpx rgba(53,178,225,0.26); overflow: hidden;}.event-img image{ width: 100%; }.event-content{ padding: 25rpx; }.event-title{ line-height: 1.7em; }.event-desc{ font-size: 14px; color: #666; line-height: 1.5em; font-weight: 200; }.event-box{ margin-top: 15px; overflow: hidden; }.event-address,.event-time{ float: left; color: #cecece; font-size: 12px; padding-right: 15px; }
WeUI是一套小程序的UI框架,所谓UI框架就是一套界面设计方案。有了组件,我们可以用它来拼接出一个内容丰富的小程序,而有了一个UI框架,就能让我们的小程序变得更加美观。
WeUI 是微信官方设计团队设计的一套同微信原生视觉体验一致的基础样式库。在手机微信里搜索WeUI小程序或者扫描WeUI小程序码即可在手机里体验。
我们还可以下载WeUI小程序的源码在开发者工具里查看它具体是怎么做的。
源码下载:WeUI小程序源码
下载解压压缩包之后可以看到weui-wxss-master文件夹,点击开发者工具工具栏里的项目菜单选择导入项目,之后就可以在开发者工具查看到WeUI的源代码了.
小任务:为什么是weui-wxss-master下的dist文件夹,而不是weui-wxss-master?你还记得什么是小程序的根目录吗?
结合WeUI在开发者工具模拟器的实际体验以及WeUI的源代码,找到WeUI基础组件里的article、flex、grid、panel,表单组件里的button、list与之对应的pages文件夹下的页面文件,并查看该页面文件的wxml、wxss代码,了解它们是如何写的。
小任务:点击开发者工具栏里的预览,用手机微信扫描二维码体验,看看与官方的WeUI小程序有什么不同。
WeUI的界面虽然非常简单,但是背后却包含着非常多的设计理念,这一点我们可以阅读小程序设计指南,来加深对UI设计的理解。
前面我们已经下载了WeUI的源代码,其实WeUI的核心文件是weui.wxss。那我们如何在我们的模板小程序里使用WeUI的样式呢?
首先我们在模板小程序的根目录(注意是在第一节建好的模板小程序里)下新建一个style的文件夹,然后把weui小程序dist/style目录下的weui.wxss文件粘贴到style的文件夹里。
├── pages├── image├── style│ ├── weui.wxss├── app.js├── app.json├── app.wxss
使用开发者工具打开模板小程序的app.wxss文件的第二行添加以下代码:
@import 'style/weui.wxss';
这样weui的css样式就被引入到我们的小程序中啦,那我们该如何使用WeUI已经写好的样式呢?
前面我们已经了解了如何给wxml文件添加文字、链接、图片等元素和组件,我们希望给这些元素和组件的排版更加结构化,不再是单纯的上下关系,还有左右关系,以及左右上下嵌套的关系,这个时候就需要了解布局方面的知识啦。
布局也是一种样式,也属于css方面的知识哦,所以大家应该知道该在哪里给组件添加布局样式啦~没错,就是在wxss文件里~
小程序的布局采用的是Flex布局。Flex是Flexible Box的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。
我们可以在home.wxml输入以下代码:
<view class="flex-box"> <view class='list-item'>Python</view> <view class='list-item'>小程序</view> <view class='list-item'>网站建设</view></view>
为了让list-item更加明显我们给他们添加一个边框、背景、高度以及居中处理,比如在home.wxss文件写入以下样式代码:
.list-item{ background-color: #82c2f7; height: 100px; text-align: center; border:1px solid #bdd2f8;}
让组件变成左右关系
我们可有看到这三个项目是上下关系,但要改成左右关系,那该怎么弄呢?我们可以在home.wxss文件写入以下样式:
.flex-box{ display: flex;}
我们给外层(也可以叫做父级)的view组件添加display:flex之后,这三个项目就成了左右结构的布局啦~
让组件的宽度均分
我们希望这三个list-item的view组件三等分该如何处理呢?我们只需要给list-item添加一个flex:1的样式,
.list-item{ flex:1;}
那怎么弄二等分、四等分、五等分呢,只需要相应增减list-item即可,有多少个list-item就有多少等分,比如四等分。
<view class="flex-box"> <view class='list-item'>Python</view> <view class='list-item'>小程序</view> <view class='list-item'>网站建设</view> <view class='list-item'>HTML5</view></view>
flex是弹性布局,flex:1这个样式是一个相对概念,这里的相对是指这每个list-item的宽度之比都为1。
让组件内的内容垂直居中
我们看到list-item组件里的文字都不是垂直居中的,我们希望文字垂直居中该如何处理呢?我们需要给list-item的组件添加以下样式。
.list-item{ display: flex; align-items:center;/*垂直居中*/ justify-content: center;/*水平居中*/ }
为什么会给list-item加了一个display:flex的样式呢?和前面一样display:flex是要给父级标签添加的样式,要让list-item里面的内容实现flex布局,就需要给list-item添加display:flex样式啦。
当然flex还可以表示更加复杂的布局结构,比如左中右,左1/4,中1/2,右1/4等等,由于小程序以及手机UI设计不会弄那么复杂,所以这里就不做更多介绍啦。
全局样式与局部样式的概念大家需要了解一下,在app.wxss技术文档里是这样描述的:
定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在 page 的 wxss 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.wxss 中相同的选择器。
也就是说我们在app.wxss引入了weui.wxss,我们新建的所有的二级页面,都会自动拥有weui的样式~
在WeUI小程序里我们发现在基础组件里也有Flex,它的目的就是把内容给几等分。我们可以在模拟器里看到有一等分(100%),二等分、三等分、四等分。它实现的原理和我们上面讲的一样。
大家可以找到WeUI文件结构下example文件夹里的flex页面,我们可以阅读一下flex.wxml的代码。比如我们找到二等分的代码:
<view class="weui-flex"> <view class="weui-flex__item"><view class="placeholder">weui</view></view> <view class="weui-flex__item"><view class="placeholder">weui</view></view></view>
我们可以直接把这段代码复制粘贴到home.wxml里,我们发现即使我们没有给weui-flex和weui-flex__item添加样式,但是它们自动就有了flex布局,这是因为我们之前把weui.wxss引入到了app.wxss文件里,关于flex布局weui.wxss都已经给我们写好啦,是不是省了我们很多的麻烦?
也就是说,WeUI框架的引入是因为它把很多css样式写好啦,省去了我们的一些麻烦,我们要使用它就是需要把我们的组件的选择器如class、id和WeUI框架的保持一致即可。
前面我们在写home.wxml文章内容的时候,不同的标题要设置不同的大小、间距,文章正文也要设置内外边距,图片也要设置模式,当然这些样式我们都可以自己写,但是看起来会不那么美观,由于是小程序,如果文章的外观和微信的设计风格一致,看起来就会舒服很多。
WeUI的设计风格符合小程序设计指南,所以它的一些样式标准值得我们参考。
设计规范:微信小程序设计指南
哦,原来WeUI框架不仅可以让我们少写一些CSS样式,引入它还可以使我们的小程序设计符合规范。我们觉得它不好看,可以不引入它自己写css吗?当然可以啦,WeUI框架只是一个方便我们的辅助工具而已,所使用的也都是我们之前掌握到的CSS的知识,在大家CSS熟练之后,我们也可以抛开它自由发挥。
那我们如何使用WeUI框架美化文章呢?我们可以先体验WeUI小程序基础组件下的article,然后打开WeUI小程序文件结构下的example的article页面下的article.wxml,copy参考它的代码,改成以下的代码
<view class="page__bd"> <view class="weui-article"> <view class="weui-article__h1">HackWork技术工坊</view> <view class="weui-article__section"> <view class="weui-article__p">HackWork技术工坊是技术普及的活动,致力于以有趣的形式让参与者学到有用的技术。任务式实战、系统指导以及社区学习能有效提高技术小白学习技术的效率以及热情。 </view> <view class="weui-article__section"> <view class="weui-article__h3">任务式实战</view> <view class="weui-article__p"> 每节都会有非常明确的学习任务,会引导你循序渐进地通过实战来掌握各个知识点。只有动手做过的技术才属于自己。 </view> <view class="weui-article__p"> <image class="weui-article__img" src="https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/img1.jpg" rel="external nofollow" mode="aspectFit" style="height: 180px" /> </view> <view class="weui-article__h3">系统指导</view> <view class="weui-article__p"> 针对所有学习问题我们都会耐心解答,我们会提供详细的学习文档,对大家的学习进行系统指导。 </view> <view class="weui-article__p"> <image class="weui-article__img" src="https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/img2.jpg" rel="external nofollow" mode="aspectFit" style="height: 180px" /> </view> <view class="weui-article__h3">社区学习</view> <view class="weui-article__p"> 参与活动即加入了我们的技术社区,我们会长期提供教学指导,不必担心学不会,也不用担心没有一起学习的伙伴。 </view> </view> </view> </view></view>
使用WeUI框架的核心在于使用它写好了样式的选择器,结构与形式不完全受限制。比如上面的class为weui-article的view组件的样式在我们之前引入的weui.wxss就写好了,样式为
.weui-article{ padding:20px 15px; font-size:15px}
所以我们只需要给view组件添加weui-article的class,view组件就有了这个写好了的样式啦。weui-articleh3,weui-articlep也是如此。
如果想给weui-article__h3这个小标题换一个颜色,该怎么处理呢?通常我们不推荐直接修改weui.wxss(除非你希望所有的小标题颜色都替换掉)。我们可以给要替换颜色的view组件再增加一个class选择器,再来添加样式即可。比如把社区学习这里的代码改成:
<view class="weui-article__h3 hw__h3">社区学习</view>
然后在 home.wxss 文件里添加
.hw__h3{ color:#1772cb;}
一个view组件可以有多个class,这样就非常方便我们定向给某个组件添加一个特定的样式啦。
可能上面新闻列表的样式很多人不喜欢,想换一个其他的排版样式,数据分离有个好处就是我们可以不用修改数据本身,而直接修改wxml里的排版即可。修改排版样式的核心在wxss,也就是修改css样式。
我们想让图文结构是上下结构,我们可以删掉weui框架所特有的一些选择器,也就是删掉一些class,比如weui-media-box__hd_in-appmsg,weui-media-box__thumb等等,然后添加一些选择器,也就是加入一些自己命令的id和class。
<view class="page__bd" id="news-list"> <view class="weui-panel__bd"> <navigator url="" class="news-item" hover-class="weui-cell_active"> <view class="news-img"> <image mode="widthFix" class="" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png" /> </view> <view class="news-desc"> <view class="weui-media-box__title">小程序可以在 PC 端微信打开了</view> <view class="weui-media-box__desc">微信开始测试「PC 端支持打开小程序」的新能力,用户终于不用在电脑上收到小程序时望手机兴叹。</view> <view class="weui-media-box__info"> <view class="weui-media-box__info__meta">深圳</view> <view class="weui-media-box__info__meta weui-media-box__info__meta_extra">8月9日</view> </view> </view> </navigator> </view> </view>
然后我们在home.wxss里添加我们想要添加的css样式。
#news-list .news-item{ margin: 15rpx; padding: 15rpx; border-bottom: 1rpx solid #ccc }#news-list .news-img image{ width: 100%; }#news-list .news-desc{ width: 100%; }
pc网页、移动端网页等也会有非常丰富的UI框架,它们和小程序的WeUI框架的核心与原理都是一样。由于它们可以大大提升我们写页面的开发速度,所以应用得非常普遍
CSS是编程科技与设计艺术结合得最为完美的一项技术,编程的优雅在于代码的清晰可读,而设计的优雅在于能够结合技术为用户带来一场视觉和交互的盛宴。借助于CSS,不仅可以做出平面设计师常用的滤镜、渐变等设计效果,还可以设计出一些交互动画,增强用户的体验。
颜色渐变是设计师必不可少的,CSS linear-gradient() 函数用于创建一个表示两种或多种颜色线性渐变的图片。
使用开发者工具新建一个gradient的页面,然后在gradient.wxml页面输入以下代码:
<view class="gradient-display"> </view>
在gradient.wxss里输入:
.gradient-display{ background-image:linear-gradient(red, blue); width: 100vw; height: 100vh; } .gradient-display{ background-image:linear-gradient(red, blue); width: 100vw; height: 100vh; }
我们发现因为背景图片使用了linear-gradient属性,它默认的渐变方向是从上到下,第一个颜色(这里为红色red)是起始颜色,第二个颜色(这里为蓝色blue)为停止颜色。
将.gradient-display里的backgound-image的值依次换成以下:
改变渐变的方向background-image: linear-gradient(45deg, blue, red); /* 渐变轴为45度,从蓝色渐变到红色 */
也可以这样写,代码具体含义可以去参考技术文档了解
background-image:linear-gradient(to left top, blue, red); /* 从右下到左上、从蓝色渐变到红色 */
增加更多颜色变换
background-image:linear-gradient(0deg, blue, green 40%, red); /* 从下到上(渐变轴为0度),从蓝色开始渐变、到高度40%位置是绿色渐变开始、最后以红色结束 */
颜色百分比
background-image: linear-gradient(19deg, rgb(33, 212, 253) 0%, rgb(183, 33, 255) 100%);
小任务:参考径向渐变技术文档下面的examples,在小程序里实现一个径向渐变的案例。通过实战的方式,理解技术文档就像辞典,前期学习不必做到全部看懂,能够拿来用就行。
滤镜对于设计师来说一定不会陌生,CSS也有滤镜filter属性,可以对图片进行高斯模糊、调整对比度、转换为灰度图像、色相旋转、图片透明等操作。
相比于Photoshop等工具的滤镜效果来说,使用CSS可以批量化处理图片滤镜效果,而且通过编程的手段不仅可以叠加各种效果,而且还能与交互相结合。
这里我们主要介绍用的最多的三个滤镜效果,高斯模糊blur,图片变灰grayscale(%),图片透明opacity(%),其他滤镜效果大家以后可以阅读技术文档。
技术文档:滤镜属性
使用开发者工具新建一个filter页面,然后在filter.wxml输入:
<view class="filter-display"> <view>blur高斯模糊</view> <image class="blur" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> <view>grayscale图片变灰</view> <image class="grayscale" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> <view>opacity图片透明</view> <image class="opacity" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> <view>多个滤镜叠加,注意css的写法即可</view> <image class="multiple" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> </view>
然后在filter.wxss里输入:
.filter-display img{ width: 150px;height: auto; } .blur{ filter: blur(8px); } .grayscale{ filter: grayscale(90%); } .opacity{ filter: opacity(25%); } .multiple{ filter: blur(8px) grayscale(90%) opacity(25%); }
图片由灰色变为彩色
在实际应用中,会在网站上添加很多不同色系的图片,比如合作伙伴的logo、嘉宾的照片、新闻图片等,为了让照片和网站的色系保持一致,因此就需要对所有图片进行统一的滤镜处理,而将图片变灰是比较常见的一种做法。
有时我们还会给这些变灰的图片添加一个交互特效,那就是当鼠标悬停在图片上时,图片会由灰色变为彩色。
在filter.wxml输入如下代码:
<view class="filter-display"> <text>将鼠标悬停(模拟器)或手指(手机微信)按住或放开图片查看效果</text> <view class="grayscale" hover-class="grayscale-hover" > <image mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> </view> </view>
在技术文档view组件,我们可以看到hover-class是指定按下去的样式类。
在filter.wxss里添加如下样式:
.filter-display image{ width: 150px;height: auto; } .grayscale{ filter: grayscale(90%); } .grayscale-hover{ filter:grayscale(0); }
高斯模糊的背景
高斯模糊是UI设计师经常用到的一个特效。平面设计师通常是人工、手动去给图片设计样式,而UI设计师则可以结合CSS给相同类别的所有图片添加统一的样式,比如我们希望每个用户信息页的背景、每篇文章顶部的背景都不一样。
在filter.wxml输入如下代码:
<view class="userinfo-display"> <view class="blur-bg"></view> <view class="user-img"> <image src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png"></image> </view> </view>
在filter.wxss里添加如下样式:
.blur-bg { width: 750rpx;height: 300rpx;overflow: hidden; background: url(https://hackwork-1251009918.cos.ap-shanghai.myqcloud.com/handbook/html5/blurimg.jpg); background-size: cover; position: fixed; filter: blur(15px); z-index: -1; } .user-img{ width: 750rpx;height: 300rpx; display: flex; justify-content: center; align-items:center; } .user-img image { width: 80rpx;height: 80rpx; border-radius: 100%; }
UI设计师在处理网页元素的设计时,不会像平面设计师一样可以对每个元素都差异化的精心雕琢,毕竟CSS是没法做到像Photoshop等设计工具那样复杂,但是他可以做到批量。所以相对于平面设计师而言,UI设计师更注重单调且统一。
CSS transform属性能通过修改CSS视觉格式化模型的坐标空间旋转、缩放、倾斜或平移给定的组件。
关于变形Transform、过渡Transition、动画Animation的技术文档,大家先不要急着钻研,粗略浏览一下即可,以后有时间再来研究。
技术文档:CSS 变形属性transform
使用微信开发者工具新建一个transform页面,在transform.wxml里输入以下代码
<view class="transform-display"> <view>缩放,scale(x,y),x为长度缩放倍数,y为宽度缩放倍数,如果只有一个值,则是长和宽缩放的倍数</view> <image class="scale" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png" ></image> <view>平移,translate(x,y),x为x轴平移的距离,y为y轴平移的距离,如果只有一个值,则是x和y轴缩放的距离,值可以为负数</view> <image class="translate" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png" ></image> <view>旋转,rotate()里的值为旋转的角度</view> <image class="rotate" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png" ></image> <view>倾斜,skew()里的值为旋转的角度</view> <image class="skew" mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png" ></image> </view>
在transform.wxss文件里添加如下样式:
.transform-display image{ width: 80px;height: 80px; } .scale{ transform: scale(1,0.5); } .translate{ transform: translate(500px,20px); } .rotate{ transform: rotate(45deg); } .skew{ transform: skew(120deg); }
CSS transitions 可以控制组件从一个属性状态切换为另外一个属性状态时的过渡效果。
技术文档:CSS 过渡属性Transition
建议大家只用简写属性transition,多个属性连着一起写会更好一些,transition的语法如下,语法比较复杂,大家可以结合后面的实际案例
.selector { transition: [transition-property] [transition-duration] [transition-timing-function] [transition-delay]; }
背景颜色的变化
同样还是把下面的代码输入到小程序的页面当中,通过实战的方式来查看效果。
使用开发者工具新建一个transition页面,然后在transition.wxml页面里面输入以下代码:
<view class="transition-display"> <view class="box bg-color" hover-class="bg-color-hover"></view> </view>
然后在transition.wxss里面输入:
.box{width: 150px;height: 150px;cursor: pointer;} .bg-color{ background-color:green; } .bg-color-hover{ background-color: yellow; transition: background-color 5s ease-out 3s; }
动画是需要触发的,这里我们使用的是悬停hover-class来触发效果,把鼠标放在元素上8秒以上,看一下正方形的背景颜色有什么变化。
了解了效果之后,我们再来结合实际案例理解语法:
技术文档:可设置动画的属性列表
我们来查看一个综合案例,在transition.wxml里输入
<view> <text>盒子的多个属性一起动画: width, height, background-color, transform. 将鼠标或手指悬停在盒子上查看动画之后放开。</text> <view class="animatebox" hover-class="animatebox-hover"></view> </view>
在transition.wxss里输入
.animatebox { display: block; width: 100px; height: 100px; background-color: #4a9d64; transition:width 2s, height 2s, background-color 2s, transform 2s; } .animatebox-hover { background-color: #cd584a; width:200px; height:200px; transform:rotate(180deg); }
CSS animations 使得可以将从一个CSS样式配置转换到另一个CSS样式配置。动画包括两个部分:描述动画的样式规则和用于指定动画开始、结束以及中间点样式的关键帧。
技术文档:CSS动画属性Animation
<view class="fadeIn"> <view>注意会有一个无限颜色渐变变化的动画</view> <image mode="widthFix" src="//7n.51coolma.cn/attachments/knowledge/202006/77455.png" ></image> </view>
在wxss里输入以下代码
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .fadeIn { animation: 4s linear 0s infinite alternate fadeIn; }
CSS3 动画库 Animate.css
Animate.css是一个有趣的,跨浏览器的css3动画库,只需要你引入一个CSS文件,就能够给指定的元素添加动画样式。
技术文档:Animate.css
它预设了抖动(shake)、闪烁(flash)、弹跳(bounce)、翻转(flip)、旋转(rotateIn/rotateOut)、淡入淡出(fadeIn/fadeOut)等多达 80种动画效果,几乎包含了所有常见的动画效果。
小任务:参考Animate.css的shake抖动,在小程序实现一个组件抖动的案例。除了引入一些weui这样的样式框架,还有一些开源的库我们也可以学习和借鉴,更多内容则需要大家以后可以深入学习了。
今天我们来了解一下数据绑定,什么是数据绑定呢?就是把WXML 中的一些动态数据分离出来放到对应的js文件的 Page 的 data里。
数据绑定这个概念其实很多学过网页开发的朋友也会比较困惑。大家可以不必执着于这个深奥的概念,而是先来动手做一下了解是一个什么效果。在潜移默化里,你会get到前端里一个非常了不得的技术知识哦~
我们可以在小程序的页面文件wxml里写这样一段代码,比如我们可以写在home.wxml里面,
<view>张明,您已登录,欢迎</view>
这样的场景我们经常遇到,不同的人使用一款App或者H5的时候,页面会根据不同的登录人不同的用户信息。
我们可以这样把wxml的代码修改成这样:
<view>{{username}},您已登录,欢迎</view>
然后再在home.js的data里面写这样一段代码,最终呈现的结果是:
data: { username:"张明" },
在模拟器我们看到呈现的结果和之前一样,我们可以data里面的”张明”修改成任何一个人的名字,前端的页面也会相应有所改变,如果通过函数的方式根据不同的用户修改username的值,这样不同的登录的人登录就会显示相应的用户名。
大家再回头来回顾一下json语法,这里的username是字段名称,也就是变量,冒号:后面的是值。在wxml文件里,只需要用双大括号{{}}把变量名包起来,就能把data里面的变量给渲染出来。
通过前面的案例我们了解到WXML 中的动态数据均来自对应 Page 的 data。 data 是小程序的页面第一次渲染使用的初始数据。小程序的页面加载时, data 将会以 JSON字符串的形式由逻辑层传至渲染层,因此 data中的数据必须是可以转成 JSON的类型:字符串String,数字Number,布尔值Boolean,对象Object,数组Array。
数组Array,结合之前所学,我们再来回顾一下:数组由中括号[ ]来分割,有点类似于列表;
数据类型在编程语言里是一个非常重要的概念,大家可以先只需要知道是啥就可以,不必强行理解哦。就像我们把不同的人分为男、女、深圳人、程序员等不同类型一样,数据类型就是一种对不同类型的数据进行了一个分类而已,只是为了区分它们才有了不同的格式规范它们。
通过数据绑定,我们还可以把 style、class 、id等属性分离出来来控制组件的样式等信息。
使用开发者工具在home.wxml里输入以下内容:
<navigator id="item-{{id}}" class="{{itemclass}}" url="{{itemurl}}" > <image style="width: {{imagewidth}}" mode="{{imagemode}}" src="{{imagesrc}}"></image></navigator>
需要按照json的语法,把下面data里面的数据添加到home.js的data里面:
data: { id: 233, itemurl:"/pages/home/imgshow/imgshow", itemclass:"event-item", imagesrc: "https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/weapp.jpg", imagemode:"widthFix", imagewidth:"100%", },
然后在模拟器里查看显示的效果,发现显示的结果上和我们之前不采用数据绑定没有什么区别,但是用数据绑定的好处是为我们以后添加大量数据以及进行编程更新打下了基础。字符串与数字
在前面我们以前说过,数字Number与字符串String在Excel里是不同的,在小程序(也就是JavaScript)里也是不同的。我们来实战了解一下,在home.wxml里输入以下代码:
<view>两个数字Number相加:{{love1+forever1}}</view><view>两个字符串String相加:{{love2+forever2}}</view>
然后把下面data里的数据添加到home.js里面:
data: { love1:520, love2:"520", forever1:1314, forever2:"1314", }
在这里我们可以看到使用””双引号包住的是字符串格式,而没有使用双引号的是数字格式。
可以看到数字格式的数字相加和四则运算的加法是一致的,而字符串与字符串的相加是拼接。+ 加号在JavaScript里既可以扮演四则运算符的角色,也可以进行拼接,取决于数据的格式。
小任务:数字格式的520和字符串格式的520,它们在页面的显示上虽然是一样的,但是字符串格式可以拼接,而数字格式的数字,则方便以后我们进行数字大小的比较。请问出身年份是应该使用数字格式,还是字符串格式?身份证号码呢?
在前面我们就已经接触过数组,比如pages配置项就是小程序里所有页面的一个列表。数组Array是值的有序集合,每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。这个索引是从0开始的非负整数,也就是0,1,2,3,4,5…..
在home.wxml里输入以下代码:
<view>互联网快讯</view><view>{{newstitle[0]}}</view>
然后把下面data里的数据添加到home.js里面:
data: { newstitle:[ "瑞幸咖啡:有望在三季度达到门店运营的盈亏平衡点", "腾讯:广告高库存量还是会持续到下一年", "上汽集团云计算数据中心落户郑州,总投资20亿元", "京东:月收入超2万元快递小哥数量同比增长163%", "腾讯:《和平精英》日活跃用户已超五千万", ], }
我们发现数组的第一条数据就显示出来了,也就是说{{array[0]}}对应着数组array的第一项,0就是索引的第一个位置,也就是我们可以使用数组名+中括号[ ]+索引的位置来访问数组的某一条数据。
小任务:我们已经知道newstitle[0]显示的是第1条新闻的标题,那怎么显示第5条新闻的标题?还记得pages配置项的第一项就是小程序的初始页面么,你现在知道它是怎么做到的么?
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。对象是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法)。
有的时候一个对象有多个属性,就拿电影来说,就有电影名称,国家,发行时间、票价、评价等等无数个属性,我们该如何把这些呈现在页面上呢?
在home.wxml文件里输入以下代码:
<image mode="widthFix" src="{{movie.img}}" style="width:300rpx"></image><view>电影名:{{movie.name}}</view><view>英文名:{{movie.englishname}}</view><view>国家:{{movie.country}}</view><view>发行年份:{{movie.year}}</view><view>简述:{{movie.desc}}</view>
在与之对应的home.js的data里,添加如下数据:
data: { movie: { name: "肖申克的救赎", englishname:"The Shawshank Redemption", country:"美国", year:1994, img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp", desc: "有的人的羽翼是如此光辉,即使世界上最黑暗的牢狱,也无法长久地将他围困!" }, },
这样,对象Object类型的数据就被渲染出来啦。也就是在双大括号{{}}里,输入变量movie+点+属性名即可,这就是对象的点表示法。
对象是可以嵌套的,也就是一个对象可以作为另外一个对象的值,除了对象里套对象,数组里也可以套对象,对象里也可以套数组。把现实生活中的事物转化成错综复杂的数据,是非常重要的数据思维。
比如上面我们只列出了豆瓣排名第1的电影,那top5前五的电影呢,它就是一个列表;每一部电影的工作人员又有导演、编剧、演员,而每一部电影的演员名单又是一个列表,每个演员又有复杂的属性,比如姓名、出身年月、所获奖项(列表)…真的是子子孙孙无穷尽。当然简单的数据我们可以写在data里面,而如此复杂的数据就要使用到数据库啦。
比如我们把下面data里的数据添加到home.js里面:
movies:[ { name: "肖申克的救赎", englishname: "The Shawshank Redemption", country: "美国", year: 1994, img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp", desc: "有的人的羽翼是如此光辉,即使世界上最黑暗的牢狱,也无法长久地将他围困!", actor:[ { name:"蒂姆·罗宾斯", role:"安迪·杜佛兰" }, { name:"摩根·弗里曼", role:"艾利斯·波伊德·瑞德" }, ] }, { name: "霸王别姬", englishname: "Farewell My Concubine", country: "中国", year: 1993, img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2561716440.webp", desc: "风华绝代", actor: [ { name: "张国荣", role: "程蝶衣" }, { name: "张丰毅", role: "段小楼" }, ] }, ],
那我们应该如何把豆瓣电影排名第2的霸王别姬的主演之一的张国荣的名字给渲染到页面呢? {{movies[1].actor[0].name}}表示的是电影列表里的第2部电影, {{movies[1].actor[0]}}表示的是第2部电影里的排名第一的主演, {{movies[1].actor[0].name}}则表示的是主演的名字啦。
在home.wxml里输入以下代码测试看一下显示的是不是张国荣?
<view>豆瓣电影排名第2、最重要的主演演员名:</view><view>{{movies[1].actor[0].name}}</view>
那如何把第2部电影里的所有数据都渲染出来呢?
<image mode="widthFix" src="{{movies[1].img}}" style="width:300rpx"></image><view>电影名:{{movies[1].name}}</view><view>英文名:{{movies[1].englishname}}</view><view>发行地:{{movies[1].country}}</view><view>发行年份:{{movies[1].year}}</view><view>简述:{{movies[1].desc}}</view>
小任务:在home.wxml输入以下代码会是什么结果?为什么不能这样?
<view&{{movies}}</view&
<view&{{movies[1]}}</view&
<view&{{movies[1].actor}}</view&
以上我们只是输出了数组里的单条数据,或者对象嵌套的数据里的单条数据,如果是商品列表、电影列表、新闻列表这些我们应该如何渲染到页面呢?后面一节我们将会介绍列表渲染和条件渲染。
在数据绑定的章节,我们学习了如何渲染数组类型和对象类型的数据,但是当时只是输出了数组里或对象的数组里的某一个数据,如果是要输出整个列表呢?这个时候就需要用到列表渲染啦。
相同的结构是列表渲染的前提
在实际的开发场景里,商品、新闻、股票、收藏、书架列表等都会有几千上万条的数据,他们都有一个共同的特征就是数据的结构相同,这也是我们可以批量化渲染的前提。还是以前面的互联网快讯的数据为例,下面的新闻标题他们的结构就非常单一。
data: { newstitle:[ "瑞幸咖啡:有望在三季度达到门店运营的盈亏平衡点", "腾讯:广告高库存量还是会持续到下一年", "上汽集团云计算数据中心落户郑州,总投资20亿元", "京东:月收入超2万元快递小哥数量同比增长163%", "腾讯:《和平精英》日活跃用户已超五千万", ], }
那我们应该如何把整个列表都渲染出来呢?这里涉及到JavaScript数组遍历的知识,JavaScript数组遍历的方法非常多,因此小程序数组的渲染也有很多方法,所以大家看技术文档的时候会有点混乱。
技术文档:列表索引
<view wx:for="{{newstitle}}" wx:key="*this"> {{item}}</view>
这里wx:for=”{{newstitle}}”,也就是在数组newstitle里进行循环,*this代表在 for 循环中的 item 本身,而{{item}}的item是默认的。也可以使用如下方法:
<view wx:for-items="{{newstitle}}" wx:for-item="title" wx:key="*this"> {{title}} </view>
默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item;
使用 wx:for-item 可以指定数组当前元素的变量名,使用 wx:for-index 可以指定数组当前下标的变量名。
首先我们把多部电影的数据写在data里面,相当于是一个数组类型的数据里面,包含着多个对象类型的数据。
movies: [{ name: "肖申克的救赎", img:"https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp", desc:"有的人的羽翼是如此光辉,即使世界上最黑暗的牢狱,也无法长久地将他围困!"}, { name: "霸王别姬", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp", desc: "风华绝代。" }, { name: "这个杀手不太冷", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p511118051.webp", desc: "怪蜀黍和小萝莉不得不说的故事。" }, { name: "阿甘正传", img: "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p510876377.webp", desc: "一部美国近现代史。" }, { name: "美丽人生", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p510861873.webp", desc: "最美的谎言。" }, { name: "泰坦尼克号", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p457760035.webp", desc: "失去的才是永恒的。" }, { name: "千与千寻", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1606727862.webp", desc: "最好的宫崎骏,最好的久石让。" }, { name: "辛德勒名单", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p492406163.webp", desc: "拯救一个人,就是拯救整个世界。" }, ],
然后我们也把数据绑定章节的代码改一下,添加一个wx:for语句,来把列表里的数据给循环渲染出来。
<view class="page__bd"> <view class="weui-panel weui-panel_access"> <view class="weui-panel__bd" wx:for="{{movies}}" wx:for-item="movies" wx:key="*item"> <navigator url="" class="weui-media-box weui-media-box_appmsg" hover-class="weui-cell_active"> <view class="weui-media-box__hd weui-media-box__hd_in-appmsg"> <image class="weui-media-box__thumb" mode="widthFix" src="{{movies.img}}" sytle="height:auto"></image> </view> <view class="weui-media-box__bd weui-media-box__bd_in-appmsg"> <view class="weui-media-box__title">{{movies.name}}</view> <view class="weui-media-box__desc">{{movies.desc}}</view> </view> </navigator> </view> </view> </view>
这里用到了一个wx:for-item,给了它一个值是movies,其实也可以是其他值,比如dianying,那{{movies.img}}、{{movies.name}}、{{movies.desc}}也相应的为{{dianying.img}}、{{dianying.name}}、{{dianying.desc}}。为什么这样?这个也是JavaScript的一个知识,可以先不必深究。
我们发现电影列表里面的图片是变形的,为什么呢?回到我们之前学的image组件,我们去查看一下image组件文档,从技术文档里找答案。
技术文档:image组件文档
在技术文档里,我们发现如果我们不写图片的模式mode,图片的模式默认为scaleToFill,也就是不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素。
那我们希望图片保持宽度不变,高度自动变化,保持原图宽高比不变,那就需要用到widthFix的模式啦。
我们给image组件添加widthFix模式,
<image class="weui-media-box__thumb" mode="widthFix" src="{{movies.img}}"
添加完模式之后,发现图片比例显示正常了,但是image组件出现了溢出的现象,这是因为weui给class为weui-media-box__hd_in-appmsg 的组件定义了一个height:60px的css样式,也就是限制了高度,那我们可以在home.wxss里添加
.weui-media-box__hd_in-appmsg{ height: auto; }
把这个height:60px给覆盖掉。
css的覆盖原理是按照优先级来的,越是写在css文件后面的样式优先级越高,会把前面的给覆盖掉;在小程序里页面里的wxss的优先级比app.wxss的优先级更高,所以也可以覆盖掉。
点击电影列表是没有链接的,大家可以回顾前面的知识点,给每部电影添加链接,在pages配置项里把每个页面的路径都添加上。
大家经常会在App里看到一些分类都是以九宫格的方式来布局的。我们在WeUI小程序里用模拟器找到基础组件下的grid,看一下grid所呈现的样式。然后参考WeUI小程序文件结构里example文件夹下grid页面文件grid.wxml里的代码,在home.wxml里添加代码:
<view class="page__bd"> <view class="weui-grids"> <block wx:for="{{grids}}" wx:for-item="grid" wx:key="*this"> <navigator url="" class="weui-grid" hover-class="weui-grid_active"> <image class="weui-grid__icon" src="{{grid.imgurl}}" /> <view class="weui-grid__label">{{grid.title}}</view> </navigator> </block> </view> </view>
在WeUI的源代码里,我们看到有一个<block&的标签,这个标签主要是说明里面包含的是一个多节点的结构块,换成<view&组件也没有太大影响,就好像<text&换成<view&没有影响一样,用不同的组件主要是为了一个区分。
然后在home.js添加data数据
grids:[ { imgurl:"https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon1.png", title:"招聘" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon2.png", title: "房产" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon3.png", title: "二手车新车" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon4.png", title: "二手" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon5.png", title: "招商加盟" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon6.png", title: "兼职" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon7.png", title: "本地" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon8.png", title: "家政" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon9.png", title: "金币夺宝" }, { imgurl: "https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/icon10.png", title: "送现金" }, ]
大家就可以看到一个很多App界面都有的一个九宫格了。这里的九宫格是一行三列,如何让九宫格变成一行五列呢?首先我们要知道为什么这个九宫格会变成一行三列,在weui.wxss里给weui-grid定义了一个width:33.33333333%的样式,我们可以在home.wxss里添加一个样式来覆盖原有的宽度。
.weui-grid{ width: 20%; }
List样式参考
大家可以先在开发者工具的模拟器里体验一下WeUI小程序表单下面的List的样式,以及找到list样式所对应的wxml代码,在开发者工具的文件目录的example/list目录下。我们可以参考一下里面的代码,并结合前面的案例在home.wxml里面输入以下代码:
<view class="weui-cells weui-cells_after-title"> <block wx:for="{{listicons}}" wx:for-item="listicons"> <navigator url="" class="weui-cell weui-cell_access" hover-class="weui-cell_active"> <view class="weui-cell__hd"> <image src="{{listicons.icon}}" style="margin-right: 5px;vertical-align: middle;width:20px; height: 20px;"></image> </view> <view class="weui-cell__bd">{{listicons.title}}</view> <view class="weui-cell__ft weui-cell__ft_in-access">{{listicons.desc}}</view> </navigator> </block> </view>
在home.js的data里添加以下数据:
listicons:[{ icon:"https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/listicons1.png", title:"我的文件", desc:"" }, { icon:"https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/listicons2.png", title:"我的收藏", desc:"收藏列表" }, { icon:"https://hackweek.oss-cn-shanghai.aliyuncs.com/hw18/hackwork/weapp/listicons3.png", title:"我的邮件", desc:"" } ],
再来查看效果,这里第一个和第三个的desc没有写内容,也是不影响这个列表展示的
在前面我们已经接触过表示文本的<text>组件、表示图像的<image>组件、表示视图容器的<view>组件,表示链接的<navigator>组件,这些组件大大丰富了小程序的结构布局和元素类型,接下来我们还将介绍一些组件。
前面我们已经通过实战的方式接触了一些组件,这个时候我们再回头理解一些基础的概念,那就是组件的属性。
公共属性是指小程序所有的组件都有的属性,比如id、class、style等,而不同属性的值就是数据,有数据就有数据类型。
技术文档:小程序组件
大家可以打开上面的技术文档,快速了解一下组件的公共属性有哪些,以及属性有哪些类型,各个类型的数据类型和取值说明。而不同的组件除了都有公共属性外,还有自己的特有属性。查阅技术文档,大家能够理解多少是多少,不要去强行理解和记忆。在实际开发中,很多属性我们不会
小任务:通过技术文档了解一下<text&组件、<image&组件、<view&组件、<navigator&组件有哪些私有属性。
擅于查阅技术文档,是任何方向的程序员必备的非常重要的能力,就跟学英语查词典一样。在实际开发中,一个新的技术方向你所能依赖的不再有老师这样的角色,因为没有人有义务教你。技术文档和搜索能力是你最可信赖的依靠。
很多App和小程序的页面顶部都有一个图片的轮播,小程序有专门的轮播组件swiper。要详细了解轮播组件swiper,当然少不了要阅读官方的技术文档啦
技术文档:轮播组件swiper
使用开发者工具,在home.wxml里输入以下代码:
<view class="home-top"> <view class="home-swiper"> <swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}" indicator-color="{{indicatorColor}}" indicator-active-color="{{activecolor}}"> <block wx:for="{{imgUrls}}" wx:key="*this" > <swiper-item> <image src="{{item}}" style="width:100%;height:200px" class="slide-image" mode="widthFix" /> </swiper-item> </block> </swiper> </view> </view>
然后在home.js里的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' ], interval: 5000, duration: 1000, indicatorDots: true, indicatorColor: "#ffffff", activecolor:"#2971f6", autoplay: true,
要构成一个完整的轮播,除了配置相同尺寸规格的图片以外,还可以配置轮播时的面板指示点、动画效果、是否自动播放等。轮播组件swiper自带很有特有的属性,大家可以自己动手多去配置,结合开发者工具实战的效果,来深入理解技术文档对这些属性以及属性的取值的说明。
audio组件是音频组件,我们在home.wxml文件里输入以下代码:
<audio src="{{musicinfo.src}}" poster="{{musicinfo.poster}}" name="{{musicinfo.name}}" author="{{musicinfo.author}}" controls></audio>
然后在home.js里的data里添加以下数据:
musicinfo: { poster: 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000', name: '此时此刻', author: '许巍', src: 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E06DCBDC9AB7C49FD713D632D313AC4858BACB8DDD29067D3C601481D36E62053BF8DFEAF74C0A5CCFADD6471160CAF3E6A&fromtag=46', },
技术文档:audio组件技术文档
可能由于audio组件使用的场景和频次都非常低,audio组件以后就要被抛弃了,需要用到小程序的API来创建音乐播放。
video组件用来表示视频,我们在home.wxml文件里输入以下代码:
<video id="daxueVideo" src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" rel="external nofollow" rel="external nofollow" autoplay loop muted initial-time="100" controls event-model="bubble"> </video>
技术文档:video组件技术文档
大家可以结合实际效果和技术文档来理解以下属性,把上面案例的autoplay或者某个属性删掉查看一下具体效果,加深自己对组件属性的理解。
我们也可以把view、图片组件覆盖在地图map或视频video组件之上。比如我们希望在视频的左上角显示视频的标题以及在右上角显示商家的logo,就可以使用cover效果。
<video id="daxueVideo" src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" rel="external nofollow" rel="external nofollow" controls event-model="bubble"> <view class="covertext">腾讯大学:腾讯特色学习交流平台</view> <image class="coverimg" src="https://imgcache.qq.com/open_proj/proj_qcloud_v2/gateway/portal/css/img/nav/logo-bg-color.svg" rel="external nofollow" ></image> </video>
在wxss文件里输入以下代码;
.covertext{ width: 500rpx; color: white; font-size: 12px; position: absolute; top:20rpx; left:10rpx; } .coverimg{ width:100rpx;height:23rpx; position: absolute; right:10rpx; top:10rpx; }
要想在地图上标记一个地点,首先我们需要知道该地点的经纬度,这个时候就需要使用到坐标拾取器的工具。
经纬度获取:腾讯地图坐标拾取器
在搜索框里我们可以搜索“深圳腾讯大厦”,得到纬度为22.540503,经度为113.934528。
使用开发者工具,在home.wxml里输入以下代码:
<map id="myMap" style="width: 100%; height: 300px;" latitude="{{latitude}}" longitude="{{longitude}}" markers="{{markers}}" covers="{{covers}}" show-location ></map>
然后在home.js的data里添加以下代码:
latitude: 22.540503, longitude: 113.934528, markers: [{ id: 1, latitude: 22.540503, longitude: 113.934528, title: '深圳腾讯大厦' }],
在开发者工具的模拟器里我们就可以看到腾讯大厦的地图了,点击marker标记,就能看到自定义的名称深圳腾讯大厦了。点击开发者工具的预览,使用手机微信扫描生成的二维码,在手机微信里的地图和模拟器的略微有点不同。
技术文档:Map组件技术文档
在地图上标记多个点
注意从技术文档里我们可以了解到markers标记点用于在地图上显示标记的位置,它的数据类型是Array数组,我们也看到上面的案例它的数据是由中括号[ ] 包住的列表。也就是我们可以在地图上标记多个点。在markers里多添加几个坐标:
markers: [ { id: 1, latitude: 22.540503, longitude: 113.934528, title: '深圳腾讯大厦' }, { id: 2, latitude: 22.540576, longitude: 113.933790, title: '万利达科技大厦' }, { id: 3, latitude: 22.522807, longitude: 113.935338, title: '深圳腾讯滨海大厦' }, { id: 4, latitude: 22.547400, longitude: 113.944370, title: '腾讯C2' }, ],
这里标记了腾讯在深圳的4个办公地点,大家可以在手机上预览了解实际的效果。
地图是一个非常复杂的组件,除了marker,还有以下属性,大家可以根据实际需求自行研究。
通过前面的实战学习,相信大家在写代码的过程中,遇到了很多问题,在不断解决问题的过程中也总结了一些经验。在这一部分会总结一些开发中的经验以及小程序的优化、部署、上线。
尽管缩进并不会对小程序的代码产生什么影响(Python才会严格强调缩进,不同的缩进也有不同的意义),但是为了代码的可读性,缩进是必不可少的。缩进除了美观,还可以体现逻辑上的层次关系,鼠标移到编辑器显示代码行数的地方,可以看到有–减号,点击即对代码进行折叠与展开,这一功能在开发上可以让我们更容易理清代码的层次、嵌套关系,避免出现少了闭合的情况。
小程序的wxml、js、json、wxss等不同的文件类型,开发时,在缩进的安排上也会有所不同,这个就需要大家自己去阅读其他优秀项目的源代码来领会了,这里也无法一一详述。
缩进有两种方式,一种是使用Tab键,还有一种是使用空格,建议大家使用Tab。小程序默认一个缩进=一个Tab=2个空格,通常前端开发是一个Tab=4个空格,你如果不习惯,可以在设置里进行设置。
代码的可读、美观甚至优雅,是一个优秀的程序员应该去追求的,缩进也只是其中很小的一环。代码可读性高,既提升自己的开发效率,也利于团队的分享与协作,后期的维护等等。
微信开发者工具也有着和其他IDE和代码编辑器比较一致的快捷键,通过使用这些快捷键,可以大大提升我们编写代码的效率。Mac和Windows的快捷键组合略微会有所不同,大家可以自行阅读技术文档来了解。
技术文档:微信开发者工具快捷键
快捷键的目的是为了自己编写代码的方便,每个人的快捷键的使用习惯都会有所不同。当然最简单通用的Ctrl+C复制、Ctrl+V粘贴、Ctrl+X剪切、Ctrl+Z重做、Ctrl+S保存,Ctrl+F搜索等这些快捷键组合是非常通用的,建议大家都掌握。
微信开发者工具的快捷键组合里有几个值得大家多使用,
官方快捷键文档写得很不全,建议大家参考下面Visual Studio Code的快捷键PDF来对快捷键有一个更全面的了解。Mac快捷键、Windows快捷键,使用快捷键的目的是为了提升开发的效率,一切还是以你的习惯为主,不要为了快捷键而快捷键。
相信大家在前面实际的开发中经常会看到开发者工具调试器里的Console,它会比较有效的指出代码的错误的信息、位置等,是日常开发非常非常重要的工具,堪称编程的指路明灯。大家务必要养成查看错误Console的习惯,也要善于根据报错信息去搜索相关的解决方法。以后我们还会介绍它更多的用处,堪称神器,不可不了解。
小程序的代码编辑器也会为我们提供一些错误信息,比如出现红色的~,这个时候就要注意啦,你是不是出现字符是中文,漏了标点符号等比较低级而小儿科的错误。
开发者工具调试器里除了有Console,还有一个wxml标签页(可能被折叠,需要你展开),它可以让我们了解当前小程序页面的wxml和wxss结构构成,可以用来调试组件的css样式等。不过这个工具目前体验还特别糟糕。
小程序开发者工具是提供一些代码自动补全与代码提示的,具体情况大家可以看一下官方文档关于自动补全的内容。在平时开发的过程中也可以多留意与摸索。
我们只需要在小程序每个页面的js文件下的Page({ }) 里面,添加以下代码,我们的小程序就有转发功能了,这个可以通过点击开发者工具的预览用手机来体验哦
技术文档:小程序的转发
onShareAppMessage: function (res) { if (res.from === 'button') { // 来自页面内转发按钮 console.log(res.target) } return { title: '云开发技术训练营', path: "pages/home/home, imageUrl:"https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/weapp.jpg", success: function (res) { // 转发成功 }, fail: function (res) { // 转发失败 } } },
要做出专业的小程序,就需要在很多细微的地方做足功夫,在互联网的世界里有专门的UX用户体验设计师,所做的工作就是尽可能的以用户为中心,增强用户使用产品的体验,这背后有一整套的知识体系,大家可以拓展了解一下。
有时候我们不希望我们的小程序底部有tabBar,那我们该怎么处理呢?我们可以删掉app.json的tabBar配置项即可。
当我们下拉很多小程序的时候,都会出现一个白色的空白,很影响美观,但是如果我们在windows的配置项里把backgroundColor和navigationBarBackgroundColor的颜色配置成一样,下拉就不会有空白啦,比如:
"window":{ "backgroundTextStyle":"light", "navigationBarBackgroundColor": "#1772cb", "navigationBarTitleText": "HackWork技术工坊", "navigationBarTextStyle":"white", "backgroundColor": "#1772cb" },
小程序的页面背景的颜色默认为为白色,我们希望整个小程序的页面背景变成其他颜色应该怎么处理呢?
我们可以可以通过直接设置page的样式来设置,在该页面的wxss文件里添加如下样式,如
page{ background-color: #1772cb;}
我们发现小程序除了页面默认的背景色是白色,很多组件的默认背景色也是白色,组件里的文字的默认颜色是黑色,文字也有默认大小,很多组件虽然我们没有去定义它们的css样式,但是它们却自带一些css样式。
有的时候我们的页面做得比较短,为了增强用户体验,不希望用户可以下拉页面,因为下拉页面会有种页面松动的感觉,可以在该页面的json文件里配置,比如
{ "window": { "disableScroll": true }}
注意,不是app.json文件,而是页面的json文件,为什么不是app.json文件而是页面的json文件呢?大家可以思考一下,小程序这么处理的逻辑。
官方默认的导航栏只能对背景颜色进行更改,对于想要在导航栏添加一些比较酷炫的效果则需要通过自定义导航栏实现。通过设置 app.json中页面配置的 navigationStyle(导航栏样式)配置项的值为 custom,即可实现自定义导航:
"window":{ "navigationStyle":"custom"}
比如我们给小程序的页面配一个好看的壁纸,比如在home.wxss里添加以下样式:
page{ background-image: url(https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/background.jpg)}
然后在手机上预览该页面,发现小程序固有的带有页面标题的顶部导航栏就被背景图片取代了。我们也还可以在顶部导航栏原有的位置上设计一些更加酷炫的元素,这些都是可以通过前面组件的知识来实现的。
有这样一个应用场景,我们希望所有的页面都有一个相同的底部版权信息,如果是每个页面都重复写这个版权信息就会很繁琐,如果可以定义好代码片段,然后在不同的地方调用就方便了很多,这就是模板的作用。
静态的页面片段
比如使用开发者工具在小程序的pages页面新建一个common文件夹,在common里新建一个foot.wxml,并输入以下代码
<template name="foot"> <view class="page-foot"> <view class="index-desc" style="text-align:center;font-size:12px;bottom:20rpx;position:absolute;bottom:20rpx;width:100%">云开发技术训练营</view> </view> </template>
在要引入的页面比如home.wxml的顶部,使用import引入这个模板,
<import src="/pages/common/foot.wxml" />
然后在要显示的地方调用比如home.wmxl页面代码的最底部来调用这个模板
<template is="foot" />
比如在页面的每一页都有一个相似的页面样式与结果,但是不同的页面有着不同的标题以及页面描述,用数据绑定就能很好的解决这个问题,不同的页面的js data里有不同的数据,而模板的wxml都是固定的框架。
比如使用开发者工具在小程序的pages页面新建一个common文件夹,在common里新建一个head.wxml,并输入以下代码:
<template name="head"> <view class="page-head"> <view class="page-head-title">{{title}}</view> <view class="page-head-line"></view> <view wx:if="{{desc}}" class="page-head-desc">{{desc}}</view> </view> </template>
我们再给每个页面的js里的data里添加不同的title和desc信息,再来在页面先引入head.wxml,然后在指定的位置比如wxml代码的前面调用该模板。
<import src="/pages/common/head.wxml" /><template is="foot" />
我们注意创建模板时,使用的是 <template name=”模板名”></template>
,而调用模板时,使用的是<template is=”模板名” />
,两者之间对应。
开发者在小程序内添加客服消息按钮组件,用户就可在小程序内唤起客服会话页面,给小程序发消息。而开发者(可绑定其他运营人员)也可以直接使用微信公众平台网页版客服工具或者移动端小程序客服小助手进行客服消息回复,非常的方便。
只需要在wxml文件里添加如下代码,即可唤起客服会话页面:
<button open-type="contact"></button>
button的样式大家可以根据之前学习的css知识修改一下。
承载网页的容器。会自动铺满整个小程序页面,个人类型的小程序暂不支持使用。web-view组件可打开关联的公众号的文章,这个对很多自媒体用户就比较友好了,公众号文章可以使用第三方的工具比如秀米、135编辑器等制作得非常精美,这些文章可以在小程序里打开啦。
<web-view src="https://mp.weixin.qq.com/cgi-bin/wx" rel="external nofollow" ></web-view>
web-view的也可以绑定备案好的域名,支持JSSDK的接口,因此很有小程序为了省开发成本,点击链接打开的都是网页,并没有做小程序的原生开发,这个就不再讨论范围之内了。
JavaScript是目前世界上最流行的编程语言之一,它也是小程序开发最重要的基础语言。要做出一个功能复杂的小程序,除了需要掌握JavaScript的基本语法,还要了解如何使用JavaScript来操作小程序(通过API接口)。
打开微信开发者工具,在调试器里可以看到Console、Sources、Network、Appdata、Wxml等标签,这些都是调试器的功能模块。 而控制台Console除了可以显示小程序的错误信息外,还可以用于输入和调试代码。
JavaScript的算数运算符和我们常见的数学运算符没有太大区别,+加、-减、乘*、除/、指数**,我们可以在控制台Console的>后面逐行输入并按Enter执行以下代码:
136+384; //加法(110/0.5+537-100)*2; //加减乘除2**5; //指数运算符
//为JavaScript的注释,可以不用输入,输入也不会有影响;JavaScript的语句之间用英文字符的分号;分隔。
在控制台输入四则运算可以直接得到结果,是因为调用了console.log()函数,我们可以把上面的四则运算在控制台里使用 console.log(321*3)打印出来,除了四则运算,console.log()还可以打印字符串String,比如:
console.log("童鞋,欢迎开始JavaScript的学习~
JavaScript是一门非常流行的编程语言,只要是有浏览器的地方就少不了JavaScript;
网页、小程序、甚至App、桌面应用等都少不了JavaScript;
JavaScript玩得溜的人我们可以称其为前端开发工程师;
前端开发工程师是需求量极大的岗位
");console.log('%c欢迎关注小程序的云开发:https://www.zhihu.com/org/teng-xun-yun-kai-fa-tcb (用云开发可以更快速学好前端开发)','color: red' );
在实际应用中,总有一些具有特殊含义的字符无法直接输入,比如换行
、Tab键
、回车
、反斜杠 ,这些我们称之为转义字符。JavaScript中单引号和双引号都表示字符串。如果字符串中存在双引号,建议最外层用单引号;如果字符串中存在单引号,建议最外层用双引号。如何在控制台给打印的字体添加颜色等,大家可以自行去研究。
我们可以在控制台使用console.log()打印数组,打印出来之后,结果的前面会有数字显示数组的长度length,以及可以展开。
console.log(["肖申克的救赎","霸王别姬","这个杀手不太冷","阿甘正传","美丽人生"])
在展开的结果里,我们可以看到数组的索引index,以及索引index对应的值(比如: 1: "霸王别姬")、该数组的长度length,以及数组的方法(在proto里可以看到,比如concat、push、shift、slice、toString等)。
我们也可以通过索引值打印数组里的单一数据,也就是通过指定数组名以及索引值,来访问某个特定的元素:
console.log(["肖申克的救赎","霸王别姬","这个杀手不太冷","阿甘正传","美丽人生"])
在控制台里使用console.log()函数打印一个对象Object,对象的结果仍然可以通过左侧的三角展开可以看到对象的属性以及属性对应的值。
console.log({name: "霸王别姬",img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp",desc: "风华绝代。"})
我们可以通过点表示法来访问该属性获取属性对应的值:
console.log({name: "霸王别姬",img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp",desc: "风华绝代。"}.desc)
当我们打印数组的某一项和通过点表示法获取对象某个属性对应的值的时候,有没有觉得打印的内容太长?这个时候我们可以把数组、对象赋值给一个变量,类似于数学里的y=ax+b,就可以大大简化代码了。
JavaScript可以使用let语句声明变量,使用等号=可以给变量赋值,等号=左侧为变量名,右侧为给该变量赋的值,变量的值可以是任何数据类型。JavaScript常见的数据类型有:数值(Number)、字符串(String)、布尔值(Boolean)、对象(Object)、函数(Function)等。
比如我们可以在控制台里,将上面的数组和对象赋值给一个变量,然后打印该变量,先来打印数组:
let movielist=["肖申克的救赎","霸王别姬","这个杀手不太冷","阿甘正传","美丽人生"]console.log(movielist)console.log(movielist[2])
再来看打印对象的情况:
let movie={name: "霸王别姬",img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp",desc: "风华绝代。"}console.log(movie)console.log(movie.name)
通过将复杂的数据信息(数组、对象)赋值给一个变量,代码得到了大大的简化,可以深刻了解到变量是用于存储信息的”容器”。
比如我们在控制台Console里使用let声明一个变量username,然后将username打印出来:
let username="小明"console.log(username)
但是如果再次使用let声明username,并给username赋值时就会出现变量名冲突的报错,比如再在控制台里输入以下代码并按Enter执行,看会报什么错?
let username="小丸子"
也就是说声明了一个变量名之后,就不能再次声明这个变量名啦。但是我们却可以给该变量重新赋值,比如:
username="小军"console.log(username)
我们发现给该变量重新赋值之后,变量的值就被覆盖了。所以let 变量名=值,相当于进行了两步操作,第一步是声明变量名,第二步是给变量赋值,具体可以通过控制台执行下面的代码来理解。
let school //声明变量school="清华" //将字符串String"清华"赋值给变量console.log(school) //打印变量school=["清华","北大","上交","复旦","浙大","南大","中科大"] //给变量赋值新的数据类型新的数据console.log(school) //打印变量
通过使用控制台实战打印具体的信息,我们就会对声明变量、赋值、覆盖(修改变量的值)有了更深的了解。
这个undefined是console.log()这个函数的返回值,每个函数都有返回值,如果函数没有提供返回值,会返回 undefined。
小任务:那我们是否可以给一个没有声明过的变量名直接赋值呢?你知道应该如何在控制台打印测试结果吗?你的实验结果是?
在前面我们已经说过,数组是一个有序的列表。下面这个数组是豆瓣电影的top5:
["肖申克的救赎","霸王别姬","这个杀手不太冷","阿甘正传","美丽人生"]
但是有时候我们需要操作一下该数组,比如想增加5项数据,变成top10,比如数据太多,只想要top3等等,这个时候就需要对数组进行操作了。要对数组进行操作,就有操作的方法。前面我们已经将给数组赋值给了movielist,下面我们可以直接使用该变量。也可以先在控制台再赋值一下。
movielist=["肖申克的救赎","霸王别姬","这个杀手不太冷","阿甘正传","美丽人生"]
join方法将数组元素拼接为字符串,以分隔符分割,默认是逗号,分割。
console.log(movielist.join("、"))
push()方法向数组的末尾添加一个或更多元素,并返回新数组的长度。
console.log(movielist.push("千与千寻","泰坦尼克号","辛德勒的名单","盗梦空间","忠犬八公的故事"))
这里返回的是新数组的长度,那我们打印一下新数组看具体包含了哪些值,push方法在原来的数组后面(不是前面)新增了5个值(不是覆盖,重新赋值)。
console.log(movielist)
pop() 从数组末尾移除最后一项,并返回移除的项的值:
console.log(movielist.pop())
返回的是数组的最后一项,我们再来打印movielist,看看有什么变化:
console.log(movielist)
以上通过一些实际的案例让大家了解如何使用控制台打印这种实战方式来了解了一些数组具体的操作方法,数组的操作方法还有很多,大家可以去查阅技术文档。
技术文档:数组Array
如果说小程序的开发离不开小程序的官方技术文档,那MDN则是每一个前端开发工程师都必须经常去翻阅的技术文档。打上MDN数组Array,在页面的左侧菜单里,我们可以看到Array有着数十种方法,而这些方法,都是我们之前打印了数组之后在proto里看到的方法。关于数组的prototype,学有余力的人可以去阅读MDN Array.Prototype
小任务:通过实战的方式了解一下数组的concat()、reverse()、shift()、slice()、sort()、splice()、unshift()方法
我们可以用点表示法访问对象的属性,通过给该属性赋值就能够添加和修改对象的属性的值了。在之前我们声明过一个对象movie:
movie={name: "霸王别姬",img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp",desc: "风华绝代。"}
比如我们给霸王别姬增加英文名的属性,直接在控制台里输入以下代码:
movie.englishname="Farewell My Concubine"
然后再在控制台打印movie看看movie是否有了englishname的属性
console.log(movie)
比如我们想删除movie的img属性,可以通过delete 方法来删除
delete movie.img
然后再在控制台打印movie看看movie的img属性是否被删除了。
console.log(movie)
比如我们想更新movie的desc属性,可以通过重新赋值的方式来更新
movie.desc="人生如戏。"
然后再在控制台打印movie看看movie的desc属性是否有了变化。
console.log(movie)
在前面我们知道变量的值可以通过重新赋值的方式来改变,但是有些数据我们希望是固定的(写死,不会经常改变),这个时候可以使用const声明创建一个值的只读引用。const声明和let声明挺像的。
比如开发小程序的时候,我们会确定小程序的色系、颜色等,使用const声明,以后直接调用这个常量,这样就不用记那么多复杂的参数,以后想全网改样式,直接改const的内容即可。比如:
const defaultStyle = { color: '#7A7E83', selectedColor: '#3cc51f', backgroundColor: '#ffffff',}
前面我们已经知道字符串是JavaScript的数据类型之一,那我们可以怎么来操作字符串呢?下面我们就来结合MDN技术文档来学习。MDN文档是前端最为依赖的技术文档,我们要像查词典一样来学习如何使用它。
技术文档:技术文档之JavaScript标准库之String
首先我们在main.js里输入以下代码,然后执行,在Console控制台查看效果:
let lesson="云开发技术训练营";let enname="CloudBase Camp"console.log(lesson.length); //返回字符串的长度console.log(lesson[4]); //返回在指定位置的字符console.log(lesson.charAt(4)); //返回在指定位置的字符console.log(lesson.substring(3,6)); //从索引3开始到6(不包括6)console.log(lesson.substring(4)); //从索引4开始到结束console.log(enname.toLowerCase()); //把一个字符串全部变为小写:console.log(enname.toUpperCase()); //把一个字符串全部变为大写:console.log(enname.indexOf('oud')); //搜索指定字符串出现的位置:console.log(enname.concat(lesson)); //连接两个字符串console.log(lesson.slice(4)); //提取字符串的某个部分,并以新的字符串返回被提取的部分
然后打开技术文档,在技术文档左侧菜单的属性和方法里,找到操作字符串用了哪些属性和方法,通过翻阅技术文档既加深对字符串的每个操作的理解,也知道该如何查阅技术文档。
字符串怎么有这么多属性和方法?多就对了,正是因为多,所以我们不能用传统的死记硬背来学习技术。技术文档怎么有这么多新词汇我见都没有见过,完全看不懂?你不需要全部都懂,就像我们不需要懂词典里的所有单词和语法一样。即使是GRE满分高手也不能认全所有单词,而通常6级单词就已经够用了,技术也是一样的道理。
Math是一个内置对象, 它具有数学常数和函数的属性和方法,但它不是一个函数对象。大家可以先在控制台实战然后再来了解这句话啊含义。
技术文档: Math对象文档
在开发者工具的控制台console里输入以下代码,根据得到的结果来弄清楚每个函数的意思。
let x=3,y=4,z=5.001,a=-3,b=-4,c=-5;console.log(Math.abs(b)); //返回b的绝对值console.log(Math.round(z));//返回z四舍五入后的整数console.log(Math.pow(x,y)) //返回x的y次幂console.log(Math.max(x,y,z,a,b,c)); //返回x,y,z,a,b,c的最大值console.log(Math.min(x,y,z,a,b,c));//返回x,y,z,a,b,c的最小值console.log(Math.sign(a)); //返回a是正数还是负数console.log(Math.hypot(x,y)); //返回所有x,y的平方和的平方根 console.log(Math.PI); //返回一个圆的周长与直径的比例,约3.1415
我们打开技术文档,在左侧菜单找一下Math对象的属性有哪些,Math对象的方法又有哪些?大致感受一下属性和方法到底是什么意思。
注意,在别的开发语言里面,我们想获取一个数的绝对值可以直接调用abs(x)函数即可,而JavaScript却是Math.abs(x),这是因为前面说的Math不是函数(函数对象),而是一个对象。
Date 对象用于处理日期和时间。时间有年、月、日、星期、小时、分钟、秒、毫秒以及时区的概念,因此Date对象属性和方法也显得比较多。
技术文档:Date对象文档
let now = new Date(); //返回当日的日期和时间。console.log(now); console.log(now.getFullYear()); //从 Date 对象以四位数字返回年份。console.log(now.getMonth()); //从 Date 对象返回月份 (0 ~ 11)。console.log(now.getDate()); //从 Date 对象返回一个月中的某一天 (1 ~ 31)。console.log(now.getDay()); //从 Date 对象返回一周中的某一天 (0 ~ 6)。console.log(now.getHours()); //返回 Date 对象的小时 (0 ~ 23)。console.log(now.getMinutes()); //返回 Date 对象的分钟 (0 ~ 59)。console.log(now.getSeconds()); //返回 Date 对象的秒数 (0 ~ 59)。console.log(now.getMilliseconds()); //返回 Date 对象的毫秒(0 ~ 999)。console.log(now.getTime()); //返回 1970 年 1 月 1 日至今的毫秒数。
wx是小程序的全局对象,用于承载小程序能力相关 API。小程序开发框架提供丰富的微信原生 API,可以方便的调起微信提供的能力,如获取用户信息,了解网络状态等。大家可以在微信开发者工具的控制台Console里了解一下这个wx这个对象。
wx
可以看到wx的所有属性和方法,如果你对wx的哪个属性和方法不了解,你可以查阅技术文档。
技术文档:API技术文档
获取网络类型技术文档:wx.getNetworkType()
大家可以切换一下开发者工具的模拟器的网络,然后多次在控制台console输入以下代码查看有什么不同:
wx.getNetworkType({ success(res) { console.log(res) } });
获取用户信息技术文档:wx.getUserInfo()
登录开发者工具之后(大家应该已经处于登录状态),在控制台console里输入以下代码,看得到什么信息:
wx.getUserInfo({ success(res) { console.log(res); }});
然后退出开发者工具,再输入以上代码,看看是什么令人兴奋的结果?
获取设备信息技术文档:wx.getSystemInfo()
wx.getSystemInfo({ success (res) { console.log(res.model) console.log(res.pixelRatio) console.log(res.windowWidth) console.log(res.windowHeight) console.log(res.language) console.log(res.version) console.log(res.platform) }})
页面跳转技术文档:wx.navigateTo()
除了可以获取到用户、设备、网络等的信息,使用控制台来调用对象的方法也可以执行一些动作,比如页面跳转。在控制台Console里输入:
wx.navigateTo({ url: '/pages/home/imgshow/imgshow'})
还可以返回页面的上一层,在控制台里输入
页面返回技术文档:wx.navigateBack()
wx.navigateBack({ delta: 1})
显示消息提示框技术文档:wx.showToast()
wx.showToast({ title: '弹出成功', icon: 'success', duration: 1000 })
设置标题技术文档:wx.setNavigationBarTitle()
wx.setNavigationBarTitle({ title: '控制台更新的标题'})
打开文件选择技术文档:wx.chooseImage()
wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success (res) { const tempFilePaths = res.tempFilePaths }})
这一部分主要是让大家明白控制台Console的强大之处,以及通过控制台实战的方法对小程序API的运行机制有一个初步的了解。
事件是视图层到逻辑层的通信方式,当我们点击tap、触摸touch、长按longpress小程序绑定了事件的组件时,就会触发事件,执行逻辑层中对应的事件处理函数。
小程序框架的视图层由 WXML 与 WXSS 来编写的,由组件来进行展示。将逻辑层的数据反应成视图,同时将视图层的事件发送给逻辑层。逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。
使用开发者工具新建一个tapevent的页面(直接在app.json的pages配置项第一行添加一个tapevent的页面,由于是第一项,这样就可以成为小程序的首页呈现出来),然后将以下代码输入到tapevent.wxml文件里:
<button type="primary" bindtap="scrollToPosition">滚动到页面指定位置</button><view class="pagetop" style="background-color:#333;width:100%;height:400px"></view><button type="primary" bindtap="scrollToTop">滚动到页面顶部(返回顶部)</button><view id="pageblock" style="background-color:#333;width:100%;height:400px"></view>
这里的type="primary"
只是引入weui给button添加的样式。而函数名scrollToPosition和scrollToTop是可以自己定义的,然后我们再来在相应的js文件里要添加和函数名scrollToPosition和scrollToTop对应的事件处理函数。
在tapevent.js的Page({})
里(也就是和 data:{}
、 onLoad: function (options) { }
等函数平级),输入以下代码:
scrollToTop() { wx.pageScrollTo({ scrollTop: 0, duration: 300 }) }, scrollToPosition() { wx.pageScrollTo({ scrollTop: 6000, duration: 300 }) },
当用户点击该button组件的时候会在该页面对应的Page中找到相应的事件处理函数。保存编译之后,看看是不是就有了页面滚动的效果了?原理是scrollToTop()和scrollToPosition()这两个函数实际上都是调用了同一个小程序的滚动API wx.pageScrollTo(),关于该API的具体参数信息,我们可以查阅技术文档。
滚动API技术文档:wx.pageScrollTo(Object object)
在官方文档我们可以看到wx.pageScrollTo()的作用是将页面滚动到目标位置,支持选择器和滚动距离两种方式定位
那如何滚动到指定的选择器的位置呢?前面我们已经给view分别添加了id和class的选择器,只需要将之前的函数的配置信息修改为如下(注意哦,如果你是添加而不是修改,函数名会冲突的,或者你可以起其他的函数名):
scrollToTop() { wx.pageScrollTo({ duration: 3000, selector:".pagetop" }) }, scrollToPosition() { wx.pageScrollTo({ duration: 300, selector:"#pageblock" }) },
小任务:如果只是在组件上绑定了事件也就是只有 bindtap="scrollToPosition",但是并没有在js文件里写相应的事件处理函数scrollToPosition,看一下控制台Console会报什么错?
不要误以为只有button组件才可以绑定事件哦,还记得我们小程序组件里看的技术文档小程序组件吗?在公共属性部分,可以看到所有组件都有以下属性bind / catch,这个属性的类型是EventHandler,bindtap就是bind*的一个类型。也就是说小程序的所有组件都可以通过以上方法触发事件处理函数,达到滚动等效果。使用button为案例只是为了便于展示而已。
命名规范:JavaScript的项目名、函数名、变量等都应该遵循简洁、语义化的原则。函数名推荐使用驼峰法来命名(camelCase),比如scrollToTop、pageScrollTo,也就是由小写字母开始,后续每个单词首字母都大写,长得跟骆驼????的驼峰似的。
消息提示框是移动端App、H5(WebApp)、小程序经常会使用到的一个交互界面。在tapevent.wxml输入代码:
<button type="primary" bindtap="toastTap">点击弹出消息对话框</button>
在js里输入以下代码:
toastTap() { wx.showToast({ title: '购买成功', icon: 'success', duration: 2000 }) },
消息提示技术文档:wx.showToast(Object object)
小任务:修改title、icon和duration,以及添加image属性,看看会有什么样不同的表现形式,以及你在哪个App的何种场景下见过类似的消息提示信息?
为了让界面显示的更加简洁,你可以使用快捷键Ctrl+/(Mac为Command+/)来将上面wxml里的代码注释掉,js文件里面的函数就不用注释啦
使用开发者工具继续在tapevent.wxml文件里添加代码,这次我们会调用一下小程序的模态框(还是强调modalTap是你可以根据命名规范任意命名的,只需要在js里添加相应的事件处理函数就可以调用API):
<button type="primary" bindtap="modalTap">显示模态对话框</button>
然后再在tapevent.js中输入以下代码:
modalTap() { wx.showModal({ title: '学习声明', content: '学习小程序的开发是一件有意思的事情,我决定每天打卡学习', showCancel: true, confirmText: '确定', success(res) { if (res.confirm) { console.log('用户点击了确定') } else if (res.cancel) { console.log('用户点击了取消') } } }) },
保存编译之后,点击模拟器上的按钮,就可以显示出一个对话框,这个对话框我们称之为Modal模态对话框。
模态对话框技术文档:wx.showModal(Object object)
阅读API的技术文档,就要了解该API有哪些属性,属性代表得是什么含义,属性是什么类型(这一点非常重要),以及它的默认值是什么,可以有哪些取值。
通过给API已有的属性赋不同的值,API所展现的内容就会有很多种变化,而具体要怎么用,则需要你根据实际的小程序开发项目来使用了。
小任务:在哪些App、小程序、H5(WebApp)你会看到模态框?这些模态框是在什么情况下出现的,它的作用是啥?你能模仿这些模态框写一下配置信息吗?
点击模态框上面的取消、确定按钮,留意一下开发者工具调试器Console的日志打印信息:当我们点击取消按钮时,会打印“用户点击了取消”;当我们点击确定按钮时,会打印“用户点击了确定”,而这打印的结果是由上面的这段代码输出的:
success(res) { if (res.confirm) { console.log('用户点击了确定') } else if (res.cancel) { console.log('用户点击了取消') }}
那这段代码到底怎么理解呢?除了 console.log('用户点击了确定'),这个之前接触过可以理解外,res是什么?res.confirm、res.cancel是什么,从哪里来的?我们可以使用console.log()打印一下。将上面这段代码增加一些打印信息。
success(res) { console.log(res) if (res.confirm) { console.log(res) console.log("点击确认之后的res.confirm是:" + res.confirm) console.log("点击确认之后的res.cancel是:" + res.cancel) } else if (res.cancel) { console.log(res) console.log('用户点击了取消') console.log("点击取消之后的res.confirm是:" + res.confirm) console.log("点击取消之后的res.cancel是:" + res.cancel) }}
再来编译之后点击模态框的取消和确定按钮,看打印出来什么结果。当点击确认时,res.confirm的值为true,就执行if分支里的语句;当res.cancel的值为true,就执行res.cancel的语句。在模态对话框技术文档:wx.showModal(Object object)也有object.success 回调函数的说明。
success、fail、complete回调函数 在技术文档里可以看到属性里有success和fail两个回调函数,success为接口调用成功的回调函数;fail为接口调用失败的回调函数。关于这方面的知识大家可以阅读技术文档小程序API,大致了解一下异步API与回调函数的参数,理解异步 API 的执行结果需要通过 Object 类型的参数中传入的对应回调函数获取。(不理解也没有关系)
手机振动API分两种,一种是长振动,一种是短振动,两个API写法大致相同,为了体验效果,我们以长振动为例,在tapevent.wxml里输入以下代码,
<button type="primary" bindtap="vibrateLong">长振动</button>
然后再在tapevent.js里添加与之对应的事件处理函数:
vibrateLong() { wx.vibrateLong({ success(res) { console.log(res) }, fail(err) { console.error(err) }, complete() { console.log('振动完成') } }) },
保存编译之后,点击预览,使用手机扫描来体验一下长振动的效果。
长振动技术文档:wx.vibrateLong()
在长振动技术文档里我们再次看到API里三个回调函数,success、fail、complete。在模拟器上点击按钮时,就可以看到打印日志。console.error向控制台的console中打印 error 日志,如果不能调用长振动,那一般是手机权限的问题了。
小任务:参考长振动的代码以及短振动的技术文档,写一个短振动的案例,体验一下两者有什么不同。
下面我们来了解一下操作操作,在tapevent.wxml输入以下代码
<button type="default" bindtap="actionSheetTap">弹出操作菜单</button>
然后再在tapevent.js里添加与之对应的事件处理函数:
actionSheetTap() { wx.showActionSheet({ itemList: ['添加照片', '删除照片', '更新照片', '查询更多'], success(e) { console.log(e.tapIndex) } }) },
弹出菜单技术文档:wx.showActionSheet(Object object)
保存之后在模拟器体验,点击按钮就会弹出显示添加照片、删除照片、更新照片、查询更多等选项的操作菜单,当然我们点击操作菜单的选项之后是没有反应的,点击之后的反应还需要我们以后来写事件处理函数才行。
当我们点击操作菜单的不同选项时,会返回不同的数字,这取决于success回调函数里的e.tapIndex的值。在官方文档里我们可以了解到,当用户点击的按钮序号,从上到下的顺序,从0开始,相当于对应着数组itemList的序号,这样就为我们以后根据不同的菜单选项来执行不同的操作提供了可能。
小任务:在success(e){}回调函数里,添加console.log(e)打印e以及console.log(e.errMsg)打印e的errMsg对象看看是什么结果。
页面路由是一个非常重要的概念,打开新页面、页面返回、Tab页面切换、页面重定向等都是也能路由的不同方式。
关于页面路由,大家可以阅读一下页面路由技术文档,页面路由我们可以简单的理解为对页面链接的管理,根据不同的url链接来显示不同的内容和页面信息。在后面的章节我们还会具体讲一下页面路由的知识的,不必苛求一次性都搞明白。
在前面我们已经学习过Navigator组件,在navigator组件的技术文档里,我们可以看到open-type属性以及合法值。在小程序API左侧也可以看到5个不同的API。它们之间的对应关系如下:
页面路由API | Navigator open-type值 | 含义 |
---|---|---|
redirectTo | redirect | 关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面。 |
navigateTo | navigate | 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。 |
navigateBack | navigateBack | 关闭当前页面,返回上一页面或多级页面。 |
switchTab | switchTab | 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面 |
reLaunch | reLaunch | 关闭所有页面,打开到应用内的某个页面 |
也就是说Navigator组件可以做到的事情,使用JavaScript调用小程序也能路由API也可以做到。Navigator组件的内容是写死的,而JavaScript则可以提供动态的数据。
我们可以在之前创建的home.wxml里输入以下代码:
<button bindtap="navigateTo">保留页面并跳转</button><button bindtap="switchTab">跳转到组件Tab页</button><button bindtap="redirectTo">关闭当前页面跳转</button>
然后在home.js文件里添加以下代码:
navigateTo() { wx.navigateTo({ url: '/pages/home/imgshow/imgshow' })},switchTab() { wx.switchTab({ url: '/pages/list/list', })},redirectTo() { wx.redirectTo({ url: '/pages/home/imgshow/imgshow' })},
保存之后在开发者工具的模拟器点击按钮,就实现了页面和Tab页的切换效果。在前面我们提到bintap是小程序所有组件的公有属性,只有bintap绑定了页面路由切换的事件处理函数,组件是不是Navigator也就不重要了,也就是链接跳转不再是Navigator组件的专利。
注意这里的url的路径,我们使用的是相对于小程序根目录的绝对路径。app.json的pages配置项前面没有/是因为app.json本来就在根目录,所以可以使用相对路径以及这里的取值,以及API很多参数的字符串String类型的赋值,单引号和双引号都是没有影响的。
在home页面里的imgshow文件夹下的imgshow.wxml(在小程序开发的第一部分建过这个页面,如果没有,你再创建也可以)输入以下代码:
<button bindtap="navigateBack">返回上一页</button>
在imgshow.js里添加以下代码
navigateBack() { wx.navigateBack({ delta: 1 })},
点击保留页面跳转按钮以及返回上一页按钮,这样我们就可以在小程序里通过点击组件实现了页面的切换与页面的返回。而如果是使用wx.redirectTo跳转到新的页面就没法使用返回上一页了。
wx.navigateTo 是保留当前页面、跳转到应用内的某个页面,使用 wx.navigateBack可以返回到原页面。对于页面不是特别多的小程序,且页面间存在经常切换时,推荐使用 wx.navigateTo进行跳转, 然后返回,提高加载速度。
在数据绑定章节,我们已经掌握如何把data里面的数据渲染到页面,这一部分我们会介绍如何通过点击组件绑定的事件处理函数来修改data里面的数据,如何把事件处理函数获取到的数据打印到页面。
还记得我们之前在控制台打印的Date对象、Math对象、字符串String对象以及常量么?在第一节里我们把这些对象赋值给了一个变量,然后通过控制台可以把这些值给console.log()打印出来,那这些值可不可以渲染到小程序的页面上呢?答案是肯定的。
使用开发者工具新建一个页面比如data,然后在data.js的Page({})函数的前面,也就是不写在Page函数里面,写在data.js的第1行输入以下代码:
let lesson = "云开发技术训练营";let enname = "CloudBase Camp";let x = 3, y = 4, z = 5.001, a = -3, b = -4, c = -5;let now = new Date();
注意上面这些是JavaScript函数的语句,所以用的是分号;分隔,这个不要和之前的逗号分隔给弄混了哦。如果语句是换行的,后面的分号;也可以不必写。
然后在data里面添加如下数据(注意没有双引号,单双引号里的是字符串)
data: { charat: lesson.charAt(4), concat: enname.concat(lesson), uppercase:enname.toUpperCase(), abs:Math.abs(b), pow: Math.pow(x, y), sign:Math.sign(a), now:now, fullyear:now.getFullYear(), date:now.getDate(), day: now.getDay(), hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), time: now.getTime()},
在data.wxml里输入以下代码:
<view>"云开发技术训练营"第5个字符 {{charat}}</view><view>两个字符串连接后的结果:{{concat}}</view><view>CloudBase Camp字母大写:{{uppercase}}</view><view>b的绝对值:{{abs}}</view><view>x的y次幂:{{pow}}</view><view>返回a是正还是负:{{sign}}</view><view>now对象:{{now}}</view><view>{{fullyear}}年</view><view>{{date}}日</view><view>星期{{day}}</view><view>{{hours}}时</view><view>{{minutes}}分</view><view>{{seconds}}秒</view><view>1970年1月1日至今的毫秒数:{{time}}</view>
因为data是一个对象Object,我们可以通过冒号:的方式将变量值赋值给data里的各个属性,而在数据绑定章节,这些数据是可以直接渲染到小程序的页面的。
我们发现{{now}}渲染的结果是一个对象[object Object],而并没有显示出字符串文本,这个时候就需要用到对象的toString()方法,得到对象的字符串。将data里now的赋值改为如下:
now:now.toString(),
技术文档:toString()方法
逻辑层js文件里的data数据,无论是基础的字符串、数组、对象等,还是通过变量给赋的值,都可以渲染到页面。不仅如此,只要对逻辑层data里的数据进行修改,视图层也会做相应的更新,我们称之为响应的数据绑定,而这是通过Page的setData()方法来实现的。
使用开发者工具在data.wxml里输入:
<view style="background-color:{{bgcolor}};width:400rpx;height:300rpx;"></view><button bindtap="redTap">让背景变红</button><button bindtap="yellowTap">让背景变黄</button>
然后在data.js里添加一个数据
bgcolor:"#000000",
然后在js里添加两个button绑定的事件处理函数redTap和yellowTap:
redTap:function(){ this.setData({ bgcolor: "#cd584a" })},yellowTap:function(){ this.setData({ bgcolor: "#f8ce5f" })},
点击button,原来view组件的背景颜色由黑色变成了其他颜色,这是因为点击组件触发事件处理函数,调用了Page的setData()方法修改了data里与之相应的属性的值(重复赋值就是修改),bgcolor由原来的”#000000″,变成了其他数据。
小任务:通过以往的学习我们了解到无论是组件的样式,图片、链接的路径,数组、对象里的数据,他们都是可以进行数据分离写到data里面的,这也就意味着,我们通过点击事件改变data里面的数据可以达到很多意想不到的效果,请发挥你的想象力做一些有意思的案例出来。
在前面我们已经了解到,有些组件的私有属性的数据类型为Boolean布尔值,比如视频、Swiper轮播组件是否自动播放、是否轮播,视频组件是否显示播放按钮等等,这些我们都可以使用setData将true改为false,false改为true来达到控制的目的。
在交互方面,响应的布尔操作可以用于单一属性true与false的切换,比如显示与隐藏、展开与折叠、聚焦与失焦、选中与不选中。
我们来看一个案例,使用开发者工具在data.wxml里输入以下代码:
<video id="daxueVideo" src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" rel="external nofollow" autoplay loop muted="{{muted}}" initial-time="100" controls event-model="bubble"></video><button bindtap="changeMuted">静音和取消静音</button>
然后给在data.js的data里新增
muted: true,
然后添加changeMuted事件处理函数
changeMuted: function (e) { this.setData({ muted: !this.data.muted })},
在开发者工具的模拟器里点击按钮,我们发现静音和取消静音都是这个按钮。这里的感叹号 !是逻辑非的意思,可以理解为not。
this.setData和 this.data都用到了一个关键字 this。 this和中文里的“这个的”有类似的指代作用,在方法中, this 指代该方法所属的对象,比如这里的是Page对象, this.data就是指Page函数对象里的data对象。
结合点击事件以及数组操作的知识,我们再来看下面这个案例,了解如何通过点击按钮新增数组里的数据和删除数组里的数据。
使用开发者工具在data.wxml里输入以下代码,注意这里视图层只有一个{{text}},也就是说我们之后会把所有的数据都赋值给data里的text。
<view>{{text}}</view><button bindtap="addLine">新增一行</button><button bindtap="removeLine">删掉最后一行</button>
然后在data.js的Page()之前声明变量,这里声明extraLine为一个空数组,我们之后会往这个数组里添加和删除数据。
let initData = '只有一行原始数据'let extraLine = [];
然后再在Page的data里添加一条数据,
text: initData,
我们先来看没有事件处理函数时,数据渲染的逻辑,首先我们把initData变量值赋值给text,这时渲染的结果只有initData里的数据,所以页面显示的是“只有一行原始数据”,而extraLine和text没有什么关系。
我们再来在Page里添加addLine和removeLine的事件处理函数:
addLine: function (e) { extraLine.push('新增的内容') this.setData({ text: initData + '
' + extraLine.join('
') })},removeLine: function (e) { if (extraLine.length > 0) { extraLine.pop() this.setData({ text: initData + '
' + extraLine.join('
') }) }},
首先回顾一下之前的数组操作知识,push为往数组的末尾新增数据,而pop则删除数组末尾一行的数据,join为数组数据之前的连接符。
点击按钮新增一行,触发绑定的事件处理函数addLine,首先会执行extraLine数组新增一条数据“新增的内容”,但是这时extraLine和text还没有关系,这时在setData()函数里将initData和extraLine进行拼接(注意extraLine本来是一个数组,但是调用join方法后返回的是数组的值拼接好的字符串)。点击按钮删除最后一行,会先删除extraLine数组里最后一行的数据。
小任务:新增内容过于单一,我们可以给它后面添加一个随机数,将
extraLine.push('新增的内容')
改成extraLine.push('新增的内容'+Math.random())
,再来看看新增数据的效果,关于Math.random()大家可以自行去MDN查阅。大家也可以把拼接的连接符由换成其他字符。
函数的作用,可以写一次代码,然后反复地重用这个代码。JavaScript的函数本身也是对象,因此可以把函数赋值给变量,或者作为参数传递给其他函数。
我们可以使用function关键词来定义一个函数,括号()里为函数的参数,参数可以有很多个,使用逗号,隔开;函数要执行的代码(语句)使用大括号{}包住:
function 函数名(参数 1, 参数 2, 参数 3) { 代码块内要执行的语句}
比如,我们使用开发者工具在data.js的Page()函数前,添加如下代码:
function greet() { console.log("你好,欢迎来到云开发训练营");};greet(); //调用greet()函数
保存之后,我们可以在控制台看到函数打印的字符串。定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。greet()函数没有参数,调用函数时,直接写函数名+括号即可。
下面定义了一个简单的平方函数square(),square为函数名,number为函数的参数(名称可以自定义),使用return语句确定函数的返回值。我们继续在data.js的Page()函数前,输入以下代码:
function square(number) { return number * number; }; square(5);
square(5),就是把5赋值给变量number,然后执行numbernumber,也就是55,然后返回return这个值。
这里的number被称之为形参,而5被称之为实参。大家可以结合案例就能大致了解形参和实参的意思。
JavaScript允许传入任意个参数而不影响调用,因此传入的参数可以比定义的参数多,但是不能少。也就是说实参的数量可以多于形参但是不能少于形参。
在小程序里我们会经常将一个匿名函数赋值给对象的一个属性,而这个属性我们可以称之为对象的方法。
函数声明function在语法上是一个语句,但函数也可以由函数表达式创建,这样的函数没有函数名称(匿名)。
使用开发者工具在data.js的Page()函数前,输入以下代码:
let square = function(number) { return number * number};console.log(square(4))//使用console.log()输出变量square
执行后,可以在控制台看到输出的结果为16。上面这个function函数没有函数名,相当于是把函数的返回值赋值给了变量square。
为什么叫箭头函数(Arrow Function),因为它定义一个函数用的就是一个箭头=>,我们来看两个例子,在data.js的Page()函数前输入以下代码:
const multiply = (x, y) => { return x * y;}const sum= (x, y) => x + y;//连{}和return语句都可以省掉console.log(multiply(20, 4));console.log(sum(20, 4));
在控制台我们可以看到箭头函数打印的结果。箭头函数相当于匿名函数,它没有函数名,而且也简化了函数定义。箭头函数可以只包含一个表达式,甚至连{ … }和return都可以省略掉。大家可以先只需要了解这个写法就可以了,以后碰到不至于比较迷惑,见多了也试着尝试多写一下。
可以使用点表示法来调用对象的方法,这个和访问对象的属性没有区别。而调用对象的方法和调用一个函数也是大同小异。
调用对象的方法我们在前面就已经接触过大量的案例了,在前面我们已经说过,wx是小程序的全局对象,而在第一节我们打印的很多API,就是调用了wx对象里的方法。
在点击事件章节里,我们创建的事件点击处理函数的写法如下:
scrollToPosition() { },
而在这一节我们创建的事件点击函数的写法为:
yellowTap:function(){},
这两种写法都是可以执行的,大家可以把这两种写法互相修改一下试试看~
在前面的列表渲染里,我们知道点击电影列表里的某一部电影,要进行页面跳转显示该电影的详情,我们需要给该电影创建一个页面,那如果要显示数千部的电影的详情,一一创建电影详情页显然不合适,毕竟所有电影的详情页都是同一一个结构,有没有办法所有电影详情都共用一个页面,但是根据点击的链接的不同,渲染相应的数据?答案是肯定的,要解决这个问题,首先我们要了解链接组件的点击信息。
当点击组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象,通过 event 对象可以获取事件触发时候的一些信息,比如时间戳、 detail 以及当前组件的一些属性值集合,尤其是事件源组件的id。
currentTarget是事件对象的一个属性,表示的是事件绑定的当前组件。使用开发者工具在data.wxml里输入以下代码
<view class="weui-navbar"> <block wx:for="{{tabs}}" wx:key="index"> <view id="{{index}}" class="weui-navbar__item {{activeIndex == index ? 'weui-bar__item_on' : ''}}" bindtap="tabClick"> <view class="weui-navbar__title">{{item}}</view> </view> </block></view><view class="weui-tab__panel"> <view hidden="{{activeIndex != 0}}">帝都</view> <view hidden="{{activeIndex != 1}}">魔都</view> <view hidden="{{activeIndex != 2}}">妖都</view> <view hidden="{{activeIndex != 3}}">渔村</view></view>
然后再往data.js的data里添加以下数据:
tabs: ["北京", "上海", "广州", "深圳"],activeIndex:0,
然后再添加事件处理函数tabClick。
tabClick: function (e) { console.log(e) this.setData({ activeIndex: e.currentTarget.id });},
编译之后在模拟器里预览。当我们点击上面的tab时,触发tabClick事件处理函数,这时候事件处理函数会收到一个事件对象e,我们可以看一下控制台打印的e对象的内容,关于e对象具体属性的解释可以看技术文档。
技术文档:事件对象
currentTarget就是事件对象的一个属性,我们可以使用点表示法获取到点击的组件的Id,并将其赋值给activeIndex,所谓active就是激活的意思,也就是我们点击哪个tab,哪个tab就激活。
当我们对字符串、Math对象、Date对象、数组对象、函数对象、事件对象所包含的信息不了解时,把他们打印出来即可。打印出来的结果基本都是字符串、列表、对象,而在前面我们已经掌握如何操作它们。通过实战,通过打印日志,既有利于我们调试代码,也加强我们对逻辑的理解。
从前面的学习我们了解到,函数可以操作(增删改查)数据(包括字符串、数组、对象、Boolean等所有数据类型),组件拥有了属性数据,也就拥有了被编程的能力,可见携带数据的重要性(id、class、style甚至点击事件都是组件携带的数据,都可以用来编程)。这一节我们就拿深入了解,组件是如何携带数据的,事件对象数据的作用以及数据如何跨页面渲染。
在日常生活中,我们经常可以看到有的链接特别长,比如百度、京东、淘宝等搜索某个关键词的链接,下面是使用百度搜索云开发时的链接:
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=云开发&rsv_pq=81ee270400007011&rsv_t=ed834wm24xdJRGRsfv7bxPKX%2FXGlLt6fqh%2BiB9x5g0EUQjyxdCDbTXHbSFE&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=20&rsv_sug1=19&rsv_sug7=100&rsv_sug2=0&inputT=5035&rsv_sug4=6227
以及之前在视频组件里用到的视频链接:
http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400
这些链接通常包括以下特殊字符,以及都有着基本相同的含义,通过这些特殊字符,链接就被塞进了很多数据信息,其中?、&、=是我们接下来关注的重点。
使用开发者工具,新建一个lifecyle的页面,以及在home页面新建一个二级页面detail(也就是在pages配置项新建一个pages/home/detail/detail,以及注意将lifecycle设置为首页)然后在lifecyle.wxml里输入以下代码,这里的url也通过?、&、=添加了很多数据:
<navigator id="detailshow" url="./../home/detail/detail?id=lesson&uid=tcb&key=tap&ENV=weapp&frompage=lifecycle" class="item-link">点击链接看控制台</navigator>
点击链接,发现页面仍然能够跳转到detail页面,给url所添加的数据并不会改变页面的路径,毕竟页面的路径通常是由/来控制的。
那链接携带的数据的作用是什么呢?大家发现没,本来点击的是lifecycle里的链接,但是却跳转到了detail,如果链接携带的数据一直都在,只要我们可以在detail里把链接的数据给获取到,那我们是不是实现了数据的跨页面呢?
onload是Page页面的生命周期函数,当页面加载时触发。一个页面只会调用一次,可以在 onLoad 函数的参数中获取打开当前页面路径中的参数。
使用开发者工具,在detail.js的onload函数里添加console.log,把onload函数的参数打印出来:
onLoad: function (options) { console.log(options)},
再次点击lifecycle.wxml页面的链接,会跳转到detail,页面加载时会触发生命周期回调函数onload,会打印函数里的参数options,我们可以看看控制台的打印信息。
{id: "lesson", uid: "tcb", key: "tap", ENV: "weapp", frompage: "lifecycle"}
相信大家会这样的数据类型非常熟悉,它就是一个对象Object,我们可以通过点表示法,获取到对象里具体的属性,比如options.id就能显示我们在lifecycle点击的组件的id。
回到之前列表渲染章节的电影列表页面(你可以把之前关于电影列表的wxml和wxsss以及数据代码复制粘贴到lifecycle),给Navigator组件添加一些信息,找到下面的代码:
<navigator url="" class="weui-media-box weui-media-box_appmsg" hover-class="weui-cell_active">
将其修改为如下,也就是添加id={{index}},将每部电影的id、name、img、desc等信息写进链接
<navigator url="./../home/detail/detail?id={{index}}&name={{movies.name}}&img={{movies.img}}&desc={{movies.desc}}" class="weui-media-box weui-media-box_appmsg" hover-class="weui-cell_active">
编译之后,在lifecycle页面点击其中一部电影,我们发现所有链接还是会跳转到detail,但是控制台输出的信息却不一样,点击哪一部电影,就会在控制台输出哪部电影的信息,数据不仅实现了跨页面,还实现了点哪个显示哪个的区分。
当然我们也可以继续把数据使用setData渲染到detail页面,为方便我们仅渲染图片信息,在detail.wxml里输入:
<image mode="widthFix" src="{{detail.img}}" sytle="height:auto"></image>
在detail.js的data里添加一个detail对象,detail对象三个属性用来接收setData的数据,所以可以为空值:
detail:{ name:"", img:"", desc:"" },
然后在onload生命周期函数里将options的值赋值给detail
onLoad: function (options) { console.log(options) this.setData({ detail: options, } )},
这样,我们在lifecycle里点击哪部电影,哪部电影的海报就在detai页里显示啦。
不过使用链接url传递参数有字节限制以及只能在跨页面中使用,但是可以用来传递比如页面链接来源,可以追踪用户来自于什么设备、什么App、通过什么方式以及来自哪个朋友的邀请链接;还可以用于一些网页链接的API必备的id、key等。跨多个页面以及传递更多参数、数据等,可以使用公共数据存储app.globalData(本节会介绍)、数据缓存(后面章节会介绍)、数据库(云开发部分会介绍)以及新增的页面间通信接口getOpenerEventChannel(这里不多介绍)
组件有公有属性和私有属性,这些属性都是数据,事件处理函数可以修改这些属性,从而让组件有丰富的表现形式。不仅如此,在组件节点中还可以附加一些自定义数据。在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理,从而让组件变成相当复杂且强大的编程对象。
使用开发者工具在lifecycle.wxml里输入以下代码,
<image id="imageclick" src="https://img13.360buyimg.com/n7/jfs/t1/842/9/3723/77573/5b997bedE4f438e5b/ccd1077b985c7150.jpg" rel="external nofollow" rel="external nofollow" mode="widthFix" style="width:200rpx" bindtap="clickImage"></image>
然后我们在lifecycle.js里添加如下代码,在上一节我们说过当点击组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象,我们仍然把这个事件对象打印出来:
clickImage:function(event){ console.log('我是button',event) wx.navigateTo({ url: "/pages/home/detail/detail?id=imageclick&uid=tcb&key=tap&ENV=weapp&frompage=lifecycle" }) },
当我们点击lifecycle页面的图片时,clickImage会收到一个事件对象,打印出来的结果里包含着target和currentTarget两个属性,currentTarget指向事件所绑定的元素,而target始终指向事件发生时的元素。由于这个案例事件绑定的元素和事件发生时的元素都是imageclick,所以它们的值相同,它们里面都包含了当前组件的id,以及dataset,那这个dataset是啥呢?
值得强调的是很多童鞋以为只有点击Navigator组件、button组件才能进行链接跳转,这是思维定势的误区,通过bindtap,组件被赋予了一定的编程能力,尽管没有url属性,使用wx.navigateTo也能具备这种能力。
我们给上面的image加一个父级组件,这里的data-sku、data-spu和data-pid的值以及图片使用的都是京东iphone的数据。这些自定义数据以 data- 开头,多个单词由连字符 – 连接。
<view id="viewclick" style="background-color: red;padding:20px;" data-sku="100000177760" data-spu="100000177756" data-pid="100000177756" data-toggle="Apple iPhone XR" data-jd-color="Red" data-productBrand="Apple" bindtap="clickView"> <image id="imageclick" src="https://img13.360buyimg.com/n7/jfs/t1/842/9/3723/77573/5b997bedE4f438e5b/ccd1077b985c7150.jpg" rel="external nofollow" rel="external nofollow" mode="widthFix" style="width:200rpx" bindtap="clickImage">点击button</image></view>
然后再在lifecycle.js里添加事件处理函数clickView,
clickView: function (event) { console.log('我是view',event) wx.navigateTo({ url:"/pages/home/detail/detail?id=viewclick&uid=tcb&key=tap&ENV=weapp&frompage=lifecycle" }) },
当我们点击红色空白处(非图片区域)时,只会触发clickView,target与currentTarget的值相同。而当我们点击图片时,就会触发两个事件处理函数。
我们点击的是图片image组件,却分别触发了绑定在image组件以及image的父级(上一级)组件view的事件处理函数,我们称这为事件冒泡。
注意这时clickView事件对象的currentTarget和target的值就不相同了。在点击图片的情况下只有在clickView事件对象的currentTarget里看到dataset获取到了view组件的自定义数据。
同时从detail页面的打印(注意两个事件的链接有id的值不同)可以看出,点击图片,跳转到的是图片绑定的事件指定的页面,页面的id为imageclick。
我们再来观察dataset的值,发现jdColor以及productbrand,这是因为dataset会把连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符。data-jd-color变成了jdColor,而data-productBrand转成了productbrand。也就是说我们点击组件,从事件对象的dataset里,我们可以通过event.currentTarget.dataset来获取组件的自定义数据。
通过事件对象我们不仅可以明确知道点击了什么组件,而且还可以获取当前组件的自定义数据。比如上面案例中我们可以轻松获取到京东该商品的pid,从而跳转到该商品的详情页(https://item.jd.com/京东商品的pid.html),我们可以在clickView事件处理函数里添加:
let jdpid=event.currentTarget.dataset.pidlet pidurl = "https://item.jd.com/" + jdpid + ".html"console.log(pidurl)
这样链接该商品的详情页就被打印出来啦~(小程序不支持navigateTo的外链跳转)。如果我们要获取当前组件的其他相关数据,使用事件对象非常方便,比如点击小图显示大图,toggle弹出其他内容等等。
小程序也支持给data-属性添加wxss样式,比如我们可以给data-pid添加样式, view[data-pid]{margin:30px;},data-属性既可以类似于选择器一样的存在,也可以对它进行编程,是不是很强大?
App()函数注册小程序,Page()函数注册小程序中的一个页面,他们都接受的是对象Object类型的参数,包含一些生命周期函数和事件处理函数。App() 必须在 app.js 中调用,必须调用且只能调用一次。开发者可以添加任意的函数或数据变量到 Object参数中,用 this 可以访问。
小程序构造器:App(Object object)
页面构造器:Page(Object object)
小任务:为什么我们不能在App()和Page()里不能直接用等号=给变量赋值?你明白了吗?注意函数语句与对象属性与方法在写法上的不同。
对小程序和页面的生命周期,我们可以通过打印日志的方式来了解生命周期函数具体的执行顺序和情况,使用开发者工具在app.js里给onLaunch、onShow、onHide添加一些打印日志。
onLaunch(opts) { console.log('onLaunch监听小程序初始化。',opts) }, onShow(opts) { console.log('onShow监听小程序启动或切前台',opts) }, onHide() { console.log('onHide监听小程序切后台') },
想必大家已经注意,有的参数写的options,有的写的却是opts;前面事件对象有的写的是event,有的则用的是e,这个参数都是可以自定义的哦
以及在lifecylce.js的js里添加
onLoad: function(options) { console.log("onLoad监听页面加载",options) }, onReady: function() { console.log("onReady监听页面初次渲染完成") }, onShow: function() { console.log("onShow监听页面显示") }, onHide: function() { console.log("onHide监听页面隐藏") }, onUnload: function() { console.log("onUnload监听页面卸载") },
通过在模拟器执行各种动作,比如编译、点击转发按钮、点击小程序转发按钮旁的关闭按钮(并没有关闭)、页面切换等来了解生命周期函数的执行顺序(比如页面生命周期),对切前台和切后台、页面的加载、渲染、显示、隐藏、卸载有一定的了解。
前面我们已经了解到,通过点击事件可以触发事件处理函数,也就是需要用户来点击某个组件才能触发;这里页面的生命周期函数也可以触发事件处理函数,它不需要用户点击组件,只需要用户打开小程序、打开某个页面,把小程序切后台等情况时就能触发里面的函数。
在 App
的 onLaunch
和 onShow
打印的对象里有一个scene为1001,这个是场景值。场景值用来描述用户进入小程序的路径方式。用户进入你的小程序的方式有很多,比如有的是扫描二维码、有的是长按图片识别二维码,有的是通过微信群进入的小程序,有的是朋友单聊进入的小程序,有的是通过公众号进入的小程序等等,这些就是场景值,而具体的场景值,可以看技术文档,场景值对产品、运营来说非常重要。
技术文档:场景值列表
onLaunch是监听小程序的初始化,初始化完成时触发,全局只会触发一次,所以在这里我们可以用来执行获取用户登录信息的函数等一些非常核心的数据,如果onLaunch的函数过多,会影响小程序的启动速度。
onShow是在小程序启动,或从后台进入前台显示时触发,也就是它会触发很多次,在这里就不大适合放获取用户登录信息的函数啦。这两者的区别要注意。
小程序用户登录和获取用户信息相对来说比较复杂,为了能够让大家可以更加直观的结合我们之前的知识来一步步探究到底是怎么一回事,建议大家重新建一个不使用云服务的小程序项目。
使用开发者工具将app.js的代码修改为如下(可以把之前的全部删掉或注释掉,把下面代码复制粘贴过去)。了解一个函数一个API,实战方面从打印日志开始,而理论方面从技术文档开始。
App({ onLaunch: function () { wx.login({ success(res){ console.log('wx.login得到的数据',res) } }) wx.getSetting({ success(res){ console.log('wx.getSetting得到的数据',res) } }) }, globalData: { userInfo: null }})
模板小程序用的是箭头函数的写法,大家可以结合之前关于箭头函数的介绍、模板小程序的代码和上面的写法对照来学习。
从控制台可以看到wx.login会得到errMsg和code,这个code是用户的登录凭证。而wx.getSetting则会得到errMsg和用户当前的权限设置authSetting,包含是否允许获取用户信息,是否允许获取用户位置,是否允许使用手机相册等权限。我们可以根据打印的结果结合技术文档来深入理解。
技术文档:获取用户登录凭证wx.login、获取用户当前权限设置wx.getSetting
如果要让小程序和自己的服务器账号打通,仅仅获取用户登录凭证是不够的,需要将这个code以及你的小程序appid和appSecret传回到你的开发服务器,然后在自己的服务器上调用auth.code2session接口,得到用户的openid和session_key。由于openid是当前用户的唯一标识,可以用来判断该用户是否已经在自己的服务器上注册过,如果注册过,则根据openid生成自定义登录态并返回给小程序,整个过程非常复杂。而由于云开发与微信登录鉴权无缝整合,这些内容都不会涉及,所以这里不多介绍。
我们要获取用户信息,首先需要判断用户是否允许,可以从authSetting对象里看scope.userInfo属性是否为true,如果为true,那我们可以调用wx.getUserInfo()接口来获取用户信息。
使用开发者工具给上面的wx.getSetting()函数添加内容,最终代码如下:
wx.getSetting({ success(res){ console.log('wx.getSetting得到的数据',res) if (res.authSetting["scope.userInfo"]){ wx.getUserInfo({ success(res){ console.log("wx.getUserInfo得到的数据",res) } }) } } })
由于scope.userInfo是一个属性名,无法使用点表示法res.authSetting.scope.userInfo来获取到它的值(会误认为是authSetting属性下的scope属性的usrInfo属性值),这里用到的是获取对象属性的另外一种表示方法,叫括号表示法,也就是用中括号[]围住属性名,属性名需用单引号或双引号围住。
在控制台console我们可以看到userInfo对象里包含着当前登录用户的昵称、头像、性别等信息。
但是这个数据是在app.js里,和我们之前接触到的数据都在页面的js文件里有不同。而且这个用户信息的数据是所有页面都通用的,放在app.js里公用是应该的,但是我们要怎么才能调用到这个数据呢?
globalData对象通常用来存放整个小程序都会使用到的数据,比如我们可以把用户信息赋值给globalData的任意自定义属性。模板小程序已经声明了一个userInfo属性,我们也可以自定义其他属性名,比如(后面我们会用到)
tcbData:{ title:"云开发训练营", year:2019, company:"腾讯Tencent" },
在上面的wx.getUserInfo的success回调函数里将获取到的userInfo对象赋值给globalData对象的userInfo属性。
wx.getUserInfo({ success(res){ console.log("wx.getUserInfo得到的数据",res) this.globalData.userInfo = res.userInfo }})
但是会提示 Cannot read property 'globalData' of undefined;报错,但是模板小程序也是这样写代码的为什么却没有报错?这是因为箭头函数的this与非箭头函数this指向有不同。
this的指向情况非常复杂,尽管哪个对象调用函数,函数里面的this就指向哪个对象,说起来非常简单,但是场景太多,大家在开发时不必强行理解,死记硬背,把this打印出来即可。我们可以将回调函数success的this打印出来,
success(res){ console.log('this是啥',this)}
结果是this undefined,并没有定义,和我们预计的是Page()函数对象并不一致,给它的this.globalData赋值当然会报错。
解决方法有两种,一种是模板小程序使用箭头函数,箭头函数继承的是外部对象的this,我们可以把代码wx.getSetting()里的success回调函数的写法都改为箭头函数的写法(这里有两个,只改一个行不行?试试看),这时我们可以再来打印this,看看是什么情况。
在控制台我们可以看到改为箭头函数之后的this的结果为一个pe对象,里面包含着Page()对象的生命周期函数和属性。
第二种方法是使用that指代,在wx.getSetting()函数的前面写一行代码:
let that=thiswx.getSetting({............}) //为了便于你找位置
然后把wx.getUserInfo的success回调函数的改为如下:
wx.getUserInfo({ success: res =>{ console.log('that是啥',that) console.log("wx.getUserInfo得到的数据",res) that.globalData.userInfo = res.userInfo }})
由于情况复杂,this的指向经常会变,但是在this的指向还是Page()对象时,我们就把this赋值给that,这样就不会因为this指向变更而出现undefined了。
那我们如何在页面的js里调用globalData呢,这个时候就需要用到getApp()函数啦。
技术文档:getApp()
使用开发者工具新建一个user页面,然后在user.js的Page()函数前面添加如下代码:
let app = getApp()console.log('user页面打印的app', app)console.log('user页面打印的globalData', app.globalData.userInfo)console.log('user页面打印的tcbData',app.tcbData.eventInfo)
这样我们就能获取app.js里的globalData和自定义的属性了。
这里还会有一个问题,就是尽管我们已经获取到了globalData,我们也能在globalData.userInfo的打印日志里看到用户的信息,但是当我们想获取里面的值时,还是会报错,这是因为 wx.getUserInfo是异步获取的信息,这里涉及到的异步,我们之后会详细介绍。
在我们使用wx.getUserInfo的方式来获取用户信息时,控制台会报错: 获取 wx.getUserInfo 接口后续将不再出现授权弹窗,请注意升级。
也就是小程序官方已经不建议开发者用wx.getUserInfo来获取用户信息了,而是建议通过button的方式来获取,对用户的体验更好,也就是用户只有点击了按钮,用户信息才会被获取。
使用开发者工具在user.wxml里输入以下代码,这是一个button组件,要获取到用户信息,有两个必备条件,一是 open-type="getUserInfo",必须是这个值;二是绑定事件处理函数的属性名为bindgetuserinfo(类似于bindtap,但是属性名必须为bindgetuserinfo,至于事件处理函数的名称可以自定义)
<button open-type="getUserInfo" bindgetuserinfo="getUserInfomation"> 点击获取用户信息 </button>
这里的getUserInfomation和之前点击事件的事件处理函数是一致的,点击组件触发getUserInfomation,仍然会收到事件对象,我们把它打印出来,在user.js里添加以下代码:
getUserInfomation: function (event) { console.log('getUserInfomation打印的事件对象',event) },
当我们点击“点击获取用户信息”的button按钮后,在控制台可以查看到getUserInfomation打印的事件对象,事件对象里有个detail属性,里面就有userInfo的数据,这个具体如何调用,详细大家结合之前学过的知识应该有所了解。
首先在user.js的data里初始化一个userInfo对象,用来接收数据:
data: { userInfo:{} },
然后在事件处理函数getUserInfomation获取到的userInfo通过this.setData赋值给它,也就是getUserInfomation的函数为
getUserInfomation: function (event) { console.log('getUserInfomation打印的事件对象',event) this.setData({ userInfo: event.detail.userInfo, }) },
这时data里的userInfo就有用户信息了,我们可以在user.wxml添加以下代码来将数据渲染出来。
<view>{{userInfo.nickName}}</view><view>{{userInfo.country}}</view><image mode="widthFix" style="width:64px;height:64px" src="{{userInfo.avatarUrl}}"></image>
当我们再次点击“点击获取用户信息”的button按钮后,数据就渲染出来了。
这种方式只能在user页面才能获取到用户信息,限制非常大,那我们应该怎么做呢?我们要把获取到的用户信息写到app.js成为页面的公共信息,以后可以跨页面只需在user页面点击一次按钮即可。
在getUserInfomation将获取到的用户信息传给globalData的userInfo属性:
getUserInfomation: function (event) { console.log('getUserInfomation打印的事件对象',event) app.globalData.userInfo = event.detail.userInfo this.setData({ userInfo: event.detail.userInfo, }) },
关于用户登录以及信息获取,这里我们只是梳理了一些比较核心的知识点,还有一些大家可以去参考模板小程序里的代码,这里有一套相对比较完整的案例。更具有实际开发意义的用户登录,之后会在云开发部分介绍。
获取用户信息还有一种方式,就是通过组件<open-data&来展示,由于比较简单,这里就不多介绍啦。
在前面的章节所用到的数据大多都是我们在js的data里写好的,在这一节里,我们会来介绍如何让用户提交数据。无论是计算器、用户注册、表单收集、发表文章、评论等等,这些都是对用户提交数据的获取。
动态设置导航栏标题是一个非常简单的API,在技术文档里面可以了解到,只要给wx.setNavigationBarTitle()的title对象赋值,就能改变小程序页面的标题。下面我们会使用多种方法来调用这个API,既是对前面知识的复习,也让大家了解API调用方法有什么不同。
技术文档:wx.setNavigationBarTitle()
结合前面的知识,我们可以在页面的生命周期函数里来调用API,使用开发者工具新建一个form页面,然后在form.js里onLoad里添加代码:
onLoad: function (options) { wx.setNavigationBarTitle({ title:"onLoad触发修改的标题" }) },
我们也还可以通过点击button组件,触发事件处理函数来调用API。在form.wxml里输入以下代码
<button type="primary" bindtap="buttonSetTitle">设置标题</button>
然后再在js里添加buttonSetTitle事件处理函数:
buttonSetTitle(e){ console.log(e) wx.setNavigationBarTitle({ title: "button触发修改的标题" }) },
然后点击设置标题,button就会触发事件处理函数重新给title赋值,页面的标题就由“onLoad触发修改的标题”变成了“button触发修改的标题”,同时点击组件就会收到一个事件对象,我们把这个事件对象e通过console.log打印出来发现并没有什么特别有用的信息。这些都是前面我们学过的知识。
那我们如何才能让标题的内容可以根据用户提交的数据进行修改呢?这就涉及到表单的知识啦。小程序一个完整的数据表单收集通常包含一个form组件,一个输入框或选择器组件(比如input组件),一个button组件。
使用开发者工具在form.wxml里输入以下代码:
<form bindsubmit="setNaivgationBarTitle"> <input type="text" placeholder="请输入页面标题并点击设置即可" name="navtitle"></input> <button type="primary" formType="submit">设置</button></form>
数据表单涉及到的组件多(至少三个),参数以及参数的类型也比较多,上面有几个非常重要的点,大家可以结合上面的代码来理解:
在form.js里添加事件处理函数setNaivgationBarTitle,同时我们把事件对象e给打印出来:
setNaivgationBarTitle(e) { console.log(e) const navtitle = e.detail.value.navtitle wx.setNavigationBarTitle({ title:navtitle }) },
编译之后,在开发者工具的模拟器里输入任意文本,点击“设置”按钮,我们发现导航栏标题都会显示为我们输入的值。在控制台里我们查看一下事件对象。此时的事件对象的type属性为submit(以前的为tap),我们在input输入框填写的值就存储在detail对象的value属性的name名里,这里就是 detail.value.navtitle。
点击button组件会执行form绑定的事件处理函数setNaivgationBarTitle,打印事件对象e,将在input输入的值赋值给navtitle,最后传入wx.setNavigationBarTitle(),赋值给title。注意有两个setNaivgationBarTitle,一个是事件处理函数,一个是API,前者可以任意命名,后者小程序官方写死不可更改。
对数据表单来说,使用console.log打印事件对象可以让我们对表单提交的数据有一个非常清晰的了解;而使用赋值以及setData可以有效的把表单收集到的数据渲染到页面。
我们也可以把上面的事件处理函数写成如下,让变量title与setNavigationBarTitle的属性title同名,这样 title:title可以简写成title。
setNaivgationBarTitle(e) { const title = e.detail.value.navtitle wx.setNavigationBarTitle({ title //等同于title:title }) },
小程序的输入框input主要用来处理文本和数字的输入,下面我们就来结合实战与技术文档,来了解一下文本输入框input的type、name、placeholder等属性。
技术文档:input技术文档
使用开发者工具在form.wxml里输入以下代码,一个form组件里面可以包含多个选择器或文本输入框组件,提交数据时,会提交form里面填写的所有数据:
<form bindsubmit="inputSubmit"> <input type="text" name="username" placeholder="请输入你的用户名"></input> <input password type="text" name="password" maxlength="6" placeholder="请输入6位数密码" confirm-type="next" /> <input type="idcard" name="idcard" placeholder="请输入你的身份证账号" /> <input type="number" name="age" placeholder="请输入你的年龄" /> <input type="digit" name="height" placeholder="请输入你身高多少米"/> <button form-type="submit">提交</button></form>
然后在form.js里添加事件处理函数inputSubmit,主要是为了打印form事件对象:
inputSubmit:function(e){ console.log('提交的数据信息:',e.detail.value) },
input输入框会因为属性的类型的不同,手机键盘外观会有比较大的差异,所以需要点击预览,用微信扫描二维码在手机上体验(也可以启用真机调试)。
在开发者工具的控制台我们可以看到打印的事件对象里的value对象,属性名即为input的name名,值即为我们输入的数据。如果没有name。
小任务:给input输入框配置confirm-type,分别输入send、search、next、go、done,然后点击预览,用微信扫描二维码体验,注意输入内容时,手机键盘显示的不同。
一个完整的数据收集表单,除了可以提交input文本框里面的数据,还可以提交开关选择器按钮switch、滑动选择器按钮slider、单选按钮radio、多选按钮checkbox等组件里面的数据。
技术文档:switch开关选择、Slider滑动选择、Radio单选、checkbox多选、form表单
使用开发者工具在form.wxml里添加以下代码,这些组件都是我们日常使用App、页面等经常会使用到的场景:
<form bindsubmit="formSubmit" bindrest="formReset"> <view>开关选择器按钮</view> <switch name="switch"/> <view>滑动选择器按钮slider</view> <slider name="process" show-value ></slider> <view>文本输入框</view> <input name="textinput" placeholder="要输入的文本" /> <view>单选按钮radio</view> <radio-group name="sex"> <label><radio value="male"/>男</label> <label><radio value="female"/>女</label> </radio-group> <view>多选按钮checkbox</view> <checkbox-group name="gamecheck"> <label><checkbox value="game1"/>王者荣耀</label> <label><checkbox value="game2"/>欢乐斗地主</label> <label><checkbox value="game3"/>连连看</label> <label><checkbox value="game4"/>刺激战场</label> <label><checkbox value="game5"/>穿越火线</label> <label><checkbox value="game6"/>天天酷跑</label> </checkbox-group> <button form-type="submit">提交</button> <button form-type="reset">重置</button></form>
然后在form.js里添加formSubmit和formReset事件处理函数
formSubmit: function (e) { console.log('表单携带的数据为:', e.detail.value) }, formReset: function () { console.log('表单重置了') }
编译之后,在开发者工具的模拟器里给选择器组件和文本输入组件做出选择以及添加一些值,然后点击提交按钮。在控制台console,我们可以看到事件对象e的value对象就记录了我们提交的数据。也就是说,表单组件提交的数据都存储在事件对象e的detail属性下的value里。
点击重置按钮,即会重置表单,并不需要formReset事件处理函数做额外的处理。
我们发现上面button属性,有时用的是form-type,有时用的是formType(注意两者的大小写),这两种写法都可以。我们也可以删掉重置的事件处理函数formReset,以及form组件的 bindreset="formReset",只需要将button的form-type设置为reset,也就是
<button form-type="reset"&重置</button&
就可以达到重置的效果,绑定事件处理函数bindreset
只要我们知道form表单存储的数据在哪里,就能够结合前面的知识把数据取出来,不同的数据类型区别对待,所以掌握如何使用JavaScript操作不同的数据类型很重要。
在技术文档里有这样一句话“当点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key”。我们也发现Slider滑动选择、Radio单选、checkbox多选等,都有自己的value值,也就是这些组件单独使用时不需要name就可以在事件对象的detail里取到value值,而组合使用时,则必须加name才能取到值,大家可以把name都取消掉,看看结果如何。
在这里我们先来介绍一下扩展运算符的概念,它的写法很简单,就是三个点 ...。我们会用案例的方式让大家先了解它的作用,以后会经常用到的。
上面的gamecheck记录了我们勾选的多选项的value值,它是一个数组Array。我们可以在formSubmit事件处理函数把选项value值给打印出来,给上面的formSubmit函数添加以下语句:
formSubmit: function (e) { const gamecheck=e.detail.value.gamecheck console.log('直接打印的gamecheck',gamecheck) console.log('拓展运算符打印的gamecheck',...gamecheck) },
然后我们再来填写表单提交数据,从控制台可以看到直接打印gamecheck,它是一个数组Array,中括号[ ]就可以看出来,展开也有index值;而使用扩展运算符打印gamecheck,是将数组里的值都遍历了出来。这就是扩展运算符…的作用,大家可以先只了解即可。
尽管我们提交了数据,但是当小程序重新编译之后,所有的数据都会被重置,也就是提交的数据并没有保存起来。小程序存储数据有三种方式,一是保存在本地手机上;二是存储到缓存里;三是存储到数据库。下面我们来介绍如何将数据存储到手机。
添加手机通讯录联系人:wx.addPhoneContact()
使用开发者工具在form.wxml添加以下代码,注意input的name名要和wx.addPhoneContact()里的属性名对应且一致,下面只举几个属性,更多属性都可以按照技术文档添加
<form bindsubmit="submitContact"> <view>姓氏</view> <input name="lastName" /> <view>名字</view> <input name="firstName" /> <view>手机号</view> <input name="mobilePhoneNumbe" /> <view>微信号</view> <input name="weChatNumber" /> <button type="primary" form-type="submit">创建联系人</button> <button type="default" form-type="reset">重置</button></form>
然后在form.js文件里面输入以下代码,(注意添加手机联系人的API在手机上使用有奇效哦)
submitContact:function(e) { const formData = e.detail.value wx.addPhoneContact({ ...formData, success() { wx.showToast({ title: '联系人创建成功' }) }, fail() { wx.showToast({ title: '联系人创建失败' }) } }) },
编译之后,点击开发者工具栏的预览,微信扫描二维码,然后给以上input填充数据并点击创建联系人,就可以把数据存储到手机里了。
多写回调函数success()、fail(),并在里面添加消息提示框wx.showToast()能够大大增强用户的体验。在编程时多写console.log,多写回调函数,可以让我们对程序的运行进行诊断,这一点非常重要。不过为了教学方便,我们后面会少写回调函数。
前面我们已经介绍过数组的拓展运算符,对象的扩展运算符 ...也有类型的作用,它可以取出对象里所有可遍历的属性,拷贝到新的对象中。为了可以看得更加清楚,我们可以进行打印对比:
submitContact:function(e) { const formData = e.detail.value console.log('打印formData对象',formData) console.log('扩展运算符打印', { ...formData }) },
尽管打印的结果好像并没有区别,但是formData是一个变量,我们把对象赋值给了它,打印它的结果就是一个对象了,而 { ...formData }本身就是一个对象,相当于把formData对象里的属性和值给拷贝到了新的对象里面。
小任务:把wx.addPhoneContact()里的…formData换成formData,看看什么结果?把…formData换成lastName,又是什么结果?为什么写lastName会报错,而写formData不会报错?
在form表单里,尽管表单里也有input组件,但是绑定事件处理函数的是form组件,input组件只提供value值,而input文本输入组件本身也是可以绑定事件处理函数的。从技术文档里我们了解到input可以绑定事件处理函数的属性有:bindinput,键盘输入时触发;bindfocus,输入框聚焦时触发;bindblur,输入框失焦时触发等等,这里主要介绍一下bindinput。
使用开发者工具在form.wxml里输入以下代码,这里使用input的bindinput绑定的事件处理函数bindKeyInput(函数名可以自己命名),
<view>你输入的是:{{inputValue}}</view><input bindinput="bindKeyInput" placeholder="输入的内容会同步到view中"/>
在Page的data里我们添加inputValue的初始值,
data: { inputValue: '你还没输入内容呢' },
编译之后,我们就可以看到data里的值渲染到了页面,这是我们前面学过的知识。
我们再在form.js里给input绑定的事件处理函数bindKeyInput添加如下代码(声明一个和data里的属性相同的变量名inputValue,并赋值,setData可以简写,本节就有了解过哈)
bindKeyInput: function (e) { const inputValue = e.detail.value console.log('响应式渲染',e.detail) this.setData({ inputValue }) },
编译之后,我们再在input里面填写内容,注意此时我们写的内容会实时渲染到页面上,无论是添加内容还是删除内容,都可以做出同步响应。而在控制台Console,我们也可以看到每输入/删除一个字符,实时的打印结果,其中cursor是focus时的光标位置。
注意:回忆一下我们之前的数据渲染,有直接初始化写在Page的data里,有使用页面生命周期和button的方式来触发事件处理函数用setData改变数据来渲染,也有form表单数据收集,这些数据渲染都没有做到响应式,也就是在不刷新页面的情况下,数据会实时根据你的修改而渲染。
本节前面的添加手机联系人是把收集到的数据存储到本地手机的通讯录里,而剪切板则是把数据存储到本地手机的剪切板里。
技术文档:设置剪切板内容wx.setClipboardData()、获取剪切板内容wx.getClipboardData()
使用开发者工具在form.wxml输入以下代码:
<input type="text" name="copytext" value="{{initvalue}}" bindinput="valueChanged"></input><input type="text" value="{{pasted}}"></input><button type="primary" bindtap="copyText">复制</button><button bindtap="pasteText">粘贴</button>
然后在Page的data里我们添加initvalue、pasted的初始值,
data: { initvalue: '填写内容复制', pasted: '这里会粘贴复制的内容', },
然后在form.js中添加input绑定的事件处理函数valueChanged、button组件绑定的两个事件处理函数copyText、pasteText:
valueChanged(e) { this.setData({ initvalue: e.detail.value }) }, copyText() { wx.setClipboardData({ data: this.data.initvalue, }) }, pasteText() { const self = this wx.getClipboardData({ success(res) { self.setData({ pasted: res.data }) } }) },
在input里面输入内容,内容会响应渲染到页面,点击复制按钮,copyText事件处理函数会调用API把数据赋值给剪切板的data(注意这里的data不是page页面的data,是wx.setClipboardData API的属性),而点击粘贴按钮,事件处理函数pasteText会调用接口,把回调函数res里面的数据赋值给Page页面data里的pasted,而且页面在没有刷新的情况下实时地把data里的pasted给渲染了出来。
小任务:上面我们用到的是input的value属性,将value改成placeholder,对比一下两者有什么不同。前面我们说过,剪切板是把数据存储到了本地手机的剪切板里,使用预览在手机里打开小程序复制内容之后,再到微信聊天界面,使用粘贴看看效果。或者在手机上复制一段内容,然后打开小程序点击粘贴,看看有什么效果。
slider滑动选择器也可以绑定事件处理函数,有:bindchange完成一次拖动后触发的事件以及bindchanging拖动过程中触发的事件。
技术文档:滑动选择器slider
我们要先回顾一下事件对象里data-携带的数据和表单组件携带的数据:首先组件data-属性的数据会存储在事件对象里的currentTarget下的dataset里的属性名里,也就是data-color的值会存储在 e.currentTarget.dataset.color里;而表单组件的数据则是存储在事件对象的detail里,也就是e.detail.value里。
使用开发者工具在form.wxml里输入以下代码,这里会既涉及到data-*携带的数据,也会涉及到表单组件携带的数据:
<view style="background-color:rgb({{R}},{{G}},{{B}});width:300rpx;height:300rpx"></view><slider data-color="R" value='{{R}}' max="255" bindchanging='colorChanging' show-value>红色</slider><slider data-color="G" value='{{G}}' max="255" bindchanging='colorChanging' show-value>绿色</slider><slider data-color="B" value='{{B}}' max="255" bindchanging='colorChanging' show-value>蓝色</slider>
然后在Page的data里我们添加R、G、B的初始值(不了解RGB颜色值的童鞋可以搜索一下,它们的取值在0~255之间),这里的R、G、B初始值既是background-color的三个颜色的初始值,也是滑动选择器的初始值,我们把它设置为绿色(小程序技术文档的VI色)
data: { R:7, G:193, B:96, },
然后在form.js里添加slider组件绑定的事件处理函数colorChanging:
colorChanging(e) { console.log(e) let color = e.currentTarget.dataset.color let value = e.detail.value; this.setData({ [color]: value }) },
编译之后,当我们滑动slider,view组件的背景颜色也会随之改变。当滑动slider时,colorChanging因为滑动的拖动会不断触发(类似于英文里的ing的状态,实时监听),也就会在控制台Console里打印多个值,e.detail.value为拖动的值,而e.currentTarget.dataset.color始终只会有三个结果R、G、B,而[color]: value就是把值赋值给R、G、B这三个值。
picker滚动选择器看起来样式非常复杂,不过小程序已经帮我们封装好了,我们只需要用几行简单的代码就可以做一个非常复杂而且类别多样的滚动选择器。
技术文档:滚动选择器picker
使用开发者工具在form.wxm里输入以下代码,只需要下面几行代码,就能从底部弹起一个日期的滚动选择器。而里面的文字可以任意填写,类似于button、navigator组件里的字,点击即可执行相应的事件。
<picker mode="date" value="{{pickerdate}}" start="2017-09-01" end="2022-09-01" bindchange="bindDateChange">选择的日期为:{{pickerdate}}</picker>
然后在Page的data里我们添加pickerdate的初始值
data: { pickerdate:"2019-8-31", },
然后在form.js中添加picker组件绑定的事件处理函数bindDateChange,我们先打印看看picker组件的事件对象:
bindDateChange: function (e) { console.log('picker组件的value', e.detail.value) },
编译之后,当我们弹起滚动选择器时,日期选择器默认会指向初始值2019年8月31日,而当我们滑动选择一个日期确定之后,可以在控制台console里看到选择的日期。这个日期是一个字符串。
小任务:那我们要如何把选择的日期比如2019-10-21,从这里取出年月日呢(也就是2019、10、21)?这个就涉及到字符串的操作了,还记得字符串的操作么?可以看MDN技术文档之JavaScript标准库之String,取出具体数字的方法有很多种,你知道应该怎么处理吗?
在这个章节里,我们讲了数据可以存储到本地手机里,在后面的章节,我们还会讲数据存储的其他方式,比如缓存、数据库等。有没有感觉到编程就是逻辑处理、调用API和玩弄数据…
在前面的章节,我们非常强调JavaScript对数据的操作,这一节我们来了解一下小程序与客户端(手机)更深的交互。前面章节将数据存储到通讯录(添加手机联系人)、存储到剪切板(用手机复制粘贴),小程序就已经与客户端手机有了交互,这一节我们将来获取手机相册里的图片和手机相机拍照的照片、手机的定位以及获取手机里的缓存、文件,并使用JavaScript操作图片、操作缓存和操作文件等。
用小程序来获取手机相册里的图片和拍照的照片听起来好像挺复杂的,不过因为有了API,我们只需要结合前面的点击事件、事件处理函数以及调用API、传入指定的参数就能很容易做到。
技术文档:wx.chooseImage()
使用开发者工具新建一个file的页面,然后在file.wxml里输入以下代码:
<button bindtap="chooseImg">选择图片</button><image mode="widthFix" src="{{imgurl}}"></image><view>上传的图片</view>
然后在file.js的data里给imgurl设置一个初始值,由于链接src是一个字符串类型,我们这里可以设置为一个字符串空值,完成imgurl的初始化:
data: { imgurl:"", },
再在file.js里添加事件处理函数chooseImg,在chooseImg里我们来调用上传函数的API wx.chooseImage(),其中count、sizeType、sourceType都是API已经写好的属性,API调用成功(图片上传成功)之后,会在success回调函数里返回图片的一些信息,返回的信息可以看技术文档。
chooseImg:function(){ let that=this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const imgurl = res.tempFilePaths that.setData({ imgurl }) } }) },
虽然在开发者工具的模拟器也可以看到效果,但是wx.chooseImage()是一个与手机客户端交互性很强的API,我们最好在手机上体验。点击开发者工具的预览,在手机微信里查看效果,点击选择图片按钮,上传一张图片或拍照看看。
tempFilePaths为临时文件的路径列表,tempFiles为临时文件列表,注意这两个值都为数组。
小任务:将sourceType的值修改为 ['album'],在手机微信上看看有什么效果?再将sizeType改为 ['compressed'],看手机是否还能够上传原图?
我们可以看到由于imgurl为空值,image组件有默认宽度300px、高度225px(会随css而改变大小),所以显示上传的图片会与选择图片的button有一段空白,处理的方法有三种:
方法一:我们可以给imgurl弄一张初始图片的链接,为了让界面更加美观、交互性更好,通常都会设置一个默认的图片,比如默认的头像,当用户上传时,setData就会取代初始图片;
方法二:判断imgurl是否有内容,比如我们可以加一层逻辑判断,当Page()里的data下的imgurl属性非空时,组件才会显示;空时就不显示。
<view wx:if="{{!!imgurl}}"> <image mode="widthFix" src="{{imgurl}}"></image></view>
方法三:这个方法和方法二类似,设置一个逻辑判断,比如在data里设置一个boolean属性比如hasImg,初始值为false,
data: { hasImg:false, },
当chooseImg回调成功之后,在that.setData里把hasImg修改为true,也就是将wx.chooseImage()的success回调函数里的that.setData()修改为:
that.setData({ imgurl, hasImg:true,})
这样是否有图片就进入到了回调函数的逻辑里了,接着我们把file.wxml的代码改为如下:
<view wx:if="{{hasImg === false}}"> <button bindtap="chooseImg">选择图片</button></view><view wx:if="{{hasImg === true}}"> <image mode="widthFix" src="{{imgurl}}"></image></view>
没有图片也就是hasImg的值为false时,会显示选择图片的button;而当有图片时,没有button只有图片,在一定的场合用户体验会更好(button要是一直在,用户就还会去点,体验不好)。
注意:这里所说的上传图片与我们日常生活中的上传图片不是一样的哦,日常生活中上传图片,图片不仅会显示在小程序(网页、App)上,还会继续上传到存储服务器里面,而我们这里只是进行了第一步,上传的图片只是存储在临时文件里面,所以重新编译,图片就不显示了。后面会有临时文件的内容以及会在云开发部分将图片上传到云存储。
如果上传的是多张照片,那么imgurl的初始值就不能是字符串了,而是一个数组Array,
data: { imgurl:[], },
而file.wxml的代码也要相应的改为列表渲染即可,这种写法在代码上通用性比较强,上传一张图片、多张图片都可以,不过具体还是要看实际产品开发需求。
<view wx:for-items="{{imgurl}}" wx:for-item="item" wx:key="*this"> <image mode="widthFix" src="{{item}}"></image></view>
然后再把count的值修改为2~9张,编译之后,在手机微信上体验一下效果。
使用小程序图片API不仅可以上传图片,还可以对上传的图片进行一定的操作,比如获取图片信息、预览图片、保存图片、压缩图片等等。
无论是存储在小程序本地,还是存储在临时文件、缓存、网络上的图片,使用wx.getImageInfo() 都可以获取到该图片的宽度、高度、路径、格式以及拍照方向。
技术文档:wx.getImageInfo()
使用开发者工具在file.js里添加以下代码,我们使用wx.getImageInfo() 来获取之前上传的图片的信息。由于获取图片信息需要等上传图片成功之后才能执行,因此我们可以在wx.chooseImage()的success回调函数里来调用wx.getImageInfo(),而获取图片信息之后才能返回图片信息,因此这又是一个回调函数:
chooseImg:function(){ let that=this wx.chooseImage({ count: 9, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const imgurl = res.tempFilePaths console.log('chooseImage回调打印的res',res) that.setData({ imgurl }) wx.getImageInfo({ src: res.tempFilePaths[0], //也可以这么写:src: that.data.imgurl[0],这里只能看到第一张照片的信息,其他照片的信息需要遍历来获取 success(res){ console.log('getImageInfo回调打印的res',res) } }) } }) },
编译之后,我们再来上传一张图片,图片上传成功之后,在控制台console里可以看到打印的信息。在上面的代码里,我们发现success回调函数嵌套success回调函数。
回调函数
经过之前的学习,相信大家对回调函数success、fail有了一定的认识,那什么是回调函数呢?简单一点说就是:回调Callback是指在另一个函数执行完成之后被调用的函数。success、fail就都是在小程序的API函数执行完成之后,才会被调用,而success和fail它们本身也是函数,也能返回数据。而复杂一点说,就是回调函数本身就是函数,但是它们却被其他函数给调用,而调用函数的函数被称为高阶函数。这些大家只需要粗略了解就可以了。异步与同步我们前面也提及过异步,那什么会有异步呢?因为JavaScript是单线程的编程语言,就是从上到下、一行一行去执行代码,类似于排队一样一个个处理,第一个不处理完,就不会处理后面的。但是遇到网络请求、I/O操作(比如上面的读取图片信息)以及定时函数(后面会涉及)以及类似于成功反馈的情况,等这些不可预知时间的任务都执行完再处理后面的任务,肯定不行,于是就有了异步处理。把要等待其他函数执行完之后,才能执行的函数(比如读取图片信息)放到回调函数里,先不处理,等图片上传成功之后再来处理,这就是异步。比如wx.showToast()消息提示框,可以放到回调函数里,当API调用成功之后再来显示提示消息。回调函数相当于是异步的一个解决方案。
预览图片就是在新页面里全屏打开图片,预览的过程中用户可以进行保存图片、发送给朋友等操作。可以预览一张照片或者多张照片。
技术文档:wx.previewImage()
使用开发者工具在file.wxml里输入以下代码,我们要预览的是从手机相册里上传的图片(保留上面的代码,接着写),如果没有上传图片,那就把预览图片的按钮给隐藏,我们来写一段完整的代码:
<view wx:if="{{hasImg === true}}"> <button bindtap="previewImg">预览照片</button></view>
然后在file.js添加事件处理函数previewImg,调用预览图片的API wx.previewImage():
previewImg:function(){ wx.previewImage({ current: '', urls: this.data.imgurl, }) },
当上传图片之后点击预览图片按钮就能预览所有图片了。
这个场景主要用于让用户可以预览、保存或分享图片,毕竟image组件是不支持图片的放大预览、保存到本地、转发给好友,现在微信还支持预览小程序码,长按就可以打开小程序,这个API主要是为了增强用户的交互体验的。
那我们应该如何实现点击其中的某一张图片,就会弹出所有图片的预览呢?这里就要用到current了。
将之前file.wxml里图片上传的代码改成如下,把事件处理函数previewImg绑定在图片上面,
<button bindtap="chooseImg">选择图片</button><view wx:for-items="{{imgurl}}" wx:for-item="item" wx:key="*this"> <image mode="widthFix" src="{{item}}" data-src="{{item}} " bindtap="previewImg" style="width:100px;float:left"></image></view>
然后将file.js的事件处理函数previewImg修改为:
previewImg:function(e){ wx.previewImage({ current: e.currentTarget.dataset.src, urls: this.data.imgurl, }) },
这样点击图片就会弹出预览窗口来预览图片了。
小程序不支持直接将网络图片保存到本地手机的系统相册,支持临时文件路径和小程序本地路径。
技术文档:wx.saveImageToPhotosAlbum()
比如我们在小程序的根目录下新建一个image文件夹并放一张图片到里面比如background.jpg,然后再在file.wxml里输入以下代码,让image组件绑定事件处理函数saveImg:
<image mode="widthFix" src="/images/background.jpg" bindtap="saveImg"></image>
然后在file.js里添加事件处理函数saveImg,
saveImg:function(e){ wx.saveImageToPhotosAlbum({ filePath: "/images/background.jpg", success(res) { wx.showToast({ title: '保存成功', }) } }) }
编译之后预览在手机里体验,点击图片就会触发事件处理函数saveImg,调用wx.saveImageToPhotosAlbum() API,filePath为小程序文件的永久链接,文件就会保存到手机相册(没有相册权限会提示)。
当然永久链接实际开发用得不会太多,使用最多的场景是把网络图片下载到临时链接(因为不能直接保存网络图片),再将临时链接的图片保存到相册,只需把上面的永久链接换成临时链接就可以了。最重要的是要搞清楚图片到底在哪里,在网络上?还是在小程序本地?还是在临时文件里?还是在缓存里?
小程序是有压缩图片的API的wx.compressImage(),尤其是在上传图片时,为了减轻存储服务器的压力,不能让用户上传分辨率过高的照片。
上传图片、获取图片信息、压缩图片、上传图片到服务器,每一步都依赖上一步,所以会不断在success回调函数里写函数,实际开发涉及的业务会更复杂,就会不断回调,这被称之为回调地狱。这就是为什么会有Promise写法的原因,这个我们会在以后提及。
由于压缩图片使用到的场景不算太多,毕竟我们在上传照片时可以不支持上传原图original,只支持压缩compressed就能保证上传图片的大小了。而且wx.compressImage()压缩图片的API也比较简单,所以这里就不写实际案例了,相信大家看文档也能玩得明白。
小程序不仅支持上传图片image,还支持上传视频、Excel、PDF、音频等等其他文件格式,但是我们只能从客户端会话里(也就是微信单聊、群聊的聊天记录里)选择其他格式的文件。
使用开发者工具在file.wxml里添加以下代码,给选择文件的button绑定事件处理函数chooseFile:
<button bindtap="chooseFile">选择文件</button>
在file.js文件里添加事件处理chooseFile,并打印上传成功后回调函数里的参数对象。
chooseFile: function () { let that = this wx.chooseMessageFile({ count: 5, type: 'file', success(res) { console.log('上传文件的回调函数返回值',res) } }) },
使用开发者工具上传一张图片或其他格式的文件,在控制台console我们可以看到打印的res对象里有tempFiles的数组对象Array(没有tempFilePaths,此处技术文档有误),tempFiles对象包含文件的名称name、文件的临时路径path、文件的大小size、选择的文件的会话发送时间戳time、文件的类型type。
我们可以把上传的文件所取得的信息给渲染到页面上,在file.wxml里添加列表渲染的代码,也就是
<button bindtap="chooseFile">选择文件</button><view wx:for-items="{{tempFiles}}" wx:for-item="item" wx:key="*this"> <view>{{item.path}}</view></view>
在Page()的data里初始化一个属性temFiles,初始值为一个空数组Array:
data: { tempFiles:[], },
然后再在chooseFile的success回调函数里将数据使用setData赋值给tempFiles:
chooseFile: function () { let that = this wx.chooseMessageFile({ count: 5, type: 'file', success(res) { let tempFiles=res.tempFiles that.setData({ tempFiles }) } }) },
编译之后预览在微信上体验,看看什么效果?注意需选择微信有文件的会话框。还是再强调一下,这个上传和我们实际里的上传还是不一样的,这里只是把文件上传到了一个临时文件里面,并没有上传到服务器。
除了可以上传图片、音视频以及各种文件格式以外,小程序还支持上传地理位置。
技术文档:wx.chooseLocation()
使用开发者工具在file.wxml里输入以下代码,前面我们上传了文件,这一次我们把手机的位置给上传并渲染出来:
<button bindtap="chooseLocation">选择地理位置</button><view>{{location.name}}</view><view>{{location.address}}</view><view>{{location.latitude}}</view><view>{{location.longitude}}</view>
然后在file.js的Page()的data里初始化location
data: { location:{}, },
在file.js里添加事件处理函数chooseLocation,
chooseLocation: function () { let that= this wx.chooseLocation({ success: function(res) { const location=res that.setData({ location }) }, fail:function(res){ console.log("获取位置失败") } }) },
编译之后预览用手机点击选择地理位置的button,就会弹出地图选择位置(这个位置既可以是你当前的位置,也可以自己选择一个位置),然后点击确定,就能在小程序上看到我们上传的位置了。要让位置信息显示在地图上,可以在file.wxml里添加一个地图组件:
<map style="width: 100%; height: 300px;" latitude="{{location.latitude}}" longitude="{{location.longitude}}" show-location></map>
小任务:上传地理位置,将该地址显示在地图上,并添加该地理位置的markers。关于markers的知识,可以去看map组件的技术文档。
在新建模板小程序里(不使用云开发服务),有一个日志logs页面,这个日志logs虽然简单,但是包含着非常复杂的JavaScript知识,是一个非常好的学习参考案例,这里我们来对它进行一一解读。
在实际开发中,日期、时间的处理经常会使用到,但是使用Date对象所获取到的时间格式与我们想要展现的形式是有非常大的差异的。这时我们可以把时间的处理抽离成为一个单独的 js 文件比如util.js(util是utility的缩写,表示程序集,通用程序等意思),作为一个模块。
把通用的模块放在util.js或者common.js,把util.js放在utils文件夹里等就跟把css放在style文件夹,把页面放在pages文件夹,把图片放在images文件夹里是一样的道理,尽管文件夹或文件的名称你可以任意修改,但是为了代码的可读性,文件结构的清晰,推荐大家采用这种一看就懂的方式。
使用开发者工具在小程序根目录新建一个utils文件夹,再在文件夹下新建util.js文件,在util.js里输入以下代码(也就是参考模板小程序的logs页面调用的util.js)
const formatTime = date => { const year = date.getFullYear() //获取年 const month = date.getMonth() + 1 //获取月份,月份数值需加1 const day = date.getDate() //获取一月中的某一天 const hour = date.getHours() //获取小时 const minute = date.getMinutes() //获取分钟 const second = date.getSeconds() //获取秒 return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') //会单独来讲解这段代码的意思} const formatNumber = n => { //格式化数字 n = n.toString() return n[1] ? n : '0' + n } module.exports = { //模块向外暴露的对象,使用require引用该模块时可以获取 formatTime: formatTime, formatNumber: formatNumber}
我们再来在file.js里调用这个模块文件util.js,也就是在file.js的Page()对象前面使用require引入util.js文件(需要引入模块文件相对于当前文件的相对路径,不支持绝对路径)
const util = require('../../utils/util.js')
然后再在onLoad页面生命周期函数里打印看看这段时间处理的代码到底做了什么效果,这里也要注意调用模块里的函数的方式。
onLoad: function (options) { console.log('未格式化的时间',new Date()) console.log('格式化后的时间',util.formatTime(new Date())) console.log('格式化后的数值',util.formatNumber(9)) },
util.formatTime()就调用了模块里的函数,通过控制台打印的日志可以看到日期时间格式的不同,比如:未格式化的时间 Mon Sep 02 2019 11:25:18 GMT+0800 (中国标准时间)格式化后的时间 2019/09/02 11:25:18
未格式化的时间 Mon Sep 02 2019 11:25:18 GMT+0800 (中国标准时间)格式化后的时间 2019/09/02 11:25:18
显然格式化后的日期时间的展现形式更符合我们的日常习惯,而9这个数值被转化成了字符串”09″。那这段格式化日期时间的代码是怎么实现的呢?这里就涉及到高阶函数的知识,一般函数调用参数,而高阶函数会调用其他函数,也就是把其他函数作为参数。
相信格式化数字的代码比较好理解,如果是15日里的15,由于n[1]是15的第二位数字5,为true会直接return返回n,也就是15;比如9月里的数字9,n[1]不存在,也就是没有第二位数,于是执行 '0' + n给它加一个0,变成09;而formatNumber是一个箭头函数。
const formatNumber = n => { //格式化数字 n = n.toString() //将数值Number类型转为字符串类型,不然不能拼接 return n[1] ? n : '0' + n //三目运算符,如果字符串n第2位存在也就是为2位数,那么直接返回n;如果不存在就给n前面加0}
而格式化日期时间则涉及到map,比如下面的这段代码就有map,
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
map也是一个数据结构,它背后的知识非常复杂,但是我们只需了解它是做什么的就可以,如果你想对数组里面的每一个值进行函数操作并且返回一个新数组,那你可以使用map。
上面这段代码就是对数组[year, month, day]和[hour, minute, second]里面的每一个数值都进行格式化数字的操作,这一点我们可以在file.js的onLoad里打印看效果就明白了
onLoad: function (options) { console.log('2019年9月2日map处理后的结果', [2019,9,2].map(util.formatNumber)) console.log('上午9点13分4秒map处理后的结果', [9, 13, 4].map(util.formatNumber)) },
从控制台打印的结果就可以看到数组里面的数字被格式化处理,有两位数的不处理,没有2位数的前面加0,而且返回的也是数组。至于数组Array的join方法,就是将数组元素拼接为字符串,以分隔符分割,上面[year, month, day]分隔符为”/”,[hour, minute, second]的分隔符为”:”。
logs页面还涉及到数据缓存Storage方面的知识。通过前面的学习,我们了解到点击事件生成的事件对象也好,使用数据表单提交的数据也好,还是上传的图片、文件也好,只要我们重新编译小程序,这些数据都会消失。前面我们也提到存储数据、文件的方式有三种,一是保存到本地手机、二就是缓存,三是上传到服务器(云开发会讲解),这里我们就来了解数据缓存方面的知识。
技术文档:wx.setStorageSync()、wx.getStorageSync()
保存文件
注意:尽管上传图片和上传文件都是把图片或文件先上传到临时文件里,但是保存图片wx.saveImageToPhotosAlbum()和保存文件wx.saveFile()是完全不同的概念,保存图片是把图片保存到手机本地相册;而保存文件则是把图片由临时文件移动到本地存储里,而本地存储每个小程序用户只有10M的空间。
保存文件技术文档:wx.saveFile()
在了解logs的数据缓存案例之前,我们先来看一个将上传的图片由临时文件保存到缓存的案例,使用开发者工具在file.wxml里输入以下代码:
<view>临时文件的图片</view><image mode="widthFix" src="{{tempFilePath}}" style="width:100px"></image><view>缓存保存的图片</view><image mode="widthFix" src="{{savedFilePath}}" style="width:100px"></image><button bindtap="chooseImage">请选择文件</button><button bindtap="saveImage">保存文件到缓存</button>
然后在file.js的data里初始化临时文件的路径tempFilePath和本地缓存的路径savedFilePath:
data: { tempFilePath: '', savedFilePath: '', },
再在file.js里添加事件处理函数chooseImage和saveImage(函数名有别于之前的chooseImg和saveImg,不要弄混了哦):
chooseImage:function() { const that = this wx.chooseImage({ count: 1, success(res) { that.setData({ tempFilePath: res.tempFilePaths[0] }) } }) }, saveImage:function() { const that = this wx.saveFile({ tempFilePath: this.data.tempFilePath, success(res) { that.setData({ savedFilePath: res.savedFilePath }) wx.setStorageSync('savedFilePath', res.savedFilePath) }, }) },
还没有完~我们还需要在file.js的onLoad生命周期函数里将缓存里存储的路径赋值给本地缓存的路径savedFilePath:
onLoad: function (options) { this.setData({ savedFilePath: wx.getStorageSync('savedFilePath') }) },
编译之后,点击请上传文件的button,会触发chooseImage事件处理函数,然后调用上传图片的API wx.chooseImage,这时会将图片上传到临时文件,并将取得的临时文件地址赋值给tempFilePath,有了tempFilePath,图片就能渲染出来了。
然后再点击保存文件到缓存的button,会触发saveImage事件处理函数,然后保存文件API wx.saveFile,将tempFilePath里的图片保存到缓存,并将取得的缓存地址赋值给savedFilePath(注意tempFilePath也就是临时路径是保存文件的必备参数),这时缓存保存的图片就渲染到页面了。然后会再来调用缓存API wx.setStorageSync(),将缓存文件的路径保存到缓存的key savedFilePath里面。有些参数名称相同但是含义不同,这个要注意。
通过wx.setStorageSync()保存到缓存里的数据,可以使用wx.getStorageSync()获取出来,我们在onLoad里把获取出来的缓存文件路径再赋值给savedFilePath。
编译页面,看看临时文件与缓存文件的不同,临时文件由于小程序的编译会被清除掉,而缓存文件有10M的空间,只要用户不刻意删除,它就会一直在。
缓存的好处非常多,比如用户的浏览文章、播放视频的进度(看了哪些文章,给个特别的样式,免得用户不知道看到哪里了)、用户的登录信息(用户登录一次,可以很长时间不用再登录)、自定义的模板样式(用户选择自己喜欢的样式,下次打开小程序还是一样)、最经常使用的小图片(保存在缓存,下次打开小程序速度更快)等等。
我们再回头看logs的缓存案例,在小程序app.js的生命周期函数onLaunch里输入以下代码,也就是在小程序初始化的时候就执行日志进行记录:
// ||为逻辑与,就是声明logs为获取缓存里的logs记录,没有时就为空数组 var logs = wx.getStorageSync('logs') || [] //unshift()是数组的操作方法,它会将一个或多个元素添加到数组的开头,这样最新的记录就放在数组的最前面, //这里是把Date.now()获取到的时间戳放置到数组的最前面 logs.unshift(Date.now()) //将logs数据存储到缓存指定的key也就是logs里面 wx.setStorageSync('logs', logs) console.log(logs) console.log(Date.now())
当我们不断编译,logs数组里面的记录会不断增加,增加的值都是时间戳。那如何把缓存里面的日志给渲染到页面呢?
在file.wxml里添加以下代码,由于logs是数组,我们使用列表渲染,这里有个数组的index值,由于index是从0开始记录,给index加1,符合我们日常使用习惯。
<view wx:for="{{logs}}" wx:for-item="log"> <view>{{index + 1}}. {{log}}</view></view>
然后在file.js的data里初始化logs
data: { logs: [] },
然后再在file.js的生命周期函数onLoad里把缓存里的日志取出来通过setData赋值给data里的logs
onLoad: function () { this.setData({ logs: (wx.getStorageSync('logs') || []).map(log => { return util.formatTime(new Date(log)) }) }) },
结合前面所了解的map、模块化知识就不难理解上面的这段代码了。缓存有同步API和异步API的区别,结合之前我们了解的同步和异步的知识,看看缓存的同步API与异步API的区别。
注意:打开开发者工具调试面板的Storage标签页,小程序的缓存记录都会在这里可以直观的看到,调试时可以留意,这一点非常重要。
数据和文件是小程序开发非常重要的元素,在前面的章节里,数据和文件等的存储都是在小程序的页面进行渲染、或是页面间传递或与本地手机交互。这一节我们会来介绍数据、文件如何与网络 HTTP 进行数据、文件的对话,如何获取并上传网络数据和文件。
小程序以及很多程序的 API 是预先就已经写好的函数,使我们不需要对底层有太多了解,只需要按照技术文档进行传递参数就能调用出非常复杂的功能。而还有一类 API 则侧重于把数据资源给开放出来,我们可以通过 HTTP 的方式来使用这些数据。
复制以下链接地址,用浏览器打开,看看会返回什么结果:
//知乎日报的最新话题https://news-at.zhihu.com/api/4/news/latest //知乎日报某一个话题的内容https://news-at.zhihu.com/api/4/news/9714883 //v2ex论坛的最新主题https://www.v2ex.com/api/topics/latest.json //CNode论坛的最新话题https://cnodejs.org/api/v1/topics
以上所返回的数据类型都是 json 格式,相信大家应该比较熟悉了。那我们如何把以上数据渲染到我们的小程序页面上呢?
数据是一种资源,比如新闻资讯、电商商品、公众号文章、股市行情、空气质量和天气、地图、词典翻译、快递信息、书籍信息、音乐视频、财务公司信息等等这些都是数据,数据也是一种商品,一种服务,通常它的使用对象是开发者,有些免费,有些也会收取一定的费用,大家可以通过综合性API服务平台聚合API来对API服务有一个基础的了解。
这里推荐几个程序员经常会拿来练手的 API 资源,你可以使用这些 API 来做网站、小程序、移动端(iOS、安卓)、桌面端,也可以用于各种框架比如 Vue、React、Flutter 等等,数据没变,只是解决方案不同。
各大公司的开发平台:比如微信开放平台 提供了微信账号体系的接入,腾讯云API中心 则提供了调用云资源的能力(包含服务器、物联网、人工智能等API)、开源网站Wordpress 也提供API调用的服务,在API资源的开放方面,国外也做得比较领先(国外免费API列表 )。而对于特定的数据资源,也可以通过爬虫等方式来自建。
要渲染从 API 里获取到的数据,首先我们需要对 API 里的字段(属性)到底是干什么的要有一定的了解。比如知乎日报的API字段如下,这个可以从 API 相关的文档里了解到以及需要我们结合 Console.log
来对比了解。
比如
date
: 日期;stories
: 当日新闻;title
: 新闻标题;images
: 图像地址;id
: url
与 share_url
中最后的数字为内容的 id
;top_stories
: 界面顶部轮播的显示内容。这些在做数据渲染前就需要有所了解。使用开发者工具新建一个 request
页面,然后在 request.js
里的 onLoad
生命周期函数里输入以下代码:
onLoad: function (options) { wx.request({ url: 'https://news-at.zhihu.com/api/4/news/latest', //知乎日报最新话题 header: { 'content-type': 'application/json' // 默认值 }, success(res) { console.log('网络请求成功之后获取到的数据',res) console.log('知乎日报最新话题',res.data) } }) },
编译之后,在控制台 Console 你会看到如下报错,你的域名不在域名白名单里面,这是因为小程序只可以跟指定的域名与进行网络通信。
request:fail url not in domain list
解决方法有两种,一是打开开发者工具工具栏右上角的详情,勾选不校验合法域名、业务域名、TLS版本以及HTTPS证书;二是你可以去小程序的管理后台(注册小程序时的页面),点击开发–开发设置,在request合法域名处添加该域名(如果你不想把这个小程序发布上线,没有必要添加)。
编译之后,在控制台 Console 就可以看到打印的 res 对象,以及 res 里的 data 对象。res.data
的数据正是我们使用浏览器打开链接所得到的 json
数据,结合我们之前学到的数据渲染方面的知识,相信大家应该对如何将数据渲染到页面就不会感到陌生了。
在打印的res对象有一些参数,比如cookies
、header
、statusCode
这些是什么意思呢?我们可以来结合技术文档深入了解。
技术文档:wx.request网络数据请求
statusCode
:开发者服务器返回的 HTTP 状态码,也就是指示 HTTP 请求是否成功,其中 200 为请求成功,404 请求失败,更多状态码的知识可以查阅 MDN HTTP 响应代码header
:开发者服务器返回的 HTTP 消息头,其中Content-Type
为服务器文档的 MIME 类型,API 的 MIME 类型通常为 "application/json; charset=UTF-8
",建议服务器返回值使用 UTF-8 编码(如果你有服务器的话)。wx.request
只能发起 HTTPS 请求,默认超时时间为60s
,最大并发限制为 10个
小任务:把
request
的链接换成 v2ex、cnode 论坛的 API 链接以及知乎日报某一个话题的内容 API,看看是什么结果?你知道返回来的 json 数据的每一条属性代表的意思吗?
既然我们已经从知乎日报的 API 取得了数据,那渲染数据的方法以及如何实现跨页面渲染,在前面的章节我们已经就有所了解了。
使用开发者工具在 request.wxml
里输入 weui
的列表样式(需要引入 weui
框架哦)
<view class="page__bd"> <view class="weui-panel weui-panel_access"> <view class="weui-panel__bd" wx:for="{{stories}}" wx:for-item="stories" wx:key="*item"> <navigator url="" class="weui-media-box weui-media-box_appmsg" hover-class="weui-cell_active"> <view class="weui-media-box__hd weui-media-box__hd_in-appmsg"> <image class="weui-media-box__thumb" mode="widthFix" src="{{stories.images[0]}}" sytle="height:auto"></image> </view> <view class="weui-media-box__bd weui-media-box__bd_in-appmsg"> <view class="weui-media-box__title">{{stories.title}}</view> </view> </navigator> </view> </view></view>
然后再在 request.js
的 data
里声明 date
、stories
、top_stories
的初始值(使用的变量和 API 的字段尽量保持一致,这样就不容易混乱)
data: { date:"", stories:[], top_stories:[], },
在 onLoad 生命周期函数里将数据通过 setData 的方式给赋值给 data:
onLoad: function (options) { let that=this wx.request({ url: 'https://news-at.zhihu.com/api/4/news/latest', header: { 'content-type': 'application/json' }, success(res) { let date=res.data.date let stories=res.data.stories let top_stories = res.data.top_stories that.setData({ date,stories,top_stories }) } }) },
编译之后,我们就能看到知乎日报的数据就渲染在页面上了。
小任务: top_stories
是界面顶部轮播的显示内容,制作一个 swiper 轮播,将 top_stories
里的内容渲染到轮播上。
打开开发者工具调试工具栏的 AppData 标签页,就能看到从网络 API 里获取到的数据。也可以在此处编辑数据,并及时地反馈到界面上。如果 AppData 里有数据,可以确认页面已经取得 `res` 里的 `data` 数据,如果数据没有渲染到页面,说明列表渲染可能有误。通过这种方式可以诊断页面渲染问题所在。
前面我们获取的只是知乎的最新文章列表,那文章里面的内容呢?通过 API 文档以及我们通过链接访问的结果来看,我们只需要取得了文章的 ID,就能从 API 里获取到文章的详情页内容:
https://news-at.zhihu.com/api/4/news/9714883 //9714883是文章的ID
使用开发者工具新建一个 story
页面,然后在 story.wxml
里输入以下代码:
<view class="page__bd"> <view class="weui-article"> <view class="weui-article__h1">{{title}}</view> <view class="weui-article__section"> <view class="weui-article__section"> <view class="weui-article__p"> <image class="weui-article__img" src="{{image}}" mode="widthFix" style="width:100%" /> </view> <view class="weui-article__p"> {{body}} </view> <view class="weui-article__p"> 知乎链接:{{share_url}} </view> </view> </view> </view> </view>
然后再在 request.js
的 data
里声明 title
、body
、image
、share_url
的初始值:
data: { title:"", body:"", image:"", share_url:"", },
在 onLoad 生命周期函数里调用 wx.request
获取文章详情页的数据,并通过 setData
的方式给赋值给 data
:
onLoad: function (options) { let stories_id=9714883 let that = this wx.request({ url: 'https://news-at.zhihu.com/api/4/news/'+stories_id, header: { 'content-type': 'application/json' }, success(res) { let title = res.data.title let body=res.data.body let image=res.data.image let share_url=res.data.share_url that.setData({ title,body,image,share_url }) } })
编译之后,发现数据虽然渲染出来了,但是存在“乱码”(是 HTML 标签),那这个要如何处理呢?这个就涉及到小程序的富文本解析了。
只需要将富文本对象放在 rich-text
的 nodes
里,就能将富文本解析出来了,比如将上面的{{body}}
替换成以下代码。
<rich-text nodes="{{body}}"></rich-text>
小程序富文本解析的方案还有:Comi ,腾讯 Omi 团队开发的小程序代码高亮和 markdown 渲染组件,Github 地址,具体效果可以在微信小程序里搜索 omiCloud;以及wxPrase,微信小程序富文本解析自定义组件,支持 HTML 及 markdown 解析,Github地址,当你遇到更加复杂的富文本解析时,可以来深入了解。
上面我们只是渲染了单篇文章的详情页,那如何点击文章列表就能渲染与之相应的文章详情页呢?这就回到了我们之前学过的跨页面数据渲染。
首先把 request
页面置于首页,然后再给 request.wxml
里的 navigator
组件的链接上携带文章的 id:
url="/pages/story/story?id={{stories.id}}"
当点击 request
页面的链接时,链接携带的数据就会传到 story
页面的生命周期函数 onLoad
的 options
对象里,将 options
里的 id
,赋值给stories_id
,也就是将文章 id 9714883
修改为 options.id
let stories_id=options.id
这样再来点击 request
页面的链接,不同的链接就会渲染不同的文章详情。
解构赋值也就是从数组 Array 和对象 Object 中提取值,按照对照的位置,对变量进行赋值。比如上面的变量声明,为了能够与 API 里的数据字段一一对应,我们会声明很多变量,知乎日报的 API 还算比较少的,多了就比较复杂了。
let title = res.data.titlelet body=res.data.bodylet image=res.data.imagelet share_url=res.data.share_url
这时可以简写成:
let { title, body, image, share_url}=res.data
知乎日报的 API 是比较开放的,并不需要我们去注册 API 服务就能获取到这些数据,但是大多数情况下,API 是商品服务,需要我们注册,那需要注册的 API 和开放的 API 有什么不同呢?
注册聚合API 并认证,认证之后可以申请开通历史上的今天、图书电商数据等免费的 API 服务,并找到你的与之对应的 AppKey。
替换下面链接你的历史上的今天对应的 key(直接输 AppKey 就行),然后在浏览器打开链接(下面这个是 1.0版)
http://api.juheapi.com/japi/toh?month=9&day=15&key=你的历史上的今天对应的key
也可以选择事件列表的 2.0版(为了讲解方便,下面以1.0版本为主)
http://v.juhe.cn/todayOnhistory/queryEvent.php?date=9/15&key=你的历史上的今天对应的key
通常我们会把拿到的key
放在app.js
的globalData
里,或者在小程序里新建一个config.js
,方便以后全局调用,而不是把key
直接写在页面里。
方法一:写在app.js
里的globalData
,或者新建一个keyData
对象,只要达到全局调用的目的都可以,以globalData
为例
globalData: { juheKey:"366444.......00ff", //聚合AppKey },
这种方式调用时首先在页面的js
文件里、Page()
函数的前面使用
const app=getApp()
之后就可以使用app.globalData.juheKey
来调用它了。
方法二:也可以在小程序的根目录或者utils
文件夹新建一个config.js
,然后结合前面模块化的知识,写入以下代码:
module.exports = { juheKey:"366444.......00ff", //聚合AppKey}
这种方式调用时我们需要先在页面的Page()
函数前面引入模块化文件
const key = require('../../utils/config.js')
然后就可以使用key.juheKey
调用它了。
将一些通用的数据、函数单独拿出来存放在globalData
里或进行模块化,是在实际开发中会经常使用到的一种方法,它可以让数据、函数更容易管理以及可以重复利用,使得代码更加精简。
使用开发者工具新建一个apidata
页面,然后在apidata.js
的Page()
函数前面输入以下代码:
const app=getApp()const now = new Date(); const month = now.getMonth()+1 //月份需要+1const day = now.getDate()
然后再在生命周期函数onLoad
里输入wx.request
数据请求:
onLoad: function (options) { wx.request({ url: 'http://api.juheapi.com/japi/toh', data: { month: month, day: day, key: app.globalData.juheKey, }, header: { 'content-type': 'application/json' }, success(res) { console.log(res.data) } }) },
wx.request
里的data
就是要传入的参数,我们把month
、day
、key
传入到请求的链接里。它等价于以下链接(注意把data
里的属性值,以免传两次参数)
url: "http://api.juheapi.com/japi/toh?" + "month=" + month + "&day=" + day + "&key=" + app.globalData.juheKey,
要将多个字符串连接起来,可以使用加号+来用作字符串的拼接,如果变量比较多,是不是很麻烦?我们还可以使用模板字符串,模板字符串使用反引号 ``来表示(在电脑键盘esc按键下面)。要在模板字符串中嵌入变量,需要将变量名写在 ${}之中,比如上面的链接也可以写成:
url: `http://api.juheapi.com/japi/toh?month=${month}&day=${day}&key=${app.globalData.juheKey}`,
在控制台我们就可以看到获取到的res.data
数据,至于如何渲染到页面,这里就不多介绍了。
注册和风天气 ,并在控制台的应用管理新建一个应用,获取到该应用的 key
,按照上面的方法将 key
添加到 globalData
里:
globalData: { heweatherKey:"732c.........0b", //和风天气key }
通过技术文档我们可以了解到免费版和风天气 API 的必备字段为weather-type
(根据不同的值可以取得不同的数据)和请求参数(其中location
为必备参数)
技术文档:和风常规天气数据API
也就是我们可以通过链接可以获取到数据,注意 now
在问号 ?
的前面,也就是它不是请求的参数, location
和 key
才是。
https://free-api.heweather.net/s6/weather/now?location=beijing&key=你的key
然后再在apidata.js Page()
的data
里初始化声明weathertype
(属性名最好不要有连接符-)和location
:
data: { weathertype:"now", location:"beijing" //location的写法有很多种,具体可以参考技术文档 },
然后再在生命周期函数里添加wx.request
请求(onLoad
里可以写多个wx.request
请求)
const weathertype=this.data.weathertype wx.request({ url: `https://free-api.heweather.net/s6/weather/${weathertype}`, data: { location: that.data.location, key: app.globalData.heweatherKey, }, header: { 'content-type': 'application/json' }, success(res) { console.log(res.data) } }) },
在控制台就能看到请求到的res.data
了。如果你想点击按钮切换不同城市以及不同的天气数据类型,结合前面所学的知识,我们只需要通过事件处理函数调用setData
修改weathertype
和location
即可。
在浏览网页的时候我们经常看到汉字或一些字符变成了一个“乱码”,原因就在于链接进行了编码处理。encodeURI()
函数可把字符串作为 URI
进行编码,而decodeURI()
函数则可以进行解码。
在开发者工具的控制台里输入以下代码
console.log(encodeURI("北京"))console.log(decodeURI("%e9%85%92%e5%ba%97"))console.log(decodeURI("https://hackwork.org/handbook/python/174/%e5%86%99%e5%87%ba%e7%ac%ac%e4%b8%80%e8%a1%8cpython%e4%bb%a3%e7%a0%81/"))
如果想在小程序中调用地图的 POI 检索(POI,即兴趣点 Point of Interest
,区域内搜索酒店、学校、ATM 等)、 关键词输入提示、地址解析、逆地址解析、行政区划、距离计算、路径规划等数据服务,这时候就需要使用到地图类相关的 API。
地图API:腾讯LBS位置服务
首先在注册后登录,点击控制台 —key 管理—创建新密钥,然后取得 key,key 的格式类似于“43UBZ-----HTBIA”。
然后点击当前 Key 的设置,启动产品里勾选微信小程序和 WebServiceAPI 里的签名校验,获取到地图的 Secret key。这两种 API 的调用方式,小程序都支持。
然后将地图的两个 key,写入到globalData
里
globalData: { mapKey:"43UBZ-*****-IITUH-*****-2M723-******",//你的key mapSecretKey:"spZwWz**********Xh20uW", //你的Secret key }
在WebServiceAPI Key配置 中签名校验里提到我们使用WebServiceAPI
的方法需要对请求路径+”?
”+请求参数+SK
进行拼接,并计算拼接后字符串md5
值,即为签名(sig)。 MD5
是计算机安全领域广泛使用到的一种加密算法,主要用于确保消息传输的完整一致。
解压之后,将 js 文件夹里的md5.min.js
复制粘贴到小程序 utils 文件夹下。然后再在Page()
前面引入这个文件
const md5 = require('../../utils/md5.min.js')
坐标的逆解析就是坐标(latitude
,longitude
)转化为详细的地址名。
技术文档:坐标的逆地址解析
再在apidata.js Page()
的 data 里初始化声明latitude
,longitude
,比如我们用腾讯大厦的经纬度值:
data: { latitude:"22.540503", longitude: "113.934528", },
然后在 onLoad 函数里调用 wx.request
发起 HTTPS 网络请求
onLoad: function (options) { let that=this const { latitude, longitude } = that.data const { mapKey, mapSecretKey}=app.globalData let SIG = md5("/ws/geocoder/v1?key=" + mapKey + "&location=" + latitude + "," + longitude + mapSecretKey) wx.request({ url: 'https://apis.map.qq.com/ws/geocoder/v1', data: { key: mapKey, location: `${latitude},${longitude}`, sig: SIG }, header: { 'content-type': 'application/json' }, success(res) { console.log(res.data) } }) },
在控制台 Console 就可以看到当前坐标(latitude
,longitude
)逆解析出来的详细信息。
小程序使用腾讯地图位置服务,还有一种更加简单的方法,具体可以阅读《微信小程序:个性地图使用指南》
如果你想免费、快速的开发出一个完整的项目,用小程序的云开发可能是最好的选择。小程序的云开发所用到的主要是前端开发的知识。
小程序的注册非常方便,打开小程序注册页面,按照要求填入个人的信息,验证邮箱和手机号,扫描二维码绑定你的微信号即可,3分钟左右的时间即可搞定。
注册小程序时不能使用注册过微信公众号、微信开放平台的邮箱哦,也就是需要你使用一个其他邮箱才行。
当我们注册成功后,就可以自动登入到小程序的后台管理页面啦,如果你不小心关掉了后台页面,也可以点击小程序后台管理登录页进行登录。
小程序和微信公众号的登录页都是同一个页面,他们会根据你的不同的注册邮箱来进行跳转。
进入到小程序的后台管理页后,点击左侧菜单的开发进入设置页,然后再点击开发设置,在开发者ID里就可以看到AppID(小程序ID),这个待会我们有用。
注意小程序的ID(AppID)不是你注册的邮箱和用户名,你需要到后台查看才行哦~
大家可以根据自己的电脑操作系统来下载相应的版本,注意要选择稳定版 Stable Build的开发者工具。
开发者工具:小程序开发者工具下载地址
和学习任何编程一样,官方技术文档都是最值得阅读的参考资料。技术文档大家先只需要花五分钟左右的时间了解大致的结构即可,先按照我们的教学步骤学完之后再来看也不迟哦。
技术文档:云开发文档
由于小程序的云开发在不断新增功能,更新非常频繁,所以要确保自己的开发者工具是最新的哦(不然会报很多奇奇怪怪的错误),比如你之前下载过要先同步到最新才行~
安装完开发者工具之后,我们使用微信扫码登录开发者工具,然后使用开发者工具新建一个小程序的项目:
点击新建确认之后就能在开发者工具的模拟器里看到云开发QuickStart小程序,在编辑器里看到这个小程序的源代码。
接下来,我们点击开发者工具的工具栏里的预览图标,就会弹出一个二维码,使用微信扫描这个二维码就能在手机里看到这个小程序啦。
如果你没有使用微信登录开发者工具,以及你的微信不是该小程序的开发者是没法预览的哦。
在手机里(或模拟器)操作这个小程序,把小程序里的每个按键都点一遍,看看会有什么反应。我们会发现很多地方都会显示“调用失败”等,这非常正常,我们接下来会通过一系列的操作让小程序不报错。
点击微信开发者工具的“云开发”图标,在弹出框里点击“开通”,同意协议后,会弹出创建环境的对话框。这时会要求你输入环境名称和环境ID,以及当前云开发的基础环境配额(基础配额免费,而且足够你使用哦)。
建议你环境名称可以使用 xly、环境ID自动生成即可,当你的云开发环境出现问题的时候,你可以提供你的环境ID,云开发团队会有专人为你解答。
按照对话框提示的要求填写完之后,点击创建,会初始化环境,环境初始化成功后会自动弹出云开发控制台,这样我们的云开发服务就开通啦。大家可以花两分钟左右的时间熟悉一下云开发控制台的界面。
点击云开发控制台窗口里的设置图标,在环境变量的标签页找到环境名称和环境ID。
当云开发服务开通后,我们可以在小程序源代码cloudfunctions文件夹名看到你的环境名称。如果在cloudfunctions文件夹名显示的不是环境名称,而是“未指定环境”,可以鼠标右键该文件夹,选择“更多设置”,然后再点击“设置”小图标,选择环境并确定。
在开发者工具中打开源代码文件夹miniprogram里的app.js,找到如下代码:
wx.cloud.init({ // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 env: 'my-env-id', traceUser: true, })
在 env: 'my-env-id'处改成你的环境ID,如 env: 'xly-snoop'
NodeJS是在服务端运行JavaScript的运行环境,云开发所使用的服务端环境就是NodeJS。npm是Node包管理器,通过npm,我们可以非常方便的安装云开发所需要的依赖包。
npm是前端开发必不可少的包(模块)管理器,它的主要功能就是来管理包package,包括安装、卸载、更新、查看、搜索、发布等,其他编程语言也有类似的包管理器,比如Python的pip,PHP的composer、Java的maven。我们可以把包管理器看成是windows的软件管理中心或手机的应用中心,只是它们用的是可视化界面,包管理器用的是命令行Command Line。
下载地址:Nodejs下载地址
大家可以根据电脑的操作系统下载相应的NodeJS安装包并安装(安装时不要修改安装目录,啥也别管直接next安装即可)。打开电脑终端(Windows电脑为cmd命令提示符,Mac电脑为终端Terminal),然后逐行输入并按Enter执行下面的代码:
node --versionnpm --version
如果显示为v10.15.0以及6.11.3(可能你的版本号会有所不同),表示你的Nodejs环境已经安装成功。
学编程要仔细,一个字母,一个单词,一个标点符号都不要出错哦。注意输上面的命令时node、npm的后面有一个空格,而且是两个短横杠–。
cloudfuntions文件夹图标里有朵小云,表示这就是云函数根目录。展开cloudfunctions,我们可以看到里面有login、openapi、callback、echo文件夹,这些就是云函数目录。而miniprogram文件夹则放置的是小程序的页面文件。
cloudfunctions里放的是云函数,miniprogram放的是小程序的页面,这并不是一成不变的,也就是说你也可以修改这些文件夹的名称,这取决于项目配置文件project.config.json里的如下配置项:
"miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/",
但是你最好是让放小程序页面的文件夹以及放云函数的文件夹处于平级关系且都在项目的根目录下,便于管理。
使用鼠标右键其中的一个云函数目录比如login,在右键菜单中选择在终端中打开,打开后在终端中输入以下代码并按Enter回车执行:
npm install
如果显示“npm不是内部或外部命令”,你需要关闭微信开发者工具启动的终端,而是重新打开一个终端窗口,并在里面输入 cd /D 你的云函数目录进入云函数目录,比如 cd /D C:download cb-projectcloudfunctionslogin进入login的云函数目录,然后再来执行npm install命令。
这时候会下载云函数的依赖模块,下载完成后,再右键login云函数目录,点击“创建并部署:所有文件”,这时会把本地的云函数上传到云端,上传成功后在login云函数目录图标会变成一朵小云。
在开发者工具的工具栏上点击“云开发”图标会打开云开发控制台,在云开发控制台点击云函数图标,就能在云函数列表里看到我们上传好的“login”云函数啦。
接下来我们按照这样的流程把其他所有云函数(如openapi)都部署都上传,也就是要执行和上面相同的步骤,总结如下:
右键云函数目录,选择在终端中打开,输入 npm install命令下载依赖文件;然后再右键云函数目录,点击“创建并部署:所有文件”在云开发控制台–云函数–云函数列表查看云函数是否部署成功。
login、openapi、echo、callback这些云函数在后面都会用到的哦,一定要确定自己部署上传成功,不然后面会报错的哦。
为什么要在云函数目录执行npm install,而不是其他地方?这是因为npm install会下载云函数目录下的配置文件package.json里的dependencies,它表示的是当前云函数需要依赖的模块。package.json在哪里,就在哪里执行npm install,没有package.json,没有dependencies,就没法下载啊。
执行npm install命令下载的依赖模块会放在node_modules文件夹里,大家可以在执行了npm install命令之后,在电脑里打开查看一下node_modules文件夹里下载了哪些模块。
既然npm install是下载模块,那它是从哪里下载的呢?就以wx-server-sdk为例,我们可以在以下链接看到wx-server-sdk的情况:
https://www.npmjs.com/package/wx-server-sdk
为什么package.json里依赖的是一个模块wx-server-sdk,但是node_modules文件夹里却下载了那么多模块?这是因为wx-server-sdk也依赖三个包tcb-admin-node、protobuf、jstslib,而这三个包又会依赖其他包,子子孙孙的,于是就有了很多模块。
node_modules文件夹这么大(几十M~几百M都可能),会不会影响小程序的大小?小程序的大小只与miniprogram文件夹有关,当你把云函数都部署上传到服务器之后,你把整个cloudfuntions文件夹删掉都没有关系。相同的依赖(比如都依赖wx-server-sdk)一旦部署到云函数之后,你可以选择不上传node_modules时,因为已经上传过了。
当我们把云函数login部署上传成功后,就可以在模拟器以及手机(需要重新点击预览图标并扫描二维码)里点击获取openid了。
openid是小程序用户的唯一标识,也就是每一个小程序用户都有一个唯一的openid。点击“点击获取openid”,在用户管理指引页面如果显示“用户id获取成功”以及一串字母+数字,那么表示你login云函数部署并上传成功啦。如果获取openid失败,你则需要解决login云函数的部署上传,才能进行下面的步骤哦。
调用云函数的解读
小程序的首页是”pages/index/index”,我们可以从app.json的配置项或者模拟器左下角的页面路径可以看出来。在index.wxml里有这段代码:
<button class="userinfo-nickname" bindtap="onGetOpenid">点击获取 openid</button>
也就是当点击“点击获取openid”按钮时,会触发bindtap绑定的事件处理函数onGetOpenid,在index.js里可以看到onGetOpenid事件处理函数(在index.js里找到事件处理函数onGetOpenid对比理解)调用了wx.cloud.callFunction()接口(打开技术文档对比理解)
技术文档:调用云函数wx.cloud.callFunction
调用云函数的方法很简单,只需要填写云函数的名称name(这里为login),以及需要传递的参数(这里并没有上传参数),就可以进行调用。在success回调函数里添加以下代码打印res对象:
console.log('调用login云函数返回的res',res)
添加完成之后记得保存代码哦,文件修改没有保存会在标签页有一个小的绿点。可以使用快捷键(同时按)Ctrl和S来保存(Mac电脑为Command和S)。
编译之后,再点击“点击获取openid”按钮,就能看到完整的res对象,res对象有三个参数:
事件处理函数onGetOpenid调用云函数成功之后,干了三件事情:
而userConsole页面就只是从globalData里将openid取出来通过setData渲染到页面。
小任务:你明白为啥wx.cloud.callFunction()是小程序端的API了么?思考一下为啥云开发会有小程序端的API和服务端API的区别?能理解多少是多少,不清楚也没有关系,后面会有更多内容助你理解。
为什么调用云函数login返回的res的result对象里会包含event对象、appid、userInfo这些结果?这就取决于云函数是怎么写的了。使用开发者工具打开login云函数(在cloudfuntions文件夹里)的index.js。
exports.main = (event, context) => {}
这是一个箭头函数的写法,其中event和context是参数。我们将两个打印日志修改为以下代码,相当于备注一下到底打印到哪里去了:
console.log('服务端打印的event',event)console.log('服务端打印的context',context)
保存之后,右键点击index.js文件,选择云函数增量上传:(更新文件),更新login云函数,我们再来点击“点击获取openid”按钮,打印的结果在哪里呢?在云开发控制台的云函数日志里面(注意不是开发者工具的控制台)。打开云开发控制台–云函数–日志,按函数名筛选,选择login云函数,可以看到云函数被调用的日志记录,我们可以在日志里发现:
在返回结果里我们可以看到return返回的数据
小任务:比较一下云开发控制台的云函数日志打印的结果和开发者工具控制台打印的结果,深入了解event对象、context对象、result对象与返回结果,这是云函数的比较重要的知识点。
云函数的打印日志会显示在云开发控制台的日志里面,这一点非常重要,要多加利用。只要是打印日志,无论是显示在开发者工具控制台还是显示在云开发控制台的就没有不重要的。
getWXContext()API是云开发服务端的工具类API,会返回小程序用户的openid、小程序appid、小程序用户的unionid等。说这么多不如直接打印,在下面添加一行打印信息:
const wxContext = cloud.getWXContext()console.log('getWXContext返回的结果',wxContext)
保存之后,右键点击index.js文件,选择云函数增量上传:(更新文件),更新login云函数,我们再来点击“点击获取openid”按钮,然后去云开发控制台的云函数日志里看到底返回了什么结果。
技术文档:getWXContext()
对照技术文档来理解返回的结果。
注意小程序用户 unionid只有在开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用才能获得。
return语句是终止函数的执行,并返回一个指定的值给函数调用者。这里返回了4个值,而前面我们就调用过login云函数,就是函数的调用者,所以我们打印的事件处理函数onGetOpenid的回调函数的res对象正是这个return返回的结果。那既然如此,我们在return多加一些内容看看,比如我们之前的一些数据结构案例,将return函数改为如下代码:
let lesson = "云开发技术训练营"; let enname = "CloudBase Camp"; let x = 3, y = 4, z = 5.001, a = -3, b = -4, c = -5; let now = new Date(); return { movie: { name: "霸王别姬", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp", desc: "风华绝代。" }, movielist:["肖申克的救赎", "霸王别姬", "这个杀手不太冷", "阿甘正传", "美丽人生"], charat: lesson.charAt(4), concat: enname.concat(lesson), uppercase: enname.toUpperCase(), abs: Math.abs(b), pow: Math.pow(x, y), sign: Math.sign(a), now: now.toString(), fullyear: now.getFullYear(), date: now.getDate(), day: now.getDay(), hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), time: now.getTime(), event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, }
保存之后,右键点击index.js文件,选择云函数增量上传:(更新文件),更新login云函数,我们再来点击“点击获取openid”按钮,然后去云开发控制台的云函数日志里看到底返回了什么结果。
这里我们多次反复提及更新了index.js文件之后就要选择云函数增量上传:(更新文件),更新login云函数,希望大家平时的时候注意,这也是小程序云开发服务端和小程序端一个非常大的区别。
鼠标右键cloudfunctions云函数根目录,在弹出的窗口选择新建Node.js云函数,比如输入sum,按Enter确认后,微信开发者工具会在本地(你的电脑)创建出sum云函数目录,同时也会在线上环境中创建出对应的云函数(也就是自动部署好了,可以到云开发控制台云函数列表里看到)
打开sum云函数目录下的index.js,添加 sum:event.a+event.b,到return函数里(把多余的内容可以删掉了),然后记得选择云函数增量上传:(更新文件),更新sum云函数。
return { sum:event.a+event.b, }
这个a和b是变量,但是和前面不一样的是,在服务端我们并没有声明a和b啊,这是因为我们可以在小程序端声明变量。
点击开发者工具模拟器的“快速新建云函数”,会跳转到addFunction页面,打开addFunction.wxml,我们看到测试云函数绑定的是testFunction事件处理函数。
<view class="list-item" bindtap="testFunction"> <text>测试云函数</text></view>
我们去看addFunction.js里的testFunction,看变量a和b这两个小程序端的变量是怎么和服务端的变量关联起来的,而又是如何把结果渲染到页面的。testFunction调用云函数sum同样是通过wx.cloud.callFunction,不同的是在data里有a和b:
data: { a: 1, b: 2},
data里填写的是传递给云函数的参数,也就是先把小程序端的参数传递给云函数,然后云函数处理之后再返回res对象,我们可以在success回调函数里打印res对象:
console.log("sum云函数返回的对象",res)
编译之后,我们再点击测试云函数,在控制台就能看到打印的结果,res.result.sum就是3。直接把res.result.sum通过setData赋值到result就能渲染出数字,那这个res.result是什么?JSON.stringify()又是什么?
result: JSON.stringify(res.result)
我们可以打印一下res.result,以及JSON.stringify(res.result)
console.log("res.result是啥", res.result)console.log("JSON.stringify(res.result)是啥", JSON.stringify(res.result))
res.result是对象,而JSON.stringify(res.result)是json格式, JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,因为对象如果直接渲染到页面是会显示 [object Object]的。
小任务:将小程序的参数传递给云端,有没有一点wx.request的感觉?相当于我们通过云函数写好了一个数据API,然后在小程序端调用。新建一个云函数,把各种数学运算都部署到云端,然后通过传递参数,调用这些算法,并将结果渲染到页面。
使用模拟器以及手机端点击云开发QuickStart小程序的上传图片按钮,选择一张图片并打开,如果在文件存储指引页面显示上传成功和文件的路径以及图片的缩略图,说明你的图片就上传到云开发服务器里啦。
点击云开发控制台的存储图标,就可以进入到存储管理页查看到你之前上传的图片啦,点击该图片名称可以看到这张图片的一些信息,如:文件大小、格式、上传者的OpenID以及存储位置、下载地址和File ID。复制下载地址链接,在浏览器就能查看到这张图片啦。
值得注意的是由于QuickStart小程序将“上传图片”这个按钮上传的所有图片都命名为my-image,所以上传同一格式的图片就会被覆盖掉,也就是无论你上传多少张相同格式的图片,只会在后台里看到最后更新的那张图片。以后我们会教大家怎么修改代码,让图片不会被覆盖。
我们可以把下载地址作为图床来使用的,也就是你可以把图片的下载地址放到其他网页图片是可以显示的。云存储的图片还有一个FileID(既云文件ID,以cloud://开头)则只能用于小程序的特定场景,也只有部分组件的部分属性支持,把链接粘贴到浏览器也是打不开的。
技术文档:组件支持
比如我们在index页面的index.wxml里输入以下代码,在image组件的src属性里输入你的云存储图片的FileID,它是可以显示出来的。
<image src="你的图片的FileID"></image>
但是如果你退出登录开发者工具,图片就不会显示,而且还会报错,所以不要把图片的FileID当做图床用,FileID另有它用。体验云调用之服务端调用
重新点击开发者工具的预览图标,然后用手机扫描二维码,在手机端点击云开发QuickStart的云调用里的服务端调用,就可以发送模板消息和获取小程序码。
点击获取小程序码,如果显示调用失败,说明你的openapi云函数没有部署成功,需要你先部署成功才行哦。调用成功,就能获取到你的小程序码啦,这个小程序码也会保存到云开发的存储里。
发送模板消息,只能在手机微信里预览测试哦,使用微信开发者工具是发送不了模板消息,而且控制台还会报错
点击发送模板消息,你的微信就会收到一则服务通知,该通知是由你的小程序发出的购买成功通知。这就是微信的模板消息啦,很多微信公众号、小程序都会有这样的功能,使用小程序云开发,我们也可以轻松定制自己的服务通知(后面会教大家如何定制)。
点击微信开发者工具的云开发图标,打开云开发控制台,点击数据库图标进入到数据库管理页,点击集合名称右侧的+号图标,就可以创建一个数据集合了,这里我们只需要添加一个counters的集合(不需添加数据)即可。
在开发者工具的编辑器里展开miniprogram文件夹,打开pages文件下databaseGuide里的databaseGuide.js文件,在这里找到 onAdd: function (){}、 onQuery: function (){}、 onCounterInc: function (){}、 onCounterDec: function (){}、 onRemove: function (){}分别选中绿色的代码块,然后同时按快捷键Ctrl和/(Mac电脑的快捷键为Command和/),就可以批量取消代码的注释。
//是前端编程语言JavaScript的单行注释,位于 // 这一行的代码都不会执行,我们使用快捷键就是批量取消这些代码的注释,让整段代码生效。之所以显示为绿色,是微信开发者工具为了让我们看得更清晰而做的语法高亮。
以上的函数是在小程序的前端页面来操作数据库,点击开发者工具模拟器云开发QuickStart里的前端操作数据库,
在第1步(数据库指引有标注),我们会获取到用户的openid,也就是说你没有获取到openid是没法通过小程序的前端来操作数据库的哦第2步,需要我们在云开发控制台里的数据库管理页创建一个counters的集合(不需添加数据);第3步,点击按钮页面的按钮“新增记录”(按钮就在这个页面的第4条与第5条之间,看起来不是那么明显),这时会调用 onAdd方法,往counters集合新增一个记录(之前手动添加有木有很辛苦?),我们可以去云开发控制台的数据库管理页查看一下counters集合是不是有了一条记录;大家可以多点击几下新增记录按钮,然后去云开发控制台看数据库又有什么变化。也就是小程序前端页面通过 onAdd方法,在数据库新增了记录。第4步,点击按钮查询记录,这时调用的是 onQuery方法就能在小程序里获取到第3步我们存储到数据库里的数据啦第5步,点击计数器按钮+号和-号,可以更新count的值,点击+号按钮会调用 onCounterInc方法,而点击-号 onCounterDec方法,比如我们点击加号到7,再去数据库管理页查看最新的一条记录(也就是最后一条),它的count由原来的1更新到了7(先点刷新按钮),我们再点击-号按钮到5,再来数据库管理页查看有什么变化变化(先点刷新按钮)第6步,点击删除记录按钮,会调用 onRemove方法,这时会删掉数据库里最新的记录(也就是第5步里的那一条记录)。
通过实战我们了解到,databaseGuide.js文件里的 onAdd、 onQuery、 onCounterInc、 onCounterDec、 onRemove可以实现小程序的前端页面来操作数据库。
这些函数大家可以结合databaseGuide.js文件和云开发技术文档关于数据库的内容来理解。(关于前端是如何操作数据库的,我们之后还会深入讲解,这里只需要了解大致的逻辑即可)
在前面JavaScript的章节里我们了解到数据以及数据的存储是非常重要的,而有了数据库,用函数生成的数据能够比缓存存储的更加持久,而且在上面我们实现了对数据进行增(添加)、删(删除)、改(修改、更新)、查(查询并渲染到页面),不仅如此,缓存的容量也比较有限,最多不过10M,而数据库可以存几百G以上,可见它的重要性。
云开发QuickStart模板小程序有很多多余的页面,这个我们只需要把miniprogram文件夹下的pages、images、components、style文件夹里的文件清空,以及app.json的pages配置项里的页面删除,把app.wxss里的样式代码都删掉就是一个全新的开始啦。这是方法之一,也可以使用下面的方法(推荐学习时使用下面的方法)。
基于没有使用云开发的项目改造当然我们也可以把前面章节没有使用云开发的项目改造成使用云服务,首先在小程序的根目录下新建一个文件夹,比如cloudfunctions,然后在project.config.json添加云函数文件夹的路径配置即可,
"cloudfunctionRoot": "cloudfunctions/",
然后新建一个miniprogram文件夹,把小程序除了project.config.json以外的其他文件,比如pages、utils、images、app.js、app.json等文件都放到miniprogram文件夹里,再在project.config.json添加miniprogramRoot配置:
"cloudfunctionRoot": "cloudfunctions/","miniprogramRoot":"miniprogram/",
值得一提的是,云函数部署上传成功,我们就可以一直调用,只要你的小程序的appid以及环境ID没有变,你创建再多的小程序项目,都可以直接调用部署好的云函数,比如前面的login、echo、callback、sum等云函数。也就是说云函数一旦部署成功,它就一直在云端服务器里,哪怕你把小程序本地的云函数都删掉也没有关系。
当新建了并配置了云函数根目录为cloudfunctions文件夹之后,云函数根目录里并没有云函数,我们可以右键点击云函数根目录cloudfunctions文件夹选择同步云函数列表,可以把所有云端的云函数列表都列举出来(这只是列举了列表),而要修改云函数里面的内容,我们可以右键点击其中的一个云函数目录选择下载云函数即可。
除此之外,我们需要小程序的app.js的生命周期函数onLaunch里使用wx.cloud.init()来初始化云开发能力:
onLaunch: function () { if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else { wx.cloud.init({ env: '你的环境ID', traceUser: true, }) } },
云开发能力全局只需要初始化一次即可,这里的traceUser属性设置为true,会将用户访问记录到用户管理中,在云开发控制台的运营分析—用户访问里可以看到访问记录。
在小程序端初始化云开发能力的代码里,涉及到wx.cloud以及基础库版本的知识。关于wx.cloud,我们可以和之前在控制台了解wx对象一样,直接在开发者工具的控制台里输入:
wx.cloud
来了解对象有哪些属性与方法。我们可以看到有如下方法:
CloudID: ƒ () //用于云调用获取开放数据callFunction: ƒ () //调用云函数database: ƒ () //获取数据库的引用deleteFile: ƒ () //从云存储空间删除文件downloadFile: ƒ () //从云存储空间下载文件getTempFileURL: ƒ () //用云文件 ID 换取真实链接init: ƒ () //初始化云开发能力uploadFile: ƒ () //上传文件至云存储空间
而关于基础库,有三个地方需要注意它的存在,平时开发的时候需要留意
开发者工具的project.config.json里有这样一个属性libVersion,这个也可以在开发者工具工具栏右上角的详情里的本地设置里的调试基础库,建议切换到最新,切换后libVersion的值也会修改到切换的版本;官方文档基础库的更新日志,小程序更新非常频繁,而更新的核心就是基础库:所以基础库更新日志要经常留意每个API,技术文档都会标明它的基础库支持的最低版本,而小程序·云开发 SDK是2.2.3以上的基础库才开始支持的。
在上一节,我们大致体验了一下云开发:开通了云开发服务,相当于在云端拥有了一个Nodejs的环境,我们可以把云函数部署到云端。通过云开发的能力进行调用云函数、上传图片、操作数据库以及使用小程序的一些开放接口,下面来进一步了解和使用云开发能力,并加强对云端测试、本地调试以及本地Console日志打印,云端日志打印的理解。
用编程来写项目,就像是在做一系列精密而复杂的实验,你不能总是劳烦他人帮你解决问题,而是要掌握调试、测试、日志打印等手段来检查每一步操作是否正确,你需要学会查看报错信息,如果不正确问题在哪、出了什么问题,你才能有针对性的去搜索,有针对性的去咨询他人。
为了能够让大家更加清楚的了解:完整操作一个云函数的流程以及本地调试与云端测试的重要性,我们以长方形的边长(a、b)求周长、面积这个简单的数学公式为例。
首先我们右键点击云函数根目录(也就是cloudfunctions文件夹),选择新建Nodejs云函数,函数名为长方形的英文rectangle,然后打开index.js,修改return里的内容为如下:
exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { circum:(event.width+event.height)*2, area:event.width*event.height }}
circum是周长,周长=(宽度width+高度height)✖️2;area是面积,面积=宽度width✖️高度height,只要我们之后把长方形的参数宽度width和高度height传递进来(之后我们会来讲怎么传),即可获得长方形的周长和面积。
建好云函数之后,我们右键点击云函数目录,也就是rectangle文件夹,选择在终端中打开,使用npm install来安装依赖。
npm install
对于一个复杂的云函数,我们最好是先在本地测试一下云函数是否正确,然后再部署上传到云端。那如何本地测试呢?右键点击云函数目录,也就是rectangle文件夹,选择本地调试(这种方式进入本地调试会默认开启rectangle的本地调试),修改以下代码:
{ "key": "value"}
我们给参数宽度width和高度height赋值(注意传递的是JSON格式,最后一个参数结尾不能有逗号,),比如赋值为3和6:
{ "width": 3, "height":7}
然后点击调用,如果显示函数执行成功(注意仍然是在调试的console标签),并得到周长circum和面积area的结果分别为20、21,那证明云函数没有写错,这时候我们就可以部署并上传到云端了。
第三步:云端测试云函数是否正确
打开云开发控制台的云函数标签页,找到rectangle云函数,点击云端测试,同样我们给参数赋值,将以下代码进行修改:
{ "key1": "test value 1", "key2": "test value 2"}
比如给宽度width赋值为4,高度height赋值为7,即代码修改为:
{ "width": 4, "height": 7}
然后点击运行测试,(会等一段时间),再来查看测试的结果,如果返回结果如下,则表示在云端的云函数可以正常调用:
{"circum":22,"area":28}
在云端测试的调用结果也是可以在云开发控制台云函数的日志里查看到的。
在第一节我们要触发云函数,需要在小程序页面里写一个组件(比如button)并绑定事件处理函数,然后再在事件处理函数(或在页面的生命周期函数)里使用wx.cloud.callFunction()调用云函数,通过这种方式来触发云函数,会比较麻烦,而本地调试和云端测试则可以直接触发云函数查看结果,大大提升了调试的便利度。
云函数的调用采用事件触发模型,小程序端的调用、本地调试和云端测试都会触发云函数的调用事件,其中本地调试调用的不是云端的云函数,而是小程序本地的云函数。
小任务:rectangle云函数需要传入两个参数才能返回值,有些云函数,比如前面的login云函数不需要传入参数,你知道应该怎么进行本地调试和云端测试吗?在本地调试的请求方式有手动触发和模拟器触发,开启模拟器触发,点击第一节“点击获取openid”的按钮试试看(注意这时调用的是本地的云函数,修改一下login云函数不上传试试看);
小程序默认可以创建两个环境,这两个环境都有云函数配置、数据库、云存储且独立隔离,开发上会存在两个环境切换的情况(一个用于生产环境,一个用于测试环境),而区别这两个环境的就是它们的环境ID,小程序端与云端的初始化时要注意。
在前面我们介绍过小程序的初始化是在app.js文件里使用wx.cloud.init来初始化,如下:
wx.cloud.init({ env: 'my-env-id', //可以填写生产环境或者测试环境的环境ID traceUser: true,})
这里的 env 只会决定小程序端API调用的云环境(如云函数、云存储、数据库,毕竟有两个环境里都有),并不会决定云函数中的 API 调用的环境。在开发者工具的控制台,也会打印默认环境:当前代码初始化的默认环境为:你的默认环境ID
当前代码初始化的默认环境为:你的默认环境ID
云函数中的API调用的环境也可以使用初始化来设置。
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,})
cloud.DYNAMIC_CURRENT_ENV设置 API 默认环境等于当前所在环境。建议所有的云函数都使用以上方式来初始化,也就是配置env的值为cloud.DYNAMIC_CURRENT_ENV或使用你的环境ID,不要为空。
每一个云函数都会用到wx-server-sdk这个Node包,而要使用这个包都需要有Nodejs环境,小程序端的本地需要我们自己下载Nodejs(前面已下载),而云端则自带Nodejs环境。那这个wx-server-sdk到底什么呢?我们可以打开它的npm包地址:
npm包地址:wx-server-sdk包地址
在Dependencies标签页可以看到wx-server-sdk依赖 tcb-admin-node、protobufjs、tslib这三个包,而其中tcb-admin-node是核心,学有余力的童鞋可以看一下它的技术文档。
在wx-server-sdk中不再兼容success、fail、complete回调,只会返回Promise。在云函数中也经常会需要处理一些异步操作,在异步操作完成后再返回结果给到调用方,我们可以通过在云函数中返回一个 Promise 的方法来实现。Promise表示异步操作返回的结果。在新建的云函数里会看到下面这样的一个语句(有 async):
exports.main = async (event, context) => {}
async表示函数里有异步操作,async函数的返回值是一个 Promise 对象。在后面还会遇到 await,表示紧跟在它后面的表达式需要等待结果;以及promise对象的then()方法(有点类似于success回调函数),和catch()方法(有点类似于fail回调函数),这些我们以后会经常遇到,先理解不了也没有关系,大家在书写时推荐云函数使用上面的写法就对了。
云函数的注意事项
在云函数部署并上传到云端之后,更新里面的文件比如index.js、config.json,建议右键点击更新好的文件(不是云函数目录)选择云函数增量上传:更新文件,不建议通过上传并部署所有文件的方式,否则在几分钟内会出现云函数调用失败的情况; 删除一个云函数之后,不建议再新建一个同名的云函数并上传部署,否则在十多分钟内会出现云函数调用失败的情况,建议换一个云函数名,比如login换成user,在小程序端使用 wx.cloud.callFunction({name: ''})调用云函数时把name的值换成user就可以了 调用云函数时,我们还可以在开发者工具调试面板的NetWork标签查看调用云函数的情况。
在生命周期章节,我们大致介绍了一下如何使用wx.getUserInfo API和通过组件的open-type=”getUserInfo” 来获取用户的信息(如头像、昵称),下面我们就来详细介绍云开发的免鉴权登录与用户信息的结合。
使用open-type=”getUserInfo” 来获取用户信息的作用和 wx.getUserInfo API基本效果是一样的,区别在于wx.getUserInfo 这种方式最好是在用户允许获取公开信息(也就是res.authSetting[‘scope.userInfo’]的值为true)之后再调用,如果用户拒绝了授权就不会再有弹窗(除非用户删掉了你的小程序再使用),调用就会失败,而使用组件的方式是用户主动点击,用户即使拒绝了,再点击仍会弹出授权弹窗。所以推荐先使用组件来获取用户授权,然后再来使用wx.getUserInfo来获取用户信息。
使用开发者工具新建一个login页面,然后在login.wxml里输入以下代码,我们通过组件的方式来获取用户的信息:
<button open-type="getUserInfo" bindgetuserinfo="getUserInfomation">点击获取用户信息</button><image src="{{avatarUrl}}"></image><view>{{city}}</view><view>{{nickName}}</view>
在login.js的data里初始化avatarUrl、nickName以及city,没有获取到用户信息时,用一张默认图片代替,昵称显示用户未登录,city显示为未知:
data: { avatarUrl: '/images/user-unlogin.png', nickName:"用户未登陆", city:"未知", },
然后在login.js文件里输入以下代码,在事件处理函数getUserInfomation我们可以打印event对象,open-type=”getUserInfo”的组件的event对象的detail里就有userInfo:
getUserInfomation: function (event) { console.log('getUserInfomation打印的事件对象', event) let { avatarUrl, city, nickName}= event.detail.userInfo this.setData({ avatarUrl,city, nickName }) },
将获取的avatarUrl,city,nickName通过this.setData()赋值给data。编译之后点击点击获取用户信息按钮,首先会弹出授权弹窗,当用户确认之后,就会显示用户的信息。
我们发现获取到的头像不是很清晰,这是因为默认的头像大小为132132(UserInfo用户头像说明),如果把avatarUrl链接后面的132修改为0就能获取到640640大小的头像了:
getUserInfomation: function (event) { let { avatarUrl, city, nickName}= event.detail.userInfo avatarUrl = avatarUrl.split("/") avatarUrl[avatarUrl.length - 1] = 0; avatarUrl = avatarUrl.join('/'); this.setData({ avatarUrl,city, nickName }) },
在获得了用户授权和用户信息的情况下,刷新页面或进行页面跳转,用户的个人信息还是不会显示,这是因为getUserInfomation事件处理函数点击组件时才触发,我们需要在页面加载时也能触发获取用户信息才行。
我们可以在login.js的onLoad生命周期函数里输入以下代码,当用户授权之后来调用wx.getUserInfo() API:
wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { wx.getUserInfo({ success: res => { let { avatarUrl, city, nickName } =res.userInfo this.setData({ avatarUrl, city, nickName }) } }) } }});
这样当我们加载页面时,用户的信息就能显示出来了,不过这里的头像是从API里重新取的,也会不清晰。我们当然可以像之前一样把头像的链接替换一下,但是如果每个页面都这么写就会很麻烦,解决的方法有2种,一种是把高清头像存储到缓存里,还有一种是把代码封装成一个组件(大家可以自己研究如何自定义组件了)。
尽管我们已经获取到了用户的头像、昵称等信息,但是这不能称之为真正意义的登录,只有获取到了用户身份的唯一ID也就是openid,我们才能把用户行为比如点赞、评论、发布文章、收藏等与用户挂钩,用户这些行为都与数据库有关,而能够确定点赞、评论、文章、收藏这些数据与用户关系的就是openid,也就是说只要获取到了openid就意味着用户已经登录,而获取用户信息(如头像、昵称)不过是一个附加服务,这两个是可以完全独立的。没有openid,我们也无法把用户信息给存储到数据库,也就没法让用户自定义用户信息。无论是用户行为,还是用户的信息,openid都是一个重要的桥梁。
通过前面的login云函数,我们就已经可以获取到用户的openid。无需维护复杂的鉴权机制,即可获取天然可信任的用户登录态(openid),是云开发的一个重要优势。无论是云存储还是云数据库,openid都扮演着一个重要的角色。
要把图片上传到云存储,会使用到wx.cloud.uploadFile,这个API是小程序端的API,它是把本地资源也就是临时文件里的文件上传到云存储里。在前面《图片、缓存与文件》章节里我们已经了解到如何把图片上传到小程序的临时文件,而要把临时文件上传到云存储,则需要调用wx.cloud.uploadFile API。
技术文档:wx.cloud.uploadFile
在wx.cloud.uploadFile技术文档里,可以看到要调用API,需要获取图片的filePath,在小程序里为临时文件的路径,也就是要把上传到小程序的临时文件路径赋值给它;还有一个cloudPath,这个为文件的云存储路径,这个是我们可以任意设置的。
使用开发者工具在login.wxml里添加以下代码,代码和前面章节基本一致,大家也可以回顾一下以前的内容:
<button bindtap="chooseImg">选择图片</button><image mode="widthFix" src="{{imgurl}}"></image>
然后在login.js的data里初始化imgurl,这里imgurl是一个字符串,
data: { imgurl: "", },
然后在login.js里添加事件处理函数chooseImg,我们再来回顾一下临时文件的知识:
chooseImg: function () { wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: function (res) { console.log(res) console.log(res.tempFilePaths) } }) },
编译后,上传一张图片,在控制台里我们可以看到res.tempFilePaths是一个数组格式,而wx.cloud.uploadFile的filePath是一个字符串,所以我们在上传时,可以把第一张图片的路径(字符串)赋值给filePath:
const filePath = res.tempFilePaths[0]
我们知道一个文件由文件名称和文件后缀构成,比如tcb.jpg和cloudbase.png,jpeg说明图片的格式是JPG格式,而png说明图片是PNG格式,文件名称相同且格式相同就是出现覆盖,如果我们随意更改了文件的后缀,大多数文件就会打不开。所以要把cloudPath云存储的路径需要我们把文件名和后缀给处理好。
当我们把图片上传到小程序的临时文件后,我们可以查看一下临时路径是什么样子的:
http://tmp/wx7124afdb64d578f5.o6zAJs291xB-a5G1FlXwylqTqNQ4.esN9ygu5Hmyfccd41d052e20322e6f3469de87f662a0.png
临时路径的文件名就不是原来的文件名,会变成一段长字符,但文件的格式还是原来的文件格式(后缀)。那cloudPath要输入文件的路径,就需要填写文件名和文件格式,这个要怎么处理呢?
在上一节的QuickStart小程序里,文件上传到云存储的处理方式如下:
const cloudPath = 'my-image' + filePath.match(/.[^.]+?$/)[0]
也就是它把上传的所有图片都命名为my-image,而文件的后缀还是原来的文件后缀(也就是文件格式不变)。这里的filePath.match(/.[^.]+?$/)[0]是字符串的正则处理,后面我们会来详细了解。我们先可以在开发者工具的控制台输入以下代码了解一下它的功能:
const filepath="http://tmp/wx7124afdb64d578f5.o6zAJs291xB-a5G1FlXwylqTqNQ4.esN9ygu5Hmyfccd41d052e20322e6f3469de87f662a0.png"filepath.match(/.[^.]+?$/)[0]
打印可以得到临时文件的后缀,这里为.png。这种把所有文件都命名为my-image的做法,会导致当文件的后缀相同时文件会被覆盖,如果不希望文件被覆盖,我们需要给文件命不同的名字,我们可以这样处理:
const cloudPath = `${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/.[^.]+?$/)[0]
给文件名加上时间戳和一个随机数,时间戳是以毫秒计算,而随机数是以1000内的正整数,除非1秒钟(1秒=1000毫秒)上传几十万张照片,不然文件名是不会重复的。
结合上面的内容,我们可以把wx.chooseImage()的success回调函数如下处理:
success: function (res) { const filePath = res.tempFilePaths[0] const cloudPath = `${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/.[^.]+?$/)[0] wx.cloud.uploadFile({ cloudPath, filePath, success: res => { console.log('上传成功后获得的res:', res) }, })}
编译之后,我们再次上传一张图片就会打印上传成功之后的res对象,里面包含图片在云存储里的fileID,注意它的文件名和文件后缀,以及我们可以在云开发控制台的存储里找到你上传的图片,也就是说我们上传图片到云存储是无法直接获取到图片的下载地址的。
在存储里我们都是把所有的图片放在根目录下,没有二级目录,那我们能不能建一个二级目录呢?当然是可以的,我们可以在cloudPath的前面加一个文件路径就可以了,比如:
const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/.[^.]+?$/)[0]
渲染云存储图片到组件
在上一节组件支持部分了解到,我们是可以把fileID直接在小程序的某些组件里渲染出来的。综合以上内容chooseImg事件处理函数最终为以下代码(注意this.setData的this指向,这里为了方便把success回调都写成了箭头函数):
chooseImg: function () { wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res=> { const filePath = res.tempFilePaths[0] const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/.[^.]+?$/)[0] wx.cloud.uploadFile({ cloudPath, filePath, success: res => { console.log('上传成功后获得的res:', res) const imgurl=res.fileID this.setData({ imgurl }) }, }) } }) },
在云开发控制台的存储里,我们可以看到每张图片的详细信息都有上传者 Open ID,无论你是使用开发者工具在模拟器的小程序里上传还是预览在手机的小程序里上传,只要你用的是同一个微信账号,这个上传者openid都是一致的,云存储会自动记录上传者的openid。
小任务:结合《图片、缓存与文件》章节里的wx.chooseMessageFile()的知识,将客户端会话(微信聊天窗口)里的视频、音频、PDF、Excel等也上传到云存储里。
云开发不仅在小程序端可以上传文件到云存储,还可以通过云函数也就是云端上传图片到云存储(这里会涉及到一点Nodejs的知识)。
技术文档:uploadFile
注意云函数上传图片的API属于服务端API,与wx.cloud.uploadFile是小程序端API不同。
使用开发者工具右键点击云函数根目录也就是cloudfunctions文件夹,选择新建Node.js云函数,云函数的名称命名为uploadimg,右键点击uploadimg文件夹,选择硬盘打开,然后拷贝一张图片如demo.jpg进去,文件结构如下:
uploadimg云函数目录 ├── index.js ├── package.json ├── demo.jpg
然后打开index.js,输入以下代码:
const cloud = require('wx-server-sdk')const fs = require('fs')const path = require('path')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,})exports.main = async (event, context) => { const fileStream = fs.createReadStream(path.join(__dirname, 'demo.jpg')) return await cloud.uploadFile({ cloudPath: 'tcbdemo.jpg', fileContent: fileStream, })}
然后右键点击uploadimg文件夹,选择在终端中打开,输入npm install安装依赖,再点击uploadimg文件夹,选择上传并部署所有文件(这时图片也一并上传到了云端)。
由于云端测试无法获取用户登陆态信息,所以我们不能在云端测试里把图片上传到云存储,需要在小程序端调用,使用开发者工具在login.wxml输入以下代码,也就是新建一个绑定uploadimg事件处理函数的button用于触发:
<button bindtap="uploadimg">云函数上传图片</button>
然后在login.js里输入以下代码,在事件处理函数uploadimg里调用uploadimg云函数,并返回调用之后的res对象:
uploadimg() { wx.cloud.callFunction({ name: 'uploadimg', success: res => { console.log(res) } }) },
编译之后,点击云函数上传图片按钮,就可以调用uploadimg云函数,从而调用uploadFile API将服务端/云端的图片上传到云存储里面啦,可以打开云开发控制台的云存储查看是否有tcbdemo.jpg这张图片。
注意,通过这种方式上传到云存储的图片,是没有上传者 Open ID的,在云存储里查看这张图片的详细信息,就可以了解到。
在调用数据库之前,我们需要先有一个比较贴近实际的数据库案例,为此把前面章节用到的知乎日报数据整理出了一个数据库文件。云开发数据库支持用文件的方式导入已有的数据(这里推荐大家使用json)。
数据库下载:知乎日报文章数据
右键点击链接,将data.json存储到电脑。为了方便大家阅读与编辑data.json文件的内容,推荐大家使用Visual Studio Code编辑器。
代码编辑器:Visual Studio Code
编辑器的汉化与插件:可能你安装的VS Code的界面是英文的,可以参照VSCode设置中文显示,将VS Code汉化。
使用VS Code编辑器打开data.json,发现数据的内容与写法我们都比较熟悉,知识各个记录对象之间是使用回车 分隔,而不是逗号,这一点需要大家注意。
打开云开发控制台,在数据库里新建一个集合zhihu_daily,导入该json文件,导入时会有冲突模式选择,看下面的介绍,推荐大家使用upsert:
导入后,发现数据库自动给每一条数据(记录)都加了唯一的标识_id。
在小程序端调用数据库的方式很简单,我们可以把下面的代码写到一个事件处理函数里,然后点击组件触发事件处理函数来调用;也可以直接写到页面的生命周期函数里面;还可以把它写到app.js小程序的生命周期函数里面。
使用开发者工具,将下面的代码写到login.js的onLoad函数里面,我们
const db = wx.cloud.database()db.collection('zhihu_daily') .get() .then(res => { console.log(res.data) }) .catch(err => { console.error(err) })
编译之后,就能在控制台看到调用的20条数据库记录了(如果没有指定 limit,则默认最多取 20 条记录)。
使用云函数也可以调用数据库,使用开发者工具右键点击云函数根目录也就是cloudfunctions文件夹,选择新建Node.js云函数,云函数的名称命名为zhihu_daily,然后打开index.js,输入以下代码:
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV})const db = cloud.database()exports.main = async (event, context) => { return await db.collection('zhihu_daily') .get()}
然后右键点击index.js,选择云函数增量上传:更新文件,我们既可以使用云函数的本地调试(要本地调试需要使用npm install安装wx-server-sdk依赖),也可以使用云端测试来了解云函数调用数据库的情况。
在云开发控制台的数据库标签里,打开上一节内容里的counters集合,在这个集合里我们可以看到每条记录除了有_id字段以外,还有一个_openid字段用来标志每条记录的创建者(也就是小程序的用户)。但是在我们使用管理端(控制台和云函数)中创建的数据比如我们之前导入的zhihu_daily,就不会有 _openid 字段,因为这些记录属于管理员(不是用户)创建的记录。
我们可以自定义 _id(也就是给数据添加一个_id字段并填入任意值),但不可自定义和修改 _openid 。 _openid 是在文档创建时由系统根据小程序用户默认创建的,可以用来标识和定位文档。和云存储一样,数据库的记录也和openid有着紧密的联系。
任何一个大型的应用程序和服务,都必须会使用到高性能的数据存储解决方案,用来准确(ACID,原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability,可以拓展了解一下)、快速、可靠地存储和检索用户的账户信息、商品以及商品交易信息、产品数据、资讯文章等等等等,而云开发就自带高性能、高可用、高拓展性且安全的数据库。
在操作数据库时,我们要对数据库database、集合collection、记录doc以及字段field要有一定的了解,首先要记住这些对应的英文单词,当你要操作某个记录doc的字段内容时,就像投送快递一样,要先搞清楚它到底在哪个数据库、在哪个集合、在哪个记录里,一级一级的去找。操作数据库通常都是对数据库、集合、记录、字段进行增、删、改、查,当你清楚了这些,操作数据库就不会迷糊了。
我们可以结合Excel以及MySQL(之前没有接触过MySQL也没有关系,只看与Excel的对应就行)来理解云开发的数据库。
云数据库 | MySQL数据库 | Excel文件 |
---|---|---|
数据库database | 数据库 database | 工作簿 |
集合 collection | 表 table | 工作表 |
字段field | 数据列column | 数据表的每一列 |
记录 record/doc | 记录row | 数据表除开第一行的每一行 |
我们现在来创建一个books的集合(相当于创建一张Excel表),用来存放图书馆里面书籍的信息,比如这样一本书:
书名title | JavaScript权威指南(第6版) | |
---|---|---|
作者author | 弗兰纳根(David Flanagan) | |
标准书号isbn | 9787111376613 | |
出版信息publishInfo | 出版社press | 机械工业出版社 |
出版年份year | 2012 |
打开云开发控制台的数据库标签,新建集合books,然后选择该集合,给books里添加记录(类似于填写Excel含字段的第一行和其中一行关于书的信息记录),依次添加字段:
在数据库创建之后,我们需要在云开发控制台-数据库-集合的权限设置标签对数据库进行权限设置。数据库的权限分为小程序端和服务端(云函数、云开发控制台)。服务端拥有读写所有数据的读写权限,所以这里的权限设置只是在设置小程序端的用户对数据库的操作权限。权限控制分简易权限控制和自定义权限(也就是安全规则),建议开发者用安全规则取代简易的权限控制。
技术文档:权限控制
要使用自定义权限(也就是安全规则)来全面取代简易的权限控制,我们需要了解4个简易的权限控制所表示的意思,以及安全规则应该如何一一取代它们,也就是我们在配置集合的权限时,不再选择简易的权限控制,而是统一选择自定义权限,填写与之对应的json规则即可。
安全规则可以让更加灵活而又明确地自定义前端数据库读写权限的能力,通过配置安全规则,开发者可以精细化的控制集合中所有记录的读read、写write权限。其中write权限还可以细分为create新建、update更新、delete删除等权限,还支持比较、逻辑运算符进行更加精细化的权限配置。
所有用户可读,仅创建者可读写:比如用户发的帖子、评论、文章,这里的创建者是指小程序端的用户,也就是存储UGC(用户产生内容)的集合要设置为这个权限;
{ "read": true, "write": "doc._openid == auth.openid"}
仅创建者可读写:比如私密相册,用户的个人信息、订单,也就是只能用户自己读与写,其他人不可读写的数据集合;
{ "read": "doc._openid == auth.openid", "write": "doc._openid == auth.openid"}
所有人可读:比如资讯文章、商品信息、产品数据等你想让所有人可以看到,但是不能修改的内容;
{ "read": true, "write": false}
所有用户不可读写:如后台用的不暴露的数据,只能你自己看到和修改的数据;
{ "read": false, "write": false}
小程序端 API 拥有严格的调用权限控制,比如在小程序端A用户是不能修改B用户的数据的,没有这样的权限,在小程序端只能修改非敏感且只是针对单个用户的数据;对于有更高安全要求的数据,我们可以在云函数内通过服务端 API 来进行操作。
如果数据库集合里的数据是通过导入的方式获取的,这个集合的权限默认为“仅创建者可读写”,这个权限在服务端(云函数)可以调用,但是在小程序端可能会返回空数组哦,所以一定要记得根据情况修改权限。
小程序端与云函数的服务端无论是在权限方面、API的写法上(有时看起来一样,但是写法不一样),还是在异步处理上(比如服务端不再使用success、fail、complete回调,而是返回Promise对象),都存在非常多的差异,这一点要分清楚。
查询集合collection里的记录是云开发数据库操作最重要的知识,在上一节我们已经将中国城市经济数据china.csv的数据导入到了集合china之中,并已经设置好了集合的权限为“所有人可读,仅创建者可读写”(或使用安全规则),接下来我们就以此为例并结合中国城市经济线上excel版来讲解数据库的查询。在中国城市经济线上excel版以及云开发控制台china集合里,我们可以看到中国332个城市的名称city、省份province、市区面积city_area、建成区面积builtup_area、户籍人口reg_pop、常住人口resident_pop、GDP的数据。
查询中国GDP在3000亿元以上的前10个城市,并要求不显示_id字段,显示城市名、所在省份以及GDP,并按照GDP大小降序排列。
使用开发者工具新建一个chinadata页面,然后再在index.js的onLoad生命周期函数里输入以下代码。操作集合里的数据涉及的知识点非常繁杂,下面的案例相对比较完整,便于大家有一个整体性的理解:
const db = wx.cloud.database() //获取数据库的引用const _ = db.command //获取数据库查询及更新指令db.collection("china") //获取集合china的引用 .where({ //查询的条件指令where gdp: _.gt(3000) //查询筛选条件,gt表示字段需大于指定值。 }) .field({ //显示哪些字段 _id:false, //默认显示_id,这个隐藏 city: true, province: true, gdp:true }) .orderBy('gdp', 'desc') //排序方式,降序排列 .skip(0) //跳过多少个记录(常用于分页),0表示这里不跳过 .limit(10) //限制显示多少条记录,这里为10 .get() //获取根据查询条件筛选后的集合数据 .then(res => { console.log(res.data) }) .catch(err => { console.error(err) })
大家可以留意一下数据查询的链式写法, wx.cloud.database().collection('数据库名').where().get().then().catch(),前半部分是数据查询时对对象的引用和方法的调用;后半部分是Promise对象的方法,Promise对象是get的返回值。写的时候为了让结构更加清晰,我们做了换行处理,写在同一行也是可以的。
在上面的案例中,就包含了构建查询条件的五个方法: Collection.where()、 Collection.field()、 Collection.orderBy()、 Collection.skip()、 Collection.limit(),这五个方法是可以单独拆开使用的,比如只使用where或只使用field、limit,也可以从这5个中抽几个组合在一起使用,还可以一次查询里写多个相同的方法,比如orderBy、where可以写多次相同的。
不过值得注意的是这5个方法顺序不同查询的结果有时也会有所不同(比如orderBy多次打乱顺序的情况下),查询性能也会有所不同。通常skip最好放在后面,不要让skip略过大量数据。skip().limit()和limit().skip()效果是等价的。构建查询条件的5个方法是基于集合引用Collection的,就拿where来说,不能写成 wx.cloud.database().where(),也不能是 wx.cloud.database().collection("china").doc.where(),只能是 wx.cloud.database().collection("china").where(),也就是只能用于查询集合collection里的记录。
技术文档:Collection.where
技术文档:Collection.field
技术文档:Collection.orderBy
技术文档:Collection.skip
技术文档:Collection.limit
传入的对象的每个 <key, value> 构成一个筛选条件,有多个 <key, value> 则表示需同时满足这些条件,是 与的关系,如果需要 或关系,可使用 command.or
指令用于查询时,都会写在where内,主要对字段的值进行比较和逻辑的筛选判断。数据库 API 提供了大于、小于等多种查询指令,这些指令都暴露在 db.command 对象上。
指令Command可以分为查询指令和更新指令,这两者的用法有很大的区别,查询指令用于db.collection的where条件筛选,而更新指令则是用于db.collection.doc的update请求的字段的更新里,这两者的区别在后面我们也会反复提及。
下面我们把查询指令的比较操作符和逻辑操作符整理成了一张表格,并附上相应的技术文档,方便大家对它们有一个清晰而整体的认识。查询指令之比较
查询指令之比较 | |||
---|---|---|---|
gt | 大于 | lt | 小于 |
eq | 等于 | neq | 不等于 |
lte | 小于或等于 | gte | 大于或等于 |
in | 在数组中 | nin | 不在数组中 |
查询指令之逻辑 | |||
and | 条件与 | or | 条件或 |
not | 条件非 | nor | 都不 |
指令command是基于database数据库引用的,我们以大于gt在小程序端(以大于3000为例)的完整写法为例:
wx.cloud.database().command.gt(3000)
为了简便,通常我们会把 wx.cloud.database()会赋值给一个变量,如 db, db.command又会赋值给 ,使用时最终被简化为 .gt(3000)。通过一层一层的声明变量并赋值,大大简化了指令的写法,大家可以在其他指令都沿用这种写法。
相比于其他的比较指令等于eq和不等于neq操作符的用法非常丰富,它可以进行数值比较,我们查询某个字段比如GDP等于某个数值如17502.8亿的城市:
.where({ gdp: _.eq(17502.8), })
它还可以进行字符串的匹配,比如我们查询某个字段比如city完整匹配一个字符串如深圳:
.where({ city: _.eq("深圳"), })
注意:在查询时,gdp: _.eq(17502.8)的效果等同于gdp:17502.8,而city: _.eq(“深圳”)等同于city:”深圳”,虽然两种方式查询的结果都是一致的,但是它们的原理不同,前者用的是等于指令,后者用的是传递对象值。
eq还可以用于字段的值是数组以及对象的情况,在后面的章节我们会再来介绍。
查询广东省内、GDP在3000亿以上且在1万亿以下的城市。在广东省内也就是让字段province的值等于”广东”,而GDP的要求则是GDP这个字段同时满足大于3000亿且小于1万亿,这时就需要用到and(条件与,也就是且的意思):
.where({ province:_.eq("广东"), gdp:_.gt(3000).and(_.lt(10000)) })
上面的案例中where内的两个条件, province:.eq("广东")和 gdp:.gt(3000).and(_.lt(10000))带有跨字段的条件与and(也就是且)的关系,那如何实现跨字段的条件或or呢?
查询中国GDP在3000亿元以上且常住人口在500万以上或建城区面积在300平方公里以上的前20个大城市。这里常住人口和建成区面积只需要满足其中一个条件即可,这就涉及到条件或or(注意下面代码的格式写法):
.where( { gdp: _.gt(3000), resident_pop:_.gt(500), }, _.or([{ builtup_area: _.gt(300)} ]), )
注意上面三个条件, gdp: _.gt(3000)和 residentpop:.gt(500)是逻辑与,而与 builtuparea: .gt(300)}的关系是逻辑或。 _.or([{条件一 },{条件二 }])内是一个数组,条件一与条件二又构成逻辑与的关系。
正则表达式能够灵活有效匹配字符串,可以用来检查一个串里是否含有某种子串,比如“CloudBase技术训练营”里是否含有”技术”这个词。云数据库正则查询支持UTF-8的格式,可以进行中英文的模糊查询。正则查询也是写在where字段的条件筛选里。
技术文档:Database.RegExp
我们可以用正则查询来查询某个字段,比如city城市名称内,包含某个字符串比如”州”的城市:
.where({ city: db.RegExp({ regexp: '州', options: 'i', }) })
注意这里的city是字段,db.RegExp()里的regexp是正则表达式,而options是flag,i是flag的值表示不区分字母的大小写。当然我们也可以直接在where内用JavaScript的原生写法或调用 RegExp对象的构造函数。比如上面的案例也可以写成:
//JavaScript原生正则写法 .where({ city:/州/i }) //JavaScript调用RegExp对象的构造函数写法 .where({ city: new db.RegExp({ regexp: "州", options: 'i', }) })
数据库查询的正则表达式也支持模板字符串,比如我们可以先声明const cityname=”州”,然后用模板字符串包住cityname变量:
city: db.RegExp({ regexp:`${cityname}`, options: 'i', })
正则表达式的用法是非常繁杂的,关于正则表达式的知识可以去搜索了解更多细节。
技术文档:正则表达式
值得注意的是,在数据库查询时应尽可能避免过度使用正则表达式来做复杂的匹配,尤其是用户访问触发较多的场景,通常情况下数据查询的响应时间(无论是小程序端还是云函数端)最好要低于500ms。
在前面我们已经介绍了集合数据请求的查询方法get,除了get查询外,请求的方法还有add新增,remove删除、update改写/更新、count统计以及watch监听,这些方法都是基于数据库集合的引用Collection的,接下来我们再来介绍如何基于Collection新增记录和统计记录的数量。
基于数据库集合的引用Collection所查询到的记录都是多条记录,也就是说我们可以对N条记录进行增、删、改、查等操作,不过目前还不支持在小程序端进行多条记录的update和remove,只能在云函数端进行这样的操作。
统计记录Collection.count
统计集合记录数或统计查询语句对应的结果记录数。小程序端与云函数端的表现会有如下差异:小程序端:注意与集合权限设置有关,一个用户仅能统计其有读权限的记录数云函数端:因属于管理端,因此可以统计集合的所有记录数。
技术文档:Collection.count()
const db = wx.cloud.database()const _ = db.commanddb.collection("china") .where({ gdp: _.gt(3000) }) .count().then(res => { console.log(res.total) })
field、orderBy、skip、limit对count是无效的,只有where才会影响count的结果,count只会返回记录数,不会返回查询到的数据。
在前面我们将知乎日报的数据导入到了zhihu_daily的集合里,接下来,我们就来给zhihu_daily新增记录。
技术文档:Collection.add
使用开发者工具新建一个zhihudaily的页面,然后在zhihudaily.wxml里输入以下代码,新建一个绑定了事件处理函数为addDaily的button按钮:
<button bindtap="addDaily">新增日报数据</button>
然后再在zhihudaily.js里输入以下代码,在事件处理函数addDaily里调用Collection.add,往集合zhihu_daily里添加一条记录,如果传入的记录对象没有 _id 字段,则由后台自动生成 _id;若指定了 _id,则不能与已有记录冲突。
addDaily(){ db.collection('zhihu_daily').add({ data: { _id:"daily9718005", title: "元素,生生不息的宇宙诸子", images: [ "https://pic4.zhimg.com/v2-3c5d866701650615f50ff4016b2f521b.jpg" ], id: 9718005, url: "https://daily.zhihu.com/story/9718005", image: "https://pic2.zhimg.com/v2-c6a33965175cf81a1b6e2d0af633490d.jpg", share_url: "http://daily.zhihu.com/story/9718005", body:"<p><strong><strong>谨以此文,纪念元素周期表发布 150 周年。</strong></strong></p>
<p>地球,世界,和生活在这里的芸芸众生从何而来,这是每个人都曾有意无意思考过的问题。</p>
<p>科幻小说家道格拉斯·亚当斯给了一个无厘头的答案,42;宗教也给出了诸神创世的虚构场景;</p>
<p>最为恢弘的画面,则是由科学给出的,另一个意义上的<strong>生死轮回,一场属于元素的生死轮回</strong>。</p>" } }) .then(res => { console.log(res) }) .catch(console.error) }
点击新增日报数据的button,会看到控制台打印的res对象里包含新增记录的_id为我们自己设置的daily9718005。打开云开发控制台的数据库标签,打开集合zhihu_daily,翻到最后一页,就能看到我们新增的记录啦。
注意和导入的数据不同的是,在小程序端新增记录,都会自动添加一个_openid的字段,它的值等于用户 openid,_openid的值是不允许修改的。当我们把集合的权限改为仅创建者可读写,或所有人可读,仅创建者可读写,在小程序端查询或更新记录时,会自动添加一个条件,
.where({ _openid:"当前用户的openid" })
所以这就是为什么尽管集合里面有数据,但是由于有了这个条件,只要记录里没有_openid或openid不匹配就查询不到记录。
集合请求方法注意事项
get、update、count、remove、add等都是请求,在小程序端可以有callback和promise两种写法,但是在云函数端只能用promise,不能用callback。为了方便,建议大家统一使用promise的写法,也就是then、catch。get、update、count、remove、add请求不能在一个数据库引用里同时存在。比如不能又是get(),又是count()的,不能这么写:
db.collection('china').where({ _openid: 'xxx', }).get().count().add()
在云开发能力章节我们已经介绍过如何在云函数端调用数据库,这里也是一样。新建一个云函数chinadata,然后在 exports.main = async (event, context) => {}输入以下代码,注意是 const db = cloud.database(),wx. cloud.database(),云函数端的数据库引用和小程序端有所不同:
const db = cloud.database()const _ = db.commandreturn await db.collection("china") .where({ gdp: _.gt(3000) }) .field({ _id: false, city: true, province: true, gdp: true }) .orderBy('gdp', 'desc') .skip(0) .limit(10) .get()
try/catch async错误处理当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。如果有多个 await 则可以将其都放在 try/catch 中。
然后右键chinadata云函数根目录选择在终端中打开,输入npm install,之后上传并部署所有文件。
在前面我们了解到,调用云函数可以使用本地调试、云端测试,我们还可以在小程序端调用云函数,将云函数的数据返回到小程序端来。使用开发者工具在chinadata.wxml里输入以下代码,也就是我们通用点击按钮触发事件处理函数:
<button bindtap="callChinaData">调用chinadata云函数</button>
再在事件处理函数里调用云函数,在chinadata.js里输入getChinaData事件处理函数来调用chinadata云函数:
getChinaData() { wx.cloud.callFunction({ name: 'chinadata', success: res => { console.log("云函数返回的数据",res.result.data) }, fail: err => { console.error('云函数调用失败:', err) } }) },
在模拟器里点击调用chinadata云函数的button按钮,就能在控制台里看到云函数返回的查询到的结果,大家可以通过setData的方式将查询的结果渲染到小程序页面,这里就不介绍啦。
基于数据库集合的引用Collection,我们可以先匹配 where 语句查询到相关条件的多条记录,再来调用Collection.remove()来进行删除。五个查询方法,skip和limit不支持,field、orderBy没有意义,只有where条件可以用来筛选记录。数据一旦删除就不能再找回了。
技术文档:Collection.remove()
我们可以把之前建好的chinadata云函数 exports.main = async (event, context) => {}里的代码修改为如下,即删除省份province为广东的所有数据:
return await db.collection('china') .where({ province:"广东" }) .remove()
在模拟器里点击调用chinadata云函数的button按钮,就能在控制台里看到云函数返回的对象,其中包含stats: {removed: 22},即删除了22条数据。
更新多条记录Collection.update
我们可以把之前建好的chinadata云函数 exports.main = async (event, context) => {}里的代码修改为如下,也就是先查询省份province为湖北的记录,给这个记录更新一个字段英文省份名pro-en:
return await db.collection('china') .where({ province:"湖北" }) .update({ data: { "pro-en": "Hubei" }, })
这里要注意的是,pro-en这个字段之前是没有的,通过Collection.update不只是起到更新的作用,还可以批量新增字段并赋值,也就是update时记录里有相同字段就更新,没有就新增; "pro-en": "Hubei",直接使用pro-en会报错,用双引号效果等价。
如果你想给导入的数据添加_openid字段,只用云函数是没法实现的,因为云函数没有用户的登录态。我们需要先在小程序端调用云函数比如login返回openid,再将openid的值再传给chinadata云函数,才能给记录添加openid。
在前面我们已经了解了基于集合引用Collection构建查询条件的5个方法,以及一些请求方法,接下来我们来讲一下基于集合记录引用Document的四个请求方法:获取单个记录数据Document.get()、删除单个记录Document.remove()、更新单个记录Document.update()、替换更新单个记录Document.set()。和基于Collection不一样的是,前者的增删改查是可以批量多条的,而基于Document则是操作单条记录。
查询集合collection里的记录常用于获取文章、资讯、商品、产品等等的列表;而查询单个记录doc的字段值则常用于这些列表里的详情内容。如果你在开发中需要增删改查某个记录的字段值,为了方便让程序可以根据_id找到对应的记录,建议在创建记录的时候_id用程序有规则的生成。
集合里的每条记录都有一个 _id 字段用以唯一标志一条记录,_id 的数据格式可以是number数字,也可以是string字符串。这个_id是可以自定义的,当导入记录或写入记录没有自定义时系统会自动生成一个非常长的字符串。查询记录doc的字段field值就是基于_id的。
技术文档:获取单个记录数据Document.get()
比如我们查询其中知乎日报的一篇文章(也就是其中一条记录)的数据,使用开发者工具zhihudaily页面的zhihudaily.js的onLoad生命周期函数里输入以下代码(db不要重复声明):
db.collection('zhihu_daily').doc("daily9718006") .get() .then(res => { console.log('单个记录的值',res.data) }) .catch(err => { console.error(err) })},
如果集合的数据是导入的,那_id是自动生成的,自动生成的_id是字符串string,所以doc内使用了单引号(双引号也是可以的哦),如果你自定义的_id是number类型,比如自定义的_id为20191125,查询时为doc(20191125)即可,这只是基础知识啦。
技术文档:删除单个记录Document.remove()
removeDaily(){ db.collection('zhihu_daily').doc("daily9718006") .remove() .then(console.log) .catch(console.error) }
技术文档:更新单个记录Document.update()
updateDaily(){ db.collection('zhihu_daily').doc("daily9718006") .update({ data:{ title: "【知乎日报】元素,生生不息的宇宙诸子", } }) },
技术文档:替换更新单个记录Document.set()
setDaily(){ db.collection('zhihu_daily').doc("daily9718006") .set({ data: { "title": "为什么狗会如此亲近人类?", "images": [ "https://pic4.zhimg.com/v2-4cab2fbf4fe9d487910a6f2c54ab3ed3.jpg" ], "id": 9717547, "url": "https://daily.zhihu.com/story/9717547", "image": "https://pic4.zhimg.com/v2-60f220ee6c5bf035d0eaf2dd4736342b.jpg", "share_url": "http://daily.zhihu.com/story/9717547", "body": "<p>让狗从凶猛的野兽变成忠实的爱宠,涉及了宏观与微观上的两层故事:我们如何在宏观上驯养了它们,以及这些驯养在生理层面究竟意味着什么。</p>
<p><img class="content-image" src="http://pic1.zhimg.com/70/v2-4147c4b02bf97e95d8a9f00727d4c184_b.jpg" alt=""></p>
<p>狗是灰狼(Canis lupus)被人类驯养后形成的亚种,至少可以追溯到 1 万多年以前,是人类成功驯化的第一种动物。在这漫长的岁月里,人类的定向选择强烈改变了这个驯化亚种的基因频率,使它呈现出极高的多样性,尤其体现在生理形态上。</p>" } }) }
在云开发能力章节我们了解到小程序端和服务端都可以上传文件到云存储,不过在实际开发中云存储里的文件链接需要被记录在数据库里才方便调用。接下来我们就来介绍云存储文件的增删改查是如何与数据库的增删改查结合在一起的。在云数据库入门章节我们所涉及到的数据库里数据类型还非常简单,在这一章里我们会来介绍如何操作数据库的数组和对象等复杂数据类型的增删改查。
不经过数据库直接把文件上传到云存储里,这样文件的上传、删除、修改、查询是无法和具体的业务对应的,比如文章商品的配图、表单图片附件的添加与删除,都需要图片等资源能够与文章、商品、表单的ID能够一一对应才能进行管理(在数据库里才能对应),而这些文章、商品、表单又可以通过数据库与用户的ID、其他业务联系起来,可见数据库在云存储的管理上扮演着极其重要的角色。
和Excel表、关系型数据库(如MySQL)以行和列、多表关系来设计表结构不同的是,云开发的数据库是基于文档的。我们可以在一个记录里嵌套多层数组和对象,把每个文档所需要的数据都嵌入到一个文档里,而不是分散到多个不同的集合。
比如我们想做一个网盘小程序,用来记录用户信息,以及创建的相册、文件夹,这里相册和文件夹因为可以创建很多个,所以它是一个数组;而每一个相册对象和文件夹对象里都可以存储一个照片列表和文件列表,我们发现在云开发数据库里一个元素的值是数组,数组里又嵌套对象,对象里又有元素是数组是非常常见的事情。
以下是网盘小程序的数据库设计,包含了一个用户的信息,上传的所有文件和照片等信息:
{ "_id": "自动生成的ID", "_openid": "用户在当前小程序的openid", "nickName": "用户的昵称", "avatarUrl": "用户的头像链接", "albums": [ { "albumName": "相册名称", "coverURL": "相册封面地址", "photos": [ { "comments": "照片备注", "fileID": "照片的地址" } ] } ], "folders": [ { "folderName": "文件夹名称", "files": [ { "name": "文件名称", "fileID": "文件的地址", "comments": "文件备注" } ] } ]}
如果是用关系型数据库,就会建user表来存储用户信息,albums表存储相册信息,folders表存储文件夹信息,photos表存储照片信息,files表存储文件信息,相信大家可以通过这个案例对云数据库是面向文档的有一个大致的了解。
当然云开发的数据库也是可以把数据分散到不同集合的,需要视不同的情况而定,在后面章节我们会介绍。这种将每个文档所需的数据都嵌入到一个文档内部的做法,我们称之为反范式化(denormalization),将数据分散到多个不同的集合,不同集合之间相互引用称之为范式化(normalization),也就是说反范式化文档里包含子文档,而范式化呢,文档的子文档则是存储在另一个集合之中。
从上面可以看出,云存储与数据库就是通过fileID来取得联系的,数据库只记录文件在云存储的fileID,我们可以访问数据库相应的fileID属性进行记录的增删改查操作,与此同时调用云存储的上传文件、下载文件、删除文件等API,这样云存储就被数据库给管理起来了。
打开云开发技术文档里云存储的所有API,如上传文件uploadFile、下载文件downloadFile、删除文件deleteFile、用云文件 ID 换取真实链接getTempFileURL,我们发现这些API始终是围绕fileID来展开的,要么fileID是success回调返回的对象,要么fileID是API必备的属性。
在前面我们已经了解到,用户在小程序里有着独一无二的openid,用openid完全可以区分用户;使用云开发时用户在小程序端上传文件到云存储,这个openid会被记录在文件信息里;添加数据到数据库这个openid会被保存在_openid的字段里(也就是说我们除了可以用云函数如前面的login来获取用户的openid,还可以通过数据库的_openid字段来获取openid);而且我们在小程序端查询数据时(查询时改、删、更新等的前提),都会默认有一个 where({_openid:当前用户的openid})的条件,限制了用户write写(改、删、更新)的权限。
当用户在小程序端往数据库用Collection.add添加记录document时,会自动给该记录生成_id,同时也会创建一个_openid,_id和_openid由于都是独一无二的,只要我们获取每个用户创建的记录_id,也就能同时确定这个用户的openid。
打开云开发控制台的数据库标签,新建一个clouddisk的集合,并修改它的权限为为“所有人可读,仅创建者可读写”(或使用安全规则)。使用开发者工具新建一个folder的页面,然后在folder.js的页面生命周期函数onLoad里输入以下代码:
this.checkUser()
this调用自定义函数,开发者可以添加任意的函数或数据到 Object 参数中,在页面的函数中用 this 可以访问
然后再在Page()对象里输入以下代码,代码的意思是如果clouddisk里没有用户创建的数据,那就在clouddisk里新增一条记录;如果有数据,就返回数据:
async checkUser() { //获取clouddisk是否有当前用户的数据,注意这里默认带了一个where({_openid:"当前用户的openid"})的条件 const userData = await db.collection('clouddisk').get() console.log("当前用户的数据对象",userData) //如果当前用户的数据data数组的长度为0,说明数据库里没有当前用户的数据 if(userData.data.length === 0){ //没有当前用户的数据,那就新建一个数据框架,其中_id和_openid会自动生成 return await db.collection('clouddisk').add({ data:{ //nickName和avatarUrl可以通过getUserInfo来获取,这里不多介绍 "nickName": "", "avatarUrl": "", "albums": [ ], "folders": [ ] } }) }else{ this.setData({ userData }) console.log('用户数据',userData) } },
一个用户只能创建一条记录,如果是开一个用户可以创建多条记录…
预先搭好文档的数据框架方便我们在后面以update的方式来更新数据。
async 是“异步”的简写,async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成,await 只能出现在 async 函数中。await 在 async 函数中才会有效。假设一个业务需要分步完成,每个步骤都是异步的,而且依赖上一步的执行结果,甚至依赖之前每一步的结果,就可以使用Async Await来完成
小程序端现在完全支持async/await的写法,不过需要在开发者工具-详情-本地设置,勾选增强编译才行,否则会报以下错误。
Uncaught ReferenceError: regeneratorRuntime is not defined
async 函数返回值是 Promise 对象, async 函数内部 return 返回的值。会成为 then 方法回调函数的参数。如果 async 函数内部抛出异常,则会导致返回的 Promise 对象状态变为 reject 状态。抛出的错误而会被 catch 方法回调函数接收到。async 函数返回的 Promise 对象,必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变。也就是说,只有当 async 函数内部的异步操作都执行完,才会执行 then 方法的回调。
在async函数中使用await,那么await这里的代码就会变成同步的了,意思就是说只有等await后面的Promise执行完成得到结果才会继续下去,await就是等待,这样虽然避免了异步,但是它也会阻塞代码,所以使用的时候要考虑周全。await会阻塞代码,每个await都必须等后面的fn()执行完成才会执行下一行代码
在小程序端创建一个文件夹,需要考虑三个方面,一是文件夹在云存储里是怎么创建的;二是文件夹在数据库里的表现形式;三是小程序端页面应该怎么交互才算是创建了一个文件夹;
在云开发能力章节我们了解到,要上传demo.jpg到云存储的cloudbase文件夹里,只需要指明cloudPath云存储的路径为cloudbase/demo.jpg即可,这里的cloudbase文件夹,在我们上传文件时代码会自动创建,也就是说我们在小程序端创建文件夹不需要对云存储做任何事情,因为在云存储这里,文件夹是只有在文件上传时才会创建。
尽管文件夹在小程序端的页面交互看来非常复杂,但是它在数据库的形式看起来却非常简单,我们创建文件夹只是在操作(增删改查)数组和对象而已,以下的folders数组是文件夹列表,而一个文件夹只是数组里的一个对象而已。
"folders": [ { "folderName": "文件夹名称", "files": [ ] } ]
通过前面的分析可知,在小程序端创建文件夹,只会操作数据库的数据,而不会操作云存储,我们来看具体的代码实现。使用开发者工具新建一个folder的页面,然后在folder.wxml里输入以下代码:
<form bindsubmit="formSubmit"> <input name="name" placeholder='请输入文件夹名' auto-focus value='{{inputValue}}' bindinput='keyInput'></input> <button type="primary" formType="submit">新建文件夹</button></form>
方法一:使用push和
在folder.js里输入以下代码:
async createFolder(e) { let foldersName = e.detail.value.foldersName const folders = this.data.userData.data[0].folders folders.push({ foldersName: foldersName, files: [] }) const _id= this.data.userData.data[0]._id return await db.collection('clouddisk').doc(_id).update({ data: { folders: _.set(folders) } }) },
技术文档:字段更新操作符set
方法二:
在folder.js里输入以下代码:
async createFolder(e) { let foldersName = e.detail.value.foldersName const _id= this.data.userData.data[0]._id return await db.collection('clouddisk').doc(_id).update({ data: { folders: _.push([{ foldersName: foldersName, files: [] }]) } }) },
技术文档:数组更新操作符push
先读后写与先写后读
相信大家都应该在其他小程序体验过文件上传的功能,在交互上这个功能虽然看起来简单,但是在代码的逻辑上却包含着四个关键步骤:
使用开发者工具在folder.wxml里输入以下代码:
<form bindsubmit="uploadFiles"> <button type="primary" bindtap="chooseMessageFile">选择文件</button> <button type="primary" formType="submit">上传文件</button></form>
然后在folder.js里输入以下代码:
chooseMessageFile(){ const files = this.data.files wx.chooseMessageFile({ count: 5, success: res => { console.log('选择文件之后的res',res) let tempFilePaths = res.tempFiles for (const tempFilePath of tempFilePaths) { files.push({ src: tempFilePath.path, name: tempFilePath.name }) } this.setData({ files: files }) console.log('选择文件之后的files', this.data.files) } }) },
技术文档:wx.cloud.uploadFile
uploadFiles(e) { const filePath = this.data.files[0].src const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/.[^.]+?$/) wx.cloud.uploadFile({ cloudPath,filePath }).then(res => { this.setData({ fileID:res.fileID }) }).catch(error => { console.log("文件上传失败",error) }) },
上传成功后会获得文件唯一标识符,即文件 ID,后续操作都基于文件 ID 而不是 URL。
addFiles(fileID) { const name = this.data.files[0].name const _id= this.data.userData.data[0]._id db.collection('clouddisk').doc(_id).update({ data: { 'folders.0.files': _.push({ "name":name, "fileID":fileID }) } }).then(result => { console.log("写入成功", result) wx.navigateBack() } ) }
匹配数组第 n 项元素如果想找出数组字段中数组的第 n 个元素等于某个值的记录,那在 <key, value> 匹配中可以以 字段.下标 为 key,目标值为 value 来做匹配。如对上面的例子,如果想找出 number 字段第二项的值为 20 的记录,可以如下查询(注意:数组下标从 0 开始)
在onload生命周期函数里输入
this.getFiles()
然后再在Page对象里添加getFiles()方法,获取该用户的数据
getFiles(){ const _id= this.data.userData.data[0]._id db.collection("clouddisk").doc(_id).get() .then(res => { console.log('用户数据',res.data) }) .catch(err => { console.error(err) }) }
要实际开发一个具体的功能,一定要先思考这个功能的页面交互是怎样的,而页面交互的背后都只不过是简单的数据,但正是这些简单的数据经过页面交互处理之后却“蒙蔽”了用户的双眼,让用户觉得复杂,觉得这个功能真实存在。
我们可以对对象、对象中的元素、数组、数组中的元素进行匹配查询,甚至还可以对数组和对象相互嵌套的字段进行匹配查询/更新
// 方式一db.collection('todos').where({ style: { color: 'red' }}).get() // 方式二db.collection('todos').where({ 'style.color': 'red'}).get()
匹配并更新数组中的元素
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV})const db = cloud.database()const MAX_LIMIT = 100exports.main = async (event, context) => { // 先取出集合记录总数 const countResult = await db.collection('china').count() const total = countResult.total // 计算需分几次取 const batchTimes = Math.ceil(total / 100) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { const promise = db.collection('china').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => { return { data: acc.data.concat(cur.data), errMsg: acc.errMsg, } })}
技术文档:wx.openDocument()、wx.cloud.downloadFile
使用云开发来下载云存储里面的文件,就不会有域名校验备案的问题
previewFile(){ wx.cloud.downloadFile({ fileID: 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/技术工坊预备手册.pdf' }).then(res => { const filePath = res.tempFilePath wx.openDocument({ filePath: filePath }) }).catch(error => { console.log(error) }) }
技术文档:deleteFile
可以根据文件 ID 下载文件,用户仅可下载其有访问权限的文件:
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV}) exports.main = async (event, context) => { const fileIDs = ['xxx', 'xxx'] const result = await cloud.deleteFile({ fileList: fileIDs, }) return result.fileList}
return await db.collection("clouddisk").doc("_id").update({ data:{ "folders.0.files.1": _.remove() } })
技术文档:getTempFileURL
技术文档:downloadFile
云函数的运行环境是 Node.js,我们可以在云函数中使用Nodejs内置模块以及使用 npm 安装第三方依赖来帮助我们更快的开发。借助于一些优秀的开源项目,避免了我们重复造轮子,相比于小程序端,能够大大扩展云函数的使用
由于云函数与Nodejs息息相关,需要我们对云函数与Node的模块以及Nodejs的一些基本知识有一些基本的了解。下面只介绍一些基础的概念,如果你想详细深入了解,建议去翻阅一下Nodejs的官方技术文档:
技术文档:Nodejs API 中文技术文档
在前面我们已经接触过Nodejs的fs模块、path模块,这些我们称之为Nodejs的内置模块,内置模块不需要我们使用npm install下载,就可以直接使用require引入:
const fs = require('fs')const path = require('path')
Nodejs的常用内置模块以及功能如下所示,这些模块都是可以在云函数里直接使用的:
在云函数中使用HTTP请求访问第三方服务可以不受域名限制,即不需要像小程序端一样,要将域名添加到request合法域名里;也不受http和https的限制,没有域名只有IP都是可以的,所以云函数可以应用的场景非常多,即能方便的调用第三方服务,也能够充当一个功能复杂的完整应用的后端。不过需要注意的是,云函数是部署在云端,有些局域网等终端通信的业务只能在小程序里进行。
module、exports、require
require用于引入模块、 JSON、或本地文件。 可以从 node_modules 引入模块,可以使用相对路径(例如 ./、)引入本地模块或 JSON 文件,路径会根据 __dirname 定义的目录名或当前工作目录进行处理。
node模块化遵循的是commonjs规范,CommonJs定义的模块分为: 模块标识(module)、模块导出(exports) 、模块引用(require)。
在node中,一个文件即一个模块,使用exports和require来进行处理。
exports表示该模块运行时生成的导出对象。如果按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js、 .json 或 .node 拓展名再加载。 .js 文件会被解析为 JavaScript 文本文件, .json 文件会被解析为 JSON 文本文件。 .node 文件会被解析为通过 process.dlopen() 加载的编译后的插件模块。以 '/' 为前缀的模块是文件的绝对路径。 例如, require('/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。以 './' 为前缀的模块是相对于调用 require() 的文件的。 也就是说, circle.js 必须和 foo.js 在同一目录下以便于 require('./circle') 找到它。
module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。
// 引入本地模块:const myLocalModule = require('./path/myLocalModule'); // 引入 JSON 文件:const jsonData = require('./path/filename.json'); // 引入 node_modules 模块或 Node.js 内置模块:const crypto = require('crypto');
tcb-admin-node、protobuf、jstslib
Nodejs有 npm官网地址
Nodejs库推荐:awesome Nodejs
当没有以 '/'、 './' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录,比如wx-server-sdk就加载自node_modules文件夹:
const cloud = require('wx-server-sdk')
Lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库,通过降低 array、number、objects、string 等数据类型的使用难度从而让 JavaScript 变得更简单。Lodash 的模块化方法非常适用于:遍历 array、object 和 string;对值进行操作和检测;创建符合功能的函数。
技术文档:Lodash官方文档、Lodash中文文档
使用开发者工具新建一个云函数,比如lodash,然后在package.json增加lodash最新版latest的依赖:
"dependencies": { "lodash": "latest" }
在index.js里的代码修改为如下,这里使用到了lodash的chunk方法来分割数组:
const cloud = require('wx-server-sdk')var _ = require('lodash');cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, })exports.main = async (event, context) => { //将数组拆分为长度为2的数组 const arr= _.chunk(['a', 'b', 'c', 'd'], 2); return arr}
右键lodash云函数目录,选择“在终端中打开”,npm install 安装模块之后右键部署并上传所有文件。我们就可以通过多种方式来调用它(前面已详细介绍)即可获得结果。Lodash作为工具,非常好用且实用,它的源码也非常值得学习,更多相关内容则需要大家去Github和官方技术文档里深入了解。
在awesome Nodejs页面我们了解到还有Ramba、immutable、Mout等类似工具库,这些都非常推荐。借助于Github的awesome清单,我们就能一手掌握最酷炫好用的开源项目,避免了自己去收集收藏。
开发小程序时经常需要格式化时间、处理相对时间、日历时间以及时间的多语言问题,这个时候就可以使用比较流行的momentjs了。
技术文档:moment官方文档、moment中文文档
使用开发者工具新建一个云函数,比如moment,然后在package.json增加moment最新版latest的依赖:
"dependencies": { "moment": "latest" }
在index.js里的代码修改为如下,我们将moment区域设置为中国,将时间格式化为 十二月 23日 2019, 4:13:29 下午的样式以及相对时间多少分钟前:
const cloud = require('wx-server-sdk')const moment = require("moment");cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, })exports.main = async (event, context) => { moment.locale('zh-cn'); time1 = moment().format('MMMM Do YYYY, h:mm:ss a'); time2 = moment().startOf('hour').fromNow(); return { time1,time2}}
不过云函数中的时区为 UTC+0,不是 UTC+8,格式化得到的时间和在国内的时间是有8个小时的时间差的,我们可以给小时数+8,也可以修改时区。云函数修改时区我们可以使用timezone依赖(和moment是同一个开源作者)。
技术文档:timezone技术文档
在package.json增加moment-timezone最新版latest的依赖,然后修改上面相应的代码即可,使用起来非常方便:
const moment = require('moment-timezone');time1 = moment().tz('Asia/Shanghai').format('MMMM Do YYYY, h:mm:ss a');
有时我们希望能够获取到服务器的公网IP,比如用于IP地址的白名单,或者想根据IP查询到服务器所在的地址,ipify就是一个免费好用的依赖,通过它我们也可以获取到云函数所在服务器的公网IP。
技术文档:ipify Github地址
使用开发者工具新建一个getip的云函数,然后输入以下代码,并在package.json的”dependencies”里新增 "ipify":"latest" ,即最新版的ipify依赖:
const cloud = require('wx-server-sdk') const ipify = require('ipify'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { return await ipify({ useIPv6: false }) }
然后右键getip云函数根目录,选择在终端中打开,输入npm install安装依赖,之后上传并部署所有文件。我们可以在小程序端调用这个云函数,就可以得到云函数服务器的公网IP,这个IP是随机而有限的几个,反复调用getip,就能够穷举所有云函数所在服务器的ip了。
可能你会在使用云函数连接数据库或者用云函数来建微信公众号的后台时需要用到IP白名单,我们可以把这些ip都添加到白名单里面,这样云函数就可以做很多事情啦。
const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/1576500614167-520.png' const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent return buffer.toString('base64') }
getServerImg(){ wx.cloud.callFunction({ name: 'downloadimg', success: res => { console.log("云函数返回的数据",res.result) this.setData({ img:res.result }) }, fail: err => { console.error('云函数调用失败:', err) } }) }
<image width="400px" height="200px" src="data:image/jpeg;base64,{{img}}"></image>
Buffer String
Buffer JSON
sharp是一个高速图像处理库,可以很方便的实现图片编辑操作,如裁剪、格式转换、旋转变换、滤镜添加、图片合成(如添加水印)、图片拼接等,支持JPEG, PNG, WebP, TIFF, GIF 和 SVG格式。在云函数端使用sharp来处理图片,而云存储则可以作为服务端和小程序端来传递图片的桥梁。
技术文档:sharp官方技术文档
使用开发者工具新建一个
const cloud = require('wx-server-sdk')const fs = require('fs')const path = require('path')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,})const sharp = require('sharp');exports.main = async (event, context) => { //这里换成自己的fileID,也可以在小程序端上传文件之后,把fileID传进来event.fileID const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/1572315793628-366.png' //要用云函数处理图片,需要先下载图片,返回的图片类型为Buffer const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent //sharp对图片进行处理之后,保存为output.png,也可以直接保存为Buffer await sharp(buffer).rotate().resize(200).toFile('output.png') // 云函数读取模块目录下的图片,并上传到云存储 const fileStream = await fs.createReadStream(path.join(__dirname, 'output.png')) return await cloud.uploadFile({ cloudPath: 'sharpdemo.jpg', fileContent: fileStream, }) }
也可以让sharp不需要先toFile转成图片,而是直接转成Buffer,这样就可以直接作为参数传给fileContent上传到云存储,如:
const buffer2 = await sharp(buffer).rotate().resize(200).toBuffer(); return await cloud.uploadFile({ cloudPath: 'sharpdemo2.jpg', fileContent: buffer2, })
技术文档:Sequelize
const sequelize = new Sequelize('database', 'username', 'password', { host: 'localhost', //数据库地址,默认本机 port:'3306', dialect: 'mysql', pool: { //连接池设置 max: 5, //最大连接数 min: 0, //最小连接数 idle: 10000 }, });
无论是MySQL,还是PostgreSQL、Redis、MongoDB等其他数据库,只要我们在
默认情况下,云开发的函数部署在公共网络中,只可以访问公网。如果开发者需要访问腾讯云的 Redis、TencentDB、CVM、Kafka 等资源,需要建立私有网络来确保数据安全及连接安全。
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, })const Redis = require('ioredis')const redis = new Redis({ port: 6379, host: '10.168.0.15', family: 4, password: 'CloudBase2018', db: 0,}) exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const cacheKey = wxContext.OPENID const cache = await redis.get(cacheKey) if (!cache) { const result = await new Promise((resolve, reject) => { setTimeout(() => resolve(Math.random()), 2000) }) redis.set(cacheKey, result, 'EX', 3600) return result } else { return cache } }
技术文档:node-qrcode Github地址
技术文档:Nodemailer Github地址、Nodemailer官方文档
使用开发者工具创建一个云函数,比如nodemail,然后在package.json增加nodemailer最新版latest的依赖:
"dependencies": { "nodemailer": "latest" }
发送邮件服务器:smtp.qq.com,使用SSL,端口号465或587
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,})exports.main = async (event, context) => { const nodemailer = require("nodemailer"); let transporter = nodemailer.createTransport({ host: "smtp.qq.com", //SMTP服务器地址 port: 465, //端口号,通常为465,587,25,不同的邮件客户端端口号可能不一样 secure: true, //如果端口是465,就为true;如果是587、25,就填false auth: { user: "344169902@qq.com", //你的邮箱账号 pass: "你的QQ邮箱授权码" //邮箱密码,QQ的需要是独立授权码 } }); let message = { from: '来自李东bbsky <344169902@qq.com>', //你的发件邮箱 to: '你要发送给谁', //你要发给谁 // cc:'', 支持cc 抄送 // bcc: '', 支持bcc 密送 subject: '欢迎大家参与云开发技术训练营活动', //支持text纯文字,html代码 text: '欢迎大家', html: '<p><b>你好:</b><img src="https://hackwork-1251009918.cos.ap-shanghai.myqcloud.com/handbook/html5/weapp.jpg" rel="external nofollow" /></p>' + '<p>欢迎欢迎<br/></p>', attachments: [ //支持多种附件形式,可以是String, Buffer或Stream { filename: 'image.png', content: Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD/' + '//+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4U' + 'g9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', 'base64' ), }, ] }; let res = await transporter.sendMail(message); return res;}
Excel是存储数据比较常见的格式,那如何让云函数拥有读写Excel文件的能力呢?我们可以在Github上搜索关键词“Node Excel”,去筛选Star比较多,条件比较契合的。
Github地址:node-xlsx
使用开发者工具新建一个云函数,在package.json里添加latest最新版的node-xlsx:
"dependencies": { "wx-server-sdk": "latest", "node-xlsx": "latest"}
读取云存储的Excel文件
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,}) const xlsx = require('node-xlsx');const db = cloud.database() exports.main = async (event, context) => { const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/china.csv' const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] var sheets = xlsx.parse(buffer); sheets.forEach(function (sheet) { for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; if (rowId > 0 && row) { const promise = db.collection('chinaexcel') .add({ data: { city: row[0], province: row[1], city_area: row[2], builtup_area: row[3], reg_pop: row[4], resident_pop: row[5], gdp: row[6] } }) tasks.push(promise) } } }); let result = await Promise.all(tasks).then(res => { return res }).catch(function (err) { return err }) return result}
将数据库里的数据保存为CSV
技术文档:json2CSV
got、superagent、request、axios、request-promise
尽管云函数的Nodejs版本比较低(目前为8.9),但绝大多数模块我们都可以使用Nodejs 12或13的环境来测试,不过有时候也要留意有些模块不支持8.9,比如got 10.0.1以上的版本。
node中,http模块也可作为客户端使用(发送请求),第三方模块request对其使用方法进行了封装,操作更方便!所以来介绍一下request模块
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,})const rp = require('request-promise')exports.main = async (event, context) => { const options = { url: 'https://news-at.zhihu.com/api/4/news/latest', json: true, method: 'GET', }; return await rp(options)}
request('https://www.jmjc.tech/public/home/img/flower.png').pipe(fs.createWriteStream('./flower.png')) // 下载文件到本地
crypto模块是nodejs的核心模块之一,它提供了安全相关的功能,包含对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。由于crypto模块是内置模块,我们引入它是无需下载,就可以直接引入。
使用开发者工具新建一个云函数,比如crypto,在index.js里输入以下代码,我们来了解一下crypto支持哪些加密算法,并以MD5加密为例:
const cloud = require('wx-server-sdk')cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV,})const crypto = require('crypto');exports.main = async (event, context) => { const hashes = crypto.getHashes(); //获取crypto支持的加密算法种类列表 //md5 加密 CloudBase2020 返回十六进制 var md5 = crypto.createHash('md5'); var message = 'CloudBase2020'; var digest = md5.update(message, 'utf8').digest('hex'); return { "crypto支持的加密算法种类":hashes, "md5加密返回的十六进制":digest };}
将云函数部署之后调用从返回的结果我们可以了解到,云函数crypto模块支持46种加密算法。
“qcloudsms_js”: “^0.1.1”
const cloud = require('wx-server-sdk')const QcloudSms = require("qcloudsms_js")const appid = 1400284950 // 替换成您申请的云短信 AppID 以及 AppKeyconst appkey = "a33b602345f5bb866f040303ac6f98ca"const templateId = 472078 // 替换成您所申请模板 IDconst smsSign = "统计小助理" // 替换成您所申请的签名 cloud.init() // 云函数入口函数exports.main = async (event, context) => new Promise((resolve, reject) => { /*单发短信示例为完整示例,更多功能请直接替换以下代码*/ var qcloudsms = QcloudSms(appid, appkey); var ssender = qcloudsms.SmsSingleSender(); var params = ["1234", "15"]; // 获取发送短信的手机号码 var mobile = event.mobile // 获取手机号国家/地区码 var nationcode = event.nationcode ssender.sendWithParam(nationcode, mobile, templateId, params, smsSign, "", "", (err, res, resData) => { /*设置请求回调处理, 这里只是演示,您需要自定义相应处理逻辑*/ if (err) { console.log("err: ", err); reject({ err }) } else { resolve({ res: res.req, resData }) } } ); })
使用开发者工具
wx.cloud.callFunction({ name: 'sendphone', data: { // mobile: '13217922526', mobile: '18565678773', nationcode: '86' }, success: res => { console.log('[云函数] [sendsms] 调用成功') console.log(res) }, fail: err => { console.error('[云函数] [sendsms] 调用失败', err) }})