效果
API 设计
先设计好了 API 写起来代码才不会犯迷糊
Toast(message: string; otherParams?: ToastParams): ToastReturn
interface ToastParams {
time?: number;
appendTo?: string | HTMLElement;
dangerouslyUseHTMLString?: boolean;
}
interface ToastReturn {
close():void
}
ToastParams 详解
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
time | Toast 存在的时长, 单位秒, -1 代表永不消失 | number | 2 |
appendTo | 将 Toast 放到那个 dom 下 | string 或 HTMLElement | document.body |
dangerouslyUseHTMLString | 是否将 message 解析为 html 片段 | boolean | false |
ToastReturn 详解
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
close | 关闭 Toast | Function | — |
难点说明
先说一下实现一个居中的 Toast 提示的基本思路:
div {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
有了这个核心样式 Toast 就有了。再继续往下做其实还需要俩步:
- 解决覆盖(遮挡)问题:需要放到顶级元素 body 下
- 如何用 js 代码进行这个组件的调用
先说第一个
fixed 的设置是有限制的,就是不能有任何祖先元素设置了 transform
、perspective
或者 filter
样式属性。也就是说如果我们想要用 CSS transform
为祖先节点 <div class="outer">
设置动画,就会不小心破坏模态框的布局!
z-index 受限于它的容器元素。如果有其他元素与 <div class="outer">
重叠并有更高的 z-index
,则它会覆盖住我们的模态框。
vue 中提供了 <Teleport>
来将元素插入到需要的元素上,实际上你也可以用这种方式来使用 ToastMessage.vue 组件。只是需要用 v-if 来控制
第二个问题
如何用 js 直接调用 ToastMessage.vue 组件呢?好像必须要写在 template 里面才行吧?这里其实就要用到一些高级技巧了:
import { createVNode, render } from "vue";
import ToastMessage from "ToastMessage.vue";
// 将组件变为 vnode 节点, 这个就是写 jsx 的时候的 h 方法
const vnode = createVNode(ToastMessage, ...);
// 将 vnode 节点挂载到浏览器的 dom 元素上
render(vnode, document.body)
这是关键的俩步,将这俩步封装成一个 Function 就可以四处调用了
注意事项
defineProps
我们平常使用 defineProps 可能是这个样子的:
const props = defineProps<{
message: string;
dangerouslyUseHTMLString?: boolean
}>();
基于上面的思路我就想到了把 defineProps 接受的类型单独拿出来变成这样方便处理,结果新的事情发生了:
// type.ts
export interface ToastMessageProps {
message: string;
dangerouslyUseHTMLString?: boolean
}
// ToastMessage.vue
import { ToastMessageProps } from "./type"
const props = defineProps<ToastMessageProps>();
这个时候就会出现下面的错误:
然后我又想到了思路二:
// ToastMessage.vue
export interface ToastMessageProps {
message: string;
dangerouslyUseHTMLString?: boolean
}
const props = defineProps<ToastMessageProps>();
这个时候其实类型 ToastMessageProps 对于 ts 来说是识别不到的,因为 .vue 文件在处理的时候是统一处理的。并没有其他类型, 可以看到 env.d.ts
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
export default component;
}
因此最后的实现方式是:
// type.ts
export const toastMessageProps = {
message: {
type: String,
default: "",
},
dangerouslyUseHTMLString: {
type: Boolean,
default: false,
},
};
export type ToastMessageProps = ExtractPropTypes<typeof toastMessageProps>;
// ToastMessage.vue
const props = defineProps(toastMessageProps);
采用了 vue 最原始的定义参数的方式,然后 vue 提供了 ExtractPropTypes 类型来帮你获得需要的类型,也是很方便的。
两个文件间的引用
目前我的文件划分是(也是正确的划分方式):
ToastMessage.vue // 实现 Toast 组件的地方
type.ts // 定义了 ToastMessage 相关的一些类型(props emit defineExpose)
index.ts // 实现非组件化调用 Toast 的地方
之前的文件划分方式:
ToastMessage.vue // 实现 Toast 的地方
index.ts // 实现非组件化调用 Toast 的地方 和 ToastMessage 的相关类型
这种方式就存在了下面的问题
// ToastMessage.vue
import { toastMessageProps } from './index.ts'
const props = defineProps(toastMessageProps);
// index.ts
import ToastMessage from "./ToastMessage.vue"
export const toastMessageProps = { ... };
function Toast( ... ) {
createVNode(ToastMessage, props);
}
简单而言就是 index.ts 依赖的导入 ToastMessage.vue 文件又依赖了 index.ts 的导出。这个时候就会出现:
其他逻辑
这里教一个小技巧,在 github 的项目界面按: SHIFT + . 可以进入 github.dev 网站,简单来说就是用浏览器端的 vscode 打开你的项目。
总结
这里主要是对于 vue 的 createVNode
render
的运用。然后还可以学到 ExtractPropTypes
这个类型处理的方法。当然了我走过的坑小伙伴们就别再走一次了
暂无评论内容