本文介绍的动态引进完成方法基于 rollup 插件 @rollup/plugin-dynamic-import-vars

什么是动态引进(DynamicImport)?

一般状况下,我们都是通过确认的字面量途径来引证文件模块的,例如:

import './a.js';
require('./a.js');
import('./a.js');

对于确认的文件途径来说,构建东西能够容易的抓取文件并进行相关的转化。

但当import或者require的方针不是一个静态字符串,而是一个动态表达式时,构建东西其实也不确认用户究竟引证了什么,所以一般这种状况只能依托 JavaScript 的运行时来解析。

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

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

// 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 剖析能够生成对应的通配符。在构建的时候,这些通配符将被用于抓取匹配的文件。随后这些文件会被添加进构建体系中,在运行时,依据导入的实践途径回来对应的文件内容。

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

`./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'

待转化的代码或许是这样的:

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

通过转化后它会变成下面这样:

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 部分章节

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

function importModule(path) {
  return import(path); // 这根本无法推断引进了什么
}

为了能够完成静态剖析,并防止或许呈现的问题,动态引进的完成上限定了一些规矩:

Import 途径须为相对途径

一切导入都必须相对于导入文件进行。导入不该该是纯变量、绝对途径或裸导入:

// Not allowed
import(bar); // 纯变量
import(`/foo/${bar}.js`); // 绝对途径
import(`${bar}.js`); // 裸导入
import(`some-library/${bar}.js`); // 裸导入

引证途径需包含文件后缀

文件夹中或许包含你不计划导入的文件。因而,我们要求导入的静态部分以文件扩展名结束

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

导入当时目录的文件需要指定详细的文件匹配格局

假如你从当时目录导入文件,很或许会导入一些原本不计划导入的文件,包含书写代码的这个文件自身。因而这种状况下需要给出一个更详细的文件名匹配格局:

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

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

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

下面的比如中,终究将会生成 ./foo/*/.js 而非 ./foo/**/.js

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

中心流程解读

插件中心转化代码仅有 100 行,且十分易懂 —— plugins/index.js at master rollup/plugins

整体流程分为以下几步:

  1. 通过 AST 剖析,拿到对应的导入途径,也便是 import 表达式括号中的源码部分。
  2. 对这部分的源码进行处理,调用 dynamicImportToGlob 函数
    1. 履行上述约束条件的判断,测验获取一个合法的通配符。
    2. 假如通配符不合法,将会引发过错,停止进程。
  3. 履行通配符,抓取相关文件。
  4. 替换 import 表达式,并注入 __variableDynamicImportRuntime__ 函数。

附上插件中心转化代码的截图,代码自身不长且十分容易理解,感兴趣的同学能够自行跳转研究。

DynamicImport实现原理