腾讯出品:PAG 动效 Vue3 保姆级上手教程,两千万用户生产环境验证可用!

4,616 阅读9分钟

image.png


一、PAG介绍

PAG(Pictorial Animation Generator)是腾讯推出的一款高效的动效引擎,它旨在帮助开发者快速实现高质量的动效,尤其是在小程序、Web 和移动端等平台上。PAG 引擎的特点是轻量级、高性能和高度可定制,能够带来流畅的动画体验,特别适用于需要复杂动效的场景。PAG 动效通过将动画文件转换成二进制格式,提升了加载速度并减轻了渲染负担。

PAG 的主要优势包括:

  • 轻量高效:相比传统的动效,PAG 动效的文件体积小,加载速度快,渲染流畅。
  • 跨平台支持:不仅支持 Web、iOS 和 Android,还支持小程序平台。
  • 灵活的动画控制:支持关键帧控制、动效的播放、暂停、反转等操作。

在 Web 项目中,PAG 可以与 Vue3 等框架结合使用,带来优秀的用户体验。

二、PAG Web SDK 安装方式

PAG 提供了 Web SDK 来支持在 Web 环境下使用其动效。我们可以通过以下三种方式来安装 PAG Web SDK:

1. 使用 CDN 引入

最简单的方式是通过 CDN 引入 PAG 的 Web SDK。这种方式无需安装任何依赖,适合快速开发。

<script src="https://cdn.jsdelivr.net/npm/libpag@latest/lib/libpag.min.js"></script>

2. 使用 npm 安装

如果希望在项目中使用 npm 管理依赖,可以通过 npm 安装 PAG Web SDK。使用以下命令来安装:

npm install @pag/pag-web-sdk --save

# 使用 yarn 安装
yarn add libpag

# 使用 pnpm 安装
pnpm add libpag

三、PAG 动效使用

1. 创建效果

在 Vue3 中,使用 PAG 动效通常涉及加载动画文件并渲染到 DOM 元素上。首先,确保你已经通过 CDN 或 npm/yarn 安装了 PAG SDK。

首先从官网上下载一个 Demo 动画文件。

地址:pag.io/docs/pag-te…

screencapture-pag-io-docs-pag-test-material-html-2024-11-30-09_38_05.png

2. 配置 PAG

这里以 Vite + Vue3 的项目工程为例进行配置演示。

如果你还没有创建工程,可以试试我的 启动模板image.png

在通过 npm 正确安装依赖后,需要进行一个简单的配置,确保在 Vite 项目中可用。

首先运行:npm install rollup-plugin-copy -D,在 vite.config.ts 具体配置如下:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import copy from 'rollup-plugin-copy'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    copy({
      targets: [
        { src: './node_modules/libpag/lib/libpag.wasm', dest: process.env.NODE_ENV === 'production' ? 'dist/' : 'public/' },
      ],
      hook: process.env.NODE_ENV === 'production' ? 'writeBundle' : "buildStart",
    }),
  ],
  base: './',
});

这个时候如果运行 npx vite dev,则当前项目的 public 目录下会出现一个 libpag.wasm 的文件,这个文件是 PAG 的核心。

image.png

如果是 React ,参考官方提供的 React 示例 Demo:github.com/libpag/pag-…

3. 加载动画

将下载好的 PAG 素材文件放置到项目工程中,这里我放在了 public 目录下。

image.png

接下来创建一个 PAGAnimation.vue 的组件,放置到 src/components 下,代码如下:

<script setup lang="ts">
import { PAGInit } from 'libpag'

PAGInit().then((PAG) => {
  const url = '/dev/like.pag'
  fetch(url)
    .then(response => response.blob())
    .then(async (blob) => {
      const file = new window.File([blob], url.replace(/(.*\/)*([^.]+)/, '$2'))
      const pagFile = await PAG.PAGFile.load(file)
      const dom = document.getElementById('pag') as HTMLCanvasElement
      if (!dom) {
        console.error('Failed to get canvas element')
        return
      }
      dom.width = pagFile.width()
      dom.height = pagFile.height()
      const pagView = await PAG.PAGView.init(pagFile, '#pag')
      if (!pagView) {
        console.error('Failed to initialize PAGView')
        return
      }
      pagView.setRepeatCount(0)
      await pagView.play()
    })
})
</script>

<template>
  <div>
    <canvas id="pag" class="canvas" />
  </div>
</template>

将组件放置到页面里我们就可以正常看到动效了。

动画.gif

更多玩法和内容参考官方文档:pag.io/docs/web-pl…

4. 正确的销毁

为了避免内存泄漏和性能问题,需要在组件销毁时销毁 PAG 动效实例。使用 Vue3 的 onBeforeUnmount 生命周期钩子来销毁 PAG 动效实例。

onBeforeUnmount(() => { pagPlayer.destroy(); });

由于刚才代码的变量在函数内部,所以将变量放到外部方便调用,完整代码如下:

<script setup lang="ts">
import type { PAGView } from 'libpag/types/pag-view'
import type { PAG } from 'libpag/types/types'
import { PAGInit } from 'libpag'

let PAGInstance: PAG

let PAGViewInstance: PAGView

PAGInit().then((PAG) => {
  PAGInstance = PAG
  const url = `/dev/like.pag`
  fetch(url)
    .then(response => response.blob())
    .then(async (blob) => {
      const file = new window.File([blob], url.replace(/(.*\/)*([^.]+)/, '$2'))
      const pagFile = await PAGInstance?.PAGFile.load(file)
      if (!pagFile) {
        console.error('Failed to load PAG file')
        return
      }
      const dom = document.getElementById('pag') as HTMLCanvasElement
      if (!dom) {
        console.error('Failed to get canvas element')
        return
      }
      dom.width = pagFile.width()
      dom.height = pagFile.height()
      PAGViewInstance = await PAG.PAGView.init(pagFile, '#pag') as PAGView
      if (!PAGViewInstance) {
        console.error('Failed to initialize PAGView')
        return
      }
      PAGViewInstance.setRepeatCount(0)
      await PAGViewInstance.play()
    })
})

onBeforeUnmount(() => {
  if (PAGViewInstance.isDestroyed)
    return
  PAGViewInstance.destroy()
})
</script>

<template>
  <div>
    <canvas id="pag" class="canvas" />
  </div>
</template>

四、浏览器兼容性

image.png

libpag Web 端是基于 WebAssembly + WebGL实现,并通过适配 Web 平台能力来支持 libpag 全能力,以上的兼容表仅代表可以运行的兼容性。

libpag 的渲染性能受以下条件影响:

  • PAG 动效文件的复杂度
  • libpag 调用方式
  • Web 浏览器环境 本文主要讲解 libpag Web 端在各个浏览器中的性能,以及一些兼容兜底方案。

1. 桌面端

  • Chrome (Win/Mac)

性能表现良好

  • Safari (Mac)

性能表现良好

  • Firefox (Win/Mac)

除带 BMP 序列帧的 PAG 动效文件外,性能表现良好。

因为 Firefox 的 Video 标签无法解析 PAG 动效文件中 BMP 序列帧转化而成的视频,所以需要注册软件解码器 ffavc 来解析。示例Demo

2. 移动端

  • Safari (iOS)

除带 BMP 序列帧的 PAG 动效文件外,性能表现良好。

libpag 解析带 BMP 序列帧的 PAG 动效文件调用了 Video 标签的 blobURL 属性解码视频,而在 iOS Safari 浏览器上当 blobURLsrcVideo 时,会存在“播放到视频末尾掉帧”、“修改 currentTime 后 currentTime 属性不变化,而 video 画面渲染成功”等 BUG。

而且当 iOS 设备处于省电模式或者在微信浏览器中,存在“用户与页面交互之后才可以使用 Video 标签进行视频播放”的规则限制,所以,使用播放动效的场景需要经过用户交互之后播放。

以上两个环境兼容性的影响,可以使用注册软件解码器 ffavc 解析视频来规避,也是当前推荐的方案。示例Demo

  • Chrome (Android)

除带 BMP 序列帧的 PAG 动效文件外,性能表现良好。

部分 Android 设备和在微信浏览器中,存在“用户与页面交互之后才可以使用 Video 标签进行视频播放”的规则限制,所以,使用播放动效的场景需要经过用户交互之后播放。

这里也使用注册软件解码器 ffavc 解析视频来规避,但是 ffavc 软解码在 Android 设备中的性能表现较差,所以我们并不推荐使用这个方案,后续我们会努力寻求更优秀的方案来覆盖这个场景。暂时需要接入方从业务场景中引导用户产生交互来规避“用户与页面交互之后才可以使用 Video 标签进行视频播放”的规则限制。

  • Android 原生浏览器

因为各个厂商都有自己的自带浏览器,而自带浏览器的环境除了会收到厂商浏览器实现差异影响还会受到 GPU 芯片差异影响。所以,我们暂时没有计划对这部分机器进行兼容,我们首要的精力会放在主流的浏览器中。

五、踩坑记录

在使用 PAG 动效时,可能会遇到一些常见的问题,下面是一些常见的踩坑及解决方法:

1. 多个 PAGView 同时存在

当多个 PAGView 同时存在时,浏览器会同时渲染多个动画,这可能导致性能下降。尤其是在页面有多个复杂的动画时,渲染的帧数可能会受到影响,导致动画卡顿,甚至浏览器崩溃。

因为 PAG Web 版是单线程的 SDK,所以不建议同屏播放多个 PAGView

对于有多个 PAGView 实例的场景,我们需要先知道,浏览器环境中 WebGL 活跃的 context 数量是有限制的,Chrome 是 16 个, Safari 是 8 个。因为有这个限制存在,我们应当及时使用 destroy 回收无用的 PAGView 实例和移除 Canvas 的引用。

调用 destroy 的同时建议一并移除 canvas 元素。

推荐的解决方案是使用 PAG 的组合模式,自己创建一个 PAGComposition,然后把需要播放的 PAGFile 通过 addLayer 的方法添加 进去,setStartTime 可以设置每个 PAGFile 相对于 PAGComposition 的开始时间,setMatrix 可以设置相当于 PAGComposition 的位置。

2. PAG 资源文件加载慢和缓存

由于作者在同一屏(首屏)用到了超过10个不同的 PAG 动效文件。应甲方要求的性能指标,需要一方面需要减少 PAG 的资源体积,另一方面进行资源缓存复用。

  • 动效文件的体积交付 UI 进行导出优化,最终结果为单个特效文件体积不超过 60kb。

    素材优化指南:pag.io/docs/optimi…

  • 所有的 PAG 动效文件全部上 CDN 进行异步懒加载。
  • 通过浏览器的 Index DB 进行资源缓存和复用。

image.png

3. libpag.wasm 体积大

在 PAG 动效时,libpag.wasm 是核心的 WebAssembly 模块,它负责处理动画的解码和渲染任务。由于 libpag.wasm 文件的大小较大,通常达到 2.8MB 左右,这对于 Web 项目来说可能会带来性能问题,尤其是在移动端或者网络环境较差的情况下,加载这么大的文件可能导致较慢的加载速度和较高的延迟。

通过使用 CDN 来加载 libpag.wasm,可以大大减少文件的加载时间和体积。文件体积从 原本的 3MB 大幅减少到了 800KB 左右。这是因为 CDN 服务器会对文件进行优化和压缩,使得传输过程中的体积变得更小,同时下载速度也更快。

image.png

使用 CDN 地址下载 PAG 核心模块体积变化:

image.png

六、组件封装

针对实际业务场景,对 PAG 动效组件进行了简单封装,内置了 PAG 的初始化、播放、暂定方法,支持文件缓存和复用。

完整组件可以访问:github.com/kieranwv/pa…

<script lang="ts" setup>
import type { PAGView } from 'libpag/types/pag-view'
import type { PAG } from 'libpag/types/types'
import { IndexedDB } from '@kieranwv/utils'
import axios from 'axios'
import { PAGInit } from 'libpag'
import { computed, onMounted, onUnmounted, ref } from 'vue'

const props = withDefaults(defineProps<{
  name: string
  path?: string
  version?: string
}>(), {
  path: '/dev',
  version: '0.0.0',
})

const emit = defineEmits<{
  (event: 'rendered'): void
}>()

let PAGInstance: PAG

let PAGViewInstance: PAGView

PAGInit().then((PAG) => {
  PAGInstance = PAG
})

const showCanvas = ref(true)

const request = axios.create()

const db = new IndexedDB()

const pagId = computed(() => `pag-${props.name}`)

const pagFileUrl = computed(() => {
  return `${props.path}/${props.name}.pag`
})

async function loadPagFile() {
  const _cache = await db.find(pagId.value)
  if (
    _cache
    && _cache.value
    && _cache.version === props.version
  ) {
    renderPagFile(_cache.value)
  }
  else {
    request(pagFileUrl.value, { responseType: 'blob' })
      .then(response => response.data)
      .then(async (blob) => {
        const file = new window.File([blob], pagFileUrl.value.replace(/(.*\/)*([^.]+)/, '$2'))
        db.insert(pagId.value, {
          value: file,
          label: pagId.value,
          timestamp: new Date().getTime(),
          version: props.version,
        })
        renderPagFile(file)
      })
  }
}

async function renderPagFile(file: File) {
  if (!PAGInstance) {
    PAGInstance = await PAGInit()
  }
  try {
    const pagFile = await PAGInstance.PAGFile.load(file)
    if (!pagFile) {
      throw new Error('pagFile is null')
    }
    const dom = document.getElementById(pagId.value) as HTMLCanvasElement
    if (!dom) {
      throw new Error('dom is null')
    }
    dom.width = pagFile.width()
    dom.height = pagFile.height()
    PAGViewInstance = await PAGInstance.PAGView.init(pagFile, `#${pagId.value}`) as PAGView
    if (!PAGViewInstance) {
      throw new Error('PAGViewInstance is null')
    }
    PAGViewInstance.setRepeatCount(0)
    await PAGViewInstance.play()
    emit('rendered')
  }
  catch (e) {
    console.error('[PAGAnimation.vue] error: ', e)
  }
}

function play() {
  showCanvas.value = true
  if (PAGViewInstance && !PAGViewInstance.isDestroyed) {
    PAGViewInstance.play()
  }
  else {
    loadPagFile()
  }
}

function stop() {
  if (PAGViewInstance && !PAGViewInstance.isDestroyed) {
    PAGViewInstance.stop()
  }
}

function destroy() {
  if (PAGViewInstance && !PAGViewInstance.isDestroyed) {
    PAGViewInstance.destroy()
    showCanvas.value = false
  }
}

defineExpose({
  play,
  stop,
  destroy,
  id: pagId.value,
})

onMounted(() => {
  play()
})

onUnmounted(() => {
  destroy()
})
</script>

<template>
  <canvas v-if="showCanvas" :id="pagId" />
</template>

这里方便演示,使用 npm 方式安装。如果有性能考量,建议使用 CDN 加载 libpag 模块,体积相对 npm 安装可减少 75%。

CDN 安装指南:pag.io/docs/use-we…

七、总结

通过学习 PAG,可以掌握如何高效地在 Web 和移动端应用中实现流畅的动画效果。PAG 提供了轻量、高效的解决方案,适用于复杂的动效需求。通过本篇文章,开发者可以了解如何在 Vue3 项目中集成和使用 PAG 动效,包括如何通过 CDN、npm 安装,如何正确加载和销毁动画资源,以及如何通过优化策略(如懒加载、资源预加载和 CDN 加速)提升性能。此外,学习如何封装 Vue 组件实现动画复用,以及如何避免常见的性能问题和内存泄漏,能够帮助开发者在实际项目中更好地应用 PAG 动效引擎,提升用户体验。

参考地址