资讯动态

React + TypeScript + Vite 构建 Bento 网格生成器:从拖拽交互到 Canvas 导出

发布时间:2026/8/22 19:51:34 来源:尧图企业网站定制
1. 项目概述一个开源的Bento风格网格生成器如果你是一名前端开发者或者对UI设计感兴趣最近肯定没少在各种设计社区和产品网站上看到“Bento Grid”的身影。这种源自日本便当盒Bento Box美学、将内容分割成大小不一但排列有序的矩形区块的布局风格因其信息密度高、视觉层次清晰正迅速成为个人作品集、产品展示页甚至仪表盘设计的宠儿。然而手动在代码中调整每个“格子”的位置、大小和样式尤其是在需要响应式适配时往往是个繁琐且重复的体力活。今天要聊的这个项目aruntemme/bento-generator正是为了解决这个痛点而生。它是一个基于React、TypeScript和Vite构建的现代化Web工具核心目标就一个让你能像在Figma或Sketch里拖拽组件一样直观、零代码地创建和定制Bento网格布局并一键导出为图片或可移植的JSON数据。无论是想快速为自己的个人主页搭建一个吸睛的“关于我”板块还是为产品设计一个功能展示墙这个工具都能大幅提升你的效率。它不是一个复杂的全功能设计软件而是一个聚焦于解决“Bento布局创建”这一具体问题的锋利工具非常适合设计师、前端开发者和内容创作者。2. 核心设计思路与技术选型解析2.1 为什么选择React TypeScript Vite这套组合这个项目的技术栈非常典型代表了当前前端工具链的最佳实践。选择React 18是因为其成熟的组件化模型和强大的Hook系统非常适合构建这种以状态网格数据、卡片属性为中心的交互式应用。每一个Bento卡片、编辑面板、工具栏都可以被封装成独立的、可复用的函数式组件状态通过Props或Context清晰传递逻辑通过自定义Hook如useDragAndDrop进行封装代码结构会非常清晰。TypeScript的引入则是为了应对复杂的状态管理。想象一下一个Bento卡片对象需要包含哪些属性位置x, y坐标、尺寸宽高、背景颜色、图片URL、文本内容、字体样式、边框……如果没有类型约束在开发拖拽交换、样式编辑这些功能时很容易因为属性名拼写错误或类型不匹配而引入难以调试的Bug。TypeScript能在编码阶段就捕获这些错误同时为组件Props和工具函数提供清晰的接口定义极大地提升了开发体验和代码的可维护性。至于Vite它取代了传统的Webpack主要优势在于极速的冷启动和热更新HMR。对于这样一个需要频繁修改样式、实时预览效果的设计工具来说开发体验至关重要。Vite基于ES模块的原生支持使得在开发过程中几乎感觉不到编译等待任何代码和样式的改动都能在浏览器中瞬间反映出来这对于保持创作流程的流畅性帮助巨大。2.2 为什么是12x6的网格尺寸与比例背后的考量项目采用了一个固定的12列、6行的网格系统单元格尺寸为80px间隙16px画布整体内边距32px。这个设计并非随意为之而是经过深思熟虑的。首先12列网格是前端UI设计中的一个经典范式例如Bootstrap、Tailwind CSS的栅格系统。12是一个高度可分解的数字能被2、3、4、6整除这意味着你可以轻松地创建出1/2、1/3、1/4、1/6等多种比例的卡片组合布局灵活性极高。同时12列也为未来可能的响应式扩展如在小屏幕上变为6列预留了基础。其次6行和16:9的画布比例最终计算尺寸约为1024x576px是一个关键决策。16:9是当前最主流的宽屏显示比例从笔记本电脑到会议室大屏都广泛采用。将画布锁定在这个比例确保了设计产出物在大多数屏幕和设备上预览时都能保持完整的、无黑边的视觉呈现避免了因比例失调导致的拉伸或裁剪问题。这相当于为你的Bento设计提供了一个“安全创作区”。最后80px的单元格基准尺寸是一个平衡点。它足够大使得1x1的“小方块”卡片也能清晰容纳图标或短文本以它为基准通过组合形成的2x1、1x2、2x2等卡片尺寸176px也具有良好的视觉比例。16px的间隙则符合现代UI设计的“8pt网格系统”原则能创造出舒适、有呼吸感的视觉节奏。实操心得网格系统的可扩展性虽然当前版本是固定网格但源码中的gridUtils.ts将网格逻辑抽象得很好。如果你需要为特定项目定制网格比如10x8只需修改GRID_COLUMNS、GRID_ROWS、CELL_SIZE和GAP这几个常量并确保画布容器的计算尺寸逻辑同步更新即可。这是一个典型的“开闭原则”实践——对扩展开放对修改封闭。2.3 状态管理为什么没有用Redux或Zustand浏览项目结构你会发现它没有引入任何外部的状态管理库如Redux, Zustand, MobX。所有的状态都通过React内置的useState、useReducer和Context API来管理。这是一个非常明智的选择。对于bento-generator这类工具其状态结构虽然嵌套画布上有多个卡片每个卡片有复杂属性但数据流是相对单向和集中的。核心状态可能就是一个cards: Card[]数组存放在最顶层的App组件或一个专门的GridContext中。拖拽、编辑、保存等所有操作最终都归结为对这个数组的增删改查。引入Redux等库会带来额外的概念复杂性Action、Reducer、Selector、Middleware对于中小型项目而言是杀鸡用牛刀。而纯React方案足以优雅地处理状态提升将cards状态和setCards方法通过Props或Context传递给子组件。性能优化使用React.memo包裹BentoCard组件并传入自定义的areEqual比较函数可以避免卡片在未发生变化时的不必要重渲染。逻辑复用将拖拽碰撞检测、本地存储读写等逻辑抽离为自定义Hook如useGriduseStorage保持组件简洁。这种“轻量级”状态管理策略使得项目结构更简单新人上手更容易也减少了打包体积。只有当应用变得极其复杂拥有大量分散的、需要跨组件通信的派生状态时才需要考虑更强大的状态管理方案。3. 核心功能实现与关键技术细节3.1 拖拽与碰撞检测智能网格布局的核心引擎拖拽交互是这个工具的灵魂而其背后的“智能”体现在碰撞检测和自动重排算法上。这部分的逻辑主要集中在src/utils/gridUtils.ts中。1. 坐标转换首先需要将浏览器中的鼠标/触摸事件坐标基于视口转换为网格坐标。这涉及到获取画布元素相对于视口的位置getBoundingClientRect然后减去画布偏移量和内边距再除以单元格尺寸间隙最后取整得到目标单元格的(gridX, gridY)。// 伪代码示例将客户端坐标转换为网格坐标 function clientToGrid(clientX: number, clientY: number, canvasRect: DOMRect): {x: number, y: number} { const relativeX clientX - canvasRect.left - PADDING; const relativeY clientY - canvasRect.top - PADDING; // 考虑间隙的影响计算更精确的网格索引 const gridX Math.floor(relativeX / (CELL_SIZE GAP)); const gridY Math.floor(relativeY / (CELL_SIZE GAP)); // 确保坐标在网格范围内 return { x: Math.max(0, Math.min(gridX, GRID_COLUMNS - 1)), y: Math.max(0, Math.min(gridY, GRID_ROWS - 1)) }; }2. 碰撞检测与有效性验证当用户将一个卡片拖到新位置时系统需要检查边界检查卡片的右边界是否超出网格总列数下边界是否超出总行数占用检查目标区域是否已经被其他卡片占据这里需要遍历所有现有卡片检查其网格位置和尺寸是否与拖拽卡片的目标区域重叠。项目实现了多种智能行为简单交换如果目标位置有一个尺寸完全相同的卡片则两者直接交换位置。这是最直观的操作。空间寻找与重排如果目标位置已被占据且无法直接交换算法会尝试在目标位置附近寻找空闲空间。这可能涉及沿着行或列“推动”一系列卡片类似于整理书架上的书。这个过程在findSpaceForCard或rearrangeGrid这样的函数中实现是算法中最有趣也最复杂的部分。视觉反馈在拖拽过程中实时计算潜在位置的有效性并通过改变拖拽预览的边框颜色如有效为绿色无效为红色给予用户即时反馈。注意事项性能优化拖拽过程中碰撞检测函数会在每一次鼠标移动事件中调用。如果卡片数量很多比如超过20个频繁的遍历计算可能造成卡顿。这里常见的优化手段包括空间分区将网格划分为区块只检查可能与拖拽卡片发生碰撞的区块内的卡片。脏矩形检查只检查拖拽卡片移动轨迹涉及的区域。使用requestAnimationFrame节流限制碰撞检测的执行频率使其与屏幕刷新率同步避免不必要的计算。 从项目描述看它已经应用了requestAnimationFrame进行拖拽节流这是保证流畅体验的关键。3.2 卡片数据模型与样式系统一个Bento卡片在代码中是如何表示的这涉及到src/types.ts中定义的核心接口。// 推测的核心类型定义基于功能描述 interface BentoCard { id: string; // 唯一标识通常使用 nanoid 或 uuid 生成 gridX: number; // 卡片左上角所在的网格列索引 gridY: number; // 卡片左上角所在的网格行索引 width: number; // 卡片占用的网格列数 (1 或 2) height: number; // 卡片占用的网格行数 (1 或 2) // 样式属性 backgroundColor: string; // 十六进制颜色码或 transparent backgroundImage: string | null; // 背景图片URL borderColor: string; borderWidth: number; // 文本属性 textContent: string; textColor: string; fontSize: number; horizontalAlign: left | center | right; verticalAlign: top | center | bottom; textOrientation: horizontal | vertical; }样式应用Tailwind CSS 与动态类名项目使用Tailwind CSS进行样式处理。对于动态样式如背景色、文字颜色通常有两种方式内联样式直接使用style{{ backgroundColor: card.backgroundColor }}。这种方式最直接但会生成大量独立的样式规则可能不利于性能优化。动态类名预定义一组颜色对应的Tailwind类然后根据状态动态选择。例如const bgColorClass bg-${getColorClass(card.backgroundColor)}; return div className{${bgColorClass} ...}.../div;这种方式需要确保所有可能用到的颜色都在Tailwind配置文件中被安全地包含通过safelist选项否则生产构建时可能会被清除。考虑到Bento Generator支持任意自定义颜色它很可能采用了内联样式与静态类名结合的方式布局、间距、边框基础样式用Tailwind静态类而完全自定义的颜色、背景图则用内联样式。编辑面板中的颜色选择器很可能集成了一个类似react-color的库来提供友好的取色体验。3.3 导入/导出与持久化导出为PNG这是通过html2canvas库实现的。原理是将指定的DOM元素这里是整个画布渲染到一个离屏的canvas上然后调用canvas.toDataURL(image/png)生成一个Data URL最后触发浏览器下载。import html2canvas from html2canvas; async function exportToPng(element: HTMLElement, filename: string) { const canvas await html2canvas(element, { scale: 2, // 提高缩放倍数以获得更高清的图片 backgroundColor: null, // 背景透明 useCORS: true, // 如果包含网络图片需要此项 }); const link document.createElement(a); link.download ${filename}.png; link.href canvas.toDataURL(image/png); link.click(); }实操心得导出图片的清晰度问题html2canvas的渲染效果受CSS和浏览器影响较大。为确保导出图片清晰设置scale: 2或更高进行高分倍率渲染。检查画布内所有图片是否已加载完成否则导出时可能是空白。如果使用了网络字体确保字体文件可访问或考虑将字体转换为图片内嵌。复杂的CSS滤镜如blur,backdrop-filter可能无法被正确渲染。导入/导出JSON与本地存储这部分功能在src/utils/storage.ts中。JSON导出很简单就是JSON.stringify(cardsArray)。导入则是读取用户上传的JSON文件解析并验证数据结构后替换当前的cards状态。本地存储使用localStorage键名可能是bento-generator-layouts。存储的是一个对象以布局名称为键卡片数组为值。// 简化示例 const STORAGE_KEY bento-generator-layouts; function saveLayout(name: string, cards: BentoCard[]) { const allLayouts JSON.parse(localStorage.getItem(STORAGE_KEY) || {}); allLayouts[name] cards; localStorage.setItem(STORAGE_KEY, JSON.stringify(allLayouts)); } function loadLayout(name: string): BentoCard[] | null { const allLayouts JSON.parse(localStorage.getItem(STORAGE_KEY) || {}); return allLayouts[name] || null; }注意事项数据迁移与版本控制如果未来卡片的数据结构BentoCard接口发生变化旧版本保存的布局可能无法正确加载。一个健壮的方案是在存储时加入一个version字段并在加载时根据版本号进行数据迁移转换。4. 从零开始开发环境搭建与项目运行实录4.1 环境准备与依赖安装假设你已经在本地机器上准备好了Node.js环境v16或更高版本推荐使用LTS版本并且有一个顺手的代码编辑器如VS Code、WebStorm或本项目关键词中提到的Cursor。接下来我们一步步拉取并运行这个项目。第一步克隆仓库与目录导航打开你的终端命令行工具执行以下命令。这里注意原始README中克隆命令的目录名是bento-gen但仓库名是bento-generator这可能是文档的小笔误。我们以实际仓库名为准。# 克隆项目到本地 git clone https://github.com/aruntemme/bento-generator.git # 进入项目目录 cd bento-generator使用ls或dir命令查看目录你应该能看到package.json、vite.config.ts等文件。第二步安装项目依赖项目使用npm或yarn作为包管理器。通常package-lock.json的存在意味着推荐使用npm。# 使用 npm推荐 npm install # 或者使用 yarn如果你全局安装了yarn yarn install这个过程会读取package.json中的dependencies和devDependencies下载React、TypeScript、Vite、Tailwind CSS等所有必要的库到本地的node_modules文件夹。网络速度会影响耗时请耐心等待。第三步启动开发服务器依赖安装完毕后运行开发命令npm run dev # 或 yarn dev如果一切顺利终端会输出类似以下的信息VITE v5.4.0 ready in 320 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose这表明Vite开发服务器已经启动并在本地的5173端口监听。现在打开你的浏览器访问http://localhost:5173。4.2 初次运行与功能初探浏览器加载完成后你应该能看到一个简洁的界面顶部是工具栏中间是带有网格线的画布区。这就是你的Bento设计工作室了。快速体验流程添加卡片点击工具栏的“Add Card”或类似按钮选择一种尺寸如Square。一个卡片会出现在画布的第一个可用位置。拖拽移动用鼠标按住卡片并拖动你会看到卡片半透明跟随网格线会高亮显示可放置的区域。将其拖到另一个位置释放。编辑内容点击卡片上的“编辑”铅笔图标。右侧或应弹出编辑面板在这里你可以修改文本、更换背景色、调整对齐方式等。尝试将背景改为一种颜色输入一些文字。保存与导出在工具栏找到“Save”按钮输入一个布局名称如“My First Bento”并保存。然后点击“Export”选择“PNG”一张你设计的图片就会下载到本地。这个快速流程验证了从开发环境搭建到核心功能增、删、改、拖、存、出的完整通路。如果你能看到并完成这些操作那么恭喜你的本地开发环境已经完美运行起来了。4.3 可选启用分析功能PostHog项目集成了PostHog用于匿名产品分析但默认是关闭的。如果你是这个项目的维护者或贡献者并且想了解用户如何使用这个工具可以启用它。操作步骤在项目根目录与package.json同级创建一个名为.env.local的文件。这个文件通常被.gitignore排除用于存放本地环境变量。在.env.local文件中填入以下内容# 启用分析功能 VITE_ENABLE_ANALYTICStrue # 替换为你自己的PostHog项目API Key VITE_PUBLIC_POSTHOG_KEYphc_your_project_key_here # 如果你在PostHog中使用的是其他区域如欧盟需要修改此地址 # VITE_PUBLIC_POSTHOG_HOSThttps://eu.i.posthog.com你需要一个PostHog账户和项目。去 PostHog官网 注册并创建一个新项目然后在项目设置中找到你的“Project API Key”。将上述配置中的phc_your_project_key_here替换为真实的Key。重启开发服务器先按CtrlC停止再运行npm run dev。现在当用户使用应用时一些匿名事件如“card_added”、“layout_saved”可能会被发送到你的PostHog面板。这对于改进产品非常有价值。隐私与合规提示如果你打算部署一个公开可用的版本并且启用了分析务必在应用的显著位置例如设置页面或页脚提供隐私政策链接明确告知用户你收集了哪些数据、用于什么目的以及他们如何选择退出。项目自带的PRIVACY.md文件是一个很好的起点你需要根据实际情况进行补充和本地化。5. 深度定制与二次开发指南5.1 修改网格系统与视觉主题你可能希望调整网格的密度、画布的比例或者改变整个工具的主题色以适应你的品牌。大部分配置都可以在几个核心文件中找到。调整网格参数打开src/utils/gridUtils.ts文件通常在文件顶部会找到定义网格的常量。// 示例修改网格为10x8单元格更大 export const GRID_COLUMNS 10; // 原为12 export const GRID_ROWS 8; // 原为6 export const CELL_SIZE 100; // 原为80 (px) export const GAP 20; // 原为16 (px) export const PADDING 40; // 原为32 (px)修改这些值后你需要同步更新画布容器的计算尺寸。这个逻辑可能在GridCanvas.tsx或gridUtils.ts的某个函数中。计算画布总宽高的公式通常是总宽度 PADDING * 2 GRID_COLUMNS * CELL_SIZE (GRID_COLUMNS - 1) * GAP高度计算同理。确保这个计算值被正确应用到画布容器的样式上。修改主题与样式项目使用Tailwind CSS。主题颜色、字体、间距等通常在tailwind.config.js中配置。// tailwind.config.js module.exports { content: [./index.html, ./src/**/*.{js,ts,jsx,tsx}], theme: { extend: { colors: { // 添加或覆盖你的品牌色 primary: #3B82F6, secondary: #10B981, }, fontFamily: { // 更改默认字体 sans: [Inter, system-ui, sans-serif], }, }, }, plugins: [], }修改后需要重启开发服务器才能生效。如果你想彻底改变UI组件的外观需要直接修改相关组件的JSX和Tailwind类名。例如工具栏的背景色可能在Toolbar.tsx中定义为classNamebg-gray-800你可以将其改为bg-primary。5.2 添加新的卡片样式或功能假设你想增加一个“3x2”的超宽卡片尺寸或者为卡片添加“圆角”和“阴影”样式选项。1. 扩展卡片类型定义首先在src/types.ts中更新CardSize类型和BentoCard接口。// 在现有类型旁添加 export type CardSize 1x1 | 2x1 | 1x2 | 2x2 | 3x2; // 新增 3x2 export interface BentoCard { // ... 其他属性 borderRadius?: number; // 新增圆角半径 boxShadow?: string; // 新增阴影样式例如 lg }2. 更新网格工具函数在gridUtils.ts中需要更新与尺寸相关的逻辑。例如getCardDimensions函数需要能处理新的3x2尺寸返回正确的宽度和高度网格单位。碰撞检测函数也需要能处理这种非标准尺寸。3. 更新UI组件BentoCard.tsx在组件的样式计算中加入对新属性borderRadius和boxShadow的处理。可能需要使用内联样式或动态类名。const style { // ... 其他样式 borderRadius: ${card.borderRadius}px, boxShadow: card.boxShadow, // 或者映射到Tailwind类如 shadow-${card.boxShadow} };EditPanel.tsx在编辑面板中增加新的表单控件如一个滑块Slider用于调整borderRadius一个下拉菜单用于选择boxShadow预设。Toolbar.tsx或添加卡片的地方在“添加卡片”的尺寸选项中加入3x2的选项。4. 更新导入/导出逻辑确保storage.ts中的序列化和反序列化函数能正确处理新增的属性。这个过程清晰地展示了如何在一个结构良好的React项目中添加新功能从数据模型TypeScript类型开始到核心逻辑工具函数最后是用户界面组件。遵循这个顺序可以避免遗漏。5.3 集成到其他项目或构建独立组件库你可能希望将Bento Generator的网格和卡片组件抽离出来集成到你自己的React应用中而不是仅仅使用它导出的图片。方案一作为组件库引用高级代码抽象将src/components/下的核心组件BentoCard,GridCanvas以及src/utils/gridUtils.ts、src/types.ts复制到你的目标项目中。状态管理对接原项目内部的状态管理需要被“外置”。你需要创建一个父组件它来维护cards状态和操作函数addCard,moveCard,updateCardStyle然后通过Props或Context传递给这些复用的组件。样式隔离确保Tailwind CSS配置被正确引入或者将组件的样式重构为独立的CSS模块以避免与你主项目的样式冲突。方案二利用导出功能简易如果你只需要在别处“展示”设计好的布局而不需要编辑功能那么利用其JSON导出功能是最简单的。在Bento Generator中设计好布局导出为JSON文件。在你的主项目中创建一个解析和渲染该JSON的组件。这个组件读取JSON数据根据每个卡片的属性位置、尺寸、样式动态生成对应的DOM元素。你只需要实现一个静态的渲染器复杂度远低于完整的交互式编辑器。踩坑记录样式复现的一致性在方案二中最大的挑战是确保渲染出的视觉效果与Bento Generator中完全一致。你需要精确复现其CSS包括网格的基准尺寸CELL_SIZE,GAP,PADDING。卡片内部文本的排版方式行高、字重、对齐方式。可能用到的CSS自定义属性CSS Variables或特定的Tailwind工具类。 最好的办法是直接从Bento Generator的产物中提取出关键CSS规则应用到你的渲染组件上。6. 常见问题排查与性能优化技巧6.1 开发与构建问题速查表问题现象可能原因解决方案npm install失败网络错误网络连接问题或npm源问题1. 检查网络。2. 尝试使用yarn install。3. 切换npm镜像源npm config set registry https://registry.npmmirror.comnpm run dev启动失败端口占用本地5173端口已被其他程序使用1. 终止占用端口的进程。2. 或在vite.config.ts中修改server.port配置。页面空白控制台有React错误依赖版本冲突或TypeScript类型错误1. 删除node_modules和package-lock.json重新npm install。2. 运行npm run typecheck查看具体类型错误。拖拽卡顿时卡片数量多碰撞检测或渲染性能瓶颈1. 确认已使用React.memo优化卡片组件。2. 检查gridUtils.ts中的算法复杂度尝试优化如空间索引。3. 使用Chrome Performance面板分析性能热点。导出PNG图片模糊或有空白html2canvas配置或资源加载问题1. 增加scale参数如设为3。2. 确保导出前所有图片已加载完成可使用Promise.all等待。3. 检查是否有元素使用了position: fixed这可能导致错位。保存的布局重新加载后样式错乱localStorage中数据格式不匹配或版本过旧1. 打开浏览器开发者工具 Application Local Storage检查保存的数据。2. 实现一个数据迁移函数在加载时将旧格式转换为新格式。6.2 生产环境部署注意事项项目README推荐了Netlify、Vercel等平台。以Vercel为例部署流程极其简单代码推送将你的代码推送到GitHub、GitLab或Bitbucket仓库。导入项目在Vercel控制台点击“New Project”导入你的仓库。自动配置Vercel会自动检测到这是一个Vite React项目并应用正确的构建配置构建命令npm run build输出目录dist。环境变量如果你启用了PostHog分析需要在Vercel项目的“Environment Variables”设置中添加你在.env.local里配置的那几个变量VITE_ENABLE_ANALYTICS,VITE_PUBLIC_POSTHOG_KEY等。部署点击部署等待几分钟即可获得一个全球加速的在线链接。部署前检查清单[ ] 运行npm run build在本地成功无错误或警告。[ ] 运行npm run preview预览生产构建测试所有核心功能。[ ] 确认dist目录下的index.html能正确加载所有资源JS、CSS。[ ] 如果使用了自定义域名在Vercel/Netlify中配置好DNS记录。6.3 性能优化深度建议虽然项目已经做了一些优化但随着布局复杂度增加以下措施可以进一步提升体验虚拟滚动如果未来支持非常大的画布或无限网格只渲染视口内的卡片可以极大减少DOM节点和渲染压力。Web Worker将复杂的碰撞检测和布局计算如“自动整理”功能放到Web Worker中避免阻塞主线程的UI渲染。Canvas渲染对于极端复杂的静态布局预览可以考虑用canvas2D API或WebGL来渲染替代DOM。但这会失去CSS的便利性和可访问性需权衡。操作历史Undo/Redo优化实现Undo/Redo时不要保存完整的卡片状态快照内存消耗大而是记录增量操作如“将卡片A从(1,1)移动到(3,2)”。可以使用Immer库来简化不可变状态的管理。按需加载第三方库例如html2canvas只在用户点击“导出PNG”时才动态导入可以减小初始包体积。const handleExport async () { const html2canvas (await import(html2canvas)).default; // ... 使用 html2canvas };这个项目作为一个功能聚焦、代码结构清晰的开源工具不仅直接解决了Bento布局的设计痛点其实现方式也为学习现代ReactTypeScriptVite技术栈、拖拽交互、Canvas操作和工具类产品开发提供了绝佳的范例。无论是直接使用还是借鉴其代码思路进行二次开发都能带来很高的价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价