mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
improve (pwa): follow the lighthouse tool recommendations for PWAs
This commit is contained in:
parent
8c1952d59c
commit
8deedcd916
11 changed files with 160 additions and 174 deletions
|
|
@ -185,3 +185,12 @@ select:-moz-focusring {
|
||||||
.pointer {
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden{
|
||||||
|
position:absolute;
|
||||||
|
left:-10000px;
|
||||||
|
top:auto;
|
||||||
|
width:1px;
|
||||||
|
height:1px;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,19 +76,19 @@ BreadCrumb.propTypes = {
|
||||||
const BreadCrumbContainer = (props) => {
|
const BreadCrumbContainer = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<ul>
|
<div className="ul">
|
||||||
{props.children}
|
{props.children}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const Logout = (props) => {
|
const Logout = (props) => {
|
||||||
return (
|
return (
|
||||||
<li className="component_logout">
|
<div className="li component_logout">
|
||||||
<Link to="/logout">
|
<Link to="/logout">
|
||||||
<Icon name="power"/>
|
<Icon name="power"/>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +144,7 @@ export class PathElementWrapper extends React.Component {
|
||||||
href += location.search;
|
href += location.search;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={className}>
|
<div className={"li "+className}>
|
||||||
<NgIf cond={this.props.isLast === false}>
|
<NgIf cond={this.props.isLast === false}>
|
||||||
<Link to={href} className="label">
|
<Link to={href} className="label">
|
||||||
<NgIf cond={this.props.path.minify !== true}>
|
<NgIf cond={this.props.path.minify !== true}>
|
||||||
|
|
@ -163,7 +163,7 @@ export class PathElementWrapper extends React.Component {
|
||||||
{this.limitSize(this.props.path.label)}
|
{this.limitSize(this.props.path.label)}
|
||||||
<Saving needSaving={this.props.needSaving} />
|
<Saving needSaving={this.props.needSaving} />
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
|
||||||
ul{
|
.ul{
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
> span{display: block; padding: 7px 0;}
|
> span{display: block; padding: 7px 0;}
|
||||||
div, li{
|
div, .li{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,21 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||||
.then(render);
|
.then(render);
|
||||||
}
|
}
|
||||||
return removeLoader().then(render);
|
return removeLoader().then(render);
|
||||||
|
}).catch((e) => {
|
||||||
|
const msg = "Couldn't boot Filestash";
|
||||||
|
Log.report(msg, location.href);
|
||||||
|
return removeLoaderWithAnimation()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.onerror = function (msg, url, lineNo, colNo, error) {
|
window.onerror = function (msg, url, lineNo, colNo, error) {
|
||||||
Log.report(msg, url, lineNo, colNo, error)
|
Log.report(msg, url, lineNo, colNo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", function() {
|
||||||
|
navigator.serviceWorker.register("/sw_cache.js").catch(function(err){
|
||||||
|
console.error("ServiceWorker registration failed:", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,15 @@ class LogManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
report(msg, link, lineNo, columnNo, error){
|
report(msg, link, lineNo, columnNo, error){
|
||||||
|
if(navigator.onLine === false) return Promise.resolve();
|
||||||
let url = "/report?";
|
let url = "/report?";
|
||||||
url += "url="+encodeURIComponent(location.href)+"&";
|
url += "url="+encodeURIComponent(location.href)+"&";
|
||||||
url += "error="+encodeURIComponent(error.message)+"&";
|
|
||||||
url += "msg="+encodeURIComponent(msg)+"&";
|
url += "msg="+encodeURIComponent(msg)+"&";
|
||||||
url += "from="+encodeURIComponent(link)+"&";
|
url += "from="+encodeURIComponent(link)+"&";
|
||||||
url += "from.lineNo="+lineNo+"&";
|
url += "from.lineNo="+lineNo+"&";
|
||||||
url += "from.columnNo="+columnNo;
|
url += "from.columnNo="+columnNo;
|
||||||
return http_post(url);
|
if(error) url += "error="+encodeURIComponent(error.message)+"&";
|
||||||
|
return http_post(url).catch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ export class Form extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<label className={"no-select input_type_" + props.params["type"]}>
|
<label htmlFor={props.params["id"]} className={"no-select input_type_" + props.params["type"]}>
|
||||||
<div>
|
<div>
|
||||||
{ $input }
|
{ $input }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ export class Submenu extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
<NgIf cond={this.state.search_input_visible !== null} type="inline">
|
<NgIf cond={this.state.search_input_visible !== null} type="inline">
|
||||||
<input ref="$input" onBlur={this.closeIfEmpty.bind(this, false)} style={{"width": this.state.search_input_visible ? "180px" : "0px"}} value={this.state.search_keyword} onChange={(e) => this.onSearchKeypress(e.target.value, true)} type="text" id="search" placeholder="search" name="search" autoComplete="off" />
|
<input ref="$input" onBlur={this.closeIfEmpty.bind(this, false)} style={{"width": this.state.search_input_visible ? "180px" : "0px"}} value={this.state.search_keyword} onChange={(e) => this.onSearchKeypress(e.target.value, true)} type="text" id="search" placeholder="search" name="search" autoComplete="off" />
|
||||||
|
<label htmlFor="search" className="hidden">search</label>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
</form>
|
</form>
|
||||||
</NgIf>
|
</NgIf>
|
||||||
|
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
const CACHE_NAME = 'v0.3';
|
|
||||||
const DELAY_BEFORE_SENDING_CACHE = 2000;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Control everything going through the wire, applying different
|
|
||||||
* strategy for caching, fetching resources
|
|
||||||
*/
|
|
||||||
self.addEventListener('fetch', function(event){
|
|
||||||
if(is_a_ressource(event.request)){
|
|
||||||
return event.respondWith(smartCacheStrategy(event.request));
|
|
||||||
}else if(is_an_api_call(event.request)){
|
|
||||||
return event;
|
|
||||||
}else if(is_an_index(event.request)){
|
|
||||||
return event.respondWith(smartCacheStrategy(event.request))
|
|
||||||
}else{
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When a new service worker is coming in, we need to do a bit of
|
|
||||||
* cleanup to get rid of the rotten cache
|
|
||||||
*/
|
|
||||||
self.addEventListener('activate', function(event){
|
|
||||||
vacuum(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When a newly installed service worker is coming in, we want to use it
|
|
||||||
* straight away (make it active). By default it would be in a "waiting state"
|
|
||||||
*/
|
|
||||||
self.addEventListener('install', function(event){
|
|
||||||
if (self.skipWaiting) { self.skipWaiting(); }
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////
|
|
||||||
// Test if what's the request is about
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
function is_a_ressource(request){
|
|
||||||
return ['css', 'js', 'img', 'logo', 'manifest.json', 'favicon.ico'].indexOf(_pathname(request)[0]) >= 0 ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_an_api_call(request){
|
|
||||||
return _pathname(request)[0] === 'api' ? true : false;
|
|
||||||
}
|
|
||||||
function is_an_index(request){
|
|
||||||
return ['login', 'files', 'view', 'logout'].indexOf(_pathname(request)[0]) >= 0? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////
|
|
||||||
// HELPERS
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
function vacuum(event){
|
|
||||||
return event.waitUntil(
|
|
||||||
caches.keys().then(function(cachesName){
|
|
||||||
return Promise.all(cachesName.map(function(cacheName){
|
|
||||||
if(cacheName !== CACHE_NAME){
|
|
||||||
return caches.delete(cacheName);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pathname(request){
|
|
||||||
return request.url.replace(/^http[s]?:\/\/[^\/]*\//, '').split('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Loading Strategy:
|
|
||||||
* use what's in cache first to make things faster but refresh it as we receive a response
|
|
||||||
*/
|
|
||||||
function smartCacheStrategy(request){
|
|
||||||
return caches.open(CACHE_NAME).then(function(cache){
|
|
||||||
return cache.match(request)
|
|
||||||
.then(function(response){
|
|
||||||
if(response && response.status === 200){
|
|
||||||
fetchAndCache(request).catch(nil);
|
|
||||||
response.headers.append('Content-Stale', 'yes');
|
|
||||||
return response;
|
|
||||||
}else{
|
|
||||||
return Promise.reject("OUPS");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
return fetchAndCache(request);
|
|
||||||
});
|
|
||||||
}).catch(() => request);
|
|
||||||
|
|
||||||
|
|
||||||
function fetchAndCache(_request){
|
|
||||||
// A request is a stream and can only be consumed once. Since we are consuming this
|
|
||||||
// once by cache and once by the browser for fetch, we need to clone the response as
|
|
||||||
// seen on https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
|
|
||||||
return fetch(_request.clone && _request.clone() || _request)
|
|
||||||
.then(function(response){
|
|
||||||
if(!response || response.status !== 200){ return response; }
|
|
||||||
|
|
||||||
// A response is a stream and can only because we want the browser to consume the
|
|
||||||
// response as well as the cache consuming the response, we need to clone it
|
|
||||||
const responseClone = response.clone();
|
|
||||||
caches.open(CACHE_NAME).then(function(cache){
|
|
||||||
cache.put(_request, responseClone);
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}).catch(() => _request);
|
|
||||||
}
|
|
||||||
function nil(e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Broken as I didn't understood the Promise.race behavior correctly first ...
|
|
||||||
// if nothing in cache it just brakes
|
|
||||||
function networkFirstStrategy(request){
|
|
||||||
return new Promise(function(done, error){
|
|
||||||
cache(request.clone && request.clone() || request).then(function(response){
|
|
||||||
if(!response || !response.headers) return;
|
|
||||||
response.headers.append('Content-Stale', 'yes');
|
|
||||||
done(response);
|
|
||||||
});
|
|
||||||
network(request.clone && request.clone() || request)
|
|
||||||
.then(done)
|
|
||||||
.catch(error);
|
|
||||||
}).catch(() => request);
|
|
||||||
|
|
||||||
function network(request){
|
|
||||||
return fetch(request)
|
|
||||||
.then(function(response){
|
|
||||||
if(!response || response.status !== 200) return Promise.reject(response);
|
|
||||||
|
|
||||||
const responseClone = response.clone();
|
|
||||||
caches.open(CACHE_NAME).then(function(cache){
|
|
||||||
cache.put(request, responseClone);
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
})
|
|
||||||
.catch(function(){
|
|
||||||
return cache(request.clone && request.clone() || request)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function cache(_request){
|
|
||||||
return timeout()
|
|
||||||
.then(function(){ return caches.open(CACHE_NAME); })
|
|
||||||
.then(function(_cache){ return _cache.match(_request); });
|
|
||||||
|
|
||||||
function timeout(){
|
|
||||||
return new Promise(function(done) {
|
|
||||||
setTimeout(function() {
|
|
||||||
done();
|
|
||||||
}, DELAY_BEFORE_SENDING_CACHE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
123
client/worker/sw_cache.js
Normal file
123
client/worker/sw_cache.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
const CACHE_NAME = "v0.3";
|
||||||
|
const DELAY_BEFORE_SENDING_CACHE = 2000;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Control everything going through the wire, applying different
|
||||||
|
* strategy for caching, fetching resources
|
||||||
|
*/
|
||||||
|
self.addEventListener("fetch", function(event){
|
||||||
|
if(is_a_ressource(event.request)){
|
||||||
|
return event.respondWith(cacheFirstStrategy(event));
|
||||||
|
}else if(is_an_api_call(event.request)){
|
||||||
|
return event;
|
||||||
|
}else if(is_an_index(event.request)){
|
||||||
|
return event.respondWith(cacheFirstStrategy(event));
|
||||||
|
}else{
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When a new service worker is coming in, we need to do a bit of
|
||||||
|
* cleanup to get rid of the rotten cache
|
||||||
|
*/
|
||||||
|
self.addEventListener("activate", function(event){
|
||||||
|
vacuum(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("error", function(err){
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When a newly installed service worker is coming in, we want to use it
|
||||||
|
* straight away (make it active). By default it would be in a "waiting state"
|
||||||
|
*/
|
||||||
|
self.addEventListener("install", function(event){
|
||||||
|
caches.open(CACHE_NAME).then(function(cache) {
|
||||||
|
return cache.addAll([
|
||||||
|
"/",
|
||||||
|
"/api/config"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (self.skipWaiting) { self.skipWaiting(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function is_a_ressource(request){
|
||||||
|
const p = _pathname(request);
|
||||||
|
if(["assets", "manifest.json", "favicon.ico"].indexOf(p[0]) !== -1){
|
||||||
|
return true;
|
||||||
|
} else if(p[0] === "api" && (p[1] === "config")){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_an_api_call(request){
|
||||||
|
return _pathname(request)[0] === "api" ? true : false;
|
||||||
|
}
|
||||||
|
function is_an_index(request){
|
||||||
|
return ["files", "view", "login", "logout", ""].indexOf(_pathname(request)[0]) >= 0? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// HELPERS
|
||||||
|
////////////////////////////////////////
|
||||||
|
|
||||||
|
function vacuum(event){
|
||||||
|
return event.waitUntil(
|
||||||
|
caches.keys().then(function(cachesName){
|
||||||
|
return Promise.all(cachesName.map(function(cacheName){
|
||||||
|
if(cacheName !== CACHE_NAME){
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pathname(request){
|
||||||
|
return request.url.replace(/^http[s]?:\/\/[^\/]*\//, "").split("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* strategy is cache first:
|
||||||
|
* 1. use whatever is in the cache
|
||||||
|
* 2. perform the network call to update the cache
|
||||||
|
*/
|
||||||
|
function cacheFirstStrategy(event){
|
||||||
|
return caches.open(CACHE_NAME).then(function(cache){
|
||||||
|
return cache.match(event.request).then(function(response){
|
||||||
|
if(!response){
|
||||||
|
return fetchAndCache(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAndCache(event).catch(nil);
|
||||||
|
response.headers.append("Content-Stale", "yes");
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchAndCache(event){
|
||||||
|
// A request is a stream and can only be consumed once. Since we are consuming this
|
||||||
|
// once by cache and once by the browser for fetch, we need to clone the response as
|
||||||
|
// seen on https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
|
||||||
|
return fetch(event.request)
|
||||||
|
.then(function(response){
|
||||||
|
if(!response || response.status !== 200){ return response; }
|
||||||
|
|
||||||
|
// A response is a stream and can only because we want the browser to consume the
|
||||||
|
// response as well as the cache consuming the response, we need to clone it
|
||||||
|
const responseClone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then(function(cache){
|
||||||
|
cache.put(event.request, responseClone);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function nil(e){}
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,7 @@ func Init(a *App) {
|
||||||
middlewares = []Middleware{ StaticHeaders }
|
middlewares = []Middleware{ StaticHeaders }
|
||||||
r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler(FILE_ASSETS), middlewares, *a))).Methods("GET")
|
r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler(FILE_ASSETS), middlewares, *a))).Methods("GET")
|
||||||
r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler(FILE_ASSETS + "/assets/logo/"), middlewares, *a)).Methods("GET")
|
r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler(FILE_ASSETS + "/assets/logo/"), middlewares, *a)).Methods("GET")
|
||||||
|
r.HandleFunc("/sw_cache.js", NewMiddlewareChain(StaticHandler(FILE_ASSETS + "/assets/worker/"), middlewares, *a)).Methods("GET")
|
||||||
|
|
||||||
// Other endpoints
|
// Other endpoints
|
||||||
middlewares = []Middleware{ ApiHeaders }
|
middlewares = []Middleware{ ApiHeaders }
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ let config = {
|
||||||
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js"
|
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js"
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [{
|
rules: [
|
||||||
|
{
|
||||||
test: path.join(__dirname, 'client'),
|
test: path.join(__dirname, 'client'),
|
||||||
use: ['babel-loader'],
|
use: ['babel-loader'],
|
||||||
exclude: /node_modules/
|
exclude: /node_modules/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue