Anders är en webbutvecklare och hårdrockare som gillar brädspel, kaffe och öl.

Baslyft, bärs och burgare - min första PWA

Jag har byggt min första Progressiva Web app: en webbplats som erbjuder okompromissad offline-funktionalitet och som kan installeras till hemskärmar på telefoner och andra enheter som stödjer det.

Det är en träningsapp, med dagboksfunktion och några hjälpmedel för att lyckas med styrketräning i gymet. Den ligger hostad på Github pages: B3: Baslyft, bärs och burgare.

Detta blogginlägg är en utvärdering av hur jag planerade projektet, vad jag ville uppnå med det och resultatet.

Disclaimer! Namnet på appen, och även några av dess funktioner, är inspirerade från min favorit bland alla poddar och sajter för styrketräning - Styrkelabbet. Deras textinnehåll är sakliga, mycket källkritiska och motiverande. Deras träningsprogram fick mig att, efter flera års försök, slutligen få in rutiner för att träna för en starkare kropp och ett friskare liv. Att träna är äntligen kul.

Därmed, kudos till Styrkelabbet! Deras app kan jag rekommendera varmt, då den vida överstiger denna app i funktionalitet och smarta funktioner. 🤗

Önskvärda funktioner

Bara för att ha något att sträva mot tog jag fram en lista som definierar en acceptabel app för syftet.

  • Rent, enkelt gränssnitt med kloka standardbeteenden snarare än konfigurationsmöjligheter.
  • En lista över utförda pass, i omvänd kronologisk ordning.
  • En 1RM-beräknare.
  • Möjlighet att döpa pass.
  • Lägg till set (övningsnamn, vikt och repetitioner) med små, smarta funktioner för ökad produktivitet.
  • Möjlighet att definiera ett set som uppvärmningsset.
  • Braiga nyckeltal: antalet lyfta ton per pass, totala antalet set och totala antalet repetitioner.
  • Får gärna fungera i splitscreen-läge (så att tidtagarurets app kan ligga bredvid).

Tekniska mål

När jag såg Jake Archibalds Google I/O-presentation av offline-first Web Apps 2016 blev jag riktigt nyfiken på Service Workers, som på den tiden var hipster-prylar. Ända sedan jag läste Going offline av Jeremy Keith 2018 visste jag att detta är något som kommer att bli en grej.

I och med mitt avancemang mot React i år var jag inte svårövertalad att hitta på något kul. Jag beslöt mig för att sätta upp litet tekniska projektmål.

Metod och arbetsprocess

Jag ägnade först en stund åt att sätta upp ett app-skal med React, Redux, Redux-Sagas (bara för att ha det på plats) och få allt detta att bundlas med Parcel.

Därefter skrev jag ett Redux-middleware för att automatiskt synka state med localStorage vid uppdatering (via reducers) och initiering (sidladdning).

En sak jag därefter donade en stund med var att få rätt tangentbord att visas vid inmatning i formulär för nya set och nya pass, samt att navigering mellan formulär skulle fungera på ett bra sätt.

Någonstans här började jag inse att jag behövde konfigurera min kodeditor till att börja linta och automatiskt formatera min kod åt mig, snarare än att jag manuellt skulle göra det. [1]

Som ett första steg mot att bli PWA skrev jag här ett Web App Manifest, den första halvan av att bli Offline first.

Den andra halvan är att skriva en Service Worker. För detta projekt kände jag att det inte var värt att investera tid i att skriva en Service Worker från skratch, så jag valde istället att låta en Parcel-plugin generera en Service Worker åt mig.

Några reducers, actions, komponenter, hjälpfunktioner och containers[2] började falla på plats, så nu var det hög tid att skriva tester. Jag lade därför en stund på att få igång Jest, automatisera testförfarandet och att därefter skriva tester för den funktionalitet jag fått på plats.

Därifrån var all grund på plats, så härifrån fortsatte jag koda på funktioner i appen. Jag skrev fler tester, jag testade av och förbättrade design, jag utökade och snyggade till kodens struktur.

Resultatet

Vid version 1.4 hade jag stängt alla tekniska mål jag ville uppnå för projektet. 🥂 I detta läge konfigurerade jag Travis CI för att påbjuda fortsatt bra rutiner.

Jag har personligen använt appen på gymet i ett par månader, och har hunnit göra ett par releaser med förbättringar och nya funktioner utöver den initiella kravlistan.

4 saker värda att uppmärksamma i koden

För det första, hur man visar rätt tangentbord för nummerfält, samt hur man ger stöd för både kommatecken och punkt som skiljetecken:

<React.Fragment>
  <label htmlFor="weight">Vikt</label>
  <input
    id="weight"
    type="number"
    min="2.5"
    step="0.5"
    lang="en-150"
    pattern="[0-9]*"
    inputMode="decimal"
    value={value}
    onChange={handleOnChange}
  />
</React.Fragment>
  • inputMode är det som visar ett numeriskt tangentbord, istället för ett alfabetiskt tangentbord. Detta är något som 90% av världens alla webbshopar skulle kunna fundera på att lägga till i sina checkout-formulär.
  • lang="en-150" ser till att både kommatecken och punkt fungerar, då vi alla inte lever i Amerika. Det här skulle gärna fler större aktörer få stödja också (jag tittar på dig, Google docs!).

Det är också värt att visa att fältet är kontrollerat av React.

Bonus: jag tar med htmlFor också, för att JSX sannerligen inte är vackert ibland i de fall man valt att ärva från uråldriga DOM API-bestämmelser, vilket bara blir förvirrande när JSX kan antas vara ämnat att likna HTML. className är en annan favorit i samma kategori.

För det andra: hur fånigt enkelt det var att skapa persistent state med localStorage och redux middleware!

const ITEM_NAME = "BBB";

// synka ändrat state till localStorage
const localStorageMiddleware = ({ getState }) => {
  return next => action => {
    const result = next(action);
    localStorage.setItem(ITEM_NAME, JSON.stringify(getState()));
    return result;
  };
};

// ladda initiellt state i createStore() från localStorage
export const reHydrateStore = () => {
  if (localStorage.getItem(ITEM_NAME) !== null) {
    return JSON.parse(localStorage.getItem(ITEM_NAME));
  }
};

export default localStorageMiddleware;

Visst, jag hade ett tag en tanke på att låta Service Workern skriva till localStorage istället, och låta appen skicka ändringarna som fetch() med Redux-Sagas. Jag övergav dock denna idé då ovanstående lösning visade sig vara god nog för denna app.

För det tredje: HTML5-elementet <details>, samt HTML5-attributet form på buttons.

{ /* Details-elementet: accordion utan JavaScript! */ }
<details>
  <summary>Press Pasodoble 2.1 (2019-11-01)</summary>
  <table>
    { /* ... */ }
  </table>
</details>

{ /* form-attributet: knappen ligger utanför formuläret! */ }
<header>
  <h1>Lägg till nytt pass</h1>
  <button type="submit" form="new-workout">Starta</button>
</header>
<form id="new-workout">
  { /* ... */ }
</form>

<details> ger utvecklare en chans att toggla saker på ett trevligt sätt utan att blanda in JavaScript. Github använder det för alla kontextberoende menyer som kan klickas fram via en handle, exempelvis en knapp med ett kugghjul på. Det finns goda möjligheter att ändra utseende med CSS, men tillgänglighetsstödet är dessvärre inte helt optimal i dagsläget.

Attributet form på knappar är otroligt smidigt! Det tillåter knappar som gör saker med formuläret ligga utanför <form>, exempelvis i en fixerad toolbar som jag tillämpar i det här projektet.

Slutligen: att det med <datalist> går alldeles utmärkt att få autofill och förslag helt utan JavaScript.

<datalist id="exercises">
  <option>Bicepscurls</option>
  <option>Bänkpress</option>
  <option>Chins</option>
  <option>Knäböj</option>
  <option>Marklyft</option>
</datalist>
{ /* ... */ }
<input type="text" autocomplete="off" list="exercises" />

I webbläsare så föreslås här saker ur listan, snyggt integrerat native i telefonens tangentbord och i en lista med matchande val, i ett <select>-liknande beteende.

Avslutande ord

Appen utvecklas hela tiden, och jag klurar hela tiden på hur den ska vara mindre i vägen från det jag gör på gymet. Mest roligt är det att skruva på designen, då jag aldrig slutas att förundras över hur små nyanser kan skapa stor skillnad.

När en design har satt sig, och jag löst några tråkiga buggar, kanske några skärmdumpar eller videoklipp kommer upp. Kanske!


  1. I VS Code, som jag använde primärt för detta projekt, är detta supersmidigt med befintliga extensions. Jag installerade Prettier-tillägget, och aktiverade "format on save" för alla filer.

  2. Detta är React- och Redux-saker. Om detta är obekant, kika på Redux in 5 minutes för att få litet koll.