为什么使用 RequireJS:
随着网站逐渐变成”互联网应用程序(WebApp)”,嵌入网页的 JavaScript 代码也变得越来越复杂和臃肿,原有通过 script
标签来导入一个个的 js 文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作、模块复用、单元测试等等一系列复杂的需求。
但是,在 ES6 之前,JavaScript 不支持模块化开发。为此 JavaScript 社区做了很多努力,在现有的运行环境中,实现“模块”的效果。
RequireJS 是一个非常小巧的 JavaScript 模块载入框架,是 AMD
规范最好的实现者之一。最新版本的 RequireJS 压缩后只有18K,堪称非常轻量。它还同时可以和其他的框架协同工作,使用 RequireJS 必将使您的前端代码质量得以提升。
Github地址:https://github.com/requirejs/requirejs
RequireJS兼容性:
- IE 6+ ……….兼容的 ✔
- Firefox 2+ …..兼容的 ✔
- Safari 3.2+ ….兼容的 ✔
- Chrome 3+ ……兼容的 ✔
- Opera 10+ ……相容 ✔
Js模块化发展史:
1.原始写法
//新建一个tool.js文件,里面有全局变量和函数
var count = 10;
function toolA() {}
function toolB() {}
//在html中引入tool.js文件
//这样就可以在html中使用tool.js中的全局变量和函数
<script src="tool.js">
toolA();
toolB();
console.log(count);
</script>;
//缺点
//1. 全局变量可能会污染全局命名空间,导致命名冲突
//2. 无法保证私有成员不被外部访问
...
2.对象写法
//新建一个tools.js文件,里面有两个模块
var toolsA = {
count: 10,
toolA: function () {},
toolB: function () {},
};
var toolsB = {
count: 20,
toolA: function () {},
toolB: function () {},
};
//在html中引入tools.js文件
//这样就可以在html中使用tools.js中的全局变量和函数
<script src="tools.js">
toolsA.toolA();
toolsA.toolB();
console.log(toolsA.count); //可以访问
toolsB.toolA();
toolsB.toolB();
console.log(toolsB.count); //可以访问
</script>;
//解决了全局变量污染全局命名空间的问题
//缺点
//1. 无法保证私有成员不被外部访问
...
3.立即执行函数写法(闭包)
//新建一个tools.js文件,里面有两个模块
var toolsA =( function () {
var count = 10; //私有变量
function toolA() {}
function toolB() {}
return {
toolA: toolA,
toolB: toolB,
};
})();
var toolsB = (function () {
var count = 20; //私有变量
function toolA() {}
function toolB() {}
return {
toolA: toolA,
toolB: toolB,
};
})();
//在html中引入tools.js文件
//这样就可以在html中使用tools.js中的全局变量和函数
<script src="tools.js">
toolsA.toolA();
toolsA.toolB();
console.log(toolsA.count); //访问不到
toolsB.toolA();
toolsB.toolB();
console.log(toolsB.count); //访问不到
</script>
// 解决了全局变量污染全局命名空间的问题
// 保证了私有成员不被外部访问
//新缺点:
//1.不利于二次开发
...
4.放大模式
//新建一个tools.js文件,里面有两个模块
var toolsA = (function () {
var count = 10; //私有变量
function toolA() {}
function toolB() {}
return {
toolA: toolA,
toolB: toolB,
};
})();
var toolsB = (function () {
var count = 20; //私有变量
function toolA() {}
function toolB() {}
return {
toolA: toolA,
toolB: toolB,
};
})();
///------------------------------------------
//新建一个tools_plus.js文件,里面给toolsA和toolsB模块扩展一个新的方法
toolsA = (function (toolsa) {
function toolC() {}
toolsa.toolC = toolC;
return toolsa;
})(toolsA);
toolB = (function (toolsb) {
function toolC() {}
toolsb.toolC = toolC;
return toolsb;
})(toolsB);
///------------------------------------------
//在html中引入tools.js和tools_plus.js两个文件
//这样就可以在html中使用tools.js和tools_plug.js中的全局变量和函数
<script src="tools.js">
toolsA.toolA();
toolsA.toolB();
console.log(toolsA.count);//访问不到
toolsB.toolA();
toolsB.toolB();
console.log(toolsB.count); //访问不到
</script>;
<script src="tools_plus.js">
toolsA.toolC();
toolsB.toolC();
</script>;
// 解决了全局变量污染全局命名空间的问题
// 保证了私有成员不被外部访问
// 保证了二次开发
//新缺点:
//1.由于引入js是异步的,所以tools_plus.js文件可能会在tools.js文件之前加载,导致toolsA和toolsB模块没有toolC方法。
// 因此,虽然它们在HTML中是按顺序书写的,但实际上它们的加载和执行顺序是不确定的,取决于哪个脚本先加载完成。
5.宽放大模式
//新建一个tools.js文件,里面有两个模块
var toolsA = (function (toolsa) {
var count = 10; //私有变量
function toolA() {}
function toolB() {}
toolsa.toolA = toolA;
toolsa.toolB = toolB;
return toolsa;
})(toolsA || {}); //如果传入的toolsA是undefined,则会传入一个新的对象
var toolsB = (function (toolsb) {
var count = 20; //私有变量
function toolA() {}
function toolB() {}
toolsb.toolA = toolA;
toolsb.toolB = toolB;
return toolsb;
})(toolsB || {}); //如果传入的toolsB是undefined,则会传入一个新的对象
///------------------------------------------
//新建一个tools_plus.js文件,里面给toolsA和toolsB模块扩展一个新的方法
var toolsA = (function (toolsa) {
function toolC() {}
toolsa.toolC = toolC;
return toolsa;
})(toolsA || {}); //如果传入的toolsA是undefined,则会传入一个新的对象
var toolB = (function (toolsb) {
function toolC() {}
toolsb.toolC = toolC;
return toolsb;
})(toolsB || {}); //如果传入的toolsA是undefined,则会传入一个新的对象
///------------------------------------------
//在html中引入tools.js和tools_plus.js两个文件
//这样就可以在html中使用tools.js和tools_plug.js中的全局变量和函数
<script src="tools.js">
toolsA.toolA();
toolsA.toolB();
console.log(toolsA.count);//访问不到
toolsB.toolA();
toolsB.toolB();
console.log(toolsB.count); //访问不到
</script>;
<script src="tools_plus.js">
toolsA.toolC();
toolsB.toolC();
</script>;
// 解决了全局变量污染全局命名空间的问题
// 保证了私有成员不被外部访问
// 保证了二次开发
// 解决了tools_plus.js文件可能会在tools.js文件之前加载的问题
由于每个人写的模块法规范不一样,会增大学习成本。
JS模块化规范:
CommonJS
用途:主要用于 Node.js 环境。
特点:
- 使用
require()
导入模块。 - 使用
module.exports
导出模块。
// math.js
module.exports = {
add: (a, b) => a + b,
};
// app.js
const math = require('./math');
console.log(math.add(2, 3));
AMD (Asynchronous Module Definition)
用途:主要用于浏览器环境,特别是需要异步加载模块的场景。
特点:
- 使用
define()
定义模块。 - 使用
require()
导入模块。
// math.js
define([], function() {
return {
add: (a, b) => a + b,
};
});
// app.js
require(['math'], function(math) {
console.log(math.add(2, 3));
});
UMD (Universal Module Definition)
用途:希望兼容 CommonJS 和 AMD 的场景。
特点:
- 结合了 CommonJS 和 AMD 的特性,根据环境选择合适的导入方式。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.math = factory();
}
}(this, function () {
return {
add: (a, b) => a + b,
};
}));
ES Modules (ESM)
用途:现代 JavaScript 的标准模块化方式,支持在浏览器和 Node.js 中使用。
引入:ESM 规范是在 ES6 中引入的,因此任何支持 ES6 及以上版本的环境都应支持 ESM。
特点:
- 使用
import
导入模块。 - 使用
export
导出模块。 - 支持静态分析和树摇优化。
// math.js
export const add = (a, b) => a + b;
// app.js
import { add } from './math.js';
console.log(add(2, 3));
IIFE (Immediately Invoked Function Expression)
用途:虽然不是标准的模块化规范,但常用于创建私有作用域。
特点:
- 通过函数立即执行来封装模块。
const math = (function() {
const add = (a, b) => a + b;
return { add };
})();
console.log(math.add(2, 3));
RequireJS介绍:
AMD(Asynchronous Module Definition)规范本身并没有直接提供任何函数,它只是一套定义模块的规范,描述了模块如何定义、依赖和加载。
关系概述
规范与实现的关系: AMD 规范就像是建筑蓝图,规定了模块应该如何构建。而 RequireJS 则是按照蓝图建造的房子,提供了具体的实现方式。
- AMD 规范:
- AMD 规范定义了一种异步加载模块的方式,使得模块能够在浏览器环境中有效地管理依赖关系。
- RequireJS:
- RequireJS 是实现 AMD 规范的一个库,它提供了
define
、require
和require.config
等方法,以便开发者能够使用 AMD 规范来定义和加载模块。
- RequireJS 是实现 AMD 规范的一个库,它提供了
方法解释
define
:用于定义一个模块及其依赖关系,在 AMD 规范中是必需的。require
:用于加载一个或多个模块,并在加载完成后执行回调,符合 AMD 的异步加载特性。require.config
:提供配置选项,允许开发者设置模块的路径和其他参数,增强了模块的灵活性和可管理性。
define 模块定义
define('模块名', ['依赖模块名', ...], function(依赖模块返回的对象,...) {
// 模块的实现 function
// return 返回结果 可以是任何数据类型或者不返回都可以
})
define('helper', ['jquery'], function($) {
return {
trim: function(str) {
return $.trim(str);
}
}
})
define 函数包含三个参数,模块名、模块依赖、模块的实现 function:
- 模块名可以不写,默认以文件路径(相对于 baseUrl)作为模块名。
- 依赖的模块是个数组,如果没有也可以不写。
- 依赖的模块执行下载完成之后,会把模块参数传到模块实现的 function 形参里面,参数的顺序对应着模块依赖的顺序。
require 模块加载
require(['模块名'], function(模块导出的对象) {
// 加载完后的 function
var str = helper.trim(' amd ');
console.log(str);
})
require(['helper'], function(helper) {
var str = helper.trim(' amd ');
console.log(str);
})
- require 的依赖是一个数组,即使只有一个依赖,你也必须使用数组来定义,否则会报 Uncaught Error: Invalid require call 错误。
- require API 的第二个参数是 callback,一个 function,是用来处理加载完毕后的逻辑。
require.config 模块配置
由于define不自定义模块名,默认是当前文件的绝对路径,通过require引入需要填写完整的路径,通过require.config配置路径后,我们再引入模块的时候就没必要再写路径了。
require.config({
baseUrl: "/js", //基础路径
waitSeconds: 0, //下载 js 等待的时间
urlArgs: "v=" + new Date().getTime(), //网址额外参数
sim: {}, //配置不支持 AMD 的库和插件
map: {}, //版本映射
paths: {
//自定义模块名:模块路径
moduleName: "modulePath",
},
});
baseUrl
requirejs 以一个相对于 baseUrl 的地址来加载所有的代码。
- 首先如果通过 require.config() 显式配置了 baseUrl,那么优先级最高 。
- 再者如果没有显示配置 baseUrl,而使用了 data-main 属性,那么 baseUrl 为 data-main 属性 JS 脚本所在的目录。
- 最后如果两个都没有,那么 baseUrl 等于包含运行 RequireJS 的 HTML 页面的目录。
paths
映射不放于 baseUrl 下的模块名。比如 jquery 的模块名不是相对于 baseUrl 下的模块,这个时候就可以配置 paths 参数,让模块名和路径能匹配上。
- paths 参数可以设置一组脚本的位置,值可以是一个字符串或数组。
- 一般都是通过 baseUrl + path 的方式来引入脚本。
- requirejs 加载的脚本不能有 .js 后缀申明(因为 requirejs 默认会加上 .js 后缀)。
- 有时候确实希望直接引用脚本,而不遵循 baseUrl + paths 规则来查找它。 如果模块 ID 具有以下特征之一,那么该 ID 将不会通过 baseUrl + paths 配置传递,而只是作为相对于文档的常规 URL 处理:
- 以
".js"
结尾. - 以
"/"
开头. - 包含 URL protocol, 如 “http:” 或 “https:”.
- 以
shim
第三方模块,配置不支持 AMD 的库和插件,比如 Modernizr.js 、bootstrap。
require.config({
shim: {
"modernizr" : {// 配置不支持 AMD 的模块
deps: ['jquery'], // 依赖的模块,此处假设依赖 jquery
exports : "Modernizr",// 把全局变量Modernizr作为模块对象导出
init: function($) { // 初始化函数,返回的对象替换 exports,作为模块对象
return $;
}
}
}
})
通过require
加载的模块一般都需要符合 AMD 规范,即使用 define
来申明模块,但是部分时候需要加载非AMD规范的 js,这时候就需要用到另一个功能:shim,shim解释起来也比较难理解,shim直接翻译为”垫”,其实也是有这层意思的,目前我主要用在两个地方:
- 非AMD模块输出,将非标准的 AMD 模块”垫”成可用的模块,例如:在老版本的 jquery 中,是没有继承 AMD 规范的,所以不能直接 require[“jquery”], 这时候就需要 shim,比如我要是用 underscore 类库,但是他并没有实现 AMD 规范,那我们可以这样配置
require.config({
shim: {
"underscore" : {
exports : "_";
}
}
})
这样配置后,我们就可以在其他模块中引用 underscore 模块:
require(["underscore"], function(_){
_.each([1,2,3], alert);
})
- 插件形式的非AMD模块,我们经常会用到 jquery 插件,而且这些插件基本都不符合 AMD 规范,比如 jquery.form 插件,这时候就需要将 form 插件”垫”到 jquery 中:
require.config({
shim: {
"underscore" : {
exports : "_";
},
"jquery.form" : {
deps : ["jquery"]
}
}
})
//也可以简写为:
require.config({
shim: {
"underscore" : {
exports : "_";
},
"jquery.form" : ["jquery"] //只有deps配置时可简化为一个数组
}
})
这样配置之后我们就可以使用加载插件后的 jquery 了
require.config(["jquery", "jquery.form"], function($){
$(function(){
$("#form").ajaxSubmit({...});
})
})
map
版本映射。和 paths 配置有点类似,可简单看做是针对某个模块的 paths 配置。
项目开发初期使用 jquery1.12.3,后期以为需要支持移动开发,升级到 jquery2.2.3。但是又担心之前依赖 jquery1.12.3 的代码升级到 2.2.3 后可能会有问题,就保守的让这部分代码继续使用 1.12.3 版本。
requirejs.config({
map: {
'*': {
'jquery': './lib/jquery'
},
'app/api': {
'jquery': './lib/jquery'
},
'app/api2': {
'jquery': './lib/jquery2'
}
}
});
*
表示所有模块中使用,将加载 jquery.js。- 当 app/api 模块里加载 jquery 模块时,将加载 jquery.js。
- 当 app/api2 模块里加载 jquery 模块时,将加载 jquery2.js。
特别注意:此功能仅适用于调用 define() 并注册为匿名模块的真正 AMD 模块的脚本。以上面的 jquery 举例(非匿名模块),map 中声明的 jquery 和在 paths 中声明的会有所不同,具体变现为在 map 中声明的 jquery 在使用 require(['jquery'])
或 define(['jquery'])
声明依赖时,脚本可以正常引入但不会被注入对应处理函数的形参中。
// app/util1.js
define(function () {
return {
name: 'util1'
};
});
// app/util2.js
define(function () {
return {
name: 'util2'
};
});
// app/admin.js
define(['util'], function (util) {
console.log(util); // 结果为 {name: 'util2'}
return {
name: 'admin'
}
});
// app/main.js
require.config({
map: {
'*': {
util: 'app/util',
},
'app/admin': {
util: 'app/util2'
}
}
});
// index.html
require(['app/admin'], function (admin) {
console.log(admin);
});
waitSeconds
下载 js 等待的时间,默认7 秒。如果设置为 0 ,则禁用超时等待。
urlArgs
下载文件时,在 url 后面增加额外的 query 参数。
require.config({
urlArgs:"_= " +(new Date()).getTime()
})
加载机制
- requireJS 使用
**head.appendChild()**
将每一个依赖加载为一个 script 标签( 可从 js 文件响应头信息Content-Type: application/javascript
看出)。所以可以跨域访问,比如从 CDN 上加载一个 JS 文件。 - 模块加载后会立即执行。
JSONP
同源策略:www.baidu.com 通过 ajax 不能获取 www.qq.com 的数据。
jsonp 是 json 的一种使用模式,可以跨域获取数据,如 json。原理通过 script 标签的跨域请求来获取跨域的数据。
//requirejs 是通过script标签来加载模块
require(['http://xxx.test/user.js'], function (user) {
console.log(user);
});
//user.js 返回内容
define({
id: '',
username: ''
})
RequireJS使用:
下载 requireJS
// src 属性规定外部脚本文件的 URL。
// defer IE兼容的异步加载js文件
// async 异步加载js文件
// data-main 配置 RequireJS 的主文件
// html中引入require.js文件
<script
data-main="main"
src="https://requirejs.org/docs/release/2.3.7/comments/require.js"
async="true"
defer
></script>;
方式一:
// 新建 main.js 文件
//引用模块
require(["jquery", "js/myModule"], function ($, myModule) {
// jquery 已加载并且 myModule 已加载,执行这里的代码
$(document).ready(function () {
myModule.sayHello();
myModule.add(1, 18);
});
});
// 在js目录下新建 myModule.js 文件
//定义一个模块,模块未定义模块名,默认为文件名
define(function () {
function add(a, b) {
console.log(a + b);
}
return {
sayHello: function () {
console.log("Hello, world!");
},
add: add,
};
});
方式二:
// 新建 main.js 文件
//管理当前.html页面所引入的所有模块
require.config({
baseUrl: "js",
paths: {
jquery: "libs/jquery-3.6.0",
myModule: "js/myModule",
},
});
//引用模块
require(["jquery", "myModule"], function ($, myModule) {
$(document).ready(function () {
myModule.sayHello();
myModule.add(1, 18);
});
});
// 在js目录下新建 myModule.js 文件
//定义一个模块,模块未定义模块名,默认为文件名
define(function () {
function add(a, b) {
console.log(a + b);
}
return {
sayHello: function () {
console.log("Hello, world!");
},
add: add,
};
});