扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长
发现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); },};二、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四、插件开发的最佳实践清单
课后 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. 确认 npm run build后dist目录下有文件2. 检查package.json的 main和module字段指向正确的文件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. 在入口文件中导入CSS: import './styles/index.css'2. 在vite.config.ts中配置CSS代码分割:
build: {lib: {// ... },cssCodeSplit: false}3. 或者让用户手动导入CSS: import 'my-vue-plugin/dist/style.css'

夜雨聆风