欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

使用 vite 构建一个表情选择插件

程序员文章站 2022-06-10 13:54:20
初始化 Vite 基于原生 ES 模块提供了丰富的内建功能,开箱即用。同时,插件足够简单,它不需要任何运行时依赖,只需要安装 vite (用于开发与构建)和 sass (用于开发环境编译 .scss 文件)。 npm i -D vite scss 项目配置 同时用 vite 开发插件和构建插件 de ......

初始化

vite 基于原生 es 模块提供了丰富的内建功能,开箱即用。同时,插件足够简单,它不需要任何运行时依赖,只需要安装 vite (用于开发与构建)和 sass (用于开发环境编译 .scss 文件)。

npm i -d vite scss

项目配置

同时用 vite 开发插件和构建插件 demo,所以我创建了两个 vite 配置文件。 在项目根目录创建 config 文件夹,存放 vite 配置文件。

插件配置

config/vite.config.ts 插件配置文件

import { defineconfig } from 'vite'
import { resolve } from 'path'

export default defineconfig({
  server: {
    open: true,
    port: 8080
  },
  build: {
    emptyoutdir: true,
    lib: {
      formats: ['es', 'umd', 'iife'],
      entry: resolve(__dirname, '../src/main.ts'),
      name: 'emojipopover'
    }
  }
})

server 对象下存放开发时配置。自动打开浏览器,端口号设为 8080。

build 中存放构建时配置。build.emptyoutdir 是指打包时先清空上一次构建生成的目录。如果这是 webpack,你通常还需要安装 clean-webpack-plugin,并在 webpack 中进行一系列套娃配置才能实现这个简单的功能,或者手动添加删除命令在构建之前。而在 vite 中,仅需一句 emptyoutdir: true

通过 build.lib 开启 vite 库模式。vite 默认将 /index.html 作为入口文件,这通常应用在构建应用时。而构建一个库通常将 js/ts 作为入口,这在 vite 中同样容易实现,lib.entry 即可指定 入口为 src/main.ts 文件,这类似于 webpackconfig.entry。

再通过 lib.formats 指定构建后的文件格式以及通过 lib.name 指定文件导出的变量名称为 emojipopover。

插件示例配置

给插件写一个用于展示使用的网页,通常将它托管到 pages 服务。直接通过 vite 本地开发和构建该插件的示例网页,同样容易实现。

config/vite.config.exm.ts 插件示例配置文件

import { defineconfig, loadenv } from 'vite'
import { resolve } from 'path'

export default ({ mode }) => {
  const __dev__ = mode === 'development'

  return defineconfig({
    base: __dev__ ? '/' : 'emoji-popover',
    root: 'example',
    server: {
      open: false,
      port: 3000
    },
    build: {
      outdir: '../docs',
      emptyoutdir: true
    }
  })
}

vite 配置文件还可以以上面这种形式存在,默认导出一个箭头函数,函数中再返回 defineconfig,这样我们可以通过解构直接取得一个参数 mode,通过它来区分当前是开发环境还是生产环境。

config.base 是指开发或生产环境服务的公共基础路径。因为我们需要将示例页面部署到 pages 服务,生产环境修改 base 以保证能够正确加载资源。

构建后的示例网页 html 资源加载路径:

使用 vite 构建一个表情选择插件

config.root 设置为 'example',因为我将示例页面资源放到 /example 目录下

通常构建后的目录为 dist, 这里 build.outdir 设为 'docs',原因是 github pages 默认只可以部署整个分支或者部署指定的 docs 目录。即将 example 构建输出到到 docs 并部署到 pages 服务。

使用 vite 构建一个表情选择插件

命令配置

我们还需要在 package.json 的 sript 字段中添加本地开发以及构建的命令,通过 --config <config path> 指定配置文件路径,因为我将 vite 配置文件都放到了 /config 下。

"scripts": {
    "dev": "vite --config config/vite.config.ts",
    "build": "vite build --config config/vite.config.ts",
    "dev:exm": "vite --config config/vite.config.exm.ts",
    "build:exm": "vite build --config config/vite.config.exm.ts"
},
  • dev 启动插件开发环境
  • build 构建插件
  • dev:exm 启动示例开发环境
  • build:exm 构建示例页面

编写插件

├─src
│  ├─utils
│  │  ├─types.ts
│  │  └─helpers.ts
│  ├─index.scss
│  └─main.ts

main.ts

import { isurl } from './utils/helper'
import { iemojiitem, ioptions } from './utils/types'
import './index.scss'

class emojipopover {
  private options: ioptions
  private wrapclassname: string
  private wrapcount: number
  private wrapcountclassname: string

  constructor(private opts: ioptions) {
    const defaultoptions: ioptions = {
      container: 'body',
      button: '.e-btn',
      targetelement: '.e-input',
      emojilist: [],
      wrapclassname: '',
      wrapanimationclassname: 'anim-scale-in'
    }

    this.options = object.assign({}, defaultoptions, opts)
    this.wrapclassname = 'emoji-wrap'
    this.wrapcount = document.queryselectorall('.emoji-wrap').length + 1
    this.wrapcountclassname = `emoji-wrap-${this.wrapcount}`

    this.init()
    this.createbuttonlistener()
  }

  /**
   * 初始化
   */
  private init(): void {
    const { emojilist, container, button, targetelement } = this.options

    const _emojicontainer = this.createemojicontainer()
    const _emojilist = this.createemojilist(emojilist)
    const _mask = this.createmask()
    _emojicontainer.appendchild(_emojilist)
    _emojicontainer.appendchild(_mask)

    const _targetelement = document.queryselector<htmlelement>(targetelement)
    const { left, top, height } = _targetelement.getclientrects()[0]
    _emojicontainer.style.top = `${top + height + 12}px`
    _emojicontainer.style.left = `${left}px`

    const _container: htmlelement = document.queryselector(container)
    _container.appendchild(_emojicontainer)
  }

  /**
   * 创建按钮事件
   */
  private createbuttonlistener(): void {
    const { button } = this.options
    const _button = document.queryselector<htmlelement>(button)
    _button.addeventlistener('click', () => this.toggle(true))
  }

  /**
   * 创建表情面板容器
   * @returns {htmldivelement}
   */
  private createemojicontainer(): htmldivelement {
    const { wrapanimationclassname, wrapclassname } = this.options
    const container: htmldivelement = document.createelement('div')
    container.classlist.add(this.wrapclassname)
    container.classlist.add(this.wrapcountclassname)
    container.classlist.add(wrapanimationclassname)
    if (wrapclassname !== '') {
      container.classlist.add(wrapclassname)
    }
    return container
  }

  /**
   * 创建表情列表面板
   * @param {iemojiitem} emojilist
   * @returns {htmldivelement}
   */
  private createemojilist(emojilist: array<iemojiitem>) {
    const emojiwrap: htmldivelement = document.createelement('div')
    emojiwrap.classlist.add('emoji-list')

    emojilist.foreach(item => {
      const emojiitem = this.createemojiitem(item)
      emojiwrap.appendchild(emojiitem)
    })

    return emojiwrap
  }

  /**
   * 创建表情项
   * @param {iemojiitem} itemdata
   * @returns {htmldivelement}
   */
  private createemojiitem(emojiitemdata): htmldivelement {
    const { value, label } = emojiitemdata
    const emojicontainer: htmldivelement = document.createelement('div')
    let emoji: htmlimageelement | htmlspanelement

    if (isurl(value)) {
      emoji = document.createelement('img')
      emoji.classlist.add('emoji')
      emoji.classlist.add('emoji-img')
      emoji.setattribute('src', value)
    } else {
      emoji = document.createelement('span')
      emoji.classlist.add('emoji')
      emoji.classlist.add('emoji-text')
      emoji.innertext = value
    }

    emojicontainer.classlist.add('emoji-item')
    emojicontainer.appendchild(emoji)

    if (typeof label === 'string') {
      emojicontainer.setattribute('title', label)
    }

    return emojicontainer
  }

  /**
   * 创建表情面板蒙层
   * @returns {htmldivelement}
   */
  private createmask(): htmldivelement {
    const mask: htmldivelement = document.createelement('div')
    mask.classlist.add('emoji-mask')
    mask.addeventlistener('click', () => this.toggle(false))
    return mask
  }

  /**
   *  打开或关闭表情面板
   * @param isshow {boolean}
   */
  public toggle(isshow: boolean) {
    const emojiwrap: htmlelement = document.queryselector(
      `.${this.wrapcountclassname}`
    )
    emojiwrap.style.display = isshow ? 'block' : 'none'
  }

  /**
   * 选择表情
   */
  public onselect(callback) {
    const emojiitems = document.queryselectorall(
      `.${this.wrapcountclassname} .emoji-item`
    )
    const _this = this

    emojiitems.foreach(function (item) {
      item.addeventlistener('click', function (e: event) {
        const currenttarget = e.currenttarget as htmlelement
        let value

        if (currenttarget.children[0].classlist.contains('emoji-img')) {
          value = currenttarget.children[0].getattribute('src')
        } else {
          value = currenttarget.innertext
        }
        _this.toggle(false)
        callback(value)
      })
    })
  }
}

export default emojipopover

编写 d.ts

使用 rollup 构建库时,通常借助 rollup 插件自动生成 d.ts 文件。但是尝试了社区的两个 vite dts 插件,效果不尽人意。由于这个项目比较简单,干脆直接手写一个 d.ts 文件。在 public 下创建 d.ts 文件,vite 会在构建时自动将 /public 中的资源拷贝到 dist 目录下。

public/emoji-popover.d.ts

export interface iemojiitem {
  value: string
  label?: string
}

export interface ioptions {
  button: string
  container?: string
  targetelement: string
  emojilist: array<iemojiitem>
  wrapclassname?: string
  wrapanimationclassname?: string
}

export declare class emojibutton {
  private options: ioptions
  private wrapclassname: string
  private wrapcount: number
  private wrapcountclassname: string

  constructor(options: ioptions)

  private init(): void
  private createbuttonlistener(): void
  private createemojicontainer()
  private createemojilist()
  private createemojiitem()
  private createmask()
  /*
   * toggle emoji popover.
   */
  public toggle(isshow: boolean): void
  /*
   * listen to choose an emoji.
   */
  public onselect(callback: (value: string) => void): void
}

export default emojibutton

构建生成的文件结构如下:

├─dist
│  ├─emoji-popover.d.ts
│  ├─emoji-popover.es.js
│  ├─emoji-popover.iife.js
│  ├─emoji-popover.umd.js
│  └─style.css

插件样式

有了 css 自定义属性(或称为 “css 变量”),可以不借助 css 预处理器即可实现样式的定制,且是运行时的。也就是说,可以通过 css 自定义属性实现插件的样式定制甚至网页深色模式的跟随,本博客评论框中的 emoji 就是基于这个插件,它可以跟随本博客的深色模式。

:root {
  --e-color-border: #e1e1e1; /* emojipopover border color */
  --e-color-emoji-text: #666; /* text emoji font color */
  --e-color-border-emoji-hover: #e1e1e1; /* emoji hover border color */
  --e-color-bg: #fff; /* emojipopover background color */
  --e-bg-emoji-hover: #f8f8f8; /* emoji hover background color */
  --e-size-emoji-text: 16px; /* text emoji font size */
  --e-width-emoji-img: 20px;  /* image emoji width */
  --e-height-emoji-img: 20px; /* image emoji height */
  --e-max-width: 288px; /* emojipopover max width */
}

.emoji-wrap {
  display: none;
  position: absolute;
  padding: 8px;
  max-width: var(--e-max-width);
  background-color: var(--e-color-bg);
  border: 1px solid var(--e-color-border);
  border-radius: 4px;
  z-index: 3;
  &::before,
  &::after {
    position: absolute;
    content: '';
    margin: 0;
    width: 0;
    height: 0;
  }
  &:after {
    top: -9px;
    left: 14px;
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
    border-bottom: 8px solid var(--e-color-border);
  }
  &::before {
    top: -8px;
    left: 14px;
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
    border-bottom: 8px solid var(--e-color-bg);
    z-index: 1;
  }
}

.emoji-list {
  display: flex;
  flex-wrap: wrap;
}

.emoji-item {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 6px 6px;
  color: var(--e-color-emoji-text);
  cursor: pointer;
  box-sizing: border-box;
  border: 1px solid transparent;
  border-radius: 4px;
  user-select: none;
  &:hover {
    background: var(--e-bg-emoji-hover);
    border-color: var(--e-color-border-emoji-hover);
    & > .emoji-text {
      transform: scale(1.2);
      transition: transform 0.15s cubic-bezier(0.2, 0, 0.13, 2);
    }
  }
}

.emoji-text {
  font-size: var(--e-size-emoji-text);
  font-weight: 500;
  line-height: 1.2em;
  white-space: nowrap;
}

.emoji-img {
  width: var(--e-width-emoji-img);
  height: var(--e-height-emoji-img);
}

.emoji-mask {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2;
  display: block;
  cursor: default;
  content: ' ';
  background: transparent;
  z-index: -1;
}

.anim-scale-in {
  animation-name: scale-in;
  animation-duration: 0.15s;
  animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5);
}

@keyframes scale-in {
  0% {
    opacity: 0;
    transform: scale(0.5);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

全局插件样式

你可以重写这些 css 变量(css 自定义属性)来定制样式。

:root {
  --e-color-border: #e1e1e1; /* emojipopover border color */
  --e-color-emoji-text: #666; /* text emoji font color */
  --e-color-border-emoji-hover: #e1e1e1; /* emoji hover border color */
  --e-color-bg: #fff; /* emojipopover background color */
  --e-bg-emoji-hover: #f8f8f8; /* emoji hover background color */
  --e-size-emoji-text: 16px; /* text emoji font size */
  --e-width-emoji-img: 20px;  /* image emoji width */
  --e-height-emoji-img: 20px; /* image emoji height */
  --e-max-width: 288px; /* emojipopover max width */
}

指定实例样式

如果有多个实例,你可以通过 css 变量 scope 应用到指定实例。

.<custom-class-name> {
  --e-color-border: #e1e1e1; /* emojipopover border color */
  --e-color-emoji-text: #666; /* text emoji font color */
  --e-color-border-emoji-hover: #e1e1e1; /* emoji hover border color */
  --e-color-bg: #fff; /* emojipopover background color */
  --e-bg-emoji-hover: #f8f8f8; /* emoji hover background color */
  --e-size-emoji-text: 16px; /* text emoji font size */
  --e-width-emoji-img: 20px;  /* image emoji width */
  --e-height-emoji-img: 20px; /* image emoji height */
  --e-max-width: 288px; /* emojipopover max width */
}

使用你的 css

emoji popover 生成非常简单的 dom 结构,你也可以使用自己的样式而不是导入 style.css

编写示例网页

├─example
│  ├─index.html
│  └─index.css

首先安装已经发布到 npm 的表情弹窗插件

npm i emoji-popover
example/index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>demo · emoji-popover</title>
  </head>
  <body>
    <div class="container">
      <div class="wrap">
        <input class="e-input" type="text" />
        <button class="e-btn">系统表情</button>
      </div>
      <div class="wrap">
        <input class="e-input-2" type="text" />
        <button class="e-btn-2">文本表情</button>
      </div>
      <div class="wrap">
        <input class="e-input-3" type="text" />
        <button class="e-btn-3">网络图片</button>
      </div>
    </div>

    <script type="module">
      import emojipopover from 'emoji-popover'
      import '../node_modules/emoji-popover/dist/style.css'
      import './index.css'

      const e1 = new emojipopover({
        button: '.e-btn',
        container: 'body',
        targetelement: '.e-input',
        emojilist: [
          {
            value: '