peakchao

搜索

peakchao

peakchao

前端开发工程师 | Go 爱好者

联系方式

NPM 三方库开发与发布完全指南

peakchao 2025-12-14 07:25 39 次浏览 0 条评论

前言:为什么要发布 npm 包

在日常开发中,我们经常会遇到一些通用的功能或组件需要在多个项目中复用。将这些代码封装成 npm 包不仅可以:

  • 提高代码复用性:一次开发,多处使用
  • 便于版本管理:通过语义化版本控制代码迭代
  • 促进团队协作:统一的组件规范和使用方式
  • 开源贡献:分享给社区,帮助更多开发者

准备工作

1. 环境要求

# Node.js 版本要求 >= 16
node -v

# npm 版本要求 >= 7
npm -v

# 推荐使用 pnpm
npm install -g pnpm
pnpm -v

2. npm 账号注册

在发布 npm 包之前,需要先注册一个 npm 账号:

  1. 访问 npmjs.com 注册账号
  2. 在终端登录 npm
npm login
# 按提示输入用户名、密码和邮箱
# 如果开启了 2FA,还需要输入验证码

3. 验证登录状态

npm whoami
# 输出当前登录的用户名

Vite + React + TypeScript 组件库开发

1. 项目初始化

# 创建项目目录
mkdir my-react-ui && cd my-react-ui

# 初始化 package.json
pnpm init

# 安装核心依赖
pnpm add -D vite react react-dom typescript @types/react @types/react-dom
pnpm add -D @vitejs/plugin-react vite-plugin-dts

2. 项目结构

my-react-ui/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── index.tsx
│   │   │   ├── style.css
│   │   │   └── types.ts
│   │   └── index.ts
│   └── index.ts
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md

3. TypeScript 配置(tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "declaration": true,
    "declarationDir": "./dist/types",
    "emitDeclarationOnly": true,
    "outDir": "./dist"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

4. Vite 配置(vite.config.ts)

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    react(),
    dts({
      insertTypesEntry: true,
      outDir: 'dist/types',
      include: ['src/**/*'],
    }),
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyReactUI',
      formats: ['es', 'cjs', 'umd'],
      fileName: (format) => `my-react-ui.${format}.js`,
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
    cssCodeSplit: false,
  },
})

5. 组件开发示例

Button 类型定义(src/components/Button/types.ts)

import { ButtonHTMLAttributes, ReactNode } from 'react'

export type ButtonType = 'primary' | 'secondary' | 'danger' | 'success'
export type ButtonSize = 'small' | 'medium' | 'large'

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** 按钮类型 */
  variant?: ButtonType
  /** 按钮尺寸 */
  size?: ButtonSize
  /** 是否加载中 */
  loading?: boolean
  /** 是否禁用 */
  disabled?: boolean
  /** 子元素 */
  children: ReactNode
}

Button 组件(src/components/Button/index.tsx)

import React from 'react'
import { ButtonProps } from './types'
import './style.css'

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'medium',
  loading = false,
  disabled = false,
  children,
  className = '',
  ...props
}) => {
  const classNames = [
    'my-btn',
    `my-btn--${variant}`,
    `my-btn--${size}`,
    loading ? 'my-btn--loading' : '',
    disabled ? 'my-btn--disabled' : '',
    className,
  ]
    .filter(Boolean)
    .join(' ')

  return (
    <button className={classNames} disabled={disabled || loading} {...props}>
      {loading && <span className="my-btn__spinner" />}
      <span className="my-btn__content">{children}</span>
    </button>
  )
}

export type { ButtonProps, ButtonType, ButtonSize } from './types'

Button 样式(src/components/Button/style.css)

.my-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s ease;
  outline: none;
}

/* 尺寸变体 */
.my-btn--small {
  padding: 6px 12px;
  font-size: 12px;
}

.my-btn--medium {
  padding: 10px 20px;
  font-size: 14px;
}

.my-btn--large {
  padding: 14px 28px;
  font-size: 16px;
}

/* 类型变体 */
.my-btn--primary {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.my-btn--primary:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.my-btn--secondary {
  background: #f0f0f0;
  color: #333;
}

.my-btn--secondary:hover:not(:disabled) {
  background: #e0e0e0;
}

.my-btn--danger {
  background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
  color: white;
}

.my-btn--success {
  background: linear-gradient(135deg, #51cf66 0%, #40c057 100%);
  color: white;
}

/* 状态 */
.my-btn--disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.my-btn--loading {
  cursor: wait;
}

/* 加载动画 */
.my-btn__spinner {
  width: 14px;
  height: 14px;
  border: 2px solid transparent;
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

组件导出(src/components/index.ts)

export { Button } from './Button'
export type { ButtonProps, ButtonType, ButtonSize } from './Button'

主入口文件(src/index.ts)

// 导出所有组件
export * from './components'

// 导入样式
import './components/Button/style.css'

6. package.json 配置

{
  "name": "my-react-ui",
  "version": "1.0.0",
  "description": "A beautiful React UI component library",
  "type": "module",
  "main": "./dist/my-react-ui.cjs.js",
  "module": "./dist/my-react-ui.es.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-react-ui.es.js",
      "require": "./dist/my-react-ui.cjs.js",
      "types": "./dist/types/index.d.ts"
    },
    "./style.css": "./dist/style.css"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "react": ">=17.0.0",
    "react-dom": ">=17.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.2.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vite-plugin-dts": "^3.6.0"
  },
  "keywords": [
    "react",
    "ui",
    "components",
    "typescript"
  ],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/my-react-ui.git"
  },
  "homepage": "https://github.com/yourusername/my-react-ui#readme",
  "bugs": {
    "url": "https://github.com/yourusername/my-react-ui/issues"
  }
}

7. 构建与测试

# 构建库
pnpm build

# 查看构建产物
ls -la dist/
# 应该包含:
# - my-react-ui.es.js (ESM 格式)
# - my-react-ui.cjs.js (CommonJS 格式)
# - my-react-ui.umd.js (UMD 格式)
# - style.css (样式文件)
# - types/ (类型声明文件)

Vite + Vue3 + TypeScript 组件库开发

1. 项目初始化

# 创建项目目录
mkdir my-vue-ui && cd my-vue-ui

# 初始化 package.json
pnpm init

# 安装核心依赖
pnpm add -D vite vue typescript vue-tsc
pnpm add -D @vitejs/plugin-vue vite-plugin-dts

2. 项目结构

my-vue-ui/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.vue
│   │   │   ├── style.css
│   │   │   └── types.ts
│   │   └── index.ts
│   └── index.ts
├── package.json
├── tsconfig.json
├── vite.config.ts
├── env.d.ts
└── README.md

3. TypeScript 配置(tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts"],
  "exclude": ["node_modules", "dist"]
}

4. 环境声明文件(env.d.ts)

/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

5. Vite 配置(vite.config.ts)

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    vue(),
    dts({
      insertTypesEntry: true,
      outDir: 'dist/types',
      include: ['src/**/*'],
      exclude: ['src/**/*.test.ts'],
    }),
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyVueUI',
      formats: ['es', 'cjs', 'umd'],
      fileName: (format) => `my-vue-ui.${format}.js`,
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
        exports: 'named',
      },
    },
    cssCodeSplit: false,
  },
})

6. 组件开发示例

Button 类型定义(src/components/Button/types.ts)

export type ButtonType = 'primary' | 'secondary' | 'danger' | 'success'
export type ButtonSize = 'small' | 'medium' | 'large'

export interface ButtonProps {
  /** 按钮类型 */
  variant?: ButtonType
  /** 按钮尺寸 */
  size?: ButtonSize
  /** 是否加载中 */
  loading?: boolean
  /** 是否禁用 */
  disabled?: boolean
}

export interface ButtonEmits {
  (e: 'click', event: MouseEvent): void
}

Button 组件(src/components/Button/Button.vue)

<template>
  <button
    :class="buttonClasses"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="my-btn__spinner" />
    <span class="my-btn__content">
      <slot />
    </span>
  </button>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import type { ButtonProps, ButtonEmits } from './types'
import './style.css'

const props = withDefaults(defineProps<ButtonProps>(), {
  variant: 'primary',
  size: 'medium',
  loading: false,
  disabled: false,
})

const emit = defineEmits<ButtonEmits>()

const buttonClasses = computed(() => [
  'my-btn',
  `my-btn--${props.variant}`,
  `my-btn--${props.size}`,
  {
    'my-btn--loading': props.loading,
    'my-btn--disabled': props.disabled,
  },
])

const handleClick = (event: MouseEvent) => {
  if (!props.disabled && !props.loading) {
    emit('click', event)
  }
}
</script>

Button 样式(src/components/Button/style.css)

.my-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  outline: none;
  position: relative;
  overflow: hidden;
}

/* 尺寸变体 */
.my-btn--small {
  padding: 8px 16px;
  font-size: 13px;
}

.my-btn--medium {
  padding: 12px 24px;
  font-size: 14px;
}

.my-btn--large {
  padding: 16px 32px;
  font-size: 16px;
}

/* 类型变体 */
.my-btn--primary {
  background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  color: white;
  box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
}

.my-btn--primary:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(99, 102, 241, 0.45);
}

.my-btn--primary:active:not(:disabled) {
  transform: translateY(0);
}

.my-btn--secondary {
  background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
  color: #475569;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.my-btn--secondary:hover:not(:disabled) {
  background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%);
  transform: translateY(-2px);
}

.my-btn--danger {
  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  color: white;
  box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}

.my-btn--danger:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(239, 68, 68, 0.45);
}

.my-btn--success {
  background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
  color: white;
  box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
}

.my-btn--success:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(34, 197, 94, 0.45);
}

/* 状态 */
.my-btn--disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none !important;
}

.my-btn--loading {
  cursor: wait;
}

/* 加载动画 */
.my-btn__spinner {
  width: 16px;
  height: 16px;
  border: 2px solid transparent;
  border-top-color: currentColor;
  border-radius: 50%;
  animation: btn-spin 0.8s linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(360deg);
  }
}

/* 涟漪效果 */
.my-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 60%);
  opacity: 0;
  transform: scale(0);
  transition: all 0.4s ease;
}

.my-btn:active::after {
  opacity: 1;
  transform: scale(2);
  transition: 0s;
}

组件导出(src/components/index.ts)

import Button from './Button/Button.vue'

export { Button }
export type { ButtonProps, ButtonType, ButtonSize } from './Button/types'

主入口文件(src/index.ts)

import type { App, Plugin } from 'vue'
import { Button } from './components'

// 单独导出组件
export { Button }
export * from './components'

// 导出类型
export type { ButtonProps, ButtonType, ButtonSize } from './components/Button/types'

// 组件列表
const components = [Button]

// 插件安装函数
const install: Plugin = {
  install(app: App) {
    components.forEach((component) => {
      app.component(component.name || 'MyButton', component)
    })
  },
}

// 默认导出插件
export default install

7. package.json 配置

{
  "name": "my-vue-ui",
  "version": "1.0.0",
  "description": "A beautiful Vue 3 UI component library",
  "type": "module",
  "main": "./dist/my-vue-ui.cjs.js",
  "module": "./dist/my-vue-ui.es.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-vue-ui.es.js",
      "require": "./dist/my-vue-ui.cjs.js",
      "types": "./dist/types/index.d.ts"
    },
    "./style.css": "./dist/style.css"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "vue": ">=3.3.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.5.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vite-plugin-dts": "^3.6.0",
    "vue": "^3.3.0",
    "vue-tsc": "^1.8.0"
  },
  "keywords": [
    "vue",
    "vue3",
    "ui",
    "components",
    "typescript"
  ],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/my-vue-ui.git"
  },
  "homepage": "https://github.com/yourusername/my-vue-ui#readme"
}

8. 构建与测试

# 构建库
pnpm build

# 查看构建产物
ls -la dist/

npm 发布流程

1. 发布前检查清单

# ✅ 确保已登录 npm
npm whoami

# ✅ 检查包名是否已被占用
npm view your-package-name
# 如果显示 404,说明包名可用

# ✅ 确保 package.json 信息完整
# - name: 包名(需要唯一)
# - version: 版本号
# - description: 包描述
# - main/module: 入口文件
# - files: 发布的文件列表
# - keywords: 搜索关键词
# - license: 开源协议
# - repository: 仓库地址

2. 创建 .npmignore 文件

# 源代码(如果不想发布源码)
src/

# 开发配置文件
tsconfig.json
vite.config.ts
.eslintrc.js
.prettierrc

# 测试相关
__tests__/
*.test.ts
*.spec.ts
coverage/

# 开发文件
.github/
.vscode/
node_modules/

# 其他
*.log
.DS_Store

3. 预发布检查

# 模拟打包,查看将要发布的文件
npm pack --dry-run

# 实际打包,生成 .tgz 文件查看内容
npm pack
tar -tzf your-package-name-1.0.0.tgz

4. 发布到 npm

# 首次发布
npm publish

# 如果包名以 @ 开头(scope package),需要指定公开
npm publish --access public

# 发布 beta 版本
npm publish --tag beta

5. 版本管理

# 补丁版本(bug 修复): 1.0.0 -> 1.0.1
npm version patch

# 次版本(新功能,向后兼容): 1.0.0 -> 1.1.0
npm version minor

# 主版本(破坏性变更): 1.0.0 -> 2.0.0
npm version major

# 预发布版本
npm version prerelease --preid=beta  # 1.0.0 -> 1.0.1-beta.0
npm version prerelease --preid=alpha # 1.0.0 -> 1.0.1-alpha.0

6. 发布后验证

# 安装刚发布的包测试
npm install your-package-name

# 在项目中使用

最佳实践与常见问题

1. Scope Package(作用域包)

如果想使用 @your-name/package-name 格式:

{
  "name": "@your-name/my-ui",
  "publishConfig": {
    "access": "public"
  }
}

2. 配置 Tree Shaking

确保 package.json 中正确配置 sideEffects:

{
  "sideEffects": [
    "*.css",
    "*.scss"
  ]
}

3. 添加 README 文档

# My UI Library

A beautiful and lightweight UI component library.

## Installation

\`\`\`bash
npm install my-ui-library
\`\`\`

## Usage

\`\`\`tsx
import { Button } from 'my-ui-library'
import 'my-ui-library/style.css'

function App() {
  return <Button variant="primary">Click Me</Button>
}
\`\`\`

## Components

- Button
- Input
- Modal
- ...

## License

MIT

4. 常见问题排查

Q1: 发布时提示包名已存在

# 解决方案:换一个独特的包名,或使用 scope 包
# @your-username/your-package-name

Q2: 发布后找不到类型声明

# 检查 package.json 中的 types 字段路径是否正确
# 确保 vite-plugin-dts 正确生成了 .d.ts 文件

Q3: 样式没有被正确打包

# 确保在入口文件中引入了样式
# 检查 vite.config.ts 中 cssCodeSplit 配置
# 确保 package.json 的 exports 包含样式文件

Q4: 发布到私有 npm 源

# 在 package.json 中配置
{
  "publishConfig": {
    "registry": "https://your-private-registry.com/"
  }
}

# 或使用命令行参数
npm publish --registry=https://your-private-registry.com/

5. 持续集成发布

使用 GitHub Actions 自动发布:

# .github/workflows/publish.yml
name: Publish to npm

on:
  release:
    types: [created]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'
          cache: 'pnpm'
          
      - run: pnpm install
      - run: pnpm build
      - run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

总结

通过本文,我们完整地介绍了如何使用 Vite + React + TypeScript 和 Vite + Vue3 + TypeScript 创建并发布 npm 第三方库的全过程。关键步骤包括:

  1. 项目初始化:使用 Vite 作为构建工具,配置 TypeScript 支持
  2. 组件开发:编写可复用的组件,注重类型安全和样式隔离
  3. 构建配置:正确配置 vite.config.ts 以支持库模式构建
  4. package.json 配置:完善模块入口、类型声明和导出配置
  5. 发布流程:npm 登录、版本管理、发布与验证

💡 提示:发布开源包时,请确保遵守开源协议,提供完整的文档,并积极维护和响应社区反馈。

希望这篇指南能帮助你成功发布自己的第一个 npm 包!如有问题,欢迎在评论区讨论。

评论 (0)

请先登录后再发表评论

暂无评论,来发表第一条评论吧!