文章目录
- 一、项目搭建
-
- [1.1 生态工具对比](#1.1 生态工具对比)
-
- [包管理 pnpm(npm、yarn)](#包管理 pnpm(npm、yarn))
- [打包工具 vite (webpack)](#打包工具 vite (webpack))
- [1.2 项目创建](#1.2 项目创建)
-
- [1.2.0 项目搭建](#1.2.0 项目搭建)
- [1.2.1 node 版本](#1.2.1 node 版本)
- [1.2.2 corepack](#1.2.2 corepack)
- [1.2.3 npm install -g](#1.2.3 npm install -g)
- [1.2.4 pnpm](#1.2.4 pnpm)
- [1.2.5 pnpm add @types/node --save-dev](#1.2.5 pnpm add @types/node --save-dev)
- [1.2.6 pnpm create vite](#1.2.6 pnpm create vite)
- [1.3 集成配置](#1.3 集成配置)
-
- tsconfig.json
- vite.config.ts
- eslint(可选)
- [集成 prittier(可选)](#集成 prittier(可选))
- pinia
- [集成 vue-router4](#集成 vue-router4)
- [集成 vueUse](#集成 vueUse)
- [集成 sass](#集成 sass)
- [集成 axios](#集成 axios)
- [封装请求参数和响应数据的所有 api](#封装请求参数和响应数据的所有 api)
- [引入 antdvue](#引入 antdvue)
- [使用 antdvue](#使用 antdvue)
- [二、vue 用法](#二、vue 用法)
-
- [2.1 动画](#2.1 动画)
-
- [2.1.1 原生 css 动画](#2.1.1 原生 css 动画)
- [2.1.2 vue 动画](#2.1.2 vue 动画)
- [2.2 jsx](#2.2 jsx)
-
- 简单示例
- [todo.jsx 示例](#todo.jsx 示例)
- [三、AntdVue 组件库使用](#三、AntdVue 组件库使用)
-
- [3.1 弹窗](#3.1 弹窗)
背景:现在前端框架多、版本多、迭代快。以 vue 为例,就有 vue2/vue3,依赖管理工具就有 npm/yarn,打包工具就有 webpack/vite,UI 框架就有 antd-vue/element_ui/element_ui_plus,没有统一的路径可循,记忆心智负担较重。
为了方便后续快速搭建项目,能利用最新的框架,并且减轻心智负担,本文采用 vue3 + yarn + vite + antd-vue,方便快速上手。
一、项目搭建
1.1 生态工具对比
包管理 pnpm(npm、yarn)
- 最早是 npm2,其node_modules 有多层嵌套问题,导致文件路径超过 windows 最长路径限制
- 之后 yarn 解决了上述路径问题,将 node_modules 全部平铺,但仍有如下问题:
- 幽灵依赖问题:即旧版本的第三方库B1引用了C库,我们即可在项目A中用 require 引用 C 库。但当B1升级到新版本B2后,不再依赖C库了,C库即会被从 node_modules 中移除。但我们自己的代码A就会报错找不到C库的错误
- 某库的多版本:占用多份磁盘空间。例如我们自己的项目A引用了C库的C1版本,我们引用的B库又依赖于C库的C2版本。那么 node_modules 中就同时存在 C1 和 C2 两个版本,浪费磁盘空间。
- 同时,npm 3 也跟进了上述 yarn 的进展
- 最新的 pnpm 则完全解决了上述问题,有如下两个优点:
- 小:其整个电脑的所有项目共用同一份依赖文件(例如将C库的C1和C2两个版本都放在电脑全局的 ~/CommonStore文件夹下,然后A项目的各依赖文件都用软链接or硬链接指向全局路径)。这样就大大节省了各项目A、X、Y之间共用的B库和C库的冗余磁盘占用(毕竟只存一份嘛)。
- 快:因为是链接形式不需要拷贝,所以速度快。
pnpm 是凭什么对 npm 和 yarn 降维打击的
pnpm官网
打包工具 vite (webpack)
- 生产环境速度相等,但开发环境 vite 比 webpack 快多了, 因为vite分阶段编译(用户点击哪个路由,才编译哪个模块),而 webpack 需要全部编译完。vite 在开发阶段明显减少了编译等待时间(1min到1s的提升幸福感)。
- 若用 vite 初始化项目则配置文件为 vite.config.ts,若用 vue-cli 初始化项目则配置文件为 vue.config.js,配置内容近似。
1.2 项目创建
1.2.0 项目搭建
其中各命令的解释, 详见下文
bash
nvm install 22
nvm alias default v22.14.0
nvm use 22
node -v # v22.14.0 (nvm ls 可查看/选择版本)
npm install -g corepack # 升级最新版 corepack
corepack -v # 查 corepack 版本 0.31.0
corepack enable # 安装 pnpm
corepack prepare pnpm@latest --activate # 升级 pnpm 到最新版本
pnpm -v # 10.5.2
pnpm create vite fe --template vue-ts # 初始化项目
cd fe && pnpm install && pnpm run dev # 安装依赖并run
pnpm add @types/node --save-dev # 为保证 node 的使用. 这是 Node.js 的 TypeScript 类型定义文件. 详见下文
1.2.1 node 版本
bash
# 安装 nvm (~/.zshrc 可配置 nvm 的环境变量如下:)
# nvm
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion
nvm use 22
nvm ls
node 版本, 不会对整套技术栈有太大影响, 反而建议尽量新, 这样可以支持更多特性:
- Vue3 对 Node.js 版本的依赖较低,主要依赖于浏览器的兼容性。Node.js 20 和 22 都不会对 Vue3 的兼容性产生显著影响。
- Ant Design Vue 是一个 UI 组件库,与 Node.js 版本的直接关联较小。只要 Node.js 版本支持 ES Module 和 CommonJS,就不会有兼容性问题。
- TypeScript 的编译和运行与 Node.js 版本的关联较小。Node.js 20 和 22 都支持 TypeScript 的最新特性。
- Vite 4 依赖于 Node.js 的模块系统和文件系统 API。Node.js 20 和 22 都会支持 Vite 4 的核心功能,但建议使用较新的 Node.js 版本以获得更好的性能和稳定性。
- PNPM 是一个包管理工具,与 Node.js 版本的兼容性较好。Node.js 20 和 22 都支持 PNPM 的最新特性。
- Pinia 是 Vue 的状态管理库,与 Node.js 版本的直接关联较小。只要 Node.js 版本支持 ES Module,就不会有兼容性问题。
1.2.2 corepack
Corepack 是 Node.js 内置的一个包管理工具管理器(Package Manager Manager),用于管理不同的 JavaScript 包管理工具(如 npm、yarn、pnpm 等)。它的主要目的是简化包管理工具的安装和版本管理,确保开发者在不同项目中使用一致的包管理工具版本。
Corepack 从 Node.js 16.9.0 开始默认启用,并在后续版本中逐步增强。
corepack enable 是一个命令,用于启用 Corepack 的功能。具体来说:运行 corepack enable 后,Corepack 会在系统中启用,并准备好管理包管理工具。启用后,你可以直接使用 yarn 或 pnpm 等命令,而无需手动安装这些工具。
默认 Node.js 16.9.0 及更高版本中,Corepack 默认是启用的,因此通常不需要手动运行 corepack enable。
示例:
启用 Corepack:corepack enable
安装特定版本的包管理工具:corepack prepare [email protected] --activate
在项目中使用指定的包管理工具:在 package.json 中指定包管理工具及其版本:
json
{
"packageManager": "[email protected]"
}
然后运行 yarn 或 pnpm 时,Corepack 会自动使用指定的版本
1.2.3 npm install -g
npm install -g vite @vue/cli
安装的是 命令行工具(CLI), 而不是 nodejs 的代码库.
-
命令行工具(CLI):
vite 和 @vue/cli 都是命令行工具,用于快速创建、构建和管理项目。
安装后,你可以在终端中直接运行 vite 或 vue 命令来执行相关操作。
-
全局安装:
-g 表示全局安装(--global),这些工具会被安装到系统的全局 node_modules 目录中,而不是当前项目的 node_modules。
全局安装后,你可以在任何目录下使用这些命令。
-
vite 工具的使用:
一个现代化的前端构建工具,用于快速启动和开发项目(尤其是 Vue、React 等框架)。
安装后,你可以使用 vite 命令来创建新项目、启动开发服务器或构建生产版本。
示例:
vite create my-project
cd my-project
vite dev -
@vue/cli 工具的使用:
Vue.js 的官方命令行工具,用于快速搭建 Vue 项目。
安装后,你可以使用 vue 命令来创建、管理和构建 Vue 项目。
示例:
vue create my-project
cd my-project
vue serve
-
安装后的位置
全局安装的 cli 工具会被安装到 全局 node_modules 目录中, mac 为
~/.nvm/versions/node/<版本>/lib/node_modules
. 另外这些工具的可执行文件会被链接到系统的 PATH 中, 所以可以直接在终端执行 vite 或 vue 命令.
npm install 可加/不加 -g 参数
pnpm add 可加/不加 -g 参数
上述二者, 都既可以安装 命令行 cli, 也可以安装代码库.
如果一个项目的 package.json 中含 "bin" 则表示为 命令行 cli, 否则表示为代码库
例如 vite 的 package.json 如下:
json
{
"name": "vite",
"bin": {
"vite": "bin/vite.js"
}
}
例如 lodash 的 package.json 如下:
json
{
"name": "lodash",
"main": "lodash.js"
}
如果命令行, 未指定 -g, 则被安装在本项目的 node_modules 目录中(而不在全局 node_modules 目录中), 则可通过 npx
运行, 例如 npx vite
1.2.4 pnpm
配置方式:
bash
Appended new lines to /Users/y/.zshrc
Next configuration changes were made:
export PNPM_HOME="/Users/y/Library/pnpm"
case ":$PATH:" in
*":$PNPM_HOME:"*) ;;
*) export PATH="$PNPM_HOME:$PATH" ;;
esac
To start using pnpm, run:
source /Users/y/.zshrc
升级最新版 pnpm self-update, (注意项目的 package.json 文件中写死了 pnpm 的旧版本号, 可手动改该文件, 或切换到非项目目录再 pnpm -v).
查看版本 pnpm -v
pnpm store path
可查看其全局存储路径:
bash
# pnpm store path
/Users/abc/Library/pnpm/store/v3
# du -sh /Users/abc/Library/pnpm/store/v3
335M /Users/abc/Library/pnpm/store/v3
安装生产依赖:
如果包是项目运行所必需的,则可不填 (或 --save)(或 -S):pnpm add lodash
全局安装:
如果包需要全局使用,使用 -g:pnpm add typescript -g
删除包:
如果需要删除包,使用 uninstall:pnpm uninstall @types/node --save-dev
1.2.5 pnpm add @types/node --save-dev
为保证 node 的使用. 这是 Node.js 的 TypeScript 类型定义文件.
--save-dev 是把包作为 开发依赖 devDependencies 安装(而不是作为生产依赖).
pnpm 会从 npm 仓库下载 @types/node 包,并将其安装到项目的 node_modules 目录中. @types/node 会被添加到 package.json 的 devDependencies 字段中.
json
{
"devDependencies": {
"@types/node": "^20.0.0"
}
}
安装后,TypeScript 编译器会自动识别 Node.js 的类型,避免类型错误
使用场景:
当你使用 TypeScript 开发 Node.js 项目时,需要安装 @types/node 来获得 Node.js API 的类型支持。
例如,在代码中使用 fs 模块时,TypeScript 会检查 fs.readFile 的参数和返回值类型。
示例:
假设你在开发一个 Node.js + TypeScript 项目,代码中使用了 fs 模块:
ts
import fs from 'fs';
fs.readFile('file.txt', 'utf-8', (err, data) => {
if (err) throw err;
console.log(data);
});
如果没有安装 @types/node,TypeScript 会报错,因为它无法识别 fs 模块的类型。安装 @types/node 后,TypeScript 就能正确识别 fs 的类型。
1.2.6 pnpm create vite
可能有一种现象: vite -v 和 vue -v 都无输出(说明没安装), 但 pnpm create vite fe --template vue-ts 却可以正常创建 vite-vue-ts 的项目.
- vite -v 和 vue -v 无输出, 是因为没有全局安装.
- pnpm create vite 是一个复合命令, 在其内部会下载并运行 create-vite 这个工具, 但不会全局安装 vite. 而是会在项目内安装(即在项目内的 node_modules 目录内), 即会在 package.json 的 devDependencies 中 配置如下:
json
"devDependencies": {
"vite": "^6.2.0"
}
在项目中, pnpm vite -v
即可运行本地安装的 vite
bash
# pnpm vite -v
vite/6.2.0 darwin-arm64 node-v20.18.3
- 建议在项目内安装 vite, 而不是在全局安装 vite. 这样避免因全局不同版本导致的兼容问题. 项目内可通过 package.json 管理, 便于团队协作版本控制.
1.3 集成配置
tsconfig.json
tsconfig.json 就用 脚手架 生成的即可, 无需修改
最新的脚手架分为如下三个文件:
tsconfig.json 如下:
json
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
tsconfig.app.json 如下:
json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
tsconfig.node.json 如下:
json
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
vite.config.ts
修改 vite.config.ts
参考. 其中关键是指定 @ 为 src 目录的别名.
(可选) 新建 /src/assets/styles/variables.scss
用于放公共的 scss 变量,其可被 .scss 文件或 .vue 文件 引用。
bash
pnpm add mockjs -S # 用于生成随机数据的 JavaScript 库,主要用于前端开发中的模拟数据(Mock Data)
pnpm add vite-plugin-mock -D # 是一个 Vite 插件,用于在 Vite 项目中集成 Mock 数据功能
pnpm add postcss-px-to-viewport -S # 是一个 PostCSS 插件,用于将 CSS 中的 px 单位转换为 viewport 单位(如 vw、vh),以实现移动端自适应布局
ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from "vite-plugin-mock";
import * as path from "path";
let backendAddr = process.env.BACKEND_ADDR || 'http://192.168.2.180:334';
export default defineConfig({
// base: "/foo/", // 开发或生产环境服务的公共基础路径
base: "/", // 开发或生产环境服务的公共基础路径
optimizeDeps: {
force: true, // 强制进行依赖预构建
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use '/src/assets/styles/variables.scss';`, // 引入全局变量文件
},
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), // 路径别名
},
extensions: [".js", ".ts", ".json"], // 导入时想要省略的扩展名列表
},
server: {
host: true, // 监听所有地址
proxy: {
// // 字符串简写写法
"/foo": "http://localhost:4567",
// // 选项写法
"^/api": {
target: backendAddr,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, ""),
},
"^/ws": {
target: backendAddr,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/ws/, ""),
}
},
},
build: {
outDir: "dist", // 打包文件的输出目录
assetsDir: "static", // 静态资源的存放目录
assetsInlineLimit: 4096, // 图片转 base64 编码的阈值
},
plugins: [vue(), viteMockServe()],
});
启动脚本
通常,本地启动前端后,需要连本机环境和联调环境(和后端同学调接口),则可在 vite.config.js 中设置如下(上文已包含):
js
let backendAddr = process.env.BACKEND_ADDR || 'http://192.168.2.99:334'; // 若有环境变量则连本地后端服务,若无则连联调机器的后端服务
// 并在 server.proxy 中引用该变量即可,示例如下:
server: {
host: true, // 监听所有地址
proxy: {
"^/ws": {
target: backendAddr,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/ws/, ""),
},
}
则指定环境变量启动即可连本地后端服务:
bash
BACKEND_ADDR='http://127.0.0.1:9999' pnpm run dev
eslint(可选)
bash
pnpm add eslint eslint-plugin-vue --save-dev
pnpm add @typescript-eslint/parser --save-dev
创建配置文件: .eslintrc.js
js
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
// override/add rules settings here, such as:
}
};
创建忽略文件:.eslintignore
bash
node_modules/
dist/
index.html
命令行式运行:修改 package.json
bash
{
...
"scripts": {
...
"eslint:comment": "使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .js 和 .vue 的文件",
"eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
}
...
}
集成 prittier(可选)
bash
pnpm add prettier eslint-config-prettier eslint-plugin-prettier --save-dev
创建配置文件: prettier.config.js 或 .prettierrc.js
js
module.exports = {
// 一行最多 80 字符
printWidth: 80,
// 使用 4 个空格缩进
tabWidth: 4,
// 不使用 tab 缩进,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号代替双引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: "as-needed",
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾使用逗号
trailingComma: "all",
// 大括号内的首尾需要空格 { foo: bar }
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: "always",
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: "preserve",
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: "css",
// 换行符使用 lf
endOfLine: "auto",
};
修改 .eslintrc.js 配置
js
module.exports = {
...
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended'
],
...
};
命令行式运行:修改 package.json
json
{
...
"scripts": {
...
"prettier:comment": "自动格式化当前目录下的所有文件",
"prettier": "prettier --write"
}
...
}
效果如下:

pinia
踩坑教训: 不可用时, 先检查是否 pnpm add 成功了, (因为 vscode 提示时好时坏)
bash
pnpm add pinia # 若报错, 则可先停止 pnpm run dev 再执行
新建 src/store 目录并在其下面创建 index.ts,导出 store
bash
import { createPinia } from 'pinia'
const store = createPinia()
export default store
在 main.ts 中引入并使用
ts
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import store from "./store";
const app = createApp(App); // 创建vue实例
app.use(store); // 挂载pinia
app.mount("#app"); // 挂载实例
定义State: 在 src/store 下面创建一个 user.ts
ts
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => {
return {
name: "张三",
};
},
actions: {
updateName(name: string) {
this.name = name;
},
},
});
获取和修改 State: 在 src/components/usePinia.vue 中使用
html
<template>
<div>{{ userStore.name }}</div>
</template>
<script lang="ts" setup>
import { useUserStore } from "@/store/user";
const userStore = useUserStore();
userStore.updateName("张三");
</script>
注意, 因为其中用到了 "@", 所以需在 tsconfig.json 或 tsconfig.app.json 中定义如下:
json
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
集成 vue-router4
bash
pnpm add vue-router
新建 src/router 目录并在其下面创建 index.ts,导出 router
ts
import { createRouter, createWebHashHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
const routes: Array<RouteRecordRaw> = [
// {
// path: "/login",
// name: "Login",
// meta: {
// title: "登录",
// keepAlive: true,
// requireAuth: false,
// },
// component: () => import("@/pages/login.vue"),
// },
{
path: "/aa",
name: "VueUse",
meta: {
title: "鼠标",
keepAlive: true,
requireAuth: true,
},
component: () => import("@/pages/vueUse.vue"), // 组件的 懒加载
},
// {
// path: "/hello",
// name: "HelloWorld",
// meta: {
// title: "计数器",
// keepAlive: true,
// requireAuth: true,
// },
// component: () => import("@/components/HelloWorld.vue"),
// },
// {
// path: "/request",
// name: "request",
// meta: {
// title: "请求页",
// keepAlive: true,
// requireAuth: true,
// },
// component: () => import("@/pages/request.vue"),
// }
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
在 main.ts 中引入 import router from '@/router';
并使用
ts
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import store from "./store";
import router from "./router"; // 或 '@/router'
const app = createApp(App); // 创建vue实例
app.use(store).use(router); // 挂载组件
app.mount("#app"); // 挂载实例
修改 App.vue
html
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>
<template>
<h1>abc</h1>
<div>123</div>
<HelloWorld msg="def" />
<RouterView />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
集成 vueUse
VueUse 是一个基于 Composition API 的实用函数集合。
bash
pnpm add @vueuse/core
新建 src/pages/vueUse.vue
如下:useMouse 只是 vueuse 的一个最基本的函数库
html
<template>
<h1>测试 use 鼠标坐标</h1>
<h3>Mouse: {{ x }} x {{ y }}</h3>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useMouse } from "@vueuse/core";
export default defineComponent({
name: "VueUse",
setup() {
const { x, y } = useMouse();
return {
x,
y,
};
},
});
</script>
localhost:5173 如下:
localhost:5173/aa 如下:
还有许多,总会有一个适合你;更多函数官方文档
localhost:/5173/hello 如下:

集成 sass
bash
pnpm add -D sass
新建 src/assets/styles/variables.scss 文件, 内容如下:
scss
$blue: #007bff;
$red: #dc3545;
$primary-color: red;
$secondary-color: blue;
使用在 .vue 文件, 因为上文已在 vite.config.ts 中 @use '/src/assets/styles/variables.scss';
, 所以直接使用即可. 例如设置 color 为 red:
html
<template>
<h1>测试 use 鼠标坐标</h1>
<h6 class="c1">{{ x }} x {{ y }}</h6>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useMouse } from "@vueuse/core";
export default defineComponent({
name: "VueUse",
setup() {
const { x, y } = useMouse();
return {
x,
y,
};
},
});
</script>
<style lang="scss">
.c1 {
color: blue; // 亲测, red/blue/yellow等不需要自己定义, 默认已经有了. 其实在 vite.config.ts 里已经 @use '/src/assets/styles/variables.scss 了, 可以再其中再定义变量, 如 $blue 等
}
</style>
localhost://5173 效果如下:

集成 axios
axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
bash
pnpm add axios
新建 src/utils/axios.ts
ts
import axios from 'axios';
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
const service = axios.create();
// Request interceptors
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// do something
return config;
},
(error: any) => {
return Promise.reject(error);
}
);
// Response interceptors
service.interceptors.response.use(
async (response: AxiosResponse) => {
console.log("response", response);
// do something
return response;
},
(error: any) => {
// do something
return Promise.reject(error);
}
);
export default service;
在页面中使用即可, 其中涉及 await 调接口, setup() 的时机调用, 调用后赋值给变量, 并在 <template>
内渲染, 完整 vue 代码和关键步骤如下:
html
<template>
<h1>测试 use 鼠标坐标</h1>
<h2 v-if="d">接口响应结果为 {{ d }}</h2>
<h3 class="c1">Mouse: {{ x }} x {{ y }}</h3>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useMouse } from "@vueuse/core"; // 安装后该引用将正常解析
import request from '@/utils/axios';
export default defineComponent({
name: "VueUse",
setup() {
const d = ref<any>(null); // 定义响应式变量, 用于存接口返回的数据
const requestRes = async () => {// 定义函数, 其调接口
try {
const result = await request({
url: '/api/abc-server/ping',
method: 'get'
})
console.log(result);
d.value = result.data; // 把接口响应的数据, 赋值给, 响应式变量
} catch (error) {
console.error("请求失败", error);
}
}
requestRes(); // 组件初始化时, 调函数
const { x, y } = useMouse(); // 将正确获取鼠标坐标
return { x, y, d }; // 所有组件内的 变量, 都要 return 出去, template 才能使用
},
});
</script>
<style lang="scss">
@import '@/assets/styles/variables.scss';
.c1 {
color: blue;
}
</style>
其对应的 vite.config.ts 的关键配置如下, 详细内容上文有介绍.
用 go 写个 http server 如下:
go
func main() {
http.HandleFunc("/ping", Ping)
http.ListenAndServe(":9999", nil)
}
func Ping(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "pong\n")
}
// curl --location --request GET 'http://localhost:9999/ping'
pong
封装请求参数和响应数据的所有 api
新建 src/api/index.ts:
ts
import * as login from './module/login';
import * as ping from './module/ping';
export default Object.assign({}, login, ping);
/*
其中 `import * as` 将 ./module/login 模块中的所有导出内容, 作为一个命名空间对象导入。
例如,如果 login.ts 导出了 loginUser 和 logoutUser 两个函数,那么 login 对象将包含这两个函数:
login = {
loginUser: Function,
logoutUser: Function,
};
*/
/*
Object.assign 是 JavaScript 的方法,用于将一个或多个对象的属性合并到目标对象中。
第一个参数是目标对象(这里是空对象 {}),后面的参数是源对象(这里是 login 和 index)。
合并后,目标对象将包含 login 和 index 的所有属性。
例如若
login = {
loginUser: Function,
logoutUser: Function,
};
index = {
getData: Function,
setData: Function,
};
则合并后的对象为
{
loginUser: Function,
logoutUser: Function,
getData: Function,
setData: Function,
}
属性冲突的情况:
其中, 如果 login 和 index 中有同名属性,Object.assign 会以后面的对象为准。例如:
login = { foo: 'login' };
index = { foo: 'index' };
则 Object.assign({}, login, index); // { foo: 'index' }
浅拷贝:
Object.assign 是浅拷贝,如果属性值是对象,拷贝的是引用而不是值。
*/
/*
export default 将 Object.assign({}, login, index) 的结果作为默认导出。
其他模块可以通过 import combined from './path/to/module'; 导入这个合并后的对象
*/
新建 src/api/module/login.ts
ts
import request from '@/utils/axios';
// 登录
// model
interface IResponseType<P = {}> {
code?: number;
status: number;
msg: string;
data: P;
}
interface ILogin {
token: string;
expires: number;
}
// function
export const login = (username: string, password: string) => {
return request<IResponseType<ILogin>>({
url: '/api/auth/login',
method: 'post',
data: {
username,
password
}
});
};
新建 src/api/module/ping.ts
ts
import request from '@/utils/axios';
// function
export const ping = () => {
return request<String>({
url: '/api/auth/login',
method: 'get'
});
};
由于使用了 typescript,所以需新增 src/types/shims-axios.d.ts
ts
import { AxiosRequestConfig } from 'axios';
// 自定义扩展axios模块
declare module 'axios' {
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<T>;
request<T = any>(config: AxiosRequestConfig): Promise<T>;
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
}
}
新建 src/pages/request.vue 页面,并在其中使用
html
<template>
<h2>这是 request 请求页</h2>
<br />
<router-link to="/">点击跳转至首页</router-link>
<button @click="requestRes()"></button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import request from '@/utils/axios';
import API from "@/api";
export default defineComponent({
name: "RequestPage",
setup() {
const requestRes = async () => {
let result = await request({
url: "/api/corgi/ping",
method: "get,"
})
console.log(result);
}
const requestResAPI = async () => {
let result = await API.login("zhangsan", "123456");
console.log(result);
}
return {
requestRes,
requestResAPI
}
}
})
</script>
效果如下:

引入 antdvue
AntdVue,引入方式参考官网
bash
pnpm add ant-design-vue
pnpm add vite-plugin-style-import --save-dev # 非必须
pnpm add vue-svg-icon
在 main.ts 全局完整引入如下:
ts
import { createApp } from 'vue'
import './style.css'
import "ant-design-vue/dist/reset.css";
import Antd from 'ant-design-vue';
import App from './App.vue'
import store from './store'
import router from './router'
createApp(App).use(router).use(store).use(Antd).mount('#app')
需要注意的是,样式文件需要单独引入。
使用 antdvue
新建 test.vue,其中用到了 <a-button>
标签
html
<template>
<h2>测试</h2>
<a-button type="primary">添加用户</a-button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import request from "@/utils/axios";
import API from "@/api";
export default defineComponent({
name: "RequestPage",
setup() {
const requestRes = async () => {
let result = await request({
url: "/api/corgi/ping",
method: "get",
});
console.log(result);
};
const requestResAPI = async () => {
let result = await API.login("zhangsan", "123456");
console.log(result);
};
return {
requestRes,
requestResAPI,
};
},
});
</script>
效果如下,说明跑通了

二、vue 用法
2.1 动画
2.1.1 原生 css 动画
transition
html
<template>
<div class="box" :style="{ width: width + 'px' }"></div>
<button @click="change">click</button>
</template>
<script lang="ts" setup>
import { ref } from "vue";
let width = ref(100);
function change() {
width.value += 100;
}
</script>
<style>
.box {
background: red;
height: 100px;
}
</style>
效果如下:

为了优化效果,可以把样式改为如下,即 width 属性需要线性过度,时间为 1s:
css
<style>
.box {
background: red;
height: 100px;
transition: width 1s linear;
}
</style>
效果如下:

animation
html
<template>
<div class="box" :style="{ width: width + 'px' }"></div>
<button @click="change">click</button>
</template>
<script lang="ts" setup>
import { ref } from "vue";
let width = ref(30);
function change() {
width.value += 100;
}
</script>
<style>
.box {
width: 30px;
height: 30px;
position: relative;
background: #d88986;
animation: move 2s linear infinite; /*持续 2s,线性变化,无限循环*/
/*
move:指定动画的名称,对应 @keyframes move 定义的动画。
2s:动画的持续时间为 2 秒。
linear:动画的时间函数为线性(匀速)。
infinite:动画无限循环。
*/
}
/* 定制动画在0%,50%,100%的位置 */
@keyframes move {
0% {
left: 0px;
}
50% {
left: 200px;
}
100% {
left: 0;
}
}
/*
@keyframes 定义了动画的关键帧,具体含义如下:
0%:动画开始时,元素的 left 值为 0px。
50%:动画进行到一半时,元素的 left 值为 200px。
100%:动画结束时,元素的 left 值回到 0px。
3. 动画效果
元素 .box 会从初始位置(left: 0px)向右移动到 left: 200px,然后再回到初始位置(left: 0px)。
整个动画持续 2 秒,匀速运动,并且无限循环。
4. 代码的完整行为
初始状态:
.box 的宽度为 30px,高度为 30px,背景色为 #d49d9b。
动画开始前,元素位于 left: 0px。
动画过程:
在 0% 时,元素位于 left: 0px。
在 50% 时(1 秒后),元素移动到 left: 200px。
在 100% 时(2 秒后),元素回到 left: 0px。
循环:
动画完成后,重新开始,无限循环。
按钮点击:
点击按钮时,.box 的宽度会增加 100px,但动画效果不受影响,继续按照 @keyframes 的定义运行。
5. 可视化效果
你会看到一个宽度为 30px 的方块,在水平方向上左右移动:
从最左侧(left: 0px)向右移动到 left: 200px,然后再回到最左侧。
每次点击按钮,方块的宽度会增加 100px,但动画的移动范围(left: 0px 到 left: 200px)不变。
*/
</style>
效果如下:
2.1.2 vue 动画
html
<template>
<button @click="toggle">click</button>
<transition name="fade">
<h1 v-if="showTitle">你好</h1>
</transition>
</template>
<script lang="ts" setup>
import { ref } from "vue";
let showTitle = ref(true);
function toggle() {
showTitle.value = !showTitle.value;
}
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s linear;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
vue 的 transition 约定如下:
效果如下:
2.2 jsx
简单示例
定义 Heading.jsx 如下:
js
import { defineComponent, h } from "vue";
export default defineComponent({
props: {
level: {
type: Number,
required: true,
},
},
setup(props, { slots }) {
return () =>
h(
"h" + props.level, // 标签名
{}, // prop 或 attribute
slots.default() // 子节点
);
},
});
在 about.vue 中使用,如下:
html
<template>
<Heading :level="1">hello xy</Heading>
</template>
<script lang="ts" setup>
import Heading from "@/components/Heading.jsx";
</script>
当 level 传 1时,效果如下:
当 level 传 6 时,效果如下:
通过 pnpm add @vitejs/plugin-vue-jsx -D
可安装 jsx 插件。
在 vite.config.ts 中配置如下:
ts
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [vue(), viteMockServe(), vueJsx()],
});
修改 Heading.jsx 如下:
js
import { defineComponent, h } from "vue";
export default defineComponent({
props: {
level: {
type: Number,
required: true,
},
},
setup(props, { slots }) {
const tag = "h" + props.level;
return () => <tag>{slots.default()}</tag>;
// return () =>
// h(
// "h" + props.level, // 标签名
// {}, // prop 或 attribute
// slots.default() // 子节点
// );
},
});
todo.jsx 示例
todo.jsx 如下:
js
import { defineComponent, ref } from "vue";
export default defineComponent({
setup(props) {
let title = ref("");
let todos = ref([
{ title: "pc", done: true },
{ title: "android", done: false },
]);
function addTodo() {
todos.value.push({ title: title.value });
title.value = "";
}
return () => (
<div>
<input type="text" vModel={title.value} />
<button onClick={addTodo}>click</button>
<ul>
{todos.value.length ? (
todos.value.map((todo) => {
return <li>{todo.title}</li>;
})
) : (
<li>no data</li>
)}
</ul>
</div>
);
},
});
效果如下:
三、AntdVue 组件库使用
业务开发,主要就是用UI组件库(如antd vue)了,可以在 github 搜项目,例如vue3-antd-admin 和 vue-antd-admin,学习别人的组织思路。
3.1 弹窗
弹窗通常用 a-model 组件
父组件:
html
<script setup lang="ts">
import { ref } from 'vue';
const visible = ref(false);
<template>
<div>
<a-button type="primary" class="btn-upload" ghost @click="handleCreate">
<template #icon>
<svg-icon class="icon" icon="icon-upload" />
</template>
打开弹窗
</a-button>
<!-- 子组件。向子组件传参为 model,接收子组件的 @save 事件 -->
<form-user
v-model:visible="visible"
:model="currentProcedure"
@save="handleSave"
/>
</div>
</template>
子组件:
html
<script setup lang="ts">
import { User } from '@/models/user';
import { antdModal } from '@/utils/antd';
import { reactive, ref, watch } from 'vue';
import rules from './rules';
// 数据
export type FormStateModel = User;
// 父组件传来的 prop
const props = defineProps<{
model?: FormStateModel;
visible: boolean;
}>();
// 向父组件发送的 emit
const emits = defineEmits<{
(e: 'save', model: FormStateModel): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 将数据变为响应式
const formState = reactive<FormStateModel>({
userName: '',
password: '',
});
// 用于二次确认的业务逻辑。将数据转为JSON字符串。当点击"取消"按钮时,将"现在的数据"和"刚打开弹窗时的数据"做对比,若有变动则需二次确认
let initModelJSON = '';
// 当 props.visible 时,调用 init 函数
watch(() => props.visible, init);
function init() {
// 组件初始化时,得到 initModelJSON 的原始值
const model = props.model;
if (!model) { // 若为"新增"业务,则父组件未传入 props.model,则先手动赋值为 '' 空字符串做保护,再序列化
(formState as User).userName = '';
formState.password = '';
initModelJSON = JSON.stringify(formState);
return;
}
initModelJSON = JSON.stringify(formState); // 若为"编辑"业务,则父组件传入了 props.model,直接序列化即可
}
// 组件的引用:下文的 <a-form ref="formRef"/>
const formRef = ref();
async function handleOk() {
try {
await formRef.value.validate(); // a-form 官方提供了 validate() 方法
emits('save', normalizeModel()); // 向父组件emit save函数,参数为 normalizeModel()
} catch (e) {
console.error(e);
}
}
async function handleCancel() {
if (
// 点击取消按钮时,对比新旧值
JSON.stringify(formState) !== initModelJSON &&
!(await antdModal('尚未保存,确定取消?'))
) {
emits('update:visible', true); // 当点击取消时, 向父组件 emit visible = true,让子组件(即本弹窗组件)可见
return;
}
emits('update:visible', false); // 当点击取消时, 向父组件 emit visible = false,让子组件(即本弹窗组件)不可见
}
// 将 normalizeModel 暴露给父组件,因为本组件在 <script setup> 中的变量默认是不被父组件可见的
defineExpose({ normalizeModel });
function normalizeModel() {
return { ...formState };
}
</script>
<template>
<!-- v-bind="$attrs" 将调用组件时的组件标签上绑定的非props的特性(class和style除外)向下传递。在子组件中应当添加inheritAttrs: false(避免父作用域的不被认作props的特性绑定应用在子组件的根元素上)。-->
<a-modal
v-bind="$attrs"
:title="(formState as Procedure).id ? '编辑用户' : '添加用户'"
:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form ref="formRef" class="form-device" :rules="rules" :model="formState">
<a-form-item label="用户账号" class="form-item" name="name">
<a-input v-model:value="formState.name" placeholder="请输入名称" />
</a-form-item>
</a-form>
</a-modal>
</template>
<style lang="scss" scoped>
@import '~/css/variables';
</style>