本demo基于 Typescript, 构建基于esbuild;
一、背景:
我们有个项目,同时销售给不同的客户,项目大部分的功能都能共用,但是一般不同的客户对于项目会有自己细微的一些定制化需求,比如 可能某一个模块的行为在不同的客户那边需要有不同的实现,客户A 需要实现A 功能, 客户B 需要实现B 功能。
Demo:
比如,项目里有两个需要被定制的模块,core.ts
和 main.ts
,客户对他们均有自己 不同的想法:
入口文件: 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
。
三、具体实现:
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…
默认构建产物:
定制化客户A 构建产物: