mkdir ui-element-vue3
cd ui-element-vue3
mkdir packages examples docs
在项目根目录中创建 pnpm-workspace.yaml 文件。
# pnpm-workspace.yaml
packages:
- docs # 组件文档
- examples # UI组件库文档测试代码
- packages/* # 组件包
分别进入到 packages、examples 和 docs 文件夹中,创建各自的 package.json 文件(pnpm init
),并将其"name"修改为“@ui-element-vue3/packages”、“@ui-element-vue3/examples”和“@ui-element-vue3/docs”。
{
"name": "@ui-element-vue3/components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
在根目录下初始化 package.json,添加依赖。
{
...
"dependencies": {
"@ui-element-vue3/utils": "workspace:*",
"@ui-element-vue3/hooks": "workspace:*",
"@ui-element-vue3/components": "workspace:*"
}
}
在项目根目录下执行pnpm install
,安装成功
Caution
在没有 package.json 文件或文件中没有上述三个依赖包的情况下去添加这几个依赖的话,一般会在根目录下的终端中执行命令pnpm install @ui-element-vue3/components -w
。但是,我会报错,导致无法安装成功。
或者
演示库用于组件开发过程的各种调试,开发人员可以直观查看组件库的开发效果。执行pnpm create vite@6 examples
,记得选择 Vue 开发框架、Javascript 语言这两项。
<!-- index.vue -->
<template>
<div>button</div>
</template>
<script setup>
defineOptions({ name: "ue-button" })
</script>
<style lang="scss" scoped></style>
要将 button 组件按需导出,需要在 components/index.js 文件中引入 components/button/index.js 文件,在 components/button/index.js 文件中引入 components/button/src/index.vue 文件,并提供按需加载的方式,最后使用 export default 导出。缺点是需要用到该组件的时候,每次都需要 import 一下,有不少重复语句。
// components/button/index.js
import { componentInstall } from "@ui-element-vue3/utils"
import Button from "./src/button.vue"
// 按需加载
export const UeButton = componentInstall(Button)
// 导出组件
export default UeButton
// utils/install.js
/**
* 安装组件
* @param {Object} com - 需要添加安装功能的 Vue 组件对象,会自动获取组件内的 name 属性
* @returns {Object} 返回处理后的组件对象,带有 install 方法
* @example
const MyComponent = {
name: 'MyComponent',
}
export default componentInstall(MyComponent);
// 然后在主文件中可以这样使用
// app.use(MyComponent);
*/
export const componentInstall = com => {
com.install = app => {
// app是要注册的组件
app.component(com.name, com)
}
return com
}
全局注册导出组件同样是所有组件汇聚到一个文件,使用循环的方式批量注册组件。
// packages/components.js
import { UeButton } from "./components/button/index.js"
export default [UeButton]
// packages/index.js
// 组件库的入口文件
// 按需加载
export * from "./components/index.js"
// 全局注册
import components from "./components.js"
// 全局安装
const install = function (app) {
if (install.installed) return
// 安装组件
components.forEach(comp => {
app.use(comp)
})
}
export default install
全局注入是最常见的一种方法,一次注入,任意位置使用。但是会增加项目的整体体积。
// example/src/main.js
import { createApp } from "vue"
import App from "./App.vue"
import UElement from "../../packages/index"
const app = createApp(App)
app.use(UElement)
app.mount("#app")
<!-- example/src/App.vue -->
<script setup></script>
<template>
123
<ue-button></ue-button>
</template>
<style scoped></style>
按需加载可以减小项目的整体体积,提升性能。无需像全局注册那样在 main.js 文件中全局注册。
<!-- example/src/App.vue -->
<script setup>
import { UeButton } from "../../packages/components/button"
</script>
<template>
<ue-button></ue-button>
</template>
<style scoped></style>
UI 组件库元素规范模仿市面上不错的现有组件库,如:Element Plus、Ant Design、Mantine 等
BEM 命名规则:Block(块)、Element(元素)、Modifier(修改器)。命名格式:
block-name__<element-name>--<modifier-name>_<modifier-value>
a-button--size_small
: 样式作用于 a-button 组件,而不是其内部其他元素,a-button 的 size 设置为 smalla-tabs--size_small
: a-tabs 组件内还有一层元素,该元素的 size 设置为 small
按钮类型:default、primary、light、outline、subtle/text、dashed、link、Gradient*
按钮类型 | 按钮默认样式 | 鼠标悬浮样式 |
---|---|---|
deafult | ![]() |
![]() |
primary | ![]() |
![]() |
light | ![]() |
![]() |
outline | ![]() |
![]() |
subtle | ![]() |
![]() |
dashed | ![]() |
|
Link | ![]() |
|
Gradient* | ![]() |
![]() |
自动触发加载属于一种业务类型的操作,根据 API 接口请求的过程实现 loading 加载的自动变更,无需手动改变状态。
通过Boolean
值去控制是否被勾选,true:勾选,false:未勾选。同时与相应的勾选/未勾选样式类对应,从而达到勾选/未勾选的视觉效果。
具体描述:隐藏原生<input>
元素,用<span>
替代。虽然原生<input>
隐藏了,但当用户点击<label>
或其内部元素的时候,原生<input>
标签会响应被点击的事件,在被勾选和未被勾选两状态之间切换。由于自定义了<span>
为<input>
元素的替身视觉元素,因此,<input>
元素如果是被勾选状态,则<span>
元素也得显示被勾选状态。要做到这点的话,需要在最外层<label>
元素上添加一个 is-checked
类名(ns.is('checked', isChecked)
),如果 isChecked
为 true
,那么就将<span>
元素的边框、背景、图标、文本做一个选中样式。反之,则相反。那isChecked
变量又跟model
绑定,当用户点击复选框时,原生 input
的勾选状态变化会触发 Vue 更新 model
值。所以能做到勾选/未勾选效果。
@change
或v-on:change
:是原生 HTML input
元素的标准事件之一。当复选框的状态发生改变时(比如用户点击选中/取消选中),input 元素会触发一个原生的 change
事件。这不需要特别定义,它是 HTML 规范中就内置的事件。
Caution
为什么一点击 checkbox,他的状态会同步切换呢?其中用到了**defineModel
**,具体解释在下文。
根据HTML规范,当label标签包含input元素时,点击label内的任何区域(包括文本)都会触发input的点击事件,从而切换复选框状态。 另外,label标签也可能通过原生属性for的值与原生属性id的值进行关联。
<!-- checkbox.vue -->
<template>
<component
:is="tag"
:class="[
ns.b(),
ns.is('disabled', isDisabled),
ns.m('size', checkboxSize),
ns.m(type),
ns.is('checked', isChecked),
]">
<!-- 视觉元素,多选框框 -->
<span :class="[ns.e('wrapper')]">
<!-- 隐藏原生input -->
<input
:class="[ns.e('input')]"
type="checkbox"
:disabled="isDisabled"
v-model="model"
:value="value"
@change="changeEvent"
/>
<!-- 多选框框的替代 -->
<span :class="[ns.e('inner')]">
<ue-icon>
<Check />
</ue-icon>
</span>
</span>
<!-- chekbox文本 -->
<span :class="[ns.e('label')]">
<slot />
</span>
</component>
</template>
<script setup>
import { useNamespace } from "@ui-element-vue3/hooks"
import { Check } from "@ui-element-vue3/icons"
import { useCheckbox } from "../composables"
defineOptions({ name: "ue-checkbox" })
const ns = useNamespace("checkbox")
const props = defineProps({
tag: {
type: String,
default: "label",
},
disabled: Boolean,
size: {
type: String,
default: "sm",
},
type: {
type: String,
default: "",
},
// 复选框的值
// NOTE: 如果checkboxGroup的v-model的值(数组)中包含checkbox的value的值,则复选框是选中状态,反之,则相反
value: {
type: [String, Number, Boolean],
default: undefined,
},
})
// 双向绑定数据变量
// NOTE: checkboxModel.value的值与<ue-checkbox></ue-checkbox>的v-model的值同步
const checkboxModel = defineModel({
type: [String, Number, Boolean],
default: "",
})
// v-model="model":model为true,说明是checkbox组件,且被选中了,然后在ue-checkbox-state.js中,isChecked又是根据这个model的值来确定的,所以v-model="model"跟isChecked是同步的
const { isDisabled, checkboxSize, isChecked, model, changeEvent } = useCheckbox({
props,
checkboxModel,
})
</script>
核心是将组件的v-model
绑定到统一的model
属性。
-
checkox
每个
<ue-checkbox>
组件在setup
阶段会调用useCheckbox()
,该函数接收props
和checkboxModel
两参数,checkboxModel
变量就是 元素上的v-model
属性的值。因为,在useCheckbox()
函数中,会执行useCheckboxGroup()
、useCheckboxModel()
和useCheckboxState()
三个函数。在useCheckboxModel()
函数中会将checkboxModel
参数传递过去,从而得到checkbox
的model
,具体逻辑可查看 use-checkbox.js代码块、 ue-check-model.js 代码块和 checkbox.vue 代码块。// use-checkbox.js import { useCheckboxState } from "./use-checkbox-state" import { useCheckboxGroup } from "./use-checkbox-group" import { useCheckboxModel } from "./use-checkbox-model" export function useCheckbox({ props, checkboxModel }) { const { isGroup, checkboxGroupKey } = useCheckboxGroup() const { model } = useCheckboxModel({ props, checkboxModel, checkboxGroupKey, isGroup }) const { isDisabled, checkboxSize, isChecked } = useCheckboxState({ props, model, checkboxGroupKey, isGroup }) return { isDisabled, checkboxSize, isChecked, model, } }
// ue-check-model.js import { computed } from 'vue' export function useCheckboxModel({ props, checkboxModel, checkboxGroupKey, isGroup }) { const model = computed({ get() { return isGroup ? checkboxGroupKey.checkboxGroupModel.value : checkboxModel.value }, set(val) { if (isGroup && Array.isArray(val)) checkboxGroupKey?.changeEvent?.(val) // 如果checkboxGroupKey存在,且changeEvent存在且是函数,则用val参数调用它 else checkboxModel.value = val } }) // console.log('model', model.value) return { model } }
-
checkboxGroup
checkboxGroup.vue 中使用
provide
共享了数据和方法,每个<ue-checkbox>
组件在setup
阶段会调用useCheckbox()
,该函数的执行就会去调用useCheckboxGroup()
,useCheckboxGroup()
方法中就inject
了<ue-checkbox-group>
组件提供的依赖provide
提供的数据:...toRefs(props)
、checkboxGroupModel
、changeEvent
,即checkboxGroupKey
,还会判断数据是checkboxGroup
的还是checkbox
的,根据是否在group
内决定数据的绑定方式(绑定到group
的model
还是自身的model
)。
<!-- checkboxGroup.vue -->
<template>
<div :class="[ns.b()]">
<slot></slot>
</div>
</template>
<script setup>
import { provide, toRefs } from "vue"
import { useNamespace } from "@ui-element-vue3/hooks"
import { CHECKBOX_GROUP_KEY } from "./constant"
defineOptions({ name: "ue-checkbox-group" })
const ns = useNamespace("checkbox-group")
const props = defineProps({
size: {
type: String,
default: "sm",
},
})
// 双向绑定数据变量
// NOTE: checkboxGroupModel.value的值与<ue-checkbox-group></ue-checkbox-group>的v-model的值同步
const checkboxGroupModel = defineModel({
type: Array,
default: () => [],
})
const changeEvent = async value => {
checkboxGroupModel.value = value
}
provide(CHECKBOX_GROUP_KEY, {
...toRefs(props),
checkboxGroupModel,
changeEvent,
})
</script>
<ue-checkbox-group size="lg" v-model="valueGroup">
<ue-checkbox>吃饭</ue-checkbox>
<ue-checkbox>睡觉</ue-checkbox>
</ue-checkbox-group>
// use-checkbox-group.js
import { inject } from "vue"
import { CHECKBOX_GROUP_KEY } from "../src/constant"
export function useCheckboxGroup() {
const checkboxGroupKey = inject(CHECKBOX_GROUP_KEY, undefined) // checkboxGroupKey是provide函数的参数:props和checkboxGroupModel数据
const isGroup = checkboxGroupKey !== undefined
return {
isGroup,
checkboxGroupKey
}
}
// use-checkbox-model.js
import { computed } from 'vue'
export function useCheckboxModel({ props, checkboxModel, checkboxGroupKey, isGroup }) {
const model = computed({
get() {
return isGroup ? checkboxGroupKey.checkboxGroupModel.value : checkboxModel.value
},
set(val) {
if (isGroup && Array.isArray(val)) checkboxGroupKey?.changeEvent?.(val) // 如果checkboxGroupKey存在,且changeEvent存在且是函数,则用val参数调用它
else checkboxModel.value = val
}
})
// console.log('model', model.value)
return {
model
}
}
全选的交互逻辑:用户勾选“全选”按钮或勾选所有复选框。即,以用户勾选的个数与选项总和为判断条件,如果勾选的个数与选项总和相等,表示全选,否则表示部分勾选或未勾选。因此,首先需要获取所有选项,将其存储为临时数据,供后期用户勾选时进行比较,以显示正确的状态。
要存储除“全选”复选框以外的选项数据,就需要获取上的v-model
属性的值,在 checkboxAll.vue 组件中,提供了一个依赖provide
共享了v-model
、props
和其他数据。然后在哪里会获取注入这些数据呢?每个<ue-checkbox>
组件在setup
阶段会调用useCheckbox()
,该函数的执行就会去调用useCheckboxGroup()
,useCheckboxGroup()
方法中就inject
了<ue-checkbox-all>
组件提供的依赖provide
提供的数据:...toRefs(props)
、 allModel
、 changeEvent
、setValuesEvent
,即checkboxAllKey
。只要checkboxAllKey
不是undefined
,就是全选组件状态。然后在 use-checkbox-model.js 中会根据是否是复选框组进行判断后给出model
的值。
composables 是 Vue3 的一个组合式 API,用来封装和复用有状态逻辑的函数。它是一种设计模式,允许开发人员将可复用的逻辑抽象成单独的函数,这些函数可在组件之间共享。
定义模块 -> 应用模块
- use-checkbox.js
- use-checkbox-state.js
- use-checkbox-group.js
- use-checkbox-model.js
- use-checkbox-event.js
- 首先,当 CheckboxAll 组件初始化时:
- 创建了
checkAll
(ref(false)
) 用于控制全选框的选中状态 - 创建了
allModel
(defineModel
) 用于存储所有选中的项 - 创建了
indeterminate
(ref(false)
) 用于控制半选状态 - 创建了
list
(ref([])
) 用于收集所有可选项的值 - 通过 provide 注入了
CHECKBOX_ALL_KEY
,向下传递了allModel
、chengEvent
和setValuesEvent
等
- 当子 Checkbox 组件被渲染时:
- 每个子 Checkbox 组件通过
useCheckboxGroup
获取注入的CHECKBOX_ALL_KEY
- 在
useCheckboxModel
中,发现自己是子复选框(通过判断isAll && !props.all
),就会调用setValuesEvent
将自己的 value 添加到父组件的list
数组中 - 此时
list
数组就收集到了所有可选项的值
- 当用户点击全选框时:
- 触发
handleAll
函数 - 如果选中全选框(val 为 true),则将
allModel.value
设置为list.value
(即选中所有选项) - 如果取消全选框(val 为 false),则将
allModel.value
设置为空数组(即取消所有选中) - 同时将
indeterminate
设置为 false(清除半选状态)
- 当用户点击单个复选框时:
- 复选框值改变,触发
useCheckboxModel
中的 computed 的 set 方法 - 因为是组内复选框,所以调用
checkboxAllKey.chengEvent
chengEvent
函数执行:- 更新
allModel.value
为新的选中值数组 - 调用
changeAllEvent
更新全选框状态 - 触发 change 事件向外部通知变化
- 更新
- 在
changeAllEvent
中:
- 计算当前选中的数量
checkedCount
- 如果选中数量等于总数量,将
checkAll.value
设为 true(显示全选) - 如果选中数量大于0但小于总数量,将
indeterminate
设为 true(显示半选) - 如果选中数量为0,则
checkAll
和indeterminate
都为 false
整个流程形成了一个完整的循环:
全选框选中/取消 ➡️ handleAll ➡️ 更新 allModel ➡️ 子复选框状态更新
子复选框选中/取消 ➡️ useCheckboxModel ➡️ chengEvent ➡️ changeAllEvent ➡️ 更新全选框状态
通过这种设计:
- 子复选框在初始化时自动向父组件注册自己
- 全选框状态和子复选框状态始终保持同步
- 支持全选、取消全选、半选等所有状态
- 状态变化时可以向外部通知
- 使用组合式API和依赖注入使得代码结构清晰,易于维护
Switch 组件通常用于表示一个二进制选择,例如打开/关闭、启用/禁用等。在组件设计中,该组件主要包含“基础”、“主题”、“文字”、“图标”、“加载”、“尺寸”、“”等类型,并且需要实现开关切换过程的过渡动画效果。
Important
实现逻辑:**通过原生 input 元素的 checkbox 类型和原生 button 按钮去实现 Switch 组件。**原生 button 按钮是 Switch 组件中间的“白色圆”。
为了主题色能够复用,需要将主题色、尺寸、大小等公共属性抽离到单独的文件,作为变量的形式引用,使 UI 组件库根据变量的变化而变化。
sass:map 是 Sass 提供的一种数据结构 map,用于存储键值对。Sass 的 map 常常被称为数据地图,因为他总是以 key:value 成对的出现,Sass 的 map 与 JSON 相似。它类似于其他语言中的字典或哈希表。
// packages/theme/src/common/var.scss
@use "sass:map";
@use "sass:color";
$types: primary, success, warning, error;
// 主题色变更
$colors: () !default;
// 主题色
$colors: map.deep-merge(
(
"white": #ffffff,
"black": #000000,
"primary": (
"base": #238be6,
),
"warning": (
"base": #fcc418,
),
"success": (
"base": #22c997,
),
"error": (
"base": #ff6b6b,
),
),
$colors
);
$color-white: map.get($colors, "white");
// 文字颜色
$text-color: () !default;
$text-color: map.deep-merge(
(
"primary": #000,
),
$text-color
);
// 字体大小
$font-size: () !default;
$font-size: map.deep-merge(
(
"sm": 14px,
"md": 16px,
"lg": 18px,
"xl": 20px,
),
$font-size
);
// 控件大小
$component-size: () !default;
$component-size: map.deep-merge(
(
"sm": 36px,
"md": 42px,
"lg": 50px,
"xl": 60px,
),
$component-size
);
// 生成主题层次色
@mixin set-light-color($type, $number, $mode, $mix-color) {
$colors: map.deep-merge(
(
$type: (
"base": "#238BE6",
"#{$mode}-#{$number}": color.mix($mix-color, map.get($colors, $type, "base"), $number *
10),
),
),
$colors
) !global;
}
@each $type in $types {
@for $i from 1 through 9 {
@include set-light-color($type, $i, "light", $color-white);
}
}
@debug map.get($colors, "primary");
$color-primary: map.get($colors, "primary", "base");
:root 伪类可以定义 CSS 全局变量,通过 var 使用定义的全局变量。我们可以通过手动定义变量的方式去给组件赋予样式,但效率特慢,尤其是“层次”颜色,数量会有几十种。因此推荐采用自动生成的方式来处理,如 Sass 的合并、混入、mix 等方法。
// packages/theme/src/index.scss
@use "./common/var.scss" as *;
@use "./isLoading.scss";
@use "./button.scss";
@use "./buttonGroup.scss";
@use "./common/config.scss";
:root {
--ue-color-white: #{$color-white};
--ue-color-primary: #{$color-primary};
}
定义“主色”、“层次色”等变量需要规划成统一格式,如 "--ue" 中的 “ue” 是整个 UI 组件库的前缀,这是前期就定义好的,因此现在也需要定义相同的规则。
// packages/theme/src/config.scss
// 定义的变量与hook/use-namespace的命名规则完全一致,全包UI组件库定义CSS类名规则的统一性
$namespace: "ue" !default; // 前缀
$connect: "-" !default; // 块、子集
$element-connect: "__" !default; // 元素
$modifier-connect: "--" !default; // 修改器
$modifier-value-connect: "_" !default; // 修改器的值
$state-prefix: "is" !default; // 状态前缀
// packages/theme/src/function.scss
@use "./config.scss" as *;
// 生成主题色变量
@function createVarName($list) {
$name: "--" + $namespace;
@each $item in $list {
@if $item != "" {
$name: $name + "-" + $item;
}
}
@return $name;
}
// packages/theme/src/mixins.scss
@use "sass:map";
@use "./function.scss" as *;
@use "./var.scss" as *;
// 生成主题色
@mixin set-main-color() {
@each $type in $types {
$color: map.get($colors, $type, "base");
#{createVarName(('color', $type))}: #{$color};
}
}
// 生成层次色
@mixin set-main-light-color() {
@each $type in $types {
@for $i from 1 through 9 {
$color: map.get($colors, $type, "light-" + $i);
#{createVarName(('color', $type, 'light', $i))}: #{$color}; // --ue-color-primary-light-1
}
}
}
// packages/theme/src/index.scss
@use "./common/var.scss" as *;
@use "./common/mixins.scss" as *;
@use "./isLoading.scss";
@use "./button.scss";
@use "./buttonGroup.scss";
@use "./common/config.scss";
:root {
@include set-main-color(); // 生成主题色
@include set-main-light-color(); // 生成层次色
}
:root 目前生成了大量的全局变量,此时可以直接使用变量名称。但如果直接这么使用,还是要写 “--a” 前缀,如果变量名称非常多的情况下,可能无法知道有哪些可以使用。因此,可以定义方法通过传参的方式来获取 :root 的变量名称。
在FormItem
中:
校验功能的实现是使用async-validator
校验库,同Ant Design
和Element
一样。async-validator
校验库采用key/value
的形式定义校验规则,那么在Form
表单中,可以这样传输数据:交互控件通过插槽的形式渲染,控件的v-model
所绑定的数据也都不一样,因此可以将v-model
绑定的数据传入到FormItem
中,这样就可以进行校验了,因为我们是打算在FormItem
中去执行校验操作的。
具体实现逻辑:在FormItem
组件中定义validate
方法用于校验数据,然后将该校验方法通过provide
方法暴露出去,在input
组件中通过引入useFormItem
钩子获取到FormItem
暴露出来的validate
方法。关于input
组件数据的校验场景有两个:1. 输入过程中校验数据;2. 鼠标失去焦点后校验数据。因此在useEvent
函数中添加一个afterBlur
函数参数,以及通过watch
函数去监听input
的值modelValue
是否有变化,如果有,就执行数据校验。
Note
async-validator:一个用于表单异步校验的库。
在Form
中:
校验规则rules
可以放在FormItem
标签上(校验逻辑如前所述),也可以放在Form
标签上。要做到在Form
组件上去校验表单数据,需要将FormItem
的所有字段传给Form
,这里采用的方法是:在Form
中通过provide
去提供一个pushField
方法(为了获取FormItem
所有字段),FormItem
一挂载,只要有name
属性,就将FormItem
所有属性,即props
,通过pushField
添加进modelFields
中,当然还有validate
、reset
这两个校验和重置的方法,它们也需要在Form
组件中去被使用的。此时,在Form
中就可以进行校验的工作了。
校验功能的实现是直接使用FormItem
提供的validate
校验方法。具体逻辑:在执行校验前,需要先明确哪些字段是需要做校验的。因此要先进行字段过滤,将需要检验的字段过滤出来,然后通过for
循环去一个一个的执行validate
校验,因为FormItem
提供过来的字段数据内都会有FormItem
标签上的属性、validate
方法和resetField
方法。Form
组件之需要对校验结果进行返回即可,成功则返回true
,失败则返回false
。
通过采用createVNode
函数去生成虚拟节点的方式去渲染Message
组件,其中涉及到render
函数等内容。同时使用transition
组件去渲染Message
组件从出现到离开的过渡动画。
createVNode
和transition
都是Vue.js 3
中的API
useResizeObserver
函数解读:
useResizeObserver(target, callback, options?)
:
target
:单个或多个 DOM 元素,或其ref/computed
引用callback(entries, observer)
:尺寸变更的回调,入参为ResizeObserverEntry[]
options.box
:观察盒模型类型,'content-box' | 'border-box' | 'device-pixel-content-box'
,默认content-box
useResizeObserver(messageRef, entries => {
const entry = entries[0]
height.value = entry.contentRect.height // message元素内容矩形的高度
})
Modal
组件位于遮罩层Mask
组件上面,因此要实现Modal
组件,可以将Modal
组件作为插槽内容插入到Mask
组件内。然后要注意z-index
的使用,让Modal
组件显示在Mask
组件上层。
teleport
组件:允许开发人员将teleport
组件包裹的子级组件“传送”给指定的 DOM 元素,用于确保被传送的元素不受父元素的样式或层叠上下文的错误影响。
<teleport to="body">
<transition>
<ue-mask>
<div>
...
</div>
</ue-mask>
</transition>
</teleport>
说明:
虽然 标签写在 Modal 组件的模板里,但其子内容(过渡 + 遮罩 + 弹窗)最终会被渲染并附加到
document.body
下,而不受父组件的定位、溢出裁剪、z-index
堆叠上下文影响。
beforeChange异步函数
UMD (Universal Module Definition) 打包是一种将 Javascript 库或模块打包成可以在不同环境中使用的通用格式的方法。UMD 打包同时兼容 CommonJS、AMD 和全局变量的使用方式,因此可以在项目的<script>标签中引入通过 UMD 打包的产物,直接在浏览器中以访问全局变量的方式使用。
在build
目录下,执行打包命令:node ./src/umnBuild.js
,得到:
-
flori-ui/dist/index.full.js
:打包生成的 umd 格式组件包 -
flori-ui/dist/index.css
:打包生成的 umd 格式组件包的样式
在text.html
文件中引入这两个文件,即可在浏览器中测试该 umd 组件包是否可用。
UMD 包属于全量模式打包,也就是将所有的组件打包为一份 JS 文件,通过在浏览器中使用 <script> 标签引入组件。经过 UMD 打包的文件大,并且无法支持按需加载。为了使打包的组件库支持按需加载模式,需要使用 ESM 和 CJS 打包模式实现按需加载,也可在打包过程中实现 Tree shaking(去除 JS 中无用的代码)优化。
需要安装 gulp
和 gulp-sass
两个库(pnpm i gulp gulp-sass --save-dev
)
全量打包 CSS 是指将所有组件的 css 文件合并为一个单独的文件。全量打包的优势在于减少 HTTP 请求次数,提高页面加载速度,并简化管理和部署过程。然而,全量打包 CSS 可能导致文件体积过大,反而影响网页性能。因此,全量打包时要考虑代码压缩和优化。
build/styleBuild.js
:
// 全量打包scss
const buildScssFull = async () => {
const sass = gulpSass(dartSass) // gulpSass支持编译scss
await new Promise((resolve) => {
gulp.src(`${pkgRoot}/theme/src/index.scss`) // 指定打包入口
.pipe(sass.sync()) // 编译
.pipe(autoprefixer({ cascade: false })) // 浏览器兼容,自动根据使用的css属性添加-webkit-、-ms-等等
.pipe(cleanCSS()) // 压缩css
.pipe(gulpConcat('index.min.css')) // 合并到指定文件
.pipe(gulp.dest(outputUmd)) // 输出到指定目录dist // NOTE: 全量打包后文件
.on("end", resolve) // 监听流完成
})
}
export const buildStyle = async () => {
await Promise.all([buildScssFull(), buildScssModules()]) // 所有任务都会并行执行,提高效率。如果任何一个构建任务失败,整个构建过程就会失败
}
按需打包和全量打包的方法类似,当然也有细微区别,具体代码如下:
// 按需打包scss
const buildScssModules = async () => {
const sass = gulpSass(dartSass)
await new Promise((resolve) => {
gulp.src(`${rootDir}/packages/theme/src/**/*.scss`)
.pipe(sass.sync()) // 编译
.pipe(autoprefixer({ cascade: false })) // 浏览器兼容,自动根据使用的css属性添加-webkit-、-ms-等等
.pipe(cleanCSS()) // 压缩css
// .pipe(gulpConcat('index.min.css')) // 合并到指定文件
.pipe(gulp.dest(`${outputDir}/theme`)) // 输出到指定目录theme // NOTE: 按需打包后文件
.on("end", resolve) // 监听流完成
})
deleteFiles() // 清理旧文件
}
// 删除指定文件或文件夹
const deleteFiles = async () => {
await deleteAsync(
[`${outputDir}/theme/index.css`, `${outputDir}/theme/common`], // 删除全量打包文件和公共样式目录
{ force: true } // 强制跨越当前目录删除文件
)
}
Tip
Promise.all()
:所有任务都会并行执行,提高效率。如果任何一个构建任务失败,整个构建过程就会失败
最后,打包UI组件库的命令为:pnpm run start
(注意是在 build
文件夹下执行),或在项目根目录下执行:pnpm build
。
-
进入打包后的
flori-ui
组件库文件夹内,执行npm login
,登陆 npm 平台,会自动跳转至 npm 官网,正常登陆即可。查看 npm 镜像源,使用不同的镜像源会导致登陆进不同的平台,如淘宝 npm 镜像、阿里云 npm 镜像等,为了发布至官方 npm 平台,需要先查看镜像地址:
npm get registry
,只要返回的不是https://registry.npmjs.org/
,就需要设置镜像地址为:npm config set registry=https://registry.npmjs.org/
,然后执行npm login
进行登陆。 -
登陆成功后,执行
npm publish
发布。
登陆过程日志:
npm login
npm notice Log in on https://registry.npmjs.org/
Login at:
https://www.npmjs.com/login?next=/login/cli/4999aa07-7b28-4bf6-903c-5d0a918b0d0e
Press ENTER to open in the browser...
Logged in on https://registry.npmjs.org/.
Caution
npm 有 24 小时的限制不允许重复发布同一个版本号/同一个包名。
发布成功的输出:
pnpm i flori-ui
或npm i flori-ui
只需在main.js
文件内引入组件库flori-ui
和其全量样式flori-ui/dist/index.min.css
,然后就可以在.vue
文件内直接使用组件。
main.js
文件:
import { createApp } from 'vue'
import App from './App.vue'
import UeUI from "flori-ui"
import "flori-ui/dist/index.min.css"
createApp(App).use(Antd)
.use(UeUI)
.mount('#app')
.vue
文件:
<template>
<UeButton type="primary">主按钮</UeButton>
</template>
按需引入不需要在main.js
文件做引入,可直接在.vue
文件按需引入使用。引入组件的同时,也自动引入了该组件的样式。
<template>
<UeButton type="success">成功按钮</UeButton>
</template>
<script setup>
import { UeButton } from "flori-ui"
</script>
-
在根目录执行
pnpm build
对 UI 组件库进行打包,生成flori-ui
文件夹,这就是打包后的 UI 组件库。 -
进入
flori-ui
文件夹内,执行pnpm link
进行全局注册。 -
进入要演示的 vue 项目根目录,执行
pnpm link flori-ui
引入该组件库至 vue 项目内(一个软链接,可在node_modules
中查看)。
Tip
npm link
可以为任意位置的 npm 包与全局的node_modules
建立链接,在系统中做快捷映射,建立链接之后即可在本地进行模块测试。
pnpm-workspace.yaml
文件自动会生成overrides
部分:
packages:
- docs
- examples
- packages/*
overrides:
flori-ui: link:../../../../../../Library/pnpm/global/5/node_modules/flori-ui
其中的overrides
字段告诉 pnpm
在整个工作区中,无论哪个项目引用 flori-ui
包,都使用指定路径的版本,而不是从 npm
仓库下载。link:
前缀表示这是一个本地路径链接,
Sass 是 CSS 预处理器,它用一种新的语言为 CSS 增加了一些编程的特性,将 CSS 作为目标生成文件。
CSS 的缺陷:
- CSS 作为一种标记语言,自定义变量的使用不够灵活;
- 语法不够强大;
- 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。
feat
增加新功能fix
修复问题/BUGstyle
代码风格相关无影响运行结果的perf
优化/性能提升refactor
重构revert
撤销修改test
测试相关docs
文档/注释chore
依赖更新/脚手架配置修改等workflow
工作流改进ci
持续集成types
类型定义文件更改wip
开发中