用Vue3構建企業級前端應用,TS能讓你更輕鬆點
摘要: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 倉庫了解代碼細節,同時也非常歡迎大家跟我討論任何相關問題,包括不合理或需要優化的地方。
倉庫地址: //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
: //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 官方文檔: //www.vue3js.cn/docs/zh/
-
TypeScript 官方文檔: //www.typescriptlang.org/docs
-
Crawlab: //github.com/crawlab-team/crawlab
-
Crawlab Frontend: //github.com/crawlab-team/crawlab-frontend
本文分享自華為雲社區《TS 加持的 Vue 3,如何幫你輕鬆構建企業級前端應用》,原文作者:Marvin Zhang 。