🔧
Next.js で next epoxrt (静的HTML出力) したときに PixiJS の Sprite が描画されない
導入
開発時の next dev
では PixiJS > Sprite の [背景色の指定 や 読み込んだ画像] が適切に描画されていたのに、next build && next export
で出力された静的HTMLでは描画されない問題がありました。一日かけて解決しましたので参考まで。
使用するライブラリのバージョン
-
next
- 13.0.3 -
pixi.js
- 7.0.4
React で PixiJS を使用する場合、通常は ReactPixi を使用する方が楽かと思いますが、現在 (2022年11月13日) 時点の Next.js では ReactPixi が動作しなかったため、pixi.js
を直接読み込んで使用しました。
方針
-
pixi.js
をクライアントサイド (CDN経由) で読み込む - TypeScript の型も解決したい
Sprite 描画されなかった原因は、next build
した際のライブラリの最適化(コンパイル)にありそうでした。
これを回避するため、ESMのCDNを直接 import したり、import するライブラリを最適化(コンパイル)させない方法も考えられそうですが、Next 13 から Webpack → Turbopack が進んだりとビルド方面が不安定なので、今回は汎用的に使えそうなクライアントサイドでの読み込みで解決としました。
実装方法
pixi.js
をクライアントサイド (CDN経由) で読み込む
1.) pages/_document.tsx
class MyDocument extends Document {
override render() {
return (
<Html className="h-full">
<Head>
+ <script
+ src="https://cdn.jsdelivr.net/npm/pixi.js@7.0.4/dist/pixi.min.js"
+ integrity="sha256-7p4g8H3cJS5tVFDTbx2ExxiJwQ/PTAZLwdc6kyvXa+0="
+ crossOrigin="anonymous"
+ />
</Head>
<body className="h-full">
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
2.) TypeScript の型も解決したい
クライアントサイドの PIXI
を使用しつつ、import type * as PIXI from 'pixi.js'
で得られる型を使いたい。
表題の不具合の解決は getGlobalPIXI()
を useEffect()
内で読み込むとのことで解決しますが、React で pixi.js
を初期化するサンプルとしてもコード一式を載せておきます。
index.tsx
import type { FC } from 'react'
import { useEffect, useState, useRef } from 'react'
+ import type * as PIXI from 'pixi.js'
import { useEffectOnce, useMeasure } from 'react-use'
+ const global =
+ typeof window !== 'undefined'
+ ? (window as Window &
+ typeof globalThis & {
+ PIXI: {
+ Application: typeof PIXI.Application
+ Container: typeof PIXI.Container
+ Graphics: typeof PIXI.Graphics
+ Sprite: typeof PIXI.Sprite
+ }
+ })
+ : null
+ const getGlobalPIXI = () => {
+ if (!global || !global.PIXI) throw new Error('PIXI is not defined')
+ return global.PIXI
+ }
const useHook = () => {
const ref = useRef<HTMLDivElement>(null)
const [measureRef, { width, height }] = useMeasure<HTMLDivElement>()
const [isReady, setIsReady] = useState(false)
const stateRef = useRef({
app: null as PIXI.Application | null,
container: null as PIXI.Container | null,
width,
height,
})
// Update stateRef by states
useEffect(() => {
stateRef.current.width = width
stateRef.current.height = height
}, [width, height])
// Initialize PIXI App
useEffectOnce(() => {
const el = ref.current as HTMLDivElement
measureRef(el)
const gPIXI = getGlobalPIXI()
const app = new gPIXI.Application({
resolution: window.devicePixelRatio,
backgroundAlpha: 0,
})
const container = new gPIXI.Container()
app.stage.addChild(container)
el.appendChild(app.view as any)
stateRef.current.app = app
stateRef.current.container = container
setIsReady(true)
return () => {
app.destroy(true, true)
stateRef.current.app = null
stateRef.current.container = null
setIsReady(false)
}
})
// Resize canvas
useEffect(() => {
const { app } = stateRef.current
if (!app) return
app.renderer.resize(width, height)
}, [height, width])
useEffect(() => {
const { app, container } = stateRef.current
if (!isReady || !app || !container) return
const gPIXI = getGlobalPIXI()
const frame = new gPIXI.Container()
const image = gPIXI.Sprite.from(
'https://avatars.githubusercontent.com/u/1271863?v=4'
)
image.anchor.set(0.5)
frame.addChild(image)
container.addChild(frame)
// Implement animation
app.ticker.add((delta: number) => {
const { width, height } = stateRef.current
image.position.set(width / 2, height / 2)
image.rotation += 0.01 * delta
})
return () => {
while (container.children[0]) {
container.removeChild(container.children[0])
}
}
}, [isReady])
return {
ref,
}
}
const Page: FC = () => {
const { ref } = useHook()
return <div ref={ref} className="h-full" />
}
export default Page
Discussion