使用纯canvas绘制一个掘金首页

使用纯 Canvas 绘制一个掘金首页

在前端开发中,我们习惯了使用 HTML 和 CSS 来构建用户界面。但你是否想过,如果完全抛弃 DOM 树,使用纯 Canvas 来绘制一个复杂的现代 Web 页面(比如稀土掘金的首页),会是怎样的体验?

在 react-canvas 这个项目中,我们进行了一次硬核的尝试:基于 Skia (CanvasKit) 和 Yoga 布局引擎,使用 React 自定义渲染器从零构建了掘金的首页。

🔗 在线体验地址 :react-canvas-design.vercel.app/#/juejin

💻 GitHub 仓库 :github.com/ouzhou/reac...

技术栈揭秘

要实现这个目标,我们不能使用标准的 react-dom。我们的底层基础设施包括:

  1. CanvasKit (Skia WebAssembly):作为底层的 2D 图形渲染引擎,负责绘制所有的矩形、文本、图像和 SVG 路径。
  2. Yoga Layout:Facebook 开源的跨平台 Flexbox 布局引擎。由于 Canvas 本身没有布局概念,我们通过 Yoga 来计算每个元素的坐标和尺寸。
  3. @react-canvas/react-v2:我们自己实现的 React 渲染器,将 React 组件树映射为底层的渲染节点。

核心实现思路

在纯 Canvas 的世界里,没有 <div>、<span> 或 <img>。一切都是自定义的节点。

1. 基础组件映射

我们将传统的 HTML 标签替换为了 react-canvas 提供的基础组件:

  • <div> -> <View>:作为基础的容器,支持 Flexbox 布局。
  • <span> / <p> -> <Text>:用于文本渲染,底层调用 Skia 的 Paragraph API。
  • <img> -> <Image>:用于渲染网络图片(如掘金的 Logo)。
  • <svg> -> <SvgPath>:用于渲染矢量图标。
  • 滚动区域 -> <ScrollView>:由于 Canvas 没有原生滚动条,我们需要自己处理滚动事件和视口裁剪。

2. 初始化画布与字体

Canvas 绘制中文需要显式加载字体文件,否则会出现乱码(豆腐块)。我们在最外层使用 CanvasProvider 初始化运行时,并加载了思源黑体:

tsx 复制代码
import { CanvasProvider, Canvas, View, Text } from "@react-canvas/react-v2";
import localParagraphFontUrl from "../assets/NotoSansSC-Regular.otf?url";

<CanvasProvider initOptions={{ defaultParagraphFontUrl: localParagraphFontUrl }}>
  {({ isReady, runtime }) => (
    <Canvas
      width={vw}
      height={vh}
      paragraphFontProvider={runtime.paragraphFontProvider}
      defaultParagraphFontFamily={runtime.defaultParagraphFontFamily}
    >
      {/* 页面内容 */}
    </Canvas>
  )}
</CanvasProvider>

3. Flexbox 布局与样式

得益于 Yoga,我们可以像写 React Native 一样使用 Flexbox 布局。所有的样式都是内联的 JS 对象,而不是 CSS 类:

tsx 复制代码
// 掘金顶部导航栏的布局示例
<View
  style={{
    width: vw,
    height: 60,
    backgroundColor: "#ffffff",
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    paddingLeft: 24,
    paddingRight: 24,
  }}
>
  {/* Logo 和 导航项 */}
</View>

4. 交互状态 (Hover)

在 DOM 中,我们通常用 :hover 伪类来处理鼠标悬停状态。在 react-canvas 中,style 属性支持传入一个函数,接收当前的交互状态:

tsx 复制代码
<View
  style={({ hovered }) => ({
    padding: 16,
    backgroundColor: hovered ? "#fafafa" : "#ffffff", // 悬停时改变背景色
    cursor: "pointer",
  })}
>
  <Text>文章标题</Text>
</View>

5. 绘制细节与踩坑:分割线

在传统的 CSS 中,我们可以轻松地写出 border-bottom: 1px solid #eee。但在我们目前的自定义渲染器中,单边边框的支持还在完善中。

为了在 Canvas 中画出完美的 1px 分割线,我们采用了绝对定位的 <View> 元素来模拟:

tsx 复制代码
// 模拟 border-bottom
<View style={{ 
  position: "absolute", 
  bottom: 0, 
  left: 0, 
  right: 0, 
  height: 1, 
  backgroundColor: "#f1f1f1" 
}} />

最终效果

通过组合这些基础能力,我们成功地 1:1 还原了掘金首页的复杂布局,包括:

  • 固定的顶部导航栏(带搜索框和图标)
  • 左侧固定的分类导航侧边栏
  • 中间的文章信息流(包含标题、摘要、作者、时间、点赞数和封面图)
  • 右侧的签到卡片、排行榜和活动 Banner
  • 右下角的悬浮按钮

所有的渲染都在一个 <canvas> 标签内完成!

总结

使用纯 Canvas 绘制复杂的 Web UI 是一次非常有趣的探索。虽然它失去了 DOM 带来的无障碍性(A11y)、SEO 和原生的文本选中能力,但它带来了极致的渲染控制权和跨平台的一致性(同一套代码可以轻易移植到原生 App 甚至桌面端)。

这正是 Flutter、React Native Skia 等技术的核心魅力所在。通过 react-canvas,我们在 Web 端也体验到了这种"掌控每一个像素"的快感。

相关推荐
海码事务所2 小时前
Google Play 新个人开发者账号上架指南:12 人连续 14 天封闭测试怎么做?
前端
田威AI2 小时前
图片内文字翻译的规格:输入输出、保真、自动化、时间与费用
前端·计算机视觉
不可能片场2 小时前
命令行中文变问号 我用环境变量救了场
前端·electron
lerhxx2 小时前
AI 应用中的上下文管理 —— 从"上下文窗口"到"分层压缩"的工程实践
前端·javascript
骑着蜗牛撵大象3273 小时前
多 Agent 分治协作:MapReduce 模式下的并行扇出与结果归并
前端
用户55318297323 小时前
Flutter iOS 热更新深入 Dart VM:读懂函数入口、解释执行与调用桥接
前端
前端snow3 小时前
为什么大厂要用Postgres + mogodb的框架?
前端
运维有小邓13 小时前
2026年主流AD域管理软件有哪些?
前端
Mickey同学3 小时前
提示词注入:AI 时代的「SQL 注入」
前端
deli0073 小时前
GROUP BY 先别想当然:我把 SQL 分组语义做成了沙盘,8 个实验 + 27 条自检全绿
前端