Writing modern SPA applications

part 2: ANATOMY OF JAVASCRIPT APP

PACKAGE.JSON

  • Every Javascript application is a module/package and has it's package.json
     
  • Package.json holds all the metadata of the package (name, author, license etc)
     
  • Package.json also keeps all dependencies, scripts and sometimes extra configuration (babel, eslint, jest)

package dependencies

  • There are 5 kinds of dependencies but until you start writing libraries, you are only interested in two
     
  • "dependencies" are runtime dependencies, they are required for your package or app to work (for ex. express, React, Bootstrap etc)
     
  • "devDependencies" are development dependencies, they are required when developing or building your app/package (for ex. babel, eslint, jest, webpack)

package dependencies

Be careful!

 

devDependencies are not installed by default when installing packages via npm --production

 

If you want to compile your assets on server (during deployment), you will probably need to work around this or keep all your compilation-related packages in dependencies instead.

example package.json

{
  "name": "my-razzle-app",
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {
    "start": "razzle start",
    "build": "razzle build",
    "test": "razzle test --env=jsdom",
    "start:prod": "NODE_ENV=production node build/server.js"
  },
  "dependencies": {
    "razzle": "^0.7.1",
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "react-router-dom": "^4.1.1"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.1",
    "node-sass": "^4.5.3",
    "postcss-flexbugs-fixes": "^3.0.0",
    "resolve-url-loader": "^2.1.0",
    "sass-loader": "^6.0.6"
  }
}

how to bootstrap your app

what starter to use?

manual way

react-starter-kit PROS

  • Isomorphic, GraphQL on the back-end by default
     
  • Supports Docker out of the box
     
  • Solves some of the common problems in isomorphic apps (SQL support with examples, FB authentication)

react-starter-kit CONS

  • Does not use React Router, only Universal Router
     
  • Generator is ancient so it requires cloning their repository instead
     
  • Lots of preconfigured garbage that might not even be needed and must be deleted
     
  • Does not abstract away any complexity
     
  • Steep learning curve

VERDICT:

SOLVES PROBLEMS,
BUT AT WHAT COST

create-react-app PROS

  • Created and maintained by Facebook®
     
  • Zero-configuration - works out of the box, entire tooling configuration is abstracted away
     
  • Does not impose Redux (or any other Flux-based architecture)

create-react-app CONS

  • Zero-configuration means you have to "eject" to change anything and it breaks the magic
     
  • Does not support server-side rendering out of the box (only SPA)
     
  • Does not support SASS, only PostCSS with autoprefixer

VERDICT:

GOOD ENOUGH

(but does not solve all the problems)

react-app-rewired PROS

  • Thin layer over create-react-app
     
  • Allows adding custom plugins and loaders to webpack

react-app-rewired CONS

  • Breaks CRA guarantees by basically being a hack
     
  • May not be up-to-date with react-scripts
     
  • Since it's thin layer over CRA, it only solves the same problems CRA does so still no SSR

VERDICT:

CAREFUL, BUT OPTIMISTIC

NWB PROS

  • More configurable than create-react-app
     
  • Supports plugins (Sass)
     
  • Allows custom configuration of webpack via nwb.config.js

NWB CONS

  • Karma as a testing framework, which means PhantomJS...
    (This can be mitigated by configuring Jest on your own but dependencies are still going to be installed 👎)

     
  • There is no ejecting so you cannot drop it easily if you don't like it
     
  • No SSR (again...)

VERDICT:

LOT OF ROOM FOR IMPROVEMENT

razzle PROS

  • Server side rendering (finally!!!)
     
  • Works very similar to nwb but was based on create-react-app
     
  • Allows custom configuration via razzle.config.js
     
  • Hot module reloading both for client and server

razzle CONS

  • No eslint by default (but can be easily added)
     
  • No eject (it's on roadmap)
     
  • No Sass by default (can be added in slightly convoluted fashion, I'll later release an npm package to make it cleaner)

VERDICT:

I LOVE IT

webpack configuration

EXPLAINED

var path = require('path');
var webpack = require('webpack');

var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: [
    'webpack-dev-server/client?http://localhost:3000',
    'webpack/hot/only-dev-server',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    publicPath: '/',
    filename: 'app.[hash].js'
  },
  devtool: 'eval',
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, use: [ 'babel-loader' ], },
      { test: /\.scss|css$/,
        use: [
          { loader: 'style-loader', options: { sourceMap: true } },
          { loader: 'css-loader', options: { sourceMap: true } },
          { loader: 'postcss-loader', options: { sourceMap: true } },
          'resolve-url-loader',
          {
            loader: 'sass-loader',
            query: {
              sourceMap: true,
              includePaths: [ path.resolve(__dirname, './node_modules') ]
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({ hash: false, template: 'html-loader!./src/index.html' }),
  ],
  resolve: {
    modules: [ path.resolve('./src'), 'node_modules' ]
  }
};

entry

  entry: [
    'webpack-dev-server/client?http://localhost:3000',
    'webpack/hot/only-dev-server',
    './src/index'
  ],

entry

  • Webpack allows creating multiple bundles at the same time.
     
  • If we create only one bundle, entry should be an array of paths to files that will be then merged into one file (this is useful for prepending things like polyfills, HMR etc)
     
  • If we create multiple bundles, entry should be an object/hash where keys are bundle names (referenced later by [name])

OUTPUT

  output: {
    path: path.join(__dirname, 'dist'),
    publicPath: '/',
    filename: 'app.[hash].js'
  },

OUTPUT

  • Output decides where Javascript bundles will be created
     
  • When setting filename you can use [hash] for digests (like sprockets) and [name] if you generate multiple bundles
     
  • publicPath is used by loaders and other tooling to figure out where your assets will be in relation to index file. It can be an /assets/ directory for example

DEVTOOL

  devtool: 'eval',

DEVTOOL

  • Devtool is actually a pretty deceiving name for source maps configuration
     
  • Different devtool configurations produce different source maps, some of them have better stack traces but are generated more slowly, some of them are fast and cheap but harder to debug

WHICH SHOULD I USE?

DEPENDS ON THE PROJECT

(AND PERSONAL PREFERENCE)

MODULE.RULES

  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, use: [ 'babel-loader' ], },
      { test: /\.scss|css$/,
        use: [
          { loader: 'style-loader', options: { sourceMap: true } },
          { loader: 'css-loader', options: { sourceMap: true } },
          { loader: 'postcss-loader', options: { sourceMap: true } },
          'resolve-url-loader',
          {
            loader: 'sass-loader',
            query: {
              sourceMap: true,
              includePaths: [ path.resolve(__dirname, './node_modules') ]
            }
          }
        ]
      }
    ]
  },

module.rules

  • List of all loaders used in project with regexes for filenames and optional include/exclude lists
     
  • Can contain multiple loaders written either as string, require.resolve() or hash with detailed configuration
     
  • Remember that loaders can be chained? If you list several loaders, remember to make sure the order is correct and that those loaders support being chained this way

PLUGINS

  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({ hash: false, template: 'html-loader!./src/index.html' }),
  ],

plugins

  • Plugins are always loaded by requiring them at the top of webpack config file and then initializing them in the list
     
  • Configuration is almost always passed via constructor arguments
     
  • Keep in mind that some plugins make sense only in either development (like HMR) or production (like CompressionPlugin)

RESOLVE

  resolve: {
    modules: [ path.resolve('./src'), 'node_modules' ]
  }

resolve

  • List of root directories for require/imports.
     
  • Useful to include your project's source here, otherwise all imports in the project must be relative (./ and ../)
     
  • Be careful: When including your project, make sure you're not overwriting installed node modules (or node modules are not overwriting your paths)!

there is more

but it's not mandatory

check out documentation

https://webpack.js.org/configuration

questions?

Writing modern SPA applications part 2

By Michał Matyas

Writing modern SPA applications part 2

  • 716