初识 SeaJS

最近NodeJSCommonJS真是好火啊,前端真的不再如从前那么“单纯”了,从此需要学习掌握更多的传统软件开发的知识和技能,也就是从前后端开发做的很多工作会逐渐前移到前端工程师。这里为什么要把NodeJSCommonJS放在一起说呢,主要是我对里面的模块机制十分感兴趣,正好NodeJS中的模块实现方式也遵循CommonJS中的相关规范。

神奇的 require

首先还是看一段代码吧:

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(1337, "127.0.0.1");
console.log('Server running at http://127.0.0.1:1337/');

这是NodeJS首页上的一段代码,我们注意看一下require这个函数的用法,这里的代码需要http这个模块,因此就var http = require('http');但是令我比较疑惑的就是明明require函数是个异步的调用,因为http的模块文件不可能同步在这里堵塞加载执行(当然可能有人会说使用同步的AJAX请求,但是我们同样需要支持跨域问题),虽然这段代码是NodeJS的代码,其主要是运行在服务器端,使用V8引擎驱动,可以使用一些手段让requrie的模块同步加载执行,但是对于一个Web应用看到这样的代码一定会疑惑为什么这里不需要一个回调函数~好吧,就此认为这就是NodeJS在服务器端的神奇之处~~~那么继续看下面一段代码:

define(function(require, exports, module) {

    // 获取依赖的模块:
    var $ = require('jquery');

    // 向外提供接口:
    exports.someMethod = someFunction;

});

这段代码是来自SeaJS首页的一段代码(额,我们的主角终于现身了~)这里再次出现了var $ = require('jquery');这样一段 require 函数,这里同样会有刚刚上述的问题,明明是个异步的需求怎么在这里可以使用同步的写法呢?一种假设是 require 函数在这里已经事先准备好了 jquery 的对象(加载和执行 jquery.js ),但是这有什么策略么?不可能所有支持的模块都必须这样提前加载,况且以后根本无法方便扩展,添加自己的模块啊;第二种假设就是 jquery 对象真是在这里第一次出现,然后下载文件,执行文件的~但是据我所知,只能使用同步的 AJAX 才能做到这样,但是跨域的问题是最致命的问题,这里不可能没有处理啊??!!纠结啊,甚是纠结啊~一个require竟然有如此神奇的功力~

拨开 require 的神秘外衣

介于NodeJS的代码性质以及难度,这里暂时没有去研究其 require 的实现方法,限制着重谈谈SeaJS。关于SeaJS是什么,建议首先访问其网站,大致了解一下,在其中自然也提到了不少CommonJS的规范问题。

为了更好地探究这个神奇的require函数,我首先从github上拉到了最新的SeaJS源代码。大致看下 seajs 代码执行后会产生些什么,如下图:

SeaJS Overview

是的,没看错,seajs 这个对象暴露出来的接口就这么简单几个,但是不能仅仅因为暴露的接口简单精巧就忽视了其丰富的内涵。我们打开其源代码,大致看一下整个项目结构,这里主要把目光集中在 src 目录下:

SeaJS Source Structure

文件命名主要是以 sea 开头的总引导文件以及 api 文件,fn 开头的辅助函数文件,util 开头的工具文件。通过 build.xml 可以了解整个SeaJS发布文件的构成方式。打开 sea.js 文件,可以看到刚刚我提到的辅助函数、工具在项目整个解构中都是以私有方式保存在暴露的 seajs 对象中,但是为什么实际看不到这些私有属性呢,愿意可以在 sea-api.js 中发现,具体看代码吧,这里不细说了~总的来说,整个 seajs 充分运用闭包的优势,把私有变量均保存在闭包中方便使用,在最终的 api 接口中删除暴露的私有属性接口,但是由于闭包特性,其内部值仍得以保留,这种方式个人感觉十分新颖,尤其实在做这样一个多文件项目时,在最终的 api 发布阶段再清除不必要的私有接口,既保证了开发时的灵活方便,又保证了数据的安全性。

好像越说离题越远,那现在回到神秘的 require 函数,那么这个接口是暴露的哪个内部辅助函数呢?看名字以为是在 fn-require.js 中,实则不然,我们再把眼光看远一些,原来 require 函数的最外层是一个 define 函数,那么打开 fn-define.js,代码看得云里来雾里去的,但是明确一下现在的目标时找到 require 相关的实现,但是首先,再岔开一下话题,看下 CommonJS 里面的这段规范说明 Modules/Wrappings,哈哈,说到的问题是不是正中下怀啊,正是我们现在关注的方向,继续看代码,注意到这样一个函数:

deps = parseDependencies(factory.toString());

/* ------------------------------------------ */

function parseDependencies(code) {
    var pattern = /\brequire\s*\(\s*['"]?([^'")]*)/g;
    var ret = [], match;

    while ((match = pattern.exec(code))) {
        if (match[1]) {
          ret.push(match[1]);
        }
    }

    return ret;
}

哈,原来如此!define 函数中的倚赖关系(实际就是 require 函数的作用),是通过硬解析函数代码,观察其中 require 使用情况来决定的啊~虽然没有想象中的那么神奇,但是也是一种解决方案嘛,说到这里看看上马的假设,更符合假设一的情况,只是这里自然用不着全部加载其他模块,通过代码的“硬解析”来完成依赖需求,在 define 阶段即可有机会首先异步加载 require 的模块,然后当真正 require 时自然也就是同步方式了,这招还真是挺绝妙的啊~

好了,这次已经大致了解了SeaJS的一些基本情况了,也终于解惑了一个一直困扰我的问题,至于源代码的进一步分析和解读就等下次吧~

5 Comments For This Post

  1. Cool~ 你也用IDEA ?

    回复该留言

    @Sam, 其实是WebStorm,不过也就当做一样的吧~看项目,还是 IDE 给力~

  2. 最终暴露的接口里,应该没有 _seajs. 我修复了下^o^
    http://seajs.com/docs/guide/hello-seajs.html

    感谢龚浩给力的分析 :biggrin:

    回复该留言

  3. 我也是今年毕业的,也做前端的,在百度实习的时候认识了好几个你们梦飞无线的成员呦

    回复该留言

  4. 你好,我最近也在学习seajs,但是js基础太差,很多地方都看不懂,能不能教我如何调用jquery的一些插件(轮播,图片滚动之类的),跪谢~~

    回复该留言

Leave a Reply

:wink: :twisted: :surprised: :smile: :sad: :rolleyes: :redface: :razz: :question: :neutral: :mrgreen: :mad: :lol: :idea: :exclaim: :evil: :eek: :cry: :cool: :confused: :biggrin: :arrow: