Repository: bahmutov/bottle-service Branch: master Commit: b44a23fbedcc Files: 14 Total size: 27.5 KB Directory structure: gitextract_eg55b8px/ ├── .gitignore ├── .npmrc ├── .travis.yml ├── Procfile ├── README.md ├── deploy.json ├── dist/ │ ├── app.css │ ├── bottle-service.js │ ├── bottle.js │ └── index.html ├── package.json ├── src/ │ ├── bottle-service.js │ └── bottle.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ .grunt/ .DS_Store npm-debug.log ================================================ FILE: .npmrc ================================================ registry=http://registry.npmjs.org/ save-exact=true ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js cache: directories: - node_modules notifications: email: false node_js: - '4' before_install: - npm i -g npm@^2.0.0 before_script: - npm prune after_success: - npm run semantic-release branches: except: - "/^v\\d+\\.\\d+\\.\\d+$/" ================================================ FILE: Procfile ================================================ web: npm start ================================================ FILE: README.md ================================================ # bottle-service > Instant web applications restored from ServiceWorker cache [![NPM][bottle-service-icon] ][bottle-service-url] [![Build status][bottle-service-ci-image] ][bottle-service-ci-url] [![semantic-release][semantic-image] ][semantic-url] [Live demo](https://glebbahmutov.com/bottle-service/) - please use Chrome or Opera Desktop [Instant app demo](https://instant-todo.herokuapp.com/) - TodoMVC that is instant on page reload, hosted on free Heroku dyno, and needs Chrome browser for now ![page source](page-source.png) ## Browser support ### Chrome * Nothing to do, `ServiceWorker` should be enabled by default ### Firefox * Open `about:config` * Set the `dom.serviceWorkers.enabled` setting to **true** * Set the `dom.serviceWorkers.interception.enabled` setting to **true** ## Api This library attaches itself as `bottleService` object to the 'window'. Every time you want to store HTML snapshot for an element with id 'myApp', call ```js // "my-app-name" will be used in the future to allow separate parts of the // page to be saved separately //
// controlled by the web app
// application has been rendered bottleService.refill('my-app-name', 'myApp') ``` There are a couple of secondary api calls ```js bottleService.clear('my-app-name'); // delete whatever is stored in the ServiceWorker cache bottleService.print('my-app-name'); // prints the stored text to console ``` ## Example See [dist/index.html](dist/index.html) that includes the "application" code. Every time the user clicks "Add item" button, the application code adds a new DOM node, then tells the bottle service to store the new snapshot ```js var applicationName = 'bottle-demo' document.getElementById('add').addEventListener('click', function () { var el = document.getElementById('app') var div = document.createElement('div') var text = document.createTextNode('hi there') div.appendChild(text) el.appendChild(div) // store HTML snapshot bottleService.refill(applicationName, 'app') }) ``` When the page loads, the ServiceWorker will intercept `index.html` and will insert the saved HTML snapshot into the page before returning it back to the browser for rendering. Thus there is no page rewriting on load, no flicker, etc. ## Related * Instant web apps without page flicker or loading screens, [blog post](http://glebbahmutov.com/blog/instant-web-application/), [source repo](https://github.com/bahmutov/instant-vdom-todo) * Dynamic page rewriting on start [hydrate-vue-todo](https://github.com/bahmutov/hydrate-vue-todo) * Fast application start from pre-rendered HTML [hydrate-vdom-todo](https://github.com/bahmutov/hydrate-vdom-todo) ## Related projects using ServiceWorkers * [express-service](https://github.com/bahmutov/express-service) - run ExpressJS server inside ServiceWorker * [service-turtle](https://github.com/bahmutov/service-turtle) - Flexible http request interception using ServiceWorker ### Small print Author: Gleb Bahmutov © 2015 * [@bahmutov](https://twitter.com/bahmutov) * [glebbahmutov.com](http://glebbahmutov.com) * [blog](http://glebbahmutov.com/blog/) License: MIT - do anything with the code, but don't blame me if it does not work. Spread the word: tweet, star on github, etc. Support: if you find any problems with this module, email / tweet / [open issue](https://github.com/bahmutov/bottle-service/issues) on Github ## MIT License Copyright (c) 2015 Gleb Bahmutov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [bottle-service-icon]: https://nodei.co/npm/bottle-service.png?downloads=true [bottle-service-url]: https://npmjs.org/package/bottle-service [bottle-service-ci-image]: https://travis-ci.org/bahmutov/bottle-service.png?branch=master [bottle-service-ci-url]: https://travis-ci.org/bahmutov/bottle-service [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg [semantic-url]: https://github.com/semantic-release/semantic-release ================================================ FILE: deploy.json ================================================ { "gh-pages": { "options": { "base": "dist" }, "src": [ "index.html", "*.js", "*.css" ] } } ================================================ FILE: dist/app.css ================================================ .hidden { visibility: hidden; display: none; } body { padding: 2em; font-family: Didot, 'Didot LT STD', 'Hoefler Text', 'Garamond', 'Times New Roman', serif; font-size: larger; color: #333333; } button { padding: 5px 5px; background-color: white; font-size: large; } a { text-decoration: none; font-family: monospace; font-size: large; } .controls { margin-top: 1em; padding: 1em 1em; border: 1px solid #dddddd; border-radius: 5px; background-color: #fefefe; } ================================================ FILE: dist/bottle-service.js ================================================ /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { 'use strict' /* This is ServiceWorker code */ /* global self, Response, Promise, location, fetch */ var myName = 'bottle-service' console.log(myName, 'startup') function dataStore () { var cachesStorage = __webpack_require__(1) return cachesStorage(myName) } self.addEventListener('install', function (event) { console.log(myName, 'installed') }) self.addEventListener('activate', function () { console.log(myName, 'activated') }) var baseHref = location.href.substr(0, location.href.indexOf('bottle-service.js')) function isIndexPageRequest (event) { return event && event.request && event.request.url === baseHref } self.addEventListener('fetch', function (event) { if (!isIndexPageRequest(event)) { return fetch(event.request) } console.log(myName, 'fetching index page', event.request.url) event.respondWith( fetch(event.request) .then(function (response) { return dataStore() .then(function (store) { return store.getItem('contents') }) .then(function (contents) { if (contents && contents.html && contents.id) { console.log('fetched latest', response.url, 'need to update') console.log('element "%s" with html "%s" ...', contents.id, contents.html.substr(0, 15)) var copy = response.clone() return copy.text().then(function (pageHtml) { console.log('inserting our html') // HACK using id in the CLOSING TAG to find fragment var toReplaceStart = '
' var toReplaceFinish = '
' var startIndex = pageHtml.indexOf(toReplaceStart) var finishIndex = pageHtml.indexOf(toReplaceFinish) if (startIndex !== -1 && finishIndex > startIndex) { console.log('found fragment') pageHtml = pageHtml.substr(0, startIndex + toReplaceStart.length) + '\n' + contents.html + '\n' + pageHtml.substr(finishIndex) } // console.log('page html') // console.log(pageHtml) var responseOptions = { status: 200, headers: { 'Content-Type': 'text/html charset=UTF-8' } } return new Response(pageHtml, responseOptions) }) } else { return response } }, function notFound () { return response }) }) ) }) // use window.navigator.serviceWorker.controller.postMessage('hi') // to communicate with this service worker self.onmessage = function onMessage (event) { console.log('message to bottle-service worker cmd', event.data && event.data.cmd) // TODO how to use application name? dataStore().then(function (store) { switch (event.data.cmd) { case 'print': { return store.getItem('contents') .then(function (res) { console.log('bottle service has contents') console.log(res) }) } case 'clear': { console.log('clearing the bottle') return store.setItem('contents', {}) } case 'refill': { return store.setItem('contents', { html: event.data.html, id: event.data.id }).then(function () { console.log('saved new html for id', event.data.id) }) } default: { console.error(myName, 'unknown command', event.data) } } }) } /***/ }, /* 1 */ /***/ function(module, exports) { // Poor man's async "localStorage" on top of Cache // https://developer.mozilla.org/en-US/docs/Web/API/Cache if (typeof caches === 'undefined') { throw new Error('Cannot find object caches?! Cannot init cache-storage') } /* global caches, Response */ function dataStore (name) { var id = name ? name + '-v1' : 'cache-storage-v1' return caches.open(id) .then(function (cache) { return { setItem: function (key, data) { return cache.put(key, new Response(JSON.stringify(data))) }, getItem: function (key) { return cache.match(key) .then(function (res) { return res && res.text().then(JSON.parse) }) } } }) } module.exports = dataStore /***/ } /******/ ]); ================================================ FILE: dist/bottle.js ================================================ !(function startBottleService (root) { 'use strict' if (!root.navigator) { console.error('Missing navigator') return } if (!root.navigator.serviceWorker) { console.error('Sorry, not ServiceWorker feature, maybe enable it?') console.error('http://jakearchibald.com/2014/using-serviceworker-today/') return } // TODO package lazy-ass and check-more-types using webpack function toString (x) { return typeof x === 'string' ? x : JSON.stringify(x) } function la (condition) { if (!condition) { var args = Array.prototype.slice.call(arguments, 1) .map(toString) throw new Error(args.join(' ')) } } function isFunction (f) { return typeof f === 'function' } function getCurrentScriptFolder () { var scriptEls = document.getElementsByTagName('script') var thisScriptEl = scriptEls[scriptEls.length - 1] var scriptPath = thisScriptEl.src return scriptPath.substr(0, scriptPath.lastIndexOf('/') + 1) } var serviceScriptUrl = getCurrentScriptFolder() + 'bottle-service.js' // assume we are running at /pathname var scope = window.location.pathname var send = function mockSend () { console.error('Bottle service not initialized yet') } function registeredWorker (registration) { la(registration, 'missing service worker registration') la(registration.active, 'missing active service worker') la(isFunction(registration.active.postMessage), 'expected function postMessage to communicate with service worker') send = registration.active.postMessage.bind(registration.active) var info = '\nbottle-service - .\n' + 'I have a valid service-turtle, use `bottleService` object to update cached page' console.log(info) registration.active.onmessage = function messageFromServiceWorker (e) { console.log('received message from the service worker', e) } } function onError (err) { if (err.message.indexOf('missing active') !== -1) { // the service worker is installed window.location.reload() } else { console.error('bottle service error', err) } } root.navigator.serviceWorker.register(serviceScriptUrl, { scope: scope }) .then(registeredWorker) .catch(onError) root.bottleService = { refill: function refill (applicationName, id) { console.log('bottle-service: html for app %s element %s', applicationName, id) var el = document.getElementById(id) la(el, 'could not find element with id', id) var html = el.innerHTML.trim() send({ cmd: 'refill', html: html, name: applicationName, id: id }) }, print: function print (applicationName) { send({ cmd: 'print', name: applicationName }) }, clear: function clear (applicationName) { send({ cmd: 'clear', name: applicationName }) } } }(window)) ================================================ FILE: dist/index.html ================================================ bottle-service

bottle-service (tested on Chrome Desktop only)

This page can rewrite itself inside a ServiceWorker behind this website. Try adding a couple of DOM nodes using a button below, then reload the page. Notice that the page arrives with new DOM nodes to the browser, thus there is no blinking / initial pause. If you inspect the fetched page's source in the Network tab you will see the updated HTML on each reload.

The self-rewriting portion is below

some content here already
Controls

How to use

Load the page first - I assume you have already done this, if you are reading this

Click "Add ..." button several times, you should see new items in the list.

Reload the page - you should see the items you have added right away - they are part of the loaded page, not added dynamically later. You can confirm this by inspecting the downloaded page's source or the contents of the page in the Network tab.

Note

Open browser console to see any messages / errors

================================================ FILE: package.json ================================================ { "name": "bottle-service", "description": "Instant web applications restored from ServiceWorker cache", "main": "index.js", "version": "0.0.0-semantic-release", "files": [ "dist/*.js" ], "scripts": { "test": "npm run lint", "lint": "standard src/*.js", "semantic-release": "semantic-release pre && npm publish && semantic-release post", "build": "cp src/bottle.js dist/ && npm run webpack", "webpack": "webpack", "deploy": "grunty grunt-gh-pages gh-pages deploy.json", "start": "http-server dist -c-1", "dev-start": "http-server dist -c-1 -p 3006", "commit": "commit-wizard", "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", "issues": "git-issues" }, "repository": { "type": "git", "url": "https://github.com/bahmutov/bottle-service.git" }, "keywords": [ "service", "worker", "serviceWorker", "cache", "hydrate", "web", "performance" ], "author": "Gleb Bahmutov ", "license": "MIT", "bugs": { "url": "https://github.com/bahmutov/bottle-service/issues" }, "homepage": "https://github.com/bahmutov/bottle-service#readme", "dependencies": { "caches-storage": "1.1.0", "http-server": "0.8.5" }, "devDependencies": { "git-issues": "1.2.0", "grunt": "0.4.5", "grunt-gh-pages": "1.0.0", "grunty": "0.2.0", "pre-git": "3.1.1", "semantic-release": "^4.3.5", "standard": "5.4.1", "webpack": "1.12.9" }, "private": false, "config": { "pre-git": { "commit-msg": [ "simple" ], "pre-commit": [ "npm run lint" ], "pre-push": [ "npm run size" ], "post-commit": [], "post-merge": [] } } } ================================================ FILE: src/bottle-service.js ================================================ 'use strict' /* This is ServiceWorker code */ /* global self, Response, Promise, location, fetch */ var myName = 'bottle-service' console.log(myName, 'startup') function dataStore () { var cachesStorage = require('caches-storage') return cachesStorage(myName) } self.addEventListener('install', function (event) { console.log(myName, 'installed') }) self.addEventListener('activate', function () { console.log(myName, 'activated') }) var baseHref = location.href.substr(0, location.href.indexOf('bottle-service.js')) function isIndexPageRequest (event) { return event && event.request && event.request.url === baseHref } self.addEventListener('fetch', function (event) { if (!isIndexPageRequest(event)) { return fetch(event.request) } console.log(myName, 'fetching index page', event.request.url) event.respondWith( fetch(event.request) .then(function (response) { return dataStore() .then(function (store) { return store.getItem('contents') }) .then(function (contents) { if (contents && contents.html && contents.id) { console.log('fetched latest', response.url, 'need to update') console.log('element "%s" with html "%s" ...', contents.id, contents.html.substr(0, 15)) var copy = response.clone() return copy.text().then(function (pageHtml) { console.log('inserting our html') // HACK using id in the CLOSING TAG to find fragment var toReplaceStart = '
' var toReplaceFinish = '
' var startIndex = pageHtml.indexOf(toReplaceStart) var finishIndex = pageHtml.indexOf(toReplaceFinish) if (startIndex !== -1 && finishIndex > startIndex) { console.log('found fragment') pageHtml = pageHtml.substr(0, startIndex + toReplaceStart.length) + '\n' + contents.html + '\n' + pageHtml.substr(finishIndex) } // console.log('page html') // console.log(pageHtml) var responseOptions = { status: 200, headers: { 'Content-Type': 'text/html charset=UTF-8' } } return new Response(pageHtml, responseOptions) }) } else { return response } }, function notFound () { return response }) }) ) }) // use window.navigator.serviceWorker.controller.postMessage('hi') // to communicate with this service worker self.onmessage = function onMessage (event) { console.log('message to bottle-service worker cmd', event.data && event.data.cmd) // TODO how to use application name? dataStore().then(function (store) { switch (event.data.cmd) { case 'print': { return store.getItem('contents') .then(function (res) { console.log('bottle service has contents') console.log(res) }) } case 'clear': { console.log('clearing the bottle') return store.setItem('contents', {}) } case 'refill': { return store.setItem('contents', { html: event.data.html, id: event.data.id }).then(function () { console.log('saved new html for id', event.data.id) }) } default: { console.error(myName, 'unknown command', event.data) } } }) } ================================================ FILE: src/bottle.js ================================================ !(function startBottleService (root) { 'use strict' if (!root.navigator) { console.error('Missing navigator') return } if (!root.navigator.serviceWorker) { console.error('Sorry, not ServiceWorker feature, maybe enable it?') console.error('http://jakearchibald.com/2014/using-serviceworker-today/') return } // TODO package lazy-ass and check-more-types using webpack function toString (x) { return typeof x === 'string' ? x : JSON.stringify(x) } function la (condition) { if (!condition) { var args = Array.prototype.slice.call(arguments, 1) .map(toString) throw new Error(args.join(' ')) } } function isFunction (f) { return typeof f === 'function' } function getCurrentScriptFolder () { var scriptEls = document.getElementsByTagName('script') var thisScriptEl = scriptEls[scriptEls.length - 1] var scriptPath = thisScriptEl.src return scriptPath.substr(0, scriptPath.lastIndexOf('/') + 1) } var serviceScriptUrl = getCurrentScriptFolder() + 'bottle-service.js' // assume we are running at /pathname var scope = window.location.pathname var send = function mockSend () { console.error('Bottle service not initialized yet') } function registeredWorker (registration) { la(registration, 'missing service worker registration') la(registration.active, 'missing active service worker') la(isFunction(registration.active.postMessage), 'expected function postMessage to communicate with service worker') send = registration.active.postMessage.bind(registration.active) var info = '\nbottle-service - .\n' + 'I have a valid service-turtle, use `bottleService` object to update cached page' console.log(info) registration.active.onmessage = function messageFromServiceWorker (e) { console.log('received message from the service worker', e) } } function onError (err) { if (err.message.indexOf('missing active') !== -1) { // the service worker is installed window.location.reload() } else { console.error('bottle service error', err) } } root.navigator.serviceWorker.register(serviceScriptUrl, { scope: scope }) .then(registeredWorker) .catch(onError) root.bottleService = { refill: function refill (applicationName, id) { console.log('bottle-service: html for app %s element %s', applicationName, id) var el = document.getElementById(id) la(el, 'could not find element with id', id) var html = el.innerHTML.trim() send({ cmd: 'refill', html: html, name: applicationName, id: id }) }, print: function print (applicationName) { send({ cmd: 'print', name: applicationName }) }, clear: function clear (applicationName) { send({ cmd: 'clear', name: applicationName }) } } }(window)) ================================================ FILE: webpack.config.js ================================================ module.exports = { output: { path: './dist', filename: 'bottle-service.js' }, entry: { library: './src/bottle-service' } }