Vue2 + Vite + TypeScript 大型分包项目搭建详细过程

搭建一个基于 pnpm、Vue2、Vite、TypeScript 和 ElementUI 的大型分包前端项目,按 common/main/pages 目录结构分包、模块可独立运行、配置完整的依赖/TS/ESLint/代理,实现基础的登录-首页跳转功能。

一、运行环境要求

工具 最低版本要求 推荐版本
Node.js 16.0.0 18.18.2
pnpm 7.0.0 8.15.6
npm 8.0.0 9.8.1
操作系统 Windows/macOS/Linux

环境验证命令

node -v   # 需输出 v16+
pnpm -v   # 需输出 7+

二、项目搭建完整步骤

步骤1:初始化项目目录结构

# 创建根目录
mkdir xx-business && cd xx-business

# 初始化pnpm工作区
pnpm init

# 创建核心目录 或 手动添加目录文件
mkdir -p common/src main/src pages/login/src pages/home/src
mkdir -p common/public main/public pages/login/public pages/home/public
mkdir -p common/types main/types pages/login/types pages/home/types
mkdir -p config static

步骤2:配置pnpm工作区(根目录)

项目根目录的核心文件结构应该是这样的:

xx-business/
├── common/                # 公共模块
│   ├── src/               # 公共源码
│   ├── package.json       # 公共模块依赖
│   └── vite.config.ts     # 公共模块Vite配置(之前补的)
├── main/                  # 主入口模块
│   ├── src/               # 主入口源码
│   ├── index.html         # 主入口HTML
│   ├── package.json       # 主模块依赖
│   └── vite.config.ts     # 主模块Vite配置(之前补的)
├── pages/                 # 业务模块
│   ├── login/             # 登录模块
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   └── home/              # 首页模块
│       ├── src/
│       ├── package.json
│       └── vite.config.ts
├── static/                # 全局静态资源(外置)
├── .env                   # 环境变量
├── .eslintrc.js           # ESLint配置
├── .prettierrc            # Prettier配置
├── package.json           # 根目录依赖/脚本
├── pnpm-workspace.yaml    # pnpm工作区
├── tsconfig.json          # 全局TS配置(之前给的)
├── vite.config.base.ts    # 基础Vite配置(根目录)
└── vite.config.proxy.ts   # 代理配置(根目录)
1. 根目录 package.json
{
  "name": "xx-business",
  "version": "1.0.0",
  "description": "Vue2 + Vite + TS 大型分包项目",
  "private": true,
  "scripts": {
    "dev": "pnpm dev:main",
    "dev:main": "pnpm -F main run dev",
    "dev:common": "pnpm -F common run dev",
    "build": "pnpm -F main run build",
    "build:all": "pnpm -r --filter=\"./**\" run build",
    "build:main": "pnpm -F main run build",
    "build:common": "pnpm -F common run build",
    "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix",
    "type-check": "tsc --noEmit"
  },
  "keywords": [
    "vue2",
    "vite",
    "typescript",
    "monorepo",
    "pnpm"
  ],
  "author": "xx <xuxiong9@qq.com>",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^18.19.39",
    "@typescript-eslint/eslint-plugin": "^5.62.0",
    "@typescript-eslint/parser": "^5.62.0",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^8.10.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-vue": "^9.27.0",
    "prettier": "^2.8.8",
    "typescript": "^4.9.5",
    "vite": "^4.5.3",
    "vite-plugin-vue2": "^2.0.3",
    "vue-eslint-parser": "^9.4.3"
  },
  "dependencies": {
    "@types/echarts": "^4.9.22",
    "@types/jquery": "^3.5.30",
    "@types/js-cookie": "^3.0.6",
    "@types/lodash": "^4.17.7",
    "@types/vue-router": "^2.0.0",
    "axios": "^1.7.2",
    "core-js": "^3.37.1",
    "dayjs": "^1.11.12",
    "echarts": "^5.4.3",
    "element-ui": "^2.15.14",
    "jquery": "^3.7.1",
    "js-cookie": "^3.0.5",
    "lodash": "^4.17.21",
    "vue": "^2.6.14",
    "vue-router": "^3.6.5",
    "vuex": "^3.6.2",
    "sass": "~1.69.5",
    "sass-loader": "^13.3.3"
  }
}

2. 根目录 pnpm-workspace.yaml
packages:
  # 公共目录,包含utils,plugins,全局样式
  - 'common'
  # 主入口
  - 'main'
  # 主目录下所有项目
  - 'pages/*'
  # 打包配置
  - 'packages/*'

步骤3:配置全局基础文件(根目录)

1. tsconfig.json (全局TS配置)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["ES2020", "DOM"],
    "types": ["vite/client", "node", "vue2"],
    "baseUrl": ".",
    "paths": {
      "/common/*": ["common/*"],
      "/main/*": ["main/*"],
      "/pages/*": ["pages/*"],
      "@/*": ["./src/*"]
    },
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "common/**/*.ts",
    "common/**/*.d.ts",
    "common/**/*.tsx",
    "common/**/*.vue",
    "main/**/*.ts",
    "main/**/*.d.ts",
    "main/**/*.tsx",
    "main/**/*.vue",
    "pages/**/*.ts",
    "pages/**/*.d.ts",
    "pages/**/*.tsx",
    "pages/**/*.vue",
    "typings/**/*.d.ts"
  ],
  "exclude": ["node_modules", "dist", "**/dist/*"]
}

2. .eslintrc.js (ESLint配置)
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  parser: "vue-eslint-parser",
  parserOptions: {
    parser: "@typescript-eslint/parser",
    ecmaVersion: "latest",
    sourceType: "module",
  },
  plugins: ["vue", "@typescript-eslint"],
  extends: [
    "eslint:recommended",
    "plugin:vue/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
  ],
  rules: {
    "vue/script-setup-uses-vars": "error",
    "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
    "@typescript-eslint/no-explicit-any": "warn",
    "vue/multi-word-component-names": "off",
    "prettier/prettier": ["error", { singleQuote: true, semi: false }],
  },
};

3. .prettierrc (Prettier配置)
{
  "singleQuote": true,
  "semi": false,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "bracketSpacing": true
}
4. vite.config.base.ts (基础Vite配置)
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import path from 'path'
import { fileURLToPath } from 'url'

// 解决 ESModule 中 __dirname 不存在的问题
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

export default defineConfig({
  // 插件配置:核心是 Vue2 支持
  plugins: [
    createVuePlugin({
      // Vue2 模板编译配置
      vueTemplateOptions: {
        compilerOptions: {
          preserveWhitespace: false // 移除模板空格,优化体积
        }
      },
      // 支持 Vue2.7 的 setup 语法糖
      jsx: true
    })
  ],

  // 路径解析配置:核心是别名,确保 /common /main 能正确访问
  resolve: {
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
    alias: {
      // 全局别名:所有模块都能通过 /common 访问公共目录
      '/common': path.resolve(__dirname, './common'),
      // 全局别名:所有模块都能通过 /main 访问主入口目录
      '/main': path.resolve(__dirname, './main'),
      '/pages': path.resolve(__dirname, './pages'), // 新指向根目录下的 pages 文件夹
      // 模块内别名:@ 指向当前模块的 src 目录(每个模块自己的 src)
      '@': path.resolve(__dirname, './src'),
      // 根目录别名
      '~': path.resolve(__dirname, './')
    }
  },

  // 依赖预构建:优化大型项目启动速度
  optimizeDeps: {
    include: [
      'vue', 'vue-router', 'vuex', 'axios', 'dayjs', 'lodash',
      'element-ui', 'js-cookie', 'echarts', 'jquery'
    ],
    exclude: ['vue-demi'] // 排除 Vue2/3 兼容层,避免冲突
  },

  // CSS 配置:全局样式、SCSS 变量
  css: {
    preprocessorOptions: {
      scss: {
        // 全局注入 SCSS 变量,所有模块都能直接用 $primary-color 等
        additionalData: `@import "/common/src/styles/variables.scss";`,
        silenceDeprecations: ['legacy-js-api', 'import'],
        charset: false // 解决 charset 警告
      }
    },
    // 提取 CSS 为单独文件(生产环境)
    devSourcemap: true, // 开发环境开启 CSS SourceMap
    postcss: {
      plugins: [
        // 可选:添加 autoprefixer 自动补全浏览器前缀
        // require('autoprefixer')({ overrideBrowserslist: ['> 1%', 'last 2 versions'] })
      ]
    }
  },

  // 构建基础配置(所有模块共用)
  build: {
    target: 'es2015', // 兼容现代浏览器
    cssTarget: 'chrome80', // CSS 兼容目标
    minify: 'terser', // 生产环境压缩
    terserOptions: {
      compress: {
        drop_console: true, // 生产环境移除 console
        drop_debugger: true // 生产环境移除 debugger
      }
    },
    chunkSizeWarningLimit: 1500, // 分包大小警告阈值
    assetsDir: 'static', // 静态资源输出目录
    rollupOptions: {
      output: {
        // 分包命名规则:带 hash 便于缓存
        chunkFileNames: 'static/js/[name]-[hash].js',
        entryFileNames: 'static/js/[name]-[hash].js',
        assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
      }
    }
  }
})

5. vite.config.proxy.ts (代理配置)
// 全局代理配置:所有模块共用的接口代理规则
export default {
  proxy: {
    // 接口代理示例:/api 开头的请求转发到后端服务
    '/api': {
      target: 'http://localhost:8080', // 后端接口地址(根据你的实际地址修改)
      changeOrigin: true, // 跨域请求时添加 Origin 头
      rewrite: (path) => path.replace(/^\/api/, ''), // 移除 /api 前缀
      timeout: 5000, // 超时时间
      secure: false // 允许访问 https 且证书无效的服务
    },
    // 上传接口代理
    '/upload': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      timeout: 10000, // 上传超时时间更长
      secure: false
    },
    // 静态资源代理(可选)
    '/static': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      secure: false
    }
  }
}

步骤4:配置common模块(公共目录)

1. common/package.json
{
  "name": "common",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite --config vite.config.ts",
    "build": "vite build",
    "preview": "vite preview --config vite.config.ts"
  },
  "main": "./src/index.ts",
  "types": "./types/index.d.ts"
}

common 模块 Vite 配置(common/vite.config.ts

import { defineConfig, mergeConfig } from 'vite'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'

// 1. 解决 ESModule 中 __dirname 问题(关键:定位main目录)
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// 2. 导入根目录的 base 配置(相对路径:main目录 → 根目录)
// 方式1:相对路径(推荐,简单直接)
import baseConfig from '../vite.config.base.ts'
// 方式2:绝对路径(备用,防止相对路径出错)
// const baseConfigPath = path.resolve(__dirname, '../vite.config.base.ts')
// const baseConfig = (await import(pathToFileURL(baseConfigPath).href)).default

// 3. 导入根目录的 proxy 配置
import proxyConfig from '../vite.config.proxy.ts'
// 备用绝对路径写法:
// const proxyConfigPath = path.resolve(__dirname, '../vite.config.proxy.ts')
// const proxyConfig = (await import(pathToFileURL(proxyConfigPath).href)).default

// 4. 合并配置并导出(main模块专属配置 + 全局公共配置)
export default mergeConfig(
  baseConfig,
  defineConfig({
    // ✅ 开发服务器配置(main专属:端口、自动打开、代理)
    server: {
      port: 9895,        // main模块固定端口
      open: true,        // 启动后自动打开浏览器
      host: '0.0.0.0',   // 允许局域网访问
      strictPort: true,  // 端口被占用时不自动切换
      ...proxyConfig     // 合并根目录的代理配置
    },

    // ✅ 构建配置(main专属:输出目录、清空目录)
    build: {
      outDir: path.resolve(__dirname, 'dist'), // main模块打包输出到 main/dist
      emptyOutDir: true,                       // 打包前清空dist
      reportCompressedSize: false              // 关闭压缩体积报告(加快打包)
    },

    // ✅ 入口配置(main专属:根目录指向main、HTML入口)
    root: __dirname,                            // Vite根目录 = main目录
    resolve: {
      alias: {
        // 模块内别名:@ 指向 main/src
        '@': path.resolve(__dirname, './src')
      }
    },
    publicDir: path.resolve(__dirname, 'public'), // 静态资源目录 = main/public
    envDir: path.resolve(__dirname, '../'),     // 环境变量读取根目录的.env

    // ✅ 日志配置(可选,优化开发体验)
    logLevel: 'info',
    clearScreen: true
  })
)

common /index.html (主入口HTML)
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue2 common入口</title>
    <meta name="version" content="1.0.0" />
    <meta name="author" content="9894664" />
    <!-- <link rel="icon" href="/favicon.ico" /> -->
  </head>
  <body>
    <div id="comApp"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

2. common/src/index.ts (公共模块入口)
// 导出公共工具
export * from './utils/axios'
export * from './utils/cookie'
// export * from './utils/date'
// export * from './utils/common'

// 导出公共组件
// export { default as CommonButton } from './components/CommonButton.vue'
// export { default as CommonTable } from './components/CommonTable.vue'

// 导出全局样式
import './styles/index.scss'

// 导出插件
export * from './plugins/element-ui'
// export * from './plugins/axios'

3. common/src/utils/cookie.ts (Cookie工具)
import Cookies from 'js-cookie'

// Token相关
export const TOKEN_KEY = 'USER_TOKEN'

/**
 * 获取Token
 */
export const getToken = (): string | undefined => {
  return Cookies.get(TOKEN_KEY)
}

/**
 * 设置Token
 * @param token Token值
 * @param expires 过期时间(天)
 */
export const setToken = (token: string, expires = 7): void => {
  Cookies.set(TOKEN_KEY, token, { expires })
}

/**
 * 移除Token
 */
export const removeToken = (): void => {
  Cookies.remove(TOKEN_KEY)
}

/**
 * 检查是否登录
 */
export const isLogin = (): boolean => {
  return !!getToken()
}
4. common/src/utils/axios.ts (Axios封装)
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { getToken, removeToken } from './cookie'
import { Message } from 'element-ui'

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

// 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 添加Token
    if (getToken()) {
      config.headers = config.headers || {}
      config.headers.Authorization = `Bearer ${getToken()}`
    }
    return config
  },
  (error: AxiosError) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const res = response.data
    // 业务错误处理
    if (res.code !== 200) {
      Message.error(res.message || '请求失败')
      // Token过期
      if (res.code === 401) {
        removeToken()
        window.location.href = '/login'
      }
      return Promise.reject(res)
    }
    return res
  },
  (error: AxiosError) => {
    Message.error(error.message || '服务器错误')
    return Promise.reject(error)
  }
)

export default service
5. common/src/styles/variables.scss (全局样式变量)
// 颜色变量
$primary-color: #409eff;
$success-color: #67c23a;
$warning-color: #e6a23c;
$danger-color: #f56c6c;
$info-color: #909399;

// 字体变量
$font-size-xs: 12px;
$font-size-sm: 14px;
$font-size-md: 16px;
$font-size-lg: 18px;

// 布局变量
$header-height: 60px;
$sidebar-width: 200px;
$footer-height: 40px;

// 圆角变量
$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 12px;
6. common/src/styles/index.scss (全局样式)
@import './variables.scss';

// 全局重置样式
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
  font-size: $font-size-sm;
  color: #333;
  background-color: #f5f5f5;
}

// 全局通用样式
.el-container {
  height: 100vh;
}

.el-header {
  height: $header-height !important;
  line-height: $header-height !important;
  background-color: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

.el-aside {
  width: $sidebar-width !important;
  background-color: #2e3b4e;
}

.el-main {
  padding: 20px;
  background-color: #f5f5f5;
}

// 自定义类名
.flex {
  display: flex;
}

.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

.flex-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
7. common/src/plugins/element-ui.ts (ElementUI插件)
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

/**
 * 注册ElementUI
 * @param app Vue实例
 */
export const useElementUI = (app: Vue): void => {
  app.use(ElementUI, {
    size: 'medium',
    zIndex: 3000
  })
}

步骤5:配置main模块(主入口)

1. main/package.json
{
  "name": "main",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite --config vite.config.ts",
    "build": "vite build",
    "preview": "vite preview --config vite.config.ts"
  },
  "dependencies": {
    "common": "workspace:*"
  }
}

main 模块 Vite 配置(main/vite.config.ts

import { defineConfig, mergeConfig } from 'vite'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'

// 1. 解决 ESModule 中 __dirname 问题(关键:定位main目录)
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// 2. 导入根目录的 base 配置(相对路径:main目录 → 根目录)
// 方式1:相对路径(推荐,简单直接)
import baseConfig from '../vite.config.base.ts'
// 方式2:绝对路径(备用,防止相对路径出错)
// const baseConfigPath = path.resolve(__dirname, '../vite.config.base.ts')
// const baseConfig = (await import(pathToFileURL(baseConfigPath).href)).default

// 3. 导入根目录的 proxy 配置
import proxyConfig from '../vite.config.proxy.ts'
// 备用绝对路径写法:
// const proxyConfigPath = path.resolve(__dirname, '../vite.config.proxy.ts')
// const proxyConfig = (await import(pathToFileURL(proxyConfigPath).href)).default

// 4. 合并配置并导出(main模块专属配置 + 全局公共配置)
export default mergeConfig(
  baseConfig,
  defineConfig({

    // ✅ 开发服务器配置(main专属:端口、自动打开、代理)
    server: {
      port: 9894,        // main模块固定端口
      open: true,        // 启动后自动打开浏览器
      host: '0.0.0.0',   // 允许局域网访问
      strictPort: true,  // 端口被占用时不自动切换
      ...proxyConfig     // 合并根目录的代理配置
    },

    // ✅ 构建配置(main专属:输出目录、清空目录)
    build: {
      outDir: path.resolve(__dirname, 'dist'), // main模块打包输出到 main/dist
      emptyOutDir: true,                       // 打包前清空dist
      reportCompressedSize: false              // 关闭压缩体积报告(加快打包)
    },

    // ✅ 入口配置(main专属:根目录指向main、HTML入口)
    publicDir: path.resolve(__dirname, 'public'), // 静态资源目录 = main/public
    envDir: path.resolve(__dirname, '../'),     // 环境变量读取根目录的.env
    root: __dirname, // 正确:指向 main 目录
    resolve: {
      alias: {
        // 模块内别名:@ 指向 main/src
        '@': path.resolve(__dirname, './src')
      }
    },
    // ✅ 日志配置(可选,优化开发体验)
    logLevel: 'info',
    clearScreen: true
  })
)

main基础环境配置

NODE_ENV=development
VITE_API_BASE_URL=/api
VITE_APP_NAME=xx-business
2. main/index.html (主入口HTML)
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue2 Vite主入口</title>
    <meta name="version" content="1.0.0" />
    <meta name="author" content="9894664" />
    <!-- <link rel="icon" href="/favicon.ico" /> -->
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

3. main/src/main.ts (主入口TS)
import Vue from 'vue'
import App from './App.vue'
import router from '@/router/index'
import store from '@/store/index'
import { useElementUI } from '/common/src/plugins/element-ui'
import '/common/src/styles/index.scss'
import { isLogin } from '/common/src/utils/cookie'
console.log('✅ main.ts 入口文件加载成功')
// 生产环境提示关闭
Vue.config.productionTip = false

// 注册ElementUI
useElementUI(Vue)

// 路由守卫:未登录跳转到登录页
router.beforeEach((to, from, next) => {
  if (to.path !== '/login' && !isLogin()) {
    next('/login')
  } else {
    next()
  }
})

// 创建Vue实例
new Vue({
  router,
  store,
  render: (h) => h(App)
}).$mount('#app')


// 创建Vue实例
new Vue({
  router,
  store,
  render: (h) => h(App)
}).$mount('#app')
4. main/src/App.vue (主入口组件)
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script lang="ts">
import Vue from "vue";

export default Vue.extend({
  name: "App",
  mounted() {
    console.log("项目版本号:1.0.0");
    console.log("个人介绍:前端开发工程师,专注Vue2/Vue3生态开发");
  },
});
</script>

<style scoped>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  width: 100%;
  height: 100vh;
}
</style>

5. main/src/router/index.ts (主路由配置)
import Vue from 'vue'
import Router from 'vue-router'
import { getToken } from '/common/src/utils/cookie'

// 懒加载路由
const Login = () => import("/pages/login/src/views/Login.vue");
const Home = () => import("/pages/home/src/views/Home.vue");

Vue.use(Router)

const router = new Router({
  mode: 'history',
  base: import.meta.env.BASE_URL,
  routes: [
    {
      path: '/',
      redirect: '/home'
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/home',
      name: 'Home',
      component: Home,
      meta: {
        requiresAuth: true
      }
    },
    {
      path: '*',
      redirect: '/'
    }
  ]
})

export default router

6. main/src/store/index.ts (Vuex配置)
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userInfo: {
      name: '',
      avatar: '',
      roles: []
    },
    sidebar: {
      opened: true
    }
  },
  mutations: {
    SET_USER_INFO(state, userInfo) {
      state.userInfo = userInfo
    },
    TOGGLE_SIDEBAR(state) {
      state.sidebar.opened = !state.sidebar.opened
    }
  },
  actions: {
    setUserInfo({ commit }, userInfo) {
      commit('SET_USER_INFO', userInfo)
    },
    toggleSidebar({ commit }) {
      commit('TOGGLE_SIDEBAR')
    }
  },
  getters: {
    getUserInfo: (state) => state.userInfo,
    getSidebarStatus: (state) => state.sidebar.opened
  },
  modules: {}
})

步骤6:配置pages/login模块(登录页)

1. pages/login/package.json
{
  "name": "login",
  "version": "1.0.0",
  "private": true,
  "scripts": {},
  "dependencies": {
    "common": "workspace:*",
    "main": "workspace:*"
  }
}

2. login 模块 Vite 配置(pages/login/vite.config.ts

import { defineConfig, mergeConfig } from 'vite'
import baseConfig from '../../vite.config.base.ts'
import proxyConfig from '../../vite.config.proxy.ts'
import path from 'path'
import { fileURLToPath } from 'url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

export default mergeConfig(
  baseConfig,
  defineConfig({
    server: {
      port: 9896,
      open: true,
      ...proxyConfig
    },
    build: {
      outDir: path.resolve(__dirname, 'dist'),
      emptyOutDir: true
    },
    root: __dirname,
    publicDir: path.resolve(__dirname, 'public')
  })
)

2. pages/login/src/Login.vue (登录组件)
<template>
  <div class="login-container flex-center">
    <el-card class="login-card" shadow="hover">
      <div class="login-header flex-center">
        <h2>系统登录</h2>
      </div>
      <el-form
        ref="loginFormRef"
        :model="loginForm"
        :rules="loginRules"
        label-width="80px"
        class="login-form"
      >
        <el-form-item label="用户名" prop="username">
          <el-input
            v-model="loginForm.username"
            placeholder="请输入用户名"
            prefix-icon="el-icon-user"
            clearable
          ></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            placeholder="请输入密码"
            prefix-icon="el-icon-lock"
            clearable
            show-password
          ></el-input>
        </el-form-item>
        <el-form-item label="验证码" prop="code">
          <el-row :gutter="10">
            <el-col :span="16">
              <el-input
                v-model="loginForm.code"
                placeholder="请输入验证码"
                prefix-icon="el-icon-check"
                clearable
              ></el-input>
            </el-col>
            <el-col :span="8">
              <div class="code-img flex-center">
                1234
              </div>
            </el-col>
          </el-row>
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            class="login-btn"
            @click="handleLogin"
            :loading="loading"
          >
            登录
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { setToken } from '/common/src/utils/cookie'

export default Vue.extend({
  name: 'Login',
  data() {
    return {
      loading: false,
      loginForm: {
        username: '',
        password: '',
        code: ''
      },
      loginRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' }
        ],
        code: [
          { required: true, message: '请输入验证码', trigger: 'blur' },
          { len: 4, message: '验证码长度为4位', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    async handleLogin() {
      try {
        // 表单验证
        const valid = await (this.$refs.loginFormRef as any).validate()
        if (!valid) return

        this.loading = true

        // 模拟登录请求
        setTimeout(() => {
          // 生成模拟Token
          const mockToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE3MTg4MDAwMDAsImV4cCI6MTcxOTQwNDgwMH0.8Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9Z9'
          
          // 设置Token
          setToken(mockToken)
          
          // 提示并跳转首页
          this.$message.success('登录成功!')
          this.$router.push('/home')
          
          this.loading = false
        }, 1000)
      } catch (error) {
        this.loading = false
        this.$message.error('登录失败,请重试!')
        console.error('登录错误:', error)
      }
    }
  }
})
</script>

<style scoped lang="scss">
.login-container {
  width: 100%;
  height: 100vh;
  background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);

  .login-card {
    width: 450px;
    padding: 20px;
    background-color: #fff;
    border-radius: $border-radius-md;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);

    .login-header {
      margin-bottom: 20px;

      h2 {
        color: $primary-color;
        font-size: $font-size-lg;
        font-weight: 600;
      }
    }

    .login-form {
      .code-img {
        width: 100%;
        height: 40px;
        background-color: #f5f5f5;
        border-radius: $border-radius-sm;
        color: #666;
        font-size: $font-size-md;
        letter-spacing: 5px;
      }

      .login-btn {
        width: 100%;
        height: 40px;
        font-size: $font-size-md;
      }
    }
  }
}
</style>

步骤7:配置pages/home模块(首页)

1. pages/home/package.json
{
  "name": "home",
  "version": "1.0.0",
  "private": true,
  "scripts": {},
  "dependencies": {
    "common": "workspace:*",
    "main": "workspace:*"
  }
}

2. home 模块 Vite 配置(pages/home/vite.config.ts

import { defineConfig, mergeConfig } from 'vite'
import baseConfig from '../../vite.config.base.ts'
import proxyConfig from '../../vite.config.proxy.ts'
import path from 'path'
import { fileURLToPath } from 'url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

export default mergeConfig(
  baseConfig,
  defineConfig({
    server: {
      port: 9897,
      open: true,
      ...proxyConfig
    },
    build: {
      outDir: path.resolve(__dirname, 'dist'),
      emptyOutDir: true
    },
    root: __dirname,
    publicDir: path.resolve(__dirname, 'public')
  })
)
2. pages/home/src/Home.vue (首页组件)
<template>
  <el-container class="home-container">
    <el-header class="header flex-between">
      <div class="logo flex-center">
        <h1>Vue2 Vite 分包项目</h1>
      </div>
      <div class="user-info flex-center">
        <el-dropdown>
          <span class="el-dropdown-link flex-center">
            <el-avatar icon="el-icon-user" size="medium"></el-avatar>
            <span class="username">管理员</span>
            <i class="el-icon-arrow-down el-icon--right"></i>
          </span>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="handleLogout">
              <i class="el-icon-switch-button"></i> 退出登录
            </el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </div>
    </el-header>
    <el-container>
      <el-aside width="200px" class="sidebar">
        <el-menu
          default-active="1"
          class="el-menu-vertical-demo"
          background-color="#2e3b4e"
          text-color="#fff"
          active-text-color="#ffd04b"
        >
          <el-menu-item index="1">
            <i class="el-icon-menu"></i>
            <span slot="title">首页</span>
          </el-menu-item>
          <el-submenu index="2">
            <template slot="title">
              <i class="el-icon-location"></i>
              菜单管理
            </template>
            <el-menu-item index="2-1">菜单1</el-menu-item>
            <el-menu-item index="2-2">菜单2</el-menu-item>
          </el-submenu>
          <el-menu-item index="3">
            <i class="el-icon-setting"></i>
            <span slot="title">系统设置</span>
          </el-menu-item>
        </el-menu>
      </el-aside>
      <el-main class="main-content">
        <el-card>
          <div class="welcome">
            <h2>欢迎使用 Vue2 + Vite + TypeScript 大型分包项目</h2>
            <p>当前版本:1.0.0</p>
            <p>开发者:新时代农民工徐哈哈</p>
            <p>技术栈:Vue2.7 + Vite4 + TypeScript + ElementUI + pnpm</p>
          </div>
          <div class="stats">
            <el-row :gutter="20">
              <el-col :span="6">
                <el-card class="stat-card" shadow="hover">
                  <div class="stat-content">
                    <p class="stat-title">用户数</p>
                    <p class="stat-value">1,234</p>
                  </div>
                </el-card>
              </el-col>
              <el-col :span="6">
                <el-card class="stat-card" shadow="hover">
                  <div class="stat-content">
                    <p class="stat-title">订单数</p>
                    <p class="stat-value">5,678</p>
                  </div>
                </el-card>
              </el-col>
              <el-col :span="6">
                <el-card class="stat-card" shadow="hover">
                  <div class="stat-content">
                    <p class="stat-title">销售额</p>
                    <p class="stat-value">¥98,765</p>
                  </div>
                </el-card>
              </el-col>
              <el-col :span="6">
                <el-card class="stat-card" shadow="hover">
                  <div class="stat-content">
                    <p class="stat-title">访问量</p>
                    <p class="stat-value">123,456</p>
                  </div>
                </el-card>
              </el-col>
            </el-row>
          </div>
        </el-card>
      </el-main>
    </el-container>
  </el-container>
</template>

<script lang="ts">
import Vue from 'vue'
import { removeToken } from '/common/src/utils/cookie'

export default Vue.extend({
  name: 'Home',
  methods: {
    handleLogout() {
      this.$confirm('确定要退出登录吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // 移除Token
        removeToken()
        // 跳转到登录页
        this.$router.push('/login')
        this.$message.success('退出登录成功!')
      }).catch(() => {
        this.$message.info('已取消退出')
      })
    }
  }
})
</script>

<style scoped lang="scss">
.home-container {
  .header {
    .logo {
      h1 {
        font-size: $font-size-lg;
        color: $primary-color;
        margin: 0;
      }
    }

    .user-info {
      .username {
        margin: 0 10px;
      }
    }
  }

  .sidebar {
    .el-menu {
      border-right: none;
      height: 100%;
    }
  }

  .main-content {
    .welcome {
      margin-bottom: 20px;
      padding-bottom: 20px;
      border-bottom: 1px solid #eee;

      h2 {
        color: $primary-color;
        margin-bottom: 10px;
      }

      p {
        color: $info-color;
        line-height: 1.8;
      }
    }

    .stats {
      .stat-card {
        .stat-content {
          .stat-title {
            color: $info-color;
            font-size: $font-size-sm;
            margin-bottom: 10px;
          }

          .stat-value {
            color: $primary-color;
            font-size: $font-size-lg;
            font-weight: 600;
          }
        }
      }
    }
  }
}
</style>

步骤8:安装依赖并运行项目

# 安装所有依赖
pnpm install

# 运行主入口项目(默认打开登录页)
pnpm dev / pnpm dev:main
# 打包
pnpm run build pnpm run build:all 等 看package配置

在这里插入图片描述

运行成功后,访问 http://localhost:9894 即可看到登录页面,输入任意用户名/密码(验证码填1234)即可登录跳转到首页。
在这里插入图片描述

三、项目优化方案

1. 性能优化

  • 代码分割:通过Vite的rollup配置实现路由懒加载和chunk分割
  • 静态资源优化:静态资源外置到static目录,通过CDN加速
  • 依赖预构建:Vite optimizeDeps 预构建常用依赖
  • 图片优化:使用vite-plugin-imagemin压缩图片
  • CSS优化:提取CSS为单独文件,开启CSS压缩

2. 开发体验优化

  • 类型检查:完整的TypeScript配置,提供类型提示
  • ESLint+Prettier:代码规范和格式化
  • 热更新:Vite原生热更新,提升开发效率
  • 环境变量:配置不同环境的.env文件

3. 打包优化

  • Tree Shaking:移除未使用代码
  • 压缩混淆:Terser压缩JS,CSS压缩
  • chunk大小控制:设置chunkSizeWarningLimit,避免大包
  • 缓存优化:文件名添加hash,实现长效缓存

四、新增pages模块快速生成模板

新增模块脚本(根目录 create-page.js

#!/usr/bin/env node
const fs = require('fs')
const path = require('path')

// 获取模块名称
const pageName = process.argv[2]
if (!pageName) {
  console.error('请输入模块名称,例如:node create-page.js dashboard')
  process.exit(1)
}

// 创建目录
const pagePath = path.join(__dirname, 'pages', pageName)
const srcPath = path.join(pagePath, 'src')
fs.mkdirSync(srcPath, { recursive: true })

// 创建package.json
const packageJson = {
  name: pageName,
  version: '1.0.0',
  private: true,
  scripts: {},
  dependencies: {
    "common": "workspace:*",
    "main": "workspace:*"
  }
}

fs.writeFileSync(
  path.join(pagePath, 'package.json'),
  JSON.stringify(packageJson, null, 2)
)

// 创建组件模板
const componentContent = `<template>
  <div class="${pageName}-container">
    <h1>${pageName} 模块</h1>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: '${pageName.charAt(0).toUpperCase() + pageName.slice(1)}',
  data() {
    return {}
  },
  mounted() {
    console.log('${pageName} 模块加载完成')
  }
})
</script>

<style scoped lang="scss">
.${pageName}-container {
  padding: 20px;
}
</style>
`

fs.writeFileSync(
  path.join(srcPath, `${pageName.charAt(0).toUpperCase() + pageName.slice(1)}.vue`),
  componentContent
)

console.log(`${pageName} 模块创建成功!路径:${pagePath}`)

使用方法

# 创建dashboard模块 需要独立运行则拷贝home文件夹下vite.config.ts
node create-page.js dashboard

五、总结

核心关键点回顾

  1. 环境要求:Node.js ≥16,pnpm ≥7,确保基础运行环境兼容
  2. 目录结构:common(公共)/main(主入口)/pages(业务模块)分离,各模块可独立运行
  3. 核心功能
    • 登录页通过Cookie模拟Token实现登录跳转
    • 路由守卫实现未登录重定向
    • 全局样式/工具/组件统一维护在common模块
  4. 配置要点
    • TS路径别名配置 /common/main 实现跨模块访问
    • 代理配置统一管理接口请求
    • ESLint+Prettier保证代码规范
  5. 运行方式pnpm dev:main 启动主项目,默认打开登录页

项目运行验证

  1. 执行 pnpm dev 启动项目
  2. 访问 http://localhost:9894 看到登录页面
  3. 输入任意用户名/密码,验证码填1234,点击登录跳转到首页
  4. 点击首页退出登录,可回到登录页

整个项目已完整实现,包括分包架构、版本号、登录跳转、TS/ESLint配置、静态资源优化等,可直接用于大型前端项目开发。
很多功能未提取为公共文件 自行按需调整。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐