微信小程序

HTTP协议的状态管理

由于HTTP协议是一款基于短连接模型的协议,所以同一个客户端发送的请求是无状态的。即服务端没有将同一个客户端发送的多次请求当成一个整体来看待,也没有将同一个客户端涉及到的数据保存下来以后使用。这就是无状态协议的特点。

HTTP协议状态的管理办法:

  1. cookie机制

    1. 客户端发送第一次请求,服务端接收请求,处理请求。
    2. 服务端在响应数据包中添加cookie信息,返回给客户端让客户端保存
    3. 客户端接收到响应,解析到cookie信息,将这些数据存入本地cookie存储区。
    4. 客户端发送后续请求时,将会自动携带域名匹配的cookie数据一起发送请求,这样,服务端就可以获取上一次请求所存储的信息,从而知道当前客户端的状态,实现http的状态管理。

    cookie不安全,无法存储敏感数据

  2. session机制

    1. 客户端发送第一次请求,服务端接收请求,处理请求。
    2. 服务端将敏感数据存入session区域,并且为该用户分配一个SESSIONID。在返回响应时,以cookie形式发给客户端。
    3. 客户端接收cookie,将SESSIONID存起来。
    4. 当客户端发送下次请求时,将会自动带着SESSIONID一起发送请求,这样服务端就可以通过SESSIONID找到以前存过的数据,从而获取用户的状态信息,完成http状态管理。
  3. token机制

    https://pan.baidu.com/s/1B3YUiJnd3A2vOGKmkEu0_Q

    2prs

微信小程序

  1. 下载对应版本的微信开发者工具。

    https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
    
  2. 强烈建议申请新邮箱账号。

    百度:网易邮箱。

    https://mail.163.com/register/index.htm?from=163mail&utm_source=163mail
    

微信公众平台

https://mp.weixin.qq.com

服务号:为企业和组织提供的进行用户管理和服务的账号类型。

订阅号:为企业、组织和个人提供的进行信息发布的账号类型。

小程序:为企业、组织或个人提供的可以达到与原生app功能相近的应用程序。在微信内部运行,其优点在于小,无需下载安装包,用完就走。

小程序接入流程

在微信公众平台首页,注册小程序开发者账号。

  1. 注册账号。
  2. 验证邮箱。
  3. 填写主体信息(个人)。
  4. 注册成功。

创建小程序项目

安装微信开发者工具IDE

扫码登录后,点击+新建小程序项目。

填写基本信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uuNrJkkO-1648187335442)(C:\Users\web\AppData\Roaming\Typora\typora-user-images\1645601263836.png)]

目录:不能有中文、不能有空格,最后一个目录得是空目录。

AppID:下拉列表中可以选择AppID

​ 后台管理网站 – 开发管理 – 开发设置 – 看到AppID

小程序项目的文件结构

小程序项目中包含的文件类型:

  1. .json文件 配置文件

    app.json 在项目的根目录下,定义项目的全局配置参数。

    页面.jsonpages文件夹下,定义单个页面的配置参数。

  2. .wxml文件

    模板文件(类似于html,定义页面结构。但是此处不能使用任何html标签)

  3. .wxss文件 样式文件

    app.wxss 项目的根目录下。定义全局样式。

    页面.wxsspages文件夹下。定义单个页面的样式。

  4. .js文件 脚本文件

    app.js 项目根目录下。用于创建App对象。小程序启动时调用,全局唯一。可以把一些共享数据、全局生命周期相关代码定义在这里。

    页面.js pages文件夹下。每一个页面都会有一个js文件。通过该js文件来创建Page对象来管理当前页面中的脚本代码。当需要显示某页面时就会创建Page对象,用于初始化页面数据,声明事件处理函数,声明生命周期等脚本代码。

app.json

app.json用于对小程序进行全局配置。

pages配置项

pages配置项用于定义当前小程序包含哪些页面(index

"pages": [
    "pages/index/index",
    "pages/test/test"
],

新建配置项:"pages/test/test",将会在pages目录下新增test目录,test目录下新增test四件套。意味着项目又多了一个页面。如果将该配置写在数组的首位,那么test将会作为项目的首页,启动时自动显示首页。

JSON文件配置的语法
  1. JSON文件不能写注释。
  2. 字符串必须在双引号之间。 JSON对象的属性名也必须在双引号之间。
  3. 数组或对象的最后一个成员后不能加逗号。
window配置项
"window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "black"
},
tabbar配置项
"tabBar": {
    "color": "#333",
    "selectedColor": "#f00",
    "list": [{
        "text": "电影",
        "pagePath": "pages/index/index",
        "iconPath": "/images/index_disable.png",
        "selectedIconPath": "/images/index_enable.png"
    },{
        "text": "影院",
        "pagePath": "pages/theatre/theatre",
        "iconPath": "/images/theatre_disable.png",
        "selectedIconPath": "/images/theatre_enable.png"
    },{
        "text": "我的",
        "pagePath": "pages/me/me",
        "iconPath": "/images/me_disable.png",
        "selectedIconPath": "/images/me_enable.png"
    }]
},
style

基础库 2.8.0 开始支持,低版本需做兼容处理

微信客户端 7.0 开始,UI 界面进行了大改版。小程序也进行了基础组件的样式升级。app.json 中配置 "style": "v2"可表明启用新版的组件样式。

本次改动涉及的组件有 button icon radio checkbox switch slider。可前往小程序示例进行体验。

sitemapLocation

用于指明 sitemap.json 的位置;默认为 ‘sitemap.json’ 即在 app.json 同级目录下名字的 sitemap.json 文件。

sitemapLocation的作用是定义一些通用的爬虫规则,指定小程序中那些页面被允许索引

注:sitemap 的索引提示是默认开启的,如需要关闭 sitemap 的索引提示,可在小程序项目配置文件 project.config.jsonsetting 中配置字段 checkSiteMapfalse

app.wxss

app.wxss用于定义全局样式。

app.js

app.js在小程序项目根目录下。是小程序全局的初始化脚本。当小程序启动时,就会执行该文件,创建App对象。 该文件仅执行一次,也就意味着App对象全局单例(唯一)。App对象用于定义整个应用程序的生命周期回调方法,全局共享的数据等内容。

微信小程序组件库

小程序中wxml用于定义页面内容,它由各式各样的组件构成,这些组件都是微信自定义的,原生html标签不能用。

关于组件属性的使用

  1. 小程序中的组件若含有布尔类型的属性,无论设置为true还是false,小程序都会当做字符串来进行解释,都会被解释为true。除非用空字符串""。但是这么写有点野,推荐通过{{}}引用js脚本变量,为属性赋值。

    <view class="v2"
        hover-stop-propagation="{{true}}"
        hover-class="v2-hover"></view>
    
  2. 小程序中组件属性的属性名,既可以使用驼峰命名法,也可以使用短横线命名法,两种属性名的命名习惯小程序都支持。

view组件

view组件为视图容器组件(类似div)。 其基本语法:

1.如果组件的属性为布尔类型,当我们直接设置属性值为true或false时,都会被当做true来看待(js中非空字符串都为true);除非使用空字符串“ ”,但这么写有点野

属性一般默认false,写上就是true,不写就是false

建议用以下方法:

 hover-stop-propagation="{{false}}"

2.小程序组件的属性名既可以使用驼峰命名法,也可以使用短横线命名法,二者够可以正确设置属性

<view	class="定义样式类名"
      	hover-class="点击view后应用的样式类名"></view>
  <view class="v2"
  hover-class="v2-hover"
  hover-start-time="按住多久设置点击态,单位毫秒"
  hover-stay-time="松开多久取消点击态,单位毫秒1000"
  hover-stop-propagation="阻止点击态向父级传播 值为布尔类型"
  >

案例:

  1. 新建页面:pages/testing/view/view
  2. 该页面中测试应用view组件。
let f = true
1	f = 'true'	
2	f = 'false'
3	f = 'abc'
4	f = ''
5	f = 1
6	f = -1
7	f = 0
8	f = undefined
9	f = null

if(f){
    console.log('真...')
}

image组件

image组件为图片组件,用于显示图片。支持GIFJPGPNGSVGWEBP等图像格式。其语法如下:

<image src="文件路径(支持网络路径)"
       lazy-load="是否采用图片懒加载模式"
       mode="图像的裁切模式"
       show-menu-by-long-press="是否显示长按菜单"></image>

  mode="aspectFill"	//图片在容器居中全部显示,多余的切除
  mode="aspectFit"	//图片完全在容器中显示,其余空间留黑
  mode="scaleToFill"  //图片在容器铺满,会失真

案例:pages/testing/image/image

wxss是小程序提供的一套样式语言,也会经过编译来渲染组件样式。wxss具备了css的大部分特性。并且对css进行了扩展,新增了rpx尺寸单位。

rpx 响应式像素

使用rpx作为尺寸单位来定义组件的宽高,可以根据屏幕的分辨率进行自动转换,在不同的屏幕下会转成不同的px物理像素值。从而实现屏幕适配。

设计规定:无论任何设备,屏幕的宽度都是750rpx

由此可知iphone6下原始宽度为375px,用rpx表示为750rpx。意味着在iphone6平台下,1px = 2rpx

设备rpx换算px (屏幕宽度/750)px换算rpx (750/屏幕宽度)
iPhone51rpx = 0.42px1px = 2.34rpx
iPhone61rpx = 0.5px1px = 2rpx
iPhone6 Plus1rpx = 0.552px1px = 1.81rpx

swiper组件

swiper组件为轮播图组件,其语法为:

<swiper autoplay="是否自动播放"
        indicator-dots="是否显示指示器"
        circular="是否采用前后循环播放"
        interval="多长时间换下一页"
        duration="切换动画的持续时间"
        ....>
    <swiper-item></swiper-item>
    <swiper-item></swiper-item>
    <swiper-item></swiper-item>
</swiper>

案例:新建pages/testing/swiper/swiper

text组件

text组件用于显示文本的组件,其语法:

<text user-select=""
      decode=""
      space="">文本</text>

navigator组件

navigator组件是页面链接组件,用于控制页面的跳转。其基本语法:

<navigator url="当前小程序内的路由地址"
           open-type="跳转方式">
    链接文本
</navigator>

open-type跳转方式有以下几种:

  1. navigate ,默认的跳转方式,可以从当前页跳转到非tabbar页面。跳转的过程将会保留当前页,新建目标页,而后跳转过去,称为保留跳转。
  2. navigateBack,返回上一级页面。这种操作将会销毁当前页,从而显示上一页。可以配合属性delta实现上n页的跳转。
  3. switchTab ,字面理解为切换标签页(底部选项卡页面)。这种跳转方式用于跳转到tabbar页面。一旦这么做,就会销毁所有非tabbar页面。
  4. redirect,这种跳转方式将会关闭当前页,跳转到非tabbar的目标页面。这种方式也将创建新页面。
  5. reLaunch,字面理解为: 重新启动应用。这种方式将会销毁所有页面。重新打开小程序中的某一个目标页面。

案例:testing/a/a tesing/b/b testing/c/c

scroll-view组件

scrollview组件用于实现可滚动的视图容器(支持水平、垂直滚动)。基本结构如下:

<scroll-view style="height:200px;"
             scroll-x="是否允许水平方向滚动"
             scroll-y="是否允许垂直方向滚动">
    <view>....</view>
    <view>....</view>
    <view>....</view>
    ...非常多....
</scroll-view>

案例: pages/testing/scroll/scroll

scroll-view组件

scrollview组件用于实现可滚动的视图容器(支持水平、垂直滚动)。基本结构如下:

<scroll-view style="height:200px;"
             scroll-x="是否允许水平方向滚动"
             scroll-y="是否允许垂直方向滚动">
    <view>....</view>
    <view>....</view>
    <view>....</view>
    ...非常多....
</scroll-view>

案例: pages/testing/scroll/scroll

input组件

input组件为输入框组件,其语法结构:

<input	type="输入框的类型:text|number|idcard|digit"
       	placeholder="占位符内容"
       	value="文本框的值"
       	maxlength="可输入的最大长度"
       	password="是否是密码框"
       
       	bindinput="输入内容后,触发"
       	bindfocus="获取焦点时,触发"
       	bindblur="焦点失去时,触发"></input>

案例:pages/testing/input/input

基于小程序的input组件实现双向数据绑定,让文本框中值与data中的某一个变量实现动态绑定。

简易双向数据绑定

wxml:

<input model:value="{{name}}" type="text"  />
输入的是:{{name}}

js:

Page({
    data: {
		name: ''
    }
})

而早期小程序的双向数据绑定需要借助于bindinput事件来进行处理。一旦用户在文本框中更新了内容,就会触发该事件,在事件处理函数中获取文本框的值,然后更新data

标准的双向数据绑定

wxml

<input bindinput="inputPwd" type="text"/>
{{pwd}}

js

Page({
    data:{
        pwd: ''
    },
    inputPwd(event){
        let val = event.detail.value  // 文本框的值 
        // 第一种更新data的方式:不会更新界面
        this.data.pwd = val
        // 第二种更新data的方式:可以自动更新界面中使用pwd的位置
        this.setData({ pwd:val })
    }
})

WXML语法基础

wxml是一套标签语言,符合标签语言的相关语法,用于定义页面的结构、内容。在wxml中经常需要呈现动态数据(动态文本、动态样式、动态属性等),而这些动态数据来源于对应js文件中data里声明的变量。就需要使用{{}}来动态引用。大概有以下几类需求:

内容绑定
Page({
    data:{
        name: 'zs',
        age: 15,
        userInfo: {}
    }
})
<view>{{name}}</view>
<view>{{age}}</view>
属性绑定

当需要动态设置组件的属性值时,就需要使用属性绑定:

Page({
    data:{
        url: '/images/1.jpg',
        num: 1,
        d: 'images'
    }
})
<image src="{{url}}"></image>
<image src="/images/{{num}}.jpg"></image>
样式绑定

动态更新组件的wxss样式:

Page({
    data:{
        className: 'blue',
        c: 'red',
        bw: 1
    }
})
.red{ color:red; }
.blue{ color:blue; }
<text class="{{className}}">内容文本</text>
<text style="color:{{c}}; border:{{bw}}px solid {{c}};">
    内容文本
</text>
列表渲染

基于小程序的提供的列表渲染的语法,可实现遍历数组中每个元素动态输出列表数据的需求。类似vue中的v-for

data: {
    foods: [
        {id:1, name:'臭豆腐', price:18.0},
        {id:2, name:'螺蛳粉', price:15.0},
        {id:3, name:'鲱鱼罐头', price:66.0},
        {id:4, name:'毛鸡蛋', price:5.0}
    ]
}

Vue应如下遍历输出foods

<div v-for="(item,i) in foods" :key="item.id">
	id: {{item.id}}
    name: {{item.name}}
    price: {{item.price}}
</div>

微信小程序的语法,应如下遍历输出foods

<view wx:for="{{foods}}">
    index: {{index}}
	id: {{item.id}}
    name: {{item.name}}
    price: {{item.price}}
</view>

设置后,发现控制台有一个警告,需要为wx:for提供一个wx:key

如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 input 中的输入内容,switch 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。

wx:key 的值以两种形式提供

  1. 字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
  2. 保留关键字 *this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字。

当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。

如不提供 wx:key,会报一个 warning, 如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略。

<view wx:for="{{foods}}" wx:key="id">
  序号:{{index}}
  ID: {{item.id}}
  菜名:{{item.name}}
  价格:{{item.price}}
</view>

wx:for将会为这次遍历默认新增两个变量:itemindex。这两个变量可以自定修改,语法如下:

<view wx:for="{{foods}}" wx:key="id"
      wx:for-item="f"  wx:for-index="i">
  序号:{{i}}
  ID: {{f.id}}
  菜名:{{f.name}}
  价格:{{f.price}}
</view>
条件渲染

使用条件渲染可以动态处理是否渲染某一个元素,类似vue中的v-if

data:{
    islogin: true
}
<text wx:if="{{islogin}}">欢迎:XXX</text>
<text wx:else>登录 注册</text>

常见写法有以下几种:

<text wx:if="{{条件表达式}}">xxx</text>
---------------------------------------------
<text wx:if="{{条件表达式}}">xxx</text>
<text wx:else>xxx</text>
---------------------------------------------
<text wx:if="{{条件表达式}}">xxx</text>
<text wx:elif="{{条件表达式}}">yyy</text>
<text wx:elif="{{条件表达式}}">zzz</text>
<text wx:elif="{{条件表达式}}">aaa</text>
<text wx:else>bbb</text>

小程序常用组件

radio-group组件

radiogroup组件为单选框组组件,包含一组单选按钮。基本结构:

<radio-group>
	<radio value="M" checked color="blue"></radio>
    <radio value="F" color="red"></radio>
</radio-group>

radio-group中的radio只有一个可以被选中。

案例:testing/form/form

checkbox-group组件

复选框组件,其语法:

<checkbox-group>
	<checkbox value="A" color="" checked>...</checkbox>
    <checkbox value="B" color="" checked>...</checkbox>
    <checkbox value="C" color="" checked>...</checkbox>
</checkbox-group>

小程序的事件处理

事件是视图层到逻辑层的通讯方式,它可以将用户的行为反馈到逻辑层进行后续处理。

<scroll-view bindscrolltolower=""></scroll-view>
<scroll-view bindscrolltoupper=""></scroll-view>
<scroll-view bindscroll=""></scroll-view>
<input bindinput=""/>
<image bindtap=""/>
小程序中的事件类型

微信小程序中,事件类型大致分为两大类:

  1. 冒泡事件:当一个组件上的事件被触发后,该事件会向父节点传递。
  2. 非冒泡事件:当一个组件上的事件被触发后,该事件不会向父节点传递。

WXML的冒泡事件列表:

类型触发条件
touchstart手指触摸动作开始
touchmove手指触摸后移动
touchcancel手指触摸动作被打断,如来电提醒,弹窗
touchend手指触摸动作结束
tap手指触摸后马上离开
longpress手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发了这个事件,tap事件将不被触发
longtap手指触摸后,超过350ms再离开(推荐使用longpress事件代替)
transitionend会在 WXSS transition 或 wx.createAnimation 动画结束后触发
animationstart会在一个 WXSS animation 动画开始时触发
animationiteration会在一个 WXSS animation 一次迭代结束时触发
animationend会在一个 WXSS animation 动画完成时触发
touchforcechange在支持 3D Touch 的 iPhone 设备,重按时会触发
小程序事件的绑定方式
<button bind事件名称="事件处理函数名称">按钮</button>
<button bind:事件名称="事件处理函数名称">按钮</button>
<button catch事件名称="事件处理函数名称">按钮</button>

bind的方式绑定事件可以应用于任何组件,而bind:的方式不能应用于原生组件。

原生组件指由操作系统直接控制的组件。如:获取焦点后的input组件、video组件、camera组件等。原生组件的特点是由AndroidIOS操作系统直接处理,并非微信自己实现的UI或功能。

bind方式与bind:方式绑定的事件不能阻止事件冒泡,而catch方式可以自动阻止事件冒泡。

小程序中事件参数的传递

无论组件绑定的是冒泡事件还是非冒泡事件,事件处理函数名称都严禁出现小括号:

<button bindtap="tapEvent">xxx</button>
<input bindinput="inputEvent"/>

问题:如何完成事件参数的传递?

需求:

<view>
    商品信息.....
	<button bindtap="tapDel" data-i="0">删除</button>
</view>
<view>
    商品信息.....
	<button bindtap="tapDel" data-i="1">删除</button>
</view>
<view>
    商品信息.....
	<button bindtap="tapDel" data-i="2">删除</button>
</view>
data: {
    items: [{购物项1}, {购物项2}, {购物项3}]
}

tapDel(event){
    // event.target 获取触发事件的事件源对象 --> Button对象
    // event.target.dataset 获取Button上封装了所有的data-*属性的对象
    // event.target.dataset.i 获取button组件上的data-i属性值
    let i = event.target.dataset.i
}

案例:购物车。

案例:吃饭睡觉打豆豆。

  1. 查询并显示待办事项列表。
  2. 添加新的待办事项。
  3. 删除代办事项。

小程序API

小程序界面交互类API

wx.showToast() 提示框

wx.showModal() 模态对话框

小程序路由跳转相关API

上述5中方法跳转的过程,与navigator组件的5中opentype一一对应,功能完全一致。

wx.navigateTo跳转时的参数传递问题

wx.navigateTo可以保留当前页,新建目标页,跳转过去。不能跳转到tabbar页面。在跳转的过程中可以传参,有两种传参的方案:

正向传参

假设A跳转到B,同时携带参数,A传参,B接收,这种方式为正向传参。

A页面:

wx.navigateTo({
    url: '/pages/testing/b/b?id=10&name=张三&pwd=1234'
})

B页面:

Page({
    data: {}, 
    // 系统自动调用,options系统自动传入
    // options封装了上一个页面传进来的参数,在此使用options形参接收
    onLoad(options){  
        
    }
})

反向传参

假设A跳转到B,在B页面中进行操作的时候,将参数回传给A,这种方式为反向传参。

A页面,定义一个事件处理函数,接收B返回回来的数据:

wx.navigateTo({
    url: 'xxx',
    events: {
        acceptCity(data){
            console.log('接受到了回传回来的数据',data)
        }
    }
})

B页面处理完业务后,通过事件通道(EventChannel)回传数据:

let ec = this.getOpenerEventChannel()
ec.emit('acceptCity', 回传的数据)

小程序的生命周期

  1. 页面的生命周期
  2. 小程序应用的生命周期
页面的生命周期

小程序页面的生命周期相关钩子方法需要在Page.js中进行定义,基本结构如下:

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

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

  /** 生命周期函数--监听页面初次渲染完成 */
  onReady: function ()  
  },

  /** 生命周期函数--监听页面显示 */
  onShow: function () { 
  },

  /** 生命周期函数--监听页面隐藏 */
  onHide: function () { 
  },

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

小程序的生命周期

  1. 页面的生命周期
  2. 小程序应用的生命周期
页面的生命周期

小程序页面的生命周期相关钩子方法需要在Page.js中进行定义,基本结构如下:

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

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

  /** 生命周期函数--监听页面初次渲染完成 仅执行一次 */
  onReady: function ()  
  },

  /** 生命周期函数--监听页面显示 执行多次 */
  onShow: function () { 
  },

  /** 生命周期函数--监听页面隐藏 执行多次 */
  onHide: function () { 
  },

  /** 生命周期函数--监听页面卸载 仅执行一次 */
  onUnload: function () { 
  },
})
小程序应用的生命周期

整个微信小程序从启动到销毁也会涉及到生命周期,称为小程序应用的生命周期。涉及到的相关生命周期钩子方法需要在app.js中进行定义:

// app.js
App({
  onLaunch(){    /** 当应用冷启动时(无运行状态中启动),执行 */

  },
  onShow(){      /** 当小程序显示时执行 */

  },
  onHide(){      /** 当小程序隐藏到后台时执行 */
    
  },
    
  globalData: {
	// 全局共享数据存储区
  }  
})

如果需要在页面中访问globalData,操作方式如下:

globalData中存数据:

getApp().globalData.cityname = '北京'
getApp().globalData.userInfo = {id:1, name:xxx,...}

globalData中取数据:

getApp().globalData.cityname
getApp().globalData.userInfo.id
getApp().globalData.userInfo.name

小程序网络相关API

小程序对于发送请求时的一些限制:

  1. 请求资源路径只支持 https协议。
  2. 必须使用域名,不能使用IP。域名必须经过ICP备案。
  3. 域名必须在小程序后台管理网站中注册。(登录管理后台,选择开发管理、开发设置、服务器域名配置,新增:https://api.tedu.cn)注册完毕的域名才可以在小程序中向该地址发送请求。

在浏览器中验证一个请求资源路径:

https://api.tedu.cn/index.php?cid=1

如果在公司网络访问不了,尝试修改DNS

打开资源管理器 – 右键 网络 – 更改适配器设置

右键所使用的网卡 – 属性 – 双击选择 TCP/IP V4

使用下方的DNS服务器地址:114.114.114.114 – 确定 – 确定.

wx.request()
wx.request({
    url: '',
    data: '',
    method: '',
    header: '',
    success: (res)=>{},
    fail: (err)=>{},
    completed: (com)=>{}
})

案例:点击按钮,发送请求

项目案例:

初始化项目

  1. 新建项目。

    1. 项目目录不准有空格、中文特殊字符。最后一个目录为空目录。

    2. 选择正确的appid

    3. 选择不适用云服务。

  2. 搭建项目的主体结构。

    1. 准备3个页面:indextheatreme
    2. 拷贝所有相关资源,搭建底部选项卡的基本结构。
  3. 项目细节调整。

    在小程序目录里找到 project.config.json,找到 setting 配置对象,将 checkSiteMap 设置为 false

初始化首页时加载电影列表

实现思路

  1. 重写Index.js中的onLoad方法。在页面初次加载时发请求,获取热映类别下的首页电影列表数据。
  2. 通过wx:for,完成电影列表渲染。
电影列表数据接口
说明
接口地址https://api.tedu.cn/index.php
请求方式GET
请求参数cid : 类别ID 热映ID:1 待映ID:2 经典ID:3
offset : 读取记录时的起始下标位置
返回值相应类别下的电影列表。

访问不同类别的首页数据:

https://api.tedu.cn/index.php?cid=1     热映电影列表 首页
https://api.tedu.cn/index.php?cid=2     待映电影列表 首页
https://api.tedu.cn/index.php?cid=3     经典电影列表 首页

访问不同类别的后续数据:

https://api.tedu.cn/index.php?cid=1&offset=20  热映电影列表
https://api.tedu.cn/index.php?cid=2&offset=20  待映电影列表
https://api.tedu.cn/index.php?cid=3&offset=20  经典电影列表

所以当前接口将会返回相应类别下的电影列表数据,返回从offset位置开始向后读取20条电影信息组成的数组。

[{电影},{电影},{电影},{电影},{电影},{电影}......]

控制顶部导航的选中项

实现思路

  1. 当选择某一个顶部导航项后,将该导航项改为激活状态,其它导航项改为默认样式。
  2. 选择某一个顶部导航后,需要获取当前选中项的类别cid(1/2/3),向服务端发送请求,获取响应类别下的首页电影列表数据。
  3. 获取数据后,重新更新电影列表即可。

触底加载下一页

需求:当列表滚动到底部后,加载当前类别的下一页数据。

实现思路

  1. 监听列表滚动触底事件。(Page中重写onReachBottom
  2. 整理请求参数,cid类别idoffset起始位置。发送新的列表请求,访问下一页电影列表数据。
  3. 当加载到新数据后,把新电影列表追加到旧电影列表的末尾。

封装loadData方法

// 该方法的作用:传递两个参数:cid,offset,帮忙发请求
// 返回查询得到的结果
loadData(cid, offset){
    return new Promise((resolve, reject)=>{
        wx.request({ 
          url: 'https://api.tedu.cn/index.php',
          method: 'GET',
          data: {cid: cid, offset: offset},
          success: (res)=>{
            resolve(res.data)
          }
        }) 
    })
}

onLoad(){
    this.loadData(1, 0).then(movielist=>{
        this.setData({movies: movielist})
    })
}
tapNav(){
    this.loadData(1, 0).then(movielist=>{
        this.setData({movies: movielist})
    })
}

onReachBottom(){
    this.loadData(动态cid, 动态offset).then(movielist=>{
        push.......
        this.setData()
    })
}

小程序缓存设计方案

什么是缓存?

客户端向服务端发送第一次请求试图获取一组数据,当数据下载完毕后,客户端可以将这些数据存入客户端本地缓存中。当下次发送相同请求时,先去本地缓存中搜索,看以前有没有存过,如果有,则直接获取后加载显示;如果没有,再发请求。

所以缓存的机制并不复杂,关键是找对时机进行缓存的存储与读取。

在小程序中如何处理缓存?

微信小程序将html5中的webstorage封装了,提供了一些wxAPI用于向storage中存,从storage中取。

缓存的设计方案

一个项目如何实现缓存还需要注重这个项目的业务形态。不同的业务,本地缓存到底存多久需要思考讨论。最终设计一个比较合理的更新缓存的方案。

一般列表展示都会配套一个下拉刷新来更新列表、更新缓存。

有些应用,每次打开时(App.onLaunch())将数据缓存清空。

有些应用,更新的频率更高些,就需要在应用使用时,每隔一段时间更新数据缓存。

基于下拉刷新更新缓存

  1. 在页面的.json配置文件中,开启当前页面的下拉刷新。
  2. 重写PageonPullDownRefresh方法,监听下拉刷新事件。
  3. 发送请求,加载当前了类别的首页电影数据,更新列表、更新缓存。

小程序位置服务

获取小程序的位置,可以使用:

wx.getLocation({
    type:'gcj02',
    altitude: true, 
    isHighAccuracy: true,
    success:  (res)=>{ .... }
})

app.json中配置权限声明:

{
  "pages": ["pages/index/index"],
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
    }
  }
}

如果希望获取当前城市名称等业务数据,就需要接入第三方位置服务。小程序天然配套腾讯位置服务。

腾讯位置服务

打开腾旭位置服务的官方网站:https://lbs.qq.com

开发文档 => 微信小程序JS SDK

Hello world!

  1. 申请开发者密钥(key):申请密钥

  2. 开通webserviceAPI服务:控制台 ->应用管理 -> 我的应用 ->添加key-> 勾选WebServiceAPI -> 保存

    (小程序SDK需要用到webserviceAPI的部分服务,所以使用该功能的KEY需要具备相应的权限)

  3. 下载微信小程序JavaScriptSDK,微信小程序JavaScriptSDK v1.1 JavaScriptSDK v1.2

  4. 安全域名设置,在小程序管理后台 -> 开发 -> 开发管理 -> 开发设置 -> “服务器域名” 中设置request合法域名,添加https://apis.map.qq.com

  5. 小程序示例

    // 引入SDK核心类,js文件根据自己业务,位置可自行放置
    var QQMapWX = require('../../libs/qqmap-wx-jssdk.js');
    var qqmapsdk = new QQMapWX({
        key: '申请的key'
    });
    qqmapsdk.reverseGeocoder({
        success: (res)=>{....}
    })
    

显示电影详情页

需求:当点击某一个电影列表项后,跳转到电影详情页面,显示当前电影的详细信息。(需要通过电影ID,获取电影详情) 。

实现思路

  1. 准备好电影详情页面。pages/movie/movie
  2. 点击电影列表项后,跳转到电影详情页,并且传递选中项的电影id
  3. 在详情页中获取id参数,通过id查询电影详细信息,渲染页面。
通过电影ID查询电影详情接口
说明
接口地址https://api.tedu.cn/detail.php
请求方式GET
请求参数id: 电影ID
返回结果object类型,返回电影的详细数据。
cover: "https://p1.meituan.net/movie/f6ec2a022d3644ef493f881d359f65303190471.jpg@218w_300h_1e_1c"
description: "如果你喜欢的女孩,得了抑郁症,你该怎么办?辛唐(孙晨竣 饰)拥有通过声音给他人制造快乐的能力,但对同一人使用三次后,性命就会和此人绑定,只有对方开心,辛唐才能活命。偶然,辛唐救下准备自杀的同校网络红人吉择(章若楠 饰),两人借此绑定。吉择表面开朗,但实际患了抑郁症。辛唐最初为了活下去,费尽心思让吉择开心,而后续也真的投入深情。遗憾辛唐的秘密总会败露,而吉择暗黑的过往也在网络上被人揭开....愿爱情的温暖,能治愈抑郁的青春。"
director: [{…}]
moviename: "如果声音不记得"
movietype: "爱情/青春/奇幻"
score: "8.2"
showingon: "2020-12-04"
star: "章若楠/孙晨竣/王彦霖"
thumb: (29) ['', ''......]

完成电影详情的页面渲染

渲染基本信息
渲染演职人员列表
渲染剧照列表
  1. 图片懒加载。

  2. 添加mode,防止图像比例失真。

  3. 点击图片后,全屏大图浏览器剧照列表。

    wx.previewImage({ 
        current: newUrls[i],
        urls:newUrls 
    })
    

小程序云开发

概述

开发者可以使用腾讯提供的云服务器来开发微信小程序、小游戏的服务端程序。而无需搭建服务器。

云开发提供的基础能力有:

  1. 云数据库

    云数据库是一个可以在小程序前端直接操作的云端数据库。它与mysql不同,是一个json类型的非关系型数据库。

  2. 云存储

    云存储是微信云服务器提供的一块存储空间,可以让小程序前端通过响应API代码直接针对云存储空间进行上传和下载。

  3. 云函数

    云函数是一个在小程序端进行编写,而后通过开发工具部署到云服务器中,并且提供给小程序远程调用的函数。

开通云开发服务

打开开发工具,点击工具栏中的云开发按钮。

云数据库

云数据库是一个可以在小程序前端直接操作的云端数据库。它与mysql不同,是一个json类型的非关系型数据库。

开通云开发服务

打开开发工具,点击工具栏中的云开发按钮。

云数据库

云数据库是一个可以在小程序前端直接操作的云端数据库。它与mysql不同,是一个json类型的非关系型数据库。

mysql存储数据的结构:

idnamegenderschool_id
1zsm1
2lsm2
3wwf1
school_idnamelocarea
1清华大学五道口1000
2北京大学中关村850

云数据库存储数据的结构:

[
    {
        "id":1,
        "name":'zs',
        "gender":'m',
        "school": {
            id: 1,
            name: '清华大学',
            loc: '五道口',
            area: 1000
        }
    },{
        "id":2,
        "name":'ls',
        "gender":'m',
        "school": {
            id: 1,
            name: '清华大学',
            loc: '五道口',
            area: 1000
        }
    },{
        "id":3,
        "name":'ww',
        "gender":'f',
        "school": {
            id: 2,
            name: '北京大学',
            loc: '中关村',
            area: 850
        }
    },
]

描述一个云数据库存储的数据:

在云数据库中有一个集合,里面存储了3条记录(3条文档),每一条记录包含四个字段,他们有不同的数据类型,其中school字段又是一个对象,该对象中又包含4个属性,用于描述学校的基本信息。

云数据库的操作

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/init.html

插入数据
const db = wx.cloud.database({
    // env: '云环境的ID (云开发控制台中复制过来)'
    env: 'cloud2012-9gl0qd6g8dc72b1c'
})
db.collection('test').add({
    data: {新增的对象},
    success: (res)=>{
        新增数据成功后的回调
    }
})

上述代码可以方便的向test集合中新增一条记录,我们发现,小程序将会自动为该记录分配唯一的_id(主键ID)。同时也会新增字段:_openid。不同的用户插入数据将会有不同的_openid,同一个用户添加的记录_openid是一样的。_openid字段标识了当前这一条记录属于谁(是哪一个用户创建的)。

小程序将会通过_openid字段来确定用户对该集合中数据的访问权限。

查询数据
db.collection('test').doc('记录的 _id').get({
    success: (res)=>{
        返回结果后执行回调方法。 res中就是返回的结果
    }
})
删除数据
修改数据

实现学子影院项目中的评论列表

  1. 创建一个新的云开发项目。xzyycloud
  2. xzyy中已经写好的内容,直接拽到新项目中,覆盖相关资源。
  3. comments.json中的评论数据导入comments集合中。
  4. xzyycloud项目中,找到电影详情页,在onLoad生命周期方法中,查询云数据库,获取当前电影的评论列表,遍历渲染。
Collection对象的常用方法
方法描述
collection.doc()通过id查询一条记录
collection.where()添加筛选条件
collection.get()发送请求,查询云数据库
collection.add()添加数据
collection.skip(n)跳过前n条
collection.limit(n)向后查询n条
collection.orderby()排序
collection.remove()删除
collection.update()更新
db.collection('test')
  .where({
    price: _.gt(10)
  })
  .field({
    name: true,
    price: true,
  })
  .orderBy('price', 'desc')
  .skip(1)
  .limit(10)
  .get()

查询指令

假设我们需要查询进度大于 30% 的待办事项,那么传入对象表示全等匹配的方式就无法满足了,这时就需要用到查询指令。数据库 API 提供了大于、小于等多种查询指令,这些指令都暴露在 db.command 对象上。

API 提供了以下查询指令:

查询指令说明
eq等于
neq不等于
lt小于
lte小于或等于
gt大于
gte大于或等于
in字段值在给定数组中
nin字段值不在给定数组中

动态选择首页左上角城市名称

  1. 准备一个城市列表页面: pages/citylist/citylist.

  2. 完善城市列表页中的列表显示内容。

    当点击右侧导航时,控制scrollview滚动到相应位置。

    <scroll-view scroll-into-view="C">
    	<view id="A"></view>
        <view id="B"></view>
        <view id="C"></view>
        <view id="D"></view>
        ....
    </scroll-view>
    
  3. 点击首页左上角城市时,跳转到citylist页面,选择城市。

  4. 城市选择完毕后,后退到首页,并且将选中的城市名称回传回来,更新首页城市名。

    选择城市后,将城市名称存入globalData,然后navigateBack

    在首页重写onShow生命周期方法,将会在onShow时区globalData中获取城市名称,更新左上角。

动态选择首页左上角城市名称

  1. 准备一个城市列表页面: pages/citylist/citylist.

  2. 完善城市列表页中的列表显示内容。

    当点击右侧导航时,控制scrollview滚动到相应位置。

    <scroll-view scroll-into-view="C">
    	<view id="A"></view>
        <view id="B"></view>
        <view id="C"></view>
        <view id="D"></view>
        ....
    </scroll-view>
    
  3. 点击首页左上角城市时,跳转到citylist页面,选择城市。

  4. 城市选择完毕后,后退到首页,并且将选中的城市名称回传回来,更新首页城市名。

    选择城市后,将城市名称存入globalData,然后navigateBack

    在首页重写onShow生命周期方法,将会在onShow时区globalData中获取城市名称,更新左上角。

实现城市列表页面的重新定位功能

  1. 当点击进入城市列表页后,需要重新调用getLocation进行定位。(需要把index.js中封装好的getLocation方法复制过来一份直接用)
  2. 获取到当前定位城市名后,将cityname存入globalData,更新citylist页面顶部的定位城市名称。
  3. 当点击顶部当前定位城市按钮时,需要将城市名称存入globalData,返回上一页即可。
  4. 首页将会在onShow是更新首页左上角,完成业务流程。

实现未授权状态下的定位业务

  1. 拒绝授权。
  2. 点击城市名,进入城市列表页。定位失败后提示重试。
  3. 点击重试弹出引导授权窗口。
  4. 授权成功后重新定位,完成业务。

实现影院模块业务

影院页面左上角城市与首页左上角城市实现联动
  1. 重写影院页面的onShow生命周期方法,读取globalData.cityname,更新左上角城市名。

看文档:

https://lbs.qq.com/miniProgram/jsSdk/jsSdkGuide/methodSearch

实现思路

  1. 将获取qqmapsdk的方法封装到app.js中,通过globalData暴露qqmapsdk的引用。
  2. 什么时候需要,就直接从globalData中读取qqmapsdk即可。
    1. index.js中需要。
    2. citylist.js中需要。
    3. theatre.js中需要。
  3. 通过qqmapsdk调用search方法,获取选中城市的影院列表。
  4. theatreList存入data,在页面中渲染显示该列表。
  5. 细节处理:
    1. 若影院没有电话,则样式有问题。
    2. 距离应该显示km,保留两位小数。
    3. 如果不是当前城市的电影院,不会返回距离。
event.targetevent.currentTarget之间的区别
<view class="v1" bindtap="tapV1">
	<view class="v2"></view>
</view>
tapV1(event){
    // 当用户点击了v2, 触发tapv1,那么:
    event.target // 指的是v2,因为真正直接被点击的元素是v2
    event.currentTarget // 指的是v1,因为bindtap绑定到了v1上
    
    // 当用户点击了v1,触发tapv1,那么:
    event.target // 指的是v1,因为真正直接被点击的元素是v1
    event.currentTarget // 指的是v1,因为bindtap绑定到了v1上
}

自定义组件

从小程序基础库版本 1.6.3 开始,小程序支持简洁的组件化编程。所有自定义组件相关特性都需要基础库版本 1.6.3 或更高。

开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。

<my-button></my-button>
<my-button color="#36d" 
           text="按钮上的字" 
           round 
           bind:doubletap="doubletapEvent">
</my-button>
doubletapEvent(){
	console.log('么么哒..')
}
自定义简单组件
  1. components右键,新建Component, 起名字,新建组件。

  2. 编写组件.wxml 组件.wxss

  3. 在需要引用组件的页面中,通过自定义组件名使用该组件。

    <my-button></my-button>
    
  4. 前提就是在该页面.json,需要声明引入该组件:

    {
      "usingComponents": {
        "my-button": "/components/mybutton/mybutton"
      }
    }
    

自定义组件

从小程序基础库版本 1.6.3 开始,小程序支持简洁的组件化编程。所有自定义组件相关特性都需要基础库版本 1.6.3 或更高。

开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。

<my-button></my-button>
<my-button color="#36d" 
           text="按钮上的字" 
           round 
           bind:doubletap="doubletapEvent">
</my-button>
doubletapEvent(){
	console.log('么么哒..')
}
自定义简单组件
  1. components右键,新建Component, 起名字,新建组件。

  2. 编写组件.wxml 组件.wxss

  3. 在需要引用组件的页面中,通过自定义组件名使用该组件。

    <my-button></my-button>
    
  4. 前提就是在该页面.json,需要声明引入该组件:

    {
      "usingComponents": {
        "my-button": "/components/mybutton/mybutton"
      }
    }
    

小程序Vant组件库

  1. 安装vant

    # 进入项目根目录, xzyycloud文件夹下执行命令:
    npm init   # 输入命令后一路回车,将会自动创建package.json
    npm i @vant/weapp -S --production
    
  2. 修改app.json.

    app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。

  3. 修改project.config.json

    "packNpmManually": true,
    "packNpmRelationList": [
        {
            "packageJsonPath": "./package.json",
            "miniprogramNpmDistDir": "./miniprogram/"
        }
    ],
    

    如上配置的目的,是希望小程序开发工具在构建npm时,可以找到package.json, 还需要定义编译后的输出目录(./miniprogram/)。

  4. 在小程序开发工具中构建npm

    一旦构建npm成功,将会在miniprogram目录下新增miniprogram_npm文件夹,里面就是打包好的组件源码。接下来就可以直接引入,使用。

使用Vant组件库中的Button

引入

在页面的.json文件中引入该组件:

{
  "usingComponents": {
    "my-button": "/components/mybutton/mybutton",
    "van-button": "@vant/weapp/button/index"
  }
}

使用

<van-button type="default">默认按钮</van-button>
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
<van-button type="warning">警告按钮</van-button>
<van-button type="danger">危险按钮</van-button>

显示失败的,清除编译缓存,重新编译多尝试。

实现微信登录业务

  1. 当点击登录时,申请用户授予获取用户基本信息的权限。
  2. 一旦用户允许,则可以得到用户的昵称、头像的等基本数据。
  3. 更新界面(昵称、头像)。

小程序提供了一个API方法,可以获取用户数据:wx.getUserProfile()

实现重新选择图片,更新头像

  1. 点击头像后,选择新图片。

  2. 获取选中图片的路径,替换头像路径。(userInfo.avatarUrl)

    选择图片的方法:

    wx.chooseImage()
    

微信登录业务的本质

wx.getUserProfile()的作用是让用户方便的提供个人微信昵称与头像。以此来证明当前用户的身份。而一个完整的微信登录业务,必然需要将这些用户信息存入自己家数据库中。(例如,存入mysql中的user表里)

id_openidnamephonenickNameavatarUrlgender
1ovoaabbcxuming13333…徐铭https://xx.jpgm
2aabbcdefglisi144https://xxxx.jpgf

这样将微信提供的用户信息存入到自己家数据库后,可以方便的修改昵称、修改头像、新增数据,做表关联,维护用户收藏、与用户喜欢等等信息。

所以一个正确的微信登录业务如下:

  1. 通过微信登录,获取用户的基本信息。
  2. 通过用户的信息,判断,该用户在自己家数据库中是否已注册:
    1. 如果没有注册,则在自己家数据库中新增一条数据,保存该用户信息,显示界面即可。
    2. 如果已经注册过,则找到自己家数据库中最新的用户数据(有可能头像、昵称已经被改过),将自家数据库中的信息更新到界面。

借助云数据库,来完成上述流程。

  1. 在云开发控制台中新建一个集合:users。 保存用户信息。
  2. 完成用户登录业务。
    1. 查询云数据库,检索当前用户以前是否已经注册过信息。
    2. 如果没有找到数据,则执行注册业务。将用户信息存入云数据库users集合。
    3. 如果找到了数据,则执行登录业务,将这些用户最新的数据,更新到界面中。

实现修改头像业务

当登录成功后,点击头像,选择本地图片,将头像更新为选中的图片临时路径既可以修改显示的头像。但是这种方式只是纯客户端版本的更新头像,无法持久化保存信息。意味着下次登录时,还会显示微信头像。这是有问题的。

真正的修改头像业务流程如下:

  1. 点击头像选择一张新图片,获取图片路径。

    wx.chooseImage()
    
  2. 将该头像上传至服务器,存到服务器某个目录下,并且返回可以访问它的网络路径。

    http://api.tedu.cn/avatar/23dfad6fas98df6sdf5sd8fads9f.jpg
    
  3. 获取访问路径后,将该路径更新到云数据库中users集合里当前用户的avatarUrl字段。

    let db = wx.cloud.database()
    db.collection('users').doc('_id').update({
        data: {
            avatarUrl : xxxxxx
        }
    })
    
  4. 一旦数据库中头像地址变了,下次登录时将会加载最新的头像路径,从而访问到上传的头像图片,完成业务。

云存储

云存储类似云盘,可以在小程序客户端方便的上传、下载文件。可以在云开发控制台中测试相应功能。

小程序提供了API,方便的实现文件上传操作:

wx.cloud.uploadFile({
    filePath: '本地文件路径',
    cloudPath: '云存储服务端的目标文件路径',
    success: (res)=>{}
})

思考:如果没有使用云数据库,使用的是自己家的mysql数据库存储用户数据,那么如何完成头像路径的更新?

没有使用小程序云数据库意味着什么?

  1. 注册用户时,向自建的users表中添加一条新纪录。

    insert into users(id, nickName, avatarUrl, gender, ) values(?,?,?,?)
    
  2. 注册业务看似简单,但是有个棘手的问题,注册之前得先判断users表中是否包含当前用户的信息?

    答案:通过每个用户每个应用唯一的_openid来解决这个问题。

如果是自建数据库,需要维护用户id_openid)。用户一旦进入小程序,就会被分配一个openid。获取openid的方式比较麻烦:

  1. 客户端需要先调用wx.login方法,获取一个校验码。
  2. 将校验码发给后端nodejs接口,后端将会通过该校验码,访问腾讯服务器,换取openid返回给客户端。

云开发环境下提供了云函数,可以方便的获取用户的openid

云函数

云函数是一种在小程序端编写、定义,通过开发工具部署到云服务器中,在小程序端可以远程调用的函数。这种函数在云服务器中执行。所以云函数可以简单替代nodejs后端接口。

体验云函数。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值