File Manager cz. I - ogólne założenia, konfiguracja i layout AngularJS

Autor: admin@ignaszewski.pl | Opublikowany: 3 maja 2014

 

Od wielu miesięcy zmagam się ze znalezieniem w miarę prostego FileManager'a, którego mógłbym użyć w swoim CMSie, zarówno jako wersję standalone jak i jako plugin np. do TinyMce. Znalazłem kilka obiecujących propozycji i pomysłów, jednak doszedłem do wniosku, że chciałbym "odkryć koło na nowo". Krótko mówiąc postanowiłem zrobić swój własny. No i jak to z takimi pomysłami bywa, jest chwila, jest zapał, a za miesiąc czy dwa wszystko przepada. Stąd postanowiłem się zmobilizować i wspólnie z Wami, czytelnikami mojego bloga, stworzyć prostą aplikację w AngularJS, która będzie realizowała moje założenia. Ponieważ problem jest dość złożony postaram się go zrealizować w odcinkach. Dziś pierwsza część: Ogólne założenia, konfiguracja i layout. Zatem zaczynamy.

Założenia

Celem projektu jest stworzenie aplikacji umożliwiającej:

  • przeglądanie listy plików i katalogów wgranych na serwer
  • możliwość wgrania nowych plików (multiupload)
  • możliwość filtrowania plików: po typie czy po nazwie
  • fajnym dodatkiem będzie to jeśli aplikację da radę uruchomić jako plugin do edytorów WYSIWYG

Konfiguracja

Skoro wiemy już co chcemy zrobić to czas pomyśleć o tym jak zautomatyzować nasz projekt. Bardzo łatwo zauważyć, że będą nam potrzebne zewnętrzne biblioteki JavaScript takie jak choćby AngularJS lub jQuery, czy frameworki CSS: Bootstrap i Fonts-Awesome.

Proponuję całość oprzeć o Grunt'a, który pozwoli nam: zainstalować zewnętrzne biblioteki przy pomocy Bower'a (grunt-bower-task), sprawdzać poprawność pisanego kodu (jshint), łączyć i minimalizować pliki (ulifyjs), uruchomić naszą aplikację na lokalnym serwerze (express) jak i parę jeszcze innych rzeczy o których zaraz napiszę.

NPM - lista potrzebnych pakietów

Zakładam, że każdy spotkał się już z nodejs i ma pojęcie o tym jak to działa, jeśli nie, to zapraszam do przeczytania moich artykułów na ten temat.

Głównym plikiem naszego projektu będzie: package.json. Zawiera on listę niezbędnych pakietów potrzebnych do uruchomienia naszej aplikacji.

{
  "name": "filemanager",
"version": "0.0.1", "options": { "server": { "src": "server/src", "dest": "server/dest", "script": "server/app.js" }, "client": { "src": "client/src",
"dist": "client/dist" } }, "dependencies": { "grunt":"0.4.2", "grunt-contrib-jshint": "0.6.3", "grunt-contrib-uglify": "0.2.2", "grunt-contrib-watch": "0.6.1", "grunt-contrib-less": "0.11.0", "grunt-express-server": "0.4.17", "grunt-reload": "0.2.0", "grunt-bower-task": "0.3.4", "connect": "2.14.5" } }

Powyższy listing przedstawia zawartość pliku package.json. Zawiera on dwa ważne elementy:

  • options - lista parametrów, z których będzie korzystał Grunt
  • dependecies - lista potrzebnych pakietów nodejs
    • grunt - moduł umożliwiający konfigurację i uruchamianie różnych zadań 
    • grunt-contrib-jshint - moduł umożliwiający walidację poprawności pisanego kodu
    • grunt-contrib-uglify - moduł odpowiedzialny za minifikację kodu JS
    • grunt-contrib-watch - moduł uruchamiający zadanie monitorujące zmiany plików (watcher)
    • grunt-contrib-less - moduł pozwalający parsować plik .less i zamieniać go na plik .css
    • grunt-express-server - moduł uruchamiający serwer nodejs
    • grunt-reload - moduł przeładowujący stronę
    • grunt-bower-task - moduł odpowiedzialny za zainstalowanie zależności bibliotek zewnętrznych
    • connect - pozwoli nam w prosty sposób uruchomić naszą aplikację jako serwer

Grunt

Mając już zainstalowane wszystkie niezbędne pakiety, przyszedł czas na skonfigurowanie naszego "task runner". Plikiem konfiguracjyjnym Grunt'a jest plik Gruntfile.js znajdujący się w głównym katalogu naszej aplikacji. Zanim jednak ustalimy jego zawartość potrzeba jeszcze kilku słów wyjaśnienia jak będzie wyglądał układ plików i katalogów naszej aplikacji:

/
|-- bower_components  - komponenty bowera
|-- client  - aplikacja (część kliencka)
| |-- components - lista niezbędnych komponentów z katalogu bower_components
| |-- data - folder z danymi w formacie json, grafikami i innymi statycznymi plikami
| |-- dist - wersja produkcyjna naszej aplikacji
| |-- src - wersja źródłowa naszej aplikacji
|-- node_modules - moduły nodejs
|-- server - część serwerowa naszej aplikacji 
|-- bower.json
|-- Gruntfile.js
|-- package.json
|-- .jshintrc 

Mając już świadomość struktury naszej aplikacji przejdźmy do pliku Gruntfile.js.

module.exports = function(grunt) {

    var SERVER_PORT = 3000;
    var path = require('path');

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        bower: {
            install: {
                options: {
                    targetDir: '<%= pkg.options.client.src %>/components',
                    install: true,
                    verbose: true,
                    cleanTargetDir: true,
                    cleanBowerDir: false,
                    layout: 'byComponent'
                }
            }
        },
        jshint: {
            options: {
                jshintrc: '.jshintrc'
            },
            files: ['<%= pkg.options.client.src %>/js/app/**/*.js']
        },
        less: {
            dist: {
                options: {
                },
                files: {
                    "<%= pkg.options.client.dist %>/css/main.min.css": "<%= pkg.options.client.src %>/css/main.less"
                }
            }
        },
        express: {
            options: {
                // Override defaults here
                background: true,
                delay: 500
            },
            dev: {
                options: {
                    script: '<%= pkg.options.server.script %>'
                }
            }
        },
        reload: {
            port: 8001,
            liveReload: {},
            proxy: {
                host: 'localhost',
                port: SERVER_PORT // should match server.port config
            }
        },
        uglify: {
            options: {
                mangle: false
            },
            files: {
                src: ['<%= pkg.options.client.src %>/js/app/app.js', '<%= pkg.options.client.src %>/js/app/controllers.js'],
                dest: '<%= pkg.options.client.dist %>/js/app.min.js'
            }
        },
        watch: {
            options: {
                spawn: false
            },
            files: ['<%= pkg.options.client.src %>/js/src/app/**', '<%= pkg.options.client.src %>/css/**', '/index.html', '<%= pkg.options.client.src %>/data/**/*.json'],
            tasks: ['jshint', 'less', 'uglify', 'reload']
        }
    });

    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-bower-task');
grunt.loadNpmTasks('grunt-express-server'); grunt.loadNpmTasks('grunt-reload'); grunt.registerTask('install', ['bower:install']); // Default task(s). grunt.registerTask('default', ['jshint', 'less', 'express', 'uglify', 'reload', 'watch']); };

 Nie zagłębiając się za bardzo w poszczególne parametry konfiguracji, postaram się przedstawić ogólne założenia kolejnych zadań grunt'a:

  • pkg - zmienna przechowująca dane z pliku package.json
  • bower - ma za zadanie zainstalować wszystkie zależności w katalogu client/src/components
  • jshint - analizuje wszystkie pliki *.js w katalogu źródłowy aplikacji pod kątem ich poprawności zgodnie z konfiguracją w pliku .jshintrc
  • less - analizuje plik client/src/css/main.less i zamienia go na plik client/dist/css/main.min.css
  • express - uruchamia serwer na lokalnym porcie 3000
  • reload - pozwala odświeżyć stronę jeśli nastąpi jakaś zmiana w plikach
  • uglifyjs - minimalizuje kod naszej aplikacji do jednego pliku js: client/dist/js/app.min.js
  • watch - nasłuchuje zmian na różnych plikach i uruchamia odpowiednie zadania 

Bower

 

Ostatnią rzeczą jaka pozostała nam do skonfigurowania jest lista zewnętrznych bibliotek JS i CSS, które chcemy wykorzystać w naszym projekcie. Lista ta przedstawia się w następująco:

{
    "name": "filemanager",
    "directory": "client/js/components",
    "dependencies": {
        "bootstrap": "latest",
        "font-awesome-bower": "*",
        "jquery": "*",
        "jqueryui": "*",
        "angular-bootstrap": "*",
        "angular": "1.2.*",
        "angular-resource": "1.2.*",
        "angular-route": "1.2.*",
        "angular-mocks": "1.2.*",
        "angular-animate": "1.2.*",
        "angular-sanitize": "1.2.*",
        "angular-i18n": "1.2.*",
        "angular-ui-router": "latest",
        "lodash": "2.4.*",
        "ng-flow": "*"
    }
    ,"exportsOverride": {
        "angular-bootstrap": {
            "./": ["**"]
        },
        "bootstrap": {
            "js": "dist/**/*.min.js",
            "css": "dist/**/*.min.css",
            "fonts": "dist/**/glyphicons*"
        },
        "font-awesome-bower": {
            "css": "**/*.css",
            "fonts": "**/*-webfont*"
        }
    }
}

Pobieramy w nim:

  • bootstrap i fonts-awesome - chyba nie trzeba przedstawiać - framework CSS + zestaw ikon 
  • jquery i jquery-ui - tak na wszelki wypadek, jeśli nie użyję to na końcu wywalę, po prostu są to tak oczywiste biblioteki, ze lubię zawsze je mieć w swoim projekcie
  • angularjs i wszystkie jego podstawowe moduły
  • lodash - ma trochę ciekawych funkcji
  • ng-flow - biblioteka ułatwiająca ładowanie plików

Instalacja 

Dzięki takiej automatyzacji instalacja naszej aplikacji jest już dziecinnie prosta.

  1. Instalujemy potrzebne pakiety nodejs: npm install
  2. Instalujemy zależności bowera: grunt bower
  3. Uruchamiamy aplikację: grunt

W tej chwili powinniśmy mieć dostępny serwer: http://localhost:3000, jednak jeszcze nic nie stworzyliśmy zatem nasz serwer nie działa.

Serwer

Pierwszą częścią naszej aplikacji będzie prosty serwer, którego jedynym zadaniem jest serwowanie tego co się znajduje w katalogu client. Taką funkcjonalność zrealizujemy poniższym plikiem app.js.

var connect = require('connect');
connect.createServer(
    connect.static(__dirname + '/../client/')
).listen(3000);

Klient

Główny plik naszej aplikacji to client/src/app/app.js.

'use strict';
var filemanager = angular.module('filemanager', []);

Tworzy on na razie tylko moduł filemanager i nic poza tym. W przyszłości na pewno ulegnie zmianie.

Pierwszy kontroler naszej aplikacji zostanie umieszczony w pliku client/src/app/controllers.js.

'use strict';
angular.module('filemanager')
    .controller('MainCtrl', ['$scope', '$http', function($scope, $http){
        $scope.dirs = [];
        $scope.files = [];

        $http.get('/data/directory.json')
            .success(function(data){
                $scope.dirs = data.dirs;
                $scope.files = data.files;
            })
        ;
    }])
;

Zawiera on dwie zmienne $scope.dirs i $scope.files, które przechowują listę katalogów i listę plików znajdujących się w obecnie przeglądanym katalogu. Na tę chwilę dla ułatwienia zapełnienia tych zmiennych danymi pobieramy statyczne dane z pliku /data/directory.json. Jego skróconą wersję prezentuję poniżej:

{
    "dirs": [
        {
            "id": 1,
            "name": "Katalog"
        },
        {
            "id": 2,
            "name": "Katalog o bardzo długiej nazwie"
        }
    ],
    "files": [
        {
            "id": 1,
            "name": "Dino 1",
            "src": "/data/images/IMG_5549.JPG"
        },
        {
            "id": 10,
            "name": "Dino 2",
            "src": "/data/images/IMG_5554.JPG"
        }
... ] }

Teraz przydałby się nam jeszcze jakiś plik html, który spiąłby to wszystko razem. W tym celu utworzymy plik index.html o następującej treści.

<html ng-app="filemanager">
<head>
<link rel="stylesheet" href="components/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="components/font-awesome-bower/css/font-awesome.min.css">
<link rel="stylesheet" href="/dist/css/main.min.css">
<script src="components/jquery/jquery.js"></script>
<script src="components/bootstrap/js/bootstrap.min.js"></script>
<script src="components/angular/angular.js"></script>
<script src="components/angular-route/angular-route.js"></script>
<script src="components/angular-resource/angular-resource.js"></script>
<script src="components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="/dist/js/app.min.js"></script>
<script>__reloadServerUrl="ws://localhost:8001";</script>
<script type="text/javascript" src="//localhost:8001/__reload/client.js"></script>
</head>
<body>
<div data-ng-controller="MainCtrl" class="container filemanager">
<h1>Filemanager</h1>
<div class="row nav-row">
<div class="col-sm-3 col-md-3 text-left">
<div class="btn-group">
<button class="btn btn-default" title="Utwórz katalog"><i class="fa fa-plus"></i><i class="fa fa-folder-o"></i></button>
<button class="btn btn-default" title="Wgraj pliki"><i class="fa fa-plus"></i><i class="fa fa-files-o"></i></button>
</div>
</div>
<div class="col-sm-4 col-md-4 text-center">
<div class="btn-group">
<button class="btn btn-default active" title="Wszystkie typy plików"><i class="fa fa-file-o"></i></button>
<button class="btn btn-default" title="Zdjęcia"><i class="fa fa-picture-o"></i></button>
<button class="btn btn-default" title="Audio"><i class="fa fa-music"></i></button>
<button class="btn btn-default" title="Wideo"><i class="fa fa-video-camera"></i></button>
<button class="btn btn-default" title="Archiwa"><i class="fa fa-archive"></i></button>
</div>
</div>
<div class="col-sm-4 col-md-4 col-md-push-1 col-sm-push-1">
<div class="input-group">
<input type="text" class="form-control" placeholder="Wyszukaj">
<span class="btn btn-default input-group-addon"><i class="fa fa-search"></i></span>
</div>
</div>
</div>
<div class="panel panel-default main-panel">
<div class="panel-body">
<div ng-repeat="dir in dirs" class="thumb thumb-folder img-thumbnail">
<i class="thumb-image fa fa-folder-o"></i>
<div class="thumb-name" data-ng-bind="dir.name"></div>
</div>
<div ng-repeat="file in files" class="thumb thumb-file img-thumbnail">
<img class="thumb-image" ng-src="{{ file.src }}">
<div class="thumb-name" data-ng-bind="file.name"></div>
</div>
</div>
</div>
</div>
</body>
</html>

Myślę, że plik jest na tyle jasny, że nie trzeba szczególnie omawiać jego struktury. Niemniej jednak muszę wspomnieć o jednej rzeczy. Dwie ostatnie linijki <script>, pozwalają na połączenie się z grunt'owym modułem reload i automatyczne odświeżanie strony poprzez websocket. 

Pozostało nam jeszcze okraszenie naszego projektu plikiem css powstałym z odpowiedniego pliku less - client/src/css/main.less

@containerClass: filemanager;

@thumbWidth: 180px;
@thumbHeight: 140px;

@thumbNameHeight: 30px;


.@{containerClass} {
	.nav-row {
		margin: 0 0 10px 0;

		div:first-child {
			margin-left: 0;
			padding-left: 0;
		}

		div:last-child {
			margin-right: 0;
			padding-right: 0;
		}
	}

	.btn {
		height: 34px;

		// Ikonki w przyciskach
		.fa {
			margin-right: 5px;
			&:last-child {
				margin-right: 2px;
			}
		}
	}


	.main-panel {
		.thumb {
			width: @thumbWidth;
			height: @thumbHeight;
			margin: 20px;


			.thumb-image {
				height: @thumbHeight - @thumbNameHeight - 10;
			}

			.thumb-name {
				height:  @thumbNameHeight;
				line-height:  @thumbNameHeight;
				background-color: #eeeeee;
				border-radius: 3px;
				padding: 0 3px;
				overflow: hidden;
				white-space:nowrap;
				text-overflow: ellipsis;
				text-align: center;
			}

			&.thumb-folder {

				text-align: center;

				.thumb-image {
					&.fa {
						font-size: @thumbHeight - @thumbNameHeight;
					}
				}
			}


			&.thumb-file {
				.thumb-image {
					width: @thumbWidth - 10;
					border-radius: 3px;
				}
			}

		}
	}
}

Teraz wykonując polecenie: grunt, wszystko powinno się już prawidłowo uruchomić. 

Podsumowanie

W ten oto sposób dobrnęliśmy do końca naszej pierwszej części. Aby wszystkim ułatwić życie proponuję skorzystać z repozytorium na githubie uruchomione specjalnie na potrzeby tego projektu: https://github.com/qjon/filemanager. Jeśli macie jakieś propozycje co do tego projektu proszę o informacje, sugestie, a także krytykę (oczywiście tą konstruktywną) na mój adres mailowy.