@JSConf.CN Shanghai July 2017
Speaker
Attendee
Alibaba Trip
Ele.me
Attendee
Wepiao.com
https://huangxuan.me/githuber.js/
API Data => UI
JavaScript Framework
Client-side Routing
Single-Page App
Transpiler
Bundler
API Data => UI
JavaScript Framework
Client-side Routing
Single-Page App
Transpiler
Bundler
https://huangxuan.me/githuber.js/
Runtime & Entry
Code via HTTP
2008-01-15
Added section on specifying a web clip icon.
2008-09-09
Bookmarked home screen apps open in full screen
<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes">
<mate name="theme-color" content="#000000">
<!-- Add to homescreen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Lighten">
<!-- Tile icon for Win8 (144x144 + tile color) -->
<meta name="msapplication-TileImage" content="images/touch/ms-touch-icon-144x144-precomposed.png">
<meta name="msapplication-TileColor" content="#3372DF">
<!-- Icons for iOS and Android Chrome M31~M38 -->
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="images/touch/apple-touch-icon-144x144-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="images/touch/apple-touch-icon-114x114-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="images/touch/apple-touch-icon-72x72-precomposed.png">
<link rel="apple-touch-icon-precomposed" href="images/touch/apple-touch-icon-57x57-precomposed.png">
<!-- Generic Icon -->
<link rel="shortcut icon" href="images/touch/touch-icon-57x57.png">
<!-- Chrome Add to Homescreen -->
<link rel="shortcut icon" sizes="196x196" href="images/touch/touch-icon-196x196.png">
{
"name": "Githuber.JS",
"short_name": "Githuber.JS",
"icons": [{
"src": "logo-512x512.png",
"type": "image/png",
"sizes": "512x512"
}],
"start_url": "./",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#f36a84",
"background_color": "#ffffff"
}
<link rel="manifest" href="/manifest.json">
// Somewhere in your javascript
var localServer = google.gears.factory.create("localserver");
var store = localServer.createManagedStore(STORE_NAME);
store.manifestUrl = "manifest.json"
{
"betaManifestVersion": 1,
"version": "1.0",
"entries": [
{ "url": "index.html"},
{ "url": "main.js"}
]
}
<html manifest="cache.appcache">
CACHE MANIFEST
CACHE:
style/default.css
images/sound-icon.png
images/background.png
NETWORK:
comm.cgi
HTTP caching Race Condition
Unrecoverable Errors (Cache the manifest)
Success only If ALL resources are cached
Not a Progressive Enhancement
Caching Dynamic Contents needs hack & re-architecture
Full of Assumption
The Cache is organized by "Manifest"
Manifest changes trigger ALL cached files redownload.
NO clean up mechanism for cache
Deficit of Flexibility
Unprogrammable
Very Limited Fallback/Routing
Network
Download & Run on the fly
Cache Storage
Network
A Client-side JavaScript Proxy
Service Worker
// register Service Worker in index.html
if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then( registration => {
console.log('Service Worker Registered');
})
.catch( error => {
console.log('Registration failed with' + error);
})
}
/* sw.js */
self.onfetch = (e) => {
e.respondWith(new Response('Hello World from SW!'))
}
// IDL
interface ExtendableEvent : Event {
void waitUntil(Promise<any> f);
};
// sw.js
self.oninstall = (e) => {
e.waitUntil(promiseA)
}
self.onactivate = (e) => {
e.waitUntil(promiseB)
}
const CACHE_NAMESPACE = 'githuber.js.dev-'
const PRECACHE = CACHE_NAMESPACE + 'precache'
const PRECACHE_LIST = [
'./',
'./static/js/bundle.js',
]
self.oninstall = (e) => {
e.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_LIST))
)
}
const CACHE_NAMESPACE = 'githuber.js.dev-'
const PRECACHE = CACHE_NAMESPACE + 'precache'
const PRECACHE_LIST = [
'./',
'./static/js/bundle.js',
]
self.oninstall = (e) => {
e.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_LIST))
)
}
Cache Storage
Network
Service Worker
self.onfetch = (e) => {
// suppose we have offline.html in caches
// match offline.html in all cache opened in caches
const sorry = caches.match("offline.html")
// if the fetched reject, we return the sorry Response.
e.respondWith(
fetched.catch(_ => sorry)
)
}
self.onfetch = (e) => {
const fetched = fetch(e.request)
// match offline.html in all cache opened in caches
const sorry = caches.match("offline.html")
// if the fetched reject, we return the sorry Response.
e.respondWith(
fetched.catch(_ => sorry)
)
}
Strategies - Network First
self.onfetch = (e) => {
// Cuz we are a SPA using History API,
// we need "rewrite" navigation requests to root route.
let url = rewriteUrl(e);
//
const cached = caches.match(url)
e.respondWith(
cached
.then(resp => resp || fetch(url))
.catch(_ => {/* eat any errors */})
)
}
self.onfetch = (e) => {
// Cuz we are a SPA using History API,
// we need "rewrite" navigation requests to root route.
let url = rewriteUrl(e);
// match url in all cache opened in caches
const cached = caches.match(url)
e.respondWith(
cached
.then(resp => resp || fetch(url))
.catch(_ => {/* eat any errors */})
)
}
self.onfetch = (e) => {
// Cuz we are a SPA using History API,
// we need "rewrite" navigation requests to root route.
let url = rewriteUrl(e);
// match url in all cache opened in caches
const cached = caches.match(url)
e.respondWith(
cached
.then(resp => resp || fetch(url))
.catch(_ => {/* eat any errors */})
)
}
Strategies - Cache First
Cache Storage
Network
Service Worker
Registration - The First Service Worker
©web/fundamentals/instant-and-offline/service-worker/lifecycle
self.onactivate = (e) => {
// Clients.claim() let SW control the page in the first load
clients.claim()
}
/**
* In production,
* We could introduce a build process to automatically
* cause byte-diffs by
* (1) injecting versioned assets manifest
* (2) increasing/generating VERSION by diffing all assets
*/
const VERSION = "0"
const VERSION = "1"
Registration - Updating Service Worker
©web/fundamentals/instant-and-offline/service-worker/lifecycle
self.oninstall = (e) => {
e.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_LIST))
.then(self.skipWaiting())
.catch(err => console.log(err))
)
}
// broadcasting clients to do window.location.reload()
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage(REFRESH_MSG)
})
})
// new API: client.navigate
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.navigate(REFRESH_URL)
})
})
// registration.waiting.postMessage()
self.onmessage = (e) => {
switch (e.data.command) {
case "SKIP_WAITING_AND_RELOAD_ALL_CLIENTS_TO_ROOT":
self.skipWaiting()
.then(_ => reloadAllClients("/"))
.catch(err => console.log(err))
break;
}
}
Cache Storage
Network
Service Worker
HTTP Disk Cache
cache-control:max-age=315360000
GET /bundle.js
self.onfetch = (e) => {
// Fetch API may support cache mode in the future:
let fetched = fetch(e.request, {cache: 'reload'})
let fetched = fetch(e.request, {cache: 'no-cache'})
// Now you can only use cache-busting query to workaround.
// but would also break revalidation depends on your CDN
let fetched = fetch(`${e.request.url}?${Date.now()}`)
e.respondWith(fetched)
}
// long-term caching
cache-control:max-age=315360000
// Could be injected in build process
const PRECACHE_LIST = [
'./',
'./script-f93bca2c.js',
'./styles-a837cb1e.css',
'./cats-0e9a2ef4.jpg'
]
self.oninstall = (e) => {
e.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_LIST))
)
}
// sw.js
const PRECACHE = "precache" + VERSION
const RUNTIME = "runtime" + VERSION
const expectedCaches = [PRECACHE, RUNTIME]
self.onactivate = (e) => {
e.waitUntil(
caches.keys().then(cacheNames => Promise.all(
cacheNames
.filter(cacheName => cacheName.startsWith(CACHE_NAMESPACE))
.filter(cacheName => !expectedCaches.includes(cacheName))
.map(cacheName => caches.delete(cacheName))
))
)
}
// sw-precache-config.js
module.exports = {
staticFileGlobs: [
'app/css/**.css',
'app/**.html',
'app/images/**.*',
'app/js/**.js'
]
};
$ sw-precache --config=path/to/sw-precache-config.js
// sw.js
var precacheConfig = [
["js/a.js", "3cb4f0"],
["css/b.css", "c5a951"]
]
// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}
// webpack.config.js
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
module.exports = {
plugins: [
new SWPrecacheWebpackPlugin({
// assets already hashed by webpack aren't concerned to be stale
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'service-worker.js',
minify: true,
navigateFallback: PUBLIC_PATH + 'index.html',
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
}),
],
};
Can be used with Gulp/Grunt as well !
Buzzword Key Technology The Architecture
// here, we hard-code the online/offline logics
// In production, we can expose callbacks to subscribers
function updateOnlineStatus(event) {
if(navigator.onLine){
document.body.classList.remove('app-offline')
}else{
document.body.classList.add('app-offline');
createSnackbar({ message: "you are offline." })
}
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
// sw.js
self.onfetch = (e) => {
// ...
if(url.includes('api.github.com')){
e.respondWith(networkFirst(url));
return;
}
if(url.includes('githubusercontent.com')){
e.respondWith(staleWhileRevalidate(url));
return;
}
if(PRECACHE_ABS_LIST.includes(url)){
e.respondWith(cacheOnly(url));
return;
}
// default: Network Only
}
// sw.js
self.onfetch = (e) => {
// ...
if(url.includes('api.github.com')){
e.respondWith(networkFirst(url));
return;
}
if(url.includes('githubusercontent.com')){
e.respondWith(staleWhileRevalidate(url));
return;
}
if(PRECACHE_ABS_LIST.includes(url)){
e.respondWith(cacheOnly(url));
return;
}
// default: Network Only
}
// sw.js
self.onfetch = (e) => {
// ...
if(url.includes('api.github.com')){
e.respondWith(networkFirst(url));
return;
}
if(url.includes('githubusercontent.com')){
e.respondWith(staleWhileRevalidate(url));
return;
}
if(PRECACHE_ABS_LIST.includes(url)){
e.respondWith(cacheOnly(url));
return;
}
// default: Network Only
}
Strategies - Stale While Revalidate
Strategies - Fastest
// sw.js
function replaceRuntimeCache(MAX_ENTRIES){
caches.open(RUNTIME)
.then(cache => {
cache.keys()
.then(entries => {
// FIFO queue
if(entries.length > MAX_ENTRIES) {
cache.delete(entries[0])
}
})
})
}
// sw.js
importScripts('sw-toolbox.js')
// sw.js
importScripts('sw-toolbox.js')
toolbox.router.get('/(.*)');
// sw.js
importScripts('sw-toolbox.js')
toolbox.router.get('/(.*)', global.toolbox.cacheFirst);
// sw.js
importScripts('sw-toolbox.js')
toolbox.router.get('/(.*)', global.toolbox.cacheFirst, {
cache: {
name: 'products',
maxEntries: 12,
},
origin: /\.products\.com$/
});
// sw.js
importScripts('sw-toolbox.js')
toolbox.router.get('/(.*)', global.toolbox.cacheFirst, {
cache: {
name: 'products',
maxEntries: 12,
maxAgeSeconds: 86400 // 24hr
},
origin: /\.products\.com$/
});
// sw-precache-config.js
module.exports = {
// ...
runtimeCaching: [{
urlPattern: /this\\.is\\.a\\.regex/,
handler: 'networkFirst'
}]
};
// sw.js with sw-toolbox imported
toolbox.precache([
"./index.a35bc762.js",
"./style.5217a6fb.css"
])
// User.jsx
export default class User extends Component {
// ...
openDedicatedCache(data){
caches.open(`${data.login}.githuber.js`)
.then(cache => cache.addAll([
`https://api.github.com/users/${data.login}`,
data.avatar_url
]))
.then(_ => {
this.setState({
cached: true
})
})
}
}
// Home.jsx
export default class Home extends Component {
// ...
inspectCache(data){
if(!window.caches) return; // PE
caches.keys()
.then(cacheNames => cacheNames.filter(
cacheName => cacheName.endsWith('githuber.js')))
.then(cacheNames => Promise.all(
cacheNames.map(
cacheName => this.mapCacheNameToData(cacheName))))
.then(cached => this.setState({cached: cached}))
}
}
Code Splitting By Route + PRPL Pattern
<link rel = "preload">
You know Dependency Graph better than browsers do
First Visit
Criteria
Repeat Visit
Criteria
Samsung Internet DeX / Chromebook / Win10
@phae
Service Worker
WASM
Fetch
React
Web GL
CSS
Promise
HTTPS
HTTP2.0
Web Payment
Device API
Web Socket
Web Push Protocol
Indexed DB
ExtendableEvent
Cache Storage
RWD
ES.Next
Web VR/AR
Vue
Angular
Web Components
Houdini
Notification
Next.js
Webpack
Babel
@brianleroux
@slightlylate
@huxpro
@JSConf.CN Shanghai July 2017
自豪的采用 @演说.io