2009-05-04

Firebug 如何提供console API到页面的window中

Firebug 如何提供console API到页面的window中?
How Firebug inject the console API to DOM window?
1. Use Components.utils.evalInSandbox and Components.utils.Sandbox

2. Register a listener to an Element in DOM window, which is created during injection phase.

3. Use attribute in precreated Element to specify the return value's type.


页面中执行的JavaScript与Firefox插件中执行的JavaScript代码是不能相互调用的。
因为各处于自己的解释器的工作范围中,运行时看不到对方定义的函数。

那么Firebug是如何把console API提供给页面的window的属性中的呢?
1. API如何绑定到页面的window中.
    如果是在页面中写代码,很容易在window属性中加入一个console对象,
    但是那样的话, 又不能调用或控制Firefox插件的控件了。

    Firebug通过在插件代码中调用 evalInSandbox方法, 传入用外部页面的window构造sandbox。
    在sandbox中执行一段注入(injection)代码, 使页面的window中有了console的定义。
           
            a. 注入代码的构造         \content\firebug\consoleInjector.js [getConsoleInjectionScript]
                var script = "";
                script += "window.__defineGetter__('console', function() {\n";                                                          //[1]
                script += " return window.loadFirebugConsole(); })\n\n";
               
                script += "window.loadFirebugConsole = function() {\n";                                                                   //[2]
                script += "window._firebug =  new _FirebugConsole();";                                                                   //[3]
                script += " return window._firebug };\n";
               
                var theFirebugConsoleScript = getResource("chrome://firebug/content/consoleInjected.js");            //[4]
                script += theFirebugConsoleScript;
            
            [1] 在此处定义了console 对象
             [2] 在此处定义了loadFirebugConsole方法,其中[3]实例化一个_FirebugConsole对象
             [4] 在此处加载_FirebugConsole的定义代码,稍后会说_FirebugConsole这段代码
             到此script中就构造好了准备注入的代码了。
          b. 构造sanbox
             // Use DOM Window 用外部的DOM window构造sandbox
            sandbox = new Components.utils.Sandbox(win);        
            sandbox.__proto__ = (win.wrappedJSObject?win.wrappedJSObject:win);

            c. 执行注入代码
            result = Components.utils.evalInSandbox(scriptToEval, sandbox);

到此页面的window有console的定义了,那console的方法(如log, debug, info, error)是如何把参数传递给Firebug插件的代码中的呢?

函数调用一般有三个过程
1. 参数准备             firebug 把要调用的函数的名字设置到预先定义的页面元素的属性中; 将参数设置为window的一个
userObjects属性。  
2. 函数执行             firebug 的chrome 会在预先定义页面元素上加一个linster, addEventListener, 进行chrome代码的调用。
3. 返回结果             firebug 把结果存回window的userObjects属性中, 并将返回值类型设置为预定的页面元素的一个属性中。



2. 函数的调用
   log, debug等函数的定义在_FirebugConsole这个类中, 代码位于 consoleInjected.js中.
        
          a. 建立一个页面元素来标示返回值的类型
           window._getFirebugConsoleElement = function()  // could this be done in extension code? but only after load....
  {
    var element = document.getElementById("_firebugConsole");                                              //[5]                  
    if (!element)
    {
        element = document.createElementNS("http://www.w3.org/1999/xhtml","html:div");
        element.setAttribute("id", "_firebugConsole");
        element.firebugIgnore = true;
         
        element.setAttribute("style", "display:none");

        document.documentElement.appendChild(element);
    }
    return element;
 };

         [5] 检测当前页面是否已经创建了用来传递数据及提供事件监听的页面元素
        

        b. 函数通过事件发出去,发送事件前的准备
            
    this.notifyFirebug = function(objs, methodName, eventID)
      {
        var element = this.getFirebugElement();                                     //获得元素

        var event = document.createEvent("Events");                             //创建事件
        event.initEvent(eventID, true, false);

        this.userObjects = [];
        for (var i=0; i<objs.length; i++)
            this.userObjects.push(objs[i]);                                              //准备参数

        var length = this.userObjects.length;
        element.setAttribute("methodName", methodName);                     //设置要调用的console对象的方法名
        element.dispatchEvent(event);                                               
        
        var result;
        if (element.getAttribute("retValueType") == "array")
            result = [];

        if (!result && this.userObjects.length == length+1)                       //如果只有一个元素新增,返回那个新增元素
            return this.userObjects[length];

        for (var i=length; i<this.userObjects.length && result; i++)
            result.push(this.userObjects[i]);                                          //将返回的结果数据向上层返回

        return result;
      };
 
         c. 调用事件监听
           在firebug注入代码到页面中后,进行了对调用事件的监听
   
         因为在sandbox中执行代码后, 页面中就会有一个叫_firebugConsole的页面元素,可以通过window.getElementById来获得。
                     
             
var handler = new FirebugConsoleHandler(context, win);  
           handler.attachTo(element);                                                       //调用内部执行addEventListener
          
        d. 响应调用事件
          handle: function(event, api, win)
  {
        var element = event.target;                                                 //获得事件对应的页面元素
        var methodName = element.getAttribute("methodName");          //获得想调用的方法名
        //获得调用的参数
        var hosed_userObjects = (win.wrappedJSObject?win.wrappedJSObject:win)._firebug.userObjects;

        var userObjects = hosed_userObjects ? cloneArray(hosed_userObjects) : [];
        var subHandler = api[methodName];                                     //找到相应的function对象
        if (!subHandler)
            return false;

        element.removeAttribute("retValueType");
        var result = subHandler.apply(api, userObjects);                    //用apply 来调用相应方法
        if (typeof result != "undefined")
        {
            if (result instanceof Array)
            {
                element.setAttribute("retValueType", "array");              //设置返回值类型
                for (var item in result)
                    hosed_userObjects.push(result[item]);
            }
            else
            {
                hosed_userObjects.push(result);
            }
        }

        return true;
  }

           到现在element.dispatchEvent(event);  后面的代码可以继续执行,并将返回值传给window中对console.log等方法。

再回顾一下:Firebug是如何把console API提供给页面的window的属性中的呢?
                答: 应用监听页面事件的方式来接收调用

注:文本中代码引自firebug 1.3.3 代码,有删减。


参考资料:
[1] JavaScript: The Definitive Guide, 5th
Edition ,
By David Flanagan
 注:作者写书时想把此书写成一本相当全面的JavaScript书,我认为,他做到了。
 作者书中对JavaScript的评价

Because JavaScript is interpreted instead of compiled, it is
often considered a scripting language instead of a true programming language.
The implication is that scripting languages are
simpler and that they are programming languages for nonprogrammers. The fact
that JavaScript is loosely typed does make it somewhat more forgiving for
unsophisticated programmers. And many web designers have been able to use
JavaScript for limited, cookbook-style programming tasks.


Beneath its thin veneer of simplicity, however, JavaScript is a
full-featured programming language, as complex as any and more complex than
some. Programmers who attempt to use JavaScript for nontrivial tasks often find
the process frustrating if they do not have a solid understanding of the
language. This book documents JavaScript comprehensively so that you can develop a sophisticated
understanding. If you are used to cookbook-style JavaScript tutorials, you may
be surprised at the depth and detail of the chapters ahead.

 [2] Firebug Console API, http://getfirebug.com/console.html
[3]https://developer.mozilla.org/en/Code_snippets/Interaction_between_privileged_and_non-privileged_pages