Статьи

Создание собственного веб-приложения доступности iPhone

Прежде чем я начну, приведу небольшой отказ от ответственности: то, что я строю здесь, полностью для развлечения и потому что я подумал, что это может быть интересно. Я критикую и совершенствую форму, созданную людьми умнее меня и более чем адекватными для 99,99% пользователей. По сути, я увидел то, что хотел построить, и сделал это.

В настоящее время я являюсь владельцем телефона HTC M8 — мое возвращение в Android после использования iPhone для нескольких версий. Мне нравится пользовательский интерфейс HTC, и в целом телефон был довольно невероятным, но после последнего обновления Android OS мой телефон начал становиться все более и более вялым. Дошло до того, что для отклика телефона потребовалось бы 30-60 секунд. Телефонные звонки, от которых я не получаю много, были еще хуже. Когда я пропустил звонок, потому что основной пользовательский интерфейс моего телефона не отвечал, я чуть не бросил его в бассейн. Я перепробовал много вещей, но в итоге вытер телефон и восстановился из резервной копии. Это «помогло», но телефон все еще толстый. Я решил, что пришло время переключиться на iOS, и решил, что iPhone 6S + будет отличным телефоном. Я также решил, что новая программа обновления iPhoneбыло бы хорошо подходит. Из того, что я прочитал, это лучше, чем следующая программа ATT. Единственная проблема заключается в том, что вы должны пойти в магазин Apple, чтобы подписаться на программу. Мой ближайший магазин Apple находится в Батон-Руж, примерно в часе езды. Стоит подвезти, но только если я знаю, у меня будет устройство, которое можно забрать.

К счастью, у Apple есть классная форма, которую вы можете использовать, чтобы узнать, доступен ли нужный телефон. Вы выбираете свой штат, свой магазин, свою модель, а затем свой перевозчик:

Shot1

Как видите, ни один не доступен. (Вздох.) Конечно, вы можете переключиться на SIM-карту (и я проверил, что мой HTC и 6S + используют SIM-карты одного типа). В этой форме меня беспокоило несколько вопросов.

  • Прежде всего — вы не можете использовать его до 8 утра. Нет, подожди, перестань смеяться, я серьезно. Это веб-система с «открытыми» часами, похожая на розничный магазин. Там, вероятно, причина данных для этого. Я говорил с представителем Apple на прошлой неделе, и они упомянули, что они получают новые данные инвентаризации в 8. Я хотел бы вообразить, что магазины Apple имеют некоторый изощренный крючок в инвентаре в реальном времени, но это, вероятно, не тот случай. Тем не менее, шокирующе видеть «закрытый» знак на веб-сайте.
  • Когда я был в Калифорнии на прошлой неделе, я пытался искать вокруг себя. Каждый раз, когда вы переключаете магазины, форма перестраивается. Поэтому, если я выбрал 6S + и ATT, я теряю эти выборы. Теперь причина этого имеет смысл. Возможно, что в другом магазине нет 6S + или ATT, но это все равно раздражает. Это та проблема, с которой умный интерфейсный код мог бы справиться изящно. В Южном Сан-Франциско вокруг меня было 5-6 магазинов, и я проверял там каждый день, и эти проклятые выпады раздражали меня каждый день. (Как я уже сказал сверху, я, вероятно, не являюсь целевым пользователем здесь.)
  • Наконец, было бы очень приятно, если бы я просто сказал: «Скажите мне, когда 6S + для ATT или без SIM-карты доступен в сером или серебристом цвете с 64 ГБ, поскольку 16 просто глупо». Но, очевидно, у Apple нет проблем с продажей iPhone, поэтому такая система, вероятно, не является для них приоритетной. (И чтобы быть ясно, это только для программы обновления. Очевидно, что «обычный» магазин позволяет вам купить прямо сейчас.)

Итак — скучно на этих выходных — я сделал то, что делает любой уважающий себя веб-разработчик — я открыл инструменты разработки, используя форму. Первое, что я заметил, было то, что приложение запускало JSON-файлы для запуска выпадающих меню:

Shot2

Затем я открыл каждый из этих файлов и посмотрел на JSON. stores.jsonбыл буквальный список всех магазинов с доступностью. Вот фрагмент кода:

"stores" : [ {
    "storeNumber" : "R414",
    "storeName" : "4th Street",
    "storeEnabled" : false,
    "storeState" : "California",
    "sellEdition" : false,
    "storeCity" : "Berkeley"
  }, {
    "storeNumber" : "R177",
    "storeName" : "ABQ Uptown",
    "storeEnabled" : true,
    "storeState" : "New Mexico",
    "sellEdition" : false,
    "storeCity" : "Albuquerque"
  }, {

availability.jsonбыли данные о доступности конечно. Вот фрагмент из этого:

  "R327" : {
    "MKVJ2LL/A" : "NONE",
    "MKQA2LL/A" : "ALL",
    "MKT62LL/A" : "ALL",
    "MKQX2LL/A" : "ALL",
    "MKR92LL/A" : "ALL",
    "MKVV2LL/A" : "NONE",
    "MKW72LL/A" : "NONE",
    "MKRQ2LL/A" : "ALL",
    "MKTM2LL/A" : "NONE",
    "MKQ62LL/A" : "ALL",
    "MKTA2LL/A" : "ALL",
    "MKT72LL/A" : "ALL",
    "MKRR2LL/A" : "ALL",
    "MKV32LL/A" : "NONE",
    "MKVW2LL/A" : "NONE",
    "MKW82LL/A" : "NONE",
    "MKTN2LL/A" : "NONE",
    "MKRE2LL/A" : "ALL",
    "MKR82LL/A" : "ALL",
    "MKWD2LL/A" : "NONE",
    "MKQ72LL/A" : "ALL",
    "MKRC2LL/A" : "ALL",
    "MKVX2LL/A" : "NONE",
    "MKW92LL/A" : "NONE",
    "MKVU2LL/A" : "ALL",
    "MKW62LL/A" : "NONE",
    "MKRF2LL/A" : "ALL",
    "MKUQ2LL/A" : "NONE",
    "MKV22LL/A" : "NONE",
    "MKQY2LL/A" : "ALL",
    "MKTY2LL/A" : "NONE",
    "MKV52LL/A" : "ALL",
    "MKT92LL/A" : "ALL",
    "MKT32LL/A" : "ALL",
    "MKQ82LL/A" : "ALL",
    "timeSlot" : {
      "en_US" : {
        "timeslotTime" : "11:00 AM",
        "contractTimeslotTime" : "11:00 AM"
      }
    },

Ключом является хранилище, и каждая позиция (кроме timeSlot) представляет позицию модели / цвета / носителя / размера. Так как я смог получить данные (щелкнуть правой кнопкой мыши в инструментах разработчика и открыть их на новой вкладке, а затем сохранить как), я начал работу над веб-приложением, которое позволило бы мне анализировать данные по-своему. В частности, я хотел несколько вещей:

  • Позвольте мне указать магазин, а затем несколько магазинов.
  • Позвольте мне указать любую модель, которую я хочу.
  • Позвольте мне указать несколько перевозчиков.

Я также хотел игнорировать 16 ГБ, но в конце концов решил против этого. Я начал работать над своим собственным кодом, который будет всасывать файлы JSON (моя локальная копия) и позволил мне разобрать его сам. Сначала я покажу результат, а затем расскажу о коде. И да — мой гораздо красивее, чем Apple.

shot3

Вверху вы можете увидеть раскрывающееся состояние и селектор магазина. Как я уже сказал, мой первоначальный план состоял в том, чтобы добавить несколько магазинов, но я так и не нашел этого.

Ниже вы можете увидеть выбор перевозчика и модели. Ниже это сетка вариантов. Я использовал CSS (woot) для серых / размытых опций, которые не были доступны. Как я получил цвета Apple iPhone? Знаете ли вы, что Firefox имеет встроенный инструмент выбора цвета?

shot4

Круги в магазине Apple на самом деле имеют хорошие градиенты, когда вы перемещаетесь из центра круга наружу. Я просто нажал «в середине», чтобы получить значение, которое выглядело хорошо для меня.

Хорошо, теперь давайте перейдем к коду кода. Я начал с простой процедуры настройки:

$(document).ready(function() {
console.log("Make it so.");

//load json files
var storeReq = $.getJSON("data/stores.json");
var availReq = $.getJSON("data/availability.json");
$.when(storeReq,availReq).then(function(stores, avail) {
storeList = stores[0].stores;
availabilityData = avail[0];
doStoresForStates();
doStateDropDowns();
startUp();
});
});

Я пропущу doStoresForStates, это просто дает мне корневую переменную с ключом состояния, которая включает в себя массив хранилищ. doStateDropDowns заполняет раскрывающееся состояние. Как я уже говорил, идея состояла в том, чтобы сделать так, чтобы вы могли добавить несколько магазинов, но я так и не дошел до этого.

function doStateDropDowns() {
var dds = $(".stateDD");
//generate the option HTML list, but only once
if(stateOptionHTML === "") {
var states = [];
for(var i=0;i<storeList.length;i++) {
if(states.indexOf(storeList[i].storeState) === -1) states.push(storeList[i].storeState);
}
states.sort();
var s = "

-- State --"; for(var i=0;i<states.length;i++) { s += "
" + states[i] + ""; } stateOptionHTML = s; } dds.each(function(index) { console.log("doing "+index); var options = $("option", this); if(options.length === 0) { $(this).html(stateOptionHTML); } }); }

В раскрывающемся списке состояния есть прослушиватель событий, который реагирует на изменения. Единственное, что здесь хорошо, — это использование next("select")рядом с ним.

function doStores() {
var selected = $(this).val();
if(selected === "") return;
var storeHTML = "

-- Location --"; for(var i=0;i " + storeData[selected][i].city + ", " + storeData[selected][i].name + ""; } $(this).next("select").html(storeHTML); }

Итак, теперь наступил страшный момент — создание фактического материала «на основе того, что вы выбираете, фильтруйте результаты». Первой проблемой, с которой я столкнулся, были данные модели. Как я уже сказал, каждая модель / емкость / цвет / носитель имеет уникальный идентификатор. Я мог бы набрать все это вручную, но вместо этого я использовал инструменты dev:

var ray = [];$(".form-choice-selector").each(function(idx) { ray.push($(this).val()); }); copy(JSON.stringify(ray));

То, что вы видите, это код, который я запустил в консоли браузера. Он извлекал каждую «ячейку» дисплея, получал значение (которое было идентификатором продукта), а затем использовал его copyдля помещения в мою буфер обмена. Затем я могу вставить в мой код. Есть 5 носителей и 2 модели, поэтому мне пришлось делать это 10 раз, но это заняло всего 1 или 2 минуты, так что это не имело большого значения.

Фильтрующий код — большой беспорядок. Мол, серьезно. Кажется, работает, но я не даю на это никаких гарантий. Вот оно — не смейся надо мной слишком сильно.

function doFilter() {
//get all locations
var locations = [];
var selectedModels = [];
var selectedCarriers = [];

$(".locationDD").each(function(idx) {
if($(this).val() != '') locations.push($(this).val());
});

//if no locations, do nothing
if(locations.length === 0) return;

$(".modelCB").each(function(idx) {
if($(this).is(':checked')) {
selectedModels.push($(this).val());
};
});

$(".carrierCB").each(function(idx) {
if($(this).is(':checked')) {
selectedCarriers.push($(this).val());
};
});

console.log("Begin to filter. "+JSON.stringify(locations)+" "+JSON.stringify(selectedModels)+" "+JSON.stringify(selectedCarriers));

/*
logic is: for each color/capacity, determine if ON/OFF
*/
for(var i=0;i<capacityData.length;i++) {
var capacity = capacityData[i];
for(var j=0;j<colorData.length;j++) {
var color = colorData[j];
var models = getModels(capacity, color, selectedCarriers, selectedModels);

console.log("check "+capacity+" "+color+" models="+JSON.stringify(models));

var enabled = false;

//did we filter by location?
if(locations.length > 0) {
for(var z = 0;z<locations.length;z++) {
var location = locations[z];
for(var k=0;k<models.length;k++) {
//console.log(availabilityData[location][models[k]]);
if(availabilityData[location][models[k]] === "ALL") {
enabled=true;
break;
}
}
}
}
console.log("ENABLED",enabled);
var cell = $("." + color + ".cap" + capacity);
if(!enabled) {
cell.addClass("outofstock");
} else {
cell.removeClass("outofstock");
}
}
}
}

По сути — я зацикливаюсь на массиве емкости и цвета, а затем проверяю доступность в каждом месте. (Опять же, помните, что я собирался поддерживать несколько местоположений.) getModels— это служебная функция, которая анализирует данные модели, которые я почерпнул из инструментов разработчика. Затем я просто добавляю / удаляю класс CSS, чтобы добавить приятный эффект серого / размытия.

Это был передний конец. Чтобы поддерживать приложение в актуальном состоянии, я поместил его в приложение Node.js, работающее в IBM Bluemix . Все, что мне было нужно, — это возможность отсасывать JSON-файлы из Apple по расписанию, и для этого я использовал библиотеку cron, которую использовал в ColdFusion Bloggers . Вот полное приложение:

/*eslint-env node*/

var https = require('https');
var fs = require('fs');

// This application uses express as its web server
// for more info, see: http://expressjs.com
var express = require('express');

// cfenv provides access to your Cloud Foundry environment
// for more info, see: https://www.npmjs.com/package/cfenv
var cfenv = require('cfenv');

// create a new express server
var app = express();

// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));

// get the app environment from Cloud Foundry
var appEnv = cfenv.getAppEnv();


//fire and forget sync method
var sync = function() {
var writeStream1 = fs.createWriteStream('./public/data/availability.json');
https.request('https://reserve.cdn-apple.com/US/en_US/reserve/iPhone/availability.json', function(res) {
res.pipe(writeStream1);
}).end();

var writeStream2 = fs.createWriteStream('./public/data/stores.json');
https.request('https://reserve.cdn-apple.com/US/en_US/reserve/iPhone/stores.json', function(res) {
res.pipe(writeStream2);
}).end();
}

var cron = require('cron');
var cronJob = cron.job('* */2 * * *', function() {
sync();
console.log('cron job complete');
});
cronJob.start();

// start server on the specified port and binding host
app.listen(appEnv.port, function() {
// print a message when the server starts listening
console.log("server starting on " + appEnv.url);
});

У меня нет какой-либо обработки ошибок в функции синхронизации, так что она хрупкая, но справляется с работой. Я также настроил это, чтобы поразить Apple раз в 2 часа. Я подумал, что это было нежно и не будет завышать налог Apple.com Я также создал маршрут, который мог бы использовать для ручного тестирования, но удалил его, когда развернул его до Bluemix.

Вы можете увидеть это сами здесь: http://applestorechecker.mybluemix.net/ . Как я уже сказал, это несколько хрупко. Я мог бы также добавить простые «файлы данных, последние обновленные в X» в заголовок, чтобы вы знали, насколько свежи данные. Если бы я не был ленивым, я мог бы также добавить возможность для вас зарегистрироваться, когда у вашей желаемой модели / цвета / носителя / магазина есть продукт, но, увы, я ленивый сегодня.

В любом случае, проверьте это, и дайте мне знать, если у вас есть какие-либо вопросы!