前者模块化

2020-03-17 作者:网站首页   |   浏览(101)

时间: 2019-12-27阅读: 78标签: 模块化

时间: 2019-01-31阅读: 275标签: 模块模块化开发优点模块化开发中,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数,并且可以按需加载。依赖自动加载,按需加载。提高代码复用率,方便进行代码的管理,使得代码管理更加清晰、规范。减少了命名冲突,消除全局变量。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统常见模块化规范CommonJs (Node.js)AMD (RequireJS)CMD (SeaJS)CommonJS(Node.js)

模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。参见阮一峰老师的文章module-loader。

CommonJS是服务器模块的规范,Node.js采用了这个规范。

一、CommonJS

根据CommonJS规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

// 定义模块math.jsvar basicNum = 0;function add(a, b) { return a   b;}module.exports = { //在这里写上需要向外暴露的函数、变量 add: add, basicNum: basicNum}// 引用自定义的模块时,参数包含路径,可省略.jsvar math = require(‘./math‘);math.add(2, 5);// 引用核心模块时,不需要带路径var http = require(‘http‘);(...).listen(3000);

CommonJS中,加载模块使用require方法。该方法读取一个文件并执行,最后返回文件内部的exports对象。

commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

Node.js主要用于服务器编程,加载的模块文件一般都已经存在本地硬盘,加载起来较快,不用考虑异步加载的方式,所以CommonJS的同步加载模块规范是比较适用的。

二、AMD和require.js

但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了AMD,CMD等解决方案。

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。

var x = 5;var addX = function(value) { return value   x;};module.exports.x = x;module.exports.addX = addX;// 也可以改写为如下module.exports = { x: x, addX: addX,};

let math = require('./math.js');console.log('math.x',math.x);console.log('math.addX', math.addX(4));

首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。

AMD (RequireJS) 异步模块定义AMD=Asynchronous Module Definition,即异步模块定义AMD规范加载模块是异步的,并允许函数回调,不必等到所有模块都加载完成,后续操作可以正常执行。AMD中,使用require获取依赖模块,使用exports导出API。

/** 网页中引入require.js及main.js **/script src="js/require.js" data-main="js/main"/script/** main.js 入口文件/主模块 **/// 首先用config()指定各模块路径和引用名require.config({ baseUrl: "js/lib", paths: { "jquery": "jquery.min", //实际路径为js/lib/jquery.min.js "underscore": "underscore.min", }});// 执行基本操作require(["jquery","underscore"],function($,_){ // some code here});
//规范 APIdefine(id?, dependencies?, factory);define.amd = {};// 定义无依赖的模块define({ add: function(x,y){ return x   y; }});// 定义有依赖的模块define(["alpha"], function(alpha){ return { verb: function(){ return alpha.verb()   1; } }});

引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。

异步加载和回调require([module], callback)中callback为模块加载完成后的回调函数

// 定义math.js模块define(function () { var basicNum = 0; var add = function (x, y) { return x   y; }; return { add: add, basicNum :basicNum };});// 定义一个依赖underscore.js的模块define([‘underscore‘],function(_){ var classify = function(list){ _.countBy(list,function(num){ return num  30 ? ‘old‘ : ‘young‘; }) }; return { classify :classify };})// 引用模块,将模块放在[]内require([‘jquery‘, ‘math‘],function($, math){ var sum = math.add(10,20); $("#sum").html(sum);});
//加载 math模块,完成之后执行回调函数require(['math'], function(math) { math.add(2, 3);});

三、CMD和sea.js

RequireJS

require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码:

RequireJS是一个前端模块化管理的工具库,遵循AMD规范,RequireJS是对AMD规范的阐述。

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的所有模块 if (false) { // 即便没用到某个模块 b,但 b 还是提前执行了 b.foo() } });

RequireJS基本思想为,通过一个函数来将所有所需的或者所依赖的模块装载进来,然后返回一个新的函数(模块)。后续所有的关于新模块的业务代码都在这个函数内部操作。

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

RequireJS要求每个模块均放在独立的文件之中,并使用define定义模块,使用require方法调用模块。

/** AMD写法 **/define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的所有模块 a.doSomething(); if (false) { // 即便没用到某个模块 b,但 b 还是提前执行了 b.doSomething() } });/** CMD写法 **/define(function(require, exports, module) { var a = require(‘./a‘); //在需要时申明 a.doSomething(); if (false) { var b = require(‘./b‘); b.doSomething(); }});/** sea.js **/// 定义模块 math.jsdefine(function(require, exports, module) { var $ = require(‘jquery.js‘); var add = function(a,b){ return a b; } exports.add = add;});// 加载模块seajs.use([‘math.js‘], function(math){ var sum = math.add(1 2);});

按照是否有依赖其他模块情况,可以分为独立模块非独立模块

四、ES6 Module

独立模块,不依赖其他模块,直接定义

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

define({ method1: function(){}, method2: function(){}});//等价于define(function() { return { method1: function(){}, method2: function(){} }});
/** 定义模块 math.js **/var basicNum = 0;var add = function (a, b) { return a   b;};export { basicNum, add };/** 引用模块 **/import { basicNum, add } from ‘./math‘;function test(ele) { ele.textContent = add(99   basicNum);}

非独立模块,依赖其他模块

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

define([ 'module1', 'module2' ], function(m1, m2) { ...});//等价于define(function(require) { var m1 = require('module1'); var m2 = require('module2'); ...});
/** export default **///定义输出export default { basicNum, add };//引入import math from ‘./math‘;function test(ele) { ele.textContent = math.add(99   math.basicNum);}

require方法调用模块

ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

require(['foo', 'bar'], function(foo, bar) { foo.func(); bar.func();});

五、 ES6 模块与 CommonJS 模块的差异1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

CMD (SeaJS)

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

CMD=Common Module Definition,即通用模块定义。CMD是SeaJS在推广过程中对模块定义的规范化产出。

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CMD规范和AMD类似,都主要运行于浏览器端,写法上看起来也很类似。主要是区别在于模块初始化时机

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

AMD中只要模块作为依赖时,就会加载并初始化CMD中,模块作为依赖且被引用时才会初始化,否则只会加载。CMD推崇依赖就近,AMD推崇依赖前置。AMD的API默认是一个当多个用,CMD严格的区分推崇职责单一。例如,AMD里require分全局的和局部的。CMD里面没有全局的require,提供seajs.use()来实现模块系统的加载启动。CMD里每个API都简单纯粹。

运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。

//AMDdefine(['./a','./b'], function (a, b) { //依赖一开始就写好 a.test(); b.test();}); //CMDdefine(function (requie, exports, module) { //依赖可以就近书写 var a = require('./a'); a.test(); ... //软依赖 if (status) { var b = requie('./b'); b.test(); }});

编译时加载: ES6 模块不是对象,而是通过export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

Sea.jsSea.js Github PageSeaJS与RequireJS最大的区别

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

使用Sea.js,在书写文件时,需要遵守CMD(Common Module Definition)模块定义规范。一个文件就是一个模块。

来源:掘金,原链接:

用法通过exports暴露接口。这意味着不需要命名空间了,更不需要全局变量。这是一种彻底的命名冲突解决方案。通过require引入依赖。这可以让依赖内置,开发者只需关心当前模块的依赖,其他事情Sea.js都会自动处理好。对模块开发者来说,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣。通过define定义模块,更多详情参考SeasJS | 极客学院。示例

例如,对于下述util.js代码

var org = {};org.CoolSite = {};org.CoolSite.Utils = {};org.CoolSite.Utils.each = function (arr) { // 实现代码};org.CoolSite.Utils.log = function (str) { // 实现代码};

可以采用SeaJS重写为

define(function(require, exports) { exports.each = function (arr) { // 实现代码 }; exports.log = function (str) { // 实现代码 };});

通过exports就可以向外提供接口。通过require('./util.js')就可以拿到util.js中通过exports暴露的接口。这里的require可以认为是Sea.js给 JavaScript 语言增加的一个语法关键字,通过require可以获取其他模块提供的接口。

define(function(require, exports) { var util = require('./util.js'); exports.init = function() { // 实现代码 };});

SeaJS与RequireJS区别

二者区别主要表现在模块初始化时机

AMD(RequireJS)中只要模块作为依赖时,就会加载并初始化。即尽早地执行(依赖)模块。相当于所有的require都被提前了,而且模块执行的顺序也不一定100%就是require书写顺序。CMD(SeaJS)中,模块作为依赖且被引用时才会初始化,否则只会加载。即只会在模块真正需要使用的时候才初始化。模块加载的顺序是严格按照require书写的顺序。

从规范上来说,AMD 更加简单且严谨,适用性更广,而在RequireJS强力的推动下,在国外几乎成了事实上的异步模块标准,各大类库也相继支持AMD规范。

但从SeaJS与CMD来说,也做了很多不错东西:1、相对自然的依赖声明风格 2、小而美的内部实现 3、贴心的外围功能设计 4、更好的中文社区支持。

UMDUMD=Universal Module Definition,即通用模块定义。UMD是AMD和CommonJS的糅合。AMD模块以浏览器第一的原则发展,异步加载模块。CommonJS模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。这迫使人们又想出另一个更通用的模式UMD(Universal Module Definition),实现跨平台的解决方案。UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

(function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function'  define.amd) { define(factory); } else { window.eventUtil = factory(); }})(this, function () { //module ...});

ES6 模块ES6模块和CommonJS区别ES6 模块输出的是值的引用,输出接口动态绑定,而CommonJS输出的是值的拷贝。CommonJS模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 输出值的拷贝

CommonJS 模块输出的是值的拷贝(类比于基本类型和引用类型的赋值操作)。对于基本类型,一旦输出,模块内部的变化影响不到这个值。对于引用类型,效果同引用类型的赋值操作。

// lib.jsvar counter = 3;var obj = { name: 'David'};function changeValue() { counter  ; obj.name = 'Peter';};module.exports = { counter: counter, obj: obj, changeValue: changeValue,};

// main.jsvar mod = require('./lib');console.log(mod.counter); // 3console.log(mod.obj.name); // 'David'mod.changeValue();console.log(mod.counter); // 3console.log(mod.obj.name); // 'Peter'// Orconsole.log(require('./lib').counter); // 3console.log(require('./lib').obj.name); // 'Peter'

counter是基本类型值,模块内部值的变化不影响输出的值变化。obj是引用类型值,模块内部值的变化影响输出的值变化。上述两点区别,类比于基本类型和引用类型的赋值操作。

也可以借助取值函数(getter),将counter转为引用类型值,效果如下。

在类的内部,可以使用get和set关键字,对某个属性设置存执函数和取值函数,拦截该属性的存取行为。 ——class | 阮一峰

// lib.jsvar counter = 3;function incCounter() { counter  ;}module.exports = { get counter() { return counter }, incCounter: incCounter,};

// main.jsvar mod = require('./lib');console.log(mod.counter); // 3mod.incCounter();console.log(mod.counter); // 4

ES6 输出值的引用

ES6 模块是动态关联模块中的值,输出的是值得引用。原始值变了,import加载的值也会跟着变。

ES6模块的运行机制与CommonJS不一样。JS 引擎对脚本静态分析时,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块中,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值。 ——ES6 Module 的加载实现 | 阮一峰

// lib.jsexport let counter = 3;export function incCounter() { counter  ;}// main.jsimport { counter, incCounter } from './lib';console.log(counter); // 3incCounter();console.log(counter); // 4

CommonJS 运行时加载 ES6静态编译

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

这是因为,CommonJS加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

ES6 模块是编译时输出接口,因此有如下2个特点

import命令会被 JS 引擎静态分析,优先于模块内的其他内容执行export命令会有变量声明提升的效果import 优先执行

在文件中的任何位置引入import模块都会被提前到文件顶部

// a.jsconsole.log('a.js')import { foo } from './b';// b.jsexport let foo = 1;console.log('b.js 先执行');// 执行结果:// b.js 先执行// a.js

虽然a模块中import引入晚于console.log('a'),但是它被 JS 引擎通过静态分析,提到模块执行的最前面,优于模块中的其他部分的执行。

export 命令变量提升效果

由于import和export是静态执行,所以import和export具有变量提升效果。即import和export命令在模块中的位置并不影响程序的输出。

// a.jsimport { foo } from './b';console.log('a.js');export const bar = 1;export const bar2 = () = { console.log('bar2');}export function bar3() { console.log('bar3');}// b.jsexport let foo = 1;import * as a from './a';console.log(a);// 执行结果:// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }// a.js

a模块引用了b模块,b模块也引用了a模块,export声明的变量也是优于模块其它内容的执行的。但具体对变量赋值需要等到执行到相应代码的时候。

ES6模块和CommonJS相同点模块不会重复执行

重复引入某个相同的模块时,模块只会执行一次。

循环依赖CommonJS 模块循环依赖

CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

Demo 1

//a.jsexports.done = false;var b = require('./b.js');console.log('在 a.js 之中,b.done = %j', b.done);exports.done = true;console.log('a.js 执行完毕');

上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

再看b.js的代码。

//b.jsexports.done = false;var a = require('./a.js');console.log('在 b.js 之中,a.done = %j', a.done);exports.done = true;console.log('b.js 执行完毕');

上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

a.js已经执行的部分,只有一行。

exports.done =false;

因此,对于b.js来说,它从a.js只输入一个变量done,值为false。

然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

var a = require('./a.js');var b = require('./b.js');console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运行结果如下。

$ node main.js在 b.js 之中,a.done = falseb.js 执行完毕在 a.js 之中,b.done = truea.js 执行完毕在 main.js 之中, a.done=true, b.done=true

上面的代码证明了2点

在b.js之中,a.js没有执行完毕,只执行了第一行main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

exports.done =true;

总之,CommonJS 输入的是被输出值的拷贝,不是引用。

另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

var a = require('a'); // 安全的写法 导入整体,保证module已经执行完成var foo = require('a').foo; // 危险的写法exports.good = function (arg) { return a.foo('good', arg); // 使用的是 a.foo 的最新值};exports.bad = function (arg) { return foo('bad', arg); // 使用的是一个部分加载时的值};

上面代码中,如果发生循环加载,require('a').foo的值很可能后面会被改写,改用require('a')会更保险一点。

Demo 2

// a.jsconsole.log('a starting');exports.done = false;const b = require('./b');console.log('in a, b.done =', b.done);exports.done = true;console.log('a done');// b.jsconsole.log('b starting');exports.done = false;const a = require('./a');console.log('in b, a.done =', a.done);exports.done = true;console.log('b done');// node a.js// 执行结果:// a starting// b starting// in b, a.done = false// b done// in a, b.done = true// a done

从上面的执行过程中,可以看到,在 CommonJS 规范中,当遇到require()语句时,会执行require模块中的代码,并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。正因为此,出现循环依赖时才不会出现无限循环调用的情况。

ES6 模块循环依赖

跟 CommonJS 模块一样,ES6 不会再去执行重复加载的模块,又由于 ES6 动态输出绑定的特性,能保证 ES6 在任何时候都能获取其它模块当前的最新值。

动态 import()

ES6 模块在编译时就会静态分析,优先于模块内的其他内容执行,所以导致了我们无法写出像下面这样的代码

if(some condition) { import a from './a';}else { import b from './b';}// or import a from (str   'b');

因为编译时静态分析,导致了我们无法在条件语句或者拼接字符串模块,因为这些都是需要在运行时才能确定的结果在 ES6 模块是不被允许的,所以 动态引入import()应运而生。

import()允许你在运行时动态地引入 ES6 模块,想到这,你可能也想起了require.ensure这个语法,但是它们的用途却截然不同的。

require.ensure的出现是webpack的产物,它是因为浏览器需要一种异步的机制可以用来异步加载模块,从而减少初始的加载文件的体积,所以如果在服务端的话,require.ensure就无用武之地了,因为服务端不存在异步加载模块的情况,模块同步进行加载就可以满足使用场景了。 CommonJS 模块可以在运行时确认模块加载。

而import()则不同,它主要是为了解决 ES6 模块无法在运行时确定模块的引用关系,所以需要引入import()。

先来看下它的用法

动态的import()提供一个基于Promise的API动态的import()可以在脚本的任何地方使用import()接受字符串文字,可以根据需要构造说明符

// a.jsconst str = './b';const flag = true;if(flag) { import('./b').then(({foo}) = { console.log(foo); })}import(str).then(({foo}) = { console.log(foo);})// b.jsexport const foo = 'foo';// babel-node a.js// 执行结果// foo// foo

当然,如果在浏览器端的import()的用途就会变得更广泛,比如 按需异步加载模块,那么就和require.ensure功能类似了。

因为是基于Promise的,所以如果你想要同时加载多个模块的话,可以是Promise.all进行并行异步加载。

Promise.all([ import('./a.js'), import('./b.js'), import('./c.js'),]).then(([a, {default: b}, {c}]) = { console.log('a.js is loaded dynamically'); console.log('b.js is loaded dynamically'); console.log('c.js is loaded dynamically');});

还有Promise.race方法,它检查哪个Promise被首先resolved或reject。我们可以使用import()来检查哪个CDN速度更快:

const CDNs = [ { name: 'jQuery.com', url: '-3.1.1.min.js' }, { name: 'googleapis.com', url: '' }];console.log(`------`);console.log(`jQuery is: ${window.jQuery}`);Promise.race([ import(CDNs[0].url).then(()=console.log(CDNs[0].name, 'loaded')), import(CDNs[1].url).then(()=console.log(CDNs[1].name, 'loaded'))]).then(()= { console.log(`jQuery version: ${window.jQuery.fn.jquery}`);});

当然,如果你觉得这样写还不够优雅,也可以结合async/await语法糖来使用。

async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]);}

动态import()为我们提供了以异步方式使用 ES 模块的额外功能。

根据我们的需求动态或有条件地加载它们,这使我们能够更快,更好地创建更多优势应用程序。

webpack中加载3种模块 | 语法

Webpack允许使用不同的模块类型,但是底层必须使用同一种实现。所有的模块可以直接在盒外运行。

ES6 模块

importMyModulefrom'./MyModule.js';

CommonJS(Require)

varMyModule =require('./MyModule.js');

AMD

define(['./MyModule.js'], function (MyModule) {});

本文由yzc216亚洲城发布于网站首页,转载请注明出处:前者模块化

关键词: yzc216亚洲城