Pull to refresh

ExtJS 7 и Spring Boot 2. Как построить SPA, взаимодействующее с вашим API и внешними ReactJS плагинами?

Reading time3 min
Views6.2K
Последние версии Ext JS, особенно Modern Toolkit снизили порог вхождения во фреймворк (примеры Kitchen Sink), упростили создание нужного интерфейса (привет Sencha Architect) и добились минимального размера веб-приложений (Sencha Cmd).

Пожалуй, Хабр нужно разбавить примером реализации «ситуационного центра», где в реальном времени можно наблюдать камеры и события с них (все данные фейковые).



Начнем. Создадим Spring Boot проект с 2 контроллерами, которые будут отдавать список камер, существующие события, а также возможность подписки на новые события (через WebSocket).


Далее добавим необходимые модели, хранилища, вьюшки и внешние зависимости:



Более детально вьюшка карты, взаимодействующая с React компонентом карты.
Ext.define('Cameras.view.override.Map', {
    override: 'Cameras.view.Map',
    
    config: {
        cameras: {},
        react: null
    },
    
    initialize: function() {
        let that = this;
        let e = React.createElement;
        
        this.setReact(ReactDOM.render(e(createReactClass({
            getInitialState: function() {
                return { items: [], center: [55.751574, 37.573856]};
            },
            render: function() {
                let placemarks = [];

                for(let i=0; i < this.state.items.length; i++) {
                    let location = this.state.items[i].get("location");

                    placemarks.push(e(window.ReactYandexMaps.Placemark, {
                        geometry: [location.latitude, location.longitude],
                            options: {
                                preset: 'islands#blueCircleDotIconWithCaption',
                                iconCaptionMaxWidth: '50'
                            }
                        }));
                }
        

                let map = e(window.ReactYandexMaps.Map, {
                    state: { center: this.state.center, zoom: 10 },
                    width: '100%',
                    height: '100%'
                }, placemarks);

                return e(window.ReactYandexMaps.YMaps, null, map);
            }
        })), this.mapContainer.dom));
    },
    
    getElementConfig: function() {
        return {
            reference: 'element',
            className: 'x-container',
            children: [{
                reference: 'bodyElement',
                style: 'width: 100%; height: 100%',
                className: 'x-inner',
                children: [{
                    style: 'width: 100%; height: 100%',
                    reference: 'mapContainer',
                    className: Ext.baseCSSPrefix + 'map-container'
                }]
            }]
        };
    },
    
    addCamera: function(cameraModel) {    
        if(!this.containsCamera(cameraModel)) {
            this.getCameras()[cameraModel.get("id")] = cameraModel;
            
            this.getReact().setState({
                items: Object.values(this.getCameras())
            });
            
            this.fitCamera(cameraModel);
        }
    },
    
    removeCamera: function(cameraModel) {
        if(this.containsCamera(cameraModel)) {
            delete this.getCameras()[cameraModel.get("id")];
            
            this.getReact().setState({
                items: Object.values(this.getCameras())
            });
        }
    },
    
    fitCamera: function(cameraModel) {
        if(this.containsCamera(cameraModel)) {
            let location = this.getCameras()[cameraModel.get("id")].get("location");
            
            this.getReact().setState({
                center: [location.latitude, location.longitude]
            });
        }
    },
    
    privates: {
        containsCamera: function(cameraModel) {
            cameraId = "" + cameraModel.get("id");
        
            return Object.keys(this.getCameras()).includes(cameraId);
        }
    }
});


Также условимся, что события будут исходить от компонента CamerasGrid, т.к. именно этот компонент отвечает за добавление/удаление камер с карты.

Контроллер компонента CamerasGrid, добавляющий компоненту генерацию событий
Ext.define('Cameras.view.CamerasGridViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.camerasgrid',

    init: function() {
        let socket = new WebSocket("ws://localhost:8080/events/sub");

        socket.onopen = function(e) {
            console.log('onopen');
        };

        socket.onmessage = this.onMessage.bind(this);
    },

    onMessage: function(event) {
        let data = Ext.decode(event.data);

        let gridData = this.getView().getStore().getData();

        for(let i=0; i < gridData.length; i++) {
            let checked = gridData.getAt(i).get("checked");

            if(checked !== undefined && checked) {
                if(gridData.getAt(i).get("id") == data.camera.id) {
                    this.fireViewEvent("cameraRecognition", data);
                }
            }
        }
    }
});


В итоге получается довольно занятный интерфейс. Замечу, что при правильном проектировании архитектуры приложения (даже такого маленького), время на создание оного совсем невелико, до пары часов.



Код примера доступен на github.com.
Tags:
Hubs:
Total votes 2: ↑1 and ↓1+2
Comments1

Articles