Dietro le quinte dei browser web moderni
Prefazione
Questo prontuario completo sulle 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 sull'interno del browser e ha dedicato molto tempo a leggere il codice sorgente del browser web. Ha scritto:
In qualità di sviluppatore web, conoscere le operazioni interne dei browser ti aiuta a prendere decisioni migliori e a conoscere le giustificazioni alla base delle best practice di sviluppo. Sebbene si tratti di un documento piuttosto lungo, ti consigliamo di dedicarvi un po' di tempo. Sarai felice di averlo fatto.
Paul Irish, Chrome Developer Relations
Introduzione
I browser web sono il software più utilizzato. In questa guida introduttiva, spiegherò
come funzionano dietro le quinte. Vedremo cosa succede quando digiti google.com
nella barra degli indirizzi finché non viene visualizzata la pagina Google nella schermata del browser.
Browser di cui parleremo
Attualmente esistono cinque browser principali utilizzati su 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 dei browser open source Firefox e Chrome e di Safari (che è parzialmente open source). Secondo le statistiche di StatCounter (a giugno 2013), Chrome, Firefox e Safari rappresentano circa il 71% dell'utilizzo dei browser desktop a livello globale. Sui dispositivi mobili, il browser Android, iPhone e Chrome rappresentano 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 mostrandola 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 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 dall'organizzazione W3C (World Wide Web Consortium), che è l'organizzazione di 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 molti aspetti in comune tra loro. Tra gli elementi comuni dell'interfaccia utente sono inclusi:
- Barra degli indirizzi per l'inserimento di un URI
- Pulsanti Avanti e Indietro
- Opzioni di aggiunta ai preferiti
- Pulsanti Aggiorna e Interrompi per aggiornare o interrompere il caricamento dei documenti correnti
- Pulsante Home che ti reindirizza alla home page
Stranamente, l'interfaccia utente del browser non è specificata in nessuna specifica formale, ma deriva da buone pratiche sviluppate nel corso di 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 queste, la barra degli indirizzi, la barra di stato e la barra degli strumenti. Naturalmente, esistono funzionalità uniche per 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 dei 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: organizza 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 in HTML, il motore di rendering analizza HTML e CSS e mostra i contenuti analizzati sullo schermo.
- Networking: per le chiamate di rete, come le richieste HTTP, vengono utilizzate implementazioni diverse per piattaforme diverse dietro un'interfaccia indipendente dalla piattaforma.
- Backend dell'interfaccia utente: utilizzato per disegnare widget di base come caselle combinate e finestre. Questo backend espone un'interfaccia generica non specifica della piattaforma. Sotto utilizza i metodi dell'interfaccia utente del sistema operativo.
- Interprete JavaScript. Utilizzato per analizzare ed eseguire il codice JavaScript.
- Archiviazione dei dati. 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 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 documenti e immagini 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 visualizzatori 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 e Safari utilizza WebKit. Chrome e Opera (dalla versione 15) utilizzano Blink, un fork di WebKit.
WebKit è un motore di rendering open source nato come motore per la piattaforma Linux e 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, questo viene fatto in blocchi di 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 giusto per essere visualizzati sullo schermo.
Dopo la costruzione dell'albero di rendering, viene seguito un processo di "layout". Ciò significa assegnare a ogni nodo le coordinate esatte in cui deve apparire sullo schermo. La fase successiva è la tinta: la struttura di rendering verrà attraversata e ogni nodo verrà dipinto utilizzando il livello di backend dell'interfaccia utente.
È importante capire che si tratta di un processo graduale. Per un'esperienza utente migliore, il motore di rendering cercherà di visualizzare i contenuti sullo schermo il prima possibile. Non attende che tutto il codice HTML venga analizzato prima di iniziare a creare e a eseguire il layout della struttura di rendering. Alcune parti dei contenuti verranno analizzate e visualizzate, mentre il processo continua con il resto dei contenuti che continuano ad arrivare 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.
Gecko chiama "albero di frame" l'albero degli elementi visivamente formattati. 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" è il termine di WebKit per collegare nodi DOM e informazioni visive al fine di creare l'albero di rendering. Una differenza non semantica minore è che Gecko ha un livello aggiuntivo tra l'HTML e la struttura DOM. Si chiama "content sink" ed è una factory per la creazione di 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.
L'analisi di un documento significa tradurlo in una struttura che il codice può utilizzare. 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 è conforme: 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 libera dal contesto. Le lingue umane non sono lingue di questo tipo e quindi non possono essere analizzate con tecniche di analisi convenzionali.
Parser - Combinazione Lexer
L'analisi sintattica può essere suddivisa in due sottoprocessi: analisi lessicale e analisi sintattica.
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. In linguaggio umano, sarà costituito da tutte le parole presenti nel dizionario della lingua in questione.
L'analisi della sintassi è l'applicazione delle regole di sintassi del linguaggio.
I parser di solito suddividono il lavoro in due componenti: il lexer (a volte chiamato tokenizer) che si occupa di suddividere l'input in token validi e il parser che si occupa di costruire l'albero di analisi analizzando la struttura del documento in base alle regole di sintassi del linguaggio.
Il lexer sa come rimuovere i caratteri irrilevanti come gli spazi e le interruzioni di riga.
Il processo di analisi è iterativo. In genere, il parser chiede al lexer un nuovo token e tenta di associarlo a una delle regole di sintassi. Se viene trovata una corrispondenza con una regola, alla struttura ad albero di analisi viene aggiunto un nodo corrispondente al token e il parser richiederà un altro token.
Se nessuna regola corrisponde, l'analizzatore memorizza il token internamente e continua a chiedere token finché non viene trovata una regola che corrisponda a tutti i token memorizzati internamente. Se non viene trovata alcuna regola, l'interprete genera 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 è 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 lo analizza innanzitutto 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 a partire da un'espressione matematica. Proviamo a definire un semplice linguaggio matematico 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
: 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à 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 corrisponde a nessuna regola ed è quindi un input non valido.
Definizioni formali per vocabolario e sintassi
In genere, il vocabolario è espresso tramite espressioni regolari.
Ad esempio, il nostro linguaggio sarà definito come:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
Come puoi vedere, gli interi sono definiti da un'espressione regolare.
La sintassi è in genere definita in un formato chiamato BNF. Il nostro linguaggio sarà definito 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 senza contesto. Una definizione intuitiva di una grammatica senza contesto è una grammatica che può essere interamente espressa in BNF. Per una definizione formale, consulta l'articolo di Wikipedia sulla grammatica libera dal contesto
Tipi di analizzatori
Esistono due tipi di parser: dall'alto verso il basso e dal basso verso l'alto. Una spiegazione intuitiva è che gli analizzatori dall'alto verso il basso esaminano la struttura di alto livello della sintassi e cercano una corrispondenza con una 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 analizzatori analizzeranno il nostro esempio.
L'analizzatore dall'alto verso il basso inizierà dalla regola di livello superiore: identificherà 2 + 3
come un'espressione. Identifica quindi 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).
L'analizzatore dal basso verso l'alto eseguirà la scansione dell'input fino a quando non viene trovata una regola corrispondente. 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.
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. Fornisci la grammatica della tua lingua, ovvero il vocabolario e le regole di sintassi, e il programma genera 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 Flex è un file contenente le definizioni delle 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 una struttura ad albero di analisi.
Grammatica HTML
Il vocabolario e la sintassi dell'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 relativi ai parser convenzionali non si applicano all'HTML (non li ho menzionati solo per divertimento: verranno utilizzati per l'analisi CSS e JavaScript). Il codice HTML non può essere facilmente definito da una grammatica senza 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.
A prima vista può sembrare strano, perché l'HTML è piuttosto simile al XML. Esistono molti analizzatori XML disponibili. Esiste una variante XML dell'HTML, XHTML, quindi qual è la grande differenza?
La differenza è che l'approccio HTML è più "tollerante": ti consente di omettere determinati tag (che vengono poi aggiunti implicitamente) o, a volte, i tag di inizio o di fine e così via. Nel complesso, si tratta di una sintassi "morbida", rispetto alla sintassi rigida e impegnativa di XML.
Questo dettaglio apparentemente piccolo fa una grande differenza. Da un lato questo è il motivo principale per cui il codice HTML è così popolare: ti consente di tollerare gli errori e semplifica la vita dell'autore web. D'altra parte, rende difficile scrivere una grammatica formale. Per riepilogare, l'HTML non può essere analizzato facilmente dai parser convenzionali, poiché la sua grammatica non è priva di contesto. L'HTML non può essere analizzato dagli analizzatori sintattici XML.
DTD HTML
La definizione 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, dei relativi attributi e della gerarchia. Come abbiamo visto in precedenza, il DTD HTML non forma una grammatica senza contesto.
Esistono alcune varianti del DTD. La modalità rigorosa è conforme solo alle specifiche, ma le altre modalità supportano il markup utilizzato in passato dai browser. Lo scopo è la compatibilità con le versioni precedenti dei contenuti. L'attuale DTD rigoroso è disponibile qui: www.w3.org/TR/html4/strict.dtd
DOM
L'albero di output ("albero di analisi") è un albero di elementi DOM e nodi di attributi. DOM è l'acronimo di Document Object Model. È la presentazione dell'oggetto del documento HTML e l'interfaccia degli elementi HTML con l'esterno, come JavaScript.
La radice dell'albero è l'oggetto "Document".
Il DOM ha una relazione quasi uno a uno con il markup. Ad esempio:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
Questo markup viene tradotto nel seguente albero DOM:
Come HTML, il DOM viene 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 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 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 analizzatori dall'alto verso il basso o dal basso verso l'alto.
I motivi sono:
- La natura permissiva del linguaggio.
- Il fatto che i browser abbiano una tolleranza di errore tradizionale per supportare casi noti di codice HTML non valido.
- Il processo di analisi è ricorsivo. 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 in dettaglio dalla specifica HTML5. L'algoritmo è costituito 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 inizio, i tag di fine, 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 dello stream di input e aggiorna lo stato successivo in base a questi caratteri. La decisione è influenzata dallo stato attuale della tokenizzazione e dallo stato della 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 da descrivere in modo esaustivo, quindi vediamo un semplice esempio che ci aiuti 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
comporta la creazione di un "token tag inizio" e lo stato viene modificato in "Stato nome tag".
Rimarremo in questo stato finché il carattere >
non viene consumato. Ogni carattere viene aggiunto al nome del nuovo 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 torniamo a "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>
. Emetteremo un token carattere per ogni carattere di Hello world
.
Ora torniamo a "Stato tag aperto".
L'utilizzo dell'input successivo /
comporterà la creazione di un end tag token
e il passaggio allo "stato Nome tag". Anche in questo caso, rimaniamo in questo stato fino a quando non raggiungiamo >
.A questo punto, verrà emesso il nuovo token del tag e torneremo a "Stato dei dati".
L'input </html>
verrà trattato come nel caso precedente.
Algoritmo di costruzione di alberi
Quando viene creato il parser, viene creato l'oggetto Document. Durante la fase di costruzione dell'albero, l'albero DOM con il documento nella radice verrà modificato e gli 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 DOM e alla serie 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. Questi stati sono chiamati "modalità di inserimento".
Vediamo la procedura di costruzione dell'albero 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". La ricezione del token "html" comporterà il passaggio alla modalità "prima di html" e il nuovo trattamento del token in quella modalità. 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". HTMLHeadElement verrà creato in modo implicito, anche se non è disponibile un token "head", e verrà aggiunto alla struttura ad albero.
Ora passiamo alla modalità "in head" e poi a "after head". Il token del corpo viene rielaborato, viene creato e inserito un elemento HTMLBody e la modalità viene trasferita a "in body".
I token di caratteri della stringa "Hello world" vengono ora ricevuti. Il primo comporterà la creazione e l'inserimento di un nodo "Testo" e gli altri caratteri verranno aggiunti a quel nodo.
La ricezione del token di fine del corpo causerà il trasferimento alla modalità "dopo il corpo". Ora riceveremo il tag di fine HTML che ci sposterà alla modalità "after after body". La ricezione del token di fine file termina l'analisi.
Azioni al termine dell'analisi
A questo punto, il browser contrassegna il documento come interattivo e inizia ad analizzare gli script in modalità "differita": 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 consultare gli algoritmi completi per la tokenizzazione e la costruzione dell'albero nella specifica HTML5.
Tolleranza di errore dei browser
Non viene mai visualizzato un errore "Sintassi non valida" in 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 altro ancora), ma il browser lo mostra comunque correttamente e non segnala errori. Pertanto, gran parte del codice del parser corregge gli errori dell'autore HTML.
La gestione degli errori è abbastanza coerente nei browser, ma abbastanza sorprendentemente non ha fatto parte delle specifiche HTML. Come i preferiti e i pulsanti Indietro/Avanti, è semplicemente una funzionalità sviluppata nei browser nel corso degli anni. Esistono strutture HTML non valide note ripetute su molti siti e i browser cercano di correggerle in modo conforme agli altri browser.
La specifica HTML5 definisce alcuni di questi requisiti. (WebKit riassume bene questo concetto nel commento all'inizio della lezione sull'analizzatore sintattico 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 formati correttamente, 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 in un secondo momento.
- 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). Potrebbe essere il caso dei seguenti tag: HTML HEAD BODY TBODY TR TD LI (ne ho dimenticato qualcuno?).
- Desideriamo aggiungere un elemento di blocco all'interno di un elemento incorporato. Chiudi tutti gli elementi in linea fino all'elemento di blocco successivo di livello superiore.
- Se il problema persiste, chiudi gli elementi finché non ci viene consentito di aggiungerlo o 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 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 e non verrà mostrata all'utente.
Una tabella fuori posto
Una tabella esterna è una tabella all'interno di un'altra tabella, ma non all'interno di una cella di 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 sorelle:
<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 dell'elemento corrente: estrae la tabella interna dalla pila di tabelle esterne. Le tabelle saranno ora sorelle.
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 solo.
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 fuori posto
Ancora una volta, il commento parla da solo.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
Quindi, autori web, attenzione: a meno che non vogliate comparire come esempio in uno snippet di codice di tolleranza degli errori di WebKit, scrivete HTML ben formato.
Analisi CSS
Ricordi i concetti di analisi sintattica 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 lessicale e sintattica del 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. "name" è un ID elemento (a cui si fa 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 insieme 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 è definita formalmente in questa definizione:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
Ciò significa che un insieme di regole è un selettore o, facoltativamente, un numero di selettori separati da una virgola e spazi (S indica uno spazio). 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. "declaration" e "selector" verranno definiti nelle seguenti definizioni BNF.
Analizza CSS di WebKit
WebKit utilizza i generatori di parser Flex e Bison per creare automaticamente i parser dai file di grammatica CSS. Come ricordi dall'introduzione al parser, Bison crea un parser di tipo shift-reduce dal basso verso l'alto. 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 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 il parser 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 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, in questo caso l'analisi del documento non verrà interrotta e lo script verrà eseguito dopo l'analisi. 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 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. A livello concettuale, sembra che, poiché i fogli di stile non modificano la struttura DOM, non ci sia motivo di attendere e interrompere l'analisi del documento. Tuttavia, si verifica un problema con gli script che richiedono informazioni sugli stili durante la fase di analisi del documento. Se lo stile non è ancora stato caricato e analizzato, lo script riceverà risposte sbagliate 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 che è 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 dai fogli di stile non caricati.
Costruzione dell'albero di rendering
Durante la costruzione dell'albero DOM, il browser ne crea un altro, l'albero di rendering. Questa struttura è composta da elementi visivi nell'ordine in cui verranno visualizzati. È la rappresentazione visiva del documento. Lo scopo di questa struttura ad albero è consentire di visualizzare i contenuti nell'ordine corretto.
Firefox chiama "frame" agli elementi nell'albero di rendering. WebKit utilizza il termine visualizzatore o oggetto di rendering.
Un renderer sa come eseguire il layout e dipingere se stesso e i suoi elementi secondari.
La classe RenderObject di WebKit, la classe di 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 dalle specifiche CSS2. Include informazioni geometriche come larghezza, altezza e posizione.
Il tipo di casella è influenzato dal valore "display" dell'attributo di stile pertinente per il 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 vuole creare un renderer speciale, sostituirà il metodo createRenderer()
.
I visualizzatori rimandano a oggetti di stile che contengono informazioni non geometriche.
La relazione dell'albero di rendering con l'albero DOM
I visualizzatori 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 visualizzato era assegnato a "nessuno" non appariranno nell'albero (mentre gli elementi con visibilità "nascosto" appariranno nell'albero).
Esistono elementi DOM che corrispondono a più oggetti visivi. Si tratta di solito di elementi con una struttura complessa che non può essere descritta da un singolo rettangolo. Ad esempio, l'elemento "select" ha tre visualizzatori: uno per l'area di visualizzazione, uno per la casella dell'elenco 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 più visualizzatori è il codice HTML non valido. Secondo le specifiche CSS, un elemento in linea deve contenere solo elementi in blocco o solo elementi in linea. Nel caso di contenuti misti, verranno creati visualizzatori di blocchi anonimi per avvolgere gli elementi in linea.
Alcuni oggetti di rendering corrispondono a un nodo DOM ma non nello stesso punto nell'albero. Gli elementi in primo piano e con posizionamento assoluto non sono inclusi nel flusso, sono posizionati in una parte diversa dell'albero e mappati al frame reale. Un frame segnaposto è dove avrebbero dovuto essere.
Il flusso di costruzione dell'albero
In Firefox, la presentazione è registrata come ascoltatore 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 creazione di un visualizzatore è chiamato "attachment". Ogni nodo DOM ha un metodo "attach". L'attacco è sincrono, l'inserimento del nodo nella struttura DOM chiama il metodo "attach" del nuovo nodo.
L'elaborazione dei tag html e body comporta la costruzione della radice della struttura ad albero del rendering.
L'oggetto di rendering principale corrisponde a quello che la specifica CSS definisce blocco contenitore: il blocco più alto che contiene tutti gli altri blocchi. Le sue dimensioni sono l'area visibile: le dimensioni dell'area di visualizzazione della finestra del browser.
Firefox lo chiama ViewPortFrame
e WebKit RenderView
.
Si tratta dell'oggetto di rendering a cui fa riferimento il documento.
Il resto dell'albero viene costruito come inserimento di nodi DOM.
Consulta le specifiche 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. 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. In Firefox, ad esempio, questo viene fatto inserendo un foglio di stile nella cartella "Profilo di 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.
La ricerca delle regole di corrispondenza per ogni elemento può causare problemi di prestazioni se non è ottimizzata. Esaminare l'intero elenco di regole per ogni elemento per trovare le corrispondenze è un'operazione complessa. I selettori possono avere una struttura complessa che può causare l'avvio della procedura di corrispondenza su un percorso apparentemente promettente che si rivela inutile e deve essere provato un altro percorso.
Ad esempio, il seguente selettore composto:
div div div div{ ... }
Significa che le regole si applicano a un
<div>
che è il discendente di tre div. Supponi di voler verificare se la regola si applica a un determinato elemento<div>
. e scegliere un determinato percorso nella struttura ad albero. Potresti dover attraversare la struttura ad albero dei nodi 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 sugli stili
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 di attributi mappati deve essere identico
- Gli stati del collegamento devono corrispondere
- Gli stati dello stato attivo devono corrispondere
- Nessuno degli elementi deve essere interessato dai selettori degli attributi, dove per interessato si intende la presenza di una corrispondenza del selettore che utilizza un selettore degli attributi in qualsiasi posizione all'interno del selettore
- Non deve essere presente alcun attributo di stile in linea negli elementi
- Non devono essere utilizzati selettori fratelli. 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 selettori come :first-child e :last-child.
Albero delle regole di Firefox
Firefox ha due alberi aggiuntivi per semplificare il calcolo degli stili: l'albero delle regole e l'albero del contesto dello stile. WebKit ha anche oggetti di stile, ma non sono memorizzati in una struttura ad albero come l'albero del contesto dello stile, solo il nodo DOM fa riferimento allo stile pertinente.
I contesti di stile contengono 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 della schermata, verrà calcolato e trasformato in unità di misura absolute. L'idea dell'albero delle regole è davvero intelligente. Consente di condividere questi valori tra i nodi per evitare di doverli calcolare 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à 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 lo stile di un 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 associare regole per un altro elemento nell'albero dei contenuti e di scoprire che le regole associate (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 fa risparmiare lavoro.
Suddivisione in struct
I contesti di stile sono suddivisi in struct. Queste strutture contengono informazioni sullo stile per una determinata categoria, come bordo o colore. Tutte le proprietà di una struct sono ereditate o non ereditate. Le proprietà ereditate sono proprietà che, se non definite dall'elemento, vengono ereditate dall'elemento principale. Le proprietà non ereditate (chiamate proprietà "reset") utilizzano i valori predefiniti se non sono definiti.
L'albero ci aiuta memorizzando nella cache intere strutture (contenenti i valori finali calcolati). 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 la struttura ad albero delle regole
Quando calcoliamo il contesto di stile per un determinato elemento, calcoliamo prima un percorso nella struttura ad albero delle regole o ne utilizziamo uno esistente. Poi iniziamo ad applicare le regole nel percorso per compilare le strutture nel 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 completare lo struct. Se non è presente alcuna specifica per la struttura nel nodo della regola, possiamo ottimizzare notevolmente: saliamo nell'albero fino a trovare un nodo che la specifichi completamente e lo indichiamo. Questa è l'ottimizzazione migliore: l'intera struttura è condivisa. In questo modo, si risparmiano memoria e calcoli dei valori finali.
Se troviamo definizioni parziali, risaliamo nell'albero fino a quando la struttura non è completa.
Se non abbiamo trovato definizioni per la nostra struttura, nel caso in cui la struttura sia di tipo "ereditato ", facciamo riferimento alla struttura del nostro elemento principale nell'albero del contesto. In questo caso siamo riusciti anche a condividere le strutture. Se è uno struct di reimpostazione, verranno usati i valori predefiniti.
Se il nodo più specifico aggiunge valori, dobbiamo eseguire alcuni calcoli aggiuntivi per trasformarli in valori effettivi. Memorizziamo quindi il risultato nel nodo dell'albero in modo che possa essere utilizzato dai bambini.
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, supponiamo di dover compilare solo due struct: la struct di colore e la struct di margine. Lo struct color contiene un solo membro: il colore Lo struct margin contiene i quattro lati.
L'albero delle regole risultante sarà simile a questo (i nodi sono contrassegnati dal nome del nodo: il numero della regola a cui fanno riferimento):
L'albero del contesto sarà simile al seguente (nome del nodo: nodo regola a cui rimanda):
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.
Le regole corrispondenti per <div>
sono 1, 2 e 6.
Ciò significa che nella struttura esiste già un percorso che il nostro elemento può utilizzare e che dobbiamo solo aggiungere un altro nodo per la regola 6 (nodo F nella struttura della regola).
Creeremo un contesto di stile e lo inseriremo nell'albero del contesto. Il nuovo contesto dello stile punterà al nodo F nella struttura ad albero delle regole.
Ora dobbiamo riempire gli struct di stile. Inizieremo compilando la struttura del margine. Poiché l'ultimo nodo della regola (F) non si aggiunge allo struct margin, possiamo risalire nella struttura ad albero finché non troviamo uno struct memorizzato nella cache calcolato in un precedente inserimento di nodo e lo utilizziamo. Lo troveremo nel nodo B, che è il nodo più alto che ha specificato le regole di margine.
Abbiamo una definizione per lo struct di colore, quindi non possiamo utilizzare 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 (converti stringa in RGB e così via) e memorizzeremo nella cache la struttura calcolata in questo nodo.
Il lavoro sul secondo elemento <span>
è ancora più semplice. Le regole verranno associate e arriveremo alla conclusione che rimanda alla regola G, come lo spazio 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 aggiungiamo regole per i caratteri in un paragrafo:
p {font-family: Verdana; font size: 10px; font-weight: bold}
L'elemento paragrafo, che è un elemento secondario del div nell'albero del contesto, potrebbe aver condiviso la stessa struttura del carattere del suo elemento principale. Questo accade se non sono state specificate regole di carattere per il paragrafo.
In WebKit, che non ha una struttura ad albero di regole, le dichiarazioni con corrispondenze vengono attraversate 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.
Per riassumere: la condivisione degli oggetti di stile (completamente o alcune delle strutture al loro interno) risolve i problemi 1 e 3. La struttura delle regole di Firefox consente anche di applicare le proprietà nell'ordine corretto.
Manipolare le regole per una corrispondenza facile
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 possono essere abbinati facilmente all'elemento poiché è il proprietario degli attributi di stile, mentre gli attributi HTML possono essere mappati utilizzando l'elemento come chiave.
Come indicato 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 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 rende molto più facile trovare corrispondenze con le 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 la procedura 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 di classi conterrà una chiave "errore" in cui viene trovata la regola per "p.error". L'elemento div avrà regole pertinenti nella mappa ID (la chiave è l'ID) e nella mappa dei tag. Pertanto, l'unica operazione rimasta è scoprire quali delle regole estratte dalle chiavi corrispondono effettivamente.
Ad esempio, se la regola per il div fosse:
table div {margin: 5px}
Verrà comunque estratto dalla mappa dei tag, perché la chiave è il selettore più a destra, ma non corrisponde all'elemento div, che non ha un'antecedente 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 stile dell'elemento principale. Le altre proprietà hanno valori predefiniti.
Il problema si verifica quando sono presenti più definizioni. Ecco l'ordine di applicazione in cascata per risolvere il problema.
Una dichiarazione per una proprietà di stile può essere visualizzata in più 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 è chiamato ordine "a cascata". Secondo la specifica CSS2, l'ordine di applicazione è (dal più basso al più alto):
- Dichiarazioni del browser
- Dichiarazioni normali utente
- Dichiarazioni normali dell'autore
- Dichiarazioni importanti dell'autore
- Dichiarazioni importanti per l'utente
Le dichiarazioni del browser sono meno importanti e l'utente sostituisce l'autore solo se la dichiarazione è 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 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 "stile" piuttosto che una regola con un selettore, altrimenti 0 (= a)
- conteggia il numero di attributi ID nel selettore (= b)
- conteggia il numero di altri attributi e pseudoclassi nel selettore (= c)
- conteggia il numero di nomi di elementi e pseudo-elementi nel selettore (= d)
La concatenazione dei quattro numeri a-b-c-d (in un sistema di numerazione con una base grande) fornisce la specificità.
La base di numeri da utilizzare è definita dal conteggio più alto presente 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 di numeri di 17 cifre. La seconda 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
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;
}
Procedura graduale
WebKit utilizza un flag che indica se tutti gli stili di primo livello (inclusi @import) sono stati caricati. Se lo stile non è completamente caricato al momento dell'attacco, vengono utilizzati dei segnaposto e lo stile viene contrassegnato nel documento. Questi verranno ricalcolati una volta caricati gli stili.
Layout
Quando il visualizzatore viene creato e aggiunto all'albero, non ha una posizione e dimensioni. Il calcolo di questi valori è chiamato layout o riflusso.
HTML utilizza un modello di layout basato sul flusso, il che significa che la maggior parte delle volte è possibile calcolare la geometria in un unico passaggio. Gli elementi "più avanti nel flusso" in genere non influiscono sulla geometria degli elementi "più indietro nel flusso ", quindi 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 in alto e a 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 tutta la gerarchia del frame, calcolando le informazioni geometriche per ogni visualizzatore che lo richiede.
La posizione del renderer principale è 0,0 e le sue dimensioni corrispondono al viewport, ovvero la parte visibile della finestra del browser.
Tutti i renderer hanno un metodo "layout" o "reflow", ogni renderer invoca il metodo di layout dei suoi elementi figlio che richiedono il layout.
Sistema di bit sporchi
Per non 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 "sporchi": necessitano di un layout.
Esistono due flag: "dirty" e "children are dirty", il che significa che, anche se il renderer stesso potrebbe essere OK, ha almeno un elemento secondario che richiede un layout.
Layout globale e incrementale
Il layout può essere attivato nell'intera struttura ad albero di rendering. Si tratta del layout "globale". Ciò può accadere a causa di:
- Una modifica dello stile globale che interessa tutti i visualizzatori, ad esempio una modifica delle dimensioni dei caratteri.
- A seguito del ridimensionamento di una schermata
Il layout può essere incrementale, verranno disposti solo i renderer sporchi (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 nuovi visualizzatori vengono aggiunti alla struttura di rendering dopo che i contenuti aggiuntivi sono stati inviati dalla rete e aggiunti alla struttura 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 i renderer "sporchi" vengono visualizzati con layout out.
Gli script che richiedono informazioni sullo stile, ad esempio "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 rendering vengono prese da una cache e non vengono ricalcolate…
In alcuni casi viene modificata solo una struttura secondaria e il layout non inizia dalla radice. Ciò può accadere nei casi in cui la modifica sia locale e non influisca sull'ambiente circostante, ad esempio il testo inserito nei campi di testo (in caso contrario, ogni pressione di tasto attiverebbe un layout a partire dalla radice).
La procedura di layout
In genere, il layout ha il seguente pattern:
- Il renderer principale determina la propria larghezza.
- Il genitore controlla i figli e:
- Posiziona il renderer secondario (imposta i relativi valori x e y).
- Consente di chiamare il layout secondario, se necessario (sono sporchi, sono in un layout globale o per qualche altro motivo), che calcola l'altezza del bambino.
- Il componente principale utilizza le altezze cumulative dei componenti figlio e le altezze di margini e spaziatura interna per impostare la propria altezza, che verrà utilizzata dal componente principale del visualizzatore principale.
- Imposta il bit dirty su false.
Firefox utilizza un oggetto "state" (nsHTMLReflowState) come parametro per il layout (chiamato "riflusso"). Tra gli altri, lo stato include la larghezza dell'elemento padre.
L'output del layout di Firefox è un oggetto "metriche" (nsHTMLReflowMetrics). Conterrà l'altezza calcolata dal renderer.
Calcolo della larghezza
La larghezza del visualizzatore viene calcolata utilizzando la larghezza del blocco del contenitore, la proprietà "width" dello stile del visualizzatore, 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 contenitore è la massima tra la larghezza disponibile dei contenitori e 0. In questo caso, availableWidth è contentWidth, che viene calcolato come segue:
clientWidth() - paddingLeft() - paddingRight()
clientLarghezza e clientHeight rappresentano l'interno di un oggetto tranne bordo e barra di scorrimento.
La larghezza degli elementi è l'attributo dello stile "width". Verrà calcolato come valore assoluto mediante il calcolo della percentuale della larghezza del contenitore.
I bordi orizzontali e le spaziature interne sono stati aggiunti.
Finora abbiamo calcolato la "larghezza preferita". Ora verranno calcolate le larghezze minime e massime.
Se la larghezza preferita è maggiore di quella massima, viene utilizzata la larghezza massima. Se è inferiore alla larghezza minima (l'unità più piccola non frazionabile), 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 visualizzatore nel mezzo di un layout decide di interrompersi, si arresta e comunica all'elemento principale 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 chiamato il metodo "paint()" del renderer per visualizzare i contenuti sullo schermo. La pittura utilizza il componente dell'infrastruttura dell'interfaccia utente.
Globali e incrementali
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 suo rettangolo sullo schermo. In questo modo il sistema operativo la vede come una "regione "sporca" e genera un evento "paint". Il sistema operativo lo fa in modo intelligente e unisce più regioni in una sola. In Chrome è più complicato perché il motore di rendering si trova in un processo diverso da quello principale. Chrome simula il comportamento del sistema operativo in una certa misura. La presentazione ascolta questi eventi e delega il messaggio alla radice di rendering. L'albero viene attraversato fino a quando non viene raggiunto il visualizzatore pertinente. Ridipinge il dispositivo stesso (e di solito anche per i relativi figli).
L'ordine di pittura
CSS2 definisce l'ordine del processo di disegno. Questo è in realtà l'ordine in cui gli elementi sono impilati nei contesti di impilamento. Questo ordine influisce sulla verniciatura, poiché le serie vengono verniciate dal retro verso l'anteriore. L'ordine di sovrapposizione di un visualizzatore 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 rettangolare dipinto. Contiene i renderer pertinenti per il rettangolo, nell'ordine di pittura corretto (sfondi dei renderer, bordi e così via).
In questo modo, l'albero deve essere attraversato una sola volta per una nuova colorazione anziché più volte: dipingere tutti gli sfondi, poi tutte le immagini, poi 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 dei rettangoli WebKit
Prima di ridipingere, WebKit salva il vecchio rettangolo come bitmap. Quindi, viene visualizzato solo il delta tra il nuovo e il vecchio rettangolo.
Modifiche dinamiche
I browser cercano di eseguire le azioni minime possibili in risposta a una modifica. Pertanto, le modifiche al colore di un elemento ne causeranno solo la nuova colorazione. Le modifiche alla posizione dell'elemento ne causano il layout e la nuova colorazione, nonché di quelli secondari e, eventualmente, dei fratelli. L'aggiunta di un nodo DOM comporta il layout e la nuova colorazione del nodo. Modifiche importanti, come l'aumento delle dimensioni dei caratteri dell'elemento "html", causeranno l'invalidazione delle cache, il nuovo layout e la nuova colorazione dell'intera struttura ad 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 è il thread principale del processo della scheda.
Le operazioni di rete possono essere eseguite da più thread paralleli. Il numero di connessioni parallele è limitato (in genere 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 (ad esempio gli eventi di layout e di disegno) ed li elabora. Questo è il codice di Firefox per il loop di eventi principale:
while (!mExiting)
NS_ProcessNextEvent(thread);
Modello visivo CSS2
La tela
Secondo la specifica CSS2, il termine canvas descrive "lo spazio in cui viene visualizzata la struttura di formattazione": dove il browser dipinge i contenuti.
La tela è infinita 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 viene fornito un colore definito dal browser.
Modello a casella CSS
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 riquadro ha un'area dei contenuti (ad es. testo, immagine e così via) e aree di spaziatura interna, bordi e margini facoltativi.
Ciascun nodo genera n...e queste caselle.
Tutti gli elementi hanno una proprietà "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 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 nella struttura di rendering è simile alla sua posizione nella struttura DOM e viene disposta in base al tipo e alle dimensioni della casella
- Mobile: l'oggetto viene prima disposto come un flusso normale, poi spostato il più a sinistra o a destra possibile
- Assoluto: l'oggetto viene inserito nell'albero di rendering in una posizione diversa rispetto all'albero DOM
Lo schema di posizionamento viene impostato dalla proprietà "position" e dall'attributo "float".
- statici e relativi causano un flusso normale
- assoluta e fissa che causano posizionamento assoluto
Nel posizionamento statico non è 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, come le dimensioni delle immagini e dello schermo
Tipi di caselle
Riquadro: 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 vengono formattati verticalmente uno dopo l'altro. Le intestazioni in linea sono formattate orizzontalmente.
Le caselle in linea vengono inserite all'interno di linee o "caselle di riga". Le linee sono alte almeno quanto la casella più alta, ma possono essere più alte quando le caselle sono allineate "alla linea di base", ovvero la parte inferiore di un elemento è allineata a un punto di un'altra casella diverso dal basso. Se la larghezza del contenitore non è sufficiente, gli elementi in linea verranno inseriti su più righe. Di solito è quello 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 verso sinistra o destra rispetto a una riga. La caratteristica interessante è che le altre scatole scorrono 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 al seguente:
Assoluti e fissi
Il layout viene definito esattamente a prescindere dal flusso normale. L'elemento non partecipa al normale flusso. Le dimensioni sono relative al contenitore. In modalità fissa, il contenitore è 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".
Le caselle sono suddivise in serie (chiamate contesti di accodamento). In ogni pila, gli elementi posteriori verranno visualizzati per primi e quelli anteriori in alto, più vicini all'utente. In caso di sovrapposizione, l'elemento più in primo piano nasconderà l'elemento precedente.
Gli stack sono ordinati in base alla proprietà z-index. Le caselle con la proprietà "z-index" formano una serie locale. L'area visibile contiene la serie di elementi esterni.
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 visualizzato prima nel flusso normale, la proprietà z-index è più alta, quindi è più avanti nella serie detenuta dalla casella principale.
Risorse
Architettura del browser
- Grosskurth, Alan. Un'architettura di riferimento per i browser web (pdf)
- Gupta, Vineet. Come funzionano i browser - Parte 1 - Architettura
Analisi
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (noto anche come "Dragon book"), 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, Layout Engine di Mozilla
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Note su 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 di compilazione 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 in coreano e turco.
Grazie a tutti.