Vue2开发插件并发布到npm

简介: 这篇文章介绍了如何使用Vue 3、TypeScript和Vite开发一个下拉框组件`vue-amazing-selector`,并将其发布到npm,包括了项目的创建、组件开发、配置webpack、编写组件代码、导出组件、编译、npm包初始化、发布流程以及在项目中使用该插件的完整步骤。

目标:创建vue**-amazing-selector下拉框组件**,并发布到npm,效果如下图:

默认时样式:

425b594d55868eecdea861fe0c319c19.png

禁用时样式:

4e206cb95af28a2bd157ec1d6086a0f5.png

①创建vue项目:

vue create vue-amazing-selector

②项目目录结构截图如下:

f09cfa98d541bc7efa7eb0bea294a00c.png

③在项目根目录新建 packages/ 文件夹用于存放组件,由于 packages/ 是新增的,webpack无法获取编译,因此需要在webpack配置文件里加上相关配置

④在项目根目录新建 vue.config.js 配置文件,并写入一下配置:

module.exports = {
  css: { // 是否将组件中的 CSS 提取至一个独立的 CSS 文件中 (而不是动态注入到 JavaScript 中的 inline 代码)。
    extract: false, // 生产环境下是 true,开发环境下是 false
  },
  // 扩展 webpack 配置,使 packages 加入编译
  // chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改
  chainWebpack: config => {
    config.module
      .rule('js')
      .include
      .add(__dirname + 'packages') // 注意这里需要绝对路径,所以要拼接__dirname
      .end()
      .use('babel')
      .loader('babel-loader')
      .tap(options => {
        // 修改它的选项...
        return options
      })
  }
}

⑤在 packages/ 文件夹下创建组件,新建 vue-amazing-selector/ 文件夹和 index.js 文件,截图如下:

5d16d8440587bf270d6ec2c63ef5812d.png

⑥在 vue-amazing-selector/ 文件加下新建 VueAmazingSelector.vue 组件文件和 index.js 文件,截图如下:

ee6f66c52a1c7b6badc7709edcb29846.png

⑦在 VueAmazingSelector.vue 中编写组件代码:

<template>
  <div class="m-amazing-select" :style="`height: ${height}px;`">
    <div
      :class="['m-select-wrap', {'hover': !disabled, 'focus': showOptions, 'disabled': disabled}]"
      :style="`width: ${width - 2}px; height: ${height - 2}px;`"
      tabindex="0"
      @mouseenter="onInputEnter"
      @mouseleave="onInputLeave"
      @blur="activeBlur && !disabled ? onBlur() : e => e.preventDefault()"
      @click="disabled ? e => e.preventDefault() : openSelect()">
      <div
        :class="['u-select-input', {'placeholder': !selectedName}]"
        :style="`line-height: ${height - 2}px;width: ${width - 37}px; height: ${height - 2}px;`"
        :title="selectedName"
      >{
  { selectedName || placeholder }}</div>
      <svg :class="['triangle', {'rotate': showOptions, 'show': !showClose}]" viewBox="64 64 896 896" data-icon="down" aria-hidden="true" focusable="false"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg>
      <svg @click.stop="onClear" :class="['close', {'show': showClose}]" focusable="false" data-icon="close-circle" aria-hidden="true" viewBox="64 64 896 896"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"></path></svg>
    </div>
    <transition name="fade">
      <div
        v-show="showOptions"
        class="m-options-panel"
        @mouseenter="onEnter"
        @mouseleave="onLeave"
        :style="`top: ${height + 6}px; line-height: ${height - 12}px; max-height: ${ num * (height - 2) }px; width: ${width}px;`">
        <p
          v-for="(option, index) in options" :key="index"
          :class="['u-option', {'option-selected': option[label]===selectedName, 'option-hover': !option.disabled&&option[value]===hoverValue, 'option-disabled': option.disabled }]"
          :title="option[label]"
          @mouseenter="onHover(option[value])"
          @click="option.disabled ? e => e.preventDefault() : onChange(option[value], option[label], index)">
          {
  { option[label] }}
        </p>
      </div>
    </transition>
  </div>
</template>
<script>
export default {
  name: 'VueAmazingSelector',
  model: {
    prop: 'selectedValue',
    event: 'model'
  },
  props: {
    options: { // 选项数据
      type: Array,
      default: () => []
    },
    label: { // 选择器字典项的文本字段名
      type: String,
      default: 'label'
    },
    value: { // 选择器字典项的值字段名
      type: String,
      default: 'value'
    },
    placeholder: { // 选择框默认文字
      type: String,
      default: '请选择'
    },
    disabled: { // 是否禁用下拉
      type: Boolean,
      default: false
    },
    allowClear: { // 是否支持清除
      type: Boolean,
      default: false
    },
    width: { // 选择框宽度
      type: Number,
      default: 200
    },
    height: { // 选择框高度
      type: Number,
      default: 36
    },
    num: { // 下拉面板最多能展示的下拉项数,超过后滚动显示
      type: Number,
      default: 6
    },
    selectedValue: { // 当前选中的option条目(v-model)
      type: [Number, String],
      default: null
    }
  },
  data () {
    return {
      selectedName: null,
      hoverValue: null, // 鼠标悬浮项的value值
      activeBlur: true, // 是否激活blur事件
      showClose: false, // 清除按钮显隐
      showOptions: false // options面板
    }
  },
  watch: {
    options () {
      this.initSelector()
      console.log('options')
    },
    selectedValue () {
      this.initSelector()
      console.log('selectedValue')
    }
  },
  mounted () {
    this.initSelector()
  },
  methods: {
    initSelector () {
      if (this.selectedValue) {
        const target = this.options.find(option => option[this.value] === this.selectedValue)
        if (target) {
          this.selectedName = target[this.label]
          this.hoverValue = target[this.value]
        } else {
          this.selectedName = this.selectedValue
          this.hoverValue = null
        }
      } else {
        this.selectedName = null
        this.hoverValue = null
      }
    },
    onBlur () {
      if (this.showOptions) {
        this.showOptions = false
      }
    },
    onInputEnter () {
  // console.log('input enter')
      if (this.allowClear && this.selectedName) {
        this.showClose = true
      }
    },
    onInputLeave () {
      // console.log('input leave')
      if (this.allowClear && this.showClose) {
        this.showClose = false
      }
    },
    onHover (value) {
      this.hoverValue = value
    },
    onEnter () {
      this.activeBlur = false
    },
    onLeave () {
      this.hoverValue = null
      this.activeBlur = true
    },
    openSelect () {
      this.showOptions = !this.showOptions
      if (!this.hoverValue && this.selectedName) {
        const target = this.options.find(option => option[this.label] === this.selectedName)
        this.hoverValue = target ? target[this.value] : null
      }
    },
    onClear () {
      this.showClose = false
      this.selectedName = null
      this.hoverValue = null
    },
    onChange (value, label, index) { // 选中下拉项后的回调
      if (this.selectedValue !== value) {
        this.selectedName = label
        this.hoverValue = value
        this.$emit('model', value)
        this.$emit('change', value, label, index)
      }
      this.showOptions = false
    }
  }
}
</script>
<style lang="less" scoped>
@themeColor: #1890ff; // 自定义主题色
P {
  margin: 0;
}
.m-amazing-select {
  position: relative;
  display: inline-block;
  font-size: 14px;
  font-weight: 400;
  color: rgba(0,0,0,.65);
}
.fade-enter-active, .fade-leave-active {
  transform: scaleY(1);
  transform-origin: 0% 0%;
  opacity: 1;
  transition: all .3s;
}
.fade-enter {
  transform: scaleY(0.8);
  transform-origin: 0% 0%;
  opacity: 0;
}
.fade-leave-to {
  transform: scaleY(1);
  opacity: 0;
}
.m-select-wrap {
  position: relative;
  display: inline-block;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  cursor: pointer;
  transition: all .3s cubic-bezier(.645,.045,.355,1);
  .u-select-input {
    display: block;
    text-align: left;
    margin-left: 11px;
    margin-right: 24px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .placeholder {
    color: #bfbfbf;
  }
  .triangle {
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    right: 12px;
    width: 12px;
    height: 12px;
    fill: rgba(0,0,0,.25);
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s ease-in-out;
  }
  .rotate {
    transform: rotate(180deg);
    -webkit-transform: rotate(180deg);
  }
  .close {
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s ease-in-out;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    right: 12px;
    width: 12px;
    height: 12px;
    fill: rgba(140, 140, 140, 0.6);
    &:hover {
      fill: rgba(100, 100, 100,.8);
    }
  }
  .show {
    opacity: 1;
    pointer-events: auto;
  }
}
.hover { // 悬浮时样式
  &:hover {
    border-color: @themeColor;
  }
}
.focus { // 激活时样式
  border-color: @themeColor;
  box-shadow: 0 0 0 2px fade(@themeColor, 20%);
}
.disabled { // 下拉禁用样式
  color: rgba(0,0,0,.25);
  background: #f5f5f5;
  user-select: none;
  cursor: not-allowed;
}
.m-options-panel {
  position: absolute;
  z-index: 999;
  overflow: auto;
  background: #FFF;
  padding: 4px 0;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,15%);
  .u-option { // 下拉项默认样式
    text-align: left;
    position: relative;
    display: block;
    padding: 5px 12px;
    font-weight: 400;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    cursor: pointer;
    transition: background .3s ease;
  }
  .option-selected { // 被选中的下拉项样式
    font-weight: 600;
    background: #fafafa;
  }
  .option-hover { // 悬浮时的下拉项样式
    background: #e6f7ff;
    // background: saturate(fade(@themeColor, 12%), 30%);
  }
  .option-disabled { // 禁用某个下拉选项时的样式
    color: rgba(0,0,0,.25);
    user-select: none;
    cursor: not-allowed;
  }
}
</style>

⑧在 vue-amazing-selector/index.js 中导出组件

// 引入组件
import VueAmazingSelector from './VueAmazingSelector.vue'

// 为组件提供 install 安装方法,供按需引入
VueAmazingSelector.install = (Vue) => {
  Vue.component(VueAmazingSelector.name, VueAmazingSelector)
}

// 导出组件
export default VueAmazingSelector

⑨在 packages/index.js 文件中对整个组件库进行导出

import VueAmazingSelector from './vue-amazing-selector'
// 存储组件列表
const components = [
  VueAmazingSelector
]
/* 
  定义install 方法,接收Vue作为参数,如果使用use注册插件,则所有的组件都将被注册
*/
const install = function (Vue) {
  // 判断是否安装
  if (install.installed) { return }
  // 遍历所有组件
  components.map(component => {
    Vue.component(component.name, component)
  })
}
// 判断是否引入文件
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}
export {
  VueAmazingSelector
}
export default {
  install
}

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

import VueAmazingSelector from '../packages'
Vue.use(VueAmazingSelector)
// 在 App.vue 中引用,并启动项目查看

⑪在 package.json 的 scripts 中添加一条编译命令

--target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
--dest : 输出目录,默认 dist。这里我们改成 lib
[entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。

"scripts": {
    "lib": "vue-cli-service build --target lib --name selector --dest lib packages/index.js"
}

⑫执行编译命令

yarn lib(或num run lib)

执行结果如下图:

4de55563b810460d4c612657307f063f.png

然后在项目根目录会生成如下图所示文件夹:

e6f339365f425ae4a39100ee698bebef.png

⑬在终端执行 npm init 初始化包配置文件package.json

package.json部分截图如下:

b975c5720069596a0dad0c3bab2e3f03.png

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

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

description: 关于包的描述。

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

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

author:作者

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

license: 开源协议

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

只有编译后的 lib 目录、package.json、README.md是需要被发布的

# 忽略目录

.DS_Store

node_modules

packages/

public/

src/

# 忽略指定文件

.eslintrc.cjs

.gitignore

.npmignore

.npmrc

babel.config.js

vue.config.js

yarn.lock

*.map

⑮编写README.md文件(使用markdown格式)

参考文档: http://markdown.p2hp.com/index.html
# vue-amazing-selector

## An Amazing Select Component For Vue2

## Install & Use

```sh
npm install vue-amazing-selector
#or
yarn add vue-amazing-selector

Import and register component

Global

import Vue from 'vue'
import VueAmazingSelector from 'vue-amazing-selector'

Vue.use(VueAmazingSelector)

Local

<template>
  <VueAmazingSelector
    :options="options"
    label="label"
    value="value"
    placeholder="请选择"
    :disabled="false"
    :width="160"
    :height="36"
    :num="6"
    v-model="selectedValue"
    @change="onChange" />
</template>
<script>
import { VueAmazingSelector } from 'vue-amazing-selector'

export default {
  components: {
    VueAmazingSelector
  }
}
</script>

Props

属性 说明 类型 默认值
options 选项数据 Array []
label 选择器字典项的文本字段名 String label
value 选择器字典项的值字段名 String value
placeholder 选择框默认文字 String 请选择
disabled 是否禁用下拉 Boolean false
allowClear 是否支持清除 Boolean false
width 选择框宽度 Number 200
height 选择框高度 Number 36
num 下拉面板最多能展示的下拉项数,超过后滚动显示 Number 6
selectedValue (v-model)当前选中的option条目 /Number/String null

Events

事件名 说明 返回值
change 选择某项下拉后的回调函数 value, label, index(value值,label文本值,index索引值)

> ⑯登录npm

如果没有npm账号,可以去npm官网( [npm](https://www.npmjs.com/ "npm")) 注册一个账号

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

npm config get registry

输出:https://registry.npmjs.org 即可

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

npm config set registry [https://registry.npmjs.org](http://registry.npmjs.org/ "https://registry.npmjs.org")

然后在终端执行:

npm login

依次输入用户名,密码,邮箱

输出Logged in as…即可

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

> ⑰发布组件到npm

在终端执行:npm publish

发布成功后即可在npm官网搜索到该组件,如下图;并可以通过 npm install vue-amazing-selector(或yarn add vue-amazing-selector)进行安装

![195b3af7389012f547f65c23f2526e2a.png](https://i-blog.csdnimg.cn/blog_migrate/195b3af7389012f547f65c23f2526e2a.png)

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

yarn add vue-amazing-selector

// 在 main.js 文件中引入并注册:
import Vue from 'vue'
import VueAmazingSelector from 'vue-amazing-selector'
Vue.use(VueAmazingSelector)


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




```

less-loader版本过高问题:

yarn add less-loader安装版本过高,报错如图

7eefcb224bf4c811b7397d846bb19697.png

解决:yarn add less-loader@6.0.0

相关文章
|
28天前
|
缓存 前端开发 Linux
哇塞!NPM 缓存竟成开发拦路虎?快来掌握清空秘籍,开启前端开发逆袭之旅!
【8月更文挑战第20天】NPM是前端开发中管理依赖的关键工具。有时需清空其缓存以解决版本不一致或包损坏等问题,确保使用最新依赖。可通过命令`npm cache clean --force`强制清空全部缓存,或手动删除各系统下的缓存文件夹。注意清空缓存可能延长后续安装时间,建议事先备份依赖或确保可重新安装。正确管理缓存有助于提升开发效率。
33 1
|
1月前
|
JavaScript 前端开发 开发者
从零到一:教你如何发布自己的npm插件包
从零到一:教你如何发布自己的npm插件包
|
4月前
|
前端开发 JavaScript 数据安全/隐私保护
从0到1开发一个自己的npm包完整过程
创建自己的 npm 包涉及六个步骤:1) 注册 npm 账号;2) 使用 `npm init` 初始化项目,确保 package.json 的 name 唯一且 private 为 false;3) 开发项目,可封装 UI 组件、函数库或命令行工具;4) 本地调试,通过 `npm link` 在项目中测试;5) `npm login` 登录账号,可能需切换至官方仓库;6) 使用 `npm publish` 发布项目。注意版本号递增,无意义的包不建议发布。
158 2
从0到1开发一个自己的npm包完整过程
|
2月前
|
JavaScript 测试技术
npm发布自己的插件包
npm发布自己的插件包
|
2月前
|
JavaScript 开发工具 数据安全/隐私保护
npm包【详解】(内含npm包的开发、发布、安装、更新、搜索、卸载、查看、版本号更新规则、package.json详解等)
npm包【详解】(内含npm包的开发、发布、安装、更新、搜索、卸载、查看、版本号更新规则、package.json详解等)
61 0
|
4月前
|
JavaScript 开发者 资源调度
Spartacus 2211 开发版本采用 npm install 结合 package-lock.json 避免 build 出错
Spartacus 2211 开发版本采用 npm install 结合 package-lock.json 避免 build 出错
Spartacus 2211 开发版本采用 npm install 结合 package-lock.json 避免 build 出错
|
4月前
|
JSON 小程序 前端开发
小程序使用npm以及常用的移动端UI插件
小程序使用npm以及常用的移动端UI插件
40 0
|
4月前
|
JavaScript iOS开发 MacOS
掌握NVM、NRM和NPM:Node.js开发的利器
掌握NVM、NRM和NPM:Node.js开发的利器
171 0
|
4月前
|
缓存 资源调度 JavaScript
Spartacus 2211 开发使用 npm,CCV2 上用 yarn 进行构建
Spartacus 2211 开发使用 npm,CCV2 上用 yarn 进行构建
|
4月前
|
资源调度 JavaScript 开发工具
从npm慢如蜗牛到飞驰如光:nrm加速你的Node.js开发
从npm慢如蜗牛到飞驰如光:nrm加速你的Node.js开发
250 0