Three.js: 导入示例 jsm 模块导致打包器将三个.js 源代码打包两次

创建于 2019-09-12  ·  43评论  ·  资料来源: mrdoob/three.js

three/examples/jsm/.../<module>导入会导致打包程序(使用汇总测试)两次(或多次)包含库。

例如,在执行import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' ,捆绑器将遵循导入,而在 OrbitControls.js 中,导入来自../../../build/three.module.js 。 但是,(外部)打包程序无法知道../../../build/three.module.jsthree是同一个模块。

对此的解决方案是将示例模块视为外部包并从three而不是../../../build/three.module.js导入。 这可能会破坏three.js 的rollup 配置,但是应该可以告诉rollup three是三个的主要入口点( src/Three.js )的别名。

最有用的评论

我认为这只是习惯了。 现在我想我明白了,我对它的样子很好。

顺便说一句,我更新了 Threejsfundamentals 以全部基于 esm 所以 🤞

所有43条评论

(通过汇总测试)

我无法通过汇总确认这一点。 如果您像在以下项目设置中那样做,一切都会按预期进行。

https://github.com/Mugen87/three-jsm

如果您将three视为外部依赖项:

export default {
    input: 'src/main.js',
    external: ['three'],
    output: [
        {
            format: 'umd',
            name: 'LIB',
            file: 'build/main.js'
        }
    ],
    plugins: [ resolve() ]
};

那么输出不应该包含three.js的源代码,但它包含所有内容。

但是,如果您不导入 OrbitControls,则输出将仅包含main.js文件的源代码。

您可以通过注释掉OrbitControls import来尝试一下,然后再次构建(但使用'three'作为外部依赖项)。

这与#17220 相关——那里提出的解决方案之一是用模块构建路径替换package.jsonmain字段,但这并不能解决这个用例。

只是要清楚这里的问题是,虽然three被标记为外部以构建一个单独的包,该包依赖于汇总配置中的三个,该包没有捕获对../../../build/three.module.js的硬引用和将其包含在构建中。 例如,构建以下文件将在不经意间包含 OrbitControls 代码 _and_ thethreejs 代码在包中,并且在使用@adrian-delgado 发布的配置构建时导入三个的另一个副本。

// src/main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

console.log(THREE, OrbitControls);

@adrian-delgado 可能值得注意的是,即使OrbitControls.js的路径更改为three OrbitControls 仍将包含在您的包中,这可能是需要的,也可能是不需要的,并且至少可能导致OrbitControls 代码在相关应用程序中包含两次。

我并不是要将此作为长期或最佳解决方案提出,而是将配置更改为将OrbitControls (以及三个文件夹中的所有文件)标记为 external 可以在两种情况下解决此问题:

export default {
    // ...

    external: p => /^three/.test(p),

    // ...
};

我并不是要将此作为长期或最佳解决方案提出,而是将配置更改为将 OrbitControls(以及三个文件夹中的所有文件)标记为外部将在两种情况下解决此问题:

出于某种原因,我希望汇总默认情况下也将'three/examples/jsm/controls/OrbitControls.js'视为外部。 所以你提出的解决方案适合我的用例。

相关的#17220 非常相关。 对话可能应该在那里继续。

那么如果你这样做会发生什么?

// src/main.js
import * as THREE from 'three/build/three.module.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

console.log(THREE, OrbitControls);

它可以工作,但它不可行,因为任何其他依赖于三的库或代码段将从“三”导入,然后再次中断。 Package.json 通常会告诉环境如何解决,“build/three.module”是一个不应泄露的分发细节。 当跳过解析时,只会引起命名空间问题。

  external: p => /^three/.test(p),

@gkjohnson如果用户想在包中包含“三个”实例和OrbitControls怎么办?

如果您尝试像这样实时使用模块,则不确定是否会发生类似的情况

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

两次加载three.js,一次来自CDN,一次来自threejs.org

也许这不是模块应该与三个一起使用的方式,但从 106 年之前开始,有 1000 多个站点和示例可以使用

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

所有示例都显示使用实时模块而不是构建(捆绑),因此从某种意义上说,它们并没有像以前那样显示使用three.js 的实际方法。 换句话说,旧的例子是开箱即用的。 新示例不是 AFAIK。 为了使示例工作,您需要从示例中提取 JavaScript 并放入一个单独的 .js 文件中,然后将三个.js 放在本地(可能通过 npm)。 修复示例中的所有路径,使其成为基于包的路径(没有 ../.././build),最后使用汇总

与非模块版本相比,这是相当大的变化,只需更改路径就足够了

@mrdoob

使用@adrian-delgado 的原始配置,three.js 将被包含一次,轨道控制将被包含一次,并且不会将任何包标记为外部。 使用我提出的配置,生成的包中将存在对three/build/three.module.jsthree/examples/jsm/controls/OrbitControls.js的外部依赖。

@EliasHasle

如果用户想要在包中包含“三个”实例和 OrbitControl 怎么办?

然后应排除external字段,在这种情况下,捆绑包中将包含三个和轨道控件的单个副本。 rollup-plugin-node-resolve (这是 rollup 支持模块解析所必需的,并且在上面的配置中使用)默认使用 package.json 的 module 字段(参见mainFields选项)所以轨道控制三个引用,“三个”将解析为相同的脚本。 _如果mainFields更改为["main", "module"]因此在 package.json_ 中使用“main”而不是“module”,那么这里将包含三个副本的两个副本,并且事情将按照以前的方式中断前面提到过。 但是,它确实需要更改该字段。 但是,如果使用“main”,则可能还需要rollup-plugin-commonjs ,因为 rollup 不知道如何处理默认使用 require 的 commonjs 文件。

@greggman

不幸的是,我认为在这种情况下,简单地替换模块不会那么容易。 没有一个提议的解决方案可以解决这个问题,我认为目前没有任何官方可以用来帮助从不同的主机导入核心脚本和示例的情况。 据我所知,导入地图是唯一可以帮助解决此问题的方法。 如果示例和三个都从同一主机导入,则只会加载三个的单个副本:

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/examples/jsm/controls/OrbitControls.js';

// or

import * as three from 'https://threejs.org/build/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

根据用例,是否最好继续使用经典脚本标签?

@greggman

如果您尝试像这样实时使用模块,则不确定是否会发生类似的情况

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

是的...不要使用那样的模块 😁

是的...不要使用那样的模块 😁

同意。 可以说,文档和示例主要针对缺乏经验的开发人员,事实上 jsm 示例是默认的,没有构建器它们都不能工作,也不能通过任何 CDN 工作,这是一种巨大的变化。

过去,您基本上可以在示例上查看源代码,复制并粘贴到 jsfiddle/codepen 等中,修复脚本标签中的路径,然后它就会运行。 现在所有的例子都不会运行,除非你直接链接到three.js站点并在每次版本被碰撞时观察它们的破坏。 (是的,我知道存在非模块示例,但那些不是从 https://threejs.org/examples 链接的示例)

@gkjohnson

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/examples/jsm/controls/OrbitControls.js';

不起作用,OrbitControls 不在 CDN 上,并且 OrbitContrls ../../../bulild/three.js 中的路径不是使其工作的正确路径

// 或者

import * as three from 'https://threejs.org/build/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js'

也不起作用,因为它会在每次 Three.js 推送新版本时中断

也许将 examples/js 文件夹推送到 CDN 和三个 CDN,以便仅修复示例代码中的 url 仍然有效? 这意味着三个.module.js 需要在

https://cdnjs.cloudflare.com/ajax/libs/three.js/108/build/three.module.js

build添加到路径中

过去,您基本上可以在示例中查看源代码,复制并粘贴到 jsfiddle/codepen 等中,修复脚本标签中的路径,然后运行...

我认为我们需要导入地图来做任何有用的事情,无论好坏。

现在,除非您直接链接到three.js站点,否则所有示例都不会运行

我真的不鼓励任何人直接链接到 Threejs 站点上的实时脚本......这永远不是一个好主意。 根据上面的评论,有版本化的替代方案。

理想情况下,可以回答这些问题的文档是通过模块导入页面。 是否有我们应该在那里报道的情况? 我想提及 CDN 是个好主意。

提及 CDN 是个好主意。 还提到 Cloudflare CDN,Google 上的第一个热门,对模块没有好处(除非发生变化)

@greggman

过去,您基本上可以在示例上查看源代码,复制并粘贴到 jsfiddle/codepen 等中,修复脚本标签中的路径,然后它就会运行。

我是支持你的。 模块最糟糕的部分是你不能再从示例中的控制台访问camerarenderer了😟

我们开始使用 unpkg 怎么样?

你的意思是开始在文档中使用它,比如通过模块导入页面,或者以某种方式在项目中使用它?

模块最糟糕的部分是您无法再从示例中的控制台访问相机或渲染器

是的,这令人沮丧。 在本地开发时,我一直在将这个(或类似的)放到示例中:

Object.assign( window, { camera, renderer, scene } );

我认为这是我们希望通过开发工具扩展解决的问题?

一个需要进行一些调查的想法,但可能很有趣……如果我们愿意为所有示例添加导入地图 polyfill ,我认为我们可以使在那里使用的导入与 npm 100% 复制/粘贴兼容-和基于打包器的工作流。 例如:

<script defer src="es-module-shims.js"></script>
<script type="importmap-shim" src="importmap.dev.js"></script>

<!-- ... -->

<script type="module-shim">
  import { Scene, WebGLRenderer } from 'three';
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

  // ...
</script>

我们开始使用 unpkg 怎么样?

你的意思是开始在文档中使用它,比如通过模块导入页面,或者以某种方式在项目中使用它?

而不是指向https://threejs.org/build/。 目前我们在ISSUE_TEMPLATE 中使用该链接。

@greggman可能会从https://cdnjs.cloudflare.com/ajax/libs/three.js/108/切换到https://unpkg.com/[email protected]/

看起来 unpkg 解决了我们在这里讨论的问题。

是的,这令人沮丧。 在本地开发时,我一直在将这个(或类似的)放到示例中:

Object.assign( window, { camera, renderer, scene } );

啊! 哈哈

我认为这是我们希望通过开发工具扩展解决的问题?

是的! 🤞

@greggman

如果您尝试像这样实时使用模块,则不确定是否会发生类似的情况

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

是的...不要使用那样的模块 😁

所以今天我发现自己就是这样做的......

在我的情况下,我从dev导入three.module.jsdev导入three.module.js OBJLoader masterOBJLoadermaster OBJLoader导入three.module.js所以BufferGeometry没有新的usage属性,而WebGLRenderer有不渲染网格,因为它没有找到usage ,但其他一切都有效😶

这毛够长的...

我认为这只是习惯了。 现在我想我明白了,我对它的样子很好。

顺便说一句,我更新了 Threejsfundamentals 以全部基于 esm 所以 🤞

看起来确实有一个three.module.min.js可能很好(或者是three.min.module.js 😜)

+1

我只是将三个 & 轨道控件作为 ES6 模块导入 & 因为(似乎)轨道控件指的是构建文件夹中的三个,所以我花了一段时间才弄清楚我的路径

超级粉丝,我们可以使用三个作为模块,但是在这方面有更多的灵活性会很好,我不会进入轨道控制文件并开始搞砸,假设其他模块也是这种情况。

也 +1 为一个 Three.min.module.js 😎

从#18239移动,我得到了做抓到了类似的问题npm link另一个程序包使用three.js所。

我开发了一个插件三缩小器,它可能有助于解决这个问题。

我面临同样的问题。 我正在使用three.js 编写一个React 组件,并且我正在从示例中导入一些模块。 一旦它与 rollup 捆绑在一起,如果我查看捆绑包,我可以看到有一个用于三个的 import 语句,然后是 Three.js 代码。

如果我在我的组件中使用这个导入语句: import * as THREE from "three/build/three.module"
一切正常,但三被嵌入到捆绑包中,这是我不想要的。
我想要三个的导入语句。 如果我使用import * as THREE from "three ,则该包将有三个作为模块导入,但是只要我使用其中一个示例,就会在包中添加三个.js(=我有一个用于三个的导入语句,然后是三个)的代码,这最终导致我的代码被破坏

@chabb

我正在使用three.js 编写一个React 组件,并且我正在从示例中导入一些模块。 一旦它与 rollup 捆绑在一起,如果我查看捆绑包,我可以看到有一个用于三个的 import 语句,然后是 Three.js 代码。

此处发布的解决方案应该可以解决您的问题: https :

我觉得很多这些问题源于人们没有完全理解他们的打包器发生了什么(这是可以理解的),但这些问题并不是三个问题所独有的。 但是,有可能意外地双重导入三个核心比其他库更引人注目。 捆绑一个像 lodash、react 组件或 OrbitControls 这样的外部依赖项可能更容易被遗漏。

根据外封装关于汇总记录了此行为,并提供了一个选项,在这里和的WebPack也有类似的选项,在这里。 在这种情况下,如果示例文件改为引用“三个”,那么虽然核心库不会被捆绑,你仍然会得到重复的示例代码包,这是它自己的问题。 而且我不认为这个项目可以做任何事情来帮助捆绑器解释 npm 链接陷阱。 我认为我见过的唯一有问题的案例不是错误配置的打包程序的结果是代码沙盒案例。

对于捆绑器案例,答案可能是记录、添加故障排除指南或链接到如何在通过模块导入页面上配置常见捆绑器。

我有一种预感,如果examples/jsm包可以改变这种模式......

// <strong i="7">@file</strong> GLTFLoader.js

// Before
import { Mesh } from '../../build/three.module.js';

// After
import { Mesh } from 'three';

...这些问题会更容易解决。 不幸的是,如果没有复杂的构建设置,我不知道我们将如何管理 Threejs 网站中的 HTML 示例。 Threejs 网站上的导入地图polyfill 可能会解决它,但我不确定。 :/

如果示例文件改为引用“三个”,那么虽然核心库不会被捆绑,你仍然会得到重复的示例代码包......

我不太遵循这一点。 因为它们是相对路径导入? 我们可以使它们与包相关。

@donmccurdy

我有一种预感,如果 examples/jsm 包可以改变这种模式……这些问题会更容易解决。

我认为这会使它看起来已解决,但人们仍然会有重复的代码,因为它不会导致应用程序中断,因此很难注意到这些代码。

我不太遵循这一点。 因为它们是相对路径导入? 我们可以使它们与包相关。

对不起,如果我不清楚,我认为这有点难以解释——希望这更清楚一点。 我将使用 Rollup 案例:

在上述情况下,人们希望将three标记为外部的包汇总,我假设他们正在构建一个库,其中three.js 将是另一个应用程序可以依赖的对等依赖项:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { stuff } from './local/src/index.js';

// library code with exports...

此处的目标是将上述three.js 导入保留在库中,并将包加载三个和OrbitControls 作为对等依赖项,因此如果应用程序还使用three.js 和OrbitControls,则您不要导入两次。

人们希望external: [ 'three' ]选项为他们实现这种行为(我当然做到了),但事实并非如此,因为该字符串与 OrbitControls 导入路径不匹配。 这导致OrbitControls被无意捆绑,因此../../../build/three.module.js被捆绑(因为它也不匹配字符串)。 我认为人们指出了被捆绑的three.js 核心文件,因为它更引人注目——应用程序中断,库包更大,等等——现实是_the OrbitControls 文件不应该被捆绑在首先。_这里配置 Rollup 的正确方法是将选项设置为external: path => /^three/.test( path )

这不是三个独有的。 Rollup 在其文档中使用 lodash作为示例,但是如果'lodash/merge'被捆绑在您的库代码中将很难/不可能注意到,因为它非常小并且不会导致重复的导入错误。 Material UI鼓励导入中的嵌套文件引用,同样设置external: ['@material-ui/core']将无法从包中排除'@material-ui/core/Button'

我认为不值得为这些用例更改示例代码,因为如果正确配置了 bundler,它仍然会导致重复代码。

这里有两种情况:

(1) 用户想要包含一次的 Threejs 和示例,得到两次

例如,在构建应用程序时。

(2) 用户想要 Threejs 和示例包含 0 次,得到 1+ 次

例如,在构建具有三个作为外部或对等依赖项的库时。


据我所知,(1)和(2)仍然是容易被绊倒的问题? 如果上述方法解决了(1),那么仅此一项就很有帮助。 我不确定(2)。 也许应该在通过模块导入时提到/^three/.test( path )技巧?

@gkjohnson谢谢你的解释,它真的帮助我澄清了我的想法

在我的汇总配置中,我external这种方式定义了

[
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {}),
        ...other_stuff
      ]

我认为它会起作用,因为三个将被视为外部依赖项; 但正如你所提到的,你必须使用正则表达式(据我所知,我想这是因为这些例子正在做
import from "../../../build/three.module.js"; )。 所以我最终做了

external: p => {
      if ([
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {}),
        'prop-types'
      ].indexOf(p) > -1) {
        return true;
      }
      return /^three/.test(p) ;
    }

这是一个无关紧要的问题,但我希望我在package.json中声明的所有依赖项都不是包的一部分? 这是一个正确的假设吗?

@donmccurdy

据我所知,(1)和(2)仍然是容易被绊倒的问题?

在我看来 (2) 是错误配置捆绑器的结果,也许我们可以通过更新文档并为捆绑器提供一些建议来解决这个问题。 (1) 可能是由于使用存在问题 (2) 的包而发生的,但除此之外,我不相信 (1) 很容易被发现。 我想看到一个真实世界的用例来演示这个问题,看看有人如何配置他们的捆绑器,但这里有一个我知道的方法列表(到目前为止):

  1. 'three/src/Three.js''three/build/three.min.js'显式导入(文档中不推荐这样做)。
  2. 重新配置您的打包器以在解析时使用package.main字段而不是package.module字段。 然而,默认情况下,三大打包工具RollupWebpackParcel都更喜欢module不是main 。 这个用例感觉并不常见,但这只是一个假设。
  3. 使用npm link包含一个依赖于三个的符号链接包(这是通过使用汇总的preserveSymlinks选项修复的)
  4. codeandbox.io 中使用三个和示例,因为平台优先考虑main 字段而不是 module

数字 4 似乎是唯一一个很容易被偶然发现的,尽管我知道人们正在为摇树做 1。 其他人觉得他们不在我们的控制范围内,或者非常罕见。

@chabb

据我了解,我想这是因为这些例子正在做import from "../../../build/three.module.js"; ...

情况并非如此,请阅读我在这里解释的内容: https : /^three有效,因为它匹配字符串'three/examples/jsm/controls/OrbitControls.js' ,该字符串也应该是外部的,因为它是three.js 库的一部分,而字符串'three'不是。 其他依赖项也可能发生同样的情况。 我建议对所有依赖项使用正则表达式以避免其他未知的陷阱或匹配任何带有裸模块说明符的包。

@gkjohnson感谢您的详细解释,这对我来说很有意义。

听起来这毕竟不能解决这个线程中的问题,但是由于我已经在线程中提到过几次,我终于测试了一个导入映射 polyfill: https : import * as THREE from 'three';可以在网络浏览器中工作了。

如果只有浏览器表现出一些信心......
https://github.com/WICG/import-maps/issues/212#issuecomment -663564421

我在向我的一个项目添加 pass 子类时遇到了同样的问题

import { /* stuff */ } from 'three'
import { Pass } from 'three/examples/jsm/postprocessing/Pass.js'

由于我更喜欢​​在我的模块中复制 Pass 代码,为了以后不必从浏览器上的 Three.js 导入它,我继续找到了一个解决方法:

const threeModulePath = path.resolve( __dirname, 'node_modules/three/build/three.module.js' );

export default {
    /* ..... */
    external: [ 'three' ],
    output: [
        {
            /* .... */
            globals : {
                'three': 'THREE',
                [ threeModulePath ]: 'THREE',
            }
        }
    ]
};

这样,它可以与浏览器一起使用,并且模块导入也应该可以正常工作。

编辑

从本地三个项目加载(参见下面的示例)将破坏这种方法并需要一些额外的解决方法。

"dependencies" : {
    "three": "file:../three.js"
}

好吧,我继续制作了一个支持本地链接的新版本:

const threeName = "three"; // Change with your three package name
const dependencies = require('./package.json').dependencies;
const splits = dependencies[threeName].split('file:');

const modulePath = (splits.length > 1) ?
    path.resolve(__dirname, splits[1], 'build/three.module.js'):                  // Resolve local path
    path.resolve(__dirname, 'node_modules', threeName, 'build/three.module.js');  // Resolve node_module path

const external = [
    threeName,
    modulePath,
]

const globals = {
    [threeName]: 'THREE',
    [modulePath]: 'THREE',
}

@Mcgode这已在https://github.com/mrdoob/three.js/issues/17482#issuecomment -530957570 中解决。 如果您正在使用 Rollup 并希望在使用示例模块时将three.js 标记为外部,您必须按照建议执行以下操作:

externals: p => /^three/.test(p),

没有理由让配置如此复杂。 这将确保 Pass.js 文件和three.js 模块都被标记为externel。

@gkjohnson我的用例并不完全相同,因为我只希望将three库标记为外部,而不是示例(我希望将示例与我的构建捆绑在一起)。

我正在构建一个带有三个作为外部的库,我希望将示例捆绑在构建的宽度但没有三个,并且如上所述,当从示例导入模块时,输出将包含三个的代码。 可以用 webpack 实现吗?

import {  } from "three";
import { Line2 } from "three/examples/jsm/lines/Line2";
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry";

@Mcgode @recardinal我认为这是不可能的。 我想做同样的事情,所以我只是复制/粘贴示例中的代码; 就我而言,我不得不“调整”进口和出口,仅此而已。 显然这并不理想,但对于我的用例来说已经足够了。

我在这里有一个类似的用例,Webpack 和 THREE 作为外部。 以下导入导致three.module.js 包含在捆绑输出中。

import * as THREE from 'three';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

我在某处读到 examples/js/* 将在某个时候被删除。 如果 jsm 示例在此之前“正常工作”,那就太好了。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

jack-jun picture jack-jun  ·  3评论

scrubs picture scrubs  ·  3评论

zsitro picture zsitro  ·  3评论

seep picture seep  ·  3评论

jlaquinte picture jlaquinte  ·  3评论