css ja asettelu

CSS @layer: miten cascade layers lopettavat spesifisyyssodan

CSS @layer: miten cascade layers lopettavat spesifisyyssodan

Sinulla on korttikomponentti design-systeemin kirjastosta: .card .card__header .card__title { font-size: 18px; } – kolme luokkaa selektorissa, koska komponentin tekijä sisäkkäisti sen niin. Haluat kumota sen yhdellä apuluokalla, .text-lg { font-size: 20px; }, joka on lisätty HTML:ään kortin luokkien jälkeen. Ei toimi. Otsikko pysyy itsepäisesti 18 pikselissä, vaikka .text-lg on koodissa myöhemmin, joten intuitiivisesti "sen uudemman pitäisi voittaa". Tämä ei ole selainbugi – kolme luokkaa selektorissa voittaa yhden luokan, riippumatta tiedoston järjestyksestä. Näin spesifisyys on toiminut vuodesta 1996, ja niin sen pitääkin toimia.

Klassinen kiertotie on oman säännön spesifisyyden nostaminen – .card .text-lg, tai jos se ei riitä, !important. Molemmat ratkaisut toimivat paikallisesti ja hajoavat systeemisesti: joka kerta kun joku tiimissä lisää säännön, jolla on vielä korkeampi spesifisyys (tai vielä yhden !important-merkinnän), koko tyylitiedoston loppuosa muuttuu ennalta-arvaamattomaksi. Juuri tämän ongelman – ei esteettisen, vaan arkkitehtonisen – ratkaisevat CSS Cascade Layers.

Uusi akseli konfliktien ratkaisuun, ei uusi tapa nostaa prioriteettia

Keskeinen asia ymmärtää: @layer ei ole vielä yksi työkalu säännön prioriteetin nostamiseen, kuten !important tai selektoriin lisätty ylimääräinen luokka. Se on kaskadialgoritmin oma, erillinen vaihe, joka ratkaistaan ennen spesifisyyttä, ei sen rinnalla. Täysi järjestys, jossa selain ratkaisee kahden CSS-säännön välisen konfliktin, näyttää (yksinkertaistettuna originin jättämällä pois) tältä:

  1. Kerros (@layer) – myöhemmin ilmoitetun kerroksen sääntö voittaa aiemmin ilmoitetun kerroksen säännön, riippumatta sisällä olevien selektorien spesifisyydestä.
  2. Spesifisyys – ratkaisee vasta silloin, kun molemmat säännöt ovat samassa kerroksessa (tai kumpikaan ei ole missään kerroksessa).
  3. Järjestys lähdekoodissa – viimeinen tuomari, kun kaksi edellistä kriteeriä päätyvät tasapeliin.

Tämän ansiosta yksi .text-lg-luokka myöhemmässä kerroksessa voi voittaa kolmen luokan selektorin aiemmassa kerroksessa – koska yhteenotto ei koskaan edes etene spesifisyyden vertailuvaiheeseen. Kerros ratkaisee asian jo aiemmin.

css

/* Kerrosten järjestys ilmoitetaan kerran, etukäteen – sen ei tarvitse
vastata järjestystä, jossa niiden sisältö oikeasti esiintyy tiedostoissa */
@layer reset, base, components, utilities;
@layer components {
.card .card__header .card__title {
font-size: 18px;
}
}
@layer utilities {
.text-lg {
font-size: 20px;
}
}

Tämä tyhjä @layer reset, base, components, utilities; -ilmoitus tiedoston alussa ei ole muodollisuus – se on prioriteettijärjestyksen eksplisiittinen lukitseminen, ennen kuin yksikään näistä kerroksista saa varsinaista sisältöä. Tämän ansiosta kerrosten järjestys on riippumaton importtien tai tiedostojen järjestyksestä – components voi fyysisesti esiintyä koodissa ennen utilities-kerrosta, ja silti utilities voittaa, koska tämä järjestys ilmoitettiin jo alussa.

Käytännön esimerkki: reset, komponentit ja apuluokat, jotka eivät koskaan taistele keskenään

Käytännössä tämä kaava – reset alempana kuin komponentit, komponentit alempana kuin apuluokat – on täsmälleen se, miten esimerkiksi Tailwindin kerrosjärjestelmä on toiminut versiosta 3.1 lähtien: apuluokan pitää aina voittaa komponenttisäännön, riippumatta siitä, kuinka monta luokkaa kyseisen komponentin selektorissa on, koska juuri tätä varten apuluokat ylipäätään ovat olemassa.

css

@layer reset, base, components, utilities;
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
color: #1a1a1a;
}
}
@layer components {
.card {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 16px;
}
.card .card__header .card__title {
font-size: 18px;
font-weight: 600;
}
}
@layer utilities {
.text-lg {
font-size: 20px !important;
}
.mt-4 {
margin-top: 16px;
}
}

Huomaa, että kerroksessa utilities !important-merkintä kohdassa .text-lg on tässä tarpeeton – kerros takaa prioriteetin jo itsessään, ilman että täytyy turvautua CSS:n raskaimpaan aseeseen. Tämä havainnollistaa hyvin, miksi cascade layers todella vähentävät !important-merkintöjen määrää koodissa: et enää tarvitse sitä spesifisyyden kiertämiseen, koska käytössäsi on työkalu, joka on suunniteltu juuri tähän tehtävään.

Sama mekanismi ratkaisee toisen, yhtä yleisen ongelman – ulkopuolisen CSS-kirjaston, jonka selektoreita et hallitse ja jonka prioriteetin haluat pitää aina omia tyylejäsi alempana, ilman että joudut nostamaan omien sääntöjesi spesifisyyttä:

css

@import url("some-ui-library.css") layer(vendor);
@layer vendor, base, components, utilities;

Koko kirjasto päätyy vendor-kerrokseen, joka on sijoitettu järjestyksen aivan alkuun – joten omat kerroksesi base, components ja utilities voittavat sen aina, riippumatta siitä, kuinka aggressiivisesti kyseinen kirjasto rakentaa selektorinsa.

Sudenkuoppa, joka rikkoo intuition: kerroksettomat tyylit voittavat kaikki kerrokset

Tämä on koko mekanismin useimmin sivuutettu, mutta samalla seurauksiltaan merkittävin yksityiskohta: sääntö, joka ei ole missään kerroksessa, voittaa säännön missä tahansa kerroksessa – tavallisten, ei-!important-julistusten kohdalla. Se ei "häviä, koska on näkymättömässä kerroksessa lopussa" – se voittaa aina, ehdoitta, jokaisen kerroksellisen säännön, vaikka viimeksi mainittu olisikin kerroksessa, joka on ilmoitettu korkeimman prioriteetin kerrokseksi.

css

@layer utilities {
.text-lg {
font-size: 20px;
}
}
/* Tämä sääntö, vaikka se ei ole missään kerroksessa ja näyttää
satunnaiselta, yksittäiseltä ylikirjoitukselta – VOITTAA yllä olevan
.text-lg-säännön, riippumatta siitä, missä kohtaa tiedostoa se on */
.title {
font-size: 16px;
}

Tämä käytös on tarkoituksellista – sen ansiosta cascade layersin asteittainen käyttöönotto olemassa olevassa, isossa projektissa on turvallista: kaikki "vanha" CSS, joka ei ole vielä päätynyt mihinkään kerrokseen, säilyttää oletuksena korkeimman prioriteetin, joten se ei ala yhtäkkiä hävitä uusille, lisätyille kerroksille. Mutta sama käytös on lähde hämmentävimmille bugeille tiimeissä, jotka ottavat @layer-ominaisuuden käyttöön osittain: yksittäinen sääntö, joka jää vahingossa kerroksen ulkopuolelle – kirjoitusvirhe, unohtunut import, kolmannen kirjaston injektoima <style>-lohko – voittaa koko huolellisesti suunnitellun prioriteettijärjestelmän, ilman minkäänlaista varoitusta. Käytännön seuraus: jos päätät ottaa cascade layersin käyttöön, laita johdonmukaisesti kaikki johonkin kerrokseen, mukaan lukien reset ja peruskerroksen tyylit – älä jätä yhtäkään sääntöä "ulkopuolelle", koska juuri se, ei utilities-kerros, voittaa jokaisen konfliktin.

Toinen sudenkuoppa: !important kääntää kerrosten järjestyksen

Jos käytät !important-merkintää kerroksen sisällä, kerrosten välinen prioriteettijärjestys kääntyy päinvastaiseksi. Tavallisten julistusten kohdalla myöhemmin ilmoitettu kerros voittaa. !important-julistusten kohdalla voittaa aiemmin ilmoitettu kerros.

css

@layer reset, components;
@layer reset {
.card { padding: 0 !important; }
}
@layer components {
.card { padding: 16px !important; }
}
/* Reset voittaa (0px), koska !important-tapauksessa etusijalla
on aiemmin ilmoitettu kerros – täsmälleen päinvastoin kuin
sääntö, joka pätee ilman !important-merkintää */

Tämä ei ole spesifikaation oikku – se on johdonmukainen laajennus säännöstä, joka on jo olemassa originien kohdalla (käyttäjän !important-tyylit ovat aina voittaneet sivun tekijän !important-tyylit, päinvastoin kuin tavallisten julistusten kohdalla). Käytännön johtopäätös: !important kerrosjärjestelmän sisällä voi yllättää jopa sellaisen, joka ymmärtää tavallisen @layer-järjestyksen hyvin – vielä yksi hyvä syy kohdella !important-merkintää asiana, johon turvaudutaan tietoisesti ja harvoin, ei oletustapana voittaa yhteenotto.

Sudenkuopat ja hyvät käytännöt

  • Ei yhtäkään sääntöä kerrosten ulkopuolella, jos järjestelmän halutaan olevan ennustettava. Kuten yllä näytettiin, jo yksi sääntö ilman @layer-merkintää voittaa koko kerrosjärjestelmän tavallisten julistusten kohdalla – johdonmukaisuus on tärkeämpää kuin mukavuus lisätä "vain yksi pieni sääntö" lennossa.
  • Spesifisyys pätee yhä yhden kerroksen sisällä. @layer eliminoi spesifisyyssodat kerrosten välillä, mutta yhden kerroksen sisällä kaksi sääntöä ratkaisevat konfliktin edelleen tavalliseen tapaan – järkevä luokkien nimeäminen ja selektorien syvän sisäkkäisyyden välttäminen kannattaa yhä.
  • Ilmoita kerrosten järjestys eksplisiittisesti, aivan alussa, tyhjällä @layer nimi1, nimi2, ...;-lauseella. Tämä poistaa riippuvuuden tiedostojen import-järjestyksestä – prioriteettijärjestys on lukittu kerralla, selkeästi, yhteen paikkaan, riippumatta siitä, missä järjestyksessä yksittäiset CSS-tiedostot myöhemmin oikeasti latautuvat.
  • Selaintuki ei ole enää syy varovaisuuteen. Cascade Layers saavutti Baseline-statuksen (laajasti saatavilla) jo vuonna 2022 – Chrome 99, Firefox 97, Safari 15.4 – joten sen voi ottaa käyttöön ilman @supports-sääntöä missä tahansa uudessa projektissa.

Yhteenveto

@layer ei kilpaile spesifisyyden kanssa – se toimii täysin eri, aikaisemmassa vaiheessa kaskadialgoritmia, ja sen ansiosta yksi apuluokka voi tietoisesti ja ennustettavasti voittaa kolmen sisäkkäisen luokan selektorin, ilman prioriteetin nostamista ja ilman !important-merkintää. Tämä on aito arkkitehtoninen työkalu resetin, peruskerroksen tyylien, komponenttien ja apuluokkien erottamiseen kerroksiin, joiden tärkeysjärjestys on ilmoitettu eksplisiittisesti – mutta kaksi sen sääntöä rikkovat intuitiota niin voimakkaasti, että ne kannattaa muistaa erikseen: mikä tahansa kerroksen ulkopuolinen tyyli voittaa jokaisen kerroksen, ja !important kerrosten sisällä kääntää niiden prioriteettijärjestyksen päinvastaiseksi. Kummankaan näistä kahdesta faktasta huomiotta jättäminen ei näy virheenä konsolissa – se näkyy CSS:nä, joka toimii eri tavalla kuin näennäisen loogisesta kerrosjärjestyksestä tiedostossa voisi päätellä.