Vue3+TypeScript+Vite开发组件库并发布到npm

4,026 阅读6分钟

Vue2开发插件并发布到npm

使用VitePress静态网站生成器创建组件库文档网站并部署到GitHub

目标:创建 vue-amazing-ui 组件库 ,并发布到npm

该组件库已发布到 npm,直接安装即可使用:

pnpm i vue-amazing-ui
#or
yarn add vue-amazing-ui
#or
npm install vue-amazing-ui

文档地址

image.png

目前拥有的 Components 组件:

Component nameDescriptionsComponent nameDescriptions
Alert警告提示Avatar头像
BackTop回到顶部Badge徽标
Breadcrumb面包屑Button按钮
Calendar日历Card卡片
Carousel轮播图Cascader级联选择
Checkbox复选框Collapse折叠面板
ColorPicker颜色选择器Countdown倒计时
DatePicker日期选择器Descriptions描述列表
Dialog对话框Divider分割线
Drawer抽屉Ellipsis文本省略
Empty空状态Flex弹性布局
FloatButton浮动按钮GradientText渐变文字
Grid栅格Image图片
Input输入框InputNumber数字输入框
InputSearch搜索框List列表
LoadingBar加载条Message全局提示
Modal模态框Notification通知提醒
NumberAnimation数值动画Pagination分页
Popconfirm弹出确认Popover气泡卡片
Progress进度条QRCode二维码
Radio单选框Rate评分
Result结果Scrollbar滚动条
Segmented分段控制器Select选择器
Skeleton骨架屏Slider滑动输入条
Space间距Spin加载中
Statistic统计数值Steps步骤条
Swiper触摸滑动插件Switch开关
Table表格Tabs标签页
Tag标签Textarea文本域
TextScroll文字滚动Timeline时间轴
Tooltip文字提示Upload上传
Video播放器Waterfall瀑布流
Watermark水印

目前拥有的 Functions 工具函数:

Function nameDescriptionsArguments
dateFormat格式化日期时间字符串函数(value: number | string | Date = Date.now(), format: string = 'YYYY-MM-DD HH:mm:ss') => string
formatNumber数字格式化函数(value: number | string, precision: number = 2, separator: string = ',', decimal: string = '.', prefix?: string, suffix?: string) => string
rafTimeout使用 requestAnimationFrame 实现的 setTimeoutsetInterval 调用函数(fn: Function, delay: number = 0, interval: boolean = false) => object
cancelRaf用于取消 rafTimeout 函数(raf: { id: number }) => void
throttle节流函数(fn: Function, delay: number = 300) => any
debounce防抖函数(fn: Function, delay: number = 300) => any
add消除 js 加减精度问题的加法函数(num1: number, num2: number) => number
downloadFile下载文件并自定义文件名,未传 name 时,从文件地址中自动提取文件名称(url: string, fileName?: string) => void
toggleDark一键切换暗黑模式函数() => void
useEventListener使用 Vue 的生命周期钩子添加和移除事件监听器(target: HTMLElement | Window | Document, event: string, callback: Function) => void
useMutationObserver使用 MutationObserver 观察 DOM 元素的变化(target: Ref | Ref[] | HTMLElement | HTMLElement[], callback: MutationCallback, options = {}) => object
useScroll实时监测目标元素滚动位置及状态(target: Ref | HTMLElement | Window | Document = window, throttleDelay: number = 0, onScroll?: (e: Event) => void, onStop?: (e: Event) => void) => object
useFps实时监测浏览器刷新率FPS() => object
useMediaQuery使用媒体查询来判断当前环境是否符合指定的媒体查询条件(mediaQuery: string) => object
useResizeObserver使用 ResizeObserver 观察 DOM 元素尺寸变化(target: Ref | Ref[] | HTMLElement | HTMLElement[], callback: ResizeObserverCallback, options = {}) => object
useSlotsExist监听给定名称或名称数组的插槽是否存在,支持监听单个插槽或一组插槽的存在(slotsName: string | string[] = 'default') => Reactive | Ref<boolean>

创建 vue3+ts+vite 项目:

输入项目名称,并依次选择需要安装的依赖项

pnpm create vue@latest

项目目录结构截图如下:

③在项目根目录新建 packages/ 文件夹用于存放组件 (以Breadcrumb为例,其他类似)

在项目根目录中的 vite.config.ts 中写入相关配置项:

import { fileURLToPath, URL } from 'node:url'

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

// ant-desing按需引入
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

// 打包体积可视化插件
// import { visualizer } from 'rollup-plugin-visualizer'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // visualizer({ // 生成的分析图文件名,默认stats.html
    //   file: 'stats.html',
    //   open: true // 打包后自动打开分析图
    // }),
    Components({
      resolvers: [AntDesignVueResolver()]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      'images': fileURLToPath(new URL('./src/assets/images', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: { // 或者globalVars
          // `themeColor` is global variables fields name
          themeColor: '#1677FF' // #1890FF
        },
        javascriptEnabled: true
      },
    },
  },
  // 构建为库
  build: {
    lib: { // 构建为库。如果指定了 build.lib,build.cssCodeSplit 会默认为 false。
      // __dirname的值是vite.config.ts文件所在目录
      entry: resolve(__dirname, 'packages/index.ts'),  // entry是必需的,因为库不能使用HTML作为入口。
      name: 'VueAmazingUI', // 暴露的全局变量
      fileName: 'vue-amazing-ui' // 输出的包文件名,默认是package.json的name选项
    },
    rollupOptions: { // 自定义底层的Rollup打包配置
      // https://rollupjs.org/configuration-options/
      // 确保外部化处理那些你不想打包进库的依赖
      external: ['vue', 'swiper', '@vuepic/vue-datepicker', 'qrcode'],
      output: {
        // format: 'es', // 默认es,可选 'amd' 'cjs' 'es' 'iife' 'umd' 'system'
        exports: 'named', // https://rollupjs.org/configuration-options/#output-exports
      //   // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue',
          // 'vue-router': 'VueRouter', // 引入vue-router全局变量,否则router.push将无法使用
          swiper: 'Swiper',
          '@vuepic/vue-datepicker': 'VueDatePicker',
          qrcode: 'qrcode'
        }
      }
    },
    /** 设置为 false 可以禁用最小化混淆,或是用来指定使用哪种混淆器。
        默认为 Esbuild,它比 terser 快 20-40 倍,压缩率只差 1%-2%。
        注意,在 lib 模式下使用 'es' 时,build.minify 选项不会缩减空格,因为会移除掉 pure 标注,导致破坏 tree-shaking。
        当设置为 'terser' 时必须先安装 Terser。(yarn add terser -D)
    */
    minify: 'terser', // Vite 2.6.x 以上需要配置 minify: "terser", terserOptions 才能生效
    terserOptions: { // 在打包代码时移除 console、debugger 和 注释
      compress: {
        /* (default: false) -- Pass true to discard calls to console.* functions.
          If you wish to drop a specific function call such as console.info and/or
          retain side effects from function arguments after dropping the function
          call then use pure_funcs instead
        */
        drop_console: true, // 生产环境时移除console
        drop_debugger: true
      },
      format: {
        comments: false // 删除注释comments
      }
    }
  }
})

packages/ 目录下创建 UI 组件

例如:新建 breadcrumb/pagination/ 文件夹,截图如下:

breadcrumb/ 文件夹下新建 Breadcrumb.vue 组件文件和 index.ts 文件,截图如下:

Breadcrumb.vue 中编写组件代码:

<script setup lang="ts">
import { computed } from 'vue'
interface Query {
  [propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
}
interface Route {
  path: string // 路由地址
  query?: Query // 路由查询参数
  name: string // 路由名称
}
interface Props {
  routes: Array<Route> // 或者Route[] router的路由数组,没有 ? 时,即表示 required: true
  fontSize: number // 字体大小
  height?: number // 面包屑高度
  maxWidth?: number // 文本最大显示宽度,超出后显示省略号
  separator?: string // 自定义分隔符
  target?: '_self'|'_blank' // 如何打开目标URL,当前窗口或新窗口
}
const props = withDefaults(defineProps<Props>(), {
  routes: () => [],
  fontSize: 14,
  height: 21,
  maxWidth: 180,
  separator: '',
  target: '_self'
  
})
const len = computed(() => {
  return props.routes.length
})
function getUrl (route: Route) {
  var targetUrl = route.path
  if (route.query && JSON.stringify(route.query) !== '{}') {
    const query = route.query
    Object.keys(query).forEach((param, index) => {
      if (index === 0) {
        targetUrl = targetUrl + '?' + param + '=' + query[param]
      } else {
        targetUrl = targetUrl + '&' + param + '=' + query[param]
      }
    })
  }
  return targetUrl
}
</script>
<template>
  <div class="m-breadcrumb" :style="`height: ${height}px;`">
    <div class="m-bread" v-for="(route, index) in routes" :key="index">
      <a
        :class="['u-route',{ active: index===len-1 }]"
        :style="`font-size: ${fontSize}px; max-width: ${maxWidth}px;`"
        :href="index === len - 1 ? 'javascript:;' : getUrl(route)"
        :title="route.name"
        :target="index === len - 1 ? '_self' : target">
        {{ route.name || '--' }}
      </a>
      <template v-if="index !== len - 1">
        <span v-if="separator" class="u-separator">{{ separator }}</span>
        <svg v-else class="u-arrow" viewBox="64 64 896 896" data-icon="right" aria-hidden="true" focusable="false"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 0 0 302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 0 0 0-50.4z"></path></svg>
      </template>
    </div>
    <div class="assist"></div>
  </div>
</template>
<style lang="less" scoped>
.m-breadcrumb {
  display: flex;
  align-items: center;
  .m-bread {
    display: inline-flex;
    align-items: center;
    line-height: 1.5;
    .u-route {
      color: rgba(0, 0, 0, 0.45);
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      cursor: pointer;
      padding: 0 4px;
      border-radius: 4px;
      transition: color 0.2s, background-color 0.2s;
      &:hover {
        background-color: rgba(0, 0, 0, 0.05);
        color: rgba(0, 0, 0, 0.88);
      }
    }
    .active {
      color: rgba(0, 0, 0, 0.88);
      cursor: default;
      &:hover {
        background-color: transparent;
      }
    }
    .u-separator {
      margin: 0 4px;
      color: rgba(0, 0, 0, 0.45);
    }
    .u-arrow {
      width: 12px;
      height: 12px;
      fill: rgba(0, 0, 0, 0.45);
    }
  }
  .assist {
    height: 100%;
    width: 0;
    display: inline-block;
    vertical-align: middle;
  }
}
</style>

breadcrumb/index.ts 中导出组件

import type { App } from 'vue'
import Breadcrumb from './Breadcrumb.vue'

// 使用install方法,在app.use挂载
Breadcrumb.install = (app: App) => {
  app.component(Breadcrumb.__name as string, Breadcrumb)
}

export default Breadcrumb

packages/index.ts 文件中对整个组件库进行导出:

import type { App } from 'vue'
import Breadcrumb from './breadcrumb'
import Pagination from './pagination'

// 所有组件列表
const components = [
  Breadcrumb,
  Pagination
]

// 定义 install 方法
const install = (app: App): void => {
  // 遍历注册所有组件
  /*
    component.__name ts报错
    Argument of type 'string | undefined' is not assignable to parameter of type 'string'.

    Type 'undefined' is not assignable to type 'string'.ts(2345)
    解决方式一:使用// @ts-ignore
    解决方式二:使用类型断言 尖括号语法(component.__name) 或 as语法(component.__name as string)
  */
  components.forEach(component => app.component(component.__name as string, component))
}

export {
  Breadcrumb,
  Pagination
}

const VueAmazingUI = {
  install
}

export default VueAmazingUI

打包组件库

pnpm build

src/main.ts 中导入刚创建的组件,检测是否正常可用

// import VueAmazingUI from '../packages'
import VueAmazingUI from '../dist/vue-amazing-ui.js'
import '../dist/style.css'
// import { Breadcrumb } from '../dist/vue-amazing-ui.js'

const app = createApp(App)

app.use(VueAmazingUI)
// app.use(Breadcrumb)

app.mount('#app')

⑪在终端执行 pnpm init 初始化包,选填并配置 package.json

{
  "name": "vue-amazing-ui",
  "version": "0.0.30",
  "private": false,
  "type": "module", // 如果 package.json 不包含 "type": "module",Vite 会生成不同的文件后缀名以兼容 Node.js。.js 会变为 .mjs 而 .cjs 会变为 .js
  "files": ["dist"], // 检测dist打包目录的所有文件
  "main": "./dist/vue-amazing-ui.umd.cjs",
  "module": "./dist/vue-amazing-ui.js",
  "exports": {
    "./dist/style.css": "./dist/style.css", // 子目录别名,方便样式引入
    "./css": "./dist/style.css",
    ".": { // 模块的主入口,优先级高于main字段,利用.这个别名,为 ES6 模块(import)和 CommonJS (require)指定不同的入口
      "import": "./dist/vue-amazing-ui.js",
      "require": "./dist/vue-amazing-ui.umd.cjs"
    }
  },
  "scripts": {
    "dev": "vite --port 9000 --open --force",
    "build": "run-p type-check build-only",
    "docs:dev": "vitepress dev docs --port 8000 --open",
    "docs:build": "vitepress build docs",
    "docs:deploy": "sh script/deploy.sh",
    "pub": "sh script/publish.sh",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
  },
  "dependencies": {
    "@vuepic/vue-datepicker": "^4.5.1",
    "@vueuse/core": "^10.1.2",
    "@vueuse/integrations": "^10.1.2",
    "ant-design-vue": "^3.2.20",
    "core-js": "^3.30.2",
    "date-fns": "^2.30.0",
    "qrcode": "^1.5.3",
    "swiper": "^9.3.2",
    "vue": "^3.3.4",
    "vue-amazing-ui": "^0.0.30",
    "vue-router": "^4.2.1"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.3.0",
    "@types/node": "^18.16.14",
    "@vitejs/plugin-vue": "^4.2.3",
    "@vue/eslint-config-typescript": "^11.0.3",
    "@vue/tsconfig": "^0.1.3",
    "eslint": "^8.41.0",
    "eslint-plugin-vue": "^9.14.0",
    "less": "^4.1.3",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.8.8",
    "rollup-plugin-visualizer": "^5.9.0",
    "terser": "^5.17.6",
    "typescript": "~4.7.4",
    "unplugin-vue-components": "^0.25.0",
    "vite": "^4.3.8",
    "vitepress": "1.0.0-beta.1",
    "vue-tsc": "^1.6.5"
  },
  "description": "This template should help get you started developing with Vue Amazing UI in Vue 3.",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/themusecatcher/vue-amazing-ui.git"
  },
  "keywords": [
    "Vue3",
    "TS",
    "Vite",
    "Amazing",
    "UI",
    "Components"
  ],
  "author": "theMuseCatcher",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/themusecatcher/vue-amazing-ui/issues"
  },
  "homepage": "https://github.com/themusecatcher/vue-amazing-ui#readme"
}

name : 包名,该名字是唯一的。可在 npm 官网搜索名字,不可重复。

version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。

private:是否私有,需要修改为 false 才能发布到 npm

description: 关于包的描述。

main: 入口文件,需指向最终编译后的包文件。

keywords:关键字,以空格分离希望用户最终搜索的词。

author:作者

license: 开源协议

vite build --watch:当启用 --watch 标志时(启用 rollup 的监听器),对 vite.config.ts 的改动,以及任何要打包的文件,都将触发重新构建

vite --port 9000 --open --force:指定端口9000,启动时打开浏览器,强制优化器忽略缓存并重新构建。

编译打包

执行打包命令

pnpm build

执行结果如下图:

在项目根目录创建 .npmignore 文件,设置忽略发布的文件,类似 .gitignore 文件

# 只有编译后的 dist 目录、package.json、README.md是需要被发布的
# 忽略目录
.DS_Store
.vscode/
node_modules
packages/
public/
src/

# 忽略指定文件
.eslintrc.cjs
.gitignore
.npmignore
.npmrc
.prettierrc.json
components.d.ts
env.d.ts
index.html
pnpm-lock.yaml
stats.html
tsconfig.config.json
tsconfig.json
vite.config.ts

编写 README.md 文件

# vue-amazing-ui

## Document & Online preview

[Vue Amazing UI](https://themusecatcher.github.io/vue-amazing-ui/)

## Install & Use

```bash
pnpm i vue-amazing-ui
# or
npm install vue-amazing-ui
# or
yarn add vue-amazing-ui
```

Import and register component

**Global**

```ts
import { createApp } from 'vue'
import App from './App.vue'

import VueAmazingUI from 'vue-amazing-ui'
import 'vue-amazing-ui/css'

const app = createApp(App)
app.use(VueAmazingUI)
```

**Local**

```vue
<script setup lang="ts">
import { Button } from 'vue-amazing-ui'
import 'vue-amazing-ui/css'
</script>
```

## Project

- Get the project code

```sh
git clone https://github.com/themusecatcher/vue-amazing-ui.git
```

- Install dependencies

```sh
cd vue-amazing-ui

pnpm i
```

- Run project

```sh
pnpm dev
```

## Components

Component name | Descriptions | Component name | Descriptions
-- | -- | -- | --
Breadcrumb | 面包屑 | Button | 按钮
Carousel | 走马灯 | Cascader | 级联选择
Checkbox | 多选框 | Collapse | 折叠面板
Countdown | 倒计时 | DatePicker | 日期选择
Dialog | 对话框 | Divider | 分割线
Empty | 空状态 | Image | 图片
InputNumber | 数字输入框 | Message | 全局提示
Modal | 信息提示 | Notification | 通知提醒框
Pagination | 分页器 | Progress | 进度条
QRCode | 二维码 | Radio | 单选框
Rate | 评分 | Select | 选择器
Slider | 滑动输入条 | Spin | 加载中
Steps | 步骤条 | Swiper | 触摸滑动插件
Switch | 开关 | Table | 表格
Tabs | 标签页 | TextScroll | 文字滚动
Timeline | 时间轴 | Tooltip | 文字提示
Upload | 上传 | Video | 播放器
Waterfall | 瀑布流

## Details

[My CSDN Blogs](https://blog.csdn.net/Dandrose)

## Functions

Function name | Descriptions | Arguments
-- | -- | --
dateFormat | 简单易用的日期格式化函数! | (timestamp: number&#124;string&#124;Date, format = 'YYYY-MM-DD HH:mm:ss') => string
requestAnimationFrame | 针对不同浏览器进行兼容处理! | 使用方式不变
cancelAnimationFrame | 针对不同浏览器进行兼容处理! | 使用方式不变
rafTimeout | 使用 requestAnimationFrame 实现的定时器函数,等效替代 (setTimeout 和 setInterval)! | (func: Function, delay = 0, interval = false) => object
cancelRaf | 用于取消 rafTimeout 函数! | (raf: { id: number }) => void
throttle | 使用 rafTimeout 实现的节流函数! | (fn: Function, delay = 300) => any
debounce | 使用 rafTimeout 实现的防抖函数! | (fn: Function, delay = 300) => any
add | 消除js加减精度问题的加法函数! | (num1: number, num2: number) => number
downloadFile | 下载文件并自定义文件名! | (url: string, name: string) => void

登录 npm

如果没有 npm 账号,可以去 npm官网 注册一个账号

注册成功后在本地查看 pnpm / npm 镜像:

pnpm/npm config get registry

输出:registry.npmjs.org 即可

如果不是则需要设置为npm镜像:

pnpm/npm config set registry https://registry.npmjs.org

然后在终端执行:

pnpm/npm login

依次输入用户名,密码,邮箱,输出Logged in as...即可

pnpm/npm whoami // 查看当前用户是否已登录

发布组件到 npm

在终端执行:

pnpm/npm publish

发布成功后即可在npm官网搜索到该组件,如下图

并可以通过 pnpm/npm install vue-amazing-ui(或 yarn add vue-amazing-ui )进行安装

image.png

为方便打包构建、发布、提交代码到github等操作,可以通过脚步一次性执行以上操作:

在项目中新建 script/ 文件夹,并创建 publish.sh 脚本文件,如下图:

publish.sh 中创建以下脚本:

# /bin/bash

# 确保脚本抛出遇到的错误
set -e

 # 读取package.json中的version
version=`jq -r .version package.json`

 # 打包构建
pnpm build

 # 提交代码到github
git add .
git commit -m "update $version"
git push

 # 发布到npm,pnpm(高性能的npm)
pnpm publish

# 升级 vue-amazing-ui 依赖版本
pnpm up vue-amazing-ui@$version

# 提交版本更新代码到github
git add .
git cm -m "update $version"
git push

之后打包构建、发布、提交代码到github 只需新增 version 版本号之后执行:sh publish.sh 即可!

在要使用的项目中安装并注册插件:

pnpm i vue-amazing-ui
#or
yarn add vue-amazing-ui
# or
npm install vue-amazing-ui

然后在 main.ts 文件中引入并注册:

import VueAmazingUI from 'vue-amazing-ui'
// import { Pagination, Breadcrumb } from 'vue-amazing-ui'
import 'vue-amazing-ui/css'

app.use(VueAmazingUI)
// app.use(Pagination).use(Breadcrumb)

在要使用组件的页面直接使用即可:

<script setup lang="ts">
const routes = [
    {
      path: '/first', // 路由地址
      query: { id: 1, tab: 2 }, // 路由参数
      name: '一级路由' // 路由名称
    },
    {
      path: '/second',
      name: '二级路由'
    },
    {
      path: '/third',
      name: '三级路由三级路由三级路由三级路由'
    }
  ]
</script>
<template>
  <Breadcrumb :routes="routes" />
</template>