Vue3 + TypeScript 项目框架搭建指南

219 阅读9分钟

Vue3 + TypeScript 项目框架搭建指南

搭建一个vue3+Ts 项目基本框架以及配置, 要求配置好server中proxy代理、有sit,development,production环境,同时 配置好axios,mock 、less、代码压缩

1. 项目初始化

# 使用 Vite 创建项目
npm create vue@latest my-vue3-project  

# 选择配置
# ✔ Project name: … my-vue3-project
# ✔ Add TypeScript? … Yes
# ✔ Add JSX Support? … No
# ✔ Add Vue Router for Single Page Application development? … Yes
# ✔ Add Pinia for state management? … Yes
# ✔ Add Vitest for Unit testing? … No
# ✔ Add an End-to-End Testing Solution? › No
# ✔ Add ESLint for code quality? … Yes
# ✔ Add Prettier for code formatting? … Yes

cd my-vue3-project
npm install

2. 安装必要依赖

# 安装 axios、mockjs、less 等
npm install axios vite-plugin-mock mockjs
npm install -D less @types/node

3. 项目目录结构

mock/          # Mock 数据
src/
├── api/           # API 接口
├── assets/        # 静态资源
├── components/    # 组件
├── router/        # 路由
├── stores/        # 状态管理
├── types/         # TypeScript 类型定义
├── utils/         # 工具函数
├── views/         # 页面组件
├── App.vue
└── main.ts

4. 环境配置

环境变量文件

.env.development

env

VITE_APP_TITLE=Development
VITE_APP_BASE_API=/api
VITE_APP_MOCK=true

.env.sit

env

VITE_APP_TITLE=SIT
VITE_APP_BASE_API=/api
VITE_APP_MOCK=false

.env.production

env

VITE_APP_TITLE=Production
VITE_APP_BASE_API=/api
VITE_APP_MOCK=false

5. Vite 配置

vite.config.ts

import { fileURLToPath, URL } from 'node:url'
import { defineConfig ,loadEnv} from 'vite'
import { viteMockServe } from 'vite-plugin-mock'

import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig(({mode})=>{
  // 加载环境变量
  const env = loadEnv(mode, process.cwd(), '')
  const enableMock = env.VITE_APP_MOCK_ENABLED === 'true';

  // console.log("env",env)

  return {
    plugins: [
      vue(),
      viteMockServe({
        enable: enableMock, // 启用开关
        mockPath: 'mock',
      }),
      vueDevTools
    ],
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      },
    },
    css: {
      preprocessorOptions: {
        less: {
          javascriptEnabled: true, // 启用 Less 中的 JavaScript 表达式
          additionalData: `@import "@/styles/variables.less";`// 全局注入 Less 变量文件
        }
      }
    },
    server: {
      // port: 3000,        // 端口号
      // open: true,        // 自动打开浏览器  [在package.json  dev --open也可以]
      // cors: true,        // 允许跨域
      // host: '0.0.0.0',   // 允许外部访问(可选)
      proxy: {
        // 代理接口如以“/api  开头
        [env.VITE_APP_BASE_API]: {
          //获取数据的服务器地址设置
          target: 'http://10.241.154.118:90',// 代理人的地址
          //需要代理跨域
          changeOrigin: true,
          //路径重写
          rewrite: (path) => path.replace(/^\/api/, '')
        }
        
      },
      // 热更新配置
      hmr: {
        overlay: true
      }
    },
    build: {
      target: 'es2015',// 构建目标
      minify: 'terser',// 压缩工具
      terserOptions: {
        compress: {
          drop_console: mode === 'production', // 生产环境移除console
          drop_debugger: mode === 'production'  // 生产环境移除debugger
        }
      },
      rollupOptions: {  // Rollup打包配置
        output: {
          chunkFileNames: 'js/[name]-[hash].js',  // 代码分割文件命名
          entryFileNames: 'js/[name]-[hash].js',  // 入口文件命名  
          assetFileNames: '[ext]/[name]-[hash].[ext]',// 静态资源命名
          manualChunks: { // 手动代码分割
            vue: ['vue', 'vue-router', 'pinia'] // Vue生态库单独打包
          }
        }
      }
    }
  }
})

安装 terser 压缩工具

npm install -D terser

Vite 中配置 Less 预处理器 

这是 Vite 中配置 Less 预处理器 的选项,让我为你详细解释:

配置作用分析
css: {
  preprocessorOptions: {
    less: {
      javascriptEnabled: true,                    // 启用 Less 中的 JavaScript 表达式
      additionalData: `@import "@/styles/variables.less";`  // 全局注入 Less 变量文件
    }
  }
}
详细功能说明
1. javascriptEnabled: true
  • 作用:允许在 Less 文件中使用 JavaScript 表达式
  • 示例
// 可以在 Less 中使用 JS 计算
@color: `Math.random() > 0.5 ? 'red' : 'blue'`;
@width: `100 + 50`;
2. additionalData: '@import "@/styles/variables.less";'
  • 作用:在每个 Less 文件开头自动注入指定的变量文件
  • 好处:无需在每个文件中手动导入变量
实际使用效果

配置前:

// 每个 .vue 文件或 .less 文件都需要手动导入
<style lang="less">
@import "@/styles/variables.less";

.container {
  color: @primary-color;
  background: @bg-color;
}
</style>

配置后:

// 无需手动导入,变量自动可用
<style lang="less">
.container {
  color: @primary-color;      // 直接使用全局变量
  background: @bg-color;
}
</style>

典型项目结构

src/
├── styles/
│   ├── variables.less        # 全局变量定义
│   └── mixins.less          # 全局混合器
├── components/
│   └── Button.vue           # 组件,自动拥有变量访问权
└── App.vue
variables.less 示例内容:
// 颜色变量
@primary-color: #1890ff;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #f5222d;

// 尺寸变量
@header-height: 64px;
@sidebar-width: 200px;

// 字体变量
@font-size-base: 14px;
@font-family: 'Arial', sans-serif;

完整配置示例

// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        javascriptEnabled: true,
        additionalData: `
          @import "@/styles/variables.less";
          @import "@/styles/mixins.less";
        `,
        modifyVars: {
          // 修改变量值(可选)
          'primary-color': '#1DA57A'
        }
      }
    }
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')  // 确保 @ 别名正确配置
    }
  }
})

优势总结

  1. 代码复用:全局变量和混合器
  2. 维护方便:统一管理样式变量
  3. 开发效率:无需重复导入
  4. 主题切换:通过修改变量实现整体主题变更

6. TypeScript 配置

上面初始化项目时候选择ts 默认生成好的 tsconfig.json

json

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

7. Axios 配置

src/utils/request.ts

import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'

interface RequestOptions extends AxiosRequestConfig {
  loading?: boolean
}

class Request {
  private instance: AxiosInstance

  constructor(baseURL: string) {
    this.instance = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })

    this.setupInterceptors()
  }

  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        // 添加 token 等
        const token = localStorage.getItem('token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const { data } = response
        // 根据后端接口规范调整
        if (data.code === 200) {
          return data
        } else {
          ElMessage.error(data.message || '请求失败')
          return Promise.reject(new Error(data.message || '请求失败'))
        }
      },
      (error) => {
        let message = '请求失败'
        if (error.response?.status) {
          switch (error.response.status) {
            case 401:
              message = '未授权'
              // 跳转到登录页
              break
            case 403:
              message = '拒绝访问'
              break
            case 404:
              message = '请求地址错误'
              break
            case 500:
              message = '服务器内部错误'
              break
            default:
              message = '网络连接错误'
          }
        }
        ElMessage.error(message)
        return Promise.reject(error)
      }
    )
  }

  public request<T = any>(config: RequestOptions): Promise<T> {
    return this.instance.request(config)
  }

  public get<T = any>(url: string, config?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'GET', url })
  }

  public post<T = any>(url: string, data?: any, config?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'POST', url, data })
  }

  public put<T = any>(url: string, data?: any, config?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'PUT', url, data })
  }

  public delete<T = any>(url: string, config?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'DELETE', url })
  }
}

// 创建请求实例
const baseURL = import.meta.env.VITE_APP_BASE_API as string
export const http = new Request(baseURL)

8. Mock 配置

安装mock

npm install -D vite-plugin-mock mockjs

在vite.config.ts文件配置

...
import { viteMockServe } from 'vite-plugin-mock'
...
export default defineConfig(({mode})=>{
  // 加载环境变量
  const env = loadEnv(mode, process.cwd(), '')
  const enableMock = env.VITE_APP_MOCK_ENABLED === 'true';
  return {
    plugins: [
      vue(),
      viteMockServe({
        enable: enableMock, // 启用开关
        mockPath: 'mock',
      }),
      ...
    ],
    ...
    }
  }
})

在env 文件配置开关

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = ''
VITE_APP_BASE_API = '/api'
VITE_APP_MOCK_ENABLED= true   // 是否开启mock
VITE_APP_SERVE=""

新建mock文件

mock/index.ts


//用户信息数据
function createUserList() {
  return [
      {
          userId: 1,
          avatar:
              'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
          username: 'admin',
          password: '111111',
          desc: '平台管理员',
          roles: ['平台管理员'],
          buttons: ['cuser.detail'],
          routes: ['home'],
          token: 'Admin Token',
      },
      {
          userId: 2,
          avatar:
              'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
          username: 'system',
          password: '111111',
          desc: '系统管理员',
          roles: ['系统管理员'],
          buttons: ['cuser.detail', 'cuser.user'],
          routes: ['home'],
          token: 'System Token',
      },
  ]
}
// 定义请求体接口
interface LoginRequestBody {
  username: string;
  password: string;
}

export default [
  // 用户登录接口
  {
      url: '/api/user/login',//请求地址
      method: 'post',//请求方式
      response: ({ body }: { body: LoginRequestBody }) => {
          //获取请求体携带过来的用户名与密码
          const { username, password } = body;
          //调用获取用户信息函数,用于判断是否有此用户
          const checkUser = createUserList().find(
              (item) => item.username === username && item.password === password,
          )
          //没有用户返回失败信息
          if (!checkUser) {
              return { code: 201, data: { message: '账号或者密码不正确' } }
          }
          //如果有返回成功信息
          const { token } = checkUser
          return { code: 200, data: { token } }
      },
  },
  // 获取用户信息
  {
      url: '/api/user/info',
      method: 'get',
      response: (request) => {
          //获取请求头携带token
          const token = request.headers.token;
          //查看用户信息是否包含有次token用户
          const checkUser = createUserList().find((item) => item.token === token)
          //没有返回失败的信息
          if (!checkUser) {
              return { code: 201, data: { message: '获取用户信息失败' } }
          }
          //如果有返回成功信息
          return { code: 200, data: {checkUser} }
      },
  },
]

测试mock数据

image.png

image.png

9. Less 样式配置

src/styles/variables.less

less

// 颜色变量
@primary-color: #1890ff;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #f5222d;

// 字体
@font-size-base: 14px;
@font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;

// 布局
@layout-header-height: 64px;
@layout-sider-width: 200px;

// 边框
@border-radius-base: 4px;
@border-color-base: #d9d9d9;

src/styles/global.less

less

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: @font-family;
  font-size: @font-size-base;
}

#app {
  height: 100%;
}

// 通用样式
.text-center {
  text-align: center;
}

.mt-20 {
  margin-top: 20px;
}

.mb-20 {
  margin-bottom: 20px;
}

10. API 接口管理

src/api/user.ts

typescript

import { http } from '@/utils/request'

export interface LoginParams {
  username: string
  password: string
}

export interface UserInfo {
  id: number
  username: string
  nickname: string
  avatar: string
  roles: string[]
}

export interface UserListParams {
  page: number
  size: number
  keyword?: string
}

export const userApi = {
  // 登录
  login: (data: LoginParams) => http.post<{ token: string; userInfo: UserInfo }>('/user/login', data),
  
  // 获取用户信息
  getUserInfo: () => http.get<UserInfo>('/user/info'),
  
  // 获取用户列表
  getUserList: (params: UserListParams) => http.get<{ total: number; list: UserInfo[] }>('/user/list', { params })
}

11. 类型定义

src/types/api.ts

typescript

export interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
}

export interface PageParams {
  page: number
  size: number
}

export interface PageResponse<T = any> {
  total: number
  list: T[]
}

12. 主入口文件

src/main.ts

typescript

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'

// 引入 mock
import './mock'

// 引入全局样式
import './styles/global.less'

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(ElementPlus)

app.mount('#app')

13. 示例页面组件

src/views/Home.vue

vue

<template>
  <div class="home-container">
    <h1 class="text-center">{{ title }}</h1>
    <el-button type="primary" @click="handleLogin">模拟登录</el-button>
    <el-button @click="getUserList">获取用户列表</el-button>
    
    <el-table :data="userList" class="mt-20">
      <el-table-column prop="id" label="ID" width="80" />
      <el-table-column prop="username" label="用户名" />
      <el-table-column prop="nickname" label="昵称" />
      <el-table-column prop="email" label="邮箱" />
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import type { UserInfo } from '@/api/user'

const title = ref('Vue3 + TypeScript 项目')
const userList = ref<UserInfo[]>([])

const handleLogin = async () => {
  try {
    const result = await userApi.login({
      username: 'admin',
      password: '123456'
    })
    console.log('登录成功:', result)
  } catch (error) {
    console.error('登录失败:', error)
  }
}

const getUserList = async () => {
  try {
    const result = await userApi.getUserList({
      page: 1,
      size: 10
    })
    userList.value = result.list
  } catch (error) {
    console.error('获取用户列表失败:', error)
  }
}

onMounted(() => {
  getUserList()
})
</script>

<style lang="less" scoped>
.home-container {
  padding: 20px;
  
  h1 {
    color: @primary-color;
    margin-bottom: 20px;
  }
}
</style>

14. Package.json 脚本配置

package.json

json

{
  "scripts": {
    "dev": "vite",
    "dev:sit": "vite --mode sit",
    "build": "vue-tsc && vite build",
    "build:sit": "vue-tsc && vite build --mode sit",
    "build:prod": "vue-tsc && vite build --mode production",
    "preview": "vite preview"
  }
}

15. 使用说明

  1. 开发环境运行

    bash

    npm run dev
    
  2. SIT环境构建

    bash

    npm run build:sit
    
  3. 生产环境构建

    bash

    npm run build:prod
    

主要特性

  • ✅ Vue3 + TypeScript
  • ✅ 环境配置(development/sit/production)
  • ✅ Proxy 代理配置
  • ✅ Axios 封装和拦截器
  • ✅ Mock 数据模拟
  • ✅ Less 预处理器
  • ✅ 代码压缩优化
  • ✅ 路由和状态管理
  • ✅ Element Plus UI 组件库
  • ✅ 类型安全

这个框架提供了完整的开发环境配置,您可以根据实际需求进一步调整和扩展。

一些报错的配置

1.tsconfig 文件

image.png

修改后

image.png

image.png

image.png

2、main.ts 文件 找不到模块

image.png

image.png

// env.d.ts 或 src/shims-vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

3、less 文件前缀

image.png

推荐方案:方案 1(删除配置文件)

对于大多数 Vue + Vite 项目,最简单有效的解决方案是直接删除 postcss.config.js,因为:

  1. Vite 内置支持:Vite 已经内置了 PostCSS 和 Autoprefixer
  2. 自动配置:会根据 package.json 中的 browserslist 自动配置
  3. 减少配置:避免配置冲突

步骤:

  1. 删除 postcss.config.js 文件
  2. 在 package.json 中添加 browserslist(可选):

json

{
  "name": "vue-project",
  "type": "module",
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ],
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

3. 确保 vite.config.ts 简洁

typescript

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

export default defineConfig({
  plugins: [vue()]
})

注意或者好用的写法

1、路由的重定向

{
  //任意路由
  path: '/:pathMatch(.*)*',
  redirect: '/404', //重定向到404
  name: 'Any',
  meta: {
    title: '任意路由',
    hidden: true,
    icon: 'DataLine',
  },
}


 {
 // 根路径重定向  用户访问网站根路径 `/` 时,自动跳转到首页
      path: '/',
      redirect: {
          name: 'home'
      }
  },

主要区别

特性任意路由捕获根路径重定向
路径/:pathMatch(.*)*/
作用404 错误处理默认首页跳转
匹配范围所有未定义的路由仅根路径
使用场景路由守卫的最后一条路由配置的开头
优先级最低(应放在最后)较高