[輕前端] Vite + PetiteVue 開發架構

Lastor
Code 隨筆放置場
17 min readJul 31, 2022

好像有陣子沒更新,繼續來記錄、分享一下關於輕量級活動專案的開發心得。先上重點,這篇主要是介紹使用 Vite 體系的自製輕前端架構,應對 HTML / CSS / JS,分別採用「模板引擎 / 原子化 CSS / PetiteVue」。

接續上一篇:

上次介紹了意外發現的 PetiteVue,實作起來相當的舒服,cost 也很小,相比 Vue CDN 的 128kb (未算壓縮),PetiteVue 僅占了不到 20kb,透過 CDN 服務的 gzip 壓縮,實際下載僅消耗約 6kb。

經歷了幾個小專案的實戰測試之後,真的感覺如同獲得一把新的神兵利器,讓我們可以用很小的 cost,就實現 MVVM 風格的前端開發方式,做專案的爽度直接提升一個檔次。

但整體來講,完全不進 build tools 編譯的話,終究還是會有很多的問題無法處理。

純拉 CDN,不進 build tools 的缺點

build tools 給我們帶來了許多便利性,回去用原始方式開發時,才會發現很多問題不透過 build tools 其實很難處理。

像是 js 跟 css 都不能拆太碎,如果拆了 10 個 ,最終瀏覽器就會 request 10 次。雖然現代瀏覽器已經比當年強很多了,但我們仍舊會希望瀏覽器的請求數越少越好。

開發時還是會很想要使用 Sass、Tailwind CSS 這類東西來幫我們更好的處理 CSS。寫習慣 Typescript 的人,也會很想要上 TS 來寫。要整合這些東西,最終還是上打包工具處理起來才方便。

再來就是打包工具在 build 時,會幫我們在 assets 檔名上自動生成 uuid,例如 index.a1b12345.css,這避免了上 Server 之後可能會出現的瀏覽器 cache 未被刷新的問題。傳統方法上,都是在屁股加上 query string 來誘發瀏覽器刷新 index.css?{timestamp},讓我們每次改完 CSS 還得記得去改 qs,實在很煩人。

所以 try 了幾次之後,最終還是覺得 build tools 是讓前端工作效率化不可或缺的重要存在。

講到 build tools (打包工具),許多人會想到的應該是 Webpack,可是 Webpack 如果不用腳手架的話,自己客制是非常麻煩的事情。但用了腳手架,又會多一堆不需要的東西,本體很肥大,不符合輕量級開發的需求。

這時候,我們就該慶幸,2022 年了,我們有 Vite 可以使用。

Vite 取代 Webpack,開啟無腦打包的世界

相信很多人應該都聽過 Vite 的大名了,除了速度快之外,更重要的是他「開箱即用」的親民設計,親切的引導 UI,讓人用過一次就回不去 Webpack 了。我們可以用非常簡單的方式,快速建立自己的專案設置。

例如現在我想要的 js 主框架,不是 Vue 也不是 React,而是 PetiteVue。並且想要使用 Typescript。那 create vite app 時只要選擇香草 ts 就可以了。

// 建立 vite app
$ pnpm create vite
// 跟著 UI 引導,選擇香草 ts
vanilla-ts

啥設置都不需要,一個素的 TS 環境就被建構了出來。CSS、HTML、TS 相關的打包設置,全都幫我們寫好了,直接就能開始開發。

初始狀態有一個簡單的 counter 用例,有點經驗的開發者,應該看一看就能理解架構,算是很貼心的設計。

接著僅需安裝 PetiteVue…

$ pnpm i petite-vue

然後在 src/main.ts 引入,就可以了開始寫了,豪不費力。

// src/main.ts
import { createApp } from 'petite-vue'
createApp({ count: 0 }).mount()
// index.html
<div v-scope>
<button @click="count++">add {{ count }}</button>
</div>

不過個人更推薦 PetiteVue 採用 CDN,而不是從 npm 拉。畢竟都說了是「輕前端」嘛,讓 cost 越小就越符合這個主思路。PetiteVue 似乎預設也是希望 user 使用 CDN,所以 Github 文件上根本沒寫 npm 的安裝方式。

如果大家都用同一個 CDN 服務的話,對於 user 來說,只要他曾經在其他網頁下載過,那他到我們的網頁時直接用 cache 就可以了,瞬間變成零成本。

要在 Vite 中使用 CDN 也很容易,直接用 PetiteVue 文件上介紹的作法,透過 script type module 的方式引入就可以了。

// index.html in Vite
<script type="module" src="/src/main.ts"></script>
// main.ts
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp().mount()
// types.d.ts (ts 的話要補一下模塊設定, 不然會抱怨)
declare module 'https://*'

除了 PetiteVue 以外,任何其他的 JS lib 都可以快速的在香草方案中導入,例如以前分享過的 Phaser 3 遊戲框架。

PetiteVue 的短版

安麗完 Vite,再拉回這篇的主題。光靠 PetiteVue 其實仍舊有很多需要補足的地方,因為 PetiteVue 僅幫我們解決了 JS 部分,然而 CSS 與 HTML 就無法獲得任何幫助。

PetiteVue 是 lite 版的 Vue,雖然頁面組件的 JS 模塊可以拆分出去,但在虛擬 DOM 直接被拿掉的情況,導致我們「無法拆分 HTML」,這個寫起來還是挺難受的,這會讓我們得到一大包 index.html,沒辦法像這樣去做分類。

<Header />
<Menu />
<SideBar />
...

而 CSS 也是類似情況,Vue 與 React 給我們帶來了一個神奇的能力是「有作用域的 CSS」,這在 Vue 是依靠 Scoped CSS 實現,而在 React 則是透過 CSS Modules 或是各流派的 CSS-in-JS 方案來實現。

這個功能看似簡單,但在中大型專案時,起到了非常關鍵的作用,因為他讓 CSS 的開發單位縮小到了組件層級,與組件內的 HTML 綑綁在一起。讓我們不再需要從一大包 CSS 規則中去維護,很大程度的降低負擔。我們只需要關注組件層級的 CSS 就夠了,再也不用擔心改 A 壞 B 的問題。

而 PetiteVue 無法做到 Scoped CSS。也就是這個輕框架只服務 JS,HTML 與 CSS 我們得另外想辦法。

尋尋覓覓之後,最終我找到了簡單又好用的方案!!

輕前端 HTML / CSS / JS 的三相之力

JS 的部分,已確定讓 PetiteVue 作為主框架,就不再多提。而 HTML 與 CSS 究竟該怎麼辦呢??

我們分別可以通過「模板引擎」以及「原子化 CSS」來解決。

HTML via 模板引擎

先來說說 HTML 吧,我個人最迫切想解決的是 HTML 無法拆分的問題。經歷過一番 Google 查資料,似乎原本 Web Components API 是支持 HTML 拆分的,但後來基於各種考量,這個功能被刪除了,相當的可惜。而 iframe 看起來又是個只會讓事情變複雜的選項。

查著查著,忽然在某篇老外的討論中看到了一個關鍵字 vite-plugin-handlebars,這一瞬間感覺我的思路被打通了。對啊!! 可以靠模板引擎來解決 HTML 拆分的問題啊!!

傳統上,前端用了 Vue / React 就不用模板引擎的固化思維,導致我最初完全沒朝這方向想過。但模板引擎其實很適合這個場景,且模板引擎是屬於預處理的模式,完全不會增加 build 後產品的 cost,非常的完美。

而且 Vite 引入模板引擎也非常的簡單,像是這邊提到的 handlebars,只要安裝之後……

$ pnpm i -D vite-plugin-handlebars

在 config 設定一下就完事了。(雖然也是要設定,但比起 Webpack 簡單好幾倍)

// vite.config.ts
import { defineConfig } from 'vite'
import handlebars from 'vite-plugin-handlebars'
import path from 'path'
export default defineConfig({
plugins: [handlebars({
// 設定 partials 資料夾
partialDirectory: path.join(__dirname, './src/partials'),
})]
})

這邊給不熟 handlebars 的人簡單說明一下,partials 是 hbs 的一個功能塊,理解成組件就可以了,必須指定存放的資料夾位置,hbs 才會知道該去哪找檔案。

然後我們就可以直接在 index.html 使用 hbs 的方式來拆分 HTML 了。

// index.html
<body>
{{> Header }}
</body>
// src/partials/Header.hbs
<header>
<div>Hello Handlebars!!</div>
</header>

這邊需要注意一下,hbs 跟 PetiteVue 的關鍵字都是使用 {{ '雙花括號' }},它們是會打架的。但是不用擔心,PetiteVue 跟 Vue 一樣,可以改分隔符,相當靈活。

createApp({
$delimiters: ['${', '}']
}).mount()

當然,如果想用 Pug 也是可以的,也有 vite-plugin-pug 可以使用。但經過我個人的實作測試,由於我只是要拆分 HTML 而已,Pug 功能有點太強了,很浪費。而且 Pug 與 Vite 的搭配其實不太穩定,配合後面提到的 CSS 方案,會有一些 bug 存在。

另外,Pug 由於寫法太過特殊,會給我們搬移 HTML 時帶來很多困擾,不像 hbs 可以複製貼上就好。但用 Pug 的話最好全進 Pug 寫,就不能在 index.html 與模板文件兩邊跑了,靈活性反而降低了。所以我個人還是更推薦使用 hbs。

拉了模板引擎之後,除了可以拆分 HTML 之外,還有另一個好處。像是我個人目前工作的活動頁需求,常常會有一份寫死的靜態商品清單,要做成一串 UI 高度重複的 product cards。

無論是用 Vue / PetiteVue 還是 React,常規操作就是把它寫成一組 object 或是 json 然後 import 到 JS 中,讓框架去做 each 生成。

// products.json
{
"products": [{}, {}, {}, ...]
}
// main.js
import { products } from './products.json'
createApp({ products }).mount()// html
<div v-for="product in products">
...
</div>

而這其實有一個小小的資源浪費問題,就是這份 json 在打包時會一起被打包進去,然後在 runtime 時現場調用、生成 DOM。

但是這份資料是靜態的,是寫死的,他根本不需要打包進去。我更希望它可以被「預渲染」出來。不然,不僅會白白浪費 cost 在這份 json 上面,還會增加 DOM 的計算量。而且活動小專案往往會發現,scripts 最大包的居然是這份 json。

如果引入模板引擎,我們就可以很輕鬆的在「編譯時 (build)」就把這份 product cards 給預渲染出來,json 就不用包進去了,PetiteVue 也不需要去做 each 了。除了減少不必要的開銷,還優化了 SEO。

// vite.config.ts
import { products } from './assets/products.json'
export default defineConfig({
plugins: [handlebars({
partialDirectory: path.join(__dirname, './src/partials'),
// 餵變數給 hbs
context: { products },
})]
})
// index.html, hbs partials 跟 Vue 組件一樣可以餵參數
<div>
{{#each products}}
{{> ProductCard product=this }}
{{/each }}
</div>
// productCard.hbs 示意
<div>
<h2>{{ product.name }}<h2>
<p>{{ product.desc }}</p>
</div>

所以引入模板引擎,反而意外的可以幫我們處理更多情況,讓我們有更豐富的手段。能預先拿到的、靜態寫死的資料,就交給模板引擎,而動態打 API 的則交給 PetiteVue,各司其職。

CSS via Atomic CSS (Utility-First CSS)

再來是 CSS 部分,前面提到了 PetiteVue 無法幫我們做到 Scoped CSS,無法將 CSS 與 Template 組件化綁定。

然而最近幾年有一個議論紛紛的新形態 CSS 解決方案,剛好可以補足這個短版,也就是原子化 CSS (Atomic CSS)。

這邊對原子化 CSS 的細節先不做展開,不知道的朋友可以參考 Huli 大大的文章,講得非常詳細。

原子化 CSS 的特徵,就是把 class name 與 CSS 屬性作成 1 對 1 對應的形式。寫起來大概這種感覺。

<div class="mt-5 py-6 text-red-500 font-bold bg-[#eee]">
...
</div>

這種 CSS 設計模式,雖然初衷跟 Scoped CSS 沒太大關係,但它就結果而言,其實起到了一樣的效果,也就是 style 被綁定到了 template 上面。讓我們不用再去維護一大包 .css file,只要專注在組件上面就可以了,一樣不用擔心改 A 壞 B 的問題。

所以這玩意剛好完美解決的 PetiteVue 在 CSS 上的短版,而且切起版來十分爽快。雖然第一次見到可能會覺得這是甚麼邪門歪道,但這妥妥的是屬於用過一次之後,就真香回不去的好東西。

原子化 CSS 的框架,目前有兩套是比較紅的,分別是 WindiCSSTailwindCSS。這兩套本身其實是攣生兄弟,WindiCSS 是從 TailwindCSS 派生出去的亞種。

當初在 Tailwind 還在 v1 時,為了解決許多 Tailwind 的問題,才誕生出 WindiCSS,曾經火爆一時。但現在 Tailwind 來到了 v3,跟 WindiCSS 已經各有千秋了。哪一套更好,真的不好說。

個人目前兩邊都玩過之後,我覺得 Tailwind 現階段綜合表現稍微強個一丟丟。兩邊主要是語法差異,設定檔寫法的差異,還有一些微妙的功能差異,這其實很難比較。

在 A 狀況可能 Windi 更強,但在 B 情況 Tailwind 又更方便,就搞得很尷尬。所以私心希望它們可以合併回去……orz||

而目前還有另一支潛力股叫做 UnoCSS,算是從 Windi 再派生出來的東西。但 UnoCSS 側重的方向稍微不太一樣,它不是 CSS 框架,而是 CSS 引擎。這東西正式 release 之後,恐怕前端生態在 CSS 部分會迎來一場變革也說不一定。

不過,無論選用哪一個,其實都大同小異,都是走原子化 CSS 的思路。所以選一個自己喜歡的就好。

再稍微說說智能輔助吧,Windi 與 Tailwind 都有提供 VScode 延伸套件,可以讓我們在寫 class name 時,有很方便的語法提示。但 Windi 這邊預設是不支援 hbs 格式的,需要去加設定。

在 VSCode setting 中,幫 windicss 延伸套件註冊 handlebars 類型。

"windicss.includeLanguages": {
"handlebars": "html"
}

在 windi 設定檔 windi.config.js 中,標記 hbs 格式。

import { defineConfig } from 'windicss/helpers'export default defineConfig({
extract: {
include: ['src/**/*.{hbs,html}'],
},
})

這個需要特別去爬一下文件才會看到相關描述。VScode 延伸,就參考各自擴充套件的文件就能找到,而 lib 本體的設定各自寫在比較不好找的地方。

Windi CSS 是放在 Extractions > Scanning 條目下。

而 Tailwind CSS 則是在安裝說明有提示,像是 Vite 環境的第三步。

其實這稍微有點坑,最初我找了一陣子才找到……XD

共用 node_modules 的做法

上面介紹完核心 HTML / CSS / JS 的解決方案之後,最後來聊聊一個小問題,就是如何讓多個專案共用 node modules。

小專案會讓人不想拉 Vue / React app,除了框架本身很肥之外,另一個頭疼的是 vue-cli 與 create-react-app 的 Webpack 架構本身也很肥。活動專案數量一多起來,對於自身電腦的硬碟空間就會很傷。

10 個活動專案,就會有 10 倍 Webpack 跟一堆 lib 佔空間。所以選擇拉 CDN 開發的同時,很大程度也是因為不想拉這些肥大的腳手架。

然而 Vite 的出現,一定程度上解決了 Webpack 體系肥大的問題,Vite 明顯要更輕巧,設置又簡單,可以很快速的客製化自己的內容,讓我們不再需要依賴功能完整,但略顯肥大的腳手架。

而複用 modules 的部分,則可以靠 pnpm 來解決。這玩意雖然不算新東西了,但我個人最近才知道,有種相見恨晚的感覺。

pnpm 在設計上,是使用軟連結的方式讓專案關聯 node modules。比較細的技術細節,我也沒花時間去全盤搞懂,有興趣的人可以自行 google 一下。簡單來講,就是它可以讓我們 10 個專案,共用同一份 modules,非常神奇。

所以用 pnpm 就可以很無腦的解決活動專案小又多,被迫裝一堆重複的 node modules 的問題,解放我們的硬碟空間。

總結

這次介紹了個人研究出來的,覺得用起來很爽的輕前端架構,簡單來說就是將 HTML / CSS / JS 分別交給 3 種 lib 處理。

HTML 交給模板引擎,CSS 交給原子化方案,JS 則可以用 PetiteVue 或是 alpine 來實現 MVVM 的設計模式。其中 HTML 與 CSS 都是「編譯時」策略,框架本身完全不會佔用 build 後的體積。

最後交給 Vite 來打包,省去一系列 Webpack 麻煩的設定。然後再利用 pnpm 的神奇魔法,優化我們自身的硬碟空間使用。

揮別殺雞用牛刀的 Vue / React app,揮別前文明的 jQuery。用更輕巧的現代化前端工程技術來使工作效率化。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。