悟夕导航

第七集:把 Canvas 塞进 React/Vue,前端框架不打架

113 0 0
“我把 Canvas 写进 useEffect,结果热更新一次,画布被重构得比我还惨。”
—— 某位被 React 刷新机制按在地上摩擦的前端小可爱

大家好,欢迎来到《Canvas 翻车指南》第七集!
前面我们让十万粒子在 WebGL 里蹦迪,但今天上“生产环境”——React & Vue

  • Hook + Ref:生命周期不对?画布瞬间闪崩
  • 自定义指令 / Hook:封装“粒子组件”,props 即配置
  • Vite 热更新:保存状态不丢 WebGL 上下文,开发体验拉满
  • 双端兼容:React 的 Concurrent、Vue 的 KeepAlive,统统拿捏

准备好枸杞泡水,咱们把 Canvas 组件化让框架乖乖当小弟


一、React 篇:useRef 与 useEffect 的黄金搭档

1. 初始化只跑一次

const CanvasParticles: React.FC = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGLRenderingContext | null>(null);
  const rafRef = useRef<number>(0);

  useEffect(() => {
    const canvas = canvasRef.current!;
    const gl = canvas.getContext('webgl')!;
    glRef.current = gl;
    initWebGL(gl); // 编译着色器、上传缓冲区
    const loop = () => {
      render(gl);
      rafRef.current = requestAnimationFrame(loop);
    };
    loop();

    return () => {
      cancelAnimationFrame(rafRef.current);
      // 清理 WebGL 资源,防止内存泄漏
      gl.getExtension('WEBGL_lose_context')?.loseContext();
    };
  }, []); // 空依赖 = 挂载时初始化
  return <canvas ref={canvasRef} width={800} height={600} />;
};

口诀:

“Ref 存对象,Effect 管生死,依赖数组别乱写,否则重载两行泪。”

2. props 响应式更新 —— useImperativeHandle + forwardRef

想让父组件动态改粒子重力

export const ParticleCanvas = forwardRef<ParticleHandle, Props>(
  ({ gravity }, ref) => {
    useImperativeHandle(ref, () => ({
      setGravity(g: number) {
        worker.postMessage({ type: 'setGravity', payload: g });
      },
    }));
    ...
  }
);

父组件调用:

const ctrlRef = useRef<ParticleHandle>(null);
<ParticleCanvas ref={ctrlRef} gravity={0.2} />
<button onClick={() => ctrlRef.current?.setGravity(0.5)}>加重力</button>

不强制重新挂载WebGL 上下文稳稳当当


二、Vue 篇:自定义指令 + composable 双剑合璧

1. 自定义指令 v-particle

// main.ts
app.directive('particle', {
  mounted(el, binding) {
    const { count = 1e4, gravity = 0.1 } = binding.value;
    const gl = el.getContext('webgl');
    initParticleSystem(gl, { count, gravity });
  },
  unmounted(el) {
    cleanup(el);
  }
});

使用:

<canvas v-particle="{ count: 15000, gravity: 0.2 }" width="800" height="600" />

模板即配置像用 v-model 一样简单

2. Composable 封装逻辑

// useParticle.ts
export function useParticle(canvasRef: Ref<HTMLCanvasElement | null>) {
  let raf = 0;
  onMounted(() => {
    const gl = canvasRef.value!.getContext('webgl');
    startLoop(gl);
  });
  onUnmounted(() => cancelAnimationFrame(raf));
  return { pause: () => ..., resume: () => ... };
}

组件内:

<template>
  <canvas ref="canvasEl" width="800" height="600" />
</template>
<script setup lang="ts">
import { useParticle } from '@/composables/useParticle';
const canvasEl = ref<HTMLCanvasElement>();
const { pause, resume } = useParticle(canvasEl);
</script>

响应式暂停 / 继续和 Vue 生命周期同步


三、热更新守护:Vite + WebGL 上下文不丢失

痛点:

  • 修改一行颜色,整页刷新十万粒子回到原点
  • WebGL 上下文重建,显存炸出双峰

解决:

  1. 把粒子状态放 Worker 内存与主线程分离
  2. 使用 import.meta.hot 手动 accept
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    // 只重编译着色器,不重建 Worker
    worker.postMessage({ type: 'hotShader', shaderSource: newSource });
  });
}
  1. Vue 3 的 <KeepAlive> + include 缓存画布组件
  2. React 用 key = stableId避免父级重排导致卸载

口诀:

“热更新像换内裤,别让 WebGL 一起裸奔。”

四、Concurrent & KeepAlive:双框架兼容坑

React 18 Concurrent:

  • 组件会被中断 / 恢复WebGL 渲染要幂等
  • useDeferredValue 把高耗任务拆帧:
const deferredCount = useDeferredValue(particleCount);
useEffect(() => {
  worker.postMessage({ type: 'setCount', payload: deferredCount });
}, [deferredCount]);

Vue 3 KeepAlive:

  • 切走缓存时 deactivated 触发,记得暂停 RAF
  • 回来 activated 再重启,防止后台偷跑 GPU
onActivated(() => raf = requestAnimationFrame(loop));
onDeactivated(() => cancelAnimationFrame(raf));

五、体积优化:Tree-Shaking 与异步 Worker

  • Worker 代码放 new URL('./worker.ts', import.meta.url)Vite 自动拆包
  • WebGL 着色器写 *.glsl 文件,?raw 导入,享受语法高亮
  • 粒子库默认导出工厂函数未用到的渲染器被 Tree-Shake 掉打包体积 < 60 KB (gzip)

六、TypeScript 类型小贴士

export interface ParticleProps {
  count?: number;
  gravity?: number;
  onFrame?: (fps: number) => void;
}
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $particle: ParticlePlugin;
  }
}

组件库即插即用IDE 提示拉满同事再也不敢乱传字符串


七、本集知识点一口闷

技能口诀翻车点
useRef + []只 mount 一次依赖写错反复重建
imperativeHandle父调子方法ref 没 forward 全白搭
v-particle指令即配置unmounted 忘清理内存
hot.accept热更着色器状态放 Worker 否则全丢
KeepAlive切页缓存后台 RAF 不停风扇起飞

八、课后作业(翻车 field)

  1. 封装 NPM 包

    • @your/canvas-particle
    • 支持 React/Vue 双入口,Tree-Shaking 着色器
  2. 加控制台面板

    • 滑条调 count / gravity / size实时生效
  3. BonusStorybook + HMR修改 props 画布不闪录屏晒 NPM 下载量

九、大结局预告(第八集·终章)

第八集《实战:画一只猫,结果画成猪?——项目复盘》
带你完整跑一遍:

  • 需求 → 原型 → 技术选型 → 踩坑 → 性能优化 → 上线
  • 把前面七集技能串成通关连招交付给老板一只“会扭屁股”的 3D 猪猫
  • 开源《Canvas 翻车指南》仓库,Star 破千直播“用 Canvas 画 KPI 曲线,画不好扣工资”

“人生就像把 Canvas 塞进框架,
你不给 Ref,
就只能被生命周期反复重构,连狗都嫌。”

—— 我说的。

第七集结束,点赞、转发、晒 NPM 包
终章一起画只会扭屁股的猪猫,回见!

0
快来点个赞吧

发表评论

隐私评论

评论列表

来写一个评论吧