SPA Bootcamp

OSCON

Brian Capouch, Ben Davisson, Danilo Zekovic

Saint Joseph's College

github.com/capouch/oscon16

My background

  • Just retired from 34 years of teaching CS
  • C, Java, Perl, PHP; networking, systems programming
  • Eighteen months of JavaScript experience

YOU?

Target audience

  • Coding experience
  • New-ish to recent (ES6) JavaScript
  • Want to make web apps

Goals

  • Introduce fundamental SPA concepts
  • Demonstrate typical development workflow
  • Review significant concepts, patterns, etc.
  • Analyze a sample app that can serve as a learning tool

Workflow

git clone; cd <repo-name>

npm install

npm start

What is a SPA?

A client application loads instead of a traditional page

Views are generated on the client, instead of server trips

By analogy, control belongs to a client-side "shell"

The page context theoretically never changes

Routing is done on both client and server

Navigation and SEO semantics are fraught


<body>
  <main id="main"/>
  <script src="/js/bundle.js"></script>
</body>

public/index.html:15

The entire body of index.html


ReactDOM.render((
  <Router history={ browserHistory }>
    <Route path="/" component={ osconSPA } history={ browserHistory }>
      <IndexRoute name="home" component={ Home }/>
      <Route path="browse" component= { Browse }/>
      <Route path="zoomer" component= { Zoom }/>
      <Route path="zoomer/:imageId" component={ Zoom }/>
      <Route path="upload" component={ Upload }/>
      <Route path="slides" component={ SlideShow } />
      <Route path="slides/:viewSet" component={ SlideShow } />
      <Route path="*" component={ Home }/>
    </Route>
  </Router>
), document.getElementById("main"))

js/Shell.jsx:39

The top-level code that fills the main div

Node.js is key

  • JavaScript end-to-end
  • Single execution thread (mostly)
  • The Github explosion

Our sample app

Scene:History

A system to assist in archiving, cataloging, finding, and rendering high-definition scanned images

It has five views

  • Launch
  • Upload/Data entry
  • Browser
  • Deep Tile Zoomer
  • Slideshow

Each of the view modules exports a single default

Awesome syntactic feature: the name you assign to the default export DOESN'T MATTER!!!!

export default class UniqueNewYork extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    // This is the default case, i.e. Zoomer with no parameters
    let sendParms = "../tiles/bremer"

    // This fires if properties are sent explictly
    if ( this.props.params.imageId ) {
      sendParms = '/tiles/' + this.props.params.imageId
      }

    return (
      <Section>
        <center><ZoomBox image={sendParms}/></center>
      </Section>
    )
  }
}

/js/Zoom.js:51

No authentication

Minimal error checking

Often simplistic

No test scaffolding

oscon.saintjoe-cs.org:2016

This ain't your former self's JavaScript

  • ES6 brings a lot to the table  
  • Transpilation makes it easy to do
  • Functional capabilities add expressive power 

ES6 

  • Babel makes it trivially easy
  • Scoping and modules make for cleaner code
  • Is there a case for "classic" JavaScript anymore?
// Render composite component
export default React.createClass ( {
  render() {
    return (
      <div>
        <InfoTable
          url={ assetBase + queryTarget}/>
      </div>
    )
  }
})

js/Browse.js:198
// fieldValues provide a form's default input--in this case empty
let fieldValues = {
  title : null,
  description : null,
  source : null,
  taglist : null
}

// We blank fields after each data entry event
const blankFieldValues = {
  title: null,
  description: null,
  source: null,
  taglist: null
}

js/Upload.js:74
fieldValues = Object.assign({}, fieldValues, fields)

js/Upload.js:103
export default {
  ...image,
  ...geopoint
};

graphql/queries/index.js:4

Think async

    At the moment I worked this out I thought, "WUT?"

    this.setState({fetchURL: searchURL}, function(){
        this.loadRecordsFromServer()
        sessionStorage.setItem('browse', JSON.stringify(this.state))
        }.bind(this))

js/Browse.js:148

Let's desconstruct:

This is the basic operation we want, to set a React state variable

this.setState({fetchURL: searchURL})
But the operation is asynchronous - i.e. we have to use a callback when it finishes

// Once we know our change is in place, reload current working set and cache component state
function(){
        this.loadRecordsFromServer()
        sessionStorage.setItem('browse', JSON.stringify(this.state))
        }

Shell.jsx

Browse.js

SlideShow.js

Upload.js

Zoom.js

Modules

  • Maintainability 
  • Namespace
  • Re-usability 
exports.operation = (function() {
  var num1 = 4; // this is a local variable

  function addNumbers (num2) {
    var sum = num1 + num2;
    return sum;
  }

  return {
    addNumbers : addNumbers
  };
})();

Module as function

ES6 solves many problems from past

let, const and var

let num1 = 4

function addNumbers (num2) {
  return num1 + num2
}

export default addNumbers

ES6 modules

import addNumbers from './addNumbers

CommonJS

  • Server-first approach
  • Synchronous loading
  • Not a good solution for the client side
  • Returns only objects
function moduleName() {
  // do something
  ...
}

module.exports = moduleName;

CommonJS

var moduleName = require("moduleName");

AMD

  • Asynchronous Module Definition
  • Browser-first approach
  • Exports more than just objects (strings, functions, constructors, JSON)
define([], function) {
  var foo = "some value"; // local variable
  return {
    bar : function(){
      // some code to be returned
    }
  }
}

AMD

require(['someFile'], function ( bar ) {
        // your code
        bar.doSomething();
});

Routing

Client

Server

HTTP request

HTML page

Server

Client

Router

HTTP request

View

Client Side

Server Side

Server Side Routing

  • Syntax did not change compared to traditional
  • It always returns the same file, regarding the route given
router.get('/', function(req, res) {
  res.sendFile('index.html', options)
})

router.get('/upload', function(req, res) {
  res.sendFile('index.html', options)
})

Server Side (Express)

Client Side Routing

ON MATCH "route/example"
    FUNCTION execute functionality
    VIEW render requested page

AngularJS

.when("/routes/example", {
  templateUrl : "views/example.html",
  controller : "exampleController"
})

Third party routing library (page.js)

page('/', index);
page('/upload', upload);
page('/browse', browse);
...
htmlElement.hide()
htmlElemetn.show()

jQuery

render((
  <Router history={hashHistory}>
    <Route path="/" component={App}>
      <Route path="/about" component={About}/>
      <Route path="/contact" component={Contact}/>
    </Route>
  </Router>
), document.getElementById('app'))

React Router

<NavItem><NavLink to="/about" className="nav-link">About</NavLink></NavItem>
<NavItem><NavLink to="/contact" className="nav-link">Contact</NavLink></NavItem>

Default router 

Parameters (:someoption)

History (HTML5)

Four Variants

Cloud

Master

Two variations:

        - uses online storage

        - uses local storage

 

 

Switched by Source Code

Desktop

Electron-dist

       - storage on cloud

Electron-local

       - storage on local file system

 

 

Switched by Branches

Electron's Promise

New Concept:

Single Codebase serves as Web App & Desktop App

Electron

Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS

Increased Security​

Offline Access

Personal Use

Use for Today

  • Web App(cloud)
  • Master Branch

Electron is Complex

  • Large Files on GitHub

  • Assets must be installed Locally

Switching Variants

Requires change in the source code

Browse.js

 

SlideShow.js

 

Zoom.js

Changes in Browse.js

Line 20

// 1.
// Select one of the two to configure for local/cloud access
// Local assets
// const assetBase = '/oscon-test?'
//
// Cloud assets
const assetBase = 'http://oscon.saintjoe-cs.org:2016/oscon-test?'
// 2.
// Local assets
// let searchURL = assetBase + queryTarget
// Cloud assets
let searchURL = 'http://oscon.saintjoe-cs.org:2016/oscon-test?' + queryTarget

Line 141

Changes in Slideshow.js

Line 14

// 2.
// cloud assets:
const urlBase = 'http://oscon.saintjoe-cs.org:2016/'
// local assets:
// const urlBase = '/'

Line 67

// 1.
// Cloud assets
const assetBase = 'http://oscon.saintjoe-cs.org:2016/oscon-test?'
//
// Local assets
// const assetBase = '/oscon-test?'

Changes in Zoom.js

Line 15

 

// 1.
  // Use cloud-based image assets
  const assetBase ="http://oscon.saintjoe-cs.org:2016/"
  //
  // Use local assets
  // const assetBase = ''

Deep Zoom Image Tiles