Evolution
-
JavaScript
- Adding script tag, passing src attribute and a reference to the JavaScript file
- Write JavaScript in HTML
- Problems:
- Doesn't scale
- Too many scripts
- Unmaintainable scripts
- Scope, size, readability, fragility, monolith files
-
Immediately Invoked Function Expression (IIFE)
- Revealing module pattern, provides encapsulation. Treat each file as an IIFE
- Concatenate, "safely" combine files without concern of scope collision
- Problems:
- Full rebuilds everytime
- Dead code, concatenate doesn't help tie usages across files
- Lots of IIFE are slows
- Can't lazy load (dynamic loading)
-
JavaScript Modules
- CommonJS, require and export keyword to import/export functions from other files
- Provides static analysis, can see what dependencies are used in the code
- NPM, Node + Modules, easy to distribute CommonJS node modules
- Problems:
- No browser support
- No live bindings, issues with circular references
- Sync module resolution loader is slow
-
Bundlers/Linkers
- Browserify (static)
- RequireJS (loader)
- SystemJS (loader)
- Problems:
- No static async/lazy loading (all bundles up-front)
- CommonJS bloat too dynamic
- Not everyone was shipping CommonJS
-
Asynchronous Module Definitions (AMD)
- Pattern of creating and sharing modules of JavaScript code
- Problems:
- Too dynamic of lazy loading
- Awkward non-standard syntax, no real module system
-
ESM
- Don't need require from CommonJS, use from keyword to loading from a file
- Reusable, encapsulated, organised, convenient
- Problems:
- ESM for browser is very slow
Introduction to Webpack
- Write any module format (mixed), compiles them for the browser
- Supports static async bundling
- Large ecosystem for Webpack, most performant way to ship JavaScript currently
Ways to use Webpack
-
Configuration file: webpack.config.js
module.exports = { entry: { vendor: './src/vendors.ts', main: './src/main.browser.ts' }, output: { path: 'dist/', filename: '[name].bundle.js' } }
-
Webpack CLI
webpack <entry.js> <result.js> --colors --progress webpack-dev-server --port=9000
-
Node API
var webpack = require("webpack"); webpack({ // Configuration objection here }, function(error, stats) { // Error });
Webpack from scratch
-
Adding npm scripts
"scripts": { "webpack": "webpack", "dev": "npm run webpack -- --mode development", "prod": "npm run webpack -- --mode production" }
-
Setting up debugging
"scripts": { "webpack": "webpack", "debug": "node --webpack --debug-brk ./node_modules/webpack/bin/webpack.js", "dev": "npm run webpack -- --mode development", "prod": "npm run webpack -- --mode production", "prod:debug": "npm run debg -- --mode production", "dev:debug": "npm run debug -- --mode development" }
-
Writing/using a module
- Export functionality:
export default a;
- Import functionality:
import a from "./file";
- Export functionality:
-
Adding watch mode
"scripts": { ..., "dev": "npm run webpack -- --mode development --watch", ... }
-
ES Module syntax
- Export functionalities:
export {a, b, c};
- Import functionalities:
import {a, b, c} from "./file";
- Export functionalities:
-
CommonJS module syntax
- Can't use commonJS and ES syntax in the same file, will throw an error
- Webpack, supports require as well
- Export functionality:
module.exports = someUtility;
- Import functionality:
import a from "./file";
-
CommonJS named exports
const a, b, c; exports.a = a; exports.b = b; exports.c = c;
-
Tree shaking
- Identify dead code that's imported but not used
- Add a webpack.config.js file
module.exports = { mode: "none" };
Webpack core concepts
-
Webpack uses a single JavaScript file as an entry point to "kick-off" the application
- The entry file tells Webpack what files to load for the browser
-
Needs an output, specify in the configration file
- The output tells Webpack where and how to distribute bundles, it works with the entry file
module.exports = { entry: './browser.main.ts', output: { path: './dist', filename: './bundle.js' } }
-
Loaders tells Webpack how to interpret and translate files
- Files are transformed one at a time before adding to the dependency graph
module: { rules: [ {test: /\.ts$/, use: ‘ts-loader’}, {test: /\.js$/, use: ‘babel-loader’}, {test: /\.css$/, use: ‘css-loader’} ] }
- test is a regex that instructs the compiler which files to run the loader against
- use is an array/string/function that returns loader objects
- enforce can be "pre" or "post", it tells Webpack to run this rule before or after all other rules
- include is an array of regex that instructs the compiler which files/folders to include
- exclude is an array of regex that instructs the compiler which files/folders to ignore
module: { rules: [ { test: regex, use: (Array|String|Function) include: RegExp[], exclude: RegExp[], issuer: (RegExp|String)[], enforce: “pre”|”post” }, ] }
- Chaining loaders, applies right-to-left (applicative-order evaluation): less-loader -> css-loader -> style-loader
rules: [ { test: /\.less$\, use: ['style', 'css', 'less'] } ]
-
Webpack plugins adds functionality to compilations (optimised bundle modules)
- Objects with an apply property
- Enables you to hook into the entire compilation lifecycle
- There are a variety of built-in plugins
function BellOnBundlerErrorPlugin () { } BellOnBundlerErrorPlugin.prototype.apply = function(compiler) { if (typeof(process) !== 'undefined') { // Compiler events that are emitted and handled compiler.plugin('done', function(stats) { if (stats.hasErrors()) { process.stderr.write('\x07'); } }); compiler.plugin('failed', function(err) { process.stderr.write('\x07'); }); } } module.exports = BellOnBundlerErrorPlugin;
- Use require to add plugin into config, add a new instance to plugins key in the config object
var BellOnBundlerErrorPlugin = require(‘bell-on-error’); var webpack = require(‘webpack’); module.exports = { ... plugins: [ new BellOnBundlerErrorPlugin(), // Couple of the built-in plugins new webpack.optimize.CommonsChunkPlugin(‘vendors’), new webpack.optimize.UglifyJsPlugin() ] ... }
Setting up Webpack config
-
Add output
module.exports = () => ({ output: { filename: "bundle.js" } });
-
Add scripts to environment
"scripts": { ..., "dev": "npm run webpack -- --env.mode development", "prod": "npm run webpack -- --env.mode production", "prod:debug": "npm run debg -- --env.mode production", "dev:debug": "npm run debug -- --env.mode development" }
module.exports = (env) => ({ return { mode: env.mode, output: { filename: "bundle.js" } }; });
-
Adding plugins
- Create new directory /build-utils
const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = (env) => ({ return { mode: env.mode, output: { filename: "bundle.js" }, plugins: [ new HtmlWebpackPlugin(), new webpack.ProgressPlugin() ] }; });
-
Setting up local development server
"scripts": { "webpack-dev-server": "webpack-dev-server", ... }
-
Splitting environment configuration files
const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const modeConfig = env => require(`./build-utils/webpack.${env}`)(env); module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => { return { mode, output: { filename: "bundle.js" }, plugins: [ new HtmlWebpackPlugin(), new webpack.ProgressPlugin() ] }; };
yarn add webpack-merge --dev
const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const modeConfig = env => require(`./build-utils/webpack.${env}`)(env); const webpackMerge = require("webpack-merge"); module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => { return webpackMerge( { mode, output: { filename: "bundle.js" }, plugins: [ new HtmlWebpackPlugin(), new webpack.ProgressPlugin() ] }, modeConfig(mode), loadPresets({ mode, presets }) ); };
Using plugins
-
Using css with Webpack
- In build-utils/webpack.development.js:
module.exports = () => {( module: { rules: [ { test:, /\.css$/, use: ["style-loader", "css-loader"] } ] }; )};
-
Hot module replacement with css, patch changes incrementally without reloading the browser
"scripts": { ..., "dev": "npm run webpack -- --env.mode development --hot", ... }
- In build-utils/webpack.production.js:
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = () => {( output: { filename: "bundle.js" }, module: { rules: [ { test:, /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] } ] }, plugins: [new MiniCssExtractPlugin()] )};
-
File and URL loaders (can add images, etc.)
yarn add file-loader url-loader --dev
- Can limit file size option in URL loader
- In webpack.config.js:
const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const modeConfig = env => require(`./build-utils/webpack.${env}`)(env); const webpackMerge = require("webpack-merge"); module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => { return webpackMerge( { mode, module: { rules: [ { test: /\.jpe?g$/, use: [ { loader: "url-loader", options: { limit: 5000 } } ] } ] }, output: { filename: "bundle.js" }, plugins: [ new HtmlWebpackPlugin(), new webpack.ProgressPlugin() ] }, modeConfig(mode), loadPresets({ mode, presets }) ); };
-
Implementing presets allow configuration settings to be specified
- In build-utils/loadPresets.js:
const webpackMerge = require("webpackge-merge"); const applyPresets = env => { const { presets } = env; const mergedPresets = [].concat(...[presets]); const mergedConfigs = mergedPresets.map( presetName => require(`./presets/webpack.${presetName}`)(env) ); return webpackMerge({}, ...mergedConfigs); }; module.exports = applyPresets;
- In webpack.config.js, add module:
const presetConfig = require("./build-utils/"); module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => { return webpackMerge( { ... }, modeConfig(mode), presetConfig({ mode, presets }) ); };
- Adding TypeScript,
yarn add ts-loader typescript@next --dev
- In build-utils/webpack.typescript.js:
module.exports = () => ({ module: { rules: [ { test: /\.ts$/, use: "ts-loader" } ] }; });
"scripts": { ..., "prod:typescript": "npm run prod -- --env.presets typescript", ... }
-
Bundle Analyser Preset
yarn add webpack-bundle-analyzer --dev
"scripts": { ..., "prod:analyse": "npm run prod -- --env.presets analyze", ... }
- Create file build-utils/webpack.analyse.js
const WebpackBundleAnalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; module.exports = () => ({ plugins: [new WebpackBundleAnalyzer()] });
-
Compression plugin
yarn add compression-webpack-plugin --dev
"scripts": { ..., "prod:compress": "npm run prod -- --env.presets compress", ... }
- In webpack.compress.js:
const CompressionWebpackPlugin = require("compression-webpack-plugin"); module.exports = () => ({ plugins: [new CompressionWebpackPlugin()] });
-
Source maps provide information during debugging such as visualising project structure and adding break points
- In build-utils/webpack.development.js:
module.exports = () => {( devtool: "source-map", module: { rules: [ { test:, /\.css$/, use: ["style-loader", "css-loader"] } ] }; )};