Vue CLI 项目迁移到 Vite(Vue3)

1,367 阅读3分钟

Vite 是一种新的前端构建工具,能够显著提升前端开发体验,关于 Vite 更多的功能请前往官网

对于 Vite 使用,如果是新项目,则可使用命令 pnpm create vite 创建;如果是使用 Vue CLI 工具创建的项目,则需要在其基础上进行改造。

本文记录 Vue CLI 项目迁移到 Vite 所遇到的坑。

迁移步骤

移除 Vue CLI

把与 Vue CLI 相关的依赖及配置进行移除,即 package.json 文件中 scripts

原先 scripts 如下:

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  }
}

改动 scripts,如下:

{
  "scripts": {
    "serve": "vite",
    "build": "vite build",
    "lint": "vite lint",
  }
}

修改入口文件

Vue CLI 入口默认是 src/main.js,而 Vite 的入口文件是 index.html,借用官方对 index.html 的解释:

Vite 将 index.html 视为源码和模块图的一部分。Vite 解析 <script type="module" src="..."> ,这个标签指向你的 JavaScript 源码。甚至内联引入 JavaScript 的 <script type="module"> 和引用 CSS 的 <link href> 也能利用 Vite 特有的功能被解析。另外,index.html 中的 URL 将被自动转换,因此不再需要 %PUBLIC_URL% 占位符了。

首先,需要将 public/index.html 移动到根目录,然后对其进行修改,即添加一行代码 <script type="module" src="/src/main.js"></script> 具体如下:

<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width,initial-scale=1.0">
      <title><%- title %></title>
    </head>

    <body>
        <noscript>
            <strong>We're sorry but <%- title %> doesn't work properly without JavaScript enabled.
        Please enable it to continue.</strong>
      </noscript>
      <div id="app"></div>

      <!-- Vite将自动解析下面的js文件 -->
      <script type="module" src="/src/main.js"></script>
    </body>
</html>

环境变量不同

Vue CLI 和 Vite 的环境变量加载都是通过 dotenv 来实现,因此在命名约定上是一致的。但是它们之间有两点不同:

暴露方式

  • Vue CLI 对外暴露的变量只有 NODE_ENVBASE_URLVUE_APP_ 开头的变量
  • Vite 对外暴露的变量:MODEBASE_URLPRODDEV 和以 VITE_ 开头的变量

访问方式

  • Vue CLI 通过 process.env 对变量进行访问
  • Vite 通过 import.meta.env 对变量进行访问

因此,在迁移项目,需要对变量进行替换,

  • 将以 VUE_APP_ 开头的变量替换为以 VITE_ 开头的变量
  • process.env 替换为 import.meta.env

不能忽略自定义导入类型扩展名

使用 Vue CLI 时,在引入文件(如:.vue)时,可不用写后缀名,IDE 可自动识别,比如

import Test from '@views/Test'

但是在 Vite 中,需要加上后缀名,否则构建时会报错。

引入 Vite

安装 Vite 依赖

pnpm add -D vite

// or
npm install -D vite

为了让 Vite 能解析 Vue,需要安装相应的插件,官方也提供插件支持,具体如下:

pnpm add -D @vitejs/plugin-vue  // Vue3

pnpm add -D vite-plugin-vue2  // Vue2

创建配置文件 vite.config.js

在根目录创建配置文件 vite.config.js,Vite 会自动加载配置文件,执行构建工作,配置具体如下:

import { fileURLToPath, URL } from 'url';

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  base: 'xxxx'
});

这是最基础的配置,根据项目需求可在此配置上进行扩展。

扩展配置 vite.config.js

proxy

在请求服务端接口时,我们都需要做一层代理来实现转发请求。在 Vue CLI 通过 proxy 配置,对于 Vite,也是通过 proxy 来转发的,具体配置如下:

import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
  server: {
    host: 'xxx.xxx.xxxx',
    proxy: {
      '^/api': {
        target: 'xxxx',
        changeOrigin: true,
    },
    }
  }
});

JSX

Vite 默认不支持 JSX,如果要支持 JSX,需要安装相关依赖及配置,对于 JSX 使用方式有两种:.jsx.vue 文件中使用 JSX 语法,以下介绍的是后者,具体如下:

  • 安装 @vitejs/plugin-vue-jsx
pnpm add -D @vitejs/plugin-vue-jsx
  • 修改 vite.config.js
import { defineConfig } from 'vite';
import vueJsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vueJsx()],
});
  • .vue 使用 JSX 语法的,需要在 script 标签添加标识 jsx,即
<script setup lang="jsx"></script>
  • 对于在 .js 使用到 JSX 语法的,需要将其后缀名改为 jsx

CSS 全局变量

在项目中,我们会定义一些 CSS 变量,将其设置为全局变量,方便直接使用,而无需引入,具体配置如下:

import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          hack: `true; @import (reference) "${path.resolve('src/assets/xxxxx/variable.less')}"`
        }
      }
    }
  }
});

SVG 雪碧图

在 Vue CLI 项目中,使用 webpack 提供的 svg-sprite-loader 插件实现 SVG 雪碧图;而在 Vite 中,也可以通过插件来实现,目前采用的方案是编写插件读取、解析 SVG 文件,根据 symbol 规则重新生成,并将其插入到 body,具体如下:

  • 编写Vite 插件
// 参考链接:https://juejin.cn/post/6932037172178616334

// src/plugins/index.js
import { readFileSync, readdirSync } from 'fs'

let idPerfix = ''
const svgTitle = /<svg([^>+].*?)>/
const clearHeightWidth = /(width|height)="([^>+].*?)"/g

const hasViewBox = /(viewBox="[^>+].*?")/g

const clearReturn = /(\r)|(\n)/g

function findSvgFile(dir) {
    const svgRes = []
    const dirents = readdirSync(dir, {
        withFileTypes: true,
    })
    for (const dirent of dirents) {
        if (dirent.isDirectory()) {
            svgRes.push(...findSvgFile(dir + dirent.name + '/'))
        } else {
            const svg = readFileSync(dir + dirent.name)
                .toString()
                .replace(clearReturn, '')
                .replace(svgTitle, ($1, $2) => {
                    let width = 0
                    let height = 0
                    let content = $2.replace(clearHeightWidth, (s1, s2, s3) => {
                        if (s2 === 'width') {
                            width = s3
                        } else if (s2 === 'height') {
                            height = s3
                        }
                        return ''
                    })
                    if (!hasViewBox.test($2)) {
                        content += `viewBox="0 0 ${width} ${height}"`
                    }
                    return `<symbol id="${idPerfix}-${dirent.name.replace(
                        '.svg',
                        ''
                    )}" ${content}>`
                })
                .replace('</svg>', '</symbol>')
            svgRes.push(svg)
        }
    }
    return svgRes
}

export const svgPlugin = (path, perfix = 'icon') => {
    if (path === '') return
    idPerfix = perfix
    const res = findSvgFile(path)
    return {
        name: 'svg-transform',
        transformIndexHtml(html) {
            return html.replace(
                '<body>',
                `
          <body>
            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">
              ${res.join('')}
            </svg>
        `
            )
        },
    }
}
  • vite.config.js 引入插件
import { defineConfig } from 'vite';

import { svgPlugin } from './src/plugins/svg'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [svgPlugin('./src/assets/xxx/svg/')],
});

vite.config.js 最终配置如下:

import { fileURLToPath, URL } from 'url';

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path'

import { svgPlugin } from './src/plugins/svg'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), vueJsx(), svgPlugin('./src/xxx/svg/')],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  server: {
    host: 'xxx.xxx.xxxx',
    proxy: {
      '^/api': {
        target: '',
        changeOrigin: true,
    },
    }
  },
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          hack: `true; @import (reference) "${path.resolve('src/assets/xxx/variable.less')}"`
        }
      }
    }
  },
  base: 'xxx'
});

报错信息收集

  1. TypeError: Failed to fetch dynamically imported module:

解决方案

参考