iT邦幫忙

2025 iThome 鐵人賽

DAY 30
2
Modern Web

原生元件養成計畫:Web Component系列 第 30

Day 30: Web Component 的樣式進階應用

  • 分享至 

  • xImage
  •  

最後一篇,我們回頭來聊聊 樣式
樣式,果然是讓我在學習 Web Component 的期間花最多心力的一個部分了。
你是否覺得奇怪,為什麼樣式我們都一定要寫在 template 或是 class 裡面呢?
難道我們無法將他抽離成獨立的 style.css 再引入嗎?
其實是可以的!為了讓程式碼看起來更乾淨好讀,就讓我們來重構,將 css 抽離出 class 吧。

定義樣式檔

這個段落將會使用 un-custom-input 元件來做示範。

  • 首先,我們需要建立一個 custom-input.css
├── custom-input.css
├── custom-input.ts
└── index.ts
  • 將原本宣告於 <template> 中的 <style> 抽離至 custom-input.css
template.innerHTML = `
  <style> //請將裡面的內容抽離至 custom-input.css  </style>
  <div class="container" part="container">
    <!--  略...  -->
  </div>
`;
  • 於 constructor 中引用 custom-input.css
constructor() {
	super();
	this.internals = this.attachInternals();
	const shadowRoot = this.attachShadow({ mode: 'open' });

	// 請在此建立樣式的連結,引用抽離的樣式檔。
	// 建立 link 並加入 style
	const link = document.createElement('link');
	link.rel = 'stylesheet';
	link.href = new URL('./custom-input.css', import.meta.url).href;
	shadowRoot.appendChild(link);

	const cloneNode = this.render().cloneNode(true);
	shadowRoot.appendChild(cloneNode);

	this.input = this.shadowRoot!.querySelector('.custom-input');
}
  • npm run dev 試試看,樣式是否有正確被加入。
    style

那麼樣式就成功地被抽離出來囉。
當然還有其他的方式可以實作,這部分就留給各位細細研究了!

CSS.registerProperty & @property

開始前,我們先來看看 Paul 的 wc-msc-ai-assistant 關於 CSS.registerProperty() 的原始碼。

css reister

可以看見大量出現了 CSS.registerProperty() 這個 API。
這個 API 是什麼呢?
接下來就讓我們來詳細說明一下吧!

初步介紹

CSS.registerProperty() 是 CSS Houdini 的一部分。

Houdini(全稱CSS Houdini)並非一個單一的技術,而是一系列提供給開發者的底層瀏覽器API,旨在讓開發者能夠擴展和修改CSS 的核心渲染過程。
-- MDN

這個 API 讓我們能夠註冊自訂屬性(Custom Properties),也就是我們在 Day 12: Web Component 的樣式 style 介紹的 --card-border-color 這種 CSS 變數。

還記得我們當時定義的方法是一行就完成變數的定義了嗎?

:host {
  --card-border-color: #333333;
}

但如果照著上面的方法定義變數,瀏覽器並不知道這個變數是什麼型別,也不知道它能不能被動畫化

動畫化 是什麼意思?

其實瀏覽器對一般的 CSS 變數沒有型別資訊,就只是把它當成一般字串(像之前定義的那樣)。
所以當我們定義了 100px 是一個字串,200px 也是一個字串。瀏覽器並不會知道這兩者之間是有數值差距的,所以無法計算中間過渡。
所以就算你定義了 default 是 width: 100px,hover 後要變 width: 200px,這中間瀏覽器不會幫順順的變寬,而是直接變成 200px。

屬性說明

透過 CSS.registerProperty(),我們可以將 CSS 變數註冊為瀏覽器認識的 正式屬性

我們來看一下 Paul 原始碼中的這一段:

 CSS.registerProperty({
	name: '--msc-ai-assistant-line-color',
	syntax: '<color>',
	inherits: true,
	initialValue: 'rgba(199 205 210)'
});

註冊成正式屬性會用到下列四種屬性:

  • name:自訂 CSS 屬性的名稱。
    • 格式:必須以 -- 開頭,例如 --gap、--input-border-color。
  • syntax:定義該屬性可以接受的值型別。
    • <length>: 長度(px, rem, em, %)。
    • <color>: 顏色 (#333, red, rgb())。
    • <number>: 純數字。
    • ... 其他屬性。
  • inherits:決定這個屬性是否會從父元素繼承。
    • true:子元素會繼承父元素的值(在 Shadow DOM 中,若想讓外面能控制內部樣式,就要設為 true)。
    • false:子元素不會繼承,會使用 initialValue(或自己設定的值)。
  • initialValue:屬性的預設值。
    • 如果沒在 CSS 中設定變數值,或設定了錯誤型別,瀏覽器會使用這個值。

接下來,我們前往 web.dev 可以看到: CSS.registerProperty() 是作用於 JS 當中,如果想要直接定義在 css 樣式檔中,就要使用 @property 來達到一樣的功能。

注意:需要確認瀏覽器的支援度,目前使用上還是以 CSS.registerProperty() 為主。

@property 的寫法

我們直接將變數名稱定義在外層,內部再進行相關設定。

@property --colorPrimary {
  syntax: '<color>';
  initial-value: magenta;
  inherits: false;
}

試試看

我們將會使用最基本的卡片元件來做範例。並使用 CSS.registerProperty() 來做 CSS 變數的註冊。

class CustomCard extends HTMLElement {
  constructor() {
    super();

    // 註冊 CSS 變數
    if (CSS?.registerProperty) {
      CSS.registerProperty({
        name: '--card-color',
        syntax: '<color>',
        inherits: false,
        initialValue: '#abcacf',
      });   
    }

    // 建立 Shadow Root
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const cloneNode = this.render().cloneNode(true);
    shadowRoot.appendChild(cloneNode);
  }
  
  render() {
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        .card {
          width: 100px;
          height: 100px;
          display: inline-block;
          margin: 10px;
          cursor: pointer;
        }

        /* 這裡使用沒註冊的變數 */
        .card {
          background-color: var(--card1-color, #55a8b5);
          transition: var(--card1-color) 0.5s; // 發現沒有動畫
        }
        
        .card1:hover {
          --card1-color: #cbcacf;
        }

        /* 這裡使用有註冊的變數 */
        .card2 {
          background-color: var(--card-color);
          transition: --card-color 0.5s;  // 有動畫
        }
        
        .card2:hover {
          --card-color: #55a8b5;
        }
      </style>
    
      <div class="card card1"></div>
      <div class="card card2"></div>
    `;

    return template.content;
  }
}

customElements.define('custom-card', CustomCard);
<body>
  <custom-card></custom-card>
</body>

原始碼請看這:https://codepen.io/unlinun/pen/raxmZPg

當你前往 codepen 玩玩看,你會發現,有註冊的元件有動畫的效果,沒有註冊的元件沒有動畫的效果。
當然,動畫是其中一個好處,還有其他好處,像是型別的定義,可以讓瀏覽器更清楚的知道這個 CSS 變數的型別是什麼,其餘的好處就留給大家發掘了!

那麼,最後一篇的主要內容就到這裡,感謝大家的收看。


尾聲

不得不說,連續 30 天發文章真的是場硬仗,尤其九、十月連假多多(這是太晚開賽沒注意到的事情)。本來想著假日應該會在家裡囤幾篇文章,沒想到幾乎都往外跑。

另一個困難點就是要把想法轉成文字,有時候都開始懷疑自己到底還能說些什麼。但每次開始寫下去後就覺得自己好像講得不夠多 (σ′▽‵)′▽‵)σ

感謝名單當然是先謝謝兩位隊友,三十天幾乎每天晚上都有提醒加油!
另外還有同事的友情協助校稿,幾乎每篇文章都能從我的文章中抓到錯字或是程式碼的問題 XD,沒有各位就沒有我的完賽之旅!

總之,這三十天真的學到了很多,雖然文筆沒變好,但是腦中真的多了很多關於 Web Component 的新知識。
也希望有讀過這系列文章的大家,也能跟我一樣有成長一咪咪囉~(ゝ∀・)b


附錄(參考資料):
webcomponents.org
MDN-Web_components
web.dev-webcomponents
javascript.info-web-components
Web Components 探索之旅,出发!
web-components
lit.dev


上一篇
Day 29: Web Component 的框架 Lit - Life cycle & Event
系列文
原生元件養成計畫:Web Component30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
nihilitypeo
iT邦新手 2 級 ‧ 2025-10-11 20:25:18

謝謝分享

我要留言

立即登入留言