让我们构建一个浏览器渲染引擎吧-1

注:本系列博文译自 Matt Brubeck 的博客

我正在构建一个玩具性的 HTML 渲染引擎 robinson,当然你也可以。 本篇是这一系列文章的第一篇:

整个系列用自写的代码来展示怎么构建一个 HTML 渲染引擎。 首先解释一下编写这一系列博客的原因。

渲染引擎是什么?

技术上说,浏览器引擎是网页浏览器的一个组成部分, 它在后台从互联网抓取网页,转换网页内容,展现为我们可以阅读、观看、耳听的形式。 Blink,Gecko, WebKit 和 Trident 都是浏览器引擎。 与它相对的是浏览器的用户界面(UI) -- 标签页、工具栏、菜单等 -- 被称为 chrome。 Firefox 和 SeaMonkey 浏览器都是基于 Gecko 引擎的,但是它们却拥有不同的界面。

一个浏览器引擎有很多子部件: HTTP 客户端,HTML 语法解析器,CSS 语法解析器,JavaScript 引擎(它自身又由语法解析器,解释器和编译器组成),等等。 而渲染引擎通常是用来解析 HTML&CSS 网页并将其展现在显示器上的子部件。

为何要写一个玩具性渲染引擎?

一个包含所有功能的浏览器引擎是非常复杂的。 Blink,Gecko,WebKit 这些引擎都有上百万行代码。 即使像 Servo 或 WeasyPrint 这些新出现、较简单的渲染引擎也有上万行代码之多。 对于一个新手来说,理解这些都不是一件简单的事情。

当学习复杂软件相关课程时,比如编译器或操作系统,你通常会制作、修改一个玩具性的编译器或内核。 这是一种学习的简单方法, 它可能从来不会被编写者之外的人运行, 但是制作一个玩具性系统可以帮你深刻的理解它们到底是如何运行的。 即使你从来不会构建一个真正的编译器或内核,但理解它们如何工作也可以帮你更好的运用它们。

因此,如果你想成为一名浏览器的开发者,或想理解浏览器后面发生了什么,为什么不自己实现一个呢? 与玩具性编译器一样,玩具性渲染引擎只是实现了 HTML&CSS 的一小部分, 它并不会替代日常中用到的浏览器引擎, 但是足以向你展示渲染一段简单的 HTML 文档的基本步骤。

闲暇之余

这时,我相信你已经心动了。 如果你已经有了一些编程经验并且了解 HTML&CSS 相关知识,本系列博客将会很容易被理解。 如果你刚开始学习编程并且遇到不懂的地方,尽管放心提问,我将把博文改写的更容易理解。

开始之前,须做一些选择:

编程语言

你可以用任何编程语言来编写玩具性渲染引擎, 尽管用你喜欢的编程语言吧,如果觉得好玩,完全可以以此来学习一门新编程语言。

如果想为 Gecko 或 WebKit 贡献代码,你可能需要用到 C++,因为这两个引擎都是基于 C++ 的。 使用 C++ 编写玩具性引擎,你可以很容易的比较它们。

我自己的玩具性引擎是用 Rust 语言编写的。 我是 Mozilla 公司 Servo 团队的一员,因此我非常喜欢这个语言。 另外,制作这个玩具性引擎的目的之一就是为了更好的理解 Servo 实现。 robinson 有时会利用 Servo 中的一些数据结构或代码的简化版。

功能库以及简化模型

这样的一个练习,你必须决定:是否从头到尾自己实现,还是借(调)用别人的代码来实现? 我建议实现那些你真正想理解的部分,不必羞于利用功能库来实现其他部分, 学习怎样利用功能库本身也是一种训练。

我写 robinson 不仅是为了自己,也是用来作为本系列博客的示例代码和练习之用。 总之,我想让 robinson 尽可能的小巧和自给自足。 目前为止,它只依赖于 Rust 的标准库,当然这并不是铁则, 以后我可能用到一个图形库,而不是自己写出一个来。

另一种避免多写代码的方式就是剔除不必要的功能。 例如,robinson 并没有网络相关的代码,它只能读取本地文件。 一个玩具性的程序最好忽略那些你觉得不必要的功能。 在讲解的过程中,我会指出那些简化之处,你可以直接跳到感兴趣的实现部分, 当然,你也可以在其间添加一些其他功能。

第一步:The DOM

准备好了吗?我们以 DOM 的数据结构开始讲解,让我们看看 robinson 的 dom 模块。

DOM 是一个节点树,一个结点有零个或多个子结点。(它也可以有其他属性和方法,这里我们忽略了)

struct Node {
    // data common to all nodes:
    children: Vec<Node>,

    // data specific to each node type:
    node_type: NodeType,
}

有多种结点类型,现在我们忽略大多数,这里只说 element 和 text 类型结点。 其他语言中,通常利用继承 Node 类型来实现它们,但是在 Rust 中,我们用 enum:

enum NodeType {
    Text(String),
    Element(ElementData),
}

一个 element 包含 tag 名和任意数量的属性,属性可以存储在一个 name-value 的 map 容器中。 robinson 不支持符号空间,因此它只是将 tag 名和属性名存储为简单的字符串形式。

struct ElementData {
    tag_name: String,
    attributes: AttrMap,
}

type AttrMap = HashMap<String, String>;

最后,用来生成结点的构造函数:

fn text(data: String) -> Node {
    Node { children: Vec::new(), node_type: NodeType::Text(data) }
}

fn elem(name: String, attrs: AttrMap, children: Vec<Node>) -> Node {
    Node {
        children: children,
        node_type: NodeType::Element(ElementData {
            tag_name: name,
            attributes: attrs,
        })
    }
}

就这些! 一个完整的 DOM 实现会有许多数据类型和方法,但这些就足够我们实现玩具了。

练习

这些只是业余推荐的训练,你可以做那些感兴趣的。

  • 用你选择的语言开始实现 DOM 的 text 结点和 element 结点。
  • 安装最新版 Rust,下载并构建 robinson。打开 dom.rs 文件,扩展 NodeType 类型, 使之包含 comment 结点。
  • 写一段代码美观输出 DOM 结点。

下一篇中,我们将会实现一个解析器来将 HTML 文本转换成 DOM 结点。

引用

想要了解更多有关浏览器引擎内部实现的信息, 请看 Tali Garsiel 极好的介绍:How Browsers Work

如果想要更多的示例代码,下面是一些较小的开源网页渲染引擎。 尽管都数倍于 robinson,但是相对于 Gecko 和 WebKit 来说,它们仍然是小菜一叠。 WebWhirr 只有 2000 多行,是我认为仅有的另一个玩具引擎。

为了灵感和引用,你会发现它们非常有用。 如果你还知道其他一些类似项目 -- 或者你自己写的 -- 请告诉我!