如何组织JavaScript用户界面?

9

我需要有关程序架构的良好示例和最佳实践。

我正在尝试为使用Google.Maps的应用程序建立JS用户界面。在第一版中,用户应该能够以类似于G.M的方式在地图上绘制几何形状。然后通过AJAX发送形状,显示响应。

问题是,仅仅通过多边形编辑代码就变得复杂了。

受Joel的“胶带程序员”启发,我尝试草拟一个简单直接的代码,使操作和切换事件处理程序,避免大型if-else树。 “新多边形”按钮创建一个map.onclick的观察者,更改其他按钮的事件处理程序或隐藏它们,自身也隐藏等等。

这种方法的缺点是数据处理代码与接口混合在一起。创建一个div容器以在新多边形上显示数据的代码与处理G.M或形状数据的代码相邻。如果我想要修改UI,则需要处理整个应用程序。

我可以稍后审查它并将此UI生成代码移到其他位置,但随后我的主程序员出现了。他坚持采用“消息传递”方法:一种简单的事件系统,对象订阅事件并触发它们。界面代码可以完全与数据处理和与G.M交互隔离,但现在每个侦听器都必须仔细检查是否传递的是给它的消息。

例如,在地图上单击多边形应该突出显示并开始编辑。但如果正在绘制另一个多边形,则不会这样做。因此,需要一些“你是在和我说话吗?”代码。

我将感激UI架构方法的良好示例。

5个回答

5

我不知道这是否与重点有关,但我将其作为我所有JavaScript项目的模板。

(function () {
var window = this,
    $ = jQuery,
    controller,
    view,
    model;

controller = {
    addEventForMenu : function () {
        // Add event function for menu
    }
};

view = {
    content : {
        doStuff : function () {

        }
    },

    menu : {
        openMenuItem : function () {

        }
    }
};

model = {
    data :  {
        makeJson : function () {
            // make json of string
        },

        doAjax : function () {

        },

        handleResponse : function () {

        }
    }
}

$.extend(true, $.view, view);
})();

这里的好处在于只有视图对象扩展到DOM,其余部分保留在匿名作用域内。
在大型项目中,我为每个部分创建了一个文件,例如map.js、content.js和editor.js。
如果你只关注视图对象中方法的命名,你可以在开发过程中使用任意数量的文件。当项目进入生产环境时,我会将它们合并成一个文件并进行缩小处理。
..fredrik

5

给您提供的事件处理思路是一个不错的方法。

这里还有一些更多的想法:

  • 将形状绘制功能作为一个组件
  • 形状绘制组件向其他代码发送事件,以响应“编辑”或“单击”等操作
  • 该组件还可以处理编辑部分-它向控制器发送“单击”事件,控制器告诉该组件进入编辑模式
  • 在编辑模式下,组件不会发送正常的“单击”事件,直到编辑被关闭,避免了您需要检查的问题

通常,拥有一个“视图”层,仅处理显示数据并向“控制器”层发送关于用户对该数据的操作(例如单击等)的事件是一个不错的想法,然后控制器决定要做什么-例如,它可以决定将视图更改为编辑模式。


1

简而言之,发布者-订阅者范例很适合绘制几何图形。首先创建一个命令行,其原始多边形是基础多边形发布者发布的。画布对象在这里似乎很明显,用于绘制,通常的方法repaint()用于更新客户端视图(事件驱动编程通常使用C语言,您可以查看例如opengl或glut事件驱动图形),再结合我也使用的不错gmap API,发布者-订阅者模式或工厂都是良好的设计模式,无论图形实现如何。棘手的gmaps特定问题是纬度和经度在json和持久层之间的数组中交换位置,目前没有服务器端的反向地理编码,命名有点不一致,并且对于多语言应用程序,名称都会相对于用户人类语言发生变化并且重复(文本中的巴黎、法国的巴黎...)。如果您喜欢我的实现方式,请查看注册地理名称和相对于geoip用户的空间坐标worldwide

function wAdd(response){
map.clearOverlays();
if(!response||response.Status.code!=200){

}
else{
    try{
        place=response.Placemark[0];
        point=new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);
        marker=new GMarker(point);
        map.addOverlay(marker);
        marker.openInfoWindowHtml('<a href="/li?lat='+place.Point.coordinates[1]+'&lon='+place.Point.coordinates[0]+'&cc='+place.AddressDetails.Country.CountryNameCode+'">'+place.AddressDetails.Country.AdministrativeArea.Locality.LocalityName+'<span id="wr2"></span> '+ nads( place.Point.coordinates[1],place.Point.coordinates[0] )+' ' +'<img src="http://geoip.wtanaka.com/flag/'+place.AddressDetails.Country.CountryNameCode.toLowerCase()+'.gif">');
    }
    catch(e){
        try{
            place=response.Placemark[0];
            point=new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);
            marker=new GMarker(point);
            map.addOverlay(marker);
            marker.openInfoWindowHtml('<a href="/li?lat='+place.Point.coordinates[1]+'&lon='+place.Point.coordinates[0]+'&cc='+place.AddressDetails.Country.CountryNameCode+'">'+place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName+'<span id="wr2"></span> '+ nads( place.Point.coordinates[1],place.Point.coordinates[0] )+' ' +'<img src="http://geoip.wtanaka.com/flag/'+place.AddressDetails.Country.CountryNameCode.toLowerCase()+'.gif">');
        } 
        catch(e){
try {
        place=response.Placemark[0];
                point=new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);
                marker=new GMarker(point);
                map.addOverlay(marker); 
                marker.openInfoWindowHtml('<a href="/li?lat='+place.Point.coordinates[1]+'&lon='+place.Point.coordinates[0]+'&cc='+place.AddressDetails.Country.CountryNameCode+'">'+place.AddressDetails.Country.CountryName+'<span id="wr2"></span> '+ nads( place.Point.coordinates[1],place.Point.coordinates[0] )+' ' +'<img src="http://geoip.wtanaka.com/flag/'+place.AddressDetails.Country.CountryNameCode.toLowerCase()+'.gif">');
            }
   catch(e){ 
place=response.Placemark[0];
      marker=new GMarker(point);
                map.addOverlay(marker);
marker.openInfoWindowHtml('<a href="/li">'+place.address+'</a>');
}

        } }
    }map.addOverlay(geoXml);
}

0
我建议使用几个对象变量来包含状态(0、绘图、编辑、...任何其他所需的状态)- 这将帮助您决定是否允许事件处理,或者仅在例如完成绘图并单击可编辑多边形时将其隐藏。
至于UI - 我不确定您的问题是针对您 - 开发脚本还是针对用户,因为您在这里混合了两件事。
请记住,对于用户来说,一切都应尽可能简单:如果他正在编辑,请不要让他绘制。如果他正在绘制,请不要让他编辑(可能会出现多边形重叠)。但是 - 允许用户快速从编辑(例如右键单击?)切换到绘图 - 或者换句话说取消当前状态。

0
首先,我会创建一个服务,它包装了谷歌API。这样,如果以后需要更改映射服务(如Windows地图或雅虎地图),您可以在谷歌服务上放置一个门面。然后,您可能希望在服务上放置一些包装器,并将其拆分为视图(输出)和模型(输入),并使用控制器/演示文稿进行管理。请在维基百科上查看Model View Controller / Model View Presenter / Presenter First / Humble Dialog。它应该讨论您正在寻找的分离。此外,Martin Fowler网页介绍了演示模式。您应该查看我的旧博客ugly-lisp-code。我引用了事件驱动/事件消息。
如果您有一对一的发布/订阅,请在将要触发事件的对象中存储事件处理程序(闭包/lambda/一级函数)。
如果您有一对多的发布/订阅,则需要一个更复杂的对象来存储闭包。

哈哈!现在我一直在看这个完全相同的问题。我打算在我的博客上写一篇关于如何在JavaScript中使用Presenter-First的文章。从零开始学习presentermodel

[编辑]你可能需要查看这个stackoverflow问题。其中一个答案有一个将关注点分离成MVC的链接。该链接在A List Apart上。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接