如何为prel语言加载模块_从零开始学习NodeJS-第二章(前端模块化之路)

JavaScript模块机制

JS模块化

自我使用JS这门语言以来,JS在整个发展历史中不断的变迁和优化,随着使用者的增多和浏览器的支持规范的发展,JS的发展大致我划分了六个阶段。 表单校验 --> 工具类库 --> 组件库 --> 前端框架 --> 前端应用 --> 前端微服务

55b8f487dc51f7f60a238835dd6d1fd5.png

前端模块化的发展是把 函数 作为第一步的:模块的本质就是实现特定功能的一组方法,在JavaScript中,函数是创建局部作用域的唯一方式,因此采用了函数作为JS模块化的第一步。

function cut(){
    //具体实现
}

Cut 方法可以作为模块被直接调用,这样的好处是实现了代码的复用,缺点是很容易造成命名冲突,并且污染了全局变量。当页面抽离出的函数模块较多时,看不出它们之间的组织关系。

函数挂载到对象 上是前端模块化迈出的第二步。把功能函数挂载到一个对象上,有需要的时候,直接通过调用对象属性的方式进行函数调用。 这里隐隐有股Java类的感觉,学过Java的可能会感觉熟悉,本质上就是namespace模式的使用。C++的应该很了解。

const FileUnit= {
  path:undefined,
  read: function(){},
  write: function(){}
}
FileUnit.read(path);

FileUnit对象挂载了模块上的成员函数和变量。在进行使用的时候,就是调用这个对象的属性。这样的写法解决了第一步的问题,但是,由此衍生出了新的问题,对象会暴露出所有的成员变量和函数,并且挂载的函数和变量有可能被改写。

立即执行函数(IIFE) 是为了解决暴露对象而迈出的第三步。

//什么是立即执行函数?-->就是声明完成立刻执行的函数
function x(){} //声明函数x
x();//调用函数x

//IIFE 声明完成立刻调用 (fn(){})()
const module= (function(){
    let _path = "./xxx.json";
    let cut = function(){
      console.log(_private)
      }
    return {
        cut: cut
    }
})()
module.cut();
module._path; // undefined

立即执行函数(IIFE) 允许在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量,而在模块外部无法修改未暴露的私有变量和函数。但缺点也很明显,很多时候,我们的模块都是架设在别的模块的依赖上进行封装,那当这个module模块依赖另一个模块怎么办?在进行探索之后,前端模块化迈出了第四步。

引入依赖

const module= (function(window,$){
    let cut = function(){
      console.log(window,$)
      }
    return {
        cut: cut
    }
})(window,jQuery)

将要引入的依赖作为匿名函数的参数进行传入,由此就可以在函数内部访问到依赖的下级模块进行逻辑的封装,由此开始,前端模块化正式开始了漫漫长路。也由此,前端模块化题提出了几个不得不解决的问题:

  • 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  • 如何唯一标识一个模块?
  • 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  • 如何方便的使用所依赖的模块?

前端的工程师对于上述问题给出的所有解决方案都有一个共同点:使用单个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。由此,前端模块规范诞生了。

JS的模块化之路

  • CommonJS 规范
  • AMD
  • CMD
  • ES6模块

CommonJS 规范

CommonJS 规范为Javascript制定了一个美好的愿景 ——— Javascript能够在任何地方运行。

出发点

在Web的发展过程中,前端的规范化在稳步推行,但后端的JS规范却远远落后,对后端的JS自身而言,还有很多缺陷 - 没有模块系统 - 标准版较少 - 缺乏包管理系统 - 没有标准化的接口

CommonJS 规范的提出,主要是为了弥补后端JS没有标准的缺陷,希望JS具有开发类似Java或者Python的大型应用的基础能力。

模块规范

CommonJS 对模块的定义分为三个部分 - 模块引用 - 模块定义 - 模块标识

//模块引用
const math=require('math')

require方法接收一个 模块标识 ,以此引入一个模块的API到上下文环境中。

//模块定义
exports.add =function(a,b){
  return a+b
}
//module.exports = value
//exports.xxx = value

CommonJS规范规定,一个文件就是一个模块。每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

```javascript //模块标识 模块标识就是传递给require()方法的参数。参数必须是符合小驼峰命名的字符串,或者以./ .. 开头的相对路径或绝对路径,可以省略js后缀。

CommonJS模块定义简单,接口简洁,**成功的将类聚的方法和变量等限定在私有作用域的中,同时支持导入和导出功能以顺畅的连接上下游依赖。**

#### 模块的加载机制

在服务器端,CommonJS模块的加载是同步的,并且模块加载的顺序,按照其在代码中出现的顺序加载,模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。和其他模块加载机制有重大不同的是CommonJS模块的加载输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异。

```javascript
// lib.js
let counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
const lib = require('./lib');
console.log(lib.counter);  // 3
lib.incCounter();
console.log(lib.counter); // 3

counter输出以后,lib.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

AMD

CommonJS模块规范的推出使得服务端Js开始模块化,但是CommonJS规范对浏览器端并不适用,CommonJS模块的加载是同步加载,并且按照其在代码中出现的顺序加载,浏览器端的模块都存放在服务器上,加载模块的等待时间取决于网速快慢,如果网速较差,会导致浏览器处于"假死"状态。因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。()

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

模块定义和使用

AMD一开始是CommonJS规范中的一个草案,全称是Asynchronous Module Definition,即异步模块加载机制。后来由该草案的作者以RequireJS实现了AMD规范,所以一般说AMD也是指RequireJS。AMD规范的实现源于想要一种比当今“编写一堆必须手动排序的具有隐式依赖项的脚本标签”更好的模块依赖解决方案而产生,AMD模块希望可以在浏览器中直接使用,并且具有良好的调试性且不依赖与特定化的服务器。AMD规范设计源于Dojo使用XHR + eval的真实经验。

RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。

//定义没有依赖的模块
define(id?, dependencies?, factory);
//id :可选参数,它指的是模块的名字。
//dependencies:可选参数,定义中模块所依赖模块的数组。
//factory:模块初始化要执行的函数或对象
//
define("alpha", ["require", "exports", "module"], function (require, exports, module) {  
      //三种暴露API的方式
      // exports.xxx=xxx
      // module.exports=xxx         
      // return xxx;               

});

//模块引入
require([module], callback);
//module:一个数组,里面的成员就是要加载的模块.
//callback:模块加载成功之后的回调函数。
require(["a","b","c"],function(a,b,c){
  //Code 
});

从总体上说,AMD规范修正了很多CMD规范的细节问题,并在模块化的路子上进行了并行的迈进。这里给出一个我觉得讲的不错的AMD规范的文章:Why AMD ? 有兴趣的可以读一读详细的了解AMD规范做了哪些改进。

CMD

CMD 即Common Module Definition, CMD是sea.js的作者在推广sea.js时提出的一种规范.SeaJS与RequireJS并称,SeaJS作者为阿里的玉伯。CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

在CMD规范中,一个模块就是一个文件。代码的书写格式如下:

define(function(require, exports, module) {
    // 模块代码
    // 使用require获取依赖模块的接口
    // 使用exports或者module或者return来暴露该模块的对外接口
})

CMD规范采用全局的define函数定义模块, 无需罗列依赖数组,在factory函数中需传入形参require,exports,module. require用来加载一个 js 文件模块和获取指定模块的接口对象module.exports

Sea.js加载依赖的方式分为两个时期:

  • 加载期:即在执行一个模块之前,将其直接或间接依赖的模块从服务器端同步到浏览器端;
  • 执行期:在确认该模块直接或间接依赖的模块都加载完毕之后,执行该模块。

那作为异步加载模块的两种规范,AMD和CMD各有千秋。

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块,CMD推崇就近依赖,只有在用到某个模块的时候再去require。
  2. AMD和CMD最大的区别是对依赖模块的执行时机处理不同,同样都是异步加载模块,AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑。CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。 因此AMD模块的使用者用户体验好,因为没有延迟,依赖模块提前执行了,CMD性能好,只有用户需要的时候才执行对应的模块。

ES6

上述提到的模块规范的都不属于JS原生支持的, 在ECMAScript 6 (ES6)中,引入了模块功能, ES6 的模块功能汲取了CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。

ES6 模块的设计思想就是:一个 JS 文件就代表一个 JS 模块。在模块中你可以使用 import 和 export 关键字来导入或导出模块中的东西。

ES6 模块主要具备以下几个基本特点:

  • 自动开启严格模式,即使你没有写 use strict
  • 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域
  • 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等
  • 每一个模块只加载一次,每一个 JS 只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。
/** 定义模块 math.js **/
let basicNum = 0;
let add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(item) {
    item.textContent = add(99 + basicNum);
}

上述的例子采用了math.js定义了模块,通过export导出了一个挂在了basicNum, add两个函数的对象。在进行引用的时候,通过对对象进行析构得到导出的对应函数。这种加载方式,使得在采用import命令进行导入的时候,用户需要知道所要加载的变量名或函数名,否则无法加载出对应的函数。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

这种导入方式使得用户可以为匿名函数指定任意名字进行使用。

ES6 模块与 CommonJS 模块的差异

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • commonJS模块一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6模块如果使用import从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,原始值变了,import加载的值也会跟着变。需要开发者自己保证,真正取值的时候能够取到值。

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

-运行时加载:commonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上读取方法,这种加载称为“运行时加载”。commonJS脚本代码在require的时候,就会全部执行。一旦出现某个模板被“循环加载”,就只能输出已经执行的部分,还未执行的部分不会输出。 - 编译时加载:ES6 模块不是对象,而是通过export命令显式指定输出的代码,import时指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

③ ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

总结

本片简述了JS的一路发展以来,模块化的历程,从最开始模块化探索,到横空出世的CommonJS规范,从草案开始的AMD规范到Sea.js推广的CMD规范,最后官方的ES6模块化为模块化立下了一个阶段的里程碑。下篇将会回到NodejS谈谈Node中模块化和核心模块。 因为笔者水平有限,如果有错误或者遗漏,欢迎留言进行建议。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值