如何在 Webpack Bundle 中包含手动 import()

How to include manual import() in Webpack Bundle

我是 Webpack 的新手,如果这是一个愚蠢的问题,请多多包涵。

我的目标是将我基于 AMD 的旧代码库转换为基于 ES6 模块的解决方案。我苦苦挣扎的是处理动态 import()s。所以我的应用程序路由器以模块为基础工作,即每个路由都映射到一个模块路径,然后 required。由于我 知道 将包含哪些模块,我只需将那些动态导入的模块添加到我的 r.js 配置中,并且能够在单个文件中构建所有内容,所有 require 调用仍然存在正在工作。

现在,我正在尝试对 ES6 模块和 Webpack 做同样的事情。对于我的开发模式,这没有问题,因为我可以将 require() 替换为 import()。但是我不能让它与捆绑一起工作。要么 Webpack 拆分了我的代码(仍然无法加载动态模块),要么 - 如果我对 entry 配置使用数组格式,则动态模块 包含在捆绑但加载仍然失败:Error: Cannot find module '/src/app/DynClass.js'

这是我的 Webpack 配置的样子:

const webpack = require('webpack');
const path = require('path');

module.exports = {
    mode: "development",
    entry: ['./main.js', './app/DynClass.js'],
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, "../client/")
    },
    resolve: {
        alias: {
            "/src": path.resolve(__dirname, '')
        }
    },
    module: {
        rules: [
            {
                test: /\.tpl$/i,
                use: 'raw-loader',
            },
        ]
    }
};

所以基本上我想告诉 Webpack:"hey, there is another module (or more) that is to be loaded dynamically and I want it to be included in the bundle"

我该怎么做?

所以,是的,经过多次摆弄之后,隧道尽头似乎出现了一盏灯。不过,这不是 100% 的解决方案,而且肯定不适合胆小的人,因为它非常丑陋和脆弱。但我还是想和你分享我的方法:

1) 手动解析我的路由配置

我的路由器使用的配置文件如下所示:

import StaticClass from "/src/app/StaticClass.js";

export default {
    StaticClass: {
        match: /^\//,
        module: StaticClass
    },
    DynClass: {
        match: /^\//,
        module: "/src/app/DynClass.js"
    }
};

因此,正如您所见,导出是一个对象,其键充当路由 ID,以及一个包含匹配项(基于正则表达式)和路由匹配时应由路由器执行的模块的对象。我可以为我的路由器提供一个构造函数(或一个对象)用于立即可用的模块(即包含在主块中)或者如果模块值是一个字符串,这意味着路由器必须动态加载这个模块使用字符串中指定的路径。

所以我知道哪些模块 可以 可能被加载(但不是如果和何时)我现在可以在我的构建过程中解析这个文件并将路由配置转换为 webpack 的东西可以理解:

const path = require("path");
const fs = require("fs");

let routesSource = fs.readFileSync(path.resolve(__dirname, "app/routes.js"), "utf8");

routesSource = routesSource.substr(routesSource.indexOf("export default"));
routesSource = routesSource.replace(/module:\s*((?!".*").)*$/gm, "module: undefined,");
routesSource = routesSource.replace(/\r?\n|\r/g, "").replace("export default", "var routes = ");

eval(routesSource);

let dummySource = Object.entries(routes).reduce((acc, [routeName, routeConfig]) => {

    if (typeof routeConfig.module === "string") {
        return acc + `import(/* webpackChunkName: "${routeName}" */"${routeConfig.module}");`;
    }

    return acc;

}, "") + "export default ''";

(是的,我知道这很丑而且有点脆弱,所以这肯定可以做得更好)

基本上我创建了一个新的,虚拟模块,其中每个需要动态导入的路由条目都被翻译,所以:

DynClass: {
    match: /^\//,
    module: "/src/app/DynClass.js"
}

变为:

import(/* webpackChunkName: "DynClass" */"/src/app/DynClass.js");

所以路由 id 只是成为块的名称!

2) 在构建中包含虚拟模块

为此我使用 virtual-module-webpack-plugin:

plugins: [
    new VirtualModulePlugin({
        moduleName: "./app/dummy.js",
        contents: dummySource
    })
],

其中 dummySource 只是一个字符串,其中包含我刚刚生成的虚拟模块的源代码。现在,这个模块被引入,“虚拟导入”可以被 webpack 处理。但是等等,我仍然需要导入虚拟模块,但我的开发模式中没有任何东西(我在本地使用所有东西,所以没有加载器)。

所以在我的主要代码中,我执行以下操作:

let isDev = false;
/** @remove */
isDev = true;
/** @endremove */

if (isDev) { import('./app/dummy.js'); }

当我处于开发模式时,“dummy.js”只是一个空的存根模块。在构建时(使用 webpack-loader-clean-pragma 加载器)删除了特殊注释之间的部分,因此当 webpack“看到”dummy.js 的导入时,这段代码将不会在构建本身中执行,因为那时 isDev 的计算结果为 false。而且由于我们已经定义了一个具有相同路径的虚拟模块,所以在构建时就像我想要的那样包含了虚拟模块,当然所有的依赖关系也被解析了。

3) 处理实际加载

对于开发来说,这很容易:

import routes from './app/routes.js';

Object.entries(routes).forEach(async ([routeId, route]) => {
    if (typeof route.module === "function") {
        new route.module;
    } else {
        const result = await import(route.module);
        new result.default;
    }
});

(请注意,这不是实际的路由器代码,足以帮助我完成 PoC)

好吧,但是对于构建我还需要其他东西,所以我添加了一些特定于构建环境的代码:

/** @remove */
const result = await import(route.module);
new result.default;
/** @endremove */

if (!isDev) {
    if (typeof route.module === "string") { await __webpack_require__.e(routeId); }
    const result = __webpack_require__(route.module.replace("/src", "."));
    new result.default;
}  

现在,dev环境的加载代码刚刚剥离出来,还有一个内部使用webpack的加载代码。我还检查模块值是函数还是字符串,如果是后者,我调用内部 require.ensure 函数来加载正确的块:await __webpack_require__.e(routeId);。还记得我在生成虚拟模块时命名了我的块吗?这就是为什么我现在还能找到它们!

4) 还有很多工作要做

我遇到的另一件事是,当几个动态加载的模块具有相同的依赖项时,webpack 会尝试生成更多名称如 module1~module2.bundle.js 的块,从而破坏我的构建。为了解决这个问题,我需要确保所有这些共享模块都进入我称为“共享”的特定命名包中:

optimization: {
    splitChunks: {
        chunks: "all",
        name: "shared"
    }
} 

并且在生产模式下,我只是在请求依赖它的任何动态模块之前手动加载这个块:

if (!isDev) {
    await __webpack_require__.e("shared");
}

同样,此代码仅在生产模式下运行!

最后,我必须防止 webpack 将我的模块(和块)重命名为“1”、“2”等名称,而是保留我刚刚定义的名称:

optimization: {
    namedChunks: true,
    namedModules: true
}

Se 是的,给你!正如我所说,这并不漂亮但似乎有效,至少在我简化的测试设置中是这样。我真的希望在我完成剩下的所有工作时(比如 ESLint、SCSS 等)没有任何阻碍!