js模块化加载以及听说很久了,苦于实际项目中并没有用到,一直没有学习。今天在学习ES6的import,export时,突然意思到自己对requireJs还不会呢,就顺道一块学习了。requireJs是外国人的作品,官方文档也是纯英文,本人英语水平不好,只能是一半看英文文档,一半看代码例子学习,再加上实践,很快就掌握了。
JS的模块加载有两种规范:commonJS,AMD。nodeJs服务器中实现的是commonJS的模块加载,而我们前端页面使用的requireJs使用的是AMD规范。
requireJs的源码由于太长,没有细看,大概扫了一下,它是用过异步加载的方式创建script标签实现的,通过这script标签上添加“onreadystatechange”或者“onload”事件来监听加载的完成事件。script标签添加到head或者body中后,浏览器发起http请求,在加载完成后(也已经执行完成了),调用script标签上监听的回调,define定义的模块会在requireJs内部的一个数组中添加一个变量指向插件对象,非define定义的模块通过window全局对象的属性来获取插件对象。然后把对象作为参数传递给使用者定义的callback回调函数中。
requireJs的逻辑不算很复杂,但代码量确实不少,它不只包含了js的加载,对于json,图片,文本等资源的加载也支持,还有一些代码用来处理兼容性问题,它能兼容到IE6浏览器。我从requireJS官网下载的源码有2000+行代码。有兴趣的也可以去研究requireJs的源码。
requirejs使用前我们先要用标签引入requirejs文件
<script data-main="js/app.js" src="js/require.js"></script>
其中的data-main属性用来表示requirejs自身加载完成后,开始异步加载的js文件,可以认为是入口文件。如果你想让requireJs也异步加载,可以采用下面的写法
<script data-main="js/app.js" src="js/require.js" defer='defer' async='async'></script>
defer是IE浏览器支持的脚本异步加载的标签,async是HTML5的新属性,表示脚本异步加载。当然这种写法我们是抛弃了FireFox和chrome的旧版本,不过无所谓了,这种现代浏览器的旧版本即使你想找,也不太好找到的。
加入了requirejs后,我们要完成data-main属性指定的入口方法(requirejs中对于data-main属性是遍历页面中的所有script标签从中获取的第一个"data-main"属性,所以你也可以写在你页面中拥有的任何script标签上)。
requirejs.config({ baseUrl: 'js/lib', paths: { app: '../app' } }); requirejs(['jquery', 'canvas', 'app/sub'],function ($, canvas, sub) { ...//加载完依赖库后执行代码 );
在app.js中,我们可以先通过config方法设置一些requirejs的属性,然后调用requirejs来加载文件,requirejs的默认后缀名是“.js”,所以我们可以省略它。
requirejs有两个参数,实际源码中是有4个参数的,
正常的我们只使用两个参数,第一个参数是deps,表示要加载的js文件,是一个数组,第二个是加载成功的回调。
此外我们还可以将第一个参数放置config对象,那么第一个参数就作为config对象,第二个参数callback就成了要加载的js数组,第三个参数errback就是加载成功的回调了,第四个参数optional就是加载错误的回调了。这种写法是将config放到requirejs方法内部了。官网没有给出这样的例子,自然是不建议这么写,你知道还可以这么用就行了。
加载完成的回调中requirejs会把加载的js作为参数传入到回调函数中。
上面的加载写法是用了文件名,默认采用的是和data-main同一个目录的路径的,可以通过config来修改加载路径。
你还可以这样写:
<script src="scripts/require.js"></script> <script> require(['scripts/config'], function() { require(['foo'], function(foo) { }); }); </script>
嵌套加载的写法requirejs也是支持的。加载就这么点东西,很easy,此外我们需要了解config可以配置的属性。
<script src="scripts/require.js"></script> <script> require.config({ baseUrl: "/another/path", paths: { "some": "some/v1.0" }, waitSeconds: 15 }); require( ["some/module", "my/module", "a.js", "b.js"], function(someModule, myModule) { } ); </script>
baseUrl用来设置加载js的跟路径,paths,用来分开设置加载js文件的路径,你也可以在require方法中的加载数组中写上加载路径。上面的“baseUrl”,"paths","some/module"是三种设置js文件路径的方式,而不是一套完整的代码。
requireJs还提供了对于非AMD规范的js的加载(就是没有使用define方法的插件)。
requirejs.config({ shim: { 'backbone': { deps: ['underscore', 'jquery'], exports: 'Backbone' }, 'underscore': { exports: '_' }, 'foo': { deps: ['bar'], exports: 'Foo', } } });
这里是通过配置“shim”属性来加载非AMD规范的插件,shim中的属性是供require作为名称加载的插件,deps是插件的依赖,会先加载依赖,然后加载对应的插件。exports是requirejs获取插件对象的途径,requirejs会在插件加载完成后,通过"window.Backbone"获得插件对象传递给回调函数的。shim配置中,deps没有可以不填,对于非AMD规范的插件,exports属性是必须的。
requireJS的config还有一些其他的属性,比如"bundles",“map”,"packages"等,但是这些属性属于更灵活的用途,本文不讲解了,有需要的可以去requirejs官网查看api。
所谓AMD规范的模块,就是使用requirejs中的define方法定义的模块插件。
define({ color: "black", size: "unisize" });
define(function () { ...//具体代码 return { color: "black", size: "unisize" } });
define(["./cart", "./inventory"], function(cart, inventory) { //带有依赖的插件定义 return { color: "blue", size: "large", addToCart: function() { inventory.decrement(this); cart.add(this); } } } );
define(function(require, exports, module) { var a = require('a'), b = require('b'); return function () {}; } );
define("foo/title", ["my/cart", "my/inventory"], function(cart, inventory) { } );
define(function(require, exports, module) { var a = require("a"); exports.foo = function () { return a.bar(); }; });
定义符合AMD规范的插件,可以采用以上几种形式来定义,通过define函数来使插件符合AMD规范。
而jquery是使用以下形式实现的:
if ( typeof define === "function" && define.amd ) { define( "jquery", [], function() { return jQuery; }); }
define方法是往requirejs内部的“globalDefQueue”数组中添加一个对象,那么这个对象的名字就叫“jquery”,那么就是说jquery的模块名就叫“jquery”,我们使用requirejs加载js的时候,第一个参数的依赖数组元素的名称就是要从这个内部数组中查找对应的对象的。所以加载jquery必须使用“jquery”为模块名加载才行,不然就只能按照非AMD模块的形式加载,设置shim属性。
下面是我用requirejs实现瀑布流页面加载的代码:
<script src="js/require.js"></script> <script> requirejs.config({ baseUrl:"js", shim:{ "mwaterfall":{ deps:["jquery"], exports:"mwaterfall" } } }); requirejs(['jquery','mwaterfall'],function($,mwaterfall){ $(function(){ var fall= mwaterfall({it_cont:".fall_cont",it_w:200}); window.onscroll=function(){ var minCol=fall.getMinHeigtColumn(); var rect=minCol[0].getBoundingClientRect(); if(rect.bottom<=document.documentElement.clientHeight){ var arr=[]; for(var i=1;i<6;i++){ var elm=$('<div class="fall_item">\ <img src="images/fall'+i+'.jpg"/>\ <p>延迟加载的瀑布流</p>\ </div>'); arr.push(elm); } fall.appendfalls(arr); } } }); });
es6中原生提供了模块的加载,但是它是静态模块加载方式,仍然替代不了requirejs的地位。
import * as myModule from "my-module"; import {myMember} from "my-module"; import MyModule, {foo, bar} from "my-module";
export { myFunction }; export function cube(x) { return x * x * x; } export default function cube(x) { return x * x * x;}
ES6像要用import加载,模块必须用export来提供加载使用的接口,export default用来设置默认的加载接口,而import默认情况下是加载export default设置的接口,但可以通过加载多个模块来加载其他的export。import加载的名称和export设置的名称是对应的。不过你可以使用 ”name as name2“的方式来设置别名。import中可以使用解构赋值的写法值加载整个模块中的单独几个export接口,而不是全部加载。
目前ES6的module在浏览器中没有任何支持,唯一的使用地方,只能是一些“typeScript”,"Babel"转换等。掌握不掌握不重要,但一定要理解,angularJS2里面也需要使用ES6的module功能的,这点算重复,ng2就是使用“typeScript”开发的。