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

发布于:2025-03-17 ⋅ 阅读:(17) ⋅ 点赞:(0)

背景:现在前端框架多、版本多、迭代快。以 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 项目搭建

其中各命令的解释, 详见下文

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 版本

# 安装 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 中指定包管理工具及其版本:

{
  "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 如下:

{
  "name": "vite",
  "bin": {
    "vite": "bin/vite.js"
  }
}

例如 lodash 的 package.json 如下:

{
  "name": "lodash",
  "main": "lodash.js"
}

如果命令行, 未指定 -g, 则被安装在本项目的 node_modules 目录中(而不在全局 node_modules 目录中), 则可通过 npx 运行, 例如 npx vite

1.2.4 pnpm

配置方式:

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 可查看其全局存储路径:

# 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 字段中.

{
  "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 模块:

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 中 配置如下:
"devDependencies": {
  "vite": "^6.2.0"
}

在项目中, pnpm vite -v 即可运行本地安装的 vite

# 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 如下:

{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}

tsconfig.app.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 如下:

{
  "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 文件 引用。

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),以实现移动端自适应布局
// 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 中设置如下(上文已包含):

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/, ""),
      },
    }

则指定环境变量启动即可连本地后端服务:

BACKEND_ADDR='http://127.0.0.1:9999' pnpm run dev

eslint(可选)

pnpm add eslint eslint-plugin-vue --save-dev
pnpm add @typescript-eslint/parser --save-dev

创建配置文件: .eslintrc.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

node_modules/
dist/
index.html

命令行式运行:修改 package.json

{
    ...
    "scripts": {
        ...
        "eslint:comment": "使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .js 和 .vue 的文件",
        "eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
    }
    ...
}

集成 prittier(可选)

pnpm add prettier eslint-config-prettier eslint-plugin-prettier --save-dev

创建配置文件: prettier.config.js 或 .prettierrc.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 配置

module.exports = {
    ...
    extends: [
        'plugin:vue/vue3-recommended',
        'plugin:@typescript-eslint/recommended',
        'prettier',
        'plugin:prettier/recommended'
    ],
    ...
};

命令行式运行:修改 package.json

{
    ...
    "scripts": {
        ...
        "prettier:comment": "自动格式化当前目录下的所有文件",
        "prettier": "prettier --write"
    }
    ...
}

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

pinia

踩坑教训: 不可用时, 先检查是否 pnpm add 成功了, (因为 vscode 提示时好时坏)

pnpm add pinia # 若报错, 则可先停止 pnpm run dev 再执行

新建 src/store 目录并在其下面创建 index.ts,导出 store

import { createPinia } from 'pinia'
const store = createPinia()
export default store

在 main.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

import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
    state: () => {
        return {
            name: "张三",
        };
    },
    actions: {
        updateName(name: string) {
            this.name = name;
        },
    },
});

获取和修改 State: 在 src/components/usePinia.vue 中使用

<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 中定义如下:

  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }

集成 vue-router4

pnpm add vue-router

新建 src/router 目录并在其下面创建 index.ts,导出 router

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'; 并使用

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

<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 的实用函数集合。

pnpm add @vueuse/core

新建 src/pages/vueUse.vue 如下:useMouse 只是 vueuse 的一个最基本的函数库

<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

pnpm add -D sass

新建 src/assets/styles/variables.scss 文件, 内容如下:

$blue: #007bff;
$red: #dc3545;

$primary-color: red;
$secondary-color: blue;

使用在 .vue 文件, 因为上文已在 vite.config.ts 中 @use '/src/assets/styles/variables.scss';, 所以直接使用即可. 例如设置 color 为 red:

<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 中。

pnpm add axios

新建 src/utils/axios.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 代码和关键步骤如下:

<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 如下:

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:

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

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

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

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 页面,并在其中使用

<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,引入方式参考官网

pnpm add ant-design-vue
pnpm add vite-plugin-style-import --save-dev # 非必须
pnpm add vue-svg-icon

在 main.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> 标签

<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
<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:

<style>
.box {
  background: red;
  height: 100px;
  transition: width 1s linear;
}
</style>

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

animation
<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 动画

<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 如下:

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 中使用,如下:

<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 中配置如下:

import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
  plugins: [vue(), viteMockServe(), vueJsx()],
});

修改 Heading.jsx 如下:

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 如下:

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 组件

父组件:

<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>

子组件:

<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>