- A+
初始化
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 资源加载路径:
config.root 设置为 'example',因为我将示例页面资源放到 /example 目录下
通常构建后的目录为 dist, 这里 build.outDir 设为 'docs',原因是 Github Pages 默认只可以部署整个分支或者部署指定的 docs 目录。即将 example 构建输出到到 docs 并部署到 Pages 服务。
命令配置
我们还需要在 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>
构建后的示例目录如下,你也可以点击 这里 查看示例