乐于分享
好东西不私藏

插件配置项怎么设计?TypeScript类型怎么加?打包发布要注意啥?

插件配置项怎么设计?TypeScript类型怎么加?打包发布要注意啥?

扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长

发现1000+提升效率与开发的AI工具和实用程序:https://tools.cmdragon.cn/zh/apps?category=ai_chat

一、配置项设计:让插件灵活又不失约束

一个好的插件应该允许用户自定义配置,同时提供合理的默认值。来看看怎么设计。

默认值 + 用户配置合并

// plugins/myPlugin.tsinterfacePluginOptions {prefix: string;theme: "light" | "dark";locale: string;zIndex: number;}constdefaultOptions: PluginOptions = {prefix: "My",theme: "light",locale: "zh",zIndex: 2000,};exportdefault {install(app, userOptions: Partial<PluginOptions> = {}) {// 合并默认配置和用户配置const options = { ...defaultOptions, ...userOptions };// 用合并后的配置    app.provide("myPluginOptions", options);  },};

使用时可以只传你想改的:

app.use(myPlugin, {theme: "dark",zIndex: 3000,// prefix和locale用默认值});

深层配置合并

如果配置项是嵌套对象,简单的展开运算符就不够了,需要深合并:

interfacePluginOptions {theme: {primaryColor: string;fontSize: number;borderRadius: number;  };components: {button: boolean;input: boolean;modal: boolean;  };}constdefaultOptions: PluginOptions = {theme: {primaryColor: "#409eff",fontSize: 14,borderRadius: 4,  },components: {button: true,input: true,modal: true,  },};function deepMerge<T extendsRecord<string, any>>(target: T,source: Partial<T>,): T {const result = { ...target };for (const key in source) {if (      source[key] &&typeof source[key] === "object" &&      !Array.isArray(source[key])    ) {      result[key] = deepMerge(target[key], source[key]!);    } else {      result[key] = source[key] asany;    }  }return result;}exportdefault {install(app, userOptions: DeepPartial<PluginOptions> = {}) {const options = deepMerge(defaultOptions, userOptions);    app.provide("myPluginOptions", options);  },};
默认配置 defaultOptions
合并
用户配置 userOptions
最终配置 options
app.provide
注册组件/指令
全局方法

二、TypeScript类型声明

如果你用TypeScript开发插件,类型声明非常重要——它能让使用者获得完整的类型提示和检查。

给install方法加类型

importtype { App } from"vue";interfaceMyPluginOptions {theme?: "light" | "dark";locale?: string;}exportdefault {install(app: App, options?: MyPluginOptions) {// app有完整的类型提示// options也有类型约束  },};

给provide/inject加类型

用InjectionKey来保证provide和inject的类型一致:

import { inject, typeInjectionKey } from"vue";interface I18nInstance {locale: Ref<string>;translate: (key: string, defaultValue?: string) =>string;setLocale: (lang: string) =>void;}constI18N_KEY: InjectionKey<I18nInstance> = Symbol("i18n");exportfunctionuseI18n(): I18nInstance {const i18n = inject(I18N_KEY);if (!i18n) {thrownewError("[i18n] Plugin not installed!");  }return i18n;}exportdefault {install(app: App, options: any) {constinstance: I18nInstance = {locale: ref("zh"),translate: (key, defaultValue = "") => {/* ... */      },setLocale: (lang) => {/* ... */      },    };    app.provide(I18N_KEY, instance);  },};

组件中使用时,useI18n()返回的类型就是I18nInstance,所有属性和方法都有类型提示。

扩展globalProperties的类型

如果你用了app.config.globalProperties,需要扩展Vue的类型声明,否则TypeScript不认识你的全局属性:

// plugins/myPlugin.tsimporttype { App } from"vue";declaremodule"vue" {exportinterfaceComponentCustomProperties {$translate: (key: string, defaultValue?: string) =>string;$locale: Ref<string>;  }}exportdefault {install(app: App, options: any) {    app.config.globalProperties.$translate = (key, defaultValue = "") => {// ...    };    app.config.globalProperties.$locale = ref("zh");  },};

加了这段declare module 'vue'之后,this.$translate和this.$locale在选项式API中就有类型提示了。

三、打包发布到NPM

如果你想把插件分享给别人用,需要打包并发布到NPM。

用Vite库模式打包

Vite提供了专门的库模式,适合打包Vue插件:

// vite.config.tsimport { defineConfig } from"vite";import vue from"@vitejs/plugin-vue";exportdefaultdefineConfig({plugins: [vue()],build: {lib: {entry: "./src/index.ts",name: "MyPlugin",fileName: (format) =>`my-plugin.${format}.js`,    },rollupOptions: {external: ["vue"],output: {globals: {vue: "Vue",        },      },    },  },});

关键配置说明:

  • • entry:插件的入口文件
  • • name:UMD格式的全局变量名
  • • fileName:输出文件名
  • • external: ['vue']:Vue不打包进去,由使用者提供
  • • globals: { vue: 'Vue' }:UMD格式中vue对应的全局变量

入口文件导出

// src/index.tsimporttype { App } from"vue";importMyButtonfrom"./components/MyButton.vue";importMyInputfrom"./components/MyInput.vue";import { useI18n } from"./composables/useI18n";const components = { MyButton, MyInput };export { useI18n };export { MyButton, MyInput };exportdefault {install(app: App, options?: any) {Object.entries(components).forEach(([name, component]) => {      app.component(name, component);    });  },};

package.json配置

{"name":"my-vue-plugin","version":"1.0.0","description":"A Vue 3 plugin","main":"./dist/my-plugin.umd.js","module":"./dist/my-plugin.es.js","exports":{".":{"import":"./dist/my-plugin.es.js","require":"./dist/my-plugin.umd.js"}},"files":["dist"],"peerDependencies":{"vue":"^3.3.0"},"keywords":["vue","vue3","plugin"],"license":"MIT"}

关键字段:

  • • main:CommonJS入口
  • • module:ES Module入口
  • • exports:现代的导出声明
  • • files:只发布dist目录
  • • peerDependencies:vue是外部依赖,不打包

发布流程

# 1. 打包npm run build# 2. 登录NPMnpm login# 3. 发布npm publish# 如果是 scoped package(如 @myorg/plugin)npm publish --access public
Syntax error in textmermaid version 11.4.1

四、插件开发的最佳实践清单

实践
说明
✅ 提供默认配置
用户不传配置也能正常工作
✅ 用Symbol做provide的key
避免命名冲突
✅ 提供useXxx函数
组合式API友好的使用方式
✅ 加TypeScript类型
更好的开发体验
✅ 扩展ComponentCustomProperties
globalProperties有类型提示
✅ Vue作为peerDependencies
不重复打包Vue
✅ 提供文档和示例
降低使用门槛
✅ 没装插件时给明确报错
inject返回undefined时throw
❌ 不要往globalProperties挂太多东西
难以维护
❌ 不要在install里做异步操作
app.use是同步的

课后 Quiz

问题 1

为什么Vue要放在peerDependencies而不是dependencies里?

答案解析

因为插件是给Vue应用用的,使用者的项目里已经有了Vue。如果把Vue放在dependencies里,会导致安装两份Vue——一份是项目本身的,一份是插件自带的。两份Vue会导致响应式系统失效、组件无法正常工作等问题。放在peerDependencies里表示"我需要Vue,但由你来提供"。

问题 2

declare module 'vue'这段代码是干啥的?

答案解析

这是TypeScript的模块扩展(Module Augmentation)语法。它告诉TypeScript编译器:"我要往Vue的类型定义里加东西"。通过扩展ComponentCustomProperties接口,我们给app.config.globalProperties上挂的全局属性添加了类型声明,这样在选项式API中使用this.$translate时就能获得类型提示和检查。

问题 3

Vite库模式打包时,为什么要设置external: ['vue']?

答案解析

因为Vue应该由使用插件的项目提供,不应该打包进插件里。如果打包进去,会导致:1)插件包体积变大;2)使用者的项目中出现两份Vue代码,可能导致响应式系统失效。设置external告诉打包工具"vue这个依赖不要打包,运行时由外部提供"。

常见报错解决方案

报错 1:Property '$translate' does not exist on type 'ComponentCustomProperties'

错误场景:

// 选项式API中使用this.$translate("hello"); // 💥 TypeScript报错

报错原因:TypeScript不知道$translate是全局属性,因为没有扩展ComponentCustomProperties的类型声明。

解决方案:添加模块扩展声明:

declaremodule"vue" {exportinterfaceComponentCustomProperties {$translate: (key: string, defaultValue?: string) =>string;  }}

报错 2:发布NPM包后安装使用报错Cannot find module

错误场景:

import myPlugin from"my-vue-plugin"; // 💥 找不到模块

报错原因:package.json中的main或exports字段配置不正确,或者打包输出路径不对。

解决方案:

  1. 1. 确认npm run build后dist目录下有文件
  2. 2. 检查package.json的main和module字段指向正确的文件
  3. 3. 检查exports字段配置
{"main":"./dist/my-plugin.umd.js","module":"./dist/my-plugin.es.js","exports":{".":{"import":"./dist/my-plugin.es.js","require":"./dist/my-plugin.umd.js"}}}

报错 3:打包后组件样式丢失

错误场景:插件安装后组件能渲染,但没有样式。

报错原因:Vite库模式默认不处理CSS,或者CSS没有被正确导入。

解决方案:

  1. 1. 在入口文件中导入CSS:import './styles/index.css'
  2. 2. 在vite.config.ts中配置CSS代码分割:
build: {lib: {// ...  },cssCodeSplit: false}
  1. 3. 或者让用户手动导入CSS:import 'my-vue-plugin/dist/style.css'

相关学习资料