解决“动态导入文件为变量”的正确姿势

1,926 阅读2分钟

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);
        }
    });
}