Singal Page App:使用Knockout和RequireJS创建高度模块化的单页应用引擎背景知识文档结构服务端API准备Require配置与系统配置模块中的工作模块间的工作烂图赏鉴代码送上

开篇扯淡

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

这篇文章是我自己的博客项目的前端重写,因为目前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 得到文章类型目录:

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

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

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

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;
        }

        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;
        }


        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的别称:

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的调用入口了,从这里启动整个前端应用:

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引入,上文中提到过,它就是整个应用程序的模板文件,先看一下它的结构我再接着解释代码内容:

<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处理字符串的方法,比较方便好用。

上文所提及的应用配置文件如下:

define(function () {
    return {
        application: {
            Events: {
                SWITCH_CATEGORY:"Miaoblog_Switch_Category",
                OPEN_ARTICLE:"Miaoblog_Open_Article"
            }
        },

        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模块为例,先贴上代码,再做解释:

/// <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;
        }

        _.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));
            }
        });

        return app;
});

这里唯一新的内容就是大杀器knockout终于出场了。

从上一节内容可以看到,主模块将会一次调用子模块的load和render方法,在这个子模块catalog中,load阶段,通过对服务端的api调用得到了文章目录,API的地址是通过config文件的解析传递过来的,的数据结构是这样的:

而在render阶段,传入的参数为仅供给当前组件的占位,组件自身可以决定怎样去布局这个占位,这就涉及到了它自身的模板文件了:

<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这个组件了,来看一下这个组件的代码:

/// <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;
        }

        _.extend(app.prototype, {
            initialize:function(){
                
            },

            load: function () {
                var self = this;
                hub.subscribe(this.config.Events.SWITCH_CATEGORY, function (msg, data) {
                    self.switchCategory(data);
                });
            },

            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));
            },

            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) {

            }
        });

        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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互联网杂技

内存卡存储原理,你知道吗?

1、 简介: SD卡(Secure Digital Memory Card)是一种为满足安全性、容量、性能和使用环境等各方面的需求而设计的一种新型存储器...

3826
来自专栏Android 技术栈

《Android群英传 神兵利器》读书笔记

这是一本Android开发的工具书,里面的提到的工具基本上是每个Android开发者必备的技能,买这本书的起因是看到有介绍Gradle和性能优化。里面提到的很多...

551
来自专栏张善友的专栏

用.NET Framework 2.0创建 Form设计器[翻译]

    这两天赶鸭子上驾,学习Form设计器,下面这篇在Msdn Magazine的文章可谓是经典,这两天学习了感觉还是做个翻译出来,更能够加强理解。对各位同...

2086
来自专栏后台及大数据开发

【高并发】秒杀系统高并发请求排队处理

今天无意中看见了这位兄弟的文章 通过请求队列的方式来缓解高并发抢购(初探)  但文章最后说并发超过500 就会出现超发,看了下代码,的确有这个问题

1251
来自专栏PHP实战技术

ThinkPHP3.2中英文切换!

最近公司项目版本升级,小梦已经忙成了狗,无暇顾及文章,今天抽时间写一篇助助兴!

4078
来自专栏从零开始学自动化测试

python接口自动化21-规范的API接口文档示例

前言 接口文档到底长啥样?做接口测试最大的障碍在于没有接口文档,很多公司不注重接口文档的编写,导致测试小伙伴没见过接口文档。 运气好一点的测试小伙伴可能厚着脸皮...

1.1K8
来自专栏即时通讯技术

金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(实战篇)

本文接上篇《金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(原理篇)》,以iOS端的Objective-C代码为例,向您演示如何使用P...

712
来自专栏landv

c语言_代码段_快说你是猪

3473
来自专栏格子的个人博客

Java定时发布文章简单方案

早上迷迷糊糊地开始了春节后的一天上班日程,脑袋还在噼里啪啦放烟花,项目管理部和SEO小伙伴就提了一桶凉水过来,往我头上一浇,瞬间烟花都湮灭了。

1011
来自专栏walterlv - 吕毅的博客

WPF 跨应用程序域的 UI(Cross AppDomain UI)

发布于 2017-11-12 16:36 更新于 2017-11...

1352

扫码关注云+社区