30分钟学会前端模块化开发
早期的javascript版本没有块级作用域、没有类、没有包、也没有模块,这样会带来一些问题,如复用、依赖、冲突、代码组织混乱等,随着前端的膨胀,模块化显得非常迫切。
前端模块化规范如下:
一、前端模块化概要
1.1、模块概要
JavaScript在早期的设计中就没有模块、包、类的概念,开发者需要模拟出类似的功能,来隔离、组织复杂的JavaScript代码,我们称为模块化。
模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块。
模块化开发的四点好处:
(1)、 避免变量污染,命名冲突
(2)、提高代码复用率
(3)、提高了可维护性
(4)、方便依赖关系管理
为了避免缺少模块带来的问题,我们可以看看程序员应对的历程:
1.2、函数封装
我们在讲函数的时候提到,函数一个功能就是实现特定逻辑的一组语句打包,而且JavaScript的作用域就是基于函数的,所以把函数作为模块化的第一步是很自然的事情,在一个文件里面编写几个相关函数就是最开始的模块了
//函数1 function fn1(){ //statement } //函数2 function fn2(){ //statement }
这样在需要的以后夹在函数所在文件,调用函数就可以了
缺点:
污染了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间没什么关系
1.3、对象封装
为了解决上面问题,对象的写法应运而生,可以把所有的模块成员封装在一个对象中
var myModule = { var1: 1, var2: 2, fn1: function(){ }, fn2: function(){ } }
这样我们在希望调用模块的时候引用对应文件,然后
myModule.fn2();
这样避免了变量污染,只要保证模块名唯一即可,同时同一模块内的成员也有了关系
缺陷:外部可以随意修改内部成员,这样就会产生意外的安全问题
myModel.var1 = 100;
1.4、立即执行函数表达式(IIFE)
可以通过立即执行函数表达式(IIFE),来达到隐藏细节的目的
var myModule = (function(){ var var1 = 1; var var2 = 2; function fn1(){ } function fn2(){ } return { fn1: fn1, fn2: fn2 }; })();
这样在模块外部无法修改我们没有暴露出来的变量、函数
缺点:功能相对较弱,封装过程增加了工作量、仍会导致命名空间污染可能、闭包是有成本的。
JavaScript最初的作用仅仅是验证表单,后来会添加一些动画,但是这些js代码很多在一个文件中就可以完成了,所以,我们只需要在html文件中添加一个script标签。
后来,随着前端复杂度提高,为了能够提高项目代码的可读性、可扩展性等,我们的js文件逐渐多了起来,不再是一个js文件就可以解决的了,而是把每一个js文件当做一个模块。那么,这时的js引入方式是怎样的呢?大概是下面这样:
<script src="jquery.js"></script> <script src="jquery.artDialog.js"></script> <script src="main.js"></script> <script src="app1.js"></script> <script src="app2.js"></script> <script src="app3.js"></script>
即简单的将所有的js文件统统放在一起。但是这些文件的顺序还不能出错,比如jquery需要先引入,才能引入jquery插件,才能在其他的文件中使用jquery。
优点:
相比于使用一个js文件,这种多个js文件实现最简单的模块化的思想是进步的。
缺点:
污染全局作用域。 因为每一个模块都是暴露在全局的,简单的使用,会导致全局变量命名冲突,当然,我们也可以使用命名空间的方式来解决。
对于大型项目,各种js很多,开发人员必须手动解决模块和代码库的依赖关系,后期维护成本较高。
依赖关系不明显,不利于维护。 比如main.js需要使用jquery,但是,从上面的文件中,我们是看不出来的,如果jquery忘记了,那么就会报错。
1.5、模块化规范
常见的的JavaScript模块规范有:CommonJS、AMD、CMD、UMD、原生模块化
1.5.1、CommonJS
CommonJs 是服务器端模块的规范,Node.js采用了这个规范。
根据CommonJS规范,一个单独的文件就是一个模块。加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。
例如:
// foobar.js //私有变量 var test = 123; //公有方法 function foobar () { this.foo = function () { // do someing ... } this.bar = function () { //do someing ... } } //exports对象上的方法和变量是公有的 var foobar = new foobar(); exports.foobar = foobar; //require方法默认读取js文件,所以可以省略js后缀 var test = require(\'./foobar\').foobar; test.bar();
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
1.5.2、AMD((Asynchromous Module Definition) 异步模块定义
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
AMD异步加载模块。它的模块支持对象 函数 构造器 字符串 JSON等各种类型的模块。
适用AMD规范适用define方法定义模块。
//通过数组引入依赖 ,回调函数通过形参传入依赖 define([\'someModule1\', ‘someModule2’], function (someModule1, someModule2) { function foo () { /// someing someModule1.test(); } return {foo: foo} });
AMD规范允许输出模块兼容CommonJS规范,这时define方法如下:
define(function (require, exports, module) { var reqModule = require("./someModule"); requModule.test(); exports.asplode = function () { //someing } });
1.5.3、CMD(Common Module Definition)通用模块定义
CMD是SeaJS 在推广过程中对模块定义的规范化产出
CMD和AMD的区别有以下几点:
1.对于依赖的模块AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
2.CMD推崇依赖就近,AMD推崇依赖前置。
//AMD define([\'./a\',\'./b\'], function (a, b) { //依赖一开始就写好 a.test(); b.test(); }); //CMD define(function (requie, exports, module) { //依赖可以就近书写 var a = require(\'./a\'); a.test(); ... //软依赖 if (status) { var b = requie(\'./b\'); b.test(); } });
虽然 AMD也支持CMD写法,但依赖前置是官方文档的默认模块定义写法。
3.AMD的api默认是一个当多个用,CMD严格的区分推崇职责单一。例如:AMD里require分全局的和局部的。CMD里面没有全局的 require,提供 seajs.use()来实现模块系统的加载启动。CMD里每个API都简单纯粹。
SeaJS 和 RequireJS的主要区别 在此有解释
1.5.4、UMD
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 ... });
1.5.5、原生JS模块化(Native JS)
上述的模块都不是原生 JavaScript 模块。它们只不过是我们用模块模式(module pattern)、CommonJS 或 AMD 模仿的模块系统。
JavaScript标准制定者在 TC39(该标准定义了 ECMAScript 的语法与语义)已经为 ECMAScript 6(ES6)引入内置的模块系统了。
ES6 为导入(importing)导出(exporting)模块带来了很多可能性。下面是很好的资源:
http://jsmodules.io/
http://exploringjs.com/
相对于 CommonJS 或 AMD,ES6 模块如何设法提供两全其美的实现方案:简洁紧凑的声明式语法和异步加载,另外能更好地支持循环依赖。
1.5.6、小结
AMD(异步模块定义) 是 RequireJS 在推广过程中对模块定义的规范化产出,CMD(通用模块定义)是SeaJS 在推广过程中被广泛认知。RequireJs出自dojo加载器的作者James Burke,SeaJs出自国内前端大师玉伯。
两者的区别如下:
RequireJS 和 SeaJS 都是很不错的模块加载器,两者区别如下: 1. 两者定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端 2. 两者遵循的标准有差异。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者 API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。 3. 两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。 4. 两者代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。 5. 两者对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便便捷。RequireJS 无这方面的支持。 6. 两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致:开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。
二、CommonJS
CommonJS就是一个JavaScript模块化的规范,该规范最初是用在服务器端NodeJS中,前端的webpack也是对CommonJS原生支持的。
根据这个规范,每一个文件就是一个模块,其内部定义的变量是属于这个模块的,不会对外暴露,也就是说不会污染全局变量。
CommonJS的核心思想就是通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports 或者 module.exports 来导出需要暴露的接口。
CommonJS API编写应用程序,然后这些应用可以运行在不同的JavaScript解释器和不同的主机环境中。
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志”Javascript模块化编程”正式诞生。因为老实说,在浏览器环境下,以前没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。NodeJS是CommonJS规范的实现,webpack 也是以CommonJS的形式来书写。
CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)} //require()用来引入外部模块; //exports对象用于导出当前模块的方法或变量,唯一的导出口; //module对象就代表模块本身。
Nodejs的模块是基于CommonJS规范实现的,通过转换也可以运行在浏览器端。
特点:
2.1、NodeJS中使用CommonJS模块管理
1、模块定义
根据commonJS规范,一个单独的文件是一个模块,每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非为global对象的属性。
模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入该对象。
mathLib.js模块定义
var message="Hello CommonJS!"; module.exports.message=message; module.exports.add=(m,n)=>console.log(m+n);
2、模块依赖
加载模块用require方法,该方法读取一个文件并且执行,返回文件内部的module.exports对象。
myApp.js 模块依赖
var math=require(\'./mathLib\'); console.log(math.message); math.add(333,888);
3、测试运行
安装好node.JS
打开控制台,可以使用cmd命令,也可以直接在开发工具中访问
运行
2.2、在浏览器中使用CommonJS 模块管理
var math = require(\'math\'); math.add(2, 3);
第二行math.add(2, 3),在第一行require(\’math\’)之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态
而browserify这样的一个工具,可以把nodejs的模块编译成浏览器可用的模块,解决上面提到的问题。本文将详细介绍Browserify实现Browserify是目前最常用的CommonJS格式转换的工具
请看一个例子,b.js模块加载a.js模块
// a.js var a = 100; module.exports.a = a; // b.js var result = require(\'./a\'); console.log(result.a);
index.html直接引用b.js会报错,提示require没有被定义
//index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="myApp_01.js"></script> </body> </html>
这时,就要使用Browserify了
【安装】
使用下列命令安装browserify
npm install -g browserify
【转换】
使用下面的命令,就能将b.js转为浏览器可用的格式bb.js
$ browserify myApp.js > myApp_01.js
转换结果:
查看myapp_01.js,browserify将mathLib.js和myApp.js这两个文件打包为MyApp01.js,使其在浏览器端可以运行
(function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module \'" + i + "\'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = {exports: {}}; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]); return o } return r })()({ 1: [function (require, module, exports) { var message = "Hello CommonJS!"; module.exports.message = message; module.exports.add = (m, n) => console.log(m + n); }, {}], 2: [function (require, module, exports) { var math = require(\'./mathLib\'); console.log(math.message); math.add(333, 888); }, {"./mathLib": 1}] }, {}, [2]);
index.html引用bb.js,控制台显示100
//index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="bb.js"></script> </body> </html>
优点:
CommonJS规范在服务器端率先完成了JavaScript的模块化,解决了依赖、全局变量污染的问题,这也是js运行在服务器端的必要条件。
缺点:
此文主要是浏览器端js的模块化, 由于 CommonJS 是同步加载模块的,在服务器端,文件都是保存在硬盘上,所以同步加载没有问题,但是对于浏览器端,需要将文件从服务器端请求过来,那么同步加载就不适用了,所以,CommonJS是不太适用于浏览器端。
三、AMD
3.1、概要
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。而AMD规范的实现,就是大名鼎鼎的require.js了。
3.1、require.js
Asynchronous Module Definition,中文名是异步模块定义。它是一个在浏览器端模块化开发的规范,由于不是js原生支持,使用AMD规范进行页面开发需要用到对应的函数库,也就是大名鼎鼎的RequireJS,实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出。Asynchronous Module Definition,中文名是异步模块。它是一个在浏览器端模块化开发的规范,由于不是js原生支持,使用AMD规范进行页面开发需要用到对应的函数库,也就是大名鼎鼎的RequireJS,实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出。
requireJS主要解决两个问题:
1 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
2 js加载的时候浏览器会停止页面渲染,加载文件愈多,页面失去响应的时间愈长。
//定义模块
define([\'dependency\'],function(){ var name = \'foo\'; function printName(){ console.log(name); } return { printName:printName } }) //加载模块 require([\'myModule\'],function(my){ my.printName(); })
语法:
AMD标准中,定义了下面两个API:
1.require([module], callback)
2. define(id, [depends], callback)
即通过define来定义一个模块,然后使用require来加载一个模块。 并且,require还支持CommonJS的模块导出方式。
requireJS定义了一个函数define,它是全局变量,用来定义模块。
define(id,dependencies,factory)
——id 可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
——dependencies 是一个当前模块用来的模块名称数组
——factory 工厂方法,模块初始化要执行的函数或对象,如果为函数,它应该只被执行一次,如果是对象,此对象应该为模块的输出值。
在页面上使用require函数加载模块;
require([dependencies], function(){});
require()函数接受两个参数:
——第一个参数是一个数组,表示所依赖的模块;
——第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
定义alert模块:
define(function () { var alertName = function (str) { alert("I am " + str); } var alertAge = function (num) { alert("I am " + num + " years old"); } return { alertName: alertName, alertAge: alertAge }; });
引入模块:
require([\'alert\'], function (alert) { alert.alertName(\'zhangsan\'); alert.alertAge(21); });
但是,在使用require.js的时候,我们必须要提前加载所有的依赖,然后才可以使用,而不是需要使用时再加载。
优点:
适合在浏览器环境中异步加载模块。可以并行加载多个模块。
缺点:
提高了开发成本,并且不能按需加载,而是必须提前加载所有的依赖。
3.3、使用技巧
请记住使用requirejs的口诀:两函数一配置一属性
3.3.1、data-main属性
requirejs需要一个根来作为搜索依赖的开始,data-main用来指定这个根。
<script src="scripts/require.js" data-main="scripts/app.js"></script>
这里就指定了根是app.js,只有直接或者间接与app.js有依赖关系的模块才会被插入到html中。
3.3.2、require.config() 配置
通过这个函数可以对requirejs进行灵活的配置,其参数为一个配置对象,配置项及含义如下:
baseUrl——用于加载模块的根路径。
paths——用于映射不存在根路径下面的模块路径。
shims——配置在脚本/模块外面并没有使用RequireJS的函数依赖并且初始化函数。假设underscore并没有使用 RequireJS定义,但是你还是想通过RequireJS来使用它,那么你就需要在配置中把它定义为一个shim。
deps——加载依赖关系数组
require.config({ //默认情况下从这个文件开始拉去取资源 baseUrl:\'scripts/app\', //如果你的依赖模块以pb头,会从scripts/pb加载模块。 paths:{ pb:\'../pb\' }, // load backbone as a shim,所谓就是将没有采用requirejs方式定义 //模块的东西转变为requirejs模块 shim:{ \'backbone\':{ deps:[\'underscore\'], exports:\'Backbone\' } } });
3.3.3、define()函数
该函数用于定义模块。形式如下。
//logger.js define(["a"], function(a) { \'use strict\'; function info() { console.log("我是私有函数"); } return { name:"一个属性", test:function(a){ console.log(a+"你好!"); a.f(); info(); } } });
define函数就受两个参数。
* 第一个是一个字符串数组,表示你定义的模块依赖的模块,这里依赖模块a;
* 第二个参数是一个函数,参数是注入前面依赖的模块,顺序同第一参数顺序。在函数中可做逻辑处理,通过return一个对象暴露模块的属性和方法,不在return中的可以认为是私有方法和私有属性。
3.3.4、require()函数
该函数用于调用定义好的模块,可以是用define函数定义的,也可以是一个shim。形式如下:
//app.js require([\'logger\'], function (logger) { logger.test("tom"); console.log(logger.name); }); //输出结果: //tom你好! //不确定(取决于a模块的f方法) //我是私有函数 //一个属性
示例:
//index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script src="js/require.js" data-main="app.js"></script> </body> </html> <script> //app.js require.config({ //By default load any module IDs from js/ baseUrl: \'js\', //except, if the module ID starts with "pb" paths: { pb: \'../pb\' }, shim: { \'world\': { deps:[\'animalWorld\'], // use the global \'Backbone\' as the module name. exports: \'world\' } } }); require([\'cat\',\'dog\',\'world\'], function (cat,dog,world) { world.world(); cat.say(); dog.say(); }); //animal.js define([], function() { \'use strict\'; function _showName(name){ console.log(name); } return { say(words){ console.log(words); }, showName(name){ //练习私有方法 _showName(name); } } }); //cat.js define([ \'pb/animal\' ], function(animal) { \'use strict\'; return { say(){ animal.say("喵喵"); animal.showName("猫"); } } }); //dog.js define([ \'pb/animal\' ], function(animal) { \'use strict\'; return { say(){ animal.say("汪汪"); animal.showName("狗"); } } }); //animalWorld.js window.animal = function() { console.log("这里是动物世界!"); } world.js define([], function() { \'use strict\'; return { world(){ animal(); } } }); </script>
View Code
3.4、简单示例
目录结构:
模块定义:
mathModule.js
define(function () { return{ message:"Hello AMD!", add:function (n1,n2) { return n1+n2; } } });
模块依赖:
app.js
require([\'mathModule\'],function (mathModule) { console.log(mathModule.message); console.log(mathModule.add(100,200)); });
测试运行:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="../js/require2.1.11.js" data-main="app.js"></script> </body> </html>
结果:
3.5、加载 JavaScript 文件
RequireJS的目标是鼓励代码的模块化,它使用了不同于传统<script>标签的脚本加载步骤。可以用它来加速、优化代码,但其主要目的还是为了代码的模块化。它鼓励在使用脚本时以module ID替代URL地址。
RequireJS以一个相对于baseUrl的地址来加载所有的代码。 页面顶层<script>标签含有一个特殊的属性data-main,require.js使用它来启动脚本加载过程,而baseUrl一般设置到与该属性相一致的目录。下列示例中展示了baseUrl的设置:
<!--This sets the baseUrl to the "scripts" directory, and loads a script that will have a module ID of \'main\'--> <script data-main="scripts/main.js" src="scripts/require.js"></script>
baseUrl亦可通过RequireJS config手动设置。如果没有显式指定config及data-main,则默认的baseUrl为包含RequireJS的那个HTML页面的所属目录。
RequireJS默认假定所有的依赖资源都是js脚本,因此无需在module ID上再加”.js”后缀,RequireJS在进行module ID到path的解析时会自动补上后缀。你可以通过paths config设置一组脚本,这些有助于我们在使用脚本时码更少的字。
有时候你想避开”baseUrl + paths”的解析过程,而是直接指定加载某一个目录下的脚本。此时可以这样做:如果一个module ID符合下述规则之一,其ID解析会避开常规的”baseUrl + paths”配置,而是直接将其加载为一个相对于当前HTML文档的脚本:
以 ".js" 结束.
以 "/" 开始.
包含 URL 协议, 如 "http:" or "https:".
一般来说,最好还是使用baseUrl及”paths” config去设置module ID。它会给你带来额外的灵活性,如便于脚本的重命名、重定位等。 同时,为了避免凌乱的配置,最好不要使用多级嵌套的目录层次来组织代码,而是要么将所有的脚本都放置到baseUrl中,要么分置为项目库/第三方库的一个扁平结构,如下:
www/
index.html
js/
app/
sub.js
lib/
jquery.js
canvas.js
app.js
index.html:
<script data-main="js/app.js" src="js/require.js"></script>
app.js:
requirejs.config({ //By default load any module IDs from js/lib baseUrl: \'js/lib\', //except, if the module ID starts with "app", //load it from the js/app directory. paths //config is relative to the baseUrl, and //never includes a ".js" extension since //the paths config could be for a directory. paths: { app: \'../app\' } }); // Start the main app logic. requirejs([\'jquery\', \'canvas\', \'app/sub\'], function ($, canvas, sub) { //jQuery, canvas and the app/sub module are all //loaded and can be used here now. });
注意在示例中,三方库如jQuery没有将版本号包含在他们的文件名中。我们建议将版本信息放置在单独的文件中来进行跟踪。使用诸如volo这类的工具,可以将package.json打上版本信息,并在磁盘上保持文件名为”jquery.js”。这有助于你保持配置的最小化,避免为每个库版本设置一条path。例如,将”jquery”配置为”jquery-1.7.2″。
理想状况下,每个加载的脚本都是通过define()来定义的一个模块;但有些”浏览器全局变量注入”型的传统/遗留库并没有使用define()来定义它们的依赖关系,你必须为此使用shim config来指明它们的依赖关系。 如果你没有指明依赖关系,加载可能报错。这是因为基于速度的原因,RequireJS会异步地以无序的形式加载这些库。
3.5.1、路径处理
假定当前路径如下:
moduleA:
define(function () { return { show: m => console.info(m) } });
moduleB:
define([\'moduleA\'], function (a) { return { add: (m, n) => a.show(m + n) } });
app.js:
require([\'module/moduleB\'], function (b) { b.add(100, 500); });
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="js/require2.1.11.js" data-main="js/app.js"></script> </body> </html>
直接运行时,报错:
解决方法一:
可以看出运行时报错,原因是baseUrl的值与app.js所在位置有关,当前应该是:baseUrl=\’js/\’,那么所有模块在依赖时都默认以js/开始,所以moduleB依赖ModuleA时也以js/开始,可以通过指定当前目录解决该问题,修改moduleB如下:
define([\'./moduleA\'], function (a) { return { add: (m, n) => a.show(m + n) } });
./表示当前目录,../上一级,/表示从域名开始不含虚拟目录
运行结果:
解决方法二:
手动配置baseUrl,修改后如下:
app.js
//配置 requirejs.config({ //模块依赖的基础路径,默认模块的加载位置 baseUrl:\'js/module/\' }); //模块引用 require([\'moduleB\'], function (b) { b.add(100, 500); });
moduleB.js
define([\'moduleA\'], function (a) { return { add: (m, n) => a.show(m + n) } });
运行结果:
3.5.2、依赖第三方的库(AMD依赖jQuery)
jQuery 1.7 开始支持将 jQuery 注册为一个AMD异步模块。有很多兼容的脚本加载器(包括 RequireJS 和 curl)都可以用一个异步模块格式来加载模块,这也就表示不需要太多 hack 就能让一切运行起来。可以看看jQuery 1.7 中的源码:
if ( typeof define === "function" && define.amd && define.amd.jQuery ) { define( "jquery", [], function () { return jQuery; } ); }
其工作的原理是,所使用的脚本加载器通过指定一个属性,即 define.amd.jQuery 为 true,来标明自己可以支持多个 jQuery 版本。如果有兴趣了解特定的实现细节的话,我们可以将 jQuery 注册为一个具名模块,因为可能会有这样的风险,即它可能被与其它使用了 AMD 的 define() 方法的文件拼合在一起,而没有使用一个合适的、理解匿名 AMD 模块定义的拼合脚本。
高版本的jQuery (1.11.1) 去掉了define.amd.jQuery判断:
if ( typeof define === "function" && define.amd ) { define( "jquery", [], function() { return jQuery; }); }
示例代码:
//配置 requirejs.config({ //模块依赖的基础路径,默认模块的加载位置 baseUrl:\'js/\', //路径 paths:{ //如果在路径前写上m/,则表示为:js/module/ m:"module/", jquery:\'common/jquery/jquery-1.12.4\' } }); //模块引用 require([\'m/moduleB\',\'m/moduleC\',\'jquery\'], function (b,c,$) { let m=500,n=100; b.add(m, n); c.sub(m,n); $("body").css({"background":"yellow"}); });
运行结果:
3.6、data-main 入口点
require.js 在加载的时候会检察data-main 属性:
<!--when require.js loads it will inject another script tag (with async attribute) for scripts/main.js--> <script data-main="scripts/main" src="scripts/require.js"></script>
你可以在data-main指向的脚本中设置模板加载 选项,然后加载第一个应用模块。.注意:你在main.js中所设置的脚本是异步加载的。所以如果你在页面中配置了其它JS加载,则不能保证它们所依赖的JS已经加载成功。
例如:
<script data-main="scripts/main" src="scripts/require.js"></script> <script src="scripts/other.js"></script>
main.js
// contents of main.js: require.config({ paths: { foo: \'libs/foo-1.1.3\' } }); // contents of other.js: // This code might be called before the require.config() in main.js // has executed. When that happens, require.js will attempt to // load \'scripts/foo.js\' instead of \'scripts/libs/foo-1.1.3.js\' require( [\'foo\'], function( foo ) { });
3.7、定义模块
模块不同于传统的脚本文件,它良好地定义了一个作用域来避免全局名称空间污染。它可以显式地列出其依赖关系,并以函数(定义此模块的那个函数)参数的形式将这些依赖进行注入,而无需引用全局变量。RequireJS的模块是模块模式的一个扩展,其好处是无需全局地引用其他模块。
RequireJS的模块语法允许它尽快地加载多个模块,虽然加载的顺序不定,但依赖的顺序最终是正确的。同时因为无需创建全局变量,甚至可以做到在同一个页面上同时加载同一模块的不同版本。
(如果你熟悉ConmmonJS,可参看CommonJS的注释信息以了解RequireJS模块到CommonJS模块的映射关系)。
一个磁盘文件应该只定义 1 个模块。多个模块可以使用内置优化工具将其组织打包。
3.7.1、简单的值对
如果一个模块仅含值对,没有任何依赖,则在define()中定义这些值对就好了:
//Inside file my/shirt.js: define({ color: "black", size: "unisize" });
3.7.2、函数式定义
如果一个模块没有任何依赖,但需要一个做setup工作的函数,则在define()中定义该函数,并将其传给define():
//my/shirt.js now does setup work //before returning its module definition. define(function () { //Do setup work here return { color: "black", size: "unisize" } });
3.7.3、存在依赖的函数式定义
如果模块存在依赖:则第一个参数是依赖的名称数组;第二个参数是函数,在模块的所有依赖加载完毕后,该函数会被调用来定义该模块,因此该模块应该返回一个定义了本模块的object。依赖关系会以参数的形式注入到该函数上,参数列表与依赖名称列表一一对应。
//my/shirt.js now has some dependencies, a cart and inventory //module in the same directory as shirt.js define(["./cart", "./inventory"], function(cart, inventory) { //return an object to define the "my/shirt" module. return { color: "blue", size: "large", addToCart: function() { inventory.decrement(this); cart.add(this); } } } );
本示例创建了一个my/shirt模块,它依赖于my/cart及my/inventory。磁盘上各文件分布如下:
my/cart.js my/inventory.js my/shirt.js
模块函数以参数”cart”及”inventory”使用这两个以”./cart”及”./inventory”名称指定的模块。在这两个模块加载完毕之前,模块函数不会被调用。
严重不鼓励模块定义全局变量。遵循此处的定义模式,可以使得同一模块的不同版本并存于同一个页面上(参见 高级用法 )。另外,函参的顺序应与依赖顺序保存一致。
返回的object定义了”my/shirt”模块。这种定义模式下,”my/shirt”不作为一个全局变量而存在。
3.7.4、将模块定义为一个函数
对模块的返回值类型并没有强制为一定是个object,任何函数的返回值都是允许的。此处是一个返回了函数的模块定义:
//A module definition inside foo/title.js. It uses //my/cart and my/inventory modules from before, //but since foo/title.js is in a different directory than //the "my" modules, it uses the "my" in the module dependency //name to find them. The "my" part of the name can be mapped //to any directory, but by default, it is assumed to be a //sibling to the "foo" directory. define(["my/cart", "my/inventory"], function(cart, inventory) { //return a function to define "foo/title". //It gets or sets the window title. return function(title) { return title ? (window.title = title) : inventory.storeName + \' \' + cart.name; } } );
3.7.5、简单包装CommonJS来定义模块
如果你现有一些以CommonJS模块格式编写的代码,而这些代码难于使用上述依赖名称数组参数的形式来重构,你可以考虑直接将这些依赖对应到一些本地变量中进行使用。你可以使用一个CommonJS的简单包装来实现:
define(function(require, exports, module) { var a = require(\'a\'), b = require(\'b\'); //Return the module value return function () {}; } );
该包装方法依靠Function.prototype.toString()将函数内容赋予一个有意义的字串值,但在一些设备如PS3及一些老的Opera手机浏览器中不起作用。考虑在这些设备上使用优化器将依赖导出为数组形式。
更多的信息可参看CommonJS Notes页面,以及”Why AMD”页面的”Sugar”段落。
3.7.6、定义一个命名模块
你可能会看到一些define()中包含了一个模块名称作为首个参数:
//Explicitly defines the "foo/title" module: define("foo/title", ["my/cart", "my/inventory"], function(cart, inventory) { //Define foo/title object in here. } );
这些常由优化工具生成。你也可以自己显式指定模块名称,但这使模块更不具备移植性——就是说若你将文件移动到其他目录下,你就得重命名。一般最好避免对模块硬编码,而是交给优化工具去生成。优化工具需要生成模块名以将多个模块打成一个包,加快到浏览器的载人速度。
示例:
moduleD.js:
define("md",[],function(){ return {hi: () => console.info("Hello ModuleD!")}; });
app.js:
//配置 requirejs.config({ //模块依赖的基础路径,默认模块的加载位置 baseUrl:\'js/\', //路径 paths:{ //如果在路径前写上m/,则表示为:js/module/ m:"module/", jquery:\'common/jquery/jquery-1.12.4\', md:\'module/moduleD\' } }); //模块引用 require([\'m/moduleB\',\'m/moduleC\',\'jquery\',\'md\'], function (b,c,$,d) { let m=500,n=100; b.add(m, n); c.sub(m,n); d.hi(); $("body").css({"background":"yellow"}); });
使用命名模块时需要为命名模块指定路径,名称需唯一。
运行结果:
3.7.7、依赖非AMD模块
配置中shim参数为那些没有使用define()来声明依赖关系、设置模块的”浏览器全局变量注入”型脚本做依赖和导出配置。
添加一个非amd模块moduleE.js:
var moduleE = { hello: () => { console.info("Hello ModuleE!"); } };
引用ModuleE:
//配置 requirejs.config({ //模块依赖的基础路径,默认模块的加载位置 baseUrl:\'js/\', //路径 paths:{ //如果在路径前写上m/,则表示为:js/module/ m:"module/", jquery:\'common/jquery/jquery-1.12.4\', md:\'module/moduleD\', moduleE:\'module/moduleE\' }, //处理非标准的amd模块 shim:{ //这个键名为要载入的目标文件的文件名,不能随便命名否则拿不到改文件对外提供的接口的 \'moduleE\':{ exports:\'moduleE\' } } }); //模块引用 require([\'m/moduleB\',\'m/moduleC\',\'jquery\',\'md\',\'moduleE\'], function (b,c,$,d,e) { let m=500,n=100; b.add(m, n); c.sub(m,n); d.hi(); $("body").css({"background":"yellow"}); e.hello(); });
运行结果:
3.7.8、注意事项
一个文件一个模块: 每个Javascript文件应该只定义一个模块,这是模块名-至-文件名查找机制的自然要求。多个模块会被优化工具组织优化,但你在使用优化工具时应将多个模块放置到一个文件中。
define()中的相对模块名: 为了可以在define()内部使用诸如require(“./relative/name”)的调用以正确解析相对名称,记得将”require”本身作为一个依赖注入到模块中:
define(["require", "./relative/name"], function(require) { var mod = require("./relative/name"); });
或者更好地,使用下述为转换CommonJS模块所设的更短的语法:
define(function(require) { var mod = require("./relative/name"); });
相对路径在一些场景下格外有用,例如:为了以便于将代码共享给其他人或项目,你在某个目录下创建了一些模块。你可以访问模块的相邻模块,无需知道该目录的名称。
生成相对于模块的URL地址: 你可能需要生成一个相对于模块的URL地址。你可以将”require”作为一个依赖注入进来,然后调用require.toUrl()以生成该URL:
define(["require"], function(require) { var cssUrl = require.toUrl("./style.css"); });
控制台调试:如果你需要处理一个已通过require([“module/name”], function(){})调用加载了的模块,可以使用模块名作为字符串参数的require()调用来获取它:
require("module/name").callSomeFunction()
注意这种形式仅在”module/name”已经由其异步形式的require([“module/name”])加载了后才有效。只能在define内部使用形如”./module/name”的相对路径。
四、CMD
CMD规范是阿里的玉伯提出来的,实现js库为sea.js。 它和requirejs非常类似,即一个js文件就是一个模块,但是CMD的加载方式更加优秀,是通过按需加载的方式,而不是必须在模块开始就加载所有的依赖。如下:
define(function(require, exports, module) { var $ = require(\'jquery\'); var Spinning = require(\'./spinning\'); exports.doSomething = ... module.exports = ... })
优点:
同样实现了浏览器端的模块化加载。
可以按需加载,依赖就近。
缺点:
依赖SPM打包,模块的加载逻辑偏重。
其实,这时我们就可以看出AMD和CMD的区别了,前者是对于依赖的模块提前执行,而后者是延迟执行。 前者推崇依赖前置,而后者推崇依赖就近,即只在需要用到某个模块的时候再require。 如下:
// AMD define([\'./a\', \'./b\'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() ... }); // CMD define(function(require, exports, module) { var a = require(\'./a\') a.doSomething() // 此处略去 100 行 var b = require(\'./b\') // 依赖可以就近书写 b.doSomething() // ... });
模块定义语法
define(id, deps, factory)
因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id;
CMD推崇依赖就近,所以一般不在define的参数中写依赖,而是在factory中写。
factory有三个参数:
function(require, exports, module){}
一、require
require 是 factory 函数的第一个参数,require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口;
二、exports
exports 是一个对象,用来向外提供模块接口;
三、module
module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。
// 定义模块 myModule.js define(function(require, exports, module) { var $ = require(\'jquery.js\') $(\'div\').addClass(\'active\'); }); // 加载模块 seajs.use([\'myModule.js\'], function(my){ });
4.1、Seajs
Seajs是一个加载器、遵循 CMD 规范模块化开发,依赖的自动加载、配置的简洁清晰。
SeaJS是一个遵循CMD规范的JavaScript模块加载框架,可以实现JavaScript的模块化开发及加载机制。
与jQuery等JavaScript框架不同,SeaJS不会扩展封装语言特性,而只是实现JavaScript的模块化及按模块加载。SeaJS的主要目的是令JavaScript开发模块化并可以轻松愉悦进行加载,将前端工程师从繁重的JavaScript文件及对象依赖处理中解放出来,可以专注于代码本身的逻辑。SeaJS可以与jQuery这类框架完美集成。使用SeaJS可以提高JavaScript代码的可读性和清晰度,解决目前JavaScript编程中普遍存在的依赖关系混乱和代码纠缠等问题,方便代码的编写和维护。
github:https://github.com/seajs/seajs
特点:
Sea.js 追求简单、自然的代码书写和组织方式,具有以下核心特性:
(一)、简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像 Node.js 一般书写模块代码。
(二)、自然直观的代码组织方式:依赖的自动加载、配置的简洁清晰,可以让我们更多地享受编码的乐趣。
Sea.js 还提供常用插件,非常有助于开发调试和性能优化,并具有丰富的可扩展接口。
兼容:
Chrome 3+ Firefox 2+ Safari 3.2+ Opera 10+ IE 5.5+
基本应用:
导入Seajs库
去官网下载最新的seajs文件,http://seajs.org/docs/#downloads
在页尾引入seajs:
<script src="/site/script/sea.js"></script>
然后在它下面写模块的配置和入口。
// 加载入口模块 seajs.use("../static/hello/src/main");
配置和入口
这里解释下配置和入口的意思。
配置
通常在配置上修改seajs的路径和别名。
seajs的路径是相对于前面引入的seajs文件的。假如是这样的目录结构:
examples/ |-- index.html | `--about | |-- news.html | `-- script |-- seajs.js |-- jquery.js `-- main.js
我们平时如果我们在index.html上引用main.js路径应该是这样写的script/main.js,从news.html引用main.js就要这样写,../script/main.js。
而在seajs是相对于seajs文件的,一律直接使用main.js就OK了,是不是很方便呢?
既然这么方便那在什么情况需要配置呢?一般情况是用不到的。但是假如你的路径特别深 或者要做路径映射的时候它的作用就来了。下面介绍下常用的几个配置。
seajs.config({ // Sea.js 的基础路径(修改这个就不是路径就不是相对于seajs文件了) base: \'http://example.com/path/to/base/\', // 别名配置(用变量表示文件,解决路径层级过深和实现路径映射) alias: { \'es5-safe\': \'gallery/es5-safe/0.9.3/es5-safe\', \'json\': \'gallery/json/1.0.2/json\', \'jquery\': \'jquery/jquery/1.10.1/jquery\' }, // 路径配置(用变量表示路径,解决路径层级过深的问题) paths: { \'gallery\': \'https://a.alipayobjects.com/gallery\' } });
入口
入口即加载,需要加载什么文件(模块加载器)就在这里引入。sea.js 在下载完成后,会自动加载入口模块。
seajs.use("abc/main"); //导入seajs.js同级的abc文件夹下的main.js模块的(后缀名可略去不写)
seajs.use()还有另外一种用法。
有时候我们写一个简单的单页并不想为它单独写一个js文件,选择在直接把js代码写在页面上,seajs通过seajs.use()实现了这个。接收两个参数第一个是文件依赖(单个用字符串数组都可以,多个需用数组表示),第二个是回调函数。
加载单个依赖
//加载模块 main,并在加载完成时,执行指定回调 seajs.use(\'./main\', function(main) { main.init(); });
加载多个依赖
//并发加载模块 a 和模块 b,并在都加载完成时,执行指定回调 seajs.use([\'./a\', \'./b\'], function(a, b) { a.init(); b.init(); });
这里回掉函数中的a和b参数是与前面的模块暴露出来的接口一一对应的。有时候也许只需要使用b的接口,但是也要把a参数写上。什么是暴露接口下面会解释。
通过seajs.use()只能在第一个参数中引入模块,不能在回调函数中使用require()载入模块。
模块开发
这里才是重点,其实也很简单就是一个书写规范(CMD)而已。
// 所有模块都通过 define 来定义 define(function(require, exports, module) { // 通过 require 引入依赖 var $ = require(\'jquery\'); var Spinning = require(\'./spinning\'); // 通过 exports 对外提供接口 exports.doSomething = ... // 或者通过 module.exports 提供整个接口 module.exports = ... });
模块是通过define()方法包装的,然后内部痛过require()方法引入需要的依赖文件(模块)。
模块最好是面向对象开发的,这样最后可以方便的通过exports.doSomething或module.exports把模块的接口给暴露出来。如果你是写的是jq插件的话就不需要这个功能了,因为你的接口是写在jquery的对象里的。如果你不需要提供接口的话也可以不使用这两个属性哦!
事实上define方法还有另外几个参数,一般情况我们用不到。具体看官方API。
注意事项
模块内的函数依赖必须交代清楚,防止模块在函数依赖加载前先加载出来。而且还增强了模块的独立性。
引入seajs的时候最好给<script>标签加个id,可以快速访问到这个标签(我是在模块合并时用到它的)
还有前面提到的使用seajs.use()在.html页面上写js时如果有多个模块依赖,需要使用暴露出来的接口就要让参数与它一一对应。
4.2、seajs示例
目录结构如下:
moduleH.js
define(function (require,exports,module) { var obj={ msg:"Hello SeaJS!", show:()=>console.info(obj.msg) }; exports.moduleH=obj; });
main.js
seajs.config({ //Sea.js 的基础路径(修改这个就不是路径就不是相对于seajs文件了) base: \'./js/\', //别名配置(用变量表示文件,解决路径层级过深和实现路径映射) alias: { \'jquery\': \'common/jquery/jquery-1.12.4\' }, //路径配置(用变量表示路径,解决路径层级过深的问题) paths: { \'m\': \'module/\' } }); seajs.use(["m/moduleH.js",\'jquery\'], function (mh,$) { mh.moduleH.show(); $("body").append("<h2>Hello CMD!</h2>"); })
main.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="js/sea.js"></script> <script src="js/main.js"></script> </body> </html>
运行结果:
4.3、官方文档
如果您要更加深入学习CMD与seajs可以参考下面的文档、https://seajs.github.io/seajs/docs/
4.3.1、入门
4.3.2、基础
4.3.3、插件
- seajs-css
- seajs-preload
- seajs-text
- seajs-style
- seajs-combo
- seajs-flush
- seajs-debug
- seajs-log
- seajs-health
4.3.4、进阶
4.3.5、探讨
五、原生模块化(ECMAScript模块化)
ES6之前使用RequireJS或者seaJS实现模块化, requireJS是基于AMD规范的模块化库, 而像seaJS是基于CMD规范的模块化库, 两者都是为了为了推广前端模块化的工具。
现在ES6自带了模块化, 也是JS第一次支持module, 在很久以后 ,我们可以直接作用import和export在浏览器中导入和导出各个模块了, 一个js文件代表一个js模块;
现代浏览器对模块(module)支持程度不同, 目前都是使用babelJS, 或者Traceur把ES6代码转化为兼容ES5版本的js代码;
之前的几种模块化方案都是前端社区自己实现的,只是得到了大家的认可和广泛使用,而ES6的模块化方案是真正的规范。 在ES6中,我们可以使用 import 关键字引入模块,通过 export 关键字导出模块,功能较之于前几个方案更为强大,也是我们所推崇的,但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require。
虽然目前import和require的区别不大,但是还是推荐使用使用es6,因为未来es6必定是主流,对于代码的迁移成本还是非常容易的。 如:
import store from \'../store/index\' import {mapState, mapMutations, mapActions} from \'vuex\' import axios from \'../assets/js/request\' import util from \'../utils/js/util.js\' export default { created () { this.getClassify(); this.RESET_VALUE(); console.log(\'created\' ,new Date().getTime()); }
5.1、ES6模块化特点
1、每一个模块只加载一次, 每一个JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例,或者说就是一个对象;
2、每一个模块内声明的变量都是局部变量, 不会污染全局作用域;
3、模块内部的变量或者函数可以通过export导出;
4、一个模块可以导入别的模块
5.2、在Chrome浏览器使用Module
Chrome 61就提供了对ES2015 import语句的支持,实现模块加载
查看版本的办法是:在chrome浏览器中输入chrome://version/
谷歌浏览器(Canary 60) – 需要在chrome:flags里开启”实验性网络平台功能(Experimental Web Platform)”
示例:lib.js
/** *定义模块 */ //导出 export let msg="求和:"; export function sum(n){ let total=0; for(var i=1;i<=n;i++){ total+=i; } return total; }
html:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Module模块</title> </head> <body> <script type="module"> //导入 import {sum,msg} from \'./lib.js\'; let result=sum(100); console.log(msg+""+result); </script> </body> </html>
结果:
5.3、在Node.js中使用Module
5.3.1、方法一
在 Node.js 模块系统中,每个文件都被视为独立的模块。
例子,假设有一个名为 foo.js 的文件:
const circle = require(\'./circle.js\');
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`);
在第一行中,foo.js 加载了同一目录下的 circle.js 模块。
circle.js 文件的内容为:
const { PI } = Math; exports.area = (r) => PI * r ** 2; exports.circumference = (r) => 2 * PI * r;
circle.js 模块导出了 area() 和 circumference() 两个函数。 通过在特殊的 exports 对象上指定额外的属性,函数和对象可以被添加到模块的根部。
模块内的本地变量是私有的,因为模块被 Node.js 包装在一个函数中(详见模块包装器)。 在这个例子中,变量 PI 是 circle.js 私有的。
module.exports属性可以被赋予一个新的值(例如函数或对象)。
如下,bar.js 会用到 square 模块,square 模块导出了 Square 类:
const Square = require(\'./square.js\'); const mySquare = new Square(2); console.log(`mySquare 的面积是 ${mySquare.area()}`);
square 模块定义在 square.js 中:
// 赋值给 `exports` 不会修改模块,必须使用 `module.exports` module.exports = class Square { constructor(width) { this.width = width; } area() { return this.width ** 2; } };
模块系统在 `require(\’module\’)` 模块中实现。
访问主模块
当 Node.js 直接运行一个文件时,require.main 会被设为它的 module。 这意味着可以通过 require.main === module 来判断一个文件是否被直接运行:
对于 foo.js 文件,如果通过 node foo.js 运行则为 true,但如果通过 require(\’./foo\’) 运行则为 false。
因为 module 提供了一个 filename 属性(通常等同于 __filename),所以可以通过检查 require.main.filename 来获取当前应用程序的入口点。
参考API:http://nodejs.cn/api/modules.html
5.3.2、方法二:experimental-modules
升级node 8.5 使用 experimental-modules参数,且要求所有文件名后缀都要修改为mjs
node –experimental-modules index.mjs
定义模块lib.mjs:
/** *定义模块 */ //导出 export let msg="求和:"; export function sum(n){ let total=0; for(var i=1;i<=n;i++){ total+=i; } return total; }
定义main.mjs文件
/** * 使用模块 */ //导入 import { sum, msg } from \'./lib.mjs\'; let result = sum(100); console.log(msg + "" + result);
在命令行下转换到当前目录,使用node加参数experimental-modules执行,结果如下:
5.4、Babel
Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。
5.4.1、配置环境
安装babel命令行工具:
npm install --global babel-cli
安装成功后可以使用babel -V查看版本,可以使用babel -help 查看帮助
创建项目,在当前项目中依赖babel-core
假定当前项目的目录为:E:\Desktop-temp\xww\FastResponse\Mobile\Hybird\vue2_01\vue07_03_babel
使用npm init可以初始化当前项目为node项目
npm install babel-core --save
依赖插件babel-preset-es2015
如果想使用es6语法,必须安装一个插件
npm install babel-preset-es2015
然后在文件夹下面创建一个叫.babelrc的文件,并写入如下代码:
{ "presets": ["es2015"] }
windows不支持直接命令为.babelrc,可以在DOS下使用@echo结合>实现:
.babelrc文件以rc结尾的文件通常代表运行时自动加载的文件,配置等等的,类似bashrc,zshrc。同样babelrc在这里也是有同样的作用的,而且在babel6中,这个文件必不可少。
在babel6中,预设了6种,分别是:es2015、stage-0、stage-1、stage-2、stage-3、react
5.4.2、转换ES6为ES5
当环境准备好了,就可以编写一个es6风格的文件如:es6.js,内容如下:
let add=(x,y)=>x+y; const n1=100,n2=200; var result=add(n1,n2); console.log(result);
在当前目录执行命令:
babel es6.js -o es5.js
转换后的结果es5.js:
"use strict"; var add = function add(x, y) { return x + y; }; var n1 = 100, n2 = 200; var result = add(n1, n2); console.log(result);
从转换后的结果可以看出es6已变成es5了,箭头函数不见了。
5.4.3、使用babel-node运行ES6模块化代码
babel-cli工具自带一个babel-node命令,提供一个支持ES6的REPL环境。它支持Node的REPL(交互式解释器环境)环境的所有功能,而且可以直接运行ES6代码。
在当前目录下创建lib.js文件:
/** *定义模块 */ //导出 export let msg="求和:"; export function sum(n){ let total=0; for(var i=1;i<=n;i++){ total+=i; } return total; }
创建main.js文件调用定义好的模块:
/** * 使用模块 */ //导入 import { sum, msg } from \'./lib.js\'; let result = sum(100); console.log(msg + "" + result);
在命令行执行:babel-node main.js 结果如下:
到这里共讲解了3种可以运行ES6模块化的环境,任选一种可以用于学习。
5.5、模块(Modules)
ES6从语言层面对模块进行了支持。编写方式借鉴了流行的JavaScript模块加载器(AMD, CommonJS)。由宿主环境的默认加载器定义模块运行时的行为,采取隐式异步模式——在模块可以被获取和加载前不会有代码执行。
定义模块:
// lib/math.js
export function sum(x, y) { return x + y; } export var pi = 3.141593;
导入模块:
//全部导入 import people from \'./example\' //有一种特殊情况,即允许你将整个模块当作单一对象进行导入 //该模块的所有导出都会作为对象的属性存在 import * as example from "./example.js" console.log(example.name) console.log(example.age) console.log(example.getName()) //导入部分 import {name, age} from \'./example\' //导出默认, 有且只有一个默认 export default App // 部分导出 export class App extend Component {};
*表示所有,as取别名
// app.js
import * as math from "lib/math";
console.log("2π = " + math.sum(math.pi, math.pi));
// otherApp.js
导入部分内容
import {sum, pi} from "lib/math";
console.log("2π = " + sum(pi, pi));
还有的功能包括:export default and export *:
// lib/mathplusplus.js
export * from "lib/math"; export var e = 2.71828182846; export default function(x) { return Math.exp(x); }
// app.js
import exp, {pi, e} from "lib/mathplusplus";
console.log("e^π = " + exp(pi));
导入的时候有没有大括号的区别:
- 1.当用export default people导出时,就用 import people 导入(不带大括号)
- 2.一个文件里,有且只能有一个export default。但可以有多个export。
- 3.当用export name 时,就用import { name }导入(记得带上大括号)
- 4.当一个文件里,既有一个export default people, 又有多个export name 或者 export age时,导入就用 import people, { name, age }
- 5.当一个文件里出现n多个 export 导出很多模块,导入时除了一个一个导入,也可以用import * as example
模块的格式:
Babel可以将ES2015的模块转换为一下几种格式:Common.js,AMD,System,以及UMD。你甚至可以创建你自己的方式。
5.5.1、导出方式一
使用 export{接口} 导出接口, 大括号中的接口名字为上面定义的变量, import和export是对应的;
//lib.js 文件 let bar = "stringBar"; let foo = "stringFoo"; let fn0 = function() { console.log("fn0"); }; let fn1 = function() { console.log("fn1"); }; export{ bar , foo, fn0, fn1} //main.js文件 import {bar,foo, fn0, fn1} from "./lib"; console.log(bar+"_"+foo); fn0(); fn1();
示例:
先配置babel的运行环境,创建util.js文件:
let PI=3.14; function getArea(r){ return PI*r*r; } //集中导出对象 export {PI,getArea}
导入模块main.js:
import {PI,getArea} from \'./util\'
console.log("R=5时面积为:"+getArea(5));
结果:
5.5.2、导出方式二
在export接口的时候, 我们可以使用 XX as YY, 把导出的接口名字改了, 比如: closureFn as sayingFn, 把这些接口名字改成不看文档就知道干什么的
//lib.js文件 let fn0 = function() { console.log("fn0"); }; let obj0 = {} export { fn0 as foo, obj0 as bar}; //main.js文件 import {foo, bar} from "./lib"; foo(); console.log(bar);
5.5.3、导出方式三
这种方式是直接在export的地方定义导出的函数,或者变量:
//lib.js文件 export let foo = ()=> {console.log("fnFoo") ;return "foo"},bar = "stringBar"; //main.js文件 import {foo, bar} from "./lib"; console.log(foo()); console.log(bar);
5.5.4、导出方式四
这种导出的方式不需要知道变量的名字, 相当于是匿名的, 直接把开发的接口给export;
如果一个js模块文件就只有一个功能, 那么就可以使用export default导出;
//lib.js export default "string"; //main.js import defaultString from "./lib"; console.log(defaultString);
5.5.5、导出方式五
export也能默认导出函数, 在import的时候, 名字随便写, 因为每一个模块的默认接口就一个
//lib.js let fn = () => "string"; export {fn as default}; //main.js import defaultFn from "./lib"; console.log(defaultFn());
5.5.6、导出方式六
使用通配符* ,重新导出其他模块的接口
//lib.js export * from "./other"; //如果只想导出部分接口, 只要把接口名字列出来 //export {foo,fnFoo} from "./other"; //other.js export let foo = "stringFoo", fnFoo = function() {console.log("fnFoo")}; //main.js import {foo, fnFoo} from "./lib"; console.log(foo); console.log(fnFoo());
在import的时候可以使用通配符*导入外部的模块:
import * as obj from "./lib";
console.log(obj);
5.6、模块加载器(Module Loaders)
这并不是ES2015的一部分:这部分ECMAScript 2015规范是由实现定义(implementation-defined)的。最终的标准将在WHATWG的Loader 规范中确定,目前这项工作正在进行中,下面的内容来自于之前的ES2015草稿。
模块加载器支持以下功能:
- 动态加载(Dynamic loading)
- 状态一致性(State isolation)
- 全局空间一致性(Global namespace isolation)
- 编译钩子(Compilation hooks)
- 嵌套虚拟化(Nested virtualization)
你可以对默认的加载器进行配置,构建出新的加载器,可以被加载于独立或受限的执行环境。
// 动态加载 – ‘System’ 是默认的加载器 System.import("lib/math").then(function(m) { alert("2π = " + m.sum(m.pi, m.pi)); }); // 创建执行沙箱 – new Loaders var loader = new Loader({ global: fixup(window) // replace ‘console.log’ }); loader.eval("console.log(\"hello world!\");"); // 直接操作模块的缓存 System.get("jquery"); System.set("jquery", Module({$: $})); // WARNING: not yet finalized
需要额外的polyfill
由于Babel默认使用common.js的模块,你需要一个polyfill来使用加载器API。
使用模块加载器
为了使用此功能,你需要告诉Babel使用system模块格式化工具。
六、UMD(通用的模块定义)
UMD(Universal Module Definition)通用的模块定义、UMD等于CommonJS加上AMD。UMD的工作其实就是做了一个判断:
- – 先判断当前环境对NodeJs支持的模块是否存在,存在就用Node.js模块模式(exports)。
- – 如果不支持,就判断是否支持AMD(define),存在就使用AMD方式加载。
CommonJs和AMD风格一样流行,似乎缺少一个统一的规范。所以人们产生了这样的需求,希望有支持两种风格的“通用”模式,于是通用模块规范(UMD)诞生了。
不得不承认,这个模式略难看,但是它兼容了AMD和CommonJS,同时还支持老式的“全局”变量规范,同时兼容前后端,模块定义写法如下:
(function (root, factory) { if (typeof define === \'function\' && define.amd) { // AMD. Register as an anonymous module. define([\'b\'], factory); } else if (typeof module === \'object\' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(require(\'b\')); } else { // Browser globals (root is window) root.returnExports = factory(root.b); } }(this, function (b) { //use b in some fashion. // Just return a value to define the module export. // This example returns an object, but the module // can return a function as the exported value. return {}; }));
写法2:
(function (root, factory) { if (typeof define === \'function\' && define.amd) { // AMD define([\'jquery\', \'underscore\'], factory); } else if (typeof exports === \'object\') { // Node, CommonJS之类的 module.exports = factory(require(\'jquery\'), require(\'underscore\')); } else { // 浏览器全局变量(root 即 window) root.returnExports = factory(root.jQuery, root._); } }(this, function ($, _) { // 方法 function a() { }; // 私有方法,因为它没被返回 (见下面) function b() { }; // 公共方法,因为被返回了 function c() { }; //公共方法,因为被返回了 // 暴露公共方法 return { b: b, c: c } }));
写法3(Vue):
/*! * Vue.js v2.5.17 * (c) 2014-2018 Evan You * Released under the MIT License. */ (function (global, factory) { typeof exports === \'object\' && typeof module !== \'undefined\' ? module.exports = factory() : typeof define === \'function\' && define.amd ? define(factory) : (global.Vue = factory()); }(this, (function () { return {}; })));
6.1、UMD示例
6.1.1、定义模块Utils.js
(function (global, factory) { if (typeof define === \'function\' && (define.amd || define.cmd)) { // AMD规范. 注册一个匿名模块,兼容AMD与CMD define([], factory); } else if (typeof module === \'object\' && module.exports) { //CommonJS规范,NodeJS运行环境 module.exports = factory(); } else { //浏览器全局对象注册 global.UMD = factory(); } }(this, function () { var msg = "UMD!"; //返回要导出的对象 return { show: function () { console.log("Hello " + msg); } }; }));
6.1.2、在CommonJS规范下运行
useUtils.js
var utils=require(\'./Utils.js\'); utils.show();
运行结果:
6.1.3、在AMD规范下运行
app.js
//配置 requirejs.config({ //模块依赖的基础路径,默认模块的加载位置 baseUrl:\'js/\', //路径 paths:{ //如果在路径前写上m/,则表示为:js/module/ m:"module/", jquery:\'common/jquery/jquery-1.12.4\', md:\'module/moduleD\', moduleE:\'module/moduleE\', mf:\'module/moduleF\', moduleG:\'module/moduleG\' }, //处理非标准的amd模块 shim:{ //这个键名为要载入的目标文件的文件名,不能随便命名否则拿不到改文件对外提供的接口的 \'moduleE\':{ exports:\'moduleE\' }, \'moduleG\':{ exports:\'moduleG\' } } }); //模块引用 require([\'m/moduleB\',\'m/moduleC\',\'jquery\',\'md\',\'moduleE\',\'mf\',\'moduleG\',\'m/Tools\',\'m/Utils\'], function (b,c,$,d,e,f,g,umd,mu) { mu.show(); });
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>AMD - requirejs</title> </head> <body> <script src="js/require2.1.11.js" data-main="js/app.js"></script> </body> </html>
运行结果:
6.1.4、在CMD规范下运行
app.js
seajs.config({ // Sea.js 的基础路径(修改这个就不是路径就不是相对于seajs文件了) base: \'./javascript/module/\', //别名配置(用变量表示文件,解决路径层级过深和实现路径映射) alias:{ \'jquery\':\'common/jquery/jquery-1.12.4\' }, // 路径配置(用变量表示路径,解决路径层级过深的问题) paths: { \'xy\': \'xDirectory/yDirectory/\' } }); //引用模块,依赖模块 seajs.use([\'moduleA\',\'jquery\',\'xy/moduleC\',\'xy/moduleD\',\'Utils\'],function (ma,$,mc,md,mu) { mu.show(); });
main.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="js/sea.js"></script> <script src="js/main.js"></script> </body> </html>
运行结果:
6.1.5、原生浏览器环境运行
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>UMD</title> </head> <body> <script src="javascript/module/Utils.js"></script> <script> UMD.show(); </script> </body> </html>
运行结果:
从上面的示例运行结果可以看出采用UMD定义的模块可以兼容CommonJS、AMD、CMD与浏览器原生环境,同时兼容前后台。写法并非固定可以根据需要变化。
七、NodeJS包管理器
npm 为你和你的团队打开了连接整个 JavaScript 天才世界的一扇大门。它是世界上最大的软件注册表,每星期大约有 30 亿次的下载量,包含超过 600000 个 包(package) (即,代码模块)。来自各大洲的开源软件开发者使用 npm 互相分享和借鉴。包的结构使您能够轻松跟踪依赖项和版本。
7.1、npm概要
npm全称为Node Package Manager,是一个基于Node.js的包管理器,也是整个Node.js社区最流行、支持的第三方模块最多的包管理器。
npm的初衷:JavaScript开发人员更容易分享和重用代码。
npm的使用场景:
- 允许用户获取第三方包并使用。
- 允许用户将自己编写的包或命令行程序进行发布分享。
npm版本查询:npm -v
npm安装:
1、安装nodejs
由于新版的nodejs已经集成了npm,所以可直接通过输入npm -v来测试是否成功安装。
2、使用npm命令来升级npm: npm install npm -g
7.2、包(package)
包是描述一个文件或一个目录。一个包的配置通常由以下构成:
- 一个文件夹包含一个package.json配置文件。
- 包含(含有package.json文件的文件夹)的Gzip压缩文件。
- 解析gzip的url
- 为注册表添加<name>@<version>的url 信息
注意的是即使你从来没有在注册中心发布你的公共包,你可能仍然可以得到很多所有这些package
7.3、模块(module)
模板是通过配置文件中的一个dom节点进行包含一个或多个包。通常一般由包和配置文件以及相关模块程序构成完成一个或多个业务功能操作。
一个模块可以在node . js 程序中装满任何的require()任何。 以下是所有事物加载模块的例子 :
一个文件夹package.json文件包含一个main字段。 一个文件夹index.js文件。 一个JavaScript文件。
7.4、包和模块的关系
一般来说在js程序中使用require加载它们的模块在节点中进行配置npm包,一个模块不一定是一个包。
例如,一些cli包, js程序节点中只包含一个可执行的 命令行界面,不提供main字段。 那么这些包不是模块。
几乎所有npm包(至少,那些节点计划)包含许多模块在他们(因为每个文件加载require()是一个模块)。
几乎所有的npm包都关联着多个模块,因为每个文件都使用require()加载一个模块。
从module加载文件中的上下文node节点。如:var req = require(\’request\’)。我们可能会说,“request模块赋值给req这个变量”。
7.5.npm的生态系统
package.json文件定义的是包。
node_modules文件夹是存储模块的地方。便于js查找模块。
例如: 如果创建一个node_modules/foo.js文件,通过var f=require(\'foo.js\')进行加载模块。因为它没有package.json文件所以foo.js不是一个包。 如果没有创建index.js包或者package.json文件"main"字段,即使是在安装node_modules,因为它没有require()所以它不是一个模块。
常用命令:
npm install [-g] 本地或全局安装模块 npm uninstall [-g] 本地或全局卸载模块 npm update 更新模块 npm ls 查看安装的模块 npm list 列出已安装模块 npm show 显示模块详情 npm info 查看模块的详细信息 npm search 搜索模块 npm publish 发布模块 npm unpublish 删除已发布的模块 npm -v 或 npm version显示版本信息 npm view npm versions 列出npm 的所有有效版本 npm install -g npm@2.14.14 /npm update -g npm@2.14.14 安装指定的npm版本 npm init 引导创建一个package.json文件,包括名称、版本、作者这些信息等 npm outdated #检查模块是否已经过时 npm root [-g] 查看包的安装路径,输出 node_modules的路径, npm help 查看某条命令的详细帮助 例如输入npm help install npm config 管理npm的配置路径
更多介绍:
npm中文网:https://www.npmjs.com.cn/
八、视频
https://www.bilibili.com/video/av37008594/
九、示例
https://git.dev.tencent.com/zhangguo5/ModuleDemo.git
https://git.dev.tencent.com/zhangguo5/Module01.git
十、作业
1、复现所有的上课示例
2、请定义一个模块(UMD),该模块实现计算斐波拉契数列功能,要求在如下面种环境下都测试通过:
2.1、CommonJS,NodeJS
2.2、AMD
2.3、CMD
2.4、浏览器 (MyUmd.feb(6),输出:8)
//斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(3)=2,F(n)=F(n-1)+F(n-2)(n>=4,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。
View Code
3、将javascript自定义插件的大作业实现UMD功能。