Vue3+TS封装组件库项目
1.介绍
2.项目文件结构
安装eslint插件
项目文件结构
components
Button
Button.vue - 组件
style.css - 样式
types.ts - 一些辅助的typescript类型
Button.test.tsx -测试文件
hooks
useMousePosition.ts
初始化项目
- vue官方基于vite的封装工具-create-vue https://github.com/vuejs/create-vue
- npm create vue@3
- Vite+Vue3+Typescript+ESlint
–save-dev 的作用
--save-dev
是 npm install 命令的一个选项,它用于将安装的软件包(dependencies)添加到 devDependencies
中。
具体来说,它的作用是将软件包添加到项目的开发环境依赖项中,而不是生产环境依赖项。
在 Node.js 项目中,通常会有两种类型的依赖项:
- 生产环境依赖项(dependencies):这些是项目在生产环境中运行时所需的依赖项,包括实际部署到生产服务器上的代码和运行时所需的工具。比如,一个 Web 服务器框架、数据库驱动程序等都是生产环境依赖项。
- 开发环境依赖项(devDependencies):这些是在开发过程中使用的依赖项,例如测试框架、构建工具、代码检查工具等。这些依赖项通常不会在生产环境中使用,只在开发、测试和构建项目时需要。
使用 --save-dev
选项安装依赖项时,npm 会将软件包添加到 package.json
文件中的 devDependencies
部分。这使得其他开发人员可以轻松地安装相同的开发依赖项,并确保项目的开发环境保持一致。
举个例子,假设你正在开发一个 JavaScript 项目,并且需要在开发过程中使用 Jest 测试框架。你可以使用以下命令安装 Jest,并将其添加到 devDependencies
中:
1 | npm install jest --save-dev |
这将在你的 package.json
文件中添加一个新的开发依赖项条目:
1 | { |
这样,其他开发人员在克隆项目后,只需运行 npm install
命令,就可以安装所有的开发依赖项,包括 Jest 测试框架。
3.Button组件
(1)需求分析
Button组件大部分关注样式,没有交互。
根据分析可以得到具体的属性列表:
type:不同的样式(Default,Primary.Danger,Info,Success,Warning)
plain:样式的不同展现模式 boolean
round:圆角 boolean
circle: 圆形按钮,适合图标boolean
size: 不同大小(small/normal/large)
disabled: 禁用boolean
图标:后面再添加
loading: 后面再添加
Button组件的本质就是class名称的组合
1
class="vk-button-primary vk-button-large is-plain is-round is-disabled"
(2)代码编写
1.导入接口问题– Vue macro
为了在声明 props
和 emits
选项时获得完整的类型推导支持,我们可以使用 defineProps
和 defineEmits
API,它们将自动地在 <script setup>
中可用:
1 | <script setup> |
defineProps
和defineEmits
都是只能在<script setup>
中使用的编译器宏。他们不需要导入,且会随着<script setup>
的处理过程一同被编译掉。defineProps
接收与props
选项相同的值,defineEmits
接收与emits
选项相同的值。defineProps
和defineEmits
在选项传入后,会提供恰当的类型推导。
问题:
在定义完按钮类型后在Button组件导入时会有爆红的问题(无法引入):
如果是Vue3.2版本及以下,我们需要安装一个插件Vue Macros来解决这个问题,但这个问题在Vue3.3中已经解决,不需要再安装插件
2.defineProps–为组件的 props 标注类型
可以将 props 的类型移入一个单独的接口中:
1 | <script setup lang="ts"> |
这同样适用于 Props
从另一个源文件中导入的情况:
1 | <script setup lang="ts"> |
当你使用 defineProps
定义组件的 props,并传入一个对象作为参数时,TypeScript 将会推断每个 prop 的类型。同样地,当你使用 defineEmits
定义组件的 emits,并传入一个对象作为参数时,TypeScript 将会推断每个 emit 的类型。
这种类型推导的好处在于,它能够帮助你在编码过程中捕获潜在的类型错误,提高代码的可靠性和可维护性。如果你的 props 或 emits 与组件的模板或其他代码不匹配,TypeScript 将会发出相应的类型错误,提醒你进行修正。
以下是一个简单的示例,演示了如何使用 defineProps
和 defineEmits
并从选项中推断类型:
1 | import { defineComponent, defineProps, defineEmits } from 'vue'; |
在这个示例中,我们首先定义了组件的 props 和 emits 的类型,然后在 defineComponent
中使用 defineProps
和 defineEmits
从选项中推断类型。在 setup
函数中,我们可以直接使用 props 和 emit,并且 TypeScript 能够正确地推断它们的类型。
3.withDefaults–为 props 声明默认值
例子1:
父组件:
1 | <template> |
子组件:
1 | <template> |
第一种方式:分离模式(推荐)
1 | export interface Props{ |
第二种方式:组合模式
1 | withDefaults( |
4.defineExpose
5.PropType
更严格的类型检查:
- 使用
String as PropType<string>
可以提供更严格的类型检查。这样可以确保传递给 prop 的值与预期的类型完全匹配,避免意外的类型错误 - 虽然在简单的情况下两者可能没有太大的区别,但在需要更严格的类型检查或者在 TypeScript 无法准确推断类型时,使用
PropType
函数会更加可靠
6.defineOptions
7.CSS方案_PostCSS
CSS预处理器
- Sass-Ruby,LibSass
- Less -Javascript,Less.js
- Stylus-Node.js
- Postcss https://postcss.org/
选中 Postcss 的原因:
- 轻量级
- 插件化-postcss-each postcss-for等
- Vite原生支持-https://vitejs,dev/guide/features,html#postcss
8.CSS Modules使用
文档地址:https://github.com/css-modules/css-modules
Vite是直接支持的:https://cn.vitejs.dev/guide/features.html#css-modules
- 可重用
- 作用域隔离
缺点:
- 看起来不像是CSS的正常写法
- 定义CSS全局样式比较麻烦
- 需要额外的loader来生成Typescript的支持
- 生成的类名并不友好
9.色彩系统
参考:https://ant.design/docs/spec/colors-cn
系统色版
- 基础色板
- 中性色板
产品色板
- 品牌色
- 功能色
(3)Button总结
需求分析:
- 确定props
- 确定组件展示方式
- 确定事件
初始化项目以及项目结构:
- create-vue
- 项目结构
组件编码
props的定义方式
对象形式
Typescript类型方式,有一些限制
原生属性
defineExpose定义实例导出
是否能支持原生的事件?比如Click–https://cn.vuejs.org/guide/components/attrs.html#nested-component-inheritance
样式解决方案
- 选择PostCSS作为预处理器
- 了解色彩系统
- 使用CSS变量添加颜色系统
- 添加Button样式
善用变量覆盖
使用PostCSS插件
- 扩展:使用了PostCSS动态生成主题颜色
4.Collapse组件
(1)需求分析
为什么选择这个组件
静态展示
简单的交互
多种解决方案
Provide/Inject
v-model实现
slots
Transition
(2)代码编写
1.确定属性
1 | interface ItemProps |
2.确定事件
1 | interface Emits{ |
3.思路分析
1 | //维护一个可变化响应式数组,代表打开的items(使用item的name) |
难点,怎样将对应的父组件Collapse属性传递给Item,这里是slot实现的,看起来不是很好传递。
答案:使用provide
和inject
传递信息
要为具名插槽传入内容,我们需要使用一个含v-slot指令的template元素,并将目标插槽的名字传给该指令:
1 | <BaseLavout> |
v-slot有对应的简写#,因此
1 | template v-slot:header |
可以简写为
1 | template #header |
其意思就是“将这部分模板片段传入子组件的header插槽中”。
4.BEM样式语法
1 | //Block component |
5.InjectionKey
在 Vue3 中使用 TS 时,父组件通过 provide 函数注入的数据类型往往是未知的,而子组件调用 inject 引入数据时也无法确定具体的数据类型是什么,这就产生了可维护性问题,比如某位同事写了下面这段代码时
1 | import { inject } from "vue" |
对于 colors
导数的数据类型我们并不知道是什么,它可以是对象 or 数组亦或是字符串,只能顺瓜摸藤找到它的 provide
,对于小项目找一下可能不花费什么时间,但对于大型项目来说很明显是不可取的,于是官方提供了 InjectionKey
函数来对传参进行类型约束,确保父子间传递的数据类型是可见、透明的。
InjectionKey
函数的使用也很简单,原理是将 provide
与 inject
的第一个参数即 key
通过声明 symbol
的方式关联起来即可
1 | // 1. 将 InjectionKey 定义的数据类型放到 keys/index.ts 下维护 |
有了 InjectionKey
函数后就不用再担心 provide&inject 之间的数据类型问题了,我们只需找到 InjectionKey 关联的 key即可
6.Transition
7.Transition JavaScript钩子
7.Record
1 | Record<K,T> |
构造一个对象类型,Keys
表示对象的属性键 、Type
表示对象的属性值,用于将一种类型属性映射到另一种类型
理解为:将 K 的每一个值都定义为 T 类型
1 |
|
即将K中的每个属性([P in K]),都转为T类型
结合项目的代码
使用 Record<string, (el: HTMLElement) => void>
主要是为了明确指定了对象的结构,即对象的键是字符串类型,而值是一个函数,这个函数接受一个参数 el
,类型为 HTMLElement
,并且不返回任何值。这样的明确定义可以提高代码的可读性和可维护性。
使用 Record
这样的类型定义有以下几个优点:
- 类型约束:
Record
提供了对对象结构的明确约束,确保了对象的键和值符合预期的类型。 - 静态类型检查: 使用
Record
可以让 TypeScript 在编译时进行静态类型检查,确保代码的类型安全性。 - 自文档化: 通过明确指定对象结构,使得代码更易于理解和维护,同时也可以作为文档来阐明代码的预期行为。
- 代码提示: 使用
Record
可以使得编辑器对对象的键和值进行更好的代码提示,帮助开发人员编写正确的代码。
(3)Collapse总结
当遇到类似列表结构组件,两种常用的实现组件的组织结构
方案一:传入数组
特点:
1.实现起来相对简单
2.展示复杂类型节点比较麻烦
3.语义化稍差
1 | <Collapse items="items"> |
方案二:语义化展示
特点:
1.语义化更好
2.复杂节点使用slot展示简单
1 | <Collapse> //父组件 |
语义化展示,父子属性传递的常规方式:
数据状态以及处理放在父组件
使用Provide/Inject 传递给子组件
v-model的实现
- 是属性modelValue和事件update:modelValue的语法糖
- 特别注意属性赋值给内部响应式对象的更新问题(使用watch)
使用内置的Transition实现动画效果
- 并不提供任何动画
提供一系列的classes标示整个动画过程
- 提供javascript钩子函数支持高级的自定功能
5.Icon组件
(1)需求分析
(2)代码编写
1.Inline SVG vs Font Icon
- svg完全可以控制,font icon只能控制字符相关的属性
- font icon需要下载的字体文件较大,除非自己切割打包。
- font icon会遇到一些比较奇怪的问题
- 矢量图标有很大的优势:可调整大小而不失品质,在视网膜屏幕上也可以清晰显示,文件尺寸也非常小。
- icon font:
浏览器认为这是文本,所以会对其使用抗锯齿。这可能导致图标不如你想象的那么锐利。 - inline SVG:
真正的矢量
- icon font:
2.现成的图标库
Bootstrap Icons
Fontawesome
Ionicon
3.安装svg
1 | //安装svg core |
4.导入组件库
1 | //导入组件svg core |
5.禁用透传
6.导入lodash-es的omit用作过滤器
做到精益求精,要过滤传值时自己添加的属性
安装lodash-es要安装es稳定版本和声明文件
1 | npm install lodash-es --save |
omit
_.omit(object, keys)
函数返回一个没有列入key属性的对象。其中,参数object为JSON格式的对象,keys表示多个需要排除掉的key属性。
1 | const _ = require('lodash/object'); |
(3)Icon总结
一些图标库解决方案
- Bootstrap Icon
- Fontawesome
- Ionicon
二次开发组件
第一步:支持组件的原始属性
- inheritAttrs:false 不继承属性
- 使用$props访问所有属性
- 要注意不继承以后一些默认属性失效的问题$attrs
第二步扩充组件属性
我们添加的type/color属性
过滤传递的属性 lodash omit
6.Alert组件
(1)需求分析
展示一个对应的条目,有几种不同的type,和我们应用的类型是一一对应
有两种主题,一种深色,一种浅色
点击右侧按钮可以关闭对应的条目
可以定义按钮是否显示
可以自定义里面的文本为复杂的结构
7.组件测试
为什么要有测试
出现的问题
- 新建一堆不同属性的组件,肉眼观测不靠谱
- 更新代码,要再次进行手动测试
测试在国内被严重忽视
没有时间
需求一直修改
不知道怎么测试
测试的优点
- 自动化完成流程,保证代码的运行结果
- 更早发现Bug
- 重构和升级更加容易和可靠
组件化的框架(Vue,React)非常适合写单元测试
- 组件化,独立单元,互不影响
- 属性和界面的映射,固定输入,固定输出
- 单向数据流
- 状态管理工具的store可以单独写测试
测试框架Vitest简介
需要选取两种测试工具
- 通用的测试框架
- 针对对应库(React或者Vue)的测试库
通用测试框架
- Mocha
- Jest
- Vitest (采用)
Vitest的优点
- 基于Vite而生,和Vite完美配合,共享一个生态系统
- 兼容Jest语法
- HMR for tests
- ESM,TS,JSX 原生支持
测试框架的几大功能
断言
- 内置Chai以及兼容Jest expect的 API
Mock
代码覆盖率
Snapshot –兼容Jest
安装Vitest
1 | npm install -D vitest |
测试方法
控制台输入
1 | npx vitest 文件名 |
文件名不需要完全一样,会模糊查询并作用全部文件
Vitest的vi用法
vi
是 vitest
测试框架提供的一个辅助工具,用于创建测试桩(mocks)和模拟对象(spies)。它可以让你在测试中模拟函数的行为,以便更容易地进行单元测试。
下面是 vi
的一些常见用法:
vi.fn()
: 创建一个模拟函数,可以用于跟踪函数的调用情况,判断是否被调用以及被调用的参数等。
1 | const myFunction = vi.fn(); |
vi.spyOn(obj, methodName)
: 对对象的指定方法进行监视,可以跟踪该方法的调用情况。
1 | const obj = { |
vi.mock('moduleName')
: 对指定的模块进行模拟,可以控制模块的行为,例如模拟模块的函数返回值等。
1 | vi.mock('axios'); |
Vitest的describe用法
describe
是 vitest
测试框架提供的一个函数,用于描述一个测试套件,通常用来组织一系列相关的测试用例。在描述中,可以包含多个测试用例,并且可以进行嵌套描述以更好地组织测试。
下面是 describe
函数的基本用法:
1 | describe(description, callback); |
description
: 描述测试套件的字符串,通常是一个简短的描述性文字,用来说明该测试套件包含的是哪些测试用例。callback
: 一个回调函数,包含了测试用例的定义和实现。在该回调函数内部,可以使用test
函数定义多个测试用例。
例如:
1 | describe('Math operations', () => { |
在上面的例子中,我们描述了一个名为 “Math operations” 的测试套件,其中包含了两个测试用例: “Addition” 和 “Subtraction”。每个测试用例都通过 test
函数来定义,并包含了相应的测试逻辑。
Vitest的expect用法
expect
是 vitest
测试框架提供的一个函数,用于对测试结果进行断言,判断实际结果是否符合预期。它通常与各种匹配器(matchers)一起使用,用于对值、对象、函数等进行断言。
下面是 expect
函数的基本用法:
1 | expect(value); |
value
: 要进行断言的值或表达式。
expect
函数返回一个包含一系列断言方法的对象,可以通过链式调用来对值进行不同类型的断言。常见的断言方法包括:
.toBe(expected)
: 断言值等于expected
。.toEqual(expected)
: 断言值深度等于expected
,即值的内容与结构都相同。.toBeDefined()
: 断言值已定义且不为undefined
。.toBeUndefined()
: 断言值为undefined
。.toBeNull()
: 断言值为null
。.toBeTruthy()
: 断言值为真值,即可以转换为true
的值。.toBeFalsy()
: 断言值为假值,即可以转换为false
的值。.toHaveBeenCalled()
: 断言函数已被调用过。.toHaveBeenCalledWith(arg1, arg2, ...args)
: 断言函数被以指定参数调用过。
例如:
1 | const result = add(1, 2); |
在这个例子中,expect
函数用于断言函数 add
的返回值是否等于 3
。
单元测试实用工具库 Vue-test-utils
介绍 | Vue Test Utils (vuejs.org)
安装
1 | ❯ npm install --save-dev @vue/test-utils |
Vnode以及Render Function
Virtual DOM: 一种虚拟的,保存在内存中的数据结构,用来代表UI的表现,和真实DOM节点保持同步。Virtual DOM 是由一系列的Vnode组成的。
1 | //模拟一个简单的 Vnode |
虚拟DOM的优点
- 可以使用一种更方便的方式,供开发者操控UI的状态和结构,不必和真实的DOM节点打交道
- 更新效率更高,计算需要的最小化操作,并完成更新
创建Vnode
h和createVnode都可以创建vnode,
h是hyperscript的缩写,意思就是”JavaScript that produces HTML(hypertext markup language)”,很多virtualDOM的实现都使用这个函数名称。
还有一个函数称之为createVnode,更形象,两个函数的用法几乎是一样的。
1 | import { h,createVnode} from 'vue' |
测试事件
触发事件
1 | await firstHeader.trigger('click') |
观测事件是否触发
1 | //方案一jsx渲染组件: |
1 | //方案二普通方式mount组件: |
Render Pipeline
- Compile,Vue组件的Template会被编译成
render function
,一个可以返回Virtual DOM树的函数。 - Mount,执行render function,遍历虚拟DOM树,生成真正的DOM节点。
- Patch,当组件中任何响应式对象(依赖)发生变化的时候,执行更新操作。生成新的虚拟节点树,Vue内部会遍历新的虚拟节点树,和旧的树做对比,然后执行必要的更新。
组件测试总结
Vue测试框架-vue-test-utils
- 挂载-mount
- 修改vitest的配置,支持dom
- environment:’jsdom’
- query
- find()/get()
- findAll()
- findComponent()
- 内容/属性
- text()/html()
- classes()
- attributes()
- stub模拟子组件
VNode以及Render
- VNode的概念
- 使用render function/JSX写vue组件
1 | //render function |
1 | // 使用 JSX 语法 |
这两种方式都可以用来编写 Vue 3 组件的 render 函数,选择其中一种取决于个人偏好和项目要求。
vitest钩子函数及其他
Vitest 是一个基于 Vite 构建的测试运行器,它提供了一些钩子函数和其他功能,帮助开发人员编写和运行测试代码。
以下是 Vitest 的一些常用钩子函数及其他功能:
beforeEach(): 在每个测试用例运行之前执行的钩子函数。可以在该钩子函数中进行一些公共的初始化工作,如创建测试环境、准备测试数据等。
afterEach(): 在每个测试用例运行之后执行的钩子函数。可以在该钩子函数中进行一些公共的清理工作,如释放资源、清除状态等。
beforeAll(): 在所有测试用例运行之前执行的钩子函数。可以在该钩子函数中进行一些全局的初始化工作,如启动服务器、连接数据库等。
afterAll(): 在所有测试用例运行之后执行的钩子函数。可以在该钩子函数中进行一些全局的清理工作,如关闭服务器、断开数据库连接等。
describe(): 用于定义测试套件的函数,可以将一组相关的测试用例进行分组,并且可以嵌套使用。通常与 beforeEach()、afterEach() 等钩子函数配合使用,以实现更复杂的测试逻辑。
test(): 用于定义单个测试用例的函数,包含待测试的代码和期望的结果。通常在 describe() 函数内部调用,以组织和管理测试用例。
expect(): 用于断言测试结果是否符合预期,可以与各种断言函数一起使用,如 toBe()、toEqual()、toBeTruthy() 等。通常在测试用例内部调用,以验证代码的行为和状态是否正确。
异步测试支持: Vitest 支持异步测试,可以处理异步操作和异步更新。开发人员可以使用 async/await 关键字等待异步操作完成,并且检查测试结果是否符合预期。
代码覆盖率支持: Vitest 提供了代码覆盖率的支持,可以生成测试覆盖率报告,帮助开发人员评估测试的质量和完整性,以及找出需要增加测试覆盖的地方。
综上所述,Vitest 提供了丰富的钩子函数和其他功能,使得编写和运行测试代码变得简单、灵活和高效。开发人员可以根据项目需求选择合适的钩子函数和功能,从而实现全面的测试覆盖和高质量的测试代码。
8.Tooltip组件
(1)需求分析
- 通用组件
- Tooltip
- Dropdown
- Select等等
功能分析
根本功能,两块区域
- 触发区
- 展示区
触发方式
- hover
- 点击
- 手动
重点就是触发区发生特定事件的时候,展示区的展示与隐藏
展示方案
1 |
|
属性
1 | interface TooltipProps{ |
事件
1 | interface TooltipEmits |
实例
1 | interface TooltipInstance{ |
动态事件的解决方案
1 | //方案一,使用DOM标准来绑定事件--不推荐 |
(2)代码编写
1.Tooltip开发计划
最基本的实现
支持click/hover两种触发方式
支持clickoutside的时候隐藏
支持手动触发
支持popper参数
动画
支持延迟显示
样式
2.使用第三方库 popper.js
3.defineProps() 和 defineEmits()
4.slot
5.watchEffect()
(3)Tooltip总结
Tooltip分析
基础组件
- 触发层
- 展示层
开发复杂组件的时候
- 需求分析
- 设立开发计划
Tooltip开发
使用popper.js来完成位置的展示
动态事件的添加–使用v-on
实现外侧点击的功能
手动触发的功能
组件实例实现对应的方法
支持Popper参数
使用第三方库的时候必不可少的功能
添加延时
使用debouce来整合短时间内多次触发的回调
debouce可以使用cancel方法来取消
添加样式
三角箭头的实现
正方形旋转45度,加特定的位移,再加边框
添加测试
- 注意定时器的影响
- vi.useFakeTimers()
- vi.runAllTimers()
- 注意点击到外侧区域的测试
9.Dropdown组件
(1)需求分析
- 根据Tooltip二次开发的组件
- 显示/隐藏一个具体的,有多个选项的菜单
- 菜单中有各种选项,用户可以自定义
- 使用语义化结构
- 使用javascript数据结构
展示方案
1 |
|
属性
1 | interface DropdownProps extends TooltipProps{ |
事件
1 | interface DropdownEmits |
实例
1 | interface DropdownInstance { |
(2)代码编写
1.Render Function
这个简单组件的主要作用是接收一个虚拟节点(vNode)作为属性,并将其渲染到页面上
1 | import { defineComponent } from 'vue' |
2.Fragment
Fragment是一种特殊的组件,用于在不添加额外节点的情况下包装多个子元素。它可以让你将一组子元素进行分组,而不会在最终渲染结果中添加额外的DOM节点。
(3)Dropdown总结
Naive UI就是用JSX编写的
- 使用通用组件 Tooltip 进行二次开发
- 在vue单文件组件template中渲染Vnode的方法
- 使用 jsx 编写组件
vue 文件 和 jsx 的对应关系
文档:https://cn.vueis.org/guide/extras/render-function
vue 使用一些特殊的自定义指令,并且会做模板编译的优化
- v-if
- v-for
- v-on
jsx 是 h 函数的语法糖,使用花括号并且可以写 javascript 表达式
属性
- Vue: setup语法支持 defineProps 泛型
- JSX:不支持泛型,要使用 props 属性
事件
- Vue: setup 语法支持 defineEmits 泛型
- JSX: 不支持泛型,要使用 emits 属性
渲染slots
- Vue使用
- 默认
<slot/>
- 具名
<slot name="footer"/>
- 默认
- JSX使用
- 默认:
{slots.default()}
- 具名:
{slots.footer()}
- 默认:
- Vue使用
传递slots
Vue
<MyComponent>Hello</MyComponent>
1
2
3<MyComponent>
<template #footer>Footer</template>
</MyComponent>JSX
<MyComponent>{()=>'hello'}</MyComponent>
1
2
3
4
5
6<MyComponent>
{{
default:()=> 'default slot',
footer:()=> <div>footer</div>,
}}
</MyComponent>
expose
- Vue使用
defineExpose
导出实例 - JSX使用
expose
函数导出实例
- Vue使用
10.Message组件
(1)需求分析
功能分析
- 在特定的行为的时候,弹出一个对应的提示(支持普通文本以及VNode)
- 提示在一定时间后可以消失
- 可以手动关闭
- 可以弹出多个提示
- 有多种类型( default,primary,danger…)
难点
1.使用函数式的方式来创建组件
1 | createMessage('hello world',props) |
2.可以弹出多个提示,并且旧提示可以根据新提示向下移动位置
1 | //创建多个实例,应该没有问题,但怎么调整位置是个难题,看起来我们需要动态调整组件的属性 |
属性分析
1 | interface MessageProps{ |
事件以及实例
由于是由函数的方式创建组件,这里暂且无法明确对应的事件以及实例
1 | const instance = createMessage('hello world',props) |
(2)代码编写
1.将组件Render到DOM节点上
使用 createApp 的弊端
这个方法太重了,它其实返回的是一个应用的实例,而我们需要轻量级的解决方案
隆重推出render函数
1 | //一个vue内部神奇的函数,文档中都没有特别的记录 |
在本案例的使用:
1 | //`h()` 函数用于创建 vnodes |
2.v-show 隐藏节点后清除节点(该节点已经挂载实例)
清除节点
1 | //渲染null 到 dom 节点上 |
3.通过使用一个数组来实现获取同一个组件不同实例的内容
这样可以为下一步动态定位多个实例做准备
1 | types.ts |
4.旧提示可以根据新提示向下移动位置
通过获取实例ref=”messageRef” 计算高度量的偏移再返回样式给实例:style=”cssStyle”并且暴露出去 bottomOffset ,更新时要等dom挂载后更新所以要用到await 和 nexttick
1 | <div |
1 |
|
5.JSX怎么在组件外部获取暴露出去的bottomOffset
通过Vnode的component属性上的expose属性就能拿到对应的属性
6.通过VNode拿到组件实例
1 | const component = vNode.component |
7.shallowReavtive
文档地址:https://cn.vuejs..org/api/reactivity-advanced.html#shallowreactive
- 和
reactive()
不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露。 - 假如是数组的话,创建一个浅层响应式的空数组,这意味着数组的元素不会被递归地转换成响应式对象。当我们对数组进行一些增删改操作时,Vue会自动检测到这些变化,并更新对应的视图。
- 性能优化
1 | const instances: MessageContext[] = shallowReactive([]) |
8.手动调用删除
1 | // 手动调用删除,其实就是手动的调整组件中 visible 的值 |
9.添加zIndex
使用hooks实现
1 | //useZindex.ts: |
1 | //method.ts: |
10.添加键盘关闭(按esc可以关闭message)
使用hooks实现
1 | //useEventListener.ts: |
11.hover到Message上面的时候不会自动关闭
基础的组件绑定事件操作实现
1 | //Message.vue: |
1 | //Message.vue: |
12.添加动画以及样式
1 | <Transition |
(3)Message总结
使用函数式的方式创建组件
1.使用render函数挂载在特定节点
1 | const container = document.createElement('div') |
2.销毁组件实例
1 | render(null,container) |
3.组件动态构造并且传入属性
1 | const newProps = { |
4.使用一个数组保存组件实例等信息,并且添加对应的函数对数组进行处理
5.计算偏移量
6.在函数中获取这个偏移量
- vnode.component-Componentinternallnstance组件内部实例
- 在组件内可以使用getCurrentlnstance()获取
- 在函数中使用vnode.component.exposed!.bottomOffset.value获得
7.将instances 数组改为响应式:使用shallowReactive([])
8.添加两个通用钩子函数
- useZIndex()
- useEventListener()
9.添加有趣的动画
使用transformY以及fade作出一个fade-up的效果
10.测试-函数式创建组件的测试
使用rAF()等待动画执行完毕
11.更好的展示工具VitePress-使用工具生成文档
1.创建放在./docs文件夹下
2.在.gitignore添加.vitepress/dist静态文件和./vitepress/cache缓存文件
3.文件夹结构
1 | docs |
3.npm run doc:dev
4.Markdown Extension Examples
Syntax Highlighting
Custom Containers
5.路由系统
index.md –> /index.html(accessible as /)
6.两个关于路径的概念
Project Root:项目路径,会在这个路径下寻找.vitepress文件夹,默认是当前的工作路径
Source Directory:Markdown文件存放的文件夹,默认和Project Root相同,在这个文件夹下,会处理所有的Md文件,并且生成对应的路由,可以使用VitePress中配置文件中的srcDir进行修改
7.在md文件中写vue组件写法,完全不冲突,支持
8.preview-demo实现组件和代码显示
9.自定义主题颜色
在custom里对vitepress的样式进行重载
1 | :root { |
10.为生产环境打包并且部署
文档:https://vitepress.dev/guide/deploy
1 | # 打包 |
12.Input组件
(1) 需求分析
- 支持Input/Textarea
- 支持不同大小
- 支持 v-model
- 支持一键清空(有值的时候显示一个按钮,点击清空)
- 支持切换是否密码显示(有值的时候显示一个按钮,点击切换密码可见/不可见)
- 支持自定义前缀/后缀slot((prefix/suffix),一般用于图标
- 支持复合型输入框自定义前置或者后置(prepend/append),一般用于说明和按钮
- 一些原生属性的支持
属性
1 | interface InputProps { |
事件
1 | interface InputEmits { |
Slots
1 | prepend,append,prefix,suffix |
Expose
1 | export interface InputInstance { |
(2) 代码编写
1.编写测试用例来验证静态结构是否正确
1 | describe('Input', () => { |
2.TDD的开发方式
TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法,它强调在编写代码之前先编写测
试用例,然后通过不断地编写和运行测试用例来驱动代码的开发。
- 1.编写测试用例
- 2.运行测试用例
- 3.编写代码:根据测试用例的要求,编写代码,实现功能。
- 4.运行测试用例
- 5.重构代码(可选)
- 6.重复上述步骤
3.支持 v-model 的测试用例和实现
1 | it('支持 v-model', async () => { |
- props 和 emits 的定义:在
<script setup>
部分使用defineProps
和defineEmits
定义了组件的 props 和 emits,分别是InputProps
和InputEmits
。其中InputProps
定义了输入框组件可能接收的属性,而InputEmits
定义了组件可能触发的事件。 - 内部数据绑定:使用
ref
创建了一个名为innerValue
的响应式引用,用于存储输入框的值。这个值会与props.modelValue
进行双向绑定,从而实现了v-model
的功能。 - 输入框的事件处理:在输入框的
@input
事件中,调用了handleInput
方法,将输入框的值更新到innerValue
中,并触发了update:modelValue
和input
事件。在handleInput
方法中,使用了emits
函数来触发这两个事件。 - 对外暴露输入框的引用:使用
defineExpose
将输入框的引用暴露给外部组件,这样外部组件就可以直接访问到输入框的实例,例如调用输入框的方法或属性。
4.支持按钮清空当前文本的测试用例和实现
测试用例:
1 | it('支持点击清空字符串', async () => { |
实现:
1 | const showClear = computed(() => |
清空按钮的显示和隐藏通过 showClear
计算属性进行控制,只有在输入框有值且获得焦点时才显示。点击清空按钮时,会调用 clear
方法,将 innerValue
的值设置为空字符串,并通过 emits
函数触发 update:modelValue
、clear
、input
和 change
事件,通知外部组件输入框的值发生了变化。
5.支持密码切换的测试用例和实现
测试用例:
1 | it('支持切换密码显示', async () => { |
实现:
1 | <Icon |
1 | const passwordVisible = ref(false) |
- 密码切换功能的展示:在模板中,通过
v-if
指令判断是否显示密码切换按钮。当满足条件时,分别显示“显示密码”和“隐藏密码”的图标按钮。 - 点击图标触发切换密码可见性:使用
@click
事件监听器,当点击密码切换按钮时,调用togglePasswordVisible
方法切换密码可见性。在该方法中,通过改变passwordVisible
的值来控制密码是否可见。 togglePasswordVisible
方法:该方法用于切换密码的可见性。如果密码是可见的,则将passwordVisible
的值设置为false
,否则设置为true
,以实现密码的显示和隐藏。- 使用
computed
计算属性控制密码切换按钮的显示:在模板中使用v-if
条件渲染判断密码切换按钮是否显示。其中,showPasswordArea
计算属性控制密码切换按钮的显示逻辑,只有在组件启用密码显示功能、未禁用且输入框有值时才显示密码切换按钮。
6.支持事件
渲染输入框或文本域以及相关的附加元素(如前缀、后缀、清除按钮等)。
监听用户输入事件,更新输入框的值,并通过
v-model
传递给父组件。监听输入框的
focus
和blur
事件,通过emits
函数触发相应的自定义事件,并执行验证逻辑。提供清除输入框内容的功能,并通过
emits
函数触发相应的自定义事件。
7.组件添加原生事件
分析出来需要添加的原生属性
- disabled(已经添加)
- placeholder-当没有值设定时,出现在表单控件上的文字
- readonly-布尔值。如果存在,其中的值将不可编辑
- autocomplete·表单自动填充特性提示。(不是一个布尔属性!)
- autofocus:一个布尔属性,如果存在,表示当页面加载完毕(或包含该元素的
<dia1og>
显示完毕)
(3) Input总结
Input组件开发是一个关键的时间点。
接触了TDD(Test Driven Development)的开发方式。
温故而知新的知识点:
- v-model 测试方式
- Vue 组件事件的测试方式
- 设置表单组件:要特别注意对于它包含的内部组件原生属性的一些支持
TDD开发方式的一些小问题:
在大部分的情况下,它能很好的进行运作,但是由于 jsdom 使用的是模拟的DOM环境,会和浏览器的真实环境有一些或多或少的差别,在一些小功能上可能会出现问题。
对于TDD的使用,要按照自己的喜欢和完成的特性和功能进行选择,没有一个确定的答案。
13.Switch组件
(1) 需求分析
Switch,并不是一个标准Form组件,而是被手机端的一种交互发扬光大的一种结构。
不同寻常
Switch的不同寻常的要点
功能类似checkbox,所以内部有可能是一个checkbox在工作,狸猫换太子。
样式非常独特,是我们面对面对样式最大的一个挑战。
属性
1 | interface SwtichProps { |
事件
1 | interface SwitchEvents { |
(2) 代码编写
1.组件样式设计
2.样式编写
1 | .vk-switch { |
3.支持v-model
1 | const switchValue = () => { |
4.支持自定义value类型
通过以下步骤实现:
- props 的定义:在
props
中定义了activeValue
和inactiveValue
,它们分别表示开关的激活状态和非激活状态下的值。这使得开关可以接受任意类型的值作为其状态。 - computed 的使用:通过计算属性
checked
来确定当前开关是否处于激活状态。在这个计算属性中,使用了innerValue
和activeValue
来判断当前值是否等于激活状态的值。 - switchValue 函数的实现:在
switchValue
函数中,根据当前开关的状态,通过判断checked
的值来确定下一个状态应该是什么值。这个逻辑不再仅限于布尔类型的切换,而是根据activeValue
和inactiveValue
的定义来切换值。 - 事件的触发:在
switchValue
函数中,除了触发update:modelValue
和change
事件外,还将下一个值作为参数传递给这些事件。这样做可以保证外部的v-model
在更新时能够得到正确的值。
5.支持文字描述
1 | <!-- 如果activeText或inactiveText存在,则显示对应的开关文字 --> |
6.支持无障碍
1 | <input |
- 提供键盘操作支持:在
<input>
元素上监听键盘事件,例如keydown.enter
,以便用户可以通过键盘切换开关状态。 - 确保焦点可见性:在用户与开关交互时,确保开关的焦点是可见的,并且具有适当的焦点样式。这可以通过 CSS 或者 JavaScript 来实现。
(3) Switch 总结
Switch组件,分析出了和它很相似的应该是checkbox,所以它是个内部包裹着checkbox,用DOM模拟对应的外貌的组件。
新的知识点:
- 学习写复制CSS样式的方式
- 表单组件设计要特别注意和原生表单元素的配合,实现比较完美的可访问性。
14.Select 组件
(1) 需求分析
类似原生的Select,不过有着更强大的功能。
最基本功能:
- 点击展开下拉选项菜单
- 点击菜单中的某一项,下拉菜单关闭
- Select获取选中状态,并且填充对应的选项。
组件本质:进阶版本的Dropdown,Input组件和Tooltip组件的组合
高级功能:
- 可清空选项:当Hovr的时候,在组件右侧显示一个可清空的按钮,点击以后清空选中的值。
- 自定义模版:可以自定义,下拉菜单的选项的格式。
- 可筛选选项: Input允许输入,输入后可以根据输入字符自动过滤下拉菜单的选项。
- 支持远程搜索(难点):类似自动联想,可以根据输入的字符发送请求,渲染返回的内容作为选项列表
- 扩展支持:比如键盘移动
结构分析
1 | <Select |
属性
1 | interface SelectProps { |
事件
1 | interface SelectEmits{ |
(2) 代码编写
1.基本结构
1 | <template> |
2.选中选项功能
1 | const findOption = (value: string) => { |
3.初步样式编写
1 | const popperOptions: any = { |
1 | .vk-select { |
4.添加箭头图标
1 | <template #suffix> |
1 | .vk-input { |
5.支持清空
可清空选项:当Hover的时候,在组件右侧显示一个可清空的按钮,点击以后清空选中的值
两种思路:
- 完全复用input组件的clear功能
- 不复用,重新写
这里选择第二个思路
1 | //模板 |
6.支持自定义模板
可以自定义,下拉菜单的选项的格式。
■ 使用函数 (e:SelectOption) => VNode
1 | export type RenderLabelFunc = (option: SelectOption) => VNode; |
1 | export interface SelectProps { |
使用之前定义过的renderVnode组件配合renderLabel 实现
1 | <RenderVnode :vNode="renderLabel ? renderLabel(item) : item.label"/> |
7.支持筛选
Input允许输入,输入后可以根据输入字符自动过滤下拉菜单的选项。
属性:添加两个属性
- 1.开启filter功能
- 2.自定义filter的处理方式 – 如果有自定义就用自定义,没有就用默认的数组上的filter
思路:
- 1.在本地存储一个可变的响应式对象
- 2.在input的时候重新计算,渲染新的值
优化
1.再次选择需要清空Input
2.再次选择改善placeholder的显示,显示当前选中的值
属性添加
1 | export type CustomFilterFunc = (value: string) => SelectOption[]; |
1 | export interface SelectProps { |
基本实现
1 | const filteredOptions = ref(props.options) |
1 | //生成新的选项 |
1 | //包装函数 generateFilterOptions |
筛选优化
清空Input优化:
1 | const controlDropdown = (show: boolean) => { |
placeholder优化:
1 |
|
8.支持远程搜索
类似自动联想,可以根据输入的字符发送请求,渲染返回的内容作为选项列表,
需求:
- 每输入一个值,就发送特定请求,返回对应的选项
- 显示状态(正在读取/没有数据等提示)
属性:
- 开启remote功能
- 自定义remote处理方式(value:string)=>Promise<SelectOption[]>
思路:
- 在Input输入的过程中,根据用户传入的remote处理方式,发起请求并且渲染结果
属性添加
1 | export type CustomFilterRemoteFunc = (value: string) => Promise<SelectOption[]>; |
1 | export interface SelectProps { |
当设置一个数组的默认初始值的时候,要使用下面的方式(知识点)
1 | const props = withDefaults(defineProps<SelectProps>(), { |
1 | // 如果props中没有filterable属性,直接返回 |
9.远程请求添加防抖部分
1 | <Input |
debouce实现:
1 | const debouceOnFilter = debounce(() => { |
10.支持键盘操作
需求:
- 在input focus的状态下,按下Enter打开下拉菜单/再次按下关闭菜单
- 按ESC关闭菜单
- 按上下键移动菜单选项,高亮显示当前移动到的选项
- 按下Enter,选中特定的选项
思路:
- 在document上绑定onKeyDown事件
- 在Input的onKeyDown事件上绑定特殊的键盘操作事件完成任务
- https://developer.mozilla.org/zh-CN/docs/Web/API/Element/keydown_event
- 使用哪个属性监控按下了哪个键
- https://developer..mozilla.org/zh-CN/docs/Web/APl/KeyboardEvent/keycode 原来经常使用的keyCode已经要被弃用
绑定事件:
1 | <Input |
对应的事件处理:
1 | const handleKeydown = (e: KeyboardEvent) => { |
完善部分:
1 | export interface SelectStates { |
(3) Select 总结
- 进阶版本的Dropdown,Input组件Tooltip组件的组合。
- 善于使用已经有的基础组件来进行排列组合,二次开发需要的组件。
- 对于复杂需求,可以使用手动控制下拉菜单的显示与隐藏。
- 当遇到列表渲染的时候,应该条件反射一样的想到两种方式。
语义化,也就是子组件,结构更清楚,渲染复杂的结构比较方便。
1 | <Select> |
使用数组,更加方便快捷,实现起来也比较容易,操作数组也更方便。
1 | <Select items={items)></Select> |
- 遇到自定义模版这样的需求,想到使用自定义函数来渲染。
1 | export type RenderLabelFunc =(option:Selectoption)=>VNode; |
使用RenderVNode这样的中间组件来渲染负责VNode或者直接使用jsx也完全可以
- 遇到用户筛选这种需求,不管是同步还是异步,也要想到自定义函数
1 | export type CustomFilterFunc = (value:string)=>Selectoption[] |
对于在短时间内会被触发多次的回调,一定要注意是否需要函数截流
使用KeyDown来监控键盘是否被按下,使用e.key而不是e.keyCode来监控哪个按键被按下
当你将props的值,作为初始值传入给一个响应式对象的时候,一定要watch原始值的修改,然后更新本地的响应式对象。
15.Form 组件
(1) 需求分析
自定义UI
整体可自定义
用户可以自定义渲染多种类型的表单元素- Input,Switch,Select等
用户可以自定义提交区域的内容(按钮样式,排列等等)
验证时机
表单元素默认Blur的时候验证,可以自定义
整个表单在点击提交提交按钮的时候全部验证
验证规则
- 每个Input可以配置多条规则(不能为空,需要是字符串,最多是多少等等)
- 可以自定义规则(比如重复密码框输入的要和密码输入的一样)
组件结构设计:
1 | const formobj = { |
这种设计的带来的问题:
不语义化,使用起来,看起来都很别扭,个性化很困难。
没办法自定义布局。
应该有的结构
第一步,静态外观:
1 | <Form> |
第二步:属性:
1 | const model = reactive({ |
第三步结合:
1 | <Form :model ="model" :rules="rules"> |
第四步,想想怎样完成验证:
1 | export interface FormInstance{ |
开发步骤:从静到动
- 根据结构,实现基础布局。
- 添加初始化数据,以及数据更新的功能。
- 添加验证功能。
- 后续的一些需求。
(2) 代码编写
0.流程图
1.基础结构
1 | <template> |
使用了作用域插槽实现数据传递:
2.添加数据和规则
1 | export interface FormItemProps { |
3.获取数据和规则
1 | const innerValue = computed(() => { |
4.学习使用 async-val(第三方库)
1 | import Schema from 'async-validator'; |
5.FormItem完成验证
验证的基本要素
- value数据
- rules规则
- 在合适的时机,触发对应的验证
设计验证的流程
验证的类型
- 表单整体验证
- 单个表单元素验证
结论:不难发现整体验证就是循环调用单个验证汇总获得的结果
单个验证的实现方式
要在Formltem中实现
基本要素,从父组件Form获取值(value)和规则(rules)
1 | const validate = async (trigger?: string) => { |
6.自动触发验证
验证的场景
- 单个ltem的验证
- 整个Form的验证
流程
规则(rules)+值(value)),在特殊的时机(比如onBlur),调用特殊的逻辑去验证最终的结果。
难点,后面注意
就是怎样在特殊的事件下,完成验证,从设计上来看,Formltem中的表单相关的组件,并没有显式绑定任
何的事件。
使用第三方库async-validator实现特殊验证
使用了promise的api
7.添加Trigger条件
1 | //子组件传入trigger |
8.父子组件通信
1 | //provide和inject实现 |
9.完成表单整个验证功能
1 | const fields: FormItemContext[] = [] |
10.添加重置状态功能
1 |
|
1 | //formitem.vue |
11.添加样式
1 | .vk-form { |
12.优化整合
解决promise错误
1 | validate: (trigger?: string) => Promise<any>; |
1 | const runValidation = (trigger?: string) => { |
(3) Form总结
父子组件数据传递的常用方式
最常用:属性/事件
provide/inject (透传,适用于父组件到子组件)
FormContext,Form => Formltem
FormltemContext,Formltem => Input
Scoped slot (给slot传递数据)
- Formltem => #template
在父组件创建数据结构,在子组件填充数据(适用于子组件到父组件)
Form => FormltemContext[]
Formltem => onMounted的时候插入数据 - addField
Formltem => onUnmounted的时候删除数据 - removeField
在Form当中调用Formltem的具体属性和方法
小思考:可以使用pub/sub模型,这样只需要提供一个 emitter 对象就可以了。
1 | const emitter mitt() |
一些小的Tip
- 注意defineExpose,一个好的组件应该尽可能的给用户提供实例中有用的信息供他们进行自定义。
- 对于有可能出现的异步操作,尽可能提供Promise
16.组件库打包与发布
任务
了解Javascript模块化发展的历史
- 全局变量
- common.js AMD
- ES6 modules
Bunder的功能和作用
- Vite(Rollup+ESBuild)在dev和build的过程为什么这么不同
- Webpack
学习使用Vite(Rollup)实现代码打包
- 代码入口文件
- 配置文件
- 生成多种文件类型
- 生成样式文件
- typescript 定义文件 d.ts
发布到NPM
- 本地测试
- 注册以及发布到NPM
- NPM钩子
模块化
模块化是什么
1 | from package import function |
1 | package main |
模块化的优点
- 可维护性
- 可复用性
ES6之前没有模块的年代
backbone.js
1 | //使用backbone.js的方法 |
全局变量+命名空间(namespace)
1 | // IIFE 自执行函数,创建一个封闭的作用域,赋值给一个全局变量 |
缺点
- 依赖全局变量,污染全局作用域,不安全
- 依赖约定命名空间来避免冲突,可靠性不高
- 需要手动管理依赖并控制执行顺序,容易出错
- 需要在最终上线前手动合并所有用到的模块
AMD - (Asynchronous module definition)
- 采用异步方式加载模块
- 仅仅需要在全局环境定义require与define,不需要其他的全局变量
- 通过文件路径或模块自己声明的模块名定位模块
- 提供了打包工具自动分析依赖并合并
- 配合特定的AMD加载器使用,RequireJS
- 同时还诞生了很多类似的模块标准CMD
1 | define(function(require){ |
ES6
1 | //通过相对路径获取依赖模块 |
- 引入和暴露的方式更加多样
- 支持复杂的静态分析
Bundler
诞生原因
使用import export这种同步加载的方式在大多数浏览器中无法使用。
是什么
- 打包工具
- 将浏览器不支持的模块进行编译,转换,合并最后生成的代码可以在浏览器端良好的运行的工具。
Webpack
对于web应用来说:一般采用单 Javascript 文件入口
Rollup
后起之秀的打包工具
1 | npx rollup main.js --file dist/bundle.js --format iife |
Vite的工作原理
解决的问题
大型应用使用webpack等传统bundler会遇到性能瓶颈:非常慢。Vite就使用浏览器发展中的新特性来解
决这个问题。
Vite开发环境 —esbuild
Vite以原生ESM方式提供源码,这实际上是让浏览器接管了打包程序的部分工作。
处理方式
- 依赖
- 使用esbuild进行预构件,esbuild由go编写,比基于Node.js的工具要快10-100倍。
- 处理CommonJS以及UMD类型文件的兼容性,转换为ESM以及ESM的导入形式。
- 提高性能,将多个模块合并成单个模块。因为原生ESM格式下,一个文件就是一次请求。
- 缓存,将预构建的依赖项缓存到node_modules/.vite中。
- 使用esbuild进行预构件,esbuild由go编写,比基于Node.js的工具要快10-100倍。
- 源码
- 包含一些非Javascript标准格式的文件,比如JSX/CSS/Vue等等,时常会被编辑。
esbuild速度对比
Vite生产环境 —Rollup
并没有使用es modules的格式,而是使用Rollup的形式构建。
为什么?
在生产环境中使用未打包的ESM仍然效率低下。Vite附带了一套已经内置的构建优化以及构件命令,可以做
到开箱即用。
为什么使用Rollup,而没有选用上面说的ESBuild?
Vite目前的插件API与使用esbuild
作为打包器并不兼容。尽管esbuild
速度更快,但Vite采用了
Rollup灵活的插件API和基础建设。
Webpack打包流程
ESM 打包流程
打包什么类型的模块
结论
- 首要格式 - ES modules,并且提供支持 typescript 的 type 文件。
- 备选方案 - UMD
Vue3的插件系统
应用
1 | import ElementPlus from 'element-plus' |
一段代码给vue
应用实例添加全局功能。它的格式是一个 object
暴露出一个install()
方法,或者一个function
配置
1 | const myPlugin = { |
功能
插件没有严格的限制,一般有以下几种功能
- 通过
app.component()
和app.directive()
注册一到多个全局组件或自定义指令。 - 通过
app.provide()
使一个资源可被注入进整个应用。 - 向
app.config.globalProperties
中添加一些全局实例属性或方法
一个插件配置的例子
使用方法
打包组件库
Vite构建生产版本
当需要将应用部署到生产环境时,只需运行vite build
命令。默认情况下,它使用<root>/index.html
作为其构建入口点,并生成能够静态部署的应用程序包。
自定义构建
1 | //vite.config.js |
库模式
1 | lib: { |
配置组件库的rollup
1 | build: { |
生成类型定义文件
1 | //tsconfig.build.json |
1 | //vite.config.ts |
生成样式文件
1 | //index.ts |
1 | rollupOptions: { |
发布npm
NPM简介
允许用户从npm服务器下载别人编写的第三方包到本地使用。
允许用户从npm服务器下载并安装别人编写的命令行程序到本地使用。
允许用户将自己编写的包或命令行程序上传到npm服务器供别人使用。
一些常见命令:
1 | #登陆,注意现在登陆都是有两步验证的,会发一个数字到你的邮箱去 |
语义化版本
版本规则:Semver标准语义化版本
https://semver.org/lang/zh-CN/
1.主版本号:当你做了不兼容的API修改,
2.次版本号:当你做了向下兼容的功能性新增,
3.修订号:当你做了向下兼容的问题修正。
package,json所有信息
https://docs.npmjs.com/cli/v10/configuring-npm/package-json
Pre&Post scripts
你script的名称前面加上pre或者post,那么当运行这个命令的时候,pre和post会自动在这个命令之前
或者之后运行。
1 | "scripts":{ |
Life Cycle Scripts
prepare
- 在package被packed之前运行。
- 在packge被published之前运行。
- 在npm install的时候运行。
prepublish(即将废弃)
prePublishOnly
npm publish会触发的hooks
-
prepublishonly
- prepare
- prepublish
- publish
- postpublish
npm命名空间
- @vikingmute/XxXx
- 前面的是命名空间名称,后面的是具体的包名。
- 命名空间通常用于避免包名冲突,并提供更好的包管理和组织,同时也可以提供更好的可读性和包的可发现性。
- 使用命名空间需要带特定参数,也就是公开,因为这个功能默认是private的并且收费的。公开就没有问题。
1 | npm publish --access public |
发布成功
总结
- Javascript模块化发展的历史
- 全局变量
- common.js / AMD / UMD
- ES6 modules
- Bunder的功能和作用
- Vite (Rollup+ESBuild) 在 dev 和 build 的过程为什么这么不同
- Webpack
- 确定要打包生成的代码格式:ES+UMD
- Vue3的插件系统
- Vue3的插件的格式
- 改造组件库的入口文件
- 使用Vite进行打包
- Rollup 的概念和简单使用
- 使用Vite内置的build模式以及库模式进行打包
- 生成typescript定义文件
- 生成样式文件
- 优化生成文件,根据不同格式 ( es / umd ) 分离打包脚本
- 优化 package.json的字段
- 使用 npm link 进行本地测试
- 使用npm进行发布
- 简介 npm 以及完成注册
- 使用 npm scripts 钩子函数 (prePublishOnly) 进行发布前 build
- 发布 npm 成功
项目难点:
思考:
这个项目的业务背景是什么,在业务上有什么比较牛逼的地方,推动了业务如何运行等等
技术实现上:这个项目的整体技术实现思路是怎样的,项目中用了什么比较牛逼的技术,解决了什么比较困难的问题等等
组件库项目的业务背景是为了提高开发效率和保持一致性。通过创建一组可重复使用的UI组件,开发团队可以在不同的项目中共享和重用这些组件,从而节省时间和精力。这样的项目通常涉及跨团队合作,以确保组件的设计和实现符合整个组织的标准和需求。
对个人而言,建立一个组件库项目可能是为了提升个人开发技能、加强代码重用性和维护性。通过创建自己的组件库,可以更快速地开发项目,并在多个项目中重复使用自己设计和实现的组件,从而节省时间和精力。这样的项目也可以作为个人的作品集,展示个人的技术能力和风格。
组件开发的方法论
- 根据需求初步确定属性/事件/slots/expose (不需要特别精确,后期随着功能开发可以持续更新)
- 组件的静态版本(不加交互,只有html结构,classes,slots)
- 将需求中有行为的功能做成开发计划列表
- 根据列表一项项完成功能
- 样式/测试等收尾工作
二次开发组件
第一步:支持组件的原始属性
- inheritAttrs:false 不继承属性
- 使用$props访问所有属性
- 要注意不继承以后一些默认属性失效的问题
v-bind = $attrs
第二步扩充组件属性(icon
组件)
可以自己添加属性
type/color
要过滤传递的属性 lodash库中的
omit
函数
结构方面
组件结构选择
一、在tooltip和dropdown 选择了两种截然不同的开发方式表示菜单选项
1.使用语义化结构开发 –tooltip 新建一个对应组件并且使用slot来实现复杂结构
2.使用JavaScript数据结构开发 —dropdown 传入一个数组,也就是传入一个JavaScript Object
两种实现方式都有各自的优势和适用场景。让我们逐一讨论它们:
使用语义化结构开发
示例组件:Tooltip
在这种方式下,我们通过编写具有良好语义的 HTML 结构来构建组件。对于较为复杂的结构,我们可以使用 Vue 的 slot
功能,允许开发者在父组件中传递任意内容到子组件中,并将其插入到指定的位置。
优势:
- 可读性强: 语义化的 HTML 结构使得组件的代码更易于理解和维护,使开发者能够更清晰地了解组件的结构和功能。
- 灵活性: 使用
slot
可以让开发者在不同的场景下传递不同的内容,使得组件更加灵活,能够适应各种使用场景。 - 可访问性: 语义化的 HTML 结构有助于提高组件的可访问性,使得组件对于残障用户也更加友好。
劣势:
- 维护成本: 在构建复杂结构时,可能需要编写大量 HTML 代码,并且需要花费更多的时间来维护和修改组件。
- 可复用性: 有时候,使用语义化结构可能会导致组件的结构过于具体化,使得组件的复用性降低。
使用 JavaScript 数据结构开发
示例组件:Message
在这种方式下,我们通过 JavaScript 对象或数组等数据结构来描述组件的结构和行为,然后在组件内部根据这些数据结构来动态生成相应的 HTML 结构。
优势:
- 灵活性: 使用 JavaScript 数据结构开发的组件可以更加灵活,可以根据不同的数据来动态生成不同的结构,适应不同的需求。
- 便于维护: 相比于直接编写 HTML 结构,使用 JavaScript 数据结构可以使组件的结构更加简洁清晰,便于维护和修改。
- 代码复用: 可以将一些通用的逻辑抽象成函数或组件,提高代码的复用性。
劣势:
- 可读性较差: 相比于直接编写 HTML 结构,使用 JavaScript 数据结构来描述组件的结构可能会使得代码的可读性下降,需要更多的注释和说明来解释代码的作用。
- 学习成本: 对于一些初学者来说,理解和使用 JavaScript 数据结构可能会有一定的学习成本,相对于直接编写 HTML 结构来说更加复杂。
对比
- 适用场景: 使用语义化结构适合构建静态内容较多,结构相对固定的组件,而使用 JavaScript 数据结构适合构建动态内容较多,结构需要根据数据动态生成的组件。
- 可维护性: 使用语义化结构的组件通常更易于理解和维护,而使用 JavaScript 数据结构的组件可能更容易被重构和调整。
- 灵活性: 使用 JavaScript 数据结构的组件通常更灵活,能够适应各种不同的需求和场景,而使用语义化结构的组件可能在结构上更受限制,但通常更易于调试和排查问题。
综上所述,选择哪种实现方式取决于具体的需求和场景,有时候也可以结合两种方式来开发组件,以取长补短,达到更好的效果。
样式方面
使用了CSS Modules 统一的颜色系统 Postcss Transition
Postcss
PostCSS 不仅仅是一个预处理器,它提供了许多功能强大的插件
- 使用未来的 CSS 语法: 开发者可以在 CSS 中使用未来的 CSS 规范,如 CSS Modules、CSS Nesting、自定义属性(CSS 变量)等,然后使用 PostCSS 插件将其转换为当前浏览器支持的标准 CSS。
- 自动添加浏览器前缀: PostCSS 可以根据配置自动为 CSS 属性添加所需的浏览器前缀,以确保在各种浏览器中获得一致的样式。
- 代码压缩: PostCSS 可以通过移除注释、空格和不必要的代码来压缩 CSS。
- CSS 校验: 一些 PostCSS 插件可以帮助检查 CSS 代码中的错误或不规范的写法,并提供警告或错误信息。
- CSS Modules: PostCSS 支持 CSS Modules,这使得在大型项目中管理 CSS 变得更加容易和可维护。
选中 Postcss 的原因:
- 轻量级
- 插件化-postcss-each postcss-for等
- Vite原生支持-https://vitejs,dev/guide/features,html#postcss
CSS Modules
CSS Modules 是一种用于管理 CSS 的技术,它可以帮助解决 CSS 的全局作用域和命名冲突等问题。使用 CSS Modules,您可以将 CSS 样式文件模块化,使每个模块的样式只在其自己的作用域内生效,从而避免了全局污染和命名冲突。
以下是 CSS Modules 的一些关键特性和用法:
- 模块化: 使用 CSS Modules,您可以将每个 CSS 文件视为一个独立的模块,每个模块都有自己的作用域,样式只在该作用域内生效。这样可以确保样式不会被其他模块影响,从而提高了代码的可维护性和可重用性。
- 局部作用域: 在 CSS Modules 中,类名和选择器都是局部的,只在当前模块中生效。这意味着您可以在不同模块中使用相同的类名而不会产生冲突,因为它们都被编译为不同的类名。
- 自动生成类名: CSS Modules 会自动为每个类名生成一个唯一的标识符,以防止命名冲突。这个标识符通常是基于文件名和类名生成的,因此您不需要手动管理类名。
- 导入和使用: 在 JavaScript 或 TypeScript 中,您可以通过导入 CSS 文件来使用 CSS Modules 中定义的样式。例如,在 React 或 Vue 组件中,您可以使用
import styles from './styles.module.css'
来导入样式,并通过styles.className
来引用特定的类名。 - 提高可维护性: CSS Modules 可以帮助提高项目的可维护性,因为它将样式和组件或模块紧密耦合在一起,使得样式更容易理解和修改。
使用CSS Modules的原因:
- 可重用
- 作用域隔离
缺点:
- 看起来不像是CSS的正常写法
- 定义CSS全局样式比较麻烦
- 需要额外的loader来生成Typescript的支持
- 生成的类名并不友好
Transition
Transition
是 Vue.js 内置的组件,用于在元素插入、更新或删除时,通过 CSS 过渡和动画来实现平滑的过渡效果。它提供了一种简单而强大的方式来控制元素的过渡动画,使得用户界面更加生动和流畅。
以下是 Transition
内置组件的一些重要属性和用法:
name 属性: 指定过渡动画的名称,可以用于在 CSS 中定义过渡动画的样式。Vue 会根据过渡状态为元素添加不同的类名,类名由 name 属性和状态名组成。
appear 属性: 指定是否在初始渲染时执行过渡动画,默认为 false。如果设置为 true,则在组件初始渲染时,会触发过渡动画。
type 属性: 指定过渡效果的类型,可以是
"transition"
(默认)或"animation"
。如果设置为"transition"
,则使用 CSS 过渡实现过渡效果;如果设置为"animation"
,则使用 CSS 动画实现过渡效果。duration 属性: 指定过渡动画的持续时间,单位为毫秒。可以设置为一个数字,表示持续时间;也可以设置为一个对象,包含
enter
、leave
和appear
等属性,分别指定不同状态下的持续时间。easing 属性: 指定过渡动画的缓动函数,用于控制动画的加速和减速效果。可以设置为 CSS 中定义的缓动函数,如
"ease"
、"ease-in"
、"ease-out"
等。css 属性: 指定是否使用 CSS 过渡或动画来实现过渡效果,默认为 true。如果设置为 false,则不会应用任何过渡动画,适用于在 JavaScript 中自定义过渡效果的场景。
onBeforeEnter、onEnter、onAfterEnter、onBeforeLeave、onLeave、onAfterLeave 属性: 这些属性用于设置过渡动画的回调函数,分别在不同阶段触发,可以用于执行自定义逻辑或处理过渡状态。
通过使用 Transition
内置组件,您可以轻松地为元素添加过渡动画,使得用户界面在元素插入、更新或删除时具有更加流畅和美观的效果。
使用transformY以及fade作出一个fade-up的效果(Message)
功能方面
组件测试
为什么想到要组件测试?
- 确保组件功能的正确性: 组件库系统的核心是提供一系列可复用的组件,这些组件在不同的项目中被频繁使用。为了保证组件的功能和行为符合预期,需要进行组件测试。测试可以验证组件的各种输入、状态和交互是否按照设计要求进行工作,从而确保组件功能的正确性。
- 提高组件库的稳定性: 组件库通常会被多个项目或团队使用,因此稳定性是非常重要的。通过组件测试,可以发现并修复潜在的问题和 bug,从而提高组件库的稳定性,减少在实际项目中出现问题的可能性。
选择Vitest的原因?
Vitest的优点
- 基于Vite而生,和Vite完美配合,共享一个生态系统
- 兼容Jest语法
- HMR for tests
- ESM,TS,JSX 原生支持
Vitest 是一个基于 Vite 构建的测试运行器,它具有许多优点和特性,使得它成为一个优秀的测试工具:
基于 Vite 构建: Vitest 是基于 Vite 构建的,与 Vite 完美配合,共享同一个生态系统。这意味着 Vitest 继承了 Vite 的许多优点,如快速的开发服务器、即时热重载(HMR)和现代的构建工具链,从而提供了高效、快速和稳定的测试环境。
兼容 Jest 语法: Vitest 兼容 Jest 的语法,可以直接使用 Jest 的断言、模拟和其他测试工具。这使得迁移现有的 Jest 测试套件到 Vitest 非常容易,无需重写现有的测试代码,从而减少了迁移的难度和成本。
HMR for tests: Vitest 支持测试文件的即时热重载(HMR),当测试文件发生变化时,测试会自动重新运行,无需手动刷新页面或重新启动测试服务器。这大大提高了开发人员的开发效率,可以更快地获得反馈和调试测试代码。
ESM、TS、JSX 原生支持: Vitest 原生支持 ES 模块、TypeScript 和 JSX,无需额外配置。开发人员可以直接编写测试文件,使用现代的 JavaScript、TypeScript 和 JSX 语法,从而提高了测试代码的可读性和可维护性。
综上所述,Vitest 是一个功能强大、易用且高效的测试工具,它基于 Vite 构建,兼容 Jest 语法,支持 HMR 和现代的 JavaScript、TypeScript 和 JSX 语法,为开发人员提供了一个优秀的测试环境和体验。
用到了单元测试工具
Vue-test-utils
Vue Test Utils 是 Vue.js 官方提供的用于编写单元测试的工具库。它提供了一系列的 API 和工具函数,帮助开发人员轻松地测试 Vue.js 组件的行为和状态。
以下是 Vue Test Utils 的一些主要特点和功能:
与各种测试框架兼容: Vue Test Utils 不依赖于任何特定的测试框架,可以与主流的单元测试框架(如 Jest、Mocha、AVA 等)兼容使用。这使得开发人员可以根据自己的喜好和项目需求选择合适的测试框架,并且无需更改测试代码。
提供了一系列 API 和工具函数: Vue Test Utils 提供了丰富的 API 和工具函数,用于创建、渲染和操作 Vue 组件,并且模拟用户行为和交互。开发人员可以使用这些 API 和工具函数编写清晰、简洁的测试代码,覆盖组件的各种情况和边界条件。
提供了异步测试支持: Vue Test Utils 支持异步测试,可以处理异步操作和异步更新。开发人员可以使用
await
关键字等待异步操作完成,并且检查组件的状态和行为是否符合预期。提供了快照测试支持: Vue Test Utils 提供了快照测试的支持,可以方便地比较组件的渲染结果。开发人员可以使用
toMatchSnapshot()
函数生成组件的快照,并且在后续的测试中比较新的渲染结果与快照是否一致。与 Vue Devtools 配合使用: Vue Test Utils 与 Vue Devtools 配合使用,可以在测试过程中方便地查看组件的状态、属性和事件,并且调试组件的行为和交互。这使得开发人员可以更加直观地理解组件的工作原理和内部实现。
综上所述,Vue Test Utils 是一个功能丰富、灵活且易于使用的工具库,为开发人员提供了编写、运行和维护 Vue.js 组件单元测试的便利性和效率。通过使用 Vue Test Utils,开发人员可以更加自信地编写高质量的测试代码,并且确保 Vue.js 组件的功能和行为符合预期。
在组件测试中用创建vnode做了什么,有什么效果?
h和createVnode都可以创建vnode,
h是hyperscript的缩写,意思就是”JavaScript that produces HTML(hypertext markup language)”,很多virtualDOM的实现都使用这个函数名称。
还有一个函数称之为createVnode,更形象,两个函数的用法几乎是一样的。
1 | import { h,createVnode} from 'vue' |
使用render function/JSX写组件
1 | //render function |
1 | // 使用 JSX 语法 |
这两种方式都可以用来编写 Vue 3 组件的 render 函数,选择其中一种取决于个人偏好和项目要求。
使用JSX写组件和使用Vue的区别
在组件编写方面,JSX 和 Vue.js 有一些相似之处,但也存在一些显著的区别。
相似之处:
组件化开发: JSX 和 Vue.js 都支持组件化开发,即将 UI 划分为多个独立的组件,每个组件负责特定的功能或视图,从而实现代码复用和逻辑分离。
父子组件通信: 在 JSX 和 Vue.js 中,父组件可以通过 props 向子组件传递数据,子组件可以通过事件向父组件发送消息。
生命周期钩子: JSX 和 Vue.js 都提供了生命周期钩子函数,可以在组件的不同阶段执行特定的逻辑,例如在组件被创建、更新或销毁时执行特定的操作。
不同之处:
语法: JSX 是 JavaScript 的一种扩展语法,可以直接在 JavaScript 中使用,组件的定义和渲染与普通的 JavaScript 代码类似;而 Vue.js 使用的是基于 HTML 的模板语法,在 Vue 组件中编写单独的模板代码,包括模板语法和 Vue 特定的指令。
模板语法: 在 Vue.js 中,可以使用模板语法来描述组件的 UI 结构和数据绑定关系,例如使用
{{ }}
插值表达式进行数据绑定,使用v-bind
和v-on
指令进行属性绑定和事件绑定;而在 JSX 中,可以直接在 JavaScript 中编写 JSX 元素,动态生成 UI 结构和处理事件逻辑。样式作用域: 在 Vue.js 中,可以使用
<style scoped>
标签来定义组件的样式,样式将自动应用于当前组件,并且会自动添加唯一的作用域标识符,防止样式污染;而在 JSX 中,通常需要手动管理组件的样式,并且需要注意避免全局样式污染。数据响应式: 在 Vue.js 中,数据的变化会自动触发组件的重新渲染,无需手动处理;而在 JSX 中,通常需要手动管理组件的状态和 UI 的更新。
综上所述,虽然 JSX 和 Vue.js 都支持组件化开发,但它们在语法、模板语法、样式作用域和数据响应式等方面存在一些差异,开发者可以根据项目需求、团队经验和个人偏好选择适合的技术栈。
测试方面的注意点?(tooltip)
- 注意定时器的影响
- vi.useFakeTimers()
- vi.runAllTimers()
- 注意点击到外侧区域的测试
防抖操作(tooltip)
这段代码使用了 lodash-es
库中的 debounce
函数来实现防抖效果。下面是具体的实现流程:
- 在组件中导入
debounce
函数:
1 | import { debounce } from 'lodash-es' |
- 定义了两个函数
openDebounce
和closeDebounce
,分别用于处理打开和关闭操作的防抖:
1 | const openDebounce = debounce(open, props.openDelay) |
这里的 open
和 close
函数是对应的打开和关闭操作。props.openDelay
和 props.closeDelay
分别表示打开和关闭操作的延迟时间,通过组件的 props 进行配置。
- 在需要立即执行打开或关闭操作时,调用
openDebounce.cancel()
或closeDebounce.cancel()
取消之前的防抖操作,然后立即执行对应的打开或关闭操作:
1 | const openFinal = () => { |
这里通过 cancel
方法取消之前的防抖操作,然后再次调用对应的打开或关闭函数,以确保立即生效。
通过使用 debounce
函数,可以限制某个函数在短时间内被多次调用,从而避免不必要的执行次数,实现防抖效果。
防抖是什么?
防抖(Debouncing)是一种常见的前端技术,用于限制某个函数在短时间内被多次调用,以减少不必要的执行次数。在防抖的实现中,当触发事件时,如果在指定的延迟时间内再次触发了相同的事件,那么前一个事件处理函数的执行就会被取消,并重新设置一个新的延迟。只有在延迟时间内没有再次触发事件时,才会执行函数。
下面是防抖的基本思想:
- 当触发事件时,设置一个定时器,在指定的延迟时间后执行事件处理函数。
- 如果在延迟时间内再次触发了相同的事件,则清除之前的定时器,重新设置一个新的定时器。
- 只有当延迟时间内没有再次触发事件时,才会执行事件处理函数。
节流是什么?
节流(Throttling)是一种常见的前端技术,用于控制某个函数在一定时间间隔内只能执行一次,以减少函数的执行频率。与防抖不同,节流是在一定时间间隔内稀释函数的执行次数,而不是将多次触发的事件合并成一次执行。
下面是节流的基本思想:
- 当触发事件时,设置一个标记(比如布尔值)来表示函数是否正在执行。
- 如果函数没有在执行,则执行函数,并将标记设置为 true。
- 在指定的时间间隔内,即使触发了相同的事件,也不会再次执行函数,直到时间间隔结束。
- 时间间隔结束后,将标记重置为 false,以允许下一次事件触发时执行函数。
节流的应用场景包括但不限于:
- 页面滚动事件:在用户滚动页面时执行某个操作,但限制操作的执行频率,避免频繁触发。
- 鼠标移动事件:在鼠标移动时执行某个操作,但限制操作的执行频率,提高性能。
- 输入框输入事件:在用户输入文本时执行某个操作,但限制操作的执行频率,减少不必要的请求或处理。
使用节流技术可以有效地控制函数的执行频率,从而提高页面性能和用户体验,并且在一些需要限制事件触发频率的场景下非常实用。
hooks –自定义钩子函数
自定义钩子函数在 Vue 中具有多种用途,它们允许你在组件的生命周期或特定阶段执行自定义逻辑,从而扩展或定制组件的行为。下面列出了几个常见的用途:
组件复用: 自定义钩子函数使得可以在多个组件之间共享相同的逻辑。例如,你可以创建一个自定义钩子函数来处理数据请求,然后在多个组件中使用这个钩子函数来获取数据。
代码组织: 自定义钩子函数有助于将组件的逻辑分解为更小的、可重用的部分,从而提高代码的可维护性和可读性。你可以将一些复杂的逻辑抽象为自定义钩子函数,并在需要的地方进行调用。
逻辑复用: 自定义钩子函数可以用于封装一些常见的逻辑,使得这些逻辑可以在不同的组件中重复使用。例如,你可以创建一个自定义钩子函数来处理表单验证逻辑,然后在多个表单组件中重复使用这个钩子函数。
测试: 自定义钩子函数可以使得组件更容易进行单元测试。你可以将一些与组件生命周期相关的逻辑封装在自定义钩子函数中,并在测试时对这些钩子函数进行单独测试。
总的来说,自定义钩子函数为你提供了一种灵活的方式来扩展组件的功能,并使得代码更加模块化、可重用和易于测试。
在项目中的使用
useEventListener.ts
这个自定义钩子函数的作用是为给定的目标对象(可以是 Ref 类型或普通对象)添加事件监听器,并在组件卸载前移除该事件监听器。具体来说,它实现了以下功能:
- 如果目标对象是 Ref 类型,它会在目标对象的值发生变化时添加或移除事件监听器。
- 如果目标对象是普通对象,它会在组件挂载时添加事件监听器,并在组件卸载前移除事件监听器。
这个自定义钩子函数采用了 Vue 3 的 Composition API,使用了 onMounted
、onBeforeUnmount
、isRef
、watch
和 unref
等函数。
具体实现流程如下:
- 如果目标对象是 Ref 类型,则通过
watch
监听目标对象的变化,并在新值中添加事件监听器,并在旧值中移除事件监听器。 - 如果目标对象不是 Ref 类型,则在组件挂载时通过
onMounted
添加事件监听器,并在组件卸载前通过onBeforeUnmount
移除事件监听器。 - 在移除事件监听器时,使用
unref
来获取目标对象的实际值,并调用其removeEventListener
方法移除事件监听器。
这个自定义钩子函数的作用是封装了事件监听器的添加和移除逻辑,使得在 Vue 组件中使用事件监听器变得更加方便和灵活。
useClickOutside.ts
这个自定义钩子函数 useClickOutside
的作用是监听点击事件,并判断点击事件是否在指定的元素外部,然后执行相应的回调函数。主要逻辑如下:
- 在点击事件发生时,检查点击的目标是否在指定的元素外部。
- 如果点击的目标不在指定的元素外部,则调用传入的回调函数。
这个自定义钩子函数采用了 Vue 3 的 Composition API,使用了 onMounted
和 onUnmounted
钩子函数。
具体实现流程如下:
- 在组件挂载后,通过
onMounted
钩子函数向document
添加一个点击事件监听器,以便在点击事件发生时触发handler
函数。 - 在点击事件发生时,
handler
函数判断点击的目标是否在指定的元素外部,如果是,则调用传入的回调函数。 - 在组件卸载前,通过
onUnmounted
钩子函数移除之前添加的点击事件监听器,以防止内存泄漏和性能问题。
这个自定义钩子函数的作用是在 Vue 组件中封装了监听点击事件的逻辑,使得可以更方便地判断点击是否在指定元素外部,并执行相应的逻辑。
useZIndex.ts
这段代码定义了一个自定义钩子函数 useZIndex
,用于管理一个全局的 z-index 值,并提供获取下一个 z-index 的功能。具体实现如下:
- 使用
ref
创建了一个名为zIndex
的响应式变量,用于存储全局的 z-index 值,初始值为 0。 - 定义了
useZIndex
函数,接受一个初始值initialValue
,默认为 2000。在函数内部:- 使用
ref
创建了一个名为initialZIndex
的响应式变量,用于存储初始值。 - 使用
computed
创建了一个名为currentZIndex
的计算属性,用于计算当前的 z-index 值,即全局的 z-index 值加上初始值。 - 定义了一个名为
nextZIndex
的函数,用于获取下一个 z-index。在函数内部,将全局的 z-index 值增加 1,并返回当前的 z-index 值。
- 使用
最后,返回了一个包含 currentZIndex
、nextZIndex
和 initialZIndex
的对象,这样在组件中使用该自定义钩子函数时,可以方便地获取当前的 z-index 值、获取下一个 z-index 值以及修改初始值。
这个自定义钩子函数的作用是在 Vue 组件中管理 z-index 值,提供了方便的接口来获取和更新 z-index 值,通常用于管理弹出框、对话框等组件的层级关系。
实现方面
InjectionKey
在 Vue3 中使用 TS 时,父组件通过 provide 函数注入的数据类型往往是未知的,而子组件调用 inject 引入数据时也无法确定具体的数据类型是什么,这就产生了可维护性问题,比如某位同事写了下面这段代码时
1 | import { inject } from "vue" |
对于 colors
导数的数据类型我们并不知道是什么,它可以是对象 or 数组亦或是字符串,只能顺瓜摸藤找到它的 provide
,对于小项目找一下可能不花费什么时间,但对于大型项目来说很明显是不可取的,于是官方提供了 InjectionKey
函数来对传参进行类型约束,确保父子间传递的数据类型是可见、透明的。
InjectionKey
函数的使用也很简单,原理是将 provide
与 inject
的第一个参数即 key
通过声明 symbol
的方式关联起来即可
1 | // 1. 将 InjectionKey 定义的数据类型放到 keys/index.ts 下维护 |
有了 InjectionKey
函数后就不用再担心 provide&inject 之间的数据类型问题了,我们只需找到 InjectionKey 关联的 key即可
语义化展示
要实现语义化展示,父子属性传递的常规方式:
把数据状态以及处理逻辑 放在父组件
使用Provide/Inject 传递给子组件
omit 过滤属性
omit
_.omit(object, keys)
函数返回一个没有列入key属性的对象。其中,参数object为JSON格式的对象,keys表示多个需要排除掉的key属性。
1 | const _ = require('lodash/object'); |
使用v-on = events 动态绑定事件(tooltip)
好处:
- 灵活性: 可以根据组件的状态或属性动态地决定何时绑定事件,以及绑定哪些具体的事件。
- 简洁性: 可以通过简单地修改组件的属性或状态来控制事件的绑定和解绑,而无需在模板中编写大量的条件语句。
- 可维护性: 通过集中管理事件绑定的逻辑,可以使代码更加清晰易懂,并且便于后续的维护和修改。
原理:
- 事件监听器的绑定: 当 Vue 解析模板时,遇到
v-on
指令时,会将其解析为一个事件监听器,并动态地绑定到相应的 DOM 元素上。这个事件监听器可以是一个函数,也可以是一个对象,对象的键为事件名,值为事件处理函数。 - 事件处理函数的动态更新: 如果
events
对象是响应式的(通过ref
或reactive
创建),则当events
对象发生变化时,相关的事件处理函数也会动态更新。这意味着可以在组件的生命周期内动态地修改events
对象,从而动态地修改事件处理逻辑。 - 事件绑定的动态性: 由于
v-on
指令是在模板编译阶段解析的,因此可以根据组件的属性、状态或其他条件动态生成events
对象,从而实现事件绑定的动态性。这使得组件的事件处理逻辑可以根据不同的情况进行动态调整。
手动触发功能的实现(tooltip)
Tooltip 组件代码通过监听 props.manual
属性的变化,来实现手动触发功能。具体实现步骤如下:
- 当
props.manual
的值为false
(即非手动模式)时,在组件初始化时会调用attachEvents
函数,该函数根据props.trigger
的值来决定绑定哪些事件监听器。 - 如果
props.trigger
的值是'hover'
,则将mouseenter
事件绑定到openFinal
函数,将mouseleave
事件绑定到closeFinal
函数;如果props.trigger
的值是'click'
,则将click
事件绑定到togglePopper
函数。 - 当用户点击触发节点时,触发
togglePopper
函数,根据当前弹出框的状态来切换打开或关闭状态。 - 如果
props.manual
的值为true
(即手动模式),则取消绑定所有事件监听器,即清空events
和outerEvents
对象。 - 这样,用户可以根据
props.manual
属性的值来控制是否手动触发弹出框的显示和隐藏。
支持popper参数(tooltip)
这段代码定义了一个计算属性 popperOptions
,用于动态计算 Popper 实例的选项。根据组件的 props(例如 placement
和 popperOptions
),它会生成一个包含 Popper 配置的对象。
具体来说:
placement
属性表示 Popper 的放置位置。modifiers
是一个数组,包含一系列修改器,用于调整 Popper 的行为。在这里,定义了一个名为'offset'
的修改器,用于设置 Popper 的偏移量,使其与触发器元素对齐。偏移量设置为[0, 9]
,表示水平偏移为 0,垂直偏移为 9。- 使用对象展开运算符
...
将用户传入的自定义配置(如果有的话)与默认配置合并,确保 Popper 实例的选项包含用户自定义配置,同时保留了默认的配置。
总之,通过这个计算属性,可以根据组件的 props 动态生成 Popper 实例的选项,使得 Popper 实例的行为可以根据组件属性的变化而动态调整。
渲染vnode列表项(Dropdown)
1 | import { defineComponent } from 'vue' |
1 | <RenderVnode :vNode="item.label"/> |
1 | import type { VNode } from 'vue' |
在提供的代码中,渲染函数 return () => props.vNode
返回的是 props.vNode
,即 vNode
prop 的值。因此,这个渲染函数实际上渲染了从父组件传递过来的虚拟节点(即 Vue 中的 VNode)。这个渲染函数并不会对传入的虚拟节点进行任何处理,它只是简单地返回传入的虚拟节点。
如果父组件将一个字符串或者一个组件的 VNode 作为 vNode
prop 传递给 RenderVnode
组件,那么 RenderVnode
组件将会渲染这个传入的虚拟节点。
使用函数式的方式创建组件
根据上面的代码,我们可以总结如下使用函数式的方式创建组件的步骤:
- 导入所需的 Vue 3 相关函数和组件类型。
- 定义组件的创建函数,该函数接收组件的属性作为参数。
- 在创建函数内部,执行必要的逻辑,例如生成唯一的组件标识符、创建容器元素等。
- 根据需要调用其他自定义 hook 或函数来获取额外的数据或执行其他逻辑。
- 创建新的组件属性对象,并在其中包含传入的属性以及其他必要的属性,如组件标识符、Z 轴顺序等。
- 使用
h
函数创建虚拟节点,并传入组件构造函数和属性对象。 - 使用
render
函数将虚拟节点渲染到容器中。 - 将容器中的组件添加到页面上的合适位置,通常是
document.body
。
这种方式的好处是,它允许以纯函数的方式创建组件,使得组件的创建过程更加灵活和可控,同时也更容易进行测试和调试。
VitePress特点
VitePress 是一个基于 Vite 构建的静态网站生成器。它有一些显著的优势和特点:
快速构建:VitePress 利用了 Vite 的快速开发能力,因此构建速度非常快。Vite 是一个基于现代 JavaScript 的构建工具,具有出色的性能,因此 VitePress 可以在几秒钟内为您的网站生成页面。
Vue 驱动:VitePress 是基于 Vue.js 构建的,这意味着您可以使用 Vue 的强大功能来构建和定制您的网站。Vue 是一个流行的 JavaScript 框架,它提供了诸如组件化、响应式数据绑定等功能,使得构建复杂的用户界面变得更加简单和高效。
Markdown 支持:VitePress 支持使用 Markdown 编写内容,这使得编写和维护文档变得非常简单。您可以在 Markdown 中编写内容,而无需关注 HTML 或其他复杂的语法。
热重载:VitePress 支持热重载,这意味着当您编辑内容或修改样式时,您所做的更改会立即在浏览器中实时显示,而无需手动刷新页面。
主题定制:VitePress 允许您轻松定制网站的外观和风格。您可以选择预先构建的主题,也可以根据需要创建自己的主题。这使得您可以根据项目的需求定制您的网站,使其与您的品牌或设计风格保持一致。
插件系统:VitePress 提供了一个灵活的插件系统,使得您可以扩展其功能。您可以编写自己的插件,或者使用社区中已有的插件来增强您的网站。
总的来说,VitePress 是一个快速、灵活且易于使用的静态网站生成器,适用于构建各种类型的网站,包括文档、博客、演示等。
TDD的开发方式
TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法,它强调在编写代码之前先编写测试用例,然后通过不断地编写和运行测试用例来驱动代码的开发。
- 1.编写测试用例
- 2.运行测试用例
- 3.编写代码:根据测试用例的要求,编写代码,实现功能。
- 4.运行测试用例
- 5.重构代码(可选)
- 6.重复上述步骤
TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法,其主要特点包括以下几点:
测试先行:在编写实际代码之前,先编写测试用例。这意味着在实现功能之前,您首先要考虑如何测试该功能。这有助于确保您的代码具有良好的覆盖率,并且在代码实现之前就已经考虑了您希望代码如何被使用。
快速反馈:TDD 鼓励频繁运行测试,以便及时发现问题。由于测试是在编写代码之前编写的,因此一旦编写的代码无法通过测试,您就可以立即得到反馈,并且可以及时进行修复。
增量开发:TDD 鼓励采用增量开发的方式,即每次只编写足够的代码来通过当前失败的测试用例。这有助于确保您的代码在不断迭代中保持可用性,并且可以减少在后续开发阶段出现的集成问题。
简化重构:由于在 TDD 中测试用例是作为开发的一部分编写的,因此更容易进行重构。重构是指对现有代码进行修改,以改善其结构、可读性或性能,而不改变其行为。通过测试用例,您可以确保重构后的代码仍然具有相同的行为。
文档化:测试用例充当了代码行为的文档。通过阅读测试用例,其他开发人员可以了解代码的预期行为,这有助于提高代码的可理解性和可维护性。
提高设计质量:TDD 有助于推动更好的软件设计实践。由于在编写代码之前先考虑如何测试代码,因此往往会促使编写更清晰、模块化和可测试的代码,从而提高软件的设计质量。
总的来说,TDD 是一种以测试为中心的开发方法,通过频繁运行测试、编写测试用例以及以测试驱动的方式编写代码,帮助开发人员编写更高质量、更可靠且更易于维护的软件。
可读性方面
Record
1 | // 这些都是js钩子与过渡transition相关 过渡效果相关的事件处理函数 包含一些函数的对象 ,用record --ts提高代码的可读性和可维护性 |
在这段代码中,Record<string, (el: HTMLElement) => void>
是 TypeScript 中的语法,它定义了一个类型为对象的变量 transitionEvents
,其中键是字符串类型,值是函数类型 (el: HTMLElement) => void
。这个对象表示了一组过渡效果相关的事件处理函数,用于在过渡动画的不同阶段执行相应的操作。
Record
是 TypeScript 中的一个泛型类型,它接受两个泛型参数,第一个参数表示对象的键的类型,第二个参数表示对象的值的类型。在这里,Record<string, (el: HTMLElement) => void>
表示一个键为字符串、值为函数的对象类型。
使用 Record
的好处在于它可以提高代码的可读性和可维护性。通过明确指定对象的键和值的类型,可以更清晰地了解对象的结构和用途。在这段代码中,transitionEvents
对象的键表示了过渡动画的不同阶段(如 beforeEnter
、enter
、afterEnter
等),值是对应阶段的事件处理函数,使得代码更易于理解和维护。
选择interface还是type?
在 TypeScript 中,interface
和 type
都用于定义类型,但它们之间存在一些区别:
语法:
interface
:使用interface
关键字定义接口,可以用来描述对象的形状(属性和方法)。type
:使用type
关键字定义类型别名,可以用来为任意类型定义别名。
扩展性:
interface
:接口可以被扩展,可以通过extends
关键字扩展其他接口,从而合并多个接口成为一个。type
:类型别名不支持扩展,不能直接使用extends
进行类型别名的继承。
可声明多次:
interface
:可以多次声明同一个接口,并且会自动合并成一个接口。type
:类型别名也可以多次声明,但是后面的声明会覆盖之前的声明,而不会进行合并。
适用场景:
interface
:适用于描述对象的形状,例如定义接口用于表示某种数据结构或类的契约。type
:适用于为任意类型定义别名,可以用来简化复杂类型或提高代码可读性,例如定义复杂的联合类型、交叉类型、元组类型等。
总的来说,interface
主要用于定义对象的形状和结构,支持扩展和合并,而 type
则更灵活,适用于为任意类型定义别名,但不支持扩展。
安全性方面
组件实例卸载
1 | //当组件被卸载时,通过调用 popperInstance?.destroy() 方法销毁 Popper 实例,以防止内存泄漏或其它问题 |
性能方面
shallowReactive
shallowReactive
是 Vue 3 中提供的一个 API,用于创建一个浅响应式对象。与 reactive
不同的是,shallowReactive
只会对对象的第一层属性进行响应式处理,而不会递归地处理对象内部的嵌套对象。这样可以在一定程度上减少 Vue 的响应式系统的开销,提高性能。
当你确定某个对象内部的嵌套结构不会发生变化,或者你不需要监视其内部属性的变化时,可以使用 shallowReactive
来创建一个浅响应式对象,从而避免不必要的性能开销。
然而,需要注意的是,使用 shallowReactive
可能会导致你失去了一些 Vue 响应式系统提供的便利特性,比如失去了对嵌套对象内部属性变化的监视能力。因此,需要根据具体的使用场景来权衡是否使用 shallowReactive
。
总的来说,shallowReactive
可以在一些特定情况下提供性能上的优势,但需要谨慎使用以确保不会丢失对对象属性变化的监视。
下面是一个简单的示例,演示了如何使用 shallowReactive
来创建一个浅响应式对象:
1 | import { shallowReactive } from 'vue'; |
在这个示例中,nestedObject
是一个普通的 JavaScript 对象,它包含一个嵌套的对象。然后,我们使用 shallowReactive
函数将其转换为一个浅响应式对象 shallowObject
。接着,我们对 shallowObject
的第一层属性进行修改,触发了响应式更新。但是,当我们修改嵌套属性 b.c
时,不会触发响应式更新,因为 shallowReactive
只对第一层属性进行响应式处理。最后,当我们添加新的属性 e
时,又会触发响应式更新。
shallowReactive
可以用于数组,但它仅在数组的长度发生变化时触发响应式更新,而不会在数组元素内部的变化上触发响应式更新。这就是所谓的浅响应式数组。
下面是一个示例,展示了如何使用 shallowReactive
创建一个浅响应式数组:
1 | import { shallowReactive } from 'vue'; |
在这个示例中,我们首先创建了一个普通的 JavaScript 数组 nestedArray
,然后使用 shallowReactive
将其转换为一个浅响应式数组 shallowArray
。接着,我们对 shallowArray
的长度进行修改,触发了响应式更新。但是,当我们修改数组元素的值时,并不会触发响应式更新,因为 shallowReactive
只对数组的长度进行响应式处理。最后,当我们向数组中添加新的元素时,又会触发响应式更新。
项目亮点
1.采用TDD的方式开发组件
2.使用函数式的方式开发组件
3.使用JSX的方式开发组件
让我们看看这三种开发方式的亮点:
采用TDD的方式开发组件:
- 可靠性增强:TDD 着眼于先编写测试用例,然后再编写实际的代码来满足测试,这有助于确保组件的可靠性。您可以通过编写测试来捕获和纠正潜在的问题,从而提高组件的质量和稳定性。
- 可维护性提高:TDD 鼓励频繁运行测试,并且测试用例充当了组件行为的文档。这使得组件的行为和预期结果变得清晰可见,有助于提高代码的可理解性和可维护性。
- 增量开发:TDD 通过逐步编写测试用例和代码来推动增量开发。这种方法使得开发过程更加可控和可预测,减少了集成问题的风险,并且有助于在开发过程中快速迭代。
使用函数式的方式开发组件:
- 简洁性:函数式组件通常比传统的基于类的组件更为简洁。通过使用函数式编程风格,您可以编写更少的代码来实现相同的功能,从而提高了代码的可读性和可维护性。
- 纯函数特性:函数式组件通常是纯函数,即相同的输入将始终产生相同的输出,而不会产生副作用。这使得函数式组件更容易进行测试和推理,并且有助于提高组件的可靠性。
- 函数式编程范式:使用函数式编程范式可以促进代码的模块化和可组合性,从而使得组件更易于重用和组合。函数式组件的设计可以使得代码更容易理解和调试。
使用JSX的方式开发组件:
- 声明式编程:JSX 允许您以声明式的方式描述组件的 UI 结构。通过将 UI 结构与组件的逻辑分离开来,使得代码更易于理解和维护。
- 组件化:JSX 支持组件化开发模式,使得您可以将 UI 拆分为更小的可重用组件。这种组件化的开发方式使得代码更易于管理和维护,并且促进了代码的重用性。
- 直观性:JSX 的语法与 HTML 类似,这使得开发人员更容易理解和学习。通过在 JavaScript 中编写类似 HTML 的标记,可以使得 UI 的构建过程更加直观和自然。
综上所述,采用 TDD 开发组件可以增强可靠性和可维护性,使用函数式开发方式可以提高代码的简洁性和纯度,而使用 JSX 开发方式可以实现声明式的编程、组件化和直观性。
4.使用语义化结构开发
在这种方式下,我们通过编写具有良好语义的 HTML 结构来构建组件。对于较为复杂的结构,我们可以使用 Vue 的 slot
功能,允许开发者在父组件中传递任意内容到子组件中,并将其插入到指定的位置。
优势:
- 可读性强: 语义化的 HTML 结构使得组件的代码更易于理解和维护,使开发者能够更清晰地了解组件的结构和功能。
- 灵活性: 使用
slot
可以让开发者在不同的场景下传递不同的内容,使得组件更加灵活,能够适应各种使用场景。 - 可访问性: 语义化的 HTML 结构有助于提高组件的可访问性,使得组件对于残障用户也更加友好。
5.使用 JavaScript 数据结构开发
在这种方式下,我们通过 JavaScript 对象或数组等数据结构来描述组件的结构和行为,然后在组件内部根据这些数据结构来动态生成相应的 HTML 结构。
优势:
- 灵活性: 使用 JavaScript 数据结构开发的组件可以更加灵活,可以根据不同的数据来动态生成不同的结构,适应不同的需求。
- 便于维护: 相比于直接编写 HTML 结构,使用 JavaScript 数据结构可以使组件的结构更加简洁清晰,便于维护和修改。
- 代码复用: 可以将一些通用的逻辑抽象成函数或组件,提高代码的复用性。
6.使用工具组件支持渲染VNode
这段代码是一个基于 Vue 3 的组件,它接受一个名为 vNode
的 prop,该 prop 可以是字符串或对象,并且是必需的。然后,通过 setup()
函数来设置组件,并返回一个渲染函数,该渲染函数接收 props
对象作为参数,并返回 props.vNode
,即接收到的虚拟节点。
这个组件的作用是将传入的虚拟节点直接渲染到组件的模板中,而不进行额外的处理或包装。这在某些情况下可能很有用,例如当您希望根据不同的条件渲染不同的内容时,可以通过动态地传递虚拟节点来实现。
下面是对代码中各部分的解释:
import { defineComponent } from 'vue'
:从 Vue 中导入defineComponent
函数,用于定义 Vue 组件。const RenderVnode = defineComponent({...})
:使用defineComponent
函数定义了一个名为RenderVnode
的组件,并传入一个包含组件选项的对象作为参数。props
:定义了组件的属性,其中vNode
属性是必需的,且可以是字符串或对象类型。setup(props)
:通过setup
函数设置组件,接收props
对象作为参数。return () => props.vNode
:返回一个渲染函数,该函数接收props
对象作为参数,并直接返回props.vNode
,即传入的虚拟节点。export default RenderVnode
:将RenderVnode
组件导出,以便在其他地方使用。
7.form 组件的自定义性
自定义UI:
- 强调 “form” 组件提供了丰富的自定义UI功能,用户可以根据自己的需求自定义整体样式和渲染多种类型的表单元素,包括 Input、Switch、Select 等。用户还可以自定义提交区域的内容,包括按钮样式、排列等,从而实现了个性化定制。
验证时机:
- 强调 “form” 组件灵活的验证时机设置,表单元素默认在失去焦点时验证,但用户也可以根据需要自定义验证时机。整个表单在点击提交按钮时会进行全部验证,确保数据的完整性和准确性。
验证规则:
- 强调 “form” 组件丰富的验证规则设置,每个 Input 元素可以配置多条验证规则,包括不能为空、需要是字符串、最多多少字符等等。用户还可以自定义规则,例如确保重复密码框输入与密码输入一致等,从而提供了更高程度的数据验证和安全性保障。
通过突出 “form” 组件的自定义UI、验证时机和验证规则等特点,可以有效地吸引用户和开发者的注意力,并展示项目在用户输入处理方面的丰富功能和灵活性。
二、Message一大章作为难点可聊
JSX 和 渲染函数可聊
三、表单作为难点一大章可聊
1.input组件 使用测试TDD的开发方式开发组件
2.form的数据传递和表单验证
3.Select的远程请求部分