Односторінковий магазин на Phalcon PHP + AngularJS. Робота над помилками

image

Введення
Всім привіт! Не так давно я написав публікацію «Односторінковий магазин з кошиком на Phalcon + AngularJS + Zurb Foundation», яка мала неоднозначний ефект м'яко кажучи. А точніше отримала багато негативних коментарів, які були об'єктивні і конструктивні, які-то немає, і вони мене змусили задуматися, чому так сталося, адже я хотів зробити корисний мануал, який може стати в нагоді мені та іншим, початківцям писати на AngularJS.

Сповідь
Так, мануал був корисний для мене, для мене старого, того, кому в 2009 році відмовили в роботі в місцевій веб-студії, і він досі жодного разу не працював у команді, ні разу не працював на найманій роботі, а покладався тільки на себе, і головним критерієм ефективності реалізації проектів був один — головне, що працює. Але це я — старий, після написання статті, і безлічі коментарів, я вперше вирішив спробувати зробити все за правилами хорошого тону, хоча б заради інтересу.

Список літератури

Зазвичай список літератури наводиться наприкінці, але з іншого боку, по ходу написання статті, будуть виникати питання, чому була обрана саме така реалізація. І щоб не залишати вас без відповідей на ваші запитання, я буду в дужках [?] вказувати на джерело, звідки було взято. Отже, ось і література, на яку я спирався, виправляючи свої помилки.

  1. Коментарі до статті Односторінковий магазин з кошиком на Phalcon + AngularJS + Zurb Foundation
  2. Сміливий стайлгайд за AngularJS для командної розробки [1/2]
  3. Сміливий стайлгайд за AngularJS для командної розробки [2/2]
  4. angular js: ng-repeat no longer allowing duplicates
  5. Using Local Storage
  6. AngularJS $http not sending X-Requested-With header
  7. ngRoute preloader example


Робота над помилками



По ходу реалізації проекту односторінкового магазину з доставки їжі були ненавмисно допущені наступні недоліки:

  • Вивантаження відразу всіх товарів на одну сторінку за допомогою php
  • Відсутність єдиного стилю написання коду
  • Використання логіки в контролерах AngularJS [1] by EugeneOZ
  • Зберігання кошика в об'єкті js, який після перезавантаження сторінки скидався [1] by hVostt
  • Додавання в корзину через вставку інлайн PHP скриптів [1] steppefox
  • Чуйність інтерфейсу по ходу завантаження нових категорій


У цій статті я хочу поділитися як я шукав рішення, і що з цього вийшло. А тепер пройдемося по кожному пункту.

Вивантаження відразу всіх товарів на одну сторінку за допомогою php

Товарів у магазині виявилося трохи більше, ніж розраховувалося, та враховуючи наш камчатський інтернет, ми порахували:
100 товарів (зображень), кожна картинка 100-300 кб, в підсумку тільки товари з'їдали дорогоцінний трафік наших клієнтів у розмірі 10 мб, плюс майже 10 мб важив сам сайт (картинки, стилі, скрипти). Зараз же мені вдалося оптимізувати всі, і сайт важить 3,9 мб під час першого завантаження, і не більше 1 мб в наступні, так як браузер кешує все що потрібно.

Така оптимізація була досягнута за рахунок стиснення великих фонових зображень без втрати якості, а так само спрайтів для іконок і дрібної графіки, так само були прибрані блоки, які не грали важливої ролі на сторінці відгуки, партнери, сертифікати, чому ми, відкрита форма зворотного зв'язку замінена на jivosite). Все таки швидкість завантаження сайту з їжею, набагато важливіше наших лавр і нагород, які нафіг нікому не потрібні.

І, звичайно ж, була оптимізація за рахунок відмови від візуалізації відразу всіх товарів на сторінку з допомогою php, і замінена на ajax підвантаження даних json по кліку на посилання виду /#!/menu/7, та мені нарешті довелося вивчити як працює роутинг в angularjs, до свого сорому, до цього жодного разу не працював з роутингом js.

І це виявилося не так складно, як я очікував, власне у мене був всього один рауса:

function config($routeProvider, $locationProvider) {
$routeProvider
.when('/menu/:id', {templateUrl: '/app/views/products.html', controller: ProductsController});
$locationProvider.hashPrefix('!');
$locationProvider.html5Mode(true);
}
angular.module('rollShop').config(config);


Він то і довантажував json дані з сервера і рендерил їх у шаблоні:

<div class="large-12 columns" ng-view></div>


Взагалі, я чув що для індексації такого підходу сервер повинен повертати html замість json, але тоді не зрозуміло як уникнути інлайн вставок PHP скриптів? Це питання залишилося для мене загадкою. І цей підхід породив ще одну проблему, якщо користувач перейшовши по посиланню /#!/menu/7 натисне кнопку «Оновити сторінку», його очі наповнюватися кров'ю від виду json даних він побачить замість нормальної сторінки просто json дані. І тут нам на допомогу приходить Phalcon PHP Framework, насправді можна було і без нього, але так як я з ним працюю, то і роблю все в його стилі. Щоб виправити цю помилку, я вирішив що буду віддавати дані json, тільки при ajax запитах, а при звичайних запитах буду перенаправляти користувача по посиланню /#!/menu/$id.

public function menuAction($id)
{
if($id != 'undefined')
{
if ($this->request->isAjax() == true) {

//Отримуємо основну категорію
$category = Category::findFirst($id);

//Перевіряємо чи є підкатегорії
$sub_category = Category::find("pid = '" . $id . "'");

if (count($sub_category) > 0) {

$products['category'] = $category->name;

foreach ($sub_category as $key = > $val) {
$products['вкладені категорії'][$key] = array(
'name' => $val->name,
'products' => Products::find("category = '" . $val->id . "'")->toArray()
);
}
} else {
$products = array(
'category' => $category->name,
'products' => Products::find("category = '" . $category->id . "'")->toArray()
);
}

$this->response->setContent(json_encode($products));
return $this->response->send();

} else {
$this->response->redirect('/#!/menu/' . $id);
return false;
}
}
else
{
return false;
}

}


Перевірка на AJAX запит в Phalcon, здійснюється з допомогою умови:

if ($this->request->isAjax() == true) {
//This is Ajax
}


Але є проблема, AngularJS за замовчуванням не відправляє спеціальний заголовок X-Requested-With сервера [6], а значить без допомоги AngularJS, ця функція не буде працювати, тому довелося додати одну сходинку в конфіг ангуляра.

var app = angular.module('rollShop', ['ngRoute', 'mm.foundation'], function ($httpProvider) {
$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
}


Тепер все працює, і користувач ніколи не побачить голий JSON.

До речі, для індексації я подумав може зробити перевірку на бота, і боту віддавати чистий html, без js-коду, без кнопок, просто товари. Правда не знаю чи спрацює це чи правильно це, може хто підкаже в коментарях.

Додавання в корзину через вставку інлайн PHP скриптів

Завдяки виправленню попереднього недоліку, автоматично було виправлено недолік зі вставкою інлайн PHP скриптів в функцію js. Давайте порівняємо як відбувалося додавання товару в корзину раніше, а як стало зараз.

До (фу):

<div class="add-cart">
<input type="number" ng-model="num<?=$p['id']?> value="1" min="1" max="50">
<button type="button" ng-click="addCart(<?=$p['id']?> num<?=$p['id']?>, '<?=$p['title']?>', <?=$p['price']?>)"></button>
</div>


$scope.addCart = function(id, num, title, price)
{
var nums = num || 1;
$scope.carts.push({
id : id,
num : nums,
title : title,
price : price
});
};


:

<div class="add-cart">
<input type="number" ng-model="item.quality" min="1" max="50" placeholder="1">
<button type="button" ng-click="addCart(item)"></button>
</div>


$scope.addCart = function (item) {
CartsService.addCart(item);
};


Чесно, мені такий підхід набагато більше подобається, хоч замовник цього не оцінить, і не заплатить більше, але як-то за себе стало гордо, і на душі приємно.

Відсутність єдиного стилю написання коду

Поки виправляв попередні два недоліку, автоматично довелося виправити ще два — «використання логіки в контролерах», як бачите тепер все робить сервіс, і «відсутність єдиного стилю написання коду».

Це моя вічна проблема, але знову ж таки, якщо дивитися з точки зору економіки, і підходу «головне — працює», то тут все досить ефективно, не заморочуючись на стайлгайдах, я економив купу свого часу, а час, як відомо — гроші. До речі бюджетів моїх клієнтів від 50 до 100 тис. руб, і їм все одно як написаний код. Але це все виправдання, адже я хочу виправити ситуацію, і зробити код красивим, читабельним і логічним, ніж мені допомогли дві статті на Хабре [2] і [3].

В результаті у мене вийшло 2 контролера, 3 фабрики, 1 вид і один загальний app.js. Виглядає це приблизно так:



Власне як і рекомендувалося робити в тих стайлгайдах. А тепер давайте подивимося на контролери, і можете порівняти з минулого статтею.

ProductsController.js

function ProductsController($scope, $routeParams, ProductsService, CartsService)
{

$scope.items = ";

ProductsService.getData($routeParams.id).success(function(data){
$scope.items = data;
});

$scope.addCart = function (item) {
CartsService.addCart(item);
};

}
angular.module('rollShop').controller('ProductsController', ProductsController);


CartsController.js

function CartController($scope, CartsService) {

$scope.carts = CartsService.getItemsCart();

$scope.total = function(){
return CartsService.summary($scope.delivery)
};

$scope.removeItem = function (carts, item) {
CartsService.removeItem(carts, item);
};

}
angular.module('rollShop').controller('CartController', CartController);


Все круто, як мені здається, навіть самому більше подобається, ніж те, що було раніше, точніше навіть, що творилося раніше у мене в контролерах.

Зберігання кошика в об'єкті js, який після перезавантаження сторінки скидався

Наступний недолік, на який в першу чергу звернули увагу читачі, це зберігання товарів кошика в об'єкті js. Чому я вибрав цей підхід, я пояснив в минулій статті, адже магазин односторінковий. У будь-якому випадку, реалізувати зберігання в localStorage не становить праці, та й думаю стати в нагоді надалі.

Я почав шукати зручний модуль для AngularJS, який би працював з localStorage, але в той же час він повинен бути простим і легким. Я знайшов кілька варіантів реалізації, але вони були складні і великі, на мій погляд, посилань зараз нажаль не пам'ятаю, і потім натрапив на той варіант, що був як раз для мене [5].

angular.module('ionic.utils', [])

.factory('$localstorage', ['$window', function($window) {
return {
set: function(key, value) {
$window.localStorage[key] = value;
},
get: function(key, defaultValue) {
return $window.localStorage[key] || defaultValue;
},
setObject: function(key, value) {
$window.localStorage[key] = JSON.stringify(value);
},
getObject: function(key) {
return JSON.parse($window.localStorage[key] || '{}');
}
}
}]);


Щоправда використання його в такому вигляді не давало 100% справну роботу. При додаванні одного товару в корзину, все було добре, але як тільки туди потрапляв другий товар або товар з іншої категорії, ng-repeat переставав виводити товари в кошику, а в консолі святилася помилка:

Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys.


Рішення цієї помилки я знайшов у джерелі [4]. Проблема була із-за додавання у масив $$hashKey, я так і не розібрався що це, звідки, навіщо, але мені потрібно було позбутися цього, щоб все працювало, і кінцевий варіант фабрики працює з localStorage виглядає так:

function LocalStorageFactory($window)
{
return {
set: function(key, value) {
$window.localStorage[key] = value;
},
get: function(key, defaultValue) {
return $window.localStorage[key] || defaultValue;
},
setObject: function(key, value) {
$window.localStorage[key] = JSON.stringify(value, function (key, val) {
if (key == '$$hashKey') {
return undefined;
}
return val;
});
},
getObject: function(key) {
return JSON.parse($window.localStorage[key] || '{}');
},
remove: function(key){
$window.localStorage.removeItem(key);
},
clear : function() {
$window.localStorage.clear();
}
}
}

angular.module('rollShop').factory('LocalStorageFactory', LocalStorageFactory);


Я думаю код невеликий, і знайти 2 відмінності буде не важко, перевірка на $$hashKey допомогла мені виправити помилку з дублікатами. Тепер сервіс кошика працював справно, і добре, запам'ятовуючи позиції, перераховуючи суму, і. т. д. Код наводжу нижче:

function CartsService(LocalStorageFactory)
{
var CartsService = {};
var CartData;

if(LocalStorageFactory.getObject('carts').length > 0)
{
CartData = LocalStorageFactory.getObject('carts');
}
else
{
CartData = [];
}

CartsService.addCart = function (item)
{
CartData.push({
id: item.id
num: item.quality || 1,
title: item.title,
price: item.price
});

CartsService.update();

};

CartsService.update = function()
{
if(LocalStorageFactory.getObject('carts').length > 0)
{
LocalStorageFactory.remove('carts');
}

LocalStorageFactory.setObject('carts',CartData);
};

CartsService.getItemsCart = function () {
return CartData;
};

CartsService.removeItem = function (items, id) {
items.splice(id, 1);
CartsService.update();
};

CartsService.summary = function(dispatch)
{
var total = 0;
var delivery = 0;

angular.forEach(CartsService.getItemsCart(), function (item) {
total += item.num * item.price;
});

//Тут був код высчитывающий вартість доставки

total return + delivery;
};

return CartsService;
}
angular.module('rollShop').factory('CartsService', CartsService);


Не ідеально, але все ж краще, ніж було.

Чуйність інтерфейсу по ходу завантаження нових категорій

JSON дані, що приходять з сервера важать не більше 10 кб, плюс час віддачі сервером, плюс час реакції js, загалом у випадку великого камчатського інтернету з величезною пінгом, є сенс показати користувачеві preloader при кожному запиті, та й це в будь-якому випадку хороший тон.

Можна звичайно це віддати контролеру, але хотілося більш універсальне рішення, і знову ж таки при цьому, просте і легке. Спочатку я думав використовувати плагін nProgress lite для AngularJS, але все ж вирішив зробити ще простіше, і знайшов приклад [7].

function run($rootScope, $timeout) {

$rootScope.layout = {};
$rootScope.layout.loading = false;

$rootScope.$on('$routeChangeStart', function () {
$timeout(function(){
$rootScope.layout.loading = true;
});
});
$rootScope.$on('$routeChangeSuccess', function () {
$timeout(function(){
$rootScope.layout.loading = false;
}, 500);
});
$rootScope.$on('$routeChangeError', function () {
$timeout(function(){
$rootScope.layout.loading = false;
}, 500);
});
}
angular.module('rollShop').run(run);


Тепер коли настає одна з подій (думаю за назвою події все зрозуміло), показується / ховається прелоадер.

<div class="large-12 columns" ng-hide="!layout.loading">
<!-- Можна текст, можна gif анімацію, можна на весь екран, загалом що хочете --> 
</div>


Тепер клієнт не буде клікати по 5 разів на категорію, не розуміючи грузиться вона чи ні.

Висновок

Я постарався виправити недоліки допущені під час розробки, але основна мета-привчити себе до грамотному використанню технологій, навіть не дивлячись на відсутність команди. Чесно зізнаюся, я отримав масу задоволення, хоч і витратив не зайве робочий час, і не отримав за це будь-якої компенсації. Для себе я зробив висновок, що всі наступні проекти я буду робити вже з більш грамотним підходом, тому що такий підхід змушує цінувати не тільки гроші, отримані за проект, але й знання та досвід отримані під час роботи над ним. Досвід і знання дають гордість за свою справу, підвищують якість роботи, любов до того що робиш. А все це не залишить тебе без грошей в кишені.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.