likes
comments
collection
share

从0到1搭建自己的组件库,详解附源码

作者站长头像
站长
· 阅读数 44

前言

项目GitHub源码地址

项目源码:github.com/tianshiyang…

为什么要搭建自己的组件库?

    其实,在咱们的工作中,不免出现同时维护很多项目的情况。这时候,假如UI设计师,设计了几个通用组件,需要在各个系统间使用。这种情况,我们总不至于在每个系统都单独维护一份这个组件吧!假如真的这样做了,假如有一天,UI设计师换了个人,说之前的组件样式不好看,想换个样式!我的天,这时候我们要同时修改很多项目组的项目,这种情况,岂不是狗看了都摇头!至此,拥有一个自己的(或公司级)的组件库显得至关重要!

从0到1搭建自己的组件库,详解附源码

技术栈选型

    在这里,我们以ElementPlus为基础,对组件进行二次封装,使其满足我们的业务需求。这里引入一个知名学者提出的一句话     我们尽可能使用已经存在的轮子,如果轮子不存在,我们再发明轮子 --知名学者     对此,我们的技术栈主要如下:

  • Vue3 + TypeScript
  • ElementPlus

目标

项目目标

  • 将组件库发布到NPM官网上,可以让各个组件库通过npm install的方式引入
  • 可以像ElementPlus一样,在给组件传递参数不满足期望类型的时候,增加TS报错提示
  • 搭建一个属于自己组件库独有的官网
  • 可以像ElementPlus一样,做到组件库可以按需引入(重中之重!!!)

个人目标

  • 能从0到1搭建自己组件库
  • 学会通过使用依赖注入,实现一父多子的效果(不要慌,后面会进行讲解~)
  • 学会如何做到按需引入(当然,这也在后面的讲解中~)

说了这么多,下面就翻开我们的《武功秘籍》的第一页

从0到1搭建自己的组件库,详解附源码

第一节 - 创建、改造项目

    首先我们先创建一个Vue3+ElementPlus+TypeScript的项目,这里建议使用VueCli进行创建,也没别的意思,就是方便,需要的核心配置项如下

从0到1搭建自己的组件库,详解附源码 重点!因为我们做的是通用型组件库,即他并不包含业务属性,所以用不到RouterVuex, 当然,如果你要做的是业务组件库的话,那你可以根据自己的需求进行选择

创建完成之后,我们对项目进行下述修改:

  • 将原来的src目录变成examples,这个examples目录就是对咱们新封装的组件进行查看效果测试的地方,可以理解为,他就是我们将要使用这个组件库的业务系统
  • 新增packages目录,此目录用于存放咱们封装的公共组件

    如果你按着如上操作一步步做的话,是不是发现项目报错了呢,报错信息为:找不到main.js,接下来,我们更改vue.config.js

const { defineConfig } = require('@vue/cli-service')
const path = require("path")

module.exports = defineConfig({
  transpileDependencies: true,
  // 修改pages入口
  pages: {
    index: {
      entry: "examples/main.ts",
      template: "public/index.html",
      filename: "index.html"
    }
  },
  outputDir: "lib", // 将文件打包到lib目录,默认为dist
  // 扩展 webpack配置
  chainWebpack: (config) => {
    config.resolve.alias
      .set("~", path.resolve("packages"))
      .set("@", path.resolve("examples"))
  }
})

更改完之后,我们的项目结构如下

从0到1搭建自己的组件库,详解附源码

之后,我们引入ElementPlus, 为什么我们要引入ElementPlus呢?因为我们的组件库是针对ElementPlus进行二次封装的,通过对他的二次封装,更好的满足我们的业务,提高开发效率。当然,如果你的组件,使用不到ElementPlus的话,可以选择不安装。安装教程在此就不详述了,如果不知道如何安装的话,可以参考ElementPlus官网

第二节 用Vue插件的形式,书写一个组件

其实,不管咱们引入什么样的组件库,或者Vue生态的哪个包,本质上,都是在main.js中的app.use()方法中,执行了组件库或者生态包的install方法,这里引入Vue3官网app.use()中对app.use()作用的解释


插件可以是一个带 install() 方法的对象,亦或直接是一个将被用作 install() 方法的函数。插件选项 (app.use() 的第二个参数) 将会传递给插件的 install() 方法


接下来,就根据Vue3对app.use()的解释,我们本地创建一个“插件”

在项目的packages中,我们创建一个components目录typings目录,components目录用于存放我们的组件,在typing目录中,存放我们项目需要的公共TS类型等。接着我们在components中创建一个组件,这里笔者简单写了一个MyButton组件,这里,我们创建一个MyButton目录,目录下创建一个src文件夹,用来存放我们的组件源码,组件源码内容如下

从0到1搭建自己的组件库,详解附源码 接着在和存放组件src目录同级的地方创建一个index.ts,目的是用于导出组件,代码如下

import MyButton from "./src/MyButton.vue"
export default MyButton

接着,在packages的根目录下,创建index.ts用于导出和install函数和注册组件,具体代码如下

import { App } from 'vue'
import MyButton from './components/MyButton/index'

// 所有组件列表
const components = [
  MyButton
]

// 定义 install 方法, App 作为参数
const install = (app: App): void => {
  // 遍历注册所有组件, app.component为Vue注册全局组件的API
  components.map(component => app.component(component.name, component))
}

// 导出所有template形式组件
export default {
  install
}

最后,在typing目录中,创建shims-vue.d.ts,这个文件是固定这样的写法,用于导出本组件库的类型说明,要不然在安装组件库的时候,会报组件库找不到的错误

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<Record<string | number | symbol, unknown>, Record<string | number | symbol, unknown>, unknown>
  export default component
}
declare module 'custom-ui-plus'; // 就是这行,创建了组件库的声明文件

package.json中增加.d.ts文件的声明

从0到1搭建自己的组件库,详解附源码

这里的lib是打包后生成的目录

因为上述的讲解中,目录用到的比较多,在此笔者放入一个目录结构截图

从0到1搭建自己的组件库,详解附源码

最后!我们在examplesmain.js中,引入我们的组件库试试吧!

import { createApp } from 'vue'
import App from './App.vue'
import { ElButton } from "element-plus"
import 'element-plus/dist/index.css'
// 这里就是我们自己创建的,但是还未发布到NPM的组件库
import customUIPlus from "../packages/index" 

createApp(App).use(ElButton).use(customUIPlus).mount('#app')

剩下的可以在examples中,像写平时业务代码一样,写一个测试的demo,这里笔者直接写在了app.vue

<template>
  <!-- 因为我们是在main.js全量引入了组件库,并挂载到了app实例上,所以我们这里可以直接使用 -->
  <!-- 后面会讲解按需引入 -->
  <my-button></my-button>
</template>

<script lang="ts" setup>
</script>

效果如下:

从0到1搭建自己的组件库,详解附源码

配置打包

build目录下新建立rollup.config.ts 这里,我们使用打包工具rollup,原因是rollup更适合去打包生成EsModule,也就是我们常用的ES6import的形式

import { nodeResolve } from '@rollup/plugin-node-resolve'
import babel from 'rollup-plugin-babel'
import path from 'path'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2'
import pkg from '../package.json'
import tsConfig from "../tsconfig.json"
import scss from "rollup-plugin-scss"; // 解析scss
// eslint-disable-next-line @typescript-eslint/no-var-requires
const vue = require('rollup-plugin-vue')
import { writeFileSync, existsSync, mkdirSync } from "fs";

export default [
  {
    input: path.resolve(__dirname, '../packages/index.ts'),
    output: [
      {
        name: "custom-ui", // 打包完之后的名称
        format: 'es', // 打包的格式为EsModule
        file: pkg.module, // 这里也可以写死为index.esm.min.js,为了保持和package一致,所以引用了package.json文件
        // sourcemap: true // sourcemap bug调试的时候打开
      }
    ],
    plugins: [
      vue({
        target: 'browser',
        css: false,
        exposeFilename: false
      }),
      // VueJsx(),
      scss({
        output: function (styles) {
          if (!existsSync("lib/")) {
            mkdirSync("lib/");
          }
          writeFileSync("lib/index.css", styles);
        },
      }),
      babel({
        exclude: 'node_modules/**', // 只转译我们的源代码
        runtimeHelpers: true
      }),
      terser(), // 压缩
      nodeResolve(),
      typescript({
        tsconfigOverride: {
          compilerOptions: {
            declaration: true // 是否创建 typescript 声明文件
          },
          include: tsConfig.include,
          exclude: tsConfig.exclude
        }
      })
    ],
  }
]

最后我们在package.json中创建命令

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "build:lib": "rollup --config ./build/rollup.config.ts" // 新增这个
  },

测试打包

从0到1搭建自己的组件库,详解附源码

从0到1搭建自己的组件库,详解附源码

OK~没什么问题

发布NPM

发布NPM是个很简单的事儿,网上也有很多相关的文章,但是,这里有一个值得一提的东西 -- .npmignore。这个文件是用来忽略不上传到NPM的文件,类似于.gitignore。如果不配置这个文件,上传NPM的时候,会把整个项目上传上去,这里我们期望的是,只上传打包后的lib目录,对此,.npmignore文件如下

# 忽略目录
.idea
.vscode
build/
docs/
examples/
packages/
public/
node_modules/
typings/
tests/

# 忽略指定文件
babel.config.js
.prettierrc
jest.config.js
tsconfig.json
tslint.json
vue.config.js
.gitignore
.browserslistrc
*.map

因网上发布包到NPM的教程比较多,过程也比较简单,在这儿就不详细讲解了,附上一篇文章给大家npm发布教程

按着上述步骤搞完之后,我们可以执行npm i custom-ui,然后可以发现已经可以从npm成功安装自己发布的包了

从0到1搭建自己的组件库,详解附源码

安装包增加typescript提示

若想有typescript提示,则需要先写一个ts类型错误时,报错的组件,这里,笔者对MyButton组件进行了简单的改造,代码如下

<template>
  <el-button type="primary">这是一个按钮,类型为{{ props.textType }}</el-button>
</template>

<script lang='ts'>
export default {
  name: 'MyButton'
}
</script>
<script lang='ts' setup>
import { defineProps, PropType } from "vue"
// 这里定一个一个类型,限制了props中的textType字段值,只能是1或者2中的一个,如果用户写了3,则报错
type textTyping = 1 | 2
const props = defineProps({
  textType: {
    type: Number as PropType<textTyping>,
    required: true
  }
})
</script>

因为我们在rollup.config.ts打包配置中,对ts的配置引入了tsconfig.ts中的includeexclude,关键代码如下

// rollup.config.ts
...
 typescript({
    tsconfigOverride: {
      compilerOptions: {
        declaration: true // 是否创建 typescript 声明文件
      },
      include: tsConfig.include, // 这里引用的是tsconfig中的include
      exclude: tsConfig.exclude // 这里引用的是tsconfig中的exclude
    }
  })
...

所以,我们修改tsconfig中的这两个值

// tsConfig
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "useDefineForClassFields": true,
    "sourceMap": true,
    "noImplicitAny": false,
    "baseUrl": ".",
    "types": [
      "webpack-env",
      "jest"
    ],
    "paths": {
      "@/*": [
        "packages/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "packages/**/*.ts",
    "packages/**/*.tsx",
    "packages/**/*.vue",
    "typings/**/*.ts",
    "typings/shims-vue.d.ts" // 这里引用了typing下的shims-vue.d.ts声明
  ],
  "exclude": [
    "node_modules",
    "examples",
  ]
}

最后,我们发布一版NPM试试吧! 当安装完我们的最新包之后,在examplemain.ts中增加对.d.ts文件的引入,代码如下

import { createApp } from 'vue'
import App from './App.vue'
import { ElButton } from "element-plus"
import 'element-plus/dist/index.css'
import customUI from "custom-ui-plus"
// 增加了.d.ts文件的引入
import "custom-ui-plus/lib/index.d.ts"

createApp(App).use(ElButton).use(customUI).mount('#app')

测试结果如下,当我们给<MyButton>传递的textType的值,不是1或2,或者没有时候报错

Error

从0到1搭建自己的组件库,详解附源码 Error

从0到1搭建自己的组件库,详解附源码 Success

从0到1搭建自己的组件库,详解附源码

OK!完美

依赖注入

1.1 依赖注入是什么

一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖

1.2 依赖注入的使用场景

思考如下使用场景:

<Tabs v-model="activeName">
  <Tab name="one">tab1</Tab>
  <Tab name="two">tab2</Tab>
  <Tab name="three">tab3</Tab>
</Tabs>

在组件开发中难免会碰到上述的使用场景,在上述场景中,存在一个父组件Tab和他的所有子组件Tabs。此时我们希望点击name="two"所对应的子组件的时候,这个子组件字体颜色变为红色,activeName变为two。

如果没有依赖注入的话,我们在设计组件的时候,也就只能将Tab组件和Tabs组件合二为一,暂且叫这个组件为TabsPlus。在这个TabsPlus中,我们肯定会设计一个参数TabsName,这个参数为数组形式,用于渲染每个tab,最后通过v-for的形式,将其渲染出来,比如下述伪代码

<!-- 定义TabsPlus组件 -->
<template>
  <div
    v-for="(item, index) in list"
    :key="item"
    @click="emit('update:modelValue', item.value)">
    {{ item.label }}
  </div>
</template>

<script lang="ts" setup>
const emit = defineEmit()
const props = defineProps({
  list: {
    type: Array as PropType<Array<{value: string, label: string}>>
  }
})
</script>
<!-- 使用 -->
<TabsPlus v-model="activeName" list="[{value: "a", label: "tab1"}]" />

但是如果此时,如果用户希望使用插槽,将tab1对应的组件的<slot name="default"></slot>槽位插入我们的另一个组件,比如MyButton呢,此时显然没有依赖注入是实现不了的,比如下述我们所期望编写的伪代码

<Tabs v-model="activeName">
  <Tab name="one">
    <MyButton>按钮1</MyButton>
  </Tab>
  <Tab name="two">
    <MyButton>按钮2</MyButton>
  </Tab>
  <Tab name="three">
     <MyButton>按钮3</MyButton>
  </Tab>
</Tabs>

1.3 依赖注入核心代码

这里,笔者不才,看了看Vant的源码,偷摸学到了点儿奇淫巧技,在Vant源码中的packages/vant-use/src/useRelation文件夹下,分别存在了两个核心文件useChildrenuseParent点击此处可跳转。这两个文件就是依赖注入的核心文件,下面我会尽可能把这个东西描述明白

1.4 依赖注入的核心原理

依赖注入的核心原理其实就是对Vue.js中提供的provide,reject进行二次封装。父组件通过provide暴露出一些属性,比如link注入子组件实例方法,unlink删除子组件实例方法。当子组件挂载的时候,通过调用link方法向父组件依赖中增加子组件实例;当子组件卸载的时候,调用unlink,删除父组件中当前子组件的实例。说了这么多,我们看看他的源码是怎么写的

// useChildren

/*
  1. 此方法用于父组件获取其归属的所有子组件集合
  examples: 
   <Father>
     <Child>子组件1</Grandchild>
     <Child>子组件2</Grandchild>
     <Child>子组件3</Child>
   </Father>
  
  2. 使用的vue类型
  (1)ComponentPublicInstance: 子组件实例
  (2)ComponentInternalInstance:子组件的虚拟DOM实例
  (3)InjectionKey: Symbol类型的Key
  
  3. flattenVNodes:
   以递归的形式获取父组件所有子组件
 
  4.sortChildren
  按着子组件在父组件中存在的顺序进行排列,目的是用于unlink时,internalChildren,和children中各项数据一一对应
  
  5.useChildren【核心方法】
  参数:key -> Symbol类型的唯一值,用于子组件通过Symbol值找到其对应的父组件
  - 子方法link:
     参数value,可用于父组件向子组件传递值,比如传递props等
     作用:分别向internalChildren和children数组中放入当前子组件
  - 子方法unlink
     参数child,子组件实例
     作用:分别在internalChildren和children中删除当前子组件
 */
import {
  VNode,
  isVNode,
  provide,
  reactive,
  InjectionKey,
  getCurrentInstance,
  VNodeNormalizedChildren,
  ComponentPublicInstance,
  ComponentInternalInstance,
} from 'vue';

export function flattenVNodes(children: VNodeNormalizedChildren) {
  const result: VNode[] = [];

  const traverse = (children: VNodeNormalizedChildren) => {
    if (Array.isArray(children)) {
      children.forEach((child) => {
        if (isVNode(child)) {
          result.push(child);

          if (child.component?.subTree) {
            result.push(child.component.subTree);
            traverse(child.component.subTree.children);
          }

          if (child.children) {
            traverse(child.children);
          }
        }
      });
    }
  };

  traverse(children);

  return result;
}

// 按vnodes顺序对子实例排序
export function sortChildren(
  parent: ComponentInternalInstance,
  publicChildren: ComponentPublicInstance[],
  internalChildren: ComponentInternalInstance[]
) {
  const vnodes = flattenVNodes(parent.subTree.children);

  internalChildren.sort(
    (a, b) => vnodes.indexOf(a.vnode) - vnodes.indexOf(b.vnode)
  );

  const orderedPublicChildren = internalChildren.map((item) => item.proxy!);

  publicChildren.sort((a, b) => {
    const indexA = orderedPublicChildren.indexOf(a);
    const indexB = orderedPublicChildren.indexOf(b);
    return indexA - indexB;
  });
}

export function useChildren<
  // eslint-disable-next-line
  Child extends ComponentPublicInstance = ComponentPublicInstance<{}, any>,
  ProvideValue = never
>(key: InjectionKey<ProvideValue>) {
  const publicChildren: Child[] = reactive([]);
  const internalChildren: ComponentInternalInstance[] = reactive([]);
  const parent = getCurrentInstance()!;

  const linkChildren = (value?: ProvideValue) => {
    // link:向父组件的子组件们中添加新的子组件
    const link = (child: ComponentInternalInstance) => {
      if (child.proxy) {
        internalChildren.push(child); // 添加子组件
        publicChildren.push(child.proxy as Child); // // 添加子组件
        sortChildren(parent, publicChildren, internalChildren); // 给子组件排序
      }
    };

    // unlink: 向父组件的子组件们中删除指定子组件
    const unlink = (child: ComponentInternalInstance) => {
      const index = internalChildren.indexOf(child);
      publicChildren.splice(index, 1); // 移除子组件
      internalChildren.splice(index, 1); // 移除子组件
    };

    // 向后代注入当前子组件实例
    provide(
      key, // 这个key是Symbol类型,目的是为了保证唯一
      Object.assign(
        {
          link,
          unlink,
          children: publicChildren, // 父组件的所有子组件的proxy对象
          internalChildren, // 父组件的所有子组件
        },
        value
      )
    );
  };

  return { // 返回子组件实例们
    children: publicChildren,
    linkChildren,
  };
}
// UseParent.ts
/**
 * 1. 此方法用于子组件将当前实例,加入到父组件child集合中,并可接受父组件传递过来的值
 * 2. 接收父组件的link和unlink方法,分别用于向父组件的child集合中,添加或删除当前子组件实例
 * 3. 方法返回值
 *  parent:子组件对应的父组件,如useChildren中的example,parent为<father>组件
 *  index: 子组件在父组件child集合中对应的索引值
 */
import {
  ref,
  inject,
  computed,
  onUnmounted,
  InjectionKey,
  getCurrentInstance,
  ComponentPublicInstance,
  ComponentInternalInstance,
} from 'vue';

type ParentProvide<T> = T & {
  link(child: ComponentInternalInstance): void;
  unlink(child: ComponentInternalInstance): void;
  children: ComponentPublicInstance[];
  internalChildren: ComponentInternalInstance[];
};

export function useParent<T>(key: InjectionKey<ParentProvide<T>>) {
  // 下面的这个key也是Symbol类型的,他和UseChildren的Key要保持一致
  const parent = inject(key, null);

  if (parent) {
    const instance = getCurrentInstance()!;
    const { link, unlink, internalChildren } = parent;

    // 向父组件的子组件们中添加当前子组件
    link(instance);
    // 页面卸载的时候,删除当前子组件
    onUnmounted(() => unlink(instance));

    const index = computed(() => internalChildren.indexOf(instance));

    return {
      parent,
      index,
    };
  }

  return {
    parent: null,
    index: ref(-1),
  };
}

1.5 依赖注入用法

什么?上述源码我没讲明白,好吧!欢迎评论区留言,我持续更新。BUT!看不懂也没关系,会用就行!下面在我们的组件库的packages/下,新建tools目录,用来存放我们的公共函数,然后把vant/packages/vant-use/useRelation下的useParentuseChildren粘过来!

然后,我们在packages/components下创建Tab.vue,内容如下

<!-- Tab.vue -->
<template>
  <div :class="parentModelValue === props.name ? 'active' : ''" @click="toggle">
    <slot></slot>
  </div>
</template>

<script lang="ts">
export default {
  name: 'Tab'
}
</script>

<script lang="ts" setup>
import { defineProps, defineExpose, Ref, computed } from "vue";
import { useParent } from "../../tools/useRelation/useParent"
import { FATHER_KEY } from "./index"

// 获取父组件
const { parent } = useParent<{ props: any, activeName: Ref<string> }>(FATHER_KEY)

// 获取父组件modelValue绑定值
const parentModelValue = computed(() => parent?.props.modelValue)

const props = defineProps({
  name: {
    type: String,
    default: ""
  }
})

// 选中子组件方法
const toggle = () => {
  parent!.activeName.value = props.name
}

// 导出子组件方法,供父组件使用
defineExpose({ toggle, name: props.name })



</script>

<style lang="scss" scoped>
.active {
  color: red
}
</style>

接着,我们创建Tabs.vue

<!-- Tabs.vue -->
<template>
  <div>
    <slot></slot>
  </div>
</template>

<script lang="ts">
export default {
  name: "Tabs"
}
</script>

<script lang="ts" setup>
import { FATHER_KEY } from "./index"
import { useChildren } from "../../tools/useRelation/useChildren"
import { defineProps, defineEmits, ref, watch } from "vue"

// 获取子组件实例
const { linkChildren } = useChildren(FATHER_KEY)

const props = defineProps({
  modelValue: {
    type: String,
    default: ""
  }
})

const activeName = ref(props.modelValue)


const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>()

// 向子组件传递当前props和activeName
linkChildren({ props, activeName })

const handleEmit = (value: string) => {
  emit("update:modelValue", value)
}

watch(() => activeName.value, () => {
  handleEmit(activeName.value)
})
</script>

创建index.ts导出Symbol类型的key

export const FATHER_KEY = Symbol("FATHER_KEY")

最后,我们在packages/index导出这个组件

...
import Tabs from "./components/TestRelation/Tabs.vue"
import Tab from "./components/TestRelation/Tab.vue"

const components = [
  ...
  Tabs,
  Tab
]

declare module "@vue/runtime-core" {
  export interface GlobalComponents {
    ...
    Tab: typeof Tab
    Tabs: typeof Tabs
  }
}
``

最后!我们编写个测试`Demo试试吧`

```html
<template>
  <h4>
    active: {{ active }}
  </h4>
  <father v-model="active">
    <child name="one">one</child>
    <child name="two">two</child>
    <child name="three" ref="childRef">three</child>
  </father>
  <button @click="handleClick">选中第三个</button>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
const active = ref("one")

const childRef = ref()


const handleClick = () => {
  childRef.value.toggle()
  console.log(active.value)
}
</script>

运行效果如下

从0到1搭建自己的组件库,详解附源码

完美!

按需引入

笔者不才,咱们继续研究下vant的实现方式,按需引入就变得很简单啦!源码如下

// withInstall.ts
import type { App, Component } from 'vue';

type EventShim = {
  new (...args: any[]): {
    $props: {
      onClick?: (...args: any[]) => void;
    };
  };
};

export type WithInstall<T> = T & {
  install(app: App): void;
} & EventShim;

export function withInstall<T extends Component>(options: T) {
  (options as Record<string, unknown>).install = (app: App) => {
    const { name } = options;
    if (name) {
      // * 挂载到全局
      app.component(name, options);
    }
  };

  return options as WithInstall<T>;
}

其实上述代码的大致原理就是给单个的组件,挂载到了全局上,注意上述带*的注释

什么?还是没懂!没关系,会用就行~

packages/tools/utils目录下新建withInstall.ts,并粘贴进去上述代码

然后更改packages/index.ts,追加按需引入的代码

...
import MyButtonCom from './components/MyButton/index'

import { withInstall } from "./tools/utils/withInstall"
export const MyButton = withInstall(MyButtonCom);
...


// 所有组件列表
const components = [
  MyButtonCom,
  Tabs,
  Tab
]
...

之后我们就可以正常使用按需引入了!

不粘贴测试截图了,直接下结论!完美!

增加VuePress

TODO

增加VueTestUtils

TODO

往期精彩

《qiankun保姆级教程》

最后,如果你也喜欢本文章的话,可以给个点赞和关注吗~ GitHub也求个Star!

如果大家感兴趣,之后我们可以仔细讲解下Vant源码中的核心功能!

最后!!!!感谢大家阅读,笔者也放上他的祖传感谢!

从0到1搭建自己的组件库,详解附源码

转载自:https://juejin.cn/post/7241514309717639228
评论
请登录