前言

SeaJS由国内的牛人lifesinger开发。目前版本是1.1.1,源码不到1500行,压缩后才4k,质量极高。

SeaJS非常强大,SeaJS可以加载任意 JavaScript
模块和css模块样式,SeaJS会保证你在使用一个模块时,已经将所依赖的其他模块载入到脚本运行环境中。

这篇会讲述SeaJS的一些基本用法,不会面面俱到,但会就个人的理解讲述官方文档没有提到的一些细节。

通过参照上文的demo,我们结合源码分析在简单的API调用的背后,到底使用了什么技巧来实现各个模块的依赖加载以及模块API的导出。

 

模块类和状态类

一、SeaJS的全局接口

首先定义了一个Module类,对应与一个模块

 

function Module { this.uri = uri this.dependencies = deps || [] this.exports = null this.status = 0 // Who depends on me this._waitings = {} // The number of unloaded dependencies this._remain = 0}

SeaJS向全局公开了两个标识符:
seajs 和 define。

Module有一些属性,uri对应该模块的绝对url,在Module.define函数中会有介绍;dependencies为依赖模块数组;exports为导出的API;status为当前的状态码;_waitings对象为当前依赖该模块的其他模块哈希表,其中key为其他模块的url;_remain为计数器,记录还未加载的模块个数。

 

var STATUS = Module.STATUS = { // 1 - The `module.uri` is being fetched FETCHING: 1, // 2 - The meta data has been saved to cachedMods SAVED: 2, // 3 - The `module.dependencies` are being loaded LOADING: 3, // 4 - The module are ready to execute LOADED: 4, // 5 - The module is being executed EXECUTING: 5, // 6 - The `module.exports` is available EXECUTED: 6}

如果你的项目中已经用了标识符seajs,又不想改。这时SeaJS可以让出全局的seajs。如

上述为状态对象,记录模块的当前状态:模块初始化状态为0,当加载该模块时,为状态fetching;模块加载完毕并且缓存在cacheMods后,为状态saved;loading状态意味着正在加载该模块的其他依赖模块;loaded表示所有依赖模块加载完毕,执行该模块的回调函数,并设置依赖该模块的其他模块是否还有依赖模块未加载,若加载完毕执行回调函数;executing状态表示该模块正在执行;executed则是执行完毕,可以使用exports的API。

var boot = seajs.noConflict();  

模块的定义

这时boot就相当于先前的seajs。

commonJS规范规定用define函数来定义一个模块。define可以接受1,2,3个参数均可,不过对于Module/wrappings规范而言,module.declare那篇会陈诉SeaJS的局地为主用法,SeaJS能够加载放肆 JavaScript【www.qy186.com】。或者define函数只能接受一个参数,即工厂函数或者对象。不过原则上接受参数的个数并没有本质上的区别,只不过库在后台给额外添加模块名。

 

seajs鼓励使用define(function(require,exports,module){})这种模块定义方式,这是典型的Module/wrappings规范实现。但是在后台通过解析工厂函数的require方法来获取依赖模块并给模块设置id和url。

如果你的项目中连标识符define也用到了,也不想改。SeaJS是很宽容的,它的define也可以让出。如

// Define a moduleModule.define = function  { var argsLen = arguments.length // define if  { factory = id id = undefined } else if  { factory = deps // define if  { deps = id id = undefined } // define else { deps = undefined } } // Parse dependencies according to the module factory code // 如果deps为非数组,则序列化工厂函数获取入参。 if  && isFunction { deps = parseDependencies } var meta = { id: id, uri: Module.resolve, // 绝对url deps: deps, factory: factory } // Try to derive uri in IE6-9 for anonymous modules // 导出匿名模块的uri if (!meta.uri && doc.attachEvent) { var script = getCurrentScript { meta.uri = script.src } // NOTE: If the id-deriving methods above is failed, then falls back // to use onload event to get the uri } // Emit `define` event, used in nocache plugin, seajs node version etc emit meta.uri ? Module.save : // Save information for "saving" work in the script onload event anonymousMeta = meta}
var boot = seajs.noConflict(true);  

模块定义的最后,通过Module.save方法,将模块保存到cachedMods缓存体中。

较上面仅多传了一个true。这时全局的define也没了。这时需要用boot.define来代替之前的define。

parseDependencies方法比较巧妙的获取依赖模块。他通过函数的字符串表示,使用正则来获取require中的模块名。

 

var REQUIRE_RE = /"*"|'*'|/*[Ss]*?*/|/+/|//.*|.s*require|brequires*\1s*)/gvar SLASH_RE = /\\/gfunction parseDependencies { var ret = [] // 此处使用函数序列化进行字符串匹配,寻找require的关键字 code.replace .replace(REQUIRE_RE, function { ret.push return ret}

用过jQuery的同学应该很熟悉$.noConflict方法,SeaJS的noConflict与之类似。

异步加载模块

 

加载模块可以有多种方式,xhr方式可以同步加载,也可以异步加载,但是存在同源问题,因此难以在此使用。另外script tag方式在IE和现代浏览器下可以保证并行加载和顺序执行,script element方式也可以保证并行加载但不保证顺序执行,因此这两种方式都可以使用。

二、SeaJS的模块写法

在seajs中,是采用script element方式来并行加载js/css资源的,并针对旧版本的webkit浏览器加载css做了hack。

 

function request(url, callback, charset) { var isCSS = IS_CSS_RE.test var node = doc.createElement(isCSS ? "link" : "script") if  { var cs = isFunction ? charset : charset if  { node.charset = cs } } // 添加 onload 函数。 addOnload(node, callback, isCSS, url) if  { node.rel = "stylesheet" node.href = url } else { node.async = true node.src = url } // For some cache cases in IE 6-8, the script executes IMMEDIATELY after // the end of the insert execution, so use `currentlyAddingScript` to // hold current node, for deriving url in `define` call currentlyAddingScript = node // ref: #185 & http://dev.jquery.com/ticket/2709 baseElement ? head.insertBefore : head.appendChild currentlyAddingScript = null}function addOnload(node, callback, isCSS, url) { var supportOnload = "onload" in node // for Old WebKit and Old Firefox if (isCSS && (isOldWebKit || !supportOnload)) { setTimeout { pollCss // Begin after node insertion return } if  { node.onload = onload node.onerror = function() { emit("error", { uri: url, node: node }) onload() } } else { node.onreadystatechange = function() { if (/loaded|complete/.test { onload() } } } function onload() { // Ensure only run once and handle memory leak in IE node.onload = node.onerror = node.onreadystatechange = null // Remove the script to reduce memory leak if (!isCSS && !data.debug) { head.removeChild } // Dereference the node node = null callback() }}// 针对 旧webkit和不支持onload的CSS节点判断加载完毕的方法function pollCss { var sheet = node.sheet var isLoaded // for WebKit < 536 if  { if  { isLoaded = true } } // for Firefox < 9.0 else if  { try { if  { isLoaded = true } } catch  { // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR" // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0 // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR" if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") { isLoaded = true } } } setTimeout { if  { // Place callback here to give time for style rendering callback() } else { pollCss } }, 20)}

SeaJS默认使用全局的define函数写模块(可把define当成语法关键字),define定义了三个形参id,
deps, factory。

其中有些细节还需注意,当采用script element方法插入script节点时,尽量作为首个子节点插入到head中,这是由于一个难以发现的bug:

 

GLOBALEVAL WORKS INCORRECTLY IN IE6 IF THE CURRENT PAGE HAS TAG IN THE HEAD

define(id?, deps?, factory);

fetch模块

 

初始化Module对象时,状态为0,该对象对应的js文件并未加载,若要加载js文件,需要使用上节提到的request方法,但是也不可能仅仅加载该文件,还需要设置module对象的状态及其加载module依赖的其他模块。

这个define很容易让你想起AMD的唯一API:define函数。 或者说让人费解,导致搞不懂SeaJS和 RequireJS define的区别。

这些逻辑在fetch方法中得以体现:

 

// Fetch a module// 加载该模块,fetch函数中调用了seajs.request函数Module.prototype.fetch = function { var mod = this var uri = mod.uri mod.status = STATUS.FETCHING // Emit `fetch` event for plugins such as combo plugin var emitData = { uri: uri } emit var requestUri = emitData.requestUri || uri // Empty uri or a non-CMD module if (!requestUri || fetchedList[requestUri]) { mod.load() return } if (fetchingList[requestUri]) { callbackList[requestUri].push return } fetchingList[requestUri] = true callbackList[requestUri] = [mod] // Emit `request` event for plugins such as text plugin emit("request", emitData = { uri: uri, requestUri: requestUri, onRequest: onRequest, charset: data.charset }) if  { requestCache ? requestCache[emitData.requestUri] = sendRequest : sendRequest() } function sendRequest() { seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset) } // 回调函数 function onRequest() { delete fetchingList[requestUri] fetchedList[requestUri] = true // Save meta data of anonymous module if  { Module.save anonymousMeta = null } // Call callbacks var m, mods = callbackList[requestUri] delete callbackList[requestUri] while  m.load() }}

它们都有个全局的define,形参都是三个,且对应的形参名也一样,会误认为SeaJS也是AMD的实现。

其中seajs.request就是上节的request方法。onRequest作为回调函数,作用是加载该模块的其他依赖模块。

 

总结

事实上SeaJS和RequireJS的define前两个参数的确一样。

以上就是seajs模块的依赖加载及模块API的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注脚本之家。

 

id都为字符串,都遵循 Module
Identifiers。deps都是指依赖模块,类型都为数组。区别仅在于第三个参数factory,虽然类型也都是函数,但factory的参数意义却不同。

 

RequireJS中factory的参数有两种情况

a、和deps(数组)元素一一对应。即deps有几个,factory的实参就有几个。

define(['a', 'b'], function(a, b){
    // todo
});

  

b、固定为require,exports, module(modules/wrappings格式)。

define(function(require, exports, module){
    // todo
});

  

这种方式是RequireJS后期向 Modules/Wrappings 的妥协,即兼容了它。而SeaJS的define仅支持RequireJS的第二种写法:Modules/Wrappings。

注意:SeaJS遵循的是 Modules/Wrappings 和 Modules/1.1.1。这两个规范中都没有提到define关键字,Modules/Wrapping中要求定义模块使用module.declare而非define。而恰恰只有AMD规范中有define的定义。即虽然SeaJS不是AMD的实现,但它却采用了让人极容易误解的标识符define。

 

 

说了这么多,还没开始写一个模块。下面我们从最简单的开始

1、简单模块

define({
    addEvent: function(el, type, fn){},
    removeEvent: function(el, type, fn){},
    fireEvent: function(el, type){}
});

  

这样就写了一个事件模块,这和写一个单例没有区别。更多的时候用该方式定义纯数据模块。它类似于

var E = {
    addEvent: function(el, type, fn){},
    removeEvent: function(el, type, fn){},
    fireEvent: function(el, type){}
};

  

2、简单的包装模块

define(function() {
    // 一些内部辅助函数
    // ...
    function addEvent() {
        // ..
    }
    function removeEvent() {
        // ..
    }
    function fireEvent() {
        // ..
    }
    return {
        addEvent: addEvent,
        removeEvent: removeEvent,
        fireEvent: fireEvent
    };
});

  

您懂的,在这个匿名函数中可以做很多事情。最后只需公开必要的接口。它类似于

var E = function() {
    // 一些内部辅助函数
    // ...
    function addEvent() {
        // ..
    }
    function removeEvent() {
        // ..
    }
    function fireEvent() {
        // ..
    }
    return {
        addEvent: addEvent,
        removeEvent: removeEvent,
        fireEvent: fireEvent
    };
}();

  

3、NodeJS风格的包装模块

 

上面两种写法看不到一丝NodeJS风格(Modules/1.1.1),改写下与“方式2”等价的。

define(function(require, exports) {
    // 一些内部辅助函数
    // ...
    function addEvent() {
        // ..
    }
    function removeEvent() {
        // ..
    }
    function fireEvent() {
        // ..
    }
    // 使用exports导出模块接口,而非返回一个对象
    exports.addEvent = addEvent;
    exports.addEvent = removeEvent;
    exports.addEvent = fireEvent;
});

  

可以看到与“方式2”区别在于:

1:匿名函数有两个参数require、exports。

2:导出接口不是return一个对象而是使用exports。

而exports不正是NodeJS的风格吗?
细心的同学可能发现这个示例中require参数没有用到,这正是下面要讲的。

 

4、有依赖的模块

 

SeaJS中“依赖”都需要使用require函数去获取,虽然SeaJS的define的第二个参数deps也有“依赖”的意思,但它是提供打包工具(SPM)用的。此外,SeaJS的require是作为参数传入匿名函数内的,RequireJS的require则是全局变量。

 

上面定义的是一个没有依赖的模块,以下是有依赖的模块。

define(function(require, exports) {
    var cache = require('cache');

    // ...

    exports.bind = bind;
    exports.unbind = unbind;
    exports.trigger = trigger;
});

  

该事件模块依赖于cache模块,函数有两个形参require和exports。抛开外层的匿名函数及define,它就是标准的NodeJS格式:使用require函数取依赖模块,使用exports导出现有模块接口。

实际上在SeaJS中具有依赖的模块必须按“方式4”写,即必须是包装模块,且匿名函数的第一个参数必须是标识符 “require”。即可以把require当初语法关键字来使用,虽然它不是全局的。

 

下面我们看看匿名函数的参数require和exports的一些有趣现象

a、如果写的不是require,改成req会是什么结果。

define(function(req, exports) {
    var cache = req('cache');

    // ...

    exports.bind = bind;
    exports.unbind = unbind;
    exports.trigger = trigger;
});

 

Firebug网络请求如下

www.qy186.com 1

会看到依赖的“cache”没有被加载,当然JS肯定会报错了。

 

b、只把匿名函数的形参改成req,函数内部仍然使用require。

define(function(req, exports) {
    var cache = require('cache');

    // ...

    exports.bind = bind;
    exports.unbind = unbind;
    exports.trigger = trigger;
});

  

看网络请求

www.qy186.com 2

这次“cache”模块竟然请求下来了。

 

仔细看上面的匿名函数代码中require没声明,且形参是req而非require。那

var cache = require('cache');

中的require从何而来?

 

看SeaJS源码可知,它的define函数中会取该匿名函数的toString,使用正则匹配解析出其中的“cache”(私有的parseDependencies函数)。

 

我们也看到,虽然cache请求下来了,却仍然报错,因为在执行阶段require是未定义的。因此写依赖模块时匿名函数的第一个参数必须为require且不能更改。

 

正因为使用factory.toString和正则解析依赖,因此require的参数不能是表达式,如

// require的参数不能是表达式运算
require("ui-" + "dialog"); 

 

也不能使用require的别名,如

// 不能将require赋值给另外一个变量
var req = require;
req("ui-dialog");

 

另据说Opera移动版不支持function的toString,这样SeaJS无法在该版本中使用了(未测)。

 

c、修改exports为expo

define(function(require, expo) {
    var cache = require('cache');

    // ...

    expo.bind = bind;
    expo.unbind = unbind;
    expo.trigger = trigger;
});

  

运行是没有问题的。即第二个参数“exports”是可以自定义的。显然SeaJS不赞成改“exports”为其它,这样明显破坏了NodeJS风格(Modules/1.1.1)的模块规范—它们正是使用“exports”导出模块接口。但这点在SeaJS中却无法被强制执行,只能是人为约定。

 

5、混合写法的模块

 

上面已经介绍了各种情形中的模块写法。为了与NodeJS风格保持一致:使用require获取“依赖”,使用exports导出“接口”。SeaJS在获取依赖这一块做了限制,即必须使用require。但导出则不一定非得使用exports,即exports可以改为其它。甚至还可以直接使用
“返回值”。

define(function(require) {
    var cache = require('cache');

    // ...

    // 使用返回值导出接口
    return {
        bind: function() {},
        unbind: function() {},
        fire: function() {}
    };
});

  

我们知道在NodeJS中模块只能是一个对象。即总是往exports上挂方法。SeaJS中如果使用exports导出接口,那么也一样,模块也只能是JS对象。如果使用“返回值”导出接口的话,那么模块可以是任意的JS类型。如下将返回一个函数类型的模块。

define(function(require) {
    var cache = require('cache');

    function ad() {
        //...
    }

    // 函数类型的模块
    return ad;
});

  

三、SeaJS的加载方式

 

虽然它提供各种方式(同步异步)加载,最简单的莫过于直接在页面中写script标签。引入SeaJS后,入门多数时候就是seajs.use方法。

seajs.use有两个参数,第一个参数可以为字符串(模块名)或数组(多个模块)。第二个参数是回调函数。模块加载后的回调。回调函数的参数与第一个参数一一对应。

seajs.use('dom', function(dom) {
    // todo with dom
});

  

如下将在回调函数中使用dom模块。当然它也提供了快捷方式data-main(同RequireJS)。

 

 

下面的demo中写了一个完整的事件模块event,它依赖于cache模块。

seajs-demo.zip

相关文章