js模块规范

267 阅读7分钟

模块化规范可以让开发者都遵循相同的规则来开发各自的模块,他们通过规范来约束模块的定义,大家就不需要太多的沟通或者大量的文档来说明自己的模块使用规则,成千上万的模块就这样产生并容易使用。它的意义不仅是让模块看起来很规范,在合作开发和社区传播也起了重大作用。 最重要是解决命名冲突的问题。

((root, factory) => {
    if(typeof exports === 'object' && typeof module === 'object')
        // CommonJS
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    }else if(typeof exports === 'object'){
        // es Module
	exports["ui"] = factory();
    } else {
        // 绑定全局变量
        root.testModule = factory(root.jQuery);
    }
})(this, ($) => {
    //todo
});

立即执行函数

可以不暴露私有成员

  • 放大模式:将模块的功能扩大
  var module1 = (function (mod){
    mod.m3 = function () {
      //...
    };
    return mod;
  })(module1);
  • 宽放大模式:如果模块没有加载成功,则可以设置默认值为空对象
  var module1 = ( function (mod){
    //...
    return mod;
  })(window.module1 || {});
  • 输入全局变量:模块的独立性是模块内部不与程序其他部分直接交互,所以模块内部调用全局变量,需要显示地将其他变量输入模块。
  var module1 = (function ($, YAHOO) {
    //...

  })(jQuery, YAHOO);

UMD

UMD(Universal Module Definition)叫做通用模块定义规范。
它可以通过运行时或者编译时让同一个代码块在使用CommonJs、CMD甚至AMD的项目中运行,就是集大成者,通过各种判断,适应所有的规范。同一个js包运行在浏览器端、服务器端甚至app端都只需要遵循同一个写法即可。

CommonJS

CommonJS是一种js模块化规范,通常会在服务端的nodejs上使用。在CommonJS规范中,每个文件就是一个模块,拥有自己独立的作用域、变量和方法。module变量代表当前模块,这个变量是一个对象,它的exports是对外的接口。加载某个模块,就是加载该模块的module.exports。require方法用于加载模块。

  • 同步加载
  • 运行时加载,即运行时才能得到模块内容
  • 浏览器无法执行CommonJS规范的代码,原因是缺少四个环境变量,如果能模拟提供这四个变量,则浏览器就能执行CommonJS规范的代码
    • module
    • exports
    • require
    • global

var math = require('math');

  math.add(2, 3);

例如上面的:math会等待math.js加载完,才会执行下面的调用,这个如果用在客户端的话,会导致如果加载时间长,页面完全卡住。
这对服务器来说不是问题,因为所有的模块都存在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间,而对于浏览器,文件放在远程服务器上,读取时间就是取决于网速了。

AMD

  • 由于CommonJS规范无法适用客户端,所以才诞生了“异步加载”的AMD:Asynchronous Module Definition(异步模块定义)。
  • 实现的原理就是回调。文件加载不影响后续语句的执行,文件加载完后执行回调函数。
  • 运行时加载,即运行时才能得到模块内容
  • 使用这个规范的代表库有requirejs
require(['math'], function (math) {

    math.add(2, 3);

  });

由此可见,是否用require函数加载文件,并不是AMD和CommonJS规范的区别,而是看它加载模块的方式,是同步还是异步

requirejs

requirejs是一个库,使用的也是AMD规范,但是它解决了两个痛点,一个是实现js文件异步加载,二是管理了文件之间的依赖性。

CMD

  • CMD(Common Module Definition),解决的一个痛点是需要使用的库用的时候才去加载。
  • AMD实现了异步加载,但是要把所有依赖一开始要全部写出来,而且一下子就要加载所有的文件,不符合书写逻辑,而CMD就是依赖就近,写法像CommonJS
  • AMD和CMD最大的区别是对依赖模块的执行时机处理不同,
  • 使用这个规范的代表库有seajs
define(function(require, exports, module) {
   var clock = require('clock');
   clock.start();
});

es Module

  • es6规范的模块化:通过export命令显式指定输出的代码,再通过import命令输入
  • 完全可以取代CommonJS和AMD
  • es6不是对象,所以没法引用es6模块本身。
// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载成为“编译时加载”或者静态加载,在编译时完成模块加载,比CommonJS模块的加载方式高。

  • export命令规定的是对外的接口,必须与模块内部的变量建立一一对应的关系。
// 报错
export 1;

// 报错
var m = 1;
export m;

像这样直接输出数值是会报错的,只能输出对外接口m,其他脚本可以import拿到这个接口m。用{}制定所要输出的一组变量,或者输出声明表达式。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

// 写法四
export function f(){}
// 写法五
export class f {}

以上五种都是正确的写法。

  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口可以取得模块内部实时的值。
export var foo = 'bar'; // 相当于export {foo: "bar"}
setTimeout(() => foo = 'baz', 500);

import以后马上能得到的值是bar,500ms后该值会变成baz。
import {foo} from xx才能获取到foo的值,只有export default才能用import a from xx;

  • export和import可以出现在模块顶层的任意位置,但如果处于块作用域内会报错,因为这样没法做静态优化。
  • import具有提升效果,会提升到整个模块的头部首先执行。
  • import输入的变量都是只读的,修改会报错。可以修改变量的属性,但很难查错,不建议使用。
  • import语句在编译时执行,所以不能有任何的动态表达式
// 报错
let module = 'my_module';
import { foo } from module;
  • import多次加载同一个模块,只会执行一次。
import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
  • import跟require写在一起,import的优先级最高,在编译时执行。require会运行时执行。
  • import {a,b} from xx,会只加载a,b两个对外接口内容,xx文件里的其他内容不会加载
  • export default 1的本质就是把1赋值给变量default。
  • export-import的复合写法:
export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

  • 模块的继承:
// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

注意:export * 忽略了circle的default方法,所以这个相当于改写了circle的e和default。

  • 动态加载:解决import x语句无法在运行时加载模块,导致if条件加载的场景无法实现的问题。import()支持动态加载模块
  • import() => Promise;import().then(({export1,export2})).catch()。then的参数是export的接口
  • import()的使用场景:1. 条件加载。2.动态路径加载。3.按需加载,合适的时机才执行加载

nodejs:exports和module.exports的区别:exports是module.exports的引用

  • 他们都是用来导出代码的
  • module.exports初始值为一个空对象{}
  • exports是指向module.exports的引用
  • require()返回的是modul.exports而不是exports
  • exports实际上只是一个指向module.exports的变量,模块执行后会被释放,而module不会释放
  • 所以,模块导出要写成 module.export = xx,那么require的时候,得到的就是xx,如果写成export=xx,require时将不能得到xx。如果写成export.xx = xx,那么实际就是module.export.xx = xx,所以require时得到的就是{xx}.

nodejs的require是单次加载,同一个模块无论require多少次,都只加载一次

nodejs的模块和包

- 模块;一个文件就是一个模块
   - nodejs模块分两大类,一是核心模块,如fs、http、net、vm等nodejs官方提供的模块,编译成二进制代码,require直接获取,拥有最高的加载优先级
   - 二是文件模块。加载方式一是按路径加载(文件路径以./或者/开头),二是查找node_modules文件夹。如果查找不是以路径表示的文件名称时,会查当前目录下的node_modules如果找不到,就会往上层的node_modules找直至遇到根目录为止
- 包:是模块基础上更深一步的抽象,nodejs的包是一个包含package.json文件的文件夹。
   - 严格符合CommonJS规范的包应具备以下特征:
      - package.json必须在包的顶层目录
      - 二进制文件应该在bin目录下
      - JavaScript代码应该在lib目录下
      - 文档在doc目录下
      - 单元测试在test目录下

参考: 什么是【CommonJs】 模块(一) CommonJs,AMD, CMD, UMD