Singal Page App

开篇扯淡

距离上一篇文章已经有好几个月,也不是没有时间记录点东西,主要是换了新的工作,在一家外资工作,目前的工作内容大多都是前端开发,新接触的东西因为时间原因,大多还不成体系,所以这么长时间什么都没记录下来,也正是因为新的工作内容,才有了今天这篇文章。

这篇文章是我自己的博客项目的前端重写,因为目前ASP.NET API和单页应用的流行,结合目前工作中用到的东西,我决定把我的博客项目的前端部分整个重写,(以前的就是一坨…)

步入正题

背景知识

RequireJS http://www.requirejs.org/

Knockout http://knockoutjs.com/

BootStrap http://getbootstrap.com/

PubSubJS https://github.com/mroderick/PubSubJS

如果没有接触过的朋友请自行谷歌百度吧,就不浪费口水,额,键盘啦,什么,没有jQuery,呵呵,呵呵,正如Knockout官方文档里说的,Everyoue loves jquery。

RequireJS我用来做模块加载器,Knockout做MVVM分离也是爽到没朋友(谁用谁知道),Bootstrap搭建界面布局,PubSub,看着名字就知道啦。

文档结构

Libs:放置上文中提到的各种框架和工具;

App:主要的工作目录,articleList、catalog、articleViewer分别代表整个前端应用中的一个组件,对应的.html文件是他们自身的视图模板;

Utilities:存放一些工具类,如检测设备、格式化Url和字符串等;

Layout:只有一个文件,存放了整个前端应用的模板,可以通过更改这个文件,来改变各个组件的表现形式。

服务端API准备

为这个示例,我只准备了三个服务端API:

GetCatalog 得到文章类型目录:

  1. namespace MiaoBlog.Controllers.API
    {
        public class CatalogController:ApiController
        {
            public IEnumerable<CategoryView> Get()
            {
                GetAllCategoriesResponse response = articleCatalogService.GetAllCategories();
                return response.Categories;
            }
        }
    }

GetArticlesByCategory 根据类型ID和页面编号获取文章目录,

GetArticle 根据文章ID获取文章内容等信息:

  1. namespace MiaoBlog.Controllers.API
    {
        public class ArticlesController:ApiController
        {
            public IEnumerable<ArticleSummaryView> GetArticlesByCategory(int categoryId, int pageNumber)
            {
                GetArticlesByCategoryRequest request = GenerateArticlesByCategoryRequestFrom(categoryId, pageNumber-1);
                GetArticlesByCategoryResponse response = articleCatalogService.GetArticlesByCategory(request);
                return response.Articles;
            }
  2.  
  3.         public ArticleDetailPageView GetArticle(int id)
            {
                ArticleDetailPageView detailView = new ArticleDetailPageView();
                GetArticleRequest request = new GetArticleRequest() { ArticleId = id };
                GetArticleResponse response = articleCatalogService.GetArticle(request);
                ArticleView articleView = response.Article;
                detailView.Article = articleView;
                detailView.Comments = response.Comments;
                detailView.Tags = response.Tags;
                return detailView;
            }
  4.  
  5.         private static GetArticlesByCategoryRequest GenerateArticlesByCategoryRequestFrom(int categoryId, int index)
            {
                GetArticlesByCategoryRequest request = new GetArticlesByCategoryRequest();
                request.NumberOfResultsPerPage = int.Parse(ApplicationSettingsFactory.GetApplicationSettings().NumberOfResultsPerPage);
                request.Index = index;
                request.CategoryId = categoryId;
                request.ExcerptLength = int.Parse(ApplicationSettingsFactory.GetApplicationSettings().ExcerptLength);
                return request;
            }
        }
    }

Require配置与系统配置

这里我用到的Require的几个常用插件:domReady、css、text.

paths配置了引用的js的别称:

  1. paths:{
            'lib/jquery': './Libs/jquery-1.11.1',
            'lib/underscore': './Libs/underscore',
            'lib/unserscore/string': './Libs/underscore.min',
            'lib/backbone':'./Libs/backbone',
            'lib/backbone/eproxy':'./Libs/backbone.eproxy',
            'lib/backbone/super': './Libs/backbone.super',
            'lib/pubsub': './Libs/pubsub',
            'r/css': './Libs/css',
            'r/less': './Libs/less',
            'r/text': './Libs/text',
            'r/domReady': './Libs/domReady',
            'r/normailize': './Libs/normalize',
            'pubsub': './Libs/pubsub',
            'lib/ko': './Libs/knockout-3.2.0',
            'utility': './Utilities/utility',
            'util/matrix2d': './Utilities/matrix2d',
            'util/meld':'./Utilities/meld',
            'lib/bootstrap': './Libs/bootstrap-3.2.0/dist/js/bootstrap',
            'lib/bootstrap/css': './Libs/bootstrap-3.2.0/dist/css/'
        },

shim的配置略过;

然后就是require的调用入口了,从这里启动整个前端应用:

  1. require(['lib/jquery', 'r/domReady', 'lib/underscore', 'config', 'r/text!Layout/template.html', 'r/css!lib/bootstrap/css/bootstrap.css', 'lib/bootstrap', ], function ($, domReady, _, config, template) {
        domReady(function () {
            var rootContainer = $("body").find("[data-container='root']");
            var oTemplate=$(template);
            var modules = $("[data-module]",oTemplate);
            _.each(modules, function (module, index) {
                require(["App/" + $(module).attr("data-module")], function (ModuleClass) {
                    var combineConfig = _.defaults(config[$(module).attr("data-module")], config.application);
                    var oModule = new ModuleClass(combineConfig);
                    oModule.load();
                    oModule.render(modules[index]);
                });
            });
            rootContainer.append(oTemplate);
        });
    });

这里看到了template.html通过r/text引入,上文中提到过,它就是整个应用程序的模板文件,先看一下它的结构我再接着解释代码内容:

  1. <div class="container">
        <div class="row">
            <div class="col-lg-3" data-module="catalog"></div>
            <div class="col-lg-9" data-module="articleList"></div>
        </div>
        <div data-module="articleViewer"></div>
    </div>
 
这样看起来,代码的意图就明晰多了,在页面中查到了data-container为root的节点,将它作为整个前端应用的根节点,然后再读取上面的模板文档,根据模板中标签的data-module属性,获得模块名称,然后动态的加载模块。
在这里我使用了Underscore的_.defaults方法,给各个模块取得了各自的配置内容和公用配置内容,Underscore是js的一个工具类,自行百度,不多介绍,还有个个人推荐的Underscore.string,它提供了很多js处理字符串的方法,比较方便好用。
上文所提及的应用配置文件如下:
 
  1. define(function () {
        return {
            application: {
                Events: {
                    SWITCH_CATEGORY:"Miaoblog_Switch_Category",
                    OPEN_ARTICLE:"Miaoblog_Open_Article"
                }
            },
  2.  
  3.         catalog: {
                APIs: {
                    GetCatalog: {
                        Url: "http://localhost:15482/api/Catalog"
                    }
                }
            },
            articleList: {
                APIs: {
                    GetArticleList: {
                        Url: "http://localhost:15482/api/Articles",
                        ParamsFormat: "categoryId={0}&pageNumber={1}"
                    }
                }
            },
            articleViewer: {
                APIs: {
                    GetArticle: {
                        Url:"http://localhost:15482/api/Articles/{0}"
                    }
                }
            }
        };
    });
结合上文中的代码,可以明确的知道一点,各个组件模块最终只会得到关于它自己的配置项目和公用的,也就是application级别的配置内容,在application对象中的Events对象在下文中将会做详细的介绍。
 

模块中的工作

就已catalog模块为例,先贴上代码,再做解释:

  1. /// <reference path="../Libs/require.js" />
    define(['lib/jquery', 'lib/ko', 'lib/underscore','pubsub', 'r/text!App/catalogList.html'],
        function ($, ko,_, hub,template) {
            var app = function (config) {
                this.catalogList = null;
                this.oTemplate = $(template);
                this.config = config;
            }
  2.  
  3.         _.extend(app.prototype, {
                load: function () {
                    var self = this;
                    $.ajax({
                        type: "GET",
                        async: false,
                        url: self.config.APIs.GetCatalog.Url,
                        dataType: "json",
                        success: function (data) {
                            self.catalogList = data;
                        },
                        error: function (jqXHR, textStatus, error) {
                            console.log(error);
                        }
                    });
                },
                render: function (container) {
                    var self = this;
                    var oContainer = $(container);
              
                    var list = {
                        categories: ko.observableArray(),
                        switchCategory: function (selected) {
                            //alert("Hello world"+selected.Id);
                            hub.publish(self.config.Events.SWITCH_CATEGORY, selected.Id);
                        }
                    };
                    list.categories(self.catalogList);
                    oContainer.append(this.oTemplate);
                    ko.applyBindings(list, this.oTemplate.get(0));
                }
            });
  4.  
  5.         return app;
    });
 
这里唯一新的内容就是大杀器knockout终于出场了。
从上一节内容可以看到,主模块将会一次调用子模块的load和render方法,在这个子模块catalog中,load阶段,通过对服务端的api调用得到了文章目录,API的地址是通过config文件的解析传递过来的,的数据结构是这样的:
 
 
 
而在render阶段,传入的参数为仅供给当前组件的占位,组件自身可以决定怎样去布局这个占位,这就涉及到了它自身的模板文件了:
 
  1. <ul class="nav nav-pills nav-stacked" data-bind="foreach:categories">
        <li>
            <a href="#" data-bind="attr:{categoryId:Id},click:$parent.switchCategory">
                <!--ko text: Name--><!--/ko-->
                <span class="badge pull-right" data-bind="text:ArticlesCount"></span>
            </a>
        </li>
    </ul>

在数据和视图两者间,我使用了Knockout进行绑定,它的优势在文档中有详细的描述,如果您想了解的话,就在文章开始找链接吧;

接着分析代码,在视图中,使用了Bootstrap的样式创建了一个目录样式,并且banding了一个switchCategory方法到viewModel中,当我们点击每一个类型链接时候,系统会通过上文中提到的Pubsub工具发布一个SWITCH_CATEGORY的事件出去,并且携带了所点击类型的ID,这个常量字符串也是在上一节中的config文件中配置的。
 

模块间的工作

上一节中提到了Pubsub发布了一个事件出去,意图是希望文章列表或者其他什么关心这个事件的组件去做它自己的工作,在这个示例中当然就只有articleList这个组件了,来看一下这个组件的代码:

  1. /// <reference path="../Libs/require.js" />
    define(['lib/jquery', 'lib/ko', 'lib/underscore', 'utility', 'pubsub', 'r/text!App/articleList.html','r/css!App/CommonStyle/style.css'],
        function ($, ko, _, utility,hub,layout) {
            var app = function (config) {
                this.config = config;
                this.oTemplate = $(layout);
                this.currentPageArticles = null;
                this.currentCategoryId = null;
                this.currentPageNumber = null;
                this.articleListViewModel = null;
            }
  2.  
  3.         _.extend(app.prototype, {
                initialize:function(){
                   
                },
  4.  
  5.             load: function () {
                    var self = this;
                    hub.subscribe(this.config.Events.SWITCH_CATEGORY, function (msg, data) {
                        self.switchCategory(data);
                    });
                },
  6.  
  7.             render: function (container) {
                    var self = this;
                    var oContainer = $(container);
                    this.articleListViewModel = {
                        articles: ko.observableArray(),
                        openArticle: function (selected) {
                            hub.publish(self.config.Events.OPEN_ARTICLE, selected.Id);
                        }
                    };
                    oContainer.append(this.oTemplate);
                    ko.applyBindings(this.articleListViewModel, this.oTemplate.get(0));
                },
  8.  
  9.             switchCategory: function (categoryId) {
                    var self = this;
                    self.currentCategoryId = categoryId;
                    self.currentPageNumber = 1;
                    $.ajax({
                        type: "GET",
                        async: true,
                        url: utility.FormatUrl(false,self.config.APIs.GetArticleList,categoryId,self.currentPageNumber),
                        dataType: "json",
                        success: function (data) {
                            self.articleListViewModel.articles(data);
                        }
                    });
                },
                turnPage: function (pageNumber) {
  10.  
  11.             }
            });
  12.  
  13.         return app;
        }
    );
 
 
这里主要看以下两个点:
1.在Load阶段,组件监听了SWITH_CATEGORY这个事件,在事件触发后,将调用switchCategory方法;因为这个SWITCH_CATEGORY这个常量是配置在application对象中,所以它在各个组件间是公用的;
2.在switchCategory中,传入的即使上一节中提到的类型ID,然后同样通过上一节的方法,调用服务端API,获得数据,然后使用knockout进行数据绑定,在ViewModel中,可以看到一个openArticle方法,同样发布了一个事件,在这个示例中,是右articleViewer监听的,由于原理相近,就不多做解释了,仅有破图了代码送上。
 

烂图赏鉴

代码送上,仅供吐槽

onedrive就不用了,虽然很搞到上,但是谁知道哪天就又…你懂的

百度网盘地址:http://pan.baidu.com/s/1o6meoKa

Singal Page App:使用Knockout和RequireJS创建高度模块化的单页应用引擎的更多相关文章

  1. bootstrap + requireJS+ director+ knockout + web API = 一个时髦的单页程序

    也许单页程序(Single Page Application)并不是什么时髦的玩意,像Gmail在很早之前就已经在使用这种模式.通常的说法是它通过避免页面刷新大大提高了网站的响应性,像操作桌面应用程序 ...

  2. 实战使用Axure设计App,使用WebStorm开发(2) – 创建 Ionic 项目

    系列文章 实战使用Axure设计App,使用WebStorm开发(1) – 用Axure描述需求  实战使用Axure设计App,使用WebStorm开发(2) – 创建 Ionic 项目   实战使 ...

  3. Nodejs之MEAN栈开发(六)---- 用Angular创建单页应用(上)

    在上一节中我们学会了如何在页面中添加一个组件以及一些基本的Angular知识,而这一节将用Angular来创建一个单页应用(SPA).这意味着,取代我们之前用Express在服务端运行整个网站逻辑的方 ...

  4. ABP示例程序-使用AngularJs,ASP.NET MVC,Web API和EntityFramework创建N层的单页面Web应用

    本片文章翻译自ABP在CodeProject上的一个简单示例程序,网站上的程序是用ABP之前的版本创建的,模板创建界面及工程文档有所改变,本文基于最新的模板创建.通过这个简单的示例可以对ABP有个更深 ...

  5. 七天学会ASP.NET MVC(七)——创建单页应用

    系列文章 七天学会ASP.NET MVC (一)——深入理解ASP.NET MVC 七天学会ASP.NET MVC (二)——ASP.NET MVC 数据传递 七天学会ASP.NET MVC (三)— ...

  6. 【单页应用】全局控制器app应该干些什么?

    前言 之前,我们形成了页面片相关的mvc结构,但是该结构还仅适用于view(页面)级,那么真正的全局控制器app应该干些什么事情呢?我觉得至少需要干这些: 功能点 ① 提供URL解析机制,以便让控制器 ...

  7. 【读书笔记】WebApi 和 SPA(单页应用)--knockout的使用

    Web API从MVC4开始出现,可以服务于Asp.Net下的任何web应用,本文将介绍Web api在单页应用中的使用.什么是单页应用?Single-Page Application最常用的定义:一 ...

  8. One Page Scroll – 实现苹果风格的单页滚动效果

    单页滚动网站已经被广泛使用了有一段时间了,它们对于快速提供信息是很有用的.One Page Scroll 是一个 jQuery 插件,简化了创建此类网站的步骤,只需创建 HTML 结构,进行简单设置, ...

  9. 七天学会ASP.NET MVC(七)——创建单页应用 【转】

    http://www.cnblogs.com/powertoolsteam/p/MVC_Seven.html 系列文章 七天学会ASP.NET MVC (一)——深入理解ASP.NET MVC 七天学 ...

随机推荐

  1. C++学习笔记13-类继承

    1.  类模板的 static 成员[不同于C#中的static] 类模板能够像随意其它类一样声明static 成员.下面代码: template <class T> class Foo ...

  2. 如何在SSIS的脚本组件中访问变量

    原文:如何在SSIS的脚本组件中访问变量 这是一个小问题,我们在SSIS的设计中很多地方都会用到变量,我习惯性地将"变量"和"表达式"称为SSIS的灵魂,虽然不 ...

  3. hdu 5072 Coprime(同色三角形+容斥)

    pid=5072">http://acm.hdu.edu.cn/showproblem.php?pid=5072 单色三角形模型 现场赛和队友想了3个小时,最后发现想跑偏了.感觉好可惜 ...

  4. Word2vec 讨论

    我没有在自然语言处理完成.但基于Deep Learning 关注,自然知道一些Word2vec强大. Word2vec 是google 在2013年提供的一款将词表征为实数值向量的高效工具.而Word ...

  5. 王立平-NGUI

    NGUI: UI插入 UI : 人机交互界面 UI大部分是2D 眼下就我们总结这些.也许会增加 版权声明:本文博主原创文章,博客,未经同意不得转载.

  6. SAP ABAP规划 使用LOOP READ TABLE该方法取代双LOOP内部表的方法

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcWlueXVlemhhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQk ...

  7. 手机号ID开关星号(*)

    .h文件 <span style="font-size:18px;">/** * 转成星号工具 */ @interface AsteriskTool : NSObjec ...

  8. 你的Java代码对JIT编译友好么?(转)

    JIT编译器是Java虚拟机(以下简称JVM)中效率最高并且最重要的组成部分之一.但是很多的程序并没有充分利用JIT的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用JIT的程度. 在本文中 ...

  9. 由一道面试题想到的:Finally

    找工作时,有这样一道题: try{}里面有一条return语句,那么紧跟在这个try后的finally{}里的代码会不会执行,什么时候执行,在return之前还是之后? 我没有怎么思考,根据脑子里仅有 ...

  10. iOS开发必看的博客汇总

    OneV's Den http://onevcat.com/ 沉船家园 http://beyondvincent.com/ NSHipster http://nshipster.cn/ Limboy ...