Dietro le quinte dei browser web moderni
Prefazione
Questa guida introduttiva completa sulle operazioni interne di WebKit e Gecko è la è il risultato di molte ricerche dello sviluppatore israeliano Tali Garsiel. Oltre alcune anni, ha esaminato tutti i dati pubblicati sulle funzionalità interne dei browser e ha trascorso un molto tempo a leggere il codice sorgente del browser web. Ha scritto:
In qualità di sviluppatore web, apprendere i componenti interni delle operazioni del browser ti aiuta a prendere decisioni migliori e a conoscere le giustificazioni alla base dello sviluppo best practice. Questo documento è piuttosto lungo, ma ti consigliamo dedichi un po' di tempo ad approfondire la questione. Sarai felice di averlo fatto.
Paul irlandese, Relazioni con gli sviluppatori di Chrome
Introduzione
I browser web sono il software più utilizzato. In questa guida introduttiva, spiegherò come
che lavorano in background. Vedremo cosa succede quando digiti google.com
nella barra degli indirizzi finché non vedi la pagina Google nella schermata del browser.
Browser di cui parleremo
Attualmente sono cinque i principali browser utilizzati sui computer: Chrome, Internet Explorer, Firefox, Safari e Opera. Sui dispositivi mobili, i browser principali sono Android Browser, iPhone, Opera Mini e Opera Mobile, UC Browser, i browser Nokia S40/S60 e Chrome, tutti basati su WebKit, ad eccezione dei browser Opera. Fornirò esempi dai browser open source, Firefox e Chrome, e da Safari (che è parzialmente open source). Secondo le statistiche di StatCounter (dati di giugno 2013), Chrome, Firefox e Safari rappresentano circa il 71% dell'utilizzo dei browser desktop a livello mondiale. Su dispositivi mobili, il browser Android, iPhone e Chrome costituiscono circa il 54% dell'utilizzo.
La funzionalità principale del browser
La funzione principale di un browser è presentare la risorsa web che scegli, richiedendola al server e visualizzandola nella finestra del browser. La risorsa è in genere un documento HTML, ma può anche essere un PDF, un'immagine o un altro tipo di contenuti. La località della risorsa viene specificata dall'utente utilizzando un URI (Uniform Resource Identifier).
Il modo in cui il browser interpreta e visualizza i file HTML è specificato nelle specifiche HTML e CSS. Queste specifiche sono gestite dall'organizzazione W3C (World Wide Web Consortium), l'organizzazione degli standard per il web. Per anni i browser sono conformi solo a una parte delle specifiche e hanno sviluppato le proprie estensioni. Questo ha causato gravi problemi di compatibilità per gli autori web. Oggi la maggior parte dei browser è più o meno conforme alle specifiche.
Le interfacce utente dei browser hanno molti aspetti in comune tra loro. Ecco alcuni degli elementi comuni dell'interfaccia utente:
- Barra degli indirizzi per l'inserimento di un URI
- Pulsanti Avanti e Indietro
- Opzioni di aggiunta ai preferiti
- Pulsanti di aggiornamento e interruzione per aggiornare o interrompere il caricamento dei documenti correnti
- Pulsante Home che ti porta alla home page
Stranamente, l'interfaccia utente del browser non è specificata in alcuna specifica formale, ma deriva solo da buone pratiche formate da anni di esperienza e da browser che si imitano a vicenda. La specifica HTML5 non definisce gli elementi dell'interfaccia utente che un browser deve avere, ma elenca alcuni elementi comuni. Tra questi ci sono la barra degli indirizzi, la barra di stato e la barra degli strumenti. Esistono, ovviamente, funzioni specifiche di un browser specifico, come il gestore dei download di Firefox.
Infrastruttura di alto livello
I componenti principali del browser sono:
- Interfaccia utente: sono inclusi la barra degli indirizzi, il pulsante Indietro/Avanti, il menu di aggiunta ai preferiti e così via. Ogni parte del browser viene visualizzata, ad eccezione della finestra in cui viene visualizzata la pagina richiesta.
- Il motore del browser: esegue il marshall delle azioni tra l'interfaccia utente e il motore di rendering.
- Il motore di rendering: responsabile della visualizzazione dei contenuti richiesti. Ad esempio, se i contenuti richiesti sono HTML, il motore di rendering analizza l'HTML e il CSS e visualizza i contenuti analizzati sullo schermo.
- Networking: per chiamate di rete come le richieste HTTP, con implementazioni diverse per piattaforme diverse dietro un'interfaccia indipendente dalla piattaforma.
- Backend UI: utilizzato per disegnare widget di base come caselle combinate e finestre. Questo backend espone un'interfaccia generica non specifica della piattaforma. Nella parte inferiore utilizza i metodi dell'interfaccia utente del sistema operativo.
- Interprete JavaScript. Utilizzato per analizzare ed eseguire il codice JavaScript.
- Archiviazione dei dati. Questo è un livello di persistenza. Il browser potrebbe dover salvare localmente tutti i tipi di dati, ad esempio i cookie. I browser supportano anche meccanismi di archiviazione come localStorage, IndexedDB, WebSQL e FileSystem.
È importante notare che i browser come Chrome eseguono più istanze del motore di rendering: una per ogni scheda. Ogni scheda viene eseguita in un processo separato.
Motori di rendering
La responsabilità del motore di rendering è bene... il rendering, ovvero la visualizzazione dei contenuti richiesti nella schermata del browser.
Per impostazione predefinita, il motore di rendering può visualizzare immagini e documenti HTML e XML. Può visualizzare altri tipi di dati tramite plug-in o estensioni. ad esempio la visualizzazione di documenti PDF utilizzando un plug-in per il visualizzatore di PDF. Tuttavia, in questo capitolo ci concentreremo sul caso d'uso principale: mostrare codice HTML e immagini formattate utilizzando CSS.
Browser diversi utilizzano motori di rendering differenti: Internet Explorer utilizza Trident, Firefox utilizza Gecko, Safari utilizza WebKit. Chrome e Opera (dalla versione 15) utilizzano Blink, un fork di WebKit.
WebKit è un motore di rendering open source che è stato avviato come motore per la piattaforma Linux ed è stato modificato da Apple per supportare Mac e Windows.
Il flusso principale
Il motore di rendering inizierà a recuperare i contenuti del documento richiesto dal livello di networking. Questa operazione di solito viene eseguita in blocchi da 8 kB.
A questo punto, seguiamo il flusso di base del motore di rendering:
Il motore di rendering inizierà ad analizzare il documento HTML e convertirà gli elementi in nodi DOM in una struttura chiamata "albero dei contenuti". Il motore analizzerà i dati di stile, sia nei file CSS esterni sia negli elementi di stile. Le informazioni sullo stile e le istruzioni visive nel codice HTML verranno utilizzate per creare un altro albero: l'albero di rendering.
L'albero di rendering contiene rettangoli con attributi visivi come colore e dimensioni. I rettangoli sono nell'ordine corretto per essere visualizzati sullo schermo.
Dopo la costruzione dell'albero di rendering, passa attraverso un "layout" e il processo di sviluppo. Ciò significa assegnare a ogni nodo le coordinate esatte in cui dovrebbe apparire sullo schermo. La fase successiva è il painting: verrà attraversato l'albero di rendering e ogni nodo verrà dipinto utilizzando il livello di backend dell'interfaccia utente.
È importante capire che si tratta di un processo graduale. Per una migliore esperienza utente, il motore di rendering cercherà di visualizzare i contenuti sullo schermo il prima possibile. Non aspetterà che venga analizzato tutto il codice HTML prima di iniziare la creazione e il layout dell'albero di rendering. Parti dei contenuti verranno analizzate e visualizzate, mentre il processo continua con gli altri contenuti che continuano a provenire dalla rete.
Esempi di flusso principale
Nelle figure 3 e 4 si può notare che, sebbene WebKit e Gecko utilizzino una terminologia leggermente diversa, il flusso è sostanzialmente lo stesso.
In Gecko l'albero degli elementi visivamente formattati è "Frame Tree". Ogni elemento è un frame. WebKit utilizza il termine "Albero di rendering" ed è costituito da "oggetti di rendering". WebKit utilizza il termine "layout" per il posizionamento degli elementi, mentre Gecko lo chiama "Reflow". "Allegato" WebKit è il termine che indica il collegamento di nodi DOM e informazioni visive per creare l'albero di rendering. Una piccola differenza non semantica è che Gecko ha un livello aggiuntivo tra l'HTML e l'albero DOM. È chiamato "sink di contenuti". ed è una fabbrica per creare elementi DOM. Parleremo di ogni parte del flusso:
Analisi - generale
Poiché l'analisi è un processo molto significativo all'interno del motore di rendering, lo approfondiremo. Iniziamo con un'introduzione sull'analisi.
Analizzare un documento significa tradurlo in una struttura utilizzabile dal codice. Il risultato dell'analisi è in genere una struttura di nodi che rappresenta la struttura del documento. Questo è chiamato albero di analisi o albero della sintassi.
Ad esempio, l'analisi dell'espressione 2 + 3 - 1
potrebbe restituire questo albero:
Grammatica
L'analisi si basa sulle regole di sintassi a cui il documento segue, ovvero il linguaggio o il formato in cui è stato scritto. Ogni formato che puoi analizzare deve avere una grammatica deterministica composta da regole di vocabolario e sintassi. È chiamato grammatica senza contesto. Le lingue umane non sono questi linguaggi e, di conseguenza, non possono essere analizzati con le tecniche di analisi convenzionali.
Combinazione di parser - Lexer
L'analisi può essere suddivisa in due processi secondari: analisi grammaticale e analisi della sintassi.
L'analisi lessicale è il processo di suddivisione dell'input in token. I token rappresentano il vocabolario linguistico, ovvero la raccolta di componenti di base validi. Nella lingua umana consisterà di tutte le parole che compaiono nel dizionario per quella lingua.
L'analisi della sintassi è l'applicazione delle regole di sintassi del linguaggio.
I parser di solito suddividono il lavoro tra due componenti: il lexer (a volte chiamato tokenizzatore) responsabile della suddivisione dell'input in token validi e il parser responsabile della costruzione dell'albero di analisi analizzando la struttura del documento in base alle regole di sintassi del linguaggio.
Il lexer sa come eliminare i caratteri non pertinenti come gli spazi e le interruzioni di riga.
Il processo di analisi è iterativo. L'analizzatore sintattico di solito chiede al lexer un nuovo token e tenta di trovare una corrispondenza tra il token e una delle regole di sintassi. In caso di corrispondenza con una regola, nell'albero di analisi verrà aggiunto un nodo corrispondente al token e l'analizzatore sintattico richiederà un altro token.
Se nessuna regola corrisponde, l'analizzatore sintattico archivia il token internamente e continua a richiedere token fino a quando non viene trovata una regola corrispondente a tutti i token archiviati internamente. Se non viene trovata alcuna regola, il parser genererà un'eccezione. Questo significa che il documento non è valido e contiene errori di sintassi.
Traduzione
In molti casi l'albero di analisi non è il prodotto finale. L'analisi è spesso utilizzata nella traduzione, ovvero trasforma il documento di input in un altro formato. Un esempio è la compilazione. Il compilatore che compila il codice sorgente in codice macchina prima lo analizza in un albero di analisi e poi traduce l'albero in un documento di codice macchina.
Esempio di analisi
Nella figura 5 abbiamo creato un albero di analisi a partire da un'espressione matematica. Proviamo a definire un linguaggio matematico semplice e a osservare il processo di analisi.
Sintassi:
- I componenti di base della sintassi del linguaggio sono espressioni, termini e operazioni.
- Il nostro linguaggio può includere un numero qualsiasi di espressioni.
- Un'espressione è definita come "termine" seguita da un comando "operation" seguito da un altro termine
- Un'operazione è un token più o un token meno
- Un termine è un token intero o un'espressione
Analizziamo l'input 2 + 3 - 1
.
La prima sottostringa che corrisponde a una regola è 2
: secondo la regola 5 si tratta di un termine.
La seconda corrispondenza è 2 + 3
: corrisponde alla terza regola, ovvero un termine seguito da un'operazione seguito da un altro termine.
La corrispondenza successiva verrà raggiunta solo alla fine dell'input.
2 + 3 - 1
è un'espressione perché sappiamo già che 2 + 3
è un termine, quindi abbiamo un termine seguito da un'operazione seguito da un altro termine.
2 + +
non corrisponde a nessuna regola e pertanto non è un valore valido.
Definizioni formali del vocabolario e della sintassi
Il vocabolario è in genere espresso da espressioni regolari.
Ad esempio, il nostro linguaggio verrà definito come:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
Come puoi vedere, i numeri interi sono definiti da un'espressione regolare.
La sintassi è in genere definita in un formato chiamato BNF. La nostra lingua sarà definita come:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
Abbiamo detto che una lingua può essere analizzata dai parser regolari se la sua grammatica è priva di contesto. Una definizione intuitiva di grammatica priva di contesto è una grammatica che può essere interamente espressa in BNF. Per una definizione formale, si veda Articolo di Wikipedia sulla grammatica senza contesto
Tipi di parser
Esistono due tipi di parser: dall'alto verso il basso e dal basso verso l'alto. Una spiegazione intuitiva è che i parser dall'alto verso il basso esaminano la struttura di alto livello della sintassi e cercano di trovare una corrispondenza di regola. I parser dal basso iniziano con l'input e lo trasformano gradualmente nelle regole di sintassi, partendo dalle regole di basso livello fino a quando non vengono soddisfatte le regole di alto livello.
Vediamo come i due tipi di parser analizzano il nostro esempio.
Il parser dall'alto verso il basso inizierà dalla regola di livello superiore: identificherà 2 + 3
come espressione. Identificherà quindi 2 + 3 - 1
come espressione (il processo di identificazione dell'espressione si evolve in base alle altre regole, ma il punto iniziale è la regola di livello più alto).
Il parser dal basso verso l'alto eseguirà la scansione dell'input finché non viene trovata una corrispondenza con una regola. Sostituirà quindi l'input corrispondente con la regola. Questo andrà avanti fino alla fine dell'input. L'espressione con corrispondenza parziale viene inserita nello stack dell'analizzatore sintattico.
Questo tipo di parser dal basso verso l'alto è chiamato parser shift-Reduce, perché l'input viene spostato a destra (immagina un puntatore che punta per primo all'inizio dell'input e si sposta a destra) e viene gradualmente ridotto a regole di sintassi.
Generazione automatica di parser
Esistono strumenti in grado di generare un parser. Devi fornire loro la grammatica della tua lingua, ovvero il vocabolario e le regole di sintassi, e generare un parser funzionante. La creazione di un parser richiede una profonda conoscenza dell'analisi e non è facile creare manualmente un parser ottimizzato, pertanto i generatori di parser possono essere molto utili.
WebKit utilizza due noti generatori di parser: Flex per la creazione di un parser e Bison per la creazione di un parser (potresti incontrarli con i nomi Lex e Yacc). L'input flessibile è un file contenente le definizioni di espressioni regolari dei token. L'input di Bison è costituito dalle regole di sintassi della lingua in formato BNF.
Analizzatore HTML
Il compito del parser HTML consiste nell'analizzare il markup HTML in un albero di analisi.
Grammatica HTML
Il vocabolario e la sintassi del codice HTML sono definiti nelle specifiche create dall'organizzazione W3C.
Come abbiamo visto nell'introduzione all'analisi, la sintassi grammaticale può essere definita formalmente utilizzando formati come BNF.
Purtroppo tutti gli argomenti dei parser convenzionali non si applicano all'HTML (non li ho visualizzati solo per divertimento, verranno utilizzati nell'analisi di CSS e JavaScript). Il codice HTML non può essere facilmente definito da una grammatica priva di contesto di cui i parser hanno bisogno.
Esiste un formato formale per la definizione dell'HTML (DTD, Document Type Definition), ma non è una grammatica priva di contesto.
Tutto ciò appare strano a prima vista: Il formato HTML è piuttosto simile al formato XML. Sono disponibili molti parser XML. Esiste una variante XML di HTML-XXL. Qual è quindi la grossa differenza?
La differenza è che l'approccio HTML è più "tollerante": ti permette di omettere determinati tag (che vengono poi aggiunti implicitamente), a volte di omettere tag di inizio e fine e così via. Nel complesso è "soft" piuttosto che la sintassi rigida ed esigente di XML.
Questi dettagli apparentemente piccoli fanno davvero la differenza. Da un lato questo è il motivo principale per cui il codice HTML è così popolare: permette di tollerare gli errori e semplifica la vita dell'autore web. D'altra parte, rende difficile scrivere una grammatica formale. Ricapitolando, il codice HTML non può essere analizzato facilmente dai parser convenzionali, poiché la sua grammatica non è esente da contesto. Il codice HTML non può essere analizzato dai parser XML.
DTD HTML
La definizione dell'HTML è in formato DTD. Questo formato viene utilizzato per definire le lingue della famiglia SGML. Il formato contiene le definizioni di tutti gli elementi consentiti, i relativi attributi e la gerarchia. Come abbiamo visto in precedenza, il DTD HTML non forma una grammatica priva di contesto.
Esistono alcune varianti della DTD. La modalità rigida è conforme esclusivamente alle specifiche, ma altre modalità contengono il supporto per il markup utilizzato in passato dai browser. Lo scopo è la compatibilità con le versioni precedenti dei contenuti. L'attuale DTD restrittiva è disponibile qui: www.w3.org/TR/html4/strict.dtd
DOM
La struttura di output (ovvero l'"albero di analisi") è una struttura di elementi DOM e nodi di attributi. DOM è l'abbreviazione di Document Object Model. È la presentazione degli oggetti del documento HTML e l'interfaccia degli elementi HTML al mondo esterno come JavaScript.
La radice dell'albero è "Documento" .
Il DOM ha una relazione quasi one-to-one con il markup. Ad esempio:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
Questo markup verrà tradotto nella seguente struttura DOM:
Come HTML, il DOM viene specificato dall'organizzazione W3C. Visita la pagina www.w3.org/DOM/DOMTR. Si tratta di una specifica generica per la manipolazione di documenti. Un modulo specifico descrive gli elementi specifici dell'HTML. Le definizioni HTML sono disponibili qui: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.
Quando dico che l'albero contiene nodi DOM, significa che l'albero è formato da elementi che implementano una delle interfacce DOM. I browser utilizzano implementazioni concrete che dispongono di altri attributi utilizzati internamente dal browser.
L'algoritmo di analisi
Come abbiamo visto nelle sezioni precedenti, non è possibile analizzare il codice HTML utilizzando i normali parser dall'alto verso il basso o dal basso verso l'alto.
I motivi sono i seguenti:
- La natura tollerante del linguaggio.
- Il fatto che i browser abbiano la tradizionale tolleranza di errore per supportare casi noti di codice HTML non valido.
- Il processo di analisi è rientrato. Per altri linguaggi, l'origine non cambia durante l'analisi, ma nel codice HTML il codice dinamico (ad esempio gli elementi di script che contengono chiamate
document.write()
) può aggiungere altri token, quindi il processo di analisi modifica effettivamente l'input.
Impossibile utilizzare le normali tecniche di analisi, i browser creano parser personalizzati per l'analisi del codice HTML.
L'algoritmo di analisi è descritto dettagliatamente nella specifica HTML5. L'algoritmo prevede due fasi: tokenizzazione e creazione ad albero.
La tokenizzazione è l'analisi lessicale, che analizza l'input in token. Tra i token HTML ci sono i tag di inizio, i tag finali, i nomi degli attributi e i valori degli attributi.
Il tokenizzatore riconosce il token, lo fornisce al costruttore ad albero e consuma il carattere successivo per riconoscerlo, e così via fino alla fine dell'input.
L'algoritmo di tokenizzazione
L'output dell'algoritmo è un token HTML. L'algoritmo è espresso come una macchina a stati. Ogni stato consuma uno o più caratteri del flusso di input e aggiorna lo stato successivo in base a quei caratteri. La decisione è influenzata dallo stato attuale di tokenizzazione e dallo stato di costruzione degli alberi. Ciò significa che lo stesso carattere utilizzato produrrà risultati diversi per lo stato successivo corretto, a seconda dello stato attuale. L'algoritmo è troppo complesso per essere descritto completamente. Vediamo un semplice esempio che ci aiuterà a comprendere il principio.
Esempio di base: tokenizzazione del seguente codice HTML:
<html>
<body>
Hello world
</body>
</html>
Lo stato iniziale è "Stato dei dati".
Quando viene rilevato il carattere <
, lo stato viene modificato in "Stato apertura tag".
L'utilizzo di un carattere a-z
causa la creazione di un "Token tag iniziale", lo stato viene modificato in "Stato nome tag".
Rimaniamo in questo stato finché non viene consumato il carattere >
. Ogni carattere viene aggiunto al nuovo nome del token. Nel nostro caso, il token creato è un token html
.
Quando viene raggiunto il tag >
, viene emesso il token attuale e lo stato cambia di nuovo in "Stato dei dati".
Il tag <body>
verrà trattato con gli stessi passaggi.
Finora sono stati emessi i tag html
e body
. Ora torniamo allo "Stato dei dati".
L'utilizzo del carattere H
di Hello world
causerà la creazione e l'emissione di un token del carattere, che va avanti fino al raggiungimento di <
di </body>
. Effettueremo un token di carattere per ogni carattere di Hello world
.
Ora torniamo allo "Stato di apertura tag".
L'utilizzo dell'input successivo /
comporterà la creazione di un end tag token
e lo spostamento allo "Stato nome tag". Ancora una volta, rimaniamo in questo stato fino a quando non raggiungiamo >
.Successivamente, il nuovo token del tag viene emesso e torniamo allo "Stato dei dati".
L'input </html>
verrà trattato come il caso precedente.
Algoritmo di costruzione di alberi
Alla creazione dell'analizzatore sintattico, viene creato l'oggetto Document. Durante la fase di creazione dell'albero, l'albero DOM con il documento nella sua radice verrà modificato e vi verranno aggiunti elementi. Ogni nodo emesso dal tokenizzatore verrà elaborato dal costruttore dell'albero. Per ogni token la specifica definisce quale elemento DOM è pertinente e verrà creato per questo token. L'elemento viene aggiunto alla struttura ad albero DOM e allo stack di elementi aperti. Questo stack viene utilizzato per correggere le errate corrispondenze di nidificazione e i tag non chiusi. L'algoritmo è anche descritto come una macchina a stati. Gli stati sono chiamati "modalità di inserimento".
Vediamo il processo di costruzione degli alberi per l'input di esempio:
<html>
<body>
Hello world
</body>
</html>
L'input alla fase di costruzione dell'albero è una sequenza di token dalla fase di tokenizzazione. La prima modalità è la "modalità iniziale". Ricezione del file "html" causerà lo spostamento alla modalità "before html" e una rielaborazione del token in tale modalità. Ciò causerà la creazione dell'elemento HTMLHTMLElement, che verrà aggiunto all'oggetto Documento principale.
Lo stato verrà modificato in "before head". Il "corpo" il token <end>. Un elemento HTMLHeadElement verrà creato in modo implicito anche se non è presente un elemento "head" e verrà aggiunto alla struttura.
Ora passiamo alla modalità "in head" e poi a "after head". Il token body viene rielaborato, creato e inserito un elemento HTMLBodyElement e la modalità viene trasferita in "in body".
I token carattere di "Hello World" di testo. Il primo comporterà la creazione e l'inserimento di un "Testo" e gli altri caratteri verranno aggiunti a quel nodo.
La ricezione del token di fine del corpo comporterà un trasferimento alla modalità "after body". Ora riceveremo il tag di fine HTML che ci sposterà alla modalità "after after body". La ricezione del token di fine del file terminerà l'analisi.
Azioni al termine dell'analisi
In questa fase il browser contrassegnerà il documento come interattivo e inizierà l'analisi degli script in stato "differito" mode: quelle che devono essere eseguite dopo l'analisi del documento. Lo stato del documento verrà quindi impostato su "completato" e un "caricamento" verrà attivato.
Puoi consultare tutti gli algoritmi per la tokenizzazione e la creazione di alberi nella specifica HTML5.
Browser tolleranza di errore
Non viene mai visualizzata una sintassi non valida su una pagina HTML. I browser correggono i contenuti non validi e continuano.
Prendi ad esempio questo codice HTML:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
Devo aver violato circa un milione di regole ("mytag" non è un tag standard, nidificazione errata degli elementi "p" e "div" e altri), ma il browser continua a mostrarlo correttamente e non si lamenta. Gran parte del codice del parser corregge gli errori dell'autore HTML.
La gestione degli errori è abbastanza coerente nei browser, ma, sorprendentemente, non ha fatto parte delle specifiche HTML. Come i pulsanti ai preferiti e ai pulsanti Indietro/Avanti, è qualcosa che si è sviluppato nei browser nel corso degli anni. Sono noti i costrutti HTML non validi ripetuti su molti siti e i browser tentano di correggerli in modo conforme ad altri browser.
Alcuni di questi requisiti sono definiti dalla specifica HTML5. (WebKit riassume bene questo concetto nel commento all'inizio della lezione sull'analizzatore sintattico HTML).
L'analizzatore sintattico analizza l'input tokenizzato nel documento, creando la struttura ad albero dei documenti. Se il formato del documento è corretto, l'analisi è semplice.
Purtroppo dobbiamo gestire molti documenti HTML che non sono correttamente formattati, quindi l'analizzatore sintattico deve tollerare gli errori.
Dobbiamo gestire almeno le seguenti condizioni di errore:
- L'elemento che viene aggiunto è espressamente vietato all'interno di alcuni tag esterni. In questo caso, dobbiamo chiudere tutti i tag fino a quello che vieta l'elemento e aggiungerlo successivamente.
- Non siamo autorizzati ad aggiungere l'elemento direttamente. È possibile che l'autore del documento abbia dimenticato qualche tag (o che il tag intermedio sia facoltativo). Questo potrebbe essere il caso dei seguenti tag: HTML HEAD BODY TBODY TR TD LI (ne ho dimenticato qualcuno?).
- Vogliamo aggiungere un elemento di blocco all'interno di un elemento incorporato. Chiudi tutti gli elementi incorporati fino all'elemento di blocco successivo più in alto.
- Se il problema persiste, chiudi gli elementi finché non avremo la possibilità di aggiungerli oppure ignora il tag.
Vediamo alcuni esempi di tolleranza di errore di WebKit:
</br>
anziché <br>
Alcuni siti utilizzano </br>
anziché <br>
. Per essere compatibile con IE e Firefox, WebKit considera questa opzione come <br>
.
Il codice:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
Tieni presente che la gestione degli errori è interna e non verrà mostrata all'utente.
Una tabella rara
Una tabella rara è una tabella all'interno di un'altra tabella, ma non all'interno di una cella.
Ad esempio:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit modificherà la gerarchia in due tabelle di pari livello:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
Il codice:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
WebKit utilizza una pila per i contenuti degli elementi correnti: farà uscire la tabella interna da quella esterna. Le tabelle saranno ora di pari livello.
Elementi del modulo nidificati
Se l'utente inserisce un modulo all'interno di un altro modulo, il secondo modulo viene ignorato.
Il codice:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
Una gerarchia di tag troppo profonda
Il commento parla da sé.
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}
Tag di chiusura body o HTML inseriti in modo errato
Ancora una volta: il commento parla da sé.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
Quindi fai attenzione agli autori web: a meno che tu non voglia apparire come esempio in uno snippet di codice di tolleranza di errore di WebKit, scrivi codice HTML ben strutturato.
Analisi CSS
Ricordi i concetti relativi all'analisi nell'introduzione? A differenza dell'HTML, il linguaggio CSS è una grammatica priva di contesto e può essere analizzato utilizzando i tipi di parser descritti nell'introduzione. Infatti, la specifica CSS definisce la grammatica e la grammatica grammaticale e di sintassi di CSS.
Vediamo alcuni esempi:
La grammatica grammaticale (vocabolario) è definita da espressioni regolari per ogni token:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
"ident" è l'abbreviazione di identificatore, come il nome di una classe. "nome" è un ID elemento (indicato con "#")
La grammatica della sintassi è descritta in BNF.
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
Spiegazione:
Un set di regole ha la seguente struttura:
div.error, a.error {
color:red;
font-weight:bold;
}
div.error
e a.error
sono selettori. La parte all'interno delle parentesi graffe contiene le regole applicate da questa serie di regole.
Questa struttura viene definita formalmente in questa definizione:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
Ciò significa che un set di regole è un selettore o, facoltativamente, un numero di selettori separati da una virgola e spazi (S sta per spazio vuoto). Una serie di regole contiene parentesi graffe e al loro interno una dichiarazione o, facoltativamente, una serie di dichiarazioni separate da un punto e virgola. "dichiarazione" e "selector" verrà definito nelle seguenti definizioni del BNF.
Parser CSS WebKit
WebKit utilizza i generatori di parser Flex e Bison per creare automaticamente parser dai file grammaticali CSS. Come ricorderai dall'introduzione dell'analizzatore sintattico, Bison crea un parser dal basso verso l'alto e di riduzione dei caratteri. Firefox utilizza un parser dall'alto verso il basso scritto manualmente. In entrambi i casi, ogni file CSS viene analizzato in un oggetto StyleSheet. Ogni oggetto contiene regole CSS. Gli oggetti regola CSS contengono oggetti selettore e dichiarazione e altri oggetti corrispondenti alla grammatica CSS.
Ordine di elaborazione di script e fogli di stile
Script
Il modello del web è sincrono. Gli autori si aspettano che gli script vengano analizzati ed eseguiti immediatamente quando l'analizzatore sintattico raggiunge un tag <script>
.
L'analisi del documento si interrompe fino all'esecuzione dello script.
Se lo script è esterno, la risorsa deve prima essere recuperata dalla rete. Questa operazione viene eseguita anche in modo sincrono e l'analisi si interrompe finché la risorsa non viene recuperata.
Questo è stato il modello per molti anni ed è specificato anche nelle specifiche HTML4 e 5.
Gli autori possono aggiungere "differenza" a uno script, nel qual caso l'analisi dei documenti non verrà interrotta e verrà eseguito dopo l'analisi del documento. HTML5 aggiunge un'opzione per contrassegnare lo script come asincrono in modo che venga analizzato ed eseguito da un thread diverso.
Analisi speculativa
Sia WebKit che Firefox eseguono questa ottimizzazione. Durante l'esecuzione degli script, un altro thread analizza il resto del documento, scopre quali altre risorse devono essere caricate dalla rete e le carica. In questo modo le risorse possono essere caricate su connessioni parallele e la velocità complessiva viene migliorata. Nota: il parser speculativo analizza solo i riferimenti a risorse esterne come script esterni, fogli di stile e immagini; non modifica l'albero DOM, che viene lasciato all'analizzatore sintattico principale.
Fogli di stile
I fogli di stile, invece, hanno un modello diverso. Concettualmente sembra che, poiché i fogli di stile non modificano l'albero DOM, non c'è motivo di attendere che vengano visualizzati e di interrompere l'analisi del documento. Esiste però un problema con gli script che richiedono informazioni sullo stile durante la fase di analisi del documento. Se lo stile non viene ancora caricato e analizzato, lo script riceverà risposte errate e ciò a quanto pare questo ha causato molti problemi. Sembra essere un caso limite, ma è abbastanza comune. Firefox blocca tutti gli script quando è ancora presente un foglio di stile in fase di caricamento e analisi. WebKit blocca gli script solo quando tentano di accedere a determinate proprietà di stile che potrebbero essere interessate dai fogli di stile non caricati.
Rendering della costruzione di alberi
Durante la creazione dell'albero DOM, il browser crea un altro albero, quello di rendering. Questa struttura è composta da elementi visivi nell'ordine in cui verranno visualizzati. È la rappresentazione visiva del documento. Lo scopo di questo albero è consentire di dipingere i contenuti nell'ordine corretto.
Firefox chiama "frame" agli elementi nell'albero di rendering. WebKit utilizza il termine renderer o oggetto di rendering.
Un renderer sa come impaginare e dipingere se stesso e i relativi figli.
La classe RenderObject di WebKit, la classe base dei renderer, ha la seguente definizione:
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
Ogni renderer rappresenta un'area rettangolare di solito corrispondente alla casella CSS di un nodo, come descritto nelle specifiche CSS2. Include informazioni geometriche come larghezza, altezza e posizione.
Il tipo di casella è influenzato dalla visualizzazione valore dell'attributo di stile pertinente al nodo (consulta la sezione computing dello stile). Di seguito è riportato il codice WebKit per decidere quale tipo di renderer creare per un nodo DOM, in base all'attributo di visualizzazione:
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
Viene preso in considerazione anche il tipo di elemento: ad esempio, i controlli del modulo e le tabelle hanno frame speciali.
In WebKit, se un elemento desidera creare un renderer speciale, sostituisce il metodo createRenderer()
.
I renderer puntano a oggetti di stile che contengono informazioni non geometriche.
La relazione dell'albero di rendering con l'albero DOM
I renderer corrispondono agli elementi DOM, ma la relazione non è uno a uno. Gli elementi DOM non visivi non verranno inseriti nell'albero di rendering. Un esempio è la sezione "head" . Anche gli elementi il cui valore visualizzato è stato assegnato a "nessuno" non verranno visualizzati nell'albero, mentre gli elementi con visibilità "nascosta" appariranno nell'albero.
Esistono elementi DOM che corrispondono a diversi oggetti visivi. Si tratta di solito di elementi con una struttura complessa che non può essere descritta da un singolo rettangolo. Ad esempio, il pulsante "select" ha tre renderer: uno per l'area di visualizzazione, uno per la casella di riepilogo a discesa e uno per il pulsante. Inoltre, quando il testo viene suddiviso in più righe perché la larghezza non è sufficiente per una sola riga, le nuove righe vengono aggiunte come renderer aggiuntivi.
Un altro esempio di renderer multipli è il codice HTML non funzionante. In base alle specifiche CSS, un elemento in linea deve contenere solo elementi di blocco o solo elementi in linea. Nel caso di contenuti misti, verranno creati renderer a blocchi anonimi per aggregare gli elementi incorporati.
Alcuni oggetti di rendering corrispondono a un nodo DOM ma non nello stesso punto nell'albero. I galleggianti e gli elementi assolutamente posizionati sono fuori flusso, posizionati in una parte diversa dell'albero e mappati al frame reale. Un frame segnaposto è il punto in cui avrebbero dovuto essere.
.Il flusso per la costruzione dell'albero
In Firefox, la presentazione è registrata come listener per gli aggiornamenti DOM.
La presentazione delega la creazione di frame all'FrameConstructor
e il costruttore risolve lo stile (vedi computing dello stile) e crea un frame.
In WebKit il processo di risoluzione dello stile e di creazione di un renderer è chiamato "allegato". Ogni nodo DOM dispone di un . Il collegamento è sincrono, l'inserimento del nodo nell'albero DOM chiama il nuovo nodo "attach" .
L'elaborazione dei tag html e body comporta la creazione della radice dell'albero di rendering.
L'oggetto di rendering principale corrisponde a ciò che la specifica CSS chiama il blocco contenitore: il blocco più alto che contiene tutti gli altri blocchi. Le sue dimensioni sono l'area visibile, ovvero le dimensioni dell'area di visualizzazione della finestra del browser.
Firefox lo chiama ViewPortFrame
, mentre WebKit lo chiama RenderView
.
Si tratta dell'oggetto di rendering a cui punta il documento.
Il resto dell'albero è costruito come inserimento di nodi DOM.
Consulta le specifiche CSS2 relative al modello di elaborazione.
Computing dello stile
La creazione dell'albero di rendering richiede il calcolo delle proprietà visive di ogni oggetto di rendering. Per farlo, devi calcolare le proprietà di stile di ogni elemento.
Lo stile include fogli di stile di varie origini, elementi di stile incorporati e proprietà visive nel codice HTML (come la proprietà "bgcolor").La query successiva viene tradotta nelle proprietà di stile CSS corrispondenti.
Le origini dei fogli di stile sono i fogli di stile predefiniti del browser, i fogli di stile forniti dall'autore della pagina e i fogli di stile dell'utente. Si tratta di fogli di stile forniti dall'utente del browser (i browser consentono di definire i tuoi stili preferiti. Ad esempio, in Firefox, questo viene eseguito inserendo un foglio di stile nella sezione "Profilo Firefox" ).
Il calcolo dello stile presenta alcune difficoltà:
- I dati di stile sono una struttura di dimensioni molto grandi, che contiene numerose proprietà degli stili e può quindi causare problemi di memoria.
Trovare le regole di corrispondenza per ogni elemento può causare problemi di rendimento se non è ottimizzato. Controllare l'intero elenco delle regole per ogni elemento per trovare corrispondenze è un'attività impegnativa. I selettori possono avere una struttura complessa che può portare il processo di abbinamento a iniziare su un percorso apparentemente promettente che si è dimostrato inutile ed è necessario provare un altro percorso.
Ad esempio, il seguente selettore composto:
div div div div{ ... }
Indica che le regole vengono applicate a un elemento
<div>
che è il discendente di 3 div. Supponi di voler verificare se la regola si applica a un determinato elemento<div>
. e scegliere un determinato percorso nella struttura ad albero. È possibile che sia necessario attraversare la struttura ad albero dei nodi solo per scoprire che ci sono solo due div e che la regola non si applica. Poi devi provare altri percorsi nell'albero.L'applicazione delle regole comporta regole a cascata piuttosto complesse che definiscono la gerarchia delle regole.
Vediamo in che modo i browser affrontano questi problemi:
Condivisione dei dati sullo stile
I nodi WebKit fanno riferimento a oggetti di stile (RenderStyle). Questi oggetti possono essere condivisi dai nodi in alcune condizioni. I nodi sono fratelli o cugini e:
- Gli elementi devono essere nello stesso stato del mouse (ad es.uno non può essere in :hover e l'altro no).
- Nessuno dei due elementi deve avere un ID
- I nomi dei tag devono corrispondere
- Gli attributi della classe devono corrispondere
- L'insieme degli attributi mappati deve essere identico
- Gli stati del collegamento devono corrispondere
- Gli stati dello stato attivo devono corrispondere
- Nessuno dei due elementi deve essere interessato dai selettori degli attributi, laddove interessati si intende la corrispondenza con qualsiasi selettore che utilizzi un selettore di attributo in qualsiasi posizione all'interno del selettore.
- Non deve essere presente alcun attributo di stile incorporato negli elementi
- Non devono essere in uso selettori di pari livello. WebCore genera semplicemente un'opzione globale quando viene rilevato un selettore di pari livello e disattiva la condivisione degli stili per l'intero documento, se presente. Sono inclusi il selettore + e i selettori come :first-child e :last-child.
Struttura delle regole di Firefox
Firefox dispone di due strutture aggiuntive per semplificare il calcolo dello stile: la struttura delle regole e la struttura di contesto degli stili. WebKit dispone inoltre di oggetti di stile che però non sono archiviati in una struttura ad albero come l'albero di contesto di stile, ma solo il nodo DOM punta allo stile pertinente.
I contesti di stile contengono valori finali. I valori vengono calcolati applicando tutte le regole corrispondenti nell'ordine corretto ed eseguendo manipolazioni che le trasformano da valori logici a valori concreti. Ad esempio, se il valore logico è una percentuale dello schermo, verrà calcolato e trasformato in unità assolute. L'idea dell'albero delle regole è molto intelligente. Consente di condividere questi valori tra nodi per evitare di calcolarli di nuovo. Ciò consente anche di risparmiare spazio.
Tutte le regole corrispondenti vengono archiviate in una struttura ad albero. I nodi inferiori in un percorso hanno una priorità maggiore. L'albero contiene tutti i percorsi per le corrispondenze delle regole trovate. L'archiviazione delle regole viene eseguita lentamente. L'albero non viene calcolato all'inizio per ogni nodo, ma ogni volta che è necessario calcolare uno stile di nodo, i percorsi calcolati vengono aggiunti all'albero.
L'idea è quella di vedere i percorsi ad albero sotto forma di parole in un vocabolario. Supponiamo di aver già calcolato questo albero di regole:
Supponiamo di dover trovare una corrispondenza con le regole di un altro elemento dell'albero dei contenuti e di scoprire che le regole corrispondenti (nell'ordine corretto) sono B-E-I. Questo percorso è già presente nell'albero perché è già stato calcolato il percorso A-B-E-I-L. Ora avremo meno lavoro da fare.
Vediamo come l'albero ci fa risparmiare sul lavoro.
Divisione in struct
I contesti di stile sono suddivisi in struct. Questi struct contengono informazioni sullo stile per una determinata categoria, come bordo o colore. Tutte le proprietà di uno struct vengono ereditate o non ereditate. Le proprietà ereditate sono proprietà che, se non definite dall'elemento, vengono ereditate dal relativo elemento principale. Le proprietà non ereditate (chiamate proprietà "reset") utilizzano valori predefiniti se non vengono definiti.
L'albero ci aiuta memorizzando nella cache interi struct (contenenti i valori finali calcolati) nell'albero. L'idea è che se il nodo inferiore non ha fornito una definizione di uno struct, è possibile utilizzare uno struct memorizzato nella cache in un nodo superiore.
Calcolo dei contesti di stile mediante l'albero delle regole
Quando calcoliamo il contesto dello stile per un determinato elemento, prima viene calcolato un percorso nella struttura di regole o ne viene utilizzato uno esistente. Iniziamo quindi ad applicare le regole nel percorso per riempire gli struct nel nuovo contesto dello stile. Iniziamo dal nodo inferiore del percorso, quello con la precedenza più alta (di solito il selettore più specifico), e attraversiamo l'albero fino a completare lo struct. Se non esiste una specifica per lo struct nel nodo della regola, è possibile ottimizzare enormemente: saliamo nell'albero fino a trovare un nodo che lo specifica completamente e punta a esso. Questa è l'ottimizzazione migliore. Viene condiviso l'intero struct. In questo modo si evita di calcolare i valori finali e la memoria.
Se troviamo definizioni parziali, saliamo nell'albero fino a completare lo struct.
Se non sono state trovate definizioni per lo struct, nel caso in cui lo struct sia "ereditato", , puntiamo allo struct dell'elemento principale nell'albero del contesto. Anche in questo caso siamo riusciti a condividere gli struct. Se è uno struct di reimpostazione, verranno usati i valori predefiniti.
Se il nodo più specifico aggiunge valori, dobbiamo fare dei calcoli aggiuntivi per convertirlo in valori effettivi. Quindi memorizziamo nella cache il risultato nel nodo ad albero in modo che possa essere utilizzato dagli elementi secondari.
Nel caso in cui un elemento abbia un elemento di pari livello o uno principale che punta allo stesso nodo ad albero, è possibile condividere l'intero contesto di stile tra i due elementi.
Vediamo un esempio: Supponiamo di avere questo codice HTML
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error</span> error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>
e le seguenti regole:
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
Per semplificare le cose, supponiamo di dover compilare solo due struct: lo struct colore e lo struct margin. Lo struct colore contiene un solo membro: il colore Lo struct Margine contiene i quattro lati.
L'albero delle regole risultante sarà simile a questo (i nodi sono contrassegnati con il nome nodo, ovvero il numero della regola a cui puntano):
L'albero di contesto sarà simile a questo (nome nodo: nodo regola a cui puntano):
Supponiamo di analizzare il codice HTML e di arrivare al secondo tag <div>
. Dobbiamo creare un contesto di stile per questo nodo e riempire i relativi struct di stile.
Abbrevieremo le regole e scopriremo che le regole corrispondenti per <div>
sono 1, 2 e 6.
Ciò significa che esiste già un percorso nell'albero che il nostro elemento può utilizzare e che dobbiamo solo aggiungere un altro nodo per la regola 6 (nodo F nell'albero delle regole).
Creeremo un contesto di stile e lo inseriremo nell'albero del contesto. Il contesto del nuovo stile punterà al nodo F nell'albero delle regole.
Ora dobbiamo riempire gli struct di stile. Inizieremo compilando lo struct margin. Poiché l'ultimo nodo della regola (F) non si aggiunge allo struct margin, possiamo salire nella struttura ad albero finché non troviamo uno struct memorizzato nella cache calcolato in un precedente inserimento di nodi e lo utilizziamo. Lo si trova sul nodo B, che è il nodo più in alto in cui sono specificate le regole per i margini.
Abbiamo una definizione dello struct colore, quindi non possiamo usare uno struct memorizzato nella cache. Poiché il colore ha un solo attributo, non è necessario salire nell'albero per riempire altri attributi. Calcoleremo il valore finale (converte la stringa in RGB e così via) e memorizzeremo nella cache lo struct calcolato su questo nodo.
Il lavoro sul secondo elemento <span>
è ancora più semplice. Abbrevieremo le regole e giungere alla conclusione che punta alla regola G, come l'intervallo precedente.
Poiché abbiamo fratelli e sorelle che puntano allo stesso nodo, possiamo condividere l'intero contesto dello stile e puntare al contesto dell'intervallo precedente.
Per gli struct che contengono regole ereditate dall'elemento padre, la memorizzazione nella cache viene eseguita nell'albero del contesto (la proprietà del colore viene effettivamente ereditata, ma Firefox la considera reimpostata e la memorizza nella cache nella struttura delle regole).
Ad esempio, se abbiamo aggiunto delle regole per i caratteri di un paragrafo:
p {font-family: Verdana; font size: 10px; font-weight: bold}
Quindi l'elemento paragrafo, che è un elemento secondario del div nell'albero di contesto, avrebbe potuto condividere lo stesso struct del carattere dell'elemento principale. Questo accade se non sono state specificate regole di carattere per il paragrafo.
In WebKit, che non ha un albero di regole, le dichiarazioni corrispondenti vengono passate quattro volte. Vengono applicate prima le proprietà ad alta priorità non importanti (le proprietà che devono essere applicate per prime perché altre dipendono da queste, ad esempio la visualizzazione), poi sono importanti le proprietà con priorità elevata e infine le regole con priorità normale non importanti e infine le regole importanti con priorità normale. Ciò significa che le proprietà che appaiono più volte verranno risolte in base all'ordine a cascata corretto. Vince l'ultima.
Ricapitolando, la condivisione degli oggetti di stile (interamente o alcuni degli struct al loro interno) risolve i problemi 1 e 3. Anche la struttura delle regole di Firefox consente di applicare le proprietà nell'ordine corretto.
Manipolare le regole per semplificare la corrispondenza
Esistono diverse origini per le regole di stile:
- Regole CSS, in fogli di stile esterni o in elementi di stile.
css p {color: blue}
- Attributi di stile in linea come
html <p style="color: blue" />
- Attributi visivi HTML (mappati a regole di stile pertinenti)
html <p bgcolor="blue" />
Gli ultimi due elementi possono essere abbinati facilmente all'elemento poiché gli attributi di stile sono di sua proprietà e gli attributi HTML possono essere mappati utilizzando l'elemento come chiave.
Come osservato in precedenza nel numero 2, la corrispondenza delle regole CSS può essere più complessa. Per risolvere il problema, le regole vengono manipolate per facilitare l'accesso.
Dopo aver analizzato il foglio di stile, le regole vengono aggiunte a una delle numerose mappe hash, in base al selettore. Esistono mappe per ID, nome della classe, nome tag e una mappa generale per tutto ciò che non rientra in queste categorie. Se il selettore è un ID, la regola verrà aggiunta alla mappa ID, se si tratta di una classe verrà aggiunta alla mappa delle classi e così via.
Questa manipolazione rende molto più facile trovare corrispondenze con le regole. Non è necessario cercare in ogni dichiarazione: possiamo estrarre le regole pertinenti per un elemento dalle mappe. Questa ottimizzazione elimina più del 95% delle regole, in modo che non debbano essere nemmeno prese in considerazione durante il processo di corrispondenza(4.1).
Vediamo ad esempio le seguenti regole di stile:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
La prima regola verrà inserita nella mappa delle classi. Il secondo nella mappa ID e il terzo nella mappa tag.
Per il seguente frammento HTML;
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
Per prima cosa cercheremo di trovare le regole per l'elemento p. La mappa delle classi contiene un "errore" chiave in base alla quale la regola per "p.error" viene trovato. L'elemento div avrà regole pertinenti nella mappa ID (la chiave è l'ID) e nella mappa dei tag. Quindi l'unico lavoro rimasto è scoprire quali delle regole estratte dalle chiavi corrispondono davvero.
Ad esempio, se la regola per il tag div era:
table div {margin: 5px}
Verrà comunque estratta dalla mappa di tag perché la chiave è il selettore più a destra, ma non corrisponderà all'elemento div, che non ha un predecessore della tabella.
Sia WebKit che Firefox eseguono questa manipolazione.
Ordine a cascata dei fogli di stile
L'oggetto di stile ha proprietà corrispondenti a ogni attributo visivo (tutti gli attributi CSS, ma più generici). Se la proprietà non è definita da nessuna delle regole corrispondenti, alcune proprietà possono essere ereditate dall'oggetto di stile dell'elemento principale. Le altre proprietà hanno valori predefiniti.
Il problema inizia quando esiste più di una definizione. In questo caso occorre l'ordine a cascata per risolvere il problema.
Una dichiarazione relativa a una proprietà di stile può essere visualizzata in diversi fogli di stile e più volte all'interno di un foglio di stile. Ciò significa che l'ordine di applicazione delle regole è molto importante. Questo processo è chiamato "cascade" ordine. Secondo la specifica CSS2, l'ordine a cascata è (dal più basso al più alto):
- Dichiarazioni del browser
- Dichiarazioni dell'utente normali
- Dichiarazioni normali dell'autore
- Dichiarazioni importanti dell'autore
- Dichiarazioni importanti dell'utente
Le dichiarazioni del browser sono meno importanti e l'utente sostituisce l'autore solo se la dichiarazione è stata contrassegnata come importante. Le dichiarazioni con lo stesso ordine verranno ordinate per specificità e successivamente in base all'ordine in cui vengono indicate. Gli attributi visivi HTML vengono tradotti nelle dichiarazioni CSS corrispondenti . Vengono trattate come regole per gli autori con priorità bassa.
Specificità
La specificità del selettore è definita dalla specifica CSS2 come segue:
- conteggio 1 se la dichiarazione da cui proviene è uno "stile" anziché una regola con un selettore, altrimenti 0 (= a)
- conta il numero di attributi ID nel selettore (= b)
- contare il numero di altri attributi e pseudo-classi nel selettore (= c)
- conta il numero di nomi di elementi e pseudo-elementi nel selettore (= d)
Concatenare i quattro numeri a-b-c-d (in un sistema numerico con una base grande) dà la specificità.
La base numerica che devi utilizzare è definita dal numero più alto che hai in una delle categorie.
Ad esempio, se a=14 puoi utilizzare una base esadecimale. Nell'improbabile caso in cui a=17 sia necessaria una base numerica di 17 cifre. La situazione successiva può verificarsi con un selettore come questo: html body div div p... (17 tag nel selettore... probabilmente non molto).
Ecco alcuni esempi:
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
Ordinamento delle regole
Una volta soddisfatte, le regole vengono ordinate in base alle regole a cascata.
WebKit utilizza l'ordinamento a bolle per gli elenchi di piccole dimensioni e l'ordinamento di unione per quelli grandi.
WebKit implementa l'ordinamento sostituendo l'operatore >
per le regole:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}
Processo graduale
WebKit utilizza un flag che contrassegna tutti i fogli di stile di primo livello (comprese le @imports) sono stati caricati. Se lo stile non è stato caricato completamente durante il caricamento, vengono utilizzati segnaposto, che vengono contrassegnati nel documento e verranno ricalcolati dopo il caricamento dei fogli di stile.
Layout
Quando il renderer viene creato e aggiunto all'albero, non sono disponibili posizione e dimensione. Il calcolo di questi valori prende il nome di layout o ripetizione flusso.
Il codice HTML utilizza un modello di layout basato sul flusso, il che significa che la maggior parte delle volte è possibile calcolare la geometria in una singola passata. Elementi più avanti "nel flusso" in genere non influisce sulla geometria degli elementi che si trovano precedentemente "nel flusso", quindi il layout può procedere da sinistra a destra e dall'alto verso il basso nel documento. Ci sono delle eccezioni: ad esempio, le tabelle HTML possono richiedere più di una tessera.
Il sistema di coordinate è relativo al frame principale. Vengono utilizzate le coordinate in alto e a sinistra.
Il layout è un processo ricorsivo. Inizia nel renderer principale, che corrisponde all'elemento <html>
del documento HTML. Layout continua in modo ricorsivo attraverso parte o tutta la gerarchia di frame, calcolando informazioni geometriche per ogni renderer che lo richiede.
La posizione del renderer principale è 0,0 e le sue dimensioni corrispondono all'area visibile, ossia la parte visibile della finestra del browser.
Tutti i renderer hanno un "layout" o "ripetizione flusso" , ogni renderer richiama il metodo di layout dei relativi elementi secondari che richiedono il layout.
Sistema dirty bit
Per non realizzare un layout completo per ogni piccola modifica, i browser utilizzano un "dirty bit" di un sistema operativo completo. Un renderer che viene modificato o aggiunto contrassegna se stesso e i relativi elementi secondari come "dirty": necessita di layout.
Sono presenti due flag: "dirty" e "children are dirty". Ciò significa che, sebbene il renderer possa andare bene, ha almeno un renderer secondario che necessita di un layout.
Layout globale e incrementale
Il layout può essere attivato sull'intero albero di rendering: è "globale" layout. Ciò può accadere per i seguenti motivi:
- Una modifica di stile globale che interessa tutti i renderer, ad esempio una modifica delle dimensioni del carattere.
- In seguito al ridimensionamento di uno schermo
Il layout può essere incrementale; verranno visualizzati solo i renderer dirty (questo può causare alcuni danni che richiederanno layout aggiuntivi).
Il layout incrementale viene attivato (in modo asincrono) quando i renderer sono "sporchi". Ad esempio, quando vengono aggiunti nuovi renderer all'albero di rendering dopo che i contenuti extra provengono dalla rete e sono stati aggiunti all'albero DOM.
Layout asincrono e sincrono
Il layout incrementale viene eseguito in modo asincrono. Firefox mette in coda i "comandi di ripetizione flusso" per i layout incrementali e uno scheduler attiva l'esecuzione in batch di questi comandi. WebKit dispone inoltre di un timer che esegue un layout incrementale: l'albero viene attraversato e "sporco" i renderer sono con layout out.
Script che richiedono informazioni sullo stile, ad esempio "offsetHeight" può attivare il layout incrementale in modo sincrono.
Il layout globale viene in genere attivato in modo sincrono.
A volte il layout viene attivato come callback dopo un layout iniziale perché alcuni attributi, come la posizione di scorrimento, sono cambiati.
Ottimizzazioni
Quando un layout viene attivato da un "ridimensionamento" o una modifica della posizione del renderer(e non delle dimensioni), le dimensioni del rendering vengono calcolate dalla cache e non vengono ricalcolate...
In alcuni casi viene modificata solo una struttura secondaria e il layout non inizia dalla radice. Questo può accadere nei casi in cui la modifica è locale e non influisce sull'ambiente circostante, come il testo inserito nei campi di testo (altrimenti ogni sequenza di tasti attiverà un layout a partire dalla radice).
Procedura di layout
In genere il layout ha il seguente pattern:
- Renderer padre determina la propria larghezza.
- Il genitore esamina i bambini e:
- Posiziona il renderer secondario (imposta i relativi valori x e y).
- Consente di chiamare il layout secondario, se necessario (è sporco, è in un layout globale o per qualche altro motivo), che calcola l'altezza del bambino.
- L'elemento principale utilizza le altezze cumulative dei publisher secondari, nonché le altezze dei margini e della spaziatura interna per impostare la propria altezza, che verrà utilizzata dal renderer principale.
- Imposta il bit dirty su false.
Firefox utilizza uno "stato" oggetto(nsHTMLReflowState) come parametro di layout (denominato "reflow"). Tra gli altri, lo stato include la larghezza dell'elemento padre.
L'output del layout di Firefox è una "metrica" object(nsHTMLReflowMetrics). Contiene l'altezza calcolata del renderer.
Calcolo della larghezza
La larghezza del renderer viene calcolata utilizzando la larghezza del blocco container, lo stile del renderer "width" i margini e i bordi.
Ad esempio, la larghezza del seguente div:
<div style="width: 30%"/>
WebKit verrebbe calcolato nel seguente modo(metodo di classe RenderBox calcwidth):
- La larghezza del container è il numero massimo di container disponibiliwidth e pari a 0. In questo caso, availablewidth è contentwidth e viene calcolato come segue:
clientWidth() - paddingLeft() - paddingRight()
clientLarghezza e clientHeight rappresentano l'interno di un oggetto escludendo bordo e barra di scorrimento.
La larghezza degli elementi è la "larghezza" attributo style. Sarà calcolato come valore assoluto calcolando la percentuale della larghezza del container.
I bordi orizzontali e le spaziature interne sono stati aggiunti.
Finora questo era il calcolo della "larghezza preferita". Ora verranno calcolate le larghezze minima e massima.
Se la larghezza preferita è maggiore della larghezza massima, viene utilizzata la larghezza massima. Se è inferiore alla larghezza minima (l'unità infrangibile più piccola), viene utilizzata la larghezza minima.
I valori vengono memorizzati nella cache nel caso sia necessario un layout, ma la larghezza non cambia.
Interruzione di riga
Quando un renderer nel mezzo di un layout decide che deve essere interrotto, il renderer si arresta e si propaga all'elemento padre del layout che deve essere interrotto. L'elemento principale crea i renderer e il layout delle chiamate aggiuntivi.
Pittura
Nella fase di disegno, viene attraversato l'albero di rendering e viene "paint()" del renderer per visualizzare i contenuti sullo schermo. La pittura utilizza il componente dell'infrastruttura UI.
Globale e incrementale
Come per il layout, anche la pittura può essere globale, ossia l'intero albero, oppure incrementale. Nel disegno incrementale, alcuni renderer cambiano in modo da non interessare l'intero albero. Il renderer modificato invalida il rettangolo sullo schermo. In questo modo il sistema operativo la vede come una "regione "sporca" e generiamo un colore . Il sistema operativo esegue queste operazioni in modo intelligente e combina diverse regioni in una sola. In Chrome è più complicato perché il renderer si trova in un processo diverso da quello principale. Chrome simula in una certa misura il comportamento del sistema operativo. La presentazione ascolta questi eventi e delega il messaggio alla radice di rendering. L'albero viene attraversato fino a raggiungere il renderer pertinente. Ridipinge il dispositivo stesso (e di solito anche per i relativi figli).
L'ordine della pittura
CSS2 definisce l'ordine del processo di disegno. Questo è in realtà l'ordine in cui gli elementi vengono impilati nei contesti di impilamento. Questo ordine influisce sulla pittura, poiché le pile sono dipinte dal retro rispetto alla parte anteriore. L'ordine di sovrapposizione di un renderer a blocchi è il seguente:
- colore sfondo
- Immagine di sfondo
- border
- bambini
- struttura
Elenco di visualizzazione di Firefox
Firefox esamina l'albero di rendering e crea un elenco di visualizzazione per il rettangolare dipinto. Contiene i renderer pertinenti per il rettangolo, nell'ordine di disegno corretto (sfondi dei renderer, quindi bordi e così via).
In questo modo l'albero deve essere attraversato solo una volta per essere ridipinto anziché più volte, dipingendo tutti gli sfondi, poi tutte le immagini, quindi tutti i bordi e così via.
Firefox ottimizza il processo non aggiungendo elementi che verranno nascosti, ad esempio elementi completamente sotto altri elementi opachi.
Spazio di archiviazione rettangolo WebKit
Prima della ricolorazione, WebKit salva il vecchio rettangolo come bitmap. Quindi, dipinge solo il delta tra il nuovo e il vecchio rettangolare.
Modifiche dinamiche
I browser cercano di eseguire il minor numero possibile di azioni possibili in risposta a un cambiamento. Di conseguenza, le modifiche al colore di un elemento comporteranno solo la ricolorazione dell'elemento. Le modifiche alla posizione dell'elemento comporteranno la modifica del layout e della ripetizione dell'elemento, dei relativi elementi secondari ed eventualmente di elementi di pari livello. L'aggiunta di un nodo DOM causerà il layout e la ricolorazione del nodo. Modifiche importanti, come l'aumento delle dimensioni del carattere dell'"html" causerà l'annullamento della convalida delle cache e il relayout e la ricolorazione dell'intero albero.
I thread del motore di rendering
Il motore di rendering è a thread singolo. Quasi tutto, tranne le operazioni di rete, avviene in un unico thread. In Firefox e Safari questo è il thread principale del browser. In Chrome si tratta del thread principale del processo relativo alle schede.
Le operazioni di rete possono essere eseguite da più thread paralleli. Il numero di connessioni parallele è limitato (di solito 2-6 connessioni).
Loop di eventi
Il thread principale del browser è un loop di eventi. È un ciclo infinito che mantiene attivo il processo. Attende gli eventi (come eventi di layout e colorazione) e li elabora. Questo è il codice di Firefox per il loop di eventi principale:
while (!mExiting)
NS_ProcessNextEvent(thread);
Modello visivo CSS2
La tela
In base alla specifica CSS2, il termine canvas descrive "lo spazio in cui viene visualizzata la struttura di formattazione", ovvero dove il browser visualizza i contenuti.
Il canvas è infinito per ogni dimensione dello spazio, ma i browser scelgono una larghezza iniziale in base alle dimensioni dell'area visibile.
Secondo www.w3.org/TR/CSS2/zindex.html, il canvas è trasparente se contenuto in un altro; in caso contrario gli viene fornito un colore definito dal browser.
Modello CSS Box
Il modello a riquadro CSS descrive le caselle rettangolari generate per gli elementi nell'albero dei documenti e disposte in base al modello di formattazione visiva.
Ogni casella include un'area di contenuti (ad es. testo, un'immagine e così via) e aree interne facoltative, bordi e margini.
Ciascun nodo genera n...e queste caselle.
Tutti gli elementi hanno un'intestazione "display" che determina il tipo di casella che verrà generato.
Esempi:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
Il valore predefinito è in linea, ma nel foglio di stile del browser potrebbero essere impostati altri valori predefiniti. Ad esempio: la visualizzazione predefinita per "div" è il blocco note.
Puoi trovare un esempio di foglio di stile predefinito qui: www.w3.org/TR/CSS2/sample.html.
Schema di posizionamento
Esistono tre schemi:
- Normale: l'oggetto viene posizionato in base alla sua posizione nel documento. Ciò significa che la sua posizione nell'albero di rendering è uguale a quella nell'albero DOM e posizionata in base al tipo di scatola e alle dimensioni
- Mobile: l'oggetto viene prima disposto come un flusso normale, poi spostato il più a sinistra o a destra possibile
- Assoluto: l'oggetto è inserito nell'albero di rendering in una posizione diversa rispetto all'albero DOM
Lo schema di posizionamento è impostato dal valore "position" e "float" .
- statici e relativi causano un flusso normale
- assoluto e fisso causano posizionamento assoluto
Nel posizionamento statico non viene definita alcuna posizione e viene utilizzato il posizionamento predefinito. Negli altri schemi, l'autore specifica la posizione: in alto, in basso, a sinistra, a destra.
Il modo in cui è strutturato il riquadro è determinato da:
- Tipo di casella
- Dimensioni della confezione
- Schema di posizionamento
- Informazioni esterne quali le dimensioni dell'immagine e dello schermo
Tipi di caselle
Casella di blocco: forma un blocco; ha un proprio rettangolo nella finestra del browser.
Casella in linea: non ha un blocco proprio, ma si trova all'interno di un blocco contenitore.
I blocchi vengono formattati verticalmente uno dopo l'altro. Le linee in linea sono formattate orizzontalmente.
Le caselle in linea vengono inserite all'interno di righe o "scatole lineari". Le linee sono alte almeno quanto la scatola più alta, ma possono essere più alte, quando i riquadri sono allineati "baseline" indica che la parte inferiore di un elemento è allineata in un punto di un'altra scatola diversa dal basso. Se la larghezza del container non è sufficiente, le linee in linea verranno disposte su più righe. Di solito è ciò che accade in un paragrafo.
Posizionamento della
Relativo
Posizionamento relativo: posizionato come al solito e poi spostato dal delta richiesto.
Float
Una casella mobile viene spostata a sinistra o a destra di una linea. La caratteristica interessante è che le altre scatole scorrono intorno. HTML:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
Sarà simile a:
Assoluto e fisso
Il layout viene definito esattamente a prescindere dal flusso normale. L'elemento non partecipa al flusso normale. Le dimensioni sono relative al contenitore. Per impostazione predefinita, il container è l'area visibile.
Rappresentazione a più livelli
Questo valore è specificato dalla proprietà CSS z-index. Rappresenta la terza dimensione del rettangolo, ovvero la sua posizione lungo l'"asse z".
I riquadri sono suddivisi in stack (chiamati contesti di sovrapposizione). In ogni pila, verranno prima dipinti gli elementi posteriori, mentre quelli anteriori nella parte superiore, più vicini all'utente. In caso di sovrapposizione, l'elemento più principale nasconde quello precedente.
Gli stack sono ordinati in base alla proprietà z-index. Riquadri con "z-index" in uno stack locale. L'area visibile ha lo stack esterno.
Esempio:
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div
style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
</div>
<div
style="z-index: 1;background-color:green;width: 2in; height: 2in;">
</div>
</p>
Il risultato sarà questo:
Anche se il tag div rosso precede quello verde nel markup e sarebbe stato già dipinto in precedenza nel flusso regolare, la proprietà z-index è più elevata, quindi è più in avanti nello stack trattenuta dalla radice.
Risorse
Architettura del browser
- Grosskurth, Alan. Architettura di riferimento per browser web (pdf)
- Gupta, Vineet. Come funzionano i browser - Parte 1 - Architettura
Analisi in corso...
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (noto anche come "il libro del drago"), Addison-Wesley, 1986
- Rick Jelliffe. Belle e belle: due nuove bozze per HTML 5.
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (video Google tech talk)
- L. David Baron, Mozilla Layout Engine
- L. David Baron, Documentazione sul sistema Mozilla Style
- Chris Waterson, Notes on HTML Reflow
- Chris Waterson, Gecko Overview
- Alexander Larsson, The life of an HTML HTTP request
WebKit
- David Hyatt, Implementazione di CSS(parte 1)
- David Hyatt, An Overview of WebCore
- David Hyatt, WebCore Rendering
- David Hyatt, The FOUC Problem
Specifiche W3C
Istruzioni per lo sviluppo dei browser
Traduzioni
Questa pagina è stata tradotta in giapponese due volte:
- Come funzionano i browser - Dietro le quinte dei browser web moderni (ja) di @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 di @ikeike443 e @kiyoto01.
Puoi visualizzare le traduzioni ospitate esternamente Coreano e Turco.
Grazie a tutti.