JS模块化工具 requirejs

为什么使用 RequireJS:

随着网站逐渐变成”互联网应用程序(WebApp)”,嵌入网页的 JavaScript 代码也变得越来越复杂和臃肿,原有通过 script 标签来导入一个个的 js 文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作、模块复用、单元测试等等一系列复杂的需求。
但是,在 ES6 之前,JavaScript 不支持模块化开发。为此 JavaScript 社区做了很多努力,在现有的运行环境中,实现“模块”的效果。
RequireJS 是一个非常小巧的 JavaScript 模块载入框架,是 AMD 规范最好的实现者之一。最新版本的 RequireJS 压缩后只有18K,堪称非常轻量。它还同时可以和其他的框架协同工作,使用 RequireJS 必将使您的前端代码质量得以提升。

官网地址:https://requirejs.org/

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 规范的一个库,它提供了 definerequirerequire.config 等方法,以便开发者能够使用 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直接翻译为”垫”,其实也是有这层意思的,目前我主要用在两个地方:

    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()
    })

    加载机制

    • 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,
      };
    });
    
    THE END
    喜欢就支持一下吧
    点赞0 分享