Heute zeige ich wie man ein Storybook Addon schreibt. Jedes Projekt ist individuell und einzigartig sind auch die Anforderungen. Storybook bietet einen breiten Katalog an Addons an. Ein tolles Feature, das das Entwicklungsteam anbietet, ist die Möglichkeit weitere Addons selber zu schreiben. Dabei kann man dann selber entscheiden, ob man es in den Katalog aufnehmen lassen möchte, oder ob man es einfach nur in sein Projekt integriert. Aber wie erstellt man nun ein Addon? Dazu gibt es eine Anleitung und auch eine API. Es gibt zum Starten zwei Möglichkeiten. Man kann auf einer grünen Wiese starten oder man klont das Starterprojekt und ergänzt dieses. Wir machen heute ersteres.
Analog zu den vorherigen Artikeln legen wir einen Ordner an, initalisieren ein NPM-Projekt und beginnen dann die Storybook Addons dependencies hinzuzufügen.
mkdir storybook-addon-hello-world
cd storybook-addon-hello-world
npm init
Damit steht das Gerüst. Unser Addon wird in React geschrieben und mit Hilfe von Babel transpiliert. Als nächstes fügen wir Babel zum Projekt hinzu.
npm install -D @babel/cli @babel/preset-env @babel/preset-react
Anschließend legen wir eine Babel-Konfigurationsdatei (.babelrc.js_) an und füllen diese mit Leben.
touch .babelrc.js
module.exports = {
presets: ["@babel/preset-env", "@babel/preset-react"],
};
In Zeile 2 sehen wir nun, dass die zuvor installierten Presets zum Zug kommen. Ein Preset ist eine kleine Sammlung von Plugins, um eine Sprache zu unterstützen. In unserem Fall ES6+.
Jetzt tragen wir den Babel-Befehl, das Babel-Skript, in unsere package.json ein.
...
scripts:{
"build": "babel ./src --out-dir ./dist --ignore '**/*.spec.jsx'",
},
....
In Zeile 3 sehen wir den Befehl. Der erste Parameter ist das Quellverzeichnis /src. Es folgt das Zielverzeichnis, /dist. Daneben gesellt sich eine Ignorierliste: die Tests. Diese müssen nicht transpiliert werden. Um es zu testen benötigen wir noch etwas Code und das Quellverzeichnis.
mkdir src
cd src
touch presets.js
touch register.js
Jetzt existiert das Quellverzeichnis und wir haben die Einstiegsdatei presets.js. Diese Datei sucht Storybook für den Einstieg. In dieser registrieren wir unser Plugin. In der register.js-Datei beginnt React.
// presets.js
function managerEntries(entry = []) {
return [...entry, require.resolve("./register")];
}
module.exports = { managerEntries };
Jetzt können wir mal den build-Befehl ausführen.
npm run build
Es entsteht ein Ordner /dist in dem sich die Datei presets.js und hat nun etwas mehr Inhalt.
"use strict";
function _toConsumableArray(arr) {
return (
_arrayWithoutHoles(arr) ||
_iterableToArray(arr) ||
_unsupportedIterableToArray(arr) ||
_nonIterableSpread()
);
}
function _nonIterableSpread() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.",
);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
return _arrayLikeToArray(o, minLen);
}
function _iterableToArray(iter) {
if (
(typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) ||
iter["@@iterator"] != null
)
return Array.from(iter);
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
function managerEntries() {
var entry =
arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return [].concat(_toConsumableArray(entry), [
require.resolve("./register"),
]);
}
module.exports = {
managerEntries: managerEntries,
};
Der Inhalt ist transpiliertes und im Browser lauffähiges Javascript. Damit funktioniert unser Setup. Ich schlage noch eine kleine Erweiterung vor: einen Watcher. Im aktuellen Setup muss man nach jeder Änderung den Befehl ausführen, um zu transpilieren. Es gibt ein npm-Modul, das eine Änderung am Sourcecode erkennt und den Build-Befehl ausführt.
npm install -D npm-watch
In die package.json fügen wir noch die Watch-Konfiguration.
// package.json
"watch": {
"build": {
"patterns": [
"src"
],
"extensions": [
"js,jsx"
]
}
},
Unter watch kommt der Befehl, der nach einer Änderung ausgeführt werden soll. Es ist der bekannte, und oben genannte Build-Befehl. Um das Watching zu starten, benötigen wir einen weiteren Skritpbefehl.
"dev": "npm-watch build",
Ich hab mir angewöhnt meine Arbeitstasks immer "dev" zu nennen. Mit npm run dev starte ich den Watcher, der beobachtet die Dateien unter patterns mit der Dateiendung (extensions) .js, .jsx.
Das erhöht ungemein die Arbeitsgeschwindigkeit und den Komfort.
Jetzt geht es an die Fachlichkeit. Wie bereits erwähnt ist die register.js die Einstiegsdatei für das UI.
// register.js
import React from "react";
import { addons, types } from "@storybook/addons";
import { AddonPanel } from "@storybook/components";
const ADDON_ID = "myaddon";
const PANEL_ID = `${ADDON_ID}/panel`;
addons.register(ADDON_ID, (api) => {
addons.add(PANEL_ID, {
type: types.PANEL,
title: "My Hello World",
render: ({ active, key }) => (
<AddonPanel active={active} key={key}>
<div> hello world </div>
</AddonPanel>
),
});
});
Die ersten vier Zeilen beinhalten die imports der Kernbibliothek React und die Elemente von Storybook. Das Addon benötigt eine ID um sich aufzubauen. In dem Callback der register-Funktion hat man nun die Möglichkeit drei Hauptelemente zu erweitern:
In unserem Fall erweitern wir das Panel. In den Zeilen 14-16 sehen wir die Storybook-Panelkomponente. Im Bauch dieser Komponente beginnt unsere grüne Wiese. Dort platzieren wir das DIV mit Hello-World.
Wir haben jetzt eine erste lauffähige Version. Doch wie bekommen wir diese nun zu sehen. Ich habe dafür mein bestehendes Storybook-Projekt genutzt. Aber jedes andere bestehende Storybook-Projekt ist dafür geeignet.
Um das Addon nicht auf NPM zu packen und es erstmal lokal zu testen, gibt es einen Trick: npm link.
Man linkt das Addon-Projekt mit dem bestehenden Projekt. Dafür muss man jeweils im Addonprojekt und im "Zielprojekt" einen npm Befehl ausführen.
//addonverzeichnis
(sudo) npm link
// im Zielverzeichnis
(sudo) npm link [NAME IN DER PACKAGE.JSON]
Ich habe sudo in Klammern geschrieben, weil es auf meiner Kiste nicht funktioniert. Auf anderen Rechnern soll es auch ohne funktionieren. Einfach mal ausprobieren.
Jetzt muss das Addon nur noch im Zielprojekt in der Storybook-Konfiguration eingetragen werden.
// .storybook/main.js
module.exports = {
stories: [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
// einfach unter die eingetragenen Addons hinzufügen
"NAME-DES_ADDONS/dist",
],
};
Wenn man nun das Zielprojekt startet, sollte man in jeder Story ein drittes Paneltab sehen, in dem Hello World steht. Durch das Watchskript sieht man nun auch jede Änderung am Addonquellcode direkt beim Neuladen der Seite. Ein Traum zum Entwickeln.
Jetzt steht euren Ideen nichts mehr im Weg.
Um das Plugin in den Storybook Addon Katalog zu bekommen, muss man noch zwei Dinge tun:
Damit Storybook euer Addon findet müsst erstmal in der package.json die Felder name, description, author, keywords, repository ausfüllen.
// Beispiel aus meinem Addon
{
"name": "storybook-addon-custom-event-broadcaster",
"description": "storybook addon for broadcasting custom events",
"main": "dist/preset.js",
"keywords": ["storybook-addons", "custom-events", "code", "debug"],
"author": "Jacob Pawlik <jacob@derkuba.de> (http://derkuba.de)",
"repository": {
"type": "git",
"url": "git+https://github.com/derKuba/storybook-custom-event-broadcaster.git"
},
...
}
Für die richtige Anzeige im Katalog werden noch die Attribute displayName, icon, unsupportedFrameworks, supportedFrameworks zur Verfügung gestellt. Diese sind aber Optional.
// Beispiel aus meinem Addon
{
"storybook": {
"displayName": "Custom Events Broadcaster",
"supportedFrameworks": ["react", "angular", "stenciljs"],
"icon": ""
}
}
Um ein package (so heißen dort die Pakete - es handelt sich aber um euer Addon) bei npmjs abzulegen, benötigt man ein Konto dort. Man registriert sich und (WICHTIG) bestätigt die E-Mailadresse. Wenn man dies nicht tut bekommt man eine kryptische Fehlermeldung.
npm ERR! code E403
npm ERR! 403 403 Forbidden - GET
npm ERR! 403 In most cases, you or one of your dependencies are requesting
npm ERR! 403 a package version that is forbidden by your security policy.
Nein, ihr habt dann keine verbotenen Pakete, sondern eure E-Mailadresse wurde nicht bestätigt ( dankt mir später :-) ).
Als nächstes müsst ihr euch per npm einloggen. Auch hier gibts 2 Möglichkeiten.
registry=https://registry.npmjs.com/
_auth="<token>"
email=<email>
always-auth=true
Den Token könnt ihr in eurem Benutzerprofil auf der Webseite erstellen.
Wenn das alles geklappt hat, muss man es noch veröffentlichen.
// für die vorsichtigen
npm publish -- dry run
Das simuliert eine Veröffentlichung und man bekommt Feedback ob alles passt.
npm publish --access public
Veröffentlicht euer package. Ihr findet das nun unter eurem Benutzerkonto unter packages. Manchmal benötigt es einige Zeit, bis ihr über die NPM-Suche fündig werdet. Ein bisschen Geduld ist gefragt. Um sicher zu gehen gibt es die Möglichkeit zu versuchen das Paket anzuzeigen oder zu installieren.
npm install packageName
npm show packagename
Das wars. Bei mir hat es gut 30 Stunden gedauert und mein Plugin war im Katalog zu finden.
Ich hoffe, dass dieser Artikel helfen kann weitere tolle Addons für Storybook zu schreiben und Storybook helfen noch besser zu werden. Es ist sehr einfach und schafft einen Spielplatz für viele Möglichkeiten und Ideen.
Schaut gerne mal in unser Addon rein und gebt mir gerne Feedback.
https://storybook.js.org/addons/storybook-addon-custom-event-broadcaster/
https://www.npmjs.com/package/storybook-addon-custom-event-broadcaster
Der Code hierzu liegt auf Github.
Ihr habt Fragen oder Anregungen? Schreibt mir bei Twitter oder per E-Mail .
Tausend Dank fürs Lesen!
Kuba