使用纯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。我们的底层基础设施包括:
- CanvasKit (Skia WebAssembly):作为底层的 2D 图形渲染引擎,负责绘制所有的矩形、文本、图像和 SVG 路径。
- Yoga Layout:Facebook 开源的跨平台 Flexbox 布局引擎。由于 Canvas 本身没有布局概念,我们通过 Yoga 来计算每个元素的坐标和尺寸。
- @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 端也体验到了这种“掌控每一个像素”的快感。