vite+vue

本文介绍如何使用Vite和Vue构建现代前端应用,包括npm配置、Vue常用API、项目初始化、ESLint集成、Tailwind CSS配置、网络请求封装、Vite配置、组件自动引入、状态管理、Three.js和ECharts使用等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

npm配置

npm config get registry                                                                 //查看镜像

npm config set registry https://registry.npmmirror.com                 //切换淘宝镜像

npm config set registry https://registry.npmjs.org                          //切回官方镜像

npm install -s element-ui@2.5.1                                                   //安装指定版本依赖

npm install -s element-ui@latest                                                  //安装最新版本依赖

npm update -s element-ui@latest                                                //更新依赖版本

vue常用api

import {
  ref, customRef, isRef, reactive, isReactive, toRefs, computed, watch,onWatcherCleanup, 
  watchEffect, nextTick, onMounted, onUpdated, onUnmounted, provide,
  inject, defineComponent, defineProps, defineEmits, defineSlots, defineExpose,
  defineModel, defineOptions, getCurrentInstance, useSlots, useAttrs, createApp
} from 'vue'
const my_name = ref('nick_name')
function useDebouncedRef(value, delay = 200) { //设置更新延迟delay,默认200ms
  let timeout //用于存储定时器
  return customRef((track, trigger) => {
    return {
      get() {
        track() //收集依赖
        return value
      },
      set(newValue) { //当值发生改变,获取新值
        clearTimeout(timeout) //清除上一个定时器
        timeout = setTimeout(() => { //设置定时器
          value = newValue //赋予新值
          trigger()//触发更新,不写则页面数据不更新
        }, delay)
      }
    }
  })
}
const phone = reactive({ name: 'vivo', money: 3333 })
isRef(my_name)
isReactive(phone)
const refs_phone = toRefs(phone)
computed(() => {
  return my_name.value + refs_phone.name.value
})
watch(
  () => my_name.value,
  (new_val, old_val) => {
    console.log(new_val, old_val);
    onWatcherCleanup(() => {
      // 下一次监听前做一些事情,和第三个参数功能一样
    })
  },
  {
    immediate: true,
    deep: true
  }
)
watchEffect(() => {
  console.log(my_name.value, phone.name);
})
nextTick(() => {
  phone.name = 'iPhone'
})
onMounted(() => {
  phone.money = 9999
})
onUpdated(() => {
  phone.money -= 1111
})
onUnmounted(() => {
  phone.money = 0
})

provide('phone', phone)

inject('money')

const my_component = defineComponent({
  name: 'my_component',
  emits: [],
  props: [],
  setup(props, { emit, slots, expose }) {
    return () => (
      'my_component的内容'  //使用jsx语法时,这里可以返回div等
    )
  }
})
//3.5版本增加解构默认赋值功能
const props = defineProps<{
  size?: string
  color?: string
}>()
const emit = defineEmits<{
  (event_name: 'name1', payload: string): void
  (event_name: 'name2', payload: number): void
}>()
defineSlots<{
  phone_color(scoped: { msg: string }): any
}>()
defineExpose<{
  func1(): void,
  obj1: Record<string, any>
}>({
  func1: () => { },
  obj1: {}
})

const new_msg = defineModel<string>('msg')

defineOptions({
  name: '组件名'
})

console.log('当前组件为', getCurrentInstance());

const slots = useSlots()

const attrs = useAttrs()

createApp('组件', { 传给组件的属性名: 属性值 }).mount('组件的父元素');

创建vue项目

npm init vite

 Eslint

安装Eslint

npm init @eslint/config

 .editorconfig

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

安装prettier

 pnpm i prettier -D

 创建.prettierrc.cjs文件

module.exports = {
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  singleQuote: false,
  semi: true,
  trailingComma: "es5",
  bracketSpacing: true,
};

 关联eslint和prettier

pnpm i eslint-config-prettier eslint-plugin-prettier -D

 新版eslint

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
//加入
import commpnParser from 'vue-eslint-parser'
import prettier from 'eslint-plugin-prettier'

export default [
  {
    files: ["**/*.{js,mjs,cjs,ts,vue}"]
  },
  {
    languageOptions: { 
      globals: {
        ...globals.browser, ...globals.node
      } 
    }
  },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  ...pluginVue.configs["flat/essential"],
  {
    files: ["**/*.vue"], 
    languageOptions: {
      parserOptions: {
        parser: tseslint.parser
      }
    }
  },
  //加入
  {
    ignores: [
      '**/*.config.js',
      'dist/**',
      'node_modules/**',
      '!**/eslint.config.js',
    ],
    languageOptions: {
      // 1.11 定义可用的全局变量
      globals: {
        
      },
      // 1.12 扩展
      // ecmaVersion: "latest",
      // sourceType: "module",
      parser: commpnParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        parser: '@typescript-eslint/parser',
        jsxPragma: 'React',
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
  },
  {
    plugins: {
      prettier,
    },
    rules: {
      // 开启这条规则后,会将prettier的校验规则传递给eslint,这样eslint就可以按照prettier的方式来进行代码格式的校验
      'prettier/prettier': 'error',
      // eslint(https://eslint.bootcss.com/docs/rules/)
      'no-var': 'error', // 要求使用 let 或 const 而不是 var
      'no-multiple-empty-lines': ['warn', { max: 2 }], // 不允许多个空行
      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'no-unexpected-multiline': 'error', // 禁止空余的多行
      'no-useless-escape': 'off', // 禁止不必要的转义字符
      // typeScript (https://typescript-eslint.io/rules)
      '@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
      '@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
      '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
      '@typescript-eslint/semi': 'off',
      // eslint-plugin-vue (https://eslint.vuejs.org/rules/)
      'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
      'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
      'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
      'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
    },
  },
];

旧版eslint

module.exports = {
  globals: { a: "readonly" },
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:vue/vue3-essential",
    "plugin:prettier/recommended",
    "eslint-config-prettier",
  ],
  overrides: [
    {
      env: {
        node: true,
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: "latest",
    parser: "@typescript-eslint/parser",
    sourceType: "module",

    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: ["@typescript-eslint", "vue", "prettier"],
  rules: {
    "prettier/prettier": "error",
    "arrow-body-style": "off",
    "prefer-arrow-callback": "off",
  },
  settings: {
    vue: {
      version: "detect",
    },
  },
};

 .editorconfig

root = true

[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100

[*.{yml,yaml,json}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

Tailwind CSS

pnpm i less

pnpm install -D tailwindcss postcss autoprefixer

npx tailwindcss init

 生成tailwind.config.js文件

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,vue,html}'],
  theme: {
    extend: {}
  },
  plugins: []
}

 创建postcss.config.cjs文件

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

 style.css

@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  background-color: #fff;
  color: #000;
}
/* 主题 */
html.dark {
  background-color: #000;
  color: #fff;
}
@media (prefers-color-scheme: dark){
  html {
    background-color: #000;
    color: #fff;
  }
}
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html,body,#app{
  width: 100%;
  height: 100%;
}
/*width:1000px ---> 1rem:100px */
html{
  font-size: calc(10vw);
}
@media screen and (min-width:1000px) {
  html{
    font-size: 100px;
  }
}
/*default---> width:1000px---> font-size:16px */
body{
  font-size: 0.16rem;
}
.overflow{ 
  white-space: nowrap; 
  overflow: hidden; 
  text-overflow: ellipsis;
}
.overflow-1{ 
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1; 
}
.safe {
  padding-bottom: calc(env(safe-area-inset-bottom));
}

 js定义主题

export const changeTheme = () => {
  const match = matchMedia("(prefers-color-scheme:dark)");
  if (match.matches) {
    document.documentElement.classList.add("dark");
  }
  //系统的主题颜色发生改变
  match.addEventListener("change", () => {
    if (match.matches) {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  });
};

 获取字体大小

getComputedStyle(document.documentElement).fontSize

封装网络请求

pnpm i axios 

/* eslint-disable @typescript-eslint/no-explicit-any */
import { router } from "@/router";
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";

interface BaseReturn<T> {
  data: T;
  code: number;
  msg: string;
}

type NODE_ENV = "development" | "production";
enum Url {
  development = "/api",
  production = "https://127.0.0.1:8080",
}
const node_env = process.env.NODE_ENV as NODE_ENV;

class Request {
  private instance: AxiosInstance;
  constructor(config: AxiosRequestConfig) {
    this.instance = axios.create(config);
    this.instance.interceptors.request.use((config) => {
      config.headers.Authorization = localStorage.getItem("token") || "";
      return config;
    });
    this.instance.interceptors.response.use((res) => {
      if (res.data.err_code !== 200) {
        console.log("请求出错=========");
        if (res.data.err_code === 401) {
          localStorage.removeItem("token");
          //登陆过期了
        }
        if (res.data.err_code === 403) {
          //没有权限
        }
      }
      return res.data;
    });
  }
  request<T>(config: AxiosRequestConfig) {
    return new Promise<BaseReturn<T>>((resolve) => {
      this.instance.request<any, BaseReturn<T>>(config).then((res) => {
        resolve(res);
      });
    });
  }
}
const useBaseRequest = new Request({
  baseURL: Url[node_env],
  timeout: 50000,
});
const useGet = <T>(url: string, params: any = {}) => {
  return useBaseRequest.request<T>({
    url,
    params,
    method: "GET",
  });
};
const usePost = <T>(url: string, data: any = {}) => {
  return useBaseRequest.request<T>({
    url,
    data,
    method: "POST",
  });
};
export { useBaseRequest, useGet, usePost };

vite.config.ts相关配置

使用env变量,转发网络请求,使用jsx,自动引入组件,配置@路径

安装插件

pnpm i -D unplugin-vue-components

pnpm i path

pnpm i --save-dev @types/node

pnpm i @vitejs/plugin-vue-jsx

pnpm i unplugin-auto-import

pnpm i reflect-metadata

 vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import Components from "unplugin-vue-components/vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import AutoImport from "unplugin-auto-import/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import { loadEnv } from "vite";

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd());
  return {
    server: {
      port: +env.VITE_PORT as number,
      proxy: {
        "/api": {
          target: env.VITE_BASE_API_URL || "http://localhost",
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ""),
        },
      },
    },
    plugins: [
      vue(),
      vueJsx(),
      Components({
        dts: true,
        resolvers: [
          (name) => {
            if (name.startsWith("My")) {
              return `@/components/${name}.vue`;
            }
          },
          ElementPlusResolver(),
        ],
      }),
      AutoImport({
        resolvers: [ElementPlusResolver()],
      }),
    ],
    resolve: {
      alias: {
        "@": path.resolve(__dirname, "./src"),
      },
    },
  };
});

 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "experimentalDecorators": true,//支持装饰器写法
    "emitDecoratorMetadata": true,//支持Reflect的Metadata扩展
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    //配置@,src路径提示
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "noImplicitAny": false,//允许使用any

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","./components.d.ts"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

    创建src/components/types/components.d.ts文件为自动引入的组件标注类型

import MyHelloWorld from "../MyHelloWorld.vue";
declare module "@vue/runtime-core" { //如果不生效,使用"vue"模块,或修改tsconfig.json
  export interface GlobalComponents {
    MyHelloWorld: typeof MyHelloWorld;
  }
}

main.ts

import { createApp } from "vue";
import "element-plus/dist/index.css";
import "element-plus/theme-chalk/dark/css-vars.css";
import "./style.css";
import App from "./App.vue";
import { router } from "./router";
import { pinia } from "./store";
import Vconsole from "vconsole";
import { changeTheme } from "@/utils/changeTheme";
import { ExceptionInterceptor } from "@/utils/ReactiveClass";
//注册所有图标
import * as ElementPlusIconsVue from "@element-plus/icons-vue";

new Vconsole();
const app = createApp(App);
app.use(router);
app.use(pinia);
changeTheme();
ExceptionInterceptor();
//注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
app.mount("#app");

KeepAlive

<script setup lang="ts"></script>

<template>
  <router-view class="w-full h-full" v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" v-if="$route.meta.keepAlive" />
    </keep-alive>
    <component :is="Component" v-if="!$route.meta.keepAlive" />
  </router-view>
</template>

<style scoped lang="less"></style>

使用Transition组件

<Transition name='xxx'>
    <div v-if='???'></div>
</Transition>

.xxx-enter-active,
.xxx-leave-active {
  transition: all 0.3s ease;
}

.xxx-enter-from,
.xxx-leave-to {
  transform: translateY(-100%);
}

v-bind

vue使用v-bind()可以在css中直接访问js变量

图片懒加载

export function useLazyImg(imgs: Array<HTMLImageElement>) {
  const io = new IntersectionObserver(function (entires) {
    //图片进入视口时就执行回调
    entires.forEach((item) => {
      // 获取目标元素
      const oImg = item.target;
      // 当图片进入视口的时候,就赋值图片的真实地址
      if (item.intersectionRatio > 0 && item.intersectionRatio <= 1) {
        oImg.setAttribute("src", oImg.getAttribute("data-url")!);
        io.unobserve(oImg);
      }
    });
  });
  Array.from(imgs).forEach((element) => {
    io.observe(element); //给每一个图片设置监听
  });
}

交通灯切换问题

interface IntervalRenderItem {
  duration: number;
}
export class IntervalRender<T extends IntervalRenderItem> {
  private intervalRenderList: T[] = [];
  private currentIndex: number = 0;
  private currentTime: number;
  constructor(intervalRenderList: T[]) {
    this.intervalRenderList = intervalRenderList;
    this.currentTime = Date.now();
  }
  private totalTime(): number {
    return this.intervalRenderList.reduce((x, y) => x + y.duration, 0);
  }
  get getCurrentItem() {
    return this.intervalRenderList[this.currentIndex];
  }
  //距离上一次调用过去的时间
  private currentTimeToBeforeTime() {
    return Date.now() - this.currentTime;
  }
  private update() {
    // 过去了多少伦
    const circleNum = Math.floor(this.currentTimeToBeforeTime() / this.totalTime());
    this.currentTime += circleNum * this.totalTime();
    //不足一轮还剩下多久
    let residualTime = this.currentTimeToBeforeTime() % this.totalTime();
    //剩余时间比当前项的时间多
    while (residualTime - this.getCurrentItem.duration > 0) {
      residualTime -= this.getCurrentItem.duration;
      this.currentTime += this.getCurrentItem.duration;
      this.currentIndex = (this.currentIndex + 1) % this.intervalRenderList.length;
    }
  }
  private requestAnimationFrameHandle;
  start() {
    this.update();
    this.requestAnimationFrameHandle = requestAnimationFrame(() => {
      this.start();
    });
  }
  stop() {
    cancelAnimationFrame(this.requestAnimationFrameHandle);
  }

  setIndex(index: number): void {
    this.currentIndex = index;
    this.currentTime = Date.now(); //重置上次切换的时间点
    this.update();
  }
  getRes(): { current: T; remainTime: number; index: number } {
    this.update();
    return {
      current: this.getCurrentItem,
      remainTime: this.getCurrentItem.duration - this.currentTimeToBeforeTime(),
      index: this.currentIndex,
    };
  }
}

路由拦截

pnpm i nprogress

pnpm i vue-router

pnpm i @types/nprogress -D

import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from "./routes";

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});
router.beforeEach(async (to, _from, next) => {
  NProgress.start();
  if (to.meta.title) {
    document.title = to.meta.title as string;
  }
  next();
});
router.afterEach(() => {
  NProgress.done(); // 在即将进入新的页面组件前,关闭掉进度条
});
export { router };

 routes.ts

import { RouteRecordRaw } from "vue-router";

export const routes: RouteRecordRaw[] = [
  {
    path: "/",
    redirect: "/home",
  },
  {
    path: "/home",
    component: () => import("@/pages/home/HomePage.vue"),
    redirect: "/home/animate",
    children: [
      {
        path: "animate",
        component: () => import("@/pages/home/animate/AnimateList.vue"),
      },
    ],
  },
  {
    path: "/login",
    component: () => import("@/pages/login/LogIn.vue"),
  },
  {
    path: "/:pathMatch(.*)",
    component: () => import("@/pages/404/NotFound.tsx"),
  },
];

状态管理

pnpm i pinia pinia-plugin-persistedstate

 store/index.ts

import { createPinia } from "pinia";
import { createPersistedState } from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(
  createPersistedState({
    storage: localStorage,
  })
);
export { pinia };

 store/modules/index.ts

import { defineStore } from "pinia";

const user_store = defineStore("user_store", {
  state: () => {
    return {
      token: "token",
    };
  },
  getters: {},
  actions: {},
  persist: true,
});
export { user_store };

大屏缩放

pnpm i v-scale-screen 

H5项目键盘弹出与tabbar冲突 

const docmHeight =  ref(0)
const hidshow = ref(true)
 
onMounted(() => {
    docmHeight.value = document.documentElement.clientHeight;//获取当前屏幕高度
    window.onresize = () => {//屏幕高度变化时判断
      return (() => {
        let showHeight = document.body.clientHeight;
        hidshow.value = docmHeight.value > showHeight ? false : true;
      })();
    };
})

组件级的权限控制 

<template>
  <div class='BtnPermition'>
    <slot v-if="show" name="default" :permission="permission"></slot>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

const props = defineProps<{
  permission: string | string[]
}>()
defineSlots<{
  default: (scoped: { permission: typeof props.permission }) => any
}>()
const user_role_info = JSON.parse(localStorage.getItem('user_role_info') || '[]') as Array<any>
const roleCodeArr = user_role_info.map((item) => item.roleCode || '') as Array<string>
const show = computed(() => {
  if (roleCodeArr.includes('admin')) {
    return true
  }
  if (typeof props.permission === 'string') {
    return roleCodeArr.includes(props.permission)
  } else {
    return props.permission.some(item => roleCodeArr.includes(item))
  }
})
</script>

动态表单

动态表单使用双向链表做parent,next

定义类型

/**
 * 基于双向链表的表单
 */
//动态表单项的类型
type DynamicFormItemType = 'input' | 'select' | 'checkbox' | 'radio' | 'datePicker';
//动态表单项的参数类型
interface ItemPayload {
  model: string;
  label: string;
  value: any; //用来在表单项中存储对应的表单值,不然只能用modelForm[model]获取
  disabled?: boolean;
  options?: Array<{
    label: string;
    value: any;
  }>;
  rule?: FormItemRule[];
  [key: string]: any;
}
//创建动态表单项的函数参数类型
//当前项的next为当前项和之前所有项功能作用下的结果,parent的主要作用是获取之前项
interface DynamicFormItem {
  type: FormItemType; //是一个什么类型的表单
  payload: ItemPayload; //表单需要的参数
  next: (current: DynamicFormItem, acients: DynamicFormItem[]) => DynamicFormItem[] | DynamicFormItem | null; //(回调函数)表单的下一项是什么,由当前表单和之前的表单共同决定
  parent: DynamicFormItem | null; //表单的上一项是什么
}
interface CreateDynamicFormItem {
  (
    dynamicFormItemType: DynamicFormItem['type'],
    payload: DynamicFormItem['payload'],
    next?: DynamicFormItem['next'],
    parent?: DynamicFormItem['parent']
  ): DynamicFormItem;
}

 编写生成动态表单表单的函数

import { isReactive, reactive } from 'vue';

/**
 *
 * @param dynamicFormItemType
 * @param payload
 * @param next
 * @param parent
 * @returns
 */
export const createDynamicForm: CreateDynamicFormItem = (dynamicFormItemType, payload, next, parent) => {
  if (!next) {
    next = () => null;
  }
  if (!parent) {
    parent = null;
  }
  const nextFunc: DynamicFormItem['next'] = (current, acients) => {
    let nextItem = next!(current, acients);
    if (!nextItem) {
      return null;
    }
    if (Array.isArray(nextItem)) {
      nextItem.forEach((item) => {
        item.parent = current;
        if (!isReactive(item)) {
          item = reactive(item);
        }
      });
    } else {
      nextItem.parent = current;
      if (!isReactive(nextItem)) {
        nextItem = reactive(nextItem);
      }
    }
    return nextItem;
  };
  const dynamicFormItem: DynamicFormItem = reactive({
    type: dynamicFormItemType,
    payload,
    next: nextFunc,
    parent,
  });
  return dynamicFormItem;
};

 使用递归组件渲染表单

<template>
  <div class='DynamicForm' v-if="formItem">
    <a-form-item :label="formItem.payload.label" :name="formItem.payload.model" :rules="formItem.payload.rules"
      :labelCol="{ span: 6 }" :wrapperCol="{ span: 18 }">
      <a-input :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'input'"
        v-model:value="modelForm[formItem.payload.model]" placeholder="请输入" />
      <a-textarea :rows="3" :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'textarea'"
        v-model:value="modelForm[formItem.payload.model]" placeholder="请输入" />
      <a-select :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'select'"
        v-model:value="modelForm[formItem.payload.model]" placeholder="请选择">
        <a-select-option v-for="ele in formItem.payload.options" :key="ele.value" :value="ele.value">
          {
  { ele.label }}
        </a-select-option>
      </a-select>
      <a-checkbox-group :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'checkbox'"
        v-model:value="modelForm[formItem.payload.model]" :name="formItem.payload.model"
        :options="formItem.payload.options" />
      <a-date-picker :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'datePicker'"
        v-model:value="modelForm[formItem.payload.model]" valueFormat="YYYY-MM-DD" style="width: 100%;" />
    </a-form-item>
    <div v-if="Array.isArray(nextFormItem()) && (nextFormItem() as DynamicFormItem[])?.length">
      <DynamicForm v-for="(item, i) in nextFormItem()" :key="i" :formItem="item" :modelForm="modelForm"></DynamicForm>
    </div>
    <div v-if="!Array.isArray(nextFormItem())">
      <DynamicForm :formItem="(nextFormItem() as (DynamicFormItem | null))" :modelForm="modelForm"></DynamicForm>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onUnmounted, watchEffect } from "vue"
const props = defineProps<{
  formItem: DynamicFormItem | null;
  modelForm: Record<string, any>
}>()
const nextFormItem = (): DynamicFormItem | DynamicFormItem[] | null => {
  let current: DynamicFormItem | null = props.formItem
  if (!current) {
    return null
  }
  const acients = [] as DynamicFormItem[]
  acients.unshift(current)
  while (current.parent) {
    current = current.parent
    acients.unshift(current)
  }
  //调用回调函数,将当前项和之前所有项传递过去,在那边做判断
  return props.formItem!.next(props.formItem!, acients)
}
watchEffect(() => {
  if (props.formItem) {
    // 需要对每个表单项更新value用来存储当前表单对应的值
    props.formItem.payload.value = props.modelForm[props.formItem.payload.model]
  }
})
onUnmounted(() => {
  if (props.formItem) {
    delete props.modelForm[props.formItem.payload.model]
  }
})
</script>

<style lang="less" scoped></style>

 测试一下

import { createDynamicForm } from './createDynamicForm';

const item1 = createDynamicForm(
  'select',
  {
    model: 'type',
    label: '类型',
    options: [
      { label: '快递信息', value: 1 },
      { label: '工艺信息', value: 2 },
    ],
    value: '',
  },
  (current, _acients) => {
    if (current.payload.value === 1) {
      return item2;
    } else if (current.payload.value === 2) {
      return item3;
    } else {
      return null;
    }
  }
);

const item2 = createDynamicForm('input', {
  label: '快递单号',
  value: '',
  model: 'code',
});

const item3 = createDynamicForm('select', {
  model: 'flow',
  label: '工艺流程',
  options: [
    { label: '打包', value: '打包' },
    { label: '裁剪', value: '裁剪' },
  ],
  value: [],
});

export default item1;

//返回的是当前项及以后的item的model,value。{ 绑定的字段: 对应的值 }
export function collectFormValues(formItem: DynamicFormItem | DynamicFormItem[] | null) {
  const formValues: { [key: string]: any } = {};
  function collect(item: DynamicFormItem | DynamicFormItem[] | null) {
    if (item) {
      //递归到它的后一项时,可能为数组
      if (Array.isArray(item) && item.length) {
        item.forEach((ele) => {
          const { name, value } = ele.payload;
          formValues[name] = value;
          collect(ele.next(ele, []));
        });
      } else if (!Array.isArray(item)) {
        const { name, value } = item.payload;
        formValues[name] = value;
        collect(item.next(item, []));
      }
    }
  }
  //从当前项收集
  collect(formItem);
  return formValues;
}

canvas时钟 

<template>
  <canvas class="clock" ref="canvasRef"> </canvas>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { getTime } from "./formatTime";
const canvasRef = ref<HTMLCanvasElement | null>();
const ctx = ref<CanvasRenderingContext2D | null>();
const initCanvas = () => {
  const width = canvasRef.value?.clientWidth || 0;
  const height = canvasRef.value?.clientHeight || 0;
  if (canvasRef.value && width && height) {
    canvasRef.value.width = width;
    canvasRef.value.height = height;
  }
  ctx.value = canvasRef.value?.getContext("2d");
  ctx.value?.translate(width / 2, height / 2);
  ctx.value?.rotate(-Math.PI / 2);
  ctx.value?.save();
};
const animate = ref();
const render = (init_width: number, init_height: number) => {
  const width = Math.max(init_width, init_height);
  const height = Math.min(init_width, init_height);
  ctx.value?.clearRect(-width / 2, -height / 2, width, height);
  const { hour, minute, second, timeType } = getTime(Date.now());
  ctx.value!.rotate(Math.PI / 2);
  ctx.value!.font = "20px Arial"; // 设置字体
  ctx.value!.fillStyle = "blue"; // 设置填充颜色
  ctx.value!.fillText(
    `${
      timeType == "PM"
        ? hour + 12 > 9
          ? hour + 12
          : "0" + (hour + 12)
        : hour > 9
        ? hour
        : "0" + hour
    }:${minute > 9 ? minute : "0" + minute}:${
      second > 9 ? second : "0" + second
    } ${timeType}`,
    -10 * 5,
    -20
  ); // 在 (50,50) 绘制文字
  ctx.value?.restore();
  ctx.value?.save();
  for (let i = 0; i < 12; i++) {
    //小时刻度
    ctx.value?.beginPath();
    ctx.value?.moveTo(height / 2 - 10, 0);
    ctx.value?.lineTo(height / 2 - 20, 0);
    ctx.value?.stroke();
    ctx.value?.closePath();
    ctx.value?.rotate(Math.PI / 6);
  }
  ctx.value?.restore();
  ctx.value?.save();
  for (let i = 0; i < 60; i++) {
    //分钟刻度
    ctx.value?.beginPath();
    ctx.value?.moveTo(height / 2 - 10, 0);
    ctx.value?.lineTo(height / 2 - 15, 0);
    ctx.value?.stroke();
    ctx.value?.closePath();
    ctx.value?.rotate(Math.PI / 30);
  }
  ctx.value?.restore();
  ctx.value?.save();
  //绘制秒针
  ctx.value?.rotate(((2 * Math.PI) / 60) * +second);
  ctx.value?.beginPath();
  ctx.value?.moveTo(-20, 0);
  ctx.value?.lineTo(height / 2 - 20, 0);
  ctx.value!.strokeStyle = "red";
  ctx.value!.lineWidth = 1;
  ctx.value?.stroke();
  ctx.value?.closePath();
  ctx.value?.restore();
  ctx.value?.save();
  //绘制分针
  ctx.value?.rotate(((+minute * 6 + +second / 10) * Math.PI) / 180);
  ctx.value?.beginPath();
  ctx.value?.moveTo(-15, 0);
  ctx.value?.lineTo(height / 2 - 50, 0);
  ctx.value!.strokeStyle = "green";
  ctx.value!.lineWidth = 5;
  ctx.value?.stroke();
  ctx.value?.closePath();
  ctx.value?.restore();
  ctx.value?.save();
  //绘制时针
  ctx.value?.rotate(((+hour * 30 + +minute / 2) * Math.PI) / 180);
  ctx.value?.beginPath();
  ctx.value?.moveTo(-10, 0);
  ctx.value?.lineTo(height / 2 - 70, 0);
  ctx.value!.strokeStyle = "blue";
  ctx.value!.lineWidth = 5;
  ctx.value?.stroke();
  ctx.value?.closePath();
  ctx.value?.restore();
  ctx.value?.save();
};
const renderWithSize = () => {
  render(canvasRef.value?.clientWidth || 0, canvasRef.value?.clientHeight || 0);
  animate.value = requestAnimationFrame(renderWithSize);
};
onMounted(() => {
  initCanvas();
  renderWithSize();
});
onUnmounted(() => {
  cancelAnimationFrame(animate.value!);
});
</script>

<style scoped>
.clock {
  width: 100%;
  height: 300px;
}
</style>

装饰器,创建响应式的类

pnpm i class-validator

import { Validator, validate } from "class-validator";
import { reactive } from "vue";

// 自定义类装饰器 Reactive
export function Reactive<T extends { new (...args: any[]): {} }>(
  my_constructor: T
) {
  return class extends my_constructor {
    constructor(...args: any[]) {
      super(...args);
      //把类的实例变成响应式的
      return reactive(this); //返回的是对象,该对象会覆盖constructor返回的对象实例
    }
  };
}

/**
 * 自定义表单异常
 * @constructor (message)自定义异常
 */
export class FormException extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FormException";
  }
}
/**
 * 初始化监听异常的方法
 */
export const ExceptionInterceptor = () => {
  window.addEventListener("error", function (event) {
    if (event.error instanceof FormException) {
      console.log(event.error.message);
    }
  });
  window.addEventListener("unhandledrejection", (event) => {
    const { reason } = event;
    if (reason instanceof FormException) {
      console.log(reason.message);
    }
  });
};

//将它的子类用@Reactive变成响应式的类,就可以使用这些方法给响应式对象赋值了
export class FormValidator extends Validator {
  [key: string]: any;

  //初始化
  init<T extends Record<string, any>>(form: T) {
    for (const key in form) {
      if (Object.prototype.hasOwnProperty.call(this, key)) {
        this[key] = form[key] as any;
      }
    }
  }

  //重置各个属性的值
  reset() {
    const instance = Reflect.construct(this.constructor, []);
    const keys = Object.keys(instance);
    keys.forEach((key) => {
      this[key] = instance[key];
    });
  }

  //表单提交
  submit(): Promise<this> {
    // eslint-disable-next-line no-async-promise-executor
    return new Promise(async (resolve) => {
      const err = await validate(this);
      if (err.length > 0) {
        //需要配合class-validator中的验证注解使用,str为验证失败的提示信息
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const str = Object.values(err[0].constraints!)[0];
        throw new FormException(str);
      }
      resolve(this);
    });
  }
}

也可以使用自定义装饰器

import 'reflect-metadata'
class Err {
  msg
  constructor(msg = '不能为空') {
    this.msg = msg
  }
}
// 自定义属性装饰器 IsNotEmpty
function IsNotEmpty(target: any, propertyName: string) {
  Reflect.defineMetadata('IsNotEmpty', true, target, propertyName)
}
// 校验函数
function validateNotEmpty(
  target: any,
  _propertyName: string,
  descriptor: PropertyDescriptor
) {
  //缓存方法原来的值
  const method = descriptor.value
  //对方法更改并执行
  descriptor.value = function (...args: any[]) {
    for (const key in this) {
      const needVal = Reflect.getMetadata('IsNotEmpty', target, key)
      if (needVal) {
        const val = this[key as keyof PropertyDescriptor]
        if (!val || (typeof val === 'string' && !val.trim())) {
          throw new Err(`${key}不能为空`)
        }
      }
    }
    //在最后执行原来的方法
    return method.apply(this, args)
  }
}
 
 
// 使用装饰器
class Example {
  @IsNotEmpty
  public name: string
 
  constructor(name: string) {
    this.name = name
  }
 
  @validateNotEmpty
  public greet() {
    console.log(`Hello, ${this.name}!`)
  }
}
 
// 测试,此时name为空,会捕获到错误
const example = new Example('   ')
try {
  example.greet()
} catch (err) {
  console.log(err)
}

vconsole

pnpm i vconsole

threejs

pnpm i threejs

<template>
    <div ref='MyThreePage'>

    </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
// @ts-ignore
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
const MyThreePage = ref();
// 画布
const scene = new THREE.Scene();
// 相机
const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
//控制器
let orbitControls
// 渲染函数
const render = new THREE.WebGLRenderer();
const initThree = () => {
    camera.position.z = 3;
    camera.position.x = 3;
    camera.position.y = 3;
    camera.lookAt(0, 0, 0);
    render.setSize(window.innerWidth, window.innerHeight);
}
const addGui = (mesh) => {
    // GUI
    const GUIObj = {
        full() {
            document.documentElement.requestFullscreen();
        },
    };
    const gui = new GUI();
    gui.add(GUIObj, "full").name("全屏");
    const folder1 = gui.addFolder("位置");
    folder1.add(mesh.position, "x").min(-10).max(10).step(1).name("X");
    folder1.add(mesh.position, "y").min(-10).max(10).step(1).name("Y");
    folder1.add(mesh.position, "z").min(-10).max(10).step(1).name("Z");
}
const createThing = (geometry, material) => {
    // 网
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
    return mesh;
}
const addXYZ_Controls = () => {
    // 坐标系
    const xyz = new THREE.AxesHelper(10);
    scene.add(xyz);
    // 控制器
    orbitControls = new OrbitControls(camera, render.domElement);
    orbitControls.enableDamping = true;
    orbitControls.dampingFactor = 0.12;
}
//渲染动画
const animate = () => {
    requestAnimationFrame(animate);
    orbitControls.update();
    render.render(scene, camera);
};
const resize = () => {
    render.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
}
onMounted(() => {
    MyThreePage.value.appendChild(render.domElement);
    initThree()
    addXYZ_Controls()
    addGui(createThing(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({
        color: 0x999999,
    })))
    animate();
    //窗口改变,重新设置场景大小
    window.addEventListener("resize", resize);
});
onUnmounted(() => {

})
</script>

<style lang="less" scoped>
.MyThreePage {
    width: 100%;
    height: 100%;
}
</style>

echarts

pnpm i echarts echarts-liquidfill

<template>
  <div class='DataVolumeRanking' ref="echart">
  </div>
</template>

<script setup lang="ts">
import * as echarts from 'echarts'
import { onMounted, ref } from "vue"
const echart = ref<HTMLDivElement>()
let myChart: IEChart
const setOption = (data: any[]) => {
  const option = {
    title: {
      text: '单位:条',
      right: 10,
      top: 10,
      textStyle: {
        color: '#A9B4DF',
        fontSize: 14
      }
    },
    grid: {
      left: '2%',
      right: '8%',
      bottom: '3%',
      top: '0%',
      containLabel: true
    },
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'none'
      },
      formatter: function (params) {
        return params[0].name + ' : ' + params[0].value
      }
    },
    xAxis: {
      splitLine: {
        show: false
      },
      axisLine: {
        show: false
      },
      axisLabel: {
        show: true,
        textStyle: {
          color: '#fff'
        },
      },
    },
    yAxis: {
      axisLabel: {
        show: true,
        textStyle: {
          color: '#fff'
        },
      },
      splitLine: {
        show: false
      },
      axisLine: {
        show: false
      },
      data: ['name1', 'name2', 'name3']
    },
    series: [
      {
        name: '值',
        type: 'bar',
        label: {
          show: true,
          position: 'right',
          color: '#fff'
        },
        itemStyle: {
          normal: {
            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
              offset: 0,
              color: 'rgb(37, 44, 79)'
            }, {
              offset: 1,
              color: 'rgb(76, 101, 169)'
            }]),
          },
        },
        barWidth: 30,
        data: data
      }
    ]
  }
  myChart.setOption(option)
}
onMounted(() => {
  myChart = echarts.init(echart.value)
  setOption([239, 181, 154, 144, 135, 117, 74, 72, 67, 55])
})
</script>

<style lang="less" scoped>
.DataVolumeRanking {
  width: 100%;
  height: 100%;
}
</style>

 canvas签名

<script setup lang="ts">
import { onUnmounted } from 'vue'
import { onMounted } from 'vue'
import { ref } from 'vue'
 
interface Props {
  width: string  //画布宽度
  height: string  //画布高度
  color: string  //画笔颜色
  bgc: string  //画布背景
  lineWidth: number  //画笔宽度,px
  saveText: string  //保存的文字
  clearText: string  //清除的文字
}
const props = withDefaults(defineProps<Partial<Props>>(), {
  width: '100vw',
  height: '3rem',
  color: '#000',
  bgc: '#fff',
  lineWidth: 1,
  saveText: '保存',
  clearText: '清空'
})
 
// 判断是否为移动端
const mobileStatus = /Mobile|Android|iPhone/i.test(navigator.userAgent)
 
const canvas = ref<HTMLCanvasElement>()
const ctx = ref<CanvasRenderingContext2D | null>()
const canSign = ref(false)
//起始位置的X Y
const clientX = ref(0)
const clientY = ref(0)
 
// 当前应该从哪里开始画
const get_draw_point = (e: MouseEvent | TouchEvent) => {
  // 画布距离页面左右的距离
  const x = canvas.value!.getBoundingClientRect().left
  const y = canvas.value!.getBoundingClientRect().top
  //设置起始位置
  if (!mobileStatus) {
    clientX.value = (e as MouseEvent).clientX - x
    clientY.value = (e as MouseEvent).clientY - y
  } else {
    if ((e as TouchEvent).touches) {
      clientX.value = (e as TouchEvent).touches[0].clientX - x
      clientY.value = (e as TouchEvent).touches[0].clientY - y
    }
  }
}
// 开始
const handleMouseDown = (e: MouseEvent | TouchEvent) => {
  ctx.value!.beginPath()
  canSign.value = true
  get_draw_point(e)
  //移动画笔
  ctx.value!.moveTo(clientX.value, clientY.value)
}
// 鼠标移动或者手指在手机上滑动的事件
const handleMouseMove = (e: MouseEvent | TouchEvent) => {
  if (!canSign.value) {
    return
  }
  get_draw_point(e)
  //画线
  ctx.value!.lineTo(clientX.value, clientY.value)
  ctx.value!.stroke()
}
// 结束
const handleMouseUp = () => {
  canSign.value = false
}
onMounted(() => {
  document.addEventListener(
    mobileStatus ? 'touchmove' : 'mousemove',
    handleMouseMove
  )
  // 初始化的配置
  const init_canvas = document.getElementById('canvas') as HTMLCanvasElement
  canvas.value = init_canvas
  //设置canvas宽高属性
  canvas.value.width = canvas.value.clientWidth
  canvas.value.height = canvas.value.clientHeight
  //画布背景的样式
  ctx.value = canvas.value!.getContext('2d')
  ctx.value!.fillStyle = props.bgc
  ctx.value!.fillRect(0, 0, canvas.value.width, canvas.value.height)
  //画笔的样式
  ctx.value!.lineWidth = props.lineWidth
  ctx.value!.strokeStyle = props.color
})
onUnmounted(() => {
  document.removeEventListener(
    mobileStatus ? 'touchmove' : 'mousemove',
    handleMouseMove
  )
})
 
// 下载或者清除
const a = ref<HTMLAnchorElement>()
const img_url = ref('')
const click_btn = (type: 'save' | 'clear') => {
  if (btn_event[type]) {
    btn_event[type]()
  }
}
const btn_event = {
  clear() {
    if (ctx.value) {
      ctx.value!.fillStyle = props.bgc
      ctx.value!.fillRect(0, 0, canvas.value!.width, canvas.value!.height)
    }
  },
  save() {
    const base64 = canvas.value?.toDataURL() || ''
    const base64_data = atob(base64.split(',')[1])
    let length = base64_data.length
    const u8arr = new Uint8Array(length)
    while (length--) {
      u8arr[length] = base64_data.charCodeAt(length)
    }
    const url = URL.createObjectURL(new Blob([u8arr]))
    // 移动端
    if (mobileStatus) {
      img_url.value = url
    } else {
      a.value!.href = url
      a.value!.click()
    }
  }
}
// 另一种保存canvas图片的方法(web端)
const save = () => {
  // 将canvas上的内容转成blob流
  canvas.value!.toBlob((blob) => {
    //也可以使用blob创建file文件,上传文件
    const url = URL.createObjectURL(blob!)
    a.value!.href = url 
    a.value!.click()
  })
}
</script>
 
<template>
  <div>
    <div :style="{ width: width, height: height }">
      <canvas
        id="canvas"
        @touchstart="handleMouseDown"
        @touchend="handleMouseUp"
        @mousedown="handleMouseDown"
        @mouseup="handleMouseUp"
        class="w-full h-full"
      ></canvas>
    </div>
    <div
      class="text-[0.36rem] text-[#fff] mt-[0.66rem] px-[2rem] flex justify-between"
    >
      <div @click="click_btn('save')" class="bg-[skyblue] p-[0.2rem]">
        {
  { saveText }}
      </div>
      <div @click="click_btn('clear')" class="bg-[red] p-[0.2rem]">
        {
  { clearText }}
      </div>
      <a download="sign.png" ref="a" href="" class="hidden">下载图片</a>
    </div>
    <!-- 移动端保存图片的方法 -->
    <div @click="img_url = ''" v-if="img_url" class="mask w-[100vw] h-[100vh]">
      <img :src="img_url" alt="" />
      <div class="mt-[0.2rem] text-[0.44rem] text-center text-[#fff]">
        长按保存
      </div>
    </div>
  </div>
</template>
 
<style scoped lang="less">
.mask {
  position: absolute;
  left: 0;
  top: 0;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  img {
    max-height: 60%;
  }
}
</style>

Nginx

######vue去掉路由#配置
######加上这个
location / {
  try_files $uri $uri/ /index.html;
}

######或者
location / {
	root /web-server/front-project/dist;
	try_files $uri $uri/ @router;
	index index.html;
}
# @router配置
location @router {
	rewrite ^.*$ /index.html last;
}
# 静态资源代理
location /myblog_static {
	alias /web-server/front-project/dist//myblog_static/;
}


######代理和负载均衡
upstream name{
   	 server 127.0.0.1:8080 weight=1;
   	 server 127.0.0.1:8081 weight=1;
}
server {
     listen 80;
     server_name  localhost;
	 location /path/ {
   	    proxy_pass http://name;
	 }
}

参考模板

我的vue模板

### Vite with Vue.js Project Setup and Development Guide #### Configuration of `vite.config.ts` For configuring a Vite project using TypeScript alongside Vue.js, the configuration file (`vite.config.ts`) plays an essential role. The initial content of this file includes necessary imports to set up aliases for paths within the project structure. The following code snippet demonstrates how to configure path aliases in a Vite project: ```typescript import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } }) ``` When working with TypeScript modules, it is important to ensure that Node core modules are properly typed by installing additional type definitions via NPM or Yarn commands as shown below[^2]: ```bash npm install --save-dev @types/node ``` Additionally, incorporating server configurations such as setting ports and handling API requests through proxies can be achieved like so: ```typescript import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from "path"; export default defineConfig({ plugins: [vue()], server: { port: 5000, proxy: { '/api': { target: 'http://example.com', changeOrigin: true, rewrite: (p) => p.replace(/^\/api/, '') }, } }, resolve: { alias: { '@': path.resolve(__dirname, 'src') } } }) ``` Both methods mentioned above generate absolute paths when resolving module references inside your application's source files[^3]. #### Path Resolution Methods Comparison Two different approaches exist for defining these aliases—one uses `fileURLToPath` combined with URLs while another employs Node’s built-in `path`. Both result in similar outcomes but may vary slightly depending on specific use cases or personal preference. --- --related questions-- 1. How does one integrate environment variables into a Vite + Vue.js project? 2. What are some best practices for optimizing build performance in Vite projects? 3. Can you provide examples of common issues encountered during development with Vite and their solutions? 4. Is there any difference between using JavaScript versus TypeScript in configuring Vite applications?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值