Skip to content

构建 Vue3 组件库 - 组件

发布于:

Table of contents

展开目录

目录

源码

组件注册

对于使用者来说,组件库可以批量导入,也可以单个导入,例如:

main.ts
// 批量导入
import { createApp } from "vue";
import App from "./App.vue";
import GeistDesign from "@whouu/geist-design";
 
createApp(App).use(GeistDesign).mount("#app");
 
// 单个导入
import { createApp } from "vue";
import App from "./App.vue";
import { GAvatar, GButton } from "@whouu/geist-design";
 
createApp(App).use(GAvatar).use(GButton).mount("#app");

app.use()

参考 官方文档app.use() 需要传递一个带 install() 方法的对象。

组件的入口文件:

src/avatar/index.ts
import Avatar from "./avatar.vue";
 
export const GAvatar = () => {
  Avatar.install = () => {};
 
  return Avatar;
};
 
export default GAvatar;

组件库的入口文件:

src/index.ts
const install = () => {};
 
export default { install };

app.component()

正是因为要通过 app.component() 注册组件,组件的名称尤其重要,参考 此处

在上面两个 install 方法中,我们调用 app.component() 将组件库中的组件注册到使用者的 Vue 实例中。

组件的入口文件:

每一个组件都需要 installWrapper,因此单独提取该方法:

src/_utils/index.ts
import type { App, Directive, Component } from "vue";
 
export type Install<T> = T & {
  install(app: App): void;
};
 
/**
 * 注册组件
 *
 * @param { Object } main 组件实例
 * @returns { Object } 组件实例
 */
export const withInstallComponent = <T extends Component>(
  main: T
): Install<T> => {
  (main as Record<string, unknown>).install = (app: App): void => {
    const { name } = main;
    name && app.component(name, main);
  };
  return main as Install<T>;
};
src/avatar/index.ts
import Avatar from "./avatar.vue";
import { withInstallComponent } from "@/_utils";
 
export const GAvatar = withInstallComponent(Avatar);
 
export default GAvatar;

组件库的入口文件:

src/index.ts
import * as components from "./components";
 
import type { App } from "vue";
 
const install = (app: App): App => {
  Object.entries(components).forEach(([key, value]) => {
    app.component(key, value);
  });
 
  return app;
};
 
export default { install };

目录结构

为了更好的维护和导出 props 的类型文件,将 props 单独抽离成一个 .ts 文件,emits 同理。

avatar
 ┣ __test__
 ┃ ┗ avatar.spec.ts
 ┣ src
 ┃ ┣ avatar.vue
 ┃ ┣ interface.ts
 ┃ ┗ props.ts
 ┗ index.ts

官方文档中,定义一个带有默认值的 props,大概是这样:

avatar.vue
<script setup lang="ts">
  export interface Props {
    msg?: string;
    labels?: string[];
  }
 
  const props = withDefaults(defineProps<Props>(), {
    msg: "hello",
    labels: () => ["one", "two"],
  });
</script>

如何将 withDefaults defineProps Props,三者抽离到 props.ts 中,并像这样使用:

avatar.vue
<script setup lang="ts">
  import { Props } from "./props";
 
  defineProps(Props);
</script>

参考复杂的 prop 类型,结合 PropTypeas const

props.ts
import type { PropType } from 'vue'
 
export const props = {
  booleanProp: Boolean,
  stringNumberBooleanProp: {
    type: [String, Number, Boolean] as unknown as PropType<string | number | boolean>,
    default: false
  },
  complexArrayProp: {
    type: Array as unknow as PropType<(string | number | boolean)[]>,
    default: () => []
  }
  enumStringProp: {
    type: String as unknown as PropType<'large' | 'medium' | 'normal' | 'small'>,
    default: 'normal'
  }
} as const

ExtractPropTypes

某些场景,可能需要在 ts 中组装组件的 props,而后直接使用 v-bind 进行传递:

example.vue
<script setup lang="ts">
  /**
   * 此时 ts 无法进行静态类型检查,
   * 你也不清楚自己写的 props 是否和开发者定义的是否匹配
   */
  const avatarProps = {};
</script>
 
<template>
  <g-avatar v-if="avatarProps" v-bind="avatarProps"></g-avatar>
</template>

在 props.ts 中,使用 ExtractPropTypes 导出 SFC props 类型:

props.ts
+ import type { ExtractPropTypes, PropType } from 'vue'
 
export const props = {
  booleanProp: Boolean,
  stringNumberBooleanProp: {
    type: [String, Number, Boolean] as unknown as PropType<string | number | boolean>,
    default: false
  },
  complexArrayProp: {
    type: Array as unknow as PropType<(string | number | boolean)[]>,
    default: () => []
  }
  enumStringProp: {
    type: String as unknown as PropType<'large' | 'medium' | 'normal' | 'small'>,
    default: 'normal'
  }
} as const
 
+ export type AvatarProps = ExtractPropTypes<typeof Props>

再回到 example.vue

example.vue
<script setup lang="ts">
+  import type { AvatarProps } from '@whouu/geist-design'
 
+  const avatarProps: AvatarProps = { size: 'medium' }
</script>
 
<template>
  <g-avatar v-if="avatarProps" v-bind="avatarProps" />
</template>