美文网首页
JavaScript 模块化编程(二):规范

JavaScript 模块化编程(二):规范

作者: 卓三阳 | 来源:发表于2018-11-30 10:59 被阅读17次

    JavaScript 模块化编程(一):模块的写法
    JavaScript 模块化编程(二):规范
    JavaScript 模块化编程(三):实现一个RequireJS
    JavaScript 模块化编程(四):结合Node源码分析CommonJs规范


    常见的JavaScript 模块化规范有3种,CommonJS、AMD(异步模块定义)、CMD(公共模块定义)

    其中
    服务端 :NodeJS 服务:CommonJS规范,新版本的Node也可以启用ES6 Module功能
    浏览器端:主要使用的是AMD规范和CMD规范,现在已经逐步被ES6 Module取代

    当ES2015标准的出现后,ES6 在语言标准的层面上,实现了模块功能。这也让AMD、CMD逐渐被淘汰。相信ES6模块最终会一统天下


    1.CommonJS规范

    (1) 每一个文件都是一个模块,每一个模块都有一个独立的作用域,文件内的变量,函数都是私有的,其他文件不可使用(除非赋值到 global上)
    (2)每个模块内部,module变量代表当前模块
    (3)每个文件对外的接口是 module.exports 属性
    (4) require用于引用其他模块,实际获得的是其他模块的module.exports这个属性

    例子

    //module_a.js
    var x = 5;
    var addX = function(value) {
      return value + x;
    };
    module.exports.x = x;
    module.exports.addX = addX;
    
    //index.js
    var example = require('./module_a.js');
    console.log(example.x); // 5
    console.log(example.addX(1)); // 6
    

    CommonJS模块的特点

    (1)所有代码都运行在模块作用域,不会污染全局作用域
    (2)模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
    (3)模块加载的顺序,按照其在代码中出现的顺序(即是同步加载)

    require内部处理流程
    require 实际是 指向当前模块的 module.require, module.require 又调用Node的 Module._load(此Module非彼module)

    Module._load = function(request, parent, isMain) {
      // 1. 检查 Module._cache,是否缓存之中有指定模块
      // 2. 如果缓存之中没有,就创建一个新的Module实例
      // 3. 将它保存到缓存
      // 4. 使用 module.load() 加载指定的模块文件,
      //    读取文件内容之后,使用 module.compile() 执行文件代码
      // 5. 如果加载/解析过程报错,就从缓存删除该模块
      // 6. 返回该模块的 module.exports
    };
    

    其中 module.compile()执行如下:

    Module.prototype._compile = function(content, filename) {
      // 1. 生成一个require函数,指向module.require
      // 2. 加载其他辅助方法到require
      // 3. 将文件内容放到一个函数之中,该函数可调用 require
      // 4. 执行该函数
    };
    

    2.AMD(Asynchromous Module Definition - 异步模块定义)

    AMD 是 RequireJS 在推广过程中对模块定义的规范化产出

    CommonJS 采用的是同步加载机制,如果用于客户端,必定受到网络的限制。所以,CommonJS不适用于客户端。
    而 AMD 采用的是模块异步加载方式,在需要执行到模块文件的时候,实现异步加载,回调执行。

    require.js
    首先下载最新require.js ,然后引入,data-main用于指定网页程序的主模块:

    <script src="js/require.js" data-main="js/main"></script>
    

    使用

    定义模块
    define(id?, dependencies?, factory)
    加载模块
    require([module], callback)

    例子

    //math.js
    define(function() {
      var add = function(x, y) {
        return x + y
      }
    
     return  {
        add: add
      }
    })
    
    //main.js
    require(['math'], function (math) {
        math.add(2, 3);
      });
    

    当执行到这一段代码的时候, 浏览器会先 加载 math模块,在math模块加载成功后, 再执行后面的回调函数


    3.CMD(Common Module Definition - 公共模块定义)

    CMD 是 SeaJS 在推广过程中对模块定义的规范化产出

    SeaJS
    使用

    定义模块
    define(factory)
    加载模块
    require(id)

    区别

    1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible
    2. CMD 推崇依赖就近,AMD 推崇依赖前置,例:
    //CMD
     define (function (require, exports, module) {
        var a = require('./a')  // 模块加载
        a.doSomething();
        var b = require('./b')  // 依赖可以就近书写
        b.doSomething();
    
        // 通过 exports 对外提供接口
        exports.doSomething = ...
        // 或者通过 module.exports 提供整个接口
        module.exports = ...
       })
    
    

    代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。而AMD是依赖前置的,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块,表现在require函数的调用结构上为:

    //AMD
    define(['./a','./b'],function(a,b){
       a.doSomething()
       b.doSomething()
    }) 
    

    代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组


    4.UMD(Universal Module Definition - 通用模块定义)

    Universal Module Definition。可以看成是AMD和CommonJS的一个合并方案。解决跨平台的解决方案。
    步骤

    1.先判断是否支持Node.js模块格式(CommonJS),存在则使用Node.js模块格式
    2.再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块
    3.前两个都不存在,则将模块公开到全局(window或global)

    以一个calculator模块为例:

    // if the module(calculator) has no dependencies, the above pattern can be simplified to
    (function (name, context, definition) {
       if (typeof module != 'undefined' && module.exports){   //CommonJs
         module.exports = definition();
      }else if (typeof define == 'function' && define.amd){    //AMD
        define(name, definition);
       } else{  // Browser globals (context is window)
        context[name] = definition();
       }
    }('calculator', this, function () {
      // your module here!
      return {
        sum: function(a, b) { return a + b; }
       };
    });
    

    5.ES6 Module

    export命令用于规定模块的对外接口

    //module_a.js
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    export {firstName, lastName, year};
    

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

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

    export default命令,为模块指定默认输出。其他模块加载该模块时,import命令可以为该输出指定任意名字

    详细见ES6 Module


    6.回顾总结

    1.CommonJS和AMD区别?
    (1)CommonJS是适用于服务器端,Node就是采用的CommonJS模式。它是同步加载不同模块文件。之所以采用同步,是因为模块文件都存放在服务器的各个硬盘上,实际的加载时间就是硬盘的文件读取时间
    (2)AMD是适用于浏览器端的一种模块加载方式。从名字可知,AMD采用的是异步加载方式。浏览器需要使用的js文件(忽略缓存)都存放在服务器端,从服务器端加载文件到浏览器受网速等各种因素的影响,如果采用同步加载方式,一旦js文件加载受阻,页面将处于阻塞状态

    2.CommonJS和ES6 模块区别?
    (1)CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
    这是因为CommonJS 的输出接口是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,使得编译时就能确定模块的依赖关系(“静态优化”)。
    (2)CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
    CommonJS 模块输出的是module.exports这个对象,我们读取的也是这个对象,而不是模块内部某个变量。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(除非引用类型)。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。


    参考

    CommonJS规范
    RequireJS和AMD规范
    AMD 和 CMD 的区别有哪些?
    Javascript 模块化管理的来世今生

    相关文章

      网友评论

          本文标题:JavaScript 模块化编程(二):规范

          本文链接:https://www.haomeiwen.com/subject/kjvpqqtx.html