前言:为什么要发布 npm 包
在日常开发中,我们经常会遇到一些通用的功能或组件需要在多个项目中复用。将这些代码封装成 npm 包不仅可以:
- 提高代码复用性:一次开发,多处使用
- 便于版本管理:通过语义化版本控制代码迭代
- 促进团队协作:统一的组件规范和使用方式
- 开源贡献:分享给社区,帮助更多开发者
准备工作
1. 环境要求
node -v
npm -v
npm install -g pnpm
pnpm -v
2. npm 账号注册
在发布 npm 包之前,需要先注册一个 npm 账号:
- 访问 npmjs.com 注册账号
- 在终端登录 npm
3. 验证登录状态
Vite + React + TypeScript 组件库开发
1. 项目初始化
mkdir my-react-ui && cd my-react-ui
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. 构建与测试
Vite + Vue3 + TypeScript 组件库开发
1. 项目初始化
mkdir my-vue-ui && cd my-vue-ui
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)
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. 构建与测试
npm 发布流程
1. 发布前检查清单
npm whoami
npm view your-package-name
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
npm pack
tar -tzf your-package-name-1.0.0.tgz
4. 发布到 npm
npm publish
npm publish --access public
npm publish --tag beta
5. 版本管理
npm version patch
npm version minor
npm version major
npm version prerelease --preid=beta npm version prerelease --preid=alpha
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: 发布时提示包名已存在
Q2: 发布后找不到类型声明
Q3: 样式没有被正确打包
Q4: 发布到私有 npm 源
{
"publishConfig": {
"registry": "https://your-private-registry.com/"
}
}
npm publish --registry=https://your-private-registry.com/
5. 持续集成发布
使用 GitHub Actions 自动发布:
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 第三方库的全过程。关键步骤包括:
- 项目初始化:使用 Vite 作为构建工具,配置 TypeScript 支持
- 组件开发:编写可复用的组件,注重类型安全和样式隔离
- 构建配置:正确配置 vite.config.ts 以支持库模式构建
- package.json 配置:完善模块入口、类型声明和导出配置
- 发布流程:npm 登录、版本管理、发布与验证
💡 提示:发布开源包时,请确保遵守开源协议,提供完整的文档,并积极维护和响应社区反馈。
希望这篇指南能帮助你成功发布自己的第一个 npm 包!如有问题,欢迎在评论区讨论。
请先登录后再发表评论