Migrating a Vue 2 Project from Vue CLI to Vite: Step‑by‑Step Guide
This article provides a comprehensive step‑by‑step guide for migrating a Vue 2 project built with Vue CLI to Vite, covering project structure analysis, removal of Vue CLI dependencies, configuration changes, handling of environment variables, alias setup, JSX support, asset handling, and additional build optimizations.
Why migrate?
The main motivation is faster build speed: Vite uses esbuild for pre‑bundling, which is significantly quicker than Vue CLI’s Webpack‑based builds, improving developer experience.
Preparation – original project structure
The original Vue CLI project contains a typical layout with
.browserslistrc, .env files, vue.config.js, src/, public/, tests/and many configuration files.
├── .browserslistrc
├── .editorconfig
├── .env.development
├── .env.production
├── .eslintrc.js
├── .gitignore
├── package.json
├── vue.config.js
└── src/Remove Vue CLI dependencies
Search for the vue-cli keyword in package.json and delete related dependencies.
Replace the original npm scripts that use vue-cli-service with Vite equivalents:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
}
}Adjust entry point
Vue CLI’s default entry is src/main.js; Vite expects an index.html at the project root that includes a <script type="module" src="/src/main.js"> tag.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>Environment variables
Vue CLI exposes NODE_ENV, BASE_URL and variables prefixed with VUE_APP_. Vite uses MODE, BASE_URL, VITE_ ‑prefixed variables and accesses them via import.meta.env. Replace all VUE_APP_ variables with VITE_ or configure envPrefix in vite.config.js to keep the old prefix.
// package.json script example
"scripts": {
"dev": "vite",
"build": "vite build"
}
// vite.config.js snippet for envPrefix
export default defineConfig({
envPrefix: ['VUE_APP_', 'VITE_']
});Import extensions
Vue CLI allowed omitting the .vue extension; Vite recommends always specifying it for better IDE support.
// Vue CLI style (allowed but not recommended)
import App from './App'
// Vite style (recommended)
import App from './App.vue'Enable JSX support
Install vite-plugin-vue2 and enable JSX by passing { jsx: true } to the plugin.
import { defineConfig } from 'vite';
const { createVuePlugin } = require('vite-plugin-vue2');
export default defineConfig(() => ({
plugins: [createVuePlugin({ jsx: true })]
}));In a component file, use <script lang="jsx"> and export a render function.
<script lang="jsx">
export default {
render() { return <div>JSX Render</div>; }
}
</script>Alias configuration
Copy the resolve.alias object from vue.config.js into vite.config.js. The same alias keys (e.g., @, @api, @components) work with Vite.
resolve: {
alias: {
'@': resolve('src'),
'@api': resolve('src/api'),
'@components': resolve('src/components'),
// ...other aliases
}
}Node built‑in modules in client code
Modules like path are not available in the browser. Replace them with browser‑compatible equivalents such as path-browserify.
// Before (Vue CLI)
import path from 'path';
export function genPath(...paths) { return path.join(...paths); }
// After (Vite)
import path from 'path-browserify';
export function genPath(...paths) { return path.join(...paths); }Global CSS variables
Vue CLI used sass-resources-loader to inject global SCSS. In Vite, use css.preprocessorOptions.scss.additionalData. Note that importing the same SCSS file twice can cause a “file is already being loaded” error.
css: {
preprocessorOptions: {
scss: {
additionalData: `@import './node_modules/web-lib/packages/web-lib-styles/src/variables/index.scss';`
}
}
}Dynamic asset imports
Webpack’s require for dynamic images is replaced by Vite’s import.meta.globEager (eager loading) or new URL(..., import.meta.url) (lazy loading).
// Eager glob import
const images = import.meta.globEager('./images/*.png');
const iconSrc = images[`./images/${iconName}.png`].default;
// URL constructor
const iconSrc = new URL(`./images/${iconName}.png`, import.meta.url);SVG sprite handling
Replace svg-sprite-loader with vite-plugin-svg-icons. The plugin automatically registers SVG symbols.
import viteSvgIcons from 'vite-plugin-svg-icons';
export default defineConfig(() => ({
plugins: [
viteSvgIcons({
iconDirs: [resolve('src/icons/svg')],
symbolId: 'icon-[name]'
})
]
}));
// In main.js
import 'virtual:svg-icons-register';Other adjustments
Hot‑module‑replacement code in the store can be removed because Vite handles HMR automatically. When using environment variables inside vite.config.js, load them with dotenv instead of import.meta.env because the config file is evaluated before Vite processes .env files.
const { PORT } = require('dotenv').config({ path: './.env' }).parsed || {};
export default defineConfig({
server: { port: PORT, open: true }
});Final project structure and Vite configuration
The migrated project ends up with a simplified tree and a comprehensive vite.config.js that includes base path, plugins, alias, CSS pre‑processor options, server proxy settings, and build target.
├── .browserslistrc
├── .editorconfig
├── .env
├── .env.development
├── .env.production
├── .eslintrc.js
├── .gitignore
├── .husky/
├── package.json
├── public/
├── src/
├── tests/
├── vite.config.js
└── yarn.lock import path from 'path';
import { defineConfig } from 'vite';
import viteSvgIcons from 'vite-plugin-svg-icons';
const { createVuePlugin } = require('vite-plugin-vue2');
function resolve(dir) { return path.join(__dirname, dir); }
export default defineConfig(({ mode }) => {
const { PORT } = require('dotenv').config({ path: './.env' }).parsed || {};
const { VITE_URI_BUSINESS_SERVICE_BASE, VITE_URI_FILE_SERVICE_BASE, VITE_USER_SOCTET_BASE } =
require('dotenv').config({ path: `./.env.${mode}` }).parsed || {};
return {
base: './',
clearScreen: false,
plugins: [
createVuePlugin({ jsx: true }),
viteSvgIcons({ iconDirs: [resolve('src/icons/svg')], symbolId: 'icon-[name]' })
],
resolve: {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
alias: {
'@': resolve('src'),
'@api': resolve('src/api'),
'@components': resolve('src/components'),
// ...other aliases
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import './node_modules/web-lib/packages/web-lib-styles/src/variables/index.scss';`
}
}
},
server: {
port: PORT,
open: true,
proxy: {
'/user': { target: VITE_URI_BUSINESS_SERVICE_BASE, changeOrigin: true, secure: false },
'/file': { target: VITE_URI_BUSINESS_SERVICE_BASE, changeOrigin: true, secure: false },
'^/group1': { target: VITE_URI_FILE_SERVICE_BASE, changeOrigin: true, secure: false },
'^/wsUser': {
target: VITE_USER_SOCTET_BASE,
ws: true,
changeOrigin: true,
secure: false,
pathRewrite: { '^/wsUser': '' },
onProxyReqWs(proxyReq, req, socket) {
socket.on('error', err => console.error(err));
}
}
}
},
build: { target: 'es2015' }
};
});Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
