您现在的位置是:网站首页> 小程序设计
小程序研发关注问题收集
- 小程序设计
- 2026-07-16
- 9419人已阅读
小程序研发关注问题收集

***使用 Uniapp + UniCloud 云开发微信小程序***
小程序请求不备案不要HTTPS方法(云函数使用http请求其他IP数据)
uniapp实现如何使用微信小程序云开发技术实现数据存储和实时通信
uni-app 微信小程序 WASM SQLite(wa-sqlite)完整示例,不用现成插件
一、分包是什么、核心作用
1.底层原理
小程序 / App 打包时分为主包 + 若干分包:
1 启动 App / 小程序时,只下载主包,打开分包页面时才按需下载对应分包;
2 解决微信小程序主包 2M 上限,降低首次启动加载体积,加快打开速度;
3 按业务模块拆分代码,大型项目解耦、团队并行开发。
2. 适用平台
1 微信 / 支付宝 / 百度 / 抖音小程序:强制需要分包,平台有严格体积限制;
2 App 端(Android/iOS):uni-app 2.7.12 + 支持分包,不分包下载提速,仅优化首页启动速度;
3 H5 无分包概念。
二、三种包类型:主包 / 普通分包 / 独立分包
1. 主包(必存在)
存放内容
tabBar 全部页面、启动首页、登录页等高频核心页面;
全局公共资源:根目录components、utils、static、全局 uni_modules、App.vue、main.js;
访问规则
启动直接加载,可访问所有分包资源。
微信体积限制
主包 ≤ 2M。
2. 普通分包(最常用,independent:false 默认)
目录规范
与pages同级新建文件夹(如packageOrder、packageMine),内部存放页面、私有 components、私有 static。
资源访问权限
✅ 可访问自身内部组件、静态、js;
✅ 可访问主包全局组件、工具、图片;
❌ 不能访问其他分包的资源、页面、组件。
加载逻辑
必须先加载主包,跳转分包页面时自动下载分包,依赖主包运行环境。
3. 独立分包(independent:true,特殊场景)
特点
不依赖主包可单独运行,无需先下载主包,适合分享、广告、活动落地页;
权限限制(严格)
✅ 仅能使用自身目录内 components/static/js;
❌ 完全不能引用主包任何资源(全局组件、utils、主包图片都会报错);
❌ 无法使用主包全局状态(vuex/pinia、全局拦截器、全局挂载方法);
使用场景
活动 H5 分享、商品分享详情页、临时营销页面,减少主包体积。
三、微信小程序硬性体积限制(重点)
单个主包 / 单个分包:最大 2M;
整个项目所有分包总和:≤30M;
tabBar 页面必须放在主包 pages 内,禁止放入分包;
图片单张建议<200KB,大图放云端 OSS,不要本地 static。
四、标准目录结构(分包规范)
uni-app项目根目录
├── pages # 主包(tab、首页、登录)
│ ├── index/index
│ ├── mine/mine
├── packageOrder # 普通分包:订单模块(与pages同级)
│ ├── pages
│ ├── orderList/orderList.vue
│ ├── orderDetail/orderDetail.vue
│ ├── components # 分包私有组件(仅分包内可用)
│ ├── static # 分包私有图片(不打进主包)
├── packageActivity # 独立分包(independent:true)
│ ├── pages
│ ├── coupon/coupon.vue
├── components # 全局公共组件(主包)
├── utils # 全局工具(主包)
├── static # 全局静态资源(主包)
├── pages.json # 分包核心配置文件
└── manifest.json # 开启分包优化
强制规则
分包 root 目录必须和 pages 同级,不能嵌套在其他分包内部;
分包内页面路径是root下相对路径,配置 pages.json 时不能写全路径;
分包私有 static、components 仅当前分包可用,主包无法引用。
五、完整配置步骤(pages.json + manifest.json)
步骤 1:pages.json 配置分包(核心)
{ // ========== 主包页面 ========== "pages": [ "pages/index/index", "pages/mine/mine" ], "tabBar": { "list": [ // tab页面全部写在pages主包内 {"pagePath":"pages/index/index","text":"首页"} ] }, // ========== 分包配置 subPackages ========== "subPackages": [ // 1.普通分包:订单模块 { "root": "packageOrder", // 分包根目录(相对项目根) "pages": [ "pages/orderList/orderList", // root下相对路径 "pages/orderDetail/orderDetail" ] }, // 2.独立分包:活动页 independent:true { "root": "packageActivity", "independent": true, // 标记独立分包 "pages": [ "pages/coupon/coupon" ] } ], // ========== 分包预加载 preloadRule(优化跳转等待) ========== "preloadRule": { // 进入首页后,WiFi下预下载订单分包 "pages/index/index": { "network": "wifi", // wifi/all(任意网络) "packages": ["packageOrder"] // 预加载分包root名称 } } }
subPackages 字段说明
| 字段 | 必填 | 说明 |
|---|---|---|
| root | 是 | 分包根文件夹名称,和 pages 同级 |
| pages | 是 | 分包页面数组,路径为 root 内部相对地址 |
| independent | 否 | true = 独立分包;false / 不写 = 普通分包 |
| name | 否 | 分包别名,调试、预加载使用 |
步骤 2:manifest.json 开启分包优化(必配,否则分包 js 打进主包)
"mp-weixin": {
"optimization": {
"subPackages": true // 开启分包代码隔离,分包JS不打包进主包vendor
}
}
作用:分包独有的 js、组件不会打包到主包 vendor.js,大幅减小主包体积。
六、页面跳转分包规范
1. 跳转普通分包页面(路径以/分包root/开头)
// 正确写法:完整根路径
uni.navigateTo({
url: "/packageOrder/pages/orderList/orderList"
})
2. 独立分包跳转无特殊语法,路径规则一致
3. 禁止跨分包直接访问组件
❌ 错误:主包 import 其他分包 components;分包 A import 分包 B 组件;
✅ 正确:跨分包公共组件放主包 components;分包私有组件仅自身使用。
七、分包预加载 preloadRule(解决分包跳转白屏等待)
作用
进入某主包页面后,后台提前下载常用分包,用户跳转时无需等待加载。
配置示例
"preloadRule": {
// 进入我的页面,所有网络下预加载订单、售后分包
"pages/mine/mine": {
"network": "all",
"packages": ["packageOrder","packageAfterSale"]
}
}
使用场景
首页、个人中心等高频页面,预加载用户大概率会打开的分包。
八、Vue2 vs Vue3 分包差异
1 Vue3 (Vite) 项目:自动识别分包配置,编译更快,分包隔离更彻底;独立分包开箱即用;
2 Vue2 (Webpack) 项目:独立分包需要额外插件支持,必须开启 manifest 分包优化,否则容易主包膨胀;
3 页面生命周期、路由跳转逻辑无任何区别。
九、分包常见坑与优化方案
坑 1:主包 vendor.js 体积过大
解决:
manifest 开启subPackages:true;
仅被单个分包使用的 uni_modules / 工具函数,移入分包目录;
大型组件库(如 uView)按需引入,不要全局完整导入。
坑 2:分包跳转白屏、加载慢
解决:
配置 preloadRule 预加载常用分包;
分包页面减少大型图片,大图走云端;
分包内 js 拆分,避免单个页面引入大量第三方 SDK。
坑 3:独立分包无法使用全局 this、store、全局拦截器
原因:独立分包不加载主包运行环境;
解决方案:独立分包内单独封装请求、本地缓存逻辑,不依赖全局状态。
坑 4:tab 页面放分包,编译报错
硬性规则:tabBar 页面只能写在主包 pages 数组。
坑 5:分包内图片主包访问不到
分包 static 私有资源路径只能分包内使用,全局图片统一放根目录 static。
十、分包最佳实践(企业项目标准)
1 分包拆分原则
主包:首页、tab、登录、公共工具 / 组件;
分包按业务拆分:订单、商品详情、售后、会员、营销活动;
低频活动页使用独立分包,不占用主包体积。
2 资源规范
全局组件 / 工具 / 图片 → 主包;
模块独有组件、图片 → 当前分包私有目录;
超过 200KB 图片全部上传 OSS,不本地存放。
3 性能优化
所有小程序开启 manifest 分包隔离;
个人中心、首页配置预加载;
每个分包体积控制在 1M 以内,预留冗余空间。
4 团队协作
不同业务模块拆分独立分包,代码互不干扰,合并代码冲突少。
十一、App 端分包补充说明
App 分包不限制 2M,主要优化首页启动速度:
1 manifest-app 配置开启分包;
2 分包逻辑、目录、pages.json 配置与小程序完全一致;
3App 不存在下载分包等待,分包是打包时拆分,不影响运行体验。
uni-app map 组件完整解答
只展示地图 + markers 标记、画线、圆形、show-location 当前点位(纯前端渲染),无需任何 key,uniapp 编译微信小程序直接可用
需要地址互转、搜索、路线规划(调用腾讯服务接口),必须申请 KEY
只要用到下面任意功能,就要去腾讯位置服务申请小程序类型 key:
1逆地址解析(经纬度 → 详细地址文字)
2正地址解析(地址 → 经纬度)
3周边门店 / 地点搜索
4驾车 / 步行路线规划
5qqmap-wx-jssdk 地图 JS SDK
一、核心结论
1 uni-app 全局支持 <map> 原生组件,Vue2、Vue3 项目都能用;
2 微信小程序完全可以使用 map 组件,底层封装微信原生地图(腾讯地图内核),开箱即用,是小程序标准能力
二、各平台兼容一览
| 平台 | 是否支持 map | 底层地图服务商 |
|---|---|---|
| 微信小程序 | ✅ 原生支持 | 腾讯地图(无需额外配置 key,组件直接渲染) |
| 支付宝小程序 | ✅ | 高德地图 |
| 百度 / 抖音小程序 | ✅ | 对应平台自有地图 |
| App(Android/iOS) | ✅ | 高德 / 腾讯二选一,需在 manifest 填地图 key |
| H5 | ✅ | 高德 / 腾讯 Web 地图,需配置 key |
三、微信小程序使用 map 完整教程(重点)
1. 最简基础代码(直接复制运行)
<template> <!-- map必须固定宽高,不推荐百分比 --> <map style="width: 750rpx; height: 600rpx;" :longitude="longitude" :latitude="latitude" :scale="14" show-location <!-- 显示当前定位蓝点 --> :markers="markers" <!-- 自定义标记点 --> @tap="onMapTap" id="myMap" ></map> </template> <script> export default { data() { return { longitude: 116.403874, latitude: 39.914885, markers: [ { id: 1, longitude: 116.403874, latitude: 39.914885, title: "标记点", iconPath: "/static/marker.png" } ] } }, onReady() { // 获取地图操作上下文 this.mapCtx = uni.createMapContext("myMap", this) // 获取当前定位 this.getMyLocation() }, methods: { getMyLocation() { uni.getLocation({ type: "gcj02", // 地图标准坐标系,必须gcj02,否则偏移 success: res => { this.longitude = res.longitude this.latitude = res.latitude } }) }, onMapTap(e) { console.log("点击地图坐标", e.detail.longitude, e.detail.latitude) } } } </script>
2. 小程序必须做的 2 个配置(否则定位失效、无法上架)
① manifest.json 声明定位权限(源码视图 mp-weixin)
微信基础库 2.28.0 + 强制要求 requiredPrivateInfos,缺一不可:
json
"mp-weixin": {
"permission": {
"scope.userLocation": {
"desc": "用于展示当前位置、周边门店地图"
}
},
"requiredPrivateInfos": ["location"]
}
② 小程序后台开通地理位置接口权限
登录微信小程序管理后台 → 开发管理 → 接口设置;
找到地理位置分类,开通「获取当前地理位置」「打开地图选择位置」权限,填写业务用途审核通过。
3. 配套逆地址解析 / 地点搜索(获取地址文字)
map 组件只渲染地图;如果需要经纬度转地址、搜索周边,需要腾讯地图 WebService API:
1 腾讯位置服务控制台创建小程序类型 key;
2 小程序后台服务器域名添加合法域名:https://apis.map.qq.com;
3 引入 qqmap-wx-jssdk 解析地址。

四、高频踩坑注意事项
1 宽高不能用百分比
map 是原生组件,层级高于普通 view,必须写固定尺寸 750rpx / 100vh;
2 坐标系统一 gcj02
小程序地图、uni.getLocation 默认 gcj02,不要用 wgs84,否则点位偏移;
3 原生组件覆盖问题
map 会盖住普通 view,自定义按钮、弹窗要用 cover-view / cover-image 嵌套在 map 内部;
4 分包无影响
map 页面放在分包、主包都正常使用,无特殊限制;
5 Vue2 / Vue3 语法完全通用
仅 <script setup> 写法变量无需 this,map 属性、事件、createMapContext API 无区别。
五、与 App/H5 端核心区别(小程序优势)
微信小程序 map不需要申请地图 key,组件直接渲染,零配置即可显示地图;
App、H5 必须去高德 / 腾讯后台申请秘钥并配置到 manifest,否则空白;
小程序地图性能更好,原生渲染不卡顿,支持点聚合、路线、实时路况全套能力。
六、常用能力拓展
轨迹绘制:polyline 属性画线;
区域覆盖:circles 圆形范围、polygons 多边形;
导航跳转:uni.openLocation 拉起微信内置导航;
点位聚合:地图上下文 initMarkerCluster 实现海量标记聚合。
需要我给你一份可直接复制的script setup Vue3 版本 map 完整页面吗?
<template>
<view class="content">
<map id="map" class="map" :show-location="true" :latitude="latitude" :longitude="longitude"></map>
</view>
</template>
<script>
const img = '/static/logo.png';
export default {
data() {
return {
latitude: 23.099994,
longitude: 113.324520,
}
},
onReady() {
this._mapContext = uni.createMapContext("map", this);
// 仅调用初始化,才会触发 on.("markerClusterCreate", (e) => {})
this._mapContext.initMarkerCluster({
enableDefaultStyle: false,
zoomOnClick: true,
gridSize: 60,
complete(res) {
console.log('initMarkerCluster', res)
}
});
this._mapContext.on("markerClusterCreate", (e) => {
console.log("markerClusterCreate", e);
});
this.addMarkers();
},
methods: {
addMarkers() {
const positions = [
{
latitude: 23.099994,
longitude: 113.324520,
}, {
latitude: 23.099994,
longitude: 113.322520,
}, {
latitude: 23.099994,
longitude: 113.326520,
}, {
latitude: 23.096994,
longitude: 113.329520,
}
]
const markers = []
positions.forEach((p, i) => {
console.log(i)
markers.push(
Object.assign({},{
id: i + 1,
iconPath: img,
width: 50,
height: 50,
joinCluster: true, // 指定了该参数才会参与聚合
label: {
width: 50,
height: 30,
borderWidth: 1,
borderRadius: 10,
bgColor: '#ffffff',
content: `label ${i + 1}`
}
},p)
)
})
this._mapContext.addMarkers({
markers,
clear: false,
complete(res) {
console.log('addMarkers', res)
}
})
}
}
}
</script>
<style>
.content {
flex: 1;
}
.map {
flex: 1;
}
</style>
小程序网络限制
大家都知道,若想在小程序中发起网络请求访问我们的WEB后端,必须要做类似如下的操作:
1.备案你的域名!
2.给你的后端服务上SSL证书!
3.到微信公众平台设置小程序请求域名白名单!
小程序里调用API请求访问我们的后端数据!
这一流程,不难!但对很多人来说,忒麻烦!
所以,这个项目出现了。
得益于小程序的云开发功能,让我们可以突破这个限制,来达到请求任何可访问的http数据!
(如:ip访问、http 80端口访问、自定义端口访问、未备案域名等场景)
不备案,不需HTTPS,不用白名单,直接在小程序里调用我们的API,请求任意网络数据!
1分钟快速部署
首先,我们前往v-request项目的开源地址:
https://github.com/guren-cloud/v-request
里边会有详细的部署和使用方法,我这里也简单介绍一下,真的很简单!新手1分钟搞定!
部署云函数
项目分为两部分,一个是我们的小程序云函数代码,在cloud目录中。
我们首先在开发者工具开通小程序云开发平台,然后初始化好环境之后,创建一个云函数,命名为
命名为 v-request(不要命名错了,重要!)

然后把index.js和package.json文件的内容替换为项目cloud目录中对应的文件内容,再右键进行上传部署(云端安装依赖)操作:

部署客户端
另一个文件,就是主目录下的v-request.js文件,这个是运行在我们小程序里的SDK客户端文件。
我们把它放入小程序的目录,如utils/目录中,然后在app.js文件中进行require加载即可:

开始体验黑科技
通过上边的简单部署,你已经可以在小程序的任意位置,使用 wx.vrequest 方法来进行任意HTTP网络数据请求啦!
(注意:是 vrequest,比官方的 wx.request 方法名前多了个v,也就是 wx.vrequest 哦!)
以下操作均在开启校验域名、HTTPS等设置以及小程序后台未配置request白名单的情况下进行的测试
GET请求测试
wx.vrequest({
url: 'https://mssnn.cn',
success: ret => {
console.log(ret.data);
}
})
返回数据

POST请求测试
wx.login({
success: ret => {
wx.vrequest({
url: 'https://wx5bbe79dd056cb238.mssnn.cn/v2/client/init',
data: 'code=' + ret.code,
dataType: 'json',
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
success: res => {
console.log('[post.res]', res);
}
})
}
})
返回数据:

是不是感觉用法很熟悉?
对!和官方的 wx.request API保持一致!不需要耗费过多学习成本!
应用场景
这个方法,已经能够让我们突破了微信官方的request白名单限制,但我们应该在哪个场景里使用比较合适呢?
我这里总结了小部分你应该会遇到的场景:
网站域名没进行备案
网站目前还是http 80端口,未开启https和配置ssl证书
网站没有域名,通过ip地址访问的
隐藏访问流量中的隐私数据,提高小程序后端的安全性
如果你有以上的需求,不妨试试这个黑科技,解决这一痛点难题,提高小程序开发效率!
项目地址:https://github.com/guren-cloud/v-request
觉得不错,欢迎点个star!
uniapp实现如何使用微信小程序云开发技术实现数据存储和实时通信
首先,我们需要在项目的app.vue文件中引入云开发的初始化函数并进行初始化。在创建云开发环境后,可以将环境ID填入初始化函数的参数中,如下所示:
import { init } from 'wx-server-sdk'
init({
env: 'your-env-id' // 云开发环境ID
})
接下来,我们需要在需要使用云数据库的页面或组件中使用云开发的api。例如,我们想要从云数据库中读取用户的信息并展示在小程序中,可以在页面的onLoad函数中使用以下代码:
onLoad() {
wx.cloud.init({
env: 'your-env-id' // 云开发环境ID
})
const db = wx.cloud.database()
db.collection('users').get({
success: (res) => {
console.log(res.data)
},
fail: (err) => {
console.log(err)
}
})
}
通过上述代码,我们使用了wx.cloud.database()来获取数据库的引用,然后通过collection函数指定集合名称,并使用get函数获取该集合中的数据。之后,我们可以在success回调函数中处理获取到的数据。
除了数据存储,实时通信也是很多应用中必不可少的功能。微信小程序的云开发提供了实时数据库功能,可以用于实时通信等场景。下面我们将介绍如何在uniapp中使用实时数据库。
首先,我们还需要在项目的app.vue文件中引入云开发的初始化函数并进行初始化。同样地,将环境ID填入初始化函数的参数中。
然后,在需要使用实时数据库的页面或组件中使用以下代码:
onLoad() {
wx.cloud.init({
env: 'your-env-id' // 云开发环境ID
})
const db = wx.cloud.database()
const watcher = db.collection('messages').where({
_roomId: 'roomId' // 指定房间ID
}).watch({
onChange(snapshot) {
console.log('docs changed:', snapshot.docs)
},
onError(err) {
console.error('watch err', err)
}
})
}
上述代码中,我们使用了watch()函数来监听指定集合中数据的变化,并通过onChange回调函数获取变化的数据。在实际应用中,我们可以根据业务需求,监听不同的集合和条件,实现实时通信的功能。
使用 Uniapp + UniCloud 云开发微信小程序
uniCloud和阿里云等传统云,的区别和关系
一、uniCloud是DCloud在阿里云和腾讯云的serverless服务上封装而成的。
它包含laaS层(由阿里云和腾讯云提供硬件和网络)和PaaS层(由DCloud提供开发环境)。
开发者可以自主选择uniCloud的硬件和网络资源的供应商,在阿里云和腾讯云之间切换。
二、开户和付费虽然通过DCloud渠道,但实际上开发者自动在云厂商处建立了账户和充值了余额。
DCloûd只获取云服务厂商的返佣。
三、开发时虽使用DCloud的工具,但应用上线时,手机端是直连阿里云或腾讯云的serverless,
不经由DCloud的服务器。
1. 云函数(普通云函数)
服务端代码(云函数)
javascript
// cloudfunctions/getUserInfo/index.js
'use strict';
const db = uniCloud.database();
exports.main = async (event, context) => {
const { userId } = event;
if (!userId) {
return {
code: 400,
message: '缺少参数 userId'
};
}
try {
const collection = db.collection('users');
const res = await collection.doc(userId).get();
if (res.data.length === 0) {
return { code: 404, message: '用户不存在' };
}
return {
code: 0,
data: res.data[0]
};
} catch (err) {
console.error(err);
return { code: 500, message: err.message };
}
};
客户端调用
javascript
// pages/demo/demo.vue
uniCloud.callFunction({
name: 'getUserInfo',
data: { userId: 'xxx' }
}).then(res => {
console.log(res.result);
if (res.result.code === 0) {
const userInfo = res.result.data;
// 处理用户信息
}
}).catch(err => {
console.error(err);
});
2. 云对象(更面向对象的方式)
服务端代码(云对象)
javascript
// cloudobjects/user/index.obj.js
module.exports = {
_before: function() {
// 前置钩子,可做权限校验
console.log('调用前', this.getClientInfo());
},
async getInfo(userId) {
if (!userId) {
return { code: 400, message: '缺少 userId' };
}
const db = uniCloud.database();
const res = await db.collection('users').doc(userId).get();
if (res.data.length === 0) {
return { code: 404, message: '用户不存在' };
}
return {
code: 0,
data: res.data[0]
};
},
async updateName(userId, newName) {
const db = uniCloud.database();
await db.collection('users').doc(userId).update({
name: newName
});
return { code: 0, message: '更新成功' };
}
};
客户端调用
javascript
const userObj = uniCloud.importObject('user');
userObj.getInfo('xxx').then(res => {
if (res.code === 0) {
console.log(res.data);
}
}).catch(err => {
console.error(err);
});
// 调用多个参数
userObj.updateName('xxx', '新名字').then(res => {
console.log(res.message);
});
3. 云数据库(clientDB / 云函数中操作)
3.1 客户端直接操作数据库(clientDB)
需要先在 uniCloud/database/db_init.json 中定义表结构和权限(permission)。
javascript
// 客户端直接查询
const db = uniCloud.database();
db.collection('goods')
.where({
price: db.command.gt(100)
})
.get()
.then(res => {
console.log(res.data);
})
.catch(err => {
console.error(err);
});
// 添加数据(需表权限允许)
db.collection('comments').add({
content: '非常好',
articleId: '123'
});
3.2 云函数中使用数据库
javascript
// cloudfunctions/addArticle/index.js
const db = uniCloud.database();
exports.main = async (event) => {
const { title, content } = event;
const res = await db.collection('articles').add({
title,
content,
createTime: Date.now()
});
return { code: 0, id: res.id };
};
3.3 使用 DB Schema 和 JQL
在云函数或云对象中使用 uniCloud.databaseForJQL() 获得 JQL 实例。
javascript
const dbJQL = uniCloud.databaseForJQL();
const res = await dbJQL.collection('users')
.where('name == $name')
.field('name,age')
.get({
name: '张三'
});
4. 云存储(上传、下载、删除文件)
客户端上传文件
javascript
// 选择图片后上传
uni.chooseImage({
count: 1,
success(chooseRes) {
const tempFilePaths = chooseRes.tempFilePaths;
uniCloud.uploadFile({
filePath: tempFilePaths[0],
cloudPath: 'avatar/' + Date.now() + '.png',
success(res) {
console.log('文件ID:', res.fileID);
// 可直接用于 image 组件 src
this.avatar = res.fileID;
},
fail(err) {
console.error(err);
}
});
}
});
云函数中操作云存储(例如删除文件)
javascript
// cloudfunctions/deleteFile/index.js
exports.main = async (event) => {
const { fileID } = event;
try {
await uniCloud.deleteFile({
fileList: [fileID]
});
return { code: 0, message: '删除成功' };
} catch (err) {
return { code: 500, message: err.message };
}
};
获取临时链接(用于非uni-app环境展示图片)
javascript
uniCloud.getTempFileURL({
fileList: ['cloud://xxx.png']
}).then(res => {
const tempUrl = res.fileList[0].tempFileURL;
// 可以使用该临时链接
});
5. WebSocket 在 uniCloud 中的实现方式
注意:uniCloud 云函数本身不支持 WebSocket 长连接。但你可以:
在客户端直接使用 uni-app 提供的 uni.connectSocket。
配合 uni-push2 实现服务端主动推送消息到客户端(类似 WebSocket 的推送能力)。
5.1 客户端 WebSocket 连接(与第三方 WebSocket 服务通信)
javascript
// 客户端建立 WebSocket 连接
const socketTask = uni.connectSocket({
url: 'wss://your-websocket-server.com/path',
success() {
console.log('连接成功');
}
});
// 监听打开事件
socketTask.onOpen(() => {
console.log('WebSocket 已打开');
socketTask.send({
data: JSON.stringify({ type: 'join', roomId: '123' })
});
});
// 接收消息
socketTask.onMessage(res => {
console.log('收到消息:', res.data);
});
// 发送消息
function sendMsg(msg) {
socketTask.send({ data: JSON.stringify(msg) });
}
5.2 使用 uni-push2 实现服务端主动推送(替代 WebSocket)
服务端云函数/云对象中调用推送
javascript
// cloudfunctions/sendPush/index.js
exports.main = async (event) => {
const { userId, title, content } = event;
const res = await uniCloud.sendPushMessage({
user_id: [userId], // 接收者的用户ID(需登录)
title: title,
content: content,
payload: {
type: 'notification',
data: 'some data'
}
});
return res;
};
客户端接收推送
在 App.vue 的 onLaunch 中监听推送消息:
javascript
// 监听推送消息
uni.onPushMessage((res) => {
console.log('收到推送:', res.data);
if (res.type === 'receive') {
// 应用在前台时收到
uni.showToast({ title: res.data.title });
} else if (res.type === 'click') {
// 用户点击通知栏打开应用
uni.navigateTo({ url: '/pages/detail/detail' });
}
});
使用 uni-push2 需要先在 uniCloud 控制台开通并配置厂商推送通道(个推、华为、小米等)。
新版云函数 WebSocket,的服务端与客户端调用例子
服务端代码 (云对象)
这个云对象负责处理所有连接,并管理消息的广播。
新建云对象:在 HBuilderX 中,右键点击 uniCloud/cloudfunctions 目录,选择“新建云对象”,并将其命名为 ws-manager。
编写代码:以下是 ws-manager.obj.js 的完整代码,通过 ws.onConnect 等钩子来处理连接的生命周期。
javascript
// uniCloud/cloudfunctions/ws-manager/index.obj.js
module.exports = {
// WebSocket 连接建立时触发
'ws.onConnect': async function (event) {
const { connectionId, query } = event;
console.log(`[${connectionId}] 客户端连接成功,房间号: ${query.roomId}`);
// 可以将 connectionId 存储到数据库,用于后续定向推送
// 主动欢迎消息
await uniCloud.ws.send({
connectionId,
data: `欢迎加入房间 ${query.roomId}!您的连接ID是 ${connectionId}`
});
return { success: true };
},
// 接收到客户端消息时触发
'ws.onMessage': async function (event) {
const { connectionId, data: message } = event;
const parsedMessage = JSON.parse(message);
console.log(`[${connectionId}] 收到消息:`, parsedMessage);
// 处理业务逻辑,例如将收到的消息广播给房间内所有客户端
// 此处示例:向所有已连接客户端广播消息
const allConnections = await uniCloud.ws.getConnections();
for (let conn of allConnections) {
if (conn.id !== connectionId) {
await uniCloud.ws.send({
connectionId: conn.id,
data: JSON.stringify({
type: 'broadcast',
from: connectionId,
content: parsedMessage.content
})
});
}
}
},
// WebSocket 连接关闭时触发
'ws.onClose': async function (event) {
const { connectionId } = event;
console.log(`[${connectionId}] 连接已关闭`);
// 可以在此执行清理工作,如从数据库中移除 connectionId
},
// (可选) 连接出错时触发
'ws.onError': async function (event) {
console.error(`[${event.connectionId}] 连接出错:`, event.error);
}
};
注意:云对象提供了 ws.onConnect、ws.onMessage、ws.onClose、ws.onError 这4个固定的钩子函数来分别处理WebSocket连接的每个生命周期事件。
💻 客户端代码 (uni-app 端)
客户端主要负责建立连接、收发消息,并处理网络波动等问题。
获取连接地址:WebSocket 云对象需要通过一个特殊的 wss 协议地址来连接。你需要先在 uniCloud Web 控制台,为 ws-manager 云对象开启 URL 化,并获取其 WebSocket 地址。获取到的地址格式通常类似 wss://xxxxxxxxxxxxx。
编写连接代码:在需要通信的页面,编写以下 JavaScript 代码来使用 WebSocket。
javascript
export default {
data() {
return {
ws: null,
messageList: []
};
},
onLoad() {
// 从第一步获取的完整 WebSocket 地址,例如 wss://xxxxx/websocket/ws-manager
const wsUrl = 'wss://xxxxx/websocket/ws-manager';
this.connectWebSocket(wsUrl);
},
methods: {
// 1. 建立连接
connectWebSocket(url) {
this.ws = uni.connectSocket({
url: url,
success() {
console.log('WebSocket 连接创建成功');
}
});
this.ws.onOpen(() => {
console.log('WebSocket 连接已打开');
uni.showToast({ title: '已连接', icon: 'success' });
});
// 2. 处理接收到的消息
this.ws.onMessage((res) => {
const message = JSON.parse(res.data);
console.log('收到消息:', message);
this.messageList.push(message);
});
// 3. 监听连接关闭事件
this.ws.onClose(() => {
console.log('WebSocket 连接已关闭');
// 可以在此实现重连逻辑
});
this.ws.onError((err) => {
console.error('WebSocket 连接发生错误', err);
});
},
// 发送消息
sendMessage(content) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = {
type: 'chat',
content: content,
timestamp: Date.now()
};
this.ws.send({ data: JSON.stringify(message) });
} else {
uni.showToast({ title: '连接尚未建立', icon: 'none' });
}
},
// 主动关闭连接
closeWebSocket() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
},
beforeDestroy() {
// 页面销毁时记得关闭 WebSocket 连接
this.closeWebSocket();
}
};
🔐 鉴权与安全保障
为了保护你的 WebSocket 服务,可以在连接 URL 上传递 token 进行身份校验。你需要在服务端的 ws.onConnect 和客户端的 uni.connectSocket 中分别进行配置。
服务端 (ws-manager.obj.js):在 ws.onConnect 中,我们可以从 query 参数里拿到客户端传来的 token。
javascript
// 在 ws.onConnect 函数内部添加鉴权逻辑
'ws.onConnect': async function (event) {
const { connectionId, query } = event;
const clientToken = query.token;
// 假设你的 secretToken 是 "my-secret-key-2026"
if (clientToken !== 'my-secret-key-2026') {
console.log(`[${connectionId}] token 验证失败,拒绝连接。`);
return { success: false }; // 返回 false 以拒绝连接
}
// ... 验证通过后的逻辑
}
客户端:在连接时,将 token 作为查询参数附加到 WebSocket URL 后面。
javascript
const token = 'my-secret-key-2026';
const wsUrl = `wss://xxxxx/websocket/ws-manager?token=${token}&roomId=123456`;
this.connectWebSocket(wsUrl);
实用代码
支付老微信代码
// 云函数部分的代码
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
const res = await cloud.cloudPay.unifiedOrder({
"body" : "商品描述",
"outTradeNo" : "商户订单号",
"spbillCreateIp" : "127.0.0.1",
"totalFee" : 1,
// 用于接收支付异步通知的云函数所在的服务空间ID和云函数名
"envId": "test-f0b102",
"functionName": "pay_cb"
})
return res
}
// 小程序部分的代码
wx.cloud.callFunction({
name: '函数名',
data: {
// ...
},
success: res => {
const payment = res.result.payment
wx.requestPayment({
...payment,
success (res) {
console.log('pay success', res)
},
fail (res) {
console.error('pay fail', err)
}
})
},
fail: console.error,
})
...payment
... 是 ES6 的扩展运算符,作用是把 payment 对象里的所有属性和值 “展开” 到当前配置对象中;
payment 是一个提前准备好的支付参数对象,里面必须包含微信支付要求的核心参数,比如:
javascript
运行
// payment 对象的典型结构
const payment = {
timeStamp: '1735689600', // 时间戳(字符串类型)
nonceStr: 'abc123def456', // 随机字符串
package: 'prepay_id=wx20260101123456789', // 预支付会话ID(核心)
signType: 'MD5', // 签名类型
paySign: 'xxxxxx' // 支付签名(后端生成)
}
在 ES6 及以上版本的 JavaScript 中,当一个对象的属性值是函数时,可以使用简写语法:
省略属性名后的 :(冒号);
省略 function 关键字;
直接写 函数名(参数) { 函数体 },此时函数名就等价于对象的 key。
举个更简单的例子帮你理解:
javascript
运行
// 传统写法(ES5)
const obj = {
sayHi: function (name) {
console.log('Hi', name)
}
}
// 简写写法(ES6+)—— 和上面完全等价
const obj = {
sayHi (name) {
console.log('Hi', name)
}
}
迁移到uniCloud + uni-app体系,代码改成这样:
// 云函数部分的代码
const unipayIns = unipay.initWeixin({
appId: 'your appId',
mchId: 'your mchId',
key: 'you parterner key',
// pfx: fs.readFileSync('/path/to/your/pfxfile'), // p12文件路径,使用微信退款时需要,需要注意的是务必使用绝对路径
})
exports.main = async (event, context) => {
const res = await unipayIns.getOrderInfo({
openid: 'user openid',
body: '商品描述',
outTradeNo: '商户订单号',
totalFee: 1, // 金额,单位分
notifyUrl: 'https://xxx.xx' // 支付结果通知地址
})
return res
}
// 客户端部分的代码
uniCloud.callFunction({
name: '云函数名',
data: {
// ...
},
success(res) {
uni.requestPayment({
provider: 'wxpay',
...res.result.orderInfo
success (res) {
console.log('pay success', res)
},
fail (res) {
console.error('pay fail', err)
}
})
}
})
注册个体户(定额报税一年一次),收费小程序前加个体名字
Q:小程序的本地能力有哪些
A:...
Q:小程序可以编辑图像视频吗
A:...
Q:给uniapp完整代码
A:..
小程序存储在本地的数据如何防止被清除呢
核心结论(单机无后端小游戏专用)
1)先天限制(无法彻底绕开)
不管用什么方案:
用户卸载小程序 / 微信 → 数据必然清空,没有任何办法阻止;
用户在手机系统里手动清除小程序数据,同样拦不住;
微信在存储空间不足时,会自动清理长期未打开小程序的缓存。
2)优化目标(只能做到:大幅降低被自动清理概率)
把 Storage(wx.setStorage) 升级为本地文件持久化存储,再加上双备份 + 防丢失机制,在不联网、无后端的前提下做到本地数据尽可能耐用。
方案一:首选方案 —— 文件系统存储(比 Storage 更难被自动清理)
原理
wx.setStorageSync 属于小程序缓存区,微信会优先清理这块缓存;
而 wx.env.USER_DATA_PATH 用户私有目录属于持久化文件区,微信自动回收策略宽松很多,大量单机小游戏都用此方案保存闯关进度、金币、存档。
完整代码(纯本地,无网络)
js
运行
// 获取文件管理器
const fs = wx.getFileSystemManager();
const filePath = `${wx.env.USER_DATA_PATH}/save_data.json`;
// 【写入存档】代替 setStorage
function saveGameData(gameData) {
try {
// 转为字符串写入本地文件
fs.writeFileSync(filePath, JSON.stringify(gameData), "utf8");
return true;
} catch (err) {
console.error("保存失败", err);
return false;
}
}
// 【读取存档】优先读文件,文件丢失再降级读取Storage备份
function loadGameData() {
try {
const text = fs.readFileSync(filePath, "utf8");
return JSON.parse(text);
} catch (e) {
// 文件丢失,读取Storage备用备份
return wx.getStorageSync("game_backup") || null;
}
}
关键优势
USER_DATA_PATH 文件目录,不会被微信常规缓存清理机制干掉;
容量更大(上限 10MB,和 Storage 一致,但回收优先级更低);
只读写文本文件,不需要上传、不需要后端,完全符合你 “仅广告联网” 的要求。
方案二:双备份机制(把丢失概率压到最低,必加)
同时写入两处本地位置,一处丢了另一处兜底:
主存档:写入 USER_DATA_PATH 文件(高持久化)
备用存档:同步写入 wx.setStorageSync("game_backup")
每次游戏进度发生变化(通关、获得金币),同时写入文件 + Storage 两份数据。
启动顺序:优先读取文件,文件不存在再读取 Storage 备份。
方案三:额外加固手段(单机小游戏实测有效)
1. 不要拆分大量 key
不要分散存几十条 storage 键值;把所有游戏进度合并成单个 JSON 对象,只保存 1 份文件 + 1 条缓存,减少碎片化被清理的概率。
2. 启动自动自检 + 修复
每次打开小程序自动校验存档完整性:
JSON 格式是否损坏;
数据字段是否齐全;
一旦文件损坏,自动从备用缓存恢复。
3. 降低被微信自动回收的概率
不要长期冷启动;
不要把文件塞满 10MB 上限,剩余 2MB 空闲空间,避免触发系统自动清理;
只保存核心进度,不写入冗余日志。
4. 数据加简单校验(防文件损坏)
写入时附带 MD5 校验串,读取时校验哈希,避免文件半截损坏导致存档清零。
三、不能做的事情(红线,你是无后端单机项目)
❌ 不能把数据写到手机系统文件夹,小程序沙箱隔离,没有权限;
❌ 不能做跨设备云同步,你禁止服务器与网络请求;
❌ 无法阻止用户手动清空小程序数据 / 卸载,这是系统权限,开发者无权拦截;
❌ 不能使用剪贴板长期保存存档,系统会清空剪贴板,并且审核容易违规。
四、最终落地架构(适合成语闯关、五子棋、2048 这类单机小游戏)
进度变更 → 同时写入:
文件:USER_DATA_PATH/save_data.json(主存档)
Storage:game_backup(备用)
程序启动:
先读文件 → 读取失败自动读取 Storage 备份
增加 JSON 异常捕获,防止文件乱码直接清空进度
所有操作纯前端,无 request、无上传、无后端,仅激励广告联网
这套方案上线后,绝大多数用户只有卸载微信才会丢存档,正常清理微信缓存很难把文件目录清空,是目前个人单机小程序能做到的最高本地持久化水平。
// #ifdef MP-WEIXIN
const fs = wx.getFileSystemManager();
const filePath = `${wx.env.USER_DATA_PATH}/save_data.json`;
// 保存数据
export function saveData(data) {
try {
fs.writeFileSync(filePath, JSON.stringify(data), 'utf8');
// 同步写入storage做双备份
uni.setStorageSync('game_backup', data);
return true;
} catch (e) {
return false;
}
}
// 读取数据
export function loadData() {
try {
const txt = fs.readFileSync(filePath, 'utf8');
return JSON.parse(txt);
} catch (err) {
// 文件丢失,读取缓存备份
return uni.getStorageSync('game_backup') || null;
}
}
// #endif
一、一句话结论
可以用,但没有原生 API,只能靠 WASM 版本的 SQLite(sql.js/wa-sqlite),纯前端运行,完全满足你:无后端、无网络、数据保存在本地文件 USER_DATA_PATH,比单纯 JSON 文件更适合大量结构化存档。
原生小程序没有内置 SQLite 文件数据库,WebSQL API 在 iOS 已经失效,不能用。
二、两条可行方案(uni-app 微信小程序专用)
方案 1:WASM SQLite(首选,单机游戏 / 离线记账必备)
原理
把 SQLite 编译成 Wasm,在 JS 内存里运行数据库;操作完成后,把整个 db 二进制文件写入 USER_DATA_PATH/game.db,用 fs.writeFileSync 持久化;下次启动再读取 db 文件载入内存。
✅ 完全离线,零网络请求;
✅ 数据库文件保存在小程序私有目录,清理优先级远低于 Storage;
✅ 支持 SQL 语句、建表、索引、事务,适合大量闯关记录、多条账单数据;
✅ 整个流程不需要插件、不需要原生代码,纯 JS+Wasm。
关键流程(和你现有的文件存储打通)
1启动:读取 ${wx.env.USER_DATA_PATH}/game.db 二进制文件
2把二进制数据载入 wa-sqlite,打开数据库
3增删改查执行 SQL
4每次数据变动:导出完整 db 二进制,写入本地文件覆盖保存
5额外再加一层 Storage 备份,双重防丢失
uni-app 条件编译写法
js
运行
// #ifdef MP-WEIXIN
const fs = wx.getFileSystemManager();
const dbPath = `${wx.env.USER_DATA_PATH}/game.db`;
// #endif
方案 2:uni-app 现成插件(开箱即用)
DCloud 插件市场:lleh-mpsqlite,封装好 wa-sqlite,自动导出 db 文件到本地目录,不用自己编译 Wasm,直接 CRUD。
限制:只支持微信小程序,H5/App 不兼容,刚好匹配你的项目。
三、优缺点对比(针对你的单机小游戏存档场景)
表格
存储方案持久度查询能力体积清理风险适合场景
wx.setStorage低(优先被微信清理)键值查询,无 SQL≤10MB很高少量配置
JSON 文件(你当前方案)中只能 JSON 遍历查询≤10MB较低简单关卡存档
WASM SQLite 文件高支持 SQL、索引、分页上限 200MB 沙箱空间最低大量闯关记录、多账号存档、多条流水
对你项目的价值
当存档数据几十条、上百条时,JSON 遍历会越来越卡;换成 SQLite 可以直接 SELECT * FROM level WHERE passed=1,查询效率更高,并且整个数据库打包成单一二进制文件,备份更简单。
四、硬性限制(必须遵守,否则审核报错)
1Wasm 文件必须打包进小程序包内,不能远程下载 wasm 二进制(你要求禁止网络请求);
2数据库 db 文件只能读写 wx.env.USER_DATA_PATH,不能访问系统目录;
3全程没有任何 wx.request,仅保留激励广告联网,符合你的项目约束;
4沙箱总文件上限:小程序私有目录一共 200MB,完全够用。
五、不推荐的坑
❌ 不要用废弃的 WebSQL:iOS 微信已经彻底砍掉,只能在部分安卓真机勉强运行,兼容性极差;
❌ 不要用云开发数据库:一联网就违背你 “无后端无网络” 的要求;
❌ 不要引入原生插件:需要审核插件权限,个人单机项目没必要增加复杂度。
六、最终落地建议
数据量小(只存关卡、金币):继续沿用 JSON 文件 + Storage 双备份,最简单稳定;
数据量大(多存档、多条游戏日志、多角色进度):升级为 wa-sqlite + 二进制 db 文件写入 USER_DATA_PATH,存档留存率最高。
如果你需要,我可以给你:
1)精简版 wa-sqlite 小程序打包版本(内置 wasm,无需外网下载);
2)自动把 db 写入本地文件 + 启动自动加载的封装代码;
3)SQLite+Storage 双备份防丢失逻辑。
uni-app 微信小程序 WASM SQLite(wa-sqlite)完整示例,不用现成插件
前提条件
1仅编译到微信小程序(MP-WEIXIN),纯离线,无网络请求
2不使用任何插件,纯 JS + WASM
3数据库持久化到 wx.env.USER_DATA_PATH/game.db
4数据变更自动写入本地文件,搭配 Storage 兜底备份
一、准备资源
下载精简版 wa-sqlite.wasm + wa-sqlite.js,点击了解和下载
把两个文件放到项目目录:
plaintext
/static/sqlite/
wa-sqlite.js
wa-sqlite.wasm
关键:把 wasm 放进静态资源,打包进小程序包,禁止远程加载。
二、封装数据库工具:db-sqlite.js
js
运行
// #ifdef MP-WEIXIN
import waSqlite from "@/static/sqlite/wa-sqlite.js";
const fs = wx.getFileSystemManager();
const DB_FILE = `${wx.env.USER_DATA_PATH}/game.db`;
let dbInstance = null;
// 读取本地db二进制文件
function readDBFile() {
try {
return fs.readFileSync(DB_FILE);
} catch (e) {
// 文件不存在,返回空,新建库
return null;
}
}
// 把内存数据库写入本地文件持久化
function writeDBFile(bytes) {
fs.writeFileSync(DB_FILE, bytes);
// 同时备份到storage
uni.setStorageSync("db_backup_bin", bytes);
}
// 初始化数据库
export async function initDB() {
if (dbInstance) return dbInstance;
const module = await waSqlite();
const SQLite = module.SQLite;
const sqlite3 = await SQLite();
// 读取已有数据库文件
const binData = readDBFile();
let db;
if (binData) {
// 从二进制打开已有库
const u8 = new Uint8Array(binData);
db = new sqlite3.DB(u8);
} else {
// 新建空白数据库
db = new sqlite3.DB();
// 建表示例:游戏存档表
db.exec(`
CREATE TABLE IF NOT EXISTS save_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level INTEGER,
gold INTEGER,
create_time TEXT
)
`);
}
dbInstance = db;
return dbInstance;
}
// 执行SQL语句
export function execSQL(sql, params = []) {
if (!dbInstance) throw new Error("数据库未初始化");
return dbInstance.exec(sql, params);
}
// 保存当前库到本地文件(每次修改后调用)
export function flushDB() {
if (!dbInstance) return;
const bytes = dbInstance.bytes();
writeDBFile(bytes);
}
// 关闭数据库
export function closeDB() {
if (dbInstance) {
dbInstance.close();
dbInstance = null;
}
}
// 兜底恢复:文件损坏时从storage备份恢复
export function restoreFromBackup() {
const bin = uni.getStorageSync("db_backup_bin");
if (bin) {
fs.writeFileSync(DB_FILE, bin);
}
}
// #endif
三、页面调用示例
vue
<template>
<view>
<button @click="addSave">写入存档</button>
<button @click="querySave">读取存档</button>
</view>
</template>
<script>
// #ifdef MP-WEIXIN
import { initDB, execSQL, flushDB } from "@/utils/db-sqlite.js";
// #endif
export default {
async onLoad() {
await initDB();
},
methods: {
addSave() {
execSQL(
"INSERT INTO save_data (level, gold, create_time) VALUES (?, ?, ?)",
[10, 999, new Date().toLocaleString()]
);
// 写入本地db文件
flushDB();
uni.showToast({ title: "保存成功" });
},
querySave() {
const res = execSQL("SELECT * FROM save_data");
console.log("存档列表", res);
}
}
};
</script>
四、关键配置(必须设置,否则 WASM 无法运行)
manifest.json -> mp-weixin
json
"mp-weixin": {
"setting": {
"wasm": true
}
}
vue.config.js(防止 wasm 被编译压缩出错)
js
运行
module.exports = {
chainWebpack(config) {
config.module
.rule("wasm")
.test(/\.wasm$/)
.type("javascript/auto")
.use("arraybuffer-loader")
.loader("arraybuffer-loader");
}
};
五、核心运行逻辑(完全贴合你的离线需求)
1小程序启动:读取 game.db 二进制文件,载入 WASM 内存数据库;
2增删改查全部执行 SQL,内存操作;
3每次写完数据调用 flushDB(),把整个库导出为二进制,写入 USER_DATA_PATH;
4同时将二进制备份到 uni.storage,实现双备份防丢失;
5全程无 request、无上传,仅激励广告联网。
六、优缺点 & 选型建议
1少量存档(关卡、金币):继续用 JSON 文件更轻量,WASM 会增加包体积;
2多角色、多条流水、需要分页查询:SQLite 优势极大,支持索引,数据不会越存越卡;
3小程序沙箱限制:整个用户目录上限 200MB,db 文件完全够用。
七、避坑要点
1wasm 不能放在网络 CDN,必须打入本地静态资源;
2不要在 H5/App 执行这段代码,依靠 #ifdef MP-WEIXIN 隔离;
3不要频繁执行 flushDB,只在进度变更时持久化,减少 IO;
4一旦数据库文件损坏,自动从 storage 二进制备份恢复。
一、官方源码仓库(原版精简编译包来源)
GitHub 主仓库
plaintext
https://github.com/rhashimoto/wa-sqlite
打开后进入 dist 目录,包含预编译好的 JS + WASM 文件。
二、直接拿到小程序可用精简文件(同步版,无 Asyncify,体积最小)
1. NPM 拉取(最简单,推荐)
bash
运行
npm install wa-sqlite
进入目录:
node_modules/wa-sqlite/dist/
选取这 2 个文件(同步版,适配微信小程序,体积最小):
wa-sqlite-sync.mjs → 改名为 wa-sqlite.js
wa-sqlite-sync.wasm → 保留原名 wa-sqlite.wasm
不要用 async 异步版本,小程序容易卡死。
2. Release 正式包下载地址
plaintext
https://github.com/rhashimoto/wa-sqlite/releases
下载源码压缩包,自行提取 dist 内同步产物。
3. 国内镜像(GitHub 访问慢时用)
GitCode 镜像:
plaintext
https://gitcode.com/gh_mirrors/wa/wa-sqlite
三、针对微信小程序的精简优化(必做,控制包大小)
只保留 sync 同步构建产物,删掉所有 async、JSPI 文件;
关闭 SQLite 多余扩展:去掉 RTree、全文搜索,只保留基础 CRUD;
wasm 文件大小最终压缩到 <250KB,不会超出小程序分包限制。
手动最小化编译参数(如需自行打包)
make
CFLAGS += -DSQLITE_OMIT_FTS5 -DSQLITE_OMIT_RTREE -DSQLITE_OMIT_JSON
四、uni-app 放置规范
把提取出来的两个文件放到静态目录,防止被 webpack 转码:
plaintext
static/sqlite/
wa-sqlite.js
wa-sqlite.wasm
五、补充备选(更轻量,纯离线单机首选)
如果 wa-sqlite 体积依然偏大,可以用官方 sqlite-wasm 精简包:
同步单文件构建,体积更小,同样可以写入 USER_DATA_PATH 文件持久化。
插件:lleh-mpsqlite(仅微信小程序,wa-sqlite 封装版,无后端、纯本地持久化)
插件 ID:26256,DCloud uni_modules 一键导入,自动内置 wasm,不用自己编译二进制文件。
一、安装步骤
插件地址:https://ext.dcloud.net.cn/plugin?id=26256
HBuilderX → 插件市场 → 搜索 lleh-mpsqlite → 导入 uni_modules
打开 manifest.json → mp-weixin 配置,开启 WASM 支持
json
"mp-weixin": {
"setting": {
"wasm": true
}
}
仅编译微信小程序,H5/App 端用条件编译屏蔽代码。
二、完整封装代码(游戏存档专用,自动保存到 USER_DATA_PATH)
1. 新建工具文件 /utils/sqlite-db.js
js
运行
// #ifdef MP-WEIXIN
import { sqliteOpen } from "@/uni_modules/lleh-mpsqlite";
// 数据库文件路径(小程序私有目录,不容易被清理)
const DB_PATH = `${wx.env.USER_DATA_PATH}/game_save.db`;
let db = null;
/**
* 打开数据库,文件持久化保存
*/
export async function openDB() {
if (db) return db;
// 打开本地db文件,不存在则自动新建
db = await sqliteOpen(DB_PATH);
// 建表:游戏存档表
db.run(`
CREATE TABLE IF NOT EXISTS game_save (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level INTEGER,
gold INTEGER,
create_time TEXT
)
`);
return db;
}
/**
* 增删改(写入语句)
* @param {string} sql SQL语句
* @param {Array} params 参数数组,防注入
*/
export function runSQL(sql, params = []) {
if (!db) throw new Error("数据库未初始化");
db.run(sql, params);
// 执行完自动把内存库写入本地db文件
db.export(DB_PATH);
// 额外备份到storage,双重防丢失
const bin = db.export();
uni.setStorageSync("db_backup", bin);
}
/**
* 查询数据
* @param {string} sql 查询语句
* @param {Array} params 参数
* @returns 二维数组结果
*/
export function querySQL(sql, params = []) {
if (!db) throw new Error("数据库未初始化");
const res = db.exec(sql, params);
// 取出表格数据
return res.length ? res[0].values : [];
}
/**
* 关闭数据库
*/
export function closeDB() {
if (db) {
db.close();
db = null;
}
}
/**
* 数据库损坏时,从Storage备份恢复文件
*/
export function restoreDB() {
const bin = uni.getStorageSync("db_backup");
if (!bin) return;
const fs = wx.getFileSystemManager();
fs.writeFileSync(DB_PATH, bin);
}
// #endif
三、页面调用示例(Vue 页面)
vue
<template>
<view>
<button @click="initDB">初始化数据库</button>
<button @click="addSave">保存游戏进度</button>
<button @click="getSave">读取进度</button>
</view>
</template>
<script>
// #ifdef MP-WEIXIN
import { openDB, runSQL, querySQL } from "@/utils/sqlite-db.js";
// #endif
export default {
async onLoad() {
// 页面打开自动打开库
await this.initDB();
},
methods: {
async initDB() {
await openDB();
uni.showToast({ title: "数据库就绪" });
},
// 插入存档
addSave() {
runSQL(
"INSERT INTO game_save (level, gold, create_time) VALUES (?, ?, ?)",
[15, 1280, new Date().toLocaleString()]
);
uni.showToast({ title: "进度已保存" });
},
// 查询所有存档
getSave() {
const list = querySQL("SELECT * FROM game_save ORDER BY id DESC");
console.log("存档列表:", list);
}
}
};
</script>
四、核心 API 说明(插件自带方法)
1 sqliteOpen(filePath)
打开数据库;传文件路径 = 持久化写入本地文件;传null= 内存临时库,关闭后数据丢失。
2 db.run(sql, [params])
执行新增、修改、删除 SQL,支持占位符?,防止 SQL 注入。
3 db.exec(sql, [params])
执行查询 SELECT,返回数组。
4 db.export(path?)
不传路径:返回二进制 Uint8Array
传入文件路径:直接把整个数据库写入本地文件(关键持久化方法)。
5 db.close() 关闭实例。
五、完全匹配你的离线需求
1 全程无 wx.request、无网络请求,只有激励广告联网;
2 db 文件保存在 USER_DATA_PATH,清理优先级远低于 Storage;
3 每次写入自动执行export()落地文件 + Storage 二进制备份;
4 WASM 内置在插件内部,不需要远程下载资源,不会触发网络审核。
六、避坑要点
1 只在微信小程序运行,必须用 #ifdef MP-WEIXIN 隔离代码;
2 不要频繁调用export(),只在游戏进度变更时持久化,减少文件 IO;
3 不要在 onUnload 之外频繁开关数据库,避免文件锁定;
4 单个 db 文件不要超过 10MB 沙箱限制。
七、对比你之前的方案
JSON 文件:适合少量存档,查询只能遍历 JSON;
原生 wa-sqlite:需要自己处理 wasm 打包、二进制读写;
lleh-mpsqlite:开箱即用,自动封装文件导出,一行代码落地持久化,个人单机小游戏首选。
一、找不到插件的原因与直达入口
插件 ID:26256
直接浏览器打开链接:
https://ext.dcloud.net.cn/plugin?id=26256
搜不到的 3 个解决办法
不要只搜名字 lleh-mpsqlite
HBuilderX 插件市场中文检索经常失效,直接粘贴插件 ID 26256 搜索才能定位到。
切换搜索范围:勾选【JS SDK】分类,不要选组件。
手动下载 ZIP 包:页面点【下载插件 ZIP】,解压后把 lleh-mpsqlite 文件夹放进项目 uni_modules 目录,不用在线导入。
二、完整持久化代码(适配你的离线单机小游戏)
js
运行
// #ifdef MP-WEIXIN
import { sqliteOpen } from "@/uni_modules/lleh-mpsqlite";
const fs = wx.getFileSystemManager();
// 存在小程序私有目录,不容易被微信清理
const DB_FILE = `${wx.env.USER_DATA_PATH}/game_save.db`;
let db = null;
// 1. 打开数据库:优先读取本地db文件,不存在则新建空白库
export async function openDB() {
if (db) return db;
try {
// 读取本地db二进制文件
const buf = fs.readFileSync(DB_FILE);
db = await sqliteOpen(new Uint8Array(buf));
} catch (e) {
// 文件不存在,创建内存空库
db = await sqliteOpen(null);
// 初始化游戏存档表
db.run(`
CREATE TABLE IF NOT EXISTS save_data(
id INTEGER PRIMARY KEY AUTOINCREMENT,
level INTEGER,
gold INTEGER,
save_time TEXT
)
`);
}
return db;
}
// 2. 写入数据(增/改/删),执行后自动落地文件
export function execWrite(sql, params = []) {
db.run(sql, params);
// 把内存数据库导出为二进制,写入本地文件持久化
const bin = db.export();
fs.writeFileSync(DB_FILE, bin.buffer);
// 双备份:存入storage防止db文件损坏
uni.setStorageSync("db_backup_bin", bin);
}
// 3. 查询数据
export function execQuery(sql, params = []) {
const res = db.exec(sql, params);
return res.length ? res[0].values : [];
}
// 4. 关闭数据库
export function closeDB() {
if (db) {
db.close();
db = null;
}
}
// 5. 备份恢复:db文件丢失时从缓存还原
export function restoreDB() {
const bin = uni.getStorageSync("db_backup_bin");
if (!bin) return;
fs.writeFileSync(DB_FILE, bin.buffer);
}
// #endif
三、页面调用示例
vue
<script>
// #ifdef MP-WEIXIN
import { openDB, execWrite, execQuery } from "@/utils/sqlite.js";
// #endif
export default {
async onLoad() {
await openDB();
},
methods: {
// 保存闯关进度
saveProgress() {
execWrite(
"INSERT INTO save_data(level,gold,save_time) VALUES(?,?,?)",
[20, 2500, new Date().toLocaleString()]
);
uni.showToast({ title: "存档已固化到本地文件" });
},
// 读取存档
loadProgress() {
const list = execQuery("SELECT * FROM save_data ORDER BY id DESC");
console.log(list);
}
}
}
</script>
四、关键配置(必开,否则 WASM 无法运行)
manifest.json -> mp-weixin
json
"mp-weixin": {
"setting": {
"wasm": true
}
}
五、备选方案(万一插件还是无法导入)
手动使用原版 wa-sqlite 同步版 JS+Wasm(无任何插件依赖);
只把二进制导出 + 文件写入封装一遍,和插件效果完全一致;
如果你需要,我可以把插件内的核心 wa-sqlite 源码剥离出来,做成无插件纯 JS 版本。
微信原生小程序 wa-sqlite 完整示例
特点:
无插件、无 uni-app,纯原生微信小程序代码
内存运行 SQLite,数据库持久化为二进制文件保存到 USER_DATA_PATH
只使用官方文件 API,全程离线,仅广告联网
自动双备份:db 文件 + Storage 兜底恢复
一、资源准备
下载 wa-sqlite 同步精简包:
wa-sqlite-sync.js + wa-sqlite-sync.wasm
放到项目目录:
plaintext
/sqlite
wa-sqlite-sync.js
wa-sqlite-sync.wasm
二、新建工具类 db.js
javascript
运行
// sqlite/db.js
const fs = wx.getFileSystemManager();
const DB_PATH = `${wx.env.USER_DATA_PATH}/game.db`;
let db = null;
let SQLiteFactory = null;
// 初始化加载wasm
async function initFactory() {
if (SQLiteFactory) return SQLiteFactory;
const module = await require("./wa-sqlite-sync.js")();
SQLiteFactory = module.SQLite;
return SQLiteFactory;
}
// 打开数据库
async function openDB() {
if (db) return db;
const SQLite = await initFactory();
const sqlite3 = await SQLite();
try {
// 读取本地db二进制文件
const buffer = fs.readFileSync(DB_PATH);
const u8 = new Uint8Array(buffer);
db = new sqlite3.DB(u8);
} catch (e) {
// 文件不存在,新建库并建表
db = new sqlite3.DB();
db.exec(`
CREATE TABLE IF NOT EXISTS save_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level INTEGER,
coin INTEGER,
save_time TEXT
);
`);
}
return db;
}
// 执行增删改,写完自动持久化到本地文件
function runWrite(sql, params = []) {
db.exec(sql, params);
// 导出整个数据库二进制
const bin = db.bytes();
// 写入db文件
fs.writeFileSync(DB_PATH, bin);
// 备份到storage,防止文件损坏丢失
wx.setStorageSync("db_backup", bin);
}
// 查询
function runQuery(sql, params = []) {
const result = db.exec(sql, params);
return result.length ? result[0].values : [];
}
// 关闭库
function closeDB() {
if (db) {
db.close();
db = null;
}
}
// 从缓存备份恢复数据库文件
function restoreBackup() {
const bin = wx.getStorageSync("db_backup");
if (!bin) return;
fs.writeFileSync(DB_PATH, bin);
}
module.exports = {
openDB,
runWrite,
runQuery,
closeDB,
restoreBackup
};
三、页面 js 使用示例(index.js)
javascript
运行
const db = require("../../sqlite/db.js");
Page({
async onLoad() {
await db.openDB();
},
// 保存游戏存档
saveData() {
db.runWrite(
"INSERT INTO save_info (level, coin, save_time) VALUES (?, ?, ?)",
[12, 1500, new Date().toLocaleString()]
);
wx.showToast({ title: "存档已保存" });
},
// 读取所有存档
queryData() {
const list = db.runQuery("SELECT * FROM save_info ORDER BY id DESC");
console.log("存档列表:", list);
},
onUnload() {
db.closeDB();
}
});
四、project.config.json 关键配置(必须开启 WASM)
json
{
"setting": {
"wasm": true,
"es6": true
}
}
五、运行机制(完全符合你的离线项目)
1启动:读取沙箱里的 game.db,加载进 WASM 内存数据库;
2增删改查全部执行 SQL;
3每次写入数据 → 导出完整二进制 → 写入本地文件;
4同时把二进制存入 Storage,文件损坏可以一键恢复;
5全程没有 wx.request,没有网络请求。
六、重要提醒
1.微信小程序没有原生 SQLite API,这套是 WASM 在 JS 虚拟机内运行数据库,属于前端方案;
2.db 文件存放在 wx.env.USER_DATA_PATH,清理优先级远低于普通 Storage;
3.数据量小的时候,依然优先用 JSON 文件方案,WASM 会增大包体积;
4.wasm 文件必须放在本地项目里,不能在线下载,否则审核不通过。
方案 1:蓝牙热敏打印机(外卖 / 收银小票最常用)
微信小程序官方蓝牙 API,连接便携蓝牙打印机
核心 API:wx.openBluetoothAdapter、wx.createBLEConnection、wx.writeBLECharacteristicValue
1 流程
初始化蓝牙适配器
搜索附近蓝牙设备
选中打印机建立 BLE 连接
获取读写特征值
拼装打印指令(ESC/POS 打印指令)
分段写入蓝牙发送打印
2 关键说明
必须用户主动点击触发蓝牙授权(不能自动搜)
安卓 /iOS 蓝牙权限差异大,需做兼容
ESC/POS 指令控制字体、居中、二维码、切纸
方案 2:WiFi 云打印机(远程打印,不用蓝牙靠近)
打印机连局域网 / 云打印服务,小程序请求后端接口,后端发指令给打印机
流程:
小程序 → 后端接口 → 云打印 SDK / 打印机 IP 接口 → 自动出纸
优势:手机不用靠近打印机,支持多设备远程打印
方案 3:网页 PDF 打印(A4 文档,微信内置浏览器)
小程序不支持直接调用系统打印,曲线方案:
1 前端生成 PDF(pdfkit、wx-pdf 等)
2 PDF 上传服务器,返回在线链接
3 wx.navigateToMiniProgram 或 wx.previewFile 打开 PDF
4 用户右上角「...」- 选择在浏览器打开,浏览器可调用系统打印
一、方案总览(按易用度排序)
1.前端纯本地生成(无需后端,推荐简单单据):pdf-lib、wx-pdf
2.Canvas 绘图转 PDF(适合海报、表格、图文混排):html2canvas + pdf-lib
3.后端生成 PDF(复杂报表、多页、字体不乱码首选):后端 wkhtmltopdf/Puppeteer/Java iText
4.云函数生成 PDF(无自建服务器,云开发用户):云函数部署 pdf-lib
二、方案 1:前端本地 pdf-lib 生成 PDF(无后端)
优势
不用上传图片、不用服务器,手机本地生成,适合小票、订单、简单报表
步骤
1.安装依赖
bash
运行
# 小程序npm构建
npm install pdf-lib
2.页面示例代码
js
运行
import { PDFDocument, rgb } from 'pdf-lib'
Page({
async createPdf() {
// 1. 创建空白PDF
const pdfDoc = await PDFDocument.create()
// 新增一页A4
const page = pdfDoc.addPage([595, 842])
const { width, height } = page.getSize()
// 写入文字
page.drawText('订单PDF单据', {
x: 50,
y: height - 80,
size: 22,
color: rgb(0, 0, 0)
})
page.drawText('订单号:DD20260716001', { x:50, y:height-120, size:14 })
page.drawText('客户:张三', { x:50, y:height-150, size:14 })
// 2. 导出PDF二进制
const pdfBytes = await pdfDoc.save()
// 3. 写入小程序本地临时文件
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/order.pdf`
fs.writeFileSync(filePath, pdfBytes)
// 4. 预览PDF
wx.openDocument({
filePath,
success(res) {
console.log('打开PDF成功')
}
})
}
})
缺点
复杂图文、表格排版代码量大
中文会方框乱码,需要嵌入中文字体
解决中文乱码:嵌入字体
需要下载中文字体 ttf 文件放进项目,用embedFont加载字体再渲染文字。
三、方案 2:HTML 页面转 PDF(可视化排版,最省心)
思路
1.用 wxml 模拟单据 HTML 布局;
2.html2canvas 把页面转图片;
3.pdf-lib 把图片塞进 PDF。
适合:有表格、logo、二维码、多图文的订单收据。
核心片段
js
运行
// 1. canvas截取页面
wx.createSelectorQuery()
.select('.pdf-wrap')
.fields({ node:true, size:true })
.exec(async res=>{
const canvas = res[0].node
const ctx = canvas.getContext('2d')
// 绘制页面内容到canvas
// ...
// canvas转图片buffer
const imgData = canvas.toDataURL('image/png')
// pdf-lib创建PDF,插入图片
})
四、方案 3:后端生成 PDF(复杂报表、大量中文、多页)
适用场景
多页合同、复杂表格、上万条数据、固定模板、字体完美兼容
主流后端工具
1.Node.js:puppeteer(渲染 HTML 转 PDF,最友好)
2.Java:iText、Apache PDFBox
3. Python:WeasyPrint、ReportLab
流程
1.小程序传参数(订单 ID、用户信息)给后端接口
2.后端读取数据,填充 HTML 模板,调用 Puppeteer 生成 PDF 文件
返回 PDF 下载链接 / 二进制流
3.小程序wx.downloadFile下载后wx.openDocument预览
4.Node.js Puppeteer 极简示例(后端)
js
运行
const puppeteer = require('puppeteer')
app.get('/create-pdf', async (req, res)=>{
const browser = await puppeteer.launch()
const page = await browser.newPage()
const html = `<h1>订单PDF</h1><p>订单号123456</p>`
await page.setContent(html)
const pdfBuffer = await page.pdf({ format: 'A4' })
res.setHeader('Content-Type','application/pdf')
res.send(pdfBuffer)
})
五、方案 4:微信云函数生成 PDF(零服务器)
云函数内引入pdf-lib,逻辑和前端一致,好处:
1.字体文件放云函数,前端不用携带字体包
2.不受小程序包体积限制
3.生成后上传云存储,返回预览链接
六、PDF 预览 & 打印配套操作
1. 预览 PDF
js
运行
wx.openDocument({
filePath: '本地临时pdf路径',
fileType: 'pdf'
})
2. PDF 纸质打印(两种方式)
1.蓝牙打印机:PDF 转图片,ESC/POS 指令打印图片小票
2.A4 完整打印:wx.previewFile打开 PDF,右上角「...」→在浏览器打开,浏览器调用系统打印
七、常见坑
1.中文方框乱码
pdf-lib 默认无中文字体,必须手动嵌入 ttf 字体文件。
2.小程序包体积超限
字体文件很大,建议放后端 / 云函数,不要放前端。
3.前端生成大图 PDF 卡顿
图片多、页数多建议改用后端生成。
4.wx.openDocument只能打开本地临时文件
网络 PDF 链接不能直接打开,必须先 downloadFile 下载到本地。
选型建议
1.简单小票、少量文字、无复杂中文 → 前端 pdf-lib
2.图文 / 表格 / 二维码排版好看 → Canvas 截图 + pdf-lib
3.多页合同、大量中文、复杂报表 → 后端 Puppeteer
4.使用微信云开发、不想买服务器 → 云函数 pdf-lib
下一篇:小程序同行者