🍣

Vue SFCのstyleタグから別のvueファイルのstyleを参照する

2025/01/25に公開

元々はコンポーネントをextendするとscoped styleが適用されない問題の解決を試みた所、

Comp.vue
<script>
export default {
  data: () => ({
    text: 'Hello'
  })
};
</script>
<template>
  <p class="text">
    {{ text }}
  </p>
</template>
<style scoped>
.text {
  background-color: yellow;
  padding: 10px;
  font-size: 1.3rem;
}
</style>
ExtendedComp.vue
<script>
import Comp from './Comp.vue';
export default {
  extends: Comp,
  data: () => ({
    text: 'Hello extended'
  })
};
</script>

Scoped Style Won't Apply
vite.config.jsのpluginsにloadを書けばビルド前のSFCを弄れる事を知り、大体似たような感じでextend元のコンポーネントのstyleタグを持って来れれば解決するなーと思って書いたら上手く行った。
Scoped Style Works
具体的にはvite.config.js に以下のようなpluginを自前で定義し、

vite.config.js
import { fileURLToPath, URL } from 'node:url'
import path from 'node:path'
import { readFileSync } from 'fs'
import { JSDOM } from 'jsdom'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    {
      async load(id) {
        if (id.endsWith('.vue')) {
          const source = readFileSync(id).toString()
          // SFCをパースできればなんでも良い。
          // windowやbodyは別にいらないのでfragmentにした
          const frag = JSDOM.fragment(source)
          const stls = frag.querySelectorAll(`style[src$=".vue"]`)
          return [...stls].reduce(async (acc, stl) => {
            const src = stl.getAttribute('src')
            const absPath = path.resolve(path.dirname(id), src)
            // resolve.aliasはthis.resolveで解決するが相対パスは解決してくれないのでこうする
            const resolved = (await this.resolve(absPath)) || (await this.resolve(src))
            const source = readFileSync(resolved.id).toString()
            const frag = JSDOM.fragment(source)
            const stls = frag.querySelectorAll(`style`)
            // .vueを参照しているstyleタグは消しておかないとコンパイルエラーになる
            const regex = new RegExp(`\\s+src=(['"])${src}\\1`)
            // DOMをmanipulateしてinnerHTML吐いた方がラクなのだが
            // HTMLとしてパースするとself-closingタグが上手く解決されず、
            // xmlとしてパースするとstyleやscriptの中身がエスケープされてしまうので
            // 元のstringにstyleを書き足すようにした
            return [...stls].reduce(
              (acc, stl) => acc + stl.outerHTML, acc.replace(regex, '')
            )
          }, source)
        }
      },
    },
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

extendしたコンポーネントから参照したいstyleを持つvueファイルを指定する

ExtendedComp.vue
<script>
import Comp from './Comp.vue';
export default {
  extends: Comp,
  data: () => ({
    text: 'Hello extended'
  })
};
</script>
<style src="./Comp.vue"></style>

※別にextendしなくても参照出来る

Discussion