在我最初开始写 JavaScript 函数时,通常是这样的:
function fun1() { // some code here } function fun2() { // some other code here } ...
函数全写在全局环境中,项目很小时,通常不会有什么冲突问题。
但代码多了后,渐渐就发现,函数名称(英文词汇)有点不够用了。于是引入命名空间的概念,开始模块化代码。
命名空间下的函数
在命名空间下,我的代码这样写:
var com = com || {}; com.zfanw = com.zfanw || {}; com.zfanw.module1 = (function() { // some code here return { func1: func1, ... }; }()); com.zfanw.module2 = (function() { // some other code here return { func1: func1, ... }; }()); ...
本着要面向对象的原则,执行函数通常我要这么写的:
com.zfanw.module1.func1.apply({},['arg1',arg2]); ...
当然,为了少打些字符,我还会在闭包中导入1公共 API 接口:
(function($, mod1) { // some code here mod1.func1.apply({},['arg1',arg2]); }(jQuery, com.zfanw.module1)); ...
至此,代码冲突的可能性已经很小,但代码依赖的问题,多脚本文件管理、阻塞的问题,渐渐浮出水面 – 命名空间的办法开始捉急。
于是 Require.js2 出场。
Require.js
首先了解下 require.js 里模块的概念3:
A module is different from a traditional script file in that it defines a well-scoped object that avoids polluting the global namespace. It can explicitly list its dependencies and get a handle on those dependencies without needing to refer to global objects, but instead receive the dependencies as arguments to the function that defines the module.
简单地说,有两点,一、模块作用域自成一体,不污染全局空间;二、模块指明依赖关系,并且依赖是通过参数传递的形式导入的,无需通过全局对象引用 – 依赖同样不污染全局空间。
定义模块
与上面的老长的命名空间方式不同,require.js 用全局方法 define 来定义模块,形式如下:
define(id?, dependencies?, factory); // ? 表示可选项
我且把模块分两种。
无依赖的模块
假如一个模块并不依赖其他模块,那么定义起来是很简单的,比如模块 hello 放在 hello.js 文件中:
define(function() { // some code here return { // some public api }; });
有依赖的模块
有依赖的模块要稍稍复杂点,define 时,我们需要罗列出模块的依赖情况:
define(['jquery'], function($) { // 比如这个模块,代码的执行依赖 jQuery,require.js 会先加载 jquery 模块代码,并加以执行,然后将依赖模块 以 $ 的参数形式传入回调函数中,回调函数将执行结果注册为模块 // maybe some code here return { // some public api }; });
这里,依赖中的 'jquery' 是模块相对于 baseUrl 的路径,等效于模块 ID。
现在,再回过头,看看上面写过的闭包中导入公共 API 的代码,跟 define 函数做个对比:
(function($, mod1) { // some code here mod1.func1.apply({},['arg1',arg2]); }(jQuery, com.zfanw.module1));
这段代码里,我同样把 jQuery 导入了,在闭包里,我同样是通过 $ 这个外部传入的参数来访问 jQuery。可以说,它「定义依赖」的方式跟 define 方法很相似,不同的是,define 导入的 jquery 不是全局变量,所以不会污染全局环境。
关于模块名称
define 函数有三个参数,第一个 id 即模块名称,这个名称的格式是相对于 baseUrl 的路径除去文件格式,比如 baseUrl 为 js 目录,一个模块放在 js/libs/hi.js 里,则如果名称是这样定义的:
define('libs/hi', ['jquery'], function($){......});
这样的定义形式的好处是,模块不可能冲突,因为同一目录下不允许同名文件。但也因此 require.js 建议我们不要设置模块名称,因为设置了 ‘libs/hi' 的模块名称后,模块就必须放在 js/libs 目录下的 hi.js 文件中,要移动位置的话,模块名称要跟着改变。至于后期利用 r.js 优化时生成了模块名称,那已经是另外一回事。
使用模块
在定义好「有依赖」、「没依赖」的各种模块后,我们该怎么用它?Require.js 提供了一个函数,require(与 requirejs 等效)。
require 函数加载依赖并执行回调,与 define 不同的是,它不会把回调结果4注册成模块:
require(['jquery'], function($) { // 这个函数加载 jquery 依赖,然后执行回调代码 console.log($); });
举一个简单的例子。我有一个文件夹,文件结构如下:
index.html js/ main.js require.js jquery.js
这里 jquery.js 已经注册为 AMD 模块,则 HTML 文件里这样引用 require.js:
<script src="https://www.jb51.net/js/require.js" data-main="js/main"></script>
require.js 会检查 data-main 属性值,这里是 js/main,根据设定,它会加载 js 目录下的 main.js 文件。
main.js 文件里,我只干一件事,用 jQuery 方法取得当前窗口的宽度:
require(['jquery'], function($) { var w = $(window).width(); console.log(w); });
执行代码就这么简单。
非 AMD 规范的模块