何
私は本当にscoped cssが好きである。
cssのmodule設計は幾多あるが、だいたいがmoduleのネストで親子間の影響がネックになってmodule粒度が大きくなる傾向があった。
おそらくそういう文脈もあって、そこと向き合ったBEMはみんなに愛されているんだろうと思う。
ブラウザがネイティブでcssにscopeを与えようとした仕様は残念ながら消えてしまったがvue-loaderとかで変換される.vueファイルのようにcssにscopeを与えながら、単一コンポーネントをwebで実現する手段はいくつかある
<template>
<div>
<p class="heading">piyo</p>
</div>
</template>
<style scoped>
p {
font-weight: bold;
}
</style>
ここで定義されたpのスタイルは上述の.heading要素にしか当たらないようにコンパイルされる
compile後のコード
変換後のコードを見てみる
<style>
p[data-v-12345] {
font-weight: bold;
}
</style>
<div data-v-12345>
<p class="heading" data-v-12345>piyo</p>
</div>
するとhtml側にもstyle側にもそれぞれの要素・セレクタの末尾にdata属性が指定されている
セレクタの末尾にコンポーネントごとにユニークな[data-v-xxxx]を付与することでそれだけに効くスタイルができあがるというロジックらしい
簡易loader
踏まえてザルな簡易ローダーを作ってみるとこんな感じ
loader({
template: `
<div>
<p class="heading">piyo</p>
</div>
`,
style: `
p {
font-weight: bold;
}
`
});
function loader(component) {
let hash = String(Date.now()); // 大雑把なuuid替わり
return `
<style>
${transformCSS(component.style, hash)}
</style>
${transformHTML(component.template, hash)}
`;
}
function transformHTML(html, hash) {
let dataAttr = `data-${hash}`;
return html.replace(/<([^/]+?)>/mg, (body, $1) => {
return `<${$1} ${dataAttr}>`
})
}
function transformCSS(style, hash) {
let dataAttr = `[data-${hash}]`;
return style.replace(/([^{])\s*\{\s*([^}]*?)\s*}/mg,(body, $1, $2) => {
let selector = $1.split(',').map(v => {
return `${v.trim()}${dataAttr}`;
}).join(',');
return `${selector} {
${$2}
}`;
})
}
出力
<style>
p[data-1576831975938] {
font-weight: bold;
}
</style>
<div data-1576831975938>
<p class="heading" data-1576831975938>piyo</p>
</div>
selectorには@rule系とかあったり、擬似セレクタもあって末尾に必ずつけていいわけでもないのでcssのちゃんとしたparser使ってtransformした方がいいがおおよそ原理はこんな風になっているらしい。