Vue2开发环境接入vite

1,512 阅读4分钟

介绍

为什么接入vite?

Vite 支持原生 ESM 模块(依托现代浏览器)+ 文件缓存,显著降低了应用的启动和重新构建时间。 其本质上是一个在开发环境为浏览器按需提供文件的 Web Server。

接入要求

vite运行环境需要node 14.18+ / 16+

为什么只在开发环境接入?

  • node版本升级,某些依赖包可能会存在兼容问题
  • 相比webpack技术比较新,与现有项目结合,可能会产生一些奇怪的问题
  • 直接上生产环境风险比较高,建议先用于本地开发提效

数据对比

vue-clivite
冷启动25s1.5s
热更新4s1s
更改环境变量后重启60s1.5s

接入指引(踩坑记录)

兼容vue2

使用 vite 插件兼容 Vue2.7:@vitejs/plugin-vue2

注意:插件vite-plugin-vue2只针对2.6及以下版本

// vite.config.js
import vue from '@vitejs/plugin-vue2'

export default defineConfig({
  plugins: [
    vue() // 兼容vue2.7
  ],
})

兼容jsx语法

vue2.7 提供的Composition Api 内部使用了jsx的语法,需要使用 vite 插件兼容:@vitejs/plugin-vue2-jsx

// vite.config.js
import vueJsx from '@vitejs/plugin-vue2-jsx'

export default defineConfig({
  plugins: [
    vueJsx() // 兼容jsx语法
  ],
})

兼容process.env语法

与webpack不同,Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里主要解决两个问题:

  1. 兼容webpack和vite的环境变量用法

使用vite插件 vite-plugin-env-compatible ,让vite可以使用webpack中读取环境变量的方式,再配合envPrefix配置,让vite可以读取到VUE_APP_开头的环境变量。

// vite.config.js
import { defineConfig } from 'vite'
import envCompatible from 'vite-plugin-env-compatible'

export default defineConfig({
  plugins: [
    envCompatible() // 兼容process.env获取环境变量
  ],
  envPrefix: ['VUE_APP_']
})
  1. vite.config.js中不能读取到环境变量

业务代码在vite环境中运行,可直接通过import.meta.env获取环境变量。vite.config.js 在 node 环境中运行,是无法直接通过import.meta.env获取环境变量,需要通过vite的 loadEnvprocess.env获取。

// vite.config.js
import { defineConfig, loadEnv } from 'vite'
import { createHtmlPlugin } from "vite-plugin-html";

export default ({ mode }) => {

    function getEnv(key) {
        return loadEnv(mode, process.cwd(),'')[key]
    }
    
    return defineConfig({
        base: getEnv('VUE_APP_BASE_URL'), // 部署应用包时的基本URL,'/'表示静态目录的根目录
        plugins: [
          createHtmlPlugin({  // 该插件用于编译时在index.html中注入title
            inject: {
              data: {
                title: process.env.VUE_APP_SYSTEM === 'MANAGE' ? '管理平台' : '作业平台',
              },
            },
          })
        ],
    })
})

兼容CommonJS模块化

vite 通过拦截原生 ESModule 加载的 http 请求来实现按需编译,预编译机制只会对 node_modules 里面的包通过esbuild 进行打包预编译,业务代码不支持 commonJs 的 requireexportsmodules.exports语法。

使用 vite 插件兼容 commonjs 语法:@originjs/vite-plugin-commonjs

// vite.config.js
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

export default defineConfig({
  plugins: [
    viteCommonjs() // 兼容commonjs
  ],
})

兼容require.context语法

webpack使用 require.context() 来动态查找文件内容,vite不支持。有两种修改方式:

  • reqiure.context()修改为import.meta.globEager()
  • 使用插件兼容:@originjs/vite-plugin-require-context
// vite.config.js
import viteRequireContext from '@originjs/vite-plugin-require-context'

export default defineConfig({
  plugins: [
    viteRequireContext(), // 兼容require.context
  ],
})

兼容sass /deep/语法

wepack中使用的node-sass,深度选择器为/deep/,在sass中不兼容,需要使用自定义插件将 /deep/ 替换为 ::v-deep

其次,在使用scss时如果使用了scss变量,需将变量声明文件添加到css预处理选项中。

// vite.config.js
// ...
function transformScss () {
  return {
    name: 'vite-plugin-transform-scss',
    enforce: 'pre',
    transform(src, id) {
      if (
        /\.(js|ts|tsx|vue)(\?)*/.test(id) &&
        id.includes('lang.scss') &&
        !id.includes('node_modules')
      ) {
        return {
          code: src.replace(/\/deep\//gi, '::v-deep'),
        };
      }
    },
  };
}

export default defineConfig({
  plugins: [
    transformScss(), // 兼容node-sass /deep/写法
  ],
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/assets/styles/scss/var.scss";',
      }
    },
  },
})

无模块默认导出

webpack会对引入的模块进行包装,即使没有导出模块也不会报错,但是在vite下会报错。可使用自定义插件,对没有模块默认导出的文件,增加默认导出

function transformFileExport() {
  return {
    name: 'vite-plugin-transform-file', // 文件未设置默认导出时使用
    transform(src, id) {
      if (
        id.includes('.vite/deps/pdfjs-dist_build_pdf__worker__entry') // 未设置默认导出的文件
      ) {
        const newCode = src.replace('export', 'export default')
        return {
          code: newCode,
        };
      }
    },
  };
}

export default defineConfig({
  plugins: [
    transformFileExport(), // 设置模块默认导出
  ],
})

解决循环引用问题

vite在热更新时,如果检测到循环引用,会直接失败,通过通过自定义插件,拷贝引用循环中的依赖到.vite缓存目录下,替换引用该依赖文件的路径指向.vite目录下,中断循环链。

import fs from 'fs';
import path from 'path';

function transformCircularReference () {
  return {
    name: 'vite-plugin-circular-reference',
    enforce: 'pre',
    buildStart() {
      fs.cpSync(
        path.resolve(__dirname, './src/fileName'),   // 循环引用的起点文件
        path.resolve(__dirname, './node_modules/.vite/fileName'),
        {
          recursive: true,
          force: true,
        },
      );
    },
    transform(src, id) {
      if (//src/fileName/index.ts$/i.test(id)) {
        const code = src.replace(
          /(?<=')./src(?=/)/gi,
          '/node_modules/.vite/fileName/src',
        );
        return {
          code,
        };
      }
    },
  };
}

export default defineConfig({
  plugins: [
    transformCircularReference(), // 解决循环引用问题
  ],
})

解决path报错问题

vite 源码中设定了不允许在客户端代码中访问内置模块代码

Error:Module "path" has been externalized for browser compatibility. Cannot access "path.resolve" in client code.

使用

import path from 'path-browserify'

替换

import path from 'path'

解决@vue/compiler-sfc对vue-property-decorator兼容问题

部分使用 vue-property-decorator 语法的组件可能会因为@vue/compiler-sfc兼容问题编译报错

[vite] Internal Server Error
Cannot overwrite across a split point
at MagicString.overwrite (D:\workspace\project\node_modules\@vue\compiler-sfc\dist\compiler-sfc.js)

使用

<script>
    @Component({})
    class CustomerComponent extends Vue {
    ...
    }
    
    export default CustomerComponent
</script>

替换

<script>
    @Component({})
    export default class CustomerComponent extends Vue {
    ...
    }
</script>

解决node-sass 和 sass 版本问题

webpack环境下对css sass的支持主要依赖于:

  • sass-loader
  • node-sass(作为sass-loader依赖)

因为 sass-loader和 node-sass 版本不匹配可能会导致一些奇怪的bug,社区提供了一些版本搭配:

// package.json
"node-sass": "4.14.1",
"sass-loader": "7.1.0",

node-sass对node环境有明确要求,v4.14.1对应node版本最高为node v14。

vite环境下对css sass的支持主要依赖于:

  • sass(封装了dart-sass)

vite运行环境需要node 14.18+/16+,所以升级node到v16以后,node-sass将无法运行。

如果目前项目里面使用的node-sass,接入vite后需要安装sass。两者会产生node版本冲突,目前的解决方案是:

  1. 在node v14下安装项目依赖
  2. 在node v16下运行vite项目

完整配置

// vite.config.js
import fs from 'fs';
import path from 'path'; // vite.config.js在node环境下运行,这里可以使用path

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue2'
import vueJsx from '@vitejs/plugin-vue2-jsx'
import envCompatible from 'vite-plugin-env-compatible'
import { createHtmlPlugin } from "vite-plugin-html";
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
import viteRequireContext from '@originjs/vite-plugin-require-context'

function resolve (dir) {
  return path.join(__dirname, dir)
}

function transformScss () {
  return {
    name: 'vite-plugin-transform-scss',
    enforce: 'pre',
    transform(src, id) {
      if (
        /\.(js|ts|vue)(\?)*/.test(id) &&
        id.includes('lang.scss') &&
        !id.includes('node_modules')
      ) {
        return {
          code: src.replace(/\/deep\//gi, '::v-deep '),
        };
      }
    },
  };
}

function transformFileExport() {
  return {
    name: 'vite-plugin-transform-file-export', // 文件未设置默认导出时使用
    transform(src, id) {
      if (
        id.includes('.vite/deps/pdfjs-dist_build_pdf__worker__entry') // 未设置默认导出的文件
      ) {
        const newCode = src.replace('export', 'export default')
        return {
          code: newCode,
        };
      }
    },
  };
}

function transformCircularReference () {
  return {
    name: 'vite-plugin-transform-circular-reference',
    enforce: 'pre',
    buildStart() {
      fs.cpSync(
        path.resolve(__dirname, './src/fileName'),   // 循环引用的起点文件
        path.resolve(__dirname, './node_modules/.vite/fileName'),
        {
          recursive: true,
          force: true,
        },
      );
    },
    transform(src, id) {
      if (//src/fileName/index.ts$/i.test(id)) {
        const code = src.replace(
          /(?<=')./src(?=/)/gi,
          '/node_modules/.vite/fileName/src',
        );
        return {
          code,
        };
      }
    },
  };
}

export default ({ mode }) => {

  function getEnv(key) {
    return loadEnv(mode, process.cwd(),'')[key]
  }

  return defineConfig({
    base: getEnv('VUE_APP_BASE_URL'), // 部署应用包时的基本URL,'/'表示静态目录的根目录
    publicDir: 'assets', // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
    plugins: [
      vue(), // 兼容vue2
      vueJsx(), // 兼容jsx语法
      envCompatible(), // 兼容process.env获取环境变量
      viteCommonjs(), // 兼容commonjs
      viteRequireContext(), // 兼容require.context
      transformScss(), // 兼容node-sass /deep/语法
      transformFileExport(), // 设置模块默认导出
      transformCircularReference(), // 解决循环引用问题
      createHtmlPlugin({ // 编译时在index.html中注入title
        inject: {
          data: {
            title: title: process.env.VUE_APP_SYSTEM === 'MANAGE' ? '管理平台' : '作业平台',
          },
        },
      })
    ],
    envPrefix: ['VUE_APP_'],
    resolve: {
      alias: [
        { find: '@', replacement: resolve('src') },
        { find: '@types', replacement: resolve('src/types/ index') },
      ],
      extensions: ['.mjs', '.js', '.ts','.vue', '.jsx', '.tsx', '.json', , '.less'] // 需要忽略的文件后缀
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@/assets/styles/scss/var.scss";',
        }
      },
    },
    server: {
      port: 8080, // 端口号
      hmr: {
        overlay: false
      },
      proxy: {
        // ...
      }
    },
  })
}