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

Azure Redis 查看器 - 面向 Azure 开发者的 Chrome 应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2015 年 5 月 21 日

CPOL

10分钟阅读

viewsIcon

16428

为 Azure 开发人员开发跨平台 Chrome 应用程序。

引言

在本文中,我想分享我开发名为“Azure Tools”的 Chrome 应用程序的经验。我将描述第一个工具——Redis Viewer 的总体思路,并解释所选择的解决方案和设计方法。Azure Tools 对 Azure 开发人员很有用,其代码对那些构建单页应用程序 (SPA) 的人,尤其是 Chrome 应用程序的开发人员很有用。

技术

  • Angular - JavaScript SPA(单页应用程序)框架
  • RedisJs - 开源 Redis 客户端
  • Browserify - 提供 require() 风格的分析器来组织代码
  • Net-Chromify - Chrome 应用程序的网络通信模块
  • Karma/Jasmine - 单元测试
  • Metro Bootstrap - CSS

源代码 - GitHub

Azure Tools - Google Web Store 安装

感谢 Sam Holder 对我的评论。

选择 Chrome 应用程序作为平台

为什么 Azure Tools 不是 Visual Studio 的插件?它是一个开发人员工具。——这个想法是创建一个可以在大多数平台(Windows、Unix,理想情况下还有移动设备)上运行的应用程序。我认为许多现有且优秀的工具(如 Azure Storage Explorer)只在 Windows 上运行,而 Mac OS 上的开发人员可能也需要使用这些工具,这是一个很大的缺陷。

为什么 Azure Tools 不是用一种跨平台技术编写的桌面应用程序?——我一直在寻找 Web 应用程序和桌面应用程序之间的折衷方案,桌面应用程序的一个缺点是难以交付和更新到新版本的软件。Chrome 应用程序提供了桌面应用程序的用户体验,并通过 Google Web Store 实现了自动化交付/更新过程。

为什么它不是一个 Web 应用程序?——访问 Azure 服务需要用户提供凭据和敏感信息。我个人在将我的云凭据发送到第三方 Web 服务器之前会三思而后行,即使所有者发誓他们不存储任何凭据,只使用它们来为您加载信息。从这个角度来看,Chrome 应用程序更值得信赖,它可以直接从用户的机器与云服务通信,而无需“中间”服务器。

设计挑战

当我开始考虑 Azure Tools 时,我面临着一些挑战。

首先,Azure Tools 应该是一个单层、完全是 JavaScript 的应用程序。如果 Chrome 应用程序平台不允许直接与 Azure 通信,那就需要实现服务器端——在用户应用程序和云服务之间充当“中间人”,正如我所说,出于安全原因,这绝对是我不想做的事情。此外,将 Azure Tools 开发为带服务器端的 Chrome 应用程序并没有真正意义,它可能最好只作为一个 Web 应用程序。

第二点紧随第一点:Chrome 应用程序应该利用 NodeJs 功能和包,并拥有足够的权限来执行此操作。

第三,应用程序应该有一个模块化设计。这并不意味着应用程序应该按需动态加载模块,而主要是关于代码组织、重用公共模块的能力以及添加新模块时的简单性。

最后但同样重要的是,如何组织 Chrome 应用程序的自动化测试。

Redis Viewer 功能

Redis Viewer 是 Azure Tools 的第一个模块,目前也是唯一的模块。

如果您还不了解 Redis,我强烈建议您了解更多关于这项技术的信息。简而言之,Redis 是一种分布式缓存和内存键值存储。Redis 中的每个值都代表一种数据结构:字符串、列表、集合、哈希、有序集合、位图等。除了常见的操作(如键过期或通过键创建、读取、更新、删除值)之外,Redis 还为每种数据结构提供了特定的操作,例如字符串值的获取子字符串和列表的排序。而且它处理命令的速度非常快。非常快。

微软将 Redis 作为 Azure 云平台的服务提供。每个人都可以在 Azure 帐户下创建自己的 Redis 集群,并且可以使用 REST 接口持久化数据和执行命令。

Redis Viewer 工具的主要目的是可视化 Azure Redis 内容并对 Redis 存储数据进行基本操作。查看器包括以下功能:

  • 显示现有键及其类型和值的列表
  • 允许按特定模式搜索键
  • 添加新的键值项
  • 更新现有值
  • 按键删除
  • 支持字符串、集合和哈希类型

UI

这基本上就是 Redis Viewer 的 UI 界面

Redis Viewer 是一个 Chrome 应用程序,因此您可以从 Web Store 安装它,该应用程序将添加到您的 Chrome 应用程序启动器面板以及任何受支持平台上的 Chrome 浏览器中。

在 Chrome 中运行 NodeJs Redis 客户端

我发现 Browserify 工具很有用,它可以按照 NodeJs 模块系统组织代码,并能够在 Chrome 应用程序中使用 NodeJs 模块。Browserify 的基本思想是开发人员可以使用 required() 方法引用 JavaScript 模块,然后执行 Browserify 预处理,该预处理使用代码分析来查找所有 require() 依赖项并将它们组合到一个文件中。例如,如果您的文件 app.js 需要模块 a.js,而 a.js 又需要模块 b.js,您可以在代码中这样引用它们

// app.js
var a = require('./a.js').A;

// a.js
var b = require('./b.js').B;
exports.A = function(){}

// b.js
exports.B = function(){}

运行 Browserify 预处理命令后:

browserify app.js -o bundle.js

将生成一个 bundle.js 文件,其中包含以下代码

代码很丑陋,但如你所见,它包含了 Browserify 分析器找到并包含在 bundle 文件中的两个依赖项 a 和 b。

考虑到这一点,我能够通过 npm 添加 NodeJs Redis 客户端,并使用 Browserify 对其进行预处理以在 Chrome 应用程序中使用。还有一个问题:Redis 客户端使用 **net** 模块,该模块封装了网络通信功能,如套接字上的读/写、数据序列化等。此模块作为 NodeJs 的一部分安装,并且无法在 Chrome 应用程序中工作,因为 Chrome 应用程序应该通过 Chrome API 与套接字一起工作。所以下一步是我需要用使用 Chrome API 的实现来替换 net。有一个名为 **net-chromify** 的包,我通过 npm 安装了它。

因此,为了准备能够与 Redis 服务器通信的代码库,我需要执行以下命令

browserify app.js -r ./node_modules/net-chromify/index.js:net > bundle.js

这将对 app.js 文件进行 Browserify 预处理,并在使用 net 模块的地方使用 **net-chromify** 实现。预处理后的代码保存在 bundle.js 文件中。

注意:实际上,在所有这些步骤之后,Redis 客户端仍然存在问题。不多,但需要对 Redis 客户端进行更改才能使其在 Chrome 应用程序中运行。因此,如果您需要修复版本,可以从 AzureTools 项目存储库中查看。我将负责将修复推送到主 RedisJs 存储库中。

最后,为了让 Redis 客户端在 Chrome 应用程序中工作,我需要在应用程序清单中设置 tcp 操作的权限

    "permissions": [
        {
            "socket": [
                "tcp-listen:*:*",
                "tcp-connect",
                "resolve-host"
            ]

        }
    ]

Redis 扫描器

Redis 提供了几个命令,我认为这些命令实际上应该被弃用。其中之一是 KEYS 命令,它允许从 Redis 数据库中获取所有键,但这会降低 Redis 服务器的速度并降低其性能。这意味着任何使用 KEYS 命令获取键列表的查看器都不能用于生产环境。

或者,可以使用 SCAN 命令集实现相同的目的,但对性能的影响要小得多。这种方法是 Redis Scanner 模块的基础,并且 Redis Viewer 将从数据库加载的键的数量限制为 100(如果用户按模式搜索键,则此限制不适用)。

Redis 扫描器的用法如下

// keys of type set or hash set
// load value entries not as a single set but one by one
// so entries must be grouped by key
var groupByKey = function(type, key, value) {
    var existing = self.Keys.filter(function(item) { return item.Key == key; });
    if (existing !== null && existing[0] !== undefined) {
       var values = JSON.parse(existing[0].Value);
       values.push(value);
       existing[0].Value = JSON.stringify(values);
       return false;
    } else {
       self.Keys.push({ Key: key, Type: type, Value: JSON.stringify([value]) });
       return true;
    }
}

// load redis data
var maxItemsToLoad = 100;
self.loadKeys = function(pattern) {
    self.Keys.length = 0;
    var loadedCount = 0;

    var client = $redisScannerFactory({
        pattern: pattern,

        each_callback: function(type, key, subkey, p, value, cb) {
             switch(type) {
               case 'set': if(groupByKey(type, key, value)) { loadedCount++; }; break;
               case 'hash': if(groupByKey(type, key, value)) { loadedCount++ }; break;
               default: 
                  self.Keys.push({ Key: key, Type: type, Value: value}); 
                  loadedCount++;
                  break;
               }

             if ((searchViewModel.Pattern === '' || searchViewModel.Pattern === '*')
                && loadedCount >= maxItemsToLoad) {
                showInfo('First ' + maxItemsToLoad + 
                         ' loaded. Use search to find specific keys.');
                cb(true);
             } else {
                cb(false);
             }
         },

         done_callback: function(err) {
             if (err) {
                 $messageBus.publish('redis-communication-error', err);
             }

             $dataTablePresenter.showKeys(self.Keys, self.updateKey, self.removeKey);
         }
   });
};

使用 Angular

模块化

Angular 与 Browserify 结合使用,可以在 Chrome 应用程序中以模块化方式组织代码并分离应用程序关注点。以下代码演示了 Azure Tools 的模块化构成

require('./exceptionHandling/exceptionHandlingModule.js').register(angular);
require('./common/commonModule.js').register(angular, angularRoute);
require('./common/dialogsModule.js').register(angular, angularRoute);
require('./common/actionBarModule.js').register(angular);
require('./redis/redisModule.js').register(angular, angularRoute);
require('./tiles/tilesModule.js').register(angular, angularRoute);

var app = angular.module('app', [
            'exceptionOverride',
            'common',
            'actionBar',
            'dialogs',
            'tiles',
            'tiles.redis'
        ], function() {}).controller('AppController', ['$state', function ($state) { }]);

每个模块都在 angular 中注册自己的控制器、指令、服务、常量。“app”模块是应用程序根,它聚合所有应用程序模块。任何新模块或工具都必须在应用程序根中注册。

模块不能动态加载。它们都由 Browserify 编译成一个单独的 bundle.js 文件,并打包到 Chrome 应用程序中。

导航

正如您从 Azure Tools 模块结构中注意到的,注册了“tiles”和“tiles.redis”模块。“tiles”表示启动屏幕并列出可用的 Azure 工具。“tiles.redis”是 Redis Viewer,可以从启动屏幕启动。所有后续工具都可以以相同的方式工作。

但是 Azure Tools 是一个单页应用程序,没有像 Web 应用程序那样的 URL 导航。因此,为了在单个 HTML 页面中实现导航和屏幕切换,我使用了 angular-ui 插件。

Angular-ui 允许将屏幕声明为状态并在它们之间进行转换。此代码注册了瓷砖和 Redis 屏幕

    // tilesModule.js
    tilesModule.config(function ($stateProvider) {
        $stateProvider
            .state('tiles', {
                url: "", // empty url redirects to tiles start screen
                templateUrl: "tiles/view/index.html",
                controller: 'TilesController',
                params: {
                    seq: {}
                }
            });
    });
    
    // redisModule.js
    redisModule
       .config(function ($stateProvider) {
           $stateProvider
               .state('redis', {
                   url: "/redis",
                   templateUrl: "redis/view/index.html",
                   controller: 'RedisController',
                   params: {
                       seq: {}
                   }
               });
       });

HTML 中当前屏幕的占位符

        <div id="content" ui-view>
        </div>

从瓷砖屏幕导航到 Redis 屏幕执行以下代码

// tilesViewModel.js
self.goToRedis = function () { $state.go('redis', {});} // where $state implemented by angular-ui 
                                                        // and can be injected in view model(controller)

Angular-UI 从 MVVM 角度来看很棒,因为开发人员可以在视图模型中指定状态,以抽象方式描述应用程序流程,并将屏幕之间动画转换等细节移动到视图层。这很棒。

Redis Viewer MVVM

模型由 Redis 结构表示:字符串、集合和哈希,以及对它们执行 CUD(创建/更新/删除)操作的存储库。对于读取操作,使用 Redis 扫描器,存储库使用 Redis NodeJs 客户端来获取实体。我们上面已经讨论过扫描器和 Redis NodeJs 客户端。此外,还有一个 Redis 设置类,用于保存用户凭据以访问 Azure Redis 集群和验证规则。

视图模型由 AngularJs 控制器表示(我个人很乐意在 angular 中看到 viewModel() 函数,但控制器是不同 MVx 模式的更通用名称)。

     // redisViewModel.js
     module
        .controller('RedisController', [
            '$scope',
            '$timeout',
            'messageBus',
            'activeDatabase',
            'redisRepositoryFactory',
            'redisScannerFactory',
            'validator',
            'redisSettings',
            'dataTablePresenter',
            'actionBarItems',
            'dialogViewModel',
            'confirmViewModel',
            'notifyViewModel',
            'busyIndicator',
            function(
                // infrastructure and utils
                $scope,
                $timeout,
                messageBus,

                // model
                activeDatabase,
                redisRepositoryFactory,
                redisScannerFactory,
                validator,
                redisSettings,
                
                // view models
                dataTablePresenter,
                actionBarItems,
                dialogViewModel,
                confirmViewModel,
                notifyViewModel,
                busyIndicator) { ... }]);

Redis 视图模型依赖于其他视图模型,如“dialogViewModel”、“notifyViewModel”、“busyIndicator”等;模型:’redisRepositoryFactory’、’redisScannerFactory’ 等,以及基础设施和工具对象。

您可能已经注意到,由于 dataTablePresenter,这不是纯粹的 MVVM 实现。我使用它来包装 jQuery DataTables(网格)上的操作,例如使用 Redis 数据绘制表格行、展开/折叠详细信息、排序等。DataTables 对 MVVM 不友好,实现 Angular 指令可能是在逆流而上。因此,抽象 presenter 在这种情况下对我来说很好用:它不会破坏可测试性,并且 redisViewModel 不会直接处理 DOM 元素。

视图是 HTML 文件和一些 Metro Bootstrap JS 代码。我在 Azure Tools 中使用以下项目结构作为视图层

     -- app
     ---- content
     ------ css
     ------ img
     ------ js             // view specific Metro Bootrap code here
     ---- redis            // redis module
     ------ view
     -------- index.html
     ---- tiles            // tiles module
     ------ view
     -------- index.html

未处理的错误处理

Angular 允许您为仅在 Angular 上下文中发生的未处理异常添加自定义处理程序,对于 Angular 之外的错误,处理程序将不会被调用。Azure Tools 通过向用户显示适当的消息和向开发人员发送包含错误详细信息的电子邮件链接来处理意外错误。

angular
    .module('exceptionOverride', [])
    .factory('$exceptionHandler', [function () {
        return function (exception, cause) {
            var data = {
                type: 'angular',
                localtime: Date.now()
            };

            if (cause) { data.cause = cause; }
            if (exception) {
                if (exception.message) { data.message = exception.message; }
                if (exception.name) { data.name = exception.name; }
                if (exception.stack) { data.stack = exception.stack; }
            }

            var el = document.getElementById('sendEmail');
            var alertArea = document.getElementById('alertArea');
            if (el && alertArea) {
                alertArea.style.display = "block";
                el.href = 'mailto:' + supportEmail + '?subject=' + 'Bug Report' + '&body='
                    + data.message + '|' + data.name + '|' + data.stack
                    + '|' + data.type + '|' + data.url + '|' + data.localtime;
            }

            throw exception;
        };
    }]);

单元测试

Azure Tools 的 MVVM 设计和 Angular 依赖注入允许用单元测试覆盖应用程序逻辑。我使用 Jasmine 测试框架和 Karma 测试启动器进行自动化。

首先,我需要用 Redis 存储的假实现来模拟真实的 Redis 客户端。Redis 存储的假实现非常简单,您可以在源代码的 app/redis/model/mocks 文件夹下找到它。

一旦完成了假的 Redis 实现,我遇到了另一个问题。如何将假的实现注入到我想测试的 redisViewModel 中?我最初的错误是试图在 Karma 配置中引用 testing redisViewModel.js 文件和所有带依赖项的文件。这种方法需要手动在单元测试中注入依赖项,但我已经在应用程序根中配置了大部分依赖项,只需要模拟其中几个。因此,我重新编写了测试,以在 Karma 中加载 browserified bundle.js 文件,并使用 angular-mocks 来模拟模型服务。

redisViewModel 添加新 Redis 键的测试如下所示

// redisTests.js
describe('RedisController', function () {
    beforeEach(module('app'));
    beforeEach(function () {
        // replace real Redis client
        // with mock implementation
        angular.module('tiles.redis')
            .factory('redisClientFactory', function () {
                return redisClientFactoryMock;
            })
          .factory('redisScannerFactory', [
            'redisDataAccess', 'redisScanner',
            function (redisDataAccess, redisScanner) {
                return redisScannerMock;
            }
          ]);
    });

    it('should save string value',
       inject(function ($rootScope, $controller, actionBarItems, dialogViewModel) {
           // arrange
           var scope = $rootScope.$new();
           $controller("RedisController", {
               $scope: scope,
           });
           var viewModel = scope.RedisViewModel;
           data = [
                { Key: 'key:1', Type: 'string', Value: '1' },
                { Key: 'key:2', Type: 'string', Value: '2' }
           ];

           // act
           // user clicks add key
           actionBarItems.addKey();
           // provides key and value for string data structure
           dialogViewModel.BodyViewModel.Key = 'key:3';
           dialogViewModel.BodyViewModel.Value = '3';
           // saves new key in redis storage
           dialogViewModel.save();

           // assert
           expect(viewModel.Keys.length).toBe(3); // 2 initial + 1 new added
           expect(viewModel.Keys[0].Key).toBe('key:1');
           expect(viewModel.Keys[1].Key).toBe('key:2');
           expect(viewModel.Keys[2].Key).toBe('key:3');
       }));
});

Karma 测试配置

//  karma.config.js
module.exports = function(config) {
    config.set({
    basePath: '',
    frameworks: ['jasmine'],
    browsers: ['Chrome'],
    files: [
         'app/node_modules/angular/angular.js',
         'app/node_modules/angular/angular-*.js',
         'tests/lib/angular/angular-mocks.js',

         'tests/*.js',

         'app/bundle.js',
         'app/redis/model/mocks/*.js',
    ]});
};

运行单元测试的命令行

karma start karma.config.js

测试结果

演示

结论

Chrome 应用程序是 Web 应用程序和桌面应用程序之间的一个很好的折衷方案。当然,它不能适用于所有类型的应用程序,所以您的选择必须是合理的。

感谢您的阅读。我非常感谢您的评论以及在 GitHub 和 Code Project 上的点赞。

 

© . All rights reserved.