摘要:Vue 3已經發布有一段時間了,到底有哪些新特性值得關注,如何用它構建企業級前端專案,怎樣快速上手Vue 3?本篇文章將對此進行詳細講解。
前言
工欲善其事,必先利其器 --《論語》
在如今被三大框架支配的前端領域,已經很少有人不知道 Vue 了。2014 年,前 Google 工程師尤雨溪釋出了所謂的漸進式(Progressive)前端應用框架 Vue,其簡化的模版繫結和元件化思想給當時還是 jQuery 時代的前端領域產生了積極而深遠的影響。Vue 的誕生,造福了那些不習慣 TS 或 JSX 語法的前端開發者。而且,Vue 較低的學習門檻,也讓初學者非常容易上手。這也是為什麼 Vue 能在短時間內迅速推廣的重要原因。從 State of JS 的調查中可以看到,Vue 的知名度接近 100%,而且整體使用者滿意度也比較高。
Vue 既強大又易學,這是不是意味著 Vue 是一個完美框架呢?很遺憾,答案是否定的。雖然 Vue 的上手門檻不高,靈活易用,但是這種優勢同時也成為了一把雙刃劍,為構建大型專案帶來了一定的侷限性。很多用 Vue 2 開發過大型專案的前端工程師對 Vue 是又愛又恨。不過,隨著 Vue 3 的釋出,這些開發大型專案時凸顯出來的劣勢得到了有效解決,這讓 Vue 框架變得非常全能,真正具備了跟 “前端框架一哥” React 一爭高下的潛力。Vue 3 究竟帶來了什麼重要的新特性呢?本篇文章將對此進行詳細介紹。
Vue 概覽
Vue 是前 Google 工程師尤雨溪於 2013 年開發、2014 年釋出的前端框架。關於 Vue 的具體定義,這裡摘抄 Vue 官網裡的介紹。
Vue (讀音 /vjuː/,類似於 view) 是一套用於構建使用者介面的漸進式框架。與其它大型框架不同的是,Vue 被設計為可以自底向上逐層應用。Vue 的核心庫只關注檢視層,不僅易於上手,還便於與第三方庫或既有專案整合。另一方面,當與現代化的工具鏈以及各種支援類庫結合使用時,Vue 也完全能夠為複雜的單頁應用提供驅動。
漸進式框架
很多人可能不理解漸進式框架(Progressive Framework)的含義。這裡簡單解釋一下。漸進主要是針對專案開發過程來說的。傳統的軟體專案開發通常是瀑布流式(Waterfall)的,也就是說,軟體設計開發任務通常有明確的時間線,任務與任務之間有明確的依賴關係,這意味著專案的不確定性容忍度(Intolerance to Uncertainty)比較低。這種開發模式在現代日趨複雜而快速變化的商業情景已經顯得比較過時了,因為很多時候需求是不確定的,這會給專案帶來很大的風險。
而漸進式框架或漸進式開發模式則可以解決這種問題。以 Vue 為例:專案開始時,功能要求簡單,可以用一些比較簡單的 API;當專案逐漸開發,一些公共元件需要抽象出來,因此用到了 Vue 的元件化功能;當專案變得非常大的時候,可以引用 Vue Router 或者 Vuex 等模組來進一步工程化前端系統。看到了麼,這樣一來,開發流程變得非常敏捷,不用提前設計整個系統,只用按需開發,因此可以快速開發產品原型以及擴充套件到生產系統。
框架特性
Vue 是利用模版語法來渲染頁面的,這也稱做宣告式渲染。Vue 好上手的重要原因也是因為這個,因為它符合了前端開發者的習慣。例如下面這個例子。
<div id="app"> {{message}} </div> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) </script>
可以看到,el 指定 Vue 例項繫結的元素,data 中的 message 與 DOM 元素的內容進行繫結。只需要操控 JS 中的資料,HTML 內容也會隨之改變。
另外,Vue 將 HTML、CSS、JS 全部整合在同一個檔案 .vue 中,以元件化應用構建的方式來組織程式碼,從語法特性上鼓勵 “高內聚、低耦合” 的設計理念,讓程式碼組織變得更加合理,提升了可讀性與邏輯性。下面是一個官方網站給出的基礎 .vue 檔案例子。
<template> <p>{{ greeting }} World!</p> </template> <script> module.exports = { data: function () { return { greeting: 'Hello' } } } </script> <style scoped> p { font-size: 2em; text-align: center; } </style>
元件的骨架(HTML)、樣式(CSS)和資料或操作(JS)都在同一個地方,開發者需要思考如何將整個系統拆分成更小的子模組,或者元件。這對於構建大型專案是非常有幫助的。
其實,除了上述兩個特點,Vue 還有很多其他的實用特性,但限於篇幅的原因,我們這裡不詳細解釋了。感興趣的讀者可以去[官方網站深入瞭解。
框架缺點
沒有什麼東西是完美的,Vue 同樣如此。當 Vue 的知名度和使用者量不斷增加時,一些前端開發者開始抱怨 Vue 的靈活性太高導致構建大型專案時缺少約束,從而容易產生大量 bug。甚至使用 Vue 生態圈裡的狀態管理系統 Vuex 也無法有效解決。關於 Vue 是否適合大型專案的問題,網上有不少爭論,甚至尤大本人都親自上知乎參與了討論(吃瓜傳送門)。
客觀來講,Vue 雖然具有較低的上手門檻,但這並不意味著 Vue 不適合開發大型專案。然而,我們也必須承認大型專案通常要求較高的穩定性和可維護性,而 Vue 框架較高的靈活性以及缺少足夠的約束讓其容易被經驗不足的前端開發者所濫用,從而產生臭不可聞的、難以直視的 “屎山” 程式碼。其實,程式碼可維護性並不強制要求較低的靈活性與自由度,只是這種自由可能會對專案的整體穩定帶來風險。
Vue 作者尤雨溪其實很早就注意到這個問題,因此才會打算從底層重構 Vue,讓其更好的支援 TypeScript。這就是 2020 年 9 月釋出的 Vue 3。
Vue 3 新特性
Vue 3 有很多實用的新特性,包括TS 支援、組合式 API 以及 Teleport 等等。本文不是關於 Vue 3 的參考文,因此不會介紹其中全部的新特性,我們只會關注其中比較重要的特性,尤其是能加強程式碼約束的 TypeScript(簡稱 TS)。
TS 支援
技術上來說,TS 支援並不是 Vue 3 的新特性,因為 Vue 2 版本就已經能夠支援 TS 了。但 Vue 2 版本的 TS 支援,是通過 vue-class-component 這種蹩腳的裝飾器方式來實現的。筆者對 “蹩腳” 這個評價深有體會,因為筆者曾經遷移過 Vue 2 版本的生產環境專案,最後發現收益並不高:語法有很大的不同,花了大量時間來重構,發現只提升了一些程式碼的規範性,但是程式碼整體變得更臃腫了,可讀性變得更差。
而在 Vue 3 中,TS 是原生支援的,因為 Vue 3 本身就是用 TS 編寫的,TS 成為了 Vue 3 中的 “一等公民”。TS 支援在我看來是 Vue 3 中最重要的特性,特別是對構建大型前端專案來說。為什麼說它重要?因為 TS 有效的解決了前端工程化和規模化的問題,它在程式碼規範和設計模式上極大的提高程式碼質量,進而增強系統的可靠性、穩定性和可維護性。關於 TS 的重要性,筆者在該公眾號前一篇文章《為什麼說 TypeScript 是開發大型前端專案的必備語言》已經做了詳細介紹,感興趣的讀者可以繼續深入閱讀一下。
Vue 3 定義了很多 TS 介面(Interface)和型別(Type),幫助開發者定義和約束各個變數、方法、類的種類。下面就是一個非常基礎的例子。
import { defineComponent } from 'vue' // 定義 Book 介面 interface Book { title: string author: string year: number } // defineComponent 定義元件型別 const Component = defineComponent({ data() { return { book: { title: 'Vue 3 Guide', author: 'Vue Team', year: 2020 } as Book // as Book 是一個斷言 } } })
上述程式碼通過 defineComponent 定義了元件型別,而在 data 裡定義了內部變數 book,這個是通過介面 Book 來定義的。因此,其他元件在引用該元件時,就能夠自動推斷出該元件的型別、內部變數型別,等等。如果引用方與被引用方的任何一個介面、變數型別不一致,TS 就會拋錯,讓你可以提前規避很多錯誤。
雖然 Vue 3 在傳統定義 Vue 例項方式中(Options API)能夠很好的支援 TS,但是我們更推薦用 TS 配合另一種新的方式來定義 Vue 例項,也就是接下來要介紹的組合式 API(Compositional API)。
組合式 API
組合式 API 的誕生是來自於大型專案中無法優雅而有效地複用大量元件的問題。如果你已經瞭解 Vue,你或許應該知道之前版本的 Vue 例項中包含很多固定的 API,包括 data、computed、methods 等。這種定義方式有個比較突出的問題:它將 Vue 例項中的功能按照型別的不同分別固定在不同的 API 中,而沒有根據實際的功能來劃分,這將導致一個複雜元件中的程式碼變得非常散亂,就像如下這張圖一樣。
在這個 “科學怪人” 式的傳統元件中,同一種顏色的程式碼負責同一種功能,但它們卻根據不同型別分散在不同的區域,這將導致初次接觸該元件的開發人員難以快速理解整個元件的功能和邏輯。而組合式 API 則允許開發者將元件中相關的功能和變數聚合在一個地方,在外部按需引用,從而避免了傳統方式的邏輯散亂問題。
在 Vue 3 的組合式 API 中,所有功能和邏輯只需要定義在 setup 這個方法中。setup 接受屬性 props 和上下文 context 兩個引數,並在方法內部定義所需要的變數和方法,返回值是包含公共變數和方法的物件,它們可以供其他元件和模組使用。傳統 Vue 例項的大部分 API,例如 data、computed、methods 等,都可以在 setup 中定義。下面是官閘道器於組合式 API 的例子。
// src/components/UserRepositories.vue import { toRefs } from 'vue' import useUserRepositories from '@/composables/useUserRepositories' import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch' import useRepositoryFilters from '@/composables/useRepositoryFilters' export default { // 引用子元件 components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, // 屬性 props: { user: { type: String } }, setup(props) { // 解構屬性,如果直接在 setup 中引用,必須要加 toRefs const { user } = toRefs(props) // 獲取 repository 相關公共方法,在其他模組中定義 const { repositories, getUserRepositories } = useUserRepositories(user) // 搜尋 repository 相關公共方法,在其他模組中定義 const { searchQuery, repositoriesMatchingSearchQuery } = useRepositoryNameSearch(repositories) // 過濾 repository 相關公共方法,在其他模組中定義 const { filters, updateFilters, filteredRepositories } = useRepositoryFilters(repositoriesMatchingSearchQuery) return { // 因為我們並不關心未經過濾的倉庫 // 我們可以在 `repositories` 名稱下暴露過濾後的結果 repositories: filteredRepositories, getUserRepositories, searchQuery, filters, updateFilters } } }
在這個例子中,該元件需要的變數或方法全部在其他模組定義了,並通過 useXXX 的函式暴露給外部元件,而且還可以被其他元件重複使用。這樣看上去是不是更清爽了呢?
你可能會思考怎麼寫 useXXX 這種函式。其實非常簡單,下面就是一個例子。
// src/composables/useUserRepositories.js import { fetchUserRepositories } from '@/api/repositories' import { ref, onMounted, watch } from 'vue' export default function useUserRepositories(user) { // 內部列表變數 const repositories = ref([]) // 獲取列表方法 const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(user.value) } // 初次獲取列表,掛載後執行,相當於傳統元件中的 mounted onMounted(getUserRepositories) // 監聽 user 並根據變化來獲取最新列表,相當於傳統元件中的 watch watch(user, getUserRepositories) // 返回公共變數和方法 return { repositories, getUserRepositories } }
傳統元件中的一些 API,例如 mounted 和 watch,已經成為了按需引用的函式,功能跟之前一模一樣。而之前的 data、computed、methods 變成 setup 函式中的內部變數,並根據是否返回來決定是否暴露給外部。
需要注意的是,Vue 3 中引入了響應式 API 的概念,之前的變數都需要根據需要用不同的響應式 API 來定義。其具體原理不深入介紹了,感興趣的讀者可以到官方文件繼續深入學習。
其他新特性
Vue 3 還有其他一些新特性,限於篇幅原因就不詳細介紹了。這裡只列出一些比較實用的新特性及其簡單介紹。
- Teleport - 適用於 Modal、Popover 等需要掛載在全域性 DOM 元素中的元件
- 片段 - 元件支援多個根節點
- 觸發元件選項 - 關於事件的相關 API 變更
全部變更列表,請參考官方文件(英文)。
大型專案實戰
前面介紹了這麼多理論知識,對於前端工程師來說可能還不夠,要在工作中讓所學知識發揮作用,還必須要用到專案實踐中,特別是大型專案。因此,這個小節將著重介紹如何用 Vue 3 來構建企業級專案。本小節將用筆者的一個 Github 倉庫 作為演示,講解如何用 Vue 3 構建大型前端專案。
這個倉庫是筆者的一個開源專案 Crawlab 的下一個版本 v0.6 的前端部分。它目前還處於開發中的狀態,並不是成品;不過程式碼組織結構已經成型,作為演示來說已經足夠。之前的版本是用 Vue 2 寫的,用的是傳統 Vue API。這個 Vue 3 版本將使用 TS 和組合式 API 來完成重構和遷移,然後在此基礎上加入更多實用的功能。對該前端專案感興趣的讀者可以訪問該 Github 倉庫瞭解程式碼細節,同時也非常歡迎大家跟我討論任何相關問題,包括不合理或需要優化的地方。
倉庫地址: https://github.com/crawlab-team/crawlab-frontend
專案結構
該專案的程式碼組織結構如下。其中忽略了一些不重要的檔案或目錄。
. ├── public // 公共資源 ├── src // 原始碼目錄 │ ├── assets // 靜態資源 │ ├── components // 元件 │ ├── constants // 常量 │ ├── i18n // 國際化 │ ├── interfaces // TS 型別宣告 │ ├── layouts // 佈局 │ ├── router // 路由 │ ├── services // 服務 │ ├── store // 狀態管理 │ ├── styles // CSS/SCSS 樣式 │ ├── test // 測試 │ ├── utils // 輔助方法 │ ├── views // 頁面 │ ├── App.vue // 主應用 │ ├── main.ts // 主入口 │ └── shims-vue.d.ts // 相容 Vue 宣告檔案 ├── .eslintrc.js // ESLint 配置檔案 ├── .eslintignore // ESLint Ignore 檔案 ├── babel.config.js // Babel 編譯配置檔案 ├── jest.config.ts // 單元測試配置檔案 ├── package.json // 專案配置檔案 └── tsconfig.json // TS 配置檔案
可以看到,這個前端專案有非常多的子模組,包括元件、佈局、狀態管理等等。在 src 目錄中有十多個子目錄,也就是十多個模組,這還不包括各個模組下的子目錄,因此模組非常多,結構也非常複雜。這是一個典型的大型前端專案的專案結構。企業級專案,例如 ERP、CRM、ITSM 或其他後臺管理系統,大部分都有很多功能模組以及清晰的專案結構。這些模組各司其職,相互協作,共同構成了整個前端應用。
其實這種專案結構並不只適用於 Vue,其他框架的專案例如 React、Angular 都可以是類似的。
TS 型別宣告
TS 幾乎是現代大型前端專案的標配,其強大的型別系統可以規避大型專案中很多常見的錯誤和風險。因此,我們在這個前端專案中也採用了 TS 來做型別系統。
在前面的專案結構中,我們在 src/interfaces 目錄中宣告 TS 型別。型別宣告檔案用 <name>.d.ts 來表示,name 表示是跟這個模組相關的型別宣告。例如,在 src/interfaces/layout/TabsView.d.ts 這個檔案中,我們定義了跟 TabsView 這個佈局元件相關的型別,內容如下。
interface Tab { id?: number; path: string; dragging?: boolean; }
更復雜的例子是狀態管理的型別宣告檔案,例如 src/interfaces/store/spider.d.ts,這是 Vue 中狀態管理庫 Vuex 的其中一個模組宣告檔案,內容如下。
// 引入第三方型別 import {GetterTree, Module, MutationTree} from 'vuex'; // 如果引入了第三方型別,需要顯式做全域性宣告 declare global { // 繼承 Vuex 的基礎型別 Module interface SpiderStoreModule extends Module<SpiderStoreState, RootStoreState> { getters: SpiderStoreGetters; mutations: SpiderStoreMutations; } // 狀態型別 // NavItem 為自定義型別 interface SpiderStoreState { sidebarCollapsed: boolean; actionsCollapsed: boolean; tabs: NavItem[]; } // Getters // StoreGetter 為自定義基礎型別 interface SpiderStoreGetters extends GetterTree<SpiderStoreState, RootStoreState> { tabName: StoreGetter<SpiderStoreState, RootStoreState, SpiderTabName>; } // Mutations // StoreMutation 為自定義基礎型別 interface SpiderStoreMutations extends MutationTree<SpiderStoreState> { setSidebarCollapsed: StoreMutation<SpiderStoreState, boolean>; setActionsCollapsed: StoreMutation<SpiderStoreState, boolean>; } }
其中,尖括號 <...> 裡的內容是 TS 中的泛型,這能大幅度提高型別的通用性,通常用作基礎型別。
下面是引用 TS 型別的例子 src/store/modules/spider.ts。
import router from '@/router'; export default { namespaced: true, state: { sidebarCollapsed: false, actionsCollapsed: false, tabs: [ {id: 'overview', title: 'Overview'}, {id: 'files', title: 'Files'}, {id: 'tasks', title: 'Tasks'}, {id: 'settings', title: 'Settings'}, ], }, getters: { tabName: () => { const arr = router.currentRoute.value.path.split('/'); if (arr.length < 3) return null; return arr[3]; } }, mutations: { setSidebarCollapsed: (state: SpiderStoreState, value: boolean) => { state.sidebarCollapsed = value; }, setActionsCollapsed: (state: SpiderStoreState, value: boolean) => { state.actionsCollapsed = value; }, }, actions: {} } as SpiderStoreModule;
這裡用了 as SpiderStoreModule 的斷言,TS 靜態檢測器會自動將 SpiderStoreModule 中的元素推斷出來,並與實際的變數做比對。如果出現了不一致,就會拋錯。
元件化
元件化是現代前端專案的主流,在 Vue 3 中也不例外。Vue 3 的元件化跟 Vue 2 比較類似,都是用 Vue 例項來定義各類元件。在這個前端專案中,元件被分類成了不同種類,同一種類的放在一個資料夾中,如下。
. └── src └── components ├── button // 按鈕 ├── context-menu // 右鍵選單 ├── drag // 拖拽 ├── file // 檔案 ├── icon // Icon ├── nav // 導航 ├── table // 表格 └── ...
元件檔案為 <ComponentName>.vue 定義,如下是其中一個關於右鍵選單的例子 src/components/context-menu/ContextMenu.vue。
<template> <el-popover :placement="placement" :show-arrow="false" :visible="visible" popper-class="context-menu" trigger="manual" > <template #default> <slot name="default"></slot> </template> <template #reference> <div v-click-outside="onClickOutside"> <slot name="reference"></slot> </div> </template> </el-popover> </template> <script lang="ts"> import {defineComponent} from 'vue'; import {ClickOutside} from 'element-plus/lib/directives'; // 定義屬性 export const contextMenuDefaultProps = { visible: { type: Boolean, default: false, }, placement: { type: String, default: 'right-start', }, }; // 定義觸發事件 export const contextMenuDefaultEmits = [ 'hide', ]; // 定義元件 export default defineComponent({ // 元件名稱 name: 'ContextMenu', // 引用外部指令 directives: { ClickOutside, }, // 觸發事件 emits: contextMenuDefaultEmits, // 屬性 props: contextMenuDefaultProps, // 組合式 API setup(props, {emit}) { // 點選事件函式 const onClickOutside = () => { emit('hide'); }; // 返回公共物件 return { onClickOutside, }; }, }); </script>
你可能會有疑慮:這裡似乎沒用到 TS 中的型別系統啊。其實這只是一個非常簡單的元件,包含完整 TS 特性的元件例子可以參考下面這個元件。
src/file/FileEditor.vue
: https://github.com/crawlab-team/crawlab-frontend/blob/main/src/components/file/FileEditor.vue
其他
限於篇幅原因,本文不會詳細介紹其他所有模組。這裡只簡單列舉一下。
- UI 框架(UI Framework)- 用了 Element+ 作為 UI 框架
- 佈局(Layouts)- 基礎佈局 BasicLayout 定義了頂部、側邊欄、底部等元素
- 狀態管理(State Management)- 相當於全域性資料管理系統
- 路由(Routing)- 頁面路由配置
- 國際化(Internationalization)- 多語言配置
- 樣式(Styles)- 利用 SCSS 定義了全域性樣式以及樣式變數等
- 服務(Services)- 包括與後端 API 的互動函式
- 常量(Constants)
- 輔助方法(Utilities)
如何學習 Vue 3
關於 Vue 3 的學習途徑,其實首先應該是閱讀官方文件,瞭解 Vue 3 的基礎概念、高階原理以及如何工程化等等。作者尤雨溪已經在文件中非常詳細的介紹了關於 Vue 的各個方面,圖文並茂、深入淺出的講解了關於 Vue 3 的概念和知識。總之 Vue 3 的文件對於初學者來說非常友好。如果你對英文比較熟悉,推薦直接閱讀英文官方文件,其中內容一般是最新的。
除開閱讀官方文件以外,筆者還推薦閱讀優秀的 Vue 3 開源專案,例如 Element+、Ant Design Vue、Vue-Admin-Beautiful,Github 上有很多優秀的 Vue 3 專案,閱讀它們的原始碼可以幫助你熟悉如何使用 Vue 3,以及構建大型專案的程式碼組織方式。
當然,自己動手用 Vue 3 實踐一個前端專案能夠幫助你深入理解 Vue 3 的原理和開發方式,特別是將 Vue 3 的新特性用在工作專案中。筆者在瞭解了 Vue 3 的基礎語法和新特性之後,將所學知識運用在了自己的開源專案中,邊學邊做,就非常快速的掌握了 Vue 3 的核心知識。
總結
這篇文章主要介紹了 Vue 3 在大型前端專案中的優勢,尤其是新特性 TS 支援和組合式 API,能夠大幅增強程式碼的可讀性和可維護性。這讓本身就上手容易的 Vue 框架變得如虎添翼,使其能夠勝任大型前端專案的設計和開發。對 TS 的原生支援,能夠讓 Vue 3 的專案程式碼能夠具有良好的可預測性。而組合式 API,能夠將散亂的程式碼邏輯變得更有秩序。這些都有助於增強 Vue 3 前端專案的健壯性,從而讓前端人員更容易編寫出穩定而可維護的程式碼。另外,本文還通過筆者的一個開發中的前端專案(Crawlab Frontend),來演示如何利用 Vue 3 開發企業級前端專案,並展示了相關的專案結構、TS 型別宣告以及元件化,等等。
比較資深的前端工程師可能會對 Vue 3 的新特性不屑一顧,因為所謂的 TS 支援和組合式 API 都在其他知名框架以其他名字被率先引入,例如 React 的 React Hooks,Vue 3 似乎只是借鑑了過去。但是,這種觀點非常不可取。在技術面前,任何方案都沒有高低貴賤,只有合不合適。就像相親一樣,只有合適的,才是最好的。尤雨溪也承認,AngularJS 和 React 都有很多優秀的技術,Vue 也借鑑了一部分。但你絕不能因此而宣判它是抄襲。就像 C# 跟 Java 語法和特性類似,但你肯定無法證明 C# 是抄襲的 Java(其實 C# 相較於 Java 有很多優秀特性,例如隱式型別推斷,這也是筆者比較喜歡 C# 的原因之一)。Vue 的成功,絕對不是偶然性的,它的易用性和相對豐富的文件資源,讓初學者能夠快速上手,這對於前端開發者來說是福音。我們做技術的應該對新技術抱有包容心,能夠辯證而理性的看待問題,這樣才不致於變得偏激從而走火入魔。
參考
-
Vue 3 官方文件: https://www.vue3js.cn/docs/zh/
-
TypeScript 官方文件: https://www.typescriptlang.org/docs
-
Crawlab Frontend: https://github.com/crawlab-team/crawlab-frontend
本文分享自華為雲社群《TS 加持的 Vue 3,如何幫你輕鬆構建企業級前端應用》,原文作者:Marvin Zhang 。