本文实践依据另一篇文章DynamicImport完结原理中介绍的理论基础,请提前翻阅,便于理解。
事务背景
近期在团队内部做 Vite 搬迁的相关改造时,发现部分老项目中存在一些八怪七喇的依靠,它们供给的构建产品并不彻底,留下了一些特别语法,这些特别语法假定了用户运用的构建东西是 Webpack,这直接导致项目在搬迁到 Vite 之后溃散。
...
"domProps": {
"innerHTML": require("!html-loader!./icon/close-big.svg")
}
...
"domProps": {
"innerHTML": require("!html-loader!./icon/" + props.name + ".svg")
}
...
Vite 依据 rollup 的封装而来,而且内置了 @rollup/plugin-dynamic-import-vars。可是该插件只会处理 import 表达式语法,并不会处理 require 的动态引进,会直接疏忽掉。尽管能够正常打包,可是产品存在运转时错误,找不到相关文件。而且Vite 并不支撑经过 !html-loader! 这种途径前缀方法指定 loader,这个问题也需求一并处理。
处理方案
上面说到的这个问题假如不处理,技术改造将无法继续进行,因为历史依靠不敢擅动,市面上又没有找到相关处理方案,咱们只能自己着手编写插件,所谓自己着手丰衣足食。
因为 Vite 在开发时运用 esbuild,生产构建时运用 rollup,想要处理问题,咱们需求同时为这两种构建东西编写插件,略显繁琐。这就不得不说到 unplugin 这个项目,依据 unplugin 编写插件,能够使一套代码运转于不同的构建系统。
最终经过借鉴 @rollup/plugin-dynamic-import-vars 的完结方法,拓宽定制了一个 100 行左右的小插件完美处理了问题,也算是趁热打铁的一份实践。
完结思路
首要,定制化插件,为避免误伤,咱们需求确定处理规模,本例中只需求处理问题依靠@test/test-module中包括的文件。
观察源码发现,需求处理的部分都是采用 “innerHTML” 的方法设置 dom 内容,而且值为对应的 svg 文件。
由此推出,咱们只需求经过剖析找到这部分代码,直接读取对应的文件,然后将文件内容内联即可完结处理。只不过在内联文件内容的时分,需求一并处理动态引进问题。
中心代码解析
依据以上剖析,插件框架代码如下:
import { createUnplugin } from 'unplugin';
import { createFilter } from '@rollup/pluginutils';
export const fixDepError = createUnplugin(() => {
return {
name: 'unplugin-fix-dynamic-import-error',
// 在这儿限制插件转化的文件规模
transformInclude: createFilter('**/@test/test-module/**/*.js'),
resolveId: (id, importer) => {
// 在这儿处理静态引证中包括的 !html-loader! 前缀
},
load: id => {
// 在这儿将静态引证的 svg 文件转化为 JavaScript module
},
transform(code, id) {
// 在这儿转化动态引证,同时一并处理,动态引证途径中包括的 !html-loader! 前缀
},
};
});
处理静态引证途径
关于静态引证途径,咱们能够利用插件生命周期中的 resolveId 和 load 函数进行联合处理。
在 resolveId 钩子中,咱们会接纳到所有静态引证的途径 id,经过构建东西的调度,回来的 id 会替代原有的 id,假如什么都没有回来,那什么也不会产生。
resolveId: (id, importer) => {
if (id.startsWith('!html-loader!')) {
return path.resolve(path.dirname(importer), id.replace('!html-loader!', ''));
}
}
上面的代码消除了静态引证途径 id 中的 !html-loader! 字样,如此一来,构建东西便能够正确解析到相关文件途径,并做进一步处理。
转化 svg 模块
从源码中统一运用 innerHTML 设置 dom 内容,以及指明运用 html-loader 处理相关 svg 文件的行为中,能够探出,此处是想将 svg 文件内容直接当做字符串进行设置。
在 resolveId 解析完途径 id 后,下一个处理钩子便是 load 钩子。
load 钩子接纳所有处理往后的途径 id,并依据这些 id 回来相应的文件内容,回来内容均被视为 JS 模块。
咱们能够经过 load 钩子在文件内容解析进程上做文章,将 svg 文件转化为 JavaScript 模块,简略包裹即可:
load: id => {
if (id.endsWith('.svg')) return `export default \`${fs.readFileSync(id, 'utf-8')}\`;`;
}
至此,静态途径中包括 !html-loader! 字样的 svg 文件引证处理完毕。
处理动态引证
动态引证的处理进程本质上是对文件内容的语法剖析,咱们能够经过 transform 钩子进行。
猜的不错,load 钩子的下一步便是 transform 钩子。
transform 钩子接纳所有 load 处理完毕的文件内容,咱们能够在这儿对文件内容进行进一步详尽的处理,例如 AST 语法剖析,进行局部替换等等。
回来内容会替换本来的文件内容,假如什么都没有回来,那什么也不会产生。
至于为什么不在 load 钩子里面做这些事,还要多出来一个 transform 钩子,想必是为了拆分责任,便于保护。
首要运用上下文中自带的 parse 方法生成 AST节点,调试发现生成的节点为 AcornNode,为了遍历节点,咱们需求引进对应的依靠 acorn-walk
其次,明确咱们要处理的源码位置,编写对应的 visitors。这儿需求借助 AST explorer 这个在线项目,咱们将需求剖析的源码张贴进去并挑选对应的 parser,也便是 acorn,右侧将会呈现对应的 AST 结构。
结合实际,咱们有如下代码:
import walk from 'acorn-walk';
import glob from 'fast-glob';
import MagicString from 'magic-string';
transform(code, id) {
const parsedAST = this.parse(code);
walk.simple(parsedAST, {
CallExpression: node => {
if (node.callee.name !== 'require' || !node.arguments[0]) return;
const argv0 = node.arguments[0];
// 因为源码中主要存在的是 二元表达式,故此处只处理 BinaryExpress 节点
if (argv0.type !== 'BinaryExpression') return;
// expressionToGlob 担任将 DynamicImport 对应的表达式转化为通配符,下文提及
let globPattern = expressionToGlob(argv0);
// 咱们仅处理包括 !html-loader! 特别符号的语法 和 svg 文件动态引进
if (!globPattern.includes('!html-loader!') || path.extname(globPattern) !== '.svg') return;
globPattern = globPattern.replace('!html-loader!', '');
// 依据地点文件目录执行通配符抓取文件,这也是为什么动态引证只支撑相对途径
// 因为咱们需求相对建议引证的文件去通配文件
const cwd = path.dirname(id);
const files = glob.sync(globPattern, { cwd });
// glob 抓取的文件假如在当时目录,是没有 './' 前缀的,咱们需求判断是否为相对途径
const paths = files.map(r => (r.startsWith('./') || r.startsWith('../')) ? r : `./${r}`);
// 假如通配符没有抓取到文件,什么也不会做
// 假如有,则会替换源码对应位置的字符串
if (paths.length) {
// ms 为当时 code 的 MagicString 实例
ms.overwrite(
node.start,
node.end,
code.substring(node.start, node.end)
// 首要消除特别符号
.replace('!html-loader!', '')
// 然后将 require 函数替换成咱们的函数
.replace('require', `__variableDynamicImportRuntime__`),
);
// 然后再代码结束注入咱们增加的函数 __variableDynamicImportRuntime__,下文提及
ms.append(createDynamicImport(paths, cwd, dynamicImportIndex));
}
}
})
if (/* 最后做一些判断,假如存在动态引证,则回来转化后的代码 */) {
return {
code: ms.toString(),
map: ms.generateMap({
file: id,
includeContent: true,
hires: true,
}),
};
}
}
生成通配符
上文说到了 expressionToGlob 函数,该函数的效果主要是将动态引进的表达式转化为对应的通配符,用于后续的文件抓取。
function expressionToGlob(node) {
return node.type === 'BinaryExpression'
? binaryExpressionToGlob(node)
: node.type === 'Literal' // 表达式中的字符串字面量类型
? node.value
: '*';
}
function binaryExpressionToGlob(node) {
// 咱们只处理操作符为 '+' 的二元表达式,代表字符串衔接
if (node.operator !== '+') {
throw new Error(`${node.operator} operator is not supported.`);
}
// 加号衔接的表达式,将左右递归衔接
return `${expressionToGlob(node.left)}${expressionToGlob(node.right)}`;
}
因为该插件归于定制化需求,咱们只需求关注二元表达式(BinaryExpression)即可。假如要作为通用插件,咱们还需求考虑更多的情况,例如模板字符串节点(TemplateLiteral)。
更多完整完结能够参阅 plugins/dynamic-import-to-glob.js at master rollup/plugins
代码注入
依据通配符匹配到文件之后,咱们需求构造一个动态函数去替换原有的 import/require 语法
function createDynamicImport(paths, cwd, index) {
return `
function __variableDynamicImportRuntime${index}__(path) {
switch (path) {
${paths.map(p => `case '${p}': return \`${fs.readFileSync(path.resolve(cwd, p), 'utf-8')}\`;`).join('\n ')}
${`default: return new Promise(function(resolve, reject) {
(typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)(
reject.bind(null, new Error("Unknown variable dynamic import: " + path))
);
})\n`} }
}\n\n
`;
}
这儿的逻辑十分简略,生成一段 switch 句子,依据途径回来对应文件内容。因为咱们的定制场景,此处将 svg 文件内容直接读取内联即可。若在通用场景下,则直接运用 import/require 替换即可,然后交由构建东西处理:
${paths.map(p => `case '${p}': return import('${p}');`).join('\n ')}
细节处理
上面的完结方法默认了代码中仅存在一处 DynamicImport,假如存在多处,则相同的函数名必定会形成冲突。最简略的方法便是界说一个计数变量,与函数名拼接:
transform(code, id){
let dynamicImportIndex = -1;
...
if (paths.length) {
dynamicImportIndex += 1; // 遇到动态引进就自增
ms.overwrite(
node.start,
node.end,
code.substring(node.start, node.end)
.replace('!html-loader!', '')
// 将 require 函数替换成咱们的函数
.replace('require', `__variableDynamicImportRuntime${dynamicImportIndex}__`),
);
ms.append(createDynamicImport(paths, cwd, dynamicImportIndex));
}
}
// 接纳 index 进行命名拼接
function createDynamicImport(paths, cwd, index) {
return `
function __variableDynamicImportRuntime${index}__(path)
`;
}
至此,问题依靠的转化处理完毕,项目现已能正常运转起来了。
最后
更多完结细节能够参阅 plugins/index.js at master rollup/plugins