在项目中我们需要针对不同的平台(Web和Native)做不同实现,在抽象中实现大部分功能,把细节实现留到具体平台的代码中,但是一直没有找到好的方法,如果直接在入口文件中引入实现文件,就会导致原始代码被打包的启动文件中,文件体积变大,但如果使用import()又无法确保每次代码加载都是ok的,总不可能自己实现顶层的await import,另外这些实现常常是侵入式的,需要对原始类进行方法覆盖。今天想到一种基于构建工具的无侵入式实现,这里无侵入是指把实现作为旁路代码,而不是主体入口代码。这种旁路代码有点像依赖注入,但是是从构建工具的角度来做,实际上也很简单,通过webpack的loader,把原始代码进行改写,把实现代码合并到原始代码中去。
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs');
const path = require('path');
module.exports = function (contents) {
const { resourcePath } = this;
const options = this.getOptions();
const { abstractDir, implementDir } = options;
if (resourcePath.indexOf(abstractDir) === 0) {
const implementFilePath = path.resolve(implementDir, resourcePath.replace(abstractDir, '.'));
if (fs.existsSync(implementFilePath)) {
const implementContents = fs.readFileSync(implementFilePath).toString();
const newContents = composeFileContents(contents, implementContents);
return newContents;
}
}
return contents;
};
function composeFileContents(sourceContents, implementContents) {
const sourceLines = sourceContents.split('\n');
const implementLines = implementContents.split('\n');
implementLines.shift(); // 去掉第一行,第一行是对原始文件(要被实现的文件)的引入
const { imports: sourceImports, codes: sourceCodes } = splitCodes(sourceLines);
const { imports: implementImports, codes: implementCodes } = splitCodes(implementLines);
const { contents: imports } = composeImports(sourceImports, implementImports);
const { contents: codes } = composeCodes(sourceCodes, implementCodes);
return `${imports}\n${codes}`;
}
function splitCodes(lines) {
const imports = [];
const codes = [];
let reach = false;
let incomment = false;
lines.forEach((line) => {
const text = line.trim();
const push = () => {
if (reach) {
codes.push(line);
} else {
imports.push(line);
}
};
// 忽略注释
if (text.indexOf('/*') === 0) {
incomment = true;
push();
return;
}
if (text.substring(text.length - 2) === '*/') {
incomment = false;
push();
return;
}
if (incomment) {
push();
return;
}
if (text.indexOf('//') === 0) {
push();
return;
}
if (text.indexOf('import ') === 0) {
imports.push(line);
} else {
codes.push(line);
reach = true;
}
});
return { imports, codes };
}
function composeImports(sourceImports, implementImports) {
// TODO: 需要考虑如果import了相同的变量名的问题
const importMapping = {};
const importVars = {};
sourceImports.forEach((line) => {
if (line.indexOf('import ') !== 0) {
return;
}
const { vars, src, def } = parseImport(line);
importMapping[src] = { vars, src, def };
if (vars) {
vars.forEach((v) => {
importVars[v] = true;
});
}
if (def) {
importVars[def] = true;
}
});
// TODO: 暂时未考虑default的冲突问题
implementImports.forEach((line) => {
const { vars, src, def } = parseImport(line);
if (importMapping[src]) {
const importVars = importMapping[src].vars;
// TODO: 暂时未考虑import as后的变量名冲突问题
if (vars && importVars) {
importVars.push(...vars);
}
} else {
importMapping[src] = { vars, src, def };
}
});
// 先处理原始的
const results = [];
sourceImports.forEach((line) => {
if (line.indexOf('import ') !== 0) {
results.push(line);
return;
}
const { src } = parseImport(line);
const importText = createImport(importMapping[src]);
results.push(importText);
delete importMapping[src];
});
// 再处理多出来的
const srcs = Object.keys(importMapping);
srcs.forEach((src) => {
const importText = createImport(importMapping[src]);
results.push(importText);
});
return { contents: results.join('\n') };
}
function composeCodes(sourceCodes, implementCodes) {
const source = sourceCodes.join('\n');
const imports = `(function() {
${implementCodes.join('\n')}
} ())`;
return { contents: `${source}\n${imports}` };
}
function parseImport(importLine) {
const [, exp, src] = importLine.match(/import ([\w\W]+) from ['"](.*?)['"]/m);
const parseVar = (txt) => {
// if (txt.indexOf(' as ') > -1) {
// const [, v] = item.split(' as ');
// return v.trim();
// }
const t = txt.trim();
return t;
};
const parseVars = (txt) => {
const t = txt.substring(1, txt.length - 1);
const items = t.split(',');
const list = items.map(parseVar);
return list;
};
const txt = exp.trim();
if (/^\{.*?\}$/.test(txt)) {
const vars = parseVars(txt);
return { vars, src };
}
if (/^\w+,.*?\}$/.test(txt)) {
const [d, i] = txt.split(',').map((item) => item.trim());
const vars = parseVars(i);
const def = parseVar(d);
return { src, vars, def };
}
const def = parseVar(txt);
return { src, def };
}
function createImport(mapItem) {
const { vars, src, def } = mapItem;
if (def && vars) {
return `import ${def}, { ${vars.join(', ')} } from '${src}';`;
}
if (def) {
return `import ${def} from '${src}';`;
}
if (vars) {
return `import { ${vars.join(', ')} } from '${src}';`;
}
return '';
}
这里的实现方式比较暴力,就是通过替换的形式,把实现代码合并进原始代码。这些代码一般被放在一个叫 @implements 的目录中,没有任何其他文件引用它们,构建工具根据其给定的路径进行匹配,如果路径匹配上了,就执行合并逻辑,因此称它们为旁路代码。不过,如果原始文件中缺少这部分实现,则无法运行。
虽然这种方式割裂了代码本身的逻辑,无法通过编辑器的源文件链接找到,但是,这种方式借助构建工具,使得代码层面更加清晰,最终的产物更加合理(主体代码中不存在与之无关的代码)。

