前端模块化规范定义-不同规范下的导入导出

时间:2021-2-20 作者:admin

随着Javascript语言的发展,前端世界对于模块化的定义也是越来越趋向于成熟了,本文来探索一下前端模块化的一些常见的概念。(AMD、CMD、CommonJS、ES Module、UMD)

前言

Javascript作为嵌入式的脚本语法,属于弱类型语言,没有像Java那些强类型语言,拥有类的概念,更不用说模块(module)了。而我们常说的“模块化”其实是开发者模仿Java世界的一个重要概念——package拟出的抽象概念,主要是来隔离、组织复杂的JS代码,模块内是一个相对独立的空间,不用担心全局环境污染、命名冲突什么的,如果外部要使用也只需做一个导入操作即可。

对于学前端的同学,我想应该会经常看到AMD、CMD、CommonJS、ES Module这几个名词吧,对模块化规范了解深一点的同学可能还听过UMD以及其他一些模块化发展史的东西。反正呢,这些东西究其缘由也没啥本质区别,无非就是语法、用途有所区别,只要记住他们就是用来规范你的代码。

简介

  • AMD

AMD 全称即Asynchronous Module Definition,中文名是异步模块定义的意思,它是一个作用在浏览器端模块化开发的规范。由于不是JavaScript原生支持,使用AMD规范进行开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出。
RequireJS其核心是定义了一个define函数:define(id?, dependencies?, factory);
基本用法:

define('moduleA',function() {
    return {
        a: 1
    }
});
  • CMD
    CMD 全称即Common Module Definiton,中文名为通用模块定义规范的意思,它也是运行于浏览器之上的。CMD规范是国内发展出来的,就是AMD有个RequireJS一样,CMD在浏览器端也有个SeaJS的经典实现,其作者是在淘系的玉伯。CMD推崇的是就近原则,所以一般不在define的参数中写依赖,在factory中写。其也推广一个模块就是一个文件。

核心函数:define(factory)
基本用法:

define(function(require, exports, module) {
    module.exports = {
        a: 1
    }
})
  • CommonJS

CommonJS是一种JavaScript模块化规范,它是一个超级大的概念,和ESMAScript规范一样,它是整个语言层面上规范。和前两种相比主要区别是它应用在服务端,我们最熟悉的NodeJS就是使用了它。
CommonJS规范中,每一个文件就是一个模块,拥有自己独立的作用域、变量、以及方法等,对其他的模块都不可见。CommonJS规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。
基本用法:

// a.js
module.exports = {
    a: 1
}
// b.js
var moduleA  = require('./a.js');
console.log(moduleA.a)
  • ES Module

ES6之前Javascript一直没有属于自己的模块规范,所以社区制定了 CommonJs规范,所以在ES6出来时提出自己的模块化规范,也就是ES Module
基本用法:

// a.js
let a = 1;
export { a }
// b.js
import { a } from './a.js';
  • UMD

UMD 全称即Universal Module Definition,中文名为通用模块定义规范的意思。也是随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了。它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身。
基本用法:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = factory(require('jquery'));
    } else {
        // 全局变量
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
      // ...
}));

UMD大致就是一个大集合吧,有兴趣的同学可以深入去研究一下,现在也比较少会谈及它了,连AMD、CMD都渐渐的淡出了人们的视野了。

运行时加载编译时加载

尽管模块化的处理操作能使我们很好的处理我们日益膨胀的代码量,在巨大的项目里妥善的管理不能功能模块的代码,这些非常好。但是我们在使用模块话处理时,还是应该有很多需要注意的事情,其中比较重要的就是“模块的导入”了,当一个页面或者一个完整的项目导入过多的模块引来的效率、性能方面的问题,这就不是我们想要看到的。下面我们介绍一下在CommonJS和ES Module中两个比较重要的概念“运行时加载”与“编译时加载”。

  • 运行时加载
// 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 模块是编译时加载,使得静态分析成为可能。
有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6 模块还有以下好处:
(1)不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
(2)将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
(3)不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

对比ES Module 与 CommonJS 导入和导出

ES Module CommonJS
导入 import require
导出 export、export default exports、module.exports

ES Module的导入和导出

  • import是在编译过程中执行,也就是在代码执行前执行,因为import有提升效果,提升到整个模块的头部,所以import不一定要写到最前面,但一般都写在最前面吧。import是静态执行,不能使用变量或者表达式,因为呢,代码都还没执行呢。
//错误1
var url = './test';
import { a,b } from url;
//错误2
let status= true;
if(status){ import { a,b } from url; }
  • import可以使用as进行重命名,但是这和导出的时候有很大关联。
  • 一个模块可以有多个export,但是只能有一个export default。export default可以和多个export共存。我个人更喜欢将export理解为单个导出,export default为批量导出。两者能在同个文件中混用,但一般不建议怎么用。
  • export default a的含义是将变量a的值赋给变量default。
//实例1
//test.js
const a = 'yd';
export {a}
//app.js
import {a} from './test.js'// 这里两个文件中的a就是同一个
console.log(a);// yd
//实例2
//test.js
const a = 'yd';
export default {a}
//app.js
import a from './test.js'// 这里两个文件中的a是不同的
console.log(a);// { a: 'yd' }
  • as重命名:两者都支持重命名,但是也有点区别。
//test.js
const a = 'aaa';
const b = 'bbb';
const c = 'ccc';
const d = 'ddd';
function fn() {
  console.log(`fn执行`);
}
export {a}
export {b}
export default { c, d, fn}
//app.js
import {a as A} from './test'; // aaa
import {* as A} from './test'; // 这是不支持的
import * as obj from './test';
//obj => {a:'aaa',b:'ccc',default:{c:'ccc',d:'ddd',fn: [Function: fn] }}
//结果就是在import {} 中不支持使用*

CommonJS 导入和导出

  • require是运行时调用,所以require理论上可以运用在代码的任何地方。require支持动态引入。
//dome1
let flag = true;
if (flag) {
  const a = require('./test.js');
  console.log(a); //这是被支持
}
//dom2
let flag = true;
let url = './test.js';
if (flag) {
  const a = require(url);
  console.log(a) //这是被支持
}
  • 根据规范,每个文件就是一个模块,有自己的作用域,文件里面每个变量、函数、类都是私有的,对其他文件不可见。
  • 每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性是对外的接口,加载某个模块实际是加载该模块的module.exports属性。每个模块其实是被一个匿名函数包裹着,这是模块私有的本质。 (function (exports, require, module, __filename, __dirname) { }))
  • exports 等价于 module.exports,exports是一个空对象,可以被覆盖,但这容易切断两者的联系。
//test.js;
const a = 'aaa';
const b = 'bbb';
const c = 'ccc';
module.exports = { c } //切断了联系,exports失效了
exports.b = b ; //没有了
module.exports.a = a;
//app.js
const obj = require('./test.js');
console.log(obj); //{ c: 'ccc', a: 'aaa' }

简单总结

(1)require,exports,module.export属于CommonJS规范,import,export,export default属于ES Module规范。

(2)require支持动态导入,动态匹配路径,import对这两者都不支持。

(3)require是运行时调用,import是编译时调用。

(4)require是赋值过程,import是解构过程。

(6)对于export和export default 不同的使用方式,import就要采取不同的引用方式,主要区别在于是否存在 {},export导出的,import导入需要{},导入和导出一一对应,export default默认导出的,import导入不需要{}。

(7)exports是module.export一种简写形式,不能直接给exports赋值当直接给module.export赋值时,exports会失效。

PS:小白初文,文笔不好,望大佬轻喷,俺只是单纯为了混缸子来的而已。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。