快速开始

注册账号

前往微信公众平台https://mp.weixin.qq.com/

下载微信开发者工具

https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

创建第一个项目

使用js基础模板

项目结构概述

JSON文件总结

app.json

是当前小程序的全局配置,包括了小程序的所有页面路径、窗口外观、界面表现、底部 tab等。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
//放页面路径
"pages": [
],
"window": {
},
"tabBar": {
},
//全局注册的组件
"usingComponents": {},
//指定分包
"subPackages":[]
}

页面文件创建:只需要在 app.json-> pages新增页面的存放路径并保存,小程序开发者工具即可帮我们自动创建对应的页面文件

修改首页:只需要调整app.json-> pages字符串数组中页面路径的前后顺序,即可修改项目的首页。小程序会把排在第一位的页面,当作项目首页进行渲染。

window

1
2
3
4
5
6
7
8
9
10
//app.json
{
....
"window": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#fff",
"backgroundColor": "#F8F8F8"
}
....
}

小程序窗口的组成部分包括:导航栏区域,背景区域,页面主体区域

navigationBar(导航栏)

  • navigationBarTitleText:修改导航栏的标题
  • navigationBarBackgroundColor:修改导航栏的背景色,只支持16进制颜色,不能设置为red,blue等
  • navigationBarTextStyle:设置标题颜色,可选值只能是black和white(为什么这里又能直接写颜色了?)

background系列(下拉页面才会显示的部分的背景配置,前提是页面开启了下拉刷新)

  • backgroundColor:指定16进制的颜色值比如#efefef
  • backgroundTextStyle:只支持dark和light。用于修改下拉刷新的加载样式(颜色)

onReachBottomDistance

触底加载的距离,默认为50px,当滚动条距离底部50px的时候触发触底刷新

在页面中配置onReachBottom就能指定触底后的回调函数

enablePullDownRefresh

设置为true,表示允许下拉刷新,然后在页面中配置onPullDownRefresh,就能指定下拉刷新的回调函数。调用wx.stopPullDownRefresh就能关闭下拉刷新的加载样式

要注意的是onPullDownRefreshonReachBottomPage 的生命周期函数,不属于 Component。

tabBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//app.json
{
....
"tabBar": {
"selectedColor": "#28b389",
"backgroundColor": "#ffffff",
"color":"black"
"list": [
{
"pagePath": "pages/home/home",
"iconPath": "static/home.png",
"selectedIconPath": "static/home-h.png",
"text": "推荐"
},
{
"pagePath": "pages/classify/classify",
"iconPath": "static/classify.png",
"selectedIconPath": "static/classify-h.png",
"text": "分类"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/user.png",
"selectedIconPath": "static/user-h.png",
"text": "我的"
}
]
}
....
}
  • selectedlconPath:选中时显示的图标路径
  • backgroundColor:tabbar的背景颜色
  • iconPath:未选中时显示的图标路径
  • color:tab上文字的默认(未选中)颜色
  • selectedColor: tab 上的文字选中时的颜色
  • pagePath:tab对应的页面路径,点击tab会跳转到指定页面。

注意

tabBar中只能配置最少2个、最多5个tab页签(list数组里的元素数目等于tab数目)

当渲染顶部 tabBar时,不显示 icon,只显示文本

自定义tabbar

app.jsontabBar字段中新增custom:true属性

然后默认的tabbar就会消失,此时需要开发者提供一个自定义组件来渲染 tabBar,所有 tabBar 的样式都由该自定义组件渲染。与 tabBar 样式相关的接口,如wx.setTabBarItem 等将失效。

在项目根目录下新建一个custom-tab-bar组件,包含以下文件

1
2
3
4
custom-tab-bar/index.js
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss

然后在所有 tab 页的 json 里需声明 usingComponents 项,但是不需要实际注册这个组件

1
2
3
{
"usingComponents": {}
}

这个组件不需要手动在每个tabbar页面中引入和注册,会自动使用

每个 tab 页下的自定义 tabBar 组件实例是不同的,在每个tabbar页面中调用this.getTabBar(),就能获取到这个页面下的自定义tabbar组件实例

1
2
3
this.getTabBar().setData({
selected: 1
})

页面的json文件

1
2
3
4
5
6
7
8
9
10
11
{
"navigationBarTitleText": "分类",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
//局部组件注册
"usingComponents": {
"nav-bar": "../../components/nav-bar/nav-bar",
"theme-item": "../../components/theme-item/theme-item",
"uni-load-more": "../../uni_modules/uni-load-more/components/uni-load-more/uni-load-more"
}
}

小程序中的每一个页面,可以使用json文件,来对本页面的窗口外观进行配置,页面中的配置项会覆盖app.json的 window属性中相同的配置项。

project.config.json

通常大家在使用一个工具的时候,都会针对各自喜好做一些个性化配置,例如界面颜色、编译配置等等,当你换了另外一台电脑重新安装工具的时候,你还要重新配置。

考虑到这点,小程序开发者工具在每个项目的根目录都会生成一个 project.config.json,你在工具上做的任何配置都会写入到这个文件,当你重新安装工具或者换电脑工作时,你只要载入同一个项目的代码包,开发者工具就自动会帮你恢复到当时你开发项目时的个性化配置,其中会包括编辑器的颜色、代码上传时自动压缩等等一系列选项。

简单的来说这个文件就是用来保存,恢复个性化配置的。

sitemap.json

站点地图,微信现已开放小程序内搜索,效果类似于PC网页的SEO(搜索引擎优化)。该文件用来配置小程序页面是否允许微信索引。

JSON 语法

这里说一下小程序里JSON配置的一些注意事项。

JSON文件都是被包裹在一个大括号中 {},通过key-value的方式来表达数据。JSON的Key必须包裹在一个双引号中,在实践中,编写 JSON 的时候,忘了给 Key 值加双引号或者是把双引号写成单引号是常见错误。

JSON的值只能是特定的格式。

还需要注意的是 JSON 文件中无法使用注释,试图添加注释将会引发报错

js文件总结

app.js

是整个小程序项目的入口文件,通过调用App()函数来启动整个小程序

1
createApp().app.mount("#app");

页面的js文件

是页面的入口文件,页面的脚本文件,存放页面的数据、事件处理函数等

通过调用Page()函数来创建并运行页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Page({
data: {
text: "This is page data."
},
onLoad: function(options) {
// 页面创建时执行
},
onShow: function() {
// 页面出现在前台时执行
},
onTabItemTap(item) {
// tab 点击时执行
console.log(item.index)
console.log(item.pagePath)
console.log(item.text)
},
// 事件响应函数
viewTap: function() {
this.setData({
text: 'Set some data for updating view.'
}, function() {
// this is setData callback
})
},
// 自由数据
customData: {
hi: 'MINA'
}
})

这么看来微信小程序的代码风格不也是选项式风格

框架接口

小程序App

App

注册小程序。接受一个 Object 参数,其指定小程序的生命周期回调等。

App() 必须在 app.js 中调用,必须调用且只能调用一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.js
App({
onLaunch (options) {
// Do something initial when launch.
},
onShow (options) {
// Do something when show.
},
onHide () {
// Do something when hide.
},
onError (msg) {
console.log(msg)
},
globalData: 'I am global data'
})

getApp

整个小程序只有一个 App 实例,是全部页面共享的。开发者可以通过 getApp 方法获取到全局唯一的 App 实例,获取App上的数据或调用开发者注册在 App 上的函数。

1
2
3
// xxx.js
const appInstance = getApp()
console.log(appInstance.globalData) // I am global data

页面

Page

注册小程序中的一个页面。接受一个 Object 类型参数,其指定页面的初始数据(data)、生命周期回调、事件处理函数等。

data 是页面第一次渲染使用的初始数据。页面加载时,data 将会以JSON字符串的形式由逻辑层传至渲染层,因此data中的数据必须是可以转成JSON的类型:字符串,数字,布尔值,对象,数组

getCurrentPages

用来获取当前页面栈, 返回一个页面实例数组,按栈顺序排列(栈底 → 栈顶)

pages[0]:栈底页面(通常是首页)
pages[pages.length - 1]:当前页面(栈顶)

1
2
const currentPage = getCurrentPages().pop(); // 或 [length - 1]
currentPage.setData({ title: '更新标题' }); // 直接操作当前页数据

自定义组件

Behavior

注册一个 behavior,接受一个 Object 类型的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = Behavior({
behaviors: [],
properties: {
myBehaviorProperty: {
type: String
}
},
data: {
myBehaviorData: {}
},
attached: function(){},
methods: {
myBehaviorMethod: function(){}
}
})

Component

创建自定义组件,接受一个 Object 类型的参数

模块化

在微信小程序中使用的是CommonJS模块化语法

require

引入模块。返回模块通过 module.exportsexports 暴露的接口。需要引入其他分包的模块的时候,可以通过配置 callback 回调函数来异步获取指定模块。异步获取失败的时候,将会触发 error 回调函数。

名称类型必填说明
pathstring需要引入模块文件相对于当前文件的相对路径,或npm模块名,或npm模块路径。默认不支持绝对路径,可通过配置 resolveAlias 自定义路径映射。
callbackfunction异步加载成功回调函数,该回调函数参数为成功加载的模块。
errorfunction异步加载失败回调函数,该回调函数参数为错误信息和模块名

同一包内调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// common.js
function sayHello(name) {
console.log(`Hello ${name} !`)
}
function sayGoodbye(name) {
console.log(`Goodbye ${name} !`)
}

module.exports.sayHello = sayHello
exports.sayGoodbye = sayGoodbye

var common = require('common.js')
Page({
helloMINA: function() {
common.sayHello('MINA')
}
})

跨分包异步调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// subpackage/common.js 分包 common 文件
export const sayHello = () => console.log("hello")
// pages/index.js 主包页面

let common;
require('../../subpackage/common.js', (mod) => {
common = mod
}, ({ errMsg, mod }) => {
console.error(`path: ${mod}, ${errMsg}`)
})

Page({
sayHello() {
common && common.sayHello()
}
})

module:当前模块对象

exports:module.exports 的引用

事件

什么是事件

事件是视图层到逻辑层的通讯方式。事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。

事件对象可以携带额外信息,如 id, dataset, touches

事件分类

事件分为冒泡事件和非冒泡事件:

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

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

在微信小程序中,事件流被简化了,只有目标阶段和冒泡阶段,但是后续触摸类事件支持捕获阶段

事件绑定

使用bind或者catch关键字来绑定事件,比如bindtap或者bind:tap,catch:tap。与 bind 不同, catch阻止事件向上冒泡

例如在下边这个例子中,点击 inner view 会先后调用handleTap3handleTap2(因为tap事件会冒泡到 middle view,而 middle view 阻止了 tap 事件冒泡,不再向父节点传递),点击 middle view 会触发handleTap2,点击 outer view 会触发handleTap1

1
2
3
4
5
6
7
8
9
<view id="outer" bindtap="handleTap1">
outer view
<view id="middle" catchtap="handleTap2">
middle view
<view id="inner" bindtap="handleTap3">
inner view
</view>
</view>
</view>

要注意的是,在微信小程序中绑定事件回调的时候,不能传参,比如下面的写法就是错误的:

1
<view id="outer" bindtap="handleTap(1)">

想要传递参数,可以通过在标签上添加data-xxx属性值,这些属性值在事件对象的e.target.dataset中可以访问到。

1
<view bind:tap="tip" data-a="{{1}}" data-b='2'> i love you</view>

其中e.target.dataset.a的值是数字,e.target.dataset.b的值是字符串。

常见的事件

tap

手指触摸后马上离开,类似于HTML中的click 事件

绑定方式:bindtap 或 bind:tap

1
2
3
<view class="container">
<button bindtap="handleTap">点击我</button>
</view>
1
2
3
4
5
6
Page({
handleTap(e) {
console.log('按钮被点击了');
// 可以在这里执行其他逻辑,如发起网络请求或更新页面状态
}
});

input

文本框输入事件

绑定方式:bindinput或 bind:input

通过e.detail.value获得最新输入的值,不同于js的e.target.value

1
2
3
<view class="container">
<input type="text" placeholder="输入内容" bindinput="handleInput" />
</view>
1
2
3
4
5
6
7
Page({
handleInput(e) {
const inputValue = e.detail.value;
console.log('当前输入值:', inputValue);
// 更新页面状态或其他操作
}
});

实现数据的双向绑定,参考vue中v-model的实现即可。

change

状态改变时触发,绑定方式同上

1
2
3
<view class="container">
<switch bindchange="handleChange" />
</view>
1
2
3
4
5
6
7
Page({
handleChange(e) {
const isChecked = e.detail.value;
console.log('开关状态改变为:', isChecked ? '开启' : '关闭');
// 根据开关状态执行相应逻辑
}
});

事件对象

当组件触发事件时,逻辑层绑定该事件的处理函数,会收到一个事件对象

基础事件对象属性列表

属性类型说明
typeString代表事件的类型,比如点击事件的事件类型就是tap
timeStampInteger页面打开到触发事件所经过的毫秒数
targetObject触发事件的源组件的一些信息。target.id表示源组件的id,target.dataset表示事件源组件上由data-开头的自定义属性组成的集合
currentTargetObject当前组件的一些属性值集合
markObject事件标记数据

在组件节点中可以附加一些自定义数据。这样,在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理。

在 WXML 中,这些自定义数据以 data- 开头,多个单词由连字符 - 连接。这种写法中,连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符。如:

  • data-element-type ,最终会呈现为 event.currentTarget.dataset.elementType
  • data-elementType ,最终会呈现为 event.currentTarget.dataset.elementtype

detail

一般的事件对象都有的属性,补充这次事件的更多信息

WXS响应事件

有频繁用户交互的效果在小程序上表现是比较卡顿的,例如页面有 2 个元素 A 和 B,用户在 A 上做 touchmove 手势,要求 B 也跟随移动。一次 touchmove 事件的响应过程为:

a、touchmove 事件从视图层(Webview)抛到逻辑层(App Service)

b、逻辑层(App Service)处理 touchmove 事件,再通过 setData 来改变 B 的位置

一次 touchmove 的响应需要经过 2 次的逻辑层和渲染层的通信以及一次渲染,通信的耗时比较大。

本方案基本的思路是减少通信的次数,让事件在视图层(Webview)响应,也就是将事件处理写在WXS中,目前只能响应内置组件的事件,不支持自定义组件事件。WXS 函数的除了纯逻辑的运算,还可以通过封装好的ComponentDescriptor 实例来访问以及设置组件的 class 和样式,对于交互动画,设置 style 和 class 足够了。WXS 函数的例子如下:

1
2
3
4
5
6
7
8
9
10
var wxsFunction = function(event, ownerInstance) {
var instance = ownerInstance.selectComponent('.classSelector') // 返回组件的实例
instance.setStyle({
"font-size": "14px" // 支持rpx
})
instance.getDataset()
instance.setClass(className)
// ...
return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault
}

其中入参 event ,在小程序基础事件对象上多了 event.instance ,来表示触发事件的组件的 ComponentDescriptor 实例。ownerInstance 表示的是触发事件的组件,所在的组件的 ComponentDescriptor 实例,如果触发事件的组件是在页面内的,ownerInstance 表示的是页面实例。

1
2
<wxs module="test" src="./test.wxs"></wxs>
<view bindtouchmove="{{test.touchmove}}" class="movable"></view>

模板语法

数据绑定

页面数据写在js文件中,具体在配置对象的data属性

1
2
3
4
5
Page({
data: {
info: 'init data'
}
})

wxml文件中使用:

1
2
<image src="{{imgSrc}}"></image>
<view>{{info}}</view>

绑定数据和属性需要使用{{}},但是进行事件回调绑定时不需,如 bindtap, bindscroll直接写函数名,不加 {{}}

微信数据绑定{{}}中只支持简单数据绑定和三元运算符,不支持函数调用、方法执行、复杂表达式,比如<view>{{ formatDate(date) }}</view> 这种语法是错误的。但是模板中能调用wxs中的函数。所以wxs中的函数通常用作过滤器。

1
2
3
4
5
6
7
<view>{{m.sum(1,2)}}</view>
<wxs module="m">
function sum(a,b){
return a+b
}
module.exports.sum = sum; //暴露出去
</wxs>

但是绑定事件的时候,又不能使用wxs中的函数,比如下面这种写法是错误的:

1
<button bindtao="m2.toLower"></button>

条件渲染

wx:if/wx:elif/wx:else

1
2
3
4
<view wx:if="{{view == 'WEBVIEW'}}"> WEBVIEW </view>
<view wx:elif="{{view == 'APP'}}"> APP </view>
<view wx:elif="{{view == 'MINA'}}"> MINA </view>
<view wx:else> UNKNOWN </view>
1
2
3
4
5
6
// page.js
Page({
data: {
view: 'MINA'
}
})

block组件

类似vue中的template,不会被渲染出来,可以避免渲染一些不必要的结点,可以用来包裹部分结构,从而批量控制显示隐藏

hidden

类似vue中的v-show,不一样的是这里值为true的话是隐藏,v-show是显示

列表渲染

  • wx:for:传入一个数组

  • wx:for-item:自定义当前项的变量名,这个变量名默认是item,在实际开发中用的不多

  • wx:for-index:自定义当前循环项的索引的变量名,这个变量名默认是index,在实际开发中用的不多

  • wx:key:类似vue中的key,如果数组item中有id属性,直接令wx:key="id"即可,不需要写成{{item.id}},否则会报错,后者是模仿vue的语法

示例

1
2
3
<view wx:for="{{arr}}">
索引是: {{index}}, item项是:{{item}}
</view>
1
2
3
<view wx:for="{{arr1}}" wx:for-index="idx" wx:for-item="itemName" wx:key="idx">
索引是: {{idx}}, item项是: {{itemName}}
</view>

数据同步setData

通过调用this.setData方法,可以修改data中的数据,并通知视图更新。需要注意的是this.setData进行的是更新操作,不是替换操作。

修改某个对象的某个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Component({
data: {
cnt:0,
rgb:{
r:0,
g:0,
b:0
}
},
methods: {
changeR(){
this.setData({
"rgb.r":this.data.rgb.r+1
})
},
changeG(){
this.setData({
"rgb.g":this.data.rgb.g+1
})
},
changeB(){
this.setData({
"rgb.b":this.data.rgb.b+1
})
}
}
})

wxml与html的区别

标签名称不同

1
2
HTML ( div, span, img, a)
wxml (view, text, image, navigator)

往往写 HTML 的时候,经常会用到的标签是 div, p, span,开发者在写一个页面的时候,可以根据这些基础的标签组合出不一样的组件,例如日历、弹窗等等。换个思路,既然大家都需要这些组件,为什么我们不能把这些常用的组件包装起来,大大提高我们的开发效率。从上边的例子可以看到,小程序的 WXML 用的标签是 view, button, text 等等,这些标签其实就是一个个封装好的组件,我们还提供了地图、视频、音频等等组件能力

多了一些 wx:if 这样的属性以及 双大括号 这样的表达式

在网页的一般开发流程中,我们通常会通过 JS 操作 DOM ,以引起界面的一些变化响应用户的行为。例如,用户点击某个按钮的时候,JS 会记录一些状态到 JS 变量里边,同时通过 DOM API 操控 DOM 的属性或者行为,进而引起界面一些变化。当项目越来越大的时候,你的代码会充斥着非常多的界面交互逻辑和程序的各种状态变量,显然这不是一个很好的开发模式,因此就有了 MVVM 的开发模式(例如 React, Vue),提倡把渲染和逻辑分离。简单来说就是不要再让 JS 直接操控 DOMJS 只需要管理状态即可,然后再通过一种模板语法,来描述状态和界面结构的关系即可

仅仅通过数据绑定,还不够完整的描述状态和界面的关系,还需要 if/else, for等控制能力,在小程序里边,这些控制能力都用 wx: 开头的属性来表达。

wxss与css的区别

  • 新增了rpx尺寸单位

    CSS中需要手动进行像素单位换算,例如rem,而wxss在底层支持新的尺寸单位rpx,在不同大小的屏幕上小程序会自动进行换算。

    鉴于不同设备屏幕的大小不同,为了实现屏幕的自动适配,rpx把所有设备的屏幕,在宽度上等分为750份(即:当前屏幕的总宽度为750rpx)。小程序在不同设备上运行的时候,会自动把 rpx的样式单位换算成对应的像素单位(px)来渲染,从而实现屏幕适配

  • wxss仅支持部分CSS选择器,目前支持的选择器有:

    选择器样例样例描述
    .class.intro选择所有拥有 class=”intro” 的组件
    #id#firstname选择拥有 id=”firstname” 的组件
    元素选择器view选择所有 view 组件
    element, elementview, checkbox选择所有文档的 view 组件和所有的 checkbox 组件
    ::afterview::after在 view 组件后边插入内容
    ::beforeview::before在 view 组件前边插入内容
  • 提供了全局的样式和局部样式。项目根目录中的app.wxss会作用于所有小程序页面,而且不需要手动在每个页面导入这个文件,而页面的wxss样式仅对当前页面生效,并会覆盖 app.wxss 中相同的选择器。

框架组件上支持使用 style、class 属性来控制组件的样式,就好像普通的html标签一样。静态的样式统一写到 class 中。style 接收动态的样式,在运行时会进行解析,尽量避免将静态的样式写进 style 中,以免影响渲染速度。

wxs与js的区别

在微信小程序项目中,核心文件还是JS文件,而不是wxs 文件

限制

  • 只支持js的部分语法,不支持es6语法,WXS 是一个受限的 JavaScript 子集
  • wxs不能调用js中定义的函数,不能调用小程序提供的API,WXS 运行在视图层的独立 JavaScript 引擎中,小程序 API(wx.xxx)只在逻辑层可用

特点

  • wxs 代码可以编写在wxml文件中的<wxs>标签内

  • wxml文件中的每个<wxs></wxs>标签,必须提供 module属性,用来指定当前wxs 的模块名称,方便在wxml中访问模块中的成员。

    必须提供src属性,用来指定要引入的脚本的路径,且必须是相对路径

  • wxs代码还可以编写在以.wxs为后缀名的文件内

  • 绑定事件不能使用wxs中的函数

  • wxs遵守cjs规范

1
2
3
4
5
6
7
<wxs module="m1">
var msg = "hello world";

module.exports.message = msg; //暴露出去
</wxs>

<view> {{m1.message}} </view>

组件

视图组件

view

普通视图区域,类似于HTML中的div,是一个块级元素常用来实现页面的布局效果

scroll-view

可滚动的视图区域,常用来实现滚动列表效果;添加scroll-y属性,允许y轴滚动。添加scroll-x属性,允许x轴滚动

但是view+overflow: auto;不也能实现滚动效果吗,为什么还要借助scroll-view?因为scroll-view提供了许多其他的关键能力。

比如可以通过 scroll-top或者sroll-left属性设置滚动条的位置,通过refresher-enabled控制是否下拉刷新,通过 bindscroll监听滚动事件。

swiperswiper-item

swiper的子元素是一个个swiper-item组件,swiper-item组件宽高自动设置为100%

轮播图组件,常用属性:

  • indicator-dots:添加导航条,默认值false
  • indicator-color:指示点颜色
  • indicator-active-color:当前选中的指示点颜色
  • autoplay:是否自动切换,默认为false
  • interval:自动切换时间间隔(单位ms)
  • circular:是否采用衔接滑动,默认为false

text

添加了selectable属性才能实现长按选择复制等操作

rich-text

通过rich-text组件的nodes属性节点,把HTML字符串渲染为对应的结构,就像vue里的v-html

这段代码会将 <h1> 元素的内容渲染到 <rich-text> 组件内部,而不是替换整个 <rich-text> 标签

1
<rich-text nodes="<h1 style='color: red; '>标题</h1>"></rich-text>

其他组件

button

功能比HTML中的button 按钮丰富

属性:

  • type:指定按钮的类型,颜色,有点像前端框架里的语法了

  • size:指定按钮的大小

  • plain:添加了就具有镂空样式

  • open-type:通过这个属性可以调用微信提供的各种功能(客服、转发、获取用户授权、获取用户信息等),比如

    1
    <button open-type="getUserInfo" bindgetuserinfo="tip">我是谁</button>

    点击按钮后在tip函数回调中就能拿到用户信息。

image

组片组件,有默认宽高(300*150)

mode属性(前3个修改图片大小,后2个修改image容器大小)

  • scaleToFill:(默认值)缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满image元素。没有空隙,没有裁剪,但是会失真)
  • aspectFit:缩放模式,保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。(就是contain)。没有裁剪,没有失真,但是可能有空隙。
  • aspectFill:缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。(就是cover)。有裁剪,但是没有失真,没有空隙。
  • widthFix:缩放模式,容器的宽度不变,高度自动变化,保持原图宽高比不变
  • heightFix:缩放模式,容器的高度不变,宽度自动变化,保持原图宽高比不变

与img区别

微信中的image更像一个容器,src指定的图片是背景图片,前3个属性值在修改背景图片大小,后2个属性在修改**容器(image元素)**的大小。

img的大小就是src指定的图片的大小。其实img就相当于mode=widthFix的image标签

获取界面上的节点信息

节点信息查询 API (createSelectorQuery) 可以用于获取节点属性、样式、在界面上的位置等信息

最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。

示例代码:

1
2
3
4
5
6
7
8
const query = this.createSelectorQuery()
query.select('#the-id').boundingClientRect(function(res){
res.top // #the-id 节点的上边界坐标(相对于显示区域)
})
query.selectViewport().scrollOffset(function(res){
res.scrollTop // 显示区域的竖直滚动位置
})
query.exec() //它不是同步 API,必须调用 .exec() 才会执行查询。

上述示例中, #the-id 是一个节点选择器,与 CSS 的选择器相近但略有区别

在自定义组件或包含自定义组件的页面中,推荐使用 this.createSelectorQuery (作用域是当前组件/页面的作用域内)来代替 wx.createSelectorQuery(作用域是整个页面) ,这样可以确保在正确的范围内选择节点。

WXML节点布局相交状态

在web中有IntersectionObserver API ,在微信小程序中也有类似的API,叫做createIntersectionObserver

这一组API涉及的主要概念如下:

  • 参照节点:监听的参照节点,取它的布局区域作为参照区域。如果有多个参照节点,则会取它们布局区域的 交集 作为参照区域。页面显示区域也可作为参照区域之一
  • 目标节点:监听的目标,默认只能是一个节点
  • 相交区域:目标节点的布局区域与参照区域的相交区域。
  • 相交比例:相交区域占目标节点的布局区域的比例。
  • 阈值:相交比例如果达到阈值,则会触发监听器的回调函数。阈值可以有多个

以下示例代码可以在目标节点(用选择器 .target-class 指定)每次进入或离开页面显示区域时

1
2
3
4
5
6
7
Page({
onLoad: function(){
this.createIntersectionObserver().relativeToViewport().observe('.target-class', (res) => {
console.log(res)
})
}
})

以下示例代码可以在目标节点(用选择器 .target-class 指定)与参照节点(用选择器 .relative-class 指定)在页面显示区域内相交或相离,且相交或相离程度达到目标节点布局区域的20%和50%时,触发回调函数。

1
2
3
4
5
6
7
8
9
Page({
onLoad: function(){
this.createIntersectionObserver({
thresholds: [0.2, 0.5]
}).relativeTo('.relative-class').relativeToViewport().observe('.target-class', (res) => {
console.log(res)
})
}
})

在自定义组件或包含自定义组件的页面中,推荐使用 this.createIntersectionObserver 来代替 wx.createIntersectionObserver ,这样可以确保在正确的范围内选择节点

导航

open-type

跳转到tabBar页面,需要指定open-type属性为

1
<navigator url=" /pages/message/message" open-type="switchTab">导航到消息页面</navigator>
  • 如果页面栈中存在tabbr页面,则这个tabbar页面一定在栈底
  • 调用switchTab方法会关闭所有非tabbar页面,跳转到一个tabbar页面
  • 页面栈中同时只能存在一个tabbar页面,其他tabbar页面会处于悬垂状态,tabBar 页面实例会被保留(不销毁),但不在当前页面栈中

跳转到非tabBar页面

  • open-type = navigate,不会关闭当前页面,页面栈长度+1
  • open-type = redirectTo:弹出并销毁栈顶页面,在此过程中,这个栈顶页面的 onUnload 生命周期将被触发;之后框架将创建新的页面,并将其推入页面栈作为新的栈顶,页面栈长度不变。

后退导航:如果要后退到上一页面或多级页面,则需要指定open-type属性为navigateBack({ delta:1 }),delta表示后退层级,将页面栈当前的栈顶的若干个页面依次弹出并销毁。如果页面栈中当前只有一个页面,navigateBack 调用请求将失败(无论指定的 delta 是多少)
重加载

重加载路由 reLaunch 表示销毁当前所有的页面,并载入一个新页面,无论它是否是 tabBar 页

导航传参

通过查询参数来传参

1
<navigator url="/pages/infolinfo?name=zs&age=20">跳转到info页面</navigator>

接收参数

传递的查询参数会被收集为一个对象,可以直接在onLoad事件中直接获取到

1
2
3
onLoad(options){
console.log(options) //输出查询参数对象
}

页面路由监听

由于每次路由可能触发多个页面的多个页面生命周期,因此当某个页面的某个生命周期被触发时,小程序往往比较难判断它被触发的原因,从而难以做出一些针对路由(而非针对页面)的响应,所以推出了一些路由监听API,这些 API 是 全局监听器,在 App() 中或任意 JS 文件中注册,一次注册,全程生效

  • wx.onBeforeAppRoute(cb):开始执行跳转逻辑前
  • wx.onAppRoute(cb):路由逻辑已执行完毕(页面栈已更新,新页面实例已创建)但动画尚未开始
  • wx.onAppRouteDone(cb):页面切换动画完全结束(用户看到最终页面)
  • 等等…

路由事件id:为了在多次监听回调中识别同一个路由事件,框架会为每一次独立的路由事件生成一个在小程序实例中唯一的 ID,称为路由事件 ID。在所有页面路由监听函数中,事件参数中都将携带一个字符串 routeEventId,表示这个路由事件 ID。小程序可以通过读取回调中的 routeEventId,来将同一个路由在不同时间节点触发的不同回调进行关联

路由处理规则

当多次路由被连续发起时,如果当前的路由事件还未处理完毕,后续的路由事件将等待当前路由处理,并排队依次执行,直到所有待处理的路由都被执行完毕

一个简单的例子:用户点击返回按钮触发了 navigateBack,小程序在页面栈的栈顶页的 onUnload 中调用 wx.redirectTo并不能 将当前正在被销毁的页面,重定向为一个新页面,而是会先完成页面返回,再将页面返回后的新栈顶页重定向到新的页面。

页面的生命周期

小程序页面的生命周期函数有以下几种:

onLoad

页面加载时触发,一个页面只会调用一次,可以在这个函数的参数中拿到当前页面中的查询参数。

onShow

页面显示的时候触发

onReady

页面初次渲染完成时触发,一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。设置导航栏的标题,应该在这个时间之后进行。类似Vue中的mounted

onHide

页面隐藏/切入后台时触发

onUnload

页面写在时触发

自定义组件

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

创建自定义组件

在根目录下的components文件夹上,右键选择新建组件。类似于页面,一个自定义组件由 json wxml wxss js 4个文件组成。要编写一个自定义组件,首先需要在 json 文件中进行自定义组件声明(将 component 字段设为 true

1
2
3
{
"component": true
}

同时,还要在 wxml 文件中编写组件模板,在 wxss 文件中加入组件样式,它们的写法与页面的写法类似

在自定义组件的 js 文件中,需要使用 Component() 来注册组件(页面调用的是Page()函数),并提供组件的属性定义、内部数据和自定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Component({
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: 'default value',
}
},
data: {
// 这里是一些组件内部数据
someData: {}
},
methods: {
// 这里是一个自定义方法
customMethod: function(){}
}
})

使用自定义组件

使用组件前需要先进行注册,注册的方式分为全局注册和局部注册,全局注册就是在app.json文件的usingComponents属性中进行配置,局部注册就是在某个页面的json文件的usingComponents属性中进行配置。

需要提供每个自定义组件的标签名,和对应的自定义组件文件路径

1
2
3
4
5
{
"usingComponents": {
"navbar":"/components/navbar/navbar"
}
}

上述例子中路径”/components/navbar/navbar” 具体到了navbar文件,而不是目录

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

一些需要注意的细节

  • 因为 WXML 节点标签名只能是小写字母、中划线和下划线的组合,所以自定义组件的标签名也只能包含这些字符。
  • 自定义组件也是可以引用自定义组件的,引用方法类似于页面引用自定义组件的方式(使用 usingComponents 字段)

样式隔离

  • 默认情况下,自定义组件的样式只对当前组件生效,不会影响到组件之外的UI结构,同时组件所在页面的样式也不会影响组件的样式
  • app.wxss中的样式也不会影响组件中的样式,前提是这些样式不是标签选择器

能实现样式隔离的本质,其实和Vue一样,就是给标签添加自定义属性,然后重写组件内的选择器,额外添加上一个属性选择器,这样组件内的样式就只能对组件内的结构生效了。

组件传值

小程序组件中,properties是组件的对外属性,用来接收外界传递到组件中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
页面 (Page)

├─ data: { dataFieldA: 'hello', dataFieldB: 123 }

└─ WXML 中使用组件:
<component-tag-name prop-a="{{dataFieldA}}" prop-b="{{dataFieldB}}">
<view>slot内容</view>
</component-tag-name>


子组件 (Component)

├─ properties: {
│ propA: String,
│ propB: Number
│ }

└─ 组件内部可通过 this.data.propA / propB 使用数据

在以上例子中,组件的属性 propApropB 将收到页面传递的数据。

页面可以通过 setData 来改变绑定的数据字段。当页面调用 this.setData({ dataFieldA: 'new value' }),子组件的 propA 会自动更新

在小程序的组件中,properties属性和data数据的用法相同,它们都是可读可写的,只不过:

  • data更倾向于存储组件的私有数据
  • properties更倾向于存储外界传递到组件中的数据

实际上小程序组件中的this.datathis.properties完全是两个相同的对象

数据监听器

批量监听基本类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Component({
data:{ n1:0,n2:0,sum:0 },//数据节点
methods: {//方法列表
addN1() { this.setData({ n1: this.data.n1 + 1 }) },
addN2() { this.setData({ n2: this.data.n2 + 1}) }
}
//类似vue中的watch
observers: {
"n1,n2":function(n1,n2){ //拿到新值
// 监听n1和n2数据的变化
this.setData({sum:n1 +n2})//通过监听器,自动计算sum的值
}
}
)

监听对象的属性变化

1
2
3
4
5
6
7
8
9
10
11
Component({
observers: {
'对象.属性A, 对象.属性B': function(属性A的新值, 属性B的新值) {
// 触发此监听器的 3 种情况:
// 【为属性A赋值】使用 setData 设置 this.data.对象.属性A 时触发
// 【为属性B赋值】使用 setData 设置 this.data.对象.属性B 时触发
// 【直接为对象赋值】使用 setData 设置 this.data.对象 时触发
// do something...
}
}
})

监听对象所有属性的变化

1
2
3
4
5
observers:{
"rgb.**":function(obj){
console.log(obj)
}
}

纯数据字段

纯数据字段指的是那些不用于界面渲染的data字段
应用场景:例如有些情况下,某些data中的字段既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用。带有这种特性的data字段适合被设置为纯数据字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Component({
options: {
// 指定所有 _ 开头的数据字段为纯数据字段
pureDataPattern: /^_/
},
data: {
// 将 rgb 改造为以 _ 开头的纯数据字段
_rgb: {
r: 0,
g: 0,
b: 0
},
fullColor: '0, 0, 0'
}
})

组件通信

父传子:父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据,不能传递方法。传递给子组件的值只是拷贝,可以在子组件内修改父组件传递过来的数据,但是父组件中的数据并不会改变,修改父组件中的数据,会重新给子组件传值并更新页面。

子传父:在子组件内部触发自定义事件,向父组件传递数据。监听自定义组件事件的方法,与监听基础组件事件的方法完全一致

注册事件

1
2
3
4
<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 -->
<component-tag-name bindmyevent="onMyEvent" />
<!-- 或者可以写成 -->
<component-tag-name bind:myevent="onMyEvent" />
1
2
3
4
5
Page({
onMyEvent: function(e){
e.detail // 自定义组件触发事件时提供的detail对象
}
})

触发事件

自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail对象和事件选项

1
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>
1
2
3
4
5
6
7
8
9
10
Component({
properties: {},
methods: {
onTap: function(){
var myEventDetail = {} // detail对象,提供给事件监听函数
var myEventOption = {} // 触发事件的选项
this.triggerEvent('myevent', myEventDetail, myEventOption)
}
}
})

如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。调用时需要传入一个匹配选择器 selector,比如this.selectComponent(".my-component"),selector类似于 CSS 的选择器,但是只支持部分选择器。

组件的生命周期

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

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

created

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

attached

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

detached

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

生命周期方法可以直接定义在 Component 构造器的第一级参数中,但是最好定义在 lifetimes 字段内,优先级更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Component({
lifetimes: {
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
},
},
// 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
},
// ...
})

组件所在页面的生命周期

还有一些特殊的生命周期,它们并非与组件有很强的关联,但有时组件需要获知,以便组件内部处理。这样的生命周期称为“组件所在页面的生命周期”,在 pageLifetimes 定义段中定义

其中可用的生命周期包括:

生命周期参数描述
show组件所在的页面被展示时执行
hide组件所在的页面被隐藏时执行
resizeObject Size组件所在的页面尺寸变化时执行
1
2
3
4
5
6
7
Component({
pageLifetimes: {
show: function() { }, // 页面被展示
hide: function() { }, // 页面被隐藏
resize: function(size) { } // 页面尺寸变化
}
})

插槽

在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点

1
2
3
4
5
<!-- 组件模板 -->
<view class="wrapper">
<view>这里是组件的内部节点</view>
<slot></slot>
</view>
1
2
3
4
5
6
7
<!-- 引用组件的页面模板 -->
<view>
<component-tag-name>
<!-- 这部分内容将被放置在组件 <slot> 的位置上 -->
<view>这里是插入到组件slot中的内容</view>
</component-tag-name>
</view>

默认情况下,一个组件的 wxml 中只能有一个 slot 。需要使用多 slot 时,可以在组件 js 中声明启用。

1
2
3
4
5
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
}
})

至于多插槽的使用方法,参考官方文档中指南的部分

代码复用behaviors

微信小程序中的behaviors,类似于Vue中的 “mixins”

定义behavior

使用Behavior函数创建一个behavior对象

1
2
3
4
5
6
7
8
9
10
11
// behaviors/log.js
module.exports = Behavior({
methods: {
log(msg) {
console.log(`[LOG] ${msg}`);
},
error(err) {
console.error(`[ERROR] ${err}`);
}
}
});

在组件中使用 behavior

1
2
3
4
5
6
7
8
9
10
11
12
// components/my-button/my-button.js
const logBehavior = require('../../behaviors/log');

Component({
behaviors: [logBehavior], // 引入 behavior

methods: {
handleClick() {
this.log('按钮被点击了!'); // 直接调用 behavior 中的方法
}
}
});

查看自定义组件的数据

wxml 面板中可以查看自定义组件在渲染时的 Data 数据。 在 wxml 中先选中需要查看的自定义组件,然后切换到 Component Data 即可实时查看当前自定义组件的数据。

组件和页面的区别

  • 组件的JSON文件中需要添加"component": true
  • 组件的js文件中调用的是Component函数,而页面中调用的是Page函数。
  • 页面中的函数可以直接定义到和data平级的位置,而组件中的函数必须定义在methods属性中

基础能力

网络

域名设置

每个微信小程序需要事先设置通讯域名,小程序只可以跟指定的域名进行网络通信。包括普通 HTTPS 请求(wx.request)、上传文件(wx.uploadFile)、下载文件(wx.downloadFile) 和 WebSocket 通信。

  • 而且域名只支持https协议或者wss协议。
  • 域名不能使用 IP 地址或 localhost
  • 不支持配置父域名,使用子域名
  • 域名必须经过 ICP 备案

在开启阶段,可以在微信开发者工具中勾选**”不校验合法域名和HTTPS证书”**的选项来跳过域名检查。

小程序中不存在跨域问题,因为小程序的宿主环境不是浏览器,小程序中的请求不叫做ajax请求,因为ajax请求是基于xhr或者fetch的。

DNS预解析域名

DNS预解析域名,是框架提供的一种在小程序启动时,提前解析业务域名的技术,从而提高请求的速度。

DNS域名配置请求「小程序后台-开发-开发设置-服务器域名」 中进行配置,配置时需要注意:

  • 预解析域名无需填写协议头
  • 预解析域名最多可添加 5 个
  • 其他安全策略同服务器域名配置策略

超时时间

  • 默认超时时间是 60s
  • 超时时间可以在 app.jsongame.json 中通过 networktimeout 配置
  • 也可以在接口调用时指定超时时间,如 wx.request({ timeout: 5000 }),单位为ms。接口调用的timeout配置优先级高于app.json中的配置

存储

每个微信小程序都可以有自己的本地缓存,可以通过 wx.setStorage/wx.setStorageSync等API进行操作。本地缓存的清理时机跟代码包一样,只有在代码包被清理的时候本地缓存才会被清理

文件系统

通过 wx.getFileSystemManager() 可以获取到全局唯一文件系统管理器,所有文件系统的管理操作通过 FileSystemManager来调用。

1
var fs = wx.getFileSystemManager()

文件主要分为两大类:

  • 代码包文件:代码包文件指的是在项目目录中添加的文件。由于代码包文件大小限制,代码包文件适用于放置首次加载时需要的文件,对于内容较大或需要动态替换的文件,不推荐用添加到代码包中,推荐在小游戏启动之后再用下载接口下载到本地
  • 本地文件:通过调用接口本地产生,或通过网络下载下来,存储到本地的文件。

其中本地文件又分为三种:

  1. 本地临时文件:临时产生,随时会被回收的文件。本地临时文件只能通过调用特定接口产生,不能直接写入内容。本地临时文件产生后,仅在当前生命周期内保证有效,重启之后不一定可用。如果需要保证在下次启动时无需下载,可通过 FileSystemManager.saveFile()FileSystemManager.copyFile() 接口把本地临时文件转换成本地缓存文件或本地用户文件

    1
    2
    3
    4
    5
    wx.chooseImage({
    success: function (res) {
    var tempFilePaths = res.tempFilePaths // tempFilePaths 的每一项是一个本地临时文件路径
    }
    })
  2. 本地缓存文件:小程序通过接口把本地临时文件缓存后产生的文件,不能自定义目录和文件名。本地缓存文件产生后,重启之后仍可用。本地缓存文件只能通过 FileSystemManager.saveFile() 接口将本地临时文件保存获得

    1
    2
    3
    4
    5
    6
    fs.saveFile({
    tempFilePath: '', // 传入一个本地临时文件路径
    success(res) {
    console.log(res.savedFilePath) // res.savedFilePath 为一个本地缓存文件路径
    }
    })

    注意:本地缓存文件是最初的设计,1.7.0 版本开始,提供了功能更完整的本地用户文件,可以完全覆盖本地缓存文件的功能,如果不需要兼容低于 1.7.0 版本,可以不使用本地缓存文件。

  3. 本地用户文件:小程序通过接口把本地临时文件缓存后产生的文件,允许自定义目录和文件名。本地用户文件是从 1.7.0 版本开始新增的概念。我们提供了一个用户文件目录给开发者,开发者对这个目录有完全自由的读写权限。通过 wx.env.USER_DATA_PATH 可以获取到这个目录的路径。

    1
    2
    3
    // 在本地用户文件目录下创建一个文件 hello.txt,写入内容 "hello, world"
    const fs = wx.getFileSystemManager()
    fs.writeFileSync(`${wx.env.USER_DATA_PATH}/hello.txt`, 'hello, world', 'utf8')

清理策略

  • 本地临时文件只保证在小程序当前生命周期内,一旦小程序被关闭就可能被清理,即下次冷启动不保证可用。
  • 本地缓存文件和本地用户文件的清理时机跟代码包一样,只有在代码包被清理的时会被清理。

分包

定义

分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载,类似webpack中的代码分割。可以优化小程序首次启动的下载时间,在多团队共同开发时可以更好的解耦协作

结构

一个主包

一般只包含项目的启动页面或TabBar页面、以及所有分包都需要用到的一些公共资源

多个分包

只包含和当前分包有关的页面和私有资源

分包的加载规则

  • 在小程序启动时,默认会下载主包并启动主包内页面(tabbar页面)
  • 非tabBar页面可以按照功能的不同,划分为不同的分包之后,进行按需下载
  • 当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示

体积限制

整个小程序所有分包大小不超过16M(主包+所有分包),单个分包/主包大小不能超过2M

配置规则

  • 根目录下的pages一般放的是所有主包页面

  • 在根目录下新建subpkg文件(自定义名称),创建一个分包,内部可以有多个页面,就如同pages文件夹。

  • 还要在app.json文件里声明分包结构。可以看到subPackages的值是一个对象数组,说明可以设置多个分包,一个分包对应一个单独的文件夹,内部可以有多个页面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "subPackages": [
    {
    "root": "subpkg",
    "name":'pkgA',//分包别名
    //当前分包下所有页面的相对路径
    "pages": [
    "detail/detail",
    "goods_list/goods_list",
    "search/search"
    ]
    }
    ]
    }

打包原则

  • subpackages之外的目录将被打包到主包中
  • tabBar 页面必须在主包内
  • subPackages 的根目录不能是另外一个 subPackages 内的子目录

引用原则

  • packageA 无法 require packageB JS 文件,但可以 require 主包、packageA 内的 JS 文件;使用 [分包异步化]时不受此条限制
  • packageA 无法 require packageB 的 template,但可以 require 主包、packageA 内的 template
  • packageA 无法使用 packageB 的资源,但可以使用主包、packageA 内的资源

独立分包

  • 普通分包的打开依赖于主包,独立分包可以不依赖主包打开,可以独立于主包和其他分包运行
  • 开发者可以按需将某些具有一定功能独立性的页面,配置到独立分包中。当小程序从普通的分包页面启动时,需要首先下载主包;而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度
  • 独立分包在配置的时候,需要额外添加independent:true属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"subPackages": [
{
"root": "subpkg",//指明是项目根目录下的subpkg文件
"name":'pkgA',//分包别名
//设置独立分包
"independent": true ,
//当前分包下所有页面的相对路径
"pages": [
"detail/detail",
"goods_list/goods_list",
"search/search"
]
}
]
}
  • 独立分包和普通分包以及主包之间,是相互隔绝的,不能相互引用彼此的资源,独立分包中不能引用主包内的公共资源
  • 主包中的 app.wxss 对独立分包无效,应避免在独立分包页面中使用 app.wxss 中的样式

分包预下载

在进入小程序的某个页面时,由框架自动预下载可能需要的分包,从而提高进入后续分包的启动速度

app.json中配置:

1
2
3
4
5
6
"preloadRule":{ //分包预下载规则
"pages/contact/contact":{ //触发分包预下载的页面路径
"network":"all", //表示在指定的网络模式下下载,默认为wifi
"packages":["pkgA"] //通过root或name指定预下载哪些分包
}
}

preloadRule 中,key 是页面路径,value 是进入此页面的预下载配置,每个配置有以下几项:

字段类型必填默认值说明
packagesStringArray进入页面后预下载分包的 rootname__APP__ 表示主包。
networkStringwifi在指定网络下预下载,可选值为: all: 不限网络 wifi: 仅wifi下预下载

同一个分包中的页面享有共同的预下载大小限额2M,如,页面 A 和 B 都在同一个分包中,A 中预下载总大小 0.5M 的分包,B中最多只能预下载总大小 1.5M 的分包。

开放能力

权限处理

小程序中部分接口需要经过用户授权同意才能调用。我们把这些接口按使用范围分成多个 scope ,用户选择对 scope 来进行授权,当授权给一个 scope 之后,其对应的所有接口都可以直接使用

开发者可以使用 wx.getSetting获取用户当前的授权状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wx.getSetting({
success(e){
console.log(e)
}
})
//输出的对象形如
{
authSetting: {
scope.writePhotosAlbum: true,
scope.address: true, scope.invoice: true,
scope.invoiceTitle: true,
scope.userInfo: true
}
errMsg: "getSetting:ok"
}

在回调函数中可以判断用户是否授予了某个权限

开发者可以调用 wx.openSetting() 打开设置界面,引导用户开启授权,但是必须由用户手动触发。

开发者可以使用 wx.authorize 在调用需授权 API 之前,提前向用户发起授权请求。在真正需要使用授权接口时,才向用户发起授权申请,并在授权申请中说明清楚要使用该功能的理由。

1
2
3
4
5
6
7
8
9
10
11
12
13
wx.getSetting({
success(res) {
if (!res.authSetting['scope.record']) {
wx.authorize({
scope: 'scope.record', //必填项目,指定请求授权的权限,这里是麦克风的权限
success () {
// 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问
wx.startRecord()
}
})
}
}
})

一旦用户明确同意或拒绝过授权,其授权关系会记录在后台,直到用户主动删除小程序。

用户信息

小程序登录

小程序可以通过微信官方提供的登录能力,方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

使用微信一键登录的功能,需要小程序,开发者服务器,微信接口服务器这三者协调配合。

  • 在小程序端先调用wx.login方法,获取临时登录凭证code,这个code是本地生成的,不需会产生请求。再调用开发者服务器的登录接口,并携带这个登录凭证code
  • 然后开发者的服务器会调用微信的登录凭证校验接口,并携带appid,appsecret和code
  • 微信接口服务器会返回用户在该小程序下的唯一标识openid会话密钥 session_key
  • 开发者服务器拿到openidsession_key生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份

注意:

  • 会话密钥 session_key 是对用户数据进行加密签名的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥
  • 临时登录凭证 code 只能使用一次

头像昵称填写

当小程序需要让用户完善个人资料时,可以通过微信提供的头像昵称填写能力快速完善

头像选择:需要将 button 组件 open-type 的值设置为 chooseAvatar(没有对应的api),当用户选择需要使用的头像之后,可以通过 bindchooseavatar="onChooseAvatar" 事件回调获取到头像信息的临时路径。

1
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">修改头像</button>
1
2
3
4
5
6
7
8
onChooseAvatar(e) {
const { avatarUrl } = e.detail; // 拿到临时图片路径
// 【重要】通常需要上传到自己的服务器,换取永久链接(不需要再向微信获取)
// wx.uploadFile({ ... })
this.setData({
avatarUrl: avatarUrl
});
}

从基础库2.24.4版本起,若用户上传的图片未通过安全监测,不触发bindchooseavatar 事件。

昵称填写:需要将 input 组件 type 的值设置为 nickname,当用户在此input进行输入时,键盘上方会展示微信昵称。

1
<input type="nickname" value="微信用户"></input>

分享页面

onShareAppMessage,用于监听用户点击页面内转发按钮(button组件 open-type="share")或右上角菜单“转发”按钮的行为,并自定义转发内容

注意:只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮

此事件处理函数需要 return 一个 Object,用于自定义转发内容,返回内容如下

字段说明默认值
title转发标题当前小程序名称
path转发路径当前页面 path ,必须是以 / 开头的完整路径
imageUrl自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径。支持PNG及JPG。显示图片长宽比是 5:4。使用默认截图
1
2
3
4
5
6
7
8
Page({
onShareAppMessage() {
return {
title: '自定义转发标题',
path: '/page/user?id=123',
}
}
})

分享到朋友圈

onShareTimeline(),监听右上角菜单“分享到朋友圈”按钮的行为,并自定义分享内容。

注意:只有定义了此事件处理函数,右上角菜单才会显示“分享到朋友圈”按钮

字段说明默认值
title自定义标题,即朋友圈列表页上显示的标题当前小程序名称
query自定义页面路径中携带的参数,如 path?a=1&b=2 的 “?” 后面部分当前页面路径携带的参数
imageUrl自定义图片路径,可以是本地文件或者网络图片。支持 PNG 及 JPG,显示图片长宽比是 1:1。默认使用小程序 Logo
1
2
3
4
5
6
7
8
9
Page({
onShareTimeline() {
return {
title: '自定义转发标题',
query: 'id=123',
imageUrl: '/images/share.png',
}
}
})

小程序页面默认不可被分享到朋友圈,开发者需主动设置“分享到朋友圈”。页面允许被分享到朋友圈,需满足两个条件:

  • 首先,页面需设置允许“发送给朋友”。即注册onShareAppMessage监听
  • 然后,页面需设置允许“分享到朋友圈,即注册onShareTimeline()监听

微信支付

创建订单

  • 请求创建订单的API接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
  • 服务器响应的结果:订单编号

订单预支付

  • 请求订单预支付的API接口:把(订单编号)发送到服务器
  • 服务器响应的结果:订单预支付的参数对象,里面包含了订单支付相关的必要参数

发起微信支付
调用uni.requestPayment这个API,发起微信支付,把步骤2得到的“订单预支付对象”作为参数传递给uni.requestPayment()方法
监听uni.requestPayment()这个API 的success,fail,complete回调函数。

小程序的生命周期

小程序从启动到最终被销毁,会经历很多不同的状态,小程序在不同状态下会有不同的表现

小程序启动

从用户认知的角度看,广义的小程序启动可以分为两种情况,一种是冷启动,一种是热启动

  • 冷启动:如果用户首次打开,或小程序销毁后被用户再次打开,此时小程序需要重新加载启动,即冷启动。
  • 热启动:如果用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时小程序并未被销毁,只是从后台状态进入前台状态,这个过程就是热启动。

从小程序生命周期的角度来看,我们一般讲的「启动」专指冷启动,热启动一般被称为后台切前台

前台与后台

小程序启动后,界面被展示给用户,此时小程序处于「前台」状态。

当用户「关闭」小程序时,小程序并没有真正被关闭,而是进入了「后台」状态,此时小程序还可以短暂运行一小段时间,但部分 API 的使用会受到限制

挂起

小程序进入「后台」状态一段时间后(目前是 5 秒),微信会停止小程序 JS 线程的执行,小程序进入「挂起」状态。此时小程序的内存状态会被保留,但开发者代码执行会停止,事件和接口回调会在小程序再次进入「前台」时触发。

当开发者使用了后台音乐播放、后台地理位置等能力时,小程序可以在「后台」持续运行,不会进入到「挂起」状态

生命周期函数

小程序应用的生命周期函数有3个

onLaunch

小程序初始化完成时触发,全局只触发一次

onShow

小程序启动后,从后台进入前台

onHide

小程序启动后,从前台进入到后台

1
2
3
4
5
6
7
App({
onLaunch: function () {

},
onShow(){},
onHide(){}
})

发布上线

一个小程序从开发完到上线一般要经过 预览-> 上传代码 -> 提交审核 -> 发布等步骤。

预览

使用开发者工具可以预览小程序,帮助开发者检查小程序在移动客户端上的真实表现。

点击开发者工具顶部操作栏的预览按钮,开发者工具会自动打包当前项目,并上传小程序代码至微信的服务器,成功之后会在界面上显示一个二维码。使用当前小程序开发者的微信扫码,即可看到小程序在手机客户端上的真实表现。

上传代码

同预览不同,上传代码是用于提交体验或者审核使用的。

点击开发者工具顶部操作栏的上传按钮,填写版本号以及项目备注,需要注意的是,这里版本号以及项目备注是为了方便管理员检查版本使用的,开发者可以根据自己的实际要求来填写这两个字段。

上传成功之后,登录小程序管理后台 - 版本管理 - 开发版本 就可以找到刚提交上传的版本了。

可以将这个版本设置 体验版 或者是 提交审核

提交审核

为了保证小程序的质量,以及符合相关的规范,小程序的发布是需要经过审核的。

在开发者工具中上传了小程序代码之后,登录 小程序管理后台 - 版本管理 - 开发版本 找到提交上传的版本。

在开发版本的列表中,点击 提交审核 按照页面提示,填写相关的信息,即可以将小程序提交审核。

需要注意的是,请开发者严格测试了版本之后,再提交审核, 过多的审核不通过,可能会影响后续的时间。

发布

审核通过之后,管理员的微信中会收到小程序通过审核的通知,此时登录 小程序管理后台 - 版本管理 - 审核版本中可以看到通过审核的版本。

点击发布后,即可发布小程序。小程序提供了两种发布模式:全量发布和分阶段发布。全量发布是指当点击发布之后,所有用户访问小程序时都会使用当前最新的发布版本。分阶段发布,是指分不同时间段,来控制部分用户使用最新的发布版本,分阶段发布我们也称为灰度发布。一般来说,普通小程序发布时采用全量发布即可,当小程序承载的功能越来越多,使用的用户数越来越多时,采用分阶段发布是一个非常好的控制风险的办法。

说说你对微信小程序的理解?优缺点?

是什么

2017年,微信正式推出了小程序,允许外部开发者在微信内部运行自己的代码,开展业务

截至目前,小程序已经成为国内前端的一个重要业务,跟 Web 和手机 App 有着同等的重要性

小程序是一种不需要下载安装即可使用的应用(但实际上还是存在一个轻量级的下载和安装流程,不过用户感知不到),它实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用

也体现了“用完即走”的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载

注意的是,除了微信小程序,还有百度小程序、微信小程序、支付宝小程序、抖音小程序,都是每个平台自己开发的,都是有针对性平台的应用程序

背景

小程序并非凭空冒出来的⼀个概念,当微信中的 WebView 逐渐成为移动 Web的⼀个重要入口时,微信就有相关的 JS-SDK

JS-SDK 解决了移动网页能⼒不⾜的问题,通过暴露微信的接⼝使得 Web 开发者能够拥有更多的能⼒,然而在更多的能⼒之外,JS-SDK的模式并没有解决使⽤移动网页遇到的体验不良的问题

因此需要设计⼀个比较好的系统,使得所有开发者在微信中都能获得⽐较好的体验:

  • 快速的加载
  • 更强⼤的能⼒
  • 原⽣的体验
  • 易⽤且安全的微信数据开放
  • 高效和简单的开发

这些是JS-SDK做不到的,需要设计一个全新的小程序系统

其中相比H5,小程序与其的区别有如下:

  • 运⾏环境:⼩程序基于浏览器内核重构的内置解析器
  • 系统权限:⼩程序能获得更多的系统权限,如⽹络通信状态、数据缓存能⼒等
  • 渲染机制:⼩程序的逻辑层和渲染层是分开的

微信小程序可以视为只能用微信打开和浏览的H5小程序和网页的技术模型是一样的,用到的 JavaScript 语言和 CSS 样式也是一样的,只是网页的 HTML 标签被稍微修改成了 WXML 标签

因此可以说,小程序页面本质上就是网页

优缺点

优点:

  • 随搜随用,用完即走:使得小程序可以代替许多APP,或是做APP的整体嫁接,或是作为阉割版功能的承载体
  • 流量大,易接受:小程序借助自身平台更加容易引入更多的流量
  • 安全,开发门槛低,降低兼容性限制

缺点:

  • 用户留存:及相关数据显示,小程序的平均次日留存在13%左右,但是双周留存骤降到仅有1%
  • 体积限制:微信小程序只有2M的大小,这样导致无法开发大型一些的小程序
  • 受控微信:比起APP,尤其是安卓版的高自由度,小程序要面对很多来自微信的限制,从功能接口,甚至到类别内容,都要接受微信的管控

说说微信小程序的实现原理?

背景

网页开发,渲染线程和脚本是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应的原因,本质就是我们常说的 JS 是单线程的。而在小程序中,将视图层和逻辑层分开的,双线程同时运行,视图层的界面使用 WebView 进行渲染,逻辑层运行在 JSCore 中。

其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。

渲染层

负责界面展示,包括布局、样式等。 实际上是通过WebView解析和渲染的。界面渲染相关的任务全都在WebView线程里执行。一个小程序存在多个界面所以渲染层存在多个 WebView 线程

逻辑层

主要使用 JavaScript 编写。负责处理业务逻辑、数据获取、API 请求等,其中网络请求微信客户端进行转发,运行在一个基于 V8 或者 JSCore 的 JavaScript 引擎之上(视操作系统而定)

小程序的逻辑层和渲染层是分开的,逻辑层运行在不同于渲染层的独立 JS 运行时中,因此并不能直接使用 DOM API 和 BOM API。这一区别导致了前端开发非常熟悉的一些库,例如 jQuery,在小程序中是无法运行的。同时逻辑层的 JS 运行时与 NodeJS 环境也不尽相同,所以一些 NPM 的包在小程序中也是无法运行的

Native

Native 层指的是微信客户端本身的原生代码,它负责:

  • 提供基础库,包含所有可用的 API
  • 处理逻辑层和渲染层之间的高效通信
  • 实现性能优化,确保小程序能够流畅运行。提供安全机制,保护用户数据和隐私。

WebView 是什么

WebView 是一个允许在应用程序内部展示网页内容的组件,它依赖于特定平台上的浏览器内核来工作

在不同的平台上有不同实现:

  • Android:使用 android.webkit.WebView
  • iOS:使用 WKWebView(较新的推荐使用方式)或 UIWebView(已废弃)。

相当于 Web 开发中的 浏览器渲染引擎

通信

逻辑层和渲染层之间的通信,是通过微信客户端(Native,或者说是宿主环境)提供的 API 实现的,比如逻辑层可以通过 setData 方法将数据传递给渲染层,触发页面更新:

1
2
3
this.setData({
message: 'Hello, World!'
});

在逻辑层发生数据变更的时候,通过宿主环境(native)提供的setData方法,把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上(小程序在渲染层,宿主环境会把wxml转化成对应的JS对象,类似web开发中的dom对象),渲染出正确的视图

运行环境

运行环境逻辑层渲染层
iOSJavaScriptCoreWKWebView
安卓V8chromium定制内核
小程序开发者工具NWJSChrome WebView

扩展:微信小程序

微信小程序的一键登录怎么做

调用uni.login 接口需要用户已经完成小程序验证,否则无法调用,完成小程序验证要300元/年

  • 先调用uni.login 方法,这会发送一个请求给微信服务器,得到一个临时登录凭证code(临时登录凭证,有效期 5 分钟,只能使用一次,这个登录凭证和微信用户信息是绑定的)

  • 然后调用uni.request方法将这个临时登录凭证,发送到小程序的后端服务器,小程序后端服务器将这个临时登录凭证code,连同小程序的APPID和APPSECRET(小程序密钥,只能在后端使用,不能暴露在前端!)发送给微信服务器,换取openid, session_key,然后返回一个token给小程序,用户的后续请求都会携带这个token,返回的token中就包含openid

  • openid表示用户在当前小程序下唯一标识(每个用户 + 每个小程序,唯一),计算方式为:openid = hash(用户微信号 + 小程序 AppID),同一个用户在同一个微信小程序中,无论登录多少次,得到的 openid 都是完全一致的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async wechatLogin() {
try {
// 调用微信登录 API
const [err, res] = await uni.login({
provider: 'weixin' // 注意:在小程序中可省略
});
if (err) {
throw err;
}
const code = res.code;
// 将 code 发送到你的小程序服务器
const loginRes = await uni.request({
url: 'https://your-api.com/api/login',
method: 'POST',
data: { code }
});
const { token, userInfo } = loginRes[1].data;
// 保存 token 到本地
uni.setStorageSync('token', token);
uni.setStorageSync('userInfo', userInfo);
// 跳转到首页
uni.switchTab({
url: '/pages/index/index'
});
} catch (error) {
uni.showToast({
title: '登录失败',
icon: 'error'
});
}
}

如果还需要用户昵称、头像等信息,则需要调用:

1
2
3
4
5
6
7
8
9
10
11
// 获取用户信息(需用户点击按钮授权)
uni.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
//res.userInfo中包含用户的昵称和头像
console.log('用户信息:', res.userInfo);
},
fail: () => {
uni.showToast({ title: '授权失败', icon: 'none' });
}
});

注意:getUserProfile 已被限制使用,推荐使用 <button open-type="getUserInfo"> 组件方式触发。

后端代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// server.js
const express = require('express');
const request = require('request');
const jwt = require('jsonwebtoken'); // 用于生成 token
const app = express();
app.use(express.json());

// 微信配置
const APPID = 'your-appid';
const APPSECRET = 'your-appsecret';
const WECHAT_LOGIN_URL = `https://api.weixin.qq.com/sns/jscode2session`;

app.post('/api/login', (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: '缺少 code' });
}

// 请求微信服务器换取 openid 和 session_key
const url = `${WECHAT_LOGIN_URL}?appid=${APPID}&secret=${APPSECRET}&js_code=${code}&grant_type=authorization_code`;

request.get(url, (error, response, body) => {
if (error) {
return res.status(500).json({ error: '网络错误' });
}
const result = JSON.parse(body);
const { openid, session_key } = result;
// 生成自定义登录态 token(例如 JWT)
const token = jwt.sign(
{ openid }, //返回用户在该小程序下的唯一凭证
'your-secret-key', // 加密token的密钥
{ expiresIn: '7d' } // 这个token在七天后过期
);
// (可选)查询或创建用户
// const user = findOrCreateUser(openid);
res.json({
token,
userInfo: {
openid
}
});
});
});

在微信小程序上怎么做微信支付

想在微信小程序中实现支付功能,需要先满足一下条件:

  • 小程序已经上线,个人小程序不支持支付
  • 个人类小程序上线,不需要支付 300 元认证费,但必须完成微信实名认证(绑定身份证)完成后可以: 提交代码审核
    , 发布上线, 使用基础功能但个人小程序功能受限,不支持:微信支付,获取用户手机号,广告组件,附近的小程序
  • pay.weixin.qq.com 申请微信商户号

然后执行如下步骤:

  • 前端请求后端创建订单,后端调用微信统下单 API,生成requestPayment所需参数并返回
  • 前端调用uni.requestPayment调起支付,用户输入密码完成支付
  • 支付成功后,微信服务器异步调用小程序后端的支付回调接口,后端更新订单状态,返回 SUCCESS
  • 前端跳转支付成功页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<view>
<button @click="handlePay">立即支付</button>
</view>
</template>

<script>
export default {
methods: {
async handlePay() {
try {
// 1. 获取用户token
const token = uni.getStorageSync('token');
// 2. 前端请求后端创建订单
const orderRes = await uni.request({
url: 'https://your-api.com/api/order/create',
method: 'POST',
header: { 'Authorization': token },
data: {
productId: 123,
totalFee: 100, // 1元 = 100分
body: '测试商品'
}
});
const payParams = orderRes[1].data;
// 3. 调起微信支付,支付成功后微信服务器触发小程序后端的支付回调接口
// 小程序后端的支付回调接口返回响应后,这个微信支付的请求才算成功
const [err, res] = await uni.requestPayment({
provider: 'weixin',
...payParams
});
if (err) {
uni.showToast({ title: '支付失败', icon: 'error' });
return;
}
// 4. 支付成功
uni.showToast({ title: '支付成功', icon: 'success' });
uni.redirectTo({ url: '/pages/order/success' });
} catch (error) {
uni.showToast({ title: '请求失败', icon: 'none' });
}
}
}
}
</script>

后端代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Node.js 示例(Express)
app.post('/api/order/create', async (req, res) => {
const { productId, userId, totalFee, body } = req.body;

// 1. 生成订单号
const outTradeNo = `ORD${Date.now()}${Math.floor(Math.random() * 1000)}`;
// 2. 请求微信统一下单 API
const params = {
appid: 'your-appid', // 小程序 appid
mch_id: 'your-mch-id', // 商户号
nonce_str: Math.random().toString(36).substr(2, 15), // 随机字符串
body: body || '商品订单', // 商品描述
out_trade_no: outTradeNo, // 商户订单号
total_fee: totalFee, // 金额(单位:分)
spbill_create_ip: '127.0.0.1', // 用户 IP(实际获取)
notify_url: 'https://your-api.com/api/pay/callback', // 支付结果通知地址
trade_type: 'JSAPI', // 小程序支付
openid: 'user-openid' // 用户的 openid(必须从登录获取)
};
// 3. 生成签名
const sign = generateSign(params, 'your-api-key'); // 自定义签名方法
params.sign = sign;
// 4. 发送请求到微信
const xml = buildXml(params); // 构建 XML 请求体
// 在后端接口中发送请求
const result = await axios.post('https://api.mch.weixin.qq.com/pay/unifiedorder', xml, {
headers: { 'Content-Type': 'text/xml' }
});
const response = parseXml(result.data); // 解析 XML 响应
if (response.return_code === 'SUCCESS' && response.result_code === 'SUCCESS') {
// 返回给小程序的参数
res.json({
appId: params.appid,
timeStamp: Math.floor(Date.now() / 1000).toString(),
nonceStr: response.nonce_str,
package: `prepay_id=${response.prepay_id}`,
signType: 'MD5',
//
paySign: generateSign({
appId: params.appid,
timeStamp: Math.floor(Date.now() / 1000).toString(),
nonceStr: response.nonce_str,
package: `prepay_id=${response.prepay_id}`,
signType: 'MD5'
}, 'your-api-key')
});
} else {
res.status(400).json({ error: response.return_msg });
}
});
// 微信支付成功后,就会异步调用这个接口
app.post('/api/pay/callback', async (req, res) => {
const xmlData = await getRawBody(req); // 获取原始 XML 数据
const params = parseXml(xmlData);
if (params.return_code === 'SUCCESS' && params.result_code === 'SUCCESS') {
// 校验签名
// 更新订单状态
// 发货逻辑(可选)
// deliverProduct(params.out_trade_no);
}
// 返回成功响应,告诉微信不要再重试
res.send('<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>');
});