Motivation

Nach der Migration unserer Firmenhomepage auf Gridsome im Jahr 2019 wurde mir schnell klar, wie anfällig Refactorings sein können. Die einfache Umbenennung eines Feldes in einem Blogartikel würde das Layout einiger abhängiger Komponenten stillschweigend zerstören. Regex-Suchen und wiederholtes manuelles Testen waren die am häufigsten verwendeten Werkzeuge. Dies hätte mit einer Typüberprüfung zur Kompilierzeit leicht verhindert werden können.

Ich hatte bereits in früheren Projekten die Erfahrung gemacht, dass die Migration von Javascript zu Typescript das Vertrauen und die Bereitschaft Änderungen vorzunehmen stärkt. Wenn es doch nur eine einfache Lösung für Gridsome gäbe. In den offiziellen Starter-Projekten konnte ich keine zufriedenstellende Lösung finden und so schrieb ich diesen Blogpost. Mein Ziel war es, Gridsome, Typescript und die Composition-API zu kombinieren, damit du es auch ausprobieren kannst.

Ich werde auch einige Einschränkungen aufzeigen, mit denen wir noch konfrontiert sind.

Was ist Gridsome?

Für diejenigen, die Gridsome noch nicht kennen. Gridsome ist es einer von vielen statischen Website-Generatoren im Javascript / Vue.js Ökosystem. Es ist auch ein Jamstack-Framework, was bedeutet, dass es Javascript, APIs und Markup kombiniert, um schnelle und sichere Websites zu erstellen. Falls du ein React-Entwickler sind, kennst du vielleicht schon Gatsby, von dem Gridsome eine Menge Inspiration erhalten hat. Im Gegensatz zu anderen statischen Website-Generatoren verfügt Gridsome und Gatsby über eine GraphQL-Datenschicht, die einen einheitlichen Zugriff auf verschiedene Datenquellen ermöglicht.

Webseiten entwicklen mit Gridsome. Quelle: https://gridsome.org/docs/how-it-works/ Gridsome: How it works

Stellen dir vor, du erstellst eine Blogging-Website. Deine Artikel werden aus Markdown-Dateien und einem externen Content-Management-Systemen (CMS) geladen. Dank der vereinheitlichten GraphQL-API kannst du alle Inhalte gemeinsam abfragen und mit Vue-Vorlagen rendern.

Datenquellen in Gridsome sind als Plugins implementiert und auf der Projekt-Website verfügbar. Dieses Plugin-Ökosystem ist eine der herausragenden Eigenschaften von Gridsome. Um nur einige zu nennen, gibt es Plugins für Wordpress, Strapi, Contentful, MySQL-Datenbanken, generische REST- und GraphQL-Endpunkte und viele mehr.

Viele Entwickler speichern ihre Inhalte jedoch gerne in lokalen Markdown-Dateien, die zusammen mit dem restlichen Code versioniert werden. Normalerweise ist im Entwicklungsmodus jede Änderung sofort im Browser sichtbar, da der Entwicklungsserver hot-reloading. Sobald der Artikel fertig ist, kann man eine neue statische Version der Website entweder manuell oder über eine CI-Pipeline erstellen.

Statisch generierte Websites können einfach in die Cloud verlagert werden und über Cloud-Anbieter wie Netlify oder Vercel kostenlos gehosted werden.

Was Gridsome für Entwickler besonders attraktiv macht, sind die Optimierungen, die Gridsome hinter den Kulissen durchführt, damit die Seite so schnell wie möglich lädt, wie zum Beispiel Code-Splitting, Asset-Optimierung, progressives Laden von Bildern, Link Prefetching, sanfte Seitenübergänge und mehr.

So können sich Entwickler auf die Erstellung schneller, schöner Websites konzentrieren, ohne dass sie zu Leistungsexperten werden müssen.

Developer experience with Gridsome

Mit der offiziellen Dokumentation kann man in wenigen Minuten ein lauffähiges Starterprojekt erstellen. Zum Zeitpunkt des Verfassens dieses Artikels besteht der offizielle Weg jedoch darin, den Code mit Vue 2 und ES6 Javascript in den Dateien *.vue und *.js zu schreiben. Wenn das Projekt jedoch wächst und mehr Leute anfangen, zum Code beizutragen, wirst du die Wichtigkeit von Nachvollziehbarkeit, Autovervollständigung und Typüberprüfung erkennen.

Moderne IDEs wie Visual Studio Code oder Intellij IDEA führen bereits eine vernünftige statische Codeanalyse durch - sogar für Javascript-basierte Projekte. IntelliJ leitet z.B. automatisch Variablentypen anhand ihrer Verwendung her und extrahiert Informationen aus JSDoc-Annotationen und *.d.ts-Dateien (Typescript Typings).

All diese Informationen sind sehr hilfreich, aber die Autovervollständigung neigt dazu, aufgrund ungenauer Typisierung viele falsch-positive Ergebnisse zu liefern. Außerdem gibt es einen erheblichen Teil des Codes, den die IDE nicht überprüft.

Nach einigen Monaten der Entwicklung wurde uns klar, dass unser Javascript-Code von vornherein mit Typescript hätte geschrieben werden können, da wir bereits so viel JSDoc und Typings geschrieben hatten.

Typescript + Composition API

Das Hinzufügen von grundlegender Typescript-Unterstützung zu Gridsome ist nicht so schwer. Es gibt bereits ein Plugin dafür: gridsome-plugin-typescript. Unsere IDE benötigt jedoch eine Möglichkeit, Typen innerhalb von Vue-Komponenten automatisch zu ermitteln, um eine zuverlässige Autovervollständigung zu gewährleisten.

Wir wussten, dass dies mit der Composition API erreicht werden kann, die gut mit Typescript zusammenarbeitet. Nach einigen Experimenten haben wir gridsome-plugin-composition-api veröffentlicht, ein einfaches Plugin, das die Composition-API in Vue2-Komponenten aktiviert. (Betrachten Sie dies als einen Workaround, bis Gridsome auf Vue3 umsteigt).

Jetzt können wir Vue-Komponenten schreiben, die wie folgt aussehen:

<script lang="ts">
import {defineComponent, PropType, computed} from "@vue/composition-api";

interface Item { // could be imported from other module
  id: string
  title: string
}

export default defineComponent({
  props: {
    items: { type:Array as PropType<Item[]>, required:true }
  },

  setup(props) { // IDE knows the type of props
    const titles = computed( () => props.items.map(item => item.title));
    return {
      titles // IDE knows this type as well
    }
  }
})
</script>

Der Hauptvorteil ist, dass unsere IDE jetzt die richtigen Typen aller Variablen kennt. Wir zahlen einen kleinen Preis in Bezug auf die Build-Zeiten, da der gesamte Code mit tsc kompiliert und typgeprüft wird.

Wir hoffen, dass neuere Versionen von Gridsome zu etwas wie Vite oder Snowpack wechseln werden, die esbuild anstelle von tsc verwenden, um die Kompilierung zu beschleunigen. Unsere IDE macht sowieso schon die ganze Typüberprüfung, daher gibt es keinen Grund, das zweimal zu machen.

Installation

Wie konfiguriert man Gridsome also mit beiden Plugins genau?

Zuerst: Typescript-Support installieren

# Add npm dependency
$ yarn add -D gridsome-plugin-typescript

Plugin in gridsome.config.js aktivieren:

plugins: [
  { use: "gridsome-plugin-typescript" }
]

Deine tsconfig.json sollte jetzt so aussehen:

{
  "compilerOptions": {
    "target": "ES5",
    "module": "ES6",
    "strict": true,
    "moduleResolution": "node",
    "experimentalDecorators": false,
    "noImplicitReturns": true,
    "noImplicitAny": false,
    "outDir": "./built/",
    "allowJs": true,
    "esModuleInterop": true,
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": [
    "./src/**/*",
    "./gridsome.*.js"
  ],
  "exclude": [
    "content",
    "node_modules",
    "static",
    "src/.temp",
    "src/assets",
    "dist"
  ]
}

Zweitens: Composition-API installieren

# Add npm dependency:
$ yarn add -D gridsome-plugin-composition-api

Plugin in der gridsome.config.js aktivieren:

plugins: [
  { use: "gridsome-plugin-composition-api" }
]

Bekannte Einschäankungen

Hier ist eine Zusammenfassung der Probleme, auf die wir sowohl in VSCode als auch in IntelliJ gestoßen sind. Die Liste für IntelliJ ist größer, weil wir IntelliJ als unsere Haupt-IDE verwenden. Einige Probleme wurden bereits gemeldet, und wir erwarten, dass sie bald behoben werden.

Problem #1: Autovervollständigung für typisierte Props in Templates

In einer typischen Vue-Komponente kann man bereits die Typen von props so beschreiben:

<script>
export default {
  props: {
    title: { type: String, required: true } // basic type definition
  }
}
</script>

Was soweit in Ordnung ist. Wenn aber die Prop ein Object ist, dann kann man so die genauere Struktur weiter spezifizieren. Bei der Verwendung von Typescript (und der Composition-API) kann man das aber so tun: type: Object as PropType<TYPENAME>:

<template>
  <my-card :title="personName" :image="person.photo"/>
</template>

<script lang="ts">
import {defineComponent, PropType, computed} from "@vue/composition-api";
import {Person, formatName} from "@/models/Person";

export default defineComponent({
  props: {
    person: {type: Object as PropType<Person>, required: true} // better type definition for objects
  },

  setup(props) {
    const personName = computed(() => formatName(props.person.name));
    return {personName}
  }
});
</script>

Nun würde man erwarten, dass die Autovervollständigung der Variable person sowohl im Template (person.photo) als auch in der Funktion setup() (person.name) funktioniert. Bei Verwendung von VSCode funktionieren beide wie erwartet. In IntelliJ funktioniert die Autovervollständigung leider nur innerhalb der Setup-Funktion.

Problem #2: Auto-unwrapping von ComputedRefs in Templates

Das zweite Problem hängt mit Ref and ComputedRef, die von der setup() function zurückgegeben werden zusammen. In IntelliJ werden diese nicht richtig ge-‘unwrapped’. (Auch das funktioniert in VSCode).

In diesesm Vue/Typescript code, der eine Variable person vom Typ ComputedRef<Person> zurückgibt.

<script lang="ts">
import {defineComponent, computed, ComputedRef} from "@vue/composition-api";

interface Person {
  name: string
  age: number
}

export default defineComponent({
  setup() {
    const person: ComputedRef<Person> = computed(() => ({
      name: "John",
      age: 30
    }));
    
    return {person}
  }
})
</script>

Wenn wir jetzt person inneralb des Vue Templates benutzen, dann schlägt die Autovervollständigung in Intellij person.value.name vor (Richtig wäre aber person.name).

Solange das noch nicht gefixt ist, kann man diesen Workaround benutzen:

import {reactive, computed} from "@vue/composition-api";

export function fixedComputed<T>(getter: () => T): T {
  return reactive(computed(getter)) as T;
}

Problem #3: Autovervollständigung in verschachtelten Komponenten

Wie im folgenden Beispiel mit dynamischen Vue-Slots (v-slot="{item}") gezeigt, bietet IntelliJ keine Autovervollständigung für die Variable item an, obwohl articles korrekt typisiert ist. Das ist ein bekanntes Problem WEB-41084.

<my-list :items="articles" v-slot="{item}">
  <my-list-item
    :image="item.previewImage"
    :title="item.title"
    :timeToRead="item.timeToRead"
  />
</my-list>

Problem #5: Untypisierte GraphQL Queries

In Gridsome kann man auf Daten aus der GraphQL-Schicht zugreifen, indem man inlined graphql queries (geschrieben innerhalb spezieller Tags <page-query> und <static-query>) verwendet. Die Ergebnisse der Abfragen sind in den Vue Templates über die Variablen $page und $static verfügbar, die global auf der Vue Instanz bereitgestellt werden.

Das Problem ist, dass unsere IDE den Typ von $page nicht kennt und daher die Variable person nicht automatisch vervollständigen kann. Was noch schlimmer ist, wenn wir unsere Abfrage irgendwo in der Zukunft ändern und vergessen, das Template zu aktualisieren, würden wir die falsche Ausgabe erzeugen, ohne es zu merken. Dies ist tatsächlich eine sehr häufige Fehlerart, die uns in unseren Projekten begegnet.

Im folgenden Beispiel erzeugen wir eine Liste von Fotos für alle Personen. Die eingebaute Komponente <g-image> gibt ein optimiertes progressives Bild aus (siehe docs/images).

Man beachte die falsche Benutzung von person.image statt person.photo in diesem Schnipsel:

<template>
  <Layout>
    <g-image v-for="{node:person} in $page.allPerson.edges"
      :key="person.id"
      :src="person.image"
      :alt="person.title"
    />
  </Layout>
</template>

<page-query>
query {
  allPerson {
    edges {
      node {
        id
        name
        photo (quality:90 blur:6)
      }
    }
  }
}
</page-query>

Im Moment ist uns kein Gridsome-Plugin bekannt, das Typen für diese GraphQL-Abfragen generiert. Wir erwägen, eines zu schreiben, da alle Informationen über die Typen in dieser Gridsome-Datenschicht leicht verfügbar sind.

Problem #5: Untypisierte $page und $static Variablen

Wie bereits erwähnt, ist es derzeit nicht möglich, Typen für die globalen Variablen $page und $static anzugeben. Es gibt jedoch einen Workaround, der ein wenig zusätzlichen Boilerplate-Code erfordert. Wir haben zwei Hilfsfunktionen usePageQuery<QueryType>() und useStaticQuery<QueryType>() erstellt, die innerhalb der Funktion setup() verwendet werden können.

// src/lib/vue-utils.ts
import {computed, ComputedRef, getCurrentInstance} from "@vue/composition-api";

export function useStaticQuery<T>(): ComputedRef<T> {
  return computed(() => (getCurrentInstance() as unknown as { $static: T }).$static);
}

export function usePageQuery<T>(): ComputedRef<T> {
  return computed(() => (getCurrentInstance() as unknown as { $page: T }).$page);
}
<!-- src/templates/Article.vue -->
<template>
  <div>
    <h1>{{ article.title }}</h1>
    <div v-html="article.content" />
  </div>
</template>

<page-query>
query ($path: String!) {
  article (path: $path) {
    title
    content
  }
}
</page-query>

<script lang="ts">
import {computed, defineComponent} from "@vue/composition-api";
import {usePageQuery} from "@/lib/vue-utils";
import {Article} from "@/models/Article";

export default defineComponent({
  setup() {
    const $page = usePageQuery<{article:Article}>(); // query type specified here
    const article = computed(() => $page.value.article);
    return {article} // type is ComputedRef<Article>
  }
})
</script>

Problem #6: Require vs Import

Bei der Migration eines Gridsome-Projekts nach Typescript gibt es zwei Javascript-Dateien, die nicht konvertiert werden können: gridsome.server.js und gridsome.config.js. Beide Dateien verwenden die “require”-Syntax von CommonJS anstelle von Importen im ES6-Stil. Glücklicherweise gehören komplexe Funktionen ohnehin nicht dorthin oder können in separate Plugins umgewandelt werden.

Zusammenfassung

Mit dieser Anleitung solltest du nun in der Lage sein, Typescript in Ihren Gridsome-Projekten zu verwenden und eine bessere Entwicklererfahrung zu erhalten, als sie die Standardinstallation von Gridsome derzeit bietet.

Die einzige verbleibende große Aufgabe ist die Generierung von Typen aus inline GraphQL-Abfragen. Am besten wäre es, dies als neues Plugin oder als Teil des bestehenden Typescript-Plugins zu veröffentlichen. Für alle anderen in diesem Artikel genannten Probleme gibt es einen Workaround oder sie werden von den IDE-Plugin-Entwicklern behoben.

Wir hoffen, dass du die Informationen in diesem Artikel nützlich fandest. Solltest du Fragen oder Kommentare haben, kannst du uns gerne kontaktieren unter: info@sparkteams.de

Live Webinar: Softwareentwicklung skalieren, ohne sich im Wachstumsprozess aufzureiben

„Im Webinar wurden genau die Pain Points angesprochen, die in unserem Wachstum gerade auftreten. Bei stetig steigender Zahl an Mitarbeitern im Dev & Product Team mussten wir selbst feststellen, wie schwierig es ist, eine sinnvolle Struktur aufzubauen, die bei jedem das Maximum an Effizienz und Leistung hervorbringt und gleichzeitig zu einer gesunden Arbeitsatmosphäre führt.”
Pascal von Briel, Co-Founder & CPO @ Exporto GmbH