Keeping the frontend under control with Symfony and Webpack

1,785 views

Published on

Webpack tutorial with tips for Symfony users. Topics covered include: current frontend trends, setup, loaders, dev tools, optimization in production, bundle splitting and tips and tricks for using webpack with existing projects.
Symfony Munich Meetup 2016.

Published in: Internet
0 Comments
4 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,785
On SlideShare
0
From Embeds
0
Number of Embeds
146
Actions
Shares
0
Downloads
10
Comments
0
Likes
4
Embeds 0
No embeds

No notes for slide

Keeping the frontend under control with Symfony and Webpack

  1. 1. Keeping the frontend under control with Symfony and Webpack Nacho Martín @nacmartin nacho@limenius.com Munich Symfony Meetup October’16
  2. 2. 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 nacho@limenius.com @nacmartin
  3. 3. Why do we need this?
  4. 4. 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
  5. 5. Building the Pyramids: 130K man years
  6. 6. Writing JavaScript: 10 man days JavaScript
  7. 7. Making JavaScript great: NaN man years JavaScript
  8. 8. Tendencies
  9. 9. Asset managers
  10. 10. Tendency Task runners
  11. 11. Tendency Task runners Bundlers Task runners + understanding of require(ments)
  12. 12. Tendency Task runners Bundlers Task runners + understanding of require(ments)
  13. 13. Package management in JS Server Side (node.js) Bower Client side (browser) Used to be
  14. 14. Package management in JS Server Side (node.js) Bower Client side (browser) Used to be Now Everywhere
  15. 15. Module loaders Server Side (node.js) Client side (browser) Used to be
  16. 16. Module loaders Server Side (node.js) Client side (browser) Used to be Now Everywhere &ES6 Style
  17. 17. Summarizing Package manager Module loader Module bundler
  18. 18. Setup
  19. 19. Directory structure app/ bin/ src/ tests/ var/ vendor/ web/ assets/
  20. 20. Directory structure app/ bin/ src/ tests/ var/ vendor/ web/ assets/ client/ js/ scss/ images/
  21. 21. Directory structure app/ bin/ src/ tests/ var/ vendor/ web/ assets/ client/ js/ scss/ images/
  22. 22. 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" }
  23. 23. Install Webpack $ npm install -g webpack $ npm install --save-dev webpack Or, to install it globally
  24. 24. First example var greeter = require('./greeter.js') greeter('Nacho'); client/js/index.js
  25. 25. 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
  26. 26. 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
  27. 27. Webpack without configuration $ webpack client/js/index.js web/assets/build/hello.js Hash: 4f4f05e78036f9dc67f3 Version: webpack 1.13.2 Time: 100ms Asset Size Chunks Chunk Names hi.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]
  28. 28. 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
  29. 29. Webpack config module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' } }; webpack.config.js
  30. 30. Webpack config module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' } }; webpack.config.js
  31. 31. Loaders
  32. 32. Now that we have modules, What about using modern JavaScript? (without caring about IE support)
  33. 33. Now that we have modules, What about using modern JavaScript? (without caring about IE support)
  34. 34. JavaScript ES2015 •Default Parameters •Template Literals •Arrow Functions •Promises •Block-Scoped Constructs Let and Const •Classes •Modules •…
  35. 35. 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
  36. 36. Install babel $ npm install --save-dev babel-core babel-loader babel-preset-es2015
  37. 37. 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
  38. 38. 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
  39. 39. 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"] }
  40. 40. 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"] }
  41. 41. Loaders SASS Markdown Base64 React Image Uglify … https://webpack.github.io/docs/list-of-loaders.html
  42. 42. Loader gymnastics: (S)CSS
  43. 43. Loading styles require(‘../css/layout.css'); //… client/js/index.js
  44. 44. Loading styles: raw* loaders: [ //… { test: /.css$/i, loader: 'raw'}, ] exports.push([module.id, "body {n line-height: 1.5;n padding: 4em 1em;n}nnh2 {n margin-top: 1em;n padding-top: 1em;n}nnh1,nh2,nstrong {n color: #333;n}nna {n color: #e81c4f;n}nn", ""]); 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)
  45. 45. Chaining styles: style loaders: [ //… { test: /.css$/i, loader: ’style!raw'}, ]
  46. 46. CSS loader header { background-image: url("../img/header.jpg"); } Problem
  47. 47. CSS loader header { background-image: url("../img/header.jpg"); } url(image.png) => require("./image.png") url(~module/image.png) => require("module/image.png") We want Problem
  48. 48. CSS loader header { 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
  49. 49. 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
  50. 50. 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
  51. 51. 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' ] },
  52. 52. Embedding CSS in JS is good in Single Page Apps What if I am not writing a Single Page App?
  53. 53. ExtractTextPlugin var 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
  54. 54. ExtractTextPlugin var 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
  55. 55. Dev tools
  56. 56. Webpack-watch $ webpack --watch Simply watches for changes and recompiles the bundle
  57. 57. 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.
  58. 58. 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
  59. 59. 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 framework: assets: packages: webpack: ~ app/config/config.yml parameters: #… assets_base_url: 'http://localhost:8080' app/config/parameters.yml
  60. 60. 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' ] ]); } }); } }
  61. 61. Hot module replacement output: { 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
  62. 62. Hot module replacement output: { 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
  63. 63. Hot module replacement output: { 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
  64. 64. SourceMaps const devBuild = process.env.NODE_ENV !== ‘production'; /… if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map'; } else {
  65. 65. SourceMaps const 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:
  66. 66. Notifier $ npm install --save-dev webpack-notifier module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] }; webpack.config.js
  67. 67. Notifier $ npm install --save-dev webpack-notifier module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] }; webpack.config.js
  68. 68. Notifier $ npm install --save-dev webpack-notifier module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] }; webpack.config.js
  69. 69. Optimize for production
  70. 70. 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;
  71. 71. 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
  72. 72. 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
  73. 73. Bundle visualizer $ webpack --json > stats.json https://chrisbateman.github.io/webpack-visualizer/
  74. 74. Bundle visualizer $ webpack --json > stats.json https://chrisbateman.github.io/webpack-visualizer/
  75. 75. More than one bundle
  76. 76. 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' },
  77. 77. Vendor bundles var 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 }),
  78. 78. 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
  79. 79. On demand loading class 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
  80. 80. On demand loading class 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
  81. 81. Hashes output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js', chunkFilename: "[id].[hash].bundle.js" },
  82. 82. Chunks are very configurable https://webpack.github.io/docs/optimization.html
  83. 83. Practical cases
  84. 84. Provide plugin plugins: [ new webpack.ProvidePlugin({ _: 'lodash', $: 'jquery', }), ] $("#item") _.find(users, { 'age': 1, 'active': true }); These just work without requiring them:
  85. 85. Exposing jQuery { test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' }, Exposes $ and jQuery globally in the browser
  86. 86. 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
  87. 87. 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)
  88. 88. Summary:
  89. 89. 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
  90. 90. MADRID · NOV 27-28 · 2015 Thanks! @nacmartin nacho@limenius.com http://limenius.com

×