从npm包的子文件夹中导入

55

我一直在创建一组小型的React组件库,以供其他多个项目使用。我正在内部发布该包(使用私有GitHub仓库),然后将其包含在另一个项目中。然而,当我尝试从包的子目录中导入时,路径不匹配,无法成功导入。

使用该包的项目都使用webpack来打包/转译代码,因为我尽可能避免在组件库中进行任何构建。

目录结构

- package.json
- src/
  - index.js
  - Button/
    - index.js
    - Button.jsx
    - ButtonGroup.jsx
  - Header/
    - index.js
    - Header.jsx (default export)

package.json

...
"main": "./src/index.js",
"scripts": "",
...

src/Button/index.js

import Button from './Button';
import ButtonGroup from './ButtonGroup';

export default Button;

export { Button, ButtonGroup};

src/index.js

如果只从子目录导入,那么这个文件是否真的必要?

import Button from './Button';
import ButtonGroup from './Button/ButtonGroup';
import Header from './Header';

export { Button, ButtonGroup, Header };

其他项目

// This project is responsible for building/transpiling after importing
import { Button, ButtonGroup } from 'components-library/Button';

例子

Material-UI 是一个React组件库,可通过以下方式进行调用:import { RadioButtonGroup } from 'material-ui/RadioButton'。我已经试图弄清楚这是如何工作的,但至今未果。

类似问题

  • 如何使用webpack在npm包子文件夹中导入模块?
    • 这几乎是我需要的正确方法,只是那里使用的导入路径涉及src/目录,而我想避免这种情况(应该是component-library/item,而不是component-library/src/item(尽管当前确实可以正常工作))
  • 发布扁平化NPM包
    • 这正是我想要的,只是我希望不必在包中有“构建”阶段(依赖于导入位置来构建/编译)

问题

  1. 我可以在导入路径中跳过src/目录吗?
  2. 我能否跳过包中的任何类型的构建阶段(以便开发人员在提交之前不必构建)?
  3. 与material-ui类似的软件包如何处理这个问题?

1
你是否发现了Material-UI是如何实现通过import npm-package/subfolder/Component的方式导入src/subfolder/Component组件的? - tonix
我最终转向其他选择,抱歉。 - Kendall
请问您能分享一下您的经验吗?我正在寻找类似的东西,但是找不到有用的资源。现在我正在打包一个我写的库,但是没有src目录 (https://github.com/tonix-tuft/react-js-utl)。我在项目的根目录下创建了ES6源代码的子文件夹,并将这些文件夹发布到NPM上,但我想这并不是最优解(例如,ES6导出在Node服务器端不起作用)... 不过这样做,我可以使用react-js-utl/subfolder语法导入所需的文件。顺便说一句,祝您新年快乐!谢谢。 - tonix
很不幸,我已经忘记了我最终做了什么,因为已经过去了一年半的时间:(。我希望有相关资源可以帮助解决这个问题,但我记得我没有能够及时找到答案,只好放弃了。 - Kendall
你可以查看我的回答:https://dev59.com/rFcP5IYBdhLWcg3weJ1R#61829655 - Muhammad Numan
6个回答

30

我能否在导入路径中跳过src/目录?

是的。可以使用"exports"字段来配置package.json,这个字段应该会在不久的将来被Webpack支持(请参阅这个issue),但已经得到Node 12 LTS的支持,遵循Bare Module Specifier Resolution proposal

package.json

...
"main": "./src/index.js",
"type": "module",
...
"exports": {
  "./Button": "./src/Button/index.js",
  "./Header": "./src/Header/index.js"
},
...

现在,以下代码:

// This project is responsible for building/transpiling after importing
import { Button, ButtonGroup } from 'components-library/Button';

应该被翻译为:

import { Button, ButtonGroup } from 'components-library/src/Button/index.js';

应该正确导入所请求的模块。

注意事项

现在,尝试使用更简单的版本可能会很诱人,如:

...
"exports": {
  "./Button": "./src/Button/",
  "./Header": "./src/Header/"
},
...

就像通常的导入语句一样

import { ... } from 'components-library/Button';

被翻译成

import { ... } from 'components-library/src/Button';

这看起来不错,但在这种情况下它将无法工作,因为您的子模块没有各自的package.json文件,而是依赖于它们的index.js文件来被找到。

/!\ 与CommonJS不同,没有自动搜索index.js或index.mjs或文件扩展名。


src/index.js - 如果仅从子目录导入,那么这个文件是否真的必要?

我不这么认为,但如果你想保留它,可以这样做。


我能跳过包中任何类型的构建阶段吗?

使用"exports"字段不需要转码您的代码。


好的回答。我在这里找到了更多信息:https://medium.com/swlh/npm-new-package-json-exports-field-1a7d1f489ccf - Toivo Säwén
2
Typescript用户:https://github.com/microsoft/TypeScript/issues/33079 - Toivo Säwén
太棒了!我多年后回到这里,有一个类似的需求,看到这个答案真是太高兴了,我之前并没有意识到exports可以使用多个文件! - undefined

2
答案可能取决于您安装组件库的方式。 如果您是通过npm install <git-host>:<git-user>/<repo-name>npm install <git repo url>进行安装的,则应按原样导入import {Button} from 'component-library/Button',就像根据您第一个链接的问题类似于Node的require()解析一样,Webpack应该相对于组件库的入口点解决组件库内子目录。您可以在webpack.config.resolve属性上找到有关自定义解析行为的文档。material-ui似乎依赖于从模块入口目录解析子目录导入。

  • 要分发ES模块库,无需在分发之前构建。 但是,例如create-react-app这样的项目可能需要预转换版本。

  • 另外,您可以编写import {Button} from'components-library'。Webpack将毫不费力地沿着每个没有烦恼的index跟踪依赖项。


    2

    其中一种可能的解决方案是使用webpack别名系统。
    您可以创建另一个项目,例如称为'app-aliases',以便您的别名可重用。
    此项目将具有一个包含所有软件包路径的js文件:

    const path = require('path');
    
    module.exports = {
    '@components': path.resolve(__dirname, 'node_modules/components-library/src'),
    '@another': path.resolve(__dirname, 'node_modules/any/path/you/want'),
    }
    

    然后将其添加到任何负责构建/转译的项目中的webpack配置文件中:
    webpack.config.js

    const appAliases = require('app-aliases');
    
    const config = {
    ...
      resolve: {
        alias: {
          ...appAlises
        }
      }
    }
    

    在运行时代码中,您将能够像这样使用它:
    import {Button} from '@components/Button';
    import {Something} from '@another'
    

    如果您正在使用TypeScript,则需要将相同的别名添加到paths tsconfig属性中。
    因此,对于您的问题的答案是:
    1. 是的,您可以在别名中使用任何路径
    2. 是的,不需要构建所有项目
    3. 我看到现在mui使用来自直接包(例如core)的导入,请参见https://material-ui.com/components/radio-buttons/,那里有import Radio from '@material-ui/core/Radio';。但我希望他们使用我下面描述的重新导出。
    此外,关于node.js的解析机制。
    当您导入某个库时,它会尝试找到node_modules/some-library/package.json,然后在其中查找main属性。该属性应该指向您的主入口点。通常是src/index.js(如果还没有设置,请在package.json中设置)。在这个文件中,您可以重新导出任何您想要从内部文件结构中使用的内容,并且可以不用完整路径使用它。 请参见此repo以获取一些示例。

    我无法在原始答案的上下文中验证此内容(因为我不再拥有该项目); 但是,它似乎与我现在了解的关于Webpack别名等的知识相匹配。 - Kendall

    2

    您需要安装babel-plugin-module-resolver软件包。

    在您的.babelrc文件中,像这样设置别名,指定软件包的相对路径:

    {
      "plugins": [
        ["module-resolver", {
          "alias": {
            "components-library": "./node_module/components-library"
          }
        }]
      ]
    }
    

    那么您可以像这样导入npm包的子目录:

    然后您可以像这样导入npm包的子目录:

    import { Button, ButtonGroup } from 'components-library/Button';
    

    1
    我是一名Angular开发者,从未使用过React,但我可以告诉你的是,Material-UI使用单一代码库(monorepo),这个概念在Angular中也存在,我们创建一个工作区,这个工作区包含多个项目/包,就像React中所命名的一样。更多信息请参见使用Yarn的工作区
    Material-UI在tsconfig中使用虚假路径,使其看起来像src文件夹不存在,这是你提供的git中的内容:tsconfig.json

    这个回答是否解决了用户的问题?您可能想要将您的技能和经验带到标记为angular的问题中。 - matty
    1
    是的,如果您按照我的答案操作,您可以创建一个与martial-ui项目相同结构的项目。他们所做的是包含多个软件包的工作区,这被称为单体库项目结构。第二部分问题是您可以将软件包导入为import { componentName} from '@company/workspaceName',这在tsconfig.json中完成,并给出了material-ui的示例: "paths": { "@material-ui/core": ["./packages/material-ui/src"], "@material-ui/core/": ["./packages/material-ui/src/"], .... } 这被称为创建虚拟路径。 - Hussein Akar

    0

    这是可能的,但需要发布一个经过筛选的dist文件夹,而不是项目的根目录。

    如果您了解模块解析的工作原理,整个过程就非常简单,您只需要编写一个小脚本来准备您的分发。

    为了避免重复所有细节,请参阅我的答案从JavaScript包的子文件夹导入


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接