2. Vite插件配置分离到单文件中

866 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

1. 配置分离的原因

为什么要将配置分离呢?其实分离的主要是插件的配置,因为项目中会用到越来越多的插件,如果全部放在一个对象字面量中去配置的话,以后插件多了管理起来会很混乱。


2. 预备知识

2.1 vite.config.tsdefineConfig导出的可以是一个函数

export declare function defineConfig(config: UserConfigExport): UserConfigExport;

// 主要看 UserConfigExport 的 UserConfigFn 类型
export declare type UserConfigExport = UserConfig | Promise<UserConfig> | UserConfigFn;

// 接收一个配置参数,并将原本导出的对象在这个函数中返回
export declare type UserConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>;

同时利用ES6的对象解构特性,我们可以将原本的defineConfig导出对象的方式改成函数的方式,这样我们能做更多的操作

// vite.config.ts

// 以 UserConfigFn 的方式导出配置
export default defineConfig(({ mode, command }: ConfigEnv): UserConfig => {
  const rootPath = process.cwd();
  // 第三个参数是可选的,默认只会加载 VITE 前缀的环境变量,可以传入自定义想要的前缀
  const env = loadEnv(mode, rootPath, ['VITE', 'TEST']);
  console.log(env);
  /** env
    {
      VITE_PUBLIC_PATH: '/',
      VITE_PORT: '3100',
      VITE_GLOB_APP_TITLE: 'Plasticine Admin',
      VITE_GLOB_APP_SHORT_NAME: 'vue_plasticine_admin',
      TEST_AAA: '123'
    }
   */

  return {
    // 导出的 vite 配置项对象
  };
});

2.2 将env的值转成正确类型

得到的env中,值全都是字符串类型,为了方便在配置中使用,我们可以写一个函数来处理env对象,目的是将值转成正确的类型,比如'3100' ==> 3100'true' ==> true,这个函数命名为wrapperEnv


2.3 抽象出公共配置项

env的类型是Record<string, string> 也就是说env中的所有属性都是string类型的,而我们需要将其转成number | string | boolean这样一个联合类型,并且我们希望将配置文件中(无论是.env.env.development还是.env.production)中共有的配置项整合到一起,因此可以定义一个ViteEnv接口,里面存放公共的配置项作为属性,由于该接口会在整个项目中多处用到,因此我们需要将它放到项目的类型声明文件中

types/global.d.ts

declare global {
  interface ViteEnv {
    VITE_PORT: number; // 端口号
    VITE_USE_MOCK: boolean; // 是否开启mock数据,关闭时需要自行对接后台接口
    VITE_USE_PWA: boolean; // 打包是否开启pwa功能
    VITE_PUBLIC_PATH: string; // 资源公共路径,需要以 / 开头和结尾
    VITE_PROXY: [string, string][]; // 本地开发代理,可以解决跨域及多地址代理,可以有多个,注意多个不能换行,否则代理将会失效
    VITE_GLOB_APP_TITLE: string; // 网站标题
    VITE_GLOB_APP_SHORT_NAME: string; // 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符
    VITE_USE_CDN: boolean;
    VITE_DROP_CONSOLE: boolean; // 是否删除Console.log
    VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none'; // 打包是否输出gz|br文件,也可以有多个, 例如 ‘gzip’|'brotli',这样会同时生成 .gz和.br文件
    VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
    VITE_LEGACY: boolean; // 是否兼容旧版浏览器。开启后打包时间会慢一倍左右。会多打出旧浏览器兼容包,且会根据浏览器兼容性自动使用相应的版本
    VITE_USE_IMAGEMIN: boolean; // 打包是否压缩图片
    VITE_GENERATE_UI: string;
  }
}

export {};

2.4 env的类型

env是一个Record类型,我们可以定义一个Recordable类型,表明一个变量是Record

types/global.d.ts

declare global {
  type Recordable<T = any> = Record<string, T>;
}

2.5 wrapperEnv函数

该函数就是将loadEnv函数读出来的配置项的值转成对应类型的值,因为loadEnv的值全是字符串类型的,使用的时候总是要手动转换就不太方便,wrapperEnv遍历loadEnv返回值的每一个key,遇到booleannumberVITE_PROXY(数组),则转成对应的js的类型,而不是以字符串的方式存在,并且还会将配置项挂载到process.env环境变量中

// build/utils.ts

/**
 * 将 loadEnv 加载的配置对象转成类型正确的配置对象,并将它们挂载到 process.env 中
 * @param envConf loadEnv 加载的配置
 * @returns 类型正确的配置对象
 */
export function wrapperEnv(envConf: Recordable): ViteEnv {
  const res: any = {};

  // 1. 遍历所有的配置项的 key
  for (const envName of Object.keys(envConf)) {
    let realName = envConf[envName];

    // 2. 进行一些类型转换

    // 将 boolean 字符串转成 boolean 类型
    realName = realName === 'true' ? true : realName === 'false' ? false : realName;

    // VUTE_PORT 是 number 类型
    if (envName === 'VITE_PORT') realName = Number(realName);

    // VITE_PROXY 可能是单个字符串也可能是数组
    if (envName === 'VITE_PROXY' && realName) {
      try {
        // JSON 中没有单引号,为了防止出错需要将单引号替换成双引号
        realName = JSON.parse(realName.replace(/\'/, '"'));
      } catch (error) {
        realName = '';
      }
    }

    // 3. 将结果拷贝到新对象中
    res[envName] = realName;

    // 4. 将配置项放到 process.env 中,值只能是字符串,因此遇到对象要进行序列化
    if (typeof realName === 'string') process.env[envName] = realName;
    else if (typeof realName === 'object') process.env[envName] = JSON.stringify(realName);
    else process.env[envName] = realName.toString();
  }

  return res;
}

3. 基本配置

3.1 base -- publicPath

开发或生产环境服务的公共基础路径,从配置文件中读取

base: VITE_PUBLIC_PATH,

3.2 构建配置

build: {
  target: 'es2015',
  outDir: OUTPUT_DIR,
  terserOptions: {
    compress: {
      // Pass true to prevent Infinity from being compressed into 1/0, which may cause performance issues on Chrome.
      keep_infinity: true,
      drop_console: VITE_DROP_CONSOLE,
    },
  },
  // 启用/禁用 gzip 压缩大小报告。压缩大型输出文件可能会很慢,因此禁用该功能可能会提高大型项目的构建性能
  reportCompressedSize: false,
  chunkSizeWarningLimit: 2000,
},

3.3 define全局常量

// 全局常量替换
define: {
  __INFINITY_PROD_DEVTOOLS__: false,
  __APP_INFO__: JSON.stringify(__APP_INFO__),
},

3.4 css预处理器配置

css: {
  preprocessorOptions: {
    less: {
      modifyVars: generateModifyVars(),
      javascriptEnabled: true,
    },
  },
},

generateModifyVars函数的作用可以回顾第一篇文章


4. 插件配置分离

这才是本篇文章的重头戏!!!vue-vben-admin源码中也说了:

The vite plugin used by the project. The quantity is large, so it is separately extracted and managed

可见将插件的配置分离真的很重要,也方便我们后期修改插件的配置,接下来就看看具体如何操作吧!

4.1 插件配置出口

新建build/vite/plugin/index.ts作为所有插件配置的出口,实现一个createVitePlugins函数,所有的插件都在这里面导出

import { PluginOption } from 'vite';

import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from '@vitejs/plugin-legacy';

import WindiCSS from 'vite-plugin-windicss';
import { configHtmlPlugin } from './html';

export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
  const { VITE_LEGACY } = viteEnv;

  const vitePlugins: (PluginOption | PluginOption[])[] = [vue(), vueJsx()];

  // vite-plugin-windicss
  vitePlugins.push(WindiCSS());

  // @vitejs/plugin-legacy -- 开启了 VITE_LEGACY 且是在构建模式下才加载 legacy 插件
  VITE_LEGACY && isBuild && vitePlugins.push(legacy());

  // vite-plugin-html
  vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));

  return vitePlugins;
}

就是维护一个vitePlugins数组,为什么它的类型是(PluginOption | PluginOption[])[]呢?很简单,因为vite.config.ts中的plugins选项的类型就是这个,看源码就可以知道:

/**
 * Array of vite plugins to use.
 */
plugins?: (PluginOption | PluginOption[])[];

然后根据viteEnv这个前面从配置文件中加载出来的环境变量对象来进行个性化的配置,以及根据isBuild来对生产环境和开发环境进行不同的插件配置。


4.2 新增个性化插件

对于普通的插件,比如@vitejs/plugin-vue这样的不需要配置的插件,我们可以直接push到vitePlugins数组中。

但是现在假设要加入一个插件,它是需要个性化配置的,该怎么办呢?以前面的vite-plugin-html为例

创建build/vite/plugin/html.ts

import { GLOB_CONFIG_FILE_NAME } from 'build/constant';
import { createHtmlPlugin } from 'vite-plugin-html';

import pkg from '../../../package.json';

export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
  const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;

  const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`;

  const getAppConfigSrc = () => {
    return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${Date.now()}`;
  };

  return createHtmlPlugin({
    minify: true,
    inject: {
      data: {
        title: VITE_GLOB_APP_TITLE,
      },
      tags: isBuild
        ? [
            {
              tag: 'script',
              attrs: {
                src: getAppConfigSrc(),
              },
            },
          ]
        : [],
    },
  });
}

用一个函数configHtmlPlugin去接收viteEnvisBuild参数,然后就可以根据不同的配置和模式进行个性化操作了,之后新增别的插件也是类似的。


4.3 调用出口函数

最后就是在vite.config.tsplugins中调用createVitePlugins函数,得到插件数组即可。

plugins: createVitePlugins(viteEnv, isBuild)