微信小程序(二)--逻辑层与界面层
一.逻辑层与界面层分离
小程序开发框架将我们需要完成的编码,划分成了两种类型的编码:逻辑编码(由JavaScript完成,业务数据供给界面事件处理),界面编码(页面结构WXML,页面样式WXSS,展示逻辑层的数据).
二.逻辑层的JavaScript
小程序开发中使用到的JavaScript同常规网页开发所使用的JavaScript有所差异,在小程序的开发之中,有很多的.js文件,他们都属于页面的逻辑层,我们知道程序一旦启动就会去执行app.js文件中的代码,我们可以使用console.log()方法来验证.
小程序不是运行在浏览器中,所以没有BOM和DOM对象,但是在小程序只用存在一些特有的全局成员:
1 //app.js
2
3 console.log("==============================");
4 //自定义逻辑代码
5 //1.小程序不是运行在浏览器中,所以没有BOM和DOM对象
6 console.log(window); //在小程序中无法获取到BOM中定义的成员
7 console.log(document); //无法获取到DOM中定义的成员
8 //2.小程序的JS有一些额外的成员(全局)
9 //App 方法:用于定义应用程序实例对象
10 //Page方法:用于定义页面对象
11 //getApp方法:用于获取全局应用程序对象,每个小程序都由唯一的应用程序实例
12 //getCurrentPages:用于获取当前页面的调用栈
13 //wx对象: 用来提供核心api
14 console.log(wx);
15 //3.小程序的JS是支持CommonJS规范(用于约定这些文件与文件之间是如何组织,相互之间协同的)的
16 //通过module 和 require来传递通用方法
17 const foo = require("./utils/foo.js");
18 foo.say("world");
19 console.log("==============================");
20 var obj = {
21 onLaunch: function() {
22 // 展示本地存储能力
23 var logs = wx.getStorageSync('logs') || []
24 logs.unshift(Date.now())
25 wx.setStorageSync('logs', logs)
26
27 // 登录
28 wx.login({
29 success: res => {
30 // 发送 res.code 到后台换取 openId, sessionKey, unionId
31 }
32 })
33 // 获取用户信息
34 wx.getSetting({
35 success: res => {
36 if (res.authSetting['scope.userInfo']) {
37 // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
38 wx.getUserInfo({
39 success: res => {
40 // 可以将 res 发送给后台解码出 unionId
41 this.globalData.userInfo = res.userInfo
42
43 // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
44 // 所以此处加入 callback 以防止这种情况
45 if (this.userInfoReadyCallback) {
46 this.userInfoReadyCallback(res)
47 }
48 }
49 })
50 }
51 }
52 })
53 },
54 globalData: {
55 userInfo: null
56 },
57 //自定义成员方法
58 foo: function() {
59 console.log("自定义成员方法");
60 }
61 };
62 //调用了一个APP方法(全局方法)
63 App(obj);
64 //调用App方法的作用是用来创建应用程序实例对象
65 //定义应用程序的生命周期事件
1 function say(msg){
2 console.log("hello " + msg);
3 }
4
5 // 导出say()方法
6 module.exports = {
7 say: say
8 }
9 //在标准的COmmonJS规范中还支持exports.say 来导出,但是在小程序当中是不支持的,只支持module.exports
1 //index.js
2 //获取应用实例
3 //完成页面的逻辑,包括功能的实现
4 const app = getApp()
5
6 Page({
7 data: {
8 motto: 'Hello World',
9 userInfo: {},
10 hasUserInfo: false,
11 canIUse: wx.canIUse('button.open-type.getUserInfo')
12 },
13 //事件处理函数
14 bindViewTap: function() {
15 wx.navigateTo({
16 url: '../logs/logs'
17 })
18 },
19 onLoad: function () {
20 if (app.globalData.userInfo) {
21 this.setData({
22 userInfo: app.globalData.userInfo,
23 hasUserInfo: true
24 })
25 } else if (this.data.canIUse){
26 // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
27 // 所以此处加入 callback 以防止这种情况
28 app.userInfoReadyCallback = res => {
29 this.setData({
30 userInfo: res.userInfo,
31 hasUserInfo: true
32 })
33 }
34 } else {
35 // 在没有 open-type=getUserInfo 版本的兼容处理
36 wx.getUserInfo({
37 success: res => {
38 app.globalData.userInfo = res.userInfo
39 this.setData({
40 userInfo: res.userInfo,
41 hasUserInfo: true
42 })
43 }
44 })
45 }
46 },
47 getUserInfo: function(e) {
48 console.log(e)
49 app.globalData.userInfo = e.detail.userInfo
50 this.setData({
51 userInfo: e.detail.userInfo,
52 hasUserInfo: true
53 })
54 },
55 onShow: function () {
56 //在每次该界面唤醒时,获取当前页面的调用栈:
57 console.log(getCurrentPages());
58 console.log("当前调用栈中的页面数量: " + getCurrentPages());
59 }
60
61 })
1 // pages/demo/demo.js
2
3 //获取当前小程序对象实例
4 var app = getApp();
5 Page({
6
7 /**
8 * 页面的初始数据
9 */
10 data: {
11
12 },
13
14 /**
15 * 生命周期函数--监听页面加载
16 */
17 onLoad: function (options) {
18 app.foo();
19
20 },
21
22 /**
23 * 生命周期函数--监听页面初次渲染完成
24 */
25 onReady: function () {
26
27 },
28
29 /**
30 * 生命周期函数--监听页面显示
31 */
32 onShow: function () {
33 //在每次该界面唤醒时,获取当前页面的调用栈:
34 console.log(getCurrentPages());
35 console.log("当前调用栈中的页面数量: " + getCurrentPages());
36 console.log(getCurrentPages()[getCurrentPages().length - 1] == this);
37 },
38
39 /**
40 * 生命周期函数--监听页面隐藏
41 */
42 onHide: function () {
43
44 },
45
46 /**
47 * 生命周期函数--监听页面卸载
48 */
49 onUnload: function () {
50
51 },
52
53 /**
54 * 页面相关事件处理函数--监听用户下拉动作
55 */
56 onPullDownRefresh: function () {
57
58 },
59
60 /**
61 * 页面上拉触底事件的处理函数
62 */
63 onReachBottom: function () {
64
65 },
66
67 /**
68 * 用户点击右上角分享
69 */
70 onShareAppMessage: function () {
71
72 }
73 })
三.界面层--数据绑定
在程序的开发过程中,界面层提供一种模板机制,可以借助一种特殊的语法来完成数据的绑定,将动态的数据绑定到模板当中,最近将嵌入过后的数据显示到界面中,小程序中使用的是Mustache 语法(双大括号)语法:
1 <!--pages/demo/demo.wxml-->
2 <!-- 框架最大的特点就是让我们开发者必须按照特定的方法来编写代码 -->
3 <!-- 数据绑定
4 1.数据在哪 (在当前页面对象Page的data属性中)
5 2.绑定到哪里 (想绑定到哪就在哪里用Mustache 语法进行输出)
6 在小程序中规定所有的数据绑定对在相应js文件的Page对象中的data属性中 -->
7 <view class="container">
8 <text>{{ message }}</text>
9 <text>{{ person.name }} : {{ person.age }}</text>
10 </view>
1 // pages/demo/demo.js
2
3 Page({
4 //为页面提供数据,界面和逻辑之间的桥梁
5 data:{
6 message:"hello world",
7 person:{
8 name: "张三",
9 age:25
10 }
11 }
12 })
--数据绑定语法补充,除了在页面标签的内部我们会使用Mustache语法进行输出,在标签的内部,我们也可以使用Mustache语法:
1 <!--pages/demo/demo.wxml-->
2 <!-- 框架最大的特点就是让我们开发者必须按照特定的方法来编写代码 -->
3 <!-- 数据绑定
4 1.数据在哪 (在当前页面对象Page的data属性中)
5 2.绑定到哪里 (想绑定到哪就在哪里用Mustache 语法进行输出)
6 在小程序中规定所有的数据绑定对在相应js文件的Page对象中的data属性中 -->
7 <view class="container">
8 <text>{{ message }}</text>
9 <text>{{ person.name }} : {{ person.age }}</text>
10 <!-- Mustache语法可以用在:
11 1. innerHTML 元素内存
12 2. 元素的属性上
13 3. 不能用在标签名和属性名上面(只能用在vlaue上而无法使用在key上) -->
14 <view class="world {{ viewClassName }}"></view>
15 <!-- 可以直接使用字面量和简单的逻辑运算符 -->
16 <text>{{ 'hello' }}</text>
17 <text>{{ 111 }}</text>
18 <text>{{ 111 + 222 }}</text>
19 <text>{{ 111 < 222 ? 'yes' : 'no' }}</text>
20 <text>{{ true }}</text>
21 <!-- 如我我们默认不添加checked属性,那么默认为false,但是如我们添加了checked属性,则不论我们指定的是什么,都将显示被勾选;
22 在JS中将所有有值的字符串都认为是true ,因此我们想要表达一个true 我们可以使用 Mustache 语法来解析字符串
23 -->
24 <checkbox checked="false">复选框1</checkbox>
25 <checkbox checked="{{ false }}">复选框2</checkbox>
26 </view>
四.列表渲染
在逻辑层中,很多的情况下都会是一个数组类型的数据(列表数据),那么此时在我们的界面层中就需要使用到列表渲染:
1 <!-- 如我我们默认不添加checked属性,那么默认为false,但是如我们添加了checked属性,则不论我们指定的是什么,都将显示被勾选;
2 在JS中将所有有值的字符串都认为是true ,因此我们想要表达一个true 我们可以使用 Mustache 语法来解析字符串
3 -->
4 <checkbox checked="false">复选框1</checkbox>
5 <checkbox checked="{{ false }}">复选框2</checkbox>
6 <!-- 列表数组渲染(列表数据绑定) -->
7 <view>
8 <!-- <view class="task_1">
9 <checkbox checked="{{ true }}"></checkbox>
10 <text>HTML</text>
11 </view>
12 <view class="task_2">
13 <checkbox checked="{{ false }}"></checkbox>
14 <text>JS</text>
15 </view>
16 <view class="task_3">
17 <checkbox checked="{{ false }}"></checkbox>
18 <text>CSS</text>
19 </view> -->
20 <!-- 循环遍历输出列表:
21 1.明确页面结构中的循环体(在这里就是 最外层的view),删除多余的内容,只留下一个循环体,在剩下的一个上加上wx:for的属性
22 属性值为我们需要进行遍历的数据源,数据源是一个数组
23 2.在循环体的内部(标签)使用item代表当前被遍历的元素
24 3.如果在我们的Page对象的data中存在一个全局的item属性,此时我们无法在内部获取到全局的item属性,如果想要获取到我们的
25 全局item属性,那么需要使用wx: for-item 属性在自定义循环体的指向
26 4.如果我们想要在循环体中拿到当前的index,那么默认我们可以直接使用index变量({{ index }})来获取我们的索引值,如果和
27 我们的全局变量中途,那么则可以使用wx:for-index来指定索引别名
28 5.在官方之中.默认是支持循环嵌套的
29 -->
30 <view wx:for="{{ todos }}" wx:for-item="aaa" wx:for-index="bbb">
31 <text>{{ bbb + 1 }}:</text>
32 <checkbox checked="{{ aaa.complete }}"></checkbox>
33 <text>{{ item.name }} {{ aaa.name }}</text>
34 </view>
35 <!-- 打印 9*9 乘法表 -->
36 <view>
37 <view wx:for="{{ [1,2,3,4,5,6,7,8,9] }}" wx:for-item="i">
38 <view wx:for="{{ [1,2,3,4,5,6,7,8,9] }}" wx:for-item="j">
39 <!-- <view wx:if="{{ i <= j }}"> -->
40 <text>{{ i }} * {{ j }} = {{ i * j }}</text>
41 <!-- </view> -->
42 </view>
43 </view>
44 </view>
45 </view>
五.事件处理
1 <!-- 绑定事件
2 1.触碰事件(bindTap):相当于click点击事件,在当前控件下触碰和送开,移除则不会触发-->
3 <button bindtap="buttonTapHandle">点击</button>
1 buttonTapHandle: function(e){
2 console.log("点击按钮打印此条.");
3 // console.dir()将一个对象以树状的形式打印到控制台,便于我们调试复杂对象
4 console.dir(e);
5 }
六.事件冒泡
1 <!-- 事件冒泡
2 1.我们需要知道手机点击的工作原理是在手机屏幕点击的点,垂直以里发射一条射线,这条射线会先碰到蓝色的块,但是蓝色的块被包裹来红色的块里,所以会同时触发两个事件(先触发内部事件,再触发外部事件).
3 2.在小程序之中阻止冒泡:绑定事件除了使用bindtap外,还能够使用catchtap,catchtap具有阻止冒泡的功能,并且绑定事件-->
4 <view style="width:200px;height:200px;background-color:red" bindtap="outerHandler">
5 <view style="width:100px;height:100px;background-color:blue" catchtap="innerHandler"> </view>
6 </view>
1 innerHandler: function(){
2 console.log("触发内部事件");
3 },
4 outerHandler: function(){
5 console.log("触发外部事件");
6 }
七.事件传参
1 <!-- 在网页的开发之中,经常会有多个组件去调用相同方法的情况,此时如果我们需要知道当前触发这个方法的是哪一个组件,通常我们呢可以通过this来获取当前对象,当然也可以通过传递一个参数来完成对于当前对象的判断.
2 但是在小程序之中,我们在指定事件属性的时候,只能去指定事件的名字,而无法像网页那样去指定方法的参数,因此我们在方法中定义的参数e所拿到的就是当前的事件对象,我们可以发现在target属性下,存在一个dataset属性,其记录了data属性信息,例如data-id,data-name这些自定义的属性,都是根据Dataset这个属性来获取的.
3 在小程序中事件处理函数中的this,表示的还是这个Page页面对象,如果我们在定义data属性的时候,使用了多个中横线,那么将自动将该中横线忽略,将后方的变量名转变为驼峰式命名法来展现
4 总结:
5 1.事件的基本使用: bind[xxx] catch[xxx]
6 2.小程序中事件冒泡处理的机制: 使用catch[xxx]
7 3.如果需要给事件处理函数指定参数,只能通过dataset的这种方式-->
8 <button bindtap="handleWithParm" data-id="123" data-name="hello" data-hello-world="hello world">参数事件</button>
1 handleWithParm(e){
2 console.log(e); //获取当前的点击元素
3 console.log(e.target.dataset); //获取元素上所有data-[xxx]属性的集合
4 }
八.单向数据流
1 <!-- 单向数据流
2 在小程序开发之中,数据一定是从逻辑层获取而来的,而后将数据绑定到界面层,界面上则可以通过一些输入将数据同步回来.从而形成一个数据扭转的回环.
3 1.bindinput事件:用户输入时触发-->
4 <view>
5 <input style="border:1px solid #000;" bindinput="inputHandler"></input>
6 <text>{{ inputMessage }}</text>
7 </view>
1 inputHandler: function(e) {
2 //e.detail.value 获取到当前输入的值
3 console.log(e.detail.value);
4 //小程序中的数据绑定,本质上是一次绑定,在绑定完成之后,并不会去监视数据的变化.如果想要得到此次的变化,那么就需要使用setData的方法进行改变
5 //setData是用来改变data数据的,与直接赋值区别在于setData可以通知界面作出变化,而直接赋值的话没有办法实现这一点(早期的JS无法实现)
6 this.setData({
7 inputMessage: "您输入的内容: " + e.detail.value
8 })
9 console.log(this.data.message);
10 }
九.登录页面案例
1 <!--pages/login/login.wxml-->
2 <view class="container">
3 <view>
4 <input class="username" placeholder="请输入用户名" style="border:1px solid #000"
5 value="{{ username }}" bindinput="usernameChangeHandler"/>
6 <input class="password" type="password" placeholder="请输入密码" style="border:1px solid #000"
7 value="{{ password }}" bindinput="passwordChangeHandler"/>
8 </view>
9 <view>
10 <button type="primary" bindtap="buttonLogin">登录</button>
11 <button type="default" bindtap="buttonRegister">注册 </button>
12 </view>
13 </view>
1 // pages/login/login.js
2 /* 实现登录界面
3 1. 设计数据的结构(data属性)
4 2. 将数据绑定到指定的元素上
5 */
6 Page({
7 data: {
8 username: "",
9 password: ""
10 },
11 //用于处理登录按钮点击的事件
12 buttonLogin: function() {
13 console.log("执行登录事件");
14 },
15 //用于处理注册按钮点击的事件
16 buttonRegister: function() {
17 //1. 首先需要知道用户输入了什么
18 console.log(this.data.username + " " + this.data.password);
19 //2. 根据用户输入的值去判断
20 //3. 根据判断的结果做出响应
21 console.log("执行注册事件");
22 },
23 //用户输入用户名
24 usernameChangeHandler: function(e) {
25 this.setData({
26 username: e.detail.value
27 })
28 },
29 //用户输入密码
30 passwordChangeHandler: function(e) {
31 this.setData({
32 password: e.detail.value
33 })
34 }
35 })
十.抽象共同的事件处理函数
如我在我们的代码之中为每一个input标签都增加一个事件绑定函数,那么在我们的代码之中,将会存在很多的重复代码,为了避免这样的的情况,我们可以抽象共同的事件处理函数:
1 <!--pages/login/login.wxml-->
2 <view class="container">
3 <view>
4 <input class="username" placeholder="请输入用户名" style="border:1px solid #000"
5 value="{{ username }}" bindinput="inputChangeHandler" data-name="username"/>
6 <input class="password" type="password" placeholder="请输入密码" style="border:1px solid #000"
7 value="{{ password }}" bindinput="inputChangeHandler" data-name="password"/>
8 </view>
9 <view>
10 <button type="primary" bindtap="buttonLogin">登录</button>
11 <button type="default" bindtap="buttonRegister">注册 </button>
12 </view>
13 </view>
1 // pages/login/login.js
2 /* 实现登录界面
3 1. 设计数据的结构(data属性)
4 2. 将数据绑定到指定的元素上
5 */
6 Page({
7 data: {
8 username: "",
9 password: ""
10 },
11 //用于处理登录按钮点击的事件
12 buttonLogin: function() {
13 console.log(this.data.username + " " + this.data.password);
14 console.log("执行登录事件");
15 },
16 //用于处理注册按钮点击的事件
17 buttonRegister: function() {
18 //1. 首先需要知道用户输入了什么
19 console.log(this.data.username + " " + this.data.password);
20 //2. 根据用户输入的值去判断
21 //3. 根据判断的结果做出响应
22 console.log("执行注册事件");
23 },
24 //用户输入用户名
25 // usernameChangeHandler: function(e) {
26 // this.setData({
27 // username: e.detail.value
28 // })
29 // },
30 //用户输入密码
31 // passwordChangeHandler: function(e) {
32 // this.setData({
33 // password: e.detail.value
34 // }),
35 // console.log(e);
36 // }
37 //抽象上述两个方法,完成事件
38 inputChangeHandler: function(e) {
39 var key = e.target.dataset.name;
40 var value = e.detail.value;
41 var changed = {};
42 changed[key] = value;
43 this.setData(changed);
44 }
45 })
--除此之外,我们还可以使用表单的方式来进行数据的接收,这样可以免除这一些列复杂的操作:
1 <view>
2 <input class="username" placeholder="请输入用户名" style="border:1px solid #000"
3 value="{{ username }}" name="username" />
4 <input class="password" type="password" placeholder="请输入密码" style="border:1px solid #000"
5 value="{{ password }}" name="password" />
6 </view>
7 <view>
8 <button type="primary" form-type="submit">登录</button>
9 <button type="default" form-type="submit" >注册 </button>
10 </view>
1 //使用表单提交的方式来完成数据接收
2 loginSubmit: function(e){
3 console.log(e.detail.value);
4 }
十一.条件渲染
1 <!-- 条件渲染 -->
2 <view>
3 <button bindtap="showButtonHandler">点击切换面板展示</button>
4 <text wx:if="{{ !show }}">
5 面板中显示的内容
6 面板中显示的内容
7 面板中显示的内容
8 </text>
9 <!-- 如果想要将多个模块同时进行判断,可以使用block标签,block标签没有其他任何的作用,只是作为一个判断属性的载体,
10 在页面渲染过程中并没有实际的意义 -->
11 <block wx:if="{{ !show }}">
12 <text style="display:block;">面板中显示的内容1</text>
13 <text style="display:block;">面板中显示的内容2</text>
14 <text style="display:block;">面板中显示的内容3</text>
15 </block>
16 <!-- hidden只是默认需显示,但是依旧会被渲染,使用if的则不会被渲染,因此wx:if是惰性的,当条件为假的时候,是无渲染的
17 1.hidden:标签还是会工作,只是不显示在界面上
18 2.wx:if: 惰性,只有显示的时候才会进行局部的渲染 -->
19 <view hidden="{{ !show }}">
20 <text style="display:block;">面板中显示的内容4</text>
21 <text style="display:block;">面板中显示的内容5</text>
22 <text style="display:block;">面板中显示的内容6</text>
23 </view>
24 </view>
十二.界面层(WXSS vs CSS)
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。即将所有设备都使用rpx为750设置为撑满屏幕宽度.
1 .demo {
2 width: 750rpx;
3 height: 200px;
4 background-color: red;
5 }