基于PixiJS的试玩广告开发-续篇

9 阅读3分钟

前言

接上篇 基于PixiJS的试玩广告开发中,我们讲到了Moloco平台的试玩广告投放,这次我们的新需求是要兼容更多平台🤣🤣🤣,Facebook、Liftoff、RevX

每个平台的 API 和要求都有些许区别:

  • Facebook: FbPlayableAd.onCTAClick()
  • Liftoff: mraid.open()
  • RevX: 需要注入 trkLinkctaLink

我们使用环境变量区分不同平台,避免维护多份代码。

实施

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() 函数,屏蔽平台差异。业务层只需要调用这个函数,无需关心底层实现。

// 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),即 HTMLCSSJS、图片、音频全部内联到一个 HTML 文件中。

Vite 插件配置

使用 vite-plugin-singlefile 实现内联。针对 RevX 平台,还需要额外注入特定的宏定义脚本。

编写一个简单的插件 plugins/revx-inject.ts

// 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

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.jsonShell 脚本实现多平台并行构建

package.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:

#!/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:alldist 目录下就会生成所有平台的包。

facebook资源内联与跨域问题

原因

Vite 将资源转为 Base64 Data URI,但 PixiJSAssets Loader@pixi/sound 底层默认使用 fetch() / XHR 加载这些 Data URI。在 MRAID 或沙盒 iframe 中,fetch("data:...") 可能会被浏览器安全策略拦截。

解决

绕过 Loader,手动处理 Base64 资源。

1. 图片:使用 Image 对象

不要用 Assets.load,改用原生 Image 对象加载 Base64,再创建 Texture

// 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

  // 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 跨域导致的无法加载问题。

附录:官方文档与测试工具

在开发过程中,常备各平台的官方文档和测试工具,能少走不少弯路。

官方文档

测试工具