94
Webpack Encore: Pro JavaScript and CSS for all my Friends! … that means you!

Webpack Encore Symfony Live 2017 San Francisco

Embed Size (px)

Citation preview

Webpack Encore:

Pro JavaScript and CSS for all my Friends!

… that means you!

> Lead of the Symfony documentation team

> Writer for KnpUniversity.com

> SensioLabs & Blackfire evangelist… Fanboy

> Husband of the much more talented @leannapelham

knpuniversity.com twitter.com/weaverryan

Yo! I’m Ryan!

> Father to my more handsome son, Beckett

… that means you!

Webpack Encore:

Pro JavaScript and CSS for all my Friends!

, ReactJS, webpack

@weaverryan

All of modern JavaScript in 45 minutes!

ES6

the 12 new JS things they invent during this presentation

, ES2015 , ECMAScript

, Babel

, NodeJS

yarn , modules …

… and of course …

Modern JavaScript is a lot like…

@weaverryan

Game of Thrones

@weaverryan

JavaScript

@weaverryan

GoT

Countless libraries and competing standards fighting

for influence

Countless characters and completing factions fighting

for influence

@weaverryan

You spent 6 months building your site in “Hipster.js” only

to read on Twitter that: “no self-respecting dev

uses that crap anymore”

That character you love and followed for 2 seasons, was

just unceremoniously decapitated

JavaScript

GoT

JavaScript is a (weird) language

IT JUST HAPPENS THAT BROWSERS CAN EXECUTE THAT LANGUAGE

@weaverryan

// yay.jsvar message = 'I like Java...Script'; console.log(message);

> node yay.js

I like Java...Script

NodeJS: server-side JavaScript engine

yarn/npm: Composer for NodeJS

// web/js/productApp.jsvar products = [ 'Sheer Shears', 'Wool Hauling Basket', 'After-Shear (Fresh Cut Grass)', 'After-Shear (Morning Dew)']; var loopThroughProducts = function(callback) { for (var i = 0, length = products.length; i < length; i++) { callback(products[i]); }}; loopThroughProducts(function(product) { console.log('Product: '+product);});

{# app/Resources/views/default/products.html.twig' #}

<script src="{{ asset('js/productApp.js') }}"></script>

our store for sheep (baaaa)

class ProductCollection{ constructor(products) { this.products = products; }} let collection = new ProductCollection([ 'Sheer Shears', 'Wool Hauling Basket', 'After-Shear (Fresh Cut Grass)', 'After-Shear (Morning Dew)', ]);let prods = collection.getProducts();let loopThroughProducts = callback => { for (let prod of prods) { callback(prods); }}; loopThroughProducts(product => console.log('Product: '+product));

what language is this?

JavaScript

@weaverryan

ECMAScriptThe official name of standard JavaScript

ES6/ES2015/HarmonyThe 6th accepted (so official) version of ECMAScript

Proper class and inheritance syntax

let: similar to var, but more hipster

function (product) { console.log(product); }

class ProductCollection{ constructor(products) { this.products = products; }} let collection = new ProductCollection([ 'Sheer Shears', 'Wool Hauling Basket', 'After-Shear (Fresh Cut Grass)', 'After-Shear (Morning Dew)', ]);let prods = collection.getProducts();let loopThroughProducts = callback => { for (let prod of prods) { callback(prods); }}; loopThroughProducts(product => console.log('Product: '+product));

class ProductCollection{ constructor(products) { this.products = products; }} let collection = new ProductCollection([ 'Sheer Shears', 'Wool Hauling Basket', 'After-Shear (Fresh Cut Grass)', 'After-Shear (Morning Dew)', ]);let prods = collection.getProducts();let loopThroughProducts = callback => { for (let prod of prods) { callback(prods); }}; loopThroughProducts(product => console.log('Product: '+product));

But will it run in a browser???

Maybe!

Now we just need to wait 5 years for the

worst browsers to support this

@weaverryan

Babel

… or do we?

A JS transpiler!

{ "devDependencies": { "babel-cli": "^6.10.1" }}

Babel is a NodeJS binary…

… package.json

> yarn add --dev babel-cli

@weaverryan

> ./node_modules/.bin/babel \ web/js/productApp.js \ -o web/builds/productApp.js

{# app/Resources/views/default/products.html.twig' #}

<script src="{{ asset('builds/productApp.js') }}"></script>

@weaverryan

> ./node_modules/.bin/babel \ web/js/productApp.js \ -o web/builds/productApp.js

{# app/Resources/views/default/products.html.twig' #}

<script src="{{ asset('builds/productApp.js') }}"></script>

But, this made no changes

js/productApp.js == builds/productApp.js

@weaverryan

Babel can transpile anything

CoffeeScript --> JavaScript

Coffee --> Tea

ES6 JS --> ES5 JS

* Each transformation is called a preset

1) Install the es2015 preset library

2) Add a .babelrc file

> yarn add babel-preset-es2015

{ "presets": [ "es2015" ]}

@weaverryan

> ./node_modules/.bin/babel \ web/js/productApp.js \ -o web/builds/productApp.js

loopThroughProducts( product => console.log('Product: '+product));

loopThroughProducts(function (product) { return console.log('Product: ' + product);});

source:

built:

But we can use new (or experimental) features now

@weaverryan

Modern JavaScript has a build step

Big Takeaway #1:

@weaverryan

New to ES6:

JavaScript Modules!

The Classic Problem:

If you want to organize your JS into multiple files, you need to manually

include all those script tags!

@weaverryan

// web/js/ProductCollection.js class ProductCollection{ constructor(products) { this.products = products; } getProducts() { return this.products; } getProduct(i) { return this.products[i]; }} export ProductCollection;

@weaverryan

// web/js/productApp.jsimport ProductCollection from './ProductCollection';var collection = new ProductCollection([ 'Sheer Shears', 'Wool Hauling Basket', 'After-Shear (Fresh Cut Grass)', 'After-Shear (Morning Dew)',]);// ...

{# app/Resources/views/default/products.html.twig' #}

<script src="{{ asset('builds/productApp.js') }}"></script>

> ./node_modules/.bin/babel \ web/js/productApp.js \ -o web/builds/productApp.js

Module loading in a browser is hard to do

@weaverryan

@weaverryan

Introducing…

@weaverryan

Webpack!

• bundler • module loader • all-around nice guy

Install webpack

> yarn add --dev webpack

@weaverryan

// web/js/ProductCollection.js class ProductCollection{ // ...} export ProductCollection;

// web/js/productApp.jsimport ProductCollection from ‘./ProductCollection';// ...

Go webpack Go!

> ./node_modules/.bin/webpack \ web/js/productApp.js \ web/builds/productApp.js

The one built file contains the code from both source files

Optional config to make it easier to use:

// webpack.config.jsmodule.exports = { entry: { product: './web/js/productApp.js' }, output: { path: './web/build', filename: '[name].js', publicPath: '/build/' }};

build/product.js

{# app/Resources/views/default/products.html.twig' #}

<script src="{{ asset('build/product.js') }}"></script>

> ./node_modules/.bin/webpack

@weaverryan

> yarn run webpack

Great!

Now let’s add more features to Webpack!

@weaverryan

@weaverryan

Features, features, features, features, features

• babel transpiling

• dev server

• production optimizations

• CSS handling

• Sass/LESS

• PostCSS

• React

• Vue

• versioning

• source maps

• image handling

• extract text

• shared entry

• jQuery providing

• TypeScript

• friendly errors

No Problem

@weaverryan

@weaverryan

var path = require("path");var process = require('process');var webpack = require('webpack');var production = process.env.NODE_ENV === 'production';// helps load CSS to their own file var ExtractPlugin = require('extract-text-webpack-plugin');var CleanPlugin = require('clean-webpack-plugin');var ManifestPlugin = require('webpack-manifest-plugin');var plugins = [ new ExtractPlugin('[name]-[contenthash].css'), // <=== where should content be piped // put vendor_react stuff into its own file // new webpack.optimize.CommonsChunkPlugin({ // name: 'vendor_react', // chunks: ['vendor_react'], // minChunks: Infinity, // avoid anything else going in here // }), new webpack.optimize.CommonsChunkPlugin({ name: 'main', // Move dependencies to the "main" entry minChunks: Infinity, // How many times a dependency must come up before being extracted }), new ManifestPlugin({ filename: 'manifest.json', // prefixes all keys with builds/, which allows us to refer to // the paths as builds/main.css in Twig, instead of just main.css basePath: 'builds/' }),];if (production) { plugins = plugins.concat([ // This plugin looks for similar chunks and files // and merges them for better caching by the user new webpack.optimize.DedupePlugin(), // This plugins optimizes chunks and modules by // how much they are used in your app new webpack.optimize.OccurenceOrderPlugin(), // This plugin prevents Webpack from creating chunks // that would be too small to be worth loading separately new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 51200, // ~50kb }), // This plugin minifies all the Javascript code of the final bundle new webpack.optimize.UglifyJsPlugin({ mangle: true, compress: { warnings: false, // Suppress uglification warnings }, sourceMap: false }), // This plugins defines various variables that we can set to false // in production to avoid code related to them from being compiled // in our final bundle new webpack.DefinePlugin({ __SERVER__: !production, __DEVELOPMENT__: !production, __DEVTOOLS__: !production, 'process.env': { BABEL_ENV: JSON.stringify(process.env.NODE_ENV), 'NODE_ENV': JSON.stringify('production') }, }), new CleanPlugin('web/builds', { root: path.resolve(__dirname , '..') }), ]);}module.exports = { entry: { main: './main', video: './video', checkout_login_registration: './checkout_login_registration', team_pricing: './team_pricing', credit_card: './credit_card', team_subscription: './team_subscription', track_organization: './track_organization', challenge: './challenge', workflow: './workflow', code_block_styles: './code_block_styles', content_release: './content_release', script_editor: './script_editor', sweetalert2_legacy: './sweetalert2_legacy', admin: './admin', admin_user_refund: './admin_user_refund', // vendor entry points to be extracted into their own file // we do this to keep main.js smaller... but it's a pain // because now we need to manually add the script tag for // this file is we use react or react-dom // vendor_react: ['react', 'react-dom'], }, output: { path: path.resolve(__dirname, '../web/builds'), filename: '[name]-[hash].js', chunkFilename: '[name]-[chunkhash].js', // in dev, make all URLs go through the webpack-dev-server // things *mostly* work without this, but AJAX calls for chunks // are made to the local, Symfony server without this publicPath: production ? '/builds/' : 'http://localhost:8090/builds/' }, plugins: plugins, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }, { test: /\.scss/, loader: ExtractPlugin.extract('style', 'css!sass'), }, { test: /\.css/, loader: ExtractPlugin.extract('style', 'css'), }, { test: /\.(png|gif|jpe?g|svg?(\?v=[0-9]\.[0-9]\.[0-9])?)$/i, loader: 'url?limit=10000', }, { // the ?(\?v=[0-9]\.[0-9]\.[0-9])? is for ?v=1.1.1 format test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, // Inline small woff files and output them below font/. // Set mimetype just in case. loader: 'url', query: { prefix: 'font/', limit: 5000, mimetype: 'application/font-woff' }, //include: PATHS.fonts }, { test: /\.ttf?(\?v=[0-9]\.[0-9]\.[0-9])?$|\.eot?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file', query: { name: '[name]-[hash].[ext]', // does this do anything? prefix: 'font/', }, //include: PATHS.fonts }, { test: /\.json$/, loader: "json-loader" }, ] }, node: { fs: 'empty' }, debug: !production, devtool: production ? false : 'eval', devServer: { hot: true, port: 8090, // tells webpack-dev-server where to serve public files from contentBase: '../web/', headers: { "Access-Control-Allow-Origin": "*" } },};

*** not visible here:

the 1000 ways you can shot yourself

in the foot

@weaverryan

Hello Webpack Encore

@weaverryan

oh hai!

Let’s start over

> rm -rf package.json node_modules/

> cowsay “Make mooooore room please”

@weaverryan

@weaverryan

… and let’s start simple

// assets/js/app.js console.log('wow');

> yarn add @symfony/webpack-encore --dev

@weaverryan

Step 1: Install Encore

@weaverryan

Step 2: webpack.config.js

var Encore = require('@symfony/webpack-encore');Encore .setOutputPath('web/build/') .setPublicPath('/build') // will output as web/build/app.js .addEntry('app', './assets/js/app.js') .enableSourceMaps(!Encore.isProduction()); module.exports = Encore.getWebpackConfig();

> yarn run encore dev

{# base.html.twig #}<script src="{{ asset('build/app.js') }}"></script>

@weaverryan

Yea… but I need my jQuery’s!

@weaverryan

// assets/js/app.jsvar $ = require('jquery');$(document).ready(function() { $('h1').append('I love big text!'); });

> yarn run encore dev

> yarn add jquery --dev

@weaverryan

> yarn run encore dev

@weaverryan

CSS: An un-handled dependency of your JS app

Could we do this?

// assets/js/app.js // ...require(‘../css/cool-app.css’); $(document).ready(function() { $('h1').append('I love big text!');});

> yarn run encore dev

{# base.html.twig #}<link ... href="{{ asset('build/app.css') }}">

Want Bootstrap?

@weaverryan

> yarn add bootstrap

@weaverryan

/* assets/css/cool-app.css */@import "~bootstrap/dist/css/bootstrap.css"; /* ... */

> yarn run encore dev

@weaverryan

But what happens with images?

/* assets/css/cool-app.css */.product-price { color: green; background-image: url('../images/logo.png'); }

> yarn add bootstrap

@weaverryan

/* assets/css/cool-app.css */@import "~bootstrap/dist/css/bootstrap.css"; /* ... */

> yarn run encore dev

image is copied to builds/ and the new URL is written into the CSS

Stop

@weaverryan

thinking of your JavaScript as random code that executes

Start

@weaverryan

thinking of your JavaScript as a single application with dependencies

that are all packaged up together

Sass instead of CSS?

@weaverryan

@weaverryan

> yarn run encore dev

// assets/js/app.js// ...require('../css/app.scss');

@weaverryan

> yarn add sass-loader node-sass --dev

// webpack.config.jsEncore // ... .enableSassLoader()

@weaverryan

But I want page-specific CSS and JS files!

// assets/js/productApp.jsimport '../css/productApp.css'; import $ from 'jquery'; $(document).ready(function() { console.log('product page loaded!'); });

// webpack.config.jsEncore // ... .addEntry('app', './assets/js/app.js') .addEntry('productApp', './assets/js/productApp.js')

{# product.html.twig #}{% block stylesheets %} {{ parent() }} <link href="{{ asset('build/productApp.css') }}"> {% endblock %}{% block javascripts %} {{ parent() }} <script src="{{ asset('build/productApp.js') }}"></script>{% endblock %}

@weaverryan

> yarn run encore dev

Wait!

@weaverryan

You have too many jQuery-ies!

@weaverryan

jQuery is in here

… and also here

// webpack.config.jsEncore // ... .createSharedEntry('app', './assets/js/app.js') .addEntry('productApp', './assets/js/productApp.js')

{# base.html.twig #}<script src="{{ asset('build/manifest.js') }}"></script><script src="{{ asset('build/app.js') }}"></script>

@weaverryan

Asset Versioning

// webpack.config.jsEncore // ... .enableVersioning()

Amazing! Automatic Cache Busting!!!

… oh wait…

@weaverryan

{# base.html.twig #}<script src="{{ asset('build/manifest.js') }}"></script><script src="{{ asset('build/app.js') }}"></script>

manifest.json to the rescue!

manifest.json to the rescue!

{ "build/app.css": "/build/app.3666e24a0be80f22bd8f31c43a70b14f.css", "build/app.js": "/build/app.f18c7a7f2785d99e0c25.js", "build/images/logo.png": "/build/images/logo.482c1dc2.png", "build/manifest.js": "/build/manifest.d41d8cd98f00b204e980.js", "build/productApp.css": "/build/productApp.01f416c68486810b3cb9.css", "build/productApp.js": "/build/productApp.1af5d8e89a35e521309b.js"}

{# base.html.twig #}<script src="{{ asset('build/manifest.js') }}"></script><script src="{{ asset('build/app.js') }}"></script>

manifest.json to the rescue!

# app/config/config.ymlframework: # ... assets: json_manifest_path: ‘%kernel.project_dir%/web/build/manifest.json'

… so add long-term caching…

@weaverryan

@weaverryan

This presentation needs more buzz words

React!

{# products.html.twig #}<div id="product-app" data-products="{{ products|json_encode|e('html_attr') }}"> </div>

// assets/js/productApp.jsimport React from 'react'; import ReactDOM from 'react-dom'; import ProductApp from './Components/ProductApp'; import $ from 'jquery'; $(document).ready(function() { const root = document.getElementById('product-app'); const startingProducts = root.dataset['products']; ReactDOM.render( <ProductApp message="Great Products!” initialProducts={startingProducts} />, root );});

// webpack.config.jsEncore // ... .enableReactPreset();

> yarn add --dev react react-dom babel-preset-react

@weaverryan

• React • Vue • TypeScript • Code splitting

• source maps

• versioning

• Sass/LESS

… and is the life of any party …

An Encore of Features

@weaverryan

Putting it all together

@weaverryan

ES6/ES2015/ECMAScript 2015

The newest version of Javascript, not supported by all browsers

@weaverryan

Babel

A tool that can transform JavaScript to different JavaScript

presets A) ES6 js to “old” JS B) React to raw JS

@weaverryan

Webpack

A tool that follows imports to bundle JavaScript, CSS, and anything else you dream up into one JavaScript package

@weaverryan

Webpack Encore

A tool that makes configuring webpack not suck

Ryan Weaver @weaverryan

THANK YOU!

https://joind.in/talk/bac7c