记一次插件发布的采坑经历
在你需要导出多个组件然后打包给外部使用的时候,需要配置package.json和vite.config.js ,下面来说一些关键的点,防止下次再遇到不会配置
解释几个配置的意思
1."keywords": [
"vue",
"video",
"flv",
"webrtc"
],
含义:这是你库的“标签”或“关键词”。
作用:当其他人在 npmjs.com 网站上搜索时,这些关键词可以帮助他们发现你的库。它相当于给你发布的包做了 SEO 优化。你的这些关键词(vue, video, flv, webrtc)非常精准,能有效吸引到目标用户。
2. main, module, types (传统入口字段)
在现代打包中,这些字段可以看作是为不支持 exports 字段的旧工具提供的向后兼容的入口。
"main": "./dist/zy-video-view.cjs.js",
含义:定义了包的 CommonJS (CJS) 入口文件。
谁会用它:主要被 Node.js 环境使用。当在 Node.js 项目中写 const myLib = require('zy-video-view'); 时,Node.js 会去加载这个 cjs.js 文件。
module": "./dist/zy-video-view.es.js",
含义:定义了包的 ES Module (ESM) 入口文件。
谁会用它:主要被现代打包工具(如 Vite, Webpack, Rollup)使用。这些工具在构建前端项目时,会优先寻找 module 字段。使用 ESM 格式的好处是支持摇树优化 (Tree Shaking),可以移除未使用的代码,减小最终打包体积。
"types": "./dist/zy-video-view.d.ts",
含义:定义了包的 TypeScript 类型声明文件 的入口。
谁会用它:TypeScript 编译器 (tsc) 和代码编辑器(如 VS Code)。正是因为这个字段,当用户 import { zyFlvVideoView } from 'zy-video-view'; 时,编辑器能提供代码自动补全、类型检查和属性文档提示。这是提升开发者体验(DX)的关键。
3. exports (现代入口字段)
"exports": {
".": {
"types": "./dist/zy-video-view.d.ts",
"import": "./dist/zy-video-view.es.js",
"require": "./dist/zy-video-view.cjs.js"
},
"./dist/style.css": "./dist/style.css",
"./package.json": "./package.json"
},
含义:这是目前最推荐的、最权威的方式来声明包的入口。当 exports 字段存在时,它会完全覆盖 main、module、types 的作用。它像一个精确的“路由表”或“海关申报单”,严格控制了外部可以访问你包里的哪些文件。
"." (主入口):
代表用户直接导入包名,如 import ... from 'zy-video-view'。
"import": 当环境使用 import (ESM) 时,提供 es.js 文件。
"require": 当环境使用 require (CJS) 时,提供 cjs.js 文件。
"types": 为这个主入口指定类型声明文件。
"./dist/style.css" (子路径导出):
这是一个非常重要的配置!它允许用户单独导入你的样式文件,例如 import 'zy-video-view/dist/style.css';。
如果没有在 exports 中声明这个路径,用户将无法访问到 style.css,即使它存在于 dist 文件夹中。exports 默认会隐藏所有未声明的文件。
"./package.json":
同理,允许外部工具访问你包的 package.json 文件。为什么允许外部访问package.json 呢?
因为:
1. 处理对等依赖 (peerDependencies) - 这是最重要的原因
你的库很可能依赖于 Vue。你应该在 package.json 中这样声明:
Generated json
这告诉用户:“我的库需要 Vue 3 才能运行,但你得自己安装它,我不会帮你打包进来。”
当用户安装你的库时,npm 或 pnpm 这样的包管理器需要读取你库的 package.json 来检查这个 peerDependencies 字段。
如果它能读取到,它就会检查用户的项目里是否安装了 Vue 3。如果没有,它会给出一个警告,提醒用户安装。
如果你不导出 package.json,包管理器就无法读取这个信息,也就无法给出这个重要的警告,可能会导致用户的项目在运行时出错。
2. 构建工具的模块解析 (Module Resolution)
像 Vite 或 Webpack 这样的现代构建工具在解析模块时,有时会需要参考 package.json。例如,它们可能会检查 "type": "module" 字段来确定如何处理 .js 文件。虽然 exports 字段中的 "import" 和 "require" 条件已经提供了足够的信息,但允许访问 package.json 可以为更复杂的解析场景提供兼容性保障。
3. 调试和版本检查
想象一下,一个用户报告了你的库的一个 bug。你可能会让他运行一个诊断脚本,这个脚本需要知道他当前使用的 zy-video-view 的确切版本号。这个脚本可能会这样做:
Generated javascript
// 在用户的项目中运行
const myLibVersion = require('zy-video-view/package.json').version;
console.log('zy-video-view version:', myLibVersion);
Use code with caution.
JavaScript
如果你的 exports 允许访问 package.json,这段代码会成功运行并打印出版本号。
如果不允许,这段代码会直接抛出 ERR_PACKAGE_PATH_NOT_EXPORTED 错误,调试将变得更加困难。
4. Monorepo (单一代码库) 工具
在 Monorepo 架构中,像 pnpm workspaces 或 Turborepo 这样的工具需要遍历工作区内所有包的 package.json 文件来构建整个项目的依赖关系图。如果某个包的 package.json 不可访问,这些工具就可能无法正常工作。
4. files
Generated json
"files": [
"dist/*"
],
Use code with caution.
Json
含义:这是一个“白名单”,指定了当你执行 npm publish 时,哪些文件或文件夹应该被上传到 NPM 仓库。
作用:保持你发布的包干净、轻量。它确保只有编译后的、可直接使用的 dist 文件夹内容会被上传,而你的源代码 (src)、配置文件 (vite.config.ts, .gitignore 等) 不会被包含进去。用户下载你的包时,只会得到必要的文件。
这就是为什么发布之前要先build构建一下的原因,没有dist的时候,调用方下载这个包是没法用的
接下来看看vite的配置部分,我的项目是一个视频播放器组件,以此为例
rollupOptions: {
external: ["vue", "mpegts.js"], //不要把vue和mpegts.js这些依赖也打包进来
output: {
globals: {
//UMD下的全局变量名称
vue: "Vue",
"mpegts.js": "mpegts",
},
exports: "named", // 使用命名导出避免警告
// 确保 CSS 文件被正确输出
assetFileNames: (assetInfo) => {
// 处理 CSS 文件
if (assetInfo.type === "asset" && assetInfo.names) {
const name = assetInfo.names[0];
if (name && name.endsWith(".css")) {
return "style.css";
}
}
return "[name][extname]";
},
},
},
讲一下assetFileNames:
1.背景问题: 当你构建一个包含 CSS 的 Vue 组件库时,Vite/Rollup 默认可能会为了避免缓存问题,给输出的 CSS 文件名加上一个随机哈希值,例如 style.a1b2c3d4.css。
对于库的危害: 如果文件名是随机的,你的库的使用者就无法像下面这样稳定地导入样式:
// 用户希望这样做,但如果文件名是随机的,这就行不通了
import 'zy-video-view/dist/style.css';
Use code with caution.
解决方案: assetFileNames 配置允许你完全控制资源文件(assets)的输出文件名。
你的这段代码逻辑检查每一个即将输出的资源文件 (assetInfo)。
如果发现这个文件是一个 CSS 文件 (assetInfo.name.endsWith('.css')),它会强制将其文件名重命名为 style.css。
这样,无论构建多少次,输出的 CSS 文件名永远都是 style.css。
2.cssCodeSplit: false
背景问题: 如果你的库里有多个组件(例如 zy-flv-video-view 和 zy-webrtc-video-view),并且它们各自有自己的 <style> 块,Vite 默认可能会把它们的 CSS 分割成多个文件(Code Splitting)。
对于库的危害: 一个组件库通常不希望用户去导入多个零散的 CSS 文件。用户只想要一个全功能的样式入口。
解决方案: 将 cssCodeSplit 设置为 false,它会告诉 Vite:“不要对 CSS 进行代码分割。请把所有组件的所有样式,全部合并到同一个 CSS 文件里去。”
✨ 核心收益: 确保了所有样式都被打包进一个单一的文件中。这个配置与上面的 assetFileNames 完美配合:
cssCodeSplit: false 负责将所有 CSS 合并成一个文件。
assetFileNames 负责将这个合并后的文件命名为 style.css。
最后看一下如何暴露给外部才可以实现既能用对象方式使用还能使用解构方式使用
import ZyFlvVideo from "./components/flv_video/index.vue";
import ZyWebRtcVideo from "./components/webrtc_video/index.vue";
import { withInstall } from "./utils/with-install";
// 确保 VideoView 的类型是 DefineComponent
const zyFlvVideoView = withInstall(ZyFlvVideo);
const zyWebRtcVideoView = withInstall(ZyWebRtcVideo);
// 创建一个包含所有组件的对象作为默认导出
const ZyVideoView = {
zyFlvVideoView,
zyWebRtcVideoView,
install(app: any) {
app.component("zy-flv-video-view", zyFlvVideoView);
app.component("zy-webrtc-video-view", zyWebRtcVideoView);
},
};
// 默认导出
export default ZyVideoView;
// 命名导出
export { zyFlvVideoView, zyWebRtcVideoView };
// 类型导出
export type { IFlvVideoProps } from "./components/flv_video/type";
export type { IWebRtcPlayerProps } from "./components/webrtc_video/type";
ZyVideoView对象可以写成
const ZyVideoView = {
install(app: any) {
app.component("zy-flv-video-view", zyFlvVideoView);
app.component("zy-webrtc-video-view", zyWebRtcVideoView);
},
};吗?
是的,可以去掉。从技术上讲 app.use() 功能不会受到任何影响。
去掉后,代码变成这样:
Generated typescript
// ... zyFlvVideoView 和 zyWebRtcVideoView 的定义 ...
// 简化后的默认导出对象
const ZyVideoView = {
install(app: any) {
// 这里的 zyFlvVideoView 和 zyWebRtcVideoView 仍然可以被访问到
// 因为它们是在同一个文件作用域内定义的常量
app.component("zy-flv-video-view", zyFlvVideoView);
app.component("zy-webrtc-video-view", zyWebRtcVideoView);
},
};
export default ZyVideoView;
Use code with caution.
TypeScript
当用户执行 app.use(ZyVideoView) 时,Vue 会调用这个对象的 install 方法。由于 zyFlvVideoView 和 zyWebRtcVideoView 这两个常量在当前文件的作用域中是存在的,所以 install 方法可以毫无问题地访问它们并完成组件的全局注册。
那么,为什么通常要保留它们呢?
尽管从 install 的角度看它们是多余的,但保留它们提供了一个重要的额外功能:将默认导出的对象变成一个“命名空间”或“工具箱”。
这为库的使用者提供了另一种灵活的使用方式。
让我们对比一下保留和去掉的区别:
情况一:保留属性(你的原始代码)
Generated typescript
// 原始代码
const ZyVideoView = {
zyFlvVideoView,
zyWebRtcVideoView,
install(app: any) { /* ... */ },
};
export default ZyVideoView;
Use code with caution.
TypeScript
使用者可以这样做:
Generated typescript
import ZyVideo from 'your-library-name'; // 默认导入
// 用法1:全局安装 (最常见)
app.use(ZyVideo);
// 用法2:手动、有选择性地注册或使用
// 用户可以通过默认导出的对象,直接访问到具体的组件定义
const flvPlayerComponent = ZyVideo.zyFlvVideoView;
const webrtcPlayerComponent = ZyVideo.zyWebRtcVideoView;
// 可以在某个组件里局部注册
export default {
components: {
'my-special-flv-player': flvPlayerComponent
}
}
// 甚至可以在 JSX 或渲染函数中使用
// import { h } from 'vue'
// h(ZyVideo.zyFlvVideoView, { props... })
Use code with caution.
TypeScript
优点: 默认导出的 ZyVideo 对象本身就是一个完整的“工具箱”,包含了库的所有部分。用户导入一次,就可以访问到所有东西,非常方便和直观。
情况二:去掉属性(你提议的修改)
Generated typescript
// 修改后的代码
const ZyVideoView = {
install(app: any) { /* ... */ },
};
export default ZyVideoView;
Use code with caution.
TypeScript
使用者的行为会发生什么变化:
Generated typescript
import ZyVideo from 'your-library-name'; // 默认导入
// 用法1:全局安装 (仍然有效)
app.use(ZyVideo);
// 用法2:尝试访问组件定义
const flvPlayerComponent = ZyVideo.zyFlvVideoView; // !!!错误!!!
// Uncaught TypeError: Cannot read properties of undefined (reading 'zyFlvVideoView')
Use code with caution.
TypeScript
缺点: ZyVideo 对象现在变成了一个“黑盒”,它除了能被 app.use() 之外,几乎没有其他用处。如果用户想要获取单个组件的定义(比如为了局部注册或在渲染函数中使用),他们将必须同时使用命名导入:
Generated typescript
import ZyVideo, { zyFlvVideoView } from 'your-library-name';
// 现在必须从命名导入中获取组件
const flvPlayerComponent = zyFlvVideoView;
Use code with caution.
TypeScript
这破坏了默认导出作为“完整集合”的便利性,可能会让用户感到困惑。
结论
可以去掉吗? 可以,核心的 app.use() 功能不受影响。
应该去掉吗? 强烈不推荐。
保留 zyFlvVideoView 和 zyWebRtcVideoView 作为 ZyVideoView 对象的属性,是一种遵循社区最佳实践的设计模式。它让你的库 API 更加健壮和灵活,为使用者提供了“开箱即用”的便利,同时保留了高级用法(如手动访问和注册)的可能性。
这就像你买一个工具箱,你既希望店家能帮你把所有工具都安装好(app.use),也希望自己能打开箱子,单独拿出某一把螺丝刀来用(ZyVideo.zyFlvVideoView)。你的原始代码完美地实现了这两点。

浙公网安备 33010602011771号