Prosta aplikacja React + Deno
Dziś krótki post dokumentujący, w jaki sposób uczę się środowiska Deno, języka JavaScript i języka TypeScript oraz biblioteki React: Uczę się nie tak, jak należy (po kolei), tylko robię wszystko na raz. Tak wygląda mini-aplikacja, ghm, ghm, a raczej małe ćwiczenie, którego celem jest użycie tych wszystkich cudowności razem.
Przetestuj tutaj!
Programik
Wiem, że Deno potrafi “zbundlować” jsx do pliku js, o czym dowiedziałam się podczas opisywania Deno we wpisie deno. Spróbuję - nie posiadając node.js na swoim systemie - zbudować jakąś prostą aplikację w Reakcie używając wyłącznie:
- Deno
- nvima
- przeglądarki
Konfiguracja nvima
Zacznę bardzo nieprofesjonalnie: chcę, żeby typescript i react (jsx) ładnie się kolorował w nvimie. Dlatego po szybkim researchu zaktualizowałam swój .vimrc:
- dodałam dwa pluginy do kolorowania składni do konfiguracji vundle:
|
|
- dodałam autocommands do ustawiania typu plików na podstawie roszerzenia, ponieważ domyślnie ani pliki .ts nie są rozpoznawane jako typscript, ani .tsx nie są rozpoznawane jako typescriptowe pliki reactowe:
|
|
Mój pomysł na programik to prosty edytorek do kolorowania orazka svg… na przykład dinozaura.
Dinozaur - iteracja 1
Moje próby narysowania dinozaura okazały się dość krzywe:
Znalazłam jednak piękną stronę svgsilh.com z grafikami svg opublikowanymi na licencji CreativeCommons0 (Public Domain), planuję więc wykorzystać obrazek dinozaura:
Najpierw jednak muszę sprawić, żeby wszystko ładnie “zagrało” - czyli z grubsza chcę, aby Deno zbundlował mi Reacta i moje typescriptowe moduły do jednego pliku .js, który będę mogła dodać do strony html.
Plik html
Zaczynam od pliku html:
- Wewnątrz
head
-a deklaruję użycie modułu app.bundle.js, który Deno zbuduje z wszystkich moich plików .tsx i potrzebnych modułów. - W
body
tworzę pustydiv
o idroot
- to będzie główny element dla Reacta - w tym elemencie DOM-a React wyrenderuje główny komponent. - W
body
znajduje się też prosty moduł, w którym importuję z app.bundle.js funkcję renderApp, a następnie wywołuję ją podając jako argument element o id root.
|
|
Główna funkcja renderująca
Funkcja renderApp
będzie zdefiniowana w pliku App.ts
, a jej zadaniem będzie wyrenderowanie komponentu App wewnątrz przekazanego elementu DOM.
|
|
Moje pierwsze typy w TypeScript
Zaczynam od bardzo prostych definicji: typ DenoConfig
zawiera dane potrzebne do narysowania obrazka svg. AppData
posiada dodatwoko atrybut title
. I tak oto powstają moje pierwsze w życiu typy w języku TypeScript :)
|
|
Komponent App
Komponent App będzie pozwalał na niezależną zmianę czterech właściwości obiektu Deno ujętych w typie DenoConfig: szerokości i wysokości obrazka SVG oraz koloru krawędzi i wypełnienia. Oto komponent App, który na razie tylko wypisuje przekazane wartości:
|
|
Problem: jak zaimportować Reacta
Pierwszy problem do rozwiązania brzmiał: jak zaimportować bibliotekę React do moich modułów? Po wielu różnych podejściach znalazłam stronę https://esm.sh (projekt na GitHubie), na której dowiedziałam się, że stanowi ona ContentDeliveryNetwork dla modułów javascriptowych.
Otóż możliwość użycia Reacta jako modułu została przedstawiona w pierwszym przykładzie na stronie:
|
|
Ponieważ chcę się odwoływać do konkretnej wersji biblioteki React oraz używać jej wariantu “deweloperskiego” a nie “produkcyjnego”, zaimportowałam React oraz ReactDOM w następujący sposób:
W pliku deps.ts
zaimportowałam obydwie nazwy, a następnie wyeksportowałam je po to, aby w innych modułach import Reacta mógł odbywać się z lokalnego modułu deps.ts
(to pozwoli mi na szybszą zmianę wersji oraz wariantu w przyszłości).
Oto mój deps.ts:
|
|
Makefile - Deno bundluje app.bundle.js
Teraz poproszę Deno, żeby zbundlował wszystko, co konieczne, do jednego pliku, który nazwała app.bundle.js
.
Spodziewam się, że zadzieje się jakaś magia, po czym do app.bundle.js
trafi React oraz funkcja renderApp
, którą eksportuję w app.jsx
i chcę zaimportować w skrypcie-module w index.html.
Podaję plik źródłowy (początkowy) od którego będą analizowane zależności oraz nazwę pliku docelowego, mojego “bundla”:
|
|
Napiszę prosty Makefile, dzięki któremu będę mogła:
- szybciej generować app.bundle.js zawsze, gdy zmienię coś w moich modułach (wpisując w wierszu poleceń
make
) - uruchomić lokalny serwer, który będzie hostował moją “stronkę” z programem
Wygląda on tak:
|
|
Rezultat
Pierwsze uruchomienie mojej aplikacji renderuje właściwości AppData w komponencie App: title oraz właściwości obiektu deno: width, height, fill oraz stroke:
To pierwszy, ważny “milestone” w moim prototypie. Mam dowód na to, że prototyp działa i robi dokładnie to, czego się spodziewałam. Teraz mogę przejść do dodawania do mojego projektu interaktywności.
Dinozaur - iteracja 2
Spróbuję teraz nie tylko wyświetlać właściwości obiektu DenoConfig, ale użyć ich jako stanu komponentu App. Każda z właściwości będzie później opakowana komponentem Reactowym odpowiedzialnym za aktualizację jednego aspektu obiektu DenoConfig, jego jednej właściwości. W przypadku wysokości i szerokości będzie to właściwość typu number
, a w przypadku kolorów - string
.
Typ Prop - właściwość DenoConfig
Wymyśliłam więc, że utworzę typ generyczny Prop (property, właściwość obiektu). Będzie on reprezentował model widoku dla pojedynczej właściwości obiektu DenoConfig.
Typ Prop:
- każda właściwość DenoConfig posiada nazwę - przechowam ją w polu
name
- właściwość posiada wartość typu ValueType (string lub number) - w polu
val
- widok będzie reprezentowany w html-u przez element
<input ...\>
, którego atrybuttype
przechowuję w poluinputType
onPropUpdated
to callback do zmiany stanu (ds
) obiektuDenoConfig
w komponencieApp
tak, aby zmieniona została jedynie edytowana wartość- opcjonalnym polem będzie
range
- ta wartość ma sens jedynie dla wartiantu Proplecz nie dla Prop (na razie zostawiam opcjonalność; myślę, że da się jakoś zdefiniować typ range jako zależny od ValueType: T == 'number' ? Range : never
…)
|
|
Stan komponentu App - state hook
Komponent App będzie komponentem stanowym. Jego stanem będzie obiekt DenoConfig z przekazanego property AppData. Używam idiomu “state hook” - czyli funkcji setState
, otrzymując listę [property, callback_do_zmiany_property].
Wyobrażam sobie, że komponent App będzie się składał z podkomponentów - edytorów, z których każdy będzie potrafił wyrenderować jedną właściwość obiektu DenoConfig.
|
|
Komponent InputElem - edytor dla typu Prop
Czas zdefiniować nowy komponent - edytor dla właściwości Prop. Wyświetli on mały formularz: label z nazwą i pole edycyjne, oraz wartość bieżącą właściwości. Jeśli pole inputType
w obiekcie Prop będzie typu number
, odczyta również pole range
i ustawi atrybuty min, max i step w elemencie <input type='range' ...>
.
Komponent ten:
- jest generyczny - w tym sensie, że przyjmuje jako
prop
obiekt typu Prop - jego stan jest inicjowany na podstawie pola
val
w obiekcie Prop - posiada funkcję
update
, którą wywołuję w reakcji na zdarzenie onChange
|
|
Niestety, nie poradziłam sobie z definicją typu zdarzenia onChange w elemencie <input .../>
; gdy deklarowałam e jako React.ChangeEventHandler<HTMLInputElement>
, wówczas otrzymywałam komunikat : 2339: Property 'target' does not exist on type 'ChangeEventHandler<HTMLInputElement>'.
Ostatecznie poddałam się i zostałam z e: any.
Użycie komponentu InputElem
Teraz użyję komponentu InputElem w komponencie App czterokrotnie wyświetlając cztery właściwości z obiektu AppData.deno. Tak naprawdę do komponentów InputElem, które są komponentami-dziećmi - muszę przekazać stan (ds
) otrzymany z hooka useState
, a jako callback przekazuję fukcję, która wywoła funkcję zmiany stanu (setDs
) otrzymaną również z hooka useState
.
Jak teraz wygląda mój komponent App?
Oto on:
|
|
Teraz wystarczy kolejny raz zawołać:
|
|
i w rezultacie otrzymuję reagujący na zmiany edytor.
Narysuj dinozaura!
A co z tym dinozaurem?
Dinozaur przypomina trochę prostokąt…
Zaczynam od narysowania prostokąta w SVG - rysowany prostokąt powinien być komponentem Reactowym i reagować na zmiany właściwości.Tworzę więc plik rect.tsx, importuję do niego Reacta i zmieniam wartości width, height, fill i stroke tak, aby nie były literałami, lecz wartościami przekazanymi do komponentu.
W międzyczasie mały refaktor: przerzucam m.in. typ DenoConfig
do pliku types.ts
, więc w rect.rs muszę ten typ zaimportować:
Oto rect.tsx:
|
|
I jeszcze zmieniam app.tsx
- zamiast wypisywać wszystkie właściwości, używam nowego komponentu Rect:
|
|
I już:
Komponent Deno
Komponent do rysowania dinozaura jest dość podobny do komponentu prostokąta, szybko wklejam svg i zmieniam w miejscach, w których jest odwołanie do edytowanych właściwości:
|
|
Oryginalny svg miał ustawiony atrybut preserveAspectRatio
(sensownie zresztą), dlatego manipulowanie wysokością i szerokością jest trochę mało intuicyjne - dinozaur próbuje się “zmieścić” wewnątrz prostokąta o wyznaczonych wymiarach (stylizuję więc komponent odrobinę - dodaję styl border
).
Zielony dinozaur
Można sobie teraz pokolorować dinozaura na zielono :)
Podsumowanie
Po pierwsze: udało mi się napisać mały program w TypeScript bez instalowania Webpacka, Babela, TypeScripta, nodejsa i-co-tam-jeszcze-byłoby-potrzebne (Alleluja!) i bez zaśmiecania katalgu projektu modułami.
Po drugie: użycie jednej binarki do zbundlowania kodu, formatowania źródeł czy uruchamiania testów jest po prostu niezwykle estetyczne. Lubię taki minimalizm. Nie muszę nic dodatkowo instalować, nie muszę uczyć się nowych narzędzi, zaśmiecać systemu pakietami, których przeznaczenia mogę się jedynie domyślać.
Mogę skupić się na moim głównym celu - którym jest uczenie się TypeScripta/JavaScripta. Mam skrzynkę z narzędziami i mogę zacząć ich używać. Nie muszę najpierw budować warsztatu (analogia trochę nie na miejscu, bo w dziedzinie programowania “warsztat” akurat zawsze się przydaje).
Jestem zdecydowanie pozytywnie zmotywowana, żeby bawić się TypeScriptem dalej. Oczywiście, w środowisku Deno :)
Może Cię również zainteresować…
Jeśli zastanawiasz się, czym jest Deno, zapraszam Cię do przeczytania wpisu: Deno - czym jest i co potrafi
Źródła
Źródła tego projekciku znajdują się w repozytorium deno-react-example na GitHubie.