1 applications avec node.js angular et mean
TRANSCRIPT
1
Applications avec Node.js,
Angular et MEAN
Table des matières I. Prérequis ............................................................................................................................. 4
1. Les « indispensables ».................................................................................................... 4
a. NPM ............................................................................................................................. 4
b. Task runner : GruntJS ................................................................................................ 5
c. MongoDB.................................................................................................................... 6
2. Les « utiles »..................................................................................................................... 7
a. Bower .......................................................................................................................... 7
b. Git ................................................................................................................................ 8
c. Une console plus agréable ...................................................................................... 9
d. Pour tester ses services Web .................................................................................. 10
II. EDI et éditeurs de texte ................................................................................................. 10
III. Mêmento Node.js ....................................................................................................... 11
1. Création du projet et installation des modules ...................................................... 11
2. Organisation de projet ............................................................................................... 12
a. Serveur ...................................................................................................................... 13
b. Configuration ........................................................................................................... 13
c. Contrôleurs ............................................................................................................... 16
d. Mongoose ................................................................................................................ 16
e. Moteurs de vues ...................................................................................................... 17
IV. Module « core » ........................................................................................................... 22
1. Partie « client » ............................................................................................................. 22
a. « config.js » ................................................................................................................ 22
b. Configuration du module « core » ........................................................................ 22
c. Contrôleurs Angular ................................................................................................ 23
d. Message flash .......................................................................................................... 23
e. Vue « header » ......................................................................................................... 25
2. Partie « server » ............................................................................................................ 26
a. Routes côté server .................................................................................................. 26
b. Le layout ................................................................................................................... 26
V. Module « users » .............................................................................................................. 28
2
1. Partie « server » ............................................................................................................ 28
a. Configuration de Passport ..................................................................................... 28
b. Création du modèle « User » .................................................................................. 32
c. Contrôleur d’’authentification .............................................................................. 33
d. « ErrorHandler » ........................................................................................................ 36
2. Partie « client » ............................................................................................................. 37
a. Module ...................................................................................................................... 37
b. Contrôleur Angular pour l’authentification ......................................................... 38
c. Service Authentication ........................................................................................... 38
d. Formulaires ............................................................................................................... 39
VI. Module quelconque .................................................................................................. 42
1. Partie client (Angular) ................................................................................................ 43
a. Module ...................................................................................................................... 43
b. Contrôleur(s) Angular ............................................................................................. 45
c. Services ..................................................................................................................... 46
d. Vues ........................................................................................................................... 47
2. Partie Serveur ............................................................................................................... 51
a. Routes côté serveur ................................................................................................ 51
b. Modèle Mongoose ................................................................................................. 51
c. Contrôleur « Express » .............................................................................................. 53
VII. Plus … ............................................................................................................................ 55
1. Express .......................................................................................................................... 55
a. Variables partagées ............................................................................................... 55
b. Charger un fichier JSON ......................................................................................... 55
c. Changer le répertoire des vues ............................................................................ 55
d. Ressources (dossier « public ») ............................................................................... 55
e. Express Generator ................................................................................................... 56
2. “Sans Express” .............................................................................................................. 57
a. Serveur ...................................................................................................................... 57
c. URL et paramètres .................................................................................................. 57
d. Créer un index listant les contrôleurs .................................................................... 58
e. Evénements.............................................................................................................. 58
3. Bases de données ....................................................................................................... 59
a. SQL Server ................................................................................................................. 59
b. MongoDB.................................................................................................................. 60
VIII. MEAN.JS ........................................................................................................................ 62
3
Génération de code ......................................................................................................... 65
IX. Publication ................................................................................................................... 67
4
I. Prérequis
1. Les « indispensables »
a. NPM
Documentation
NPM (Node Package Manager) permet d’installer et publier des modules.
Générer « package.json » de l’application.
Depuis une invite de commande, naviguer jusqu’au dossier du projet (cd …) du
projet puis
npm init
Installation de modules
On peut soit :
Installer des modules un par un
Installation globale
npm install nommodule –g
Installation locale au projet
npm install nommodule
Installation avec ajout de la dépendance à « package.json »
npm install nommodule --save
Autres commandes utiles :
Désinstaller un module (suppression dossier et dépendance dans « package.json »)
npm rm nomdule Mettre à jour
npm update
Soit définir les dépendances dans « package.json » et les installer toutes avec
la commande. Cette méthode est utile pour alléger le projet, on pourra
supprimer tous les modules et les installer ensuite par cette simple commande.
npm install
Exemple « package.json »
{
"name": "AngNodeDemo",
"version": "1.0.0",
"description": "Node Angular application demo.",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
5
"author": "ROMAGNY13",
"license": "ISC",
"dependencies": {
"body-parser": "^1.14.2",
"client-sessions": "^0.7.0",
"consolidate": "^0.14.0",
"express": "^4.13.4",
"mongoose": "^4.4.2",
"passport": "^0.3.2",
"passport-facebook": "^2.1.0",
"passport-google-oauth": "^1.0.0",
"passport-local": "^1.0.0",
"swig": "^1.4.2"
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-nodemon": "^0.4.1"
}
}
b. Task runner : GruntJS
Documentation
Permet d’éviter d’avoir à relancer le serveur à chaque modification du code.
Installer Grunt en global
npm install -g grunt-cli
Installer Grunt-Nodemon en local au projet
npm install grunt-nodemon --save-dev
Créer un fichier de configuration pour Grunt « gruntfile.js »
module.exports = function (grunt){
grunt.initConfig({
nodemon: {
all: {
script: 'app.js',
options: {
watchedExtensions: ['js']
}
}
}
});
grunt.loadNpmTasks('grunt-nodemon');
grunt.registerTask('default', ['nodemon']);
};
Pour lancer le serveur il suffit désormais de taper la commande « grunt » à la place
de « node server.js »
grunt
Dépendances qui seront installées
(« ^ » pour version minimum)
6
c. MongoDB
Installation de MongoDB
Par défaut la base est installée dans « C:\Program Files\MongoDB »
La base de données est stockée par défaut dans « C:\data\db »
Les bases de données sont sur le port 27017. Exemple de chaine de connexion
« mongodb://localhost:27017/local-dev ». Les collections seront ajoutées à la
base de données « local-dev ».
Pour lancer , depuis une invite de commande, naviguer jusqu’au dossier
« bin » de MongoDB puis
mongod
Il est possible de changer la base de données de répertoire et définir ce nouveau
dossier :
mongod –dbpath ./data
Robomongo permet de gérer facilement ses bases de données MongoDB
Autres outils d’administration
7
2. Les « utiles »
a. Bower
Documentation et packages disponibles
Bower est un gestionnaire de dépendances. Indispensable avec « Angular ». Il
permet d’installer des packages, les dépendances seront automatiquement
installées. Par exemple jQuery sera automatiquement ajouté avec Bootstrap. Bower
nécessite que Node.js et Git soient installés.
Installation de Bower (en global)
npm install bower -g
Installation de Bower localement au projet
npm install bower --save
Avec dépendance de développement (« devDependencies » de « package.json »)
npm install bower --save-dev
Pour définir le dossier où seront installées les librairies … Créer un fichier « .bowerrc »à
la racine du projet
{
"directory": "public/lib"
}
Le fonctionnement est un peu le même qu’avec NPM. On peut créer un fichier
« bower.json » qui contiendra la liste des dépendances
bower init
… et utiliser la commande pour installer tous les packages
bower install
On peut également installer les packages un par un
bower install bootstrap --save
Mettre à jour tous les packages
bower update
Mettre à jour un package
bower update bootstrap
Les fichiers seront installés
dans le dossier « public/lib »
8
b. Git
Documentation
Installation
Télécharger et installer la version de Git (Windows, Mac, Linux, etc.)
On peut configurer si on veut (avec « Git Bash » )
git config --global user.name "Jerome Romagny"
git config --global user.email "romagny13@....."
git config --list
Définir le dossier du projet :
Avec git bash, naviguer jusqu’au dossier du projet (cd … ) puis
git init
Commit
Valider les changements dans le dossier (ajout/ suppression de fichiers, etc.)
git commit -a -m "initial commit"
On peut également définir les fichiers à ajouter puis « commit »
git add .
git commit -m "inital commit"
Commandes utiles :
- « git help »
- « git status » permet de lister les changements validés avec « git add » avant
un « commit »
- « git log » permet de lister tous les « commit »
Créer un fichier « .gitignore » à la racine du projet et indiquer les dossiers et fichiers à
ignorer. Exemple
/.idea
/vendor
composer.lock
Github
Cloner un projet github (copié dans le dossier défini auparavant) git clone http://github.com/...git
Remote
git remote add origin https ://github.com/…git
git push
(Demande les identifiants avec « https », on peut également utiliser « ssh »)
…Puis seulement « git push » les fois suivantes.
Message décrivant la build
Exemple ajout de variables globales
puis listing
9
c. Une console plus agréable
Pour avoir une console plus agréable, on peut utiliser cmder.
Autre possibilité console 2 combiné avec gitbash (installée avec Git) pour avoir une
console moins austère.
Lancer console 2 puis menu « Edit » … « Settings »… onglet « Tabs »… « Add »
Title : « git bash » par exemple
Shell : C:\Windows\SysWOW64\cmd.exe /c ""C:\Program Files\Git\bin\sh.exe" --
login -i"
Startup uri : ce que l’on veut
On peut désormais ouvrir une nouvelle tab git bash
Le chemin vers git peut varier
10
Commandes utiles
« cd » pour naviguer dans les dossiers
Glisser les répertoires dans l’invite de commande
« dir » lister les dossiers et fichiers du répertoire courant
« md » créer un dossier
touche « espace » et « flèches » permet de répéter la dernière commande entrée.
« cls » pour nettoyer la console
d. Pour tester ses services Web
DHC ,extension pour Chrome
Fiddler de Telerik (free)
II. EDI et éditeurs de texte WebStorm (ou PHPStorm) : chercher des plugins pour Node et Angular depuis
les settings
Visual Studio avec l’extension NodeJS tools for Visual Studio (ajout d’un onglet
de Templates « nodejs » + ouverture de la console depuis le menu contextuel
du projet et fenêtre « Node.js interactive window »)
Brackets, Visual Studio Code, Sublime Text, Atom, etc.
11
III. Mêmento Node.js
1. Création du projet et installation des modules a. Créer le dossier du projet
b. Ajout de « package.json »
npm init
c. Modules
Express
npm install express --save
Moteur de vues
o Pour vash
npm install vash --save
o Pour ejs
npm install ejs --save
npm install ejs-locals --save
o Pour jade
npm install jade --save
o Pour swig
npm install consolidate --save
npm install swig --save
Mongoose
npm install mongoose --save
Body parser pour récupérer les valeurs de formulaires
npm install body-parser --save
Session
npm install client-sessions --save
Passport pour l’authentification
npm install passport --save
Stratégies (local, facebook, google,etc.)
npm install passport-local --save
npm install passport-facebook --save
npm install passport-google-oauth --save
12
2. Organisation de projet Partie « server » Partie « client »
« node_modules » : librairies
installées par NPM pour node.js
« public » : dossier contenant la
partie « visible » du site (feuilles de
styles, images, modules Angular,
etc.)
« server » : On peut ranger dans
un dossier « server » si on veut
organiser son code
« config » : configuration des
différents modules Node.js
« controllers » Contrôleurs
« Express »
« models » : Modèles Mongoose
« views » : le layout et la page
index du site
A la racine « server.js », le serveur
Node.js
Dans le dossier « public » :
« css » : feuille de styles du site
« lib » : dossier où sont
installées les librairies par
Bower
« modules » dossier contenant
tous les modules Angular
13
a. Serveur
Nommé « server.js » plutôt que « app.js » pour éviter la confusion avec Angular.
var express = require('express');
var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var app = express();
var config = require('./server/config/config')[env];
require('./server/config/express')(app, config);
require('./server/config/mongoose')(config);
require('./server/config/passport')(app,config);
require('./server/config/routes')(app);
app.listen(config.port);
console.log('Listening on port ' + config.port + '...');
b. Configuration
config.js var path = require('path');
var rootPath = path.normalize(__dirname + '/../');
module.exports = {
development: {
db: 'mongodb://localhost:27017/local-dev',
rootPath: rootPath,
port: process.env.PORT || 3000,
facebook: {
clientID: 'your client id',
clientSecret: 'your client secret',
callbackURL: '/api/auth/facebook/callback'
},
google: {
clientID: 'your client id',
clientSecret: 'your client secret',
callbackURL: 'http://localhost:3000/api/auth/google/callback'
}
},
production: {
rootPath: rootPath,
db: 'mongodb://....', // à configurer
port: process.env.PORT || 80,
facebook: {
clientID: 'your client id',
clientSecret: 'your client secret',
callbackURL: '/api/auth/facebook/callback'
},
google: {
clientID: 'your client id',
clientSecret: 'your client secret',
callbackURL: 'http://localhost:3000/api/auth/google/callback'
}
}
};
14
Express var express = require('express'),
bodyParser = require('body-parser'),
session = require('client-sessions'),
consolidate = require('consolidate');
module.exports = function(app, config) {
app.set('views', config.rootPath + '/views');
app.engine('html', consolidate['swig']);
app.set('view engine', 'html');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
cookieName: 'session',
secret: 'secret'
}));
app.use(express.static(config.rootPath + '/public'));
};
Mongoose var mongoose = require('mongoose'),
userModel = require('../models/user'),
articleModel = require('../models/article');
module.exports = function(config) {
mongoose.connect(config.db);
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error...'));
db.once('open', function callback() {
console.log('Db opened');
});
//articleModel.createDefaultArticles();
};
Passport
Voir le module “users”
15
Routes var articleController = require('../controllers/articleController'),
auth = require('../controllers/authController');
module.exports = function (app) {
app.get('/api/articles/', articleController.list);
app.get('/api/articles/:id', articleController.read);
app.post('/api/articles/', articleController.create);
app.put('/api/articles/', articleController.update);
app.delete('/api/articles/:id', articleController.delete);
app.get('/', function (req,res) {
if (req.session && req.session.passport && req.session.passport.user) {
res.render('index', { 'user' : req.session.passport.user });
}
else {
res.render('index');
}
});
// local
app.route('/api/auth/register').post(auth.register);
app.route('/api/auth/login').post(auth.login);
app.route('/api/auth/facebook').get(auth.oauthCall('facebook',{ scope: ['email'] }));
app.route('/api/auth/facebook/callback').get(auth.oauthCallback('facebook'));
app.route('/api/auth/google').get(auth.oauthCall('google', {
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
]
}));
app.route('/api/auth/google/callback').get(auth.oauthCallback('google'));
// log out
app.get('/api/auth/logout', auth.logout);
};
Si on ne veut pas passer « app » on peut utiliser « router »
var articleController = require('../controllers/articleController'),
express = require('express'),
router = express.router();
module.exports = function () {
router.get('/api/articles/', articleController.list);
// etc.
};
Utilise les contrôleurs
On passe express en argument
Route et appel de fonction du
contrôleur
16
c. Contrôleurs
Exemple de contrôleur
var mongoose = require('mongoose'),
Article = mongoose.model('Article'),
errorHandler = require('./errorHandler');
exports.list = function (req, res) {
Article.find().sort('-created').populate('user').exec(function (err, articles) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};
d. Mongoose
Documentation
Création des modèles dans un dossier « models »
var mongoose = require('mongoose');
var schema = new mongoose.Schema({
title: {type:String, required:'{PATH} is required!'},
content: {type:String, required:'{PATH} is required!'}
});
var Article = mongoose.model('Article', schema);
Types de données :
String
Number
Date
Boolean
Buffer
ObjectId
Mixed
Array
Ne pas oublier « require » dans ce cas dans « mongoose.js »
var mongoose = require('mongoose'),
articleModel = require('../models/article');
Dans les contrôleurs on utilisera
var mongoose = require('mongoose'),
Article = mongoose.model('Article');
17
Une variante
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var ArticleSchema = new Schema({
title: {type:String, required:'{PATH} is required!'},
content: {type:String, required:'{PATH} is required!'}
});
module.exports = mongoose.model('Article', ArticleSchema, 'articles');
Pas besoin de require dans « mongoose.js » mais require dans les contrôleurs
var mongoose = require('mongoose'),
Article = require('../models/article');
e. Moteurs de vues
Choix du moteur de template (Express)
app.set("view engine", "ejs");
Définir le chemin du dossier content les vues
app.set('views', __dirname + '/views');
Passage de paramètres à la vue
res.render('articles-list',{title:'Mon titre', articles:articles});
Swig
Documentation
Fichiers avec extension « *.html »
Pour pouvoir utiliser Swig, installer avec NPM
npm install consolidate --save
npm install swig --save
Puis dans la configuration d’Express
var express = require('express'),
// etc.
consolidate = require('consolidate');
module.exports = function(app, config) {
app.set('views', config.rootPath + '/views');
app.engine('html', consolidate['swig']);
app.set('view engine', 'html');
// etc.
app.use(express.static(__dirname + "/../../public"));
};
Les variables entre accolades {{ }}
18
{{ title }}
Exemple de boucle
{% for article in articles %}
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
{% endfor %}
Note : avec Angular on utilisera plutôt les directives
19
VASH
Documentation
Fichiers avec extension « *.vash »
Master Page « layout.vash »
<!doctype html>
<html>
<head>
<title>@model.title</title>
</head>
<body>
@html.block("body")
@html.block("scripts")
</body>
</html>
Vue
@html.extend('layout', function(model){
@html.block("body", function (model) {
<p>Le contenu</p>
});
});
Variable avec @model
@model.title
Exemple de boucle
@model.articles.forEach(function(article){ <h2>@article.title</h2> <p>@article.content</p> });
20
EJS
Documentation
Fichiers avec extension « *.ejs »
Installer ejs-locals pour le support des master pages
npm install ejs –save
npm install ejs-locals --save
configuration
var engine = require('ejs-locals');
app.engine('ejs', engine);
app.set('view engine', 'ejs');
Master page “layout.ejs”
<!doctype html>
<html>
<head>
<title><%- title %></title>
</head>
<body>
<%- body %>
</body>
</html>
Vue
<% layout('layout.ejs') -%>
<p>Le contenu</p>
Variables entre <%= %>
<h1><%= title %></h1>
Exemple de boucle
<% articles.forEach(function(article){ %>
<h2><%= article.title %></h2>
<p><%= article.content %></p>
<% }) %>
Vues partielles (dans un dossier “partials” de “views”)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<link href="/css/default.css" rel="stylesheet" />
</head>
<body>
<%= include partials/footer.ejs %>
</body>
</html>
Exemple de vue partielle
<footer>
<div>Copyright © J. ROMAGNY</div>
21
</footer>
Jade
Documentation
Fichiers avec extension « *.jade »
Master page “layout.jade”
doctype
html
head
title= title
body
block content
Vue
extends layout
block content
p Le contenu
Variables
h1= title
Exemple de boucle
each article in articles
h2=article.title
p=article.content
22
IV. Module « core »
1. Partie « client »
a. « config.js »
Enregistrement de tous les modules de l’application + dépendances
'use strict';
var app = angular.module('app',['core','articles','users','ngRoute']);
b. Configuration du module « core »
Contient essentiellement les routes vers la page d’accueil et vers la page « 404 »
'use strict';
var core = angular.module('core',['ngRoute']);
core.config(function ($routeProvider) {
$routeProvider
.when('/',{
controller:'HomeController',
templateUrl:'modules/core/views/home.html'
})
.otherwise({ redirectTo: "/" })
}
);
Enregistrement de tous les modules de
l’application
Contrôleurs Angular pour la vue
« header » insérée dans le layout et
pour la vue d’accueil du site.
Vues
Configuration du module
« core » et routes
Directives et services
partagés
23
c. Contrôleurs Angular
« HeaderController » On injecte « Authentication » afin de pouvoir mettre en forme le
menu du header selon que l’utilisateur est connecté ou non.
angular.module('core').controller('HeaderController',function($scope,Auth
entication, flash, $rootScope){
$scope.authentication = Authentication;
function flashMessage() {
if(flash.hasMessage()){
$scope.message = flash.getMessage();
}
else{
$scope.message = undefined;
}
}
$rootScope.$on('$routeChangeStart',
function () { flashMessage(); }
);
});
« HomeController » est très classique voir quasiment vide, selon ce que l’on affiche
en page d’accueil. Il peut avoir un « title ».
d. Message flash
On peut se créer un service très simple de message. Le message n’est affiché
qu’une fois (supprimé après un changement de route)
angular.module('core').service("flash", function($rootScope) {
var message = undefined;
$rootScope.$on("$routeChangeSuccess", function() {
message = undefined;
});
this.setMessage = function(text,type){
message ={};
message.text = text;
message.type = typeof type !== 'undefined' ? 'success' : type;
message.cssclass = typeof message.type !== 'success' ? 'alert-
success' : 'alert-error';
};
this.getMessage = function() {
return message;
};
this.hasMessage = function() {
return message;
};
});
Il suffit ensuite de l’injecter dans les contrôleurs devant l’utiliser. On indique un
message avec « setMessage »
flash.setMessage('Article modifié.');
24
On ajoute dans une vue partielle que l’on inclut dans le layout par exemple.
<div ng-show="message" class="alert {{ message.cssclass }}">
<span dynamic="message.text"></span>
</div>
Et dans le contrôleur de cette vue partielle, on injecte le service « flash » puis on teste
à chaque changement de route si un message est présent.
function flashMessage() {
if(flash.hasMessage()){
$scope.message = flash.getMessage();
}
else{
$scope.message = undefined;
}
}
$rootScope.$on('$routeChangeStart',
function () { flashMessage(); }
);
La directive « dynamic » permet d’insérer du contenu au format HTML dans le
message.
angular.module('core').directive('dynamic', ['$compile', function
($compile) {
return {
restrict: 'A',
replace: true,
link: function link(scope, ele, attrs) {
return scope.$watch(attrs.dynamic, function (html) {
ele.html(html);
return $compile(ele.contents())(scope);
});
}
};
}]);
Exemple de contenu au format HTML
flash.setMessage('<strong>Article ajouté!</strong> vous pouvez le
<i>modifier</i> si vous le désirez.');
C’est un exemple simple. On peut aussi utiliser angular-flash
Exemple de message flash
25
e. Vue « header »
(Mise en forme avec Bootstrap)
<div class="navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="#/" class="navbar-brand">Angular Node Demo</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li ng-class="{ active: isActive('/')}"><a href="#/">Accueil</a></li>
<li ng-class="{ active: isActive('/articles/')}"><a
href="#/articles/">Blog</a></li>
</ul>
<ul class="nav navbar-nav navbar-right" ng-hide="authentication.user">
<li><a href="#/register">S'inscrire</a></li>
<li class="divider-vertical"></li>
<li><a href="#/login">Se connecter</a></li>
</ul>
<ul class="nav navbar-nav navbar-right" ng-show="authentication.user">
<li><a href="#">Bonjour {{ authentication.user.username }}</a></li>
<li class="divider-vertical"></li>
<li><a href="/api/auth/logout">Se déconnecter</a></li>
</ul>
</div>
</div>
</div>
<div class="separator-20"></div>
<div class="container">
<div ng-show="message" class="alert {{ message.cssclass }}">
<span dynamic="message.text"></span>
</div>
</div>
Menu hamburger
visible sur mobile
Menu à gauche avec
« Accueil » ,etc.
Menu à droite pour
utilisateur anonyme et
connecté
Pour l’exemple
message flash
Menu
« déconnecté »
Menu
« connecté »
26
2. Partie « server »
a. Routes côté server
Dans « routes.js » du dossier « config » du « server ». Essentiellement la route vers la
page d’accueil
var articleController = require('../controllers/articleController'),
auth = require('../controllers/authController');
module.exports = function (app) {
// etc.
app.get('/', function (req,res) {
if (req.session && req.session.passport && req.session.passport.user) {
res.render('index', { 'user' : req.session.passport.user });
}
else {
res.render('index');
}
});
};
b. Le layout <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title>Titre</title>
<link type="text/css" rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
<link type="text/css" rel="stylesheet" href="/css/style.css" />
</head>
<body ng-app="app">
<header ng-include="'/modules/core/views/header.html'" ng-
controller="HeaderController"></header>
<section class="content">
<section class="container">
{% block content %}{% endblock %}
</section>
</section>
<script type="text/javascript">
window.user = {{ user | json | safe }}
</script>
<!-- vendors -->
<script type="text/javascript" src="/lib/angular/angular.min.js"></script>
<script type="text/javascript" src="/lib/angular/angular-locale_fr-fr.js"></script>
<script type="text/javascript" src="/lib/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="/lib/angular-resource/angular-
resource.min.js"></script>
<!-- modules -->
<script type="text/javascript" src="/modules/core/core.module.js"></script>
<script type="text/javascript" src="/modules/users/users.module.js"></script>
<script type="text/javascript" src="/modules/articles/articles.module.js"></script>
27
<script type="text/javascript" src="/modules/core/config/config.js"></script>
<!-- controllers -->
<script type="text/javascript"
src="/modules/articles/controllers/articleController.js"></script>
<script type="text/javascript" src="/modules/users/controllers/authController.js"></script>
<script type="text/javascript"
src="/modules/core/controllers/headerController.js"></script>
<script type="text/javascript" src="/modules/core/controllers/homeController.js"></script>
<!-- Directives -->
<script type="text/javascript" src="/modules/core/directives/dynamic.js"></script>
<script type="text/javascript"
src="/modules/articles/directives/contenteditable.js"></script>
<!-- Services -->
<script type="text/javascript"
src="/modules/articles/services/articles.service.js"></script>
<script type="text/javascript" src="/modules/core/services/flash.js"></script>
<script type="text/javascript" src="/modules/users/services/authentication.js"></script>
</body>
</html>
« Index.html »
C’est la page principale du site qui va afficher les vues avec la directive ng-view (ou
ui-view avec ui-router). Elle utilise le layout.
{% extends 'layout.html' %}
{% block content %}
<div ng-view></div>
{% endblock %}
28
V. Module « users »
1. Partie « server » Passport permet de simplifier l’authentification. Il faut définir les stratégies utilisées :
local, facebook, google, … Stratégies disponibles
a. Configuration de Passport var passport = require('passport'),
User = require('mongoose').model('User');
module.exports = function (app, config) {
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (id, done) {
User.findOne({
_id: id
}, '-salt -password', function (err, user) {
done(err, user);
});
});
require('./strategies/facebook')(config);
require('./strategies/google')(config);
require('./strategies/local')();
app.use(passport.initialize());
app.use(passport.session());
};
Stratégie « local »
npm install passport-local --save
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
User = require('mongoose').model('User');
module.exports = function () {
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
},
function (username, password, done) {
User.findOne({
username: username
}, function (err, user) {
if (err) {
return done(err);
}
if (!user || !user.authenticate(password)) {
return done(null, false, {
message: 'Utilisateur ou mot de passe incorrect.'
});
}
return done(null, user);
});
}
));
};
29
Stratégie « Facebook »
npm install passport-facebook --save
var passport = require('passport'),
FacebookStrategy = require('passport-facebook').Strategy,
auth = require('../../controllers/authController');
module.exports = function (config) {
passport.use(new FacebookStrategy({
clientID: config.facebook.clientID,
clientSecret: config.facebook.clientSecret,
callbackURL: config.facebook.callbackURL,
profileFields: ['id', 'name', 'displayName', 'emails',
'photos'],
passReqToCallback: true
},
function (req, accessToken, refreshToken, profile, done) {
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
profileImageURL: (profile.id) ? '//graph.facebook.com/' +
profile.id + '/picture?type=large' : undefined,
provider: 'facebook',
providerIdentifierField: 'id',
providerData: providerData
};
auth.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};
Création d’une application Facebook
30
Récupérer l’App ID et l’APP Secret et les indiquer dans « config.js »
Stratégie « Google »
On utilise ici « passport-google-oauth », il en existe d’autres pour Google.
npm install passport-google-oauth --save
var passport = require('passport'),
GoogleStrategy = require('passport-google-oauth').OAuth2Strategy,
auth = require('../../controllers/authController');
module.exports = function (config) {
passport.use(new GoogleStrategy({
clientID: config.google.clientID,
clientSecret: config.google.clientSecret,
callbackURL: config.google.callbackURL,
passReqToCallback: true
},
function (req, accessToken, refreshToken, profile, done) {
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
profileImageURL: (providerData.picture) ?
providerData.picture : undefined,
provider: 'google',
providerIdentifierField: 'id',
providerData: providerData
};
auth.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};
31
Création d’une application Google avec la console Google Developers
Avec creation d’une clé et URI de redirection (exemple
« http://localhost:3000/api/auth/google/callback » en local)
Récupérer le Client ID et le code secret et les indiquer dans « config.js »
32
b. Création du modèle « User » var mongoose = require('mongoose'),
Schema = mongoose.Schema,
crypto = require("crypto");
var UserSchema = new Schema({
firstName: {type: String, trim: true, default: ''},
lastName: {type: String, trim: true, default: ''},
displayName: {type: String, trim: true},
email: { type: String, trim: true, unique: true, default: ''},
username: {type: String, unique: 'Un utilisateur avec cet identifiant
est déja enregistré.', required: 'Vous devez indiquer un nom
d\'utilisateur', trim: true},
password: {type: String, default: ''},
salt: {type: String},
profileImageURL: {type: String},
roles: {
type: [{
type: String,
enum: ['user', 'admin']
}],
default: ['user']
},
provider: {type: String, required: 'Un provider est requis'},
providerData: {},
created: {type: Date, default: Date.now}
});
UserSchema.pre('save', function (next) {
this.salt = createSalt();
this.password = hashPassword(this.password, this.salt);
next();
});
var createSalt = function () {
return crypto.randomBytes(128).toString('base64');
};
var hashPassword = function (password, salt) {
var hmac = crypto.createHmac("sha1", salt);
return hmac.update(password).digest("hex");
};
UserSchema.methods.authenticate = function (password) {
return this.password === hashPassword(password,this.salt);
};
var User = mongoose.model('User', UserSchema);
33
c. Contrôleur d’’authentification
Partie pour la stratégie « local »
var passport = require('passport'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
errorHandler = require('./errorHandler');
exports.register = function (req, res) {
var user = new User(req.body);
var message = null;
user.provider = 'local';
user.displayName = user.firstName + ' ' + user.lastName;
user.save(function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
// sensitive data
user.password = undefined;
user.salt = undefined;
req.login(user, function (err) {
if (err) {
res.status(400).send(err);
} else {
res.json(user);
}
});
}
});
};
exports.login = function (req, res) {
passport.authenticate('local', function (err, user, info) {
if (err || !user) {
res.status(400).send(info);
} else {
user.password = undefined;
user.salt = undefined;
req.login(user, function (err) {
if (err) {
res.status(400).send(err);
} else {
res.json(user);
}
});
}
})(req, res);
};
exports.logout = function(req,res){
req.logout();
req.session.destroy();
res.redirect('/'); //
};
34
Stratégies Facebook, Google, etc.
var noReturnUrls = [
'/login',
'/register'
];
exports.oauthCall = function(strategy,scope){
return passport.authenticate(strategy, scope)
};
exports.oauthCall = function (strategy, scope) {
return function (req, res, next) {
if (noReturnUrls.indexOf(req.query.redirect_to) === -1) {
req.session.redirect_to = req.query.redirect_to;
}
passport.authenticate(strategy, scope)(req, res, next);
};
};
exports.oauthCallback = function (strategy) {
return function (req, res, next) {
var sessionRedirectURL = req.session.redirect_to;
delete req.session.redirect_to;
passport.authenticate(strategy, function (err, user, redirectURL) {
if (err) {
return res.redirect('/login');
}
if (!user) {
return res.redirect('/login');
}
req.login(user, function (err) {
if (err) {
return res.redirect('/login');
}
else{
return res.redirect(redirectURL || '/#' + sessionRedirectURL || '/');
}
});
})(req, res, next);
};
};
35
La fonction servant à connecter/enregistrer l’utilisateur avec stratégie Facebook,
Google, etc.
exports.saveOAuthUserProfile = function (req, providerUserProfile, done) {
var identifierField = 'providerData.' + providerUserProfile.providerIdentifierField;
var searchQuery = {};
searchQuery.provider = providerUserProfile.provider; // facebook, google, etc.
searchQuery[identifierField] =
providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; // id
User.findOne(searchQuery, function (err, user) {
if (err) {
return done(err);
} else {
if (!user) {
var availableUsername = providerUserProfile.username ||
((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : '');
user = new User({
firstName: providerUserProfile.firstName,
lastName: providerUserProfile.lastName,
username: availableUsername,
displayName: providerUserProfile.displayName,
email: providerUserProfile.email,
profileImageURL: providerUserProfile.profileImageURL,
provider: providerUserProfile.provider,
providerData: providerUserProfile.providerData
});
user.save(function (err) {
return done(err, user);
});
}
else {
return done(err, user);
}
}
});
};
36
d. « ErrorHandler »
Pour afficher des messages d’erreurs lisibles pour les visiteurs.
exports.getErrorMessage = function (err) {
var message = '';
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = getUniqueErrorMessage(err);
break;
default:
message = 'Une erreur est survenue';
}
} else {
for (var errName in err.errors) {
if (err.errors[errName].message) {
message = err.errors[errName].message;
}
}
}
return message;
};
var getUniqueErrorMessage = function (err) {
var output;
try {
var fieldName = err.errmsg.substring(err.errmsg.lastIndexOf('.$') + 2,
err.errmsg.lastIndexOf('_1'));
output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' existe
déja';
} catch (ex) {
output = 'Ce champ existe déja';
}
return output;
};
37
2. Partie « client »
a. Module
Routes vers les vues « login » et « register » et check à changement de route pour voir
si l’utilisateur est autorisé à aller à la route suivante
'use strict';
var users = angular.module('users',['ngRoute']);
users.config(function ($routeProvider) {
$routeProvider
.when('/login',{
controller:'AuthController',
templateUrl:'modules/users/views/login.html'
})
.when('/register',{
controller:'AuthController',
templateUrl:'modules/users/views/register.html'
})
}
);
users.run(function ($rootScope, Authentication,$location) {
$rootScope.$on('$routeChangeStart', function (event,next, current) {
if (next && next.$$route && next.$$route.secure) {
if (!Authentication.user) {
$rootScope.$evalAsync(function () {
$rootScope.previousUrl = next.$$route.originalPath;
$location.path('/login');
});
}
}
});
});
Contrôleur gérant
l’authentification
Service permettant de
stocker l’utilisateur
Vues pour se connecter et
s’inscrire
Configuration et routes du
module
38
b. Contrôleur Angular pour l’authentification 'use strict';
angular.module('users').controller('AuthController',
function ($rootScope, $scope, $http, $location, $window, Authentication) {
$scope.authentication = Authentication;
// erreur passée dans l’url
$scope.error = $location.search().err;
var redirect_to = $rootScope.previousUrl || '/';
if ($scope.authentication.user) {
$location.path('/');
}
$scope.register = function () {
$http.post('/api/auth/register',$scope.credentials).then(function(response){
$scope.authentication.user = response.data;
$location.path('/');
},function(response){
$scope.error = response.data.message;
});
};
$scope.login = function () {
$http.post('/api/auth/login',$scope.credentials).then(function(response){
$scope.authentication.user = response.data;
$location.path(redirect_to);
},function(response){
$scope.error = response.data.message;
});
};
$scope.callOauthProvider = function (url) {
$window.location.href = url + (redirect_to ? '?redirect_to=' +
encodeURIComponent(redirect_to) : '');
};
});
c. Service Authentication
Permettant de stocker l’utilisateur connecté
angular.module('users').service('Authentication', ['$window',
function ($window) {
var auth = {
user: $window.user
};
return auth;
}
]);
39
Dans le layout
<script type="text/javascript">
window.user = {{ user | json | safe }}
</script>
d. Formulaires
Connexion
<div class="col-md-offset-4 col-md-4">
<h3>Se connecter avec</h3>
<form ng-submit="login()" class="form-horizontal" autocomplete="off">
<fieldset>
<div class="form-group">
<a class="btn btn-block btn-social btn-facebook" href ng-
click="callOauthProvider('/api/auth/facebook')">
<span class="fa fa-facebook"></span>
</a>
<a class="btn btn-block btn-social btn-google" href ng-
click="callOauthProvider('/api/auth/google')">
<span class="fa fa-google"></span>
Google+
</a>
</div>
<hr class="hrOr">
<div class="form-group">
<label class="sr-only" for="email">Email</label>
<input type="email" id="email" name="email" placeholder="Email"
class="form-control" ng-model="credentials.email"/>
</div>
<div class="form-group">
<label class="sr-only" for="password">Password</label>
<input type="password" id="password" name="password" class="form-control"
placeholder="Mot de passe" ng-model="credentials.password"/>
</div>
<div class="text-center form-group">
<button type="submit" class="btn btn-primary">Se connecter</button>
ou
<a href="#/register">S'inscrire</a>
</div>
</fieldset>
</form>
<alert type="danger" ng-show="error" class="text-center text-danger">
<span ng-bind="error"></span>
</alert>
</div>
Utilisation de Bootstrap social pour les boutons Facebook et Google+
bower install bootstrap-social --save
Référencer bootstrap social et font awesome dans le layout
<link type="text/css" rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
<link type="text/css" rel="stylesheet" href="/lib/bootstrap-social/bootstrap-social.css"
/>
<link type="text/css" rel="stylesheet" href="/lib/font-awesome/css/font-awesome.css" />
40
Inscription
<div class="col-md-offset-4 col-md-4">
<h3>Inscription</h3>
<alert type="danger" ng-show="error" class="text-center text-danger">
<span ng-bind="error"></span>
</alert>
<form name="userForm" ng-submit="register()" class="form-horizontal" novalidate
autocomplete="off">
<fieldset>
<div class="form-group">
<label for="firstName">Prénom</label>
<input type="text" required id="firstName" name="firstName" class="form-
control"
ng-model="credentials.firstName" placeholder="Prénom">
</div>
<div class="form-group">
<label for="lastName">Nom</label>
<input type="text" id="lastName" name="lastName" class="form-control"
ng-model="credentials.lastName" placeholder="Nom">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control"
ng-model="credentials.username" placeholder="Username">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="Email"
class="form-control" ng-model="credentials.email"/>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password" class="form-control"
placeholder="Mot de passe" ng-model="credentials.password"/>
</div>
<div class="form-group">
<label for="confirmpassword">Confirmer le mot de passe</label>
<input type="password" id="confirmpassword" name="confirmpassword"
class="form-control" ng-model="credentials.confirmpassword" placeholder="Confirmer le mot
de passe" required>
</div>
<div class="text-center form-group">
<button type="submit" class="btn btn-primary">S'inscrire</button>
ou
<a href="#/login">Se connecter</a>
</div>
</fieldset>
</form>
</div>
41
Ce qui donne …
Connexion
Inscription
42
VI. Module quelconque Exemple avec « articles » que l’on pourrait retrouver avec un CMS.
Détails Liste
Edition
Boutons édition,
suppression visibles
que si l’utilisateur
est l’auteur de
l’article
Affichage du contenu
au format HTML
Content éditable
et sauvegarde au
format HTML vers
MongoDB. L’ajout
et la modification
d’articles
réclament des
utilisateurs
authentifiés
43
1. Partie client (Angular)
a. Module
Avec une organisation du projet par modules.
Pour chaque module on crée un fichier dans lequel on définit le nom du
module, ses dépendances, routes pour ce module uniquement (on peut aussi
créer un fichier de routes à part dans un dossier « routes »)
'use strict';
var articles = angular.module('articles',['ngRoute','ngResource']);
articles.config(function ($routeProvider) {
$routeProvider
.when('/articles/',{
controller:'ArticleController',
templateUrl:'modules/articles/views/list.html'
})
.when('/articles/view/:id',{
controller:'ArticleController',
templateUrl:'modules/articles/views/view.html'
})
.when('/articles/create',{
controller:'ArticleController',
templateUrl:'modules/articles/views/create.html',
secure:true
})
.when('/articles/edit/:id',{
controller:'ArticleController',
templateUrl:'modules/articles/views/edit.html',
secure:true
})
}
);
Routes : On peut utiliser soit $routeProvider, soit $stateProvider
On enregistre le module dans un module principal (dans un fichier « config.js »
dans un dossier « config » du module « core »)
var app = angular.module('app',['core','articles','users','ngRoute']);
Configuration du
module
Vues
Services du module
Contrôleurs Angular
Directives
44
Sécuriser une route
Dans l’exemple la création et modification d’articles demandent d’être authentifié.
De plus, chaque utilisateur ne pourra modifier que les articles dont il est l’auteur.
Indiquer « secure » dans la route
$routeProvider
.when('/articles/create',{
controller:'ArticleController',
templateUrl:'modules/articles/views/create.html',
secure:true
})
Dans le module « users » on vérifie à chaque changement de route si l’utilisateur est
autorisé à accéder à la route suivante. Si ce n’est pas le cas on le redirige vers la vue
de login.
'use strict';
var users = angular.module('users',['ngRoute']);
// routing du module
users.run(function ($rootScope, Authentication,$location) {
$rootScope.$on('$routeChangeStart', function (event,next, current) {
if (next && next.$$route && next.$$route.secure) {
if (!Authentication.user) {
$rootScope.$evalAsync(function () {
$rootScope.previousUrl = next.$$route.originalPath;
$location.path('/login');
});
}
}
});
});
Pour rendre un article éditable seulement par son auteur, on n’affiche les boutons
édition et suppression que si l’identifiant de l’utilisateur actuel correspond à celui de
l’auteur de l’article.
<div class="pull-right" ng-show="authentication.user._id == article.user._id">
<a href="#/articles/edit/{{ article._id}}" class="btn btn-default">Modifier</a>
<a class="btn btn-primary" ng-click="delete();">
Supprimer
</a>
</div>
45
b. Contrôleur(s) Angular
Contiennent les crud. Un contrôleur peut servir pour plusieurs vues. Exemple de
contrôleur utilisant un service avec $resource
'use strict';
angular.module('articles').controller('ArticleController', ['$scope', '$routeParams',
'$location', 'Authentication', 'Articles', 'flash',
function ($scope, $routeParams, $location, Authentication, Articles, flash) {
$scope.authentication = Authentication;
$scope.getAll = function () {
$scope.articles = Articles.query();
};
$scope.findOne = function () {
$scope.article = Articles.get({
id: $routeParams.id
});
};
$scope.create = function () {
var article = new Articles({
title: this.title,
content: this.content
});
article.$save(function (response) {
flash.setMessage('Article ajouté.');
$location.path('/articles/view/' + response._id);
$scope.title = '';
$scope.content = '';
}, function (errorResponse) {
$scope.error = errorResponse.data.message;
});
};
$scope.update = function () {
var article = $scope.article;
article.$update(function () {
flash.setMessage('Article modifié.');
$location.path('/articles/view/' + article._id);
}, function (errorResponse) {
$scope.error = errorResponse.data.message;
});
};
$scope.delete = function () {
$scope.article.$delete({id: this.article._id},function () {
flash.setMessage('Article supprimé.');
$location.path('/articles/');
});
};
}
]);
46
c. Services
Service avec $http
Documentation
'use strict';
angular.module('articles').service('Articles',function($http){
var baseUrl = "/api/articles/";
this.getAll = function(){
return $http.get(baseUrl);
};
this.findOne = function (id) {
return $http.get(baseUrl + id);
};
this.create = function (article) {
return $http.post(baseUrl,article);
};
this.update = function (article) {
return $http.put(baseUrl,article);
};
this.delete = function (id) {
return $http.delete( baseUrl + id);
}
});
Service avec $resource
Documentation
Installer avec Bower angular-resource
bower install angular-resource --save
Le référencer dans le layout
<script type="text/javascript" src="/lib/angular-resource/angular-
resource.min.js"></script>
Création du service utilisé par le contrôleur
'use strict';
angular.module('articles').factory('Articles', ['$resource',
function ($resource) {
return $resource('api/articles/:id', {
id: '@id'
}, {
update: {
method: 'PUT'
}
},
{
delete:{
method:'DELETE'
}
});
}
]);
Création d’une fonction
delete avec passage du
paramètre id dans l’url
Création d’une fonction
update avec utilisation de la
méthode PUT (non présente
de base)
47
d. Vues
Vue liste
<section ng-init="getAll()">
<div>
<h1>Articles</h1>
<hr>
</div>
<div ng-show="articles.length > 0">
<a href="#/articles/create" class="btn btn-primary">Ajouter un nouvel
article</a>
<div ng-repeat="article in articles">
<article>
<header class="entry-header">
<h2><a href="#/articles/view/{{ article._id }}" >{{ article.title
}}</a></h2>
<div class="entry-meta">
<small>
<em class="text-muted">
Posté le <span class="created">{{ article.created |
date:'mediumDate' }}</span>
par <span class="author">{{ article.user.username
}}</span>
</em>
</small>
</div>
</header>
<div class="entry-content">
<div dynamic="article.content" ></div>
</div>
</article>
</div>
</div>
<div class="alert alert-warning text-center" ng-show="articles.length==0">
Voulez-vous ajouter <a href="#/articles/create">le premier article</a>?
</div>
</section>
ng-init : chargement des données à partir du contrôleur Angular, pour la vue détails
on utilse « findOne() »
<section ng-init="getAll()">
ng-repeat pour faire une boucle avec utilisation soit de directive « ng-bind » sur une
balise soit de variable avec accolades {{ }}
Liens
Pour afficher directement une vue
<a href="#/articles/create" class="btn btn-primary">Ajouter un nouvel
article</a>
Avec un paramètre
<a href="#/articles/view/{{ article._id }}" >{{ article.title }}</a>
Lien avec appel d’une fonction du contrôleur
<a class="btn btn-primary" ng-click="delete();">Supprimer</a>
Note ici Bootstrap met en forme le lien, mais on peut afficher le lien correctement
sans style en indiquant simplement « href » sans valeur. Exemple
<a href ng-click="delete();">Supprimer</a>
48
Localisation des dates
Documentation avec Angular pour les dates
Copier le fichier dans la langue désirée et le référencer. Les dates avec filtres par
exemple seront automatiquement localisées.
{{ article.created | date:'mediumDate' }}
Affichage du contenu au format HTML
Créer une directive dans un dossier « directives »
angular.module('core').directive('dynamic', ['$compile', function
($compile) {
return {
restrict: 'A',
replace: true,
link: function link(scope, ele, attrs) {
return scope.$watch(attrs.dynamic, function (html) {
ele.html(html);
return $compile(ele.contents())(scope);
});
}
};
}]);
Utilisation
<div dynamic="article.content" ></div>
49
Formulaire <section>
<div class="page-header">
<h1>Ajouter un nouvel article</h1>
</div>
<div class="col-md-12">
<form name="articleForm" class="form-horizontal" ng-submit="create()" novalidate>
<fieldset>
<div class="form-group">
<label class="control-label" for="title">Titre</label>
<div class="controls">
<input name="title" type="text" ng-model="title" id="title"
class="form-control" placeholder="Saisissez votre titre ici" required>
</div>
</div>
<div class="form-group">
<div class="controls">
<div contenteditable ng-model="content" class="form-control
content-editable"></div>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-default" value="Publier">
</div>
<div ng-show="error" class="text-danger">
<strong ng-bind="error"></strong>
</div>
</fieldset>
</form>
</div>
</section>
ng-submit sur la balise form pointe la fonction du contrôleur permettant de valider
l’ajout, la modification du formulaire.
Une variante consiste à utiliser ng-click sur le bouton submit
ng-model pour binder les valeurs en édition du formulaire
Afficher un message d’erreur
<div ng-show="error" class="text-danger">
<strong ng-bind="error"></strong>
</div>
50
Permettre de sauvegarder le contenu formaté en html dans une base MongoDB
Création d’une directive
angular.module('core').directive("contenteditable", function() {
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attrs, ngModel) {
function read() {
ngModel.$setViewValue(element.html());
}
ngModel.$render = function() {
element.html(ngModel.$viewValue || "");
};
element.bind("blur keyup change", function() {
scope.$apply(read);
});
}
};
});
Utilisation
<div contenteditable ng-model="article.content"></div>
On peut mettre en forme ce bloc avec Bootstrap par exemple
<div contenteditable ng-model="article.content" class="form-control
content-editable"></div>
Et ajouter une classe CSS simple
.content-editable{ height: 250px; overflow: auto;}
On peut imaginer créer une barre permettant de mettre en forme, insérer des
images, etc. un peu comme un « tinyMCE »
Une div éditable avec
l’illusion que c’est un
textarea
51
2. Partie Serveur
a. Routes côté serveur
Il faut renseigner les routes dans « routes.js » du dossier « config » du « server »
var articleController = require('../controllers/articleController'),
auth = require('../controllers/authController');
module.exports = function (app) {
app.get('/api/articles/', articleController.list);
app.get('/api/articles/:id', articleController.read);
app.post('/api/articles/', articleController.create);
app.put('/api/articles/', articleController.update);
app.delete('/api/articles/:id', articleController.delete);
// etc.
}
b. Modèle Mongoose
Dans un dossier « models » du « server »
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var articleSchema = new Schema({
title: {type:String, required:'Vous devez renseigner un titre'},
content: {type:String, required:'Votre article n\'a aucun contenu.'},
user : { type: Schema.ObjectId, ref: 'User' },
created: {type: Date, default: Date.now }
});
var Article = mongoose.model('Article', articleSchema);
Ajouter les routes
Créer un contrôleur
pour mettre à jour la
base de données
Créer un modèle
Mongoose
52
Modèle avec relation
Pour “user” on indique le nom du schéma en relation
user : { type: Schema.ObjectId, ref: 'User' },
L’élément “user” sera chargé depuis le contrôleur grâce à la function “populate”
exports.list = function (req, res) {
Article.find().sort('-created').populate('user').exec(function (err, articles) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};
53
c. Contrôleur « Express »
C’est lui qui va recevoir des requêtes depuis le service Angular et renvoyer les
réponses au format JSON
var mongoose = require('mongoose'),
Article = mongoose.model('Article'),
errorHandler = require('./errorHandler');
exports.list = function (req, res) {
Article.find().sort('-created').populate('user').exec(function (err, articles) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};
exports.read = function (req,res){
Article.findOne({ _id: req.params.id }).populate('user').exec(function (err,
article) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
exports.create = function (req, res) {
var article = new Article(req.body);
article.user = req.user || req.session.passport.user;
article.save(function(err){
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
exports.update = function (req, res) {
Article.update({ _id: req.body._id }, { $set: { title: req.body.title, content:
req.body.content } },
function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(req.body);
}
});
54
};
exports.delete = function (req, res) {
Article.remove({ _id: req.params.id }, function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.send('Ok')
}
});
};
55
VII. Plus …
1. Express
a. Variables partagées var express = require('express'),
app = express();
app.locals.pageTitle="Ma variable";
Pas besoin de passer la variable pour pouvoir l’afficher dans la vue. Exemple avec
ejs
<h1><%= pageTitle %></h1>
b. Charger un fichier JSON
On charge simplement un fichier json
app.locals.customers = require("./customers.json");
Le contenu du fichier “customers.json”
[
{"name":"M. PILLON","email":"[email protected]"},
{"name":"M. BELLIN","email":"[email protected]"}
]
Exemple avec ejs
<ul>
<% customers.forEach(function(customer){ %>
<li><%= customers.name %></li>
<% }) %>
</ul>
c. Changer le répertoire des vues app.set('views', __dirname + '/views');
d. Ressources (dossier « public »)
app.use(express.static(__dirname + "/public"));
Utilisation d’une ressource
<link href="/css/default.css" rel="stylesheet" />
Référencer le répertoire « public » de
l’application
Dans un dossier « public »
Inutile d’indiquer le répertoire « public »
56
e. Express Generator
Créer un projet avec Express generator
Installer express generator en global
npm install express-generator -g
Naviguer jusqu’au dossier parent qui contiendra le du projet puis
express nomprojet
Installation des dépendances du projet généré : Naviguer jusqu’au dossier du projet
généré puis
npm install
Lancer le serveur
node bin/www
On peut se rendre à la page « http://localhost:3000/ »
57
2. “Sans Express”
a. Serveur
b. Créer un serveur (fichier « server.js » ou « app.js » par exemple)
var http = require('http'); var html = "<!DOCTYPE html><html><head><meta charset=\"utf-8\" /><title>Demo NodeJS</title></head><body><h1>Démonstration NodeJS</h1></body></html>"; var server = http.createServer(function (req, res) { res.writeHead(200, { "Content-Type": "text/html" }); res.write(html); res.end(); }); server.listen(8080); console.log("listen on port 8080");
b. Lancer un serveur :
Depuis une invite de commande, naviguer jusqu’au dossier
du projet puis
node server.js
c. Aller à « http://localhost:8080/ » pour tester
(CTRL + C pour arrêter le serveur)
c. URL et paramètres
Paramètres d’url (querystring)
var http = require('http'); var url = require('url'); var querystring = require('querystring'); var server = http.createServer(function (req, res) { var params = querystring.parse(url.parse(req.url).query); res.writeHead(200, { "Content-Type": "text/plain" }); if ('firstname' in params && 'lastname' in params) { res.write('Hello ' + params['firstname'] + ' !'); } else { } res.end(); }); server.listen(8080);
Réponse. Types de contenu :
Texte : text/plain
HTML : text/html
CSS : text/css
Image : image/jpeg
Vidéo : video/mp4
Zip : application/zip
Etc.
Création du serveur
« req » pour request,
« res » pour response
Chargement du module « http »
Port 3000 ou 8080 en local
Besoin du module
« querystring » pour récupérer
les paramètres dans l’url
Récupération des paramètres
passés dans un tableau
Accès aux membres du tableau
de paramètres
58
d. Créer un index listant les contrôleurs (function (controllers) { var homeController = require('./homeController.js'); var contactController = require('./contactController.js'); controllers.init = function (app) { homeController.init(app); contactController.init(app); } })(module.exports);
Utilisation
var express = require('express'), app = express(), controllers = require('./controllers'); app.set("view engine", "vash"); controllers.init(app); app.listen(8080);
e. Evénements
Ecouter et émettre des événements
var eventEmitter = require('events').EventEmitter; var myEvent = new eventEmitter(); myEvent.on('myEventEmittted', function (message) { console.log(message); }); myEvent.emit('myEventEmittted', 'Evénement déclenché');
On indique le dossier
contenant les contrôleurs
Initialisation des contrôleurs en
appelant la méthode
Besoin du module « events »
On écoute l’événement, avec fonction callback
59
3. Bases de données
a. SQL Server
Plusieurs Drivers existent… Exemple avec Tedious (documentation)
npm install tedious
Note : il faut lancer SQL Browser
var Connection = require('tedious').Connection; var Request = require('tedious').Request; exports.getAll = function (req, res) { var config = { server: 'DonJR', userName: 'sa', password: 'password123456' , database: 'peopledb', options : { instanceName: 'SqlExpress', rowCollectonOnRequestCompletion: 'true' } } var connection = new Connection(config); connection.on('connect', function (err) { if (err) { console.log("Erreur"); } else { var request = new Request("use peopledb; select * from Person", function (err, rowCount, columns) { var result = []; columns.forEach(function (column) { var person = new Person(column[1].value, column[2].value); result.push(person); }); res.render('index.ejs', { people: result }); }); connection.execSql(request); } }); };
Autre exemple avec paramètres
var TYPES = require('tedious').TYPES; var sql = 'use peopledb; insert into Person(name,twitter) values(@name,@twitter);'; var request = new Request(sql, function (err) { console.log("Erreur"); }); request.addParameter('name', TYPES.VarChar, 'M. Bellin'); request.addParameter('twitter', TYPES.VarChar, '@mbellin'); connection.execSql(request);
Nom du serveur, indiquer le
nom de l’instance
(instanceName) dans
« options ».
« rowCompletionOnRequestCompletion »
permet de récupérer le résultat de la
requête en callback
Exécution de la
requête
Paramètres
60
b. MongoDB
Autres bases NoSQL : RavenDB, Cassandra, Neo4j, Redis, CouchDB
Installation locale au projet avec Windows
a. Télécharger le zip de MongoDB (ou le msi et choisir le dossier du projet
pourl’installation)
b. Créer un dossier « mongodb » dans le projet et y copier l’ensemble de
l’archive
c. Créer un dossier « data » dans « mongodb »
d. Ouvrir une invite de commande, naviguer jusqu’au dossier « mongodb » du
projet et indiquer le dossier dans lequel la base est installée
mongod –dbpath ./data
(Un message « waiting for connections on port 27017 » apparait si tout se passe bien)
Pour avoir accès aux bases depuis « http://localhost:28017 »
mongod –dbpath ./data --rest
61
Créer une base en invite de commandes
Une base de données NoSQL a des collections. Chaque collection a des documents
(JSON), « schemaless » (par exemple un document peut avoir seulement un nom et
un autre avoir un nom et un email)
Créer ou utiliser une base
use peopledb
Insertion d’un document dans une collection
db.people.insert({name:"J. Romagny", twitter:"@romagny13"})
Un identifiant unique est ajouté à chaque document (_id)
Obtenir un document
Utilisation de la méthode « find »
Retourner tous les documents de la collection
db.people.find()
Filtrer les résultats
$gt : plus grand
$lt : plus petit
$gte : plus grand ou égal
$lte: plus petit ou égal
$or : ou
$and : et
$in, $all, $exist, $type ,$regex
db.people.find({name : "J. Romagny"})
db.people.find({age : {$gt :18}})
Trier
Avec la méthode « sort »
db.people.find().sort({name :1})
Modification de document
db.people.update({name : "J. Romagny"}, {$set :{twitter : "@jerome_romagny13"}})
Supprimer un document
db.people.remove ({name : "J. Romagny"})
$unset permet de supprimer une propriété du document
Collection Document
Modification de la propriété « twitter » Filtre des documents recherchés Méthode « update »
Tri de la collection de personnes sur le nom en ordre
croissant
Pour un ordre décroissant utiliser « -1 »
62
VIII. MEAN.JS MEAN = MongoDB + Express + AngularJS + Node.js
Documentation
Installation
a. Pré-requis
Node.js et NPM
MongoDB
Bower
Grunt
b. Installation avec Yo
Installation de Yeoman en global
npm install -g yo
Installation du generator meanjs
npm install -g generator-meanjs
Puis création d’un projet MEAN …Naviguer jusqu’au dossier où créer le projet avec la
commande puis
yo meanjs
Répondre aux questions… donner un nom à l’application …
c. Une fois le projet généré. Installer tous les modules avec la commande
npm install
Lancer MongoDB. Depuis une invite de commande, naviguer jusqu’au dossier
d’installation puis
mongod
Et lancer le serveur. Depuis une invite de commande naviguer jusqu’au projet mean
et
grunt
Ou simplement
node server.js
63
Le projet généré
Configuration
Serveur
Package avec informations du
projet et dépendances
Chaque module est divisé avec une partie
client (Angular) et une partie serveur (Node)
Librairies installées avec NPM
64
Exemple de module (« users » pour l’authentification, utilise Passport)
Moteur de vues utilisé Swig
Les vues sont dans la partie « client » des modules
Le layout, la page index, pages « complètes » sont dans la partie « server »
Le module core
Client
Contient le fichier de config permettant d’enregistrer des modules pour
l’application
Fichier init qui permet également d’intercepter au changement d’état et
vérifier l’autorisation. Des rôles sont définis pour les états et c’est par rapport à
ceux-ci que l’on vérifie si l’utilisateur a les droits pour accéder au prochain
état. S’il n’est pas autorisé, l’utilisateur est redirigé vers la page
d’authentification.
Routes avec $stateProvider avec les vues home, 404, un fichier pour les routes
admin
Contient également le header avec le menu principale inclus dans le layout
du site
Un contrôleur pour le header (avec le service « Authentication » pour switcher
entre utilisateur connecté et anonyme) et un contrôleur pour la vue home
Feuille de style core.css, images (favicon,logo)
Un service pour le menu
Server
Routes pour page index (‘/’) et pages not found,etc.
Le contrôleur ne sert qu’à afficher les pages index, not found,etc.
Le layout, la page index, pages « complètes » (404,500)
65
Génération de code Documentation
Création d’un module MEAN
yo meanjs:mean-module
Server … « Express »
Création d’un contrôleur côté serveur avec crud (create, read, update,
delete, list)
yo meanjs:express-controller
Choisir le module (exemple « voyages ») puis donner un nom au contrôleur
Modèle pour Mongoose
yo meanjs:express-model
Choix du module et nom du modèle
Routes pour le module
yo meanjs:express-route
Choix du module et nom du fichier de routes
Client … « Angular »
Contrôleur Angular
yo meanjs:angular-controller
Choix du module et nom du contrôleur
Routes Angular avec $stateProvider
yo meanjs:angular-route
Choix du module, nom, …
Vue
yo meanjs:angular-view
Choix du module, nom de la vue, du contrôleur, routes
Exemple de module généré nommé « voyage »
66
Angular
« Express »
67
IX. Publication Heroku
1. Créer un compte
2. Créer une nouvelle application
3. Donner un nom d’application
68
4. Plusieurs méthodes de déploiement disponibles
Publication du projet
1. « package.json »
{
"name": "NodejsAuthDemo",
"version": "0.0.0",
"description": "NodejsAuthDemo",
"main": "server.js",
"author": {
"name": "romagny13",
"email": ""
},
"engines": {
"node": "0.12.x",
"npm": "2.7.x"
},
"dependencies": {
"body-parser": "^1.12.3",
"ejs": "^2.3.1",
"express": "^4.12.3",
"mongoose": "^4.0.2",
"passport": "^0.2.1",
"passport-local": "^1.0.0"
}
}
2. « procfile.js »
Créer un fichier « procfile.js » à la racine du projet et y ajouter le nom du serveur
web: node app.js
Ajouter une section « engines » avec les
versions de node et npm
… que l’on peut obtenir avec « node --
version » et « npm --version »).
69
3. Dans le serveur « server.js » ou « app.js » selon le nom donné, modifier le port
var port = process.env.PORT || 3000;
app.listen(port);
console.log("Listen on " + port + "...");
4. MongoDB addons (option « free » mais demande quand même de rentrer des
informations de paiement)
Permet de créer une base. On obtient également les informations de connexion
(Créer un utilisateur minimum) et modifier la chaine de connexion dans le projet.
mongoose.connect('mongodb://………);
Port de test