前言
接上篇 基于PixiJS的试玩广告开发中,我们讲到了Moloco平台的试玩广告投放,这次我们的新需求是要兼容更多平台🤣🤣🤣,Facebook、Liftoff、RevX。
每个平台的 API 和要求都有些许区别:
- Facebook :
FbPlayableAd.onCTAClick() - Liftoff :
mraid.open() - RevX : 需要注入
trkLink和ctaLink宏
我们使用环境变量区分不同平台,避免维护多份代码。
实施
1. 配置环境变量
在根目录下为每个平台创建对应的 .env 文件:
.env.facebook:VITE_AD_PLATFORM=facebook.env.liftoff:VITE_AD_PLATFORM=liftoff.env.moloco:VITE_AD_PLATFORM=moloco.env.revx:VITE_AD_PLATFORM=revx
构建时可通过 import.meta.env.VITE_AD_PLATFORM 获取平台标识。
2. 统一跳转逻辑 (CTA)
在 src/utils/ad.ts 中封装一个统一的 handleCTA() 函数,屏蔽平台差异。业务层只需要调用这个函数,无需关心底层实现。
typescript
// src/utils/ad.ts
/** Liftoff 跳转 */
function ctaLiftoff() {
// mraid 仅在真机环境存在
if (typeof mraid !== "undefined") {
mraid.open("${CLICK_URL}"); // 平台会自动替换宏
} else {
alert("【本地测试】Liftoff 跳转");
}
}
/** Facebook / Moloco 跳转 */
function ctaFacebook() {
if (typeof FbPlayableAd !== "undefined" && FbPlayableAd.onCTAClick) {
FbPlayableAd.onCTAClick();
} else {
alert("【本地测试】Facebook/Moloco 跳转");
}
}
/** RevX 跳转 */
function ctaRevX() {
const w = window as any;
const trkLink = w["trkLink"]; // 追踪链接
const ctaLink = w["ctaLink"]; // 落地页链接
// 本地测试或宏未替换时
if (!ctaLink || ctaLink === "|CLICK_URL|") {
alert("【本地测试】RevX 跳转");
return;
}
// 触发点击追踪像素
if (trkLink && trkLink !== "|CLICK|NOENCODING|") {
new Image().src = trkLink + encodeURIComponent(ctaLink);
}
window.open(ctaLink, "_blank");
}
/** 统一入口 */
export function handleCTA() {
const platform = import.meta.env.VITE_AD_PLATFORM;
if (platform === "liftoff") {
ctaLiftoff();
} else if (platform === "facebook") {
ctaFacebook();
} else if (platform === "revx") {
ctaRevX();
} else {
// 默认 fallback
ctaFacebook();
}
}
构建配置
试玩广告通常要求 单文件交付 (Single HTML) ,即 HTML、CSS、JS、图片、音频全部内联到一个 HTML 文件中。
Vite 插件配置
使用 vite-plugin-singlefile 实现内联。针对 RevX 平台,还需要额外注入特定的宏定义脚本。
编写一个简单的插件 plugins/revx-inject.ts:
typescript
// plugins/revx-inject.ts
import type { Plugin } from "vite";
export function revxInjectPlugin(): Plugin {
return {
name: "revx-inject-macros",
transformIndexHtml(html) {
// 在 head 顶部注入宏变量
const snippet = `<script>window.trkLink="|CLICK|NOENCODING|";window.ctaLink="|CLICK_URL|";</script>`;
return html.replace("<head>", `<head>${snippet}`);
},
};
}
配置 vite.config.ts:
typescript
import { defineConfig, loadEnv } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
import { revxInjectPlugin } from "./plugins/revx-inject";
export default defineConfig(({ mode }) => {
// 加载对应的 .env 文件
const env = loadEnv(mode, process.cwd(), "VITE_");
const isRevX = env.VITE_AD_PLATFORM === "revx";
return {
plugins: [
// RevX 专属插件
...(isRevX ? [revxInjectPlugin()] : []),
// 单文件打包插件
viteSingleFile(),
],
build: {
// 强制内联所有资源 (100MB)
assetsInlineLimit: 100000000,
minify: "esbuild",
},
};
});
自动化构建脚本
配置 package.json 及 Shell 脚本实现多平台并行构建。
package.json:
json
"scripts": {
"build:facebook": "vite build --mode facebook",
"build:liftoff": "vite build --mode liftoff",
"build:revx": "vite build --mode revx",
"build:all": "bash scripts/build-all.sh"
}
scripts/build-all.sh:
bash
#!/usr/bin/env bash
set -e
PLATFORMS=("liftoff" "moloco" "facebook" "revx")
echo "▶ 开始并行构建..."
for platform in "${PLATFORMS[@]}"; do
(
echo " [${platform}] Building..."
npx vite build --mode "$platform" --outDir "dist/${platform}" --logLevel warn
# 此处可添加 zip 压缩逻辑
) &
done
wait
echo "✅ 所有平台构建完成!"
执行 npm run build:all,dist 目录下就会生成所有平台的包。
facebook资源内联与跨域问题
原因
Vite 将资源转为 Base64 Data URI,但 PixiJS 的 Assets Loader 和 @pixi/sound 底层默认使用 fetch() / XHR 加载这些 Data URI。在 MRAID 或沙盒 iframe 中,fetch("data:...") 可能会被浏览器安全策略拦截。
解决
绕过 Loader,手动处理 Base64 资源。
1. 图片:使用 Image 对象
不要用 Assets.load,改用原生 Image 对象加载 Base64,再创建 Texture。
typescript
// src/utils/textures.ts
function loadImg(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url; // url is base64 string
});
}
// 使用
const img = await loadImg(base64Url);
const texture = Texture.from(img);
2. 音频:使用 AudioContext
@pixi/sound 也会发起请求。我们需要手动将 Base64 解码为 ArrayBuffer。
typescript
// src/utils/audio.ts
/**
* Preload all audio assets (no fetch / no network).
*/
public async init() {
if (this.initialized) return;
this.ctx = new AudioContext();
const entries: [string, string][] = [
["spinButton", spinButtonUrl],
["jackpot", jackpotUrl],
];
await Promise.all(
entries.map(async ([key, url]) => {
let arrayBuf: ArrayBuffer;
if (url.startsWith("data:")) {
arrayBuf = dataUrlToArrayBuffer(url);
} else {
const response = await fetch(url);
arrayBuf = await response.arrayBuffer();
}
this.buffers[key] = await this.ctx.decodeAudioData(arrayBuf);
}),
);
this.initialized = true;
}
function dataUrlToArrayBuffer(dataUrl: string): ArrayBuffer {
const base64 = dataUrl.split(",")[1];
const binary = atob(base64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
// 解码
const arrayBuf = dataUrlToArrayBuffer(base64Url);
const audioBuf = await ctx.decodeAudioData(arrayBuf);
改用这种方式后,可以解决 Facebook 跨域导致的无法加载问题。
附录:官方文档与测试工具
在开发过程中,常备各平台的官方文档和测试工具,能少走不少弯路。
官方文档
- Liftoff : Creative Integration Guide v2
- Moloco : IEC 素材指南
- Facebook : 试玩广告素材规格
- RevX : Playable Requirements
测试工具
- Facebook : Playable Preview
- Liftoff : Creative Validator