前端工程实践 + Vue3 基础

590 阅读18分钟

写在前面

适合阅读人群

本文适合具备一定前端基础的同学,特别是那些已经熟悉 Vue 2 或希望巩固 Vue 3 的同学。

对于完全零基础的同学来说,本文的代码部分可能有些难以理解。如果你想系统学习 Vue 3,建议先小步快跑,随意观看小 Po 站的一些基础教学视频,逐步掌握 Vue 相关概念和技术。此外,如果你有足够的时间和精力,更建议你全面阅读 Vue 官方网站提供的教程,其中包含了详细的指南和示例。

如果你已经是资深前端工程师,本文或许对你而言稍显简单,直接右上角~

Vue 学习行径指南

假设你已经具备了HTML、CSS 和 JavaScript 语法的基础,最好是掌握 ES6+ 版本。如果还不熟悉这些知识,也没关系。因为在学习前端的过程中,最终还是会偿还之前欠下的技术债!

单页面应用

  1. 了解 Vue 是什么以及如何安装;
  2. 掌握创建 Vue 实例的方法以及如何使用数据和方法;
  3. 熟悉 Vue 生命周期的钩子函数;
  4. 理解插值表达式的多种用法;
  5. 理解单向和双向数据绑定的概念;
  6. 理解属性和 DOM 事件在 Vue 中的应用;
  7. 掌握使用 v-bind 进行属性绑定的方法;
  8. 掌握使用 v-on 进行事件绑定的方法;
  9. 掌握使用 v-ifv-show 进行条件渲染的技巧;
  10. 掌握使用 v-for 进行列表渲染的方法;
  11. 熟悉 computed 计算属性和 watch 侦听器的使用;
  12. 理解组件化开发的思想,这样做的好处是什么?同时了解组件之间数据和事件通信的方式。

注:以上内容可以在一个简单的单页面中完成,无需进行工程化项目配置。在 Vue.js 的初级阶段,你不需要关心学习的是 Vue 2 还是 Vue 3! 如果想验证自己在单页面基础方面是否合格,很简单,请尝试自己 实现前端经典案例中的 To-Do List 功能 即可。

工程项目阶段

请阅读以下全文,其中详细分析了从工程化角度开发前端的方法以及常用的基础组件,这将让你对前端开发有全新的认知!

最后,还提供了多种 Vue 3API 写法示例,特别是包含 TypeScript 版本的 HelloWorld 项目代码示例

让我们首先大致描述一下,通过本文的最终案例 Vue 3.3 + TS 代码分享,你将会收获哪些技术内容:

  • 项目方面:
    • 一个较为完整的项目目录结构排版;
    • 一套灵活的 SvgIcon 插件导入方案;
  • Vite 方面:
    • .env 环境变量加载;
    • 自定义 Vite 模版语法插件;
    • Develop 模式下的基础配置;
  • Vue 方面:
    • 全面的基础知识(v-ifv-forv-bindv-onwatchcomputedonMounted )等;
    • 基于 Vue 3.2Composition API + Setup 语法糖方式;
    • Vue 3 + TS 定义的 Ref 响应式对象;
    • Vue 3 + TS 定义属性的父子组件间通讯(propsemitsdefinePropsdefineEmits);
    • Vue 3.3 新推出的 defineOptions 宏;
    • Vue 3.2 组件状态驱动的动态 CSSv-bind());
    • Vue 3Hooks 模块化抽离;
    • Vue 3Teleport 传送特性;
    • Vue 3Suspense 异步请求加载;
    • Vue 3 的全局配置文件 main.ts
  • CSS 方面:
    • 采用 Less 动态样式语言;
    • 采用 Flex 布局方案;
  • TypeScript 方面:
    • 接口声明;
    • 类型断言;
    • 泛型;

image.png

如果你真正按照示例实现一遍 终极版 HelloWorld 项目,会发现它几乎涵盖了你在 Vue 3 + TS 前端开发中所需的方方面面!麻雀虽小,五脏俱全!

此外,结合学习一款优秀的 UI 样式库,如 Arco Design,相信在真正接触企业级项目时,你将更加得心应手。

想说的话

在开始任何事情之前,不要为自己开脱任何的 合理性 理由。“想学游泳吗?先下水!有问题,水里解决。”

首先,你要有 “兴趣”。其次,我们再来谈论 “学习” 这件事情!

在学习中,方法至关重要。一定要先打好基础,而不是直接盲目的进入项目实战。从一个个小的案例中逐渐积累自己的成就感,而不是一开始就遭受挫折,使你失去兴趣。

在本人学习 Vue 的过程中,也曾走过很多弯路,因此总结出了一些关于前端工程化的经验和 Vue 3 项目的精华部分,旨在帮助大家少走弯路。也祝你在 Vue 3 + TypeScript 的前端学习中取得进步!

两把利器

Yarn 是一个先进的 JavaScript 包管理工具,Vite 是一个现代化的前端构建工具。这两个工具在现代前端开发中扮演着重要的角色。Yarn 确保了依赖管理的可靠性和效率,而 Vite 则提供了令人印象深刻的开发体验,为前端开发带来了速度和高效。

Yarn 包管理

概述

大厂背书,Yarn 是由 FacebookGoogleExponentTilde 联合推出的一款新的 JavaScript 包管理工具,旨在取代 NPM

Yarn 与 NPM

image.png

NPM 的缺点

  1. 安装速度慢:npm install 在拉取新项目时可能需要较长时间。这可能会影响开发人员的效率和快速启动新项目的能力。

  2. 版本一致性问题: 在同一个项目中,NPM 的版本安装无法保持一致性。这是由于 package.json 文件中的版本号表示不同的含义:

    • "5.0.3" 表示安装指定的 5.0.3 版本。
    • "~5.0.3" 表示安装 5.0.x 中最新的版本。
    • "^5.0.3" 表示安装 5.x.x 中最新的版本。

    这种不同的版本号表示方式可能导致依赖项在不同环境中产生差异,带来一致性问题。

  3. 安装过程中的错误处理问题: 当执行 NPM 安装时,多个包会同时下载和安装。如果其中一个包抛出错误,NPM 仍然会继续执行。这可能导致相关的报错信息混杂在大量 NPM 打印的警告中,有时甚至难以注意到实际发生的错误。

Yarn 的优点

特点描述
速度快Yarn 缓存了每个下载过的包(离线模式),因此再次使用时无需重复下载。同时利用并行下载以最大化资源利用率,从而提高安装速度。
版本统一为了避免出现不同的版本冲突,Yarn 使用一个锁定文件(lock file)来记录已经确切安装的模块的版本号。每次新增一个模块时,Yarn 会创建或更新 yarn.lock 文件,以保持版本一致性。
输出简洁默认情况下,Yarn 使用 emoji 打印必要的信息,使输出更加直观和简洁。
算法安全在执行代码之前,Yarn 会使用算法验证每个安装包的完整性。所有依赖包只会从一个注册来源进行安装,防止出现混乱和不一致的情况。
语义化命令Yarn 使用 yarn add/remove 替代原来的 npm install/uninstall 命令,使命令更符合语义化和易于记忆。

yarn.lock 文件

  • 为了解决 NPM 的一些缺点,Yarn 引入了 yarn.lock 机制。
  • 这个文件会锁定所有依赖模块的版本号。当执行 yarn install 命令时,Yarn 会读取这个文件来获取依赖的版本号,并根据这些版本号进行安装。这样就不再需要担心版本不一致的问题了。
  • 注意:请不要手动修改这个文件,当你使用诸如 yarn add 的操作时,Yarn 会自动更新 yarn.lock 文件。

基础命令

NPM 命令Yarn 命令
npm installyarn
npm install <package> --saveyarn add <package>
npm uninstall <package> --saveyarn remove <package>
npm install <package> --save-devyarn add -D <package>yarn add <package> --dev
npm update --saveyarn upgrade
npm install -g <package>yarn global add <package>

其它工具

作者 Zoltan Kochan 发表的 “为什么要用pnpm?

Yarn 的优点:

  • 社区支持广泛:Yarn 是一个成熟、受欢迎且有大量用户的包管理工具,社区中有很多资源和支持。
  • 兼容性:Yarn 兼容 npm 的配置文件和命令,可以无缝迁移到 Yarn
  • 稳定性:Yarn 经过长时间的发展和测试,已经在许多项目中被广泛使用,因此相对稳定。

pnpm 的优点:

  • 节省磁盘空间:pnpm 使用硬链接技术来共享已安装的包,可以节省大量磁盘空间。
  • 快速安装:由于并行安装和硬链接的使用,pnpm 安装依赖的速度通常更快。
  • 兼容性:pnpmnpmYarn 的配置文件和命令兼容,易于迁移。

image.png

Vite 构建工具

概述

Slogan ”Vite 下一代的前端工具链,为开发提供极速响应“

Vite 是一个快速且灵活的构建工具,它被广泛用于构建 Vue.js 3 项目,并且也可以用于其他 Framework(如 ReactSvelte)的开发。你可以通过访问官方网站 cn.vitejs.dev 获取更多关于 Vite 的详细信息。

image.png

主要特点

  • 快速启动Vite 启动速度快 💨,使开发者能够迅速进入开发状态。
  • HMR 支持Vite 提供热模块替换(Hot Module Replacement)功能,即在应用程序运行时更新代码,无需刷新整个页面。
  • 默认支持多种语言Vite 默认支持 TypeScriptJSXCSS 等常见的编程语言和样式表语言。
  • 多种打包模式Vite 提供了多种打包模式,可以灵活地进行应用程序的构建和优化。
  • 通用插件Vite 可以使用众多的插件来扩展功能,满足不同项目的需求。
  • 底层 API 支持 TypeScriptVite 的底层 API 是使用 TypeScript 编写的,因此开发者可以获得类型安全和更好的开发体验。

构建工具对比

特点WebpackVite
发展介绍2012 年发布,有 9 年的历史,截止到目前获得了63.4k🌟2021 年发布,约 1~2 年的历史,截止到目前获得了 58.5k🌟(尤雨溪作者)
当前版本Webpack 5Vite 4
构建速度较慢极快
开发模式依赖 HMR 实现热更新,但速度较慢且需要额外配置使用原生 ES 模块的导入语法实现热更新,速度快且无需额外配置
配置复杂需要手动配置各种文件类型的 loader 和插件,配置较为繁琐内置了常用的配置项,开箱即用,配置简单明了
生态系统成熟稳定,拥有广泛的社区支持和插件生态系统新兴框架,生态系统相对较小
插件支持支持各种插件,可根据需求灵活扩展目前插件支持相对较少,但正在快速增长
资源体积构建结果较大,会生成多个 bundle 文件构建结果较小,只生成一个或少量文件
构建方式基于中心化的构建方式,所有代码通过一个入口文件打包基于原生 ES 模块的构建方式,按需动态引入模块
开发模式加载图示image.pngimage.png

总结起来,Webpack 是一个功能强大但配置繁琐、学习曲线陡峭的打包工具,适用于处理复杂的项目和需求。它具有完整的生态系统和丰富的插件,可以满足各种需求,但在编译、资源加载和启动方面存在一定的性能缓慢问题。

相比之下,Vite 提供了更高级的 API,它本身不提供任何的编译能力。可以说 Vite 更像是一个专注于工程构建的工具,而非传统意义上的打包工具!正如俗话所说,专业的事情应该交给专业的人来处理。

运行原理

Vite 的设计目标是为了在开发环境下提供快速的冷启动和热模块替换(HMR),以提高开发效率。它使用 esbuild(一个用 Go 语言开发的 JavaScript 编译工具)进行即时构建,并利用原生 ES 模块的特性实现了快速的开发体验。

esbuild.gif

在生产环境中,Vite 并不直接进行代码打包。相反,它将项目交给 rollup.js(一个打包工具)来处理,以生成最终的生产模式代码。这样可以确保生产版本的代码更加优化、可靠,并符合常规的 JavaScript 打包结果。

        ┌───────────┐                ┌───────────────┐
        │           │                │               │
        │   Vite    ├────develop────>│   esbuild     │
        │           │                │               │
        └───────────┘                └───────────────┘
                │                    esbuild 能够快速地将 ES Module 转换成浏览器可执行的 JavaScript 代码,
                │                    由于是 Go 编写,利用其高效的 goroutine 并行构建能力,提供了快速的开发体验。
         build  │    
                │                    注: ES Module (ECMAScript Modules) 是 JavaScript 中的一种模块化系统
                │                    用于组织和管理代码的结构。它是 ES6 标准中引入的 import 模块化机制!
                │
                ▼
        ┌───────────┐
        │           │
        │  rollup   │
        │           │
        └───────────┘
        Rollup.js 可以将多个模块打包成一个或多个浏览器可识别的文件,
        同时还支持代码分割、静态分析和 Tree Shaking 等优化技术,以生成更小、更高效的生产模式代码。

VueCLI 和 Vite

Vue 项目中,有一些关于打包工具的不同点需要注意:

  • Vue 2 中的默认打包工具:在 Vue 2 中,vue-cli 是广泛使用的构建工具。它提供了一整套的脚手架工具,帮助开发者快速搭建 Vue 项目并进行打包。vue-cli 默认使用 Webpack 作为打包工具,可以处理各种文件类型、模块化加载、代码压缩等。
  • Vue 3 中推崇的打包方式:在 Vue 3 中,官方更加推崇使用 Vite 作为打包工具。Vite 是一个基于 ES 模块的构建工具,利用浏览器原生的 ES 模块导入语法来实现快速的冷启动和热模块替换(HMR)。相较于传统的 Webpack,在开发过程中,Vite 能够以原生 ES 模块的形式直接加载依赖,避免了不必要的编译和打包步骤,从而提升了开发效率。此外,Vite 还支持按需引入模块,减少了资源的冗余加载,进一步优化了性能。

需要注意的是,尽管在 Vue 3 中推崇使用 Vite 作为打包工具,但仍然可以选择使用 Webpack 进行打包。具体选择哪个打包工具,取决于项目的需求和个人偏好。

初次体验

尽管前端领域涌现了许多不同的 JavaScript 库,但如果要我在这些选择中挑选一个最为重要且具备高度定制性的库,我毫不犹豫地会为 ESLint 投下一票!当然,Babel 也同样重要。这也是为什么在使用诸如 Vue CLI 这样的工具创建项目时,它们会默认集成这两个工具的原因之一。

构建项目

使用 VueCLI 创建 Vue 2 项目

  1. 安装 Vue CLI 工具:
➜ npm install -g @vue/cli

➜ vue --version
@vue/cli 5.0.8
  1. 交互式安装:
➜ vue create <project-name>

# 进入交互界面,选择安装:
Vue CLI v5.0.8
? Please pick a preset:
  Default ([Vue 3] babel, eslint)
❯ Default ([Vue 2] babel, eslint)        # Vue2 项目默认将搭配 babel 和 eslint
  Manually select features

# 安装完毕后,提示下面命名:
$ cd <project-name>
$ yarn serve

提示:Vue CLI 3.x 是默认支持 Vue 3 的版本,不再支持直接创建 Vue 2 项目,因此只能使用交互式。

vue2-demo.gif

使用 Vite 创建 Vue 3 项目

  1. 安装 Yarn 工具:
# Vite 在默认情况下会随 Yarn 1.22+ 版本一起安装
➜ npm install -g yarn

# 如果您使用的是旧版本的 Yarn(低于1.22),则可能需要先全局安装 Vite CLI,然后使用 vite 命令来创建项目
➜ yarn global add create-vite
➜ vite <project-name>
  1. 非交互式安装:
# Vue 3 + JavaScript 方案
➜ yarn create vite <project-name> --template vue

# Vue 3 + TypeScript 方案
➜ yarn create vite <project-name> --template vue-ts

提示:Vite 不仅可以用于初始化 Vue 3 项目,还支持其他流行的前端框架,例如 Vanilla(纯原生 JavaScript)、ReactSvelteSolid 等。

vue3-demo.gif

开启 ESLint

运行报错

  1. 来看下面这样一段 Vue 2 的代码示例:
<template>
  <div>
    <p>正序: {{ message }}</p>
    <p>反序: {{ reversedMsg }}</p>
  </div>
</template>

<script>
  export default {
    name: 'Demo',                        // 组件的名称,用于 Vue 内部和开发工具
    data() {
      return {
        message: 'Hello, world!'         // 一个响应式的数据属性,初始值为 'Hello, world!'
      };
    },
    computed: {
      reversedMsg() {                    // 计算属性用于生成反序的消息
        let reversed = '';
        for (let i = this.message.length - 1; i >= 0; i--) {
          reversed += this.message[i];
        }
        return reversed;
      }
    },
    created() {
      console.log(this.message);         // 在组件被创建时打印消息到控制台
    }
  }
</script>

<style scoped>
  /* Your scoped styles here */
</style>
  1. 运行发生了 webpack 编译报错,实际为 ESlint 规则触发产生:
➜ yarn serve
yarn run v1.22.19
$ vue-cli-service serve
 INFO  Starting development server...

You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
ERROR in [eslint] 
/Users/mystic/vue3/demo/vue2-demo/src/components/Demo.vue
  19:53  error  Unary operator '--' used  no-plusplus

✖ 2 problems (1 error, 1 warning)

webpack compiled with 1 error

eslint-err.png

ESlint 配置

ESLint 是用于规范前端代码和执行代码检查的 JavaScript 库。ESLint 用于确保代码符合一致的编码标准,并提供了可配置的规则集,以便开发团队可以根据项目需求进行定制,以达到提高代码质量和可维护性的目标。ESLint 在前端开发中被广泛使用,可以帮助开发人员避免一些常见的错误、潜在的问题和不规范的写法,从而提高代码的可读性和一致性。

  1. 在项目根路径下,修改 .eslintrc.js 配置文件:
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: ["eslint:recommended", "plugin:vue/essential"],
  rules: {
    // 自定义规则(对于每条 Linter 规则可按需降级或添加)
    "no-console": "warn",         // 此规则将控制台调用 console.log 警告,以防止在生产环境中留下不必要的调试代码
    'no-plusplus': "error",       // 此规则将不允许使用一元递增/递减运算符,例如 i++ 或 i-- ,会报错
    "vue/multi-word-component-names": "off",    // 此规则要求组件名称使用多个单词
    ...
  },
};
  1. 当修改完毕,重新运行使 linter 生效,我们再来看下最终的前端展示:

image.png

前端工程

前端工程是集成了一揽子 JavaScript 工具库的集合,尽管在编写业务代码时我们不必深入了解每个库的内部工作原理,但了解它们的基本功能、配置、插件的使用等,将有助于我们更加熟练地进行开发,能事半功倍。

约定式提交

众所周知,最著名的 Git 约定式提交规范Git Commit Convention)是由 Angular 前端团队首先定义和提出的。但为什么没有哪个后端语言团队提出类似的约定呢?个人猜测,一方面可能是因为后端更注重并发性能和数据安全,另一方面可能是因为后端语言太多,很难达成统一的规范,只能各自遵循各自的规范。

<type>(<scope>): <short summary>
  │       │             │
  │       │             └─⫸ Summary in present tense. Not capitalized. No period at the end.
  │       │
  │       └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
  │                          elements|forms|http|language-service|localize|platform-browser|
  │                          platform-browser-dynamic|platform-server|router|service-worker|
  │                          upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve|
  │                          devtools
  │
  └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test

协同工具库

由于前端的技术栈相对固定,主要包括 HTMLCSSJavaScript。在这个相对固定的基础上,上层的库和框架的发展就更容易达成统一的规范。

前端项目通常会使用多个 JavaScript 库和框架,这些工具能够协同工作,帮助你构建强大的应用程序。以下是一些常见的前端项目中搭配使用的 JavaScript 库和框架:

库/框架说明
Vue.js前端开发中非常流行的组件化框架。Vue.js 能帮助你构建可维护的用户界面,提供了虚拟 DOM 和组件化开发的概念。
VuexPinia用于状态管理的库。这些库帮助你管理应用程序的状态,确保不同组件之间的状态同步和管理。
Vue Router用于管理单页应用程序 (SPA) 路由的库。它们能够帮助你在应用程序中实现页面之间的导航和路由管理。
AxiosFetch用于发起网络请求的库。它们能够帮助你从服务器获取数据,与后端进行交互。
Babel用于将 ES6+ 代码转换为浏览器支持的旧版 JavaScript 代码的工具。Babel 也可以用来处理 JSX 语法,将其转换为普通的 JavaScript 代码。
WebpackVite构建工具,用于打包、优化和构建前端项目。它们能够处理模块、资源文件、代码分割等任务。
Mock用于生成接口模拟数据,它允许我们创建虚拟的替代品,以便快速测试接口和模拟数据。这种方式帮助我们在前后端分离开发模式中能够独立地进行前端和后端的开发,而无需依赖真实的后端服务。
Jest测试框架,用于编写和运行单元测试和集成测试,确保代码的正确性。
ESLintPrettier代码规范和静态代码分析工具。它们能够帮助你强制执行一致的代码风格,并捕获潜在的错误。
Lodash实用工具库,提供了许多常用的函数和工具,简化了编码。
UI 框架例如 Element UI PlusAnt DesignArco Design 等,这些框架提供了丰富的 UI 组件,可以加速界面开发。

最佳搭配

推荐一套当前主流的技术栈搭配方案,使用以下技术栈进行项目开发:

  • 使用 Vue + ES6 + TypeScript + Vite 进行项目构建;

  • 另外 Pinia + Vue Router + Axios + Echart 是你大概率会用到的三方库;

  • UI 库选择也非常多,我们主推自家产品 Arco Design

目录结构

我们来看看标准的前端目录树都应包含哪些内容,这里以 Vue 3 + Ts 项目为例:

.
├── babel.config.js                      # Babel 的配置文件,用于转译 ES6+ 的代码到 ES5 以便浏览器能够兼容运行
├── components.d.ts                      # 用于定义全局组件的类型声明文件
├── config                               # 用于存放 Vite 的配置文件
├── index.html                           # 项目的入口 HTML 文件
├── node_modules                         # 存放 NPM 或 Yarn 安装的所有依赖包
├── package.json                         # 项目的基本信息及依赖包配置文件
├── src                                  # 存放项目的源代码,包括组件、工具函数、静态资源等
│    ├── App.vue                         # Vue 应用的根组件
│    ├── api                             # 存放与后端接口通信的代码
│    ├── assets                          # 存放静态资源,如图片、字体等
│    ├── components                      # 存放共用抽象组件
│    ├── config                          # 存放应用配置信息
│    ├── directive                       # 存放自定义指令
│    ├── env.d.ts                        # 存放环境变量类型声明
│    ├── hooks                           # 存放自定义 Hooks
│    ├── icons                           # 存放 SVG 图标文件
│    ├── layout                          # 存放应用布局相关组件
│    ├── locale                          # 存放 i18n 多语言相关代码
│    ├── main.ts                         # 应用入口文件
│    ├── mock                            # 存放模拟数据
│    ├── router                          # 存放路由相关代码
│    ├── store                           # 存放 Pinia 状态管理相关代码
│    ├── types                           # 存放类型声明
│    ├── utils                           # 存放工具函数
│    └── views                           # 存放页面组件
├── tsconfig.json                        # TypeScript 的配置文件,用于配置 TypeScript 的编译选项
└── yarn.lock                            # Yarn 的锁文件,用于锁定依赖包的版本

提示:更详细目录结构可参考字节跳动 Arco 前端团队出品的 arco-design-pro-vue 项目脚手架。

回顾 Vue 2

⚠️ Vue 2 将于 2023 年 12 月 31 日停止维护!

什么是 Vue

Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTMLCSSJavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

两个核心功能:

  • 声明式渲染Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTMLJavaScript 状态之间的关系。
  • 响应性Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM

渐进式框架

Vue 是一个渐进式框架的概念是指它具有逐步增强的特性,允许开发者按需选择使用框架的不同功能。这种设计理念使得 Vue 非常灵活和易于集成到现有项目中。

1. 逐步增强: Vue 允许你从一个简单的 JavaScript 库开始使用,然后逐步引入更多的功能和工具来满足你的需求。你可以只使用 Vue 的核心库来构建简单的交互页面,或者结合 Vue 的路由、状态管理、表单验证等插件来构建复杂的单页应用。

2. 灵活性: Vue 提供了一系列的特性和功能模块,你可以根据项目的需求选择性地使用它们。无论是构建小型应用还是大型复杂的应用程序,你都可以根据需要选取不同的功能,而无需引入整个框架。

3. 可组合性: Vue 的各个功能模块都被设计为独立可组合的,你可以方便地将它们与其他工具和库进行集成。例如,你可以将 Vue 与其他流行的前端库(如 ReactAngular)结合使用,或者将 Vue 嵌入到已存在的项目中,而不需要重写整个应用程序。

4. 渐进增强: Vue 允许你在现有的 HTMLCSSJavaScript 代码中逐步引入 Vue 的功能,而无需全面改变现有的代码结构。这使得迁移到 Vue 变得更加容易,并且可以平滑地与现有的技术栈进行集成。

最基本示例

这段代码是一个 MinimalVue 组件。它包含了一个按钮和一个计数器变量 count。每次点击按钮时,count 的值会递增,并在界面上显示。

<template>
  <div id="app">
    <button @click="count++">
      Count is: {{ count }}
    </button>
  </div>
</template>

<script>
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        count: 0
      }
    }
  }).mount('#app')
</script>

开始 Vue 3

诞生的动机

Vue 2 已经足够简单易用了,为什么要考虑升级到 Vue 3?Vue 3 有哪些优势呢?

代码结构编排

附上热力图一张,很清晰的描述了 Vue 2Vue 3 在代码编排上理念的不同!

vue2-vue3.png

可以看到,在 Vue 2 中,为了实现同一个功能,相关变量、方法、计算属性、生命周期钩子、监听函数等逻辑被分散到不同的代码块中。随着功能的增加,复杂组件的代码变得越来越难以维护。如果需要修改某个功能,可能需要频繁滚动滚轮或使用 Ctrl+F 来查找关键字。

Mixin 的缺陷

Vue 2 中,尽管 Mixin 可以解决按特征分类的问题,但也存在一些缺点:

  • 命名冲突:由于不同的 Mixin 可能定义相同的变量或方法,可能会导致命名冲突,使代码难以维护和理解。
  • 不清楚暴露出来变量的作用:当一个组件使用了多个 Mixin 时,很难清楚地知道每个 Mixin 暴露出来的变量和方法的作用和来源,增加了理解和调试的难度。
  • 重用到其他组件经常会遇到问题:将一个组件中的 Mixin 应用到其他组件时,可能会面临各种兼容性和依赖关系的问题,使得复用变得困难并且容易引入 BUG

什么是 Mixin 呢?举个例子

当我们在不同的组件中需要实现相似的逻辑和功能时,可以将这些相关的代码逻辑按特征分类进行封装。

假设我们有两个组件:ButtonComponent(按钮组件)和 FormComponent(表单组件),它们都具备类似的表单验证功能。

Vue 2 中,我们可以创建一个名为 formValidationMixinMixin,其中定义了一些共享的表单验证方法和属性。然后,我们可以将这个 Mixin 应用到 ButtonComponentFormComponent 中,以便复用该表单验证逻辑。

// formValidationMixin.js
export default {
  methods: {
    validateForm() {
      // 表单验证逻辑...
    },
    resetForm() {
      // 重置表单逻辑...
    }
    // 其他共享的表单相关方法...
  }
}

// ButtonComponent.vue
<template>
  <button @click="validateForm">Submit</button>
</template>

<script>
import formValidationMixin from './formValidationMixin';

export default {
  mixins: [formValidationMixin]
}
</script>

// FormComponent.vue
<template>
  <form @submit.prevent="validateForm">
    <!-- 表单输入字段... -->
    <button type="submit">Submit</button>
  </form>
</template>

<script>
import formValidationMixin from './formValidationMixin';

export default {
  mixins: [formValidationMixin]
}
</script>

无法支持 TS

Vue 2 对于 Typescript 的支持是十分有限的,其具体表现如下:

  • Vue 2 中,依赖于 this 上下文向外暴露属性,但在处理 this 上下文时,引入了一些复杂的机制,这使得 Vue 2API 在设计之初没有考虑到与 TypeScript 集成和类型推断的问题。
  • 然而,在 Vue 3 中,API 的设计更加简洁,大部分使用普通的 JavaScript 对象和方法,并且天生就对 TypeScript 更友好。特别是在使用 Composition API 时,能够与 TypeScript 的类型推断完美搭配,这样可以加快我们开发出更严谨、更少 BUG 的代码。

新特性详解

Teleport 传送

Teleport 是一个内置组件,它可以将一个组件内部的一部分模板 “传送” 到该组件的 DOM 结构外层的位置去。

Dialog 使用场景

  • 有时候我们可能会面临这样的情况:一个组件模板的一部分在逻辑上属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
  • 如果按照正常的逻辑编写代码,Dialog 对话框可能会被嵌套在非常深的 HTML 标签之下,这可能导致编写和维护该模态框或对话框变得非常困难,并且容易遇到 CSS 冲突的问题。
  • 从用户的角度来看,Dialog 对话框应该是一个独立的组件,显示在屏幕中央。然而,从 DOM 的渲染结果来看,Dialog 被包裹在其他组件之中,容易受到干扰。此外,由于样式定义在其他组件中,代码也容易变得混乱不堪。

传统嵌套写法

子组件:

<template>
  <div id="center">
    <h2>this is a modal</h2>
  </div>
</template>

<style>
  #center {
    width: 200px;
    height: 200px;
    border: 2px solid black;
    background: white;
    position: fixed;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
  }
</style>

父组件:

<template>
  <div id="main">
    <Modal />
  </div>
</template>

<script>
  import { defineComponent } from 'vue'
  import Modal from './components/Modal.vue'

  export default defineComponent({
    name: 'App',
    components: {
      Modal,
    }
  });
</script>

效果展示:

modal-inner.png

使用瞬移特性

子组件:

<template>
  <!-- Teleport 的参数 to 代表需要渲染到哪个指定的 DOM 元素上 -->
  <Teleport to="#modal">
    <div id="center">
      <h2>this is a modal</h2>
    </div>
  </Teleport>
</template>

<style>
  #center {
    width: 200px;
    height: 200px;
    border: 2px solid black;
    background: white;
    position: fixed;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
  }
</style>

挂载元素:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue + TS</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- 创建一个 modal 节点,作为一个根元素 -->
    <div id="modal"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

效果展示:

modal-outer.png

SFC CSS 特性

组件作用域 CSS

scope 属性

<style> 标签带有 scoped attribute 的时候,它的 CSS 只会影响当前组件的元素。

<style scoped>
  .example {
    color: red;
  }
</style>

<template>
  <div class="example">hi</div>
</template>

上面的代码会被编译成:

<style>
  .example[data-v-f3f3eg9] {
    color: red;
  }
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

深度选择器

处于 scoped 样式中的选择器如果想要做更“深度”的选择,即影响到子组件,可以使用 :deep() 这个伪类。

<style scoped>
  .a :deep(.b) {
    /* ... */
  }
</style>

上面的代码会被编译成:

<style>
  .a[data-v-f3f3eg9] .b {
    /* ... */
  }
</style>

全局选择器

如果想让其中一个样式规则应用到全局,比起另外创建一个 <style>,可以使用 :global 伪类来实现。

<style scoped>
  :global(.red) {
    color: red;
  }
</style>

CSS 中的 v-bind()

Vue 3.2 的最新更新中,引入了组件状态驱动的动态 CSS 值功能,这意味着我们可以在 CSS 样式中使用简单的 JS 表达式来实现动态效果。

<template>
	<div class="container">高度为: {{ logoHeight }}</div>
</template>

<script setup>
	const logoHeight = 50
</script>

<style scoped>
  // 为 container 指定高度
  .container {
    height: v-bind(logoHeight) + 'px'; 
  }
</style>

传统方式是通过使用 :class 指令来动态绑定 class,根据变量的真假来动态控制元素是否应用某个 class,实现灵活的样式控制。

<template>
  <div :class="{ 'container': showContainer }">高度为: 50</div>
</template>

<script setup>
  const showContainer = true;    // 控制是否显示.container类
</script>

<style scoped>
  // 为 container 指定高度
  .container {
    height: 50px; 
  }
</style>

常用插件

SvgIcon 支持全局图标

  1. 安装 dependencies 库:
➜ yarn add vite-plugin-svg-icons fast-glob
  1. 编辑 Vite 配置文件:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

export default defineConfig({
  plugins: [
    vue(),
    // 启用 vite-plugin-svg-icons 插件
    createSvgIconsPlugin({
       // 指定需要缓存的图标目录位置
       iconDirs: [resolve(process.cwd(), './src/icons/svg')],
     })
  ],
})
  1. 下载并添加一个 SVG 图片:
# 新增 src/icons 目录,层级结构如下
src/
└── icons/
    ├── index.vue
    └── svg/
        └── mickey.svg   # 添加一张米老鼠的 SVG
图标网站说明
iconpark字节开源项目
iconfont阿里开源项目
iconfinder丹麦哥本哈根一家专门提供图形资源的平台
  1. 修改 SVG 文件代码:
<svg
  t="1691739809030"
  class="icon"
  viewBox="0 0 1024 1024"
  version="1.1"
  xmlns="http://www.w3.org/2000/svg"
  p-id="3795"
  width="200"
  height="200"
>
  <!-- 将 fill 属性从默认改为 currentColor 的目的是为了可以使颜色背景发生动态变化! -->
  <path
    d="M229.12 311.68m-165.12 0a165.12 165.12 0 1 0 330.24 0 165.12 165.12 0 1 0-330.24 0Z"
    fill="currentColor"
    p-id="3796"
  ></path>
  <path
    d="M794.88 311.68m-165.12 0a165.12 165.12 0 1 0 330.24 0 165.12 165.12 0 1 0-330.24 0Z"
    fill="currentColor"
    p-id="3797"
  ></path>
  <path
    d="M517.973333 582.826667m-294.826666 0a294.826667 294.826667 0 1 0 589.653333 0 294.826667 294.826667 0 1 0-589.653333 0Z"
    fill="currentColor"
    p-id="3798"
  ></path>
</svg>
  1. 新增 src/icons/index.vue 文件:
<template>
  <div>
    <!-- SVG 图标 -->
    <svg :class="svgClass" v-bind="$attrs" :style="{ color: color }">
      <!-- 从 SVG 2 规范开始,不再需要使用 xlink:href 属性,而是直接使用 href 属性来引用图像 -->
      <use :href="iconName"></use>
    </svg>
  </div>
</template>

<script setup>
  import { computed } from 'vue';

  // 定义 Props
  const props = defineProps({
    name: {
      type: String,
      required: true,
    },
    color: {
      type: String,
      default: '',
    },
  });

  // 计算图标名称
  const iconName = computed(() => `#icon-${props.name}`);

  // 计算 SVG 图标的类名
  const svgClass = computed(() => {
    if (props.name) return `svg-icon icon-${props.name}`;
    return 'svg-icon';
  });
</script>

<style scoped>
  /* SVG 图标样式 */
  .svg-icon {
    width: auto;             /* 使 SVG 图标的宽度自适应其内容大小 */
    height: auto;            /* 使 SVG 图标的高度自适应其内容大小 */
    fill: currentColor;      /* 使 SVG 图标的颜色与其所在元素的文本颜色保持一致(currentColor 是一个特殊的CSS变量) */
    vertical-align: middle;  /* 使 SVG 图标实现垂直居中的效果 */
    overflow: hidden;        /* 使 SVG 图标在其指定的尺寸范围内显示,超出部分将被裁剪隐藏 */
  }
</style>
  1. 修改 main.js 文件:
import { createApp } from 'vue'
import 'virtual:svg-icons-register'       // 导入 SVG 图标注册器
import './style.css'                      // 导入样式文件
import App from './App.vue'               // 导入根组件
import SvgIcon from './icons/index.vue'   // 导入自定义的图标组件

// 创建 Vue 应用实例
const app = createApp(App);

// 注册全局组件 <svg-icon />
app.component('svgIcon', SvgIcon);

// 挂载应用到 DOM 元素
app.mount('#app');
  1. view 业务代码中使用:
<template>
  <div>
    <!-- 引用一张红色的默认大小的米老鼠图标 -->
    <svg-icon name="mickey" color="red" />

    <!-- 引用一张蓝色的只有 1em 大小的米老鼠图标 -->
    <svg-icon
      name="mickey"
      color="blue"
      width="1em"
      height="1em"
    />
  </div>
</template>

项目准备

信息描述

版本依赖

{
  "name": "vue3-demo",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": {
    "fast-glob": "^3.3.1",
    "less": "^4.2.0",
    "path": "^0.12.7",
    "vite-plugin-svg-icons": "^2.0.1",
    "vue": "^3.3.4"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3",
    "vite": "^4.4.5"
  }
}

目录结构

➜ tree -a
.
├── .env.development
├── .gitignore
├── README.md
├── index.html
├── node_modules
├── package.json
├── public
├── src
│   ├── App.vue                   # 演示父组件
│   ├── assets
│   ├── components
│   │   └── HelloWorld.vue        # 演示子组件
│   ├── icons
│   │   ├── index.vue
│   │   └── svg
│   │       └── close.svg
│   ├── main.js
│   └── style.css
├── vite.config.js
└── yarn.lock

完整配置

环境配置文件

当你在开发模式下启动应用程序时,Vite 将自动读取并加载 .env.development 文件中的配置,并将其作为环境变量供应用程序使用。这样可以方便地管理不同环境下的配置,让开发人员能够轻松地切换和调整开发环境的设置。

BACKEND_URL = 'http://127.0.0.1:8888'
DEBUG_MODE = true

Vite 配置文件

vite.config.js 文件:

import { resolve } from 'path';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

// 设置后端 URL
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8080';

// https://vitejs.dev/config/
export default defineConfig({
  // 使用的插件
  plugins: [
    vue(),
    createSvgIconsPlugin({
       iconDirs: [resolve(process.cwd(), './src/icons/svg')],
     })
  ],

  // 模块解析
  resolve: {
    alias: [
      {
        find: '@',
        replacement: resolve(__dirname, './src'),
      },
    ],
    extensions: ['.js'],
  },

  // 定义全局变量
  define: {
    'process.env': {},
  },

  // 开发模式
  mode: 'development',

  // 服务器设置
  server: {
    open: true,
    host: '127.0.0.1',
    port: 3000,
    fs: {
      strict: true,
    },
    proxy: {
      '/api': {
        target: `${backendUrl}/api/v1`,
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
        configure: (proxy, options) => {},
      },
    },
  },
})

注:具体 SvgIcon 相关配置见前一章节。

页面效果

最终,我们 HelloWorld.vue 组件在前端页面呈现的效果如下所示:

vue3-api.gif

API 风格

灵活的组合方式

只要你喜欢,Vue 3 提供了多种灵活的组合方式供你选择:

关于 SFC(Vue):

  • 你可以继续使用 Vue 2Option API 风格;
  • 当然更推荐尝试 Vue 3Composition API 风格;
  • 甚至更加推荐使用 Vue 3.2 带来的 Setup 语法糖写法;✅
  • 甚至更更推荐使用今年最新发布的 Vue 3.3 版本,以获得更加友好的 TypeScript 支持,聚焦提升开发体验,无增加新特性。

关于 JS/TS:

  • 你也可以继续使用 Vue 2 JavaScript
  • 但更值得推荐的是搭配 Vue 3TypeScript;✅ (若使用,请好好使用,拒绝 AnyScript

总之,由于 Vue 的渐进式框架特性,你能够轻松地与其他流行框架进行集成。当然,你也可以让你的 Vue 代码与 React 风格非常相似,甚至完全使用更高级的 JSX / TSX 语法。就像开头说的那样,一切只要你喜欢!

选项式 API

在 Vue 3 中,你可以使用 Vue 2 的方式编写 Vue 组件,将属性、方法、计算属性、生命周期钩子函数和 Watch 监测函数等分散在不同的代码块中,这种方式在 Vue 2 中称为 Option API。

父组件:

<template>
  <div>
    <!-- 显示 "打开窗体" 按钮,点击时调用 showPanel 方法 -->
    <button v-if="!visible" @click="showPanel">打开窗体</button>

    <!-- 当 visible 变量为 true 时显示 HelloWorld 组件,
         同时传递 list 数组作为 buttonText 属性,
         并监听 closeVisible 事件来调用 showPanel 方法 -->
    <HelloWorld
      v-if="visible"
      :button-text="list"
      @close-visible="showPanel"
    />
  </div>
</template>

<script>
  import HelloWorld from './components/HelloWorld.vue';

  export default {
    // 引用组件
    components: {
      HelloWorld,
    },

    // 定义属性
    data() {
      return {
        // 响应式引用类型变量 list
        list: ['👍 点赞', '🥏 投币', '🎲 三连'],
        // 响应式布尔类型变量 visible,初始值为 true
        visible: true,
      }
    },

    // 定义方法
    methods: {
      // 定义 showPanel 方法,切换 visible 的值
      showPanel() {
        this.visible = !this.visible
      }
    },
  }
</script>

子组件:

<template>
  <div class="container">
    <div class="panel-header">
      <div class="close-icon" @click="closeVisible">
        <svg-icon name="close" width="1.5em" height="1.5em" title="关闭" />
      </div>
    </div>
    <div class="panel-content">
      <img alt="logo" src="@/assets/awesome.svg" />
      <h3>方法:{{ count }}</h3>
      <h3>计算属性:{{ double }}</h3>
      <button
        v-for="(text, idx) in buttonText"
        :key="idx"
        @click="increment"
      >
        {{ text }}
      </button>
    </div>
  </div>
</template>

<script>
  import { defineComponent } from 'vue';

  export default defineComponent({
    // 子组件名称
    name: 'HelloWorld',

    // 使用 props 选项接收外部传递的值
    props: {
      buttonText: {
        type: Array,
        required: true,
        validator: function (value) {
          // 验证传入的值是否为字符串数组
          return Array.isArray(value) && value.every(item => typeof item === 'string');
        },
      },
    },

    // data() 返回的属性将会成为响应式的状态
    // 并且暴露在 `this` 上
    data() {
      return {
        count: 0,
      }
    },

    // methods 是一些用来更改状态与触发更新的函数
    // 它们可以在模板中作为事件处理器绑定
    methods: {
      increment() {
        this.count++
      },

      // 关闭面板时触发 closeVisible 事件
      closeVisible() {
        this.$emit('closeVisible');
      },
    },

    // 计算属性
    computed: {
      double() {
        return this.count * 2
      }
    },

    // 生命周期钩子会在组件生命周期的各个不同阶段被调用
    // 例如这个函数就会在组件挂载完成后被调用
    mounted() {
      console.log(`The initial count is ${this.count}.`)
    },

    // 添加一个 watch 监听器来观察 count 变量的变化并执行相应的操作
    watch: {
      count(newValue, oldValue) {
        console.log(`count changed from ${oldValue} to ${newValue}.`)
      }
    },
  })
</script>

<style scoped lang="less">
  .container {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);

    border: 1px solid black;
    border-radius: 10px;
    padding: 40px 40px;
    width: 500px;
    height: 280px;

    .panel-header {
      display: flex;
      justify-content: flex-end;
      align-items: center;

      .close-icon {
        color: grey; /* 默认灰色 */
        cursor: pointer;
      }

      .close-icon:hover {
        color: blue; /* 鼠标悬停时蓝色 */
      }
    }

    .panel-content {
      text-align: center;

      img {
        width: 120px;
      }

      button {
        margin-right: 20px;
      }
    }
  }
</style>

组合式 API

在 Vue 3 中,我们更推荐使用 Composition API 来开发组件。其中,与传值、父子组件通讯(props、emits)、事件触发以及属性定义的方式上存在较大的区别!下面我们一一介绍。

index.html

由于 Vue 3 的示例全部使用到了新特性 Teleport 组件,因此,在这里提供项目的 index.html 文件内容。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="https://vitejs.dev/logo.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue3</title>
  </head>
  <body>
    <div id="app"></div>
    <div id="card"></div>   <!-- 真正子组件的部分被转移到了这里!-->
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Vue 3.0 + JS 版本

父组件:

<template>
  <button v-if="!visible" @click="showPanel">打开窗体</button>
  <HelloWorld
    v-if="visible"
    :button-text="list"
    @close-visible="showPanel"
  />
</template>

<script>
  import { defineComponent, ref } from 'vue';
  import HelloWorld from '@/components/HelloWorld.vue';

  export default defineComponent({
    // 组件的名称为 App
    name: 'App',

    // 注册 HelloWorld 组件
    components: {
      HelloWorld,
    },

    // 设置组件的数据和方法
    setup() {
      // 响应式引用类型变量 list
      const list = ref(['👍 点赞', '🥏 投币', '🎲 三连']);

      // 响应式布尔类型变量 visible,初始值为 true
      const visible = ref(true);

      // 定义 showPanel 方法,切换 visible 的值
      const showPanel = () => {
        visible.value = !visible.value;
      }

      // 将对象暴露给模板
      return {
        list,
        visible,
        showPanel,
      }
    }
  })
</script>

子组件:

<template>
  <Teleport to="#card">
    <div class="container">
      <div class="panel-header">
        <div class="close-icon" @click="closeVisible">
          <svg-icon name="close" width="1.5em" height="1.5em" title="关闭" />
        </div>
      </div>
      <div class="panel-content">
        <img alt="logo" src="@/assets/awesome.svg" />
        <h3>方法:{{ count }}</h3>
        <h3>计算属性:{{ double }}</h3>
        <button
          v-for="(text, idx) in buttonText"
          :key="idx"
          @click="increment"
        >
          {{ text }}
        </button>
      </div>
    </div>
  </Teleport>
</template>

<script>
  import {
    defineComponent,
    ref,
    computed,
    onMounted,
    watch,
  } from 'vue';

  export default defineComponent({
    // 组件的名称为 HelloWorld
    name: 'HelloWorld',

    // 使用 props 选项接收外部传递的值
    props: {
      buttonText: {
        type: Array,
        required: true,
      },
    },

    // 声明 closeVisible 事件
    emits: ['closeVisible'],

    // 设置组件的数据和方法
    setup(props, context) {
      // 响应式对象 ref 属性
      const count = ref(0);                // 类型 Ref<number>

      // 计算属性
      const double = computed(() => {      // 类型 ComputedRef<number>
        return count.value * 2
      });

      // 定义方法
      const increment = () => {
        count.value++
      };

      // 生命周期钩子
      onMounted(() => {
        console.log(`The initial count is ${count.value}.`)
      })

      // watch 监听器
      watch(
        () => count.value,
        (newValue, oldValue) => {
           console.log(`count changed from ${oldValue} to ${newValue}.`)
        }
      )

      // 关闭面板时触发 closeVisible 事件
      const emit = context.emit;
      const closeVisible = () => {
        emit('closeVisible');
      }

      // 将对象暴露给模板
      return {
        count,
        increment,
        double,
        closeVisible,
      }
    },
  })
</script>

<style scoped lang="less">
  .container {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);

    border: 1px solid black;
    border-radius: 10px;
    padding: 40px 40px;
    width: 500px;
    height: 280px;

    .panel-header {
      display: flex;
      justify-content: flex-end;
      align-items: center;

      .close-icon {
        color: grey; /* 默认灰色 */
        cursor: pointer;
      }

      .close-icon:hover {
        color: blue; /* 鼠标悬停时蓝色 */
      }
    }

    .panel-content {
      text-align: center;

      img {
        width: 120px;
      }

      button {
        margin-right: 20px;
      }
    }
  }
</style>

Vue 3.2 + JS 版本

父组件:

<template>
  <button v-if="!visible" @click="showPanel">打开窗体</button>
  <HelloWorld
    v-if="visible"
    :button-text="list"
    @close-visible="showPanel"
  />
</template>

<script setup>
  import { ref } from 'vue';
  import HelloWorld from '@/components/HelloWorld.vue';

  // 响应式引用类型变量 list
  const list = ref(['👍 点赞', '🥏 投币', '🎲 三连']);

  // 响应式布尔类型变量 visible,初始值为 true
  const visible = ref(true);

  // 定义 showPanel 方法,切换 visible 的值
  const showPanel = () => {
    visible.value = !visible.value;
  }
</script>

<script>
	import { defineComponent } from 'vue';

  export default defineComponent({
    name: 'App',
  });
</script>

子组件:

<template>
  <Teleport to="#card">
    <div class="container">
      <div class="panel-header">
        <div class="close-icon" @click="closeVisible">
          <svg-icon name="close" width="1.5em" height="1.5em" title="关闭" />
        </div>
      </div>
      <div class="panel-content">
        <img alt="logo" src="@/assets/awesome.svg" />
        <h3>方法:{{ count }}</h3>
        <h3>计算属性:{{ double }}</h3>
        <button
          v-for="(text, idx) in props.buttonText"
          :key="idx"
          @click="increment"
        >
          {{ text }}
        </button>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
  import {
    ref,
    computed,
    onMounted,
    watch,
  } from 'vue';

  // 使用 props 选项接收外部传递的值
  const props = defineProps({
    buttonText: Array,
  });

  // 定义自定义事件
  const emit = defineEmits(['closeVisible']);
  
  // 关闭面板时触发 closeVisible 事件
  const closeVisible = () => {
    emit('closeVisible');
  }

  // 响应式对象ref属性
  const count = ref(0);                // 类型 Ref<number>

  // 计算属性
  const double = computed(() => {      // 类型 ComputedRef<number>
    return count.value * 2
  });

  // 定义方法
  const increment = () => {
    count.value++
  };

  // 生命周期钩子
  onMounted(() => {
    console.log(`The initial count is ${count.value}.`)
  });

  // watch 监听器
  watch(
    () => count.value,
    (newValue, oldValue) => {
      console.log(`count changed from ${oldValue} to ${newValue}.`)
    }
  );
</script>

<script>
  import { defineComponent } from 'vue';

  // Vue 3.2 setup 只能接受混编方式,真的很丑陋! 但是 Vue 3.3 版本解决了这个问题!
  export default defineComponent({
    name: 'HelloWorld',
  })
</script>

<style scoped lang="less">
  .container {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  
    border: 1px solid black;
    border-radius: 10px;
    padding: 40px 40px;
    width: 500px;
    height: 280px;
  
    .panel-header {
      display: flex;
      justify-content: flex-end;
      align-items: center;
  
      .close-icon {
        color: grey; /* 默认灰色 */
        cursor: pointer;
      }
  
      .close-icon:hover {
        color: blue; /* 鼠标悬停时蓝色 */
      }
    }
  
    .panel-content {
      text-align: center;
  
      img {
        width: 120px;
      }
  
      button {
        margin-right: 20px;
      }
    }
  }
</style>

Vue 3.3 + JS 版本

父组件:

<template>
  <button v-if="!visible" @click="showPanel">打开窗体</button>
  <HelloWorld
    v-if="visible"
    :button-text="list"
    @close-visible="showPanel"
  />
</template>

<script setup>
  import { ref } from 'vue';
  import HelloWorld from '@/components/HelloWorld.vue';

  defineOptions({ name: 'App', inheritAttrs: false });

  // 响应式引用类型变量 list
  const list = ref(['👍 点赞', '🥏 投币', '🎲 三连']);

  // 响应式布尔类型变量 visible,初始值为 true
  const visible = ref(true);

  // 定义 showPanel 方法,切换 visible 的值
  const showPanel = () => {
    visible.value = !visible.value;
  }
</script>

子组件:

<template>
  <Teleport to="#card">
    <div class="container">
      <div class="panel-header">
        <div class="close-icon" @click="closeVisible">
          <svg-icon name="close" width="1.5em" height="1.5em" title="关闭" />
        </div>
      </div>
      <div class="panel-content">
        <img alt="logo" src="@/assets/awesome.svg" />
        <h3>方法:{{ count }}</h3>
        <h3>计算属性:{{ double }}</h3>
        <button
          v-for="(text, idx) in props.buttonText"
          :key="idx"
          @click="increment"
        >
          {{ text }}
        </button>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
  import {
    ref,
    computed,
    onMounted,
    watch,
  } from 'vue';

  // Vue 3.3 新推出 defineOptions 宏(使用了 Vue Marcos 的特性)终于不用再写 Vue 3.2 版本那种混编的默认名字导出了!
  defineOptions({ name: 'HelloWorld', inheritAttrs: false });

  // 使用 props 选项接收外部传递的值
  const props = defineProps({
    buttonText: Array,
  });

  // 定义自定义事件
  const emit = defineEmits(['closeVisible']);

  // 关闭面板时触发 closeVisible 事件
  const closeVisible = () => {
    emit('closeVisible');
  }

  // 响应式对象ref属性
  const count = ref(0);                // 类型 Ref<number>

  // 计算属性
  const double = computed(() => {      // 类型 ComputedRef<number>
    return count.value * 2
  });

  // 定义方法
  const increment = () => {
    count.value++
  };

  // 生命周期钩子
  onMounted(() => {
    console.log(`The initial count is ${count.value}.`)
  });

  // watch 监听器
  watch(
    () => count.value,
    (newValue, oldValue) => {
       console.log(`count changed from ${oldValue} to ${newValue}.`)
    }
  );
</script>

<style scoped lang="less">
  .container {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);

    border: 1px solid black;
    border-radius: 10px;
    padding: 40px 40px;
    width: 500px;
    height: 280px;

    .panel-header {
      display: flex;
      justify-content: flex-end;
      align-items: center;

      .close-icon {
        color: grey; /* 默认灰色 */
        cursor: pointer;
      }

      .close-icon:hover {
        color: blue; /* 鼠标悬停时蓝色 */
      }
    }

    .panel-content {
      text-align: center;

      img {
        width: 120px;
      }

      button {
        margin-right: 20px;
      }
    }
  }
</style>

Vue 3.x + TS 版本

已将更完整的示例项目代码发布到 Gitlab,请参考!

image.png