1.介绍

2.项目文件结构

安装eslint插件

项目文件结构

  • components

    ​ Button

    ​ Button.vue - 组件

    ​ style.css - 样式

    ​ types.ts - 一些辅助的typescript类型

    ​ Button.test.tsx -测试文件

  • hooks

    ​ useMousePosition.ts

初始化项目

–save-dev 的作用

--save-dev 是 npm install 命令的一个选项,它用于将安装的软件包(dependencies)添加到 devDependencies 中。

具体来说,它的作用是将软件包添加到项目的开发环境依赖项中,而不是生产环境依赖项。

在 Node.js 项目中,通常会有两种类型的依赖项:

  1. 生产环境依赖项(dependencies):这些是项目在生产环境中运行时所需的依赖项,包括实际部署到生产服务器上的代码和运行时所需的工具。比如,一个 Web 服务器框架、数据库驱动程序等都是生产环境依赖项。
  2. 开发环境依赖项(devDependencies):这些是在开发过程中使用的依赖项,例如测试框架、构建工具、代码检查工具等。这些依赖项通常不会在生产环境中使用,只在开发、测试和构建项目时需要。

使用 --save-dev 选项安装依赖项时,npm 会将软件包添加到 package.json 文件中的 devDependencies 部分。这使得其他开发人员可以轻松地安装相同的开发依赖项,并确保项目的开发环境保持一致。

举个例子,假设你正在开发一个 JavaScript 项目,并且需要在开发过程中使用 Jest 测试框架。你可以使用以下命令安装 Jest,并将其添加到 devDependencies 中:

1
npm install jest --save-dev

这将在你的 package.json 文件中添加一个新的开发依赖项条目:

1
2
3
4
5
{
"devDependencies": {
"jest": "^27.0.6"
}
}

这样,其他开发人员在克隆项目后,只需运行 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
2
3
4
5
6
7
8
<script setup>
const props = defineProps({
foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
  • defineProps 和 defineEmits 都是只能在 <script setup> 中使用的编译器宏。他们不需要导入,且会随着 <script setup> 的处理过程一同被编译掉。
  • defineProps 接收与 props 选项相同的值,defineEmits 接收与 emits 选项相同的值。
  • defineProps 和 defineEmits 在选项传入后,会提供恰当的类型推导。

问题:

在定义完按钮类型后在Button组件导入时会有爆红的问题(无法引入):

如果是Vue3.2版本及以下,我们需要安装一个插件Vue Macros来解决这个问题,但这个问题在Vue3.3中已经解决,不需要再安装插件

2.defineProps–为组件的 props 标注类型

可以将 props 的类型移入一个单独的接口中:

1
2
3
4
5
6
7
8
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}

const props = defineProps<Props>()
</script>

这同样适用于 Props 从另一个源文件中导入的情况:

1
2
3
4
5
<script setup lang="ts">
import type { Props } from './foo'

const props = defineProps<Props>()
</script>

当你使用 defineProps 定义组件的 props,并传入一个对象作为参数时,TypeScript 将会推断每个 prop 的类型。同样地,当你使用 defineEmits 定义组件的 emits,并传入一个对象作为参数时,TypeScript 将会推断每个 emit 的类型。

这种类型推导的好处在于,它能够帮助你在编码过程中捕获潜在的类型错误,提高代码的可靠性和可维护性。如果你的 props 或 emits 与组件的模板或其他代码不匹配,TypeScript 将会发出相应的类型错误,提醒你进行修正。

以下是一个简单的示例,演示了如何使用 defineProps 和 defineEmits 并从选项中推断类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { defineComponent, defineProps, defineEmits } from 'vue';

// 定义组件的 props 类型
const Props = {
message: String,
count: {
type: Number,
required: true,
},
};

// 定义组件的 emits 类型
const Emits = {
increment: (count: number) => true,
};

export default defineComponent({
// 从选项中推断 props 和 emits 的类型
props: defineProps<Props>(),
emits: defineEmits<Emits>(),
setup(props, { emit }) {
// props.message 类型为 string | undefined
const message = props.message;

// props.count 类型为 number
const count = props.count;

// emit.increment 的类型为 (count: number) => void
const increment = () => {
emit('increment', count + 1);
};

return {
message,
count,
increment,
};
},
});

在这个示例中,我们首先定义了组件的 props 和 emits 的类型,然后在 defineComponent 中使用 defineProps 和 defineEmits 从选项中推断类型。在 setup 函数中,我们可以直接使用 props 和 emit,并且 TypeScript 能够正确地推断它们的类型。

3.withDefaults–为 props 声明默认值

例子1:

父组件:

1
2
3
<template>
<TsSample :msg='msg' @on-updated='onUpdated' title='title' @on-delete='onDelete'/>
</template>

子组件:

1
2
3
4
<template>
<h1>ts sample</h1>
<p>{{ msg }}</p>
</template>

第一种方式:分离模式(推荐)

1
2
3
4
5
6
7
8
export interface Props{
msg?:(string|number|boolean),
title?:string[]
}
const props = withDefaults(defineProps<Props>(),{
msg:'hello',
title:()=>['one','two']
})

第二种方式:组合模式

1
2
3
4
5
6
withDefaults(
defineProps<{ msg?: (string | number | boolean), title?: string }>(),{
msg:'hello vite',
title:'默认标题'
}
);

4.defineExpose

5.PropType

更严格的类型检查:

  • 使用 String as PropType<string> 可以提供更严格的类型检查。这样可以确保传递给 prop 的值与预期的类型完全匹配,避免意外的类型错误
  • 虽然在简单的情况下两者可能没有太大的区别,但在需要更严格的类型检查或者在 TypeScript 无法准确推断类型时,使用 PropType 函数会更加可靠

6.defineOptions

7.CSS方案_PostCSS

CSS预处理器

选中 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
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ItemProps
name:string number,
title:string,
disabled:boolean
}
//支持v-model
interface CollapseProps{
//当前打开的items,可以多个,比如
ref(['a','c'])
//那么就是a,c item打开
modelvalue:string[]
//是否支持手风琴模式,开启一个,另外一个自动关闭
accordion:boolean
}

2.确定事件

1
2
3
4
interface Emits{
(e:'change',values:string[])
(e:'update:modelvalue',values:string[]
}

3.思路分析

1
2
3
4
5
6
7
8
9
10
11
12
13
//维护一个可变化响应式数组,代表打开的items(使用item的name)
ref(['a'])

//点击特定的item的时候,观察它是否存在于 数组,进行添加或者删除
['a']
添加=>['a','b']
删除=>['b']

//在Item组件内部,判断当前name是否存在与数组中,来判断是否打开或者关闭
//Item a中
['b'].includes(props.title)
//不存在 关闭
//Item b中

难点,怎样将对应的父组件Collapse属性传递给Item,这里是slot实现的,看起来不是很好传递。

答案:使用provide和inject传递信息

要为具名插槽传入内容,我们需要使用一个含v-slot指令的template元素,并将目标插槽的名字传给该指令:

1
2
3
4
5
<BaseLavout>
< template v-slot.header>
<!--header插槽的内容放这里-->
</template>
</BaseLayout>

v-slot有对应的简写#,因此

1
template v-slot:header

可以简写为

1
template #header

其意思就是“将这部分模板片段传入子组件的header插槽中”。

4.BEM样式语法

1
2
3
4
5
6
7
//Block component
.btn {}
//Element that depends upon the block *
.btn__price {}
//Modifier that changes the style of the block *
.btn--orange{}
.btn--big {}

5.InjectionKey

在 Vue3 中使用 TS 时,父组件通过 provide 函数注入的数据类型往往是未知的,而子组件调用 inject 引入数据时也无法确定具体的数据类型是什么,这就产生了可维护性问题,比如某位同事写了下面这段代码时

1
2
import { inject } from "vue"
inject('Colors');

对于 colors 导数的数据类型我们并不知道是什么,它可以是对象 or 数组亦或是字符串,只能顺瓜摸藤找到它的 provide,对于小项目找一下可能不花费什么时间,但对于大型项目来说很明显是不可取的,于是官方提供了 InjectionKey 函数来对传参进行类型约束,确保父子间传递的数据类型是可见、透明的。

InjectionKey 函数的使用也很简单,原理是将 provide 与 inject 的第一个参数即 key 通过声明 symbol 的方式关联起来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 将 InjectionKey 定义的数据类型放到 keys/index.ts 下维护
// keys/index.ts
import {InjectionKey, Ref } from "vue"
// 限制了 provide 导出的数据必须是 ref 且 boolean 类型
export const showPopupKey: InjectionKey<Ref<boolean>> = Symbol()
// 限制了 provide 导出的数据必须是 string
export const titleKey: InjectionKey<string> = Symbol()

// 2. 在 A .vue 文件中调用 provide 导出数据,第一个参数则是我们上面定义好的数据类型,第二个参数是对应数据类型的值
import { provide, InjectionKey, Ref } from "vue"
import { showPopupKey } '@/keys'
const showPopup = ref(false)
// 正确
provide(showPopupKey, showPopup)
// TS 报错: 'Hello' 是字符串,与 showPopupKey 不匹配
provide(showPopupKey, 'Hello')
// 正确
provide(titleKey, 'Hello')

// 3. 在 B.vue 文件中导入数据
import { showPopupKey } from '@/keys'
import { inject } from 'vue'
inject(showPopupKey) // 现在获取到的数据类型是安全的

有了 InjectionKey 函数后就不用再担心 provide&inject 之间的数据类型问题了,我们只需找到 InjectionKey 关联的 key即可

6.Transition

7.Transition JavaScript钩子

7.Record

1
Record<K,T>

构造一个对象类型,Keys 表示对象的属性键 、Type 表示对象的属性值,用于将一种类型属性映射到另一种类型

理解为:将 K 的每一个值都定义为 T 类型

1
2
3
4

type Record<K extends keyof any, T> = {
[P in K]: T;
};

即将K中的每个属性([P in K]),都转为T类型

结合项目的代码

使用 Record<string, (el: HTMLElement) => void> 主要是为了明确指定了对象的结构,即对象的键是字符串类型,而值是一个函数,这个函数接受一个参数 el,类型为 HTMLElement,并且不返回任何值。这样的明确定义可以提高代码的可读性和可维护性。

使用 Record 这样的类型定义有以下几个优点:

  1. 类型约束: Record 提供了对对象结构的明确约束,确保了对象的键和值符合预期的类型。
  2. 静态类型检查: 使用 Record 可以让 TypeScript 在编译时进行静态类型检查,确保代码的类型安全性。
  3. 自文档化: 通过明确指定对象结构,使得代码更易于理解和维护,同时也可以作为文档来阐明代码的预期行为。
  4. 代码提示: 使用 Record 可以使得编辑器对对象的键和值进行更好的代码提示,帮助开发人员编写正确的代码。

(3)Collapse总结

当遇到类似列表结构组件,两种常用的实现组件的组织结构

方案一:传入数组

特点:

1.实现起来相对简单

2.展示复杂类型节点比较麻烦

3.语义化稍差

1
2
3
4
5
6
7
8
9
<Collapse items="items">
</Collapse>
const items =[
{
name:'a',
title:'title a',
content:''
}
]

方案二:语义化展示

特点:

1.语义化更好

2.复杂节点使用slot展示简单

1
2
3
4
5
6
7
8
<Collapse> //父组件
<CollapseItem name="a"title="title a"> //子组件
<div>
<h1>复杂样式</h1>
<p>He11o~</p>
</div>
</CollapseItem>
</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:
      真正的矢量

2.现成的图标库

  • Bootstrap Icons

  • Fontawesome

  • Ionicon

3.安装svg

1
2
3
4
5
6
//安装svg core
npm i --save @fortawesome/fontawesome-svg-core
//安装图标库
npm i --save @fortawesome/free-solid-svg-icons
//安装基于vue3的包装
npm i --save @fortawesome/vue-fontawesome@latest-3

4.导入组件库

1
2
3
4
//导入组件svg core
import { library } from '@fortawesome/fontawesome-svg-core'
//导入组件库
import { fas } from '@fortawesome/free-solid-svg-icons'

5.禁用透传

6.导入lodash-es的omit用作过滤器

做到精益求精,要过滤传值时自己添加的属性

安装lodash-es要安装es稳定版本和声明文件

1
2
3
npm install lodash-es --save

npm install @types/lodash-es --save-dev

omit
_.omit(object, keys)
函数返回一个没有列入key属性的对象。其中,参数object为JSON格式的对象,
keys表示多个需要排除掉的key属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const _ = require('lodash/object');
const originObject = {
A: 1,
B: 2,
C: 3,
D: 4
};
const newObject = _.omit(originObject, 'B', 'C');
console.log(originObject);
console.log(newObject);

{ A: 1, B: 2, C: 3, D: 4 }
{ A: 1, D: 4 }

(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 的一些常见用法:

  1. vi.fn(): 创建一个模拟函数,可以用于跟踪函数的调用情况,判断是否被调用以及被调用的参数等。
1
2
3
4
const myFunction = vi.fn();
myFunction(1, 2);
expect(myFunction).toHaveBeenCalled(); // 断言函数被调用过
expect(myFunction).toHaveBeenCalledWith(1, 2); // 断言函数被以特定参数调用过
  1. vi.spyOn(obj, methodName): 对对象的指定方法进行监视,可以跟踪该方法的调用情况。
1
2
3
4
5
6
const obj = {
method: () => {}
};
const spy = vi.spyOn(obj, 'method');
obj.method();
expect(spy).toHaveBeenCalled(); // 断言方法被调用过
  1. vi.mock('moduleName'): 对指定的模块进行模拟,可以控制模块的行为,例如模拟模块的函数返回值等。
1
2
3
vi.mock('axios');
const mockedAxios = axios as Mocked<typeof axios>;
mockedAxios.get.mockResolvedValue({ data: 'mocked data' });

Vitest的describe用法

describe 是 vitest 测试框架提供的一个函数,用于描述一个测试套件,通常用来组织一系列相关的测试用例。在描述中,可以包含多个测试用例,并且可以进行嵌套描述以更好地组织测试。

下面是 describe 函数的基本用法:

1
describe(description, callback);
  • description: 描述测试套件的字符串,通常是一个简短的描述性文字,用来说明该测试套件包含的是哪些测试用例。
  • callback: 一个回调函数,包含了测试用例的定义和实现。在该回调函数内部,可以使用 test 函数定义多个测试用例。

例如:

1
2
3
4
5
6
7
8
9
describe('Math operations', () => {
test('Addition', () => {
expect(1 + 2).toBe(3);
});

test('Subtraction', () => {
expect(5 - 3).toBe(2);
});
});

在上面的例子中,我们描述了一个名为 “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
2
const result = add(1, 2);
expect(result).toBe(3);

在这个例子中,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
2
3
4
5
6
7
8
9
//模拟一个简单的 Vnode
const vnode = {
type:'div',
props:{
id:'hello'
},
children: [
]
}

虚拟DOM的优点

  • 可以使用一种更方便的方式,供开发者操控UI的状态和结构,不必和真实的DOM节点打交道
  • 更新效率更高,计算需要的最小化操作,并完成更新

创建Vnode

h和createVnode都可以创建vnode,

h是hyperscript的缩写,意思就是”JavaScript that produces HTML(hypertext markup language)”,很多virtualDOM的实现都使用这个函数名称。

还有一个函数称之为createVnode,更形象,两个函数的用法几乎是一样的。

1
2
3
4
5
6
7
8
import { h,createVnode} from 'vue'
const vnode =h(
'div',//type
{id:'foo',class:'bar'},//props
[
/*children */
]
)

测试事件

触发事件

1
await firstHeader.trigger('click')

观测事件是否触发

1
2
3
//方案一jsx渲染组件:
const onchange = vi.fn()
expect(onChange).toHaveBeenCalledwith([])
1
2
3
4
//方案二普通方式mount组件:
const changeEvent = wrapper.emitted('change')
expect(changeEvent[1]).toEqual([['b']]
expect(changeEvent[0]).toEqual([[]]

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//render function
import { h, ref } from 'vue';

export default {
setup(props) {
const count = ref(1);

// 使用 render 函数
return () => h('div', [
h('h1', props.msg),
h('p', count.value)
]);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 JSX 语法
import { ref } from 'vue';

export default {
setup(props) {
const count = ref(1);
return () => (
<div>
<h1>{props.msg}</h1>
<p>{count.value}</p>
</div>
);
}
}


这两种方式都可以用来编写 Vue 3 组件的 render 函数,选择其中一种取决于个人偏好和项目要求。

vitest钩子函数及其他

Vitest 是一个基于 Vite 构建的测试运行器,它提供了一些钩子函数和其他功能,帮助开发人员编写和运行测试代码。

以下是 Vitest 的一些常用钩子函数及其他功能:

  1. beforeEach(): 在每个测试用例运行之前执行的钩子函数。可以在该钩子函数中进行一些公共的初始化工作,如创建测试环境、准备测试数据等。

  2. afterEach(): 在每个测试用例运行之后执行的钩子函数。可以在该钩子函数中进行一些公共的清理工作,如释放资源、清除状态等。

  3. beforeAll(): 在所有测试用例运行之前执行的钩子函数。可以在该钩子函数中进行一些全局的初始化工作,如启动服务器、连接数据库等。

  4. afterAll(): 在所有测试用例运行之后执行的钩子函数。可以在该钩子函数中进行一些全局的清理工作,如关闭服务器、断开数据库连接等。

  5. describe(): 用于定义测试套件的函数,可以将一组相关的测试用例进行分组,并且可以嵌套使用。通常与 beforeEach()、afterEach() 等钩子函数配合使用,以实现更复杂的测试逻辑。

  6. test(): 用于定义单个测试用例的函数,包含待测试的代码和期望的结果。通常在 describe() 函数内部调用,以组织和管理测试用例。

  7. expect(): 用于断言测试结果是否符合预期,可以与各种断言函数一起使用,如 toBe()、toEqual()、toBeTruthy() 等。通常在测试用例内部调用,以验证代码的行为和状态是否正确。

  8. 异步测试支持: Vitest 支持异步测试,可以处理异步操作和异步更新。开发人员可以使用 async/await 关键字等待异步操作完成,并且检查测试结果是否符合预期。

  9. 代码覆盖率支持: Vitest 提供了代码覆盖率的支持,可以生成测试覆盖率报告,帮助开发人员评估测试的质量和完整性,以及找出需要增加测试覆盖的地方。

综上所述,Vitest 提供了丰富的钩子函数和其他功能,使得编写和运行测试代码变得简单、灵活和高效。开发人员可以根据项目需求选择合适的钩子函数和功能,从而实现全面的测试覆盖和高质量的测试代码。

8.Tooltip组件

(1)需求分析

  • 通用组件
  • Tooltip
  • Dropdown
  • Select等等

功能分析

根本功能,两块区域

  • ​ 触发区
  • ​ 展示区

触发方式

  • hover
  • 点击
  • 手动

重点就是触发区发生特定事件的时候,展示区的展示与隐藏

展示方案

1
2
3
4
5
6
7
8
9
10
11
12

//content 字符串
<Tooltip content="this is the tooltip">
<Button size="large">Trigger me</Button>
</Tooltip>
//content 复杂slot
<Tooltip>
<Button size="large">Trigger me</Button>
<template #content>
<h1>He11o!</h1>
</template>
</Tooltip>

属性

1
2
3
4
5
interface TooltipProps{
content?:string;
trigger:'hover'|'click';
manual?:boolean;
}

事件

1
2
interface TooltipEmits
(e:'visible-change',value:boolean):void;

实例

1
2
3
4
interface TooltipInstance{
show:()=>void;
hide:()=>void;
}

动态事件的解决方案

1
2
3
4
5
6
7
8
9
10
11
12
//方案一,使用DOM标准来绑定事件--不推荐
//click
domNode.addEventListener('click',callback)
//hover
domNode.addEventListener('mouseenter',callback)
//要注意事件的清除

//方案二,使用v-on来完成--推荐
//@click = v-on:click 缩写形式
//v-on还可以接受一个object作为参数,对object中的每一项都可以作为对应的事件回调
const events ='click':callback,'mouseenter':callback2
<div v-on="events"/>

(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
2
3
4
5
6

<Dropdown
:menu-options="options"
>
<Button size="large">The Dropdown</Button>
</Dropdown>

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface DropdownProps extends TooltipProps{
menuOptions:Menuoption[]
}
interface MenuOption {
//VNode这个是难点
//选项内容
label:string | VNode;
//唯一标识
key:string | number;
//该选项是否禁用
disabled?:boolean;
//是否需要划分线
divided?:boolean;
}

事件

1
2
3
4
interface DropdownEmits
(e:'visible-change',value:boolean):void;
(e:'select',value:key):void;
}

实例

1
2
3
4
interface DropdownInstance {
show:()=>void;
hide:()=>void;
}

(2)代码编写

1.Render Function

这个简单组件的主要作用是接收一个虚拟节点(vNode)作为属性,并将其渲染到页面上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineComponent } from 'vue'
const RenderVnode = defineComponent({
props: {
vNode: {
type: [String, Object],
required: true
}
},
setup(props) {
return () => props.vNode
}
})

export default RenderVnode

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()}

  • 传递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函数导出实例

10.Message组件

(1)需求分析

功能分析

  • 在特定的行为的时候,弹出一个对应的提示(支持普通文本以及VNode)
  • 提示在一定时间后可以消失
  • 可以手动关闭
  • 可以弹出多个提示
  • 有多种类型( default,primary,danger…)

难点

1.使用函数式的方式来创建组件

1
2
3
createMessage('hello world',props)
//怎么将一个组件 函数式的渲染到一个节点上呢?
//或许可以采用 createApp 函数

2.可以弹出多个提示,并且旧提示可以根据新提示向下移动位置

1
2
3
4
//创建多个实例,应该没有问题,但怎么调整位置是个难题,看起来我们需要动态调整组件的属性
const component1 = createApp(Message)
const component2 = createApp(Message)
//component2 能感知到 component1 的位置,然后动态的进行调整

属性分析

1
2
3
4
5
6
interface MessageProps{
message: string | VNode;
duration: number;
showClose: boolen;
type: 'primary' | ....
}

事件以及实例

由于是由函数的方式创建组件,这里暂且无法明确对应的事件以及实例

1
2
const instance = createMessage('hello world',props)
instance.close()

(2)代码编写

1.将组件Render到DOM节点上

使用 createApp 的弊端

这个方法太重了,它其实返回的是一个应用的实例,而我们需要轻量级的解决方案

隆重推出render函数

1
2
3
4
5
6
//一个vue内部神奇的函数,文档中都没有特别的记录
//它负责将一个 vnode 渲染到 dom节点上
//它是一个很轻量级的解决方案
import { render } from 'vue'

render(vNode,DOM节点) //返回的是void

在本案例的使用:

1
2
3
4
5
6
//`h()` 函数用于创建 vnodes
const vnode = h(MessageConstructor, newProps)
//渲染vnode 到 dom 节点上
render(vnode, container)
//非空断言操作符 appendChild只能element,但这个类型可能为null,所以使用非空断言
document.body.appendChild(container.firstElementChild!)

2.v-show 隐藏节点后清除节点(该节点已经挂载实例)

清除节点

1
2
3
4
5
6
7
8
9
10
11
//渲染null 到 dom 节点上 
render(null, container)
//扩展新属性onDestory
const newProps = {
...props,
id,
zIndex: nextZIndex(),
onDestory: destory
}
//`h()` 函数用于创建 vnodes
const vnode = h(MessageConstructor, newProps)

3.通过使用一个数组来实现获取同一个组件不同实例的内容

这样可以为下一步动态定位多个实例做准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
types.ts
//Message数组
export interface MessageContext {
//唯一标识message
id: string;
vnode: VNode;
vm: ComponentInternalInstance;
props: MessageProps;
destory: () => void;
}

//Omit作用
//要操作的类型MessageProps,要忽略的对应的属性'onDestory' | 'id' | 'zIndex'
export type CreateMessageProps = Omit<MessageProps, 'onDestory' | 'id' | 'zIndex'>

method.ts:
import type { CreateMessageProps, MessageContext } from './types'

//初始id为1
let seed = 1
//初始为空Messaage数组
const instances: MessageContext[] = shallowReactive([])
//创建Message组件函数
export const createMessage = (props: CreateMessageProps) => {
const { nextZIndex } = useZIndex()
//id + 1 操作,用来表示不同的message
const id = `message_${seed++}`
//创建dom节点
const container = document.createElement('div')


//清除节点
const destory = () => {
//删除数组中的instance实例
//通过id找对应的instance实例
const idx = instances.findIndex(instance => instance.id === id)
//没找到就返回
if (idx === -1) return
//找到就删除
instances.splice(idx, 1)
//渲染null 到 dom 容器节点上
render(null, container)
}


//拿到最后一项instance实例
export const getLastInstance = () => {
return instances.at(-1)
}

4.旧提示可以根据新提示向下移动位置

通过获取实例ref=”messageRef” 计算高度量的偏移再返回样式给实例:style=”cssStyle”并且暴露出去 bottomOffset ,更新时要等dom挂载后更新所以要用到await 和 nexttick

1
2
3
4
5
6
7
8
9
10
11
12
13
<div
class="vk-message"
v-show="visible"
:class="{
[`vk-message--${type}`]: type,
'is-close': showClose
}"
role="alert"
ref="messageRef"
:style="cssStyle"
@mouseenter="clearTimer"
@mouseleave="startTimer"
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

const props = withDefaults(defineProps<MessageProps>(), {
type: 'info',
duration: 3000,
//偏移值默认为20px
offset: 20,
transitionName: 'fade-up'
})


//获取message组件的实例
const messageRef = ref<HTMLDivElement>()
// 计算偏移高度
// 这个 div 的高度
const height = ref(0)
// 上一个实例的最下面的坐标数字,第一个是 0
const lastOffset = computed(() => getLastBottomOffset(props.id))
// 这个元素应该使用的 top
const topOffset = computed(() => props.offset + lastOffset.value)
// 这个元素为下一个元素预留的 offset,也就是它最低端 bottom 的 值
const bottomOffset = computed(() => height.value + topOffset.value)

const cssStyle = computed(() => ({
top: topOffset.value + 'px',
zIndex: props.zIndex
}))

function updateHeight() {
height.value = messageRef.value!.getBoundingClientRect().height
}
defineExpose({
bottomOffset,
visible
})

5.JSX怎么在组件外部获取暴露出去的bottomOffset

​ 通过Vnode的component属性上的expose属性就能拿到对应的属性

6.通过VNode拿到组件实例

1
2
3
4
5
const component = vNode.component
//它是一个特殊类型,称之为ComponentInternalInstance
//它代表组件的内部类型
//在组件内部也可以使用一个特殊的函数获取这个实例
const instance = getCurrentInstance()

7.shallowReavtive

文档地址:https://cn.vuejs..org/api/reactivity-advanced.html#shallowreactive

  • 和reactive()不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露。
  • 假如是数组的话,创建一个浅层响应式的空数组,这意味着数组的元素不会被递归地转换成响应式对象。当我们对数组进行一些增删改操作时,Vue会自动检测到这些变化,并更新对应的视图。
  • 性能优化
1
const instances: MessageContext[] = shallowReactive([])

8.手动调用删除

1
2
3
4
5
6
7
8
// 手动调用删除,其实就是手动的调整组件中 visible 的值
// visible 是通过 expose 传出来的
const manualDestroy = () => {
const instance = instances.find(instance => instance.id === id)
if (instance) {
instance.vm.exposed!.visible.value = false
}
}

9.添加zIndex

使用hooks实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//useZindex.ts:
import { computed, ref } from 'vue'

const zIndex = ref(0)
const useZIndex = (initialValue = 2000) => {
const initialZIndex = ref(initialValue)
const currentZIndex = computed(() => zIndex.value + initialZIndex.value)
const nextZIndex = () => {
zIndex.value ++
return currentZIndex.value
}
return {
currentZIndex,
nextZIndex,
initialZIndex
}
}

export default useZIndex
1
2
3
4
5
6
7
//method.ts:

import useZIndex from '../../hooks/useZIndex'

const { nextZIndex } = useZIndex()

zIndex: nextZIndex(),

10.添加键盘关闭(按esc可以关闭message)

使用hooks实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//useEventListener.ts:
import { onMounted, onBeforeUnmount, isRef, watch, unref } from 'vue'
import type { Ref } from 'vue'
export default function useEventListener(
target: Ref<EventTarget | null> | EventTarget,
event: string,
handler: (e: Event) => any
) {
if (isRef(target)) {
watch(target, (value, oldValue) => {
oldValue?.removeEventListener(event, handler)
value?.addEventListener(event, handler)
})
} else {
onMounted(() => {
target.addEventListener(event, handler)
})
}

onBeforeUnmount(() => {
unref(target)?.removeEventListener(event, handler)
})
}

//Message.vue:
import useEventListener from '../../hooks/useEventListener'

function keydown(e: Event) {
const event = e as KeyboardEvent
if (event.code === 'Escape') {
visible.value = false
}
}
useEventListener(document, 'keydown', keydown)

11.hover到Message上面的时候不会自动关闭

基础的组件绑定事件操作实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Message.vue:
<div
class="vk-message"
v-show="visible"
:class="{
[`vk-message--${type}`]: type,
'is-close': showClose
}"
role="alert"
ref="messageRef"
:style="cssStyle"
@mouseenter="clearTimer"
@mouseleave="startTimer"
> </div>


1
2
3
4
5
6
7
8
9
10
11
12
//Message.vue:
function startTimer() {
if (props.duration === 0) return
timer = setTimeout(() => {
visible.value = false
}, props.duration)
}


function clearTimer() {
clearTimeout(timer)
}

12.添加动画以及样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<Transition 
:name="transitionName"
@after-leave="destroyComponent"
@enter="updateHeight"
>
</Transition>


const props = withDefaults(defineProps<MessageProps>(), {
type: 'info',
duration: 3000,
//偏移值默认为20px
offset: 20,
transitionName: 'fade-up'
})


function destroyComponent () {
props.onDestory()
}
function updateHeight() {
height.value = messageRef.value!.getBoundingClientRect().height
}


transition: top var(--vk-transition-duration), opacity var(--vk-transition-duration), transform var(--vk-transition-duration);
.vk-message__content {
color: var(--vk-message-text-color);
overflow-wrap: anywhere;
}
&.is-close .vk-message__content {
padding-right: 30px;
}
.vk-message__close {
display: flex;
align-items: center;
}
.vk-message__close svg {
cursor: pointer;
}
}
@each $val in info,success,warning,danger {
.vk-message--$(val) {
--vk-message-bg-color: var(--vk-color-$(val)-light-9);
--vk-message-border-color: var(--vk-color-$(val)-light-8);
--vk-message-text-color: var(--vk-color-$(val));
.vk-message__close {
--vk-icon-color: var(--vk-color-$(val));
}
}
}
.vk-message.fade-up-enter-from,
.vk-message.fade-up-leave-to {
opacity: 0;
transform: translate(-50%, -100%);
}

(3)Message总结

使用函数式的方式创建组件

1.使用render函数挂载在特定节点

1
2
3
4
const container =  document.createElement('div')
const vnode = h(MessageConstructor,props)
render(vnode,container)
document.body.appendchild(container.firstElementchild!)

2.销毁组件实例

1
render(null,container)

3.组件动态构造并且传入属性

1
2
3
4
5
const newProps = {
...props,
onDestory:destory
}
const vnode = h(MessageConstructor,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
2
3
4
5
6
docs
-.vitepress
--config.ts
-api-examples.md
-markdown-examples.md
package.json

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
:root {
--vp-c-brand: var(--vk-color-primary);
--vp-c-brand-light: var(--vk-color-primary-light-3);
--vp-c-brand-lighter: var(--vk-color-primary-light-5);
--vp-c-brand-dark: var(--vk-color-primary-dark-2);
--vp-c-brand-darker: #265F99;

--component-preview-bg: var(--vp-code-block-bg);
--component-preview-soft: var(--vp-code-block-bg-light);
}

body {
color: var(--vp-c-text-1);
}
h1, h2, h3, h4, h5, h6, p {
color: var(--vp-c-text-1);
}

10.为生产环境打包并且部署

文档:https://vitepress.dev/guide/deploy

1
2
3
4
5
6
7
8
9
10
11
# 打包
npm run docs:build
# 打包生成的文件位于:
# docs/.vitepress/dist
# 本地测试
# 本地启动一个静态文件服务器用于 docs/.vitepress/dist
# 可以测试一下最终站点是否OK
npm run docs:preview
# 部署
# 拷贝dist文件中的文件到远程服务器
# 或者使用第三方服务 Github Pages/Gitee Pages 等等

12.Input组件

(1) 需求分析

  • 支持Input/Textarea
  • 支持不同大小
  • 支持 v-model
  • 支持一键清空(有值的时候显示一个按钮,点击清空)
  • 支持切换是否密码显示(有值的时候显示一个按钮,点击切换密码可见/不可见)
  • 支持自定义前缀/后缀slot((prefix/suffix),一般用于图标
  • 支持复合型输入框自定义前置或者后置(prepend/append),一般用于说明和按钮
  • 一些原生属性的支持

属性

1
2
3
4
5
6
7
interface InputProps {
type?:'text'|'textarea'|'password';
size?:'large'|'small';
clearable?:boolean;
showPassword?:boolean;
disabled?:boolean;
}

事件

1
2
3
4
5
6
7
interface InputEmits {
(e:'change',value:string):void;
(e:'inputl',value:string):void;
(e:'focus'):void;
(e:'blur'):void;
(e:'clear'):void;
}

Slots

1
prepend,append,prefix,suffix

Expose

1
2
3
export interface InputInstance {
ref:HTMLInputElement | HTMLTextAreaElement;
}

(2) 代码编写

1.编写测试用例来验证静态结构是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
describe('Input', () => {
it('基本展示', () => {
// 针对动态 class,查看 classes 是否正确
// 针对 v-if 是否渲染正确的标签以及内容
// 针对 slots,是否渲染对应的 slots 内容
const wrapper = mount(Input, {
props: {
size: 'small',
type: 'text',
modelValue: ''
},
slots: {
prepend: 'prepend',
prefix: 'prefix'
}
})
console.log(wrapper.html())
// classes
expect(wrapper.classes()).toContain('vk-input--small')
expect(wrapper.classes()).toContain('is-prepend')
// should render input
expect(wrapper.find('input').exists()).toBeTruthy()
expect(wrapper.get('input').attributes('type')).toBe('text')
// slots
expect(wrapper.find('.vk-input__prepend').exists()).toBeTruthy()
expect(wrapper.get('.vk-input__prepend').text()).toBe('prepend')
expect(wrapper.find('.vk-input__prefix').exists()).toBeTruthy()
expect(wrapper.get('.vk-input__prefix').text()).toBe('prefix')

// textarea
const wrapper2 = mount(Input, {
props: {
type: 'textarea',
modelValue: ''
}
})
expect(wrapper2.find('textarea').exists()).toBeTruthy()
})

2.TDD的开发方式

TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法,它强调在编写代码之前先编写测
试用例,然后通过不断地编写和运行测试用例来驱动代码的开发。

  • 1.编写测试用例
  • 2.运行测试用例
  • 3.编写代码:根据测试用例的要求,编写代码,实现功能。
  • 4.运行测试用例
  • 5.重构代码(可选)
  • 6.重复上述步骤

3.支持 v-model 的测试用例和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
it('支持 v-model', async () => {
const wrapper:VueWrapper = mount(Input, {
props: {
modelValue: 'test',
'onUpdate:modelValue': (e: any) => wrapper.setProps({ modelValue: e }),
type: 'text'
}
})
// 初始值
const input = wrapper.get('input')
expect(input.element.value).toBe('test')
// 更新值
// 注意 setValue 是组合事件会触发 input 以及 change
await input.setValue('update')
//TypeScript 可能无法正确地推断出返回的对象的类型
//手动指定返回对象的类型
expect((wrapper.props() as { modelValue:string }).modelValue).toBe('update')
expect(input.element.value).toBe('update')

console.log('the events', wrapper.emitted())
expect(wrapper.emitted()).toHaveProperty('input')
expect(wrapper.emitted()).toHaveProperty('change')
// [ [ 'update' ], ...更多事件 ]
const inputEvent = wrapper.emitted('input')
const changeEvent = wrapper.emitted('change')
expect(inputEvent![0]).toEqual(['update'])
expect(changeEvent![0]).toEqual(['update'])
// v-model 的异步更新
await wrapper.setProps({ modelValue: 'prop update' })
expect(input.element.value).toBe('prop update')
})
  1. props 和 emits 的定义:在 <script setup> 部分使用 defineProps 和 defineEmits 定义了组件的 props 和 emits,分别是 InputProps 和 InputEmits。其中 InputProps 定义了输入框组件可能接收的属性,而 InputEmits 定义了组件可能触发的事件。
  2. 内部数据绑定:使用 ref 创建了一个名为 innerValue 的响应式引用,用于存储输入框的值。这个值会与 props.modelValue 进行双向绑定,从而实现了 v-model 的功能。
  3. 输入框的事件处理:在输入框的 @input 事件中,调用了 handleInput 方法,将输入框的值更新到 innerValue 中,并触发了 update:modelValue 和 input 事件。在 handleInput 方法中,使用了 emits 函数来触发这两个事件。
  4. 对外暴露输入框的引用:使用 defineExpose 将输入框的引用暴露给外部组件,这样外部组件就可以直接访问到输入框的实例,例如调用输入框的方法或属性。

4.支持按钮清空当前文本的测试用例和实现

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
it('支持点击清空字符串', async () => {
const wrapper = mount(Input, {
props: {
modelValue: 'test',
clearable: true,
type: 'text'
},
global: {
stubs: ['Icon']
}
})
// 不出现对应的 Icon 区域
expect(wrapper.find('.vk-input__clear').exists()).toBeFalsy()
const input = wrapper.get('input')
await input.trigger('focus')
expect(wrapper.emitted()).toHaveProperty('focus')
// 出现 Icon 区域
expect(wrapper.find('.vk-input__clear').exists()).toBeTruthy()
// 点击值变为空并且消失
await wrapper.get('.vk-input__clear').trigger('click')
expect(input.element.value).toBe('')
// 点击值变为空并且消失,特别注意这里不仅仅会触发 clear 事件,对应的 input 以及 change 应该都会被触发,因为对应的值发生了变化
expect(wrapper.emitted()).toHaveProperty('clear')
expect(wrapper.emitted()).toHaveProperty('input')
expect(wrapper.emitted()).toHaveProperty('change')
const inputEvent = wrapper.emitted('input')
const changeEvent = wrapper.emitted('change')
expect(inputEvent![0]).toEqual([''])
expect(changeEvent![0]).toEqual([''])

await input.trigger('blur')
expect(wrapper.emitted()).toHaveProperty('blur')
})

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const showClear = computed(() => 
props.clearable &&
!props.disabled &&
!!innerValue.value &&
isFocus.value
)


const handleFocus = (event: FocusEvent) => {
isFocus.value = true
emits('focus', event)
}
const handleBlur = (event: FocusEvent) => {
console.log('blur triggered')
isFocus.value = false
emits('blur', event)
runValidation('blur')
}
const clear = () => {
console.log('clear triggered')
innerValue.value = ''
emits('update:modelValue', '')
emits('clear')
emits('input', '')
emits('change', '')
}

清空按钮的显示和隐藏通过 showClear 计算属性进行控制,只有在输入框有值且获得焦点时才显示。点击清空按钮时,会调用 clear 方法,将 innerValue 的值设置为空字符串,并通过 emits 函数触发 update:modelValue、clear、input 和 change 事件,通知外部组件输入框的值发生了变化。

5.支持密码切换的测试用例和实现

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
it('支持切换密码显示', async () => {
const wrapper = mount(Input, {
props: {
modelValue: '',
showPassword: true,
type: 'text'
},
global: {
stubs: ['Icon']
}
})
// 不出现对应的 Icon 区域, 因为当前值为空
expect(wrapper.find('.vk-input__password').exists()).toBeFalsy()
const input = wrapper.get('input')
expect(input.element.type).toBe('password')
// 出现 Icon 区域,并且 Icon 为特点的图标
await input.setValue('123')
const eyeIcon = wrapper.find('.vk-input__password')
expect(eyeIcon.exists()).toBeTruthy()
expect(eyeIcon.attributes('icon')).toBe('eye-slash')
// 点击值变会切换input 类型,并且图标的 Icon 会切换
await eyeIcon.trigger('click')
expect(input.element.type).toBe('text')
expect(wrapper.find('.vk-input__password').attributes('icon')).toBe('eye')
})
})

实现:

1
2
3
4
5
6
7
8
9
10
11
12
<Icon 
icon="eye"
v-if="showPasswordArea && passwordVisible"
class="vk-input__password"
@click="togglePasswordVisible"
/>
<Icon
icon="eye-slash"
v-if="showPasswordArea && !passwordVisible"
class="vk-input__password"
@click="togglePasswordVisible"
/>
1
2
3
4
5
6
7
8
9
10
11
const passwordVisible = ref(false)

const showPasswordArea = computed(() =>
props.showPassword &&
!props.disabled &&
!!innerValue.value
)
const togglePasswordVisible = () => {
passwordVisible.value = !passwordVisible.value
}
const NOOP = () => {}
  1. 密码切换功能的展示:在模板中,通过 v-if 指令判断是否显示密码切换按钮。当满足条件时,分别显示“显示密码”和“隐藏密码”的图标按钮。
  2. 点击图标触发切换密码可见性:使用 @click 事件监听器,当点击密码切换按钮时,调用 togglePasswordVisible 方法切换密码可见性。在该方法中,通过改变 passwordVisible 的值来控制密码是否可见。
  3. togglePasswordVisible 方法:该方法用于切换密码的可见性。如果密码是可见的,则将 passwordVisible 的值设置为 false,否则设置为 true,以实现密码的显示和隐藏。
  4. 使用 computed 计算属性控制密码切换按钮的显示:在模板中使用 v-if 条件渲染判断密码切换按钮是否显示。其中,showPasswordArea 计算属性控制密码切换按钮的显示逻辑,只有在组件启用密码显示功能、未禁用且输入框有值时才显示密码切换按钮。

6.支持事件

  1. 渲染输入框或文本域以及相关的附加元素(如前缀、后缀、清除按钮等)。

  2. 监听用户输入事件,更新输入框的值,并通过 v-model 传递给父组件。

  3. 监听输入框的 focus 和 blur 事件,通过 emits 函数触发相应的自定义事件,并执行验证逻辑。

  4. 提供清除输入框内容的功能,并通过 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
2
3
4
5
6
7
8
9
10
11
interface SwtichProps {
modelValue:boolean;
disabled?:boolean;
activeText?:string;
inactiveText?:string;
name?:string;
id?:string;
size?:'small'|'large';
}
//有可能可以扩展的点
//modelValue的类型,不仅仅是boolean,还可以支持特更多的基本类型,比如string以及number

事件

1
2
3
interface SwitchEvents {
(e:'change',value:boolean):void;
}

(2) 代码编写

1.组件样式设计

2.样式编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
.vk-switch {
--vk-switch-on-color: var(--vk-color-primary);
--vk-switch-off-color: var(--vk-border-color);
--vk-switch-on-border-color: var(--vk-color-primary);
--vk-switch-off-border-color: var(--vk-border-color);
}

.vk-switch {
display: inline-flex;
align-items: center;
font-size: 14px;
line-height: 20px;
height: 32px;
.vk-swtich__input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
&:focus-visible {
& ~ .vk-switch__core {
outline: 2px solid var(--vk-switch-on-color);
outline-offset: 1px;
}
}
}
&.is-disabled {
opacity: .6;
.vk-switch__core {
cursor: not-allowed;
}
}
&.is-checked {
.vk-switch__core {
border-color:var(--vk-switch-on-border-color);
background-color: var(--vk-switch-on-color);
.vk-switch__core-action {
left: calc(100% - 17px);
}
.vk-switch__core-inner {
padding: 0 18px 0 4px;
}
}
}
}
.vk-switch--large {
font-size: 14px;
line-height: 24px;
height: 40px;
.vk-switch__core {
min-width: 50px;
height: 24px;
border-radius: 12px;
.vk-switch__core-action {
width: 20px;
height: 20px;
}
}
&.is-checked {
.vk-switch__core .vk-switch__core-action {
left: calc(100% - 21px);
color: var(--vk-switch-on-color);
}
}
}
.vk-switch--small {
font-size: 12px;
line-height: 16px;
height: 24px;
.vk-switch__core {
min-width: 30px;
height: 16px;
border-radius: 8px;
.vk-switch__core-action {
width: 12px;
height: 12px;
}
}
&.is-checked {
.vk-switch__core .vk-switch-core-action {
left: calc(100% - 13px);
color: var(--vk-switch-on-color);
}
}
}
.vk-switch__core {
display: inline-flex;
align-items: center;
position: relative;
height: 20px;
min-width: 40px;
border: 1px solid var(--vk-switch-off-border-color);
outline: none;
border-radius: 10px;
box-sizing: border-box;
background: var(--vk-switch-off-color);
cursor: pointer;
transition: border-color var(--vk-transition-duration),background-color var(--vk-transition-duration);
.vk-switch__core-action {
position: absolute;
left: 1px;
border-radius: var(--vk-border-radius-circle);
width: 16px;
height: 16px;
background-color: var(--vk-color-white);
transition: all var(--vk-transition-duration);
}
.vk-switch__core-inner {
width: 100%;
transition: all var(--vk-transition-duration);
height: 16px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
padding: 0 4px 0 18px;
.vk-switch__core-inner-text {
font-size: 12px;
color: var(--vk-color-white);
user-select: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

3.支持v-model

1
2
3
4
5
6
7
const switchValue = () => {
if (props.disabled) return
const newValue = checked.value ? props.inactiveValue : props.activeValue
innerValue.value = newValue
emits('update:modelValue', newValue)
emits('change', newValue)
}

4.支持自定义value类型

通过以下步骤实现:

  1. props 的定义:在 props 中定义了 activeValue 和 inactiveValue,它们分别表示开关的激活状态和非激活状态下的值。这使得开关可以接受任意类型的值作为其状态。
  2. computed 的使用:通过计算属性 checked 来确定当前开关是否处于激活状态。在这个计算属性中,使用了 innerValue 和 activeValue 来判断当前值是否等于激活状态的值。
  3. switchValue 函数的实现:在 switchValue 函数中,根据当前开关的状态,通过判断 checked 的值来确定下一个状态应该是什么值。这个逻辑不再仅限于布尔类型的切换,而是根据 activeValue 和 inactiveValue 的定义来切换值。
  4. 事件的触发:在 switchValue 函数中,除了触发 update:modelValue 和 change 事件外,还将下一个值作为参数传递给这些事件。这样做可以保证外部的 v-model 在更新时能够得到正确的值。

5.支持文字描述

1
2
3
4
<!-- 如果activeText或inactiveText存在,则显示对应的开关文字 -->
<span v-if="activeText || inactiveText" class="vk-switch__core-inner-text">
{{checked ? activeText : inactiveText}}
</span>

6.支持无障碍

1
2
3
4
5
6
7
8
9
10
11
<input 
class="vk-switch__input"
type="checkbox"
role="switch"
ref="input"
:name="name"
:disabled="disabled"
:aria-checked="checked"
@keydown.enter="switchValue"
/>

  1. 提供键盘操作支持:在 <input> 元素上监听键盘事件,例如 keydown.enter,以便用户可以通过键盘切换开关状态。
  2. 确保焦点可见性:在用户与开关交互时,确保开关的焦点是可见的,并且具有适当的焦点样式。这可以通过 CSS 或者 JavaScript 来实现。

(3) Switch 总结

Switch组件,分析出了和它很相似的应该是checkbox,所以它是个内部包裹着checkbox,用DOM模拟对应的外貌的组件。
新的知识点:

  • 学习写复制CSS样式的方式
  • 表单组件设计要特别注意和原生表单元素的配合,实现比较完美的可访问性。

14.Select 组件

(1) 需求分析

类似原生的Select,不过有着更强大的功能。

最基本功能:

  • ​ 点击展开下拉选项菜单
  • ​ 点击菜单中的某一项,下拉菜单关闭
  • ​ Select获取选中状态,并且填充对应的选项。

组件本质:进阶版本的Dropdown,Input组件和Tooltip组件的组合

高级功能:

  • 可清空选项:当Hovr的时候,在组件右侧显示一个可清空的按钮,点击以后清空选中的值。
  • 自定义模版:可以自定义,下拉菜单的选项的格式。
  • 可筛选选项: Input允许输入,输入后可以根据输入字符自动过滤下拉菜单的选项。
  • 支持远程搜索(难点):类似自动联想,可以根据输入的字符发送请求,渲染返回的内容作为选项列表
  • 扩展支持:比如键盘移动

结构分析

1
2
3
4
5
6
7
8
9
10
11
12
<Select
placeholder="select"
:options="options"
v-model="selectvalue"
/>
const options2 = [
label:'hello',value:1 }
label:'xyz',value:2 }
label:'testing',value:3 )
label:'check',value:4}
]

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface SelectProps {
//v-model
modelValue:string;
//选项
options:Selectoption;
//一些基本表单属性
placeholder:string;
disabled:boolean;
//可清空
clearable:boolean;
//自定义菜单
renderLabel:(option:SelectOption)=>VNode;
//可筛选
filterable:boolean;
//远程搜索
remoteMethod:(value:any) => Promise<SelectOption[]>


//选项属性
export interface Selectoption {
label:string;
value:string | number;
disabled?:boolean;
}

事件

1
2
3
4
5
interface SelectEmits{
(e:'change',value:string):void;
(e:'update:modelvalue',value:string):void;
(e:'clear'):void;
}

(2) 代码编写

1.基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>
<div
class="vk-select"
:class="{'is-disabled': disabled }"
@click="toggleDropdown"
@mouseenter="states.mouseHover = true"
@mouseleave="states.mouseHover = false"
>
<Tooltip
placement="bottom-start"
ref="tooltipRef"
:popperOptions="popperOptions"
@click-outside="controlDropdown(false)"
manual
>
<Input
v-model="states.inputValue"
:disabled="disabled"
:placeholder="filteredPlaceholder"
ref="inputRef"
:readonly="!filterable || !isDropdownShow"
@input="debouceOnFilter"
@keydown="handleKeydown"
>
<template #suffix>
<Icon
icon="circle-xmark"
v-if="showClearIcon"
class="vk-input__clear"
@mousedown.prevent="NOOP"
@click.stop="onClear"
/>

<Icon
v-else
icon="angle-down"
class="header-angle"
:class="{ 'is-active': isDropdownShow }"
/>
</template>
</Input>
<template #content>
<div class="vk-select__loading" v-if="states.loading"><Icon icon="spinner" spin/></div>
<div class="vk-select__nodata" v-else-if="filterable && filteredOptions.length === 0">no matching data</div>
<ul class="vk-select__menu" v-else>
<template v-for="(item, index) in filteredOptions" :key="index">
<li
class="vk-select__menu-item"
:class="{
'is-disabled': item.disabled,
'is-selected': states.selectedOption?.value === item.value ,
'is-highlighted': states.highlightIndex === index
}"
:id="`select-item-${item.value}`"
@click.stop="itemSelect(item)"
>
<RenderVnode :vNode="renderLabel ? renderLabel(item) : item.label"/>
</li>
</template>
</ul>
</template>
</Tooltip>
</div>
</template>

2.选中选项功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const findOption = (value: string) => {
const option = props.options.find(option => option.value === value)
return option ? option : null
}

const initialOption = findOption(props.modelValue)

const itemSelect = (e: SelectOption) => {
if (e.disabled) return
states.inputValue = e.label
states.selectedOption = e
emits('change', e.value)
emits('update:modelValue', e.value)
controlDropdown(false)
inputRef.value.ref.focus()
}

3.初步样式编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const popperOptions: any = {
modifiers: [
{
name: 'offset',
options: {
offset: [0, 9],
},
},
{
name: "sameWidth",
enabled: true,
fn: ({ state }: { state: any }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
phase: "beforeWrite",
requires: ["computeStyles"],
}
],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
.vk-select {
--vk-select-item-hover-bg-color: var(--vk-fill-color-light);
--vk-select-item-font-size: var(--vk-font-size-base);
--vk-select-item-font-color: var(--vk-text-color-regular);
--vk-select-item-selected-font-color: var(--vk-color-primary);
--vk-select-item-disabled-font-color: var(--vk-text-color-placeholder);
--vk-select-input-focus-border-color: var(--vk-color-primary);
}

.vk-select {
display: inline-block;
vertical-align: middle;
line-height: 32px;
.vk-tooltip .vk-tooltip__popper {
padding: 0;
}
.vk-input.is-focus .vk-input__wrapper {
box-shadow: 0 0 0 1px var(--vk-select-input-focus-border-color) inset!important
}
.vk-input {
.header-angle {
transition: transform var(--vk-transition-duration);
&.is-active {
transform: rotate(180deg);
}
}
}

.vk-input__inner {
cursor: pointer;
}
.vk-select__nodata, .vk-select__loading {
padding: 10px 0;
margin: 0;
text-align: center;
color: var(--vk-text-color-secondary);
font-size: var(--vk-select-font-size);
}
.vk-select__menu {
list-style: none;
margin: 6px 0;
padding: 0;
box-sizing: border-box;
}
.vk-select__menu-item {
margin: 0;
font-size: var(--vk-select-item-font-size);
padding: 0 32px 0 20px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--vk-select-item-font-color);
height: 34px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
&:hover {
background-color: var(--vk-select-item-hover-bg-color);
}
&.is-selected {
color: var(--vk-select-item-selected-font-color);
font-weight: 700;
}
&.is-highlighted {
background-color: var(--vk-select-item-hover-bg-color);
}
&.is-disabled {
color: var(--vk-select-item-disabled-font-color);
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}

}
}

4.添加箭头图标

1
2
3
4
5
6
7
8
<template #suffix>
<Icon
v-else
icon="angle-down"
class="header-angle"
:class="{ 'is-active': isDropdownShow }"
/>
</template>
1
2
3
4
5
6
7
8
.vk-input {
.header-angle {
transition: transform var(--vk-transition-duration);
&.is-active {
transform: rotate(180deg);
}
}
}

5.支持清空

可清空选项:当Hover的时候,在组件右侧显示一个可清空的按钮,点击以后清空选中的值

两种思路:

  • 完全复用input组件的clear功能
  • 不复用,重新写

这里选择第二个思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//模板
<Icon
icon="circle-xmark"
v-if="showClearIcon"
class="vk-input__clear"
@mousedown.prevent="NOOP"
@click.stop="onClear"
/>

<Icon
v-else
icon="angle-down"
class="header-angle"
:class="{ 'is-active': isDropdownShow }"
/>


//实现

const showClearIcon = computed(() => {
// * hover 上去
// * props.clearable 为 true
// 必须要有选择过选项
// Input 的值不能为空
return props.clearable
&& states.mouseHover
&& states.selectedOption
&& states.inputValue.trim() !== ''
})
const onClear = () => {
states.selectedOption = null
states.inputValue = ''
emits('clear')
emits('change', '')
emits('update:modelValue', '')
}

const NOOP = () => {}

6.支持自定义模板

可以自定义,下拉菜单的选项的格式。

■ 使用函数 (e:SelectOption) => VNode

1
export type RenderLabelFunc = (option: SelectOption) => VNode;
1
2
3
4
5
6
7
8
9
10
11
12
export interface SelectProps {
// v-model
modelValue: string;
// 选项
options?: SelectOption[];
// 一些基本表单属性
placeholder: string;
disabled: boolean;
clearable?: boolean;
//自定义模板属性renderLabel 对应函数 RenderLabelFunc
renderLabel?: RenderLabelFunc;
}

使用之前定义过的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
2
3
4
5
6
7
8
9
10
11
12
13
14
export interface SelectProps {
// v-model
modelValue: string;
// 选项
options?: SelectOption[];
// 一些基本表单属性
placeholder: string;
disabled: boolean;
clearable?: boolean;
renderLabel?: RenderLabelFunc;
//筛选功能属性 和 对应的函数
filterable?: boolean;
filterMethod?: CustomFilterFunc;
}

基本实现

1
2
3
4
5
const filteredOptions = ref(props.options)
watch(() => props.options, (newOptions) => {
filteredOptions.value = newOptions
})
//当传入的属性是外部传来时,要watch属性变化
1
2
3
4
5
6
7
8
9
//生成新的选项
const generateFilterOptions = async (searchValue: string) => {
if (!props.filterable) return
if (props.filterMethod && isFunction(props.filterMethod)) {
filteredOptions.value = props.filterMethod(searchValue)
} else {
filteredOptions.value = props.options.filter(option => option.label.includes(searchValue))
}
}
1
2
3
4
//包装函数 generateFilterOptions
const onFilter = () => {
generateFilterOptions(states.inputValue)
}

筛选优化

清空Input优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const controlDropdown = (show: boolean) => {
if (show) {
// filter 模式
// 优化清空input
// 之前选择过对应的值
if (props.filterable && states.selectedOption) {
states.inputValue = ''
}
// 进行一次默认选项的生成
if (props.filterable) {
generateFilterOptions(states.inputValue)
}
tooltipRef.value.show()
} else {
tooltipRef.value.hide()
// placeholder优化 blur的部分
// blur 时候将之前的值回灌到 input 中
if (props.filterable) {
states.inputValue = states.selectedOption ? states.selectedOption.label : ''
}
states.highlightIndex = -1
}
isDropdownShow.value = show
emits('visible-change', show)
}

placeholder优化:

1
2
3
4
5

const filteredPlaceholder = computed(() => {
return (props.filterable && states.selectedOption && isDropdownShow.value)
? states.selectedOption.label : props.placeholder
})

8.支持远程搜索

类似自动联想,可以根据输入的字符发送请求,渲染返回的内容作为选项列表,

需求:

  • 每输入一个值,就发送特定请求,返回对应的选项
  • 显示状态(正在读取/没有数据等提示)

属性:

  • 开启remote功能
  • 自定义remote处理方式(value:string)=>Promise<SelectOption[]>

思路:

  • 在Input输入的过程中,根据用户传入的remote处理方式,发起请求并且渲染结果

属性添加

1
export type CustomFilterRemoteFunc = (value: string) => Promise<SelectOption[]>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export interface SelectProps {
// v-model
modelValue: string;
// 选项
options?: SelectOption[];
// 一些基本表单属性
placeholder: string;
disabled: boolean;
clearable?: boolean;
renderLabel?: RenderLabelFunc;
filterable?: boolean;
filterMethod?: CustomFilterFunc;
// 远程搜索功能属性 和 对应的函数
remote?: boolean;
remoteMethod?: CustomFilterRemoteFunc;
}

当设置一个数组的默认初始值的时候,要使用下面的方式(知识点)

1
2
3
const props = withDefaults(defineProps<SelectProps>(), {
options: () => []
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 如果props中没有filterable属性,直接返回
const generateFilterOptions = async (searchValue: string) => {
if (!props.filterable) return
// 如果props中有filterMethod属性,并且是函数,则调用filterMethod方法
if (props.filterMethod && isFunction(props.filterMethod)) {
filteredOptions.value = props.filterMethod(searchValue)
// 如果props中有remote属性,并且是函数,则调用remoteMethod方法
} else if (props.remote && props.remoteMethod && isFunction(props.remoteMethod)) {
//表示正在进行对应的加载
states.loading = true
//处理异步
try {
// 调用remoteMethod方法,并将结果赋值给filteredOptions.value
filteredOptions.value = await props.remoteMethod(searchValue)
} catch (e) {
console.error(e)
//清空,代表有问题
filteredOptions.value = []
} finally {
//加载属性消失
states.loading = false
}
//如果请求成功,则将结果赋值给 filteredOptions,加载状态设置为 false。
//如果请求失败,则打印错误信息,将 filteredOptions 设置为空数组,加载状态设置为 false。
// 否则,调用options的filter方法,将searchValue包含在label中的option过滤出来
} else {
filteredOptions.value = props.options.filter(option => option.label.includes(searchValue))
}
states.highlightIndex = -1

}

9.远程请求添加防抖部分

1
2
3
4
5
6
7
8
9
<Input 
v-model="states.inputValue"
:disabled="disabled"
:placeholder="filteredPlaceholder"
ref="inputRef"
:readonly="!filterable || !isDropdownShow"
@input="debouceOnFilter"
@keydown="handleKeydown"
>

debouce实现:

1
2
3
4
5
const debouceOnFilter = debounce(() => {
onFilter()
}, timeout.value)

const timeout = computed(() => props.remote ? 300 : 0)

10.支持键盘操作

需求:

  • 在input focus的状态下,按下Enter打开下拉菜单/再次按下关闭菜单
  • 按ESC关闭菜单
  • 按上下键移动菜单选项,高亮显示当前移动到的选项
  • 按下Enter,选中特定的选项

思路:

绑定事件:

1
2
3
4
5
6
7
8
9
10
 <Input 
v-model="states.inputValue"
:disabled="disabled"
:placeholder="filteredPlaceholder"
ref="inputRef"
:readonly="!filterable || !isDropdownShow"
@input="debouceOnFilter"
@keydown="handleKeydown"
>
</Input>

对应的事件处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
if (!isDropdownShow.value) {
//打开菜单
controlDropdown(true)
} else {
//高光移动到非默认值并且值存在
if (states.highlightIndex > -1 && filteredOptions.value[states.highlightIndex]) {
itemSelect(filteredOptions.value[states.highlightIndex])
} else {
//关闭菜单
controlDropdown(false)
}
}
break
case 'Escape':
if (isDropdownShow.value) {
controlDropdown(false)
}
break
case 'ArrowUp':
e.preventDefault()
//初始值states.highlightIndex = -1
//判断菜单有值
if (filteredOptions.value.length > 0) {
//高亮到最后一项 和 循环操作对应states.highlightIndex===0
if (states.highlightIndex === -1 || states.highlightIndex === 0) {
states.highlightIndex = filteredOptions.value.length - 1
} else {
// 向上移动
states.highlightIndex--
}
}
break
case 'ArrowDown':
//阻止默认行为 页面的滚动条移动
e.preventDefault()
// states.highlightIndex = -1
//判断菜单有值
if (filteredOptions.value.length > 0) {
//高亮到最后一项 和 循环操作对应states.highlightIndex===0
if (states.highlightIndex === -1 || states.highlightIndex === (filteredOptions.value.length - 1)) {
states.highlightIndex = 0
} else {
// 向下移动
states.highlightIndex++
}
}
break
default:
break;
}
}

完善部分:

1
2
3
4
5
6
7
8
export interface SelectStates {
inputValue: string;
selectedOption: null | SelectOption;
mouseHover: boolean;
loading: boolean;
//高亮属性
highlightIndex: number;
}

(3) Select 总结

  • 进阶版本的Dropdown,Input组件Tooltip组件的组合。
  • 善于使用已经有的基础组件来进行排列组合,二次开发需要的组件。
  • 对于复杂需求,可以使用手动控制下拉菜单的显示与隐藏。
  • 当遇到列表渲染的时候,应该条件反射一样的想到两种方式。

​ 语义化,也就是子组件,结构更清楚,渲染复杂的结构比较方便。

1
2
3
4
5
<Select>
<Item>First</Item>
<Item>2</Item>
<Item disabled>Disabled</Item>
</Select>

​ 使用数组,更加方便快捷,实现起来也比较容易,操作数组也更方便。

1
<Select items={items)></Select>
  • 遇到自定义模版这样的需求,想到使用自定义函数来渲染。
1
export type RenderLabelFunc =(option:Selectoption)=>VNode;

​ 使用RenderVNode这样的中间组件来渲染负责VNode或者直接使用jsx也完全可以

  • 遇到用户筛选这种需求,不管是同步还是异步,也要想到自定义函数
1
2
export type CustomFilterFunc = (value:string)=>Selectoption[]
export type CustomFilterRemoteFunc = (value:string)=>Promise<Selectoption[]>;
  • 对于在短时间内会被触发多次的回调,一定要注意是否需要函数截流

  • 使用KeyDown来监控键盘是否被按下,使用e.key而不是e.keyCode来监控哪个按键被按下

  • 当你将props的值,作为初始值传入给一个响应式对象的时候,一定要watch原始值的修改,然后更新本地的响应式对象。

15.Form 组件

(1) 需求分析

  • 自定义UI

    • 整体可自定义

    • 用户可以自定义渲染多种类型的表单元素- Input,Switch,Select等

    • 用户可以自定义提交区域的内容(按钮样式,排列等等)

  • 验证时机

    • 表单元素默认Blur的时候验证,可以自定义

    • 整个表单在点击提交提交按钮的时候全部验证

  • 验证规则

    • 每个Input可以配置多条规则(不能为空,需要是字符串,最多是多少等等)
    • 可以自定义规则(比如重复密码框输入的要和密码输入的一样)

组件结构设计:

1
2
3
4
5
const formobj = {
'name':{key:'name',value:'',rules:[],component:Input },
'password':{key:'name',value:'',rules:[],component:Select },
}
<Form :items="formobj"></Form>

这种设计的带来的问题:

不语义化,使用起来,看起来都很别扭,个性化很困难。

没办法自定义布局。

应该有的结构

第一步,静态外观:

1
2
3
4
5
6
7
8
9
<Form>
<Item label='用户名'name='name'>
<Input />
</Item>
<Item label='密码'name='password'>
<Input type="password"/>
</Item>
<Button type="submit">登陆</Button>
</Form>

第二步:属性:

1
2
3
4
5
6
7
8
const model = reactive({
name: '',
password:''
})
const rules = {
name:[{type:'email',required:true,trigger:'blur'}],
password:[{type:'string',required:true,trigger:'blur'),{min:3,max:5,message:'Length should be 3 to 5',trigger:'blur'}],
}

第三步结合:

1
2
3
4
5
6
7
8
9
<Form :model ="model" :rules="rules">
<Item label='用户名' name='name'>
<Input v-model="model.name"/>
</Item>
<Item label='密码' name='password' rules={[{ type:'string',required:true,min:3,max:8}]}>
<Input type="password" v-model="model.password"/>
</Item>
<Button type="submit">登陆</Button>
</Form>

第四步,想想怎样完成验证:

1
2
3
4
export interface FormInstance{
validate: ()=>void;
resetFields: ()=>void;
}

开发步骤:从静到动

  • 根据结构,实现基础布局。
  • 添加初始化数据,以及数据更新的功能。
  • 添加验证功能。
  • 后续的一些需求。

(2) 代码编写

0.流程图

1.基础结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<Form>
<FormItem label="the email">
<Input/>
</FormItem>
<FormItem label="the password">
<template #label="{label}"
<Button>{{label}}</Button>
</template>
<Input type="password" />
</FormItem>
<div>
<Button type="primary">Submit</Button>
<Button >Reset</Button>
</div>
</Form>
</div>
</template>



使用了作用域插槽实现数据传递:

2.添加数据和规则

1
2
3
4
5
6
7
8
export interface FormItemProps {
label: string;
prop?: string;
}
export interface FormProps {
model: Record<string, any>;
rules: FormRules;
}

3.获取数据和规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const innerValue = computed(() => {
const model = formContext?.model
if (model && props.prop && !isNil(model[props.prop])) {
return model[props.prop]
} else {
return null
}
})
const itemRules = computed(() => {
const rules = formContext?.rules
if (rules && props.prop && rules[props.prop]) {
return rules[props.prop]
} else {
return []
}
})
const getTriggeredRules = (trigger?: string) => {
const rules = itemRules.value
if (rules) {
return rules.filter(rule => {
if (!rule.trigger || !trigger) return true
return rule.trigger && rule.trigger === trigger
})
} else {
return []
}
}

4.学习使用 async-val(第三方库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Schema from 'async-validator';
const descriptor = {
name:{
type:'string',
required:true,
}
}

const validator = new Schema(descriptor)

//两种验证方式,第一种是回调
validator.validate({name:'muji'},(errors,fields)=>{
if (errors){
//errors的具体信息,fields是具体字段
return handleErrors(errors,fields);
}
//validation passed
});

//第二种是Promise类型(强推)
validator.validate({name:'muji',age:16 }).then(()=>{
//validation passed or without error message
}).catch(({errors,fie1ds})=>{
return handleErrors(errors,fields);
});

5.FormItem完成验证

验证的基本要素
  • value数据
  • rules规则
  • 在合适的时机,触发对应的验证
设计验证的流程

验证的类型

  • 表单整体验证
  • 单个表单元素验证

结论:不难发现整体验证就是循环调用单个验证汇总获得的结果

单个验证的实现方式

​ 要在Formltem中实现

​ 基本要素,从父组件Form获取值(value)和规则(rules)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const validate = async (trigger?: string) => {
const modelName = props.prop
const triggeredRules = getTriggeredRules(trigger)
if (triggeredRules.length === 0) {
return true
}
if (modelName) {
const validator = new Schema({
[modelName]: triggeredRules
})
validateStatus.loading = true
return validator.validate({ [modelName]: innerValue.value })
.then(() => {
validateStatus.state = 'success'
})
.catch((e: FormValidateFailure) => {
const { errors } = e
validateStatus.state = 'error'
validateStatus.errorMsg = (errors && errors.length > 0) ? errors[0].message || '' : ''
console.log(e.errors)
return Promise.reject(e)
})
.finally(() => {
validateStatus.loading = false
})
}

6.自动触发验证

验证的场景
  • 单个ltem的验证
  • 整个Form的验证

流程

规则(rules)+值(value)),在特殊的时机(比如onBlur),调用特殊的逻辑去验证最终的结果。

难点,后面注意

就是怎样在特殊的事件下,完成验证,从设计上来看,Formltem中的表单相关的组件,并没有显式绑定任
何的事件。

使用第三方库async-validator实现特殊验证

使用了promise的api

7.添加Trigger条件

1
2
3
4
5
6
7
8
9
10
11
12
//子组件传入trigger
const getTriggeredRules = (trigger?: string) => {
const rules = itemRules.value
if (rules) {
return rules.filter(rule => {
if (!rule.trigger || !trigger) return true
return rule.trigger && rule.trigger === trigger
})
} else {
return []
}
}

8.父子组件通信

1
2
3
4
5
6
7
8
9
//provide和inject实现
provide(formContextKey, {
...props,
addField,
removeField
})
provide(formItemContextKey, context)

const formContext = inject(formContextKey)

9.完成表单整个验证功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fields: FormItemContext[] = []
const addField: FormContext['addField'] = (field) => {
fields.push(field)
}
const removeField: FormContext['removeField'] = (field) => {
if (field.prop) {
fields.splice(fields.indexOf(field), 1)
}
}

const validate = async () => {
let validationErrors:ValidateFieldsError = {}
console.log('fields', fields)
for (const field of fields) {
try {
await field.validate('')
} catch(e) {
const error = e as FormValidateFailure
validationErrors = {
...validationErrors,
...error.fields
}
}
}
if (Object.keys(validationErrors).length === 0) return true
return Promise.reject(validationErrors)
}

10.添加重置状态功能

1
2
3
4
5
6
7
8
9
10

//form.vue
const resetFields = (keys: string[] = []) => {
const filterArr = keys.length > 0 ? fields.filter(field => keys.includes(field.prop)) : fields
filterArr.forEach(field => field.resetField())
}
const clearValidate = (keys: string[] = []) => {
const filterArr = keys.length > 0 ? fields.filter(field => keys.includes(field.prop)) : fields
filterArr.forEach(field => field.clearValidate())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//formitem.vue
const clearValidate = () => {
validateStatus.state = 'init'
validateStatus.errorMsg = ''
validateStatus.loading = false
}
const resetField = () => {
clearValidate()
const model = formContext?.model
if (model && props.prop && !isNil(model[props.prop])) {
model[props.prop] = initialValue
}
}

11.添加样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
.vk-form {
--vk-form-label-font-size: var(--vk-font-size-base);
--vk-form-content-font-size: var(--vk-font-size-base);
}
.vk-form-item {
display: flex;
margin-bottom: 18px;
.vk-form-item__label {
width: 150px;
height: 32px;
line-height: 32px;
padding: 0 12px 0 0;
box-sizing: border-box;
display: inline-flex;
justify-content: flex-end;
font-size: var(--vk-form-label-font-size);
color: var(--vk-text-color-regular);
}
.vk-form-item__content {
display: flex;
flex-wrap: wrap;
align-items: center;
flex: 1;
line-height: 32px;
font-size: var(--vk-form-content-font-size);
min-width: 0;
position: relative;
}
.vk-form-item__error-msg {
position: absolute;
top: 100%;
left: 0;
padding-top: 2px;
color: var(--vk-color-danger);
font-size: 12px;
line-height: 1;
}
}

.vk-form-item.is-error .vk-input__wrapper {
box-shadow: 0 0 0 1px var(--vk-color-danger) inset;
}
.vk-form-item.is-success .vk-input__wrapper {
box-shadow: 0 0 0 1px var(--vk-color-success) inset;
}
.vk-form-item.is-required > .vk-form-item__label::before {
content: "*";
color: var(--vk-color-danger);
margin-right: 4px;
}


12.优化整合

解决promise错误
1
validate: (trigger?: string) => Promise<any>;
1
2
3
const runValidation = (trigger?: string) => {
formItemContext?.validate(trigger).catch((e) => console.log(e.errors))
}

(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
2
3
4
5
6
7
8
9
10
11
const emitter mitt()
emitter.on('addField',()=>{})
emitter.on('removeField',()=>{})

provide(key,{emitter}}

//FormItem

emitter inject(formContextKey)

emitter.emit('addField',formItemContext)

一些小的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
2
3
4
package main
import (
"fmt"
)

模块化的优点

  • 可维护性
  • 可复用性

ES6之前没有模块的年代

backbone.js
1
2
3
4
5
6
//使用backbone.js的方法
<script src="spec/support/jquery.js"></script>
<script src="spec/support/underscore.js"></script>
<script src="spec/support/backbone.js"></script>
<script src="backbone.localStorage.js"></script>
<script src="todos.js"></script>
全局变量+命名空间(namespace)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// IIFE  自执行函数,创建一个封闭的作用域,赋值给一个全局变量
var namescollection = (function(){
//private members
var objects = [];

//Public Method
function addobject(object){
objects.push(object);
printMessage(object);
}

//Private Method
function printMessage(object){
console.log("Object successfully added:",object);
}

//public members,exposed with return statement
return {
addName:addobject,
};
})();
namesCollection.addNwame('viking')

缺点

  • 依赖全局变量,污染全局作用域,不安全
  • 依赖约定命名空间来避免冲突,可靠性不高
  • 需要手动管理依赖并控制执行顺序,容易出错
  • 需要在最终上线前手动合并所有用到的模块
AMD - (Asynchronous module definition)
  • 采用异步方式加载模块
  • 仅仅需要在全局环境定义require与define,不需要其他的全局变量
  • 通过文件路径或模块自己声明的模块名定位模块
  • 提供了打包工具自动分析依赖并合并
  • 配合特定的AMD加载器使用,RequireJS
  • 同时还诞生了很多类似的模块标准CMD
1
2
3
4
5
6
7
define(function(require){
//通过相对路径获取依赖模块
const bar = require('./bar')
//模块产出
return function (){
}
})
ES6
1
2
3
4
5
//通过相对路径获取依赖模块
import bar from './bar'
//模块产出
export default function (){
}
  • 引入和暴露的方式更加多样
  • 支持复杂的静态分析

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中。
  • 源码
    • 包含一些非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
2
3
import ElementPlus from 'element-plus'
const app = createApp(App)
app.use(ElementPlus)

一段代码给vue应用实例添加全局功能。它的格式是一个 object 暴露出一个install()方法,或者一个function

配置

1
2
3
4
5
const myPlugin = {
install(app,options){
//配置此应用
}
}

功能

插件没有严格的限制,一般有以下几种功能

  • 通过app.component()和app.directive()注册一到多个全局组件或自定义指令。
  • 通过app.provide()使一个资源可被注入进整个应用。
  • 向app.config.globalProperties中添加一些全局实例属性或方法

一个插件配置的例子

使用方法

打包组件库

Vite构建生产版本

当需要将应用部署到生产环境时,只需运行vite build命令。默认情况下,它使用<root>/index.html作为其构建入口点,并生成能够静态部署的应用程序包。

自定义构建

1
2
3
4
5
6
7
8
//vite.config.js
export default defineConfig({
build: {
rollupOptions:{
//https://rollupjs.org/configuration-options/
},
},
})

库模式

1
2
3
4
5
6
7
8
9
10
lib: {
//入口文件
entry:resolve(dirname,lib/main.js'),
// name则是暴露的全局变量,并且在 formats 包含'umd'或'iife'时是必需的。
name:'VElement',
//是输出的包文件名,默认 fileName 是 package.json 的 name 选项
fileName:'v-element',
//默认formats是['es','umd'],
formats:['es']
},

配置组件库的rollup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'VElement',
fileName: 'v-element'
},
rollupOptions: {
external: ['vue', '@fortawesome/fontawesome-svg-core', '@fortawesome/free-solid-svg-icons', '@fortawesome/vue-fontawesome'],
output: {
exports: 'named',
globals: {
vue: 'Vue'
},
assetFileNames: (chunkInfo) => {
if (chunkInfo.name === 'style.css') {
return 'index.css'
}
return chunkInfo.name as string
}
}
}
}

生成类型定义文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//tsconfig.build.json
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["src/index.ts", "src/components/**/*", "src/hooks/**/*"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["unplugin-vue-macros/macros-global" /* ... */]
},
"vueCompilerOptions": {
"plugins": [
"@vue-macros/volar/define-model",
"@vue-macros/volar/define-props",
"@vue-macros/volar/define-props-refs",
"@vue-macros/volar/short-vmodel",
"@vue-macros/volar/define-slots",
"@vue-macros/volar/export-props"
]
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

1
2
3
4
5
6
7
//vite.config.ts

import dts from 'vite-plugin-dts'

dts({
tsconfigPath: './tsconfig.build.json'
})

生成样式文件

1
2
3
4
5
//index.ts
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import './styles/index.css'
library.add(fas)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rollupOptions: {
//忽略后面的文件
external: ['vue', '@fortawesome/fontawesome-svg-core', '@fortawesome/free-solid-svg-icons', '@fortawesome/vue-fontawesome'],
output: {
exports: 'named',
globals: {
vue: 'Vue'
},
assetFileNames: (chunkInfo) => {
if (chunkInfo.name === 'style.css') {
return 'index.css'
}
return chunkInfo.name as string
}
}
}

发布npm

NPM简介

允许用户从npm服务器下载别人编写的第三方包到本地使用。
允许用户从npm服务器下载并安装别人编写的命令行程序到本地使用。
允许用户将自己编写的包或命令行程序上传到npm服务器供别人使用。

一些常见命令:

1
2
3
4
5
6
7
8
#登陆,注意现在登陆都是有两步验证的,会发一个数字到你的邮箱去
npm login
#注册或者使用 web界面 https://www.npmjs.com/signup
npm adduser
#查看是否登陆
npm whoami
#特别注意,关闭淘宝代理,要不操作会失败
npm config ls
语义化版本

版本规则: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
2
3
4
5
"scripts":{
"precompress":"{executes BEFORE the compress script }}",
"compress":"{run command to compress files }}",
"postcompress":"{executes AFTER compress script }}
}
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 不仅仅是一个预处理器,它提供了许多功能强大的插件

  1. 使用未来的 CSS 语法: 开发者可以在 CSS 中使用未来的 CSS 规范,如 CSS Modules、CSS Nesting、自定义属性(CSS 变量)等,然后使用 PostCSS 插件将其转换为当前浏览器支持的标准 CSS。
  2. 自动添加浏览器前缀: PostCSS 可以根据配置自动为 CSS 属性添加所需的浏览器前缀,以确保在各种浏览器中获得一致的样式。
  3. 代码压缩: PostCSS 可以通过移除注释、空格和不必要的代码来压缩 CSS。
  4. CSS 校验: 一些 PostCSS 插件可以帮助检查 CSS 代码中的错误或不规范的写法,并提供警告或错误信息。
  5. CSS Modules: PostCSS 支持 CSS Modules,这使得在大型项目中管理 CSS 变得更加容易和可维护。

选中 Postcss 的原因:

CSS Modules

CSS Modules 是一种用于管理 CSS 的技术,它可以帮助解决 CSS 的全局作用域和命名冲突等问题。使用 CSS Modules,您可以将 CSS 样式文件模块化,使每个模块的样式只在其自己的作用域内生效,从而避免了全局污染和命名冲突。

以下是 CSS Modules 的一些关键特性和用法:

  1. 模块化: 使用 CSS Modules,您可以将每个 CSS 文件视为一个独立的模块,每个模块都有自己的作用域,样式只在该作用域内生效。这样可以确保样式不会被其他模块影响,从而提高了代码的可维护性和可重用性。
  2. 局部作用域: 在 CSS Modules 中,类名和选择器都是局部的,只在当前模块中生效。这意味着您可以在不同模块中使用相同的类名而不会产生冲突,因为它们都被编译为不同的类名。
  3. 自动生成类名: CSS Modules 会自动为每个类名生成一个唯一的标识符,以防止命名冲突。这个标识符通常是基于文件名和类名生成的,因此您不需要手动管理类名。
  4. 导入和使用: 在 JavaScript 或 TypeScript 中,您可以通过导入 CSS 文件来使用 CSS Modules 中定义的样式。例如,在 React 或 Vue 组件中,您可以使用 import styles from './styles.module.css' 来导入样式,并通过 styles.className 来引用特定的类名。
  5. 提高可维护性: CSS Modules 可以帮助提高项目的可维护性,因为它将样式和组件或模块紧密耦合在一起,使得样式更容易理解和修改。

使用CSS Modules的原因:

  • 可重用
  • 作用域隔离

缺点:

  • 看起来不像是CSS的正常写法
  • 定义CSS全局样式比较麻烦
  • 需要额外的loader来生成Typescript的支持
  • 生成的类名并不友好

Transition

Transition 是 Vue.js 内置的组件,用于在元素插入、更新或删除时,通过 CSS 过渡和动画来实现平滑的过渡效果。它提供了一种简单而强大的方式来控制元素的过渡动画,使得用户界面更加生动和流畅。

以下是 Transition 内置组件的一些重要属性和用法:

  1. name 属性: 指定过渡动画的名称,可以用于在 CSS 中定义过渡动画的样式。Vue 会根据过渡状态为元素添加不同的类名,类名由 name 属性和状态名组成。

  2. appear 属性: 指定是否在初始渲染时执行过渡动画,默认为 false。如果设置为 true,则在组件初始渲染时,会触发过渡动画。

  3. type 属性: 指定过渡效果的类型,可以是 "transition"(默认)或 "animation"。如果设置为 "transition",则使用 CSS 过渡实现过渡效果;如果设置为 "animation",则使用 CSS 动画实现过渡效果。

  4. duration 属性: 指定过渡动画的持续时间,单位为毫秒。可以设置为一个数字,表示持续时间;也可以设置为一个对象,包含 enter、leave 和 appear 等属性,分别指定不同状态下的持续时间。

  5. easing 属性: 指定过渡动画的缓动函数,用于控制动画的加速和减速效果。可以设置为 CSS 中定义的缓动函数,如 "ease"、"ease-in"、"ease-out" 等。

  6. css 属性: 指定是否使用 CSS 过渡或动画来实现过渡效果,默认为 true。如果设置为 false,则不会应用任何过渡动画,适用于在 JavaScript 中自定义过渡效果的场景。

  7. onBeforeEnter、onEnter、onAfterEnter、onBeforeLeave、onLeave、onAfterLeave 属性: 这些属性用于设置过渡动画的回调函数,分别在不同阶段触发,可以用于执行自定义逻辑或处理过渡状态。

通过使用 Transition 内置组件,您可以轻松地为元素添加过渡动画,使得用户界面在元素插入、更新或删除时具有更加流畅和美观的效果。

使用transformY以及fade作出一个fade-up的效果(Message)

功能方面

组件测试

为什么想到要组件测试?

  1. 确保组件功能的正确性: 组件库系统的核心是提供一系列可复用的组件,这些组件在不同的项目中被频繁使用。为了保证组件的功能和行为符合预期,需要进行组件测试。测试可以验证组件的各种输入、状态和交互是否按照设计要求进行工作,从而确保组件功能的正确性。
  2. 提高组件库的稳定性: 组件库通常会被多个项目或团队使用,因此稳定性是非常重要的。通过组件测试,可以发现并修复潜在的问题和 bug,从而提高组件库的稳定性,减少在实际项目中出现问题的可能性。

选择Vitest的原因?

Vitest的优点

  • 基于Vite而生,和Vite完美配合,共享一个生态系统
  • 兼容Jest语法
  • HMR for tests
  • ESM,TS,JSX 原生支持

Vitest 是一个基于 Vite 构建的测试运行器,它具有许多优点和特性,使得它成为一个优秀的测试工具:

  1. 基于 Vite 构建: Vitest 是基于 Vite 构建的,与 Vite 完美配合,共享同一个生态系统。这意味着 Vitest 继承了 Vite 的许多优点,如快速的开发服务器、即时热重载(HMR)和现代的构建工具链,从而提供了高效、快速和稳定的测试环境。

  2. 兼容 Jest 语法: Vitest 兼容 Jest 的语法,可以直接使用 Jest 的断言、模拟和其他测试工具。这使得迁移现有的 Jest 测试套件到 Vitest 非常容易,无需重写现有的测试代码,从而减少了迁移的难度和成本。

  3. HMR for tests: Vitest 支持测试文件的即时热重载(HMR),当测试文件发生变化时,测试会自动重新运行,无需手动刷新页面或重新启动测试服务器。这大大提高了开发人员的开发效率,可以更快地获得反馈和调试测试代码。

  4. 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 的一些主要特点和功能:

  1. 与各种测试框架兼容: Vue Test Utils 不依赖于任何特定的测试框架,可以与主流的单元测试框架(如 Jest、Mocha、AVA 等)兼容使用。这使得开发人员可以根据自己的喜好和项目需求选择合适的测试框架,并且无需更改测试代码。

  2. 提供了一系列 API 和工具函数: Vue Test Utils 提供了丰富的 API 和工具函数,用于创建、渲染和操作 Vue 组件,并且模拟用户行为和交互。开发人员可以使用这些 API 和工具函数编写清晰、简洁的测试代码,覆盖组件的各种情况和边界条件。

  3. 提供了异步测试支持: Vue Test Utils 支持异步测试,可以处理异步操作和异步更新。开发人员可以使用 await 关键字等待异步操作完成,并且检查组件的状态和行为是否符合预期。

  4. 提供了快照测试支持: Vue Test Utils 提供了快照测试的支持,可以方便地比较组件的渲染结果。开发人员可以使用 toMatchSnapshot() 函数生成组件的快照,并且在后续的测试中比较新的渲染结果与快照是否一致。

  5. 与 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
2
3
4
5
6
7
8
import { h,createVnode} from 'vue'
const vnode =h(
'div',//type
{id:'foo',class:'bar'},//props
[
/*children */
]
)

使用render function/JSX写组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//render function
import { h, ref } from 'vue';

export default {
setup(props) {
const count = ref(1);

// 使用 render 函数
return () => h('div', [
h('h1', props.msg),
h('p', count.value)
]);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用 JSX 语法
import { ref } from 'vue';

export default {
setup(props) {
const count = ref(1);
return () => (
<div>
<h1>{props.msg}</h1>
<p>{count.value}</p>
</div>
);
}
}


这两种方式都可以用来编写 Vue 3 组件的 render 函数,选择其中一种取决于个人偏好和项目要求。

使用JSX写组件和使用Vue的区别

在组件编写方面,JSX 和 Vue.js 有一些相似之处,但也存在一些显著的区别。

相似之处:

  1. 组件化开发: JSX 和 Vue.js 都支持组件化开发,即将 UI 划分为多个独立的组件,每个组件负责特定的功能或视图,从而实现代码复用和逻辑分离。

  2. 父子组件通信: 在 JSX 和 Vue.js 中,父组件可以通过 props 向子组件传递数据,子组件可以通过事件向父组件发送消息。

  3. 生命周期钩子: JSX 和 Vue.js 都提供了生命周期钩子函数,可以在组件的不同阶段执行特定的逻辑,例如在组件被创建、更新或销毁时执行特定的操作。

不同之处:

  1. 语法: JSX 是 JavaScript 的一种扩展语法,可以直接在 JavaScript 中使用,组件的定义和渲染与普通的 JavaScript 代码类似;而 Vue.js 使用的是基于 HTML 的模板语法,在 Vue 组件中编写单独的模板代码,包括模板语法和 Vue 特定的指令。

  2. 模板语法: 在 Vue.js 中,可以使用模板语法来描述组件的 UI 结构和数据绑定关系,例如使用 {{ }} 插值表达式进行数据绑定,使用 v-bind 和 v-on 指令进行属性绑定和事件绑定;而在 JSX 中,可以直接在 JavaScript 中编写 JSX 元素,动态生成 UI 结构和处理事件逻辑。

  3. 样式作用域: 在 Vue.js 中,可以使用 <style scoped> 标签来定义组件的样式,样式将自动应用于当前组件,并且会自动添加唯一的作用域标识符,防止样式污染;而在 JSX 中,通常需要手动管理组件的样式,并且需要注意避免全局样式污染。

  4. 数据响应式: 在 Vue.js 中,数据的变化会自动触发组件的重新渲染,无需手动处理;而在 JSX 中,通常需要手动管理组件的状态和 UI 的更新。

综上所述,虽然 JSX 和 Vue.js 都支持组件化开发,但它们在语法、模板语法、样式作用域和数据响应式等方面存在一些差异,开发者可以根据项目需求、团队经验和个人偏好选择适合的技术栈。

测试方面的注意点?(tooltip)

  • 注意定时器的影响
    • vi.useFakeTimers()
    • vi.runAllTimers()
  • 注意点击到外侧区域的测试

防抖操作(tooltip)

这段代码使用了 lodash-es 库中的 debounce 函数来实现防抖效果。下面是具体的实现流程:

  1. 在组件中导入 debounce 函数:
1
import { debounce } from 'lodash-es'
  1. 定义了两个函数 openDebounce 和 closeDebounce,分别用于处理打开和关闭操作的防抖:
1
2
const openDebounce = debounce(open, props.openDelay)
const closeDebounce = debounce(close, props.closeDelay)

这里的 open 和 close 函数是对应的打开和关闭操作。props.openDelay 和 props.closeDelay 分别表示打开和关闭操作的延迟时间,通过组件的 props 进行配置。

  1. 在需要立即执行打开或关闭操作时,调用 openDebounce.cancel() 或 closeDebounce.cancel() 取消之前的防抖操作,然后立即执行对应的打开或关闭操作:
1
2
3
4
5
6
7
8
const openFinal = () => {
closeDebounce.cancel()
openDebounce()
}
const closeFinal = () => {
openDebounce.cancel()
closeDebounce()
}

这里通过 cancel 方法取消之前的防抖操作,然后再次调用对应的打开或关闭函数,以确保立即生效。

通过使用 debounce 函数,可以限制某个函数在短时间内被多次调用,从而避免不必要的执行次数,实现防抖效果。

防抖是什么?

防抖(Debouncing)是一种常见的前端技术,用于限制某个函数在短时间内被多次调用,以减少不必要的执行次数。在防抖的实现中,当触发事件时,如果在指定的延迟时间内再次触发了相同的事件,那么前一个事件处理函数的执行就会被取消,并重新设置一个新的延迟。只有在延迟时间内没有再次触发事件时,才会执行函数。

下面是防抖的基本思想:

  1. 当触发事件时,设置一个定时器,在指定的延迟时间后执行事件处理函数。
  2. 如果在延迟时间内再次触发了相同的事件,则清除之前的定时器,重新设置一个新的定时器。
  3. 只有当延迟时间内没有再次触发事件时,才会执行事件处理函数。

节流是什么?

节流(Throttling)是一种常见的前端技术,用于控制某个函数在一定时间间隔内只能执行一次,以减少函数的执行频率。与防抖不同,节流是在一定时间间隔内稀释函数的执行次数,而不是将多次触发的事件合并成一次执行。

下面是节流的基本思想:

  1. 当触发事件时,设置一个标记(比如布尔值)来表示函数是否正在执行。
  2. 如果函数没有在执行,则执行函数,并将标记设置为 true。
  3. 在指定的时间间隔内,即使触发了相同的事件,也不会再次执行函数,直到时间间隔结束。
  4. 时间间隔结束后,将标记重置为 false,以允许下一次事件触发时执行函数。

节流的应用场景包括但不限于:

  • 页面滚动事件:在用户滚动页面时执行某个操作,但限制操作的执行频率,避免频繁触发。
  • 鼠标移动事件:在鼠标移动时执行某个操作,但限制操作的执行频率,提高性能。
  • 输入框输入事件:在用户输入文本时执行某个操作,但限制操作的执行频率,减少不必要的请求或处理。

使用节流技术可以有效地控制函数的执行频率,从而提高页面性能和用户体验,并且在一些需要限制事件触发频率的场景下非常实用。

hooks –自定义钩子函数

自定义钩子函数在 Vue 中具有多种用途,它们允许你在组件的生命周期或特定阶段执行自定义逻辑,从而扩展或定制组件的行为。下面列出了几个常见的用途:

  1. 组件复用: 自定义钩子函数使得可以在多个组件之间共享相同的逻辑。例如,你可以创建一个自定义钩子函数来处理数据请求,然后在多个组件中使用这个钩子函数来获取数据。

  2. 代码组织: 自定义钩子函数有助于将组件的逻辑分解为更小的、可重用的部分,从而提高代码的可维护性和可读性。你可以将一些复杂的逻辑抽象为自定义钩子函数,并在需要的地方进行调用。

  3. 逻辑复用: 自定义钩子函数可以用于封装一些常见的逻辑,使得这些逻辑可以在不同的组件中重复使用。例如,你可以创建一个自定义钩子函数来处理表单验证逻辑,然后在多个表单组件中重复使用这个钩子函数。

  4. 测试: 自定义钩子函数可以使得组件更容易进行单元测试。你可以将一些与组件生命周期相关的逻辑封装在自定义钩子函数中,并在测试时对这些钩子函数进行单独测试。

总的来说,自定义钩子函数为你提供了一种灵活的方式来扩展组件的功能,并使得代码更加模块化、可重用和易于测试。

在项目中的使用

useEventListener.ts

这个自定义钩子函数的作用是为给定的目标对象(可以是 Ref 类型或普通对象)添加事件监听器,并在组件卸载前移除该事件监听器。具体来说,它实现了以下功能:

  1. 如果目标对象是 Ref 类型,它会在目标对象的值发生变化时添加或移除事件监听器。
  2. 如果目标对象是普通对象,它会在组件挂载时添加事件监听器,并在组件卸载前移除事件监听器。

这个自定义钩子函数采用了 Vue 3 的 Composition API,使用了 onMounted、onBeforeUnmount、isRef、watch 和 unref 等函数。

具体实现流程如下:

  1. 如果目标对象是 Ref 类型,则通过 watch 监听目标对象的变化,并在新值中添加事件监听器,并在旧值中移除事件监听器。
  2. 如果目标对象不是 Ref 类型,则在组件挂载时通过 onMounted 添加事件监听器,并在组件卸载前通过 onBeforeUnmount 移除事件监听器。
  3. 在移除事件监听器时,使用 unref 来获取目标对象的实际值,并调用其 removeEventListener 方法移除事件监听器。

这个自定义钩子函数的作用是封装了事件监听器的添加和移除逻辑,使得在 Vue 组件中使用事件监听器变得更加方便和灵活。

useClickOutside.ts

这个自定义钩子函数 useClickOutside 的作用是监听点击事件,并判断点击事件是否在指定的元素外部,然后执行相应的回调函数。主要逻辑如下:

  1. 在点击事件发生时,检查点击的目标是否在指定的元素外部。
  2. 如果点击的目标不在指定的元素外部,则调用传入的回调函数。

这个自定义钩子函数采用了 Vue 3 的 Composition API,使用了 onMounted 和 onUnmounted 钩子函数。

具体实现流程如下:

  1. 在组件挂载后,通过 onMounted 钩子函数向 document 添加一个点击事件监听器,以便在点击事件发生时触发 handler 函数。
  2. 在点击事件发生时,handler 函数判断点击的目标是否在指定的元素外部,如果是,则调用传入的回调函数。
  3. 在组件卸载前,通过 onUnmounted 钩子函数移除之前添加的点击事件监听器,以防止内存泄漏和性能问题。

这个自定义钩子函数的作用是在 Vue 组件中封装了监听点击事件的逻辑,使得可以更方便地判断点击是否在指定元素外部,并执行相应的逻辑。

useZIndex.ts

这段代码定义了一个自定义钩子函数 useZIndex,用于管理一个全局的 z-index 值,并提供获取下一个 z-index 的功能。具体实现如下:

  1. 使用 ref 创建了一个名为 zIndex 的响应式变量,用于存储全局的 z-index 值,初始值为 0。
  2. 定义了 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
2
import { inject } from "vue"
inject('Colors');

对于 colors 导数的数据类型我们并不知道是什么,它可以是对象 or 数组亦或是字符串,只能顺瓜摸藤找到它的 provide,对于小项目找一下可能不花费什么时间,但对于大型项目来说很明显是不可取的,于是官方提供了 InjectionKey 函数来对传参进行类型约束,确保父子间传递的数据类型是可见、透明的。

InjectionKey 函数的使用也很简单,原理是将 provide 与 inject 的第一个参数即 key 通过声明 symbol 的方式关联起来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 将 InjectionKey 定义的数据类型放到 keys/index.ts 下维护
// keys/index.ts
import {InjectionKey, Ref } from "vue"
// 限制了 provide 导出的数据必须是 ref 且 boolean 类型
export const showPopupKey: InjectionKey<Ref<boolean>> = Symbol()
// 限制了 provide 导出的数据必须是 string
export const titleKey: InjectionKey<string> = Symbol()

// 2. 在 A .vue 文件中调用 provide 导出数据,第一个参数则是我们上面定义好的数据类型,第二个参数是对应数据类型的值
import { provide, InjectionKey, Ref } from "vue"
import { showPopupKey } '@/keys'
const showPopup = ref(false)
// 正确
provide(showPopupKey, showPopup)
// TS 报错: 'Hello' 是字符串,与 showPopupKey 不匹配
provide(showPopupKey, 'Hello')
// 正确
provide(titleKey, 'Hello')

// 3. 在 B.vue 文件中导入数据
import { showPopupKey } from '@/keys'
import { inject } from 'vue'
inject(showPopupKey) // 现在获取到的数据类型是安全的

有了 InjectionKey 函数后就不用再担心 provide&inject 之间的数据类型问题了,我们只需找到 InjectionKey 关联的 key即可

语义化展示

要实现语义化展示,父子属性传递的常规方式:

  • 把数据状态以及处理逻辑 放在父组件

  • 使用Provide/Inject 传递给子组件

omit 过滤属性

omit
_.omit(object, keys)
函数返回一个没有列入key属性的对象。其中,参数object为JSON格式的对象,
keys表示多个需要排除掉的key属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const _ = require('lodash/object');
const originObject = {
A: 1,
B: 2,
C: 3,
D: 4
};
const newObject = _.omit(originObject, 'B', 'C');
console.log(originObject);
console.log(newObject);

{ A: 1, B: 2, C: 3, D: 4 }
{ A: 1, D: 4 }

使用v-on = events 动态绑定事件(tooltip)

好处:

  1. 灵活性: 可以根据组件的状态或属性动态地决定何时绑定事件,以及绑定哪些具体的事件。
  2. 简洁性: 可以通过简单地修改组件的属性或状态来控制事件的绑定和解绑,而无需在模板中编写大量的条件语句。
  3. 可维护性: 通过集中管理事件绑定的逻辑,可以使代码更加清晰易懂,并且便于后续的维护和修改。

原理:

  1. 事件监听器的绑定: 当 Vue 解析模板时,遇到 v-on 指令时,会将其解析为一个事件监听器,并动态地绑定到相应的 DOM 元素上。这个事件监听器可以是一个函数,也可以是一个对象,对象的键为事件名,值为事件处理函数。
  2. 事件处理函数的动态更新: 如果 events 对象是响应式的(通过 ref 或 reactive 创建),则当 events 对象发生变化时,相关的事件处理函数也会动态更新。这意味着可以在组件的生命周期内动态地修改 events 对象,从而动态地修改事件处理逻辑。
  3. 事件绑定的动态性: 由于 v-on 指令是在模板编译阶段解析的,因此可以根据组件的属性、状态或其他条件动态生成 events 对象,从而实现事件绑定的动态性。这使得组件的事件处理逻辑可以根据不同的情况进行动态调整。

手动触发功能的实现(tooltip)

Tooltip 组件代码通过监听 props.manual 属性的变化,来实现手动触发功能。具体实现步骤如下:

  1. 当 props.manual 的值为 false(即非手动模式)时,在组件初始化时会调用 attachEvents 函数,该函数根据 props.trigger 的值来决定绑定哪些事件监听器。
  2. 如果 props.trigger 的值是 'hover',则将 mouseenter 事件绑定到 openFinal 函数,将 mouseleave 事件绑定到 closeFinal 函数;如果 props.trigger 的值是 'click',则将 click 事件绑定到 togglePopper 函数。
  3. 当用户点击触发节点时,触发 togglePopper 函数,根据当前弹出框的状态来切换打开或关闭状态。
  4. 如果 props.manual 的值为 true(即手动模式),则取消绑定所有事件监听器,即清空 events 和 outerEvents 对象。
  5. 这样,用户可以根据 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineComponent } from 'vue'
const RenderVnode = defineComponent({
props: {
vNode: {
//可以是字符串或对象,且是必需的
type: [String, Object],
required: true
}
},
setup(props) {
//这个渲染函数接收 props 对象作为参数,并返回 props.vNode,即接收到的虚拟节点
return () => props.vNode
}
})

//将 RenderVnode 组件导出
export default RenderVnode
1
<RenderVnode :vNode="item.label"/>
1
2
3
4
5
6
7
import type { VNode } from 'vue'
export interface MenuOption {
label: string | VNode; //传入的可能是VNode
key: string | number;
disabled?: boolean;
divided?: boolean;
}

在提供的代码中,渲染函数 return () => props.vNode 返回的是 props.vNode,即 vNode prop 的值。因此,这个渲染函数实际上渲染了从父组件传递过来的虚拟节点(即 Vue 中的 VNode)。这个渲染函数并不会对传入的虚拟节点进行任何处理,它只是简单地返回传入的虚拟节点。

如果父组件将一个字符串或者一个组件的 VNode 作为 vNode prop 传递给 RenderVnode 组件,那么 RenderVnode 组件将会渲染这个传入的虚拟节点。

使用函数式的方式创建组件

根据上面的代码,我们可以总结如下使用函数式的方式创建组件的步骤:

  1. 导入所需的 Vue 3 相关函数和组件类型。
  2. 定义组件的创建函数,该函数接收组件的属性作为参数。
  3. 在创建函数内部,执行必要的逻辑,例如生成唯一的组件标识符、创建容器元素等。
  4. 根据需要调用其他自定义 hook 或函数来获取额外的数据或执行其他逻辑。
  5. 创建新的组件属性对象,并在其中包含传入的属性以及其他必要的属性,如组件标识符、Z 轴顺序等。
  6. 使用 h 函数创建虚拟节点,并传入组件构造函数和属性对象。
  7. 使用 render 函数将虚拟节点渲染到容器中。
  8. 将容器中的组件添加到页面上的合适位置,通常是 document.body。

这种方式的好处是,它允许以纯函数的方式创建组件,使得组件的创建过程更加灵活和可控,同时也更容易进行测试和调试。

VitePress特点

VitePress 是一个基于 Vite 构建的静态网站生成器。它有一些显著的优势和特点:

  1. 快速构建:VitePress 利用了 Vite 的快速开发能力,因此构建速度非常快。Vite 是一个基于现代 JavaScript 的构建工具,具有出色的性能,因此 VitePress 可以在几秒钟内为您的网站生成页面。

  2. Vue 驱动:VitePress 是基于 Vue.js 构建的,这意味着您可以使用 Vue 的强大功能来构建和定制您的网站。Vue 是一个流行的 JavaScript 框架,它提供了诸如组件化、响应式数据绑定等功能,使得构建复杂的用户界面变得更加简单和高效。

  3. Markdown 支持:VitePress 支持使用 Markdown 编写内容,这使得编写和维护文档变得非常简单。您可以在 Markdown 中编写内容,而无需关注 HTML 或其他复杂的语法。

  4. 热重载:VitePress 支持热重载,这意味着当您编辑内容或修改样式时,您所做的更改会立即在浏览器中实时显示,而无需手动刷新页面。

  5. 主题定制:VitePress 允许您轻松定制网站的外观和风格。您可以选择预先构建的主题,也可以根据需要创建自己的主题。这使得您可以根据项目的需求定制您的网站,使其与您的品牌或设计风格保持一致。

  6. 插件系统:VitePress 提供了一个灵活的插件系统,使得您可以扩展其功能。您可以编写自己的插件,或者使用社区中已有的插件来增强您的网站。

总的来说,VitePress 是一个快速、灵活且易于使用的静态网站生成器,适用于构建各种类型的网站,包括文档、博客、演示等。

TDD的开发方式

TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法,它强调在编写代码之前先编写测试用例,然后通过不断地编写和运行测试用例来驱动代码的开发。

  • 1.编写测试用例
  • 2.运行测试用例
  • 3.编写代码:根据测试用例的要求,编写代码,实现功能。
  • 4.运行测试用例
  • 5.重构代码(可选)
  • 6.重复上述步骤

TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法,其主要特点包括以下几点:

  1. 测试先行:在编写实际代码之前,先编写测试用例。这意味着在实现功能之前,您首先要考虑如何测试该功能。这有助于确保您的代码具有良好的覆盖率,并且在代码实现之前就已经考虑了您希望代码如何被使用。

  2. 快速反馈:TDD 鼓励频繁运行测试,以便及时发现问题。由于测试是在编写代码之前编写的,因此一旦编写的代码无法通过测试,您就可以立即得到反馈,并且可以及时进行修复。

  3. 增量开发:TDD 鼓励采用增量开发的方式,即每次只编写足够的代码来通过当前失败的测试用例。这有助于确保您的代码在不断迭代中保持可用性,并且可以减少在后续开发阶段出现的集成问题。

  4. 简化重构:由于在 TDD 中测试用例是作为开发的一部分编写的,因此更容易进行重构。重构是指对现有代码进行修改,以改善其结构、可读性或性能,而不改变其行为。通过测试用例,您可以确保重构后的代码仍然具有相同的行为。

  5. 文档化:测试用例充当了代码行为的文档。通过阅读测试用例,其他开发人员可以了解代码的预期行为,这有助于提高代码的可理解性和可维护性。

  6. 提高设计质量:TDD 有助于推动更好的软件设计实践。由于在编写代码之前先考虑如何测试代码,因此往往会促使编写更清晰、模块化和可测试的代码,从而提高软件的设计质量。

总的来说,TDD 是一种以测试为中心的开发方法,通过频繁运行测试、编写测试用例以及以测试驱动的方式编写代码,帮助开发人员编写更高质量、更可靠且更易于维护的软件。

可读性方面

Record

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 这些都是js钩子与过渡transition相关 过渡效果相关的事件处理函数 包含一些函数的对象 ,用record --ts提高代码的可读性和可维护性
const transitionEvents: Record<string, (el: HTMLElement) => void> = {
beforeEnter(el) {
el.style.height = '0px'
el.style.overflow = 'hidden'
},
enter(el) {
el.style.height = `${el.scrollHeight}px`
},
afterEnter(el) {
el.style.height = ''
el.style.overflow = ''
},
beforeLeave(el) {
el.style.height = `${el.scrollHeight}px`
el.style.overflow = 'hidden'
},
leave(el) {
el.style.height = '0px'
},
afterLeave(el) {
el.style.height = ''
el.style.overflow = ''
}

}

在这段代码中,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 都用于定义类型,但它们之间存在一些区别:

  1. 语法:

    • interface:使用 interface 关键字定义接口,可以用来描述对象的形状(属性和方法)。
    • type:使用 type 关键字定义类型别名,可以用来为任意类型定义别名。
  2. 扩展性:

    • interface:接口可以被扩展,可以通过 extends 关键字扩展其他接口,从而合并多个接口成为一个。
    • type:类型别名不支持扩展,不能直接使用 extends 进行类型别名的继承。
  3. 可声明多次:

    • interface:可以多次声明同一个接口,并且会自动合并成一个接口。
    • type:类型别名也可以多次声明,但是后面的声明会覆盖之前的声明,而不会进行合并。
  4. 适用场景:

    • interface:适用于描述对象的形状,例如定义接口用于表示某种数据结构或类的契约。
    • type:适用于为任意类型定义别名,可以用来简化复杂类型或提高代码可读性,例如定义复杂的联合类型、交叉类型、元组类型等。

总的来说,interface 主要用于定义对象的形状和结构,支持扩展和合并,而 type 则更灵活,适用于为任意类型定义别名,但不支持扩展。

安全性方面

组件实例卸载

1
2
3
4
//当组件被卸载时,通过调用 popperInstance?.destroy() 方法销毁 Popper 实例,以防止内存泄漏或其它问题
onUnmounted(() => {
popperInstance?.destroy()
})

性能方面

shallowReactive

shallowReactive 是 Vue 3 中提供的一个 API,用于创建一个浅响应式对象。与 reactive 不同的是,shallowReactive 只会对对象的第一层属性进行响应式处理,而不会递归地处理对象内部的嵌套对象。这样可以在一定程度上减少 Vue 的响应式系统的开销,提高性能。

当你确定某个对象内部的嵌套结构不会发生变化,或者你不需要监视其内部属性的变化时,可以使用 shallowReactive 来创建一个浅响应式对象,从而避免不必要的性能开销。

然而,需要注意的是,使用 shallowReactive 可能会导致你失去了一些 Vue 响应式系统提供的便利特性,比如失去了对嵌套对象内部属性变化的监视能力。因此,需要根据具体的使用场景来权衡是否使用 shallowReactive。

总的来说,shallowReactive 可以在一些特定情况下提供性能上的优势,但需要谨慎使用以确保不会丢失对对象属性变化的监视。

下面是一个简单的示例,演示了如何使用 shallowReactive 来创建一个浅响应式对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { shallowReactive } from 'vue';

// 创建一个普通的 JavaScript 对象
const nestedObject = {
a: 1,
b: {
c: 2,
d: 3
}
};

// 使用 shallowReactive 创建一个浅响应式对象
const shallowObject = shallowReactive(nestedObject);

// 修改第一层属性,触发响应式更新
shallowObject.a = 10;
console.log(shallowObject.a); // 输出 10

// 修改嵌套属性,不会触发响应式更新
shallowObject.b.c = 20;
console.log(shallowObject.b.c); // 输出 20,但不会触发更新

// 添加新的属性,也会触发响应式更新
shallowObject.e = 5;
console.log(shallowObject.e); // 输出 5

在这个示例中,nestedObject 是一个普通的 JavaScript 对象,它包含一个嵌套的对象。然后,我们使用 shallowReactive 函数将其转换为一个浅响应式对象 shallowObject。接着,我们对 shallowObject 的第一层属性进行修改,触发了响应式更新。但是,当我们修改嵌套属性 b.c 时,不会触发响应式更新,因为 shallowReactive 只对第一层属性进行响应式处理。最后,当我们添加新的属性 e 时,又会触发响应式更新。

shallowReactive 可以用于数组,但它仅在数组的长度发生变化时触发响应式更新,而不会在数组元素内部的变化上触发响应式更新。这就是所谓的浅响应式数组。

下面是一个示例,展示了如何使用 shallowReactive 创建一个浅响应式数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { shallowReactive } from 'vue';

// 创建一个普通的 JavaScript 数组
const nestedArray = [1, [2, 3]];

// 使用 shallowReactive 创建一个浅响应式数组
const shallowArray = shallowReactive(nestedArray);

// 修改数组的长度,触发响应式更新
shallowArray.length = 2;
console.log(shallowArray); // 输出 [1, [2, 3]]

// 修改数组元素的值,不会触发响应式更新
shallowArray[1][0] = 5;
console.log(shallowArray); // 输出 [1, [5, 3]]

// 添加新的元素,也会触发响应式更新
shallowArray.push(4);
console.log(shallowArray); // 输出 [1, [5, 3], 4]

在这个示例中,我们首先创建了一个普通的 JavaScript 数组 nestedArray,然后使用 shallowReactive 将其转换为一个浅响应式数组 shallowArray。接着,我们对 shallowArray 的长度进行修改,触发了响应式更新。但是,当我们修改数组元素的值时,并不会触发响应式更新,因为 shallowReactive 只对数组的长度进行响应式处理。最后,当我们向数组中添加新的元素时,又会触发响应式更新。

项目亮点

1.采用TDD的方式开发组件

2.使用函数式的方式开发组件

3.使用JSX的方式开发组件

让我们看看这三种开发方式的亮点:

  1. 采用TDD的方式开发组件:

    • 可靠性增强:TDD 着眼于先编写测试用例,然后再编写实际的代码来满足测试,这有助于确保组件的可靠性。您可以通过编写测试来捕获和纠正潜在的问题,从而提高组件的质量和稳定性。
    • 可维护性提高:TDD 鼓励频繁运行测试,并且测试用例充当了组件行为的文档。这使得组件的行为和预期结果变得清晰可见,有助于提高代码的可理解性和可维护性。
    • 增量开发:TDD 通过逐步编写测试用例和代码来推动增量开发。这种方法使得开发过程更加可控和可预测,减少了集成问题的风险,并且有助于在开发过程中快速迭代。
  2. 使用函数式的方式开发组件:

    • 简洁性:函数式组件通常比传统的基于类的组件更为简洁。通过使用函数式编程风格,您可以编写更少的代码来实现相同的功能,从而提高了代码的可读性和可维护性。
    • 纯函数特性:函数式组件通常是纯函数,即相同的输入将始终产生相同的输出,而不会产生副作用。这使得函数式组件更容易进行测试和推理,并且有助于提高组件的可靠性。
    • 函数式编程范式:使用函数式编程范式可以促进代码的模块化和可组合性,从而使得组件更易于重用和组合。函数式组件的设计可以使得代码更容易理解和调试。
  3. 使用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 组件的自定义性

  1. 自定义UI:

    • 强调 “form” 组件提供了丰富的自定义UI功能,用户可以根据自己的需求自定义整体样式和渲染多种类型的表单元素,包括 Input、Switch、Select 等。用户还可以自定义提交区域的内容,包括按钮样式、排列等,从而实现了个性化定制。
  2. 验证时机:

    • 强调 “form” 组件灵活的验证时机设置,表单元素默认在失去焦点时验证,但用户也可以根据需要自定义验证时机。整个表单在点击提交按钮时会进行全部验证,确保数据的完整性和准确性。
  3. 验证规则:

    • 强调 “form” 组件丰富的验证规则设置,每个 Input 元素可以配置多条验证规则,包括不能为空、需要是字符串、最多多少字符等等。用户还可以自定义规则,例如确保重复密码框输入与密码输入一致等,从而提供了更高程度的数据验证和安全性保障。

通过突出 “form” 组件的自定义UI、验证时机和验证规则等特点,可以有效地吸引用户和开发者的注意力,并展示项目在用户输入处理方面的丰富功能和灵活性。

二、Message一大章作为难点可聊

JSX 和 渲染函数可聊

三、表单作为难点一大章可聊

1.input组件 使用测试TDD的开发方式开发组件

2.form的数据传递和表单验证

3.Select的远程请求部分