【前端】Vue3 + AntdVue + Ts + Vite4 + pnpm + Pinia 实战

文章目录

  • 一、项目搭建
    • [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)
  • [三、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 项目创建

vite 项目搭建教程

vite 官网

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 yarn@1.22.19 --activate

在项目中使用指定的包管理工具:在 package.json 中指定包管理工具及其版本:

json 复制代码
{
  "packageManager": "yarn@1.22.19"
}

然后运行 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 的项目.

  1. vite -v 和 vue -v 无输出, 是因为没有全局安装.
  2. 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
  1. 建议在项目内安装 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"
    }
    ...
}

效果如下:

![](https://i-blog.csdnimg.cn/direct/d773c52752e44981a8bed24b1997401c.png = 200x200)

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>

效果如下:

![](https://img-blog.csdnimg.cn/1c31481b245640d19c22cdeb9dac63e1.gif =300)

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-adminvue-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>
相关推荐
程序员爱钓鱼2 分钟前
使用 Node.js 批量导入多语言标签到 Strapi
前端·node.js·trae
鱼樱前端3 分钟前
uni-app开发app之前提须知(IOS/安卓)
前端·uni-app
V***u4534 分钟前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端
i听风逝夜42 分钟前
Web 3D地球实时统计访问来源
前端·后端
iMonster1 小时前
React 组件的组合模式之道 (Composition Pattern)
前端
呐呐呐呐呢1 小时前
antd渐变色边框按钮
前端
元直数字电路验证1 小时前
Jakarta EE Web 聊天室技术梳理
前端
wadesir1 小时前
Nginx配置文件CPU优化(从零开始提升Web服务器性能)
服务器·前端·nginx
牧码岛1 小时前
Web前端之canvas实现图片融合与清晰度介绍、合并
前端·javascript·css·html·web·canvas·web前端
灵犀坠1 小时前
前端面试八股复习心得
开发语言·前端·javascript