Dietro le quinte dei browser web moderni
Prefazione
Questa guida completa alle operazioni interne di WebKit e Gecko è il risultato di molte ricerche condotte dallo sviluppatore israeliano Tali Garsiel. Nel corso di alcuni anni, ha esaminato tutti i dati pubblicati sugli aspetti interni dei browser e ha dedicato molto tempo alla lettura del codice sorgente dei browser web. Ha scritto:
In qualità di sviluppatore web, apprendere i meccanismi interni delle operazioni del browser ti aiuta a prendere decisioni migliori e a comprendere le motivazioni alla base delle best practice di sviluppo. Anche se si tratta di un documento piuttosto lungo, ti consigliamo di dedicare un po' di tempo ad approfondirlo. Sarai felice di averlo fatto.
Paul Irish, Chrome Developer Relations
Introduzione
I browser web sono i software più utilizzati. In questa guida di base, spiego come funzionano dietro le quinte. Vedremo cosa succede quando digiti google.com nella barra degli indirizzi finché non vedi la pagina di Google nella schermata del browser.
Browser di cui parleremo
Oggi su computer vengono utilizzati cinque browser principali: 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 (a giugno 2013), Chrome, Firefox e Safari rappresentano circa il 71% dell'utilizzo globale dei browser desktop. Sui 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 contenuto. La posizione 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 dal W3C (World Wide Web Consortium), l'organizzazione che definisce gli standard per il web. Per anni i browser si sono conformati solo a una parte delle specifiche e hanno sviluppato le proprie estensioni. Ciò 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 molto in comune tra loro. Tra gli elementi comuni dell'interfaccia utente ci sono:
- Barra degli indirizzi per l'inserimento di un URI
- Pulsanti Indietro e Avanti
- Opzioni di aggiunta ai preferiti
- Pulsanti Aggiorna e Interrompi per aggiornare o interrompere il caricamento dei documenti correnti
- Pulsante Home che ti porta alla tua home page
Curiosamente, l'interfaccia utente del browser non è specificata in alcuna specifica formale, ma deriva semplicemente da buone pratiche plasmate da anni di esperienza e da browser che si imitano a vicenda. La specifica HTML5 non definisce gli elementi UI che un browser deve avere, ma elenca alcuni elementi comuni. Tra questi, la barra degli indirizzi, la barra di stato e la barra degli strumenti. Esistono, ovviamente, funzionalità uniche per un browser specifico, come Gestione dei download di Firefox.
Infrastruttura di alto livello
I componenti principali del browser sono:
- L'interfaccia utente: include la barra degli indirizzi, il pulsante Indietro/Avanti, il menu dei preferiti e così via. Ogni parte della visualizzazione del browser, ad eccezione della finestra in cui vedi la pagina richiesta.
- Il motore del browser: gestisce le 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 HTML e CSS e visualizza i contenuti analizzati sullo schermo.
- Networking: per le chiamate di rete, come le richieste HTTP, utilizzando 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 che non è specifica per la piattaforma. In background, utilizza i metodi dell'interfaccia utente del sistema operativo.
- Interprete JavaScript. Utilizzato per analizzare ed eseguire il codice JavaScript.
- Datastore. Si tratta di 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 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 è, beh… il rendering, ovvero la visualizzazione dei contenuti richiesti sullo schermo del browser.
Per impostazione predefinita, il motore di rendering può visualizzare documenti e immagini HTML e XML. Può visualizzare altri tipi di dati tramite plug-in o estensioni, ad esempio documenti PDF utilizzando un plug-in di visualizzazione PDF. Tuttavia, in questo capitolo ci concentreremo sul caso d'uso principale: la visualizzazione di HTML e immagini formattate utilizzando CSS.
Browser diversi utilizzano motori di rendering diversi: 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 è nato 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 rete. In genere, questa operazione viene eseguita in blocchi da 8 kB.
Dopodiché, il flusso di base del motore di rendering è il seguente:
Il motore di rendering inizierà ad analizzare il documento HTML e a convertire gli elementi in nodi DOM in un albero chiamato "albero dei contenuti". Il motore analizzerà i dati di stile, sia nei file CSS esterni sia negli elementi di stile. Le informazioni sullo stile, insieme alle 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, viene eseguito un processo di "layout". Ciò significa assegnare a ogni nodo le coordinate esatte in cui deve apparire sullo schermo. La fase successiva è la pittura: l'albero di rendering verrà attraversato e ogni nodo verrà dipinto utilizzando il livello di backend dell'interfaccia utente.
È importante capire che si tratta di un processo graduale. Per offrire un'esperienza utente migliore, il motore di rendering cercherà di visualizzare i contenuti sullo schermo il prima possibile. Non attende che tutto l'HTML venga analizzato prima di iniziare a creare e disporre l'albero di rendering. Parti dei contenuti verranno analizzate e visualizzate, mentre il processo continua con il resto dei contenuti che continuano ad arrivare dalla rete.
Esempi di flussi principali
Dalle figure 3 e 4 puoi vedere che, sebbene WebKit e Gecko utilizzino una terminologia leggermente diversa, il flusso è fondamentalmente lo stesso.
Gecko chiama l'albero degli elementi formattati visivamente "albero dei frame". Ogni elemento è un frame. WebKit utilizza il termine "albero di rendering" ed è composto da "oggetti di rendering". WebKit utilizza il termine "layout" per il posizionamento degli elementi, mentre Gecko lo chiama "Reflow". "Attachment" è il termine utilizzato da WebKit per indicare la connessione dei nodi DOM e delle 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. Si chiama "content sink" ed è una factory per la creazione di elementi DOM. Parleremo di ogni parte del flusso:
Analisi sintattica - Generale
Poiché l'analisi è un processo molto importante all'interno del motore di rendering, la esamineremo un po' più a fondo. Iniziamo con una breve introduzione all'analisi sintattica.
Analizzare un documento significa tradurlo in una struttura che il codice può utilizzare. Il risultato dell'analisi è in genere un albero di nodi che rappresentano la struttura del documento. Questo è chiamato albero di analisi o albero di sintassi.
Ad esempio, l'analisi dell'espressione 2 + 3 - 1 potrebbe restituire questo albero:
Grammatica
L'analisi si basa sulle regole di sintassi rispettate dal documento, ovvero la lingua o il formato in cui è stato scritto. Ogni formato che puoi analizzare deve avere una grammatica deterministica composta da regole di vocabolario e sintassi. Si tratta di una grammatica context-free. Le lingue umane non sono lingue di questo tipo e, pertanto, non possono essere analizzate con tecniche di analisi convenzionali.
Combinazione parser-lexer
L'analisi può essere suddivisa in due sottoprocessi: analisi lessicale e analisi sintattica.
L'analisi lessicale è il processo di suddivisione dell'input in token. I token sono il vocabolario della lingua: la raccolta di elementi costitutivi validi. Nel linguaggio umano, sarà composto da tutte le parole che compaiono nel dizionario di quella lingua.
L'analisi della sintassi consiste nell'applicazione delle regole di sintassi del linguaggio.
I parser di solito dividono il lavoro tra due componenti: il lexer (a volte chiamato tokenizer), che è responsabile della suddivisione dell'input in token validi, e il parser, che è responsabile della creazione dell'albero di analisi analizzando la struttura del documento in base alle regole di sintassi del linguaggio.
Il lexer sa come rimuovere i caratteri non pertinenti, come gli spazi vuoti e le interruzioni di riga.
Il processo di analisi è iterativo. In genere, l'analizzatore sintattico richiede un nuovo token all'analizzatore lessicale e tenta di abbinarlo a una delle regole di sintassi. Se viene trovata una corrispondenza con una regola, all'albero di analisi viene aggiunto un nodo corrispondente al token e l'analizzatore sintattico richiederà un altro token.
Se nessuna regola corrisponde, l'analizzatore memorizza il token internamente e continua a richiedere token finché non viene trovata una regola corrispondente a tutti i token memorizzati internamente. Se non viene trovata alcuna regola, l'analizzatore genererà un'eccezione. Ciò significa che il documento non era valido e conteneva errori di sintassi.
Traduzione
In molti casi, l'albero di analisi non è il prodotto finale. L'analisi viene spesso utilizzata nella traduzione: trasformazione del documento di input in un altro formato. Un esempio è la compilazione. Il compilatore che compila il codice sorgente in codice macchina lo analizza prima in un albero di analisi e poi lo traduce in un documento di codice macchina.
Esempio di analisi
Nella figura 5 abbiamo creato un albero di analisi da un'espressione matematica. Proviamo a definire un linguaggio matematico semplice e vediamo 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 un "termine" seguito da un'"operazione" seguita 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: in base alla regola n. 5, si tratta di un termine.
La seconda corrispondenza è 2 + 3: corrisponde alla terza regola, ovvero un termine seguito da un'operazione seguita da un altro termine.
La corrispondenza successiva verrà trovata 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 seguita da un altro termine.
2 + + non corrisponderà ad alcuna regola e pertanto è un input non valido.
Definizioni formali per vocabolario e sintassi
Il vocabolario è solitamente 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 è solitamente 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 un linguaggio può essere analizzato da parser regolari se la sua grammatica è una grammatica context-free. Una definizione intuitiva di grammatica context-free è una grammatica che può essere espressa interamente in BNF. Per una definizione formale, consulta l'articolo di Wikipedia sulla grammatica context-free.
Tipi di parser
Esistono due tipi di parser: parser top-down e parser bottom-up. Una spiegazione intuitiva è che i parser top-down esaminano la struttura di alto livello della sintassi e cercano di trovare una corrispondenza con una regola. I parser bottom-up iniziano con l'input e lo trasformano gradualmente nelle regole di sintassi, partendo dalle regole di basso livello fino a raggiungere quelle di alto livello.
Vediamo come i due tipi di parser analizzeranno il nostro esempio.
Il parser top-down inizierà dalla regola di livello superiore: identificherà 2 + 3 come espressione. Quindi, identificherà 2 + 3 - 1 come espressione (il processo di identificazione dell'espressione si evolve, corrispondendo alle altre regole, ma il punto di partenza è la regola di livello più alto).
Il parser bottom-up analizzerà l'input fino a quando non verrà trovata una corrispondenza con una regola. L'input corrispondente verrà quindi sostituito con la regola. Questo processo continuerà fino alla fine dell'input. L'espressione parzialmente corrispondente viene inserita nello stack dell'analizzatore sintattico.
Questo tipo di parser bottom-up è chiamato parser shift-reduce, perché l'input viene spostato a destra (immagina un puntatore che punta prima all'inizio dell'input e si sposta a destra) e viene gradualmente ridotto alle regole di sintassi.
Generazione automatica dei parser
Esistono strumenti che possono generare un parser. Fornisci loro la grammatica della tua lingua, ovvero il vocabolario e le regole di sintassi, e loro generano un parser funzionante. La creazione di un parser richiede una conoscenza approfondita dell'analisi sintattica e non è facile creare un parser ottimizzato manualmente, quindi i generatori di parser possono essere molto utili.
WebKit utilizza due generatori di parser ben noti: Flex per creare un lexer e Bison per creare un parser (potresti incontrarli con i nomi Lex e Yacc). L'input di Flex è un file contenente le definizioni delle espressioni regolari dei token. L'input di Bison è costituito dalle regole di sintassi del linguaggio in formato BNF.
Parser HTML
Il compito del parser HTML è analizzare il markup HTML in un albero di analisi.
Grammatica HTML
Il vocabolario e la sintassi di HTML sono definiti nelle specifiche create dall'organizzazione W3C.
Come abbiamo visto nell'introduzione all'analisi, la sintassi della grammatica può essere definita formalmente utilizzando formati come BNF.
Purtroppo, tutti gli argomenti relativi ai parser convenzionali non si applicano all'HTML (non li ho citati solo per divertimento, verranno utilizzati nell'analisi di CSS e JavaScript). L'HTML non può essere facilmente definito da una grammatica context-free di cui hanno bisogno i parser.
Esiste un formato formale per la definizione di HTML, ovvero DTD (Document Type Definition), ma non è una grammatica non contestuale.
A prima vista, questo sembra strano, dato che l'HTML è piuttosto simile all'XML. Sono disponibili molti parser XML. Esiste una variante XML dell'HTML, ovvero l'XHTML, quindi qual è la grande differenza?
La differenza è che l'approccio HTML è più "permissivo": ti consente di omettere determinati tag (che vengono poi aggiunti implicitamente) o, a volte, di omettere i tag di apertura o chiusura e così via. Nel complesso, si tratta di una sintassi "soft", a differenza della sintassi rigida e impegnativa di XML.
Questo dettaglio apparentemente piccolo fa un'enorme differenza. Da un lato, questo è il motivo principale per cui l'HTML è così popolare: perdona gli errori e semplifica la vita dell'autore web. D'altra parte, rende difficile scrivere una grammatica formale. In sintesi, l'HTML non può essere analizzato facilmente dai parser convenzionali, poiché la sua grammatica non è indipendente dal contesto. L'HTML non può essere analizzato dai parser XML.
DTD HTML
La definizione HTML è in formato DTD. Questo formato viene utilizzato per definire i linguaggi della famiglia SGML. Il formato contiene le definizioni di tutti gli elementi consentiti, dei relativi attributi e della gerarchia. Come abbiamo visto in precedenza, la DTD HTML non forma una grammatica context-free.
Esistono alcune varianti della DTD. La modalità StrictMode è conforme esclusivamente alle specifiche, ma altre modalità supportano il markup utilizzato dai browser in passato. Lo scopo è la compatibilità con le versioni precedenti dei contenuti. La DTD rigida corrente è disponibile qui: www.w3.org/TR/html4/strict.dtd
DOM
L'albero di output (l'"albero di analisi") è un albero di nodi di elementi e attributi DOM. DOM è l'abbreviazione di Document Object Model. È la rappresentazione dell'oggetto del documento HTML e l'interfaccia degli elementi HTML con il mondo esterno, come JavaScript.
La radice dell'albero è l'oggetto "Document".
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 verrebbe tradotto nel seguente albero DOM:
Come l'HTML, il DOM è specificato dall'organizzazione W3C. Consulta www.w3.org/DOM/DOMTR. Si tratta di una specifica generica per la manipolazione dei documenti. Un modulo specifico descrive elementi HTML specifici. 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, intendo dire che l'albero è costituito da elementi che implementano una delle interfacce DOM. I browser utilizzano implementazioni concrete che hanno altri attributi utilizzati internamente dal browser.
L'algoritmo di analisi
Come abbiamo visto nelle sezioni precedenti, l'HTML non può essere analizzato utilizzando i normali parser top-down o bottom-up.
I motivi sono:
- La natura permissiva del linguaggio.
- Il fatto che i browser abbiano una tolleranza agli errori tradizionale per supportare casi noti di HTML non valido.
- Il processo di analisi è rientrante. Per altri linguaggi, l'origine non cambia durante l'analisi, ma in HTML il codice dinamico (come gli elementi di script contenenti chiamate
document.write()) può aggiungere token aggiuntivi, quindi il processo di analisi modifica effettivamente l'input.
Non potendo utilizzare le normali tecniche di analisi, i browser creano parser specializzati per analizzare l'HTML.
L'algoritmo di analisi è descritto in dettaglio dalla specifica HTML5. L'algoritmo è composto da due fasi: tokenizzazione e costruzione dell'albero.
La tokenizzazione è l'analisi lessicale, che analizza l'input in token. Tra i token HTML sono inclusi i tag di apertura, i tag di chiusura, i nomi degli attributi e i valori degli attributi.
Il tokenizer riconosce il token, lo passa al costruttore dell'albero e consuma il carattere successivo per riconoscere il token successivo 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 questi caratteri. La decisione è influenzata dallo stato di tokenizzazione corrente e dallo stato di costruzione dell'albero. Ciò significa che lo stesso carattere consumato produrrà risultati diversi per lo stato successivo corretto, a seconda dello stato corrente. L'algoritmo è troppo complesso per essere descritto in dettaglio, quindi vediamo un semplice esempio che ci aiuterà a comprenderne 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 "Tag open state".
L'utilizzo di un carattere a-z comporta la creazione di un "token di tag di apertura" e lo stato viene modificato in "Stato del nome del tag".
Rimaniamo in questo stato finché non viene utilizzato 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 corrente e lo stato torna a "Stato dei dati".
Il tag <body> verrà trattato con gli stessi passaggi.
Finora sono stati emessi i tag html e body. Ora siamo tornati allo "Stato dei dati".
L'utilizzo del carattere H di Hello world causerà la creazione e l'emissione di un token di carattere, che continuerà fino al raggiungimento di < di </body>. Verrà emesso un token di carattere per ogni carattere di Hello world.
Ora siamo tornati allo "Stato di apertura del tag".
L'utilizzo dell'input successivo / causerà la creazione di un end tag token e il passaggio allo "stato del nome del tag". Anche in questo caso, rimaniamo in questo stato fino a quando non raggiungiamo >.Poi, viene emesso il nuovo token del tag e torniamo allo "Stato dei dati".
L'input </html> verrà trattato come nel caso precedente.
Algoritmo di costruzione dell'albero
Quando viene creato l'analizzatore, viene creato l'oggetto Document. Durante la fase di costruzione dell'albero, l'albero DOM con il documento nella radice verrà modificato e verranno aggiunti elementi. Ogni nodo emesso dal tokenizer 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 all'albero DOM e anche allo stack di elementi aperti. Questo stack viene utilizzato per correggere le mancate corrispondenze di nidificazione e i tag non chiusi. L'algoritmo è descritto anche come macchina a stati. Gli stati sono chiamati "modalità di inserimento".
Vediamo il processo di creazione dell'albero per l'input di esempio:
<html>
<body>
Hello world
</body>
</html>
L'input della fase di costruzione dell'albero è una sequenza di token della fase di tokenizzazione. La prima modalità è la "modalità iniziale". La ricezione del token "html" comporta il passaggio alla modalità "before html" e la rielaborazione del token in questa modalità. In questo modo verrà creato l'elemento HTMLHtmlElement, che verrà aggiunto all'oggetto Document principale.
Lo stato verrà modificato in "before head". Viene quindi ricevuto il token "body". Verrà creato implicitamente un HTMLHeadElement, anche se non abbiamo un token "head ", che verrà aggiunto all'albero.
Ora passiamo alla modalità "in head" e poi a "after head". Il token body viene rielaborato, viene creato e inserito un HTMLBodyElement e la modalità viene trasferita a "in body".
I token dei caratteri della stringa "Hello world" sono stati ricevuti. Il primo causerà la creazione e l'inserimento di un nodo "Text" e gli altri caratteri verranno aggiunti a quel nodo.
La ricezione del token di fine del corpo causerà un trasferimento alla modalità "after body". Ora riceveremo il tag di chiusura html che ci porterà alla modalità "after after body". La ricezione del token di fine file terminerà l'analisi.
Azioni al termine dell'analisi
In questa fase, il browser contrassegnerà il documento come interattivo e inizierà ad analizzare gli script in modalità "differita", ovvero quelli che devono essere eseguiti dopo l'analisi del documento. Lo stato del documento verrà quindi impostato su "complete" e verrà attivato un evento "load".
Puoi visualizzare gli algoritmi completi per la tokenizzazione e la creazione dell'albero nella specifica HTML5.
Tolleranza agli errori dei browser
Non riceverai mai un errore "Sintassi non valida" su una pagina HTML. I browser correggono i contenuti non validi e continuano.
Considera questo codice HTML, ad esempio:
<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 altro ancora), ma il browser lo mostra comunque correttamente e non segnala errori. Quindi, gran parte del codice del parser corregge gli errori dell'autore del codice HTML.
La gestione degli errori è abbastanza coerente nei browser, ma sorprendentemente non fa parte delle specifiche HTML. Come i preferiti e i pulsanti Indietro/Avanti, è semplicemente una funzionalità che si è sviluppata nei browser nel corso degli anni. Esistono costrutti HTML non validi noti che vengono ripetuti su molti siti e i browser cercano di correggerli in modo conforme ad altri browser.
La specifica HTML5 definisce alcuni di questi requisiti. (WebKit riassume bene questo concetto nel commento all'inizio della classe dell'analizzatore HTML).
Il parser analizza l'input tokenizzato nel documento, creando l'albero del documento. Se il documento è ben formato, l'analisi è semplice.
Purtroppo, dobbiamo gestire molti documenti HTML che non sono in un formato corretto, quindi l'analizzatore deve essere tollerante nei confronti degli errori.
Dobbiamo occuparci almeno delle seguenti condizioni di errore:
- L'elemento che stai aggiungendo è esplicitamente vietato all'interno di un tag esterno. In questo caso, dobbiamo chiudere tutti i tag fino a quello che vieta l'elemento e aggiungerlo in seguito.
- Non è consentito aggiungere l'elemento direttamente. È possibile che la persona che ha scritto il documento abbia dimenticato un tag intermedio (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ù alto.
- Se questo non aiuta, chiudi gli elementi finché non ci è consentito aggiungere l'elemento o ignora il tag.
Ecco alcuni esempi di tolleranza agli errori di WebKit:
</br> anziché <br>
Alcuni siti utilizzano </br> anziché <br>. Per essere compatibile con IE e Firefox, WebKit lo tratta come <br>.
Il codice:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
Tieni presente che la gestione degli errori è interna: non verrà presentata all'utente.
Una tabella isolata
Una tabella isolata è una tabella all'interno di un'altra tabella, ma non all'interno di una cella della tabella.
Ad esempio:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit cambierà 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 uno stack per i contenuti dell'elemento corrente: estrae la tabella interna dallo stack della tabella esterna. Le tabelle ora saranno elementi 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 html o body posizionati in modo errato
Anche in questo caso, il commento parla da sé.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
Quindi, gli autori web devono fare attenzione: a meno che non vogliano apparire come esempio in uno snippet di codice di tolleranza agli errori di WebKit, devono scrivere codice HTML ben formato.
Analisi CSS
Ricordi i concetti di analisi nell'introduzione? A differenza dell'HTML, il CSS è una grammatica libera dal contesto e può essere analizzato utilizzando i tipi di parser descritti nell'introduzione. Infatti, la specifica CSS definisce la grammatica lessicale e sintattica del CSS.
Vediamo alcuni esempi:
La grammatica lessicale (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 un nome di classe. "name" è un ID elemento (a cui viene fatto riferimento 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 questa 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 questo set di regole.
Questa struttura è 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 da spazi (S sta per spazio vuoto). Un set di regole contiene parentesi graffe e, al loro interno, una dichiarazione o, facoltativamente, un numero di dichiarazioni separate da un punto e virgola. "declaration" e "selector" verranno definiti nelle seguenti definizioni BNF.
Parser CSS WebKit
WebKit utilizza i generatori di parser Flex e Bison per creare automaticamente i parser dai file di grammatica CSS. Come ricorderai dall'introduzione al parser, Bison crea un parser shift-reduce bottom-up. Firefox utilizza un parser top-down scritto manualmente. In entrambi i casi, ogni file CSS viene analizzato in un oggetto StyleSheet. Ogni oggetto contiene regole CSS. Gli oggetti delle regole CSS contengono oggetti selettore e dichiarazione e altri oggetti corrispondenti alla grammatica CSS.
Ordine di elaborazione per script e fogli di stile
Script
Il modello del web è sincrono. Gli autori si aspettano che gli script vengano analizzati ed eseguiti immediatamente quando il parser raggiunge un tag <script>.
L'analisi del documento viene interrotta fino all'esecuzione dello script.
Se lo script è esterno, la risorsa deve prima essere recuperata dalla rete. Anche questa operazione viene eseguita in modo sincrono e l'analisi viene interrotta fino al recupero della risorsa.
Questo è stato il modello per molti anni ed è specificato anche nelle specifiche HTML4 e 5.
Gli autori possono aggiungere l'attributo "defer" a uno script, nel qual caso l'analisi del documento non verrà interrotta e verrà eseguita 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 e individua le altre risorse che devono essere caricate dalla rete e le carica. In questo modo, le risorse possono essere caricate su connessioni parallele e la velocità complessiva migliora. Nota: il parser speculativo analizza solo i riferimenti a risorse esterne come script, fogli di stile e immagini esterni; non modifica l'albero DOM, che viene lasciato al parser 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 ci sia motivo di attenderli e interrompere l'analisi del documento. Tuttavia, esiste un problema con gli script che richiedono informazioni sullo stile durante la fase di analisi del documento. Se lo stile non è ancora stato caricato e analizzato, lo script riceverà risposte errate e, a quanto pare, questo ha causato molti problemi. Sembra un caso limite, ma è abbastanza comune. Firefox blocca tutti gli script quando è presente un foglio di stile ancora in fase di caricamento e analisi. WebKit blocca gli script solo quando tentano di accedere a determinate proprietà di stile che potrebbero essere interessate da fogli di stile non caricati.
Costruzione dell'albero di rendering
Durante la creazione dell'albero DOM, il browser crea un altro albero, l'albero di rendering. Questo albero è composto da elementi visivi nell'ordine in cui verranno visualizzati. È la rappresentazione visiva del documento. Lo scopo di questo albero è consentire la visualizzazione dei contenuti nell'ordine corretto.
Firefox chiama gli elementi nell'albero di rendering "frame". WebKit utilizza il termine renderer o oggetto di rendering.
Un renderer sa come eseguire il layout e la visualizzazione di se stesso e dei suoi elementi secondari.
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 che di solito corrisponde a un riquadro CSS di un nodo, come descritto dalla specifica CSS2. Include informazioni geometriche come larghezza, altezza e posizione.
Il tipo di casella è influenzato dal valore "display" dell'attributo di stile pertinente al nodo (consulta la sezione Calcolo dello stile). Ecco il codice WebKit per decidere quale tipo di renderer deve essere creato per un nodo DOM, in base all'attributo display:
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 dei moduli e le tabelle hanno frame speciali.
In WebKit, se un elemento vuole creare un renderer speciale, esegue l'override del metodo createRenderer().
I renderer puntano a oggetti di stile che contengono informazioni non geometriche.
La relazione tra l'albero di rendering e 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 è l'elemento "head". Inoltre, gli elementi il cui valore di visualizzazione è stato assegnato a "none" non verranno visualizzati nell'albero (mentre gli elementi con visibilità "hidden" verranno visualizzati nell'albero).
Esistono elementi DOM che corrispondono a diversi oggetti visivi. In genere si tratta di elementi con una struttura complessa che non può essere descritta da un singolo rettangolo. Ad esempio, l'elemento "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 riga, le nuove righe verranno aggiunte come renderer aggiuntivi.
Un altro esempio di più renderer è l'HTML non valido. In base alla specifica CSS, un elemento incorporato deve contenere solo elementi di blocco o solo elementi incorporati. Nel caso di contenuti misti, verranno creati renderer di blocchi anonimi per aggregare gli elementi incorporati.
Alcuni oggetti di rendering corrispondono a un nodo DOM, ma non nella stessa posizione nell'albero. Gli elementi mobili e con posizionamento assoluto sono fuori dal flusso, posizionati in una parte diversa dell'albero e mappati al frame reale. Al loro posto, c'è un frame segnaposto.
Il flusso di costruzione dell'albero
In Firefox, la presentazione viene registrata come listener per gli aggiornamenti del DOM.
La presentazione delega la creazione del frame a FrameConstructor e il costruttore risolve lo stile (vedi Calcolo dello stile) e crea un frame.
In WebKit, il processo di risoluzione dello stile e di creazione di un renderer è chiamato "attachment". Ogni nodo DOM ha un metodo "attach". Il collegamento è sincrono, l'inserimento del nodo nell'albero DOM chiama il metodo "attach" del nuovo nodo.
L'elaborazione dei tag html e body comporta la creazione della radice dell'albero di rendering.
L'oggetto di rendering principale corrisponde a quello che la specifica CSS chiama blocco contenitore: il blocco più in 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 la chiama ViewPortFrame e WebKit la chiama RenderView.
Questo è l'oggetto di rendering a cui punta il documento.
Il resto dell'albero viene costruito come inserimento di nodi DOM.
Consulta la specifica CSS2 sul modello di elaborazione.
Calcolo dello stile
La creazione dell'albero di rendering richiede il calcolo delle proprietà visive di ogni oggetto di rendering. A questo scopo, vengono calcolate le proprietà di stile di ogni elemento.
Lo stile include fogli di stile di varie origini, elementi di stile incorporati e proprietà visive nell'HTML (come la proprietà "bgcolor").Quest'ultima viene tradotta in 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, ovvero i fogli di stile forniti dall'utente del browser (i browser consentono di definire gli stili preferiti). In Firefox, ad esempio, questo viene fatto inserendo un foglio di stile nella cartella "Profilo Firefox").
Il calcolo dello stile presenta alcune difficoltà:
- I dati di stile sono un costrutto molto grande, che contiene numerose proprietà di stile, il che può causare problemi di memoria.
Se non è ottimizzata, la ricerca delle regole corrispondenti per ogni elemento può causare problemi di prestazioni. Scorrere l'intero elenco di regole per ogni elemento per trovare le corrispondenze è un'attività complessa. I selettori possono avere una struttura complessa che può far sì che il processo di corrispondenza inizi su un percorso apparentemente promettente che si rivela inutile e che deve essere provato un altro percorso.
Ad esempio, questo selettore composto:
div div div div{ ... }Significa che le regole si applicano a un elemento
<div>che è discendente di tre elementi div. Supponiamo che tu voglia verificare se la regola si applica a un determinato elemento<div>. Scegli un determinato percorso lungo l'albero per il controllo. Potresti dover attraversare l'albero dei nodi verso l'alto solo per scoprire che ci sono solo due div e la regola non si applica. Devi quindi provare altri percorsi nell'albero.L'applicazione delle regole prevede regole di propagazione piuttosto complesse che definiscono la gerarchia delle regole.
Vediamo come 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 di pari livello o di livello inferiore e:
- Gli elementi devono trovarsi nello stesso stato del mouse (ad esempio, uno non può essere in :hover mentre l'altro non lo è)
- Nessun elemento deve avere un ID
- I nomi dei tag devono corrispondere
- Gli attributi della classe devono corrispondere
- L'insieme di attributi mappati deve essere identico
- Gli stati dei link devono corrispondere
- Gli stati attivi devono corrispondere
- Nessuno dei due elementi deve essere interessato dai selettori di attributi, dove "interessato" è definito come avente una corrispondenza di selettore che utilizza un selettore di attributi in qualsiasi posizione all'interno del selettore.
- Non deve essere presente alcun attributo di stile in linea sugli elementi
- Non devono essere utilizzati selettori di elementi di pari livello. WebCore attiva semplicemente un'opzione globale quando viene rilevato un selettore di elementi di pari livello e disattiva la condivisione degli stili per l'intero documento quando sono presenti. Sono inclusi il selettore + e selettori come :first-child e :last-child.
Albero delle regole di Firefox
Firefox ha due alberi aggiuntivi per facilitare il calcolo dello stile: l'albero delle regole e l'albero del contesto di stile. Anche WebKit ha oggetti di stile, ma non sono archiviati in un albero come l'albero del contesto di stile, solo il nodo DOM punta allo stile pertinente.
I contesti di stile contengono i valori finali. I valori vengono calcolati applicando tutte le regole di corrispondenza nell'ordine corretto ed eseguendo manipolazioni che li 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 è davvero intelligente. Consente di condividere questi valori tra i nodi per evitare di ricalcolarli. In questo modo, risparmierai anche spazio.
Tutte le regole corrispondenti vengono archiviate in un albero. I nodi inferiori di un percorso hanno una priorità più alta. L'albero contiene tutti i percorsi per le corrispondenze delle regole trovate. La memorizzazione delle regole viene eseguita in modo lazy. 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 è di vedere i percorsi dell'albero come parole in un lessico. Supponiamo di aver già calcolato questo albero delle regole:
Supponiamo di dover trovare le regole corrispondenti per un altro elemento nell'albero dei contenuti e di scoprire che le regole corrispondenti (nell'ordine corretto) sono B-E-I. Questo percorso è già presente nell'albero perché abbiamo già calcolato il percorso A-B-E-I-L. Ora avremo meno lavoro da fare.
Vediamo come l'albero ci risparmia lavoro.
Suddivisione in struct
I contesti di stile sono suddivisi in strutture. Queste strutture contengono informazioni sullo stile per una determinata categoria, ad esempio il bordo o il colore. Tutte le proprietà di una struttura sono 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 i valori predefiniti se non sono definite.
L'albero ci aiuta memorizzando nella cache intere strutture (contenenti i valori finali calcolati) nell'albero. L'idea è che se il nodo inferiore non ha fornito una definizione per una struttura, è possibile utilizzare una struttura memorizzata nella cache in un nodo superiore.
Calcolo dei contesti di stile utilizzando l'albero delle regole
Quando calcoliamo il contesto di stile per un determinato elemento, calcoliamo prima un percorso nell'albero delle regole o ne utilizziamo uno esistente. Iniziamo quindi ad applicare le regole nel percorso per compilare le strutture nel nostro nuovo contesto di 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 quando la nostra struttura non è piena. Se non esiste una specifica per la struttura in quel nodo della regola, possiamo ottimizzare notevolmente: risaliamo l'albero fino a trovare un nodo che la specifica completamente e puntiamo a questo. Questa è l'ottimizzazione migliore: l'intera struttura è condivisa. In questo modo, si risparmiano memoria e calcolo dei valori finali.
Se troviamo definizioni parziali, risaliamo l'albero fino a quando la struttura non viene compilata.
Se non troviamo definizioni per la nostra struct, nel caso in cui la struct sia un tipo "ereditato ", facciamo riferimento alla struct del nostro elemento principale nell'albero di contesto. In questo caso siamo anche riusciti a condividere gli struct. Se si tratta di una struttura di reset, verranno utilizzati i valori predefiniti.
Se il nodo più specifico aggiunge valori, dobbiamo eseguire alcuni calcoli aggiuntivi per trasformarli in valori effettivi. Quindi memorizziamo nella cache il risultato nel nodo dell'albero in modo che possa essere utilizzato dai nodi secondari.
Se un elemento ha un elemento di pari livello o un elemento fratello che punta allo stesso nodo dell'albero, l'intero contesto di stile può essere condiviso tra loro.
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 del colore e lo struct del margine. La struttura del colore contiene un solo membro: il colore. La struttura del margine contiene i quattro lati.
L'albero delle regole risultante sarà simile al seguente (i nodi sono contrassegnati con il nome del nodo: il numero della regola a cui puntano):
L'albero del contesto sarà simile al seguente (nome del nodo: nodo della regola a cui punta):
Supponiamo di analizzare il codice HTML e di arrivare al secondo tag <div>. Dobbiamo creare un contesto di stile per questo nodo e compilare le relative strutture di stile.
Confronteremo le regole e scopriremo che le regole corrispondenti per <div> sono 1, 2 e 6.
Ciò significa che nell'albero esiste già un percorso che il nostro elemento può utilizzare e 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 nuovo contesto di stile farà riferimento al nodo F nell'albero delle regole.
Ora dobbiamo compilare le strutture di stile. Inizieremo compilando la struttura del margine. Poiché l'ultimo nodo della regola (F) non aggiunge elementi alla struttura del margine, possiamo risalire l'albero fino a trovare una struttura memorizzata nella cache calcolata in un inserimento di nodo precedente e utilizzarla. Lo troveremo sul nodo B, che è il nodo più in alto che ha specificato le regole di margine.
Abbiamo una definizione per lo struct del colore, quindi non possiamo utilizzare uno struct memorizzato nella cache. Poiché il colore ha un attributo, non è necessario risalire l'albero per compilare altri attributi. Calcoleremo il valore finale (convertiremo la stringa in RGB e così via) e memorizzeremo nella cache la struttura calcolata su questo nodo.
Il lavoro sul secondo elemento <span> è ancora più semplice. Confronteremo le regole e giungeremo alla conclusione che punta alla regola G, come lo span precedente.
Poiché abbiamo elementi di pari livello che puntano allo stesso nodo, possiamo condividere l'intero contesto di stile e puntare semplicemente al contesto dello span precedente.
Per le strutture che contengono regole ereditate dall'elemento principale, la memorizzazione nella cache viene eseguita nell'albero del contesto (la proprietà del colore viene effettivamente ereditata, ma Firefox la tratta come reimpostata e la memorizza nella cache nell'albero delle regole).
Ad esempio, se aggiungiamo regole per i caratteri in un paragrafo:
p {font-family: Verdana; font size: 10px; font-weight: bold}
In questo modo, l'elemento paragrafo, che è un elemento secondario del div nell'albero del contesto, avrebbe potuto condividere lo stesso struct del carattere dell'elemento principale. Questo accade se non sono state specificate regole per i caratteri del paragrafo.
In WebKit, che non ha un albero delle regole, le dichiarazioni corrispondenti vengono attraversate quattro volte. Vengono applicate prima le proprietà non importanti con priorità alta (proprietà che devono essere applicate per prime perché altre dipendono da loro, ad esempio display), poi quelle importanti con priorità alta, poi quelle non importanti con priorità normale e infine le regole importanti con priorità normale. Ciò significa che le proprietà che compaiono più volte verranno risolte in base all'ordine di applicazione corretto. L'ultimo vince.
In sintesi, la condivisione degli oggetti di stile (interamente o di alcune delle strutture al loro interno) risolve i problemi 1 e 3. Anche l'albero delle regole di Firefox aiuta ad applicare le proprietà nell'ordine corretto.
Manipolare le regole per una partita facile
Le regole di stile possono provenire da diverse fonti:
- 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 alle regole di stile pertinenti)
html <p bgcolor="blue" />Gli ultimi due sono facilmente abbinabili all'elemento, poiché quest'ultimo possiede gli attributi di stile e gli attributi HTML possono essere mappati utilizzando l'elemento come chiave.
Come indicato in precedenza nel problema n. 2, la corrispondenza delle regole CSS può essere più complessa. Per risolvere il problema, le regole vengono manipolate per facilitare l'accesso.
Dopo l'analisi del foglio di stile, le regole vengono aggiunte a una delle diverse mappe hash, in base al selettore. Esistono mappe per ID, per nome della classe, per nome del 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 degli ID, se è una classe verrà aggiunta alla mappa delle classi e così via.
Questa manipolazione semplifica notevolmente la corrispondenza delle regole. Non è necessario esaminare ogni dichiarazione: possiamo estrarre le regole pertinenti per un elemento dalle mappe. Questa ottimizzazione elimina oltre il 95% delle regole, in modo che non debbano nemmeno essere 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 della classe. La seconda nella mappa degli ID e la terza nella mappa dei tag.
Per il seguente frammento HTML:
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
Innanzitutto, cercheremo di trovare le regole per l'elemento p. La mappa delle classi conterrà una chiave "error" in cui si trova la regola per "p.error". L'elemento div avrà regole pertinenti nella mappa degli ID (la chiave è l'ID) e nella mappa dei tag. Quindi l'unico lavoro rimasto è scoprire quali delle regole estratte dalle chiavi corrispondono effettivamente.
Ad esempio, se la regola per il div era:
table div {margin: 5px}
Verrà comunque estratto dalla mappa dei tag, perché la chiave è il selettore più a destra, ma non corrisponderà al nostro elemento div, che non ha un predecessore table.
Sia WebKit che Firefox eseguono questa manipolazione.
Ordine di cascata dei fogli di stile
L'oggetto style 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. Altre proprietà hanno valori predefiniti.
Il problema sorge quando esiste più di una definizione: è qui che entra in gioco l'ordine di applicazione a cascata per risolvere il problema.
Una dichiarazione per 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 è l'ordine "a cascata". In base alla specifica CSS2, l'ordine di priorità è (dal più basso al più alto):
- Dichiarazioni del browser
- Dichiarazioni normali dell'utente
- Dichiarazioni normali dell'autore
- Dichiarazioni importanti dell'autore
- Dichiarazioni importanti dell'utente
Le dichiarazioni del browser sono le meno importanti e l'utente sostituisce l'autore solo se la dichiarazione è stata contrassegnata come importante. Le dichiarazioni con lo stesso ordine verranno ordinate in base alla specificità e poi all'ordine in cui sono specificate. Gli attributi visivi HTML vengono tradotti in dichiarazioni CSS corrispondenti . Vengono trattate come regole dell'autore con priorità bassa.
Specificità
La specificità del selettore è definita dalla specifica CSS2 come segue:
- conteggio 1 se la dichiarazione da cui proviene è un attributo "style" anziché una regola con un selettore, altrimenti 0 (= a)
- Conta il numero di attributi ID nel selettore (= b)
- conta il numero di altri attributi e pseudo-classi nel selettore (= c)
- conta il numero di nomi di elementi e pseudo-elementi nel selettore (= d)
La concatenazione dei quattro numeri a-b-c-d (in un sistema numerico con una base grande) fornisce la specificità.
La base numerica da utilizzare è definita dal conteggio più alto in una delle categorie.
Ad esempio, se a=14, puoi utilizzare la base esadecimale. Nell'improbabile caso in cui a=17, avrai bisogno di una base numerica di 17 cifre. Quest'ultima situazione può verificarsi con un selettore come questo: html body div div p… (17 tag nel selettore… non molto probabile).
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
Dopo che le regole sono state abbinate, vengono ordinate in base alle regole di applicazione a cascata.
WebKit utilizza l'ordinamento a bolle per gli elenchi di piccole dimensioni e l'ordinamento per fusione per quelli di grandi dimensioni.
WebKit implementa l'ordinamento eseguendo l'override dell'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 indica se sono stati caricati tutti i fogli di stile di primo livello (inclusi gli @import). Se lo stile non è completamente caricato durante l'allegato, vengono utilizzati i segnaposto e vengono contrassegnati nel documento, quindi verranno ricalcolati una volta caricati i fogli di stile.
Layout
Quando il renderer viene creato e aggiunto all'albero, non ha una posizione e una dimensione. Il calcolo di questi valori è chiamato layout o ridisposizione.
HTML utilizza un modello di layout basato sul flusso, il che significa che nella maggior parte dei casi è possibile calcolare la geometria in un unico passaggio. Gli elementi che si trovano più avanti "nel flusso" in genere non influiscono sulla geometria degli elementi che si trovano prima "nel flusso ", pertanto il layout può procedere da sinistra a destra e dall'alto verso il basso nel documento. Esistono delle eccezioni: ad esempio, le tabelle HTML potrebbero richiedere più di un passaggio.
Il sistema di coordinate è relativo al frame principale. Vengono utilizzate le coordinate superiore e sinistra.
Il layout è un processo ricorsivo. Inizia dal renderer principale, che corrisponde all'elemento <html> del documento HTML. Il layout continua in modo ricorsivo attraverso una parte o l'intera gerarchia dei frame, calcolando le informazioni geometriche per ogni renderer che le richiede.
La posizione del renderer principale è 0,0 e le sue dimensioni sono l'area visibile, ovvero la parte visibile della finestra del browser.
Tutti i renderer hanno un metodo "layout" o "reflow" e ogni renderer richiama il metodo layout dei relativi elementi secondari che richiedono il layout.
Sistema di bit dirty
Per evitare di eseguire un layout completo per ogni piccola modifica, i browser utilizzano un sistema di "dirty bit". Un renderer modificato o aggiunto contrassegna se stesso e i suoi elementi secondari come "dirty", ovvero che richiedono il layout.
Esistono due flag: "dirty" e "children are dirty", il che significa che, sebbene il renderer stesso possa essere OK, ha almeno un elemento secondario che richiede un layout.
Layout globale e incrementale
Il layout può essere attivato sull'intero albero di rendering: si tratta del layout "globale". Ciò può accadere a causa di:
- Una modifica dello stile globale che influisce su tutti i renderer, ad esempio una modifica delle dimensioni dei caratteri.
- In seguito al ridimensionamento di uno schermo
Il layout può essere incrementale, verranno disposti 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 dirty. Ad esempio, quando vengono aggiunti nuovi renderer all'albero di rendering dopo che sono stati ricevuti contenuti aggiuntivi dalla rete e aggiunti all'albero DOM.
Layout asincrono e sincrono
Il layout incrementale viene eseguito in modo asincrono. Firefox mette in coda i "comandi di reflow" per i layout incrementali e uno scheduler attiva l'esecuzione in batch di questi comandi. WebKit ha anche un timer che esegue un layout incrementale: l'albero viene attraversato e i renderer "dirty" vengono disposti.
Gli script che richiedono informazioni sullo stile, come "offsetHeight", possono attivare il layout incrementale in modo sincrono.
In genere, il layout globale viene 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 da una modifica della posizione del renderer(e non delle dimensioni), le dimensioni dei renderer vengono prese da una cache e non ricalcolate…
In alcuni casi, viene modificato solo un sottoalbero e il layout non inizia dalla radice. Ciò può accadere nei casi in cui la modifica è locale e non influisce sull'ambiente circostante, ad esempio il testo inserito nei campi di testo (altrimenti ogni pressione di tasto attiverebbe un layout a partire dalla radice).
La procedura di layout
Il layout di solito ha il seguente pattern:
- Il renderer principale determina la propria larghezza.
- Il genitore esamina i figli e:
- Posiziona il renderer secondario (imposta x e y).
- Chiama il layout secondario se necessario (se è dirty, se siamo in un layout globale o per qualche altro motivo), che calcola l'altezza del layout secondario.
- Il componente principale utilizza le altezze cumulative dei componenti secondari e le altezze dei margini e del padding per impostare la propria altezza, che verrà utilizzata dal componente principale del renderer principale.
- Imposta il relativo dirty bit su false.
Firefox utilizza un oggetto "state" (nsHTMLReflowState) come parametro per il layout (denominato "reflow"). Tra gli altri, lo stato include la larghezza dell'elemento principale.
L'output del layout di Firefox è un oggetto "metrics" (nsHTMLReflowMetrics). Contiene l'altezza calcolata del renderer.
Calcolo della larghezza
La larghezza del renderer viene calcolata utilizzando la larghezza del blocco contenitore, la proprietà "width" dello stile del renderer, i margini e i bordi.
Ad esempio, la larghezza del seguente div:
<div style="width: 30%"/>
Viene calcolato da WebKit come segue(metodo calcWidth della classe RenderBox):
- La larghezza del contenitore è il valore massimo tra availableWidth dei contenitori e 0. In questo caso, availableWidth è contentWidth, che viene calcolato come segue:
clientWidth() - paddingLeft() - paddingRight()
clientWidth e clientHeight rappresentano l'interno di un oggetto, esclusi il bordo e la barra di scorrimento.
La larghezza degli elementi è l'attributo di stile "width". Verrà calcolato come valore assoluto calcolando la percentuale della larghezza del contenitore.
I bordi e i padding orizzontali sono stati aggiunti.
Finora abbiamo calcolato la "larghezza preferita". Ora verranno calcolate le larghezze minima e massima.
Se la larghezza preferita è maggiore della larghezza massima, viene utilizzata quest'ultima. Se è inferiore alla larghezza minima (l'unità indivisibile più piccola), viene utilizzata la larghezza minima.
I valori vengono memorizzati nella cache nel caso in cui sia necessario un layout, ma la larghezza non cambia.
Interruzione di riga
Quando un renderer nel mezzo di un layout decide che deve interrompersi, si ferma e comunica al layout principale che deve essere interrotto. Il genitore crea i renderer aggiuntivi e chiama il layout su di essi.
Pittura
Nella fase di pittura, l'albero di rendering viene attraversato e viene chiamato il metodo "paint()" del renderer per visualizzare i contenuti sullo schermo. La pittura utilizza il componente dell'infrastruttura UI.
Globale e incrementale
Come il layout, anche la pittura può essere globale (viene dipinto l'intero albero) o incrementale. Nel disegno incrementale, alcuni dei renderer cambiano in un modo che non influisce sull'intero albero. Il renderer modificato invalida il suo rettangolo sullo schermo. In questo modo, il sistema operativo la considera una "regione dirty" e genera un evento "paint". Il sistema operativo lo fa in modo intelligente e unisce diverse regioni in una sola. In Chrome è più complicato perché il renderer si trova in un processo diverso rispetto a 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. Si ridisegnerà (e di solito ridisegnerà anche i suoi elementi secondari).
L'ordine di disegno
CSS2 definisce l'ordine del processo di disegno. Questo è l'ordine in cui gli elementi sono impilati nei contesti di impilamento. Questo ordine influisce sulla pittura, poiché gli stack vengono dipinti dal retro verso il fronte. L'ordine di sovrapposizione di un renderer di blocchi è:
- 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 rettangolo disegnato. Contiene i renderer pertinenti per il rettangolo, nell'ordine di disegno corretto (sfondi dei renderer, poi bordi e così via).
In questo modo, l'albero deve essere attraversato una sola volta per un ridisegno anziché più volte: vengono dipinti tutti gli sfondi, poi tutte le immagini, poi tutti i bordi e così via.
Firefox ottimizza il processo non aggiungendo elementi che verranno nascosti, come quelli completamente sotto altri elementi opachi.
Spazio di archiviazione dei rettangoli WebKit
Prima di ridisegnare, WebKit salva il vecchio rettangolo come bitmap. Poi, disegna solo il delta tra i rettangoli nuovi e quelli precedenti.
Modifiche dinamiche
I browser cercano di eseguire il minor numero possibile di azioni in risposta a una modifica. Pertanto, le modifiche al colore di un elemento causeranno solo la ridisegnazione dell'elemento. Le modifiche alla posizione dell'elemento causeranno il layout e la ridisegnazione dell'elemento, dei relativi elementi secondari ed eventualmente di quelli di pari livello. L'aggiunta di un nodo DOM comporta il layout e la ridisegnazione del nodo. Modifiche importanti, come l'aumento delle dimensioni del carattere dell'elemento "html", causeranno l'annullamento della convalida delle cache, il ricalcolo del layout e la ridisegnazione dell'intero albero.
I thread del motore di rendering
Il motore di rendering è single-thread. Quasi tutto, ad eccezione delle operazioni di rete, avviene in un unico thread. In Firefox e Safari, si tratta del thread principale del browser. In Chrome, è il thread principale del processo della scheda.
Le operazioni di rete possono essere eseguite da diversi thread paralleli. Il numero di connessioni parallele è limitato (in genere da 2 a 6 connessioni).
Loop di eventi
Il thread principale del browser è un loop di eventi. Si tratta di un loop infinito che mantiene attivo il processo. Attende gli eventi (come gli eventi di layout e paint) e li elabora. Questo è il codice di Firefox per il loop di eventi principale:
while (!mExiting)
NS_ProcessNextEvent(thread);
Modello visivo CSS2
Il canvas
Secondo la specifica CSS2, il termine canvas descrive "lo spazio in cui viene eseguito il rendering della 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 all'interno di un altro, altrimenti gli viene assegnato un colore definito dal browser.
Modello di box CSS
Il modello di box CSS descrive i box rettangolari generati per gli elementi nell'albero del documento e disposti in base al modello di formattazione visiva.
Ogni casella ha un'area dei contenuti (ad es. testo, un'immagine e così via) e aree di spaziatura interna, bordo e margine circostanti facoltative.
Ogni nodo genera da 0 a n caselle di questo tipo.
Tutti gli elementi hanno una proprietà "display" che determina il tipo di casella che verrà generata.
Esempi:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
Il valore predefinito è inline, ma il foglio di stile del browser potrebbe impostare altri valori predefiniti. Ad esempio, la visualizzazione predefinita per l'elemento "div" è block.
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 è simile alla sua posizione nell'albero DOM e viene disposta in base al tipo e alle dimensioni della casella.
- Float: l'oggetto viene prima disposto come nel flusso normale, poi spostato il più possibile a sinistra o a destra
- Assoluto: l'oggetto viene inserito nell'albero di rendering in una posizione diversa rispetto all'albero DOM
Lo schema di posizionamento è impostato dalla proprietà "position" e dall'attributo "float".
- static e relative causano un flusso normale
- absolute e fixed causano il 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 layout della casella è determinato da:
- Tipo di box
- Dimensioni della confezione
- Schema di posizionamento
- Informazioni esterne, come le dimensioni dell'immagine e dello schermo
Tipi di box
Casella di blocco: forma un blocco e ha un proprio rettangolo nella finestra del browser.
Riquadro in linea: non ha un proprio blocco, ma si trova all'interno di un blocco contenitore.
I blocchi sono formattati verticalmente uno dopo l'altro. Gli elementi incorporati sono formattati orizzontalmente.
Le caselle in linea vengono inserite all'interno di righe o "caselle di riga". Le righe sono alte almeno quanto la casella più alta, ma possono essere più alte quando le caselle sono allineate "baseline", ovvero la parte inferiore di un elemento è allineata in un punto di un'altra casella diverso dalla parte inferiore. Se la larghezza del container non è sufficiente, gli elementi in linea verranno inseriti su più righe. In genere, questo è ciò che accade in un paragrafo.
Presentazione
Relativo
Posizionamento relativo: posizionato come al solito e poi spostato del delta richiesto.
Float
Una casella mobile viene spostata a sinistra o a destra di una riga. La caratteristica interessante è che le altre caselle si dispongono intorno. Il codice 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 è definito esattamente indipendentemente dal flusso normale. L'elemento non partecipa al flusso normale. Le dimensioni sono relative al contenitore. In fixed, il container è l'area visibile.
Rappresentazione a livelli
Questo viene specificato dalla proprietà CSS z-index. Rappresenta la terza dimensione della casella: la sua posizione lungo l'"asse Z".
Le caselle sono divise in stack (chiamati contesti di impilamento). In ogni pila, gli elementi posteriori verranno visualizzati per primi e gli elementi anteriori in alto, più vicini all'utente. In caso di sovrapposizione, l'elemento in primo piano nasconderà quello precedente.
Le pile sono ordinate in base alla proprietà z-index. Le caselle con la proprietà "z-index" formano 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à il seguente:
Sebbene il div rosso preceda quello verde nel markup e sarebbe stato dipinto prima nel flusso normale, la proprietà z-index è più alta, quindi è più avanti nello stack contenuto dalla casella principale.
Risorse
Architettura del browser
- Grosskurth, Alan. A Reference Architecture for Web Browsers (pdf)
- Gupta, Vineet. How Browsers Work - Part 1 - Architecture
Analisi sintattica
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (noto anche come "Dragon book"), Addison-Wesley, 1986
- Rick Jelliffe. The Bold and the Beautiful: 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 Sviluppatori web (video di un tech talk di Google)
- L. David Baron, Mozilla's Layout Engine
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Notes on HTML Reflow
- Chris Waterson, Gecko Overview
- Alexander Larsson, The life of an HTML HTTP request
WebKit
- David Hyatt, Implementing CSS(part 1)
- David Hyatt, An Overview of WebCore
- David Hyatt, WebCore Rendering
- David Hyatt, The FOUC Problem
Specifiche W3C
Istruzioni di build dei browser
Traduzioni
Questa pagina è stata tradotta in giapponese due volte:
- How Browsers Work - Dietro le quinte dei moderni browser web (ja) di @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 by @ikeike443 and @kiyoto01.
Puoi visualizzare le traduzioni in coreano e turco ospitate esternamente.
Grazie a tutti.