vue3项目随记

223 阅读12分钟

设置resolve.alias别名

在引用文件时,可以使用相对路径的方式,但是这样嵌套的页面非常复杂,有可能会造成多个层级../../../这种引用情况,所以有时候可以通过配置resolve.alias别名来进行缓解。

vite.config.ts文件中,引入path中的resolve,然后在defineConfig写入配置的相对路径:

import { defineConfig } from 'vite' 
import vue from '@vitejs/plugin-vue' 
import { resolve } from 'path'; 

// https://vitejs.dev/config/ 
export default defineConfig({
    plugins: [vue()], 
    resolve: { alias: { '@': resolve(__dirname, './src') } 
    } 
})

unplugin-auto-import:自动按需引入 vue\vue-router\pinia 等的api

1、安装插件·

pnpm i unplugin-auto-import  -D --save

2、在 vite.config.js 中的配置

// vue.config.js
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'          //自动按需引入 vue\vue-router\pinia\vueuse 等的api
module.exports = defineConfig({
        // ...
        configureWebpack: {
        plugins: [
            vue(),
            // 
            AutoImport({
                dts: './src/auto-imports.d.js',
                imports: ['vue', 'pinia', 'vue-router'],
                vueTemplate: true, // 是否在 vue 模板中自动导入
            }),
        ],
    },
})

3、使用前后对比

// 没有使用unplugin-auto-import
<script setup>
    import { computed, ref } from 'vue'
    const count = ref(0)
    const doubled = computed(() => count.value * 2)
</script>
// 使用unplugin-auto-import
<script setup>
    const count = ref(0)
    const doubled = computed(() => count.value * 2)
</script>

unplugin-vue-components:自动按需引入 第三方的组件库组件 和 我们自定义的组件

1、安装插件·

pnpm i unplugin-vue-components  -D --save

2、在 vite.config.js 中的配置

// vue.config.js
import vue from '@vitejs/plugin-vue'
import Components  from 'unplugin-vue-components/vite'     //自动按需引入 第三方的组件库组件 和 我们自定义的组件
module.exports = defineConfig({
        // ...
        configureWebpack: {
        plugins: [
            vue(),
            // 
            Components({
                dts: './src/components.d.js',
                // imports 指定组件所在位置,默认为 src/components
                dirs: ['src/components/']
            }),
        ],
    },
})

3、使用前后对比

// 没有使用unplugin-vue-import
<script setup>
    import HelloWorld from "@/components/HelloWorld.vue"    
</script>
<template>
    <HelloWorld>
</template>
// 使用unplugin-vue-import
<script setup>
  
</script>
<template>
    <HelloWorld>
</template>

VueUse Hooks合集

VueUse是一个基于 Composition API 实现的基本 Vue 组合实用函数的集合,可以看做是vue版的hook。 1、安装vueUse

pnpm i @vueuse/core

2、使用案例1 (在vue文件中直接引用)

<script setup> 
import {useMouse} from '@vueuse/core'
const { x , y } = useMouse()
</script>

3、使用案例2 (在vite或者webpack中配置搭配unplugin-auto-importunplugin-vue-components插件自动按需引用)

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components  from 'unplugin-vue-components/vite'

import { VueUseComponentsResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
    plugins: [
        vue(),
        AutoImport({
            imports: [
                'vue',
                '@vueuse/core'                             // 自动导入 vueuse 中的 API
            ],
            dts: './src/auto-imports.d.js',               // 生成声明文件
        }),
        
        Components({
            dts: './src/components.d.js',
            dirs: ['src/components/'],                   // imports 指定组件所在位置,默认为 src/components
            resolvers: [ VueUseComponentsResolver()],    // 自动按需引入 vueuse 的组件
        }),
    ],
})
<script setup>
    // `useMouse` 从 vueuse 中自动导入
    const { x, y } = useMouse()
</script>

Element-Plus按需导入

1、安装element-plus

pnpm install element-plus

2、按需自动导入(首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件)、

// vite.config.ts
import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import Icons from "unplugin-icons/vite"; 
import IconsResolver from "unplugin-icons/resolver";

export default defineConfig({
    // ...
    plugins: [
    // ...
        AutoImport({
            //通过 `resolvers` 选项指定使用 ElementPlusResolver 来处理 Element Plus api的自动导入
            resolvers: [
                // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式) 
                ElementPlusResolver(), 
                // 自动导入图标组件 
                IconsResolver({}),
            ],
            vueTemplate: true, // 是否在 vue 模板中自动导入
        }),
        
        Components({
            //通过 `resolvers` 选项指定使用 `ElementPlusResolver` 来处理 `Element Plus` 组件的自动引入
            resolvers: [ 
                // 自动导入 Element Plus 组件 
                ElementPlusResolver(), 
                // 自动注册图标组件,element-plus图标库,其他图标库 https://icon-sets.iconify.design/
                IconsResolver({ enabledCollections: ["ep"] }), 
            ],
        }),
        
        Icons({ 
            // 自动安装图标库 
            autoInstall: true, 
        }),
    ],
})

3、安装自动导入 Icon 依赖

npm i -D unplugin-icons

4、vite.config.js 配置

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';

// 自动导入
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";

// element-plus
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),

        AutoImport({
            // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
            imports: ['vue', 'pinia', 'vue-router'],

            //通过 `resolvers` 选项指定使用 ElementPlusResolver 来处理 Element Plus api的自动导入
            resolvers: [
                // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式) 
                ElementPlusResolver(),
                // 自动导入图标组件 
                IconsResolver({}),
            ],

            vueTemplate: true, // 是否在 vue 模板中自动导入

            dts: './src/auto-imports.d.js', // 指定自动导入函数类型声明文件路径
        }),

        Components({

            //通过 `resolvers` 选项指定使用 `ElementPlusResolver` 来处理 `Element Plus` 组件的自动引入
            resolvers: [
                // 自动导入 Element Plus 组件 
                ElementPlusResolver(),
                // 自动注册图标组件,element-plus图标库,其他图标库 https://icon-sets.iconify.design/
                IconsResolver({ enabledCollections: ["ep"] }),
            ],

            // 指定自定义组件位置(默认:src/components)
            dirs: ["src/components", "src/**/components"],

            dts: './src/components.d.js', // 指定自动导入组件类型声明文件路径
        }),
        
        Icons({
            // 自动安装图标库 
            autoInstall: true,
        }),
    ],
    resolve: {
        alias: { '@': resolve(__dirname, './src') }
    },

})

5、自动引入Element Plus后,当我们想要使用命令的方式创建element组件时,样式会无法自动引入,我们以ElMessage为例

import { ElMessage } from 'element-plus'
ElMessage.warning('warning')

这时候message组件的样式会无法引入,需要安装unplugin-element-plus进行弥补

pnpm i unplugin-element-plus -D
import ElementPlus from 'unplugin-element-plus/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    ElementPlus({
          importStyle: 'sass',
          useSource: true
    }),
    Components({
      resolvers: [
          ElementPlusResolver()
      ]
    })
  ]
})

安装SCSS

pnpm i sass -D --save

安装unocss

1、

   npm install -D unocss

2、在 vite.config.js 或 vite.config.ts 文件中添加 UnoCSS 配置

// vite.config.js
import { defineConfig } from 'vite';
import UnoCSS from 'unocss/vite';

export default defineConfig({
    plugins: [
        UnoCSS(),
    ],
});

3、在入口文件main.js中引入

import 'uno.css'

安装Pinia

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 1、安装依赖

npm install pinia

2、main.ts 引入 pinia

// src/main.ts
import { createPinia } from "pinia";
import App from "./App.vue";

createApp(App).use(createPinia()).mount("#app");

3、定义Store,新建文件 src/store/counter.ts

// src/store/counter.ts
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", () => {
  // ref变量 → state 属性
  const count = ref(0);
  
  // computed计算属性 → getters
  const double = computed(() => {
    return count.value * 2;
  });
  
  // function函数 → actions
  function increment() {
    count.value++;
  }

  return { count, double, increment };
});

父组件

<!-- src/App.vue -->
<script setup lang="ts">
import HelloWorld from "@/components/HelloWorld.vue";

import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>

<template>
  <h1 class="text-3xl">vue3-element-admin-父组件</h1>
  <el-button type="primary" @click="counterStore.increment">count++</el-button>
  <HelloWorld />
</template>

子组件

<!-- src/components/HelloWorld.vue -->
<script setup lang="ts">
import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>

<template>
  <el-card  class="text-left text-white border-white border-1 border-solid mt-10 bg-[#242424]" >
    <template #header> 子组件 HelloWorld.vue</template>
    <el-form>
      <el-form-item label="数字:"> {{ counterStore.count }}</el-form-item>
      <el-form-item label="加倍:"> {{ counterStore.double }}</el-form-item>
    </el-form>
  </el-card>
</template>

效果预览

e4089181af2744128dcb67cebf5ca66d~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.awebp

环境变量

Vite 环境变量主要是为了区分开发、测试、生产等环境的变量

参考: Vite 环境变量配置官方文档

env配置文件

项目根目录新建 .env.development 、.env.production

  • 开发环境变量配置:.env.development

    properties
     代码解读
    复制代码
    # 变量必须以 VITE_ 为前缀才能暴露给外部读取
    VITE_APP_TITLE = 'vue3-element-admin'
    VITE_APP_PORT = 3000
    VITE_APP_BASE_API = '/dev-api'
    
  • 生产环境变量配置:.env.production

    properties
     代码解读
    复制代码
    VITE_APP_TITLE = 'vue3-element-admin'
    VITE_APP_PORT = 3000
    VITE_APP_BASE_API = '/prod-api'
    

    反向代理解决跨域

跨域原理

浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。

本地开发环境通过 Vite 配置反向代理解决浏览器跨域问题,生产环境则是通过 nginx 配置反向代理 。

vite.config.ts 配置代理

表面肉眼看到的请求地址: http://localhost:3000/dev-api/api/v1/users/me

真实访问的代理目标地址: http://vapi.youlai.tech/api/v1/users/me

整合 Axios

Axios 基于promise可以用于浏览器和node.js的网络请求库

参考: Axios 官方文档

安装依赖

bash
 代码解读
复制代码
npm install axios

Axios 工具类封装

typescript
 代码解读
复制代码
//  src/utils/request.ts
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { useUserStoreHook } from '@/store/modules/user';

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: 50000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' }
});

// 请求拦截器
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const userStore = useUserStoreHook();
    if (userStore.token) {
      config.headers.Authorization = userStore.token;
    }
    return config;
  },
  (error: any) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, msg } = response.data;
    // 登录成功
    if (code === '00000') {
      return response.data;
    }

    ElMessage.error(msg || '系统出错');
    return Promise.reject(new Error(msg || 'Error'));
  },
  (error: any) => {
    if (error.response.data) {
      const { code, msg } = error.response.data;
      // token 过期,跳转登录页
      if (code === 'A0230') {
        ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', {
          confirmButtonText: '确定',
          type: 'warning'
        }).then(() => {
          localStorage.clear(); // @vueuse/core 自动导入
          window.location.href = '/';
        });
      }else{
          ElMessage.error(msg || '系统出错');
      }
    }
    return Promise.reject(error.message);
  }
);

// 导出 axios 实例
export default service;

vue-router 动态路由

安装 vue-router

bash
 代码解读
复制代码
npm install vue-router@next

路由实例

创建路由实例,顺带初始化静态路由,而动态路由需要用户登录,根据用户拥有的角色进行权限校验后进行初始化

typescript
 代码解读
复制代码
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';

export const Layout = () => import('@/layout/index.vue');

// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/redirect',
    component: Layout,
    meta: { hidden: true },
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },

  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'Dashboard',
        meta: { title: 'dashboard', icon: 'homepage', affix: true }
      }
    ]
  }
];

/**
 * 创建路由
 */
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 })
});

/**
 * 重置路由
 */
export function resetRouter() {
  router.replace({ path: '/login' });
  location.reload();
}

export default router;

全局注册路由实例

typescript
 代码解读
复制代码
// main.ts
import router from "@/router";

app.use(router).mount('#app')

动态权限路由

路由守卫 src/permission.ts ,获取当前登录用户的角色信息进行动态路由的初始化

最终调用 permissionStore.generateRoutes(roles) 方法生成动态路由

typescript
 代码解读
复制代码
// src/store/modules/permission.ts 
import { listRoutes } from '@/api/menu';

export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteRecordRaw[]>([]);

  function setRoutes(newRoutes: RouteRecordRaw[]) {
    routes.value = constantRoutes.concat(newRoutes);
  }
  /**
   * 生成动态路由
   *
   * @param roles 用户角色集合
   * @returns
   */
  function generateRoutes(roles: string[]) {
    return new Promise<RouteRecordRaw[]>((resolve, reject) => {
      // 接口获取所有路由
      listRoutes()
        .then(({ data: asyncRoutes }) => {
          // 根据角色获取有访问权限的路由
          const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
          setRoutes(accessedRoutes);
          resolve(accessedRoutes);
        })
        .catch(error => {
          reject(error);
        });
    });
  }
  // 导出 store 的动态路由数据 routes 
  return { routes, setRoutes, generateRoutes };
});

接口获取得到的路由数据

根据路由数据 (routes)生成菜单的关键代码

src/layout/componets/Sidebar/index.vuesrc/layout/componets/Sidebar/SidebarItem.vue
image-20230326145836872

按钮权限

除了 Vue 内置的一系列指令 (比如 v-modelv-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives),以下就通过自定义指令的方式实现按钮权限控制。

参考:Vue 官方文档-自定义指令

自定义指令

typescript
 代码解读
复制代码
// src/directive/permission/index.ts

import { useUserStoreHook } from '@/store/modules/user';
import { Directive, DirectiveBinding } from 'vue';

/**
 * 按钮权限
 */
export const hasPerm: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    // 「超级管理员」拥有所有的按钮权限
    const { roles, perms } = useUserStoreHook();
    if (roles.includes('ROOT')) {
      return true;
    }
    // 「其他角色」按钮权限校验
    const { value } = binding;
    if (value) {
      const requiredPerms = value; // DOM绑定需要的按钮权限标识

      const hasPerm = perms?.some(perm => {
        return requiredPerms.includes(perm);
      });

      if (!hasPerm) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
      throw new Error(
        "need perms! Like v-has-perm="['sys:user:add','sys:user:edit']""
      );
    }
  }
};

全局注册自定义指令

typescript
 代码解读
复制代码
// src/directive/index.ts
import type { App } from 'vue';

import { hasPerm } from './permission';

// 全局注册 directive 方法
export function setupDirective(app: App<Element>) {
  // 使 v-hasPerm 在所有组件中都可用
  app.directive('hasPerm', hasPerm);
}
typescript
 代码解读
复制代码
// src/main.ts
import { setupDirective } from '@/directive';

const app = createApp(App);
// 全局注册 自定义指令(directive)
setupDirective(app);

组件使用自定义指令

html
 代码解读
复制代码
// src/views/system/user/index.vue
<el-button v-hasPerm="['sys:user:add']">新增</el-button>
<el-button v-hasPerm="['sys:user:delete']">删除</el-button>

px 转 vw

这是一个将像素单位转换为视口单位(vw、vh、vmin、vmax)的PostCSS插件 安装 要使用这个插件,你需要在你的项目中设置好PostCSS。如果你还没有设置PostCSS,你可以通过运行以下命令来安装:

npm install postcss --save

接下来,安装postcss-px-conversion插件:

npm install postcss-px-conversion --save

使用

要在你的PostCSS配置中使用这个插件,将其添加到PostCSS插件列表中,同时加上所需的配置选项。

以下是在postcss.config.js中的示例配置:

// postcss.config.js

module.exports = {
  plugins: {
    "postcss-px-conversion": {
      unitType: "px", // 要从哪种单位转换(默认为'px')
      viewportWidth: 375,
      enablePerFileConfig: true, // 启用per-file配置
      viewportWidthComment: "viewport-width", // 用于指定视口宽度的注释
      // 其他配置选项...
    },
  },
};

Nuxt3 集成

nuxt.config.ts

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  postcss: {
    plugins: {
      "postcss-px-conversion": {
        unitType: "px", // 要从哪种单位转换(默认为'px')
        viewportWidth: 375,
        unitPrecision: 10,
        viewportUnit: "vw",
        minPixelValue: 1,
        includeFiles: [//pages//],
        excludeFiles: [/node_modules/, /pages/active/index.vue/]
      }
    }
  }
});

目录结构

- package.json
- pages
 - index
    - index.vue
 - active
    -index.vue

index 页面会被自动转换单位,active页面不会

你可以使用各种选项来配置这个插件:

  • unitType:要从哪种单位转换(默认为'px')。
  • viewportWidth:视口的宽度。
  • unitPrecision:vw单位的小数位数。
  • allowedProperties:要转换为vw的CSS属性列表。
  • excludedProperties:要排除在转换之外的CSS属性列表。
  • viewportUnit:期望的视口单位(vw、vh、vmin、vmax)。
  • fontViewportUnit:期望的字体视口单位。
  • selectorBlacklist:要忽略的选择器(字符串或正则表达式)。
  • minPixelValue:要替换的最小像素值。
  • allowMediaQuery:在媒体查询中允许px到vw的转换。
  • replaceRules:替换包含vw的规则而不是添加回退规则。
  • excludeFiles:要忽略的文件(作为正则表达式数组)。
  • includeFiles:只转换匹配的文件(作为正则表达式数组)。
  • enableLandscape:为横向模式添加@media (orientation: landscape)。
  • landscapeUnit:横向模式的期望单位。
  • landscapeViewportWidth:横向方向的视口宽度。
  • enablePerFileConfig:启用per-file配置(默认为true)。
  • viewportWidthComment:用于指定视口宽度的注释(默认为"viewport-width")。

请根据你的项目需求调整这些选项。

Per-File 配置

此插件现在支持per-file配置,允许你为每个CSS或SCSS文件指定不同的视口宽度。要使用这个功能,只需在文件的开头添加一个特殊的注释:

/* viewport-width: 1920 */

插件会读取这个注释并使用指定的宽度来进行单位转换。这对于在同一个项目中为不同的设备(如PC、平板、手机)创建不同的CSS文件特别有用。

示例

以下是一个示例配置,将像素值转换为vw单位,默认视口宽度为750像素,并启用per-file配置:

// postcss.config.js

module.exports = {
  plugins: {
    "postcss-px-conversion": {
      unitType: "px",
      viewportWidth: 750,
      unitPrecision: 5,
      allowedProperties: ["*"],
      excludedProperties: [],
      viewportUnit: "vw",
      fontViewportUnit: "vw",
      selectorBlacklist: [],
      minPixelValue: 1,
      allowMediaQuery: false,
      replaceRules: true,
      excludeFiles: [],
      includeFiles: [],
      enableLandscape: false,
      landscapeUnit: "vw",
      landscapeViewportWidth: 568,
      enablePerFileConfig: true,
      viewportWidthComment: "viewport-width",
    },
  },
};

使用这个配置,你的CSS中的像素值将在PostCSS处理期间自动转换为视口单位。同时,你可以在每个文件中使用注释来指定该文件的特定视口宽度。

例如,在一个针对桌面设备的CSS文件中:

/* viewport-width: 1920 */
.header {
  width: 1600px; /* 将被转换为 83.33333vw */
}

而在一个针对移动设备的CSS文件中:

/* viewport-width: 375 */
.header {
  width: 350px; /* 将被转换为 93.33333vw */
}

这样,你就可以在一次构建中生成适配多种设备的CSS,同时保持了代码的灵活性和可维护性。