ee-uix
v1.0.1-alpha.4
Published
基于 Vue3 的自适应 UI 框架
Maintainers
Readme
Vue 3 + TypeScript + Vite
组件开发
注意
- 重名函数在不同的 TS 文件中定义,输入 {函数名} 时可以选择从哪个文件夹中导入挺方便的。但是在入口的 index.ts 中 export * from 就不能写从两个有重名函数的文件中导出,否则会编译报错。可以针对在入口的 index.ts 中导出 文件名+重名函数 防止重名
组件需要输出类型
props
// ts
props: {
/** 特殊类型 */
user: Object as PropType<{
/** 用户名 */
account?:string,
/** 密码 */
password?: string
}>
}
// js
props: {
/** 特殊类型
* @type {import('vue').PropType<{ account?:string, password?: string }>}
*/
user: Object
}emits
// ts
emits: {
/** 点击事件
* @param msg - 事件参数
*/
click: (msg: string) => true,
}
// js
emits: {
/** 点击事件
* @param {string} msg - 事件参数
*/
click: (msg) => true,
}注意:显示定义了 emits,传递事件时就不会将事件函数显示在 $attrs 中,导致要透传给子组件时无法简单使用 v-bind="$attrs" 透传事件函数
解决方案:不注入 emits,类型上有提示就好
emits: {} as {
/** 点击事件
* @param msg - 事件参数
*/
click: (msg: string) => any,
}slots
// ts
slots: Object as SlotsType<{
/** 默认插槽 */
default: {
/** 插槽参数 */
msg: string
}
}>
// js
/** @type { SlotsType<{ default: { msg: string } }>} */
slots: {}ts 还是 js
使用 ts 还是 js 有如下特性
- ts 方便,且可以为插槽和插槽参数写入 jsdoc 注释
- js 发布后,slots 的类型会和 ts 一致,但是 slots 的 jsdoc 用于标注类型的注释也还在
综上所述,为了组件能更好的输出类型信息,应使用 ts 来编写组件定义代码
组件实例
props 特别是 emits, slots 需要用到组件实例类型时,由于组件正在定义,所以无法使用自身,如下 ? 不知道写什么
export default {
emits: {
click: (instance: ?) => true,
},
slots: {} as SlotsType<{
default?: { instance: ? }
}>
}此时需要使用继承来实现
// 正常定义组件
const instance = defineComponent({
props: {},
methods: {},
...
})
export default {
// 这里继承定义的组件
extends: instance,
// 再单独定义 emits 和 slots,类型使用 InstanceType<typeof instance>
emits: {
click: (instance: InstanceType<typeof instance>) => true,
},
slots: {} as SlotsType<{
default?: { instance: InstanceType<typeof instance> }
}>
}这样在正常定义的组件里,就是无法获得 emits 的代码提示
这个实现方式和泛型组件类似
模板透传插槽
当外层包装组件引用了子组件时,可以通过以下代码透传所有插槽到子组件内部
<template v-for="(_, key) in $slots" #[key]="data">
<slot :name="key" v-bind="data"></slot>
</template>递归组件透传
- 透传 props: 应当包含 $attrs, scoped, $props
computed: {
props() {
// 可以去掉 popup 属性
const { popup, ...props } = this.$props;
return { ...this.$attrs, [this.$.vnode.scopeId || '']: '', ...props };
}
}- 透传 emits: 无法像 props 一样传递,而需要在模板额外绑定
<!-- 父组件正常触发事件 -->
<parent @click="emit('event', event)">
<!-- 子组件触发事件时,向外抛出事件 -->
<parent @event="e => emit('event', e)" />
</parent>除此之外,emits 还有很多其它问题,全部总结如下
- 上面提到的,事件不能简单透传
- 在用于继承时并不能继承,导致 $emit('') 没有事件提示
- 对于 optionClick 这样的事件,this 会显示 onOptionClick 的变量且没有注释,实际 this.onOptionClick 就是 undefined
- $emit 不是即刻触发,不能异步,不能等待返回值,不方便追加逻辑处理
综上所述,emits 可以统一改成用 on 开头的 props,以上问题得到如下解决
- 可以通过 $props 简单透传
- 继承的 props 有代码提示
- on 开头的变量可以成功获取传递的 props,且变量有注释
- 即刻触发,可异步,可返回值
还有个没有提到的好处,在传递 prop 时,同样可以使用 emit 事件的绑定形式。以下两种绑定方式都可以绑定上事件
<parent @optionClick="" />
<parent :onOptionClick="" />ts问题
承接上一篇,虽然 ts 在组件定义上有着绝对优势,但是在代码实现上比起 js 有很多缺点
多类型提示问题
当一个变量具有两种类型,例如对象或函数 let a:(() => boolean) | { value: any }
js 中 a. 直接有函数类型和对象类型两种类型的提示
ts 中 a. 将无法提供类型提示,必须先断言其类型后才有提示
if (typeof a == 'function')
; // 这里 a. 有函数类型的提示
else
; // 这里 a. 有对象类型的提示可空类型
正常来说,在 js 中定义变量,无论是字符串,数字还是对象,都可以将其赋值为 undefined
ts 定义了类型必须加上 | undefined 才能赋值 undefined,这显然在定义类型时很臃肿
对象类型键问题
用到对象时,js 中经常会写 const obj = {},然后对其键赋值和获取
但是 ts 则必须显示指定 obj 的类型为 Record<string, any>
使用 Record 类型获取字段时,很多时候传过来的 key 不仅仅是 string 类型,而是 string | symbol 类型,在定义 Record 时为了全面,就要写成 Record<keyof any, any>,这无非也会给开发者带来心智负担
空判断
在 Vue 开发中,经常会有 v-if 等控制流程,进而让某些代码不会执行,执行了则某值一定不为空
在 js 中,直接使用该变量即可,因为流程上你确定该值不为空
在 ts 中,可能该变量的类型为可空类型,则编译会报错,你不得不通过加上 ! 或 ?. 来调用其内容
其它小问题
- “&=”运算符不允许用于布尔类型
a &= b必须写成a = a && b
泛型组件
从使用场景出发,以 Form 表单组件为例,泛型组件的目的如下
- props 可以配置字段,字段对应编辑器,验证规则等
- emits 在字段值改变时发出改变事件
- slots 每个字段都有相应插槽
可以得出如下结论
- 泛型 T 主要用于 props 字段
- 泛型 T 做泛型体操生成 emits 和 slots
假设泛型 T 的一个实例类型如下
props: {
form: {
a: { value: 0 },
b: { value: '' },
}
}
// 值类型为 type Value<T> = { value: T }在实现组件代码或渲染时,其实 value 的类型无论是 number 还是 string 都不影响,调用如下
// js 调用
for (const key in this.form) {
// this.form[key].value
}
// 模板渲染
<template>
<form>
<div v-for="item, key in form">{{ key }}: {{ item.value }}</div>
</form>
</template>所以在实现组件时 form 字段的类型用 Record<string, Value<any>> 就足够了
对于特别复杂的类型,可以写泛型函数做类型体操来让用户构建 form ,如下
function buildForm<T>(form: { [K in keyof T]: Value<T[K]> }) { return form }方便构建 form 字段后,还缺少了 emits 和 slots 的定义,缺少定义就只是缺少代码提示,但实际能用。为了让组件拥有类型体操生成的 emits 和 slots 定义,只能让组件为泛型组件
泛型组件方案 1
使用 vue3.4+ 新出的泛型方案 <script setup lang="ts" generic="T">
方案具体步骤:
- 使用 T 来定义 props 和通过泛型体操定义 emits 和 slots
import { eeform } from './ee-form.vue'
defineProps<{ form: T }>()
defineEmits<{
[K in keyof T]: [value: T[K]]
}>()
defineSlots<{
[K in keyof T]: (props: { value: T[K] }) => any
}>在模板渲染原本的 eeform 组件
<template><eeform /></template>,使用时传递的 props 和 attrs 都会传递进 eeform 组件使用泛型组件
<template><ee-generic-form :form="{ a: { value: 0 } }" /></template>
泛型组件问题
泛型组件的实现是写在非泛型的 eeform 中的,而实际使用是用的泛型组件
按照方案1实现的泛型组件还存在很多的问题
- 非泛型的 eeform 定义的额外 props, emits, slots 也需要在泛型类型里面定义一遍才能在使用泛型组件时获取完整的类型提示
- 渲染泛型组件时,还需要将插槽透传到非泛型组件中
- 泛型组件通过 ref 获得泛型组件实例时,也获取不到非泛型组件中的 data, computed, methods 等
泛型组件方案 2
使用泛型函数来运行时创建非泛型组件,没有 vue 版本要求,且直接解决泛型组件问题
方案具体步骤:
- 声明泛型函数来创建选项式组件
import { defineComponent, SlotsType } from 'vue'
import { eeform } from './ee-form.vue'
function buildForm<T>(form: { [K in keyof T]: Value<T[K]> }) {
return defineComponent({
// 继承直接解决所有问题
extends: eeform,
emits: {} as {
[K in keyof T]: (value: T[K]) => any;
},
slots: {} as SlotsType<{
[K in keyof T]: { value: T[K] };
} & ComponentSlots<typeof eeform>>,
mounted() {
// 给非泛型的 form 赋值,泛型和非泛型类型是兼容的
this.form = form;
}
})
}- 使用泛型组件
// 模板渲染:不再需要赋值 form
// <template><ee-form /></template>
import { buildForm } from 'ee-ui'
export default {
// 组件用泛型函数创建
components: { EeForm: buildForm({ a: { value: 0 } }) }
}可以看到,这种方案更简洁,且不存在方案 1 的问题。但是也存在另外的问题
- components 中的组件需要调用函数来创建,这种组件的引入方式是所有其它 ui 框架没有的,首次使用这种新颖的方式的开发者可能会有些不适应
- 组件会有个非泛型的方案,泛型组件虽然可以推断 emits 和 slots 的类型,但是并不能改变 props 的类型
泛型组件继承
继承时有以下几个特性
- 带 slots 的组件不能继承,就算能继承也不包含 slots
- emits 同样不能继承,且 emits 已经改为用 on 开头的 props 代替
- props 可以继承,泛型 props 可能用于推断出其它 props 的类型
综上所述,继承主要是 props,根据泛型组件方案2,泛型组件是由函数指定的
但如果泛型组件 A 要被另一个泛型组件 B 继承,且 B 无法确定 A 的泛型类型,需要构建 B 的时候传递 A 的泛型类型
并且泛型组件方案2的问题 2 中也指出了,props 的类型并没有改变,方案的泛型仅针对推断 emits 和 slots 时没什么问题
泛型组件方案 3 (推荐)
总结前面几种方案最终的问题及其解决方案
- 泛型 slots:泛型函数构建组件,组件继承非泛型带模板的组件
- 泛型 props:定义非泛型组件,使用 ComponentWithoutSlots 修改 props 的类型
泛型组件的使用场景
- 继承:对于框架内部,一般用于继承
- 使用:对于用户使用框架,一般直接使用
使用泛型组件的方式如下
- 函数构造:
eeFormWithGeneric().buildForm(),slots 有类型推断,传入的 props 有复杂类型 - 类型转换:
eeOptions as eeOptionsWithGeneric<string>,没有额外类型推断,不需传入 props,仅将已有 props 断言一个泛型类型
综上所述,定义泛型组件的步骤如下
- 实现非泛型组件:例如 eeOptions
const eeOptions = defineComponent({
...
})- 定义泛型组件构建函数 eeOptionsGeneric
import { ComponentWithoutSlots } from 'ee-uix'
export function eeOptionsGeneric<ModelValue>() {
return eeOptions as ComponentWithoutSlots<typeof eeOptions, { modelValue: ModelValue }>
}- 定义泛型组件类型 eeOptionsGenericType
import { ComponentWithoutSlots } from 'ee-uix'
export type eeOptionsTypeof<ModelValue> = ComponentWithoutSlots<typeof eeOptions, { modelValue: ModelValue }>继承组件小技巧
- 修改继承组件的 props 的默认值
beforeCreate() {
this.$nextTick(() => {
this.$.props.字段 = 默认值
})
}BUG
Vue
- defineComponent 不写 data 整个组件的 this 指针错误。解决方案 defineComponent 的 D 加上默认类型 = {} 即可
- this. 代码提示缺少 $props. 的内容
- props 定义在 data 前时,props 中的函数(ee-vuex 的 injectStore 会有函数)无法通过 this. 获取 data 中定义的字段。未知为什么有这样的错误,解决办法将 props 的定义放到 data 之后即可。
VSCode
模块开发流程
公共模块
- 通用 ts 类型体操:types/
- 通用 ts/js 实现:util/
- 通用组件接口:common/
- 通用指令:directives/
- 通用配置:config/ 包含全局仓库,i18n,样式主题等整体影响 ui 框架的配置内容
自动生成
- index.ts:每个文件夹层级下,都自动生成 index.ts,内容则是导入并导出当前文件夹下所有的 .js / .ts / .vue 以及子文件夹下 index.ts 中的所有模块
- 使用 TypeScript API 生成文档
组件库目录
指令
- v-class: 布局,动画等常用 css 样式
- v-loading: 指定元素上显示加载中样式
- v-placeholder: fixed 定位元素生成等尺寸占位符方便布局
- v-size: 侦听元素尺寸变化
- v-transition: 增加 transition 动画并侦听动画的开始和结束
表单
- form: 所有的表单推荐使用,除了指定表单内容外,考虑根据 js 对象自动选择合适的编辑器
- switch: 开关,boolean 类型推荐使用
- check: 选择,可单选可多选
- input: 输入框,可补全,考虑将 select 融入到一起
- input-number: 输入数字
- textarea: 多行输入框,可以考虑加入自定义富文本
- select: 下拉选择,可输入,可单选
- selection: 下拉选择,多选
- date: 选择年月日,可选范围
- time: 选择时分秒,可选范围
- date-time: 选择年月日时分秒,可选范围
- cascader: 级联选择器,用于省市县等选择
- item: 列表项,可以添加/删除一项,项的可编辑内容使用 form 指定
- upload: 上传
其它未确定的或作为框架扩展的控件
- tree-select: 树状选择器,可作为 selection 打开选择的样式
- rate: 评分
- slider: 滑块,选择数字范围
弹窗
- popup: 通用弹窗,带遮罩,带抽屉效果,显示内容带加载
- dialog: 对话框,等待用户确认
- loading: 加载,防止重复或中断操作
- notify: 通知,等待用户确认。感觉和 dialog 一样
- alert: 消息提示,自动消失
- tooltip: 提示框,针对指定元素进行悬浮提示
展示
- icon: 通用图标
- badge: 显示红点,未读消息
- calendar: 日历
- carousel: 走马灯,网站头部,商品头部用得多
- guide: 引导
- page: 分页,一般用于列表底部
- progress: 进度条
- skeleton: 骨架,在列表内容未加载出来前显示股价以占位
- table: 表格
- tag: 标签,一般展示分类属性
- timeline: 时间线
- tree: 树,考虑子树懒加载
- countdown: 倒计时
导航
- breadcrumb: 面包屑,页面层级目录
- contents: 目录,单页面纵向内容较长时可快速跳转
- menu: 菜单,左侧导航,上方横向导航均可使用
- steps: 步骤条,横向,竖向均可
- tabs: 页签,内容分类用得最多的导航
服务器成本
阿里云服务器
2vcpu 2g内存 40g硬盘 5年2.3折
- 按流量:833.84元+0.8元/g流量
- 包月:2006.84元(差额相当于1466.25g流量,每月24.4375g)
20g硬盘:674.24元+0.8元/g流量
公众号
认证费 = 300元/年 * 5 = 1500
域名
tomoyo.xin 5年 168元
备案
免费
https
未知
总结
启动资金:833.84服务器 + 100流量费预存 + 300公众号认证 + 168域名 = 1401.84 5 年花费:833.84服务器 + 500流量费 + 1500公众号认证 + 168域名 = 3001.84 最简:99服务器包年 + 12域名 + 300公众号认证 = 411
