RequireJS 入门

发布时间 2023-03-27 21:44:03作者: 白開水

Requirejs和自动打包视频教程

为什么使用 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
})

最佳实践:不建议自定义模块名,比如下面的写法就会出现问题:
image.png

  • 能引入 helper.js 文件,但并能获取到模块的导出,打印 helper 为 undefined。
    • 需要在 helper.js 中命名模块名为 add/helper 或者直接删除模块名。

image.png

  • 解决上面这个问题后,刷新后还是报上述错误,发现能正常引入 jquery.js 了,但打印 $ 为 undefined。
    • 同样是模块名的问题,因为 jquery.js 文件中定义了模块名叫 jquery,而当 requireJS 根据模块名 ../lib/jquery 去查找模块时是找不到的,结果为 undefined 。
      • image.png
    • 不建议直接修改 jquery 源码中的模块名,可通过在 app.js 中配置 require.configpaths: {jquery: "lib/jquery"}参数。这样一来模块名和模块地址就都对应的上了。

image.png
正确写法:
image.png

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直接翻译为"垫",其实也是有这层意思的,目前我主要用在两个地方:

  1. 非AMD模块输出,将非标准的 AMD 模块"垫"成可用的模块,例如:在老版本的 jquery 中,是没有继承 AMD 规范的,所以不能直接 require["jquery"], 这时候就需要 shim,比如我要是用 underscore 类库,但是他并没有实现 AMD 规范,那我们可以这样配置
require.config({
    shim: {
        "underscore" : {
            exports : "_";
        }
    }
})

这样配置后,我们就可以在其他模块中引用 underscore 模块:

require(["underscore"], function(_){
    _.each([1,2,3], alert);
})
  1. 插件形式的非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 插件

https://segmentfault.com/a/1190000002390643

用于加载样式文件的 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 实现切换

打包

参考文档1

完成开发后并希望为最终用户部署代码,可以使用优化器将 JavaScript 文件组合在一起并压缩它。在上面的例子中,它可以将 main.js 和 helper/util.js 合并到一个文件中并压缩。

开发阶段:

  • 不打包,不压缩,模块化开发

部署阶段

  • 自动打包、压缩

安装环境

  • 首先需要安装 nodejs 环境,再安装 requirejs,**npm install -g requirejs**
  • 或下载 r.js 文件,使用 r.js 打包(这种方式可能更适合于服务器端打包)

命令打包

image.png

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'
    }]
})
  1. 把配置信息的 modules下的所有模块建立好完整的依赖关系,再把相应的文件打包合并到 dir 目录
  2. 把所有的 css文件中,使用 @import 语法的文件自动打包合并到 dir目录
  3. 把其他文件复制到 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"
  }
}

例子