65.9K
CodeProject 正在变化。 阅读更多。
Home

使用客户端框架 Durandal 实现单页应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (5投票s)

2016年1月7日

CPOL

5分钟阅读

viewsIcon

15477

downloadIcon

205

这是一个单页应用程序。本文使用的技术和版本是 MVC 5.0 和 Durandal 2.1(客户端框架)。

引言

SPA(单页应用程序),顾名思义,就是应用程序中的所有相关页面都加载在单个网页上。换句话说,任何应用程序的整个必要资源,如 HTML 页面、样式表、JavaScript 文件,在第一次请求后加载到浏览器中,并在请求时渲染。

市面上有许多框架可以用来构建 SPA,但在本文中,我们将介绍 Durandal SPA 框架。

Durandal

简单来说,它结合了不同的标准 JavaScript 库,如 Knockout、RequireJS、jQuery,这有助于我们构建耗时更少、响应式 UI,以获得更好的用户体验。

背景

在实现客户端框架时,我在视图与视图模型、knockout.js、require.js、CSS 等配置方面遇到了许多问题。然后我发现了 Durandal,一个提供了启动新项目所需的所有必要配置的框架,无需在配置方面付出太多努力。

实现

通常,单页应用程序的客户端数据模型会通过 Ajax 调用连接到服务层,用于发送和接收 JSON 数据。

在此演示中,我们不会触及任何服务器端组件;相反,我们将完全专注于客户端组件。

实现总结

将创建一个简单的菜单控件,带有子菜单选项;每次单击子菜单时,它将动态加载视图。

将要学习

  • 将 Durandal 框架导入 Web 应用程序
  • 创建视图和视图模型
  • 将视图绑定到相应的视图模型
  • 将视图和视图模型与动态数据绑定
  • 在需要时动态加载视图
  • 在单个页面中交互不同的视图

设置解决方案

步骤 1

创建一个名为 Demo.SPA.Solution 的解决方案。

第二步

添加一个名为 ASP.NET Web 应用程序的新项目,并选择空白 - MVC 模板。

解决方案的外观如下:

步骤 3

将 Durandal 入门套件添加到 Web 应用程序

它将添加一些额外的文件夹,如 AppApp\ViewsApp\ViewModels 和依赖文件,durandalconfid.csdurandalBundle.configDurandalController

我的解决方案外观如下:

注意App\ViewModels 下的 shell.js 将用于定义路由。

步骤 4

生成并运行解决方案。

由于 durandalController 是默认控制器,请按以下方式浏览 URL:

https://:XXXX/durandal

网页将显示如下:

欢迎和闪烁标签是 Durandal 框架自带的。

要删除此标签,您需要注释掉 shell.js 中的以下行。

          { route: '', title:'Welcome', moduleId: 'viewmodels/welcome', nav: true },

          { route: 'flickr', moduleId: 'viewmodels/flickr', nav: true }

Using the Code

继续我们的示例应用程序。

步骤 5

App/views 文件夹中添加视图(home.htmlmenu.htmlproductlist.htmlproductdetails.htmladdnewproduct.html)。

步骤 6

app/viewmodels 文件夹中添加与 HTML 同名的 viewmodel(例如:home.jsmenu.js..)。

我创建了一个名为 Repository 的文件夹,它将处理数据部分,因为在此演示中我们不包含任何服务器端实现。

Repository 文件夹中,我创建了两个 JS 文件,名为 BookRepository.jsMenuRepository.js

script 文件夹中,我创建了 globaldata.js 文件,我将在其中声明全局作用域变量。

并通过以下行将其捆绑到 DurandalBundle.config.cs 文件中

Include("~/Scripts/globaldata.js")

因此,我的解决方案现在显示如下:

Using the Code

获取数据

BookRepository.jsMenuRepository.js 中,我定义了一些变量和方法,我们将在应用程序的后续部分使用它们。

BookRepository.js

define(function (require) {
    return {
        _books: [
                    { id: 0, title: 'The Low Land', writter: 'Jumpa Lahiri', 
                    price: '12', description: 'Test low land description' },
                    { id: 1, title: 'The Story of time being', writter: 'Ruth Ozeki', 
                    price: '13', description: 'Test Story of time being description' },
                    { id: 2, title: 'Alchemist', writter: 'Paulo', 
                    price: '14', description: 'Test Story of time being description' },
                    { id: 3, title: 'The Narrow Road to the Deep North', 
                    writter: 'Richard Flanagan', price: '10', 
                    description: 'Test Narrow Road to the Deep North description' },
                    { id: 4, title: 'Luminaries', writter: 'Eleanor Catton', 
                    price: '11', description: 'Test Luminaries description' },
                    { id: 5, title: 'Sense of an Ending', writter: 'Julian Barnes', 
                    price: '12', description: 'Test Sense of an Ending description' } ],

        listBooks: function () { return this._books; },
        getBooksById: function (id) {
            for (var i = 0; i < this._books.length; i++) {
                if (this._books[i].id == id) {
                    return this._books[i];
                    break;
                }
            }
        }
    };
});

MenuRepository.js

define(function () {
    return {
        _menusItems: {
            menu: [
                    { name: 'Home', link: '0', sub: null },
                    {
                        name: 'Products', link: '1', 
                                          sub: [{ name: 'List of Products', sub: null },
                                          { name: 'Register New Product', sub: null },
                                          { name: 'Enquiry Product', sub: null }]
                    },
                    {
                        name: 'About US', link: '2', sub: [{ name: '', sub: null },
                                                         { name: '', sub: null }]
                    },
                    {
                        name: 'Contact', link: '3', 
                                          sub: [{ name: 'Corporate Office', sub: null },
                                          { name: 'Home Office', sub: null }]
                    }
            ]
        },
        menuItems: function () { return this._menusItems; }
    }
})

步骤 7

现在我们将处理菜单视图模型。

由于我们从 MenuRepository.js 获取菜单数据,因此要获取菜单视图模型中的菜单数据,首先需要加载 MenuRepository,我们通过将其声明在 define 框内来做到这一点,例如:

define(['Repository/MenuRepository'], function (mRepository)

通过这样做,我们可以访问 MenuRepository.js 的变量和函数到此页面。

我们将菜单数据存储在此可观察数组中,并将其绑定到视图。

因此,相应的数据将显示在网页上。

Menu.js

define(['Repository/MenuRepository'], function (mRepository) {
    var menuConstructor = function () {
        var self = this;
        g_menuItemObservable = ko.observable("");
        self.menuItems = ko.observableArray([]);
        self._menus = [];
        self.getMenu = function () {
            self._menus = mRepository.menuItems();
        }
        self.attached = function (view) {
            self.getMenu();
            self.menuItems(self._menus.menu);
        }   
    }
    return new menuConstructor();
})

Menu.html

<div class="menu-style" style=" height:80%; width:70%; margin-left:10%; margin-top:2%;">
    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
            <div>
                <ul class="nav navbar-nav" data-bind="foreach:menuItems">
                    <li class="dropdown">
                        <a class="dropdown-toggle" 
                        data-toggle="dropdown" data-bind="text:name"></a>
                        <ul class="dropdown-menu" data-bind="foreach: sub">
                            <li data-bind="text:name"></li>
                        </ul></li>
                </ul>
            </div></div>
    </nav>
</div>

视图模型遵循一些生命周期。在这里,我根据需要使用组合生命周期(activate\attach)。

有关生命周期,请参阅 Durandal docs.linkhttp://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.html)。

同样,我们将创建 productlist 视图和视图模型。

Productlist.js

define(['Repository/BookRepository'], function (repository) {
   var productConstructorViewModel;
   var productConstructor = function () {
       var self = this;
       productDetailObservable = ko.observable("");
        self.products = ko.observableArray([]);
        self._products = []
        self.getProducts = function () {
            self._products = repository.listBooks();
            self.products(self._products);
        };
        self.activate = function (data) {
            self.getProducts();
        }
        self.removeProfile = function (list) {
            if (confirm("Are you sure you want to delete this profile?")) {
                self.products.remove(list);
            }
        }
        self.editProfile = function (list) {
            // Implement your logic
        }
        self.getList = function (data, event) {
            g_productID = data.id;
            $('#divProductdetails').css("display", "block");
        }
     }
    return new productConstructor();
})

Productlist.html

<div><div>     
        <table class="table table-striped table-bordered table-condensed" >
            <tr><th>Title</th><th>Author</th></tr>
            <tbody data-bind="foreach:products">
                <tr><td><a data-bind="text:title"></a></td>
                    <td data-bind="text:writter"></td>
                    <td><button class="btn btn-mini btn-danger" 
                    data-bind="click: function(data, event) {$root.getList(data, event); 
                    return true;}">Details</button></td>
                    <td><button class="btn btn-mini btn-danger" 
                    data-bind="click: $parent.removeProfile">Remove</button></td>
                    <td><button class="btn btn-mini btn-danger" 
                    data-bind="click: $parent.editProfile">Edit</button></td></tr>
            </tbody>
        </table>
    </div>

现在,我将把菜单与 home 绑定,并将 productlist 与菜单绑定。

在这里,我们使用了一个重要的 knockout 绑定概念,称为 compose

Home.js

define([], function () {
    var homeconstructorViewModel;
    homeconstructorViewModel = function () {
        self = this;
        self.menuItemObservable = ko.observable("");
        self.categories = ["Product List"];
        self.productListObservable = ko.observable("");
        self.loadProductList = function (data, event) {
            self.productListObservable({ view: 'views/productlist.html', 
                                         model: 'viewmodels/productlist' });
            $('#divProducts').css("display", "block")
        }
        $(document).on("click", ".dropdown-menu li", function (e) {
            var ctrl = $(this).text();
            if (ctrl == "List of Products") {
                self.menuItemObservable({ view: 'views/productlist.html', 
                                          model: 'viewmodels/productlist' })
            }
            if (ctrl == "Register New Product") {
                self.menuItemObservable({ view: 'views/addnewproduct.html', 
                                          model: 'viewmodels/addnewproduct' })
            }
        })
    }
    return homeconstructorViewModel;
});

Home.html

<div>
    <div style="height:80%; width:70%"><div>
            <div data-bind="compose:
                         { model:'viewmodels/menu', view:'views/menu.html'}">
            </div> </div>    </div>
    <div>
        <div id="divMenuItemBody" style="margin-left:10%; 
                                  margin-top:2%; width:70%; height:40%">
            <div data-bind="compose: menuItemObservable" style="height:100%;"></div>
        </div> </div>
</div>

在这里,在 HTML 中,您可以看到我使用了两个 data-bind。在第一种情况下,compose 使用 静态 定义的视图和视图模型,而在第二种情况下,它是一个可观察对象,每当可观察变量值更改时,它会动态加载视图/视图模型(请在 home.js 中查看)。

我还通过使用相同的逻辑设计了其余页面,您可以下载示例进行查看。

步骤 8

现在让我们转向路由部分。

我希望在页面加载时浏览主页,因此将在 shell.js 文件中定义路由如下:

{ route: 'home', title: 'Demo', moduleId: 'viewmodels/home', nav: true }

关注点

在本文中,我们讨论了如何在 Web 应用程序中使用 Durandal。

我写这篇文章时考虑到了初学者。

希望本文对您有所帮助。

源代码

我已将此示例项目与本文一起上传,其中包含更多功能。您可以下载此示例代码,以便更好地理解这些概念和进一步的实现。

使用以下 URL 运行应用程序。

https://:XXXX/durandal

XXXX 是您本地系统上应用程序运行的端口号。

历史

  • 2016 年 1 月 7 日:初始版本
© . All rights reserved.