JAVAScript:前端模块化开发

早期的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规范实现的,通过转换也可以运行在浏览器端。

特点:

1、所有代码都运行在模块作用域,不会污染全局作用域。
2、模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
3、模块加载的顺序,按照其在代码中出现的顺序。

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 模块管理

由于浏览器不支持 CommonJS 格式。要想让浏览器用上这些模块,必须转换格式。浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量(module、exports、require、global)。只要能够提供这四个变量,浏览器就能加载 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> 
复制代码
 
运行结果:
 
 
虽然 Browserify 很强大,但不能在浏览器里操作,有时就很不方便。
 
纯浏览器的 CommonJS 模块加载器 require1k (https://github.com/Stuk/require1k)。完全不需要命令行,直接放进浏览器即可。

优点:

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在推广过程中对模块定义的规范化的产出。

官网:http://www.requirejs.cn

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方法)
//我是私有函数
//一个属性
复制代码

示例:

 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>
复制代码

结果:

四、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){

});
复制代码

什么是Seajs
Seajs是一个加载器
遵循 CMD 规范模块化开发,依赖的自动加载、配置的简洁清晰。
兼容性
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 的简单配置
seajs.config({
base: "../sea-modules/",
alias: {
"jquery": "jquery/jquery/1.10.1/jquery.js"
}
});

// 加载入口模块
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()载入模块。 ——141023补充

模块开发
这里才是重点,其实也很简单就是一个书写规范(CMD)而已。

// 所有模块都通过 define 来定义
define(function(require, exports, module) {

// 通过 require 引入依赖
var $ = require('jquery');
var Spinning = require('./spinning');

// 通过 exports 对外提供接口
exports.doSomething = ...

// 或者通过 module.exports 提供整个接口
module.exports = ...

});
模块是通过define()方法包装的,然后内部痛过require()方法引入需要的依赖文件(模块)。(也可以引入.css文件哦~)
模块最好是面向对象开发的,这样最后可以方便的通过exports.doSomething或module.exports把模块的接口给暴露出来。如果你是写的是jq插件的话就不需要这个功能了,因为你的接口是写在jquery的对象里的。如果你不需要提供接口的话也可以不使用这两个属性哦!

事实上define方法还有另外几个参数,一般情况我们用不到。具体看官方API。

小结
其实Seajs的基本使用就这么简单,日常使用足够了,之前看官网的5分钟教程楞是没看懂,等会的时候回头想想真的是5分钟学会啊,悟性太低- -||

注意事项
模块内的函数依赖必须交代清楚,防止模块在函数依赖加载前先加载出来。而且还增强了模块的独立性。
引入seajs的时候最好给<script>标签加个id,可以快速访问到这个标签(我是在模块合并时用到它的)
还有前面提到的使用seajs.use()在.html页面上写js时如果有多个模块依赖,需要使用暴露出来的接口就要让参数与它一一对应。

五、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());

    }
复制代码

2.1、ES6模块化特点

1、每一个模块只加载一次, 每一个JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例,或者说就是一个对象;

2、每一个模块内声明的变量都是局部变量, 不会污染全局作用域;

3、模块内部的变量或者函数可以通过export导出;

4、一个模块可以导入别的模块

2.1、在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>
复制代码

结果:

2.2、在Node.js中使用Module

2.2.1、方法一:experimental-modules

在 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

2.2.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执行,结果如下:

2.3、Babel

Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。

2.3.1、配置环境

安装babel命令行工具:

npm install --global babel-cli

安装成功后可以使用babel -V查看版本,可以使用babel -help 查看帮助

创建项目,在当前项目中依赖babel-core

假定当前项目的目录为:E:Desktop-tempxwwFastResponseMobileHybirdvue2_01vue07_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

2.3.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了,箭头函数不见了。 

2.3.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模块化的环境,任选一种可以用于学习。

2.4、模块(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。你甚至可以创建你自己的方式。

2.4.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));

结果:

2.4.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);
复制代码

2.4.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);
复制代码

2.4.4、导出方式四

这种导出的方式不需要知道变量的名字, 相当于是匿名的, 直接把开发的接口给export;
如果一个js模块文件就只有一个功能, 那么就可以使用export default导出;

复制代码
//lib.js
export default "string";

//main.js
import defaultString from "./lib";
console.log(defaultString);
复制代码

2.4.5、导出方式五

export也能默认导出函数, 在import的时候, 名字随便写, 因为每一个模块的默认接口就一个

复制代码
//lib.js
let fn = () => "string";
export {fn as default};

//main.js
import defaultFn from "./lib";
console.log(defaultFn());
复制代码

2.4.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);

2.5、模块加载器(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模块格式化工具。

六、NodeJS包管理器

npm 为你和你的团队打开了连接整个 JavaScript 天才世界的一扇大门。它是世界上最大的软件注册表,每星期大约有 30 亿次的下载量,包含超过 600000 个 包(package) (即,代码模块)。来自各大洲的开源软件开发者使用 npm 互相分享和借鉴。包的结构使您能够轻松跟踪依赖项和版本。

仓库:https://www.npmjs.com/

6.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

6.2、包(package)

包是描述一个文件或一个目录。一个包的配置通常由以下构成:

  • 一个文件夹包含一个package.json配置文件。
  • 包含(含有package.json文件的文件夹)的Gzip压缩文件。
  • 解析gzip的url
  • 为注册表添加<name>@<version>的url 信息

注意的是即使你从来没有在注册中心发布你的公共包,你可能仍然可以得到很多所有这些package

6.3、模块(module)

模板是通过配置文件中的一个dom节点进行包含一个或多个包。通常一般由包和配置文件以及相关模块程序构成完成一个或多个业务功能操作。

一个模块可以在node . js 程序中装满任何的require()任何。 以下是所有事物加载模块的例子 :

复制代码
一个文件夹package.json文件包含一个main字段。

一个文件夹index.js文件。

一个JavaScript文件。
复制代码

6.4、包和模块的关系

一般来说在js程序中使用require加载它们的模块在节点中进行配置npm包,一个模块不一定是一个包。

例如,一些cli包, js程序节点中只包含一个可执行的 命令行界面,不提供main字段。 那么这些包不是模块。

几乎所有npm包(至少,那些节点计划)包含许多模块在他们(因为每个文件加载require()是一个模块)。

几乎所有的npm包都关联着多个模块,因为每个文件都使用require()加载一个模块。

从module加载文件中的上下文node节点。如:var req = require('request')。我们可能会说,“request模块赋值给req这个变量”。

6.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的配置路径
复制代码
原文地址:https://www.cnblogs.com/xiongshuangping/p/10057573.html