前些年开发的Vue项目,基本都是基于webpack构建的,打包构建和开发启动都比较慢,这几年流行了vite,给予了我们这些前端开发攻城狮一个新的选择。
今天在这里,我们不讨论如何使用vite构建一个新项目,而是带领大家,将基于webpack构建的Vue2项目一步步修改为基于vite构建,解决运行中出现的问题,使其能正常开发和打包,是不是有些迫不及待了!
呵呵,别急,开始之前,我们还是先了解下为什么使用vite和使用Vite有什么要求吧。
为什么使用vite
Vite 支持原生 ESM 模块(依托现代浏览器)和文件缓存,能够显著提升前端项目的启动和重新构建的速度。
vite使用要求
vite运行环境需要node 14.18+ / 16+
温馨提醒
- 由于node版本升级,一些依赖包可能存在兼容问题;
- 和webpack比起来,vite技术比较新,结合在现有项目,可能会产生一些未意料到的问题;
- 建议先用于本地开发联调,测试无误再用此打包上上生产环境。
哦,先给大家说一下,由于不是创建vite项目,而是修改一个已经存在的vue2项目,所以肯定会出现一些我们意想不到的情况需要解决,甚至会出现在我们修改了基本的vite配置,搭建好服务器后,项目跑不起来的情况。所以大家得有个心里准备,别想一两步就改好,万事大吉了。
好了,知道了这些,接下来就开始我们的修改的征程吧,文笔不好,大家请将就下吧
克隆项目
克隆原本的项目或者在原本的项目新开个vite分支,用于修改为vite;
创建vite.config.js
在项目根目录创建vite.config.js(跟原来的 vue.config.js同级),作用就不用我多说了吧,有什么配置都得在里头写。
export default {
// 配置选项
}
安装Vite依赖
Vite官方文档:插件
安装vite、@vitejs/plugin-vue2
npm install vite@latest @vitejs/plugin-vue2 --save-dev
温馨提醒: 这里下载的是vite的最新版本,其实截止到目前,下载vite的最新版本会下载到5点几的版本了,5点几的版本并不适配我们vue2的项目,会出错。请允许我暂时打个哑谜,因为后面会讲到。如果你不想后面走这无用功,可现在就下载vite@4.5.3
npm install vite@4.5.3 --save-dev
@vitejs/plugin-vue则是为了更好地支持Vue框架的开发而诞生的Vite插件。
@vitejs/plugin-vue旨在支持基于Vite构建的Vue项目,它使用Vue 3的编译器来处理Vue文件,同时也支持在模板中使用Vue.js官方提供的自定义块。该插件还提供了Vue单文件组件(SFC)的加载和解析方式,以及CSS、SCSS等样式语言文件的处理方式。
通过使用@vitejs/plugin-vue,开发者可以在Vite工程中无缝地使用Vue框架。而且,@vitejs/plugin-vue的运行速度极快,能够在即时编译和热更新上提供出色的性能表现。在项目中,我们只需要通过npm安装该插件,然后在vite.config.js中配置插件即可使用。
而@vitejs/plugin-vue2只作用于Vue@^2.7.0。
从Vite官方文档,插件 页面列表我们看到,想要将vue2项目迁移到vite,@vitejs/plugin-vue 没有支持vue版本低于2.7的,所以我们需要升级vue版本到2.7(如果您发现有支持vue2.7以下版本,请给我留言),我个人是喜欢vue2.7的,毕竟vue2.7支持vue3的很多特性,用起来别提有多香。


npm uninstall vue // 卸载vue@2.6.11
npm install vue@2.7.14 --save // 下载vue@2.7.14
// "vue": "^2.7.14", //
安装了vue@2.7.14 ,vue-template-compiler就可以卸载了 ( vue@2.7以前版本需要和vue配合使用,并且两者版本号最好一致,v2.7以后可以抛弃了)
将@vitejs/plugin-vue2 引入defineConfig 的配置plugins中
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue2'
export default defineConfig({
plugins: [ Vue(), ],
})
如果你不知道,一开始就使用了@vitejs/plugin-vue,可能会遇到这样的报错
根据上面提示,我们知道了@vitejs/plugin-vue需要在vue版本大于等于3.2.25才能使用
配置服务器
Vite官方文档:开发服务器选项
devServer 改为 server ,主要功能选项跟在webpack中使用相同,部分配置有差异的,可参考Vite的官方文档【服务器选项】修改:
// vue.config.js
devServer: {
port,
open: true,
overlay: {
warnings: false,
errors: true
}
}
// vite.config.js
server: {
open: true,
port: port
},
配置构建选项
Vite官方文档:构建选项
根据文档,整理了一些构建配置在webpack和vite中各自的名称,如下表:
| webpack | vite | 说明 |
|---|---|---|
| publicPath | base | 开发或生产环境服务的公共基础路径,用于指定项目中静态资源的基本路径 |
| outputDir | build.outDir | 指定输出路径(相对于 项目根目录). |
| assetsDir | build.assetsDir | 指定生成静态资源的存放路径(相对于 build.outDir)。在 库模式 下不能使用。 |
| productionSourceMap | build.sourcemap | 构建后是否生成 source map 文件 |
| lintOnSave | 未查到,有知道的请告诉我 | 用于指定是否在保存文件时执行代码检查(linting)。设置为 false,则在保存文件时不会运行 lint 检查,禁用 linting 功能 |
// vue.config.js
module.exports = {
publicPath: './',
outputDir: 'dist',
assetsDir: 'static',
// lintOnSave: import.meta.env.MODE === 'development',
lintOnSave: false,
productionSourceMap: false,
}
// vite.config.js
export default defineConfig({
// 开发或生产环境服务的公共基础路径,用于指定项目中静态资源的基本路径
base: './',
// 构建选项
build: {
// 指定输出路径(相对于 项目根目录).
outDir: "dist",
// 指定生成静态资源的存放路径(相对于 build.outDir)。在 库模式 下不能使用。
assetsDir: 'static',
// 构建后是否生成 source map 文件
sourcemap: false,
},
})
关于index.html
Vite官方文档:index.html 与项目根目录
迁移 public/index.html 到根目录(和vite.config.js同级),删除public文件夹,同时修改里面的 webpack变量 BASE_URL、webpackConfig.name,最后在body末尾引入main.js
// 在 webpack 项目的 public/index.html
<link rel="icon" href="<%= BASE_URL %>logo.png" />
<title><%= webpackConfig.name %></title>
// 在 vite 项目的 index.html
<link id="title-icon" rel="icon" href="logo.png" />
<script type="module">
import settings from './src/settings.js'
document.title = settings.title
</script>
<script type="module" src="/src/main.js"></script>
关于 package.json
Vite官方文档:命令行界面
1、修改原本的webpack命令 vue-cli-service serve 为 vite , vue-cli-service build 为 vite build
// 在 webpack 项目中
"scripts": {
"dev": "vue-cli-service serve",
"dev:test": "vue-cli-service serve --mode test",
"dev:prod": "vue-cli-service serve --mode production",
"build": "vue-cli-service build --mode development",
"build:test": "vue-cli-service build --mode test",
"build:prod": "vue-cli-service build --mode production",
},
// 在 vite 项目中
"scripts": {
"dev": "vite",
"dev:test": "vite --mode test",
"dev:prod": "vite --mode production",
"build": "vite build --mode development",
"build:test": "vite build --mode test",
"build:prod": "vite build --mode production",
},
2、删除原来项目中跟webpack相关的依赖
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-plugin-unit-jest": "^4.5.0",
"@vue/cli-service": "^4.5.0",
关于 configureWebpack 和 chainWebpack
configureWebpack 和 chainWebpack 的作用相同,两个都能对webpack进行配置,唯一的区别就是他们修改webpack配置方式不同;
configureWebpack 通过操作对象得形式来修改webpack配置,该对象将会被webpack-merge合并到最终得webpack配置中;
chainWebpack 是一个函数,会接收一个基于webpack-chain的ChainableConfig实例,通过链式编程的形式来修改默认的webpack配置。
讲偏了,回到vue.config.js中的代码
// vue.config.js
const CompressionPlugin = require('compression-webpack-plugin')
const sftpUploader = require('sftp-uploader')
const sftp = new sftpUploader({
dir: path.join(__dirname, 'dist/'),
host: 'host',
url: 'url',
port: 'port',
username: 'username',
password: 'password',
previewPath: 'previewPath'
})
const GitRevisionPlugin = require('git-revision-webpack-plugin')
const gitRevisionPlugin = new GitRevisionPlugin({
versionCommand: 'describe --tags --always --dirty="-dev"',
commithashCommand: 'log --max-count=1 --no-merges --pretty="%ai|%s"'
})
function resolve (dir) {
return path.join(__dirname, dir)
}
module.exports = {
configureWebpack: config => {
const plugins = [
gitRevisionPlugin,
new webpack.DefinePlugin({
'process.VERSION': JSON.stringify(gitRevisionPlugin.version()),
'process.COMMIT': JSON.stringify(gitRevisionPlugin.commithash())
}),
sftp
]
plugins.push(
new CompressionPlugin({
test: /\.js$|\.html$|\.css/,
threshold: 10240,
deleteOriginalAssets: false
})
)
return {
name: name,
resolve: {
alias: {
'@': resolve('src')
}
},
devtool: 'source-map',
plugins
}
},
chainWebpack (config) {
// 代码已省略
}
}
resolve
resolve.alias
Vite官方文档:resolve.alias
// vite.config.js
import path from 'path'
export default defineConfig({
resolve: {
// 别名
alias: {
"@": path.resolve(__dirname, "src"), // path.resolve(__dirname, './src')
},
},
})
resolve.extensions
Vite官方文档:resolve.extensions

怎么会引入文件出错了呢?
原来啊,
在Vue CLI项目中,默认配置已经包含了对.vue文件的支持,并且在导入时自动省略文件扩展名。 但在vite中,并不推荐这种方式,默认需要引入时添加上后缀.vue。
我们可在文档中看到,如下截图:

虽然vite不建议,但是我们以前基本都是省略了后缀.vue的啊,况且我就是不想写后缀,怎么办呢?
其实我们只需在extensions数组中加入.vue即可。
export default defineConfig({
resolve: {
// 导入时想要省略的扩展名列表,默认值['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']
// 官方文档中说:不建议忽略自定义导入类型的扩展名(例如:.vue),因为它会影响 IDE 和类型支持
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
})
webpack.DefinePlugin
在vue.config.js中通过 webpack.DefinePlugin 定义的变量,通常可以在vite.config.js的 define中定义;
// vite.config.js
export default defineConfig({
define: {
'process.VERSION': JSON.stringify(gitRevisionPlugin.version()),
'process.COMMIT': JSON.stringify(gitRevisionPlugin.commithash())
},
})
其他plugins方法
其他vue.config.js中plugins引入的方法,可直接迁移到vite.config.js中,只需将 引入方式 require 修改为 import即可。
比如 sftpUploader
import sftpUploader from 'sftp-uploader'
const sftp = new sftpUploader({
dir: path.join(__dirname, 'dist/'),
host: 'host',
url: 'url',
port: 'port',
username: 'username',
password: 'password',
previewPath: 'previewPath'
})
export default defineConfig({
plugins: [
Vue(),
// ViteRequireContext(), // 亲测有效,兼容require.context
// transformScss(), // 亲测有效,但是不推荐,建议全局替换deep
// vueJsx(),
sftp
]
})
在这里我要额外唠叨一下:
在我这个项目中,引入"sftp-uploader"会出现问题(不是上面的代码或者使用方式有问题,而是其他原因),我们这个插件作用是用于将前端打包后的代码上传到服务器,这段代码我们在迁移到vite时,可以暂时先不处理,等到迁移到vite结束,项目正常运行起来后再将之引入,遇到的问题我们另开一篇来讲。
感兴趣的可打开后方的链接: 迁移到vite后使用sftp-uploader遇到的坑
环境变量
环境变量(.env.development、.env.production、.env.test)中所有以VUE_开头的变量都得改为VITE_开头,并在整个项目中全局替换这些修改的变量。
// 在 webpack 项目中
ENV = 'development'
VUE_APP_HOST = host_name
VUE_APP_API_PREFIX= prefix_name
VUE_APP_BASE_API = '${VUE_APP_HOST}/${VUE_APP_API_PREFIX}/one-travel-api/'
VUE_APP_UPLOAD_API = '${VUE_APP_HOST}/upload-api-noauth/'
VUE_APP_AUDIT_RECORD_API = '${VUE_APP_HOST}/${VUE_APP_API_PREFIX}/audit-api/'
VUE_APP_H5_GENERATOR_API = '${VUE_APP_HOST}/demo/xinjiang/h5-generator/#/home'
// 在 vite 项目中
# 不需要配置 ENV 或 NODE_ENV,判断环境统一使用更可靠的 import.meta.env.MODE
VITE_APP_HOST = host_name
VITE_APP_API_PREFIX= prefix_name
VITE_APP_BASE_API = '${VITE_APP_HOST}/${VITE_APP_API_PREFIX}/one-travel-api/'
VITE_APP_UPLOAD_API = '${VITE_APP_HOST}/upload-api-noauth/'
VITE_APP_AUDIT_RECORD_API = '${VITE_APP_HOST}/${VITE_APP_API_PREFIX}/audit-api/'
VITE_APP_H5_GENERATOR_API = '${VITE_APP_HOST}/demo/xinjiang/h5-generator/#/home'
在vite中,不需要配置 ENV 或 NODE_ENV,判断环境统一使用更可靠的 import.meta.env.MODE
在整个项目中,全局替换 process.env.NODE_ENV、 process.env.ENV 为 import.meta.env.MODE
process.env
在使用Vite替换webpack作为构建工具后,一般都会遇到环境变量访问process.env失败的问题。
Vite默认使用Vite特有的方式(import.meta.env)来导入环境变量,而不是通过process.env。
解决方法:
1、使用Vite的环境变量引导入方式。
在Vite中,你可以直接在代码中导入环境变量,如下所示:
import.meta.env.VITE_API_URL;
所以,针对本项目,我采用了这种Vite官方推荐的方式 。
在整个项目中,全局替换 process.env 为 import.meta.env 。
2、如果你希望继续使用
process.env访问环境变量,你可以通过Vite插件define来实现。
首先,安装vite-plugin-env-compatible插件:
npm install vite-plugin-env-compatible --save-dev
然后,在Vite配置文件中使用该插件:
// vite.config.js
import EnvCompatiblePlugin from 'vite-plugin-env-compatible';
export default {
plugins: [
EnvCompatiblePlugin({
// 可以指定需要转换的环境变量前缀,默认是'VITE_'
prefix: 'VITE_'
})
]
};
使用插件后,process.env应该能够正常访问环境变量。
第二种方案,是我在网上见到的,不推荐,我也没有尝试过,具体是否可行待验证。
请注意
Vite本身推荐使用导入方式来访问环境变量,因为这更符合现代JavaScript工具链的模式。
使用插件方式只是为了兼容旧代码,在新项目中还是应该优先考虑使用Vite推荐的方法(即import.meta.env方式引入环境变量)。
关于文件压缩
在Vite中,你不需要使用compression-webpack-plugin来处理生产环境的文件压缩,因为Vite本身就内置了生产环境的压缩优化。
在Vite项目中,你可以通过配置来启用压缩。Vite使用Rollup作为构建工具,并且默认情况下已经为你启用了多种压缩优化。
以下是一些你可以在Vite配置文件中启用的压缩优化选项:
- 树摇 (Tree-shaking): Vite 通过 Rollup 的静态分析能够自动移除未使用的代码。
- 代码分割 (Code splitting): Vite 默认支持按需加载,只有当某个模块被实际请求时,才会加载该模块的代码。
- 压缩 JS 和 CSS: Vite 使用 Rollup 插件来压缩你的 JavaScript 和 CSS 文件。
- 图片优化: Vite 内置了对图片的压缩,通过插件如vite-plugin-imagemin可以进一步优化。
如果你需要更多控制,可以自定义 Rollup 配置来添加更多的插件。
如果你确实需要进一步控制压缩过程,可以查看Vite的官方文档来了解如何自定义配置。
因此譬如下方原本vue.config.js中压缩js、html、css的代码,可以忽略不管了。
plugins.push(
new CompressionPlugin({
test: /\.js$|\.html$|\.css/,
threshold: 10240,
deleteOriginalAssets: false
})
)
到了这里,基本配置似乎改得也差不多了啊
【 实际上还有后面即将提到的 require、reqiure.context 等不支持的问题,还有很多问题需要修改后才能将本地服务器跑起来 】
我们先在本地运行一下项目,瞧瞧
如果你见到服务器能跑起来,那先恭喜你,已经小半只脚踏入了成功的门槛了。

但是,如果你遇到了下面的错误,

别慌,
SyntaxError: Unexpected token '??=' , 这个错误表明你的代码中出现了一个不被期望的标记 ??= ,
我在项目中全局搜索,也没发现 '??=',又搜索?.,倒是在项目中搜到了不少,关键这里报错并没有指向我们自己写的代码,你喵的什么情况?
我查了好些资料,别人也没有提到这种情况啊,怎么办???
后来突然灵光一现,以前不是经常遇到因为版本问题引起过这样的问题么?是不是node版本导致的呢?查看node

node版本14.19.2,满足vite运行的条件啊!再看看,瞧到了这里

怎么vite版本都5.3了,原来用的好像是4点几的版本吧,莫不是它在作怪的吧。立马从新卸载了vite@5.3.0,从新下载了vite@ 4.5.3
npm uninstall vite
npm install vite@4.5.3 --save-dev
喵的,这个问题没有了,又出来个问题

百度搜索 The package "@esbuild/win32-x64" could not be found, and is needed by esbuild.
别人给了个解决方案:
在运行dev之前先运行node node_modules/esbuild/install.js命令来解决esbuild安装问题;如下:

到此,总算见到服务器跑起来了。
到这里,虽然本地服务器是运行起来了,可还能见到代码中的报错

好吧,别着急,我们接下来还得一点点解决
替换CommonJS
vite 使用 ESM 作为模块化方案,因此不支持使用 require 方式来导入模块。否则在运行时会报 Uncaught ReferenceError: require is not defined 的错误(浏览器并不支持 CJS,自然没有 require 方法注入)。
此外,也可能会遇到 ESM 和 CJS 的兼容问题。当然这并不是 vite 构建所导致的问题,但需要注意这一点。简单来说就是 ESM 有 default 这个概念,而 CJS 没有。任何导出的变量在 CJS 看来都是 module.exports 这个对象上的属性,ESM 的 default 导出也只是 cjs 上的 module.exports.default 属性而已。
例如之前的代码:
/****** 基于 webpack 构建的项目 ******/
// settings.js
module.exports = {
title: '一码游新疆',
areaCode: '650000000000', // 新疆区域code
// 其余参数已省略
}
// 使用时
import { title as systitle } from '@/settings.js'
import { areaCode } from '@/settings.js'
在导出和导入上都需要修改为 ESM,例如:
/****** 基于vite 构建的项目 ******/
export default {
title: '一码游新疆',
areaCode: '650000000000', // 新疆区域code
// 其余参数已省略
}
// 使用时
import settings from '@/settings.js'
const {title } = settings
安装sass
npm install -D sass
看上面的报错,是好多原来node-sass的代码的一些写法不支持了, 我看了下文档和百度,也没发现支持node-sass的(或许有,是我没查到吧),故只能改为sass了。
继续运行本地服务器

喵的好几个报错,怎么办,先从简单的开始解决
先瞧瞧这个
[sass] expected selector.
243 │ & /deep/ .el-form-item__content {
src\styles\index.scss 243:5 root stylesheet
其实,这种报错,下方还有很多,看这意思,是 /deep/ 不支持了,
解决办法:搜索整个项目,全局替换 /deep/ 为 ::v-deep
其实还有一种解决方案,就是定义一个vite plugin 方法,此方法函数用于找到 /deep/ 并替换为 ::v-deep, 然后在 vite.config.js的plugins 中 引入此方法即可。
// vite.config.js
function transformScss() {
return {
name: 'vite-plugin-transform-scss',
enforce: 'pre',
transform(src, id) {
if (
/\.(js|ts|tsx|vue|scss)(\?)*/.test(id) &&
!id.includes('node_modules')
) {
return {
code: src.replace(/\/deep\//gi, '::v-deep'),
};
}
},
};
}
export default defineConfig({
plugins: [
Vue(),
transformScss(),
]
})
再看另一个问题
~ 符号不支持

还有出现这样的错误提示的
[sass] Can't find stylesheet to import.
25 │ @import "~element-ui/packages/theme-chalk/src/index";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src\styles\element-variables.scss 25:9 root stylesheet
按照错误提示,是没有找到 ~element-ui/packages/theme-chalk/src/index 这个文件啊,可我再项目中确认了,这个文件确实存在的啊,
那应该是前面这个符号~ 无法识别了,我们可以在别名alias中处理此符号,
export default defineConfig({
resolve: {
alias: [
{
find: /^~/,
replacement: ''
}
],
},
})
但是不知道什么原因,我第一次尝试这样设置的时候,是成功了的,可是后来这种解决方式失效了。
因此到最后,我采用了直接在整个项目中删除此~符号。
其他跟 ~ 相关的代码修改:
<!-- 基于 webpack 构建的项目 -->
<img src="~./home.jpg" style="width:100%;pointer-events:none">
<!-- 基于 vite 构建的项目 -->
<img src="./home.jpg" style="width:100%;pointer-events:none">
/****** 基于 webpack 构建的项目 ******/
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";
/****** 基于 vite 构建的项目 ******/
$--font-path: 'node_modules/element-ui/lib/theme-chalk/fonts';
@import "node_modules/element-ui/packages/theme-chalk/src/index";
继续本地运行,报错
Deprecation Warning:Using / for division outside of calc() is deprecated and will be removed in Dart Sass 2.0.0.

这样的报错,在项目中有很多,且大多数都指向引入的element-ui,使我无限懵逼,后来我才想到,应该是在新安装的Sass版本
"sass": "^1.77.5",
不支持此语法了,为了不改动原有的代码和修改element-ui 版本,我将sass版本降级到了
"sass": "^1.32.12",
接下来还得继续,报错

根据错误提示,目前加载store的时候有问题,查看里面的代码,有几个地方涉及require引入文件,问题来了。
require 和 require.context 只在基于webpack构建的项目中有效,那这里该如何修改呢?
require.context不支持
webpack使用 require.context() 来动态查找文件内容,但vite不支持。
有两种修改方式:第一种是修改为vite的导入方式,另一种则是使用插件兼容原来的语法。两种修改方式思想不同,结果都一样,各有千秋。个人还是建议使用第一种修改方式。
第一种
导入多个模块
Vite官方文档:Glob 导入
将reqiure.context()修改为import.meta.globEager()或者 import.meta.glob()
// 在webpack中
const context = require.context('./', false, /\.vue$/)
// 在vite中
const context = import.meta.glob("./**/*.vue")
在本项目中,require.context主要在 批量注册全局组件 和 自动引入Vuex中分割的Module 两个功能下有涉及,如下图

相应的如何修改并让原本的功能正常运行,我已经在另外两篇文章中做了详细阐述:
在这里我就不再做过多阐述了。
第二种
使用插件兼容:@originjs/vite-plugin-require-context
处理办法:
安装 @originjs/vite-plugin-require-context 支持 require.context
import ViteRequireContext from '@originjs/vite-plugin-require-context'
export default defineConfig({
plugins: [
ViteRequireContext(), // 兼容require.context
]
})
此种兼容require.context的方式我已经在项目中测试通过,尚未发现问题。
require不支持
作为webpack中常用的一个导入函数,require在导入图片、js、vue等各种文件中发挥着重要的作用。
然而在vite中,引入图片不能再用require了,使用 new URL(url,import.meta.url).href 才能获取到引入图片的链接地址。
// 在webpack中
logo: require('./images/logo.png')
pathStr = require('@/assets/audit_images/audit_wait.png')
// 在vite中
logo: new URL('./images/logo.png', import.meta.url).href
pathStr = new URL('@/assets/audit_images/audit_wait.png', import.meta.url).href
<!-- 在webpack中-->
<img class="radios-temp" :src="require('./images/type' + item.dataValue + '.png')" />
<!-- 在vite中-->
<!-- <img class="radios-temp" :src="new URL('./images/type' + item.dataValue + '.png', import.meta.url).href" />-->
<img class="radios-temp" :src="$requireImg('./images/type' + item.dataValue + '.png')" />
<!-- 在html中require图片地址,需要将new URL方法封装为函数使用,否则在本地开发时OK,但打包时会报错,具体可查看本篇末尾`打包时的尴尬-问题二` -->
由于在项目中使用require导入图片的地方比较多,我们也可以将导入图片抽象为一个公共方法使用(推荐)
import Vue from 'vue'
export function requireImg (url) {
return new URL(url,import.meta.url).href
}
Vue.prototype.$requireImg = requireImg
项目中其他导入vue文件的代码也需要修改,如下:
/****** 在webpack中 ******/
const menuContainer = [
resolve => require(['@/layout'], resolve),
resolve => require(['@/layout/twoLevelLayout.vue'], resolve),
resolve => require(['@/layout/twoLevelLayout.vue'], resolve)
]
/****** 在vite中 ******/
const menuContainer = [
() => import('@/layout'),
() => import('@/layout/twoLevelLayout.vue'),
() => import('@/layout/twoLevelLayout.vue')
]
/****** 在webpack中 ******/
const secondLevelMenu = parentPath + item.path
try {
item.component = resolve => require([`@/views${secondLevelMenu}`], resolve)
} catch (e) {
console.error('找不到该界面:' + secondLevelMenu)
}
/****** 在vite中 ******/
const viewsPath = import.meta.glob('@/views/**/*.vue') // vite动态引入文件
const secondLevelMenu = parentPath + item.path
try {
item.component = viewsPath[`/src/views${secondLevelMenu}.vue`]
} catch (e) {
console.error('找不到该界面:' + secondLevelMenu)
}
改完调试,还能看到下面的问题

上面提示在这几个地方引入sass变量文件的时候加入'?inline'
为什么会这样啊?我们打开它提示报警的文件,发现在这两个sass文件后面都有一个共同点,有需要导出的变量 ,
如element-variables.scss 中
$--color-primary: #1890ff;
:export {
theme: $--color-primary;
}
所以我们不难看出,在vite中,有导出变量的sass文件,在其它文件引入时,需要在后方加上"?inline"
这应该也是vite和webpack两者之间的差异吧
按照提示,在这两段引入的scss变量文件后面加入了'?inline'
/****** 在webpack中 ******/
import variables from '@/styles/element-variables.scss'
/****** 在vite中 ******/
import variables from '@/styles/element-variables.scss?inline'

改好后,我们继续看

而在浏览器中报错这样的:

定位到出错文件的代码:
<style lang="scss" scoped>
.icon-size {
width: 18px !important;
margin-right: 10px !important;
// margin-left: 15px !important;
margin-left: 0px !important;
}
</style>
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
if (icon.includes('-')) {
vnodes.push(<i class={icon + ' icon-size'}></i>)
} else {
vnodes.push(<svg-icon icon-class={icon} class="icon-size" />)
}
}
if (title) {
vnodes.push(<span slot="title">{title}</span>)
}
return vnodes
}
}
</script>
看这报错,应该是不支持 jsx了,最先安装了@vitejs/plugin-vue-jsx ,尝试运行,发现还是报错,再看文档,发现是针对Vue3的

,而针对Vue2的是 @vitejs/plugin-vue2-jsx,所以又安装了@vitejs/plugin-vue2-jsx,结果发现还是继续报错。
百度了好些,都没找到能真正行之有效的解决方案。后来想着算了,反正项目中这样的写法也不多,直接修改这个组件得了。
<style lang="scss" scoped>
.icon-size {
width: 18px !important;
margin-right: 5px !important;
margin-top: -3px !important;
margin-left: 0px !important;
}
.is-active .icon-size {
color: #409eff;
}
</style>
<template>
<span>
<i v-if="icon" :class="[icon, 'icon-size']"></i>
{{ title }}
</span>
</template>
<script>
export default {
name: 'MenuItem',
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
}
}
</script>
到此,我们项目已经能正常跑起来了,本地服务器后台也没有了报错,我也正常运行项目,登录成功跳转到首页,随便点了些页面,发现都能正常跑起来。 可是,别以为这样就结束了,如果我们要将改写好的代码用到实际中,最后上到生产的话,我们还是得每个页面每个功能,都得重新回归测试的。
譬如我修改的这个项目,就在我以为没啥大问题的时候,无意中点到了几个页面,就发现页面一片空白,诡异的是浏览器里面没有任何报错,本地服务器后台同样如此。可偏偏页面就是空白了,我又重新点击了好些个页面,发现一些页面同样如此,这样就把我搞蒙圈了,没任何报错,就是空白,什么情况这是?
我又仔细观察了空白页面跟正常运行页面之间的差别,终于被我发现,原来,空白的页面跟正常页面文件结构不同,比如项目中景区管理文件夹

我们配置的路由路径,前两个到文件夹名scenic和venue这里就结束了,index.vue 作为默认文件名,在webpack中,它是可以自己查询到的,
可是到了vite中,它就没去自动查找获取了,结果在vite中,它去找的是scenic.vue、venue.vue,结果很显然,项目中并不存在这两个文件,而在我们根据后台router数据渲染页面的代码中,
// 菜单目录容器, 暂时极限支持四级菜单
const menuContainer = [
() => import('@/layout'),
() => import('@/layout/twoLevelLayout.vue'),
() => import('@/layout/twoLevelLayout.vue')
]
export function filterAsyncRoutes (routes) {
let viewsPath = []
viewsPath = import.meta.glob('@/views/**/*.vue') // vite动态引入文件
function replaceComponent (routes, parentPath = '', level = 0) {
routes.map(item => {
if (item.path && !item.path.startsWith('/')) {
item.path = '/' + item.path // 补全根路径
}
if (item.type === '0' && !item.children) {
return // 去掉空目录
}
// 目录
if (item.type === '0') {
item.component = menuContainer[level] // 目录容器
}
// 菜单
if (item.type === '1') {
const secondLevelMenu = parentPath + item.path
try {
console.log('---viewsPath---',viewsPath)
item.component = viewsPath[`/src/views${secondLevelMenu}.vue`]
} catch (e) {
console.error('找不到该界面:' + secondLevelMenu)
}
}
if (item.children) {
const children = replaceComponent(
item.children,
parentPath + item.path,
1 + level
)
// 访问根路径时定向到第一个界面
item.children = [
{
path: '/',
redirect: parentPath + item.path + `/${children[0].path}`
},
...children
]
} else {
item.children = []
}
if (level != 0) {
// 除了根目录,vue-router 其他子目录不需要 /
item.path = item.path.substring(1)
}
return item
})
return routes
}
如下图箭头指向

为了能清楚的说明上面代码发生了什么,我以场馆管理页面为例,在生成路由的地方添加console,打印路由地址和对应需要引入的vue文件,我们就能清晰的弄清楚问题所在了。
在webpack中
路由地址为:/applets/scenic/venue
对应的vue文件路径:@/views/applets/scenic/venue
require 导入这个vue文件,它可以自动找到venue文件夹下的index.vue,相当于使用require,它先找了@/views/applets/scenic/venue.vue,如果找不到,它还可以自动去找@/views/applets/scenic/venue/index.vue
如下图


在vite中
路由地址为:/applets/scenic/venue
对应的vue文件路径:/src/views/applets/scenic/venue.vue,
可以看出,在vite中,只能去找 【/src/views/applets/scenic/venue.vue】,由于这个文件不存在,故出现了空白。
如下图

或许有人会说了,你的vue模板路径,在webpack中,你并没有在末尾加上".vue"后缀,却在vite中加上了,你把在vite中加上的".vue"去掉不就可以了么?
可事实上是,如果我在vite中去除了".vue",那么整个vue模板,再使用import导入后,连原本可行的页面都一起空白了,根本就行不通。
至于
item.component = viewsPath[`/src/views${secondLevelMenu}.vue`]
这里为啥写成/src,而不是@,是因为上方的 viewsPath变量,它使用 import.meta.glob
viewsPath = import.meta.glob('@/views/**/*.vue')
一次性将views下所有的vue文件都导入生成了一个对象
我把它打印出来,如下图

这个对象里,vue文件地址为key,import导入的文件为vue,key值都是 /src开头
所以知道在vite中,为啥我们使用的是/src开头了吧。
既然我们已经知道了,在vite中,是对应路由下,未能正常获取到vue文件路径,那么,我们也该知道怎么修改了吧!
方法一:
调整出现空白的页面路径,将对应文件夹下的index.vue,按照文件夹名改名并挪到上一级即可(挪移的过程中可能会涉及到修改对应引入的文件路径)
显然这种方式并不友好,对于要迁移的老项目,这种方式的显然不少,何况挪移修改的同时,由于其相对路径的变更,还得同时修改其它引入的文件相对路径。
方法二:
修改获取vue文件的代码
// 优化前
item.component = viewsPath[`/src/views${secondLevelMenu}.vue`]
// 优化后
const viewUrl = `/src/views${secondLevelMenu}`
item.component = viewsPath[`${viewUrl}.vue`]??viewsPath[`${viewUrl}/index.vue`]
到目前为止,我要迁移的整个项目在启动本地服务器后各个页面都正常跑起来了,各个页面点点点也没见到报错了,是不是有点小激动(当然可能还存在其他问题,需要整个项目整体重新测试一番才行哦)。
打包时的尴尬
本以为项目都能正常跑起来了,打包应该也没问题呗,实际上还又遇到问题了。
运行 npm run build ,打包过程出现了如下几个报错:
问题一:

根据报错提示,定位到报错的vue文件,发现模板
<template lang="html"></template>
template 上多了个属性lang,其实vue文件模板默认就是以html来解析的,这种写法不推荐使用。 不晓得是原来哪个同事写的,在webpack中并不会出错,结果现在迁到vite就报错了,
无须多做考虑,本就是默认值,直接删除lang="html"即可,
问题二:

定位到文件,发现原来是图片img由require 改为 new URL , 里面 import.meta 无法识别导致报错。

建议将 require图片的 new URL 封装为公共函数,需要require图片的时候直接调用即可。
function $requireImg(url){
return new URL(url, import.meta.url).href
}
打包时可能需要的其他设置
删除注释、debugger、console
在webpack中,我们一般使用插件 terser-webpack-plugin、uglifyjs-webpack-plugin 来实现;
1、使用 terser-webpack-plugin
npm install terser-webpack-plugin --save-dev
yarn add -D terser-webpack-plugin
pnpm add -D terser-webpack-plugin
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
configureWebpack: {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
format: {
comments: false
}
},
extractComments: false ,
compress: {
}
})
]
}
},
}
2、使用 uglifyjs-webpack-plugin
npm install uglifyjs-webpack-plugin -D
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
configureWebpack: {
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
// 删除注释
output: {
comments: false
},
// 删除console debugger 删除警告
compress: {
drop_console: true, //console
drop_debugger: false,
pure_funcs: ['console.log'] //移除console
}
}
})
]
}
}
}
在vite中,我们可以设置 build.minify为'terser'(需要下载terser),然后在 build.terserOptions中设置terser的参数即可
Vite官方文档:build-terseroptions
npm add -D terser
export default defineConfig({
minify:'terser',
terserOptions: {
compress: {
drop_console: true, //打包时删除console
pure_funcs: ['console.log'],
drop_debugger: true, //打包时删除debugger
},
output: {
comments: true, // 打包时删除注释
},
},
})
拆分打包
Vite官方文档:build-rollupoptions
在webpack中,拆分打包(亦或者叫代码分割或者代码拆分)我们一般在optimization.splitChunks中实现
module.exports = {
chainWebpack (config) {
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
config.optimization.runtimeChunk('single')
}
}
改为vite后,通过打包出来的文件我们可以看到
运行时,
个人觉得,已经很不错了,如果你还是想在vite中自己定义规则做拆分,你可以通过 rollupOptions.manualChunks实现
export default defineConfig({
build: {
rollupOptions: {
manualChunks(id) {
if (id.includes('node_modules') ) {
if(id.includes('node_modules/element-ui')){
return 'chunk-elementUI';
}else{
return 'chunk-libs';
}
}
if(id.includes('src/components')){
return 'chunk-components';
}
}
}
}
})
最后
删除跟webpack相关的无用文件
删除build文件夹及其里面的文件index.js;
删除public文件夹(public/index.html需要迁移到根目录,前面已提及)
删除vue.config.js文件
......
总结
vue2项目从webpack 迁移到 vite, 需要完成以下几步:
- 创建vite.config.js
- 安装Vite依赖(vite@4.5.3、plugin-vue2、sass...),升级vue@2.7.14
- 基本配置(开发服务器、构建选项、plugins、resolve、define、环境变量、index.html、package.json等)
- 其他webpack变量修改(import.meta.env、jsx问题、require、require.context)
- 文件报错问题(1、替换CommonJS;2、sass相关问题,比如不支持node-sass、/deep/、~,?inline,sass变量语法报错...;3、导入多个文件,使用import.meta.glob;4、path-browserify)
- 打包vite build
- 删除无用的文件和依赖包
附: vite.config.js
/*
* @Descripttion:
* @Author: wang pingqi
* @Date: 2024-06-13 11:41:00
* @LastEditors: wang pingqi
* @LastEditTime: 2024-06-26 16:58:12
*/
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue2'
import path from 'path'
// import ViteRequireContext from '@originjs/vite-plugin-require-context'
// import vueJsx from '@vitejs/plugin-vue2-jsx'
// 打印git日志
import GitRevisionPlugin from 'git-revision-webpack-plugin'
const gitRevisionPlugin = new GitRevisionPlugin({
versionCommand: 'describe --tags --always --dirty="-dev"',
commithashCommand: 'log --max-count=1 --no-merges --pretty="%ai|%s"'
})
// function transformScss() {
// return {
// name: 'vite-plugin-transform-scss',
// enforce: 'pre',
// transform(src, id) {
// if (
// /\.(js|ts|tsx|vue|scss)(\?)*/.test(id) &&
// !id.includes('node_modules')
// ) {
// return {
// code: src.replace(/\/deep\//gi, '::v-deep'),
// };
// }
// },
// };
// }
export default defineConfig({
// 开发或生产环境服务的公共基础路径,用于指定项目中静态资源的基本路径
base: './',
// 构建选项
build: {
// 指定输出路径(相对于 项目根目录).
outDir: "dist",
// 指定生成静态资源的存放路径(相对于 build.outDir)。在 库模式 下不能使用。
assetsDir: 'static',
// 构建后是否生成 source map 文件
sourcemap: false,
minify:'terser',
terserOptions: {
compress: {
drop_console: true, //打包时删除console
pure_funcs: ['console.log'],
drop_debugger: true, //打包时删除debugger
},
output: {
comments: true, // 打包时删除注释
},
},
rollupOptions: {
manualChunks(id) {
if (id.includes('node_modules') ) {
if(id.includes('node_modules/element-ui')){
return 'chunk-elementUI';
}else{
return 'chunk-libs';
}
}
if(id.includes('src/components')){
return 'chunk-components';
}
}
}
},
server: {
open: true
},
resolve: {
// // 别名
// alias: {
// "@": path.resolve(__dirname, "src"), // path.resolve(__dirname, './src')
// // "components": path.resolve(__dirname, "src/components"),
// // "styles": path.resolve(__dirname, "src/styles"),
// // "views": path.resolve(__dirname, "src/views"),
// // "utils": path.resolve(__dirname, "src/utils")
// },
alias: [
{
find: /^~/,
replacement: ''
},
{
find: '@',
replacement: path.resolve(__dirname, 'src')
}
],
// 导入时想要省略的扩展名列表,默认值['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']
// 官方文档中说:不建议忽略自定义导入类型的扩展名(例如:.vue),因为它会影响 IDE 和类型支持
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
plugins: [
Vue(),
// ViteRequireContext(), // 亲测有效,兼容require.context
// transformScss(), // 亲测有效,但是不推荐,建议全局替换deep
// vueJsx()
],
define: {
'process.VERSION': JSON.stringify(gitRevisionPlugin.version()),
'process.COMMIT': JSON.stringify(gitRevisionPlugin.commithash())
}
})