Mini Program Notes
# Mini Program Notes
# Fundamentals Course
# WeChat Official Platform
https://mp.weixin.qq.com/ (opens new window)
When registering, you can choose from these types: Subscription Account, Service Account, Mini Program, Enterprise WeChat
Each email can only register one mini program.
Personal-type mini programs: cannot use WeChat Pay or card & coupon features
# Mini Program API Documentation
Mini Program Development Docs (opens new window)
# WeChat Open Community
WeChat Developer Community (opens new window)
# Directory Structure
Default Directory
pages-----------------------Pages
index ----------------- Home page folder
index.js ------------Home page JS
index.json---------Home page config
index.wxml-------Home page HTML
index.wxss--------Home page CSS
utils------------------------Utilities
app.js ----------------------Main project JS
app.json-------------------Global config (page routes, header/footer navigation config, etc.)
app.wxss -----------------Main project styles CSS
project.config.json ----Project config
Code Structure
.json : Config files, stored in JSON format
There are three types of config in a project: project config (project.config.json), global config (app.json), page config (index.json)
.wxml: Equivalent to HTML files
.wxss: Equivalent to CSS
.js : JavaScript
# File Details
project.config.json project config -- key fields
setting:{
urlCheck -- whether to check for secure domains
es6 -- whether to transpile ES6 to ES5
postcss -- whether to auto-prefix CSS styles
minified -- whether to minify
}
app.json global config
Global Config API (opens new window)
wxml introduction
<view>{{motto}}</view>
Loop rendering
<view wx:for="{{list}}" wx:key="{{index}}">
{{index}} {{item}}
</view>
Renaming for-loop item and index
<block wx:for="{{list}}" wx:for-item="data" wx:for-index="inx">
{{inx}} {{data}}
</block>
Conditional rendering (similar to Vue's v-if / v-else)
<view wx:if="{{isLogin}}">Logged in</view>
<view wx:else>Please log in</view>
Conditional display (similar to Vue's v-show)
<view hidden="{{isLogin}}">Display content</view>
Binding click events
<button bindtap="tapName">Button</button>
Page({
tapName: function(event) {
console.log(event)
}
})
...
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
wxss introduction
Size unit: rpx -- adapts automatically to screen width.
Importing external wxss: @import '...'
JS introduction
WXS (WeiXin Script) is a scripting language for mini programs
Binding click events
<button bindtap="onTapHandler">Click me +1</button>
<view>{{count}}</view>
2
Page({
data: {
count: 0
},
onTapHandler: function() {
this.setData({
count: this.data.count++
})
}
})
2
3
4
5
6
7
8
9
10
# Preventing Event Bubbling
Change the binding method from bindtap to catchtap.
# Third-Party Libraries
WeUI
WeUI is a set of base style libraries consistent with WeChat's native visual experience
iView Weapp
A high-quality UI component library for WeChat mini programs
Vant Weapp
A lightweight, reliable mini program UI component library
# Cloud Development
Traditional mini program development model
Client -----> Server (backend code, database) ------> Operations (DB maintenance, file storage, content acceleration, network protection, container services, load balancing, security hardening, etc.)
Mini program cloud development model
Client -----> Cloud Development (cloud functions, cloud database, cloud storage)
Traditional Development vs Cloud Development
Low development efficiency Serverless
High operation costs Developers focus more on business logic
Serverless development is the future trend
# Three Core Capabilities of Cloud Development
# Cloud Functions
(Equivalent to backend APIs in traditional development)
Get appid, get openid, generate share images, call Tencent Cloud SDK ...
# Cloud Database
CRUD operations on data ...
# Cloud Storage
Manage files, upload files, download files, share files ...
Each mini program account can create two environments for free. Recommendation: development environment and production environment
# Cloud Database Capabilities
Cloud development provides a JSON database with 2GB of free storage.
# Data Types
String
Number
Object
Array
Boolean
GeoPoint
Date (precise to milliseconds, client-side time)
Null
# Operating the Cloud Database
Mini program control (read/write subject to permission restrictions)
Cloud function control (has full read/write database permissions)
Console control (has full read/write database permissions)
# Cloud Database Permission Management
Only the creator can write, everyone can read (suitable for articles)
Only the creator can read/write (suitable for private content)
Only admins can write, everyone can read (suitable for product information)
Only admins can read/write (suitable for sensitive backend data)
# Operating the Cloud Database
// Initialize the database
const db = wx.cloud.database() // Initialize database on the mini program side; in cloud functions, no need for the wx. prefix
// Switch environments (development/production)
const testDB = wx.cloud.database({
env: 'test'
})
2
3
4
5
6
7
# Practical Course
# Serverless
Concept: Functions as a Service -- when you need backend services, you don't need to worry about IP addresses or domain names; you just call a function like any ordinary function.
# Advantages of Cloud Development
Rapid deployment, focus on core business, independently develop a complete WeChat mini program, no need to learn new languages -- just JavaScript, no operations needed -- saves costs, data security
# Cloud Development Capabilities
Cloud Functions: code that runs in the cloud, with built-in authentication via WeChat's proprietary protocol (Think: equivalent to the backend)
Cloud Database: a JSON database that can be operated from both the mini program client and cloud functions
Cloud Storage: store files in the cloud, with visual management in the cloud console
Cloud Calls: use mini program open APIs from cloud functions without authentication (e.g., push messages to users)
HTTP API: developers can access cloud resources from existing servers via HTTP API, enabling interoperability with cloud development (Purpose: for mini programs originally developed in the traditional model, enabling interoperability with cloud development)
# appID
A unique ID for each mini program
# Default Cloud Development Project Directory Structure
cloudfunctions ----------------------------Cloud Functions
callback ---------------------------------- Callback functions
config.json ---------------------------
index.js --------------------------------
package.json ------------------------
echo ----------------------------------------
login ----------------------------------------
openapi -----------------------------------
miniprogram ------------------------------- Mini Program
images ------------------------------------- Images
pages --------------------------------------- Pages
style ----------------------------------------- Styles
app.js --------------------------------------- Main project JS
app.json ----------------------------------- Global config
app.wxss ---------------------------------- Main project styles
sitemap.json ----------------------------- (Mini program SEO related)
project.config.json ----------------------- Project config
# Cloud Development Environments
Cloud development allows creating two environments. Recommendation: one for development, one for production
# Pre-Development Setup
Developer Tools > Details (top right) > Local Settings > Debug Base Library -- set to the latest version
app.js > wx.cloud.init > env -- set the environment ID
# project.config.json File Details
miniprogramRoot -- mini program frontend code directory
cloudfunctionRoot -- cloud function code directory
# app.json
pages -- set up pages; after setting, the corresponding directories and files will be auto-generated in the pages directory
Setting up bottom tab bar buttons:
"tabBar": {
"color": "#474747", // Text color
"selectedColor": "#d43c43", // Selected text color
"list": [{ // Button list, 2-5 items
"pagePath": "pages/playlist/playlist", // Page associated with the button
"text": "Music", // Text
"iconPath": "images/music.png", // Icon path
"selectedIconPath": "images/music-actived.png" // Selected icon path
},
{
"pagePath": "pages/blog/blog",
"text": "Discover",
"iconPath": "images/blog.png",
"selectedIconPath": "images/blog-actived.png"
},
{
"pagePath": "pages/profile/profile",
"text": "Profile",
"iconPath": "images/profile.png",
"selectedIconPath": "images/profile-actived.png"
}]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Icons from https://www.iconfont.cn (opens new window)
Alibaba's icon library, including vector icons, icon fonts, fonts, etc.
# Code Standards
Many companies follow this code style guide: https://github.com/airbnb/javascript (opens new window)
# "Music" Page Development
<!-- Carousel component. Parameters: indicator-dots (dots), autoplay, interval, duration (animation duration) -->
<swiper indicator-dots="true" circular="true" interval="3000" duration="500">
<block wx:for="{{swiperImgUrls}}" wx:key="{{index}}"> <!-- Empty node -->
<swiper-item>
<image src="{{item.url}}" mode="widthFix" class="img"></image>
</swiper-item>
</block>
</swiper>
2
3
4
5
6
7
8
9
# Custom Components
Creating a Component
Create directory: components > component folder name > right-click New Component
# Importing a Component
In the page's JSON file:
{
"usingComponents": {
"x-playlist":"/components/playlist/playlist"
}
}
2
3
4
5
In the page's WXML:
<x-playlist> </x-playlist>
Importing components in a page and importing sub-components within a component use the same method -- both require setting the JSON file.
# Component Data Passing
Parent component: when importing a component, define a custom attribute name and pass data into the child component
<!-- Parameter: playlist is a custom name, the data passed to the component -->
<x-playlist playlist="{{dataToPass}}"></x-playlist>
2
Child component: Child component's JS file:
/**
* Component property list
*/
properties: {
playlist:{ // Receive data passed from the parent component
type: Object // Data type
}
},
// The child component's WXML can directly use the data: {{playlist}}
2
3
4
5
6
7
8
9
10
# wx:key Usage
The key value should not use index, because when data changes cause DOM structure changes, index-based references won't update accordingly.
Use a unique value within each data item, such as an id
<block wx:for="{{swiperImgUrls}}" wx:key="url"> Here url doesn't need double braces; index would require {{}}
<view>
<image src="{{item.url}}" mode="widthFix" class="img"></image>
</view>
</block>
<view class="playlist-container">
<block wx:for="{{playlist}}" wx:key="_id">
<!-- Parameter: playlist is a custom name, the data passed to the component -->
<x-playlist playlist="{{item}}"></x-playlist>
</block>
</view>
2
3
4
5
6
7
8
9
10
11
12
13
# async/await Syntax
Currently, in cloud functions, since the minimum Node version is 8.9, async/await syntax is natively supported. However, on the mini program client side, it's different. In the WeChat developer tools and on Android devices (QQ browser's X5 kernel), async/await is natively supported, but on iOS devices with older versions, it's not. An additional file needs to be imported.
You can import this runtime.js (opens new window) file into files that use async/await.
// Note: it must be named regeneratorRuntime
import regeneratorRuntime from '../../utils/runtime.js'
2
# Using Cloud Functions
In the cloudfunctions directory, right-click New Node.js Cloud Function > enter directory name getPlaylist
To make requests from cloud functions to third-party servers, you need a third-party library
Installing dependencies
Right-click on the cloud function directory getPlaylist > Open in Terminal > enter commands:
npm install --save request
npm install --save request-promise
2
GitHub request-promise: https://github.com/request/request-promise (opens new window)
Then write the corresponding code
// Cloud function entry file
const cloud = require('wx-server-sdk')
cloud.init()
const rp = require('request-promise') // Requires installing dependencies
const URL = 'http://musicapi.xiecheng.live/personalized'
// Cloud function entry function
exports.main = async (event, context) => {
const playlist = await rp(URL).then((res) => {
return JSON.parse(res).result
})
console.log(playlist)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
After writing the code, right-click on the cloud function directory getPlaylist > Upload and Deploy: Install Dependencies in Cloud (don't upload node_modules) to upload and deploy the code to the cloud. Wait for the upload to succeed, then open the Cloud Development Console to see the uploaded cloud function, where you can also test it.
# Database Operations
Database > Create Collection > playlist
// Cloud function entry file
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database() // Initialize database; on the mini program side, add wx. prefix
const rp = require('request-promise') // Requires installing dependencies
const URL = 'http://musicapi.xiecheng.live/personalized' // Third-party server URL (the instructor deployed data from NetEase Cloud Music on his server, updated daily)
const playlistCollection = db.collection('playlist') // Get the playlist collection from the database
const MAX_LIMIT = 10 // Define a constant for the maximum number of records per query
// Cloud function entry function
exports.main = async (event, context) => {
/**
* Notes:
* - All database operations are asynchronous and require the await keyword
* - console.log output can be viewed in the Cloud Console under Cloud Function Testing
* - There's a per-query limit: cloud functions can fetch up to 100 records, mini program client up to 20
*/
// const list = await playlistCollection.get() // Get data from the collection (not used directly due to record limits)
// Breaking through the record limit (to read all data for comparison with third-party server data for deduplication)
const countResult = await playlistCollection.count() // Get total record count, returns an object
const total = countResult.total // Get the total count
const batchTimes = Math.ceil(total / MAX_LIMIT)
const tasks = []
for(let i = 0; i < batchTimes; i++) {
let promise = playlistCollection.skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() // Skip n records, fetch up to limit records
tasks.push(promise)
}
let list = {
data: []
}
if (tasks.length > 0) {
list = (await Promise.all(tasks)).reduce((acc, cur) => { // reduce array method for cumulative concatenation
return {
data: acc.data.concat(cur.data)
}
})
}
// Get data from the third-party server
const playlist = await rp(URL).then((res) => {
return JSON.parse(res).result
})
// Compare database and server data for deduplication (don't re-add data that already exists in the database)
const newData = []
for(let i = 0, len1 = playlist.length; i < len1; i++) {
let flag = true
for(let j = 0, len2 = list.data.length; j < len2; j++) {
if(playlist[i].id === list.data[j].id){
flag = false
break
}
}
if(flag){
newData.push(playlist[i])
}
}
// Insert data into the database, one record at a time
for (let i = 0, len = newData.length; i < len; i++) {
await playlistCollection.add({ // Add data to the database collection
data: {
...newData[i],
createTime: db.serverDate(), // db.serverDate() gets the server time
}
}).then((res) => { // Data added successfully
console.log('Data added successfully')
}).catch((err) => { // Failed
console.error(err)
})
}
return newData.length // Number of records inserted
}
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# Querying the Database
// Example of querying the database in a cloud function:
// Cloud function entry file
const cloud = require('wx-server-sdk')
cloud.init()
const TcbRouter = require('tcb-router')
const db = cloud.database() // Initialize database
const blogCollection = db.collection('blog') // Blog database collection
// Cloud function entry function
exports.main = async (event, context) => {
const app = new TcbRouter({ event }) // Initialize TcbRouter
app.router('list', async (ctx, next) => {
// skip: start from which record, limit: how many records to fetch, orderBy(sortField, sortOrder): desc/asc
ctx.body = await blogCollection.skip(event.start).limit(event.count)
.orderBy('createTime', 'desc').get().then((res) => {
return res.data
})
})
return app.serve() // Must return
}
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
# Cloud Function Debugging
Cloud functions can be tested in the cloud console
After calling a cloud function from the mini program, you can view the cloud function logs
# Scheduled Cloud Function Triggers
If a cloud function needs to be executed on a schedule, i.e., timed triggers, you can use cloud function timed triggers. A cloud function configured with a timed trigger will be automatically triggered at the corresponding time. The function's return value will not be returned to the caller.
Create a config.json file in the cloud function directory
{
"triggers": [
{
"name": "myTriggers",
"type": "timer",
"config":"0 0 10,14,16,20 * * * *" // Trigger once daily at 10, 14, 16, and 20 o'clock
}
]
}
2
3
4
5
6
7
8
9
After editing the trigger, right-click on the cloud function directory > Upload Trigger
# Configuring Cloud Function Timeout
When a cloud function is complex, the default 3-second timeout may not be enough. You can set a more reasonable timeout.
Cloud Development Console > Cloud Functions > Configuration > Timeout
# Pull-Up Loading and Pull-Down Refresh
In the page's JSON file:
"enablePullDownRefresh": true
In the page's JS file, these two functions exist:
/**
* Page event handler -- listen for user pull-down action
*/
onPullDownRefresh: function() {
this.setData({
playlist: []
})
this._getPlaylist()
},
/**
* Handler for page pull-up reaching bottom
*/
onReachBottom: function() {
this._getPlaylist()
},
After pull-down refresh data request completes:
wx.stopPullDownRefresh() // Stop the pull-down refresh animation
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
# Cloud Function Routing with tcb-router
Each user can only create 50 cloud functions per cloud environment
What if the mini program is very complex and 50 cloud functions aren't enough?
Group similar requests into the same cloud function
tcb-router is a Koa-style cloud function routing library
In simple terms, it lets you group many API endpoints into a single cloud function.
GitHub tcb-router: https://github.com/TencentCloudBase/tcb-router (opens new window)
Koa onion model...
Installation:
Open a terminal in the cloud function directory that uses tcb-router, then run:
npm install --save tcb-router
2
// Cloud function index.js
const TcbRouter = require('router'); // Required
exports.main = (event, context) => {
const app = new TcbRouter({ event }); // Required
// app.use means this middleware applies to all routes (global middleware). Optional
app.use(async (ctx, next) => { // This middleware is called by all routes, while route middleware is called individually
ctx.data = {}; // Data to pass to the mini program client
ctx.data.openId = event.userInfo.openId // The openId obtained here is distributed to all routes
await next(); // Execute next middleware
});
// Array route: this middleware applies to both user and timer routes
app.router(['user', 'timer'], async (ctx, next) => {
ctx.data.company = 'Tencent'; // Data obtained here is distributed to both user and timer routes
await next(); // Execute next middleware
});
// String route: this middleware only applies to the user route
app.router('user', async (ctx, next) => {
ctx.data.name = 'heyli'; // Data to pass to the mini program client
await next(); // Execute next middleware
}, async (ctx, next) => {
ctx.data.sex = 'male'; // Data to pass to the mini program client
await next(); // Execute next middleware
}, async (ctx) => {
ctx.data.city = 'Foshan'; // Data to pass to the mini program client
// ctx.body returns data to the mini program client
ctx.body = { code: 0, data: ctx.data}; // Data to pass to the mini program client
});
// String route: this middleware only applies to the timer route
app.router('timer', async (ctx, next) => {
ctx.data.name = 'flytam';
await next(); // Execute next middleware
}, async (ctx, next) => {
ctx.data.sex = await new Promise(resolve => {
// Wait 500ms, then execute next middleware
setTimeout(() => {
resolve('male');
}, 500);
});
await next(); // Execute next middleware
}, async (ctx)=> {
ctx.data.city = 'Taishan';
// ctx.body returns data to the mini program client
ctx.body = { code: 0, data: ctx.data };
});
return app.serve(); // Required
}
Mini program client:
// Call the cloud function named router, with the route name user
wx.cloud.callFunction({
// Cloud function name to call
name: "router",
// Parameters passed to the cloud function
data: {
$url: "user", // Route path to call, pass exact path or wildcard *
other: "xxx"
}
}).then((res) => {
console.log(res)
})
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
64
65
66
67
68
69
70
The tcb-router code above follows the onion model: entering each middleware from top to bottom, then exiting each middleware from bottom to top.
# Local Storage (Cache)
// Store:
wx.setStorageSync(key, data) // Synchronous storage (wait for storage to complete before continuing)
wx.setStorage(key, data) // Asynchronous storage (continues to next code even if storage hasn't completed)
// Read:
wx.getStorageSync(key) // Synchronous (wait for data to be read before continuing)
wx.setStorage(key) // Asynchronous
2
3
4
5
6
7
8
# API to Set Title
wx.setNavigationBarTitle({
title: '',
})
2
3
# Background Audio
BackgroundAudioManager (opens new window) -- the globally unique background audio manager
// Must be configured in app.json to enable background music playback
"requiredBackgroundModes": ["audio", "location"]
2
3
// Get the globally unique background audio manager
const backgroundAudioManager = wx.getBackgroundAudioManager()
backgroundAudioManager.src = audioURL
backgroundAudioManager.title = audioTitle
2
3
4
5
6
7
# createSelectorQuery for Querying Node Information
createSelectorQuery (opens new window) -- mini program method for querying nodes and other operations
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect() // Node layout information
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // Top boundary coordinate of #the-id node
res[1].scrollTop // Vertical scroll position of the viewport
})
2
3
4
5
6
7
# Component Methods
Component(Object object) (opens new window)
# Component Lifecycle
// Lifecycle
lifetimes: {
ready() { // Executes after the component layout is complete in the view layer
...
}
},
2
3
4
5
6
# Page Lifecycle of a Component's Host Page
Component({
pageLifetimes: {
show: function() {
// Page is shown
},
hide: function() {
// Page is hidden
},
resize: function(size) {
// Page size changed
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
# Component Data Observers
observers: { // Data observers (also fires on initial data load)
observedData(newData){
console.log(newData)
}
},
2
3
4
5
# Child Component Custom Event to Parent Component
Child component JS:
// Trigger a custom event to pass data to the parent. Parameter x (optional, data passed to the parent -- can be an object or other type)
this.triggerEvent('customEventName', parameterX)
Parent component WXML:
<child-component-tag bind:customEventName="handler" />
Parent component JS:
handler(event) {
console.log(event.detail.parameter)
}
2
3
4
5
6
7
8
9
10
11
12
# Parent Component Custom Event to Child Component
Parent component WXML:
<child-component-tag class="child-class-name">
Parent component JS:
// Select the component and pass event and parameters
this.selectComponent('.child-class-name').customEventName(parameters)
Child component JS:
methods: {
customEventName(parameterX){
console.log(parameterX)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# Sibling Component Event and Data Passing
Example: Child component 1 passes parameters to Child component 2
Parent component WXML:
<child-tag-1 bind:customEvent1="handler">
<child-tag-2 class="child2-class-name">
Parent component JS:
handler(event) {
this.selectComponent('.child2-class-name').customEvent2(event.detail.parameterX) // Pass value to child component 2
}
Child component 1 JS:
// Trigger custom event to pass data to parent. Parameter x (optional)
this.triggerEvent('customEvent1', parameterX)
Child component 2 JS:
methods: {
customEvent2(parameterX){
console.log(parameterX) // Receive value from parent
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Getting Device Information
wx.getSystemInfo(Object object) (opens new window)
wx.getSystemInfo({
success(res){
console.log(res) // Device information
}
})
2
3
4
5
# Scroll Component
scroll-view (opens new window)
<scroll-view scroll-y scroll-top="{{scrollTop}}" scroll-with-animation="true">
</scroll-view>
2
# Global Properties and Methods (similar to Vuex)
In app.js:
onLaunch: function () {
this.globalData = { // Set global properties and methods
test: 0
}
},
setGlobalData(dataItem, val) { // Set global property
this.globalData[dataItem] = val
},
getGlobalData(dataItem) { // Get global property
return this.globalData[dataItem]
}
In the page JS where you need to use it:
const app = getApp() // Call the app method at the top
// Set global property
app.setGlobalData('test', 1)
// Get global property
app.getGlobalData('test')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Toast Message
wx.showToast({
title: 'Success',
icon: 'success', // Icon: success, loading, none
duration: 2000
})
2
3
4
5
# "Discover" Page
# Using External Styles in Components
Components inside the components directory cannot directly use external styles. You can use the following methods:
Method 1:
Parent component WXML:
<!-- iconfont and icon-sousuo are style names passed into the component. iconfont(custom name)="iconfont(style name defined in external stylesheet)" -->
<x-search iconfont="iconfont" icon-sousuo="icon-sousuo"/>
Child component JS:
// External component styles
externalClasses: [
'iconfont', // Corresponds to the name before the = sign above
'icon-sousuo'
],
Child component WXML: now external styles are available
<i class="iconfont icon-sousuo" />
Note: if you want to further modify the style within the component, do not reference the external class name. Instead, use a different class name for modifications.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Method 2:
Removing style isolation (opens new window)
Inside the component:
Component({
options: {
styleIsolation: 'apply-shared'
}
})
2
3
4
5
6
# Component Slots
Single Slot
Parent component passes slot content:
<component-tag>
<view>
<view>Slot content</view>
<view>Slot content</view>
</view>
</component-tag>
Component defines the slot tag:
<view>
<!-- slot -->
<slot></slot>
</view>
2
3
4
5
6
7
8
9
10
11
12
13
Multiple Slots
Parent component passes slot content:
<component-tag>
<view slot="slot2">
<view>Slot 1 content</view>
<view>Slot 1 content</view>
</view>
<view slot="slot1">
<view>Slot 2 content</view>
<view>Slot 2 content</view>
</view>
</component-tag>
Component JS:
options: { // Settings
multipleSlots: true // Enable multiple slots
},
Component defines named slot tags:
<view>
<!-- Named slots -->
<slot name="slot1"></slot>
<slot name="slot2"></slot>
</view>
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
# Checking User Authorization
Authorization (opens new window)
// Check if user is authorized
wx.getSetting({
success: (res) => { // Arrow function changes inner this to outer this
console.log(res)
if (res.authSetting['scope.userInfo']) { // Authorized
wx.getUserInfo({ // Get user info
success(res) {
console.log(res)
}
})
} else { // Not authorized
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Button's Open Capability (Getting User Info) 1
<button class="login"
open-type="getUserInfo"
bindgetuserinfo="onGetUserInfo" // bindgetuserinfo is fixed
>
Get WeChat Authorization Info
</button>
The bindgetuserinfo event will ask the user whether to authorize
JS:
onGetUserInfo(event) { // Get user info
const userInfo = event.detail.userInfo
if (userInfo) { // User allowed authorization
this.setData({
modalShow: false
})
this.triggerEvent('loginSuccess', userInfo) // Pass user data to parent
} else { // User denied authorization
this.triggerEvent('loginFail')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Native Components
Native Components (opens new window)
auto-focus: automatically get focus
<textarea
class="content"
placeholder="Share something new..."
maxlength="140"
auto-focus
bindinput="onInput"
bindfocus="onFocus"
bindblur="onBlur"
></textarea>
2
3
4
5
6
7
8
9
10
11
12
# Selecting and Uploading Images
Choose Image (opens new window)
let max = 9 - this.data.images.length // How many more images can be selected
wx.chooseImage({
count: max, // How many more images can be selected
sizeType: ['original', 'compressed'], // Original and compressed
sourceType: ['album', 'camera'], // From album or camera
success: (res) => { // Arrow function changes this reference
console.log(res)
},
})
2
3
4
5
6
7
8
9
# Image Cropping
Image Cropping (opens new window)
<!-- mode: image cropping. aspectFill ensures the short edge is fully displayed -->
<image class="image" src="{{item}}" mode="aspectFill"></image>
2
# Getting Custom data-* Attributes (Deleting Images)
<!-- Display images -->
<block wx:for="{{images}}" wx:key="*this">
<view class="image-wrap">
<!-- mode: image cropping. aspectFill ensures the short edge is fully displayed -->
<image class="image" src="{{item}}" mode="aspectFill"></image>
<icon class="iconfont icon-shanchu" bindtap="onDelImage" data-index="{{index}}"></icon>
</view>
</block>
// Delete image
onDelImage(event) {
// event.target.dataset.index gets the value of the data-index attribute
this.data.images.splice(event.target.dataset.index, 1) // splice modifies the original array
this.setData({
images: this.data.images
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Full-Screen Image Preview (Click to Enlarge)
Full-Screen Image Preview (opens new window)
// Full-screen image preview
onPreviewImage(event) {
wx.previewImage({
urls: this.data.images, // Image URL list
current: event.target.dataset.imgsrc // Currently previewed image URL
})
},
2
3
4
5
6
7
# Uploading Files to Cloud Storage (Blog Publishing Example)
Upload File to Cloud Storage (opens new window)
// Combined with the "publish" example:
send() {
// Validate input
if (content.trim() === '') { // trim() removes whitespace
wx.showToast({
title: 'Please enter content',
icon: 'none'
})
return
}
wx.showLoading({
title: 'Publishing',
})
/**
* Implementation approach and steps:
* 1. Images -> Upload to cloud storage -> Generate image fileIDs (cloud file IDs)
* 2. Data -> Insert into cloud database
* Data includes: text content, image fileIDs, nickname, avatar, publish time, openId (user unique identifier; the system auto-adds _openId field when inserting into the database)
*/
let promiseArr = []
let fileIds = []
// Upload images to cloud storage
this.data.images.forEach((item) => {
let p = new Promise((resolve, reject) => {
let suffix = /\.\w+$/.exec(item)[0] // File extension
wx.cloud.uploadFile({ // Only one file can be uploaded at a time
/**
* cloudPath: cloud path. If the path is the same, later uploads overwrite earlier ones
* Path: blog/ folder in cloud storage + Date.now() timestamp + Math.random()*1000000 random number + file extension
*/
cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,
filePath: item, // Local temporary file path
success: (res) => {
fileIds.push(res.fileID)
resolve()
},
fail: (err) => {
console.error(err)
reject()
}
})
})
promiseArr.push(p)
})
// Save to cloud database
Promise.all(promiseArr).then((res) => {
db.collection('blog').add({
data: {
...userInfo, // Nickname, avatar
content, // Content
img: fileIds, // Image fileID list
createTime: db.serverDate() // Creation time, using server time
}
}).then((res) => {
wx.hideLoading()
wx.showToast({
title: 'Published successfully',
})
// Return to blog page and refresh
wx.navigateBack()
})
}).catch((err) => {
wx.hideLoading()
wx.showToast({
title: 'Sorry, publishing failed',
icon: 'none'
})
})
},
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
64
65
66
67
68
69
70
71
# JS Modularization (Date Formatting)
Create a formatTime.js file in the utils directory
// Date formatting module
module.exports = (date) => { // date parameter must be a Date type
let fmt = 'yyyy-MM-dd hh:mm:ss' // Predefined format
const o = {
// + means one or more in regex
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
if (/(y+)/.test(fmt)) {
// $1 refers to the first group in the regex, i.e., (y+)
fmt = fmt.replace(RegExp.$1, date.getFullYear()) // replace
}
for (let k in o) {
if (new RegExp('('+ k +')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, o[k].toString().length === 1 ? '0' + o[k] : o[k])
}
}
return fmt
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Import the JS module in a component
import formatTime from '../../utils/formatTime.js'
Usage:
formatTime(new Date('Wed Aug 28 2019 16:23:06 GMT+0800 (China Standard Time)'))
2
3
4
# Preventing Event Bubbling
Both bind and catch can bindEvents. The difference is that bind allows event bubbling, while catch does not.
# Navigate Back and Execute a Method
// Return to the blog page and refresh
wx.navigateBack()
const pages = getCurrentPages() // Get the current page stack
const prevPage = pages[pages.length - 2] // Get the previous page
prevPage.onPullDownRefresh() // Execute the previous page's onPullDownRefresh method
2
3
4
5
# Image Lazy Loading
Set lazy-load to true on the image tag
<image class="img" src="{{item}}" lazy-load="true"></image>
.img {
background: #eee;
}
2
3
4
5
6
A background image or background color on the image element can serve as a lazy-loading placeholder
# Fuzzy Search
// Get blog list
app.router('blogList', async (ctx, next) => {
const keyword = event.keyword // Search keyword passed from the API call
let w = {}
if (keyword.trim() != '') {
w = {
content: db.RegExp({ // Regex
regexp: keyword,
options: 'i' // i means case-insensitive
})
}
}
// where: query condition, skip: start from which record, limit: how many records, orderBy(field, order): desc/asc
ctx.body = await blogCollection.where(w).skip(event.start).limit(event.count)
.orderBy('createTime', 'desc').get().then((res) => {
return res.data
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Improving Fuzzy Search Performance (Adding Indexes, significant for large datasets)
Cloud Development Console > Database > corresponding collection > Index Management > Add Index > enter custom index name, whether the field value is unique, field name to query, ascending/descending > OK
# Mini Program Client-Side Database Calls
Database operations are typically written in cloud functions, but the mini program client can also operate on the database directly.
The mini program client can query up to 20 records at a time; cloud functions can query up to 100. You can use multiple queries with concatenation to break through the limit.
// Mini program client-side database call example
const db = wx.cloud.database() // Initialize database
db.collection('blog').orderBy('createTime','deac').get().then((res) => {
console.log(res)
})
2
3
4
5
# Cloud Database Permission Management
Note: The cloud console and server-side (cloud functions) always have full read/write permissions.
Permission management only applies to requests initiated from the mini program client.
Only the creator can write, everyone can read (suitable for articles)
Only the creator can read/write (suitable for private content)
Only admins can write, everyone can read (suitable for product information)
Only admins can read/write (suitable for sensitive backend data)
# Three Design Approaches for 1-to-N Relationships in Databases
# Approach 1: N is a small number (within a few dozen)
Store N sub-items within 1 record
For example, a blog post has at most 9 images; these 9 images can be stored alongside other data in a single record.
[
{
id:...
img:[
'...', '...', '...', '...', '...', '...', '...', '...', '...'
]
}
]
2
3
4
5
6
7
8
# Approach 2: N is a moderate number (dozens to hundreds)
Store IDs of each N item in the 1 record
Use two database collections:
One is a "catalog" collection, storing IDs from the "details" collection
One is a "details" collection, where each record has its own unique ID and detailed data
Catalog collection:
[
{
'id':"11",
'name': 'Product 1',
'xqs': ['111','222','333', ... ] // Store IDs from the details collection
}
]
Details collection:
[
{'id':"111",name:'Part 1',title:'...' ...},
{'id':"222",name:'Part 2',title:'...' ...},
{'id':"333",name:'Part 3',title:'...' ...},
...
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Like the relationship between a playlist and song details.
# Approach 3: N is very large (hundreds, thousands, or more)
Each N item stores the ID of the 1 record
For example, a Sina blog post with thousands of comments
A blog post:
[{
'id':'11',
'content':'Blog content'
...
}]
Thousands of comments:
[
{
'id':'111111'
'blogId':'11', // This ID corresponds to the blog post's ID
'content': 'Comment 1'
},
{
'id':'222222'
'blogId':'11', // This ID corresponds to the blog post's ID
'content': 'Comment 2'
},
{
'id':'33333'
'blogId':'11', // This ID corresponds to the blog post's ID
'content': 'Comment 3'
},
...
]
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
# Cloud Calls
Call server-side open APIs through cloud functions
These APIs include: template message push, mini program QR code generation, etc.
# Template Message Push
1. A form element is required to trigger message push, with report-submit="true" set
<form slot="modal-content" report-submit="true" bind:submit="onSend">
<textarea name="content" class="comment-content" placeholder="Write a comment" value="{{content}}" fixed="true"></textarea>
<button class="send" form-type="submit">Send</button>
</form>
2
3
4
5
2. Configuration is needed on the WeChat Official Platform:
WeChat Official Platform > Features > Template Messages > Add Template > Choose the appropriate template > After adding, you'll get a template ID
3. Create a new cloud function for the cloud call. Create a config.json configuration file in that cloud function directory for permissions
config.json:
{
"permissions": {
"openapi": [
"templateMessage.send"
]
}
}
2
3
4
5
6
7
Cloud function for message push:
// Cloud function entry function
exports.main = async (event, context) => {
// Get openid
const { OPENID } = cloud.getWXContext()
// Template push message
const result = await cloud.openapi.templateMessage.send({
touser: OPENID,
page: `/pages/blog-comment/blog-comment?blogId=${event.blogId}`, // Page opened when user taps the push notification
data: { // Template content; keyword fields correspond to fields set in the template on the official platform
keyword1: { // Comment content
value: event.context
},
keyword2: { // Comment time
value: event.time
}
},
templateId: 'LNwKMcYwlz-0HabgBhmZi6CWZrlNSBiNJ2h0SMorcxQ', // Template ID, obtained from the official platform
formId: event.formId // Form ID that triggers the push
})
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
4. Call the message push cloud function after form submission completes
wx.cloud.callFunction({
name: 'sendMessage',
data: {
content,
formId,
blogId: this.properties.blogId
}
}).then((res) => {
console.log(res)
})
2
3
4
5
6
7
8
9
10
# Cloud Function Multi-Collection Database Query
// Blog detail (blog content + comments)
app.router('blogDetail', async(ctx, next) => {
let blogId = event.blogId
// Blog content
let detail = await blogCollection.where({
_id: blogId
}).get().then((res) => {
return res.data
})
// Comment query
const countResult = await blogCollection.count()
const total = countResult.total
let commentList = {
data: []
}
if (total > 0) {
// Break through the 100-record limit
const batchTimes = Math.ceil(total / MAX_LIMIT)
const tasks = []
for (let i = 0; i < batchTimes; i++) {
let promise = db.collection('blog-comment').skip(i * MAX_LIMIT)
.limit(MAX_LIMIT).where({
blogId
}).orderBy('createTime', 'desc').get()
tasks.push(promise)
}
if (tasks.length > 0) {
commentList = (await Promise.all(tasks)).reduce((acc, cur) => {
return {
data: acc.data.concat(cur.data)
}
})
}
}
ctx.body = {
detail,
commentList
}
})
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
# Share Functionality
The share feature requires a button tag with open-type="share"
<button open-type="share" data-blogid="{{blogId}}" data-blog="{{blog}}" class="share-btn" hover-class="share-hover">
<i class="iconfont icon-fenxiang icon"></i>
<text>Share</text>
</button>
2
3
4
5
In JS, the onShareAppMessage method is automatically called when the button is tapped
onShareAppMessage: function (event) {
console.log(event)
// Customize the share card
let blogObj = event.target.dataset.blog
return {
title: blogObj.content,
path: `/pages/blog-comment/blog-comment?blogId=${blogObj._id}`,
// imageUrl: '' // Custom image, cloud storage images not supported
}
}
2
3
4
5
6
7
8
9
10
11
# Different Ways to Get User Info in Different Scenarios
# Scenario 1: Just want to display your own nickname and avatar on the UI
Using a component approach: get different user data based on the type
This approach does not require authorization; can only be used to display your own info in WXML
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
...
2
3
# Scenario 2: Getting user info in JavaScript
This approach requires user authorization before getting user info
wx.getUserInfo (opens new window)
wx.getUserInfo({
success: (res) => {
console.log(res)
}
})
2
3
4
5
Authorization is needed first if not yet authorized:
// Check if user is authorized
wx.getSetting({
success: (res) => { // Arrow function changes inner this to outer this
if (res.authSetting['scope.userInfo']) { // Authorized
wx.getUserInfo({ // Get user info
success: (res) => { // Arrow function changes inner this to outer this
app.setGlobalData('userInfo', res.userInfo) // Set global property
this.onLoginSuccess({
detail: res.userInfo
})
}
})
} else { // Not authorized
this.setData({ // Show modal with user info button
modalShow: true
})
}
}
})
Authorization button:
<button class="login" open-type="getUserInfo" bindgetuserinfo="onGetUserInfo">Get WeChat Authorization Info</button>
onGetUserInfo(event) { // Get user info
const userInfo = event.detail.userInfo
if (userInfo) { // User allowed authorization
this.setData({
modalShow: false
})
this.triggerEvent('loginSuccess', userInfo) // Pass user data to parent
} else { // User denied authorization
this.triggerEvent('loginFail')
}
}
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
Note: this approach does not obtain the openId
# Scenario 3: Getting the openId
Getting the openId does not require user authorization
- Traditional development approach: the backend server is self-developed, no cloud development
Mini program client WeChat server Backend server
Steps:
Mini program client calls wx.login to get a code from WeChat server
Mini program client calls wx.request to pass the code to the backend server
Backend server uses the code to exchange for openid and session_key from WeChat server
Backend server sends the openid to the mini program client
- Cloud development approach
In the login cloud function:
// Get WX Context (WeChat call context), including OPENID, APPID, and UNIONID (if conditions are met)
const wxContext = cloud.getWXContext()
return {
event,
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
}
2
3
4
5
6
7
8
9
Regular button:
<button bindtap="getOpenid">Get openid</button>
getOpenid() {
wx.cloud.callFunction({
name: 'login'
}).then((res) => {
console.log(res)
})
}
2
3
4
5
6
7
8
9
10
openid is different between mini programs and official accounts
unionid is the same between mini programs and official accounts
# "Profile" Page
JSON file
"navigationBarTitleText": "Profile",
"disableScroll": true // Disable page scrolling
2
# Navigation Page Link
# Background Images
WXSS background images don't support local relative paths, only network images and base64 images
Base64 images are recommended; keep file sizes small.
# The page Tag Present on Every Page
page {
background-color: #f1f1f1;
}
2
3
# Play History and Local Storage
Option 1: Store play history in the database, accessible across devices. Slower read speed.
Option 2: Store play history locally, accessible only on the current device. Faster read speed.
This project uses local storage:
Use openid as the local storage key, with play history as the value
Get openid in app.js, i.e., obtain openid when the mini program opens.
// app.js
onLaunch: function () {
this.getOpenid() // Get and store openid
},
getOpenid() { // Get and store openid
wx.cloud.callFunction({
name: 'login'
}).then((res) => {
const openid = res.result.openid
this.globalData.openid = openid // Save to global variable
if (wx.getStorageSync(openid) == '') { // User has never opened the mini program, no openid stored locally
wx.setStorageSync(openid, []) // Store openid locally
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
During song playback:
// Save play history to local storage
savePlayHistory() {
const currentSong = musiclist[nowPlayingIndex] // Currently playing song
const openid = app.globalData.openid // Get openid from global property
const playHistory = wx.getStorageSync(openid) // Get play history array from local storage
for (let i = 0, len = playHistory.length; i < len; i++) {
if (playHistory[i].id === currentSong.id) { // Current song already exists in play history
playHistory.splice(i, 1) // Remove the existing record
break
}
}
playHistory.unshift(currentSong) // Insert at the beginning
wx.setStorage({ // Save locally
key: openid,
data: playHistory
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Play history page retrieval:
onLoad: function (options) {
const openid = app.globalData.openid // Get openid from global property
const playHistory = wx.getStorageSync(openid) // Read local play history data
if (playHistory.length !== 0) { // Has play history
this.setData({
playHistory
})
wx.setStorage({ // Replace the musiclist (playlist) in storage with the play history list
key: 'musiclist',
data: playHistory,
})
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# My Posts
The code demonstrates fetching data from both cloud functions and the mini program client. Fetching from the mini program client has permission management capabilities and doesn't require passing openid.
# Mini Program QR Code
Get Mini Program QR Code (opens new window)
This project demonstrates using API B: suitable for scenarios requiring a very large number of codes (opens new window) via cloud call.
Steps:
Create cloud function getQRCode
Create config.json in the getQRCode cloud function directory for permission configuration:
{ "permissions":{ "openapi":[ "wxacode.getUnlimited" ] } }1
2
3
4
5
6
7
// Cloud function entry function
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
const result = await cloud.openapi.wxacode.getUnlimited({
scene: wxContext.OPENID, // Link parameter; doesn't have to be openid, can be any data used to identify who scanned the code
// page: "pages/blog/blog" // Defaults to the main page
// lineColor: { // Line color
// 'r': 211,
// 'g': 60,
// 'b': 57
// },
// isHyaline: true // Whether transparent
})
// result is binary data; upload to cloud storage first
// Upload to cloud storage
const upload = await cloud.uploadFile({
cloudPath: 'qrcode/qrcode' + Date.now() + Math.random() + '.png',
fileContent: result.buffer
})
return upload.fileID
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Detecting QR Code Entry and Getting Parameters
// In the onLoad method of the page entered via QR code:
onLoad: function (options) {
console.log(options.scene) // Get the QR code entry parameter
}
2
3
4
5
# Version Update Detection
// app.js
onLaunch: function(options) {
this.checkUpate()
},
checkUpate(){
const updateManager = wx.getUpdateManager()
// Check for version updates
updateManager.onCheckForUpdate((res)=>{
if (res.hasUpdate){
updateManager.onUpdateReady(()=>{
wx.showModal({
title: 'Update Available',
content: 'A new version is ready. Restart the app?',
success(res){
if(res.confirm){
updateManager.applyUpdate()
}
}
})
})
}
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Performance Optimization
Official Performance Tips (opens new window)
Use the developer tools' debugger and Audits for scoring, then optimize the project based on the suggestions.
# Scene Values and Their Applications
Scene Values (opens new window)
Scene values describe the path through which a user enters the mini program. See the Scene Value List (opens new window) for complete details.
Different business logic can be implemented based on different entry scenes. For example, a food ordering mini program with QR codes posted in the restaurant -- users scanning the code can go directly to the ordering page.
In app.js, onLaunch(options) and onShow(options), options contains the scene value.
In the developer tools, use "Switch Background" to simulate different entry scenes.
# Mini Program "SEO" -- Page Indexing with sitemap
The sitemap.json file is in the same directory as app.js, used to configure indexing rules.
sitemap Configuration (opens new window)
Purpose:
Enable mini program search to find results based on mini program content.
How to use:
On the WeChat Official Platform: Mini Program Info > Page Indexing Settings > Enable (enabled by default)
Open sitemap.json and configure indexing rules
{
"desc": "For more info, see https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{ // Indexing rules
"action": "allow",// Whether to be indexed: allow or disallow
"page": "*" // * means all pages are indexed
}]
}
2
3
4
5
6
7
{
"desc": "For more info, see https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{ // Indexing rules, can add multiple
"action": "allow", // Whether to be indexed
"page":"pages/player/player", // Page
"params": ["musicId","index"], // Dynamic link parameters
"matching":'exact' // Whether params must match exactly
},{
"action": "disallow", // Whether to be indexed
"page":"*", /
}]
}
// The above rules mean: only the player page is indexed, all other pages are not
2
3
4
5
6
7
8
9
10
11
12
13
14
# Mini Program Launch Review Process
WeChat Official Platform > Version Management > Upload as beta version > Submit for review > Launch
# Admin Panel
# Architecture Diagram
Frontend Backend Mini Program Cloud Dev
vue-admin-template <---via ajax--> Koa2-based; HTTP API or tcb-admin-node ---->Cloud Functions, Cloud DB, Cloud Storage
# Building the Admin Frontend with vue-admin-template
vue-element-admin (opens new window) -- admin system template based on Element
vue-admin-template (opens new window) -- simplified version of vue-element-admin (opens new window)
See the official documentation for usage.
# Building the Admin Backend with Koa2
Official site: https://koa.bootcss.com/ (opens new window)
Create a new empty folder wx-music-admin-backend, open terminal:
# Generate package.json file, -y means default configuration
npm init -y
# Install Koa
npm install koa
# Create app.js file (Windows 10 command), as the project entry file
type nul > app.js
2
3
4
5
6
7
8
9
app.js:
const Koa = require('koa')
const chalk = require('chalk') // Plugin for colored console.log output, requires: npm i chalk
const app = new Koa()
app.use(async (ctx) => {
ctx.body = 'Hello Wolrd'
})
const port = 3000
app.listen(port, () => { // Port, callback after starting the service
console.log(chalk.green(`> Service started, visit: http://localhost:${port}`))
})
2
3
4
5
6
7
8
9
10
11
Terminal:
# Start the project with Node
node app.js
# Visit: http://localhost:3000
2
3
4
# Caching and Refreshing the access_token
access_token, the WeChat API call credential. Details: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html (opens new window)
Back to the project wx-music-admin-backend, open terminal:
# HTTP request plugin
npm i request
npm i request-promise
2
3
/**
* Get WeChat API call credential
* Details: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
*/
const rp = require('request-promise') // Node HTTP request plugin
const fs = require('fs') // Node file module
const path = require('path') // Node path module
// fileName = __dirname (absolute path of the current file's directory) + './access_token.json'
const fileName = path.resolve(__dirname, './access_token.json')
// Get these parameters from: WeChat Official Platform > Development > Development Settings
const APPID = 'wxc4e0b2d98063b103'
const APPSECRET = 'xxx' // Mini program secret key, keep it confidential!
// WeChat access_token request URL
const URL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
// Send request to get AccessToken
const updateAccessToken = async () => {
const resStr = await rp(URL)
const res = JSON.parse(resStr)
if (res.access_token) {
// Node writeFile: param 1: file path, param 2: file content. First write creates file, subsequent writes overwrite
fs.writeFileSync(fileName, JSON.stringify({
access_token: res.access_token,
createTime: new Date()
}))
} else { // If not obtained, try again
await updateAccessToken()
}
}
// Read access_token
const getAccessToken = async () => {
try {
// Node readFile: param 1: file to read, param 2: character set
const readRes = fs.readFileSync(fileName, 'utf8')
const readObj = JSON.parse(readRes)
// If the server was down and setInterval couldn't refresh on time, check access_token validity here
const createTime = new Date(readObj.createTime).getTime()
const nowTime = new Date().getTime()
if((nowTime - createTime) / 1000 / 60 / 60 >= 2) {
await updateAccessToken()
await getAccessToken()
return
}
return readObj.access_token
} catch (error) { // Catch exception: file doesn't exist yet, create it first
await updateAccessToken()
await getAccessToken()
}
}
// access_token is valid for 2 hours, refresh on schedule
setInterval(async () => {
await updateAccessToken()
}, (7200 - 300) * 1000)
module.exports = getAccessToken
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
64
65
# Triggering Cloud Functions via HTTP API from the Backend
HTTP API to Trigger Cloud Functions (opens new window)
# CORS Issues and Backend Solutions
CORS issues arise when the admin frontend requests data from the admin backend
// Situations that cause CORS issues:
// http://www.a.com https://www.a.com -- different protocols
// http://www.a.com http://www.b.com -- different domains
// http://www.a.com http://news.a.com -- different subdomains
// http://www.a.com:8080 http://www.a.com:3000 -- different ports
// Methods to solve CORS:
// JSONP
// iframe
// postMessage cross-origin
// Cross-Origin Resource Sharing (CORS)
2
3
4
5
6
7
8
9
10
11
Admin backend, install:
// Koa package for solving CORS issues
npm i koa2-cors
2
app.js
// Handle CORS
app.use(cors({
origin: ['http://localhost:9528'], // Allowed domain to access this service
credentials: true
}))
2
3
4
5
# Cloud Database CRUD APIs
Database Query Records (opens new window)
# Receiving POST Request Data on the Backend
GET request data can be accessed directly via ctx.request.query, but POST requests require koa-body
npm i koa-body
app.js
const koaBody = require('koa-body') // Required for accessing POST request data from the frontend
// Parse POST request body
app.use(koaBody({
multipart: true
}))
2
3
4
5
6
7
API route file:
router.post('/updatePlaylist', async (ctx, next) => {
const params = ctx.request.body // Get POST request data from the frontend; requires koa-body installed and configured
})
2
3
4
5
# Getting Cloud Storage Images from the Backend
Upload images to cloud storage; create an image collection in the cloud database and add data fields including the cloud file's fileID.
The backend project retrieves data by calling the cloud database
router.get('/list', async (ctx, next) => {
// The API reads up to 10 records from the database by default
const query = `db.collection('swiper').get()`
const res = await callCloudDB(ctx, 'databasequery', query)
console.log(res)
})
2
3
4
5
6
7
However, the retrieved data contains fileIDs, which cannot directly display images. You need to use the WeChat HTTP API for cloud storage to get the image URLs.
Get Cloud Storage Files (opens new window)