阅读视图

发现新文章,点击刷新页面。

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

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

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

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

🔗 在线体验地址react-canvas-design.vercel.app/#/juejin
💻 GitHub 仓库github.com/ouzhou/reac…

screenshot-20260414-161131.png

技术栈揭秘

要实现这个目标,我们不能使用标准的 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 初始化运行时,并加载了思源黑体:

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 类:

// 掘金顶部导航栏的布局示例
<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 属性支持传入一个函数,接收当前的交互状态:

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

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

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

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

// 模拟 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 端也体验到了这种“掌控每一个像素”的快感。

❌