使用 React + Konva 构建交互式立方体绘制工具

使用 React + Konva 构建交互式立方体绘制工具

项目概述

最近在学习 Konva.js,正好实战一下,本文将详细介绍如何使用 React 和 Konva.js 构建一个交互式的立方体绘制工具。该工具允许用户通过四次点击来绘制一个3D立方体的投影,具有实时预览和多立方体支持功能。

技术栈

  • React 19.1.0 - 前端框架
  • Konva 9.3.22 - 2D Canvas 图形库
  • react-konva 19.0.7 - React 的 Konva 绑定
  • Vite - 构建工具

核心功能特性

1. 状态驱动的绘制流程

应用采用状态机模式管理绘制过程:

javascript 复制代码
// 绘制状态:0-等待第一个点,1-绘制矩形,2-等待第二个点,3-等待第三个点,4-绘制立方体,5-完成
const [drawingState, setDrawingState] = useState(0);

2. 四步绘制流程详解

第一步:确定第一个矩形的起始点

当用户第一次点击画布时,应用记录起始点并进入动态矩形绘制状态:

javascript 复制代码
if (drawingState === 0) {
  // 第一个点
  setPoints([pos]);
  setDrawingState(1);
}

此时鼠标移动会触发动态矩形的实时预览:

javascript 复制代码
// 在 handleMouseMove 中
if (drawingState === 1 && points.length === 1) {
  const corners = calculateRectCorners(points[0], pos);
  setRectCorners(corners);
}

// 动态矩形渲染
const renderDynamicRect = () => {
  if (drawingState === 1 && points.length === 1) {
    const width = Math.abs(mousePos.x - points[0].x);
    const height = Math.abs(mousePos.y - points[0].y);
    const x = Math.min(points[0].x, mousePos.x);
    const y = Math.min(points[0].y, mousePos.y);

    return (
      <Rect
        x={x} y={y} width={width} height={height}
        stroke="blue" strokeWidth={2}
        dash={[5, 5]} fill="transparent"
      />
    );
  }
  return null;
};
第二步:完成第一个矩形(前面)

第二次点击确定矩形的对角点,完成前面矩形的绘制:

javascript 复制代码
else if (drawingState === 1) {
  // 第二个点,确定矩形
  const newPoints = [...points, pos];
  setPoints(newPoints);
  const corners = calculateRectCorners(points[0], pos);
  setRectCorners(corners);
  setDrawingState(2);
}

此时渲染固定的矩形,并显示从矩形四角到鼠标的虚线预览:

javascript 复制代码
// 固定矩形渲染
const renderFixedRect = () => {
  if (drawingState === 2 && rectCorners.length === 4) {
    const minX = Math.min(...rectCorners.map(p => p.x));
    const maxX = Math.max(...rectCorners.map(p => p.x));
    const minY = Math.min(...rectCorners.map(p => p.y));
    const maxY = Math.max(...rectCorners.map(p => p.y));

    return (
      <Rect
        x={minX} y={minY}
        width={maxX - minX} height={maxY - minY}
        stroke="black" strokeWidth={2}
        fill="rgba(173, 216, 230, 0.3)"
      />
    );
  }
  return null;
};

// 虚线连接预览
const renderDashedLines = () => {
  if (drawingState === 2 && rectCorners.length === 4) {
    return rectCorners.map((corner, index) => (
      <Line
        key={`dynamic-${index}`}
        points={[corner.x, corner.y, mousePos.x, mousePos.y]}
        stroke="gray" strokeWidth={1} dash={[3, 3]}
      />
    ));
  }
  return null;
};
第三步:确定第二个矩形的起始点

第三次点击确定后面矩形的起始点:

javascript 复制代码
else if (drawingState === 2) {
  // 第三个点
  const newPoints = [...points, pos];
  setPoints(newPoints);
  setDrawingState(3);
}

此时开始显示完整的立方体预览,包括前面矩形、后面矩形和连接线:

javascript 复制代码
const renderDynamicCube = () => {
  if (drawingState === 3 && points.length >= 3 && rectCorners.length === 4) {
    const point3 = points[2];
    const point4 = mousePos; // 当前鼠标位置作为第四个点

    // 计算第二个矩形的四个角点
    const backCorners = calculateRectCorners(point3, point4);

    return (
      <>
        {/* 前面的矩形(已确定) */}
        <Rect
          x={Math.min(...rectCorners.map(p => p.x))}
          y={Math.min(...rectCorners.map(p => p.y))}
          width={Math.max(...rectCorners.map(p => p.x)) - Math.min(...rectCorners.map(p => p.x))}
          height={Math.max(...rectCorners.map(p => p.y)) - Math.min(...rectCorners.map(p => p.y))}
          fill="rgba(173, 216, 230, 0.3)"
          stroke="black" strokeWidth={2}
        />

        {/* 后面的矩形(动态生成) */}
        <Rect
          x={Math.min(...backCorners.map(p => p.x))}
          y={Math.min(...backCorners.map(p => p.y))}
          width={Math.max(...backCorners.map(p => p.x)) - Math.min(...backCorners.map(p => p.x))}
          height={Math.max(...backCorners.map(p => p.y)) - Math.min(...backCorners.map(p => p.y))}
          fill="rgba(255, 255, 0, 0.2)"
          stroke="blue" strokeWidth={2}
        />

        {/* 连接前后两个矩形对应角点的线 */}
        {rectCorners.map((corner, index) => (
          <Line
            key={`cube-edge-${index}`}
            points={[corner.x, corner.y, backCorners[index].x, backCorners[index].y]}
            stroke="green" strokeWidth={2} dash={[3, 3]}
          />
        ))}
      </>
    );
  }
  return null;
};
第四步:完成立方体绘制

第四次点击确定后面矩形的对角点,完成整个立方体的绘制:

javascript 复制代码
else if (drawingState === 3) {
  // 第四个点,完成立方体
  // 计算第二个矩形的四个角点
  const backCorners = calculateRectCorners(points[2], pos);

  // 保存完成的立方体(包含两个矩形的角点)
  const newCube = {
    frontRect: [...rectCorners],  // 第一个矩形
    backRect: [...backCorners]    // 第二个矩形
  };
  setCompletedCubes([...completedCubes, newCube]);

  // 重置状态,准备绘制下一个立方体
  setPoints([]);
  setRectCorners([]);
  setDrawingState(0);
}

完成的立方体会被永久保存并渲染:

javascript 复制代码
const renderCompletedCubes = () => {
  return completedCubes.map((cube, cubeIndex) => {
    const { frontRect, backRect } = cube;

    return (
      <React.Fragment key={`cube-${cubeIndex}`}>
        {/* 前面的矩形 */}
        <Rect
          x={Math.min(...frontRect.map(p => p.x))}
          y={Math.min(...frontRect.map(p => p.y))}
          width={Math.max(...frontRect.map(p => p.x)) - Math.min(...frontRect.map(p => p.x))}
          height={Math.max(...frontRect.map(p => p.y)) - Math.min(...frontRect.map(p => p.y))}
          fill="rgba(173, 216, 230, 0.5)"
          stroke="black" strokeWidth={3}
        />

        {/* 后面的矩形 */}
        <Rect
          x={Math.min(...backRect.map(p => p.x))}
          y={Math.min(...backRect.map(p => p.y))}
          width={Math.max(...backRect.map(p => p.x)) - Math.min(...backRect.map(p => p.x))}
          height={Math.max(...backRect.map(p => p.y)) - Math.min(...backRect.map(p => p.y))}
          fill="rgba(255, 255, 0, 0.3)"
          stroke="black" strokeWidth={3}
        />

        {/* 连接前后两个矩形对应角点的线 */}
        {frontRect.map((corner, index) => (
          <Line
            key={`completed-edge-${cubeIndex}-${index}`}
            points={[corner.x, corner.y, backRect[index].x, backRect[index].y]}
            stroke="black" strokeWidth={3}
          />
        ))}
      </React.Fragment>
    );
  });
};

3. 实时预览功能

  • 动态矩形预览:第一次点击后,矩形跟随鼠标实时变化
  • 虚线连接预览:第二次点击后,显示从矩形四角到鼠标的虚线
  • 立方体预览:第三次点击后,实时显示完整的立方体结构

核心实现解析

状态管理架构

应用使用多个 React 状态来管理绘制过程:

javascript 复制代码
const [drawingState, setDrawingState] = useState(0);     // 当前绘制阶段
const [points, setPoints] = useState([]);               // 用户点击的点
const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); // 实时鼠标位置
const [rectCorners, setRectCorners] = useState([]);     // 第一个矩形的四个角点
const [completedCubes, setCompletedCubes] = useState([]); // 已完成的立方体

关键算法:矩形角点计算

这是整个应用的核心算法,将两个对角点转换为矩形的四个角点:

javascript 复制代码
const calculateRectCorners = (point1, point2) => {
  return [
    { x: point1.x, y: point1.y }, // 左上角
    { x: point2.x, y: point1.y }, // 右上角
    { x: point2.x, y: point2.y }, // 右下角
    { x: point1.x, y: point2.y }  // 左下角
  ];
};

这个函数确保了无论用户如何拖拽,都能正确计算出矩形的四个角点,为后续的立方体连接提供准确的坐标。

事件处理系统

鼠标移动处理
javascript 复制代码
const handleMouseMove = (e) => {
  const pos = e.target.getStage().getPointerPosition();
  setMousePos(pos);

  // 状态1:动态绘制矩形
  if (drawingState === 1 && points.length === 1) {
    const corners = calculateRectCorners(points[0], pos);
    setRectCorners(corners);
  }
};
鼠标点击处理

点击事件根据当前状态执行不同的逻辑,这是状态机模式的典型应用。每次点击都会推进绘制流程到下一个阶段。

状态转换流程图

scss 复制代码
状态 0 (等待第一个点)
    ↓ 第一次点击
状态 1 (绘制矩形) ← 鼠标移动时实时预览矩形
    ↓ 第二次点击
状态 2 (等待第三个点) ← 鼠标移动时显示虚线连接
    ↓ 第三次点击
状态 3 (等待第四个点) ← 鼠标移动时预览完整立方体
    ↓ 第四次点击
状态 0 (重置,可绘制下一个立方体)

数据结构设计

点的存储
javascript 复制代码
// points 数组存储用户点击的点
[
  { x: 100, y: 100 },  // 第一个点
  { x: 200, y: 200 },  // 第二个点
  { x: 150, y: 150 },  // 第三个点
  // 第四个点直接用于计算,不存储
]
立方体数据结构
javascript 复制代码
// 完成的立方体存储格式
{
  frontRect: [
    { x: 100, y: 100 }, // 前面矩形的四个角点
    { x: 200, y: 100 },
    { x: 200, y: 200 },
    { x: 100, y: 200 }
  ],
  backRect: [
    { x: 150, y: 150 }, // 后面矩形的四个角点
    { x: 250, y: 150 },
    { x: 250, y: 250 },
    { x: 150, y: 250 }
  ]
}

渲染系统

应用采用模块化的渲染方式:

  1. renderDynamicRect() - 动态矩形渲染
  2. renderFixedRect() - 固定矩形渲染
  3. renderDashedLines() - 虚线连接渲染
  4. renderDynamicCube() - 动态立方体渲染
  5. renderCompletedCubes() - 完成立方体渲染
  6. renderPoints() - 点击点渲染

视觉设计亮点

颜色方案

  • 前面矩形 :浅蓝色填充 rgba(173, 216, 230, 0.3)
  • 后面矩形 :浅黄色填充 rgba(255, 255, 0, 0.2)
  • 连接线:绿色虚线表示预览,黑色实线表示完成

交互反馈

  • 鼠标样式:十字光标提示绘制模式
  • 点标记:蓝色圆点标记前两个点,红色圆点标记第三个点
  • 虚线预览:提供清晰的视觉引导

技术难点与解决方案

1. 3D 投影的 2D 表示

挑战:如何在2D画布上表现3D立方体的空间感?

解决方案

  • 使用两个平行矩形表示立方体的前后面
  • 通过四条连接线表示立方体的深度和透视关系
  • 采用不同的颜色和透明度增强层次感
javascript 复制代码
// 前面矩形:较深的颜色,表示距离观察者更近
fill="rgba(173, 216, 230, 0.5)"

// 后面矩形:较浅的颜色,表示距离观察者更远
fill="rgba(255, 255, 0, 0.3)"

// 连接线:表示立方体的边,创造深度感
{frontRect.map((corner, index) => (
  <Line
    points={[corner.x, corner.y, backRect[index].x, backRect[index].y]}
    stroke="black" strokeWidth={3}
  />
))}

2. 实时预览的性能优化

挑战:鼠标移动时频繁的重渲染可能导致性能问题。

解决方案

  • 使用条件渲染,只在特定状态下渲染预览元素
  • 将渲染逻辑拆分为独立的函数,便于 React 优化
  • 避免在渲染函数中进行复杂计算
javascript 复制代码
// 条件渲染避免不必要的计算
const renderDynamicRect = () => {
  if (drawingState === 1 && points.length === 1) {
    // 只在需要时进行计算和渲染
    return <Rect ... />;
  }
  return null; // 其他状态下不渲染
};

3. 状态一致性管理

挑战:确保鼠标移动和点击事件的状态始终同步。

解决方案

  • 使用统一的状态管理,所有组件共享同一个状态源
  • 在状态转换时同时更新相关的所有状态
  • 使用严格的状态检查避免异常情况
javascript 复制代码
// 状态转换时的完整更新
if (drawingState === 3) {
  const backCorners = calculateRectCorners(points[2], pos);
  const newCube = {
    frontRect: [...rectCorners],
    backRect: [...backCorners]
  };

  // 原子性更新:同时更新多个相关状态
  setCompletedCubes([...completedCubes, newCube]);
  setPoints([]);           // 清空点击点
  setRectCorners([]);      // 清空矩形角点
  setDrawingState(0);      // 重置绘制状态
}

4. 坐标系统的处理

挑战:处理用户可能以任意方向拖拽矩形的情况。

解决方案

  • 使用 Math.minMath.max 确保矩形坐标的正确性
  • 计算角点时考虑所有可能的拖拽方向
javascript 复制代码
// 处理任意方向的拖拽
const width = Math.abs(mousePos.x - points[0].x);
const height = Math.abs(mousePos.y - points[0].y);
const x = Math.min(points[0].x, mousePos.x);
const y = Math.min(points[0].y, mousePos.y);

5. 多立方体的层次管理

挑战:多个立方体重叠时的显示顺序和交互问题。

解决方案

  • 使用数组顺序控制渲染层次(后绘制的在上层)
  • 为每个立方体分配唯一的 key 值
  • 完成的立方体使用更粗的线条和更高的透明度
javascript 复制代码
// 渲染顺序:先渲染完成的立方体,再渲染当前绘制的
{renderCompletedCubes()}  // 底层
{renderDynamicCube()}     // 顶层

最终效果

完整代码

js 复制代码
import React, { useState } from "react";
import { Stage, Layer, Rect, Line, Circle } from "react-konva";
import './App.css'

export default function CubeDrawer() {
  // 绘制状态:0-等待第一个点,1-绘制矩形,2-等待第二个点,3-等待第三个点,4-绘制立方体,5-完成
  const [drawingState, setDrawingState] = useState(0);

  // 存储点击的点
  const [points, setPoints] = useState([]);

  // 当前鼠标位置
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });

  // 矩形的四个角点
  const [rectCorners, setRectCorners] = useState([]);

  // 完成的立方体
  const [completedCubes, setCompletedCubes] = useState([]);

  // 计算矩形的四个角点
  const calculateRectCorners = (point1, point2) => {
    return [
      { x: point1.x, y: point1.y }, // 左上
      { x: point2.x, y: point1.y }, // 右上
      { x: point2.x, y: point2.y }, // 右下
      { x: point1.x, y: point2.y }  // 左下
    ];
  };

  // 处理鼠标移动
  const handleMouseMove = (e) => {
    const pos = e.target.getStage().getPointerPosition();
    setMousePos(pos);

    // 状态1:动态绘制矩形
    if (drawingState === 1 && points.length === 1) {
      const corners = calculateRectCorners(points[0], pos);
      setRectCorners(corners);
    }
  };

  // 处理鼠标点击
  const handleMouseClick = (e) => {
    const pos = e.target.getStage().getPointerPosition();

    if (drawingState === 0) {
      // 第一个点
      setPoints([pos]);
      setDrawingState(1);
    } else if (drawingState === 1) {
      // 第二个点,确定矩形
      const newPoints = [...points, pos];
      setPoints(newPoints);
      const corners = calculateRectCorners(points[0], pos);
      setRectCorners(corners);
      setDrawingState(2);
    } else if (drawingState === 2) {
      // 第三个点
      const newPoints = [...points, pos];
      setPoints(newPoints);
      setDrawingState(3);
    } else if (drawingState === 3) {
      // 第四个点,完成立方体
      // 计算第二个矩形的四个角点
      const backCorners = calculateRectCorners(points[2], pos);

      // 保存完成的立方体(包含两个矩形的角点)
      const newCube = {
        frontRect: [...rectCorners],  // 第一个矩形
        backRect: [...backCorners]    // 第二个矩形
      };
      setCompletedCubes([...completedCubes, newCube]);

      // 重置状态
      setPoints([]);
      setRectCorners([]);
      setDrawingState(0);
    }
  };

  // 渲染动态矩形
  const renderDynamicRect = () => {
    if (drawingState === 1 && points.length === 1) {
      const width = Math.abs(mousePos.x - points[0].x);
      const height = Math.abs(mousePos.y - points[0].y);
      const x = Math.min(points[0].x, mousePos.x);
      const y = Math.min(points[0].y, mousePos.y);

      return (
        <Rect
          x={x}
          y={y}
          width={width}
          height={height}
          stroke="blue"
          strokeWidth={2}
          dash={[5, 5]}
          fill="transparent"
        />
      );
    }
    return null;
  };

  // 渲染确定的矩形(只在状态2时显示,状态3时由动态立方体处理)
  const renderFixedRect = () => {
    if (drawingState === 2 && rectCorners.length === 4) {
      const minX = Math.min(...rectCorners.map(p => p.x));
      const maxX = Math.max(...rectCorners.map(p => p.x));
      const minY = Math.min(...rectCorners.map(p => p.y));
      const maxY = Math.max(...rectCorners.map(p => p.y));

      return (
        <Rect
          x={minX}
          y={minY}
          width={maxX - minX}
          height={maxY - minY}
          stroke="black"
          strokeWidth={2}
          fill="rgba(173, 216, 230, 0.3)"
        />
      );
    }
    return null;
  };

  // 渲染虚线连接(第三个点到矩形四个角)
  const renderDashedLines = () => {
    if (drawingState === 2 && rectCorners.length === 4) {
      // 鼠标移动时的虚线(只在状态2时显示)
      return rectCorners.map((corner, index) => (
        <Line
          key={`dynamic-${index}`}
          points={[corner.x, corner.y, mousePos.x, mousePos.y]}
          stroke="gray"
          strokeWidth={1}
          dash={[3, 3]}
        />
      ));
    }
    // 状态3时不显示红色虚线,因为已经在绘制立方体了
    return null;
  };

  // 渲染动态立方体(第三次点击后,鼠标移动时)
  const renderDynamicCube = () => {
    if (drawingState === 3 && points.length >= 3 && rectCorners.length === 4) {
      const point3 = points[2];
      const point4 = mousePos; // 当前鼠标位置作为第四个点

      // 计算第二个矩形的四个角点(基于第三个点和第四个点)
      const backCorners = calculateRectCorners(point3, point4);

      return (
        <>
          {/* 前面的矩形(已确定的第一个矩形) */}
          <Rect
            x={Math.min(...rectCorners.map(p => p.x))}
            y={Math.min(...rectCorners.map(p => p.y))}
            width={Math.max(...rectCorners.map(p => p.x)) - Math.min(...rectCorners.map(p => p.x))}
            height={Math.max(...rectCorners.map(p => p.y)) - Math.min(...rectCorners.map(p => p.y))}
            fill="rgba(173, 216, 230, 0.3)"
            stroke="black"
            strokeWidth={2}
          />

          {/* 后面的矩形(动态生成的第二个矩形) */}
          <Rect
            x={Math.min(...backCorners.map(p => p.x))}
            y={Math.min(...backCorners.map(p => p.y))}
            width={Math.max(...backCorners.map(p => p.x)) - Math.min(...backCorners.map(p => p.x))}
            height={Math.max(...backCorners.map(p => p.y)) - Math.min(...backCorners.map(p => p.y))}
            fill="rgba(255, 255, 0, 0.2)"
            stroke="blue"
            strokeWidth={2}
          />

          {/* 连接前后两个矩形对应角点的线 */}
          {rectCorners.map((corner, index) => (
            <Line
              key={`cube-edge-${index}`}
              points={[corner.x, corner.y, backCorners[index].x, backCorners[index].y]}
              stroke="green"
              strokeWidth={2}
              dash={[3, 3]}
            />
          ))}
        </>
      );
    }
    return null;
  };

  // 渲染完成的立方体
  const renderCompletedCubes = () => {
    return completedCubes.map((cube, cubeIndex) => {
      const { frontRect, backRect } = cube;

      return (
        <React.Fragment key={`cube-${cubeIndex}`}>
          {/* 前面的矩形 */}
          <Rect
            x={Math.min(...frontRect.map(p => p.x))}
            y={Math.min(...frontRect.map(p => p.y))}
            width={Math.max(...frontRect.map(p => p.x)) - Math.min(...frontRect.map(p => p.x))}
            height={Math.max(...frontRect.map(p => p.y)) - Math.min(...frontRect.map(p => p.y))}
            fill="rgba(173, 216, 230, 0.5)"
            stroke="black"
            strokeWidth={3}
          />

          {/* 后面的矩形 */}
          <Rect
            x={Math.min(...backRect.map(p => p.x))}
            y={Math.min(...backRect.map(p => p.y))}
            width={Math.max(...backRect.map(p => p.x)) - Math.min(...backRect.map(p => p.x))}
            height={Math.max(...backRect.map(p => p.y)) - Math.min(...backRect.map(p => p.y))}
            fill="rgba(255, 255, 0, 0.3)"
            stroke="black"
            strokeWidth={3}
          />

          {/* 连接前后两个矩形对应角点的线 */}
          {frontRect.map((corner, index) => (
            <Line
              key={`completed-edge-${cubeIndex}-${index}`}
              points={[corner.x, corner.y, backRect[index].x, backRect[index].y]}
              stroke="black"
              strokeWidth={3}
            />
          ))}
        </React.Fragment>
      );
    });
  };

  // 渲染点击的点
  const renderPoints = () => {
    return points.map((point, index) => (
      <Circle
        key={`point-${index}`}
        x={point.x}
        y={point.y}
        radius={4}
        fill={index === 2 ? "red" : "blue"}
        stroke="black"
        strokeWidth={1}
      />
    ));
  };

  return (
    <div>
      <Stage
        width={800}
        height={600}
        onMouseMove={handleMouseMove}
        onClick={handleMouseClick}
        style={{ border: "1px solid #ccc", cursor: "crosshair" }}
      >
        <Layer>
          {/* 渲染完成的立方体 */}
          {renderCompletedCubes()}

          {/* 渲染当前绘制的矩形 */}
          {renderDynamicRect()}
          {renderFixedRect()}

          {/* 渲染虚线连接 */}
          {renderDashedLines()}

          {/* 渲染动态立方体 */}
          {renderDynamicCube()}

          {/* 渲染点击的点 */}
          {renderPoints()}
        </Layer>
      </Stage>
    </div>
  );
}

扩展可能性

  1. 撤销/重做功能:添加操作历史管理
  2. 立方体编辑:支持选中和修改已绘制的立方体
  3. 导出功能:将绘制结果导出为图片或SVG
  4. 3D 渲染:集成 Three.js 实现真正的3D效果
  5. 颜色自定义:允许用户自定义立方体颜色
  6. 网格对齐:添加网格和对齐功能
相关推荐
风清云淡_A9 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界9 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架
NicolasCage11 小时前
react-typescript学习笔记
javascript·react.js
NicolasCage13 小时前
Icon图标库推荐
vue.js·react.js·icon
Lazy_zheng15 小时前
React架构深度解析:从 Stack 到 Fiber,解决 CPU 和 I/O 瓶颈问题
前端·react.js·前端框架
WildBlue16 小时前
React 遇上原子 CSS:前端开发的超级进化 🚀
前端·react.js
namehu16 小时前
“c is not a function” - 一次由 useEffect 异步函数引发的 React 底层崩溃分析
前端·javascript·react.js
iaku16 小时前
🔥React工程化实践:构建企业级可维护应用架构
前端·react.js·前端框架
晓得迷路了18 小时前
栗子前端技术周刊第 91 期 - 新版 React Compiler 文档、2025 HTML 状态调查、Bun v1.2.19...
前端·javascript·react.js