Это первая из серии публикаций, где я расскажу о простом приложении, которое позволяет вам анализировать и представлять данные интересными способами. Я напишу хороший клиент AngularJS с подключением WebSocket к бэкэнду Haskell.
Если вы нетерпеливы, возьмите исходный код с https://github.com/janm399/hwsexp ; и запустите, cabal run
чтобы запустить сервер WebSocket. Чтобы увидеть (на данный момент примитивный) пользовательский интерфейс, откройте web/numbers.html
в современном браузере. Вы увидите (в стиле фанк и трогательно) представление HTML.
$ cabal run Preprocessing library hwssexp-0.1.0.0... In-place registering hwssexp-0.1.0.0... Preprocessing executable 'ws' for hwssexp-0.1.0.0... Server is running
Код на Haskell
Наш серверный код на Haskell прослушивает все локальные адреса на порту 9160 для соединений WebSocket. Мы также хотели бы поддерживать состояние, которое представляет собой список подключенных сеансов. В течение срока службы сервера мы будем изменять это состояние, которое разделяется между потоками.
-- |The main entry point for the WS application main :: IO () main = do putStrLn "Server is running" state < - newMVar newServerState WS.runServer "0.0.0.0" 9160 $ application state
Оставляя в putStrLn
стороне очевидное , мы создаем состояние MVar ServerState
, которое является состоянием при запуске сервера. Затем мы используем ServerState
значение, когда затем (вспомним, как работает Haskell!) Запускается сервер WebSocket. Состояние, которое сохраняет наш сервер, представляет собой список запросов, которые пользователь отправил вместе с соединением WebSocket, на которое сервер отправит результаты. Это дает нам хорошее место для определения этих типов вместе с newServerState
функцией.
-- |Client is a combination of the statement that we're running and the
-- WS connection that we can send results to
type Client = (Text, WS.Connection)
-- |Server state is simply an array of active @Client@s
type ServerState = [Client]
-- |Named function that retuns an empty @ServerState@
newServerState :: ServerState
newServerState = []
Большой; Чтобы завершить картину, давайте добавим функции, которые позволяют нам добавлять и удалять клиентов.
-- |Adds new client to the server state addClient :: Client -- ^ The client to be added -> ServerState -- ^ The current state -> ServerState -- ^ The state with the client added addClient client clients = client : clients -- |Removes an existing client from the server state removeClient :: Client -- ^ The client being removed -> ServerState -- ^ The current state -> ServerState -- ^ The state with the client removed removeClient client = filter ((/= fst client) . fst)
Теперь у нас есть весь вспомогательный код; все, что нам нужно сделать, это обеспечить реализацию application
функции; эта функция представляет то, что мы называем контроллером в старом мире. Он получает запросы и, как побочный эффект, может изменять состояние сервера и давать ответы.
-- |The handler for the application's work application :: MVar ServerState -- ^ The server state -> WS.ServerApp -- ^ The server app that will handle the work application state pending = do conn < - WS.acceptRequest pending query <- WS.receiveData conn clients <- liftIO $ readMVar state let client = (query, conn) modifyMVar_ state $ return . addClient client perform state client
Сначала мы принимаем запрос (мы принимаем любые запросы WS), давая нам Connection
значение; Затем мы получаем данные, которые клиент отправил нам String
. Наконец, мы извлекаем ServerState
значение из общего ресурса MVar ServerState
.
Мы создаем Client
значение: кортеж, содержащий запрос и соединение WebSocket для этого запроса. Довольно сложная линия modifyMVar_ state $ return . addClient client
. Мы модифицируем общий доступ ServerState
. Если мы расширим выражение, исключив бессмысленный стиль, мы получим
modifyMVar_ state (\s' -> return (addClient client s'))
Мы можем исключить s'
в (\s' -> return ... s')
уравнении: return . addClient client
это одно и то же: функция, которая принимает ServerState
и возвращает IO ServerState
. Отлично. Наконец, мы можем удалить заключительные скобки, используя ($)
функцию. Это дает нам финал
modifyMVar_ state $ return . addClient client
Это оставляет нам только последнее выражение; тот, который на самом деле делает работу. (Мы будем держать эту часть довольно легкой на данном этапе, но улучшим ее в течение следующих нескольких постов; и поверьте мне, есть много улучшений!)
-- |Performs the query on behalf of the client, -- cleaning up after itself when the client disconnects perform :: MVar ServerState -- ^ The server state -> Client -- ^ The query to perform and the conn for results -> IO () -- ^ The output perform state client@(query, conn) = handle catchDisconnect $ forever $ do numbers < - replicateM 100 ((`mod` 100) <$> randomIO :: IO Int) WS.sendTextData conn (T.pack $ show numbers) threadDelay 1000000 where catchDisconnect :: SomeException -> IO () catchDisconnect _ = liftIO $ modifyMVar_ state $ return . removeClient client
Рассекая код, мы оборачиваем вечно повторяющиеся вычисления в обработчик исключений. Это дает нам основную форму кода. В блоке forever мы генерируем 100 случайных чисел, каждое в диапазоне от 0 до 100, которые мы отправляем в прослушивающий WebSocket. Затем (о человечество! Подробнее об этом в будущем.) Мы спим в течение 1 секунды, а затем повторяем. Блок catch обрабатывает любые исключения, удаляя клиента из общего ресурса сервера ServerState
.
Веб-приложение
Продолжим, давайте взломаем хорошее веб-приложение AngularJS. Мы хотим подключиться к нашему серверу на Haskell, а затем отобразить числа, которые мы получаем, в текстовом поле, а также — используя D3.js — в красивой диаграмме.
<!doctype html> <html> <head> <title>Number cruncher</title> ... </head> <body> <div ng-app="numbers.app" ng-controller="NumbersCtrl"> <tabs> <pane title="Raw"> <h3>Raw data</h3> <pre>{{numbers}}</pre> </pane> <pane title="Canvas"> <h3>Visual representation</h3> <barchart2d data="{{numbers}}"/> </pane> </tabs> </div> </body> </html>
И это все, что нужно сделать — хорошо, если вы решите игнорировать магию AngularJS, в частности, NumbersCtrl
контроллер и barchart2d
компонент. Стоит изучить их чуть подробнее, начиная с NumbersCtrl
.
angular.module('numbers.app', ['d3.directives', 'numbers.directives']) .controller('NumbersCtrl', ['$scope', function($scope) { function createWebSocket(path) { var host = window.location.hostname; if (host == '') host = 'localhost'; var uri = 'ws://' + host + ':9160' + path; var Socket = "MozWebSocket" in window ? MozWebSocket : WebSocket; return new Socket(uri); } $scope.numbers = {}; var socket = createWebSocket('/'); socket.onopen = function() { // we'll have that in the next session socket.send("even 0-100 every 1s"); }; socket.onmessage = function(e) { $scope.$apply(function() { $scope.numbers = e.data; }); }; }]);
Это код нашего приложения AngularJS. Это зависит d3.directives
и от numbers.directives
модулей; Эти модули содержат директивы (компоненты Think) для диаграмм D3 и нашего элемента управления вкладками. Директивы tab являются необработанным примером AngularJS, поэтому давайте рассмотрим компоненты D3. Мы разделили его на два модуля: один, который предоставляет d3
услугу (используя D3 JavaScript), а затем модуль, который предоставляет компоненты.
// creates the d3.core module, which contains the d3Service angular.module('d3.core', []) // creates d3Service by injecting the D3JS JavaScript to the document .factory('d3Service', ['$document', '$q', '$rootScope', function($document, $q, $rootScope) { ... return { d3: ... // the d3 namespace }; }]);
Захватите код с https://github.com/janm399/hwsexp для получения полной информации. d3.directives
Модуль обеспечивает barchart2d
для нас использование:
// creates the d3.core module, which contains the various D3 charts angular.module('d3.directives', ['d3.core']) .directive('barchart2d', ['d3Service', function(d3Service) { return { restrict: 'E', transclude: true, scope: { data: '@' }, template: '<div class="barchart2d" ng-transclude=""></div>', replace: true, link: function(scope, element, attrs) { d3Service.d3().then(function(d3) { function fmt(element, x) { element.style("width", function(d) { return x(d) + "px"; }) .text(function(d) { return d; }); } attrs.$observe('data', function(rawValue) { var data = JSON.parse(rawValue); var x = d3.scale.linear() .domain([0, d3.max(data)]) .range([0, 420]); var p = d3.select(element[0]).selectAll("div").data(data); fmt(p.enter().append("div"), x); fmt(p.transition(), x); p.exit().remove(); }); }); } }; }]);
Опять же, это базовая диаграмма D3, небольшая галочка включает использование attrs.$observe
для подключения к изменениям данной модели.
Резюме
И там у вас есть это. Вы можете запустить сервер WebSocket на основе Haskell, а затем иметь современное веб-приложение, которое отображает выходные данные, отправленные сервером Haskell. Теперь, когда у нас есть основные строительные блоки, мы будем добавлять больше функций, особенно для анализа запроса, который вводят пользователи, а затем его выполнения.