深度阅读<Javascript Modules 从IIFEs 到CommonJS 到 ES6 Modules>

栏目: JavaScript · 发布时间: 5年前

内容简介:原文:本文通过现代社会工厂生产一块手表的过程,引申出如何构建一个物理逻辑都隔离的模块,论述了其包含的思想原则。另外从js发展过程中为实现这些原则而不断做出的努力和尝试,通过了解这些历史,我们能更深入了解一块手表由成千上万个零部件构成,每一个零部件都有其自身的作用,并且如何与其它零部件搭配都有比较清晰的规定,把它们组装在一起就是一块手表,那这其中能给我们带来哪些启示呢?

原文: tylermcginnis.com/javascript-…

深度阅读<Javascript Modules 从IIFEs 到CommonJS 到 ES6 Modules>

本文通过现代社会工厂生产一块手表的过程,引申出如何构建一个物理逻辑都隔离的模块,论述了其包含的思想原则。另外从js发展过程中为实现这些原则而不断做出的努力和尝试,通过了解这些历史,我们能更深入了解 ES Modules 的设计原则,希望能够对我们平常编写代码提供一些启发。

一块手表由成千上万个零部件构成,每一个零部件都有其自身的作用,并且如何与其它零部件搭配都有比较清晰的规定,把它们组装在一起就是一块手表,那这其中能给我们带来哪些启示呢?

  • 可复用性
  • 可组合型
  • 中心化
  • 独立性

延伸到实际js开发中,对每个文件或者代码块的要求就是能够被重复使用,具有相对独立性(自己负责自己的一块),能够和相关模块进行组合,且整个模块有一个统一的调度中心负责去组合这些独立的模块。

IIFE

我们先看下原始时代,即Jquery还是巅峰的时代,那个时候我们是如何分割代码的,以下就是一个简单的增加用户,列举用户的一个curd例子

// users.js
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}
复制代码
// dom.js

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = window.getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
复制代码
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>
复制代码

看着代码好像我们是把文件分割开了,但实际上并没有,这种方式只是物理上看起来把项目分成多个模块,而其实他们都是挂靠在 window 对象上的,运行代码查看即可发现。那容易带来的问题就是,第三方可以随意去修改它们,回想下,是不是不符合模块独立性原则。同时这样也容易对window对象造成污染。

然后紧接着,我们想到既然不能放在 window 对象上,我们就自己定义一个变量,比如 App 来承载这些属性和方法,称之为 命名空间 。代码会变成如下这样

// App.js
var APP = {}
复制代码
// users.js
function usersWrapper () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
}

usersWrapper()
复制代码
// dom.js

function domWrapper() {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
}

domWrapper()
复制代码
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="app.js"></script>
    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>
复制代码

我们首先不讨论命名空间也容易被污染的问题,这种方式,我们的用户列表现在不容易被外部篡改以及增加用户的逻辑都放在 App 对象下,独立性有了保证,唯一多了 usersWrapperdomWrapper 两个包裹函数需要主动去调用下。相比之前有了很大改进。但这两个函数还是暴露在 window 对象上,后面就有了立即执行函数-IIFE。

// App.js
var APP = {}
复制代码
// users.js

(function () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
})()
复制代码
// dom.js

(function () {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
})()
复制代码

现在除了 App 变量还暴露在 window 对象上之外,另外两个函数都有了自己的独立的作用域,外部不能修改它们。虽然这种方式不是很完美,但是还是迈进了一大步。

CommonJS

后面Node.js出来了,有个CommonJS规范,能够导出一个方法或变量,在需要的文件中能够导入一个方法或变量,但它在现代浏览器中无法运行,且它是同步的,无法满足现代浏览器对性能的要求。基于此社区也出现了很多方案,最火的莫过于 webpack ,通过 webpack 你能将基于CommonJS规范编写的代码打包成一个bundle,在入口index.html文件中直接引用这个bundle即可。然而通过查看webpack编译后的代码你会发现本质上运用的还是IIFE模式,且最关键的还是CommonJS是同步的,不支持异步加载,另外就是它是运行时加载,无法做静态分析导致类如 tree shaking 等特性无法被满足。

(function(modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // Execute the module function
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
  }
  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;
  // expose the module cache
  __webpack_require__.c = installedModules;
  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(
        exports,
        name,
        { enumerable: true, get: getter }
      );
    }
  };
  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string')
      for(var key in value)
        __webpack_require__.d(ns, key, function(key) {
          return value[key];
        }.bind(null, key));
    return ns;
  };
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
  };
  // __webpack_public_path__
  __webpack_require__.p = "";
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({

/***/ "./dom.js":
/*!****************!*\
  !*** ./dom.js ***!
  \****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval(`
  var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
  function addUserToDOM(name) {\n
    const node = document.createElement(\"li\")\n
    const text = document.createTextNode(name)\n
    node.appendChild(text)\n\n
    document.getElementById(\"users\")\n
      .appendChild(node)\n}\n\n
    document.getElementById(\"submit\")\n
      .addEventListener(\"click\", function() {\n
        var input = document.getElementById(\"input\")\n
        addUserToDOM(input.value)\n\n
        input.value = \"\"\n})\n\n
        var users = getUsers()\n
        for (var i = 0; i < users.length; i++) {\n
          addUserToDOM(users[i])\n
        }\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),

/***/ "./users.js":
/*!******************!*\
  !*** ./users.js ***!
  \******************/
/*! no static exports found */
/***/ (function(module, exports) {

eval(`
  var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
  function getUsers() {\n
    return users\n}\n\nmodule.exports = {\n
      getUsers: getUsers\n
    }\n\n//# sourceURL=webpack:///./users.js?`);})
});
复制代码

ES Modules

为了解决以上种种问题,TC-39发布了 ES Modules ,对比以往,没有任何新的命名空间被创建,每个模块都是独立的,互不干扰的,可以随时被组合在一起。

// users.js

var users = ["Tyler", "Sarah", "Dan"]

export default function getUsers() {
  return users
}
复制代码
// dom.js

import getUsers from './users.js'

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
复制代码
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users">
    </ul>
    <input id="input" type="text" placeholder="New User"></input>
    <button id="submit">Submit</button>

    <script type=module src='dom.js'></script>
  </body>
</html>
复制代码

Tree Shaking

CommonJS modules 和 ES Modules有一个最大的不同,通过CommonJS你能导入任何模块在任何地点

if (pastTheFold === true) {
  require('./parallax')
}
复制代码

而ES Modules因为是静态的,只能在文件最开头导入

if (pastTheFold === true) {
  import './parallax' // "import' and 'export' may only appear at the top level"
}
复制代码

为什么这么设计呢,原因是静态分析,我们能够分析出导入的模块,如果有些模块没有被使用,我们通过tree shaking去除这些无用的代码,从而减少代码体积,进而提升运行性能,而CommonJS是动态分析的,就无法做到这一点,这也是为啥webpack后面版本才只是tree skaking特性的原因,因为它必须依赖于ES6 Modules静态编译特性。

Export Default的问题

Es Modules导出有export 和 export default两种方式,它们区别如下:

  • export与export default均可用于导出常量、函数、文件、模块等
  • 在一个文件或模块中,export、import可以有多个,export default仅有一个
  • 通过export方式导出,在导入时要加{ },export default则不需要
  • export能直接导出变量表达式,export default不行。

我这里主要想讲的是尽量减少export default的使用,理由如下:

  1. export default因为是整体导出,tree shaking无法分析哪些使用哪些没使用,从而无法减少无效代码
  2. 个人觉得代码应该符合一致性原则,由于export default导出在引入的时候可以随意命名使用变量,在团队分工从事的情况下,容易造成引入同一个模块命名不一样带来的代码前后不一致的问题。

以上就是我对整篇文章的深度阅读,希望这边文章对您在认识模块系统上有一定的帮助,如果喜欢我的文章,欢迎您的点赞!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

HTTP Developer's Handbook

HTTP Developer's Handbook

Chris Shiflett / Sams Publishing / 2003-3-29 / USD 39.99

The largest group with an unsatisfied demand for a good book on HTTP is the worldwide group of Web developers. A good book on HTTP can help new and old Web developers alike, as a thorough understandin......一起来看看 《HTTP Developer's Handbook》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具