フォーラムをあちこち探したのですが、なかったので作ってみました

・03/09[追記] サンプルがIEで動かなかったのを修正しました

Ext.ux.JsonpTreeLoader
Code:
Ext.ux.JsonpTreeLoader = Ext.extend(Ext.tree.TreeLoader, function(){

  return {

    timeout : 30000,

    callbackParam : "callback",

    requestData : function(node, callback){
      if(this.fireEvent("beforeload", this, node, callback) !== false){
  			this.scriptRequest(node,callback);
      }else{
        if(typeof callback == "function"){
          callback();
        }
      }
    },

    // private
    // retrieves data from cross-domain servers by dynamically creating a script tag
    // referred to the "Cross-domain Ext.Ajax/Ext.data.Connection" by Animal
    // url: http://extjs.com/forum/showthread.php?t=1769
    scriptRequest : function(node, cb) {
      var transId = ++Ext.data.ScriptTagProxy.TRANS_ID;
      var trans = {
        id : transId,
        cb : "stcCallback"+transId,
        scriptId : "stcScript"+transId,
				argument: {node: node, callback: cb}
      };

			var url = this.dataUrl || this.url;
      url += (url.indexOf("?") != -1 ? "&" : "?") + this.getParams(node) + String.format("&{0}={1}", this.callbackParam, trans.cb);

      var conn = this;
      window[trans.cb] = function(o){
        conn.handleResponse({responseObject: o },trans);
      };

      trans.timeoutId = this.handleScriptFailure.defer(this.timeout, this, [trans]);

      var script = document.createElement("script");
      script.setAttribute("src", url);
      script.setAttribute("type", "text/javascript");
      script.setAttribute("id", trans.scriptId);
      document.getElementsByTagName("head")[0].appendChild(script);

      return trans;
    },

    // private
    // called inside requestData as a callback with response data from remote server
    handleResponse : function(response,trans){
      var a = trans.argument;
      this.processResponse(response, a.node, a.callback, trans);
      this.fireEvent("load", this, a.node, response);
    },

    // private
    // tries to create tree nodes from retrieved data
    // referred to the "Cross-domain Ext.Ajax/Ext.data.Connection" by Animal and Ext.data.JsonReader
    // url: http://extjs.com/forum/showthread.php?t=1769
    processResponse : function(response, node, callback, trans){
  		var o = response.responseObject;

      this.transId = false;
      this.destroyScriptTrans(trans, true);

      if(this.totalProperty) {
        this.getTotal = this.getJsonAccessor(this.totalProperty);
      }

	    this.getRoot = this.root ? this.getJsonAccessor(this.root) : function(p){return p;};
    	var root = this.getRoot(o), c = root.length, totalRecords = c;

    	if(this.totalProperty){
        var v = parseInt(this.getTotal(o), 10);
        if(!isNaN(v)){
          totalRecords = v;
        }
      }

			node.attributes.totalRecords = totalRecords;

      try {
				var rootName = this.root.split('.').pop();

        node.beginUpdate();
        node.appendChild(this.parseJson(root,rootName));
        node.endUpdate();

        if(typeof callback == "function"){
          callback(this, node);
        }
      }catch(e){
        this.handleFailure(trans);
      }
    },

    isNodeProperty: function(property){
      return this.nodeProperty 
         ? this.nodeProperty.indexOf(property) !== -1 
         : true; 
    },

    // private
    // parses and creates tree node from retrieved JSON data
		parseJson : function(o, name){
			if(typeof o !== 'object') return;
      else if(name && !this.isNodeProperty(name)) return;
      else if(o instanceof Array) return this.parseArray(o, name);
    
			var nodes = [];
			for(var i in o){
				if(o.hasOwnProperty(i) && this.isNodeProperty(i)){
					if(typeof o[i] == 'object'){
						var treeNode = this.createNode(o[i],i);
						var child = null;
						child = this.parseJson(o[i],i);

						if(child && child.length >= 1){
							treeNode.appendChild(child);
						}

						nodes.push(treeNode);
					}
				}
			}
			return nodes;
		},

    // private
    // parses array and creates tree nodes
		// Assumption: Direct member of array must not be an array
		// Assumption: Array members must be objects, not primitives
    parseArray : function(o, name){
			if(!(o instanceof Array)) return;
			else if(name && !this.isNodeProperty(name)) return;

      var nodes = [];

      Ext.each(o, function(n){
				if(typeof n == 'object'){
				  var treeNode = this.createNode(n,name);
					var child = this.parseJson(n,name);
					if(child && child.length >= 1){
						treeNode.appendChild(child);
					}
					nodes.push(treeNode);
				}
			},this);

			return nodes;
		},

    createNode : function(node,name){
      var attr = {
        propName: name 
      };
		        
			for(var i in node){
				if(node.hasOwnProperty(i) && (typeof node[i] !== 'object' || i == 'nodeLoader')){
				  attr[i] = node[i];	
				}
			}
			        
			this.processAttributes(attr, node);
			        
			var n = Ext.ux.JsonpTreeLoader.superclass.createNode.call(this, attr);

      // set loader after superclass call (because n.loader is set to "this" inside superclass)
			if(attr.nodeLoader){
				n.loader = attr.nodeLoader;
			}

			return n;
		},

    // private
    // used inside createNode method to process the attributes of the node
		processAttributes: function(attr, n){
			attr.text = attr.text || attr.propName;
			if(this.mappingFn && this.mappingFn[attr.propName]){
				this.mappingFn[attr.propName](attr, n);	
			}else{
				attr.loaded = typeof attr.loaded !== 'undefined' ? attr.loaded : true;
			}
		},

    // private
    // copied from Ext.data.JsonReader
    getJsonAccessor: function(){
      var re = /[\[\.]/;
      return function(expr) {
        try {
          return(re.test(expr))
            ? new Function("obj", "return obj." + expr)
            : function(obj){
              return obj[expr];
            };
        } catch(e){}
        return Ext.emptyFn;
      };
    }(),

    // private
    // fires "loadexception" event
    // referred to the "Cross-domain Ext.Ajax/Ext.data.Connection" by Animal
    // url: http://extjs.com/forum/showthread.php?t=1769
    handleScriptFailure: function(trans) {
      this.transId = false;
      this.destroyScriptTrans(trans, false);
      var response = {
        argument:  trans.argument,
        status: 500,
        statusText: 'Server failed to respond',
        responseText: ''
      };

      var node = trans.argument.node;

      this.fireEvent("loadexception", this, node, response);
    },
    
    // private
    // remove scrip tag which was used to retrieve data
    // referred to the "Cross-domain Ext.Ajax/Ext.data.Connection" by Animal
    // url: http://extjs.com/forum/showthread.php?t=1769
    destroyScriptTrans : function(trans, isLoaded){
      document.getElementsByTagName("head")[0].removeChild(document.getElementById(trans.scriptId));
      clearTimeout(trans.timeoutId);
      if(isLoaded){
        window[trans.cb] = undefined;
        try{
          delete window[trans.cb];
        }catch(e){}
      }else{
        // if hasn't been loaded, wait for load to remove it to prevent script error
        window[trans.cb] = function(){
          window[trans.cb] = undefined;
          try{
            delete window[trans.cb];
          }catch(e){}
        };
      }
    }

   }; // end return
  }()
);
その名の通り、JSONPで提供されているWeb APIからデータを取得してきてExt.data.TreeNode形式にしてくれるクラスです(サンプルではPanoramioのAPIを使っています)。

SDKのサンプルに含まれているExt.ux.XmlTreeLoaderと、このスレッド(英語)、そしてExt.data.ScriptTagProxyとExt.data.JsonReaderからいくつかメソッドを参考&コピペして作ってみました。

もともとのExt.tree.TreeLoaderは、
  • 同一ドメインからのデータ取得のみ
  • 決まった形のJSONデータのみ

と、ちょっと使い勝手がよくなかったので、クロスドメイン対応と、あとExt.data.JsonReaderのようなマッピング機能も追加してみました(このマッピング機能は、上記XmlTreeLoaderや既存のTreeLoaderにも簡単にポーティング可能です)。

バグ報告、突っ込み、大歓迎ですので、お試しください