improve (pwa): follow the lighthouse tool recommendations for PWAs

This commit is contained in:
Mickael Kerjean 2019-11-15 00:12:35 +11:00
parent 8c1952d59c
commit 8deedcd916
11 changed files with 160 additions and 174 deletions

View file

@ -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;
}

View file

@ -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>
); );
} }
} }

View file

@ -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;
} }
} }

View file

@ -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);
});
});
}

View file

@ -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();
} }
} }

View file

@ -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>

View file

@ -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>

View file

@ -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
View 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){}
}

View file

@ -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 }

View file

@ -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/