木灵鱼儿 - vue3
type: channel
link: https://www.mulingyuer.com/tag/vue3/
items: 手把手实现 SWR 缓存策略，彻底告别页面闪烁
items: Vue 3 进阶实战：基于 Promise 的通用弹窗管理器（命令式调用）
items: 搭建基于 Vite + TypeScript + Vue 3 + Electron的小项目
items: vue3中使用shiki实现代码高亮和注意事项
items: 搭建一个vite+vue+ts的Chrome插件开发模板
items: vite+vue3和nuxt3实现低版本浏览器兼容的解决方案（白屏问题）
items: vite+vue3 代替require()的hooks
items: vite+vue3多页面（多入口）教程
items: vue3 自定义移动端拖拽指令（支持吸边和offset间距以及回调）
items: vue3+ts+vite4的一些踩坑实录

手把手实现 SWR 缓存策略，彻底告别页面闪烁
type: item
date: 2026-01-06T11:32:43+08:00
description: 前言在开发移动端 H5 项目时，我们经常面临一个棘手的用户体验问题：页面切换时的加载闪烁。当用户在 Tabbar 之间切换时，页面组件通常会重新渲染并触发 API 请求。即使后端响应迅速，用户依然会看到短暂的“白屏”或“骨架屏”，随后内容突然跳出。这种反复的 Loading 状态打断了用户的操作流，体验并不友好。为了解决这一痛点，通常的思路是进行接口缓存。即：将数据缓存在本地，再次进入页面时优先读取缓存。但如果完全依赖缓存，又会导致数据更新不及时。如何在“极速渲染”和“数据新鲜度”之间找到平衡？业界有一个成熟的方案——SWR (Stale-While-Revalidate)。本文将带你在 Vue 3 项目中从零实现一套轻量级的 SWR 缓存机制。什么是 SWR？SWR 全称是 Stale-While-Revalidate，其核心理念可以概括为：“优先返回陈旧数据（Stale），同时后台发起请求（Revalidate），数据返回后更新 UI。”这种策略将网络请求与 UI 渲染解耦，让应用能够瞬间响应用户操作，而不必等待网络请求完成。为什么选择 SWR？相比传统的请求模式，SWR 带来了质的飞跃：极致的性能体验：优先读取缓存，用户切换页面时数据几乎“秒开”，完全感知不到加载过程。消除加载闪烁：UI 始终有内容展示，告别了反复出现的 Loading 动画或骨架屏。减轻服务器压力：配合 TTL（Time To Live，有效期）机制，在有效期内的重复访问可以直接拦截请求，降低后端负载。响应式与自动化：结合 Vue 3 的响应式系统，由于数据是 Ref，一旦后台请求返回新数据，视图会自动更新。核心实现解析我们的实现分为两层架构：底层的缓存管理工具和上层的 Vue Composable (Hooks)。1. 缓存管理工具 (src/utils/cache)为了满足不同场景的需求，我们设计了一个支持内存缓存和持久化缓存的双层缓存工具。内存缓存：使用 Map，刷新页面即失效，适合临时数据。持久化缓存：借助 vueuse 的 useLocalStorage，适合长期存储（如字典数据、用户信息）。首先定义类型接口：// src/utils/cache/types.ts /** 缓存项结构 */ export interface CacheItem<T = any> { data: T; timestamp: number; // 存入时间，用于计算过期 } export interface GetCacheOptions { key: string; ttl: number; // 有效期 (ms) persist?: boolean; // 是否持久化 } export interface SetCacheOptions<T> { key: string; data: T; persist?: boolean; }接下来是具体的实现逻辑，核心在于 getCache 时的 TTL 过期判定：// src/utils/cache/index.ts import { useLocalStorage } from "@vueuse/core"; import type { CacheItem, GetCacheOptions, SetCacheOptions } from "./types"; export type * from "./types"; // 内存缓存堆 const memoryCache = new Map<string, CacheItem>(); // 持久化缓存 (基于 LocalStorage) const persistentCache = useLocalStorage<Record<string, CacheItem>>("swr-cache-storage", {}); /** 读取缓存 */ export function getCache<T>(options: GetCacheOptions): T | null { const { key, ttl, persist = false } = options; if (!key) return null; let item: CacheItem | undefined; if (persist) { item = persistentCache.value[key]; } else { item = memoryCache.get(key); } // 判断ttl if (item && Date.now() - item.timestamp < ttl) { return item.data; } // 过期清理 removeCache(key, persist…

Vue 3 进阶实战：基于 Promise 的通用弹窗管理器（命令式调用）
type: item
date: 2025-11-27T15:19:17+08:00
description: 🚀 前言在 Vue 项目开发中，弹窗（Dialog/Modal/Drawer）是交互的核心载体。然而，随着业务复杂度的攀升，传统的声明式弹窗管理逐渐暴露出“样板代码多”、“状态分散”、“逻辑割裂”等痛点。本文将介绍一种基于 Promise 驱动的命令式弹窗管理方案，它能让你像调用 API 一样从 JS/TS 中直接唤起弹窗，并获取用户操作结果，彻底告别繁琐的 visible 变量。😫 痛点分析：传统模式的“四宗罪”看看这段典型的父组件代码，你是否感到熟悉且头疼？ <UserEditDialog v-model="showUserEdit" :id="curId" @success="refresh" /> <FilePicker v-model="showFilePicker" @confirm="onSelect" /> <ConfirmDialog v-model="showDelete" @confirm="doDelete" /> 核心痛点：状态爆炸：每增加一个弹窗，就要增加一套 ref 和 props 变量。模板污染：组件即使未被使用，也必须在 中预先挂载，影响 DOM 结构清晰度。复用困难：想在 Pinia 或纯 JS 文件中唤起弹窗？传统方式几乎做不到。流程割裂：触发逻辑与回调逻辑在代码空间上分离，难以阅读。🛠️ 解决方案：Promise 驱动的命令式调用我们借鉴 Element Plus ElMessageBox 的设计思想，实现一套通用的管理器。调用体验对比：// ✅ 优化后：像 await 异步函数一样流畅 const handleEdit = async (user) => { try { // 一行代码唤起，直到用户操作才继续往下执行 const formData = await modal.open({ component: UserEditDialog, props: { userId: user.id } }); // 拿到结果直接调用 API await updateUser(formData); Message.success('更新成功'); } catch (e) { console.log('用户取消了操作'); } }🧠 硬核解析：useModal 核心实现原理为了让技术实现更透彻，我们需要像“手术刀”一样剖析 useModal 内部到底发生了什么。1. 核心设计图解整个管理器可以看作是一个 "Promise 句柄劫持中心"。调用 open()：创建一个 Promise，但暂时不 resolve。劫持句柄：将 resolve 和 reject 函数提取出来，连同组件信息（Props, Component）挂载到一个响应式数组 modals 中。渲染层响应：全局挂载的渲染组件监测到 modals 变化，自动渲染对应的弹窗组件。用户交互：用户在弹窗点击“确定”，触发渲染层的 confirm 方法。归还句柄：管理器找到对应的 resolve 函数并执行，Promise 状态翻转，await 结束，父组件拿到数据。2. 代码实现 (Hooks)先定义类型// src/hooks/useModal/types.ts import type { Component } from "vue"; /** 弹窗持久化配置 */ export interface PersistentOptions { /** 手动指定唯一 key（推荐） */ key?: string; /** 根据 props 动态生成 key */ keyGenerator?: (props: Record<string, any>) => string; /** 全局单例模式（整个应用只有一个实例，无论 props 是什么） */ singleton?: boolean; } /** 打开弹窗参数 */ export interface OpenOptions { /** 弹窗组件 */ component: Component; /** props */ props?: Record<string, any>; /** 是否常驻 */ persistent?: boolean | PersistentOptions; } /** 弹窗实例 */ export type ModalInstance<T = any> = { modalId: symbol; visible: boolean; component: Component; props?: Record<string, any>; resolve: undefined | ((value: T) => void); reject: undefined | ((reaso…

搭建基于 Vite + TypeScript + Vue 3 + Electron的小项目
type: item
date: 2025-11-13T15:20:00+08:00
description: 前言有时候我们需要将前端项目打包成一个exe文件供人使用，在不考虑性能或者包体大小的情况下，使用Electron来打包项目是一个不错的选择，那么有没有办法从现有的前端项目中加入Electron呢？答案，当然是可以的，这里以我自己的项目为例，我的项目基于Vue3 + TypeScript + Vite，目的是为了实现一个excel文件的格式化处理功能，打包成exe安装包也只是为了减少运维的工作，用户自己安装本地运行就可以了，没必要部署到线上。核心概念：主进程与渲染进程在开始之前，理解 Electron 的两个核心概念至关重要：主进程 (Main Process)：每个 Electron 应用有且只有一个主进程。它相当于应用的“大脑”，是整个应用的入口点。主进程在 Node.js 环境中运行，拥有操作原生 GUI（如图形用户界面）、管理所有渲染进程以及与操作系统底层交互的能力。我们将在 electron/main.ts 文件中编写主进程代码。渲染进程 (Renderer Process)：每个 Electron 应用可以有一个或多个渲染进程。每个 BrowserWindow 实例都运行着一个独立的渲染进程，负责渲染 Web 页面，也就是我们用 Vue、React 等框架编写的用户界面。它运行在 Chromium 环境中，出于安全考虑，默认无法直接访问 Node.js API 或操作系统资源。环境准备在开始之前，请确保您的开发环境中已安装以下软件：Node.js: 建议使用 LTS 版本。Yarn: 本教程推荐使用 Yarn 作为包管理器。pnpm 我在测试中发现有很多问题，electron相关的文档都是推荐npm或者yarn。初始化vue3项目这里就使用官方的cli命令：yarn create vue这里我建议使用yarn来管理项目依赖，因为Electron使用pnpm会出现依赖问题。大家根据自己需求来配置项目，这里我配置的是：[##] 2/2┌ Vue.js - The Progressive JavaScript Framework │ ◇ 请输入项目名称： │ test │ ◆ 请选择要包含的功能： (↑/↓ 切换，空格选择，a 全选，回车确认) │ ◼ TypeScript │ ◻ JSX 支持 │ ◼ Router（单页面应用开发） │ ◼ Pinia（状态管理） │ ◻ Vitest（单元测试） │ ◻ 端到端测试 │ ◼ ESLint（错误预防） │ ◼ Prettier（代码格式化） └回车后，等待项目初始化完成。集成 Electron为了将我们的 Web 应用打包成桌面应用，需要引入 Electron 相关依赖。yarn add electron dotenv yarn add electron-builder vite-plugin-electron -Delectron: Electron 的核心框架。dotenv: 用于从 .env 文件加载环境变量到 process.env 中，方便主进程使用。electron-builder: 功能强大且广泛使用的 Electron 应用打包和分发工具。vite-plugin-electron: 一个 Vite 插件，它简化了在开发模式下同时运行 Vite 开发服务器和 Electron 应用的过程。创建主进程入口文件安装完成后我们需要创建一个electron的入口文件：在项目根目录，也就是与src同级目录下创建electron文件夹，并创建main.js文件，注意必须是js文件，不能是ts，内容如下：import { app, BrowserWindow, globalShortcut } from "electron"; // import { Menu } from "electron"; import path from "path"; import { fileURLToPath } from "url"; import dotenv from "dotenv"; // 常量 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDev = Boolean(process.env.VITE_DEV_SERVER_URL ?? false); // 加载环境变量 { const envPath = isDev ? path.resolve(__dirname, "../.env.development") : path.resolve(__dirname, "../.env.production"); doten…

vue3中使用shiki实现代码高亮和注意事项
type: item
date: 2025-01-08T10:13:00+08:00
description: 前言最近在写AI训练器的UI界面，有一个需求就是展示TOML配置，为此就需要用到代码高亮，我之前用过 highlight.js 和 PrismJS，但是在撰写VitePress项目配置时发现了一个新的代码高亮库：shiki。shiki可以省去维护css和html，并且原生就是ESM按需加载，支持多种主题等，已经满足了我对一个代码高亮的需求，为此我去尝试用了下这个，于是有了本篇文章。插件文档：Shiki语法高亮器github：shiki注意事项shiki是按需加载的，它的实现方式就是除了一个主体的js文件，在打包后会有非常多的不同代码语言的解析器脚本文件，但是不用担心，这些脚本文件只有在真实使用的时候会加载，只是预先生成了罢了。如果你使用了 vite-bundle-analyzer 这类的分析库，可能会导致你看到的包体大小非常大，解决办法就是你可以使用CDN的方式来做，具体官方有文档，可以自行研究，我个人觉得这不是什么大的问题。效果图教程我们先安装插件：pnpm add -D shiki安装完成后就可以创建vue组件来使用代码高亮，这里我直接贴出我的组件代码： 首先声明了一个props类型，要求传入 toml 代码文本，组件会将这个代码文本转换成代码高亮的html元素并渲染。由于我的项目是支持 dark/light 两种主题模式，所以我从pinia仓库引入了useAppStore来获取当前主题模式是dark还是light。剩下的就是代码高亮的逻辑，创建了 tomlHtml 变量接收高亮后的html元素文本，再通过 v-html 渲染。组件watch监听toml文本或者isDark是否发生变化，变化了就重新生代码高亮文本成并渲染。需要注意的是 createHighlighter 创建的 shiki 实例是可以重复使用的，官方也推荐你复用这一个实例，所以我会判断，如果实例存在就不会重复 init 初始化了，反之，如果组件被销毁了就会将实例也进行销毁。这里比较有意思的就是 createHighlighter 它是一个异步的操作，所以我们除了在 onUnmounted 做实例销毁的处理，还需要在 init 中判断组件是否销毁，因为有可能用户显示了这个组件又很快的不显示了，这时候 init 触发了，init调用createHighlighter又是一个异步的，导致组件不显示后这个异步才完成，但是在完成之前onUnmounted就已经被触发了，这就导致了这个创建的实例无法被销毁。为了解决这个问题，我们可以通过 getCurrentInstance 获取当前组件实例对象，通过对象上的 isUnmounted 来判断组件是不是被卸载了，卸载了我们就进行销毁实例处理。基本逻辑就是这样了，这也是我第一次使用 isUnmounted 这种属性，以防有其他人也碰到这样的问题，特此记录一下。

搭建一个vite+vue+ts的Chrome插件开发模板
type: item
date: 2024-11-15T17:13:00+08:00
description: 前言最近在开发一个Chrome插件，在阅读官方文档时发现它提供的都是js的方式，完全没有工程化的概念，而且插件的版本已经到v3了，v3版本限制了非常多的东西，比如我们使用vue的template渲染，由于vue用到了eval来实现，导致插件会报错，蛋疼的很，总不能写手写render函数吧，太累了。最好的解决办法就是用vite来打包vue项目，这样既可以用vue组件的方式开发，打包时也会将template转换成render函数，从而避免了eval的报错，我们还可以使用到ts和scss这些便利的工具。为此我们搭建一个开发模板需要实现以下功能：支持vite打包支持typescript支持scss支持pinia支持vue-router自动生成manifest.json文件自动生成background.js文件封装常用插件方法：message发布订阅、上下文菜单生成、浏览器消息通知、fetch请求等[hide]Chrome插件文档官方其实提供了中文的文档教程，在开发前可以先看看《Hello World 扩展程序》这个例子。Hello World 扩展程序初始化项目简单看了文档后我们也应该知道了插件中使用html页面有这么几种方式：弹出式窗口 popup 页面；侧边栏窗口 side panel 页面；background脚本通过tab方法打开固定的html文件；既然有这么多页面，那说明这是一个多页面的项目，初始化的时候就得往多页面那块发展，需要自己配置input入口和输出。我们先初始化：pnpm create vue@latest这种方式初始化比较便捷。Vue.js - The Progressive JavaScript Framework √ 请输入项目名称： ... chrome-extension-template √ 是否使用 TypeScript 语法？ ... 是 √ 是否启用 JSX 支持？ ... 否 √ 是否引入 Vue Router 进行单页面应用开发？ ... 是 √ 是否引入 Pinia 用于状态管理？ ... 是 √ 是否引入 Vitest 用于单元测试？ ... 否 √ 是否要引入一款端到端（End to End）测试工具？ » 不需要 √ 是否引入 ESLint 用于代码质量检测？ » 是，并同时引入 Oxlint 以加快检测（试验阶段） √ 是否引入 Prettier 用于代码格式化？ ... 是 正在初始化项目 E:\code\chrome-extension-template\chrome-extension-template... 项目初始化完成，可执行以下命令： cd chrome-extension-template pnpm install pnpm format pnpm dev 添加：TypeScript、Vue Router、Pinia 、ESLint 、Prettier 依赖即可。Oxlint 是最近新出的一个代码检查工具，比Eslint快了几十倍，有兴趣可以尝试一下。配置Prettier.prettierrc.json{ "$schema": "https://json.schemastore.org/prettierrc", "semi": true, "useTabs": true, "tabWidth": 2, "singleQuote": false, "printWidth": 100, "trailingComma": "none", "endOfLine": "lf" }.prettierignoredist/* html/* .local node_modules/** **/*.svg **/*.sh public/*没有的文件就创建，这两个文件都是在项目根目录。安装主要依赖谷歌插件要求项目中存在一个manifest.json配置文件，它存储这插件的各种页面入口配置和背景脚本路径，但是它是一个纯静态的文件，我们想要动态的调整并不方便，比如插件的名称在开发模式可以在开头加上[dev]来区分。而且谷歌插件在开发时加载的文件是build后的产物，如何能使其支持dev模式和HMR热更也是一个难题。解决以上问题的答案就是安装：crxjsnpm仓库地址：@crxjs/vite-pluginpnpm i @crxjs/vite-plugin@2.0.0-beta.28 -D需要注意，该插件支持vite3版本还在beta阶段，但是已经可以正常使用，安装的时候记得指定版本，如果已经正式到2.0版本就不需要指定了。大家可以在npm仓库的versions tab页面查看最新版本。创建manifest.json依赖安装成功后我们在项目根目录创建manifest.json文件，内容如下：{ …

vite+vue3和nuxt3实现低版本浏览器兼容的解决方案（白屏问题）
type: item
date: 2024-07-11T21:39:00+08:00
description: 前言项目全线使用vue3的时候，自然使用的是配套更加契合的vite打包工具，于是自然而然会用到很多新的语法，比如这两年开始普及的可选链语法，这个语法都已经纳入声呐的代码规范中了，但是vite自身打包的时候，并不会做过多的兼容处理。默认情况vite的兼容目标是支持原生ES模块，原生ESM动态导入和import.meta的浏览器，官方预设的兼容目标如下：es2020edge 88firefox 78chrome 87safari 14而我们的可选链正是es2020版本才有的，所以vite在打包后不会做任何兼容处理，这就导致了一些浏览器版本低一点的设备，打开网站就会出现白屏的情况。显然这么新的版本在大部分的目标用户中，也是很超前的，所以做一些适当的兼容处理还是有必要的。教程[hide]只兼容到ES2015（ES6）由于vue3的特性，最低也只能兼容只是es2015的浏览器，因为再低点没有proxy了，vite官方也提供了方便的配置项：// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], build: { target: "es2015" } })通过指定打包后的目标版本就行了，vite会将新的语法通过esbuild进行转换，此时我们在支持esm的浏览器上访问时正常的。（注意只有打包后的代码才会有效）这里我特意大写了 支持ESM的浏览器是有用意的，我们看下打包后的index.html文件。可以看到script标签上使用了type="module"属性，显然即便我们的脚本里面已经兼容到了es2015，但是如果浏览器稍微低一点的版本的，它是支持es2015但是它不支持ESM模块啊。我们看一下对应的支持统计：es2015es6版本最经典的就是proxy的支持ESM模块可以看到和es2015的支持度还是有一些差别的。那么就进入到第二个问题，如何兼容不支持esm的浏览器！兼容ESM浏览器官方提供了一个插件：legacy官方对它的定义是：为打包后的文件提供传统浏览器兼容性支持其实就是对不支持esm的浏览器的支持，同时还有语法降级处理。先进行安装：pnpm i @vitejs/plugin-legacy@4.1.1 terser -D需要注意的是vite4的legacy插件最高目前是4.1.1，再高的版本只支持vite5了，使用起来会报错。legacy插件需要依赖terser对代码进行压缩。使用起来也很简单，我们直接引入插件并启用即可：// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import legacy from "@vitejs/plugin-legacy"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), legacy() ], build: { target: "es2015" } })官方定义，在没有明确指定插件配置参数targets时，他会去读取项目中的.browserslistrc文件，我们知道这个文件是一个用于配置支持的浏览器范围的文件，通常会被Babel、Autoprefixer等插件使用。我个人根据es2015的支持版本书写了一下内容：.browserslistrcChrome >= 51 Edge >= 15 Safari >= 10 Firefox >= 54 Opera >= 38 iOS >= 10 not ie <= 11 Android >= 5 not IE <= 11然后我们再去打包项目，可以发现index.html的脚本引入发生了变化。可以看到script发生了变化，除了type="module"还有一个nomodule脚本，nomodule这个属性表示在支持esm的浏览器不运行里面的代码，而不支持esm的浏览器又无法识别type="module"，反而会去运行nomodule的script，从而实现了降级区分。而降级的脚本都会携带一个legacy文本这个就是用于兼容不支持esm模块的浏览器降级方案。这个方式的好处在较新的浏览器中可以使用新的特性，旧的浏览器使用legacy降级脚本，从而避免了以前打包时一刀切的兼容方式，所有的代码全部降级处理，使得一些新设备没法体现出新语法的性能。nuxt3…

vite+vue3 代替require()的hooks
type: item
date: 2023-12-12T14:53:00+08:00
description: 前言最近发现动态去拼接资源路径还是挺常用的，于是把我之前封装的hooks单独分享出来。教程useAssets.tsexport function useAssets() { /** 前缀补路径斜杠 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars function addPrefixSlash(path: string) { return path.startsWith("/") ? path : `/${path}`; } /** 前缀去路径斜杠 */ function removePrefixSlash(path: string) { return path.startsWith("/") ? path.slice(1) : path; } /** * @description: 获取assets路径 * @param {string} path * @Date: 2023-08-03 17:35:35 * @Author: copilot */ function getAssetsUrl(path: string) { return new URL(`../assets/${removePrefixSlash(path)}`, import.meta.url).href; } /** * @description: 获取images路径 * @param {string} path * @Date: 2023-08-03 17:41:23 * @Author: copilot */ function getImagesUrl(path: string) { return new URL(`../assets/images/${removePrefixSlash(path)}`, import.meta.url).href; } return { getAssetsUrl, getImagesUrl }; } 封装了两个方法，一个是比较通用的拼接assets目录路径的，另一个也是用的比较多的，去拼接assets中images目录的方法，需要注意的是，new URL中的相对路径需要自行根据自己的目录结构调整下，比如我在src/hooks目录下，那么../就能返回到src，所以路径就不用调整了。使用 比较便捷的是我做了路径前面的/斜线处理，你可以不写，也可以加上。在hooks也增加了目前没有用到的addPrefixSlash，看大家自己的需求可以自行新增更多便捷的hooks。

vite+vue3多页面（多入口）教程
type: item
date: 2023-10-09T15:04:00+08:00
description: 前言多页面其实可以理解为多个独立的业务之间共享部分代码逻辑的实现。业务之间是相对独立的，只是小部分或者大部分的代码是可以共用的，常见的一个场景就是：改头换面形式的换肤。不同的皮肤之间毫无可复用组件代码的情况下，我们只能将通用的业务逻辑抽离成可复用的部分，简单点来说就是将数据与ui分离，数据是共用部分，ui是各自独立。最终衍生出的多页面结构大体如下：pages ├── pageA │ ├── App.vue │ ├── index.html │ ├── main.ts └── pageB ├── App.vue ├── index.html └── main.ts那么我们使用vite该如何实现这样的特性呢，下面开始一步步实践它！[hide]初始化项目目前官方其实有两种初始化项目的方式：vite官方初始化vue官方初始化其中vite官方提供的初始化命令：pnpm create vite 提供了一个非常简洁的环境，简洁带来了更好的应用广度，但是由于cli工具的日益复杂，很多配置网上的分享都是五花八门，所以我个人更推荐vue官方提供的：pnpm create vue@latest。包管理工具默认：pnpm可以看到官方提供了非常丰富的选项：✔ Project name: … <your-project-name> ✔ Add TypeScript? … No / Yes ✔ Add JSX Support? … No / Yes ✔ Add Vue Router for Single Page Application development? … No / Yes ✔ Add Pinia for state management? … No / Yes ✔ Add Vitest for Unit testing? … No / Yes ✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright ✔ Add ESLint for code quality? … No / Yes ✔ Add Prettier for code formatting? … No / Yes Scaffolding project in ./<your-project-name>... Done.除了JSX、Vitest、End-to-End Testing我选择的no，其他都是yes，这些依赖项都已经是一个完整项目的标配了。配置多入口初始化完毕后我们定义下多入口的层级格式：ui ├── theme1 │ ├── App.vue │ ├── index.html │ ├── main.ts ├── theme2 │ ├── App.vue │ ├── index.html │ ├── main.ts └── theme3 ├── App.vue ├── index.html └── main.tsvite官方提供了多入口配置方式，如下：vite.config.tsimport { defineConfig } from "vite"; import { resolve } from "node:path"; export default defineConfig({ build: { rollupOptions: { input: { theme1: resolve(__dirname, "./src/ui/theme1/index.html"), theme2: resolve(__dirname, "./src/ui/theme2/index.html"), theme3: resolve(__dirname, "./src/ui/theme3/index.html"), }, }, }, })vite和webpack不同，vite是以html文件为入口的，webpack是用main.ts文件作为入口的，这点需要注意，我尝试通过插件将vite改为main文件为入口，打包时会出现各种问题，所以不建议main这种方式。然后我们将每个页面的index.html文件中的main文件因为改为相对地址： main文件也简单调整一下，去除一些不用的引入，暂时先只挂载app。import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const app = createApp(App) app.use(createPinia()) app.mount('#app')现在我们启动项目就可以通过访问：启动的地址+/src/ui/theme1/index.html 这种形式访问…

vue3 自定义移动端拖拽指令（支持吸边和offset间距以及回调）
type: item
date: 2023-08-13T09:29:00+08:00
description: 前言移动端开发的时候，总是会遇到编写全局浮动组件的情况，比如客服图标啦，消息通知啦，但是浮动就会产生一个问题：阻挡了下层内容展示，常见的做法就是让这个浮动的内容支持拖拽功能，用户将浮动组件拖拽到其他位置即可。这里我参考了vConsole组件，自己根据实际情况编写了一个拖拽指令。代码指令文件：drag.tsimport type { Directive } from "vue"; /** 指令接收的参数 */ interface BindingValue { /** 出现位置的偏移量 [x,y] */ offset?: [number, number]; /** 缓存的key前缀 */ storageKey: string; /** 是否吸边x轴 */ adsorptionX?: boolean; /** 是否吸边y轴 */ adsorptionY?: boolean; /** 拖拽开始 */ dragStart?: (event: TouchEvent) => void; /** 拖拽中 */ dragMove?: (event: TouchEvent) => void; /** 拖拽结束 */ dragEnd?: (event: TouchEvent) => void; } /** 默认参数 */ type DefaultBindingValue = Required<Omit<BindingValue, "storageKey">>; /** 拖拽的数据 */ interface DragData { x: number; y: number; startX: number; startY: number; endX: number; endY: number; hasMove: boolean; init: boolean; } /** 拖拽元素 */ interface DragElement extends HTMLElement { __dragData: DragData; __bindingValue: Required<BindingValue>; } /** 缓存位置数据 */ interface LocalPositionData { x: number | null; y: number | null; } /** ios安全底部高度 */ const IOS_SAFE_BOTTOM = 20; /** 指令参数校验 */ function validateBindingValue(bindingValue: BindingValue): [Error | null, boolean] { if (!bindingValue) { return [new Error("Missing argument for drag directive!"), false]; } if (typeof bindingValue.storageKey !== "string" || bindingValue.storageKey.trim() === "") { const error = new Error( "The storageKey parameter of the drag directive must be a string and cannot be an empty string!" ); return [error, false]; } if (!Array.isArray(bindingValue.offset) || bindingValue.offset.length !== 2) { return [new Error("The drag directive offset argument must be array and length 2!"), false]; } else { const isNotNumber = bindingValue.offset.some((item) => typeof item !== "number"); if (isNotNumber) { return [new Error("The drag instruction offset array must be all numbers"), false]; } } if (typeof bindingValue.adsorptionX !== "boolean") { return [new Error("The adsorptionX parameter of the drag directive must be a boolean!"), false]; } if (typeof bindingV…

vue3+ts+vite4的一些踩坑实录
type: item
date: 2023-05-06T19:14:00+08:00
description: 全局方法挂载和类型声明全局挂载还是很简单的，照着官方的例子写就可以了，我的做法是创建了一个单独ts文件，通过app.use()的方式来进行批量挂载，代码如下：import type { App } from "vue"; /** * @description: 获取assets文件 * @param {string} path * @Date: 2023-05-05 10:05:18 * @Author: mulingyuer */ export function getAssetsFile(path: string): string { return new URL(`../assets/${path}`, import.meta.url).href; } export default { install(app: App) { app.config.globalProperties[`$getAssetsFile`] = getAssetsFile; app.provide(`$getAssetsFile`, getAssetsFile); } };除了通过globalProperties的方式注册在全局上，我还通过provide注入的方式挂载，这个注入可比globalProperties省事的多了，但是它的类型推断最后是unknown，还得自个声明一下类型，就又给人一种鸡肋的感觉。最后在main.ts中use一下import { createApp } from "vue"; import App from "./App.vue"; //全局方法 import globalMethods from "./utils/globalMethods"; const app = createApp(App); app.use(globalMethods); app.mount("#app");这就完事了，使用的时候如下： 直接通过inject接，或者通过getCurrentInstance获取，各有各的恶心人的地方：inject：获取的内容类型是unknown，使用时需要自己把类型贴上，比如：const fns = inject<Function>("$getAssetsFile");。为了更加准确的推导，你可能还需要从别处引入类型声明，我都从别处引入类型声明了，我还用个勾八全局方法，都是import，我直接import引入对应的方法不是更加省事了。getCurrentInstance：这个玩意也很膈应人，getCurrentInstance()返回值可能是null，它的proxy属性也可能null，所以不得不加!声明存在，然后再通过属性获取到挂载的方法。使用这种方式的优势在于如果我们事先声明了类型，那么在这就能直接推导出来，恶心的点也在这，新版的类型声明又特么变了，具体如下：创建一个global.d.ts文件，global名字自己随意，你也可以起个vue.d.ts文件。import { ComponentCustomProperties } from "vue"; import { getAssetsFile } from "../utils/globalMethods"; declare module "vue" { export interface ComponentCustomProperties { $getAssetsFile: typeof getAssetsFile; } } export default ComponentCustomProperties;注意：务必默认导出ComponentCustomProperties否则ts类型各种报错，这个问题之前是没有的。vw的转换相关配置这次是新项目，所以我才用了现在比较新的vw转换，用于代替rem转换，他有一个优势：不需要单独的js脚本去更改根元素字体大小由于不需要单独的js脚本，对于spa做ssr还是很省事的，就不需要单独把这个rem脚本提取出来，然后在index.html中引入了。vw目前业界支持度还是很高的，如果不是支持远古设备，值得体验。vw的原理和rem差不多，只不过现在是相对于视口宽度vw来做了。安装插件：pnpm i postcss-px-to-viewport-8-plugin autoprefixer -Dpostcss-px-to-viewport-8-plugin做vw转换，autoprefixer 做浏览器前缀添加。vite内置了postcss，所以不需要我们单独安装，直接在项目目录与src目录同级，创建postcss.config.js文件，填入以下内容：module.exports = { plugins: { "postcss-px-…