これは、WebKit と Gecko の内部操作についての包括的な入門書であり、イスラエルの開発者 Tali Gahir による広範な研究の結果です。過去数年にわたり、彼女はブラウザの内部に関する公開されているすべてのデータを調査し、Web ブラウザのソース コードを何時間もかけて精査してきました。彼女はこう書きました:
IE が市場シェアの 90% を占めていた時代、私たちはブラウザを「ブラック ボックス」として扱う以外に何もできませんでした。しかし現在、オープンソース ブラウザーが市場シェアの半分以上を占めているため、ベールを剥がして Web ブラウザーの内部で何が起こっているのかを見てみる時期が来ています。そうですね、そこには数百万行の C++ コードが含まれています...
タリーは研究結果を自身の Web サイトで公開しましたが、より多くの人に知ってもらう価値があると感じたため、再構成して公開しましたここ。
Web 開発者として、ブラウザーの内部動作を学ぶことは、より賢明な決定を下し、ベストな開発プラクティスが機能する理由を理解するのに役立ちます。これはかなり長い文書ですが、時間をかけてじっくり読むことをお勧めします。時間を費やす価値は十分にあったと感じていただけると思います。 Paul Irish、Chrome Developer Affairs
Web ブラウザはおそらく最も広く使用されているソフトウェアです。この紹介記事では、それらが舞台裏でどのように機能するかを見ていきます。アドレス バーに google.com と入力してから、ブラウザ画面に Google ホームページが表示されるまでに何が起こるかを学びます。
現在使用されている主流ブラウザは、Internet Explorer、Firefox、Safari、Chrome、Opera の 5 つです。 。この記事では、例としてオープン ソース ブラウザ、つまり Firefox、Chrome、Safari (一部オープン ソース) を使用します。 StatCounter Browser Statistics によると、現在 (2011 年 8 月) Firefox、Safari、Chrome の合計市場シェアは 60% 近くです。オープンソース ブラウザが現在、ブラウザ市場の非常に強固な部分を占めていることがわかります。
ブラウザの主な機能は、サーバーにリクエストを送信し、選択したネットワーク リソースをブラウザ ウィンドウに表示することです。ここで説明するリソースは通常 HTML ドキュメントを指しますが、PDF、画像、またはその他のタイプのリソースである場合もあります。リソースの場所は、ユーザーが URI (Uniform Resource Identifier) を使用して指定します。
ブラウザーが HTML ファイルを解釈して表示する方法は、HTML および CSS の仕様で指定されています。これらの仕様は、Web 標準化団体である W3C (World Wide Web Consortium) によって維持されています。
長年にわたり、ブラウザーは独自の拡張機能を開発する際にこれらの仕様に完全には準拠していないため、Web 開発者に深刻な互換性の問題が発生してきました。現在、ほとんどのブラウザは多かれ少なかれ準拠しています。
ブラウザのユーザー インターフェイスは、次のような多くの要素を相互に共有します。
奇妙なことに、次のようなものがあります。これはブラウザのユーザー インターフェイスの正式な仕様ではなく、長年にわたるベスト プラクティスの自然な進化と相互の模倣です。また、HTML5 ではブラウザーに必須のユーザー インターフェイス要素は定義されていませんが、アドレス バー、ステータス バー、ツールバーなどのいくつかの一般的な要素がリストされています。もちろん、Firefox のダウンロード マネージャーなど、各ブラウザーが独自の機能を備えている場合もあります。
ブラウザの主なコンポーネントは次のとおりです ():
ほとんどのブラウザとは異なり、Chrome ブラウザの各タブがレンダリング エンジン インスタンスに対応していることは注目に値します。各タブは独立したプロセスです。
プレゼンテーション エンジンの役割は…もちろん「レンダリング」、つまり要求されたコンテンツをブラウザ画面に表示することです。
デフォルトでは、レンダリング エンジンは HTML および XML ドキュメントと画像を表示できます。プラグイン (またはブラウザ拡張機能) を通じて、他のタイプのコンテンツを表示することもできます。たとえば、PDF ビューア プラグインを使用すると、PDF ドキュメントを表示できます。ただし、この章では、CSS を使用してフォーマットされた HTML コンテンツと画像を表示するという主な用途に焦点を当てます。
この記事で説明するブラウザ (Firefox、Chrome、Safari) は 2 つのレンダリング エンジンで構築されています。 Firefox は、Mozilla の「自家製」レンダリング エンジンである Gecko を使用しています。 Safari ブラウザと Chrome ブラウザはどちらも WebKit を使用します。
WebKit は、もともと Linux プラットフォームで使用されていたオープンソースのレンダリング エンジンで、後に Mac と Windows をサポートするために Apple によって変更されました。詳細については、webkit.org を参照してください。
レンダリング エンジンは、最初にネットワーク層から要求されたドキュメントのコンテンツを取得します。コンテンツのサイズは通常 8000 ブロックに制限されます。
次に、以下に示す基本プロセスを進めます。
図: レンダリング エンジンの基本プロセス。レンダリング エンジンは HTML ドキュメントの解析を開始し、各タグを「コンテンツ ツリー」上のノードに 1 つずつ変換します。外部 CSS ファイルおよびスタイル要素内のスタイル データも解析されます。 HTML の視覚的な指示を含むこれらのスタイル情報は、別のツリー構造を作成するために使用されます。
レンダリング ツリーには、色やサイズなどの視覚的プロパティを持つ複数の四角形が含まれています。これらの四角形の配置順序が、画面上に表示される順序になります。
プレゼンテーション ツリーが構築された後、「」処理段階に入ります。この段階では、各ノードに画面上で表示される正確な座標を割り当てます。次の段階は、レンダリング エンジンがレンダリング ツリーを横断し、各ノードがユーザー インターフェイス バックエンド層によって描画されることです。
これは段階的なプロセスであることに注意することが重要です。より良いユーザー エクスペリエンスを実現するために、レンダリング エンジンはコンテンツをできるだけ早く画面に表示するよう努めます。レンダリング ツリーの構築とレイアウトの設定を開始する前に、HTML ドキュメント全体が解析されるまで待つ必要はありません。レンダリング エンジンは、コンテンツの一部を解析して表示すると同時に、残りのコンテンツをネットワークから受信して処理し続けます。
図 3 と図 4 からわかるように、WebKit と Gecko で使用される用語は次のとおりです。わずかに異なりますが、全体的なプロセスは基本的に同じです。
Gecko では、視覚的な書式設定要素のツリーを「フレーム ツリー」と呼びます。すべての要素がフレームです。 WebKit で使用される用語は「レンダリング ツリー」であり、これは「レンダリング オブジェクト」で構成されます。要素の配置について、WebKit では「レイアウト」という用語が使用されますが、Gecko ではそれを「リフロー」と呼びます。 WebKit では、DOM ノードと視覚情報を接続してレンダリング ツリーを作成するプロセスに対して「追加」という用語が使用されます。セマンティックではない微妙な違いの 1 つは、Gecko には DOM 要素を生成するための HTML と DOM ツリーの間に「コンテンツ スロット」と呼ばれるレイヤーがあることです。プロセスの各部分を 1 つずつ説明します。
解析はレンダリング エンジンの非常に重要な部分であるため、さらに詳しく説明します。まず、解析を紹介します。
ドキュメントの解析とは、ドキュメントをコードで理解して使用できる意味のある構造に変換することを意味します。解析の結果は通常、文書構造を表すノード ツリーであり、これは解析ツリーまたは構文ツリーと呼ばれます。
例 - 式 2 + 3 - 1 を解析すると、次のツリーが返されます。
図: 数学式ツリー ノード解析は、文書に続く文法規則に基づいて行われます (文書が書かれている言語または形式)。解析できるすべての形式は、特定の文法 (語彙と文法規則で構成される) に対応している必要があります。これをこう呼びます。人間の言語はそのような言語ではないため、従来の解析技術を使用して解析することはできません。
解析プロセスは、字句分析と構文分析の 2 つのサブプロセスに分割できます。
字句解析は、入力を多数のトークンに分割するプロセスです。トークンは言語の単語であり、コンテンツを構成する単位です。人間の言語では、言語辞書の単語に相当します。
文法分析は、言語の文法規則を適用するプロセスです。
パーサーは通常、解析作業を次の 2 つのコンポーネントに分割します: 字句解析器 (トークン ジェネレーターとも呼ばれる)。入力コンテンツを有効なトークンに分解します。 パーサー は、言語の文法規則に従って文書の構造を分析し、解析ツリーを構築する役割を果たします。字句アナライザーは、スペースや改行などの無関係な文字を分離する方法を知っています。
図: ソース文書から解析ツリーまで解析は反復プロセスです。通常、パーサーはレクサーに新しいトークンを要求し、それを何らかの文法規則と照合しようとします。一致するルールが見つかった場合、パーサーはそのトークンに対応するノードを解析ツリーに追加し、次のトークンの要求に進みます。
一致するルールがない場合、パーサーはトークンを内部に保存し、内部に保存されているすべてのトークンに一致するルールが見つかるまでトークンの要求を続けます。一致するルールが見つからない場合、パーサーは例外を発生させます。これは、ドキュメントが無効であり、構文エラーが含まれていることを意味します。
多くの場合、解析ツリーはまだ最終製品ではありません。解析は通常、入力ドキュメントを別の形式に変換する翻訳中に使用されます。コンパイルはその一例です。コンパイラは、まずソース コードを解析ツリーに解析し、次に解析ツリーをマシン コード ドキュメントに変換することによって、ソース コードをマシン コードにコンパイルできます。
図: コンパイル プロセス図 5 では、数式を使用して解析ツリーを構築します。ここで、解析プロセスを示すために単純な数学言語を定義してみましょう。
語彙: 整数、プラス記号、マイナス記号を含む言語を使用します。
構文:
2 + 3 - 1 を分析しましょう。
文法規則に一致する最初の部分文字列は 2 で、これは文法規則 5 に従った項目です。構文ルールに一致する 2 番目の部分文字列は 2 + 3 で、これはルール 3 (用語の後に演算子、その後に用語) に従った式です。次の一致は入力の終わりに達しました。 2 + 3 - 1 は式です。2 + 3 が項であることはすでにわかっており、「項の後に演算子、次に項」のルールに従います。 2++ はどのルールにも一致しないため、無効な入力です。
語彙は通常、正規表現で表されます。
たとえば、サンプル言語は次のように定義できます。
INTEGER :0|[1-9][0-9]*PLUS : +MINUS: -
ご覧のとおり、ここでは正規表現を使用して整数の定義が与えられています。
文法は通常、BNF と呼ばれる形式を使用して定義されます。この例の言語は次のように定義できます。
expression := term operation termoperation := PLUS | MINUSterm := INTEGER | expression
前に、言語の文法が文脈自由文法であれば、通常のパーサーで解析できると言いました。文脈自由文法の直感的な定義は、BNF 形式で完全に表現できる文法です。正式な定義については、文脈自由文法に関するウィキペディアの記事を参照してください。
パーサーには、トップダウン パーサーとボトムアップ パーサーの 2 つの基本的な種類があります。直感的には、トップダウン パーサーは文法の高レベル構造から開始し、そこで一致する構造を見つけようとします。ボトムアップ パーサーは、低レベルのルールから開始し、高レベルのルールが満たされるまで、入力コンテンツを徐々に文法ルールに変換します。
両方のパーサーがこの例をどのように解析するかを見てみましょう:
トップダウン パーサーは高レベルのルールから開始します。最初に 2 + 3 を式として識別し、次に 2 + 3 を識別します。 - 式として 1 (式を識別するプロセスには他のルールの照合が含まれますが、開始点は最上位のルールです)。
ボトムアップ パーサーは入力コンテンツをスキャンし、一致するルールを見つけて、一致する入力コンテンツをルールで置き換えます。このように入力内容が終わるまで置換を続けます。部分的に一致した式はパーサーのスタックに保存されます。
堆栈 | 输入 |
---|---|
2 + 3 - 1 | |
项 | + 3 - 1 |
项运算 | 3 - 1 |
表达式 | - 1 |
表达式运算符 | 1 |
表达式 |
这种自下而上的解析器称为移位归约解析器,因为输入在向右移位(设想有一个指针从输入内容的开头移动到结尾),并且逐渐归约到语法规则上。
有一些工具可以帮助您生成解析器,它们称为解析器生成器。您只要向其提供您所用语言的语法(词汇和语法规则),它就会生成相应的解析器。创建解析器需要对解析有深刻理解,而人工创建并优化解析器并不是一件容易的事情,所以解析器生成器是非常实用的。
WebKit 使用了两种非常有名的解析器生成器:用于创建词法分析器的 Flex 以及用于创建解析器的 Bison (您也可能遇到 Lex 和 Yacc 这样的别名)。Flex 的输入是包含标记的正则表达式定义的文件。Bison 的输入是采用 BNF 格式的语言语法规则。
HTML 解析器的任务是将 HTML 标记解析成解析树。
HTML 的词汇和语法在 W3C 组织创建的中进行了定义。当前的版本是 HTML4,HTML5 正在处理过程中。
正如我们在解析过程的简介中已经了解到的,语法可以用 BNF 等格式进行正式定义。
很遗憾,所有的常规解析器都不适用于 HTML(我并不是开玩笑,它们可以用于解析 CSS 和 JavaScript)。HTML 并不能很容易地用解析器所需的与上下文无关的语法来定义。
有一种可以定义 HTML 的正规格式:DTD(Document Type Definition,文档类型定义),但它不是与上下文无关的语法。
这初看起来很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一个 XML 变体 (XHTML),那么有什么大的区别呢?
区别在于 HTML 的处理更为“宽容”,它允许您省略某些隐式添加的标记,有时还能省略一些起始或者结束标记等等。和 XML 严格的语法不同,HTML 整体来看是一种“软性”的语法。
显然,这种看上去细微的差别实际上却带来了巨大的影响。一方面,这是 HTML 如此流行的原因:它能包容您的错误,简化网络开发。另一方面,这使得它很难编写正式的语法。概括地说,HTML 无法很容易地通过常规解析器解析(因为它的语法不是与上下文无关的语法),也无法通过 XML 解析器来解析。
HTML 的定义采用了 DTD 格式。此格式可用于定义 SGML 族的语言。它包括所有允许使用的元素及其属性和层次结构的定义。如上文所述,HTML DTD 无法构成与上下文无关的语法。
DTD 存在一些变体。严格模式完全遵守 HTML 规范,而其他模式可支持以前的浏览器所使用的标记。这样做的目的是确保向下兼容一些早期版本的内容。最新的严格模式 DTD 可以在这里找到: www.w3.org/TR/html4/strict.dtd
解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。
解析树的根节点是“ Document ”对象。
DOM 与标记之间几乎是一一对应的关系。比如下面这段标记:
<html> <body> <p> Hello World </p> <div> <img src="example.png"/></div> </body></html>
可翻译成如下的 DOM 树:
图 :示例标记的 DOM 树和 HTML 一样,DOM 也是由 W3C 组织指定的。请参见 www.w3.org/DOM/DOMTR 。这是关于文档操作的通用规范。其中一个特定模块描述针对 HTML 的元素。HTML 的定义可以在这里找到: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html 。
我所说的树包含 DOM 节点,指的是树是由实现了某个 DOM 接口的元素构成的。浏览器在具体的实现中会有一些供内部使用的其他属性。
我们在之前章节已经说过,HTML 无法用常规的自上而下或自下而上的解析器进行解析。
原因在于:
由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML。
HTML5 规范详细地描述了解析算法 。此算法由两个阶段组成:标记化和树构建。
标记化是词法分析过程,将输入内容解析成多个标记。HTML 标记包括起始标记、结束标记、属性名称和属性值。
标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。
图 :HTML 解析流程(摘自 HTML5 规范)该算法的输出结果是 HTML 标记。该算法使用状态机来表示。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即使接收的字符相同,对于下一个正确的状态也会产生不同的结果,具体取决于当前的状态。该算法相当复杂,无法在此详述,所以我们通过一个简单的示例来帮助大家理解其原理。
基本示例 - 将下面的 HTML 代码标记化:
<html> <body> Hello world </body></html>
初始状态是数据状态。遇到字符 < 时,状态更改为 “标记打开状态” 。接收一个 a-z 字符会创建“起始标记”,状态更改为 “标记名称状态” 。这个状态会一直保持到接收 > 字符。在此期间接收的每个字符都会附加到新的标记名称上。在本例中,我们创建的标记是 html 标记。
遇到 > 标记时,会发送当前的标记,状态改回 “数据状态” 。
标记也会进行同样的处理。目前 html 和 body 标记均已发出。现在我们回到 “数据状态” 。接收到 Hello world 中的 H 字符时,将创建并发送字符标记,直到接收 中的 < 。我们将为 Hello world 中的每个字符都发送一个字符标记。现在我们回到 “标记打开状态” 。接收下一个输入字符 / 时,会创建 end tag token 并改为 “标记名称状态” 。我们会再次保持这个状态,直到接收 > 。然后将发送新的标记,并回到 “数据状态” 。