如何编写 Cloud9 JavaScript IDE 的功能扩展

上周末我们在JSConf.eu发布了 Cloud9 IDE ,同时发布了对应的GitHub项目。在4天时间里该项目得到340个人的关注和将近50个fork。Cloud9的口号是由"由Javascripters 为Javascripters创建的IED",这口号有点递归,它意味着你可以hack这个ide使它变得更强大。Cloud9项目开始之初就尤其注意考虑这点了;Cloud9中的每一个功能点都是一个扩展(extension)。在IED启动的时候我们用优秀的 requireJS 库加载所有的扩展。前端UI使用 ajax.org platform (apf),apf 使我们轻松地模块化Cloud9的用户界面。下面开始详细介绍怎样为Cloud9编写扩展。

一个扩展的生命周期是从它作为requireJS的模块开始的。我将简述requireJS的基本语法,想深入了解requireJS请参考这个文档。一个扩展会依赖其他的扩展和一些核心模块。我们将编写一个给编辑器中选定的JSON代码进行格式化的扩展。该扩展依赖核心模块:core/ide, core/ext, core/util 和编辑器管理扩展:ext/editors/editors.让我们称该扩展为formatjson,然后将其置于ext文件夹下。

require.def("ext/formatjson/formatjson",
    ["core/ide", 
     "core/ext", 
     "core/util", 
     "ext/editors/editors", 
     "text!ext/formatjson/formatjson.xml"],
    function(ide, ext, util, editors, markup) {

return ext.register("ext/formatjson/formatjson", {
        //Object definition
});

    }
);
require.def  第一个参数标识扩展的名字,第二参数中 ide,ext ,util和 editors 代表传入该扩展依赖的对象引用,formatjson扩展的第五个依赖是加载为一个文本的xml文件。 ‘text!’ 语法告诉 requireJS 不要将参数引入的文件解析为 javascript,仅将其中的内容作为文本返回即可。所有依赖加载完毕后将调用第三个参数代表的回调函数,在回调函数中将我们的扩展注册到扩展管理器中,让我们看看扩展文件的结构。
{
    name   : "JSON Formatter",
    dev    : "Your Name Here",
    alone  : true,
    type   : ext.GENERAL,
    markup : markup,

    hook    : function(){},
    init    : function(){},
    enable  : function(){},
    disable : function(){},
    destroy : function(){}
}

属性和方法详解:

属性

属性名 是否必须 描述
name 必须 扩展的名字,供管理器中显示
dev 可选 开发者名字,供扩展管理器中显示,主要是表彰开发者的荣誉
alone Boolean值,标识该扩展是个独立的扩展还是某个扩展的子扩展
type 扩展类型,现在只支持 ext.GENERAL和ext.EDITOR,这个属性极有可能在未来版本中弃用
markup String,该扩展的UI定义的标记文本
visible Boolean值标识该扩展在加载时是否可见,该属性仅对 Panel 扩展有效

方法

方法名 必须 描述
hook 可选 在扩展注册时调用该方法,允许你延迟该扩展的初始化。 例如你可以添加一个菜单项来初始化该扩展。 初始化的时候, markup 参数的值被解析然后调用 init方法。如果没有定义hook方法,则扩展注册时会立即初始化。当你指定hook后,就要自己全权负责扩展的初始化。扩展的初始化是由调用ext.initExtension(_self);完成的,其中 _self 是.Panel 扩展一个个引用。对于panel hook函数 通常只有一单单的一个声明:panels.register(this);
init 必须 初始化时解析完UI markup标记字符串后 调用该函数。使所有markup中引入的该扩展的元素可用,并可以对应到其正确的位置。在扩展管理器中启动该扩展时也会调用该函数。

对editor 型扩展,第一个参数是tab page元素,指示该扩展可以用之填充到自己的UI.Panel 对panel ,第一个参数应该给this.panel传一个在Cloud9 UI的panel元素 (通常是window元素)。

enable 必须 前端启用该扩展时调用。这个函数是通过在前端菜单中点击某个panel扩展时被立即调用的(例如点击完某个菜单项后显现勾勾的这个动作)。不要与在扩展管理器中的启用和禁用扩展混淆,启扩展调用的是 destroy和init方法
disable 必须  前端停用扩展时调用。这个函数是通过在前端菜单中点击隐藏panel扩展时被立即调用的(例如点击完某个菜单项后勾勾不显示的这个动作)。不要与在扩展管理器中的启用和禁用扩展混淆,启扩展调用的是 destroy和init方法
destroy 必须 注销扩展时调用。注销时清除引入的UI元素,事件处理器,和其他状态等。在扩展管理器中禁用扩展时调用。

实现 Format JSON扩展

好,现在我们已经有了基本概念,让我们开始真正来实现 format json扩展。首先完成我们需要属性和方法。我将添加一nodes数组,其中包含该扩展所需的所有UI元素。我们用hook方法来创建一个菜单来初始化formatjson扩展,并显示一个格式化窗口接受用户输入的缩进值。代码如下:

{
    name   : "JSON Formatter",
    dev    : "Ajax.org",
    alone  : true,
    type   : ext.GENERAL,
    markup : markup,

    nodes : [],

    hook : function(){
        var _self = this;
        this.nodes.push(
            mnuEdit.appendChild(new apf.item({
                caption : "Format JSON",
                onclick : function(){
                    ext.initExtension(_self);
                    _self.winFormat.show();
                }
            }))
        );
    },

    init : function(amlNode){
        this.winFormat = winFormat;
    },

    enable : function(){
        this.nodes.each(function(item){
            item.enable();
        });
    },

    disable : function(){
        this.nodes.each(function(item){
            item.disable();
        });
    },

    destroy : function(){
        this.nodes.each(function(item){
            item.destroy(true, true);
        });
        this.nodes = [];
        this.winFormat.destroy(true, true);
    }
}

在hook方法中创建一个菜单依附到mnuEdit。mnuEdit是对编辑器菜单的全局引用。现在我们的UI元素的名字挂靠在全局命名空间下(可能会在将来的版本中变更)。Cloud9中可用的UI元素表如下,并指定了哪些扩展添加了这个元素。

Name Extension Purpose
mnuFile
顶部菜单栏的 File 菜单
mnuEdit
顶部菜单栏的 Edit 菜单
mnuView
顶部菜单栏的 View菜单
mnuEditors
view菜单的 editors
mnuModes
Window菜单的布局菜单
mnuPanels ext/panels/panels 顶部菜单栏的windows菜单
vbMain
布局的主vbox
tbMain
主菜单栏
barMenu
菜单
barTools
主菜单栏的第一栏
sbMain
底部的状态栏
mnuFile

mnuFile

winDbgConsole ext/console/console 控制台面板
tabConsole ext/console/console 控制台窗口的tab元素
winFilesViewer ext/tree/tree 树面板
trFiles ext/tree/tree 树面板中的树元素
还有更多建好的元素。可以在各自的扩展或通过DOM/XPath操作找到他们。例如在工具栏和状态栏之间有一个hbox包含3个vbox元素。
<a:hbox>
    <a:vbox />
    <a:vbox />
    <a:vbox />
</a:hbox>
可以用XPath选择器来访问元素:

vbMain.selectSingleNode("a:hbox/a:vbox[2]");

这条查询将定位到hbox中的第二个vbox。这个vbox含有了打开的文件tab和控制台面板。然后你可以像我们在formatjson扩展中对菜单的处理方法一样将你想要的元素添加到该vbox。

MarkupUI 标记

然后format json 扩展会弹出个窗口给用户来设置缩进的空格数。我们用aml标记语法来创建这个窗口。我将aml代码放到名为formatjson.xml的xml文件中,并在最外层添加了一个扩展所需的根元素:a:application,看起来像这样:

<a:application xmlns:a="http://ajax.org/2005/aml">
    <!-- Your UI markup here -->
</a:application>
UI标记可以包含html和 AML元素。我们使用AML的一个下拉列表spinner和两个按钮来描述对json格式化的窗口。

<a:window
  id        = "winFormat"
  title     = "Format JSON"
  center    = "true"
  modal     = "false"
  buttons   = "close"
  kbclose   = "true"
  width     = "200">
    <a:vbox>
        <a:hbox padding="5" edge="10">
            <a:label width="100">Indentation</a:label>
            <a:spinner id="spIndent" flex="1" min="1" max="20" />
        </a:hbox>
        <a:divider />
        <a:hbox pack="end" padding="5" edge="10 10 5 10">
            <a:button default="2" caption="Format" 
              onclick = "
                require('ext/formatjson/formatjson').format(spIndent.value);
              "/>
            <a:button onclick="winFormat.hide()">Done</a:button>
        </a:hbox>
    </a:vbox>
</a:window>

格式化按钮绑定了onclick事件来调用我们扩展的format方法,它传入了spinner的值。这就是我们在扩展中需要实现的方法,让我们动手吧。

自定义函数

格式化函数有一个参数,来指定json中缩进的空格数。首先获取当前选择的代码,如果选中的代码为有效的json,则对其格式化,更新到当前选中的代码,否则给用户一个错误提示。

我们需要加载另一个依赖来完成该功能,就是ace编辑器的Range模块。于是我在顶部将ace/Range添加到依赖列表中,然后调用参数"Range"。格式化函数看起来如下(我给每个部分添加了注解)。

{
    ...

    format : function(indent){
        //获取当前编辑器
        var editor = editors.currentEditor;

        //从当前编辑器获取选中的对象
        var sel   = editor.getSelection();

        //获取当前的文档对象引用
        var doc   = editor.getDocument();

        //获取当前选中对象的range对象
        var range = sel.getRange();

        //从range对象获取选中的文本
        var value = doc.getTextRange(range);

        //尝试将选中的文本转换为JSON,并格式化 
        //然后再回转为文本字符串,如果出现错误则给用户显示错误.
        try{
            value = JSON.stringify(JSON.parse(value), null, indent);
        }
        catch(e){
            util.alert(
                "Invalid JSON", 
                "The selection contains an invalid or incomplete JSON string",
                "Please correct the JSON and try again");
            return;
        }

        //如果格式化成功则用格式化后值替换掉range对象
        var end = doc.replace(range, value);

        //用格式化的值更新当前选中的部分
		<p>
			sel.setSelectionRange(Range.fromPoints(range.start, end));
		</p>
},

    ...
}

我们的扩展现在可以使用了,但让我们再添加点东西。

Key快捷键绑定

我希望使用快捷键来使用这个扩展,window使用: Ctrl-Shift-J,mac用Command-Shift-J。Cloud9中用户可以自行配置快捷键。要实现上述功能,还需几个步骤。首先在ext/keybindings_default文件中为我们的扩展新添windows和mac的默认键绑定部分。

...

"ext" : {
    ...
    "formatjson" : {
        "format" : "Ctrl-Shift-J" // Or "Command-Shift-J" for the mac file
    },
    ...
}

...
然后必须要让快捷键管理器知道该扩展对什么快捷键响应和显示什么UI元素。添加名为hotkeys和hotitems的hash表:

hotkeys  : {"format":1},
    hotitems : {},
现在你有两种途径为键绑定添加处理器了。直接的方式是在扩展中添加响应方法,方法的名称与hotkeys中指定的名称相同即可,此处就是“format ”。因为我们的json格式化扩展有一个菜单来显示快捷键,我更喜欢将响应方法连接到菜单的onclick事件上,这样当我按下快捷键时这个方法被执行。而且当我使用快捷键时这个菜单按钮应该点亮。可以在hotitems哈希表中添加菜单项来达到目的:

this.hotitems["format"] = [this.nodes[0]];
现在我们可以在Tools菜单下的Extendtion Manage中来激活该扩展了,可以观看下面这段视频来看看,如何在3分钟内完成这个扩展。

其他资源

When you need help with creating an extension 在你开发扩展需要帮助的时候请到Cloud9的 Google Group 。可以向github的issue跟踪issue tracker of GitHub提交任何你发现的问题。Cloud9的所有开发者在Twitter上十分活跃。在扩展Cloud9的路上祝你好运。我都等不及要看你会扩展出什么了。 我们非常乐意将你酷毙了的扩展添加为Cloud9的子模块,或者在Github上提交pull request。

玩得开心!

你可能感兴趣的:(JavaScript,ide,开发者,控制台,编辑器)