一)WXS

小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。

与javascript类似,可以类似于使用<script></script>标签包裹脚本代码。

使用步骤:

1)在wxml中定义wxs模版 <wxs module="msg" > xxxxx </wxs>

2)使用:<text> {{ msg.xxx }} </text>

wxml 

<!-- 1.在wxs中定义变量 -->
<wxs module="msg">
  var message1 = '我是在wxs中定义的变量message1';
  var message2 = '我是在wxs中定义的变量message2';

  //module.exports.message1=message1;
  //module.exports.message2=message2;

  <!-- 导出定义的变量 -->
  module.exports = {
    message1:message1, //这里不能简写
    message2:message2
  }
</wxs>

<!-- 2.使用data定义的变量 -->
<view>{{value}}</view>

<!-- 3.使用wxs中定义的变量 -->
<view>{{msg.message1}}</view>
<view>{{msg.message2}}</view>

js

/**
   * 页面的初始数据
   */
  data: {
    value:'我是在data定义的变量',
    time:1545117587000
  },

 注意:

wxs 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。

wxs 与 javascript 是不同的语言,有自己的语法,并不和 javascript 一致。

wxs 的运行环境和其他 javascript 代码是隔离的,wxs 中不能调用其他 javascript 文件中定义的函数,也不能调用小程序提供的API。

wxs 函数不能作为组件的事件回调

由于运行环境的差异,在 iOS 设备上小程序内的 wxs 会比 javascript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。

另外,小程序脚本语言主要是用来增强页面的表达能力,对es6的支持不友好,比如const、let、箭头函数不支持。

编写wxs工具类

步骤:

编写函数;

导入和使用。

新建目录,新建wxs文件,编写函数:

微信小程序开发进阶_小程序

tools.wxs

var message = '价格的格式化';

var formPrice = function(price){
  //1.把字符串转成int
  price = parseFloat(price);
  //2.保留2位小数点
  return price.toFixed(2);
}

module.exports= {
  message:message,  //不支持简写
  formPrice: formPrice
}

usewxs.wxml

<!-- 引用外部wxs定义的工具 -->
<wxs src="../wxs/tools.wxs" module="tools"></wxs>
<view>{{ tools.message }}:{{ tools.formPrice('112.242343') }}</view>

二)自定义组件

这里统一以自定义组件my-view为例,外部使用自定义组件my-view的组件是use-component

1、新建自定义组件

一个自定义组件由 json wxml wxss js 4个文件组成。

编写一个自定义组件,首先需要在json文件中进行自定义组件声明(将component字段设为true将一组文件设为自定义组件) 。

微信小程序开发进阶_生命周期_02

my-view.json

{
  "component": true
}

2、使用自定义组件

使用已注册的自定义组件前,首先要在使用该组件页面的 json 文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径

微信小程序开发进阶_生命周期_03

{
    "usingComponents": {
      "my-view": "../../components/my-view/my-view"
    }
}

这样,在页面的wxml中就可以像使用基础组件一样使用自定义组件。节点名即自定义组件的标签名,节点属性即传递给组件的属性值。

微信小程序开发进阶_xml_04

<my-view></my-view>

3、自定义组件插槽

my-view.wxml

<view>
    <view>我是自定义my-view组件</view>
    
    <!-- 插槽定义 -->
    <slot></slot>
</view>

微信小程序开发进阶_小程序_05

使用插槽,use-component.wxml。

<my-view>
    <text>替换插槽的内容1</text>
    <text>替换插槽的内容2</text>
</my-view>

微信小程序开发进阶_自定义组件_06

    

微信小程序开发进阶_微信小程序_07

多个插槽

my-view.wxml,定义:

<view>
  <view>我是自定义my-view组件</view>

  <!-- 我是组件中的插槽 -->
  <slot name="content"></slot>
  <slot name="footer"></slot>

</view>

my-view.js,声明启动插槽:

options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
},

use-component.wxml,使用插槽:

<my-view>
    <text slot="content">content布局\n<text>
    <text slot="footer">footer布局<text>
</my-view>

4、自定义组件的样式

1)默认样式

my-view.wxss

/* components/my-view/my-view.wxss */
:host{
  /* 这些样式会被继承 */
  color: pink;
  font-size: 50rpx;
  line-height: 50rpx;

  /* 这个不会被继承 */
  background: red;
}

说明:

1、不可继承的:display、margin、border、padding、background、height、min-height、max-height、width、min-width、max-width、overflow、position、left、right、top、bottom、z-index、float、clear、table-layout、vertical-align、page-break-after、page-bread-before和unicode-bidi

2、所有元素可继承:visibility和cursor

3、内联元素可继承:letter-spacing、word-spacing、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction。

2)组件内部样式

也就是class样式,组件对应wxss文件的样式,只对组件wxml内的节点生效。

my-view.wxml

<view class="my-view">
  <view class="title">我是自定义my-view组件</view>

  <!-- 我是组件中的插槽 -->
  <slot name="content">content布局\n</slot>
  <slot name="footer">footer布局</slot>
</view>

my-view.wxss

.my-view{
    background:red;
}

.title{
    color:white;
}

微信小程序开发进阶_微信小程序_08

3)组件外部样式

my-view.js

Component({
    options:{
        multipleSlots: true
    },

    // 1.定义该组件可以接收的外部样式类
    externalClasses: ['my-view-class','my-view-active-class'],
})

my-view.wxml

<view class="my-view">
  <!-- 1.我是自定义组件 -->	
  <view class="title">我是自定义my-view组件</view>

  <!-- 2.我是组件中的插槽 -->
  <slot name="content"></slot>
  <slot name="footer"></slot>

  <!-- 3.我是组件中的样式 -->
  <view class="my-view-class">我使用的是外部传递进来的样式类</view>
  <view class="{{ true ? 'my-view-active-class' : '' }}">我使用的是外部传递进来的样式类</view>
</view>

外部样式类传到自定义组件内部。

use-component.wxml

<!-- 使用自定义组件,这里将 my-view 类传递给my-view-class... -->
<my-view my-view-class="my-view" my-view-active-class="my-view-active">
   <text slot="content">content布局\n</text>
   <text slot="footer">footer布局</text>
</my-view>

use-component.wxss

/* pages/02-use-components/use-components.wxss */
.my-view{
  color: white;
  background: red;
}

.my-view-active{
  color: white;
  font-size: 50rpx;
  background: pink;
}

5、自定义组件接收外部数据

use-component.wxml

<!-- 使用自定义组件 -->
<my-view my-view-class="my-view" my-view-active-class="my-view-active"
  arrs-pramas = "{{ [1,2,3,4] }}"
  str-pramas = "liujun"
  num-pramas = "12"
>
   
   <text slot="content">content布局\n</text>
   <text slot="footer">footer布局</text>
</my-view>

my-view.js

Component({ 
  
  /**
   * 组件的属性列表
   */
  properties: {
    arrsPramas:{
      type: Array, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型)
      value: [0,0,0,0], // 属性初始值(可选),如果未指定则会根据类型选择一个
    },
    
    strPramas: { // 属性名
      type: String, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型)
      value: '', // 属性初始值(可选),如果未指定则会根据类型选择一个
    },
    
    numPramas: Number // 简化的定义方式
  },
})

my-view.wxml

<view class="my-view">
  <view>===========接收外面的属性==============</view>
  <view wx:for="{{arrsPramas}}">
    {{item}}
  </view>
  <view>{{strPramas}}</view>
  <view>{{numPramas}}</view>
</view>

注意:properties 中定义的属性直接在模版(页面)中使用( 不能通过this.data.xxx获取传递过来的数据 )。

6、小程序的页面

事实上,小程序的页面也可以视为自定义组件,可以称为“组件页面”。

因而,页面也可以使用Component构造器构造,拥有与普通组件一样的定义段与实例方法。

但此时要求对应 json 文件中包含 usingComponents 定义段。

此时,组件的属性可以用于接收页面的参数,如访问页面 /pages/index/index?paramA=123&paramB=xyz ,如果声明有属性 paramAparamB ,则它们会被赋值为 123xyz。

注意:页面的生命周期方法(即 on 开头的方法),应写在 methods 定义段中,因为不是Page构造器。

以新建 home-page 组件页面和使用该组件页面的组件 use-component 为例,代码举例如下。

home-page.json

{
  "usingComponents": {}
}

use-component.wxml

<!-- 点击跳转到组件页面 -->
<navigator url="../../components/home-page/home-page?name=jack&age=13">
  <button>点击跳转到组件页面</button>
</navigator>

微信小程序开发进阶_微信小程序_09

home-page.js

// components/home-page/home-page.js
Component({
  /**
   * 组件的属性列表
   * 1.定义接收上一个页面传递过来的name/age参数
   * 
   * ( 可以直接拿到wxml中用 )
   */
  properties: {
    name: String,
    age: Number,
  },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {
    onLoad(){
      //2.接收上一个页面传递过来的name/age参数 【非页面组件(普通组件)不能这样接收外部传递的数据】
      console.log(this.data.name);  // jack
      console.log(this.data.age);  // 13
    }
  }
})

注意:与自定义组件不同的是,小程序的页面 properties 中定义的属性可以在页面中使用,也可以通过this.data.xxx获取。

home-page.wxml

<!--components/home-page/home-page.wxml-->
<text>components/home-page/home-page.wxml</text>
<view>{{name}}-{{age}}</view>

微信小程序开发进阶_小程序_10

三)组件的生命周期

组件的生命周期,指组件自身的一些函数,这些函数在特殊的时间点,或遇到一些特殊的框架事件时被自动触发。

其中,最重要的生命周期是 created attached detached,包含一个组件实例生命流程的最主要时间点。

created:组件实例刚刚被创建好时,created生命周期被触发,此时,组件数据this.data就是在Component构造器中定义的数据data,此时还不能调用setData,通常情况下,这个生命周期只应该用于给组件this添加一些自定义属性字段。

attached:在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。

detached:在组件离开页面节点树后,detached生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则detached会被触发。 

所有生命周期如下:

生命周期

参数

描述

最低版本

created


在组件实例刚刚被创建时执行

1.6.3

ready


在组件在视图层布局完成后执行

1.6.3

moved


在组件实例被移动到节点树另一个位置时执行

1.6.3

attached


在组件实例进入页面节点树时执行

1.6.3

detached


在组件实例被从页面节点树移除时执行

1.6.3

error

Object Error

每当组件方法抛出错误时执行

2.4.1

示例代码如下:

Component({
  // 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容
  attached() {
    // 在组件实例进入页面节点树时执行
  },
  detached() {
    // 在组件实例被从页面节点树移除时执行
  }
})

生命周期方法可以直接定义在 Component 构造器的第一级参数中,但是自小程序基础库版本 2.2.3 起,组件的的生命周期也可以在 lifetimes 字段内进行声明(这是推荐的方式,其优先级最高)。

代码举例如下。

Component({
   /**
   * 组件的方法列表
   */
  methods: {},
  
  // 以下是新的定义方式,可以保持对 >=2.2.3 版本基础库的兼容
  lifetimes: {
    created() {
      //在组件实例刚刚被创建时执行
      console.log('1.在组件实例刚刚被创建时执行')
    },

    attached() {
      // 在组件实例进入页面节点树时执行
      console.log('2.在组件实例进入页面节点树时执行')
    },

    ready() {
      // 在组件在视图层布局完成后执行
      console.log('3.在组件在视图层布局完成后执行')
    },

    detached() {
      // 在组件实例被从页面节点树移除时执行
      console.log('4.在组件实例被从页面节点树移除时执行')
    },
  }
})

组件所在页面的生命周期

另外,还有一种特殊的生命周期,叫做“组件所在页面的生命周期”

pageLifetimes

可用的生命周期有:

生命周期

参数

描述

最低版本

show


组件所在的页面被展示时执行( 回到前台 )

2.2.3

hide


组件所在的页面被隐藏时执行( 回到后台 )

2.2.3

resize

Object Size

组件所在的页面尺寸变化时执行

2.4.0

比如在use-component页面下,使用了一个组件my-view,那就可以在该组件my-view查看页面use-component的生命周期了。

代码举例如下。

my-view.js

// components/my-view/my-view.js
Component({
  options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
  },
  
  // 1.定义该组件可以接收的外部样式类
  externalClasses: ['my-view-class','my-view-active-class'],

  /**
   * 组件的属性列表
   */
  properties: {
    arrsPramas:{
      type:Array,
      value:[0,0,0,],
    },
    strPramas:{
      type:String,
      value:''
    },
    numPramas:Number
  },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {},

  lifetimes: {
    created() {
      //在组件实例刚刚被创建时执行
      console.log('1.在组件实例刚刚被创建时执行')
    },

    attached() {
      // 在组件实例进入页面节点树时执行
      console.log('2.在组件实例进入页面节点树时执行')
    },

    ready() {
      // 在组件在视图层布局完成后执行
      console.log('3.在组件在视图层布局完成后执行')
    },

    detached() {
      // 在组件实例被从页面节点树移除时执行
      console.log('4.在组件实例被从页面节点树移除时执行')
    },
  },

  
  //2.组件所在页面的生命周期
  pageLifetimes: {
    show() {
      // 页面被展示
      console.log('1.页面被展示')
    },
    hide() {
      // 页面被隐藏( 例如切换到后台 )
      console.log('2.页面被隐藏')
    },
    resize(size) {
      // 页面尺寸变化
      console.log('3.页面尺寸变化')
    }
  }
})

四)组件之间的通信

组件间的基本通信方式有以下几种:

WXML 数据绑定( 传递参数 ):用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9 开始,还可以在数据中包含函数)。具体在 组件模板和样式 章节中介绍。

事件:用于子组件向父组件传递数据,可以传递任意数据。

如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent

1、通信步骤

1)组件内部触发事件

my-view.wxml

<view class="my-view">
  <button bindtap="onbindtap">组件内部触发事件</button>
</view>

my-view.js

/**
   * 组件的方法列表
   */
  methods: {
    //1.监听组件内部的点击事件
    onbindtap(event){
      const myEventDetail = {des:'这个对象会赋值给detail对象'} // detail对象,提供给事件监听函数
      const myEventOption = {} // 触发事件的选项
      //2.组件内部触发事件,外部可以监听 bindbtnclick 事件 
      //触发一个事件 -> 儿子向父亲发送事件
      this.triggerEvent('btnclick', myEventDetail, myEventOption)
    }
  },

2)使用组件的地方监听组件内部的事件触发

事件系统是组件间通信的主要方式之一。

自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件。

关于事件的基本概念和用法,参见事件

监听自定义组件事件的方法与监听基础组件事件的方法完全一致。

use-component.wxml

<!-- 使用自定义组件 -->
<my-view my-view-class="my-view" my-view-active-class="my-view-active"
    arrs-pramas = "{{ [1,2,3,4] }}"
    str-pramas = "liujun"
    num-pramas = "12"
    bindbtnclick="onbindbtnclick"
>
   <text slot="content">content布局\n</text>
   <text slot="footer">footer布局</text>
</my-view>

use-component.js

Page({
    data:{},

    // 监听组件内部触发的事件
    onbindbtnclick:function(event){
        console.log(event.detail);  // {des:'这个对象会赋值给detail对象'}
    }
})

2、调用组件内部的方法

特殊的,我们可以在使用组件的地方调用组件内部的方法。

1)组件内部方法定义

首先说明一下,触发事件的选项myEventOption包括:

选项名

类型

是否必填

默认值

描述

bubbles

Boolean


false

事件是否冒泡

composed

Boolean


false

事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部

capturePhase

Boolean


false

事件是否拥有捕获阶段

my-view.wxml

<view class="my-view">
  <button bindtap="onbindtap">组件内部触发事件</button>
</view>

my-view.js

/**
   * 组件的方法列表
   */
  methods: {
    //1.监听组件内部的点击事件
    onbindtap(event){
        const myEventDetail = {des:'这个对象会赋值给detail对象'} // detail对象,提供给事件监听函数
        const myEventOption = {} // 触发事件的选项

        // 组件内部触发事件,外部可以监听 bindbtnclick 事件
        this.triggerEvent('btnclick', myEventDetail, myEventOption)

        // 调用内部方法语法:this.showText('haha'); 
    },

    //2.在组件内部定义一个内部的方法共外部调用( 只能在methods方法中定义 )
    showText(value){
      console.log('value='+value)
    }
  },

2)外部调用组件内部定义的方法

父组件还可以通过 this.selectComponent

use-component.js

/**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    // 获取组件对象(只能获取自定义组件的对象)
    let myView = this.selectComponent('#my-view'); // myView 等于组件的 this
    myView.showText('haha')
    
    console.log(myView.data.name)
    console.log(myView)
  },

五)组件的behaviors

behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 mixins

每个 behavior 可以包含一组属性、数据、生命周期函数和方法

组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用;

每个组件可以引用多个 behavior;

behavior 也可以引用其他 behavior;

behavior 需要使用 Behavior() 构造器定义。

字段的覆盖和组合规则

组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:

如果有同名的属性或方法,组件本身的属性或方法会覆盖 behavior 中的属性或方法,如果引用了多个 behavior ,在定义段中靠后 behavior 中的属性或方法会覆盖靠前的属性或方法;

如果有同名的数据字段,如果数据是对象类型,会进行对象合并,如果是非对象类型则会进行相互覆盖;

生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用。如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。

behaviors使用步骤

1)定义

新建目录,新建behaviors.js。

module.exports = Behavior({
  //1.引入外部的behavior
  behaviors: [],

  //2.定义属性,该属性将会合并到使用该behavior的组件中
  properties: {
    myBehaviorProperty: {
      type: String
    }
  },

  //3.定义data,该data将会合并到使用该behavior的组件中
  data: {
    myBehaviorData: {
      name:'seemydog'
    }
  },

  //4.定义生命周期函数,当和组件的attached合并后,这个会比组件的attached先执行
  attached() {
    console.log('myBehaviorData -> attached')
  },

  // add  lifetimes
  lifetimes: {
    //定义生命周期函数,当和组件的attached合并后,这个会比组件的attached先执行
    ready() {
      console.log('myBehaviorData -> ready')
    },
  },

  //5.定义方法,该方法将会合并到使用该behavior的组件中
  methods: {
    myBehaviorMethod() { 
      console.log("---myBehaviorMethod---")
    }
  }

})

2)使用

my-view.js

// components/my-view/my-view.js
// 1.引入这个myBehavior
const myBehavior = require('../../behavior/my-behavior');

Component({
  behaviors: [myBehavior], // 2.引入这个myBehavior, 它的属性、数据和方法会被合并到组件中
  
  lifetimes: {
    created() {
      // 3.调用behaviors合并过来的方法
      this.myBehaviorMethod();
    },
  }

})

my-view.wxml

<view>使用behavior中的属性和数据</view>
<view>{{myBehaviorProperty}}--{{myBehaviorData.name}}</view>

六)自定义对话框

步骤如下。

1)新建目录,右键新建自定义组件xmg-alert,并配置声明导入

use-alert.json,声明

{
  "usingComponents": {
    "xmg-alert": "../../components/xmg-alert/xmg-alert"
  }
}

use-alert.wxml,使用

<!-- 在json文件中声明导入后,直接使用自定义组件 -->
<xmg-alert>

</xmg-alert>

2)编写对话框,并写好样式布局

既然声明好了,这里就可以开始进行对话框的编写了。

xmg-alert.wxml

<view class="xmg-alert">
  <view class="alert-content">
      <view class="alert-title">title</view>  
      <view class="alert-desp">desp</view>

      <view class="alert-bar">
        <view class="alert-clear">clear</view>
        <view class="alert-commit">commit</view>
      </view>
  </view>
</view>

xmg-alert.wxss

/* components/xmg-alert/xmg-alert.wxss */
page{
  height: 100%;
}

.xmg-alert{
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.3);

  position: fixed;
  top: 0;
  left: 0;
  right: 0;

  display: flex;
  justify-content: center;
  align-items: center;

  z-index: 100;
}

/* 内容布局 */
.alert-content{
  background-color: white;
  width: 500rpx;
  height: 350rpx;
  border-radius: 20rpx;

  display: flex;
  flex-direction: column;
}

/* 上 */
.alert-title{
  text-align: center;
  font-size: 45rpx;
  flex: 2;
  
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 中 */
.alert-desp{
  text-align: center;
  font-size: 40rpx;
  color: gray;

  flex: 3;
  background: skyblue;

  display: flex;
  justify-content: center;
  align-items: center;
}

/* 下 */
.alert-bar{
  flex: 2;
  display: flex;
  flex-direction: row;
}

.alert-clear,
.alert-commit{
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
}

微信小程序开发进阶_xml_11

3)我们可以给对话框传数据

在使用对话框组件的地方,传递数据给它。

use-alert.wxml

<!-- 在json文件中声明导入后,直接使用自定义组件 -->
<xmg-alert
  id="xmg-alert"
  xmg-title="授权提示"
  xmg-content="亲,你还没有授权哦!"
  xmg-clear-text="取消"
  xmg-commit-text="确认"
>

</xmg-alert>

xmg-alert.js,接收数据。

// components/xmg-alert/xmg-alert.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    xmgTitle:{
      type:String,
      value:'标题'
    },
    xmgContent: {
      type: String,
      value: '内容描述'
    },
    xmgClearText: {
      type: String,
      value: 'clear'
    },
    xmgCommitText: {
      type: String,
      value: 'ok'
    },
  },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {}
})

xmg-alert.wxml,展示接收的数据。

<view class="xmg-alert">
  <view class="alert-content">
      <view class="alert-title">{{xmgTitle}}</view>  
      <view class="alert-desp">{{xmgContent}}</view>

      <view class="alert-bar">
        <view class="alert-clear">{{xmgClearText}}</view>
        <view class="alert-commit">{{xmgCommitText}}</view>
      </view>
  </view>
</view>

微信小程序开发进阶_xml_12

4)我们还可以设置默认选中“确认”

use-alert.wxss,定义要传的样式,选中时的样式。

.text-active{
    color:#ac0;
}

use-alert.wxml,传递外部class给组件。

<!-- 在json文件中声明导入后,直接使用自定义组件 -->
<xmg-alert
  id="xmg-alert"
  xmg-title="授权提示"
  xmg-content="亲,你还有授权哦!"
  xmg-clear-text="取消"
  xmg-commit-text="确认"

  xmg-text-active-class="text-active"
>

</xmg-alert>

xmg-alert.js,组件内部接收外部class。

// components/xmg-alert/xmg-alert.js
Component({

  // 定义该组件可以接收的外部样式类
  externalClasses: ['xmg-text-active-class'],

  /**
   * 组件的属性列表
   */
  properties: { ...... },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {}
})

xmg-alert.wxml,使用接收到的class样式。

<view class="xmg-alert">
  <view class="alert-content">
      <view class="alert-title">{{xmgTitle}}</view>  
      <view class="alert-desp">{{xmgContent}}</view>

      <view class="alert-bar">
        <view class="alert-clear">{{xmgClearText}}</view>

        <!-- 使用接收到外部的class -->
        <view class="alert-commit xmg-text-active-class">{{xmgCommitText}}</view>
      </view>
  </view>
</view>

微信小程序开发进阶_小程序_13

5)点击事件处理

首先,监听点击事件。

xmg-alert.wxml

<view class="xmg-alert">
  <view class="alert-content">
      <view class="alert-title">{{xmgTitle}}</view>  
      <view class="alert-desp">{{xmgContent}}</view>

      <view class="alert-bar">
        <!-- 监听点击事件 -->
        <view bindtap="oncleartap" class="alert-clear">{{xmgClearText}}</view>
        <view bindtap="oncommittap" class="alert-commit xmg-text-active-class">{{xmgCommitText}}</view>
      </view>
  </view>
</view>

xmg-alert.js

// components/xmg-alert/xmg-alert.js
Component({
  ...
  ...

  /**
   * 组件的方法列表
   */
  methods: {
    //1.取消
    oncleartap(){
      console.log('xmg-alert oncleartap')  
    },
    //2.确认
    oncommittap(){
      console.log('xmg-alert oncommittap')  
    }
  }
})

然后,触发点击事件。

xmg-alert.js,注意函数都写在 methods 中。

// components/xmg-alert/xmg-alert.js
Component({
  ....
  ....

  /**
   * 组件的方法列表
   */
  methods: {
    // 取消
    oncleartap(){
      // 触发点击事件
      this.dispatchTap('clear');
    },
    // 确认
    oncommittap(){ 
      // 触发点击事件
      this.dispatchTap('commit');
    },

    // 触发点击事件
    dispatchTap(value){
      const myEventDetail = { 'btnText': value } // detail对象,提供给事件监听函数
      const myEventOption = {} // 触发事件的选项
      // 组件内部触发事件,外部可以监听 bindxxxx 事件
      this.triggerEvent('btnclick', myEventDetail, myEventOption)
    }
  }
})

最后,外部使用这个对话框的地方,监听点击事件。

use-alert.wxml

<!-- 在json文件中声明导入后,直接使用自定义组件 -->
<xmg-alert
  id="xmg-alert"
  xmg-title="授权提示"
  xmg-content="亲,你还有授权哦!"
  xmg-clear-text="取消"
  xmg-commit-text="确认"

  xmg-text-active-class="text-active"
  
  bindbtnclick="handlerClick"
>

</xmg-alert>

use-alert.js

// pages/03-alert-test/03-alert-test.js
Page({
  /**
   * 页面的初始数据
   */
  data: {},

  handlerClick:function(event){
     if (event.detail.btnText=='commit'){
      console.log("父亲处理事件commit...")
    }else{
      console.log("父亲处理事件clear...")
    }
  }
})

微信小程序开发进阶_小程序_14

6)控制显示和隐藏

xmg-alert.js

// components/xmg-alert/xmg-alert.js
Component({
  ....
  ....

  /**
   * 组件的初始数据
   */
  data: {
    isShow:false,
  },

  /**
   * 组件的方法列表
   */
  methods: {
    ...
    ...

    // 触发点击事件
    dispatchTap(value){
      const myEventDetail = { value: value } // detail对象,提供给事件监听函数
      const myEventOption = {} // 触发事件的选项
      // 组件内部触发事件,外部可以监听 bindxxxx 事件
      this.triggerEvent(value, myEventDetail, myEventOption);
      
      // 隐藏对话框
      this.hidden();
    },

    // 显示对话框
    show(){
        this.setData({
            isShow:true,
        })
    },

    // 隐藏对话框
    hidden(){
        this.setData({
            isShow: false,
        })
    },

    // 显示和隐藏切换
    toggle(){
      this.setData({
        isShow: !this.data.isShow
      });
    }
  }
})

xmg-alert.wxml

<view class="xmg-alert" wx:if="{{isShow}}">
  <view class="alert-content">
      <view class="alert-title">{{xmgTitle}}</view>  
      <view class="alert-desp">{{xmgContent}}</view>

      <view class="alert-bar">
        <view bindtap="oncleartap" class="alert-clear">{{xmgClearText}}</view>
        <view bindtap="oncommittap" class="alert-commit xmg-text-active-class">{{xmgCommitText}}</view>
      </view>
  </view>
</view>

7)最后,还可以给对话框添加动画

xmg-alert.wxml

<view class="xmg-alert" wx:if="{{isShow}}">
   <!-- 添加动画类:alert-animated -->
  <view class="alert-content alert-animated">
      <view class="alert-title">{{xmgTitle}}</view>  
      <view class="alert-desp">{{xmgContent}}</view>

      <view class="alert-bar">
        <view bindtap="oncleartap" class="alert-clear">{{xmgClearText}}</view>
        <view bindtap="oncommittap" class="alert-commit xmg-text-active-class">{{xmgCommitText}}</view>
      </view>
  </view>
</view>

xmg-alert.wxss

.alert-animated{
  animation: alertAnimated 0.3s;
}

@keyframes alertAnimated{
  0%{
    transform: scale3d(0.5,0.5,0.5);
  }
  60%{
     transform: scale3d(1.2,1.2,1.2);
  }
  100%{
     transform: scale3d(1,1,1);
  }
}

七)网络请求封装

微信小程序允许使用网络请求数据,为我们提供了网络请求相关的api。

注意:

1)正式版有appid的小程序要在小程序管理平台配置允许访问的域名,小程序管理平台入口:微信公众平台

2)正式版有appid小程序网络请求必须使用https协议。

简单的使用。

wx.request({})

// 1.发起网络请求POST
    wx.request({
       url: 'http://....../login.json',
       method:'POST',
       data:'username=admin&password=123456',
       header:{
         'content-type': 'application/x-www-form-urlencoded'  // 添加的参数为表单
       },

      // 2.拿到请求的结果
      success: function (result){
        console.log(result.data);  // 获取返回的数据
        console.log(result.statusCode);  // 获取返回的状态码
      },
      // 3.处理请求失败
      fail:function(error){
        console.log(error);
      }
    })

我们使用的时候,一般进行封装。

fetch.js

// 封装了一个fetch函数
let fetch = (options)=>{
    return new Promise(function(resolve,reject){
        wx.request({
            url: options.url || '',
            method: options.method || 'GET',
            data:options.data || '',
            header:options.header || {'content-type':'application/json'}, //指定提交的数据类型
            dataType:options.dataType || 'json', //指定返回的数据类型
            success:function(res){
                //1.相当于调用外部传递进来的then函数
                resolve(res);
            },
            fail:function(error){
                //2.相当于调用外部传递进来的catch函数
                reject(error);
            }
        })
    })
}

module.exports = {
    fetch: fetch
}

use.js

let fetch = require("../../util/fetch.js");

Page({
    data: {
        homeData:null,
    },

    onLoad: function(options){
        let _this = this;

        fetch({
            url:"https://....../home",
        }).then( res => {
            _this.setData({
                homeData: res.data,
            })
        }).catch( err => {
            console.log(err)
        })
    }
})

我们还可以继续封装成工具类,这样使用起来更有针对性,也有利于后续扩展和维护。

api.js

// 定义基本的url
let baseurl = 'http://11.22.33.44';

module.exports = {
    HOME: baseurl + '/home.json',
    MINE: baseurl + '/mine.json',
    SHOPCAR: baseurl + '/shopcar.json',
    DETAILS: baseurl + '/detail.json'
}

service.js

// 1.引入发起网络请求的函数
const fecth = require('./fetch.js').fetch;
//2.引入API接口文件
const API = require('./api.js');

// 3.获取首页的数据( 以后可以在这里扩展,方便后期维护 )
const getHomeData = ()=>{
  //4.发起网络请求
  return fecth( {url:API.HOME} );
}

// 获取其他页面的数据
const getShopCarData = ()=>{
  return fecth( {url:API.SHOPCAR} );
}

const getMineData = ()=>{
  return fecth( {url:API.MINE} );
}

const getDetailsData = ()=>{
  return fecth( {url:API.DETAILS} );
}

module.exports = {
    getHomeData: getHomeData,
    getShopCarData: getShopCarData,
    getMineData: getMineData,
    getDetailsData: getDetailsData
}

那么,上面的use.js使用网络请求就可以改成:

let httpService = require("../../util/service.js");

Page({
    data: {
        homeData:null,
    },

    onLoad: function(options){
        let _this = this;

        httpService.getHomeData()
        .then( res => {
            _this.setData({
                homeData: res.data
            })
        }).catch( err => {
            console.log(err)
        })
    }
})

其他网络相关的问题可以查阅:网络 | 微信开放文档

八)授权

一般进行的是首页授权。

index.wxml

<!--index.wxml-->
<import src="../../templates/loading"></import>

<!-- 有数据 -->
<block wx:if="{{homeBean}}">
  <view class="home-content">
    <block wx:if="{{!userInfo}}">
        <!-- 显示授权的布局 -->
        <button style="margin:0rpx;padding:0"  open-type="getUserInfo" bindgetuserinfo="getUserInfo">
            <view class="authorize">
                <icon type="info"/>
                <text>请授权头像等信息,以便为您提供更好的服务</text>
            </view>
        </button>
    </block>
  </view>
</block>

<!-- 没有数据 -->
<block wx:else>
  <!-- 显示加载进度... -->
  <template is="loading"></template>
</block>

index.wxss

.authorize{
  font-size: 28rpx;
  height: 100rpx;
  background-color: #f4f4f4;

  display: flex;
  justify-content: center;
  align-items: center;
}

.authorize icon{
  margin-right: 20rpx;
}

index.js

// 导入httpService
let httpService = require('../../util/service.js');
//导入App对象
let app = getApp();

Page({
  /**
   * 页面的初始数据
   */
  data: {
    homeBean:null,
    userInfo:null,
  },
  
  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    //当页面再次显示的时候,重新赋值userInfo,如果不为空代表已经授权过了
    this.setData({
      userInfo: app.globalData.userInfo
    })
  },

  /**
   * 获取用户信息
   */
  getUserInfo(event) {
    let userInfo = event.detail.userInfo;

    if (userInfo){
        //授权之后将数据缓存起来
        app.globalData.userInfo = userInfo;
        this.setData({
            userInfo: userInfo
        })

    //登录
    wx.login({
        success: res => {
            wx.request({
                url: 'http://11.22.33.44/login.json',
                data: {
                    code: res.code
                },
                success: res => {
                    if (res.data.success) {
                        //登录成功
                        //token/sessionKey,openid
                        app.globalData.token = res.data.data.token;
                        app.globalData.isLogin = true;
                        
                        this.setData({
                            isLogin: true,
                            mineBean: res.data.data.settings
                        })
                    }
                }
            })
        }
    })
  }
})

如果授权时与其他页面产生bug,比如mine页面,则bug解决如下:

mine.js

// pages/mine/mine.js
let app = getApp();

Page({
  /**
   * 页面的初始数据
   */
  data: {
    userInfo: null,
    minebean:null,
    isLogin: null,
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    //1.当页面显示时,重新赋值useinfo到page中的data中
    if (app.globalData.isLogin){
      this.setData({
        mineBean: app.globalData.mineBean,
        userInfo: app.globalData.userInfo
      })
    }
  }
})

九)微信支付

微信支付是小程序开发中的一个重要部分,具体介绍请浏览:

wx.requestPayment(Object object) | 微信开放文档

十)音乐播放器实现

可以通过slider滑动组件和微信提供的wx.createInnerAudioContext()创建一个音乐播放器。

微信小程序开发进阶_自定义组件_15

下面是代码实现过程。

music.js

// pages/music.js
// 导入网络请求封装
const httpService = require('../../network/service')
// 播放器创建
const audioPlayer = wx.createInnerAudioContext()
// timer
let timer = null

Page({
  /**
   * 页面的初始数据
   */
  data: {
    songInfo: {},
    selectedSongInfo: {},
    tokenInfo: {},
    isPlay: false,
    url: '',
    currentValue: 0,
    currentTime: '',
    allTime: '',
    allValue: 0
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {},

  // 歌曲、歌手搜索
  onBindBlur: function (event) {
    this.getSongInfo(event.detail.value)
  },

  // 搜索歌曲、歌手
  getSongInfo: function (songName) {
    httpService.getSongInfo(songName).then(res => {
      let tempStr = res.data.split('callback(')[1]
      tempStr = tempStr.substring(0, tempStr.length - 1)
      // console.log(JSON.parse(tempStr).data)
      this.setData({
        songInfo: JSON.parse(tempStr).data
      })
    }).catch(err => {
      wx.showToast({
        title: '搜索歌曲失败!',
        icon: 'none',
        duration: 3000
      })
    })
  },

  // 点击听歌
  onClickToListen: function (event) {
    this.setData({
      selectedSongInfo: event.currentTarget.dataset.song,
      isPlay: true
    })
    this.getToken(event.currentTarget.dataset.song.songmid)
  },

  // 获取token
  getToken: function (songmid) {
    httpService.getToken(songmid).then(res => {
      this.setData({
        tokenInfo: res.data.data.items[0]
      })
      // 获取到token再执行获取地址的函数
      this.getSongPlayAddress(this.data.tokenInfo)
      this.playSong(this.data.url)
    }).catch(err => {
      wx.showToast({
        title: '获取token出错!',
        icon: 'none',
        duration: 3000
      })
    })
  },

  // 获取音乐播放地址
  getSongPlayAddress: function (options) {
    this.setData({
      url: httpService.getSongPlayAddress(options)
    })
  },

  // 播放歌曲
  playSong: function (url) {
    clearInterval(timer)
    this.setData({
      isPlay: true
    })
    if (url && this.data.tokenInfo.vkey) {
      // 播放
      audioPlayer.src = this.data.url
      audioPlayer.volume = 0.4
      audioPlayer.loop = true
      audioPlayer.play()
      timer = setInterval(() => {
        this.setData({
          currentValue: Math.floor(audioPlayer.currentTime),
          currentTime: this.format(audioPlayer.currentTime),
          allTime: this.format(audioPlayer.duration),
          allValue: Math.floor(audioPlayer.duration)
        })
        if (Math.floor(audioPlayer.currentTime) > 0 && Math.floor(audioPlayer.currentTarget) >= Math.floor(audioPlayer.duration)) {
          clearInterval(timer)
        }
      }, 1000);
    } else {
      // 停止
      audioPlayer.stop()
      this.setData({
        isPlay: false
      })
      wx.showToast({
        title: '音源出错!',
        icon: 'none',
        duration: 3000
      })
    }
    // 音乐播放失败监听方法
    audioPlayer.onError(err => {
      wx.showToast({
        title: err,
        icon: 'none',
        duration: 3000
      })
    })
  },

  // 暂停播放
  pauseSong: function () {
    clearInterval(timer)
    this.setData({
      isPlay: false,
      currentValue: Math.floor(audioPlayer.currentTime)
    })
    audioPlayer.pause()
    // 音乐播放失败监听方法
    audioPlayer.onError(err => {
      wx.showToast({
        title: err,
        icon: 'none',
        duration: 3000
      })
    })
  },

  // 滑动选择器滑动
  onBindSliderChange: function (e) {
    // console.log(e)
  },

  // 时间格式化
  format: function (t) {
    let time = Math.floor(t / 60) >= 10 ? Math.floor(t / 60) : '0' + Math.floor(t / 60);
    t = time + ':' + ((t % 60) / 100).toFixed(2).slice(-2);
    return t;
  }
})

music.wxml

<!--pages/music.wxml-->
<view class="music-container">
	<!-- 音乐搜索框 -->
	<view class="song-search">
		<input class="song-search-input" type="text" value="" placeholder="请输入歌手、音乐搜索" bindblur="onBindBlur"></input>
	</view>
	<!-- 只是margin -->
	<view class="just-margin" style="height:60px;width:100%;border-bottom:1px solid #f7f7f7;"></view>
	<!-- 搜索列表 -->
	<scroll-view style="background:rgba(255,222,173,.7);padding:10px 0;border-bottom:1px solid #f7f7f7;" wx:for="{{songInfo.song.list}}" wx:key="index" scroll-y="{{true}}">
		<view class="song-info-box" bindtap="onClickToListen" data-song="{{item}}">
			<text class="song-name">{{item.songname}}</text>
			<text class="song-albumname">歌手:{{item.singer[0].name}} 专辑名:{{item.albumname}}</text>
		</view>
	</scroll-view>

	<!-- 音乐播放器 -->
	<view class="music-box">
		<text wx:if="{{!isPlay}}" class="iconfont icon-bofang" style="color:#d81e06;" bindtap="playSong" data-num="1"></text>
		<text wx:else class="iconfont icon-zanting" style="color:#d81e06;" bindtap="pauseSong" data-num="2"></text>
		<text class="music-song-name">{{selectedSongInfo.songname || '歌名'}} - {{selectedSongInfo.singer[0].name || '歌手'}}</text>
		<text class="start-point">{{currentTime || '00:00'}}</text>
		<view class="slider-box">
			<slider block-size="16" value="{{currentValue}}" max="{{allValue}}" bindchange="onBindSliderChange"></slider>
		</view>
		<text class="end-point">{{allTime || '00:00'}}</text>
	</view>
</view>

music.wxss

/* pages/music.wxss */
/* 整个盒子 */
.music-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;

  /* 微信小程序设置元素背景图片时,推荐使用base64格式,可以网上找工具转换 */
  background: url(data:image/jpg;base64,......);
  background-color: #FFDEAD;
  background-size: cover;
}

/* 搜索框 */
.song-search {
  position: fixed;
  top: 22rpx;
  z-index: 1;
  width: 100%;
}
.song-search .song-search-input {
  background: #fff;
  height: 70rpx;
  width: 70%;
  margin: 0 auto;
  color: #666;
  border-radius: 15rpx;
  padding: 0 20rpx;
  font-size: 13px;
}

/* 音乐信息搜索列表 */
.song-info-box {
  padding-left: 20rpx;
}
.song-name {
  margin-bottom: 15rpx;
  color: #222;
  font-size: 18px;
  display: block;
}
.song-albumname {
  color: #555;
  font-size: 15px;
}

/* 音乐播放器 */
.music-box {
  position: fixed;
  left: 0;
  bottom: 0;
  height: 130rpx;
  line-height: 130rpx;
  width: 100%;
  background-color: #FFDEAD;
  display: flex;
  flex-direction: row;
}
.icon-bofang,.icon-zanting {
  margin-left: 30rpx;
}
.music-song-name {
  position: absolute;
  left: 15%;
  top: -30rpx;
  font-size: 16px;
}
.slider-box {
  width: 65%;
  position: absolute;
  right: 12%;
  bottom: 0;
}
.start-point,.end-point {
  position: absolute;
  font-size: 12px;
  top: 30rpx;
}
.start-point {
  left: 15%;
}
.end-point{
  right: 5%;
}

这里再说一下网络请求的封装。

service.js

const fetch = require('./fetch').fetch;
const API = require('./api');

// 获取天气信息
const getTemperature = (city) => {
  return fetch({
    url: API.TEMPERATURE,
    data: {
      key: '4r9bergjetiv1tsd',
      location: city || 'shenzhen',
      // ts:'1584861674',
      unit: 'c'
    },
  })
}

// 获取歌曲
const getSongInfo = (songName) => {
  return fetch({
    url: API.MUSIC_GET_SONG,
    data: {
      aggr: 1,
      cr: 1,
      flag_qc: 0,
      p: 1, //页数
      n: 30, //每一页条数
      w: songName //搜索关键字
    }
  })
}

// 获取封面
const getCover = (albumid) => {
  return API.MUSIC_GET_COVER + albumid + '_0.jpg'
}

// 获取token
const getToken = (songmid) => {
  return fetch({
    url: API.MUSIC_GET_TOKEN,
    data: {
      format: 'json205361747',
      platform: 'yqq',
      cid: 205361747,
      songmid: songmid,
      filename: 'C400' + songmid + '.m4a',
      guid: 126548448
    }
  })
}

// 获取音乐播放地址
const getSongPlayAddress = (options) => {
  return API.MUSIC_GET_PLAY_ADDRESS + options.filename + '?fromtag=0&guid=126548448&vkey=' + options.vkey
}

module.exports = {
  getTemperature: getTemperature,
  getSongInfo: getSongInfo,
  getCover: getCover,
  getToken: getToken,
  getSongPlayAddress: getSongPlayAddress
}

fetch.js

let fetch = (options) => {
  return new Promise((resolve, reject) => {
    wx.request({
      url: options.url || '',
      method: options.method || 'GET',
      data: options.data || '',
      header: options.header || {
        'content-type': 'application/json' // 默认为json
      },
      dataType:options.dataType || 'json',
      success: function (res) {
        resolve(res)
      },
      fail: function (err) {
        reject(err)
      }
    })
  })
}

module.exports = {
  fetch: fetch
}

api.js

module.exports = {
  TEMPERATURE:"https://api.seniverse.com/v3/weather/now.json",
  MUSIC_GET_SONG:"https://c.y.qq.com/soso/fcgi-bin/client_search_cp",
  MUSIC_GET_COVER:"http://imgcache.qq.com/music/photo/album_300/17/300_albumpic_",
  MUSIC_GET_TOKEN:"https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg",
  MUSIC_GET_PLAY_ADDRESS:"http://ws.stream.qqmusic.qq.com/"
}

OK,一个音乐播放器基本完成,后期还可以继续对slider的拖动继续实现功能代码。

另外,应该注意wx.createInnerAudioContext()只能创建一次,避免创建多次的情况,所以一般是在全局环境定义一个innerAudioContext对象。

十)其他功能

进入场景值:

扫码接口 | 微信开放文档

小程序发布流程:

产品定位及功能介绍 | 微信开放文档

小程序发布时请注意选对小程序所属范围,目前个人的小程序开发开放功能有限,企业类的则开放功能较多。