Next.js中使用PIXI.js出现双画布问题求助
大家好,我在Next.js项目里用PIXI.js开发游戏的时候遇到了个头疼的问题——页面上居然渲染出了两个canvas,而不是预期的一个。这导致了一些渲染和功能上的异常,我检查过代码,确认没有重复调用创建画布或者添加到DOM的逻辑,但问题还是存在。
我的代码(index.js)
import { useEffect } from 'react'; import * as PIXI from 'pixi.js'; import { Howl } from 'howler'; import { createCharacter, handleCharacterClick } from '../utils/character.js'; import { createScoreCounter, incrementScore, getScore, getScoreCounter } from '../utils/score.js'; import { createBonusCounter, incrementBonusScore, decayBonusScore } from '../utils/bonus.js'; export default function Home() { useEffect(() => { let app; function resize() { const aspectRatio = 719 / 420; // Aspect ratio of the background image let w, h; if (window.innerWidth / window.innerHeight > aspectRatio) { w = window.innerHeight * aspectRatio; h = window.innerHeight; } else { w = window.innerWidth; h = window.innerWidth / aspectRatio; } app.view.style.width = w + 'px'; app.view.style.height = h + 'px'; app.view.style.left = (window.innerWidth - w) / 2 + 'px'; // Center horizontally app.view.style.top = (window.innerHeight - h) / 2 + 'px'; // Center vertically } // Load the background image and get its dimensions let backgroundImageOnLoad = new Promise((resolve, reject) => { const img = new window.Image(); img.onload = () => resolve({ width: img.width, height: img.height }); img.onerror = reject; img.src = '/assets/images/bed.jpg'; }); backgroundImageOnLoad.then(dimensions => { // create a new Pixi application with the same dimensions as the background image app = new PIXI.Application({ width: dimensions.width, height: dimensions.height }); // call resize function to properly size the view initially resize(); // add the app view (canvas element) to the document body document.getElementById('game').appendChild(app.view); // create a texture from the background image and add it to the stage let backgroundTexture = PIXI.Texture.from('/assets/images/bed.jpg'); let backgroundSprite = new PIXI.Sprite(backgroundTexture); app.stage.addChild(backgroundSprite); // create character and add to stage let character = createCharacter(app); character.interactive = true; character.on('pointerdown', () => { handleCharacterClick(character, app); incrementScore(); }); app.stage.addChild(character); // create and add score counter to stage createScoreCounter(); let scoreCounter = getScoreCounter(); app.stage.addChild(scoreCounter); // create and add bonus counter to stage const bonusCounter = createBonusCounter(); app.stage.addChild(bonusCounter.container); // Load the background music const backgroundMusic = new Howl({ src: ['/assets/audio/background-music-2.mp3'], loop: true, volume: 0.5, }); // Play the background music backgroundMusic.play(); // attach resize function to window resize event window.addEventListener('resize', resize); }); // cleanup logic if needed return () => { window.removeEventListener('resize', resize); }; }, []); return ( <div id="game"></div> ); }
渲染后的HTML片段
<html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <meta name="next-head-count" content="2"> <noscript data-n-css=""></noscript> <script defer="" nomodule="" src="/_next/static/chunks/polyfills.js?ts=1688999013977"></script><script src="/_next/static/chunks/webpack.js?ts=1688999013977" defer=""></script><script src="/_next/static/chunks/main.js?ts=1688999013977" defer=""></script><script src="/_next/static/chunks/pages/_app.js?ts=1688999013977" defer=""></script><script src="/_next/static/chunks/pages/index.js?ts=1688999013977" defer=""></script><script src="/_next/static/development/_buildManifest.js?ts=1688999013977" defer=""></script><script src="/_next/static/development/_ssgManifest.js?ts=1688999013977" defer=""></script> <style> /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZVJvb3QiOiIifQ== */ </style> <noscript id="__next_css__DO_NOT_USE__"></noscript> </head> <body> <div id="__next"> <div id="game"> <canvas style="touch-action: none; width: 883.343px; height: 516px; left: 518.329px; top: 0px; cursor: inherit;" width="719" height="420"></canvas> <canvas style="touch-action: none; width: 883.343px; height: 516px; left: 518.329px; top: 0px; cursor: inherit;" width="719" height="420"></canvas> </div> </div> <script src="/_next/static/chunks/react-refresh.js?ts=1688999013977"></script><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"development","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script> <div id="__next-build-watcher" style="position: fixed; bottom: 10px; right: 20px; width: 0px; height: 0px; z-index: 99999;" data-dashlane-shadowhost="true" data-dashlane-observed="true"></div> <next-route-announcer> <p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px none; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; top: 0px; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p> </next-route-announcer> </body> </html>
我尝试过的排查方向和可能的解决思路
我已经确认主文件里没有重复创建PIXI.Application或者重复调用appendChild的逻辑,但还是出现了双画布。结合Next.js的特性,我猜测可能是以下几个原因:
Next.js开发模式的React Strict Mode:开发模式下Strict Mode会刻意重复调用
useEffect这类生命周期函数,这可能导致PIXI.Application被创建两次,进而生成两个canvas。解决办法:可以先暂时关闭Strict Mode试试——找到
_app.js文件,去掉<React.StrictMode>的包裹,重启开发服务看问题是否消失。如果有效,就给useEffect加个初始化标记,确保只执行一次:export default function Home() { const appInitialized = useRef(false); useEffect(() => { if (appInitialized.current) return; appInitialized.current = true; // 原有代码... return () => { window.removeEventListener('resize', resize); app?.destroy(true); // 清理PIXI实例 }; }, []); return <div id="game"></div>; }PIXI.js的自动附加行为:部分版本的PIXI.js在创建
Application时,默认会自动把canvas附加到document.body,而我们又手动添加到了#game元素里,导致两次添加。解决办法:创建
PIXI.Application时加上autoAttach: false选项,手动控制DOM添加:app = new PIXI.Application({ width: dimensions.width, height: dimensions.height, autoAttach: false });工具函数的意外创建:虽然主文件没问题,但
createCharacter、createScoreCounter这些工具函数里会不会有额外创建画布的逻辑?建议检查这些文件,确保没有重复生成canvas的代码。
希望有朋友能帮我定位下问题,或者分享类似问题的解决经验!
备注:内容来源于stack exchange,提问作者Martin Erlic

