前端模块化

80 阅读4分钟

模块化的发展历史

我们首先不聊webpack, rollup之类的打包工具,先了解下这些工具没出来之前前端项目是怎么进行开发的

  <script src="./src/index1.js"></script>
  <script src="./src/index2.js"></script>
  <script src="./src/index3.js"></script>

以前没有构建工具的时候,我们在页面上引用脚本只能这样引用,但这样引用就会出现许多问题,首先引用的顺序不能出错,因为你有些JS文件可能要依赖上一个文件中的变量,其次由于开发项目是多人开发,这样的话就可能出现全局污染的问题

比如小A在index1.js中定义了

const a = 1

小B在index2.js中定义了

const a = 2

这样首先会报语法错误,其次就算用var定义,后面的也会把前面的掩盖掉,就会出现全局污染的事情,有人说可以使用立即执行函数包裹起来,这样每个文件就是一个独立的作用域,但这样就会出现新的问题

  • 我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用
  • 代码写起来杂乱无章,每个文件中的代码都需要包裹在一个匿名函数中来编写
  • 没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况

虽然大公司会制定一些强规范来改善这些问题,但对于大多数公司协同开发依然是一件比较麻烦的事情

CommonJS规范

最早出现的是CommonJS规范,学过Node的都有了解过,Node就是使用的CommonJS规范,使用moduleexports,和require关键字进行模块化的开发

基本使用

// 在a.js中定义
const name = '张三'
const age = 23
module.exports = { name, age }

// 在b.js中使用
const { name, age } = require('./a.js')
console.log(name, age) // 张三, 23

CommonJS实现原理

首先我们看下a.js中,它把一个对象赋值给了module.exports,然后又通过require引入了这个对象,下面我们来大概模拟下这个过程

var modules = {
  './a.js': () => {
      var module = {}
      module.exports = { name, age }
      return module.exports
  }
}

function require (modulePath) {
  return modules[modulePath]()
}

其实源码的大体思路也是这样的,下面我们来看下源码中具体的实现过程

// 在demo1中定义
const name = '张三'
const age = 23
module.exports = {
  name,
  age
}

// 在main.js中定义
const { name, age } = require('./demo1')
console.log(name, age)

再用webpack打包,看下打包后的代码(删除了注释和换行符)

(() => {
  var __webpack_modules__ = {
    "./src/demo1.js": (module) => {
      eval(
        `const name = '张三'
         const age = 23
         module.exports = { 
          name,
          age
         }`
      );
    },

    "./src/main.js": (
      __unused_webpack_module,
      __unused_webpack_exports,
      __webpack_require__
    ) => {
      eval(
        `const { name, age } = __webpack_require__("./src/demo1.js")
         console.log(name, age)
        `
      );
    },
  };

  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/main.js");
})();

上面就是打包后的代码,把上述文件引用到html文件中就能打印出name和age

上述代码主要分为四个步骤

  • 初始化:定义 modules 对象
  • 定义缓存对象cache
  • 定义加载模块函数require
  • 执行入口函数 首先定义模块对象
(() => {
  var __webpack_modules__ = {
    "./src/demo1.js": (module) => {
      eval(
        `const name = '张三'
         const age = 23
         module.exports = { 
          name,
          age
         }`
      );
    },

    "./src/main.js": (
      __unused_webpack_module,
      __unused_webpack_exports,
      __webpack_require__
    ) => {
      eval(
        `const { name, age } = __webpack_require__("./src/demo1.js")
         console.log(name, age)
        `
      );
    },
  };

webpack会从入口文件触发,去找其依赖的模块,最终合并成一个模块对象

定义缓存对象(eval会把字符串当成JS代码执行)

var __webpack_module_cache__ = {};

定义加载模块函数

  function __webpack_require__(moduleId) {
    // 根据模块路径在缓存中查找
    var cachedModule = __webpack_module_cache__[moduleId];
    // 如果存在的话就直接返回exports对象
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // 不存在创建module对象
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},
    });
    // 然后调用modules中相应的函数给module对象赋值
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
  }

执行入口函数

 var __webpack_exports__ = __webpack_require__("./src/main.js");

ESModule规范

基本使用

// 在demo1.js中定义
const age = 23
export const name = '张三'
export default age 

// 在main.js中定义
import age, { name } from './demo1'
console.log(age, name)

下面我们看下webpack打包后的代码(去除了注释和换行符)

(() => {
  "use strict";
  var __webpack_modules__ = {
    "./src/demo1.js": (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      eval(
        `
          __webpack_require__.r(__webpack_exports__);
          __webpack_require__.d(
            __webpack_exports__,
            {
              "default": () => (__WEBPACK_DEFAULT_EXPORT__),
              "name": () => (name)
            }
          );
          const age = 23
          const name = '张三'
          const __WEBPACK_DEFAULT_EXPORT__ = (age);
        `
      );
    },

    "./src/main.js": (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      eval(
        `
          __webpack_require__.r(__webpack_exports__);
          var _demo1__WEBPACK_IMPORTED_MODULE_0__ = 
          __webpack_require__("./src/demo1.js");
          console.log(
            _demo1__WEBPACK_IMPORTED_MODULE_0__["default"],
            _demo1__WEBPACK_IMPORTED_MODULE_0__.name
          )
        `
      );
    },
  };

  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;
  }

  (() => {
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          });
        }
      }
    }
  })()

  (() => {
    __webpack_require__.o = (obj, prop) => 
    Object.prototype.hasOwnProperty.call(obj, prop)
  })()

  (() => {
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })()

  var __webpack_exports__ = __webpack_require__("./src/main.js")
})();

其实流程上基本和上CommonJS基本一样,不过在定义导出对象时有些许差别,还多了些辅助函数,下面我们看下具体的代码 首先我们先看下一些辅助函数

  // 检查是否是对象自身的属性
  (() => {
    __webpack_require__.o = 
    (obj,prop)=>Object.prototype.hasOwnProperty.call(obj, prop)
  })()
  
  // 给exports对象赋值格外的属性
  (() => {
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })()
  
  // 给exports对象属性设置get函数
  (() => {
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          });
        }
      }
    }
  })()

代码的大体思路都是一样的,都是建立exports对象

总结

写这篇文章的目的也是为了巩固自己的知识点,主要查考了不要秃头啊这位大佬的文章,我这篇文章介绍的比较简单,有兴趣的可以看下下面的文章链接

# 从构建产物洞悉模块化原理