你真正了解import/require的导包规则吗?

2,443 阅读6分钟

一、前言

作为前端开发者,和我们打交道最多的就是 npm 包,在项目中可以通过 import/require 导入 npm 包,但是突然有一天因为某个问题你想在项目中调试某个 npm 包,此时你就必须要知道我们导入的这些 Api 是来自哪个文件,这个文件有什么特点,比如 antd、mobx 他们都有几个不同的入口文件,通过阅读本篇文章你将会理解一下以下几个问题:

  1. 首先是为什么需要打包出不同版本的代码。
  2. 我们在不同环境下使用 import/require 到底引用的是哪个版本下的代码?
  3. 一般我们都会使用 webpack 和 babel 对我们的库进行编译打包,那这两者在处理模块化代码时分别扮演什么角色?

二、browser、module和main字段

我们要弄明白有些第三方库为什么需要打包出不同版本的代码,就的先了解 package.json 中这三个字段的作用,这三个字段分别指向我们最终打包生成的文件,我们拿 mobx 来举例:

image.png umd:main 其实指的就是 browser,下面来看下每个文件下的代码有什么不同。

  • browser
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (global = global || self, factory(global.mobx = {}));
}(this, (function (exports) {
    // ...
    exports.$mobx = $mobx;
    exports.FlowCancellationError = FlowCancellationError;
    exports.ObservableMap = ObservableMap;
    exports.ObservableSet = ObservableSet;
    // ...
    Object.defineProperty(exports, '__esModule', { value: true });

})));

在这个文件中定义的其实就是一个自执行函数,当执行这个函数时会将 mobx 中定义的 Api 全部绑定到 global.mobx 这个对象上,这样我们就能直接在全局对象上使用这些 Api。如果想直接在浏览器环境使用 mobx 我们可以直接通过 script 标签来引入:

 <script src=" https://unpkg.com/mobx/dist/mobx.umd.production.min.js"></script>
  • module
// ...
function autorun(view, opts) {
  var _opts$name, _opts;
  var name = (_opts$name = (_opts = opts) == null ? void 0 : _opts.name) != null ? _opts$name : process.env.NODE_ENV !== "production" ? view.name || "Autorun@" + getNextId() : "Autorun";
  var runSync = !opts.scheduler && !opts.delay;
  var reaction;

  if (runSync) {
    reaction = new Reaction(name, function () {
      this.track(reactionRunner);
    }, opts.onError, opts.requiresObservable);
  } else {
    // ...
    var scheduler = createSchedulerFromOptions(opts); // debounced autorun
  }
  // ...
  return reaction.getDisposer_();
}
export { $mobx, FlowCancellationError, autorun };

在这个文件中使用的是 ESM 模块化规范中的 export 将 mobx 中的 Api 进行导出。

  • main
// ...
function autorun(view, opts) {
  var _opts$name, _opts;
  var name = (_opts$name = (_opts = opts) == null ? void 0 : _opts.name) != null ? _opts$name : process.env.NODE_ENV !== "production" ? view.name || "Autorun@" + getNextId() : "Autorun";
  var runSync = !opts.scheduler && !opts.delay;
  var reaction;

  if (runSync) {
    reaction = new Reaction(name, function () {
      this.track(reactionRunner);
    }, opts.onError, opts.requiresObservable);
  } else {
    // ...
    var scheduler = createSchedulerFromOptions(opts); // debounced autorun
  }
  // ...
  return reaction.getDisposer_();
}
exports._startAction = _startAction;
exports.action = action;
exports.autorun = autorun;
exports.comparer = comparer;
// ...

main 字段指定的文件自然是使用 Commonjs 模块化规范的 exports 进行导出。

这里有一点我们需要了解下, 一般项目中使用 babel 编译代码时都会去屏蔽 node_modules 目录下的文件,因为按照约定发布到 npm 的代码都是基于 Commonjs 的 es5 语法的代码,因此配置 babel 插件屏蔽 node_mosules 目录可以极大的提高编译的速度。所以我们发布的包一定是基于 ESM规范/Commonjs规范的 es5 代码。

因此现在就清楚了这三个字段的作用,他们所指定的文件分别采用了不同的导出方式将第三方库中的 Api 进行导出(如果想打包出不同格式的包可参考microbundle),这也就解释了我们的第一个问题,因为我们开发的第三方库需要覆盖所有的使用场景,在支持 ESM 模块规范的场景中我们可以使用 module 所指定的文件,这样也可以很好的利用 ESM 静态分析的特性实现 tree-shaking 功能。

三、在使用import/require时是怎么知道要引入哪个文件下的代码?

上面讲过在 package.json 中可以指定三个入口 mainmodulebrowser,他们在不同的环境下有着不同的优先级,下将从以下几个环境进行介绍:

  1. webpack + web 环境 当使用 webpack 对代码进行编译打包时,他会按照 mainFields 字段的配置去引入相应的包,在默认情况或者 target 字段设置为 web 时加载包的顺序是 browser > module > main,详细配置可参考这里

  2. webpack + node 环境

在 node 环境中 webpack 的 target 字段将会设置为 node,此时优先级会是 module > main。

  1. 纯 node 环境 在纯 node 环境中不管是使用 import 还是 require 都将从 main 字段指定的 文件下面去引入。

四、webpack和babel在处理模块化时扮演什么角色?

在项目中我们一般都会使用 webpack 配合 babel 对我们的代码进行编译打包,但是对于 babel 来说他编译过后的代码都是 es5 的代码,这里也包括模块化的转化,这样也就享受不了 ESM 规范带来的特性,下面将详细介绍这两者的关系。

1、webpack模块化原理

webpack 本身自带了一套模块化系统,这套系统兼容了前端大多数模块规范,包括 AMD、Commonjs、ESM 等,下面通过一个具体的例子来了解一下:

// a.js
export const fun1 = () => {
    console.log('a')
}

// b.js
export const fun2 = () => {
    console.log('b')
}

// index.js 入口文件
import {fun1} from './a';
import {fun2} from './b';
const a = 1;
fun1();
fun2();

最终编译后的代码如下:


 (() => {
	"use strict";
        // 使用一个对象来保存每个模块的代码,对象的key是这个模块的相对路径,value就是一个一个函数,接收__unused_webpack_module,__webpack_exports__,__webpack_require__作为参数。
 	var __webpack_modules__ = ({

            "./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
               __webpack_require__.r(__webpack_exports__);
               var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
               var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/b.js");
               const a = 1;
               (0,_a__WEBPACK_IMPORTED_MODULE_0__.fun1)();
               (0,_b__WEBPACK_IMPORTED_MODULE_1__.fun2)();
            }),

            "./src/a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                __webpack_require__.r(__webpack_exports__);
                __webpack_require__.d(__webpack_exports__, {"fun1": () => (fun1)});
                const fun1 = () => {
                    console.log('a')
                }
            }),

            "./src/b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                __webpack_require__.r(__webpack_exports__);
                __webpack_require__.d(__webpack_exports__, {"fun2": () => (fun2)});
                const fun2 = () => {
                     console.log('b')
                }
            })

        });
        var __webpack_module_cache__ = {}; 	
        function __webpack_require__(moduleId) {
            var cachedModule = __webpack_module_cache__[moduleId];
            if (cachedModule !== undefined) {
                    return cachedModule.exports;
            }
            var module = __webpack_module_cache__[moduleId] = {
                    exports: {}
            };
            __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
            return module.exports;
        }
        // 执行入口文件
 	var __webpack_exports__ = __webpack_require__("./src/index.js");
 })()

上面这段代码就是 webpack 编译后的代码,其中就包含了 webpack 自身的模块化机制的实现 _webpack_require_ 函数,这个函数就是 require 或者 import 的替代品,我们看到他接受的参数其实是对应模块的相对地址,而模块的具体实现被保存到了一个以该模块的相对地址为 key 的对象中,这样我们在 _webpack_require_ 中可以直接去这个对象中获取其代码然后执行。

当我们执行某个模块的代码时会将 module、module.exports、_webpack_require_ 作为参数传进去,这样我们就能在外层拿到该模块导出的数据。

2、babel模块化

通过上面的讲解我们知道 webpack 已经能够很好的解决模块化的问题,那么 babel 在模块化中扮演什么角色呢?首先 webpack 只能对模块化的语法进行转化,但是其余的es6语法还需要做兼容处理,babel 就使用于专门处理 es6 转 es5 的,这当然也包括了模块化语法的转化,即 import -> require,这样 webpack 就无需再做其他的处理直接使用自身的模块化机制进行处理,而 webpack 多出来的处理其实就是静态分析,方便做 Tree-shakig。

所以为了同时利用 webpack 的模块处理机制和 babel 的 es6 转换能力,我们需要对 babel 配置作如下更改:

use: { 
    loader: 'babel-loader', 
    options: { 
        presets: [['babel-preset-es2015', {modules: false}]]} 
    }