keeping the frontend under control with symfony and webpack

90
Keeping the frontend under control with Symfony and Webpack Nacho Martín @nacmartin [email protected] Munich Symfony Meetup October’16

Upload: ignacio-martin

Post on 16-Apr-2017

2.107 views

Category:

Internet


1 download

TRANSCRIPT

Page 1: Keeping the frontend under control with Symfony and Webpack

Keeping the frontend under control with Symfony and Webpack

Nacho Martín @nacmartin

[email protected]

Munich Symfony Meetup October’16

Page 2: Keeping the frontend under control with Symfony and Webpack

I write code at Limenius

We build tailor made projects using mainly Symfony and React.js

So we have been figuring out how to organize better the frontend

Nacho Martín

[email protected] @nacmartin

Page 3: Keeping the frontend under control with Symfony and Webpack

Why do we need this?

Page 4: Keeping the frontend under control with Symfony and Webpack

Assetic?No module loading

No bundle orientation

Not a standard solution for frontenders

Other tools simply have more manpower behind

Written before the Great Frontend Revolution

Page 5: Keeping the frontend under control with Symfony and Webpack

Building the Pyramids: 130K man years

Page 6: Keeping the frontend under control with Symfony and Webpack

Writing JavaScript: 10 man days

JavaScript

Page 7: Keeping the frontend under control with Symfony and Webpack

Making JavaScript great: NaN man years

JavaScript

Page 8: Keeping the frontend under control with Symfony and Webpack

Tendencies

Page 9: Keeping the frontend under control with Symfony and Webpack

Asset managers

Page 10: Keeping the frontend under control with Symfony and Webpack

Tendency

Task runners

Page 11: Keeping the frontend under control with Symfony and Webpack

Tendency

Task runners Bundlers

Task runners + understanding of require(ments)

Page 12: Keeping the frontend under control with Symfony and Webpack

Tendency

Task runners Bundlers

Task runners + understanding of require(ments)

Page 13: Keeping the frontend under control with Symfony and Webpack

Package management in JS

Server Side (node.js)

Bower

Client side (browser)

Used to be

Page 14: Keeping the frontend under control with Symfony and Webpack

Package management in JS

Server Side (node.js)

Bower

Client side (browser)

Used to be Now

Everywhere

Page 15: Keeping the frontend under control with Symfony and Webpack

Module loaders

Server Side (node.js)

Client side (browser)

Used to be

Page 16: Keeping the frontend under control with Symfony and Webpack

Module loaders

Server Side (node.js)

Client side (browser)

Used to be Now

Everywhere

&ES6 Style

Page 17: Keeping the frontend under control with Symfony and Webpack

Summarizing

Package manager Module loader

Module bundler

Page 18: Keeping the frontend under control with Symfony and Webpack

Setup

Page 19: Keeping the frontend under control with Symfony and Webpack

Directory structureapp/bin/src/tests/var/vendor/web/ assets/

Page 20: Keeping the frontend under control with Symfony and Webpack

Directory structureapp/bin/src/tests/var/vendor/web/ assets/client/ js/ scss/ images/

Page 21: Keeping the frontend under control with Symfony and Webpack

Directory structureapp/bin/src/tests/var/vendor/web/ assets/client/ js/ scss/ images/

Page 22: Keeping the frontend under control with Symfony and Webpack

NPM setup$ npm init

$ cat package.json

{ "name": "webpacksf", "version": "1.0.0", "description": "Webpack & Symfony example", "main": "client/js/index.js", "directories": { "test": "client/js/tests" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Nacho Martín", "license": "MIT"}

Page 23: Keeping the frontend under control with Symfony and Webpack

Install Webpack

$ npm install -g webpack

$ npm install --save-dev webpack

Or, to install it globally

Page 24: Keeping the frontend under control with Symfony and Webpack

First example

var greeter = require('./greeter.js')

greeter('Nacho');

client/js/index.js

Page 25: Keeping the frontend under control with Symfony and Webpack

First example

var greeter = require('./greeter.js')

greeter('Nacho');

client/js/index.js

var greeter = function(name) { console.log('Hi '+name+'!');}

module.exports = greeter;

client/js/greeter.js

Page 26: Keeping the frontend under control with Symfony and Webpack

First example

var greeter = require('./greeter.js')

greeter('Nacho');

client/js/index.js

var greeter = function(name) { console.log('Hi '+name+'!');}

module.exports = greeter;

client/js/greeter.js

Page 27: Keeping the frontend under control with Symfony and Webpack

Webpack without configuration

$ webpack client/js/index.js web/assets/build/hello.jsHash: 4f4f05e78036f9dc67f3Version: webpack 1.13.2Time: 100msAsset Size Chunks Chunk Nameshi.js 1.59 kB 0 [emitted] main [0] ./client/js/index.js 57 bytes {0} [built] [1] ./client/js/greeter.js 66 bytes {0} [built]

Page 28: Keeping the frontend under control with Symfony and Webpack

Webpack without configuration

<!DOCTYPE html><html> <head> <meta charset="UTF-8" /> <title>{% block title %}Webpack & Symfony!{% endblock %}</title> {% block stylesheets %}{% endblock %} <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" /> </head> <body> {% block body %}{% endblock %} {% block javascripts %} <script src="{{ asset('assets/build/hello.js') }}"></script> {% endblock %} </body></html>

app/Resources/base.html.twig

Page 29: Keeping the frontend under control with Symfony and Webpack

Webpack config

module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }};

webpack.config.js

Page 30: Keeping the frontend under control with Symfony and Webpack

Webpack config

module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }};

webpack.config.js

Page 31: Keeping the frontend under control with Symfony and Webpack

Loaders

Page 32: Keeping the frontend under control with Symfony and Webpack

Now that we have modules, What about using modern JavaScript? (without caring about IE support)

Page 33: Keeping the frontend under control with Symfony and Webpack

Now that we have modules, What about using modern JavaScript? (without caring about IE support)

Page 34: Keeping the frontend under control with Symfony and Webpack

JavaScript ES2015

•Default Parameters •Template Literals •Arrow Functions •Promises •Block-Scoped Constructs Let and Const •Classes •Modules •…

Page 35: Keeping the frontend under control with Symfony and Webpack

Why Babel matters

import Greeter from './greeter.js';

let greeter = new Greeter('Hi');greeter.greet('gentlemen');

class Greeter { constructor(salutation = 'Hello') { this.salutation = salutation; }

greet(name = 'Nacho') { const greeting = `${this.salutation}, ${name}!`; console.log(greeting); }}

export default Greeter;

client/js/index.js

client/js/greeter.js

Page 36: Keeping the frontend under control with Symfony and Webpack

Install babel$ npm install --save-dev babel-core \\

babel-loader babel-preset-es2015

Page 37: Keeping the frontend under control with Symfony and Webpack

Install babel$ npm install --save-dev babel-core \\

babel-loader babel-preset-es2015

module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, ] }};

webpack.config.js

Page 38: Keeping the frontend under control with Symfony and Webpack

Install babel$ npm install --save-dev babel-core \\

babel-loader babel-preset-es2015

module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, ] }};

webpack.config.js

Page 39: Keeping the frontend under control with Symfony and Webpack

Install babel$ npm install --save-dev babel-core \\

babel-loader babel-preset-es2015

module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, ] }};

webpack.config.js .babelrc

{ "presets": ["es2015"]}

Page 40: Keeping the frontend under control with Symfony and Webpack

Install babel$ npm install --save-dev babel-core \\

babel-loader babel-preset-es2015

module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, ] }};

webpack.config.js .babelrc

{ "presets": ["es2015"]}

Page 41: Keeping the frontend under control with Symfony and Webpack

LoadersSASS

Markdown

Base64

React

Image

Uglify

…https://webpack.github.io/docs/list-of-loaders.html

Page 42: Keeping the frontend under control with Symfony and Webpack

Loader gymnastics: (S)CSS

Page 43: Keeping the frontend under control with Symfony and Webpack

Loading styles

require(‘../css/layout.css');//…

client/js/index.js

Page 44: Keeping the frontend under control with Symfony and Webpack

Loading styles: raw*

loaders: [ //… { test: /\.css$/i, loader: 'raw'},]

exports.push([module.id, "body {\n line-height: 1.5;\n padding: 4em 1em;\n}\n\nh2 {\n margin-top: 1em;\n padding-top: 1em;\n}\n\nh1,\nh2,\nstrong {\n color: #333;\n}\n\na

{\n color: #e81c4f;\n}\n\n", ""]);

Embeds it into JavaScript, but…

*(note: if you are reading the slides, don’t use this loader for css. Use css loader, that will be explained later)

Page 45: Keeping the frontend under control with Symfony and Webpack

Chaining styles: style

loaders: [ //… { test: /\.css$/i, loader: ’style!raw'},]

Page 46: Keeping the frontend under control with Symfony and Webpack

CSS loaderheader { background-image: url("../img/header.jpg");}

Problem

Page 47: Keeping the frontend under control with Symfony and Webpack

CSS loaderheader { background-image: url("../img/header.jpg");}

url(image.png) => require("./image.png") url(~module/image.png) => require("module/image.png")

We want

Problem

Page 48: Keeping the frontend under control with Symfony and Webpack

CSS loaderheader { background-image: url("../img/header.jpg");}

url(image.png) => require("./image.png") url(~module/image.png) => require("module/image.png")

We want

Problem

loaders: [ //… { test: /\.css$/i, loader: ’style!css'},]

Solution

Page 49: Keeping the frontend under control with Symfony and Webpack

File loaders{ test: /\.jpg$/, loader: 'file-loader' },

{ test: /\.png$/, loader: 'url-loader?limit=10000' },

Copies file as [hash].jpg, and returns the public url

If file < 10Kb: embed it in data URL. If > 10Kb: use file-loader

Page 50: Keeping the frontend under control with Symfony and Webpack

Using loaders

When requiring a file

In webpack.config.js, verbose{ test: /\.png$/, loader: "url-loader", query: { limit: "10000" }}

require("url-loader?limit=10000!./file.png");

{ test: /\.png$/, loader: 'url-loader?limit=10000' },

In webpack.config.js, compact

Page 51: Keeping the frontend under control with Symfony and Webpack

SASS

{ test: /\.scss$/i, loader: 'style!css!sass'},

In webpack.config.js, compact

$ npm install --save-dev sass-loader node-sass

Also

{ test: /\.scss$/i, loaders: [ 'style', 'css', 'sass' ]},

Page 52: Keeping the frontend under control with Symfony and Webpack

Embedding CSS in JS is good in Single Page Apps

What if I am not writing a Single Page App?

Page 53: Keeping the frontend under control with Symfony and Webpack

ExtractTextPluginvar ExtractTextPlugin = require("extract-text-webpack-plugin");const extractCSS = new ExtractTextPlugin('stylesheets/[name].css');

const config = { //… module: { loaders: [ { test: /\.css$/i, loader: extractCSS.extract(['css'])},

//… ] }, plugins: [ extractCSS, //… ]};

{% block stylesheets %} <link href="{{asset('assets/build/stylesheets/hello.css')}}" rel="stylesheet">{% endblock %}

app/Resources/base.html.twig

webpack.config.js

Page 54: Keeping the frontend under control with Symfony and Webpack

ExtractTextPluginvar ExtractTextPlugin = require("extract-text-webpack-plugin");const extractCSS = new ExtractTextPlugin('stylesheets/[name].css');

const config = { //… module: { loaders: [ { test: /\.css$/i, loader: extractCSS.extract(['css'])},

//… ] }, plugins: [ extractCSS, //… ]};

{% block stylesheets %} <link href="{{asset('assets/build/stylesheets/hello.css')}}" rel="stylesheet">{% endblock %}

app/Resources/base.html.twig

webpack.config.js

{ test: /\.scss$/i, loader: extractCSS.extract(['css','sass'])},

Also

Page 55: Keeping the frontend under control with Symfony and Webpack

Dev tools

Page 56: Keeping the frontend under control with Symfony and Webpack

Webpack-watch

$ webpack --watch

Simply watches for changes and recompiles the bundle

Page 57: Keeping the frontend under control with Symfony and Webpack

Webpack-dev-server

$ webpack-dev-server —inline

http://localhost:8080/webpack-dev-server/

Starts a server. The browser opens a WebSocket connection with it

and reloads automatically when something changes.

Page 58: Keeping the frontend under control with Symfony and Webpack

Webpack-dev-server config Sf{% block javascripts %} <script src="{{ asset('assets/build/hello.js', 'webpack') }}"></script>{% endblock %}

app/Resources/base.html.twig

framework: assets: packages: webpack: base_urls: - "%assets_base_url%"

app/config/config_dev.yml

parameters: #… assets_base_url: 'http://localhost:8080'

app/config/parameters.yml

Page 59: Keeping the frontend under control with Symfony and Webpack

Webpack-dev-server config Sf{% block javascripts %} <script src="{{ asset('assets/build/hello.js', 'webpack') }}"></script>{% endblock %}

app/Resources/base.html.twig

framework: assets: packages: webpack: base_urls: - "%assets_base_url%"

app/config/config_dev.ymlframework: assets: packages: webpack: ~

app/config/config.yml

parameters: #… assets_base_url: 'http://localhost:8080'

app/config/parameters.yml

Page 60: Keeping the frontend under control with Symfony and Webpack

Optional web-dev-server

Kudos Ryan Weaver

class AppKernel extends Kernel{ public function registerContainerConfiguration(LoaderInterface $loader) { //… $loader->load(function($container) { if ($container->getParameter('use_webpack_dev_server')) { $container->loadFromExtension('framework', [ 'assets' => [ 'base_url' => 'http://localhost:8080' ] ]); } }); }}

Page 61: Keeping the frontend under control with Symfony and Webpack

Hot module replacementoutput: { publicPath: 'http://localhost:8080/assets/build/', path: './web/assets/build', filename: '[name].js'},

Will try to replace the code without even page reload

$ webpack-dev-server --hot --inline

Page 62: Keeping the frontend under control with Symfony and Webpack

Hot module replacementoutput: { publicPath: 'http://localhost:8080/assets/build/', path: './web/assets/build', filename: '[name].js'},

Will try to replace the code without even page reload

Needs full URL (so only in dev), or…

$ webpack-dev-server --hot --inline

Page 63: Keeping the frontend under control with Symfony and Webpack

Hot module replacementoutput: { publicPath: 'http://localhost:8080/assets/build/', path: './web/assets/build', filename: '[name].js'},

$ webpack-dev-server --hot --inline --output-public-path http://localhost:8080/assets/build/

Will try to replace the code without even page reload

Needs full URL (so only in dev), or…

$ webpack-dev-server --hot --inline

Page 64: Keeping the frontend under control with Symfony and Webpack

SourceMapsconst devBuild = process.env.NODE_ENV !== ‘production';

/…

if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map';} else {

Page 65: Keeping the frontend under control with Symfony and Webpack

SourceMapsconst devBuild = process.env.NODE_ENV !== ‘production';

/…

if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map';} else {

eval source-map hidden-source-map inline-source-map eval-source-map cheap-source-map cheap-module-source-map

Several options:

Page 66: Keeping the frontend under control with Symfony and Webpack

Notifier

$ npm install --save-dev webpack-notifier

module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] };

webpack.config.js

Page 67: Keeping the frontend under control with Symfony and Webpack

Notifier

$ npm install --save-dev webpack-notifier

module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] };

webpack.config.js

Page 68: Keeping the frontend under control with Symfony and Webpack

Notifier

$ npm install --save-dev webpack-notifier

module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] };

webpack.config.js

Page 69: Keeping the frontend under control with Symfony and Webpack

Optimize for production

Page 70: Keeping the frontend under control with Symfony and Webpack

Optimization options var WebpackNotifierPlugin = require('webpack-notifier');var webpack = require(‘webpack');

const devBuild = process.env.NODE_ENV !== 'production';

const config = { entry: { hello: './client/js/index.js' }, //…};

if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map';} else {

console.log('Webpack production build'); config.plugins.push( new webpack.optimize.DedupePlugin() ); config.plugins.push( new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) );}

module.exports = config;

Page 71: Keeping the frontend under control with Symfony and Webpack

Optimization options var WebpackNotifierPlugin = require('webpack-notifier');var webpack = require(‘webpack');

const devBuild = process.env.NODE_ENV !== 'production';

const config = { entry: { hello: './client/js/index.js' }, //…};

if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map';} else {

console.log('Webpack production build'); config.plugins.push( new webpack.optimize.DedupePlugin() ); config.plugins.push( new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) );}

module.exports = config;

$ export NODE_ENV=production; webpack

Page 72: Keeping the frontend under control with Symfony and Webpack

Optimization options var WebpackNotifierPlugin = require('webpack-notifier');var webpack = require(‘webpack');

const devBuild = process.env.NODE_ENV !== 'production';

const config = { entry: { hello: './client/js/index.js' }, //…};

if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map';} else {

console.log('Webpack production build'); config.plugins.push( new webpack.optimize.DedupePlugin() ); config.plugins.push( new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) );}

module.exports = config;

$ export NODE_ENV=production; webpack

Page 73: Keeping the frontend under control with Symfony and Webpack

Bundle visualizer$ webpack --json > stats.json

https://chrisbateman.github.io/webpack-visualizer/

Page 74: Keeping the frontend under control with Symfony and Webpack

Bundle visualizer$ webpack --json > stats.json

https://chrisbateman.github.io/webpack-visualizer/

Page 75: Keeping the frontend under control with Symfony and Webpack

More than one bundle

Page 76: Keeping the frontend under control with Symfony and Webpack

Separate entry points

var config = { entry: { front: './assets/js/front.js', admin: './assets/js/admin.js', }, output: { publicPath: '/assets/build/', path: './web/assets/build/', filename: '[name].js' },

Page 77: Keeping the frontend under control with Symfony and Webpack

Vendor bundlesvar config = { entry: { front: './assets/js/front.js', admin: './assets/js/admin.js', 'vendor-admin': [ 'lodash', 'moment', 'classnames', 'react', 'redux', ]

},

plugins: [ extractCSS, new webpack.optimize.CommonsChunkPlugin({ name: 'vendor-admin', chunks: ['admin'], filename: 'vendor-admin.js', minChunks: Infinity }),

Page 78: Keeping the frontend under control with Symfony and Webpack

Common Chunks

var CommonsChunkPlugin = require(“webpack/lib/optimize/CommonsChunkPlugin”);

module.exports = { entry: { page1: "./page1", page2: "./page2", }, output: { filename: "[name].chunk.js" }, plugins: [ new CommonsChunkPlugin("commons.chunk.js") ]}

Produces page1.chunk.js, page2.chunk.js and commons.chunk.js

Page 79: Keeping the frontend under control with Symfony and Webpack

On demand loadingclass Greeter { constructor(salutation = 'Hello') { this.salutation = salutation; }

greet(name = 'Nacho', goodbye = true) { const greeting = `${this.salutation}, ${name}!`; console.log(greeting); if (goodbye) { require.ensure(['./goodbyer'], function(require) { var goodbyer = require('./goodbyer'); goodbyer(name); }); } }}

export default Greeter;

module.exports = function(name) { console.log('Goodbye '+name);}

greeter.js

goodbyer.js

Page 80: Keeping the frontend under control with Symfony and Webpack

On demand loadingclass Greeter { constructor(salutation = 'Hello') { this.salutation = salutation; }

greet(name = 'Nacho', goodbye = true) { const greeting = `${this.salutation}, ${name}!`; console.log(greeting); if (goodbye) { require.ensure(['./goodbyer'], function(require) { var goodbyer = require('./goodbyer'); goodbyer(name); }); } }}

export default Greeter;

module.exports = function(name) { console.log('Goodbye '+name);}

greeter.js

goodbyer.js

Page 81: Keeping the frontend under control with Symfony and Webpack

Hashes

output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js', chunkFilename: "[id].[hash].bundle.js"},

Page 82: Keeping the frontend under control with Symfony and Webpack

Chunks are very configurablehttps://webpack.github.io/docs/optimization.html

Page 83: Keeping the frontend under control with Symfony and Webpack

Practical cases

Page 84: Keeping the frontend under control with Symfony and Webpack

Provide plugin

plugins: [ new webpack.ProvidePlugin({ _: 'lodash', $: 'jquery', }),]

$("#item")

_.find(users, { 'age': 1, 'active': true });

These just work without requiring them:

Page 85: Keeping the frontend under control with Symfony and Webpack

Exposing jQuery

{ test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' },

Exposes $ and jQuery globally in the browser

Page 86: Keeping the frontend under control with Symfony and Webpack

Dealing with a mess

require('imports?define=>false&exports=>false!blueimp-file-upload/js/vendor/jquery.ui.widget.js');require('imports?define=>false&exports=>false!blueimp-load-image/js/load-image-meta.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.iframe-transport.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.fileupload.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.fileupload-process.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.fileupload-image.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.fileupload.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.fileupload-validate.js');require('imports?define=>false&exports=>false!blueimp-file-upload/js/jquery.fileupload-ui.js');

Broken packages that are famous have people that have figured out how to work

with them

Page 87: Keeping the frontend under control with Symfony and Webpack

CopyWebpackPlugin for messes

new CopyWebpackPlugin([ { from: './client/messyvendors', to: './../mess' }]),

For vendors that are broken, can’t make work with Webpack but I still need them

(during the transition)

Page 88: Keeping the frontend under control with Symfony and Webpack

Summary:

Page 89: Keeping the frontend under control with Symfony and Webpack

Summary:• What is Webpack • Basic setup • Loaders are the bricks of Webpack • Have nice tools in Dev environment • Optimize in Prod environment • Split your bundle as you need • Tips and tricks

Page 90: Keeping the frontend under control with Symfony and Webpack

MADRID · NOV 27-28 · 2015

Thanks!@nacmartin

[email protected]

http://limenius.com