【Esbuild】改写Typescript import默认行为,实现客户定制化功能

164 阅读2分钟

本demo基于 Typescript, 构建基于esbuild;

一、背景:

我们有个项目,同时销售给不同的客户,项目大部分的功能都能共用,但是一般不同的客户对于项目会有自己细微的一些定制化需求,比如 可能某一个模块的行为在不同的客户那边需要有不同的实现,客户A 需要实现A 功能, 客户B 需要实现B 功能。

Demo:

比如,项目里有两个需要被定制的模块,core.tsmain.ts,客户对他们均有自己 不同的想法:
image.png

入口文件: entry/index.ts

import { core } from '$src/core';
import { main } from '$src/main';
 
main();
core();

默认core.ts

export function core() {
    console.log('default core');
}

默认main.ts

export function main() {
    console.log('default core');
}

客户A 想实现的:core.ts

export function core() {
    console.log('customer A core');
}

客户B 想实现的:main.ts

export function main() {
    console.log('customer A main');
}

二、解决思路:

有的同学可能已经想到,用面向对象多态不就能解决这个问题了吗, 大部分场景下,多态确实可以解决定制化的问题,不过本文提供的是另外一种思路,改写import的默认行为:
1、先将所有需要的定制化模块归类到一个目录, 定制化下的 目录结构与项目根目录保持一致,统一管理;
2、在编译时, 如果定制化目录下有客户的定制化模块,那import则优先使用定制化目录下的模块,否则走默认逻辑。
比如:编译A 客户的项目, 入口文件import { core } from '$src/core'; 默认情况下应该是import src目录下的 core.ts 模块, 不过定制化目录 src下有定制版core.ts模块,我们得让 import 优先选择定制化的core.ts

image.png

三、具体实现:

1、tsconfig.json里需要先设置 customers 目录的别名:

 "paths": {
        "$src/*": ["src/*"], 
        "$customers/*": ["customers/*"],
      }

2、编写esbuild 插件,改写 import 默认行为,让import 优先使用 定制化目录下模块:

import type { PluginBuild, OnResolveArgs } from "esbuild"; 
export const customImportPlugin = (custom: string) => ({
  name: "custom-import",
  setup(build: PluginBuild) {
    // 检测需要特殊import的模块
    build.onResolve({ filter: /^\$src\// }, async (args: OnResolveArgs) => {
      // 检测定制化目录下是否有可代替的模块
      const result = await build.resolve(
        `$customers/${custom}/${args.path.replace(/^\$src\//, "src/")}`,
        { resolveDir: args.resolveDir }
      );
      // 报错则走import 默认行为
      if (result.errors.length > 0) {
        return undefined;
      }
      return { path: result.path };
    });
  },
});


3、调用插件:

import { join } from "path";
import { BuildOptions, build as esBuild } from "esbuild";
import { customImportPlugin } from "./custom-import";

const tsconfigJson = join(__dirname, "tsconfig.json");

export const createBuildOptions = (
  isBuild: boolean,
  outDir: string
): BuildOptions => {
  return {
    entryPoints: [join(__dirname, "entry/index.ts")],
    target: "es2020",
    outdir: outDir,
    format: "cjs",
    bundle: true,
    platform: "node",
    tsconfig: tsconfigJson,
    sourcemap: true,
    plugins: [customImportPlugin((process.env as any).CUSTOM)],
    external: [],
    treeShaking: true,
    metafile: isBuild, // 可以生成报告
  };
};

export function build() {
  const outDir = join(__dirname, "dist");
  const buildOptions = createBuildOptions(true, outDir);

  return esBuild(buildOptions);
}

build();

4、通过环境变量指定编译客户:
package.json

 "scripts": {
    "build": "node -r ts-node/register build.ts",
    "build:a": "cross-env CUSTOM=a node -r ts-node/register build.ts"
  },

四、效果:

通过该方案,以一种新的思路的解决 各个客户 针对项目 任意模块的 定制化问题,维护也相对比较容易。
完整demo:github.com/ouxuwen/cus…

默认构建产物:
image.png 定制化客户A 构建产物:
image.png