为什么使用 requirejs
随着网站逐渐变成"互联网应用程序(WebApp)",嵌入网页的 JavaScript 代码也变得越来越复杂和臃肿,原有通过 script
标签来导入一个个的 js 文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作、模块复用、单元测试等等一系列复杂的需求。
但是,在 ES6 之前,JavaScript 不支持模块化开发。为此 JavaScript 社区做了很多努力,在现有的运行环境中,实现“模块”的效果。
RequireJS 是一个非常小巧的 JavaScript 模块载入框架,是 **AMD**
规范最好的实现者之一。最新版本的 RequireJS 压缩后只有18K,堪称非常轻量。它还同时可以和其他的框架协同工作,使用 RequireJS 必将使您的前端代码质量得以提升。
兼容性
- IE 6+ ..... 兼容 ✔
- Firefox 2+ ..... 兼容 ✔
- Safari 3.2+ .... 兼容 ✔
- Chrome 3+ ......兼容 ✔
- Opera 10+ ......兼容 ✔
主要功能
- 异步加载文件
- 一个文件一个模块
- 减少全局变量
- jsonp 支持
模块化写法变迁
原始写法
模块就是实现特定功能的一组方法。只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。比如 tool.js :
var count = 10;
function showA() {}
function showB() {}
- 上面的函数 showA 和 showB,组成了一个模块。使用的时候,直接调用就行了。
- 这种做法的缺点很明显:“污染”了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
对象写法
为了解决原始写法的缺点,可以把模块写成一个对象,所有的模块成员都放到一个对象里面。
var moduleA = {
count: 10,
showA: function() {},
showB: function() {}
}
- 最大问题,count 变量会暴露在外,有被篡改的风险。
立即执行函数(闭包)
使用立即执行函数(IIFE),可以达到不暴露私有成员的目的。
(function() {
var count = 10;
function showA() {}
function showB() {}
return {
outA: showA,
outB: showB
}
})()
- 可以解决全局变量污染问题和私有化变量和方法。最大的问题:不利于二次开发。
放大模式
//moduleA.js
var moduleA = (function() {
var count = 10;
function showA() {}
function showB() {}
return {
outA: showA,
outB: showB
}
})();
//moduleA.Plus.js
moduleA = (function(mod) {
mod.showC = function() {};
return mod;
})(moduleA)
- 最大的缺点就是,加载必须有先后顺序。
宽放大模式
//moduleA.js
var moduleA = (function(mod) {
var count = 10;
function showA() {}
function showB() {}
mod.outA = showA;
mod.showB = showB;
return mod;
})(moduleA || {});
//moduleA.Plus.js
var moduleA = (function(mod) {
mod.showC = function() {};
return mod;
})(moduleA || {})
模块化规范
CommonJS
适用于服务器端规范,js 文件是本地下载,代码同步执行。
module.exports = {
outA: showA,
outB: showB
}
var moduleA = require('moduleA');
moduleA.outA();
moduleA.outB();
AMD
客户端/浏览器。所有的 js 文件都得先下载,代码采用异步执行的方式。
define(function() {
return {
outA: showA,
outB: showB
}
})
require(['moduleA'], function(moduleA) {
//模块引入之后执行
})
alert('同步执行');
requirejs 与常规写法对比
正常编写方式
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="js/a.js"></script>
</head>
<body>
<span>body</span>
</body>
</html>
function fun1(){
alert("it works");
}
fun1();
可能你更喜欢这样写
(function(){
function fun1(){
alert("it works");
}
fun1();
})()
第二种方法使用了块作用域来申明 function 防止污染全局变量,本质还是一样的,当运行上面两种例子时不知道你是否注意到,alert 执行的时候,html 内容是一片空白的,即<span>body</span>
并未被显示,当点击确定后,才出现,这就是JS阻塞浏览器渲染导致的结果。
requirejs写法
下载 requireJS
规范建议
- 模块化开发的作用域:管理当前页面上引入的所有
.js
文件。 - 入口文件 main.js :管理当前 html 页面使用所有的 js 代码。每一个 html 文件都要一个入口文件。
- 也即是说整个 html 文件只能有这么一个 scritp 标签。所有的 js 文件都在 main.js 文件中管理(在 main.js 内部,可以使用 require函数来加载需要运行的任何其他脚本)。每个入口文件给一个 html 文件使用。
<!-- data-main attribute tells require.js to load scripts/main.js after require.js loads. -->
<!-- 指定的 data-main 脚本是异步加载的 -->
<script src="require.js" data-main="main" async="true" defer></script>
使用 requirejs 改写常规写法
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="require.js"></script>
<script type="text/javascript">
require(["js/a"]);
</script>
</head>
<body>
<span>body</span>
</body>
</html>
define(function(){
function fun1(){
alert("it works");
}
fun1();
})
浏览器提示了 "it works"
,说明运行正确,但是有一点不一样,这次浏览器并不是一片空白,body 已经出现在页面中。目前为止可以知道 requirejs 具有如下优点:
- 防止 js 加载阻塞页面渲染
- 使用程序调用的方式加载 js,防出现如下丑陋的场景
<script type="text/javascript" src="js/a.js"></script>
<script type="text/javascript" src="js/b.js"></script>
<script type="text/javascript" src="js/c.js"></script>
<script type="text/javascript" src="js/d.js"></script>
<script type="text/javascript" src="js/e.js"></script>
<script type="text/javascript" src="js/f.js"></script>
<script type="text/javascript" src="js/g.js"></script>
<script type="text/javascript" src="js/h.js"></script>
<script type="text/javascript" src="js/i.js"></script>
<script type="text/javascript" src="js/j.js"></script>
基本API
require 会定义三个变量:
- define()
- require()
- requirejs()
其中 require === requirejs
,一般使用 require 更简短。
define
定义一个模块。
define('模块名', ['依赖模块名', ...], function(依赖模块返回的对象,...) {
// 模块的实现 function
// return 返回结果 可以是任何数据类型或者不返回都可以
})
define('helper', ['jquery'], function($) {
return {
trim: function(str) {
return $.trim(str);
}
}
})
define 函数包含三个参数,模块名、模块依赖、模块的实现 function:
- 模块名可以不写,默认以文件路径(相对于 baseUrl)作为模块名。
- 依赖的模块是个数组,如果没有也可以不写。
- 依赖的模块执行下载完成之后,会把模块参数传到模块实现的 function 形参里面,参数的顺序对应着模块依赖的顺序。
define({
username: 'silva',
age: 18
})
最佳实践:不建议自定义模块名,比如下面的写法就会出现问题:
- 能引入 helper.js 文件,但并能获取到模块的导出,打印 helper 为 undefined。
- 需要在 helper.js 中命名模块名为 add/helper 或者直接删除模块名。
- 解决上面这个问题后,刷新后还是报上述错误,发现能正常引入 jquery.js 了,但打印 $ 为 undefined。
- 同样是模块名的问题,因为 jquery.js 文件中定义了模块名叫 jquery,而当 requireJS 根据模块名
../lib/jquery
去查找模块时是找不到的,结果为 undefined 。 - 不建议直接修改 jquery 源码中的模块名,可通过在 app.js 中配置
require.config
的paths: {jquery: "lib/jquery"}
参数。这样一来模块名和模块地址就都对应的上了。
- 同样是模块名的问题,因为 jquery.js 文件中定义了模块名叫 jquery,而当 requireJS 根据模块名
正确写法:
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,是用来处理加载完毕后的逻辑。
加载机制
- 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: ''
})
全局配置
上面的例子中重复出现了require.config
配置,如果每个页面中都加入配置,必然显得十分不雅,requirejs 提供了一种叫"主数据"的功能,我们首先创建一个 main.js:
require.config({
urlArgs: "_=" + (new Date()).getTime(),
waitSeconds: 7,
paths : {
"jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery", "js/jquery"],
"a" : "js/a"
}
})
然后再页面中使用下面的方式来使用 requirejs:
<script data-main="js/main" src="js/require.js"></script>
解释一下,加载 requirejs 脚本的 script 标签加入了**data-main**
属性,这个属性指定的 js 将在加载完 require.js 后处理(异步加载,不会阻塞后面的 js)。这样当我们把require.config
的配置加入到data-main
指定的 js 文件后,就可以使后面每一个页面都使用这个配置,然后页面中就可以直接使用 require
来加载所有的短模块名。
data-main
还有一个重要的功能,当 script 标签指定 data-main 属性时,require 会默认的将 data-main 指定的 js 为根路径,是什么意思呢?
- 如上面的
data-main="js/main"
设定后,我们在使用require(['jquery'])
后(不配置 jquery 的 paths),require 会自动加载 js/jquery.js 这个文件,而不是 jquery.js,相当于默认配置了:require.config({ baseUrl : "js"})
。
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:".
- 以
require.config({
baseUrl: '/js',
paths: {
jquery: 'lib/jquery', //格式为 模块名: 模块路径
}
})
之前的例子中加载模块都是本地 js,但是大部分情况下网页需要加载的 JS 可能来自本地服务器、其他网站或 CDN,这样就不能通过这种方式来加载了,以加载一个 jquery 库为例:
require.config({
paths : {
"jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"]
}
})
require(["jquery", "js/a"],function($){
$(function(){
alert("load finished");
})
})
这边涉及了**require.config**
,paths
是用来配置模块加载位置,简单点说就是给模块起一个更短更好记的名字,比如将百度的 jquery 库地址标记为 jquery
,这样在 require 时只需要写["jquery"]
就可以加载该 js,本地的 js 我们也可以这样配置:
require.config({
paths : {
"jquery" : "http://libs.baidu.com/jquery/2.0.3/jquery",
"a" : "js/a"
}
})
require(["jquery", "a"],function($){
$(function(){
alert("load finished");
})
})
通过 **paths**
的配置会使我们的模块名字更精炼,paths 还有一个重要的功能,就是可以配置多个路径,如果远程 cdn 库没有加载成功,可以加载本地的库,如:
require.config({
paths : {
"jquery" : [
"http://libs.baidu.com/jquery/2.0.3/jquery",
"js/jquery"
],
"a" : "js/a"
}
})
require(["jquery", "a"],function($){
$(function(){
alert("load finished");
})
})
这样配置后,当百度的 jquery 没有加载成功后,会加载本地 js目录下的 jquery
- 在使用 requirejs 时,加载模块时不用写
**.js**
后缀的,当然也是不能写 js 后缀 - 上面例子中的 callback 函数中发现有
$
参数,这个就是依赖的jquery
模块的输出变量,如果你依赖多个模块,可以依次写入多个参数来使用:
require(["jquery","underscore"],function($, _){
$(function(){
_.each([1,2,3],alert);
})
})
如果某个模块不输出变量值,则没有,所以尽量将输出的模块写在前面,防止位置错乱引发误解。
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()
})
插件
text 插件
用于加载文本文件的 requirejs 插件。通过 ajax 请求来加载文本。
require.config({
paths: {
text: './lib/require/text'
},
config: {
// 配置 text
text: {
onXhr: function(xhr, url) {
//发送ajax请求前
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
//Called after the XHR has been created and after the
//xhr.open() call, but before the xhr.send() call.
//Useful time to set headers.
//xhr: the xhr object
//url: the url that is being used with the xhr object.
},
createXhr: function() {
//覆盖ajax请求对象
//Overrides the creation of the XHR object. Return an XHR
//object from this function.
//Available in text.js 2.0.1 or later.
},
onXhrComplete: function(xhr, url) {
//ajax请求完成之后
//Called whenever an XHR has completed its work. Useful
//if browser-specific xhr cleanup needs to be done.
}
}
}
})
text 后面的路径如果为相对路径,则相对的是当前 js 脚本所在的路径。
// 前缀 text! 加载 /user.html 内容
require(['text!/user.html'], function(template) {
$('#userinfo').html(template);
})
//使用 !strip 只获取html的body部分内容
require(['text!/user.html!strip'], function(template) {
$('#userinfo').html(template);
})
css 插件
用于加载样式文件的 requires插件。
require.config({
map: {
'*': {
css: './lib/require/css' // 当然也可以在 paths 下配置
}
},
// paths: {
// css: './lib/require/css'
// }
})
// css! 前缀
require([
'css!/css/jquery-ui/jquery-ui.css',
'css!/css/jquery-ui/jquery-ui.theme.css'
], function() {
})
require.config({
map: {
'*': {
css: './lib/require/css'
}
},
shim: {
// 简化
'jquery-ui': [
'css!/css/jquery-ui/jquery-ui.css',
'css!/css/jquery-ui/jquery-ui.theme.css'
]
}
})
require(['jquery-ui'], function() {
})
i18n 插件
i8n,支持国际化多语言,比如同时支持英语和中文。
require.config({
paths: {
i18n: './lib/require/i18n'
},
config: {
i18n: {
locale: 'en'
}
}
})
// nls/messages.js
define({
zh: true,
en: true
})
// nls/zh/messages.js
define({
edit: '编辑'
})
// nls/en/messages.js
define({
edit: 'Edit'
})
// 前缀 i18n! 相对的是当前脚本路径
require(['i18n!../nls/messages'], function(i18n) {
console.log(i18n) //{edit: 'Edit'}
})
- 模块名必须包含 nls 目录
如何指定使用那种语言:
- 通过浏览器的 navigator.language 或 navigator. userLanguage 属性
- 通过配置文件 require.config 配置,可配合 cookie 实现切换
打包
完成开发后并希望为最终用户部署代码,可以使用优化器将 JavaScript 文件组合在一起并压缩它。在上面的例子中,它可以将 main.js 和 helper/util.js 合并到一个文件中并压缩。
开发阶段:
- 不打包,不压缩,模块化开发
部署阶段:
- 自动打包、压缩
安装环境
- 首先需要安装 nodejs 环境,再安装 requirejs,
**npm install -g requirejs**
- 或下载 r.js 文件,使用 r.js 打包(这种方式可能更适合于服务器端打包)
命令打包
r.js.cmd -o baseUrl=src/js name=app out=build.js
或
node r.js -o baseUrl=src/js name=app out=build.js
- baseUrl:设置打包的基础目录
- name:要打包的文件名(不含后缀)
- out:打包后输出的文件名(需加后缀)
配置文件打包
node r.js -o app.build.js
({
appDir: './src', //要打包的根目录
baseUrl: './js', //js文件在这个baseUrl下
dir: './build',//打包后的输出目录
mainConfigFile: 'src/js/main.js',//requirejs配置文件
name: 'app', //打包哪一个模块
})
多模块打包
modules :数组格式,列出所有需要打包的模块。
当打包一个模块时,默认会打包所有依赖的模块。
({
appDir: './src', //要打包的根目录
baseUrl: './js', //js文件在这个baseUrl下
dir: './build',//打包后的输出目录
mainConfigFile: 'src/js/main.js',//requirejs配置文件
optimize: 'none',//uglify
modules: [{
name: 'app',
include: ['modernizr'],//一起打包的文件
insertRequire: [],//额外加载模块
exclude: [], //移除打包的文件
excludeShallow: ['backbone'], //浅移除
}, {
name: 'user'
}]
})
- 把配置信息的
modules
下的所有模块建立好完整的依赖关系,再把相应的文件打包合并到dir
目录 - 把所有的
css
文件中,使用@import
语法的文件自动打包合并到dir
目录 - 把其他文件复制到
dir
目录,比如图片、附件等
requirejs 插件打包
比如 text、css、i18n 插件。
({
appDir: './src', //要打包的根目录
baseUrl: './js', //脚本的根路径 相对于程序的根路径
dir: './build',//打包后的输出到的路径
optimize: 'none',//打包结果优化; 压缩等 uglify
mainConfigFile: 'src/js/main.js',//requirejs配置文件
inlineText: false, //是否打包text插件所引入的html文件
// 需要打包合并的js模块,数组形式,可以有多个
// 比如 main 依赖 a 和 b,a 又依赖 c,则 {name: 'main'} 会把 c.js、a.js、b.js、main.js 合并成一个 main.js
modules: [{
name: 'app', //以 baseUrl 为相对路径,无需写 .js 后缀
include: [],//一起打包的文件 强制建立依赖关系
insertRequire: [],//额外加载模块
exclude: [], //移除打包的文件
excludeShallow: [],
}],
// 通过正则以文件名排除文件/文件夹
// 比如当前的正则表示排除 .svn、.git 这类的隐藏文件
fileExclusionRegExp: /^\./
})
css 打包
需要去 require-css 下载 css-builder.js 和 normalize.js 这两个文件放到 css.js 同级目录,就可以把 css 文件和模块一起打包。
使用 npm工具打包
之前的打包命令难以记住,容易出错,可以使用 npm init 生成 package.json 文件,配置 scripts 选项,然后执行 npm run-script 或者 npm run 命令。
{
"scripts": {
"build" : "node src/r.js -o src/app.build.js"
}
}