前端模块化总结(commonJs,AMD,CMD,ES6 Module)

  前端模块化已经不是一个新技术了,但是在项目中还是碰到一些不太明白各种引入,导出模块的方法的使用和区别的小伙伴。所以还是想总结一下形成文档,来龙去脉搞清楚了,用起来自然不会混淆。

--------------------------------------------------------------

  补充:在基于webpack的前端工程里,代码里使用了import做模块导入,module.exports 做导出,这个写法也是可以运行的,因为webpack的模块化遵循commonJs规范,而es6的import语句会被转换成__webpack_require__();

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  • 关于commonJs

  对于javaScript语言来说,在ES6标准之前,它是没有模块化的概念的,commonJs规范的提出,也不是来解决JS模块化的问题。commonJs规范提出的本 意,是用来补充ES缺失的规范,希望JS可以能够在任何地方运行,并具备开发大型应用的基础能力,而不是单单作为web脚本来使用。其中,nodeJs的模块系统,就遵循了commonJs规范。

加载方式:同步加载,根据模块在代码中出现的先后顺序加载;也就是说,只有加载完成,才能执行后面的操作。这样就会对浏览器造成阻塞。而服务器端的文件相当于在本地存储,不存在请求网络等问题。故commonJs规范被认为不适合浏览器环境而适合服务器端;

执行特点:第一次加载时执行,并缓存执行结果,输出的是一个值的拷贝,即一旦输出一个值,模块内部的变化就影响不到这个值,自然也没有动态更新;

关键字:

     require:加载外部模块,并返回模块的exports对象;该对象为一个值的拷贝。

     module.exports:暴露模块方法和属性;

   exports:exports其实是指向于module.exports的一个变量,相当于在模块的开始,定义了 var exports = module.exports; 故使用exports时需要注意不要给该变量重新赋值;

  以遵循该规范的nodeJs模块化为例

require('module'); // 通过模块名加载,需要注意的使,在配置文件中要配置该模块的具体路径;
require('path'); // 通过路径加载
// 使用exports添加属性的方式暴露 exports.xxx = xxx; // 暴露xxx属性
// 或者使用给module.exports赋值的方法 module.exports = yyy; // require时得到的就是运行后yyy值的拷贝

 exports和module.exports 不管谁被重新赋值,他们的关联关系都会断掉。模块最终return出去的是module.exports;

exports.a = 2;
module.exports.b = 3;

console.log(module.exports); //{ a: 2, b: 3 }
console.log(exports); // { a: 2, b: 3 }
console.log(exports === module.exports); // true

 给module.exports重新赋值,exports中新添加的a属性不会返回

exports.a = 2;
module.exports = {"b": 3}; // moudle.exports 被重新指向新的内存,它与exports的关联关系断开
console.log(module.exports);  // { b: 3 }
console.log(exports);  // { a: 2 }
console.log(exports === module.exports);  // false

 给exports重新赋值,exports与module.exports断开关联

exports = 2;   // exports重新赋值,切断了与module.exports的联系
module.exports.b= 3;

console.log(module.exports); // { b: 3 }
console.log(exports); // 2
console.log(exports === module.exports); // fasle
  • 关于AMD  

   Asynchronous Module Definition,异步模块加载;异步的加载方式,更适用于浏览器。请求发出后,继续执行其他脚本,依赖于请求结果的代码,放到一个回调函数里;AMD规范的代表则是require.js;

  特点:依赖前置,所有依赖都在模块定义时声明。不管是在define中声明的依赖,还是在callback中通过require加载的依赖,都会先加载完再去执行callback;故它属于运行时加载。这个特性就导致了在模块定义时被声明的依赖,虽然没有被用到,也做了加载执行的操作;更多requireJs的实现细节,可以参考这篇文章

  以require.js为例:define方法用来定义模块,require方法用来加载模块,config方法用来做配置

config配置

require.config({
    baseUrl:'js/',
    paths:{  //  需要通过模块名来加载的模块,都定义到这里,支持网络资源,本地资源路径在baseUrl下
        'jquery':'http://xxx.xxxx.com/jquery.js',
        'index':'index/index.js',
    },
    shim:{
        'aaa':{  // 不符合标准的文件,可以在shim中来定义
            deps:['./a','./b'],
            init:function(){
                return {
                          // 这里定义非标准文件的返回
                }
            }
        }
    }
});        

  模块定义:define([id,deps,] callback);

// define 的模块id是唯一的,为避免麻烦,一般可省略,require.js会自动生成一个唯一标识
define(['jquery','index','./utils'], function($,index,utils){
   // dosth...
     return { // 这里定义模块向外暴露的方法和属性,没有可以省略;
        'aaa': 1,
        'bbb': 1
     } 
});

  define 也可以定义一组键值对并返回,这种用法常用于动态的config配置;

define({
    'aaa': 1,
    'bbb': 2,
    'ccc': 3
});

  模块载入:require(deps[,callback]);

require(["moduleName","path","url"], function (module) {
    // dosth;    
});

如果要在define中使用require,那需要加入require的依赖,简写也可以省略

define(function(require, exports, module ) {  // 这种定义模块的方式可以兼容commonJs规范,但实质上还是被转换为requireJs的规范来实现;
var a = require('a'), // 通过这种方式可以实现按需加载
b = require('b');
// 模块需要暴露的方法也可以通过 exports向外暴露
exports.eee = 123;
});
// 等价于:
define(['require'], function(require){
var a = require('a'),
b = require('b');
})
  • 关于CMD

     Common Module Definition 通用模块定义,CMD规范的概念是随着sea.js的推出产生的。同时,sea.js也借鉴了很多require.js的东西。它与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。通过 CMD 规范书写的模块,可以很容易在 Node.js 中运行;

      特点:同AMD一样,CMD也属于运行时加载。但是CMD规范中,依赖模块可以通过require.async在需要的地方引入并执行(懒加载)。这个特性使它可以实现按需加载。

   以sea.js为例:通过define定义模块,通过require加载模块,通过exports或return向外提供API;

模块定义:define(id?, deps?, factory);factory可以是函数,也可以是对象或者字符串

// define 函数的标准使用方法。但是官方强烈推荐不传入 id 和 deps,模块加载器会自动获取这两个参数,id默认为模块所在文件的访问路径,
// deps数组模块加载器会从factory.toString() 中解析。同时,function的第一个参数,必须是require,这是seajs的使用规则;
define('hello', ['jquery'], function(require, exports, module) {   //dosth...
  // 向外暴露模块的API有三种方式:   exports.aaa = sth; // seajs中exports也是module.exports的引用;
  module.exports = {}
  return {}
});
// factory 为对象,相当于定义一个json数据模块,加载该模块时得到的就是这组json数据;这个用法跟require是一致的 define({ "foo": "bar" });
// factory为字符串时,相当于定义一个字符串模板 define('I am a template. My name is {{name}}.');  

模块引用:require(id);  require 在seajs中可以看作是语法关键字,不可重新赋值,不可引用;id为要引用模块的唯一标识,且必须是字符串直接量

define(function(require, exports, module) {
  // 同步加载模块,通过这种方式加载的文件,会在静态分析阶段就被下载好;
    var a = require('./a');
    a.doSomething();
  // 异步加载一个模块,通过这种方式加载的文件,在用的时候才会下载;
    require.async('./b', function(b) {
        b.doSomething();
    });
  
  // 异步加载多个模块
    require.async(['./c', './d'], function(c, d) {
       // do something
    });
  // 条件加载模块;PS:如果这里依然使用require来加载模块,那模块加载器会把两个模块都下载下来
    if (todayIsWeekend){
         require.async("play");
    }else{
         require.async("work");
    }
});

  无论是AMD还是CMD,都是module2.0 的一个分支,正所谓条条大路通罗马,也没有哪个解决方案就明显优于哪个。作为一个前端开发,内心永远向往大一统。ES6的模块化,在目前看来,已经算是前端模块化的大一统了。

  • 关于ES6 Module

  就目前考虑浏览器的兼容性来说,ES6的模块化,还是需要进行兼容性转化的,当然,这个在前端自动化构建的大潮中已经不需要再被提起了。

  特点:与AMD和CMD不同的是,它的设计理念是尽量的静态化,在编译阶段就能确定模块的依赖关系,输入输出等,即编译时加载(静态加载);与commonJs提供的是值的拷贝不同得是,export向外提供的是一个只读的动态引用,故通过import加载的模块,是不会被缓存的。这一特点也说明ES6是支持动态更新的。

       关键字:通过import引用其他模块,通过export对外提供接口。

 

export default anything;  // 模块的默认输出,一个模块只能有一个默认输出; 
// 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以default后面不能跟变量声明语句。
import anyName  from  'path';

export var a = 1;  //这里要说明一点,export是对外的接口,所以它必须与模块内部的变量建立一一对应关系;
export function  foo(){};
//  也可以写成下面的形式:
var  a = 1;
function foo(){};
export {a, foo}

// 然而下面的这两种写法都是错误的,因为它没有提供对外的接口
export 1; 

var a = 1;
export a;

// 在import中,就要指出要加载的方法和属性的具体名字
import {a, foo} from 'path';
// 通过 as关键字,可以给对外输出的方法重新命名。
function v1() { ... } 
function v2() { ... } 
export { 
  v1 as streamV1, 
  v2 as streamV2, 
  v2 as streamLatestVersion // 同一个方法可以输出多次
};
// import 中也支持as关键字
import { streamV1 as newName } from 'path';
// import也可以整体加载
import * as newName from 'path';

// import也可以用来加载并执行一个js文件,并且没有任何输入。
import 'path';   
import 'path';  // 即使多次加载,该文件也只执行一次,

// 对于同一个模块中的多次加载,import也只执行一次,因为import语句是单例的(singleton)
import {foo} from 'path';
import {bar} from 'path';
//相当于
import {foo, bar} from 'path';

 commonJs规范和es6模块化规范对循环加载的处理

  本来想再总结下这个知识点,但是看了大神阮一峰的总结文章,感觉已经很清晰明了了,这里贴出链接:http://www.ruanyifeng.com/blog/2015/11/circular-dependency.html

  总之一句话,commonJs中遇到循环引用,是执行了多少返回多少,因为它是值的拷贝。ES6里是值的引用,方法或者属性真正被使用的时候才去取。所以如果你的打包构建是基于webpack,那要尽量避免循环引用,或者保证循环引用的变量和方法是已经运行过的。

 
posted @ 2020-03-19 21:58  solaZhan  阅读(387)  评论(0编辑  收藏  举报