keeping the frontend under control with symfony and webpack
Post on 16-Apr-2017
2.107 Views
Preview:
TRANSCRIPT
Keeping the frontend under control with Symfony and Webpack
Nacho Martín @nacmartin
nacho@limenius.com
Munich Symfony Meetup October’16
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
Why do we need this?
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
Building the Pyramids: 130K man years
Writing JavaScript: 10 man days
JavaScript
Making JavaScript great: NaN man years
JavaScript
Tendencies
Asset managers
Tendency
Task runners
Tendency
Task runners Bundlers
Task runners + understanding of require(ments)
Tendency
Task runners Bundlers
Task runners + understanding of require(ments)
Package management in JS
Server Side (node.js)
Bower
Client side (browser)
Used to be
Package management in JS
Server Side (node.js)
Bower
Client side (browser)
Used to be Now
Everywhere
Module loaders
Server Side (node.js)
Client side (browser)
Used to be
Module loaders
Server Side (node.js)
Client side (browser)
Used to be Now
Everywhere
&ES6 Style
Summarizing
Package manager Module loader
Module bundler
Setup
Directory structureapp/bin/src/tests/var/vendor/web/ assets/
Directory structureapp/bin/src/tests/var/vendor/web/ assets/client/ js/ scss/ images/
Directory structureapp/bin/src/tests/var/vendor/web/ assets/client/ js/ scss/ images/
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"}
Install Webpack
$ npm install -g webpack
$ npm install --save-dev webpack
Or, to install it globally
First example
var greeter = require('./greeter.js')
greeter('Nacho');
client/js/index.js
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
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
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]
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
Webpack config
module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }};
webpack.config.js
Webpack config
module.exports = { entry: { hello: './client/js/index.js' }, output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js' }};
webpack.config.js
Loaders
Now that we have modules, What about using modern JavaScript? (without caring about IE support)
Now that we have modules, What about using modern JavaScript? (without caring about IE support)
JavaScript ES2015
•Default Parameters •Template Literals •Arrow Functions •Promises •Block-Scoped Constructs Let and Const •Classes •Modules •…
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
Install babel$ npm install --save-dev babel-core \\
babel-loader babel-preset-es2015
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
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
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"]}
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"]}
LoadersSASS
Markdown
Base64
React
Image
Uglify
…https://webpack.github.io/docs/list-of-loaders.html
Loader gymnastics: (S)CSS
Loading styles
require(‘../css/layout.css');//…
client/js/index.js
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)
Chaining styles: style
loaders: [ //… { test: /\.css$/i, loader: ’style!raw'},]
CSS loaderheader { background-image: url("../img/header.jpg");}
Problem
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
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
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
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
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' ]},
Embedding CSS in JS is good in Single Page Apps
What if I am not writing a Single Page App?
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
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
Dev tools
Webpack-watch
$ webpack --watch
Simply watches for changes and recompiles the bundle
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.
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
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
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' ] ]); } }); }}
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
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
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
SourceMapsconst devBuild = process.env.NODE_ENV !== ‘production';
/…
if (devBuild) { console.log('Webpack dev build'); config.devtool = 'eval-source-map';} else {
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:
Notifier
$ npm install --save-dev webpack-notifier
module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] };
webpack.config.js
Notifier
$ npm install --save-dev webpack-notifier
module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] };
webpack.config.js
Notifier
$ npm install --save-dev webpack-notifier
module.exports = { //… plugins: [ new WebpackNotifierPlugin(), ] };
webpack.config.js
Optimize for production
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;
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
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
Bundle visualizer$ webpack --json > stats.json
https://chrisbateman.github.io/webpack-visualizer/
Bundle visualizer$ webpack --json > stats.json
https://chrisbateman.github.io/webpack-visualizer/
More than one bundle
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' },
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 }),
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
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
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
Hashes
output: { publicPath: '/assets/build/', path: './web/assets/build', filename: '[name].js', chunkFilename: "[id].[hash].bundle.js"},
Chunks are very configurablehttps://webpack.github.io/docs/optimization.html
Practical cases
Provide plugin
plugins: [ new webpack.ProvidePlugin({ _: 'lodash', $: 'jquery', }),]
$("#item")
_.find(users, { 'age': 1, 'active': true });
These just work without requiring them:
Exposing jQuery
{ test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' },
Exposes $ and jQuery globally in the browser
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
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)
Summary:
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
MADRID · NOV 27-28 · 2015
Thanks!@nacmartin
nacho@limenius.com
http://limenius.com
top related