你了解模块化吗?介绍模块化发展历程。

407 阅读10分钟

ps:其实一直都想写关于模块化的东西,刚刚好刷到一个题目是需要讲模块化的,就借着这个机会了解了解。

可从 IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module"> 这几个角度考虑。

原文地址:个人博客

IIFE(立即调用函数表达式)

是一个在定义时就会立即执行的 JavaScript 函数。

;(function() {
  statements
})()

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

CommonJS

  • 特点

    1. 同步加载,模块加载会阻塞接下来代码的执行,需要等到模块加载完成才能继续执行

    2. 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

  • 环境

    Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用 module.exports 定义当前模块对外输出的接口(不推荐直接用 exports),用 require 加载模块。

  • 语法

    在 CommonJS 中,有一个全局性方法 require(),用于加载模块。假定有一个数学模块 math.js,就可以像下面这样加载。

    require 命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存中取值。

    var math = require('math')
    

    调用模块里面的方法

    var math = require('math')
    
    math.add(2, 3) // 5
    

    定义和暴露模块:

    注意暴露出去的是值的拷贝也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

    // 定义模块math.js
    var basicNum = 0
    function add(a, b) {
      return a + b
    }
    
    module.exports = {
      //在这里写上需要向外暴露的函数、变量
      add: add,
      basicNum: basicNum
    }
    

AMD

AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。

  • 特点

    1. 它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

    首先我们说为什么是异步模式:

    var math = require('math')
    
    math.add(2, 3)
    

    第二行 math.add(2, 3),在第一行 require('math')之后运行,因此必须等 math.js 加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

    这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

    因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。

  • 环境

    浏览器环境

  • 语法

    AMD 也采用 require()语句加载模块,但是不同于 CommonJS,它要求两个参数:

    require([module], callback)
    

    第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数 callback,则是加载成功之后的回调函数。如果将前面的代码改写成 AMD 形式,就是下面这样:

    require(['math'], function(math) {
      math.add(2, 3)
    })
    

    math.add()与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。

    定义和暴露模块:

    // 无模块依赖
    define(function() {
      var add = function(x, y) {
        return x + y
      }
    
      return {
        add: add
      }
    })
    // 有模块依赖
    define([module1, module2], function(module1, module2) {
      // ...
      return module
    })
    
  • 应用

    目前,主要有两个 Javascript 库实现了 AMD 规范:require.js 和 curl.js。

CMD

  • 特点

    1. CMD 是在 AMD 基础上改进的一种规范,CMD 推崇依赖就近,延迟执行此规范其实是在 sea.js 推广过程中产生的。可以把你的依赖写进代码的任意一行。
  • 环境

    浏览器环境

  • 语法

    define(factory)
    

    factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require、exports 和 module。

    // CMD
    define(function(require, exports, module) {
      var a = require('./a')
      a.doSomething()
      var b = require('./b')
      b.doSomething()
    })
    
    /** sea.js **/
    // 定义模块 math.js
    define(function(require, exports, module) {
      var $ = require('jquery.js')
      var add = function(a, b) {
        return a + b
      }
      exports.add = add
    })
    // 加载模块
    seajs.use(['math.js'], function(math) {
      var sum = math.add(1, 2)
    })
    

UMD

  • 特点

    兼容 AMD 和 commonJS 规范的同时,还兼容全局引用的方式

  • 环境

    浏览器和服务器环境

  • 语法

    无导入导出规范,只有如下的一个常规写法:

    ;(function(root, factory) {
      if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory)
      } else if (typeof exports === 'object') {
        //Node, CommonJS之类的
        module.exports = factory(require('jquery'))
      } else {
        //浏览器全局变量(root 即 window)
        root.returnExports = factory(root.jQuery)
      }
    })(this, function($) {
      //方法
      function myFunc() {}
      //暴露公共方法
      return myFunc
    })
    

webpack(require.ensure)

  • 特点

    1. webpack2.x 代码分割。webpack 在编译时,会静态地解析代码中的 require.ensure(),同时将模块添加到一个分开的 chunk 当中。这个新的 chunk 会被 webpack 通过 jsonp 来按需加载。
  • 环境

    webpack2.x

  • 语法

    require.ensure(dependencies: String[], callback: function(require), chunkName: String)
    // 依赖 dependencies
    // 这是一个字符串数组,通过这个参数,在所有的回调函数的代码被执行前,我们可以将所有需要用到的模块进行声明。
    
    // 回调 callback
    // 当所有的依赖都加载完成后,webpack会执行这个回调函数。require 对象的一个实现会作为一个参数传递给这个回调函数。因此,我们可以进一步 require() 依赖和其它模块提供下一步的执行。
    
    // chunk名称 chunkName
    // chunkName 是提供给这个特定的 require.ensure() 的 chunk 的名称。通过提供 require.ensure() 不同执行点相同的名称,我们可以保证所有的依赖都会一起放进相同的 文件束(bundle)。
    

ES Module

  • 特点

    1. 编译时期加载,ES6 模块是编译时输出接口。

    2. 输出不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    ES6 模块输出的是值的引用。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

  • 环境

    浏览器或者服务器环境

  • 语法

    // CommonJS模块
    let { stat, exists, readFile } = require('fs')
    
    // 等同于
    let _fs = require('fs')
    let stat = _fs.stat
    let exists = _fs.exists
    let readfile = _fs.readfile
    

    上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

    ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。

    // ES6模块
    import { stat, exists, readFile } from 'fs'
    

    上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

    除了静态加载带来的各种好处,ES6 模块还有以下好处。

    • 不再需要 UMD 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
    • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者 navigator 对象的属性。
    • 不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。

    定义和暴露:

    模块功能主要由两个命令构成:exportimportexport 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

    // profile.js
    export var firstName = 'Michael'
    export var lastName = 'Jackson'
    export var year = 1958
    

    上面代码是 profile.js 文件,保存了用户信息。ES6 将其视为一个模块,里面用 export 命令对外部输出了三个变量。

    使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。

    // main.js
    import { firstName, lastName, year } from './profile.js'
    
    function setName(element) {
      element.textContent = firstName + ' ' + lastName
    }
    

    上面代码的 import 命令,用于加载 profile.js 文件,并从中输入变量。import 命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

  • 应用

    ES6 的最新语法支持规范

<script type="module">

  • 特点

    1. 异步加载,浏览器对于带有 type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的 defer 属性。
  • 语法

    <script type="module" src="./foo.js"></script>
    <!-- 等同于  -->
    <script type="module" src="./foo.js" defer></script>
    

    如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

    <script>标签的 async 属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

    <script type="module" src="./foo.js" async></script>
    

    一旦使用了 async 属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

    举例来说,jQuery 就支持模块加载。

    <script type="module">
      import $ from "./jquery/src/jquery.js"; $('#message').text('Hi from
      jQuery!');
    </script>
    

    对于外部的模块脚本(上例是 foo.js ),有几点需要注意。

    • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
    • 模块脚本自动采用严格模式,不管有没有声明 use strict
    • 模块之中,可以使用 import 命令加载其他模块(.js 后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口。
    • 模块之中,顶层的 this 关键字返回 undefined,而不是指向 window。也就是说,在模块顶层使用 this 关键字,是无意义的。
    • 同一个模块如果加载多次,将只执行一次。

参考文章

Javascript 模块化编程(二):AMD 规范

前端模块化:CommonJS,AMD,CMD,ES6

Module 的语法

模块化流程图

js 模块规范

代码分割 - 使用 require.ensure


  • 欢迎斧正,拍砖~
  • 你的代码里有你读过的书和走过的路
  • 经历过才会有更好的成长

博客地址 欢迎到访

GitHub 我不要Star✨(疯狂暗示)