动态引入DynamicImport实现原理

动态引入允许在运行时根据变量决定导入哪个模块,基于rollup的@rollup/plugin-dynamic-import-vars插件可以处理动态导入路径,通过AST分析生成通配符匹配文件。然而,动态导入有限制,如必须是相对路径,包含文件后缀等。插件的核心流程包括AST分析、路径处理、通配符匹配和代码替换。
摘要由CSDN通过智能技术生成

什么是动态引入(DynamicImport)?

本文介绍的动态引入实现方式基于 rollup 插件 @rollup/plugin-dynamic-import-vars

通常情况下,我们都是通过确定的字面量路径来引用文件模块的,例如:

1

2

3

import './a.js';

require('./a.js');

import('./a.js');

对于确定的文件路径来说,构建工具可以轻易的抓取文件并进行相关的转换。

但当import或者require的目标不是一个静态字符串,而是一个动态表达式时,构建工具其实也不确定用户到底引用了什么,所以通常这种情况只能依靠 JavaScript 的运行时来解析。

若动态表达式实际代表的路径无法被解析,则运行时会引起控制台的错误。通常是因为生成的文件路径并没有被纳入打包体系,所以找不到文件。

下面列出了一些常见的动态引入表达式:

1

2

3

4

5

6

7

8

// TemplateLiteral 模板字符串

import(`./icons/arrow-${type}.svg`);

require(`./icons/arrow-${type}.svg`);

// BinaryExpression 二元表达式

import('./icon/arrow-' + type + '.svg');

// 直接引用一个变量

import(path);

require(path)

但经过前人们的实践发现,当动态表达式满足一定的结构时,构建工具便可以通过一些特殊手段抓取并打包路径匹配的相关文件,并自动注入一些 polyfill,从而实现动态引入(DynamicImport)的效果,也就是本文的主题。

动态引入的实现原理

本节内容翻译加工自 @rollup/plugin-dynamic-import-vars README.md 部分章节

当动态导入的路径中包含变量时,经过 AST 分析可以生成对应的通配符。在构建的时候,这些通配符将被用于抓取匹配的文件。随后这些文件会被添加进构建体系中,在运行时,根据导入的实际路径返回对应的文件内容。

下面是一些通配符的转换示例:

1

2

3

4

5

6

7

8

9

10

`./locales/${locale}.js` -> './locales/*.js'

`./${folder}/${name}.js` -> './*/*.js'

`./module-${name}.js` -> './module-*.js'

`./modules-${name}/index.js` -> './modules-*/index.js'

'./locales/' + locale + '.js' -> './locales/*.js'

'./locales/' + locale + foo + bar '.js' -> './locales/*.js'

'./locales/' + `${locale}.js` -> './locales/*.js'

'./locales/' + `${foo + bar}.js` -> './locales/*.js'

'./locales/'.concat(locale, '.js') -> './locales/*.js'

'./'.concat(folder, '/').concat(name, '.js') -> './*/*.js'

待转换的代码可能是这样的:

1

2

3

function importLocale(locale) {

  return import(`./locales/${locale}.js`);

}

经过转换后它会变成下面这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

function __variableDynamicImportRuntime__(path) {

  switch (path) {

    case './locales/en-GB.js':

      return import('./locales/en-GB.js');

    case './locales/en-US.js':

      return import('./locales/en-US.js');

    case './locales/nl-NL.js':

      return import('./locales/nl-NL.js');

    default:

      return new Promise(function (resolve, reject) {

        queueMicrotask(reject.bind(null, new Error('Unknown variable dynamic import: ' + path)));

      });

  }

}

function importLocale(locale) {

  return __variableDynamicImportRuntime__(`./locales/${locale}.js`);

}

可以看到,实际的 import 被替换成了注入的 __variableDynamicImportRuntime__ 函数,该函数会根据运行时拼接的具体字符串返回对应的打包文件。

动态引入的限制

本节内容翻译加工自 @rollup/plugin-dynamic-import-vars README.md 部分章节

为了知道要在代码中注入什么,我们必须能够对代码进行一些静态分析,并对可能的导入做出一些假设。例如,如果只使用一个变量,理论上可以从整个文件系统中导入任何内容。

1

2

3

function importModule(path) {

  return import(path); // 这根本无法推断引入了什么

}

为了能够实现静态分析,并避免可能出现的问题,动态引入的实现上限定了一些规则:

Import 路径须为相对路径

所有导入都必须相对于导入文件进行。导入不应该是纯变量、绝对路径或裸导入:

1

2

3

4

5

// Not allowed

import(bar); // 纯变量

import(`/foo/${bar}.js`); // 绝对路径

import(`${bar}.js`); // 裸导入

import(`some-library/${bar}.js`); // 裸导入

引用路径需包含文件后缀

文件夹中可能包含你不打算导入的文件。因此,我们要求导入的静态部分以文件扩展名结束

1

2

import(`./foo/${bar}`); // Not allowed

import(`./foo/${bar}.js`); // Allowed

导入当前目录的文件需要指定具体的文件匹配格式

如果你从当前目录导入文件,很可能会导入一些原本不打算导入的文件,包括书写代码的这个文件本身。因此这种情况下需要给出一个更具体的文件名匹配格式:

1

2

import(`./${foo}.js`); // not allowed

import(`./module-${foo}.js`); // allowed

通配符(Glob Pattern)仅有一层深度

在生成通配符时,字符串中的每个变量都会被转换为通配符中的*,每个层级的目录最多一个星号。这避免了无意中从更多的目录中添加文件到导入中。

下面的例子中,最终将会生成 ./foo/*/.js 而非 ./foo/**/.js

1

import(`./foo/${x}${y}/${z}.js`);

核心流程解读

插件核心转换代码仅有 100 行,且非常易懂 —— plugins/index.js at master · rollup/plugins

整体流程分为以下几步:

通过 AST 分析,拿到对应的导入路径,也就是 import 表达式括号中的源码部分。

对这部分的源码进行处理,调用 dynamicImportToGlob 函数

执行上述限制条件的判断,尝试获取一个合法的通配符。

如果通配符不合法,将会引发错误,终止进程。

执行通配符,抓取相关文件。

替换 import 表达式,并注入 __variableDynamicImportRuntime__ 函数。

附上插件核心转换代码的截图,代码本身不长且非常容易理解,感兴趣的同学可以自行跳转研究。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值