记录一次webpack项目改造vite

198 阅读4分钟

改造动机

公司的项目是基于若伊二开的,当初考虑到vue3还不是很稳定,所以上的vue2版本。这个版本是用webpack构建打包的,vite的开发体验效果很好,尤其是代码更新后的秒开,真是爽到爆炸,对于我们公司的这种中小型项目来说简直太适合了。于是说干就干,进行vite改造。

准备

  • Node.js 18 / 20+
  • Yarn (强烈推荐使用yarn下包,体验很爽)
  • vue2+webpack项目

开始改造

  1. 新建工程
  • npm init
  • 下载需要的包

这里注意:vue低于2.7.0的版本需要升级到2.7.0

yarn add vite -D
yarn add vue@2.7.0 -S
  1. vite配置文件
  • 新建vite.config.js文件
import { defineConfig, loadEnv } from "vite";
import path from "path";
import createVitePlugins from "./vite/plugins";

export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd());
  const { VITE_APP_ENV } = env;
  return {
    base: VITE_APP_ENV === "production" ? "/" : "/", // 设置项目根目录
    plugins: createVitePlugins(env, command === "build"), // vite插件
    resolve: {
      // 配置别名
      alias: {
        "~": path.resolve(__dirname, "./"),
        "@": path.resolve(__dirname, "./src"),
      },
      // 配置文件扩展名
      extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
    },

    server: {
      port: 90, // 设置端口号
      host: true, // 设置为true允许外部访问
      open: true, // 设置为true自动打开浏览器
    },

    build: {
      outDir: VITE_APP_ENV === "production" ? "dist" : "dist_test", // 打包输出目录
      sourcemap: false, // 生成sourceMap文件
      commonjsOptions: {
        transformMixedEsModules: true, // 解决node_modules中es6模块打包问题
      },
      rollupOptions: {
        output: {
          chunkFileNames: "js/[name]-[hash].js", // 引入文件名的名称
          entryFileNames: "js/[name]-[hash].js", // 包的入口文件名称
          assetFileNames: "[ext]/[name]-[hash].[ext]", // 资源文件像 字体,图片等
          manualChunks(id) {
            if (id.includes("node_modules")) {
              // 让每个插件都打包成独立的文件
              return id
                .toString()
                .split("node_modules/")[1]
                .split("/")[0]
                .toString();
            }

            // return "vendor"; // 将所有代码打包到一个文件中,减少http请求
          },
        },
      },

      terserOptions: {
        compress: {
          drop_console: true, // 生产环境自动去除console
          drop_debugger: true, // 生产环境自动去除debugger
        },
      },
    },
  };
});
  • vite插件配置,这里单独开一个文件夹:vite/plugins文件夹,插件在这里面统一管理
import vue from "@vitejs/plugin-vue2";
import vueJsx from "@vitejs/plugin-vue2-jsx";
import { viteCommonjs } from "@originjs/vite-plugin-commonjs"; // 让浏览器支持commonjs语法
import { visualizer } from "rollup-plugin-visualizer"; // 打包分析
import imagemin from "./imagemin";

import createSvgIcon from "./svg-icon";
import createCompression from "./compression";

export default function createVitePlugins(viteEnv, isBuild = false) {
  const vitePlugins = [vue(), vueJsx()];
  vitePlugins.push(createSvgIcon(isBuild));
  vitePlugins.push(viteCommonjs());

  if (isBuild) {
    vitePlugins.push(...createCompression(viteEnv));
    vitePlugins.push(visualizer({ open: true }));
    vitePlugins.push(imagemin());
  }
  return vitePlugins;
}

import compression from "vite-plugin-compression";

export default function createCompression(env) {
  const { VITE_BUILD_COMPRESS } = env;
  const plugin = [];
  if (VITE_BUILD_COMPRESS) {
    const compressList = VITE_BUILD_COMPRESS.split(",");
    if (compressList.includes("gzip")) {
      plugin.push(
        compression({
          ext: ".gz", // 压缩文件后缀
          deleteOriginFile: false, // 压缩后是否删除原文件
        })
      );
    }
    if (compressList.includes("brotli")) {
      plugin.push(
        compression({
          ext: ".br", // 压缩文件后缀
          algorithm: "brotliCompress", // 使用br压缩
          deleteOriginFile: false, // 压缩后是否删除原文件
        })
      );
    }
  }
  return plugin;
}

import viteImagemin from "vite-plugin-imagemin";

export default function imagemin(isBuild) {
  return viteImagemin({
    disable: !isBuild, // 生产环境才启用
    gifsicle: {
      // 压缩gif图片
      optimizationLevel: 7, // 优化等级
      interlaced: false, // 是否隔行扫描
    },
    optipng: {
      // optipng 图片压缩
      optimizationLevel: 7, // 优化等级
    },
    mozjpeg: {
      // 压缩jpg图片
      quality: 20, // 压缩质量
    },
    pngquant: {
      // 压缩png图片
      quality: [0.8, 0.9], // 压缩质量
      speed: 4, // 压缩速度
    },
    svgo: {
      // 压缩svg图片
      plugins: [
        {
          name: "removeViewBox", // 移除viewbox
        },
        {
          name: "removeEmptyAttrs", // 移除空属性
          active: false, // 默认不启用
        },
      ],
    },
  });
}

import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import path from "path";

export default function createSvgIcon() {
  return createSvgIconsPlugin({
    iconDirs: [path.resolve(process.cwd(), "src/assets/icons/svg")],
    symbolId: "icon-[dir]-[name]",
    svgoOptions: true,
  });
}


  1. 环境配置
  • 分环境配置,这里一共配置三个环境,开发(development)、测试(release)、生产(product)
//package.json
{
  "name": "webpack2vite",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build --mode production",
    "build:release": "vite build --mode release"
  },
  "dependencies": {
    "vue": "^2.7.0",
  },
  "devDependencies": {
    "vite": "^5.4.1",
  },
}

  • 根目录下添加环境配置文件
# 页面标题
VITE_APP_TITLE = webpack2vite

# 开发环境配置
VITE_APP_ENV = 'development'

# webpack2vite /开发环境
VITE_APP_BASE_API = 'https://****.***.com'

# 页面标题
VITE_APP_TITLE = webpack2vite

# 开发环境配置
VITE_APP_ENV = 'release'

# webpack2vite /测试环境
VITE_APP_BASE_API = 'https://****.***.com'

# 页面标题
VITE_APP_TITLE = webpack2vite

# 开发环境配置
VITE_APP_ENV = 'product'

# webpack2vite /生产环境
VITE_APP_BASE_API = 'https://****.***.com'

  • 插件分析

支持vue2

import vue from "@vitejs/plugin-vue2";

export default function createVitePlugins(viteEnv, isBuild = false) {
  const vitePlugins = [vue()];
  return vitePlugins;
}

  • 支持jsx

如果项目中有用到jsx的话,项目项目文件需要设置lang='jsx',并且引入支持jsx的插件,否则会报错

<script lang="jsx">
export default {
  name: "MenuItem",
  render(h, context) {
    const { icon, title } = context.props;
    const vnodes = [];

    vnodes.push(<svg-icon icon-class={icon} />);
    return vnodes;
  },
};
</script>
import vue from "@vitejs/plugin-vue2";
import vueJsx from "@vitejs/plugin-vue2-jsx";

export default function createVitePlugins(viteEnv, isBuild = false) {
  const vitePlugins = [vue(),vueJsx()];
  return vitePlugins;
}

  • 项目使用svg图标,批量引入svg

vite的svg图标引入插件配置,vite中不支持require.context('./svg', false, /.svg$/)批量引入图标的方式

import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import path from "path";

export default function createSvgIcon() {
  return createSvgIconsPlugin({
    iconDirs: [path.resolve(process.cwd(), "src/assets/icons/svg")],
    symbolId: "icon-[dir]-[name]",
    svgoOptions: true,
  });
}

svg图标组件

<template>
  <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>
export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`
    },
    svgClass() {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
    styleExternalIcon() {
      return {
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
      }
    }
  }
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover!important;
  display: inline-block;
}
</style>

全局挂载使用svg

import SvgIcon from "@/components/SvgIcon"; // svg component
Vue.component("svg-icon", SvgIcon);

打包压缩插件配置

deleteOriginFile: false, 该配置请务必设置为false,否则打包压缩后会删除原文件,只剩下gz后缀文件,上线后会报错!

import compression from "vite-plugin-compression";

export default function createCompression(env) {
  const { VITE_BUILD_COMPRESS } = env;
  const plugin = [];
  if (VITE_BUILD_COMPRESS) {
    const compressList = VITE_BUILD_COMPRESS.split(",");
    if (compressList.includes("gzip")) {
      plugin.push(
        compression({
          ext: ".gz", // 压缩文件后缀
          deleteOriginFile: false, // 压缩后是否删除原文件
        })
      );
    }
    if (compressList.includes("brotli")) {
      plugin.push(
        compression({
          ext: ".br", // 压缩文件后缀
          algorithm: "brotliCompress", // 使用br压缩
          deleteOriginFile: false, // 压缩后是否删除原文件
        })
      );
    }
  }
  return plugin;
}

图片压缩插件

import viteImagemin from "vite-plugin-imagemin";

export default function imagemin(isBuild) {
  return viteImagemin({
    disable: !isBuild, // 生产环境才启用
    gifsicle: {
      // 压缩gif图片
      optimizationLevel: 7, // 优化等级
      interlaced: false, // 是否隔行扫描
    },
    optipng: {
      // optipng 图片压缩
      optimizationLevel: 7, // 优化等级
    },
    mozjpeg: {
      // 压缩jpg图片
      quality: 20, // 压缩质量
    },
    pngquant: {
      // 压缩png图片
      quality: [0.8, 0.9], // 压缩质量
      speed: 4, // 压缩速度
    },
    svgo: {
      // 压缩svg图片
      plugins: [
        {
          name: "removeViewBox", // 移除viewbox
        },
        {
          name: "removeEmptyAttrs", // 移除空属性
          active: false, // 默认不启用
        },
      ],
    },
  });
}

打包分析插件

打包完成会弹出包分析页面

import vue from "@vitejs/plugin-vue2";
import { visualizer } from "rollup-plugin-visualizer"; // 打包分析
export default function createVitePlugins(viteEnv, isBuild = false) {
  const vitePlugins = [vue(), vueJsx()];
  if (isBuild) {
    vitePlugins.push(visualizer({ open: true })); //open开启关闭分析
  }
  return vitePlugins;
}

  • 文件路径别名配置
import { defineConfig, loadEnv } from "vite";
import path from "path";
export default defineConfig(({ mode, command }) => {
    return {
        base: "/", // 设置项目根目录
        plugins:[], //插件
        resolve:{  //配置路径别名
            alias: {
            "~": path.resolve(__dirname, "./"),
            "@": path.resolve(__dirname, "./src"),
            },
            // 配置文件扩展名,没有的话,会报错Could not resolve "./App"
            extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
        },
        server:{ //服务设置
        
        },
        build:{ //打包构建设置
            
        }
    }
});
  • sass样式文件报错处理

1、element-variables.scss文件会报引入错误:

$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";

改为

$--font-path: "element-ui/lib/theme-chalk/fonts";
@import "element-ui/packages/theme-chalk/src/index";

2、sass变量导出文件报错

$base-menu-color:#bfcbd9;
:export {
  menuColor: $base-menu-color;
}

改为

$base-menu-color:#bfcbd9;
:export {
  menuColor: $base-menu-color;
}

  • baseapi动态获取

process.env.VUE_APP_BASE_API调整为import.meta.env.VITE_APP_BASE_API

  • 图片引入

require("../../../assets/logo/logo-xy.png")调整为new URL("../../../assets/logo/logo-xy.png", import.meta.url).href

  • 动态路由,批量引入页面引入页面配置
// 匹配views里面所有的.vue文件
const modules = import.meta.glob("@/views/**/*.vue");

export default [
  {
    path: "",
    component: Layout,
    hidden: true,
    redirect: "index",
    children: [
      {
        path: "index",
        component: loadView('/text/index'),
        name: "index",
        meta: { title: 首页, icon: "dashboard", affix: true },
      },
    ],
  },
]

const loadView = (view) => {
  let res;
  for (const path in modules) {
    let dir = "";
    dir = path.split("views/")[1].split(".vue")[0];
    if (dir === view || `${view}/index` === dir) {
      res = () => modules[path]();
    }
  }
  return res;
};

结语

以上vue2配置vite打包,就全部完成了,现在可以愉快的体验vite开发的迅捷

附上demo地址

github.com/sunwang1991…