您现在的位置是:网站首页> 小程序设计

小程序研发关注问题收集

摘要

小程序研发关注问题收集


1.jpg




小程序分包技术收集

实用界面组件&本地能力

***使用 Uniapp + UniCloud 云开发微信小程序***

使用 Uniapp + UniCloud 云开发微信小程序

小程序请求不备案不要HTTPS方法(云函数使用http请求其他IP数据)

uniapp实现如何使用微信小程序云开发技术实现数据存储和实时通信

实用代码

小程序收款

小程序本地能力

小程序本地存储

小程序用SQLite可行?

uni-app 微信小程序 WASM SQLite(wa-sqlite)完整示例,不用现成插件

uni-app 现成插件(开箱即用)

微信小程序原生操作Sqlite例子代码不用uniapp

小程序打印功能

小程序如何生成 PDF


小程序分包技术收集

分包介绍

uniapp微信小程序分包



分包介绍

一、分包是什么、核心作用

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 内部相对地址
independenttrue = 独立分包;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.jpg


四、高频踩坑注意事项

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>





小程序请求不备案不要HTTPS方法

小程序网络限制

大家都知道,若想在小程序中发起网络请求访问我们的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(不要命名错了,重要!)

1.png

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

1.png


部署客户端

另一个文件,就是主目录下的v-request.js文件,这个是运行在我们小程序里的SDK客户端文件。

我们把它放入小程序的目录,如utils/目录中,然后在app.js文件中进行require加载即可:

1.png


开始体验黑科技

通过上边的简单部署,你已经可以在小程序的任意位置,使用 wx.vrequest 方法来进行任意HTTP网络数据请求啦!

(注意:是 vrequest,比官方的 wx.request 方法名前多了个v,也就是 wx.vrequest 哦!)

以下操作均在开启校验域名、HTTPS等设置以及小程序后台未配置request白名单的情况下进行的测试


GET请求测试

wx.vrequest({

  url: 'https://mssnn.cn',

  success: ret => {

    console.log(ret.data);

  }

})

返回数据

1.png


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);

      }

    })

  }

})


返回数据:

1.png

是不是感觉用法很熟悉?

对!和官方的 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文档

查看视频教程

相关研发文章1

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 用户私有目录属于持久化文件区,微信自动回收策略宽松很多,大量单机小游戏都用此方案保存闯关进度、金币、存档。

完整代码(纯本地,无网络)

点击查看相对应的uniapp代码

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


小程序用SQLite可行?

一、一句话结论

可以用,但没有原生 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 精简包:

项目地址:https://sqlite.org/wasm

同步单文件构建,体积更小,同样可以写入 USER_DATA_PATH 文件持久化。


uni-app 现成插件(开箱即用)

插件: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 版本。


微信小程序原生操作Sqlite例子代码不用uniapp

微信原生小程序 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 用户右上角「...」- 选择在浏览器打开,浏览器可调用系统打印


小程序如何生成 PDF

一、方案总览(按易用度排序)

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



Top