昨天聊到Vue他是一個Framework它可以小到像JQuery一樣在HTML裡面就能跑,但使用上更方便,大也可以撐起整個正式專案的重責大任。
備註:不過兩者雖然用起來一樣輕巧,思考方式卻很不同:jQuery 是由你親手去改畫面,Vue 則是你改資料,畫面自己跟著變
講起來好像有點八股很常見到,但實際上差異在哪?讓我們今天用一個簡單的計數器從最小的那一端開始在慢慢引領個到目前業界最常用到的Vite專案,最後在偷看一下.vue檔案在背後被轉換成什麼。
不用安裝任何東西,開一個 index.html,貼上這段:
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>CDN 版計數器</title>
</head>
<body>
<div id="app">
<button @click="count++">點了 {{ count }} 次</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const count = ref(0)
return { count }
}
}).mount('#app')
</script>
</body>
</html>
直接用瀏覽器打開,按鈕就會動了。這段程式碼做了三件事:
Vue 物件,我們從裡面取出 createApp 和 ref。setup() 裡用 ref(0) 建立一個會變動的資料 count,並且把它 return 出去,模板才看得到它。mount('#app') 讓 Vue 接管 #app 這個區塊,裡面的 {{ count }} 和 @click 才會生效。return」,文章最後會再回來看它。至於為什麼模板裡寫 count 就好,JS 裡卻要用 ref() 包起來,這牽涉到 Vue 的響應式機制,後面幾天會從 Proxy 開始慢慢拆解。
CDN 版很方便,但需求一多就會開始卡。假設我們想把按鈕抽成一個可以重複使用的組件:
<div id="app">
<counter-button label="蘋果"></counter-button>
<counter-button label="香蕉"></counter-button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue
const CounterButton = {
props: ['label'],
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">{{ label }}:{{ count }}</button>
`
}
createApp({})
.component('CounterButton', CounterButton)
.mount('#app')
</script>
還能跑,但可以想像當組件變成十個、二十個的時候:
setup() 都要手動 return:變數一多,漏寫一個就會在模板裡找不到。counter-button 這種小寫連字號,也不能寫自閉合標籤。打開終端機,用官方的 create-vue 建立專案:
npm create vue@latest
過程中會問專案名稱和要不要加入 TypeScript、Router、Pinia 等功能,今天先全部不選,保持最乾淨的狀態。建好之後:
cd my-vue-app
npm install
npm run dev
終端機會顯示一個本機網址,打開就能看到預設畫面。
專案資料夾裡檔案不少,但今天只需要認識三個:
my-vue-app/
├─ index.html
└─ src/
├─ main.js
└─ App.vue
index.html:整個應用程式的入口。打開會發現裡面很空,只有一個 <div id="app"></div> 和一行 <script type="module" src="/src/main.js">。
src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
有沒有覺得眼熟?這和 CDN 版做的是同一件事:建立 app、掛載到 #app。差別只在 Vue 從全域變數變成了 import,組件設定從寫在原地變成從 App.vue 引入。
src/App.vue:把預設內容清空,換成我們的計數器:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">點了 {{ count }} 次</button>
</template>
<style scoped>
button {
padding: 8px 16px;
font-size: 16px;
}
</style>
這就是 SFC(Single-File Component,單檔組件):一個 .vue 檔裡,邏輯、模板、樣式各自放在 <script>、<template>、<style> 區塊。加上 scoped 之後,樣式只會作用在這個組件上,不會影響到別人。
仔細看 <script setup> 的部分:沒有 setup() 函式,也沒有 return。宣告完 count,模板就直接能用了。
| 比較項目 | CDN 版 | Vite + SFC 版 |
|---|---|---|
| 需要安裝 | 不用,引入 script 即可 | 需要 Node.js 和建置工具 |
| 模板寫在哪 | HTML 裡或 JS 字串裡 | .vue 檔的 <template> |
| 變數給模板用 | 在 setup() 裡手動 return |
<script setup> 宣告即可 |
| 組件拆檔 | 不方便 | 一個組件一個檔案 |
| 樣式管理 | 另外寫 CSS | <style scoped> 跟著組件走 |
| 編輯器支援 | 模板字串沒有語法提示 | 完整的語法提示與檢查 |
| 適合情境 | 既有頁面局部加互動 | 從零開始的完整應用程式 |
兩種都是 Vue,沒有誰比較「正統」,而是看需求選擇。這也是「漸進式框架」真正的意思:你可以依照專案規模,決定要用到 Vue 的多少。
.vue 檔最後變成了什麼?到這裡可能會有個疑問:瀏覽器根本不認識 .vue 這種檔案,它是怎麼跑起來的?
答案是:Vite 在背後透過 Vue 的編譯器,把 .vue 檔轉換成瀏覽器看得懂的 JavaScript。我們可以用官方的 Vue SFC Playground 親眼看看。
把剛剛的 App.vue 貼進 Playground 左側,切到右上方的「JS」分頁,就能看到編譯結果。實際輸出會因為版本和模式有些差異,這邊只挑重點、簡化後長這樣:
import { ref } from 'vue'
const __sfc__ = {
setup() {
const count = ref(0)
return { count } // ← 編譯器幫你補上的 return
}
}
function render(_ctx, _cache, $props, $setup) {
return _createElementBlock('button', {
onClick: () => $setup.count++
}, '點了 ' + _toDisplayString($setup.count) + ' 次')
}
這裡只需要看懂兩件事:
第一,return 回來了。 還記得 CDN 版一定要自己寫 return { count } 嗎?<script setup> 並不是不需要 return,而是編譯器自動幫你補上了。
第二,<template> 變成了一個 render 函式。 我們寫的 HTML 模板,最後會被轉成一段 JavaScript 函式,負責產生畫面。所以模板不是被瀏覽器「直接讀取」,而是先被翻譯成程式碼。
至於 render 函式裡那些 _createElementBlock 做了什麼,今天先點到為止,知道「模板會被編譯成函式」就夠了。
今天從一個 script 標籤出發,看到 Vite 和 SFC 幫我們解決了哪些問題,也知道 <script setup> 省下的 return 其實是編譯器代勞的。
但還有一件事沒解釋:按下按鈕、count++ 之後,畫面就自動更新了。Vue 是怎麼知道資料變了?
明天就從 Vue 3 響應式系統的底層,JavaScript 的 Proxy 開始說起。