• Название:

    Webpacksf

  • Размер: 1.25 Мб
  • Формат: PDF
  • или
  • Название: webpacksf

Keeping the frontend
under control with

Symfony and Webpack
Munich Symfony Meetup
October’16

Nacho Martín
@nacmartin
nacho@limenius.com

Nacho Martín
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@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
Used to be
Server Side (node.js)

Client side (browser)

Bower

Package management in JS
Used to be

Now

Server Side (node.js)
Everywhere
Client side (browser)

Bower

Module loaders
Used to be
Server Side (node.js)

Client side (browser)

Module loaders
Used to be
Server Side (node.js)

Client side (browser)

Now
Everywhere

&ES6 Style

Summarizing
Package manager

Module loader

Module bundler

Setup

Directory structure
app/
bin/
src/
tests/
var/
vendor/
web/
assets/

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

Directory structure
app/
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 --save-dev webpack
Or, to install it globally
$ npm install -g webpack

First example
client/js/index.js
var greeter = require('./greeter.js')
greeter('Nacho');

First example
client/js/index.js
var greeter = require('./greeter.js')
greeter('Nacho');

client/js/greeter.js
var greeter = function(name) {
console.log('Hi '+name+'!');
}
module.exports = greeter;

First example
client/js/index.js
var greeter = require('./greeter.js')
greeter('Nacho');

client/js/greeter.js
var greeter = function(name) {
console.log('Hi '+name+'!');
}
module.exports = greeter;

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]

Webpack without configuration
app/Resources/base.html.twig
<!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>

Webpack config
webpack.config.js

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

Webpack config
webpack.config.js

module.exports = {
entry: {
hello: './client/js/index.js'
},
output: {
publicPath: '/assets/build/',
path: './web/assets/build',
filename: '[name].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
client/js/index.js
import Greeter from './greeter.js';
let greeter = new Greeter('Hi');
greeter.greet('gentlemen');

client/js/greeter.js
class Greeter {
constructor(salutation = 'Hello') {
this.salutation = salutation;
}
greet(name = 'Nacho') {
const greeting = `${this.salutation}, ${name}!`;
console.log(greeting);
}
}
export default Greeter;

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

webpack.config.js
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/
},
]
}
};

Install babel
$ npm install --save-dev babel-core \\
babel-loader babel-preset-es2015

webpack.config.js
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/
},
]
}
};

Install babel
$ npm install --save-dev babel-core \\
babel-loader babel-preset-es2015

webpack.config.js
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/
},
]
}
};

.babelrc
{
"presets": ["es2015"]
}

Install babel
$ npm install --save-dev babel-core \\
babel-loader babel-preset-es2015

webpack.config.js
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/
},
]
}
};

.babelrc
{
"presets": ["es2015"]
}

Loaders
SASS
Markdown
Base64
React
Image
Uglify


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

Loader gymnastics: (S)CSS

Loading styles
client/js/index.js

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

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 loader
Problem
header {
background-image: url("../img/header.jpg");
}

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

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

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

We want
url(image.png) => require("./image.png")
url(~module/image.png) => require("module/image.png")
Solution
loaders: [
//…
{ test: /\.css$/i, loader: ’style!css'},
]

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

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

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

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

Using loaders
In webpack.config.js, compact
{ test: /\.png$/, loader: 'url-loader?limit=10000' },

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

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

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

In webpack.config.js, compact
{ test: /\.scss$/i, loader: 'style!css!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?

ExtractTextPlugin
webpack.config.js
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,
//…
]
};

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

ExtractTextPlugin
webpack.config.js
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,
//…
]
};

Also

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

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

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
app/Resources/base.html.twig
{% block javascripts %}
<script src="{{ asset('assets/build/hello.js', 'webpack') }}"></script>
{% endblock %}

app/config/config_dev.yml
framework:
assets:
packages:
webpack:
base_urls:
- "%assets_base_url%"

app/config/parameters.yml
parameters:
#…
assets_base_url: 'http://localhost:8080'

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

app/config/config_dev.yml
framework:
assets:
packages:
webpack:
base_urls:
- "%assets_base_url%"

app/config/parameters.yml
parameters:
#…
assets_base_url: 'http://localhost:8080'

app/config/config.yml
framework:
assets:
packages:
webpack: ~

Optional web-dev-server
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'
]
]);
}
});
}
}

Kudos Ryan Weaver

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

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

Will try to replace the code without even page reload

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

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

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

Will try to replace the code without even page reload

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

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

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

Will try to replace the code without even page reload

SourceMaps
const devBuild = process.env.NODE_ENV !== ‘production';
/…
if (devBuild) {
console.log('Webpack dev build');
config.devtool = 'eval-source-map';
} else {

SourceMaps
const devBuild = process.env.NODE_ENV !== ‘production';
/…
if (devBuild) {
console.log('Webpack dev build');
config.devtool = 'eval-source-map';
} else {

Several options:

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

Notifier
$ npm install --save-dev webpack-notifier

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

Notifier
$ npm install --save-dev webpack-notifier

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

Notifier
$ npm install --save-dev webpack-notifier

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

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'
},
//…
};

$ export NODE_ENV=production; webpack

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'
},
//…
};

$ export NODE_ENV=production; webpack

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;

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 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
}),

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 loading
greeter.js
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;

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

On demand loading
greeter.js
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;

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

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

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

Practical cases

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

These just work without requiring them:
$("#item")
_.find(users, { 'age': 1, 'active': true });

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

Thanks!
@nacmartin
nacho@limenius.com

http://limenius.com

MADRID · NOV 27-28 · 2015