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

  • A+
所属分类:Web前端
摘要

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


初始化

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: '?',             label: '笑哭'           },           {             value: '?',             label: '笑哭'           },           {             value: '?',             label: '大笑'           },           {             value: '?',             label: '苦笑'           },           {             value: '?',             label: '斜眼笑'           },           {             value: '?',             label: '得意'           },           {             value: '?',             label: '微笑'           },           {             value: '?',             label: '酷!'           },           {             value: '?',             label: '花痴'           },           {             value: '?',             label: '呵呵'           },           {             value: '?',             label: '好崇拜哦'           },           {             value: '?',             label: '思考'           },           {             value: '?',             label: '白眼'           },           {             value: '?',             label: '略略略'           },           {             value: '?',             label: '呆住'           },           {             value: '?',             label: '大哭'           },           {             value: '?',             label: '头炸了'           },           {             value: '?',             label: '冷汗'           },           {             value: '?',             label: '吓死了'           },           {             value: '?',             label: '略略略'           },           {             value: '?',             label: '晕'           },           {             value: '?',             label: '愤怒'           },           {             value: '?',             label: '祝贺'           },           {             value: '?',             label: '小丑竟是我'           },           {             value: '?',             label: '嘘~'           },           {             value: '?',             label: '猴'           },           {             value: '?',             label: '笑笑不说话'           },           {             value: '?',             label: '牛'           },           {             value: '?',             label: '啤酒'           }         ]       })        e1.onSelect(value => {         document.querySelector('.e-input').value += value       })        const e2 = new EmojiPopover({         button: '.e-btn-2',         container: 'body',         targetElement: '.e-input-2',         emojiList: [           {             value: '(=・ω・=)',             label: ''           },           {             value: '(`・ω・´)',             label: ''           },           {             value: '(°∀°)ノ',             label: ''           },           {             value: '←_←',             label: ''           },           {             value: '→_→',             label: ''           },           {             value: 'Σ(゚д゚;)',             label: ''           },           {             value: '(。・ω・。)',             label: ''           },           {             value: '(-_-#)',             label: ''           }         ]       })        e2.onSelect(value => {         document.querySelector('.e-input-2').value += value       })        const e3 = new EmojiPopover({         button: '.e-btn-3',         container: 'body',         targetElement: '.e-input-3',         emojiList: [           {             value:               'https://img1.baidu.com/it/u=3060109128,4247188337&fm=26&fmt=auto&gp=0.jpg',             label: ''           },           {             value:               'https://img2.baidu.com/it/u=358795348,3036825421&fm=26&fmt=auto&gp=0.jpg',             label: ''           }         ]       })        e3.onSelect(value => {         document.querySelector('.e-input-3').value += value       })     </script>   </body> </html> 

构建后的示例目录如下,你也可以点击 这里 查看示例

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

链接