小游戏启动还在黑屏?官方封面图插件接入教程

前言

点开一个小游戏,最劝退的不是玩法,是开头那几秒黑屏。

原因不复杂:首包里塞着游戏引擎和一大堆启动代码,微信得先把它们下载完、注入完、等 WebGL 初始化,屏幕上才画得出第一帧。这段时间内玩家什么也看不到,很多人直接就退了。

社区里流传的最佳实践是「首包只画一张静态图」,不依赖引擎、直接用 WebGL 画,让玩家第一时间看到画面。思路没错,但自己写这套逻辑挺别扭:想加点进度条、多图切换,就得把渲染引擎也塞进首包,包又大回去了。

微信官方把这件事封装成了 封面图插件(MinigameLoading)。这篇就按官方文档把接入流程走一遍,顺带说说几个照着文档抄也容易翻车的地方。

官方文档:封面图插件(最后更新 2026-04-09)

它到底做了什么

一句话:把「等引擎」的时间,换成「先看封面」。

插件把绘制静态图的 WebGL 逻辑封装在内部,渲染引擎由基础库内置、官方维护并静默更新,所以不占用你自己的代码包体积。基于它还能用更丰富的封面模板,官方点名了静态图模板、多图切换模板、进度条模板三类。

插件本身不占包体,但真正让首包变小的,是你把游戏代码拆成分包。官方把改造成本说得很直白:准备一张封面图,更改初始化流程,把游戏代码都改成子包加载,首帧逻辑改成初始化插件并异步加载游戏代码 —— 有图的话,预计 1~2 天接完。

以前整包启动与使用封面图插件后的流程对比

官方给的应用案例是《星途WeGoing》,表现是「封面图 → 游戏 loading → 正式进入游戏」,安卓总启动耗时 2.3s、提升 34%,iOS 1.6s、提升 33%。这是官方案例数据,不是接入后必然拿到的数字,自己项目还是要真机测。

接入前准备

  • MP 后台添加插件:登录后台,打开「小游戏封面图」插件页面点「添加插件」。按官方《小游戏插件》文档的说法,通常无需审批,添加后即可使用。申请入口在封面图插件文档的「1. 申请插件」里。
  • 一张封面图:放到构建产物目录(如 images/),社区实践一般用 JPG,同尺寸下比 PNG 小不少。
  • 基础库 2.1.0 及以上:分包能力从这个版本开始支持,官方示例也用 compareVersion(SDKVersion, '2.1.0') 做判断。低版本要做兼容。
  • 微信开发者工具 + 真机:封面阶段涉及 GL 上下文,工具里能跑不代表真机没问题。

另外提醒一句:目前小游戏只开放使用官方提供的插件,不能自己发布插件;封面图插件属于官方插件列表里的一员。

五步接入

官方五步接入流程

第一步:申请插件

在 MP 后台打开封面图插件页面点「添加插件」,通常无需审批。这一步不能跳,没添加的话后面 requirePlugin 会直接失败。

第二步:在 game.json 声明插件

1
2
3
4
5
6
7
8
9
{
"plugins": {
"MinigameLoading": {
"version": "latest",
"provider": "wxbd990766293b9dc4",
"contexts": [{ "type": "isolatedContext" }]
}
}
}

几个点:

  • MinigameLoading 是你自己起的引用名,后面 requirePlugin 要用同一个名字;
  • provider 是封面图插件的固定 AppID wxbd990766293b9dc4,不能改;
  • version: "latest" 用最新版,社区实践里也有钉死具体版本(如 1.0.22)的写法;
  • contexts: [{ "type": "isolatedContext" }] 表示独立域插件,照抄别漏,漏了容易在运行时找不到插件。

第三步:先取 contextType 和 contextAttributes

这一步最容易被跳过,但它是后面不出画面问题的关键。官方要求在游戏画面渲染后执行这段代码,把结果记下来:

1
2
3
4
5
6
7
let ctx = GameGlobal.canvas.getContext('2d')
if (ctx) {
console.log('contextType: 2d; ', 'contextAttributes: ', ctx.getContextAttributes())
} else {
ctx = GameGlobal.canvas.getContext('webgl')
console.log('contextType: webgl; ', 'contextAttributes: ', ctx.getContextAttributes())
}

不同引擎、不同项目拿到的 contextAttributes 可能不一样(alpha、antialias、depth、stencil、preserveDrawingBuffer 这些)。用自己项目里打印出来的值,不要从别人博客复制,否则插件创建的 GL 上下文和游戏上下文参数不一致,轻则画面异常,重则冲突黑屏。

第四步:game.js 里渲染封面

官方示例(我按官方结构整理,catch 里的变量名后面会说):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (compareVersion(wx.getSystemInfoSync().SDKVersion, '2.1.0') > -1) {
try {
GameGlobal.LoadingManager = requirePlugin('MinigameLoading', {
customEnv: {
wx,
canvas: GameGlobal.canvas,
}
}).default;

GameGlobal.LoadingManager.create({
images: [{ src: 'images/p1.png' }],
contextType: 'webgl',
contextAttributes: {}, // 填第三步取到的真实值
}).then(() => {
// 封面已显示,这里开始加载游戏代码
}).catch((err) => {
console.error('封面图显示失败', err);
});
} catch (err) {
console.error('当前环境不支持使用插件', err);
}
} else {
// 基础库过低:走原来的整包启动流程
}

逐项说:

  • requirePlugin(name, { customEnv }) 把 wx 和 canvas 注入给插件,customEnv 里的 canvas 在有些引擎工程里是全局 canvas 变量,按你的构建产物调整;
  • .default 是插件导出的 LoadingManager,挂到 GameGlobal 上是为了后面 main.js 也能调用;
  • create({ images, contextType, contextAttributes }) 是渲染封面的入口,images[].src 就是封面图相对路径;
  • create() 返回 Promise,.then() 里封面已经显示,适合在这里启动分包加载;
  • 基础库判断用的 compareVersion 实现见官方版本号比较文档,思路是把版本号按 . 拆开逐段比大小,补零对齐后再比;
  • 社区实践里还会传 scaleMode: GameGlobal.LoadingManager.ScaleMode.FIXED_WIDTH 控制封面缩放,以及多图、进度条等模板参数 —— 这些属于插件独立 API,完整参数要登录 MP 后台看插件文档,公开文档里只给了最小示例。

这里有个官方文档的小笔误:原文 catch(err) 块里写的是 console.error('当前环境不支持使用插件', error),变量 error 并没有定义,照抄会在捕获异常时再抛一个 ReferenceError。改成 err 就行。

第五步:隐藏封面

游戏首帧就绪后销毁封面:

1
2
3
GameGlobal.LoadingManager.destroy().then(() => {
// 封面已移除,切到真正的游戏画面
})

destroy() 同样返回 Promise。时机很讲究,尤其封面和游戏共用一个 canvas 时,必须在引擎初始化之前销毁,否则两边抢 GL 上下文。社区里的 Cocos 改造用 LoadingManager.isMainCanvas 区分两种情况:

  • 共用主屏:在 cc.game.run() 之前 destroy().then(() => cc.game.run(...));
  • 非共用主屏:在首场景 loadScene 的完成回调里 destroy()。

真正的关键:把游戏代码拆进分包

只加封面不拆包,等于白干 —— 首包还是那么大,玩家照样得等。

官方《分包加载》文档里的配置方式:

1
2
3
4
5
6
{
"subpackages": [
{ "name": "stage1", "root": "stage1/" },
{ "name": "stage2", "root": "stage2.js" }
]
}

subpackages 里配置的目录或 JS 文件会被打成独立分包,没配置的都进主包。主包上限 4M,所有主包加分包合计不超过 30M,单个普通分包不限大小、独立分包不超过 4M。

触发加载用 wx.loadSubpackage:

1
2
3
4
5
6
7
8
9
const loadTask = wx.loadSubpackage({
name: 'stage1',
success(res) { /* 分包加载完成 */ },
fail(res) { /* 加载失败 */ }
})

loadTask.onProgressUpdate(res => {
console.log('下载进度', res.progress)
})

整体顺序是:首包启动 → 初始化插件并显示封面 → 封面背后 loadSubpackage 异步拉引擎和游戏代码 → 加载完成、引擎跑起来、首帧就绪 → destroy() 销毁封面。

老版本兼容不用自己写两套:微信后台会编译分包版和整包兼容版两份代码包,旧客户端下载整包启动;基础库 2.1.0 以下没有 wx.loadSubpackage,开发者用 require('stage1/game.js') 触发入口加载即可。

官方还给了一个完整改造示例仓库:cocos-blackjack。

Cocos Creator 项目怎么改

如果你用 Cocos Creator 构建微信小游戏,社区里有一份很完整的实操记录(Asroads’Blog,Cocos Creator 2.4.11),核心是改三个文件、加一张图:

文件 改什么
game.json 在原有 plugins 里加 MinigameLoading 声明(引擎插件如 cocos 保留)
game.js 把原来同步 require 引擎的流程收进 loadGame();先 create() 显示封面,再异步加载游戏
main.js 按 isMainCanvas 决定 destroy() 时机:共用主屏在 cc.game.run() 前,否则在 loadScene 回调里
images/img_bj.jpg 新增封面图,放在构建产物目录里

game.js 里的 loadGame() 依次 requirePlugin('cocos')、__globalAdapter.adaptEngine()、require('./ccRequire')、require('./src/settings')、require('./main'),最后 window.boot()。也就是说引擎相关的加载全被推到封面显示之后,首包只剩封面逻辑和插件初始化。

顺带说下那份实战里的一个细节:引擎插件声明里带 path: "cocos" 只用于本地调试,正式环境要删掉。

九个容易踩的坑

  1. 没在 MP 后台添加插件就调 requirePlugin,直接失败。
  2. game.json 漏了 contexts: [{ "type": "isolatedContext" }],插件加载不起来。
  3. contextAttributes 照抄别人的。一定要接入前在自己项目里读一次,这是最隐蔽的坑。
  4. **官方示例 catch(err) 里写的是 error**,照抄会多抛一个 ReferenceError(前面提过)。
  5. 封面图路径写错或图太大。路径是相对构建产物的,图片本身会进主包,别放几 MB 的原图。
  6. destroy() 时机不对。共用主屏必须在引擎初始化前销毁,不然 GL 上下文冲突;太早销毁又会露黑屏。
  7. 只改封面、没拆分包。首包没变小,优化等于没做。
  8. 低版本基础库没兜底。2.1.0 以下既没有分包也没有插件能力,else 分支要走原来的整包启动流程;极老客户端不支持插件时可能黑屏。
  9. 平台绑定。这是微信小游戏的能力,抖音、QQ 小游戏等平台不通用,做多端发布时要单独处理。

怎么验证有没有生效

  • 微信开发者工具里看首包体积是否明显下降,以及启动流程里是否出现「封面 → loading → 游戏」三段;
  • 真机(尤其低端安卓)冷启动,确认第一眼就是封面而不是黑屏;
  • 检查封面显示期间分包是否真的在后台下载,别把 loadSubpackage 写成了串行等待;
  • 对比接入前后的启动耗时,也可以配合官方的自定义启动上报做漏斗分析。

写在最后

封面图插件不难接,五步、几段代码,官方文档也就一页。它真正要求你动的是项目结构:把「所有东西塞进首包、按次序同步加载」改成「先给玩家一张图,剩下的后台慢慢来」。

如果你的小游戏首包已经逼近 4M,或者冷启动黑屏被吐槽过,这个改造值得花那一两天。毕竟玩家愿不愿意留下来,往往就在点开后的头两秒。

参考链接