<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          讓我們來構(gòu)建一個瀏覽器引擎吧(建議收藏)

          共 7981字,需瀏覽 16分鐘

           ·

          2021-01-24 09:59


          來源:https://segmentfault.com/a/1190000038859456

          DevUI是一支兼具設(shè)計視角和工程視角的團隊,服務(wù)于華為云DevCloud平臺和華為內(nèi)部數(shù)個中后臺系統(tǒng),服務(wù)于設(shè)計師和前端工程師。
          官方網(wǎng)站:devui.design
          Ng組件庫:ng-devui(歡迎Star)
          官方交流:添加DevUI小助手(devui-official)
          DevUIHelper插件:DevUIHelper-LSP(歡迎Star)

          引言

          前端有一個經(jīng)典的面試題:在瀏覽器地址欄輸入URL到最終呈現(xiàn)出頁面,中間發(fā)生了什么?

          中間有一個過程是獲取后臺返回的HTML文本,瀏覽器渲染引擎將其解析成DOM樹,并將HTML中的CSS解析成樣式樹,然后將DOM樹和樣式樹合并成布局樹,并最終由繪圖程序繪制到瀏覽器畫板上。

          本文通過親自動手實踐,教你一步一步實現(xiàn)一個迷你版瀏覽器引擎,進而深入理解渲染引擎的工作原理,干貨滿滿。

          主要分成七個部分:

          • 第一部分:開始
          • 第二部分:HTML
          • 第三部分:CSS
          • 第四部分:樣式
          • 第五部分:盒子
          • 第六部分:塊布局
          • 第七部分:繪制 101

          原文寫于2014.8.8。

          原文地址:https://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html

          以下是正文:

          第一部分:開始

          我正在構(gòu)建一個“玩具”渲染引擎,我認為你也應(yīng)該這樣做。這是一系列文章中的第一篇。

          完整的系列文章將描述我編寫的代碼,并向你展示如何編寫自己的代碼。但首先,讓我解釋一下原因。

          你在造什么?

          讓我們談?wù)勑g(shù)語。瀏覽器引擎是web瀏覽器的一部分,它在“底層”工作,從Internet上獲取網(wǎng)頁,并將其內(nèi)容轉(zhuǎn)換成可以閱讀、觀看、聽等形式。Blink、Gecko、WebKit和Trident都是瀏覽器引擎。相比之下,瀏覽器本身的用戶界面(標簽、工具欄、菜單等)被稱為chrome。Firefox和SeaMonkey是兩個瀏覽器,使用不同的chrome,但使用相同的Gecko引擎。

          瀏覽器引擎包括許多子組件:HTTP客戶端、HTML解析器、CSS解析器、JavaScript引擎(本身由解析器、解釋器和編譯器組成)等等。那些涉及解析HTML和CSS等web格式,并將其轉(zhuǎn)換成你在屏幕上看到的內(nèi)容的組件,有時被稱為布局引擎或渲染引擎。

          為什么是一個“玩具”渲染引擎?

          一個功能齊全的瀏覽器引擎非常復雜。Blink,GeckoWebKit,它們每一個都有數(shù)百萬行代碼。更年輕、更簡單的渲染引擎,如ServoWeasyPrint,也有成千上萬行。這對一個新手來說是不容易理解的!

          說到非常復雜的軟件:如果你參加了編譯器或操作系統(tǒng)的課程,在某些時候你可能會創(chuàng)建或修改一個“玩具”編譯器或內(nèi)核。這是一個為學習而設(shè)計的簡單模型;它可能永遠不會由作者以外的任何人管理。但是

          制作一個玩具系統(tǒng)對于了解真實的東西是如何工作的是一個有用的工具。

          即使你從未構(gòu)建過真實的編譯器或內(nèi)核,

          了解它們的工作方式也可以幫助你在編寫自己的程序時更好地使用它們。

          因此,如果你想成為一名瀏覽器開發(fā)人員,或者只是想了解瀏覽器引擎內(nèi)部發(fā)生了什么,為什么不構(gòu)建一個玩具呢?就像實現(xiàn)“真正的”編程語言子集的玩具編譯器一樣,玩具渲染引擎也可以實現(xiàn)HTML和CSS的一小部分。它不會取代日常瀏覽器中的引擎,但應(yīng)該能夠說明呈現(xiàn)一個簡單HTML文檔所需的基本步驟。

          在家試試吧。

          我希望我已經(jīng)說服你去試一試了。如果你已經(jīng)有一些扎實的編程經(jīng)驗并了解一些高級HTML和CSS概念,那么學習本系列將會非常容易。然而,如果你剛剛開始學習這些東西,或者遇到你不理解的東西,請隨意問問題,我會盡量讓它更清楚。

          在你開始之前,我想告訴你一些你可以做的選擇:

          關(guān)于編程語言

          你可以用任何編程語言構(gòu)建一個玩具式的布局引擎,真的!用一門你了解和喜愛的語言吧。如果這聽起來很有趣,你也可以

          以此為借口學習一門新語言。

          如果你想開始為主要的瀏覽器引擎(如Gecko或WebKit)做貢獻,你可能希望使用C++,因為C++是這些引擎中使用的主要語言,使用C++可以更容易地將你的代碼與它們的代碼進行比較。

          我自己的玩具項目,robinson,是用Rust寫的。我是Mozilla的Servo團隊的一員,所以我非常喜歡Rust編程。此外,我創(chuàng)建這個項目的目標之一是了解更多的Servo的實現(xiàn)。Robinson有時會使用Servo的簡化版本的數(shù)據(jù)結(jié)構(gòu)和代碼。

          關(guān)于庫和捷徑

          在這樣的學習練習中,你必須決定是使用別人的代碼,還是從頭編寫自己的代碼。我的建議是

          為你真正想要理解的部分編寫你自己的代碼,但是不要羞于為其他的部分使用庫。

          學習如何使用特定的庫本身就是一項有價值的練習。

          我寫robinson不僅僅是為了我自己,也是為了作為這些文章和練習的示例代碼。出于這樣或那樣的原因,我希望它盡可能地小巧和獨立。到目前為止,除了Rust標準庫之外,我沒有使用任何外部代碼。(這也避免了使用同一版本的Rust來構(gòu)建多個依賴的小麻煩,而該語言仍在開發(fā)中。)不過,這個規(guī)則并不是一成不變的。例如,我以后可能決定使用圖形庫,而不是編寫自己的低級繪圖代碼。

          另一種避免編寫代碼的方法是省略一些內(nèi)容。例如,robinson還沒有網(wǎng)絡(luò)代碼;它只能讀取本地文件。在一個玩具程序中,如果你想跳過一些東西,你可以跳過。我將在討論過程中指出類似的潛在捷徑,這樣你就可以繞過不感興趣的步驟,直接跳到好的內(nèi)容。如果你改變了主意,你可以在以后再補上空白。

          第一步:DOM

          準備好寫代碼了嗎?我們將從一些小的東西開始:DOM的數(shù)據(jù)結(jié)構(gòu)。讓我們看看robinson的dom模塊。

          DOM是一個節(jié)點樹。一個節(jié)點有零個或多個子節(jié)點。(它還有其他各種屬性和方法,但我們現(xiàn)在可以忽略其中的大部分。)

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

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

          有多種節(jié)點類型,但現(xiàn)在我們將忽略其中的大多數(shù),并將節(jié)點定義為元素節(jié)點文本節(jié)點。在具有繼承的語言中,這些是Node的子類型。在Rust中,它們可以是枚舉enum(Rust的關(guān)鍵字用于“tagged union”或“sum type”):

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

          元素包括一個標記名稱和任意數(shù)量的屬性,它們可以存儲為從名稱到值的映射。Robinson不支持名稱空間,所以它只將標記和屬性名稱存儲為簡單的字符串。

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

          type AttrMap = HashMap;

          最后,一些構(gòu)造函數(shù)使創(chuàng)建新節(jié)點變得容易:

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

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

          這是它!一個成熟的DOM實現(xiàn)將包含更多的數(shù)據(jù)和幾十個方法,但這就是我們開始所需要的。

          練習

          這些只是一些在家可以遵循的建議。做你感興趣的練習,跳過不感興趣的。

          1. 用你選擇的語言啟動一個新程序,并編寫代碼來表示DOM文本節(jié)點和元素樹。
          2. 安裝最新版本的Rust,然后下載并構(gòu)建robinson。打開dom.rs和擴展NodeType以包含其他類型,如注釋節(jié)點。
          3. 編寫代碼來美化DOM節(jié)點樹。

          在下一篇文章中,我們將添加一個將HTML源代碼轉(zhuǎn)換為這些DOM節(jié)點樹的解析器。

          參考文獻

          有關(guān)瀏覽器引擎內(nèi)部結(jié)構(gòu)的更多詳細信息,請參閱Tali Garsiel非常精彩的瀏覽器的工作原理及其到更多資源的鏈接。

          例如代碼,這里有一個“小型”開源web呈現(xiàn)引擎的簡短列表。它們大多比robinson大很多倍,但仍然比Gecko或WebKit小得多。只有2000行代碼的WebWhirr是唯一一個我稱之為“玩具”引擎的引擎。

          • CSSBox (Java)
          • Cocktail (Haxe)
          • gngr (Java)
          • litehtml (c++)
          • LURE (Lua)
          • NetSurf (C)
          • Servo (Rust)
          • Simple San Simon (Haskell)
          • WeasyPrint (Python)
          • WebWhirr (C++)

          你可能會發(fā)現(xiàn)這些有用的靈感或參考。如果你知道任何其他類似的項目,或者如果你開始自己的項目,請讓我知道!

          第二部分:HTML

          這是構(gòu)建一個玩具瀏覽器渲染引擎系列文章的第二篇。

          本文是關(guān)于解析HTML源代碼以生成DOM節(jié)點樹的。解析是一個很吸引人的話題,但是我沒有足夠的時間或?qū)I(yè)知識來介紹它。你可以從任何關(guān)于編譯器的優(yōu)秀課程或書籍中獲得關(guān)于解析的詳細介紹?;蛘咄ㄟ^閱讀與你選擇的編程語言一起工作的解析器生成器的文檔來獲得動手操作的開始。

          HTML有自己獨特的解析算法。與大多數(shù)編程語言和文件格式的解析器不同,HTML解析算法不會拒絕無效的輸入。相反,它包含了特定的錯誤處理指令,因此web瀏覽器可以就如何顯示每個web頁面達成一致,即使是那些不符合語法規(guī)則的頁面。Web瀏覽器必須做到這一點才能使用:因為不符合標準的HTML在Web早期就得到了支持,所以現(xiàn)在大部分現(xiàn)有Web頁面都在使用它。

          簡單的HTML方言

          我甚至沒有嘗試實現(xiàn)標準的HTML解析算法。相反,我為HTML語法的一小部分編寫了一個基本解析器。我的解析器可以處理這樣的簡單頁面:



          Title



          Hello world!





          允許使用以下語法:

          • 閉合的標簽:

          • 帶引號的屬性:id="main"
          • 文本節(jié)點:world

          其他所有內(nèi)容都不支持,包括:

          • 評論
          • Doctype聲明
          • 轉(zhuǎn)義字符(如&)和CDATA節(jié)
          • 自結(jié)束標簽:

            沒有結(jié)束標簽
          • 錯誤處理(例如未閉合或不正確嵌套的標簽)
          • 名稱空間和其他XHTML語法:
          • 字符編碼檢測

          在這個項目的每個階段,我都或多或少地編寫了支持后面階段所需的最小代碼。但是如果你想學習更多的解析理論和工具,你可以在你自己的項目中更加雄心勃勃!

          示例代碼

          接下來,讓我們回顧一下我的HTML解析器,記住這只是一種方法(而且可能不是最好的方法)。它的結(jié)構(gòu)松散地基于Servo的cssparser庫中的tokenizer模塊。它沒有真正的錯誤處理;在大多數(shù)情況下,它只是在遇到意外的語法時中止。代碼是用Rust語言寫的,但我希望它對于使用類似語言(如Java、C++或C#)的人來說具有相當?shù)目勺x性。它使用了第一部分中的DOM數(shù)據(jù)結(jié)構(gòu)。

          解析器將其輸入字符串和當前位置存儲在字符串中。位置是我們還沒有處理的下一個字符的索引。

          struct Parser {
          pos: usize, // "usize" is an unsigned integer, similar to "size_t" in C
          input: String,
          }

          我們可以用它來實現(xiàn)一些簡單的方法來窺視輸入中的下一個字符:

          impl Parser {
          // Read the current character without consuming it.
          fn next_char(&self) -> char {
          self.input[self.pos..].chars().next().unwrap()
          }

          // Do the next characters start with the given string?
          fn starts_with(&self, s: &str) -> bool {
          self.input[self.pos ..].starts_with(s)
          }

          // Return true if all input is consumed.
          fn eof(&self) -> bool {
          self.pos >= self.input.len()
          }

          // ...
          }

          Rust字符串存儲為UTF-8字節(jié)數(shù)組。要進入下一個字符,我們不能只前進一個字節(jié)。相反,我們使用char_indices來正確處理多字節(jié)字符。(如果我們的字符串使用固定寬度的字符,我們可以只將pos加1。)

          // Return the current character, and advance self.pos to the next character.
          fn consume_char(&mut self) -> char {
          let mut iter = self.input[self.pos..].char_indices();
          let (_, cur_char) = iter.next().unwrap();
          let (next_pos, _) = iter.next().unwrap_or((1, ' '));
          self.pos += next_pos;
          return cur_char;
          }

          通常我們想要使用一個連續(xù)的字符串。consume_while方法使用滿足給定條件的字符,并將它們作為字符串返回。這個方法的參數(shù)是一個函數(shù),它接受一個char并返回一個bool值。

          // Consume characters until `test` returns false.
          fn consume_while(&mut self, test: F) -> String
          where F: Fn(char) -> bool {
          let mut result = String::new();
          while !self.eof() && test(self.next_char()) {
          result.push(self.consume_char());
          }
          return result;
          }

          我們可以使用它來忽略空格字符序列,或者使用字母數(shù)字字符串:

          // Consume and discard zero or more whitespace characters.
          fn consume_whitespace(&mut self) {
          self.consume_while(CharExt::is_whitespace);
          }

          // Parse a tag or attribute name.
          fn parse_tag_name(&mut self) -> String {
          self.consume_while(|c| match c {
          'a'...'z' | 'A'...'Z' | '0'...'9' => true,
          _ => false
          })
          }

          現(xiàn)在我們已經(jīng)準備好開始解析HTML了。要解析單個節(jié)點,我們查看它的第一個字符,看它是元素節(jié)點還是文本節(jié)點。在我們簡化的HTML版本中,文本節(jié)點可以包含除<之外的任何字符。

          // Parse a single node.
          fn parse_node(&mut self) -> dom::Node {
          match self.next_char() {
          '<' => self.parse_element(),
          _ => self.parse_text()
          }
          }

          // Parse a text node.
          fn parse_text(&mut self) -> dom::Node {
          dom::text(self.consume_while(|c| c != '<'))
          }

          一個元素更為復雜。它包括開始和結(jié)束標簽,以及在它們之間任意數(shù)量的子節(jié)點:

          // Parse a single element, including its open tag, contents, and closing tag.
          fn parse_element(&mut self) -> dom::Node {
          // Opening tag.
          assert!(self.consume_char() == '<');
          let tag_name = self.parse_tag_name();
          let attrs = self.parse_attributes();
          assert!(self.consume_char() == '>');

          // Contents.
          let children = self.parse_nodes();

          // Closing tag.
          assert!(self.consume_char() == '<');
          assert!(self.consume_char() == '/');
          assert!(self.parse_tag_name() == tag_name);
          assert!(self.consume_char() == '>');

          return dom::elem(tag_name, attrs, children);
          }

          在我們簡化的語法中,解析屬性非常容易。在到達開始標記(>)的末尾之前,我們重復地查找后面跟著=的名稱,然后是用引號括起來的字符串。

          // Parse a single name="value" pair.
          fn parse_attr(&mut self) -> (String, String) {
          let name = self.parse_tag_name();
          assert!(self.consume_char() == '=');
          let value = self.parse_attr_value();
          return (name, value);
          }

          // Parse a quoted value.
          fn parse_attr_value(&mut self) -> String {
          let open_quote = self.consume_char();
          assert!(open_quote == '"' || open_quote == '\'');
          let value = self.consume_while(|c| c != open_quote);
          assert!(self.consume_char() == open_quote);
          return value;
          }

          // Parse a list of name="value" pairs, separated by whitespace.
          fn parse_attributes(&mut self) -> dom::AttrMap {
          let mut attributes = HashMap::new();
          loop {
          self.consume_whitespace();
          if self.next_char() == '>' {
          break;
          }
          let (name, value) = self.parse_attr();
          attributes.insert(name, value);
          }
          return attributes;
          }

          為了解析子節(jié)點,我們在循環(huán)中遞歸地調(diào)用parse_node,直到到達結(jié)束標記。這個函數(shù)返回一個Vec,這是Rust對可增長數(shù)組的名稱。

          // Parse a sequence of sibling nodes.
          fn parse_nodes(&mut self) -> Vec {
          let mut nodes = Vec::new();
          loop {
          self.consume_whitespace();
          if self.eof() || self.starts_with(" break;
          }
          nodes.push(self.parse_node());
          }
          return nodes;
          }

          最后,我們可以把所有這些放在一起,將整個HTML文檔解析成DOM樹。如果文檔沒有顯式包含根節(jié)點,則該函數(shù)將為文檔創(chuàng)建根節(jié)點;這與真正的HTML解析器的功能類似。

          // Parse an HTML document and return the root element.
          pub fn parse(source: String) -> dom::Node {
          let mut nodes = Parser { pos: 0, input: source }.parse_nodes();

          // If the document contains a root element, just return it. Otherwise, create one.
          if nodes.len() == 1 {
          nodes.swap_remove(0)
          } else {
          dom::elem("html".to_string(), HashMap::new(), nodes)
          }
          }

          就是這樣!robinson HTML解析器的全部代碼。整個程序總共只有100多行代碼(不包括空白行和注釋)。如果你使用一個好的庫或解析器生成器,你可能可以在更少的空間中構(gòu)建一個類似的玩具解析器。

          練習

          這里有一些你可以自己嘗試的替代方法。與前面一樣,你可以選擇其中的一個或多個,并忽略其他。

          1. 構(gòu)建一個以HTML子集作為輸入并生成DOM節(jié)點樹的解析器(“手動”或使用庫或解析器生成器)。
          2. 修改robinson的HTML解析器,添加一些缺失的特性,比如注釋?;蛘哂酶玫慕馕銎魈鎿Q它,可能使用庫或生成器構(gòu)建。
          3. 創(chuàng)建一個無效的HTML文件,導致你的(或我的)解析器失敗。修改解析器以從錯誤中恢復,并為測試文件生成DOM樹。

          捷徑

          如果想完全跳過解析,可以通過編程方式構(gòu)建DOM樹,向程序中添加類似這樣的代碼(偽代碼,調(diào)整它以匹配第1部分中編寫的DOM代碼):

          // Hello, world!
          let root = element("html");
          let body = element("body");
          root.children.push(body);
          body.children.push(text("Hello, world!"));

          或者你可以找到一個現(xiàn)有的HTML解析器并將其合并到你的程序中。

          本系列的下一篇文章將討論CSS數(shù)據(jù)結(jié)構(gòu)和解析。

          第三部分:CSS

          本文是構(gòu)建玩具瀏覽器呈現(xiàn)引擎系列文章中的第三篇。

          本文介紹了用于讀取層疊樣式表(CSS)的代碼。像往常一樣,我不會試圖涵蓋該規(guī)范中的所有內(nèi)容。相反,我嘗試實現(xiàn)足以說明一些概念并為后期渲染管道生成輸入的內(nèi)容。

          剖析樣式表

          下面是一個CSS源代碼示例:

          h1, h2, h3 { margin: auto; color: #cc0000; }
          div.note { margin-bottom: 20px; padding: 10px; }
          #answer { display: none; }

          接下來,我將從我的玩具瀏覽器引擎robinson中瀏覽css模塊。雖然這些概念可以很容易地轉(zhuǎn)換成其他編程語言,但代碼還是用Rust寫的。先閱讀前面的文章可能會幫助您理解下面的一些代碼。

          CSS樣式表是一系列規(guī)則。(在上面的示例樣式表中,每行包含一條規(guī)則。)

          struct Stylesheet {
          rules: Vec,
          }

          一條規(guī)則包括一個或多個用逗號分隔的選擇器,后跟一系列用大括號括起來的聲明。

          struct Rule {
          selectors: Vec,
          declarations: Vec,
          }

          一個選擇器可以是一個簡單的選擇器,也可以是一個由_組合符_連接的選擇器鏈。Robinson目前只支持簡單的選擇器。

          注意:令人困惑的是,新的Selectors Level 3標準使用相同的術(shù)語來表示略有不同的東西。在本文中,我主要引用CSS2.1。盡管過時了,但它是一個有用的起點,因為它更小,更獨立(與CSS3相比,CSS3被分成無數(shù)互相依賴和CSS2.1的規(guī)范)。

          在robinson中,一個簡單選擇器可以包括一個標記名,一個以'#'為前綴的ID,任意數(shù)量的以'.'為前綴的類名,或以上幾種情況的組合。如果標簽名為空或'*',那么它是一個“通用選擇器”,可以匹配任何標簽。

          還有許多其他類型的選擇器(特別是在CSS3中),但現(xiàn)在這樣就可以了。

          enum Selector {
          Simple(SimpleSelector),
          }

          struct SimpleSelector {
          tag_name: Option,
          id: Option,
          class: Vec,
          }

          聲明只是一個名稱/值對,由冒號分隔并以分號結(jié)束。例如,“margin: auto;”是一個聲明。

          struct Declaration {
          name: String,
          value: Value,
          }

          我的玩具引擎只支持CSS眾多值類型中的一小部分。

          enum Value {
          Keyword(String),
          Length(f32, Unit),
          ColorValue(Color),
          // insert more values here
          }

          enum Unit {
          Px,
          // insert more units here
          }

          struct Color {
          r: u8,
          g: u8,
          b: u8,
          a: u8,
          }

          注意:u8是一個8位無符號整數(shù),f32是一個32位浮點數(shù)。

          不支持所有其他CSS語法,包括@-rules、注釋和上面沒有提到的任何選擇器/值/單元。

          解析

          CSS有一個規(guī)則的語法,這使得它比它古怪的表親HTML更容易正確解析。當符合標準的CSS解析器遇到解析錯誤時,它會丟棄樣式表中無法識別的部分,但仍然處理其余部分。這是很有用的,因為它允許樣式表包含新的語法,但在舊的瀏覽器中仍然產(chǎn)生定義良好的輸出。

          Robinson使用了一個非常簡單(完全不符合標準)的解析器,構(gòu)建的方式與第2部分中的HTML解析器相同。我將粘貼一些代碼片段,而不是一行一行地重復整個過程。例如,下面是解析單個選擇器的代碼:

          // Parse one simple selector, e.g.: `type#id.class1.class2.class3`
          fn parse_simple_selector(&mut self) -> SimpleSelector {
          let mut selector = SimpleSelector { tag_name: None, id: None, class: Vec::new() };
          while !self.eof() {
          match self.next_char() {
          '#' => {
          self.consume_char();
          selector.id = Some(self.parse_identifier());
          }
          '.' => {
          self.consume_char();
          selector.class.push(self.parse_identifier());
          }
          '*' => {
          // universal selector
          self.consume_char();
          }
          c if valid_identifier_char(c) => {
          selector.tag_name = Some(self.parse_identifier());
          }
          _ => break
          }
          }
          return selector;
          }

          注意沒有錯誤檢查。一些格式不正確的輸入,如###*foo*將成功解析并產(chǎn)生奇怪的結(jié)果。真正的CSS解析器會丟棄這些無效的選擇器。

          優(yōu)先級

          優(yōu)先級是渲染引擎在沖突中決定哪一種樣式覆蓋另一種樣式的方法之一。如果一個樣式表包含兩個匹配元素的規(guī)則,具有較高優(yōu)先級的匹配選擇器的規(guī)則可以覆蓋較低優(yōu)先級的選擇器中的值。

          選擇器的優(yōu)先級基于它的組件。ID選擇器比類選擇器優(yōu)先級更高,類選擇器比標簽選擇器優(yōu)先級更高。在每個“層級”中,選擇器越多優(yōu)先級越高。

          pub type Specificity = (usize, usize, usize);

          impl Selector {
          pub fn specificity(&self) -> Specificity {
          // http://www.w3.org/TR/selectors/#specificity
          let Selector::Simple(ref simple) = *self;
          let a = simple.id.iter().count();
          let b = simple.class.len();
          let c = simple.tag_name.iter().count();
          (a, b, c)
          }
          }

          (如果我們支持鏈選擇器,我們可以通過將鏈各部分的優(yōu)先級相加來計算鏈的優(yōu)先級。)

          每個規(guī)則的選擇器都存儲在排序的向量中,優(yōu)先級最高的優(yōu)先。這對于匹配非常重要,我將在下一篇文章中介紹。

          // Parse a rule set: ` {  }`.
          fn parse_rule(&mut self) -> Rule {
          Rule {
          selectors: self.parse_selectors(),
          declarations: self.parse_declarations()
          }
          }

          // Parse a comma-separated list of selectors.
          fn parse_selectors(&mut self) -> Vec {
          let mut selectors = Vec::new();
          loop {
          selectors.push(Selector::Simple(self.parse_simple_selector()));
          self.consume_whitespace();
          match self.next_char() {
          ',' => { self.consume_char(); self.consume_whitespace(); }
          '{' => break, // start of declarations
          c => panic!("Unexpected character {} in selector list", c)
          }
          }
          // Return selectors with highest specificity first, for use in matching.
          selectors.sort_by(|a,b| b.specificity().cmp(&a.specificity()));
          return selectors;
          }

          CSS解析器的其余部分相當簡單。你可以在GitHub上閱讀全文。如果您在第2部分中還沒有這樣做,那么現(xiàn)在是嘗試解析器生成器的絕佳時機。我的手卷解析器完成了簡單示例文件的工作,但它有很多漏洞,如果您違反了它的假設(shè),它將嚴重失敗。有一天,我可能會用rust-peg或類似的東西來取代它。

          練習

          和以前一樣,你應(yīng)該決定你想做哪些練習,并跳過其余的:

          1. 實現(xiàn)您自己的簡化CSS解析器和優(yōu)先級計算。
          2. 擴展robinson的CSS解析器,以支持更多的值,或一個或多個選擇器組合符。
          3. 擴展CSS解析器,丟棄任何包含解析錯誤的聲明,并遵循錯誤處理規(guī)則,在聲明結(jié)束后繼續(xù)解析。
          4. 讓HTML解析器將任何
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲一区小电影网站 | 国产久久久精品 | 天堂AV中文 | 青青草草草在线视频资源站 | 国产精品久久久久久久猫咪 |