最近写一个小游戏的时候用的是RequireJs构建项目,顺便补了一下RequireJs,下面讲解一些基础和进阶的用法。

AMD

AMDAsync Module Definition代表的意思为异步模块定义,是Javascript模块化的浏览器解决方案,它采用异步的方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD规范定义了一个函数define,通过define方法定义模块:

 define(id?, dependencies?, factory);

并且采用require()语句加载模块:

require([module], callback);

引入的模块和回调函数不是同步的,所以浏览器不会因为引入的模块加载不成功而假死。

RequireJS

RequireJS是一个基于AMD规范实现的JavaScript文件和模块加载器。它针对浏览器内使用进行了优化,并且对其他JS环境(Rhino和Node)做了兼容。使用RequireJS这样的模块化脚本加载器可以提高代码的速度和质量。

  • 异步加载: 使用 RequireJS,会在相关的 js 加载后执行回调函数,这个过程是异步的,所以它不会阻塞页面。
  • 按需加载: 通过 RequireJS,你可以在需要加载 js 逻辑的时候再加载对应的 js 模块,不需要的模块就不加载,这样避免了在初始化网页的时候发生大量的请求和数据传输。

基本使用

根据官方文档和项目实例,接下来说一下ReuqireJS的基本使用:

Reuqire Download

下载最新版的RequireJS。

Project Structure

下面是官方示例的RequireJS项目结构,对内容做了小小的改动,www作为项目的根目录,lib中存放项目依赖即需要的一些JS库,app.js为主入口文件,app中存放自己写的模块文件。

Project Code

1. index.html

index.html中定义了一个script标签来引入require.js,其中data-main属性是一个自定义属性,这个属性指定在加载完 reuqire.js 后,就将属性指定路径下的JS文件并运行,这个文件即入口文件,这里的app.jsjs后缀被省略掉了。

<!DOCTYPE html><html>    <head>        <script data-main="app" src="lib/require.js"></script>    </head>    <body>        <h1>Hello World</h1>    </body></html>
如果 <script/>~~~~ 标签引入 require.js 时没有指定 data-main 属性,则以引入该 jshtml 文件所在的路径为根路径,如果有指定 data-main 属性,也就是有指定入口文件,则以入口文件所在的路径为根路径。

2. app.js

Main.js加载主模块并且配置项目依赖,要改变 RequireJS的默认配置,可以使用require.config函数传入一个可选参数对象。下面是一些可以使用的配置:

// For any third party dependencies, like jQuery, place them in the lib folder.// Configure loading modules from the lib directory,// except for 'app' ones, which are in a sibling// directory.requirejs.config({  // 模块加载的根路径。  baseUrl: ".",     // 用于一些常用库文件或者文件夹路径映射,js后缀可以省略  paths: {    app: "app/",    fmt: "lib/fmt",  },});// Start loading the main app file. Put all of// your application logic in there.requirejs(["app/main"]);
如果在 require.config() 中有配置 baseUrl,则以 baseUrl 的路径为根路径,这条规则会覆盖上面data-main的效果。

3. app/

Main.js中我们通过require函数加载了一个message模块,该模块用于打印一些定义好的字符串。

define(function (require) {  var msg = require("./message");  msg.helloWorld();});

Main.js中使用的模块定义在message.js中,他引入了一个输出依赖fmt

define(["fmt"], function (fmt) {  return {    helloWorld: function () {      fmt.println("hello word");    },  };});
这两种依赖的加载方式又和不同稍后介绍。

4. lib/

Lib/fmt.js中我定义一个 js 模块模拟gofmt包,通过return对外暴露出接口。注意,暴露的对象就是引入的对象。

define(function () {  var print = function (msg) {    console.log(msg);  };  var println = function (msg) {    console.log(msg + "\n");  };  return {    moduleName: "fmt",    print: print,    println: println,  };});

require.config 函数配置

要改变RequireJS的默认配置,可以使用require.config函数传入一个可选参数对象,其实这个对象可以配置五个属性:

require.config({    baseUrl: '.',      paths: {        app: "app/",        fmt: "lib/fmt",      },    shim: {        'backbone': {            deps: ['underscore', 'jquery'],            exports: 'Backbone'        },        'underscore': {            exports: '_'        }    },    config: {        'app/main': {            ENV: 'development'        }    },     map: {        'script/newmodule': {            'foo': 'foo1.2'        },        'script/oldmodule': {            'foo': 'foo1.0'        }    },});

1. baseUrl

baseUrl作为加载模块的根路径。在配置这个属性后,之后所有的路径定义都是基于这个根路径的(包括配置和依赖引入中)。

2. path

用于一些常用库文件或者文件夹路径映射,定义之后可以直接在依赖引入中使用。

3. shim

虽然目前已经有一部分流行的函数库符合 AMD 规范,但还有很多库并不符合。shim就是为了加载这些非AMD规范的js,并解决其载入顺序的,比如上面的backbone

4. config

config将配置信息传给一个模块。这些配置往往是application级别的信息,需要一个手段将它们向下传递给模块。

config: {  'app/main': {    ENV: 'development'  }}

可以通过加载特殊的依赖module来获取这些信息。

define(['module'], function (module) {  var ENV = module.config().ENV;  // development  var msg = require("./message");  msg.helloWorld();});

5. map

对于给定的模块前缀,使用一个不同的模块 ID 来加载该模块。该手段对于某些大型项目很重要。比如上面配置以后,不同的模块会使用不同版本的foo

some/newmodule调用了require('foo'),它将获取到foo1.2.js文件,当oldmodule调用 require('foo'),时它将获取到 foo1.0.js 文件。

map还支持*,意思是“对于所有的模块加载,使用本 map 配置”。如果还有更细化的 map 配置,会优先于*配置。

requirejs.config({    map: {        '*': {            'foo': 'foo1.2'        },        'some/oldmodule': {            'foo': 'foo1.0'        }    }});

模块定义

1. 对象

如果一个模块仅含键值对,没有任何依赖,可以直接在define中定义。

define({    foo: "foo",    bar: function(){}});

2. 需要预处理的对象

define(function () {    console.log("Do something...");     return {        foo: "foo",        bar: function(){}    }});

3. 只有一个函数

没有任何依赖的函数直接这么定义:

define(function () {    return function (){};});

调用时直接打()调用:

require(['lib/foo'],function (foo) {    foo();});

4. 需要其他的依赖:

define(['jquery'],function($){    return function (){};});

循环加载

假设我们有如下 a、b 两个互相依赖的模块,我们如果调用 b 模块的 b 方法。

// app/a.jsdefine(['app/b'],function(b){    return function() { b() }});// app/b.jsdefine(['app/a'],function(a){    return function() { a() }});

会发现 b 调用 a 正常,但是 a 中调用 b 方法会报 undefined 错误。

require(['app/b'],function (b) {    b();    // b is not defined.});

解决:

循环依赖比较罕见,对于循环依赖,只要依赖环中任何一条边是运行时依赖,这个环理论上就是活的。而如果全部边都是装载时依赖,这个环就是死的。

对模块 a 进行如下修改,即不再依赖前置加载。而是通过引入 require 依赖,然后再通过 require() 方法去载入模块 b,并在回调中去执行。

// app/a.jsdefine(['require'],function(require){    var b = require('b')    return function() {      b()    }});