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__ ? 'http://www.likecs.com/' : '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.tsmain.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