JavaScript для… настольного IP телефона?



    Немногим известно, что IP телефоны Digium — это не совсем обычные телефоны. Казалось бы зачем производителю выпускать аппараты собственной марки на таком насыщенном и низкомаржинальном рынке? Но поверьте — оно того стоило. Помимо отличных физических характеристик: приятный пластик, яркий экран, отличные динамики и микрофон. Эти аппараты обладают своим API и вы можете написать свое приложение под них!

    Кто другой может похвастаться таким функционалом?




    Написание собственного приложения под новую платформу это не только интересно и познавательно, но и полезно! Я как-то говорил в предыдущем обзоре, что идея приложения может ограничиться только вашей фантазией.

    Моя фантазия посчитала, что полезными для настольного IP телефона могут оказаться следующие вещи:

    1) Синхронизация контактов с Google аккаунтом
    2) Синхронизация контактов Facebook, VK
    3) Вывод курса валют с ЦБ
    4) Оповещения о непрочитанных email (интеграция с почтовиками по POP3)
    5) Приложения погоды (с русскоязычных Gismeteo, yandex и т.п.)
    6) синхронизация контактов с LDAP
    7) приложение, которое дает супервайзеру подслушать или вклиниться в разговор сотрудника.

    Последние два пункта — это конечно очень «круто», но боюсь предназначена только для гуру JavaScript и астерискера по совместительству.

    Я бы с удовольствием прочел о Ваших идеях таких вот апплетов.

    Приложение погоды с GISMETEO




    Решено было начать с самого простого. Виджета погоды.
    Ознакомившись в документацией и примерами получился такой вот код:

    Informer.js
    // Lets include the app (for standard settings option):
    var app = require('app');
    
    // And initialize it as required:
    app.init();
    
    // Our select tool input:
    var SelectInput = require('SelectInput').SelectInput;
    
    // Include some utilities:
    var util = require('util');
    
    // Screen instance:
    var screen = require('screen');
    
    // Idle window reference;
    var wnd = null;
    
    // On foreground event call:
    var onforeground_called = false;
    
    // On foreground event call to display error:
    var onforeground_error = false;
    
    // If app is already running it has true value:
    var instantiated = false;
    
    // Default city for "no-config" app:
    var DEF_CITY_ID = 27612;
    
    // Screen update interval (for 4 different forecasts)
    var DEF_FORECAST_SCREEN_INT = 15000;
    
    // Server request interval (to update weather data):
    var DEF_FORECAST_UPDATE_INT = 3600000;
    
    // Server request interval (to update weather data in case of error):
    var DEF_FORECAST_ERROR_UPDATE_INT = 60000;
    
    // App runtime configuration:
    var config = {};
    
    // Runtime gathered weather data:
    var weather_data = {};
    
    // Which forecast should display next refresh;
    var forecast_display = 0;
    
    // Count of available forecasts:
    var forecasts_count = 0;
    
    // Timer id of weather update request timeout
    var timeout_request = null;
    
    // Timer id of forecast display refresh timeout
    var timeout_refresh = null;
    
    // Extended time of day explanations:
    var tod_extended = {
    	'0': ['Утром', 'Днём', 'Вечером'],
    	'1': ['Днём', 'Вечером', 'Ночью'],
    	'2': ['Вечером', 'Ночью', 'Завтра утром'],
    	'3': ['Ночью', 'Завтра утром', 'Завтра днём']
    };
    
    // Weekday russian shortcuts (Gismeteo format):
    var week_days = {
    	'1':'вс',
    	'2':'пн',
    	'3':'вт',
    	'4':'ср',
    	'5':'чт',
    	'6':'пт',
    	'7':'сб'
    };
    
    // Gismeteo wind direction map:
    var wind_dir = {
    	'0':'C',
    	'1':'СВ',
    	'2':'В',
    	'3':'ЮВ',
    	'4':'Ю',
    	'5':'ЮЗ',
    	'6':'З',
    	'7':'СЗ'
    };
    
    // Decode CP1251 (name of city):
    function decode1251 (str) {
    	var i, result = '', l = str.length, cc;
    
    	for (i = 0; i < l; i++) {
    		cc = str.charCodeAt(i);
    
    		if ('\r' == str[i]) continue;
    
    		if (cc < 192) {
    			if (168 == cc)
    				result += 'Ё';
    			else if (184 == cc)
    				result += 'ё';
    			else
    				result += String.fromCharCode(cc);
    		}
    		else
    			result += String.fromCharCode(cc + 848);
    	}
    
    	return result;
    }
    
    // Just a little workaround for IDE:
    function endForegroundMonitor () {
    	onforeground_called	= false;
    	onforeground_error		= false;
    }
    
    // Show window for selecting user-defined city:
    function showFormCities () {
    	if (!onforeground_called) return;
    
    	digium.event.stopObserving({'eventName'	: 'digium.app.background'});
    
    	digium.event.observe({
    		'eventName'	: 'digium.app.background',
    		'callback'	: function () {
    			endForegroundMonitor();
    			digium.event.stopObserving({'eventName'	: 'digium.app.background'});
    		}
    	});
    
    	screen.clear();
    
    	var i, select_options = [];
    
    	for (i in cities) {
    		if (cities.hasOwnProperty(i))
    			select_options.push({'value':i,'display':cities[i]});
    	}
    
    	var select = new SelectInput({
    		'options':select_options,
    		'width':window.w - 120,
    		'title':'Выберите город из списка',
    		'x':100,
    		'y':20+Text.LINE_HEIGHT,
    		'initialValue':config['CITY_ID'].toString()
    	});
    
    	select.onFocus = function(){return true};
    
    	window.add(screen.setTitleText({'title' : 'Настройка местоположения'}));
    	window.add(new Text(20, 20+Text.LINE_HEIGHT, 70, Text.LINE_HEIGHT, 'Ваш город:'));
    	window.add(select.textWidget);
    
    	select.takeFocus();
    
    	select.textWidget.setSoftkey(4, 'Назад', showAppWindow);
    
    	select.textWidget.setSoftkey(1, 'Готово', function(){
    		config['CITY_ID'] = parseInt(select.value);
    		confirmUserCity();
    		updateWeatherData();
    		digium.background();
    	});
    }
    
    // Saving user's choice:
    function confirmUserCity () {
    	try {
    		digium.writeFile(
    			'nv',
    			'settings.json',
    			JSON.stringify({'CITY_ID':config['CITY_ID']})
    		);
    	}
    	catch(e) {}
    }
    
    // Getting app-level configuration:
    function getApplicationConfig () {
    	return util.defaults(app.getConfig().settings, {
    		'CITY_ID':DEF_CITY_ID,
    		'FORECAST_SCREEN_INT':DEF_FORECAST_SCREEN_INT,
    		'FORECAST_UPDATE_INT':DEF_FORECAST_UPDATE_INT
    	});
    }
    
    // Getting user-level configuration:
    function getLocalConfig () {
    	var result;
    
    	try {
    		var configFile = digium.readFile('nv', 'settings.json');
    
    		result = JSON.parse(configFile);
    	} catch (e) {
    		result = {};
    	}
    
    	return result;
    }
    
    // Function-helper for parsing single node attrs:
    function getMappedAttributes (node, map) {
    	var i, result = {};
    
    	for (i in map) {
    		if (map.hasOwnProperty(i))
    			result[map[i]] = node.getAttribute(i);
    	}
    
    	return result;
    }
    
    // Parse xml weather data to object:
    function parseWeatherData (src) {
    	var data = {}, nodes, node, l, i, subnode;
    
    	var parser = new DOMParser();
    
    	try {
    		var doc = parser.parseFromString(src, 'application/xml');
    
    		nodes = doc.getElementsByTagName('TOWN');
    
    		data.city = decode1251(unescape(nodes[0].getAttribute('sname'))).replace('+', ' ');
    
    		nodes = doc.getElementsByTagName('FORECAST');
    
    		l = nodes.length; i = 0;
    
    		forecasts_count = l;
    
    		node = nodes[0];
    
    		data.day = node.getAttribute('day');
    		data.month = node.getAttribute('month');
    		data.weekday = node.getAttribute('weekday');
    		data.tod = node.getAttribute('tod');
    		data.forecasts = {};
    
    		var forecast, tmp;
    
    		do {
    			forecast = {};
    
    			subnode = node.getElementsByTagName('PHENOMENA')[0];
    
    			forecast = util.defaults(forecast, getMappedAttributes(subnode, {
    				'cloudiness':'clouds',
    				'precipitation':'precip',
    				'rpower':'rpower',
    				'spower':'spower'
    			}));
    
    			subnode = node.getElementsByTagName('PRESSURE')[0];
    
    			tmp =  getMappedAttributes(subnode, {
    				'min':'min',
    				'max':'max'
    			});
    
    			forecast.press = tmp.min + '-' + tmp.max;
    
    			subnode = node.getElementsByTagName('TEMPERATURE')[0];
    
    			tmp =  getMappedAttributes(subnode, {
    				'min':'min',
    				'max':'max'
    			});
    
    			forecast.temp = parseInt((parseInt(tmp.min) + parseInt(tmp.max)) / 2);
    
    			subnode = node.getElementsByTagName('WIND')[0];
    
    			tmp =  getMappedAttributes(subnode, {
    				'min':'min',
    				'max':'max',
    				'direction':'wdir'
    			});
    
    			forecast.wspeed = parseInt((parseInt(tmp.min) + parseInt(tmp.max)) / 2);
    			forecast.wdir = tmp.wdir;
    
    			subnode = node.getElementsByTagName('RELWET')[0];
    
    			forecast = util.defaults(forecast, getMappedAttributes(subnode, {
    				'max':'hum'
    			}));
    
    			data.forecasts[i] = forecast;
    
    			node = nodes[++i];
    		}
    		while (i < l);
    	}
    	catch(e) {
    		data = {error:true};
    	}
    
    	return data;
    }
    
    // Currently disabled (show error message in window of the app)
    /*function reportError (info) {
    	endForegroundMonitor();
    
    	onforeground_error = true;
    
    	digium.foreground();
    
    	screen.clear();
    
    	window.add(new Text(0, 0, window.w,Text.LINE_HEIGHT * 2, info.type + ': ' + info.description));
    
    	digium.event.stopObserving({'eventName'	: 'digium.app.background'});
    
    	digium.event.observe({
    		'eventName'	: 'digium.app.background',
    		'callback'	: function () {
    			endForegroundMonitor();
    			digium.event.stopObserving({'eventName'	: 'digium.app.background'});
    		}
    	});
    }*/
    
    // Get current no of forecast to display:
    function getForecastToDisplay () {
    	if (forecast_display > (forecasts_count - 1) )
    		return forecast_display = 0;
    	else
    		return forecast_display++;
    }
    
    // Get labels values (with updated data):
    function getUpdatedLabels (forecast_no) {
    	var result = {}, time = '';
    
    	var forecast = weather_data.forecasts[forecast_no];
    
    	if (!forecast_no) time = 'Сейчас';
    	else time = tod_extended[weather_data.tod][forecast_no - 1];
    
    	result.temp = time + ' ' + forecast.temp + ' °C';
    	result.press = forecast.press + ' мм рт. ст.';
    	result.hum = 'Влажность ' + forecast.hum + '%';
    	result.wind = 'Ветер ' + forecast.wspeed + ' м/с ' + wind_dir[forecast.wdir];
    
    	return result;
    }
    
    // "Humanize" Gismeteo phenomena:
    function getPhenomenaObj (forecast, nt) {
    	// Default state of the phenomena part:
    	var result = {icon:'unknown',status:'Нет данных'};
    
    	if ((1 == forecast.spower) &&
    		((8 == forecast.precip) || (9 == forecast.precip))) {
    
    		result.icon = 'storm';
    		result.status = 'Грозы';
    	}
    	else if (5 == forecast.precip) {
    		result.icon = 'rainfall';
    		result.status = 'Ливень';
    	}
    	else if (4 == forecast.precip) {
    		result.icon = 'rain';
    		result.status = 'Дождь';
    	}
    	else if ((6 == forecast.precip) || (7 == forecast.precip)) {
    		result.icon = 'snow';
    		result.status = 'Снег';
    	}
    	else if (3 == forecast.clouds) {
    		result.icon = 'mostlycloudy';
    		result.status = 'Пасмурно';
    	}
    	else if (2 == forecast.clouds) {
    		result.icon = 'cloudy';
    		result.status = 'Облачно';
    	}
    	else if ((1 == forecast.clouds) && (10 == forecast.precip)) {
    		if (nt)
    			result.icon = 'nt_mostlyclear';
    		else
    			result.icon = 'mostlyclear';
    
    		result.status = 'Перем. облач.';
    	}
    	else if ((0 == forecast.clouds) && (10 == forecast.precip)) {
    		if (nt)
    			result.icon = 'nt_clear';
    		else
    			result.icon = 'clear';
    
    		result.status = 'Ясно';
    	}
    
    	return result;
    }
    
    // Refresh widget contents on the idle display:
    function idleRefresh (on_timer) {
    	var labels, fno, f, nt = false;
    
    	if (util.isDef(on_timer) && on_timer)
    	{
    		fno = getForecastToDisplay();
    		labels = getUpdatedLabels(fno);
    	}
    	else {
    		forecast_display = 0;
    		fno = forecast_display++;
    		labels = getUpdatedLabels(fno);
    	}
    
    	f = weather_data.forecasts[fno];
    
    	if (weather_data.tod == 0 && fno == 0)
    		nt = true;
    	else if (weather_data.tod == 1 && fno == 3)
    		nt = true;
    	else if (weather_data.tod == 2 && fno == 2)
    		nt = true;
    	else if (weather_data.tod == 3 && fno == 1)
    		nt = true;
    
    	var phen = getPhenomenaObj(f, nt);
    
    	wnd[0] = new Image('app', phen.icon + '.gif', 15, Text.LINE_HEIGHT, 50, 50);
    	wnd[2].label = labels.temp;
    	wnd[3].label = phen.status;
    	wnd[4].label = labels.press;
    	wnd[5].label = labels.hum;
    	wnd[6].label = labels.wind;
    
    	clearTimeout(timeout_refresh);
    
    	timeout_refresh = setTimeout(function(){
    		idleRefresh(true);
    	}, config['FORECAST_SCREEN_INT']);
    }
    
    // Function to finalize "get weather request"
    function getWeatherCb (data) {
    	if (data.hasOwnProperty('error') && data.error) {
    		wnd[1].label = 'Обновление данных...';
    		setTimeout(updateWeatherData, DEF_FORECAST_ERROR_UPDATE_INT);
    	}
    	else {
    		weather_data = data; // overwrite previous data
    
    		wnd[1].label = data.city + ', ' + week_days[data.weekday] +
    			' ' + data.day + '.' + data.month;
    
    		idleRefresh();
    	}
    }
    
    // Get weather data from Gismeteo:
    function getWeather (cb) {
    	wnd[1].label = 'Обновление данных...';
    
    	var uri = 'http://informer.gismeteo.ru/xml/' + config['CITY_ID'] + '.xml';
    
    	var request = new NetRequest();
    
    	request.open('GET', uri, true);
    
    	request.onreadystatechange = function () {
    		if (4 == request.readyState) {
    			if (200 == request.status)
    				cb(parseWeatherData(request.responseText));
    			else {
    				setTimeout(updateWeatherData, DEF_FORECAST_ERROR_UPDATE_INT);
    			}
    		}
    	};
    
    	request.send();
    
    	clearTimeout(timeout_request);
    
    	timeout_request = setTimeout(
    		updateWeatherData,
    		config['FORECAST_UPDATE_INT']
    	);
    }
    
    // Function for usage in setTimeout func:
    function updateWeatherData () {
    	getWeather(getWeatherCb);
    }
    
    // Idle window initialization and reference store;
    function initialize () {
    	var cursor = 0, label;
    
    	wnd = digium.app.idleWindow;
    
    	wnd.add(new Image('app', 'unknown.gif', 15, Text.LINE_HEIGHT, 50, 50));
    
    	for (var i = 0; i < 6; i++) {
    		if (0 == i)
    			label = new Text(0, Text.LINE_HEIGHT * cursor++, wnd.w, Text.LINE_HEIGHT, 'Получение данных...');
    		else if (2 == i)
    			label = new Text(10, 50, 65, Text.LINE_HEIGHT, 'Нет данных');
    		else
    			label = new Text(65, Text.LINE_HEIGHT * cursor++, wnd.w - 65, Text.LINE_HEIGHT);
    
    		label.align(Widget.CENTER);
    
    		wnd.add(label);
    	}
    
    	digium.app.exitAfterBackground = false;
    
    	wnd.hideBottomBar = true;
    
    	digium.event.observe({
    		'eventName'	: 'digium.app.start',
    		'callback'	: function () {
    			setTimeout(function(){instantiated = true;}, 1000);
    		}
    	});
    
    	digium.event.observe({
    		'eventName'	: 'digium.app.idle_screen_show',
    		'callback'	: function () {
    			if (digium.app.idleWindowShown)
    				idleRefresh();
    		}
    	});
    
    	digium.event.observe({
    		'eventName'	: 'digium.app.foreground',
    		'callback'	: function () {
    			// Stopping recursive call when error message box
    			// should be shown by calling digium.foreground()
    			if (onforeground_called) return ;
    
    			onforeground_called = true;
    
    			if (!instantiated) {
    				// bring app to idle on the first launch:
    				digium.background();
    				instantiated = true;
    				endForegroundMonitor();
    			}
    			else {
    				// Show select options list: 1. setting up; 2. exit widget
    				// showFormCities();
    				showAppWindow();
    			}
    		}
    	});
    }
    
    function showAppWindow () {
    	if (!onforeground_called) return;
    
    	digium.event.stopObserving({'eventName'	: 'digium.app.background'});
    
    	digium.event.observe({
    		'eventName'	: 'digium.app.background',
    		'callback'	: function () {
    			endForegroundMonitor();
    			digium.event.stopObserving({'eventName'	: 'digium.app.background'});
    		}
    	});
    
    	screen.clear();
    
    	window.add(screen.setTitleText({'title' : 'Погода'}));
    
    	try {
    		window.add(new Text(4, 20, window.w, Text.LINE_HEIGHT, 'Выберите опцию:'));
    
    		var menuObj = new List(4, 20 + Text.LINE_HEIGHT, window.w, window.h);
    
    		menuObj.setProp('cols', 2).setProp('rows', 2);
    
    		menuObj.set(0, 0, '1.');
    		menuObj.set(0, 1, 'Выбрать город');
    		menuObj.set(1, 0, '2.');
    		menuObj.set(1, 1, 'Закрыть виджет');
    
    		menuObj.setColumnWidths(15, 0);
    		menuObj.select(0);
    
    		menuObj.onFocus = function() {return true; };
    
    		var selected = function() {
    			if (0 == menuObj.selected)
    				showFormCities();
    			else {
    				digium.app.exitAfterBackground = true;
    				digium.background();
    			}
    		};
    
    		menuObj.onkey1 = function(){ menuObj.select(0); selected(); };
    		menuObj.onkey2 = function(){ menuObj.select(1); selected(); };
    
    		menuObj.onkeyselect = selected;
    
    		menuObj.setSoftkey(1, 'OK', selected);
    
    		window.add(menuObj);
    
    		menuObj.takeFocus();
    	}
    	catch(e) {
    		window.add(new Text(0, 20, window.w, Text.LINE_HEIGHT, e.message));
    	}
    }
    
    // Get summary configuration data with user-level priority:
    config = util.defaults(getLocalConfig(), getApplicationConfig());
    
    // Initialize:
    initialize();
    
    // Start app main cycle:
    updateWeatherData();
    



    т.к. на кнопках телефона отсутствует кириллица, а набирать их как-то в поиске нужно, пришлось проделать некую манипуляцию: а именно получить весь список городов и их ID, перевести их на латиницу и создать файл базы данных для приложения.
    Выглядит так
    var cities = {"20476":"Mys Sterligova","20674":"Dikson","20744":"Malye Karmakuly","20864":"Tambey","20891":"Khatanga","20978":"Karaul","20982":"Volochanka","21802":"Saskylakh","21824":"Tiksi","21946":"Chokurdakh","22004":"Nikel","22019":"Polyarnyy","22028":"Teriberka","22112":"Kola","22113":"Murmansk","22127":"Lovozero","22204":"Kovdor","22213":"Apatity","22216":"Kirovsk (Murm.)","22217":"Kandalaksha","22324":"Umba","22408":"Kalevala","22429":"Solovki","22471":"Mezen","22522":"Kem-Port","22546":"Severodvinsk","22550":"Arkhangelsk","22559":"Kholmogory","22563":"Pinega","22573":"Leshukonskoe","22621":"Segezha","22641":"Onega","22671":"Karpogory","22686":"Vendinga","22695":"Koslan","22717":"Suoyarvi","22721":"Medvezhegorsk","22727":"Kondopoga","22762":"Dvinskoy Bereznik","22768":"Shenkursk","22778":"Verkhnyaya Toyma","22798":"Yarensk","22802":"Sortavala","22807"
    

    и тд и тп.


    Если коротко, то можно охарактеризовать разработку следующим образом:

    1. Плюсы.
    1.1 Javascript)
    1.2 Наличие для взаимодействия с вебом объекта Netrequest (аля XHR);
    1.3 Множество дополнителных объектов для выполнения рутинных задач и взаимодействия с предлагаемым API;
    1.4 Иерархия предлагаемых к использованию встроенных объектов;
    1.5 Механизм событий;
    1.6 Поддержка отладки;

    2. Минусы.
    2.1 Небольшое количество примеров по использованию API вкупе с минималистичным подходом к самой документации;
    2.2 Ограниченность в ресурсах — про парсинг большого кол-ва данных при помощи DomParser лучше даже не начинать думать, для обращения к небольшим XML вполне годен;
    2.3 Нестандартные названия для некоторых объектов и элементов управления (например, List, который, вообще говоря, может отображать таблицу), что вместе с п. 2.1 делает «знакомство» с API чуть более долгим;
    2.4 Нет встроенной «прокрутки» текста по таймеру при отображении в ограниченных для объёма текста размерах (пригодилось бы хотя бы в виджетах);
    2.5 Иерархия доступных объектов нечёткая, некоторые вещи излишни (в целом, идея не плоха, но рассматривать как плюс можно только если у разрабов есть план по развитию всего этого);

    3. Особенности.
    3.1 Наличие объектов как window, так и screen (при этом window на самом деле является виджетом Group, не кисло, правда? — см. пп. 2.5, 2.3);
    3.2 При разработке приложений для работы с веб нужно учитывать, что куки ответов для использования в javascript нельзя получить.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 11
    • +1
      У Nortel вроде подобные аппараты были лет 8 назад. Слышал о решении на их базе, когда в отеле через них много чего работало, от заказа уборки в номерах и до управления жалюзи и т.п.
      • +6
        >Кто другой может похвастаться таким функционалом?

        Cisco. Avaya. Наверное, и все остальные.
        • +2
          На моём IP-телефоне Note фирмы SЛMSUNG есть всё это и даже круче. И самих voip несколько видов, SIP включая.
          • 0
            Топовым решением, среди ip телефонах, согласно их сайта, у самсунга является SMT-i5243, если у вас именно этот телефон, то отпишитесь о впечатлениях — очень интересно, ну а если вы о sip клиенте в мобильнике, то это немного не то.
            • +1
              Топовыми у самсунга являются galaxy s5 и note 3. И это отличные VoIP устройства, с поддержкой такого числа протоколов, которое не снилась описываемой в посте штуке.

              Я даже не троллю сейчас — обычный телефон на андроиде функиональнее и выгоднее, чем любая железка с надписью VoIP.
          • +1
            Пока digium не выпустит русскую локаль, толку от этой фичи для обывателя будет мало.
            • +1
              Сравниваю я SPA525G и Digium D50\D70…
              В общем, если бы D50 стоил 5 тысячи рублей (как SPA502) — я бы его ещё рассматривал.
              А так складывается ощущение, что дигиум зажрался.
              И цисколинксисы прекрасно работают с астериском, практически без допиливания. Провайжнинг, LLDP\CDP, войсвлан — всё умеют, с коммутаторами HP дружат, работают в огромном количестве и не жужжат.

              За что переплачивать — не понятно.
              • 0
                Как это не понятно? За погоду на экране! :)
              • –2
                >> 4) Оповещения о непрочитанных email (интеграция с почтовиками по POP3)
                Поправьте ели ошибаюсь, но pop3 не умеет показывать непрочитанные — нужен imap.
                • 0
                  Прекрасная штука, но API всё же хуже у ней, чем у Node.js; например, нельзя переменной exports присваивать объект, и приводится поэтому такой пример:

                  var example2 = {};
                  example2.init = function () {
                     this.status = 'Status set by example2.init()';
                  };
                  example2.getStatus = function () {
                     return this.status;
                  };
                  example2.setStatus = function (param) {
                     this.status = param;
                  };
                   
                  example2.init();
                  exports.getStatus = example2.getStatus.bind(example2);
                  exports.setStatus = example2.setStatus.bind(example2);
                  

                  Работать можно и с этим, но возрастает вероятность случайно пропустить bind и облажаться.

                  Необходимость заклинания «"type": "foreground"» в app.json также вызывает недоумение («ну почему нельзя было по умолчанию сделать нормальное значение, да и всё»).
                  • 0
                    А с чего API телефона вообще должно быть лучше/не хуже node.js?

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое