diff --git a/Cargo.toml b/Cargo.toml index 7b6a08a39..377ff37b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "packages/signals", "packages/hot-reload", "docs/guide", + "examples/PWA-example", ] # This is a "virtual package" diff --git a/examples/PWA-example/Cargo.toml b/examples/PWA-example/Cargo.toml new file mode 100644 index 000000000..f11d80713 --- /dev/null +++ b/examples/PWA-example/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dioxus-pwa-example" +version = "0.1.0" +authors = ["Antonio Curavalea "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus = { path = "../../packages/dioxus", version = "^0.3.0"} +dioxus-web = { path = "../../packages/web", version = "^0.3.0"} + +log = "0.4.6" + +# WebAssembly Debug +wasm-logger = "0.2.0" +console_error_panic_hook = "0.1.7" diff --git a/examples/PWA-example/Dioxus.toml b/examples/PWA-example/Dioxus.toml new file mode 100644 index 000000000..e3a472520 --- /dev/null +++ b/examples/PWA-example/Dioxus.toml @@ -0,0 +1,42 @@ +[application] + +# App (Project) Name +name = "dioxus-pwa-example" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "dioxus | ⛺" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = [] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/examples/PWA-example/LICENSE b/examples/PWA-example/LICENSE new file mode 100644 index 000000000..bcdd828e9 --- /dev/null +++ b/examples/PWA-example/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Dioxus + +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. diff --git a/examples/PWA-example/README.md b/examples/PWA-example/README.md new file mode 100644 index 000000000..dfd986302 --- /dev/null +++ b/examples/PWA-example/README.md @@ -0,0 +1,44 @@ +# Dioxus PWA example + +This is a basic example of a progressive web app (PWA) using Dioxus and Dioxus CLI. +Currently PWA functionality requires the use of a service worker and manifest file, so this isn't 100% Rust yet. + +It is also very much usable as a template for your projects, if you're aiming to create a PWA. + +## Try the example + +Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`). + +You can run `dioxus serve` in this directory to start the web server locally, or run +`dioxus build --release` to build the project so you can deploy it on a separate web-server. + +## Project Structure +``` +├── Cargo.toml +├── Dioxus.toml +├── index.html // Custom HTML is needed for this, to load the SW and manifest. +├── LICENSE +├── public +│ ├── favicon.ico +│ ├── logo_192.png +│ ├── logo_512.png +│ ├── manifest.json // The manifest file - edit this as you need to. +│ └── sw.js // The service worker - you must edit this for actual projects. +├── README.md +└── src + └── main.rs +``` + +## Resources + +If you're just getting started with PWAs, here are some useful resources: + +* [PWABuilder docs](https://docs.pwabuilder.com/#/) +* [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) + +For service worker scripting (in JavaScript): + +* [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro) +* [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers) + +If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though. \ No newline at end of file diff --git a/examples/PWA-example/index.html b/examples/PWA-example/index.html new file mode 100644 index 000000000..44bfa59cc --- /dev/null +++ b/examples/PWA-example/index.html @@ -0,0 +1,30 @@ + + + + {app_title} + + + + + + {style_include} + + +
+ + {script_include} + + \ No newline at end of file diff --git a/examples/PWA-example/public/favicon.ico b/examples/PWA-example/public/favicon.ico new file mode 100644 index 000000000..b11015bdb Binary files /dev/null and b/examples/PWA-example/public/favicon.ico differ diff --git a/examples/PWA-example/public/logo_192.png b/examples/PWA-example/public/logo_192.png new file mode 100644 index 000000000..112eb5ee4 Binary files /dev/null and b/examples/PWA-example/public/logo_192.png differ diff --git a/examples/PWA-example/public/logo_512.png b/examples/PWA-example/public/logo_512.png new file mode 100644 index 000000000..43129772a Binary files /dev/null and b/examples/PWA-example/public/logo_512.png differ diff --git a/examples/PWA-example/public/manifest.json b/examples/PWA-example/public/manifest.json new file mode 100644 index 000000000..ef542899d --- /dev/null +++ b/examples/PWA-example/public/manifest.json @@ -0,0 +1,34 @@ +{ + "name": "Dioxus", + "icons": [ + { + "src": "logo_192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo_512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any" + }, + { + "src": "logo_512.png", + "type": "image/png", + "sizes": "any", + "purpose": "any" + } + ], + "start_url": "/", + "id": "/", + "display": "standalone", + "display_override": ["window-control-overlay", "standalone"], + "scope": "/", + "theme_color": "#000000", + "background_color": "#ffffff", + "short_name": "Dioxus", + "description": "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.", + "dir": "ltr", + "lang": "en", + "orientation": "portrait" +} \ No newline at end of file diff --git a/examples/PWA-example/public/sw.js b/examples/PWA-example/public/sw.js new file mode 100644 index 000000000..162d37f4a --- /dev/null +++ b/examples/PWA-example/public/sw.js @@ -0,0 +1,198 @@ +"use strict"; + +//console.log('WORKER: executing.'); + +/* A version number is useful when updating the worker logic, + allowing you to remove outdated cache entries during the update. +*/ +var version = 'v1.0.0::'; + +/* These resources will be downloaded and cached by the service worker + during the installation process. If any resource fails to be downloaded, + then the service worker won't be installed either. +*/ +var offlineFundamentals = [ + // add here the files you want to cache + 'favicon.ico' +]; + +/* The install event fires when the service worker is first installed. + You can use this event to prepare the service worker to be able to serve + files while visitors are offline. +*/ +self.addEventListener("install", function (event) { + //console.log('WORKER: install event in progress.'); + /* Using event.waitUntil(p) blocks the installation process on the provided + promise. If the promise is rejected, the service worker won't be installed. + */ + event.waitUntil( + /* The caches built-in is a promise-based API that helps you cache responses, + as well as finding and deleting them. + */ + caches + /* You can open a cache by name, and this method returns a promise. We use + a versioned cache name here so that we can remove old cache entries in + one fell swoop later, when phasing out an older service worker. + */ + .open(version + 'fundamentals') + .then(function (cache) { + /* After the cache is opened, we can fill it with the offline fundamentals. + The method below will add all resources in `offlineFundamentals` to the + cache, after making requests for them. + */ + return cache.addAll(offlineFundamentals); + }) + .then(function () { + //console.log('WORKER: install completed'); + }) + ); +}); + +/* The fetch event fires whenever a page controlled by this service worker requests + a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it + comprehends even the request for the HTML page on first load, as well as JS and + CSS resources, fonts, any images, etc. +*/ +self.addEventListener("fetch", function (event) { + //console.log('WORKER: fetch event in progress.'); + + /* We should only cache GET requests, and deal with the rest of method in the + client-side, by handling failed POST,PUT,PATCH,etc. requests. + */ + if (event.request.method !== 'GET') { + /* If we don't block the event as shown below, then the request will go to + the network as usual. + */ + //console.log('WORKER: fetch event ignored.', event.request.method, event.request.url); + return; + } + /* Similar to event.waitUntil in that it blocks the fetch event on a promise. + Fulfillment result will be used as the response, and rejection will end in a + HTTP response indicating failure. + */ + event.respondWith( + caches + /* This method returns a promise that resolves to a cache entry matching + the request. Once the promise is settled, we can then provide a response + to the fetch request. + */ + .match(event.request) + .then(function (cached) { + /* Even if the response is in our cache, we go to the network as well. + This pattern is known for producing "eventually fresh" responses, + where we return cached responses immediately, and meanwhile pull + a network response and store that in the cache. + + Read more: + https://ponyfoo.com/articles/progressive-networking-serviceworker + */ + var networked = fetch(event.request) + // We handle the network request with success and failure scenarios. + .then(fetchedFromNetwork, unableToResolve) + // We should catch errors on the fetchedFromNetwork handler as well. + .catch(unableToResolve); + + /* We return the cached response immediately if there is one, and fall + back to waiting on the network as usual. + */ + //console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url); + return cached || networked; + + function fetchedFromNetwork(response) { + /* We copy the response before replying to the network request. + This is the response that will be stored on the ServiceWorker cache. + */ + var cacheCopy = response.clone(); + + //console.log('WORKER: fetch response from network.', event.request.url); + + caches + // We open a cache to store the response for this request. + .open(version + 'pages') + .then(function add(cache) { + /* We store the response for this request. It'll later become + available to caches.match(event.request) calls, when looking + for cached responses. + */ + cache.put(event.request, cacheCopy); + }) + .then(function () { + //console.log('WORKER: fetch response stored in cache.', event.request.url); + }); + + // Return the response so that the promise is settled in fulfillment. + return response; + } + + /* When this method is called, it means we were unable to produce a response + from either the cache or the network. This is our opportunity to produce + a meaningful response even when all else fails. It's the last chance, so + you probably want to display a "Service Unavailable" view or a generic + error response. + */ + function unableToResolve() { + /* There's a couple of things we can do here. + - Test the Accept header and then return one of the `offlineFundamentals` + e.g: `return caches.match('/some/cached/image.png')` + - You should also consider the origin. It's easier to decide what + "unavailable" means for requests against your origins than for requests + against a third party, such as an ad provider. + - Generate a Response programmaticaly, as shown below, and return that. + */ + + //console.log('WORKER: fetch request failed in both cache and network.'); + + /* Here we're creating a response programmatically. The first parameter is the + response body, and the second one defines the options for the response. + */ + return new Response('

Service Unavailable

', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); + } + }) + ); +}); + +/* The activate event fires after a service worker has been successfully installed. + It is most useful when phasing out an older version of a service worker, as at + this point you know that the new worker was installed correctly. In this example, + we delete old caches that don't match the version in the worker we just finished + installing. +*/ +self.addEventListener("activate", function (event) { + /* Just like with the install event, event.waitUntil blocks activate on a promise. + Activation will fail unless the promise is fulfilled. + */ + //console.log('WORKER: activate event in progress.'); + + event.waitUntil( + caches + /* This method returns a promise which will resolve to an array of available + cache keys. + */ + .keys() + .then(function (keys) { + // We return a promise that settles when all outdated caches are deleted. + return Promise.all( + keys + .filter(function (key) { + // Filter by keys that don't start with the latest version prefix. + return !key.startsWith(version); + }) + .map(function (key) { + /* Return a promise that's fulfilled + when each outdated cache is deleted. + */ + return caches.delete(key); + }) + ); + }) + .then(function () { + //console.log('WORKER: activate completed.'); + }) + ); +}); diff --git a/examples/PWA-example/src/main.rs b/examples/PWA-example/src/main.rs new file mode 100644 index 000000000..4c09a9bc5 --- /dev/null +++ b/examples/PWA-example/src/main.rs @@ -0,0 +1,20 @@ +use dioxus::prelude::*; + +fn main() { + // init debug tool for WebAssembly + wasm_logger::init(wasm_logger::Config::default()); + console_error_panic_hook::set_once(); + + dioxus_web::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! ( + div { + style: "text-align: center;", + h1 { "🌗 Dioxus 🚀" } + h3 { "Frontend that scales." } + p { "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust." } + } + )) +} diff --git a/examples/svg.rs b/examples/svg.rs index 829a559ad..329cbe6ae 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { onclick: move |_| { use rand::Rng; let mut rng = rand::thread_rng(); - val.set(rng.gen_range(1..6)); + val.set(rng.gen_range(1..=6)); } } }