(and your JavaScript)
Øredev 2017
Gleb Bahmutov, PhD
Friday Nov 10, 14:20
Homo Agitatus
undefined is not a function
CoffeeScript then JavaScript
C++ / C#
SilverLight
Java
JavaScript
JavaScript
<div onclick="alert('hi')">Click me</div>
<script src="app.js"></script>
<div id="app" />
Single Page Applications (SPA)
React, Angular, Vue, Ember
JScript
VBScript
Dart
Flash
Silverlight
Java
Replacing mobile applications
I don't want to download 200MB app to read tweets
I can't afford to download 200MB app to read tweets
me
billions of people
Code Web App convert to Native app
node sever.js -p 3000
const http = require('http')
const hostname = '127.0.0.1'
const port = 3000
const server = http.createServer((req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('Hello World\n')
})
server.listen(port, hostname, () => {
console.log(`Server running at
http://${hostname}:${port}/`)
})
Build cross platform desktop apps with JavaScript, HTML, and CSS
# Clone the Quick Start repository
$ git clone https://github.com/electron/electron-quick-start
# Go into the repository
$ cd electron-quick-start
# Install the dependencies and run
$ npm install && npm start
const electron = require('electron')
let mainWindow = new BrowserWindow()
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
Platform | CI | Output Binary |
---|---|---|
Darwin | Buildkite | Electron |
Linux | CircleCI | Electron |
Windows | AppVeyor | Electron |
Qt, Java, ... Node 👍
Cypress GUI is a web app itself running as Electron application
if [ -d "foo" ] then:
cp -r foo bar
fi
const shell = require('shelljs')
if (fs.existsSync('foo')) {
shell.cp('-r', 'foo', 'bar')
}
Works (Linux)
Mostly works (Darwin)
🔥 (Windows)
Just works!
I don't want to learn your cryptic script commands
const shell = require('shelljs')
shell.set('-v') // verbose
shell.set('-e') // any error is fatal
shell.exec('npm run binary-build ...')
shell.exec('npm run binary-zip')
shell.ls('-l', '*.zip')
// more commands
CI requires only Node
make, curl, git, bash, zip, s3cmd
FROM: node
Docker
Browser, server, mobile, shell scripts
Let's look at the JavaScript language
EcmaScript (ES) = standard
JavaScript = implementation
* "JavaScript" is a trademark of Oracle Co
*
All modern browsers
var x = 'foo'
var y = 42
function add(a, b) {
return a + b
}
add(x, y)
// EcmaScript has no input / output ⚠️
function double(x) {
return x + x
}
function greatherThan4(x) {
return x > 4
}
['1', '2', '3'].map(parseFloat)
.map(double)
.filter(greatherThan4)
.forEach(console.log)
Arrays with .map .filter .reduce
function double(x) {
return x + x
}
function greatherThan4(x) {
return x > 4
}
['1', '2', '3'].map(parseFloat)
.map(double)
.filter(greatherThan4)
.forEach(console.log)
Pass functions as arguments
The present*
* mostly
const x = 'foo'
[1, 2, 3].map(x => x + x)
console.log(`x = ${x}`)
// and lots of other things...
[1, 2, 3].includes(2) // true
2 ** 3 // 8
async function userName () {
const user = await fetch()
return user.name
}
// plus a few other features
async function userName () {
try {
const user = await fetch()
return user.name
} catch (err) {
// deal with error
}
}
Anyone can propose a new feature
I want JavaScript to have X
an idea is born
Discussion, demos, polyfills
Feature gets a "champion"
TC39 committee: about 25 members
Mozilla, Google, Apple, Intel, universities
Draft and precise spec language is being developed
All semantics, syntax and API are completed described
Feature has tests
Two reference implementations
Stage 4 features as of March
become part of the standard published in June
try {
eval('2 ** 8')
} catch () {
// ** is not supported
}
// ** is ✅
*
* except for (maybe) ES6 modules
Detect and polyfill only what's missing with modernizr.com
<script src="polyfills.js"></script>
<script>
[1, 2, 3].includes(2) // works now
</script>
Does not work with every feature
<script src="polyfills.js"></script>
<script>
2 ** 3
// SyntaxError
</script>
Rewrite code to older standard
2 ** 3 // app.js
babel app.js
Math.pow(2, 3)
ES2016
ES5
* not every feature can be transpiled
*
Evergreen browser
Chrome, FF, Safari Technical Preview, Edge
aka OSS heaven
source: http://www.modulecounts.com
source: https://libraries.io
source: https://libraries.io
source: https://npms.io
Creating your own module
$ mkdir my-app
$ cd my-app
$ git init
$ npm init -y
$ npm publish
$ npm init -y
Wrote to my-app/package.json
{
"name": "my-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
$ yo node-bahmutov
{
"name": "stop-build",
"version": "0.0.0-development",
"description": "Exits with non-zero code if there are modified Git files",
"author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
"bugs": "https://github.com/bahmutov/stop-build/issues",
"config": {
"next-update": {
"commands": {
"pre-git": "./.git/hooks/pre-commit"
}
},
"pre-git": {
"commit-msg": "simple",
"pre-commit": [
"npm run pretest",
"npm run deps",
"npm run ban",
"git add ."
],
"pre-push": [
"npm test",
"npm run secure",
"npm run license",
"npm run ban -- --all",
"npm run size"
],
"post-commit": [],
"post-merge": []
}
},
"engines": {
"node": ">=6"
},
"files": [
"src/*.js",
"!src/*-spec.js"
],
"bin": {
"stop-build": "src/index.js"
},
"homepage": "https://github.com/bahmutov/stop-build#readme",
"keywords": [
"build",
"ci",
"exit",
"git",
"utility"
],
"license": "MIT",
"main": "src/",
"publishConfig": {
"registry": "http://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/bahmutov/stop-build.git"
},
"scripts": {
"ban": "ban",
"deps": "deps-ok",
"issues": "git-issues",
"license": "license-checker --production --onlyunknown --csv",
"pretty": "prettier-standard 'src/*.js'",
"prelint": "npm run pretty",
"lint": "standard --verbose --fix src/*.js",
"pretest": "npm run lint",
"secure": "nsp check",
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"test": "npm run unit",
"unit": "mocha src/*-spec.js",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
},
"devDependencies": {
"ban-sensitive-files": "1.9.0",
"deps-ok": "1.2.0",
"git-issues": "1.3.1",
"license-checker": "11.0.0",
"mocha": "3.4.2",
"nsp": "2.6.3",
"pre-git": "3.15.0",
"prettier-standard": "5.1.0",
"semantic-release": "^6.3.6",
"standard": "10.0.2"
},
"dependencies": {
"ggit": "1.15.1",
"pluralize": "5.0.0"
}
}
Yeoman
$ yo node-bahmutov
{
"name": "stop-build",
"version": "0.0.0-development",
"description": "Exits with non-zero code if there are modified Git files",
"author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
"bugs": "https://github.com/bahmutov/stop-build/issues",
"config": {
"next-update": {
"commands": {
"pre-git": "./.git/hooks/pre-commit"
}
},
"pre-git": {
"commit-msg": "simple",
"pre-commit": [
"npm run pretest",
"npm run deps",
"npm run ban",
"git add ."
],
"pre-push": [
"npm test",
"npm run secure",
"npm run license",
"npm run ban -- --all",
"npm run size"
],
"post-commit": [],
"post-merge": []
}
},
"engines": {
"node": ">=6"
},
"files": [
"src/*.js",
"!src/*-spec.js"
],
"bin": {
"stop-build": "src/index.js"
},
"homepage": "https://github.com/bahmutov/stop-build#readme",
"keywords": [
"build",
"ci",
"exit",
"git",
"utility"
],
"license": "MIT",
"main": "src/",
"publishConfig": {
"registry": "http://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/bahmutov/stop-build.git"
},
"scripts": {
"ban": "ban",
"deps": "deps-ok",
"issues": "git-issues",
"license": "license-checker --production --onlyunknown --csv",
"pretty": "prettier-standard 'src/*.js'",
"prelint": "npm run pretty",
"lint": "standard --verbose --fix src/*.js",
"pretest": "npm run lint",
"secure": "nsp check",
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"test": "npm run unit",
"unit": "mocha src/*-spec.js",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
},
"devDependencies": {
"ban-sensitive-files": "1.9.0",
"deps-ok": "1.2.0",
"git-issues": "1.3.1",
"license-checker": "11.0.0",
"mocha": "3.4.2",
"nsp": "2.6.3",
"pre-git": "3.15.0",
"prettier-standard": "5.1.0",
"semantic-release": "^6.3.6",
"standard": "10.0.2"
},
"dependencies": {
"ggit": "1.15.1",
"pluralize": "5.0.0"
}
}
generator-node-bahmutov
almost 1 million downloads PER DAY
var express = require('express')
var app = express()
app.get('/', function (req, res) {
res.send('Hello World')
})
app.listen(3000)
The page HTML arrives ready to be rendered by the browser
The browsers are REALLY REALLY good at rendering static pages
Server-side rendering is good
If the server goes down in the forest ...
<html manifest="example.appcache">
...
</html>
CACHE MANIFEST
# v1 2011-08-14
index.html
style.css
image1.png
# Use from network if available
NETWORK:
network.html
# Fallback content
FALLBACK:
/ fallback.html
declarative list
Turns out declaring caching strategy is hard.
ServiceWorker
Server
browser
Web Workers
ServiceWorker
(JavaScript)
Transforms
the response
Transforms
the request
Server
browser
service
worker
navigator.serviceWorker.register(
'app/my-service-worker.js')
Chrome, Opera, Firefox, Edge
Must be https
self.addEventListener('install', ...)
self.addEventListener('activate', ...)
self.addEventListener('message', ...)
self.addEventListener('push', ...)
self.addEventListener('fetch', function (event) {
console.log(event.request.url)
event.respondWith(...)
})
// Cache API
self.addEventListener('install', e => {
const urls = ['/', 'app.css', 'app.js']
e.waitUntil(
caches.open('my-app')
.then(cache =>
cache.addAll(urls))
)
})
Cache resources on SW install
self.addEventListener('fetch', e => {
e.respondWith(
caches.open('my-app')
.then(cache =>
cache => match(e.request))
)
})
Return cached resource
browser
ServiceWorker
Request
Response
Server
Server
browser
ServiceWorker
Request
Response
express.js
http.ClientRequest
JavaScript
http.ServerResponse
JavaScript
browser
ServiceWorker
Server
express.js
Server
browser
ServiceWorker
express.js
http.ClientRequest(Request)
http.ServerResponse(Response)
Demo at: https://express-service.gleb-demos.com
The library: github.com/bahmutov/express-service
Blog post with examples: Run Express server in your browser
There is no client-side* JavaScript :)
<link rel="serviceworker" scope="/" href="/sw.js">
*after first ServiceWorker registration
$ npm i -g nativefier
$ nativefier https://express-service.gleb-demos.com/
Packaging app for platform darwin x64 using electron v1.1.3
App built to /Users/gleb/git/ExpressService-darwin-x64
Use static linters to catch simple mistakes
Start "index.js" file
Prettier is an opinionated code formatter
One JavaScript Style to Rule Them All
$ npm home prettier
$ npm home standard
$ npm home prettier
$ npm home standard
$ npm install --save-dev \
prettier-standard standard
+ standard@10.0.3
+ prettier-standard@6.0.0
{
"scripts": {
"prelint": "prettier-standard '*.js'",
"lint": "standard --fix *.js",
"pretest": "npm run lint",
"test": "exit 1"
}
}
package.json
$ npm run lint
// index.js
const add = (a, b) => a + b
const sub = (a, b) => {
return a + b
}
module.exports.add = ad
module.exports.sub = sub
> standard --fix *.js
my-app/index.js:1:7:
'add' is assigned a value but never used.
my-app/index.js:5:22:
'ad' is not defined.
Node v8.x.x includes NPM v5.x.x and NPX
A helper tool for executing binaries from packages
$ node_modules/.bin/standard *.js
or
$ $(npm bin)/standard *.js
$ npx standard *.js
Write tests first
Keep refactoring while tests are passing
Tape, QUnit, Mocha, Ava, Jest
Delightful JavaScript Testing
$ npm install --save-dev jest
$ npx jest
/* eslint-env jest */
describe('add', () => {
const add = require('.').add
it('adds numbers', () => {
expect(add(1, 2)).toBe(3)
})
})
$ npx jest
PASS ./test.js
add
✓ adds numbers (4ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.763s, estimated 1s
Ran all test suites.
PASS ./test.js
add
✓ adds numbers (7ms)
sub
✓ subs numbers (1ms)
Snapshot Summary
› 2 snapshots written in 1 test suite.
it('adds numbers', () => {
expect(add(1, 2)).toMatchSnapshot()
expect(add(100, -50)).toMatchSnapshot()
})
$ cat __snapshots__/test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`add adds numbers 1`] = `3`;
exports[`add adds numbers 2`] = `50`;
Saved snapshot files
expect(value).toMatchSnapshot()
Received value does not match stored snapshot 1.
- 3
+ 21
it('adds numbers', () => {
expect(add(1, 20)).toMatchSnapshot()
expect(add(100, -50)).toMatchSnapshot()
})
// add.js
function add (a, b) {
return a + b
}
module.exports = add
// index.js
const add = require('./add')
add(2, 3) // 5
add.js sub.js index.js
<script src="bundle.js"></script>
bundle.js
Huge JS bundles because have to include everything, even if code is unused
// add.js
export function add (a, b) {
return a + b
}
// index.js
import {add} from './add'
$ node add.js
./add.js:2
export function add (a, b) {
^^^^^^
SyntaxError: Unexpected token export
* Target: Node 10, April 2018
*
$ npm i -S @std/esm
{
"@std/esm": {
"esm": "js"
},
"scripts": {
"start": "node -r @std/esm index.js"
}
}
add to package.json
install dependency
// index.js
import {add} from './add'
import {sub} from './sub'
console.log(add(2, 3))
$ npm start
> node -r @std/esm index.js
5
$ npm i -D rollup
{
"scripts": {
"build": "rollup index.js -o bundle.js --f iife"
}
}
add to package.json
install dependency
$ npm run build
build the bundle
// index.js
import {add} from './add'
import {sub} from './sub'
console.log(add(2, 3))
Thanks, explicit "import / export" declarations!
(function () {
'use strict';
function add (a, b) {
return a + b
}
console.log(add(2, 3));
}());
$ cat bundle.js
There is no "sub" code in the bundle!
<head>
<!-- in case ES6 modules are supported -->
<script src="app/index.js" type="module"></script>
<!-- in case ES6 modules aren't supported -->
<script src="dist/bundle.js" defer nomodule></script>
</head>
IoT, robots, databases,
WebSockets, Audio and Video, P2P, WebAssembly, VR, compiled to JS languages
JavaScript language is running the modern web
The tools and libraries are incredibly diverse and can satisfy anyone's needs
There is a good process for introducing new language features every year
Øredev 2017