其實樣式這件事情,是不是有動態載入好像不是那麼重要。除非你像是 AMP 那樣嚴格規範你的樣式使用範圍,不然通常我們都是全部打一包丟上去。然後就大家都用那一包了,你看看精美的 Bootstrap ,你會因為沒有使用到什麼設定而把他拿掉嗎?通常不會。

或者是你可以做一個客製化的 Bootstrap ,然後拿去偽裝成 bootstrap.min.css ,你這樣試試看後續接手的人會不會叫你出來,然後保證不打死你?


樣式區塊

我們在 Vue 裡面其實有支援 CSS Modules 的作法:

Vue.js 與 CSS Modules
CSS Modules

所以說,我們這樣就能夠在 Template 裡面使用 $style 的隱式變數來讀取樣式資料。而且,在我們的元件裡面,也能使用 this.$style 來讀取樣式的名稱。不過,這些樣式會被加入一個哈希數值,用以避免碰撞問題。

你可以在元件的生命週期當中,讀取到 this.$style 的設定,但是其實在真實世界中,這些東西是不是對我們有任何好處呢?

對於程式邏輯來說,目前看不出來有什麼好處。

為何說沒有?舉例來說:

// 前略 ...
  created () {
    console.log(this.$style.red)
  },
// 後略 ...

如果以我們上面的例子來看,你會拿到一個叫做 red_1C66Z 的字串。他不能代表什麼,他只能代表說我有這個 設定 ,而這個設定僅會來自於 類別選擇器 以及 ID 選擇器 兩種,舉例來說:

<style module>
.red {
  color: red;
}
#myid {
  color: blue;
}
div.qq {
  color: #666;
}
input.qq[name="test"] {
  color: #fff;
}
.red.qq {
  color: #666;
}
#myid .red {
  color: #fff;
}
</style>

最後你只會拿到三組設定:

然後實際上被產生出來的樣式,會依照命名的邏輯來替換掉原本寫好的部分:

用這種 CSS Modules 有一個絕大的好處:

一般用戶永遠猜不透你的樣式叫什麼名字。而且這樣的樣式跟 DOM 結構樹本身幾乎是深度綁定,換句話說,比較不容易透過產生好的程式碼,來猜出你原始邏輯是什麼。換句話說,我也可以透過一些設定,然後把它變成混淆器來使用。

我們用一樣的例子,然後修改 localIdentName 的設定,就能變出這種輸出:

所以說,如果哪天客戶拖款、欠錢、跑路、老闆跳樓、家裡小狗生病或是小孩考不上大學,除了可以不給原始碼之外,編譯好的檔案就這樣給他就好了。如果覺得不夠亂的話,可以把 hash 加大一點,這樣樣式檔案產出也超大,感覺就是很厲害的樣子!

所以說,這個功能壓根跟動態無關,但是也請不要拿來玩弄你的客戶。


動態樣式

我們之前都是再聊元件或是 App 的事情,那麼樣式到底能不能,或者說,適不適合這樣做?在你的網站有做好簡易的讀取進度畫面的狀況下,可以。如果沒有,那麼你的用戶就會在樣式還沒有載入之前,看到你的網站架構裸奔的狀況。

而我們所做的 Hybrid 模式下,其實都已經先將所有元件所產出的相關樣式檔案,先放到 <head> 裡面去了,所以,在正常情況下,他並不會因為樣式載入問題而讓畫面異常。但是,在網站的讀取流程當中,那些樣式其實對於 當下 的畫面,可能是用不到的。所以,是否能讓那些樣式稍微 延後 一點,或是當我的元件真的載入之前,再將樣式加入畫面當中。

所以,我們要做的事情依序會有:

  1. 得知元件什麼時候載入。
  2. 元件載入之前是否有樣式需要預載。
  3. 預載樣式。

相對來說,知道元件什麼時候需要載入應該是比較容易的。例如說,我同一個骨幹要長出不同的外觀,那麼,我從伺服器端就知道我應該要載入哪些元件,所以,我在伺服器端就已經能夠決定,要輸出給前端的 樣式檔案 有哪些。

那麼,這些 樣式檔案 要怎麼個預載呢?我們可以透過 JavaScript 來完成:

<html>
    <head>
        <title>CSS Loading</title>
        <noscript>
            <link rel="stylesheet" href="/static/css/component.css">
        </noscript>
    </head>
    <body>
        <script>
            (function() {
                var s = document.createElement('link');
                // 以下略...
                // 這種方式太常見,就不在這邊充版面了。
            })();
        </script>
    </body>
</html>

或許你會說,在 <head> 裡面放 <noscript> 好像哪裡怪怪的。不過根據 HTML5 的規範, <noscript><head> 當中是可以合法使用的。

The noscript element

所以,我們可以把想要動態載入的樣式先放到 <noscript> 裡面,然後等到真的元件被載入後,再次利用 JavaScript 把樣式放進去。這樣做的好處是,你的樣式檔案只有在需要的時候會被載入,但是請記得,當樣式檔案被載入後,瀏覽器會進行重繪的動作,如果你的動態效果很多的話,要小心效能問題。


所以說,我們在元件裡面具體可以怎麼做?其實,只要在 created 的地方確認一下就可以了。但是,這邊有一個前提,

你的元件應該會有一個獨立被打包出來的樣式檔案。當這個檔案存在時,你才有必要做動態載入這件事情,不然,你的元件其實加了這行等於白加。而且,一般的 Webpack 打包時,會使用哈希去計算一個新的檔案名稱,你必須把這件事情 關閉 。不然,你會無法得知最後的檔案叫什麼名字。

不知道人家的名字,還想叫人家,你是不是想(住口)!

// 前略 ...
  beforeCreate () {
    let style = window.document.querySelector('link[href="/static/css/component.css"]')
    if (style === null) {
      style = window.document.createElement('link')
      // 中略 ...
      // 這邊就用傳統的方式把 link 放到 head 裡面去
      // 這樣你就可以在建立元件之前,把 css 給載入
    }
  }
// 後略 ...

小結

動態載入樣式大概就是這樣,主要的目的是可以防止瀏覽器因為 CSS 樣式載入的關係,而去干涉到第一次的渲染過程。至於說是不是每個專案都適合這樣做,也不一定,例如 SPA 就應該非常不適合(都在同一個頁面了,你還不載入也實在說不過去吧)。

ITHome 鐵人賽同步刊登 動態載入 CSS Style Day 23