Skip to content

前面我们学习过 nodejs 的cjs/esm互操作和main入口读取规律。这里我们看看打包工具的。

之前nodejs的入口加载规律

  • 先看是否有exports,有则以exports为准
  • 没有exports则以main字段为准。
  • nodejs里没有打包的概念,因此它只有“寻找入口文件是谁”、“按esm还是cjs解析某个文件”、“解析后导出的是什么(按照esm和cjs互操作规则)”

打包工具的入口加载问题

打包工具,它相当于自己实现了一套模块加载机制。基于此,它其实要比nodejs原生的更灵活,当然也会更复杂。因为打包工具它其实是要把任何模块化类型的都最终和转换成一个最终的模块化方式:

  • 例如你产物目标是esm,那么它就会把所有模块都转换成esm,然后导出esm。
  • 例如你产物目标是cjs,那么它就会把所有模块都转换成cjs,然后导出cjs。

无论你各个源码模块到底是啥格式,反正都是要经过rollup之类的先处理(而且是从代码转换层面,并不是从运行时层面)成其他模块化方式,那么它就能做到一些nodejs原生互操作和入口加载完全做不到的事情,

例如入口加载方面:

  • 可以让dep依赖包支持配置 browser和node字段。从而根据你rollup打包目标环境自动选择入口文件。这对于那种编写“同构包”的场景是万分有用的,例如axios。而Nodejs原生则只能根据import还是require区分一下加载esm场景和cjs场景而已。
  • 无论你依赖包里的main/module/exports配置的是啥扩展名。rollup都不care,他自己会匹配到入口后,直接看入口里的代码到底是用啥方式导出的从而确定是esm还是cjs,并按需转译到产物里。

而在esm和cjs互操作方面:

  • 他能解决调原生nodejs互操作的时候,cjs根本无法同步加载一个esm模块的问题(因为只能用import函数语法加载)。 而rollup他直接把esm打包转换成产物里的模块化方式,在产物内就会放上依赖包的代码,然后适当处理后被调用方直接同步引用。(不过rollup下cjs调用esm也会有问题,例如esm有默认导出和具名导出,但调用方require后是把具名导出和默认导出全放到一个对象上了。有时候默认导出是个函数的话,倒是可以让require的结果直接是那个函数,但如果是数值的话rollup就没法搞了,只能把{x:1, default: 3} 一起返回给调用方)。

打包工具的互操作规律

打包工具其实也遵循之前学习的nodejs的加载规律。即他们也会去识别main,另外他们打包工具还自己研究出来了module/browser这俩新的字段。分别代表当打包工具配置成“某个目标环境”的时候,则去找对应环境的入口。

对于rollup来说,要想加载node_modules下面的东西,需要用plugin-rosolve插件。安装后rollup的 plugin-rosolve插件里面就有个 browser字段,默认不配置的时候,它就是按照module/main方式去找。如果配置成true,他就去读依赖包里面package.json里面browser字段指定的那个入口(这对于同构包做node和前端区分非常有用)。

由于最新标准都建议用package.json里面的exports字段了,那我就用exports字段来讲解:

js
  "exports": {
    ".": {
      "node": {
        "import": "./final/nodeesm/app.js",
        "require": "./final/nodecjs/app.js"
      },
      "browser": {
        "import": "./final/browseresm/app.js",
        "require": "./final/browsercjs/app.js"
      },
      "default": {
        "import": "./final/nodeesm/app.js",
        "require": "./final/nodecjs/app.js"
      }
    }
  },
  "type": "module"

理论上,你的type是module的情况下,应该让cjs那种文件扩展名写成 cjs。 不过还好,rollup并不care这个,他rollup是这样操作的:

  1. 先看看调用方rollup配置的brower字段是否true。true的话就去找依赖包里的browser字段。
  2. 再看看你调用方是用import还是用require来调用依赖包,若是import那就找browser字段下面的import字段所指向的文件。
  3. 找到这个最终 final/browseresm/app.js,之后,他也不管你文件是啥扩展名。他就去你文件里面看文件内容分析这个文件是esm还是cjs,然后按需转成调用方需要的代码。假设这个final/browsersesm/app.js依赖文件是cjs写的,也没关系,那么你此时rollup配置里面就得加上 plugin-commonjs插件,他就能识别出这是个cjs模块。否则他构建时会报错:无法找到导出内容。
  4. 假设你的调用方项目是用cjs语法写的,那么他去依赖包里面寻找时,就是找 browser下 require字段下的 Final/browsercjs/app.js。这种cjs调用cjs/esm的情况,无论怎样你都得搞个 plugin-commonjs插件,否则他构建产物里就根本不会把依赖包打进去(就只会在产物中放一个require('dep')写法,将他作为一个external算是)。

由于我们使用rollup一般都是esm调用依赖包,所以我们主要关注上述第1、2、3步。

同构模块

例如axios这样的同构模块,你会发现他们是在dist里面构建了cjs版本和浏览器的cjs版本。然后开始利用上文提到的exports字段往外暴露。