likes
comments
collection
share

为 Vite 赋予条件编译指令的能力

作者站长头像
站长
· 阅读数 10

使用过 uni-app 的人都知道,它内置了 preprocess,实现了通过 #ifdef#ifndef 等注释来排除不属于当前编译平台的代码,从而减少构建后的包体积。

那么如何为 Vite 提供类似的能力?

思考

要实现最简单的 /#ifn?def/ 核心就两点,环境变量的读取和文本替换。

环境变量可以直接读取 process.env,但是默认情况这是不安全的,所以我们选择从 configResolved 钩子中读取 config.env 即可。

文本替换即在 transform 钩子中对传入的代码进行正则匹配并替换即可。不过这个正则需要匹配到任意语言的注释当中的关键词和条件。

打开 regex101 一顿操作,便有了如下正则:

const reg = /^.*?#v-if(n?)def\s*(\S+).*[\r\n]{1,2}([\s\S]+?)\s*.*?#v-endif.*?$/gm

为 Vite 赋予条件编译指令的能力

查看在 Regex Vis 上的可视化效果

  • 第一个分组 (n?) 用于判断是否为反模式
  • 第二个分组 (\S+).* 用于捕获条件
  • 第三个分组 ([\s\S]+?) 用于捕获条件编译块内容

实现

首先还是经典插件定义,这里把 config 提升到跟,方便到处使用。

import type { Plugin, ResolvedConfig } from "vite";
let config: ResolvedConfig = undefined!;

const replaceMatched = (code: string, _id: string) => {}

const VitePluginConditionalCompile = (): Plugin => {
  return {
    name: "vite-plugin-conditional-compile",
    enforce: "pre",
    configResolved(_config) {
      config = _config;
    },
    transform(code, id) {
      return replaceMatched(code, id);
    },
  };
};

export default VitePluginConditionalCompile;

接下来,聚焦 replaceMatched 函数

const replaceMatched = (code: string, _id: string) => {
  const env = config.env;

  code = code.replace(
    /^.*?#v-if(n?)def\s*(\S+).*[\r\n]{1,2}([\s\S]+?)\s*.*?#v-endif.*?$/gm,

    (_, $1, $2, $3) => {
      const isNot = !!$1;
      const isKeep = $2.split("||").some((v: string) => {
        let flag = false;
        const [key, value] = v.split("=");
        if (value === undefined) {
          flag = !!env[key];
        } else {
          flag = String(env[key]) === value;
        }
        flag = isNot ? !flag : flag;
        return flag;
      });
      return isKeep ? $3 : "";
    }
  );
  return {
    code,
    map: null,
  };
};

首先是 const env = config.env; 即获取 vite 提供给应用的环境变量(非内置环境变量需要使用 VITE_ 开头)。

然后捏,就是核心 replace 的第二个函数参数, $1, $2, $3 为三个分组,isNot 根据有没有 n 来判断是否为反模式,为了支持条件 ||(或),isKeep 将条件部分使用 split 进行拆分,然后用 some(即有一个为 true 就为 true)判断,在 some 内部对单一条件进行判断,最后如果 isKeeptrue 则保留,否则就舍弃。

/**
 * 条件替换
 * @param _ 匹配的字符串
 * @param $1 是否为 not 模式
 * @param $2 条件
 * @param $3 code
 */
(_, $1, $2, $3) => {
  const isNot = !!$1;
  const isKeep = $2.split("||").some((v: string) => {
    let flag = false;
    const [key, value] = v.split("=");
    if (value === undefined) {
      flag = !!env[key];
    } else {
      flag = String(env[key]) === value;
    }
    flag = isNot ? !flag : flag;
    return flag;
  });
  return isKeep ? $3 : "";
}

使用

插件我已经上传到 KeJunMao/vite-plugin-conditional-compile 了,使用起来特别简单!

// vite.config.ts
import { defineConfig } from "vite";
import ConditionalCompile from "vite-plugin-conditional-compiler";

export default defineConfig({
  plugins: [ConditionalCompile()],
});

问题

这个插件已经能完成绝大多数条件编译能力了,不过依然有几个问题:

  • 不支持嵌套
  • 不支持 else、elif
  • 不支持 && 或者 更复杂的表达式

尽管在后期,我已经支持了 else 指令。

最后,如果你有条件编译的需求,我已经不再推荐你用这个插件了,而是推荐你使用 KeJunMao/unplugin-preprocessor-directives

它使用了 unplugin 可以在任何主流打包器上使用,其次条件编译把上述问题都解决了。

最重要是这个插件将指令抽象成了插件的插件,除了内置了条件编译指令外、还支持定义、取消定义、在代码行处打印信息(编译时)

你还可以自定义指令,是的,如果你愿意,你甚至可以定义出 foreach、include 等指令。

为 Vite 赋予条件编译指令的能力

如果对你有用,点个赞再走呗~