【问题标题】:Webpack: Common chunks for code shared between Webworker and Web code?Webpack:Webworker 和 Web 代码之间共享代码的通用块?
【发布时间】:2018-08-27 11:52:36
【问题描述】:

我的浏览器应用程序的网络和网络工作者部分之间共享了大量代码。

我如何告诉 webpack 将我的代码分成公共块,以保证结果 100% 工作?

在我告诉 webpack 生成公共块(它确实如此)之后,webworker 代码中断(在运行时失败)。即使我修复了微不足道的“未定义窗口”错误,工作人员也什么都不做。

我相信这与 webpack 的“target”选项有关,默认设置为“web”。但我需要“网络”目标,因为我没有纯粹的网络工作者代码。

我也不能做多个 webpack 配置,因为我不能用多个配置做公共块的事情......

我该怎么办?

如果有人感兴趣:我正在尝试为我的应用构建一个最小尺寸的构建,其中包括 monaco 编辑器(提供工作人员):

https://github.com/Microsoft/monaco-editor/blob/master/docs/integrate-esm.md

您可以在此处(页面底部)看到入口点由 1 个主入口文件 + 工人组成。

由于我正在使用重复的代码,目前至少浪费了 6 MB,并且由于这个问题目前无法拆分。浪费了很多流量。

有什么想法吗? :)

我的 webpack 4.1.1 配置基本上是:

module.exports = (env, options) => {
    const mode = options.mode;
    const isProduction = mode === 'production';
    const outDir = isProduction ? 'build/release' : 'build/debug';

    return {

        entry: {
            "app": "./src/main.tsx",
            "editor.worker": 'monaco-editor/esm/vs/editor/editor.worker.js',
            "ts.worker": 'monaco-editor/esm/vs/language/typescript/ts.worker.js'
        },
        output: {
            filename: "[name].bundle.js",
            path: `${__dirname}/${outDir}`,
            libraryTarget: 'umd',
            globalObject: 'this',
            library: 'app',
            umdNamedDefine: true
        },
        node: {
            fs: 'empty' 
        },
        devtool: isProduction ? undefined : "source-map",
        resolve: {
            extensions: [".ts", ".tsx", ".js", ".json"],
            alias: {
                "@components": path.resolve(__dirname, "src/components"),
                "@lib": path.resolve(__dirname, "src/lib"),
                "@common": path.resolve(__dirname, "src/common"),
                "@redux": path.resolve(__dirname, "src/redux"),
                "@services": path.resolve(__dirname, "src/services"),
                "@translations": path.resolve(__dirname, "src/translations"),
                "@serverApi": path.resolve(__dirname, "src/server-api")
            }
        },
        optimization: isProduction ? undefined : {
            splitChunks: {
                minSize: 30000,
                minChunks: 1,
                name: true,
                maxAsyncRequests: 100,
                maxInitialRequests: 100,
                cacheGroups: {
                    default: {
                        chunks: "all",
                        priority: -100,
                        test: (module) => {
                            const req = module.userRequest;
                            if (!req) return false;
                            return (!/node_modules[\\/]/.test(req));
                        },
                    },
                    vendor: {
                        chunks: "all",
                        test: (module) => {
                            const req = module.userRequest;
                            if (!req) return false;
                            if (!/[\\/]node_modules[\\/]/.test(req)) return false;
                            return true;
                        },
                        priority: 100,
                    }
                }
            },
        },
        module: {
            rules: [...(isProduction ? [] : [
                {
                    enforce: "pre", test: /\.js$/, loader: "source-map-loader",
                    exclude: [
                        /node_modules[\\/]monaco-editor/ 
                    ]
                }
            ]),
            {
                test: require.resolve('jquery.hotkeys'),
                use: 'imports-loader?jQuery=jquery'
            },
            {
                test: /\.tsx?$/,
                loader: "awesome-typescript-loader",
                options: {
                    configFileName: 'src/tsconfig.json',
                    getCustomTransformers: () => {
                        return {
                            before: [p => keysTransformer(p)]
                        };
                    }
                }
            },
            {
                test: /\.(css|sass|scss)$/,
                use: extractSass.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {
                                minimize: isProduction
                            }
                        },
                        {
                            loader: "postcss-loader",
                            options: {
                                plugins: () => [autoprefixer({
                                    browsers: [
                                        'last 3 version',
                                        'ie >= 10'
                                    ]
                                })]
                            }
                        },
                        { loader: "sass-loader" }
                    ],
                    fallback: "style-loader"
                })
            },
            {
                test: /node_modules[\/\\]font-awesome/,
                loader: 'file-loader',
                options: {
                    emitFile: false
                }
            },
            {
                test: { not: [{ test: /node_modules[\/\\]font-awesome/ }] },
                rules: [
                    {
                        test: { or: [/icomoon\.svg$/, /fonts[\/\\]seti\.svg$/] },
                        rules: [
                            { loader: 'file-loader?mimetype=image/svg+xml' },
                        ]
                    }, {
                        test: { not: [/icomoon\.svg$/, /fonts[\/\\]seti\.svg$/] },
                        rules: [
                            {
                                test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
                                use: {
                                    loader: 'svg-url-loader',
                                    options: {}
                                }
                            },
                        ]
                    },
                    {
                        test: /\.(png|jpg|gif)$/,
                        loader: 'url-loader'
                    },
                    { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?mimetype=application/font-woff" },
                    { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?mimetype=application/font-woff" },
                    { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?mimetype=application/octet-stream" },
                    { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader" },
                ]
            },

            ]
        },
        plugins: [
            new HardSourceWebpackPlugin({
                cacheDirectory: '../node_modules/.cache/hard-source/[confighash]', configHash: function (webpackConfig) {
                    return require('node-object-hash')({ sort: false }).hash(Object.assign({}, webpackConfig, { devServer: false }));
                },
                environmentHash: {
                    root: process.cwd(),
                    directories: [],
                    files: ['../package-lock.json'],
                }
            }),
            new webpack.ProvidePlugin({
                "window.$": "jquery"
            }),
            new CleanWebpackPlugin(outDir),
            extractSass,
            new HtmlWebpackPlugin({
                title: 'my title',
                filename: 'index.html',
                minify: isProduction ? {
                    collapseWhitespace: true,
                    collapseInlineTagWhitespace: true,
                    removeComments: true,
                    removeRedundantAttributes: true
                } : false,
                template: 'index_template.html',
                excludeChunks: ['ts.worker', "editor.worker"]
            }),
            new webpack.IgnorePlugin(/^((fs)|(path)|(os)|(crypto)|(source-map-support))$/, /vs[\\\/]language[\\\/]typescript[\\\/]lib/)
        ].concat(isProduction ? [new webpack.optimize.LimitChunkCountPlugin({
            maxChunks: 1
        })] : [])
    }
};

【问题讨论】:

标签: javascript webpack bundle web-worker code-splitting


【解决方案1】:

您正在寻找通用library target,又名umd

这会在所有模块定义下公开您的库,从而允许 它可以与 CommonJS、AMD 和作为全局变量一起使用。

要让你的 Webpack 包编译成 umd,你应该像这样配置 output 属性:

output: {
    filename: '[name].bundle.js',
    libraryTarget: 'umd',
    library: 'yourName',
    umdNamedDefine: true,
},

Webpack 4 中有一个 issue,但如果您仍想使用它,可以通过在配置中添加 globalObject: 'this' 来解决此问题:

output: {
    filename: '[name].bundle.js',
    libraryTarget: 'umd',
    library: 'yourName',
    umdNamedDefine: true,
    globalObject: 'this'
},

【讨论】:

  • 将不起作用 :( 没有控制台错误,因此我不必手动更改全局对象。但工作人员仍然无法工作(摩纳哥编辑器不再强调错误,如果我不这样做,它会拆分块)。在开始帖子中添加了我的 webpack 配置
  • 很难说为什么它不起作用,尤其是如果你没有得到任何错误。你需要调试它:确保它把所有的块都带入了网络选项卡,然后放置一些断点,看看它失败的地方。
  • 它根本不执行,这就是它不工作的原因。代码永远挂起,等待其他永远不会到来的数据,因为它从未请求或执行过。
【解决方案2】:

这是一个非常糟糕的答案,但我已经设法在工作线程和主线程之间共享块。

线索是这样的

  1. globalObject 必须如上定义为(self || this)
output: {
    globalObject: "(self || this)"
}
  1. Webpack 加载带有document.createElement('script')document.head.appendChild() 序列的块,这在worker 上下文中不可用,但我们有self.importScript。所以这只是一个“polyfiling”它的问题。 这是工作“polyfill”(直接来自地狱):
console.log("#faking document.createElement()");
(self as any).document = {
    createElement(elementName: string): any {
        console.log("#fake document.createElement", elementName);
        return {};
    },
    head: {
        appendChild(element: any) {
            console.log("#fake document.head.appendChild", element);
            try {
                console.log("loading", element.src);
                importScripts(element.src);
                element.onload({
                    target: element,
                    type: 'load'
                })
            } catch(error) {
                element.onerror({
                    target: element,
                    type: 'error'
                })
            }
        }
    }
};
  1. 确保您的真实代码在安装polyfill 后被解析,通过使用动态导入,这将。假设正常的“worker main”在“./RealWorkerMain”中,那将是“main worker script”:
// so, typescript recognizes this as module
export let dummy = 2;

// insert "polyfill from hell" from here

import("./RealWorkerMain").then(({ init }) => {
    init();
});
  1. 您可能需要在 webpack 中配置动态 import,因为文档中的 here 也不容易,this answer 非常有帮助。

【讨论】:

  • 这是唯一有意义的答案,但天哪,这太丑了:(
  • @Zbigniew Zagórski 非常感谢你拯救了我的一天!关于 4. 我不需要插件 @babel/plugin-syntax-dynamic-import 但这个 @babel/plugin-transform-runtime 以避免 ReferenceError: regeneratorRuntime is not defined 错误。 (使用 babel 7.11.x)
【解决方案3】:

编辑:好吧,我根据大家的知识编写了一个 webpack 插件。

https://www.npmjs.com/package/worker-injector-generator-plugin

您可以忽略下面的内容,使用插件,或者如果您想了解插件是如何产生的并自己动手(这样您就不必依赖我的代码),您可以继续阅读。

================================================ ======

好吧,经过大量研究,我找到了这个解决方案,您需要创建一个注入文件,对于一个简单的案例,您需要 https://github.com/webpack-contrib/copy-webpack-plugin,因为它运行良好......所以假设您的设置是:

entry: {
    "worker": ["./src/worker.ts"],
    "app": ["./src/index.tsx"],
  },

你已经设置了你的常用插件,让我们说这个例子。

optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2
        },
      }
    }
  },

您现在需要创建一个注入“Vanilla JS”,它可能看起来像这样:

var base = location.protocol + "//" + location.host;
window = self;

self.importScripts(base + "/resources/commons.js", base + "/resources/worker.js");

然后你可以在你的工人旁边添加它,比如src/worker-injector.js

并使用复制插件

new CopyPlugin([
      {
        from: "./src/worker-injector.js",
        to: path.resolve(__dirname, 'dist/[name].js'),
      },
    ]),

确保您的输出设置为 umd。

output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    libraryTarget: "umd",
    globalObject: "this",
  }

这不过是一种 hack,但允许您按原样使用所有东西,而不必做一些夸张的事情。

如果您需要散列(因此复制插件不起作用)功能,您必须生成此文件(而不是复制它),请参阅此。

How to inject Webpack build hash to application code

为此,您必须创建自己的插件,该插件将生成 vanilla js 文件并考虑其本身的哈希值,您将传递要一起加载的 url,并将哈希附加到它们,这是更棘手,但如果你需要哈希,用你的自定义插件实现应该很简单。

遗憾的是,目前似乎没有其他方法。

我可能自己编写插件来解决问题并创建注入器,但我确实认为这更像是一种 hack,不应该成为公认的解决方案。

我以后可能会去写注入器插件,它可能是这样的:

类似new WorkerInjectorGeneratorPlugin({name: "worker.[hash].injector.js", importScripts: ["urlToLoad.[hash].js", secondURLToLoad.[hash].js"])

参考这个问题以供参考,以及为什么它应该在 webpack 中修复,而 WorkerInjectorGeneratorPlugin 之类的东西几乎是一个 hack 插件。

https://github.com/webpack/webpack/issues/6472

【讨论】:

  • 我的天啊,用 Monaco Editor 和 Webpack 和 Web Workers 数小时拉扯头发……终于找到你的答案和 npm 包……现在一切正常!
  • @OlivierLance :) 很高兴它有帮助,我拉了几天头发;尽管如此,这似乎并不是开发人员遇到的常见问题,因为它似乎仍未解决; webpack 应该能够为 webworker 目标提供一些 importScripts 和异步功能,以便嵌入代码;目前我制作它的方式让你依赖一个额外的文件(注入器);这基本上与普通网站通过获取源所做的事情相同;这个逻辑需要直接在 webpack 生成的文件中实现。但我们会等待 :)
  • 是的,我实际上想尝试另一种方法,使用一个类似于 MonacoEditorWebpackPlugin 的插件,通过使用似乎在编译时注入的内部 WebWorkerTemplatePlugin。我对 Webpack 内部不是很流利,所以这可能不可行,但我会尝试 :)
【解决方案4】:

在 webpack 5 中引入了 Native Worker 支持。使用此功能,您可以使用简单的 splitChunk 选项在应用程序代码和 webwoker 之间共享块

{
    optimization: {
        splitChunks: {
            chunks: 'all',
            minChunks: 2,
        },
    },
}

当将资产的新 URL 与新 Worker/new SharedWorker/navigator.serviceWorker.register 组合时,webpack 将自动为 web worker 创建一个新入口点。

new Worker(new URL("./worker.js", import.meta.url))

选择语法是为了允许在没有捆绑程序的情况下运行代码。此语法也可用于浏览器的原生 ECMAScript 模块。

https://webpack.js.org/blog/2020-10-10-webpack-5-release/#native-worker-support

【讨论】:

  • 我不明白这是如何工作的。 Webpack 5 确实会检测到 webworker 并生成它。但是 Webpack 5 不会在主应用程序和工作程序之间共享模块,而是在捆绑代码中,您在主应用程序中有 N 个模块的副本,在工作程序中有 N 个模块的副本。以下是澄清github.com/webpack/webpack/issues/6472#issuecomment-797078303
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-24
  • 1970-01-01
  • 2014-07-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多