前端项目中经常使用到开源的 npm 包,而现代前端框架的包中往往会提供不同格式的包(esm的,cjs的),如果我们在使用时没有正确的导入,便可能导致构建出来的包包含我们不需要的的内容。
举个例子@ant-design/icons一个前端经常用到的图标库,在包里提供了es和lib两个目录,平时我们可能是如下使用,可是我们这样导入的代码到底是es目录下的esm格式的代码还是lib目录下的cjs格式的代码呢,esm格式的可以进行tree shaking,而cjs的却不行,这是有很大区别。
import { TagOutlined } from '@ant-design/icons';
如果我们按照如下方式导入,又是导入的什么呢
import { TagOutlined } from @ant-design/icons/lib
上面两种导入方式都可以正常的运行,可如果我们不小心在代码里使用了第二种导入方式,恭喜你,项目的构建体积又变大了不少。所以,理解webpack模块的解析流程是非常重要的。
在我们使用的前端库的package.json中,通常可能包含main,module,browser,exports中的一个或多个字段,webpack在进行模块解析时便与这几个字段息息相关。
假如某个叫icons库的package.json中是这样的,其中exports,browser,module可能不存在
"main": "./main.js",
"module": "./module.js",
"browser": "./browser.js",
"exports": "./exports.js",
然后我们像下面这么引入
import icons from "icons";
那么解析的流程便是如下:
1、判断是否存在exports字段,没有跳到步骤2,否则判断对应值./exports.js文件是否存在,存在引入./exports.js,如果有不存在,判断./exports.js/index.js是否存在,存在引入./exports.js/index.js,否则跳到步骤6;
2、判断是否存在browser字段,没有跳到步骤3,否则判断对应值./browser.js文件是否存在,存在引入./browser.js,如果有不存在,判断./browser.js/index.js是否存在,存在引入./browser.js/index.js,否则跳到步骤5。(如果构建目标不是web或webworker,会跳过browser字段);
3、判断是否存在module字段,没有跳到步骤4,否则判断对应值./module.js文件是否存在,存在引入./module.js,如果有不存在,判断./module.js/index.js是否存在,存在引入./module.js/index.js,否则跳到步骤5;
4、判断是否存在main字段,没有跳到步骤5,否则判断对应值./main.js文件是否存在,存在引入./main.js,如果有不存在,判断./main.js/index.js是否存在,存在引入./main.js/index.js,否则跳到步骤5;
5、是否存在icons/index.js文件,如果存在,引入icons/index.js文件;
6、如果都没有,抛出错误;
当然,上面是一个都是字符串的情况,实际的情况可能更加复杂,一个比较完整的流程见下图,
上图流程对应的是webpack默认参数的解析流程,我们也可以自定义我们的参数,主要的几个参数如下
resolve.mainFields
默认值为 ['browser', 'module', 'main'], 如果我们改成 ['browser', 'main'] ,解析时便不会再关注module字段
resolve.exportsFields
默认值为[exports],解析过程中优先级最高的字段
resolve.aliasFields
默认值为['browser'],这个字段比较类似alias别名,可以注意到的是mainFields和aliasFields默认值都包含browser,很多博客都是把aliasFields的browser字段和aliasFields的browser混为一谈,其实在package.json中只有当这个字段是string的时候,才会当做是mainFields,如果是对象的话,可以把它当做一个alias,并不会存在同个字段同时在mainFields和aliasFields同时产生作用的情况
resolve.mainFiles
默认值是['index'],用于引入目录时的默认文件名,一般不会修改