阅读视图
Vue3 项目:宠物照片变身冰球运动员的 AI 应用
让用户愿意等待的秘密:实时图片预览
让宠物打冰球!手把手教你用 Coze 多模态工作流 + Vue 3 打造 AI 拟人生成器
Vue3中v-show如何通过CSS修改display属性控制条件显示?与v-if的应用场景该如何区分?
前端轮子(1)--前端部署后-判断页面是否为最新
前端轮子系列:
- 前端部署后-判断页面是否为最新
- 如何优雅diy响应数据
- 如何优雅进行webview调试
- 如何实现测试环境的自动登录
项目部署后:通知更新、强制刷新、自测访问的页面为最新代码(是否发版成功)
重点:自测、提测、修改bug后,确认访问的为最新页面
实现思路
目的:生成新的版本标识,通过对比标识 ==> 当前为最新 还是 旧页面
- 生成版本号:如:1.1.10 ,或者时间戳(202512201416)
- 自动化脚本生成
- 手动控制
- 对比版本号:相等=> 当前为最新;不等 => 当前为旧页面
- 通过对比结果 对应处理产品逻辑 完事儿~
具体实现
方案一:生成时间戳,注入变量,控制台打印
在vue-cli项目中
通过vue.config.js 入变量
然后在main.js中打印变量
// main.js
console.log('__BUILD_TIME__', __BUILD_TIME__)
// vue.config.js
const _date = new Date()
const BUILD_TIME = _date.toLocaleString ? _date.toLocaleString() : _date.getTime()
module.exports = {
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
__BUILD_TIME__: JSON.stringify(BUILD_TIME)
})
]
},
}
在vite项目中
通过vite.config.js 注入 BUILD_TIME 变量
然后在main.js中打印 BUILD_TIME 变量
// main.js
console.log('__BUILD_TIME__', __BUILD_TIME__)
// vite.config.js
const _date = new Date()
const BUILD_TIME = _date.toLocaleString ? _date.toLocaleString() : _date.getTime()
export default defineConfig(() => {
return {
define: {
__BUILD_TIME__: JSON.stringify(BUILD_TIME)
},
}
})
这种方案,实现了在控制台打印。但是对于生产环境,无法做到版本号的对比
为啥? 因为上线 一般会去掉日志的打印, 所以咱通过版本号来~
方案二:记录版本号,轮询对比版本号
- 编写生成版本号的脚本,生成版本号文件
- build结束,调用脚本去生成版本号文件
- 轮询对比版本号文件,如果版本号不一致,做相应操作
// dist/version.json
{
"buildTime": "2025-12-20 14:16:00"
}
prebuild: build前执行的脚本
postbuild:build结束后执行的脚本,用于生成版本号文件
// package.json
{
"scripts": {
"postbuild": "node scripts/generate-version-dist.mjs"
}
}
// scripts/generate-version-dist.mjs
import { writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '..')
async function main() {
const _date = new Date()
const BUILD_TIME = _date.toLocaleString ? _date.toLocaleString() : _date.getTime()
const outPath = path.join(root, 'dist', 'version.json')
await writeFile(outPath, JSON.stringify({ BUILD_TIME }, null, 2) + '\n', 'utf-8')
}
main().catch((e) => {
console.error('[generate-version] failed', e)
process.exit(1)
})
轮询对比版本号
- 轮询对比版本号文件,如果版本号不一致,做相应操作
- 页面隐藏、切后台时,停止轮询,页面关闭时,停止轮询
- 页面显示、切前台时,开始轮询
// main.js
import { createVersionPoller } from './utils/versionPoller'
createVersionPoller({
versionUrl: '/version.json',
storageKey: '__VERSION_CHECK__CURRENT_BUILD_TIME__',
intervalMs: 15_000,
maxDelayMs: 60_000,
onResult: ({ remoteVersion, remoteBuildTime }) => {
// 轮询到json文件 成功,额外处理逻辑
},
onError: (e) => {
// 轮询失败,额外处理逻辑
},
onUpdate: () => {
// 版本号不一致,需要更新,额外处理逻辑
}
}).start()
/**
* 轮询 /version.json,对比 buildTime 判断是否需要更新(setTimeout + 错误指数退让)
*
* 行为说明:
* - start() 会自动注册监听:visibilitychange / beforeunload / unload
* - stop() 会清理定时器并卸载监听
* - 请求失败会指数退让(delay *= 2,最大不超过 maxDelayMs);成功后恢复 intervalMs
* - currentBuildTime 会写入 localStorage(storageKey)
*
* @param {Object} options
* @param {string} [options.versionUrl='/version.json'] 版本文件地址
* @param {string} [options.storageKey='__VERSION_CHECK__CURRENT_BUILD_TIME__'] localStorage key
* @param {number} [options.intervalMs=15000] 正常轮询间隔(毫秒)
* @param {number} [options.maxDelayMs=60000] 退让最大间隔(毫秒)
* @param {(info:{localBuildTime:string,remoteBuildTime:string,remoteVersion:string,data:any})=>void} [options.onResult] 每次成功拉取后的回调
* @param {(error:any)=>void} [options.onError] 拉取失败回调
* @param {(info:{localBuildTime:string,remoteBuildTime:string,remoteVersion:string,data:any})=>void} [options.onUpdate] 发现新 buildTime 时回调(默认会 stop)
*/
export function createVersionPoller({
versionUrl = '/version.json',
storageKey = '__VERSION_CHECK__CURRENT_BUILD_TIME__',
intervalMs = 15000,
maxDelayMs = 60000,
onResult,
onError,
onUpdate
} = {}) {
let timer = null
let stopped = true
let delayMs = intervalMs
let bound = false
const readLocal = () => window.localStorage.getItem(storageKey) || ''
const writeLocal = (v) => v && window.localStorage.setItem(storageKey, String(v))
function clear() {
if (!timer) return
window.clearTimeout(timer)
timer = null
}
function isVisible() {
return document.visibilityState !== 'hidden'
}
async function fetchRemote() {
const sep = versionUrl.includes('?') ? '&' : '?'
const url = `${versionUrl}${sep}t=${Date.now()}`
const res = await fetch(url, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } })
if (!res.ok) throw new Error(`fetch ${versionUrl} failed: ${res.status}`)
return await res.json()
}
async function tick() {
if (stopped || !isVisible()) return
const localBuildTime = readLocal() || ''
try {
const data = await fetchRemote()
const remoteVersion = String(data?.version || '')
const remoteBuildTime = String(data?.BUILD_TIME || '')
delayMs = intervalMs
onResult?.({ localBuildTime, remoteBuildTime, remoteVersion, data })
console.log( remoteBuildTime , '=>', localBuildTime)
if (remoteBuildTime && remoteBuildTime !== localBuildTime) {
// 记录最新 buildTime,避免重复提示同一个更新
writeLocal(remoteBuildTime)
onUpdate?.({ localBuildTime, remoteBuildTime, remoteVersion, data })
}
} catch (e) {
delayMs = Math.min(maxDelayMs, Math.max(500, delayMs * 2))
onError?.(e)
}
timer = window.setTimeout(tick, delayMs)
}
function onVisibilityChange() {
if (stopped) return
if (!isVisible()) return clear()
clear()
timer = window.setTimeout(tick, 0)
}
function ensureBound() {
if (bound) return
bound = true
document.addEventListener('visibilitychange', onVisibilityChange)
window.addEventListener('beforeunload', stop)
window.addEventListener('unload', stop)
}
function ensureUnbound() {
if (!bound) return
bound = false
document.removeEventListener('visibilitychange', onVisibilityChange)
window.removeEventListener('beforeunload', stop)
window.removeEventListener('unload', stop)
}
function start() {
if (!stopped) return
stopped = false
delayMs = intervalMs
ensureBound()
if (isVisible()) timer = window.setTimeout(tick, 0)
}
function stop() {
stopped = true
clear()
ensureUnbound()
}
return { start, stop }
}
注意点
- 轮询版本文件,拼 时间戳、设置请求头 避免命中缓存
- 轮询版本文件,时间间隔不要太短,耗费网络,虽然已经做了轮询指数退让
- 生成的版本文件 生成到dist下 注意访问路径
- 版本号需要上传到git,需要手动执行脚本&commit + push
源码
结语
如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻
因为收藏===会了
如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾
告别代码屎山!UniApp + Vue3 自动化规范:ESLint 9+ 扁平化配置全指南
配置初衷是为了保证,团队开发中的代码规范问题。
以下是全部配置过程,我的项目是npm创建的,并非hbuilder创建的。如果你的项目的hbuilder创建的,需要执行下 npm init -y。
配置后想要达到的效果:
- 保证缩进统一性
- vue组件多个属性可以自动换行。
- 在代码里用了
uni.showToast,ESLint 却疯狂报错uni is not defined。
2025 年,ESLint 迎来了史上最大变革——Flat Config(扁平化配置) 时代。今天,我们就用一套最硬核的方案,把 Vue3、TypeScript、SCSS 和 Git 自动化 全部打通!
一、 为什么 2025 年要用 ESLint 9+ ?
传统的 .eslintrc.js 采用的是“层级继承”逻辑,配置多了就像迷宫。而 ESLint 9+ 的 Flat Config (eslint.config.mjs) 采用纯 JavaScript 数组对象,逻辑更扁平、加载更快速、对 ESM 原生支持更好。
二、 核心依赖安装:一步到位
首先,清理掉项目里的旧配置文件,然后在根目录执行这行“全家桶”安装命令:
npm install eslint @eslint/js typescript-eslint eslint-plugin-vue globals eslint-config-prettier eslint-plugin-prettier prettier husky lint-staged --save-dev
三、 配置实战:三剑客齐聚
- 魔法启动:生成 ESLint 9 配置文件
新版 ESLint 推荐通过交互式命令生成基础框架,但针对 UniApp,我们建议直接创建 eslint.config.mjs 以获得极致控制力。
核心逻辑:
- 2 空格缩进:强迫症的福音。
- 属性换行:组件属性 > 3 个自动起新行。
- 多语言全开:JS / TS / Vue / CSS / SCSS 完美兼容。
/* eslint.config.mjs */
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
export default tseslint.config(
// 【1】配置忽略名单:不检查编译后的代码
{ ignores: ['dist/**', 'unpackage/**', 'node_modules/**', 'static/**'] },
// 【2】JS 基础规则 & UniApp 全局变量支持
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser, ...globals.node,
uni: 'readonly', wx: 'readonly', plus: 'readonly' // 解决 uni 报错
},
},
},
// 【3】TypeScript 强类型支持
...tseslint.configs.recommended,
// 【4】Vue 3 核心规范(属性换行策略)
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: tseslint.parser } // Vue 模板内支持 TS
},
rules: {
'vue/multi-word-component-names': 'off', // 适配 UniApp 页面名
'vue/html-indent': ['error', 2], // 模板强制 2 空格
'vue/max-attributes-per-line': ['error', {
singleline: { max: 3 }, // 超过 3 个属性就换行
multiline: { max: 1 } // 多行模式下每行只能有一个属性
}],
'vue/first-attribute-linebreak': ['error', {
singleline: 'beside', multiline: 'below'
}]
}
},
// 【5】Prettier 冲突处理:必须放在数组最后一行!
pluginPrettierRecommended,
);
- 视觉统领:
.prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"trailingComma": "all",
"endOfLine": "auto"
}
- 编辑器底层逻辑:
.editorconfig
让 IDEA 和 VS Code 在你打字的第一秒就明白:缩进只要两个空格。
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
四、 极速体验:让工具为你打工
- IDEA / WebStorm 深度联动
别再手动敲 Ctrl+Alt+L 了!
- 进入 Settings -> ESLint,勾选 Run eslint --fix on save。
- 进入 Settings -> Prettier,勾选 Run on save。
IDEA 2024+ 会完美识别你的eslint.config.mjs。
- Git 提交自动“洗地” (Husky + lint-staged)
想要代码仓库永远干干净净?在 package.json 中加入这道闸门:
json
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss,json,md}": ["prettier --write"]
}
执行 npx husky init 并将 .husky/pre-commit 改为 npx lint-staged。现在,任何不符合规则的代码都别想溜进 Git 仓库!
五、 结语
规范不是为了限制自由,而是为了让开发者在 2025 年繁重的业务中,能拥有一份优雅的代码底座。
从零搭一个 Vue 小家:用 Vite + 路由轻松入门现代前端开发
从零开始,轻松走进 Vue 的世界:一个“全家桶”小项目的搭建之旅
如果你刚刚接触前端开发,听到“Vue”、“Vite”、“路由”这些词时是不是有点懵?别担心!我们可以把写代码想象成搭积木、装修房子、甚至安排一场家庭旅行。今天,我们就通过一个名为 all-vue 的小项目,带你一步步理解现代 Vue 应用是怎么“搭起来”的。
🏠 第一步:选好地基——用 Vite 快速建项目
什么是vite?
Vite(法语,意为“快”)是一个由 Vue.js 作者 尤雨溪(Evan You) 主导开发的现代化前端构建工具。它旨在解决传统打包工具(如 Webpack)在开发阶段启动慢、热更新(HMR)延迟高等问题,提供极速的开发体验。
想象你要盖一栋房子。传统方式可能要先打地基、砌砖、铺电线……繁琐又耗时。而 Vite 就像一位超级高效的建筑承包商,你只要说一句:“我要一个 Vue 房子”,它立刻给你搭好框架,连水电都通好了!
在终端里运行:
npm init vite@latest all-vue -- --template vue
几秒钟后,你就得到了一个结构清晰的项目目录。其中最关键的是:
-
index.html:这是你房子的“大门”,浏览器一打开就看到它。 -
src/main.js:这是整栋房子的“总开关”,负责启动整个应用。 -
src/App.vue:这是“客厅”,所有房间(页面)都要从这里进出。
Vite 的优势在于快——修改代码后,浏览器几乎瞬间刷新,就像你换了个沙发,家人马上就能坐上去试舒服不舒服。
🏗️ 第二步:认识整栋楼——项目结构概览
运行 npm init vite@latest all-vue -- --template vue 后,你会得到这样一栋“数字公寓”:
项目结构简略预览:
/all-vue
├── public/ # 公共资源(如 logo.png)
├── src/
│ ├── assets/ # 图片、字体等静态资源
│ ├── components/ # 可复用的小部件(按钮、卡片等)
│ ├── views/ # 独立页面(首页、关于页等)
| | |—— About.vue # 关于页面的Vue组件
| | |—— Home.vue # 主页的vue组件
│ ├── router/ # 室内导航系统
| | |—— index.js # 路由总控
│ ├── App.vue # 中央控制台(客厅)
│ └── main.js # 智能钥匙
├── index.html # 入户大门
├── package.json # 公寓的“住户手册 + 装修清单”
└── vite.config.js # 建筑规范说明书
其中,package.json 就像这栋楼的住户手册 + 装修材料清单。打开它,你会看到:
{
"name": "all-vue",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}
-
dependencies:这是“入住必需品”,比如 Vue 框架本身、路由系统——没有它们,房子没法正常运转; -
devDependencies:这是“装修工具包”,只在开发时用(比如 Vite 构建工具),住户入住后就不需要了; -
scripts:这是“快捷指令”,比如npm run dev就是“启动预览模式”,npm run build是“打包交付”。
有了这份清单,任何开发者都能一键还原你的整套环境——就像照着宜家说明书组装家具一样可靠。
🚪 第三步:认识“大门”——index.html 的两个秘密
虽然现代 Vue 应用的逻辑几乎全在 JavaScript 和 .vue 文件里,但一切的起点,其实是这个看似简单的 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>all-vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
别小看这十几行代码,它藏着两个关键设计:
🔌 1. <div id="app"></div>:Vue 的“插座”
你可以把它想象成墙上预留的一个智能插座面板。它本身空无一物,但一旦通电(Vue 应用启动),就会自动“投影”出整个用户界面。
在 main.js 中,我们这样写:
createApp(App).mount('#app')
这句话的意思就是:“请把 App.vue 这个‘客厅’的内容,投射到 id 为 app 的那个插座上。”
没有这个插座,Vue 再厉害也无处施展;有了它,动态内容才能在静态 HTML 中生根发芽。
⚡ 2. <script type="module" src="/src/main.js"></script>:原生 ES 模块的魔法
注意这里的 type="module"。这是现代浏览器支持的一种原生模块加载方式。传统脚本是“一股脑全塞进来”,而模块化脚本则像快递包裹——每个文件独立打包,按需引用,互不干扰。
Vite 正是利用了这一特性,无需打包即可直接在浏览器中运行模块化的代码。这意味着:
- 开发时启动飞快(冷启动快);
- 修改文件后热更新极快(HMR 精准替换);
- 代码结构清晰,符合现代工程规范。
所以,index.html 不仅是入口,更是连接静态 HTML 世界与动态 Vue 世界的桥梁。
🔑 第四步:打造“钥匙”——main.js 如何启动应用
有了大门,就得有钥匙。main.js 就是这把精密的电子钥匙,负责激活整套智能家居系统:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'
createApp(App).use(router).mount('#app')
这段代码做了三件事,环环相扣:
-
引入核心模块:从 Vue 拿到“造房子”的工具(
createApp),从本地拿到“客厅设计图”(App.vue)和“导航系统”(router); -
组装系统:用
.use(router)把导航插件装进主程序; -
插入插座:
.mount('#app')表示:“请把这套系统通电安装在index.html中 id 为app的插座上。”
没有这把钥匙,再漂亮的客厅也只是一堆图纸;有了它,整个房子才真正“活”起来。
💡 第五步:点亮客厅——根组件 App.vue
钥匙转动,门开了,我们走进 App.vue —— 这是所有功能的总控中心:
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view />
</div>
</template>
多人一开始会直接写
<div>Home | About</div>,但这只是静态文字。要让它们变成可点击的导航,就得用 Vue Router 提供的<router-link>组件。
这里有两个核心元素:
-
<router-link>:智能门把手,点击不刷新页面,只切换内容; -
<router-view />:魔法地板,当前该展示哪个房间,它就实时投影出来。
虽然原始文件只写了 HomeAbout,但正确的写法应如上所示——让文字变成可交互的导航。
🗺️ 第六步:装上导航系统——配置 Vue Router
路由,就像是你家里的智能导航系统。没有它,你只能待在客厅;有了它,你才能自由穿梭于各个房间。
我们在 src/router/index.js 中这样配置:
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/about', name: 'About', component: About }
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
这段代码的意思是:
- 当用户访问
/(也就是主页),就显示Home.vue这个房间; - 当用户访问
/about,就带他去About.vue那个房间。
注意这里用了 createWebHashHistory(),这意味着网址会变成 http://localhost:5173/#/about。那个 # 就像门牌号里的“分隔符”,告诉系统:“后面的部分是内部房间号,不是新地址”。
🛋️ 第七步:布置房间——编写页面组件
现在,我们来装修两个房间。
首页(Home.vue)
<template>
<div>
<h1>Home</h1>
</div>
</template>
关于页(About.vue)
<template>
<div>
<h1>About</h1>
</div>
</template>
每个 .vue 文件都是一个自包含的“功能单元”:有自己的结构(template)、逻辑(script)和样式(style)。它们彼此隔离,却能通过路由无缝切换。
🎨 第八步:美化家园——全局样式 style.css
虽然功能齐备,但房子还是灰扑扑的。这时候,style.css 就派上用场了。你可以在这里写:
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
}
nav {
padding: 1rem;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
就像给墙壁刷漆、给地板打蜡,让整个家更温馨舒适。
▶️ 最后一步:启动你的 Vue 家园!
现在,所有“装修材料”都已就位——地基打好了(Vite 项目)、大门装上了(index.html)、钥匙配好了(main.js)、客厅布置妥当(App.vue),连房间(Home.vue、About.vue)和导航系统(Vue Router)也都调试完毕。是时候打开电闸,点亮整栋房子了!
请在终端(命令行)中依次执行以下两条命令(确保你已在 all-vue 项目目录下):
# 第一步:安装“住户手册”里列出的所有依赖(比如 Vue 和路由)
npm install
# 第二步:启动开发服务器——相当于按下“智能家居总开关”
npm run dev
运行成功后,你会看到类似这样的提示:
VITE v5.0.0 ready in 320 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
这时,只需打开浏览器,访问 http://localhost:5173/ (端口号可能略有不同),就能看到你的 Vue 小家啦!
- 点击 Home,客厅中央显示 “Home”;
- 点击 About,瞬间切换到 “About” 页面——全程无需刷新,就像在家自由走动一样丝滑。
🎉 恭喜你!你不仅看懂了代码,还亲手让它跑起来了!
这不再是一堆抽象的文件,而是一个真正能交互的 Web 应用。你已经完成了从“零”到“一”的飞跃——而这,正是所有伟大项目的起点。
🧩 总结:Vue 项目的“生活化”逻辑链
让我们用一次智能家居入住体验来串起全过程:
- Vite 是开发商:提供标准化精装修样板间;
-
index.html 是入户门:设有智能插座(
#app)和模块化接线口(type="module"); - main.js 是电子钥匙:插入后激活整套系统;
- App.vue 是中央控制台:集成导航与内容展示区;
- Vue Router 是室内导航图:定义各房间路径;
- Home.vue / About.vue 是功能房间:各自独立,按需进入;
- style.css 是全屋软装方案:统一视觉风格。
✨ 写在最后:你已经站在 Vue 的门口
这个 all-vue 项目虽小,却包含了现代 Vue 应用的核心骨架:组件化 + 路由 + 响应式 + 工程化构建。你不需要一开始就懂所有细节,就像学骑自行车,先扶稳车把,再慢慢蹬脚踏。
当你运行 npm run dev,看到浏览器里出现“Home”和“About”两个链接,并能自由切换时——恭喜你,你已经成功迈出了 Vue 开发的第一步!
接下来,你可以:
- 在 Home 里加一张图片;
- 在 About 里写一段自我介绍;
- 用 CSS 让导航栏变彩色;
- 甚至添加第三个页面……
编程不是魔法,而是一步步搭建的过程。而你,已经搭好了第一块积木。
Vue的“小外挂”:玩转自定义指令
🔥 以龙息淬炼代码,在时光灰烬中重铸技术星河 !
欢迎来到
晷龙烬的博客小窝✨! 这里记录技术学习点滴,分享实用技巧,偶尔聊聊奇思妙想~原创内容✍️,转载请注明出处~感谢支持❤️!请尊重原创📩! 欢迎在评论区交流🌟!
引言
你好呀!在Vue的世界里,我们每天都在用 v-model、v-if 这些内置指令,它们就像官方给我们的“瑞士军刀”,特别好用。但有时候,我们总想搞点“个性化”操作,比如一进页面就让某个输入框自动聚焦,或者给按钮加个“防抖”防止用户疯狂点击。
这时候,Vue的“自定义指令”就该闪亮登场了!它就像是你给Vue安装的“小外挂”,让你能自己定义一些特殊的DOM行为。今天,咱们就把它聊明白。
一、 自定义指令是啥?能吃吗?
简单说,自定义指令就是一套你自己写的、能用在HTML元素上的“操作说明书” 。
Vue的核心思想是“数据驱动视图”,我们通常通过改变数据来让视图自动更新。但有些时候,我们不得不直接去操作真实的DOM元素(比如让输入框聚焦、初始化一个第三方库),这些“副作用”操作,就是自定义指令的用武之地。
你可以把它理解成一个可复用的“DOM操作工具箱” 。哪里需要,就“v-你的指令名”一下,工具箱里的工具就会自动在那个元素上工作。
二、 怎么创建一个“小外挂”?
创建自定义指令主要有两种方式:全局注册和局部注册。咱们先看最常用的全局注册,一次定义,全项目通用。
1. 全局注册:给Vue加个“全家桶”工具 在你的主文件(比如 main.js)里,可以这样写:
// 定义一个名为 `v-focus` 的指令
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
用起来超简单,在模板里直接:
<input v-focus>
这样,只要这个 input 元素被插入页面,它就会自动获得焦点,用户可以直接打字,体验满分!
2. 局部注册:某个组件私有的“小工具” 如果这个指令只在某个特定组件里用,可以把它定义在该组件内部:
export default {
directives: {
focus: {
inserted: function (el) {
el.focus()
}
}
}
}
用法和全局的一样。这种方式让指令的作用范围更清晰。
三、 指令的“生命周期钩子”
一个指令不是简单执行一下就完了,它也有自己的“人生阶段”,Vue提供了几个钩子函数让我们在关键时刻介入:
-
bind:只调用一次,指令第一次绑定到元素时调用。可以在这里做一次性的初始化设置。 -
inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档)。上面让输入框聚焦的例子,就用这个钩子。 -
update:所在组件的 VNode 更新时调用,但可能发生在其子 VNode 更新之前。指令的值可能发生了变化,也可能没有。 -
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。 -
unbind:只调用一次,指令与元素解绑时调用。适合在这里做清理工作,比如移除事件监听器,防止内存泄漏。
这些钩子函数都会接收到几个参数,最常用的是 el(指令绑定的元素)和 binding(一个包含指令信息的对象)。
四、 钩子函数的参数详解
让我们深入看看 binding 这个对象,它包含了指令的所有关键信息:
-
name:指令名,不包括v-前缀。 -
value:指令的绑定值。例如v-my-directive="1 + 1"中,绑定值是2。 -
oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子中可用。 -
expression:字符串形式的指令表达式。例如v-my-directive="1 + 1"中,表达式是"1 + 1"。 -
arg:传给指令的参数。例如v-my-directive:foo中,参数是"foo"。 -
modifiers:一个包含修饰符的对象。例如v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。
理解这些参数,你就能写出更灵活、强大的指令。
五、 来点更深入实用的例子
光说不练假把式,咱们看几个深入、并且实用的场景。
例子1:按钮权限控制
假设我们有不同权限的用户,有些按钮只有管理员能点。
Vue.directive('permission', {
inserted: function (el, binding) {
// binding.value 就是我们传给指令的值,比如用户角色
const userRole = 'user' // 假设当前用户是普通用户
const requiredRole = binding.value // 指令要求的管理员角色 'admin'
if (userRole !== requiredRole) {
// 如果不是管理员,就把按钮禁用或者隐藏
el.style.display = 'none'
// 或者 el.disabled = true
}
}
})
模板里可以这样用:
<button v-permission="'admin'">删除文章</button>
普通用户就看不到这个删除按钮啦,权限管理轻松实现。
例子2:按钮防重复点击
// main.js(Vue2 全局注册)
Vue.directive('throttle', {
// 元素插入DOM时绑定事件
inserted(el, binding) {
// 1. 基础配置:默认节流间隔500ms,支持通过参数自定义(如 v-throttle:1000)
const delay = binding.arg ? Number(binding.arg) : 500;
// 2. 校验回调函数(避免非函数报错)
const callback = typeof binding.value === 'function' ? binding.value : () => {};
// 3. 核心变量:记录上一次执行时间
let lastClickTime = 0;
// 4. 节流点击事件处理函数
const handleClick = () => {
const now = Date.now();
// 距离上次点击超过设定间隔,才执行回调
if (now - lastClickTime >= delay) {
callback(); // 执行业务逻辑
lastClickTime = now; // 更新最后点击时间
}
};
// 5. 绑定点击事件 + 缓存函数(方便后续销毁)
el.addEventListener('click', handleClick);
el._throttleClick = handleClick; // 把函数存到元素上
},
// 元素销毁时清理事件(避免内存泄漏)
unbind(el) {
el.removeEventListener('click', el._throttleClick);
el._throttleClick = null; // 清空缓存
}
});
使用方式:
<template>
<!-- 1. 默认500ms节流 -->
<button v-throttle="handleClick">默认节流点击</button>
<!-- 2. 自定义1000ms节流(通过参数指定) -->
<button v-throttle:1000="handleClick">1秒节流点击</button>
</template>
例子3:图片懒加载 这是一个非常经典的自定义指令应用场景,可以大幅提升页面加载性能。
Vue.directive('lazy', {
inserted: function (el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 图片进入视口
const img = entry.target
img.src = binding.value // 将data-src的值赋给src
observer.unobserve(img) // 停止观察
}
})
}, {
rootMargin: '0px',
threshold: 0.1
})
observer.observe(el)
// 存储observer,以便在unbind时清理
el._lazyObserver = observer
},
unbind: function (el) {
if (el._lazyObserver) {
el._lazyObserver.disconnect()
}
}
})
HTML中使用:
<img v-lazy="'https://example.com/image.jpg'" alt="懒加载图片">
六、 自定义指令的适用场景总结
- DOM操作封装:聚焦、选择文本、内容复制等。
- 事件处理优化:防抖、节流、长按、双击等。
- UI功能增强:拖拽、滚动监听、无限滚动、懒加载。
- 权限与状态控制:按钮权限、元素可见性、功能开关。
- 第三方库集成:图表库初始化、富文本编辑器、地图组件。
- 样式与动画:动态样式绑定、动画触发、主题切换。
结语
自定义指令不是什么高深莫测的黑魔法,它就是一个帮你封装DOM操作、提升代码复用性和可维护性的好帮手。下次当你发现自己在多个地方重复写着相同的DOM操作代码时,不妨停下来想想:“是不是可以抽象成一个自定义指令?”
从简单的自动聚焦,到复杂的权限管理、性能优化,自定义指令都能让你的Vue项目代码更干净、更专业。希望这篇文章能帮你打开思路,在实际项目中用起来!
—— 完 ——
✨ 至此结束 ✨
💡 点赞关注,解锁更多技术干货!
我是 晷龙烬 期待与你的下次相遇~
【性能优化】给Vue应用“瘦身”:让你的网页快如闪电的烹饪秘籍
欢迎使用我的小程序👇👇👇👇
想象一下:你精心烹制的Vue应用端上桌,用户却因加载慢而转身离开...别担心!今天我来教你几招性能优化“烹饪技巧”,让你的应用“色香味”俱全!
🍳 前菜:为什么需要性能优化?
你的Vue应用就像一道菜,用户希望:
- 快速上菜(首屏加载快)
- 口感顺滑(交互流畅)
- 回味无穷(使用体验好)
性能差的网站就像冷掉的披萨,再好吃也没人爱!
🔪 主菜:Vue性能优化七大秘籍
1. 代码打包:给食材“瘦身”
// vue.config.js - 就像主厨的调味秘诀
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
chainWebpack: config => {
// 开启Gzip压缩 - 像真空压缩食材
config.plugin('compression').use(require('compression-webpack-plugin'))
// 拆分包 - 分开装盘更优雅
config.optimization.splitChunks({
chunks: 'all',
maxSize: 244 * 1024, // 每个“餐盘”不超过244KB
})
}
})
小技巧:使用 vue-cli 的 --report 参数生成打包分析报告,像X光一样看清你的“脂肪”分布!
2. 懒加载:像自助餐一样按需取用
<template>
<div>
<!-- 常规加载 - 一次全上桌 -->
<!-- <HeavyComponent /> -->
<!-- 懒加载 - 客人需要时才上菜 -->
<button @click="showComponent = true">点这道菜</button>
<component v-if="showComponent" :is="HeavyComponent" />
</div>
</template>
<script>
export default {
data() {
return {
showComponent: false,
HeavyComponent: () => import('./components/HeavyComponent.vue')
}
}
}
</script>
路由懒加载更是神器:
// 路由配置 - 每道菜单独包装
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue') // 客人进入餐厅才准备这道菜
}
]
3. 虚拟滚动:长列表的“折叠椅”
想象一下10000个项目的列表——就像要同时展示10000道菜,不可能!虚拟滚动只渲染可视区域:
<template>
<!-- 普通列表 - 所有菜都摆出来 -->
<!-- <div v-for="item in 10000" :key="item.id">{{ item.name }}</div> -->
<!-- 虚拟滚动 - 只摆客人能看到的几道 -->
<VirtualList :items="largeList" :item-height="50">
<template #default="{ item }">
<ListItem :item="item" />
</template>
</VirtualList>
</template>
4. 计算属性 vs 方法:聪明的“预制菜”
<template>
<div>
<!-- 方法调用 - 每次点单都现做(性能差) -->
<!-- <p>{{ calculateTotal() }}</p> -->
<!-- 计算属性 - 提前准备好的预制菜(性能好) -->
<p>{{ totalPrice }}</p>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ price: 100, quantity: 2 },
{ price: 200, quantity: 1 }
]
}
},
computed: {
// 依赖变化时才重新计算
totalPrice() {
return this.items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
)
}
},
methods: {
// 每次渲染都会执行
calculateTotal() {
return this.items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
)
}
}
}
</script>
5. Keep-Alive:给组件盖“保温盖”
<template>
<!-- 常规组件 - 离开就倒掉 -->
<!-- <TabContent /> -->
<!-- Keep-Alive - 盖上保温盖,回来还是热的 -->
<keep-alive>
<component :is="currentTab" />
</keep-alive>
</template>
6. 图片优化:给视觉“减负”
懒加载图片:
<template>
<img
v-lazy="imageUrl"
alt="美味佳肴"
loading="lazy" <!-- 原生懒加载 -->
/>
</template>
<script>
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.3, // 提前1.3屏加载
attempt: 3 // 尝试3次加载
})
</script>
现代图片格式:
- WebP:比JPEG小25-35%
- AVIF:下一代格式,压缩率更高
7. 监控与分析:安装“厨房摄像头”
// 性能监控
export default {
mounted() {
// 测量组件加载时间
const start = performance.now()
this.$nextTick(() => {
const end = performance.now()
console.log(`组件渲染耗时: ${end - start}ms`)
// 发送到监控平台
this.sendMetrics('component_render_time', end - start)
})
}
}
🍰 甜点:快速优化清单
✅ 立即能做的:
- 开启Gzip压缩(服务器配置)
- 使用路由懒加载
- 压缩图片(Tinypng.com)
- 移除未使用的代码
✅ 进阶技巧:
- 使用CDN分发静态资源
- 服务端渲染(SSR)改善首屏
- PWA提升离线体验
- Web Workers处理繁重计算
📊 成果展示:优化前后对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏加载 | 4.2s | 1.8s | ⬇️ 57% |
| 打包体积 | 2.1MB | 890KB | ⬇️ 58% |
| Lighthouse评分 | 62 | 92 | ⬆️ 30分 |
🎯 结语:优化是持续的过程
性能优化就像保持身材——不是一次性节食,而是养成健康习惯。每周花15分钟检查你的Vue应用:
- 运行
npm run build -- --report - 查看Lighthouse报告
- 优化最慢的3个组件
记住:每毫秒都很重要——100毫秒的延迟就能让转化率下降7%!
今日主厨推荐:从路由懒加载开始,这是性价比最高的优化方式!
💡 小测验:你的Vue应用现在“体重”多少?运行 npm run build 看看打包后的体积,在评论区分享你的“减肥”成果吧!
优化愉快,让你的Vue应用飞起来!🚀
Vue项目中字体引入:完整实操指南
给DOM元素加超能力:Vue自定义指令入门指南
Vue原型链:让你的组件继承“超能力”
用 Vite 搭建现代化 Vue 3 项目:从零到工程化入门
微信内容emoji表情包编辑器 + vue3 + ts + WrchatEmogi Editor
wechat-emoji-editor组件效果
| 功能 | 效果 |
|---|---|
| 添加表情 | |
| 预览 | |
| 目录 |
代码-组件: (需要源码留言)
//
import WechatEmojiEditor from '@/components/wechat-emoji-editor/index.vue'
const welcomeMsg = ''
<WechatEmojiEditor
:rows="4"
type="textarea"
:maxlength="1200"
show-word-limit
v-model="welcomeMsg"
></WechatEmojiEditor>
图片打包脚本 (需要源码留言)
package.json
{
"name": "build",
"version": "1.0.0",
"description": "build",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"buildImg": "sudo node sharpbuild2.js",
"build": "node sharpbuild3.js && vite build"
},
"devDependencies": {
"sharp": "^0.34.3"
}
}
sharpbuild脚本
```
const imgs = [
{
cn: "[微笑]",
hk: "[微笑]",
us: "[Smile]",
code: "/::)",
web_code: "/微笑",
src: "/src/assets/emojis/Smile.png",
},
{
cn: "[撇嘴]",
hk: "[撇嘴]",
us: "[Grimace]",
code: "/::~",
web_code: "/撇嘴",
src: "/src/assets/emojis/Grimace.png",
},
// ...
]
console.log('imgs', imgs)
const list = imgs.map((item, index) => {
return {
input: `${__dirname}${item.src}`,
top: Math.floor(index / 10) * 128, // 垂直偏移量
left: (index % 10) * 128, // 水平偏移量
};
});
const sharp = require("sharp");
const fs = require("fs");
// 指定输入文件路径
const url = `${__dirname}/src/assets/wechat.png`;
sharp({
create: {
width: 10 * 128,
height: 11 * 128,
channels: 3,
background: { r: 255, g: 255, b: 255, alpha: 0 },
},
})
.composite(list)
.toFile(url)
.then((info) => {
console.log("Image composite successfully:", info);
})
.catch((error) => {
console.error("Error processing image:", error);
});
```