1 问题
使用vite开发项目时,发现动态导入的对象是变量时,无法build成功。涉事代码片段如下:
const API: React.FC<IProps> = ({ filePath }) => {
const Lazy = React.lazy(() => import(filePath))
return Suspense fallback={'loading...'}>
<Lazy
{...props}
/>
</Suspense>
}
2 探索过程
之后在vite官网上看到一个插件:@rollup/plugin-dynamic-import-vars.,看样子是可以支持变量的,但是尝试一番之后发现不管用,又仔细看了文档之后发现这个插件有诸多限制:
- Imports must start with ./ or ../
- Imports must end with a file extension
- Imports to your own directory must specify a filename pattern
首先第一条就不符合需求场景,因为我们动态导入的路径并不是固定在某个目录下的,而且传入的是绝对路径,如果采取把要导入的文件统一拷贝到一个目录下的方式的话,会导致被拷贝的文件中的导入都是错误的(因为文件中都是相对路径),所以就得另辟蹊径。
疯狂stackoverflow之后发现并没有解决方案,那怎么办呢?
既然这个插件可以解析变量,那肯定是可以按照这个思路自定义一个插件的啊!!!
3 解决方案:读源码,自定义插件
step1 :先搞清楚不能解析的本质原因
import()动态导入语法是在代码静态分析的时候分割代码用的,import的代码会被单独打包,而如果import一个变量的话,之后运行时才能知道变量的值,这时候静态分析就无法知道导入的是什么代码。
step2 :源码中是如何解决这个问题的
关键代码:
ms.prepend(
`function __variableDynamicImportRuntime${dynamicImportIndex}__(path) {
switch (path) {
${paths.map((p) => ` case '${p}': return import('${p}');`).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`
);
// call the runtime function instead of doing a dynamic import, the import specifier will
// be evaluated at runtime and the correct import will be returned by the injected function
ms.overwrite(
node.start,
node.start + 6,
`__variableDynamicImportRuntime${dynamicImportIndex}__`
);
其实就是遍历import可能的动态变量,然后转成静态的代码。
step3:知道了原理之后就可以自己手动撸一个插件了(前提是要先了解一下AST啦)
import { createFilter } from '@rollup/pluginutils';
import { walk } from 'estree-walker';
import fs from 'fs';
import MagicString from 'magic-string';
import path from 'path';
/**
* @desc 用来转换import(path)这种动态语法
* @param {*} options
* @returns
*/
export default function transformCodePlugin(options = {}) {
const filter = createFilter(options.include, options.exclude,options.sourceScope);
//feature:从外层的mc配置文件中读取,会在拉取模板的时候注入
___INSERT_CODE___
const insertcode = genCode(sourcePath)
return {
name: 'transform-full-dynamic-import',
transform(code, id) {
if (!filter(id)) return;
let ms = null
let transformIndex = -1
const parsed = this.parse(code);
walk(parsed, {
enter: (node) => {
if (node.type !== 'ImportExpression') {
return;
}
if (node.source.type === 'Identifier') {
transformIndex += 1
ms = ms || new MagicString(code);
ms.prepend(
`function __variableDynamicImportRuntime_${transformIndex}__(path) {\n
${insertcode}
}\n\n`
);
ms.overwrite(
node.start,
node.start + 6,
`__variableDynamicImportRuntime_${transformIndex}__`
);
}
}
})
if (ms) {
return {
code: ms.toString(),
map: ms.generateMap({
file: id,
includeContent: true,
hires: true
})
};
}
return null
}
};
}
function genCode(scopes){
const paths = genScope(scopes)
const codes = paths.map(path => {
return `case '${path}' : return import('${path}'); `
})
return `switch (path) {
${codes.join('\n')}
}`
}
//获取可能涉及组件的所有文件的绝对路径
function genScope(scopes){
const paths = []
scopes && scopes.forEach(scope => {
//这里要解析到项目目录,因为配置的组件的路径都是相对于项目目录的
const dir = path.resolve(process.cwd(),'../../../',scope)
walkSync(dir,(filePath)=>{
paths.push(filePath)
})
})
return paths
}
function walkSync(currentDirPath, callback) {
fs.readdirSync(currentDirPath).forEach(function (name) {
var filePath = path.join(currentDirPath, name);
var stat = fs.statSync(filePath);
if (stat.isFile()) {
callback(filePath, stat);
} else if (stat.isDirectory()) {
walkSync(filePath, callback);
}
});
}