基于webpack的Vue2项目改为vite闭坑实践

3,538 阅读22分钟

前些年开发的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的很多特性,用起来别提有多香。

image.png

image.png

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,可能会遇到这样的报错

image.png 根据上面提示,我们知道了@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中各自的名称,如下表:

webpackvite说明
publicPathbase开发或生产环境服务的公共基础路径,用于指定项目中静态资源的基本路径
outputDirbuild.outDir指定输出路径(相对于 项目根目录).
assetsDirbuild.assetsDir指定生成静态资源的存放路径(相对于 build.outDir)。在 库模式 下不能使用。
productionSourceMapbuild.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

image.png

怎么会引入文件出错了呢?

原来啊,

在Vue CLI项目中,默认配置已经包含了对.vue文件的支持,并且在导入时自动省略文件扩展名。 但在vite中,并不推荐这种方式,默认需要引入时添加上后缀.vue。

我们可在文档中看到,如下截图:

image.png

虽然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 项目中
# 不需要配置 ENVNODE_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配置文件中启用的压缩优化选项:

  1. 树摇 (Tree-shaking): Vite 通过 Rollup 的静态分析能够自动移除未使用的代码。
  2. 代码分割 (Code splitting): Vite 默认支持按需加载,只有当某个模块被实际请求时,才会加载该模块的代码。
  3. 压缩 JS 和 CSS: Vite 使用 Rollup 插件来压缩你的 JavaScript 和 CSS 文件。
  4. 图片优化: 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 等不支持的问题,还有很多问题需要修改后才能将本地服务器跑起来 】

我们先在本地运行一下项目,瞧瞧

如果你见到服务器能跑起来,那先恭喜你,已经小半只脚踏入了成功的门槛了。

image.png

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

image.png

别慌,

SyntaxError: Unexpected token '??=' , 这个错误表明你的代码中出现了一个不被期望的标记 ??= ,

我在项目中全局搜索,也没发现 '??=',又搜索?.,倒是在项目中搜到了不少,关键这里报错并没有指向我们自己写的代码,你喵的什么情况?

我查了好些资料,别人也没有提到这种情况啊,怎么办???

后来突然灵光一现,以前不是经常遇到因为版本问题引起过这样的问题么?是不是node版本导致的呢?查看node

image.png

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

image.png

怎么vite版本都5.3了,原来用的好像是4点几的版本吧,莫不是它在作怪的吧。立马从新卸载了vite@5.3.0,从新下载了vite@ 4.5.3

npm uninstall vite 
npm install vite@4.5.3 --save-dev

喵的,这个问题没有了,又出来个问题

image.png

百度搜索 The package "@esbuild/win32-x64" could not be found, and is needed by esbuild.

别人给了个解决方案:

在运行dev之前先运行node node_modules/esbuild/install.js命令来解决esbuild安装问题;如下:

image.png

到此,总算见到服务器跑起来了。

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

image.png

好吧,别着急,我们接下来还得一点点解决

替换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了。

继续运行本地服务器

image.png

喵的好几个报错,怎么办,先从简单的开始解决

先瞧瞧这个

[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(), 
    ] 
})

再看另一个问题

~ 符号不支持

image.png

还有出现这样的错误提示的

[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.

image.png

这样的报错,在项目中有很多,且大多数都指向引入的element-ui,使我无限懵逼,后来我才想到,应该是在新安装的Sass版本

"sass": "^1.77.5",

不支持此语法了,为了不改动原有的代码和修改element-ui 版本,我将sass版本降级到了

"sass": "^1.32.12",

接下来还得继续,报错

image.png

根据错误提示,目前加载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 两个功能下有涉及,如下图

image.png

相应的如何修改并让原本的功能正常运行,我已经在另外两篇文章中做了详细阐述:

如何在本地Vue项目中批量注册全局组件

如何自动引入Vuex中分割的Module

在这里我就不再做过多阐述了。

第二种

使用插件兼容:@originjs/vite-plugin-require-context

处理办法:

juejin.cn/post/728295…

juejin.cn/post/707113…

安装 @originjs/vite-plugin-require-context 支持 require.context

www.npmjs.com/package/@or…

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) 
}

改完调试,还能看到下面的问题

image.png

上面提示在这几个地方引入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'

image.png

改好后,我们继续看

image.png

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

image.png

定位到出错文件的代码:

<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的

image.png

,而针对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>

到此,我们项目已经能正常跑起来了,本地服务器后台也没有了报错,我也正常运行项目,登录成功跳转到首页,随便点了些页面,发现都能正常跑起来。 可是,别以为这样就结束了,如果我们要将改写好的代码用到实际中,最后上到生产的话,我们还是得每个页面每个功能,都得重新回归测试的。

譬如我修改的这个项目,就在我以为没啥大问题的时候,无意中点到了几个页面,就发现页面一片空白,诡异的是浏览器里面没有任何报错,本地服务器后台同样如此。可偏偏页面就是空白了,我又重新点击了好些个页面,发现一些页面同样如此,这样就把我搞蒙圈了,没任何报错,就是空白,什么情况这是?

我又仔细观察了空白页面跟正常运行页面之间的差别,终于被我发现,原来,空白的页面跟正常页面文件结构不同,比如项目中景区管理文件夹

image.png

我们配置的路由路径,前两个到文件夹名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
  }

如下图箭头指向

image.png

为了能清楚的说明上面代码发生了什么,我以场馆管理页面为例,在生成路由的地方添加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

如下图

image.png

image.png

在vite中

路由地址为:/applets/scenic/venue

对应的vue文件路径:/src/views/applets/scenic/venue.vue,

可以看出,在vite中,只能去找 【/src/views/applets/scenic/venue.vue】,由于这个文件不存在,故出现了空白。

如下图

image.png

或许有人会说了,你的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文件都导入生成了一个对象

我把它打印出来,如下图

image.png

这个对象里,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 ,打包过程出现了如下几个报错:

问题一:

image.png

根据报错提示,定位到报错的vue文件,发现模板

<template lang="html"></template>

template 上多了个属性lang,其实vue文件模板默认就是以html来解析的,这种写法不推荐使用。 不晓得是原来哪个同事写的,在webpack中并不会出错,结果现在迁到vite就报错了,

无须多做考虑,本就是默认值,直接删除lang="html"即可,

问题二:

image.png

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

image.png

建议将 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: falsecompress: {  
          } 
        })
      ]
    }
  },
}

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后,通过打包出来的文件我们可以看到

1720668085320.png 运行时, 1720668460862.png

个人觉得,已经很不错了,如果你还是想在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())
  }
})