如何用ts搭建一个vue3通用项目底座 | 第三篇:vite配置

前言

接下来进入项目搭建的重点部分:Vite配置。
本篇内容可能会有些多,耐心看完会有很多收获。

1、在根目录下新建一个build文件夹

虽然vue从很早就使用vue.config.js或者vite.config.ts来进行打包配置,但在webpick时期,似乎是用build文件夹来进行打包配置的,所以这里也使用build文件夹。
在这里插入图片描述
先看一下需要进行的打包配置,后面我会逐个文件的解释。

2、vite配置

先把vite.config.ts文件放出来,后面再一项一项解析

// vite.config.ts
import { ConfigEnv, loadEnv, UserConfig } from 'vite';
import { resolve } from 'path';

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

import { exclude, include } from './build/optimize';
import { createProxy } from './build/vite/proxy';
import { wrapperEnv } from './build/utils';
import { createVitePlugins } from './build/vite/plugins';
import { OUTPUT_DIR } from './build/constant';
import { generateModifyVars } from './build/generate/generateModifyVars';

function pathResolve(dir: string) {
  return resolve(process.cwd(), '.', dir);
}

const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
  pkg: { dependencies, devDependencies, name, version },
  // lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  lastBuildTime: dayjs().unix(),
};

export default ({ command, mode }: ConfigEnv): UserConfig => {
  const root = process.cwd();

  const env = loadEnv(mode, root);

  const viteEnv = wrapperEnv(env);

  const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE } = viteEnv;

  const isBuild = command === 'build';

  return {
    base: VITE_PUBLIC_PATH,
    root,
    resolve: {
      alias: [
        // // 避免vue-i18n警告
        // {
        //   find: 'vue-i18n',
        //   replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
        // },
        // /@/xxxx => src/xxxx
        {
          find: /\/@\//,
          replacement: pathResolve('src') + '/',
        },
        // /#/xxxx => types/xxxx
        {
          find: /\/#\//,
          replacement: pathResolve('types') + '/',
        },
      ],
    },
    server: {
      // 监听所有本地IP
      host: true,
      port: VITE_PORT,
      // 从.env加载代理配置
      proxy: createProxy(VITE_PROXY),
    },
    esbuild: {
      // 从.env加载no_console
      drop: VITE_DROP_CONSOLE ? ['console', 'debugger'] : [],
    },
    build: {
      sourcemap: true,
      // 兼容原生ESM的浏览器
      target: 'es2015',
      // 兼容移动端css样式
      cssTarget: 'chrome80',
      // 生成打包文件名称 dist
      outDir: OUTPUT_DIR,
      // 不报告压缩大小
      reportCompressedSize: false,
      // 消除打包大小超过500kb警告
      chunkSizeWarningLimit: 4000,
      rollupOptions: {
        output: {
          // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
          entryFileNames: 'js/[name].[hash].js',
          // 用于命名代码拆分时创建的共享块的输出命名
          chunkFileNames: 'js/[name].[hash].js',
          // 用于输出静态资源的命名,[ext]表示文件扩展名
          assetFileNames: (assetInfo: any) => {
            const info = assetInfo.name.split('.');
            let extType = info[info.length - 1];
            if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
              extType = 'media';
            } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
              extType = 'img';
            } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
              extType = 'fonts';
            }
            return `${extType}/[name].[hash].[ext]`;
          },
        },
        // 这一部分等后续nginx熟练了再启用
        // output: {
        //   // 最小化拆分包
        //   manualChunks: (id) => {
        //     if (id.includes('node_modules')) {
        //       return id.toString().split('node_modules/')[1].split('/')[0].toString();
        //     }
        //   },
        //   // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
        //   entryFileNames: `${OUTPUT_DIR}/js/[name].[hash].js`,
        //   // 用于命名代码拆分时创建的共享块的输出命名
        //   chunkFileNames: `${OUTPUT_DIR}/js/[name].[hash].js`,
        //   // 用于输出静态资源的命名,[ext]表示文件扩展名
        //   assetFileNames: `${OUTPUT_DIR}/[ext]/[name].[hash].[ext]`,
        // },
      },
    },
    define: {
      // 定义全局变量替换方式
      __APP_INFO__: JSON.stringify(__APP_INFO__),
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: generateModifyVars(),
        },
      },
    },
    // 插件
    plugins: createVitePlugins(viteEnv, isBuild),
    // 依赖预构建配置项
    optimizeDeps: {
      include,
      exclude,
    },
  };
};

全部粘贴过去以后会有很多爆红,没事先都注释掉,后面逐项放开。

// vite.config.ts
// import { ConfigEnv, loadEnv, UserConfig } from 'vite';
import { UserConfig } from 'vite';
import { resolve } from 'path';

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

// import { exclude, include } from './build/optimize';
// import { createProxy } from './build/vite/proxy';
// import { wrapperEnv } from './build/utils';
// import { createVitePlugins } from './build/vite/plugins';
// import { OUTPUT_DIR } from './build/constant';
// import { generateModifyVars } from './build/generate/generateModifyVars';

function pathResolve(dir: string) {
  return resolve(process.cwd(), '.', dir);
}

const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
  pkg: { dependencies, devDependencies, name, version },
  // lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  // lastBuildTime: dayjs().unix(),
};

export default (): UserConfig => {
  // export default ({ command, mode }: ConfigEnv): UserConfig => {
  const root = process.cwd();

  // const env = loadEnv(mode, root);

  // const viteEnv = wrapperEnv(env);

  // const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE } = viteEnv;

  // const isBuild = command === 'build';

  return {
    // base: VITE_PUBLIC_PATH,
    root,
    resolve: {
      alias: [
        // 避免vue-i18n警告
        {
          find: 'vue-i18n',
          replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
        },
        // /@/xxxx => src/xxxx
        {
          find: /\/@\//,
          replacement: pathResolve('src') + '/',
        },
        // /#/xxxx => types/xxxx
        {
          find: /\/#\//,
          replacement: pathResolve('types') + '/',
        },
      ],
    },
    server: {
      // 监听所有本地IP
      host: true,
      // port: VITE_PORT,
      // 从.env加载代理配置
      // proxy: createProxy(VITE_PROXY),
    },
    esbuild: {
      // 从.env加载no_console
      // drop: VITE_DROP_CONSOLE ? ['console', 'debugger'] : [],
    },
    build: {
      sourcemap: true,
      // 兼容原生ESM的浏览器
      target: 'es2015',
      // 兼容移动端css样式
      cssTarget: 'chrome80',
      // 生成打包文件名称 dist
      // outDir: OUTPUT_DIR,
      // 不报告压缩大小
      reportCompressedSize: false,
      // 消除打包大小超过500kb警告
      chunkSizeWarningLimit: 4000,
      rollupOptions: {
        output: {
          // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
          entryFileNames: 'js/[name].[hash].js',
          // 用于命名代码拆分时创建的共享块的输出命名
          chunkFileNames: 'js/[name].[hash].js',
          // 用于输出静态资源的命名,[ext]表示文件扩展名
          assetFileNames: (assetInfo: any) => {
            const info = assetInfo.name.split('.');
            let extType = info[info.length - 1];
            if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
              extType = 'media';
            } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
              extType = 'img';
            } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
              extType = 'fonts';
            }
            return `${extType}/[name].[hash].[ext]`;
          },
        },
        // 这一部分等后续nginx熟练了再启用
        // output: {
        //   // 最小化拆分包
        //   manualChunks: (id) => {
        //     if (id.includes('node_modules')) {
        //       return id.toString().split('node_modules/')[1].split('/')[0].toString();
        //     }
        //   },
        //   // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
        //   entryFileNames: `${OUTPUT_DIR}/js/[name].[hash].js`,
        //   // 用于命名代码拆分时创建的共享块的输出命名
        //   chunkFileNames: `${OUTPUT_DIR}/js/[name].[hash].js`,
        //   // 用于输出静态资源的命名,[ext]表示文件扩展名
        //   assetFileNames: `${OUTPUT_DIR}/[ext]/[name].[hash].[ext]`,
        // },
      },
    },
    define: {
      // 定义全局变量替换方式
      __APP_INFO__: JSON.stringify(__APP_INFO__),
    },
    css: {
      preprocessorOptions: {
        scss: {
          // additionalData: generateModifyVars(),
        },
      },
    },
    // 插件
    // plugins: createVitePlugins(viteEnv, isBuild),
    // 依赖预构建配置项
    optimizeDeps: {
      // include,
      // exclude,
    },
  };
};

3、scss全局变量-generate文件

在build文件下新建一个generate文件夹,并在里面新建一个generateModifyVars.ts文件用来存放我们的全局变量文件。

// generateModifyVars.ts
/**
 * scss全局变量
 */
export function generateModifyVars() {
  // 用于全局导入,以避免需要单独导入每个样式文件
  return `@use '/@/style/config.scss' as *;`;
}

/@/这个路径前缀后面我们会配置,先这么用着吧,指向的src目录。
按照/@/style/config.scss新建scss全局变量文件config.scss。

// config.scss
// generateModifyVars函数使用@use引用,此处不能再次使用@use引用
// 原因未知

@import './color';
@import './var/index';
// color.scss
html {
  // let sider

  --sider-dark-bg-color: #273352;
}

$content-bg: #f4f7f9;
$text-color-help-dark: #909399;
$primary-color: #13c2c2;

// let -menu
$sider-dark-bg-color: var(--sider-dark-bg-color);

// var/index.scss
$namespace: hp;

此时可以把vite.config.ts里generateModifyVars()相关的代码放开了。
这里放一些示例。接着我们可以这么用。
在这里插入图片描述

4、dayjs

下载dayjs插件,这个没什么好说的。

// package.json
"dayjs": "^1.11.1",

此时可以放开dayjs相关的部分。

5、constant-常量

在build文件里新建constant.ts文件。

// constant.ts
/**
 * 在生产环境中输入的配置文件的名称
 * 全局常量
 */
export const GLOB_CONFIG_FILE_NAME = '_app.config.js';

export const OUTPUT_DIR = 'dist';

此时output部分可以放开了,注意:// 这一部分等后续nginx熟练了再启用。
建议使用该注释上面的部分。

6、utils-打包需要的一些工具函数

这个文件要仔细看一下,会影响到.env文件的命名、环境变量的前缀和类型、打包预览的生成这些部分。
和constant.ts同级,新建utils文件。

// utils.ts
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';

/**
 * 对环境变量的类型进行处理
 * @param envConf Obj
 * @returns
 */
export function wrapperEnv(envConf: Recordable): ViteEnv {
  const ret: any = {};

  for (const envName of Object.keys(envConf)) {
    let realName = envConf[envName].replace(/\\n/g, '\n');
    realName = realName === 'true' ? true : realName === 'false' ? false : realName;

    if (envName === 'VITE_PORT') {
      realName = Number(realName);
    }
    if (envName === 'VITE_PROXY' && realName) {
      try {
        realName = JSON.parse(realName.replace(/'/g, '"'));
      } catch (error) {
        realName = '';
      }
    }
    ret[envName] = realName;
  }
  return ret;
}

/**
 * 是否生成打包预览
 */
export function isReportMode(): boolean {
  return process.env.REPORT === 'true';
}

/**
 * 获取当前环境下生效的配置文件名
 */
function getConfFiles() {
  const script = process.env.npm_lifecycle_script;
  const reg = new RegExp('--mode ([a-z_\\d]+)');
  const result = reg.exec(script as string) as any;
  if (result) {
    const mode = result[1] as string;
    return ['.env', `.env.${mode}`];
  }
  return ['.env', '.env.production'];
}

/**
 * 获取以指定前缀开头的环境变量
 * @param match prefix
 * @param confFiles ext
 */
export function getEnvConfig(match = 'VITE_GLOB_', confFiles = getConfFiles()) {
  let envConfig = {};
  confFiles.forEach((item) => {
    try {
      const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
      envConfig = { ...envConfig, ...env };
    } catch (e) {
      console.error(`Error in parsing ${item}`, e);
    }
  });
  const reg = new RegExp(`^(${match})`);
  Object.keys(envConfig).forEach((key) => {
    if (!reg.test(key)) {
      Reflect.deleteProperty(envConfig, key);
    }
  });
  return envConfig;
}

/**
 * 获取用户根目录
 * @param dir file path
 */
export function getRootPath(...dir: string[]) {
  return path.resolve(process.cwd(), ...dir);
}

这里会有些报错,顺便配置一下全局类型和dotenv插件安装。

// package.json
"dotenv": "^16.0.3",

在根目录下新建types文件夹,里面新建global.d.ts文件,内容如下。

// global.d.ts
declare type Recordable<T = any> = Record<string, T>;

declare interface ViteEnv {
  VITE_PORT: number;
  VITE_USE_MOCK: boolean;
  VITE_USE_PWA: boolean;
  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;
  VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';
  VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
  VITE_LEGACY: boolean;
  VITE_GENERATE_UI: string;
}
const __APP_INFO__: {
  pkg: {
    name: string;
    version: string;
    dependencies: Recordable<string>;
    devDependencies: Recordable<string>;
  };
  lastBuildTime: string;
};

declare type Nullable<T> = T | null;

declare type DeepPartial<T> = {
  [P in keyof T]?: DeepPartial<T[P]>;
};

declare type TimeoutHandle = ReturnType<typeof setTimeout>;
declare type IntervalHandle = ReturnType<typeof setInterval>;

declare module '*.vue' {
  import { ComponentOptions } from 'vue';
  const componentOptions: ComponentOptions;
  export default componentOptions;
}

此时utils文件的报错就ok了。
接着放开./build/utils相关的部分。
// proxy: createProxy(VITE_PROXY),这部分先注释。
这部分会涉及到一些环境变量,先贴一份完整的,可以根据需要改动,建议详细看看配置里涉及到环境变量的部分。

// .env.development
# 是否打开mock
VITE_USE_MOCK = true

# 公共地址
VITE_PUBLIC_PATH = /

# 选择gzip | brotli | none的打包压缩方式
# Optional: gzip | brotli | none
# 可以同时使用两种打包方式,使用,分割即可
VITE_BUILD_COMPRESS = 'none'

# 可以配置多个跨域代理
# 注意没有换行符
VITE_PROXY = [["/devApi/devPrefix","http://localhost:3000"]]

# 是否删除警告
VITE_DROP_CONSOLE = false

# 是否兼容旧的浏览器
VITE_LEGACY = false

# 接口地址
VITE_GLOB_API_URL = /devApi

# 接口前缀
VITE_GLOB_API_URL_PREFIX = /devPrefix

# 文件下载接口地址
VITE_GLOB_UPLOAD_URL = /devUpload

// .env.production
# 是否打开mock
VITE_USE_MOCK = true

# 公共地址
VITE_PUBLIC_PATH = /

# 是否删除警告
VITE_DROP_CONSOLE = true

# 选择gzip | brotli | none的打包压缩方式
# Optional: gzip | brotli | none
# 可以同时使用两种打包方式,使用,分割即可
VITE_BUILD_COMPRESS = 'none'

# 是否兼容旧的浏览器
VITE_LEGACY = false

# 使用压缩时是否删除原始文件,默认为false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

# 接口地址
VITE_GLOB_API_URL = /proApi

# 接口前缀
VITE_GLOB_API_URL_PREFIX = /proPrefix

# 文件下载接口地址
VITE_GLOB_UPLOAD_URL = /proUpload

7、optimizeDeps-依赖预构建配置项

在build文件夹下,和constant.ts同级,新建optimize.ts文件

// optimize.ts
/**
 * 用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项
 * 引入到 src/main.ts 里的依赖就不需要再添加到 include了
 */
const include = [];

/**
 * 预构建中强制排除的依赖项
 */
const exclude = [];
// const exclude = ['@iconify-icons/ep', '@iconify-icons/ri'];

export { include, exclude };

这部分等后面写到icon再说。
此时可以放开./build/optimize相关的项。

8、plugins-插件

在build文件夹下新建vite文件夹,里面新建plugins文件夹、script文件夹和proxy文件。

8.1、先说proxy文件。

// proxy.ts文件
/**
 * 使用 .env.development proxy 配置
 */
import type { ProxyOptions } from 'vite';

type ProxyItem = [string, string];

type ProxyList = ProxyItem[];

type ProxyTargetList = Record<string, ProxyOptions>;

const httpsRE = /^https:\/\//;

/**
 * 代理生成
 * @param list
 */
export function createProxy(list: ProxyList = []) {
  const ret: ProxyTargetList = {};
  for (const [prefix, target] of list) {
    const isHttps = httpsRE.test(target);

    ret[prefix] = {
      target: target,
      changeOrigin: true,
      ws: true,
      rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
      // https 模式需要 secure=false
      ...(isHttps ? { secure: false } : {}),
    };
  }

  // 编译时会根据VITE_PROXY生成proxy对象
  return ret;
}

在.env.development和.env.production两个文件里查看proxy地址。
地址根据需要可以变动,可以根据实际情况更改createProxy函数。
此时可以放开createProxy(VITE_PROXY)部分。

8.2、script-生成配置文件函数

在这个文件夹下新建两个文件,buildConf.ts和postBuild.ts。
它们是用来生成配置文件的,除此之外还有其他的作用,感兴趣可以深入看一下,反正代码量不多。

// buildConf.ts
/**
 * 用于打包时生成其他配置文件。该文件可以配置一些全局变量,这样就可以在不重新打包的情况下直接从外部更改
 */
import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from '../../constant';
import fs, { writeFileSync } from 'fs-extra';
import colors from 'picocolors';

import { getEnvConfig, getRootPath } from '../../utils';
import { getConfigFileName } from '../../getConfigFileName';

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

interface CreateConfigParams {
  // windows对象下的存储配置的属性名
  configName: string;
  // 存储配置的对象
  config: any;
  // JS文件名
  configFileName?: string;
}

function createConfig(params: CreateConfigParams) {
  const { configName, config, configFileName } = params;
  try {
    // 变量字符串
    const windowConf = `window.${configName}`;
    // 将配置的对象以JSON字符串格式拼接到变量字符串后面,最后用正则将空格去掉,确保变量不会被修改
    let configStr = `${windowConf}=${JSON.stringify(config)};`;
    configStr += `
      Object.freeze(${windowConf});
      Object.defineProperty(window, "${configName}", {
        configurable: false,
        writable: false,
      });
    `.replace(/\s/g, '');

    // 创建dist文件夹
    fs.mkdirp(getRootPath(OUTPUT_DIR));
    // 将字符串写入到dist文件下的指定JS文件名的文件中
    writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);

    console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
    console.log(colors.gray(OUTPUT_DIR + '/' + colors.green(configFileName)) + '\n');
  } catch (error) {
    console.log(colors.red('configuration file configuration file failed to package:\n' + error));
  }
}

export function runBuildConfig() {
  // 获取我们可以配置的环境变量对象,默认取VITE_GLOB_开头的环境变量
  const config = getEnvConfig();
  // 获取配置文件的JS名
  const configFileName = getConfigFileName(config);
  // 创建文件
  createConfig({ config, configName: configFileName, configFileName: GLOB_CONFIG_FILE_NAME });
}

这里可以看到如果启动脚本参数带有disabled-config就不会生成对应的配置参数

// postBuild.ts
import { runBuildConfig } from './buildConf';
import colors from 'picocolors';

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

export const runBuild = async () => {
  try {
    // 取运行脚本命令里的参数
    const argvList = process.argv.splice(2);

    if (!argvList.includes('disabled-config')) {
      // 同步创建配置文件-
      runBuildConfig();
    }

    console.log(`${colors.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
  } catch (error) {
    console.log(colors.red('vite build error:\n' + error));
    process.exit(1);
  }
};
runBuild();

安装几个插件,在build文件夹下新建getConfigFileName.ts文件。

// package.json
"picocolors": "^1.0.0",
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0",```

这里控制生成的配置文件名

// getConfigFileName.ts
/**
 * 获取配置文件变量名
 * @param env
 */
export const getConfigFileName = (env: Record<string, any>) => {
  return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__`
    .toUpperCase()
    .replace(/\s/g, '');
};

8.3、plugins-插件

vite提供了很多好用的插件,集中放在这里
vite-plugin-compression,用于打包和输出gzip

// compress.ts
/**
 * 用于打包和输出gzip;
 * https://github.com/anncwb/vite-plugin-compression
 */
import type { PluginOption } from 'vite';
import compressPlugin from 'vite-plugin-compression';

export function configCompressPlugin(
  compress: 'gzip' | 'brotli' | 'none',
  deleteOriginFile = false,
): PluginOption | PluginOption[] {
  const compressList = compress.split(',');

  const plugins: PluginOption[] = [];

  if (compressList.includes('gzip')) {
    plugins.push(
      compressPlugin({
        ext: '.gz',
        deleteOriginFile,
      }),
    );
  }

  if (compressList.includes('brotli')) {
    plugins.push(
      compressPlugin({
        ext: '.br',
        algorithm: 'brotliCompress',
        deleteOriginFile,
      }),
    );
  }
  return plugins;
}

vite-plugin-html,针对 index.html,提供压缩和基于 ejs 模板功能

// html.ts
/**
 * 针对 index.html,提供压缩和基于 ejs 模板功能
 * https://github.com/anncwb/vite-plugin-html
 */
import type { PluginOption } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
import pkg from '../../../package.json';
import { GLOB_CONFIG_FILE_NAME } from '../../constant';

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}-${new Date().getTime()}`;
  };

  const htmlPlugin: PluginOption[] = createHtmlPlugin({
    // 是否压缩 html
    minify: isBuild,
    inject: {
      // 将数据注入ejs模板
      data: {
        title: VITE_GLOB_APP_TITLE,
      },
      // 嵌入生成的app.config.js文件
      tags: isBuild
        ? [
            {
              tag: 'script',
              attrs: {
                src: getAppConfigSrc(),
              },
            },
          ]
        : [],
    },
  });
  return htmlPlugin;
}

vite-plugin-mock,用于开发和生产的mock插件

// mock.ts
/**
 * 用于开发和生产的mock插件.
 * https://github.com/anncwb/vite-plugin-mock
 */
import { viteMockServe } from 'vite-plugin-mock';

export function configMockPlugin(isBuild: boolean) {
  return viteMockServe({
    ignore: /^\_/,
    mockPath: 'mock',
    localEnabled: !isBuild,
    prodEnabled: isBuild,
    injectCode: `
      import { setupProdMockServer } from '../mock/_createProductionServer';

      setupProdMockServer();
      `,
  });
}

rollup-plugin-visualizer,打包构建分析

// visualizer.ts
/**
 * 打包构建分析
 */
import visualizer from 'rollup-plugin-visualizer';
import { isReportMode } from '../../utils';

export function configVisualizerConfig() {
  if (isReportMode()) {
    return visualizer({
      filename: './node_modules/.cache/visualizer/stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true,
    }) as Plugin;
  }
  return [];
}

先装插件,再新建文件

// package.json
"mockjs": "^1.1.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "2.9.6",
"rollup-plugin-visualizer": "^5.9.0",

vite-plugin-mock插件需要我们新建一个mock文件夹,按照上面路径新建一个
也就是根目录下,里面新建_createProductionServer.ts和_util.ts文件

// _createProductionServer.ts
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';

// 问题描述
// 1. `import.meta.globEager` 已被弃用, 需要升级vite版本,有兼容问题
// 2. `vite-plugin-mock` 插件问题 https://github.com/vbenjs/vite-plugin-mock/issues/56

// const modules: Record<string, any> = import.meta.glob("./**/*.ts", {
//   import: "default",
//   eager: true,
// });

// const mockModules = Object.keys(modules).reduce((pre, key) => {
//   if (!key.includes("/_")) {
//     pre.push(...modules[key]);
//   }
//   return pre;
// }, [] as any[]);

const modules = import.meta.glob('./**/*.ts', { eager: true });

const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
  if (key.includes('/_')) {
    return;
  }
  mockModules.push(...(modules as Recordable)[key].default);
});

// const modules = import.meta.globEager('./**/*.ts');

// const mockModules: any[] = [];
// Object.keys(modules).forEach((key) => {
//   if (key.includes('/_')) {
//     return;
//   }
//   mockModules.push(...modules[key].default);
// });

/**
 * 用于生产环境。需要手动导入所有模块
 */
export function setupProdMockServer() {
  createProdMockServer(mockModules);
}

_util.ts是用来设置mock接口返回数据结构模板的

// _util.ts
// 用于返回统一格式的接口数据格式
import { ResultEnum } from '/@/enums/httpEnum';

export function resultSuccess<T = Recordable>(result: T, { message = 'ok' } = {}) {
  return {
    code: ResultEnum.SUCCESS,
    result,
    message,
    type: 'success',
  };
}

export function resultPageSuccess<T = any>(
  page: number,
  pageSize: number,
  list: T[],
  { message = 'ok' } = {},
) {
  const pageData = pagination(page, pageSize, list);

  return {
    ...resultSuccess({
      items: pageData,
      total: list.length,
    }),
    message,
  };
}

export function resultError(
  message = 'Request failed',
  { code = ResultEnum.ERROR, result = null } = {},
) {
  return {
    code,
    result,
    message,
    type: 'error',
  };
}

export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
  const offset = (pageNo - 1) * Number(pageSize);
  return offset + Number(pageSize) >= array.length
    ? array.slice(offset, array.length)
    : array.slice(offset, offset + Number(pageSize));
}

export interface requestParams {
  method: string;
  body: any;
  headers?: { authorization?: string };
  query: any;
}

/**
 * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改
 *
 */
export function getRequestToken({ headers }: requestParams): string | undefined {
  return headers?.authorization;
}

// 接口地址baseUrl
export const baseUrl = '/devApi/devPrefix';

/@/enums/httpEnum’是我个人用的数据返回结构,放在这里做示例
src目录下新建enums文件夹

// src/enums/httpEnum.ts
/**
 * @description: 请求结果
 */
export enum ResultEnum {
  SUCCESS = 200,
  ERROR = 500,
  TIMEOUT = 504,
  TYPE = 'success',
}

/**
 * @description: 请求方法
 */
export enum RequestEnum {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  DELETE = 'DELETE',
}

/**
 * @description:  contentType
 */
export enum ContentTypeEnum {
  // json
  JSON = 'application/json;charset=UTF-8',
  // form-data qs
  FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
  // form-data  upload
  FORM_DATA = 'multipart/form-data;charset=UTF-8',
}

直接和_createProductionServer.ts平级新建一个demo文件夹,这里放一些mock数据的示例

// mock/demo/list.ts
import { resultSuccess, resultError, getRequestToken, requestParams, baseUrl } from '../_util';
import { MockMethod } from 'vite-plugin-mock';

const listInfo = {
  content: [
    {
      id: 1,
      name: '示例数据1',
      createBy: '示例数据创建人1',
    },
    {
      id: 2,
      name: '示例数据2',
      createBy: '示例数据创建人2',
    },
    {
      id: 3,
      name: '示例数据3',
      createBy: '示例数据创建人3',
    },
    {
      id: 4,
      name: '示例数据4',
      createBy: '示例数据创建人4',
    },
    {
      id: 5,
      name: '示例数据5',
      createBy: '示例数据创建人5',
    },
    {
      id: 6,
      name: '示例数据6',
      createBy: '示例数据创建人6',
    },
  ],
  page: 1,
  pageSize: 10,
  total: 100,
};

export default [
  {
    url: `${baseUrl}/getListInfo`,
    timeout: 1000,
    method: 'get',
    response: (request: requestParams) => {
      const token = getRequestToken(request);
      if (!token) {
        return resultError('Invalid token!');
      }

      return resultSuccess(listInfo);
    },
  },
] as MockMethod[];

文件夹名可以任意命名,原因可以看_createProductionServer.ts文件
接着来看plugins里面的index.ts文件,这里面也有一些vite插件

// build/vite/plugins/index.ts
import { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from '@vitejs/plugin-legacy';
import { webUpdateNotice } from '@plugin-web-update-notification/vite';

import { configCompressPlugin } from './compress';
import { configHtmlPlugin } from './html';
import { configVisualizerConfig } from './visualizer';
import { configMockPlugin } from './mock';

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

  const vitePlugins: (PluginOption | PluginOption[])[] = [
    vue(),
    vueJsx(),
    // 打包部署后提醒插件
    webUpdateNotice({
      logVersion: true,
      // 取消默认通知栏,监听更新事件自定义
      // hiddenDefaultNotification: true
    }),
    // 在其他文件中监听自定义更新事件
    // document.body.addEventListener('plugin_web_update_notice', (e) => {
    //   const { version, options } = e.detail;
    //   // write some code, show your custom notification and etc.
    //   alert('System update!');
    // }),
  ];

  // 兼容低版本浏览器
  VITE_LEGACY && isBuild && vitePlugins.push(legacy());

  // 生成打包预览
  vitePlugins.push(configVisualizerConfig());

  // 生成mock数据
  VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild));

  // 用于最小化和使用index.html中ejs模板语法的插件
  vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));

  if (isBuild) {
    // 打包gzip | brotli压缩
    vitePlugins.push(
      configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE),
    );
  }

  return vitePlugins;
}

@vitejs/plugin-legacy,兼容低版本浏览器
@plugin-web-update-notification/vite,打包部署后提醒插件
安装一下

// package.json
"@vitejs/plugin-legacy": "^4.0.3",
"@plugin-web-update-notification/vite": "^1.7.1",

至此build打包配置完成。
在vite.config.ts里记得放开createVitePlugins函数相关的部分

9、安装一些辅助插件

// package.json
"rimraf": "^5.0.0",
"esno": "^0.16.3",
"cross-env": "^7.0.3",

10、配置一下package.json的脚本命令

可以把vue-tsc去掉,因为我们已经装了Lint代码检查工具了。

// package.json
  "author": {
    "name": "hit-point",
    "email": "13213726869@163.com"
  },
  "scripts": {
    "commit": "git add . && czg",
    "bootstrap": "pnpm install",
    "release": "release-it",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 -n ./changelog-option.cjs",
    "dev": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite --mode development",
    "dev:pro": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite --mode production",
    "serve": "pnpm dev",
    "build": "rimraf dist && cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=8192 vite build --mode development && esno ./build/vite/script/postBuild.ts",
    "build:pro": "rimraf dist && cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production && esno ./build/vite/script/postBuild.ts",
    "preview": "pnpm run build && vite preview",
    "report": "cross-env REPORT=true pnpm run build",
    "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
    "clean:lib": "rimraf node_modules",
    "test:unit": "vitest",
    "lint:eslint": "eslint --max-warnings 0  \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
    "lint:prettier": "prettier --write  \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
    "lint:stylelint": "stylelint --fix \"**/*.{vue,css,scss,postcss,less}\" node_modules/.cache/stylelint/",
    "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.cjs",
    "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
    "prepare": "husky install"
  },

接下来pnpm clean:lib,然后删掉-lock文件,重新install一下。

结语

vite配置这部分本想跟规范篇一样分成很多篇的,但是看了一下比较复杂还是全放在一起比较好,后面出些文章丰富一下吧。未完待续…

  • 33
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值