Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4种模块规范

模块化的好处

  • 将系统分离成独立功能的模块,不仅可复用,还可按需加载
  • 提高了代码的可复用性和维护性
  • 隔离作用域,避免变量冲突

发展过程

IIFE自执行函数 => CommonJS(服务端) => AMD(浏览器) => CMD(浏览器) => ES6 Module

以前ES5没有模块化规范,所以产生了CommonJS、AMD、CMD三种规范,另外ES6又新增了一种

如今webpack自己实现了一套模块机制,无论是CommonJS的require语法还是ES6的import语法,都能被解析成指定环境的可运行代码,因此ES6广泛应用(也就是说,不一定要浏览器原生支持)

IIFE自执行函数

  • 思路

    • 在一个单独的函数作用域中执行代码并返回
  • 使用

    (function(){
        return {
            data:[]
        }
    })()
    

CommonJS

  • 思路

    • 每个js文件都可以作为一个模块
    • 在服务器端
      • 模块的加载运行是同步阻塞的,会等待引用完毕再执行下面
    • 在浏览器端
      • 模块需要提前编译处理打包,不然浏览器不能识别
  • 使用

    //定义
    module.exports = obj
    或者
    exports.xxx = value
    
    //引入
    var yyy = require(模块路径);
    
  • 用途

    • 后端的Node.js和Webpack采用此种模块化规范
  • 注意

    • 在前端浏览器中并不支持,因为缺少四个Node.js环境的变量:module/exports/require/global
    • 当引用加载同一个模块时,不会再执行该模块,而是取到缓存中的值,也就是说,都只会在第一次加载时运行一次,除非手动清除系统缓存

AMD规范

  • 思路

    • 由于CommonJS的同步机制只适用于服务器端,搬到浏览器端就容易陷入等待的假死状态,因此需要改进为异步机制
    • 异步机制的实现:模块在定义的时候,就必须先知道其需要依赖的模块,然后把本模块代码写在回调函数中执行,因此衍生出AMD规范
  • 使用

    • 没有依赖的module1模块(一个参数)

      //module1.js
      define(function(require,exports,module){
          exports.name = "jack"  //暴露
      })
      
    • 依赖module1的module2模块(两个参数)

      //module2.js
      define(['module1','module2'], function(m1,m2){
          
      })
      
    • 在main.js中引入模块

      require(['module1','module2'], function(m1,m2){
          //通过m1和m2去使用这两个模块
          console.log(m1.name)
      })
      
  • 具体实现:RequireJS

    • 顺序:模块化加载 => 全部模块预执行 => 主逻辑中调用模块

    • 核心原理:创建script元素,通过指定script元素的src属性来实现动态加载模块的

    • 内部机制:模块的定义是一个function,这个function实际是一个 factory(工厂模式),加载执行过一次就不重复加载了,直接返回模块实例

    • 缺点:饱受诟病的是,定义模块的时候需要把所有依赖模块都罗列一遍(推崇前置依赖),而且在使用时还需要在 factory 中作为形参传进去

      • 即:
      define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... });
      

CMD规范

  • 思路

    • 由于AMD的具体实现RequireJS不优雅,“前置依赖”的模块管理能力差,因此要改进
    • 所以出现了新的具体实现:SeaJS,基于新的CMD规范,推崇“就近依赖
    • 顺序:模块预加载 => 主逻辑调用模块时才执行模块中的代码
  • 用法

    • 基本和AMD相同,都是异步的
    • 不过还融合了CommonJS的写法,模块引入有同步和异步方式
  • 二者区别在于:

    1. AMD当加载了依赖模块之后立即执行依赖模块,依赖模块的执行顺序和我们函数内书写的顺序不一定一致;
    2. 而CMD加载完依赖模块之后,并不会立即执行,等所有的依赖模块都加载好之后,进入回到函数逻辑,遇到require语句的时候,才执行对应的模块,这样模块的执行顺序就和我们书写的时候一致
  • 使用

    • 定义模块
    //module1.js
    define(function (require, exports, module) {
        exports.name="i am module1"
    })
    
    • 引入模块
    //main.js
    define(function (require, exports, module) {
        //引入依赖模块(同步)
        var module2 = require('./module2');
        console.log(module2.name)
        
        //引入依赖模块(异步1)
        require.async('./module3', function (m3) {
        //这里m3对应module3
        
        })
        //引入依赖模块(异步2)
        var module4=require.async('./module4');
        console.log(module4.name)
    })
    

ES6的Module

使用:

  • 定义:通过export暴露API
  • 引入:通过import用结构语法{ }引入,除非是针对default导出

注意:ES6模块只能在模块顶层定义和引入,属于编译时加载不是对象,在代码块中会报错,也就是说无法像CommonJS那样生成一个对象后在运行时动态加载,对于此缺点,有一个新提案建议引入import()方法,返回一个Promise对象,实现动态引入

注意:在某些浏览器是不支持的,因此需要Babel先转译,转成ES5语法才能兼容,涉及到的包有babel-cli,babel-preset-es2015,browserify,还要定义.babelrc文件

  • 三种方式

    • 分别暴露

      //module1.js
      export let m = 1
      export let arr = [1,2,4]
      export function fun(){
        console.log('i am a fun');
      }
      
      //引入与使用(结构引入)
      import { m as name, arr, fun } from './module1'
      console.log(name);
      fun()
      
    • 统一暴露

      //module2.js
      let m=1;
      let arr=[1,2,4]
      function fun() {
        console.log('i am a fun');
      }
      export { m, arr, fun }
      
      //引入与使用(结构引入)
      import { m, arr, fun } from '/module2.js'
      console.log(arr);
      fun()
      
    • 默认暴露

      //错误,因为是导出一个叫default的变量,其后不能跟声明语句
      export default var a = 1;
      
      //正确,导出一个对象,对象即default
      export default {
          m:1,
          fun(){
              console.log('i am a fun from export defalut')
          }
      }
      
      //引入和使用(module为自定义任意名字)
      import module from './module3.js'
      module.fun()
      
  • 对比CommonJS的案例

    //CommonJS模块
    let { basename, dirname, parse } = require('path');
    
    //ES6模块
    import { basename, dirname, parse } from 'path';
    
    • 区别:
      • 当 require path 时,CommonJS会将path模块运行一遍,并返回一个对象,这个对象包含path模块的所有API,只是通过ES6的对象操作符,取出其中几个变量
      • 当 import path 时,ES6只会讲列出的几个变量从path模块中提取出来,预编译放入,从而实现tree-shaking