Webpack: un pas à pas avec une application AngularJs

Familiarisation

Si je devais résumer, Webpack est un outils permettant le packaging de nos applications, en nous évitant (autant que possible) d'avoir à écrire des scripts compliqués, avec ou sans outils extérieure (comme Gulp ou Gulp).

Le principe aussi est de se reposer intégralement sur un fichier de configuration.

Pour illustrer tout ça, allons faire une petite application toute simple, en VanillaJS et ES6.

Soit notre fichier HTML src/index.html:

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type="text/javascript" src="../target/bundle.js" charset="utf-8"></script>
    </body>
</html>


Le but étant de charger le fichier src/entry.js:

import content from './content';

['a content', 'a second content'].forEach(contentToInject => {
    let divElement = document.createElement('div');
    divElement.innerText = `${content} :: ${contentToInject}`;
    divElement.classList.add('my-content');
    document.body.append(divElement);
});


Qui importera le fichier src/content.js:

export default 'It works from content.js.';


Il nous "suffit" alors de déclarer un fichier "webpack.config.js" et de lancer la commande "webpack" à la racine du projet.

Regardons le contenu du fichier de configuration Webpack:

// https://webpack.js.org/concepts'use strict';

const path = require('path');

module.exports = {
    'entry': path.resolve(__dirname, './src/entry.js'),
    'output': {
        'path': path.resolve(__dirname, 'target'),
        'filename': 'bundle.js',
        'libraryTarget': 'umd'    },
    'devtool': 'inline-source-map',
    'module': {
        'rules': [
            {
                'test': /\.js$/,
                'exclude': /(node_modules|bower_components)/,
                'use': {
                    'loader': 'babel-loader'                }
            }
        ]
    }
};


Comme nous pouvons le voir, par le biais de "entry" et "output", nous précisons le fichier d'entrée de notre application JavaScript et où nous devons l'exporter. Il est à noté que nous pouvons créer un mapping d'objets en entrée et en sortie (mais nous le ferons plus tard).


Nous voyons ensuite "devtool" afin d'indiquer (si c'est nécessaire) nous devons générer des sourcemaps.

Mais surtout le plus intéressant est la section module, avec ces "rules". Si nous regardons bien, nous allons préciser, pour un ensemble de ressources, quels "loaders" nous devons appliquer.

Un loader est en quelque sorte un plugin Webpack de transformation. Ici, nous avons "babel-loader", pour transpiler notre code JavaScript. Mais nous regardons bien, nous en avons une grande quantitée:



Si nous avions envie d'avoir plusieurs fichiers d'entrés, et donc plusieurs fichiers de sortie, nous aurions procédé de la façon suivante:

// https://webpack.js.org/concepts'use strict';

const path = require('path');
const fs = require('fs');

module.exports = {
    'target': 'web',
    'entry': {
        'gettingStarted': path.resolve(__dirname, './src/entry.js'),
        'angularApp': path.resolve(__dirname, './src/index.js')
    },
    'output': {
        'path': path.resolve(__dirname, './target'),
        'filename': '[name].js',
        'libraryTarget': 'umd'    },
    'devtool': 'inline-source-map',
    'module': {
        'rules': [
            {
                'test': /\.js$/,
                'exclude': /(node_modules)/,
                'use': {
                    'loader': 'babel-loader'                }
            }
        ]
    }
};

Notre application AngularJs

Maintenant, essayons de voir par rapport à une application Angular. Pour plus de facilités par la suite, nous allons déplacer notre fichier de configuration Webpack dans un sous-dossier "config":


Et nous lançerons Webpack avec la commande suivante:

webpack -- --config ./config/webpack.config.js


Imaginons, nous en avons une. nous la convertissons en application ES6, et notre fichier principale est le suivant:

/** * A module which contains the definition of the application and the base of configuration * * @module app/index/services/index * @exports app/index * * @author Julien Roche * @version 0.0.1 * @since 0.0.1 */
import 'angular';
import 'angular-route';
import 'angular-messages';

import 'lodash';
import 'moment';

import './constants';
import './vendors';
import './users/index';

export default angular    .module('app', [
        'ngRoute',
        'ngMessages',
        'app.constants',
        'app.vendors',
        'app.users'    ])
    .config(['$routeProvider', function ($routeProvider) {
        $routeProvider
            .otherwise({
                'redirectTo': '/home'            });
    }]);


Et bien sachez le, la configuration que nous avons au-dessus va parfaitement marcher, et votre fichier index.html ne sera pas à modifier.

En revanche, ici, dans les fichiers générés, nous allons trouvé dedans le contenu des frameworks "angular", "lodash", ....

Et ce n'est pas forcément ce que nous voulons (surtout si nous réalisons un plugin).

Du coup, nous allons modifier le fichier HTML pour pointer vers les ressources extérieures. Et surtout, nous allons modifier le fichier webpack.config.js comme suit:

// https://webpack.js.org/concepts'use strict';

const path = require('path');
const fs = require('fs');

module.exports = {
    'target': 'web',
    'entry': {
        'gettingStarted': path.resolve(__dirname, '../src/entry.js'),
        'angularApp': path.resolve(__dirname, '../src/index.js')
    },
    'output': {
        'path': path.resolve(__dirname, '../target'),
        'filename': '[name].js',
        'libraryTarget': 'umd'    },
    'devtool': 'inline-source-map',
    'module': {
        'rules': [
            {
                'test': /\.js$/,
                'exclude': /(node_modules)/,
                'use': {
                    'loader': 'babel-loader'                }
            }
        ]
    },
    'resolve': {
        'modules': [
            path.resolve(__dirname, '../src'),
            'node_modules'        ],
        'extensions': [
            '.js',
            '.jsx',
            '.css',
            '.json'        ]
    },
    'externals': [
        fs.readdirSync('node_modules')
    ]
};

Ici, nous indiquons via "externals" que les fichiers JavaScripts venant de ces répertoires ne doivent pas être pris en compte. A noter la présence de "resolve" qui précise juste le comportement de l'import par défaut (si nous omettons l'extension, nous recherchons des fichiers .js, .jsx, ...)


Et si j'utilise Less ?

Ici, nous allons devoir modifier deux fichiers.

Le premier est le fichier d'entrée de notre application Angular afin d'importer noter fichier Less:

import 'lodash';
import 'moment';

import './styles/index.less';

import './constants';
import './vendors';
import './users/index';


Ensuite, nous allons modifier le fichier webpack.config.js afin de traiter les fichiers Less, de les compiler, etc ...


// https://webpack.js.org/concepts'use strict';

const path = require('path');
const fs = require('fs');

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractLess = new ExtractTextPlugin({
    'filename': `[name].css`});

module.exports = {
    'target': 'web',
    'entry': {
        'gettingStarted': path.resolve(__dirname, '../src/entry.js'),
        'angularApp': path.resolve(__dirname, '../src/index.js')
    },
    'output': {
        'path': path.resolve(__dirname, '../target'),
        'filename': '[name].js',
        'libraryTarget': 'umd'    },
    'devtool': 'inline-source-map',
    'module': {
        'rules': [
            {
                'test': /\.js$/,
                'exclude': /(node_modules)/,
                'use': {
                    'loader': 'babel-loader'                }
            },
            {
                'test': /\.less$/,
                'use': extractLess.extract({
                    'use': [
                        {
                            'loader': 'css-loader'                        },
                        {
                            'loader': 'less-loader',
                            'options': {
                                'compress': false                            }
                        }
                    ],
                    // use style-loader in development                    'fallback': 'style-loader'                })
            }
        ]
    },
    'resolve': {
        'modules': [
            path.resolve(__dirname, '../src'),
            'node_modules'        ],
        'extensions': [
            '.js',
            '.jsx',
            '.css',
            '.json'        ]
    },
    'externals': [
        fs.readdirSync('node_modules')
    ],
    'plugins': [
        extractLess    ]
};

Il est à noter que le "css-loader" et le "style-loader" sont nécessaires afin que Webpack analyse les fichiers CSS générés. Ce sera utile notamment lorsque nous voudrons copier les images / Webfonts automatiquement (autrement, Webpack va juste changer l'url).

A noter que nous pouvons importer des ressources "externals" Less. En revanche, pour éviter qu'elles soient injectés dans le code CSS généré, il faudra utiliser "@import (reference)"


Et pour copier les ressources, il faudra tout simplement déclarer les "rules" suivantes:

// fonts{
    'test':  /\.(woff|woff2|eot|ttf|otf)$/,
    'exclude': /(node_modules)/,
    'use': {
        'loader': 'file-loader',
        'options': {
            'name': '[name].[ext]',
            'useRelativePath': true        }
    }
},
// assets{
    'test':  /\.(png|svg|jpg|gif)$/,
    'exclude': /(node_modules)/,
    'use': {
        'loader': 'file-loader',
        'options': {
            'name': '[name].[ext]',
            'useRelativePath': true        }
    }
},


Simplifions notre code / projet

Un autre intérêt de Wepack: la simplification de notre outillage. Pour une application Angular, il fallait souvent avoir une tâche qui allait générer les fichiers templates Angular, ou encore une autre pour avoir nos fichiers de traduction.


Pour le cas des templates, imaginons que nous avons une "rule" webpack suivante:

{
    'test': /\.html/,
    'exclude': /(node_modules)/,
    'use': {
        'loader': 'html-loader'    }
}


Cette dernière permet tout simplement de pouvoir importer des fichiers HTML. Ce qui fait que notre directive ressemble à ça:

import template from './user-details.html';

export default function () {
    return {
        'restrict': 'E',
        'template': template,
        'scope': {
            'user': '='        },
        'controller': [
            '$scope',
            function ($scope) {

                /**                 * The user has got an address ?                 *                 * @method                 * @returns {boolean}                 */                $scope.hasAddress = function () {
                    return !!($scope.user && $scope.user.address);
                };
            }
        ]
    };
};

Donc plus besoin d'un tâche pour compiler nos templates Angular !


Et pour nos traductions, si nous utilisons angular-gettext, nous pouvons déclarer la "rule" suivante:

{
    'test': /\.po/,
    'exclude': /(node_modules|bower_components)/,
    'use': [
        {
            'loader': 'angular-gettext-loader'        }
    ]
},


Et du coup, d'importer les fichiers ".po" dans notre projet:

import './styles/index.less';

import './locales/fr.po';
import './locales/en.po';
import './locales/es.po';

import './constants';

Et ces derniers seront compilés et injectés dans notre application


Petite analyse

Nous pouvons voir que Webpack est puissant. En revanche, méfiez-vous

En effet, pour arriver à cette configuration, en toute honnêteté, ce n'est pas simple !


D'une part, nous avons une transition cassante entre Wepack 1.X et Webpack 2.X. Dans cet article, j'utilisais Webpack 3.X, donc la nouvelle façon d'écrire un fichier de configuration. Et cela va vous poser problème lorsque nous cherchons des informations et des exemples sur Internet.


Ensuite, la documentation est une catastrophe: minimaliste au possible et très compliqué d'extraire quelque chose d'utile. Et du coup, nous plongeons des heures dans StackOverflow.


De plus, cela fait que pendant un moment, le fonctionnement de Webpack semble mystérieux, et que le moindre "fixe" de notre configuration peut nous amener à cette situation:




Et nous n'avons pas regardé ce qu'il faut faire pour tester et pour être en mode "production" ! Car il reste du travail à faire.


En soit, Webpack est un outil intéressant, mais vraiment pas à la portée de tous

Commentaires

Posts les plus consultés de ce blog

ISO: liens & outils utiles

NodeJs et SSL: une petite analyse

Créer sa commande slack en quelques minutes