摘要:Vue 3已经发布有一段时间了,到底有哪些新特性值得关注,如何用它构建企业级前端项目,怎样快速上手Vue 3?本篇文章将对此进行详细讲解。
工欲善其事,必先利其器 --《论语》前端
在现在被三大框架支配的前端领域,已经不多有人不知道 Vue 了。2014 年,前 Google 工程师尤雨溪发布了所谓的渐进式(Progressive)前端应用框架 Vue,其简化的模版绑定和组件化思想给当时仍是 jQuery 时代的前端领域产生了积极而深远的影响。Vue 的诞生,造福了那些不习惯 TS 或 JSX 语法的前端开发者。并且,Vue 较低的学习门槛,也让初学者很是容易上手。这也是为何 Vue 能在短期内迅速推广的重要缘由。从 State of JS 的调查中能够看到,Vue 的知名度接近 100%,并且总体用户满意度也比较高。vue
Vue 既强大又易学,这是否是意味着 Vue 是一个完美框架呢?很遗憾,答案是否认的。虽然 Vue 的上手门槛不高,灵活易用,可是这种优点同时也成为了一把双刃剑,为构建大型项目带来了必定的局限性。不少用 Vue 2 开发过大型项目的前端工程师对 Vue 是又爱又恨。不过,随着 Vue 3 的发布,这些开发大型项目时凸显出来的劣势获得了有效解决,这让 Vue 框架变得很是全能,真正具有了跟 “前端框架一哥” React 一争高下的潜力。Vue 3 究竟带来了什么重要的新特性呢?本篇文章将对此进行详细介绍。git
Vue 是前 Google 工程师尤雨溪于 2013 年开发、2014 年发布的前端框架。关于 Vue 的具体定义,这里摘抄 Vue 官网里的介绍。github
Vue (读音 /vjuː/,相似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不一样的是,Vue 被设计为能够自底向上逐层应用。Vue 的核心库只关注视图层,不只易于上手,还便于与第三方库或既有项目整合。另外一方面,当与现代化的工具链以及各类支持类库结合使用时,Vue 也彻底可以为复杂的单页应用提供驱动。vuex
不少人可能不理解渐进式框架(Progressive Framework)的含义。这里简单解释一下。渐进主要是针对项目开发过程来讲的。传统的软件项目开发一般是瀑布流式(Waterfall)的,也就是说,软件设计开发任务一般有明确的时间线,任务与任务之间有明确的依赖关系,这意味着项目的不肯定性容忍度(Intolerance to Uncertainty)比较低。这种开发模式在现代日趋复杂而快速变化的商业情景已经显得比较过期了,由于不少时候需求是不肯定的,这会给项目带来很大的风险。typescript
而渐进式框架或渐进式开发模式则能够解决这种问题。以 Vue 为例:项目开始时,功能要求简单,能够用一些比较简单的 API;当项目逐渐开发,一些公共组件须要抽象出来,所以用到了 Vue 的组件化功能;当项目变得很是大的时候,能够引用 Vue Router 或者 Vuex 等模块来进一步工程化前端系统。看到了么,这样一来,开发流程变得很是敏捷,不用提早设计整个系统,只用按需开发,所以能够快速开发产品原型以及扩展到生产系统。json
Vue 是利用模版语法来渲染页面的,这也称作声明式渲染。Vue 好上手的重要缘由也是由于这个,由于它符合了前端开发者的习惯。例以下面这个例子。segmentfault
<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 有不少实用的新特性,包括TS 支持、组合式 API 以及 Teleport 等等。本文不是关于 Vue 3 的参考文,所以不会介绍其中所有的新特性,咱们只会关注其中比较重要的特性,尤为是能增强代码约束的 TypeScript(简称 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 的诞生是来自于大型项目中没法优雅而有效地复用大量组件的问题。若是你已经了解 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 还有其余一些新特性,限于篇幅缘由就不详细介绍了。这里只列出一些比较实用的新特性及其简单介绍。
所有变动列表,请参考官方文档(英文)。
前面介绍了这么多理论知识,对于前端工程师来讲可能还不够,要在工做中让所学知识发挥做用,还必需要用到项目实践中,特别是大型项目。所以,这个小节将着重介绍如何用 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
限于篇幅缘由,本文不会详细介绍其余全部模块。这里只简单列举一下。
BasicLayout
定义了顶部、侧边栏、底部等元素关于 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.or...
Crawlab: https://github.com/crawlab-team/crawlab
Crawlab Frontend: https://github.com/crawlab-team/crawlab-frontend
《为何说 TypeScript 是开发大型前端项目的必备语言》
本文分享自华为云社区《TS 加持的 Vue 3,如何帮你轻松构建企业级前端应用》,原文做者:Marvin Zhang 。